Api::V1::PanosController#check_mask_uploading (avg 66605ms, max 159799ms)
RCA: Api::V1::PanosController#check_mask_uploading (avg 66605ms, max 159799ms)
Overview#
What Happened#
2026-06-26 10:5110:53 KST 사이 3회 + 재시도 sleep 누적 형태로 늘어진 것으로 분석된다.cupixworks-api (ap-southeast-2, tenant cupix) 의 Api::V1::PanosController#check_mask_uploading 요청 4건이 평균 66.6초, 최대 159.8초의 비정상 지연을 보였다. 동일 시간/리전/테넌트에서 panos 테이블을 대상으로 한 Mysql2::Error::TimeoutError: Lock wait timeout exceeded (50087ms ≈ innodb_lock_wait_timeout 50초) 502 응답이 다수 발생하고 있었고, status board 도 본 cluster 를 parent incident 2026-06-26-svc-cupixworks-api--unknown-1 (총 7 cluster) 에 자동으로 묶었다. 본 cluster 의 trace 들은 결과적으로 200 응답을 받았으나, MaskableRepository#check_mask_uploading 내부의 panos UPDATE / state 전이가 InnoDB row-lock 경합에 묶여 50초 lock-wait timeout 1
Quick Facts#
| Field | Value |
|---|---|
| resource_name | Api::V1::PanosController#check_mask_uploading |
| avg_duration_ms | 66605 |
| max_duration_ms | 159799 |
| occurrences | 4 |
| sample_trace_id | 134794658443570874 |
| env | production, ap-southeast-2 |
| tenant | cupix |
Affected Teams#
| Team / Domain | Error Count | Impact |
|---|---|---|
| cupix (tenant), ap-southeast-2 | 4 traces (이 cluster) + 6 동시 cluster (parent incident 2026-06-26-svc-cupixworks-api--unknown-1) |
pano mask 업로드 후처리 응답이 평균 66초 / 최대 159초 지연 — 클라이언트(SK앱/업로더) 타임아웃 또는 사용자 인지 실패 가능. 같은 시간대 check_mask_uploading 1건은 502 LockWaitTimeout 으로 실패. |
Timeline#
- 2026-06-26 10:25 KST — Parent incident
2026-06-26-svc-cupixworks-api--unknown-1시작 (Api::V1::PanosController#update41.5s latency, cluster943fdcb4). - 2026-06-26 10:46 KST 이후 —
panos테이블 대상Mysql2::Error::TimeoutError: Lock wait timeout exceeded502 응답 다수 (check_tile_uploading,check_mask_uploading,mask_upload_url). duration ≈ 50087ms =innodb_lock_wait_timeout기본값 50s. - 2026-06-26 10:51:38 KST — 본 cluster 의 first_seen.
check_mask_uploading요청이 평균 66s 대로 지연되기 시작. - 2026-06-26 10:53:29 KST —
PUT /api/v1/panos/14133757/check_mask_uploading가 50s lock-wait timeout 으로 502 반환 (별도 cluster). - 2026-06-26 10:53:30 KST — 본 cluster last_seen (159799ms 트레이스 포함). 동시 cluster 들도 같은 패턴.
Error Log#
{
"resource_name": "Api::V1::PanosController#check_mask_uploading",
"service": "cupixworks-api",
"occurrences": 4,
"avg_ms": 66605,
"max_ms": 159799,
"sample_trace_id": "134794658443570874"
}
Impact#
- Service:
cupixworks-api - 발생 횟수: 4
- 최초 발생: 2026-06-26 10:51 KST
- 최근 발생: 2026-06-26 10:53 KST
Root Cause Summary#
본 cluster 는 단독 코드 결함이 아니라 parent incident 2026-06-26-svc-cupixworks-api--unknown-1 의 한 면이다. check_mask_uploading 액션은 MaskableRepository#check_mask_uploading 에서 @model.save! 및 @model.uploaded_mask_state! (state machine UPDATE) 로 panos row 를 쓰는 흐름을 가진다. 사건 시각 ap-southeast-2 production DB 의 panos 테이블에는 광범위한 InnoDB row-lock 경합이 발생 중이었고 (정확히 50087ms ≈ innodb_lock_wait_timeout 50s 에 끊긴 502 다수가 동일 시간/리전/테넌트에서 관측됨), 본 cluster 의 4 traces 는 같은 row 들에 대한 lock 대기로 50s timeout + 재시도/sleep loop 누적 형태로 66~159s 지연된 채 결국 200 으로 종료된 케이스다. 추가로 MaskableRepository#check_mask_uploading 내부에 존재하는 StateMachines::InvalidTransition 시 최대 5회 sleep(retries) (1+2+3+4+5=15s) 재시도 로직이 최악 시 50s lock-wait + 15s sleep 패턴을 한 요청 안에서 증폭시킨다.
Technical Analysis#
Code Path#
엔드포인트 → controller concern → repository concern → S3 / DB 의 흐름.
def check_mask_uploading
mask = repository_instance.check_mask_uploading(params[:mask_type])
render_api Renderable.new({
contents: mask,
serializer: MaskSerializer
})
end
저장/상태 전이는 repository concern 에서 수행한다. lock 경합 시 50s 로 끊기는 핵심 두 지점이 @model.save! 와 @model.uploaded_mask_state! (state machine 의 UPDATE) 다:
def check_mask_uploading(mask_type)
_mask_type = mask_type.presence || 'custom'
mask = @model.masks.find_by_mask_type(_mask_type)
raise Cupix::Errors::NotFound.new(code: 'ARG10002', reason: "Mask not found. mask_type: #{mask_type}") if mask.nil?
if mask.mask_uploaded?(mask_revision: mask.mask_upload_revision)
mask.uploaded
retries = 0
if _mask_type == 'custom'
@model.mask = mask
@model.current_mask_url = mask.mask_url
@model.save! # ← panos row UPDATE (lock 1)
# ...
end
begin
@model.uploaded_mask_state! # ← panos state 전이 (lock 2)
rescue StateMachines::InvalidTransition
if (retries += 1) <= 5 && @model.mask_state_uploading?
# ...
sleep(retries) # ← 1+2+3+4+5 = 15s budget
retry
end
end
else
mask.missing
@model.missing_mask_state! # ← panos state 전이
raise Cupix::Errors::InvalidState.new(code: 'STAT10000', reason: 'Mask does not uploaded')
end
mask
end
S3 HEAD 호출 자체는 standard AWS SDK 의 Aws::S3::Object#exists? 이며 보통 ms 단위로 끝난다:
def mask_uploaded?(params = {})
mask_object(params).exists?
end
def mask_object(params = {})
Cupix::StorageService.object(
storage_option: storage_option,
bucket_name: hosting_bucket_name,
key: mask_object_key(params)
)
end
- Entry point:
app/controllers/concerns/maskable_controller.rb:7 - 1차 lock 지점:
app/repositories/concerns/maskable_repository.rb:17(@model.save!) - 2차 lock 지점:
app/repositories/concerns/maskable_repository.rb:28(@model.uploaded_mask_state!) - 재시도/sleep 증폭 지점:
app/repositories/concerns/maskable_repository.rb:30-40(최대 1+2+3+4+5=15s 의sleep(retries))
기대 동작: 정상 시간대 (예: 10:50:42 KST 의 PUT /api/v1/panos/14133460/check_mask_uploading 200) 처럼 access log 한 줄로 완료 — 100ms 수준.
실제 동작: 4 traces 가 66605159799ms 지연. 50s lock-wait timeout 의 13배 + retry sleep 누적 형태에 부합. 같은 윈도우의 1건은 50087ms 에 끊기며 502 LockWaitTimeout 으로 실패 (별도 cluster, 같은 parent incident).
Log Evidence#
같은 시간/리전/테넌트에서 발생한 Lock wait timeout 을 좁혀 조회.
service:cupixworks-api "Lock wait timeout" "check_mask_uploading"
시간 범위: 2026-06-26T01:25:00Z ~ 2026-06-26T02:00:00Z
본 cluster 윈도우(first_seen ~ last_seen 포함) 안의 502 LockWaitTimeout:
{
"timestamp": "2026-06-26 10:53:29 KST",
"status": "info",
"message": "[502] PUT /api/v1/panos/14133757/check_mask_uploading (Api::V1::PanosController#check_mask_uploading)",
"error": {
"message": "Mysql2::Error::TimeoutError: Lock wait timeout exceeded; try restarting transaction",
"class": "ActiveRecord::LockWaitTimeout"
}
}
같은 parent incident 의 sibling RCA (d22ba254-b364-411b-801f-af7799b40331) 가 수집한 panos 테이블 lock 경합 로그 (정확히 50087ms 에 끊김 → innodb_lock_wait_timeout 50s 일치):
2026-06-26 10:46:39 [502] PUT /api/v1/panos/14131474/check_tile_uploading ActiveRecord::LockWaitTimeout
2026-06-26 10:46:43 [502] PUT /api/v1/panos/14132644/check_tile_uploading ActiveRecord::LockWaitTimeout
2026-06-26 10:47:28 [502] PUT /api/v1/panos/14132652/check_tile_uploading ActiveRecord::LockWaitTimeout
2026-06-26 10:47:34 [502] PUT /api/v1/panos/14131474/check_tile_uploading ActiveRecord::LockWaitTimeout
2026-06-26 10:48:41 [502] PUT /api/v1/panos/14131939/check_tile_uploading ActiveRecord::LockWaitTimeout
2026-06-26 10:53:28 [502] POST /api/v1/panos/14133747/mask_upload_url ActiveRecord::LockWaitTimeout
2026-06-26 10:53:29 [502] PUT /api/v1/panos/14133757/check_mask_uploading ActiveRecord::LockWaitTimeout
정상 시간대 같은 엔드포인트는 access log 한 줄로 완료 (수십~수백 ms):
2026-06-26 10:50:42 [200] PUT /api/v1/panos/14133460/check_mask_uploading
2026-06-26 10:51:10 [200] PUT /api/v1/panos/14133487/check_mask_uploading
2026-06-26 10:51:10 [200] PUT /api/v1/panos/14133481/check_mask_uploading
avg 66605ms, max 159799ms 의 패턴은 단일 50s lock-wait 보다 길어 (1) 50s timeout 후 retry (state machine 재시도 sleep 15s budget + 두 번째 lock 대기) 또는 (2) 50s lock-wait 이 같은 요청 안에서 2~3회 누적된 형태로 해석된다. 단일 코드 경로에서 보장된 wait time (50s × N + sleep ≤ 15s) 의 산술 범위 안에 본 cluster 의 모든 trace duration 이 들어간다.
status board 도 본 cluster 를 2026-06-26-svc-cupixworks-api--unknown-1 에 묶었으며 같은 윈도우의 다른 6 cluster (Api::V1::PanosController#update, Admin::CapturesController#index, WorkspacesController#index, AssetsController#download, ...) 도 ap-southeast-2 / tenant cupix 의 동일 latency 폭증 패턴을 공유한다.
Hypotheses Considered#
| # | Hypothesis | Evidence for | Evidence against | Verdict |
|---|---|---|---|---|
| H1 | 같은 시간 ap-southeast-2 / tenant cupix production DB 의 panos 테이블 InnoDB row-lock 경합으로 MaskableRepository#check_mask_uploading 내부의 @model.save! / @model.uploaded_mask_state! 가 50s lock-wait 에 묶이고, state machine 재시도 sleep 까지 누적되어 66~159s 지연 후 200 으로 종료. |
(a) 동일 윈도우 같은 엔드포인트에서 50087ms 502 LockWaitTimeout 1건 (10:53:29 KST). (b) 인접 시간대 check_tile_uploading/mask_upload_url 등 panos 쓰기 경로에서 다수 50087ms 502. (c) status board 가 본 cluster 를 동일 parent incident 의 다른 6 cluster 와 묶음. (d) duration 분포 (66.6~159.8s) 가 50s × N + ≤15s sleep 산술 범위에 부합. |
— | Confirmed (외부 기여 원인: DB row-lock 경합) |
| H2 | mask.mask_uploaded? 가 호출하는 S3 HEAD (Aws::S3::Object#exists?) 자체의 ap-southeast-2 S3 latency 가 원인. |
— | (a) 같은 시간대 같은 region 의 다른 엔드포인트 (DB write 없는 endpoint 도) 가 동일하게 늘어진 흔적이 없고, panos 쓰기 경로에 집중됨. (b) parent incident sibling RCA 가 동일 시간대 50s 정확 매칭으로 DB lock 경합을 확정. (c) HEAD 호출 자체로 50/100s 단위 지연은 비전형적. | Rejected |
| H3 | MaskableRepository#check_mask_uploading 의 StateMachines::InvalidTransition 재시도 로직 (sleep(retries), 1+2+3+4+5=15s) 이 단독으로 본 cluster 의 지연을 만들었다. |
코드상 최대 15s sleep budget 존재. | 15s 만으로는 66~159s 지연을 설명 불가. lock-wait 와 결합되어야만 산술이 맞는다. (단, retry 가 latency 를 증폭시키는 보조 요인은 맞음) | Rejected (단독 원인으로는) |
| H4 | 클라이언트의 동일 mask 에 대한 동시 호출 race 로 같은 panos row 를 본인이 lock 하고 있는 경우. | mask 업로드 후처리 흐름상 같은 pano 에 대해 짧은 시간 내 중복 호출 가능. | sibling RCA 에서 panos 테이블 광범위 경합 (다수 pano_id 에 걸쳐) 확인 — 본 4 trace 만의 self-contention 으로 설명 불가. parent incident 의 일부로 해석하는 것이 정합. |
Rejected (보조 가능성은 있으나 본 원인 아님) |
| H5 | AWS / EBS / RDS instance 장애 (외부 의존성) | — | status board 가 dep:* outage 가 아닌 svc:cupixworks-api::unknown 으로 분류. AWS Health 등 외부 outage 확인은 본 RCA 범위 밖이나, sibling RCA 에서 DB row-lock 패턴을 확정함. |
Rejected |
Fix Recommendation#
즉시 조치 (Critical)#
- 본 cluster 는 단독 코드 결함이 아니라 parent incident
2026-06-26-svc-cupixworks-api--unknown-1의 부수 피해다. 코드 변경 불필요. 같은 parent incident 의 핵심 cluster (Api::V1::PanosController#update943fdcb4,check_tile_uploadingLockWaitTimeout cluster 등) 의 lock 경합 원인 분석에 합류. - DB lock 경합의 직접 원인 추적 지점:
Api::V1::PanosController#update,#check_tile_uploading,#check_mask_uploading,#mask_upload_url의 transaction 경계 (PanoRepository,MaskableRepository,TilableRepository).MaskableRepository#check_mask_uploading의@model.save!+@model.uploaded_mask_state!가 단일 transaction 내에 다중 panos UPDATE 를 묶고 있는지 확인 (app/repositories/concerns/maskable_repository.rb:17,28).- 같은 capture/cluster 의 mask/tile upload 후처리가 동시에 다발하면서 같은 row 의 lock 을 잡는 패턴 확인.
단기 개선 (1주 이내)#
MaskableRepository#check_mask_uploading의 state machine 재시도 sleep 정책 (sleep(retries)최대 15s) 을 lock 경합 시나리오까지 가산되는 것을 고려하여 짧은 fail-fast 모드 (예: 총 sleep budget 3s, retry 2회) 로 조정 검토. lock 경합 시 다음 50s timeout 까지 같은 요청이 점유하지 않게 해 사용자 인지 latency 와 DB connection 점유 시간을 모두 줄임.check_mask_uploading의 write 경로를 짧은 transaction 으로 분리:@model.save!의 UPDATE 컬럼 범위를 축소 (mask_id,current_mask_url만 갱신하는 partial update), state 전이도 별도 transaction.- ap-southeast-2 production DB 의 connection pool 가용성 /
pool_waitDatadog dashboard 추가 — lock 경합 시 connection 회수 지연 모니터링.
장기 개선 (재발 방지)#
- panos write 경로(
update,check_*_uploading,*_upload_url) 의 transaction 범위 축소: 단일 transaction 안에 다중 row UPDATE / 외부 호출 (S3 HEAD 등) 이 혼재되어 있다면 분리. innodb_lock_wait_timeout단축 (현재 50s 추정) — read-heavy API 경로에 영향이 크므로 30s 이하로 검토하고, 동시에 long-running write 를 줄이는 작업과 병행. (sibling RCA 와 동일 권고)- 동일 pano 에 대한 mask/tile 후처리 호출의 client-side 직렬화 또는 server-side advisory lock (예: Redis lock) 으로 같은 pano row 의 동시 UPDATE 충돌 감소.
- region 별 endpoint p99 trace duration 알림 (현재 ap-southeast-2 만 튀는 패턴 조기 감지).
Monitoring#
avg:trace.rack.request.duration{service:cupixworks-api,env:production,resource_name:api::v1::panoscontroller#check_mask_uploading}
max:trace.rack.request.duration{service:cupixworks-api,env:production,resource_name:api::v1::panoscontroller#check_mask_uploading}
sum:trace.rack.request.duration.by_resource_service.errors{service:cupixworks-api,env:production,region:ap-southeast-2}.as_count()
sum:mysql.innodb.row_lock_waits{service:cupixworks-api,env:production,region:ap-southeast-2}.as_rate()
max:mysql.innodb.row_lock_time{service:cupixworks-api,env:production,region:ap-southeast-2}
추가 권장:
ActiveRecord::LockWaitTimeoutDatadog Logs 기반 metric (region / resource_name 별):textlogs("service:cupixworks-api @error.class:ActiveRecord::LockWaitTimeout env:production").index("*").rollup("count").by("region","resource_name")panos쓰기 4 endpoint (update,check_tile_uploading,check_mask_uploading,mask_upload_url) 의 p99 latency 를 동일 timeseries widget 에 묶어 동시 spike 패턴 감지.
Risk Assessment#
- Risk level: medium — 4건의 trace 자체 영향은 한정적이나, 같은 parent incident 의 6 cluster (모두 ap-southeast-2/tenant cupix) 와 동시에 발생한 DB 광범위 영향의 일부.
- 예상 복잡도: standard — 코드 결함은 없고, parent incident (panos write 경로 lock 분석) 와 연계 작업 필요. retry-sleep 정책 조정은 trivial, transaction 범위 축소는 standard.