ChildProcessManager::setupEventHandlers | Child process stderr: <--- Last few GCs --->
RCA: ChildProcessManager Child process stderr: <--- Last few GCs --->
Overview#
What Happened#
2026-07-21 21:00 KST경, cupixworks-any-bimrevision-agent 가 bim_id 17216 리비전 비교(V4 → V5)를 처리하던 도중 forge-agents 를 실행하는 child process 가 V8 FATAL ERROR: Ineffective mark-compacts near heap limit — JavaScript heap out of memory 로 죽었다. child 는 SIGABRT 로 종료되었고, BimRevisionService::run 은 Process killed by signal SIGABRT (code: null) 로 실패했다. 이 클러스터가 담고 있는 대표 로그는 OOM 직전 V8 이 stderr 로 출력한 <--- Last few GCs ---> 헤더이며, 같은 사건에서 24 건의 stderr 라인이 25 개 클러스터로 분리되어 수집됐다(status board 2026-07-21-svc-cupixworks-any-bimrevision-agent--unknown-1).
Quick Facts#
| Field | Value |
|---|---|
| exception.class | FATAL ERROR (V8 OOM in child process) |
| exception.message | Ineffective mark-compacts near heap limit Allocation failed - JavaScript heap out of memory |
| top_frame | packages/base/src/manager/child-process.manager.ts:140 (stderr forwarder) |
| child_process | packages/cupix-tesla-bim-revision-agent/src/process/bim-compare.process.ts |
| heap_at_death | ~7916 MB used / ~8233 MB limit (Node default cap) |
| exit_signal | SIGABRT (from V8 OOMErrorHandler → abort) |
| agent_version | 10.98.0 |
| env | production, us-west-2 |
Affected Teams#
| Team / Domain | Error Count | Impact |
|---|---|---|
| clark-vdc (BIM revision) | 25 클러스터 (동일 인시던트) | bim_id 17216 리비전 비교 1건 실패, si_trace_id a052780a-c69b-457d-b72d-3aa6332272a3 사용자에게 REV1 에러로 회귀 |
Timeline#
- 2026-07-21 20:47 KST —
BimRevisionService::loadAllElements | begin - bimId: 17216, facilityKey: 3fq7tj - 2026-07-21 20:53:08 KST —
loadAllElements | end - 211677 cpElements loaded - created: 154331, deleted: 57346 - 2026-07-21 20:53:10 KST —
BimRevisionService::REVISE-BEGIN | cp_elements_count: 211677 - 2026-07-21 20:53:11 KST —
BimRevisionService::runBimCompare | query.entities.length: 154331, checkExistence.length: 57346— child process 로 IPCexecute전달 - 2026-07-21 21:00:39 KST — child stderr 출력 시작.
<--- Last few GCs --->, Mark-Compact7916.1 (8233.8) -> 7916.1 (8233.8) MB,FATAL ERROR: Ineffective mark-compacts near heap limit→V8::FatalProcessOutOfMemory→ native stack trace - 2026-07-21 21:01:45 KST —
Child process closed/Child process exited, parent 는Process killed by signal SIGABRT (code: null)를 rethrow →BimRevisionService::REVISE-ENDstatus=error, elapsed_ms=883421 - 2026-07-21 21:17:11 KST — 후속 재시도로 보이는 실행에서
Forge error: INVALID_OTG_ERROR - OTG status is pending발생 (별도 실패이나 동일 리비전 재시도 흔적)
Error Log#
ChildProcessManager::setupEventHandlers | Child process stderr: <--- Last few GCs --->
Impact#
- Service:
cupixworks-any-bimrevision-agent - Team: clark-vdc
- 발생 횟수: 1 (representative). 동일 사건이 25 개 클러스터로 분리 (OOM stderr 라인 하나마다 별도 fingerprint)
- 최초 발생: 2026-07-21 21:00 KST
- 최근 발생: 2026-07-21 21:00 KST
Root Cause Summary#
BimRevisionService 가 bim_id 17216 의 V4 → V5 리비전 비교를 실행하면서 BimCompareManager 를 통해 bim-compare.process child 로 entities 154,331 + checkExistence 57,346 (총 211,677 element) 크기의 query 를 IPC 로 전달했다. child 는 ForgeAgent.extract (@cupix/forge-agents) 를 호출해 이 규모의 모델을 메모리에 로드/비교하는 과정에서 V8 old-space 가 기본 상한(~8 GB) 을 초과해 mark-compact 가 계속 실패하고 결국 V8::FatalProcessOutOfMemory 로 abort(SIGABRT) 되었다. ChildProcessManager::setupEventHandlers 는 child 의 stderr 를 그대로 logger.error 로 forward 하도록 되어 있어(child-process.manager.ts:138-141), OOM 시 V8 가 찍는 GC 리포트/네이티브 스택 라인 하나하나가 별개의 error 로그로 남았다. 원인은 stderr forwarding 이 아니라 child process 의 heap 이 이 규모의 BIM revision compare 를 감당하지 못한다는 것 이며, BimCompareManager 가 child 를 fork 할 때 heap 상한 확대(--max-old-space-size) 나 executeTimeoutMs 옵션도 전달하지 않는다.
Technical Analysis#
Code Path#
- Entry point:
packages/cupix-tesla-bim-revision-agent/src/bim-revision-service.ts→runBimCompare(log 상BimRevisionService::runBimCompare | beginat 20:53:10 KST) - Delegation:
BimCompareManager::execute(parent) →ChildProcessManager::execute('execute', params) - Failure point:
packages/cupix-tesla-bim-revision-agent/src/process/bim-compare.process.ts:72—ForgeAgent.extract(...)호출 도중 child 가 OOM abort - stderr surface:
packages/base/src/manager/child-process.manager.ts:138-141— V8 fatal 출력을 error 로그로 emit
Parent 는 child 를 fork 할 때 옵션을 넘기지 않는다. 즉 stdioMode 기본 inherit, executeTimeoutMs 없음, --max-old-space-size 설정 없음:
constructor() {
this.childProcessManager = new ChildProcessManager(__dirname, 'process/bim-compare.process');
}
ChildProcessManager 는 execArgv 를 지정하지 않고 default fork 를 호출하기 때문에 child Node 프로세스는 parent 의 NODE_OPTIONS 만 상속한다. Datadog 로그가 보여준 상한이 약 8233 MB 였다는 것은 컨테이너에 별도로 --max-old-space-size 가 지정돼 있지 않다는 뜻:
try {
this.process = fork(actualScriptPath, {
stdio: ['inherit', stdOut, stdErr, 'ipc'],
});
Child 의 실제 heavy work 지점:
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
}
});
params.query.entities 길이는 이번 실행에서 154,331 이었다(로그 참조). ForgeAgent.extract 는 두 revision 의 SVF2 model 을 로드해 tree-path 매칭을 수행하며 element 별 geometry/bounds 를 메모리에 유지한다. 이 크기가 V8 old-space 상한을 넘어서면서 OOM 이 발생했다.
Stderr forwarding 은 다음 지점에서 이루어지며, 이 forwarder 자체는 정상 동작하지만 V8 이 OOM 출력을 라인 단위로 흘리기 때문에 logger.error 호출이 20+회 반복된다 — 이것이 25 개 클러스터로 나뉜 이유:
this.process.stderr?.on('data', (data: Buffer) => {
const text = data.toString();
logger.error(`ChildProcessManager::setupEventHandlers | Child process stderr: ${text}`);
});
Close handler 에서 SIGABRT 로 반환되는 이유:
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));
Log Evidence#
Datadog 쿼리 (재현용):
service:cupixworks-any-bimrevision-agent "Child process stderr"
service:cupixworks-any-bimrevision-agent "BimCompareManager" OR "BimRevisionService" OR "BimCompareProcess"
BIM compare 시작 로그 — child 에 넘어간 workload 규모가 확인된다:
2026-07-21 20:53:11 INFO BimRevisionService::runBimCompare | params.query.entities.length: 154331, checkExistence.length: 57346, levels: [{"id":72846,"name":"ROOF"},{"id":72845,"name":"LEVEL 2"},{"id":72827,"name":"LEVEL 1"}], propNames: ["Volume"], customPropNames: ["Level","Reference Level","LENGTH","Length"]
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}
V8 OOM 출력 (child stderr → parent error log, 발췌):
2026-07-21 21:00:39 ERROR ChildProcessManager::setupEventHandlers | Child process stderr: <--- Last few GCs --->
2026-07-21 21:00:39 ERROR ChildProcessManager::setupEventHandlers | 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 ChildProcessManager::setupEventHandlers | 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 ChildProcessManager::setupEventHandlers | Child process stderr: <--- JS stacktrace --->
2026-07-21 21:00:39 ERROR ChildProcessManager::setupEventHandlers | 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 ChildProcessManager::setupEventHandlers | Child process stderr: ----- Native stack trace -----
2026-07-21 21:00:39 ERROR ChildProcessManager::setupEventHandlers | Child process stderr: 1: 0xb78db3 node::OOMErrorHandler(char const*, v8::OOMDetails const&) [/usr/bin/node]
2026-07-21 21:00:39 ERROR ChildProcessManager::setupEventHandlers | 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 ChildProcessManager::setupEventHandlers | Child process stderr: 16: 0x12cbb55 v8::internal::ArrayConstructInitializeElements(v8::internal::Handle<v8::internal::JSArray>, ...) [/usr/bin/node]
2026-07-21 21:00:39 ERROR ChildProcessManager::setupEventHandlers | Child process stderr: 17: 0x1512628 v8::internal::Runtime_NewArray(int, unsigned long*, v8::internal::Isolate*) [/usr/bin/node]
Child 종료 / parent 에러 propagation:
2026-07-21 21:01:45 ERROR ChildProcessManager::setupEventHandlers | Child process exited
2026-07-21 21:01:45 ERROR ChildProcessManager::setupEventHandlers | Child process closed
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)"
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}}
Status board (동일 사건이 25 개 클러스터로 분리된 증거):
scope: svc:cupixworks-any-bimrevision-agent::unknown
incident: 2026-07-21-svc-cupixworks-any-bimrevision-agent--unknown-1
cluster_ids: 25 (79974cd5..., 88b38893..., 2679796a..., ..., 339e3092...)
first event 12:00:39.174Z, last 12:01:45.911Z
Hypotheses Considered#
| # | Hypothesis | Evidence for | Evidence against | Verdict |
|---|---|---|---|---|
| H1 | BimCompareProcess 가 대규모 BIM revision(entities 154,331 / total 211,677 elements) 을 ForgeAgent.extract 로 처리하면서 V8 old-space 기본 상한(~8 GB) 을 넘어 OOM abort |
V8 stderr 에 Mark-Compact 7916.1 (8233.8) MB FATAL ERROR: Ineffective mark-compacts near heap limit, V8::FatalProcessOutOfMemory 프레임 명시. REVISE-BEGIN 로그의 cp_elements_count: 211677. BimCompareManager 가 fork 시 --max-old-space-size 나 heap 옵션 미전달 (bim-compare.manager.ts:27, child-process.manager.ts:73-77) |
— | Confirmed |
| H2 | 외부 의존성(Autodesk Forge) 장애로 인한 OOM (예: 응답 폭증) | 동시간대 status board 는 dep:* 인시던트 없음. 이후 21:17 에 INVALID_OTG_ERROR 가 나오지만 이는 별개의 후속 실행이며 OOM 이후 발생 |
21:00 시점 OOM 은 명확히 V8 heap 소진이며 Forge 응답이 아닌 in-process 자료 구조에서 터짐. entities.length 자체가 154k 로 이미 크다. | Rejected |
| H3 | ChildProcessManager 의 stderr forwarding 자체가 문제 (버그로 error 스팸 유발) |
대표 로그가 stderr forwarder 에서 나옴 | 소스 상 forwarder 는 단순히 stderr 라인을 그대로 error 로그로 emit 하고 있어 정상 동작. V8 이 fatal 시 20+ 라인을 stderr 로 뿌리는 특성 때문에 다수 클러스터로 나뉜 것이지, forwarder 가 원인이 아님. | Rejected |
| H4 | 부모 프로세스 leak 이 원인 | OOM 이 발생한 pid 는 [45:0x17bccd20] 로 child. parent 는 SIGABRT 를 관찰하고 정상 정리 (REVISE-END 로그) |
— | Rejected |
| H5 | executeTimeoutMs 미설정으로 인한 hang 이 OOM 을 유발 | BimCompareManager 가 timeout 없이 execute 호출 (bim-compare.manager.ts:65) |
Timeout 부재는 OOM 을 유발하지 않는다 — memory 사용량은 시간이 아닌 자료 구조 크기에 좌우. 다만 실패 회수 지연을 만든다는 side effect 는 있음 | Rejected (as root cause), 별도 개선 항목으로 이관 |
Fix Recommendation#
즉시 조치 (Critical)#
- Child process 힙 상한 확대:
packages/base/src/manager/child-process.manager.ts의fork호출에execArgv: ['--max-old-space-size=<NN>']를 전달할 수 있도록ChildProcessOptions에maxOldSpaceSizeMb옵션을 추가하고,BimCompareManager(packages/cupix-tesla-bim-revision-agent/src/manager/bim-compare.manager.ts:27) 에서 이 서비스의 컨테이너 메모리에 맞춰 값(예: 컨테이너 12–16 GB 인스턴스에서12288이상)을 지정한다. 값은 배포된 인스턴스 메모리 실측 후 결정. - 컨테이너 메모리 상향 검토 (인프라 팀 조율 필요): 현재 8 GB 근방에서 abort 되는 것으로 보아 pod/task 의 memory limit 도 함께 재조정해야 한다. 이 항목은 코드 변경만으로 완결되지 않으므로 자동 code-fix 대상 아님.
- workload 규모 방어:
BimRevisionService::runBimCompare(bim-revision-service.ts) 진입 시query.entities.length + checkExistence.length총합이 임계치(예: 100k) 를 초과하면 chunk 단위로 분할하거나, revision 크기별로 별도 큐/노드로 라우팅한다. 임계치와 분할 전략은 forge-agents 팀과 조율 필요.
단기 개선 (1주 이내)#
- executeTimeoutMs 설정: 이번 사건에서 OOM 감지에 ~7분 걸렸고 SIGABRT 로 종료됐지만, 다른 hang 시나리오를 위해
ChildProcessOptions.executeTimeoutMs를 명시적으로 지정한다(예: BIM compare 는 15–20 분 정도). 이미 인프라에 존재하는 SIGKILL 경로(child-process.manager.ts:213-226) 를 활용. - OOM 로그 정리: V8 stderr 다중 라인이 25 개 클러스터로 갈라지지 않도록 stderr 를 line-buffer 하지 말고 stderr 스트림 전체를 한 이벤트로 묶어
logger.error('child stderr', { chunk })로 emit 하는 방식을 검토. 이는 관측성 개선이며 근본 원인 수정은 아님. - structured error 마킹: child stderr 에
FATAL ERROR:또는JavaScript heap out of memory가 포함되면error_code: AGENT_CHILD_OOM같은 태그를 붙여 재발 관측/알림에 사용.
장기 개선 (재발 방지)#
- BIM revision size 기반 pre-flight 검사: 리비전 등록 단계에서 element 총량을 계산해 200k+ 규모는 별도 파이프라인(더 큰 인스턴스 + 스트리밍 비교)으로 라우팅. 대량 revision 이 늘어나는 추세라면 forge-agents 의 스트리밍/청크 API 로의 마이그레이션 검토.
- Memory profiling:
ForgeAgent.extract실행 중 heap snapshot 을 최소 1회 이상 확보해(dev/qa 재현), FixedArray 폭증 지점(로그의Runtime_NewArray,ArrayConstructInitializeElements) 원인이 되는 자료 구조를 규명한다. - Child restart 정책: 한 번 실행 후 child 를 종료하는 fire-and-forget 실행 모델로 전환해 revision 간 heap 잔재 누적 가능성 제거.
Monitoring#
- BIM revision agent 에러 rate (동일 인시던트 재발 조기 탐지):
service:cupixworks-any-bimrevision-agent status:error "Child process stderr"
- OOM 판별용 fatal 시그니처:
service:cupixworks-any-bimrevision-agent "JavaScript heap out of memory"
- SIGABRT/child abort:
service:cupixworks-any-bimrevision-agent "Process killed by signal SIGABRT"
- 대규모 revision 관찰(선제 지표): REVISE-BEGIN 의
cp_elements_count를 대시보드화. 임계 초과 시 사전 경고.
service:cupixworks-any-bimrevision-agent "REVISE-BEGIN"
- 컨테이너 메모리 사용률 (Datadog 메트릭):
avg:container.memory.usage{service:cupixworks-any-bimrevision-agent}
Risk Assessment#
- Risk level: high — 대량 리비전(bim_id 17216 규모) 이 다시 유입되면 동일하게 실패한다. 단일 사용자에서 시작해 clark-vdc 도메인 확산 가능.
- 예상 복잡도: standard —
ChildProcessOptions확장 +BimCompareManager에서 옵션 전달은 pure-code 변경으로 안전. 컨테이너 메모리 상향과 workload 분할은 조율/실측 필요.