selene_graph/shared.rs
1//! Shared graph wrapper implementing lock-free reads and serialized writes.
2
3use std::path::Path;
4use std::sync::{
5 Arc,
6 atomic::{AtomicU64, Ordering},
7};
8
9use arc_swap::ArcSwap;
10use parking_lot::{Mutex, RwLock};
11
12use selene_core::GraphId;
13use selene_persist::{AuditLog, SyncPolicy, WalConfig, WalWriter};
14
15use crate::committer_batch::CommitBatching;
16use crate::core_provider::{CoreProvider, DurableState};
17use crate::durable_provider::DurableProvider;
18use crate::error::{GraphError, GraphResult};
19use crate::graph::SeleneGraph;
20use crate::graph_types::GraphTypeDef;
21use crate::id_allocator::IdAllocator;
22use crate::index_provider::{IndexProvider, ProviderTag};
23use crate::vector_index::{VectorIndexMaintenancePolicy, VectorIndexRebuildReport};
24use crate::write_txn::WriteTxn;
25
26/// Per-graph shared runtime state.
27///
28/// Since v1.2 (BRIEF 1) every snapshot publish is funneled through a single
29/// per-graph committer thread (`CommitterThread`), which is
30/// the **sole writer** of the `snapshot` [`ArcSwap`] cell. `begin_write` hands
31/// each [`WriteTxn`] a cheap submit handle; `commit`/`compact` seal-and-submit
32/// to the committer and block until it publishes. This single-committer +
33/// sole-publisher discipline is what preserves D10 strict-serializability once
34/// `seal()` drops the write lock early — it is load-bearing and NOT
35/// type-enforced (a second committer or ArcSwap writer would silently break it).
36pub struct SharedGraph {
37 shared: Arc<RwLock<Arc<SeleneGraph>>>,
38 snapshot: Arc<ArcSwap<SeleneGraph>>,
39 schema_version: Arc<AtomicU64>,
40 allocator: Arc<Mutex<IdAllocator>>,
41 /// Fixed provider registry, frozen at construction. Shared as one
42 /// allocation so `begin_write` hands the registry to each transaction
43 /// with a single refcount bump instead of a per-transaction `Vec` clone.
44 providers: Arc<[Arc<dyn IndexProvider>]>,
45 durable_providers: Vec<Arc<dyn DurableProvider>>,
46 /// The single per-graph committer thread; sole publisher of `snapshot`.
47 /// Dropped last via [`SharedGraph`]'s implicit drop order, which joins the
48 /// thread once every outstanding [`WriteTxn`] submit handle is gone.
49 committer: crate::committer::CommitterThread,
50}
51
52impl SharedGraph {
53 /// Construct an empty shared graph.
54 #[must_use]
55 pub fn new(graph_id: GraphId) -> Self {
56 Self::from_graph(SeleneGraph::new(graph_id))
57 }
58
59 /// Start building an empty shared graph with optional providers.
60 #[must_use]
61 pub fn builder(graph_id: GraphId) -> SharedGraphBuilder {
62 SharedGraphBuilder::new(graph_id)
63 }
64
65 /// Construct shared state from a pre-built graph snapshot.
66 ///
67 /// The allocator floors are derived from storage length so that stale
68 /// `GraphMeta.next_*_id` values cannot allow ID reuse over rows that
69 /// already exist (recovery hardening — spec 02 §4 forbids ID reuse).
70 ///
71 /// # Panics
72 ///
73 /// Panics if the supplied graph contains more than `u32::MAX` rows in
74 /// either store. Selene-graph's row index is `u32` by construction;
75 /// `SeleneGraph::new()` always satisfies this, and any caller-built
76 /// fixture must too. Use [`SharedGraph::try_from_graph`] for the
77 /// fallible variant when validating untrusted snapshots.
78 #[must_use]
79 pub fn from_graph(graph: SeleneGraph) -> Self {
80 Self::try_from_graph(graph).expect("graph store row count exceeds u32::MAX")
81 }
82
83 /// Fallible variant of [`SharedGraph::from_graph`]. Returns
84 /// [`GraphError::Inconsistent`] when the graph's stores exceed the
85 /// `u32` row capacity.
86 pub fn try_from_graph(graph: SeleneGraph) -> GraphResult<Self> {
87 Self::from_graph_with_core(graph, Vec::new())
88 }
89
90 /// Construct shared state from a graph snapshot and fixed provider list.
91 ///
92 /// # Errors
93 ///
94 /// Returns [`GraphError::Provider`] when two providers declare the same
95 /// [`ProviderTag`], and [`GraphError::Inconsistent`] when the graph's
96 /// stores exceed the `u32` row capacity.
97 pub fn from_graph_with_providers(
98 graph: SeleneGraph,
99 providers: Vec<Arc<dyn IndexProvider>>,
100 ) -> GraphResult<Self> {
101 Self::from_graph_with_core(graph, providers)
102 }
103
104 /// Construct shared state from a graph snapshot and commit-critical WAL file.
105 ///
106 /// Since v1.2 (BRIEF 2) the committer is the sole fsync caller, so the WAL is
107 /// **always** opened in [`SyncPolicy::OnFlushOnly`] regardless of the
108 /// `config.sync_policy` passed (it is overwritten before
109 /// [`WalWriter::open`]). This non-builder constructor uses
110 /// [`CommitBatching::Off`], so the committer still fsyncs once per commit —
111 /// behaviorally identical to BRIEF 1's `EveryN(1)`.
112 ///
113 /// # Errors
114 ///
115 /// Returns [`GraphError::Persist`] when the WAL cannot be opened, plus the
116 /// same consistency and provider-registration errors as [`Self::try_from_graph`].
117 pub fn from_graph_with_wal(
118 graph: SeleneGraph,
119 path: impl AsRef<Path>,
120 mut config: WalConfig,
121 ) -> GraphResult<Self> {
122 // BRIEF 2: the committer owns fsync via flush_durables(); force the
123 // committer-managed WAL into OnFlushOnly before opening it (overwriting
124 // any caller policy), keeping open-error timing unchanged.
125 config.sync_policy = SyncPolicy::OnFlushOnly;
126 let writer = WalWriter::open(path.as_ref(), config)?;
127 Self::from_graph_with_core_and_durables(
128 graph,
129 Vec::new(),
130 Vec::new(),
131 Some(writer),
132 None,
133 CommitBatching::Off,
134 )
135 }
136
137 fn from_graph_with_core(
138 graph: SeleneGraph,
139 providers: Vec<Arc<dyn IndexProvider>>,
140 ) -> GraphResult<Self> {
141 Self::from_graph_with_core_and_durables(
142 graph,
143 providers,
144 Vec::new(),
145 None,
146 None,
147 CommitBatching::Off,
148 )
149 }
150
151 pub(crate) fn from_graph_with_core_and_durables(
152 graph: SeleneGraph,
153 providers: Vec<Arc<dyn IndexProvider>>,
154 mut durable_providers: Vec<Arc<dyn DurableProvider>>,
155 wal_writer: Option<WalWriter>,
156 audit_log: Option<AuditLog>,
157 batching: CommitBatching,
158 ) -> GraphResult<Self> {
159 if audit_log.is_some() && wal_writer.is_none() {
160 return Err(GraphError::Inconsistent {
161 reason: "audit log configured without a WAL; audit mirroring requires durable WAL \
162 state"
163 .to_owned(),
164 });
165 }
166 let snapshot = Arc::new(ArcSwap::from_pointee(graph.clone()));
167 let has_wal = wal_writer.is_some();
168 let durable = wal_writer
169 .map(DurableState::new)
170 .map(|durable| match audit_log {
171 Some(audit) => durable.with_audit_log(audit),
172 None => durable,
173 });
174 let core = CoreProvider::new_for_live_with_wal(Arc::clone(&snapshot), durable);
175 let mut all_providers = Vec::with_capacity(providers.len() + 1);
176 all_providers.push(core.clone() as Arc<dyn IndexProvider>);
177 all_providers.extend(providers);
178 if has_wal {
179 durable_providers.push(core as Arc<dyn DurableProvider>);
180 }
181 validate_unique_provider_tags(&all_providers)?;
182 Self::from_graph_parts_and_snapshot(
183 graph,
184 all_providers,
185 durable_providers,
186 snapshot,
187 batching,
188 )
189 }
190
191 pub(crate) fn from_graph_parts_and_snapshot(
192 graph: SeleneGraph,
193 providers: Vec<Arc<dyn IndexProvider>>,
194 durable_providers: Vec<Arc<dyn DurableProvider>>,
195 snapshot: Arc<ArcSwap<SeleneGraph>>,
196 batching: CommitBatching,
197 ) -> GraphResult<Self> {
198 validate_unique_provider_tags(&providers)?;
199 // Freeze the registry into one shared allocation: the committer and
200 // every `begin_write` transaction clone the `Arc`, not the `Vec`.
201 let providers: Arc<[Arc<dyn IndexProvider>]> = providers.into();
202 let mut graph = graph;
203 rebuild_derived_state(&mut graph)?;
204 crate::property_index::rebuild_property_indexes(&mut graph)?;
205 crate::property_index::rebuild_edge_property_indexes(&mut graph)?;
206 crate::composite_property_index::rebuild_composite_property_indexes(&mut graph)?;
207 crate::vector_index::rebuild_vector_indexes(&mut graph)?;
208 crate::text_index::rebuild_text_indexes(&mut graph)?;
209 if let Some(type_def) = graph.meta.bound_type.as_deref() {
210 // Why: GraphMeta is publicly constructible, so SharedGraph::from_graph
211 // can land a malformed bound_type that bypassed builder().bound_to()'s
212 // validate(). Re-check self-consistency here so every constructor
213 // arrives at the same closed-graph admissibility contract.
214 type_def.validate_ref()?;
215 crate::type_validator::validate_entity_state(&graph, type_def)?;
216 }
217
218 let node_floor = (graph.node_store.labels.len() as u64).saturating_add(1);
219 let edge_floor = (graph.edge_store.label.len() as u64).saturating_add(1);
220 let allocator = IdAllocator::from_meta_with_floors(&graph.meta, node_floor, edge_floor);
221
222 // Debug-only structural net on the snapshot-load / recovery path: the
223 // rebuild_* helpers above re-derive all indexes from columns, so a
224 // rebuild bug would otherwise surface only as silent query
225 // corruption. Highest-value placement — verify the rebuilt snapshot
226 // before it is ever published. Compiled out in release builds.
227 #[cfg(debug_assertions)]
228 if let Err(reason) = graph.assert_indexes_consistent() {
229 return Err(GraphError::Inconsistent {
230 reason: format!("rebuilt snapshot failed index consistency check: {reason}"),
231 });
232 }
233
234 let graph = Arc::new(graph);
235 snapshot.store(Arc::clone(&graph));
236 let shared = Arc::new(RwLock::new(graph));
237 let schema_version = Arc::new(AtomicU64::new(0));
238 let allocator = Arc::new(Mutex::new(allocator));
239 // Spawn the single per-graph committer thread. It captures clones of
240 // every handle it needs to publish + compact; it is the sole writer of
241 // `snapshot`. All commit/compact/index-DDL publishes route through it.
242 let committer =
243 crate::committer::CommitterThread::spawn(crate::committer::CommitterHandles {
244 snapshot: Arc::clone(&snapshot),
245 schema_version: Arc::clone(&schema_version),
246 providers: Arc::clone(&providers),
247 durable_providers: durable_providers.clone(),
248 batching,
249 });
250 Ok(Self {
251 shared,
252 snapshot,
253 schema_version,
254 allocator,
255 providers,
256 durable_providers,
257 committer,
258 })
259 }
260
261 /// Load the current immutable snapshot without taking the write lock.
262 #[must_use]
263 pub fn read(&self) -> Arc<SeleneGraph> {
264 self.snapshot.load_full()
265 }
266
267 /// Return compaction pressure for the current published snapshot.
268 ///
269 /// This is a lock-free read of row counts and liveness counters. It does not
270 /// compact, rebuild indexes, or take the writer lock.
271 #[must_use]
272 pub fn compaction_stats(&self) -> crate::compaction::CompactionStats {
273 self.read().compaction_stats()
274 }
275
276 /// Compact the live graph in place: reclaim every dead / hole row, renumber
277 /// rows dense, and atomically republish the result so the RAM held by deleted
278 /// rows is reclaimed immediately (BRIEF-Item-4c — the live-densify half of
279 /// snapshot-time compaction).
280 ///
281 /// This is pure space reclamation: it changes only the internal row layout,
282 /// never external `NodeId`/`EdgeId`, properties, or labels, so it emits **no**
283 /// [`selene_core::Change`] and writes **no** WAL entry. Durability
284 /// comes from the next snapshot, which encodes the now-dense live graph (the
285 /// CORE provider reads the same `snapshot` cell this method publishes into). A
286 /// crash before that snapshot simply reloads the pre-compaction state and
287 /// recompacts later — compaction can never lose data.
288 ///
289 /// The dense graph is built under the write lock on the calling thread
290 /// (seal-and-handover, exactly like a commit), and is allocated a publish
291 /// `seal_seq` under that same lock; the single committer then swaps it into
292 /// the published `snapshot` cell strictly in `seal_seq` order. So compaction
293 /// serializes with writers exactly like a commit and can never be reordered
294 /// ahead of an earlier-sealed commit (which would let that commit's stale,
295 /// non-dense frozen snapshot clobber the dense one). Lock-free readers keep
296 /// observing the old snapshot until the dense graph is published. The
297 /// monotonic allocator high-water marks are preserved (the live allocator is
298 /// untouched, and [`compact_core`](crate::compact_core) carries `GraphMeta`
299 /// verbatim — and the allocator is kept in sync with `GraphMeta` on every
300 /// commit), so no external id is ever reused after a later recovery.
301 ///
302 /// # Errors
303 ///
304 /// Returns [`GraphError`] if the graph's id↔row mapping is corrupt or the
305 /// recompacted graph fails its consistency check (see
306 /// [`compact_core`](crate::compact_core)).
307 pub fn compact(&self) -> GraphResult<crate::CompactionReport> {
308 // Seal-and-handover for compaction (v1.2 BRIEF 1, P1 fix): build the
309 // dense graph HERE, on the caller thread, under the write lock — exactly
310 // like a commit seals under the lock — then hand the committer a
311 // pre-built dense snapshot to publish in seal_seq order. This keeps the
312 // committer off the write lock entirely (no deadlock surface) and, more
313 // importantly, ties compaction's publish position to a seal_seq taken
314 // under the same lock as commits, so a compact can never be reordered
315 // ahead of an earlier-sealed commit (which would otherwise let an
316 // earlier commit's stale, non-dense frozen snapshot clobber the dense
317 // one in the published cell).
318 //
319 // Ordering under the lock is load-bearing for the reorder buffer's
320 // gap-free invariant: densify FIRST (the only fallible step), and only
321 // THEN allocate the seal_seq, so a failed compaction consumes no
322 // sequence number (which would otherwise wedge the committer waiting for
323 // a seq that never arrives).
324 let committer = self.committer.handle();
325 let (seal_seq, dense, report) = {
326 let mut guard = self.shared.write();
327 let compacted = crate::compaction::compact_core(&guard)?;
328 let dense = Arc::new(compacted.graph);
329 // Allocate the publish-order key under the lock, after the fallible
330 // densify, so seal_seq order == lock-acquisition order and no seq is
331 // ever burned by a failed compaction.
332 let seal_seq = committer.next_seal_seq();
333 *guard = Arc::clone(&dense);
334 (seal_seq, dense, compacted.report)
335 // Lock released here, before the (blocking) enqueue + recv — the
336 // committer never needs the write lock, but releasing here also
337 // means a compactor never holds the lock while blocked on the
338 // committer.
339 };
340 committer.submit_compact(seal_seq, dense, report)
341 }
342
343 /// Rebuild every registered vector index from primary node values.
344 ///
345 /// HNSW indexes retain stale deleted entries after vector update/delete so
346 /// in-flight search can still traverse the neighbor graph safely. This
347 /// maintenance path reclaims those stale entries by rebuilding only the
348 /// derived vector-index state; it does not change graph data, emit
349 /// [`selene_core::Change`], write a WAL entry, bump schema epoch, or
350 /// notify providers. The HNSW graph is derived, not durable: snapshots and
351 /// recovery persist only vector-index registrations plus primary values, so
352 /// a reopen rebuilds the index from that authoritative state.
353 ///
354 /// The rebuild is strict on live data: if an indexed row no longer satisfies
355 /// the registered vector dimension/metric invariant, this method returns an
356 /// error instead of silently dropping the row from the index.
357 pub fn rebuild_vector_indexes(&self) -> GraphResult<VectorIndexRebuildReport> {
358 let committer = self.committer.handle();
359 let (seal_seq, rebuilt, report) = {
360 let mut guard = self.shared.write();
361 let mut rebuilt = guard.as_ref().clone();
362 let report = crate::vector_index::rebuild_vector_indexes_strict(&mut rebuilt)?;
363 let rebuilt = Arc::new(rebuilt);
364 let seal_seq = committer.next_seal_seq();
365 *guard = Arc::clone(&rebuilt);
366 (seal_seq, rebuilt, report)
367 };
368 committer.submit_vector_index_rebuild(seal_seq, rebuilt, report)
369 }
370
371 /// Rebuild only vector indexes whose diagnostics recommend maintenance.
372 ///
373 /// This is the bounded maintenance variant for IVF drift: it uses each index's current
374 /// [`ivf_rebuild_recommended`](crate::vector_index::VectorIndexMemoryUsage::ivf_rebuild_recommended)
375 /// value to decide whether to rebuild that derived index. Indexes that do not recommend rebuild
376 /// are left untouched, and a no-op call returns an empty report without publishing a maintenance
377 /// item.
378 ///
379 /// The rebuild is strict on live data for selected indexes, matching
380 /// [`Self::rebuild_vector_indexes`].
381 pub fn rebuild_recommended_vector_indexes(&self) -> GraphResult<VectorIndexRebuildReport> {
382 self.maintain_vector_indexes(VectorIndexMaintenancePolicy::recommended())
383 }
384
385 /// Maintain recommended vector indexes under a caller-supplied policy.
386 ///
387 /// This is the explicit orchestration API for amortized vector-index maintenance. It rebuilds
388 /// only indexes whose diagnostics currently recommend maintenance and applies the policy cap
389 /// after ordering recommended indexes by pending IVF retrain pressure. It remains a
390 /// maintenance-tier operation: reads never trigger it, and a no-op call returns an empty report
391 /// without publishing a derived-state replacement.
392 ///
393 /// The rebuild is strict on live data for selected indexes, matching
394 /// [`Self::rebuild_vector_indexes`].
395 pub fn maintain_vector_indexes(
396 &self,
397 policy: VectorIndexMaintenancePolicy,
398 ) -> GraphResult<VectorIndexRebuildReport> {
399 let committer = self.committer.handle();
400 let (seal_seq, rebuilt, report) = {
401 let mut guard = self.shared.write();
402 let mut rebuilt = guard.as_ref().clone();
403 let report = crate::vector_index::maintain_vector_indexes_strict(&mut rebuilt, policy)?;
404 if report.entries.is_empty() {
405 return Ok(report);
406 }
407 let rebuilt = Arc::new(rebuilt);
408 let seal_seq = committer.next_seal_seq();
409 *guard = Arc::clone(&rebuilt);
410 (seal_seq, rebuilt, report)
411 };
412 committer.submit_vector_index_rebuild(seal_seq, rebuilt, report)
413 }
414
415 /// Return the runtime schema-version epoch used for plan-cache invalidation.
416 ///
417 /// The epoch starts at zero for each [`SharedGraph`] instance and advances
418 /// only after a successful commit whose change set contains
419 /// [`selene_core::Change::SchemaChanged`].
420 #[must_use]
421 pub fn schema_version(&self) -> u64 {
422 self.schema_version.load(Ordering::Acquire)
423 }
424
425 /// Return the bound graph type, if this is a closed graph.
426 #[must_use]
427 pub fn graph_type(&self) -> Option<Arc<GraphTypeDef>> {
428 self.read().meta.bound_type.as_ref().map(Arc::clone)
429 }
430
431 /// Return true when this graph is bound to a closed graph type.
432 #[must_use]
433 pub fn is_closed(&self) -> bool {
434 self.read().meta.bound_type.is_some()
435 }
436
437 /// Look up a registered provider by tag.
438 #[must_use]
439 pub fn index_provider_by_tag(&self, tag: ProviderTag) -> Option<Arc<dyn IndexProvider>> {
440 self.providers
441 .iter()
442 .find_map(|provider| (provider.provider_tag() == tag).then(|| Arc::clone(provider)))
443 }
444
445 /// Borrow the fixed provider registry for executor procedure contexts.
446 #[must_use]
447 pub fn index_providers(&self) -> &[Arc<dyn IndexProvider>] {
448 &self.providers
449 }
450
451 /// Borrow the fixed commit-critical durable provider registry.
452 #[must_use]
453 pub fn durable_providers(&self) -> &[Arc<dyn DurableProvider>] {
454 &self.durable_providers
455 }
456
457 /// Begin a write transaction by acquiring the single graph write lock.
458 ///
459 /// Concurrent writers from other threads queue normally on the write
460 /// lock; the engine does **not** panic legitimate concurrent writes
461 /// during another commit's provider fanout.
462 ///
463 /// Since v1.2 (BRIEF 1) the actual snapshot publish happens on the single
464 /// committer thread, not here: `WriteTxn::commit` seals under this lock,
465 /// releases it, and hands the frozen bundle to the committer. Provider
466 /// fan-out therefore now runs on the **committer thread**, so the
467 /// re-entrancy guard below protects the committer thread (sound with exactly
468 /// one committer — see `reentry.rs` and the v1.2 design §7.7).
469 ///
470 /// # Panics
471 ///
472 /// Panics when called from inside an [`IndexProvider`] callback **on
473 /// the committer thread** as the active fanout. Re-entrant writes from a
474 /// provider callback are unsupported; the committer is publishing, so a
475 /// nested write would recurse indefinitely. The panic is caught by the
476 /// committer's `notify_providers` boundary; provider state may drift, but
477 /// the commit still completes.
478 ///
479 /// Cross-thread re-entry — a provider spawning a worker thread that
480 /// calls `begin_write` and waiting for it — is **documented misuse**
481 /// rather than a detectable footgun (the engine cannot trace causal
482 /// thread ancestry). See the module docs in `reentry.rs` and the
483 /// `IndexProvider` rustdoc for the contract.
484 #[must_use]
485 #[tracing::instrument(name = "selene.graph.begin_write", skip(self))]
486 pub fn begin_write(&self) -> WriteTxn<'_> {
487 if crate::reentry::in_fanout() {
488 panic!(
489 "selene-graph: SharedGraph::begin_write() called from within \
490 a provider fan-out callback on the committer thread; \
491 re-entrant writes from a provider callback are not supported. \
492 The committer's fan-out boundary will catch this panic; \
493 the commit succeeds, but the offending provider's \
494 chained mutation does not."
495 );
496 }
497 WriteTxn::new(
498 self.shared.write(),
499 self.committer.handle(),
500 self.allocator.lock(),
501 Arc::clone(&self.providers),
502 )
503 }
504
505 #[cfg(test)]
506 pub(crate) fn locked_arc_ptr_for_test(&self) -> *const SeleneGraph {
507 let guard = self.shared.read();
508 Arc::as_ptr(&*guard)
509 }
510
511 /// Read the generation of the **live RwLock graph** (`*shared`), as opposed
512 /// to the published `ArcSwap` snapshot. Used by divergence tests to assert
513 /// the two never disagree after a failed / cancelled commit (the P0
514 /// WAL-failure + cancel rollback invariants).
515 #[cfg(test)]
516 pub(crate) fn locked_generation_for_test(&self) -> u64 {
517 self.shared.read().meta.generation
518 }
519
520 /// Submit an already-[`seal`](crate::WriteTxn::seal)ed commit straight to the
521 /// committer, blocking until it is durable + visible. Test-only seam for
522 /// exercising the BRIEF-117 cancellation cut-line (which has no production
523 /// producer yet) without re-entering `commit_with_principal`.
524 #[cfg(test)]
525 pub(crate) fn submit_sealed_for_test(
526 &self,
527 sealed: crate::write_txn::SealedCommit,
528 ) -> GraphResult<crate::CommitOutcome> {
529 self.committer.handle().submit_commit(sealed)
530 }
531
532 /// Enqueue a sealed commit and return its reply receiver without waiting.
533 #[cfg(test)]
534 pub(crate) fn submit_sealed_async_for_test(
535 &self,
536 sealed: crate::write_txn::SealedCommit,
537 ) -> GraphResult<std::sync::mpsc::Receiver<GraphResult<crate::CommitOutcome>>> {
538 self.committer.handle().submit_commit_async_for_test(sealed)
539 }
540}
541
542mod builder;
543mod index_ddl;
544mod rebuild;
545pub use builder::SharedGraphBuilder;
546pub(crate) use rebuild::{rebuild_derived_state, validate_unique_provider_tags};
547
548#[cfg(test)]
549mod compaction_tests;
550#[cfg(test)]
551#[path = "shared_property_tests.rs"]
552mod property_tests;
553#[cfg(test)]
554mod tests;