ES /docs

ChildProcessManager::setupEventHandlers | Child process stderr: 15: 0x12c1b3b [/usr/bin/node]

RCA: ChildProcessManager::setupEventHandlers | Child process stderr: 15: 0x12c1b3b [/usr/bin/node]

Overview#

What Happened#

2026-07-21 21:00:39 KST, cupixworks-any-bimrevision-agent 서비스에서 BIM comparison 자식 프로세스가 V8 heap OOM 으로 SIGABRT 종료됐다. 부모 프로세스의 ChildProcessManager 가 자식 stderr 를 라인 단위로 로거에 그대로 전달하는 구조이기 때문에, V8 이 crash 시 출력하는 native stack trace 한 줄 한 줄이 각각 별도의 error 로그로 쪼개져 25개의 유사 클러스터로 fingerprint 되었다. 이 클러스터(eecaced6)는 그 stack trace 중 15번 프레임(0x12c1b3b [/usr/bin/node]) 하나에 해당한다.

Quick Facts#

Field Value
exception.class V8 FatalProcessOutOfMemory (parent forwards child stderr)
exception.message ChildProcessManager::setupEventHandlers | Child process stderr: 15: 0x12c1b3b [/usr/bin/node]
top_frame packages/base/src/manager/child-process.manager.ts:140 (stderr listener)
child_crash_frame v8::internal::ArrayConstructInitializeElementsv8::internal::Runtime_NewArray
runtime Node.js on /usr/bin/node, heap limit --max-old-space-size=8192 (8 GB)
env production, us-west-2, tenant cupix

Affected Teams#

Team / Domain Error Count Impact
clark-vdc (BIM revision agent) 1 for this cluster / 25 clusters in the same crash 단일 BIM revision job (bim_id=17216, src_revision_id=25433, prev_revision_id=21647) 실패, 883421ms(약 14.7분) 처리 후 SIGABRT

Timeline#

  1. 2026-07-21 20:45:56 KSTBimRevisionService::run 시작 (역산: REVISE-END 시각 − elapsed_ms=883421)
  2. 2026-07-21 21:00:39 KST — V8 이 Ineffective mark-compacts near heap limit 로 OOM abort. 자식 프로세스가 stderr 로 <--- Last few GCs ---> … native stack … FATAL ERROR 를 순차 출력
  3. 2026-07-21 21:00:39 KST — 부모 ChildProcessManager 가 각 stderr 라인을 별도 error 로그로 emit (이 클러스터의 라인 포함)
  4. 2026-07-21 21:01:45 KST — Child process close/exit 이벤트 발생, BimRevisionService::run | error: "Process killed by signal SIGABRT (code: null)"
  5. 2026-07-21 21:01:46 KSTBimRevisionService::REVISE-END 로그로 job 실패 확정 (status:error, counts 전부 0)

Error Log#

Datadog Logs

text
ChildProcessManager::setupEventHandlers | Child process stderr: 15: 0x12c1b3b  [/usr/bin/node]

Impact#

  • Service: cupixworks-any-bimrevision-agent
  • Team: clark-vdc
  • 발생 횟수: 1 (단일 crash 이벤트의 stderr 라인 하나. 동일 crash 에 연결된 클러스터 25개 존재)
  • 최초 발생: 2026-07-21 21:00:39 KST
  • 최근 발생: 2026-07-21 21:00:39 KST
  • 사용자 영향: bim_id=17216 의 BIM revision 비교가 실패했고, 파생 산출물(modified/removed/exist 카운트 등) 이 갱신되지 않았다.

Root Cause Summary#

BIM comparison 을 담당하는 fork 된 Node.js 자식 프로세스(bim-compare.process) 가 실행 도중 --max-old-space-size=8192 (8 GB) heap 한계에 도달했고, v8::internal::ArrayConstructInitializeElements 호출 시점의 배열 할당에서 V8 이 FatalProcessOutOfMemory 를 발생시키며 프로세스가 SIGABRT 로 종료됐다. 이 클러스터의 대표 메시지는 crash 자체가 아니라 그 native stack trace 중 한 줄(프레임 15, 0x12c1b3b)에 불과하다. 부모 ChildProcessManagerprocess.stderr.on('data') 핸들러에서 stderr chunk 를 라인 단위로 error 레벨로 로깅하기 때문에 (packages/base/src/manager/child-process.manager.ts:138-141), 하나의 crash 로부터 20여 줄의 stderr 가 각각 fingerprint 되어 별도 클러스터로 잡힌다.

Technical Analysis#

Code Path#

  • Entry point: packages/cupix-tesla-bim-revision-agent/src/bim-revision-service.ts (BimRevisionService::run)
  • Manager: packages/cupix-tesla-bim-revision-agent/src/manager/bim-compare.manager.ts:65BimCompareManager.execute 가 IPC 로 execute 를 자식에게 보냄
  • Child process entrypoint: packages/cupix-tesla-bim-revision-agent/src/process/bim-compare.process.ts:72ForgeAgent.extract(...) 호출 (Autodesk Forge SVF2 model + previous URN 비교)
  • Fork 지점: packages/base/src/manager/child-process.manager.ts:74fork(actualScriptPath, { stdio: ['inherit', 'pipe'|'inherit', 'pipe'|'inherit', 'ipc'] }). execArgv 를 override 하지 않으므로 부모의 --max-old-space-size=8192 를 그대로 상속함
  • Failure surface: packages/base/src/manager/child-process.manager.ts:138-141 — 자식 stderr 를 라인 단위 error 로그로 forwarding

부모 run script (heap 한계 정의 지점):

applications/agents/docker_services/80_run-agent.sh:3bash
node --max-old-space-size=8192 /tmp/agent/dist/app.cjs \
  --env_name $CPX_ENVIRONMENT_NAME \
  ...

Fork 호출:

packages/base/src/manager/child-process.manager.ts:70-84typescript
const stdOut = this.options.stdioMode === 'pipe' ? 'pipe' : 'inherit';
const stdErr = this.options.stdioMode === 'pipe' ? 'pipe' : 'inherit';

try {
    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}`);
} catch (error: any) {
    logger.error(`ChildProcessManager::start | Fork failed: ${error.message}`, {
        actualScriptPath,
        error: error.stack
    });
    throw error;
}

Stderr forwarding (라인 fanout 의 원인):

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/exit 이벤트 (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));
    ...
});

자식 프로세스가 실행하는 실제 작업:

packages/cupix-tesla-bim-revision-agent/src/process/bim-compare.process.ts:67-81typescript
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
            }
        });

기대 동작: ForgeAgent.extract 가 두 revision 을 비교하고 결과를 IPC 로 반환. 실제 동작: 비교 도중 대용량 배열(V8 native stack 의 ArrayConstructInitializeElements)이 heap 한계(8 GB)를 초과하는 순간 V8 이 fatal OOM 을 던지고 프로세스가 SIGABRT 로 죽음. 부모는 자식 stderr 를 라인 단위로 error 로그 emit → 로그 파이프라인이 fingerprint 별 클러스터로 분산.

Log Evidence#

Cluster 파일에서 그대로 복사한 Datadog 쿼리:

text
service:cupixworks-any-bimrevision-agent status:error @environment:production "ChildProcessManager::setupEventHandlers"

추가 확인 쿼리 (native stack 전체 재구성):

text
service:cupixworks-any-bimrevision-agent status:error @environment:production "ChildProcessManager"

같은 이벤트의 stderr 를 시간순으로 재구성한 결과 (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(char const*, v8::OOMDetails const&) [/usr/bin/node]
Child process stderr:  2: 0xee8300 v8::Utils::ReportOOMFailure(v8::internal::Isolate*, char const*, v8::OOMDetails const&) [/usr/bin/node]
Child process stderr:  3: 0xee85e7 v8::internal::V8::FatalProcessOutOfMemory(v8::internal::Isolate*, char const*, v8::OOMDetails const&) [/usr/bin/node]
Child process stderr:  4: 0x10fb205  [/usr/bin/node]
Child process stderr:  5: 0x10fb794 v8::internal::Heap::RecomputeLimits(v8::internal::GarbageCollector) [/usr/bin/node]
Child process stderr:  6: 0x1112684 v8::internal::Heap::PerformGarbageCollection(...) [/usr/bin/node]
Child process stderr:  7: 0x1112e9c v8::internal::Heap::CollectGarbage(...) [/usr/bin/node]
Child process stderr:  8: 0x10e91f1 v8::internal::HeapAllocator::AllocateRawWithLightRetrySlowPath(...) [/usr/bin/node]
Child process stderr:  9: 0x10ea385 v8::internal::HeapAllocator::AllocateRawWithRetryOrFailSlowPath(...) [/usr/bin/node]
Child process stderr: 10: 0x10c69a6 v8::internal::Factory::AllocateRaw(...) [/usr/bin/node]
Child process stderr: 11: 0x10b7e4a v8::internal::FactoryBase<v8::internal::Factory>::AllocateRawArray(...) [/usr/bin/node]
Child process stderr: 12: 0x10b7fb4 v8::internal::FactoryBase<v8::internal::Factory>::NewFixedArrayWithFiller(...) [/usr/bin/node]
Child process stderr: 13: 0x129975f  [/usr/bin/node]
Child process stderr: 14: 0x12a6793  [/usr/bin/node]
Child process stderr: 15: 0x12c1b3b  [/usr/bin/node]   ← 이 클러스터 (eecaced6)
Child process stderr: 16: 0x12cbb55 v8::internal::ArrayConstructInitializeElements(v8::internal::Handle<v8::internal::JSArray>, v8::internal::Arguments<(v8::internal::ArgumentsType)1>*) [/usr/bin/node]
Child process stderr: 17: 0x1512628 v8::internal::Runtime_NewArray(int, unsigned long*, v8::internal::Isolate*) [/usr/bin/node]
Child process stderr: 18: 0x7f9597eda476
Child process closed
Child process exited

Job-level 결과 로그 (BimRevisionService):

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

Status board 결과: svc:cupixworks-any-bimrevision-agent::unknown 스코프에 25개 클러스터가 동일 crash 이벤트로 grouping 되어 있음 (id 2026-07-21-svc-cupixworks-any-bimrevision-agent--unknown-1, 이미 resolved). 이 클러스터도 그 25개 중 하나.

Hypotheses Considered#

# Hypothesis Evidence for Evidence against Verdict
H1 자식 Node.js 프로세스의 V8 heap OOM 이 근본 원인이고, 이 클러스터는 그 crash native stack 의 프레임 15 한 줄이 fingerprint 된 결과이다 같은 timestamp(21:00:39 KST)에 FATAL ERROR: Ineffective mark-compacts near heap limit Allocation failed - JavaScript heap out of memory, Mark-Compact 헤더가 7909.6 → 7909.6 MB / heap limit 8231.3 MB 로 8 GB 한계 근접, 그 직후 native stack 프레임 1 – 18 이 순차 stderr 출력. 부모 run script 에 --max-old-space-size=8192, fork()execArgv override 없음 → 자식도 동일 8 GB 한계. 최종 Process killed by signal SIGABRT (code: null) job error 로그로 확정 Confirmed
H2 Datadog 로그 파이프라인/컬렉터 자체의 버그로 하나의 로그가 여러 클러스터로 쪼개졌다 각 stderr chunk 가 별도 error 로그로 emit 되는 것은 ChildProcessManagerstderr.on('data') 구현(packages/base/src/manager/child-process.manager.ts:138-141) 이 라인 단위로 logger.error 를 호출하기 때문이며, 이는 애플리케이션 레벨 동작이다. status board 도 25개 클러스터를 정상적으로 하나의 서비스 인시던트로 묶었다 Rejected
H3 Autodesk Forge API 의 API_NOT_FOUND 오류가 root cause 21:42 시간대 로그에 ForgeUtilsError: Resource not found / errorCode: 'API_NOT_FOUND' 다수 존재 해당 로그의 timestamp 는 21:42 KST 로 이 클러스터의 21:00:39 KST 와 42분 차이. 21:00:39 시점에는 Forge API 오류 대신 V8 OOM stack 만 출력됨 (`BimRevisionService::run error` 도 signal SIGABRT 로 확정). Forge 404 는 별개 사건
H4 Sidekiq/timeout 등 프로세스 외부 요인이 SIGABRT 를 유발 SIGABRT 는 V8 이 OOMErrorHandler 에서 abort() 호출 시 발생하는 신호이고, stack trace 프레임 1 이 node::OOMErrorHandler 로 시작. 외부 kill 신호 (SIGKILL/SIGTERM) 가 아님. ChildProcessManager::sendMessage 의 timeout 경로는 SIGKILL 을 보내므로(packages/base/src/manager/child-process.manager.ts:221) SIGABRT 와 매치되지 않음 Rejected

Fix Recommendation#

즉시 조치 (Critical)#

  • 클러스터링 fingerprint 개선 (파이프라인 측): 하나의 V8 OOM crash 가 20여 개 클러스터로 쪼개지는 것은 대응·리포팅 노이즈를 크게 키운다. ChildProcessManager stderr chunk 를 로거로 흘릴 때 개별 라인 대신 crash boundary(예: <--- Last few GCs ---> 시작부터 Native stack trace 종료까지)를 하나의 로그 이벤트로 묶어 emit 하도록 검토. 파일: packages/base/src/manager/child-process.manager.ts:138-141. 이 조치는 프런트엔드와 무관하지만 로깅 스키마 변경이므로 downstream(Datadog fingerprint, error-sweeper) 영향 확인 필요 — 별도 트랙으로 진행 권장.
  • 실패한 job 재처리 여부 판단: bim_id=17216, src_revision_id=25433, prev_revision_id=21647 는 실패 후 카운트 0 으로 종료. 재시도가 자동인지, 동일 revision 조합에서 OOM 이 재현되는지(→ 결정적 결함) 확인 필요.

단기 개선 (1주 이내)#

  • 자식 프로세스 heap 한계 명시: fork() 호출 시 execArgv 를 명시적으로 지정해 자식이 어떤 heap 한계로 도는지 코드만으로 파악 가능하게 만들 것. 현재는 shell script(80_run-agent.sh)를 확인해야 한다. 파일: packages/base/src/manager/child-process.manager.ts:74.
  • BIM comparison memory instrumentation: bim-compare.processexecute 진입/종료 및 주기적 지점에서 process.memoryUsage()(heapUsed, external, rss)를 debug 로 남기고, 임계치 초과 시 warn 로그. 어떤 query.entities.length / 두 URN 조합에서 heap 이 급증하는지 재현 조건을 좁힐 수 있음. 파일: packages/cupix-tesla-bim-revision-agent/src/process/bim-compare.process.ts:67-81.
  • OOM 이벤트 명시적 판단: 부모 close 핸들러에서 signal === 'SIGABRT' 이거나 최근 stderr 에 FATAL ERROR: ... heap out of memory 가 있었다면 error 로그 필드에 oom: true 태그를 붙여 fingerprint 개선을 도울 것. 파일: packages/base/src/manager/child-process.manager.ts:107-120, 138-141.

장기 개선 (재발 방지)#

  • BIM 비교 파이프라인의 대용량 데이터 처리 재설계: 8 GB 한계에서 터진다는 것은 model / query.entities 크기가 특정 데이터셋에서 예측 불가능하게 폭증한다는 뜻. ForgeAgent.extract 내부에서 entity/model 을 스트리밍/청크 처리하거나, 사전에 entity 수 상한을 정하고 초과 시 batching 하도록 리팩터.
  • 컨테이너 메모리 vs --max-old-space-size 정합성 재검토: 인스턴스 total memory 대비 heap limit 이 적정한지, 두 프로세스(부모 + 자식) 총합이 컨테이너 한도를 초과하지 않는지 확인. 필요하면 자식만 상향 조정하되 RSS 관측을 붙일 것.
  • Job 실패 시 관찰 가능성 향상: BimRevisionService::REVISE-ENDcrash_kind: v8_oom | timeout | other 같은 구조적 필드 추가로 SRE 대시보드에서 단일 grouping 가능하게.

Monitoring#

REQUIRED SUB-SKILL: 이 쿼리들은 release dashboard timeseries widget 에 그대로 들어간다는 전제로 monitor-only 문법(| stats, count by(...) 등)을 피했다.

  • 부모 프로세스가 감지한 자식 OOM 관련 stderr 폭발 (라인 fanout 을 그대로 세는 프록시):
text
service:cupixworks-any-bimrevision-agent status:error @environment:production "FATAL ERROR: Ineffective mark-compacts near heap limit"
  • BIM revision job 실패 (SIGABRT):
text
service:cupixworks-any-bimrevision-agent status:error @environment:production "Process killed by signal SIGABRT"
  • 자식 프로세스 close/exit 이벤트 (신호 상관없이 종료 자체):
text
service:cupixworks-any-bimrevision-agent status:error @environment:production "ChildProcessManager::setupEventHandlers | Child process closed"
  • Job-level 실패 (elapsed_ms, bim_id 확인용):
text
service:cupixworks-any-bimrevision-agent @environment:production "BimRevisionService::REVISE-END" @status:error

Risk Assessment#

  • Risk level: medium — 단일 job 이 실패했고 시스템 전반은 영향 없음. 다만 25개 클러스터로 fanout 되어 알림 소음을 크게 만들었고, 동일 조건 재현 시 재발 가능성 있음.
  • 예상 복잡도: standard — 즉시 조치(로그 grouping, 재시도 여부 판단)는 trivial ~ standard. 장기 개선(대용량 BIM 비교 스트리밍화)은 별도 프로젝트 규모.