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#
- 2026-07-01 09:00 KST (2026-07-01 00:00 UTC) — workspace
billing_expires_at도달,applied_billing_state= expired 로 전환 - 2026-07-01 09:20:10 KST —
RefinementService::run이 PUT/api/v1/captures/725158시도, tesla가403 PERM33000반환 (Capturerefinement_state가 이후error로 남음) - 2026-07-01 09:25:04 KST — Postprocessor agent 재시도,
CupixAuth::handleError가statusCode 403, PERM33000로그 - 2026-07-01 09:25:04 KST —
BaseService::getApiErrorToDeleteMessage가 4xx로 판단, SQS 메시지 삭제 대상으로 표시 (error msgwarn 로그) - 2026-07-01 09:25:06 KST —
BaseService::handlingMessageErrors가 error 레벨로 최종 로그, error-sweeper가 클러스터로 수집
Error Log#
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_state가 error 로 남았다 (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#update → check_updatable_by_billing_state! 경로에서 정상적으로 거절됐다. 이는 라이선스 만료 정책이 의도대로 동작한 결과이며, agent 코드나 데이터 결함이 아니다. Agent는 4xx 응답에 대해 SQS 메시지를 삭제하도록 이미 설계돼 있지만, 최종 로그 라인이 logger.error 로 남으면서 error-sweeper 가 클러스터로 수집한 것이 이번 알림의 실제 원인이다.
Technical Analysis#
Code Path#
Agent 측 (cupixworks / postprocessor):
await this.cupixApi.capture.update(cpCapture.id, {
skat_compute_stopped_at: skatComputeStoppedAt,
skat_compute_duration: skatComputeDuration,
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 메시지 처리):
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();
};
getApiErrorToDeleteMessage 은 statusCode 가 400 <= x <= 500 이면 (401 제외) 삭제 대상으로 판단하므로, 403 PERM33000 은 SQS 에서 정상 폐기된다.
if (statusCode != undefined && statusCode >= 400 && statusCode <= 500) {
if (statusCode === 401) return;
return errorMsg;
}
Tesla 측 (production origin/master, PATCH/PUT 흐름):
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)
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 쿼리:
service:cupixworks-capture-postprocessor-agent "PERM33000"
핵심 로그 (KST, tenant=cupix, env=production):
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):
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) 상태 발췌:
{
"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/master 의 base_repository.rb:132 → lib/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:132와app/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 발생률:
service:cupixworks-capture-postprocessor-agent "PERM33000" status:error
전 서비스 라이선스 만료 관련 403 추이:
"PERM33000" "billing expired"
Refinement 실패와의 연쇄 (동일 capture 반복 방지):
service:cupixworks-capture-postprocessor-agent "RefinementService::run" "PERM33000"
Risk Assessment#
- Risk level: low
- 예상 복잡도: trivial (정책상 예상된 동작; 필요한 경우 log level 조정만 검토)