ES /docs

ThreeDReconstruction::runThreeDReconstruction | Capture ID: 33815 | Error: {}

RCA: ThreeDReconstruction::runThreeDReconstruction | Capture ID: 33815 | Error: {}

Overview#

What Happened#

2026-04-20 23:13:59 UTC에 cupixworks-capture-3dreconstruction-instance 서비스에서 Capture 33815에 대한 densemapper 실행이 exit code 1로 실패했다. 에러 메시지가 Error: {}로 기록되어 실제 실패 원인이 로그에서 완전히 소실되었다. 이는 densemapper 바이너리 crash와 JSON.stringify(error) 직렬화 버그가 결합된 복합 장애이다.

Quick Facts#

Field Value
exception.class Error
exception.message Failed to execute densemapper for capture ID: 33815 - error: {}
top_frame ThreeDReconstruction.runThreeDReconstruction (app.cjs:8318)
runtime Node.js (bundled as app.cjs)
env production, eu-central-1

Timeline#

  1. 2026-04-20T22:06:31Z — ThreeDReconstruction::init 시작, 인증 및 데이터 로드 (Job 90769, Capture 33815, 1 video, 1 cluster)
  2. 2026-04-20T22:06:48Z — densemapper 실행 시작 (domain: innovo, region: us-west-2)
  3. 2026-04-20T23:13:59.288Z — densemapper 자식 프로세스가 exit code 1로 종료
  4. 2026-04-20T23:13:59.289ZrunThreeDReconstruction catch 블록에서 Error: {} 로그 기록
  5. 2026-04-20T23:13:59.290ZFailed to execute densemapper 에러 throw
  6. 2026-04-20T23:13:59.696Z — 서비스 force shutdown (10초 타이머)

Error Log#

Datadog Logs

text
ThreeDReconstruction::runThreeDReconstruction | Capture ID: 33815 | Error: {}

Impact#

  • Service: cupixworks-capture-3dreconstruction-instance
  • Team: innovo
  • 발생 횟수: 1
  • 최초 발생: 2026-04-20T23:13:59.289Z
  • 최근 발생: 2026-04-20T23:13:59.289Z

Capture 33815 (team domain: innovo, user: bot@fieldai.com)의 3D 재구성 처리가 실패했다. 동일 시간대에 같은 팀의 Capture 33814는 ETIMEDOUT, Capture 33817은 "not found cpc files" 에러가 발생하여 innovo 팀의 EU 리전 capture 처리 전반에 문제가 있었을 가능성이 있다.

Root Cause Summary#

densemapper 네이티브 바이너리가 ~67분간 실행 후 exit code 1로 비정상 종료했다. 바이너리의 stdout/stderr 출력이 Datadog에 전달되지 않아 정확한 crash 원인은 확인 불가하다. 한편 runThreeDReconstruction 메서드의 catch 블록에서 JSON.stringify(error)로 Error 객체를 직렬화하는데, JavaScript Error 객체의 messagestack 프로퍼티는 non-enumerable이므로 JSON.stringify()가 항상 {}를 반환한다. 이로 인해 "Process terminated with non-zero code: 1"이라는 실제 에러 메시지가 로그에서 완전히 소실되었다.

Technical Analysis#

Code Path#

1. Entry point — densemapper 실행 요청

ThreeDReconstructorManager.execute()ChildProcessManager를 통해 forked 자식 프로세스(ThreeDReconstructionProcess)에 execute 메시지를 전송한다.

applications/agents/packages/cupix-capture-3d-reconstruction-agent/src/manager/three-d-reconstructor.manager.ts:32-49typescript
async execute(params: ThreeDReconstructorParams): Promise<void> {
    logger.debug('ThreeDReconstructorManager::execute | begin');

    try {
        await this.initialize();
        await this.childProcessManager.execute('execute', params);
        logger.debug('ThreeDReconstructorManager::execute | completed successfully');
    } catch (error) {
        logger.error('ThreeDReconstructorManager::execute | error:', error);

        const errorCode = ErrorCode.Densemapper.Default;

        if (this.setJobErrorCode) {
            this.setJobErrorCode(errorCode);
        }

        throw error;
    }
}

2. 자식 프로세스에서 densemapper 바이너리 spawn

forked 자식 프로세스 내에서 child_process.spawn()으로 densemapper 쉘 스크립트를 실행한다. 바이너리가 exit code 1로 종료하면 Error 객체를 reject한다.

applications/agents/packages/cupix-capture-3d-reconstruction-agent/src/process/three-d-reconstruction.process.ts:59-71typescript
_spawn.on('exit', (code, signal) => {
    if (code === null) {
        const errorMessage = `Process terminated unexpectedly by signal: ${signal}`;
        this.log(`ThreeDReconstructionProcess::execute | ${errorMessage}`);
        reject(new Error(errorMessage));
    } else if (code !== 0) {
        const errorMessage = `Process terminated with non-zero code: ${code}`;
        this.log(`ThreeDReconstructionProcess::execute | ${errorMessage}`);
        reject(new Error(errorMessage));
    } else {
        this.log('ThreeDReconstructionProcess::execute | 3D Reconstruction processing completed successfully');
        resolve({ success: true });
    }
});

3. BaseProcess가 에러를 IPC로 부모에게 전달

reject된 에러가 BaseProcess.handleMessage() catch 블록에서 포착되어 sendError()를 통해 부모 프로세스로 전달된다. 이 시점에서 error.message가 정상적으로 추출된다.

applications/agents/packages/base/src/process/base.process.ts:56-58typescript
protected sendError(id: string, error: unknown): void {
    const errorMessage = error instanceof Error ? error.message : String(error);
    this.sendResponse(id, '', false, undefined, errorMessage);
}

4. ChildProcessManager가 에러를 재구성

부모 프로세스의 ChildProcessManager.handleMessage()에서 response.error 문자열로부터 새 Error 객체를 생성한다.

applications/agents/packages/base/src/manager/child-process.manager.ts:179-183typescript
if (response.success) {
    pending.resolve(response.data);
} else {
    pending.reject(new Error(response.error || 'Unknown error'));
}

5. Failure point — JSON.stringify(error) 직렬화 버그

catch 블록에서 JSON.stringify(error)로 Error 객체를 직렬화한다. JavaScript Error 객체의 message, stack 프로퍼티는 non-enumerable이므로 JSON.stringify()는 항상 {}를 반환한다.

applications/agents/packages/cupix-capture-3d-reconstruction-agent/src/three-d-reconstruction-service.ts:321-324typescript
} catch (error: any) {
    logger.error(`ThreeDReconstruction::runThreeDReconstruction | Capture ID: ${cpCapture.id} | Error: %s`, JSON.stringify(error));
    this.jobManager.setErrorCode(ErrorCode.Densemapper.Execute);
    throw new Error(`Failed to execute densemapper for capture ID: ${cpCapture.id} - error: ${JSON.stringify(error)}`);
}

기대 동작: Error: Process terminated with non-zero code: 1이 로그에 기록되어야 한다. 실제 동작: Error: {}가 로그에 기록되어 원래 에러 메시지가 완전히 소실되었다.

동일한 패턴이 authenticate 메서드에도 존재한다:

applications/agents/packages/cupix-capture-3d-reconstruction-agent/src/three-d-reconstruction-service.ts:94-96typescript
} catch (error) {
    logger.error('ThreeDReconstruction::authenticate | end - %s', JSON.stringify(error));
}

Log Evidence#

Datadog에서 Capture 33815 관련 전체 실행 흐름을 추적했다.

사용한 쿼리:

text
service:cupixworks-capture-3dreconstruction-instance @capture.id:33815

실행 흐름 로그 (info level):

text
2026-04-20T22:06:31.276Z [info] ThreeDReconstruction::init
2026-04-20T22:06:31.276Z [info] ThreeDReconstruction::authenticate | begin
2026-04-20T22:06:31.332Z [info] CupixAuth::setSession | session_id: 5e55e307418aaf453d02bd50510f782a02e817e7
2026-04-20T22:06:31.333Z [info] ThreeDReconstruction::authenticate | end
2026-04-20T22:06:31.333Z [info] ThreeDReconstruction::run | begin
2026-04-20T22:06:31.334Z [info] JobManager::loadJob | begin - job id: 90769
2026-04-20T22:06:31.382Z [info] JobManager::loadJob | end - job id: 90769
2026-04-20T22:06:31.783Z [info] ThreeDReconstruction::loadVideos | video count: 1
2026-04-20T22:06:31.841Z [info] ThreeDReconstruction::loadClusters | cluster count: 1
2026-04-20T22:06:48.167Z [info] ThreeDReconstruction::runThreeDReconstruction environments | domain: innovo, envName: production, launchMode: CUPIXWORKS, region: us-west-2, userEmail: undefined

67분간의 로그 공백 — 22:06:48 ~ 23:13:59 사이 densemapper 바이너리 실행 중 로그 없음.

에러 로그 시퀀스 (3개, 1ms 간격):

text
2026-04-20T23:13:59.288Z [error] Process terminated with non-zero code: 1
  → Stack: ChildProcessManager3.handleMessage (app.cjs:5219)

2026-04-20T23:13:59.289Z [error] ThreeDReconstruction::runThreeDReconstruction | Capture ID: 33815 | Error: {}

2026-04-20T23:13:59.290Z [error] Failed to execute densemapper for capture ID: 33815 - error: {}
  → Stack: ThreeDReconstruction.runThreeDReconstruction (app.cjs:8318)
         ThreeDReconstruction.run (app.cjs:8142)
         ThreeDReconstruction.init (app.cjs:8093)

동일 팀의 동시간대 에러:

text
service:cupixworks-capture-3dreconstruction-instance (status:error OR status:warn)
text
2026-04-20T22:55:49Z [error] Capture 33817 — not found cpc files (host: 9990219feaf0)
2026-04-20T23:13:59Z [error] Capture 33815 — densemapper exit code 1 (host: 6d9c6457c9b1)
2026-04-20T23:39:59Z [error] Capture 33814 — read ETIMEDOUT during upload (host: 7ed35fad7d06)

innovo 팀(bot@fieldai.com)의 EU 리전 capture 3건이 모두 다른 원인으로 실패했다.

Hypotheses Considered#

# Hypothesis Evidence for Evidence against Verdict
H1 densemapper 바이너리가 GPU/메모리 자원 부족 또는 입력 데이터 문제로 crash exit code 1로 비정상 종료, 67분 실행 후 실패 (정상 처리 시간 범위 내이나 처리 중 crash), 동일 팀 다른 capture도 실패 densemapper stdout/stderr가 Datadog에 전달되지 않아 직접 증거 불충분 Inconclusive
H2 JSON.stringify(error) 직렬화 버그로 에러 메시지 소실 three-d-reconstruction-service.ts:322에서 JSON.stringify(error) 사용, JS Error 객체는 non-enumerable properties로 {} 반환, 로그에 Error: {} 기록 확인 Confirmed
H3 네트워크 이슈 (cross-region 통신 문제) EU 리전 인스턴스에서 region: us-west-2 설정 사용, Capture 33814가 ETIMEDOUT 발생 densemapper가 67분 실행 후 실패한 것은 네트워크 초기 연결 실패와 다름, exit code 1은 프로세스 자체 에러를 시사 Rejected
H4 ChildProcessManager IPC 통신 실패 BaseProcess.sendError()error.message를 정상 추출하여 IPC로 전송, ChildProcessManager.handleMessage()가 문자열 에러를 수신하여 new Error()로 재구성 — IPC 경로는 정상 동작, 문제는 최종 직렬화 단계 Rejected

Fix Recommendation#

즉시 조치 (Critical)#

three-d-reconstruction-service.ts:322three-d-reconstruction-service.ts:324에서 JSON.stringify(error) 대신 error instanceof Error ? error.message : JSON.stringify(error) 또는 error instanceof Error ? error.stack : String(error) 패턴을 사용하도록 수정한다. 동일 파일 line 95authenticate 메서드에도 같은 패턴이 있으므로 함께 수정한다.

이 변경으로 densemapper 실패 시 "Process terminated with non-zero code: 1"과 같은 실제 에러 메시지가 로그에 기록되어 디버깅이 가능해진다.

단기 개선 (1주 이내)#

ThreeDReconstructionProcess.execute() (three-d-reconstruction.process.ts:74-75)에서 densemapper의 stdout/stderr를 this.log()로 전달하고 있지만, 이 로그가 Datadog에 효과적으로 전달되는지 확인이 필요하다. densemapper 바이너리의 stderr 출력이 crash 원인 파악에 핵심적이므로, 해당 출력이 구조화된 로그로 Datadog에 저장되도록 보장해야 한다.

장기 개선 (재발 방지)#

  1. 에이전트 코드베이스 전체에서 JSON.stringify(error) 패턴을 검색하여 일괄 수정한다. 공통 유틸리티 함수(예: serializeError(error))를 만들어 Error 객체의 message, stack, 커스텀 프로퍼티를 안전하게 직렬화하도록 한다.
  2. densemapper 바이너리의 exit code별 의미를 문서화하고, 가능하다면 결과 디렉토리의 로그 파일(log.json)을 실패 시에도 수집하여 Datadog에 전달하는 메커니즘을 추가한다.

Monitoring#

  • densemapper 실패 빈도 추적:
text
service:cupixworks-capture-3dreconstruction-instance status:error "Failed to execute densemapper"
  • Error: {} 패턴 잔존 여부 모니터링 (수정 후 0건이어야 함):
text
service:cupixworks-capture-3dreconstruction-instance "Error: {}"
  • innovo 팀 EU 리전 capture 처리 실패율:
text
service:cupixworks-capture-3dreconstruction-instance status:error @team.domain:innovo

Risk Assessment#

  • Risk level: medium
  • 예상 복잡도: standard — JSON.stringify(error) 수정은 trivial하지만, densemapper crash의 근본 원인은 바이너리 로그 수집 개선 후에야 파악 가능하다.