ES /docs

FileSystemManager::loadProcessOutput | zip fail

RCA: FileSystemManager::loadProcessOutput | zip fail

Overview#

What Happened#

2026-07-23 19:31 KST, cupixworks-capture-postprocessor-agent 가 capture 737236 (workspace /efs/38b6e1868151d946/) 을 처리하던 중 FileSystemManager::loadProcessOutput 단계에서 archiverskatmaster/results/process_output/ 디렉토리를 zip 으로 묶는 작업이 실패해 zip fail error 로그가 남았다. 실제로는 파이프라인 흐름이 중단되지 않고 후속 단계로 진행됐다 — try/catch 로 감싼 뒤 zip 부재를 정상 흐름으로 처리하는 로직이 존재하기 때문. 14 일 window 안에서 발생 횟수는 2 건으로 매우 낮다.

Quick Facts#

Field Value
exception.class (no exception class — logger.error string only)
exception.message FileSystemManager::loadProcessOutput | zip fail
top_frame packages/cupix-capture-postprocessor-agent/src/manager/file-system.manager.ts:461
deploy archiver ^7.0.1, package cupix-capture-postprocessor-agent 1.11.0
env production, us-west-2

Affected Teams#

Team / Domain Error Count Impact
capture-postprocessor-agent (wmblanchard) 1 (14d: 2) 특정 capture 에서 process_output.zip 이 생성되지 않음. 파이프라인은 계속 진행되지만 해당 capture 의 process_output artifact 는 서버로 업로드되지 않음.

Timeline#

  1. 2026-07-23 19:30:52 KST — 이전 capture(740326) 정리, SQS 메시지 삭제, S3 PUT retry 3/3
  2. 2026-07-23 19:31:03 KSTBaseService::runByMessage id: 1219574, capture key -nD7JPxEH (capture 737236) 처리 시작
  3. 2026-07-23 19:31:06 KSTFileSystemManager::loadProcessOutput | zip fail (error) — 동시에 debug 레벨로 not found file - /efs/38b6e1868151d946/skatmaster/results/process_output.zip 기록
  4. 2026-07-23 19:31:06 KST — 이후 copySourceDir 는 정상 실행 — 파이프라인은 계속 진행

Error Log#

Datadog Logs

text
FileSystemManager::loadProcessOutput | zip fail

Impact#

  • Service: cupixworks-capture-postprocessor-agent
  • Team: wmblanchard
  • 발생 횟수: 1 (14 일 window 에서 2 건)
  • 최초 발생: 2026-07-23 19:31 KST
  • 최근 발생: 2026-07-23 19:31 KST

기능적 영향은 국소적이다. loadProcessOutputtry/catch 로 감싸져 있고, catch 이후 getFileSize(...) === -1 분기가 zip 부재를 정상 흐름으로 처리하기 때문에 상위 파이프라인은 abort 되지 않는다. 다만 해당 capture 는 process_output.zip 이 서버에 업로드되지 않아, 나중에 process output artifact 를 참조하는 후속 단계에서 데이터 부재가 나타날 수 있다.

Root Cause Summary#

Postprocessor 가 skatmaster 단계의 산출물인 /efs/38b6e1868151d946/skatmaster/results/process_output/ 디렉토리를 zip 으로 아카이빙하려 했으나, 해당 소스 디렉토리가 존재하지 않아 archiver 가 error 이벤트를 emit → zipDir promise 가 reject 됐다. loadProcessOutput 는 catch 블록에서 logger.error('... zip fail') 만 남기고 실제 error 객체 (파일 경로, ENOENT 등) 는 로그에 포함하지 않는다. 결과적으로 "무엇을 zip 하려 했는지", "왜 실패했는지" 가 error 로그만으로는 판단 불가하고, debug 레벨 후속 로그 (not found file - .../process_output.zip) 로만 원인이 유추된다. 근본 원인은 (1) upstream skatmaster 파이프라인이 process_output/ 디렉토리를 생성하지 않은 케이스에 대한 방어가 부족하고, (2) 실패 로그에 컨텍스트 (source path, error message) 가 누락된 두 가지 layered defect 이다.

Technical Analysis#

Code Path#

  • Entry point: packages/cupix-capture-postprocessor-agent/src/postprocessor-service.ts:102
  • FileSystemManager.loadProcessOutput 진입: packages/cupix-capture-postprocessor-agent/src/manager/file-system.manager.ts:447
  • Failure point: packages/cupix-capture-postprocessor-agent/src/manager/file-system.manager.ts:461
  • Zip 작업: packages/cupix-capture-postprocessor-agent/src/manager/file-system.manager.ts:473-489
packages/cupix-capture-postprocessor-agent/src/manager/file-system.manager.ts:447-471typescript
loadProcessOutput = async (cpCapture: CPCapture): Promise<void> => {
    if (!cpCapture.workspaceDirPath || !this.skatMasterDirPath) {
        logger.error('FileSystemManager::loadProcessOutput | undefined directory path');
        return;
    }

    const processOutputZipFileName = `${Constants.DefaultProcessOutputDirName}.zip`;
    const processOutputDirPath = path.join(this.skatMasterDirPath, Constants.DefaultResultsDirName, Constants.DefaultProcessOutputDirName);
    const processOutputZipFilePath = path.join(this.skatMasterDirPath, Constants.DefaultResultsDirName, processOutputZipFileName);

    try {
        await this.zipDir(processOutputDirPath, processOutputZipFilePath);
        logger.debug('FileSystemManager::loadProcessOutput | zip success');
    } catch (err) {
        logger.error('FileSystemManager::loadProcessOutput | zip fail');
    }

    if (CPUtils.getFileSize(processOutputZipFilePath) === -1) {
        logger.debug('FileSystemManager::loadProcessOutput | not found file - %s', processOutputZipFilePath);
    } else {
        const localProcessOutputZipFilePath = path.join(cpCapture.workspaceDirPath, processOutputZipFileName);
        logger.debug('FileSystemManager::loadProcessOutput | copy - source: %s, target: %s', processOutputZipFilePath, localProcessOutputZipFilePath);
        fs.copyFileSync(processOutputZipFilePath, localProcessOutputZipFilePath);
    }
};
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));
    });
};

기대 동작: skatmaster 결과 디렉토리 <skatMasterDirPath>/results/process_output/ 가 존재하고 그 내용을 zip 으로 만들어 같은 위치에 process_output.zip 을 생성.

실제 동작: 해당 시점에 소스 디렉토리 (/efs/38b6e1868151d946/skatmaster/results/process_output/) 가 존재하지 않았거나 접근할 수 없어 archiver v7 이 error 이벤트를 emit, zipDir promise 가 reject, catch 블록에서 컨텍스트 없이 zip fail 만 로깅. 이후 getFileSize(...) === -1 분기가 zip 미생성을 정상 흐름으로 흡수 (not found file debug 로그 기록) 하고 파이프라인 계속 진행.

catch 블록에서 원본 err 를 로그에 담지 않아, error 원인 (ENOENT / EACCES / disk full / archiver 내부 상태 등) 을 Datadog 만으로는 특정할 수 없다는 것이 이 클러스터의 핵심 관찰점.

Log Evidence#

Datadog 쿼리 (에러 시점 ±1 분):

text
service:cupixworks-capture-postprocessor-agent

(time range: 2026-07-23T10:30:30Z2026-07-23T10:31:10Z)

해당 capture 처리 시퀀스 (Datadog, info/warn/error 만):

text
19:31:03 info   BaseService::runByMessage | id: 1219574
19:31:03 info   CupixAuth::setSession | session_id: 3e4d7e645436b7c60a1384c129b6b086dd5bc3e6
19:31:03 info   JobManager::loadJob | begin - job id: 1219574
19:31:03 info   JobManager::loadJob | end - job id: 1219574
19:31:03 info   PostprocessorService::run | skip update running action job - waiting_actions: [], running_actions: [ 'postprocessor' ]
19:31:03 info   CPCapture::fromMeta | key: -nD7JPxEH, version: 1
19:31:03 info   CPCapture::updateVideosfromMeta | begin
19:31:03 info   CPCapture::updateVideosfromMeta | end
19:31:03 info   CPCapture::fromSkatMasterResults | {"error_code":"","skatsdk_version":{"ver":"3.30.26\t2c6644a7e2192c73c3ce86276ac08e29b3df4123\t20260708_060625\tarm64\tUbuntu 22.04.5 LTS"},"task_name":"..."}
19:31:06 error  FileSystemManager::loadProcessOutput | zip fail
19:31:10 warn   FinalizationService::syncFromServer | begin - retry detected (receiveCount: 22), capture id: 737236

fromSkatMasterResultserror_code 는 빈 문자열이라 skatmaster 자체는 error 로 마킹되지 않았지만, process_output/ 디렉토리 산출 여부는 skat 결과 코드와 별개다.

Watch (Kibana) debug 로그, 같은 순간 (동일 capture workspace /efs/38b6e1868151d946/):

text
19:31:06 debug  FileSystemManager::loadProcessOutput | not found file - /efs/38b6e1868151d946/skatmaster/results/process_output.zip
19:31:06 error  FileSystemManager::loadProcessOutput | zip fail
19:31:06 debug  FileSystemManager::copySourceDir | copy - source: /efs/38b6e1868151d946/preprocessor/results, target: /tmp/workspace/737236

debug 로그가 confirmed 하는 사실: (1) zipDir 실패 직후 zip 파일은 생성되지 않음 (getFileSize === -1), (2) 파이프라인은 abort 되지 않고 다음 단계 (copySourceDir) 로 진행. 따라서 이 error 는 실제로 downstream 작업을 멈추지 않는 "loud warning" 성격이다.

14 일 window 발생 빈도:

text
service:cupixworks-capture-postprocessor-agent "zip fail"

결과: 2 건 (2026-07-20 17:17 KST, 2026-07-23 19:31 KST) — 매우 낮은 빈도.

Hypotheses Considered#

# Hypothesis Evidence for Evidence against Verdict
H1 archiver 가 존재하지 않는 소스 디렉토리 (skatmaster/results/process_output/) 에 대해 error 이벤트 emit 하여 zipDir promise reject debug 로그 not found file - .../process_output.zip 가 catch 직후 emit; skatmaster 파이프라인이 process_output/ 을 생성하지 않은 케이스로 설명 가능; 파이프라인이 abort 되지 않고 getFileSize === -1 분기로 흡수되는 code path 일치 catch 블록이 원본 err 를 로그에 담지 않아 정확한 ENOENT 여부는 로그로 재확인 불가 — 다만 debug 로그의 "not found file" 이 강력한 정황 증거 Confirmed (정황 근거)
H2 EFS 마운트 이슈 / permission 오류로 zip 쓰기 실패 write 대상은 같은 EFS (/efs/38b6e1868151d946/skatmaster/results/) 인데 앞뒤 로그에서 copySourceDir 는 정상 수행됨 → mount/permission 은 정상 copySourceDir/efs/38b6e1868151d946/preprocessor/results 를 읽고 /tmp/workspace/737236 에 씀 — postprocessor 가 skatmaster EFS 서브트리에 쓰기 권한이 있는지는 별개; 그러나 write 실패시 getFileSize 도 특정 값을 반환했을 텐데 -1 (fs.statSync ENOENT 로 추정) 이므로 write 문제라기보다 source read 문제에 가까움 Rejected
H3 archiver v7 라이브러리 자체의 regression / race package 는 archiver: ^7.0.1 로 최근 major, 이전에도 유사 로그 1 건 (7/20) 존재 14d window 2 건은 라이브러리 결함이라기엔 너무 낮은 빈도; 다른 capture 는 정상 처리됨 Rejected
H4 상위 파이프라인이 이미 abort 된 상태에서의 부수적 로그 postprocessor run 은 정상 진행되어 copySourceDir 실행됨; upload/finalization 로그가 이후에 나타남 실제로 파이프라인이 abort 되지 않았음 (Log Evidence 참조) → 이 가설은 사실이 아님 Rejected

Fix Recommendation#

즉시 조치 (Critical)#

  • 대상: packages/cupix-capture-postprocessor-agent/src/manager/file-system.manager.ts:460-462
  • 접근: catch 블록에서 원본 error 를 로그에 포함해 후속 트리아지 가능하도록 개선. 소스 디렉토리 부재는 skatmaster 산출물이 없는 정상 케이스일 수 있으므로 severity 를 error 에서 warn 으로 다운그레이드하는 것을 검토. 원본 error 를 로그 argument 로 전달해 파일 경로, ENOENT 여부, archiver 내부 오류를 후속 트리아지에서 확인할 수 있게 한다.
  • 근거: 현재는 원본 err 를 완전히 삭제하여 root cause 를 debug 로그에 의존해서만 유추 가능. 실제 파이프라인 abort 는 없으므로 warn 이 semantic 에 더 맞고, cupixworks agents 코드베이스의 cplogger 컨벤션 (Error 객체를 직접 넘김) 을 따르는 것이 team convention 과 일치.

단기 개선 (1주 이내)#

  • 대상: packages/cupix-capture-postprocessor-agent/src/manager/file-system.manager.ts:447-471
  • 접근: zipDir 호출 전에 fs.existsSync(processOutputDirPath) guard 를 추가해 소스 디렉토리가 없을 때는 zip 을 시도하지 않고 debug 로그만 남기고 조기 return. 이렇게 하면 "정상 케이스 (skatmaster 가 process_output 을 생성하지 않은 flow)" 와 "실제 zip 실패 (권한/디스크/archiver 내부 오류)" 를 로그 상에서 분리할 수 있다.
  • 근거: 현재는 두 케이스가 모두 zip fail error 하나로 뭉쳐 있어 alert 노이즈와 실제 장애의 구분이 어렵다.

장기 개선 (재발 방지)#

  • skatmaster 파이프라인 계약을 명시화: results/process_output/ 이 언제 생성되고 언제 생략되는지 CPCapture / skatmaster 결과 스펙에 문서화. 필요하다면 fromSkatMasterResults 단계에서 has_process_output 플래그를 노출해 postprocessor 가 명시적으로 skip 결정하도록 한다.
  • agents 공통 error logging convention 강제: catch 블록에서 원본 err 를 절대 삭제하지 않도록 lint rule 또는 shared logger helper 도입 (예: logCaughtError(logger, 'context', err)).

Monitoring#

  • Datadog 쿼리 예시 (release dashboard timeseries widget 용):
text
service:cupixworks-capture-postprocessor-agent status:error "loadProcessOutput"
text
service:cupixworks-capture-postprocessor-agent "zip fail"
  • 알람 임계: 현재 14 일 window 2 건 baseline. 시간당 5 건 초과 or 하루 20 건 초과 시 alert — 그 이하는 skatmaster 정상 skip 케이스일 가능성이 높으므로 noise.
  • 함께 볼 지표: service:cupixworks-capture-postprocessor-agent "FileSystemManager" info/warn 로그 볼륨 대비 error 비율 — 비율 급증 시 EFS 또는 archiver 라이브러리 이슈 의심.

Risk Assessment#

  • Risk level: low
  • 예상 복잡도: trivial
  • 근거: (1) 발생 빈도가 14 일에 2 건으로 매우 낮고, (2) 상위 파이프라인은 abort 되지 않으며, (3) 수정 범위가 단일 파일의 catch 블록과 pre-check guard 로 좁게 한정. 다만 skatmaster 산출물 계약을 실제로 확인하지 않고 코드만 수정하면 "정상 skip 인 줄 알았는데 실은 skat 결함" 을 놓칠 수 있으므로, warn 다운그레이드 전에 최근 발생 capture (예: capture 737236) 의 skatmaster 로그를 함께 확인하는 것을 권장.