ES /docs

ChildProcessManager::setupEventHandlers | Child process stderr: 1: 0xb78db3 node::OOMErrorHandler(c

RCA: ChildProcessManager child process OOM (JavaScript heap out of memory)

Overview#

What Happened#

BIM revision agent(cupixworks-any-bimrevision-agent)의 BimCompareProcess 자식 프로세스가 bim_id=17216 리비전 비교(V4→V5, 총 211,677 elements) 실행 중 V8 힙 상한(≈8GB)에 도달해 FATAL ERROR: Ineffective mark-compacts near heap limit 로 종료되었다. 부모 프로세스는 자식의 stderr 스트림을 그대로 error 레벨로 로깅했고(ChildProcessManager::setupEventHandlers | Child process stderr: ...), 이후 자식이 SIGABRT 로 죽자 BimRevisionService::run 이 실패로 종료했다. 총 25개의 에러 클러스터가 동일 시각에 발생했으며 이 파일은 그중 OOM 헤더 라인 하나에 해당한다.

Quick Facts#

Field Value
exception.class (no exception — child-process stderr forwarded to error log)
exception.message ChildProcessManager::setupEventHandlers | Child process stderr: 1: 0xb78db3 node::OOMErrorHandler(char const*, v8::OOMDetails const&) [/usr/bin/node]
top_frame packages/base/src/manager/child-process.manager.ts:140
runtime Node.js on /usr/bin/node (agent runs with node --max-old-space-size=8192)
env production, region us-west-2, tenant cupix

Affected Teams#

Team / Domain Error Count Impact
clark-vdc (bim-revision-agent) 25 clusters at 2026-07-21 21:00:39 KST 1건의 BIM 리비전 비교 작업 실패 (bim_id=17216, src_revision_id=25433, prev_revision_id=21647, si_trace_id a052780a-c69b-457d-b72d-3aa6332272a3). 사용자가 요청한 리비전 diff 산출 실패.

Timeline#

  1. 2026-07-21 20:53:10 KSTBimRevisionService::REVISE-BEGIN — bim_id 17216, cp_elements_count 211,677 (created 154,331 / deleted 57,346), agent_version 10.98.0.
  2. 2026-07-21 20:53:11 KSTBimRevisionService::runBimCompare 호출, query.entities.length=154,331, checkExistence.length=57,346, 3 levels, matchingStrategy treePath.
  3. 2026-07-21 21:00:39 KST — 자식 프로세스가 V8 OOM. stderr 로 FATAL ERROR: Ineffective mark-compacts near heap limit Allocation failed - JavaScript heap out of memory 및 네이티브 스택 트레이스(24 프레임) 출력. 부모가 그대로 error 레벨 로깅.
  4. 2026-07-21 21:01:45 KSTChildProcessManager::setupEventHandlers | Child process closed, Process killed by signal SIGABRT (code: null). BimRevisionService::run 에러 반환.
  5. 2026-07-21 21:01:46 KSTBimRevisionService::REVISE-END (status error, elapsed_ms 883,421 ≈ 14분 43초).

Error Log#

Datadog Logs

text
ChildProcessManager::setupEventHandlers | Child process stderr:  1: 0xb78db3 node::OOMErrorHandler(char const*, v8::OOMDetails const&) [/usr/bin/node]

Impact#

  • Service: cupixworks-any-bimrevision-agent
  • Team: clark-vdc
  • 발생 횟수: 1 (이 클러스터). 동일 이벤트로 인해 status-board 인시던트 2026-07-21-svc-cupixworks-any-bimrevision-agent--unknown-1 하에 25개 클러스터가 함께 개봉·해소되었다.
  • 최초 발생: 2026-07-21 21:00:39 KST
  • 최근 발생: 2026-07-21 21:00:39 KST

Root Cause Summary#

BimCompareProcess 자식 프로세스가 ForgeAgent.extract 로 154,331개 엔티티에 대한 tree-path 매칭·비교 연산을 수행하는 동안 V8 힙 사용량이 상한(부모에서 상속된 --max-old-space-size=8192, 즉 8GB) 을 초과해 OOM 으로 종료됐다. Datadog stderr 로그의 GC 리포트가 최종 mark-compact 직전 7916.1MB 를 보고하고 있어 heap ceiling 도달이 사실로 확인된다. 자식이 OOM으로 SIGABRT 종료되면 부모의 ChildProcessManager::setupEventHandlersclose/exit 이벤트를 잡아 pending IPC 요청을 reject 하고, BimCompareManager::execute 가 이 에러를 재던져 BimRevisionService::run 이 실패한다. 근본 원인은 (a) 자식 프로세스에 명시적 힙 한도가 설정되어 있지 않아 부모의 8GB 값을 그대로 상속하고 있고, (b) 154K entities × treePath 매칭 을 in-memory 로 수행하는 forge-agents extract 경로가 이 크기의 revision 을 처리하기에 부족하다는 두 지점이 결합한 결과다.

Technical Analysis#

Code Path#

  • Entry point: packages/cupix-tesla-bim-revision-agent/src/manager/bim-compare.manager.ts:27 (new ChildProcessManager(__dirname, 'process/bim-compare.process'))
  • Child fork: packages/base/src/manager/child-process.manager.ts:74-77fork()execArgv 를 지정하지 않으므로 Node 는 부모의 CLI 인자(--max-old-space-size=8192, docker_services/80_run-agent.sh:3)를 자식에 상속.
  • Child work: packages/cupix-tesla-bim-revision-agent/src/process/bim-compare.process.ts:67-80 (ForgeAgent.extract 호출, extractCompare.query.entities 154,331개).
  • Failure point (V8): 자식 프로세스 힙 8GB 도달 → V8::FatalProcessOutOfMemorySIGABRT.
  • Log emission: packages/base/src/manager/child-process.manager.ts:138-141 — 자식 stderr 를 그대로 error 레벨로 forwarding. 클러스터 헤더 라인은 이 forwarding 의 첫 프레임.
  • Cleanup: packages/base/src/manager/child-process.manager.ts:107-120close 이벤트에서 rejectAllPending(new Error("Process killed by signal SIGABRT ...")).
  • Rethrow: packages/cupix-tesla-bim-revision-agent/src/manager/bim-compare.manager.ts:64-78catch 블록이 ErrorCode.Agent.BimRevisionExtractorExecute 를 설정하고 에러를 재던짐.

기대 동작: 자식 프로세스가 comparison 결과를 IPC 로 반환. 실제 동작: 자식이 heap ceiling 에 부딪혀 SIGABRT 로 종료, 부모는 Process killed by signal SIGABRT 로 pending IPC 를 reject.

packages/base/src/manager/child-process.manager.ts:73-77typescript
try {
    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}`);
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);
});
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}`);
});
packages/cupix-tesla-bim-revision-agent/src/process/bim-compare.process.ts:67-81typescript
private async execute(params: BimComparerParams): Promise<ForgeAgent.Result> {
    try {
        this.log(`BimCompareProcess::execute | forge-agents version: ${forgeAgentsPkg.version}, spec_version: ${SpecVersion}`);
        this.log('BimCompareProcess::execute | start');

        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
            }
docker_services/80_run-agent.sh:3bash
node --max-old-space-size=8192 /tmp/agent/dist/app.cjs \

Log Evidence#

Datadog query 재현:

text
service:cupixworks-any-bimrevision-agent status:error

시간창: 2026-07-21T11:55:00Z2026-07-21T12:05:00Z (28개 결과).

핵심 GC 리포트(발생 직전 두 번의 Mark-Compact — heap 이 8GB 상한에 걸리고 있음):

text
[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
[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

자식 stderr 헤더:

text
FATAL ERROR: Ineffective mark-compacts near heap limit Allocation failed - JavaScript heap out of memory

Native stack (관련 프레임):

text
 1: 0xb78db3 node::OOMErrorHandler(char const*, v8::OOMDetails const&) [/usr/bin/node]
 2: 0xee8300 v8::Utils::ReportOOMFailure(v8::internal::Isolate*, char const*, v8::OOMDetails const&) [/usr/bin/node]
 3: 0xee85e7 v8::internal::V8::FatalProcessOutOfMemory(v8::internal::Isolate*, char const*, v8::OOMDetails const&) [/usr/bin/node]
 ...
16: 0x12cbb55 v8::internal::ArrayConstructInitializeElements(v8::internal::Handle<v8::internal::JSArray>, v8::internal::Arguments<(v8::internal::ArgumentsType)1>*) [/usr/bin/node]
17: 0x1512628 v8::internal::Runtime_NewArray(int, unsigned long*, v8::internal::Isolate*) [/usr/bin/node]

상단 프레임이 Runtime_NewArray / ArrayConstructInitializeElements 인 것에서 대규모 배열 할당(154K entities 를 다루는 tree-path 매칭이 유력) 시점에 OOM 이 터졌음이 시사된다.

작업 컨텍스트:

text
BimRevisionService::REVISE-BEGIN | {"bim_id":17216,"src_revision_id":25433,"src_revision_name":"V5","prev_revision_id":21647,"prev_revision_name":"V4","agent_version":"10.98.0","si_trace_id":"a052780a-c69b-457d-b72d-3aa6332272a3","cp_elements_count":211677,"cp_elements_created_count":154331,"cp_elements_deleted_count":57346, ...}
BimRevisionService::runBimCompare | params - query.entities.length: 154331, checkExistence.length: 57346, levels: [3 levels], ...
BimRevisionService::runBimCompare | matchingStrategy: treePath

에필로그:

text
BimRevisionService::run | error: "Process killed by signal SIGABRT (code: null)"
BimRevisionService::REVISE-END | {..., "elapsed_ms":883421, "status":"error", "error_message":"Process killed by signal SIGABRT (code: null)", "counts":{"total":0,"modified":0,"removed":0,"exist":0}, ...}

Status-board 결과 (동일 이벤트로 25개 클러스터가 함께 개봉·해소):

text
scope: svc:cupixworks-any-bimrevision-agent::unknown
incident id: 2026-07-21-svc-cupixworks-any-bimrevision-agent--unknown-1
status: resolved (started 2026-07-21T12:00:39.174Z, resolved 2026-07-21T12:01:45.911Z)
cluster_ids: 25 (this cluster included)

Hypotheses Considered#

# Hypothesis Evidence for Evidence against Verdict
H1 자식 프로세스가 V8 heap 상한(8GB)에 도달하여 OOM abort stderr에 FATAL ERROR: Ineffective mark-compacts near heap limit ... JavaScript heap out of memory, GC 리포트 7916.1(8233.8)MB 도달, 부모의 --max-old-space-size=8192 를 자식이 상속, 스택 상단이 Runtime_NewArray, 154,331 entities 처리 중 Confirmed
H2 컨테이너/Pod 레벨 OOM Kill (OS SIGKILL) 자식이 종료 신호를 SIGABRT 로 받았음(부모 로그: Process killed by signal SIGABRT). Node 의 V8 OOM 핸들러가 abort() 를 호출해 SIGABRT 를 유발; 커널 OOM Killer 는 통상 SIGKILL. 부모 프로세스는 살아남아 이후 로그를 계속 남겼음. 부모가 함께 죽지 않았다는 사실이 컨테이너 OOM 을 배제. Rejected
H3 ChildProcessManager 의 executeTimeoutMs 초과로 인한 SIGKILL child-process.manager.ts:32executeTimeoutMs 옵션이 있으나 BimCompareManager 생성자(bim-compare.manager.ts:27)는 옵션을 넘기지 않아 timeout 이 없음. 실제 종료는 SIGKILL 이 아닌 SIGABRT. Rejected
H4 Forge/Autodesk 다운스트림 서비스 장애 로그에 Forge HTTP 에러나 4xx/5xx 응답 흔적 없음. 실패는 in-process V8 abort. status-board 는 dep:* 가 아닌 svc:* 인시던트로 개봉. Rejected
H5 특정 리비전 페어(V4→V5)의 tree-path 매칭 조합에서 폭발적 메모리 사용 154,331 created + 57,346 deleted 로 규모가 크고 matchingStrategy: treePath 사용, 스택 상단이 대규모 배열 할당 프레임. 정확한 forge-agents 내부 알고리즘 heap profile 이 없어 트리거 지점을 라인 단위로 확정 불가 (uncertain — needs verification). Inconclusive (contributing factor)

Fix Recommendation#

즉시 조치 (Critical)#

  • packages/base/src/manager/child-process.manager.ts:73-77 fork() 호출에 자식 프로세스 heap 한도를 명시적으로 지정할 수 있도록 execArgv 를 옵션화한다. 방향:
    1. ChildProcessOptionsmaxOldSpaceSizeMb?: number 를 추가하고, fork()execArgv: [\--max-old-space-size=${maxOldSpaceSizeMb}`]` 를 전달.
    2. BimCompareManager 는 대용량 리비전(≥100K entities) 을 다루므로 부모보다 큰 값(예: 12288 또는 16384) 을 지정. 단, 컨테이너 memory limit 이 이를 수용하는지 인프라 확인 필요 (uncertain — needs verification).
  • 그 외 자체 코드 변경 없이도, agent 컨테이너의 memory limit 을 검토해 실제 사용 프로파일 대비 여유가 있는지 확인 (조율 필요 항목 — cupix-infrastructure 팀).

단기 개선 (1주 이내)#

  • BimRevisionService::runBimCompare 진입 시 query.entities.length 가 임계값(예: 100,000) 을 넘으면 chunk 로 분할해 여러 회 ForgeAgent.extract 를 호출하거나, 명시적 사전 검증으로 실패 원인을 명확히 로깅. 방향: entities 를 level/modelId 기준으로 분할해 부분 결과를 병합.
  • ChildProcessManager 에 자식 프로세스 heap usage 를 주기적으로 process.memoryUsage() 로 IPC 리포팅해, OOM 근접 시 사전 경고 로그를 남긴다. 이는 다음 대용량 리비전에서 임계값 조정에 근거를 제공.
  • 자식 stderr 를 무조건 error 레벨로 forwarding 하는 현재 로깅(child-process.manager.ts:138-141) 은 25개 별도 error 클러스터를 생성해 노이즈가 되므로, stack frame 라인들을 하나의 aggregated 에러(예: 처음 라인 + 원본 stderr 전체를 error.stderr 필드로 첨부) 로 묶는 로깅 개선을 고려. 방향만 제시하며 구현은 별도 검토 필요.

장기 개선 (재발 방지)#

  • forge-agents extract 의 메모리 프로파일을 정식으로 벤치마킹하고, entity 수 대비 예상 peak heap 을 문서화. 이 벤치마크를 기반으로 (1) child heap 한도, (2) chunking 임계값, (3) 컨테이너 memory limit 을 함께 조정.
  • BIM 리비전 파이프라인에 사전 크기 예측(예: previous/current revision entity count 로부터 예상 peak) 을 넣어, 특정 임계 초과 시 별도 큐(대용량 전용, 큰 인스턴스) 로 라우팅.
  • 자식 프로세스 OOM 시 커널 core dump 나 --heapsnapshot-on-oom 기반 아티팩트를 저장·업로드하는 파이프라인 정비 (재현이 어려운 대용량 케이스에서 유일한 사후 분석 수단).

Monitoring#

Datadog 쿼리 (release dashboard timeseries widget 에 그대로 사용 가능):

  • OOM stderr forwarding 발생률 (agent-side signal):
text
service:cupixworks-any-bimrevision-agent status:error "JavaScript heap out of memory"
  • 자식 프로세스 SIGABRT 종료율:
text
service:cupixworks-any-bimrevision-agent status:error "Process killed by signal SIGABRT"
  • BIM revision 실패율 (서비스 관점):
text
service:cupixworks-any-bimrevision-agent "BimRevisionService::REVISE-END" "\"status\":\"error\""
  • 대용량 리비전 감지 (사전 신호, entities 수가 큰 요청 발생):
text
service:cupixworks-any-bimrevision-agent "BimRevisionService::runBimCompare | params - query.entities.length"

Alerts: JavaScript heap out of memory 로그가 5분 창에서 1건 이상 발생 시 clark-vdc 팀 알림.

Risk Assessment#

  • Risk level: medium — 단일 리비전 작업 실패이지만 대용량(≥100K entities) 리비전에서 재발 가능. 부모 프로세스는 살아남아 다른 요청은 계속 처리하므로 서비스 전면 장애는 아님.
  • 예상 복잡도: standard — 즉시 조치는 ChildProcessOptions 확장으로 국소적이나, chunking / 벤치마크는 forge-agents 팀과의 조율 필요.