ES /docs

Failed to create event: Failed to open TCP connection to kinesis.eu-central-1.amazonaws.com:443 (exe

RCA: Failed to create event — Kinesis TCP connect timeout (eu-central-1)

Overview#

What Happened#

2026-07-21 19:22 KST 시점에 cupixworks-api (eu-central-1 / cupix tenant) 에서 ActiveRecord after_update 콜백이 Kinesis event stream 으로 이벤트를 발행하는 도중 kinesis.eu-central-1.amazonaws.com:443 에 대한 TCP connect 이 만료되어(execution expired) 예외가 상위로 재발생했다. 동일 순간에 두 건이 관측되었으며 짝을 이루는 하위 클러스터(3539f442-..., "Failed to put records") 와 함께 총 4 개의 로그가 남았다. 사용자 관점에서는 이벤트가 발생한 모델 업데이트 트랜잭션이 롤백되어 실패한 것으로 보인다.

Quick Facts#

Field Value
exception.class Seahorse::Client::NetworkingError (wrapping Net::OpenTimeout) — inferred from message
exception.message Failed to open TCP connection to kinesis.eu-central-1.amazonaws.com:443 (execution expired)
top_frame app/models/concerns/eventable/events/base.rb:20 (log site); underlying raise from lib/cupix/aws/kinesis.rb:16
env production, region eu-central-1, tenant cupix

Affected Teams#

Team / Domain Error Count Impact
cupixworks-api (eu-central-1) 2 (this cluster) + 2 (sibling Cupix::Aws::Kinesis#put_records!) = 4 동일 요청 2건에서 after_update 콜백이 Kinesis 발행 실패로 예외를 재발생시켜 해당 모델 업데이트 트랜잭션이 롤백됨

Timeline#

  1. 2026-07-21 19:22:06 KSTCupix::Aws::Kinesis.put_records! 가 Kinesis eu-central-1 로 put_records 호출 (2 건 동시). AWS Ruby SDK 내부 Net::HTTP 가 open_timeout 을 초과하며 execution expired 로 실패.
  2. 2026-07-21 19:22:06 KSTCupix::Aws::Kinesis 의 rescue 절이 "Failed to put records: ..." 를 error 로 로깅하고 재발생(sibling cluster 3539f442-...).
  3. 2026-07-21 19:22:06 KST — 상위 Cupix::EventService.publish_eventrescue RestClient::Exception 절이 이 예외를 잡지 못하고 통과.
  4. 2026-07-21 19:22:06 KSTEventable::Events::Base.create_eventrescue StandardError 가 "Failed to create event: ..." 로 로깅 후 재발생 (본 클러스터, 2 건).
  5. 이후 동일 서비스에서 6 시간 내 추가 재발 없음 (Datadog service:cupixworks-api "kinesis" status:error — 4 건 이후 0 건).

Error Log#

[Datadog Logs](https://app.datadoghq.com/logs?query=service%3Acupixworks-api%20status%3Aerror%20%40environment%3Aproduction%20%22Failed%20to%20create%20event%3A%20Failed%20to%20open%20TCP%20connection%20to%20kinesis.eu-central-1.amazonaws.com%3A443%20(exe%22&from_ts=1784625720000&to_ts=1784632980000&live=false)

text
Failed to create event: Failed to open TCP connection to kinesis.eu-central-1.amazonaws.com:443 (execution expired)

Impact#

  • Service: cupixworks-api
  • 발생 횟수: 2
  • 최초 발생: 2026-07-21 19:22 KST
  • 최근 발생: 2026-07-21 19:22 KST
  • Region/Tenant: eu-central-1 / cupix

Root Cause Summary#

Cupix::Aws::Kinesis.put_records! 가 AWS Ruby SDK 를 통해 kinesis.eu-central-1.amazonaws.com:443 로 TCP connect 를 시도했으나 소켓 오픈 단계에서 execution expired(Net::HTTP open_timeout 초과) 로 실패했다. Cupix::EventService.publish_eventrescue RestClient::Exception 로만 예외를 잡기 때문에 AWS SDK 가 던지는 Seahorse::Client::NetworkingError/Net::OpenTimeout 은 통과하여 상위 Eventable::Events::Base.create_event 의 rescue 로 전파되며, 이 rescue 는 로깅 후 raise e 로 재발생시켜 결과적으로 after_update 콜백을 통해 발동된 사용자 트랜잭션 자체가 롤백된다. Datadog 상 동일 6 시간 창에서 다른 Kinesis/execution expired 로그가 없어 지속적 리전 장애라기보다 6 시간 창 밖에서 발생한 순간적 네트워크/DNS/AWS endpoint 반응 지연으로 판단된다. 근본 결함은 (1) 이벤트 발행 실패가 도메인 트랜잭션 실패로 확산되도록 예외가 재발생된다는 점과 (2) 잘못된 rescue 필터(RestClient::Exception)로 AWS SDK 네트워크 예외가 정의된 시스템 에러(Cupix::Errors::System / SYS20000) 로 변환되지 못한다는 점이다.

Technical Analysis#

Code Path#

  • Entry point: ActiveRecord after_update 콜백 — app/models/concerns/eventable/callbacks.rb:34-36
  • Event 발행 진입점: Eventable::Events::Base.create_eventapp/models/concerns/eventable/events/base.rb:7
  • Kinesis 호출: Cupix::EventService.publish_eventlib/cupix/event_service.rb:43
  • Failure point: Cupix::Aws::Kinesis.put_records!lib/cupix/aws/kinesis.rb:9-16 (AWS SDK 호출에서 TCP connect 만료)
  • Log site (본 클러스터): app/models/concerns/eventable/events/base.rb:19-21
app/models/concerns/eventable/callbacks.rb:34-36ruby
after_update do |model|
  Eventable::Events::Update.create_event(model) if model.event_creation_on_update?
end

기대 동작: 모델 저장이 성공한 후 Kinesis 로 이벤트가 비동기적/부수 효과로 발행되고, 발행 실패가 발생하더라도 사용자 트랜잭션은 커밋된 상태로 유지된다. 실제 동작: Kinesis 예외가 rescue 를 통과해 after_update 콜백 밖으로 전파되어 감싸는 save/트랜잭션이 롤백된다.

app/models/concerns/eventable/events/base.rb:7-26ruby
def create_event(model)
  return nil if invalid_event?(model)

  begin
    event = _create_event(model)
    reason = extract_reason(event, model)
    properties = build_properties(model)

    track_event(model, reason, properties)

    Cupix::EventService.publish_event([event])
    Cupix::Event.publish(event.serializable_hash(stringify_nested_fields: false))
  rescue StandardError => e
    Cupix::Logger.error("Failed to create event: #{e.message}", function: __method__, class: self.name, model: model.class.to_s, id: model.id, event_params: model.event_params, error: e)
    raise e
  else
    model.event_created!
    event
  end
end

rescue StandardError => e 에서 raise e 로 재발생시키기 때문에 이벤트 발행 실패가 곧 도메인 트랜잭션 실패로 이어진다. Datadog 로그가 이 rescue 지점에서 남았다("Failed to create event: ...").

lib/cupix/event_service.rb:21-54ruby
def self.publish_event(events = [])
  return if events.blank?
  return if %w[development test].include?(Rails.env)
  return unless release_date_by(Rails.env)
  # ...
  response = Cupix::Aws::Kinesis.put_records!({ stream_name: stream_name, records: records })
  # ...
rescue RestClient::Exception => e
  Cupix::Logger.error("Failed to publish event: #{e.message}", ...)
  raise Cupix::Errors::System.new(code: 'SYS20000', reason: "Failed to publish event: #{e.message}")
end

rescue RestClient::Exception 는 AWS Ruby SDK(aws-sdk-kinesis) 가 사용하는 Seahorse HTTP 스택과 무관하다. Kinesis 클라이언트가 던지는 Seahorse::Client::NetworkingError/Net::OpenTimeout 은 여기서 잡히지 않고 통과된다. 결과적으로 이 rescue 아래의 Cupix::Errors::System(code: 'SYS20000', ...) 로의 변환 경로가 사실상 죽어있는 상태이며, 상위 Eventable::Events::Base 는 원시 네트워크 예외를 그대로 받아 재발생한다.

lib/cupix/aws/kinesis.rb:5-17ruby
def put_records!(opts = {})
  raise Cupix::Errors::Argument.new(code: 'ARG10001', reason: 'stream_name is blank') if opts[:stream_name].blank?
  raise Cupix::Errors::Argument.new(code: 'ARG10001', reason: 'records is blank') if opts[:records].blank?

  kinesis_client.put_records({
    stream_name: opts[:stream_name],
    records: opts[:records]
  })
rescue => e
  Cupix::Logger.error("Failed to put records: #{e.message}", class: self.name, function: __method__, stream_name: opts[:stream_name], record_count: opts[:records]&.size, record_sizes: opts[:records]&.map { |r| r[:data]&.bytesize })

  raise e
end

여기서 남은 로그가 sibling cluster 3539f442-901a-4e41-b702-cadbbdb05ad4 (Class Cupix::Aws::Kinesis, function put_records!) 이며, raise e 로 인해 위 두 계층의 rescue 체인으로 전파된다.

Log Evidence#

사용한 Datadog 쿼리:

text
service:cupixworks-api "kinesis.eu-central-1"

시간 창: now-6h (2026-07-21 13:22 ~ 19:22 KST 부근). 6 시간 창에서 총 4 건, 모두 2026-07-21 19:22:06 KST (=10:22:06 UTC) 한 순간에 집중.

두 계층의 로그 (동일 이벤트, 서로 다른 rescue 지점):

json
{
  "timestamp": "2026-07-21 19:22:06",
  "status": "error",
  "message": "Failed to put records: Failed to open TCP connection to kinesis.eu-central-1.amazonaws.com:443 (execution expired)",
  "class": "Cupix::Aws::Kinesis",
  "function": "put_records!"
}
json
{
  "timestamp": "2026-07-21 19:22:06",
  "status": "error",
  "message": "Failed to create event: Failed to open TCP connection to kinesis.eu-central-1.amazonaws.com:443 (execution expired)",
  "class": "Eventable::Events::Update",
  "function": "create_event",
  "error": {
    "msg": "Failed to open TCP connection to kinesis.eu-central-1.amazonaws.com:443 (execution expired)"
  }
}

지속성 확인 — 동일 쿼리 확장 창(24h): 0 건.

text
service:cupixworks-api "kinesis" status:error   (now-24h)   → 0

Status board (bun run cli/incident-board.ts for-cluster 20291be6-...) 는 scope: "svc:cupixworks-api::unknown", active: null 로 확인, dep-scope 외부 장애는 아니다. 동시각의 sibling cluster(3539f442-...) 만 함께 묶여 있으며 이는 동일 원인 사슬의 하위 로그이다.

Datadog 에서 확인되지 않는 항목 (uncertain — needs verification):

  • AWS Health Dashboard 상 eu-central-1 Kinesis 부분 장애 유무: 로컬 로그 만으로는 판단 불가. AWS 콘솔 확인 필요.
  • 해당 호스트에서 나가는 DNS/네트워크 지연 메트릭 (aws.kinesis.*.latency, 인스턴스 outbound RTT): 이 스킬에서 수집하지 않음.

Hypotheses Considered#

# Hypothesis Evidence for Evidence against Verdict
H1 Kinesis eu-central-1 endpoint 로의 TCP connect 이 open_timeout 을 초과 (순간적 네트워크/endpoint 반응 지연) 하여 AWS SDK 가 network error 를 던졌고, EventService.publish_event 의 rescue 필터(RestClient::Exception) 가 이 예외를 잡지 못해 상위로 그대로 전파됨 로그 메시지 Failed to open TCP connection to ...:443 (execution expired) 는 Ruby Net::HTTP open_timeout 시그니처. lib/cupix/aws/kinesis.rb:9 는 AWS Ruby SDK(Seahorse) 사용. lib/cupix/event_service.rb:50rescue RestClient::Exception 는 이 예외 유형과 무관. 두 로그(class Cupix::Aws::KinesisEventable::Events::Update) 가 동일 timestamp 에 함께 발생하는 점이 예외 재발생 체인과 일치 Confirmed
H2 Kinesis eu-central-1 지속적 리전 장애 에러 텍스트가 리전 endpoint 를 지목 6h 및 24h 창에서 동시각 4 건 외 재발 없음 (service:cupixworks-api "kinesis" status:error = 0 for 24h). 지속 장애면 최소 수십/수백 건이 관측되어야 함 Rejected
H3 잘못된 payload/stream_name 등 애플리케이션 인자 오류 실패는 TCP connect 단계(open TCP connection 문구) 로 AWS 서비스 응답을 받기 전. put_records! 의 인자 검증(ARG10001) 도 통과한 후. payload 오류라면 서비스 응답 기반 에러 코드가 나옴 Rejected
H4 Status board 에서 이미 감지한 외부 종속성 장애(dep-scope incident) for-cluster 결과 scope: "svc:cupixworks-api::unknown", active: null. dep:* 스코프 아님 Rejected
H5 EventService.publish_eventrescue RestClient::Exception 가 AWS SDK 네트워크 예외까지 커버한다 AWS Ruby SDK 는 RestClient 미사용. 실제로 상위 Eventable::Events::Base 의 rescue 로그가 원시 메시지 그대로 남았다는 사실이 이 rescue 가 무효했음을 방증 Rejected

Fix Recommendation#

즉시 조치 (Critical)#

  • lib/cupix/event_service.rb:50 — rescue 대상 확대. 현재 rescue RestClient::Exception => e 는 AWS Ruby SDK 가 던지는 Seahorse::Client::NetworkingError, Aws::Kinesis::Errors::ServiceError, Net::OpenTimeout, Net::ReadTimeout 등을 잡지 못한다. AWS SDK 계열 네트워크/서비스 예외까지 포함하도록 rescue 를 넓히거나 별도 rescue 절을 추가해 Cupix::Errors::System(code: 'SYS20000', ...) 로 일관되게 변환해야 한다. (구현 방향만 — 코드 작성 금지)
  • app/models/concerns/eventable/events/base.rb:19-21raise e 로 인해 이벤트 발행 실패가 after_update 콜백을 뚫고 나가 사용자 트랜잭션을 롤백시키는 것이 진짜 결함이다. 이벤트 발행은 감사/분석 성격이며 도메인 트랜잭션의 성공 조건이 아니므로, "log + swallow"(재발생하지 않음) 로 바꾸는 방향이 자연스럽다. 다만 model.event_created! 사이드이펙트/후속 파이프라인이 발행 성공에 의존한다면 트랜잭션 후처리(after_commit + 재시도 큐) 로 이전하는 편이 안전하다. 정확한 방향은 이벤트 소비자 요구사항 확인이 필요하므로 팀 조율 대상으로 표시.
  • 이 두 조치는 성격이 다르다 — 전자는 예외 매핑/로깅 위생, 후자는 트랜잭션 경계 결정이라 병렬 진행 가능.

단기 개선 (1주 이내)#

  • Kinesis 클라이언트 옵션 재검토 (lib/cupix/aws/kinesis.rb:20). AWS SDK 는 retry_limit, retry_backoff, http_open_timeout, http_read_timeout 을 지원하며 현재 코드에는 아무 것도 지정돼 있지 않아 SDK 기본값(약 15s open, 3회 재시도)에 의존한다. Kinesis PutRecords 특성상 짧은 timeout + 재시도가 유리한지, 아니면 비동기 큐(Sidekiq KinesisPutRecordsWorker 는 이미 존재함 — app/workers/kinesis_put_records_worker.rb) 로 오프로딩할지 결정.
  • 이벤트 발행을 요청 스레드에서 분리 (KinesisPutRecordsWorker 활용). 현재 after_update 콜백이 동기 HTTP 호출을 포함해 사용자 응답 시간의 하한이 Kinesis 네트워크 지연에 종속됨.

장기 개선 (재발 방지)#

  • 도메인 트랜잭션과 audit-event 발행의 결합 해제 (outbox 패턴 또는 after_commit + 지속성 있는 재시도 큐). 이벤트 발행이 실패해도 사용자 요청은 성공해야 한다.
  • Kinesis 발행 신뢰성 SLO 정의 및 실패율 알람. AWS Ruby SDK 예외 계층(Aws::Kinesis::Errors::*, Seahorse::Client::NetworkingError)을 팀의 표준 에러 매핑(Cupix::Errors::System 등) 으로 변환하는 공통 유틸 도입.

Monitoring#

레포지토리 대시보드/알림에 추가할 Datadog timeseries widget 쿼리 (release dashboard 에 그대로 삽입 가능):

Kinesis 발행 실패율:

text
sum:trace.rack.request.errors{service:cupixworks-api,resource_name:*kinesis*}.as_count()

Cupix::Aws::Kinesis#put_records! 자체의 에러 카운트 (log-based metric, @class:Cupix::Aws::Kinesis @function:put_records!):

text
logs("service:cupixworks-api status:error @class:Cupix::Aws::Kinesis @function:put_records!").index("*").rollup("count").by("@environment,region").last("15m")

Eventable::Events::*.create_event 실패:

text
logs("service:cupixworks-api status:error @class:Eventable\\:\\:Events\\:\\:*  @function:create_event").index("*").rollup("count").by("@environment").last("15m")

알림 임계: 5분 창에서 10 건 초과 시 warn, 30 건 초과 시 alert (현 사건 2 건은 이 문턱 아래 — 반복 발생 시 노출 목적).

Risk Assessment#

  • Risk level: medium — 순간적 네트워크 이벤트 자체는 low-severity 이나, 이벤트 발행 실패가 사용자 트랜잭션 롤백으로 확산되는 구조적 결합은 medium-severity. 지속 장애 재발 시 impact 가 크게 증폭될 수 있음.
  • 예상 복잡도: standard — rescue 확장/로깅은 trivial, 트랜잭션 경계 재설계는 이벤트 소비자 계약 확인 필요.