ThreeDReconstruction::run | end
RCA: ThreeDReconstruction::run | end
Overview#
What Happened#
2026-06-26 09:53:50 KST, ap-southeast-2 region(nswgov tenant, sinsw 팀)에서 실행 중이던 cupixworks-capture-3dreconstruction-instance agent의 outer try/catch가 발동되어 ThreeDReconstruction::run | end 로그가 error 레벨로 1건 기록되었다. agent 자체의 3D 재구성 연산은 완료되었으나, 결과물을 백엔드에 업로드하는 마지막 단계에서 POST /api/v1/pointclouds 가 403 Permission denied (PERM10000) 로 거부되어 capture 45315의 job 109199 가 error 상태로 종료되었다.
Quick Facts#
| Field | Value |
|---|---|
| exception.class | HttpError |
| exception.message | HTTP request failed (HTTP 403, body {}) |
| top_frame | three-d-reconstruction-service.ts:129 (outer catch) |
| upstream_failure | pointcloudApi.js:874 → POST /api/v1/pointclouds |
| api_error | PERM10000 / Cupix::Errors::PermissionDenied (RecordPolicy#create?) |
| capture.id | 45315 |
| job.id | 109199 |
| user.id | 378 (chanifah.abdurahman2@det.nsw.edu.au) |
| env | production, region ap-southeast-2, tenant nswgov |
| sdk | @tesla/typescript-node-sdk@1.13.3-SNAPSHOT.202605301249_1e9855b03300e7077469e8a23e5d7e36 |
Affected Teams#
| Team / Domain | Error Count | Impact |
|---|---|---|
| sinsw (NSW Gov) | 1 | 단일 사용자의 capture 45315 3D 재구성 결과가 업로드 단계에서 실패 — densemapper 출력물이 서버에 등록되지 않음 |
Timeline#
- 2026-06-26 09:40:16 KST — agent 부팅,
JobManager::loadJob으로 job 109199 로드,ThreeDReconstruction::run | begin(capture 45315) - 2026-06-26 09:40:21 KST —
runThreeDReconstruction시작, densemapper 실행 - 2026-06-26 09:53:50 KST —
POST /api/v1/pointclouds→[403] PERM10000(Permission denied on creating a model Record byRecordPolicy#create?for user 378) - 2026-06-26 09:53:50 KST — agent:
TransferManager::uploadNewPointclouds가 HttpError(403)를 reject, outer catch에서ThreeDReconstruction::run | end가 error 레벨로 로깅됨 (이 클러스터) - 2026-06-26 09:53:51 KST —
PUT /api/v1/jobs/109199job statecreated → stopped - 2026-06-26 09:53:51 KST —
ThreeDReconstruction::terminateService | force shutdown after 10 seconds - 2026-06-26 09:53:53 KST —
reconstruction_state has transitioned from processing to error on Capture 45315
Error Log#
ThreeDReconstruction::run | end
직전(같은 session, 같은 millisecond)에 함께 기록된 upstream 에러 — 본 클러스터의 실제 원인을 식별하는 데 필요한 payload:
TransferManager::uploadNewPointclouds | {"stack":"HttpError: HTTP request failed
at Request._callback (/tmp/agent/dist/node_modules/.pnpm/@tesla+typescript-node-sdk@1.13.3-SNAPSHOT.202605301249_1e9855b03300e7077469e8a23e5d7e36/node_modules/@tesla/typescript-node-sdk/api/pointcloudApi.js:874:40)
...
","message":"HTTP request failed","response":{"body":{},"statusCode":403},"body":{},"statusCode":403,"name":"HttpError"}
Impact#
- Service:
cupixworks-capture-3dreconstruction-instance - Team: sinsw
- 발생 횟수: 1
- 최초 발생: 2026-06-26 09:53:50 KST
- 최근 발생: 2026-06-26 09:53:50 KST
영향 범위는 capture 45315 단건. 3D 재구성 결과(pointcloud, octree, ref-planes, cpc mesh 등) 가 모두 서버에 등록되지 않아, NSW gov 사용자가 해당 capture의 결과를 열람할 수 없다. 동시에 이미 동일 fingerprint의 sibling 클러스터(05c63b73-c68a-474f-b505-75310f689720, TransferManager::uploadNewPointclouds)가 permission_error 로 분류되어 별도 RCA가 진행 중이다. 두 클러스터는 동일 사건의 다른 레이어 로그이다.
Root Cause Summary#
이 클러스터는 upstream HTTP 403 의 cascade log 이다. agent의 ThreeDReconstruction::run outer try/catch (three-d-reconstruction-service.ts:128-130) 가 하위 step 인 uploadPointcloudFiles → TransferManager.uploadNewPointclouds → UploadNewPointcloudsContainer.createGroupPointcloud → cupixApi.pointcloud.create(...) 에서 발생한 HttpError(403) 를 받아 logger.error('ThreeDReconstruction::run | end', error) 로 다시 기록한 결과이다.
실제 원인은 Rails API 측 RecordPolicy#create? 검사: agent 가 보낸 POST /api/v1/pointclouds 의 parent record_id 에 대해 user 378 의 applied_permission 비트 3 와 4 가 모두 0 이라 Cupix::Errors::PermissionDenied (PERM10000) 가 raise 되었다(application_policy.rb:17-32). agent는 이 응답을 받아 catch 했지만, 두 번째 catch (run의 outer) 는 payload 없이 error 인자만 logger 의 두 번째 파라미터로 넘겨 Datadog 메시지 컬럼에는 "ThreeDReconstruction::run | end" 만 남는 noisy log 가 되었다.
Technical Analysis#
Code Path#
- Entry point:
three-d-reconstruction-service.ts:99(ThreeDReconstruction.run) - Step that triggers the upstream failure:
three-d-reconstruction-service.ts:122→uploadPointcloudFiles - Pointcloud upload step:
three-d-reconstruction-service.ts:378→transferManager.uploadNewPointclouds - Container init:
upload-new-pointclouds.container.ts:38→cupixApi.pointcloud.create(createPointcloudRequest) - API factory:
tesla/app/factories/pointcloud_factory.rb:20(Pundit policy check) - Failure point (this cluster's log line):
three-d-reconstruction-service.ts:129(outer catch logger.error)
agent outer catch — 본 클러스터의 로그를 출력한 위치:
private run = async () => {
logger.info('ThreeDReconstruction::run | begin');
let cpCapture: CPCapture | undefined;
try {
const srvJob = await this.jobManager.loadJob(Environment.CPX_JOB_ID!);
cpCapture = await this.createCPCaptureByJob(srvJob);
if (cpCapture) {
// ...
await this.runThreeDReconstruction(cpCapture);
await this.checkPointcloudFiles(cpCapture);
await this.checkCpcLogJsonFile(cpCapture);
await this.uploadPointcloudFiles(cpCapture); // ← 여기서 reject (HttpError 403)
await this.uploadCpcThumbnails(cpCapture);
}
logger.info('ThreeDReconstruction::run | end');
await this.jobManager.updateCompleteActionJob(this.ActionName, TESLA.UpdateJobRequest.StateEnum.Stopped);
} catch (error: any) {
logger.error('ThreeDReconstruction::run | end', error); // ← THIS cluster's error log
await this.jobManager.updateErrorActionJob(this.ActionName);
} finally {
await this.removeWorkspaceAndPointCloudDir(cpCapture!);
}
};
업로드 컨테이너 — HTTP 호출이 일어나는 지점:
private createGroupPointcloud = async (): Promise<void> => {
const cpCapture = this.cpCluster.cpCapture;
const createPointcloudRequest = {
kind: 'group',
name: cpCapture?.name + '_' + this.cpCluster.name,
pointcloud_type: TESLA.PointcloudType._3dReconstructed,
level_id: cpCapture?.srvLevel?.id as number,
record_id: cpCapture?.srvRecord?.id as number,
capture_id: cpCapture?.id as number,
cluster_id: this.cpCluster?.id as number
};
logger.debug('UploadNewPointcloudsContainer::createGroupPointcloud | createPointcloudRequest - %s', JSON.stringify(createPointcloudRequest));
this._groupPointcloud = await this.cupixApi.pointcloud.create(createPointcloudRequest);
// ↑ pointcloudApi.js:874 — HttpError 403 가 발생하는 지점
...
};
Rails 측 permission 검사 — 403 의 실제 근원:
if params[:record_id].present?
self.parent = RecordRepository.new(current_user: self.current_user).show(params[:record_id])
else
raise Cupix::Errors::Parameter.new(code: 'ARG10000', reason: 'record_id is required')
end
self.model.record = self.parent
self.model.facility = self.parent.facility
raise Cupix::Errors::PermissionDenied.new(code: 'PERM10000', reason: 'Permission denied') unless Pundit.policy(self.current_user, self.parent).create?
def create?
return true if admin_administrator?
return false if archived_entity?
if record.has_attribute?(:applied_permission)
if record.applied_permission[3] == 1 || record.applied_permission[4] == 1
true
else
Cupix::Logger.info(
"Permission denied on creating a model #{record.class.name}",
class: self.class.name,
function: __method__,
user: { id: user.id, email: user.email }
)
false
end
else
true
end
end
기대 동작 vs 실제 동작:
- 기대: agent 가 사용한 session(
d85a59602c66c724b35c4e29cca296fd830b2098, user 378) 이 parent Record(record_idfromcpCapture.srvRecord.id) 에 대해 create 권한(bit 3) 또는 full(bit 4) 을 보유 → 정상Pointcloud생성, 결과 업로드 후 job stopped. - 실제: user 378 의
applied_permission이 두 비트 모두 0 → Punditcreate?false → 403/PERM10000. agent 의 outer catch 가 동작해 capture 45315 의reconstruction_state가processing → error로 전이됨.
Log Evidence#
본 클러스터의 raw 로그(Elasticsearch document):
{
"service": "cupixworks-capture-3dreconstruction-instance",
"status": "error",
"message": "ThreeDReconstruction::run | end",
"level": "error",
"session": { "id": "d85a59602c66c724b35c4e29cca296fd830b2098" },
"capture": { "id": 45315 },
"job": { "id": 109199 },
"team": { "domain": "sinsw", "id": 4 },
"user": { "id": 378, "email": "chanifah.abdurahman2@det.nsw.edu.au" },
"environment": "production",
"region": "ap-southeast-2",
"tenant": "nswgov",
"@timestamp": "2026-06-26T00:53:50.820Z",
"log": { "file": { "path": "/tmp/workspace/agent-json-2026.06.26.log" }, "offset": 347272 }
}
같은 session/timestamp/job 의 직전 로그 (offset 345907) — upstream 원인:
TransferManager::uploadNewPointclouds | {"stack":"HttpError: HTTP request failed
at Request._callback (/tmp/agent/dist/node_modules/.pnpm/@tesla+typescript-node-sdk@1.13.3-.../api/pointcloudApi.js:874:40)
...
","message":"HTTP request failed","response":{"body":{},"statusCode":403},"body":{},"statusCode":403,"name":"HttpError"}
Rails 측 (cupixworks-api) 동일 시각 로그:
2026-06-26 09:53:51 info Permission denied on creating a model Record
class: RecordPolicy function: create?
user: { id: 378, email: chanifah.abdurahman2@det.nsw.edu.au }
2026-06-26 09:53:51 info [403] POST /api/v1/pointclouds (Api::V1::PointcloudsController#create)
error: { reason: Permission denied, code: PERM10000,
class: Cupix::Errors::PermissionDenied }
2026-06-26 09:53:51 info [200] PUT /api/v1/jobs/109199 (Api::V1::JobsController#update)
2026-06-26 09:53:51 info [200] PUT /api/v1/captures/45315 (Api::V1::CapturesController#update)
2026-06-26 09:53:53 info reconstruction_state has transitioned from processing to error on Capture 45315
사용한 Datadog 쿼리:
service:cupixworks-capture-3dreconstruction-instance status:error @environment:production "ThreeDReconstruction::run"
service:cupixworks-capture-3dreconstruction-instance @capture.id:45315
service:cupixworks-api @user.id:378
service:cupixworks-api 109199
service:cupixworks-api 45315
상태 게시판 결과(bun run cli/incident-board.ts for-cluster ...): 같은 service 의 unknown scope 에 두 클러스터(05c63b73..., 본 클러스터) 가 동시에 묶여있음. 직전 인시던트(2026-06-24) 도 동일 service 에 4 클러스터 발생 후 해소된 이력이 있어, 이 service 의 outer-catch noisy logging 패턴이 재발 중임을 시사한다.
Hypotheses Considered#
| # | Hypothesis | Evidence for | Evidence against | Verdict |
|---|---|---|---|---|
| H1 | agent outer catch 가 upstream 403 (PointcloudsController#create PERM10000) 을 받아 payload 없이 noisy error 로그를 남김 |
동일 session/timestamp/offset에서 TransferManager::uploadNewPointclouds 가 403 stack과 함께 error 로깅(offset 345907) → 직후 본 클러스터 로그(offset 347272). API 측 [403] POST /api/v1/pointclouds + Permission denied on creating a model Record (user 378). 코드 경로: three-d-reconstruction-service.ts:122 → 378 → upload-new-pointclouds.container.ts:38 → pointcloudApi.js:874 → application_policy.rb:17-32 |
— | Confirmed |
| H2 | densemapper 자체가 실패해 runThreeDReconstruction 단계에서 throw (Failed to execute densemapper) |
같은 service 에 그 패턴의 error(capture 721885, southlandind, 2026-06-25 21:31)가 7일 내 다수 존재 | 본 클러스터 session(d85a59...)의 직전 로그는 densemapper 에러가 아닌 TransferManager::uploadNewPointclouds 403 stack 이고, runThreeDReconstruction environments info 가 정상 출력된 후 업로드 단계까지 진행 |
Rejected |
| H3 | session token 만료 또는 region mismatch 로 인한 인증 실패 | agent 로그 region: us-west-2 와 capture region ap-southeast-2 의 불일치가 외형상 보임 |
API 측 응답이 401 Unauthorized 가 아닌 403 PERM10000, 그리고 같은 session 의 다른 호출(PUT /api/v1/captures/45315, voxels_upload_credentials, jobs/...)이 모두 200 으로 통과. 인증은 성공했고 권한만 부족 |
Rejected |
| H4 | record_id 등 request payload 누락 (ARG10000) |
factory 가 record_id 누락 시 ARG10000 raise (pointcloud_factory.rb:13-14) |
Rails 측 error code 는 PERM10000 이고 record.class.name = Record 로 record 가 정상 lookup 된 상태에서 Pundit create? 가 false 를 반환했음 |
Rejected |
| H5 | 외부 dependency(S3, vendor) 장애 | — | status-board scope 가 svc:*(internal) 이고, 동시 사건이 같은 service 내부에서만 발생. 다른 region/service 에 동시 영향 없음 |
Rejected |
Fix Recommendation#
즉시 조치 (Critical)#
이 클러스터는 upstream 권한 문제의 cascade noise 이므로 코드 변경의 우선순위는 다음 두 갈래로 나뉜다.
- 권한 데이터 점검 (운영 작업): tenant
nswgov, user 378, capture 45315 의 parent Record 에 대해 user 의applied_permission비트 3/4 가 의도대로 설정되었는지 확인 (tesla/app/factories/pointcloud_factory.rb:20의 Pundit 검사 기준). 만약 권한이 정상이어야 한다면 권한 동기화 누락 가능성을 우선 조사 — 코드 변경 불필요. - agent 측 로깅 정리 (
three-d-reconstruction-service.ts:128-130): outer catch 가 이미 하위 step 에서 logged 된 에러를 다시error레벨로 기록하면서 message 컬럼에는 format string 만 남기는 noisy 패턴이다. 본 클러스터처럼 payload 없는"ThreeDReconstruction::run | end"error 로그가 alert/cluster 신호를 오염시키므로, 1) 이중 로깅을 피하거나 2) 두 번째 인자로 넘기는error가 logger 직렬화 정책상 message 컬럼에 들어가도록 변경하는 방향을 검토한다. 단, 팀의 fix logger 규약(memory/services/cupixworks-capture-3dreconstruction-instance.md참고 — "JSON.stringify(error) 금지, logger 가 Error 객체를 직접 처리") 을 따른다.
단기 개선 (1주 이내)#
uploadPointcloudFiles가HttpError의 statusCode 를 검사해 4xx(권한/인자) 와 5xx/네트워크를 구분하고, 4xx 는warn+ 운영 핸드오프 메시지, 5xx 는error+ retry 로 분리하는 방향을 권장한다. (memory/MEMORY.md의 "true bug vs expected operational scenario" 지침에 부합)- Rails 측
PointcloudsController#create가 PERM10000 을 raise 할 때 응답 body 에 record_id/user_id/applied_permission 요약을 포함해 agent 측 진단 비용을 줄이는 방안을 검토 (보안 노출 범위는 별도 리뷰 필요).
장기 개선 (재발 방지)#
- 같은 fingerprint 의 cascade noise (outer-catch 가 inner-catch 가 이미 logged 한 에러를 재로깅) 가 같은 service 에 6/24 와 6/26 두 차례 svc:* 인시던트로 잡혔다. agent 전체에서
logger.error('X | end', error)패턴을 일괄 점검해 (1) 중복 로깅 제거, (2) message column 에 정보가 남도록 format string 통일하는 리팩터링을 권장한다. service note 의 "중복 error log 금지" 컨벤션과 일치. - 3D 재구성 비용이 큰 만큼, 업로드 단계 직전(=densemapper 종료 시점)에 사전 permission probe(
HEAD /api/v1/records/{id}또는 dry-runpointcloud.create)를 두어 compute 결과 폐기를 조기 차단하는 방향도 고려.
Monitoring#
발생률을 추적하기 위한 Datadog timeseries 쿼리:
count:logs("service:cupixworks-capture-3dreconstruction-instance status:error \"ThreeDReconstruction::run | end\" -\"TransferManager\"").index("*").rollup("count").by("region,team.domain,tenant")
403 의 실제 발생을 별도로 추적:
count:logs("service:cupixworks-capture-3dreconstruction-instance status:error \"TransferManager::uploadNewPointclouds\" \"statusCode\\\":403\"").index("*").rollup("count").by("region,team.domain,tenant")
Rails 측 PERM10000 (pointclouds create) 추적:
count:logs("service:cupixworks-api \"[403] POST /api/v1/pointclouds\" \"PERM10000\"").index("*").rollup("count").by("region,team_domain")
Capture state 전이를 통한 user-visible 실패 추적:
count:logs("service:cupixworks-api \"reconstruction_state has transitioned from processing to error\"").index("*").rollup("count").by("region")
알림 임계치 권장: 같은 region+team.domain 조합에서 첫 번째와 두 번째 쿼리 모두 5분 내 ≥ 3 건이면 svc:* incident 자동 생성과 함께 페이지.
Risk Assessment#
- Risk level: low (단건, blast radius = 1 capture, 데이터 손실 없음 — densemapper 출력물은 워크스페이스에 잠시 존재했다가
removeWorkspaceAndPointCloudDir로 정리됨. 재시도 시 권한만 회복되면 정상 복구 가능) - 예상 복잡도: trivial (코드 측: 로그 정리 1-2 라인, 운영 측: 권한 데이터 확인. 단, 동일 service 의 cascade-noise 패턴을 전반적으로 리팩터링한다면 standard)