Skip to main content

rightkit_daemon/
event_log.rs

1//! A bounded, pollable log of recent service events, for clients that cannot hold a stream open
2//! (a C ABI with no cancel entry point, for example). Every published event is appended with a
3//! sequence number; a client asks for what is newer than the last number it saw, waiting a bounded
4//! time ([`MAX_WAIT_MS`]) when nothing is.
5//!
6//! Numbers: a sequence number is unique and increasing within one process, and the first one is
7//! the process's start time in microseconds, so a restarted service never reuses a number a client
8//! saw from the one before. A cursor older than the oldest kept event, or newer than the newest, is
9//! reported as `dropped`: the client re-reads its state and polls from `next`.
10
11use std::collections::VecDeque;
12use std::sync::{Arc, Condvar, Mutex, MutexGuard};
13use std::time::{Duration, Instant, SystemTime, UNIX_EPOCH};
14
15use serde_json::Value;
16
17/// Events kept; older ones are dropped first.
18pub const MAX_EVENTS: usize = 512;
19/// Serialized payload bytes kept in total (an event larger than this is replaced by a `{truncated, bytes}` marker).
20pub const MAX_BYTES: usize = 4 * 1024 * 1024;
21/// The longest a poll waits for an event, in milliseconds. A larger request is clamped.
22pub const MAX_WAIT_MS: u32 = 25_000;
23/// One reply carries at most this many events and this many payload bytes (the newest unread
24/// event past either limit is left for the next poll), so it always fits a frame.
25const REPLY_EVENTS: usize = 256;
26const REPLY_BYTES: usize = 8 * 1024 * 1024;
27
28struct Entry {
29    seq: u64,
30    topic: String,
31    payload: Arc<Value>,
32    size: usize,
33}
34
35struct State {
36    /// Contiguous sequence numbers, oldest first.
37    entries: VecDeque<Entry>,
38    bytes: usize,
39    /// The number the next event gets.
40    next_seq: u64,
41    closed: bool,
42}
43
44/// One event a poll returns.
45pub struct Polled {
46    /// The event's sequence number.
47    pub seq: u64,
48    /// The event's topic.
49    pub topic: String,
50    /// The event's payload (a `{truncated, bytes}` marker when it was too large to keep).
51    pub payload: Arc<Value>,
52}
53
54/// What a poll answers.
55pub struct PollResult {
56    /// The events, oldest first.
57    pub events: Vec<Polled>,
58    /// The cursor for the next poll: the last number this reply covers.
59    pub next: u64,
60    /// The cursor `after` is not one this log can continue from, so events may have been missed
61    /// (or none were). `events` is empty and `next` is the newest number: re-read state, then
62    /// poll from `next`.
63    pub dropped: bool,
64}
65
66/// The bounded event log; share it behind an `Arc`.
67pub struct EventLog {
68    state: Mutex<State>,
69    wake: Condvar,
70}
71
72impl Default for EventLog {
73    fn default() -> EventLog {
74        let start = SystemTime::now()
75            .duration_since(UNIX_EPOCH)
76            .map(|d| d.as_micros() as u64)
77            .unwrap_or(0);
78        EventLog {
79            state: Mutex::new(State {
80                entries: VecDeque::new(),
81                bytes: 0,
82                next_seq: start.saturating_add(1),
83                closed: false,
84            }),
85            wake: Condvar::new(),
86        }
87    }
88}
89
90impl EventLog {
91    fn lock(&self) -> MutexGuard<'_, State> {
92        self.state.lock().unwrap_or_else(|e| e.into_inner())
93    }
94
95    /// Appends one event and wakes every waiting poll.
96    pub fn push(&self, topic: &str, payload: &Value) {
97        let mut size = serde_json::to_vec(payload).map(|v| v.len()).unwrap_or(0);
98        // An event larger than the whole bound is replaced by a marker so the bound is hard.
99        let oversized = size > MAX_BYTES;
100        let marker = serde_json::json!({ "truncated": true, "bytes": size });
101        let payload = if oversized { &marker } else { payload };
102        if oversized {
103            size = serde_json::to_vec(payload).map(|v| v.len()).unwrap_or(0);
104        }
105        let mut state = self.lock();
106        let seq = state.next_seq;
107        state.next_seq = seq.saturating_add(1);
108        state.bytes += size;
109        state.entries.push_back(Entry {
110            seq,
111            topic: topic.to_string(),
112            payload: Arc::new(payload.clone()),
113            size,
114        });
115        while state.entries.len() > 1
116            && (state.entries.len() > MAX_EVENTS || state.bytes > MAX_BYTES)
117        {
118            if let Some(old) = state.entries.pop_front() {
119                state.bytes -= old.size;
120            }
121        }
122        drop(state);
123        self.wake.notify_all();
124    }
125
126    /// Ends every wait now and makes later polls answer at once (service shutdown).
127    pub fn close(&self) {
128        self.lock().closed = true;
129        self.wake.notify_all();
130    }
131
132    /// The events with a number above `after` on one of `topics` (every topic when it is empty),
133    /// waiting up to `wait` when there are none. Never waits when the cursor is `dropped`, and
134    /// never busy-waits: a poll sleeps on the condition variable until an event or the deadline.
135    pub fn poll(&self, after: u64, topics: &[String], wait: Duration) -> PollResult {
136        let deadline = Instant::now() + wait;
137        let mut state = self.lock();
138        loop {
139            let last = state.next_seq - 1;
140            let oldest = state.entries.front().map_or(state.next_seq, |e| e.seq);
141            if after > last || after.saturating_add(1) < oldest {
142                return PollResult {
143                    events: Vec::new(),
144                    next: last,
145                    dropped: true,
146                };
147            }
148            // Numbers are contiguous, so the first unread entry is at a fixed offset.
149            let start = ((after + 1 - oldest) as usize).min(state.entries.len());
150            let mut events = Vec::new();
151            let mut bytes = 0usize;
152            let mut truncated = false;
153            for entry in state.entries.range(start..) {
154                if !topics.is_empty() && !topics.contains(&entry.topic) {
155                    continue;
156                }
157                if !events.is_empty()
158                    && (events.len() >= REPLY_EVENTS || bytes + entry.size > REPLY_BYTES)
159                {
160                    truncated = true;
161                    break;
162                }
163                bytes += entry.size;
164                events.push(Polled {
165                    seq: entry.seq,
166                    topic: entry.topic.clone(),
167                    payload: Arc::clone(&entry.payload),
168                });
169            }
170            if !events.is_empty() {
171                let next = if truncated {
172                    events.last().map_or(last, |e| e.seq)
173                } else {
174                    last
175                };
176                return PollResult {
177                    events,
178                    next,
179                    dropped: false,
180                };
181            }
182            // Nothing matched everything up to `last`: a later poll starts after it.
183            let now = Instant::now();
184            if state.closed || now >= deadline {
185                return PollResult {
186                    events,
187                    next: last,
188                    dropped: false,
189                };
190            }
191            state = self
192                .wake
193                .wait_timeout(state, deadline - now)
194                .unwrap_or_else(|e| e.into_inner())
195                .0;
196        }
197    }
198}