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-agent 의 BimCompareProcess 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#
- 2026-07-21 20:53:10 KST —
BimRevisionService::runBimCompare | begin— 부모 서비스가 child 로 비교 요청 시작 - 2026-07-21 20:53:11 KST —
runBimCompare | params - query.entities.length: 154331, checkExistence.length: 57346— 대형 페이로드 전송 - 2026-07-21 21:00:39 KST —
Child process stderr: FATAL ERROR: Ineffective mark-compacts near heap limit— V8 이 7909→7916 MB 근처에서 mark-compact 반복 실패 후 OOM abort - 2026-07-21 21:01:45 KST —
Child process closed,Process killed by signal SIGABRT— child 종료, 부모의rejectAllPending이 실행되고BimRevisionService::run | error: "Process killed by signal SIGABRT"로 revision 상태가Error로 마감
Error Log#
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-agents 의 extract 가 두 개 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 로 전달한다:
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 의 모델 데이터를 동시에 로드해 비교한다:
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 로그로 흘린다:
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 한계에서 동작한다:
#!/bin/bash
node --max-old-space-size=8192 /tmp/agent/dist/app.cjs \
Log Evidence#
Datadog query (재현):
service:cupixworks-any-bimrevision-agent status:error "ChildProcessManager"
Time range: 2026-07-21T11:55:00Z → 2026-07-21T12:05:00Z.
핵심 stderr 시퀀스 (KST):
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_NewArray→NewFixedArrayWithFiller— 큰 배열 할당 시점에 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), SIGTERM 은 stop() 경로 (: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 를 라인 단위 individuallogger.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 SIGABRTatchild-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:74의fork(...)옵션에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 발생율:
sum:trace.apm.request{service:cupixworks-any-bimrevision-agent,resource_name:runBimCompare,status:error}.as_count()
- V8 OOM stderr 감지용 로그 카운트:
logs("service:cupixworks-any-bimrevision-agent status:error \"Ineffective mark-compacts near heap limit\"").index("*").rollup("count").last("1h")
- Bim revision 실패 카운트(부모 error path):
logs("service:cupixworks-any-bimrevision-agent status:error \"BimRevisionService::run | error\"").index("*").rollup("count").last("1h")
- Child process close (signal) 카운트:
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 내부 최적화는 별도 큰 작업.