ES /docs

failed to merge voxels - error: Timed out reading data from server

RCA: failed to merge voxels - error: Timed out reading data from server

Overview#

What Happened#

2026-08-07 07:22 KST 에 production cupixworks-api 에서 GET /api/v1/levels/47975/voxels (voxel viewer 의 merged-voxels 다운로드) 요청 1건이 HTTP 500 으로 실패했다. tesla 가 downstream voxel-service 의 /merge Lambda 를 호출했으나 Lambda 가 30 s 안에 응답을 시작하지 못해 RestClient::Exceptions::ReadTimeout (Timed out reading data from server) 이 발생했고, Cupix::VoxelService.merge_voxel 이 이를 Cupix::Errors::System (SYS20000) 으로 변환해 사용자에게 500 을 반환했다. downstream Athena 쿼리 지연에 의한 transient 실패이며 tesla 코드 결함이 아니다.

Quick Facts#

Field Value
exception.class Cupix::Errors::System
exception.message failed to merge voxels - error: Timed out reading data from server
error.code SYS20000
top_frame app/services/cupix/voxel_service.rb:66
underlying RestClient::Exceptions::ReadTimeout (read_timeout 30 s 초과)
deploy production-us-west-2-20260806T2125Z0-9b66f324-cupixworks
env production, us-west-2

Affected Teams#

Team / Domain Error Count Impact
cupix (voxel viewer) 1 level 47975 의 merged voxels 다운로드 1회 500. 직전(07:21 KST)·직후(07:23 KST) 요청은 302 성공

Timeline#

  1. 2026-08-06 07:07 KST — level 47975 merged voxels 요청 성공 ([302]), 하루 전 정상 접근
  2. 2026-08-07 07:21:42 KST — level 47975 요청 성공 ([302], signed_url redirect)
  3. 2026-08-07 07:22:35 KST — 30 s cache 만료 후 재요청, downstream /merge 가 30 s 안에 응답 못함 → read timeout → HTTP 500 (본 인시던트)
  4. 2026-08-07 07:23:07 KST — level 47975 요청 재성공 ([302]), transient 확인

Error Log#

Datadog Logs

text
failed to merge voxels - error: Timed out reading data from server

Impact#

  • Service: cupixworks-api
  • Team: cupix
  • 발생 횟수: 1
  • 최초 발생: 2026-08-07 07:22 KST
  • 최근 발생: 2026-08-07 07:22 KST

Root Cause Summary#

voxel viewer 가 GET /api/v1/levels/47975/voxels 로 merged voxels 를 요청하면 tesla 는 downstream voxel-service 의 /merge Lambda 를 동기 호출한다. Lambda 는 tb_raw_voxel 을 집계하는 Athena 쿼리를 실행하고 완료될 때까지 blocking 폴링한다 (merge_voxel.py:109-112, timeout/sleep 없는 busy-wait). 이 쿼리가 tesla 의 Cupix::HttpClient read timeout (DEFAULT_READ_TIMEOUT = 30) 을 초과하면, tesla 는 Lambda 응답을 받기 전에 연결을 포기하고 RestClient::Exceptions::ReadTimeout 을 발생시킨다. merge_voxel 은 이 예외를 Cupix::Errors::System (SYS20000) 으로 감싸 raise 하고, 이 경로는 controller 에서 rescue 되지 않아 server_error_controller 가 HTTP 500 으로 매핑한다. read timeout 은 HTTP status code 가 없어 (e.http_code 가 nil) HttpClient 의 status 기반 재시도 대상이 아니다. root cause 는 downstream Athena 응답 지연이라는 transient 조건이며 tesla 코드 결함이 아니다.

Technical Analysis#

Code Path#

Entry point 는 voxel viewer 가 호출하는 merged-voxels 다운로드 endpoint 다. 요청은 controller 에서 rescue 되지 않고 그대로 VoxelService 로 전달된다.

app/controllers/concerns/voxels_controller.rb:15-24ruby
  def merged_voxels
    raw_access_token = request.headers['X-CUPIX-AUTH'] || params['x-cupix-auth']
    merge_voxel_params = repository_instance.create_merge_voxel_params!(params)

    if merge_voxel_params[:capture_ids].blank? && merge_voxel_params[:pointcloud_ids].blank?
      render body: 'x,y,z,w,voxel_size', status: :ok
    else
      redirect_to Cupix::VoxelService.merge_voxel(merge_voxel_params.merge('x-cupix-auth' => raw_access_token)), allow_other_host: true
    end
  end

merge_voxel 은 30 s TTL cache miss 시 downstream /merge 를 동기 호출한다. RestClient::Exception 을 rescue 해 SYS20000 으로 raise 하는 지점이 failure point 다.

app/services/cupix/voxel_service.rb:48-71ruby
    def merge_voxel(params = {})
      access_token = params.delete('x-cupix-auth')
      cache_key = merge_voxel_cache_key(params)

      Rails.cache.fetch(cache_key, expires_in: 30.seconds) do
        headers = {
          'Content-Type': :json,
          'x-cupix-auth': access_token
        }
        body = params.to_json

        begin
          response = Cupix::HttpClient.put("#{$CUPIX_VOXEL_SERVICE_URL}/merge", body, headers)

          body = JSON.parse(response.body)

          body['signed_url']
        rescue RestClient::Exception => e
          Cupix::Logger.error("failed to merge voxels - error: #{e.message}", class: self.name, function: __method__, model_id: params[:model_id], model_type: params[:model_type])

          raise Cupix::Errors::System.new(code: 'SYS20000', reason: "failed to merge voxels - error: #{e.message}")

Cupix::HttpClient.put 은 read timeout 을 30 s 로 고정하고, retry 는 RETRIABLE_STATUS_CODESe.http_code 가 포함될 때만 수행한다. read timeout 예외는 HTTP status 가 없어 e.http_code 가 nil 이므로 재시도되지 않는다.

lib/cupix/http_client.rb:8-11ruby
    RETRIABLE_STATUS_CODES = [429, 502, 503, 504].freeze
    MAX_RETRIES = 3
    DEFAULT_OPEN_TIMEOUT = 5
    DEFAULT_READ_TIMEOUT = 30
lib/cupix/http_client.rb:71-91ruby
    def self.put(url, payload, headers = {}, retries: MAX_RETRIES)
      attempt = 0
      begin
        RestClient::Request.execute(
          method: :put,
          url: url,
          payload: payload,
          headers: headers,
          open_timeout: DEFAULT_OPEN_TIMEOUT,
          read_timeout: DEFAULT_READ_TIMEOUT
        )
      rescue RestClient::Exception => e
        if RETRIABLE_STATUS_CODES.include?(e.http_code) && attempt < retries
          attempt += 1
          sleep((2**(attempt - 1)) + rand(0.0..0.5))
          retry
        end
        raise
      end
    end

downstream 은 voxel-service 의 /merge Lambda 다. Athena 쿼리를 시작한 뒤 완료까지 timeout/sleep 없는 busy-wait 로 폴링하므로, 쿼리가 길어지면 Lambda 는 30 s 안에 응답을 시작하지 못한다.

services/voxel/lambda/merge_voxel.py:108-114python
    # Wait for the query to complete
    while True:
      response = athena.get_query_execution(QueryExecutionId=query_execution_id)
      if response['QueryExecution']['Status']['State'] in ('SUCCEEDED', 'FAILED', 'CANCELLED'):
        break

    print("Lambda::MergeVoxel | Athena query id: " + query_execution_id + " | state: " + response['QueryExecution']['Status']['State'])

Lambda handler 는 200 / 400 / 500 만 반환한다 (merge_voxel.py:134,136,138,145). 즉 Timed out reading data from server 는 Lambda 가 반환한 응답이 아니라, tesla 가 Lambda 응답을 기다리다 30 s 를 초과해 발생시킨 client-side read timeout 이다.

기대 동작 vs 실제 동작: Lambda 가 30 s 안에 signed_url (200) 을 반환하면 tesla 는 redirect_to 로 302 응답한다. 실제로는 Athena 쿼리가 30 s 를 초과해 tesla 가 먼저 연결을 끊었고, 사용자는 500 을 받았다.

Log Evidence#

Datadog 쿼리 (14일 window):

text
service:cupixworks-api "failed to merge voxels"

이 쿼리는 14일간 26건을 반환했고, downstream 실패 유형이 혼재한다. 500 Internal Server Error 11건, 503 Service Unavailable 1건, 그리고 본 인시던트의 Timed out reading data from server 1건이다.

본 인시던트의 error 로그 (원문):

json
{
  "timestamp": "2026-08-06T22:22:35.055Z",
  "message": "failed to merge voxels - error: Timed out reading data from server",
  "level": "error",
  "model_type": "level",
  "model_id": 47975,
  "si_trace_id": "2dfd5358-e9dd-4a9b-a1ba-788239a6d7a1",
  "dd": {
    "service": "cupixworks-api",
    "env": "production",
    "version": "production-us-west-2-20260806T2125Z0-9b66f324-cupixworks"
  }
}

61 ms 뒤 짝을 이루는 request 로그가 HTTP 500 을 확증한다:

json
{
  "timestamp": "2026-08-06T22:22:35.116Z",
  "message": "[500] GET /api/v1/levels/47975/voxels (Api::V1::LevelsController#merged_voxels)",
  "error": {
    "reason": "failed to merge voxels - error: Timed out reading data from server",
    "code": "SYS20000",
    "message": "failed to merge voxels - error: Timed out reading data from server",
    "class": "Cupix::Errors::System"
  }
}

transient 임을 보여주는 level 47975 접근 이력 (query service:cupixworks-api "levels/47975/voxels", 14일 14건):

text
2026-08-06T07:07:10.053Z [302] GET /api/v1/levels/47975/voxels
2026-08-06T22:21:42.285Z [302] GET /api/v1/levels/47975/voxels
2026-08-06T22:22:35.116Z [500] GET /api/v1/levels/47975/voxels
2026-08-06T22:23:07.604Z [302] GET /api/v1/levels/47975/voxels

같은 level 이 500 직전(22:21:42)과 직후(22:23:07)에 모두 302 로 성공했다. 30 s cache TTL (voxel_service.rb:52) 을 고려하면, 22:21:42 성공 응답의 cache 가 만료된 뒤 22:22:35 요청이 cache miss 로 downstream 을 다시 호출했고 그 한 번만 Athena 지연에 걸린 것이다. level 47975 특정 데이터가 항상 실패하는 결정론적 조건이 아니라 transient downstream 지연이다.

Hypotheses Considered#

# Hypothesis Evidence for Evidence against Verdict
H1 downstream voxel-service /merge Lambda 의 Athena 쿼리 지연이 tesla 30 s read timeout 을 초과해 발생한 transient 실패 error 로그 message Timed out reading data from server = RestClient::Exceptions::ReadTimeout; http_client.rb:11 DEFAULT_READ_TIMEOUT = 30; merge_voxel.py:109-112 timeout 없는 busy-wait; 같은 level 이 500 전후로 302 성공 없음 Confirmed
H2 Lambda 가 500 (Athena FAILED/CANCELLED) 을 반환해 발생 같은 endpoint 에서 500 Internal Server Error 변형이 14일간 11건 존재 message 가 500 Internal Server Error 가 아니라 Timed out reading data from server = client-side read timeout; Lambda 500 이면 tesla e.message500 Internal Server Error 가 실림 (다른 ET 변형) Rejected
H3 HttpClient retry 누락이 root cause (재시도했다면 성공했을 것) read timeout 은 재시도되지 않음 (e.http_code nil → RETRIABLE_STATUS_CODES 미매칭, http_client.rb:83) retry 부재는 status 없는 예외의 설계적 미포함이지 결함 아님; 재시도해도 동일 지연이면 다시 30 s 초과. resilience gap 이지 causal defect 아님 Rejected
H4 level 47975 데이터가 항상 merge 실패하는 결정론적 조건 model_id 47975 로 특정됨 22:21:42·22:23:07 동일 level 302 성공, 07-27~08-06 다수 302 성공 → transient Rejected
H5 진행 중인 external dependency / service 인시던트의 일부 status-board 조회 scope svc:cupixworks-api::unknown, active null. recent resolved 인시던트(05:42-07:21 UTC)는 cluster_ids 에 본 cluster 미포함 + 시간대(22:22 UTC) 불일치 Rejected

Fix Recommendation#

즉시 조치 (Critical)#

코드 변경 불필요. 단일 transient 발생이며 tesla 코드 결함이 아니다. Error Tracking 에서 이 변형을 noise 로 처리(ignore) 하는 것을 권장한다. downstream Athena 지연이 반복되는지 모니터링으로 관찰한다.

단기 개선 (1주 이내)#

read timeout 케이스의 로그 레벨을 조정할 수 있다. 현재 voxel_service.rb:66 은 모든 RestClient::Exceptionerror 로 로깅한다. Timed out reading data from server / Net::ReadTimeout 처럼 status code 가 없는 transport-level timeout 은 downstream 지연 신호이므로 warn 으로 분리해 alarm noise 를 줄이는 방향을 검토한다. downstream 500 (Athena 실패) 은 error 로 유지한다. 다만 이 경로는 사용자에게 500 을 반환하므로, 로그 레벨만 낮추기 전에 프런트 UX (viewer 재시도/로딩 표시) 와 함께 검토해야 한다.

장기 개선 (재발 방지)#

  • voxel-service /merge Lambda 의 Athena 폴링을 개선한다. merge_voxel.py:109-112 의 timeout 없는 busy-wait 에 상한과 sleep 을 추가해, 쿼리가 길어질 때 502/503 같은 retriable status 를 반환하면 tesla HttpClient 재시도(4회, exp backoff) 가 동작한다.
  • merge 를 비동기 계약으로 전환하는 방향을 검토한다. Lambda 가 즉시 job id 를 반환하고 tesla/viewer 가 폴링하거나, signed_url 을 캐시 워밍하는 구조면 30 s 동기 한도에 묶이지 않는다.
  • tb_raw_voxel 집계 쿼리(merge_voxel.py:71-86) 를 대용량 level 기준으로 최적화한다 (파티셔닝 / 사전집계) — 30 s 이내 완료를 목표로 한다.

Monitoring#

merge voxels read timeout 발생 추이:

text
service:cupixworks-api status:error "failed to merge voxels - error: Timed out reading data from server"

merge voxels 전체 실패 (downstream 유형 무관) 추이 — 500 / 503 / timeout 합산:

text
service:cupixworks-api status:error "failed to merge voxels"

사용자 노출 500 추이 (merged_voxels endpoint):

text
service:cupixworks-api "merged_voxels" "[500]"

downstream voxel-service merge Lambda 오류 메트릭 (function 명은 환경별 확인 필요):

text
sum:aws.lambda.errors{functionname:cupix-production-voxel-merge}

Risk Assessment#

  • Risk level: low
  • 예상 복잡도: trivial (코드 변경 불필요, ET ignore). 장기 downstream Lambda / Athena 개선은 standard