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#
- 2026-07-10 12:51:13 KST —
PUT /api/v1/captures/732118정상 200 응답 (reconstruction_state transitioned to done) - 2026-07-10 13:06:20 KST —
Cupix::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) 반환 - 이후 동일 fingerprint 로그 재발 없음 (Datadog
service:cupixworks-api "SSL_connect"last 14d 검색 결과 5건 중 관련 항목 3건이 모두 동일 timestamp 13:06:20 KST에 집중)
Error Log#
Failed to put records: Connection reset by peer - SSL_connect
동일 요청 컨텍스트에서 관측된 관련 로그:
{
"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!"
}
{
"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" }
}
{
"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#update의 rescue 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 SDKkinesis_client.put_records호출 중 SSL_connect 실패
Controller 진입 지점:
def update
@model = repository_instance.update(params)
super
end
Repository 는 @model.save! 이후 모든 StandardError 를 Cupix::Errors::Parameter(ARG10001) 로 재포장한다. 여기서 save! 는 ActiveRecord after_commit 훅을 통해 Eventable 흐름을 트리거하므로, Kinesis 네트워크 오류도 이 rescue 에 걸린다.
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 오류가 요청 흐름을 중단시킨다.
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 는 별도 처리가 없어 그대로 상위로 전파된다.
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:
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 응답 렌더링:
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 쿼리 (재현용):
service:cupixworks-api status:error @environment:production "Failed to put records: Connection reset by peer - SSL_connect"
service:cupixworks-api "SSL_connect"
service:cupixworks-api "732118"
첫 번째 쿼리 결과 1건:
{
"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 응답:
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#update의 rescue 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-191—rescue 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_event의rescue에서 인프라 예외에 한해 log 후 swallow. 방향만 제시하며 실제 구현은 별도 티켓에서 아키텍처 담당자와 협의 필요.lib/cupix/aws/kinesis.rb— 명시적 retry (예: AWS SDKretry_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 오류 트렌드:
service:cupixworks-api "put_records" "SSL_connect"
- Event publish 실패 트렌드 (Kinesis + RestClient 모두 포함):
service:cupixworks-api ("Failed to put records" OR "Failed to publish event" OR "Failed to create event")
- 인프라 오류가 400으로 오분류된 케이스 감지:
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.