ES /docs

BaseService::handlingMessageErrors | Error and message object - {"error":"undefined response","sqsMe

RCA: BaseService::handlingMessageErrors | undefined response (Gemini Files upload terminated)

Overview#

What Happened#

2026-06-25 04:57 KST에 cupixworks-capture-intelligence-agent (us-west-2, walmart 테넌트)이 capture 721058 처리 중 Python summary 서브프로세스가 exit code 1 로 종료되었다. Python compass-capture_summary 모듈이 Gemini Files API 로 비디오를 업로드하던 중 google.genai.errors.ClientError: 400 Bad Request. Upload has already been terminated. 예외가 발생했고, Node 측 BaseService.handlingMessageErrors 가 이 예외를 HTTP 응답으로 인식하지 못해 "undefined response" 문자열로 직렬화한 뒤 SQS 메시지를 삭제했다. 1 건 발생, capture/spacetime summary_stateerror 로 마킹됨.

Quick Facts#

Field Value
exception.class google.genai.errors.ClientError (Python) → Error (Node, message "undefined response")
exception.message 400 Bad Request. {'message': 'Upload has already been terminated.', 'status': 'Bad Request'}
top_frame compass-capture_summary/src/llm_client.py:157 (video_upload_and_wait)
node_relay_frame packages/base/src/base-service.ts:251 (getApiErrorToDeleteMessage)
runtime python3.12, google-genai SDK + Node.js child_process
env production, us-west-2

Affected Teams#

Team / Domain Error Count Impact
walmart (capture intelligence) 1 Capture 721058 (spacetime 1469334) 의 LLM summary 생성 실패. 해당 capture/spacetime summary_stateerror 로 영구 기록됨 (재시도 없음 — 메시지 삭제됨).

Timeline#

  1. 2026-06-25 04:55 KST — capture 721058 SQS 메시지 처리 시작 (BaseService::runByMessage, message id a474af0c-cf41-47c4-be30-f09758c709ac).
  2. 2026-06-25 04:55 KST — Python 서브프로세스 spawn (CaptureIntelligenceProcessManager::runPythonProcess).
  3. 2026-06-25 04:57:03 KST — Gemini Files client.files.upload(file=video_path) 호출이 400 Upload has already been terminated 로 실패. 비디오 임시 파일 정리 (_uploaded_video:267).
  4. 2026-06-25 04:57:04 KST — Python 프로세스 exit code 1 종료 → Node setupProcessHandlers 가 plain Error 로 reject.
  5. 2026-06-25 04:57:04 KSTBaseService.getApiErrorToDeleteMessageerror.responseundefined 인 것을 보고 'undefined response' 반환. SQS 메시지 삭제 + updateErrorState 호출.
  6. 2026-06-25 04:57:04 KSThandlingMessageErrors 가 최종 에러 로그 emit.

Error Log#

Datadog Logs

text
BaseService::handlingMessageErrors | Error and message object - {"error":"undefined response","sqsMessage":{"MessageId":"a474af0c-cf41-47c4-be30-f09758c709ac","Attributes":{"ApproximateReceiveCount":"1"}}}

Impact#

  • Service: cupixworks-capture-intelligence-agent
  • Team: walmart
  • 발생 횟수: 1
  • 최초 발생: 2026-06-25 04:57 KST
  • 최근 발생: 2026-06-25 04:57 KST
  • 영향: capture 721058 와 spacetime 1469334 의 summary 생성이 실패하고 summary_state=error 로 마킹. SQS 메시지가 삭제되어 자동 재시도 없음. 사용자 가시 영향: 해당 capture 의 LLM 요약이 비어 있음.

Root Cause Summary#

Python compass-capture_summary 가 Gemini Files API 로 비디오 업로드를 수행하다 Google 측에서 400 Upload has already been terminated 응답을 받았다 — Gemini Files 의 resumable upload 세션이 서버 측에서 만료/중단된 후에도 클라이언트가 동일 세션으로 PUT 을 진행해 발생하는 일반적 케이스다. Python 코드에 이 에러에 대한 재시도가 없어 즉시 exit 1 로 죽었고, Node 측 CaptureIntelligenceProcessManager.setupProcessHandlersnew Error("Python process failed with exit code 1...") 로 reject 한다. 이 plain Error 객체는 HTTP 응답이 아니므로 BaseService.getApiErrorToDeleteMessage 에서 error.response == undefined 분기를 타고 'undefined response' 문자열을 반환한다. 이 분기는 "API 4xx 처럼 메시지를 삭제할 사유가 있다" 는 의미로 해석되어 SQS 메시지가 즉시 삭제되고 capture/spacetime 가 error 상태로 고착된다. 즉 두 개의 결함이 겹쳤다 — (1) Gemini Files 업로드의 transient 실패에 재시도가 없음, (2) 서브프로세스 실패가 'undefined response' 라는 의미 없는 문자열로 분류되어 Datadog 검색/대응 신호가 지워짐.

Technical Analysis#

Code Path#

  • Entry point: packages/base/src/base-service.ts:108 — SQS 메시지 처리 루프
  • Python 호출: packages/cupix-capture-intelligence-agent/src/manager/capture-intelligence-process.manager.ts:82-94
  • Reject 지점: packages/cupix-capture-intelligence-agent/src/manager/capture-intelligence-process.manager.ts:111-119
  • 에러 분류 지점 (failure point): packages/base/src/base-service.ts:249-252
  • Python 실패 원인: compass-capture_summary/src/llm_client.py:157 (video_upload_and_waitclient.files.upload)
packages/base/src/base-service.ts:107-115typescript
} else {
    this._countWaitedToStopTask = 0;
    try {
        await this.runByMessages();
    } catch (error) {
        await this.handlingMessageErrors(error);
    }
    this.resetMessages();
packages/cupix-capture-intelligence-agent/src/manager/capture-intelligence-process.manager.ts:111-119typescript
childProcess.on('close', (code) => {
    logger.info('CaptureIntelligenceProcess | process exited with code: %d', code);

    if (code === 0) {
        resolve();
    } else {
        reject(new Error(`Python process failed with exit code ${code}${stderr ? `. STDERR: ${stderr}` : ''}`));
    }
});

위 reject 로 만들어진 객체에는 response 필드가 없다. 이후:

packages/base/src/base-service.ts:240-275typescript
private getApiErrorToDeleteMessage = (error: any): any => {
    if (error == undefined) {
        logger.warn('BaseService::getApiErrorToDeleteMessage | undefined error');
        return 'undefined error';
    }
    if (error.errno != undefined && error.code != undefined && error.syscall != undefined) {
        logger.warn('BaseService::getApiErrorToDeleteMessage | nodejs common system error', error);
        return;
    }
    const response = CPUtils.isJsonString(error) ? JSON.parse(error) : error.response;
    if (response == undefined) {
        logger.warn('BaseService::getApiErrorToDeleteMessage | undefined response', error);
        return 'undefined response';
    }
    // ... statusCode 기반 4xx 판정 ...
};

errorError 인스턴스이므로 isJsonString 은 false, error.responseundefined. 따라서 'undefined response' 가 반환된다. 호출 측인 handlingMessageErrors 는 이 truthy 반환값을 보고 SQS 메시지를 삭제한다:

packages/base/src/base-service.ts:290-313typescript
private handlingMessageErrors = async (error: any): Promise<void> => {
    const errorAndMessage = { error: error, sqsMessage: {} };
    if (this.messageInProcess) {
        errorAndMessage.sqsMessage = {
            MessageId: this.messageInProcess.MessageId,
            Attributes: this.messageInProcess.Attributes
        };
        const apiErrorObject = this.getApiErrorToDeleteMessage(error);
        if (apiErrorObject != undefined || this.checkReceiveCountToDeleteMessage()) {
            try {
                errorAndMessage.error = apiErrorObject;
                await this.deleteByMessage(this.messageInProcess);
                if (this._modelInProcess != undefined && this._modelInProcess.id > 0) await this.updateErrorState(this._modelInProcess);
            } catch (error) {
                logger.error('BaseService::handlingMessageErrors | Errors in error handling', error);
            }
        }
    }
    logger.error('BaseService::handlingMessageErrors | Error and message object - %s', JSON.stringify(errorAndMessage));
    resetLogMeta();
};

기대 동작과 실제 동작:

  • 기대: Python 서브프로세스 transient 실패는 SQS 재시도(visibility timeout 만료 또는 redrive)로 회복. 또는 영구 실패라면 stderr 의 실제 예외 클래스/메시지가 Datadog 에 남아 alert/검색이 가능해야 한다.
  • 실제: ApproximateReceiveCount=1 즉 첫 시도에 메시지가 즉시 삭제됨. 에러 메시지는 의미 없는 "undefined response" 문자열로 직렬화되어, Python 측 실제 원인(Gemini upload terminated)이 최종 error 로그에서 사라진다.

Log Evidence#

사용한 Datadog 쿼리:

text
service:cupixworks-capture-intelligence-agent (status:warn OR status:error OR status:info)
시간 범위: 2026-06-24T19:56:30Z ~ 2026-06-24T19:57:10Z

핵심 로그 (시간 역순, 발췌):

json
{
  "timestamp": "2026-06-25 04:57:04",
  "status": "error",
  "message": "BaseService::handlingMessageErrors | Error and message object - {\"error\":\"undefined response\",\"sqsMessage\":{\"MessageId\":\"a474af0c-cf41-47c4-be30-f09758c709ac\",\"Attributes\":{\"ApproximateReceiveCount\":\"1\"}}}"
}
json
{
  "timestamp": "2026-06-25 04:57:04",
  "status": "warn",
  "message": "CaptureIntelligenceService::updateErrorState | captureId: 721058, spacetimeId: 1469334, type: cupixworks"
}
json
{
  "timestamp": "2026-06-25 04:57:04",
  "status": "info",
  "message": "AwsQueueManager::deleteMessage | end - message id: a474af0c-cf41-47c4-be30-f09758c709ac"
}
json
{
  "timestamp": "2026-06-25 04:57:04",
  "status": "info",
  "message": "CaptureIntelligenceProcess | process exited with code: 1"
}
text
CaptureIntelligenceProcess | STDERR: ... google.genai.errors.ClientError: 400 Bad Request.
{'message': 'Upload has already been terminated.', 'status': 'Bad Request'}

Python 스택 (stderr 합본):

text
File "/tmp/lib/compass-capture_summary/src/service.py", line 321, in generate_works_capture_summary_with_video
    with _uploaded_video(capture_id, video_path) as video_file:
File "/tmp/lib/compass-capture_summary/src/service.py", line 254, in _uploaded_video
    video_file = video_upload_and_wait(capture_id, video_path)
File "/tmp/lib/compass-capture_summary/src/llm_client.py", line 157, in video_upload_and_wait
    video_file = client.files.upload(file=video_path)
File "/usr/local/lib/python3.12/dist-packages/google/genai/_api_client.py", line 1824, in _upload_fd
    errors.APIError.raise_for_response(response)
google.genai.errors.ClientError: 400 Bad Request. {'message': 'Upload has already been terminated.', 'status': 'Bad Request'}

재발 패턴 — 같은 에러가 7일 내 한 번 더 발생:

text
service:cupixworks-capture-intelligence-agent "Upload has already been terminated"
시간 범위: now-7d
timestamp (KST) captureId message
2026-06-25 04:57 721058 Upload has already been terminated
2026-06-19 06:48 (다른 capture) Upload has already been terminated

즉 일회성이 아니라 ~6 일 간격 재발. Gemini Files 의 알려진 transient 실패 모드.

Hypotheses Considered#

# Hypothesis Evidence for Evidence against Verdict
H1 Gemini Files API resumable upload 세션이 서버 측에서 종료된 뒤 클라이언트가 같은 세션으로 PUT 시도 → 400 Upload has already been terminated. Python 측 재시도 부재로 즉시 exit 1. Node 측은 plain Error 를 받아 error.response == undefined 분기로 'undefined response' 반환 → 메시지 삭제. stderr 의 google-genai 스택 (_api_client.py:1824, llm_client.py:157); process exited with code: 1; getApiErrorToDeleteMessage 코드 (base-service.ts:249-252); ApproximateReceiveCount=1로 첫 시도에 삭제; 7일 내 동일 에러 재발 (2026-06-19) Confirmed
H2 Cupix Tesla API (capture/spacetime PATCH) 가 4xx 를 반환하여 메시지를 의도적으로 삭제. error 메시지가 'undefined response' 임 — 4xx 분기였다면 {statusCode, requestUriHref, bodyResult, modelId} JSON 이 반환됨. stderr 가 Tesla API 가 아닌 google.genai 호출에서 발생. Rejected
H3 SQS visibility timeout / connectivity 문제로 메시지가 잘못 처리됨. AwsQueueManager::deleteMessage 가 정상 완료, ApproximateReceiveCount=1 (재전송 흔적 없음). SQS 자체는 정상. Rejected
H4 비디오 파일 손상 또는 zero-byte 로 Gemini 가 거부. ffmpeg stderr 에 "not enough frames to estimate rate" 경고 존재 Python 로그상 Cleaned up tempfile: /tmp/capture_721058_52v74qgq.mp4 가 정상적으로 출력 — 파일은 생성됨. ffmpeg 경고는 informational 이며 실패 사유가 아님. Gemini 응답이 "Upload has already been terminated" 로 file content 가 아닌 session 상태 문제임을 명시. Rejected
H5 Walmart 테넌트 / 특정 capture 의 데이터 문제. 다른 capture 에서도 6일 후 동일 에러 재발. tenant-specific 패턴 아님. Rejected
H6 google-genai SDK 자체 버그 — uncertain (해당 SDK 버전을 확인하지 않았음). google-genai 의 resumable upload 가 서버 측 timeout 시 자동 재개를 안 한다는 일반적 동작 SDK 버전을 검증하지 못함 Inconclusive — needs verification

Fix Recommendation#

즉시 조치 (Critical)#

  1. Python video_upload_and_wait 에 retry 추가compass-capture_summary/src/llm_client.py:157 주변에 client.files.upload 호출을 tenacity 또는 동등한 backoff 로 감싸 google.genai.errors.ClientError (특히 status 400 message contains "terminated", 그리고 5xx) 에 대해 2-3 회 재시도. 매 재시도마다 새로운 upload 세션이 생성되어 서버 측 종료 문제는 해소된다. (해당 repo 가 본 분석 환경에 없어 정확한 file:line 은 stderr 에서 확인된 llm_client.py:157. uncertain — needs verification: 현재 코드에 retry 가 전혀 없는지 직접 확인 필요.)

  2. Node 측 에러 분류를 의미 있는 문자열로 교체packages/base/src/base-service.ts:251. 'undefined response' 대신 error.message (또는 stderr 첫 줄) 를 포함하도록 변경하면 Datadog 검색/clustering 이 정확해진다. 별도 fingerprint 가 만들어져 본 cluster 와 같은 의미 없는 그룹화가 사라진다.

단기 개선 (1주 이내)#

  • CaptureIntelligenceProcessManager.setupProcessHandlers 의 reject 객체에 구조화 필드 추가 (exitCode, stderrTail, cause: 'python_subprocess_failure'). handlingMessageErrors 에서 이런 cause 를 별도 분기로 처리해 "API 4xx" 와 "subprocess 실패" 를 구분.
  • 첫 시도(ApproximateReceiveCount==1)에서 subprocess 실패한 경우 즉시 메시지를 삭제하지 말고 SQS 가 visibility timeout 후 재배달하도록 두는 정책 검토. 현재 getApiErrorToDeleteMessage 는 비-API 에러도 즉시 삭제 사유로 분류한다.
  • BaseService.getApiErrorToDeleteMessage'undefined response' / 'undefined error' 매직 문자열을 enum/심볼로 교체하여 호출자가 의도를 명확히 인식하게 변경.

장기 개선 (재발 방지)#

  • Python 서브프로세스 와 Node orchestrator 사이의 에러 컨트랙트 정의 — 예: Python 이 실패 시 output.json{"success": false, "error_class": "...", "error_message": "...", "retryable": bool} 를 항상 기록. Node 는 output.json 을 우선 신뢰하고 retryable 에 따라 메시지 삭제 여부를 결정.
  • Gemini Files API 호출에 대한 별도 dashboard (latency, error rate by error class) — Upload has already been terminated, 429 RESOURCE_EXHAUSTED 등은 Google 측 transient 이므로 별도 SLO/alert 분리.
  • summary_state=error 인 capture 를 주기적으로 재처리하는 reconciliation worker 도입 — 현재는 메시지 삭제 후 영구 error 로 남는다.

Monitoring#

추가할 Datadog 모니터/위젯 (각 쿼리는 release dashboard timeseries 위젯에 그대로 사용 가능):

text
service:cupixworks-capture-intelligence-agent status:error "BaseService::handlingMessageErrors"
text
service:cupixworks-capture-intelligence-agent "undefined response"
text
service:cupixworks-capture-intelligence-agent "Upload has already been terminated"
text
service:cupixworks-capture-intelligence-agent status:warn "Python process failed with exit code"
text
service:cupixworks-capture-intelligence-agent "google.genai.errors"

알림 권장: "undefined response" 가 1 시간에 ≥3 건 이면 Slack 알림. 현재 7일에 2 건이므로 baseline 매우 낮음.

Risk Assessment#

  • Risk level: medium — 단발이지만 재발 패턴이 확인됨 (6 일 간격). 발생할 때마다 capture summary 가 영구 error 로 고착되어 사용자 가시 데이터 결함이 남는다. 즉시 위험은 작지만 외부(Gemini) 의존성의 transient 실패에 시스템이 견고하지 않다는 신호.
  • 예상 복잡도: standard — Python 에 retry 추가는 trivial. Node 측 에러 분류 개선은 base-service 의 동작 변경이라 회귀 테스트 필요 (다른 agent 들이 같은 BaseService 를 공유: forge, potree, thumbnail).