1mod text_search;
4pub(in crate::serve) use text_search::{Words, not_configured};
5
6use std::cell::Cell;
7use std::io::{self, Write};
8use std::num::{NonZeroU32, NonZeroU64, NonZeroUsize};
9use std::sync::{Arc, Condvar, Mutex, TryLockError};
10use std::time::{Duration, Instant};
11
12use haematite::db::ReadOnlyDatabase;
13use serde_json::value::RawValue;
14use serde_json::{Value, json};
15
16use super::embed::job::Bounds;
17use super::embed::settings::Restart;
18use super::embedding::{Driver, Guard, Worker};
19use super::protocol::{
20 BranchPage, Change, CollectionPage, CountsRebuild, DocumentPage, DraftsRun, EdgePage,
21 EmbedRewalk, GraphPage, MutationContext, Operation, PROTOCOL, ResponseFailure, ReviewPage,
22 Selection, StoreRead,
23};
24use super::query::Seal;
25use super::query::admission::Slots;
26use super::query::probe::Prepared;
27use super::releaser::{self, Releaser};
28use super::storage::{
29 Captured, Catalogue, DiscardPasses, Failure, Held, Storage, edges, graph, reads,
30};
31
32use super::access::{self, Access, Source, Standalone};
33use super::changes::{self, ChangesRequest};
34use super::pass::{self, Gate};
35use super::placement::{self, Placer};
36
37thread_local! {
38 static WRITER_DEADLINE: Cell<Option<(Instant, Duration)>> = const { Cell::new(None) };
39}
40
41struct RequestWait(Option<(Instant, Duration)>);
42impl RequestWait {
43 fn begin(timeout: Duration) -> Result<Self, Failure> {
44 if timeout.is_zero() {
45 return Err(Failure::new(
46 "invalid_params",
47 "writer timeout must be positive",
48 ));
49 }
50 let end = Instant::now().checked_add(timeout).ok_or_else(|| {
51 Failure::new(
52 "invalid_params",
53 "writer timeout exceeds the monotonic clock",
54 )
55 })?;
56 Ok(Self(WRITER_DEADLINE.replace(Some((end, timeout)))))
57 }
58}
59impl Drop for RequestWait {
60 fn drop(&mut self) {
61 WRITER_DEADLINE.set(self.0);
62 }
63}
64
65#[derive(Default)]
66struct WriterAvailable {
67 active: Mutex<bool>,
68 changed: Condvar,
69}
70
71pub struct Service {
72 access_source: Arc<dyn Source>,
73 lys: Option<Arc<Gate>>,
76 storage: Arc<Mutex<Storage>>,
77 writer_available: Arc<WriterAvailable>,
78 reader: ReadOnlyDatabase,
79 catalogue: Catalogue,
80 discard_passes: DiscardPasses,
82 pub max_page: NonZeroU32,
83 pub append_prune_limits: super::storage::AppendPruneLimits,
84 pub query: Option<Prepared>,
85 query_slots: Slots,
87 query_seal: Seal,
89 pub changes: changes::Settings,
91 releaser: Option<Releaser>,
93 embedding: Option<Driver>,
95 pub embed_search_bound: Option<NonZeroU64>,
97 placer: Option<Placer>,
100}
101
102impl Service {
103 pub fn new(storage: Storage, max_page: NonZeroU32) -> Result<Self, Failure> {
107 Self::with_query(storage, max_page, None)
108 }
109
110 pub fn with_query(
116 storage: Storage,
117 max_page: NonZeroU32,
118 query: Option<Prepared>,
119 ) -> Result<Self, Failure> {
120 Self::with_access_source(storage, max_page, query, Arc::new(Standalone))
121 }
122
123 pub(in crate::serve) fn with_access_source(
124 storage: Storage,
125 max_page: NonZeroU32,
126 query: Option<Prepared>,
127 access_source: Arc<dyn Source>,
128 ) -> Result<Self, Failure> {
129 let reader = storage.reader()?;
130 let catalogue = storage.catalogue();
131 let discard_passes = storage.discard_passes();
132 let running = running_limit(query.as_ref().map(Prepared::settings))?;
133 let embed_search_bound = query
134 .as_ref()
135 .map(|prepared| prepared.settings().scan_records);
136 Ok(Self {
137 access_source,
138 lys: None,
139 storage: Arc::new(Mutex::new(storage)),
140 writer_available: Arc::default(),
141 reader,
142 catalogue,
143 discard_passes,
144 max_page,
145 append_prune_limits: super::storage::AppendPruneLimits::default(),
146 query,
147 query_slots: Slots::new(running),
148 query_seal: Seal::new()?,
149 changes: changes::Settings::default(),
150 releaser: None,
151 embedding: None,
152 embed_search_bound,
153 placer: None,
154 })
155 }
156
157 #[must_use]
161 pub fn with_lys(mut self, gate: Gate) -> Self {
162 if let Some(placer) = &self.placer {
163 placer.tell(gate.made());
164 }
165 self.lys = Some(Arc::new(gate));
166 self
167 }
168
169 pub fn with_placer(mut self, placer: Placer) -> Result<Self, Failure> {
177 if let Some(gate) = &self.lys {
178 placer.tell(gate.made());
179 }
180 placer.existing(placement::held(&self.catalogue, &self.reader)?);
181 self.placer = Some(placer);
182 Ok(self)
183 }
184
185 pub fn present(&self, token: &str) -> Result<Value, Failure> {
192 let Some(gate) = &self.lys else {
193 return Err(Failure::new(
194 "invalid_operation",
195 "this service is not served through Lys, so it takes no pass",
196 ));
197 };
198 gate.present(token, pass::now())
199 }
200
201 pub fn run_drafts(&self, token: Option<&str>, run: &DraftsRun) -> Result<Value, Failure> {
209 let Some(gate) = &self.lys else {
210 return Err(Failure::new(
211 "invalid_operation",
212 "this service is not served through Lys, so it runs no drafts",
213 ));
214 };
215 gate.run_drafts(self, token, run.after.as_deref())
216 }
217
218 pub(in crate::serve) fn run_drafts_waiting(
220 &self,
221 token: Option<&str>,
222 run: &DraftsRun,
223 timeout: Duration,
224 ) -> Result<Value, Failure> {
225 let _wait = RequestWait::begin(timeout)?;
226 self.run_drafts(token, run)
227 }
228
229 pub fn follow(
240 &self,
241 branch: &str,
242 expires: Option<u64>,
243 clock: impl Fn() -> std::time::SystemTime,
244 send: impl FnMut(&Value) -> io::Result<()>,
245 ) -> io::Result<Failure> {
246 super::follow::follow(&self.catalogue, branch, expires, clock, send)
247 }
248
249 pub(in crate::serve) fn follow_socket(
250 &self,
251 socket: &std::os::unix::net::UnixStream,
252 branch: &str,
253 expires: Option<u64>,
254 send: impl FnMut(&Value) -> io::Result<()>,
255 ) -> io::Result<Failure> {
256 use std::sync::atomic::{AtomicBool, Ordering};
257 let cancelled = Arc::new(AtomicBool::new(false));
258 let closed = Arc::clone(&cancelled);
259 let catalogue = self.catalogue.clone();
260 let _monitor = super::follow::Monitor::start(socket.try_clone()?, move || {
261 closed.store(true, Ordering::Release);
262 catalogue.wake_followers();
263 })?;
264 super::follow::follow_cancellable(
265 &self.catalogue,
266 branch,
267 expires,
268 std::time::SystemTime::now,
269 &cancelled,
270 send,
271 )
272 }
273
274 pub fn authorize(
283 &self,
284 token: Option<&str>,
285 operation: &Operation,
286 words: Option<&str>,
287 ) -> Result<(), Failure> {
288 let Some(gate) = &self.lys else {
289 return Ok(());
290 };
291 let absent = |kind: &str, name: &str| match kind {
292 "branch" => self.catalogue.head(name).is_ok_and(|head| head.is_none()),
293 "collection" => self.collection_absent(operation, name),
294 _ => false,
295 };
296 gate.authorize_with(token, operation, words, pass::now(), &absent)
297 .map(drop)
298 }
299
300 fn collection_absent(&self, operation: &Operation, collection: &str) -> bool {
304 let Operation::Mutate(mutation) = operation else {
305 return false;
306 };
307 self.catalogue
308 .capture_held(&Selection {
309 branch: mutation.branch.clone(),
310 version: None,
311 })
312 .and_then(|at| reads::collection(&self.reader, &at, collection))
313 .is_ok_and(|found| found.is_none())
314 }
315
316 pub fn start_release(&mut self, settings: releaser::Settings) -> Result<(), Failure> {
323 self.releaser = Some(Releaser::start(Arc::clone(&self.storage), settings)?);
324 Ok(())
325 }
326
327 pub fn start_embedding(
334 &mut self,
335 worker: Arc<dyn Worker>,
336 bounds: Bounds,
337 writer_wait: std::time::Duration,
338 ) -> Result<(), Failure> {
339 self.start_embedding_with(worker, bounds, writer_wait, Restart::default())
340 }
341
342 pub fn start_embedding_with(
349 &mut self,
350 worker: Arc<dyn Worker>,
351 bounds: Bounds,
352 writer_wait: std::time::Duration,
353 restart: Restart,
354 ) -> Result<(), Failure> {
355 self.start_embedding_apart(worker, None, bounds, writer_wait, restart)
356 }
357
358 pub fn start_embedding_apart(
367 &mut self,
368 worker: Arc<dyn Worker>,
369 questions: Option<Arc<dyn Worker>>,
370 bounds: Bounds,
371 writer_wait: std::time::Duration,
372 restart: Restart,
373 ) -> Result<(), Failure> {
374 let storage = Arc::clone(&self.storage);
375 let driver = Driver::start_apart(storage, worker, questions, bounds, writer_wait, restart)?;
376 self.embedding = Some(driver);
377 Ok(())
378 }
379
380 #[must_use]
382 pub const fn embedding(&self) -> Option<&Driver> {
383 self.embedding.as_ref()
384 }
385
386 #[cfg(test)]
387 pub(in crate::serve) fn start_embedding_steps(
388 &mut self,
389 worker: Arc<dyn Worker>,
390 bounds: Bounds,
391 restart: Restart,
392 ) -> Result<(), Failure> {
393 self.embedding = Some(Driver::for_steps(&self.storage, worker, bounds, restart)?);
394 Ok(())
395 }
396
397 #[cfg(test)]
398 pub(in crate::serve) fn embedding_step_at(
399 &self,
400 bounds: Bounds,
401 now: std::time::Instant,
402 ) -> Result<bool, String> {
403 self.embedding
404 .as_ref()
405 .ok_or("embedding driver missing")?
406 .step_at(&self.storage, bounds, now)
407 }
408
409 #[must_use]
411 pub const fn releaser(&self) -> Option<&Releaser> {
412 self.releaser.as_ref()
413 }
414
415 pub(crate) fn activate_reclaim(&self) -> Result<(), Failure> {
416 self.admission()?.activate_reclaim()
417 }
418
419 #[cfg(test)]
425 pub fn quiesce_reclaim(&self) -> Result<(), Failure> {
426 self.admission()?.quiesce_reclaim()
427 }
428
429 #[cfg(test)]
434 pub fn reclaim_status(&self) -> Result<haematite::db::ReclaimStatus, Failure> {
435 Ok(self.admission()?.reclaim_status())
436 }
437
438 fn releasing(&self) -> bool {
439 self.releaser.as_ref().is_some_and(Releaser::is_releasing)
440 }
441
442 fn admission(&self) -> Result<Guard<'_, Storage>, Failure> {
443 let Some((end, timeout)) = WRITER_DEADLINE.get() else {
444 return self.admission_now();
445 };
446 let started = Instant::now();
447 loop {
448 if Instant::now() >= end {
449 return Err(Failure::new(
450 "busy",
451 format!(
452 "writer lock wait reached timeout_ms={} after {} ms",
453 timeout.as_millis(),
454 started.elapsed().as_millis()
455 ),
456 ));
457 }
458 match self.admission_now() {
459 Ok(guard) => {
460 if Instant::now() < end {
461 return Ok(guard);
462 }
463 drop(guard);
464 return Err(Failure::new(
465 "busy",
466 format!(
467 "writer lock wait reached timeout_ms={} after {} ms",
468 timeout.as_millis(),
469 started.elapsed().as_millis()
470 ),
471 ));
472 }
473 Err(failure) if failure.code == "busy" => {
474 self.wait_writer(end, started, timeout)?;
475 }
476 Err(failure) => return Err(failure),
477 }
478 }
479 }
480
481 fn wait_writer(
482 &self,
483 end: Instant,
484 started: Instant,
485 timeout: Duration,
486 ) -> Result<(), Failure> {
487 let unavailable = || {
488 Failure::new(
489 "writer_unavailable",
490 "writer wait state requires process recovery",
491 )
492 };
493 let busy = || {
494 Failure::new(
495 "busy",
496 format!(
497 "writer lock wait reached timeout_ms={} after {} ms",
498 timeout.as_millis(),
499 started.elapsed().as_millis()
500 ),
501 )
502 };
503 if Instant::now() >= end {
504 return Err(busy());
505 }
506 let waited = {
507 let mut active = self
508 .writer_available
509 .active
510 .lock()
511 .map_err(|_| unavailable())?;
512 if !*active {
513 *active = true;
514 #[cfg(test)]
515 self.writer_available.changed.notify_all();
516 let storage = Arc::clone(&self.storage);
517 let ready = Arc::clone(&self.writer_available);
518 if let Err(error) = std::thread::Builder::new()
519 .name("haematite-writer-ready".to_owned())
520 .spawn(move || {
521 match storage.lock() {
523 Ok(guard) => drop(guard),
524 Err(error) => eprintln!("haematite writer_unavailable: {error}"),
525 }
526 match ready.active.lock() {
527 Ok(mut active) => {
528 *active = false;
529 ready.changed.notify_all();
530 }
531 Err(error) => {
532 eprintln!("haematite writer_wait_unavailable: {error}");
533 ready.changed.notify_all();
534 }
535 }
536 })
537 {
538 *active = false;
539 self.writer_available.changed.notify_all();
540 return Err(Failure::new(
541 "writer_unavailable",
542 format!("writer wait thread could not start: {error}"),
543 ));
544 }
545 }
546 let left = end
547 .checked_duration_since(Instant::now())
548 .filter(|left| !left.is_zero())
549 .ok_or_else(busy)?;
550 self.writer_available
551 .changed
552 .wait_timeout_while(active, left, |active| *active)
553 .map_err(|_| unavailable())
554 }?;
555 let timed_out = waited.1.timed_out();
556 drop(waited);
557 if timed_out {
558 return Err(busy());
559 }
560 Ok(())
561 }
562
563 fn admission_now(&self) -> Result<Guard<'_, Storage>, Failure> {
564 if let Some(driver) = &self.embedding {
565 return match WRITER_DEADLINE.get() {
566 Some((end, _)) => driver.admission().foreground_until(&self.storage, end),
567 None => driver
568 .admission()
569 .foreground(&self.storage, self.releasing()),
570 };
571 }
572 let unavailable = || {
573 Failure::new(
574 "writer_unavailable",
575 "writer state requires process recovery",
576 )
577 };
578 match self.storage.try_lock() {
579 Ok(guard) => Ok(Guard::plain(guard)),
580 Err(TryLockError::WouldBlock)
581 if self.releasing() && WRITER_DEADLINE.get().is_none() =>
582 {
583 self.storage
584 .lock()
585 .map(Guard::plain)
586 .map_err(|_| unavailable())
587 }
588 Err(TryLockError::WouldBlock) => Err(Failure::new(
589 "busy",
590 "another mutation is being admitted; retry this request identity",
591 )),
592 Err(TryLockError::Poisoned(_)) => Err(unavailable()),
593 }
594 }
595
596 pub fn dispatch(&self, operation: Operation) -> Result<Value, ResponseFailure> {
604 let mutation = MutationContext::from_operation(&operation);
605 let result = access::request(self.access_source.as_ref())
606 .and_then(|access| self.dispatch_with_entry_budget(&access, operation, None))
607 .and_then(Answer::into_value);
608 result.map_err(|failure| ResponseFailure::new(failure, mutation))
609 }
610
611 pub fn dispatch_waiting(
617 &self,
618 operation: Operation,
619 timeout: std::time::Duration,
620 ) -> Result<Value, ResponseFailure> {
621 let mutation = MutationContext::from_operation(&operation);
622 let _wait = RequestWait::begin(timeout)
623 .map_err(|failure| ResponseFailure::new(failure, mutation))?;
624 self.dispatch(operation)
625 }
626
627 pub fn dispatch_text_waiting(
633 &self,
634 operation: Operation,
635 limit: NonZeroUsize,
636 timeout: Duration,
637 ) -> Result<Box<RawValue>, ResponseFailure> {
638 let mutation = MutationContext::from_operation(&operation);
639 let _wait = RequestWait::begin(timeout)
640 .map_err(|failure| ResponseFailure::new(failure, mutation))?;
641 self.dispatch_text(operation, limit)
642 }
643
644 pub fn dispatch_text(
654 &self,
655 operation: Operation,
656 response_byte_limit: NonZeroUsize,
657 ) -> Result<Box<RawValue>, ResponseFailure> {
658 let mutation = MutationContext::from_operation(&operation);
659 let access = access::request(self.access_source.as_ref())
660 .map_err(|failure| ResponseFailure::new(failure, mutation.clone()))?;
661 let text = self
662 .dispatch_with_entry_budget(&access, operation, Some(response_byte_limit))
663 .and_then(Answer::into_text)
664 .map_err(|failure| ResponseFailure::new(failure, mutation.clone()))?;
665 if text.get().len() > response_byte_limit.get() {
666 return Err(ResponseFailure::after_dispatch(
667 Failure::new(
668 "response_limit",
669 "response exceeds the configured byte budget; use a smaller page",
670 ),
671 mutation,
672 ));
673 }
674 Ok(text)
675 }
676
677 pub fn dispatch_bounded(
685 &self,
686 operation: Operation,
687 response_byte_limit: NonZeroUsize,
688 ) -> Result<Value, ResponseFailure> {
689 let mutation = MutationContext::from_operation(&operation);
690 let access = access::request(self.access_source.as_ref())
691 .map_err(|failure| ResponseFailure::new(failure, mutation.clone()))?;
692 let result = self
693 .dispatch_with_entry_budget(&access, operation, Some(response_byte_limit))
694 .and_then(Answer::into_value)
695 .map_err(|failure| ResponseFailure::new(failure, mutation.clone()))?;
696 if !fits_serialized(&result, response_byte_limit)
697 .map_err(|failure| ResponseFailure::after_dispatch(failure, mutation.clone()))?
698 {
699 return Err(ResponseFailure::after_dispatch(
700 Failure::new(
701 "response_limit",
702 "response exceeds the configured byte budget; use a smaller page",
703 ),
704 mutation,
705 ));
706 }
707 Ok(result)
708 }
709
710 fn dispatch_with_entry_budget(
711 &self,
712 access: &Access,
713 operation: Operation,
714 response_byte_limit: Option<NonZeroUsize>,
715 ) -> Result<Answer, Failure> {
716 match operation {
717 Operation::Mutate(request) => {
718 if matches!(&request.change, Change::Discard(_)) && !access.can_read_store() {
719 return Err(Failure::new(
720 "discard_access_refused",
721 "whole store authority is required",
722 ));
723 }
724 if matches!(&request.change, Change::Merge(_)) && !access.can_read_store() {
726 return Err(Failure::new(
727 "merge_access_refused",
728 "whole store authority is required",
729 ));
730 }
731 let mut writer = self.admission()?;
732 let receipt = writer.mutate_with_append_prune_limits(
733 &request,
734 self.max_page,
735 self.append_prune_limits,
736 )?;
737 let discarded = matches!(&request.change, Change::Discard(_));
739 let release_due = self.releaser.as_ref().is_some_and(|releaser| {
740 discarded || releaser.due(writer.advanced_since_release(&request.branch))
741 });
742 drop(writer);
743 if release_due && let Some(releaser) = &self.releaser {
744 if discarded {
745 releaser.discarded(&request.branch);
746 } else {
747 releaser.wake(&request.branch);
748 }
749 }
750 if let Some(driver) = &self.embedding {
751 wake_embedding(driver, &request.branch, &request.change);
752 }
753 if let Some(gate) = &self.lys
756 && let Some(child) = placement::made_by(&request.change)
757 {
758 gate.placed(
759 child.kind.lys(),
760 &child.name,
761 pass::now(),
762 self.placer.is_some(),
763 );
764 }
765 if let Some(placer) = &self.placer
768 && let Some(child) = placement::made_by(&request.change)
769 {
770 placer.committed(child);
771 }
772 serde_json::to_value(receipt)
773 .map(Answer::Value)
774 .map_err(Failure::storage)
775 }
776 Operation::Receipt(query) => serde_json::to_value(self.admission()?.receipt(&query)?)
777 .map(Answer::Value)
778 .map_err(Failure::storage),
779 Operation::CountsRebuild(request) => {
780 self.counts_rebuild(access, &request).map(Answer::Value)
781 }
782 Operation::EmbedRewalk(request) => {
783 self.embed_rewalk(access, &request).map(Answer::Value)
784 }
785 Operation::QueryRun(request) => self
786 .query_run(access, &request, response_byte_limit)
787 .map(Answer::Text),
788 Operation::ChangesList(request) => self
789 .changes_list(access, &request, response_byte_limit)
790 .map(Answer::Text),
791 read => self
792 .dispatch_read(access, read, response_byte_limit)
793 .map(Answer::Value),
794 }
795 }
796
797 fn dispatch_read(
798 &self,
799 access: &Access,
800 operation: Operation,
801 response_byte_limit: Option<NonZeroUsize>,
802 ) -> Result<Value, Failure> {
803 match operation {
804 Operation::Hello(agreement) => {
805 agreement.validate(agreement.max_frame)?;
806 Ok(
807 json!({"protocol": PROTOCOL, "capabilities": [super::protocol::RECEIPT_CAPABILITY], "max_page": self.max_page}),
808 )
809 }
810 Operation::QueryCheck(request) => Ok(super::query::check::check(&request)),
811 Operation::QueryWords(request) => super::query::check::words(&request),
812 Operation::Branches(page) => self.branch_page(&page),
813 Operation::Collections(page) => {
814 self.collection_page(access, &page, response_byte_limit)
815 }
816 Operation::Get(read) => {
817 let at = self.capture(&read.selection, false)?;
818 let document = reads::document(
819 access,
820 &self.reader,
821 &at,
822 &read.collection,
823 &read.document_id,
824 )?;
825 Ok(json!({"selection":view(&at), "document":document}))
826 }
827 Operation::List(page) => self.document_page(access, &page, response_byte_limit),
828 Operation::Review(page) => self.branch_review(&page),
829 Operation::ReviewCounts(page) => self.branch_review_counts(&page),
830 Operation::MergePrepare(page) => self.branch_merge_prepare(&page),
831 Operation::MergeDangling(page) => self.branch_merge_dangling(&page),
832 Operation::History(page) => self.history_page(&page),
833 Operation::EdgesOut(page) => self.edge_page(access, &page, false, response_byte_limit),
834 Operation::EdgesIn(page) => self.edge_page(access, &page, true, response_byte_limit),
835 Operation::GraphExport(page) => self.graph_export(access, &page, response_byte_limit),
836 Operation::VectorGet(read) => {
837 let at = self.capture(&read.selection, false)?;
838 let vector = super::storage::vectors::get(
839 access,
840 &self.reader,
841 &at,
842 &read.collection,
843 &read.field,
844 &read.document_id,
845 )?;
846 Ok(json!({"selection":view(&at), "vector":vector}))
847 }
848 Operation::VectorList(page) => {
849 let limit = self.limit(page.limit)?;
850 let at = self.capture(&page.selection, page.after.is_some())?;
851 let (vectors, next_after) = super::storage::vectors::list(
852 access,
853 &self.reader,
854 &at,
855 &page.collection,
856 &page.field,
857 page.after.as_deref(),
858 (limit, response_limit(response_byte_limit)?),
859 )?;
860 Ok(json!({"selection":view(&at), "vectors":vectors, "next_after":next_after}))
861 }
862 Operation::VectorSearch(search) => {
863 let k = self.limit(search.k)?;
864 let at = self.capture(&search.selection, false)?;
865 let (hits, scanned) =
866 super::storage::vectors::search(access, &self.reader, &at, &search, k)?;
867 Ok(json!({"selection":view(&at), "hits":hits, "scanned":scanned}))
868 }
869 Operation::VectorSearchText(search) => self.search_text(access, &search),
870 Operation::StoreStats(read) => self.store_stats(access, &read),
871 Operation::CountsAudit(selection) => self.counts_audit(access, &selection),
872 Operation::VectorFields(read) => self.vector_fields(access, &read),
873 Operation::Relations(read) => {
874 let at = self.capture(&read.selection, false)?;
875 let relations = super::storage::relations::list(
876 access,
877 &self.reader,
878 &at,
879 response_limit(response_byte_limit)?,
880 )?;
881 Ok(json!({"selection":view(&at), "relations":relations}))
882 }
883 Operation::Mutate(_)
884 | Operation::Receipt(_)
885 | Operation::CountsRebuild(_)
886 | Operation::EmbedRewalk(_) => Err(Failure::new(
887 "invalid_operation",
888 "mutation must use writer admission",
889 )),
890 Operation::QueryRun(_) | Operation::ChangesList(_) => Err(Failure::new(
891 "invalid_operation",
892 "query.run and changes.list are answered as text",
893 )),
894 Operation::Present(_) | Operation::DraftsRun(_) | Operation::Follow(_) => {
895 Err(Failure::new(
896 "invalid_operation",
897 "session.pass, drafts.run and head.follow are answered by the connection \
898 that asks them, with its pass",
899 ))
900 }
901 }
902 }
903
904 fn history_page(&self, page: &super::protocol::HistoryPage) -> Result<Value, Failure> {
905 let limit = self.limit(page.limit)?;
906 let rows = self
907 .catalogue
908 .history(&page.branch, page.after, limit.get() + 1)?;
909 let more = rows.len() > limit.get();
910 let rows: Vec<_> = rows.into_iter().take(limit.get()).collect();
911 let next = more.then(|| rows.last().map(|row| row.version)).flatten();
912 Ok(json!({"entries":rows.iter().map(view).collect::<Vec<_>>(), "next_after":next}))
913 }
914
915 fn branch_page(&self, page: &BranchPage) -> Result<Value, Failure> {
916 let limit = self.limit(page.limit)?;
917 let rows = self
918 .catalogue
919 .branches(page.after.as_deref(), limit.get() + 1)?;
920 let more = rows.len() > limit.get();
921 let rows: Vec<_> = rows.into_iter().take(limit.get()).collect();
922 let next = more
923 .then(|| rows.last().map(|row| row.branch.clone()))
924 .flatten();
925 Ok(json!({"entries": rows.iter().map(view).collect::<Vec<_>>(), "next_after": next}))
926 }
927
928 fn counts_rebuild(&self, access: &Access, request: &CountsRebuild) -> Result<Value, Failure> {
929 if !access.can_read_store() {
930 return Err(Failure::new(
931 "counts_access_refused",
932 "whole store authority is required",
933 ));
934 }
935 let mut writer = self.admission()?;
936 serde_json::to_value(writer.rebuild_counts(request)?).map_err(Failure::storage)
937 }
938
939 fn embed_rewalk(&self, access: &Access, request: &EmbedRewalk) -> Result<Value, Failure> {
941 if !access.can_read_store() {
942 return Err(Failure::new(
943 "embed_rewalk_access_refused",
944 "whole store authority is required",
945 ));
946 }
947 let Some(driver) = &self.embedding else {
948 return Err(Failure::new(
949 "embed_not_configured",
950 "this service embeds no field",
951 ));
952 };
953 let (branch, collection, field) = (&request.branch, &request.collection, &request.field);
954 let through = self
955 .admission()?
956 .embed_rewalk_longer(branch, collection, field)?;
957 driver.declared(branch, collection, field);
958 Ok(
959 json!({"branch": branch, "collection": collection, "field": field,
960 "through": through}),
961 )
962 }
963
964 fn counts_audit(&self, access: &Access, selection: &Selection) -> Result<Value, Failure> {
965 let at = self.capture(selection, false)?;
966 let mut answer = serde_json::to_value(Storage::audit_counts(access, &self.reader, &at)?)
967 .map_err(Failure::storage)?;
968 answer
969 .as_object_mut()
970 .ok_or_else(|| Failure::new("answer_shape", "audit answer must be an object"))?
971 .insert("selection".to_owned(), view(&at));
972 Ok(answer)
973 }
974
975 fn changes_list(
977 &self,
978 access: &Access,
979 request: &ChangesRequest,
980 response_byte_limit: Option<NonZeroUsize>,
981 ) -> Result<Box<RawValue>, Failure> {
982 let context = changes::Context {
983 access,
984 reader: &self.reader,
985 catalogue: &self.catalogue,
986 settings: &self.changes,
987 seal: &self.query_seal,
988 };
989 changes::list(&context, request, response_byte_limit)
990 }
991
992 fn branch_review(&self, page: &ReviewPage) -> Result<Value, Failure> {
993 let result = super::storage::review::page(
994 &self.reader,
995 &self.catalogue,
996 page,
997 self.limit(page.limit)?,
998 )?;
999 Ok(
1001 json!({"branch": result.selection.branch, "selection": view(&result.selection),
1002 "from": result.from, "diff": result.diff, "next_after": result.next_after,
1003 "changes": null, "conflicts": null, "dangling": null, "base_moved": result.base_moved}),
1004 )
1005 }
1006
1007 fn branch_review_counts(&self, page: &ReviewPage) -> Result<Value, Failure> {
1008 let result = super::storage::review::counts::page(
1009 &self.reader,
1010 &self.catalogue,
1011 page,
1012 self.limit(page.limit)?,
1013 )?;
1014 Ok(json!({
1015 "branch": result.selection.branch, "selection": view(&result.selection),
1016 "from": result.from, "parent": view(&result.parent), "partial": result.partial,
1017 "base_moved": result.base_moved, "done": result.next_after.is_none(),
1018 "next_after": result.next_after,
1019 }))
1020 }
1021
1022 fn branch_merge_prepare(&self, page: &ReviewPage) -> Result<Value, Failure> {
1023 let result = super::storage::review::prepare::page(
1024 &self.reader,
1025 &self.catalogue,
1026 page,
1027 self.limit(page.limit)?,
1028 )?;
1029 Ok(json!({
1030 "branch": result.selection.branch, "selection": view(&result.selection),
1031 "from": result.from, "parent": view(&result.parent), "lines": result.lines,
1032 "done": result.next_after.is_none(), "next_after": result.next_after,
1033 }))
1034 }
1035
1036 fn branch_merge_dangling(&self, page: &ReviewPage) -> Result<Value, Failure> {
1037 let result = super::storage::review::dangling::page(
1038 &self.reader,
1039 &self.catalogue,
1040 page,
1041 self.limit(page.limit)?,
1042 )?;
1043 Ok(json!({
1044 "branch": result.selection.branch, "selection": view(&result.selection),
1045 "from": result.from, "parent": view(&result.parent), "lines": result.lines,
1046 "done": result.next_after.is_none(), "next_after": result.next_after,
1047 }))
1048 }
1049
1050 fn collection_page(
1051 &self,
1052 access: &Access,
1053 page: &CollectionPage,
1054 response_byte_limit: Option<NonZeroUsize>,
1055 ) -> Result<Value, Failure> {
1056 let limit = self.limit(page.limit)?;
1057 let at = self.capture(&page.selection, page.after.is_some())?;
1058 let result = if let Some(byte_limit) = response_byte_limit {
1059 reads::collections_bounded(
1060 access,
1061 &self.reader,
1062 &at,
1063 page.after.as_deref(),
1064 limit,
1065 byte_limit,
1066 )?
1067 } else {
1068 let result = reads::collections(&self.reader, &at, page.after.as_deref(), limit)?;
1069 reads::Page {
1070 branch: result.branch,
1071 version: result.version,
1072 entries: reads::render_collections(access, &self.reader, &at, result.entries)?,
1073 next_after: result.next_after,
1074 }
1075 };
1076 Ok(json!({
1077 "selection": view(&at), "entries":result.entries, "next_after":result.next_after,
1078 }))
1079 }
1080
1081 fn document_page(
1082 &self,
1083 access: &Access,
1084 page: &DocumentPage,
1085 response_byte_limit: Option<NonZeroUsize>,
1086 ) -> Result<Value, Failure> {
1087 reads::validate_document_content(page.content.as_deref())?;
1088 reads::validate_document_prefix(page.prefix.as_deref(), page.after.as_deref())?;
1089 let limit = self.limit(page.limit)?;
1090 let at = self.capture(&page.selection, page.after.is_some())?;
1091 let result =
1092 reads::list_documents(access, &self.reader, &at, page, limit, response_byte_limit)?;
1093 Ok(json!({"selection":view(&at), "entries":result.entries, "next_after":result.next_after}))
1094 }
1095
1096 fn store_stats(&self, access: &Access, read: &StoreRead) -> Result<Value, Failure> {
1097 let at = self.capture(&read.selection, false)?;
1098 let stats = reads::stats(access, &self.reader, &at)?;
1099 let mut answer = json!({
1100 "selection":view(&at), "collections":stats.collections,
1101 "embedded_fields":stats.embedded_fields,
1102 "embedded_fields_reason":stats.embedded_fields_reason,
1103 "live_bytes":null, "history_bytes":null, "reclaimable_bytes":null,
1104 });
1105 stats.documents.insert(&mut answer, "documents")?;
1106 stats.edges.insert(&mut answer, "edges")?;
1107 stats.vectors.insert(&mut answer, "vectors")?;
1108 stats
1109 .collection_count
1110 .insert(&mut answer, "collection_count")?;
1111 stats.relation_count.insert(&mut answer, "relation_count")?;
1112 if let Some(object) = answer.as_object_mut() {
1113 let release = self
1114 .releaser
1115 .as_ref()
1116 .map_or_else(|| json!({"state":"not_started"}), Releaser::status);
1117 object.insert("release".to_owned(), release);
1118 object.insert("discard_pass".to_owned(), self.discard_passes.status());
1119 let embedding = self
1120 .embedding
1121 .as_ref()
1122 .map_or_else(|| json!({"state":"not_started"}), Driver::status);
1123 object.insert("embedding".to_owned(), embedding);
1124 }
1125 Ok(answer)
1126 }
1127
1128 fn vector_fields(&self, access: &Access, read: &StoreRead) -> Result<Value, Failure> {
1129 let at = self.capture(&read.selection, false)?;
1130 let mut listing = super::storage::vectors::fields(access, &self.reader, &at)?;
1131 if let Some(driver) = &self.embedding {
1133 listing.statused(|collection, field| super::storage::vectors::FieldState {
1134 status: driver.field_status(&at.branch, collection, field),
1135 longer_than_model: driver.longer_than_model(&at.branch, collection, field),
1136 embed_input_empty: driver.embed_input_empty(&at.branch, collection, field),
1137 windows: driver.windows(&at.branch, collection, field),
1138 failure: driver.field_failure(&at.branch, collection, field),
1139 });
1140 }
1141 Ok(json!({"selection":view(&at), "fields":listing.fields,
1142 "embedded_fields":listing.embedded_fields}))
1143 }
1144
1145 fn edge_page(
1146 &self,
1147 access: &Access,
1148 page: &EdgePage,
1149 incoming: bool,
1150 response_byte_limit: Option<NonZeroUsize>,
1151 ) -> Result<Value, Failure> {
1152 let limit = self.limit(page.limit)?;
1153 let at = self.capture(&page.selection, page.after.is_some())?;
1154 let query = if incoming {
1155 edges::visible_incoming
1156 } else {
1157 edges::visible_out
1158 };
1159 let result = query(
1160 &self.reader,
1161 &at,
1162 &edges::VisibleEdges {
1163 access,
1164 node: &page.node,
1165 relation: page.relation.as_deref(),
1166 after: page.after.as_deref(),
1167 limits: reads::PageLimits {
1168 rows: limit,
1169 bytes: response_limit(response_byte_limit)?,
1170 },
1171 },
1172 )?;
1173 Ok(json!({"selection":view(&at), "edges":result.entries, "next_after":result.next_after}))
1174 }
1175
1176 fn graph_export(
1177 &self,
1178 access: &Access,
1179 page: &GraphPage,
1180 response_byte_limit: Option<NonZeroUsize>,
1181 ) -> Result<Value, Failure> {
1182 let limit = self.limit(page.limit)?;
1183 let at = self.capture(&page.selection, page.after.is_some())?;
1184 if graph::is_filtered(page) {
1185 let result = graph::export(
1186 &self.reader,
1187 &at,
1188 access,
1189 page,
1190 reads::PageLimits {
1191 rows: limit,
1192 bytes: response_limit(response_byte_limit)?,
1193 },
1194 self.max_page,
1195 )?;
1196 return Ok(json!({"selection":view(&at), "edges":result.edges,
1197 "nodes":result.nodes, "next_after":result.next_after,
1198 "examination_ended":result.examination_ended}));
1199 }
1200 let result = edges::visible_export(
1201 &self.reader,
1202 &at,
1203 access,
1204 page.after.as_deref(),
1205 reads::PageLimits {
1206 rows: limit,
1207 bytes: response_limit(response_byte_limit)?,
1208 },
1209 )?;
1210 Ok(json!({"selection":view(&at), "edges":result.entries, "next_after":result.next_after}))
1211 }
1212
1213 fn capture(&self, selection: &Selection, continuation: bool) -> Result<Held, Failure> {
1217 if continuation && selection.version.is_none() {
1218 return Err(Failure::new(
1219 "version_required",
1220 "continuation requires the captured version",
1221 ));
1222 }
1223 self.catalogue.capture_held(selection)
1224 }
1225
1226 fn limit(&self, limit: NonZeroU32) -> Result<NonZeroUsize, Failure> {
1227 if limit > self.max_page {
1228 return Err(Failure::new(
1229 "page_limit",
1230 format!("maximum page size is {}", self.max_page),
1231 ));
1232 }
1233 let value = usize::try_from(limit.get()).map_err(Failure::storage)?;
1234 NonZeroUsize::new(value)
1235 .filter(|limit| limit.get() < usize::MAX)
1236 .ok_or_else(|| Failure::new("page_limit", "page must allow one lookahead entry"))
1237 }
1238}
1239
1240fn response_limit(limit: Option<NonZeroUsize>) -> Result<NonZeroUsize, Failure> {
1241 let bytes = limit.map_or(usize::MAX, NonZeroUsize::get);
1242 NonZeroUsize::new(bytes)
1243 .ok_or_else(|| Failure::new("response_limit", "response byte limit is zero"))
1244}
1245
1246fn running_limit(settings: Option<&super::query::Settings>) -> Result<NonZeroUsize, Failure> {
1247 let running = settings.map_or(1, |settings| settings.max_running.get());
1248 NonZeroUsize::new(running)
1249 .ok_or_else(|| Failure::new("query_limit", "query running limit is zero"))
1250}
1251
1252fn view(at: &Captured) -> Value {
1253 json!({"branch":at.branch, "version":at.version,
1254 "roots":at.shards.iter().map(|shard| json!({"shard":shard.shard_id, "root":shard.head.to_string()})).collect::<Vec<_>>()})
1255}
1256
1257enum Answer {
1260 Value(Value),
1261 Text(Box<RawValue>),
1262}
1263
1264impl Answer {
1265 fn into_value(self) -> Result<Value, Failure> {
1267 match self {
1268 Self::Value(value) => Ok(value),
1269 Self::Text(text) => serde_json::from_str(text.get()).map_err(Failure::storage),
1270 }
1271 }
1272
1273 fn into_text(self) -> Result<Box<RawValue>, Failure> {
1275 match self {
1276 Self::Value(value) => serde_json::value::to_raw_value(&value).map_err(Failure::storage),
1277 Self::Text(text) => Ok(text),
1278 }
1279 }
1280}
1281
1282pub(super) fn fits_serialized(value: &Value, limit: NonZeroUsize) -> Result<bool, Failure> {
1283 let mut output = CappedWriter {
1284 remaining: limit.get(),
1285 exceeded: false,
1286 };
1287 match serde_json::to_writer(&mut output, value) {
1288 Ok(()) => Ok(true),
1289 Err(_) if output.exceeded => Ok(false),
1290 Err(error) => Err(Failure::storage(error)),
1291 }
1292}
1293
1294struct CappedWriter {
1295 remaining: usize,
1296 exceeded: bool,
1297}
1298
1299impl Write for CappedWriter {
1300 fn write(&mut self, bytes: &[u8]) -> io::Result<usize> {
1301 if bytes.len() > self.remaining {
1302 self.exceeded = true;
1303 return Err(io::Error::other("serialized response exceeds byte limit"));
1304 }
1305 self.remaining -= bytes.len();
1306 Ok(bytes.len())
1307 }
1308
1309 fn flush(&mut self) -> io::Result<()> {
1310 Ok(())
1311 }
1312}
1313
1314fn wake_embedding(driver: &Driver, branch: &str, change: &Change) {
1317 match change {
1318 Change::Put(write) => driver.wrote(branch, &write.collection),
1319 Change::AppendPrune(write) => driver.wrote(branch, &write.collection),
1320 Change::Delete(delete) => driver.wrote(branch, &delete.collection),
1321 Change::DeleteMany(delete) => driver.wrote(branch, &delete.collection),
1322 Change::PutMany(import) => driver.wrote(branch, &import.collection),
1323 Change::VectorDeclare(declare) => {
1324 driver.declared(branch, &declare.collection, &declare.field);
1325 }
1326 _ => {}
1327 }
1328}
1329
1330#[cfg(test)]
1331#[path = "service_tests.rs"]
1332mod tests;
1333
1334#[cfg(test)]
1335#[path = "service_merge_prepare_tests.rs"]
1336mod merge_prepare_tests;
1337
1338#[cfg(test)]
1339#[path = "service_relationship_access_tests.rs"]
1340mod relationship_access_tests;
1341
1342#[cfg(test)]
1343#[path = "service_access_tests.rs"]
1344mod access_tests;
1345
1346#[cfg(test)]
1347#[path = "service_mutation_outcome_tests.rs"]
1348mod mutation_outcome_tests;
1349
1350#[cfg(test)]
1351#[path = "service_held_tests.rs"]
1352mod held_tests;
1353
1354#[cfg(test)]
1355mod count_access_tests {
1356 use super::Service;
1357 use crate::serve::access::TestSource;
1358 use crate::serve::protocol::{CountsRebuild, Operation, Selection};
1359 use crate::serve::storage::Storage;
1360 use haematite::db::DatabaseConfig;
1361 use haematite::store::NodeCacheBudget;
1362 use std::error::Error;
1363 use std::num::NonZeroU32;
1364 use std::sync::Arc;
1365
1366 #[test]
1367 fn whole_store_authority_is_required_without_disclosing_or_rebuilding()
1368 -> Result<(), Box<dyn Error>> {
1369 let directory = tempfile::tempdir()?;
1370 let storage = Storage::create(
1371 DatabaseConfig {
1372 data_dir: directory.path().join("database"),
1373 shard_count: 1,
1374 distributed: None,
1375 executor_threads: Some(1),
1376 node_cache_budget: Some(NodeCacheBudget::Unlimited),
1377 retention: haematite::Retention::KeepAll,
1378 },
1379 NodeCacheBudget::Unlimited,
1380 fixture_long_wait_interval(),
1381 )?;
1382 let before = storage
1383 .capture(&Selection {
1384 branch: "main".to_owned(),
1385 version: None,
1386 })?
1387 .version;
1388 let source = Arc::new(TestSource::denying(&[("records", "one")]));
1389 let service = Service::with_access_source(storage, NonZeroU32::MIN, None, source)?;
1390 let error = service
1391 .dispatch(Operation::CountsRebuild(CountsRebuild {
1392 branch: "main".to_owned(),
1393 request_id: "withheld".to_owned(),
1394 }))
1395 .err()
1396 .ok_or("restricted rebuild was admitted")?;
1397 let error = serde_json::to_value(error)?;
1398 assert_eq!(error["code"], "counts_access_refused");
1399 assert_eq!(error["outcome"], "not_applied");
1400 let answer = service.dispatch(Operation::CountsAudit(Selection {
1401 branch: "main".to_owned(),
1402 version: None,
1403 }))?;
1404 assert_eq!(answer["result"], "cannot_tell");
1405 assert_eq!(answer["reason"], "withheld");
1406 assert!(answer.get("buckets").is_none());
1407 assert_eq!(
1408 answer["selection"]["version"]["sequence"],
1409 before.sequence.to_string()
1410 );
1411 Ok(())
1412 }
1413
1414 const fn fixture_long_wait_interval() -> std::time::Duration {
1415 std::time::Duration::from_secs(1)
1416 }
1417}
1418
1419#[cfg(test)]
1420#[path = "service_writer_wait_tests.rs"]
1421mod writer_wait_tests;