Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1,024 changes: 1,024 additions & 0 deletions libsql-server/src/http/admin/fence.rs

Large diffs are not rendered by default.

19 changes: 15 additions & 4 deletions libsql-server/src/http/admin/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ use crate::namespace::{DumpStream, NamespaceName, NamespaceStore, RestoreOption}
use crate::net::Connector;
use crate::LIBSQL_PAGE_SIZE;

pub mod fence;
pub mod stats;

#[derive(Clone)]
Expand All @@ -49,6 +50,9 @@ struct AppState<C> {
connector: C,
metrics: Metrics,
set_env_filter: Option<Box<dyn Fn(&str) -> anyhow::Result<()> + Sync + Send + 'static>>,
/// Whether an admin auth key is configured. Namespace fence commands refuse to run without
/// one (`docs/NAMESPACE_FENCE.md` section 4.1).
admin_auth_configured: bool,
}

impl<C> FromRef<Arc<AppState<C>>> for Metrics {
Expand Down Expand Up @@ -170,12 +174,14 @@ where
.route("/profile/heap/disable/:id", post(disable_profile_heap))
.route("/profile/heap/:id", delete(delete_profile_heap))
.route("/log-filter", post(handle_set_log_filter))
.merge(fence::routes())
.with_state(Arc::new(AppState {
namespaces: namespaces.clone(),
connector,
user_http_server,
metrics,
set_env_filter,
admin_auth_configured: auth.is_some(),
}))
.layer(
tower_http::trace::TraceLayer::new_for_http()
Expand Down Expand Up @@ -326,10 +332,11 @@ async fn handle_post_config<C>(
// Check that the jwt keys are correct
parse_jwt_keys(jwt_key)?;
}
let store = app_state
.namespaces
.config_store(NamespaceName::from_string(namespace.clone())?)
.await?;
let namespace_name = NamespaceName::from_string(namespace.clone())?;
// Config mutation is lifecycle work: refused while a fence denies it, before the namespace
// is loaded (and again in the metastore transaction that would store it).
app_state.namespaces.check_lifecycle(&namespace_name)?;
let store = app_state.namespaces.config_store(namespace_name).await?;
let original = (*store.get()).clone();
let mut updated = original.clone();
updated.block_reads = req.block_reads;
Expand Down Expand Up @@ -396,6 +403,10 @@ async fn handle_create_namespace<C: Connector>(
) -> crate::Result<()> {
let mut config = DatabaseConfig::default();

// Creating over a name whose fence denies lifecycle work is refused before a dump is
// fetched or anything is stored.
app_state.namespaces.check_lifecycle(&namespace)?;

if let Some(jwt_key) = req.jwt_key {
// Check that the jwt keys are correct
parse_jwt_keys(&jwt_key)?;
Expand Down
31 changes: 31 additions & 0 deletions libsql-server/src/namespace/fence/controller.rs
Original file line number Diff line number Diff line change
Expand Up @@ -233,6 +233,14 @@ pub enum LeaseKind {
Replication,
}

/// The live drain counters of a namespace (see [`FenceController::drain_counters`]).
#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
pub struct DrainCounters {
pub active_writers: usize,
pub read_leases: ReadLeaseCounts,
pub import_writers: usize,
}

/// The number of read leases held on a namespace, by kind.
#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
pub struct ReadLeaseCounts {
Expand Down Expand Up @@ -652,6 +660,29 @@ impl FenceController {
self.capabilities.lock().import_writers
}

/// The live drain counters reported by `InspectFence` and every admin response
/// (`docs/NAMESPACE_FENCE.md` section 4.3): connections holding a write slot for a write
/// transaction, read leases by kind, and running import calls. A snapshot; never waits.
pub fn drain_counters(&self) -> DrainCounters {
let active_writers = self
.live_write_drains()
.iter()
.filter(|source| source.manager.has_writer())
.count();
DrainCounters {
active_writers,
read_leases: self.read_lease_counts(),
import_writers: self.import_writers(),
}
}

/// The replication log id of the namespace as it is loaded now, if it is loaded on this
/// server as a primary. After a dirty restart this can differ from the log id a source was
/// acquired on (section 8.5).
pub fn current_log_id(&self) -> Option<Uuid> {
self.live_write_drains().last().map(|source| source.log_id)
}

/// The capabilities issued and still live.
pub fn live_capabilities(&self) -> usize {
self.capabilities.lock().live.len()
Expand Down
17 changes: 17 additions & 0 deletions libsql-server/src/namespace/fence/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -45,3 +45,20 @@ pub(crate) mod proto {

/// Version of the fence admin protocol reported by capability discovery.
pub const FENCE_PROTOCOL_VERSION: u32 = 1;

/// Whether this server fills the proxy protocol's additive `Error.stable_code` field and maps
/// it on the replica side (`docs/NAMESPACE_FENCE.md` section 6.1). Reported by capability
/// discovery so that deployment tooling can check every server before fences are used.
pub const PROXY_STABLE_CODE: bool = false;

/// The identity of this server process: its build and an id generated once per process. It is
/// written into records and receipts, and reported by the admin API.
pub fn server_identity() -> record::ServerIdentity {
static IDENTITY: std::sync::OnceLock<record::ServerIdentity> = std::sync::OnceLock::new();
IDENTITY
.get_or_init(|| record::ServerIdentity {
build: crate::version::version(),
instance_id: uuid::Uuid::new_v4(),
})
.clone()
}
27 changes: 27 additions & 0 deletions libsql-server/src/namespace/fence/registry.rs
Original file line number Diff line number Diff line change
Expand Up @@ -82,6 +82,33 @@ impl FenceRegistry {
}
}

/// Refuse generic lifecycle and configuration work on `namespace` while its gate denies it
/// (section 3.3, the lifecycle column): an active fence, a closing transition being
/// installed, a target being created, an indeterminate commit or an unavailable state. A
/// name without a controller has no fence state and is not refused here.
pub fn check_lifecycle(&self, namespace: &NamespaceName) -> Result<(), FenceError> {
match self.get(namespace) {
Some(controller) => controller.gate().permits(OperationClass::Lifecycle),
None => Ok(()),
}
}

/// How many namespaces have an active fence (`docs/NAMESPACE_FENCE.md` section 4.4,
/// `active_fences`): a record in any state but `RELEASED` or `TARGET_WRITABLE`, an
/// unavailable state, a target being created, or a commit whose outcome is not known yet.
pub fn active_count(&self) -> usize {
let controllers: Vec<_> = self.controllers.lock().values().cloned().collect();
controllers
.iter()
.filter(|controller| {
let gate = controller.gate();
gate.state().is_active()
|| gate.indeterminate.is_some()
|| gate.is_creating_target()
})
.count()
}

pub fn len(&self) -> usize {
self.controllers.lock().len()
}
Expand Down
Loading
Loading