docs: consolidate design documentation and standardize crate English
This commit is contained in:
@@ -1,28 +1,11 @@
|
||||
# feature-storage
|
||||
|
||||
Neutral Host-owned SQLite lifecycle manager for trusted Features. Owns placement,
|
||||
WAL/connection settings, transaction serialization, migrations, snapshots,
|
||||
restore/delete fencing, and shutdown. Features receive a managed
|
||||
`FeatureDatabase` after registration. Worker/model input never selects a database
|
||||
path or receives SQL access.
|
||||
Manages the SQLite lifecycle for Features within a trusted Host.
|
||||
Does not own Feature domain schemas or authentication, or provide SQL access to Workers or models.
|
||||
|
||||
Use `FeatureStorage::new(trusted_root)`, `register(FeatureRegistration)`, and
|
||||
`scope(host_issued_scope_id).open(®istration)`. `ScopedFeatureStorage` and
|
||||
`FeatureDatabase::scope_id()` are neutral storage isolation APIs: a local Host's
|
||||
persisted random scope does not create a Workspace record or imply Backend
|
||||
membership. Existing placement `<root>/<scope>/features/<feature>.sqlite` and
|
||||
snapshot `workspace=` keys remain unchanged for physical compatibility.
|
||||
## Main entry points
|
||||
|
||||
`WorkspaceFeatureStorage`, `workspace()`, `workspace_id()`, and
|
||||
`for_server_database()` are Backend compatibility aliases/helpers. They do not
|
||||
introduce a Server dependency. The Host is responsible for state-root privacy,
|
||||
symlink/path validation, and external execution leases before constructing this
|
||||
trusted manager.
|
||||
|
||||
`configure_connection(&rusqlite::Connection)` is reusable by other Host stores
|
||||
(such as JobStore). It sets the five-second busy timeout, foreign keys ON, WAL,
|
||||
and synchronous FULL before migrations/transactions. The Feature manager uses
|
||||
this same helper; WAL lock-contention retries stay within the busy timeout.
|
||||
|
||||
Dependencies: `rusqlite` (existing workspace backup support), `thiserror`, `uuid`;
|
||||
tests use `tempfile`.
|
||||
- [`FeatureStorage`], [`ScopedFeatureStorage`]: registration, scope isolation, storage, and shutdown
|
||||
- [`FeatureDatabase`]: connections and transactions
|
||||
- [`FeatureMigration`]: Feature-owned schema changes
|
||||
- [`configure_connection`]: shared SQLite connection policy
|
||||
|
||||
@@ -1,9 +1,4 @@
|
||||
//! Host-owned SQLite storage for trusted Features.
|
||||
//!
|
||||
//! The Host owns database placement, connection configuration, migrations, and
|
||||
//! operational lifecycle. A Feature owns its schema and only receives a scoped
|
||||
//! [`FeatureDatabase`] after trusted registration; Worker/model inputs never
|
||||
//! select a path or receive a SQL surface.
|
||||
#![doc = include_str!("../README.md")]
|
||||
|
||||
use std::collections::{HashMap, HashSet};
|
||||
use std::fs;
|
||||
@@ -79,6 +74,9 @@ impl FeatureStorageError {
|
||||
}
|
||||
}
|
||||
|
||||
/// Feature-owned schema changes. Commits pending changes and history in one IMMEDIATE transaction.
|
||||
/// Failure rolls back both changes and history, allowing retry on the next open.
|
||||
/// Rejects stored versions/names that differ from the plan and schemas newer than the plan.
|
||||
#[derive(Clone, Copy)]
|
||||
pub struct FeatureMigration {
|
||||
version: u32,
|
||||
@@ -135,6 +133,12 @@ impl RegisteredFeature {
|
||||
}
|
||||
}
|
||||
|
||||
/// Manages physical layout, connections, migrations, and shutdown within a trusted Host.
|
||||
///
|
||||
/// Features own schemas and domain transactions; the Host owns authentication before calls.
|
||||
/// This separation is an in-process API boundary, not a sandbox for arbitrary native code.
|
||||
/// The constructing Host owns state-root protection, path validation, and execution leases.
|
||||
/// Registration alone creates no database and does not delete existing data for disabled Features.
|
||||
#[derive(Clone)]
|
||||
pub struct FeatureStorage {
|
||||
inner: Arc<FeatureStorageInner>,
|
||||
@@ -155,8 +159,8 @@ struct FeatureStorageState {
|
||||
}
|
||||
|
||||
impl FeatureStorage {
|
||||
/// Creates a lazy manager. No directories or databases are created until a
|
||||
/// registered Feature is opened, restored, or explicitly backed up.
|
||||
/// Lazy manager. Creates no directories or databases until a registered
|
||||
/// Feature is opened, restored, or backed up.
|
||||
pub fn new(root: impl Into<PathBuf>) -> Self {
|
||||
Self {
|
||||
inner: Arc::new(FeatureStorageInner {
|
||||
@@ -184,8 +188,9 @@ impl FeatureStorage {
|
||||
Ok(Self::new(parent.join("feature-storage")))
|
||||
}
|
||||
|
||||
/// Binds trusted storage to a Host-issued opaque scope. This is a storage
|
||||
/// isolation key, not a Workspace record or model authorization credential.
|
||||
/// Restricts storage to a Host-issued scope.
|
||||
/// A scope is a physical sharing boundary; it creates no Workspace record or model authorization credentials.
|
||||
/// The same API supports Backend Workspace IDs and persistent local Host scope IDs.
|
||||
pub fn scope(&self, scope_id: &str) -> Result<ScopedFeatureStorage> {
|
||||
Ok(ScopedFeatureStorage {
|
||||
manager: self.clone(),
|
||||
@@ -198,6 +203,8 @@ impl FeatureStorage {
|
||||
self.scope(scope_id)
|
||||
}
|
||||
|
||||
/// Registers a schema once per manager. The returned registration is reusable across scopes.
|
||||
/// Rejects duplicate and foreign-manager registrations, fixing the migration plan used on open.
|
||||
pub fn register(&self, registration: FeatureRegistration) -> Result<RegisteredFeature> {
|
||||
let feature_id = validate_identifier("Feature", registration.feature_id)?.to_string();
|
||||
validate_migrations(&feature_id, registration.migrations)?;
|
||||
@@ -215,8 +222,8 @@ impl FeatureStorage {
|
||||
})
|
||||
}
|
||||
|
||||
/// Closes every managed connection after any operation currently holding a
|
||||
/// connection lock completes. New opens and operations then fail.
|
||||
/// Stops admission, waits for operations holding connection locks, and closes all managed connections.
|
||||
/// Retained handles also fail thereafter, so writes after shutdown cannot be treated as successful.
|
||||
pub fn shutdown(&self) -> Result<()> {
|
||||
let connections = {
|
||||
let mut state = self.lock_state()?;
|
||||
@@ -471,6 +478,7 @@ impl Drop for FeatureStorageInner {
|
||||
}
|
||||
}
|
||||
|
||||
/// Feature storage restricted to a Host scope. Scope identity denotes only a storage boundary.
|
||||
#[derive(Clone)]
|
||||
pub struct ScopedFeatureStorage {
|
||||
manager: FeatureStorage,
|
||||
@@ -494,26 +502,33 @@ impl ScopedFeatureStorage {
|
||||
self.manager.register(registration)
|
||||
}
|
||||
|
||||
/// Reuses a managed connection, validating and applying migrations on first open.
|
||||
/// Serializes concurrent opens within a manager; SQLite serializes migration writers across managers.
|
||||
/// Syncs metadata-directory ancestors before opening SQLite. DB/WAL sync alone cannot
|
||||
/// persist links to newly created ancestors, so a sync failure prevents connection registration.
|
||||
pub fn open(&self, registration: &RegisteredFeature) -> Result<FeatureDatabase> {
|
||||
self.manager.open(&self.scope_id, registration)
|
||||
}
|
||||
|
||||
/// Creates a consistent online SQLite snapshot of every Feature database,
|
||||
/// including data for Features that are currently disabled/unregistered.
|
||||
/// Backs up each Feature DB, including disabled/unregistered ones, using SQLite online backup.
|
||||
/// Avoids file copies that omit WAL; checks integrity and foreign keys before publishing by rename.
|
||||
/// Each DB is internally consistent, but no common-point snapshot across DBs or Host authority is guaranteed.
|
||||
/// Blobs outside the DB are not saved, so this is not a complete Drive backup.
|
||||
pub fn backup(&self, destination: impl AsRef<Path>) -> Result<()> {
|
||||
self.manager
|
||||
.backup_scope(&self.scope_id, destination.as_ref())
|
||||
}
|
||||
|
||||
/// Restores a snapshot only into an absent scope storage directory and
|
||||
/// only while no Feature database for this scope is open.
|
||||
/// Restores a snapshot only into an uncreated scope with no connections.
|
||||
/// Rejects scope mismatches and corrupt DBs; publishes the validated temporary directory by rename.
|
||||
pub fn restore(&self, snapshot: impl AsRef<Path>) -> Result<()> {
|
||||
self.manager
|
||||
.restore_scope(&self.scope_id, snapshot.as_ref())
|
||||
}
|
||||
|
||||
/// Closes this scope's Feature connections and removes only its scoped
|
||||
/// storage tree. This is the operation used by Workspace deletion.
|
||||
/// Closes connections in the scope and deletes only its storage tree. Retries are idempotent.
|
||||
/// Keeps the same manager's admission fence after physical deletion, preventing cached tasks
|
||||
/// from recreating databases before the Host finalizes deletion. Host deletion reservation is a separate responsibility.
|
||||
pub fn delete(&self) -> Result<()> {
|
||||
self.manager.delete_scope(&self.scope_id)
|
||||
}
|
||||
@@ -523,6 +538,9 @@ impl ScopedFeatureStorage {
|
||||
}
|
||||
}
|
||||
|
||||
/// Host-managed synchronous SQLite connection. Async callers use a blocking executor.
|
||||
/// Domain repositories retain this handle and never pass connections or SQL to Workers/Runtimes.
|
||||
/// Locks serialize operations and shutdown on each connection; deletion/shutdown fences are checked before and after locking.
|
||||
#[derive(Clone)]
|
||||
pub struct FeatureDatabase {
|
||||
inner: Arc<FeatureDatabaseInner>,
|
||||
@@ -584,8 +602,9 @@ impl FeatureDatabase {
|
||||
operation(connection)
|
||||
}
|
||||
|
||||
/// Executes a Feature-owned multi-table operation atomically. Returning an
|
||||
/// error rolls the transaction back; successful return commits it.
|
||||
/// Updates multiple tables within one Feature DB in an IMMEDIATE transaction.
|
||||
/// Commits after the closure succeeds; closure or commit failures never return success.
|
||||
/// Does not guarantee atomic writes with the Host control-plane DB or other Feature DBs.
|
||||
pub fn transaction<T>(
|
||||
&self,
|
||||
operation: impl FnOnce(&Transaction<'_>) -> Result<T>,
|
||||
@@ -593,8 +612,8 @@ impl FeatureDatabase {
|
||||
self.try_transaction(operation)
|
||||
}
|
||||
|
||||
/// Variant of [`Self::transaction`] that preserves a Feature repository's
|
||||
/// typed domain errors while still rolling every failure back atomically.
|
||||
/// Preserves the rollback, connection ownership, and fences of [`Self::transaction`],
|
||||
/// returning typed Feature domain errors without converting them to storage strings.
|
||||
pub fn try_transaction<T, E>(
|
||||
&self,
|
||||
operation: impl FnOnce(&Transaction<'_>) -> std::result::Result<T, E>,
|
||||
@@ -742,11 +761,10 @@ fn sync_directory(path: &Path) -> std::io::Result<()> {
|
||||
fs::File::open(path)?.sync_all()
|
||||
}
|
||||
|
||||
/// Applies the common durable Host SQLite connection policy: five-second busy
|
||||
/// timeout, foreign keys, WAL, and FULL synchronous commits. Call immediately
|
||||
/// after opening a file-backed database, before transactions or migrations.
|
||||
/// WAL initialization retries lock contention within the same bounded timeout.
|
||||
/// This does not grant path authority or select a database schema.
|
||||
/// Applies the shared SQLite connection policy before migrations or transactions.
|
||||
/// Enables foreign keys, WAL, and FULL synchronous commits, with bounded lock-contention waits.
|
||||
/// Retries WAL initialization within the same wait limit and rejects silent fallback to another journal mode.
|
||||
/// Grants neither path-selection authority nor domain schemas.
|
||||
pub fn configure_connection(connection: &Connection) -> Result<()> {
|
||||
connection.busy_timeout(BUSY_TIMEOUT)?;
|
||||
connection.execute_batch(
|
||||
|
||||
Reference in New Issue
Block a user