Skip to main content

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}