ES /docs

TransferManager::failTask - path: /tmp/workspace/678511/original_panos/80668710.jpg, url: http://api

RCA: TransferManager::failTask - pano download 404

Error Log#

Datadog Logs

text
TransferManager::failTask - path: /tmp/workspace/678511/original_panos/80668710.jpg, url: http://api-tesla.cupix.internal/api/v1/panos/80668710/download?original=true, count: 5/5

Impact#

  • Service: cupixworks-capture-singleshot-agent
  • 발생 횟수: 1
  • 최초 발생: 2026-04-09T21:20:43.279Z
  • 최근 발생: 2026-04-09T21:20:43.279Z

Root Cause Summary#

Pano 80668710의 원본 이미지가 S3에 업로드되지 않은 상태(resource_state: "created", filesize: null)에서 singleshot agent가 해당 pano의 원본 이미지를 다운로드하려고 시도했다. Tesla API(PanosController#download_single_resource)는 resource의 revision을 기반으로 S3 presigned URL을 생성하여 302 redirect를 반환했으나, 실제 S3 객체가 존재하지 않아 404가 반환되었다. CupixAuth::handleError가 S3 404 응답을 구조화되지 않은 에러 객체로 감싸면서 checkStatusCode에서 statusCode를 인식하지 못해, 4xx 에러임에도 5회 재시도 후 최종 실패했다.

Technical Analysis#

Code Path#

  • Entry point: SingleshotService::run (singleshot-service.ts:37) → runAlignScript (singleshot-service.ts:67)
  • runAlignScriptrunAlign을 호출 (align/align.module.ts:24)
  • runAlign 내에서 각 pano에 대해 downloadFile을 호출 (align.module.ts:61-62)
  • 다운로드 URL: ${cupixAuth.apiUrl}/panos/${panoId}/download?original=true

참고: 현재 develop 브랜치의 align.module.ts는 자체 downloadFile 함수 (line 347-373)를 사용하지만, 프로덕션 배포 버전은 TransferManager를 사용하는 것으로 확인됨 (로그에 TransferManager::failTask, TransferManager::downloadFile 메시지 존재, original_panos 디렉토리 사용). 현재 코드에서는 InputPanoDirName = 'input_panos'를 사용하지만, OriginalPanoDirName = 'original_panos'가 상수로 정의되어 있어 배포된 코드가 이 상수를 사용하는 것으로 판단.

API download flow — Tesla API가 302 redirect를 통해 S3 presigned URL로 전달:

ruby
# app/controllers/concerns/single_resourcable/panos_controller.rb:7-11
def download_single_resource
  stitched, original = parse_download_params
  redirect_to @resource.download_url(ver: @model.get_revision_by_download_option([stitched, original])), allow_other_host: true
end

original=true일 때 revision 계산:

ruby
# app/models/concerns/resourcable/pano.rb:106-118
def get_revision_by_download_option(options = [])
  stitched = options[0]
  original = options[1]
  # ...
  if original == true
    self.initial_revision + 1  # initial_revision=0 → revision=1 → S3 key .../v1
  else
    nil
  end
end

S3 presigned URL 생성:

ruby
# app/models/concerns/storagable/resource.rb:185-205
def download_url(opts = {})
  ver = opts[:ver] || self.revision
  # ...
  self.object(ver).presigned_url(:get, expires_in: exp, response_content_disposition: "filename=#{CGI.escape(filename) rescue nil}")
end

TransferManager retry flow (배포된 버전, 현 코드의 preprocessor agent와 동일 패턴):

typescript
// transfer.manager.ts:170-172 (preprocessor agent 기준)
private checkStatusCode = (error: any): boolean => {
  if (error?.statusCode != undefined && error.statusCode > 400 && error.statusCode < 500) return false;
  return true;  // statusCode가 undefined이면 retry 허용
};

CupixAuth::handleError가 S3 404 응답을 문자열화된 JSON으로 감싸 에러 객체에 statusCode 프로퍼티가 직접 설정되지 않아 checkStatusCodetrue를 반환, 404임에도 재시도가 진행됨.

Log Evidence#

Datadog 쿼리:

text
service:cupixworks-capture-singleshot-agent status:error 80668710
Time: 2026-04-09T20:20:43Z to 2026-04-09T21:50:43Z

다운로드 시도 타임라인 (6회 시도, ~10초 간격으로 재시도):

text
06:19:51 KST - TransferManager::downloadFile - response path: /tmp/workspace/678511/original_panos/80668710.jpg, code: 404, message: Not Found
06:20:02 KST - TransferManager::downloadFile - response path: /tmp/workspace/678511/original_panos/80668710.jpg, code: 404, message: Not Found
06:20:12 KST - TransferManager::downloadFile - response path: /tmp/workspace/678511/original_panos/80668710.jpg, code: 404, message: Not Found
06:20:22 KST - TransferManager::downloadFile - response path: /tmp/workspace/678511/original_panos/80668710.jpg, code: 404, message: Not Found
06:20:33 KST - TransferManager::downloadFile - response path: /tmp/workspace/678511/original_panos/80668710.jpg, code: 404, message: Not Found
06:20:43 KST - TransferManager::downloadFile - response path: /tmp/workspace/678511/original_panos/80668710.jpg, code: 404, message: Not Found
06:20:43 KST - TransferManager::failTask - path: /tmp/workspace/678511/original_panos/80668710.jpg, url: http://api-tesla.cupix.internal/api/v1/panos/80668710/download?original=true, count: 5/5

API 측 로그 — 모든 요청에 302 redirect 정상 반환:

text
Datadog query: service:cupixworks-api "80668710"
Time: 2026-04-09T20:20:43Z to 2026-04-09T21:50:43Z

06:19:52 KST - [302] GET /api/v1/panos/80668710/download (Api::V1::PanosController#download_single_resource)
06:20:03 KST - [302] GET /api/v1/panos/80668710/download (Api::V1::PanosController#download_single_resource)
06:20:12 KST - [302] GET /api/v1/panos/80668710/download (Api::V1::PanosController#download_single_resource)
06:20:24 KST - [302] GET /api/v1/panos/80668710/download (Api::V1::PanosController#download_single_resource)
06:20:33 KST - [302] GET /api/v1/panos/80668710/download (Api::V1::PanosController#download_single_resource)
06:20:43 KST - [302] GET /api/v1/panos/80668710/download (Api::V1::PanosController#download_single_resource)

S3 404 에러 상세 (CupixAuth::handleError 로그에서):

json
{
  "statusCode": 404,
  "headers": {
    "server": "AmazonS3",
    "x-amz-request-id": "308Q51CF0EVPZ140"
  },
  "request": {
    "uri": {
      "pathname": "/cupixworks-source-dc9dcff32488-usea1/resources/oty8x8/usea1/v1",
      "hostname": "s3.amazonaws.com"
    },
    "method": "GET",
    "headers": {
      "referer": "http://api-tesla.cupix.internal/api/v1/panos/80668710/download?original=true"
    }
  }
}

S3 경로 resources/oty8x8/usea1/v1에 객체가 존재하지 않음.

Kibana 데이터베이스 확인:

  • Pano 80668710: state: "created", resource_state: "created", filesize: null — 원본 이미지가 S3에 업로드되지 않음
  • Capture 678511: state: "done", singleshot_state: "stopped", creation_platform: "app", stat.size.pano_original_images: 0
  • Capture의 editing_state: "escalated" — 문제로 인해 에스컬레이션됨

Fix Recommendation#

즉시 조치 (Critical)#

  • 파일: app/controllers/concerns/single_resourcable/panos_controller.rb:7-11 (tesla)
  • download_single_resource 메서드에서 @resource의 상태를 확인하여, resource가 아직 업로드되지 않은 상태(created)일 때 404 또는 적절한 에러 응답을 반환하도록 변경. 현재는 revision == 0일 때만 체크하지만 (single_resourcable_controller.rb:11-13), revision이 계산된 값(initial_revision + 1)일 때 실제 S3 객체 존재 여부와 무관하게 302 redirect를 반환함.
  • 파일: TransferManager의 checkStatusCode 로직 (배포된 singleshot agent 버전)
  • CupixAuth::handleError가 반환하는 에러 객체에서 statusCode를 올바르게 추출하도록 수정. 현재 S3 404 응답이 문자열로 감싸져 statusCode가 인식되지 않아 불필요한 재시도가 발생.

단기 개선 (1주 이내)#

  • Singleshot agent의 pano 다운로드 전에 pano의 resource_state를 API를 통해 확인하고, 업로드 완료되지 않은 pano는 건너뛰거나 적절한 에러 처리를 하도록 로직 추가.
  • download_single_resource에서 S3 presigned URL을 생성하기 전에 해당 revision의 객체가 실제로 존재하는지 확인하는 방어 로직 추가 (또는 resource state가 done인지 검증).

장기 개선 (재발 방지)#

  • Pano 리소스 업로드 파이프라인에서 업로드 완료 여부를 보장하는 상태 머신 검증 강화. resource_state: "created"인 pano가 processing pipeline에 진입하지 않도록 capture의 processing 시작 조건에 리소스 상태 체크 추가.
  • Agent의 에러 핸들링 통합 — CupixAuth::handleError가 반환하는 에러 객체에 항상 statusCode를 포함하도록 표준화하여, checkStatusCode가 4xx 에러를 올바르게 식별하고 불필요한 재시도를 방지.

Monitoring#

  • resource_state: "created"인 pano에 대한 다운로드 시도를 감지하는 모니터 추가:
text
service:cupixworks-capture-singleshot-agent status:error "TransferManager::failTask" "404"
  • Pano 리소스 상태 이상 감지:
text
service:cupixworks-api "download_single_resource" status:error

Risk Assessment#

  • Risk level: low
  • 예상 복잡도: standard