kevy-rt 6.4.0

kevy thread-per-core shared-nothing runtime — pure Rust, zero deps.
Documentation
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
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
//! S3: the epoll/kqueue reactors' AOF writer lane — the poll-based
//! counterpart of the io_uring offload (`uring_aof`). Appends queue on
//! [`kevy_persist::Aof`] exactly as under S1; a per-shard thread drains
//! them with sequential `write_all`s on a cloned O_APPEND handle
//! (byte-identical to the synchronous path) and runs the fsyncs, so
//! the reactor never blocks on either. Fsync completions carry the S2
//! record watermark: under `always` the reply gate in `flush_conn`
//! holds output until the covering fsync lands, and the lane wakes the
//! shard through its poller waker to release it.
//!
//! Ordering: one mpsc channel is the whole discipline — appends and
//! fsyncs execute in submission order, so a fsync submitted after the
//! queue drained provably covers every earlier record (the same
//! drain-then-fsync contract the ring path keeps via SQ emptiness).

// A discarded fsync. A transient failure self-heals — `dirty` stays
// set and the next tick retries — but a persistent one (full disk,
// read-only remount, EIO) means `appendfsync everysec` has quietly
// become "never" with nothing saying so. Open question §2.
#![expect(
    clippy::let_underscore_must_use,
    reason = "a persistent fsync failure is invisible; see .claude/OPEN-QUESTIONS-6.4.md"
)]

use std::fs::File;
use std::io::Write;
use std::sync::Arc;
use std::sync::mpsc::{Receiver, Sender, TryRecvError};
use std::time::Instant;

use crate::Commands;
use crate::shard::Shard;
use kevy_sys::Waker;

enum Job {
    Append(Vec<u8>),
    Fsync {
        covers: u64,
    },
    /// Swap the lane onto a fresh handle (post-rewrite reopen). The
    /// old clone points at the renamed-away inode; dropping it here
    /// keeps that close off the reactor.
    Reopen(File),
}

enum Done {
    Appended,
    Synced { covers: u64 },
    Failed { what: &'static str, err: String },
}

/// Per-shard lane state. Lives on [`Shard`]; every method is a no-op
/// unless [`Self::enabled`].
pub(crate) struct AofWriterLane {
    pub(crate) enabled: bool,
    chans: Option<(Sender<Job>, Receiver<Done>)>,
    /// Append jobs submitted vs completed — the structural-op drain
    /// predicate (a rewrite swap must not run over in-flight writes).
    submitted: u64,
    completed: u64,
    fsync_inflight: bool,
    /// An append completed since the last fsync completion.
    dirty_since_sync: bool,
    last_sync: Instant,
    /// Records fsync-proven durable (S2 semantics — compare against
    /// [`kevy_persist::Aof::queued_watermark`] stamps).
    pub(crate) durable_watermark: u64,
    /// Conns whose output the always gate held; flushed on watermark
    /// advance (the poller would otherwise spin on a writable socket
    /// we refuse to write).
    pub(crate) held_conns: Vec<u64>,
    /// A structural op arrived while writes were in flight; the tick
    /// retries it after the drain.
    pub(crate) want_restructure: bool,
}

impl Default for AofWriterLane {
    fn default() -> Self {
        Self {
            enabled: false,
            chans: None,
            submitted: 0,
            completed: 0,
            fsync_inflight: false,
            dirty_since_sync: false,
            last_sync: Instant::now(),
            durable_watermark: 0,
            held_conns: Vec::new(),
            want_restructure: false,
        }
    }
}

impl AofWriterLane {
    /// Spawn the worker on `file` (a `queued_file_clone`). The thread
    /// exits when the job sender drops (shard teardown).
    fn spawn(&mut self, shard_id: usize, file: File, waker: Arc<Waker>) {
        let (job_tx, job_rx) = std::sync::mpsc::channel::<Job>();
        let (done_tx, done_rx) = std::sync::mpsc::channel::<Done>();
        let spawned = std::thread::Builder::new()
            .name(format!("kevy-aofw-{shard_id}"))
            .spawn(move || run_worker(file, &job_rx, &done_tx, &waker));
        if spawned.is_ok() {
            self.chans = Some((job_tx, done_rx));
            self.enabled = true;
        } else {
            eprintln!(
                "kevy: shard {shard_id} aof writer thread failed to spawn; keeping the synchronous path"
            );
        }
    }

    fn submit(&mut self, job: Job) -> Result<(), Job> {
        match &self.chans {
            Some((tx, _)) => tx.send(job).map_err(|e| e.0),
            None => Err(job),
        }
    }

    /// Every append this lane has taken is completed and nothing else
    /// is queued behind them.
    fn appends_drained(&self) -> bool {
        self.submitted == self.completed
    }
}

/// Worker body: strict submission order; a failed write retries in
/// place (loud, 10 ms backoff) because the bytes exist only here — the
/// same never-drop contract the ring path keeps by re-arming the SQE.
fn run_worker(mut file: File, jobs: &Receiver<Job>, done: &Sender<Done>, waker: &Arc<Waker>) {
    while let Ok(job) = jobs.recv() {
        let d = match job {
            Job::Append(bytes) => {
                while let Err(e) = file.write_all(&bytes) {
                    eprintln!("kevy: aof writer append failed: {e}; retrying");
                    std::thread::sleep(std::time::Duration::from_millis(10));
                }
                Done::Appended
            }
            Job::Fsync { covers } => match file.sync_data() {
                Ok(()) => Done::Synced { covers },
                Err(e) => Done::Failed { what: "fsync", err: e.to_string() },
            },
            Job::Reopen(f) => {
                file = f;
                continue;
            }
        };
        if done.send(d).is_err() {
            return; // shard gone
        }
        // Best-effort: a failed wake only delays the reap to the next
        // natural poller wakeup.
        let _ = waker.wake();
    }
}

impl<C: Commands> Shard<C> {
    /// Writer-lane mode at reactor setup — DEFAULT ON for the
    /// epoll/kqueue reactor with an AOF (mirrors `uring_aof_setup`;
    /// `KEVY_AOF_OFFLOAD=0/off/no/false` keeps the classic synchronous
    /// path). Under `always` the replies are watermark-gated in
    /// `flush_conn` instead of fsync-blocking the reactor.
    pub(crate) fn epoll_aof_setup(&mut self) {
        let Some(aof) = &mut self.aof else { return };
        if matches!(std::env::var("KEVY_AOF_OFFLOAD").as_deref(), Ok("0" | "off" | "no" | "false"))
        {
            return;
        }
        aof.enable_queued_appends();
        let file = match aof.queued_file_clone() {
            Some(Ok(f)) => f,
            Some(Err(e)) => {
                eprintln!(
                    "kevy: shard {} aof handle clone failed: {e}; keeping the synchronous path",
                    self.id
                );
                return;
            }
            None => return,
        };
        let (id, waker) = (self.id, Arc::clone(&self.waker));
        self.aof_lane.spawn(id, file, waker);
    }

    /// Per-iteration pump (replaces the synchronous `maybe_sync` call
    /// in the reactor loop): reap completions, hand new queue contents
    /// to the worker, schedule the fsync. Falls back to the classic
    /// `maybe_sync` when the lane is off — byte-identical behavior.
    pub(crate) fn epoll_aof_tick(&mut self) {
        if !self.aof_lane.enabled {
            if let Some(aof) = &mut self.aof {
                let _ = aof.maybe_sync();
            }
            return;
        }
        self.epoll_aof_reap();
        let holding = self.aof.as_ref().is_some_and(kevy_persist::Aof::swap_holding);
        if !holding
            && let Some(aof) = &mut self.aof
            && let Some((_, bytes)) = aof.take_pending()
        {
            match self.aof_lane.submit(Job::Append(bytes)) {
                Ok(()) => self.aof_lane.submitted += 1,
                Err(Job::Append(bytes)) => {
                    // The chunk's bytes exist only here — put them
                    // back at the queue front so the fallback's
                    // flush_queued lands them exactly once, in order.
                    if let Some(aof) = &mut self.aof {
                        aof.requeue_front(bytes);
                    }
                    self.epoll_aof_dead_lane();
                }
                Err(_) => self.epoll_aof_dead_lane(),
            }
        }
        if !holding {
            self.epoll_aof_maybe_fsync();
        }
    }

    /// Drain the done channel: advance the durable watermark on fsync
    /// completions and release anything the always gate held.
    fn epoll_aof_reap(&mut self) {
        let mut advanced = false;
        loop {
            let msg = match &self.aof_lane.chans {
                Some((_, rx)) => rx.try_recv(),
                None => return,
            };
            match msg {
                Ok(Done::Appended) => {
                    self.aof_lane.completed += 1;
                    self.aof_lane.dirty_since_sync = true;
                }
                Ok(Done::Synced { covers }) => {
                    self.aof_lane.fsync_inflight = false;
                    self.aof_lane.dirty_since_sync = false;
                    self.aof_lane.last_sync = Instant::now();
                    self.aof_lane.durable_watermark = self.aof_lane.durable_watermark.max(covers);
                    advanced = true;
                }
                Ok(Done::Failed { what, err }) => {
                    // Same contract as the ring path: the watermark
                    // stays put (held replies stay held — no false
                    // ack) and the tick resubmits.
                    self.aof_lane.fsync_inflight = false;
                    eprintln!("kevy: shard {} aof writer {what} failed: {err}", self.id);
                }
                Err(TryRecvError::Empty) => break,
                Err(TryRecvError::Disconnected) => {
                    self.epoll_aof_dead_lane();
                    return;
                }
            }
        }
        if advanced {
            self.epoll_release_held();
        }
    }

    /// One DATASYNC at a time, only when the queue and the worker are
    /// drained of appends (submission order then makes it cover every
    /// queued record). always: as soon as any record is unproven.
    /// everysec: on the 1 s cadence.
    fn epoll_aof_maybe_fsync(&mut self) {
        let lane = &self.aof_lane;
        if lane.fsync_inflight || !lane.appends_drained() {
            return;
        }
        let Some(aof) = &self.aof else { return };
        if !aof.queued_is_empty() {
            return;
        }
        let due = match aof.fsync_policy() {
            kevy_persist::Fsync::Always => aof.queued_watermark() > lane.durable_watermark,
            kevy_persist::Fsync::EverySec => {
                lane.dirty_since_sync && lane.last_sync.elapsed().as_secs() >= 1
            }
            kevy_persist::Fsync::No => false,
        };
        if !due {
            return;
        }
        let covers = aof.queued_watermark();
        if self.aof_lane.submit(Job::Fsync { covers }).is_ok() {
            self.aof_lane.fsync_inflight = true;
        } else {
            self.epoll_aof_dead_lane();
        }
    }

    /// Flush every conn the always gate parked, plus any held
    /// cross-shard response batches the watermark now covers.
    fn epoll_release_held(&mut self) {
        let held = std::mem::take(&mut self.aof_lane.held_conns);
        for cid in held {
            let _ = self.flush_conn(cid);
        }
        self.flush_held_responses();
    }

    /// The worker is gone (panicked/exited): fall back to the honest
    /// synchronous path for good. Queued bytes are safe — they are
    /// still in the queue or were written before the death; the
    /// structural entry points' `flush_queued` covers the rest.
    #[cold]
    fn epoll_aof_dead_lane(&mut self) {
        if self.aof_lane.enabled {
            eprintln!(
                "kevy: shard {} aof writer lane lost; reverting to the synchronous path",
                self.id
            );
        }
        self.aof_lane.enabled = false;
        self.aof_lane.chans = None;
        // Anything held must not wait on a fsync that will never come:
        // sync in place, mark durable, release.
        if let Some(aof) = &mut self.aof {
            if let Err(e) = aof.sync_now() {
                eprintln!("kevy: shard {} fallback sync failed: {e}", self.id);
                return; // keep holds — no false ack
            }
            self.aof_lane.durable_watermark = aof.queued_watermark();
        }
        self.epoll_release_held();
    }

    /// S3 stamp (the epoll counterpart of `uring_stamp_hold`): if the
    /// dispatch bracketed by `w0` appended queued records, mark the
    /// conn's pending output as held until the covering fsync. A conn
    /// holding replies for several batches keeps the highest stamp.
    pub(crate) fn epoll_stamp_hold(&mut self, w0: Option<u64>, cid: u64) {
        let Some(w0) = w0 else { return };
        if !self.aof_lane.enabled {
            return; // uring shards stamp through their own path
        }
        let Some(aof) = self.aof.as_ref() else { return };
        let w1 = aof.queued_watermark();
        if w1 > w0
            && let Some(c) = self.conns.get_mut(&cid)
        {
            c.held_watermark = Some(c.held_watermark.map_or(w1, |h| h.max(w1)));
        }
    }

    /// May a structural file operation run right now? (The epoll
    /// counterpart of `uring_aof_restructure_ready`.)
    pub(crate) fn epoll_aof_restructure_ready(&self) -> bool {
        !self.aof_lane.enabled
            || (self.aof_lane.appends_drained()
                && self.aof.as_ref().is_none_or(kevy_persist::Aof::queued_is_empty))
    }

    /// Tick wrapper for the persistence trio (the epoll counterpart of
    /// `uring_tick_persist`): structural work only runs with the lane
    /// drained; a deferred BGREWRITEAOF fires here.
    pub(crate) fn epoll_tick_persist(&mut self) {
        self.check_tee_overrun();
        if self.epoll_aof_restructure_ready() {
            if self.aof_lane.want_restructure {
                self.aof_lane.want_restructure = false;
                self.start_bg_rewrite();
            }
            self.tick_persist();
        }
    }

    /// A structural op (worker-fsynced image swap) made everything
    /// queued so far durable; also hand the lane the post-swap handle.
    pub(crate) fn epoll_aof_on_swap_finalized(&mut self) {
        if !self.aof_lane.enabled {
            return;
        }
        if let Some(aof) = &self.aof {
            self.aof_lane.durable_watermark =
                self.aof_lane.durable_watermark.max(aof.queued_watermark());
            match aof.queued_file_clone() {
                Some(Ok(f)) => {
                    if self.aof_lane.submit(Job::Reopen(f)).is_err() {
                        self.epoll_aof_dead_lane();
                        return;
                    }
                }
                Some(Err(e)) => {
                    eprintln!("kevy: shard {} post-swap handle clone failed: {e}", self.id);
                    self.epoll_aof_dead_lane();
                    return;
                }
                None => {}
            }
        }
        self.epoll_release_held();
    }

    /// Settle the lane: wait for in-flight appends to complete. Used
    /// at reactor exit (so the shutdown `sync_now` sees a complete
    /// file) and before any owner-handle write that would otherwise
    /// interleave with the clone's (fsync-policy switch).
    pub(crate) fn epoll_aof_settle(&mut self) {
        while self.aof_lane.enabled && !self.aof_lane.appends_drained() {
            self.epoll_aof_reap();
            std::thread::yield_now();
        }
    }
}