[CupixAerialMap] postprocess fail - error:({"name":"Pix4dError","code":"AMB725","reason":"Mesh Outpu
RCA: [CupixAerialMap] postprocess fail — Pix4dError AMB725 (Mesh Output Data Not Found)
Overview#
What Happened#
2026-07-22 19:00 KST에 production aerial-map-service (us-west-2)의 postprocess 단계에서 aerial map id 230 이 Pix4dError / code:AMB725 / reason:Mesh Output Data Not Found 로 종료됐다. 원인은 reprocessing_option.type === 'postprocess' 로 호출된 재실행이 이전 Pix4D 처리에서 생성되지 않은 mesh output 을 요구했기 때문이다. Postprocess 는 Pix4D 를 다시 돌리지 않고 이미 존재하는 output 만 소비하므로, 존재하지 않는 output kind 를 요청하면 즉시 실패한다. 이후 19:08 KST 에 orthomosaic 만 요청하는 재실행이 트리거되어 19:27 KST 에 성공으로 종료됐다.
Quick Facts#
| Field | Value |
|---|---|
| exception.class | Pix4dError (via AerialMapError) |
| exception.code | AMB725 |
| exception.message | Mesh Output Data Not Found |
| top_frame | src/postprocess/index.ts:126 |
| env | production, us-west-2, tenant cupix |
| aerial_map.id | 230 |
| pix4d_project_id | 2588304 |
Affected Teams#
| Team / Domain | Error Count | Impact |
|---|---|---|
| aerial-map-service (postprocess) | 1 | aerial map id 230 의 mesh output 을 요구한 reprocess 요청 1건 실패. 후속으로 outputs 를 축소한 재실행이 성공하여 사용자 관점 최종 결과는 복구됨. |
Timeline#
- 2026-07-22 18:50 KST —
[CupixAerialMap] aerial map 230 reinvoke for mesh,dsm,pointcloud,orthomosaic(postprocess 재실행 트리거) - 2026-07-22 18:51 KST —
getProcessingOutput결과 로그:pix4d_log,report_pdf,report_json,orthomosaic,orthomosaic_gdalinfo,flight_path,thumbnail,scene_ref_frame만 존재.mesh/dsm/pointcloud키 없음. - 2026-07-22 19:00 KST —
postprocess fail - error:({"name":"Pix4dError","code":"AMB725","reason":"Mesh Output Data Not Found"})(cluster first/last_seen) - 2026-07-22 19:08 KST —
[CupixAerialMap] aerial map 230 reinvoke for orthomosaic(축소된 outputs 로 재실행) - 2026-07-22 19:27 KST —
[CupixAerialMap] postprocess success/postprocess time: 1121901ms
Error Log#
[CupixAerialMap] postprocess fail - error:({"name":"Pix4dError","code":"AMB725","reason":"Mesh Output Data Not Found"}) / message:(Mesh Output Data Not Found)
Impact#
- Service:
aerial-map-service - 발생 횟수: 1
- 최초 발생: 2026-07-22 19:00 KST
- 최근 발생: 2026-07-22 19:00 KST
- Blast radius: aerial map id 230 의 postprocess 재실행 1건. 동일 fingerprint 로 지난 14일간 다른 발생 없음 (Datadog
service:aerial-map-service Pix4dError= 1건). 후속 축소 재실행으로 최종적으로 복구됨.
Root Cause Summary#
postprocess 는 Pix4D processing 을 재실행하지 않고 pix4dApi.getProcessingOutput(projectId) 로 이미 존재하는 output metadata 만 조회하여 소비한다. aerialMap.reprocessing_option.type === 'postprocess' 경로에서 reprocessing_option.outputs 에 mesh 가 포함됐지만 이전 Pix4D 처리에서 mesh(result_type: '3d_mesh_obj_zip') 가 생성되지 않아 pix4dOutput['mesh'] 가 undefined 였고, src/postprocess/index.ts:126 의 if (!pix4dOutput['mesh']) throw new AerialMapError('AMB725') 가치기(guard)가 즉시 예외를 던지며 프로세스가 exit code 1 로 종료됐다. 즉 원인은 Pix4D 나 postprocess 로직의 결함이 아니라, 재실행 트리거가 이전 처리에 없었던 output kind 를 요청한 입력 불일치다.
Technical Analysis#
Code Path#
- Entry point:
src/postprocess/index.ts:36app(input)— postprocess Fargate 컨테이너 진입 - Outputs 결정:
src/postprocess/index.ts:43-48—reprocessing_option.type === 'postprocess'이면reprocessing_option.outputs사용, 아니면processing_option.outputs - Pix4D output metadata 조회:
src/postprocess/index.ts:70pix4dApi.getProcessingOutput(projectId) - Failure point:
src/postprocess/index.ts:126—meshoutput 존재 확인 실패 시AMB725throw - Catch/report:
src/postprocess/index.ts:148-157—AerialMapError인 경우cupixApi.saveError(aerialMapId, error.code, error.reason)후 재-throw, 최상단에서process.exit(1)
let outputs: TOutput[] = [];
if (aerialMap['reprocessing_option']?.type === 'postprocess') {
outputs = aerialMap['reprocessing_option'].outputs || [];
logger.info(`[CupixAerialMap] aerial map ${aerialMapId} reinvoke for ${outputs}`);
} else {
outputs = aerialMap['processing_option'].outputs || [];
}
outputs.forEach((output) => {
if (output === 'orthomosaic') {
if (!pix4dOutput['orthomosaic'] || !pix4dOutput['orthomosaic_gdalinfo']) throw new AerialMapError('AMB722');
tasks.push(
processTiff('orthomosaic', cupixApi, aerialMapId, pix4dOutput, downloadCredential, pix4dApi, projectId),
);
}
if (output === 'dsm') {
if (!pix4dOutput['dsm'] || !pix4dOutput['dsm_gdalinfo']) throw new AerialMapError('AMB723');
tasks.push(processTiff('dsm', cupixApi, aerialMapId, pix4dOutput, downloadCredential, pix4dApi, projectId));
}
if (output === 'pointcloud') {
if (!pix4dOutput['pointcloud']) throw new AerialMapError('AMB724');
tasks.push(processPointcloud(cupixApi, aerialMapId, pix4dOutput, downloadCredential));
}
if (output === 'mesh') {
if (!pix4dOutput['mesh']) throw new AerialMapError('AMB725');
tasks.push(processMesh(cupixApi, aerialMapId, pix4dOutput, downloadCredential));
}
});
// textured mesh
if (
output['availability'] === 'done' &&
output['result_type'] === '3d_mesh_obj_zip' &&
output['output_type'] === '3d_mesh_obj_zip' &&
output['s3_key']?.endsWith('.zip')
) {
source['mesh'] = {
region: output['s3_region'],
bucket: output['s3_bucket'],
key: output['s3_key'],
extension: path.extname(output['s3_key']).toLowerCase() || '.zip',
};
}
기대 동작: reprocess 트리거 측이 이전 Pix4D 처리에서 실제로 생성된 output kind subset 만 reprocessing_option.outputs 로 요청해야 한다.
실제 동작: mesh 를 포함해 요청되었지만 Pix4D 응답에 3d_mesh_obj_zip output 이 존재하지 않아 (18:51 로그의 postprocess - pix4d output:(...) 페이로드에 mesh 키가 없음) 가치기가 실패했다.
pix4d-process-check 는 초기 처리 파이프라인에서만 mesh 존재 여부를 게이팅하며, 오직 aerialMap['processing_option'].outputs 만 참조한다 — reprocess-only 재실행 경로에는 이 검증이 걸리지 않는다:
const { outputs } = aerialMap['processing_option'];
...
const processingOutputKind: Record<string, IOutputKey[]> = {
orthomosaic: ['orthomosaic', 'orthomosaic_gdalinfo'],
dsm: ['dsm', 'dsm_gdalinfo'],
pointcloud: ['pointcloud'],
mesh: ['mesh'],
};
...
const hasProcessingOutputData = outputs.every((output) => {
const processingOutputData = processingOutputKind[output];
return processingOutputData ? processingOutputData.every((key) => pix4dOutput[key]) : true;
});
result = hasCommonOutputData && hasProcessingOutputData ? 'success' : 'processing';
Navigator 는 reprocessing_option.type === 'postprocess' 요청을 preprocess/process 단계를 건너뛰고 곧바로 postprocess 로 라우팅하므로 Pix4D 는 다시 돌아가지 않는다:
const getRoute = (event: INavigateInput): TRoute => {
if (event.external_source) return 'import';
const kind = event.aerial_map?.['reprocessing_option']?.type;
if (kind === 'preprocess' || kind === 'process') return 'preprocess';
if (kind === 'postprocess') return 'postprocess';
if (kind === 'data-collect') return 'data-collect';
return 'preprocess';
};
Log Evidence#
사용한 Datadog 쿼리:
service:aerial-map-service AMB725
service:aerial-map-service @aerial_map.id:230
service:aerial-map-service reinvoke
핵심 로그 항목 (aerial_map.id: 230, 2026-07-22 KST):
18:50:57 info [CupixAerialMap] aerial map 230 reinvoke for mesh,dsm,pointcloud,orthomosaic
18:51:01 info [CupixAerialMap] postprocess - pix4d output:({"pix4d_log":{...},"report_pdf":{...},"report_json":{...},"orthomosaic":{...},"orthomosaic_gdalinfo":{...},"flight_path":{...},"thumbnail":{...},"scene_ref_frame":{...}})
19:00:20 info [CupixAerialMap] pix4d aerial photo post-processed
19:00:21 error [CupixAerialMap] postprocess fail - error:({"name":"Pix4dError","code":"AMB725","reason":"Mesh Output Data Not Found"}) / message:(Mesh Output Data Not Found)
19:00:22 info [CupixAerialMap] pix4d thumbnail output post-processed
19:00:23 info [CupixAerialMap] pix4d log output post-processed
19:08:52 info [CupixAerialMap] aerial map 230 reinvoke for orthomosaic
19:18:26 info [CupixAerialMap] pix4d aerial photo post-processed
19:27:34 info [CupixAerialMap] postprocess success
19:27:34 info [CupixAerialMap] postprocess time: 1121901ms
18:51:01 UTC 의 Pix4D output payload 에서 mesh (와 dsm, pointcloud) 키가 존재하지 않음을 육안으로 확인:
{
"pix4d_log": { "key": ".../logs/cupix-production-wgyates-230-1774410027063_processing_task.log" },
"report_pdf": { "key": ".../reports/cupix-production-wgyates-230-1774410027063_report.pdf" },
"report_json": { "key": ".../reports/cupix-production-wgyates-230-1774410027063_report.json" },
"orthomosaic": { "key": ".../reconstructions/cupix-production-wgyates-230-1774410027063_ortho.tiff" },
"orthomosaic_gdalinfo": { "key": ".../post_processing/ortho/Ortho_gdalinfo.json" },
"flight_path": { "key": ".../calibrated_camera_parameters.json" },
"thumbnail": { "key": ".../thumb/project_thumb.jpg" },
"scene_ref_frame": { "key": ".../scene_reference_frame.json" }
}
즉, mesh 키가 페이로드에 없어서 postprocess/index.ts:126 의 undefined 체크가 성립.
Container attributes:
label: postprocess
log.file.path: /tmp/workspace/postprocess-json-2026.07.22.log
host: ip-10-1-167-201.us-west-2.compute.internal
aerial_map.id: 230
Hypotheses Considered#
| # | Hypothesis | Evidence for | Evidence against | Verdict |
|---|---|---|---|---|
| H1 | reprocessing_option.outputs 에 mesh 가 포함되었으나 이전 Pix4D 처리에 mesh output 이 없어 postprocess 진입 시 pix4dOutput['mesh'] 가 undefined → AMB725 |
aerial map 230 reinvoke for mesh,dsm,pointcloud,orthomosaic (18:50) 로그와, 이어지는 pix4d output 페이로드에 mesh/dsm/pointcloud 키 부재 (18:51); src/postprocess/index.ts:126 가치기 정확히 일치; 후속 reinvoke for orthomosaic 만으로 성공(19:27) |
— | Confirmed |
| H2 | Pix4D 처리는 mesh 를 생성했으나 getProcessingOutput 응답 파싱 조건(result_type === '3d_mesh_obj_zip', s3_key.endsWith('.zip'))이 실제 응답과 미스매치되어 mesh 키가 세팅되지 않았다 |
api.ts:368-381 은 특정 조건에서만 source['mesh'] 를 채운다 (조건 실패 시 조용히 누락) |
동일 project 에 대한 19:08 재실행에서도 payload 에 mesh 키 없음. Pix4D 가 mesh 를 만들었다면 축소 재실행 시에도 나타나야 함. 19:08 요청 자체가 mesh 를 제외했다는 사실은 운영 측에서도 mesh 부재를 이미 인지했음을 시사 | Rejected |
| H3 | Pix4D 측 일시적 API 장애로 outputs 응답이 비어옴 | — | 응답에 pix4d_log, orthomosaic 등 8개 output 이 정상 포함됨. 부분적으로만 없는(missing) 상태이므로 전면 장애 아님 |
Rejected |
| H4 | postprocess 컨테이너 재시작/타이밍 race — Pix4D 처리 종료 직전에 postprocess 가 조회하여 mesh 만 lag 됨 | Fargate 프로세스이므로 이론적으로 가능 | Reprocess 경로(type === 'postprocess')는 pix4d-process-check 게이트를 우회한다 (navigate/index.ts:12). 또한 축소된 19:08 재실행에서도 mesh 는 여전히 없음. 시간 race 라면 결국 나타나야 함 |
Rejected |
| H5 | 광범위한 아웃라이어 클러스터 (여러 aerial_map 에서 동일 오류) | — | 지난 14일 service:aerial-map-service Pix4dError 쿼리에 1건만 매칭 (한 번의 aerial_map 230 재실행) |
Rejected |
Fix Recommendation#
즉시 조치 (Critical)#
- reprocess 트리거 측 검증:
reprocessing_option.type === 'postprocess'를 사용하는 호출 지점(외부 orchestrator / Rails 서버 / 사용자 CLI 등, aerial-map-service 밖)에서 요청outputs를 이전 Pix4D 처리에서 실제 생성된 output kind subset 으로 제한해야 한다. 이 aerial-map-service 리포지토리 안에서는 postprocess 실행 자체가 이미 정답 데이터(Pix4D outputs list)를 손에 쥐고 있으므로,src/postprocess/index.ts:70이후outputs필터링을 한 겹 두어 없는 output 은 skip + warn 하도록 방향을 잡을 수 있다. 구현 방향(코드 스니펫 없음):outputs.forEach앞에서outputs.filter((o) => pix4dOutput[o] != null)로 축소하고, 제거된 항목을logger.warn로 남겨 관측성 유지.- 또는 mesh/dsm/pointcloud 미존재를 error 대신 warn 으로 downgrade 하고,
orthomosaic처럼 사용자 관점 필수 output 만 실패로 유지. - 두 접근 모두 "재실행 요청자의 실수/누락" 에 대해 사용자 경험을 보호. 단, 요청된 output 이 하나도 없다면 여전히 실패하도록 최소 안전장치 유지.
단기 개선 (1주 이내)#
- reprocess 게이팅 대칭화:
pix4d-process-check/index.ts:17에서aerialMap['processing_option'].outputs만 참조하는 부분을, reprocess 경로에도 동일한 존재 확인 로직이 적용되도록 리팩터. 최소한 postprocess entrypoint 초입에 "요청된 outputs vs Pix4D 실제 outputs" diff 를 명시적으로 로깅해 사후 조사 시간을 줄인다. - 에러 코드 세분화: AMB722–AMB725 는 모두 동일한 "요청한 output kind 가 Pix4D 응답에 없음" 원인을 공유하므로, 로그에서
label: reprocess-outputs-missing태그를 추가하거나 예외 payload 에 요청/실제 outputs list 를 함께 남겨 재발 시 root cause 판별을 가속.
장기 개선 (재발 방지)#
- Contract 명세화: aerial-map-service 를 호출하는 상위 시스템(Rails
AerialMap모델 / reprocess 트리거 UI)에서reprocessing_option.outputs를 이전 처리 결과와 교차 검증하는 서버 측 validation 도입. 클라이언트가 "mesh 재생성" 을 원한다면type: 'process'(또는'preprocess') 로 다시 돌리도록 라우팅하고,type: 'postprocess'는 "이미 존재하는 output 재-업로드" 에 국한. - integration test: reprocess-postprocess 경로에 대해 "요청 outputs 가 Pix4D outputs 의 superset 인 경우" 시나리오를 스텁하여 회귀 방지.
Monitoring#
추가할 Datadog 알림:
service:aerial-map-service status:error @attributes.label:postprocess "AMB72"
service:aerial-map-service status:error "postprocess fail" ("AMB722" OR "AMB723" OR "AMB724" OR "AMB725")
Reprocess 트리거와 실패 상관관계 확인용 (같은 aerial_map.id 로 reinvoke 후 곧 실패):
service:aerial-map-service ("reinvoke for" OR "postprocess fail")
Risk Assessment#
- Risk level: low — 지난 14일 1건 발생, 사용자 관점 축소 재실행으로 즉시 복구됨. 다만 재실행 UX 가 계속 유효하다면 유사 오류 재발 가능.
- 예상 복잡도: standard — postprocess entrypoint 내 filter/downgrade 는 국소 변경. contract-level 개선은 상위 시스템 협조 필요.