ES /docs

[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#

  1. 2026-07-17 07:46:31 KSTPOST /api/v1/aerial_maps/46/invoke (200), Step Function / Fargate preprocess 태스크 시작
  2. 2026-07-17 07:47:19 KSTPUT /api/v1/aerial_maps/46/trash (204) — 사용자가 처리 진행 중인 aerial map 을 휴지통으로 이동
  3. 2026-07-17 07:47:23 KST — preprocess Fargate 컨테이너 부팅 완료, preprocess fargate memory limit, method, outputs, resolution 로그 출력
  4. 2026-07-17 07:47:24 KST — inner catch (preprocess/index.ts:148) 가 [CupixAerialMap] preprocess fail {...ENT4000...} 로그 출력 → 본 클러스터 943116b3 생성
  5. 2026-07-17 07:47:24 KST — outer catch (preprocess/index.ts:175) 가 preprocess fail - error:({}) / message:(...) 로그 출력 → sibling 클러스터 7b2a836a 생성
  6. 2026-07-17 07:47:24 KSTprocess.exit(1) 로 preprocess 태스크 종료

Error Log#

Datadog Logs

text
[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 호출인 saveProcessingStateENT4000 "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 이 403 ENT4000
  • tesla 측 거부 지점: BaseRepository#find_by (app/repositories/base_repository.rb:351-357) 가 in_trash 인 레코드에 대해 ENT4000 재발생

preprocess app 함수의 첫 API 호출이 saveProcessingState 이므로, 부팅 후 첫 tesla 호출에서 실패한 실행 흐름과 일치한다:

applications/aerial-map-service/src/code/src/preprocess/index.ts:44-51typescript
  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 로 빠진다:

applications/aerial-map-service/src/code/src/preprocess/index.ts:144-153typescript
  } 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 결과가 정확히 본 클러스터 로그 형태와 일치:

applications/aerial-map-service/src/code/src/common/api.ts:646-658typescript
  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 로 이동된 레코드가 정확히 이 조건에 부합:

app/repositories/base_repository.rb:351-357ruby
    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 쿼리:

text
service:aerial-map-service status:error @environment:production "[CupixAerialMap] preprocess fail {\"result\":{\"code\":\"ENT4000\""

본 클러스터에 매칭되는 유일한 로그:

text
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):

text
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 실패 순서 확정:

text
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:353in_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-45 saveProcessingState 실패 처리 — 응답이 ENT4000 (Cupix::Errors::NotFound) 이면 "aerial map removed during preprocessing" 시나리오로 판정, logger.warn 로 기록하고 process.exit(0) 로 조용히 종료. 사용자 액션에 의한 예상 시나리오이므로 error 로그로 알림을 발생시키지 않는다.
  • preprocess/index.ts:149 saveError 도 trashed 대상이면 스킵 — 이미 trash 된 레코드에 두 번째 PUT 을 시도해 두 번째 403 을 만드는 부분(sibling 클러스터 로그 스팸의 원인) 을 제거.

단기 개선 (1주 이내)#

  • 에러 랩핑 방식 개선common/api.ts:646-658 updateAerialMapthrow 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_execution grep 결과 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 정의 후):
text
sum:aerial_map.preprocess.fail{env:production}.as_count()
  • ENT4000 로 인한 preprocess 실패 (log-based metric 분리 정의 후):
text
sum:aerial_map.preprocess.fail_ent4000{env:production}.as_count()
  • 라이브 확인용 log query:
text
service:aerial-map-service status:error @environment:production "AerialMap not found"
  • Fargate preprocess 부팅 지연 (invoke 시각 vs preprocess fargate memory limit 로그 시각 차이) 커스텀 메트릭:
text
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-153 catch 분기 조정만으로 즉시 조치 가능. 단기/장기 항목은 tesla 컨트롤러 변경과 AWS Step Functions 통합이 필요해 standard 수준.