Skip to main content

haematite_cli/serve/
embedding.rs

1//! EW1: the embedding driver.
2//!
3//! One thread reads pending pages through held immutable snapshots and has
4//! the worker embed their texts, then admits one durable batch commit.
5//! Foreground admission excludes later embedding commits while a write waits.
6//! At most one already-admitted batch, sized by `embed_page_records`, finishes
7//! one full flush before that writer; model, diff and text reads hold no writer.
8//!
9//! What waits is a deduplicated queue of fields, woken by a write that
10//! touches a declared collection and by a `vector.declare`, and filled at
11//! start with every field declared on every live branch. A page that did not
12//! end its walk puts its field at the back. Failed pages also go to the back,
13//! with a per-field delay that doubles to the restart ceiling. Only a page
14//! commit clears that count; a successful worker probe does not.
15//!
16//! The worker is started by the thread, off the serving path. EWR1: a worker
17//! that died or timed out is started again by the thread, at once the first
18//! time, then after a wait that doubles up to a ceiling (`Restart`), for as
19//! long as the service runs; the field it was making goes back to wait, and
20//! its walk carries on from its last commit. The wait is on the driver's own
21//! signal, never with the storage lock held. Until a worker is ready a text
22//! search waits for one up to `embed_ready_ms`, then is refused as
23//! `embed_worker_starting` or `embed_worker_restarting`. Shutdown tells the
24//! thread and joins it: a page under way ends its worker call, within the
25//! call bound, and its commit.
26
27use std::collections::{BTreeMap, BTreeSet, VecDeque};
28use std::num::NonZeroUsize;
29use std::sync::{Arc, Condvar, Mutex, MutexGuard, PoisonError};
30use std::thread::JoinHandle;
31use std::time::{Duration, Instant};
32
33#[path = "embedding_admission.rs"]
34mod admission;
35pub(in crate::serve) use admission::{Admission, Guard};
36
37use haematite::SyncCounts;
38use haematite::branch::{Timestamp, current_timestamp};
39use serde::Serialize;
40use serde_json::{Value, json};
41
42use super::embed::job::{self, Bounds, Embed};
43use super::embed::question::Ask;
44use super::embed::settings::Restart;
45use super::embed::{Embedded, Embedder, Settings};
46use super::protocol::{Selection, Version};
47use super::storage::embed_job::Reach;
48use super::storage::embed_pending::PendingReader;
49use super::storage::embedded::Position;
50use super::storage::{Failure, Storage};
51
52/// What the driver needs of a worker: documents for the walk, a question
53/// for a search.
54pub trait Worker: Embed + Ask + Send + Sync {}
55
56impl<W: Embed + Ask + Send + Sync> Worker for W {}
57
58/// One page reads `embed_page_records` records, has them embedded in worker
59/// calls of `embed_request_items` texts, and commits once (EW1-fix).
60#[must_use]
61pub fn bounds(settings: &Settings) -> Bounds {
62    let input_bytes = usize::try_from(settings.limits.input_bytes)
63        .ok()
64        .and_then(NonZeroUsize::new)
65        .unwrap_or(NonZeroUsize::MIN);
66    Bounds {
67        page_records: settings.page_records,
68        batch: settings.request_items,
69        input_bytes,
70        call: settings.document_call(),
71    }
72}
73
74/// HANG1: the most stopping the driver waits for its threads: a page under
75/// way (each of its worker calls, `embed_call_ms` and with windows on that
76/// for each window a text may need, ⌈`page_records` / batch⌉ of them), its
77/// commit's wait for a writer (`embed_writer_wait_ms`), and
78/// the question worker's start (`embed_ready_ms`). `None` past the clock's
79/// range, when stopping waits for nothing.
80fn stop_within(bounds: Bounds, writer_wait: Duration, restart: Restart) -> Option<Duration> {
81    let calls = bounds.page_records.get().div_ceil(bounds.batch.get());
82    bounds
83        .call
84        .checked_mul(u32::try_from(calls).ok()?)?
85        .checked_add(writer_wait)?
86        .checked_add(restart.ready)
87}
88
89/// One declared field of one branch.
90pub type Field = (String, String, String);
91
92/// The worker's state, as the status names it.
93#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize)]
94#[serde(rename_all = "snake_case")]
95pub enum WorkerState {
96    EmbedWorkerStarting,
97    Ready,
98    /// It died or timed out, and is to be started again (EWR1).
99    Restarting,
100}
101
102/// A failure kept for the status, with the field and the time.
103#[derive(Debug, Clone, Serialize)]
104pub struct Failed {
105    pub branch: String,
106    pub collection: String,
107    pub field: String,
108    pub at: Timestamp,
109    pub failure: Failure,
110}
111
112/// Where the walk's thread is.
113#[derive(Debug, Default, Clone, Copy, PartialEq, Eq)]
114enum Phase {
115    /// Waiting for a field, or for the worker's start.
116    #[default]
117    Waiting,
118    /// Making a page of a field it took.
119    Stepping,
120    /// STOP2: a page's commit has started and not yet been answered.
121    Committing,
122    /// HANG1: the thread has left `run` and says nothing more.
123    Finished,
124}
125
126impl Phase {
127    /// A page is under way: being made, or committed.
128    const fn stepping(self) -> bool {
129        matches!(self, Self::Stepping | Self::Committing)
130    }
131}
132
133#[derive(Debug, Default)]
134struct Waiting {
135    order: VecDeque<Field>,
136    names: BTreeSet<Field>,
137}
138
139impl Waiting {
140    fn insert(&mut self, field: Field) -> bool {
141        if !self.names.insert(field.clone()) {
142            return false;
143        }
144        self.order.push_back(field);
145        true
146    }
147
148    fn requeue(&mut self, field: Field) {
149        self.order.retain(|queued| queued != &field);
150        self.names.insert(field.clone());
151        self.order.push_back(field);
152    }
153
154    fn contains(&self, field: &Field) -> bool {
155        self.names.contains(field)
156    }
157
158    #[cfg(test)]
159    fn is_empty(&self) -> bool {
160        self.order.is_empty()
161    }
162
163    fn pop_ready(
164        &mut self,
165        failures: &BTreeMap<Field, PageFailure>,
166        now: Instant,
167    ) -> Option<Field> {
168        for _ in 0..self.order.len() {
169            let field = self.order.pop_front()?;
170            if failures
171                .get(&field)
172                .and_then(|failed| failed.due)
173                .is_some_and(|due| due > now)
174            {
175                self.order.push_back(field);
176            } else {
177                self.names.remove(&field);
178                return Some(field);
179            }
180        }
181        None
182    }
183}
184
185#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
186struct PagePosition {
187    committed: Option<Position>,
188    toward: Version,
189    end_after: Option<String>,
190}
191
192#[derive(Debug, Serialize)]
193struct PageFailure {
194    position: Option<PagePosition>,
195    consecutive_failures: u32,
196    failure: Failure,
197    at: Timestamp,
198    next_attempt_due: Option<Timestamp>,
199    #[serde(skip)]
200    due: Option<Instant>,
201}
202
203enum Scheduled {
204    Stop,
205    Start,
206    Page(Field),
207    Wait(Option<Duration>),
208}
209
210#[derive(Debug, Default)]
211struct State {
212    waiting: Waiting,
213    failures: BTreeMap<Field, PageFailure>,
214    /// Every field the driver knows is declared, for the wake on a write.
215    known: BTreeSet<Field>,
216    worker: Option<WorkerState>,
217    stopping: bool,
218    phase: Phase,
219    /// STOP2: `stop` gave up on the thread, which starts no commit after.
220    left: bool,
221    died: Option<String>,
222    pages: u64,
223    /// Vectors made, by field, since the service started.
224    made: Vec<(Field, u64)>,
225    /// EMB1: records given no vector, by field, since the service started,
226    /// because the model reads only part of their text.
227    longer_than_model: Vec<(Field, u64)>,
228    /// EMB-E1: records passed with no vector, by field, since the service
229    /// started, because their text is empty or only whitespace.
230    embed_input_empty: Vec<(Field, u64)>,
231    /// EMB-E2: records given windows in place of one vector, by field.
232    windowed: Vec<(Field, u64)>,
233    /// EMB-E2: records given no vector because their whole cover needs more
234    /// windows than the cap, by field.
235    windows_over_cap: Vec<(Field, u64)>,
236    /// Walks that reached their end, by field.
237    ended: BTreeSet<Field>,
238    last_failure: Option<Failed>,
239    durable: SyncCounts,
240    /// Times the worker was started since the service began (EWR1).
241    starts: u64,
242    /// Deaths since the worker last answered.
243    deaths: u32,
244    /// When the next start is due, by the clock and as a time.
245    next_start: Option<(Instant, Timestamp)>,
246    /// Each wait before a start, in order.
247    #[cfg(test)]
248    waits: Vec<Duration>,
249    /// Searches waiting for a worker now.
250    #[cfg(test)]
251    askers: usize,
252}
253
254impl State {
255    fn failed_page(
256        &mut self,
257        field: &Field,
258        position: Option<PagePosition>,
259        failure: Failure,
260        restart: Restart,
261        now: Instant,
262    ) -> Result<(), String> {
263        let consecutive = self
264            .failures
265            .get(field)
266            .map_or(1, |failed| failed.consecutive_failures.saturating_add(1));
267        let wait = restart.wait(consecutive.saturating_add(1));
268        let due = now
269            .checked_add(wait)
270            .ok_or("embed_page_retry_clock: retry deadline cannot be represented")?;
271        let nanos = u64::try_from(wait.as_nanos())
272            .map_err(|error| format!("embed_page_retry_clock: {error}"))?;
273        let at = current_timestamp();
274        self.failures.insert(
275            field.clone(),
276            PageFailure {
277                position,
278                consecutive_failures: consecutive,
279                failure,
280                at,
281                next_attempt_due: Some(at.saturating_add(nanos)),
282                due: Some(due),
283            },
284        );
285        self.waiting.requeue(field.clone());
286        Ok(())
287    }
288
289    fn committed_page(&mut self, field: &Field) {
290        if let Some(failed) = self.failures.get_mut(field) {
291            failed.consecutive_failures = 0;
292            failed.next_attempt_due = None;
293            failed.due = None;
294        }
295    }
296
297    fn next_page(&mut self, now: Instant) -> Option<Field> {
298        self.waiting.pop_ready(&self.failures, now)
299    }
300
301    fn scheduled(&mut self, now: Instant) -> Scheduled {
302        if self.stopping {
303            return Scheduled::Stop;
304        }
305        if self.worker == Some(WorkerState::Restarting) {
306            return match self
307                .next_start
308                .and_then(|(due, _at)| due.checked_duration_since(now))
309            {
310                Some(left) if !left.is_zero() => Scheduled::Wait(Some(left)),
311                _ => Scheduled::Start,
312            };
313        }
314        if let Some(field) = self.next_page(now) {
315            self.phase = Phase::Stepping;
316            return Scheduled::Page(field);
317        }
318        Scheduled::Wait(self.page_wait(now))
319    }
320
321    fn page_wait(&self, now: Instant) -> Option<Duration> {
322        self.waiting
323            .names
324            .iter()
325            .filter_map(|field| self.failures.get(field).and_then(|failed| failed.due))
326            .filter_map(|due| due.checked_duration_since(now))
327            .min()
328    }
329
330    /// The worker died or timed out: it is to be started again after the
331    /// wait its deaths in a row call for.
332    fn died(&mut self, restart: Restart, failed: Failed) {
333        self.died_at(restart, failed, Instant::now());
334    }
335
336    fn died_at(&mut self, restart: Restart, failed: Failed, now: Instant) {
337        self.worker = Some(WorkerState::Restarting);
338        self.deaths = self.deaths.saturating_add(1);
339        let wait = restart.wait(self.deaths);
340        let nanos = u64::try_from(wait.as_nanos()).unwrap_or(u64::MAX);
341        let due = now.checked_add(wait).unwrap_or(now);
342        self.next_start = Some((due, failed.at.saturating_add(nanos)));
343        self.last_failure = Some(failed);
344        #[cfg(test)]
345        self.waits.push(wait);
346    }
347}
348
349#[derive(Debug, Default)]
350struct Shared {
351    state: Mutex<State>,
352    changed: Condvar,
353}
354
355impl Shared {
356    fn lock(&self) -> MutexGuard<'_, State> {
357        self.state.lock().unwrap_or_else(PoisonError::into_inner)
358    }
359
360    fn wait<'a>(&self, state: MutexGuard<'a, State>) -> MutexGuard<'a, State> {
361        self.changed
362            .wait(state)
363            .unwrap_or_else(PoisonError::into_inner)
364    }
365
366    /// As [`Self::wait`], for at most `most`; the lock is let go meanwhile.
367    fn wait_for<'a>(&self, state: MutexGuard<'a, State>, most: Duration) -> MutexGuard<'a, State> {
368        self.changed.wait_timeout(state, most).map_or_else(
369            |poisoned| poisoned.into_inner().0,
370            |(state, _timeout)| state,
371        )
372    }
373}
374
375/// SL1: the worker that answers questions alone, apart from the walk's, so
376/// a search never waits behind a walk batch: one `Embedder` keeps one worker
377/// behind one lock for a whole batch (embedder.rs `embed`). SL2: it is
378/// started when the service starts, on a thread of its own, so the first
379/// search does not pay the model's load; a search that comes first waits for
380/// it up to `embed_ready_ms`. After a death the next question starts it
381/// again at once; a death after that is followed by the same doubling waits
382/// as the walk's worker (EWR1), and a question before the start is due is
383/// refused as `embed_worker_restarting`. It holds its own copy of the model,
384/// so with it the service's workers may use twice `embed_memory_bytes`.
385struct Questions {
386    worker: Arc<dyn Worker>,
387    restart: Restart,
388    state: Mutex<Asked>,
389    changed: Condvar,
390}
391
392/// The question worker's state, as the status names it.
393#[derive(Debug, Default)]
394struct Asked {
395    /// The start begun with the service is under way (SL2).
396    starting: bool,
397    ready: bool,
398    starts: u64,
399    deaths: u32,
400    next_start: Option<(Instant, Timestamp)>,
401    last_failure: Option<Failed>,
402}
403
404impl Questions {
405    fn lock(&self) -> MutexGuard<'_, Asked> {
406        self.state.lock().unwrap_or_else(PoisonError::into_inner)
407    }
408
409    /// Begin the start on a thread of its own; the caller does not wait. A
410    /// thread that cannot be made leaves the start to the first question,
411    /// and says why in the status.
412    fn begin(questions: &Arc<Self>) -> Option<JoinHandle<()>> {
413        questions.lock().starting = true;
414        let starting = Arc::clone(questions);
415        let begun = std::thread::Builder::new()
416            .name("haematite-question".to_owned())
417            .spawn(move || {
418                let started = starting.worker.question("ready");
419                starting.lock().starting = false;
420                starting.answered(true, &started);
421            });
422        match begun {
423            Ok(thread) => Some(thread),
424            Err(error) => {
425                let mut asked = questions.lock();
426                asked.starting = false;
427                asked.last_failure = Some(Failed {
428                    branch: String::new(),
429                    collection: String::new(),
430                    field: String::new(),
431                    at: current_timestamp(),
432                    failure: Failure::new("embed_thread_unstarted", error.to_string()),
433                });
434                drop(asked);
435                None
436            }
437        }
438    }
439
440    /// Keep what a call of the worker came to: ready, or a death and when
441    /// the next start is due; `starting` counts it as a start.
442    fn answered(&self, starting: bool, answer: &Result<Embedded, Failure>) {
443        let mut asked = self.lock();
444        if starting {
445            asked.starts = asked.starts.saturating_add(1);
446        }
447        match answer {
448            Ok(_embedded) => {
449                asked.ready = true;
450                asked.deaths = 0;
451                asked.next_start = None;
452            }
453            Err(failure) if failure.code.starts_with("embed_worker") => {
454                asked.ready = false;
455                asked.deaths = asked.deaths.saturating_add(1);
456                let wait = self.restart.wait(asked.deaths);
457                let at = current_timestamp();
458                let nanos = u64::try_from(wait.as_nanos()).unwrap_or(u64::MAX);
459                let due = Instant::now()
460                    .checked_add(wait)
461                    .unwrap_or_else(Instant::now);
462                asked.next_start = Some((due, at.saturating_add(nanos)));
463                asked.last_failure = Some(Failed {
464                    branch: String::new(),
465                    collection: String::new(),
466                    field: String::new(),
467                    at,
468                    failure: failure.clone(),
469                });
470            }
471            // A refused question is the call's, not the worker's: the worker
472            // stays ready, and the refusal is kept as the last failure, as a
473            // page refusal is.
474            Err(failure) => {
475                asked.last_failure = Some(Failed {
476                    branch: String::new(),
477                    collection: String::new(),
478                    field: String::new(),
479                    at: current_timestamp(),
480                    failure: failure.clone(),
481                });
482            }
483        }
484        drop(asked);
485        self.changed.notify_all();
486    }
487
488    fn status(&self) -> Value {
489        let asked = self.lock();
490        let state = match (asked.starting, asked.ready, asked.deaths) {
491            (true, _, _) => "starting",
492            (false, true, _) => "ready",
493            (false, false, 0) => "not_started",
494            (false, false, _) => "restarting",
495        };
496        let answer = json!({
497            "state": state,
498            "starts": asked.starts,
499            "next_start_due": asked.next_start.map(|(_due, at)| at),
500            "last_failure": asked.last_failure,
501        });
502        drop(asked);
503        answer
504    }
505}
506
507impl Ask for Questions {
508    /// The question, asked of this worker alone with no lock held; while
509    /// the start begun with the service is under way, after it, waiting up
510    /// to `embed_ready_ms` on this worker's own signal.
511    fn question(&self, text: &str) -> Result<Embedded, Failure> {
512        let until = Instant::now().checked_add(self.restart.ready);
513        let mut asked = self.lock();
514        while asked.starting {
515            let left = until.and_then(|until| until.checked_duration_since(Instant::now()));
516            let Some(left) = left.filter(|left| !left.is_zero()) else {
517                return Err(Failure::new(
518                    "embed_worker_starting",
519                    "the question worker has not answered yet; text search waits for it",
520                ));
521            };
522            asked = self.changed.wait_timeout(asked, left).map_or_else(
523                |poisoned| poisoned.into_inner().0,
524                |(asked, _timeout)| asked,
525            );
526        }
527        let due = asked.next_start.map(|(due, _at)| due);
528        let starting = !asked.ready;
529        drop(asked);
530        if due.is_some_and(|due| due > Instant::now()) {
531            return Err(Failure::new(
532                "embed_worker_restarting",
533                "the question worker died and is being started again; store.stats says when",
534            ));
535        }
536        let answer = self.worker.question(text);
537        self.answered(starting, &answer);
538        answer
539    }
540}
541
542/// The embedding thread and what it shares with the service.
543pub struct Driver {
544    admission: Arc<Admission>,
545    shared: Arc<Shared>,
546    worker: Arc<dyn Worker>,
547    /// SL1: the question worker, when questions are kept apart from the walk.
548    questions: Option<Arc<Questions>>,
549    /// SL2: the thread starting it, begun with the service.
550    question_start: Mutex<Option<JoinHandle<()>>>,
551    restart: Restart,
552    thread: Mutex<Option<JoinHandle<()>>>,
553    /// HANG1: the most `stop` waits for the threads ([`stop_within`]).
554    stop_within: Option<Duration>,
555}
556
557impl std::fmt::Debug for Driver {
558    fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
559        formatter.debug_struct("Driver").finish_non_exhaustive()
560    }
561}
562
563/// The service's embedder over its settings.
564#[must_use]
565pub fn embedder(settings: Settings) -> Arc<dyn Worker> {
566    Arc::new(Embedder::new(Some(settings)))
567}
568
569impl Driver {
570    /// Start the thread over `storage` with `worker`, restarted as
571    /// [`Restart::default`] says: every field declared on every live branch
572    /// waits.
573    ///
574    /// # Errors
575    /// The declared fields unread, or the thread unstarted, by name.
576    pub fn start(
577        storage: Arc<Mutex<Storage>>,
578        worker: Arc<dyn Worker>,
579        bounds: Bounds,
580        writer_wait: Duration,
581    ) -> Result<Self, Failure> {
582        Self::start_with(storage, worker, bounds, writer_wait, Restart::default())
583    }
584
585    /// As [`Self::start`], with the worker started again as `restart` says.
586    ///
587    /// # Errors
588    /// The declared fields unread, or the thread unstarted, by name.
589    pub fn start_with(
590        storage: Arc<Mutex<Storage>>,
591        worker: Arc<dyn Worker>,
592        bounds: Bounds,
593        writer_wait: Duration,
594        restart: Restart,
595    ) -> Result<Self, Failure> {
596        Self::start_apart(storage, worker, None, bounds, writer_wait, restart)
597    }
598
599    /// As [`Self::start_with`], with questions asked of `questions` when it
600    /// is given, never of the walk's `worker` (SL1). The question worker
601    /// never commits, so it never takes the admission (JOIN1).
602    ///
603    /// # Errors
604    /// The declared fields unread, or the thread unstarted, by name.
605    pub fn start_apart(
606        storage: Arc<Mutex<Storage>>,
607        worker: Arc<dyn Worker>,
608        questions: Option<Arc<dyn Worker>>,
609        bounds: Bounds,
610        writer_wait: Duration,
611        restart: Restart,
612    ) -> Result<Self, Failure> {
613        let admission = Arc::new(Admission::new(writer_wait)?);
614        let (fields, pending_reader) = {
615            let writer = storage.lock().map_err(|_| {
616                Failure::new("writer_unavailable", "writer state requires recovery")
617            })?;
618            (declared(&writer)?, writer.pending_reader())
619        };
620        let shared = Arc::new(Shared::default());
621        let mut state = shared.lock();
622        state.known.extend(fields.iter().cloned());
623        for field in fields {
624            state.waiting.insert(field);
625        }
626        state.worker = Some(WorkerState::EmbedWorkerStarting);
627        drop(state);
628        let thread_shared = Arc::clone(&shared);
629        let thread_worker = Arc::clone(&worker);
630        let thread_admission = Arc::clone(&admission);
631        let thread = std::thread::Builder::new()
632            .name("haematite-embed".to_owned())
633            .spawn(move || {
634                let ended = run(
635                    &thread_shared,
636                    &storage,
637                    &thread_admission,
638                    &pending_reader,
639                    thread_worker.as_ref(),
640                    bounds,
641                    restart,
642                );
643                let mut state = thread_shared.lock();
644                state.phase = Phase::Finished;
645                if !state.stopping {
646                    state.died = Some(ended.err().unwrap_or_else(|| {
647                        "embed_thread_ended: it stopped without being told".to_owned()
648                    }));
649                }
650                drop(state);
651                thread_shared.changed.notify_all();
652            })
653            .map_err(|error| Failure::new("embed_thread_unstarted", error.to_string()))?;
654        let questions = questions.map(|worker| {
655            Arc::new(Questions {
656                worker,
657                restart,
658                state: Mutex::new(Asked::default()),
659                changed: Condvar::new(),
660            })
661        });
662        // SL2: begun now, off the serving path; nothing waits for it here.
663        let question_start = questions.as_ref().and_then(Questions::begin);
664        Ok(Self {
665            admission,
666            shared,
667            worker,
668            questions,
669            question_start: Mutex::new(question_start),
670            restart,
671            thread: Mutex::new(Some(thread)),
672            stop_within: stop_within(bounds, writer_wait, restart),
673        })
674    }
675
676    pub(in crate::serve) fn admission(&self) -> &Admission {
677        &self.admission
678    }
679
680    /// A write on `branch` touched `collection`: each field declared there
681    /// waits, once.
682    pub fn wrote(&self, branch: &str, collection: &str) {
683        let mut guard = self.shared.lock();
684        let state = &mut *guard;
685        let mut added = false;
686        for field in state
687            .known
688            .iter()
689            .filter(|(known, at, _field)| known == branch && at == collection)
690        {
691            added |= state.waiting.insert(field.clone());
692        }
693        drop(guard);
694        if added {
695            self.shared.changed.notify_all();
696        }
697    }
698
699    /// `field` of `collection` on `branch` was declared: it is known, and
700    /// its walk waits.
701    pub fn declared(&self, branch: &str, collection: &str, field: &str) {
702        let key = (branch.to_owned(), collection.to_owned(), field.to_owned());
703        let mut state = self.shared.lock();
704        state.known.insert(key.clone());
705        state.ended.remove(&key);
706        state.waiting.insert(key);
707        drop(state);
708        self.shared.changed.notify_all();
709    }
710
711    /// The worker, for a question, once it is ready: a search that finds
712    /// none ready waits for one up to `embed_ready_ms`, on the driver's
713    /// signal, with no storage lock held.
714    ///
715    /// # Errors
716    /// `embed_worker_starting` before the worker first answered;
717    /// `embed_worker_restarting` while one that died is started again.
718    pub fn asker(&self) -> Result<&dyn Ask, Failure> {
719        // SL1: a question worker of its own answers whatever the walk does.
720        if let Some(questions) = &self.questions {
721            return Ok(questions.as_ref());
722        }
723        let until = Instant::now().checked_add(self.restart.ready);
724        let mut state = self.shared.lock();
725        #[cfg(test)]
726        {
727            state.askers = state.askers.saturating_add(1);
728            self.shared.changed.notify_all();
729        }
730        let worker = loop {
731            let left = until.and_then(|until| until.checked_duration_since(Instant::now()));
732            match (state.worker, left) {
733                (Some(WorkerState::Ready), _) => break WorkerState::Ready,
734                (_, Some(left)) if !left.is_zero() && !state.stopping && state.died.is_none() => {
735                    state = self.shared.wait_for(state, left);
736                }
737                (worker, _) => break worker.unwrap_or(WorkerState::EmbedWorkerStarting),
738            }
739        };
740        #[cfg(test)]
741        {
742            state.askers = state.askers.saturating_sub(1);
743        }
744        drop(state);
745        match worker {
746            WorkerState::Ready => Ok(self.worker.as_ref()),
747            WorkerState::Restarting => Err(Failure::new(
748                "embed_worker_restarting",
749                "the embedding worker died and is being started again; store.stats says when",
750            )),
751            WorkerState::EmbedWorkerStarting => Err(Failure::new(
752                "embed_worker_starting",
753                "the embedding worker has not answered yet; text search waits for it",
754            )),
755        }
756    }
757
758    /// What `vectors.fields` says of a field this driver serves: the
759    /// worker's state while it is not ready, else whether its walk is open.
760    #[must_use]
761    pub fn field_status(&self, branch: &str, collection: &str, field: &str) -> &'static str {
762        let worker = self.shared.lock().worker;
763        match worker {
764            Some(WorkerState::Ready) if self.walk_open(branch, collection, field) => "embedding",
765            Some(WorkerState::Ready) => "embedded",
766            Some(WorkerState::Restarting) => "embed_worker_restarting",
767            Some(WorkerState::EmbedWorkerStarting) | None => "embed_worker_starting",
768        }
769    }
770
771    /// EMB1: the records of a field this driver has given no vector since
772    /// the service started, because the model reads only part of their text.
773    #[must_use]
774    pub fn longer_than_model(&self, branch: &str, collection: &str, field: &str) -> u64 {
775        let key = (branch.to_owned(), collection.to_owned(), field.to_owned());
776        let state = self.shared.lock();
777        let count = counted(&state.longer_than_model, &key);
778        drop(state);
779        count
780    }
781
782    /// EMB-E1: the records of a field this driver has passed with no vector
783    /// since the service started, because their text is empty or only
784    /// whitespace (`embed_input_empty`).
785    #[must_use]
786    pub fn embed_input_empty(&self, branch: &str, collection: &str, field: &str) -> u64 {
787        let key = (branch.to_owned(), collection.to_owned(), field.to_owned());
788        let state = self.shared.lock();
789        let count = counted(&state.embed_input_empty, &key);
790        drop(state);
791        count
792    }
793
794    /// EMB-E2: the records of a field this driver has given windows, and
795    /// those given none for needing more than the cap, since it started.
796    #[must_use]
797    pub fn windows(&self, branch: &str, collection: &str, field: &str) -> (u64, u64) {
798        let key = (branch.to_owned(), collection.to_owned(), field.to_owned());
799        let state = self.shared.lock();
800        let counts = (
801            counted(&state.windowed, &key),
802            counted(&state.windows_over_cap, &key),
803        );
804        drop(state);
805        counts
806    }
807
808    #[must_use]
809    pub fn field_failure(&self, branch: &str, collection: &str, field: &str) -> Option<Value> {
810        let key = (branch.to_owned(), collection.to_owned(), field.to_owned());
811        let state = self.shared.lock();
812        state.failures.get(&key).map(|failed| json!(failed))
813    }
814
815    /// Whether the walk of a field is still open as far as this driver
816    /// knows: it waits, or a page of it is being made.
817    #[must_use]
818    pub fn walk_open(&self, branch: &str, collection: &str, field: &str) -> bool {
819        let key = (branch.to_owned(), collection.to_owned(), field.to_owned());
820        let state = self.shared.lock();
821        let open = state.waiting.contains(&key) || !state.ended.contains(&key);
822        drop(state);
823        open
824    }
825
826    /// The driver's state for the service's status answer.
827    #[must_use]
828    pub fn status(&self) -> Value {
829        // A handle `stop` has taken says nothing; the thread's own flag does.
830        let handle = self
831            .thread
832            .lock()
833            .unwrap_or_else(PoisonError::into_inner)
834            .as_ref()
835            .map(JoinHandle::is_finished);
836        let question = self
837            .questions
838            .as_ref()
839            .map_or_else(|| json!("off"), |questions| questions.status());
840        let state = self.shared.lock();
841        let finished = handle.unwrap_or(state.phase == Phase::Finished);
842        let thread = match (&state.died, finished, state.stopping) {
843            (Some(_), _, _) | (None, true, false) => "died",
844            (None, true, true) => "stopped",
845            (None, false, _) if state.phase.stepping() => "embedding",
846            (None, false, _) => "waiting",
847        };
848        let fields: Vec<Value> = state
849            .known
850            .iter()
851            .map(|key| {
852                let made = counted(&state.made, key);
853                let walk = if state.ended.contains(key) && !state.waiting.contains(key) {
854                    "done"
855                } else {
856                    "open"
857                };
858                json!({
859                    "branch": key.0, "collection": key.1, "field": key.2,
860                    "made_since_start": made, "walk": walk,
861                    "longer_than_model": counted(&state.longer_than_model, key),
862                    "embed_input_empty": counted(&state.embed_input_empty, key),
863                    "windowed": counted(&state.windowed, key),
864                    "windows_over_cap": counted(&state.windows_over_cap, key),
865                    "failure": state.failures.get(key),
866                })
867            })
868            .collect();
869        let answer = json!({
870            "thread": thread,
871            "died": state.died,
872            "worker": state.worker,
873            // SL1: both workers by name; the walk's is `worker` above.
874            "workers": {"walk": state.worker, "question": question},
875            "pages": state.pages,
876            "fields": fields,
877            "last_failure": state.last_failure,
878            "starts": state.starts,
879            "next_start_due": state.next_start.map(|(_due, at)| at),
880            "durable": {
881                "record_installs": state.durable.record_installs,
882                "barrier_device_flushes": state.durable.barrier_device_flushes,
883                "ref_file_full_flushes": state.durable.ref_file_full_flushes,
884                "directory_full_flushes": state.durable.directory_full_flushes,
885            },
886        });
887        drop(state);
888        answer
889    }
890}
891
892impl Driver {
893    /// HANG1: tell the threads to stop and wait for them on their own
894    /// signals, for at most [`stop_within`] in all: a page under way ends its
895    /// worker calls and its commit, and no new one starts. A thread still
896    /// running then is named and left, never waited on for ever. Each
897    /// thread is waited on once; a later call finds nothing to wait for.
898    ///
899    /// # Errors
900    /// `embed_thread_unended`, naming each thread that did not end in time.
901    pub fn stop(&self) -> Result<(), Failure> {
902        let until = self
903            .stop_within
904            .and_then(|within| Instant::now().checked_add(within));
905        let left = || until.and_then(|until| until.checked_duration_since(Instant::now()));
906        let mut state = self.shared.lock();
907        state.stopping = true;
908        self.shared.changed.notify_all();
909        while state.phase != Phase::Finished {
910            let Some(wait) = left().filter(|wait| !wait.is_zero()) else {
911                break;
912            };
913            state = self.shared.wait_for(state, wait);
914        }
915        let walk_ended = state.phase == Phase::Finished;
916        // STOP2: from here the thread, if it is left, starts no commit; one
917        // already started finishes, and is said.
918        state.left = !walk_ended;
919        let under_way = state.phase == Phase::Committing;
920        drop(state);
921        let question_ended = self.questions.as_ref().is_none_or(|questions| {
922            let mut asked = questions.lock();
923            while asked.starting {
924                let Some(wait) = left().filter(|wait| !wait.is_zero()) else {
925                    break;
926                };
927                asked = questions.changed.wait_timeout(asked, wait).map_or_else(
928                    |poisoned| poisoned.into_inner().0,
929                    |(asked, _timeout)| asked,
930                );
931            }
932            !asked.starting
933        });
934        let mut unended = Vec::new();
935        for (name, slot, ended) in [
936            ("haematite-embed", &self.thread, walk_ended),
937            ("haematite-question", &self.question_start, question_ended),
938        ] {
939            let thread = slot.lock().unwrap_or_else(PoisonError::into_inner).take();
940            match thread {
941                Some(thread) if ended => drop(thread.join()),
942                Some(_left) => unended.push(name),
943                None => {}
944            }
945        }
946        if unended.is_empty() {
947            return Ok(());
948        }
949        let commit = match (walk_ended, under_way) {
950            (true, _) => "",
951            (false, true) => "; its commit already under way finishes",
952            (false, false) => "; it starts no commit",
953        };
954        Err(Failure::new(
955            "embed_thread_unended",
956            format!(
957                "{} did not end within {:?} of being told to stop, and is left running{commit}",
958                unended.join(" and "),
959                self.stop_within.unwrap_or_default()
960            ),
961        ))
962    }
963}
964
965impl Drop for Driver {
966    /// [`Driver::stop`]; a thread it leaves is named on the service's
967    /// standard error, the one place a drop can say it.
968    fn drop(&mut self) {
969        if let Err(failure) = self.stop() {
970            eprintln!("haematite embedding: {failure}");
971        }
972    }
973}
974
975/// Every field declared on every live branch.
976fn declared(storage: &Storage) -> Result<Vec<Field>, Failure> {
977    let mut fields = Vec::new();
978    for branch in storage.branch_names() {
979        let at = storage.capture(&Selection {
980            branch: branch.clone(),
981            version: None,
982        })?;
983        for (collection, field) in storage.embedded_fields(&at)? {
984            fields.push((branch.clone(), collection, field));
985        }
986    }
987    Ok(fields)
988}
989
990/// The thread: start the worker, then take a waiting field and make one page
991/// of it, until told to stop. A worker that died is started again once its
992/// start is due, before any page.
993fn run(
994    shared: &Shared,
995    storage: &Mutex<Storage>,
996    admission: &Admission,
997    pending_reader: &PendingReader,
998    worker: &dyn Worker,
999    bounds: Bounds,
1000    restart: Restart,
1001) -> Result<(), String> {
1002    start_worker(shared, worker, restart);
1003    loop {
1004        let mut state = shared.lock();
1005        let field = loop {
1006            match state.scheduled(Instant::now()) {
1007                Scheduled::Stop => return Ok(()),
1008                Scheduled::Start => break None,
1009                Scheduled::Page(field) => break Some(field),
1010                Scheduled::Wait(Some(left)) => state = shared.wait_for(state, left),
1011                Scheduled::Wait(None) => state = shared.wait(state),
1012            }
1013        };
1014        drop(state);
1015        // JOIN1: a restart takes neither the storage lock nor the
1016        // admission; only a page's commit goes through admission.background.
1017        match field {
1018            None => start_worker(shared, worker, restart),
1019            Some(field) => page(
1020                shared,
1021                (storage, admission),
1022                pending_reader,
1023                worker,
1024                bounds,
1025                (restart, None),
1026                field,
1027            )?,
1028        }
1029    }
1030}
1031
1032/// Start the worker, off the serving path and with no lock held: one
1033/// question, whose answer only says the worker is ready.
1034fn start_worker(shared: &Shared, worker: &dyn Worker, restart: Restart) {
1035    let started = worker.question("ready");
1036    let mut state = shared.lock();
1037    state.starts = state.starts.saturating_add(1);
1038    match started {
1039        Ok(_ready) => {
1040            state.worker = Some(WorkerState::Ready);
1041            state.deaths = 0;
1042            state.next_start = None;
1043        }
1044        Err(failure) => state.died(
1045            restart,
1046            Failed {
1047                branch: String::new(),
1048                collection: String::new(),
1049                field: String::new(),
1050                at: current_timestamp(),
1051                failure,
1052            },
1053        ),
1054    }
1055    drop(state);
1056    shared.changed.notify_all();
1057}
1058
1059/// A snapshot page and model call hold no writer. Admission lets one
1060/// foreground writer finish before the next batch commit can begin; the
1061/// storage and the admission its commit goes through come together.
1062fn page(
1063    shared: &Shared,
1064    (storage, admission): (&Mutex<Storage>, &Admission),
1065    pending_reader: &PendingReader,
1066    worker: &dyn Worker,
1067    bounds: Bounds,
1068    (restart, attempted): (Restart, Option<Instant>),
1069    field: Field,
1070) -> Result<(), String> {
1071    let (branch, collection, name) = (&field.0, &field.1, &field.2);
1072    let read = pending_reader.read_page(
1073        branch,
1074        collection,
1075        name,
1076        bounds.page_records,
1077        bounds.input_bytes,
1078    );
1079    let mut position = None;
1080    let outcome = match read {
1081        Err(failure) => Err(failure),
1082        Ok(None) => Ok(None),
1083        Ok(Some(read)) => {
1084            let pending = read.pending;
1085            position = Some(PagePosition {
1086                committed: read.position,
1087                toward: pending.toward,
1088                end_after: match &pending.reach {
1089                    Reach::Upto(after) => Some(after.clone()),
1090                    Reach::End => None,
1091                },
1092            });
1093            match job::make(
1094                &pending,
1095                collection,
1096                name,
1097                &read.declaration,
1098                worker,
1099                bounds,
1100            ) {
1101                Err(failure) => Err(failure),
1102                Ok(made) => {
1103                    let mut writer = admission
1104                        .background(storage)
1105                        .map_err(|failure| failure.to_string())?;
1106                    // STOP2: the last word before a commit. Under the lock
1107                    // `stop` gives up under, so a commit either started
1108                    // before it gave up, and finishes, or never starts: the
1109                    // page is not written and is made again on the next start.
1110                    let mut state = shared.lock();
1111                    if state.left {
1112                        state.phase = Phase::Waiting;
1113                        drop(state);
1114                        drop(writer);
1115                        return Ok(());
1116                    }
1117                    state.phase = Phase::Committing;
1118                    drop(state);
1119                    let before = writer.sync_counts();
1120                    let committed = writer.embed_commit(branch, &made.page, bounds.page_records);
1121                    let durable = writer.sync_counts().since(before);
1122                    drop(writer);
1123                    committed.map(|_| Some((made, durable)))
1124                }
1125            }
1126        }
1127    };
1128    finish_page(shared, field, position, outcome, restart, attempted)
1129}
1130
1131fn finish_page(
1132    shared: &Shared,
1133    field: Field,
1134    position: Option<PagePosition>,
1135    outcome: Result<Option<(job::MadePage, SyncCounts)>, Failure>,
1136    restart: Restart,
1137    attempted: Option<Instant>,
1138) -> Result<(), String> {
1139    let mut state = shared.lock();
1140    state.phase = Phase::Waiting;
1141    let now = attempted.unwrap_or_else(Instant::now);
1142    match outcome {
1143        // Nothing pending: the walk is at the head.
1144        Ok(None) => {
1145            state.ended.insert(field);
1146        }
1147        Ok(Some((made, durable))) => {
1148            state.committed_page(&field);
1149            state.worker = Some(WorkerState::Ready);
1150            state.pages = state.pages.saturating_add(1);
1151            state.durable = plus(state.durable, durable);
1152            count(&mut state.made, &field, made.page.made.len());
1153            // EMB-E2 (i): a text past the cap is one the model reads part of.
1154            let over = made.windows_over_cap.len();
1155            let longer = made.longer_than_model.len().saturating_add(over);
1156            count(&mut state.longer_than_model, &field, longer);
1157            count(&mut state.embed_input_empty, &field, made.empty_input.len());
1158            count(&mut state.windowed, &field, made.page.windowed.len());
1159            count(&mut state.windows_over_cap, &field, over);
1160            // Asked again until the walk says it is at the head.
1161            if made.page.reach != Reach::End {
1162                state.ended.remove(&field);
1163            }
1164            state.waiting.insert(field);
1165        }
1166        // The worker died or timed out: it is started again, and the field
1167        // waits to carry on from its last commit (EWR1).
1168        Err(failure) if failure.code.starts_with("embed_worker") => {
1169            state.failed_page(&field, position, failure.clone(), restart, now)?;
1170            state.died_at(
1171                restart,
1172                Failed {
1173                    branch: field.0,
1174                    collection: field.1,
1175                    field: field.2,
1176                    at: current_timestamp(),
1177                    failure,
1178                },
1179                now,
1180            );
1181        }
1182        // A page refusal keeps its position and yields to every other field.
1183        Err(failure) => {
1184            state.failed_page(&field, position, failure.clone(), restart, now)?;
1185            state.last_failure = Some(Failed {
1186                branch: field.0,
1187                collection: field.1,
1188                field: field.2,
1189                at: current_timestamp(),
1190                failure,
1191            });
1192        }
1193    }
1194    drop(state);
1195    shared.changed.notify_all();
1196    Ok(())
1197}
1198
1199/// `counts`' number for `field`; 0 for a field it does not name.
1200fn counted(counts: &[(Field, u64)], field: &Field) -> u64 {
1201    counts
1202        .iter()
1203        .find(|(known, _)| known == field)
1204        .map_or(0, |(_, number)| *number)
1205}
1206
1207/// Add `more` to `counts`' number for `field`, naming the field first if
1208/// `counts` does not yet.
1209fn count(counts: &mut Vec<(Field, u64)>, field: &Field, more: usize) {
1210    let more = u64::try_from(more).unwrap_or(u64::MAX);
1211    match counts.iter_mut().find(|(known, _)| known == field) {
1212        Some((_, number)) => *number = number.saturating_add(more),
1213        None => counts.push((field.clone(), more)),
1214    }
1215}
1216
1217const fn plus(left: SyncCounts, right: SyncCounts) -> SyncCounts {
1218    SyncCounts {
1219        record_installs: left.record_installs.saturating_add(right.record_installs),
1220        barrier_device_flushes: left
1221            .barrier_device_flushes
1222            .saturating_add(right.barrier_device_flushes),
1223        ref_file_full_flushes: left
1224            .ref_file_full_flushes
1225            .saturating_add(right.ref_file_full_flushes),
1226        directory_full_flushes: left
1227            .directory_full_flushes
1228            .saturating_add(right.directory_full_flushes),
1229    }
1230}
1231
1232#[cfg(test)]
1233impl Driver {
1234    pub(in crate::serve) fn for_steps(
1235        storage: &Mutex<Storage>,
1236        worker: Arc<dyn Worker>,
1237        bounds: Bounds,
1238        restart: Restart,
1239    ) -> Result<Self, Failure> {
1240        let writer = storage
1241            .lock()
1242            .map_err(|_| Failure::new("writer_unavailable", "writer state requires recovery"))?;
1243        let fields = declared(&writer)?;
1244        drop(writer);
1245        let shared = Arc::new(Shared::default());
1246        {
1247            let mut state = shared.lock();
1248            state.known.extend(fields.iter().cloned());
1249            for field in fields {
1250                state.waiting.insert(field);
1251            }
1252        }
1253        start_worker(&shared, worker.as_ref(), restart);
1254        Ok(Self {
1255            admission: Arc::new(Admission::new(Duration::from_secs(1))?),
1256            shared,
1257            worker,
1258            questions: None,
1259            question_start: Mutex::new(None),
1260            restart,
1261            thread: Mutex::new(None),
1262            stop_within: stop_within(bounds, Duration::from_secs(1), restart),
1263        })
1264    }
1265
1266    pub(in crate::serve) fn step_at(
1267        &self,
1268        storage: &Mutex<Storage>,
1269        bounds: Bounds,
1270        now: Instant,
1271    ) -> Result<bool, String> {
1272        let mut state = self.shared.lock();
1273        let scheduled = state.scheduled(now);
1274        drop(state);
1275        let field = match scheduled {
1276            Scheduled::Page(field) => Some(field),
1277            Scheduled::Start => None,
1278            Scheduled::Wait(_) | Scheduled::Stop => return Ok(false),
1279        };
1280        if let Some(field) = field {
1281            let reader = storage
1282                .lock()
1283                .map_err(|error| error.to_string())?
1284                .pending_reader();
1285            page(
1286                &self.shared,
1287                (storage, &self.admission),
1288                &reader,
1289                self.worker.as_ref(),
1290                bounds,
1291                (self.restart, Some(now)),
1292                field,
1293            )?;
1294        } else {
1295            start_worker(&self.shared, self.worker.as_ref(), self.restart);
1296        }
1297        Ok(true)
1298    }
1299
1300    #[must_use]
1301    pub(in crate::serve) fn retry_due(
1302        &self,
1303        branch: &str,
1304        collection: &str,
1305        field: &str,
1306    ) -> Option<Instant> {
1307        self.shared
1308            .lock()
1309            .failures
1310            .get(&(branch.to_owned(), collection.to_owned(), field.to_owned()))
1311            .and_then(|failed| failed.due)
1312    }
1313
1314    /// Wait until the worker answered or failed, nothing waits, and no page
1315    /// is being made, or the thread ended.
1316    pub fn wait_idle(&self) {
1317        let mut state = self.shared.lock();
1318        while (state.worker == Some(WorkerState::EmbedWorkerStarting)
1319            || state.worker == Some(WorkerState::Restarting)
1320            || !state.waiting.is_empty()
1321            || state.phase.stepping())
1322            && state.died.is_none()
1323        {
1324            state = self.shared.wait(state);
1325        }
1326        drop(state);
1327    }
1328
1329    #[must_use]
1330    pub fn pages(&self) -> u64 {
1331        self.shared.lock().pages
1332    }
1333
1334    #[must_use]
1335    pub fn durable(&self) -> SyncCounts {
1336        self.shared.lock().durable
1337    }
1338
1339    #[must_use]
1340    pub fn last_failure(&self) -> Option<Failed> {
1341        self.shared.lock().last_failure.clone()
1342    }
1343
1344    /// Each wait before a start of the worker, in order.
1345    #[must_use]
1346    pub fn waits(&self) -> Vec<Duration> {
1347        self.shared.lock().waits.clone()
1348    }
1349
1350    /// Wait until the walk's thread has left `run` (HANG1).
1351    pub fn wait_walk_ended(&self) {
1352        let mut state = self.shared.lock();
1353        while state.phase != Phase::Finished {
1354            state = self.shared.wait(state);
1355        }
1356        drop(state);
1357    }
1358
1359    /// Wait until the question worker's start begun with the service has
1360    /// answered or failed (SL2).
1361    pub fn wait_question_started(&self) {
1362        let Some(questions) = &self.questions else {
1363            return;
1364        };
1365        let mut asked = questions.lock();
1366        while asked.starting {
1367            asked = questions
1368                .changed
1369                .wait(asked)
1370                .unwrap_or_else(PoisonError::into_inner);
1371        }
1372        drop(asked);
1373    }
1374
1375    /// Wait until `count` searches wait for a worker.
1376    pub fn wait_askers(&self, count: usize) {
1377        let mut state = self.shared.lock();
1378        while state.askers < count {
1379            state = self.shared.wait(state);
1380        }
1381        drop(state);
1382    }
1383}
1384
1385#[cfg(test)]
1386#[path = "embedding_tests.rs"]
1387mod tests;