ES /docs

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_byin_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#

  1. 2026-07-17 17:14 KST — SI-Lite 첫 실행 성공 (SiliteProcessManager::execute | captureId: 736992, workareas: 1, trades: 9).
  2. 2026-07-24 01:53 KST — Capture 736992 가 trash 로 이동됨 (cycle_state=trashed, cycle_state_updated_at=2026-07-23T16:53:36Z).
  3. 2026-07-24 16:34:56.340 KST — SQS 메시지 53531766-cc74-4823-8c03-26026d6962c5 수신 → SiliteService::run | beginupdateSiLiteState(Processing) 호출 → 본 클러스터 로그 발생 (state:processing, 403 ENT4000).
  4. 2026-07-24 16:34:56.373 KSTruncatch 절이 updateSiLiteState(Error) 재호출 → 동일 403 (sibling ab8a602b).
  5. 2026-07-24 16:34:56.382 KSTBaseService::handlingMessageErrors 최종 에러 로깅 (sibling 23a74e95) → getApiErrorToDeleteMessage 가 4xx 이므로 SQS 메시지 삭제 후 종료.

Error Log#

Datadog Logs

text
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.rbrescue_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::runByMessageSiliteService.run(736992).

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);  // ← 본 클러스터의 실패 지점
        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: updateSiLiteStatethis.cupixApi.capture.update 를 호출하다 SDK 가 HttpError 403 을 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 측 실패 지점 — 트래시된 캡처를 find_by 에서 발견하고 ENT4000 을 raise:

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

Trashable::Capture.in_trash:

app/models/concerns/trashable/capture.rb:18-20ruby
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 변환:

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

기대 동작: 트래시된 캡처는 SI-Lite 분석 대상이 아니므로 agent 가 상태 전환을 시도하기 전에 trash 여부를 감지해 warn 레벨로 skip.

실제 동작: run 이 검증 이전 시점에 무조건 updateSiLiteState(Processing) 을 먼저 호출하고, 403 을 다른 authorization 실패와 구분하지 않아 error 로그가 남는다.

Log Evidence#

Datadog query (재현용):

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

같은 SQS 메시지에서 파생된 두 개의 updateSiLiteState 실패 로그 (Datadog 검색 결과):

text
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 클러스터 23a74e95handlingMessageErrors 로그에서 확인된 bodyResult:

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 에서 확인된 데이터:

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 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) 와 sibling ab8a602b (state:error), 23a74e95 (handlingMessageErrors) 세 개가 모두 warn 한 줄로 정리된다.
  • ENT4000 전용 로그 레벨 다운그레이드: applications/agents/packages/cupix-si-lite-agent/src/silite-service.ts:353-354updateSiLiteState catch 블록에서 error 응답의 bodyResult.code === 'ENT4000' 인 경우 logger.warn 으로 다운그레이드. 단, 코드 특정 (ENT4000) 로 좁힐 것 — 모든 4xx 를 warn 으로 낮추면 실제 authorization 실패 (PERM10000) 를 놓칠 수 있음.

장기 개선 (재발 방지)#

  • Tesla API 응답 semantics 정리: ENT4000entity.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 발생 추이:
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 + ENT4000 warn 다운그레이드) 복잡도: standard