net/adapter/net/redex/config.rs
1//! Per-file configuration for RedEX.
2
3use std::time::Duration;
4
5use super::replication_config::ReplicationConfig;
6
7/// Disk-side fsync policy for persistent `RedexFile`s.
8///
9/// Governs **only** the append path on the disk mirror. `close()` and
10/// explicit `RedexFile::sync()` calls always fsync regardless of
11/// policy — these are the caller's explicit durability barriers.
12///
13/// | Policy | Process crash | Kernel / power crash |
14/// |--------|---------------|---------------------|
15/// | `Never` | Loses the tail since last close / `sync()` | Same |
16/// | `EveryN(N)` | Loses ≤ (N−1) entries from the last sync point | Same |
17/// | `Interval(d)` | Loses ≤ `d` seconds of writes | Same |
18/// | `IntervalOrBytes { period, max_bytes }` | Loses ≤ min(`period` of writes, `max_bytes` of writes) | Same |
19///
20/// Default is [`FsyncPolicy::Never`], matching the pre-`FsyncPolicy`
21/// behavior — OS page cache only, fsync on close. Callers that need
22/// tighter bounds opt into `EveryN`, `Interval`, or
23/// `IntervalOrBytes`.
24#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
25pub enum FsyncPolicy {
26 /// Never fsync on append. `close()` still syncs. Lowest latency;
27 /// fine for telemetry / best-effort logs.
28 #[default]
29 Never,
30 /// Fsync after every N successful appends. The fsync runs on a
31 /// background task — the appender returns as soon as the bytes
32 /// land in the page cache and signals the worker; the worker
33 /// runs `fsync_all` off the hot path. Concurrent notifies during
34 /// an in-flight fsync coalesce into a single follow-up.
35 ///
36 /// Worst-case loss bound: (N − 1) entries since the last sync
37 /// **point**, plus the bytes from any fsync that was in flight
38 /// when the crash interrupted it. `0` and `1` both collapse to
39 /// "signal on every append."
40 ///
41 /// Under heavy concurrent appends the bound loosens slightly:
42 /// the threshold check (`fetch_add` then `if cross { reset }`)
43 /// is not a CAS, so K appenders racing past the threshold can
44 /// each cross before any of them resets the counter. The next
45 /// sync covers all K of those entries; the durability contract
46 /// still holds (no entry survives unsynced past the next
47 /// fsync), but the practical bound becomes
48 /// `(N − 1) + (concurrent appenders at threshold)`. Pick a
49 /// smaller N if you need a tighter bound under contention.
50 EveryN(u64),
51 /// Fsync on a timer, independent of append rate. A per-file
52 /// background tokio task drives the sync; `close()` cancels it.
53 Interval(Duration),
54 /// Fsync when **either** `period` elapses **or** `max_bytes` of
55 /// writes have accumulated since the last sync, whichever comes
56 /// first. The byte threshold counts every byte written to dat,
57 /// idx, and ts.
58 ///
59 /// Use this for bursty workloads where a long `period` would
60 /// leave too much data unsynced under load, but a short `period`
61 /// would over-fsync when idle.
62 ///
63 /// Configuration matrix:
64 ///
65 /// | `period` | `max_bytes` | Behavior |
66 /// |----------|-------------|----------|
67 /// | `> 0` | `> 0` | Full both-arms worker (timer + byte signal) |
68 /// | `> 0` | `0` | Timer-only worker (equivalent to `Interval(period)`) |
69 /// | `0` | `> 0` | Byte-only worker (no timer arm); fsyncs when the byte threshold crosses |
70 /// | `0` | `0` | No worker; equivalent to `Never` |
71 ///
72 /// The same concurrency caveat as [`Self::EveryN`] applies to
73 /// the byte arm: K concurrent appenders can each cross the
74 /// threshold before any of them resets the counter, so the
75 /// effective bound is
76 /// `max_bytes + (concurrent appenders' bytes at threshold)`.
77 IntervalOrBytes {
78 /// Maximum wall-clock interval between syncs. `0` disables
79 /// the timer arm; pair with a non-zero `max_bytes` to get a
80 /// byte-only worker.
81 period: Duration,
82 /// Maximum bytes (across dat + idx + ts) accumulated since
83 /// the last sync before the worker is signaled. `0`
84 /// disables the byte arm; pair with a non-zero `period` to
85 /// get a timer-only worker (equivalent to `Interval`).
86 max_bytes: u64,
87 },
88}
89
90/// Per-file configuration supplied at `Redex::open_file` time.
91///
92/// Was `Copy` pre-replication. The `replication` field carries a
93/// `Vec<NodeId>` when [`PlacementStrategy::Pinned`](super::replication_config::PlacementStrategy::Pinned) is in use, so
94/// the type is now `Clone`-only. The struct is small and rarely
95/// passed in hot paths; existing callers add a `.clone()` where they
96/// previously relied on bit-copy semantics.
97#[derive(Debug, Clone)]
98pub struct RedexFileConfig {
99 /// Heap-only (`false`) vs heap + simple disk segment (`true`).
100 ///
101 /// `true` requires the `redex-disk` feature **and** a persistent
102 /// base directory configured on the owning `Redex` manager via
103 /// `Redex::with_persistent_dir`. With no base dir, `open_file`
104 /// returns an error.
105 ///
106 /// With `redex-disk` off, this field is silently ignored — the
107 /// file is heap-only regardless.
108 pub persistent: bool,
109
110 /// Disk fsync policy for persistent files. Ignored when
111 /// `persistent == false`. Defaults to [`FsyncPolicy::Never`].
112 pub fsync_policy: FsyncPolicy,
113
114 /// Initial reservation hint for the heap payload segment. Used
115 /// only as the capacity passed to the backing `Vec` on open,
116 /// capped at 64 MiB internally — the segment grows past this
117 /// value on append up to a 3 GB hard limit. **Retention is NOT
118 /// driven by this field** in v1; use `retention_max_events`,
119 /// `retention_max_bytes`, or `retention_max_age_ns` for that.
120 ///
121 /// v2's warm-tier rollover will consume this value as the
122 /// rollover trigger (see REDEX_V2_PLAN §3).
123 pub max_memory_bytes: usize,
124
125 /// Keep only the newest K events. `None` = unbounded.
126 pub retention_max_events: Option<u64>,
127
128 /// Keep only the newest M bytes of payload. `None` = unbounded.
129 pub retention_max_bytes: Option<u64>,
130
131 /// Drop entries older than this many nanoseconds at the next
132 /// [`super::RedexFile::sweep_retention`] tick. Age is measured
133 /// against `SystemTime::now()` at append time.
134 ///
135 /// v2 limitation: per-entry timestamps are in-memory only. On
136 /// reopen of a persistent file, all recovered entries get "now"
137 /// as their fake timestamp — age retention starts fresh from
138 /// the reopen moment. v2 mmap tier will persist timestamps.
139 pub retention_max_age_ns: Option<u64>,
140
141 /// Per-subscription buffer depth for `tail()` streams. Caps the
142 /// memory a slow subscriber can pin at `tail_buffer_size *
143 /// avg_event_size`. Subscribers that can't drain this many
144 /// pending events get disconnected with a best-effort
145 /// `RedexError::Lagged` signal.
146 ///
147 /// Tune up for bursty workloads with brief consumer pauses;
148 /// tune down to reclaim memory faster from misbehaving
149 /// subscribers. Default: 1024.
150 pub tail_buffer_size: usize,
151
152 /// Cross-node replication opt-in per
153 /// `docs/plans/REDEX_DISTRIBUTED_PLAN.md` §1. `None` (default)
154 /// keeps the file single-node; `Some(cfg)` opts the channel
155 /// into the `ReplicationCoordinator` lifecycle Phase C wires.
156 ///
157 /// Validate via `cfg.validate()` before committing to a
158 /// `Redex`; Phase C's `Redex::open_file` will surface a typed
159 /// `ReplicationConfigError` if the field is `Some(cfg)` with
160 /// `cfg.validate().is_err()`.
161 pub replication: Option<ReplicationConfig>,
162
163 /// Dataforts Phase 3 — id of the `BlobAdapter` (from the
164 /// `dataforts` module, gated behind the `dataforts`
165 /// feature) this channel's events resolve against when an
166 /// event payload's first byte is the `BlobRef`
167 /// discriminator. `None` (default) means callers of
168 /// `RedexFile::resolve_one` MUST pass an adapter
169 /// explicitly; `Some(id)` lets them route through
170 /// `global_blob_adapter_registry()` automatically. The
171 /// field is advisory metadata at the RedEX layer —
172 /// substrate reads still return raw payload bytes; the
173 /// resolution decision happens at the convenience read
174 /// helpers.
175 pub blob_adapter_id: Option<String>,
176
177 /// Per-channel override for the blob adapter registry. `None`
178 /// (default) routes through `global_blob_adapter_registry()`;
179 /// `Some(reg)` looks `blob_adapter_id` up in the supplied
180 /// registry instead. Used by multi-tenant binding hosts to
181 /// scope adapter ids per tenant — a tenant's
182 /// `register_blob_adapter("s3-primary", ...)` lands in its own
183 /// registry without colliding with another tenant's same-named
184 /// adapter.
185 ///
186 /// Wrapped in `Arc` so the config is `Clone`-cheap and
187 /// multiple channels can share one registry.
188 #[cfg(feature = "dataforts")]
189 pub blob_adapter_registry:
190 Option<std::sync::Arc<super::super::dataforts::blob::BlobAdapterRegistry>>,
191}
192
193impl Default for RedexFileConfig {
194 fn default() -> Self {
195 Self {
196 persistent: false,
197 fsync_policy: FsyncPolicy::Never,
198 max_memory_bytes: 64 * 1024 * 1024, // 64 MiB soft cap
199 retention_max_events: None,
200 retention_max_bytes: None,
201 retention_max_age_ns: None,
202 tail_buffer_size: 1024,
203 replication: None,
204 blob_adapter_id: None,
205 #[cfg(feature = "dataforts")]
206 blob_adapter_registry: None,
207 }
208 }
209}
210
211impl RedexFileConfig {
212 /// Start from defaults.
213 pub fn new() -> Self {
214 Self::default()
215 }
216
217 /// Enable persistent (disk-backed) storage.
218 pub fn with_persistent(mut self, persistent: bool) -> Self {
219 self.persistent = persistent;
220 self
221 }
222
223 /// Set the disk fsync policy. See [`FsyncPolicy`] for the
224 /// durability / latency trade-offs each variant offers.
225 pub fn with_fsync_policy(mut self, policy: FsyncPolicy) -> Self {
226 self.fsync_policy = policy;
227 self
228 }
229
230 /// Set the initial reservation size for the heap segment (capped
231 /// at 64 MiB internally). Does NOT enforce a retention cap — use
232 /// [`Self::with_retention_max_bytes`] for that.
233 pub fn with_max_memory_bytes(mut self, bytes: usize) -> Self {
234 self.max_memory_bytes = bytes;
235 self
236 }
237
238 /// Keep at most `events` entries.
239 pub fn with_retention_max_events(mut self, events: u64) -> Self {
240 self.retention_max_events = Some(events);
241 self
242 }
243
244 /// Keep at most `bytes` bytes of payload.
245 pub fn with_retention_max_bytes(mut self, bytes: u64) -> Self {
246 self.retention_max_bytes = Some(bytes);
247 self
248 }
249
250 /// Drop entries older than `max_age`. Measured in nanoseconds
251 /// against `SystemTime::now()` at append time.
252 pub fn with_retention_max_age(mut self, max_age: Duration) -> Self {
253 self.retention_max_age_ns = Some(max_age.as_nanos() as u64);
254 self
255 }
256
257 /// Set the per-subscription buffer depth for `tail()` streams.
258 /// See the field doc on [`Self::tail_buffer_size`].
259 pub fn with_tail_buffer_size(mut self, size: usize) -> Self {
260 self.tail_buffer_size = size;
261 self
262 }
263
264 /// Opt the channel into cross-node replication. Pass `None` to
265 /// restore single-node behavior. The supplied
266 /// [`ReplicationConfig`] should validate cleanly (see
267 /// [`ReplicationConfig::validate`]); Phase C's `Redex::open_file`
268 /// surfaces validation errors typed.
269 pub fn with_replication(mut self, replication: Option<ReplicationConfig>) -> Self {
270 self.replication = replication;
271 self
272 }
273
274 /// Set the dataforts blob adapter id used by
275 /// `RedexFile::resolve_one` (under the `dataforts`
276 /// feature). Pass `None` to clear.
277 pub fn with_blob_adapter_id(mut self, id: Option<String>) -> Self {
278 self.blob_adapter_id = id;
279 self
280 }
281
282 /// Bind a specific blob adapter registry for `resolve_one` to
283 /// look up `blob_adapter_id` against. `None` (default) falls
284 /// back to `global_blob_adapter_registry()`. Multi-tenant
285 /// binding hosts construct one registry per tenant and pass
286 /// it here to isolate adapter ids across tenants.
287 #[cfg(feature = "dataforts")]
288 pub fn with_blob_adapter_registry(
289 mut self,
290 registry: Option<std::sync::Arc<super::super::dataforts::blob::BlobAdapterRegistry>>,
291 ) -> Self {
292 self.blob_adapter_registry = registry;
293 self
294 }
295}