ES /docs

ChildProcessManager::setupEventHandlers | Child process stderr: 7: 0x1112e9c v8::internal::Heap::Co

RCA: ChildProcessManager child process stderr — V8 heap OOM crash

Overview#

What Happened#

2026-07-21 21:00:39 KST 에 cupixworks-any-bimrevision-agent 의 BIM 비교 child process 가 V8 heap OOM 으로 crash 했다. Node 가 FATAL ERROR: Ineffective mark-compacts near heap limit Allocation failed - JavaScript heap out of memory 를 stderr 로 출력하고 native stack trace 를 뱉으며 SIGABRT 로 종료했다. ChildProcessManager::setupEventHandlersstderr handler 가 각 stack frame 라인을 logger.error 로 개별 기록해, 하나의 crash 가 error 레벨 로그 다수와 25개 fingerprint 클러스터로 확산되었다. 대표 클러스터 는 V8 heap CollectGarbage 프레임 라인 (3b141d70-...) 이다.

Quick Facts#

Field Value
exception.class (child process fatal — no JS exception; V8 FATAL ERROR)
exception.message FATAL ERROR: Ineffective mark-compacts near heap limit Allocation failed - JavaScript heap out of memory
top_frame packages/base/src/manager/child-process.manager.ts:138-141 (stderr handler in parent)
runtime Node.js (V8 heap ceiling ≈ 8 GB, observed Mark-Compact 7916.1 (8233.8) -> 7916.1 (8233.8) MB)
env production, us-west-2, tenant cupix

Affected Teams#

Team / Domain Error Count Impact
clark-vdc (cupixworks-any-bimrevision-agent) 25 clusters (single crash event) 해당 BIM revision 작업 1 건 BimComparisonState=Error 로 마감. child process SIGABRT 로 즉시 종료.

Timeline#

  1. 2026-07-21 21:00:39.174 KST — child process 가 <--- Last few GCs ---> 를 출력. 직전 Mark-Compact 후 7909.6 -> 7909.6 MB (거의 회수 안 됨, average mu = 0.091).
  2. 2026-07-21 21:00:39.180 KST — 두 번째 Mark-Compact 7916.1 -> 7916.1 MB (current mu = 0.006) — GC 가 무력화됨.
  3. 2026-07-21 21:00:39.181 KSTFATAL ERROR: ... JavaScript heap out of memory + ----- Native stack trace -----.
  4. 2026-07-21 21:00:39.181–.196 KST — V8 native frame 18줄 (OOMErrorHandlerCollectGarbageNewFixedArrayWithFillerRuntime_NewArray) 이 각각 별도 error 로그로 기록됨 (cluster 3b141d70 의 대표는 프레임 #7).
  5. 2026-07-21 21:01:45.908 KST — parent 가 Child process exited 감지.
  6. 2026-07-21 21:01:45.910 KSTProcess killed by signal SIGABRT (code: null).
  7. 2026-07-21 21:01:45.911 KSTBimRevisionService::run | error: "Process killed by signal SIGABRT (code: null)". 상태 Error 로 저장 후 종료.
  8. status-board — 25 클러스터로 폭발했지만 후속 재발 없음. 2026-07-21-svc-cupixworks-any-bimrevision-agent--unknown-1 인시던트가 12:01:45.911Z 에 resolved.

Error Log#

Datadog Logs

text
ChildProcessManager::setupEventHandlers | Child process stderr:  7: 0x1112e9c v8::internal::Heap::CollectGarbage(v8::internal::AllocationSpace, v8::internal::GarbageCollectionReason, v8::GCCallbackFlags) [/usr/bin/node]

Impact#

  • Service: cupixworks-any-bimrevision-agent
  • Team: clark-vdc
  • 발생 횟수: 1 (fingerprint 기준, 실제로는 동일 crash 가 25개 fingerprint 로 분산)
  • 최초 발생: 2026-07-21 21:00:39 KST
  • 최근 발생: 2026-07-21 21:00:39 KST

동일 crash 이벤트가 stderr 한 줄씩 logger.error 로 흘러가며 25개 별도 클러스터 (79974cd5..339e3092) 로 파편화됐다. status-board 는 이를 하나의 svc:cupixworks-any-bimrevision-agent::unknown 인시던트로 묶었고, 66 초 뒤 (SIGABRT 시점) resolved 처리했다.

Root Cause Summary#

BIM 비교 child process (BimCompareProcess, packages/cupix-tesla-bim-revision-agent/src/process/bim-compare.process.ts) 가 ForgeAgent.extract 호출 도중 V8 heap 한계 (약 8 GB) 를 초과해 OOM 으로 abort 했다. Mark-Compact GC 가 연속으로 mu = 0.091 → 0.006 로 붕괴한 것 (거의 회수 못 함) 이 heap 이 대형 Forge 모델 데이터로 포화 상태였음을 보여준다. Parent 의 ChildProcessManager::setupEventHandlers (packages/base/src/manager/child-process.manager.ts:138) 는 child stderr 를 라인 단위 로 logger.error 로 출력하기 때문에, V8 fatal handler 가 뱉은 native stack trace 한 줄 한 줄이 개별 error 로그로 기록되고 fingerprinter 가 이를 25개 클러스터로 나누는 부수 효과가 생겼다.

Technical Analysis#

Code Path#

  • Entry point: BimRevisionService::runrunBimCompare (packages/cupix-tesla-bim-revision-agent/src/bim-revision-service.ts:603)
  • IPC dispatch: BimCompareManager::executeChildProcessManager::execute (packages/cupix-tesla-bim-revision-agent/src/manager/bim-compare.manager.ts:53-79)
  • Child worker: BimCompareProcess::execute invokes ForgeAgent.extract (packages/cupix-tesla-bim-revision-agent/src/process/bim-compare.process.ts:67-104)
  • Failure point: child process V8 heap OOM during ForgeAgent.extract — stderr handler at packages/base/src/manager/child-process.manager.ts:138-141.

Parent forks child without a custom heap flag; execArgv 는 지정되지 않음 (기본값 = parent NODE_OPTIONS 상속):

packages/base/src/manager/child-process.manager.ts:73-77typescript
this.process = fork(actualScriptPath, {
    stdio: ['inherit', stdOut, stdErr, 'ipc'],
});
logger.debug(`ChildProcessManager::start | Child process forked with PID: ${this.process.pid}, stdio: ${this.options.stdioMode}`);

Stderr handler가 라인당 하나의 error 로그를 만든다 (fingerprint 파편화의 원인):

packages/base/src/manager/child-process.manager.ts:138-141typescript
this.process.stderr?.on('data', (data: Buffer) => {
    const text = data.toString();
    logger.error(`ChildProcessManager::setupEventHandlers | Child process stderr: ${text}`);
});

Child 는 Forge extract 만 실행:

packages/cupix-tesla-bim-revision-agent/src/process/bim-compare.process.ts:72-81typescript
const result: ForgeAgent.Result = await ForgeAgent.extract(params.apiConfig, params.urn, {
    region: params.region,
    extractRoom: false,
    extractMeta: false,
    extractCompare: {
        urn: params.previousUrn,
        region: params.previousRegion,
        query: params.query
    }
});

Parent 는 SIGABRT 를 close 이벤트로 잡고 pending IPC 를 모두 reject:

packages/base/src/manager/child-process.manager.ts:107-120typescript
this.process.on('close', (code: number | null, signal: string | null) => {
    logger.error('ChildProcessManager::setupEventHandlers | Child process closed', {
        code,
        signal
    });

    const errorMsg = signal
        ? `Process killed by signal ${signal} (code: ${code})`
        : `Process exited with code ${code}`;

    this.rejectAllPending(new Error(errorMsg));
    this.process = undefined;
    this.emit('close', code, signal);
});

Reject 는 runBimCompare 로 전파되어 BimComparisonState=Error 로 상태 저장 (bim-revision-service.ts:188-190).

기대 동작 vs 실제 동작: bim-compare.process.ts 는 파일 헤더 주석에서 "This isolation prevents memory-intensive Forge operations from affecting the main service" 라고 명시 — child 를 두는 이유가 바로 OOM 격리다. 실제로도 parent 는 살아남았고, child 만 SIGABRT. 하지만 (a) child heap 한계 자체가 여전히 부족했고, (b) crash 로그가 25개 fingerprint 로 파편화되어 알럿·상관관계 분석을 저해했다.

Log Evidence#

사용한 Datadog 쿼리:

text
service:cupixworks-any-bimrevision-agent status:error
  from: 2026-07-21T11:59:00Z
  to:   2026-07-21T12:02:00Z
  sort: ascending

핵심 로그 (원문, KST 로는 21:00:39):

text
2026-07-21T12:00:39.174Z  ChildProcessManager::setupEventHandlers | Child process stderr: <--- Last few GCs --->
2026-07-21T12:00:39.175Z  ChildProcessManager::setupEventHandlers | Child process stderr: [45:0x17bccd20]   814987 ms: Mark-Compact 7909.6 (8231.3) -> 7909.6 (8227.1) MB, 1558.72 / 0.00 ms  (average mu = 0.091, current mu = 0.008) allocation failure; scavenge might not succeed
2026-07-21T12:00:39.180Z  ChildProcessManager::setupEventHandlers | Child process stderr: [45:0x17bccd20]   816543 ms: Mark-Compact 7916.1 (8233.8) -> 7916.1 (8233.8) MB, 1547.82 / 0.00 ms  (average mu = 0.050, current mu = 0.006) allocation failure; scavenge might not succeed
2026-07-21T12:00:39.181Z  ChildProcessManager::setupEventHandlers | Child process stderr: <--- JS stacktrace --->
2026-07-21T12:00:39.181Z  ChildProcessManager::setupEventHandlers | Child process stderr: FATAL ERROR: Ineffective mark-compacts near heap limit Allocation failed - JavaScript heap out of memory
2026-07-21T12:00:39.181Z  ChildProcessManager::setupEventHandlers | Child process stderr: ----- Native stack trace -----
2026-07-21T12:00:39.181Z  ChildProcessManager::setupEventHandlers | Child process stderr:  1: 0xb78db3 node::OOMErrorHandler(char const*, v8::OOMDetails const&) [/usr/bin/node]
...
2026-07-21T12:00:39.195Z  ChildProcessManager::setupEventHandlers | Child process stderr:  7: 0x1112e9c v8::internal::Heap::CollectGarbage(...)   ← 대표 클러스터 3b141d70
...
2026-07-21T12:00:39.196Z  ChildProcessManager::setupEventHandlers | Child process stderr: 16: 0x12cbb55 v8::internal::ArrayConstructInitializeElements(...)
2026-07-21T12:00:39.196Z  ChildProcessManager::setupEventHandlers | Child process stderr: 17: 0x1512628 v8::internal::Runtime_NewArray(int, unsigned long*, v8::internal::Isolate*)
2026-07-21T12:01:45.908Z  ChildProcessManager::setupEventHandlers | Child process exited
2026-07-21T12:01:45.909Z  ChildProcessManager::setupEventHandlers | Child process closed
2026-07-21T12:01:45.910Z  Process killed by signal SIGABRT (code: null)
2026-07-21T12:01:45.911Z  BimRevisionService::run | error: "Process killed by signal SIGABRT (code: null)"

주목할 점:

  • 두 번째 Mark-Compact 에서 current mu = 0.006 — CPU 의 99.4% 가 GC 에 쓰이고 있는 상태. V8 이 "ineffective mark-compacts" 로 판단해 즉시 abort.
  • Runtime_NewArrayNewFixedArrayWithFiller 최상위 프레임 — JS 레벨에서 큰 배열 (전형적으로 Forge 모델 파싱, AECModelData.json 로드 후 vertex/index buffer 생성) 을 할당하려다 실패.
  • Native stack 은 있지만 JS stack (<--- JS stacktrace --->) 은 heap 이 이미 고갈 상태라 V8 이 dump 하지 못했다.
  • 별개 시간대(12:42:22–25Z) 에는 같은 stderr handler 로 ForgeUtilsError: API_NOT_FOUND (Autodesk Forge SDK 의 별개 downstream 이슈) 가 들어오는 것도 관측됨 — 이번 클러스터의 root cause 는 아니지만, stderr 로 여러 종류의 issue 가 뭉치는 구조적 문제를 재확인.

Hypotheses Considered#

# Hypothesis Evidence for Evidence against Verdict
H1 BIM child process 가 V8 heap 한계 (~8 GB) 를 넘어 OOM 으로 abort FATAL ERROR: Ineffective mark-compacts near heap limit Allocation failed - JavaScript heap out of memory; Mark-Compact 7916.1 -> 7916.1 MB (current mu = 0.006); SIGABRT (code: null, signal: SIGABRT); Runtime_NewArray/NewFixedArrayWithFiller 상위 프레임 → JS 배열 할당 실패 Confirmed
H2 Autodesk Forge API_NOT_FOUND (ForgeUtilsError) 가 원인 12:42:22–25Z 로그에서 errorCode: 'API_NOT_FOUND' 관측 대표 클러스터 시각 12:00:39Z 와 42 분 차이. 12:00:39 창에는 API_NOT_FOUND 로그 없음; stderr 는 V8 fatal 로만 채워짐 Rejected
H3 Parent (ChildProcessManager) 코드 결함으로 다중 클러스터 발생 25개 클러스터가 동일 서비스에서 동시 발생; stderr handler 는 라인마다 logger.error 호출 하지만 이는 결과이지 crash 원인 아님. root cause 는 여전히 child heap OOM. handler 는 log 파편화만 유발 Rejected (as root cause) — 다만 관측성 개선 필요 (fix 섹션 참고)
H4 execute timeout (parent side) 이 SIGKILL 을 트리거해 child 를 죽였다 ChildProcessManager::sendMessage (child-process.manager.ts:213-225) 에 timeout → SIGKILL 로직 존재 실제 신호는 SIGABRT (V8 self-abort), SIGKILL 이 아니며, executeTimeoutMs 사용 흔적도 BimCompareManager 에는 없음 (bim-compare.manager.ts:53-79 는 timeout 인자 미지정) Rejected
H5 컨테이너 OOMKilled (cgroup OOM) 로 인한 abort signal 은 SIGABRT (V8 자체 abort), SIGKILL 이 아님. cgroup OOM 이면 no stderr, 즉시 SIGKILL. 이번 케이스는 V8 이 스스로 FATAL ERROR 를 찍고 abort Rejected

Fix Recommendation#

즉시 조치 (Critical)#

  • child fork 에 --max-old-space-size 명시: packages/base/src/manager/child-process.manager.ts:73-76fork() 호출에서 execArgv 를 지정해 child heap 한계를 부모와 무관하게 명확히 관리하도록 한다. 현재는 parent 의 NODE_OPTIONS 를 상속하기 때문에 배포 이미지 변경 시 조용히 낮아질 수 있다. ChildProcessOptionsmaxOldSpaceMB 옵션을 추가하고 BimCompareManager 는 최소 8192 이상을 요구하도록 초기화한다. 방향만 제시 (구현 코드는 별도 PR).
  • child 사망 시 log 파편화 완화: packages/base/src/manager/child-process.manager.ts:138-141 의 stderr handler 가 라인마다 logger.error 를 부르지 않도록 버퍼링 후 하나의 로그 이벤트 (혹은 logger.warn + 첫 라인 요약) 로 묶는다. 이번 crash 가 25개 fingerprint 로 파편화된 것은 알럿/상관관계 분석을 저해했다.

단기 개선 (1주 이내)#

  • 입력 크기 기반 방어: BimCompareManager::execute (bim-compare.manager.ts:53-79) 진입 시 params.query.entities.length 등을 기준으로 heap 위험 임계를 계산·경고하고, 특정 임계 초과 시 사전에 BimRevisionRequest.BimComparisonStateEnum.Error + 명시적 errorCode: BimRevisionTooLargeInput 로 조기 종료. 지금은 heap 한계까지 밀어붙이다가 SIGABRT 로 끝난다.
  • 크래시 상관관계 로그: parent 가 close (signal=SIGABRT) 이벤트를 받았을 때, 원본 IPC message id/job id/urn 을 함께 남기도록 child-process.manager.ts:107-120 을 확장한다. 현재 로그는 "Process killed by signal SIGABRT" 만 있어 어떤 BIM revision 인지 즉시 추적이 어렵다.
  • Forge extract streaming: ForgeAgent.extract 결과를 in-memory Result.Compare.modified/removed/exist 로 완전 적재하는 대신 stream/chunk 로 소비하는 옵션을 forge-agents 팀에 요청. 이번 crash 상위 프레임이 Runtime_NewArray 인 점이 array-heavy 소비 패턴을 시사.

장기 개선 (재발 방지)#

  • child worker 리소스 계약: heap/CPU/timeout 을 ChildProcessOptions 로 명시하고, agent 별 SLA (BIM revision: 8 GB / 30 min 등) 를 표준화. 지금은 각 agent 가 암묵적으로 컨테이너 default 에 의존한다.
  • 관측성: Node.js --heapsnapshot-near-heap-limit=1--report-on-fatalerror 로 crash 직전 heap snapshot / diagnostic report 를 생성해 S3 로 업로드하는 hook 추가. 재발 시 어떤 객체가 heap 을 채웠는지 정량적으로 확인할 수 있다.
  • fingerprint 규칙: error-sweeper 쪽에서 ChildProcessManager::setupEventHandlers | Child process stderr: prefix 를 가진 로그는 한 서비스 내에서 하나의 클러스터로 뭉치도록 collector 규칙 조정 검토.

Monitoring#

writing-datadog-monitoring-queries 가이드에 따라 dashboard timeseries widget 에 그대로 넣을 수 있는 형태로 작성.

Child process fatal OOM 발생 건수:

text
service:cupixworks-any-bimrevision-agent status:error "JavaScript heap out of memory"

Child SIGABRT (원인 무관, 상관 알럿용):

text
service:cupixworks-any-bimrevision-agent status:error "Process killed by signal SIGABRT"

BimRevisionService 실패율 (per-job 관점):

text
service:cupixworks-any-bimrevision-agent status:error "BimRevisionService::run | error"

Mark-Compact ineffective 조기 경고 (heap 압박이 반복될 때 감지):

text
service:cupixworks-any-bimrevision-agent status:error "Ineffective mark-compacts"

권장 알럿: "JavaScript heap out of memory" 가 24h 내 1건 이상이면 clark-vdc 채널에 P2 알럿. 하루 5건 이상이면 heap 한계 상향 또는 입력 크기 방어 즉시 롤아웃.

Risk Assessment#

  • Risk level: medium — 개별 job 실패로 격리되고 parent service 는 살아남지만, 특정 BIM revision 은 항상 실패 (동일 URN 이 다시 처리되면 재발 가능성 높음). 발생 빈도가 늘면 clark-vdc 워크플로 차단으로 승격.
  • 예상 복잡도: standard — fork() 호출부의 execArgv 추가와 stderr handler 버퍼링은 국소 변경. 단, packages/base 는 여러 agent 가 공유하므로 회귀 테스트 (packages/base/src/manager/child-process.manager.spec.ts) 갱신 필수.