1use 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
52pub trait Worker: Embed + Ask + Send + Sync {}
55
56impl<W: Embed + Ask + Send + Sync> Worker for W {}
57
58#[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
74fn 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
89pub type Field = (String, String, String);
91
92#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize)]
94#[serde(rename_all = "snake_case")]
95pub enum WorkerState {
96 EmbedWorkerStarting,
97 Ready,
98 Restarting,
100}
101
102#[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#[derive(Debug, Default, Clone, Copy, PartialEq, Eq)]
114enum Phase {
115 #[default]
117 Waiting,
118 Stepping,
120 Committing,
122 Finished,
124}
125
126impl Phase {
127 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 known: BTreeSet<Field>,
216 worker: Option<WorkerState>,
217 stopping: bool,
218 phase: Phase,
219 left: bool,
221 died: Option<String>,
222 pages: u64,
223 made: Vec<(Field, u64)>,
225 longer_than_model: Vec<(Field, u64)>,
228 embed_input_empty: Vec<(Field, u64)>,
231 windowed: Vec<(Field, u64)>,
233 windows_over_cap: Vec<(Field, u64)>,
236 ended: BTreeSet<Field>,
238 last_failure: Option<Failed>,
239 durable: SyncCounts,
240 starts: u64,
242 deaths: u32,
244 next_start: Option<(Instant, Timestamp)>,
246 #[cfg(test)]
248 waits: Vec<Duration>,
249 #[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 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 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
375struct Questions {
386 worker: Arc<dyn Worker>,
387 restart: Restart,
388 state: Mutex<Asked>,
389 changed: Condvar,
390}
391
392#[derive(Debug, Default)]
394struct Asked {
395 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 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 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 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 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
542pub struct Driver {
544 admission: Arc<Admission>,
545 shared: Arc<Shared>,
546 worker: Arc<dyn Worker>,
547 questions: Option<Arc<Questions>>,
549 question_start: Mutex<Option<JoinHandle<()>>>,
551 restart: Restart,
552 thread: Mutex<Option<JoinHandle<()>>>,
553 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#[must_use]
565pub fn embedder(settings: Settings) -> Arc<dyn Worker> {
566 Arc::new(Embedder::new(Some(settings)))
567}
568
569impl Driver {
570 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 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 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 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 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 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 pub fn asker(&self) -> Result<&dyn Ask, Failure> {
719 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 #[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 #[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 #[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 #[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 #[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 #[must_use]
828 pub fn status(&self) -> Value {
829 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 "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 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 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 fn drop(&mut self) {
969 if let Err(failure) = self.stop() {
970 eprintln!("haematite embedding: {failure}");
971 }
972 }
973}
974
975fn 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
990fn 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 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
1032fn 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
1059fn 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 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 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 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 if made.page.reach != Reach::End {
1162 state.ended.remove(&field);
1163 }
1164 state.waiting.insert(field);
1165 }
1166 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 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
1199fn counted(counts: &[(Field, u64)], field: &Field) -> u64 {
1201 counts
1202 .iter()
1203 .find(|(known, _)| known == field)
1204 .map_or(0, |(_, number)| *number)
1205}
1206
1207fn 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 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 #[must_use]
1346 pub fn waits(&self) -> Vec<Duration> {
1347 self.shared.lock().waits.clone()
1348 }
1349
1350 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 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 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;