ES /docs

Api::V1::BimsController#create_resource (avg 111692ms, max 111692ms)

RCA: Api::V1::BimsController#create_resource 111.7s latency

Overview#

What Happened#

2026-07-10 16:49–16:51 KST 사이 cupixworks-api (ap-southeast-2)의 Api::V1::BimsController#create_resource 요청 1건이 111.5초 소요된 뒤 [400] Duplicate kind: mesh 로 응답됨. 같은 시간대에 3건의 ActiveRecord::LockWaitTimeout (Mysql2::Error::TimeoutError, 50s) 502 응답과 19건의 60s 초과 write 요청이 관측된 광범위 RDS InnoDB row-lock contention 이벤트의 일부. 대상 요청은 락 큐에 갇혀 있다가 락 해제 후 실행되었고, 그 사이 동일 세션의 선행 요청이 이미 mesh kind Resource 를 생성해 두어 중복 검증에 걸림.

Quick Facts#

Field Value
exception.class Cupix::Errors::Parameter (ARG10001) — 응답 시점
exception.message Duplicate kind: mesh
top_frame app/controllers/concerns/multiple_resourcable_controller.rb:53 (@model.resources.find_by_kind(...) 중복 검사)
runtime Rails / Ruby (tesla monolith), user_agent cupix-agent
deploy production-ap-southeast-2-20260709t0654z0-6eb3f711-cupixworks
env production, region ap-southeast-2
trace_id 103694830734669946

Affected Teams#

Team / Domain Error Count Impact
cupix (team 6, user 8 yohan.kim@cupix.com, session f66c6083…) 1 (target cluster) Bim 3566 에 mesh resource 생성 요청이 111.5s 뒤 400 반환 — cupix-agent 자동화 파이프라인 지연
built (team 16) 3× 502 + 다수 slow request 동일 시간대 Capture#81421 및 자식 리소스에 대한 write 가 50s LockWaitTimeout / 60s+ 지연

Timeline#

  1. 2026-07-10 16:47:04 KST — 평시: 동일 세션이 GET/PUT /api/v1/bims/3566 정상 응답.
  2. 2026-07-10 16:47:34–16:47:52 KSTBimAiOperation#get 에서 404 no current extraction for bim 7400 8건 발생 (환경은 dev, region us-west-2 — 이 클러스터와 직접 관련 없음, 인과 관계 확인되지 않음).
  3. 2026-07-10 16:48:47 KST — 첫 이상 조짐: capture 81421 update 가 39.8s(db 33.4s)로 지연.
  4. 2026-07-10 16:49:35 KST — 대상 클러스터 이벤트 시작 (first_seen). 동일 세션에서 POST /api/v1/bims/3566/resources (kind=mesh, name=mesh.zip) 시작. 이 시점 이후 요청은 락 대기 큐에 진입.
  5. 2026-07-10 16:50:34 KST — 선행/병렬 요청 POST /api/v1/bims/3566/resources 가 먼저 200 으로 완료되며 mesh kind Resource row 를 생성. 같은 초에 PUT /bims/3566, check_uploading, resource_upload_credentials 도 완료.
  6. 2026-07-10 16:51:16–16:51:20 KSTbuilt 테넌트의 세 write 요청이 정확히 50s 만에 Mysql2::Error::TimeoutError 로 502 반환.
  7. 2026-07-10 16:51:27.210 KST — 락이 해제되면서 대상 요청이 111,502ms 뒤 find_by_kind(:mesh) 검사에 도달, 이미 생성된 mesh row 를 발견하고 Cupix::Errors::Parameter(ARG10001, "Duplicate kind: mesh") 로 400 반환. 같은 초에 sibling 클러스터 요청들(AssetsController#create 125.5s, RoomsController#create 49.7s) 도 flush.
  8. 2026-07-10 16:51:32 KST — capture 81421 update 가 810ms 수준으로 회복 → 인시던트 해소.

Error Log#

Datadog Logs

Representative span (from cluster file)text
{
  "resource_name": "Api::V1::BimsController#create_resource",
  "service": "cupixworks-api",
  "occurrences": 1,
  "avg_ms": 111692,
  "max_ms": 111692,
  "sample_trace_id": "103694830734669946"
}

Impact#

  • Service: cupixworks-api
  • 발생 횟수: 1
  • 최초 발생: 2026-07-10 16:49 KST
  • 최근 발생: 2026-07-10 16:49 KST

Sibling 클러스터(c25d10f8-… AssetsController#create 125.5s, ca6ef0b7-… RoomsController#create 49.7s, 5a1789e2-…)와 함께 status board 인시던트 2026-07-10-svc-cupixworks-api--unknown-2 (resolved, 2026-07-10T07:40:16Z–07:49:35Z) 로 묶여 있음. 동일 원인의 recent 인시던트 6건(2026-07-03 ~ 2026-07-10) 이 svc:cupixworks-api::unknown scope 로 존재 → 재발 패턴. 실제 사용자 영향은 이 단일 클러스터보다 훨씬 넓음 (같은 창에서 60s+ 지연 요청 19건, 502 응답 3건).

Root Cause Summary#

RDS MySQL (ap-southeast-2) 에서 다중 write 엔드포인트가 동일 부모 row(특히 built 테넌트의 Capture#81421 및 자식 Pano/Asset) 를 대상으로 동시에 UPDATE 를 시도하면서 InnoDB row-lock 대기열이 쌓임 — sibling AssetsController#create RCA 에서 이미 확정된 근본 원인. 이 클러스터의 요청(POST /bims/3566/resources) 은 그 lock contention 큐의 후미에 걸려 82초를 락 대기, 29.5초를 실제 SQL 실행에 소비 (duration=111502.69ms, db=29548.64ms). 대기 도중 동일 세션의 다른 POST /bims/3566/resources (또는 선행 요청) 가 먼저 커밋되어 mesh kind Resource row 가 생성되었고, 대상 요청이 락 획득 후 진입한 MultipleResourcableController#create_resource:53find_by_kind 중복 검사에서 이미 존재하는 row 를 발견해 Cupix::Errors::Parameter(ARG10001) 로 400 을 반환. 즉 latency 자체는 DB lock contention, 400 응답 자체는 client 측 (또는 세션 내부의) 중복/재시도 POST 로 이해할 수 있음.

Technical Analysis#

Code Path#

Entry point: app/controllers/api/v1/bims_controller.rbApi::V1::BimsControllerMultipleResourcableController concern 을 include, 라우팅 POST /api/v1/bims/:id/resourcescreate_resource 액션으로 매핑됨.

config/routes.rb:291-295ruby
scope 'resources' do
  get '', action: :index_resource
  post '', action: :create_resource
  get ':kind_or_key', action: :show_resource
  delete ':kind_or_key', action: :destroy_resource

Before-action 체인: bims_controller.rb:9before_action :set_bim (create_resource 는 제외되지 않음) → set_bimBimRepository#show(params[:id]) 로 Bim(3566) 을 로드.

app/controllers/api/v1/bims_controller.rb:100-102ruby
def set_bim
  @model = repository_instance.show(params[:id])
end

추가로 MultipleResourcableController.included do ... before_action :check_kind, only: [:create_resource]set_resource_serializer_option 이 실행됨.

app/controllers/concerns/multiple_resourcable_controller.rb:4-10ruby
included do
  include CyclableController

  before_action :set_multiple_resource, only: %i[show_resource download_multiple_resource destroy_resource resource_upload_url resource_upload_credentials check_resource_uploading]
  before_action :check_kind, only: [:create_resource]
  before_action :set_resource_serializer_option, only: %i[index_resource show_resource create_resource check_resource_uploading resource_upload_url]
end

Failure(대기) point: app/controllers/concerns/multiple_resourcable_controller.rb:52-63 — 중복 검사 후 resource.save 를 호출. 이 트랜잭션이 resources INSERT 와 함께 bims (updated_at touch) 및 facilities (counter_culture: bims_count) 등 부모 row 를 갱신하며 락을 획득해야 함. 동일 시간대 다른 write 트랜잭션이 부모 row 를 이미 잡고 있으면 최대 innodb_lock_wait_timeout 초까지 대기.

app/controllers/concerns/multiple_resourcable_controller.rb:52-78ruby
def create_resource
  raise Cupix::Errors::Parameter.new(code: 'ARG10001', reason: "Duplicate kind: #{params[:kind]}") and return unless @model.resources.find_by_kind(params[:kind]).blank?

  resource = @model.resources.new kind: params[:kind],
                                  user: current_user,
                                  team: @model.team,
                                  name: params[:name]

  begin
    unless resource.save
      raise Cupix::Errors::Parameter.new(code: 'ENT10003', reason: resource.errors.full_messages)
    end
  rescue Cupix::Errors::Parameter => e
    raise e
  else
    render_api Renderable.new({
      contents: resource,
      serializer: ResourceSerializer,
      serializer_option: {
        fields: {
          resource: @fields
        },
        is_collection: false
      }
    })
  end
end

주목할 점: find_by_kind 검사 자체가 락을 잡지는 않지만, set_bim 등의 SELECT 쿼리가 트랜잭션에 참여하거나 다른 요청과 동일 튜플에 대한 shared lock 을 기다리며 큐잉될 수 있음.

중복 응답 원인 (400 Duplicate kind): 대기 도중 다른 요청이 먼저 mesh kind Resource 를 생성했음을 로그로 확인.

POST /api/v1/bims/3566/resources 순서text
07:50:34 UTC   200  duration=127.15 ms  db=- ms         (선행 요청, mesh row 생성)
07:51:27 UTC   400  duration=111,502 ms db=29,548 ms    (대상 요청, "Duplicate kind: mesh")

두 요청은 동일 session_id: f66c60830f9f328a00169df8fc54eb26cf9d9565, 동일 user id:8(cupix-agent), 동일 params name: mesh.zip, id: 3566. cupix-agent (자동화 클라이언트) 가 첫 요청 응답 지연 중 재시도했거나 병렬로 두 번 요청한 것으로 추정 — 로그에는 재시도 로직 자체가 남지 않아 uncertain.

Bim 모델의 락 확산 경로: app/models/bim.rb:43-46

app/models/bim.rb:29-46ruby
include EntityUpdates::Child

belongs_to :user, optional: true
belongs_to :team, optional: true
belongs_to :workspace, optional: true
belongs_to :facility, optional: true
belongs_to :last_bim_revision, optional: true, class_name: 'BimRevision'

counter_culture :facility,
                column_name: proc { |model| model.untrashed? ? 'bims_count' : nil },
                column_names: { ::Bim.untrashed => :bims_count }
  • Resource INSERT 트랜잭션이 bims.updated_at touch 를 유발 → facilities.bims_count counter_culture 도 트리거 가능.
  • EntityUpdates::Child 는 부모(Facility) cached_entity_updates reset 을 실행 (16:50:36 로그: reset Facility (ID: 4189) cached entity updates).
  • 동시에 진행되는 built 테넌트 요청들이 서로 다른 facility 라도 shared indexes/pages 및 innodb metadata lock 을 통해 wait queue 를 형성할 수 있음.

Log Evidence#

사용한 Datadog 쿼리 (시간 범위 2026-07-10T07:40:00Z – 2026-07-10T07:55:00Z):

text
service:cupixworks-api "Api::V1::BimsController#create_resource" env:production
service:cupixworks-api env:production region:ap-southeast-2 "bims/3566"
service:cupixworks-api env:production "Lock wait timeout"
service:cupixworks-api env:production status:error
service:cupixworks-api status:error

대상 요청 원문 (POST /api/v1/bims/3566/resources, 400):

target 111.5s request — Api::V1::BimsController#create_resourcejson
{
  "@timestamp": "2026-07-10T07:51:27.210Z",
  "controller": "Api::V1::BimsController",
  "action": "create_resource",
  "duration": 111502.69,
  "db": 29548.64,
  "view": 0.17,
  "http": { "status_code": 400, "method": "POST", "url_details": { "path": "/api/v1/bims/3566/resources" } },
  "error": {
    "reason": "Duplicate kind: mesh",
    "code": "ARG10001",
    "message": "Duplicate kind: mesh",
    "class": "Cupix::Errors::Parameter"
  },
  "params": { "name": "mesh.zip", "id": "3566", "fields": ["key","name","kind","size","state","meta","created_at","updated_at","upload_url"] },
  "tenant": "cupix",
  "team": { "domain": "cupix", "id": 6 },
  "user": { "id": 8, "email": "yohan.kim@cupix.com", "firstname": "Cupix", "lastname": "Support" },
  "user_agent": "cupix-agent",
  "session_id": "f66c60830f9f328a00169df8fc54eb26cf9d9565",
  "remote_ip": "54.253.237.17",
  "host": "ip-10-1-147-96.ap-southeast-2.compute.internal",
  "region": "ap-southeast-2",
  "request_id": "98ccc4df-9e46-4381-9cea-8e009b60c6c5"
}
  • duration - db = 111502.69 - 29548.64 = ~81,954 ms — 이 시간은 실제 SQL 실행에는 포함되지 않았음. 락 대기, 이후 Ruby-side 렌더/트랜잭션 rollback, 또는 트랜잭션 밖 처리 중 어딘가에서 소비되었으나 로그의 단일 필드로 세분화는 불가 (uncertain — needs verification via APM span).

Bim 3566 관련 시간대 요청 (asc 정렬):

bims/3566 request timelinetext
07:47:04 UTC   200  GET  /api/v1/bims/3566                           (평시)
07:47:04 UTC   200  PUT  /api/v1/bims/3566
07:48:28 UTC   200  GET  /api/v1/bims/3566
07:48:30 UTC   200  PUT  /api/v1/bims/3566
07:48:47 UTC   200  GET  /api/v1/bims/3566
07:48:47 UTC   200  PUT  /api/v1/bims/3566
07:50:14 UTC   200  POST /api/v1/bims/3566/grid_system_upload_url
07:50:14 UTC   200  PUT  /api/v1/bims/3566/check_grid_system_uploading
07:50:14 UTC   200  POST /api/v1/rooms                                (같은 flush wave)
07:50:14 UTC   200  PUT  /api/v1/bims/3566/meta
07:50:14 UTC   200  PUT  /api/v1/bims/3566
07:50:34 UTC   200  POST /api/v1/bims/3566/resources                  (선행 mesh 생성)
07:50:34 UTC   200  POST /api/v1/bims/3566/resources/mesh/upload_credentials
07:50:34 UTC   200  PUT  /api/v1/bims/3566/resources/mesh/check_uploading
07:50:34 UTC   200  PUT  /api/v1/bims/3566
07:51:27 UTC   400  POST /api/v1/bims/3566/resources                  (target 111.5s, Duplicate kind: mesh)

같은 시간대 502 LockWaitTimeout (production, ap-southeast-2):

ActiveRecord::LockWaitTimeout — 3× at 07:51:16–07:51:20 UTCtext
07:51:16 UTC   [502] PUT /api/v1/assets/ze125xzhp1dz            (AssetsController#update)
07:51:18 UTC   [502] PUT /api/v1/panos/14606467/check_tile_uploading
07:51:20 UTC   [502] PUT /api/v1/captures/81421                 (CapturesController#update)
  • 각 요청의 error: Mysql2::Error::TimeoutError: Lock wait timeout exceeded; try restarting transaction (ActiveRecord::LockWaitTimeout).
  • db 값이 정확히 ~50,180 ms → innodb_lock_wait_timeout=50s 기본값과 일치.
  • 이 세 요청은 모두 built 테넌트(team 16), user_agent cupix-agent. 대상 클러스터 요청과 동일 host ip-10-1-147-96.ap-southeast-2.compute.internal 에서 처리된 것들도 포함 — DB(공유 자원) 문제이지 특정 앱 인스턴스 이슈가 아님.

Status board 인시던트 (자동 감지된 클러스터 그룹):

incident-board.ts for-cluster c9b5537f-…text
id:             2026-07-10-svc-cupixworks-api--unknown-2
scope:          svc:cupixworks-api::unknown
status:         resolved
started_at:     2026-07-10T07:40:16.818Z
resolved_at:    2026-07-10T07:49:35.295Z
cluster_ids:    5a1789e2-…, ca6ef0b7-…, c25d10f8-…, c9b5537f-… (this)

동일 scope 로 최근 7일간 6회 재발 (2026-07-03, 07-06, 07-08×2, 07-10×2).

Hypotheses Considered#

# Hypothesis Evidence for Evidence against Verdict
H1 RDS InnoDB row-lock contention 큐로 대상 요청이 ~82s 대기, 이후 실행된 트랜잭션이 29.5s 소요. 대기 도중 동일 세션의 선행 요청이 mesh Resource 를 먼저 커밋해 중복 검증 실패 ActiveRecord::LockWaitTimeout (정확히 50s, db≈50,180 ms) 이 같은 창에서 502; 대상 요청 db=29548.64 ms, duration=111502.69 ms; sibling AssetsController#create RCA(c25d10f8-…) 에서 동일 원인 확정; 07:50:34 UTC 선행 POST /bims/3566/resources 200 로그 확인 Confirmed
H2 클라이언트(cupix-agent) 가 idempotent 하지 않게 재시도해 400 을 유도한 것이 근본 원인 두 요청이 동일 session_id/user/params 로 91초 간격; 첫 응답이 느려 재시도되었을 가능성 지연 자체는 클라이언트 재시도로 설명되지 않음(대기 82s + DB 29.5s 는 DB lock contention 신호); 재시도 로직 로그는 남지 않아 클라이언트가 병렬 전송했을 수도 있음 Rejected (main cause 아님, 400 응답 자체의 파생 원인)
H3 애플리케이션 코드의 명시적 with_lock/SELECT ... FOR UPDATE 가 원인 코드 조사 결과 resource, bim 모델과 create_resource 경로에 명시적 락 없음; contention 이 여러 서로 다른 controller(Bim, Asset, Pano, Capture)·다른 테넌트에 걸쳐 광범위 → 단일 코드경로 락으로는 설명 불가 Rejected
H4 외부 서비스(BimAi, Forge) 호출 대기가 원인 07:47:34–07:47:52 UTC 에 BimAi get failed: 404 Not Found 다수 발생 해당 로그의 environment: dev, region: us-west-2 — 이 프로덕션 클러스터와 다른 환경. create_resource 경로에는 BimAi/Forge 호출이 없음 Rejected
H5 Datadog Rails middleware / 로깅 지연 duration=111502.69, db=29548.64 는 log_type=request 로 실 응답 시간이며, 인접한 502 응답의 db 값과 innodb_lock_wait_timeout(50s) 이 정확히 일치 → 실제 DB 대기 신호 Rejected
H6 이 sibling incident 는 status board 상 svc:cupixworks-api::unknown scope 이지만 실질적으로 dep:rds-mysql-ap-southeast-2 에 가까움 recent 인시던트 6건이 root_cause_types: [unknown] 로 분류되나 실제로는 lock contention 확정 Inconclusive — needs verification (스코프 분류 로직 재검토 필요)

Fix Recommendation#

즉시 조치 (Critical)#

  • RDS 슬로우 쿼리 / InnoDB lock 지표 확인cupixworks-api production ap-southeast-2 RDS 에서 07:47–07:52 UTC 구간 SHOW ENGINE INNODB STATUS 스냅샷, performance_schema.data_locks / data_lock_waits 조회로 어떤 트랜잭션이 어떤 row 를 오래 잡고 있었는지 특정. 근거: sibling cluster RCA(c25d10f8-…) 와 동일 근본 원인이므로 우선 DB 측 원인(장기 트랜잭션, 대량 UPDATE, 옵티마이저 플랜 변경 등) 을 먼저 확인해야 함.
  • status board scope 재분류 검토 (error-sweeper 저장소, svc:*::unknown 자동 분류) — Mysql2::Error::TimeoutError / ActiveRecord::LockWaitTimeout 시그니처를 감지해 dep:rds-mysql-* scope 로 승격시키면 재발 시 대응이 빨라짐.

단기 개선 (1주 이내)#

  • ActiveRecord::LockWaitTimeout 별도 rescue (app/factories/base_factory.rb:123-140, MultipleResourcableController#create_resource) — 현재 StandardError 계열로 잡혀 502 로 렌더됨. 별도 rescue 절로 429/503 + Retry-After 응답 + warn 로그 레벨 + Datadog 메트릭 emit 을 방향으로 설정. sibling RCA 와 공통 조치.
  • MultipleResourcableController#create_resource 의 중복 검사 원자화 — 현재 find_by_kind 로 pre-check 후 save 하는 구조(multiple_resourcable_controller.rb:52-63) 는 동시성 하에서 TOCTOU 취약. DB 레벨 unique constraint (resources(resourcable_type, resourcable_id, kind) 부분 index) 를 활용하거나 ActiveRecord::RecordNotUnique 를 잡아 idempotent 하게 400/200 을 반환하도록 방향 조정. 이번 사건에서 대상 요청이 락 대기 후 400 을 받은 것도 이 pre-check 가 원인.
  • cupix-agent 클라이언트 idempotency — 동일 session/user 가 POST /bims/:id/resources 를 91초 간격으로 두 번 발행한 패턴 확인 필요. agent 측 재시도 정책에 idempotency-key 또는 지수 backoff 및 응답 대기 상한 도입 검토 (별도 저장소, 이 RCA 범위 밖).

장기 개선 (재발 방지)#

  • DB 관측성 강화: innodb_row_lock_time_avg / innodb_row_lock_waits / mysql.innodb.row_lock_current_waits 를 Datadog 대시보드 상시 노출.
  • svc:cupixworks-api::unknown recent 인시던트 6건 회고: 최근 7일간 재발. 이번 및 sibling RCA 로 최소 두 건은 DB lock contention 확정 — 나머지 4건도 동일 원인인지 배치 재조사 후 공통 대책 수립.
  • counter_culture / EntityUpdates::Child 재검토 — Bim/Resource/Facility 체인의 부모 row touch 가 write concurrency 를 떨어뜨리는 주요 경로일 수 있음. 배치화 또는 async 화 검토.

Monitoring#

Datadog 쿼리(각 timeseries widget 에 그대로 사용 가능한 형태):

ActiveRecord::LockWaitTimeout 발생 건수 (분당)text
sum:trace.rack.request.errors{service:cupixworks-api,error.type:ActiveRecord::LockWaitTimeout,env:production}.as_count()
BimsController#create_resource p95 latencytext
p95:trace.rack.request{service:cupixworks-api,resource_name:api::v1::bimscontroller#create_resource,env:production,region:ap-southeast-2}
cupixworks-api p99 request duration by resource (ap-southeast-2)text
p99:trace.rack.request{service:cupixworks-api,env:production,region:ap-southeast-2} by {resource_name}
RDS InnoDB row lock waits (rate)text
sum:mysql.innodb.row_lock_waits{env:production,region:ap-southeast-2}.as_rate()
RDS InnoDB row lock average time (ms)text
avg:mysql.innodb.row_lock_time_avg{env:production,region:ap-southeast-2}
BimsController#create_resource 4xx (Duplicate kind 신호)text
sum:trace.rack.request{service:cupixworks-api,resource_name:api::v1::bimscontroller#create_resource,http.status_code:400,env:production}.as_count()

알림 추가 제안:

  • logs("service:cupixworks-api \"Lock wait timeout exceeded\" env:production") 5분 창 3건 이상 → PagerDuty.
  • BimsController#create_resource p95 30s 초과 지속 시 warning.

Risk Assessment#

  • Risk level: medium
    • 이번 이벤트는 ~4분 만에 자연 해소, 대상 요청은 400 으로 응답됨(500대 아님). 그러나 같은 시간대 502 3건 + 60s+ 지연 19건 발생, svc:cupixworks-api::unknown scope 로 최근 7일 6회 재발 → 재발 확률 높음.
  • 예상 복잡도: standard
    • 즉시 조치(RDS 지표 확인 + status board scope 승격) 는 낮은 복잡도.
    • 단기 조치(LockWaitTimeout 전용 rescue, unique constraint 도입) 는 표준 크기 PR.
    • 장기 조치(counter_culture / EntityUpdates::Child 재설계) 는 별도 investigation 필요 — 이번 RCA 범위 밖.