RefinementService::run | end - {}
RCA: RefinementService::run | end - {}
Error Log#
RefinementService::run | end - {}
Impact#
- Service:
cupixworks-capture-refinement-arm-instance - Team: tgsp-jv
- 발생 횟수: 6
- 최초 발생: 2026-04-20T01:48:34.927Z
- 최근 발생: 2026-04-20T02:00:21.360Z
Root Cause Summary#
RefinementService::run의 catch 블록에서 JSON.stringify(error)로 Error 객체를 직렬화할 때 {}가 출력되는 것이 이 클러스터의 fingerprint를 형성한다. 실제 에러는 두 가지 원인으로 발생한다: (1) 주요 원인 — scenemapper 바이너리가 align_preview_meta_refinement_with_prior_map.json 출력 파일을 생성하지 않아 copyAlignmentToEfs에서 throw (5회 이상), (2) 부차 원인 — capture의 selected_unrefined_cluster_id가 null이어서 loadCluster에서 throw (1회). 두 에러 모두 run 메서드의 catch 블록에서 JSON.stringify(new Error(...)) → {}로 직렬화되어 동일한 에러 메시지 "RefinementService::run | end - {}"로 기록된다.
Technical Analysis#
Code Path#
- Entry point:
app.ts:54—new RefinementService().init()호출 refinement-service.ts:43-48—init()이authenticate()→run()→terminateService()순서로 실행
// refinement-service.ts:43-48
init = async (): Promise<void> => {
logger.info('RefinementService::init');
await this.authenticate();
await this.run();
this.terminateService();
};
refinement-service.ts:79-111—run()메서드가 job 로드 후 capture 처리 파이프라인 실행. try-catch에서 모든 에러를JSON.stringify(error)로 로깅
// refinement-service.ts:107-110
} catch (error) {
logger.error('RefinementService::run | end - %s', JSON.stringify(error));
await this.jobManager.updateErrorActionJob('refinement');
}
핵심 문제: JSON.stringify(new Error('message'))는 {}를 반환한다. JavaScript의 Error 객체는 message, stack 등의 프로퍼티가 non-enumerable이기 때문에 JSON.stringify가 빈 객체를 출력한다. 이로 인해 실제 에러 원인이 로그에서 완전히 손실된다.
Failure Path 1 — copyAlignmentToEfs (5+ occurrences):
refinement-service.ts:96-97—runRefinement()실행 후copyAlignmentToEfs()호출refinement-service.ts:370-382—align_preview_meta_refinement_with_prior_map.json파일 크기가 0이거나 존재하지 않으면 throw
// refinement-service.ts:370-382
const alignPreviewRefinementFileLocalPath = path.join(cpCapture.resultsDirPath, Constants.DefaultAlignPreviewRefinementJsonFileName);
if (CPUtils.getFileSize(alignPreviewRefinementFileLocalPath) > 0) {
// ... copy file
} else {
logger.warn('RefinementService::copyAlignmentToEfs | not found refined %s', Constants.DefaultAlignPreviewRefinementJsonFileName);
this.jobManager.setErrorCode(ErrorCode.ScenemapperUtils.RefinerNotFoundInputFiles);
throw new Error('not found refined align_preview_meta_refinement_with_prior_map.json');
}
scenemapper 바이너리(ScenemapperManager::execute)가 정상 종료했지만 기대하는 출력 파일을 생성하지 못한 것이다. ScenemapperManager::execute의 warn 로그가 없으므로 바이너리 자체는 에러 없이 완료되었으나 refinement 결과를 산출하지 못한 경우이다.
Failure Path 2 — loadCluster (1 occurrence, capture 69024):
refinement-service.ts:175-182— capture API 응답에서selected_unrefined_cluster_id가 undefined
// refinement-service.ts:175-182
private loadCluster = async (cpCapture: CPCapture): Promise<void> => {
logger.debug('RefinementService::loadCluster | begin');
const clusterId = cpCapture.srvCapture?.selected_unrefined_cluster_id;
if (clusterId == undefined) {
logger.error('RefinementService::loadCluster | end - not found selected_unrefined_cluster_id');
this.jobManager.setErrorCode(ErrorCode.ScenemapperUtils.RefinerNotFoundCluster);
throw new Error('not found selected_unrefined_cluster_id');
}
Tesla 측에서 capture_invoker.rb:312-323의 reset_refinement 메서드가 selected_unrefined_cluster_id = nil로 초기화하며, capture.rb:270-278의 update_selected_unrefined_cluster_id에서만 이 값이 설정된다. Capture 69024는 refinement job이 시작되었으나 selected_unrefined_cluster_id가 설정되지 않은 채 또는 reset된 후 job이 실행된 경우이다.
Log Evidence#
사용한 Datadog 쿼리:
service:cupixworks-capture-refinement-arm-instance status:error "RefinementService::run"
service:cupixworks-capture-refinement-arm-instance "copyAlignmentToEfs" "not found refined"
service:cupixworks-capture-refinement-arm-instance "loadCluster" OR "selected_unrefined_cluster_id"
에러 타임라인 (KST):
| 시간 (KST) | 이벤트 | Capture | Job | Region |
|---|---|---|---|---|
| 10:48:34 | loadCluster | end - not found selected_unrefined_cluster_id |
69024 | 178802 | ap-southeast-2 |
| 10:48:34 | run | end - {} (error) |
69024 | 178802 | ap-southeast-2 |
| 10:50:06 | copyAlignmentToEfs | not found refined ...json (warn) + run | end - {} (error) |
— | — | — |
| 10:52:30 | 동일 패턴 반복 | — | — | — |
| 10:54:29 | 동일 패턴 반복 | — | — | — |
| 10:58:41 | 동일 패턴 반복 | — | — | — |
| 11:00:21 | copyAlignmentToEfs | not found refined ...json (warn) + run | end - {} (error) |
69026 | 178801 | ap-southeast-2 |
성공 사례도 확인됨:
10:45:56 KST — RefinementService::run | end (info, 성공)
10:24:39 KST — RefinementService::run | end (info, 성공)
10:04:48 KST — RefinementService::run | end (info, 성공)
copyAlignmentToEfs 실패 로그 원문 (raw):
{
"message": "RefinementService::copyAlignmentToEfs | not found refined align_preview_meta_refinement_with_prior_map.json",
"status": "warn",
"timestamp": "2026-04-20T02:00:21.359Z",
"capture": { "id": 69026 },
"job": { "id": 178801 },
"team": { "domain": "tgsp-jv", "id": 155 },
"region": "ap-southeast-2",
"user": { "id": 5893, "email": "hongphuong.le@tgspjv.com" }
}
loadCluster 실패 로그 원문 (raw):
{
"message": "RefinementService::loadCluster | end - not found selected_unrefined_cluster_id",
"status": "error",
"timestamp": "2026-04-20T01:48:34.927Z",
"capture": { "id": 69024 },
"job": { "id": 178802 },
"team": { "domain": "tgsp-jv", "id": 155 },
"region": "ap-southeast-2",
"user": { "id": 5893, "email": "hongphuong.le@tgspjv.com" }
}
모든 에러가 동일 팀(tgsp-jv, team 155), 동일 지역(ap-southeast-2), 동일 사용자(5893)에서 발생했다. 같은 시간대에 성공 사례도 있으므로 서비스 전체 장애는 아니고 특정 capture 데이터 조건에 따른 실패이다.
Fix Recommendation#
즉시 조치 (Critical)#
refinement-service.ts:108:JSON.stringify(error)대신 에러 메시지를 직접 로깅하도록 변경.error instanceof Error ? error.message : JSON.stringify(error)또는error?.message ?? JSON.stringify(error)패턴 사용. 현재{}만 출력되어 실제 실패 원인 파악이 불가능하다.- 같은 파일의 다른 catch 블록(
authenticate등)도 동일한JSON.stringify(error)패턴을 사용하고 있어 동일하게 수정 필요.
단기 개선 (1주 이내)#
- scenemapper 출력 검증 강화:
runRefinement()완료 후,copyAlignmentToEfs()진입 전에 결과 디렉토리의 파일 목록을 info 레벨로 로깅하여, scenemapper가 어떤 파일을 생성했는지(또는 생성하지 못했는지) 즉시 확인할 수 있도록 한다. loadCluster실패 시 capture ID 로깅:refinement-service.ts:179에서cpCapture.id를 포함하여 어떤 capture에서selected_unrefined_cluster_id가 누락되었는지 추적할 수 있도록 한다.- Tesla 측
selected_unrefined_cluster_id설정 타이밍 검증:reset_refinement(capture_invoker.rb:312)이 호출된 후 refinement job이 다시 시작되는 flow에서,selected_unrefined_cluster_id가 재설정되기 전에 job이 실행될 수 있는 race condition이 있는지 확인 필요.
장기 개선 (재발 방지)#
- scenemapper 바이너리의 exit code나 stdout/stderr를 파싱하여, 출력 파일 미생성 시 구체적인 실패 원인(입력 데이터 문제, 메모리 부족 등)을 로그에 기록하는 구조 도입.
ChildProcessManager에서 프로세스 종료 코드와 stderr를 캡처하여ScenemapperManager::execute에서 활용할 수 있도록 개선.
Monitoring#
- scenemapper refinement 실패율 추적:
service:cupixworks-capture-refinement-arm-instance "copyAlignmentToEfs" "not found refined"
selected_unrefined_cluster_id누락 모니터링:
service:cupixworks-capture-refinement-arm-instance "loadCluster" "not found selected_unrefined_cluster_id"
- 전체 refinement 성공/실패 비율:
service:cupixworks-capture-refinement-arm-instance "run | end"
Risk Assessment#
- Risk level: medium
- 예상 복잡도: standard
- 에러 로깅 개선(
JSON.stringify→error.message)은 trivial 수준이지만, scenemapper 출력 파일 미생성의 근본 원인은 바이너리 내부 로직에 의존하므로 추가 조사 필요.selected_unrefined_cluster_id누락은 Tesla API 측 race condition 가능성이 있어 양쪽 코드 확인이 필요하다.