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_state 가 error 로 마킹됨.
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_state 가 error 로 영구 기록됨 (재시도 없음 — 메시지 삭제됨). |
Timeline#
- 2026-06-25 04:55 KST — capture
721058SQS 메시지 처리 시작 (BaseService::runByMessage, message ida474af0c-cf41-47c4-be30-f09758c709ac). - 2026-06-25 04:55 KST — Python 서브프로세스 spawn (
CaptureIntelligenceProcessManager::runPythonProcess). - 2026-06-25 04:57:03 KST — Gemini Files
client.files.upload(file=video_path)호출이400 Upload has already been terminated로 실패. 비디오 임시 파일 정리 (_uploaded_video:267). - 2026-06-25 04:57:04 KST — Python 프로세스 exit code 1 종료 → Node
setupProcessHandlers가 plainError로 reject. - 2026-06-25 04:57:04 KST —
BaseService.getApiErrorToDeleteMessage가error.response가undefined인 것을 보고'undefined response'반환. SQS 메시지 삭제 +updateErrorState호출. - 2026-06-25 04:57:04 KST —
handlingMessageErrors가 최종 에러 로그 emit.
Error Log#
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와 spacetime1469334의 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.setupProcessHandlers 는 new 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_wait→client.files.upload)
} else {
this._countWaitedToStopTask = 0;
try {
await this.runByMessages();
} catch (error) {
await this.handlingMessageErrors(error);
}
this.resetMessages();
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 필드가 없다. 이후:
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 판정 ...
};
error 가 Error 인스턴스이므로 isJsonString 은 false, error.response 는 undefined. 따라서 'undefined response' 가 반환된다. 호출 측인 handlingMessageErrors 는 이 truthy 반환값을 보고 SQS 메시지를 삭제한다:
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 쿼리:
service:cupixworks-capture-intelligence-agent (status:warn OR status:error OR status:info)
시간 범위: 2026-06-24T19:56:30Z ~ 2026-06-24T19:57:10Z
핵심 로그 (시간 역순, 발췌):
{
"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\"}}}"
}
{
"timestamp": "2026-06-25 04:57:04",
"status": "warn",
"message": "CaptureIntelligenceService::updateErrorState | captureId: 721058, spacetimeId: 1469334, type: cupixworks"
}
{
"timestamp": "2026-06-25 04:57:04",
"status": "info",
"message": "AwsQueueManager::deleteMessage | end - message id: a474af0c-cf41-47c4-be30-f09758c709ac"
}
{
"timestamp": "2026-06-25 04:57:04",
"status": "info",
"message": "CaptureIntelligenceProcess | process exited with code: 1"
}
CaptureIntelligenceProcess | STDERR: ... google.genai.errors.ClientError: 400 Bad Request.
{'message': 'Upload has already been terminated.', 'status': 'Bad Request'}
Python 스택 (stderr 합본):
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일 내 한 번 더 발생:
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)#
-
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 가 전혀 없는지 직접 확인 필요.) -
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 위젯에 그대로 사용 가능):
service:cupixworks-capture-intelligence-agent status:error "BaseService::handlingMessageErrors"
service:cupixworks-capture-intelligence-agent "undefined response"
service:cupixworks-capture-intelligence-agent "Upload has already been terminated"
service:cupixworks-capture-intelligence-agent status:warn "Python process failed with exit code"
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).