Skip to content

Commit eb21592

Browse files
libsql-server: reconcile indeterminate fence commits and restart at each boundary
Add crash-restart tests for the namespace fence: each server lifetime runs on its own runtime and is ended without any shutdown code while a fence command is parked at a hook point, so the next start takes the real dirty-recovery path on the same directory. They cover every persistence boundary of AcquireSourceWriteFence and ReleaseSourceWriteFence (including a marker that lags the metastore commit), a restart in SOURCE_DRAINING with a writer active at the crash, indeterminate commits through the drain path (applied and not applied), and lost acquisition responses resolved by replay and inspection. The tests exposed that a source restarted while draining could never finish its drain: dirty recovery rebuilds the replication log under a new log id, and completing the drain refused a boundary on a log other than the one the fence was acquired against, leaving the namespace in SOURCE_DRAINING for good. The frozen boundary now names the log that is live when the drain is proven, the record's identity keeps the acquisition log id, and the server warns when the two differ. Write admission was durably closed throughout, so the data at the boundary is unchanged. The BeforeMetastoreCommit test hook can now report a commit as indeterminate without running it. The contract document describes the restart and log rebuild semantics. Co-authored-by: Tomasz Szymczyszyn <tomasz.szymczyszyn@shopify.com>
1 parent 7d5a6a8 commit eb21592

6 files changed

Lines changed: 942 additions & 21 deletions

File tree

‎libsql-server/src/namespace/fence/controller.rs‎

Lines changed: 10 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -458,11 +458,17 @@ impl Transition {
458458
}
459459
}
460460

461-
if let HookOutcome::Fail(e) = controller.hook(HookPoint::BeforeMetastoreCommit).await {
462-
return Err(e.into());
463-
}
461+
let result = match controller.hook(HookPoint::BeforeMetastoreCommit).await {
462+
HookOutcome::Continue => run.await,
463+
HookOutcome::Fail(e) => return Err(e.into()),
464+
// A commit that failed without applying, but whose outcome the controller cannot
465+
// know (test hook).
466+
HookOutcome::Indeterminate => {
467+
Err(indeterminate(key, "the commit was not acknowledged (test hook)").into())
468+
}
469+
};
464470

465-
let result = match run.await {
471+
let result = match result {
466472
Ok(commit) => match controller.hook(HookPoint::AfterMetastoreCommit).await {
467473
HookOutcome::Continue => Ok(commit),
468474
HookOutcome::Indeterminate | HookOutcome::Fail(_) => Err(indeterminate(

‎libsql-server/src/namespace/fence/drain.rs‎

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -121,6 +121,16 @@ pub async fn acquire_source_write_fence(
121121
};
122122

123123
// Step 7.
124+
let acquired_on = commit.record.as_ref().and_then(|r| r.identity.log_id);
125+
if acquired_on.is_some_and(|log_id| log_id != boundary.log_id) {
126+
tracing::warn!(
127+
namespace = %controller.namespace(),
128+
acquired_on = ?acquired_on,
129+
boundary_log_id = %boundary.log_id,
130+
"the replication log was rebuilt since the write fence was acquired (the source \
131+
restarted while draining); the frozen boundary names the rebuilt log"
132+
);
133+
}
124134
ctx.now_ms = now_ms();
125135
transition
126136
.complete_drain(

‎libsql-server/src/namespace/fence/hooks.rs‎

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -56,7 +56,9 @@ pub enum HookAction {
5656
/// Fail at this point with `error`, as if the step had failed before it took effect.
5757
Fail(FenceError),
5858
/// At `AfterMetastoreCommit`: report the commit as indeterminate even though it happened,
59-
/// which is what a lost commit acknowledgement looks like to the controller.
59+
/// which is what a lost commit acknowledgement looks like to the controller. At
60+
/// `BeforeMetastoreCommit`: report it as indeterminate without running it, which is a
61+
/// commit that failed without applying but whose outcome the controller cannot know.
6062
Indeterminate,
6163
}
6264

‎libsql-server/src/namespace/fence/mod.rs‎

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,9 @@ pub mod state;
2828
pub mod store;
2929
pub mod transition;
3030

31+
#[cfg(test)]
32+
mod tests;
33+
3134
#[allow(clippy::all)]
3235
pub(crate) mod proto {
3336
include!("../../generated/namespace_fence.rs");

0 commit comments

Comments
 (0)