Skip to content

Add unix/unixgram destinations + table-driven tests#9

Merged
randomizedcoder merged 2 commits into
mainfrom
uds-destination
Jun 3, 2026
Merged

Add unix/unixgram destinations + table-driven tests#9
randomizedcoder merged 2 commits into
mainfrom
uds-destination

Conversation

@randomizedcoder

Copy link
Copy Markdown
Owner

Summary

Adds two stdlib-only destinations alongside the existing kafka / udp / nsq / nats / valkey / null set, with table-driven coverage:

  • unix:/path/to/sock — SOCK_STREAM, framed with a varint length prefix per record. Header + payload now coalesced into one writev(2) via net.Buffers (see "Reliability hardening" below).
  • unixgram:/path/to/sock — SOCK_DGRAM. One record per datagram; the kernel preserves message boundaries.

CLI: -dest unix:/run/xtcp2.sock or -dest unixgram:/run/xtcp2.sock.

Architectural touches

  • New fatalf field on XTCP (defaults to log.Fatalf) so Init* paths can be exercised from tests without taking the process down. Existing destinations still call log.Fatalf directly — opt-in.
  • closeDestination now strings.Cuts the scheme so the switch is "unix" / "unixgram" rather than the full -dest string.
  • Proto dest max_len bumped 40 → 128 to fit unixgram: (9 bytes) + Linux sun_path (108 bytes).
  • Concurrency contract spelled out at the top of destinations.go: "implementations may assume serial access; concurrent callers are not supported without an internal mutex."

Reliability hardening (the substantive small fix on top of the original commit)

destUnix was issuing two sequential Write calls — varint header, then payload. If the header succeeded and the payload failed partway through (peer disconnect, EPIPE on backpressure), the receiver saw a varint header promising N bytes followed by fewer than N bytes of data — an unrecoverable torn-frame state that would wedge the daemon-side binary.ReadUvarint + io.ReadFull reader. Switched to net.Buffers{hdr, payload}.WriteTo(conn), which Go's stdlib lowers to a single writev(2) on *net.UnixConn. Header + payload land atomically or fail as a unit. Same wire shape, same accounting, half the syscalls in the happy path.

Test plan

  • go build ./pkg/xtcp/... clean
  • go vet ./pkg/xtcp/... clean
  • go test -race -run 'TestDestinations|TestDestUnix|TestDestUnixGram' ./pkg/xtcp/ PASS
  • Table-driven TestDestinations covers null + udp + unix + unixgram with single + multi-record cases.
  • TestDestUnix_StreamFraming exercises payloads of 1 / 256 / 50 KB (the 50 KB case exercises the multi-byte varint path).
  • TestDestUnixGram_MissingSocket and TestDestUnix_MissingDaemon verify the fail-loudly contract via the captured fatalf hook.
  • Benchmarks for all four destinations with b.SetBytes for per-record throughput reporting.

Known follow-ups (not blocking)

  • Setup-helper duplication: setupUDPDest / setupUDPDestTB etc. are near-identical pairs because the helpers take *testing.T while benchmarks need testing.TB. Taking testing.TB directly would collapse the pair.
  • InitDestUnixGram's os.Stat pre-check has a small TOCTOU window with the subsequent net.Dial("unixgram", path). Tiny in practice (unixgram is connectionless so dial can't verify the peer anyway); worth documenting if anyone cares.
  • No round-trip test case for an empty payload (varint of 0 is one byte). Implementation handles it correctly; coverage gap.

🤖 Generated with Claude Code

randomizedcoder and others added 2 commits June 3, 2026 14:25
xtcp2 now supports writing records to a local unix-domain socket so that
a daemon (kubernetes daemonset, machine-local collector, etc.) can read
them off without a network hop. Two schemes:

  unix:/path/to/sock      SOCK_STREAM, varint-length-prefixed framing
  unixgram:/path/to/sock  SOCK_DGRAM, one Write == one datagram == one
                          record, no framing

Both follow the established destination pattern (function stored in
sync.Map, init function dialled once at startup, blocking writes, log
on error). For unixgram, init pre-checks os.Stat so the "fail loudly at
startup" contract holds even though SOCK_DGRAM dial doesn't verify the
peer.

The dest proto field's max_len cap goes from 40 to 128 to accommodate
the longer unixgram:/path strings; comment is rewritten to enumerate
all current schemes. Generated bindings regenerated via buf.

Adds the repo's first destination_test.go: one table-driven test
covering null/udp/unix/unixgram round-trip and multiple-record cases,
a varying-size stream-framing test, missing-socket / missing-daemon
sanity tests, and benchmarks for all four. Each row uses a real local
socket in t.TempDir() — no mocks. To make InitDest* paths testable
without taking the process down, a hookable x.fatalf field on XTCP
defaults to log.Fatalf and is overridden to t.Fatalf in tests; only
the two new InitDest* functions use it, existing destinations are
untouched.

go test -race ./pkg/xtcp/... -run TestDest is clean.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
… one writev

destUnix was issuing two sequential net.Conn.Write calls — first the
varint length header, then the payload. If the header write succeeded
but the payload write failed midway (peer disconnect, EPIPE during a
backpressure event, etc.) the receiver was left with a varint length
prefix promising N bytes followed by fewer than N bytes of data. That
shape is unrecoverable from the daemon-side reader's
binary.ReadUvarint + io.ReadFull pattern: the next reader pass would
read garbage into the next-frame header position and never recover.

Switch to net.Buffers{hdr, payload}.WriteTo(conn). Go's stdlib lowers
this to a single writev(2) on *net.UnixConn — header and payload land
atomically or fail as a unit. Same on-wire shape, same per-record
accounting, half the syscalls in the happy path. Removes the
TODO that explicitly called this out.

Tested:
- go build ./pkg/xtcp/... clean
- go vet ./pkg/xtcp/... clean
- go test -race -run 'TestDestinations|TestDestUnix|TestDestUnixGram' ./pkg/xtcp/
  PASS

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
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