ES /docs

SiliteService::updateSiLiteState | failed - captureId: 736992, state: error, error: {"stack":"HttpEr

RCA: SiliteService::updateSiLiteState — 403 ENT4000 (state=error) 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 분석을 시작. run 이 첫 상태 전환인 updateSiLiteState(Processing) 에서 Tesla API PATCH /api/v1/captures/736992 로부터 HTTP 403 + ENT4000 / Cupix::Errors::NotFound 를 받아 예외가 던져졌고 (sibling 619a1e49), 이어서 run 의 catch 절이 updateSiLiteState(Error) 를 재호출하면서 동일 캡처가 여전히 trash 상태이므로 33ms 뒤 동일한 403 이 다시 반환되어 본 클러스터 로그가 남았다. 근본 원인은 sibling 과 동일 — capture 736992 가 2026-07-24 01:53 KST 에 cycle_state=trashed 로 전환된 뒤 stale SQS 메시지가 뒤늦게 소비된 것. 사용자 영향 없음.

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:109 (updateSiLiteState(Error) in catch) → 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 (sibling 619a1e49 Kibana 조회)

Affected Teams#

Team / Domain Error Count Impact
sprouts (team_id 1185) 1 단일 capture (736992) 의 SI-Lite 재분석 실패 처리 경로에서 발생. 사용자 영향 없음 — 캡처가 이미 trash 로 이동됨

Timeline#

  1. 2026-07-24 01:53 KST — Capture 736992 가 trash 로 이동됨 (cycle_state=trashed, cycle_state_updated_at=2026-07-23T16:53:36Z).
  2. 2026-07-24 16:34:56.340 KST — SQS 메시지 53531766-cc74-4823-8c03-26026d6962c5 수신 → runupdateSiLiteState(Processing) 이 403 ENT4000 (sibling 619a1e49).
  3. 2026-07-24 16:34:56.373 KSTrun catch 절의 updateSiLiteState(Error) 재호출이 동일 403 ENT4000 로 실패 → 본 클러스터 로그 발생.
  4. 2026-07-24 16:34:56.382 KSTBaseService::handlingMessageErrors 가 최종 에러 로깅 (sibling 23a74e95) → 4xx 이므로 SQS 메시지 삭제 후 종료.

Error Log#

Datadog Logs

text
SiliteService::updateSiLiteState | failed - captureId: 736992, state: error, 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 619a1e49 (state=processing) 및 23a74e95 (handlingMessageErrors) 와 함께 동일 SQS 메시지에서 43ms 이내 연속으로 발생. 세 클러스터의 근본 원인은 동일 (trashed capture 에 대한 stale SQS 메시지). 데이터 손상, 사용자 노출, 재시도 폭주 없음.

Root Cause Summary#

SI-Lite agent run 이 첫 상태 전환 (updateSiLiteState(Processing)) 에서 403 을 만나 예외를 던진 뒤 catch 절이 동일 API 경로로 상태만 Error 로 바꿔 재호출 (silite-service.ts:108-111). 그러나 캡처가 여전히 cycle_state=trashed 이므로 Tesla BaseRepository#find_byin_trash 분기가 다시 Cupix::Errors::NotFound(code: 'ENT4000') 를 raise 하고 client_error_controller.rb 가 이를 HTTP 403 으로 응답 (base_repository.rb:352-353, client_error_controller.rb:57-59). updateSiLiteState catch 블록 (silite-service.ts:352-355) 이 이 403 을 그대로 logger.error 로 남기면서 본 클러스터 로그가 기록되고, 이후 예외가 BaseService::handlingMessageErrors 로 전파돼 sibling 23a74e95 로그가 남는다. 즉 본 클러스터는 sibling 619a1e49후행 파생 로그 — trashed capture 감지 부재라는 동일 결함이 catch 경로에서 한 번 더 노출된 것.

Technical Analysis#

Code Path#

Entry point: SI-Lite agent 가 SQS 메시지 { id: 736992 } 를 수신 → BaseService::runByMessageSiliteService.run(736992).

updateSiLiteState(Processing) (sibling 619a1e49) 이 실패하면 catch 절이 updateSiLiteState(Error) 를 다시 호출:

applications/agents/packages/cupix-si-lite-agent/src/silite-service.ts:84-112typescript
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);  // sibling 619a1e49 실패
        const cpCapture = await this.fetchAndValidateCapture(targetId);
        // ...
        await this.updateSiLiteState(targetId, TESLA.SiLiteState.Done);
        logger.info('SiliteService::run | end - captureId: %d', targetId);
    } catch (error) {
        await this.updateSiLiteState(targetId, TESLA.SiLiteState.Error);  // ← 본 클러스터의 실패 지점 (line 109)
        throw error;
    }
};

Failure point: updateSiLiteState(Error) 가 다시 this.cupixApi.capture.update 를 호출하며 SDK 가 HttpError 403 을 throw. catch 블록이 logger.error(...) 로 로그 기록 후 throw:

applications/agents/packages/cupix-si-lite-agent/src/silite-service.ts:343-357typescript
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 측: BaseRepository#find_by 가 trashed 여부를 감지해 ENT4000 을 raise (sibling RCA 에서 확인된 흐름 그대로):

app/repositories/base_repository.rb:342-357ruby
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

ENT4000 → HTTP 403:

app/controllers/concerns/client_error_controller.rb:27,57-59ruby
rescue_from Cupix::Errors::NotFound, with: :not_found_403_error
# ...
def not_found_403_error(exception)
    raise_error(403, exception)
end

기대 동작: run 이 첫 시도에서 trashed 를 인지하면 catch 절에서 updateSiLiteState(Error) 를 시도하지 않고 warn 로그만 남긴 채 종료. 최소한 catch 절이 status=403 + ENT4000 인 경우 재호출을 skip.

실제 동작: catch 절이 원인 코드와 상관없이 무조건 updateSiLiteState(Error) 를 호출하므로 이미 확실히 실패할 요청이 한 번 더 나가고 두 번째 error 로그가 남는다.

Log Evidence#

Datadog query (재현용):

text
service:cupixworks-si-lite-agent status:error @environment:production "SiliteService::updateSiLiteState"

Datadog 검색 결과 — 동일 SQS 메시지에서 33ms 간격으로 파생된 두 개의 실패 로그:

text
2026-07-24 16:34:56.340  [error]  SiliteService::updateSiLiteState | failed - captureId: 736992, state: processing, error: {"stack":"HttpError ... statusCode":403,"body":{}, "name":"HttpError"}
2026-07-24 16:34:56.373  [error]  SiliteService::updateSiLiteState | failed - captureId: 736992, state: error,      error: {"stack":"HttpError ... statusCode":403,"body":{}, "name":"HttpError"}  ← 본 클러스터

Sibling 23a74e95handlingMessageErrors 로그에서 확인된 bodyResult (본 요청과 동일 HTTP 응답):

json
{
  "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 619a1e49 에서 확인:

json
{
  "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 본 클러스터는 sibling 619a1e49 (state=processing) 실패에 대한 catch 절 파생 로그로, 원인은 동일하게 trashed capture 736992 에 대한 Tesla API 403 ENT4000. silite-service.ts:109updateSiLiteState(Error) 가 catch 블록에서 재호출되어 두 번째 403 로그 생성 Code: silite-service.ts:108-111 catch 블록 존재; Datadog: 두 로그가 동일 SQS 메시지에서 33ms 간격으로 (state:processingstate:error) 발생; Kibana: cycle_state=trashed 확인 (sibling RCA); Tesla code: base_repository.rb:352-353 in_trash 분기 및 client_error_controller.rb:57-59 403 매핑 Confirmed
H2 catch 절 대신 별개의 SQS retry (ApproximateReceiveCount 증가) 로 인한 재시도 두 개의 유사 로그가 존재 sibling 23a74e95 bodyResult 에서 ApproximateReceiveCount=1 확인. 두 로그의 timestamp 간격이 33ms 로 SQS visibility timeout (수십초) 대비 지나치게 짧아 재수신이 불가능 Rejected
H3 Sprouts 팀 API 토큰 만료로 인한 403 statusCode 403 권한 실패는 PERM10000 코드. body 는 ENT4000 이며 첫 요청 (sibling 619a1e49) 도 동일 자격증명 사용. 즉 자격증명 문제가 아닌 리소스 상태 문제 Rejected
H4 catch 절에서 던지는 예외가 별도의 error 로그를 발생시킨 것 (즉 새로운 네트워크 호출이 아님) 두 로그가 매우 근접 두 로그의 message 가 state: processing vs state: error 로 다르며 updateSiLiteState 함수 내 log format 에 state 인자가 포함됨 (silite-service.ts:353). 서로 다른 인자로 두 번 호출되었음이 확실 Rejected

Fix Recommendation#

즉시 조치 (Critical)#

  • 없음. 사용자 영향/데이터 손상 없고 SQS 메시지가 4xx 로 분류되어 자동 삭제됨. 코드 변경 불필요.

단기 개선 (1주 이내)#

  • run 초기에 trashed capture skip (sibling 619a1e49 와 통합): applications/agents/packages/cupix-si-lite-agent/src/silite-service.ts:84-112 의 순서를 재정렬해 updateSiLiteState(Processing) 전에 capture 를 먼저 fetch. capture.get 이 403 + body.code === 'ENT4000' 로 실패하면 warn 로그만 남기고 정상 종료. 이 개선 하나로 sibling 619a1e49, 본 클러스터 ab8a602b, sibling 23a74e95 세 개의 error 로그가 모두 사라진다.
  • catch 절의 재호출을 조건부로 변경: silite-service.ts:108-111 catch 블록에서 error 가 이미 SDK HttpError 403+ENT4000 인 경우 updateSiLiteState(Error) 를 skip. 예:
    applications/agents/packages/cupix-si-lite-agent/src/silite-service.ts:108-111
     \} catch (error) \{-    await this.updateSiLiteState(targetId, TESLA.SiLiteState.Error);+    if (!isTrashedCaptureError(error)) \{+        await this.updateSiLiteState(targetId, TESLA.SiLiteState.Error);+    \}     throw error; \}
    이 변경은 위 첫 번째 개선의 부분 대응이지만, 첫 번째 개선이 지연되는 경우 본 클러스터만이라도 즉시 제거 가능. 단독으로는 sibling 619a1e49 는 남는다.
  • ENT4000 전용 로그 레벨 다운그레이드: silite-service.ts:352-354 catch 블록에서 bodyResult.code === 'ENT4000' 인 경우 logger.warn 으로 다운그레이드. 코드 특정으로 좁혀 실제 authorization 실패 (PERM10000) 를 놓치지 않도록 주의.

장기 개선 (재발 방지)#

  • Tesla API 응답 semantics 정리: ENT4000reason 필드가 "Capture not found" 로 표기되어 trashed 상황을 클라이언트가 감지하기 어렵다. reason"is trashed" 로 수정하거나 HTTP 410 Gone 으로 분리 검토. 프런트엔드 및 다수 agent 팀 조율 필요 — 이 RCA 의 자동화 fix 대상에서 제외.
  • SI-Lite dispatch 시점 필터: Tesla 측 run_si_lite_sync? 등의 dispatch 조건에 !capture.trashed? 추가 검토. 다만 큐잉 후 소비 전 trash 이동 케이스는 완전히 막을 수 없어 agent 측 skip 이 근본 대책.

Monitoring#

  • SI-Lite agent 의 ENT4000 발생 추이:
text
service:cupixworks-si-lite-agent "ENT4000"
  • updateSiLiteState 실패 (state 별로 확인 가능):
text
service:cupixworks-si-lite-agent status:error "SiliteService::updateSiLiteState"
  • SI-Lite agent 전체 error 량:
text
service:cupixworks-si-lite-agent status:error

Risk Assessment#

  • Risk level: low
  • 예상 복잡도: trivial (사용자 영향 없음, self-heal, 즉시 조치 없음)
  • 단기 개선 (agent 측 trashed-capture skip 통합) 복잡도: standard