ES /docs

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::runProcess 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#

  1. 2026-07-21 20:47 KSTBimRevisionService::loadAllElements | begin - bimId: 17216, facilityKey: 3fq7tj
  2. 2026-07-21 20:53:08 KSTloadAllElements | end - 211677 cpElements loaded - created: 154331, deleted: 57346
  3. 2026-07-21 20:53:10 KSTBimRevisionService::REVISE-BEGIN | cp_elements_count: 211677
  4. 2026-07-21 20:53:11 KSTBimRevisionService::runBimCompare | query.entities.length: 154331, checkExistence.length: 57346 — child process 로 IPC execute 전달
  5. 2026-07-21 21:00:39 KST — child stderr 출력 시작. <--- Last few GCs --->, Mark-Compact 7916.1 (8233.8) -> 7916.1 (8233.8) MB, FATAL ERROR: Ineffective mark-compacts near heap limitV8::FatalProcessOutOfMemory → native stack trace
  6. 2026-07-21 21:01:45 KSTChild process closed / Child process exited, parent 는 Process killed by signal SIGABRT (code: null) 를 rethrow → BimRevisionService::REVISE-END status=error, elapsed_ms=883421
  7. 2026-07-21 21:17:11 KST — 후속 재시도로 보이는 실행에서 Forge error: INVALID_OTG_ERROR - OTG status is pending 발생 (별도 실패이나 동일 리비전 재시도 흔적)

Error Log#

Datadog Logs

text
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.tsrunBimCompare (log 상 BimRevisionService::runBimCompare | begin at 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:72ForgeAgent.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 설정 없음:

packages/cupix-tesla-bim-revision-agent/src/manager/bim-compare.manager.ts:26-28typescript
constructor() {
	this.childProcessManager = new ChildProcessManager(__dirname, 'process/bim-compare.process');
}

ChildProcessManagerexecArgv 를 지정하지 않고 default fork 를 호출하기 때문에 child Node 프로세스는 parent 의 NODE_OPTIONS 만 상속한다. Datadog 로그가 보여준 상한이 약 8233 MB 였다는 것은 컨테이너에 별도로 --max-old-space-size 가 지정돼 있지 않다는 뜻:

packages/base/src/manager/child-process.manager.ts:73-77typescript
try {
	this.process = fork(actualScriptPath, {
		stdio: ['inherit', stdOut, stdErr, 'ipc'],
	});

Child 의 실제 heavy work 지점:

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
			}
		});

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 개 클러스터로 나뉜 이유:

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

Close handler 에서 SIGABRT 로 반환되는 이유:

packages/base/src/manager/child-process.manager.ts:107-120typescript
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 쿼리 (재현용):

text
service:cupixworks-any-bimrevision-agent "Child process stderr"
text
service:cupixworks-any-bimrevision-agent "BimCompareManager" OR "BimRevisionService" OR "BimCompareProcess"

BIM compare 시작 로그 — child 에 넘어간 workload 규모가 확인된다:

text
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, 발췌):

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

text
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 개 클러스터로 분리된 증거):

text
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.tsfork 호출에 execArgv: ['--max-old-space-size=<NN>'] 를 전달할 수 있도록 ChildProcessOptionsmaxOldSpaceSizeMb 옵션을 추가하고, 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 (동일 인시던트 재발 조기 탐지):
text
service:cupixworks-any-bimrevision-agent status:error "Child process stderr"
  • OOM 판별용 fatal 시그니처:
text
service:cupixworks-any-bimrevision-agent "JavaScript heap out of memory"
  • SIGABRT/child abort:
text
service:cupixworks-any-bimrevision-agent "Process killed by signal SIGABRT"
  • 대규모 revision 관찰(선제 지표): REVISE-BEGIN 의 cp_elements_count 를 대시보드화. 임계 초과 시 사전 경고.
text
service:cupixworks-any-bimrevision-agent "REVISE-BEGIN"
  • 컨테이너 메모리 사용률 (Datadog 메트릭):
text
avg:container.memory.usage{service:cupixworks-any-bimrevision-agent}

Risk Assessment#

  • Risk level: high — 대량 리비전(bim_id 17216 규모) 이 다시 유입되면 동일하게 실패한다. 단일 사용자에서 시작해 clark-vdc 도메인 확산 가능.
  • 예상 복잡도: standardChildProcessOptions 확장 + BimCompareManager 에서 옵션 전달은 pure-code 변경으로 안전. 컨테이너 메모리 상향과 workload 분할은 조율/실측 필요.