Class: Beacon::Transport::Http
- Inherits:
-
Object
- Object
- Beacon::Transport::Http
- Defined in:
- lib/beacon/transport.rb
Overview
Persistent Net::HTTP-backed transport. Stdlib only — a Beacon host should not need Faraday or HTTParty just to ship events.
Why persistent: the Ruby client runs on a host serving real traffic. Opening a fresh TCP+TLS connection per flush (the pre-Card-4 behavior) turns the flusher into a source of background network chatter — one handshake/s sustained on a busy box. That handshake cost shows up in Beacon’s own dashboards as noise and adds pointless tail latency on the Beacon server.
Reconnect policy: a single persistent connection is held under a Mutex. On ‘Errno::EPIPE` / `Errno::ECONNRESET` / `EOFError` / `IOError` during `request`, we close the connection, open a fresh one, and retry the request ONCE. If the retry also fails, the exception propagates into `post`’s rescue and surfaces as a ‘Result` transport error, which the flusher turns into a backoff/retry at its own layer. Two layers of retry is intentional — transport-level heals a flaky socket cheaply, flusher-level handles longer outages with backoff and a circuit breaker.
Fork safety: the held connection is a socket FD. If a parent process inherits a forked child, sharing the FD is undefined. ‘after_fork` drops the connection so the child opens its own on first request. `Client#after_fork` calls it during its own fork handling.
Constant Summary collapse
- RECONNECTABLE_ERRORS =
Errors that mean “this socket is dead, reconnect and retry once.” Timeouts are included because a Net::ReadTimeout leaves the Net::HTTP instance in an indeterminate state (partial data buffered, server may still be writing) — continuing to reuse it is a known footgun. Beacon’s idempotency-key header makes retry after a timeout safe from the server’s perspective.
[ Errno::EPIPE, Errno::ECONNRESET, EOFError, IOError, Net::OpenTimeout, Net::ReadTimeout, ].freeze
Instance Method Summary collapse
-
#after_fork ⇒ Object
Drop the held connection.
-
#initialize(config) ⇒ Http
constructor
A new instance of Http.
- #post(body, idempotency_key:) ⇒ Object
-
#reconnects ⇒ Object
Number of times a dead socket was recovered by the reconnect-once path.
Constructor Details
#initialize(config) ⇒ Http
Returns a new instance of Http.
49 50 51 52 53 54 55 56 |
# File 'lib/beacon/transport.rb', line 49 def initialize(config) @config = config @uri = URI.parse("#{config.endpoint.to_s.chomp("/")}/api/events") @mutex = Mutex.new @http = nil @user_agent = "beacon-client/#{Beacon::VERSION} (ruby #{RUBY_VERSION})".freeze @reconnects = 0 end |
Instance Method Details
#after_fork ⇒ Object
Drop the held connection. Called by Client#after_fork so a forked child never writes into a socket FD inherited from its parent.
81 82 83 |
# File 'lib/beacon/transport.rb', line 81 def after_fork @mutex.synchronize { close_connection } end |
#post(body, idempotency_key:) ⇒ Object
66 67 68 69 70 71 72 73 74 75 76 77 |
# File 'lib/beacon/transport.rb', line 66 def post(body, idempotency_key:) response = @mutex.synchronize do request_with_reconnect { build_request(body, idempotency_key) } end Result.new( status: response.code.to_i, retry_after: response["retry-after"]&.to_i, error: nil, ) rescue => e Result.new(status: 0, retry_after: nil, error: e) end |
#reconnects ⇒ Object
Number of times a dead socket was recovered by the reconnect-once path. Exposed via Beacon.stats so operators can distinguish “Beacon P99 spiked because the server was flaky and we healed it” from “Beacon P99 spiked because of something else.”
62 63 64 |
# File 'lib/beacon/transport.rb', line 62 def reconnects @mutex.synchronize { @reconnects } end |