ES /docs

Cupix::Errors::System: Mysql2::Error::TimeoutError: Lock wait timeout exceeded; try restarting transaction

RCA: Mysql2::Error::TimeoutError: Lock wait timeout exceeded

Overview#

What Happened#

cupixworks-api (tesla) 의 다수 write 엔드포인트(주로 PUT /api/v1/element_traces bulk)에서 MySQL InnoDB Lock wait timeout exceeded 가 발생한다. 요청은 약 50초(InnoDB innodb_lock_wait_timeout 기본값)동안 row lock 을 기다린 뒤 ActiveRecord::LockWaitTimeout 을 던지고 HTTP 502 (BG10000) 로 매핑된다. Sitetrack post-processing 이 같은 facility 의 ElementTrace row 를 동시에 대량 갱신하면서 자기 자신과 lock 경합을 일으키는 것이 주 원인이다. 16개월간 38건으로 저빈도이며 대부분 qa/regression 테스트 트래픽이다.

Quick Facts#

Field Value
exception.class ActiveRecord::LockWaitTimeout (representative 는 Cupix::Errors::System)
exception.message Mysql2::Error::TimeoutError: Lock wait timeout exceeded; try restarting transaction
top_frame app/controllers/concerns/server_error_controller.rb:18 (502 mapping)
runtime Rails / ActiveRecord 7.2.2, MySQL (InnoDB)
env qa (dominant), us-west-2

Affected Teams#

Team / Domain Error Count Impact
algtest (qa, Sitetrack regression) 대부분 element_traces bulk 갱신이 502 로 실패 → postprocessor agent 재시도
기타 (panos/jobs/captures update 등) 소수 산발적 write 요청 502

Timeline#

  1. 2025-04-13 09:17 KST — 최초 발생 (first_seen).
  2. 2026-07-30 ~ 08-04element_traces#bulk 502 burst 반복 (facility qinlxf, Sitetrack 처리 중).
  3. 2026-08-03 15:02 KST — 최근 발생 (last_seen, cluster 기준).
  4. 2026-08-04 13:40~13:46 KST — retention 창 내 최신 burst 관측 (요청당 50s db, 45건 연속).

Error Log#

Datadog Logs

text
Mysql2::Error::TimeoutError: Lock wait timeout exceeded; try restarting transaction

Impact#

  • Service: cupixworks-api
  • 발생 횟수: 38
  • 최초 발생: 2025-04-13 09:17 KST
  • 최근 발생: 2026-08-03 15:02 KST

Root Cause Summary#

ActiveRecord::LockWaitTimeout 은 InnoDB row lock 경합이 innodb_lock_wait_timeout(~50s)을 초과할 때 발생한다. Sitetrack post-processing 이 활성화된 facility 에서 cupix-sitetrack-postprocessor-agentPUT /api/v1/element_traces (bulk create/update/delete) 를 한 transaction 으로 실행하는 동시에, 같은 처리 파이프라인의 background 경로(Cupix::EditingSplitServiceElementTrace.update_all(editing_id:), TaskSyncWorker 등)가 동일한 ElementTrace row 를 갱신한다. 두 write 가 같은 row 를 서로 다른 순서로 잠그면서 bulk transaction 이 50초를 대기하다 timeout → server_error_controller.rb:18 에서 502 (BG10000) 로 매핑된다. 이 502 매핑은 팀이 의도적으로 만든 transient 처리(503 그룹에서 502 그룹으로 명시적으로 이동, :32 주석 참조)이지만, synchronous bulk 경로에는 다른 write 경로들이 이미 갖춘 lock-wait retry 가 빠져 있어 경합이 그대로 502 로 표면화된다. 근본 결함은 downstream 장애가 아니라 tesla 내부의 self-contention 과 retry 누락이다.

Technical Analysis#

Code Path#

  • Entry point: app/controllers/api/v1/element_traces_controller.rb:6 (include BulkableController) → bulk
  • Bulk 실행: app/controllers/concerns/bulkable_controller.rb:25-45repository_instance.bulk(...) / factory_instance.bulk(...) 를 transaction 내에서 실행
app/controllers/concerns/bulkable_controller.rb:25-35ruby
  def bulk
    _bulked_ids = []
    _invalid_items = []
    case params[:bulk_action]
    when 'create'
      _bulked_ids, _invalid_items = factory_instance.bulk(params, current_user: @current_user, current_team: @current_team, partial_mode: true)
    when 'update', 'delete'
      _bulked_ids, _invalid_items = repository_instance.bulk(params, current_user: @current_user, current_team: @current_team, partial_mode: true)
  • Lock 경합 상대(background writer): app/services/cupix/editing_split_service.rb:769-784 — 같은 facility 의 ElementTrace row 를 update_all 로 대량 갱신. 주석 자체가 LockWaitTimeout 을 피하려 batch 처리하고 있음을 명시.
app/services/cupix/editing_split_service.rb:769-776ruby
      # Update ElementTrace and Element editing_id (batched to avoid LockWaitTimeout)
      et_ids = ::ElementTrace.where(task_id: task_ids, editing_id: editing.id).pluck(:id)
      et_affected_total = 0
      if et_ids.any?
        et_ids.each_slice(REASSIGN_BATCH_SIZE) do |batch|
          et_affected_total += ::ElementTrace.where(id: batch).update_all(editing_id: new_editing.id)
        end
      end
  • Failure point → 502 mapping: app/controllers/concerns/server_error_controller.rb:18-20. ActiveRecord::LockWaitTimeout 은 팀이 503 그룹(:32 에 주석 처리된 라인)에서 명시적으로 502 그룹으로 이동시킨 상태다.
app/controllers/concerns/server_error_controller.rb:18-20ruby
    rescue_from ActiveRecord::LockWaitTimeout,
                Errno::ENOMEM,
                RuntimeError, with: :badgateway_on_system_502_error
app/controllers/concerns/server_error_controller.rb:54-56ruby
    def badgateway_on_system_502_error(exception)
      raise_error(502, exception, code: 'BG10000', type: Cupix::Errors::BadGateway, reason: 'BadGateway', message: exception.message)
    end
  • 대조: background write 경로는 이미 lock-wait retry 를 갖고 있다 — synchronous bulk 경로에만 누락.
app/models/concerns/finalization/editing_entity.rb:16-28ruby
      def assign_editing_to_editing_entity
        retries = 0
        begin
          _do_assign_editing_to_editing_entity
        rescue ActiveRecord::LockWaitTimeout, ActiveRecord::Deadlocked => e
          retries += 1
          if retries <= LOCK_RETRY_MAX_ATTEMPTS
            delay = LOCK_RETRY_BASE_DELAY * (2**(retries - 1))
            ...
            sleep(delay)
            retry
app/workers/task_sync_worker.rb:53-61ruby
    rescue ActiveRecord::LockWaitTimeout => e
      if retries < LOCK_WAIT_MAX_RETRIES
        ...
        sleep(2**retries)
        retries += 1
        retry
      else
        raise e
      end
  • 기대 동작: 일시적 lock 경합은 재시도로 흡수되어야 한다(worker 경로처럼).
  • 실제 동작: element_traces#bulk transaction 은 재시도 없이 50초 대기 후 그대로 502 로 실패하고, postprocessor agent 가 재요청하면서 burst 로 반복된다.

Log Evidence#

Datadog query (retention 창 내):

text
service:cupixworks-api "Lock wait timeout exceeded"

status code / error class 분포 (now-14d, 50건 표본):

text
[502] 41  (class ActiveRecord::LockWaitTimeout)
[400]  7  (class Cupix::Errors::Parameter — "Lock wait timeout" 문자열만 포함, 별개)
[500]  2  (class Cupix::Errors::System)

엔드포인트 분포 (502 건):

text
17  PUT /api/v1/element_traces (Api::V1::ElementTracesController#bulk)
 8  PUT /api/v1/panos/{id} (PanosController#update)
 7  PUT /api/v1/jobs/{id} (JobsController#update)
 3  PUT /api/v1/panos/{id}/check_tile_uploading
 3  PUT /api/v1/panos/{id}/check_mask_uploading
 3  PUT /api/v1/captures/{id} (CapturesController#update)
 ... (levels/create, admin/editings/trash, assets/update 등 소수)

대표 로그 원문 (element_traces bulk, ~50s DB 대기 후 502):

json
{
  "message": "[502] PUT /api/v1/element_traces (Api::V1::ElementTracesController#bulk)",
  "error": {
    "message": "Mysql2::Error::TimeoutError: Lock wait timeout exceeded; try restarting transaction",
    "class": "ActiveRecord::LockWaitTimeout"
  },
  "duration": 50238.51,
  "db": 50166.15,
  "controller": "Api::V1::ElementTracesController",
  "action": "bulk",
  "user_agent": "cupix-sitetrack-postprocessor-agent",
  "team": { "domain": "algtest", "id": 3 },
  "params": { "facility_key": "qinlxf" },
  "environment": "qa",
  "http": { "status_code": 502, "method": "PUT" }
}

burst 타임스탬프 (element_traces, facility qinlxf, KST):

text
2026-08-04 13:40:45, 13:41:33, 13:45:06, 13:45:54, 13:46:54
2026-08-03 17:43:45 ~ 17:46:23 (4연속)
2026-07-31 16:54:47 ~ 16:57:23 (4연속)

→ 각 요청 ~50s db, 같은 facility 에서 연속 재시도되는 burst 패턴.

동일 facility 에서 Sitetrack 처리가 활성이었음 (같은 window):

text
Sitetrack(ID: 1272) processing started/completed with Capture ID 8587 (facility qinlxf, algtest/regression)

Hypotheses Considered#

# Hypothesis Evidence for Evidence against Verdict
H1 Sitetrack post-processing 의 동시 ElementTrace write(bulk vs editing_split/task_sync)가 self-contention → 50s lock timeout, synchronous bulk 경로에 retry 누락 duration/db ~50166ms = innodb_lock_wait_timeout; element_traces#bulk UA cupix-sitetrack-postprocessor-agent; editing_split_service.rb:769 ElementTrace.update_all; 같은 facility qinlxf Sitetrack "processing" 로그 동시성; worker 경로엔 retry(finalization/editing_entity.rb:20, task_sync_worker.rb:53) 존재하나 controller bulk 엔 없음 Confirmed
H2 외부 dependency / DB 엔진 장애 (503 성격) status-board svc:cupixworks-api::unknown active 없음; 팀이 LockWaitTimeout 을 503 그룹에서 502 transient 로 의도적으로 이동(server_error_controller.rb:18,32); DB 자체는 정상, 특정 row 경합만 Rejected
H3 Representative Cupix::Errors::System (500) 가 현재 지배적 발생 retention 창 표본 2건만 Cupix::Errors::System(500) 41/50 이 ActiveRecord::LockWaitTimeout(502); 현재 지배 경로는 502. Representative 는 STALE — ET 가 "Lock wait timeout" 메시지의 여러 클래스 변형을 한 이슈로 묶음 Rejected (stale)
H4 클라이언트 입력 오류로 인한 4xx (noise) [400] Cupix::Errors::Parameter 7건 존재 이들은 error.message 에 "Lock wait timeout" 문자열이 우연히 포함된 별개 param 검증 실패로, 지배 502 경로와 무관 Rejected

Representative 불일치 note: cluster representative 는 Cupix::Errors::System 이지만 retention 창의 실제 지배 발생은 ActiveRecord::LockWaitTimeout502 (41/50) 이다. 최신 발생 기준으로 502 경로를 root cause 로 분석했다.

Fix Recommendation#

즉시 조치 (Critical)#

  • 없음(장애 아님). LockWaitTimeout → 502 매핑은 의도된 transient 처리이며 사용자 데이터 손상/영구 실패는 없다.

단기 개선 (1주 이내)#

  • element_traces#bulk(및 BulkableController#bulk / BulkableRepository#bulk) 의 write transaction 에 기존 in-repo lock-wait retry 패턴을 재사용하여 감싼다. 새로 발명하지 말 것 — app/workers/task_sync_worker.rb:41-63create_tasks_with_lock_wait_retry 또는 app/models/concerns/finalization/editing_entity.rb:16-36LOCK_RETRY_MAX_ATTEMPTS=3 + exp backoff 를 그대로 차용. bulk 는 멱등이 아닐 수 있으므로 partial_mode 재시도 시 이미 커밋된 항목 중복 방지를 확인.
  • postprocessor agent 파이프라인에서 같은 facility 의 element_trace 대량 write 를 직렬화(advisory lock / 큐 단일화)하여 self-contention 원천 감소를 검토. editing_split_service 는 이미 REASSIGN_BATCH_SIZE batch 로 lock 보유 시간을 줄이고 있으므로 bulk 쪽도 batch 크기 / 트랜잭션 범위 축소를 검토.

장기 개선 (재발 방지)#

  • InnoDB row lock 순서를 통일(항상 id 오름차순 lock)하여 경합 자체를 줄인다.
  • LockWaitTimeout 재시도 로직을 공통 concern(with_lock_wait_retry) 으로 추출해 synchronous / worker 경로가 동일 구현을 공유하도록 통일.

Monitoring#

  • element_traces bulk 502 발생 추이:
text
sum:trace.rack.request.errors{service:cupixworks-api,resource_name:put_/api/v1/element_traces}.as_count()
  • api 전체 502 응답 추이(경합 급증 감지):
text
sum:trace.rack.request.hits{service:cupixworks-api,http.status_code:502}.as_count()

Risk Assessment#

  • Risk level: low
  • 예상 복잡도: standard

Noise Verdict#

bug — 팀이 이미 worker 경로(task_sync_worker.rb:53, finalization/editing_entity.rb:20)에서 채택한 lock-wait retry 가 synchronous element_traces#bulk 경로에만 누락되어 self-contention 이 502 로 표면화되는 실제 resilience 결함이므로 코드 수정(기존 retry 패턴 재사용)이 필요하다.