ES /docs

Eventable::Events::Base#create_event — Kinesis timeout propagation

RCA: Failed to create event — Net::OpenTimeout to Kinesis

Overview#

What Happened#

2026-07-11 03:33 KST 에 cupixworks-api (us-west-2) 의 Eventable::Events::Update.create_event 가 Kinesis (kinesis.us-west-2.amazonaws.com:443) 로 이벤트를 publish 하다 Net::OpenTimeout 으로 실패했다. 이 실패는 동일 시간대에 여러 Kinesis 네트워크 오류 (end of file reached, execution expired, Connection reset by peer) 와 함께 발생했고, status board 에서는 2026-07-10-svc-cupixworks-api--unknown-4 인시던트로 그룹핑됐다.

Quick Facts#

Field Value
exception.class Net::OpenTimeout
exception.message Net::OpenTimeout
top_frame app/models/concerns/eventable/events/base.rb:20
downstream kinesis.us-west-2.amazonaws.com:443
env production, us-west-2

Affected Teams#

Team / Domain Error Count Impact
cupixworks-api (Eventable) 1 (본 클러스터) ActiveRecord after_update 콜백에서 Kinesis publish 실패 시 예외 재발생 → 원 요청이 500 으로 실패할 가능성
status board 관련 클러스터 6 (18:18~18:43 UTC 창) 동일 시간대 Failed to create event / Failed to put records 계열 오류 다수

Timeline#

  1. 2026-07-11 03:18 KST — 최초 Failed to open TCP connection to kinesis.us-west-2.amazonaws.com:443 (execution expired) 로그 (status board 인시던트 시작).
  2. 2026-07-11 03:33 KST — 본 클러스터 발생: Failed to create event: Net::OpenTimeout (Eventable::Events::Update.create_event).
  3. 2026-07-11 03:43 KST — 동일 창의 마지막 이벤트 Failed to create event: end of file reached.
  4. 2026-07-11 (진행중) — status board 인시던트 2026-07-10-svc-cupixworks-api--unknown-4 는 아직 open 상태.

Error Log#

Datadog Logs

text
Failed to create event: Net::OpenTimeout

Impact#

  • Service: cupixworks-api
  • 발생 횟수: 1 (이 fingerprint), 동일 인시던트 창에서 관련 클러스터 6건
  • 최초 발생: 2026-07-11 03:33 KST
  • 최근 발생: 2026-07-11 03:33 KST

Root Cause Summary#

Cupix::Aws::Kinesis.put_records!kinesis.us-west-2.amazonaws.com:443 에 TCP 연결을 열지 못하고 Net::OpenTimeout 을 던졌다. 예외는 Cupix::EventService.publish_event 를 통과해 Eventable::Events::Base.create_eventrescue StandardError 블록에서 로깅된 뒤 raise e 로 다시 던져진다. create_event 는 ActiveRecord after_create/after_update 콜백에서 호출되므로, Kinesis 로의 짧은 네트워크 장애가 도메인 모델의 write 트랜잭션 실패로 승격된다. 14일 창에서 동종 오류가 6회만 관찰되므로 근본 원인은 코드 결함이 아니라 일시적인 Kinesis outbound 네트워크 지연/reset 이며, 문제는 이 transient failure 가 사용자 요청까지 전파되는 error propagation 설계에 있다.

Technical Analysis#

Code Path#

  • Entry point: ActiveRecord after_update 콜백 — app/models/concerns/eventable/callbacks.rb:34-36
  • Event publish: app/models/concerns/eventable/events/base.rb:7-26
  • Kinesis publish: lib/cupix/event_service.rb:43
  • Failure point: lib/cupix/aws/kinesis.rb:9-17 (Kinesis client put_records 가 TCP open 실패)
app/models/concerns/eventable/callbacks.rb:30-40ruby
after_create do |model|
  Eventable::Events::Create.create_event(model) if model.event_creation_on_create?
end

after_update do |model|
  Eventable::Events::Update.create_event(model) if model.event_creation_on_update?
end

after_destroy do |model|
  Eventable::Events::Delete.create_event(model) if model.event_creation_on_delete?
end
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
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
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

기대 동작 vs 실제 동작

  • 기대: 이벤트 publish 는 도메인 write 의 사이드이펙트 이므로 Kinesis 장애가 write 자체를 실패시키지 말아야 한다.
  • 실제: publish_eventRestClient::Exception 만 잡아 wrapping 하고 Net::OpenTimeout (Ruby 표준 net/http) 은 rescue 하지 않는다. 결과적으로 예외는 Kinesis.put_records! (rescue => e; raise e) → EventService.publish_event (unrescued) → Eventable::Events::Base.create_event (rescue StandardError; raise e) 를 그대로 통과해 after_update 콜백 밖으로 재발생한다. Rails 콜백에서 예외가 raise 되면 트랜잭션이 롤백되고 요청이 500 으로 응답된다.

Log Evidence#

사용한 Datadog 쿼리:

text
service:cupixworks-api "Failed to create event"
text
service:cupixworks-api "Failed to put records"

핵심 로그 (같은 사건 창):

json
{
  "timestamp": "2026-07-11 03:33:00",
  "status": "error",
  "message": "Failed to create event: Net::OpenTimeout",
  "class": "Eventable::Events::Update",
  "function": "create_event",
  "error": { "msg": "Net::OpenTimeout" }
}
json
{
  "timestamp": "2026-07-11 03:33:00",
  "status": "error",
  "message": "Failed to put records: Net::OpenTimeout",
  "class": "Cupix::Aws::Kinesis",
  "function": "put_records!"
}

같은 인시던트 창의 인접 실패 (원인이 Kinesis outbound 라는 것을 뒷받침):

json
{ "timestamp": "2026-07-11 03:18:24", "message": "Failed to put records: Failed to open TCP connection to kinesis.us-west-2.amazonaws.com:443 (execution expired)" }
{ "timestamp": "2026-07-11 03:03:42", "message": "Failed to put records: end of file reached" }
{ "timestamp": "2026-07-11 02:37:28", "message": "Failed to put records: Failed to open TCP connection to kinesis.us-west-2.amazonaws.com:443 (execution expired)" }
{ "timestamp": "2026-07-11 03:43:26", "message": "Failed to put records: end of file reached" }

14일 창 전체에서 Failed to put records 오류는 6건뿐 — 지속적 결함이 아니라 산발적인 network transient 임.

Status board 컨텍스트:

text
scope: svc:cupixworks-api::unknown
active: 2026-07-10-svc-cupixworks-api--unknown-4
cluster_ids: [b5654325..., c5d71d31..., b8e9d2fd..., 1293ac87..., af240fd9..., f4a287c3...]

Hypotheses Considered#

# Hypothesis Evidence for Evidence against Verdict
H1 Kinesis us-west-2 로의 outbound TCP 연결이 순간적으로 실패 (transient network / AWS-side latency) 하고, EventService.publish_eventNet::OpenTimeout 을 rescue 하지 않아 예외가 ActiveRecord 콜백 밖으로 전파 Cupix::Aws::Kinesis.put_records! 로그(Failed to put records: Net::OpenTimeout, ... TCP connection ... (execution expired)) 가 동일 타임스탬프에 존재, event_service.rb:50RestClient::Exception 만 rescue, base.rb:21raise e 재발생 Confirmed
H2 애플리케이션 로직/코드 결함 (예: 잘못된 모델 데이터, invalid event_params) 에러 메시지가 Net::OpenTimeout 이며, Cupix::Aws::Kinesis 에서 발생. 도메인 검증은 invalid_event? 에서 통과 후에만 publish 진입 Rejected
H3 Kinesis 스트림 이름 오설정 / 스트림 미존재 스트림 문제라면 AWS SDK 가 ResourceNotFoundException 등을 반환. 실제로 TCP open 단계에서 실패 (Net::OpenTimeout, execution expired) Rejected
H4 kinesis_client 메모이제이션(`@kinesis_client = ...`) 으로 stale 커넥션 재사용 클라이언트가 @kinesis_client 로 프로세스 수명 동안 재사용됨 (lib/cupix/aws/kinesis.rb:19-21)

Fix Recommendation#

즉시 조치 (Critical)#

  • 대상 파일: lib/cupix/event_service.rb:21-54
  • 접근 방식: publish_eventrescue 범위를 확장해 Net::OpenTimeout, Net::ReadTimeout, Seahorse::Client::NetworkingError, Aws::Kinesis::Errors::ServiceError 등 Kinesis-쪽 transient 예외를 포함시키고, 로그 레벨을 warn 으로 낮춘 뒤 예외를 재발생시키지 않도록 한다. 이벤트 publish 는 도메인 write 의 부수효과이므로, transient network 로 인해 원 write 트랜잭션이 실패해서는 안 된다.
  • 근거: Eventable::Events::Base.create_eventafter_create/after_update 콜백(callbacks.rb:30-40) 에서 호출되므로, raise e 는 Rails 트랜잭션 롤백과 5xx 응답을 유발한다. 14일 6건은 사용자 write 실패로 흡수될 만한 빈도가 아니다.

단기 개선 (1주 이내)#

  • Kinesis publish 를 별도 Sidekiq worker (예: 기존 KinesisPutRecordsWorker) 로 비동기화한다. Eventable::Events::Base 는 job enqueue 만 수행하고, 실제 Kinesis 호출은 worker 에서 실행 → transient 실패 시 Sidekiq retry 로 흡수. 이렇게 하면 요청 응답 시간에서 Kinesis SDK 의 open_timeout 도 제거된다.
  • Cupix::Aws::Kinesis.kinesis_client 의 timeout 파라미터(http_open_timeout, http_read_timeout) 를 명시적으로 짧게(예: 2s / 5s) 지정하고 SDK retry_limit / retry_backoff 를 튜닝한다. 현재 코드는 SDK 기본값에 의존한다.

장기 개선 (재발 방지)#

  • 도메인 이벤트를 outbox 패턴 으로 전환: DB 트랜잭션 안에서 outbox 테이블에 이벤트를 커밋하고, 별도 워커/relay 가 Kinesis 로 publish. 이 경우 Kinesis 장애가 write 경로에 절대 영향을 주지 않고, 재시도/순서 보장이 자연스럽게 성립한다.
  • Kinesis publish 실패 메트릭을 별도 트래킹하고, us-west-2 outbound 네트워크 error rate 를 관측하는 alert 를 추가한다.

Monitoring#

Kinesis publish 실패 빈도 (수정 배포 후 이 그래프가 사라져야 함):

text
service:cupixworks-api status:error "Failed to put records"

수정으로 인해 사용자 요청 실패로 전파되지 않는지 확인:

text
service:cupixworks-api status:error "Failed to create event" ("Net::OpenTimeout" OR "end of file reached" OR "execution expired")

Kinesis 스트림 자체의 health (AWS 메트릭):

text
sum:aws.kinesis.put_records.failed_records{streamname:*cupixworks-production-EventStream*}.as_count()

Risk Assessment#

  • Risk level: medium — 산발적이지만 사용자 write 요청 실패로 전파될 가능성이 있는 경로. 프로덕션 트래픽 시간대(KST 새벽) 에도 관찰됨.
  • 예상 복잡도: standard — 즉시 조치(rescue 확장 + swallow) 는 몇 줄 변경. 단기/장기 개선은 비동기화 워커 도입이 필요해 별도 태스크로 분리 권장.