[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#
- 2026-07-13 11:26 KST — TSLA-13581 (
add id mapping to RevisionRequest) 이 master 에 머지됨 (commitf84b559e7). - 2026-07-15 15:09:48 KST —
us-west-2migration-worker 가es:migrate실행 중 신규 마이그레이션의append_mapping!('id')호출에서 ES 400 응답을 수신. Datadog 에 error-level 로그. - 2026-07-15 15:46:25 KST — 두 번째 migration-worker 인스턴스에서 동일한 error-level 로그 재발생.
- 2026-07-15 15:47 KST 이후 — 신규 로그 없음.
ElasticsearchMigration레코드가 기록되어 이후 배포에서는 재실행되지 않음.
Error Log#
[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::RevisionRequest 에 indexes '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):
su "$EB_APP_USER" -s /bin/bash -c "bundle exec rake es:migrate"
Rake task 는 ElasticsearchMigrationRunner#migrate 를 실행하고, 미실행 마이그레이션 파일들을 순차적으로 로드한다:
def migrate
ensure_migrations_table_exists
pending_migrations.each do |migration_file|
execute_migration_with_retry(migration_file)
end
end
문제의 migration 파일:
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_mapping 은 Searchable concern 의 클래스 메서드로, 실제 ES 호출은 bang 버전이 수행한다:
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 라인):
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_mappingnon-bang 이 rescue 하여 no-op — migration 은 silent 하게 실행 완료. - 실제: rescue 는 정상 동작하지만
append_mapping!내부 rescue 가 먼저 error-level 로그를 남긴다. Datadog 관찰 관점에서는 배포 알림/on-call 페이지를 트리거할 수 있다.
Log Evidence#
Datadog query (14일 retention 내):
service:cupixworks-migration-worker "mapper [id] cannot be changed"
두 건의 로그 항목 (Datadog UI 는 KST 표시):
{
"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!"
}
{
"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).
관련 배포/변경 이력:
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 설명(발췌):
- 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::RevisionRequest 에 id: 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주 이내)#
app/models/concerns/searchable.rb:385-388의 로그 레벨을 재검토.- "append_mapping 이 상위에서 rescue 되어 no-op 로 처리되는 시나리오"가 정상 흐름의 일부라면, 이 경로에서만
warn레벨로 낮추는 것을 고려. 다만append_mapping!을 직접 호출하는 다른 코드 경로(있다면)에서는 여전히 error 로 남아야 하므로, 로그 레벨을 무조건 낮추면 안 됨. - 대안: 로그 레벨은 유지하되, 상위
append_mappingnon-bang 이 이미 rescue 하고 있다는 사실을 감안해 하위에서는raise만 하고 로깅은 상위 호출자가 결정하도록 책임 분리.
- "append_mapping 이 상위에서 rescue 되어 no-op 로 처리되는 시나리오"가 정상 흐름의 일부라면, 이 경로에서만
- TSLA-13581 후속 작업으로 언급된
rake es:zero_downtime_reindex[RevisionRequest]를 US/EU 리전에서 실행해 매핑을integer로 통일. 완료 후 이 클러스터의 재발 위험이 완전히 사라진다.
장기 개선 (재발 방지)#
- ES migration DSL 에 "expected no-op" 을 표시할 수 있는 API 를 추가. 예:
append_mapping('id', if_mismatch: :warn_and_skip)— 상위/하위 rescue 가 각자 로그 레벨을 명시적으로 결정. Searchableconcern 을 새로 만들거나 필드를 추가할 때는id를 항상 명시(76개 이미 그렇다)하도록 lint/spec 규칙 추가. TSLA-13581 이 사후 대응이었음.- 배포 후 첫 30분 동안의
cupixworks-migration-workererror 로그는 별도 대시보드/monitor 로 분리해 "배포로 인한 예상된 error 로그" 와 실제 문제를 구분.
Monitoring#
배포 시점에 발생하는 이 로그 자체는 정상 흐름의 일부이므로, 알림은 다음 조건에서만 트리거되도록 구성:
Datadog widget query (timeseries):
sum:trace.rack.request.errors{service:cupixworks-migration-worker,resource_name:*append_mapping*}.as_count()
로그 기반 카운트 (배포 시간 외 발생 감지용):
service:cupixworks-migration-worker status:error @class:RevisionRequest @function:append_mapping!
추가 지표:
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