Skip to content

Sans-I/O rewrite: pure state machine + wire codec + async/embedded drivers - #92

Open
al8n wants to merge 129 commits into
mainfrom
refactor/sansio
Open

Sans-I/O rewrite: pure state machine + wire codec + async/embedded drivers#92
al8n wants to merge 129 commits into
mainfrom
refactor/sansio

Conversation

@al8n

@al8n al8n commented Jul 13, 2026

Copy link
Copy Markdown
Owner

No description provided.

al8n added 30 commits June 23, 2026 21:47
Add buffa-build codegen (proto/serf/v1/messages.proto → OUT_DIR/serf_wire_generated.rs),
the typed UserEventMessage + LamportTime, the user_event_{to,from}_pb bridge, and two
round-trip tests (happy-path + missing-ltime rejection). Build and test both green.
Adds `serf-proto::framing`:
- `MessageType` enum with 10 variants (Leave/Join/PushPull/UserEvent/
  Query/QueryResponse/ConflictResponse/Relay/KeyRequest/KeyResponse) +
  `Unknown(u8)` for forward compatibility; tag bytes 1–10 match the
  legacy serf-core constants.
- `encode_message(ty, &impl buffa::Message) -> Result<Vec<u8>, FrameError>`:
  writes `[tag][LEB128 body_len][buffa body]` into a single allocation.
- `decode_message(&Bytes) -> Result<(MessageType, Bytes, usize), FrameError>`:
  zero-copy body slice via `Bytes::slice`, returns bytes consumed.
- `FrameError` covers Empty / Incomplete / VarintOverflow / FrameTooLarge /
  Decode.

All four items re-exported from the crate root. 6 framing tests + 2
pre-existing messages tests — 8 total, all green.
…roto + bridge

Adds the four foundation field types:
- QueryFlag: bitflags! u32 (ACK | NO_BROADCAST); no standalone proto, u32 wire
- Coordinate: typed + pb::Coordinate (repeated double portion, error/adjustment/height)
- Tags: typed + pb::Tags (map<string,string> entries via buffa HashMap)
- Filter: typed enum + pb::Filter (oneof NodeIdList|TagFilter, boxed by buffa)

Bridges: coordinate_{to,from}_pb, tags_{to,from}_pb, filter_{to,from}_pb.
Tests: 14 new round-trip tests (22 total), all green.
…esponseMessage

Add proto definitions, typed generics, and bridge conversions for the three
membership messages. Node-id (I) and Node<I,A> are embedded as opaque
`bytes` encoded via memberlist_proto::Data / DataRef, mirroring the bridge
pattern from memberlist-proto. ltime is proto3 `optional uint64` (required
on decode). Round-trip tests use I=SmolStr, A=SocketAddr.
…ssage

- Make Filter generic over I: Id(Vec<I>) with where-clause Data bounds;
  default I = SmolStr preserves backward compatibility.
- NodeIdList proto field changed from repeated string to repeated bytes;
  each I encodes via memberlist_proto::Data (mirrors the Node bytes pattern).
- Add QueryMessage<I,A> and QueryResponseMessage<I,A> typed structs with
  ack()/no_broadcast() helpers; Duration stored as uint64 nanos on the wire.
- Add pb::QueryMessage and pb::QueryResponseMessage to messages.proto with
  field numbers matching the legacy serf-core tag constants.
- Add filter_to_pb/filter_from_pb bridge generics (now return Result);
  add query_to_pb/query_from_pb + query_response_to_pb/query_response_from_pb.
- Export all new types and bridge functions from lib.rs.
- Update existing Filter tests for new Result-returning bridge API; add 8
  new query tests covering roundtrip, filters, and required-field rejection.
- All 36 tests pass (cargo test -p serf-proto, exit 0).
…elayMessage

PushPullMessage<I>: 3 lamport clocks (ltime/event_ltime/query_ltime) required-on-decode;
status_ltimes as repeated NodeStatusTime (map<bytes,...> forbidden in proto3);
left_members as repeated bytes; events as repeated UserEvents.

UserEvent + UserEvents batch added as separate typed + proto + bridge types.

KeyRequestMessage: optional bytes key (None = list-keys).
KeyResponseMessage: result + message + repeated bytes keys + optional primary_key.
KeyResponse<I> (machine aggregation) intentionally NOT ported per spec.

RelayMessage<I,A>: destination Node<I,A> via Data bytes; payload carried verbatim.

54 tests pass (24 new).
- Add `src/any/mod.rs`: `AnyMessage<I,A>` enum (one variant per MessageType)
  with `decode(buf: &Bytes) -> Result<Self, DecodeError>` that dispatches on
  the framing tag byte → buffa decode → bridge → typed variant.
  `DecodeError` wraps `FrameError`, `BridgeError`, `Buffa { tag }`, and
  `UnknownTag(u8)`. KeyRequest/KeyResponse variants and DecodeError::UnknownTag
  arms are gated on `any(aes-gcm, chacha20-poly1305)` with matching doc(cfg).
- Add `src/any/tests.rs`: end-to-end `encode_message → AnyMessage::decode` round-
  trip for every message type (11 plain + 2 encryption-only). Unknown tag and empty
  buffer error cases. No-encryption guard for key tags → UnknownTag.
- Re-export `AnyMessage` and `DecodeError` from `lib.rs`; add `pub mod any`.
- Sweep minors:
  - `data_to_bytes`: delegate to `Data::encode_to_bytes()` (eliminates local reimpl).
  - `data_from_bytes`: widen signature from `&Bytes` to `&[u8]`; all call sites
    coerce via Deref unchanged.
  - `KeyResponseMessage`: add `#[derive(Default)]` (fixes clippy::new_without_default
    + clippy::derivable_impls; `new()` delegates to `Self::default()`).
- Full gate: all 6 commands (build/test/clippy × plain/encryption) RC=0.
  63 tests plain, 69 tests encryption.
Pure Sans-I/O serf state machine over memberlist_proto::Endpoint: membership FSM, three Lamport clocks, user events, queries/responses/relay, conflict and key-management interceptors, push-pull anti-entropy, Vivaldi coordinates, and snapshot replay.
Modernize the coverage nightly-toolchain step (drop the deprecated actions-rs/toolchain action) and quote the tarpaulin output-dir path.
…inators

A narrow pub(crate) trait the serf core uses to reach a memberlist reliable coordinator (queue, send, snapshot, push-pull, leave, poll), implemented for the Stream (tcp/tls) and QUIC coordinators. Adds the tcp/tls/quic feature forwards to serf-proto.
…ver the Reliable seam

serf_proto::Endpoint drops its inner memberlist Endpoint and reaches the transport through a disjointly-borrowed &mut impl Reliable; a transitional StreamEndpoint { core, transport } runs the composed tick and preserves the test surface.
…reamEndpoint

Wraps the real reliable coordinator (tcp/tls) plus the serf core; the coordinator owns the stream lifecycle and sieves DialRequested internally, so serf observes only RemoteStateReceived. The composed handle_timeout keeps the before_inner, transport, after_inner order.
Add the docsrs doc(cfg) attribute to the feature-gated impl blocks so docs.rs renders the gate badges, and tighten the composed-timeout doc comment.
The no-transport build exposes only wire/data/config/snapshot types; the whole members module (the membership FSM state) is gated on any(tcp, quic).
…a seam

set_tags encodes the local tag map into the node meta, re-advertises it via the coordinator through the new Reliable::update_meta seam, and refreshes an already-present local member's tags so tag-filtered local queries observe them immediately. Both super-machines forward it. handle_node_update no longer marks local-state dirty for a NodeUpdated, since a member's tags and address are not part of the push-pull snapshot.
…f drivers

serf-driver holds the membership SerfSnapshot view, the common driver error payloads, and the observation byte-weight helper — the runtime-independent pieces both serf-compio and serf-reactor will need. Mirrors memberlist-driver; the run loops, channel/cell substrate, and delegate dispatch live per-runtime.
Expose the Sans-I/O accessors a runtime driver needs to pump the
StreamEndpoint and QuicEndpoint super-machines: start_scheduling,
start_push_pull, handle_message, gossip_mtu, max_stream_frame_size,
local_id, members_snapshot, and encrypt_gossip/decrypt_gossip, plus
members_snapshot on Endpoint. FSM and wire behavior are unchanged.
JoinFailed was never wired into a SerfError variant and has no consumer
under the dispatch-only Join semantics the drivers use: the Join command
is dispatched into the machine, not awaited for a seed-contact result.
The compio runtime driver (io_uring on Linux, polling/kqueue on
macOS/BSD, IOCP on Windows) for serf's Sans-I/O SWIM machine, mirroring
memberlist-compio: TCP, TLS-over-TCP, and QUIC reliable coordinators
over the shared UDP gossip plane, gossip encryption (AES-GCM and
ChaCha20-Poly1305), DNS/getifs/OS address resolvers, serde and clap
option layers, and the Serf handle with observable event and observation
drop counters.

A single-owner select loop per transport drives the endpoint
super-machine and republishes a lock-free snapshot. When a suspicion
deadline is already past, the drain reaps the proactor with a
zero-timeout poll before re-checking the socket, so a buffered Ack
resolves its probe before suspicion fires: a backend-portable empty
observation on a completion-based runtime where no synchronous check
exists.
…nts, options

The agnostic-generic reactor-driver foundation for serf's Sans-I/O machine (Send/Arc/agnostic R, flume everywhere, no cmd_fairness_budget knob); ports serf-compio's command/delegate/error/events/options into the reactor Send model, mirroring memberlist-reactor's shape. Compiles --no-default-features as the foundation; the tcp/quic-gated driver and transports follow.
Agnostic-generic reactor substrate: Arc<Shared> with a Mutex<VecDeque<Command>>
+ stored Waker command queue (no channel), ArcSwap snapshot, drop counters, and
teardown latch; a Send Transport trait over R plus agnostic TCP (bind +
ephemeral-port retry) and the resolver infra. The stream pump is an impl
Future::poll driving serf-proto's StreamEndpoint (readiness recv-loop ->
drain_surfaces -> single inline handle_timeout; NO poll_with(ZERO) drain) with
the readiness bridge task; ports serf-compio's serf-logic (PendingJoin
await-result join, per-exchange ignore_old, events->observation) into the
reactor model mirroring memberlist-reactor. The handle carries construction +
command dispatch; ergonomic constructors and tests follow.
…e TCP tests

Add ergonomic Serf::tcp / tcp_with_rng over the generic Serf::new::<TcpTransport>,
plus src/snapshot.rs and the snapshot public read-forwarders
(members/local_member/state/advertise_node/advertise_address/local_id/default_query_*/remove_failed_node*)
porting serf-compio's semantics. Add the real-node TCP suite on tokio (2-node
join/converge/user_event/query/leave-LeftCluster/shutdown) proving the reactor stream
driver works end-to-end; mirrors memberlist-reactor's harness.
…e before the timer

The readiness pump capped the UDP recv (and bridge-inbound) at iter_drain_cap but still ran the single handle_timeout and the join/leave deadline reaps, so a due timeout could fire before a ready pre-deadline Ack / ExchangeCompleted / LeftCluster queued behind the cap was drained (false suspicion / spurious JoinAllFailed / LeaveTimeout under bursts). It keeps memberlist-reactor's bounded per-poll recv cap but now gates the single handle_timeout site and the deadline reaps on quiescence (!more), so they run only once the socket is drained to Poll::Pending and drain_surfaces has no more ready work. Regression with iter_drain_cap=1.
… for liveness

drain_surfaces is now a fixed-point (repeat the ordered pass while any surface made progress), so a later surface feeding an earlier one no longer reports false quiescence — the withheld Close and the KeyRequest respond_key gossip are drained the same poll.

The timer + deadline reaps fire at quiescence OR after a bounded number of capped-poll deferrals (TIMER_DEFERRAL_LIVENESS_BOUND), so a sustained ingress flood can no longer starve failure-detection / join-leave-query deadlines, while staying non-premature (FIFO drains the pre-deadline backlog first).

Regressions: continuous-flood liveness, the fixed-point withheld-Close late surface, and the kept non-premature completion-behind-cap case.
…orcement

Ports serf-compio's two encrypted regressions onto the reactor tcp real-node
suite: a shared-keyring 2-node cluster joins and converges over the AEAD-sealed
gossip plane, and a disjoint-keyring pair must NOT exchange membership — the
negative case proves the cfg-gated encrypt_gossip/decrypt_gossip hops are real
AEAD enforcement, not an identity transform. Gated #[cfg(encryption)].
al8n added 23 commits July 12, 2026 18:49
The reactor exported an async MergeDelegate that nothing ever invoked — by
the time any event surfaces, the inner machine has already applied the
push/pull merge, so a driver-side async gate can never veto anything. The
machine's own synchronous MergeDelegate predicate is the real admission
point: it runs inline for EVERY push/pull merge (a join and a periodic
anti-entropy refresh alike, deliberately tighter than the reference
implementation's join-only gate), and a vetoed peer set is never applied.

Both composed serf endpoints now forward set_merge_delegate to the inner
machine, every handle constructor accepts an optional boxed predicate
(installed by each transport's run body exactly like the reconnect
delegate), and the inert async trait and its noop impl are retired in favor
of re-exporting the machine trait — an application needing async policy
resolves it ahead of time and answers from the resolved state.

An e2e pins the wiring: a recording delegate on the joined node observes at
least one notify_merge carrying the joining peer's state. (Consultation is
the stable assertion — a merge-only veto is transient by design, since a
rejected peer can still be admitted moments later through gossip Alives,
as in the reference implementation.)
The machine has owned the snapshot record format and the replay fold since
the port, but no driver ever touched a file — snapshot_path was a silent
no-op and rejoin_after_leave was inert. The driver now owns the file end to
end: a SnapshotOptions constructor argument names the path (and compaction
threshold), the Serf constructor reads and decodes it so a corrupt record
fails construction loudly (SerfError::SnapshotOpen; a truncated tail from a
crash mid-append is tolerated and repaired to a whole-record boundary), the
transport body replays the records — honoring rejoin_after_leave at the
Leave marker — and hands the result to Endpoint::load_snapshot, which
recovers the clock floors and re-dials the recovered peers through the
normal push/pull machinery.

The pump appends at its event chokepoint: Alive/NotAlive per surfaced
member event, the advancing clock floors, and the Leave marker on
LeftCluster; appends are buffered and flushed per batch, and past the
threshold the file is compacted to the live alive-set + clocks via a
sibling temp file and an atomic rename.

Coverage: five snapshotter units (reopen replay, the leave gate both ways,
torn-tail tolerance + repair, corrupt-middle refusal, compaction identity)
and two e2e scenarios on both runtimes — an abruptly-killed node reboots
from its snapshot and rejoins with NO join call, and a cleanly-left node
stays solo on the default posture but recovers its membership when
rejoin_after_leave opts in.
…tion

Additive changes ride proto3 field semantics (new optional fields default on
old nodes; unknown message tags are consumed and dropped); breaking changes
ship as a new cluster generation fenced by the cluster label, cut over
blue/green; the delegate surface is compile-time API versioned by semver. No
in-band version negotiation exists by design — the stance that replaces the
legacy protocol_version and delegate_version knobs.
…ence

Compaction preserved only the clocks and live alive-set, silently erasing a
clean-leave marker: a churned or low-threshold file rewritten after leave()
would auto-rejoin on the default posture. The writer now tracks clean-left
state (set by the Leave append, cleared by membership activity, recovered
from the tail on reopen) and re-emits the marker LAST in the compacted
file, which therefore replays exactly like the sequence it replaces under
both rejoin postures — pinned by a threshold-1 compaction test both ways,
plus the post-leave-activity case.

The compacted replacement is now written, synced, and OPENED before the
rename, so no fallible operation remains after the swap — a rename that
succeeded can no longer pair with a failed reopen leaving the writer on the
unlinked inode. (Also fixes a bug the rework surfaced: truncate+append is a
rejected OpenOptions combination, so the previous chain silently fell to
the grown-file branch.)

The keyring file's replacing temp inode is born owner-only (0600 on Unix),
so a rotation can never widen a restrictive mode under a permissive umask —
pinned by a Unix permissions test. And keyring_updated no longer touches
storage inline on the pump: rotations hand off to a dedicated persistence
thread through an unbounded channel (rare, small payloads), keeping the
callback non-blocking as the delegate contract requires.

The merge-veto docs now state the boundary plainly: a push/pull filter, not
admission control — a rejected peer can still enter through gossiped
Alives, exactly as in the reference implementation.
…ponses, leave-last snapshot order

Keyring persistence: each rotation writes through an exclusively-created
(create_new), owner-only, OS-entropy-named sibling temp — key bytes can no
longer land in a pre-existing inode or behind a planted symlink — and
construction sweeps the legacy fixed-name temp and abandoned temps.
keyring_updated now returns a persistence acknowledgement: the pumps park a
rotated op's key response until it resolves, folding a failure into the
response (result = false carrying the error) exactly as the reference
implementation folds a keyring-file write error, bounded by the requester's
response deadline, with the re-poll cadence folded into the idle-arm timer
target. FileKeyringDelegate acknowledges from its persistence worker, and
dropping the delegate joins that worker after it drains, so a shutdown
cannot discard a rotation the wire already carries.

Snapshot: both pumps append the clock floors BEFORE the leave marker, so a
clean shutdown ends the file at the Leave record — the terminal shape
compaction preserves and replay expects (a trailing clock record would
resurrect floors the default no-rejoin posture zeroes). The snapshotter
pins original-versus-compacted replay equivalence under both postures and
the leave-gate e2e asserts the on-disk tail shape.

serf-embedded: the key-management ops pass relay_factor through (the plain
forms pin 0, _with variants expose it), restoring the encryption-feature
build the widened endpoint signatures had broken.
…tion; make the rename durable

A destination whose own extension is tmp is its with_extension image, so
the construction sweep was deleting the persisted keyring itself — and
under the previous implementation such a destination was written in place,
so the file can hold the only copy of legitimate key material. The sweep
now skips the legacy path when it equals the destination, and the
random-suffix matcher accepts only the exact .name.16-hex.tmp shape so an
operator's own sibling files are never this delegate's to delete.

A completed rename is not crash-durable until the directory entry is: the
acknowledgement — which releases a successful key response to the cluster
— now follows a sync of the containing directory, so a crash after the
response cannot revert the node to the old ring while peers believe the
rotation durable. On non-Unix platforms std cannot open a directory
handle; durability is left to the filesystem's metadata journaling there.
A rotation into a directory that cannot be synced acknowledges failure.

KeyringPersistence is must_use so a future call site cannot silently drop
the acknowledgement.
…Delegate Unix-only

The legacy-sweep guard compared paths lexically, so a destination named
with an uppercase TMP extension differed from its lowercase with_extension
image while aliasing the same file on the case-insensitive filesystems
that are the default on macOS and Windows — construction could still
delete the only persisted keyring. The guard now treats any ASCII case of
the tmp extension as aliasing the destination and skips the sweep;
the regression rotates into a .TMP-named destination and proves load
returns it after reconstruction.

The acknowledgement contract is rename durability, and no safe standard
API can flush a directory entry on Windows — rather than acknowledge a
rotation a power loss could revert, the turnkey delegate is now
compile-time Unix-only (the trait and acknowledgement types stay
portable; a Windows application implements KeyringDelegate with a
platform-durable strategy). The file-backed rotation e2e is gated
accordingly.
… names

A symlinked destination defeats every name-based alias guard: a configured
ring.current pointing at a file that lives at the legacy temp name has an
extension that never case-folds to tmp, so the sweep unlinked the symlink's
target — the only persisted keyring — and left the destination dangling.
Every sweep candidate (the legacy fixed name and the random-suffix temps
alike) is now gated on resolved device/inode identity against the
destination: a candidate reaching the destination's storage — through
lexical identity, a case-folding filesystem, or a symlink on either side —
is never touched, a dangling link or a provably distinct file is swept, and
unresolvable identity conservatively skips. This subsumes the extension
case-fold guard and restores hygiene for the mixed-case corner it
over-skipped. The regression seeds a ring at the legacy-named path, links
the configured destination to it, and proves construction-then-load
preserves the keyring.
Sequential identity observations race a concurrent rotation: for a
candidate whose NAME can itself name the destination — lexically, or equal
under the ASCII case folding that aliases names on case-insensitive
filesystems — a rename landing between the two metadata reads makes the
identities differ and authorizes unlinking the freshly persisted keyring.
remove_file unlinks a name, so for those shapes no point-in-time identity
observation can ever make the unlink safe: they are refused
unconditionally, before any filesystem access. The identity gate remains
for genuinely distinct names, where the unlink cannot remove the
destination's entry and resolved device/inode still guards the symlink and
hard-link shapes.

The regression widens the observation gap through a test seam, lands an
atomic replacement mid-check exactly as a rotation would, and proves
construction never unlinks the destination.
…n extensions

ASCII case folding is not the complete filename-alias relation on every
supported filesystem: case-insensitive HFS+ ignores certain Unicode
scalars when comparing names, so a destination extension carrying one —
t<ZWJ>mp, say — aliases the generated legacy image while passing both
name guards, reopening the mid-check deletion race there. The legacy
image differs from the destination only in its extension, so the sweep
now runs only when that extension is provably distinct from tmp: pure
ASCII and not ASCII-case-folding to tmp, or absent entirely (the image
then appends four non-ignorable characters no folding can absorb); any
non-ASCII scalar conservatively refuses. Random-suffix candidates need
no classifier — their names carry a dot prefix and 16-hex infix the
destination's name does not, an excess of non-ignorable ASCII no alias
relation can erase.

The classifier regression covers the tmp case folds, an HFS+-ignorable
scalar, and a non-ASCII extension; the behavioral regression proves an
unprovable extension keeps its legacy sibling on every filesystem —
exactly the file that IS the persisted keyring where the alias folds.
…ke-driven farewell completion

The secret-key codec re-export rode a cipher-only cfg while the bridge
module it re-exports from needs a transport: a coordinates-plus-cipher
build dangled. The gate now mirrors the module's full compound condition.

The QEMU firmware harness is workspace-excluded with its own lockfile,
which still pinned memberlist from before the farewell-compound seam —
farewell_capacity did not exist there. Re-pinned to current main.

The farewell epoch test demanded send completion on the single poll after
eligibility, but a fresh socket's writable readiness may not have reached
the reactor yet on a loaded runner — the send legitimately returns Pending
and production advances on the registered writable wake. The completion
half now drives bounded wake iterations; the epoch-gate assertions are
unchanged.
The first full-matrix coverage run showed the fold work exercised almost
entirely through the tcp suite: the quic pump's snapshotter, key-response
parking, and replay threading — and the tls constructor's snapshot
threading — had no end-to-end coverage at all. Mirror the leave-gate
snapshot scenario (including the on-disk tail-shape assertion) onto the
quic driver, add a quic file-backed rotation proving the parked response
routes within the query window with the key already durable, and run the
leave-gate scenario over tls. Unit coverage for the remaining dark arms:
every settle outcome of a parked key response (pending, persisted, failed,
worker-vanished), the nameless-destination input error, the snapshot
coordinate builders and accessors, and the quic endpoint's operator
forwarders (health score, merge-delegate install, coordinate resets).
…out of serf-reactor

The file snapshotter and the keyring-file persistence engine are pure
std file mechanics with no runtime coupling, and the compio driver needs
both for operator parity — one shared implementation beats a fork of the
hardened logic (exclusive owner-only temps, the identity-gated stale-temp
sweep, the directory-synced rename the acknowledgement waits on, the
compaction clean-leave gate). They now live in serf-driver alongside the
shared key-management apply logic, together with the
KeyringPersistence/KeyringPersistRx/KeyringPersistError acknowledgement
contract; the snapshotter opens from a bare path plus threshold so it
carries no per-crate options type. serf-reactor keeps its public surface
through re-exports, with FileKeyringDelegate now a thin wrapper that
turns the engine's acknowledgement into KeyringPersistence::Pending. The
engine and snapshotter test suites move with the code; the reactor keeps
delegate-level round-trip, drop-flush, and first-boot tests.
… machine's merge predicate

keyring_updated now returns the shared KeyringPersistence acknowledgement
(default Durable, so observation-only delegates are unchanged): a rotated
op's key response is parked until the acknowledgement resolves and a
persistence failure folds into the response exactly as the reference
implementation folds a keyring-file write error, bounded by the
requester's response deadline. Both pumps reap parked responses ahead of
every output drain (so a sent response flushes in the same pass) and fold
the re-poll cadence into their timer targets only while something is
parked; the settle logic itself is the serf-driver implementation the
reactor already runs. FileKeyringDelegate arrives as a thin wrapper over
the shared engine, Unix-only per its durability contract.

The async MergeDelegate is retired: it had no call site — the machine
merges push/pull state before any event surfaces, so a driver-side veto
can never run — and its replacement is the machine's own synchronous
push/pull filter, re-exported with docs stating exactly what it bounds
(one exchange's bulk admission, not durable exclusion).
Serf::new gains the two remaining construction hooks: merge_delegate
installs the machine's synchronous push/pull filter into the endpoint each
transport builds, and snapshot opens the shared file snapshotter BEFORE any
socket binds — a corrupt snapshot refuses construction loudly — with the
decoded records riding the runtime bundle into T::run, which replays them
under the serf option rejoin_after_leave and hands the writer to the pump.
Both pump loops append at their event chokepoint: member records, then the
advancing clock floors, with the clean-leave marker written last so a clean
shutdown ends the file at the Leave record (the terminal shape compaction
preserves and replay expects), flushing — and compacting past the
threshold — before the drain returns. SnapshotOptions joins the driver
options (constructor argument, not a runtime knob) and the snapshot-open
failure surfaces as SerfError::SnapshotOpen.
Both pumps now attach the driver-side live readings when publishing the
membership snapshot — health score, broadcast queue depth, the encryption
flag, and (under the coordinates feature) the local Vivaldi coordinate and
reset count — and the handle reads them lock-free: stats(),
encryption_enabled(), health_score(), and coordinate(). cached_coordinate
rides a read-only command answered from the endpoint's coordinate cache in
every lifecycle state, with the shutdown drain failing a parked probe like
any other queued command.
Mirror the reactor's scenario classes onto the compio driver: the snapshot
leave-gate (on-disk tail ends at the Leave record; the default posture
starts fresh on restart while the opt-in posture rejoins), the recording
merge predicate consulted on a join push/pull with the joining peer's state,
the operator aggregate and coordinate surfaces on the handle, and a
file-backed rotation whose parked response is still collected within the
query window with the key already durable when it arrives.
…ures

The hoist moved the persistence-failure warnings — snapshot append, flush,
compaction, and keyring-write — behind serf-driver's tracing feature, which
neither runtime crate's tracing feature forwarded: an operator enabling
tracing on serf-reactor or serf-compio would have compiled none of the only
diagnostics for silently dropped snapshot records or unpersisted rotations.
Both manifests forward it now, and each runtime carries a wiring test
asserting serf-driver's TRACING_WIRED probe so a dropped forward fails
loudly instead of silencing telemetry.
The runtime crates' all-targets clippy gates deny assertions on constants,
which the wiring tests' runtime asserts tripped. The pin now const-evaluates
inside each test, which both satisfies the lint and strengthens the check:
a dropped serf-driver/tracing forward fails every tracing-featured compile
of the test target outright rather than waiting for the test run.
@codecov

codecov Bot commented Jul 13, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 95.06587% with 206 lines in your changes missing coverage. Please review.
✅ Project coverage is 95.19%. Comparing base (ddbef9d) to head (b57b960).

Files with missing lines Patch % Lines
serf-compio/src/driver/stream/mod.rs 93.24% 89 Missing ⚠️
serf-compio/src/bridge/mod.rs 71.72% 41 Missing ⚠️
serf-compio/src/driver/quic/mod.rs 95.83% 40 Missing ⚠️
serf-compio/src/tls/mod.rs 93.13% 14 Missing ⚠️
serf-compio/src/tcp/mod.rs 95.08% 9 Missing ⚠️
serf-compio/src/resolver/dns/mod.rs 93.61% 6 Missing ⚠️
serf-compio/src/resolver/getifs/mod.rs 94.82% 3 Missing ⚠️
serf-compio/src/driver/shared/mod.rs 97.22% 2 Missing ⚠️
serf-compio/src/serf/mod.rs 99.51% 2 Missing ⚠️
Additional details and impacted files
@@             Coverage Diff             @@
##             main      #92       +/-   ##
===========================================
+ Coverage   70.10%   95.19%   +25.08%     
===========================================
  Files          42       94       +52     
  Lines        5402    18899    +13497     
===========================================
+ Hits         3787    17990    +14203     
+ Misses       1615      909      -706     
Files with missing lines Coverage Δ
serf-compio/src/command/mod.rs 100.00% <100.00%> (ø)
serf-compio/src/delegate/keyring_file/mod.rs 100.00% <100.00%> (ø)
serf-compio/src/delegate/mod.rs 100.00% <100.00%> (ø)
serf-compio/src/delegate/void.rs 100.00% <100.00%> (ø)
serf-compio/src/driver/options/mod.rs 100.00% <100.00%> (ø)
serf-compio/src/drop_counter/mod.rs 100.00% <100.00%> (ø)
serf-compio/src/error/mod.rs 100.00% <100.00%> (ø)
serf-compio/src/events.rs 100.00% <100.00%> (ø)
serf-compio/src/lib.rs 100.00% <100.00%> (ø)
serf-compio/src/quic/mod.rs 100.00% <100.00%> (ø)
... and 84 more

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 ddbef9d...b57b960. Read the comment docs.

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

al8n added 3 commits July 13, 2026 15:37
main's only divergence is dependabot CI-action bumps (actions/checkout 6→7,
actions/cache 5→6, codecov-action 6→7). Bumps to workflows this branch still
carries merge cleanly; the bumps to the four pre-Sans-I/O workflows this branch
replaced with the per-driver ones — ci.yml, coverage.yml, net.yml, fuzz.yml —
resolve to the deletion, since those workflows are gone here and the new ones
already run on the same action versions.
@al8n
al8n force-pushed the refactor/sansio branch from 60c27a7 to 1459bb0 Compare July 13, 2026 03:47
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