ES /docs

RefinementService::run | end - {}

RCA: RefinementService::run | end - {}

Error Log#

Datadog Logs

text
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:54new RefinementService().init() 호출
  • refinement-service.ts:43-48init()authenticate()run()terminateService() 순서로 실행
typescript
// 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-111run() 메서드가 job 로드 후 capture 처리 파이프라인 실행. try-catch에서 모든 에러를 JSON.stringify(error)로 로깅
typescript
// 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-97runRefinement() 실행 후 copyAlignmentToEfs() 호출
  • refinement-service.ts:370-382align_preview_meta_refinement_with_prior_map.json 파일 크기가 0이거나 존재하지 않으면 throw
typescript
// 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
typescript
// 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-323reset_refinement 메서드가 selected_unrefined_cluster_id = nil로 초기화하며, capture.rb:270-278update_selected_unrefined_cluster_id에서만 이 값이 설정된다. Capture 69024는 refinement job이 시작되었으나 selected_unrefined_cluster_id가 설정되지 않은 채 또는 reset된 후 job이 실행된 경우이다.

Log Evidence#

사용한 Datadog 쿼리:

text
service:cupixworks-capture-refinement-arm-instance status:error "RefinementService::run"
text
service:cupixworks-capture-refinement-arm-instance "copyAlignmentToEfs" "not found refined"
text
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

성공 사례도 확인됨:

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

json
{
  "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):

json
{
  "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 실패율 추적:
text
service:cupixworks-capture-refinement-arm-instance "copyAlignmentToEfs" "not found refined"
  • selected_unrefined_cluster_id 누락 모니터링:
text
service:cupixworks-capture-refinement-arm-instance "loadCluster" "not found selected_unrefined_cluster_id"
  • 전체 refinement 성공/실패 비율:
text
service:cupixworks-capture-refinement-arm-instance "run | end"

Risk Assessment#

  • Risk level: medium
  • 예상 복잡도: standard
  • 에러 로깅 개선(JSON.stringifyerror.message)은 trivial 수준이지만, scenemapper 출력 파일 미생성의 근본 원인은 바이너리 내부 로직에 의존하므로 추가 조사 필요. selected_unrefined_cluster_id 누락은 Tesla API 측 race condition 가능성이 있어 양쪽 코드 확인이 필요하다.