ES /docs

## Datadog

RCA: Floorplan check_tile_uploading 500 — Elasticsearch timeout during tile state transition

Error Log#

Datadog Logs

json
{
  "name": "HttpError",
  "statusCode": 500,
  "response": {
    "body": { "error": "Internal Server Error", "status": 500 },
    "request": {
      "method": "PUT",
      "uri": { "pathname": "/api/v1/floorplans/626/check_tile_uploading" }
    },
    "headers": { "x-runtime": "10.470838" }
  }
}

Impact#

  • Service: cupixworks-any-floorplan-agent
  • 발생 횟수: 1
  • 최초 발생: 2026-04-16T06:50:34.627Z
  • 최근 발생: 2026-04-16T06:50:34.627Z

Root Cause Summary#

Floorplan agent가 floorplan 626의 타일 업로드 완료 후 Tesla API의 PUT /api/v1/floorplans/626/check_tile_uploading 엔드포인트를 호출했으나, 해당 API에서 500 에러(10.47초 소요)가 반환되었습니다. Tesla API 로그에서 Faraday::TimeoutError: Operation timed out after 10002 milliseconds with 0 bytes received 에러가 확인되었으며, 이는 floorplan의 tile_state:uploaded 상태 전환 시 발생하는 after_commit 콜백에서 Elasticsearch 문서 업데이트(_update_document)가 타임아웃된 것이 원인입니다. 동일 시간대에 Elasticsearch 클러스터에서 circuit_breaking_exception(heap 7.8gb > limit 7.5gb)이 대량 발생하여 모든 ES write 요청이 거부/지연되고 있었습니다.

Technical Analysis#

Code Path#

Agent 측 (cupixworks-any-floorplan-agent):

  1. FloorplanService::run이 SQS 메시지로 floorplan 626 처리 시작 (state: resource_uploaded)
  2. FloorplanService::tileFloorplan에서 타일 생성 (15:49:55 ~ 15:50:03)
  3. S3 타일 업로드 후 checkTileUploading API 호출:
typescript
// applications/agents/packages/cupix-tesla-floorplan-agent/src/floorplan-service.ts:357
await this.cupixApi.floorplan.checkTileUploading(cpFloorplan.id);
  1. Tesla API가 500 반환 → agent가 HttpError로 로깅

Tesla API 측 (cupixworks-api):

  1. TilableController#check_tile_uploadingTilableRepository#check_tile_uploading 호출:
ruby
# app/repositories/concerns/tilable_repository.rb:4-9
def check_tile_uploading(params)
  @model.check_tile_uploading!
  @model.done_state if @model.respond_to?(:state_cloning?) && @model.state_cloning?
  @model
end
  1. Tile::S3#check_tile_uploading!에서 S3 타일 확인 → 상태 전환 트리거:
ruby
# app/models/concerns/tile/s3.rb:9-14
def check_tile_uploading!
  self.tile_size = tile_uploading_objects.size if self.has_attribute?(:tile_size)
  raise Cupix::Errors::InvalidState.new(code: 'STAT10000', reason: 'No tile objects found in S3') if tile_size.nil? || tile_size.zero?
  self.uploaded_tile_state!
end
  1. tile_state:uploaded 상태 전환의 after_transition 콜백:
ruby
# app/models/concerns/statable/floorplan.rb:167-174
after_transition any => [:uploaded] do |floorplan, transition|
  floorplan.increase_tile_revision
  if floorplan.state_creating?
    floorplan.save!          # → after_commit → _update_document → ES update
  else
    floorplan.fire_events(:done_state)  # → state → :done → save! → ES update
  end
end
  1. Failure point: save! 호출 시 after_commit 콜백에서 Elasticsearch 문서 업데이트 시도:
ruby
# app/models/concerns/searchable.rb:16-17, 55, 94
after_commit on: [:update] do
  _update_document
end

# line 94 (inside _update_document)
results = __elasticsearch__.client.update(request.merge({ index: __elasticsearch__.index_name }))

이 ES client.update 호출이 Faraday의 10초 타임아웃에 걸려 Faraday::TimeoutError가 발생했고, _update_documentrescue StandardError 블록에서 에러가 re-raise되어 API가 500을 반환했습니다.

Log Evidence#

Agent 로그 타임라인 (service:cupixworks-any-floorplan-agent):

text
# 사용 쿼리: service:cupixworks-any-floorplan-agent -f "2026-04-16T05:50:00Z" -t "2026-04-16T07:50:00Z"

15:49:54 KST [info] BaseService::runByMessage | id: 626
15:49:54 KST [info] FloorplanService::run | floorplan state: resource_uploaded, resource_state: uploaded
15:49:55 KST [info] FloorplanService::tileFloorplan | begin
15:50:03 KST [info] FloorplanService::tileFloorplan | tiling done and now save to `/tmp/workspace/626/tile`
15:50:03 KST [info] AwsS3Manager::setCredentials | begin / end
15:50:34 KST [warn] CupixAuth::handleError | Response statusCode: 500, requestUriHref: http://api-tesla.cupix.internal/api/v1/floorplans/626/check_tile_uploading?fields[...]
15:50:34 KST [error] HttpError (statusCode: 500, body: {"error":"Internal Server Error","status":500})
15:50:34 KST [info] BaseService::cleanUpAnythingRelatedModel | path: /tmp/workspace/626

Tesla API 로그 (service:cupixworks-api):

text
# 사용 쿼리: service:cupixworks-api "floorplans/626" -f "2026-04-16T06:49:00Z" -t "2026-04-16T06:52:00Z"

15:49:55 KST [info] [200] GET /api/v1/floorplans/626 (FloorplansController#show)
15:49:56 KST [info] [200] PUT /api/v1/floorplans/626 (FloorplansController#update)
15:50:03 KST [info] [200] POST /api/v1/floorplans/626/tile_upload_credentials (FloorplansController#create_tile_upload_credentials)
15:50:36 KST [info] [500] PUT /api/v1/floorplans/626/check_tile_uploading (FloorplansController#check_tile_uploading)
                   error: ["Faraday::TimeoutError", "Operation timed out after 10002 milliseconds with 0 bytes received"]
                   duration: 10463.21ms, db: 19.22ms
                   request_id: 1807baae-a7da-424a-aef3-1dd8f3c2012d

DB 시간은 19ms에 불과하지만 전체 duration은 10463ms — 나머지 ~10444ms가 Elasticsearch 호출에서 소요되었습니다.

Elasticsearch circuit_breaking_exception (동시간대):

text
# 사용 쿼리: service:cupixworks-api "circuit_breaking_exception" -f "2026-04-16T06:49:00Z" -t "2026-04-16T06:52:00Z"

15:51:50~57 KST [info] 다수의 502 에러 발생:
  - PUT /api/v1/panos/430342/check_tile_uploading → circuit_breaking_exception
  - PUT /api/v1/panos/430343/check_tile_uploading → circuit_breaking_exception
  - PUT /api/v1/panos/430344/check_tile_uploading → circuit_breaking_exception
  ... (최소 10건 이상)
json
{
  "type": "circuit_breaking_exception",
  "reason": "[parent] Data too large, data for [indices:data/write/bulk[s]] would be [8373437306/7.7gb], which is larger than the limit of [8160437862/7.5gb], real usage: [8373433368/7.7gb]",
  "bytes_wanted": 8373437306,
  "bytes_limit": 8160437862,
  "durability": "PERMANENT"
}

Floorplan 626의 에러(15:50:34)가 Pano들의 circuit_breaking_exception(15:51:50~)보다 약 1분 먼저 발생했지만, ES heap이 이미 한계에 근접한 상태에서 write 요청이 극도로 느려지며 timeout이 발생한 것으로 판단됩니다. 1분 후에는 heap이 limit을 초과하여 요청이 즉시 거부(429)되기 시작했습니다.

Fix Recommendation#

즉시 조치 (Critical)#

Tesla API 측에서 _update_document(searchable.rb:112-115)의 rescue StandardError에서 ES 에러를 re-raise하지 않고, 비동기 재시도 큐로 위임하는 방식으로 변경이 필요합니다. 현재 ES 인덱싱 실패가 API 응답 자체를 500으로 만들어, 타일 상태 전환(DB에는 이미 커밋됨)이 성공했음에도 agent가 실패로 인식합니다.

  • 파일: app/models/concerns/searchable.rb:112-115
  • ES 인덱싱 에러를 raise 대신 로깅 + 비동기 재인덱싱 큐로 처리하는 방향

단기 개선 (1주 이내)#

  1. Elasticsearch heap 모니터링 강화: 현재 7.5gb limit에 근접(7.7~7.8gb)하고 있으므로, ES 클러스터의 JVM heap 크기를 늘리거나 노드를 추가해야 합니다.
  2. Floorplan agent 재시도 로직: checkTileUploading 호출 실패 시 재시도 로직 추가. 현재 agent는 API 500 응답에 대해 별도 재시도 없이 실패로 처리합니다.

장기 개선 (재발 방지)#

  1. ES 인덱싱을 API 요청 경로에서 분리: after_commit 콜백에서 동기적으로 ES를 업데이트하는 대신, Sidekiq worker를 통해 비동기로 인덱싱하여 ES 장애가 API 응답에 영향을 주지 않도록 합니다.
  2. ES 클러스터 autoscaling: heap 사용량 기반의 자동 스케일링 정책을 추가하여 트래픽 급증 시에도 circuit breaker가 동작하지 않도록 합니다.

Monitoring#

  • ES heap 사용량 메트릭 알림 추가 (80% 이상 시 warning, 90% 이상 시 critical)
  • check_tile_uploading 엔드포인트의 5xx 비율 모니터링
text
# Datadog 쿼리 예시
service:cupixworks-api "check_tile_uploading" status:error
service:cupixworks-api "circuit_breaking_exception"
service:cupixworks-api "Faraday::TimeoutError"

Risk Assessment#

  • Risk level: medium
  • 예상 복잡도: standard — ES 클러스터 heap 확장은 인프라 변경이 필요하며, searchable.rb의 에러 핸들링 변경은 다른 모델에도 영향을 줄 수 있어 신중한 테스트가 필요합니다.