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}