VoxelService /captured_area — API Gateway timeout exceeds Athena query duration
RCA: failed to get captured area - error: 500 Internal Server Error
Overview#
What Happened#
2026-04-24 09:35 KST, cupixworks-worker의 cron job Cupix::Cron::Facility.flush_stale_captured_size가 실행되면서 다수의 Facility에 대해 voxel-service의 /captured_area Lambda 엔드포인트를 호출했다. 이 과정에서 일부 요청이 500 Internal Server Error(Athena 쿼리 실패)와 503 Service Unavailable(API Gateway 30초 타임아웃)로 실패했다.
Quick Facts#
| Field | Value |
|---|---|
| exception.class | RestClient::Exception |
| exception.message | failed to get captured area - error: 500 Internal Server Error |
| top_frame | app/services/cupix/voxel_service.rb:92 |
| env | production, us-west-2 |
Timeline#
- 09:31 KST — cron job
flush_stale_captured_size실행 시작,stale상태 Facility 최대 30건 순차 처리 - 09:31~09:35 KST — 다수의 Facility captured_area 계산 성공 (info 로그 확인)
- 09:35:09 KST — Facility 11519에 대해 500 Internal Server Error 발생 (Athena 쿼리 실패)
- 09:37~09:42 KST — 이후 다수 Facility에서 503 Service Unavailable 연쇄 발생 (API Gateway 타임아웃)
- 09:42 KST — cron batch 처리 완료
Error Log#
failed to get captured area - error: 500 Internal Server Error
Impact#
- Service:
cupixworks-worker - 발생 횟수: 1 (클러스터 기준; 실제 연관 에러는 503 포함 약 20건 이상)
- 최초 발생: 2026-04-24T00:35:09.106Z
- 최근 발생: 2026-04-24T00:35:09.106Z
Root Cause Summary#
flush_stale_captured_size cron job이 voxel-service의 /captured_area Lambda 엔드포인트를 호출할 때, Lambda 내부의 Athena 쿼리가 실패하거나 응답 시간이 API Gateway 타임아웃(30초)을 초과하여 에러가 발생한다. 500 에러는 Athena 쿼리 자체 실패(파티션 추가 실패 또는 빈 결과), 503 에러는 API Gateway가 30초 타임아웃으로 연결을 끊을 때 발생한다. Lambda 타임아웃은 300초이지만 API Gateway 타임아웃이 30초로 설정되어 있어, Athena 쿼리가 30초 이상 걸리면 Lambda는 아직 실행 중이어도 클라이언트에는 503이 반환된다.
Technical Analysis#
Code Path#
- Entry point: cron schedule이
Cupix::Cron::Facility.flush_stale_captured_size를 4시간마다:31분에 호출한다.
every '31 */4 * * *' do # 00:31,04:31,08:31 ...
runner 'Cupix::Cron::Cache.flush_user_permissions'
runner 'Cupix::Cron::Facility.flush_stale_captured_size'
end
flush_stale_captured_size는captured_size_state: :stale인 Facility 최대 30건을 순차 처리한다.calculate_captured_size(bang이 아닌 버전)를 호출하므로 개별 실패가 전체 batch를 중단시키지는 않는다.
def flush_stale_captured_size
session = Cupix::Initializer::User.tesla_internal_user.default_session
if session.blank?
Cupix::Logger.info('Failed to flush stale captured size: session is nil or blank', class: self.name, function: __method__, module: 'Cupix::Cron')
return
end
::Facility.stale_captured_size.limit(30).find_each do |facility|
Cupix::Logger.info("Flushing stale captured size for Facility #{facility.id}", class: self.name, function: __method__, module: 'Cupix::Cron')
if facility.calculate_captured_size(session: session)
Cupix::Logger.info("Successfully flushed stale captured size for Facility #{facility.id}", class: self.name, function: __method__, module: 'Cupix::Cron')
else
Cupix::Logger.warn("Failed to flush stale captured size for Facility #{facility.id}", class: self.name, function: __method__, module: 'Cupix::Cron')
end
end
end
VoxelModule#calculate_captured_size는 예외를 rescue하여error로그를 남기고false를 반환한다.
def calculate_captured_size(session: nil)
calculate_captured_size!(session: session)
rescue => e
Cupix::Logger.error("failed to calculate captured size for #{self.class.name} ID: #{id}, error: #{e.message}", class: self.class.name, function: __method__, model: { id: id, type: self.class.name })
false
else
true
end
- Failure point:
Cupix::VoxelService.captured_area!가 RestClient로 voxel-service Lambda를 호출할 때RestClient::Exception이 발생한다.
def captured_area!(params = {})
# ...
begin
response = RestClient.put("#{$CUPIX_VOXEL_SERVICE_URL}/captured_area", body.to_json, headers)
body = JSON.parse(response.body)
body['results']
rescue RestClient::Exception => e
Cupix::Logger.error("failed to get captured area - error: #{e.message}", class: self.name, function: __method__, facility_key: params[:facility_key], group_by_record: params[:group_by_record])
raise Cupix::Errors::System.new(code: 'SYS20000', reason: "failed to get captured area - error: #{e.message}")
end
end
- Downstream Lambda:
captured_area.py에서 Athena 쿼리 실패 시 500을 반환한다. 또한 Athena 쿼리가 30초 이상 걸리면 API Gateway가 503을 반환한다.
# Create partition if not exists
partition_query = f"""
ALTER TABLE tb_tesla_reality_captures ADD IF NOT EXISTS PARTITION (facility_key = '{facility_key}');
"""
if run_athena_query(partition_query, output_location) is None:
return handler_response(500, f'Athena query failed - response is None')
# Get captured area
# ... (Athena query)
response = run_athena_query(captured_area_query, output_location)
if response is not None and len(response['Rows']) > 0:
return handler_response(200, { ... })
else:
return handler_response(500, f'Athena query failed - response is None')
- 타임아웃 불일치: API Gateway 타임아웃은 30초, Lambda 타임아웃은 300초이다.
"PUT /api/v1/voxels/captured_area" = {
# ...
lambda_arn = module.captured_area.lambda_function_arn
timeout_milliseconds = 30000 # ms ← API Gateway 30초
}
timeout = 300 # Lambda 300초 (5분)
Log Evidence#
Datadog 쿼리:
service:cupixworks-worker "failed to get captured area" status:error
최초 500 에러 (09:35:09 KST):
{
"timestamp": "2026-04-24 09:35:09 KST",
"status": "error",
"message": "failed to get captured area - error: 500 Internal Server Error",
"class": "Cupix::VoxelService",
"function": "captured_area!"
}
{
"timestamp": "2026-04-24 09:35:09 KST",
"status": "error",
"message": "failed to calculate captured size for Facility ID: 11519, error: failed to get captured area - error: 500 Internal Server Error",
"class": "Facility",
"function": "calculate_captured_size"
}
이후 연쇄 503 에러 (09:37~09:42 KST, 다수 Facility):
{
"timestamp": "2026-04-24 09:40:17 KST",
"status": "error",
"message": "failed to get captured area - error: 503 Service Unavailable",
"class": "Cupix::VoxelService",
"function": "captured_area!"
}
{
"timestamp": "2026-04-24 09:40:17 KST",
"status": "error",
"message": "failed to calculate captured size for Facility ID: 13068, error: failed to get captured area - error: 503 Service Unavailable",
"class": "Facility",
"function": "calculate_captured_size"
}
성공 사례도 같은 시간대에 존재 (일부 Facility는 정상 처리):
{
"timestamp": "2026-04-24 09:35:09 KST",
"status": "info",
"message": "Completed - total: 40128, details: [{\"level_id\"=>47618, \"record_id\"=>125856, \"captured_area\"=>2213}, ...]",
"class": "Cupix::VoxelService",
"function": "captured_area!"
}
cron job 시작 로그:
{
"timestamp": "2026-04-24 09:39:47 KST",
"status": "info",
"message": "Flushing stale captured size for Facility 12925",
"class": "Cupix::Cron::Facility",
"function": "flush_stale_captured_size"
}
warn 레벨 실패 기록:
{
"timestamp": "2026-04-24 09:39:47 KST",
"status": "warn",
"message": "Failed to flush stale captured size for Facility 12925",
"class": "Cupix::Cron::Facility",
"function": "flush_stale_captured_size"
}
Hypotheses Considered#
| # | Hypothesis | Evidence for | Evidence against | Verdict |
|---|---|---|---|---|
| H1 | Athena 쿼리가 API Gateway 30초 타임아웃을 초과하여 503 반환 | API Gateway timeout=30s, Lambda timeout=300s (main.tf:336,430). 대다수 에러가 503. Athena busy-wait 패턴(captured_area.py:49) |
일부 요청은 같은 시간대에 성공 | Confirmed |
| H2 | Athena 파티션 추가 또는 쿼리 자체가 실패하여 Lambda가 500 반환 | Facility 11519에서 500 에러 발생 (captured_area.py:108,132). run_athena_query가 None 반환 시 500 |
500은 1건만 발생, 나머지는 503 | Confirmed (부분) |
| H3 | voxel-service Lambda cold start로 인한 지연 | Lambda 128MB 메모리 (main.tf:431), Python 3.8 런타임 |
cold start는 보통 수초 이내이며 30초를 초과하지 않음. 연속 호출이므로 warm instance 재활용 가능 | Rejected |
| H4 | RestClient 타임아웃 설정 누락으로 worker가 장시간 블로킹 | RestClient.put 호출에 timeout 미설정 (voxel_service.rb:84) |
API Gateway가 30초에 503을 반환하므로 RestClient는 30초 안에 응답 수신 | Inconclusive |
Fix Recommendation#
즉시 조치 (Critical)#
captured_area.py:49의 Athena busy-wait 루프에 sleep 간격을 추가하여 Athena API 호출 과다를 방지한다. 현재while True루프가get_query_execution을 0ms 간격으로 반복 호출하여 Athena throttling을 유발할 수 있다.voxel_service.rb:84에서RestClient.put호출에 명시적timeout을 설정한다 (예: 60초). API Gateway가 30초에 503을 반환하더라도 RestClient 기본 타임아웃이 없으면 예외 처리가 불안정할 수 있다.
단기 개선 (1주 이내)#
- API Gateway 타임아웃을 Lambda 타임아웃에 맞추어 늘린다:
data-pipeline/cupix-data/services/voxel/main.tf:336의timeout_milliseconds를 30000에서 적어도 120000(120초)으로 증가. 현재 API Gateway 최대 타임아웃은 29초(REST API) 또는 30초(HTTP API)이므로, HTTP API인 경우 비동기 invocation 패턴이나 콜백 패턴 도입을 검토한다. voxel_module.rb:19-27에서 error 로그를 503/500 분리하여 일시적 타임아웃(503)은warn레벨로 다운그레이드한다. 현재 모든 실패가error로 기록되어 불필요한 알림을 발생시킨다.- cron job에 재시도 로직 추가:
flush_stale_captured_size에서 503 실패 시 Facility의captured_size_state를stale로 유지하여 다음 cron 사이클에서 자동 재시도되도록 한다 (현재 코드 상 이미 update되지 않으므로 stale 유지되지만, 명시적 처리가 바람직하다).
장기 개선 (재발 방지)#
- captured_area 계산을 동기 API 호출 대신 비동기 패턴으로 전환한다: Lambda를 직접 비동기 invoke하고, 결과를 SQS/SNS 콜백으로 수신하는 아키텍처를 검토한다. 이렇게 하면 API Gateway 타임아웃 제약을 우회할 수 있다.
- Athena 쿼리 최적화:
captured_area.py의 쿼리에 파티셔닝과 결과 캐시(ResultReuseByAgeConfiguration: 720분)가 설정되어 있으나, 대용량 Facility에서는 여전히 30초를 초과할 수 있다. 쿼리 플랜 분석과 테이블 최적화(Parquet 포맷, 버킷팅)를 검토한다.
Monitoring#
- voxel-service captured_area 호출의 지연 시간 p95/p99 메트릭 추가
- Datadog 쿼리로 503 에러 추적:
service:cupixworks-worker "failed to get captured area" "503 Service Unavailable" status:error
- API Gateway 타임아웃 발생 비율 모니터링:
service:cupixworks-worker "failed to get captured area" status:error
Risk Assessment#
- Risk level: medium
- 예상 복잡도: standard