ES /docs

SYSTEM - Uncaught Exception: Unknown system error -116: Unknown system error -116, close

RCA: SYSTEM - Uncaught Exception: Unknown system error -116, close

Overview#

What Happened#

2026-07-16 12:25 KST 에 cupixworks-capture-postprocessor-agent 컨테이너 하나가 libuv 가 매핑하지 못한 시스템 에러 -116 을 close syscall 에서 던지며 uncaughtException 핸들러에 의해 프로세스를 종료했다. Linux 상 raw errno 116 은 ESTALE (Stale file handle) 로, 이 agent 가 매 job 처리 시 마운트하여 사용하는 EFS(/efs/...) 경로의 파일 핸들이 무효화된 상황을 가리킨다. 발생 횟수는 14 일 retention 범위 내 1 회이며, ECS/Nomad 등의 오케스트레이터가 컨테이너를 재기동하여 처리를 이어받았을 가능성이 높다.

Quick Facts#

Field Value
exception.class Error (Node.js system error)
exception.message Unknown system error -116: Unknown system error -116, close
top_frame packages/cupix-capture-postprocessor-agent/src/app.ts:27-31 (uncaughtException handler)
runtime Node.js (libuv) — errno -116 not mapped to UV_E* code
env production, us-west-2

Affected Teams#

Team / Domain Error Count Impact
clark-vdc / capture postprocessor 1 컨테이너 1개 프로세스 종료. 재기동 후 다른 인스턴스가 SQS 큐에서 처리를 재개하므로 사용자 관측 영향은 미미(재시도로 흡수).

Timeline#

  1. 2026-07-16 12:24:52 KST — 마지막 job (capture: 736273, us-west-2) PostprocessorService::getCqaAssignment 정보 로그 기록
  2. 2026-07-16 12:24:58 KSTBaseService::cleanUpAnythingRelatedModel | path: /tmp/workspace/736273, 이어서 SQS 메시지 삭제 (message id: a049ee6b-...)
  3. 2026-07-16 12:25:14 KSTSYSTEM - Uncaught Exception: Unknown system error -116: Unknown system error -116, close (첫 발생 = 마지막 발생, in-flight job 없음)
  4. 이후 — 동일 exception 재발 없음 (14 일 기준). 12:28 이후 다른 인스턴스에서 정상 job 처리 재개

Error Log#

Datadog Logs

text
SYSTEM - Uncaught Exception: Unknown system error -116: Unknown system error -116, close

Impact#

  • Service: cupixworks-capture-postprocessor-agent
  • Team: clark-vdc
  • 발생 횟수: 1
  • 최초 발생: 2026-07-16 12:25 KST
  • 최근 발생: 2026-07-16 12:25 KST

Root Cause Summary#

Node.js libuv 는 Linux errno ESTALE (raw 값 116) 를 UV_E* 코드로 매핑하지 않는다. process.binding('uv') 확인 결과 UV_EALREADY = -114UV_EREMOTEIO = -121 사이에 -116 자리는 비어 있어, 이 errno 를 만난 순간 libuv 는 "Unknown system error -116" 이라는 fallback 메시지를 만든다. 메시지 끝의 , close 는 syscall 이름 (close(2)) 을 의미한다. 이 agent 는 job 마다 EFS(/efs/{skatKey}/...) 로부터 skat-master 결과를 읽어들이는데(file-system.manager.tsloadSkatResults 등), NFS 프로토콜 기반인 EFS 에서 파일 핸들이 open 되어 있는 동안 원격 inode 가 사라지거나 export 재발행/네트워크 파티션이 발생하면 close()ESTALE 을 반환한다. app.ts:27-31uncaughtException 핸들러가 이 예외를 잡아 로깅한 뒤 process.exit(1) 을 호출하여 프로세스가 종료됐다.

Technical Analysis#

Code Path#

Entry point 및 실패 지점은 top-level 프로세스 훅이다.

applications/agents/packages/cupix-capture-postprocessor-agent/src/app.ts:27-31typescript
process.on('uncaughtException', (error: any) => {
    console.error('SYSTEM - Uncaught Exception:', error);
    logger.error('SYSTEM - Uncaught Exception: %s', error instanceof Error ? error.message : JSON.stringify(error));
    exitHandler({ exit: true }, 1);
});

close syscall 을 사용하는 지점은 이 패키지에 두 곳이 존재한다. EFS 경로에 붙는 스트림은 첫 번째:

applications/agents/packages/cupix-capture-postprocessor-agent/src/manager/transfer.manager.ts:44-79typescript
private File = (url: string, path: string, headers: any): Promise<void> => new Promise((resolve, reject) => {
    this.cupixAuth.checkToken()
        .then(() => {
            logger.debug('TransferManager::File | begin path: %s, url: %s', path, url);
            const fileStream = fs.createWriteStream(path);
            const sendReq = request.get(url, { headers: headers });

            fileStream
                .on('finish', async () => {
                    logger.debug('TransferManager::File | complete path: %s, size: %d', path, CPUtils.getFileSize(path));
                    await CPUtils.sleep(100);
                    fileStream.close();
                    resolve();
                });
    // ...

두 번째는 zip 아카이브를 EFS 가 아닌 로컬(/tmp/workspace/...) 로 쓰지만, outputStream.on('close') 콜백 자체를 사용하고 있다:

applications/agents/packages/cupix-capture-postprocessor-agent/src/manager/file-system.manager.ts:473-489typescript
private zipDir = (sourcePath: string, outputPath: string): Promise<number> => {
    const archive = archiver('zip', { zlib: { level: 9 } });
    const outputStream = fs.createWriteStream(outputPath);

    archive.pipe(outputStream);
    archive.directory(sourcePath, 'process_output');
    archive.finalize();

    return new Promise((resolve, reject) => {
        outputStream.on('close', () => {
            const byte = archive.pointer();
            resolve(byte);
        });
        archive.on('error', (err) => reject(err));
    });
};

기대 동작: fileStream.close() 또는 stream teardown 시 close(2) 가 성공하고 스트림의 close 이벤트가 정상 발생한다.

실제 동작: EFS 상의 파일 핸들이 stale 상태로 전이된 뒤 libuv 가 close(fd) 를 호출하며 커널이 ESTALE(116) 을 반환. Node.js 의 fs.WriteStream/ReadStreamclose 는 결과를 destroy 경로에서 emit 하는데, 여기서 발생한 에러는 error 이벤트가 아닌 uncaught 예외로 전파됐다. File/uploadFile 함수는 fileStream 자체의 error 이벤트를 리스닝하지 않아(sendReq.on('error', ...) 만 존재), 스트림 destroy 중 발생한 시스템 에러가 이벤트 루프에 유출된다.

Log Evidence#

Datadog query (재현):

text
service:cupixworks-capture-postprocessor-agent "Unknown system error -116"

핵심 로그(정렬: 시간 오름차순, 시간은 KST):

text
12:24:52  info   PostprocessorService::getCqaAssignment | capture: 736273, levelId: 85571, total floorplans: 3, bim: 2, published non-bim: 1
12:24:58  info   BaseService::cleanUpAnythingRelatedModel | path: /tmp/workspace/736273
12:24:58  info   AwsQueueManager::deleteMessage | begin - queue url: https://sqs.us-west-2.amazonaws.com/002596530511/cupix-capture-postprocessor-agent-production
12:24:58  info   AwsQueueManager::deleteMessage | end - message id: a049ee6b-a76f-44bb-9ad4-1203c360f165
12:25:14  error  SYSTEM - Uncaught Exception: Unknown system error -116: Unknown system error -116, close

Node.js/libuv 매핑 확인 (재현 커맨드):

text
$ node -e "const b=process.binding('uv'); for (const k of Object.keys(b)) if (k.startsWith('UV_E')) console.log(k, b[k]);" | sort -t' ' -k2 -n
UV_EALREADY -114
UV_EREMOTEIO -121

-116 자리는 비어 있음 → "Unknown system error -116" fallback 이 트리거되는 값. Linux errno.h#define ESTALE 116 이며, 이 코드는 통상 NFS/EFS 마운트에서 관측된다.

Historic 발생 빈도:

text
service:cupixworks-capture-postprocessor-agent "Uncaught Exception"   (last 14d) → 1
service:cupixworks-capture-postprocessor-agent "-116"                (last 14d) → 22 (21 건은 pano 파일명의 "(116)" 문자열, 이 exception 과 무관)

Hypotheses Considered#

# Hypothesis Evidence for Evidence against Verdict
H1 libuv 가 매핑하지 않은 Linux errno 116 (ESTALE) 이 close(2) 에서 발생하여 fs stream 의 destroy 경로가 uncaught 예외로 유출됨 메시지 포맷("Unknown system error -N: ..., <syscall>") 이 Node.js libuv fallback 과 정확히 일치. -116UV_EALREADY(-114)UV_EREMOTEIO(-121) 사이 빈 슬롯. errno.h 매핑상 ESTALE=116. 서비스가 /efs/... 경로를 상시 사용 (FileSystemManager::loadSkatResults 로그 다수 확인). TransferManager::FilefileStream.on('error') 를 리스닝하지 않음 (transfer.manager.ts:53-73) Confirmed
H2 특정 job 처리 중 명시적 예외 (network/HTTP/JSON) 가 uncaught 로 전파 12:24:58 SQS deleteMessage 종료 후 16 초 동안 로그 침묵 뒤 예외 발생. 예외 시점에 in-flight job 로그 없음 메시지 포맷이 시스템 에러 fallback 이며 syscall = close. HTTP/JSON 예외라면 syscall 필드가 존재하지 않음 Rejected
H3 외부 의존성(EFS/S3) 광역 장애 bun cli/incident-board.ts for-cluster 결과 active incident 없음. 최근 recent 인시던트 2건 (2026-07-10, 2026-07-13) 은 root_cause_type=unknown 으로 자동 그룹핑된 별개 사건이고, 이 클러스터와 시간이 겹치지 않음 Rejected
H4 반복적 EFS stale handle 로 인한 상시 유출 14 일간 이 서비스에서 "Uncaught Exception" 은 이번 1 건. 지속적 EFS 문제가 아니라 순간적 stale (mount refresh, target 재생성 등) 로 판단 Rejected

Fix Recommendation#

즉시 조치 (Critical)#

  • 파일 스트림에 error 리스너 추가packages/cupix-capture-postprocessor-agent/src/manager/transfer.manager.ts:44-79 (File) 및 transfer.manager.ts:81-107 (uploadFile) 의 fs.createWriteStream / fs.createReadStream 결과에 .on('error', ...) 를 추가하여 스트림 destroy 중 발생하는 시스템 에러를 Promise reject 로 전환. 현재 sendReq 측 에러만 리스닝하고 있어 stream close 단계 에러가 uncaughtException 으로 유출된다.
  • archive 파이프의 outputStream 도 동일 조치file-system.manager.ts:473-489zipDir 에서 outputStream.on('error', reject) 를 추가 (현재는 archive.on('error') 만 있음).

단기 개선 (1주 이내)#

  • uncaughtException 핸들러의 정책 재검토app.ts:27-31 는 어떤 uncaught 라도 process.exit(1) 로 종료한다. 우발적 stale handle 로 컨테이너 전체가 죽는 것을 허용할지, 아니면 특정 시스템 에러(예: syscall=close)는 로그만 남기고 컨테이너를 유지할지 팀 정책 정리. 오케스트레이터가 재기동을 처리한다면 현 정책 유지가 낫지만, 재기동 지연이 SLA 에 영향을 준다면 selective 처리 검토.
  • EFS 접근 wrapper 강화FileSystemManager 의 EFS 읽기/쓰기 유틸에 ESTALE(errno 116) / "Unknown system error -116" 감지 시 짧은 지연 후 재시도(1~2 회) 로직 추가. 커널이 stale 캐시를 재해석하여 두 번째 시도에서는 성공하는 경우가 많다.

장기 개선 (재발 방지)#

  • EFS 마운트/mount option 점검 — Terraform 인프라 (cupix-infrastructure 또는 data-pipeline) 에서 이 agent 의 EFS mount target 설정(actimeo, hard vs soft, retrans) 을 확인. hard,timeo=600,retrans=2 계열이 아니라면 stale handle 노출이 잦아질 수 있음.
  • libuv 매핑 관측 자동화 — 로그에 "Unknown system error -" 패턴이 뜨면 즉시 Sentry/Datadog monitor 로 알람. errno 별로 원인(NFS stale=116, ...) 매핑 문서화하여 재발 시 트리아지 시간 단축.

Monitoring#

Datadog release dashboard timeseries widget 용 쿼리:

text
service:cupixworks-capture-postprocessor-agent "Unknown system error"
text
service:cupixworks-capture-postprocessor-agent "Uncaught Exception"
text
service:cupixworks-capture-postprocessor-agent status:error @environment:production

세 쿼리 모두 log-search 문법이며, count 를 얻고 싶다면 Datadog Analytics 에서 count aggregation 을 붙여 사용한다 (widget 은 pipe/stats 문법 없이도 자동으로 timeseries 그래프를 렌더한다).

Risk Assessment#

  • Risk level: low — 14 일 내 1 회만 관측, EFS stale handle 은 산발적 발생이 정상이며 오케스트레이터 재기동으로 흡수됨.
  • 예상 복잡도: trivial — 스트림 error 리스너 추가만으로 uncaught 유출을 제거할 수 있음. 재시도 로직/mount 옵션 조정은 별도 트랙.