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#
- 2026-05-22 04:30:20 UTC — capture_72952에서 pano 다운로드 실패 4건 발생
- 2026-05-22 04:30:53 UTC — capture_72927에서 pano 다운로드 실패 2건 발생
- 2026-05-22 04:46:32 UTC — capture_72927 "Preview is ready", "refine processing done" 로그 확인 (API 정상 동작)
- 2026-05-22 05:20:28 UTC — postprocessor 완료:
dd_total:16&dd_completed:16&dd_errored:0(에러 미반영)
Error Log#
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:46—init()→run() - Pano 목록 조회:
pano-postprocessor-manager.ts:29-31—getCPPanosByCaptureId() - S3 다운로드 시작:
pano-postprocessor-service.ts:117—awsS3Manager.downloadParallel(cpPanos) - Failure point:
aws-s3.manager.ts:68-71—this.download()호출 시 S3 NoSuchKey 에러 발생
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도 포함될 수 있다.
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_key는 resource.object_key로 매핑된다 (pano_serializer.rb:102).
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#
Datadog query: service:cupixworks-pano-postprocessor-instance status:error "AwsS3Manager::downloadParallel"
Time: 1779417000000 to 1779427860000
영향받은 capture별 실패 pano 목록:
[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에서 에러 미반영 확인:
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은 에러 직후 정상적으로 처리 완료된 것으로 확인됨:
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:117—downloadParallel결과로 실패한 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 쿼리:
service:cupixworks-pano-postprocessor-instance status:error "The specified key does not exist"
- 해당 쿼리로 monitor 생성하여 NoSuchKey 발생 시 알림
Risk Assessment#
- Risk level: low
- 예상 복잡도: standard