PanoPostprocessorService::run | not found panos
RCA: PanoPostprocessorService::run | not found panos
Error Log#
text
PanoPostprocessorService::run | not found panos
Impact#
- Service:
cupixworks-pano-postprocessor-instance - Team: ebcinc
- 발생 횟수: 3 (동일 에러가 3개 filebeat agent에서 중복 수집)
- 최초 발생: 2026-04-17T17:23:30.643Z
- 최근 발생: 2026-04-17T17:23:30.643Z
- 영향 범위: Capture 682964 (팀 ebcinc, 시설 "Connect Cite - Station Aeroport du REM Project").
pano_postprocessor_state가done에서error로 전환되었으나, 실제 pano 처리는 이미 첫 번째 job(1020995)에서 정상 완료됨. 기능적 영향은 최종 상태가error로 남아 사용자에게 잘못된 실패 알림을 줄 수 있다는 점.
Root Cause Summary#
동일 capture 682964에 대해 pano postprocessor job이 21초 간격으로 2개 생성되었다 (job 1020995, job 1020997). 첫 번째 job 1020995가 정상적으로 모든 pano를 처리 완료하여 pano_postprocessor_state가 done으로 전환된 후, 늦게 시작된 두 번째 job 1020997이 실행되었다. PanoPostprocessorManager.getCPPanosByCaptureId()가 state !== 'done'인 pano만 필터링하므로, 이미 모든 pano가 done 상태여서 빈 배열이 반환되었고 "not found panos" 에러가 발생했다. 근본 원인은 CaptureInvoker.create_pano_postprocessor()에 중복 job 방지 로직이 없어 race condition이 발생한 것이다.
Technical Analysis#
Code Path#
- Job 생성 (Rails 측): Capture의
job_stopping_callback이 capture를finalizing상태로 전환하면,after_transition to: :finalizing콜백이run_pano_postprocessor를 호출한다.
ruby
# app/models/concerns/jobable/capture.rb:23-27
def job_stopping_callback(job)
run_callbacks(:job_stopping_callback) do
finalizing_state if job.update_jobable_state?
end
end
ruby
# app/models/concerns/statable/capture.rb:164-170
after_transition from: any, to: :finalizing do |capture, transition|
if capture.run_pano_postprocessor?
capture.run_pano_postprocessor
else
capture.jobs.stopping.each(&:stopped_state!)
end
end
- Pano postprocessor 실행 가능 여부 확인:
run_pano_postprocessor?는state가done/error가 아니고resource_state가uploaded인 pano가 존재하는지 확인한다.
ruby
# app/models/concerns/pano_postprocessor.rb:83-88
def run_pano_postprocessor?
return true if self.panos.untrashed.where.not(state: %i[done error]).where(resource_state: :uploaded).exists?
Cupix::Logger.info("No processable pano found for capture #{id}", class: self.class.name, function: __method__)
false
end
- Job 생성 — 중복 방지 없음:
create_pano_postprocessor는 기존 processing job 존재 여부를 확인하지 않고 무조건 새 job을 생성한다.
ruby
# app/invokers/capture_invoker.rb:106-130
def create_pano_postprocessor(opts = {})
params = {
jobable: @model,
user: self.current_user,
team: self.current_team
}
# ... task_revision, task_definition 처리 ...
job = CreatePanoPostprocessorJob.create!(params)
Cupix::Logger.info("Pano postprocessor job is created for capture #{self.model.id}. job_id: #{job.id}", ...)
self.model.log_trace_event(__method__.to_s)
job
end
중복 방지 메서드가 이미 존재하지만 호출되지 않음:
ruby
# app/models/concerns/invokable/capture.rb:81-85
def create_capture_pano_processor_invokable?
if self.jobs.processing.where(kind: 'create_pano_postprocessor').exists?
raise Cupix::Errors::Entity.new(code: 'ENT10000', reason: 'Capture has processing create_pano_postprocessor job')
end
end
- Job SQS 전송:
CreatePanoPostprocessorJob.run이 SQS로 메시지를 보내고pano_postprocessor_state를queued로 전환한다.
ruby
# app/jobs/create_pano_postprocessor_job.rb:19-26
def run
send_message
self.jobable.queued_pano_postprocessor_state
rescue => e
false
else
true
end
- Pano 목록 조회 (Node.js 측): Pano postprocessor service가 시작되면 API에서 pano 목록을 가져오되,
state === 'done'인 pano는 필터링한다.
typescript
// cupixworks/applications/agents/packages/cupix-pano-postprocessor/src/manager/pano-postprocessor-manager.ts:29-32
async getCPPanosByCaptureId(captureId: number): Promise<CPPano[]> {
const srvPanos = await this.cupixApi.pano.getAll(captureId);
return srvPanos.filter((srvPano) => srvPano.state !== 'done').map((srvPano) => new CPPano(srvPano));
};
- 에러 발생 지점: 필터링 후 빈 배열이면 에러를 throw한다.
typescript
// cupixworks/applications/agents/packages/cupix-pano-postprocessor/src/pano-postprocessor-service.ts:96-101
const cpPanos = await this.panoPostprocessorManager.getCPPanosByCaptureId(captureId);
if (cpPanos.length == 0) {
logger.error('PanoPostprocessorService::run | not found panos');
throw new Error('not found panos');
}
Race Condition 타임라인#
| 시간 (UTC) | 이벤트 |
|---|---|
| 17:18:19.532Z | Job 1020995 생성 (첫 번째 pano postprocessor job) |
| 17:18:19.533Z | pano_postprocessor_state: none -> queued |
| 17:18:38.037Z | pano_postprocessor_state: queued -> processing (job 1020995 시작) |
| 17:18:40.038Z | Job 1020997 생성 (두 번째 pano postprocessor job) — SQS cupix-tesla-ece-gpu 큐로 전송 |
| 17:18:40.039Z | pano_postprocessor_state: processing -> queued (두 번째 job이 상태를 되돌림) |
| 17:20:11.505Z | Job 1020995 완료 (success) |
| 17:20:12.144Z | pano_postprocessor_state: queued -> done |
| 17:23:29.719Z | Job 1020997 시작 (이미 모든 pano가 done 상태) |
| 17:23:29.884Z | pano_postprocessor_state: done -> processing (다시 되돌림) |
| 17:23:30.643Z | "not found panos" 에러 발생 |
| 17:23:31.885Z | pano_postprocessor_state: processing -> error |
Log Evidence#
Datadog 검색 쿼리:
text
service:cupixworks-pano-postprocessor-instance status:error @environment:production "PanoPostprocessorService::run"
에러 로그 (job 1020997):
text
17:23:30.643Z [ERROR] PanoPostprocessorService::run | not found panos
text
17:23:30.879Z [ERROR] PanoPostprocessorService::run | end - {"stack":"Error: not found panos
at PanoPostprocessorService.run (/tmp/agent/dist/app.cjs:6828:17)
at process.processTicksAndRejections (node:internal/process/task_queues:95:5)
at async PanoPostprocessorService.init (/tmp/agent/dist/app.cjs:6784:7)","message":"not found panos"}
Job 1020997 실행 흐름 (Datadog: service:cupixworks-pano-postprocessor-instance @job.id:1020997):
text
17:23:29.312Z [INFO] PanoPostprocessorService::init
17:23:29.313Z [INFO] PanoPostprocessorService::authenticate | begin
17:23:29.373Z [INFO] CupixAuth::setSession | session_id: 31baf8cea58937aec7c67e911484e605af811d89
17:23:29.374Z [INFO] PanoPostprocessorService::authenticate | end
17:23:29.374Z [INFO] PanoPostprocessorService::run | begin
17:23:30.643Z [ERROR] PanoPostprocessorService::run | not found panos
17:23:30.879Z [ERROR] PanoPostprocessorService::run | end - Error: not found panos
17:23:31.922Z [INFO] PanoPostprocessorService::terminateService | force shutdown after 10 seconds
정상 완료된 Job 1020995 (같은 capture 682964):
text
17:18:37.189Z [INFO] PanoPostprocessorService::init
17:18:37.360Z [INFO] PanoPostprocessorService::run | begin
17:18:39.658Z [INFO] Elapsed: download_panos 969ms
17:19:38.475Z [INFO] Elapsed: blur_panos 58816ms
17:19:40.295Z [INFO] Elapsed: change_masks 1820ms
17:19:49.419Z [INFO] Elapsed: infer_panos 9124ms
17:20:10.824Z [INFO] Elapsed: resize_panos 21405ms
17:20:17.533Z [INFO] PanoPostprocessorService::run | end (success)
API 측 중복 job 생성 확인 (Datadog: service:cupixworks-api "682964" "pano_postprocessor"):
text
17:18:19.532Z [INFO] Pano postprocessor job is created for capture 682964. job_id: 1020995
17:18:40.038Z [INFO] Pano postprocessor job is created for capture 682964. job_id: 1020997
Fix Recommendation#
즉시 조치 (Critical)#
app/models/concerns/pano_postprocessor.rb:50-55—run_pano_postprocessor메서드에서create_pano_postprocessor호출 전에 기존 processing/queued/created 상태의create_pano_postprocessorjob이 있는지 확인하는 guard를 추가해야 한다.- 이미 존재하는
create_capture_pano_processor_invokable?메서드(app/models/concerns/invokable/capture.rb:81-85)를create_pano_postprocessor호출 전에 연결하거나,run_pano_postprocessor시작 시점에서 중복 검사를 수행. pano-postprocessor-service.ts:98-101— 빈 pano 목록을 에러가 아닌 조기 정상 종료(early success)로 처리하도록 변경. 이미 모든 pano가done이면 중복 실행으로 간주하고 graceful하게 종료.
단기 개선 (1주 이내)#
app/invokers/capture_invoker.rb:106—create_pano_postprocessor메서드에 idempotency guard 추가. 동일 capture에 대해 active(created/queued/processing) 상태의create_pano_postprocessorjob이 이미 존재하면 새 job 생성을 건너뛰고 기존 job을 반환.app/jobs/create_pano_postprocessor_job.rb:19-21—run메서드에서 SQS 메시지 전송 전, capture의pano_postprocessor_state가 이미done이면 재전송하지 않도록 guard 추가.
장기 개선 (재발 방지)#
- Capture 처리 파이프라인에서 job 생성 시 전반적인 idempotency 패턴 도입.
create_pano_postprocessor,create_capture,create_refinement등 모든 job 생성 메서드에 동일한 중복 방지 로직 적용. - Pano postprocessor service(Node.js)에서
pano_postprocessor_state를 확인하여, 이미done상태면 처리를 건너뛰는 self-protection 로직 추가. - 두 번째 job이 실행되어
pano_postprocessor_state를done에서error로 되돌리는 문제는 상태 전환에 방향성 제약(예:done에서processing으로 역전환 금지)을 추가하여 방지.
Monitoring#
pano_postprocessor_state가done->processing->error로 역전환되는 패턴 감지:
text
service:cupixworks-api "pano_postprocessor_state" "from done to processing"
- 동일 capture에 대한 중복 pano postprocessor job 생성 감지:
text
service:cupixworks-api "Pano postprocessor job is created" | pattern
- Pano postprocessor "not found panos" 에러 빈도:
text
service:cupixworks-pano-postprocessor-instance status:error "not found panos"
Risk Assessment#
- Risk level: low — 실제 pano 처리는 첫 번째 job에서 정상 완료됨. 두 번째 job의 실패로 최종 상태가
error로 남는 것이 문제이나, capture 자체의 최종 상태(done)에는 영향을 주지 않을 가능성이 높음. - 예상 복잡도: standard — 중복 방지 guard 추가는 기존
create_capture_pano_processor_invokable?메서드를 활용할 수 있어 구현이 비교적 단순하지만, state machine 전환 로직 변경은 테스트가 필요.