Skip to main content

majordomus_cli/capability/
readings.rs

1//! The latest reading of a source, handed over at once.
2//!
3//! [`CapabilityExecutor::execute`](super::executor::CapabilityExecutor::execute) answers a
4//! question: it runs the handler, or returns an answer byte-identical to what the handler
5//! would return (`project.cache-is-invisible`). A page a person is looking at asks for
6//! something else. It wants what is known *now*, at once, with how old that is, and it
7//! wants the answer to catch up behind it. That is a [`read`], and it never passes itself
8//! off as an answer: a [`Latest`] always carries when it was read, whether the repository
9//! has moved since, and whether a newer reading is on its way.
10//!
11//! # What a read costs
12//!
13//! One lookup in this process's memory. The handler, git and the file system are touched
14//! only by a worker of this module, off the path of the request:
15//!
16//! - **The source was read in the repository as it is now.** The reading is returned
17//!   immediately. When it is older than [`REVALIDATE`], a worker reads it again behind the
18//!   caller, so a source the repository does not determine (health, the mesh, the forge)
19//!   stays close to the present without any reader waiting for it.
20//! - **The repository has moved since.** A worker reads the source again and the caller
21//!   waits for it for at most [`Patience::moved`]. A fast source is therefore current on the
22//!   same request; a slow one is returned as it was, marked [`Freshness::Stale`] and
23//!   `refreshing`. Every other source this process has been asked for lately is read again
24//!   at the same moment, so the next screen a reader opens is ready before it is asked for.
25//! - **The source has never been read here.** There is nothing to hand over, so the caller
26//!   waits for the first reading, for at most [`Patience::cold`], and is told
27//!   [`Freshness::Pending`] past that.
28//!
29//! One worker reads a source at a time, however many callers ask. The readings to take wait
30//! in one queue, drained by at most [`WORKERS`] threads, so a change of the repository that
31//! sends every remembered source to be read again starts no more threads than that. A
32//! reading a caller is waiting for goes to the front of the queue and the ones nobody is
33//! waiting for go to the back, so a reader never waits behind the background. Under the
34//! workers the executor's own cache and single flight still apply: a worker whose source
35//! did not change is answered from the cache and costs nearly nothing.
36//!
37//! # Switching it off
38//!
39//! `MAJORDOMUS_CAPABILITY_CACHE=off` makes every read run its handler and wait for it, as
40//! it makes every `execute` do. [`PATIENCE_SWITCH`] set to `wait` keeps the store and makes
41//! every read wait for a current reading, which is what a test that changes a repository
42//! and then reads a page needs in order to be exact.
43//!
44//! # A reading, from first to known
45//!
46//! ```
47//! use majordomus_cli::capability::readings::{read, Freshness, Patience};
48//! use majordomus_cli::synthetic::SyntheticRepository;
49//! use serde_json::json;
50//!
51//! let repo = SyntheticRepository::small().unwrap();
52//! let ctx = repo.context().unwrap();
53//!
54//! // never read here: the caller waits for the first reading, and it is current
55//! let first = read(&ctx, "plan.status", json!({}), Patience::WAIT);
56//! assert_eq!(first.freshness, Freshness::Current);
57//! assert!(first.value.is_ok() && first.read_at.is_some());
58//!
59//! // known now: handed over from memory, with the age it has, and no handler runs for it
60//! let ran = ctx.executor.handler_invocations();
61//! let again = read(&ctx, "plan.status", json!({}), Patience::PAGE);
62//! assert_eq!(again.value, first.value);
63//! assert!(again.age.is_some() && !again.catching_up());
64//! assert_eq!(ctx.executor.handler_invocations(), ran);
65//!
66//! // a source that cannot answer is a reading that says why, never a missing one
67//! let unknown = read(&ctx, "no.such.capability", json!({}), Patience::WAIT);
68//! assert!(unknown.value.is_err());
69//! assert_eq!(unknown.freshness, Freshness::Current);
70//! ```
71
72use std::collections::{HashMap, VecDeque};
73use std::sync::{Arc, Condvar, Mutex, MutexGuard};
74use std::time::{Duration, Instant, SystemTime};
75
76use serde_json::Value;
77
78use super::executor::{bypassed, canonical_json};
79use super::handler::Context;
80
81/// How old a reading of the repository as it is now may grow before a worker reads its
82/// source again behind the caller.
83pub const REVALIDATE: Duration = Duration::from_secs(1);
84
85/// How many sources are read at once by this module's workers, all sources together.
86pub const WORKERS: usize = 4;
87
88/// How long a source nobody asked for is remembered, and so read again when the repository
89/// moves. A page left open asks for its sources every minute, so its sources stay; a screen
90/// nobody has looked at for five minutes stops costing anything when the repository changes.
91pub const REMEMBERED: Duration = Duration::from_secs(300);
92
93/// The most sources remembered at once; the one asked for longest ago is dropped first.
94pub const MAX_SOURCES: usize = 512;
95
96/// The environment variable that sets how long a read waits: a number of milliseconds for
97/// a source whose repository has moved, or `wait` to wait for a current reading every time.
98pub const PATIENCE_SWITCH: &str = "MAJORDOMUS_READING_PATIENCE_MS";
99
100/// How long a read waits for a worker before it returns what it has.
101///
102/// ```
103/// use majordomus_cli::capability::readings::Patience;
104/// use std::time::Duration;
105///
106/// // a page waits a minute for a first reading and 50 ms for a moved repository
107/// assert_eq!(Patience::PAGE.cold, Duration::from_secs(60));
108/// assert_eq!(Patience::PAGE.moved, Duration::from_millis(50));
109/// // an exact read waits for as long as it takes
110/// assert_eq!(Patience::WAIT.moved, Duration::MAX);
111/// let brief = Patience { cold: Duration::from_millis(5), moved: Duration::ZERO };
112/// assert!(brief.moved < Patience::PAGE.moved);
113/// ```
114#[derive(Debug, Clone, Copy, PartialEq, Eq)]
115pub struct Patience {
116    /// For a source that has never been read in this process.
117    pub cold: Duration,
118    /// For a source read before the repository last moved.
119    pub moved: Duration,
120}
121
122impl Patience {
123    /// Wait for a current reading, however long it takes.
124    pub const WAIT: Patience = Patience {
125        cold: Duration::MAX,
126        moved: Duration::MAX,
127    };
128
129    /// What a page is served with: a first reading is waited for up to a minute, because
130    /// there is nothing else to show, and a reading the repository has moved past for 50
131    /// ms, half of the 100 ms a warm page is allowed (the owner's UI-performance pack).
132    pub const PAGE: Patience = Patience {
133        cold: Duration::from_secs(60),
134        moved: Duration::from_millis(50),
135    };
136
137    /// The patience of this process: [`PATIENCE_SWITCH`] when it is set, and otherwise
138    /// [`Patience::PAGE`]. The crate's own unit tests wait, so that a test which renders a
139    /// screen asserts on a current reading and never on a timing.
140    ///
141    /// ```
142    /// use majordomus_cli::capability::readings::{Patience, PATIENCE_SWITCH};
143    ///
144    /// // what the switch says when it is set and means something, a page's patience otherwise
145    /// let set = std::env::var(PATIENCE_SWITCH).ok();
146    /// let expected = Patience::parse(set.as_deref()).unwrap_or(Patience::PAGE);
147    /// assert_eq!(Patience::of_process(), expected);
148    /// ```
149    pub fn of_process() -> Patience {
150        let set = std::env::var(PATIENCE_SWITCH).ok();
151        match Patience::parse(set.as_deref()) {
152            Some(patience) => patience,
153            None if cfg!(test) => Patience::WAIT,
154            None => Patience::PAGE,
155        }
156    }
157
158    /// What a value of [`PATIENCE_SWITCH`] means, or `None` when it means nothing: `wait`
159    /// is [`Patience::WAIT`], and a number is that many milliseconds for a repository that
160    /// has moved, with a first reading waited for no less than a page waits.
161    ///
162    /// ```
163    /// use majordomus_cli::capability::readings::Patience;
164    /// assert_eq!(Patience::parse(Some("wait")), Some(Patience::WAIT));
165    /// let twenty = Patience::parse(Some(" 20 ")).unwrap();
166    /// assert_eq!(twenty.moved.as_millis(), 20);
167    /// assert_eq!(twenty.cold, Patience::PAGE.cold, "a first reading is still waited for");
168    /// assert_eq!(Patience::parse(Some("soon")), None, "not a number and not `wait`");
169    /// assert_eq!(Patience::parse(None), None);
170    /// ```
171    pub fn parse(value: Option<&str>) -> Option<Patience> {
172        let value = value?.trim();
173        if value == "wait" {
174            return Some(Patience::WAIT);
175        }
176        let millis: u64 = value.parse().ok()?;
177        Some(Patience {
178            cold: Patience::PAGE.cold.max(Duration::from_millis(millis)),
179            moved: Duration::from_millis(millis),
180        })
181    }
182}
183
184/// How a reading stands against the repository as it is now.
185///
186/// ```
187/// use majordomus_cli::capability::readings::{read, Freshness, Patience};
188/// use majordomus_cli::synthetic::SyntheticRepository;
189/// use std::time::Duration;
190///
191/// let repo = SyntheticRepository::small().unwrap();
192/// let ctx = repo.context().unwrap();
193/// // a caller with no patience at all is told the first reading is pending, or handed it
194/// // when the worker was quicker than the caller: never anything in between
195/// let none = Patience { cold: Duration::ZERO, moved: Duration::ZERO };
196/// let first = read(&ctx, "plan.status", serde_json::json!({}), none);
197/// assert!(matches!(first.freshness, Freshness::Pending | Freshness::Current));
198/// // and a caller that waits is handed a current one; nothing here moved the repository,
199/// // so nothing is `Freshness::Stale`
200/// let waited = read(&ctx, "plan.status", serde_json::json!({}), Patience::WAIT);
201/// assert_eq!(waited.freshness, Freshness::Current);
202/// assert_ne!(waited.freshness, Freshness::Stale);
203/// ```
204#[derive(Debug, Clone, Copy, PartialEq, Eq)]
205pub enum Freshness {
206    /// Read in the repository as it is now.
207    Current,
208    /// Read before the repository last moved.
209    Stale,
210    /// Never read in this process; the first reading has not arrived.
211    Pending,
212}
213
214/// The latest reading of one source.
215///
216/// ```
217/// use majordomus_cli::capability::readings::{read, Freshness, Latest, Patience};
218/// use majordomus_cli::synthetic::SyntheticRepository;
219///
220/// let repo = SyntheticRepository::small().unwrap();
221/// let ctx = repo.context().unwrap();
222/// let latest: Latest = read(&ctx, "plan.status", serde_json::json!({}), Patience::WAIT);
223/// // what the source answered, how long that took, when it was read and how it stands
224/// assert!(latest.value.is_ok());
225/// assert!(latest.read_at.is_some() && latest.age.is_some());
226/// assert_eq!(latest.freshness, Freshness::Current);
227/// assert!(latest.took >= std::time::Duration::ZERO);
228/// ```
229#[derive(Debug, Clone)]
230pub struct Latest<V = Value> {
231    /// What the source answered, or why it could not. `Err` for a pending reading too.
232    pub value: Result<V, String>,
233    /// How long the executor took to produce this reading.
234    pub took: Duration,
235    /// When it was read; `None` while pending.
236    pub read_at: Option<SystemTime>,
237    /// How long ago that was; `None` while pending.
238    pub age: Option<Duration>,
239    /// How it stands against the repository as it is now.
240    pub freshness: Freshness,
241    /// Whether a worker is reading the source again at this moment.
242    pub refreshing: bool,
243}
244
245impl<V> Latest<V> {
246    /// A reading taken just now, by the caller itself.
247    fn now(value: Result<V, String>, took: Duration) -> Latest<V> {
248        Latest {
249            value,
250            took,
251            read_at: Some(SystemTime::now()),
252            age: Some(Duration::ZERO),
253            freshness: Freshness::Current,
254            refreshing: false,
255        }
256    }
257
258    /// Whether a reader should be told that what they see is about to be replaced: the
259    /// reading is not of the repository as it is now.
260    ///
261    /// ```
262    /// use majordomus_cli::capability::readings::{read, Freshness, Patience};
263    /// use majordomus_cli::synthetic::SyntheticRepository;
264    ///
265    /// let repo = SyntheticRepository::small().unwrap();
266    /// let ctx = repo.context().unwrap();
267    /// let mut latest = read(&ctx, "plan.status", serde_json::json!({}), Patience::WAIT);
268    /// assert!(!latest.catching_up(), "a current reading is not about to be replaced");
269    /// latest.freshness = Freshness::Stale;
270    /// assert!(latest.catching_up());
271    /// latest.freshness = Freshness::Pending;
272    /// assert!(latest.catching_up());
273    /// ```
274    pub fn catching_up(&self) -> bool {
275        self.freshness != Freshness::Current
276    }
277}
278
279/// What the first reading of a source says while it has not arrived.
280pub const PENDING: &str = "the first reading of this source has not arrived yet";
281
282/// The state of the repository a reading was taken in: the context's reading of it, the
283/// registry built from it, and how many times this process has written to it.
284#[derive(Debug, Clone, PartialEq, Eq)]
285struct World {
286    reading: u64,
287    fingerprint: String,
288    writes: u64,
289}
290
291impl World {
292    fn of(ctx: &Context) -> World {
293        World {
294            reading: ctx.reading,
295            fingerprint: ctx.registry.fingerprint().to_string(),
296            writes: ctx.executor.writes(),
297        }
298    }
299}
300
301#[derive(Debug)]
302struct Outcome {
303    /// The answer as the executor's cache shares it: reading a source again whose answer is
304    /// cached copies nothing, and a caller copies it only after the slot's lock is released.
305    value: Result<Arc<Value>, String>,
306    world: World,
307    at: Instant,
308    wall: SystemTime,
309    took: Duration,
310}
311
312#[derive(Debug)]
313struct State {
314    input: Value,
315    outcome: Option<Outcome>,
316    flying: bool,
317    asked: Instant,
318}
319
320#[derive(Debug)]
321struct Slot {
322    state: Mutex<State>,
323    done: Condvar,
324}
325
326type Key = (String, String);
327
328/// One reading to take: the source, its slot, and the repository it is to be taken in.
329struct Job {
330    ctx: Context,
331    id: String,
332    landing: Landing,
333}
334
335impl std::fmt::Debug for Job {
336    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
337        f.debug_struct("Job")
338            .field("id", &self.id)
339            .finish_non_exhaustive()
340    }
341}
342
343/// The readings waiting to be taken, and how many workers are taking them.
344#[derive(Debug, Default)]
345struct Queue {
346    jobs: VecDeque<Job>,
347    workers: usize,
348}
349
350/// The latest readings of one executor.
351#[derive(Debug, Default)]
352pub(crate) struct Readings {
353    slots: Mutex<HashMap<Key, Arc<Slot>>>,
354    world: Mutex<Option<World>>,
355    queue: Mutex<Queue>,
356}
357
358fn lock<T>(m: &Mutex<T>) -> MutexGuard<'_, T> {
359    m.lock().unwrap_or_else(|e| e.into_inner())
360}
361
362impl Readings {
363    /// How many sources are remembered.
364    pub(crate) fn remembered(&self) -> usize {
365        lock(&self.slots).len()
366    }
367
368    /// The slot of a source, made when it is first asked for. Sources nobody has asked for
369    /// in [`REMEMBERED`] are forgotten here, and the oldest past [`MAX_SOURCES`].
370    fn slot(&self, id: &str, input: &Value) -> Arc<Slot> {
371        let key = (id.to_string(), canonical_json(input));
372        let mut slots = lock(&self.slots);
373        if let Some(slot) = slots.get(&key) {
374            return Arc::clone(slot);
375        }
376        slots.retain(|_, slot| {
377            let state = lock(&slot.state);
378            state.flying || state.asked.elapsed() < REMEMBERED
379        });
380        while slots.len() >= MAX_SOURCES {
381            let oldest = slots
382                .iter()
383                .filter(|(_, slot)| !lock(&slot.state).flying)
384                .min_by_key(|(_, slot)| lock(&slot.state).asked)
385                .map(|(key, _)| key.clone());
386            match oldest {
387                Some(key) => slots.remove(&key),
388                None => break,
389            };
390        }
391        let slot = Arc::new(Slot {
392            state: Mutex::new(State {
393                input: input.clone(),
394                outcome: None,
395                flying: false,
396                asked: Instant::now(),
397            }),
398            done: Condvar::new(),
399        });
400        slots.insert(key, Arc::clone(&slot));
401        slot
402    }
403
404    /// Note the world a caller is in. `true` when it is not the one last noted: the
405    /// repository has moved, and everything remembered is to be read again.
406    fn moved_to(&self, world: &World) -> bool {
407        let mut noted = lock(&self.world);
408        if noted.as_ref() == Some(world) {
409            return false;
410        }
411        let first = noted.is_none();
412        *noted = Some(world.clone());
413        !first
414    }
415
416    /// Every remembered source that is not being read and was not read in `world`.
417    fn behind(&self, world: &World) -> Vec<(String, Arc<Slot>)> {
418        lock(&self.slots)
419            .iter()
420            .filter(|(_, slot)| {
421                let state = lock(&slot.state);
422                !state.flying
423                    && state.asked.elapsed() < REMEMBERED
424                    && state.outcome.as_ref().is_some_and(|o| &o.world != world)
425            })
426            .map(|((id, _), slot)| (id.clone(), Arc::clone(slot)))
427            .collect()
428    }
429}
430
431/// The latest reading of `id` asked with `input`: what this process knows now, returned
432/// within `patience`, with a worker reading the source again behind the caller whenever
433/// what is known is not current. See the module for what each case costs.
434///
435/// ```
436/// use majordomus_cli::capability::readings::{read, Patience};
437/// use majordomus_cli::synthetic::SyntheticRepository;
438/// use serde_json::json;
439///
440/// let repo = SyntheticRepository::small().unwrap();
441/// let ctx = repo.context().unwrap();
442/// let reading = read(&ctx, "plan.status", json!({}), Patience::WAIT);
443/// // a reading holds what `execute` answers: it is the same source, asked the same way
444/// assert_eq!(reading.value.ok(), ctx.execute("plan.status", json!({})).ok());
445/// // one source per input: asked again it is remembered, not read a second time
446/// read(&ctx, "plan.status", json!({}), Patience::PAGE);
447/// assert_eq!(ctx.executor.readings_remembered(), 1);
448/// ```
449pub fn read(ctx: &Context, id: &str, input: Value, patience: Patience) -> Latest {
450    let latest = read_shared(ctx, id, input, patience);
451    Latest {
452        value: latest.value.map(Arc::unwrap_or_clone),
453        took: latest.took,
454        read_at: latest.read_at,
455        age: latest.age,
456        freshness: latest.freshness,
457        refreshing: latest.refreshing,
458    }
459}
460
461/// The same reading, with the answer as the slot shares it rather than a copy of it: what a
462/// screen that only reads the answer asks, so that a large one is copied for nobody.
463pub(crate) fn read_shared(
464    ctx: &Context,
465    id: &str,
466    input: Value,
467    patience: Patience,
468) -> Latest<Arc<Value>> {
469    if bypassed() {
470        let started = Instant::now();
471        let value = ctx.execute(id, input).map_err(|e| e.to_string());
472        return Latest::now(value.map(Arc::new), started.elapsed());
473    }
474    let readings = &ctx.executor.readings;
475    let world = World::of(ctx);
476    let moved = readings.moved_to(&world);
477    let slot = readings.slot(id, &input);
478
479    let wait = {
480        let mut state = lock(&slot.state);
481        state.asked = Instant::now();
482        // the executor may already hold the answer for this state of the repository: a source
483        // whose cache is keyed by what did not change, or one another caller just asked. Then
484        // it is current at once, and no worker is needed for it.
485        let behind = state.outcome.as_ref().is_none_or(|o| o.world != world);
486        if behind {
487            if let Some(value) = ctx.executor.cached_shared(ctx, id, &state.input) {
488                state.outcome = Some(Outcome {
489                    value: Ok(value),
490                    world: world.clone(),
491                    at: Instant::now(),
492                    wall: SystemTime::now(),
493                    took: Duration::ZERO,
494                });
495            }
496        }
497        let (due, wait) = match &state.outcome {
498            None => (true, patience.cold),
499            Some(o) if o.world != world => (true, patience.moved),
500            Some(o) => (o.at.elapsed() > REVALIDATE, Duration::ZERO),
501        };
502        // a caller that will wait for the reading goes first; one handed the reading it has
503        // does not, and neither does the background
504        let urgent = !wait.is_zero();
505        if due && !state.flying {
506            state.flying = true;
507            drop(state);
508            work(ctx, id, Arc::clone(&slot), world.clone(), urgent);
509        } else if due && urgent {
510            // already queued, behind readings nobody waits for: this caller moves it forward
511            drop(state);
512            hurry(readings, &slot);
513        }
514        wait
515    };
516    if moved {
517        for (other, behind) in readings.behind(&world) {
518            let mut state = lock(&behind.state);
519            if state.flying {
520                continue;
521            }
522            state.flying = true;
523            drop(state);
524            work(ctx, &other, behind, world.clone(), false);
525        }
526    }
527
528    let deadline = Instant::now().checked_add(wait);
529    let mut state = lock(&slot.state);
530    loop {
531        let current = state.outcome.as_ref().is_some_and(|o| o.world == world);
532        if current || !state.flying || wait.is_zero() {
533            break;
534        }
535        state = match deadline {
536            // a wait too long to be a moment in time is a wait without an end
537            None => slot.done.wait(state).unwrap_or_else(|e| e.into_inner()),
538            Some(deadline) => {
539                let left = deadline.saturating_duration_since(Instant::now());
540                if left.is_zero() {
541                    break;
542                }
543                slot.done
544                    .wait_timeout(state, left)
545                    .unwrap_or_else(|e| e.into_inner())
546                    .0
547            }
548        };
549    }
550    let refreshing = state.flying;
551    let (value, took, read_at, age, freshness) = match &state.outcome {
552        None => (
553            Err(PENDING.to_string()),
554            Duration::ZERO,
555            None,
556            None,
557            Freshness::Pending,
558        ),
559        Some(o) => (
560            o.value.clone(),
561            o.took,
562            Some(o.wall),
563            Some(o.at.elapsed()),
564            if o.world == world {
565                Freshness::Current
566            } else {
567                Freshness::Stale
568            },
569        ),
570    };
571    // a caller's own copy, when it wants one, is made once the slot is free again, so that a
572    // large answer copied for one page holds up neither the worker landing a reading nor
573    // another page
574    drop(state);
575    Latest {
576        value,
577        took,
578        read_at,
579        age,
580        freshness,
581        refreshing,
582    }
583}
584
585/// Queue a reading of the source of `slot`, to be filed under `world`, and make sure a
586/// worker will take it. The slot is already marked as being read; whatever happens to the
587/// job, the mark is cleared ([`Landing`]), so a handler that panics leaves a reading that
588/// says so and never a source nobody reads.
589fn work(ctx: &Context, id: &str, slot: Arc<Slot>, world: World, urgent: bool) {
590    let job = Job {
591        ctx: ctx.clone(),
592        id: id.to_string(),
593        landing: Landing {
594            slot,
595            world,
596            landed: false,
597        },
598    };
599    let readings = &ctx.executor.readings;
600    let start = {
601        let mut queue = lock(&readings.queue);
602        if urgent {
603            queue.jobs.push_front(job);
604        } else {
605            queue.jobs.push_back(job);
606        }
607        let start = queue.workers < WORKERS;
608        if start {
609            queue.workers += 1;
610        }
611        start
612    };
613    if !start {
614        return;
615    }
616    let executor = Arc::clone(&ctx.executor);
617    let drain = move || {
618        let readings = &executor.readings;
619        loop {
620            let job = {
621                let mut queue = lock(&readings.queue);
622                match queue.jobs.pop_front() {
623                    Some(job) => job,
624                    None => {
625                        queue.workers -= 1;
626                        return;
627                    }
628                }
629            };
630            take(job);
631        }
632    };
633    // a process that cannot start a thread takes the reading on the caller's, which is what
634    // it did before this module existed
635    if let Err(refused) = std::thread::Builder::new()
636        .name("mj-reading".into())
637        .spawn(drain)
638    {
639        tracing::warn!("no thread for a reading of a source: {refused}");
640        let job = {
641            let mut queue = lock(&readings.queue);
642            queue.workers -= 1;
643            queue.jobs.pop_front()
644        };
645        if let Some(job) = job {
646            take(job);
647        }
648    }
649}
650
651/// Move the queued reading of `slot` to the front of the queue: a caller is now waiting for
652/// it. A reading a worker has already taken is no longer queued, and is left as it is.
653fn hurry(readings: &Readings, slot: &Arc<Slot>) {
654    let mut queue = lock(&readings.queue);
655    if let Some(at) = queue
656        .jobs
657        .iter()
658        .position(|job| Arc::ptr_eq(&job.landing.slot, slot))
659    {
660        if let Some(job) = queue.jobs.remove(at) {
661            queue.jobs.push_front(job);
662        }
663    }
664}
665
666/// Take one reading: ask the executor, and land what it answered in the slot. A panic of
667/// the handler is caught here, so the worker goes on to the next job.
668fn take(job: Job) {
669    let Job {
670        ctx,
671        id,
672        mut landing,
673    } = job;
674    let input = lock(&landing.slot.state).input.clone();
675    let started = Instant::now();
676    let asked = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
677        ctx.executor
678            .execute_shared(&ctx, &id, input)
679            .map_err(|e| e.to_string())
680    }));
681    let value = asked.unwrap_or_else(|_| Err("the source panicked while answering".to_string()));
682    landing.land(value, started.elapsed());
683}
684
685/// What a worker owes its slot: an outcome, and the mark cleared.
686struct Landing {
687    slot: Arc<Slot>,
688    world: World,
689    landed: bool,
690}
691
692impl Landing {
693    fn land(&mut self, value: Result<Arc<Value>, String>, took: Duration) {
694        let mut state = lock(&self.slot.state);
695        state.outcome = Some(Outcome {
696            value,
697            world: self.world.clone(),
698            at: Instant::now(),
699            wall: SystemTime::now(),
700            took,
701        });
702        state.flying = false;
703        self.landed = true;
704        drop(state);
705        self.slot.done.notify_all();
706    }
707}
708
709impl Drop for Landing {
710    fn drop(&mut self) {
711        if !self.landed {
712            self.land(
713                Err("the source panicked while answering".into()),
714                Duration::ZERO,
715            );
716        }
717    }
718}