ES /docs

VoxelService Lambda returns HTTP 500 on Athena failures

RCA: Cupix::Errors::System: failed to merge voxels - error: 500 Internal Server Error

Overview#

What Happened#

GET /api/v1/levels/{id}/voxels (Api::V1::LevelsController#merged_voxels) 요청이 downstream voxel service ($CUPIX_VOXEL_SERVICE_URL/merge, data-pipeline-functions Lambda) 를 호출한다. Lambda 가 500 을 반환하면 tesla 의 Cupix::VoxelService.merge_voxelRestClient::Exception 을 잡아 Cupix::Errors::System (code SYS20000) 로 재발생시키고, 사용자에게는 500 응답이 나간다. 2026-07-14 부터 2026-08-01 까지 여러 tenant 와 level 에 걸쳐 374건 발생했다.

Quick Facts#

Field Value
exception.class Cupix::Errors::System
exception.message failed to merge voxels - error: 500 Internal Server Error
exception.code SYS20000
top_frame app/services/cupix/voxel_service.rb:68
downstream voxel service PUT $CUPIX_VOXEL_SERVICE_URL/merge (data-pipeline-functions merge_voxel.py)
runtime Rails (tesla), Ruby
deploy production-us-west-2-20260731T0510Z0-5d9c7658-cupixworks
env production, us-west-2

Affected Teams#

@http.status_code:500merged_voxels 요청 로그 (최근 14일, 100건 샘플) 의 team.domain 분포다. 특정 tenant 하나가 아니라 여러 tenant 에 걸쳐 발생한다.

Team / Domain Error Count (sample) Impact
southlandind 71 특정 level (66844, 66884) 에서 merge 반복 실패
whitingturner 6 merged voxel 다운로드 실패
realitysa 4 merged voxel 다운로드 실패
cmdintl 4 merged voxel 다운로드 실패
clark-vdc 3 merged voxel 다운로드 실패
기타 (siteline, algtest 등) 나머지 merged voxel 다운로드 실패

Timeline#

  1. 2026-07-14 17:35 KST — 클러스터 최초 발생 (first_seen).
  2. 2026-07-23 14:43 KSTsouthlandind level 66844/66884 에서 대량 발생 (수초 내 수십 건, 클라이언트 재시도 폭주 추정).
  3. 2026-08-01 09:40 KST — 최근 발생 (last_seen), realitysa level 55863, 요청 duration 약 22.6초.

Error Log#

Datadog Logs

text
failed to merge voxels - error: 500 Internal Server Error

Impact#

  • Service: cupixworks-api
  • 발생 횟수: 374
  • 최초 발생: 2026-07-14 17:35 KST
  • 최근 발생: 2026-08-01 09:40 KST

Root Cause Summary#

Root cause 는 downstream voxel service Lambda (merge_voxel.py) 가 500 을 반환하는 것이다. Lambda 는 Athena query 가 FAILED/CANCELLED 로 끝나거나 (merge_voxel.py:136) 처리 중 예외가 발생하면 (merge_voxel.py:145) 500 을 돌려준다. tesla 의 Cupix::VoxelService.merge_voxel 은 이 500 을 RestClient::Exception 으로 받아 Cupix::Errors::System (SYS20000) 로 재발생시킨다 (voxel_service.rb:65-68). tesla 의 Cupix::HttpClient 는 재시도 대상 상태 코드를 [429, 502, 503, 504] 로만 정의하므로 (http_client.rb:8) 500 은 재시도 없이 즉시 전파된다. Representative Error 와 최근 (last_seen 부근) 로그 메시지가 동일하여 stale 하지 않다.

Technical Analysis#

Code Path#

  • Entry point: app/controllers/concerns/voxels_controller.rb:16 (merged_voxels)
  • 컨트롤러가 downstream 호출로 redirect 하는 서비스 진입: app/services/cupix/voxel_service.rb:48 (merge_voxel)
  • Downstream 호출 및 500 → 예외 재발생 지점 (failure point): app/services/cupix/voxel_service.rb:60-68
  • Downstream 500 발생 원인: data-pipeline-functions/services/voxel/lambda/merge_voxel.py:136, :145

컨트롤러는 merge 파라미터를 만들고 downstream 결과 signed URL 로 redirect 한다.

app/controllers/concerns/voxels_controller.rb:16-25ruby
  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

서비스는 downstream 에 PUT /merge 를 보내고, RestClient::Exception 을 잡아 SYS20000 으로 재발생시킨다. downstream 이 200 이 아닌 500 을 주면 여기서 예외가 난다.

app/services/cupix/voxel_service.rb:59-68ruby
        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 의 재시도 대상은 [429, 502, 503, 504] 뿐이라 500 은 재시도되지 않고 즉시 전파된다.

lib/cupix/http_client.rb:8-9ruby
    RETRIABLE_STATUS_CODES = [429, 502, 503, 504].freeze
    MAX_RETRIES = 3

Downstream Lambda 는 Athena query 가 성공하지 않으면 500 을 반환하고, except Exception 에서도 500 을 반환한다. 500 응답 본문은 Athena query failed - {reason} 또는 Failed to merge voxel - {error} 형태다.

data-pipeline-functions/services/voxel/lambda/merge_voxel.py:135-145python
    else:
      return handler_response(500, f'Athena query failed - {response["QueryExecution"]["Status"]["StateChangeReason"]}')
  except KeyError as e:
    return handler_response(400, {
      'result': {
        'code': 'ARG10000',
        'message': 'Missing required attribute in the request body:' + str(e)
      }
    })
  except Exception as e:
    return handler_response(500, f'Failed to merge voxel - {str(e)}')

기대 동작은 downstream 이 merge 결과 signed URL 을 200 으로 반환하는 것이다. 실제로는 Athena query 실패 또는 예외로 500 을 반환하고, tesla 는 이를 재시도 없이 사용자 에러로 전파한다. tesla 로그의 e.message500 Internal Server Error 로만 남아 (RestClient 가 status line 만 표기) Athena 실패 사유가 tesla 측 로그에는 드러나지 않는다.

Log Evidence#

Datadog 쿼리 (representative 메시지, last_seen 부근):

text
service:cupixworks-api "merge voxels"

Time range: 2026-07-31T00:00:00Z ~ 2026-08-01T06:00:00Z. 결과 (요약):

json
{
  "timestamp": "2026-08-01 09:40:24",
  "status": "error",
  "message": "failed to merge voxels - error: 500 Internal Server Error",
  "class": "Cupix::VoxelService",
  "function": "merge_voxel"
}

동일 request 의 request 로그 (500, duration 약 22.6초):

json
{
  "message": "[500] GET /api/v1/levels/55863/voxels (Api::V1::LevelsController#merged_voxels)",
  "status": "info",
  "http": { "status_code": 500, "method": "GET", "url_details": { "path": "/api/v1/levels/55863/voxels" } },
  "error": {
    "reason": "failed to merge voxels - error: 500 Internal Server Error",
    "code": "SYS20000",
    "class": "Cupix::Errors::System"
  },
  "duration": 22637.17,
  "team": { "domain": "realitysa", "id": 1150 },
  "environment": "production"
}

분포 확인 쿼리 (최근 14일, 100건 샘플):

text
service:cupixworks-api "merged_voxels" status:info @http.status_code:500

team.domain 별 집계 결과: southlandind 71, whitingturner 6, realitysa 4, cmdintl 4, clark-vdc 3, 기타. path 별 집계: /api/v1/levels/66844/voxels 37, /api/v1/levels/66884/voxels 18, /api/v1/levels/66881/voxels 9, /api/v1/levels/55863/voxels 4 등 다수 level. 2026-07-23 05:43~05:44 (UTC) 구간에 southlandind level 66844/66884 로 수초 내 수십 건이 몰려 있어 클라이언트 재시도 폭주로 보인다.

downstream Lambda 로그는 CloudWatch 에 있고 Datadog 로 수집되지 않아 (tesla RestClient 는 status line 만 로깅), Athena 실패의 구체 사유는 tesla 로그만으로는 확인 불가하다 (uncertain -- CloudWatch 확인 필요).

Hypotheses Considered#

# Hypothesis Evidence for Evidence against Verdict
H1 downstream voxel Lambda 가 500 을 반환하고 tesla 가 재시도 없이 전파 merge_voxel.py:136/:145 가 500 반환; http_client.rb:8 재시도 대상에 500 없음; tesla error 로그 class: Cupix::VoxelService function: merge_voxel 없음 Confirmed
H2 representative error 가 stale 하고 실제 최근 메시지가 다름 Error Tracking 이 변형을 묶는 일반적 특성 last_seen (2026-08-01 09:40 KST) 로그 메시지가 representative 와 동일 (failed to merge voxels - error: 500 Internal Server Error) Rejected
H3 단일 facility/level 데이터 문제로 인한 국소 이슈 07-23 버스트가 southlandind level 66844/66884 에 집중 14일 샘플에서 southlandind/whitingturner/realitysa/cmdintl/clark-vdc 등 다수 tenant, 다수 level 에 분산 발생 Rejected
H4 일시적 외부 의존성 outage 500 이 downstream 에서 옴 status-board svc:cupixworks-api active incident 없음; 500 은 [429,502,503,504] 재시도 대상 아니며 18일간 지속 재발 Rejected

Fix Recommendation#

즉시 조치 (Critical)#

  • data-pipeline-functions/services/voxel/lambda/merge_voxel.py:109-112 의 Athena polling while True 루프에 timeout 과 sleep 을 추가한다. 현재 상태 폴링이 무한 루프이고 sleep 도 없어 API Gateway 통합 timeout (약 29초) 에 걸려 500 을 유발할 수 있다. 최근 요청 duration 약 22.6초가 이 timeout 근접 가능성을 시사한다 (uncertain -- CloudWatch/API Gateway 로그로 timeout 여부 확정 필요).
  • downstream 500 원인 진단을 위해 CloudWatch 에서 해당 Lambda 로그 (Lambda::MergeVoxel | Athena query id ... state: FAILED, Failed to merge voxel - ...) 를 확인해 Athena 실패 사유를 특정한다.

단기 개선 (1주 이내)#

  • tesla Cupix::VoxelService.merge_voxel (voxel_service.rb:65-68) 에서 downstream 응답 body 를 로그에 포함하도록 개선한다. 현재 e.message500 Internal Server Error 만 남아 근본 원인 추적이 불가능하다. RestClient::Exceptione.response&.body 를 error 로그에 함께 기록하면 Athena 실패 사유가 tesla 측에서도 보인다.
  • Lambda 500 응답을 원인별로 분리한다. Athena query 실패 (데이터/쿼리 문제, 재시도 무의미) 와 일시적 예외를 구분해, 후자는 5xx (예: 503) 로 반환하면 tesla HttpClient 재시도 (RETRIABLE_STATUS_CODES) 가 흡수할 수 있다.

장기 개선 (재발 방지)#

  • merged voxels 조회를 동기 HTTP redirect 대신 비동기 처리 (job + polling/notification) 로 전환하는 것을 검토한다. Athena query 는 수초~수십초가 걸릴 수 있어 API Gateway/Lambda 동기 timeout 경계에 취약하다.
  • 클라이언트 재시도 정책을 검토한다. 07-23 southlandind 버스트처럼 실패한 level 에 대해 수초 내 수십 회 재시도가 발생하면 downstream 부하와 로그 노이즈가 증폭된다.

Monitoring#

merged voxels 5xx 발생 추이 (log search):

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

merged voxels endpoint 의 500 응답 추이 (log search):

text
service:cupixworks-api @http.status_code:500 "merged_voxels"

Risk Assessment#

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

Noise Verdict#

bug — downstream voxel Lambda 의 Athena query 실패/무한 polling 으로 500 이 재시도 없이 전파되며 18일간 다수 tenant·level 에 걸쳐 374건 재발하는 실제 결함이다.