ES /docs

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/pointclouds403 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:874POST /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#

  1. 2026-06-26 09:40:16 KST — agent 부팅, JobManager::loadJob 으로 job 109199 로드, ThreeDReconstruction::run | begin (capture 45315)
  2. 2026-06-26 09:40:21 KSTrunThreeDReconstruction 시작, densemapper 실행
  3. 2026-06-26 09:53:50 KSTPOST /api/v1/pointclouds[403] PERM10000 (Permission denied on creating a model Record by RecordPolicy#create? for user 378)
  4. 2026-06-26 09:53:50 KST — agent: TransferManager::uploadNewPointclouds 가 HttpError(403)를 reject, outer catch에서 ThreeDReconstruction::run | end 가 error 레벨로 로깅됨 (이 클러스터)
  5. 2026-06-26 09:53:51 KSTPUT /api/v1/jobs/109199 job state created → stopped
  6. 2026-06-26 09:53:51 KSTThreeDReconstruction::terminateService | force shutdown after 10 seconds
  7. 2026-06-26 09:53:53 KSTreconstruction_state has transitioned from processing to error on Capture 45315

Error Log#

Datadog Logs

text
ThreeDReconstruction::run | end

직전(같은 session, 같은 millisecond)에 함께 기록된 upstream 에러 — 본 클러스터의 실제 원인을 식별하는 데 필요한 payload:

text
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 비트 34 가 모두 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:122uploadPointcloudFiles
  • Pointcloud upload step: three-d-reconstruction-service.ts:378transferManager.uploadNewPointclouds
  • Container init: upload-new-pointclouds.container.ts:38cupixApi.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 — 본 클러스터의 로그를 출력한 위치:

applications/agents/packages/cupix-capture-3d-reconstruction-agent/src/three-d-reconstruction-service.ts:99-134typescript
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 호출이 일어나는 지점:

applications/agents/packages/cupix-capture-3d-reconstruction-agent/src/manager/transfer/upload-new-pointclouds.container.ts:26-52typescript
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 의 실제 근원:

tesla/app/factories/pointcloud_factory.rb:11-20ruby
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?
tesla/app/policies/application_policy.rb:13-36ruby
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_id from cpCapture.srvRecord.id) 에 대해 create 권한(bit 3) 또는 full(bit 4) 을 보유 → 정상 Pointcloud 생성, 결과 업로드 후 job stopped.
  • 실제: user 378 의 applied_permission 이 두 비트 모두 0 → Pundit create? false → 403/PERM10000. agent 의 outer catch 가 동작해 capture 45315 의 reconstruction_stateprocessing → error 로 전이됨.

Log Evidence#

본 클러스터의 raw 로그(Elasticsearch document):

json
{
  "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 원인:

text
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) 동일 시각 로그:

text
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 쿼리:

text
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주 이내)#

  • uploadPointcloudFilesHttpError 의 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-run pointcloud.create)를 두어 compute 결과 폐기를 조기 차단하는 방향도 고려.

Monitoring#

발생률을 추적하기 위한 Datadog timeseries 쿼리:

text
count:logs("service:cupixworks-capture-3dreconstruction-instance status:error \"ThreeDReconstruction::run | end\" -\"TransferManager\"").index("*").rollup("count").by("region,team.domain,tenant")

403 의 실제 발생을 별도로 추적:

text
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) 추적:

text
count:logs("service:cupixworks-api \"[403] POST /api/v1/pointclouds\" \"PERM10000\"").index("*").rollup("count").by("region,team_domain")

Capture state 전이를 통한 user-visible 실패 추적:

text
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)