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#
- 2026-08-06 07:07 KST — level 47975 merged voxels 요청 성공 (
[302]), 하루 전 정상 접근 - 2026-08-07 07:21:42 KST — level 47975 요청 성공 (
[302], signed_url redirect) - 2026-08-07 07:22:35 KST — 30 s cache 만료 후 재요청, downstream
/merge가 30 s 안에 응답 못함 → read timeout → HTTP 500 (본 인시던트) - 2026-08-07 07:23:07 KST — level 47975 요청 재성공 (
[302]), transient 확인
Error Log#
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 로 전달된다.
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 다.
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_CODES 에 e.http_code 가 포함될 때만 수행한다. read timeout 예외는 HTTP status 가 없어 e.http_code 가 nil 이므로 재시도되지 않는다.
RETRIABLE_STATUS_CODES = [429, 502, 503, 504].freeze
MAX_RETRIES = 3
DEFAULT_OPEN_TIMEOUT = 5
DEFAULT_READ_TIMEOUT = 30
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 안에 응답을 시작하지 못한다.
# 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):
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 로그 (원문):
{
"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 을 확증한다:
{
"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건):
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.message 에 500 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::Exception 을 error 로 로깅한다. 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
/mergeLambda 의 Athena 폴링을 개선한다.merge_voxel.py:109-112의 timeout 없는 busy-wait 에 상한과sleep을 추가해, 쿼리가 길어질 때 502/503 같은 retriable status 를 반환하면 teslaHttpClient재시도(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 발생 추이:
service:cupixworks-api status:error "failed to merge voxels - error: Timed out reading data from server"
merge voxels 전체 실패 (downstream 유형 무관) 추이 — 500 / 503 / timeout 합산:
service:cupixworks-api status:error "failed to merge voxels"
사용자 노출 500 추이 (merged_voxels endpoint):
service:cupixworks-api "merged_voxels" "[500]"
downstream voxel-service merge Lambda 오류 메트릭 (function 명은 환경별 확인 필요):
sum:aws.lambda.errors{functionname:cupix-production-voxel-merge}
Risk Assessment#
- Risk level: low
- 예상 복잡도: trivial (코드 변경 불필요, ET ignore). 장기 downstream Lambda / Athena 개선은 standard