Skip to content

Add syslog stream and datagram listeners - #9

Merged
alistairjevans merged 7 commits into
masterfrom
syslog-listeners
Sep 21, 2026
Merged

alistairjevans merged 7 commits into
masterfrom
syslog-listeners

Conversation

@alistairjevans

@alistairjevans alistairjevans commented Sep 21, 2026

Copy link
Copy Markdown
Contributor

Summary

Adds syslog transport to the server alongside HTTP: a stream listener (Unix socket or TCP, optional PROXY protocol v2 for the peer address) framed with RFC 6587 octet counting and a newline fallback, and a datagram listener (UDP, SO_REUSEPORT) where one datagram is one message.

Messages are delivered on the existing worker threads with the same GVL discipline as requests: the block passed to Server#run_worker receives either a HyperRuby::Request (returns a Response, as before) or a HyperRuby::SyslogMessage (message, peer_ip, transport, received_at_ns, message_id, attempt) and returns whether the message was admitted. A refused stream message is retried with a bounded delay while that connection's reads stall; a refused datagram is dropped and counted.

Details

  • Framing is a port of Vector 0.48.0's octet-counting decoder (ext/hyper_ruby/src/syslog/framing.rs, MPL-2.0, noted in LICENSE.txt and the gemspec's licences), with one documented divergence: a read boundary landing on a digit inside a discarded oversize body no longer restarts counting.
  • Bounds: per-connection and global pending limits, max frame bytes, max connections, optional idle timeout, datagram size with truncation detection, PROXY header size and read timeout.
  • Fairness: with both channels ready a worker takes up to syslog_work_ratio messages per request (default 4); servers without a syslog listener run the unchanged HTTP-only loop.
  • Lifecycle: listeners start and stop with the server; shutdown drains framed messages and counts what it could not admit. Server#syslog_listening? and Server#syslog_stats expose readiness and counters.
  • Configuration keys are documented in lib/hyper_ruby.rb.

Tests

  • cargo test: 28 Rust tests (framing at every byte boundary, PROXY parsing, oversize and UTF-8 handling).
  • rake test: 67 runs; the syslog suite covers TCP and UDP loopback, split writes, oversize frames, invalid UTF-8, PROXY v2, backpressure isolation between connections, retry ids and attempts, delivery under HTTP load, and clean shutdown. The one failing test, TestBackpressure#test_backpressure_handling, fails identically on master.

alistairjevans and others added 5 commits September 21, 2026 10:06
Adds two syslog transports to the server alongside HTTP, both delivering
complete messages to a Ruby handler on the existing worker threads:

- a stream listener (Unix socket or TCP) framed with RFC 6587 octet
  counting and a newline fallback, with optional PROXY v2 parsing to
  recover the peer address, and
- a UDP listener with SO_REUSEPORT where one datagram is one message,
  with receive-buffer truncation detected and dropped.

Undelivered messages are bounded per connection and overall; a refused
message stalls stream reads and is retried, while a refused datagram is
dropped and counted. Listener readiness and transport counters are
exposed as syslog_listening? and syslog_stats.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
The worker fast path always drained requests first, so sustained request
load could starve syslog delivery indefinitely and stall every sender
behind the per-connection backpressure. The two channels now alternate
which is asked first, and the blocking select already picks uniformly
between them. Workers with no syslog listener configured keep the
original single-channel path.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Licence: the framing module, including its test data, is a port of
Vector 0.48.0's octet counting decoder, so the file, the README and the
licence file record that it is MPL-2.0 rather than MIT.

Framing: a read boundary landing on a digit inside an oversize body no
longer restarts octet counting and hands that body over as messages.

Delivery: the per-connection queue is bounded separately from the
listener-wide one, so a stalled sender cannot take every credit; the
handler is given a stable message id and attempt number, its exceptions
are counted apart from refusals, and retries end when shutdown starts.
Messages the drain could not admit are counted instead of vanishing.

Lifecycle: a cancellation token replaces the per-connection broadcast so
a connection accepted in the same round as the shutdown still sees it,
and stop() takes owned handles before releasing the GVL. Connection and
idle limits bound stream resources, and datagram receive errors back
off. A PROXY header naming a Unix or unspecified family falls back to
the transport peer, with read failures counted separately.

Workers may take several syslog messages per request, so a busy request
queue cannot pace syslog delivery. The handler now reports :stream or
:datagram, a peerless stream gives nil, the datagram buffer keys are
named for what they bound, and the handler stays reachable through the
server object rather than a permanent GC root.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Requests and syslog messages now reach Ruby the same way: the block
given to run_worker receives either a Request, which it answers with a
Response, or a SyslogMessage, which it answers with an admission
verdict. The separate configured handler, and the GC root it needed, are
gone; delivery, limits, retries and accounting are unchanged.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
The framing module is MPL-2.0 while the rest is MIT, so the gem metadata
lists both.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
@alistairjevans alistairjevans changed the title [Fable] Add syslog stream and datagram listeners Add syslog stream and datagram listeners Sep 21, 2026
alistairjevans and others added 2 commits September 21, 2026 10:50
A block that answers a syslog message with a response was not giving a
verdict, but its response read as truthy and admitted the message. That
now logs once and refuses, counted as a handler error, and the failure
log names the message and its transport.

The message object is freed as soon as it is collected and reports its
payload to the GC, its string is built without revalidating bytes the
framer already checked, and inspect prints the transport as the reader
returns it.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
The README described the removed callable, and the extension manifest
named only one of the two licences the gem declares.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
@alistairjevans
alistairjevans merged commit 642454c into master Sep 21, 2026
3 checks passed
@alistairjevans
alistairjevans deleted the syslog-listeners branch September 21, 2026 18:00
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant