ES /docs

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=25433error 로 종료했다. 이 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::RecomputeLimitsRuntime_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#

  1. 2026-07-21 20:47:04 KST — job 시작 (REVISE-END 로그의 elapsed_ms:883421 로 역산)
  2. 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)
  3. 2026-07-21 21:01:45 KST — child process 종료: close / exit 이벤트 및 Process killed by signal SIGABRT (code: null)
  4. 2026-07-21 21:01:46 KSTBimRevisionService::REVISE-END (status:"error", elapsed_ms:883421), workspace cleanup, SQS 메시지 삭제 (재시도 없음)
  5. 2026-07-21 21:01:45 KST — status-board incident 2026-07-21-svc-cupixworks-any-bimrevision-agent--unknown-1 자동 resolved

Error Log#

Datadog Logs

text
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-agentsForgeAgent.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.0CompareExtractor.compare / GridAlignment.computeGridCorrection — stderr 스택에서 확인
  • Stderr relay (parent): packages/base/src/manager/child-process.manager.ts:138-141

부모 프로세스 기동 스크립트가 8GB 힙 상한으로 Node 를 띄운다. 이 값이 child 로 그대로 상속된다.

applications/agents/docker_services/80_run-agent.sh:3-9bash
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

ChildProcessManagerfork() 호출 시 execArgv 를 명시하지 않아 Node 기본 동작에 따라 부모의 CLI flag 를 상속한다. 즉 child 도 동일한 8GB heap 을 갖는다.

applications/agents/packages/base/src/manager/child-process.manager.ts:73-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}`);

Child process 는 IPC 로 execute 메시지를 받아 forge-agents 의 extract 를 호출한다. 이 호출이 두 revision 의 model 데이터를 메모리에 올려 비교한다.

applications/agents/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
            }
        });

Child 의 stderr 는 부모의 logger.error 로 라인 단위 전달되며, 각 라인이 별도 로그 레코드가 된다. V8 OOM 시 native stack 이 수십 줄로 출력되고 각 줄이 개별 error 로그가 되므로 error-sweeper collector 가 fingerprint 별로 25 개의 cluster 를 생성했다.

applications/agents/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}`);
});

기대 동작: 정상 job 은 힙 사용을 반환한 뒤 종료. 실제 동작: 힙이 8GB 근처에서 GC 후에도 회수되지 않아(live set 이 이미 그 수준) V8 이 Ineffective mark-compacts 로 판정하고 프로세스를 abort 함.

Log Evidence#

Datadog 쿼리 (재현용):

text
service:cupixworks-any-bimrevision-agent status:error @environment:production "ChildProcessManager::setupEventHandlers"
text
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):

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

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

SQS 메시지는 정상 삭제되어 재시도가 발생하지 않았다:

text
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:74fork() 호출에 execArgv: ['--max-old-space-size=<N>'] 를 넘겨 부모/자식의 힙 한도를 코드에서 분리 가능하게 한다. 부모는 SQS/IPC 만 처리하므로 힙 예산이 작아도 되고, child 는 필요시 상향할 수 있다. 컨테이너 물리 메모리(RAM) 와 남는 예산을 확인한 뒤 값을 결정할 것 (현재 8GB → 예: 10–12GB).
  • BIM revision 데이터 규모 기반 사전 필터/청킹: bim-compare.manager.ts:53execute() 진입 시 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 에서 처리):
text
service:cupixworks-any-bimrevision-agent "JavaScript heap out of memory"
  • SIGABRT 로 종료된 child process 횟수:
text
service:cupixworks-any-bimrevision-agent "Process killed by signal SIGABRT"
  • BIM revision 전체 실패율 (성공 대비):
text
service:cupixworks-any-bimrevision-agent "BimRevisionService::REVISE-END"

(dashboard 위젯에서 @status:error vs @status:success 로 그룹화하여 실패율 계산)

  • Child process 비정상 종료 이벤트 (일반):
text
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.