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#
- 2026-04-28T00:34:23Z — 최초 에러 발생 (
cupixworks-api) - 2026-04-28T00:48:21Z — 마지막 에러 발생
- 2026-04-28T00:51:09Z — Lambda 측 마지막 Athena 실패 로그 확인
Error Log#
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-api의 Cupix::VoxelService.merge_voxel이 이 500 응답을 받아 에러를 로깅한다.
Technical Analysis#
Code Path#
- Entry point — 사용자가 merged voxel을 요청하면
VoxelsController#merged_voxels가 호출된다:
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
- API에서 downstream 호출 —
Cupix::VoxelService.merge_voxel이RestClient.put으로 voxel-service Lambda를 호출한다:
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
- Failure point — Lambda 내부 Athena 쿼리에서
CAST(... AS INTEGER)오버플로우 발생:
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})
"""
- 에러 반환 경로 — Athena 쿼리 실패 시 Lambda는 500을 반환한다:
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):
Datadog query: service:cupixworks-api status:error "failed to merge voxels"
Time range: 2026-04-27T23:30:00Z to 2026-04-28T01:00:00Z
{
"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):
Datadog query: "Lambda::MergeVoxel" "FAILED"
Time range: 2026-04-27T23:30:00Z to 2026-04-28T01:00:00Z
Lambda::MergeVoxel | Athena query id: eb797054-0e55-4e76-b9a9-558e0c9c6327 | state: FAILED
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 INTEGER→AS BIGINT(x, y 두 곳)
변경 후 Lambda를 재배포해야 한다 (Terraform apply 또는 S3에 새 zip 업로드).
단기 개선 (1주 이내)#
-
Lambda 에러 로깅 수준 변경: 현재 500 응답을
info레벨로print()하고 있어 Datadog의status:error필터에 잡히지 않는다.handler_response에서 status_code >= 400일 때 적절한 에러 레벨로 로깅하거나, Datadog Log Pipeline에서 "status_code: 500" 패턴을 에러로 리매핑하는 것을 검토한다. -
captured_area.py검토: 동일 리포지토리의captured_area.py:120도tb_raw_voxel테이블을 쿼리하므로 유사한 정수 오버플로우 가능성이 있는지 확인한다.
장기 개선 (재발 방지)#
-
Glue crawler 스키마 검증: crawler가
CombineCompatibleSchemas로 자동 추론하는 컬럼 타입이 실제 데이터 범위와 일치하는지 정기적으로 검증하는 프로세스를 도입한다. -
Athena 쿼리 테스트: 대규모 좌표를 가진 테스트 데이터셋으로 voxel Lambda의 Athena 쿼리를 테스트하여 유사한 오버플로우를 사전에 발견한다.
Monitoring#
- Athena 쿼리 실패를 감지하는 Datadog 모니터 추가:
"Lambda::MergeVoxel" "FAILED"
- cupixworks-api의 voxel merge 에러 모니터:
service:cupixworks-api status:error "failed to merge voxels"
- Lambda 500 응답 비율 메트릭 (CloudWatch Lambda Errors metric):
aws.lambda.errors{functionname:nswgov-production-voxel-merge}
Risk Assessment#
- Risk level: medium
- 예상 복잡도: trivial —
merge_voxel.py에서INTEGER→BIGINT변경 2곳 + Lambda 재배포