Skip to main content

selene_graph/shared/
builder.rs

1//! Shared graph construction helpers.
2
3use std::path::Path;
4use std::sync::Arc;
5
6use selene_core::GraphId;
7use selene_persist::{AuditLog, SyncPolicy, WalConfig, WalWriter};
8
9use super::SharedGraph;
10use crate::committer_batch::CommitBatching;
11use crate::error::{GraphError, GraphResult};
12use crate::graph::SeleneGraph;
13use crate::graph_types::GraphTypeDef;
14use crate::index_provider::IndexProvider;
15
16/// Builder for a [`SharedGraph`] and its fixed provider registry.
17pub struct SharedGraphBuilder {
18    graph: SeleneGraph,
19    providers: Vec<Arc<dyn IndexProvider>>,
20    wal_writer: Option<WalWriter>,
21    audit_log: Option<AuditLog>,
22    commit_batching: CommitBatching,
23}
24
25impl SharedGraphBuilder {
26    /// Construct a builder for an empty graph.
27    pub(super) fn new(graph_id: GraphId) -> Self {
28        Self {
29            graph: SeleneGraph::new(graph_id),
30            providers: Vec::new(),
31            wal_writer: None,
32            audit_log: None,
33            commit_batching: CommitBatching::Off,
34        }
35    }
36
37    /// Register an index provider.
38    ///
39    /// Providers are retained in registration order, which is the order used
40    /// for committed mutation delivery.
41    #[must_use]
42    pub fn with_provider(mut self, provider: Arc<dyn IndexProvider>) -> Self {
43        self.providers.push(provider);
44        self
45    }
46
47    /// Open a WAL file and route commits through the CORE durable provider.
48    ///
49    /// The path is the WAL file path, not a directory. Callers using the
50    /// conventional layout should pass `dir.join(selene_persist::DEFAULT_WAL_FILE_NAME)`.
51    ///
52    /// # SyncPolicy is OVERRIDDEN (v1.2 BRIEF 2 — read this)
53    ///
54    /// The single per-graph committer thread is the **sole fsync caller** for the
55    /// committer-managed WAL: it appends a contiguous run of commits with fsync
56    /// deferred, then issues exactly one [`WalWriter::flush`] per run (the R1
57    /// fsync-before-publish barrier). To make that the *only* fsync path, this
58    /// method **forces `config.sync_policy` to [`SyncPolicy::OnFlushOnly`]**
59    /// before opening the WAL — **whatever policy you pass is discarded.** The
60    /// fsync cadence is instead controlled by [`Self::with_commit_batching`]:
61    /// [`CommitBatching::Off`] (the default) fsyncs once per commit (behaviorally
62    /// identical to the old `EveryN(1)`), and [`CommitBatching::On`] coalesces a
63    /// contiguous run into one fsync. `config.snapshot_seq` is passed through
64    /// verbatim. Durability is unchanged: the committer always flushes before it
65    /// publishes or acks, so a commit is durable before it is ever visible.
66    ///
67    /// # Errors
68    ///
69    /// Returns [`GraphError::Persist`] when the WAL cannot be opened, including
70    /// when another writer already holds the file lock.
71    pub fn with_wal(mut self, path: impl AsRef<Path>, mut config: WalConfig) -> GraphResult<Self> {
72        // BRIEF 2: the committer owns fsync. Force OnFlushOnly before opening so
73        // the committer's group flush is the single durability barrier. Done
74        // before WalWriter::open so open-error timing (e.g. WriterLockHeld) is
75        // unchanged for existing .unwrap() call sites.
76        config.sync_policy = SyncPolicy::OnFlushOnly;
77        self.wal_writer = Some(WalWriter::open(path.as_ref(), config)?);
78        Ok(self)
79    }
80
81    /// Set the group-commit batching policy for the committer-managed WAL
82    /// (v1.2 BRIEF 2). Default [`CommitBatching::Off`].
83    ///
84    /// With [`CommitBatching::Off`] the committer fsyncs once per commit
85    /// (behaviorally identical to BRIEF 1). With [`CommitBatching::On`] it
86    /// coalesces up to `max_commits` (capped by aggregate `max_bytes`) contiguous
87    /// commits into one fsync — higher throughput + lower tail latency under
88    /// fan-in, at the cost of grouping several commits behind one barrier (all
89    /// still durable before any of them is acked or published). Has no effect
90    /// without [`Self::with_wal`] (no durable provider to flush).
91    #[must_use]
92    pub fn with_commit_batching(mut self, batching: CommitBatching) -> Self {
93        self.commit_batching = batching;
94        self
95    }
96
97    /// Attach a durable audit log at `path` (conventionally
98    /// `dir.join(selene_persist::DEFAULT_AUDIT_FILE_NAME)`).
99    ///
100    /// Engine-owned audit events committed through this graph are mirrored to
101    /// the audit log so they survive WAL-archive pruning (Item 7 / Seam D, D24).
102    /// Requires [`Self::with_wal`]: audit mirroring is part of the durable
103    /// commit path, so [`Self::build`] errors if an audit log is configured
104    /// without a WAL.
105    ///
106    /// # Errors
107    ///
108    /// Returns [`GraphError::Persist`] when the audit log cannot be opened.
109    pub fn with_audit_log(mut self, path: impl AsRef<Path>) -> GraphResult<Self> {
110        self.audit_log = Some(AuditLog::open(path.as_ref()).map_err(GraphError::Persist)?);
111        Ok(self)
112    }
113
114    /// Bind this graph to `type_def` at construction time.
115    ///
116    /// # Errors
117    ///
118    /// Returns [`GraphError::Inconsistent`] when the builder is already bound
119    /// or when `type_def` fails self-consistency validation.
120    pub fn bound_to(mut self, type_def: GraphTypeDef) -> GraphResult<Self> {
121        if self.graph.meta.bound_type.is_some() {
122            return Err(GraphError::Inconsistent {
123                reason: "graph builder is already bound to a graph type".to_owned(),
124            });
125        }
126        self.graph.meta.bound_type = Some(Arc::new(type_def.validate()?));
127        Ok(self)
128    }
129
130    /// Build shared graph state and validate provider registration.
131    ///
132    /// # Errors
133    ///
134    /// Returns [`GraphError::Provider`] when provider tags are duplicated.
135    pub fn build(self) -> GraphResult<SharedGraph> {
136        SharedGraph::from_graph_with_core_and_durables(
137            self.graph,
138            self.providers,
139            Vec::new(),
140            self.wal_writer,
141            self.audit_log,
142            self.commit_batching,
143        )
144    }
145}