Skip to main content

rushdb_storage/
commit.rs

1//! Command admission, ordered publication, and transaction commit.
2//! The store owns the health fence and publication locks across these phases.
3
4use super::{
5    log_key, mpsc, resources, wal, Arc, BTreeMap, BTreeSet, CommitBatch, CommitOutcome,
6    CommitPhase, DependencySet, DeserializeOwned, Op, Ordering, OutcomeKind, OutcomeStatus,
7    OverlayView, PendingCommit, PreparedCommand, RefCell, SequencerState, Serialize, StorageError,
8    Store, TenantId, TxAccumulator, Txn, TxnBase, Uuid, LAST_APPLIED_KEY, SERIALIZABLE_CONFLICT,
9};
10
11impl Store {
12    /// Data-map guard for a transaction base. `None` when the transaction
13    /// carries a pinned snapshot (the snapshot owns the read, and taking this
14    /// guard too would self-deadlock re-reading through the memory snapshot
15    /// while holding `data.read()`, since std `RwLock` is not reentrant);
16    /// otherwise the live committed data guard.
17    fn txn_data(
18        &self,
19        snapshot: bool,
20    ) -> Result<Option<std::sync::RwLockReadGuard<'_, BTreeMap<Vec<u8>, Vec<u8>>>>, StorageError>
21    {
22        if snapshot {
23            Ok(None)
24        } else {
25            Ok(Some(self.health.guard(self.data.read())?))
26        }
27    }
28
29    // Shared by every commit path (autocommit, serializable, explicit tx): reject an expanded
30    // op-set (plus command) over `max_expanded_commit_bytes` before a slot is reserved, as a
31    // retryable resource error rather than the store-fencing a durability failure would cause.
32    fn reject_oversized_commit(
33        &self,
34        ops: &[Op],
35        command_payload_len: usize,
36    ) -> Result<(), StorageError> {
37        let expanded_bytes = wal::op_set_encoded_len(ops).saturating_add(command_payload_len);
38        if expanded_bytes > self.resource_config.max_expanded_commit_bytes {
39            return Err(StorageError::ResourceExhausted(format!(
40                "expanded mutation batch exceeds {} bytes (got {expanded_bytes})",
41                self.resource_config.max_expanded_commit_bytes
42            )));
43        }
44        Ok(())
45    }
46
47    /// Deterministic expansion + commit bookkeeping for `batch` at commit index
48    /// `ci`, against whatever base `txn` is positioned at (live committed
49    /// state, a pinned snapshot, or a sequencer overlay). Returns the drained
50    /// op-set exactly as the forward publish path persists it. This is the one
51    /// home for expansion: the live commit path and the sequencer overlay path
52    /// funnel through it, so their op-sets cannot drift.
53    ///
54    /// # Errors
55    /// Returns [`StorageError::CorruptLog`] when no expander is installed and
56    /// [`StorageError::Builder`] when expansion fails.
57    fn expand_commit_ops(
58        &self,
59        ci: u64,
60        batch: &CommitBatch,
61        payload_bytes: &[u8],
62        mut txn: Txn<'_>,
63    ) -> Result<Vec<Op>, StorageError> {
64        let expander = self.health.guard(self.expander.read())?.clone();
65        let Some(expand) = expander.as_ref() else {
66            return Err(StorageError::CorruptLog);
67        };
68        expand(ci, batch, &mut txn).map_err(StorageError::Builder)?;
69        // A durable store derives the journal from the WAL instead;
70        // an in-memory store has no WAL, so it keeps this row (test-mode only).
71        if !self.is_durable() {
72            let command_payload = rushdb_types::command_codec::encode(batch);
73            txn.put(log_key(ci), wal::encode_wal_record(0, ci, &command_payload));
74        }
75        self.put_receipt(
76            &mut txn,
77            batch.commit_key,
78            OutcomeStatus::Committed,
79            ci,
80            Some(payload_bytes),
81        );
82        txn.set(LAST_APPLIED_KEY.to_vec(), wal::encode_last_applied(0, ci));
83        Ok(txn.drain())
84    }
85
86    /// Sequencer expansion primitive: the deterministic ordered op-set for
87    /// `batch` at commit index `ci` computed against `overlay` — a committed
88    /// base plus lower-index *pending* ops — instead of the live committed
89    /// state. Deterministic and side-effect free: takes no admission, shard
90    /// or publish lock, and publishes nothing.
91    ///
92    /// When `overlay` carries the expanded output of commits `< ci`, the
93    /// result is byte-identical to the serialized forward path publishing
94    /// those commits first (`sequencer_tests` asserts the equivalence). The
95    /// sequencer calls this to expand without holding the shard lock
96    /// across durability.
97    ///
98    /// # Errors
99    /// Returns the same errors as forward expansion: a base read error, or
100    /// [`StorageError::CorruptLog`]/[`StorageError::Builder`] when no
101    /// expander is installed or expansion fails.
102    pub fn expand_against_overlay<P>(
103        &self,
104        ci: u64,
105        batch: &CommitBatch,
106        payload: &P,
107        overlay: Arc<OverlayView>,
108    ) -> Result<Vec<Op>, StorageError>
109    where
110        P: Serialize,
111    {
112        self.check_health()?;
113        let payload_bytes =
114            serde_json::to_vec(payload).map_err(|_| StorageError::OutcomePayload)?;
115        let txn = Txn {
116            base: TxnBase::Overlay(overlay),
117            buffer: BTreeMap::new(),
118            purge_keys: BTreeSet::default(),
119            set_keys: BTreeSet::default(),
120            watermark_keys: BTreeSet::default(),
121            ids: &self.ids,
122            deps: None,
123        };
124        self.expand_commit_ops(ci, batch, &payload_bytes, txn)
125    }
126
127    /// Autocommit path: admission builds and validates a canonical
128    /// `CommitBatch` inside `build`, storage publishes everything atomically.
129    /// Retrying with the same commit key discovers the durable outcome.
130    ///
131    /// # Errors
132    /// Returns [`StorageError`] if the publisher has failed, a precondition is
133    /// violated, expansion fails, or durable publication fails.
134    pub fn commit<P, F>(&self, commit_key: Uuid, build: F) -> Result<CommitOutcome<P>, StorageError>
135    where
136        P: Serialize + DeserializeOwned,
137        F: FnOnce(u64, &mut Txn) -> Result<(CommitBatch, P), String>,
138    {
139        self.check_health()?;
140        self.commit_sharded(0, None, commit_key, None, build)
141    }
142
143    /// Tenant-aware autocommit. Outer shards preserve per-tenant replication
144    /// ordering; the local logical writer serializes planning and expansion.
145    ///
146    /// # Errors
147    /// Returns [`StorageError`] if the publisher has failed, a precondition is
148    /// violated, expansion fails, or durable publication fails.
149    pub fn commit_for_tenant<P, F>(
150        &self,
151        tenant: &TenantId,
152        commit_key: Uuid,
153        build: F,
154    ) -> Result<CommitOutcome<P>, StorageError>
155    where
156        P: Serialize + DeserializeOwned,
157        F: FnOnce(u64, &mut Txn) -> Result<(CommitBatch, P), String>,
158    {
159        self.check_health()?;
160        let shard = Store::shard_for(Some(tenant));
161        self.commit_sharded(shard, Some(tenant), commit_key, None, build)
162    }
163
164    /// Tenant-aware autocommit of a batch planned at an earlier snapshot: it
165    /// commits only if no commit after `snapshot_ci` wrote a key `deps`
166    /// covers (published or still in flight); otherwise it is
167    /// [`CommitOutcome::Conflicted`] with a `serializable_conflict` reason.
168    ///
169    /// # Errors
170    /// As [`Store::commit_for_tenant`], plus
171    /// [`StorageError::ConflictHistoryUnavailable`] when the conflict window
172    /// no longer holds a commit the check needs (retryable).
173    pub fn commit_for_tenant_since<P, F>(
174        &self,
175        tenant: &TenantId,
176        commit_key: Uuid,
177        snapshot_ci: u64,
178        deps: &DependencySet,
179        build: F,
180    ) -> Result<CommitOutcome<P>, StorageError>
181    where
182        P: Serialize + DeserializeOwned,
183        F: FnOnce(u64, &mut Txn) -> Result<(CommitBatch, P), String>,
184    {
185        self.check_health()?;
186        let shard = Store::shard_for(Some(tenant));
187        self.commit_sharded(
188            shard,
189            Some(tenant),
190            commit_key,
191            Some((snapshot_ci, deps)),
192            build,
193        )
194    }
195
196    /// The first commit after `snapshot_ci` that wrote a key `deps` covers:
197    /// published commits from the conflict window, in-flight ones from the
198    /// admission buffer. The caller holds admission (so no commit is
199    /// assigned meanwhile) and has drained publication.
200    fn first_conflict_since(
201        &self,
202        admission: &SequencerState,
203        snapshot_ci: u64,
204        deps: &DependencySet,
205    ) -> Result<Option<u64>, StorageError> {
206        let published = self.published.load(Ordering::SeqCst);
207        let window = self.health.guard(self.conflicts.lock())?;
208        for probe in (snapshot_ci + 1)..=published {
209            let key_set = window
210                .keys(probe)
211                .ok_or(StorageError::ConflictHistoryUnavailable {
212                    commit_index: probe,
213                })?;
214            if key_set.iter().any(|key| deps.intersects(key)) {
215                return Ok(Some(probe));
216            }
217        }
218        drop(window);
219        Ok(admission
220            .buffer
221            .iter()
222            .filter(|(key, (ci, _))| *ci > snapshot_ci && deps.intersects(key))
223            .map(|(_, (ci, _))| *ci)
224            .min())
225    }
226
227    #[expect(
228        clippy::too_many_lines,
229        reason = "the commit state machine keeps admission, expansion, slot reservation, and publication together so the ordering contract is auditable in one place"
230    )]
231    pub(super) fn commit_sharded<P, F>(
232        &self,
233        shard: usize,
234        routed_tenant: Option<&TenantId>,
235        commit_key: Uuid,
236        since: Option<(u64, &DependencySet)>,
237        build: F,
238    ) -> Result<CommitOutcome<P>, StorageError>
239    where
240        P: Serialize + DeserializeOwned,
241        F: FnOnce(u64, &mut Txn) -> Result<(CommitBatch, P), String>,
242    {
243        self.check_health()?;
244        let shard_guard = self.health.guard(self.shards[shard].lock())?;
245        // Replication re-enters through apply_replicated_batch; do not hold
246        // this mutex across the replication call. Local writes hold it through
247        // planning + expansion + enqueue, then release it before the durability
248        // wait so concurrent commits can coalesce; every expansion reads
249        // the preceding logical state from the pending overlay instead.
250        let replicator = self.replicator();
251        let admission_started = std::time::Instant::now();
252        let mut admission = if replicator.is_none() {
253            Some(self.acquire_admission()?)
254        } else {
255            None
256        };
257        self.observe_phase(CommitPhase::AdmissionWait, admission_started);
258        self.check_publisher()?;
259        if let Some((status, prev_ci)) = self.lookup_outcome_checked(commit_key)? {
260            return match status {
261                OutcomeStatus::Aborted => Ok(CommitOutcome::Conflicted(
262                    "previously aborted under this commit key".to_string(),
263                )),
264                OutcomeStatus::Committed => Ok(CommitOutcome::Committed {
265                    commit_index: prev_ci,
266                    payload: self.committed_payload(commit_key)?,
267                }),
268            };
269        }
270
271        // Admission builds + validates against a stable read view. Aborts do
272        // NOT consume a commit-index slot, keeping the log dense and replay
273        // position-indexed. `ci` is only reserved after the build succeeds.
274        // Refresh the pending overlay, then plan and expand against it so the
275        // command sees every lower-index commit whether or not it is published
276        // yet (the property the shard lock used to guarantee). Phase A holds
277        // admission across publication, so the overlay is empty here and this
278        // is behavior-preserving; Phase C releases it before the ack wait.
279        if let Some(state) = admission.as_mut() {
280            self.trim_sequencer(state);
281        }
282        let sequencer_overlay = match admission.as_ref() {
283            Some(state) => Some(self.sequencer_overlay(state)?),
284            None => None,
285        };
286        let exists_in_view = |key: &[u8]| -> Result<bool, StorageError> {
287            match &sequencer_overlay {
288                Some(overlay) => Ok(overlay.get(key)?.is_some()),
289                None => Ok(self.try_get(key)?.is_some()),
290            }
291        };
292        let provisional = self.next_commit_index.load(Ordering::SeqCst);
293        let build_deps = RefCell::new(DependencySet::default());
294        let (mut batch, payload) = {
295            // When an overlay is present it is authoritative, so the base reads
296            // go through it and never touch the live map. Acquiring
297            // `data.read()` here and then reading through the overlay would
298            // re-enter the same in-memory RwLock while a publisher waits on the
299            // write side — a self-deadlock.
300            let guard = if sequencer_overlay.is_none() {
301                Some(self.health.guard(self.data.read())?)
302            } else {
303                None
304            };
305            let native = if sequencer_overlay.is_none() {
306                self.native_transaction_view()?
307            } else {
308                None
309            };
310            let base = TxnBase::resolve(sequencer_overlay.clone(), None, native, guard.as_deref());
311            let mut txn = Txn {
312                base,
313                buffer: BTreeMap::new(),
314                purge_keys: BTreeSet::default(),
315                set_keys: BTreeSet::default(),
316                watermark_keys: BTreeSet::default(),
317                ids: &self.ids,
318                deps: Some(&build_deps),
319            };
320            let build_started = std::time::Instant::now();
321            let build_result = build(provisional, &mut txn);
322            self.observe_phase(CommitPhase::Build, build_started);
323            match build_result {
324                Ok(outcome) => outcome,
325                Err(reason) => {
326                    drop(txn);
327                    if admission.is_some() {
328                        self.record_aborted_outcome_admitted(commit_key)?;
329                    } else {
330                        self.record_aborted_outcome(commit_key)?;
331                    }
332                    return Ok(CommitOutcome::Conflicted(reason));
333                }
334            }
335        };
336        // The storage boundary owns idempotency. Planners cannot accidentally
337        // emit a nil or mismatched key into the replicated command log.
338        batch.commit_key = commit_key;
339        if routed_tenant.is_some_and(|tenant| batch.tenant != *tenant) {
340            return Err(StorageError::Builder(
341                "commit batch tenant does not match the routed tenant".into(),
342            ));
343        }
344
345        // When a replication seam is installed, admission still validates
346        // locally (leader planning), then the batch is routed through the
347        // leader. `apply_replicated_batch` on this node publishes the outcome
348        // durably, and the returned commit index + payload reproduce the
349        // caller's response. Aborts stay local and durable.
350        if let Some(replicator) = replicator {
351            for pc in &batch.preconditions {
352                let exists = self.try_get(&pc.key)?.is_some();
353                let violated =
354                    exists != matches!(pc.kind, rushdb_types::command::PreconditionKind::Exists);
355                if violated {
356                    self.record_aborted_outcome(commit_key)?;
357                    return Err(StorageError::Precondition {
358                        path: rushdb_types::keys::describe_key(&pc.key),
359                    });
360                }
361            }
362            let payload_bytes =
363                serde_json::to_vec(&payload).map_err(|_| StorageError::OutcomePayload)?;
364            replicator
365                .replicate(&batch, &payload_bytes)
366                .map_err(StorageError::Builder)?;
367            return match self.lookup_outcome_checked(commit_key)? {
368                Some((OutcomeStatus::Committed, ci)) => Ok(CommitOutcome::Committed {
369                    commit_index: ci,
370                    payload,
371                }),
372                _ => Err(StorageError::CorruptLog),
373            };
374        }
375
376        if let (Some((snapshot_ci, deps)), Some(state)) = (since, admission.as_ref()) {
377            {
378                let _publish = self.health.guard(self.publish_lock.lock())?;
379                self.drain_and_publish();
380            }
381            if let Some(probe) = self.first_conflict_since(state, snapshot_ci, deps)? {
382                self.record_aborted_outcome_admitted(commit_key)?;
383                return Ok(CommitOutcome::Conflicted(format!(
384                    "{SERIALIZABLE_CONFLICT} on write at ci {probe}"
385                )));
386            }
387        }
388
389        for pc in &batch.preconditions {
390            let exists = exists_in_view(&pc.key)?;
391            let violated =
392                exists != matches!(pc.kind, rushdb_types::command::PreconditionKind::Exists);
393            if violated {
394                // Terminal and durable: a retry under the same commit key
395                // must discover `aborted`, never run again or report success.
396                self.record_aborted_outcome_admitted(commit_key)?;
397                return Err(StorageError::Precondition {
398                    path: rushdb_types::keys::describe_key(&pc.key),
399                });
400            }
401        }
402
403        let provisional = self.next_commit_index.load(Ordering::SeqCst);
404        batch.start_index = provisional.saturating_sub(1);
405
406        // Deterministic expansion + log/outcome bookkeeping. The expansion
407        // runs BEFORE the slot is reserved so an expansion failure does not
408        // consume a commit index (the log stays dense). The reservation is a
409        // CAS from the expansion's `provisional`; if another admission
410        // reserved in between, the expansion is retried at the current value.
411        let payload_bytes =
412            serde_json::to_vec(&payload).map_err(|_| StorageError::OutcomePayload)?;
413        let expand_at = |ci: u64| -> Result<(Vec<Op>, DependencySet), StorageError> {
414            let expand_deps = RefCell::new(DependencySet::default());
415            // As in the build block: with an overlay the live map is never
416            // read, so do not take its read guard (which the overlay's base
417            // read would re-enter).
418            let guard = if sequencer_overlay.is_none() {
419                Some(self.health.guard(self.data.read())?)
420            } else {
421                None
422            };
423            let native = if sequencer_overlay.is_none() {
424                self.native_transaction_view()?
425            } else {
426                None
427            };
428            let base = TxnBase::resolve(sequencer_overlay.clone(), None, native, guard.as_deref());
429            let wtxn = Txn {
430                base,
431                buffer: BTreeMap::new(),
432                purge_keys: BTreeSet::default(),
433                set_keys: BTreeSet::default(),
434                watermark_keys: BTreeSet::default(),
435                ids: &self.ids,
436                // Record expansion reads so `prepared.reads` is non-empty and
437                // admission is held through publication. Otherwise a
438                // read-dependent expander (e.g. a counter) releases admission
439                // early and concurrent commits overwrite each other.
440                deps: Some(&expand_deps),
441            };
442            let ops = self.expand_commit_ops(ci, &batch, &payload_bytes, wtxn)?;
443            Ok((ops, expand_deps.into_inner()))
444        };
445
446        // Computed once, up front, so the size check and the WAL/outcome writers never re-serialize it.
447        let command_payload = rushdb_types::command_codec::encode(&batch);
448
449        // Reserve the slot with a CAS loop: expansion runs *before* reservation so a failed
450        // expansion consumes no commit index (the log stays dense); if another admission
451        // reserved in between, re-read and re-expand at the new value — never `fetch_add` a
452        // slot before expansion, which would leak a hole on a failed retry. The size check
453        // runs inside the loop, before reservation.
454        let expand_started = std::time::Instant::now();
455        let (ci, ops, expand_deps) = loop {
456            let ci = self.next_commit_index.load(Ordering::SeqCst);
457            let (ops, deps) = expand_at(ci)?;
458            self.reject_oversized_commit(&ops, command_payload.len())?;
459            if self
460                .next_commit_index
461                .compare_exchange(ci, ci + 1, Ordering::SeqCst, Ordering::SeqCst)
462                .is_ok()
463            {
464                break (ci, ops, deps);
465            }
466        };
467        self.observe_phase(CommitPhase::Expand, expand_started);
468
469        let shard =
470            resources::physical_shard_for(Some(&batch.tenant), self.resource_config.shard_count);
471        let mut prepared = PreparedCommand::from_ops(ci, &ops);
472        let mut build_deps = build_deps.into_inner();
473        build_deps.extend(expand_deps);
474        prepared.reads = build_deps;
475        // Planned and expanded against the sequencer overlay under admission
476        // (the replicated path returned above), so every predecessor's writes
477        // were visible to this command's reads.
478        prepared.sequenced = sequencer_overlay.is_some();
479        self.enqueue_and_publish_prepared(
480            ci,
481            shard,
482            ops,
483            command_payload,
484            Some(prepared),
485            admission,
486            Some(shard_guard),
487        )?;
488        Ok(CommitOutcome::Committed {
489            commit_index: ci,
490            payload,
491        })
492    }
493
494    /// Enqueue a fully-admitted op-set and wait until it is durably
495    /// persisted + applied. The committing thread attempts to become the
496    /// group-commit publisher; if another thread already holds the publish
497    /// lock, that publisher drains everything currently pending (including
498    /// this entry) and acks it.
499    pub(super) fn enqueue_and_publish(
500        &self,
501        ci: u64,
502        shard: usize,
503        ops: Vec<Op>,
504        command_payload: Vec<u8>,
505    ) -> Result<(), StorageError> {
506        self.check_health()?;
507        self.enqueue_and_publish_prepared(ci, shard, ops, command_payload, None, None, None)
508    }
509
510    #[expect(
511        clippy::too_many_arguments,
512        reason = "one ordered enqueue carries the reserved index, shard, ops, originating command, prepared metadata, and the two serialization guards it releases before the durability wait"
513    )]
514    pub(super) fn enqueue_and_publish_prepared(
515        &self,
516        ci: u64,
517        shard: usize,
518        ops: Vec<Op>,
519        command_payload: Vec<u8>,
520        prepared: Option<PreparedCommand>,
521        mut admission: Option<std::sync::MutexGuard<'_, SequencerState>>,
522        shard_guard: Option<std::sync::MutexGuard<'_, ()>>,
523    ) -> Result<(), StorageError> {
524        let started = std::time::Instant::now();
525        self.check_health()?;
526        if self.maintenance_aborted() {
527            return Err(StorageError::Durability(
528                "store is shutting down; commit was not enqueued".into(),
529            ));
530        }
531        let (ack_tx, ack_rx) = mpsc::channel();
532        {
533            let mut pending = self.health.guard(self.pending.lock())?;
534            self.check_publisher()?;
535            // Record the ordered commit in the pending overlay *before* it can
536            // be published, so the very next writer to acquire admission sees
537            // it. Recording under `pending` means an aborted enqueue cannot
538            // leave a phantom overlay entry.
539            if let Some(state) = admission.as_mut() {
540                state.record(ci, &ops);
541            }
542            pending.insert(
543                ci,
544                PendingCommit {
545                    ci,
546                    shard,
547                    prepared: prepared.unwrap_or_else(|| PreparedCommand::from_ops(ci, &ops)),
548                    ops,
549                    command_payload,
550                    ack: ack_tx,
551                },
552            );
553        }
554        // The ordered entry is now visible in `pending`, so the next writer may
555        // sequence. Release the serialization locks *before* waiting on
556        // durability: concurrent commits then accumulate in `pending` and
557        // `drain_and_publish` coalesces them into one fsync. Their expansions
558        // already read this commit's ops from the pending overlay, so releasing
559        // here cannot expose them to stale state.
560        drop(admission);
561        drop(shard_guard);
562        self.pending_cv.notify_all();
563
564        loop {
565            // Try to become the publisher — only until this commit is
566            // published; later commits belong to their own committers.
567            if let Ok(guard) = self.publish_lock.try_lock() {
568                self.drain_and_publish_until(Some(ci));
569                drop(guard);
570            }
571            match ack_rx.try_recv() {
572                Ok(outcome) => {
573                    if outcome.is_ok() {
574                        self.observe_phase(CommitPhase::Durable, started);
575                    }
576                    return outcome;
577                }
578                Err(mpsc::TryRecvError::Disconnected) => return Err(StorageError::CorruptLog),
579                Err(mpsc::TryRecvError::Empty) => {}
580            }
581            // Wait for publish progress (a lower ci may still be admitted).
582            let mut pending = self.health.guard(self.pending.lock())?;
583            match ack_rx.try_recv() {
584                Ok(outcome) => {
585                    if outcome.is_ok() {
586                        self.observe_phase(CommitPhase::Durable, started);
587                    }
588                    return outcome;
589                }
590                Err(mpsc::TryRecvError::Disconnected) => return Err(StorageError::CorruptLog),
591                Err(mpsc::TryRecvError::Empty) => {}
592            }
593            self.check_publisher()?;
594            // A shutdown must not leave a committer waiting forever for a
595            // publisher that will never run. Atomically dequeue our own entry
596            // under `pending`: if it is still queued, no publisher can have
597            // published it, so a terminal error is accurate. If a publisher
598            // already took it, the commit is in flight — keep waiting for its
599            // ack (or the dropped-sender `Disconnected` error) so we report the
600            // true outcome instead of a spurious failure.
601            if self.maintenance_aborted() && pending.remove(&ci).is_some() {
602                return Err(StorageError::Durability(
603                    "store is shutting down before the commit was published".into(),
604                ));
605            }
606            // A read-view/maintenance operation may hold publish_lock without
607            // becoming a publisher. Retry after a bounded wait in that case;
608            // a notification must not be the only route to terminal progress.
609            pending = self
610                .health
611                .guard(
612                    self.pending_cv
613                        .wait_timeout(pending, std::time::Duration::from_millis(10)),
614                )?
615                .0;
616            drop(pending);
617        }
618    }
619
620    /// Serializable-optimistic entry point (document 07): `deps` records
621    /// point/range reads performed at `snapshot_ci`; validation rejects if
622    /// any committed mutation between then and now writes into a recorded
623    /// range. Runs while the publish lock is held so validation and
624    /// publication are atomic; a loser records its terminal-aborted outcome.
625    ///
626    /// # Errors
627    /// Returns [`StorageError`] if the publisher has failed, a precondition is
628    /// violated, a conflict summary is missing, the snapshot is in the future,
629    /// or publication fails. A poisoned admission, publish, or data lock
630    /// permanently fences the store and returns [`StorageError::Unhealthy`].
631    ///
632    /// # Panics
633    /// Panics only if `batch` fails to serialize (never, for a well-formed
634    /// `CommitBatch`).
635    pub fn commit_serializable<P, F>(
636        &self,
637        commit_key: Uuid,
638        snapshot_ci: u64,
639        deps: DependencySet,
640        build: F,
641    ) -> Result<CommitOutcome<P>, StorageError>
642    where
643        P: Serialize + DeserializeOwned,
644        F: FnOnce(&mut Txn) -> Result<(CommitBatch, P), String>,
645    {
646        self.check_health()?;
647        let mut admission = self.acquire_admission()?;
648        self.check_publisher()?;
649        if let Some((status, prev_ci)) = self.lookup_outcome_checked(commit_key)? {
650            return match status {
651                OutcomeStatus::Aborted => Ok(CommitOutcome::Conflicted(
652                    "previously aborted under this commit key".to_string(),
653                )),
654                OutcomeStatus::Committed => Ok(CommitOutcome::Committed {
655                    commit_index: prev_ci,
656                    payload: self.committed_payload(commit_key)?,
657                }),
658            };
659        }
660
661        if snapshot_ci > self.committed_version() {
662            return Err(StorageError::FutureSnapshot {
663                requested: snapshot_ci,
664                visible: self.committed_version(),
665            });
666        }
667        // Refresh the pending overlay and plan/expand against it so this commit
668        // sees every lower-index commit, published or not. In-flight writes are
669        // additionally checked against `deps` below because they are not yet in
670        // the conflict window.
671        self.trim_sequencer(&mut admission);
672        let sequencer_overlay = self.sequencer_overlay(&admission)?;
673        // This entry point preserves the existing staging-at-commit contract;
674        // supplied dependencies describe the caller's earlier reads. It does
675        // not itself implement begin-time transactional reads.
676        let dep_recorder = RefCell::new(deps);
677        let (mut batch, payload) = {
678            // The sequencer overlay is authoritative, so base reads go through
679            // it and never touch the live map; taking its read guard would let
680            // the overlay's base read re-enter the same RwLock (self-deadlock).
681            let mut txn = Txn {
682                base: TxnBase::Overlay(Arc::clone(&sequencer_overlay)),
683                buffer: BTreeMap::new(),
684                purge_keys: BTreeSet::default(),
685                set_keys: BTreeSet::default(),
686                watermark_keys: BTreeSet::default(),
687                ids: &self.ids,
688                deps: Some(&dep_recorder),
689            };
690            match build(&mut txn) {
691                Ok(outcome) => outcome,
692                Err(reason) => {
693                    drop(txn);
694                    self.record_aborted_outcome_admitted(commit_key)?;
695                    return Ok(CommitOutcome::Conflicted(reason));
696                }
697            }
698        };
699        let deps = dep_recorder.into_inner();
700        batch.commit_key = commit_key;
701
702        // Admission excludes concurrent logical writers through publication.
703        // Select a tentative CI; reserve it only after successful expansion.
704        let ci = {
705            let _guard = self.health.guard(self.publish_lock.lock())?;
706            // Publish any pending commits so the committed log is complete.
707            self.drain_and_publish();
708
709            // Never replay an old command against today's state to infer what
710            // it once wrote. Missing legacy summaries are an explicit retry,
711            // not evidence of the absence of a conflict.
712            // In-flight commits (assigned but not yet published) are not in
713            // the conflict window; `first_conflict_since` consults the pending
714            // overlay too, so a dependency cannot be missed against an
715            // unpublished writer.
716            if let Some(probe) = self.first_conflict_since(&admission, snapshot_ci, &deps)? {
717                // An aborted tx consumes no commit index; the terminal aborted
718                // outcome is keyed by the commit key, so the contiguous
719                // publisher never has to fill a hole for it.
720                self.record_outcome_locked(commit_key, OutcomeStatus::Aborted, 0)?;
721                return Ok(CommitOutcome::Conflicted(format!(
722                    "{SERIALIZABLE_CONFLICT} on write at ci {probe}"
723                )));
724            }
725
726            for pc in &batch.preconditions {
727                let exists = sequencer_overlay.get(&pc.key)?.is_some();
728                let violated =
729                    exists != matches!(pc.kind, rushdb_types::command::PreconditionKind::Exists);
730                if violated {
731                    self.record_outcome_locked(commit_key, OutcomeStatus::Aborted, 0)?;
732                    return Err(StorageError::Precondition {
733                        path: rushdb_types::keys::describe_key(&pc.key),
734                    });
735                }
736            }
737
738            // The candidate slot is derived from the published watermark; the
739            // actual reservation happens after expansion succeeds so an
740            // expansion failure never consumes a commit index.
741            self.published.load(Ordering::SeqCst) + 1
742        };
743
744        // Feeds the WAL's embedded command on a durable store, or
745        // the in-memory command-journal row otherwise (no WAL to derive it from).
746        let command_payload = rushdb_types::command_codec::encode(&batch);
747        let payload_bytes =
748            serde_json::to_vec(&payload).map_err(|_| StorageError::OutcomePayload)?;
749        let expand_at = |ci: u64| -> Result<Vec<Op>, StorageError> {
750            // The overlay is authoritative; skip the live map's read guard.
751            let wtxn = Txn {
752                base: TxnBase::Overlay(Arc::clone(&sequencer_overlay)),
753                buffer: BTreeMap::new(),
754                purge_keys: BTreeSet::default(),
755                set_keys: BTreeSet::default(),
756                watermark_keys: BTreeSet::default(),
757                ids: &self.ids,
758                deps: None,
759            };
760            self.expand_commit_ops(ci, &batch, &payload_bytes, wtxn)
761        };
762
763        // Admission is held for the whole protocol, so the prospective `ci`
764        // cannot move. Expand first (a failure must not consume a slot), then
765        // reserve with a plain store; the previous CAS/`fetch_add` fallback
766        // could leak a reserved index when the re-expansion failed.
767        let ops = expand_at(ci)?;
768        self.reject_oversized_commit(&ops, command_payload.len())?;
769        admission.record(ci, &ops);
770        self.next_commit_index.store(ci + 1, Ordering::SeqCst);
771        let shard =
772            resources::physical_shard_for(Some(&batch.tenant), self.resource_config.shard_count);
773        self.enqueue_and_publish(ci, shard, ops, command_payload)?;
774        Ok(CommitOutcome::Committed {
775            commit_index: ci,
776            payload,
777        })
778    }
779
780    /// Run one statement's planning + expansion against a transaction's
781    /// accumulated overlay (read-your-writes), appending the produced writes to
782    /// the transaction. Nothing is committed or visible outside the tx.
783    ///
784    /// # Errors
785    /// Returns [`StorageError`] if the publisher has failed, no expander is
786    /// installed, or the supplied `build`/expansion closure fails.
787    ///
788    /// Lock poisoning fences the store until reopen.
789    pub fn tx_apply<P, F>(
790        &self,
791        tx: &mut TxAccumulator,
792        commit_key: Uuid,
793        build: F,
794    ) -> Result<P, StorageError>
795    where
796        P: Serialize + DeserializeOwned,
797        F: FnOnce(u64, &mut Txn) -> Result<(CommitBatch, P), String>,
798    {
799        self.check_health()?;
800        self.check_publisher()?;
801        let ci = self.next_commit_index.load(Ordering::SeqCst);
802        let payload = {
803            // Statement planning reads the transaction's pinned snapshot, not
804            // current committed state, so read-your-writes is stable and
805            // later commits cannot leak into the plan.
806            let snapshot = tx.base_view.is_some();
807            let guard = self.txn_data(snapshot)?;
808            let native = if snapshot {
809                None
810            } else {
811                self.native_transaction_view()?
812            };
813            let base = TxnBase::resolve(None, tx.base_view.clone(), native, guard.as_deref());
814            // Both statement planning and deterministic expansion can branch
815            // on committed state. Preserve those reads in the accumulator so
816            // tx_commit can reject a stale read-dependent transaction even
817            // when the branch did not emit the key it inspected.
818            let dep_recorder = RefCell::new(tx.deps.clone());
819            let mut txn = Txn {
820                base,
821                buffer: tx.buffer.clone(),
822                purge_keys: BTreeSet::default(),
823                set_keys: BTreeSet::default(),
824                watermark_keys: BTreeSet::default(),
825                ids: &self.ids,
826                deps: Some(&dep_recorder),
827            };
828            let (batch, payload) = build(ci, &mut txn).map_err(StorageError::Builder)?;
829            let mut batch = batch;
830            batch.commit_key = commit_key;
831            let expander = self.health.guard(self.expander.read())?.clone();
832            let Some(expand) = expander.as_ref() else {
833                return Err(StorageError::CorruptLog);
834            };
835            expand(ci, &batch, &mut txn).map_err(StorageError::Builder)?;
836            tx.buffer = txn.buffer;
837            tx.watermarks.extend(txn.watermark_keys);
838            tx.mutations.append(&mut batch.mutations);
839            tx.preconditions.append(&mut batch.preconditions);
840            tx.deps = dep_recorder.into_inner();
841            payload
842        };
843        Ok(payload)
844    }
845
846    /// Run a read inside a transaction: the closure sees committed state merged
847    /// with the transaction's overlay (read-your-writes) and records its reads
848    /// into the transaction's dependency set. Use `get_tracked`/range-tracked
849    /// reads so the conflict check at commit covers what this read observed.
850    ///
851    /// # Errors
852    /// Returns [`StorageError`] if a backend read fails or the closure returns
853    /// an error.
854    ///
855    /// Lock poisoning fences the store until reopen.
856    pub fn tx_read<P, F>(&self, tx: &mut TxAccumulator, f: F) -> Result<P, StorageError>
857    where
858        F: FnOnce(&mut Txn) -> Result<P, String>,
859    {
860        self.check_health()?;
861        // Reads resolve at the transaction's pinned snapshot.
862        let snapshot = tx.base_view.is_some();
863        let guard = self.txn_data(snapshot)?;
864        let native = if snapshot {
865            None
866        } else {
867            self.native_transaction_view()?
868        };
869        let base = TxnBase::resolve(None, tx.base_view.clone(), native, guard.as_deref());
870        let dep_recorder = RefCell::new(tx.deps.clone());
871        let mut txn = Txn {
872            base,
873            buffer: tx.buffer.clone(),
874            purge_keys: BTreeSet::default(),
875            set_keys: BTreeSet::default(),
876            watermark_keys: BTreeSet::default(),
877            ids: &self.ids,
878            deps: Some(&dep_recorder),
879        };
880        let result = f(&mut txn).map_err(StorageError::Builder);
881        tx.deps = dep_recorder.into_inner();
882        result
883    }
884
885    /// Stage a pre-built commit batch (e.g. a statement-language mutation delta)
886    /// into a transaction: the batch's writes expand against the transaction's
887    /// overlay and append to its accumulated mutations. Nothing is committed or
888    /// visible outside the tx.
889    ///
890    /// # Errors
891    /// Returns [`StorageError`] if the publisher has failed, no expander is
892    /// installed, or expansion fails.
893    ///
894    /// Lock poisoning fences the store until reopen.
895    pub fn tx_apply_batch(
896        &self,
897        tx: &mut TxAccumulator,
898        commit_key: Uuid,
899        batch: &CommitBatch,
900    ) -> Result<(), StorageError> {
901        self.check_health()?;
902        self.check_publisher()?;
903        let mut batch = batch.clone();
904        batch.commit_key = commit_key;
905        let ci = self.next_commit_index.load(Ordering::SeqCst);
906        // Batch expansion reads the transaction's pinned snapshot.
907        let snapshot = tx.base_view.is_some();
908        let guard = self.txn_data(snapshot)?;
909        let native = if snapshot {
910            None
911        } else {
912            self.native_transaction_view()?
913        };
914        let base = TxnBase::resolve(None, tx.base_view.clone(), native, guard.as_deref());
915        // A batch expander may read a key to decide which derived rows to
916        // produce. Track that dependency just like tx_apply tracks planner
917        // and expansion reads; otherwise stale read-dependent commits can
918        // pass conflict validation when their write-set is disjoint.
919        let dep_recorder = RefCell::new(tx.deps.clone());
920        let mut txn = Txn {
921            base,
922            buffer: tx.buffer.clone(),
923            purge_keys: BTreeSet::default(),
924            set_keys: BTreeSet::default(),
925            watermark_keys: BTreeSet::default(),
926            ids: &self.ids,
927            deps: Some(&dep_recorder),
928        };
929        let expander = self.health.guard(self.expander.read())?.clone();
930        let Some(expand) = expander.as_ref() else {
931            return Err(StorageError::CorruptLog);
932        };
933        expand(ci, &batch, &mut txn).map_err(StorageError::Builder)?;
934        tx.buffer = txn.buffer;
935        tx.watermarks.extend(txn.watermark_keys);
936        tx.mutations.append(&mut batch.mutations);
937        tx.preconditions.append(&mut batch.preconditions);
938        tx.deps = dep_recorder.into_inner();
939        Ok(())
940    }
941
942    /// Atomically commit a transaction's accumulated writes. Conflict rule: any
943    /// committed mutation since `base_ci` that wrote a key the transaction read
944    /// (public read-set) or writes (write-set) aborts it. Monotone counters
945    /// (`Txn::set_watermark`) are exempt: ids come from the store's
946    /// [`crate::IdAllocator`], so concurrent transactions never share an
947    /// ordinal, and each commit only raises the counter. Success publishes the writes as one
948    /// atomic commit; conflict aborts the transaction, which may retry or roll back.
949    ///
950    /// # Errors
951    /// Returns [`StorageError`] if the publisher has failed, the base snapshot
952    /// is in the future, a precondition fails, a conflict is detected, or
953    /// publication fails.
954    pub fn tx_commit(&self, commit_key: Uuid, tx: &TxAccumulator) -> Result<u64, StorageError> {
955        self.check_health()?;
956        self.tx_commit_impl(commit_key, tx, None)
957    }
958
959    /// Commit a cross-request transaction with an optional revalidator that runs inside
960    /// the publish lock (atomic with conflict detection), reading keys via the injected
961    /// reader — never by opening a new store view, which would nest view locks under the
962    /// publish lock — and returning `Err` to abort the commit and record a terminal outcome.
963    ///
964    /// # Errors
965    /// Returns [`StorageError`] if the publisher has failed, the base snapshot is in the future,
966    /// a precondition fails, the revalidator rejects, a conflict is detected, or publication fails.
967    pub fn tx_commit_revalidated(
968        &self,
969        commit_key: Uuid,
970        tx: &TxAccumulator,
971        revalidate: impl FnOnce(&dyn Fn(&[u8]) -> Result<Option<Vec<u8>>, StorageError>) -> Result<(), String>
972            + 'static,
973    ) -> Result<u64, StorageError> {
974        self.check_health()?;
975        let revalidate: Box<
976            dyn FnOnce(
977                &dyn Fn(&[u8]) -> Result<Option<Vec<u8>>, StorageError>,
978            ) -> Result<(), String>,
979        > = Box::new(revalidate);
980        self.tx_commit_impl(commit_key, tx, Some(revalidate))
981    }
982
983    #[expect(
984        clippy::too_many_lines,
985        reason = "transaction commit keeps conflict probing, revalidation, index reservation, and publication in one linear protocol for auditability"
986    )]
987    pub(super) fn tx_commit_impl(
988        &self,
989        commit_key: Uuid,
990        tx: &TxAccumulator,
991        revalidate: Option<
992            Box<
993                dyn FnOnce(
994                    &dyn Fn(&[u8]) -> Result<Option<Vec<u8>>, StorageError>,
995                ) -> Result<(), String>,
996            >,
997        >,
998    ) -> Result<u64, StorageError> {
999        self.check_health()?;
1000        // A replicated commit is assigned and published by the replication
1001        // path. Reserving a local slot first would advance
1002        // `next_commit_index`; a loopback replicator would then apply at the
1003        // following index and leave the reserved slot permanently missing.
1004        // Keep the selected replicator for the call below so the admission
1005        // decision and the publication path agree.
1006        let replicator = self.replicator();
1007        // One writer-admission protocol for the whole commit: discovery,
1008        // validation, expansion, CI reservation, publication and ack. Without
1009        // it, two transactions can validate against the same published
1010        // watermark and both reserve, missing interleaved write/read conflicts
1011        // (a lost update). A replicator owns ordering, so admission is skipped
1012        // there exactly as in the autocommit path (holding it across the
1013        // synchronous replication call would deadlock the state-machine apply).
1014        let mut admission = if replicator.is_none() {
1015            Some(self.acquire_admission()?)
1016        } else {
1017            None
1018        };
1019        self.check_publisher()?;
1020        if let Some((OutcomeStatus::Committed, ci)) = self.lookup_outcome_checked(commit_key)? {
1021            return Ok(ci);
1022        }
1023        if matches!(
1024            self.try_outcome_for(commit_key)?,
1025            Some(OutcomeKind::Aborted)
1026        ) {
1027            return Err(StorageError::Builder(
1028                "commit key has a terminal aborted outcome".into(),
1029            ));
1030        }
1031        if tx.base_ci > self.committed_version() {
1032            return Err(StorageError::FutureSnapshot {
1033                requested: tx.base_ci,
1034                visible: self.committed_version(),
1035            });
1036        }
1037        // This path expands against the transaction's pinned snapshot, not the
1038        // live overlay: a cross-request transaction must not observe commits
1039        // after its snapshot. `drain_and_publish` below publishes every
1040        // assigned-but-unpublished commit before validation, so the durable
1041        // conflict summaries already cover them; the pending buffer is trimmed
1042        // here and consulted below as a defensive second source.
1043        if let Some(state) = admission.as_mut() {
1044            self.trim_sequencer(state);
1045        }
1046
1047        // Conflict check + CI selection under the publish lock. The guard is
1048        // scoped to this block: `enqueue_and_publish` must be able to become
1049        // the publisher afterwards.
1050        let ci = {
1051            let _guard = self.health.guard(self.publish_lock.lock())?;
1052            self.drain_and_publish();
1053            // The slot is reserved only after every abort check passes. An
1054            // aborted transaction must NOT consume a commit index: it records
1055            // a terminal aborted outcome keyed by its commit key, and the
1056            // contiguous publisher never has to fill a hole for it.
1057            let published = self.published.load(Ordering::SeqCst);
1058
1059            // Authorization revalidation runs before the generic read/write
1060            // conflict scan so a rotated or malformed policy surfaces as an
1061            // explicit `authorization_changed`/`invalid authorization policy`
1062            // abort rather than being masked by the policy key's read-write
1063            // conflict. It still runs under the publish lock, atomic with
1064            // conflict detection and publication.
1065            if let Some(revalidate) = revalidate {
1066                let read =
1067                    |key: &[u8]| -> Result<Option<Vec<u8>>, StorageError> { self.try_get(key) };
1068                if let Err(reason) = revalidate(&read) {
1069                    self.record_outcome_locked(commit_key, OutcomeStatus::Aborted, 0)?;
1070                    return Err(StorageError::Builder(reason));
1071                }
1072            }
1073
1074            // Read-set (public reads) + write-set (buffer, incl. counters) check.
1075            // Only published commits since the tx's snapshot can conflict; an
1076            // in-flight reserved slot is not visible yet and is serialized by
1077            // the publisher before this commit.
1078            let mut conflicted: Option<u64> = None;
1079            let window = self.health.guard(self.conflicts.lock())?;
1080            for probe in (tx.base_ci + 1)..=published {
1081                let key_set =
1082                    window
1083                        .keys(probe)
1084                        .ok_or(StorageError::ConflictHistoryUnavailable {
1085                            commit_index: probe,
1086                        })?;
1087                for k in key_set.iter() {
1088                    if tx.deps.intersects_key(k) || writes_conflicting(tx, k) {
1089                        conflicted = Some(probe);
1090                        break;
1091                    }
1092                }
1093                if conflicted.is_some() {
1094                    break;
1095                }
1096            }
1097            drop(window);
1098            // Defensive: consult the pending overlay for any commit that
1099            // `drain_and_publish` did not publish (for example a mid-assignment
1100            // gap), so an unpublished writer cannot be missed.
1101            if let Some(state) = admission.as_ref() {
1102                for (key, (ci, _)) in &state.buffer {
1103                    if *ci > tx.base_ci
1104                        && (tx.deps.intersects_key(key) || writes_conflicting(tx, key))
1105                    {
1106                        conflicted = Some(*ci);
1107                        break;
1108                    }
1109                }
1110            }
1111            if let Some(probe) = conflicted {
1112                self.record_outcome_locked(commit_key, OutcomeStatus::Aborted, 0)?;
1113                return Err(StorageError::Builder(format!(
1114                    "transaction_conflict on write at ci {probe}"
1115                )));
1116            }
1117            for pc in &tx.preconditions {
1118                // Preconditions from statements in one transaction must see
1119                // the transaction's own write overlay. Relationship batches,
1120                // for example, require endpoints created earlier in this same
1121                // transaction to exist before the final commit is published.
1122                let exists = match pc.kind {
1123                    // An `Absent` guard belongs to the create operation that
1124                    // also stages this key. Check committed state so that the
1125                    // operation does not conflict with its own overlay.
1126                    rushdb_types::command::PreconditionKind::Absent => {
1127                        self.try_get(&pc.key)?.is_some()
1128                    }
1129                    // An `Exists` guard holds when the key is committed or
1130                    // this transaction wrote it: relationship batches need
1131                    // endpoints created earlier in the same transaction, and
1132                    // a delete guards a key its own write removes.
1133                    rushdb_types::command::PreconditionKind::Exists => {
1134                        matches!(tx.buffer.get(&pc.key), Some(Some(_)))
1135                            || self.try_get(&pc.key)?.is_some()
1136                    }
1137                };
1138                let violated =
1139                    exists != matches!(pc.kind, rushdb_types::command::PreconditionKind::Exists);
1140                if violated {
1141                    self.record_outcome_locked(commit_key, OutcomeStatus::Aborted, 0)?;
1142                    return Err(StorageError::Precondition {
1143                        path: rushdb_types::keys::describe_key(&pc.key),
1144                    });
1145                }
1146            }
1147            // Prospective slot only. The slot is reserved after expansion
1148            // succeeds so a fallible expansion can never consume (and strand) a
1149            // commit index. Admission is held for the local path, so no other
1150            // local writer can change the counter in between; a replicator owns
1151            // index assignment and must observe the current slot unchanged.
1152            self.next_commit_index.load(Ordering::SeqCst)
1153        };
1154
1155        let batch = CommitBatch {
1156            command_version: rushdb_types::command::COMMAND_VERSION,
1157            tenant: tx.tenant,
1158            transaction_id: String::new(),
1159            commit_key,
1160            start_index: ci.saturating_sub(1),
1161            isolation: rushdb_types::command::IsolationMode::Snapshot,
1162            preconditions: tx.preconditions.clone(),
1163            mutations: tx.mutations.clone(),
1164        };
1165
1166        // When a replication seam is installed, the tx commits by replicating
1167        // the batch (like every other commit) instead of publishing locally.
1168        if let Some(replicator) = replicator {
1169            let payload_bytes: Vec<u8> = b"{}".to_vec();
1170            replicator
1171                .replicate(&batch, &payload_bytes)
1172                .map_err(StorageError::Builder)?;
1173            return match self.lookup_outcome_checked(commit_key)? {
1174                Some((OutcomeStatus::Committed, replicated_ci)) => Ok(replicated_ci),
1175                _ => Err(StorageError::CorruptLog),
1176            };
1177        }
1178
1179        // Feeds the WAL's embedded command on a durable store, or
1180        // the in-memory command-journal row otherwise (no WAL to derive it from).
1181        let command_payload = rushdb_types::command_codec::encode(&batch);
1182        let ops = {
1183            // Expand against current state (committed plus pending), as an
1184            // autocommit and a replay of this command do. The conflict check
1185            // above aborted the transaction if any key it read or wrote
1186            // changed since its snapshot, so for those keys this is the state
1187            // it observed. The exception is monotone counters
1188            // (`Txn::set_watermark`), which concurrent commits may have
1189            // advanced: reading them here keeps them from moving back.
1190            let overlay = match admission.as_ref() {
1191                Some(state) => Some(self.sequencer_overlay(state)?),
1192                None => None,
1193            };
1194            // The overlay is authoritative; taking the live map's read guard
1195            // as well would re-enter the RwLock a publisher may be waiting on.
1196            let guard = if overlay.is_none() {
1197                Some(self.health.guard(self.data.read())?)
1198            } else {
1199                None
1200            };
1201            let native = if overlay.is_none() {
1202                self.native_transaction_view()?
1203            } else {
1204                None
1205            };
1206            let base = TxnBase::resolve(overlay, None, native, guard.as_deref());
1207            let mut wtxn = Txn {
1208                base,
1209                buffer: BTreeMap::new(),
1210                purge_keys: BTreeSet::default(),
1211                set_keys: BTreeSet::default(),
1212                watermark_keys: BTreeSet::default(),
1213                ids: &self.ids,
1214                deps: None,
1215            };
1216            let expander = self.health.guard(self.expander.read())?.clone();
1217            let Some(expand) = expander.as_ref() else {
1218                return Err(StorageError::CorruptLog);
1219            };
1220            expand(ci, &batch, &mut wtxn).map_err(StorageError::Builder)?;
1221            if !self.is_durable() {
1222                wtxn.put(log_key(ci), wal::encode_wal_record(0, ci, &command_payload));
1223            }
1224            // Interactive transactions return no stored response body.
1225            self.put_receipt(&mut wtxn, commit_key, OutcomeStatus::Committed, ci, None);
1226            wtxn.set(LAST_APPLIED_KEY.to_vec(), wal::encode_last_applied(0, ci));
1227            wtxn.drain()
1228        };
1229
1230        self.reject_oversized_commit(&ops, command_payload.len())?;
1231        // Expansion succeeded: reserve the slot. Admission is held until this
1232        // function returns, so the prospective `ci` is still current.
1233        self.next_commit_index.store(ci + 1, Ordering::SeqCst);
1234        let shard =
1235            resources::physical_shard_for(Some(&batch.tenant), self.resource_config.shard_count);
1236        self.enqueue_and_publish(ci, shard, ops, command_payload)?;
1237        Ok(ci)
1238    }
1239}
1240
1241/// Whether a key written by a later commit collides with the transaction's
1242/// own writes. A monotone counter both advance is not a collision.
1243fn writes_conflicting(tx: &TxAccumulator, key: &[u8]) -> bool {
1244    tx.buffer.contains_key(key) && !tx.watermarks.contains(key)
1245}