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#
- 2026-07-16 12:24:52 KST — 마지막 job (
capture: 736273, us-west-2)PostprocessorService::getCqaAssignment정보 로그 기록 - 2026-07-16 12:24:58 KST —
BaseService::cleanUpAnythingRelatedModel | path: /tmp/workspace/736273, 이어서 SQS 메시지 삭제 (message id: a049ee6b-...) - 2026-07-16 12:25:14 KST —
SYSTEM - Uncaught Exception: Unknown system error -116: Unknown system error -116, close(첫 발생 = 마지막 발생, in-flight job 없음) - 이후 — 동일 exception 재발 없음 (14 일 기준). 12:28 이후 다른 인스턴스에서 정상 job 처리 재개
Error Log#
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 = -114 와 UV_EREMOTEIO = -121 사이에 -116 자리는 비어 있어, 이 errno 를 만난 순간 libuv 는 "Unknown system error -116" 이라는 fallback 메시지를 만든다. 메시지 끝의 , close 는 syscall 이름 (close(2)) 을 의미한다. 이 agent 는 job 마다 EFS(/efs/{skatKey}/...) 로부터 skat-master 결과를 읽어들이는데(file-system.manager.ts 의 loadSkatResults 등), NFS 프로토콜 기반인 EFS 에서 파일 핸들이 open 되어 있는 동안 원격 inode 가 사라지거나 export 재발행/네트워크 파티션이 발생하면 close() 가 ESTALE 을 반환한다. app.ts:27-31 의 uncaughtException 핸들러가 이 예외를 잡아 로깅한 뒤 process.exit(1) 을 호출하여 프로세스가 종료됐다.
Technical Analysis#
Code Path#
Entry point 및 실패 지점은 top-level 프로세스 훅이다.
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 경로에 붙는 스트림은 첫 번째:
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') 콜백 자체를 사용하고 있다:
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/ReadStream 의 close 는 결과를 destroy 경로에서 emit 하는데, 여기서 발생한 에러는 error 이벤트가 아닌 uncaught 예외로 전파됐다. File/uploadFile 함수는 fileStream 자체의 error 이벤트를 리스닝하지 않아(sendReq.on('error', ...) 만 존재), 스트림 destroy 중 발생한 시스템 에러가 이벤트 루프에 유출된다.
Log Evidence#
Datadog query (재현):
service:cupixworks-capture-postprocessor-agent "Unknown system error -116"
핵심 로그(정렬: 시간 오름차순, 시간은 KST):
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 매핑 확인 (재현 커맨드):
$ 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 발생 빈도:
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 과 정확히 일치. -116 은 UV_EALREADY(-114) 와 UV_EREMOTEIO(-121) 사이 빈 슬롯. errno.h 매핑상 ESTALE=116. 서비스가 /efs/... 경로를 상시 사용 (FileSystemManager::loadSkatResults 로그 다수 확인). TransferManager::File 은 fileStream.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-489의zipDir에서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 용 쿼리:
service:cupixworks-capture-postprocessor-agent "Unknown system error"
service:cupixworks-capture-postprocessor-agent "Uncaught Exception"
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 옵션 조정은 별도 트랙.