ES /docs

flush_geo_coordinate - error - message: Failed to open TCP connection to s3.me-south-1.amazonaws.com

RCA: flush_geo_coordinate — TCP connection to s3.me-south-1.amazonaws.com timed out

Overview#

What Happened#

cupixworks-migration-worker (us-west-2 host ip-10-1-144-200) 에서 FlushRecordGeoCoordinateWorker 가 migration 후속 단계로 실행되었고, 대상 record 의 storage bucket 리전이 me-south-1 (Bahrain) 이었다. Rails Sidekiq 프로세스가 s3.me-south-1.amazonaws.com:443 으로 TCP 연결을 시도했으나 open_timeout 안에 응답을 받지 못하고 Net::OpenTimeout (Ruby 표현: execution expired) 이 발생, 43건의 flush 실패가 기록되었다.

Quick Facts#

Field Value
exception.class Net::OpenTimeout (Ruby, 메시지에서 execution expired 로 표기)
exception.message Failed to open TCP connection to s3.me-south-1.amazonaws.com:443 (execution expired)
top_frame app/models/concerns/record_geo_coordinate.rb:45 (geo_coordinate_s3_object.upload_stream)
runtime Ruby / Rails / Sidekiq, service_role: migrationworker
deploy production-us-west-2-20260713T0506Z0-4f01ffc0-cupixworks
env production, region us-west-2, host ip-10-1-144-200.us-west-2.compute.internal

Affected Teams#

Team / Domain Error Count Impact
cupix (tenant) — records with storage_option s3_bucket_region: me-south-1 43 migration 직후 record 의 geo_coordinate JSON 이 me-south-1 S3 hosting bucket 에 업로드되지 못함. geo_coordinate_url_updated_at 이 갱신되지 않아 downstream 에서 최신 pano geo_coordinate JSON 을 서빙하지 못함.

로그 tag 는 tenant:cupix 만 노출되고 record 단위 team domain 은 로그에 붙지 않음 (uncertain -- team_domain not tagged in this log stream).

Timeline#

  1. 2026-07-02 13:07 KST — 동일 오류 계열이 최초 관측 (flush_geo_coordinate - error - message: Failed to open TCP connection to 207.127.99.67:80 (execution expired), 동일 코드 경로). me-south-1 host 로 조준되는 실패는 이전부터 존재.
  2. 2026-07-13 13:07 KST — 현재 클러스터 첫 발생 (first_seen 2026-07-13T04:07:25.667Z UTC).
  3. 2026-07-13 13:07~13:30 KST — 23분 동안 43건 반복, 평균 32초 간격 (record ID 131788, 131790 등 최소 2개 이상 record 대상).
  4. 2026-07-13 13:30 KST — 클러스터 last_seen 이후에도 14:07~14:13 KST 구간에서 동일 오류 재발이 확인됨 (Datadog now-2h 재검색).

Error Log#

Datadog Logs

text
flush_geo_coordinate - error - message: Failed to open TCP connection to s3.me-south-1.amazonaws.com:443 (execution expired)

Impact#

  • Service: cupixworks-migration-worker
  • 발생 횟수: 43
  • 최초 발생: 2026-07-13 13:07 KST
  • 최근 발생: 2026-07-13 13:30 KST

Root Cause Summary#

FlushRecordGeoCoordinateWorker 가 migration import 완료 후 ImportWorker#schedule_flush_geo_coordinate 를 통해 큐잉되어 (app/workers/import_worker.rb:254-262) record.flush_geo_coordinate 를 호출한다. 이 메서드는 record 의 storage_option.s3_bucket_region 을 사용해 S3 endpoint 를 구성하는데, 문제 record 들의 hosting bucket 은 me-south-1 (Bahrain) 이다. Sidekiq 워커는 us-west-2 EC2 (ip-10-1-144-200.us-west-2.compute.internal) 에서 실행되며, 이 노드에서 s3.me-south-1.amazonaws.com:443 으로의 TCP 3-way handshake 가 AWS SDK 기본 http_open_timeout (aws-sdk-ruby 기본 15초) 안에 완료되지 않아 Net::OpenTimeout 이 발생한다. storage_service.rb 에서 Aws::S3::Client.new 는 커스텀 timeout/retry 설정 없이 SDK 기본값만 사용하므로 재시도 없이 rescue StandardError 로 폴백되어 unlock_flushing_geo_coordinate 후 error 로그만 남긴다. 크로스 리전(us-west-2 → me-south-1)에 걸친 지속적인 네트워크 지연/차단이 근본 원인으로 보이며, S3 me-south-1 자체 outage 지표는 없다 (동일 시간대 다른 me-south-1 트래픽 로그가 이 서비스 로그 스트림에 없어 uncertain -- needs verification).

Technical Analysis#

Code Path#

  • Entry point: app/workers/import_worker.rb:225-226 — migration 성공 직후 schedule_flush_geo_coordinate 호출
  • Enqueue: app/workers/import_worker.rb:254-262 — record 별로 FlushRecordGeoCoordinateWorker.perform_async(record_id)
  • Worker: app/workers/flush_record_geo_coordinate_worker.rb:31record.flush_geo_coordinate(accept_delay: accept_delay) 호출
  • S3 upload: app/models/concerns/record_geo_coordinate.rb:45-49geo_coordinate_s3_object.upload_stream (TCP connect 발생 지점)
  • S3 target: app/models/concerns/record_geo_coordinate.rb:103-109Cupix::StorageService.object(..., bucket_name: storage_option.s3_hosting_bucket_name, ...) — bucket region 이 me-south-1 로 확정됨
  • Client 생성: app/services/cupix/storage_service.rb:5-12Aws::S3::Client.new(opts) — timeout/retry 옵션 미지정
  • Failure point: record_geo_coordinate.rb:45 의 upload_stream 이 던지는 Seahorse::Client::NetworkingErrorrescue StandardError 로 잡히면서 record_geo_coordinate.rb:93 에서 error 로그로 기록
app/workers/import_worker.rb:254-262ruby
def schedule_flush_geo_coordinate(migration_id, record_ids)
  return if record_ids.blank?

  Cupix::Logger.info("Database import - migration id(#{migration_id}): scheduling flush_geo_coordinate for #{record_ids.size} records", class: self.class.name, method: __method__)

  record_ids.each do |record_id|
    FlushRecordGeoCoordinateWorker.perform_async(record_id)
  end
end
app/models/concerns/record_geo_coordinate.rb:37-97ruby
def flush_geo_coordinate(accept_delay: false)
  lock_flushing_geo_coordinate

  _delayed_flush_geo_coordinate and return if accept_delay

  Cupix::Logger.info('flush_geo_coordinate - begin', class: self.class.name, function: __method__, module: 'RecordGeoCoordinate', record: { id: self.id })
  start_time = DateTime.now.to_i

  geo_coordinate_s3_object.upload_stream(
    content_type: 'application/json',
    cache_control: "max-age=#{1.year.to_i}",
    acl: 'bucket-owner-full-control'
  ) do |write_stream|
    # ... writes JSON of panos in batches ...
  end
  # ...
rescue StandardError => e
  unlock_flushing_geo_coordinate

  Cupix::Logger.error("flush_geo_coordinate - error - message: #{e.message}", class: self.class.name, function: __method__, module: 'RecordGeoCoordinate', record: { id: self.id })
  false
app/models/concerns/record_geo_coordinate.rb:103-109ruby
def geo_coordinate_s3_object
  Cupix::StorageService.object(
    storage_option: storage_option,
    bucket_name: storage_option.s3_hosting_bucket_name,
    key: s3_object_key
  )
end
app/services/cupix/storage_service.rb:5-12ruby
def client(storage_option: nil, **kwargs)
  opts = parse_storage_option(storage_option).merge(kwargs)
  opts[:force_path_style] = true

  check_required_params(opts, %i[region])

  Aws::S3::Client.new(opts)
end

기대 동작: migration 직후 record 의 hosting bucket 리전(me-south-1) S3 로 geo_coordinate JSON 업로드 완료, geo_coordinate_url_updated_at 갱신. 실제 동작: us-west-2 워커가 me-south-1 S3 endpoint 로 TCP 연결 실패 (open timeout) → error 로그만 남기고 종료. Sidekiq 은 retry: 1 (flush_record_geo_coordinate_worker.rb:3) 로 설정되어 한 번 더 재시도한 후 죽는다.

Log Evidence#

Datadog query (재현):

text
service:cupixworks-migration-worker "me-south-1"

에러 로그 원문 (representative):

json
{
  "timestamp": "2026-07-13T04:13:38.193Z",
  "status": "error",
  "message": "flush_geo_coordinate - error - message: Failed to open TCP connection to s3.me-south-1.amazonaws.com:443 (execution expired)",
  "class": "Record",
  "function": "flush_geo_coordinate",
  "module": "RecordGeoCoordinate",
  "service": "cupixworks-migration-worker",
  "service_role": "migrationworker",
  "environment": "production",
  "region": "us-west-2",
  "host": "ip-10-1-144-200.us-west-2.compute.internal",
  "tenant": "cupix"
}

같은 record 에 대해 flush_geo_coordinate - begin info 로그가 선행하고 곧바로 error 로 종료되는 begin/error 페어가 확인됨:

text
2026-07-13 14:13:38 INFO  flush_geo_coordinate - begin                                 record.id=131790
2026-07-13 14:13:38 ERROR flush_geo_coordinate - error ... me-south-1 ... execution expired
2026-07-13 14:12:40 INFO  flush_geo_coordinate - begin                                 record.id=131788
2026-07-13 14:12:40 ERROR flush_geo_coordinate - error ... me-south-1 ... execution expired

동일 오류의 최초 관측은 14일 retention 창 앞단(2026-07-02):

text
2026-07-02 13:07:40  ERROR  flush_geo_coordinate - error - message: Failed to open TCP connection to 207.127.99.67:80 (execution expired)
2026-07-02 13:11:53  ERROR  flush_geo_coordinate - error - message: Failed to open TCP connection to s3.me-south-1.amazonaws.com:443 (execution expired)

Storage → region 매핑 (코드 근거):

app/models/storage_option.rb:99-100ruby
when 'me-south-1'
  @s3_region_code = 'meso1'

즉 이 record 들은 team storage 가 me-south-1 (meso1) 로 배정된 것이며, migration 후 hosting bucket 이 me-south-1 로 정해진다.

Hypotheses Considered#

# Hypothesis Evidence for Evidence against Verdict
H1 us-west-2 migration Sidekiq → me-south-1 S3 사이의 크로스리전 TCP 연결이 AWS SDK 기본 open_timeout 안에 완료되지 못함 (네트워크 latency/차단) 43건 전부 동일 host ip-10-1-144-200.us-west-2.compute.internal 에서 발생, 대상은 s3.me-south-1.amazonaws.com:443, 에러 문구가 정확히 Ruby Net::OpenTimeout (execution expired). storage_service.rb 에서 timeout/retry 미지정. 동일 패턴이 2026-07-02 부터 재현 중. Confirmed
H2 S3 me-south-1 리전 전체 outage H1 과 동일 로그 다른 me-south-1 소비 경로(예: pano download, storage_service 다른 호출) 에서 유사 에러가 이 서비스 로그 스트림에 없음. 클러스터가 오직 flush_geo_coordinate 만이라는 점, 그리고 me-south-1 관련 상태 페이지 신호가 없어 outage 로 단정 못함. Rejected (uncertain — AWS Health 별도 확인 필요)
H3 storage_option 이 잘못된 값 (예: 존재하지 않는 리전) 으로 저장되어 endpoint 자체가 도달 불가 에러가 me-south-1 로 고정 me-south-1 는 실제 존재하는 AWS 리전(Bahrain) 이고, storage_option.rb:99-100 매핑도 정상. bucket name/hosting 설정 이상을 시사하는 로그 없음. Rejected
H4 record 데이터 자체 문제로 upload_stream 이 시작도 못함 begin 로그가 error 직전 정상 기록됨 → upload_stream 진입 후 네트워크 단계에서 실패. record 데이터 문제라면 다른 예외 클래스 (e.g. NoMethodError, ActiveRecord) 가 나와야 함. Rejected
H5 SDK 재시도 정책 미설정으로 일시적 네트워크 튐 을 흡수하지 못함 Aws::S3::Client.new(opts)retry_limit, http_open_timeout 등 옵션 미지정 (storage_service.rb:5-12). 43건이 23분 동안 지속되는 것도 재시도 부재를 시사. 재시도가 있어도 크로스리전 연결 자체가 안 되면 실패는 유지. 재시도 부재는 노출 증폭 요인일 뿐 유일 원인은 아님. Confirmed (contributing)

Fix Recommendation#

즉시 조치 (Critical)#

  • 인프라 확인: us-west-2 migration Sidekiq 노드에서 s3.me-south-1.amazonaws.com:443 로 outbound 가 실제로 열려 있는지 확인. VPC/NACL/보안그룹/NAT/HTTP proxy 정책, S3 gateway VPC endpoint 가 me-south-1 를 커버하는지 점검. 파일: .claude/architecture.md 대신 실제 배포는 tesla .ebextensions/* (Elastic Beanstalk) 및 cupix-infrastructure 리전 설정에서 확인. 본 항목은 코드 변경이 아닌 운영 작업이므로 자동 code-fix 대상 아님.
  • 워커 격리 검토: FlushRecordGeoCoordinateWorker 를 record 의 hosting bucket region 별로 다른 큐/노드로 분리해 크로스리전 트래픽을 없앨 수 있는지 검토. 관련 위치: app/workers/flush_record_geo_coordinate_worker.rb:3 (sidekiq_options queue: :fresh) — 큐 라우팅 정책 결정 필요.

단기 개선 (1주 이내)#

  • AWS SDK client 에 명시적 timeout / retry 부여: app/services/cupix/storage_service.rb:5-12Aws::S3::Client.new 호출에 http_open_timeout, http_read_timeout, retry_limit, retry_mode: 'adaptive' 등을 옵션으로 전달하는 방향 검토. 크로스리전 latency 를 감안한 값(예: open 30s, read 60s)으로 조정하되 다른 호출부(spec/service/cupix/storage_service_spec.rb, analyze_pano.rb, user_export_service.rb)에 미치는 영향도 확인 필요.
  • 에러 분류 세분화: record_geo_coordinate.rb:90rescue StandardError 를 유지하되 Seahorse::Client::NetworkingError / Net::OpenTimeout 은 warn 레벨로 낮추고 error 는 재시도 소진 시점(sidekiq_retries_exhausted)에서만 발생시키는 방향 검토. 현재 노이즈가 43건/23분으로 알림 피로 유발.
  • 재시도 정책 강화: flush_record_geo_coordinate_worker.rb:3retry: 1 이 크로스리전 timeout 에 너무 짧음. exponential backoff 로 3~5회 재시도 검토.

장기 개선 (재발 방지)#

  • 리전별 워커 pool: hosting bucket region 이 다양한 tenant 를 지원하기 위해 리전별 Sidekiq deployment 를 두거나, 각 리전에 소형 Lambda 로 upload_stream 을 위임하는 아키텍처 검토.
  • VPC endpoint 정책: 자주 사용되는 non-primary 리전(me-south-1, eu-, ap-)에 대해 VPC endpoint 또는 CloudFront/S3 accelerate 도입 검토.
  • Monitor for cross-region storage_option 통계: 어떤 team/tenant 이 어떤 리전에 매핑되어 있는지 대시보드화. migration 시 사전에 리전별 throughput 추정 가능.

Monitoring#

Release dashboard timeseries widget 에 넣을 쿼리 (모두 count/sum 기반 aggregator):

text
logs("service:cupixworks-migration-worker status:error \"me-south-1\" \"execution expired\"").index("*").rollup("count").by("host").fill("zero")
text
logs("service:cupixworks-migration-worker status:error \"flush_geo_coordinate\" \"Failed to open TCP connection\"").index("*").rollup("count").by("service_role").fill("zero")
text
logs("service:cupixworks-migration-worker \"flush_geo_coordinate - begin\"").index("*").rollup("count").fill("zero")
  • 첫 두 쿼리: me-south-1 대상 open timeout 실패의 host / role 별 카운트. 배포 후 0 근처로 회귀하는지 확인.
  • 세 번째 쿼리: begin 로그 카운트 (총 시도량) — error 대비 성공률 계산의 분모.
  • monitor 조건 예: 첫 쿼리가 5분 동안 5건 이상이면 warn, 15건 이상이면 alert. (monitor-only 문법이므로 위 widget 쿼리와 분리해 monitor 로 별도 생성 필요.)

Risk Assessment#

  • Risk level: medium — 데이터 손실은 없음(record 자체는 저장됨). 단, migration 완료 후 geo_coordinate JSON 이 me-south-1 tenant 에서 갱신되지 않아 downstream 지도/위치 기능이 stale 될 수 있음. 알림 피로도 큼.
  • 예상 복잡도: standard — SDK 옵션 추가, 로그 레벨 조정, worker retry 튜닝은 소규모 변경. 인프라 측 (VPC/보안그룹) 검토와 리전별 worker pool 은 별도 트랙 (operations/architecture).