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#
- 2026-07-21 19:22:06 KST —
Cupix::Aws::Kinesis.put_records!가 Kinesis eu-central-1 로put_records호출 (2 건 동시). AWS Ruby SDK 내부 Net::HTTP 가 open_timeout 을 초과하며execution expired로 실패. - 2026-07-21 19:22:06 KST —
Cupix::Aws::Kinesis의 rescue 절이 "Failed to put records: ..." 를 error 로 로깅하고 재발생(sibling cluster3539f442-...). - 2026-07-21 19:22:06 KST — 상위
Cupix::EventService.publish_event의rescue RestClient::Exception절이 이 예외를 잡지 못하고 통과. - 2026-07-21 19:22:06 KST —
Eventable::Events::Base.create_event의rescue StandardError가 "Failed to create event: ..." 로 로깅 후 재발생 (본 클러스터, 2 건). - 이후 동일 서비스에서 6 시간 내 추가 재발 없음 (Datadog
service:cupixworks-api "kinesis" status:error— 4 건 이후 0 건).
Error Log#
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_event 는 rescue 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_event—app/models/concerns/eventable/events/base.rb:7 - Kinesis 호출:
Cupix::EventService.publish_event—lib/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
after_update do |model|
Eventable::Events::Update.create_event(model) if model.event_creation_on_update?
end
기대 동작: 모델 저장이 성공한 후 Kinesis 로 이벤트가 비동기적/부수 효과로 발행되고, 발행 실패가 발생하더라도 사용자 트랜잭션은 커밋된 상태로 유지된다. 실제 동작: Kinesis 예외가 rescue 를 통과해 after_update 콜백 밖으로 전파되어 감싸는 save/트랜잭션이 롤백된다.
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: ...").
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 는 원시 네트워크 예외를 그대로 받아 재발생한다.
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 쿼리:
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 지점):
{
"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!"
}
{
"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 건.
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:50 의 rescue RestClient::Exception 는 이 예외 유형과 무관. 두 로그(class Cupix::Aws::Kinesis 와 Eventable::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_event 의 rescue 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-21—raise 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 + 재시도가 유리한지, 아니면 비동기 큐(SidekiqKinesisPutRecordsWorker는 이미 존재함 —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 발행 실패율:
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!):
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 실패:
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, 트랜잭션 경계 재설계는 이벤트 소비자 계약 확인 필요.