Skip to main content

haematite_cli/serve/
service.rs

1//! Bounded request dispatch. Socket handling owns framing and client admission.
2
3mod 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    /// The Lys gate every request on a connection is decided by; none for an
74    /// owner serving without a directory.
75    lys: Option<Arc<Gate>>,
76    storage: Arc<Mutex<Storage>>,
77    writer_available: Arc<WriterAvailable>,
78    reader: ReadOnlyDatabase,
79    catalogue: Catalogue,
80    /// What came of the last discard's ask for a reclaim pass, for `store.stats`.
81    discard_passes: DiscardPasses,
82    pub max_page: NonZeroU32,
83    pub append_prune_limits: super::storage::AppendPruneLimits,
84    pub query: Option<Prepared>,
85    /// The one limit on queries running at once, made from the settings.
86    query_slots: Slots,
87    /// The key this service's continuations are sealed with, made with it.
88    query_seal: Seal,
89    /// The named limits of `changes.list`.
90    pub changes: changes::Settings,
91    /// The release thread, once started; none in a service built for a test.
92    releaser: Option<Releaser>,
93    /// The embedding thread, once started; none without embed settings.
94    embedding: Option<Driver>,
95    /// The most vectors one text search compares: `query_scan_records`.
96    pub embed_search_bound: Option<NonZeroU64>,
97    /// Places each new collection and branch in Lys after its write commits
98    /// (serve/placement.rs); none unless configured.
99    placer: Option<Placer>,
100}
101
102impl Service {
103    /// # Errors
104    ///
105    /// Propagates any error from `reader`.
106    pub fn new(storage: Storage, max_page: NonZeroU32) -> Result<Self, Failure> {
107        Self::with_query(storage, max_page, None)
108    }
109
110    /// Build the service with optional prepared query settings.
111    ///
112    /// # Errors
113    ///
114    /// Returns a storage reader, query admission, or continuation-key refusal.
115    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    /// Decide every request through Lys (ACCESS-001 R2): a connection
158    /// presents its pass with `session.pass`, and nothing in configuration
159    /// grants a writer.
160    #[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    /// Place this store's collections and branches in Lys under its database
170    /// (serve/placement.rs): every one it holds now, handed to `placer` at
171    /// once, then each new one after the write that makes it commits.
172    ///
173    /// # Errors
174    ///
175    /// The store's branches or collections could not be listed.
176    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    /// Whether every request is decided through Lys. A service that decides
186    /// nothing is its owner's alone and binds no network endpoint
187    /// (HAEMNET-001 R2).
188    #[must_use]
189    pub const fn decides(&self) -> bool {
190        self.lys.is_some()
191    }
192
193    /// Verify a pass a connection presents, answering `{holder, expires}`.
194    ///
195    /// # Errors
196    ///
197    /// Refuses with the pass's stable name, or `invalid_operation` when this
198    /// service is not served through Lys.
199    pub fn present(&self, token: &str) -> Result<Value, Failure> {
200        let Some(gate) = &self.lys else {
201            return Err(Failure::new(
202                "invalid_operation",
203                "this service is not served through Lys, so it takes no pass",
204            ));
205        };
206        gate.present(token, pass::now())
207    }
208
209    /// Run one page of the drafts Lys approved for this database, with the
210    /// connection's pass `token` as the connector's (ACCESS-001 R2).
211    ///
212    /// # Errors
213    ///
214    /// `pass_missing` without a pass, `invalid_operation` when this service
215    /// is not served through Lys, or where the run stopped, by name.
216    pub fn run_drafts(&self, token: Option<&str>, run: &DraftsRun) -> Result<Value, Failure> {
217        let Some(gate) = &self.lys else {
218            return Err(Failure::new(
219                "invalid_operation",
220                "this service is not served through Lys, so it runs no drafts",
221            ));
222        };
223        gate.run_drafts(self, token, run.after.as_deref())
224    }
225
226    /// Carry the request's writer deadline through approved draft execution.
227    pub(in crate::serve) fn run_drafts_waiting(
228        &self,
229        token: Option<&str>,
230        run: &DraftsRun,
231        timeout: Duration,
232    ) -> Result<Value, Failure> {
233        let _wait = RequestWait::begin(timeout)?;
234        self.run_drafts(token, run)
235    }
236
237    /// Follow `branch`'s head for one connection, giving each head frame to
238    /// `send`, with the connection's pass expiring at `expires` seconds since
239    /// the epoch as `clock` reads it (serve/follow.rs). The route was decided
240    /// before this, as `head.follow`.
241    ///
242    /// Answers the refusal that ends the follow.
243    ///
244    /// # Errors
245    ///
246    /// The first error `send` returns: the client went away.
247    pub fn follow(
248        &self,
249        branch: &str,
250        expires: Option<u64>,
251        clock: impl Fn() -> std::time::SystemTime,
252        send: impl FnMut(&Value) -> io::Result<()>,
253    ) -> io::Result<Failure> {
254        super::follow::follow(&self.catalogue, branch, expires, clock, send)
255    }
256
257    pub(in crate::serve) fn follow_socket(
258        &self,
259        peer: std::os::fd::OwnedFd,
260        branch: &str,
261        expires: Option<u64>,
262        send: impl FnMut(&Value) -> io::Result<()>,
263    ) -> io::Result<Failure> {
264        use std::sync::atomic::{AtomicBool, Ordering};
265        let cancelled = Arc::new(AtomicBool::new(false));
266        let closed = Arc::clone(&cancelled);
267        let catalogue = self.catalogue.clone();
268        let _monitor = super::follow::Monitor::start(peer, move || {
269            closed.store(true, Ordering::Release);
270            catalogue.wake_followers();
271        })?;
272        super::follow::follow_cancellable(
273            &self.catalogue,
274            branch,
275            expires,
276            std::time::SystemTime::now,
277            &cancelled,
278            send,
279        )
280    }
281
282    /// Decide `operation` from the connection's pass before it is dispatched.
283    /// `words` are the request's exact text, drafted with it when Lys holds
284    /// it for approval. A service without a Lys gate is its owner's, and
285    /// decides nothing.
286    ///
287    /// # Errors
288    ///
289    /// Returns the first refusal of the request's subjects.
290    pub fn authorize(
291        &self,
292        token: Option<&str>,
293        operation: &Operation,
294        words: Option<&str>,
295    ) -> Result<(), Failure> {
296        let Some(gate) = &self.lys else {
297            return Ok(());
298        };
299        let absent = |kind: &str, name: &str| match kind {
300            "branch" => self.catalogue.head(name).is_ok_and(|head| head.is_none()),
301            "collection" => self.collection_absent(operation, name),
302            _ => false,
303        };
304        gate.authorize_with(token, operation, words, pass::now(), &absent)
305            .map(drop)
306    }
307
308    /// Whether a change's branch holds no collection named `collection`, at
309    /// its head. Anything but a change, or a read that fails, answers `false`,
310    /// so the collection's own refusal stands.
311    fn collection_absent(&self, operation: &Operation, collection: &str) -> bool {
312        let Operation::Mutate(mutation) = operation else {
313            return false;
314        };
315        self.catalogue
316            .capture_held(&Selection {
317                branch: mutation.branch.clone(),
318                version: None,
319            })
320            .and_then(|at| reads::collection(&self.reader, &at, collection))
321            .is_ok_and(|found| found.is_none())
322    }
323
324    /// Starts the release thread over this service's storage. A write that
325    /// finds a release running waits for it rather than being told `busy`.
326    ///
327    /// # Errors
328    ///
329    /// The decisions left at open could not be read.
330    pub fn start_release(&mut self, settings: releaser::Settings) -> Result<(), Failure> {
331        self.releaser = Some(Releaser::start(Arc::clone(&self.storage), settings)?);
332        Ok(())
333    }
334
335    /// Starts the embedding thread over this service's storage with
336    /// `worker`; the worker starts on that thread, off the serving path.
337    ///
338    /// # Errors
339    ///
340    /// The declared fields could not be read, or the thread not started.
341    pub fn start_embedding(
342        &mut self,
343        worker: Arc<dyn Worker>,
344        bounds: Bounds,
345        writer_wait: std::time::Duration,
346    ) -> Result<(), Failure> {
347        self.start_embedding_with(worker, bounds, writer_wait, Restart::default())
348    }
349
350    /// As [`Self::start_embedding`], with a worker that dies started again
351    /// as `restart` says (EWR1).
352    ///
353    /// # Errors
354    ///
355    /// The declared fields could not be read, or the thread not started.
356    pub fn start_embedding_with(
357        &mut self,
358        worker: Arc<dyn Worker>,
359        bounds: Bounds,
360        writer_wait: std::time::Duration,
361        restart: Restart,
362    ) -> Result<(), Failure> {
363        self.start_embedding_apart(worker, None, bounds, writer_wait, restart)
364    }
365
366    /// As [`Self::start_embedding_with`], with every text search's question
367    /// asked of `questions` when given, a worker of its own that the walk
368    /// never uses (SL1). The question worker never commits, so it never
369    /// takes the admission; the walk's commits go through it (JOIN1).
370    ///
371    /// # Errors
372    ///
373    /// The declared fields could not be read, or the thread not started.
374    pub fn start_embedding_apart(
375        &mut self,
376        worker: Arc<dyn Worker>,
377        questions: Option<Arc<dyn Worker>>,
378        bounds: Bounds,
379        writer_wait: std::time::Duration,
380        restart: Restart,
381    ) -> Result<(), Failure> {
382        let storage = Arc::clone(&self.storage);
383        let driver = Driver::start_apart(storage, worker, questions, bounds, writer_wait, restart)?;
384        self.embedding = Some(driver);
385        Ok(())
386    }
387
388    /// The embedding thread, once started.
389    #[must_use]
390    pub const fn embedding(&self) -> Option<&Driver> {
391        self.embedding.as_ref()
392    }
393
394    #[cfg(test)]
395    pub(in crate::serve) fn start_embedding_steps(
396        &mut self,
397        worker: Arc<dyn Worker>,
398        bounds: Bounds,
399        restart: Restart,
400    ) -> Result<(), Failure> {
401        self.embedding = Some(Driver::for_steps(&self.storage, worker, bounds, restart)?);
402        Ok(())
403    }
404
405    #[cfg(test)]
406    pub(in crate::serve) fn embedding_step_at(
407        &self,
408        bounds: Bounds,
409        now: std::time::Instant,
410    ) -> Result<bool, String> {
411        self.embedding
412            .as_ref()
413            .ok_or("embedding driver missing")?
414            .step_at(&self.storage, bounds, now)
415    }
416
417    /// The release thread, once started.
418    #[must_use]
419    pub const fn releaser(&self) -> Option<&Releaser> {
420        self.releaser.as_ref()
421    }
422
423    pub(crate) fn activate_reclaim(&self) -> Result<(), Failure> {
424        self.admission()?.activate_reclaim()
425    }
426
427    /// Wait until no reclaim pass runs, so a test can look at the files.
428    ///
429    /// # Errors
430    ///
431    /// The writer is held or the pass failed.
432    #[cfg(test)]
433    pub fn quiesce_reclaim(&self) -> Result<(), Failure> {
434        self.admission()?.quiesce_reclaim()
435    }
436
437    /// Each shard's reclaim (SL2: a test's diagnosis).
438    ///
439    /// # Errors
440    /// `writer_unavailable` when the storage lock is poisoned.
441    #[cfg(test)]
442    pub fn reclaim_status(&self) -> Result<haematite::db::ReclaimStatus, Failure> {
443        Ok(self.admission()?.reclaim_status())
444    }
445
446    fn releasing(&self) -> bool {
447        self.releaser.as_ref().is_some_and(Releaser::is_releasing)
448    }
449
450    fn admission(&self) -> Result<Guard<'_, Storage>, Failure> {
451        let Some((end, timeout)) = WRITER_DEADLINE.get() else {
452            return self.admission_now();
453        };
454        let started = Instant::now();
455        loop {
456            if Instant::now() >= end {
457                return Err(Failure::new(
458                    "busy",
459                    format!(
460                        "writer lock wait reached timeout_ms={} after {} ms",
461                        timeout.as_millis(),
462                        started.elapsed().as_millis()
463                    ),
464                ));
465            }
466            match self.admission_now() {
467                Ok(guard) => {
468                    if Instant::now() < end {
469                        return Ok(guard);
470                    }
471                    drop(guard);
472                    return Err(Failure::new(
473                        "busy",
474                        format!(
475                            "writer lock wait reached timeout_ms={} after {} ms",
476                            timeout.as_millis(),
477                            started.elapsed().as_millis()
478                        ),
479                    ));
480                }
481                Err(failure) if failure.code == "busy" => {
482                    self.wait_writer(end, started, timeout)?;
483                }
484                Err(failure) => return Err(failure),
485            }
486        }
487    }
488
489    fn wait_writer(
490        &self,
491        end: Instant,
492        started: Instant,
493        timeout: Duration,
494    ) -> Result<(), Failure> {
495        let unavailable = || {
496            Failure::new(
497                "writer_unavailable",
498                "writer wait state requires process recovery",
499            )
500        };
501        let busy = || {
502            Failure::new(
503                "busy",
504                format!(
505                    "writer lock wait reached timeout_ms={} after {} ms",
506                    timeout.as_millis(),
507                    started.elapsed().as_millis()
508                ),
509            )
510        };
511        if Instant::now() >= end {
512            return Err(busy());
513        }
514        let waited = {
515            let mut active = self
516                .writer_available
517                .active
518                .lock()
519                .map_err(|_| unavailable())?;
520            if !*active {
521                *active = true;
522                #[cfg(test)]
523                self.writer_available.changed.notify_all();
524                let storage = Arc::clone(&self.storage);
525                let ready = Arc::clone(&self.writer_available);
526                if let Err(error) = std::thread::Builder::new()
527                    .name("haematite-writer-ready".to_owned())
528                    .spawn(move || {
529                        // Waiting for the lock never carries a mutation beyond its caller's deadline.
530                        match storage.lock() {
531                            Ok(guard) => drop(guard),
532                            Err(error) => eprintln!("haematite writer_unavailable: {error}"),
533                        }
534                        match ready.active.lock() {
535                            Ok(mut active) => {
536                                *active = false;
537                                ready.changed.notify_all();
538                            }
539                            Err(error) => {
540                                eprintln!("haematite writer_wait_unavailable: {error}");
541                                ready.changed.notify_all();
542                            }
543                        }
544                    })
545                {
546                    *active = false;
547                    self.writer_available.changed.notify_all();
548                    return Err(Failure::new(
549                        "writer_unavailable",
550                        format!("writer wait thread could not start: {error}"),
551                    ));
552                }
553            }
554            let left = end
555                .checked_duration_since(Instant::now())
556                .filter(|left| !left.is_zero())
557                .ok_or_else(busy)?;
558            self.writer_available
559                .changed
560                .wait_timeout_while(active, left, |active| *active)
561                .map_err(|_| unavailable())
562        }?;
563        let timed_out = waited.1.timed_out();
564        drop(waited);
565        if timed_out {
566            return Err(busy());
567        }
568        Ok(())
569    }
570
571    fn admission_now(&self) -> Result<Guard<'_, Storage>, Failure> {
572        if let Some(driver) = &self.embedding {
573            return match WRITER_DEADLINE.get() {
574                Some((end, _)) => driver.admission().foreground_until(&self.storage, end),
575                None => driver
576                    .admission()
577                    .foreground(&self.storage, self.releasing()),
578            };
579        }
580        let unavailable = || {
581            Failure::new(
582                "writer_unavailable",
583                "writer state requires process recovery",
584            )
585        };
586        match self.storage.try_lock() {
587            Ok(guard) => Ok(Guard::plain(guard)),
588            Err(TryLockError::WouldBlock)
589                if self.releasing() && WRITER_DEADLINE.get().is_none() =>
590            {
591                self.storage
592                    .lock()
593                    .map(Guard::plain)
594                    .map_err(|_| unavailable())
595            }
596            Err(TryLockError::WouldBlock) => Err(Failure::new(
597                "busy",
598                "another mutation is being admitted; retry this request identity",
599            )),
600            Err(TryLockError::Poisoned(_)) => Err(unavailable()),
601        }
602    }
603
604    /// # Errors
605    ///
606    /// Returns a refusal with mutation context if dispatch fails.
607    /// The answer as a value, for a caller in this process. A stored body in
608    /// a `query.run` or `changes.list` answer is equal as a value to what was
609    /// written, but a value cannot keep a number's text (`1e2` becomes `1e+2`):
610    /// the transport answers through [`Self::dispatch_text`] (NB1).
611    pub fn dispatch(&self, operation: Operation) -> Result<Value, ResponseFailure> {
612        let mutation = MutationContext::from_operation(&operation);
613        let result = access::request(self.access_source.as_ref())
614            .and_then(|access| self.dispatch_with_entry_budget(&access, operation, None))
615            .and_then(Answer::into_value);
616        result.map_err(|failure| ResponseFailure::new(failure, mutation))
617    }
618
619    /// Dispatch with the request's configured bound on writer admission.
620    ///
621    /// # Errors
622    ///
623    /// Returns a dispatch refusal, invalid timeout, or `busy` naming an exhausted wait.
624    pub fn dispatch_waiting(
625        &self,
626        operation: Operation,
627        timeout: std::time::Duration,
628    ) -> Result<Value, ResponseFailure> {
629        let mutation = MutationContext::from_operation(&operation);
630        let _wait = RequestWait::begin(timeout)
631            .map_err(|failure| ResponseFailure::new(failure, mutation))?;
632        self.dispatch(operation)
633    }
634
635    /// Dispatch response text with the request's writer admission bound.
636    ///
637    /// # Errors
638    ///
639    /// Returns a dispatch refusal, invalid timeout, or `busy` naming an exhausted wait.
640    pub fn dispatch_text_waiting(
641        &self,
642        operation: Operation,
643        limit: NonZeroUsize,
644        timeout: Duration,
645    ) -> Result<Box<RawValue>, ResponseFailure> {
646        let mutation = MutationContext::from_operation(&operation);
647        let _wait = RequestWait::begin(timeout)
648            .map_err(|failure| ResponseFailure::new(failure, mutation))?;
649        self.dispatch_text(operation, limit)
650    }
651
652    /// The answer as the text the transport writes (NB1): a `query.run` or
653    /// `changes.list` answer carries every stored body as the text it is
654    /// stored as, never decoded and encoded again; any other answer is its
655    /// value written once. Bounded as [`Self::dispatch_bounded`].
656    ///
657    /// # Errors
658    ///
659    /// Returns a refusal with mutation context if dispatch fails or the text
660    /// is over `response_byte_limit`.
661    pub fn dispatch_text(
662        &self,
663        operation: Operation,
664        response_byte_limit: NonZeroUsize,
665    ) -> Result<Box<RawValue>, ResponseFailure> {
666        let mutation = MutationContext::from_operation(&operation);
667        let access = access::request(self.access_source.as_ref())
668            .map_err(|failure| ResponseFailure::new(failure, mutation.clone()))?;
669        let text = self
670            .dispatch_with_entry_budget(&access, operation, Some(response_byte_limit))
671            .and_then(Answer::into_text)
672            .map_err(|failure| ResponseFailure::new(failure, mutation.clone()))?;
673        if text.get().len() > response_byte_limit.get() {
674            return Err(ResponseFailure::after_dispatch(
675                Failure::new(
676                    "response_limit",
677                    "response exceeds the configured byte budget; use a smaller page",
678                ),
679                mutation,
680            ));
681        }
682        Ok(text)
683    }
684
685    /// Dispatch with a cumulative serialized-entry budget for collection and
686    /// document pages, then verify the complete result against the same bound.
687    /// The transport still owns the final envelope serialization and frame cap.
688    ///
689    /// # Errors
690    ///
691    /// Propagates any error from `dispatch_with_entry_budget` or `fits_serialized`.
692    pub fn dispatch_bounded(
693        &self,
694        operation: Operation,
695        response_byte_limit: NonZeroUsize,
696    ) -> Result<Value, ResponseFailure> {
697        let mutation = MutationContext::from_operation(&operation);
698        let access = access::request(self.access_source.as_ref())
699            .map_err(|failure| ResponseFailure::new(failure, mutation.clone()))?;
700        let result = self
701            .dispatch_with_entry_budget(&access, operation, Some(response_byte_limit))
702            .and_then(Answer::into_value)
703            .map_err(|failure| ResponseFailure::new(failure, mutation.clone()))?;
704        if !fits_serialized(&result, response_byte_limit)
705            .map_err(|failure| ResponseFailure::after_dispatch(failure, mutation.clone()))?
706        {
707            return Err(ResponseFailure::after_dispatch(
708                Failure::new(
709                    "response_limit",
710                    "response exceeds the configured byte budget; use a smaller page",
711                ),
712                mutation,
713            ));
714        }
715        Ok(result)
716    }
717
718    fn dispatch_with_entry_budget(
719        &self,
720        access: &Access,
721        operation: Operation,
722        response_byte_limit: Option<NonZeroUsize>,
723    ) -> Result<Answer, Failure> {
724        match operation {
725            Operation::Mutate(request) => {
726                if matches!(&request.change, Change::Discard(_)) && !access.can_read_store() {
727                    return Err(Failure::new(
728                        "discard_access_refused",
729                        "whole store authority is required",
730                    ));
731                }
732                // A merge carries every key the source changed.
733                if matches!(&request.change, Change::Merge(_)) && !access.can_read_store() {
734                    return Err(Failure::new(
735                        "merge_access_refused",
736                        "whole store authority is required",
737                    ));
738                }
739                let mut writer = self.admission()?;
740                let receipt = writer.mutate_with_append_prune_limits(
741                    &request,
742                    self.max_page,
743                    self.append_prune_limits,
744                )?;
745                // A discard leaves rows of its generation for release to take.
746                let discarded = matches!(&request.change, Change::Discard(_));
747                let release_due = self.releaser.as_ref().is_some_and(|releaser| {
748                    discarded || releaser.due(writer.advanced_since_release(&request.branch))
749                });
750                drop(writer);
751                if release_due && let Some(releaser) = &self.releaser {
752                    if discarded {
753                        releaser.discarded(&request.branch);
754                    } else {
755                        releaser.wake(&request.branch);
756                    }
757                }
758                if let Some(driver) = &self.embedding {
759                    wake_embedding(driver, &request.branch, &request.change);
760                }
761                // A pass issued before this commit cannot carry what it made;
762                // the gate asks Lys live for it (serve/pass.rs).
763                if let Some(gate) = &self.lys
764                    && let Some(child) = placement::made_by(&request.change)
765                {
766                    gate.placed(
767                        child.kind.lys(),
768                        &child.name,
769                        pass::now(),
770                        self.placer.is_some(),
771                    );
772                }
773                // After the commit, never before: a placement that fails
774                // leaves the write standing (serve/placement.rs).
775                if let Some(placer) = &self.placer
776                    && let Some(child) = placement::made_by(&request.change)
777                {
778                    placer.committed(child);
779                }
780                serde_json::to_value(receipt)
781                    .map(Answer::Value)
782                    .map_err(Failure::storage)
783            }
784            Operation::Receipt(query) => serde_json::to_value(self.admission()?.receipt(&query)?)
785                .map(Answer::Value)
786                .map_err(Failure::storage),
787            Operation::CountsRebuild(request) => {
788                self.counts_rebuild(access, &request).map(Answer::Value)
789            }
790            Operation::EmbedRewalk(request) => {
791                self.embed_rewalk(access, &request).map(Answer::Value)
792            }
793            Operation::QueryRun(request) => self
794                .query_run(access, &request, response_byte_limit)
795                .map(Answer::Text),
796            Operation::ChangesList(request) => self
797                .changes_list(access, &request, response_byte_limit)
798                .map(Answer::Text),
799            read => self
800                .dispatch_read(access, read, response_byte_limit)
801                .map(Answer::Value),
802        }
803    }
804
805    fn dispatch_read(
806        &self,
807        access: &Access,
808        operation: Operation,
809        response_byte_limit: Option<NonZeroUsize>,
810    ) -> Result<Value, Failure> {
811        match operation {
812            Operation::Hello(agreement) => {
813                agreement.validate(agreement.max_frame)?;
814                Ok(
815                    json!({"protocol": PROTOCOL, "capabilities": [super::protocol::RECEIPT_CAPABILITY], "max_page": self.max_page}),
816                )
817            }
818            Operation::QueryCheck(request) => Ok(super::query::check::check(&request)),
819            Operation::QueryWords(request) => super::query::check::words(&request),
820            Operation::Branches(page) => self.branch_page(&page),
821            Operation::Collections(page) => {
822                self.collection_page(access, &page, response_byte_limit)
823            }
824            Operation::Get(read) => {
825                let at = self.capture(&read.selection, false)?;
826                let document = reads::document(
827                    access,
828                    &self.reader,
829                    &at,
830                    &read.collection,
831                    &read.document_id,
832                )?;
833                Ok(json!({"selection":view(&at), "document":document}))
834            }
835            Operation::List(page) => self.document_page(access, &page, response_byte_limit),
836            Operation::Review(page) => self.branch_review(&page),
837            Operation::ReviewCounts(page) => self.branch_review_counts(&page),
838            Operation::MergePrepare(page) => self.branch_merge_prepare(&page),
839            Operation::MergeDangling(page) => self.branch_merge_dangling(&page),
840            Operation::History(page) => self.history_page(&page),
841            Operation::EdgesOut(page) => self.edge_page(access, &page, false, response_byte_limit),
842            Operation::EdgesIn(page) => self.edge_page(access, &page, true, response_byte_limit),
843            Operation::GraphExport(page) => self.graph_export(access, &page, response_byte_limit),
844            Operation::VectorGet(read) => {
845                let at = self.capture(&read.selection, false)?;
846                let vector = super::storage::vectors::get(
847                    access,
848                    &self.reader,
849                    &at,
850                    &read.collection,
851                    &read.field,
852                    &read.document_id,
853                )?;
854                Ok(json!({"selection":view(&at), "vector":vector}))
855            }
856            Operation::VectorList(page) => {
857                let limit = self.limit(page.limit)?;
858                let at = self.capture(&page.selection, page.after.is_some())?;
859                let (vectors, next_after) = super::storage::vectors::list(
860                    access,
861                    &self.reader,
862                    &at,
863                    &page.collection,
864                    &page.field,
865                    page.after.as_deref(),
866                    (limit, response_limit(response_byte_limit)?),
867                )?;
868                Ok(json!({"selection":view(&at), "vectors":vectors, "next_after":next_after}))
869            }
870            Operation::VectorSearch(search) => {
871                let k = self.limit(search.k)?;
872                let at = self.capture(&search.selection, false)?;
873                let (hits, scanned) =
874                    super::storage::vectors::search(access, &self.reader, &at, &search, k)?;
875                Ok(json!({"selection":view(&at), "hits":hits, "scanned":scanned}))
876            }
877            Operation::VectorSearchText(search) => self.search_text(access, &search),
878            Operation::StoreStats(read) => self.store_stats(access, &read),
879            Operation::CountsAudit(selection) => self.counts_audit(access, &selection),
880            Operation::VectorFields(read) => self.vector_fields(access, &read),
881            Operation::Relations(read) => {
882                let at = self.capture(&read.selection, false)?;
883                let relations = super::storage::relations::list(
884                    access,
885                    &self.reader,
886                    &at,
887                    response_limit(response_byte_limit)?,
888                )?;
889                Ok(json!({"selection":view(&at), "relations":relations}))
890            }
891            Operation::Mutate(_)
892            | Operation::Receipt(_)
893            | Operation::CountsRebuild(_)
894            | Operation::EmbedRewalk(_) => Err(Failure::new(
895                "invalid_operation",
896                "mutation must use writer admission",
897            )),
898            Operation::QueryRun(_) | Operation::ChangesList(_) => Err(Failure::new(
899                "invalid_operation",
900                "query.run and changes.list are answered as text",
901            )),
902            Operation::Present(_) | Operation::DraftsRun(_) | Operation::Follow(_) => {
903                Err(Failure::new(
904                    "invalid_operation",
905                    "session.pass, drafts.run and head.follow are answered by the connection \
906                     that asks them, with its pass",
907                ))
908            }
909        }
910    }
911
912    fn history_page(&self, page: &super::protocol::HistoryPage) -> Result<Value, Failure> {
913        let limit = self.limit(page.limit)?;
914        let rows = self
915            .catalogue
916            .history(&page.branch, page.after, limit.get() + 1)?;
917        let more = rows.len() > limit.get();
918        let rows: Vec<_> = rows.into_iter().take(limit.get()).collect();
919        let next = more.then(|| rows.last().map(|row| row.version)).flatten();
920        Ok(json!({"entries":rows.iter().map(view).collect::<Vec<_>>(), "next_after":next}))
921    }
922
923    fn branch_page(&self, page: &BranchPage) -> Result<Value, Failure> {
924        let limit = self.limit(page.limit)?;
925        let rows = self
926            .catalogue
927            .branches(page.after.as_deref(), limit.get() + 1)?;
928        let more = rows.len() > limit.get();
929        let rows: Vec<_> = rows.into_iter().take(limit.get()).collect();
930        let next = more
931            .then(|| rows.last().map(|row| row.branch.clone()))
932            .flatten();
933        Ok(json!({"entries": rows.iter().map(view).collect::<Vec<_>>(), "next_after": next}))
934    }
935
936    fn counts_rebuild(&self, access: &Access, request: &CountsRebuild) -> Result<Value, Failure> {
937        if !access.can_read_store() {
938            return Err(Failure::new(
939                "counts_access_refused",
940                "whole store authority is required",
941            ));
942        }
943        let mut writer = self.admission()?;
944        serde_json::to_value(writer.rebuild_counts(request)?).map_err(Failure::storage)
945    }
946
947    /// EMB-E2 (j): opens one field's rewalk, then wakes its walk.
948    fn embed_rewalk(&self, access: &Access, request: &EmbedRewalk) -> Result<Value, Failure> {
949        if !access.can_read_store() {
950            return Err(Failure::new(
951                "embed_rewalk_access_refused",
952                "whole store authority is required",
953            ));
954        }
955        let Some(driver) = &self.embedding else {
956            return Err(Failure::new(
957                "embed_not_configured",
958                "this service embeds no field",
959            ));
960        };
961        let (branch, collection, field) = (&request.branch, &request.collection, &request.field);
962        let through = self
963            .admission()?
964            .embed_rewalk_longer(branch, collection, field)?;
965        driver.declared(branch, collection, field);
966        Ok(
967            json!({"branch": branch, "collection": collection, "field": field,
968            "through": through}),
969        )
970    }
971
972    fn counts_audit(&self, access: &Access, selection: &Selection) -> Result<Value, Failure> {
973        let at = self.capture(selection, false)?;
974        let mut answer = serde_json::to_value(Storage::audit_counts(access, &self.reader, &at)?)
975            .map_err(Failure::storage)?;
976        answer
977            .as_object_mut()
978            .ok_or_else(|| Failure::new("answer_shape", "audit answer must be an object"))?
979            .insert("selection".to_owned(), view(&at));
980        Ok(answer)
981    }
982
983    /// Reads at two captures of its own and never takes the writer.
984    fn changes_list(
985        &self,
986        access: &Access,
987        request: &ChangesRequest,
988        response_byte_limit: Option<NonZeroUsize>,
989    ) -> Result<Box<RawValue>, Failure> {
990        let context = changes::Context {
991            access,
992            reader: &self.reader,
993            catalogue: &self.catalogue,
994            settings: &self.changes,
995            seal: &self.query_seal,
996        };
997        changes::list(&context, request, response_byte_limit)
998    }
999
1000    fn branch_review(&self, page: &ReviewPage) -> Result<Value, Failure> {
1001        let result = super::storage::review::page(
1002            &self.reader,
1003            &self.catalogue,
1004            page,
1005            self.limit(page.limit)?,
1006        )?;
1007        // Whole-review totals are collected through the separate counting pages.
1008        Ok(
1009            json!({"branch": result.selection.branch, "selection": view(&result.selection),
1010            "from": result.from, "diff": result.diff, "next_after": result.next_after,
1011            "changes": null, "conflicts": null, "dangling": null, "base_moved": result.base_moved}),
1012        )
1013    }
1014
1015    fn branch_review_counts(&self, page: &ReviewPage) -> Result<Value, Failure> {
1016        let result = super::storage::review::counts::page(
1017            &self.reader,
1018            &self.catalogue,
1019            page,
1020            self.limit(page.limit)?,
1021        )?;
1022        Ok(json!({
1023            "branch": result.selection.branch, "selection": view(&result.selection),
1024            "from": result.from, "parent": view(&result.parent), "partial": result.partial,
1025            "base_moved": result.base_moved, "done": result.next_after.is_none(),
1026            "next_after": result.next_after,
1027        }))
1028    }
1029
1030    fn branch_merge_prepare(&self, page: &ReviewPage) -> Result<Value, Failure> {
1031        let result = super::storage::review::prepare::page(
1032            &self.reader,
1033            &self.catalogue,
1034            page,
1035            self.limit(page.limit)?,
1036        )?;
1037        Ok(json!({
1038            "branch": result.selection.branch, "selection": view(&result.selection),
1039            "from": result.from, "parent": view(&result.parent), "lines": result.lines,
1040            "done": result.next_after.is_none(), "next_after": result.next_after,
1041        }))
1042    }
1043
1044    fn branch_merge_dangling(&self, page: &ReviewPage) -> Result<Value, Failure> {
1045        let result = super::storage::review::dangling::page(
1046            &self.reader,
1047            &self.catalogue,
1048            page,
1049            self.limit(page.limit)?,
1050        )?;
1051        Ok(json!({
1052            "branch": result.selection.branch, "selection": view(&result.selection),
1053            "from": result.from, "parent": view(&result.parent), "lines": result.lines,
1054            "done": result.next_after.is_none(), "next_after": result.next_after,
1055        }))
1056    }
1057
1058    fn collection_page(
1059        &self,
1060        access: &Access,
1061        page: &CollectionPage,
1062        response_byte_limit: Option<NonZeroUsize>,
1063    ) -> Result<Value, Failure> {
1064        let limit = self.limit(page.limit)?;
1065        let at = self.capture(&page.selection, page.after.is_some())?;
1066        let result = if let Some(byte_limit) = response_byte_limit {
1067            reads::collections_bounded(
1068                access,
1069                &self.reader,
1070                &at,
1071                page.after.as_deref(),
1072                limit,
1073                byte_limit,
1074            )?
1075        } else {
1076            let result = reads::collections(&self.reader, &at, page.after.as_deref(), limit)?;
1077            reads::Page {
1078                branch: result.branch,
1079                version: result.version,
1080                entries: reads::render_collections(access, &self.reader, &at, result.entries)?,
1081                next_after: result.next_after,
1082            }
1083        };
1084        Ok(json!({
1085            "selection": view(&at), "entries":result.entries, "next_after":result.next_after,
1086        }))
1087    }
1088
1089    fn document_page(
1090        &self,
1091        access: &Access,
1092        page: &DocumentPage,
1093        response_byte_limit: Option<NonZeroUsize>,
1094    ) -> Result<Value, Failure> {
1095        reads::validate_document_content(page.content.as_deref())?;
1096        reads::validate_document_prefix(page.prefix.as_deref(), page.after.as_deref())?;
1097        let limit = self.limit(page.limit)?;
1098        let at = self.capture(&page.selection, page.after.is_some())?;
1099        let result =
1100            reads::list_documents(access, &self.reader, &at, page, limit, response_byte_limit)?;
1101        Ok(json!({"selection":view(&at), "entries":result.entries, "next_after":result.next_after}))
1102    }
1103
1104    fn store_stats(&self, access: &Access, read: &StoreRead) -> Result<Value, Failure> {
1105        let at = self.capture(&read.selection, false)?;
1106        let stats = reads::stats(access, &self.reader, &at)?;
1107        let mut answer = json!({
1108            "selection":view(&at), "collections":stats.collections,
1109            "embedded_fields":stats.embedded_fields,
1110            "embedded_fields_reason":stats.embedded_fields_reason,
1111            "live_bytes":null, "history_bytes":null, "reclaimable_bytes":null,
1112        });
1113        stats.documents.insert(&mut answer, "documents")?;
1114        stats.edges.insert(&mut answer, "edges")?;
1115        stats.vectors.insert(&mut answer, "vectors")?;
1116        stats
1117            .collection_count
1118            .insert(&mut answer, "collection_count")?;
1119        stats.relation_count.insert(&mut answer, "relation_count")?;
1120        if let Some(object) = answer.as_object_mut() {
1121            let release = self
1122                .releaser
1123                .as_ref()
1124                .map_or_else(|| json!({"state":"not_started"}), Releaser::status);
1125            object.insert("release".to_owned(), release);
1126            object.insert("discard_pass".to_owned(), self.discard_passes.status());
1127            let embedding = self
1128                .embedding
1129                .as_ref()
1130                .map_or_else(|| json!({"state":"not_started"}), Driver::status);
1131            object.insert("embedding".to_owned(), embedding);
1132        }
1133        Ok(answer)
1134    }
1135
1136    fn vector_fields(&self, access: &Access, read: &StoreRead) -> Result<Value, Failure> {
1137        let at = self.capture(&read.selection, false)?;
1138        let mut listing = super::storage::vectors::fields(access, &self.reader, &at)?;
1139        // EWR1: a field this service embeds says what its driver is doing.
1140        if let Some(driver) = &self.embedding {
1141            listing.statused(|collection, field| super::storage::vectors::FieldState {
1142                status: driver.field_status(&at.branch, collection, field),
1143                longer_than_model: driver.longer_than_model(&at.branch, collection, field),
1144                embed_input_empty: driver.embed_input_empty(&at.branch, collection, field),
1145                windows: driver.windows(&at.branch, collection, field),
1146                failure: driver.field_failure(&at.branch, collection, field),
1147            });
1148        }
1149        Ok(json!({"selection":view(&at), "fields":listing.fields,
1150            "embedded_fields":listing.embedded_fields}))
1151    }
1152
1153    fn edge_page(
1154        &self,
1155        access: &Access,
1156        page: &EdgePage,
1157        incoming: bool,
1158        response_byte_limit: Option<NonZeroUsize>,
1159    ) -> Result<Value, Failure> {
1160        let limit = self.limit(page.limit)?;
1161        let at = self.capture(&page.selection, page.after.is_some())?;
1162        let query = if incoming {
1163            edges::visible_incoming
1164        } else {
1165            edges::visible_out
1166        };
1167        let result = query(
1168            &self.reader,
1169            &at,
1170            &edges::VisibleEdges {
1171                access,
1172                node: &page.node,
1173                relation: page.relation.as_deref(),
1174                after: page.after.as_deref(),
1175                limits: reads::PageLimits {
1176                    rows: limit,
1177                    bytes: response_limit(response_byte_limit)?,
1178                },
1179            },
1180        )?;
1181        Ok(json!({"selection":view(&at), "edges":result.entries, "next_after":result.next_after}))
1182    }
1183
1184    fn graph_export(
1185        &self,
1186        access: &Access,
1187        page: &GraphPage,
1188        response_byte_limit: Option<NonZeroUsize>,
1189    ) -> Result<Value, Failure> {
1190        let limit = self.limit(page.limit)?;
1191        let at = self.capture(&page.selection, page.after.is_some())?;
1192        if graph::is_filtered(page) {
1193            let result = graph::export(
1194                &self.reader,
1195                &at,
1196                access,
1197                page,
1198                reads::PageLimits {
1199                    rows: limit,
1200                    bytes: response_limit(response_byte_limit)?,
1201                },
1202                self.max_page,
1203            )?;
1204            return Ok(json!({"selection":view(&at), "edges":result.edges,
1205                "nodes":result.nodes, "next_after":result.next_after,
1206                "examination_ended":result.examination_ended}));
1207        }
1208        let result = edges::visible_export(
1209            &self.reader,
1210            &at,
1211            access,
1212            page.after.as_deref(),
1213            reads::PageLimits {
1214                rows: limit,
1215                bytes: response_limit(response_byte_limit)?,
1216            },
1217        )?;
1218        Ok(json!({"selection":view(&at), "edges":result.entries, "next_after":result.next_after}))
1219    }
1220
1221    /// The capture a read answers from, held until the answer is built: no
1222    /// reclaim pass gives its roots back while the read runs (STOR-003
1223    /// H1b2-A1).
1224    fn capture(&self, selection: &Selection, continuation: bool) -> Result<Held, Failure> {
1225        if continuation && selection.version.is_none() {
1226            return Err(Failure::new(
1227                "version_required",
1228                "continuation requires the captured version",
1229            ));
1230        }
1231        self.catalogue.capture_held(selection)
1232    }
1233
1234    fn limit(&self, limit: NonZeroU32) -> Result<NonZeroUsize, Failure> {
1235        if limit > self.max_page {
1236            return Err(Failure::new(
1237                "page_limit",
1238                format!("maximum page size is {}", self.max_page),
1239            ));
1240        }
1241        let value = usize::try_from(limit.get()).map_err(Failure::storage)?;
1242        NonZeroUsize::new(value)
1243            .filter(|limit| limit.get() < usize::MAX)
1244            .ok_or_else(|| Failure::new("page_limit", "page must allow one lookahead entry"))
1245    }
1246}
1247
1248fn response_limit(limit: Option<NonZeroUsize>) -> Result<NonZeroUsize, Failure> {
1249    let bytes = limit.map_or(usize::MAX, NonZeroUsize::get);
1250    NonZeroUsize::new(bytes)
1251        .ok_or_else(|| Failure::new("response_limit", "response byte limit is zero"))
1252}
1253
1254fn running_limit(settings: Option<&super::query::Settings>) -> Result<NonZeroUsize, Failure> {
1255    let running = settings.map_or(1, |settings| settings.max_running.get());
1256    NonZeroUsize::new(running)
1257        .ok_or_else(|| Failure::new("query_limit", "query running limit is zero"))
1258}
1259
1260fn view(at: &Captured) -> Value {
1261    json!({"branch":at.branch, "version":at.version,
1262        "roots":at.shards.iter().map(|shard| json!({"shard":shard.shard_id, "root":shard.head.to_string()})).collect::<Vec<_>>()})
1263}
1264
1265/// What a verb answers (NB1): a value, or text that carries stored bodies as
1266/// they are stored.
1267enum Answer {
1268    Value(Value),
1269    Text(Box<RawValue>),
1270}
1271
1272impl Answer {
1273    /// The answer as a value; text is read back once, equal as a value.
1274    fn into_value(self) -> Result<Value, Failure> {
1275        match self {
1276            Self::Value(value) => Ok(value),
1277            Self::Text(text) => serde_json::from_str(text.get()).map_err(Failure::storage),
1278        }
1279    }
1280
1281    /// The answer as text; a value is written once, text is passed on as it is.
1282    fn into_text(self) -> Result<Box<RawValue>, Failure> {
1283        match self {
1284            Self::Value(value) => serde_json::value::to_raw_value(&value).map_err(Failure::storage),
1285            Self::Text(text) => Ok(text),
1286        }
1287    }
1288}
1289
1290pub(super) fn fits_serialized(value: &Value, limit: NonZeroUsize) -> Result<bool, Failure> {
1291    let mut output = CappedWriter {
1292        remaining: limit.get(),
1293        exceeded: false,
1294    };
1295    match serde_json::to_writer(&mut output, value) {
1296        Ok(()) => Ok(true),
1297        Err(_) if output.exceeded => Ok(false),
1298        Err(error) => Err(Failure::storage(error)),
1299    }
1300}
1301
1302struct CappedWriter {
1303    remaining: usize,
1304    exceeded: bool,
1305}
1306
1307impl Write for CappedWriter {
1308    fn write(&mut self, bytes: &[u8]) -> io::Result<usize> {
1309        if bytes.len() > self.remaining {
1310            self.exceeded = true;
1311            return Err(io::Error::other("serialized response exceeds byte limit"));
1312        }
1313        self.remaining -= bytes.len();
1314        Ok(bytes.len())
1315    }
1316
1317    fn flush(&mut self) -> io::Result<()> {
1318        Ok(())
1319    }
1320}
1321
1322/// Wake the embedding thread for a change that writes a record's text or
1323/// declares a field; nothing else is waited on.
1324fn wake_embedding(driver: &Driver, branch: &str, change: &Change) {
1325    match change {
1326        Change::Put(write) => driver.wrote(branch, &write.collection),
1327        Change::AppendPrune(write) => driver.wrote(branch, &write.collection),
1328        Change::Delete(delete) => driver.wrote(branch, &delete.collection),
1329        Change::DeleteMany(delete) => driver.wrote(branch, &delete.collection),
1330        Change::PutMany(import) => driver.wrote(branch, &import.collection),
1331        Change::VectorDeclare(declare) => {
1332            driver.declared(branch, &declare.collection, &declare.field);
1333        }
1334        _ => {}
1335    }
1336}
1337
1338#[cfg(test)]
1339#[path = "service_tests.rs"]
1340mod tests;
1341
1342#[cfg(test)]
1343#[path = "service_merge_prepare_tests.rs"]
1344mod merge_prepare_tests;
1345
1346#[cfg(test)]
1347#[path = "service_relationship_access_tests.rs"]
1348mod relationship_access_tests;
1349
1350#[cfg(test)]
1351#[path = "service_access_tests.rs"]
1352mod access_tests;
1353
1354#[cfg(test)]
1355#[path = "service_mutation_outcome_tests.rs"]
1356mod mutation_outcome_tests;
1357
1358#[cfg(test)]
1359#[path = "service_held_tests.rs"]
1360mod held_tests;
1361
1362#[cfg(test)]
1363mod count_access_tests {
1364    use super::Service;
1365    use crate::serve::access::TestSource;
1366    use crate::serve::protocol::{CountsRebuild, Operation, Selection};
1367    use crate::serve::storage::Storage;
1368    use haematite::db::DatabaseConfig;
1369    use haematite::store::NodeCacheBudget;
1370    use std::error::Error;
1371    use std::num::NonZeroU32;
1372    use std::sync::Arc;
1373
1374    #[test]
1375    fn whole_store_authority_is_required_without_disclosing_or_rebuilding()
1376    -> Result<(), Box<dyn Error>> {
1377        let directory = tempfile::tempdir()?;
1378        let storage = Storage::create(
1379            DatabaseConfig {
1380                data_dir: directory.path().join("database"),
1381                shard_count: 1,
1382                distributed: None,
1383                executor_threads: Some(1),
1384                node_cache_budget: Some(NodeCacheBudget::Unlimited),
1385                retention: haematite::Retention::KeepAll,
1386            },
1387            NodeCacheBudget::Unlimited,
1388            fixture_long_wait_interval(),
1389        )?;
1390        let before = storage
1391            .capture(&Selection {
1392                branch: "main".to_owned(),
1393                version: None,
1394            })?
1395            .version;
1396        let source = Arc::new(TestSource::denying(&[("records", "one")]));
1397        let service = Service::with_access_source(storage, NonZeroU32::MIN, None, source)?;
1398        let error = service
1399            .dispatch(Operation::CountsRebuild(CountsRebuild {
1400                branch: "main".to_owned(),
1401                request_id: "withheld".to_owned(),
1402            }))
1403            .err()
1404            .ok_or("restricted rebuild was admitted")?;
1405        let error = serde_json::to_value(error)?;
1406        assert_eq!(error["code"], "counts_access_refused");
1407        assert_eq!(error["outcome"], "not_applied");
1408        let answer = service.dispatch(Operation::CountsAudit(Selection {
1409            branch: "main".to_owned(),
1410            version: None,
1411        }))?;
1412        assert_eq!(answer["result"], "cannot_tell");
1413        assert_eq!(answer["reason"], "withheld");
1414        assert!(answer.get("buckets").is_none());
1415        assert_eq!(
1416            answer["selection"]["version"]["sequence"],
1417            before.sequence.to_string()
1418        );
1419        Ok(())
1420    }
1421
1422    const fn fixture_long_wait_interval() -> std::time::Duration {
1423        std::time::Duration::from_secs(1)
1424    }
1425}
1426
1427#[cfg(test)]
1428#[path = "service_writer_wait_tests.rs"]
1429mod writer_wait_tests;