ES /docs

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#

  1. 09:31 KST — cron job flush_stale_captured_size 실행 시작, stale 상태 Facility 최대 30건 순차 처리
  2. 09:31~09:35 KST — 다수의 Facility captured_area 계산 성공 (info 로그 확인)
  3. 09:35:09 KST — Facility 11519에 대해 500 Internal Server Error 발생 (Athena 쿼리 실패)
  4. 09:37~09:42 KST — 이후 다수 Facility에서 503 Service Unavailable 연쇄 발생 (API Gateway 타임아웃)
  5. 09:42 KST — cron batch 처리 완료

Error Log#

Datadog Logs

text
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#

  1. Entry point: cron schedule이 Cupix::Cron::Facility.flush_stale_captured_size를 4시간마다 :31분에 호출한다.
config/schedule.rb:103-106ruby
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
  1. flush_stale_captured_sizecaptured_size_state: :stale인 Facility 최대 30건을 순차 처리한다. calculate_captured_size(bang이 아닌 버전)를 호출하므로 개별 실패가 전체 batch를 중단시키지는 않는다.
lib/cupix/cron/facility.rb:80-96ruby
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
  1. VoxelModule#calculate_captured_size는 예외를 rescue하여 error 로그를 남기고 false를 반환한다.
app/models/concerns/voxel_module.rb:19-27ruby
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
  1. Failure point: Cupix::VoxelService.captured_area!가 RestClient로 voxel-service Lambda를 호출할 때 RestClient::Exception이 발생한다.
app/services/cupix/voxel_service.rb:72-99ruby
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
  1. Downstream Lambda: captured_area.py에서 Athena 쿼리 실패 시 500을 반환한다. 또한 Athena 쿼리가 30초 이상 걸리면 API Gateway가 503을 반환한다.
data-pipeline-functions/services/voxel/lambda/captured_area.py:104-132python
    # 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')
  1. 타임아웃 불일치: API Gateway 타임아웃은 30초, Lambda 타임아웃은 300초이다.
data-pipeline/cupix-data/services/voxel/main.tf:331-336text
"PUT /api/v1/voxels/captured_area" = {
  # ...
  lambda_arn             = module.captured_area.lambda_function_arn
  timeout_milliseconds   = 30000 # ms  ← API Gateway 30초
}
data-pipeline/cupix-data/services/voxel/main.tf:430text
timeout       = 300  # Lambda 300초 (5분)

Log Evidence#

Datadog 쿼리:

text
service:cupixworks-worker "failed to get captured area" status:error

최초 500 에러 (09:35:09 KST):

json
{
  "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!"
}
json
{
  "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):

json
{
  "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!"
}
json
{
  "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는 정상 처리):

json
{
  "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 시작 로그:

json
{
  "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 레벨 실패 기록:

json
{
  "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:336timeout_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_statestale로 유지하여 다음 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 에러 추적:
text
service:cupixworks-worker "failed to get captured area" "503 Service Unavailable" status:error
  • API Gateway 타임아웃 발생 비율 모니터링:
text
service:cupixworks-worker "failed to get captured area" status:error

Risk Assessment#

  • Risk level: medium
  • 예상 복잡도: standard