Skip to main content

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}