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    /// Open the packed seed substrate (`fieldpack/`) instead of `seed/`.
30    /// Mutually exclusive with `entropyfs`.
31    pub packed: bool,
32    /// In-memory typed-model memo budget; `0` disables the memo.
33    pub model_memo_bytes: u64,
34    /// Opt-in durable promotion policy (Phase 15.6); disabled by default.
35    pub promote: PromotePolicy,
36}
37
38impl Default for SessionOptions {
39    fn default() -> Self {
40        SessionOptions {
41            entropyfs: false,
42            packed: false,
43            model_memo_bytes: DEFAULT_MODEL_MEMO_BYTES,
44            promote: PromotePolicy::default(),
45        }
46    }
47}
48
49/// One process serving many observations against one field.
50///
51/// The store and field are opened **once** ([`open`]); every [`observe`] then
52/// reuses that open field and the derived-cache probe, so manifest/descriptor
53/// reads, their content-id BLAKE3 checks, the descriptor parse, and the typed
54/// model decodes are paid once rather than per observation.
55///
56/// [`open`]: DocumentFieldSession::open
57/// [`observe`]: DocumentFieldSession::observe
58pub struct DocumentFieldSession {
59    store: FieldStore,
60    field: Field,
61    field_id: FieldId,
62    /// The observation index store, opened **once** alongside the field and kept
63    /// open across observations so the cache-first probe never re-opens it.
64    index: FsIndexStore,
65    models: ModelMemo,
66    /// The one-time open cost, attributed to the first observation (so the sum
67    /// over a batch is honest), then dropped.
68    pending_open_io: Option<IoSnapshot>,
69}
70
71impl DocumentFieldSession {
72    /// Open the store and the field once. Does the manifest read + content_id
73    /// BLAKE3, descriptor read + Id::of BLAKE3 + Descriptor::parse, and the
74    /// FieldId hex parse exactly once.
75    pub fn open(store_dir: &Path, field_hex: &str, opts: SessionOptions) -> Result<Self> {
76        let prof = super::warm_prof_enabled();
77        let t0 = std::time::Instant::now();
78        let mut store = if opts.packed {
79            FieldStore::open_packed(store_dir)?
80        } else if opts.entropyfs {
81            #[cfg(feature = "entropyfs-store")]
82            {
83                FieldStore::open_entropyfs(store_dir)?
84            }
85            #[cfg(not(feature = "entropyfs-store"))]
86            {
87                return Err(Error::unsupported_feature(
88                    "--entropyfs requires a build with the entropyfs-store feature",
89                ));
90            }
91        } else {
92            FieldStore::open(store_dir)?
93        };
94        store.set_promote(opts.promote);
95        let t_store = t0.elapsed();
96        let field_id = FieldId::from_hex(field_hex)?;
97        let field = Field::open(&store, &field_id, Limits::DEFAULT)?;
98        let open_io = field.open_io();
99        let t_field = t0.elapsed();
100        // Open the index store once; the probe path borrows it for every request.
101        let index = open_session_index(&store)?;
102        let t_index = t0.elapsed();
103        if prof {
104            eprintln!(
105                "[vole-profile] store_open_us={} field_open_us={} index_open_us={} session_open_total_us={}",
106                t_store.as_micros(),
107                t_field.as_micros().saturating_sub(t_store.as_micros()),
108                t_index.as_micros().saturating_sub(t_field.as_micros()),
109                t_index.as_micros()
110            );
111        }
112        Ok(Self {
113            store,
114            field,
115            field_id,
116            index,
117            models: ModelMemo::with_budget(opts.model_memo_bytes),
118            pending_open_io: Some(open_io),
119        })
120    }
121
122    /// The field this session serves.
123    pub fn field_id(&self) -> FieldId {
124        self.field_id
125    }
126
127    /// One observation. `&mut self.store`, `&self.field`, `&self.index`, and a
128    /// clone of the `Rc`-based memo are disjoint field borrows in one call.
129    pub fn observe(
130        &mut self,
131        req: &ObserveRequest,
132        limits: Limits,
133    ) -> Result<(FieldAnswer, ObserveStats, FieldId)> {
134        let open_io = self.pending_open_io.take().unwrap_or_default();
135        observe_session(
136            &mut self.store,
137            &self.field,
138            &self.index,
139            open_io,
140            req,
141            limits,
142            self.models.clone(),
143        )
144    }
145
146    /// N observations in one process, in order.
147    pub fn observe_batch(
148        &mut self,
149        reqs: &[ObserveRequest],
150        limits: Limits,
151    ) -> Vec<Result<(FieldAnswer, ObserveStats, FieldId)>> {
152        reqs.iter().map(|r| self.observe(r, limits)).collect()
153    }
154
155    /// EntropyFS epoch barrier; no-op on the filesystem backend.
156    pub fn sync(&self) -> Result<()> {
157        self.store.sync()
158    }
159}