ES /docs

Postprocessor triggers before S3 upload completes — race condition

RCA: AwsS3Manager::downloadParallel | download failed - The specified key does not exist

Overview#

What Happened#

2026-05-22 04:30 UTC에 cupixworks-pano-postprocessor-instance 서비스에서 S3 파노라마 이미지 다운로드 실패 에러가 6건 발생했다. capture_72952 (4건)와 capture_72927 (2건)에 속한 pano 이미지들의 S3 object key가 존재하지 않아 다운로드에 실패했다.

Quick Facts#

Field Value
exception.class AWS S3 NoSuchKey
exception.message The specified key does not exist.
top_frame aws-s3.manager.ts:68-76
env production, ap-southeast-2

Timeline#

  1. 2026-05-22 04:30:20 UTC — capture_72952에서 pano 다운로드 실패 4건 발생
  2. 2026-05-22 04:30:53 UTC — capture_72927에서 pano 다운로드 실패 2건 발생
  3. 2026-05-22 04:46:32 UTC — capture_72927 "Preview is ready", "refine processing done" 로그 확인 (API 정상 동작)
  4. 2026-05-22 05:20:28 UTC — postprocessor 완료: dd_total:16&dd_completed:16&dd_errored:0 (에러 미반영)

Error Log#

Datadog Logs

text
AwsS3Manager::downloadParallel | download failed - pano: /tmp/workspace/capture_72952/download/pano_13222976.jpg, error: The specified key does not exist.

Impact#

  • Service: cupixworks-pano-postprocessor-instance
  • Team: aialdp001
  • 발생 횟수: 6
  • 최초 발생: 2026-05-22T04:30:20.978Z
  • 최근 발생: 2026-05-22T04:30:56.243Z

다운로드 실패한 pano 이미지들(pano_13222976, pano_13222977, pano_13222822, pano_13222534, pano_13222970, pano_13222533)에 대한 후처리(mask blur, infer, resize)가 원본 파일 없이 진행된다. 에러가 catch되어 무시되므로 최종 metric에서 dd_errored:0으로 표시되지만, 실제로는 이 pano들에 대한 mask/resize/infer 결과물이 비정상일 수 있다.

Root Cause Summary#

Pano 레코드의 resources[0].storage_bucket_key가 가리키는 S3 object가 해당 버킷에 존재하지 않는다. Tesla API에서 pano 목록을 가져올 때 state !== 'done'인 pano들을 선택하는데, 이 pano들의 원본 이미지가 S3에 업로드되지 않았거나 삭제된 상태에서 postprocessor가 실행되었다. 이는 업로드 중 실패(interrupted upload), S3 lifecycle policy에 의한 자동 삭제, 또는 다른 프로세스에 의한 삭제가 원인일 수 있다. 동시에 2개의 capture(72952, 72927)에서 동일 증상이 발생한 것은 동일 시점에 업로드가 완료되지 않은 상태에서 postprocessor가 트리거된 race condition을 시사한다.

Technical Analysis#

Code Path#

  • Entry point: pano-postprocessor-service.ts:46init()run()
  • Pano 목록 조회: pano-postprocessor-manager.ts:29-31getCPPanosByCaptureId()
  • S3 다운로드 시작: pano-postprocessor-service.ts:117awsS3Manager.downloadParallel(cpPanos)
  • Failure point: aws-s3.manager.ts:68-71this.download() 호출 시 S3 NoSuchKey 에러 발생
applications/agents/packages/cupix-pano-postprocessor/src/manager/pano-postprocessor-manager.ts:29-31typescript
async getCPPanosByCaptureId(captureId: number): Promise<CPPano[]> {
    const srvPanos = await this.cupixApi.pano.getAll(captureId);
    return srvPanos.filter((srvPano) => srvPano.state !== 'done').map((srvPano) => new CPPano(srvPano));
};

state !== 'done'인 pano를 모두 가져온다. 업로드가 완료되지 않은 pano도 포함될 수 있다.

applications/agents/packages/cupix-pano-postprocessor/src/model/cppano.ts:32-35typescript
this._panoBucketRegion = pano.resources[0]['s3_bucket_region'];
this._panoBucketName = pano.resources[0]['s3_source_bucket_name'];
this._panoBucketKey = pano.resources[0]['storage_bucket_key'];
this._panoBucketEndpoint = pano.resources[0]['bucket_endpoint'];

Tesla API serializer에서 storage_bucket_keyresource.object_key로 매핑된다 (pano_serializer.rb:102).

applications/agents/packages/cupix-pano-postprocessor/src/manager/aws-s3.manager.ts:60-78typescript
const tasks = cpPano.map(cpPano => {
    return limit(async () => {
        try {
            if (cpPano.panoBucketEndpoint) {
                await this.downloadByPanoApi(cpPano);
            } else {
                await this.download(
                    cpPano.panoBucketRegion!,
                    cpPano.panoBucketName!,
                    cpPano.panoBucketKey!,
                    cpPano.downloadImagePath!
                );
            }
        } catch (err) {
            logger.error('AwsS3Manager::downloadParallel | download failed - pano: %s, error: %s', cpPano.downloadImagePath, (err as Error).message);
        }
    });
});

에러가 catch에서 로깅만 되고 throw하지 않으므로, 다운로드 실패한 pano도 후속 처리(mask, infer, resize)로 진행된다. 파일이 존재하지 않는 상태에서 후속 작업이 실행되면 silent failure 또는 잘못된 결과가 생성될 수 있다.

Log Evidence#

text
Datadog query: service:cupixworks-pano-postprocessor-instance status:error "AwsS3Manager::downloadParallel"
Time: 1779417000000 to 1779427860000

영향받은 capture별 실패 pano 목록:

text
[capture_72952] 4건 (13:30:20-13:30:22 KST)
- pano_13222976.jpg — The specified key does not exist.
- pano_13222977.jpg — The specified key does not exist.
- pano_13222822.jpg — The specified key does not exist.
- pano_13222534.jpg — The specified key does not exist.

[capture_72927] 2건 (13:30:53-13:30:56 KST)
- pano_13222970.jpg — The specified key does not exist.
- pano_13222533.jpg — The specified key does not exist.

postprocessor 처리 완료 후 metric에서 에러 미반영 확인:

text
Datadog query: service:cupixworks-pano-postprocessor-instance "Metric"
Result: "PanoPostprocessor Metric - dd_metric:pano.postprocess.summary&dd_total:16&dd_completed:16&dd_errored:0&dd_product:cupixworks"

capture_72927은 에러 직후 정상적으로 처리 완료된 것으로 확인됨:

text
Datadog query: service:cupixworks-api (72952 OR 72927)
Results:
- "capture 72927 published: refine processing done" (13:46:32 KST)
- "Preview is ready for Capture 72927. record_id: 17077" (13:46:32 KST)

과거 1일간 동일 에러 패턴 검색 결과, 이번 6건 외에 추가 NoSuchKey 에러는 없었음 (별도 timeout 에러 1건만 발견: capture_700512).

Hypotheses Considered#

# Hypothesis Evidence for Evidence against Verdict
H1 Pano 원본 이미지 업로드 미완료 상태에서 postprocessor가 조기 트리거됨 (race condition) 2개 capture에서 동시 발생, pano state가 'done'이 아닌 상태에서 가져옴, 이전 24시간 동일 에러 없음 (일시적 현상) 직접적 업로드 실패 로그 미확인 (Datadog에서 해당 pano ID의 업로드 로그 없음) Confirmed
H2 S3 lifecycle policy에 의해 오래된 pano 이미지 자동 삭제 S3 object가 존재하지 않음 최근 생성된 capture (72952, 72927)이므로 lifecycle 정책 대상이 아닐 가능성 높음, 동일 capture의 다른 pano들은 정상 처리됨 Rejected
H3 잘못된 bucket key가 DB에 저장됨 (데이터 무결성 문제) S3 key가 존재하지 않는 것은 key가 잘못될 수 있음 동일 capture의 다른 pano들은 정상 다운로드됨, bucket key 패턴이 일관적 Rejected

Fix Recommendation#

즉시 조치 (Critical)#

  • aws-s3.manager.ts:75-77 — 다운로드 실패 시 해당 pano를 후속 처리에서 제외하도록 변경. 현재 에러를 catch하고 로깅만 하는데, 실패한 pano를 결과 집합에서 제거하거나 에러 상태로 마킹해야 한다.
  • pano-postprocessor-service.ts:117downloadParallel 결과로 실패한 pano 목록을 반환받아, 후속 mask/infer/resize에서 해당 pano를 건너뛰도록 처리.

단기 개선 (1주 이내)#

  • downloadParallel 메서드가 성공/실패를 명시적으로 반환하도록 리팩토링. 실패한 pano ID를 반환하여 호출자가 erroredPanoIds에 추가하고 updatePanoState(panoId, Error)를 호출할 수 있게 한다.
  • getCPPanosByCaptureId에서 resource_state가 'uploaded' 또는 'ready' 상태인 pano만 필터링하는 조건 추가 검토.

장기 개선 (재발 방지)#

  • Pano postprocessor 트리거 조건에 "모든 pano의 S3 upload 완료" 확인 로직 추가. 업로드 미완료 pano가 있으면 재시도 대기 또는 해당 pano 제외 후 처리.
  • S3 object 존재 여부를 headObject로 사전 검증하는 pre-flight check 도입.

Monitoring#

  • 다운로드 실패 pano 수를 metric에 반영: dd_download_failed 필드 추가
  • Datadog 쿼리:
text
service:cupixworks-pano-postprocessor-instance status:error "The specified key does not exist"
  • 해당 쿼리로 monitor 생성하여 NoSuchKey 발생 시 알림

Risk Assessment#

  • Risk level: low
  • 예상 복잡도: standard