ForgeAgent.extract heap OOM on large BIM revisions
RCA: BIM Compare child process OOM crash (V8 heap exhausted)
Overview#
What Happened#
2026-07-21 21:00:39 KST 에 cupixworks-any-bimrevision-agent (production, us-west-2) 의 BIM 비교 child process 가 V8 FATAL ERROR: Ineffective mark-compacts near heap limit — JavaScript heap out of memory 로 crash 하였다. 부모 프로세스가 SIGABRT 를 감지하고 job bim_id=17216, src_revision_id=25433 을 error 로 종료했다. 이 cluster 파일은 crash 직후 stderr 로 쏟아진 V8 native stack frame 한 줄(4: 0x10fb205 [/usr/bin/node]) 이 fingerprint 로 채택된 결과이며, 동일 crash 이벤트로 총 25 개의 sibling cluster 가 status-board incident 2026-07-21-svc-cupixworks-any-bimrevision-agent--unknown-1 로 묶여 있다.
Quick Facts#
| Field | Value |
|---|---|
| exception.class | V8 FATAL ERROR (OOM, not a JS exception) |
| exception.message | Ineffective mark-compacts near heap limit Allocation failed - JavaScript heap out of memory |
| top_frame | v8::internal::Heap::RecomputeLimits → Runtime_NewArray (native) |
| runtime | Node.js on /usr/bin/node, --max-old-space-size=8192 |
| deploy | forge-agents 10.98.0 (from stderr paths) |
| env | production, us-west-2, tenant cupix |
Affected Teams#
| Team / Domain | Error Count | Impact |
|---|---|---|
| clark-vdc (bim-revision-agent) | 25 clusters / 1 job | 단일 BIM revision job 실패 (bim_id=17216, src_revision_id=25433); SQS 메시지가 정상 삭제되어 재시도는 발생하지 않음 |
Timeline#
- 2026-07-21 20:47:04 KST — job 시작 (REVISE-END 로그의
elapsed_ms:883421로 역산) - 2026-07-21 21:00:39 KST — child process V8 heap 8GB 도달,
Mark-Compact 7916.1 -> 7916.1 MB후 OOM,FATAL ERROR및 native stack dump (본 cluster 의 first/last_seen) - 2026-07-21 21:01:45 KST — child process 종료:
close/exit이벤트 및Process killed by signal SIGABRT (code: null) - 2026-07-21 21:01:46 KST —
BimRevisionService::REVISE-END(status:"error",elapsed_ms:883421), workspace cleanup, SQS 메시지 삭제 (재시도 없음) - 2026-07-21 21:01:45 KST — status-board incident
2026-07-21-svc-cupixworks-any-bimrevision-agent--unknown-1자동 resolved
Error Log#
ChildProcessManager::setupEventHandlers | Child process stderr: 4: 0x10fb205 [/usr/bin/node]
Impact#
- Service:
cupixworks-any-bimrevision-agent - Team: clark-vdc
- 발생 횟수: 1 (cluster) — 그러나 동일 crash 이벤트가 25 개 sibling cluster 로 파편화됨
- 최초 발생: 2026-07-21 21:00:39 KST
- 최근 발생: 2026-07-21 21:00:39 KST
Root Cause Summary#
BIM 비교를 담당하는 fork 된 child process 가 실행 도중 V8 old-space heap 상한(--max-old-space-size=8192, 8GB) 을 초과하여 Ineffective mark-compacts 판단 후 OOM 으로 종료되었다. Child process 는 부모(80_run-agent.sh) 로부터 execArgv 를 상속받아 동일한 8GB 힙 제한을 갖는데, @cupix/forge-agents 의 ForgeAgent.extract 가 두 BIM revision(urn=25433 신규, previousUrn=21647 이전) 의 SVF2 model data (AECModelData.json) 를 로드하고 CompareExtractor.compare 로 element 배열을 구성하는 과정에서 old-space 가 채워졌다. GC log 의 Mark-Compact 7909.6 -> 7909.6 MB 는 GC 후에도 회수되지 않는 live object 가 이미 힙의 대부분을 차지하고 있었음을 의미하며, 마지막 실패 지점 native frame 이 Factory::AllocateRaw → AllocateRawArray → NewFixedArrayWithFiller → Runtime_NewArray 인 것으로 보아 큰 JS Array 신규 할당이 트리거였다. 단일 job(약 15 분 실행) 의 데이터 규모 자체가 8GB 힙 예산을 넘어선 것이 근본 원인이다.
Technical Analysis#
Code Path#
- Entry point:
docker_services/80_run-agent.sh:3— 부모 프로세스에--max-old-space-size=8192지정 - Fork:
packages/base/src/manager/child-process.manager.ts:74 - Child target:
packages/cupix-tesla-bim-revision-agent/src/process/bim-compare.process.ts:72 - Failure point: forge-agents
10.98.0의CompareExtractor.compare/GridAlignment.computeGridCorrection— stderr 스택에서 확인 - Stderr relay (parent):
packages/base/src/manager/child-process.manager.ts:138-141
부모 프로세스 기동 스크립트가 8GB 힙 상한으로 Node 를 띄운다. 이 값이 child 로 그대로 상속된다.
node --max-old-space-size=8192 /tmp/agent/dist/app.cjs \
--env_name $CPX_ENVIRONMENT_NAME \
--api_endpoint $CPX_API_ENDPOINT \
--aws_region $AWS_REGION \
--aws_queue_name $AWS_QUEUE_NAME \
--encoded_auth_params $CPX_ENCODED_AUTH_PARAMS \
--team_domain $CPX_TEAM_DOMAIN
ChildProcessManager 는 fork() 호출 시 execArgv 를 명시하지 않아 Node 기본 동작에 따라 부모의 CLI flag 를 상속한다. 즉 child 도 동일한 8GB heap 을 갖는다.
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}`);
Child process 는 IPC 로 execute 메시지를 받아 forge-agents 의 extract 를 호출한다. 이 호출이 두 revision 의 model 데이터를 메모리에 올려 비교한다.
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
}
});
Child 의 stderr 는 부모의 logger.error 로 라인 단위 전달되며, 각 라인이 별도 로그 레코드가 된다. V8 OOM 시 native stack 이 수십 줄로 출력되고 각 줄이 개별 error 로그가 되므로 error-sweeper collector 가 fingerprint 별로 25 개의 cluster 를 생성했다.
this.process.stderr?.on('data', (data: Buffer) => {
const text = data.toString();
logger.error(`ChildProcessManager::setupEventHandlers | Child process stderr: ${text}`);
});
기대 동작: 정상 job 은 힙 사용을 반환한 뒤 종료. 실제 동작: 힙이 8GB 근처에서 GC 후에도 회수되지 않아(live set 이 이미 그 수준) V8 이 Ineffective mark-compacts 로 판정하고 프로세스를 abort 함.
Log Evidence#
Datadog 쿼리 (재현용):
service:cupixworks-any-bimrevision-agent status:error @environment:production "ChildProcessManager::setupEventHandlers"
service:cupixworks-any-bimrevision-agent ("heap out of memory" OR "FATAL ERROR" OR "Allocation failed" OR "JavaScript heap")
핵심 stderr 시퀀스 (2026-07-21 21:00:39 KST):
Child process stderr: <--- Last few GCs --->
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: [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: <--- JS stacktrace --->
Child process stderr: FATAL ERROR: Ineffective mark-compacts near heap limit Allocation failed - JavaScript heap out of memory
Child process stderr: ----- Native stack trace -----
Child process stderr: 1: 0xb78db3 node::OOMErrorHandler(...)
Child process stderr: 2: 0xee8300 v8::Utils::ReportOOMFailure(...)
Child process stderr: 3: 0xee85e7 v8::internal::V8::FatalProcessOutOfMemory(...)
Child process stderr: 4: 0x10fb205 [/usr/bin/node]
...
Child process stderr: 10: 0x10c69a6 v8::internal::Factory::AllocateRaw(...)
Child process stderr: 11: 0x10b7e4a v8::internal::FactoryBase<...>::AllocateRawArray(...)
Child process stderr: 12: 0x10b7fb4 v8::internal::FactoryBase<...>::NewFixedArrayWithFiller(...)
Child process stderr: 16: 0x12cbb55 v8::internal::ArrayConstructInitializeElements(...)
Child process stderr: 17: 0x1512628 v8::internal::Runtime_NewArray(...)
average mu = 0.091, current mu = 0.008 — mutator utilization 이 극단적으로 낮아 프로세스가 GC 에 시간을 대부분 소비하고 있었음(전형적 heap-thrash / GC death-spiral).
Job 종료 로그 (2026-07-21 21:01:45–21:01:46 KST):
ChildProcessManager::setupEventHandlers | Child process closed
ChildProcessManager::setupEventHandlers | Child process exited
Process killed by signal SIGABRT (code: null)
BimRevisionService::run | error: "Process killed by signal SIGABRT (code: null)"
{
"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 }
}
SQS 메시지는 정상 삭제되어 재시도가 발생하지 않았다:
AwsQueueManager::deleteMessage | begin - queue url: https://sqs.us-west-2.amazonaws.com/002596530511/cupix-tesla-bim-revision-agent-production
AwsQueueManager::deleteMessage | end - message id: dd2cac85-c941-428d-8fbb-bb97168d9bfe
참고: 21:42 KST 대에 별도로 ForgeUtilsError: Resource not found (errorCode: 'API_NOT_FOUND') — Autodesk AECModelData.json 404 — 로그가 존재하지만 이는 다른 job 의 별도 이벤트이며, 본 cluster 의 21:00:39 crash 와는 무관하다 (타임스탬프 및 실패 모드가 다름).
Hypotheses Considered#
| # | Hypothesis | Evidence for | Evidence against | Verdict |
|---|---|---|---|---|
| H1 | Child process V8 heap 상한(8GB) 초과로 인한 OOM | stderr 에 FATAL ERROR: ... JavaScript heap out of memory, Mark-Compact 7916.1 -> 7916.1 MB, average mu = 0.091, native frame Factory::AllocateRaw → NewFixedArrayWithFiller → Runtime_NewArray; 부모 스크립트 --max-old-space-size=8192 (80_run-agent.sh:3); SIGABRT 종료 |
— | Confirmed |
| H2 | 컨테이너 memory cgroup limit 에 의한 OOM kill (Linux OOM killer) | 프로세스가 종료됨 | 종료 signal 이 SIGABRT (V8 자체 abort) 이지 SIGKILL 이 아님; V8 이 자체 FATAL ERROR 를 먼저 출력함 — 컨테이너 OOM 이라면 V8 stack trace 없이 즉시 SIGKILL |
Rejected |
| H3 | Autodesk Forge API 404 (API_NOT_FOUND) 로 인한 실패 |
같은 서비스에서 21:42 KST 에 ForgeUtilsError: Resource not found 스택 다수 관찰 |
본 cluster 의 first_seen 은 21:00:39 KST 로 42 분 차이; 21:00:39 이벤트의 stderr 는 V8 native stack 이지 ForgeUtilsError JS stack 이 아님 |
Rejected |
| H4 | ChildProcessManager 의 IPC / 메시지 누수로 부모 프로세스 힙이 부풀어 오름 | — | OOM stack 은 child pid [45:0x17bccd20] 에서 발생; 부모의 logger.error 는 정상 동작하여 stderr 를 계속 relay 하고 있었음. 종료 이벤트는 부모가 감지 (Child process closed) |
Rejected |
| H5 | forge-agents 10.98.0 특정 버전의 회귀 (regression) |
crash frame 이 CompareExtractor.compare, GridAlignment.computeGridCorrection 등 forge-agents 내부; forge-agents 버전이 stderr 경로에 하드코딩 관찰 |
단발성(1회) 이므로 데이터 특성 vs 버전 회귀를 구분할 정보 부족 | Inconclusive — needs verification |
Fix Recommendation#
즉시 조치 (Critical)#
- 별도 즉시 코드 변경 없음. 실패한 SQS 메시지는 이미 삭제되었고 재시도 대상이 아님. 다만 다음 항목을 오늘 중 결정할 것:
bim_id=17216, src_revision_id=25433에 대해 수동 재실행 여부 (clark-vdc 팀과 협의) — 데이터 규모가 유사하게 크면 아래 단기 개선을 선반영해야 재실행이 성공한다.
단기 개선 (1주 이내)#
- Child heap 상한을 명시적으로 부여:
packages/base/src/manager/child-process.manager.ts:74의fork()호출에execArgv: ['--max-old-space-size=<N>']를 넘겨 부모/자식의 힙 한도를 코드에서 분리 가능하게 한다. 부모는 SQS/IPC 만 처리하므로 힙 예산이 작아도 되고, child 는 필요시 상향할 수 있다. 컨테이너 물리 메모리(RAM) 와 남는 예산을 확인한 뒤 값을 결정할 것 (현재 8GB → 예: 10–12GB). - BIM revision 데이터 규모 기반 사전 필터/청킹:
bim-compare.manager.ts:53의execute()진입 시params.query.entities.length및 대상 model 크기를 로깅/게이팅하여, 임계치 초과 job 은 별도 파이프라인(더 큰 인스턴스, 청킹 처리) 으로 라우팅한다. 무제한 힙 증설은 근본책이 아니므로 방향성 제안. - OOM 재발 시 재시도 정책 명확화: 현재 SIGABRT 발생 → SQS delete 로 조용히 실패. 데이터 손실 위험이 있으므로
BimRevisionService::run의 catch 경로에서 OOM (혹은 SIGABRT) 을 구분하여 DLQ 또는 alert 로 보내는 정책을 검토.
장기 개선 (재발 방지)#
- Streaming/incremental BIM compare: forge-agents
CompareExtractor.compare가 전체 element 배열을 한 번에 메모리에 올리는 구조라면, 청크 스트리밍 또는 external sort 로 리팩터링을 forge-agents 유지 팀과 협의. - 관측성 강화: child process 의
process.memoryUsage()를 주기적으로(예: 30 초) IPC 로 부모에 보고하여 Datadog custom metric 으로 발행. OOM 임박 상태에서 사전 알림 가능. - Stderr fingerprint 그룹화: 본 crash 이벤트 하나가 25 개 error cluster 로 파편화된 것은 collector 관점에서 노이즈다.
Child process stderr:접두어를 벗기고 crash-window 로 묶는 fingerprint 규칙을 error-sweeper 쪽에 검토.
Monitoring#
writing-datadog-monitoring-queries 가이드에 따라 timeseries widget 에서 그대로 사용 가능한 쿼리로 작성.
- OOM crash 발생 횟수 (rollup 필요 시 dashboard 에서 처리):
service:cupixworks-any-bimrevision-agent "JavaScript heap out of memory"
- SIGABRT 로 종료된 child process 횟수:
service:cupixworks-any-bimrevision-agent "Process killed by signal SIGABRT"
- BIM revision 전체 실패율 (성공 대비):
service:cupixworks-any-bimrevision-agent "BimRevisionService::REVISE-END"
(dashboard 위젯에서 @status:error vs @status:success 로 그룹화하여 실패율 계산)
- Child process 비정상 종료 이벤트 (일반):
service:cupixworks-any-bimrevision-agent status:error "Child process closed"
알림 임계치 예시(모니터에서만 사용, widget 에서는 위 원본 쿼리를 사용):
heap out of memory가 15 분 내 1회 이상 → warning; 1 시간 내 3회 이상 → critical.
Risk Assessment#
- Risk level: medium — 현재까지 1 회 발생, 재시도 없이 job 이 조용히 실패하지만 데이터 상관관계상 대형 BIM revision 은 재발 가능. 다른 job 은 정상 처리되므로 서비스 전체는 계속 동작 중.
- 예상 복잡도: standard — child fork 옵션 조정과 game-day 검증은 표준 작업. 장기 개선(streaming compare) 은 별도로 critical.