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#
- 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 로 조준되는 실패는 이전부터 존재. - 2026-07-13 13:07 KST — 현재 클러스터 첫 발생 (first_seen 2026-07-13T04:07:25.667Z UTC).
- 2026-07-13 13:07~13:30 KST — 23분 동안 43건 반복, 평균 32초 간격 (record ID 131788, 131790 등 최소 2개 이상 record 대상).
- 2026-07-13 13:30 KST — 클러스터 last_seen 이후에도 14:07~14:13 KST 구간에서 동일 오류 재발이 확인됨 (Datadog
now-2h재검색).
Error Log#
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:31—record.flush_geo_coordinate(accept_delay: accept_delay)호출 - S3 upload:
app/models/concerns/record_geo_coordinate.rb:45-49—geo_coordinate_s3_object.upload_stream(TCP connect 발생 지점) - S3 target:
app/models/concerns/record_geo_coordinate.rb:103-109—Cupix::StorageService.object(..., bucket_name: storage_option.s3_hosting_bucket_name, ...)— bucket region 이me-south-1로 확정됨 - Client 생성:
app/services/cupix/storage_service.rb:5-12—Aws::S3::Client.new(opts)— timeout/retry 옵션 미지정 - Failure point:
record_geo_coordinate.rb:45의 upload_stream 이 던지는Seahorse::Client::NetworkingError이rescue StandardError로 잡히면서record_geo_coordinate.rb:93에서 error 로그로 기록
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
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
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
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 (재현):
service:cupixworks-migration-worker "me-south-1"
에러 로그 원문 (representative):
{
"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 페어가 확인됨:
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):
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 매핑 (코드 근거):
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-12의Aws::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:90의rescue StandardError를 유지하되Seahorse::Client::NetworkingError/Net::OpenTimeout은 warn 레벨로 낮추고error는 재시도 소진 시점(sidekiq_retries_exhausted)에서만 발생시키는 방향 검토. 현재 노이즈가 43건/23분으로 알림 피로 유발. - 재시도 정책 강화:
flush_record_geo_coordinate_worker.rb:3의retry: 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):
logs("service:cupixworks-migration-worker status:error \"me-south-1\" \"execution expired\"").index("*").rollup("count").by("host").fill("zero")
logs("service:cupixworks-migration-worker status:error \"flush_geo_coordinate\" \"Failed to open TCP connection\"").index("*").rollup("count").by("service_role").fill("zero")
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).