ES /docs

failed to merge voxels - error: 500 Internal Server Error

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

Overview#

What Happened#

2026-04-28 00:34~00:48 UTC 사이에 cupixworks-api 서비스의 Cupix::VoxelService.merge_voxel 메서드에서 downstream voxel-service Lambda (nswgov-production-voxel-merge)로의 HTTP 호출이 500 Internal Server Error로 23회 실패했다. 원인은 Lambda 내부의 Athena SQL 쿼리에서 voxel 좌표값(x, y)을 CAST(... AS INTEGER)로 변환할 때 값이 32-bit 정수 범위(±2.1B)를 초과하여 NUMERIC_VALUE_OUT_OF_RANGE 오류가 발생한 것이다. nswgov 테넌트의 ap-southeast-2 리전에서만 발생했다.

Quick Facts#

Field Value
exception.class RestClient::InternalServerError
exception.message 500 Internal Server Error
top_frame app/services/cupix/voxel_service.rb:56
deploy production-ap-southeast-2-20260428T0046Z0-b5ccc284-cupixworks
env production, ap-southeast-2

Affected Teams#

Team / Domain Error Count Impact
nswgov (ap-southeast-2) 23 Level 42546의 merged voxel 데이터 조회 불가. 사용자가 해당 레벨의 voxel 시각화를 볼 수 없음

Timeline#

  1. 2026-04-28T00:34:23Z — 최초 에러 발생 (cupixworks-api)
  2. 2026-04-28T00:48:21Z — 마지막 에러 발생
  3. 2026-04-28T00:51:09Z — Lambda 측 마지막 Athena 실패 로그 확인

Error Log#

Datadog Logs

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

Impact#

  • Service: cupixworks-api
  • 발생 횟수: 23
  • 최초 발생: 2026-04-28T00:34:23.473Z
  • 최근 발생: 2026-04-28T00:48:21.163Z

nswgov 테넌트의 Level 42546에 대한 merged voxel 조회가 전부 실패한다. 사용자가 해당 레벨의 voxel 시각화를 요청할 때마다 에러가 발생하며, API는 500 에러를 클라이언트에 전파한다. 다른 테넌트/레벨은 영향받지 않으나, 동일하게 큰 좌표값을 가진 다른 facility/level에서도 잠재적으로 동일 문제가 발생할 수 있다.

Root Cause Summary#

downstream voxel-service Lambda (nswgov-production-voxel-merge)의 Athena SQL 쿼리에서 voxel 좌표값을 CAST(FLOOR(pv.x / {voxel_size}) AS INTEGER)로 변환할 때 32-bit 정수 오버플로우가 발생한다. nswgov 테넌트의 특정 데이터에서 x 좌표를 voxel_size(기본값 2)로 나눈 결과가 약 7.1×10⁹으로, 32-bit signed integer 최댓값(2,147,483,647)을 크게 초과한다. Athena는 NUMERIC_VALUE_OUT_OF_RANGE: Out of range for integer: 7.117375719E9 에러로 쿼리를 실패시키고, Lambda는 이를 HTTP 500으로 반환하며, cupixworks-apiCupix::VoxelService.merge_voxel이 이 500 응답을 받아 에러를 로깅한다.

Technical Analysis#

Code Path#

  1. Entry point — 사용자가 merged voxel을 요청하면 VoxelsController#merged_voxels가 호출된다:
app/controllers/concerns/voxels_controller.rb:16-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
  1. API에서 downstream 호출Cupix::VoxelService.merge_voxelRestClient.put으로 voxel-service Lambda를 호출한다:
app/services/cupix/voxel_service.rb:48-70ruby
def merge_voxel(params = {})
  headers = {
    'Content-Type': :json,
    'x-cupix-auth': params.delete('x-cupix-auth')
  }
  body = params.to_json

  begin
    response = RestClient.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}")
  end
end
  1. Failure point — Lambda 내부 Athena 쿼리에서 CAST(... AS INTEGER) 오버플로우 발생:
services/voxel/lambda/merge_voxel.py:71-86python
query = f"""
  SELECT
    CAST(FLOOR(pv.x / {voxel_size}) AS INTEGER) as x, CAST(FLOOR(pv.y / {voxel_size}) AS INTEGER) as y, 0 as z, SUM(pv.w) / {voxel_size * voxel_size} as w, {int(voxel_size)} as voxel_size
  FROM (
      SELECT
          x, y, 0 as z, MAX(w) as w
      FROM
          tb_raw_voxel
      WHERE
        {' OR '.join(conditions)}
      GROUP BY
          x, y, model_id, model_type
  ) AS pv
  GROUP BY
      FLOOR(pv.x / {voxel_size}), FLOOR(pv.y / {voxel_size})
"""
  1. 에러 반환 경로 — Athena 쿼리 실패 시 Lambda는 500을 반환한다:
services/voxel/lambda/merge_voxel.py:117-136python
if response['QueryExecution']['Status']['State'] == 'SUCCEEDED':
  # ... generate signed URL
  return handler_response(200, { 'signed_url': signed_url})
else:
  return handler_response(500, f'Athena query failed - {response["QueryExecution"]["Status"]["StateChangeReason"]}')

기대 동작: FLOOR(x / 2) 결과가 32-bit INTEGER 범위 내에 있어 정상적으로 voxel 데이터가 집계되고 signed URL이 반환됨.

실제 동작: nswgov 테넌트의 좌표값이 ~14.2×10⁹에 달하여 FLOOR(x / 2) ≈ 7.1×10⁹이 되고, 이는 32-bit INTEGER 최댓값(~2.1×10⁹)을 초과하여 NUMERIC_VALUE_OUT_OF_RANGE 오류 발생.

Log Evidence#

cupixworks-api 에러 로그 (Datadog):

text
Datadog query: service:cupixworks-api status:error "failed to merge voxels"
Time range: 2026-04-27T23:30:00Z to 2026-04-28T01:00:00Z
json
{
  "timestamp": "2026-04-28T00:48:21.163Z",
  "status": "error",
  "message": "failed to merge voxels - error: 500 Internal Server Error",
  "class": "Cupix::VoxelService",
  "function": "merge_voxel",
  "model_id": 42546,
  "model_type": "level",
  "tenant": "nswgov",
  "request_id": "29511973-3be0-4c15-82fa-73ef012aeee4",
  "host": "ip-10-1-17-214.ap-southeast-2.compute.internal"
}

downstream Lambda 로그 (Datadog, service:cupixworks-prod-data-voxel):

text
Datadog query: "Lambda::MergeVoxel" "FAILED"
Time range: 2026-04-27T23:30:00Z to 2026-04-28T01:00:00Z
text
Lambda::MergeVoxel | Athena query id: eb797054-0e55-4e76-b9a9-558e0c9c6327 | state: FAILED
text
Lambda::MergeVoxel | handler_response - status_code: 500, body: "Athena query failed - NUMERIC_VALUE_OUT_OF_RANGE: Out of range for integer: 7.117375719E9"

Lambda는 이 실패를 info 레벨로 로깅하기 때문에 status:error 필터에 나타나지 않는다. "Lambda::MergeVoxel" "FAILED" 검색으로 50건 이상의 실패 로그가 확인되었으며, 오버플로우 값은 7.117375717E9 ~ 7.11737572E9 범위로 일관적이다.

Hypotheses Considered#

# Hypothesis Evidence for Evidence against Verdict
H1 Athena SQL의 CAST(... AS INTEGER) 32-bit 오버플로우 Lambda 로그에 NUMERIC_VALUE_OUT_OF_RANGE: Out of range for integer: 7.117375719E9 명시. merge_voxel.py:73에서 CAST(FLOOR(pv.x / {voxel_size}) AS INTEGER) 사용. 7.1E9 > 2^31-1 (2.147E9) Confirmed
H2 voxel-service Lambda의 타임아웃 또는 메모리 부족 Lambda timeout 120초 설정 (main.tf:388) Lambda 로그에 타임아웃 관련 메시지 없음. Athena 쿼리가 정상 완료 후 FAILED 상태 반환. 에러 메시지가 명확히 NUMERIC_VALUE_OUT_OF_RANGE Rejected
H3 API Gateway 타임아웃 (30초, main.tf:328) API Gateway timeout이 Lambda timeout(120초)보다 짧아 긴 쿼리에서 타임아웃 가능 에러 메시지가 "500 Internal Server Error"이며 타임아웃 아님. Lambda가 정상적으로 500 응답을 반환했고 API에서 이를 수신함 Rejected
H4 Athena 테이블 스키마에서 x 컬럼 타입 문제 (Glue crawler 추론) Glue crawler가 CombineCompatibleSchemas 정책으로 스키마를 자동 추론 (main.tf:217). 초기 데이터가 작으면 int로 추론 가능 문제의 근본은 테이블 스키마가 아닌 쿼리의 CAST(... AS INTEGER). 테이블의 x 컬럼이 double/float이어도 쿼리에서 INTEGER로 캐스팅하면 동일 오류 발생 Rejected (contributing factor)

Fix Recommendation#

즉시 조치 (Critical)#

merge_voxel.py:73에서 CAST(FLOOR(pv.x / {voxel_size}) AS INTEGER)CAST(FLOOR(pv.x / {voxel_size}) AS BIGINT)로 변경한다. y 컬럼도 동일하게 변경한다. BIGINT는 64-bit 정수(-9.2×10¹⁸ ~ 9.2×10¹⁸)를 지원하므로 현재 데이터 범위(~7.1×10⁹)를 충분히 수용한다.

  • 파일: data-pipeline-functions/services/voxel/lambda/merge_voxel.py:73
  • 변경: AS INTEGERAS BIGINT (x, y 두 곳)

변경 후 Lambda를 재배포해야 한다 (Terraform apply 또는 S3에 새 zip 업로드).

단기 개선 (1주 이내)#

  1. Lambda 에러 로깅 수준 변경: 현재 500 응답을 info 레벨로 print()하고 있어 Datadog의 status:error 필터에 잡히지 않는다. handler_response에서 status_code >= 400일 때 적절한 에러 레벨로 로깅하거나, Datadog Log Pipeline에서 "status_code: 500" 패턴을 에러로 리매핑하는 것을 검토한다.

  2. captured_area.py 검토: 동일 리포지토리의 captured_area.py:120tb_raw_voxel 테이블을 쿼리하므로 유사한 정수 오버플로우 가능성이 있는지 확인한다.

장기 개선 (재발 방지)#

  1. Glue crawler 스키마 검증: crawler가 CombineCompatibleSchemas로 자동 추론하는 컬럼 타입이 실제 데이터 범위와 일치하는지 정기적으로 검증하는 프로세스를 도입한다.

  2. Athena 쿼리 테스트: 대규모 좌표를 가진 테스트 데이터셋으로 voxel Lambda의 Athena 쿼리를 테스트하여 유사한 오버플로우를 사전에 발견한다.

Monitoring#

  • Athena 쿼리 실패를 감지하는 Datadog 모니터 추가:
text
"Lambda::MergeVoxel" "FAILED"
  • cupixworks-api의 voxel merge 에러 모니터:
text
service:cupixworks-api status:error "failed to merge voxels"
  • Lambda 500 응답 비율 메트릭 (CloudWatch Lambda Errors metric):
text
aws.lambda.errors{functionname:nswgov-production-voxel-merge}

Risk Assessment#

  • Risk level: medium
  • 예상 복잡도: trivial — merge_voxel.py에서 INTEGERBIGINT 변경 2곳 + Lambda 재배포