[CupixAerialMap] preprocess fail {"result":{"code":"ENT4000","type":"Cupix::Errors::NotFound","reaso
RCA: [CupixAerialMap] preprocess fail — AerialMap not found (ENT4000)
Overview#
What Happened#
2026-07-17 07:47:24 KST 에 aerial-map-service 의 preprocess Fargate 태스크가 실행 중 Cupix::Errors::NotFound (ENT4000, "AerialMap not found") 응답을 받아 실패했다. 사용자가 POST /api/v1/aerial_maps/46/invoke 로 처리를 시작한 뒤 preprocess 컨테이너가 실제로 tesla API 를 호출하기 전에 같은 aerial map 을 PUT /api/v1/aerial_maps/46/trash 로 휴지통으로 옮겼다. 이 클러스터는 preprocess 내부 catch (preprocess/index.ts:148) 가 남긴 로그 라인이며, 같은 잡의 outer catch (:175) 는 sibling 클러스터 7b2a836a 에 해당한다.
Quick Facts#
| Field | Value |
|---|---|
| exception.class | Cupix::Errors::NotFound (tesla 응답) / rethrown Error (aerial-map-service) |
| exception.message | {"result":{"code":"ENT4000","type":"Cupix::Errors::NotFound","reason":"AerialMap not found","message":"AerialMap not found"}} |
| top_frame | applications/aerial-map-service/src/code/src/preprocess/index.ts:148 |
| runtime | Node.js on ECS Fargate (preprocess container) |
| env | production, ap-southeast-2 (tenant: cupix) |
| affected aerial_map_id | 46 (sibling cluster 7b2a836a 의 tesla 로그에서 확인) |
Affected Teams#
| Team / Domain | Error Count | Impact |
|---|---|---|
| aerial-map-service (production, ap-southeast-2) | 1건 (sibling cluster 포함 시 동일 잡에서 2건) | 단일 AerialMap 처리 잡 1건 실패. 사용자가 이미 trash 로 옮긴 상태라 최종 상태는 사용자 의도와 일치. Pix4D 프로젝트 생성 전에 실패해 외부 비용 소모 없음. |
Timeline#
- 2026-07-17 07:46:31 KST —
POST /api/v1/aerial_maps/46/invoke(200), Step Function / Fargate preprocess 태스크 시작 - 2026-07-17 07:47:19 KST —
PUT /api/v1/aerial_maps/46/trash(204) — 사용자가 처리 진행 중인 aerial map 을 휴지통으로 이동 - 2026-07-17 07:47:23 KST — preprocess Fargate 컨테이너 부팅 완료,
preprocess fargate memory limit,method,outputs,resolution로그 출력 - 2026-07-17 07:47:24 KST — inner catch (
preprocess/index.ts:148) 가[CupixAerialMap] preprocess fail {...ENT4000...}로그 출력 → 본 클러스터943116b3생성 - 2026-07-17 07:47:24 KST — outer catch (
preprocess/index.ts:175) 가preprocess fail - error:({}) / message:(...)로그 출력 → sibling 클러스터7b2a836a생성 - 2026-07-17 07:47:24 KST —
process.exit(1)로 preprocess 태스크 종료
Error Log#
[CupixAerialMap] preprocess fail {"result":{"code":"ENT4000","type":"Cupix::Errors::NotFound","reason":"AerialMap not found","message":"AerialMap not found"}}
Impact#
- Service:
aerial-map-service - 발생 횟수: 1건 (sibling 클러스터 포함 동일 잡에서 총 2 로그 라인)
- 최초 발생: 2026-07-17 07:47:24 KST
- 최근 발생: 2026-07-17 07:47:24 KST
- 사용자 영향: 낮음 — 사용자가 명시적으로 trash 로 이동시켰으므로 최종 상태는 사용자 의도와 일치. 데이터/파일 손실 없음, Pix4D 자원 소모 없음.
Root Cause Summary#
사용자가 aerial map processing (invoke) 을 시작한 뒤 preprocess Fargate 컨테이너가 실제로 tesla API 호출을 시작하기까지 약 52초의 부팅 지연이 있고, 이 창 사이에 사용자가 같은 aerial map 을 PUT .../trash 로 휴지통으로 옮기면 preprocess 의 첫 tesla API 호출인 saveProcessingState 가 ENT4000 "AerialMap not found" 를 받는다. axios 응답이 예외로 던져지면서 AerialMapError 가 아닌 일반 Error 로 감싸지므로 inner catch (preprocess/index.ts:144-153) 의 else 브랜치를 타고 logger.error("[CupixAerialMap] preprocess fail ${error.message}") 로그를 남긴 뒤 rethrow 된다. 이것이 본 클러스터의 로그이며, 이어서 outer catch 가 sibling 클러스터 로그를 남긴다.
Technical Analysis#
Code Path#
- Entry point:
applications/aerial-map-service/src/code/src/preprocess/index.ts:168-179(IIFE 로app(input)호출) - 실패 지점 (본 클러스터): inner catch else 브랜치 —
preprocess/index.ts:148 - 근본 실패 API 호출:
saveProcessingState(preprocess/index.ts:45) →updateAerialMap(common/api.ts:646-658) →PUT /api/v1/aerial_maps/46이 403ENT4000 - tesla 측 거부 지점:
BaseRepository#find_by(app/repositories/base_repository.rb:351-357) 가in_trash인 레코드에 대해ENT4000재발생
preprocess app 함수의 첫 API 호출이 saveProcessingState 이므로, 부팅 후 첫 tesla 호출에서 실패한 실행 흐름과 일치한다:
try {
await cupixApi.saveProcessingState(aerialMapId, 'preprocessing', { preprocessBeginTimestamp: beginTime });
// aerial map camera data
const aerialPhotos = await cupixApi.getAerialPhotosByAerialMapId(aerialMapId);
const sortedAerialPhotos = aerialPhotos.sort((a, b) => a.id - b.id);
const firstAerialPhoto = sortedAerialPhotos[0];
if (!firstAerialPhoto) throw new AerialMapError('AMB404');
본 클러스터의 로그 라인은 inner catch else 브랜치에서 발생. 던져진 error 는 AerialMapError 가 아니라 updateAerialMap 이 만든 일반 Error(JSON.stringify(...)) 이므로 else 로 빠진다:
} catch (error: any) {
if (error instanceof AerialMapError) {
await cupixApi.saveError(aerialMapId, error.code, error.reason);
} else {
logger.error(`[CupixAerialMap] preprocess fail ${error.message}`);
await cupixApi.saveError(aerialMapId, ERROR_CODE['AMB710'].code, ERROR_CODE['AMB710'].reason);
}
throw error;
}
updateAerialMap 이 axios 에러 응답 body 를 그대로 JSON.stringify 해 새 Error 를 던지므로 error.message 는 {"result":{"code":"ENT4000",...}} JSON 문자열이 된다 → template literal 결과가 정확히 본 클러스터 로그 형태와 일치:
private async updateAerialMap(aerialMapId: number, param: IAerialMapUpdateParam) {
try {
console.log(`[CupixApi] updateAerialMap - /api/v1/aerial_maps/${aerialMapId}`);
const response = await axios.put(`${this._endpoint}/api/v1/aerial_maps/${aerialMapId}?fields=id,key`, param, {
headers: { 'x-cupix-auth': `${this._accessToken}` },
});
return response.data;
} catch (error: any) {
console.error(`[CupixApi] updateAerialMap - /api/v1/aerial_maps/${aerialMapId} - ${error.message}`);
throw new Error(JSON.stringify(error.response?.data || error.message));
}
}
tesla 측 BaseRepository#find_by 는 조회된 model 이 nil 이면서 같은 조건의 trashed 레코드가 존재하면 ENT4000 을 던진다. trash 로 이동된 레코드가 정확히 이 조건에 부합:
if model.nil?
if self.where(attrs).in_trash.present?
raise Cupix::Errors::NotFound.new(code: 'ENT4000', reason: "#{current_class.name} not found")
else
raise Cupix::Errors::NotFound.new(code: 'ARG10002', reason: "#{current_class.name} not found")
end
end
기대 동작: 사용자가 처리 중인 aerial map 을 trash 로 옮기면 (a) 진행 중 Step Function / Fargate 태스크가 명시적으로 정지되거나, (b) preprocess 가 시작 시점에 trashed 상태를 감지해 warn 레벨로 조용히 종료. 실제 동작: preprocess 가 inner catch (:148) 와 outer catch (:175) 에서 각각 error 레벨 로그를 남기고 process.exit(1) 로 종료 → error-sweeper 가 두 개의 서로 다른 fingerprint 로 분리 클러스터를 생성.
Log Evidence#
Datadog 쿼리:
service:aerial-map-service status:error @environment:production "[CupixAerialMap] preprocess fail {\"result\":{\"code\":\"ENT4000\""
본 클러스터에 매칭되는 유일한 로그:
2026-07-17 07:47:24 KST error [CupixAerialMap] preprocess fail {"result":{"code":"ENT4000","type":"Cupix::Errors::NotFound","reason":"AerialMap not found","message":"AerialMap not found"}}
Sibling 클러스터 (7b2a836a) 는 같은 잡 outer catch (preprocess/index.ts:175) 가 남긴 다음 로그 (동일 시각 07:47:24 KST):
2026-07-17 07:47:24 KST error [CupixAerialMap] preprocess fail - error:({}) / message:({"result":{"code":"ENT4000","type":"Cupix::Errors::NotFound","reason":"AerialMap not found","message":"AerialMap not found"}})
tesla API 접근 로그 (aerial_map_id=46, sibling cluster RCA 조사에서 이미 확보): invoke → trash → PUT 실패 순서 확정:
2026-07-17 07:46:31 KST info [200] POST /api/v1/aerial_maps/46/invoke (Api::V1::AerialMapsController#invoke)
2026-07-17 07:47:19 KST info [204] PUT /api/v1/aerial_maps/46/trash (Api::V1::AerialMapsController#trash)
2026-07-17 07:47:25 KST info [403] PUT /api/v1/aerial_maps/46 (Api::V1::AerialMapsController#update) error={reason:"AerialMap not found", code:"ENT4000"}
2026-07-17 07:47:25 KST info [403] PUT /api/v1/aerial_maps/46 (Api::V1::AerialMapsController#update) error={reason:"AerialMap not found", code:"ENT4000"}
두 번째 403 은 inner catch 안의 saveError (preprocess/index.ts:149) 가 trashed 레코드에 다시 PUT 을 시도한 결과. 이후 outer catch (:175) 가 rethrown 예외를 잡아 sibling 클러스터 로그를 남긴다.
Hypotheses Considered#
| # | Hypothesis | Evidence for | Evidence against | Verdict |
|---|---|---|---|---|
| H1 | 사용자가 preprocess 진행 중 aerial map 을 trash 로 옮겨 preprocess 첫 API 호출이 ENT4000 실패 |
POST .../46/invoke @ 07:46:31 → PUT .../46/trash (204) @ 07:47:19 → PUT .../46 (403 ENT4000) @ 07:47:25. tesla base_repository.rb:353 가 in_trash 일 때 정확히 ENT4000 을 던짐. 본 클러스터 로그 텍스트는 preprocess/index.ts:148 의 template literal 결과와 문자열 일치. |
— | Confirmed |
| H2 | AerialMap 46 이 처음부터 존재하지 않았거나 잘못된 ID | ENT4000 메시지만 보면 가능 | 같은 잡의 upload / invoke 호출이 200 응답. 레코드는 정상 존재 후 trash 로 이동. | Rejected |
| H3 | 인증/세션 문제로 인한 403 | 응답 status 403 | 응답 body 의 code:ENT4000 은 인증 (PERM10000/401) 아닌 리소스 미존재. 같은 세션 다른 요청은 200. |
Rejected |
| H4 | 두 클러스터 (943116b3, 7b2a836a) 가 서로 다른 사건 |
두 개 클러스터 존재 | 두 로그 라인이 동일 초 (07:47:24) 에 aerial_map_id=46 로 발생. 각각 preprocess/index.ts:148 와 :175 의 서로 다른 template literal 결과라 fingerprint 만 분리됨. |
Rejected |
| H5 | 알려진 external dependency outage (dep:*) | — | status board 결과 svc:aerial-map-service::unknown 내부 서비스 그룹핑. dep:* 아님. |
Rejected |
Fix Recommendation#
즉시 조치 (Critical)#
preprocess/index.ts:44-45saveProcessingState실패 처리 — 응답이ENT4000(Cupix::Errors::NotFound) 이면 "aerial map removed during preprocessing" 시나리오로 판정,logger.warn로 기록하고process.exit(0)로 조용히 종료. 사용자 액션에 의한 예상 시나리오이므로error로그로 알림을 발생시키지 않는다.preprocess/index.ts:149saveError도 trashed 대상이면 스킵 — 이미 trash 된 레코드에 두 번째 PUT 을 시도해 두 번째 403 을 만드는 부분(sibling 클러스터 로그 스팸의 원인) 을 제거.
단기 개선 (1주 이내)#
- 에러 랩핑 방식 개선 —
common/api.ts:646-658updateAerialMap이throw new Error(JSON.stringify(error.response?.data || error.message))로 재래핑하는 방식이 (a)error instanceof AerialMapError체크를 실패시켜 else 브랜치로 빠지게 하고 (b) inner/outer catch 가 서로 다른 문자열 로그를 남겨 두 개의 fingerprint 클러스터로 분리되게 만든다. 커스텀 클래스 (예:AerialMapApiError { code, message, isNotFound }) 로 감싸 상위 catch 가 조건 분기하도록 정리하면 클러스터 중복도 해소된다. AerialMapsController#trash— 진행 중 (state_preprocessing? || state_processing? || state_postprocessing?) 인 경우 400/409 (STAT10000) 로 거부하거나, trash 를 허용하되 tesla 쪽에서 Step Function 실행 ARN 을 저장해StopExecution호출.
장기 개선 (재발 방지)#
- Step Function 실행 ARN 을 AerialMap 모델에 저장하고 trash/untrash 시
Aws::States::Client#stop_execution을 호출해 진행 중 Fargate 태스크를 명시적으로 종료. 현재 tesla 코드베이스에서 aerial map 용 Step Function 취소 경로가 존재하지 않는다 (sibling cluster RCA 에서stop_executiongrep 결과 0건 확인). - preprocess 컨테이너 부팅 지연 (07:46:31 → 07:47:23 ≈ 52초) 이 사용자 액션과의 race window 를 만든다. Fargate 태스크가 시작 즉시 aerial map 존재/state 를 확인하고, boot 후에도 이를 heartbeat 로 유지해 race 진입을 조기 차단.
Monitoring#
writing-datadog-monitoring-queries 규칙에 따라 dashboard timeseries widget 에 그대로 넣을 수 있는 형태로 작성:
- preprocess 실패 (log-based metric
aerial_map.preprocess.fail정의 후):
sum:aerial_map.preprocess.fail{env:production}.as_count()
- ENT4000 로 인한 preprocess 실패 (log-based metric 분리 정의 후):
sum:aerial_map.preprocess.fail_ent4000{env:production}.as_count()
- 라이브 확인용 log query:
service:aerial-map-service status:error @environment:production "AerialMap not found"
- Fargate preprocess 부팅 지연 (invoke 시각 vs
preprocess fargate memory limit로그 시각 차이) 커스텀 메트릭:
avg:aerial_map.preprocess.boot_lag_seconds{env:production}
알림: sum:aerial_map.preprocess.fail_ent4000{env:production}.as_count() 가 15분 창에서 5건 이상이면 Slack 통지 (사용자 액션 race vs 시스템 문제 구분 필요).
Risk Assessment#
- Risk level: low — 단일 사용자 액션 race, 결과 상태가 사용자 최종 의도 (trash) 와 일치. 데이터 손실/외부 자원 낭비 없음.
- 예상 복잡도: trivial —
preprocess/index.ts:44-153catch 분기 조정만으로 즉시 조치 가능. 단기/장기 항목은 tesla 컨트롤러 변경과 AWS Step Functions 통합이 필요해 standard 수준.