ES /docs

PanoPostprocessorService::run | mask pano id:90201594 | error {"stack":"HttpError: HTTP request fail

RCA: PanoPostprocessorService::run mask pano HttpError 400

Overview#

What Happened#

2026-06-26 09:47~09:48 KST 사이 cupixworks-pano-postprocessor-instance (us-west-2)에서 PanoPostprocessorService::run의 mask 단계가 22건의 pano에 대해 HttpError statusCode:400으로 실패했다. 모든 실패는 panoApi.js:375 (SDK checkMaskUploading 호출)에서 발생하며, response body 가 빈 {} 로 직렬화된 채 throw 된다. 실패한 pano 들은 panoPostprocessorManager.updatePanoState(...Error)로 표시되었지만 전체 capture 흐름은 중단되지 않고 진행되었다.

Quick Facts#

Field Value
exception.class HttpError
exception.message HTTP request failed
top_frame node_modules/@tesla/typescript-node-sdk/api/panoApi.js:375:40
failing call cupixApi.pano.checkMaskUploading(panoId, maskType) (PUT /panos/{id}/check_mask_uploading)
caller MaskWork.changeMaskpano-postprocessor-service.ts:135
runtime Node SDK @tesla/typescript-node-sdk@1.13.3-SNAPSHOT.202605110829
env production, us-west-2

Affected Teams#

Team / Domain Error Count Impact
rsconstruction / pano postprocessing 22 22개 pano 가 mask 단계에서 실패하여 pano.state = Error로 기록됨. 동일 capture 내 다른 단계(infer/resize)는 계속 진행되어 capture 자체는 완료될 수 있으나, 해당 pano 들은 mask 미적용 상태가 된다.

Timeline#

  1. 2026-06-26 09:46:59 KSTMaskWork::maskPanosface-body-detector python script 가 UserWarning: No PYTORCH_KERNEL_CACHE_PATH or HOME environment variable set! 를 stderr 로 출력 (script 자체는 계속 실행).
  2. 2026-06-26 09:47:07 KST — 동일 script 가 RuntimeWarning: overflow encountered in exp 를 출력 (logits → sigmoid 변환 시 overflow). 이 warning 들은 fatal 이 아니지만 일부 pano 의 mask 결과 품질에 영향 가능.
  3. 2026-06-26 09:47:43 KST — 첫 mask pano id:90201594 | error ... statusCode:400 발생 (cluster first_seen).
  4. 2026-06-26 09:47:48 ~ 09:48:39 KST — 동일 패턴의 400 에러가 21건 추가 발생 (pano id 90201594, 90201496, 90201494, 90201401, 90201398, 90201397, 90201305, 90201303, 90201216, 90201092, 90201086, 90201088, 90200811, 90200326, 90200324, 90200315, 90200072, 90200073, 90199912, 90199913, 90199914, 90199915 등).
  5. 2026-06-26 09:48:02 KST — 한 인스턴스에서 pano.postprocess.summary&dd_total:971&dd_completed:971&dd_errored:0 출력 (다른 capture instance 는 정상 종료). 실패는 다른 capture instance 들에 국한됨.

Error Log#

Datadog Logs

representative errortext
PanoPostprocessorService::run | mask pano id:90201594 | error {"stack":"HttpError: HTTP request failed
    at Request._callback (/tmp/agent/dist/node_modules/.pnpm/@tesla+typescript-node-sdk@1.13.3-SNAPSHOT.202605110829_c0aaa38c9a9fff396d4124c32a1f39fa/node_modules/@tesla/typescript-node-sdk/api/panoApi.js:375:40)
    at self.callback (/tmp/agent/dist/node_modules/.pnpm/request@2.88.2/node_modules/request/request.js:185:22)
    at Request.emit (node:events:524:28)
    at Request.<anonymous> (/tmp/agent/dist/node_modules/.pnpm/request@2.88.2/node_modules/request/request.js:1154:10)
    at IncomingMessage.<anonymous> (/tmp/agent/dist/node_modules/.pnpm/request@2.88.2/node_modules/request/request.js:1076:12)
    at IncomingMessage.emit (node:events:536:35)","message":"HTTP request failed","response":{"body":{},"statusCode":400},"body":{},"statusCode":400,"name":"HttpError"}

Impact#

  • Service: cupixworks-pano-postprocessor-instance
  • Team: rsconstruction
  • 발생 횟수: 22
  • 최초 발생: 2026-06-26 09:47:43 KST
  • 최근 발생: 2026-06-26 09:48:39 KST

22개 pano 가 pano.state = Error로 기록된다. 사용자 가시적 영향: 해당 pano 들은 face/body mask 처리가 누락된 상태로 capture 에 남는다. mask 가 누락된 pano 는 후속 viewer/review 단계에서 face blur 처리가 안 된 원본이 노출될 수 있어 프라이버시 측면에서 follow-up 이 필요하다.

Root Cause Summary#

pano-postprocessor-service.ts:133MaskWork.changeMask 가 호출하는 cupixApi.pano.checkMaskUploading (PUT /panos/{id}/check_mask_uploading) 가 HTTP 400 을 반환한다. 서버측 MaskableRepository#check_mask_uploadingmask.mask_uploaded? (S3 object 존재 여부) 가 false 일 때 Cupix::Errors::InvalidState (code: STAT10000, "Mask does not uploaded") 를 raise 하며 이 예외는 ClientErrorController#client_400_error 핸들러에서 400 으로 매핑된다. 즉 직전 단계인 awsS3Manager.uploadBySignedUrl(mask.uploadUrl, cpPano.maskImagePath) 가 실제로는 S3 에 mask 이미지를 올리지 못했거나 (maskImagePath 는 존재하지만 0-byte / 손상 상태), PUT 자체는 200 으로 응답했지만 S3 가 객체를 영속화하기 전에 후속 checkUploading 이 호출되었기 때문에 객체 lookup 이 실패한 것이다. (master 의 aws-s3.manager.ts:98-101if (response.status !== 200) return; 분기를 가지지만, axios 의 기본 validateStatus 는 4xx/5xx 를 reject 하므로 이 분기는 실제로 도달 불가능이다 — Revision 2 에서 정정.) MaskWork.maskPanos 스크립트가 동일 시간대에 RuntimeWarning: overflow encountered in expNo PYTORCH_KERNEL_CACHE_PATH warning 을 출력한 것도 일부 pano 에 대해 mask 결과 PNG 생성 자체가 불완전했을 가능성을 시사한다.

Technical Analysis#

Code Path#

Entry point: packages/cupix-pano-postprocessor/src/pano-postprocessor-service.ts:130-141

packages/cupix-pano-postprocessor/src/pano-postprocessor-service.ts:130-141typescript
const maskTasks = cpPanos.map((cpPano) => {
    return PARALLEL_TASK_LIMIT(async () => {
        try {
            await this.maskWork.changeMask(cpPano, maskTypeOption);
        } catch (error) {
            logger.error('PanoPostprocessorService::run | mask pano id:%d | error %s', cpPano.panoId, stringifyError(error));
            await this.panoPostprocessorManager.updatePanoState(cpPano.panoId!, TESLA.UpdatePanoRequest.StateEnum.Error);
            erroredPanoIds.add(cpPano.panoId!);
        }
    });
});
await Promise.all(maskTasks);

changeMask 의 3-step 흐름:

packages/cupix-pano-postprocessor/src/work/mask-work.ts:90-100typescript
async changeMask(cpPano: CPPano, maskType: 'face' | 'facebody') {
    if (!cpPano.panoId || !cpPano.maskImagePath || !fs.existsSync(cpPano.maskImagePath)) return;

    const mask = await this.createMask(cpPano.panoId, maskType);

    if (!mask) return;

    await this.awsS3Manager.uploadBySignedUrl(mask.uploadUrl, cpPano.maskImagePath);  // (1) S3 PUT
    await this.checkUploading(cpPano.panoId, maskType);                                // (2) FAILS 400
    await this.updateMaskType(cpPano.panoId, mask.maskId);
}

문제의 silent swallow:

packages/cupix-pano-postprocessor/src/manager/aws-s3.manager.ts:86-106typescript
async uploadBySignedUrl(signedUrl: string, imagePath: string) {
    const agent = new https.Agent({ keepAlive: true });
    const fileSize = fs.statSync(imagePath).size;
    // ...
    const fileStream = fs.createReadStream(imagePath);
    const response = await axios.put(signedUrl, fileStream, {
        httpsAgent: agent,
        headers: { 'Content-Type': 'application/octet-stream', 'Content-Length': fileSize }
    });

    if (response.status !== 200) {
        logger.error('AwsS3Manager::uploadBySignedUrl | upload fail - %s %s', imagePath, JSON.stringify(response.statusText));
        return;   // ← non-200 인데 throw 하지 않고 silent return
    }
    // ...
    return true;
}

Failure point — SDK 호출 지점:

node_modules/@tesla/typescript-node-sdk/api/panoApi.js:355-381 (checkMaskUploading)javascript
return interceptorPromise.then(() => {
    // ...
    const requestPromise = () => new Promise((resolve, reject) => {
        request_1.default(localVarRequestOptions, (error, response, body) => {
            if (error) { reject(error); }
            else {
                body = models_1.ObjectSerializer.deserialize(body, "MaskResponse");
                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 375
                }
            }
        });
    });
    return this.cupixRetriableRequest(requestPromise);
});

서버측 400 발생 지점:

app/repositories/concerns/maskable_repository.rb:4-49ruby
def check_mask_uploading(mask_type)
  _mask_type = mask_type.presence || 'custom'
  mask = @model.masks.find_by_mask_type(_mask_type)

  raise Cupix::Errors::NotFound.new(code: 'ARG10002', reason: "Mask not found. mask_type: #{mask_type}") if mask.nil?

  if mask.mask_uploaded?(mask_revision: mask.mask_upload_revision)
    mask.uploaded
    # ... happy path: transition to uploaded, save, return mask ...
  else
    mask.missing
    @model.missing_mask_state!
    raise Cupix::Errors::InvalidState.new(code: 'STAT10000', reason: 'Mask does not uploaded')  # ← 400 raise
  end
  # ...
end

400 매핑:

app/controllers/concerns/client_error_controller.rb:7-14, 61-63ruby
rescue_from Cupix::Errors::Unknown,
            Cupix::Errors::Resource,
            Cupix::Errors::Session,
            Cupix::Errors::Parameter,
            Cupix::Errors::Entity,
            Cupix::Errors::Billing,
            Cupix::Errors::InvalidState,
            Cupix::Errors::Siteinsights, with: :client_400_error

def client_400_error(exception)
  raise_error(400, exception)
end

기대 동작: uploadBySignedUrl 가 PUT 성공 후 S3 object 가 존재하므로 mask_uploaded? 가 true 를 반환하고 check_mask_uploading 가 200 으로 응답. 실제 동작: uploadBySignedUrl 가 실패했거나 (non-200, 또는 0-byte stream, 또는 axios timeout/abort) 정상 종료했지만 S3 가 객체를 가지고 있지 않은 상태에서 check_mask_uploading 호출 → 서버가 S3 lookup 실패 → 400.

Log Evidence#

Datadog query (재현용):

text
service:cupixworks-pano-postprocessor-instance status:error "mask pano"
text
service:cupixworks-pano-postprocessor-instance
from: 2026-06-26T00:45:00Z to: 2026-06-26T00:50:00Z

핵심 로그 (시간 흐름):

2026-06-26 09:46:59 KSTtext
MaskWork::maskPanos | script /tmp/lib/face-body-detector/sam3/model/encoder.py:358: UserWarning: No PYTORCH_KERNEL_CACHE_PATH or HOME environment variable set! This disables kernel caching.
2026-06-26 09:47:07 KSTtext
MaskWork::maskPanos | script /tmp/lib/face-body-detector/cupix_inference.py:266: RuntimeWarning: overflow encountered in exp
  sigmoid_mask = 1.0 / (1.0 + np.exp(-logits_resized))
2026-06-26 09:47:37~09:48:16 KST (다양한 capture instance)text
PanoPostprocessorService Elapsed Time - dd_step:blur_panos&dd_elapsed_time:1908158ms
PanoPostprocessorService Elapsed Time - dd_step:blur_panos&dd_elapsed_time:667008ms
PanoPostprocessorService Elapsed Time - dd_step:blur_panos&dd_elapsed_time:665051ms

22건의 동일 stack trace 에러 중 대표:

error log payloadjson
{
  "message": "PanoPostprocessorService::run | mask pano id:90201594 | error ...",
  "response": { "body": {}, "statusCode": 400 },
  "body": {},
  "statusCode": 400,
  "name": "HttpError"
}

body 가 빈 {} 인 것은 cupixworks-api 가 비어있는 응답을 보냈기 때문이 아니라 SDK 가 응답을 models_1.ObjectSerializer.deserialize(body, "MaskResponse") 로 변환하면서 (panoApi.js:370) MaskResponse 스키마에 없는 code/reason/message 필드를 제거했기 때문이다. 따라서 로그에서 정확한 STAT10000 코드/메시지가 직접 보이지 않는다 — uncertain (실제 응답 body 검증을 위해 cupixworks-api 측 access log 가 필요하나 동일 시간대 service:cupixworks-api 검색으로는 매칭되는 항목을 찾지 못함, 14일 기준).

cupixworks-api 측 상관 로그 부재:

cross-service searchtext
service:cupixworks-api status:(error OR warn) (90201594 OR 90199912)
Found 0 logs

→ api 가 400 으로 거절했더라도 error level 로 남기지 않았거나 별도 access log 채널을 통해 기록되었음. 이는 STAT10000 raise 가 통제된 흐름 (rescue_from → 400) 으로 처리되어 application-level error log 가 남지 않는 동작과 부합한다.

Hypotheses Considered#

# Hypothesis Evidence for Evidence against Verdict
H1 checkMaskUploading 시점에 S3 에 mask 객체가 없어 MaskableRepository#check_mask_uploadingInvalidState (STAT10000) 를 raise → 400. 원인은 직전 uploadBySignedUrl 가 실패했음에도 silent return 한 것 (또는 PUT 자체가 timeout/abort). (a) 서버 코드에서 mask_uploaded? false 시 STAT10000 raise, ClientErrorController 가 STAT10000 → 400 매핑 (maskable_repository.rb:42-46, client_error_controller.rb:13-14). (b) aws-s3.manager.ts:99-100 가 non-200 응답에 throw 없이 return. (c) face-body-detector python script 가 동일 시간대 RuntimeWarning(overflow) 출력 — 일부 mask PNG 가 비정상일 가능성. (d) 동일 stack trace 22건 모두 panoApi.js:375 (checkMaskUploading) 에서 발생. response body 가 SDK 직렬화로 비워져 STAT10000 코드를 로그에서 직접 확인 불가 — uncertain, but 매핑 로직으로 가장 정합 Confirmed (primary)
H2 createMaskUploadUrl (mask_upload_url repository) 가 uploading_mask_state! 에서 StateMachines::InvalidTransition 을 raise → 400. mask_state 가 special state 일 때 이론적으로 가능 (a) state machine 정의상 event :uploading do transition any => :uploading end 로 어떤 상태에서도 :uploading 으로 전이 허용 (statable/pano.rb:196-198). (b) SDK 호출 stack 의 line 375 deserialization 타입이 MaskResponse 인 메서드는 createMaskUploadUrlcheckMaskUploading 둘 다 해당하지만, changeMask 흐름에서 createMask 가 정상 반환된 후 uploadBySignedUrl 가 실행됐다면 mask 객체가 만들어진 상태이므로 후속 check 단계가 실패 지점. Rejected
H3 panoApi.js:375update(panoId, { mask_id }) (mask-work.ts:99) 의 실패. changeMask 의 세 번째 await (a) update SDK 메서드의 reject line 은 다른 위치이며 deserialize 타입이 PanoResponse 인 것이 일반적. (b) 22건 모두 동일 line 375 + MaskResponse 직렬화 흐름. Rejected
H4 cupixworks-api 다운/디플로이/rate-limit 등 외부 dependency 이슈. status-board 조회 결과 svc:cupixworks-pano-postprocessor-instance::unknown 에 active incident 없음; service:cupixworks-api status:error 동일 시간대에 mask 관련 에러 없음; 동일 인스턴스 다른 capture 들은 dd_errored:0 으로 성공 종료. Rejected

Fix Recommendation#

즉시 조치 (Critical)#

  • origin/develop 적용 우선 고려 — develop 에는 이 클러스터의 업로드/마스크 경로를 직접 건드리는 미머지 커밋이 있다 (origin/master..origin/develop 비교 결과). 자세한 효과 분석은 아래 "develop 적용 효과 분석" 참조. 운영 적용 가능 시 (a) TSLA-13233 (567dd4802) 의 uploadBySignedUrl 재구현 + 재시도, (b) TSLA-12759 (add674797) 의 getCPPanosByCaptureId 필터, (c) mask-work.ts 의 stderr 캡쳐 + Error reject 를 한 번에 가져온다.
  • applications/agents/packages/cupix-pano-postprocessor/src/manager/aws-s3.manager.ts:91-99 (master)axios.put 응답 처리: master 의 if (response.status !== 200) return; 분기는 사실상 도달 불가다 (axios 의 validateStatus 기본값은 4xx/5xx 를 reject 함). 따라서 "silent swallow" 는 4xx/5xx 자체에 대해서는 일어나지 않지만, mid-stream EPIPE/ECONNRESET 같은 transient 네트워크 오류 는 axios 가 즉시 throw 하여 retry 없이 실패로 끝난다. develop 의 @agents/base/uploadFile 는 이를 3회 지수 백오프로 재시도한다 (packages/base/src/util/transfer.ts:11-33).
  • applications/agents/packages/cupix-pano-postprocessor/src/work/mask-work.ts:97-98 (master): uploadBySignedUrl 가 정상 throw 한 경우 이미 changeMask 의 catch 로 흘러간다. 그러나 mask PNG 가 0-byte 거나 (filesystem 에는 존재) PUT 자체는 200 으로 끝났지만 S3 가 객체를 가지고 있지 않은 보기 드문 케이스에서는 checkUploading 이 400 으로 반환된다. 이 경로에 대한 보강이 별도로 필요하다 — fs.statSync(maskImagePath).size > 0 pre-check 를 추가하라.

단기 개선 (1주 이내)#

  • face-body-detector python script 의 RuntimeWarning: overflow encountered in exp 처리: cupix_inference.py:266 의 sigmoid 계산을 scipy.special.expit 또는 logits clipping 으로 변경하여 NaN/비정상 mask PNG 가 생성되지 않도록 한다 (uncertain — 실제 PNG 출력 손상 여부는 별도 검증 필요).
  • MaskWork.changeMaskfs.existsSync(cpPano.maskImagePath) 체크에 file size > 0 검증을 추가 (fs.statSync(...).size > 0). 0-byte mask 가 S3 에 올라가도 check_mask_uploading 은 객체 존재만 보므로 통과할 수 있어 별도 검증 가치 있음.
  • panoApi.js:370 직렬화로 인해 4xx 응답 body 가 사라지는 문제 — @tesla/typescript-node-sdk 의 reject path 에서 error response body 는 raw 로 전달하도록 codegen template 조정 (cupix-api repo). 현재 운영 디버깅 시 STAT10000 같은 코드가 보이지 않아 RCA 가 어려워진다.

장기 개선 (재발 방지)#

  • agents 의 fix logger convention 에 맞춰 stringifyError(error) 대신 logger 에 Error 객체를 직접 넘기는 패턴으로 통일 (memory 노트 참조 — agents 코드베이스는 fix logger 가 Error 객체를 native 로 처리).
  • cupixApi.pano.checkMaskUploading 에 대한 짧은 retry/backoff 추가 (이미 cupixRetriableRequest 가 있으므로 STAT10000 같은 일시적 객체 미존재 케이스에 한해 1-2회 retry). 단, retry 가 silent S3 PUT 실패를 가리지 않도록 H1 의 즉시 조치를 먼저 적용해야 한다.
  • pano 단위 mask 실패에 대한 dashboard/alert (아래 Monitoring 참조).

develop 적용 효과 분석#

origin/master..origin/developapplications/agents/packages/cupix-pano-postprocessor/ 디렉토리에는 5개 파일에 걸친 변경이 미머지 상태로 존재한다 (commits: 567dd4802, add674797, 60faccf97, ef4029946, f2f3f24e6, eb940111f, b849d81e6, 8fbf55736, ...).

커밋 변경 요약 이번 incident 와의 관련성
567dd4802 TSLA-13233 — unify S3/signed-URL uploads onto @agents/base retry infra uploadBySignedUrl@agents/base/util/transfer.ts:55-63 uploadFile 호출로 교체됨. interceptor 가 ECONNRESET/ETIMEDOUT/ECONNABORTED/EPIPE/EAI_AGAIN 및 5xx 에 대해 3회 지수 백오프 재시도 (base delay 1000ms), 요청 타임아웃 60s. 실패 시 throw error 로 명시 전파. 부분 도움 — Datadog 에서 동일 시간대 ECONNRESET/ETIMEDOUT/EPIPE 검색 결과 0건 (service:cupixworks-pano-postprocessor-instance 14일 기준). 이번 incident 는 transient 네트워크 실패로 보이지 않아 retry 가 직접 해결하지는 않는다. 다만 향후 재발 방지 측면에서 가치 있음.
add674797 TSLA-12759 — Filter panos with resource_state !== Uploaded in getCPPanosByCaptureId pano-postprocessor-manager.ts:31-38 에서 source image 가 S3 에 완전히 올라가지 않은 pano 를 처리 대상에서 제외. (Mask 결과가 0-byte/손상되어 후속 checkUploading 에서 400 이 발생하는 케이스 차단) 유의미 — 이번 incident 는 22개 pano 가 동시에 같은 시간대에 실패. 한 capture 의 다수 pano 가 동시에 mask 단계에서 400 을 받았다면, 일부는 source image 가 미완성 업로드 상태였을 가능성이 있다. 단, source image 의 resource_state 를 직접 확인하려면 Kibana 검증이 필요 (이번 revision 에서는 수행하지 않음).
mask-work.ts stderr 캡쳐 + reject(new Error(message)) 마스크 스크립트 stderr 누적 후, 종료 코드가 0 이 아닐 때 마지막 500 자를 포함한 Error 로 reject. 기존 reject(false) 는 caller 의 catch 가 의미있는 메시지를 받지 못함. 진단 개선 — 이번 incident timeline 의 RuntimeWarning: overflow encountered in exp 가 종료 코드 != 0 으로 이어졌는지, 단순 warning 이었는지 구분할 수 있게 된다. RCA 가 좀 더 빨라짐.
ef4029946/8fbf55736/36f7326b8 TSLA-12479 — AWS SDK v2 → v3 마이그레이션 download / uploadDirectoryByCredentialS3Client + Upload (@aws-sdk/lib-storage) 로 전환. uploadBySignedUrl 자체는 axios PUT 이므로 영향 없음. 이번 incident 와 무관 — 이번 실패 지점은 uploadBySignedUrl (signed URL PUT). download/credential upload 경로가 아니다.
60faccf97 — bucket_region for S3 client signing region credential 기반 업로드의 signing region 수정. 이번 incident 와 무관 — signed URL PUT 은 region 결정을 S3 측 URL host 가 담당.

결론: develop 을 그대로 master 에 가져오면 (1) signed-URL 업로드의 transient 네트워크 실패에 대한 자동 retry 가 생기고, (2) source image 가 미완성 업로드인 pano 는 mask 단계에 진입조차 하지 않으며, (3) mask 스크립트가 비정상 종료할 경우 실제 에러 메시지가 로그/예외에 남게 된다. 이번 incident 의 22건 중 transient 네트워크 케이스는 로그상 0건이므로 (1) 의 즉효는 제한적이지만, (2) 가 root cause 후보 중 "S3 에 source image 가 미완성 업로드인 pano 가 처리됨" 시나리오를 차단하고, (3) 가 향후 동일 증상의 RCA 속도를 크게 단축시킨다 — 따라서 개선 효과는 있지만 incident 의 모든 케이스를 자동으로 해결한다고 단정할 수는 없다. 추가로, 마스크 PNG 의 file size > 0 검증이 develop 에도 누락되어 있어 0-byte mask 시나리오는 develop 적용 후에도 별도 패치가 필요하다.

Monitoring#

  • Datadog metric: pano.postprocess.summarydd_errored 가 0 이 아닌 capture 비율을 추적.
  • 로그 기반 알람: 다음 쿼리 결과가 임계치를 넘으면 알림.
text
service:cupixworks-pano-postprocessor-instance status:error "mask pano"
text
service:cupixworks-pano-postprocessor-instance "AwsS3Manager::uploadBySignedUrl | upload fail"
text
service:cupixworks-pano-postprocessor-instance "MaskWork::maskPanos" "RuntimeWarning"

(각 쿼리는 release dashboard timeseries widget 에 그대로 입력 가능 — pipe/stats 문법 미사용)

Risk Assessment#

  • Risk level: medium — 단일 capture 의 mask 단계가 부분 실패하면 face/body blur 가 누락된 pano 가 production 에 노출될 수 있다 (privacy 영향). 다만 전체 capture pipeline 이 중단되지는 않음.
  • 예상 복잡도: standardorigin/develop 의 관련 커밋 (TSLA-13233, TSLA-12759, mask-work stderr 캡쳐) 을 master 로 가져오는 작업이 가장 자연스러운 즉시 조치이며, 잔여 위험 (0-byte mask PNG, sigmoid overflow) 은 별도 패치가 필요.

Revision History#

Revision 1#

Feedback: "origin/develop 에 적용된걸 적용하면 업로드 문제가 개선될까" — develop 에 머지된 변경을 master 로 가져오면 이번 업로드 실패가 개선되는지 확인 요청.

판정:

피드백 항목 판정 근거
develop 의 변경 적용 시 업로드 문제 개선 여부 부분 수용 origin/master..origin/developapplications/agents/packages/cupix-pano-postprocessor/ 에는 5개 파일 변경이 미머지로 존재한다. (a) TSLA-13233 567dd4802: uploadBySignedUrl@agents/base/util/transfer.ts:55-63 uploadFile 로 교체되어 ECONNRESET/ETIMEDOUT/ECONNABORTED/EPIPE/EAI_AGAIN/5xx 에 대해 3회 지수 백오프 retry + 60s timeout 적용 (packages/base/src/util/transfer.ts:5-33). (b) TSLA-12759 add674797: getCPPanosByCaptureIdresource_state !== Uploaded pano 를 처리 대상에서 제외 (pano-postprocessor-manager.ts:31-38). (c) mask-work.ts 가 child process stderr 를 누적해 Error(message) 로 reject. → 업로드 신뢰성 (retry) 과 진단성 (stderr 캡쳐) 은 개선되지만, Datadog 검색 결과 동일 시간대 ECONNRESET/ETIMEDOUT/EPIPE/socket hang up 0 건 (service:cupixworks-pano-postprocessor-instance 14일 기준) — 이번 22건의 400 이 transient 네트워크 실패였다는 직접 증거는 없다. 따라서 retry 가 이번 incident 의 모든 케이스를 자동으로 해결하지는 않는다. (b) 의 source-image 미완성 업로드 필터는 root cause 후보 중 하나를 차단하지만 별도 Kibana 검증이 필요하다. develop 에도 mask PNG file size > 0 검증은 누락되어 있으므로 0-byte mask 시나리오는 develop 적용 후에도 추가 패치 필요.

변경 사항:

  • Root Cause Summary 의 "silent swallow" 단정을 정정 — axios validateStatus 기본값으로 4xx/5xx 는 이미 throw 되므로 if (response.status !== 200) return; 분기는 도달 불가. 실제 가설은 (i) 0-byte/손상 mask PNG, (ii) S3 eventual consistency 이슈, (iii) transient 네트워크 실패 후 재시도 부재.
  • Fix Recommendation > 즉시 조치 최상단에 "origin/develop 적용 우선 고려" 항목 추가.
  • 새 subsection ### develop 적용 효과 분석 추가 — 미머지 커밋별 변경 요약, 이번 incident 와의 관련성 표.
  • Risk Assessment 의 예상 복잡도 문구를 "silent return → throw" 에서 develop 적용 기반 표현으로 정정.

추가 조사 내용:

  • 레포 탐색: $REPOS_DIR/cupixworks 에서 origin/master..origin/develop 차이 확인 (git log / git diff).
  • 신규 코드 경로: applications/agents/packages/base/src/util/transfer.ts (axios interceptor 기반 retry 로직), applications/agents/packages/base/src/manager/transfer.manager.ts (re-export).
  • Datadog 검색: service:cupixworks-pano-postprocessor-instance (ECONNRESET OR ETIMEDOUT OR EPIPE OR "socket hang up") → 0건; service:cupixworks-pano-postprocessor-instance "upload fail" → 0건. transient 네트워크 실패 / uploadBySignedUrl | upload fail 로그 없음.