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    /// Verify a pass a connection presents, answering `{holder, expires}`.
186    ///
187    /// # Errors
188    ///
189    /// Refuses with the pass's stable name, or `invalid_operation` when this
190    /// service is not served through Lys.
191    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    /// Run one page of the drafts Lys approved for this database, with the
202    /// connection's pass `token` as the connector's (ACCESS-001 R2).
203    ///
204    /// # Errors
205    ///
206    /// `pass_missing` without a pass, `invalid_operation` when this service
207    /// is not served through Lys, or where the run stopped, by name.
208    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    /// Carry the request's writer deadline through approved draft execution.
219    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    /// Follow `branch`'s head for one connection, giving each head frame to
230    /// `send`, with the connection's pass expiring at `expires` seconds since
231    /// the epoch as `clock` reads it (serve/follow.rs). The route was decided
232    /// before this, as `head.follow`.
233    ///
234    /// Answers the refusal that ends the follow.
235    ///
236    /// # Errors
237    ///
238    /// The first error `send` returns: the client went away.
239    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    /// Decide `operation` from the connection's pass before it is dispatched.
275    /// `words` are the request's exact text, drafted with it when Lys holds
276    /// it for approval. A service without a Lys gate is its owner's, and
277    /// decides nothing.
278    ///
279    /// # Errors
280    ///
281    /// Returns the first refusal of the request's subjects.
282    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    /// Whether a change's branch holds no collection named `collection`, at
301    /// its head. Anything but a change, or a read that fails, answers `false`,
302    /// so the collection's own refusal stands.
303    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    /// Starts the release thread over this service's storage. A write that
317    /// finds a release running waits for it rather than being told `busy`.
318    ///
319    /// # Errors
320    ///
321    /// The decisions left at open could not be read.
322    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    /// Starts the embedding thread over this service's storage with
328    /// `worker`; the worker starts on that thread, off the serving path.
329    ///
330    /// # Errors
331    ///
332    /// The declared fields could not be read, or the thread not started.
333    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    /// As [`Self::start_embedding`], with a worker that dies started again
343    /// as `restart` says (EWR1).
344    ///
345    /// # Errors
346    ///
347    /// The declared fields could not be read, or the thread not started.
348    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    /// As [`Self::start_embedding_with`], with every text search's question
359    /// asked of `questions` when given, a worker of its own that the walk
360    /// never uses (SL1). The question worker never commits, so it never
361    /// takes the admission; the walk's commits go through it (JOIN1).
362    ///
363    /// # Errors
364    ///
365    /// The declared fields could not be read, or the thread not started.
366    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    /// The embedding thread, once started.
381    #[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    /// The release thread, once started.
410    #[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    /// Wait until no reclaim pass runs, so a test can look at the files.
420    ///
421    /// # Errors
422    ///
423    /// The writer is held or the pass failed.
424    #[cfg(test)]
425    pub fn quiesce_reclaim(&self) -> Result<(), Failure> {
426        self.admission()?.quiesce_reclaim()
427    }
428
429    /// Each shard's reclaim (SL2: a test's diagnosis).
430    ///
431    /// # Errors
432    /// `writer_unavailable` when the storage lock is poisoned.
433    #[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                        // Waiting for the lock never carries a mutation beyond its caller's deadline.
522                        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    /// # Errors
597    ///
598    /// Returns a refusal with mutation context if dispatch fails.
599    /// The answer as a value, for a caller in this process. A stored body in
600    /// a `query.run` or `changes.list` answer is equal as a value to what was
601    /// written, but a value cannot keep a number's text (`1e2` becomes `1e+2`):
602    /// the transport answers through [`Self::dispatch_text`] (NB1).
603    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    /// Dispatch with the request's configured bound on writer admission.
612    ///
613    /// # Errors
614    ///
615    /// Returns a dispatch refusal, invalid timeout, or `busy` naming an exhausted wait.
616    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    /// Dispatch response text with the request's writer admission bound.
628    ///
629    /// # Errors
630    ///
631    /// Returns a dispatch refusal, invalid timeout, or `busy` naming an exhausted wait.
632    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    /// The answer as the text the transport writes (NB1): a `query.run` or
645    /// `changes.list` answer carries every stored body as the text it is
646    /// stored as, never decoded and encoded again; any other answer is its
647    /// value written once. Bounded as [`Self::dispatch_bounded`].
648    ///
649    /// # Errors
650    ///
651    /// Returns a refusal with mutation context if dispatch fails or the text
652    /// is over `response_byte_limit`.
653    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    /// Dispatch with a cumulative serialized-entry budget for collection and
678    /// document pages, then verify the complete result against the same bound.
679    /// The transport still owns the final envelope serialization and frame cap.
680    ///
681    /// # Errors
682    ///
683    /// Propagates any error from `dispatch_with_entry_budget` or `fits_serialized`.
684    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                // A merge carries every key the source changed.
725                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                // A discard leaves rows of its generation for release to take.
738                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                // A pass issued before this commit cannot carry what it made;
754                // the gate asks Lys live for it (serve/pass.rs).
755                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                // After the commit, never before: a placement that fails
766                // leaves the write standing (serve/placement.rs).
767                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    /// EMB-E2 (j): opens one field's rewalk, then wakes its walk.
940    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    /// Reads at two captures of its own and never takes the writer.
976    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        // Whole-review totals are collected through the separate counting pages.
1000        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        // EWR1: a field this service embeds says what its driver is doing.
1132        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    /// The capture a read answers from, held until the answer is built: no
1214    /// reclaim pass gives its roots back while the read runs (STOR-003
1215    /// H1b2-A1).
1216    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
1257/// What a verb answers (NB1): a value, or text that carries stored bodies as
1258/// they are stored.
1259enum Answer {
1260    Value(Value),
1261    Text(Box<RawValue>),
1262}
1263
1264impl Answer {
1265    /// The answer as a value; text is read back once, equal as a value.
1266    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    /// The answer as text; a value is written once, text is passed on as it is.
1274    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
1314/// Wake the embedding thread for a change that writes a record's text or
1315/// declares a field; nothing else is waited on.
1316fn 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;