ES /docs

Net::OpenTimeout: Net::OpenTimeout

RCA: Net::OpenTimeout (outbound AWS connect timeout)

Overview#

What Happened#

cupixworks-api (tesla)의 Record#flush_geo_coordinate 가 geo-coordinate JSON 을 S3 에 업로드할 때, AWS SDK 내부의 net/http 커넥트 단계에서 대상 엔드포인트(s3.me-south-1.amazonaws.com:443) 로의 TCP 연결이 timeout 되어 Net::OpenTimeout 이 발생했다. Datadog Error Tracking 은 최하위 프레임이 net/http 이므로 이를 service: net/http, Net::OpenTimeout 이슈로 묶는다. 이 예외는 애플리케이션 코드에서 이미 rescue StandardError 로 처리되어 lock 해제 후 error 로그를 남기고 정상적으로 종료된다. 15.5개월간 누적 60건으로 발생 빈도가 매우 낮은 간헐적 외부 연결 timeout 이다.

Quick Facts#

Field Value
exception.class Net::OpenTimeout
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 (cupixworks-api, tesla repo)
env production (multi-region: me-south-1, me-central-1, us-west-2, eu-central-1)

Affected Teams#

Team / Domain Error Count Impact
cupixworks-api (Record#flush_geo_coordinate) 최근 버스트 다수 (s3.me-south-1) geo-coordinate S3 업로드 실패 → 다음 cron/재시도에서 재수행되어 최종적으로는 flush 됨
cupixworks-api (Cupix::Aws::Kinesis#put_records!) 소수 (kinesis.us-west-2, kinesis.eu-central-1) 이벤트 스트림 전송 실패 (동일 예외 클래스로 그룹화됨)

Timeline#

  1. 2025-04-17 06:45 KST — 이슈 최초 발생 (first_seen). Error Tracking Representative Error 는 이 시점 샘플로 message 가 비어 있어 STALE.
  2. 2026-07-22 04:22 KST / 2026-07-25 02:40 KSTCupix::Aws::Kinesis#put_records! 에서 kinesis.eu-central-1 / kinesis.us-west-2 커넥트 timeout (동일 이슈로 그룹화).
  3. 2026-08-04 14:55 KST — 이슈 last_seen.
  4. 2026-08-04 15:17~16:36 KSTRecord#flush_geo_coordinate 에서 s3.me-south-1.amazonaws.com:443 커넥트 timeout 이 분당 1~2건 간헐 반복. 사이사이 flush_geo_coordinate - finished - duration: 1 seconds 성공 로그가 다수 관측됨 → 특정 리전 엔드포인트로의 간헐적 연결 실패.

Error Log#

Datadog Logs

text
Net::OpenTimeout

실제 최근 발생(Representative Error 는 message 가 비어 STALE 이며, last_seen 근처 로그가 보여주는 실제 메시지):

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

Impact#

  • Service: net/http (실제 애플리케이션은 cupixworks-api / tesla)
  • 발생 횟수: 60 (15.5개월 누적, 매우 낮은 빈도)
  • 최초 발생: 2025-04-17 06:45 KST
  • 최근 발생: 2026-08-04 14:55 KST

Root Cause Summary#

Net::OpenTimeout 은 코드 결함이 아니라 간헐적 외부 의존성(AWS S3 / Kinesis) TCP 연결 timeout 이다. Record#flush_geo_coordinate 는 AWS SDK 의 upload_stream 을 통해 S3 에 업로드하는데, SDK 는 내부적으로 Ruby net/http 로 커넥션을 연다. 대상 리전 엔드포인트(주로 s3.me-south-1.amazonaws.com:443)로의 커넥트가 SDK 의 connect timeout 안에 완료되지 못하면 Net::OpenTimeout 이 발생한다. 이 예외는 record_geo_coordinate.rb:90rescue StandardError => e 로 이미 포착되어 lock 을 해제하고 error 로그를 남긴 뒤 false 를 반환하므로 요청 흐름으로 전파되지 않는다. Datadog Error Tracking 은 APM Ruby tracer 가 기록한 span error 를 최하위 프레임(net/http) 기준으로 묶기 때문에 service: net/http, Net::OpenTimeout 라는 인프라 어댑터 이름으로 나타난다. Representative Error 는 first_seen(2025-04) 샘플이라 message 가 비어 STALE 하며, last_seen 근처 실제 메시지는 위 S3 커넥트 timeout 이다.

Technical Analysis#

Code Path#

  • Entry point: app/models/concerns/record_geo_coordinate.rb:37flush_geo_coordinate
  • S3 업로드(외부 호출): app/models/concerns/record_geo_coordinate.rb:45geo_coordinate_s3_object.upload_stream(...)
  • Failure point: AWS SDK 내부 net/http connect → Net::OpenTimeout
  • 예외 처리: app/models/concerns/record_geo_coordinate.rb:90-94rescue StandardError 에서 이미 포착
app/models/concerns/record_geo_coordinate.rb:44-49ruby
    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|
      write_stream << '{ "panos": ['
app/models/concerns/record_geo_coordinate.rb:90-97ruby
  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
  else
    unlock_flushing_geo_coordinate
  end

기대 동작: S3 커넥트 성공 → JSON 스트림 업로드 → geo_coordinate_url_updated_at 갱신 → finished 로그. 실제 동작: 대상 리전 엔드포인트 커넥트가 간헐적으로 timeout → Net::OpenTimeout → 이미 존재하는 rescue 에서 포착 → lock 해제 + error 로그 + false 반환. 예외는 상위로 전파되지 않으며, stale_over_hour / delayed flush cron(lib/cupix/cron/record.rb)이 다음 주기에 재수행하므로 최종적으로 flush 된다.

동일 예외 클래스가 다른 외부 호출(Cupix::Aws::Kinesis#put_records!kinesis.*.amazonaws.com:443)에서도 발생하여 하나의 ET 이슈로 그룹화된다.

Log Evidence#

Datadog 쿼리:

text
"execution expired"
text
service:cupixworks-api ("Net::OpenTimeout" OR "execution expired")

Net::OpenTimeout 문자열 자체로는 log index 에서 0건(예외가 ET/APM span 으로만 기록되고 애플리케이션 로그에는 Ruby 렌더링 메시지로 남음). last_seen 근처 실제 발생 로그:

json
{
  "timestamp": "2026-08-04 16:36:27",
  "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"
}

동일 이슈로 묶인 다른 엔드포인트(Kinesis):

json
{
  "timestamp": "2026-07-24 17:40:33",
  "status": "error",
  "message": "Failed to put records: Failed to open TCP connection to kinesis.us-west-2.amazonaws.com:443 (execution expired)",
  "class": "Cupix::Aws::Kinesis",
  "function": "put_records!"
}

간헐성 근거 — 실패 사이사이 성공 로그가 다수:

text
flush_geo_coordinate - finished - duration: 1 seconds
flush_geo_coordinate - begin
flush_geo_coordinate - finished - duration: 1 seconds

Hypotheses Considered#

# Hypothesis Evidence for Evidence against Verdict
H1 간헐적 외부 의존성(AWS S3/Kinesis) TCP connect timeout last_seen 근처 로그가 Failed to open TCP connection to s3.me-south-1.amazonaws.com:443 (execution expired); 실패 사이 성공(finished - duration: 1 seconds) 다수; 15.5개월 60건의 낮은 빈도; 여러 리전 엔드포인트(me-south-1/me-central-1/us-west-2/eu-central-1)에서 발생 Confirmed
H2 애플리케이션 코드 결함(nil/logic)으로 예외가 전파되어 요청 실패 record_geo_coordinate.rb:90 에서 이미 rescue StandardError 로 포착, lock 해제 후 false 반환하여 전파되지 않음 Rejected
H3 Representative Error 의 빈 message 가 실제 현재 증상 ET 이슈에 그렇게 표시됨 first_seen(2025-04) 샘플로 STALE. last_seen 근처 실제 메시지는 S3 connect timeout Rejected
H4 특정 코드 배포/리전 설정 변경으로 인한 회귀 15.5개월에 걸쳐 산발 발생, 특정 배포와 상관관계 없음; 성공 케이스가 다수로 정상 동작 중 Rejected

Fix Recommendation#

즉시 조치 (Critical)#

  • 코드 변경 불필요. 예외는 이미 record_geo_coordinate.rb:90Cupix::Aws::Kinesis#put_records! 에서 처리되고, cron 재수행으로 최종 flush 가 보장된다. 이는 외부 연결의 간헐적 timeout 으로 코드 버그가 아니다.

단기 개선 (1주 이내)#

  • Error Tracking 노이즈 감소: 이 이슈는 이미 rescue 되는 transient timeout 이므로 ET 에서 IGNORE 처리하거나, 반복 발생 시 임계치 기반 알림으로만 전환하는 것을 권장.
  • s3.me-south-1 등 특정 리전 엔드포인트에서 timeout 비율이 지속적으로 높다면 AWS SDK 의 http_open_timeout / retry 설정 점검. (설정값 확인 필요 — uncertain, needs verification.)

장기 개선 (재발 방지)#

  • Datadog Error Tracking 에서 최하위 프레임(net/http) 기준 그룹화로 인해 서로 다른 외부 호출(S3 upload, Kinesis put_records)이 하나의 이슈로 묶이는 문제 → 애플리케이션 레벨에서 Net::OpenTimeout 을 도메인별 예외로 래핑하거나 span tag(peer.hostname, component)를 활용해 그룹 세분화 검토.

Monitoring#

특정 리전 S3 connect timeout 발생 추이:

text
service:cupixworks-api "execution expired" "s3"

flush_geo_coordinate 에러 발생 건수:

text
service:cupixworks-api @function:flush_geo_coordinate status:error

Kinesis 전송 실패:

text
service:cupixworks-api "Failed to put records" "execution expired"

Risk Assessment#

  • Risk level: low
  • 예상 복잡도: trivial (코드 수정 없음, ET IGNORE / 모니터링 조정 수준)

Noise Verdict#

noise — 이미 코드에서 rescue 되고 cron 재수행으로 최종 flush 되는 간헐적 AWS S3/Kinesis TCP connect timeout 이므로 코드 수정이 필요 없는 일시적 외부 의존성 오류다.