Skip to content

Commit 993abd6

Browse files
libsql-server: do not create a replica namespace the primary's fence refuses
A replica server creating a namespace lazily for a name the primary's fence refuses (a quarantined or aborted target, a read-fenced source, a fence state the primary cannot establish) now fails the request at once with the primary's code (Error::NamespaceFence: 423 and the stable code) instead of retrying the handshake, and leaves no local namespace behind: the namespace directory the setup created is removed (a directory that already existed is kept), and NamespaceStore::with forgets the metastore entry handle() added and the controller the attempt created, when neither holds anything durable or is in use by another attempt. A later request, once the primary admits the name, creates it normally. Co-authored-by: Tomasz Szymczyszyn <tomasz.szymczyszyn@shopify.com>
1 parent 5f0ba5e commit 993abd6

5 files changed

Lines changed: 313 additions & 8 deletions

File tree

‎libsql-server/src/namespace/configurator/replica.rs‎

Lines changed: 25 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -67,6 +67,9 @@ impl ConfigureNamespace for ReplicaConfigurator {
6767
Box::pin(async move {
6868
tracing::debug!("creating replica namespace");
6969
let db_path = self.base.base_path.join("dbs").join(name.as_str());
70+
// Whether this server already had a copy of the namespace: a directory this setup
71+
// creates is removed again if the primary's fence refuses the namespace.
72+
let had_copy = db_path.try_exists()?;
7073
let channel = self.channel.clone();
7174
let uri = self.uri.clone();
7275

@@ -112,7 +115,28 @@ impl ConfigureNamespace for ReplicaConfigurator {
112115
)
113116
.await;
114117
}
115-
Err(e) => Err(e)?,
118+
Err(e) => {
119+
if let Some(refusal) = PrimaryFenceRefusal::of(&e) {
120+
// The primary's fence refuses the namespace (a quarantined target, a
121+
// read-fenced source, a fence state it cannot establish; section 13.3):
122+
// fail at once with its code rather than retrying the handshake, and
123+
// leave no local copy behind that this setup created.
124+
let denial = refusal.0.clone();
125+
drop(replicator);
126+
if !had_copy {
127+
if let Err(e) = tokio::fs::remove_dir_all(&db_path).await {
128+
if e.kind() != std::io::ErrorKind::NotFound {
129+
tracing::warn!(
130+
"failed to remove {} after the primary refused {name}: {e}",
131+
db_path.display()
132+
);
133+
}
134+
}
135+
}
136+
return Err(crate::Error::NamespaceFence(denial));
137+
}
138+
Err(e)?
139+
}
116140
Ok(_) => (),
117141
}
118142

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

Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -66,6 +66,29 @@ impl FenceRegistry {
6666
self.controllers.lock().remove(namespace)
6767
}
6868

69+
/// Forget `namespace`'s controller if it holds nothing worth keeping: no fence record and
70+
/// no in-memory gate (only what a replica learned of its primary's fence, which the next
71+
/// answered `hello` would replace), and nothing but the registry refers to it. For a name
72+
/// whose setup failed before it was ever served, such as a replica's lazy creation that
73+
/// the primary's fence refused. Returns whether it was forgotten.
74+
pub fn forget_idle(&self, namespace: &NamespaceName) -> bool {
75+
let mut controllers = self.controllers.lock();
76+
let idle = controllers.get(namespace).is_some_and(|controller| {
77+
let gate = controller.gate();
78+
// Under the registry lock nobody can take another reference to it.
79+
Arc::strong_count(controller) == 1
80+
&& matches!(gate.fence, StoredFence::None { .. })
81+
&& gate.indeterminate.is_none()
82+
&& gate.installing.is_none()
83+
&& gate.closing_reads.is_none()
84+
&& gate.creating_target.is_none()
85+
});
86+
if idle {
87+
controllers.remove(namespace);
88+
}
89+
idle
90+
}
91+
6992
/// Refuse a namespace whose fence state is `UNKNOWN_UNAVAILABLE`, or that is being created
7093
/// as a quarantined target, before any work is done to serve it.
7194
pub fn check_available(&self, namespace: &NamespaceName) -> Result<(), FenceError> {
@@ -245,4 +268,41 @@ mod tests {
245268
assert!(registry.remove(&"ns".into()).is_some());
246269
assert!(!Arc::ptr_eq(&registry.controller(&"ns".into()), &a));
247270
}
271+
272+
/// A replica's lazy creation that the primary refused leaves nothing behind in the registry,
273+
/// unless the controller holds fence state or somebody else still refers to it.
274+
#[test]
275+
fn forget_idle_only_unreferenced_plain_controllers() {
276+
let registry = FenceRegistry::default();
277+
assert!(!registry.forget_idle(&"missing".into()));
278+
279+
// What a refused replication taught it does not keep it.
280+
let refused = registry.controller(&"refused".into());
281+
refused.observe_primary(Some(FenceError::new(
282+
FenceOutcome::MigrationTargetQuarantined,
283+
"quarantined on the primary",
284+
)));
285+
// Still referenced: kept.
286+
assert!(!registry.forget_idle(&"refused".into()));
287+
drop(refused);
288+
assert!(registry.forget_idle(&"refused".into()));
289+
assert!(registry.get(&"refused".into()).is_none());
290+
// The next use starts from a fresh UNFENCED controller.
291+
assert!(registry
292+
.controller(&"refused".into())
293+
.permits(OperationClass::NormalRead)
294+
.is_ok());
295+
296+
// A controller with fence state is never forgotten.
297+
let registry = FenceRegistry::seeded([(
298+
"lost".into(),
299+
StoredFence::Unavailable {
300+
detail: FenceDetail::CorruptRecord,
301+
reason: "test".into(),
302+
marker: None,
303+
},
304+
)]);
305+
assert!(!registry.forget_idle(&"lost".into()));
306+
assert!(registry.get(&"lost".into()).is_some());
307+
}
248308
}

‎libsql-server/src/namespace/meta_store.rs‎

Lines changed: 68 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@ use libsql_sys::wal::{
1515
};
1616
use parking_lot::Mutex;
1717
use prost::Message;
18-
use rusqlite::TransactionBehavior;
18+
use rusqlite::{OptionalExtension, TransactionBehavior};
1919
use tokio::sync::oneshot;
2020
use tokio::sync::{
2121
mpsc,
@@ -1303,6 +1303,41 @@ impl MetaStore {
13031303
r
13041304
}
13051305

1306+
/// Take out the in-memory entry that [`handle`](Self::handle) put in the map for a
1307+
/// namespace whose creation then failed, so that [`exists`](Self::exists) and
1308+
/// [`lookup`](Self::lookup) do not report a namespace that was never created
1309+
/// (`docs/NAMESPACE_FENCE.md` section 13.3, replica lazy creation). Only an entry that no
1310+
/// handle is subscribed to any more and that has no stored config row is removed: a config
1311+
/// that was persisted, or a creation of the same name still in progress, keeps its entry.
1312+
/// Returns whether the entry was removed.
1313+
pub async fn forget_unstored(&self, namespace: NamespaceName) -> Result<bool> {
1314+
let inner = self.inner.clone();
1315+
tokio::task::spawn_blocking(move || -> std::result::Result<bool, FenceStoreError> {
1316+
// The connection lock first, as everywhere else that takes both: a config being
1317+
// persisted concurrently is either already in its row here, or finds no entry when
1318+
// it publishes and inserts its own.
1319+
let conn = inner.conn.blocking_lock();
1320+
let stored = conn
1321+
.query_row(
1322+
"SELECT 1 FROM namespace_configs WHERE namespace = ?1",
1323+
[namespace.as_str()],
1324+
|_| Ok(()),
1325+
)
1326+
.optional()?
1327+
.is_some();
1328+
let mut configs = inner.configs.blocking_lock();
1329+
match configs.get(&namespace) {
1330+
Some(sender) if !stored && sender.receiver_count() == 0 => {
1331+
configs.remove(&namespace);
1332+
Ok(true)
1333+
}
1334+
_ => Ok(false),
1335+
}
1336+
})
1337+
.await?
1338+
.map_err(fence_store_error)
1339+
}
1340+
13061341
// TODO: we need to either make sure that the metastore is restored
13071342
// before we start accepting connections or we need to contact bottomless
13081343
// here to check if a namespace exists. Preferably the former.
@@ -1739,6 +1774,38 @@ mod fence_tests {
17391774
}
17401775
}
17411776

1777+
/// The in-memory entry a failed creation left is forgotten, so `exists()` and `lookup()` do
1778+
/// not report the name; a stored config, or a handle still held, keeps the entry.
1779+
#[tokio::test]
1780+
async fn forget_unstored_only_unused_unstored_entries() {
1781+
let tmp = tempdir().unwrap();
1782+
let meta = open(tmp.path(), true).await;
1783+
let ns = NamespaceName::from("lazy");
1784+
1785+
assert!(!meta.forget_unstored(ns.clone()).await.unwrap());
1786+
1787+
let handle = meta.handle(ns.clone()).await.unwrap();
1788+
assert!(meta.exists(&ns).await);
1789+
// A handle is still held (a creation in progress): kept.
1790+
assert!(!meta.forget_unstored(ns.clone()).await.unwrap());
1791+
assert!(meta.exists(&ns).await);
1792+
drop(handle);
1793+
assert!(meta.forget_unstored(ns.clone()).await.unwrap());
1794+
assert!(!meta.exists(&ns).await);
1795+
assert!(meta.lookup(&ns).await.unwrap().is_none());
1796+
1797+
// A stored config is never forgotten.
1798+
let stored = NamespaceName::from("stored");
1799+
meta.handle(stored.clone())
1800+
.await
1801+
.unwrap()
1802+
.store(DatabaseConfig::default())
1803+
.await
1804+
.unwrap();
1805+
assert!(!meta.forget_unstored(stored.clone()).await.unwrap());
1806+
assert!(meta.lookup(&stored).await.unwrap().is_some());
1807+
}
1808+
17421809
fn request(
17431810
ns: &'static str,
17441811
op: Uuid,

‎libsql-server/src/namespace/store.rs‎

Lines changed: 35 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -400,17 +400,46 @@ impl NamespaceStore {
400400

401401
// A lookup that cannot create: only the default namespace and lazy creation create a
402402
// namespace here, and those refuse a name whose fence state is not established.
403-
let handle = match self.inner.metadata.lookup(&namespace).await? {
404-
Some(handle) => handle,
403+
let (handle, created) = match self.inner.metadata.lookup(&namespace).await? {
404+
Some(handle) => (handle, false),
405405
None if namespace == NamespaceName::default() || self.inner.allow_lazy_creation => {
406-
self.inner.metadata.handle(namespace.clone()).await?
406+
(self.inner.metadata.handle(namespace.clone()).await?, true)
407407
}
408408
None => return Err(Error::NamespaceDoesntExist(namespace.to_string())),
409409
};
410-
f(self
410+
let entry = match self
411411
.load_namespace(&namespace, handle, RestoreOption::Latest)
412-
.await?)
413-
.await
412+
.await
413+
{
414+
Ok(entry) => entry,
415+
Err(e) => {
416+
if created && e.fence_error().is_some() {
417+
self.forget_refused_creation(&namespace).await;
418+
}
419+
return Err(e);
420+
}
421+
};
422+
f(entry).await
423+
}
424+
425+
/// Undo what a lazy creation that a fence refused left in memory (on a replica server, the
426+
/// primary refused to replicate the name; `docs/NAMESPACE_FENCE.md` section 13.3): the
427+
/// metastore entry [`MetaStore::handle`] added, which would otherwise make the name look
428+
/// like an existing namespace, and the controller the attempt created. Both are kept when
429+
/// they hold anything durable or are in use by another attempt.
430+
async fn forget_refused_creation(&self, namespace: &NamespaceName) {
431+
match self.inner.metadata.forget_unstored(namespace.clone()).await {
432+
Ok(forgotten) => {
433+
let controller = self.inner.fences.forget_idle(namespace);
434+
tracing::debug!(
435+
"refused creation of {namespace}: metastore entry forgotten: {forgotten}, \
436+
controller forgotten: {controller}"
437+
);
438+
}
439+
Err(e) => {
440+
tracing::warn!("failed to forget the refused creation of {namespace}: {e}")
441+
}
442+
}
414443
}
415444

416445
fn resolve_attach_fn(&self) -> ResolveNamespacePathFn {

‎libsql-server/tests/fence/protocol.rs‎

Lines changed: 125 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1335,3 +1335,128 @@ fn replica_resumes_after_clear_read_fence() {
13351335
});
13361336
sim.run().unwrap();
13371337
}
1338+
1339+
/// Walks the quarantined target `ns` of `op` to `TARGET_WRITE_FENCED` (readable): seal the
1340+
/// (empty) import, record a successful validation, publish. Returns the revision.
1341+
async fn publish_target(admin: &Admin, ns: &str, op: Uuid) -> anyhow::Result<u64> {
1342+
let (_, body) = admin.inspect(ns).await?;
1343+
let (state, mut rev) = state_of(&body);
1344+
assert_eq!(state, "TARGET_QUARANTINED", "{body}");
1345+
for (n, route, from, extra, to) in [
1346+
(
1347+
2,
1348+
"target/seal-import",
1349+
"TARGET_QUARANTINED",
1350+
json!({}),
1351+
"TARGET_VALIDATING",
1352+
),
1353+
(
1354+
3,
1355+
"target/validation-receipt",
1356+
"TARGET_VALIDATING",
1357+
json!({ "result": "ok", "summary": "empty" }),
1358+
"TARGET_VALIDATING",
1359+
),
1360+
(
1361+
4,
1362+
"target/publish-readable",
1363+
"TARGET_VALIDATING",
1364+
json!({}),
1365+
"TARGET_WRITE_FENCED",
1366+
),
1367+
] {
1368+
let (status, body) = admin
1369+
.command(
1370+
ns,
1371+
route,
1372+
command_body(op, uuid(op.as_u128() + n), from, rev, extra),
1373+
)
1374+
.await?;
1375+
assert_eq!(status, StatusCode::OK, "{route}: {body}");
1376+
assert_eq!(state_of(&body).0, to, "{route}: {body}");
1377+
rev = state_of(&body).1;
1378+
}
1379+
Ok(rev)
1380+
}
1381+
1382+
/// A replica server creating a namespace lazily, for a name the primary's fence refuses to
1383+
/// replicate (here a quarantined target), fails the request at once with the primary's code
1384+
/// instead of retrying the handshake, and leaves no local copy: no namespace directory it
1385+
/// created (a directory that was already there stays) and no metastore entry. Once the target
1386+
/// is published the same name is created and served normally (section 13.3).
1387+
#[test]
1388+
fn replica_lazy_creation_refused_by_fence() {
1389+
let mut sim = sim();
1390+
let primary = tempdir().unwrap();
1391+
let replica = tempdir().unwrap();
1392+
// A directory that was on the replica before: a refused creation must not delete it.
1393+
let kept = replica.path().join("dbs").join("kept");
1394+
std::fs::create_dir_all(&kept).unwrap();
1395+
std::fs::write(kept.join("sentinel"), b"x").unwrap();
1396+
let dbs = replica.path().join("dbs");
1397+
make_primary(&mut sim, primary.path().to_path_buf(), Primary::default());
1398+
make_replica(&mut sim, replica.path().to_path_buf());
1399+
sim.client("client", async move {
1400+
let admin = Admin::new(Some(ADMIN_KEY));
1401+
let user = User::on("replica0");
1402+
let op = uuid(0x100);
1403+
quarantined_target(&admin, "tgt", op).await?;
1404+
quarantined_target(&admin, "kept", uuid(0x200)).await?;
1405+
1406+
for attempt in 0..2 {
1407+
let started = tokio::time::Instant::now();
1408+
assert_locked(
1409+
&format!("replica read of a quarantined target, attempt {attempt}"),
1410+
&user.legacy("tgt", &["select 1"]).await?,
1411+
QUARANTINED,
1412+
);
1413+
// Refused at the first handshake, not after a second of retries per attempt.
1414+
let took = started.elapsed();
1415+
assert!(
1416+
took < std::time::Duration::from_millis(500),
1417+
"took {took:?}"
1418+
);
1419+
assert!(
1420+
!dbs.join("tgt").exists(),
1421+
"the refused creation left dbs/tgt behind"
1422+
);
1423+
}
1424+
assert_locked(
1425+
"replica read of a quarantined target with a directory",
1426+
&user.legacy("kept", &["select 1"]).await?,
1427+
QUARANTINED,
1428+
);
1429+
assert!(
1430+
kept.join("sentinel").exists(),
1431+
"a pre-existing directory was removed"
1432+
);
1433+
1434+
let rev = publish_target(&admin, "tgt", op).await?;
1435+
let (status, body) = user.legacy("tgt", &["select 1"]).await?;
1436+
assert_eq!(status, StatusCode::OK, "after publication: {body}");
1437+
assert!(
1438+
dbs.join("tgt").exists(),
1439+
"published target not created on the replica"
1440+
);
1441+
1442+
// Writes through the replica reach the primary once writes are enabled.
1443+
let (status, body) = admin
1444+
.command(
1445+
"tgt",
1446+
"target/enable-writes",
1447+
command_body(
1448+
op,
1449+
uuid(op.as_u128() + 5),
1450+
"TARGET_WRITE_FENCED",
1451+
rev,
1452+
json!({}),
1453+
),
1454+
)
1455+
.await?;
1456+
assert_eq!(status, StatusCode::OK, "{body}");
1457+
let (status, body) = user.legacy("tgt", &["create table t (x)"]).await?;
1458+
assert_eq!(status, StatusCode::OK, "write through the replica: {body}");
1459+
Ok(())
1460+
});
1461+
sim.run().unwrap();
1462+
}

0 commit comments

Comments
 (0)