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#
- 06:20:08 UTC — 3D reconstruction 에이전트 시작 (job 1040058, capture 686679)
- 06:20:09 UTC — 비디오 1개, 클러스터 6개 로드 완료
- 06:20:26 UTC — 3D reconstruction 환경 설정 시작 (accoes, production, us-west-2)
- 09:07:55 UTC —
UploadNewPointcloudTask:finalize에서 HTTP 400 에러 발생 (Pointcloud 1059399) - 09:08:09-09:09:09 UTC — Potree 에이전트가 동일 pointcloud에 대해 CPC mesh 업로드, potree 업로드 진행
- 09:09:08 UTC —
check_uploading성공 (200) → pointcloud state:queued→done전환 - 09:09:14 UTC —
potree_upload_credentials호출 시 400 반환 (potree_state is uploaded state) - 09:09:16 UTC — Pointcloud state:
done→error전환 - 09:10:08 UTC — 3D reconstruction 에이전트
run | end - 09:10:09 UTC — 서비스 강제 종료
Error Log#
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:finalize가 PUT /pointclouds/1059399/check_uploading API를 호출했을 때, pointcloud의 resource_state가 아직 uploaded로 전환되지 않은 상태(예: uploading 또는 created)였다. API의 check_uploading 메서드는 resource_state가 uploading/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 메서드
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;
}
};
finalize는 BaseTask.postDoneProcess에서 호출된다. 실패 시 retryTask로 전달된다:
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 엔드포인트를 호출한다:
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한다:
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_state가 uploading/missing/created이면 S3 리소스 존재 여부를 확인한다. 리소스가 없으면 400 에러를 반환한다:
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를 반환한다:
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 자체는 통과):
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 에러가 발생했다:
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 에이전트 전체 라이프사이클:
service:cupixworks-capture-3dreconstruction-instance @job.id:1040058
에이전트 라이프사이클 로그 (job 1040058):
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 이벤트:
service:cupixworks-api 1059399
API-side 핵심 이벤트:
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초 간격) 후 최종 에러 로그 시점의 차이로 추정된다.
에이전트 로그 속성:
{
"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)#
-
checkStatusCode조건 수정 —transfer.manager.ts:171에서error.statusCode > 400을error.statusCode >= 400으로 변경하여 400 에러도 retry 제외 대상에 포함시켜야 한다. 400 (Bad Request)은 클라이언트-side 에러로, 재시도해도 동일한 결과가 나오며 불필요한 API 부하만 발생시킨다. -
finalize에러 처리 개선 —upload-new-pointclouds.task.ts:78-80에서JSON.stringify(error, Object.getOwnPropertyNames(error))대신 error 객체를 직접 logger에 전달하여 에이전트 로거 규약에 맞추고, statusCode와 response body를 구조화된 로그 필드로 기록해야 한다.
단기 개선 (1주 이내)#
-
finalize호출 시점에 지연 추가 — S3 업로드 완료 후check_uploading호출 전 짧은 지연(1-2초)을 두거나, 응답 코드에 따른 재시도 전략을 분리하여 S3 eventual consistency를 고려한 retry를 구현해야 한다. -
Potree 에이전트의 credential 갱신 로직 검토 —
potree-service.ts:364에서AwsS3Manager의setApiCreateCredentials콜백이 업로드 완료 후에도 credential 갱신을 시도할 수 있는 구조인지 검토하고,potree_state가 이미uploaded인 경우 갱신을 건너뛰도록 방어 로직을 추가해야 한다.
장기 개선 (재발 방지)#
-
에이전트 간 상태 동기화 메커니즘 — 3D reconstruction 에이전트와 potree 에이전트가 동일 pointcloud를 동시에 처리하는 경쟁 조건을 방지하기 위해, 에이전트 간 상태 확인/동기화 프로토콜을 도입하거나 API-side에서 상태 전환을 idempotent하게 처리해야 한다.
-
API
potree_upload_credentials의 idempotent 처리 —potree_state가uploaded일 때 400 대신, 이미 업로드 완료되었음을 나타내는 성공 응답(기존 credentials 또는 no-op 응답)을 반환하도록 변경을 검토해야 한다.
Monitoring#
- 400 에러 발생 빈도 모니터링:
service:cupixworks-capture-3dreconstruction-instance status:error "UploadNewPointcloudTask:finalize"
- Potree 상태 충돌 모니터링:
service:cupixworks-api "potree_state is uploaded state"
- Pointcloud
done→error전환 모니터링:
service:cupixworks-api "pointcloud state changed from done to error"
Risk Assessment#
- Risk level: medium
- 예상 복잡도: standard —
checkStatusCode조건 수정은 간단하나, 에이전트 간 경쟁 조건 해소는 아키텍처 검토가 필요하다. 현재 1건 발생으로 빈도는 낮지만, pointcloud가done에서error로 전환되는 것은 사용자 데이터에 직접적인 영향을 미칠 수 있다.