SiliteService::updateSiLiteState | failed - captureId: 736992, state: processing, error: {"stack":"H
RCA: SiliteService::updateSiLiteState — 403 ENT4000 (state=processing) on trashed capture 736992
Overview#
What Happened#
2026-07-24 16:34 KST, cupixworks-si-lite-agent (Sprouts tenant, us-west-2) 가 SQS 메시지로 capture 736992 의 SI-Lite 분석을 시작하려고 updateSiLiteState(Processing) 을 호출했으나 Tesla API PATCH /api/v1/captures/736992 가 HTTP 403 + ENT4000 / Cupix::Errors::NotFound 로 응답했다. 해당 캡처는 2026-07-24 01:53 KST 에 cycle_state=trashed 상태로 이동된 뒤였고, Tesla BaseRepository#find_by 의 in_trash 분기가 이를 걸러내면서 NotFound 를 발생시켰다. 이 클러스터는 sibling 클러스터 23a74e95 (handlingMessageErrors) 및 ab8a602b (updateSiLiteState state:error) 와 동일 SQS 메시지에서 파생된 3개 로그 중 첫 번째 error 라인이다. 사용자 노출 영향 없음.
Quick Facts#
| Field | Value |
|---|---|
| exception.class | HttpError (@tesla/typescript-node-sdk) |
| exception.message | HTTP request failed (statusCode 403, body ENT4000) |
| top_frame | captureApi.js:4350 (SDK) → silite-service.ts:88 (updateSiLiteState(Processing)) → silite-service.ts:350 (this.cupixApi.capture.update) |
| upstream_source | Tesla API app/repositories/base_repository.rb:352-353 (ENT4000 on in_trash) |
| runtime | Node.js agent (/tmp/agent/dist), @tesla/typescript-node-sdk@1.13.3-SNAPSHOT.202605301249 |
| env | production, us-west-2, tenant cupix, team sprouts |
| capture_state | DB: state=done, cycle_state=trashed, cycle_state_updated_at=2026-07-23T16:53:36Z |
Affected Teams#
| Team / Domain | Error Count | Impact |
|---|---|---|
| sprouts (team_id 1185) | 1 | 단일 capture (736992) 의 SI-Lite 재분석 첫 상태 전환 실패. 사용자 영향 없음 — 이미 사용자가 trash 로 이동한 캡처 |
Timeline#
- 2026-07-17 17:14 KST — SI-Lite 첫 실행 성공 (
SiliteProcessManager::execute | captureId: 736992, workareas: 1, trades: 9). - 2026-07-24 01:53 KST — Capture 736992 가 trash 로 이동됨 (
cycle_state=trashed,cycle_state_updated_at=2026-07-23T16:53:36Z). - 2026-07-24 16:34:56.340 KST — SQS 메시지
53531766-cc74-4823-8c03-26026d6962c5수신 →SiliteService::run | begin→updateSiLiteState(Processing)호출 → 본 클러스터 로그 발생 (state:processing, 403 ENT4000). - 2026-07-24 16:34:56.373 KST —
run의catch절이updateSiLiteState(Error)재호출 → 동일 403 (siblingab8a602b). - 2026-07-24 16:34:56.382 KST —
BaseService::handlingMessageErrors최종 에러 로깅 (sibling23a74e95) →getApiErrorToDeleteMessage가 4xx 이므로 SQS 메시지 삭제 후 종료.
Error Log#
SiliteService::updateSiLiteState | failed - captureId: 736992, state: processing, error: {"stack":"HttpError: HTTP request failed
at Request._callback (/tmp/agent/dist/node_modules/.pnpm/@tesla+typescript-node-sdk@1.13.3-SNAPSHOT.202605301249_1e9855b03300e7077469e8a23e5d7e36/node_modules/@tesla/typescript-node-sdk/api/captureApi.js:4350:40)
at self.callback (/tmp/agent/dist/node_modules/.pnpm/request@2.88.2/node_modules/request/request.js:185:22)
at Request.emit (node:events:524:28)
at Request.<anonymous> (/tmp/agent/dist/node_modules/.pnpm/request@2.88.2/node_modules/request/request.js:1154:10)
at Request.emit (node:events:524:28)
at IncomingMessage.<anonymous> (/tmp/agent/dist/node_modules/.pnpm/request@2.88.2/node_modules/request/request.js:1076:12)
at Object.onceWrapper (node:events:638:28)
at IncomingMessage.emit (node:events:536:35)
at endReadableNT (node:internal/streams/readable:1698:12)
at process.processTicksAndRejections (node:internal/process/task_queues:82:21)","message":"HTTP request failed","response":{"body":{},"statusCode":403},"body":{},"statusCode":403,"name":"HttpError"}
Impact#
- Service:
cupixworks-si-lite-agent - Team: sprouts
- 발생 횟수: 1
- 최초 발생: 2026-07-24 16:34 KST
- 최근 발생: 2026-07-24 16:34 KST
이 클러스터는 sibling ab8a602b (state=error) 및 23a74e95 (handlingMessageErrors) 와 함께 동일 SQS 메시지에서 43ms 이내 연속으로 발생. 세 클러스터의 근본 원인은 동일 (트래시된 캡처에 대한 stale SQS 메시지). 데이터 손상, 사용자 노출, 재시도 폭주 없음.
Root Cause Summary#
SI-Lite agent 의 run 흐름은 캡처 검증(fetchAndValidateCapture) 이전에 먼저 updateSiLiteState(Processing) 을 호출해 상태를 갱신한다 (silite-service.ts:87-88). 이 PATCH 요청도 Tesla BaseRepository#find_by 를 거치므로, 대상 캡처의 cycle_state ∈ {trashing, trashed} 인 경우 Trashable::Capture.in_trash scope 에 걸려 Cupix::Errors::NotFound.new(code: 'ENT4000') 가 raise 되고 client_error_controller.rb 의 rescue_from Cupix::Errors::NotFound, with: :not_found_403_error 에 의해 HTTP 403 으로 응답된다. Capture 736992 는 SQS 메시지가 소비되기 약 15시간 전에 사용자가 trash 로 이동했고, agent 에는 소비 전에 trash 여부를 확인하는 로직이 없어 첫 상태 전환 시도에서 즉시 이 error 로그가 남았다. 이후 run 의 catch 절이 updateSiLiteState(Error) 를 다시 호출하면서 sibling ab8a602b 로그가 발생하고, 최종적으로 BaseService::handlingMessageErrors 가 sibling 23a74e95 를 남긴 뒤 SQS 메시지는 4xx 분류로 삭제된다.
Technical Analysis#
Code Path#
Entry point: SI-Lite agent 가 SQS 메시지 { id: 736992 } 를 수신 → BaseService::runByMessage → SiliteService.run(736992).
protected override run = async (targetId: number, _msgObject: SqsMessageBody): Promise<void> => {
logger.info('SiliteService::run | begin - captureId: %d', targetId);
try {
await this.updateSiLiteState(targetId, TESLA.SiLiteState.Processing); // ← 본 클러스터의 실패 지점
const cpCapture = await this.fetchAndValidateCapture(targetId);
// ...
await this.updateSiLiteState(targetId, TESLA.SiLiteState.Done);
} catch (error) {
await this.updateSiLiteState(targetId, TESLA.SiLiteState.Error); // ← sibling ab8a602b 의 실패 지점
throw error;
}
};
Failure point: updateSiLiteState 가 this.cupixApi.capture.update 를 호출하다 SDK 가 HttpError 403 을 throw.
private async updateSiLiteState(captureId: number, state: TESLA.SiLiteState): Promise<void> {
if (DEBUG_MODE) {
logger.debug('SiliteService::updateSiLiteState | DEBUG_MODE - skipping API call - captureId: %d, state: %s', captureId, state);
return;
}
try {
await this.cupixApi.capture.update(captureId, { si_lite_state: state });
logger.debug('SiliteService::updateSiLiteState | updated - captureId: %d, state: %s', captureId, state);
} catch (error) {
logger.error('SiliteService::updateSiLiteState | failed - captureId: %d, state: %s, error: %s',
captureId, state, stringifyError(error));
throw error;
}
}
Tesla 측 실패 지점 — 트래시된 캡처를 find_by 에서 발견하고 ENT4000 을 raise:
scope = current_class.visibility_scope(visibility)
model = query.merge(scope).first
if model.nil? && current_user.present? && current_user.team.domain == 'admin'
unless (::UserRepository.new(model: current_user).group_codes & %w[administrator senior_editing_engineers]).empty?
model = self.where(attrs).merge(scope).first
end
end
if model.nil?
if self.where(attrs).in_trash.present?
raise Cupix::Errors::NotFound.new(code: 'ENT4000', reason: "#{current_class.name} not found")
else
raise Cupix::Errors::NotFound.new(code: 'ARG10002', reason: "#{current_class.name} not found")
end
end
Trashable::Capture.in_trash:
def self.in_trash
trashed.or(where(record: ::Record.in_trash)).or(where(level: ::Level.in_trash))
end
trashed scope 는 cycle_state ∈ {trashing, trashed} (app/models/concerns/cyclable/base_scope.rb). Kibana 로 확인한 736992 의 cycle_state=trashed 이므로 매칭.
ENT4000 → HTTP 403 변환:
rescue_from Cupix::Errors::NotFound, with: :not_found_403_error
# ...
def not_found_403_error(exception)
raise_error(403, exception)
end
기대 동작: 트래시된 캡처는 SI-Lite 분석 대상이 아니므로 agent 가 상태 전환을 시도하기 전에 trash 여부를 감지해 warn 레벨로 skip.
실제 동작: run 이 검증 이전 시점에 무조건 updateSiLiteState(Processing) 을 먼저 호출하고, 403 을 다른 authorization 실패와 구분하지 않아 error 로그가 남는다.
Log Evidence#
Datadog query (재현용):
service:cupixworks-si-lite-agent status:error @environment:production "SiliteService::updateSiLiteState"
같은 SQS 메시지에서 파생된 두 개의 updateSiLiteState 실패 로그 (Datadog 검색 결과):
2026-07-24 16:34:56.340 [error] SiliteService::updateSiLiteState | failed - captureId: 736992, state: processing, error: {"stack":"HttpError: HTTP request failed ... statusCode":403,"body":{}, "name":"HttpError"}
2026-07-24 16:34:56.373 [error] SiliteService::updateSiLiteState | failed - captureId: 736992, state: error, error: {"stack":"HttpError: HTTP request failed ... statusCode":403,"body":{}, "name":"HttpError"}
Sibling 클러스터 23a74e95 의 handlingMessageErrors 로그에서 확인된 bodyResult:
{
"error": {
"statusCode": 403,
"requestUriHref": "http://api-tesla.cupix.internal/api/v1/captures/736992?fields%5B0%5D=id&...",
"bodyResult": {
"code": "ENT4000",
"type": "Cupix::Errors::NotFound",
"reason": "Capture not found",
"message": "Capture not found"
},
"modelId": 736992
},
"sqsMessage": {
"MessageId": "53531766-cc74-4823-8c03-26026d6962c5",
"Attributes": { "ApproximateReceiveCount": "1" }
}
}
Kibana captures index (env=production, id=736992) 발췌 — sibling RCA 에서 확인된 데이터:
{
"id": 736992,
"state": "done",
"trashed_at": null,
"cycle_state": "trashed",
"cycle_state_updated_at": "2026-07-23T16:53:36.000Z",
"team": { "id": 1185, "domain": "sprouts" },
"facility": { "id": 21347, "cycle_state": "created" },
"record": { "id": 138303 },
"level": { "id": 86715 }
}
Hypotheses Considered#
| # | Hypothesis | Evidence for | Evidence against | Verdict |
|---|---|---|---|---|
| H1 | Capture 736992 가 trash 로 이동되어 Tesla API PATCH /captures/:id 가 ENT4000 → HTTP 403 을 반환. run 의 첫 updateSiLiteState(Processing) 호출이 이 응답에 부딪혀 본 클러스터 로그 생성 |
Kibana: cycle_state=trashed, cycle_state_updated_at=2026-07-23T16:53:36Z. Code: base_repository.rb:352-353 in_trash 분기 → client_error_controller.rb:57-59 403 변환. Sibling 23a74e95 로그의 bodyResult.code=ENT4000 이 동일 SQS 메시지에서 43ms 뒤 기록됨. Datadog 재현 쿼리로 state:processing / state:error 두 로그가 43ms 간격으로 확인됨 |
— | Confirmed |
| H2 | 캡처 736992 가 실제로 DB 에서 삭제되어 발생 | 로그의 message=Capture not found |
Kibana 조회 결과 캡처 레코드는 존재 (state=done). 실제 삭제라면 ARG10002 코드가 반환되어야 함 (base_repository.rb:355). 실제 응답은 ENT4000 |
Rejected |
| H3 | Sprouts 팀 API 권한 회수 / 만료된 서비스 토큰 | statusCode 403 | 권한 실패는 PERM10000 코드 사용. body 는 ENT4000 로 코드가 명확하며 sibling 로그 (7-17 성공) 시점과 동일 자격증명 사용. 동일 시점 다른 캡처에는 성공 요청이 관찰되지 않았으나, sibling 클러스터의 getApiErrorToDeleteMessage 가 4xx 를 삭제로 분류하는 로직이 정상 동작한 것으로 보아 인증 자체는 정상 |
Rejected |
| H4 | @tesla/typescript-node-sdk@1.13.3-SNAPSHOT 의 PATCH 직렬화 버그 |
SDK 버전이 SNAPSHOT (captureApi.js:4350) |
서버 응답 body 에 명확히 ENT4000 이 포함되어 있어 서버가 결정한 결과. SDK 문제라면 클라이언트 사이드 에러 (예: TypeError) 로 나타나야 함 |
Rejected |
| H5 | run 실패 시 catch 절의 updateSiLiteState(Error) 재시도가 무한 loop 를 유발 |
catch 절이 재차 API 호출 (silite-service.ts:108-110) |
updateSiLiteState(Error) 도 즉시 403 이지만, 이후 throw error 로 상위 handler 로 전달되어 SQS 메시지 삭제. ApproximateReceiveCount=1, cluster occurrence_count=1 로 재시도 폭주는 없음 |
Rejected |
Fix Recommendation#
즉시 조치 (Critical)#
- 없음. 사용자 영향/데이터 손상 없고 SQS 메시지가 자동 삭제되어 self-heal. 코드 변경 불필요.
단기 개선 (1주 이내)#
run초기에 trashed capture 를 정상 skip:applications/agents/packages/cupix-si-lite-agent/src/silite-service.ts:84-112의 흐름을 재정렬해updateSiLiteState(Processing)이전에 캡처를 먼저 fetch 하도록 변경.capture.get이 403 +body.code === 'ENT4000'로 실패하면logger.warn('SiliteService::run | capture in trash, skipping - captureId: %d', targetId)로 warn 레벨 로그만 남기고 정상 종료 (SQS 메시지 삭제). 이렇게 하면 본 클러스터 (updateSiLiteState state:processing) 와 siblingab8a602b(state:error),23a74e95(handlingMessageErrors) 세 개가 모두 warn 한 줄로 정리된다.- ENT4000 전용 로그 레벨 다운그레이드:
applications/agents/packages/cupix-si-lite-agent/src/silite-service.ts:353-354의updateSiLiteStatecatch 블록에서 error 응답의bodyResult.code === 'ENT4000'인 경우logger.warn으로 다운그레이드. 단, 코드 특정 (ENT4000) 로 좁힐 것 — 모든 4xx 를 warn 으로 낮추면 실제 authorization 실패 (PERM10000) 를 놓칠 수 있음.
장기 개선 (재발 방지)#
- Tesla API 응답 semantics 정리:
ENT4000의entity.yml정의는"Record has trashed"이지만reason필드는"#{current_class.name} not found"로 다르게 표기되어 클라이언트 디버깅이 어렵다.reason을"is trashed"로 수정하거나 HTTP 410 Gone 응답으로 분리하는 방안을 검토. 이 변경은 다수 클라이언트에 영향이 있으므로 프런트엔드/agent 팀 조율 필요 — 이 RCA 의 자동화 fix 대상에는 포함하지 않음. - SI-Lite dispatch 시점 필터: Tesla 측
run_si_lite_sync?에!capture.trashed?조건 추가 검토. 다만 큐잉 후 소비 전 trash 로 이동한 케이스는 이걸로 완전히 막을 수 없어 agent 측 skip 로직이 근본 대책.
Monitoring#
- SI-Lite agent 의 ENT4000 발생 추이:
service:cupixworks-si-lite-agent "ENT4000"
updateSiLiteState실패 (state 별로 분리):
service:cupixworks-si-lite-agent status:error "SiliteService::updateSiLiteState"
- SI-Lite agent 전체 error 량:
service:cupixworks-si-lite-agent status:error
Risk Assessment#
- Risk level: low
- 예상 복잡도: trivial (사용자 영향 없음, self-heal, 즉시 조치 없음)
- 단기 개선 (agent 측 trashed-capture skip + ENT4000 warn 다운그레이드) 복잡도: standard