ES /docs

PreprocessorService::run | end - {"stack":"AbortError: The operation was aborted

RCA: PreprocessorService::run AbortError (Process timeout)

Overview#

What Happened#

2026-06-11 22:23:18 KST에 cupixworks-pix-genie-preprocessor-instance 서비스(capture 709953 처리 작업)에서 spawn된 Python child process(/tmp/lib/pixgenie/pixgenie/scripts/docker_entrypoint.py)가 정확히 2시간 후 AbortSignal.timeout에 의해 abort되며 AbortError로 종료되었다. 단일 occurrence이며, 동일 instance에서 그 이전·이후의 다른 capture 처리는 정상적으로 PreprocessorService::run | end info log로 마무리되었다.

Quick Facts#

Field Value
exception.class AbortError
exception.message The operation was aborted (cause: Process timeout)
exception.code ABORT_ERR
top_frame /tmp/agent/dist/app.cjs:6613:31pixgenie.process.ts:60 (this._abortController!.abort(new Error('Process timeout')))
timeout config TIMEOUT_MS = 2 * 60 * 60 * 1000 (2 hours)
runtime Node.js (child_process spawn → python3)
env production, us-west-2, tenant cupix
capture_id 709953 (uncertain — inferred from preceding PixGenieProcess::execute | begin - captureId: 709953 log at 20:23:17 KST on the same instance)

Affected Teams#

Team / Domain Error Count Impact
cana / pix-genie preprocessor 1 1 capture(추정 id 709953)의 Pix Genie 전처리 작업이 error 상태로 마킹됨. capture는 pix_genie_state = error로 전이, 사용자는 해당 capture의 Pix Genie 결과를 사용할 수 없다. 동일 시간대의 다른 capture 처리는 영향 없음.

Timeline#

  1. 2026-06-11 20:23:16 KSTPreprocessorService::run | begin (info log, same instance)
  2. 2026-06-11 20:23:17 KSTPixGenieProcess::execute | begin - captureId: 709953 (Python child process spawn, 2시간 timeout 등록)
  3. 2026-06-11 22:23:17 KST — 정확히 2시간 경과, timeoutSignal fire → PixGenieProcess | process timeout, aborting... (warn log) → abortController.abort(new Error('Process timeout'))
  4. 2026-06-11 22:23:17 KST — Node child_process 모듈이 abort signal에 반응해 abortChildProcess를 호출, AbortError reject로 promise 거부 → PixGenieProcess::execute | failed - {"code":"ABORT_ERR",...} (error log)
  5. 2026-06-11 22:23:18 KSTPreprocessorService::run catch block에서 pix_genie_state = error로 업데이트, 본 cluster의 error log 발생: PreprocessorService::run | end - {"stack":"AbortError: ..."}. 이어 finally block에서 updateJobState(stopped) 호출, info PreprocessorService::run | end 출력.

Error Log#

Datadog Logs

text
PreprocessorService::run | end - {"stack":"AbortError: The operation was aborted
    at abortChildProcess (node:child_process:725:27)
    at EventTarget.onAbortListener (node:child_process:795:7)
    at [nodejs.internal.kHybridDispatch] (node:internal/event_target:831:20)
    at EventTarget.dispatchEvent (node:internal/event_target:766:26)
    at abortSignal (node:internal/abort_controller:370:10)
    at AbortController.abort (node:internal/abort_controller:392:5)
    at timeoutSignal.addEventListener.once (/tmp/agent/dist/app.cjs:6613:31)
    at [nodejs.internal.kHybridDispatch] (node:internal/event_target:831:20)
    at EventTarget.dispatchEvent (node:internal/event_target:766:26)
    at abortSignal (node:internal/abort_controller:370:10)","message":"The operation was aborted","cause":{"stack":"Error: Process timeout
    at timeoutSignal.addEventListener.once (/tmp/agent/dist/app.cjs:6613:37)
    at [nodejs.internal.kHybridDispatch] (node:internal/event_target:831:20)
    at EventTarget.dispatchEvent (node:internal/event_target:766:26)
    at abortSignal (node:internal/abort_controller:370:10)
    at Timeout._onTimeout (node:internal/abort_controller:127:7)
    at listOnTimeout (node:internal/timers:581:17)
    at process.processTimers (node:internal/timers:519:7)","message":"Process timeout","name":"Error"},"code":"ABORT_ERR","name":"AbortError"}

Impact#

  • Service: cupixworks-pix-genie-preprocessor-instance
  • Team: cana
  • 발생 횟수: 1
  • 최초 발생: 2026-06-11 22:23:18 KST
  • 최근 발생: 2026-06-11 22:23:18 KST

Root Cause Summary#

PixGenieProcess.runPythonProcess가 spawn한 Python child process(docker_entrypoint.py)가 하드코딩된 2시간(TIMEOUT_MS = 2 * 60 * 60 * 1000) 안에 종료되지 못했고, AbortSignal.timeout가 등록한 abort handler가 발화하여 abortController.abort(new Error('Process timeout'))로 child process를 강제 종료했다. Node.js child_process 모듈이 abort signal을 받아 자식 프로세스를 kill하면서 spawn promise가 AbortError(code: ABORT_ERR, cause: Error('Process timeout'))로 reject되었고, PreprocessorService::run의 catch block에서 그대로 JSON.stringify 직렬화되어 error log로 출력되었다. 즉, 본 에러는 코드 결함이 아니라 "2시간 SLA를 초과한 capture 처리"라는 운영 시나리오를 그대로 error로 표면화한 것이다.

Technical Analysis#

Code Path#

  • Entry point: applications/agents/packages/cupix-pix-genie-preprocessor-agent/src/preprocessor-service.ts:66 (PreprocessorService::run)
  • 호출 체인:
    • preprocessor-service.ts:93PixGenieManager.executePixGenieProcessing(cpCapture)
    • pix-genie-manager.ts:75PixGenieProcess.execute(processParams)
    • pixgenie.process.ts:33runPythonProcess(params) (Python 자식 프로세스 spawn)
  • Failure point: pixgenie.process.ts:56-61AbortSignal.timeout(2h) 이 발화하면 즉시 abortController.abort(new Error('Process timeout')) 가 실행되어 spawn 옵션의 signal(line 69) 을 통해 Node 가 자식 프로세스를 kill 하고 promise 를 AbortError 로 reject.
applications/agents/packages/cupix-pix-genie-preprocessor-agent/src/process/pixgenie.process.ts:18-20typescript
private _abortController?: AbortController;
private readonly TIMEOUT_MS = 2 * 60 * 60 * 1000; // 2 hours
private static readonly PIXGENIE_LIB_PATH = '/tmp/lib/pixgenie';
applications/agents/packages/cupix-pix-genie-preprocessor-agent/src/process/pixgenie.process.ts:52-73typescript
private runPythonProcess(params: PixGenieProcessParams): Promise<void> {
    return new Promise((resolve, reject) => {
        this._abortController = new AbortController();

        const timeoutSignal = AbortSignal.timeout(this.TIMEOUT_MS);

        timeoutSignal.addEventListener('abort', () => {
            logger.warn('PixGenieProcess | process timeout, aborting...');
            this._abortController!.abort(new Error('Process timeout'));
        }, { once: true });

        logger.debug('PixGenieProcess::runPythonProcess | executing: python3 %s', PixGenieProcess.PIXGENIE_SCRIPT_PATH);

        this._process = spawn('python3', [PixGenieProcess.PIXGENIE_SCRIPT_PATH], {
            env: { ...process.env, ...this.buildEnvironment(params) },
            stdio: ['ignore', 'pipe', 'pipe'],
            cwd: params.workspaceDir,
            signal: this._abortController.signal
        });

        this.setupProcessHandlers(resolve, reject);
    });
}
applications/agents/packages/cupix-pix-genie-preprocessor-agent/src/preprocessor-service.ts:100-106typescript
} catch (error) {
    await this.pixGenieManager.updatePixGenieState(captureId!, TESLA.PixGenieState.Error);
    logger.error('PreprocessorService::run | end - %s', stringifyError(error));
} finally {
    await this.pixGenieManager.updateJobState(jobId!, TESLA.UpdateJobRequest.StateEnum.Stopped);
    logger.info('PreprocessorService::run | end');
}

기대 동작 vs 실제 동작:

  • 기대: Python child process가 2시간 안에 SQLite 결과 파일을 생성하고 code === 0로 종료(pixgenie.process.ts:108-109).
  • 실제: 2시간이 경과해도 종료하지 않아 timeout signal 이 발화, child process 가 kill 되어 AbortError 로 reject. 결과 SQLite 파일은 생성되지 않고 capture 의 pix_genie_stateerror 로 마킹됨.

또한 setupProcessHandlerserror 핸들러가 그대로 reject 하기 때문에(pixgenie.process.ts:115-118), 자식 프로세스의 stdout/stderr 진행 상황(어디까지 처리되었는지) 정보가 error log 의 cause 에는 누락되어 있다. capture id 도 root error 메시지에는 포함되지 않아 어떤 capture 가 실패했는지 별도 시간 정합으로 추적해야 한다(아래 Log Evidence 참고).

Log Evidence#

Datadog query (재현용):

text
service:cupixworks-pix-genie-preprocessor-instance status:error @environment:production "PreprocessorService::run"

보조 query (begin/end pair, capture id 식별):

text
service:cupixworks-pix-genie-preprocessor-instance ("PreprocessorService::run | begin" OR "PixGenieProcess::execute | begin" OR "process timeout" OR "PreprocessorService::run | end")

핵심 로그 시퀀스(시간순):

text
2026-06-11 20:23:16 KST  info   PreprocessorService::run | begin
2026-06-11 20:23:17 KST  info   PixGenieProcess::execute | begin - captureId: 709953
2026-06-11 22:23:17 KST  warn   PixGenieProcess | process timeout, aborting...
2026-06-11 22:23:17 KST  error  PixGenieProcess | process error: The operation was aborted
2026-06-11 22:23:17 KST  error  PixGenieProcess::execute | failed - {"code":"ABORT_ERR","name":"AbortError"}
2026-06-11 22:23:18 KST  error  PreprocessorService::run | end - {"stack":"AbortError: ...","cause":{...,"message":"Process timeout"},...}
2026-06-11 22:23:18 KST  info   PreprocessorService::run | end

PixGenieProcess::execute | begin (20:23:17 KST) 와 process timeout (22:23:17 KST) 사이의 간격이 정확히 2 시간 0 분 0 초 — 즉 코드의 TIMEOUT_MS 와 일치한다. 이는 자연 종료 또는 다른 종류의 실패가 아니라 프로세스가 진행 중인 상태에서 timeout 한도에 도달해 강제 종료되었다는 결정적 증거다.

동일 시간대의 다른 instance 비교(정상 종료 사례, 동 24시간 내):

text
2026-06-11 21:40:49 KST  info  PreprocessorService::run | end   (정상)
2026-06-11 21:54:52 KST  info  PreprocessorService::run | end   (정상)
2026-06-11 20:35:57 KST  info  PreprocessorService::run | end   (정상)

→ pix-genie preprocessor 자체는 동일 환경에서 다른 capture 들에 대해 정상 동작 중. 본 인시던트는 특정 capture(추정 id 709953) 의 처리 시간이 2 시간을 초과한 단일 케이스다.

이전 24 시간 동안 동일 fingerprint 의 timeout 발생 횟수: 1회(본 occurrence). 동일 service 의 다른 에러는 모두 별개 fingerprint(HttpError, ARG10000 등).

Hypotheses Considered#

# Hypothesis Evidence for Evidence against Verdict
H1 Python child process가 2시간 hardcoded timeout(TIMEOUT_MS = 2 * 60 * 60 * 1000)을 초과하여 AbortSignal.timeout에 의해 강제 abort됨 (a) PixGenieProcess::execute | begin 20:23:17 KST 와 process timeout, aborting... 22:23:17 KST 사이 정확히 2h 경과. (b) error log 의 cause.message = "Process timeout", stack 의 at Timeout._onTimeout 이 코드 pixgenie.process.ts:58-61timeoutSignal.addEventListener('abort', ...) 와 일치. (c) code: ABORT_ERR, child_process 의 signal 옵션을 통한 abort 경로(abortChildProcess at node:child_process:725:27) 가 stack 에 명시. Confirmed
H2 Tesla API(capture/job update)가 응답하지 못해 hang 됨 없음. catch block 의 updatePixGenieState 는 timeout 이후 호출되며, log 상 그 직후 PreprocessorService::run | end info 가 정상 출력됨. error 의 causeProcess timeout 이지 HttpError 가 아님. 동일 instance 의 finally updateJobState 호출이 같은 초에 완료되었음(시간 간격 0초). Rejected
H3 PixGenieProcess 인스턴스가 재사용되어 이전 abort controller / signal 누수가 timeout 을 잘못 트리거 PixGenieManager_pixGenieProcess 를 한 번만 생성(pix-genie-manager.ts:23) 하므로 이론적으로 가능 본 instance 에서 단 1 capture(709953) 만 처리 중이었고(PreprocessorService::run | begin 1회), runPythonProcess 는 매 호출마다 새 AbortController/AbortSignal.timeout 을 생성(pixgenie.process.ts:54-56). Rejected
H4 Python 스크립트가 무한 루프 / deadlock 에 빠짐 (H1 의 하위 원인) timeout 한도까지 child process 가 alive 상태였다는 점은 일관 본 cluster 자체로는 stdout/stderr 가 caching 안되어(error 객체에는 stderr 포함되지 않음) Python 측 행 위치를 확인할 수 없음 — 별도 debug log / Watch (Kibana) 조사가 필요함. Inconclusive — needs verification

Fix Recommendation#

즉시 조치 (Critical)#

특별한 즉시 조치는 권장하지 않는다. 단일 capture 가 2 시간을 초과하여 강제 종료된 1 회 이벤트이며, capture 의 pix_genie_state 도 정상적으로 error 로 전이되었다(preprocessor-service.ts:101). 동일 instance 의 다른 capture 처리는 정상이다. 사용자가 동일 capture(추정 id 709953) 를 재처리하면 회복 가능하다.

운영팀은 capture 709953 이 실제로 어떤 데이터(panorama 수, 해상도, BIM 사용 여부 등)를 가지고 있어 2 시간을 초과했는지 확인하고, 단순 재시도가 안전한지 판단할 것을 권장한다.

단기 개선 (1주 이내)#

  1. 에러 로그 컨텍스트 보강pixgenie.process.ts:36-39 의 catch block 에서 현재는 JSON.stringify(error) 만 남기고 있어 (a) captureId, (b) 경과 시간, (c) stderr 누적값(setupProcessHandlersstderr 변수는 close handler 에만 전달됨) 이 모두 누락된다. timeout 경로에서도 captureIdelapsed_ms 를 포함하도록 로깅을 보강하여 추후 동일 timeout 이 다시 일어났을 때 어떤 capture 가 영향을 받았는지 cluster 만으로 즉시 파악할 수 있게 한다.
  2. Timeout 발화 시 stderr 캡처setupProcessHandlers 가 누적한 stderrrunPythonProcessreject 분기(특히 abort 케이스) 에서도 함께 reject 페이로드에 포함시키면 H4(Python 스크립트 무한 루프 vs 정상 진행 중 시간 초과) 를 사후 가설로 분리 가능. 다만 이는 RCA 의 fix 범위가 아닌 다음 인시던트 대비용 개선 권고.

장기 개선 (재발 방지)#

  1. Timeout 의 환경변수화 — 현재 TIMEOUT_MS 가 코드에 하드코딩되어 있어 capture 크기/리전별로 다른 SLA 가 필요할 때 코드 배포가 필수다. 환경변수(PIXGENIE_PROCESS_TIMEOUT_MS 등) 로 분리하면 운영 중 조정 가능. pixgenie.process.ts:18 한 줄 변경 + config/environment.ts 엔트리 추가 수준의 작업.
  2. 장시간 capture 사전 진단 — pano 개수 / 이미지 크기로 처리 시간 예측 메트릭을 발행하고, 2 시간 SLA 초과가 의심되는 capture 는 큐 단계에서 분리 처리. (ops 결정 영역, 본 cluster 만으로 ROI 판단 어려움 — Inconclusive 권고.)

Monitoring#

다음 Datadog 쿼리들은 release dashboard timeseries widget 에 그대로 사용 가능하다. monitor-only 문법(| stats, count by(...)) 은 사용하지 않았다.

1. Pix Genie process timeout 발생 추이

text
service:cupixworks-pix-genie-preprocessor-instance "process timeout, aborting..."

2. PreprocessorService::run 단의 AbortError 발생 추이

text
service:cupixworks-pix-genie-preprocessor-instance status:error "AbortError"

3. 전체 PreprocessorService::run 에러 추이 (HttpError 등 포함, 비교용)

text
service:cupixworks-pix-genie-preprocessor-instance status:error "PreprocessorService::run | end"

4. 정상 종료 대비 (분모용)

text
service:cupixworks-pix-genie-preprocessor-instance "PixGenieProcess::execute | begin"

운영 팀이 임계치 알림을 추가한다면, 동일 24 시간 윈도우에서 process timeout, aborting... 발생 횟수가 1 건을 초과하는 경우(즉 산발적 단일 occurrence 가 아닌 패턴) 알림이 적절하다. 단일 occurrence 알림은 false-positive 비용이 더 클 가능성이 있다.

Risk Assessment#

  • Risk level: low — 단일 occurrence, 동일 fingerprint 24h 내 1 건. 다른 capture 처리는 정상. capture state 가 error 로 전이되어 사용자가 재처리할 수 있는 상태.
  • 예상 복잡도: trivial (즉시 코드 변경 권고 없음. 단기 로깅 보강은 standard 수준의 1-2줄 변경.)