ES /docs

UploadNewPointcloudTask:finalize | Pointcloud ID: 1059399 - {"stack":"HttpError: HTTP request failed

RCA: UploadNewPointcloudTask:finalize | Pointcloud ID: 1059399 - HttpError 400

Overview#

What Happened#

2026-04-28 09:07:55 UTC에 cupixworks-capture-3dreconstruction-instance 서비스에서 Pointcloud ID 1059399에 대한 UploadNewPointcloudTask:finalize 단계가 HTTP 400 에러로 실패했다. 3D reconstruction 에이전트가 pointcloud 업로드 완료 확인 API(check_uploading)를 호출했으나, API가 400 응답을 반환하며 리소스 업로드가 완료되지 않았음을 알렸다. 이후 별도의 potree 에이전트가 동일 pointcloud를 처리하여 최종적으로 done 상태까지 도달했지만, potree 업로드 자격증명 재요청 시 상태 충돌(potree_state is uploaded state)로 인해 pointcloud가 error 상태로 전환되었다.

Quick Facts#

Field Value
exception.class HttpError
exception.message HTTP request failed
top_frame @tesla/typescript-node-sdk/api/pointcloudApi.js:298
env production, us-west-2

Affected Teams#

Team / Domain Error Count Impact
accoes 1 Pointcloud 1059399의 3D reconstruction finalization 실패 → pointcloud 최종 error 상태 전환

Timeline#

  1. 06:20:08 UTC — 3D reconstruction 에이전트 시작 (job 1040058, capture 686679)
  2. 06:20:09 UTC — 비디오 1개, 클러스터 6개 로드 완료
  3. 06:20:26 UTC — 3D reconstruction 환경 설정 시작 (accoes, production, us-west-2)
  4. 09:07:55 UTCUploadNewPointcloudTask:finalize 에서 HTTP 400 에러 발생 (Pointcloud 1059399)
  5. 09:08:09-09:09:09 UTC — Potree 에이전트가 동일 pointcloud에 대해 CPC mesh 업로드, potree 업로드 진행
  6. 09:09:08 UTCcheck_uploading 성공 (200) → pointcloud state: queueddone 전환
  7. 09:09:14 UTCpotree_upload_credentials 호출 시 400 반환 (potree_state is uploaded state)
  8. 09:09:16 UTC — Pointcloud state: doneerror 전환
  9. 09:10:08 UTC — 3D reconstruction 에이전트 run | end
  10. 09:10:09 UTC — 서비스 강제 종료

Error Log#

Datadog Logs

text
UploadNewPointcloudTask:finalize | Pointcloud ID: 1059399 - {"stack":"HttpError: HTTP request failed
    at Request._callback (/tmp/agent/dist/node_modules/@tesla/typescript-node-sdk/api/pointcloudApi.js:298:40)
    at self.callback (/tmp/agent/dist/node_modules/request/request.js:185:22)
    at Request.emit (node:events:524:28)
    at Request.emit (node:domain:489:12)
    at Request.<anonymous> (/tmp/agent/dist/node_modules/request/request.js:1154:10)
    at Request.emit (node:events:524:28)
    at Request.emit (node:domain:489:12)
    at IncomingMessage.<anonymous> (/tmp/agent/dist/node_modules/request/request.js:1076:12)
    at Object.onceWrapper (node:events:638:28)
    at IncomingMessage.emit (node:events:536:35)","message":"HTTP request failed","response":{"body":{},"statusCode":400},"body":{},"statusCode":400,"name":"HttpError"}

Impact#

  • Service: cupixworks-capture-3dreconstruction-instance
  • Team: accoes
  • 발생 횟수: 1
  • 최초 발생: 2026-04-28T09:07:55.328Z
  • 최근 발생: 2026-04-28T09:07:55.328Z

Root Cause Summary#

3D reconstruction 에이전트의 UploadNewPointcloudTask:finalizePUT /pointclouds/1059399/check_uploading API를 호출했을 때, pointcloud의 resource_state가 아직 uploaded로 전환되지 않은 상태(예: uploading 또는 created)였다. API의 check_uploading 메서드는 resource_stateuploading/missing/created일 때 S3에 리소스가 실제 업로드되었는지 확인하고(check_resource_uploading), 업로드가 완료되지 않았으면 RESC10000 (400) 에러를 발생시킨다. S3 업로드가 아직 진행 중이거나 완료되지 않은 시점에 finalize가 호출되어 400 에러가 발생했다. 에이전트의 retry 로직(checkStatusCode)이 statusCode 400을 retry 대상으로 판단하여(> 400 조건으로 400은 제외되지 않음) 최대 5회까지 재시도했으나, 이 재시도 과정에서 별도의 potree 에이전트가 동일 pointcloud를 처리하며 상태 충돌이 발생했다.

Technical Analysis#

Code Path#

Entry point: 3D reconstruction 에이전트의 UploadNewPointcloudTask:finalize 메서드

packages/cupix-capture-3d-reconstruction-agent/src/manager/transfer/upload-new-pointclouds.task.ts:73-82typescript
finalize = async (): Promise<void> => {
    try {
        const resPointcloud = await this._container.cupixApi.pointcloud.checkUploading(this.pointcloudId);
        this.cpPointcloud.setServerPointcloudData(resPointcloud);
        logger.debug('UploadNewPointcloudTask:finalize | Pointcloud ID: %d, State: %s', resPointcloud.id, resPointcloud.state);
    } catch (error) {
        logger.error('UploadNewPointcloudTask:finalize | Pointcloud ID: %d - %s', this.pointcloudId, JSON.stringify(error, Object.getOwnPropertyNames(error)));
        throw error;
    }
};

finalizeBaseTask.postDoneProcess에서 호출된다. 실패 시 retryTask로 전달된다:

packages/cupix-capture-3d-reconstruction-agent/src/manager/transfer/base.task.ts:44-52typescript
postDoneProcess = (): void => {
    this.finalize()
        .then(() => {
            this._state = TaskType.Completed;
            this._container.checkTaskDone();
        })
        .catch(error => {
            this._container.transferManager.retryTask(this, error);
        });
};

API wrapper: checkUploading은 Tesla SDK의 PUT /pointclouds/{id}/check_uploading 엔드포인트를 호출한다:

packages/api/src/api/pointcloud.api.ts:182-186typescript
checkUploading = async (pointcloudId: number): Promise<TESLA.Pointcloud> => {
    const api = await this.api();
    const res = await api.checkUploading(pointcloudId, Fields.PointCloudFields);
    return unwrapAttributes(res);
};

Failure point: Tesla SDK의 HTTP callback에서 non-2xx 응답 시 HttpError를 throw한다:

@tesla/typescript-node-sdk/api/pointcloudApi.js:290-299javascript
body = models_1.ObjectSerializer.deserialize(body, "PointcloudResponse");
if (response.statusCode && response.statusCode >= 200 && response.statusCode <= 299) {
    resolve({ response: response, body: body });
}
else {
    reject(new apis_1.HttpError(response, body, response.statusCode));  // line 298
}

API-side check_uploading 로직: Rails API에서 resource_stateuploading/missing/created이면 S3 리소스 존재 여부를 확인한다. 리소스가 없으면 400 에러를 반환한다:

app/repositories/pointcloud_repository.rb:267-283ruby
def check_uploading
  case @model.resource_state_name
  when :uploading, :missing, :created
    if @model.manual_potree_convert
      @model.uploaded_resource_state
    else
      raise Cupix::Errors::Resource.new(code: 'RESC10000', reason: 'Resource does not uploaded') unless @model.check_resource_uploading
    end
  when :uploaded
    case @model.potree_state_name
    when :uploading, :missing, :created
      raise Cupix::Errors::InvalidState.new(code: 'STAT10000', reason: 'Potree does not uploaded') unless @model.check_potree_uploading
    end
  end

  @model
end

S3 리소스 확인 로직: check_resource_uploading은 S3에서 리소스 오브젝트 존재를 확인하고, 없으면 missing_resource_state로 전환 후 false를 반환한다:

app/models/concerns/resourcable/pointcloud.rb:24-34ruby
def check_resource_uploading
  resource = self.resource

  if resource.check_uploading
    self.uploaded_resource_state
    true
  else
    self.missing_resource_state
    false
  end
end

Retry 로직 결함: checkStatusCode에서 statusCode 400은 retry 제외 대상이 아니다 (> 400 조건이므로 400 자체는 통과):

packages/cupix-capture-3d-reconstruction-agent/src/manager/transfer.manager.ts:170-173typescript
private checkStatusCode = (error: any): boolean => {
    if (error?.statusCode != undefined && error.statusCode > 400 && error.statusCode < 500) return false;
    return true;
};

400 에러는 MaxRetries (5회)까지 RetryInterval (10초 간격)로 재시도된다.

Potree 에이전트의 상태 충돌: 별도의 potree 에이전트(cupix-tesla-potree-agent)가 동일 pointcloud의 potree 데이터를 업로드 후 checkUploading을 호출하여 pointcloud를 done 상태로 전환시켰다. 이후 potree 에이전트가 potree_upload_credentials를 재요청했을 때, potree_state가 이미 uploaded여서 400 에러가 발생했다:

app/models/concerns/potree/s3.rb:31-32ruby
def potree_upload_credentials
  raise Cupix::Errors::InvalidState.new(code: 'STAT10000', reason: 'potree_state is uploaded state') if %I[uploaded].include?(potree_state_name)

Log Evidence#

Datadog 쿼리 — 3D reconstruction 에이전트 전체 라이프사이클:

text
service:cupixworks-capture-3dreconstruction-instance @job.id:1040058

에이전트 라이프사이클 로그 (job 1040058):

text
06:20:08.521 [info] ThreeDReconstruction::init
06:20:08.521 [info] ThreeDReconstruction::authenticate | begin
06:20:08.601 [info] CupixAuth::setSession | session_id: 2cdce96d32c211bf5fd900660123c6454387842a
06:20:08.601 [info] ThreeDReconstruction::authenticate | end
06:20:08.601 [info] ThreeDReconstruction::run | begin
06:20:08.602 [info] JobManager::loadJob | begin - job id: 1040058
06:20:08.754 [info] JobManager::loadJob | end - job id: 1040058
06:20:09.316 [info] ThreeDReconstruction::loadVideos | video count: 1
06:20:09.400 [info] ThreeDReconstruction::loadClusters | cluster count: 6
06:20:26.327 [info] ThreeDReconstruction::runThreeDReconstruction environments | domain: accoes, envName: production, launchMode: CUPIXWORKS, region: us-west-2
09:07:55.328 [error] UploadNewPointcloudTask:finalize | Pointcloud ID: 1059399 - HttpError: HTTP request failed (statusCode: 400)
09:10:08.241 [info] ThreeDReconstruction::run | end
09:10:09.143 [info] ThreeDReconstruction::terminateService | force shutdown after 10 seconds

Datadog 쿼리 — API-side pointcloud 1059399 이벤트:

text
service:cupixworks-api 1059399

API-side 핵심 이벤트:

text
09:09:00.875 [200] POST /potree_upload_credentials — 성공 (1차 호출)
09:09:08.880 [200] PUT /check_uploading — 성공 (potree_state → uploaded, state → done)
09:09:08.932 [info] pointcloud state changed from queued to done. id: 1059399
09:09:08.932 [info] Pointcloud::publish_on_finish | pointcloud is published on finish - id: 1059399
09:09:14.883 [400] POST /potree_upload_credentials — FAILED: InvalidState (STAT10000) "potree_state is uploaded state"
09:09:15.679 [200] PUT /update — state change to error
09:09:16.396 [info] pointcloud state changed from done to error. id: 1059399

에이전트 에러 로그의 타임스탬프(09:07:55)와 API-side 이벤트(09:08-09:09)의 차이는 에이전트 컨테이너 클럭 스큐 또는 cupixRetriableRequest의 exponential backoff 재시도(최대 5회, 2^n초 간격) 후 최종 에러 로그 시점의 차이로 추정된다.

에이전트 로그 속성:

json
{
  "job.id": 1040058,
  "capture.id": 686679,
  "team.domain": "accoes",
  "team.id": 816,
  "user.id": 34848,
  "user.email": "zneary@accoes.com",
  "session.id": "2cdce96d32c211bf5fd900660123c6454387842a",
  "host": "637986985af5",
  "region": "us-west-2"
}

Hypotheses Considered#

# Hypothesis Evidence for Evidence against Verdict
H1 check_uploading 호출 시 S3 리소스가 아직 업로드 완료되지 않아 API가 RESC10000 (400) 반환 에이전트의 finalize는 S3 업로드 완료 후 즉시 check_uploading을 호출하며, S3 eventual consistency로 인해 오브젝트가 즉시 조회되지 않을 수 있음. 에러 응답 body가 {} (빈 객체)인 것은 SDK의 ObjectSerializer.deserialize가 에러 응답 body를 파싱하며 빈 객체를 반환한 것과 일치. API-side에서 check_uploading이 최종적으로 09:09:08에 성공(200)한 것은 재시도 사이에 업로드가 완료되었음을 시사 에이전트 에러 타임스탬프(09:07:55)와 API 성공 타임스탬프(09:09:08) 사이 ~73초 차이가 있어 타이밍 정합성이 완벽하지 않음 Confirmed
H2 Potree 에이전트와의 경쟁 조건으로 potree_upload_credentials에서 400 발생 API 로그에서 09:09:14에 potree_upload_credentials 400 확인 (potree_state is uploaded state). Potree 에이전트의 AwsS3Manager가 credential 갱신 시 동일 API를 재호출하는 구조 이 400은 potree 에이전트(cupix-tesla-potree-agent) 서비스에서 발생했으며, 3D reconstruction 에이전트의 UploadNewPointcloudTask:finalize 에러와는 별개의 에러 Confirmed (secondary)
H3 네트워크 일시 장애로 인한 transient 에러 에이전트가 ECI 컨테이너에서 실행되므로 네트워크 불안정 가능성 존재 에러 응답이 HTTP 400 (클라이언트 에러)이며, 네트워크 장애라면 connection error 또는 5xx가 예상됨. API-side 로그에서 다른 API 호출은 모두 200으로 정상 Rejected

Fix Recommendation#

즉시 조치 (Critical)#

  1. checkStatusCode 조건 수정transfer.manager.ts:171에서 error.statusCode > 400error.statusCode >= 400으로 변경하여 400 에러도 retry 제외 대상에 포함시켜야 한다. 400 (Bad Request)은 클라이언트-side 에러로, 재시도해도 동일한 결과가 나오며 불필요한 API 부하만 발생시킨다.

  2. finalize 에러 처리 개선upload-new-pointclouds.task.ts:78-80에서 JSON.stringify(error, Object.getOwnPropertyNames(error)) 대신 error 객체를 직접 logger에 전달하여 에이전트 로거 규약에 맞추고, statusCode와 response body를 구조화된 로그 필드로 기록해야 한다.

단기 개선 (1주 이내)#

  1. finalize 호출 시점에 지연 추가 — S3 업로드 완료 후 check_uploading 호출 전 짧은 지연(1-2초)을 두거나, 응답 코드에 따른 재시도 전략을 분리하여 S3 eventual consistency를 고려한 retry를 구현해야 한다.

  2. Potree 에이전트의 credential 갱신 로직 검토potree-service.ts:364에서 AwsS3ManagersetApiCreateCredentials 콜백이 업로드 완료 후에도 credential 갱신을 시도할 수 있는 구조인지 검토하고, potree_state가 이미 uploaded인 경우 갱신을 건너뛰도록 방어 로직을 추가해야 한다.

장기 개선 (재발 방지)#

  1. 에이전트 간 상태 동기화 메커니즘 — 3D reconstruction 에이전트와 potree 에이전트가 동일 pointcloud를 동시에 처리하는 경쟁 조건을 방지하기 위해, 에이전트 간 상태 확인/동기화 프로토콜을 도입하거나 API-side에서 상태 전환을 idempotent하게 처리해야 한다.

  2. API potree_upload_credentials의 idempotent 처리potree_stateuploaded일 때 400 대신, 이미 업로드 완료되었음을 나타내는 성공 응답(기존 credentials 또는 no-op 응답)을 반환하도록 변경을 검토해야 한다.

Monitoring#

  • 400 에러 발생 빈도 모니터링:
text
service:cupixworks-capture-3dreconstruction-instance status:error "UploadNewPointcloudTask:finalize"
  • Potree 상태 충돌 모니터링:
text
service:cupixworks-api "potree_state is uploaded state"
  • Pointcloud doneerror 전환 모니터링:
text
service:cupixworks-api "pointcloud state changed from done to error"

Risk Assessment#

  • Risk level: medium
  • 예상 복잡도: standard — checkStatusCode 조건 수정은 간단하나, 에이전트 간 경쟁 조건 해소는 아키텍처 검토가 필요하다. 현재 1건 발생으로 빈도는 낮지만, pointcloud가 done에서 error로 전환되는 것은 사용자 데이터에 직접적인 영향을 미칠 수 있다.