ES /docs

Failed to put records: Connection reset by peer - SSL_connect

RCA: Failed to put records: Connection reset by peer - SSL_connect

Overview#

What Happened#

2026-07-10 13:06:20 KST에 cupixworks-api (us-west-2, production)의 Cupix::Aws::Kinesis.put_records!가 AWS Kinesis 엔드포인트에 대한 TLS 세션 도중 Connection reset by peer - SSL_connect 예외로 실패했다. 이 실패가 Capture#update 요청 흐름의 Eventable 훅에서 발생했기 때문에, CaptureRepository#update의 광범위한 rescue StandardError가 네트워크 오류를 Cupix::Errors::Parameter(ARG10001)로 재포장하여 HTTP 400을 반환했다. 단일 요청(capture 732118) 1회만 관측된 transient 이벤트다.

Quick Facts#

Field Value
exception.class Cupix::Errors::Parameter (외부 표면), 원인 예외는 AWS SDK의 Seahorse::Client::NetworkingError / OpenSSL::SSL::SSLError 계열
exception.message Connection reset by peer - SSL_connect
top_frame lib/cupix/aws/kinesis.rb:14 (put_records!)
env production, region us-west-2, tenant cupix

Affected Teams#

Team / Domain Error Count Impact
cupixworks-api (Capture update) 1 단일 PUT /api/v1/captures/732118 요청이 HTTP 400 (ARG10001)으로 실패. 동일 capture는 15분 전(12:51 KST) 및 이후 정상 응답 확인 — 사용자 영향은 이 1건 재시도로 회복 가능한 수준.

Timeline#

  1. 2026-07-10 12:51:13 KSTPUT /api/v1/captures/732118 정상 200 응답 (reconstruction_state transitioned to done)
  2. 2026-07-10 13:06:20 KSTCupix::Aws::Kinesis.put_records!Connection reset by peer - SSL_connect로 실패, Eventable::Events::Update.create_event가 동일 메시지로 실패 로그를 남긴 후 예외를 상위로 전파, PUT /api/v1/captures/732118 요청이 HTTP 400 (ARG10001, Cupix::Errors::Parameter) 반환
  3. 이후 동일 fingerprint 로그 재발 없음 (Datadog service:cupixworks-api "SSL_connect" last 14d 검색 결과 5건 중 관련 항목 3건이 모두 동일 timestamp 13:06:20 KST에 집중)

Error Log#

Datadog Logs

text
Failed to put records: Connection reset by peer - SSL_connect

동일 요청 컨텍스트에서 관측된 관련 로그:

json
{
  "timestamp": "2026-07-10 13:06:20 KST",
  "status": "error",
  "message": "Failed to put records: Connection reset by peer - SSL_connect",
  "class": "Cupix::Aws::Kinesis",
  "function": "put_records!"
}
json
{
  "timestamp": "2026-07-10 13:06:20 KST",
  "status": "error",
  "message": "Failed to create event: Connection reset by peer - SSL_connect",
  "class": "Eventable::Events::Update",
  "function": "create_event",
  "error": { "msg": "Connection reset by peer - SSL_connect" }
}
json
{
  "timestamp": "2026-07-10 13:06:20 KST",
  "status": "info",
  "message": "[400] PUT /api/v1/captures/732118 (Api::V1::CapturesController#update)",
  "error": {
    "reason": "Invalid argument",
    "code": "ARG10001",
    "message": "Connection reset by peer - SSL_connect",
    "class": "Cupix::Errors::Parameter"
  }
}

Impact#

  • Service: cupixworks-api
  • 발생 횟수: 1
  • 최초 발생: 2026-07-10 13:06:20 KST
  • 최근 발생: 2026-07-10 13:06:20 KST

Datadog에서 service:cupixworks-api "SSL_connect" 로 14일 범위를 검색한 결과 총 5건, 그 중 오늘 이벤트에 해당하는 것은 정확히 13:06:20 KST의 3건(같은 요청 흐름의 로그)뿐이며, 나머지 2건은 2026-07-05 별개 이벤트(Seahorse::Client::NetworkingError on pano tile upload). 즉 오늘 이 클러스터는 1건의 transient 발생이다.

Root Cause Summary#

Cupix::EventService.publish_event가 Capture after_save 훅 안에서 동기적으로 AWS Kinesis에 put_records를 호출하는 도중, us-west-2 Kinesis 엔드포인트와의 TLS 핸드셰이크가 Connection reset by peer (server 측 RST)로 끊겼다. 이는 AWS SDK가 던지는 순수 네트워크 예외지만, 상위 호출자인 CaptureRepository#updaterescue StandardError가 원인 예외를 구분 없이 Cupix::Errors::Parameter(ARG10001, "Invalid argument")로 재포장했다. 그 결과 인프라 계열 transient 실패가 클라이언트 파라미터 오류로 잘못 분류되어 HTTP 400으로 반환되었고, Eventable 실패가 요청 자체를 실패시키는 hard-coupling 문제도 함께 노출되었다. transient TLS reset 자체는 원인 (AWS Kinesis 측 idle connection reap, connection pool의 stale socket, 또는 순간적 네트워크 hiccup — 단일 발생이므로 특정 원인 확정 불가, uncertain -- needs verification) 이며, 코드 문제는 이 원인 예외를 잘못 분류·전파한 부분이다.

Technical Analysis#

Code Path#

  • Entry point: app/controllers/api/v1/captures_controller.rb:46 (Api::V1::CapturesController#update)
  • Repository 저장: app/repositories/capture_repository.rb:182-194@model.save! 실행 후 광범위 rescue 로 예외 변환
  • Eventable 훅: app/models/concerns/eventable/events/base.rb:7-26 (Eventable::Events::Base.create_event)
  • Event 발행: lib/cupix/event_service.rb:21-54 (Cupix::EventService.publish_event)
  • Failure point: lib/cupix/aws/kinesis.rb:9-17 (Cupix::Aws::Kinesis.put_records!) — AWS SDK kinesis_client.put_records 호출 중 SSL_connect 실패

Controller 진입 지점:

app/controllers/api/v1/captures_controller.rb:46-50ruby
def update
  @model = repository_instance.update(params)

  super
end

Repository 는 @model.save! 이후 모든 StandardErrorCupix::Errors::Parameter(ARG10001) 로 재포장한다. 여기서 save! 는 ActiveRecord after_commit 훅을 통해 Eventable 흐름을 트리거하므로, Kinesis 네트워크 오류도 이 rescue 에 걸린다.

app/repositories/capture_repository.rb:182-194ruby
def update(params = {})
  super

  set_parameters(params)

  begin
    @model.save!
  rescue StandardError => e
    raise Cupix::Errors::Parameter.new(code: 'ARG10001', reason: 'Invalid argument', message: e.message)
  end

  @model
end

Eventable base 는 StandardError 전체를 잡아 로그만 남기고 그대로 re-raise 한다. 즉 Kinesis 오류가 요청 흐름을 중단시킨다.

app/models/concerns/eventable/events/base.rb:10-26ruby
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

EventService 는 RestClient::Exception 만 특수 처리하고, AWS SDK 의 Seahorse::Client::NetworkingError / OpenSSL::SSL::SSLError 는 별도 처리가 없어 그대로 상위로 전파된다.

lib/cupix/event_service.rb:42-53ruby
response = Cupix::Aws::Kinesis.put_records!({ stream_name: stream_name, records: records })
Cupix::Logger.info("Published event - failed_record_count: #{response.failed_record_count} / #{records.size}", class: self.name, function: __method__, event: _event, model: { type: _event.eventable_type, id: _event.eventable_id })

errors = response.records.select { |record| record.error_code.present? } rescue []
if errors.present?
  Cupix::Logger.error("Failed to publish event: #{errors.map(&:error_message).join(', ') rescue nil}", class: self.name, function: __method__, event: _event, model: { type: _event.eventable_type, id: _event.eventable_id })
end
rescue RestClient::Exception => e
  Cupix::Logger.error("Failed to publish event: #{e.message}", class: self.name, function: __method__, event: event, model: { type: _event.eventable_type, id: _event.eventable_id })

  raise Cupix::Errors::System.new(code: 'SYS20000', reason: "Failed to publish event: #{e.message}")
end

최종 실패 지점 — SDK 예외를 그대로 재-raise:

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

400 응답 렌더링:

app/controllers/concerns/client_error_controller.rb:7-14ruby
rescue_from Cupix::Errors::Unknown,
            Cupix::Errors::Resource,
            Cupix::Errors::Session,
            Cupix::Errors::Parameter,
            Cupix::Errors::Entity,
            Cupix::Errors::Billing,
            Cupix::Errors::InvalidState,
            Cupix::Errors::Siteinsights, with: :client_400_error

기대 동작: 인프라(TLS/네트워크) 오류는 5xx 로 응답하고 event publish 실패는 요청 성공을 막지 않아야 한다. 실제 동작: Kinesis TLS 실패가 400 ARG10001 로 사용자에게 노출되고, Capture update 자체는 이미 DB 저장이 커밋된 상태에서 요청은 실패로 반환됐다 (after_commit 훅이 발생시킨 예외가 저장 후 흐름을 되돌리지는 못하지만 응답은 400).

Log Evidence#

Datadog 쿼리 (재현용):

text
service:cupixworks-api status:error @environment:production "Failed to put records: Connection reset by peer - SSL_connect"
text
service:cupixworks-api "SSL_connect"
text
service:cupixworks-api "732118"

첫 번째 쿼리 결과 1건:

json
{
  "timestamp": "2026-07-10 13:06:20 KST",
  "status": "error",
  "message": "Failed to put records: Connection reset by peer - SSL_connect",
  "class": "Cupix::Aws::Kinesis",
  "function": "put_records!"
}

같은 요청의 앞뒤 컨텍스트 — capture 732118은 15분 전과 그 전에는 정상 200 응답:

text
2026-07-10 12:51:13 KST  [200] PUT /api/v1/captures/732118 (Api::V1::CapturesController#update)
2026-07-10 12:51:13 KST  reconstruction_state has transitioned from processing to done on Capture 732118
2026-07-10 13:06:20 KST  [400] PUT /api/v1/captures/732118  ← 본 이슈

SSL_connect 키워드 14일 검색 결과에서, 같은 fingerprint의 재발은 없고 2026-07-05에 별개 Seahorse::Client::NetworkingError (pano tile upload, unexpected eof while reading)만 관측됨 — transient 이벤트임을 시사.

Hypotheses Considered#

# Hypothesis Evidence for Evidence against Verdict
H1 AWS Kinesis 엔드포인트와의 TLS 핸드셰이크 도중 transient network reset (idle socket reap, 잠깐의 네트워크 hiccup, 또는 클라이언트 측 stale connection pool 항목)로 Cupix::Aws::Kinesis.put_records!가 실패했다 로그 메시지 "Connection reset by peer - SSL_connect"는 TCP RST가 TLS 핸드셰이크 중 발생했을 때 OpenSSL이 던지는 문구; lib/cupix/aws/kinesis.rb:14가 원 SDK 예외 메시지를 그대로 로그; 단일 발생(occurrence_count=1), 앞뒤로 같은 capture 정상 200 응답 존재 특정 root cause (AWS 측 outage vs 클라이언트 conn pool)를 확정할 근거 없음 — uncertain -- needs verification Confirmed (transient network fault)
H2 CaptureRepository#updaterescue StandardError가 인프라 예외를 클라이언트 오류(400 ARG10001)로 오분류하여 사용자에게 부적절한 상태 코드를 반환하고 있다 app/repositories/capture_repository.rb:189-190 — 모든 StandardError를 Cupix::Errors::Parameter 로 재포장; Datadog 응답 로그에 [400], code: ARG10001, class: Cupix::Errors::Parameter, message: "Connection reset by peer - SSL_connect" 관측 Confirmed (secondary defect that amplifies impact)
H3 잘못된 파라미터 (예: 유효하지 않은 값)로 인해 Capture#save!가 validation 실패하여 진짜 ARG10001 이 발생했다 응답 코드가 ARG10001 "Invalid argument"임 원 메시지가 정확히 "Connection reset by peer - SSL_connect"이며 이는 ActiveRecord validation 문구가 아닌 OpenSSL 문구; 같은 capture가 15분 전 성공, 이후 정상 처리; validation 실패라면 여러 요청에서 재발해야 하나 1건만 관측 Rejected
H4 Kinesis stream 자체가 존재하지 않아 Cupix::Errors::Argument.new(code: 'ARG10001', ...) 가 kinesis.rb 상단 guard에서 raised되었다 코드 kinesis.rb:6-7에 stream_name/records blank 시 raise; 응답 code=ARG10001 로그 메시지가 "Failed to put records: Connection reset by peer - SSL_connect" 이며 이는 rescue => e 이후 로그로 kinesis_client.put_records 호출 자체가 실패했음을 의미; blank guard 발동 시 로그는 "Failed to put records: stream_name is blank" 형태여야 함 Rejected
H5 다른 서비스(EventService의 RestClient 호출)에서 발생한 오류가 같은 스택에 섞여 나왔다 EventService에 rescue RestClient::Exception 존재 로그의 top-level class 가 명확히 Cupix::Aws::Kinesis / Eventable::Events::Update; RestClient 예외는 별도 SYS20000 코드로 매핑되며 ARG10001 아님 Rejected

Fix Recommendation#

즉시 조치 (Critical)#

  • 없음. 1회만 발생한 transient 이벤트이며 사용자는 요청 재시도로 회복 가능. 지금 시점에 hotfix 대상 아님.

단기 개선 (1주 이내)#

  • app/repositories/capture_repository.rb:187-191rescue StandardError 를 실제로 예상되는 검증/저장 오류(ActiveRecord::RecordInvalid, ActiveRecord::RecordNotSaved, ArgumentError, TypeError)로 좁힌다. 인프라·네트워크 예외 (AWS SDK 계열, Seahorse::Client::NetworkingError, OpenSSL::SSL::SSLError, Aws::Errors::ServiceError)는 이 rescue에서 catch되지 않도록 하여 5xx 계열로 상위 핸들러가 처리하게 한다. 같은 패턴이 app/repositories/*_repository.rb 전반에 퍼져 있으므로 (facility_repository, pano_repository, team_repository, video_repository 등) 함께 검토 — 단, 이번 RCA 범위는 capture_repository 만 확정. 다른 리포지토리의 광범위 rescue 재검토는 별도 티켓으로 분리 권장.
  • lib/cupix/event_service.rb:21-54 / app/models/concerns/eventable/events/base.rb:19-21 — event publish 실패가 원본 도메인 요청 (Capture update)을 실패시키지 않도록 out-of-band 처리 검토. 옵션: (a) Cupix::Event.publish / Cupix::EventService.publish_event 를 별도 Sidekiq worker로 위임 (after_commit :publish_event_later), (b) Eventable::Events::Base.create_eventrescue에서 인프라 예외에 한해 log 후 swallow. 방향만 제시하며 실제 구현은 별도 티켓에서 아키텍처 담당자와 협의 필요.
  • lib/cupix/aws/kinesis.rb — 명시적 retry (예: AWS SDK retry_limit, retry_backoff) 설정을 client 생성부에 추가 검토. 현재 코드는 ::Aws::Kinesis::Client.new(region: ...)만 사용하여 SDK 기본 재시도에 의존한다. 기본값이 SSL_connect 계열 예외를 재시도하는지 확인 필요 — uncertain -- needs verification.

장기 개선 (재발 방지)#

  • Eventable 발행을 요청 흐름과 분리하는 아키텍처 개선 (async publish + DLQ). 이번 이슈는 낮은 빈도지만, 도메인 성공 후 관측성 이벤트 실패로 사용자 요청이 실패하는 카테고리는 재발 시 대량 500/400을 유발할 수 있다.
  • Cupix::Errors::* 오류 분류 정책 문서화 — 인프라 계열(SYS20000)과 클라이언트 계열(ARG10001) 사이의 경계를 각 repository/service 레이어에서 지키는 규약 정립.

Monitoring#

  • Kinesis SSL/network 오류 트렌드:
text
service:cupixworks-api "put_records" "SSL_connect"
  • Event publish 실패 트렌드 (Kinesis + RestClient 모두 포함):
text
service:cupixworks-api ("Failed to put records" OR "Failed to publish event" OR "Failed to create event")
  • 인프라 오류가 400으로 오분류된 케이스 감지:
text
service:cupixworks-api status:info "[400]" ("SSL_connect" OR "NetworkingError" OR "Connection reset")

각 쿼리는 timeseries widget에 그대로 사용 가능하며 monitor-only 문법(| stats, count by(...))을 포함하지 않는다.

Risk Assessment#

  • Risk level: low
  • 예상 복잡도: standard

단일 발생·회복 가능 이슈지만, rescue StandardError 광범위 사용 패턴이 여러 리포지토리에 존재하므로 유사 오분류가 다른 흐름에서 반복될 수 있다는 잠재 위험은 medium.