ES /docs

[CupixAerialMap] preprocess fail - error:({}) / message:({"message":"Endpoint request timed out"})

RCA: [CupixAerialMap] preprocess fail - Endpoint request timed out

Overview#

What Happened#

2026-04-21 09:51~10:09 UTC 사이에 aerial-map-service의 preprocess ECS Fargate 태스크 2건이 Pix4D API 호출 시 "Endpoint request timed out" 응답을 받아 실패했다. 동일 세션(140423), 동일 팀(rakentava)의 aerial map ID 24(oblique)와 25(nadir) 처리가 모두 영향을 받았으며, ECS 컨테이너는 exit code 1로 종료되어 Step Functions States.TaskFailed 에러 경로가 트리거되었다.

Quick Facts#

Field Value
exception.message {"message":"Endpoint request timed out"}
top_frame src/preprocess/index.ts:175 (outer catch)
env production, eu-central-1
container_image cupix-aerial-map-preprocess:prod (sha256:4e7e2be7a3bb)
ECS cluster cupix-aerial-map-ecs-production
fargate_config 1024 CPU, 2048 MB Memory

Affected Teams#

Team / Domain Error Count Impact
rakentava (eu-central-1) 2 aerial map ID 24, 25 preprocess 실패 → 사용자에게 처리 실패 상태 노출

Timeline#

  1. 09:50:51Z — aerial map 24 (oblique) preprocess ECS 태스크 시작
  2. 09:50:58Z — Pix4D project 생성 성공 (cupix-production-rakentava-24-*)
  3. 09:51:27Z — Pix4D API 호출에서 "Endpoint request timed out" 수신, preprocess 실패
  4. 09:52:23Zcupix-preprocess-check Lambda가 ECS 태스크 실패 감지
  5. 09:52:24Zcupix-process-fail Lambda가 aerial map 24 상태를 fail로 업데이트
  6. 10:05:16Z — aerial map 25 (nadir) cupix-navigate Lambda가 preprocess로 라우팅
  7. 10:06:09Z — aerial map 25 preprocess ECS 태스크 시작
  8. 10:08:43Z — GCP 확인 및 configProcessing 완료
  9. 10:09:12Z — Pix4D startProcessing API 호출에서 "Endpoint request timed out" 수신, preprocess 실패
  10. 10:10:09Zcupix-preprocess-check Lambda가 ECS 태스크 실패 감지

Error Log#

Datadog Logs

text
[CupixAerialMap] preprocess fail - error:({}) / message:({"message":"Endpoint request timed out"})

Impact#

  • Service: aerial-map-service
  • 발생 횟수: 2
  • 최초 발생: 2026-04-21T09:51:27.792Z
  • 최근 발생: 2026-04-21T10:09:12.873Z

Root Cause Summary#

Pix4D 외부 API 엔드포인트가 요청 처리 시간 초과(gateway timeout)를 반환하여 preprocess 단계가 실패했다. 에러 메시지 {"message":"Endpoint request timed out"}는 Pix4D API Gateway 레벨에서 발생한 504-유형 timeout 응답이며, aerial-map-service의 axios 클라이언트가 5xx 에러에 대해 3회 재시도(exponential backoff: 1s, 2s, 4s)를 수행한 후에도 동일한 timeout이 반복되어 최종 실패한 것이다. aerial map 24에서는 getPix4dS3Credential 호출에서, aerial map 25에서는 startProcessing 호출에서 각각 실패했다. 두 건 모두 Pix4D 측 서비스 일시 장애로 인한 외부 의존성 문제이다.

Technical Analysis#

Code Path#

Entry point: src/preprocess/index.ts:168 — IIFE가 app(input)을 호출

src/preprocess/index.ts:168-179typescript
(async () => {
  try {
    await app(input);
    logger.info(`[CupixAerialMap] preprocess success`);
    await sleep(30000); // sleep for log push
    process.exit(0);
  } catch (error: any) {
    logger.error(`[CupixAerialMap] preprocess fail - error:(${JSON.stringify(error)}) / message:(${error.message})`);
    await sleep(30000); // sleep for log push
    process.exit(1);
  }
})();

Inner catch (first error log): src/preprocess/index.ts:144-153app() 함수 내부의 catch 블록

src/preprocess/index.ts:144-153typescript
  } catch (error: any) {
    if (error instanceof AerialMapError) {
      await cupixApi.saveError(aerialMapId, error.code, error.reason);
    } else {
      logger.error(`[CupixAerialMap] preprocess fail ${error.message}`);
      await cupixApi.saveError(aerialMapId, ERROR_CODE['AMB710'].code, ERROR_CODE['AMB710'].reason);
    }

    throw error;
  }

이 catch 블록에서 첫 번째 에러 로그 preprocess fail {"message":"Endpoint request timed out"}가 기록된다 (line 148). 에러는 AerialMapError 인스턴스가 아니므로 else 분기를 탄다. 이후 에러가 re-throw되어 outer catch(line 174-175)에서 두 번째 에러 로그 preprocess fail - error:({}) / message:(...) 가 기록된다.

error:({}) 출력 원인: JSON.stringify(error)가 빈 객체 {}를 반환하는 이유는 Pix4dApi 메서드들이 catch 블록에서 throw new Error(JSON.stringify(error.response?.data || error.message))로 새 Error 객체를 생성하기 때문이다. Error 객체는 JSON.stringify()로 직렬화하면 {} (빈 객체)가 된다 — message 속성이 enumerable: false이기 때문.

Aerial map 24 — 실패 지점: src/preprocess/index.ts:79

src/preprocess/index.ts:75-80typescript
    const project = await pix4dApi.createPix4dProject(projectName);  // 09:50:58 성공
    const projectId = project.projectId;
    await cupixApi.saveProjectId(aerialMapId, projectId);

    const credential = await pix4dApi.getPix4dS3Credential(projectId);  // ← 여기서 timeout
    const imageKeys: string[] = [];

마지막 성공 로그(create pix4d project)가 09:50:58이고 에러가 09:51:27이므로, getPix4dS3Credential (또는 saveProjectId 후 해당 호출)에서 약 29초간 재시도 후 실패한 것으로 판단된다.

Aerial map 25 — 실패 지점: src/preprocess/index.ts:135

src/preprocess/index.ts:127-135typescript
    await pix4dApi.configProcessing(projectId, method, outputs, resolution, {
      horizontalCrs: horizontal_crs,
      verticalCrs: vertical_crs,
      geoidModel: geoid_model,
      geoidHeight: geoid_height,
    });
    await sleep(1000);

    await pix4dApi.startProcessing(projectId);  // ← 여기서 timeout

마지막 성공 로그(configProcessing)가 10:08:43이고 에러가 10:09:12이므로, startProcessing 호출에서 약 29초간 재시도 후 실패했다.

Axios retry 로직: src/common/axios.ts:12-33

src/common/axios.ts:1-35typescript
import _axios from 'axios';

const RETRY_CODES = ['ECONNRESET', 'ETIMEDOUT', 'ECONNABORTED', 'EAI_AGAIN'];
const MAX_RETRIES = 3;
const BASE_DELAY_MS = 1000;
const REQUEST_TIMEOUT_MS = 60000;

const axios = _axios.create({
  timeout: REQUEST_TIMEOUT_MS,
});

axios.interceptors.response.use(undefined, async (error) => {
  const config = error.config;
  if (!config) throw error;

  config.__retryCount = config.__retryCount || 0;

  const retryable = RETRY_CODES.includes(error.code) || (error.response?.status && error.response.status >= 500);

  if (!retryable || config.__retryCount >= MAX_RETRIES) {
    throw error;
  }

  config.__retryCount += 1;
  const delay = BASE_DELAY_MS * Math.pow(2, config.__retryCount - 1);

  console.warn(
    `[AerialMap] Request failed (${error.code || error.response?.status}), retrying ${config.__retryCount}/${MAX_RETRIES} after ${delay}ms - ${config.method?.toUpperCase()} ${config.url}`,
  );

  await new Promise((resolve) => setTimeout(resolve, delay));
  return axios(config);
});

Pix4D API Gateway가 HTTP 5xx(504 Gateway Timeout)를 반환하면, axios interceptor가 error.response.status >= 500 조건으로 최대 3회 재시도한다. 재시도 간격은 1s, 2s, 4s(exponential backoff). Pix4D API Gateway의 응답 body가 {"message":"Endpoint request timed out"}인데, 이는 API Gateway의 29초 기본 timeout 메시지와 일치한다 (AWS API Gateway의 기본 integration timeout은 29초).

Pix4dApi 에러 처리 패턴: src/common/api.ts:239-256

src/common/api.ts:239-256typescript
  async startProcessing(projectId: number) {
    try {
      console.log(`[CupixApi-Pix4d] startProcessing - /project/api/v3/projects/${projectId}/start_processing`);

      const response = await axios.post(
        `${this._endpoint}/project/api/v3/projects/${projectId}/start_processing`,
        {},
        { headers: { Authorization: `Bearer ${this._accessToken}` } },
      );

      return response.data;
    } catch (error: any) {
      console.error(
        `[CupixApi-Pix4d] startProcessing - /project/api/v3/projects/${projectId}/start_processing - ${error.message}`,
      );
      throw new Error(JSON.stringify(error.response?.data || error.message));
    }
  }

catch 블록에서 new Error(JSON.stringify(error.response?.data || error.message))를 throw한다. 여기서 error.response.data{"message":"Endpoint request timed out"} 객체이므로, JSON.stringify 결과는 '{"message":"Endpoint request timed out"}' 문자열이 된다. 이것이 새 Error 객체의 message가 된다.

Log Evidence#

Datadog 검색 쿼리:

text
service:aerial-map-service status:error @environment:production
text
service:aerial-map-service @environment:production

Aerial map 24 — 에러 로그 (2건):

json
{
  "timestamp": "2026-04-21T09:51:27.709Z",
  "message": "[CupixAerialMap] preprocess fail {\"message\":\"Endpoint request timed out\"}",
  "host": "ip-10-1-111-195.eu-central-1.compute.internal",
  "source": "logstash",
  "log_file": "/tmp/workspace/preprocess-json-2026.04.21.log"
}
json
{
  "timestamp": "2026-04-21T09:51:27.792Z",
  "message": "[CupixAerialMap] preprocess fail - error:({}) / message:({\"message\":\"Endpoint request timed out\"})",
  "host": "ip-10-1-111-195.eu-central-1.compute.internal"
}

Aerial map 25 — 에러 로그 (2건):

json
{
  "timestamp": "2026-04-21T10:09:12.790Z",
  "message": "[CupixAerialMap] preprocess fail {\"message\":\"Endpoint request timed out\"}",
  "host": "ip-10-1-45-66.eu-central-1.compute.internal"
}
json
{
  "timestamp": "2026-04-21T10:09:12.873Z",
  "message": "[CupixAerialMap] preprocess fail - error:({}) / message:({\"message\":\"Endpoint request timed out\"})",
  "host": "ip-10-1-45-66.eu-central-1.compute.internal"
}

ECS 태스크 실패 — cupix-preprocess-check Lambda 로그:

json
{
  "timestamp": "2026-04-21T09:52:23.357Z",
  "message": "[CupixAerialMap] preprocess-check - event({\"Error\":\"States.TaskFailed\",\"Cause\":\"{...ExitCode:1...StopCode:EssentialContainerExited...StoppedReason:Essential container in task exited...}\"})"
}
json
{
  "timestamp": "2026-04-21T09:52:23.393Z",
  "message": "[CupixAerialMap] cupix-preprocess-check - cause(undefined) / environment(INPUT_DATA: session 140423, team rakentava, aerialMap id 24, method oblique, outputs [pointcloud,mesh,dsm], resolution high, route preprocess)"
}

실행 흐름 비교 — 두 건의 마지막 성공 단계가 다름:

Aerial Map Method Last Successful Step Failure Step Time Gap
24 oblique createPix4dProject (09:50:58) getPix4dS3Credential 추정 ~29s
25 nadir configProcessing (10:08:43) startProcessing ~29s

두 건 모두 약 29초의 gap을 보이며, 이는 Pix4D API Gateway의 29초 integration timeout + 3회 재시도(backoff 포함) 후 최종 실패한 시간과 일치한다. 다만 축적된 재시도 시간(3 × 29s + 7s delay = ~94s)보다 실제 gap이 짧은 것으로 볼 때, Pix4D API가 29초 전에 5xx 응답을 즉시 반환한 경우(즉, 실제 처리 시작 전 gateway 레벨에서 거부)일 가능성도 있다.

Cross-service 검색 결과: "Endpoint request timed out" 메시지는 aerial-map-service 에러 4건에서만 발견되었다. 다른 서비스에서는 동일 시간대에 관련 에러가 없었다. 이는 문제가 Pix4D 외부 API에 한정됨을 확인한다.

Hypotheses Considered#

# Hypothesis Evidence for Evidence against Verdict
H1 Pix4D API Gateway timeout — Pix4D 측 서비스 일시 장애로 API Gateway가 timeout 응답 반환 에러 메시지 "Endpoint request timed out"는 AWS API Gateway 504 timeout의 전형적 응답 형식. 두 건 모두 서로 다른 Pix4D API 엔드포인트(S3 credential, start_processing)에서 동일 메시지 발생. 동일 시간대(09:51~10:09)에 집중 발생. 다른 서비스에서는 동일 에러 없음. Confirmed
H2 aerial-map-service의 네트워크 문제 (ECS Fargate → Pix4D 간 connectivity) 두 건이 서로 다른 서브넷/AZ에서 발생 (eu-central-1b, eu-central-1a) 서로 다른 AZ에서 동시에 네트워크 문제가 발생할 가능성은 낮음. 또한 createPix4dProject, configProcessing 등 다른 Pix4D API 호출은 성공함. Rejected
H3 Axios timeout (60s) 초과 axios client의 REQUEST_TIMEOUT_MS가 60초로 설정됨 실제 gap이 ~29초로 60초보다 훨씬 짧음. axios timeout이면 ECONNABORTED 에러 코드가 발생하지만, 실제로는 HTTP 응답({"message":"Endpoint request timed out"})이 반환됨 Rejected
H4 Pix4D API rate limiting 동일 팀에서 연속 2건 처리 에러 메시지가 rate limit 관련이 아닌 timeout 메시지. Rate limit이면 429 응답이 예상됨 Rejected

Fix Recommendation#

즉시 조치 (Critical)#

  • 조치 불필요: Pix4D 외부 API의 일시적 장애이므로, aerial-map-service 코드 변경은 불필요하다. 영향받은 aerial map 24, 25에 대해 재처리를 트리거하면 정상 처리될 가능성이 높다.

단기 개선 (1주 이내)#

  • 에러 로깅 개선 (src/preprocess/index.ts:175): JSON.stringify(error)Error 객체에 대해 {}를 반환하는 문제가 있다. outer catch 블록에서 error.message, error.stack, 그리고 원본 error.response?.status/error.response?.data를 별도로 로깅하도록 개선하면 디버깅이 용이해진다.
  • Pix4dApi 에러 전파 개선 (src/common/api.ts): 각 Pix4dApi 메서드의 catch 블록에서 new Error(JSON.stringify(...))로 감싸면 원본 에러의 stack trace와 HTTP 상태 코드가 소실된다. Custom error class를 사용하거나, 원본 에러를 cause 옵션으로 전달(new Error(msg, { cause: error }))하면 디버깅 정보가 보존된다.
  • 재시도 로그 레벨 조정 (src/common/axios.ts:27-29): retry 로그가 console.warn으로 출력되어 filebeat/Datadog에 잡히지 않을 수 있다. logger.warn으로 변경하여 retry 발생 내역을 Datadog에서 확인할 수 있도록 해야 한다.

장기 개선 (재발 방지)#

  • Pix4D API 장애 시 자동 재시도 파이프라인: Step Functions 레벨에서 ECS 태스크 실패 시 일정 시간 후 자동 재시도하는 로직을 추가한다. 현재는 실패 시 cupix-process-fail Lambda가 최종 실패 상태로 마킹하지만, 외부 API timeout 같은 일시적 장애에 대해서는 자동 복구가 가능하다.
  • Pix4D API health check/circuit breaker: Pix4D API 상태를 사전에 확인하여, 장애 상태에서 불필요한 처리 시도를 방지하는 circuit breaker 패턴을 고려한다.

Monitoring#

  • Pix4D API timeout 빈도 모니터링:
text
service:aerial-map-service status:error "Endpoint request timed out"
  • Preprocess 실패율 추적:
text
service:aerial-map-service "preprocess fail" status:error
  • Pix4D API 응답 시간 메트릭 추가를 고려하여 timeout 임계값 초과 빈도를 사전 감지

Risk Assessment#

  • Risk level: low — Pix4D 외부 API의 일시적 장애이며, aerial-map-service 자체의 버그가 아니다. 재처리로 복구 가능.
  • 예상 복잡도: trivial — 즉시 코드 수정 필요 없음. 단기 로깅 개선은 minor change.