Skip to main content

vole_document/field/
session.rs

1//! Resident field runtime (Phase 15.2): open the store and field ONCE and serve
2//! many observations in one process. Observe-only: it is off the exactness path
3//! and never backs `materialize --exact`.
4
5use std::path::Path;
6
7#[cfg(not(feature = "entropyfs-store"))]
8use crate::error::Error;
9use crate::error::Result;
10use crate::limits::Limits;
11use crate::store::IoSnapshot;
12
13use super::index::FsIndexStore;
14use super::observe::{
15    ModelMemo, ObserveRequest, ObserveStats, observe_session, open_session_index,
16};
17use super::promote::PromotePolicy;
18use super::provenance::FieldAnswer;
19use super::{Field, FieldId, FieldStore};
20
21/// Default in-memory typed-model memo budget (bytes of materialized node output).
22pub const DEFAULT_MODEL_MEMO_BYTES: u64 = 64 * 1024 * 1024;
23
24/// Options for opening a [`DocumentFieldSession`].
25#[derive(Debug, Clone, Copy)]
26pub struct SessionOptions {
27    /// Open the EntropyFS backend instead of the filesystem backend.
28    pub entropyfs: bool,
29    /// In-memory typed-model memo budget; `0` disables the memo.
30    pub model_memo_bytes: u64,
31    /// Opt-in durable promotion policy (Phase 15.6); disabled by default.
32    pub promote: PromotePolicy,
33}
34
35impl Default for SessionOptions {
36    fn default() -> Self {
37        SessionOptions {
38            entropyfs: false,
39            model_memo_bytes: DEFAULT_MODEL_MEMO_BYTES,
40            promote: PromotePolicy::default(),
41        }
42    }
43}
44
45/// One process serving many observations against one field.
46///
47/// The store and field are opened **once** ([`open`]); every [`observe`] then
48/// reuses that open field and the derived-cache probe, so manifest/descriptor
49/// reads, their content-id BLAKE3 checks, the descriptor parse, and the typed
50/// model decodes are paid once rather than per observation.
51///
52/// [`open`]: DocumentFieldSession::open
53/// [`observe`]: DocumentFieldSession::observe
54pub struct DocumentFieldSession {
55    store: FieldStore,
56    field: Field,
57    field_id: FieldId,
58    /// The observation index store, opened **once** alongside the field and kept
59    /// open across observations so the cache-first probe never re-opens it.
60    index: FsIndexStore,
61    models: ModelMemo,
62    /// The one-time open cost, attributed to the first observation (so the sum
63    /// over a batch is honest), then dropped.
64    pending_open_io: Option<IoSnapshot>,
65}
66
67impl DocumentFieldSession {
68    /// Open the store and the field once. Does the manifest read + content_id
69    /// BLAKE3, descriptor read + Id::of BLAKE3 + Descriptor::parse, and the
70    /// FieldId hex parse exactly once.
71    pub fn open(store_dir: &Path, field_hex: &str, opts: SessionOptions) -> Result<Self> {
72        let mut store = if opts.entropyfs {
73            #[cfg(feature = "entropyfs-store")]
74            {
75                FieldStore::open_entropyfs(store_dir)?
76            }
77            #[cfg(not(feature = "entropyfs-store"))]
78            {
79                return Err(Error::unsupported_feature(
80                    "--entropyfs requires a build with the entropyfs-store feature",
81                ));
82            }
83        } else {
84            FieldStore::open(store_dir)?
85        };
86        store.set_promote(opts.promote);
87        let field_id = FieldId::from_hex(field_hex)?;
88        let field = Field::open(&store, &field_id, Limits::DEFAULT)?;
89        let open_io = field.open_io();
90        // Open the index store once; the probe path borrows it for every request.
91        let index = open_session_index(&store)?;
92        Ok(Self {
93            store,
94            field,
95            field_id,
96            index,
97            models: ModelMemo::with_budget(opts.model_memo_bytes),
98            pending_open_io: Some(open_io),
99        })
100    }
101
102    /// The field this session serves.
103    pub fn field_id(&self) -> FieldId {
104        self.field_id
105    }
106
107    /// One observation. `&mut self.store`, `&self.field`, `&self.index`, and a
108    /// clone of the `Rc`-based memo are disjoint field borrows in one call.
109    pub fn observe(
110        &mut self,
111        req: &ObserveRequest,
112        limits: Limits,
113    ) -> Result<(FieldAnswer, ObserveStats, FieldId)> {
114        let open_io = self.pending_open_io.take().unwrap_or_default();
115        observe_session(
116            &mut self.store,
117            &self.field,
118            &self.index,
119            open_io,
120            req,
121            limits,
122            self.models.clone(),
123        )
124    }
125
126    /// N observations in one process, in order.
127    pub fn observe_batch(
128        &mut self,
129        reqs: &[ObserveRequest],
130        limits: Limits,
131    ) -> Vec<Result<(FieldAnswer, ObserveStats, FieldId)>> {
132        reqs.iter().map(|r| self.observe(r, limits)).collect()
133    }
134
135    /// EntropyFS epoch barrier; no-op on the filesystem backend.
136    pub fn sync(&self) -> Result<()> {
137        self.store.sync()
138    }
139}