ES /docs

BaseService::handlingMessageErrors | Error and message object - {"error":{"statusCode":403,"requestU

RCA: BaseService::handlingMessageErrors — PERM33000 on expired workspace capture update

Overview#

What Happened#

2026-07-01 09:25 KST에 cupixworks-capture-postprocessor-agent가 Capture 725158의 postprocessor 결과를 tesla API에 반영하려고 PUT /api/v1/captures/725158을 호출했고, tesla가 HTTP 403 PERM33000 ("Updating entity on billing expired team/workspace is not allowed")으로 거절했다. 해당 Capture가 속한 workspace의 billing_expires_at이 같은 날 00:00:00 UTC에 만료되면서 applied_billing_state = expired 상태로 전환된 결과다. 발생 건수는 1건이고, agent는 handlingMessageErrors에서 SQS 메시지를 삭제하며 사이클을 종료했다.

Quick Facts#

Field Value
exception.class Cupix::Errors::PermissionDenied (code PERM33000)
exception.message Updating entity on billing expired team/workspace is not allowed
top_frame packages/base/src/base-service.ts:311 (agent 측 로깅 지점)
origin lib/cupix/abstract/base.rb:44 (tesla 측 raise 지점)
env production, us-west-2
tenant cupix (team devcon)
affected resource Capture 725158, model row id 1163872
workspace state billing_state=expired, billing_expires_at=2026-07-01T00:00:00Z

Affected Teams#

Team / Domain Error Count Impact
devcon (workspace license expired) 1 단일 Capture의 postprocessor 후처리가 tesla에 반영되지 못함. SQS 메시지는 정상 폐기됨

Timeline#

  1. 2026-07-01 09:00 KST (2026-07-01 00:00 UTC) — workspace billing_expires_at 도달, applied_billing_state = expired 로 전환
  2. 2026-07-01 09:20:10 KSTRefinementService::run 이 PUT /api/v1/captures/725158 시도, tesla가 403 PERM33000 반환 (Capture refinement_state가 이후 error 로 남음)
  3. 2026-07-01 09:25:04 KST — Postprocessor agent 재시도, CupixAuth::handleErrorstatusCode 403, PERM33000 로그
  4. 2026-07-01 09:25:04 KSTBaseService::getApiErrorToDeleteMessage 가 4xx로 판단, SQS 메시지 삭제 대상으로 표시 (error msg warn 로그)
  5. 2026-07-01 09:25:06 KSTBaseService::handlingMessageErrors 가 error 레벨로 최종 로그, error-sweeper가 클러스터로 수집

Error Log#

Datadog Logs

text
BaseService::handlingMessageErrors | Error and message object - {"error":{"statusCode":403,"requestUriHref":"http://api-tesla.cupix.internal/api/v1/captures/725158?fields%5B0%5D=id&...&fields%5B44%5D=summary_state","bodyResult":{"code":"PERM33000","type":"Cupix::Errors::PermissionDenied","reason":"Updating entity on billing expired team/workspace is not allowed","message":"Updating entity on billing expired team/workspace is not allowed"},"modelId":1163872},"sqsMessage":{"MessageId":"b5789c55-bfeb-4240-bcb3-fa74baaf30df","Attributes":{"ApproximateReceiveCount":"1"}}}

Impact#

  • Service: cupixworks-capture-postprocessor-agent
  • Team: devcon
  • 발생 횟수: 1
  • 최초 발생: 2026-07-01 09:25 KST
  • 최근 발생: 2026-07-01 09:25 KST

Capture 725158의 postprocessor 결과(우측 상태/URL 필드)가 tesla에 반영되지 않았고, 앞선 RefinementService 도 실패하여 Capture refinement_stateerror 로 남았다 (Kibana captures/725158 확인). Agent는 SQS 메시지를 정상 폐기하여 재시도 폭주는 없고, 서비스 가용성 영향은 없다.

Root Cause Summary#

Tesla는 만료된 workspace의 리소스 업데이트를 PERM33000으로 차단한다. Capture 725158이 속한 workspace의 라이선스가 2026-07-01 00:00:00 UTC에 만료되었고, 그 25분 뒤 postprocessor agent가 Capture 상태를 PUT /api/v1/captures/725158 로 갱신하려 하자 BaseRepository#updatecheck_updatable_by_billing_state! 경로에서 정상적으로 거절됐다. 이는 라이선스 만료 정책이 의도대로 동작한 결과이며, agent 코드나 데이터 결함이 아니다. Agent는 4xx 응답에 대해 SQS 메시지를 삭제하도록 이미 설계돼 있지만, 최종 로그 라인이 logger.error 로 남으면서 error-sweeper 가 클러스터로 수집한 것이 이번 알림의 실제 원인이다.

Technical Analysis#

Code Path#

Agent 측 (cupixworks / postprocessor):

packages/cupix-capture-postprocessor-agent/src/postprocessor-service.ts:296-298typescript
await this.cupixApi.capture.update(cpCapture.id, {
    skat_compute_stopped_at: skatComputeStoppedAt,
    skat_compute_duration: skatComputeDuration,
packages/api/src/api/capture.api.ts:56-59typescript
async update(captureId: number, updateCaptureRequest: TESLA.UpdateCaptureRequest): Promise<TESLA.Capture> {
    const api = await this.api();
    const res = await api.updateCapture(captureId, Fields.CaptureFields, updateCaptureRequest);
    return unwrapAttributes(res);
}

Agent 측 error handler (SQS 메시지 처리):

packages/base/src/base-service.ts:290-312typescript
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();
};

getApiErrorToDeleteMessagestatusCode400 <= x <= 500 이면 (401 제외) 삭제 대상으로 판단하므로, 403 PERM33000 은 SQS 에서 정상 폐기된다.

packages/base/src/base-service.ts:270-275typescript
if (statusCode != undefined && statusCode >= 400 && statusCode <= 500) {
    if (statusCode === 401) return;

    return errorMsg;
}

Tesla 측 (production origin/master, PATCH/PUT 흐름):

app/repositories/base_repository.rb:125-137ruby
def update(params = {}, current_user = nil)
    raise Cupix::Errors::PermissionDenied.new(code: 'PERM10000', reason: 'Permission denied') unless @model.updatable_by?(@current_user)

    if self.current_user.present? && self.model.present? && self.model.respond_to?(:applied_cycle_state)
      raise Cupix::Errors::PermissionDenied.new(code: 'PERM32000', reason: 'Archived entity') if %w[archived archiving].include?(self.model.applied_cycle_state) && !Pundit.policy(self.current_user, self.model).update?
    end

    check_updatable_by_billing_state!

    set_params(params)
lib/cupix/abstract/base.rb:35-49 (origin/master)ruby
def check_updatable_by_billing_state!
  Cupix::Logger.debug("[check_updatable_by_billing_state!] begins on #{self.model.class.name}", class: self.class.name, method: __method__)

  if self.current_user.present? && self.model.present? && (self.model.respond_to?(:workspace_id) && self.model.respond_to?(:team_id)) && self.current_user.team_id != TeamRepository.admin_team_id && self.model.respond_to?(:applied_billing_state)
    Cupix::Logger.debug("[check_updatable_by_billing_state!] in progress on #{self.model.class.name} ...")

    if self.model.applied_billing_state == 'expired'
      raise Cupix::Errors::PermissionDenied.new(code: 'PERM33000', reason: 'Updating entity on billing expired team/workspace is not allowed')
    end
  end
end

Failure point: production 코드는 update 경로에서도 check_updatable_by_billing_state! 를 호출하므로 만료된 workspace 하위 Capture 의 어떤 필드 업데이트든 403 이 된다. 참고로 develop 브랜치에는 TSLA-12641 (74f00a3fb) 로 update 경로에서 이 체크가 제거된 상태이지만, 아직 master 에 반영되지 않았다.

기대 동작 vs 실제 동작:

  • 기대: 라이선스 만료된 workspace 의 신규 Capture 생성은 차단하되, 이미 존재하는 Capture 의 processing pipeline 이 남긴 상태 업데이트는 통과시키기 (TSLA-12641 도입 이유).
  • 실제: master 는 update 도 차단. Postprocessor agent 가 skat 결과를 반영하려다 403 을 받고 후처리 결과가 유실됨.

Log Evidence#

Datadog 쿼리:

text
service:cupixworks-capture-postprocessor-agent "PERM33000"

핵심 로그 (KST, tenant=cupix, env=production):

text
2026-07-01 09:25:04  warn  CupixAuth::handleError | Response statusCode: 403, requestUriHref: http://api-tesla.cupix.internal/api/v1/captures/725158?..., body.result: {"code":"PERM33000","type":"Cupix::Errors::PermissionDenied","reason":"Updating entity on billing expired team/workspace is not allowed","message":"Updating entity on billing expired team/workspace is not allowed"}
2026-07-01 09:25:04  warn  BaseService::getApiErrorToDeleteMessage | error msg - {"statusCode":403,"requestUriHref":".../captures/725158?...","bodyResult":{"code":"PERM33000","reason":"Updating entity on billing expired team/workspace is not allowed"},"modelId":1163872}
2026-07-01 09:25:06  error BaseService::handlingMessageErrors | Error and message object - {"error":{"statusCode":403,...},"sqsMessage":{"MessageId":"b5789c55-bfeb-4240-bcb3-fa74baaf30df","Attributes":{"ApproximateReceiveCount":"1"}}}

앞선 refinement 실패 (동일 Capture):

text
2026-07-01 09:20:10  RefinementService::run | end - {"response":{"statusCode":403,"body":{"result":{"code":"PERM33000",...}}, ... "method":"PUT","headers":{"User-Agent":"cupix-agent","X-CUPIX-AUTH":"session_token:r93vjem4qr8r,session_id:10787729"}}}

Kibana captures/725158 (env prod, index captures) 상태 발췌:

json
{
  "id": 725158,
  "state": "done",
  "refinement_state": "error",
  "team": { "domain": "devcon" },
  "workspace": {
    "billing_state": "expired",
    "billing_expires_at": "2026-07-01T00:00:00.000Z",
    "applied_billing_state": "expired"
  }
}

지난 7일 전체 PERM33000 로그 건수는 4건 (2026-06-25 2건, 2026-07-01 3건 중 이 클러스터에 속하는 항목 포함) 으로, 반복/폭주 패턴은 없다.

Hypotheses Considered#

# Hypothesis Evidence for Evidence against Verdict
H1 Workspace 라이선스 만료로 tesla 가 PUT /captures/725158 를 정상 차단, agent 는 이를 error 레벨로 로깅 Kibana 에서 workspace applied_billing_state=expired, billing_expires_at=2026-07-01T00:00:00Z 확인. tesla origin/masterbase_repository.rb:132lib/cupix/abstract/base.rb:44 에서 PERM33000 raise. 로그 타임라인(00:00 UTC 만료 → 00:20 UTC 첫 실패 → 00:25 UTC 두 번째 실패) 일치 Confirmed
H2 Agent 자체 버그 (잘못된 payload, 인증 실패) 로그에 session_token/session_id 정상 전달, 다른 5xx/401 없음. 응답 본문이 명확히 PERM33000 (permission) tesla 서버가 4xx 로 명확한 코드 반환하므로 agent 결함 아님 Rejected
H3 Agent 의 error handling 회귀 (SQS 재시도 폭주) 클러스터가 error-sweeper 에 잡힘 getApiErrorToDeleteMessage 가 statusCode 403 을 삭제 대상으로 반환 (base-service.ts:270-274), ApproximateReceiveCount:1, 이후 재시도 로그 없음 Rejected
H4 만료 시각 계산/타임존 결함 (조기 만료) Workspace billing_expires_at=2026-07-01T00:00:00Z 이고 실패는 00:20 UTC 부터 시작, 만료 이후 발생 Rejected
H5 Update 경로 차단은 정책 위반 (develop 의 TSLA-12641 미배포로 인한 회귀) develop 브랜치에는 74f00a3fb TSLA-12641 feat: restrict license expiry check to Workspace/Facility/Capture 로 update 차단 제거, origin/master 는 여전히 update 차단 정책 자체는 유효(TSLA-12641 목적이 "만료 이후에도 processing pipeline 이 남긴 update 는 통과") — 즉 이번 에러는 아직 배포되지 않은 개선의 회귀 사례 Confirmed (secondary)

Fix Recommendation#

즉시 조치 (Critical)#

  • 코드 변경 불필요. Tesla 의 PERM33000 응답은 라이선스 만료 정책의 의도된 동작이며, agent 는 4xx 응답을 이미 SQS 에서 폐기한다.
  • 운영 판단: 이 error 로그가 반복 알림을 유발한다면, tesla 측에서 라이선스 만료된 workspace 에 대해 agent-originated PUT 을 warn 수준으로 다운그레이드하거나, agent 측 handlingMessageErrors 에서 statusCode === 403 && code === 'PERM33000' 인 경우에만 logger.warn 으로 남기도록 조정할 수 있다. 이 경우 memory 의 "AUTH20022/AUTH20023 log level downgrade" episode 와 동일한 패턴이므로 사용자 승인 후 진행.

단기 개선 (1주 이내)#

  • Tesla TSLA-12641 (74f00a3fb TSLA-12641 feat: restrict license expiry check to Workspace/Facility/Capture) 을 master 로 배포하여 processing pipeline 이 만료 후에도 Capture 상태를 반영할 수 있게 한다. 이 변경은 app/repositories/base_repository.rb:132app/repositories/measurement_repository.rb:40 에서 check_updatable_by_billing_state! 를 제거하고 lib/cupix/abstract/base.rb 의 화이트리스트 (BILLING_EXPIRY_BLOCKED_ON_CREATE = %w[Workspace Facility Capture]) 로 create 만 차단하도록 좁힌다.
  • 배포 전에는 이 클러스터를 resolved (won't fix, expected) 로 처리한다.

장기 개선 (재발 방지)#

  • Postprocessor agent 의 updateErrorState 경로가 만료 workspace 에서도 최소한 refinement_state=error 를 남기도록 tesla 측에서 별도 endpoint (또는 서비스 계정 우회) 를 제공한다. 현재는 update 자체가 403 이라 processing 상태가 tesla 에 반영되지 않아 UI 가 stale 상태를 보인다.
  • 라이선스 만료 후 pending SQS 잡을 폐기하는 정책을 tesla 측에서 명시적으로 문서화 (기존 TSLA-12641 문서 확장).

Monitoring#

만료 workspace 로 인한 agent PERM33000 발생률:

text
service:cupixworks-capture-postprocessor-agent "PERM33000" status:error

전 서비스 라이선스 만료 관련 403 추이:

text
"PERM33000" "billing expired"

Refinement 실패와의 연쇄 (동일 capture 반복 방지):

text
service:cupixworks-capture-postprocessor-agent "RefinementService::run" "PERM33000"

Risk Assessment#

  • Risk level: low
  • 예상 복잡도: trivial (정책상 예상된 동작; 필요한 경우 log level 조정만 검토)