ES /docs

ChildProcessManager::setupEventHandlers | Child process stderr: 14: 0x12a6793 [/usr/bin/node]

RCA: BIM Compare Child Process V8 Heap Out-of-Memory

Overview#

What Happened#

cupixworks-any-bimrevision-agent (us-west-2, production) 에서 BIM revision compare 작업(bim_id=17216, src_revision_id=25433 V5 vs prev_revision_id=21647 V4) 을 처리하던 자식 프로세스가 V8 힙 한계인 약 8GB (--max-old-space-size=8192) 에 도달해 FATAL ERROR: Ineffective mark-compacts near heap limit 로 SIGABRT 되었다. 부모 프로세스는 child stderr 의 heap dump/native stack trace 각 라인을 error 레벨로 로깅하며 오늘 오전 21:00:39 KST 에 28건의 error 클러스터로 확산되었다 (status board 상 2026-07-21-svc-cupixworks-any-bimrevision-agent--unknown-1 인시던트, resolved). 이 클러스터(14: 0x12a6793 [/usr/bin/node]) 는 그 stack trace 중 하나.

Quick Facts#

Field Value
exception.class Node.js V8 FatalProcessOutOfMemory (child process)
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:140 (stderr forwarder)
runtime Node.js (child forked from docker_services/80_run-agent.sh with --max-old-space-size=8192)
deploy agent_version 10.98.0, forge-agents @cupixapps/forge-agents@10.98.0
env production, us-west-2, tenant cupix

Affected Teams#

Team / Domain Error Count Impact
clark-vdc / bim-revision-agent 25 개 클러스터 (동일 인시던트), 이 클러스터 1건 BIM revision compare 1건 실패 (bim_id=17216, revision V4 → V5). modified=0, removed=0, exist=0 — 사용자에게 변경 리포트가 생성되지 않음.

Timeline#

  1. 2026-07-21 20:53:10 KSTBimRevisionService::REVISE-BEGINbim_id=17216, src_revision_id=25433 (V5), prev_revision_id=21647 (V4), cp_elements_count=211,677, cp_elements_created_count=154,331, cp_elements_deleted_count=57,346, si_trace_id=a052780a-c69b-457d-b72d-3aa6332272a3. IAD157 Coordination Model (Autodesk Forge SVF2).
  2. 2026-07-21 21:00:39 KST — 자식 프로세스가 약 816.5s 실행 후 힙 7916 MB / 8233 MB 에 도달, Mark-Compact 가 회수 실패하고 FATAL ERROR ... JavaScript heap out of memory 로 abort. ChildProcessManager::setupEventHandlers 가 stderr 27+ 줄을 error 레벨로 흘려보내 클러스터가 다수 생성됨. (이 클러스터의 첫 로그)
  3. 2026-07-21 21:01:45 KST — 부모의 Child process closed / exited 이벤트 로깅, BimRevisionService::runProcess killed by signal SIGABRT (code: null) 로 실패, REVISE-END status=error, elapsed_ms=883,421.
  4. 2026-07-21 21:01:46 KST — SQS 메시지 삭제 및 /tmp/workspace/25433 정리, 서비스는 자연 회복 (재현 없음).

Error Log#

Datadog Logs

text
ChildProcessManager::setupEventHandlers | Child process stderr: 14: 0x12a6793  [/usr/bin/node]

Impact#

  • Service: cupixworks-any-bimrevision-agent
  • Team: clark-vdc
  • 발생 횟수: 1 (이 클러스터). 동일 crash 사건에서 파생된 sibling stderr 클러스터 총 24개 (status board 인시던트 상 25 clusters, 이 클러스터 포함)
  • 최초 발생: 2026-07-21 21:00:39 KST
  • 최근 발생: 2026-07-21 21:00:39 KST

Root Cause Summary#

BIM revision compare 자식 프로세스가 매우 큰 Autodesk Forge 모델 (IAD157 Coordination Model, cp_elements_count=211,677, V4→V5 사이 154,331 신규 + 57,346 삭제) 을 in-memory 로 diff 하는 도중 V8 old-generation 힙이 상한 (--max-old-space-size=8192, 실측 8233 MB) 에 도달했고, Mark-Compact 가 유효 회수를 못 해 V8 이 FatalProcessOutOfMemory 로 abort (SIGABRT) 했다. 자식 프로세스의 stderr 로 나가는 heap dump/native stack trace 각 라인이 ChildProcessManager::setupEventHandlersstderr.on('data') 핸들러에서 logger.error 로 재발행되어 (packages/base/src/manager/child-process.manager.ts:140) 단일 crash 가 다수의 error 클러스터로 분화되었다. Root cause 는 (a) forge-agents 의 compare pipeline 이 model diff 를 스트리밍 없이 전체 fixed-array 로 할당하는 특성 + (b) 214k 규모 모델에 대한 heap headroom 부족이며, 로그 폭발 자체는 stderr 를 무조건 error 로 승격시키는 forwarder 의 secondary contributor.

Technical Analysis#

Code Path#

  • Entry point: applications/agents/packages/cupix-tesla-bim-revision-agent/src/manager/bim-compare.manager.ts:65BimCompareManager.execute 가 자식 프로세스로 execute IPC 를 보냄.
  • Child worker: applications/agents/packages/cupix-tesla-bim-revision-agent/src/process/bim-compare.process.ts:72ForgeAgent.extract(...) 호출로 두 URN 을 각각 로드해 Compare 리포트를 생성.
  • Failure point: 자식 프로세스 V8 힙. 부모의 stderr forwarder 는 applications/agents/packages/base/src/manager/child-process.manager.ts:140.

부모의 fork 지점 — execArgv 를 재정의하지 않으므로 --max-old-space-size=8192 가 자식에 그대로 상속된다. 로그의 7916 MB / 8233 MB 수치가 이를 확증한다.

applications/agents/packages/base/src/manager/child-process.manager.ts:73-77ts
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}`);

자식 crash 시 stderr 의 모든 라인이 error 레벨로 승격되어 collector 가 각 라인마다 fingerprint 를 만든 결과 25개 sibling 클러스터가 발생.

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

자식 프로세스 내부에서는 forge-agents 라이브러리가 예외를 던지지 않은 상태로 OOM 이 발생 — 즉 try/catch 로 잡을 수 없는 프로세스-레벨 abort.

applications/agents/packages/cupix-tesla-bim-revision-agent/src/process/bim-compare.process.ts:67-83ts
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
            }
        });

부모 매니저의 close 핸들러가 Process killed by signal SIGABRT 로 pending IPC 를 reject 하여 BimCompareManager.execute 는 error 로 전파.

applications/agents/packages/base/src/manager/child-process.manager.ts:107-120ts
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));
    ...
});

기대 동작 vs 실제 동작

  • 기대: 214k element 규모의 revision compare 도 자식 프로세스가 성공적으로 diff 를 계산해 Compare.modified/removed/exist 를 반환.
  • 실제: V8 heap 이 8GB 상한에 부딪혀 자식이 SIGABRT, 상위 BimRevisionService::REVISE-ENDstatus=error, counts.total=0 로 종료.

Log Evidence#

Datadog 쿼리:

text
service:cupixworks-any-bimrevision-agent status:error @environment:production "ChildProcessManager::setupEventHandlers"

작업 시작 로그 (bim_id=17216 compare 크기 문맥):

json
{
  "timestamp": "2026-07-21 20:53:10 KST",
  "level": "info",
  "message": "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,
  "src_forge_urn": "dXJuOmFkc2sub2JqZWN0czpvcy5vYmplY3Q6dGVzbGEtcHJvZHVjdGlvbi8xNzIxNl8xNzg0NjMzMjM0OTY3X0lBRDE1N19Db29yZGluYXRpb24lMjBNb2RlbF9DdXBpeC5ud2Q=",
  "prev_forge_urn": "dXJuOmFkc2sub2JqZWN0czpvcy5vYmplY3Q6dGVzbGEtcHJvZHVjdGlvbi8xNzIxNl8xNzcyODEyMzI4NTA3X0lBRDE1N19Db29yZGluYXRpb24lMjBNb2RlbF9DdXBpeC5ud2Q="
}

V8 OOM 스택 트레이스 (자식 프로세스 stderr, 21:00:39 KST). 특히 heap 사용량 라인:

text
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
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
Child process stderr: <--- JS stacktrace --->
Child process stderr: FATAL ERROR: Ineffective mark-compacts near heap limit Allocation failed - JavaScript heap out of memory

Native stack (Array 확장 중 OOM):

text
Child process stderr: 12: 0x10b7fb4 v8::internal::FactoryBase<v8::internal::Factory>::NewFixedArrayWithFiller(...) [/usr/bin/node]
Child process stderr: 16: 0x12cbb55 v8::internal::ArrayConstructInitializeElements(...) [/usr/bin/node]
Child process stderr: 17: 0x1512628 v8::internal::Runtime_NewArray(int, unsigned long*, v8::internal::Isolate*) [/usr/bin/node]

부모 매니저의 종료/실패 로그:

text
21:01:45 error ChildProcessManager::setupEventHandlers | Child process exited
21:01:45 error ChildProcessManager::setupEventHandlers | Child process closed
21:01:45 error Process killed by signal SIGABRT (code: null)
21:01:45 error BimRevisionService::run | error: "Process killed by signal SIGABRT (code: null)"

작업 종료 요약:

json
{
  "timestamp": "2026-07-21 21:01:46 KST",
  "level": "info",
  "message": "BimRevisionService::REVISE-END",
  "bim_id": 17216,
  "src_revision_id": 25433,
  "prev_revision_id": 21647,
  "si_trace_id": "a052780a-c69b-457d-b72d-3aa6332272a3",
  "elapsed_ms": 883421,
  "status": "error",
  "error_message": "Process killed by signal SIGABRT (code: null)",
  "counts": {"total": 0, "modified": 0, "removed": 0, "exist": 0}
}

지난 7일간 동일 서비스에서 "JavaScript heap out of memory" 발생 횟수: 1 (아래 쿼리로 확인).

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

Hypotheses Considered#

# Hypothesis Evidence for Evidence against Verdict
H1 자식 프로세스가 V8 힙 한계(8GB) 를 초과해 OOM abort. compare 대상 모델이 214k elements 로 특히 크다. Mark-Compact 7916.1 (8233.8) MB ... allocation failure; FATAL ERROR: Ineffective mark-compacts near heap limit; native stack 이 NewFixedArrayWithFiller / ArrayConstructInitializeElements / Runtime_NewArray 로 시작; REVISE-BEGIN 의 cp_elements_count=211,677, cp_elements_created_count=154,331; 재시작 없이 close/exit 로 SIGABRT (Process killed by signal SIGABRT). 없음 — 로그, 스택, 시그널, 실행 시간(816.5s Mark-Compact 반복) 모두 일치. Confirmed
H2 Autodesk Forge API 오류 (API_NOT_FOUND 등) 가 원인. 같은 서비스에서 20:42 대에 ForgeUtilsError: Resource not found ... AECModelData.json (errorCode: 'API_NOT_FOUND') 로그가 존재. 이 오류는 크래시와 무관한 21:42 KST (약 40분 후) 다른 job. 21:00:39 크래시 스택 어디에도 ForgeUtilsError, handleApiError, ForgeClient 프레임이 없음. REVISE-END 는 API 오류가 아니라 SIGABRT. Rejected
H3 ChildProcessManager 의 IPC 타임아웃 또는 부모 측 로직 버그. Process killed by signal SIGABRT (code: null) 문자열이 부모에서 로그됨. executeTimeoutMs 미설정 (child-process.manager.ts:39-47), 실제 사망은 child 내부의 V8 FatalProcessOutOfMemory 이며 부모는 close/exit 시그널을 관찰만 함. Rejected
H4 단일 crash 가 아니라 25건의 개별 error 가 발생했다. 같은 인시던트에 25 cluster_ids. Datadog 로그가 모두 Child process stderr: 접두어 + 하나의 V8 heap dump 를 라인별로 쪼갠 것이며, 모두 21:00:39 초 단위 timestamp 하나로 몰림. 부모의 stderr forwarder 가 각 라인을 logger.error 로 승격시켜 collector 가 sibling cluster 로 분화. Rejected (secondary contributor 로만 인정)

Fix Recommendation#

즉시 조치 (Critical)#

  • 자식 프로세스 힙 상한 상향applications/agents/packages/base/src/manager/child-process.manager.ts:74fork(...) 호출 시 execArgv 를 명시적으로 설정하여 대용량 BIM compare 에 필요한 heap 을 확보. 예: execArgv: ['--max-old-space-size=<container_limit_-_1GB>']. 컨테이너 memory limit 을 함께 상향해야 실제 효과. Docker/ECS task definition 의 memory 값도 검토 (현재 --max-old-space-size=8192 는 부모 스크립트 docker_services/80_run-agent.sh:3 기준이며 자식이 이를 상속받고 있음). 주의: 단순 상향은 근본 처방이 아니라 완충. bim revision compare 는 214k 규모까지 자연 스케일되므로 (H1 로그의 cp_elements_created_count=154,331) 300k+ 모델이 오면 다시 재발할 수 있음.
  • 자식 stderr forwarder 로그 레벨 downgradeapplications/agents/packages/base/src/manager/child-process.manager.ts:138-141stderr.on('data') 핸들러가 stderr 를 무조건 logger.error 로 흘리고 있어 단일 crash 로 다수의 fingerprint 가 발생. 실제 fatal 판정은 close/exit 이벤트 (line 107, 122) 에서 이미 error 로 로깅되고 있으므로, stderr chunk 는 warn 또는 debug 로 낮추고 fatal 은 close 시점에 정리된 summary 하나로 집계. (팀 컨벤션 확인 필요 — 로거가 Error 객체를 처리하는 방식은 MEMORY 의 cplogger 규칙 참고)

단기 개선 (1주 이내)#

  • REVISE-BEGIN 시 규모 기반 pre-flight guardcp_elements_count, cp_elements_created_count + cp_elements_deleted_count 가 임계치를 넘으면 (a) 별도 higher-memory 큐로 라우팅하거나 (b) 사용자에게 revision 을 chunk 로 나눠 재요청하도록 안내. 로그 상 이 job 은 20:53:10 에 시작해 21:00:39 crash 까지 약 7.5분간 heap 을 눈덩이처럼 키움 — 사전 예측 가능한 시그널.
  • 자식 프로세스에 --heapsnapshot-near-heap-limit=1 을 추가해 다음 OOM 발생 시 자동으로 heap snapshot 을 dump, 어느 라이브러리 (forge-agents/forge-utils) 가 어느 자료구조로 힙을 채우는지 원인 특정. 현재 native stack 에서 확인되는 프레임은 NewFixedArrayWithFiller 까지로 애플리케이션 프레임이 없음.
  • 부모 매니저에 executeTimeoutMs 활성화child-process.manager.ts:39-47 의 옵션이 이미 존재하나 BimCompareManager 에서 미설정. 15-20분 상한을 걸어 OOM 직전 slow-GC 루프(로그에 mu=0.006 로 나타남) 를 조기 종결.

장기 개선 (재발 방지)#

  • forge-agents compare pipeline 을 streaming/chunked diff 로 리팩터링 — 214k elements 를 전체 array 로 유지하는 현재 알고리즘은 힙 상수시간이 elements 에 선형. Autodesk Forge SDK 의 model iterator 를 chunk 단위로 소비하고 partial Compare.modified/removed/exist 를 append-only 로 flush 하는 방식으로 재설계.
  • 에이전트 큐를 모델 크기별로 분리 — small/medium/large 큐를 두고 large 는 higher memory instance 에 배정. cupix-tesla-bim-revision-agent-production SQS 하나에 모든 크기가 몰리는 현재 구조는 tail-latency 및 재시도 정책 튜닝이 어려움.

Monitoring#

  • Node child process OOM (across all agents) — 개수 시계열:
text
sum:trace.node.request.errors{service:cupixworks-any-bimrevision-agent} by {resource_name}.as_count()
  • BIM revision agent 에서 SIGABRT 시그니처 카운트 (log-based custom metric 필요 시 아래 쿼리로 monitor 정의):
text
service:cupixworks-any-bimrevision-agent "Process killed by signal SIGABRT"
  • Child stderr forwarder 폭발 (heap dump 라인) 시계열:
text
service:cupixworks-any-bimrevision-agent "JavaScript heap out of memory"
  • BIM compare 작업 크기 분포 — cp_elements_count P95/P99 를 log-based metric 으로 등록해 200k 이상 job 비율을 관측:
text
service:cupixworks-any-bimrevision-agent "REVISE-BEGIN"
  • ECS/Fargate task memory utilization (컨테이너 레벨 상한 도달 여부):
text
avg:aws.ecs.memory_utilization{service:cupixworks-any-bimrevision-agent}

Risk Assessment#

  • Risk level: medium — 단일 job 실패로 사용자 impact 는 국소적(1 revision compare 미생성)이나, 로그 폭증으로 온콜 노이즈가 크고 모델이 계속 커지면 재발 확률 높음.
  • 예상 복잡도: standardexecArgv 세팅, stderr forwarder 레벨 조정, pre-flight guard 는 기존 인터페이스만 만짐. forge-agents streaming diff 는 별도 이니셔티브 (critical scale) 로 트랙 분리 권장.