Skip to content

Navigation Menu

Sign in
Appearance settings

Search code, repositories, users, issues, pull requests...

Provide feedback

We read every piece of feedback, and take your input very seriously.

Saved searches

Use saved searches to filter your results more quickly

Appearance settings

feat(serf-compio)!: API parity with serf-reactor (+ 3 real defects fixed)#97

Open
al8n wants to merge 6 commits into
refactor/sansioal8n/serf:refactor/sansiofrom
feat/compio-parityal8n/serf:feat/compio-parityCopy head branch name to clipboard
Open

feat(serf-compio)!: API parity with serf-reactor (+ 3 real defects fixed)#97
al8n wants to merge 6 commits into
refactor/sansioal8n/serf:refactor/sansiofrom
feat/compio-parityal8n/serf:feat/compio-parityCopy head branch name to clipboard

Conversation

@al8n

@al8n al8n commented Jul 13, 2026

Copy link
Copy Markdown
Owner

What

Brings serf-compio to public-API parity with serf-reactor. The two crates wrap the same serf-proto machine and are meant to be feature-equivalent siblings differing only in runtime model (Send/agnostic vs !Send/compio) — they had drifted badly. An audit found 14 divergences; three of them were real defects, not just missing surface.

Breaking changes are intentional and were authorized.

The three defects (fixes, not parity surface)

1. compio sent QUIC gossip as plaintext UDP. UnreliableTransport::Datagram is the default and is documented "encrypted + attested", but compio's QUIC driver ignored the setting and sent every gossip packet over a plain UDP socket — so a node configured for datagram gossip silently got unencrypted UDP. The driver now queues datagrams on the connection and falls back to UDP only where the send status directs. Mutation-verified: forcing gossip back onto UDP fails the datagram test while the Udp-opt-out test still passes — the two together are the discriminator.

2. The dns and getifs resolvers could not build a node — in both crates. OsResolver/DnsResolver declare Address = HostAddr<SmolStr>, but the Transport impls demanded A: Data, and HostAddr has no Data impl (the orphan rule makes one impossible). So the resolvers were unusable for the advertise address and the declared default A = HostAddr<SmolStr> was dead. The bound was gratuitous — A is resolved to a SocketAddr at construction and never encoded. Removed. Verified: a node now builds and joins over A = HostAddr<SmolStr>, and re-adding the bound reproduces the exact user-facing error.

3. compio's DNS resolver defeated its own timeout. DnsResolver::resolve swallowed every tcp_query error including TimedOut and escalated into the unbounded OS resolver — a slow or hostile nameserver could burn the TCP budget then hang bootstrap. Now a timed-out query surfaces the error, matching the reactor. This was caught by a ported reactor unit test (failed pre-fix, passes post-fix).

A fourth, smaller one: compio's quic-only build didn't compile (ExchangeId gated on tcp alone while the QUIC driver imports it). Fixed to any(tcp, quic), matching the reactor; a cargo hack --each-feature sweep found no siblings.

Parity surface

  • Serf<I>Serf<I, A> (breaking) — the handle regains the advertise address-domain brand; compio drops only the reactor's R (compio is the runtime). join stays generic over any Resolver — joining by hostname while advertising a resolved SocketAddr is deliberately kept, and the reactor's doc comment that wrongly claimed otherwise was corrected in both crates.
  • Six ergonomic constructorstcp/tls/quic + *_with_rng, replacing the ten-argument Serf::new::<..> turbofish.
  • Idempotent shutdown() (breaking behavior) — a second/concurrent call now awaits teardown and returns Ok once the port is released, proven by a rebind assertion.
  • 21 SWIM timing knobs (7 × tcp/tls/quic) — failure detection was previously un-tunable on compio; nodes were pinned to memberlist defaults.
  • SerfError::LeaveFarewellUndelivered, the tls-rustls-ring/-aws-lc-rs features, the test-gated message dropper, and re-exports of MaybeResolved/DnsError/JoinFailed/DEFAULT_JOIN_DEADLINE (the first two already appeared in compio's public signatures yet were unnameable). The serf umbrella now forwards compio's rustls features.
  • Ported serf-reactor's unit suites (resolvers, transports, options, parked key-response) adapted to !Send; skipped only the genuinely reactor-specific ones (Send/Sync assertions, runtime-generic scaffolding).

Verification

  • serf-compio 262 tests (from 230), serf-reactor 271 — green, run twice including the --test-threads=1 CI shape.
  • The previously-broken --no-default-features --features quic-rustls-ring build now compiles; cargo hack --each-feature clean.
  • -D warnings clippy (including the literal ci-compio feature string), cargo fmt --all --check, cargo doc for both crates and the umbrella at --all-features, and the umbrella smoke test: all green.
  • Every fix that pins a defect was mutation-verified — the fix reverted, the specific test confirmed failing, then restored.

Note

Delegate::message_dropper() is honored only on TCP in both crates (TLS could support it; QUIC would need a serf-proto addition). Mirrored the reactor rather than diverging — a candidate follow-up.

Stacked on #96 (now merged). Held for review — not auto-merged.

🤖 Generated with Claude Code

al8n added 6 commits July 13, 2026 20:05
OsResolver and DnsResolver both declare Address = HostAddr<SmolStr>, but the
Transport impls demanded A: Data and HostAddr has no Data impl — the orphan rule
makes one impossible. So neither resolver could supply an advertise address, and
the declared default A = HostAddr<SmolStr> was dead. A is the caller's
unresolved address domain, resolved to a SocketAddr once at construction and
never encoded, so the bound bought nothing.

Re-exports JoinFailed and DEFAULT_JOIN_DEADLINE, which appeared in the public
API but could not be named, and corrects the doc claiming A constrains join's
seeds — it does not, and joining by hostname while advertising a resolved
address is deliberately allowed.
…expose the SWIM knobs

UnreliableTransport::Datagram is the default and is documented as encrypted and
attested, but the QUIC driver sent every gossip packet over a plain UDP socket
and never consulted the setting — so a node configured for datagram gossip got
plaintext UDP instead, silently. It now queues datagrams on the connection and
falls back to UDP only where the status says to.

The transports gained the seven SWIM timing knobs the reactor has; without them
EndpointOptions took no override and failure detection could not be tuned at all.
Joins now clamp their deadline to the coordinator's exchange deadline, so a join
can no longer report JoinAllFailed before the caller's deadline elapses. Adds
SerfError::LeaveFarewellUndelivered, the rustls backend features, the test-only
message dropper, and the re-exports for MaybeResolved and DnsError, which the
public signatures already named but callers could not.
… to the OS

DnsResolver::resolve swallowed every tcp_query error, including TimedOut, and
escalated into the unbounded OS resolver — so a slow or hostile nameserver could
burn the per-query TCP budget and then hang bootstrap in unbounded OS DNS,
defeating the timeout contract. A timed-out query now returns the error, matching
serf-reactor.
The QUIC driver imports ExchangeId, but it was gated on the tcp feature alone,
so a quic-without-tcp build failed to compile. Gate it on either transport, as
serf-reactor does.
…rs, idempotent shutdown

The handle gains the advertise address-domain parameter it was missing, so it
now mirrors serf-reactor's shape (minus the runtime parameter compio does not
need). Adds the tcp/tls/quic constructors and their *_with_rng variants, so a
node no longer needs the ten-argument turbofish. shutdown() is now idempotent: a
second or concurrent call awaits teardown and returns Ok once the sockets are
released, rather than failing immediately while the port is still bound.

Ports serf-reactor's unit suites (resolvers, transports, options, the parked
key-response path) adapted to the !Send model, and forwards compio's rustls
backend features through the serf umbrella.
Two QUIC scenarios failed under coverage instrumentation and on Windows while
passing on fast hardware: the membership always converged, but the assertions
raced the event delivery.

Five hook checks asserted a notify_* callback synchronously right after polling
the snapshot, but the callback lands on a separate path that can lag snapshot
publication — a gap QUIC's handshake widens. They now poll until the hook fires,
bounded by the same window, the way the reactor's suite does.

The parked-key test released B's acknowledgement after a fixed 300 ms, which
could fall outside the key query's response window under load. The acknowledgement
now completes on the next runtime turn, tied to the rotation rather than a
wall-clock offset, so park and ack scale together — mirroring the reactor's
persistence-worker mechanism.
@codecov

codecov Bot commented Jul 13, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 92.76411% with 50 lines in your changes missing coverage. Please review.
✅ Project coverage is 95.05%. Comparing base (b57b960) to head (630e948).

Files with missing lines Patch % Lines
serf-compio/src/driver/quic/mod.rs 83.75% 13 Missing ⚠️
serf-compio/src/driver/stream/mod.rs 80.00% 12 Missing ⚠️
serf-compio/src/driver/shared/mod.rs 85.24% 9 Missing ⚠️
serf-compio/src/quic/mod.rs 91.95% 7 Missing ⚠️
serf-compio/src/tls/mod.rs 92.04% 7 Missing ⚠️
serf-compio/src/tcp/mod.rs 97.72% 2 Missing ⚠️
Additional details and impacted files
@@                 Coverage Diff                 @@
##           refactor/sansio      #97      +/-   ##
===================================================
- Coverage            95.19%   95.05%   -0.14%     
===================================================
  Files                   94       94              
  Lines                18899    19555     +656     
===================================================
+ Hits                 17990    18588     +598     
- Misses                 909      967      +58     
Files with missing lines Coverage Δ
serf-compio/src/delegate/mod.rs 100.00% <ø> (ø)
serf-compio/src/delegate/void.rs 100.00% <100.00%> (ø)
serf-compio/src/error/mod.rs 100.00% <ø> (ø)
serf-compio/src/lib.rs 100.00% <ø> (ø)
serf-compio/src/resolver/dns/mod.rs 97.89% <100.00%> (+4.27%) ⬆️
serf-compio/src/serf/mod.rs 99.68% <100.00%> (+0.16%) ⬆️
serf-compio/src/transport/runtime.rs 100.00% <100.00%> (ø)
serf-reactor/src/lib.rs 100.00% <ø> (ø)
serf-reactor/src/quic/mod.rs 100.00% <ø> (ø)
serf-reactor/src/serf/mod.rs 99.51% <ø> (ø)
... and 8 more

... and 1 file with indirect coverage changes


Continue to review full report in Codecov by Harness.

Legend - Click here to learn more
Δ = absolute <relative> (impact), ø = not affected, ? = missing data
Powered by Codecov. Last update b57b960...630e948. Read the comment docs.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

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

Morty Proxy This is a proxified and sanitized view of the page, visit original site.