ES /docs

MySQL lock wait timeout — database contention

RCA: BaseService::handlingMessageErrors | 504 Gateway Timeout on capture update

Overview#

What Happened#

2026-04-23 08:39:59 UTC에 cupixworks-any-voxel-agent 서비스에서 capture 685954의 voxel_state"error"로 업데이트하는 PUT 요청이 HTTP 504 Gateway Timeout으로 실패했다. 동일 시간대에 cupixworks-api에서 대규모 MySQL lock wait timeout이 발생하고 있었으며, 이로 인해 내부 API 요청이 게이트웨이 타임아웃으로 실패한 것이다.

Quick Facts#

Field Value
exception.class Error (axios)
exception.message Request failed with status code 504
top_frame /tmp/agent/dist/app.js:65374 (createError in bundled axios)
env production, us-west-2

Timeline#

  1. 08:38:02 UTC — Voxel agent가 capture 685954 처리 시작 (setCaptureVoxelsParams)
  2. 08:38:56 UTC — Voxel 계산 완료 (768 voxels), S3 raw 업로드 시작
  3. 08:38:59 UTCupdateVoxelState 또는 upload credentials 요청 시 400 에러 발생 (State not changed)
  4. 08:39:56 UTC — Upload credentials 요청에 504 에러 발생
  5. 08:39:59 UTC — Error handler에서 voxel_state: "error" PUT 요청도 504로 실패 → BaseService::handlingMessageErrors 에러 로그 기록

Error Log#

Datadog Logs

text
BaseService::handlingMessageErrors | Error and message object - {"error":{"message":"Request failed with status code 504","name":"Error","stack":"Error: Request failed with status code 504
    at createError (/tmp/agent/dist/app.js:65374:16)
    at settle (/tmp/agent/dist/app.js:65401:13)
    at IncomingMessage.handleStreamEnd (/tmp/agent/dist/app.js:69373:12)
    ...","config":{"timeout":0,"method":"put","data":"{\"voxel_state\":\"error\"}","url":"http://api-tesla.cupix.internal/api/v1/captures/685954?fields=..."},"status":504},"sqsMessage":{"MessageId":"dec12b10-e68c-4f0c-9a4d-b106a3411dfa","Attributes":{"ApproximateReceiveCount":"1"}}}

Impact#

  • Service: cupixworks-any-voxel-agent
  • 발생 횟수: 1 (이 fingerprint). 동일 시간대 동일 서비스에서 504 에러 다수 발생 (최소 10건 이상)
  • 최초 발생: 2026-04-23T08:39:59.632Z
  • 최근 발생: 2026-04-23T08:39:59.632Z

Root Cause Summary#

cupixworks-api (Rails) 서비스의 MySQL 데이터베이스에서 Lock wait timeout exceeded 에러가 대규모로 발생하면서, API 요청 처리가 지연/실패했다. 이로 인해 내부 로드밸런서/게이트웨이가 504 Gateway Timeout을 반환했다. Voxel agent는 voxel 계산 완료 후 결과를 업로드하는 과정에서 이 504를 받았고, error handler 내에서 voxel_state: "error" 상태 업데이트 시도도 동일한 504로 실패했다. Agent 코드의 API 호출에는 retry 로직이 적용되지 않으며, axios timeout도 0 (무제한)으로 설정되어 있어 게이트웨이 타임아웃에 취약하다.

Technical Analysis#

Code Path#

1. SQS 메시지 수신 및 처리 시작

Entry point: base-service.ts:107-111checkingQueue에서 메시지를 받아 runByMessages를 호출하고, 에러 시 handlingMessageErrors로 전달한다.

packages/base/src/base-service.ts:105-116typescript
} else {
    this._countWaitedToStopTask = 0;
    try {
        await this.runByMessages();
    } catch (error) {
        await this.handlingMessageErrors(error);
    }
    this.resetMessages();
    await CPUtils.sleep(500);
    await this.checkingQueue();
}

2. Voxel 처리 실행

VoxelService.run에서 capture를 로드하고, voxel을 계산/업로드한다. Catch 블록에서 updateVoxelState(VoxelState.Error)를 호출하는데, 이 호출 자체도 API 요청이므로 504에 취약하다.

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);
        // ... voxel 계산, 업로드 ...
        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);
    }
};

3. Upload credentials 요청 실패 (504)

VoxelManager.uploadXYPlaneVoxelscreateUploadCredentialscupixApi.capture.createVoxelsUploadCredentials가 504로 실패. 에러 메시지: "failed to create upload credentials - \"Request failed with status code 504\"".

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);
        }
    } catch (error: any) {
        throw new Error('failed to create upload credentials - ' + JSON.stringify(error.message));
    }
    return s3Credentials;
};

4. Error state 업데이트도 504로 실패

VoxelService.run의 catch에서 updateVoxelState(Error) 호출 → cupixApi.capture.update(685954, {voxel_state: "error"}) → 504. 이 에러가 BaseService.runByMessage까지 전파되어 handlingMessageErrors에서 최종 로깅된다.

packages/cupix-tesla-voxel-agent/src/manager/reality_capture.manager.ts:81-97typescript
updateVoxelState = async (state: TESLA.VoxelState): Promise<void> => {
    if (!this.realityCaptureId || !this.realityCaptureType) {
        logger.error('RealityCaptureManager::updateVoxelState | end - undefined reality capture id or type');
        throw new Error('Invalid realityCaptureId or realityCaptureType');
    }
    if (this.realityCaptureType === 'capture') {
        this._srvRealityCapture = await this.cupixApi.capture.update(this.realityCaptureId, { voxel_state: state });
    }
};

5. Error handler에서의 메시지 삭제 판단

Failure point: base-service.ts:290-313handlingMessageErrors에서 getApiErrorToDeleteMessage를 호출하는데, 504 에러는 axios 에러이므로 error.response가 undefined가 아닌 상태. 그러나 statusCoderesponse.statusCode가 아니라 response.status로 설정되고, 504는 400-500 범위 밖(>500)이므로 getApiErrorToDeleteMessage는 undefined를 반환한다. 따라서 checkReceiveCountToDeleteMessage로 fallback하고, ApproximateReceiveCount: 1 < MaxReceiveCount이므로 메시지를 삭제하지 않는다.

packages/base/src/base-service.ts:270-276typescript
if (statusCode != undefined && statusCode >= 400 && statusCode <= 500) {
    if (statusCode === 401) return;
    return errorMsg;
}
return;  // 504는 여기서 undefined 반환 → 메시지 삭제 안 함

6. Retry 로직 미적용

CupixAuth.retryable (cupix-auth.ts:59-77)는 statusCode > 500일 때 retry하는 로직이 있으나, CaptureApiModule.updateCaptureApiModule.createVoxelsUploadCredentialsretryable을 사용하지 않고 직접 API를 호출한다. 또한 axios 설정에 timeout: 0 (무제한)이 설정되어 있다.

Log Evidence#

Datadog 쿼리 1: Voxel agent 에러 로그

text
service:cupixworks-any-voxel-agent status:error
Time: 2026-04-23T07:30:00Z to 2026-04-23T09:30:00Z

08:09~08:39 시간대에 504 에러가 반복적으로 발생. 다수의 capture ID (685930, 685941, 685954, 685176)에서 동일한 패턴 확인:

json
{"timestamp": "2026-04-23 17:39:59 KST", "status": "error", "message": "BaseService::handlingMessageErrors | Error and message object - {...\"status\":504}"}
json
{"timestamp": "2026-04-23 17:39:56 KST", "status": "error", "message": "VoxelService::run | end - error: \"failed to create upload credentials - \\\"Request failed with status code 504\\\"\""}
json
{"timestamp": "2026-04-23 17:38:59 KST", "status": "error", "message": "VoxelService::run | end - error: \"Request failed with status code 400\""}

Datadog 쿼리 2: Capture 685954 관련 전체 로그

text
service:cupixworks-any-voxel-agent 685954
Time: 2026-04-23T07:30:00Z to 2026-04-23T09:30:00Z

처리 타임라인:

json
{"timestamp": "2026-04-23 17:38:02 KST", "message": "CPRealityCapture::setCaptureVoxelsParams | capture ID: 685954 - voxelSize: 1"}
{"timestamp": "2026-04-23 17:38:56 KST", "message": "VoxelManager::calculateVoxels | finished - capture ID: 685954, voxelCount: 768"}
{"timestamp": "2026-04-23 17:38:56 KST", "message": "AwsS3Manager::uploadFileStreamToS3 | begin - filePath: .../capture_685954_voxels.csv"}
{"timestamp": "2026-04-23 17:38:59 KST", "status": "warn", "message": "CupixAuth::handleError | Response statusCode: 400, ...body.result: {\"code\":\"STAT40000\",\"reason\":\"State not changed\"}"}
{"timestamp": "2026-04-23 17:39:56 KST", "status": "warn", "message": "CupixAuth::handleError | Response statusCode: 504, ...voxels_upload_credentials..."}
{"timestamp": "2026-04-23 17:39:59 KST", "status": "warn", "message": "CupixAuth::handleError | Response statusCode: 504, ...captures/685954..."}
{"timestamp": "2026-04-23 17:39:59 KST", "status": "error", "message": "BaseService::handlingMessageErrors | ...status:504"}

Datadog 쿼리 3: API-side MySQL lock timeout 에러 (동일 시간대)

text
service:cupixworks-api (504 OR "Gateway Timeout" OR "timeout")
Time: 2026-04-23T08:35:00Z to 2026-04-23T08:45:00Z

API 서비스에서 대규모 MySQL lock wait timeout이 발생 중:

json
{"timestamp": "2026-04-23 17:44:29 KST", "message": "[502] PUT /api/v1/captures/684805/meta/prop", "error": {"message": "Mysql2::Error::TimeoutError: Lock wait timeout exceeded; try restarting transaction", "class": "ActiveRecord::LockWaitTimeout"}}
json
{"timestamp": "2026-04-23 17:44:16 KST", "message": "[500] POST /api/v1/pointclouds", "error": {"message": "Mysql2::Error::TimeoutError: Lock wait timeout exceeded; try restarting transaction", "class": "Cupix::Errors::System"}}
json
{"timestamp": "2026-04-23 17:43:29 KST", "message": "[500] POST /api/v1/clusters", "error": {"message": "Mysql2::Error::TimeoutError: Lock wait timeout exceeded; try restarting transaction"}}

10분 간 captures, pointclouds, clusters 등 다수 엔드포인트에서 lock wait timeout이 발생. 이는 DB lock contention이 광범위했음을 의미하며, agent의 504는 이 DB 문제로 인해 API 응답이 게이트웨이 타임아웃을 초과한 결과이다.

Hypotheses Considered#

# Hypothesis Evidence for Evidence against Verdict
H1 API-side MySQL lock wait timeout으로 인한 504 Gateway Timeout 동일 시간대 cupixworks-api에서 Mysql2::Error::TimeoutError: Lock wait timeout exceeded 다수 발생 (captures, pointclouds, clusters 엔드포인트). 여러 capture ID에서 동시에 504 발생. Confirmed
H2 Voxel agent의 네트워크 문제 (DNS, 연결 실패 등) 에러가 status: 504로 HTTP 응답을 정상 수신했음. 시스템 에러(errno/code/syscall)가 아님. 다른 API 호출(loadRealityCapture, calculateVoxels)은 정상 완료. Rejected
H3 API 서버 과부하 또는 배포 중 일시적 장애 504가 여러 agent에서 동시에 발생 로그에서 배포 관련 징후 없음. 에러 패턴이 DB lock wait와 정확히 일치. Rejected
H4 Agent의 retry 미적용으로 일시적 504 복구 실패 CaptureApiModule.update에 retry 로직 없음. CupixAuth.retryable이 존재하지만 API 모듈에서 사용하지 않음. DB lock contention이 지속적이었으므로 retry해도 실패했을 가능성 높음. 그러나 retry로 일부 요청은 성공 가능. Inconclusive

Fix Recommendation#

즉시 조치 (Critical)#

이 에러의 근본 원인은 cupixworks-api(tesla) 쪽 MySQL lock contention이다. Agent 측에서는 별도의 코드 수정이 필요하지 않으며, DB lock contention의 원인을 조사해야 한다.

  • Tesla 레포: 동일 시간대에 captures, pointclouds, clusters 테이블에서 lock contention이 발생한 원인을 조사. 대량 bulk 작업 또는 장기 트랜잭션이 있었는지 확인 필요.

단기 개선 (1주 이내)#

  • Agent API 호출에 retry 적용: CaptureApiModule.update, createVoxelsUploadCredentials 등 중요 API 호출에 CupixAuth.retryable 래퍼를 적용하거나, axios interceptor 레벨에서 5xx 에러에 대한 exponential backoff retry를 추가한다.

    • 파일: packages/api/src/api/capture.api.ts:56-60
    • 파일: packages/api/src/api/base.api.ts — BaseApiModule에 공통 retry 래퍼 추가 가능
  • VoxelService error handler 보강: voxel-service.ts:63-66의 catch 블록에서 updateVoxelState(Error) 호출이 실패하더라도 에러가 상위로 전파되지 않도록 try-catch로 감싸야 한다. 현재는 error handler 내 API 호출 실패가 BaseService.handlingMessageErrors까지 전파된다.

    • 파일: packages/cupix-tesla-voxel-agent/src/voxel-service.ts:65
  • Axios timeout 설정: 현재 timeout: 0 (무제한)이므로, 적절한 timeout (예: 30초)을 설정하여 게이트웨이 타임아웃 전에 클라이언트에서 타임아웃을 감지하도록 한다.

장기 개선 (재발 방지)#

  • DB lock contention 모니터링: cupixworks-api에서 ActiveRecord::LockWaitTimeout 발생 빈도를 모니터링하고, 특정 임계치 초과 시 알림을 설정한다.
  • Agent resilience pattern: Circuit breaker 패턴을 도입하여 API가 지속적으로 5xx를 반환할 때 불필요한 요청을 줄이고, backoff 후 재시도하는 구조를 고려한다.

Monitoring#

  • DB lock wait timeout 알림:
text
service:cupixworks-api "Lock wait timeout exceeded"
  • Voxel agent 504 빈도 모니터링:
text
service:cupixworks-any-voxel-agent status:error "504"
  • API 5xx 비율 메트릭:
text
sum:rails.request.errors{service:cupixworks-api,status_code:5xx}.as_rate()

Risk Assessment#

  • Risk level: medium — DB lock contention이 해소되면 agent 에러도 자연 소멸하지만, 재발 가능성 있음
  • 예상 복잡도: standard — agent 측 retry 추가는 표준적 변경이며, 근본 원인인 DB lock contention 해결은 별도 조사 필요