unlit3dx/request.rs
1//! Requests a world answers, and the waiting for their answers.
2//!
3//! A job is one way: what one world says to another is a closure, and nothing
4//! comes back with it. A caller that needs a value the other world makes asks
5//! for it, and [Pending](crate::request::Pending) is the asking: it sends the job, names what it asked
6//! for with a key of the caller's own — a ball's entity, say — and folds
7//! whatever came back into the world on the caller's own tick.
8//!
9//! Three things make it more than a queue of answers:
10//!
11//! - **An answer is named.** Two requests are not told apart by the payload
12//! they carry: the key the caller asked with is the key the answer comes
13//! back under, so a ball finds its own instance however many others were
14//! asked for at the same time.
15//! - **The wait is bounded.** A request whose answer has not come within
16//! `timeout` seconds is given up on, and so is every
17//! request still outstanding when the world that owes them ends: a job a
18//! panic took with it never answers, and neither does a world that stopped.
19//! - **Nothing waits on an empty slot.** The answers are folded on the
20//! caller's tick, so a caller never blocks, and an answer that arrives for
21//! a request already given up on is dropped instead of delivered twice.
22//!
23//! A [Pending](crate::request::Pending) is not a future and the wait is not a wakeup: an answer lands
24//! in a queue, and the tick that needs it folds it. That is what keeps a
25//! stepped world's wait identical to a threaded one's, and what lets an answer
26//! be applied straight to the world rather than handed to a continuation
27//! outside it — a continuation could not hold the `&mut World` the answer is
28//! for, and would have to wait for the tick anyway.
29
30use async_channel::{Receiver, Sender, unbounded};
31
32use unlit_ecs::World;
33
34use crate::channel::SnapshotReader;
35use crate::job::{JobSender, SendError};
36use crate::thread::WorldState;
37
38/// Whether the world behind a reader of its state has ended.
39///
40/// A question asked of a world that panicked is never answered, and neither is
41/// one asked of a world that stopped: the job is gone with the turn it ran in.
42/// A caller that asked waits on this instead of waiting forever.
43fn ended(state: &SnapshotReader<WorldState>) -> bool {
44 matches!(
45 state.latest().as_deref(),
46 Some(WorldState::Stopped | WorldState::Panicked(_))
47 )
48}
49
50/// How one request ended.
51#[derive(Clone, Copy, Debug, PartialEq, Eq)]
52pub enum Settled<T> {
53 /// The world it was asked of answered, with this.
54 ///
55 /// A request the world could not carry out answers as well, with a value
56 /// that says so: a show that did not happen answers `false` rather than
57 /// nothing, so the caller learns the difference between a world that has
58 /// not answered and one that answered no.
59 Answered(T),
60 /// The wait is over and nothing came: the request's time ran out, or the
61 /// world that owed it ended.
62 Expired,
63}
64
65/// The requests one world owes a caller, and the answers that have come back.
66///
67/// One [Pending] serves one world: it is built with the state that world
68/// publishes, and every request made through it goes to it. A caller that asks
69/// two worlds for two things keeps two.
70///
71/// `K` is what the caller names a request by, and `T` what the world answers
72/// with.
73pub struct Pending<K, T> {
74 /// What a job publishes its answer through. Cloned into every request.
75 answerer: Sender<(K, T)>,
76 /// Where the answers land, in the order they were published.
77 answers: Receiver<(K, T)>,
78 /// The state of the world that owes the answers.
79 state: SnapshotReader<WorldState>,
80 /// How many seconds a request may go unanswered; `None` waits for as long
81 /// as the world owes it.
82 timeout: Option<f32>,
83 /// The requests still outstanding, and how many seconds each has left.
84 outstanding: Vec<(K, Option<f32>)>,
85}
86
87impl<K, T> Pending<K, T>
88where
89 K: Eq + Clone + Send + 'static,
90 T: Send + 'static,
91{
92 /// Requests for the world `state` reports on, each allowed to go
93 /// unanswered for `timeout` seconds. `None` waits until the world answers
94 /// or ends.
95 #[must_use]
96 pub fn new(state: SnapshotReader<WorldState>, timeout: Option<f32>) -> Self {
97 let (answerer, answers) = unbounded();
98 Self {
99 answerer,
100 answers,
101 state,
102 timeout,
103 outstanding: Vec::new(),
104 }
105 }
106
107 /// Ask the world behind `sender` for what `make` returns, as the request
108 /// `key` names.
109 ///
110 /// `make` runs once, on the world's own thread, and whatever it returns is
111 /// the answer `key` receives. The send is refused when that world's queue
112 /// is full or it has stopped, and then no request is outstanding: the
113 /// caller decides whether to ask again on a later tick.
114 ///
115 /// A request already outstanding under `key` is sent again without
116 /// lengthening its wait: the answer already owed is the one that arrives,
117 /// and the tick the first was asked on is the one the wait is measured
118 /// from.
119 pub fn request<F>(&mut self, sender: &JobSender, key: K, make: F) -> Result<(), SendError>
120 where
121 F: FnOnce(&mut World) -> T + Send + 'static,
122 {
123 let answerer = self.answerer.clone();
124 let asked = key.clone();
125 sender.send(Box::new(move |world: &mut World| {
126 // The receiver outlives this job: the queue is unbounded and never
127 // closes while a Pending holds it, and an answer nobody waits for
128 // is dropped on the next advance rather than held.
129 let _ = answerer.try_send((asked, make(world)));
130 }))?;
131 if !self.outstanding.iter().any(|(asked, _)| *asked == key) {
132 self.outstanding.push((key, self.timeout));
133 }
134 Ok(())
135 }
136
137 /// What ended on this tick: the requests the world answered, and the ones
138 /// given up on, each under the key it was asked with.
139 ///
140 /// Call it once per tick of the asking world, and take every item it
141 /// writes: an unanswered request is given up on only once, and an answer
142 /// is handed out only once. A request is expired when the time it was
143 /// given runs out, or at once when the world that owed it ended — a job
144 /// that panicked takes its answer with it, and a world that stopped never
145 /// runs another.
146 ///
147 /// An answer that arrived for a request no longer outstanding is dropped:
148 /// the request was given up on, and whatever it asked for has already been
149 /// dealt with. Requests come out oldest first — the answered in the order
150 /// the world published them, then the expired in the order they were
151 /// asked — so two runs of the same durations settle the same requests in
152 /// the same order.
153 ///
154 /// `settled` is cleared and reused, so a caller that keeps one allocates
155 /// nothing per tick but what the answers themselves weigh.
156 pub fn advance(&mut self, delta_seconds: f32, settled: &mut Vec<(K, Settled<T>)>) {
157 settled.clear();
158 while let Ok((key, value)) = self.answers.try_recv() {
159 if self.take_outstanding(&key) {
160 settled.push((key, Settled::Answered(value)));
161 }
162 }
163 let ended = ended(&self.state);
164 let mut outstanding = std::mem::take(&mut self.outstanding);
165 outstanding.retain_mut(|(key, remaining)| {
166 if ended {
167 settled.push((key.clone(), Settled::Expired));
168 return false;
169 }
170 let Some(left) = remaining else {
171 return true;
172 };
173 *left -= delta_seconds.max(0.0);
174 if *left <= 0.0 {
175 settled.push((key.clone(), Settled::Expired));
176 return false;
177 }
178 true
179 });
180 self.outstanding = outstanding;
181 }
182
183 /// Whether a request named `key` is still owed, taking it out if it is.
184 fn take_outstanding(&mut self, key: &K) -> bool {
185 let Some(at) = self.outstanding.iter().position(|(asked, _)| asked == key) else {
186 return false;
187 };
188 self.outstanding.remove(at);
189 true
190 }
191}
192
193#[cfg(test)]
194mod tests {
195 use super::*;
196 use crate::channel::Snapshot;
197 use crate::job::Job;
198
199 /// Requests for a world in `state`, and the queue its requests go through.
200 fn asking(
201 state: WorldState,
202 timeout: Option<f32>,
203 ) -> (Pending<u32, u32>, JobSender, async_channel::Receiver<Job>) {
204 let snapshot = Snapshot::new();
205 snapshot.publish(state);
206 let pending = Pending::new(snapshot.reader(), timeout);
207 let (sender, receiver) = async_channel::unbounded();
208 (pending, JobSender::new(sender), receiver)
209 }
210
211 /// Run every job queued so far.
212 fn drain(receiver: &async_channel::Receiver<Job>, world: &mut World) {
213 while let Ok(job) = receiver.try_recv() {
214 job(world);
215 }
216 }
217
218 /// What one advance of `delta_seconds` settled.
219 fn settled(pending: &mut Pending<u32, u32>, delta_seconds: f32) -> Vec<(u32, Settled<u32>)> {
220 let mut settled = Vec::new();
221 pending.advance(delta_seconds, &mut settled);
222 settled
223 }
224
225 /// An answer reaches the caller under the key it was asked with, however
226 /// many were asked at once.
227 #[test]
228 fn an_answer_comes_back_under_its_key() {
229 let (mut pending, jobs, receiver) = asking(WorldState::Running, None);
230 let mut world = World::new();
231 for key in 0..4u32 {
232 pending
233 .request(&jobs, key, move |_: &mut World| key * 10)
234 .expect("the queue has room");
235 }
236 drain(&receiver, &mut world);
237
238 assert_eq!(
239 settled(&mut pending, 1.0),
240 vec![
241 (0, Settled::Answered(0)),
242 (1, Settled::Answered(10)),
243 (2, Settled::Answered(20)),
244 (3, Settled::Answered(30)),
245 ]
246 );
247 assert!(
248 settled(&mut pending, 1.0).is_empty(),
249 "an answer is handed out once"
250 );
251 }
252
253 /// A request whose job never ran is given up on once its time runs out,
254 /// and not before.
255 #[test]
256 fn a_request_is_given_up_on_after_its_time_runs_out() {
257 let (mut pending, jobs, _receiver) = asking(WorldState::Running, Some(2.0));
258 pending
259 .request(&jobs, 1, |_: &mut World| 0)
260 .expect("the queue has room");
261
262 assert!(
263 settled(&mut pending, 1.0).is_empty(),
264 "one tick of two is spent"
265 );
266 assert_eq!(settled(&mut pending, 1.0), vec![(1, Settled::Expired)]);
267 }
268
269 /// A world that ended owes nothing: every request still outstanding is
270 /// given up on at once, without waiting for its time to run out.
271 #[test]
272 fn an_ended_world_answers_nothing() {
273 let (mut pending, jobs, receiver) = asking(WorldState::Stopped, Some(100.0));
274 pending
275 .request(&jobs, 1, |_: &mut World| 0)
276 .expect("the queue has room");
277
278 assert_eq!(
279 settled(&mut pending, 1.0),
280 vec![(1, Settled::Expired)],
281 "the job sat in the queue of a world that never ran it"
282 );
283 assert_eq!(receiver.len(), 1, "the job was still sent");
284 }
285
286 /// An answer the world published before it ended is still delivered: the
287 /// world owed it and paid, and ending afterwards takes nothing back.
288 #[test]
289 fn an_answer_published_before_the_world_ended_is_delivered() {
290 let (mut pending, jobs, receiver) = asking(WorldState::Stopped, Some(100.0));
291 let mut world = World::new();
292 pending
293 .request(&jobs, 1, |_: &mut World| 7)
294 .expect("the queue has room");
295 drain(&receiver, &mut world);
296
297 assert_eq!(settled(&mut pending, 1.0), vec![(1, Settled::Answered(7))]);
298 }
299
300 /// An answer that arrived for a request already given up on is dropped
301 /// rather than delivered late.
302 #[test]
303 fn an_answer_for_a_request_given_up_on_is_dropped() {
304 let (mut pending, jobs, receiver) = asking(WorldState::Running, Some(1.0));
305 let mut world = World::new();
306 pending
307 .request(&jobs, 1, |_: &mut World| 7)
308 .expect("the queue has room");
309
310 assert_eq!(settled(&mut pending, 1.0), vec![(1, Settled::Expired)]);
311
312 // The job runs on a drain after the wait is over, which is what a
313 // world that was a tick behind looks like from here.
314 drain(&receiver, &mut world);
315 assert!(
316 settled(&mut pending, 1.0).is_empty(),
317 "the late answer was dropped"
318 );
319 }
320
321 /// A request refused by the world's queue was never asked: it waits for
322 /// nothing and expires to nobody.
323 #[test]
324 fn a_refused_request_waits_for_nothing() {
325 let (mut pending, jobs, receiver) = asking(WorldState::Running, Some(1.0));
326 drop(receiver);
327 assert_eq!(
328 pending.request(&jobs, 1, |_: &mut World| 0).err(),
329 Some(SendError::Closed)
330 );
331
332 assert!(
333 settled(&mut pending, 1.0).is_empty(),
334 "nothing was ever asked"
335 );
336 }
337
338 /// Asking again under a key already outstanding does not lengthen its
339 /// wait: the tick the first was asked on is the one that counts.
340 #[test]
341 fn asking_again_keeps_the_first_wait() {
342 let (mut pending, jobs, _receiver) = asking(WorldState::Running, Some(2.0));
343 pending
344 .request(&jobs, 1, |_: &mut World| 0)
345 .expect("the queue has room");
346 settled(&mut pending, 1.0);
347 pending
348 .request(&jobs, 1, |_: &mut World| 0)
349 .expect("the queue has room");
350
351 // One tick stood between the first ask and this one, so the wait the
352 // first ask was given runs out here however recently it was repeated.
353 assert_eq!(settled(&mut pending, 1.0), vec![(1, Settled::Expired)]);
354 }
355
356 /// A request with no timeout waits as long as the world owes it.
357 #[test]
358 fn a_request_with_no_timeout_waits_until_the_world_ends() {
359 let (mut pending, jobs, _receiver) = asking(WorldState::Running, None);
360 pending
361 .request(&jobs, 1, |_: &mut World| 0)
362 .expect("the queue has room");
363
364 for _ in 0..10 {
365 assert!(settled(&mut pending, 1.0).is_empty(), "the wait has no end");
366 }
367 }
368}