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}