ES /docs

ChildProcessManager::setupEventHandlers | Child process stderr: 2: 0xee8300 v8::Utils::ReportOOMFai

RCA: ChildProcessManager Child process OOM (V8 heap exhaustion)

Overview#

What Happened#

2026-07-21 21:00 KST, cupixworks-any-bimrevision-agent 서비스에서 BIM 리비전 비교 작업(bim_id=17216, V4 → V5)을 수행하던 child process가 V8 heap 한도(~8 GB) 를 초과하여 FATAL ERROR: Ineffective mark-compacts near heap limit Allocation failed - JavaScript heap out of memory 로 abort 되었다. 21:01 KST 에 부모 프로세스가 SIGABRT 로 child 종료를 감지하고 BimRevisionService::run 이 예외를 전파했다. 동일 태스크에서 25개 cluster 로 fingerprint 가 분리되었지만 실제 사용자 영향은 리비전 한 건 실패이다.

Quick Facts#

Field Value
exception.class V8 FATAL ERROR (OOM)
exception.message 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 pipe)
runtime Node.js (child process forked via child_process.fork), --max-old-space-size=8192 상속
env production, us-west-2
tenant cupix, team_domain: clark-vdc

Affected Teams#

Team / Domain Error Count Impact
clark-vdc (tenant: cupix) 25 로그 (한 태스크, bim_id=17216, si_trace_id=a052780a-c69b-457d-b72d-3aa6332272a3) BIM revision V5 비교 1건 실패 — Forge compare 결과가 저장되지 않아 리비전 diff 미완성

Timeline#

  1. 2026-07-21 20:47 KSTBimRevisionService::run 시작 (si_trace_id: a052780a-c69b-457d-b72d-3aa6332272a3, bim_id=17216)
  2. 2026-07-21 20:47 KSTloadAllElements 시작 (facilityKey: 3fq7tj)
  3. 2026-07-21 20:53 KST — 211,677 cpElements 로드 완료 (created: 154,331, deleted: 57,346)
  4. 2026-07-21 20:53 KSTrunBimCompare | begin; query.entities.length: 154331, checkExistence.length: 57346
  5. 2026-07-21 21:00:39 KST — Child process V8 heap 반복 Mark-Compact (7909.6 → 7916.1 MB / 한도 ~8231 MB), allocation failure; scavenge might not succeed
  6. 2026-07-21 21:00:39 KST — V8 FATAL ERROR: Ineffective mark-compacts near heap limit, OOM native stack trace 출력
  7. 2026-07-21 21:01:45 KSTChild process exited / Child process closed / Process killed by signal SIGABRT (code: null)
  8. 2026-07-21 21:01:45 KSTBimRevisionService::run | error: "Process killed by signal SIGABRT (code: null)"

Error Log#

Datadog Logs

text
ChildProcessManager::setupEventHandlers | Child process stderr:  2: 0xee8300 v8::Utils::ReportOOMFailure(v8::internal::Isolate*, char const*, v8::OOMDetails const&) [/usr/bin/node]

Impact#

  • Service: cupixworks-any-bimrevision-agent
  • Team: clark-vdc
  • 발생 횟수: 1 (fingerprint 기준; 실제 stderr 로그는 25건, 모두 단일 태스크 실패에서 파생)
  • 최초 발생: 2026-07-21 21:00 KST
  • 최근 발생: 2026-07-21 21:00 KST
  • 비즈니스 영향: BIM revision 비교 1건 실패. 리비전 V5 결과가 저장되지 않아 사용자가 revision diff 를 볼 수 없다.

Root Cause Summary#

BimCompareProcess@cupixapps/forge-agentsForgeAgent.extract 를 호출하여 Autodesk Forge 모델 두 개(V4, V5)를 비교한다. 이번 태스크는 query.entities.length: 154331, checkExistence.length: 57346, 총 cp_elements_count: 211677 규모로 매우 크다. Fork 로 생성된 child process 는 parent 의 execArgv 를 상속받아 --max-old-space-size=8192 (약 8 GB) heap 한도를 사용하지만, forge-agents 가 SVF2 매니페스트/AEC ModelData/grid alignment/compare 결과 자료구조를 로드/생성하는 과정에서 heap 사용량이 7,909 MB → 7,916 MB 로 증가한 뒤 GC 가 회수하지 못해 V8 이 FATAL ERROR: Ineffective mark-compacts near heap limit 로 abort 했다. 즉, 입력 데이터 규모가 현재 heap 한도(8 GB) 를 초과했고, child-process.manager.ts 는 crash 를 감지하지만 memory guard/스트리밍 처리가 없어 그대로 실패한다.

Technical Analysis#

Code Path#

  • Entry point (parent): packages/cupix-tesla-bim-revision-agent/src/manager/bim-compare.manager.ts:65childProcessManager.execute<ForgeAgent.Result>('execute', params)
  • Fork 지점: packages/base/src/manager/child-process.manager.ts:74-77fork(actualScriptPath, { stdio: ['inherit', stdOut, stdErr, 'ipc'] }). execArgv 를 미지정하므로 parent 의 --max-old-space-size=8192 를 상속한다.
  • Child entry: packages/cupix-tesla-bim-revision-agent/src/process/bim-compare.process.ts:67-81ForgeAgent.extract(params.apiConfig, params.urn, { ..., extractCompare: { urn, region, query } })
  • Failure point: V8 heap in child (FATAL ERROR: Ineffective mark-compacts near heap limit), stderr 를 parent 가 stderr.on('data') 로 수신하여 logger.error 로 방출. ChildProcessManager::setupEventHandlers 의 close 핸들러가 SIGABRT 신호로 pending message 를 reject.

Parent 가 child 를 fork 하는 부분:

packages/base/src/manager/child-process.manager.ts:70-84typescript
const stdOut = this.options.stdioMode === 'pipe' ? 'pipe' : 'inherit';
const stdErr = this.options.stdioMode === 'pipe' ? 'pipe' : 'inherit';

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}`);
} catch (error: any) {
    logger.error(`ChildProcessManager::start | Fork failed: ${error.message}`, {
        actualScriptPath,
        error: error.stack
    });
    throw error;
}

Child 의 stderr 를 부모 로거로 재출력하는 handler (에러 메시지가 여기서 방출됨):

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 crash 로 인한 pending message 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);
});

Child 에서 실제 비교를 수행하는 지점 (heap 를 소비하는 곳):

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

Parent heap 한도는 8 GB 로 고정되어 있음:

applications/agents/docker_services/80_run-agent.sh:3bash
node --max-old-space-size=8192 /tmp/agent/dist/app.cjs \

기대 동작: 입력 규모와 무관하게 comparison 완료 또는 재시도 가능한 에러 반환. 실제 동작: 입력 규모가 heap 한도를 넘겨 V8 abort → 상위 서비스에서 SIGABRT 로 실패.

Log Evidence#

Datadog 검색 쿼리 (재현 가능):

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

시간 범위 2026-07-21T11:59:00Z ~ 2026-07-21T12:02:00Z.

작업 입력 규모 (사고 발생 7분 전 로그):

text
2026-07-21T11:53:10.688Z 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":"...","prev_forge_urn":"..."}
2026-07-21T11:53:11.068Z BimRevisionService::runBimCompare | params - query.entities.length: 154331, checkExistence.length: 57346, levels: [{"id":72846,"name":"ROOF","elevation":19.1262},{"id":72845,"name":"LEVEL 2","elevation":9.2837},{"id":72827,"name":"LEVEL 1","elevation":0}], ...

V8 GC 압박 및 abort (핵심 stderr 스트림):

text
2026-07-21T12:00:39.174Z <--- Last few GCs --->
2026-07-21T12:00:39.175Z [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 [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 <--- JS stacktrace --->
2026-07-21T12:00:39.181Z FATAL ERROR: Ineffective mark-compacts near heap limit Allocation failed - JavaScript heap out of memory
2026-07-21T12:00:39.181Z  1: 0xb78db3 node::OOMErrorHandler(char const*, v8::OOMDetails const&) [/usr/bin/node]
2026-07-21T12:00:39.182Z  2: 0xee8300 v8::Utils::ReportOOMFailure(...)
2026-07-21T12:00:39.196Z 17: 0x1512628 v8::internal::Runtime_NewArray(int, unsigned long*, v8::internal::Isolate*) [/usr/bin/node]

Abort 후 signal 및 서비스 예외 전파:

text
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)"

V8 top-of-stack frame (Runtime_NewArray + NewFixedArrayWithFiller) 은 큰 array/객체 할당 시점에 OOM 이 났음을 시사한다. 이는 forge-agents 가 154,331 entity 규모의 compare 결과 array 를 만드는 과정과 부합한다.

관련 (동일 태스크 이후, 21:42 KST 재시도로 추정) 로그에서는 ForgeUtilsError: Resource not found / errorCode: 'API_NOT_FOUND' (Autodesk 360 translation storage AEC ModelData) 도 관측되나, 이 클러스터의 fingerprint 원인은 OOM 이며 API_NOT_FOUND 는 별개의 이후 이벤트이다.

Hypotheses Considered#

# Hypothesis Evidence for Evidence against Verdict
H1 Child process 가 heap 8 GB 한도 안에서 대규모 BIM compare (154K entities) 를 처리하다가 OOM 으로 abort V8 GC 로그 7916.1 (8233.8) MB, allocation failure; scavenge might not succeed; FATAL ERROR: Ineffective mark-compacts near heap limit; Runtime_NewArray top-of-stack; 사고 직전 query.entities.length: 154331, checkExistence.length: 57346; parent --max-old-space-size=8192 (80_run-agent.sh:3) 을 fork 가 상속. Confirmed
H2 Autodesk Forge API 실패 (API_NOT_FOUND) 로 인한 실패 동일 시간대 stderr 에 ForgeUtilsError: Resource not found ... AECModelData.json / errorCode: 'API_NOT_FOUND' 관측 이 이벤트들은 12:00 UTC 가 아니라 12:42 UTC (21:42 KST) 이후 관찰됨. 클러스터 대표 로그 2026-07-21T12:00:39.182Z 는 OOM native stack (ReportOOMFailure) 로, API_NOT_FOUND 는 이후 재시도 경로. Fingerprint 불일치. Rejected
H3 Child process 메모리 누수 (heap 이 시간 경과에 따라 서서히 증가) GC 로그 두 줄만 노출되어 이전 heap 추이 불명 해당 프로세스는 태스크 시작(11:47) 부터 crash(12:00) 까지 약 13분 실행, 그 동안 loadAllElements 로 211K row 를 로드하고 runBimCompare 로 대용량 diff 를 계산 — 누수가 아니라 단일 작업의 절대 사용량으로 설명 가능. 누수를 확정할 debug/heapsnapshot 증거 없음. Inconclusive
H4 Docker 컨테이너 cgroup 메모리 한도가 heap 보다 낮아 강제 OOM-kill Signal 은 SIGABRT (V8 self-abort) 이지 SIGKILL 이 아니다. cgroup OOM 은 통상 SIGKILL 을 낸다. GC 로그에서 V8 스스로 fatal 을 낸 시퀀스가 확인됨. 동일 Rejected

Fix Recommendation#

즉시 조치 (Critical)#

  • packages/cupix-tesla-bim-revision-agent/src/manager/bim-compare.manager.ts:26-27ChildProcessManager 생성 시 execArgv 를 override 해서 child 의 heap 한도를 명시적으로 부여하도록 baseChildProcessOptions 를 확장하고, BIM revision agent 는 12–16 GB 처럼 여유분을 준다. 컨테이너 메모리 예산과 합의 필요 (인프라 팀 확인). 접근 방식: ChildProcessManagernodeOptions?: string[] 옵션 추가 → fork(scriptPath, { execArgv: [...], stdio: [...] }) 로 명시.
  • packages/base/src/manager/child-process.manager.ts:74-77 — 현재 execArgv 를 지정하지 않아 parent 의 flag 를 상속하지만, 이 값은 서비스별로 다르게 튜닝 가능해야 한다. Fork 옵션에 명시적 execArgv 를 노출하는 방향으로 수정. 구현 코드는 후속 PR 에서.
  • Ops 대응: 실패한 revision bim_id=17216, src_revision_id=25433 을 사후 재처리 큐로 넣고 사용자에게 상태 노출 (별도 트랙 — 팀 협의 필요).

단기 개선 (1주 이내)#

  • BimCompareProcess::execute (bim-compare.process.ts:67-116) 에서 params.query.entities.length 가 임계치 (예: 80,000) 를 초과할 때 chunked 처리 또는 warning telemetry 를 추가한다. Forge 의 extractCompare 를 여러 subset 으로 나눠 호출하고 결과를 병합하는 방향.
  • OOM signal (SIGABRT, exit code null 또는 FATAL ERROR stderr) 을 명시적으로 감지하는 로직을 child-process.manager.ts:107-120 의 close handler 에 추가하여, 상위 서비스가 error code 를 ErrorCode.Agent.OutOfMemory 로 분류 (bim-compare.manager.ts:71 근처) — 재시도 정책과 알림을 OOM 전용으로 분기하기 위함.
  • BIM revision 규모(cp_elements_count) 를 Datadog custom metric 으로 방출 (BimRevisionService::REVISE-BEGIN 시점) — 규모별 실패율 분석 근거.

장기 개선 (재발 방지)#

  • forge-agents 의 compare 파이프라인을 스트리밍 또는 out-of-core (디스크 기반) 로 리팩터. 154K 엔티티를 in-memory array 로 유지하는 현재 구조는 heap 을 선형적으로 소비한다.
  • BIM revision agent 인스턴스에 heap dump on OOM (--heapsnapshot-near-heap-limit=3) 을 옵션으로 활성화, 실패 시 S3 로 업로드 — 다음 발생 시 즉시 원인 파악 가능.
  • Autoscaling: 대규모 BIM (cp_elements_count > 100k) 태스크는 memory-optimized 인스턴스 큐로 라우팅.

Monitoring#

Datadog release dashboard timeseries widget 에 그대로 넣을 쿼리:

  • BIM revision agent OOM 발생 건수 (분당):
text
sum:datadog.trace_agent.trace_writer.spans_dropped{service:cupixworks-any-bimrevision-agent}.rollup(count, 60)

대체 쿼리 (로그 기반, log metric 미리 생성 필요):

text
logs("service:cupixworks-any-bimrevision-agent \"ReportOOMFailure\"").index("*").rollup("count").by("environment")
  • Child process 이상 종료 (SIGABRT / SIGKILL) 추적:
text
logs("service:cupixworks-any-bimrevision-agent \"Process killed by signal\"").index("*").rollup("count").by("environment")
  • BIM revision 입력 규모 (custom metric 신규 계측 후):
text
avg:cupixworks.bim_revision.cp_elements_count{service:cupixworks-any-bimrevision-agent} by {team_domain}
  • 알림 임계치 권장: OOM 로그가 5분 동안 1건 이상이면 clark-vdc / VDC 채널로 알림.

Risk Assessment#

  • Risk level: medium — 실제 사용자 impact 는 revision 1건 실패 (25 로그 fingerprint 는 단일 태스크). 다만 대규모 프로젝트 (예: 15만 엔티티 이상) 는 반복 실패 가능성이 있다.
  • 예상 복잡도: standardChildProcessManagerexecArgv 옵션 추가는 소규모 리팩터. 그러나 heap 한도 상향은 컨테이너/EC2 인스턴스 memory 여유와 합의가 필요하므로 인프라 팀과 협의(별도 트랙).