1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
//! Synchronous blocking client for the weir daemon.
//!
//! Connects over a Unix domain socket (or TCP + mutual TLS behind the `tls`
//! feature) and exchanges frames using the weir wire protocol v1. Each method
//! issues one request and reads one response — no pipelining. For concurrent
//! producers, create one [`WeirClient`] per thread.
//!
//! # Platforms
//!
//! The Unix-socket transport is, unsurprisingly, Unix-only. **The TCP + mutual
//! TLS transport is not** — it builds wherever rustls does, Windows included,
//! as of 2.0.3. Before then it was gated on `unix` by accident: it lives in its
//! own module but imported two types that happened to sit in the Unix one.
//!
//! # Throughput: one record per round-trip
//!
//! [`push`](WeirClient::push) is synchronous: it sends one frame and blocks for
//! the ack before returning, so one connection carries one record at a time.
//! What that costs depends on the tier, and the two are bounded by different
//! things:
//!
//! - **`Buffered`** is round-trip bound. The daemon acks from memory, so the
//! ceiling is roughly `1 / RTT`.
//! - **`Durable`** is fsync bound, not RTT bound. The ack waits on the daemon's
//! fsync, which dominates the round trip by orders of magnitude — batching at
//! the daemon amortises it across concurrent producers, which is why fanning
//! out helps this tier *more* than it helps `Buffered`.
//!
//! Measured on one machine (Apple M3 Max, Unix socket, 256-byte records): a bare
//! AF_UNIX round trip is **~7 µs**, and a single client reaches **~32,000 rec/s
//! `Buffered`** and **~5,700 rec/s `Durable`**. On a 2-vCPU CI runner the same
//! figures are ~13,900 and ~3,700. Treat these as the shape of the problem, not
//! as a spec: they move with hardware, filesystem, and record size. An earlier
//! version of this paragraph quoted "~50 µs RTT" and "~20k rec/s", which matched
//! neither tier on any machine we have measured.
//!
//! To go faster, **fan out across connections**: create one `WeirClient` per
//! producer thread (they're independent and the daemon handles many
//! concurrently). Ordering is only guaranteed within a single connection's
//! sequential pushes, not across connections.
//!
//! **A pooled client needs no protocol change at all** — the wire protocol
//! already permits several frames in flight on one connection, and fanning out
//! across connections is measurable today: roughly 5× on `Buffered` at 16
//! connections and 9× on `Durable` at 48. A built-in batched-push *frame* is the
//! part that needs new wire machinery. An earlier version of this paragraph
//! attributed the protocol cost to both and so steered readers away from the one
//! option that already works.
//!
//! # Example
//!
//! ```no_run
//! # #[cfg(unix)] {
//! use weir_client::{WeirClient, Durability}; // Durability is re-exported from weir-core
//!
//! let mut client = WeirClient::connect("/run/weir/weir.sock").unwrap();
//! client.push(b"hello world", Durability::Durable).unwrap();
//! # }
//! ```
//!
//! # Knowing *which* record you pushed
//!
//! [`push`](WeirClient::push) returns `()`: the record was accepted, and nothing
//! more. [`push_tracked`](WeirClient::push_tracked) instead returns a
//! [`RecordCoordinate`] — the buffer segment the record landed in, its index
//! within that segment, and the `record_id` the downstream sink will see for it
//! as its idempotency key. That is what lets a producer reconcile what it sent
//! against what arrived, rather than carrying its own sequence inside the
//! payload and reconciling server-side.
//!
//! ```no_run
//! # #[cfg(unix)] {
//! use weir_client::{WeirClient, Durability};
//!
//! let mut client = WeirClient::connect("/run/weir/weir.sock").unwrap();
//! let at = client.push_tracked(b"hello world", Durability::Durable).unwrap();
//! println!("{} record {} -> {}", at.segment(), at.index(), at.record_id_hex());
//! # }
//! ```
//!
//! It is a **buffer address, not a per-producer sequence**: other producers'
//! records interleave, so your own indices have holes. See
//! [`push_tracked`](WeirClient::push_tracked) for the full list of what it does
//! and does not promise, including how to detect a daemon that predates it.
//!
//! # Ack vs. delivery
//!
//! A successful [`push`](WeirClient::push) means the record is **durably buffered
//! at the requested [`Durability`] tier** — fsync'd to the write-ahead buffer for
//! [`Durable`](Durability::Durable), in memory for
//! [`Buffered`](Durability::Buffered). It does **not** mean the record has reached
//! your downstream sink yet: the daemon drains buffered records to the sink in
//! batches, only once a WAB segment seals (its size threshold, or daemon
//! shutdown). For a small smoke test the sink may not be touched at all — watch
//! `weir_records_ack_total` (acceptance), not the sink-commit metric, to confirm
//! the daemon took your records.
//!
//! [`push_tracked`](WeirClient::push_tracked) does not change this. Its
//! coordinate says *where the record will be read from*, not that it has been
//! read — it is an address, not a delivery receipt.
//!
//! # Running the daemon
//!
//! This crate is the producer side; the daemon is the `weir-server` binary:
//!
//! ```text
//! mkdir -p /run/weir/wab # the daemon does not create its directories
//! weir-server --wab-dir /run/weir/wab --socket-path /run/weir/weir.sock
//! ```
//!
//! `--wab-dir` must already exist. On macOS, do not place the socket directly in
//! `/tmp` (it is a symlink the hardened bind rejects) — use a dedicated `0700`
//! directory. Run `weir-server --help` for the full option list.
//!
//! # Observability
//!
//! The daemon serves Prometheus metrics at `127.0.0.1:9185/metrics` by default.
//! The counters a producer cares about are labelled by tier/reason, e.g.
//! `weir_records_accepted_total{tier="durable"}`,
//! `weir_records_ack_total{tier="durable"}`, and
//! `weir_records_nack_total{tier="durable",reason="empty_payload"}`.
/// Transport-agnostic client core. Split out of `unix` so that `tls` — which
/// needs a `TcpStream` and rustls, and nothing Unix-specific — is gated on the
/// feature alone rather than on the platform.
pub use ;
/// Re-export of [`weir_core::Durability`] so the common producer path needs a
/// single crate import (`weir_client::Durability`).
pub use Durability;
/// Re-exported: the durable address [`WeirClient::push_tracked`] returns for an
/// accepted record. Lives in `weir-core` because both ends of the socket build
/// it from the same bytes.
pub use RecordCoordinate;
/// Re-export of [`weir_core::NackReason`] — the payload of [`ClientError::Nack`].
/// Re-exported so consumers can match on the reason (e.g. to distinguish the
/// connection-closing Nacks) without taking a direct dependency on `weir-core`.
pub use NackReason;
pub use ;