ES /docs

VoxelService::run | end - error: "failed to create upload credentials - \"Request failed with status

RCA: VoxelService::run | failed to create upload credentials - 504

Overview#

What Happened#

2026-04-23 08:08~08:10 UTC 사이에 cupixworks-any-voxel-agent 서비스에서 voxel 계산 완료 후 결과 업로드를 위한 S3 credential 요청이 cupixworks-api (tesla) 내부 API로부터 504 Gateway Timeout을 받아 실패했다. 동일 시간대에 cupixworks-api 서비스가 배포/재시작 중이었으며(Sidekiq::Shutdown 로그 확인), 이로 인해 API 전반에 걸쳐 504 응답이 발생했다. 영향받은 capture ID는 685941, 685176이며, 총 2건의 "failed to create upload credentials" 에러가 기록되었다.

Quick Facts#

Field Value
exception.class Error
exception.message failed to create upload credentials - "Request failed with status code 504"
top_frame voxel.manager.ts:171 (createUploadCredentials)
env production, us-west-2

Timeline#

  1. 08:05:26Z -- cupixworks-api Sidekiq::Shutdown 발생 (ExportWorker, 서비스 재시작 시작)
  2. 08:06:26Z -- Voxel agent가 capture 685941 작업 시작 (entity parameters 로드)
  3. 08:07:05Z -- Capture 685941 voxel 계산 완료, raw CSV 업로드 시작
  4. 08:08:05Z -- Capture 685941 voxels_upload_credentials 요청 -> 504 응답 -> "failed to create upload credentials" 에러 (1차)
  5. 08:09:53Z -- Capture 685176 voxel 계산 완료, raw CSV 업로드 시작
  6. 08:10:00Z -- 다수 capture (685954, 685941, 685930)에서 API 504 에러 연쇄 발생
  7. 08:10:26Z -- cupixworks-api 두 번째 Sidekiq::Shutdown 로그
  8. 08:10:53Z -- Capture 685176 voxels_upload_credentials 요청 -> 504 응답 -> "failed to create upload credentials" 에러 (2차)
  9. 08:12:27Z -- API 복구 후 capture 69503 정상 처리 (setSession -> calculateVoxels -> uploadXYPlaneVoxels 성공)

Error Log#

Datadog Logs

text
VoxelService::run | end - error: "failed to create upload credentials - \"Request failed with status code 504\""

Impact#

  • Service: cupixworks-any-voxel-agent
  • 발생 횟수: 2 (upload credentials 에러 기준), 동일 시간대 총 14건의 504 관련 에러
  • 최초 발생: 2026-04-23T08:08:05.107Z
  • 최근 발생: 2026-04-23T08:10:53.781Z
  • 영향 범위: Capture 685941, 685176의 voxel 처리 실패 (upload credentials 504). 추가로 capture 685930, 685954에서도 pano 조회 및 loadRealityCapture 단계에서 504 발생. 총 4개 capture, 4명의 사용자(qatest3 팀 3명, devcon 팀 1명), 6개 호스트에서 에러 발생. Voxel 계산은 완료되었으나 결과물이 S3에 업로드되지 못함. voxel_stateerror로 전환 시도되었으나, 이 상태 업데이트 API 호출도 504로 실패한 경우 있음. CloudFront 레벨에서도 OriginCommError 504가 다수 tenant에서 동시 발생하여 플랫폼 전반 영향 확인.

Root Cause Summary#

cupixworks-api (tesla) 서비스의 배포 또는 재시작이 08:05~08:11 UTC 시간대에 진행되면서, 내부 ALB(api-tesla.cupix.internal)가 backend 인스턴스 교체 과정에서 504 Gateway Timeout을 반환했다. Voxel agent는 voxel 계산 완료 후 POST /api/v1/captures/{id}/voxels_upload_credentials 엔드포인트를 호출하여 S3 업로드 credential을 발급받는데, 이 시점에 API가 응답 불가 상태여서 504를 받았다. Voxel agent의 axios HTTP 클라이언트는 timeout: 0 (무제한)으로 설정되어 있어 자체 timeout은 없으며, ALB의 기본 60초 idle timeout이 504를 반환한 것으로 판단된다.

agents 공통 모듈에 CupixAuth::retryable (cupix-auth.ts:59-77)이라는 retry 메커니즘이 존재하지만(statusCode > 500 시 최대 5회, exponential backoff), CaptureApi::createVoxelsUploadCredentials (capture.api.ts:178)는 이 retryable wrapper를 사용하지 않는다. 따라서 504 에러 시 retry 없이 즉시 실패하며, VoxelManager::createUploadCredentials의 catch 블록에서 에러를 new Error()로 재포장하여 throw하므로 단일 실패로 전체 작업이 에러 상태로 전환되었다.

Technical Analysis#

Code Path#

Entry point: VoxelService::run -- SQS 메시지를 받아 voxel 계산 및 업로드를 수행하는 메인 메서드.

cupixworks/applications/agents/packages/cupix-tesla-voxel-agent/src/voxel-service.ts:45-67typescript
run = async (targetId: number, msgObject?: any): Promise<void> => {
    const targetType = msgObject.type ?? 'capture';
    try {
        const serverRealityCapture = await this.realityCaptureManager.loadRealityCapture(targetId, targetType);
        const cpRealityCapture = this.realityCaptureManager.createCPRealityCapture(serverRealityCapture, targetType);

        if (!DEBUG_MODE) await this.realityCaptureManager.updateVoxelState(TESLA.VoxelState.Aggregating);
        await this.realityCaptureManager.loadEntityParameters(cpRealityCapture);
        await this.realityCaptureManager.loadSubModels(cpRealityCapture);

        const result = await this.voxelManager.calculateVoxels(cpRealityCapture);

        await this.voxelManager.saveRawVoxels(result, cpRealityCapture.rawVoxelFilePath);
        await this.voxelManager.saveXYPlaneVoxels(result, cpRealityCapture.voxelFilePath);
        if (!DEBUG_MODE) await this.voxelManager.uploadRawVoxels(cpRealityCapture);
        if (!DEBUG_MODE) await this.voxelManager.uploadXYPlaneVoxels(cpRealityCapture);  // <- 여기서 실패
    } catch (error: any) {
        logger.error('VoxelService::run | error', error);
        if (!DEBUG_MODE) await this.realityCaptureManager.updateVoxelState(TESLA.VoxelState.Error);
    }
};

uploadXYPlaneVoxels 내에서 createUploadCredentials를 호출한다:

cupixworks/applications/agents/packages/cupix-tesla-voxel-agent/src/manager/voxel.manager.ts:71-84typescript
uploadXYPlaneVoxels = async (cpRealityCapture: CPRealityCapture): Promise<void> => {
    this.awsS3Manager.resetS3Info();
    const s3Credentials = await this.createUploadCredentials(cpRealityCapture);  // <- 504 발생 지점

    if (s3Credentials == undefined
        || s3Credentials.aws_access_key_id == undefined
        || s3Credentials.aws_secret_access_key == undefined
        || s3Credentials.bucket_region == undefined
        || s3Credentials.bucket_name == undefined
        || s3Credentials.basepath == undefined
    ) {
        throw new Error('Invalid S3Credentials');
    }
    // ...
};

Failure point: createUploadCredentials -- API 호출이 504로 실패하면 에러 메시지를 재포장하여 throw:

cupixworks/applications/agents/packages/cupix-tesla-voxel-agent/src/manager/voxel.manager.ts:160-174typescript
private createUploadCredentials = async (cpRealityCapture: CPRealityCapture): Promise<TESLA.UploadCredentials> => {
    let s3Credentials: TESLA.UploadCredentials = {};
    try {
        if (cpRealityCapture.isCapture) {
            s3Credentials = await this.cupixApi.capture.createVoxelsUploadCredentials(cpRealityCapture.id);
        } else if (cpRealityCapture.isPointcloud) {
            s3Credentials = await this.cupixApi.pointcloud.createVoxelsUploadCredentials(cpRealityCapture.id);
        } else {
            throw new Error('unknown target type');
        }
    } catch (error: any) {
        throw new Error('failed to create upload credentials - ' + JSON.stringify(error.message));
    }
    return s3Credentials;
};

Downstream API (tesla): voxels_upload_credentials 엔드포인트는 모델의 voxel_stateuploading으로 전환한 후 STS credential을 발급한다:

tesla/app/models/concerns/voxel_module/s3.rb:28-37ruby
def voxels_upload_credentials
  self.uploading_voxel_state

  Cupix::StorageService.upload_credentials(
    storage_option: storage_option,
    id: id,
    bucket_name: storage_option.s3_hosting_bucket_name,
    key: voxels_basepath(voxels_upload_revision)
  )
end

이 코드 자체에는 문제가 없으며, API 서버가 정상 가동 중이면 STS credential을 생성하여 반환한다. 문제는 API 서버가 재시작 중이어서 요청이 도달하지 못한 것이다.

Retry 메커니즘 부재: agents 공통 모듈에 retry 로직이 존재하지만, createVoxelsUploadCredentials에는 적용되지 않음:

cupixworks/applications/agents/packages/api/src/authentication/cupix-auth.ts:59-77typescript
retryable = <T>(f: () => Promise<T>, retries?: number): Promise<T> => new Promise((resolve, reject) => {
    const _retries = retries != undefined ? retries : 0;
    f()
        .then(response => resolve(response))
        .catch(e => {
            if (e && e.statusCode > 500 && _retries < Constants.MaxRetries) {
                const delay = Math.pow(2, _retries) * 1000;
                logger.warn(`CupixAuth::retryable | pathname: ..., code: ${e.statusCode}, try: ${_retries + 1}`);
                setTimeout(() => {
                    this.retryable(f, _retries + 1).then(resolve).catch(reject);
                }, delay);
            } else {
                reject(e);
            }
        });
});
cupixworks/applications/agents/packages/api/src/api/capture.api.ts:178-182typescript
@SkipInDebug()
async createVoxelsUploadCredentials(captureId: number): Promise<TESLA.UploadCredentials> {
    const api = await this.api();
    const res = await api.createVoxelsUploadCredentials(captureId, Fields.VoxelFields);
    return unwrapResult(res);  // retryable()로 감싸지 않음
}

retryablee.statusCode > 500을 체크하지만, axios 에러 객체는 e.response.status에 상태 코드를 가지므로 e.statusCodeundefined가 된다. 설사 retryable로 감싸더라도 현재 구현에서는 retry가 동작하지 않을 수 있다.

Log Evidence#

Datadog 쿼리 1: voxel-agent 에러 로그

text
service:cupixworks-any-voxel-agent status:error
Time: 2026-04-23T07:08:00Z to 2026-04-23T08:15:00Z

두 건의 upload credentials 504 에러:

json
{"timestamp": "2026-04-23T08:08:05.107Z", "message": "VoxelService::run | end - error: \"failed to create upload credentials - \\\"Request failed with status code 504\\\"\""}
{"timestamp": "2026-04-23T08:10:53.781Z", "message": "VoxelService::run | end - error: \"failed to create upload credentials - \\\"Request failed with status code 504\\\"\""}

504가 발생한 실제 API URL (warn 로그에서 확인):

text
CupixAuth::handleError | Response statusCode: 504, requestUriHref: http://api-tesla.cupix.internal/api/v1/captures/685941/voxels_upload_credentials?fields=id%2Cname%2Cvoxel_state%2Cvoxel_state_updated_at, body.result: undefined
CupixAuth::handleError | Response statusCode: 504, requestUriHref: http://api-tesla.cupix.internal/api/v1/captures/685176/voxels_upload_credentials?fields=id%2Cname%2Cvoxel_state%2Cvoxel_state_updated_at, body.result: undefined

동일 시간대에 pano 조회, capture update 등 다양한 API 엔드포인트에서도 504 발생:

text
CupixAuth::handleError | Response statusCode: 504, requestUriHref: http://api-tesla.cupix.internal/api/v1/panos?...&capture_id=685930
CupixAuth::handleError | Response statusCode: 504, requestUriHref: http://api-tesla.cupix.internal/api/v1/captures/685930?...
CupixAuth::handleError | Response statusCode: 504, requestUriHref: http://api-tesla.cupix.internal/api/v1/captures/685954?...
CupixAuth::handleError | Response statusCode: 504, requestUriHref: http://api-tesla.cupix.internal/api/v1/captures/685941?...

Datadog 쿼리 2: cupixworks-api 서비스 에러

text
service:cupixworks-api status:error
Time: 2026-04-23T08:05:00Z to 2026-04-23T08:15:00Z

API 서비스에서 Sidekiq::Shutdown 에러 2건 확인 -- 서비스 재시작의 직접적 증거:

text
[08:05:26Z] Database export model failed: migration id(1252) last step(read_capture) - Sidekiq::Shutdown
[08:10:26Z] Database export model failed: migration id(1252) last step(read_capture) - Sidekiq::Shutdown

복구 증거: 08:12:27Z부터 voxel agent가 정상적으로 API 호출 성공 (capture 69503):

text
[08:12:27Z] CupixAuth::setSession | session_id: 0a2633095a57a032448298310a5ac088301a1458
[08:12:30Z] VoxelManager::uploadXYPlaneVoxels | s3Credentials bucket_region: ap-southeast-2, bucket_name: cupixworks-hosting-a63f5f96341d-apse2, basepath: c1d779b31b/voxels/apse2/capture/69503/v2
[08:12:30Z] AwsS3Manager::uploadDirectoryToS3 | end

Axios 설정 (에러 로그에서 추출):

json
{"timeout": 0, "maxContentLength": -1, "maxBodyLength": -1, "method": "put"}

timeout: 0은 axios에서 "무제한"을 의미하며, 자체 timeout 없이 ALB 504를 그대로 수신한다.

Hypotheses Considered#

# Hypothesis Evidence for Evidence against Verdict
H1 API 서비스 배포/재시작으로 인한 일시적 504 Sidekiq::Shutdown 로그 2건 (08:05, 08:10), 동일 시간대 다수 엔드포인트에서 504, 08:12Z 이후 정상 복구 -- Confirmed
H2 voxels_upload_credentials 엔드포인트 자체 버그 (STS 발급 실패) -- 에러가 upload credentials뿐 아니라 pano 조회, capture update 등 모든 API 호출에서 발생; API 서비스 에러 로그에 해당 엔드포인트 관련 에러 없음; 복구 후 정상 작동 Rejected
H3 Voxel agent의 인증 토큰 만료로 인한 401→504 -- 에러 응답이 명확히 504이며, 에러 발생 전후로 다른 API 호출(entity parameters, panos)은 성공; CupixAuth::setSession 로그에서 세션이 정상 설정됨 Rejected
H4 네트워크 인프라 문제 (ALB 장애) 504는 ALB가 backend로부터 응답을 받지 못할 때 반환하는 코드; CloudFront OriginCommError 동시 발생 Sidekiq::Shutdown이 원인을 더 구체적으로 설명; 특정 시간대에만 발생하고 자연 복구됨 Rejected (ALB 자체 장애가 아닌, backend 비가용이 원인)
H5 기존 CupixAuth::retryable이 retry했으나 5회 모두 실패 retryablestatusCode > 500 체크 시 retry 동작 가능 CaptureApi::createVoxelsUploadCredentialsretryable로 감싸지 않음 (capture.api.ts:178); Datadog warn 로그에 CupixAuth::retryable 경고 메시지 없음 Rejected

Fix Recommendation#

즉시 조치 (Critical)#

  • 없음. 이 에러는 API 배포/재시작 중 발생한 일시적 504이며, 이미 자연 복구되었다. Capture 685941, 685176의 voxel 처리는 SQS 메시지 재시도를 통해 자동 복구 가능한지 확인 필요.

단기 개선 (1주 이내)#

  • CaptureApi::createVoxelsUploadCredentials (capture.api.ts:178)를 기존 CupixAuth::retryable wrapper로 감싸거나, VoxelManager::createUploadCredentials (voxel.manager.ts:160) 레벨에서 retry 로직 추가. 504, 502, 503 등 일시적 서버 에러에 대해 exponential backoff로 재시도하면 배포 중 발생하는 일시적 장애를 자동 복구할 수 있다.
  • CupixAuth::retryable (cupix-auth.ts:64)의 e.statusCode > 500 조건이 axios 에러 객체의 e.response.status와 불일치하는 문제 검토. 다른 API 호출에서도 retry가 의도대로 동작하지 않을 가능성 있음.
  • VoxelService::run의 catch 블록에서 updateVoxelState(Error) 호출이 역시 504로 실패하는 케이스가 확인됨 (08:10:00Z 로그). Error state 업데이트 실패 시에도 retry 또는 최소 warn 로그를 남기도록 개선 필요.

장기 개선 (재발 방지)#

  • Voxel agent의 axios 클라이언트에 적절한 timeout 설정 (timeout: 30000 등). 현재 timeout: 0이므로 ALB의 60초 idle timeout까지 무한 대기하며, 장애 감지가 늦어진다.
  • cupixworks-api 배포 시 rolling update / graceful shutdown 전략 검토. Sidekiq::Shutdown이 반복 발생하는 것은 배포 과정에서 worker가 강제 종료되고 있음을 의미하며, API 서버(Puma/Unicorn)도 동시에 비가용 상태가 되고 있다.
  • SQS 메시지의 ApproximateReceiveCount: 1로 보아 재시도가 이루어지지 않았을 수 있음. SQS Dead Letter Queue 설정과 재시도 정책을 확인하여, 일시적 실패 시 메시지가 재처리되도록 보장.

Monitoring#

  • Voxel agent의 upload credentials 504 에러 빈도 모니터링:
text
service:cupixworks-any-voxel-agent "failed to create upload credentials" status:error
  • API 서비스 504 발생률 모니터링 (배포 시 스파이크 감지):
text
service:cupixworks-any-voxel-agent "504" status:(error OR warn)

Risk Assessment#

  • Risk level: low
  • 예상 복잡도: trivial
  • API 배포 중 발생한 일시적 장애로, 자연 복구됨. Retry 로직 추가로 재발 시 자동 복구 가능.