ES /docs

ChildProcessManager::setupEventHandlers | Child process stderr: <--- JS stacktrace --->

RCA: ChildProcessManager child process OOM crash in bim-revision agent

Overview#

What Happened#

2026-07-21 21:00:39 KST, cupixworks-any-bimrevision-agentBimCompareProcess child process 가 V8 heap out-of-memory 로 fatal error 를 던지고 종료되었다. 부모 서비스의 ChildProcessManager 는 child 의 stderr(FATAL ERROR: Ineffective mark-compacts near heap limit) 를 error 로그로 흘렸고, 이 stderr 스택트레이스가 25 개의 error cluster (본 클러스터 포함) 로 분류되었다. 실질적으로는 하나의 child 크래시이며, clark-vdc 팀이 요청한 대형 BIM revision 비교(154,331 entities) 처리 중 발생했다.

Quick Facts#

Field Value
exception.class FATAL ERROR: Ineffective mark-compacts near heap limit Allocation failed - JavaScript heap out of memory
exception.message Child process stderr: <--- JS stacktrace --->
top_frame packages/base/src/manager/child-process.manager.ts:140 (stderr handler)
runtime Node.js (V8 heap limit ~8192 MB via --max-old-space-size=8192)
env production, us-west-2, tenant cupix

Affected Teams#

Team / Domain Error Count Impact
clark-vdc (bim-revision) 25 clusters, single crash 하나의 BIM revision 비교 job 실패 (revision comparison 결과가 Error 상태로 기록됨)

Timeline#

  1. 2026-07-21 20:53:10 KSTBimRevisionService::runBimCompare | begin — 부모 서비스가 child 로 비교 요청 시작
  2. 2026-07-21 20:53:11 KSTrunBimCompare | params - query.entities.length: 154331, checkExistence.length: 57346 — 대형 페이로드 전송
  3. 2026-07-21 21:00:39 KSTChild process stderr: FATAL ERROR: Ineffective mark-compacts near heap limit — V8 이 7909→7916 MB 근처에서 mark-compact 반복 실패 후 OOM abort
  4. 2026-07-21 21:01:45 KSTChild process closed, Process killed by signal SIGABRT — child 종료, 부모의 rejectAllPending 이 실행되고 BimRevisionService::run | error: "Process killed by signal SIGABRT" 로 revision 상태가 Error 로 마감

Error Log#

Datadog Logs

text
ChildProcessManager::setupEventHandlers | Child process stderr: <--- JS stacktrace --->

Impact#

  • Service: cupixworks-any-bimrevision-agent
  • Team: clark-vdc
  • 발생 횟수: 1
  • 최초 발생: 2026-07-21 21:00:39 KST
  • 최근 발생: 2026-07-21 21:00:39 KST

Root Cause Summary#

Autodesk Forge 기반 BIM revision 비교를 담당하는 child process (BimCompareProcess) 가 V8 old-space 한계인 8192 MB 를 초과하며 FATAL ERROR: Ineffective mark-compacts near heap limit Allocation failed - JavaScript heap out of memory 로 abort 되었다. 원인은 이번 revision 비교 페이로드가 매우 컸다는 점이다 — query.entities.length: 154331, checkExistence.length: 57346. @cupix/forge-agentsextract 가 두 개 URN 의 모델 데이터를 함께 로드해 dbId/geometry/property 를 대조하므로, 엔티티 수가 15만을 넘어가면 배열/객체 할당 총량이 V8 old-space heap 상한에 근접한다. 로그의 Mark-Compact 7916.1 (8233.8) MB 는 이미 heap 이 상한(≈8 GB) 근처에서 반복 GC 를 시도했으나 회수되지 않았음을 보여준다. child 가 SIGABRT 로 죽자 부모의 ChildProcessManager.setupEventHandlers 가 stderr 를 라인 단위로 error 로그에 기록해 로그가 폭증했고, 이 stderr fragment 하나(“<--- JS stacktrace --->”) 가 본 클러스터로 fingerprint 되었다.

Technical Analysis#

Code Path#

  • Entry point: packages/cupix-tesla-bim-revision-agent/src/bim-revision-service.ts:603 (runBimCompare)
  • Fork 지점: packages/base/src/manager/child-process.manager.ts:74 (fork(actualScriptPath, { stdio: [...] }))
  • Child work: packages/cupix-tesla-bim-revision-agent/src/process/bim-compare.process.ts:72 (ForgeAgent.extract(...))
  • Failure point: child V8 heap OOM → stderr 방출 → packages/base/src/manager/child-process.manager.ts:138-141 (stderr handler) 에서 각 라인이 logger.error(...) 로 흘러 나가 error 클러스터로 잡힘

부모 서비스는 대형 비교를 아래처럼 그대로 IPC 로 전달한다:

packages/cupix-tesla-bim-revision-agent/src/bim-revision-service.ts:617-647typescript
const params: BimComparerParams = {
    apiConfig: { clientId: Environment.ADF_CLIENT_ID, clientSecret: Environment.ADF_CLIENT_SECRET },
    urn: cpBimRevision.forgeUrn,
    formatType: cpBimRevision.forgeFormatType,
    region: cpBimRevision.forgeRegion as 'US' | 'EMEA' | undefined,
    bimOrigin: bimSetting.origin,
    previousUrn: cpPreviousBimRevision.forgeUrn,
    previousFormatType: cpPreviousBimRevision.forgeFormatType,
    previousRegion: cpPreviousBimRevision.forgeRegion as 'US' | 'EMEA',
    query: {
        entities: cpBim.getBimEntities(),
        checkExistence: checkExistenceList,
        levels: this.iLevels,
        bimSetting: bimSetting,
        propNames: ['Volume'],
        customPropNames: cpBim.getCustomPropNames(),
        configuration: { boxTolerance }
    }
};
// ...
const bimCompareResults = await this.bimCompareManager.execute(params);

child 는 이 params 를 forge-agents 로 그대로 넘기고, 두 URN 의 모델 데이터를 동시에 로드해 비교한다:

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

부모의 ChildProcessManager.setupEventHandlers 는 child 의 stderr 를 라인 단위로 그대로 error 로그로 흘린다:

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 가 OOM 으로 abort 되면 V8 이 마지막 GC 요약 + 네이티브 스택트레이스를 stderr 로 뿜어 20+ 줄이 각각 error 로그로 남고, 이 각 라인이 서로 다른 fingerprint 로 클러스터화된다. 본 클러스터가 잡은 라인은 <--- JS stacktrace ---> 이다.

부모의 fork 는 execArgv 을 명시하지 않으므로 Node 기본 동작에 따라 부모의 execArgv 를 상속한다. 부모는 컨테이너 진입점에서 --max-old-space-size=8192 로 기동되므로, child 도 8 GB 한계에서 동작한다:

applications/agents/docker_services/80_run-agent.sh:1-3bash
#!/bin/bash

node --max-old-space-size=8192 /tmp/agent/dist/app.cjs \

Log Evidence#

Datadog query (재현):

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

Time range: 2026-07-21T11:55:00Z2026-07-21T12:05:00Z.

핵심 stderr 시퀀스 (KST):

text
2026-07-21 20:53:10 info  BimRevisionService::runBimCompare | begin
2026-07-21 20:53:11 info  runBimCompare | params - query.entities.length: 154331, checkExistence.length: 57346, ...
2026-07-21 21:00:39 error Child process stderr: <--- Last few GCs --->
2026-07-21 21:00:39 error 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-21 21:00:39 error 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-21 21:00:39 error Child process stderr: <--- JS stacktrace --->
2026-07-21 21:00:39 error Child process stderr: FATAL ERROR: Ineffective mark-compacts near heap limit Allocation failed - JavaScript heap out of memory
2026-07-21 21:00:39 error Child process stderr: ----- Native stack trace -----
2026-07-21 21:00:39 error Child process stderr:  1: 0xb78db3 node::OOMErrorHandler(char const*, v8::OOMDetails const&) [/usr/bin/node]
2026-07-21 21:00:39 error Child process stderr:  3: 0xee85e7 v8::internal::V8::FatalProcessOutOfMemory(v8::internal::Isolate*, char const*, v8::OOMDetails const&) [/usr/bin/node]
2026-07-21 21:00:39 error Child process stderr: 12: 0x10b7fb4 v8::internal::FactoryBase<v8::internal::Factory>::NewFixedArrayWithFiller(...) [/usr/bin/node]
2026-07-21 21:00:39 error Child process stderr: 16: 0x12cbb55 v8::internal::ArrayConstructInitializeElements(...) [/usr/bin/node]
2026-07-21 21:00:39 error Child process stderr: 17: 0x1512628 v8::internal::Runtime_NewArray(...) [/usr/bin/node]
2026-07-21 21:01:45 error ChildProcessManager::setupEventHandlers | Child process closed
2026-07-21 21:01:45 error ChildProcessManager::setupEventHandlers | Child process exited
2026-07-21 21:01:45 error Process killed by signal SIGABRT (code: null)
2026-07-21 21:01:45 error BimRevisionService::run | error: "Process killed by signal SIGABRT (code: null)"

주목할 지점:

  • 7909.6 → 7916.1 MB 두 번의 Mark-Compact 가 거의 회수 못 함 (average mu = 0.050) → 참조가 살아 있는 대형 데이터 구조
  • 마지막 native stack 이 Runtime_NewArrayNewFixedArrayWithFiller — 큰 배열 할당 시점에 abort
  • 종료 signal: SIGABRT (V8 OOMErrorHandler 경로), signal handler 는 부모의 setupEventHandlers.close 에서 Process killed by signal SIGABRT 로 표현됨

Hypotheses Considered#

# Hypothesis Evidence for Evidence against Verdict
H1 Child process 가 대형 비교 페이로드(154k entities) 를 처리하다 V8 old-space 8 GB 한계에서 OOM stderr 에 FATAL ERROR: Ineffective mark-compacts near heap limit ... JavaScript heap out of memory, Mark-Compact 7916.1 (8233.8) MB, `runBimCompare params - query.entities.length: 154331`
H2 Child process 가 부모로부터 memory limit 을 못 물려받아 기본 heap (≈2 GB) 에서 죽음 부모 fork()execArgv 를 명시하지 않는다는 코드 관찰 (child-process.manager.ts:74) 로그의 실제 한계는 8233.8 MB — Node 는 default 로 부모 execArgv 를 상속하며 컨테이너 진입점(80_run-agent.sh) 이 --max-old-space-size=8192 를 준다. 상속 자체는 정상 동작 중 Rejected
H3 ChildProcessManager IPC/타임아웃 로직 결함으로 부모가 child 를 죽임 Process killed by signal SIGABRT 라는 문구가 로그에 있음 부모의 SIGKILL 경로(sendMessage timeout) 는 SIGKILL 을 쓰고 (child-process.manager.ts:221), SIGTERMstop() 경로 (:322,327) 에서만 사용. SIGABRT 는 V8 OOMErrorHandler 가 abort() 를 호출하는 표준 경로이며 stderr 에 V8 OOM 배너가 선행. child 가 스스로 abort 한 것 Rejected
H4 Autodesk Forge API/네트워크 오류 (403, 429, 502 등) 에서 파생된 exception 원문 에러 메시지가 ChildProcessManager stderr 라 Forge 문자열 아님 Forge 관련 에러는 `runBimCompare Critical Forge error detected 로 별도 error 코드로 잡히도록 되어 있음 (bim-revision-service.ts:667-670`) — 이번 로그엔 없음
H5 외부 dependency 장애 (status board 상 dep 인시던트) status-board for-cluster 결과: scope: svc:*, active: null. dep:* 인시던트 없음. 최근 resolved 인시던트도 같은 서비스의 다중 stderr 라인 폭발로 발생한 동일 사건 Rejected

Fix Recommendation#

즉시 조치 (Critical)#

  • Log noise 완화: packages/base/src/manager/child-process.manager.ts:138-141 의 stderr handler 를 라인 단위 individual logger.error 대신 (a) 버퍼링 후 한 번에 aggregate 하거나 (b) V8 OOM 배너(FATAL ERROR:, <--- JS stacktrace --->, <--- Last few GCs --->) 를 감지해 단일 logger.error + 첨부 payload 로 요약. 근거: 한 번의 OOM 이 20 라인 이상 stderr 를 뿜어 25 개 error cluster 로 fragment 되어 알림/분류 노이즈가 커진다. 실제 exception path (Process killed by signal SIGABRT at child-process.manager.ts:113-117) 는 이미 부모에서 명확히 error 로그를 남기므로 stderr 는 warn level 또는 aggregated error 로 충분.
  • Child 페이로드 크기 로그화: bim-revision-service.ts:640-643 에 이미 entities.length, checkExistence.length 가 info 로 남는다 — 이 정보를 revision 종료 시점(성공/실패 모두) 에도 함께 기록해 대형 revision 을 사후 식별 가능하게 유지 (현재도 남지만 실패 로그와 correlation 하려면 재확인). uncertain — 실제 대시보드 요건은 검증 필요.

단기 개선 (1주 이내)#

  • Entity 규모 threshold 도입: runBimCompare 진입 시 query.entities.length + checkExistence.length 가 사전 임계치(예: 10만) 를 넘으면 (a) child 를 --max-old-space-size 를 상향(예: 12 GB, 컨테이너 메모리 여유 확인 후) 해 재기동하거나 (b) 비교 job 을 조기 실패 처리하고 revision 상태에 명시적 error code (BimRevisionPayloadTooLarge 등) 로 표기. 근거: 로그의 mark-compact 가 7909→7916 MB 로 거의 회수 못 함 → old-space 확장 없이는 재시도해도 같은 결과.
  • Child heap 명시 지정: child-process.manager.ts:74fork(...) 옵션에 execArgv: ['--max-old-space-size=<N>'] 를 명시적으로 넘겨 부모 argv 상속에 의존하지 않도록 하고, 값 결정 로직을 환경변수(예: BIM_COMPARE_CHILD_MAX_OLD_SPACE_MB) 로 이관. 근거: 현재는 부모 컨테이너 진입 스크립트(80_run-agent.sh) 를 바꾸지 않으면 child 만 조정 불가.

장기 개선 (재발 방지)#

  • Streaming/batching in forge-agents extract: 154k entity 대비 O(n) 배열/객체 사본을 여러 번 만드는 코어 로직이 있다면 batch 단위(예: 10k) 로 자르고 결과를 diff/merge 하는 파이프라인으로 리팩터. uncertain — @cupix/forge-agents 내부 구현은 이번 조사 범위 밖. 별도 티켓 필요.
  • 프로세스별 RSS/heap 메트릭을 Datadog 에 노출: child heap used, mark-compact 빈도, entities.length 를 히스토그램/gauge 로 상시 관측해 임계 근접을 사전 경보로 잡음.
  • BIM revision 파일 크기/entity 수 상한을 제품 정책으로 명시. 임계 초과 시 사용자에게 revision 을 분할 업로드하도록 UX 안내.

Monitoring#

Release dashboard 에 넣을 timeseries 쿼리 예시 (writing-datadog-monitoring-queries 규칙 준수 — pipe/stats/by(...) 미사용):

  • Child OOM 발생율:
text
sum:trace.apm.request{service:cupixworks-any-bimrevision-agent,resource_name:runBimCompare,status:error}.as_count()
  • V8 OOM stderr 감지용 로그 카운트:
text
logs("service:cupixworks-any-bimrevision-agent status:error \"Ineffective mark-compacts near heap limit\"").index("*").rollup("count").last("1h")
  • Bim revision 실패 카운트(부모 error path):
text
logs("service:cupixworks-any-bimrevision-agent status:error \"BimRevisionService::run | error\"").index("*").rollup("count").last("1h")
  • Child process close (signal) 카운트:
text
logs("service:cupixworks-any-bimrevision-agent status:error \"Child process closed\"").index("*").rollup("count").last("1h")

알림: Ineffective mark-compacts 로그 카운트가 지난 1시간 대비 급증하거나 >=1/시간 이면 clark-vdc 채널로 알림.

Risk Assessment#

  • Risk level: medium — 단일 revision 실패이나, 대형 BIM revision 은 계속 유입되므로 recurrence 가능성 있음. 사용자 노출 영향은 revision 하나의 comparison 결과가 Error 로 마감되는 수준.
  • 예상 복잡도: standard — stderr aggregation 은 소폭 변경, threshold/execArgv 조정도 명확. forge-agents 내부 최적화는 별도 큰 작업.