ES /docs

[400] {"error":{"root_cause":[{"type":"illegal_argument_exception","reason":"mapper [id] cannot be c

RCA: RevisionRequest ES append_mapping! — mapper [id] cannot be changed from type [long] to [integer]

Overview#

What Happened#

2026-07-15 배포 시 migration-worker 인스턴스에서 ES migration 20260713000000_add_id_to_revision_request.rb 가 실행되었다. Migration 은 RevisionRequest 인덱스에 id: integer 매핑을 명시적으로 추가하려 했으나, US 리전의 인덱스에는 이미 dynamic 매핑으로 id: long 이 존재하여 Elasticsearch 가 illegal_argument_exception (mapper [id] cannot be changed from type [long] to [integer]) 을 반환했다. 이 예외는 Searchable.append_mapping 의 상위 rescue StandardError 로 흡수되어 마이그레이션 자체는 성공 처리되었지만, 하위 append_mapping!Cupix::Logger.error 로 로깅한 후 재-raise 하기 때문에 Datadog 에는 error-level 로그가 남았다.

Quick Facts#

Field Value
exception.class Elasticsearch::Transport::Transport::Errors::BadRequest
exception.message [400] {"error":{"root_cause":[{"type":"illegal_argument_exception","reason":"mapper [id] cannot be changed from type [long] to [integer]"}], ...}
top_frame app/models/concerns/searchable.rb:386 (append_mapping! rescue)
triggered_by es/migrate/20260713000000_add_id_to_revision_request.rb
deploy TSLA-13581 (merged 2026-07-13, PR 88824)
env production, us-west-2

Affected Teams#

Team / Domain Error Count Impact
Search / RevisionRequest ES 매핑 2 (Datadog 15:09:48, 15:46:25 KST) 사용자 영향 없음 — migration 은 no-op 처리, 기존 dynamic long 매핑으로 조회/정렬 계속 동작

Timeline#

  1. 2026-07-13 11:26 KST — TSLA-13581 (add id mapping to RevisionRequest) 이 master 에 머지됨 (commit f84b559e7).
  2. 2026-07-15 15:09:48 KSTus-west-2 migration-worker 가 es:migrate 실행 중 신규 마이그레이션의 append_mapping!('id') 호출에서 ES 400 응답을 수신. Datadog 에 error-level 로그.
  3. 2026-07-15 15:46:25 KST — 두 번째 migration-worker 인스턴스에서 동일한 error-level 로그 재발생.
  4. 2026-07-15 15:47 KST 이후 — 신규 로그 없음. ElasticsearchMigration 레코드가 기록되어 이후 배포에서는 재실행되지 않음.

Error Log#

Datadog Logs

text
[400] {"error":{"root_cause":[{"type":"illegal_argument_exception","reason":"mapper [id] cannot be changed from type [long] to [integer]"}],"type":"illegal_argument_exception","reason":"mapper [id] cannot be changed from type [long] to [integer]"},"status":400}

Impact#

  • Service: cupixworks-migration-worker
  • 발생 횟수: 1 (클러스터 프론트매터 기준); 실제 Datadog 상에는 동일 fingerprint 2건
  • 최초 발생: 2026-07-15 15:09 KST
  • 최근 발생: 2026-07-15 15:09 KST (Datadog 상 동일 fingerprint 재발생: 2026-07-15 15:46 KST)

Root Cause Summary#

TSLA-13581 은 CA(ca-central-1) 리전에서 RevisionRequest ES 인덱스에 문서가 0건이라 dynamic 매핑이 생성되지 않아 admin 조회 정렬이 실패하던 문제를 고치기 위해 Searchable::RevisionRequestindexes 'id', type: 'integer' 를 명시하고, 배포 시 append_mapping('id') 를 호출하는 ES migration 을 함께 넣었다. CA 리전에서는 매핑이 새로 생성되어 정상 동작하지만, US(us-west-2) 리전에는 이미 dynamic 매핑으로 id: long 이 존재해 ES 가 타입 변경을 거부한다(illegal_argument_exception). Migration 파일은 non-bang append_mapping 을 호출해 상위에서 rescue StandardError 로 이 상황을 no-op 처리하도록 설계되었으나, 하위 append_mapping! 이 rescue 절에서 Cupix::Logger.error(...) 로 로그를 먼저 남기고 예외를 재-raise 하기 때문에 "설계상 예상된 상황"이 error-level 로 관측되었다.

Technical Analysis#

Code Path#

Entry point (배포 postdeploy hook):

.platform/hooks/postdeploy/50_init_tesla.sh:11bash
su "$EB_APP_USER" -s /bin/bash -c "bundle exec rake es:migrate"

Rake task 는 ElasticsearchMigrationRunner#migrate 를 실행하고, 미실행 마이그레이션 파일들을 순차적으로 로드한다:

app/services/elasticsearch_migration_runner.rb:23-28ruby
def migrate
  ensure_migrations_table_exists
  pending_migrations.each do |migration_file|
    execute_migration_with_retry(migration_file)
  end
end

문제의 migration 파일:

es/migrate/20260713000000_add_id_to_revision_request.rbruby
class AddIdToRevisionRequestEs < ElasticsearchMigration::Base
  def up
    # Explicit `id` mapping was missing; dynamic mapping is not generated when
    # an index has zero documents, which caused CA-region admin queries to fail
    # with "No mapping found for [id] in order to sort on".
    # Regions with existing documents already have a dynamic `long` mapping,
    # so a plain append_mapping would conflict. Recreate the index via
    # zero-downtime reindex to align every region on the explicit `integer`
    # mapping.
    #   rake es:zero_downtime_reindex[RevisionRequest]
    # See: docs/zero_downtime_reindexing_guide.md
    ::RevisionRequest.append_mapping('id')
  end
end

RevisionRequest.append_mappingSearchable concern 의 클래스 메서드로, 실제 ES 호출은 bang 버전이 수행한다:

app/models/concerns/searchable.rb:361-389ruby
def append_mapping(field)
  append_mapping!(field)
rescue StandardError => e
  false
else
  true
end

def append_mapping!(field)
  raise Cupix::Errors::Argument.new(code: 'ARG10000', reason: 'field is required') if field.blank?

  new_indexes = self.__elasticsearch__.mappings.to_hash[:properties].slice(field.to_s)

  raise Cupix::Errors::Argument.new(code: 'ARG15000', reason: "#{field} is not defined in Searchable") if new_indexes.blank?

  _mappings = self.__elasticsearch__.client.indices.get_mapping(index: self.__elasticsearch__.index_name)
  _mappings = _mappings.dig(self.__elasticsearch__.index_name, 'mappings')
  _mappings['properties'].merge!(new_indexes)
  request = {
    index: __elasticsearch__.index_name,
    body: _mappings
  }

  self.__elasticsearch__.client.indices.put_mapping(request)
rescue Elasticsearch::Transport::Transport::Errors::BadRequest => e
  Cupix::Logger.error(e.message.to_s, class: self.name, function: __method__)

  raise e
end

Failure point: searchable.rb:384 (put_mapping) — ES 가 400 응답. Rescue 절(:385-388)이 error-level 로 로깅한 뒤 예외를 재-raise. 상위 append_mapping (:361-367) 이 StandardError 로 흡수해 false 반환. Migration .up 은 예외 없이 종료 → record_migration 이 실행되어 elasticsearch_migrations 테이블에 기록.

명시적 매핑 선언 (변경 전에는 없었던 id 라인):

app/models/concerns/searchable/revision_request.rb:34-57ruby
mappings dynamic: 'true' do
  indexes 'state', type: 'keyword'
  indexes 'approval_type', type: 'keyword'
  # ...
end

TSLA-13581 은 state 앞에 indexes 'id', type: 'integer' 를 추가해 다른 76개 Searchable concern 과 통일. US/EU 는 dynamic 매핑으로 이미 id: long 이 잡혀있어 재선언 시 충돌.

기대 동작 vs 실제 동작:

  • 기대: append_mapping non-bang 이 rescue 하여 no-op — migration 은 silent 하게 실행 완료.
  • 실제: rescue 는 정상 동작하지만 append_mapping! 내부 rescue 가 먼저 error-level 로그를 남긴다. Datadog 관찰 관점에서는 배포 알림/on-call 페이지를 트리거할 수 있다.

Log Evidence#

Datadog query (14일 retention 내):

text
service:cupixworks-migration-worker "mapper [id] cannot be changed"

두 건의 로그 항목 (Datadog UI 는 KST 표시):

json
{
  "timestamp": "2026-07-15 15:46:25",
  "status": "error",
  "message": "[400] {\"error\":{\"root_cause\":[{\"type\":\"illegal_argument_exception\",\"reason\":\"mapper [id] cannot be changed from type [long] to [integer]\"}],\"type\":\"illegal_argument_exception\",\"reason\":\"mapper [id] cannot be changed from type [long] to [integer]\"},\"status\":400}",
  "class": "RevisionRequest",
  "function": "append_mapping!"
}
json
{
  "timestamp": "2026-07-15 15:09:48",
  "status": "error",
  "message": "[400] {\"error\":{\"root_cause\":[{\"type\":\"illegal_argument_exception\",\"reason\":\"mapper [id] cannot be changed from type [long] to [integer]\"}],\"type\":\"illegal_argument_exception\",\"reason\":\"mapper [id] cannot be changed from type [long] to [integer]\"},\"status\":400}",
  "class": "RevisionRequest",
  "function": "append_mapping!"
}

관측된 사실:

  • 로그의 @class:RevisionRequest, @function:append_mapping! 태그는 Cupix::Logger.error(e.message.to_s, class: self.name, function: __method__) 호출과 정확히 일치.
  • 15:09 와 15:46 두 건만 존재. 이후 재발 없음 — ElasticsearchMigration 테이블에 version 이 기록되면 다음 실행에서 skip 되기 때문.
  • 두 건이 서로 다른 인스턴스에서 나온 것으로 보이는데(Auto Scaling 순차 배포 또는 동일 인스턴스에서의 재시도), 이 부분은 확인 필요 — 원본 로그에는 host 태그 없음 (uncertain -- needs verification).

관련 배포/변경 이력:

text
d5c980a40 TSLA-13581 [TSLA-13581] Fix admin revision_requests 500 on empty ES index
d742fa4c2 TSLA-13581 [TSLA-13581] Add ES migration for RevisionRequest id mapping
f84b559e7 Merged PR 88824: TSLA-13581 admin revision_requests 500 fix — ES id 매핑 명시  (2026-07-13)

PR 설명(발췌):

text
- CA (0 docs): append_mapping 성공 → 정렬 정상화
- US/EU (dynamic long 이미 존재): append_mapping 이 rescue 로 no-op. 서비스는 dynamic long 으로 계속 정상 동작
- US/EU 를 명시 integer 로 통일하려면 후속: rake es:zero_downtime_reindex[RevisionRequest] (필수 아님)

이는 error 로그가 의도된 no-op 상황 임을 명시적으로 확인해 준다.

Hypotheses Considered#

# Hypothesis Evidence for Evidence against Verdict
H1 TSLA-13581 신규 ES migration 이 US 리전에서 기존 dynamic long 매핑과 충돌하여 append_mapping! 내부 rescue 가 error 로그를 남긴다 es/migrate/20260713000000_add_id_to_revision_request.rb 존재, Searchable::RevisionRequestid: integer 명시 추가, searchable.rb:385-388 의 error 로그 코드, Datadog 태그 @class:RevisionRequest @function:append_mapping! 완전 일치, PR 설명에 US/EU 는 no-op 이라고 명시 Confirmed
H2 애플리케이션 트래픽 요청이 실시간으로 append_mapping! 을 호출했다 append_mapping! 호출자는 ES migration 뿐. Grep 결과 다른 호출부 없음. 로그 시각(deploy 시간대)과도 불일치 Rejected
H3 Elasticsearch 클러스터 자체 장애(mapping storage 손상 등) 다른 인덱스/모델의 mapping 관련 error 없음. 에러 메시지가 명확한 타입 충돌(long → integer) Rejected
H4 상위 append_mapping rescue 가 동작하지 않아 migration 이 실패 처리됨 Ruby 의 rescue 는 순차 언와인딩 — 하위 rescue 가 raise 하면 상위 rescue 가 잡는다. 이후 재발이 없다는 사실이 migration 이 record 되었음을 뒷받침(있었다면 재시도로 계속 재발생) Rejected

Fix Recommendation#

즉시 조치 (Critical)#

없음. 사용자 영향이 없고 이미 완료된 배포로 인해 재발되지 않는다.

단기 개선 (1주 이내)#

  1. app/models/concerns/searchable.rb:385-388 의 로그 레벨을 재검토.
    • "append_mapping 이 상위에서 rescue 되어 no-op 로 처리되는 시나리오"가 정상 흐름의 일부라면, 이 경로에서만 warn 레벨로 낮추는 것을 고려. 다만 append_mapping! 을 직접 호출하는 다른 코드 경로(있다면)에서는 여전히 error 로 남아야 하므로, 로그 레벨을 무조건 낮추면 안 됨.
    • 대안: 로그 레벨은 유지하되, 상위 append_mapping non-bang 이 이미 rescue 하고 있다는 사실을 감안해 하위에서는 raise 만 하고 로깅은 상위 호출자가 결정하도록 책임 분리.
  2. TSLA-13581 후속 작업으로 언급된 rake es:zero_downtime_reindex[RevisionRequest] 를 US/EU 리전에서 실행해 매핑을 integer 로 통일. 완료 후 이 클러스터의 재발 위험이 완전히 사라진다.

장기 개선 (재발 방지)#

  1. ES migration DSL 에 "expected no-op" 을 표시할 수 있는 API 를 추가. 예: append_mapping('id', if_mismatch: :warn_and_skip) — 상위/하위 rescue 가 각자 로그 레벨을 명시적으로 결정.
  2. Searchable concern 을 새로 만들거나 필드를 추가할 때는 id 를 항상 명시(76개 이미 그렇다)하도록 lint/spec 규칙 추가. TSLA-13581 이 사후 대응이었음.
  3. 배포 후 첫 30분 동안의 cupixworks-migration-worker error 로그는 별도 대시보드/monitor 로 분리해 "배포로 인한 예상된 error 로그" 와 실제 문제를 구분.

Monitoring#

배포 시점에 발생하는 이 로그 자체는 정상 흐름의 일부이므로, 알림은 다음 조건에서만 트리거되도록 구성:

Datadog widget query (timeseries):

text
sum:trace.rack.request.errors{service:cupixworks-migration-worker,resource_name:*append_mapping*}.as_count()

로그 기반 카운트 (배포 시간 외 발생 감지용):

text
service:cupixworks-migration-worker status:error @class:RevisionRequest @function:append_mapping!

추가 지표:

text
avg:elasticsearch.indices.mappings.total_field_count{cluster_name:*,index_name:revision_requests}

인덱스별 매핑 필드 수 변화를 추적하면 zero_downtime_reindex 이후 매핑이 실제로 재구성되었는지 확인 가능.

Risk Assessment#

  • Risk level: low
  • 예상 복잡도: trivial (즉시 조치 없음, 후속 개선은 standard)
  • 사용자 영향: 없음 (dynamic long 매핑으로 정렬/조회 계속 정상 동작)
  • 재발 가능성: 배포마다 최초 1회만 발생, 이후 elasticsearch_migrations 레코드로 skip