ES /docs

ChildProcessManager::setupEventHandlers | Child process stderr: 11: 0x10b7e4a v8::internal::FactoryB

RCA: ChildProcessManager child process OOM (V8 heap allocation failure)

Overview#

What Happened#

2026-07-21 21:00:39 KST 에 cupixworks-any-bimrevision-agent (us-west-2, production) 에서 BIM revision 비교 작업 중 fork 된 child process 가 V8 heap 을 소진하고 FATAL ERROR: Ineffective mark-compacts near heap limit Allocation failed - JavaScript heap out of memory 로 crash 했다. 부모 프로세스는 ChildProcessManager::setupEventHandlers 의 stderr 리스너를 통해 V8 fatal error 스택 트레이스 각 줄을 error 로그로 방출했고, 이로 인해 동일 사고에서 25 개의 클러스터가 생성되었다. 이 클러스터는 AllocateRawArray frame 을 대표 로그로 담고 있다.

Quick Facts#

Field Value
exception.class Node.js FATAL ERROR: Ineffective mark-compacts near heap limit (V8 OOM)
exception.message Allocation failed - JavaScript heap out of memory (child SIGABRT)
top_frame packages/base/src/manager/child-process.manager.ts:140 (stderr 리스너)
runtime Node.js (parent launched with --max-old-space-size=8192, child inherits execArgv)
env production, region us-west-2, tenant cupix

Affected Teams#

Team / Domain Error Count Impact
clark-vdc (bim-revision-agent) 25 clusters / SQS message dd2cac85-c941-428d-8fbb-bb97168d9bfe bim_id: 17216, src_revision_id: 25433, prev_revision_id: 21647 BIM revision 비교 실패. 사용자에게 revision diff 결과가 저장되지 않음 (counts.total: 0).

Timeline#

  1. 2026-07-21 20:45:56 KST — child process fork 후 약 816 초간 실행 (V8 uptime 816543 ms 기준 역산). BIM comparison workload 진행.
  2. 2026-07-21 21:00:39 KST — child process 에서 V8 heap 이 ~7916 MB / 8233 MB 로 포화. Mark-Compact GC 두 차례 (814987 ms, 816543 ms) 실패 후 FactoryBase::AllocateRawArray 에서 OOM. Node OOMErrorHandler 가 SIGABRT 로 프로세스 종료.
  3. 2026-07-21 21:00:39 KST — 부모의 ChildProcessManager::setupEventHandlers stderr 리스너가 crash 스택을 라인 단위로 logger.error 로 방출 (25 개 로그 → 25 개 클러스터).
  4. 2026-07-21 21:01:45 KST — 부모가 close/exit 이벤트 감지, Process killed by signal SIGABRT (code: null) 로 대기 중이던 execute() promise reject. BimRevisionService::run 이 error 처리로 진입.
  5. 2026-07-21 21:01:46 KSTBimRevisionService::REVISE-END 로그 (elapsed_ms: 883421, status: error) 후 SQS 메시지 삭제, 워크스페이스 /tmp/workspace/25433 정리.
  6. 2026-07-21 21:01:45 KST — status-board 가 svc-level incident 2026-07-21-svc-cupixworks-any-bimrevision-agent--unknown-1 로 자동 resolve.

Error Log#

Datadog Logs

text
ChildProcessManager::setupEventHandlers | Child process stderr: 11: 0x10b7e4a v8::internal::FactoryBase<v8::internal::Factory>::AllocateRawArray(int, v8::internal::AllocationType, v8::internal::AllocationAlignment) [/usr/bin/node]

Impact#

  • Service: cupixworks-any-bimrevision-agent
  • Team: clark-vdc
  • 발생 횟수: 1 (동일 crash 로 파생된 클러스터 25 개 중 하나. status-board incident 2026-07-21-svc-cupixworks-any-bimrevision-agent--unknown-1 로 그룹화됨)
  • 최초 발생: 2026-07-21 21:00:39 KST
  • 최근 발생: 2026-07-21 21:00:39 KST

Root Cause Summary#

BIM revision 25433 (bim_id 17216) 을 매우 오래된 baseline revision 21647 과 비교하는 Autodesk Forge ForgeAgent.extract 작업이 fork 된 child process 안에서 실행되던 중, V8 heap 이 --max-old-space-size=8192 로 지정된 8 GB 상한에 도달했다. Mark-Compact GC 가 연속으로 유효 메모리를 회수하지 못하고 (allocation failure; scavenge might not succeed) V8 이 FatalProcessOutOfMemory 를 발생시켜 child 가 SIGABRT 로 종료되었다. 부모의 ChildProcessManager::setupEventHandlers stderr 리스너가 V8 crash dump 를 라인마다 logger.error 로 방출하면서 error-sweeper 가 다수의 클러스터를 열게 된 것이 관찰 표면이다 — 실제 root cause 는 Forge BIM 비교 workload 의 메모리 소비가 현재 8 GB 상한을 초과하는 입력이 존재한다는 점이며, 특히 이번 케이스는 revision id gap 이 매우 큰 (25433 vs 21647, 약 3800 revision 격차) 조합이었다.

Technical Analysis#

Code Path#

  • Entry point: packages/cupix-tesla-bim-revision-agent/src/bim-revision-service.ts:177run() 에서 runBimCompare(this.cpBimRevision, this.cpPreviousBimRevision) 호출
  • Delegate: packages/cupix-tesla-bim-revision-agent/src/bim-revision-service.ts:647bimCompareManager.execute(params)
  • Fork: packages/base/src/manager/child-process.manager.ts:74fork(actualScriptPath, { stdio: ['inherit', stdOut, stdErr, 'ipc'] })
  • Workload: packages/cupix-tesla-bim-revision-agent/src/process/bim-compare.process.ts:72ForgeAgent.extract(params.apiConfig, params.urn, { extractCompare: { urn: params.previousUrn, ... } })
  • Failure point: child process V8 heap 내부 (FactoryBase::AllocateRawArray frame). 부모 관찰 지점은 packages/base/src/manager/child-process.manager.ts:140

부모는 child process 를 fork 한 뒤 stderr 를 pipe 로 캡처하여 라인 단위로 error 레벨 로그로 방출한다:

packages/base/src/manager/child-process.manager.ts:74-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}`);
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 SDK 호출:

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

기대 동작: ForgeAgent.extract 가 두 URN 의 BIM 모델을 로드 → diff → Result 반환 후 heap 회수. 실제 동작: 8 GB heap 안에서 diff 대상 배열/문자열이 계속 커져 GC 가 회수하지 못하고 OOM 종료.

부모 상위 계층은 child close 이벤트를 예외로 재throw 하고, REVISE-END 로 elapsed 883 s + SIGABRT 를 기록한다:

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);
});

docker_services/80_run-agent.sh:3 에서 부모 agent 가 node --max-old-space-size=8192 /tmp/agent/dist/app.cjs 로 기동된다. fork()execArgv 를 기본적으로 상속하므로 child 도 동일한 8 GB 상한을 가진다 — Datadog 로그의 7916.1 (8233.8) MB 실측이 이 상한과 일치한다.

Log Evidence#

사용한 Datadog 쿼리:

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

시간 범위: from_ts=1784631600000, to_ts=1784638860000 (2026-07-21 20:00-22:01 KST). 26 건 hit.

V8 fatal error 원문 (Datadog):

text
ChildProcessManager::setupEventHandlers | Child process stderr: FATAL ERROR: Ineffective mark-compacts near heap limit Allocation failed - JavaScript heap out of memory

바로 앞의 GC 로그 (heap 사용량과 상한이 명시됨):

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

Native stack trace 상위 프레임 (요약):

text
 3: 0xee85e7 v8::internal::V8::FatalProcessOutOfMemory(...)
 5: 0x10fb794 v8::internal::Heap::RecomputeLimits(...)
 7: 0x1112e9c v8::internal::Heap::CollectGarbage(...)
 8: 0x10e91f1 v8::internal::HeapAllocator::AllocateRawWithLightRetrySlowPath(...)
 9: 0x10ea385 v8::internal::HeapAllocator::AllocateRawWithRetryOrFailSlowPath(...)
10: 0x10c69a6 v8::internal::Factory::AllocateRaw(...)
11: 0x10b7e4a v8::internal::FactoryBase<v8::internal::Factory>::AllocateRawArray(...)
12: 0x10b7fb4 v8::internal::FactoryBase<v8::internal::Factory>::NewFixedArrayWithFiller(...)
16: 0x12cbb55 v8::internal::ArrayConstructInitializeElements(...)
17: 0x1512628 v8::internal::Runtime_NewArray(...)

부모 측 처리 (Datadog):

json
{
  "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 일 정상 REVISE-END 샘플 — elapsed_ms 는 대부분 8000-13000 ms 범위:

text
2026-07-21 17:47:12 bim_id 18044 elapsed_ms 4026  status success
2026-07-21 17:40:00 bim_id 17926 elapsed_ms 13431 status success
2026-07-21 17:39:19 bim_id 17931 elapsed_ms 12083 status success
2026-07-21 15:45:10 bim_id 228   elapsed_ms 8211  status success

이번 실패 케이스만 elapsed_ms: 883421 (약 220 배 이상) 로, 워크로드 자체가 이례적으로 무거웠음을 뒷받침한다.

Hypotheses Considered#

# Hypothesis Evidence for Evidence against Verdict
H1 Child process 가 Forge BIM 비교 workload (ForgeAgent.extract) 실행 중 8 GB heap 을 소진하고 V8 OOM 으로 crash FATAL ERROR: Ineffective mark-compacts near heap limit, GC 로그 7916 / 8233 MB, stack top FactoryBase::AllocateRawArrayRuntime_NewArray, BimRevisionService::REVISE-ENDProcess killed by signal SIGABRT, elapsed_ms: 883421 Confirmed
H2 ChildProcessManager::setupEventHandlers 로직 자체의 버그 (예: stderr 리스너의 무한 recursion, IPC 오류) stack trace 는 순수 V8 heap 경로이며 사용자 JS frame 없음. 스택 어디에도 setupEventHandlers, child_process, EventEmitter frame 이 없음 관찰 로그는 부모의 리스너에서만 나오지만, 그 위치는 stderr 를 그대로 재방출하는 라인 (child-process.manager.ts:140) 이라 로그 폭발의 원인일 뿐 crash 원인이 아님 Rejected
H3 외부 의존성 (Forge Data API, Autodesk backend, us-west-2 network) 장애 REVISE-END 이 실패 case 는 다른 revision 에서 INVALID_OTG_ERROR - OTG status is pending 로 즉시 (4 s) 실패하는 별개 경로가 존재. 이번 케이스는 그 에러가 아니라 883 s 동안 정상적으로 데이터를 로드하다 OOM 이번 클러스터 데이터에는 Forge 4xx/5xx 흔적 없음. status-board 도 dep:* scope 가 아닌 svc:* scope Rejected
H4 Memory leak — 같은 child process 가 반복 재사용되며 메모리가 누적 child V8 uptime 이 816543 ms (약 13.6 분) 로 단일 job 수명. BimRevisionService::REVISE-ENDelapsed_ms: 883421 과 근사. child process 는 job 별로 새로 fork 됨 (BimCompareManager 생성자에서 new ChildProcessManager(...)) 재사용 흔적 없음 (근접한 다른 REVISE-BEGIN/END 없음) Rejected
H5 bim_id 17216 의 revision id 격차가 크면 (25433 vs 21647) Forge diff 데이터 크기가 훨씬 커져 OOM 정상 REVISE-END 들은 src_revision_id - prev_revision_id gap 이 작음 (예: 25429 vs 25279 = 150). 실패 케이스는 gap ~3786. 실측 elapsed_ms 도 정상 8-13 s vs 이번 883 s (100 배 이상) 단일 사례라 통계적으로 확정하기엔 부족 — 다른 대형 gap revision 을 히스토리컬로 재현 필요 Inconclusive (H1 root cause 를 유발한 possible trigger)

Fix Recommendation#

즉시 조치 (Critical)#

  • bim_id: 17216, src_revision_id: 25433, prev_revision_id: 21647 잡의 요청 파라미터 확인: 사용자/워크플로우가 매우 오래된 baseline 을 지정한 것인지, 시스템이 부정확한 prev_revision_id 를 골랐는지 clark-vdc 팀과 확인. 잘못된 baseline 자동 선정이라면 그것부터 수정 (재실행 시에도 동일하게 실패할 것).
  • 재시도 회로 확인: 현재는 SQS message 를 deleteMessage 로 소진했으므로 (2026-07-21 21:01:46 로그) 자동 재시도되지 않는다 — 데이터 손실이 아닌 결과 미생성 상태이며 유저가 다시 트리거하지 않는 한 재발생하지 않음. 이 정책이 의도된 것인지 확인.

단기 개선 (1주 이내)#

  • Child process heap 상한을 명시적으로 늘리거나 분리 지정: packages/base/src/manager/child-process.manager.ts:74 fork() 호출 시 execArgv: ['--max-old-space-size=<N>'] 를 명시하여, 부모(8 GB) 와 무관하게 BIM 비교 child 에 예를 들어 12-16 GB 를 부여하는 방향을 검토. 단, docker_services/80_run-agent.sh 기반 컨테이너 메모리 상한이 충분해야 함 (kubectl describe pod 또는 ECS task definition 확인 선행).
  • 크기 pre-flight: runBimCompare (bim-revision-service.ts:603) 진입 시 query.entities.length, previousUrn 의 revision gap 등으로 리스크 스코어를 계산해 임계 초과 시 warning 로그 + 별도 큐/스트리밍 처리로 분기. 대량 diff 는 chunk 단위로 ForgeAgent.extract 를 여러 번 호출해 결과를 합치도록 재설계.
  • ChildProcessManager::setupEventHandlers 의 stderr 로그를 aggregate: 현재 라인별 logger.error 로 나가 error-sweeper 가 fingerprint 마다 클러스터를 만든다 (한 crash → 25 개 클러스터). stderr 전체를 rolling buffer 에 모아 close/error 이벤트에서 한번에 방출하거나, V8 fatal stack 라인은 warn 으로 낮춰 alert noise 축소. 주의: 이 변경은 로그 수집/온콜 알림 정책 변경이므로 반드시 관측성 팀과 사전 조율 필요 — 코드-only 자동 수정 대상 아님.

장기 개선 (재발 방지)#

  • BIM 비교 workload 를 stateless streaming 모델로 재설계: 현재는 전체 diff 결과를 in-process JS heap 에 올려두고 마지막에 반환한다 (packages/cupix-tesla-bim-revision-agent/src/process/bim-compare.process.ts:72). 결과를 chunk 로 부모에 IPC 전송하거나 S3 로 직접 flush 하면 heap 상한이 사실상 무관해진다.
  • 워크로드 기반 리소스 프로파일링: BIM 크기(entity count, geometry size, revision gap) 를 dimensioning 지표로 삼아 EC2/ECS task size 를 자동 선택하는 dispatcher 도입.

Monitoring#

  • Datadog timeseries widget 용 쿼리 (모두 timeseries 문법):

Child process OOM 발생 건수:

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

SIGABRT 로 종료된 REVISE 잡:

text
service:cupixworks-any-bimrevision-agent @environment:production "REVISE-END" "Process killed by signal SIGABRT"

REVISE-END 중 실패 상태:

text
service:cupixworks-any-bimrevision-agent @environment:production "REVISE-END" @status:error

메모리 사용 트렌드 (metrics):

text
avg:container.memory.usage{service:cupixworks-any-bimrevision-agent}
  • 알림: Ineffective mark-compacts near heap limit 은 severity high 로 pager. elapsed_ms > 60000 인 REVISE-END 는 warning 으로 track (통상 8-13 s 대비 이상치).

Risk Assessment#

  • Risk level: medium — 단일 대량 revision 케이스에서 발생. 하지만 유사한 pattern (오래된 baseline 지정, 대규모 BIM) 이 반복될 수 있음.
  • 예상 복잡도: standard — heap 상한 조정과 streaming 리팩터링은 별도 트랙. immediate 는 데이터 검증부터 수행.