ChildProcessManager::setupEventHandlers | Child process stderr: 10: 0x10c69a6 v8::internal::Factory:
RCA: BIM revision compare child process OOM (v8 Factory::AllocateRaw)
Overview#
What Happened#
2026-07-21 21:00:39 KST, cupixworks-any-bimrevision-agent(clark-vdc, us-west-2)에서 BimCompareProcess child process 가 v8 heap OOM 으로 SIGABRT 종료했다. bim_id 17216, src_revision 25433(V5) vs prev_revision 21647(V4) 비교 작업 중 154,331 created + 57,346 deleted = 211,677 개 element 를 로드한 뒤, forge-agents 의 ForgeAgent.extract 호출이 8GB 힙 한도를 넘어서서 "Ineffective mark-compacts near heap limit — Allocation failed" 로 종료했다. 이 25-cluster 이벤트는 동일 job 하나에서 발생한 stack trace/GC 로그의 각 라인이 서로 다른 fingerprint 로 클러스터링된 결과이며, 실제 impact 는 단일 BIM revision job 1건이다.
Quick Facts#
| Field | Value |
|---|---|
| exception.class | FATAL ERROR: Ineffective mark-compacts near heap limit |
| exception.message | Allocation failed - JavaScript heap out of memory |
| top_frame | v8::internal::Factory::AllocateRaw (child_process pid=45) |
| runtime | Node.js on compute-base:22.04, --max-old-space-size=8192 |
| deploy | cupix-tesla-bim-revision-agent v1.10.9, @cupix/forge-agents@10.21.0, agent_version 10.98.0 |
| env | production, us-west-2, tenant cupix, team clark-vdc |
Affected Teams#
| Team / Domain | Error Count | Impact |
|---|---|---|
| clark-vdc (BIM revision) | 1 job (bim_id 17216, revision 25433) | Revision compare 실패 → bim_comparison_state=Error. si_trace_id a052780a-c69b-457d-b72d-3aa6332272a3. |
Timeline#
- 2026-07-21 20:47:03 KST —
loadAllElementsbegin (bim_id 17216, facilityKey 3fq7tj) - 2026-07-21 20:53:08 KST —
loadAllElementsend (211,677 elements: created 154,331, deleted 57,346) - 2026-07-21 20:53:10 KST —
REVISE-BEGINsrc 25433(V5) vs prev 21647(V4) - 2026-07-21 20:53:11 KST —
runBimCompareparams (query.entities.length 154,331, checkExistence.length 57,346) — child process 로 넘김 - 2026-07-21 21:00:39 KST — child process stderr flush: Last GC Mark-Compact 7916.1 → 7916.1 MB (0 회수), FATAL OOM
- 2026-07-21 21:01:45 KST — Child process close SIGABRT,
BimRevisionService::run | error: "Process killed by signal SIGABRT (code: null)". elapsed_ms 883,421 (약 14.7 분) - 2026-07-21 21:01:45 KST —
REVISE-ENDstatus=error,bim_comparison_state=Error
Error Log#
ChildProcessManager::setupEventHandlers | Child process stderr: 10: 0x10c69a6 v8::internal::Factory::AllocateRaw(int, v8::internal::AllocationType, v8::internal::AllocationAlignment) [/usr/bin/node]
Impact#
- Service:
cupixworks-any-bimrevision-agent - Team: clark-vdc
- 발생 횟수: 1 (단일 job — cluster 25건은 동일 stderr flush 의 라인별 fingerprint)
- 최초 발생: 2026-07-21 21:00:39 KST
- 최근 발생: 2026-07-21 21:00:39 KST
Root Cause Summary#
BimRevisionService.runBimCompare 는 bim_id 17216 의 대규모 revision(154,331 개 created element + 57,346 개 removed element)을 comparison 하기 위해 전체 element 목록을 그대로 query.entities 와 query.checkExistence 로 packing 하여 BimCompareManager.execute → ChildProcessManager.execute('execute', params) 로 child process 에 IPC 전달한다. child process(BimCompareProcess.execute)는 forge-agents 10.21.0 의 ForgeAgent.extract 를 호출하여 두 개 Forge SVF2 URN (V4/V5 IAD157 Coordination Model) 을 다운로드/파싱하고 tree-path 매칭을 수행한다. 이 워크로드가 child process 의 --max-old-space-size=8192 (8GB) 한도를 초과하여 mark-compact 가 회수량 0MB 로 실패하고 (current mu = 0.006) v8 이 FatalProcessOutOfMemory 로 abort 했다. 즉, 알고리즘 단일 job 의 workload 가 heap ceiling 을 초과한 케이스이며, code bug 는 아니지만 element 수가 큰 경우 (>150k) 를 다루는 방어 로직 (스트리밍/청킹/조기 exit) 이 없어서 재발 위험이 높다.
Technical Analysis#
Code Path#
- Entry point:
packages/cupix-tesla-bim-revision-agent/src/bim-revision-service.ts:150(BimRevisionService.run) - Element 로딩:
bim-revision-service.ts:328-350(loadAllElements) — 211,677 개 element 를 메모리에 CPElement 객체로 보유 - Child process invoke:
bim-revision-service.ts:647(this.bimCompareManager.execute(params)) - Manager → IPC:
packages/cupix-tesla-bim-revision-agent/src/manager/bim-compare.manager.ts:65(this.childProcessManager.execute<ForgeAgent.Result>('execute', params)) - Child fork 지점:
packages/base/src/manager/child-process.manager.ts:74(fork(actualScriptPath, { stdio: [...]})—execArgvoverride 없음 → parent 의--max-old-space-size=8192를 상속) - 실제 OOM 발생:
packages/cupix-tesla-bim-revision-agent/src/process/bim-compare.process.ts:72(await ForgeAgent.extract(...)inside child) - 파열 지점: v8 native
Factory::AllocateRaw— Forge model download/tree traversal 중 array/object allocation 실패
Parent 서비스가 element 전체 목록을 그대로 넘기는 부분:
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(), // 154,331개
checkExistence: checkExistenceList, // 57,346개
levels: this.iLevels,
bimSetting: bimSetting,
propNames: ['Volume'],
customPropNames: cpBim.getCustomPropNames(),
configuration: { boxTolerance }
}
};
Child process 는 이 params 을 그대로 forge-agents 로 위임:
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
}
});
Fork 는 execArgv 를 상속하므로 child 도 8GB 한도 (--max-old-space-size=8192) 를 가지지만, workload 자체가 이 한도를 초과했다:
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;
}
Parent 는 child 종료 시 pending IPC 를 모두 reject 하여 runBimCompare 가 throw 하고, run() 의 catch 에서 상태를 Error 로 업데이트한다:
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);
});
기대 동작: 대용량 revision(≥150k element)도 안정적으로 comparison 완료. 실제 동작: child process 가 힙 한도에 도달하여 mark-compact 회수량 0MB → v8 FatalProcessOutOfMemory → SIGABRT.
Log Evidence#
Datadog query (재현):
service:cupixworks-any-bimrevision-agent status:error @environment:production "ChildProcessManager::setupEventHandlers"
service:cupixworks-any-bimrevision-agent (17216 OR "runBimCompare" OR "loadAllElements")
로드 규모 및 job 시작:
2026-07-21 20:53:08 info BimRevisionService::loadAllElements | end - 211677 cpElements loaded - created: 154331, deleted: 57346
2026-07-21 20:53:10 info 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="}
2026-07-21 20:53:11 info BimRevisionService::runBimCompare | params - query.entities.length: 154331, checkExistence.length: 57346, ...
Child process OOM stderr (v8 GC 실패 증거 — 회수량 0MB, mutator utilization 0.6%):
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: 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: 3: 0xee85e7 v8::internal::V8::FatalProcessOutOfMemory(...)
2026-07-21 21:00:39 error Child process stderr: 10: 0x10c69a6 v8::internal::Factory::AllocateRaw(...)
종료 및 상태 업데이트:
2026-07-21 21:01:45 error ChildProcessManager::setupEventHandlers | Child process exited (signal=SIGABRT)
2026-07-21 21:01:45 error ChildProcessManager::setupEventHandlers | Child process closed
2026-07-21 21:01:45 error BimRevisionService::run | error: "Process killed by signal SIGABRT (code: null)"
2026-07-21 21:01:46 info BimRevisionService::REVISE-END | {"bim_id":17216,"src_revision_id":25433,"prev_revision_id":21647,"elapsed_ms":883421,"status":"error","error_message":"Process killed by signal SIGABRT (code: null)","counts":{"total":0,"modified":0,"removed":0,"exist":0}}
Heap ceiling 근거 (node --max-old-space-size=8192):
node --max-old-space-size=8192 /tmp/agent/dist/app.cjs \
fork() 는 execArgv 를 상속하므로 child 도 8GB 한도를 사용 (log 의 (8233.8) — 최대 heap 이 약 8192MB + 소량의 external — 와 일치).
Hypotheses Considered#
| # | Hypothesis | Evidence for | Evidence against | Verdict |
|---|---|---|---|---|
| H1 | Child process (BimCompareProcess) 의 forge-agents ForgeAgent.extract 처리량이 8GB heap 한도를 초과하여 v8 OOM 발생 |
154,331 entities + 57,346 checkExistence 페이로드; GC 로그 7916/8192 MB 도달; Ineffective mark-compacts; Factory::AllocateRaw frame |
— | Confirmed |
| H2 | Parent 프로세스가 IPC 로 넘긴 대형 payload 가 child 초기 힙을 팽창시켜 OOM 유발 (IPC serialization 문제) | params 에 211k+ element 참조 데이터 포함 (query.entities, checkExistence); Node IPC 는 JSON serialize |
GC 로그 시각(814987 ms, 816543 ms)은 child 시작 후 ~13.6분에서 발생. 시작 직후가 아니라 comparison 진행 중 누적된 것 → IPC payload 자체보다 ForgeAgent.extract 내부 처리가 주 원인. Payload 는 기여했지만 root 는 아님 |
Rejected (contributing, not root) |
| H3 | 무한 루프 / memory leak (반복적으로 동일 element 를 alloc) | Retention loop 관측되면 짧은 시간에 heap 팽창 | Job 이 정상 14.7 분 동안 진행되며 runBimCompare 실행 중 점진적으로 heap 소진; leak 시그니처(짧은 시간 급증) 없음. elapsed_ms 883421 는 정상적인 대규모 compare 시간대 |
Rejected |
| H4 | Forge API 응답이 손상되어 파싱 중 무한 재귀 등이 발생 | — | Forge 오류/재시도 로그 없음; child 는 SIGABRT (OOM) 로 종료, exception 이 아님 | Rejected |
| H5 | External dependency 장애 (Forge/S3) | — | status-board 는 svc:cupixworks-any-bimrevision-agent::unknown 스코프로 반환, dep:* 아님; 다른 서비스 동시 에러 없음 |
Rejected |
| H6 | Cluster 25건 = 실제 25건의 job 실패 | 25개 fingerprint 가 동시에 발생 | 이벤트 시각이 모두 2026-07-21T12:00:39.174~196Z 범위 (22ms 내 flush) 이며, 로그 검색 결과 REVISE-END 는 1건, si_trace_id 도 하나. child stderr 의 각 stack line 이 서로 다른 fingerprint 로 clustering 됨 |
Rejected (실제 impact = 1 job) |
Fix Recommendation#
즉시 조치 (Critical)#
- 재시도 및 알림: 해당 bim_id 17216 V4→V5 revision 은 현재
bim_comparison_state=Error상태이므로 clark-vdc 담당자와 사용자에게 결과를 안내하고, 필요 시 workload 를 나눠 (예: 분할 model / 부분 URN) 재시도할지 결정. 코드 변경 없이 운영 조치. - 런타임 heap 상향 검토:
applications/agents/docker_services/80_run-agent.sh:3의--max-old-space-size=8192를 상향 (예: 12288 또는 16384) 하기 전에 반드시 컨테이너 메모리 limit(k8s pod / ECS task) 을 함께 상향해야 함 — 인프라 팀과 조율 필요. Node 프로세스만 상향하면 OOMKilled 로 대체될 뿐 근본 해결 아님.- 주의: 이 항목은 인프라 리소스 조율이 필요하므로 자동 code-fix 대상에서 제외 (fix-scope filtering 규칙에 따라 조율 필요 항목).
단기 개선 (1주 이내)#
- Child process 에 명시적 execArgv 지정:
packages/base/src/manager/child-process.manager.ts:74fork()호출에execArgv: ['--max-old-space-size=<child_limit>']를 추가하여 child 힙을 parent 와 독립적으로 관리. BIM 비교처럼 무거운 워크로드는 child 를 더 크게, 그 외 agent 는 작게 유지할 수 있음. - 워크로드 pre-check + 조기 실패:
BimRevisionService.runBimCompare(bim-revision-service.ts:603이후) 진입 시query.entities.length + checkExistence.length가 임계값 (예: 150k) 을 넘으면ErrorCode.Agent.BimRevisionExtractorInvalidResult계열의 명시적 코드로 조기 실패시켜 SIGABRT 대신 상태 보고 가능하게 함. 이후 병렬 실행 여부(다른 agent 를 죽이지 않도록)도 함께 검토. - Compare 결과 파이프라인의 스트리밍 검토:
BimCompareProcess.execute(bim-compare.process.ts:72) 에서ForgeAgent.extract가 리턴하는 대형Result를 파일/청크로 흘려서 parent 로 IPC 하지 않고 디스크 경유로 넘기는 방식 검토 (파일 경로만 IPC).
장기 개선 (재발 방지)#
- Chunked/Streaming Forge comparison: forge-agents 팀과 협의하여
ForgeAgent.extract에 chunked query API (모델별 or level 별 서브셋으로 나눠서 처리) 도입. 현재는 모든 entity 를 한 번에 넘기지만, level(3개) 또는 model 단위로 분할하면 각 파티션의 peak heap 이 낮아진다. - 관측성 강화: child process 에
--heap-prof/NODE_OPTIONS=--heapsnapshot-near-heap-limit=3등 자동 heap dump 수집을 옵션으로 붙여, 재발 시 실제 retention set 파악. S3 로 dump 업로드 후 후속 분석. - 워크로드 캐패시티 모니터링:
cp_elements_count,entities.length,elapsed_ms를 Datadog custom metric 으로 emit 하여, 실패 임계값과 성공 분포를 대시보드로 관리.
Monitoring#
Datadog 쿼리 예시 (release dashboard 위젯용 timeseries):
service:cupixworks-any-bimrevision-agent status:error "heap out of memory"
service:cupixworks-any-bimrevision-agent status:error "Process killed by signal SIGABRT"
service:cupixworks-any-bimrevision-agent "ChildProcessManager::setupEventHandlers | Child process closed"
service:cupixworks-any-bimrevision-agent "REVISE-END" @status:error
추가 권장:
- Alert:
heap out of memory발생 1건 이상 5분 within → clark-vdc 채널 알림. - Trend:
cp_elements_count분포(P95/P99) 를 log-based metric 으로 만들어 대규모 revision workload 증가 추적.
Risk Assessment#
- Risk level: medium — 단일 job 실패로 즉각적인 서비스 전체 impact 는 없지만, BIM 규모 증가 추세에서 동일 크기(≥150k element) revision 이 다시 들어오면 재발 가능. Clark VDC 의 IAD157 model (V6+) 후속 revision 이 실패할 위험 존재.
- 예상 복잡도: standard — pre-check 조기 실패 + child execArgv 명시화는 국소 변경. Chunked comparison 은 forge-agents 협업 필요 (critical 로 격상 가능).