From 00a203638c4e0194a99dec01ceada0f6678a4c1a Mon Sep 17 00:00:00 2001 From: Abhinav Chordia Date: Fri, 16 Oct 2026 05:50:16 +0000 Subject: [PATCH] sbx-mgr: callers use the store's own handle and transaction types sbx-mgr's node-local SQLite store, `src/db`, now defines the types its callers hold, so nothing outside it names rusqlite: - `db::SharedConnection` replaces `Arc>`. It wraps the same mutex, so the jobs worker shares it (`background_worker`), and `lock()` returns a `ConnectionGuard` with `Mutex::lock`'s poisoning. `connection_or_signal_failure` keeps its semantics on the new handle. - `db::Connection`, a `repr(transparent)` view (a private `RefCastCustom` view, so the crate stays `forbid(unsafe_code)` and publishes no `RefCast` impl), is what every `db` function takes. Its raw accessor and statement methods, `Error::raw`, and the row helpers that take or return rusqlite values are private to `db` in production builds, and the first two groups are crate-visible only under `cfg(test)`. The jobs worker the store builds returns common-jobs' own `PruneTerminalJobsError` from `try_prune_terminal_jobs_once`, with the same `Display`. So production code outside `db` cannot reach a rusqlite value; the only rusqlite type left in the store's public API is `db::Error`'s inbound `From`. - `db::Transaction::{immediate, deferred}` wraps rusqlite's transaction and derefs to the connection. - `db::Error` is a `thiserror` `#[error(transparent)]` wrapper around rusqlite's error, so `Display` and `source` are unchanged wherever clients and operators see them. Every `db` function and error enum returns or wraps it; callers test it with `is_no_rows` and `is_invalid_query` instead of matching rusqlite variants. - Opening and migrating the database move into `db` (`db::open`, `db::migrate`, `db::OpenError`, transparent over the shared helpers' error), and the cleanup executor's job-store calls go through `db::cleanup_jobs`. Tests outside the store get raw SQL through the test-visible statement methods on `db::Connection` and a small `#[cfg(test)]` `db::fixture` re-export (`params!`, trace types, `Value`, `Type`, `StatementStatus`), plus test-only error predicates. No behavior changes. The `sbxmgr_sqlite` baseline is now empty and `no_new_entries` is required for it; the `sbxmgr_sqlite_schema` baseline lists only the database migrator, which milestone 3 moves onto the store's migration entry point. --- ARCHITECTURE.toml | 14 +- Cargo.lock | 1 + Cargo.toml | 1 + architecture-baseline.toml | 52 +-- crates/libs/common/jobs/src/lib.rs | 3 +- crates/libs/common/jobs/src/worker.rs | 15 +- crates/libs/sbx/mgr/server/Cargo.toml | 1 + crates/libs/sbx/mgr/server/src/db.rs | 74 +++- .../sbx/mgr/server/src/db/auto_lifecycle.rs | 8 +- .../src/db/auto_lifecycle_delete_receipts.rs | 109 +++--- .../auto_lifecycle_delete_receipts/tests.rs | 11 +- .../sbx/mgr/server/src/db/cleanup_episodes.rs | 38 +- .../sbx/mgr/server/src/db/cleanup_jobs.rs | 103 ++++-- .../mgr/server/src/db/cleanup_jobs/tests.rs | 46 +-- .../mgr/server/src/db/cleanup_target_scan.rs | 3 +- .../server/src/db/discovery_indexes_tests.rs | 2 +- crates/libs/sbx/mgr/server/src/db/eviction.rs | 10 +- .../sbx/mgr/server/src/db/eviction_scan.rs | 7 +- .../mgr/server/src/db/guest_health_scan.rs | 8 +- crates/libs/sbx/mgr/server/src/db/handle.rs | 327 ++++++++++++++++++ .../libs/sbx/mgr/server/src/db/inventory.rs | 14 +- .../sbx/mgr/server/src/db/inventory_read.rs | 19 +- .../mgr/server/src/db/inventory_read/tests.rs | 6 +- .../src/db/lifecycle_validation_tests.rs | 15 +- .../server/src/db/local_artifact_leases.rs | 39 ++- .../sbx/mgr/server/src/db/local_artifacts.rs | 12 +- .../sbx/mgr/server/src/db/network_policy.rs | 13 +- crates/libs/sbx/mgr/server/src/db/pool.rs | 2 +- .../libs/sbx/mgr/server/src/db/quarantine.rs | 13 +- .../mgr/server/src/db/quarantine_history.rs | 5 +- .../mgr/server/src/db/quarantine_recovery.rs | 12 +- .../sbx/mgr/server/src/db/quarantine_scan.rs | 3 +- .../server/src/db/restore_transfer_scopes.rs | 4 +- .../server/src/db/route_activation_claims.rs | 13 +- .../mgr/server/src/db/sandbox_row/tests.rs | 3 +- .../libs/sbx/mgr/server/src/db/sandboxes.rs | 80 ++--- .../libs/sbx/mgr/server/src/db/snapshot_gc.rs | 3 +- .../mgr/server/src/db/snapshot_gc/tests.rs | 2 +- .../mgr/server/src/db/snapshot_upload_jobs.rs | 9 +- .../sbx/mgr/server/src/db/startup_recovery.rs | 6 +- .../mgr/server/src/db/state_transitions.rs | 36 +- .../libs/sbx/mgr/server/src/db/state_write.rs | 10 +- .../sbx/mgr/server/src/db/teardown_jobs.rs | 11 +- .../sbx/mgr/server/src/db/teardown_suspend.rs | 45 +-- crates/libs/sbx/mgr/server/src/db/tests.rs | 44 +-- .../tests/accepted_shapes_cleanup_episodes.rs | 3 +- .../db/tests/accepted_shapes_quarantine.rs | 3 +- .../tests/accepted_shapes_single_statement.rs | 2 +- .../db/tests/cleanup_quarantine_refusal.rs | 5 +- .../db/tests/local_network_policy_clear.rs | 2 +- .../src/db/tests/state_write_fixtures.rs | 3 +- .../src/db/tests/state_writer_refusal.rs | 15 +- .../src/db/tests/stopped_artifact_pin.rs | 9 +- .../src/db/tests/thin_writer_coverage.rs | 7 +- .../server/src/db/tests/update_state_from.rs | 11 +- .../src/db/tests/update_state_matrix.rs | 8 +- .../libs/sbx/mgr/server/src/db/timestamps.rs | 11 +- crates/libs/sbx/mgr/server/src/db/transfer.rs | 2 +- .../sbx/mgr/server/src/lifecycle_metrics.rs | 4 +- .../sbx/mgr/server/src/local_artifacts.rs | 81 ++--- .../src/local_artifacts/refusal_tests.rs | 18 +- .../mgr/server/src/local_artifacts/tests.rs | 91 +++-- .../sbx/mgr/server/src/node_state_sync.rs | 15 +- .../mgr/server/src/route_activation_claims.rs | 4 +- crates/libs/sbx/mgr/server/src/run.rs | 30 +- crates/libs/sbx/mgr/server/src/run/tests.rs | 65 ++-- .../mgr/server/src/runtime_reconciliation.rs | 4 +- .../sbx/mgr/server/src/sandbox_eviction.rs | 10 +- .../mgr/server/src/sandbox_eviction/tests.rs | 10 +- .../server/src/service/activation_support.rs | 20 +- .../src/service/activation_support/tests.rs | 18 +- .../sbx/mgr/server/src/service/auth_lookup.rs | 11 +- .../service/auth_lookup/resume_begin/tests.rs | 6 +- .../server/src/service/auth_lookup/tests.rs | 6 +- .../auth_lookup/wait_ready_policy_tests.rs | 2 +- .../server/src/service/auth_lookup_tests.rs | 2 +- .../mgr/server/src/service/cache_cleanup.rs | 26 +- .../src/service/cache_cleanup/periodic.rs | 2 +- .../server/src/service/cache_cleanup/tests.rs | 24 +- .../sbx/mgr/server/src/service/cleanup.rs | 30 +- .../service/cleanup/stop_admission_tests.rs | 6 +- .../server/src/service/cleanup_executor.rs | 38 +- .../src/service/cleanup_executor/tests.rs | 55 +-- .../mgr/server/src/service/cleanup_support.rs | 14 +- .../src/service/cleanup_support/tests.rs | 20 +- .../mgr/server/src/service/cleanup_targets.rs | 4 +- .../src/service/cleanup_targets/tests.rs | 16 +- .../libs/sbx/mgr/server/src/service/create.rs | 2 +- .../mgr/server/src/service/create_support.rs | 14 +- .../create_support/state_write_tests.rs | 6 +- .../src/service/gateway_authority/tests.rs | 4 +- .../grpc_core/create_after_lookup_tests.rs | 14 +- .../mgr/server/src/service/grpc_core/tests.rs | 14 +- .../grpc_core/unpersisted_cleanup_tests.rs | 4 +- .../grpc_lifecycle/backup_policy_tests.rs | 16 +- .../service/grpc_lifecycle/pause_resume.rs | 4 +- .../pause_resume/supersession_tests.rs | 6 +- .../grpc_lifecycle/retired_teardown_tests.rs | 40 ++- .../service/grpc_lifecycle/suspend/tests.rs | 4 +- .../grpc_lifecycle/teardown_backup_tests.rs | 15 +- .../grpc_lifecycle/teardown_suspend.rs | 4 +- .../src/service/grpc_lifecycle/tests.rs | 32 +- .../src/service/grpc_pool_wait_clone.rs | 3 +- .../src/service/grpc_pool_wait_clone/tests.rs | 9 +- .../src/service/grpc_sandbox_transfer.rs | 2 +- .../service/grpc_snapshot_transfer/tests.rs | 6 +- .../rollback_state.rs | 2 +- .../grpc_spaced_process_snapshots/tests.rs | 2 +- .../server/src/service/grpc_state_snapshot.rs | 2 +- .../src/service/grpc_state_snapshot/tests.rs | 10 +- .../server/src/service/host_control/tests.rs | 2 +- crates/libs/sbx/mgr/server/src/service/mod.rs | 42 +-- .../mgr/server/src/service/network_runtime.rs | 10 +- .../src/service/network_runtime/tests.rs | 14 +- .../sbx/mgr/server/src/service/reconcile.rs | 2 +- .../mgr/server/src/service/reconcile/tests.rs | 13 +- .../sbx/mgr/server/src/service/recovery.rs | 10 +- .../service/recovery/staging_cleanup/tests.rs | 10 +- .../mgr/server/src/service/recovery/tests.rs | 95 ++--- .../server/src/service/recovery_cross_node.rs | 26 +- .../src/service/recovery_cross_node/tests.rs | 40 +-- .../src/service/route_identity_tests.rs | 8 +- .../libs/sbx/mgr/server/src/service/start.rs | 6 +- .../sbx/mgr/server/src/service/start/tests.rs | 2 +- .../mgr/server/src/service/start_support.rs | 2 +- .../server/src/service/start_support/tests.rs | 2 +- .../server/src/service/startup_resident.rs | 2 +- .../sbx/mgr/server/src/service/storage.rs | 2 +- .../mgr/server/src/service/storage/tests.rs | 2 +- .../src/service/template_snapshot_support.rs | 6 +- .../mgr/server/src/service/test_options.rs | 2 +- .../libs/sbx/mgr/server/src/service/tests.rs | 290 ++++++++-------- .../tests/cancelled_cleanup_rpc_tests.rs | 4 +- .../tests/local_backup_inventory_tests.rs | 4 +- .../tests/node_manager_transport_tests.rs | 8 +- crates/libs/sbx/mgr/server/src/snapshot_gc.rs | 11 +- .../sbx/mgr/server/src/snapshot_gc/tests.rs | 66 ++-- .../sbx/mgr/server/src/snapshot_restore.rs | 12 +- .../sbx/mgr/server/src/snapshot_scheduler.rs | 10 +- .../server/src/snapshot_scheduler/tests.rs | 4 +- .../mgr/server/src/snapshot_upload_jobs.rs | 56 +-- .../server/src/snapshot_upload_jobs/tests.rs | 72 ++-- crates/libs/sbx/mgr/server/src/watchdog.rs | 46 ++- .../server/src/watchdog/diagnostics/tests.rs | 4 +- .../libs/sbx/mgr/server/src/watchdog/tests.rs | 34 +- 145 files changed, 1744 insertions(+), 1355 deletions(-) create mode 100644 crates/libs/sbx/mgr/server/src/db/handle.rs diff --git a/ARCHITECTURE.toml b/ARCHITECTURE.toml index b122c61d0..9eae39872 100644 --- a/ARCHITECTURE.toml +++ b/ARCHITECTURE.toml @@ -250,10 +250,12 @@ mode = "required" # connection or transaction, using the shared SQLite open and migration # helpers or the shared job store's SQL functions, binding parameters, # executing a statement, or writing an SQL statement literal outside the -# store is a crossing. Other services' crates use rusqlite for their own -# databases, so `paths` limits the rule to sbx-mgr's. Existing callers that -# still name the connection, error and transaction types are baselined -# until they use the store's own. +# store is a crossing; callers use the store's own handle, transaction and +# error types. Other services' crates use rusqlite for their own databases, +# so `paths` limits the rule to sbx-mgr's. The patterns are a line-based +# tripwire: aliases, wrapped imports and calls through names imported +# earlier escape them, so they catch common crossings rather than prove +# their absence. [rules.pattern_ratchet.sbxmgr_sqlite] patterns = [ '\brusqlite\s*::', @@ -278,9 +280,9 @@ allow = [ [rules.pattern_ratchet.sbxmgr_sqlite.no_crossing] mode = "required" -# Seeding the ratchet reports its initial entries against the base. +# The baseline is empty: no file outside the store may be added to it. [rules.pattern_ratchet.sbxmgr_sqlite.no_new_entries] -mode = "advisory" +mode = "required" [rules.pattern_ratchet.sbxmgr_sqlite.no_raised_caps] mode = "required" diff --git a/Cargo.lock b/Cargo.lock index d0d03b362..b6b5e7638 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -6700,6 +6700,7 @@ dependencies = [ "prost", "prost-types", "rcgen", + "ref-cast", "rstest", "rusqlite", "rustix", diff --git a/Cargo.toml b/Cargo.toml index 71b776909..5d01f07a8 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -226,6 +226,7 @@ prost-types = "0.14" rand = "0.10" rcgen = { version = "0.14", default-features = false } redis = { version = "1", default-features = false, features = ["cluster-async", "script", "tokio-rustls-comp", "tls-rustls-webpki-roots"] } +ref-cast = "1.0.27" regex = "1" reqwest = { version = "0.13", default-features = false, features = ["json", "rustls"] } rstest = { version = "0.27", default-features = false } diff --git a/architecture-baseline.toml b/architecture-baseline.toml index 48e0fc864..7fa0f514c 100644 --- a/architecture-baseline.toml +++ b/architecture-baseline.toml @@ -75,16 +75,16 @@ "crates/libs/object-store/src/lib.rs" = 1810 "crates/libs/observability/src/lib.rs" = 1063 "crates/libs/public/api/src/lib.rs" = 1793 -"crates/libs/sbx/mgr/server/src/db/auto_lifecycle_delete_receipts.rs" = 1158 +"crates/libs/sbx/mgr/server/src/db/auto_lifecycle_delete_receipts.rs" = 1149 "crates/libs/sbx/mgr/server/src/db/cleanup_episodes.rs" = 1833 "crates/libs/sbx/mgr/server/src/db/quarantine_recovery.rs" = 1042 -"crates/libs/sbx/mgr/server/src/db/sandboxes.rs" = 1547 -"crates/libs/sbx/mgr/server/src/db/state_transitions.rs" = 1016 -"crates/libs/sbx/mgr/server/src/run.rs" = 1101 +"crates/libs/sbx/mgr/server/src/db/sandboxes.rs" = 1541 +"crates/libs/sbx/mgr/server/src/db/state_transitions.rs" = 1012 +"crates/libs/sbx/mgr/server/src/run.rs" = 1091 "crates/libs/sbx/mgr/server/src/sandbox_eviction.rs" = 1253 "crates/libs/sbx/mgr/server/src/service/activation_support.rs" = 1574 "crates/libs/sbx/mgr/server/src/service/auth_lookup.rs" = 1221 -"crates/libs/sbx/mgr/server/src/service/cache_cleanup.rs" = 1455 +"crates/libs/sbx/mgr/server/src/service/cache_cleanup.rs" = 1452 "crates/libs/sbx/mgr/server/src/service/cleanup.rs" = 2704 "crates/libs/sbx/mgr/server/src/service/create.rs" = 1195 "crates/libs/sbx/mgr/server/src/service/grpc_core.rs" = 1087 @@ -145,50 +145,14 @@ "crates/libs/node/mgr/server/src/service/artifact_fs/receive.rs" = 4 "crates/libs/node/mgr/server/src/net/fallback.rs" = 1 -# sbx-mgr code outside its SQLite store that still names rusqlite types, the -# shared SQLite helpers or the shared job store, or opens a transaction itself. +# sbx-mgr code outside its SQLite store uses only the store's own handle, +# transaction and error types, so no file is listed. [pattern_ratchet.sbxmgr_sqlite] -"crates/libs/sbx/mgr/server/src/lifecycle_metrics.rs" = 1 -"crates/libs/sbx/mgr/server/src/local_artifacts.rs" = 28 -"crates/libs/sbx/mgr/server/src/node_state_sync.rs" = 1 -"crates/libs/sbx/mgr/server/src/route_activation_claims.rs" = 2 -"crates/libs/sbx/mgr/server/src/run.rs" = 6 -"crates/libs/sbx/mgr/server/src/runtime_reconciliation.rs" = 1 -"crates/libs/sbx/mgr/server/src/sandbox_eviction.rs" = 5 -"crates/libs/sbx/mgr/server/src/service/activation_support.rs" = 9 -"crates/libs/sbx/mgr/server/src/service/auth_lookup.rs" = 5 -"crates/libs/sbx/mgr/server/src/service/cache_cleanup.rs" = 12 -"crates/libs/sbx/mgr/server/src/service/cleanup.rs" = 15 -"crates/libs/sbx/mgr/server/src/service/cleanup_executor.rs" = 7 -"crates/libs/sbx/mgr/server/src/service/cleanup_support.rs" = 7 -"crates/libs/sbx/mgr/server/src/service/cleanup_targets.rs" = 1 -"crates/libs/sbx/mgr/server/src/service/create_support.rs" = 5 -"crates/libs/sbx/mgr/server/src/service/grpc_lifecycle/pause_resume.rs" = 2 -"crates/libs/sbx/mgr/server/src/service/grpc_lifecycle/teardown_suspend.rs" = 2 -"crates/libs/sbx/mgr/server/src/service/grpc_pool_wait_clone.rs" = 1 -"crates/libs/sbx/mgr/server/src/service/grpc_spaced_process_snapshots/rollback_state.rs" = 1 -"crates/libs/sbx/mgr/server/src/service/grpc_state_snapshot.rs" = 1 -"crates/libs/sbx/mgr/server/src/service/mod.rs" = 13 -"crates/libs/sbx/mgr/server/src/service/network_runtime.rs" = 5 -"crates/libs/sbx/mgr/server/src/service/recovery.rs" = 5 -"crates/libs/sbx/mgr/server/src/service/recovery_cross_node.rs" = 12 -"crates/libs/sbx/mgr/server/src/service/start_support.rs" = 1 -"crates/libs/sbx/mgr/server/src/service/startup_resident.rs" = 1 -"crates/libs/sbx/mgr/server/src/service/storage.rs" = 1 -"crates/libs/sbx/mgr/server/src/service/template_snapshot_support.rs" = 3 -"crates/libs/sbx/mgr/server/src/service/test_options.rs" = 1 -"crates/libs/sbx/mgr/server/src/snapshot_gc.rs" = 1 -"crates/libs/sbx/mgr/server/src/snapshot_restore.rs" = 5 -"crates/libs/sbx/mgr/server/src/snapshot_scheduler.rs" = 3 -"crates/libs/sbx/mgr/server/src/snapshot_upload_jobs.rs" = 19 -"crates/libs/sbx/mgr/server/src/watchdog.rs" = 21 # Code outside the store that still names sbx-mgr's migrations or database -# path: its open and migration in `run.rs`, and the migrator, until it calls -# the store. +# path: the database migrator, until it calls the store. [pattern_ratchet.sbxmgr_sqlite_schema] "crates/libs/db/migrator/src/lib.rs" = 5 -"crates/libs/sbx/mgr/server/src/run.rs" = 3 # [crate_names] maps each workspace package with a catch-all name (a part such # as `common`, `utils` or `core`) to its member path. Only remove entries: a pull diff --git a/crates/libs/common/jobs/src/lib.rs b/crates/libs/common/jobs/src/lib.rs index cd2696d2b..6b71f34b9 100644 --- a/crates/libs/common/jobs/src/lib.rs +++ b/crates/libs/common/jobs/src/lib.rs @@ -31,7 +31,8 @@ mod worker; pub use worker::{ BackgroundWorker, COMPLETED_JOB_RETENTION, DuplicatePolicy, FAILED_JOB_RETENTION, LeaseConfig, - PriorityBand, SubjectConcurrency, TERMINAL_JOB_PRUNE_INTERVAL, WorkerConfig, + PriorityBand, PruneTerminalJobsError, SubjectConcurrency, TERMINAL_JOB_PRUNE_INTERVAL, + WorkerConfig, }; /// Caller-selected scheduling priority. Higher values run first within a job kind. diff --git a/crates/libs/common/jobs/src/worker.rs b/crates/libs/common/jobs/src/worker.rs index 90beda536..cda7d37b2 100644 --- a/crates/libs/common/jobs/src/worker.rs +++ b/crates/libs/common/jobs/src/worker.rs @@ -370,6 +370,13 @@ pub enum SubjectConcurrency { Serialized, } +/// A terminal-job prune that failed in the database. `Display` and `source` +/// are the SQLite error's; the error itself stays private to this crate, so a +/// worker's owner gets no database value back. +#[derive(Debug, thiserror::Error)] +#[error(transparent)] +pub struct PruneTerminalJobsError(rusqlite::Error); + /// A background job worker that persists jobs to SQLite and drives execution /// with bounded concurrency, exponential-backoff retry, and graceful drain. /// @@ -518,10 +525,14 @@ impl BackgroundWorker { /// Preserve database failures so maintenance callers can retry without /// treating an unsuccessful pass as a successful zero-deletion pass. - pub fn try_prune_terminal_jobs_once(&self, worker_label: &str) -> rusqlite::Result { + pub fn try_prune_terminal_jobs_once( + &self, + worker_label: &str, + ) -> Result { let pruned = { let conn = self.lock_db_for_background(); - db::prune_terminal_jobs(&conn, COMPLETED_JOB_RETENTION, FAILED_JOB_RETENTION)? + db::prune_terminal_jobs(&conn, COMPLETED_JOB_RETENTION, FAILED_JOB_RETENTION) + .map_err(PruneTerminalJobsError)? }; if pruned > 0 { tracing::info!( diff --git a/crates/libs/sbx/mgr/server/Cargo.toml b/crates/libs/sbx/mgr/server/Cargo.toml index b5efb7417..2b6d98b5f 100644 --- a/crates/libs/sbx/mgr/server/Cargo.toml +++ b/crates/libs/sbx/mgr/server/Cargo.toml @@ -41,6 +41,7 @@ jiff = { workspace = true } libc = { workspace = true } prost = { workspace = true } prost-types = { workspace = true } +ref-cast = { workspace = true } rusqlite = { workspace = true } serde = { workspace = true, features = ["derive"] } serde_json = { workspace = true } diff --git a/crates/libs/sbx/mgr/server/src/db.rs b/crates/libs/sbx/mgr/server/src/db.rs index 9ca82fb87..d9edf661f 100644 --- a/crates/libs/sbx/mgr/server/src/db.rs +++ b/crates/libs/sbx/mgr/server/src/db.rs @@ -30,7 +30,11 @@ use pplx_space_net_common::{NetworkPolicy, proto::NetworkPolicySource}; use rusqlite::types::Type; use pplx_space_common_core::sql; -use rusqlite::{Connection, OptionalExtension, Result, params}; +use rusqlite::{OptionalExtension as _, params}; + +mod handle; +pub(crate) use handle::OptionalExtension; +pub use handle::{Connection, ConnectionGuard, Error, Result, SharedConnection, Transaction}; pub(crate) mod auto_lifecycle_delete_receipts; pub(crate) mod cleanup_jobs; @@ -109,7 +113,7 @@ pub use pplx_space_common_core::types::QuarantineRecoveryNoProgressReason; const EXECUTION_DELETE_RECEIPT_RETENTION_SECS: i64 = 7 * 24 * 60 * 60; -fn parse_policy_authority(value: &str, column: usize) -> Result { +fn parse_policy_authority(value: &str, column: usize) -> rusqlite::Result { value.parse().map_err(|error| { rusqlite::Error::FromSqlConversionFailure(column, Type::Text, Box::new(error)) }) @@ -218,16 +222,47 @@ impl CleanupCompletion { /// background loops evict them. const STATEMENT_CACHE_CAPACITY: usize = 128; -pub fn open() -> Result { +/// Opening or migrating the database failed. `Display` and `source` are the +/// shared SQLite helpers' error's, unchanged. +#[derive(Debug, thiserror::Error)] +#[error(transparent)] +pub struct OpenError(#[from] pplx_space_common_svc::sql::sqlite::SqliteError); + +/// Apply the service and common schema migrations to the database. +pub async fn migrate() -> Result<(), OpenError> { + migrate_at(Path::new(DB_PATH)).await +} + +/// [`migrate`] for the database at `db_path`. +pub async fn migrate_at(db_path: &Path) -> Result<(), OpenError> { + use pplx_space_sbxmgr_migrations::{COMMON_SCHEMA_MIGRATOR, SANDBOX_MANAGER_MIGRATOR}; + Ok(pplx_space_common_svc::sql::sqlite::migrate_embedded( + "sandbox-manager", + db_path, + pplx_space_common_svc::sql::sqlite::EmbeddedMigrators { + service: &SANDBOX_MANAGER_MIGRATOR, + common: Some(&COMMON_SCHEMA_MIGRATOR), + }, + ) + .await?) +} + +/// Open the migrated database, with the connection settings every store +/// operation assumes. +pub fn open() -> Result { let conn = pplx_space_common_svc::sql::sqlite::open_rusqlite( Path::new(DB_PATH), pplx_space_common_svc::sql::sqlite::RusqliteOpenOptions::default(), )?; conn.set_prepared_statement_cache_capacity(STATEMENT_CACHE_CAPACITY); - Ok(conn) + Ok(Connection::from_raw(conn)) +} + +pub(crate) fn describe_error(error: &Error) -> String { + describe_raw_error(error.raw()) } -pub(crate) fn describe_error(error: &rusqlite::Error) -> String { +fn describe_raw_error(error: &rusqlite::Error) -> String { match error.sqlite_error() { Some(sqlite) => format!( "{error} (sqlite_code={:?}, sqlite_extended_code={})", @@ -237,8 +272,8 @@ pub(crate) fn describe_error(error: &rusqlite::Error) -> String { } } -pub(crate) fn immediate_transaction(conn: &Connection) -> Result> { - rusqlite::Transaction::new_unchecked(conn, rusqlite::TransactionBehavior::Immediate) +pub(crate) fn immediate_transaction(conn: &Connection) -> Result> { + Transaction::immediate(conn) } pub(crate) fn now() -> Result { @@ -257,9 +292,7 @@ struct PersistedClockBeforeEpoch; fn persisted_epoch_seconds(timestamp: jiff::Timestamp) -> Result { if timestamp < jiff::Timestamp::UNIX_EPOCH { - return Err(rusqlite::Error::ToSqlConversionFailure(Box::new( - PersistedClockBeforeEpoch, - ))); + return Err(Error::to_sql_conversion(PersistedClockBeforeEpoch)); } Ok(timestamp.as_second()) } @@ -480,9 +513,9 @@ fn test_schema_image() -> &'static [u8] { if let Some(image) = cache.as_ref().and_then(|cache| cache.read()) { return image; } - let mut conn = Connection::open_in_memory().unwrap(); + let mut conn = Connection::from_raw(rusqlite::Connection::open_in_memory().unwrap()); create_test_schema(&mut conn); - let image = conn.serialize(rusqlite::MAIN_DB).unwrap().to_vec(); + let image = conn.raw().serialize(rusqlite::MAIN_DB).unwrap().to_vec(); if let Some(cache) = &cache { cache.write(&image); } @@ -511,10 +544,10 @@ pub(crate) fn create_test_schema_on_disk(db_path: &std::path::Path) { #[cfg(test)] pub(crate) fn test_conn() -> Connection { let image = test_schema_image(); - let mut conn = Connection::open_in_memory().unwrap(); + let mut conn = rusqlite::Connection::open_in_memory().unwrap(); conn.deserialize_read_exact(rusqlite::MAIN_DB, image, image.len(), false) .unwrap(); - conn + Connection::from_raw(conn) } /// Seed pre-validation lifecycle evidence without weakening subsequent writes. @@ -529,7 +562,7 @@ pub(crate) fn seed_historical_lifecycle_rows( conn: &Connection, seed: impl FnOnce(&Connection) -> T, ) -> T { - let tx = conn.unchecked_transaction().unwrap(); + let tx = Transaction::deferred(conn).unwrap(); let validators: Vec<(String, String)> = tx .prepare( "SELECT name, sql FROM sqlite_master @@ -541,7 +574,7 @@ pub(crate) fn seed_historical_lifecycle_rows( .unwrap() .query_map([], |row| Ok((row.get(0)?, row.get(1)?))) .unwrap() - .collect::>() + .collect::>() .unwrap(); assert!( !validators.is_empty(), @@ -558,6 +591,15 @@ pub(crate) fn seed_historical_lifecycle_rows( result } +/// What tests outside the store need for raw SQL fixtures and tripwires, +/// besides the statement methods of [`Connection`]. +#[cfg(test)] +pub(crate) mod fixture { + pub(crate) use rusqlite::trace::{TraceEvent, TraceEventCodes}; + pub(crate) use rusqlite::types::{Type, Value}; + pub(crate) use rusqlite::{StatementStatus, params}; +} + #[cfg(test)] pub(crate) mod tests; diff --git a/crates/libs/sbx/mgr/server/src/db/auto_lifecycle.rs b/crates/libs/sbx/mgr/server/src/db/auto_lifecycle.rs index a5e189c58..ba04f9292 100644 --- a/crates/libs/sbx/mgr/server/src/db/auto_lifecycle.rs +++ b/crates/libs/sbx/mgr/server/src/db/auto_lifecycle.rs @@ -121,10 +121,10 @@ pub(crate) fn due_auto_lifecycle_batch( let (id, deadline) = match (row.get::<_, String>(1), row.get::<_, Option>(2)) { (Ok(id), Ok(deadline)) => (id, deadline), (Err(error), _) | (_, Err(error)) if is_row_decode_error(&error) => { - tracing::warn!(rowid = row.get::<_, i64>(0)?, error = %describe_error(&error), "skipping malformed auto lifecycle discovery row"); + tracing::warn!(rowid = row.get::<_, i64>(0)?, error = %describe_raw_error(&error), "skipping malformed auto lifecycle discovery row"); return Ok(None); } - (Err(error), _) | (_, Err(error)) => return Err(error), + (Err(error), _) | (_, Err(error)) => return Err(Error::from(error)), }; let Some(deadline) = deadline else { return Ok(None); @@ -140,10 +140,10 @@ pub(crate) fn due_auto_lifecycle_batch( let state = match row.get::<_, String>(3) { Ok(state) => state, Err(error) if is_row_decode_error(&error) => { - tracing::warn!(rowid = row.get::<_, i64>(0)?, error = %describe_error(&error), "skipping malformed auto lifecycle discovery row"); + tracing::warn!(rowid = row.get::<_, i64>(0)?, error = %describe_raw_error(&error), "skipping malformed auto lifecycle discovery row"); return Ok(None); } - Err(error) => return Err(error), + Err(error) => return Err(Error::from(error)), }; if state.parse::().ok() == Some(SandboxState::Deleting) { return Ok(None); diff --git a/crates/libs/sbx/mgr/server/src/db/auto_lifecycle_delete_receipts.rs b/crates/libs/sbx/mgr/server/src/db/auto_lifecycle_delete_receipts.rs index 83a577845..7d62a839c 100644 --- a/crates/libs/sbx/mgr/server/src/db/auto_lifecycle_delete_receipts.rs +++ b/crates/libs/sbx/mgr/server/src/db/auto_lifecycle_delete_receipts.rs @@ -1,11 +1,12 @@ use std::str::FromStr; +use crate::db::{Connection, Error, OptionalExtension}; use pplx_space_common_core::types::{ AutoLifecycleDeleteReceiptId, ExecutionGeneration, NodeManagedBy, NodeRole, PolicyAuthority, SandboxActivationAttemptId, SandboxId, }; use pplx_space_common_svc::{sql::sqlite::SqliteU64, types::AutoLifecyclePolicyFingerprint}; -use rusqlite::{Connection, OptionalExtension, params, types::Type}; +use rusqlite::{params, types::Type}; use crate::auto_lifecycle_delete_receipts::{ AutoLifecycleDeleteAdmission, AutoLifecycleDeleteBlockReason, AutoLifecycleDeleteBlockedStats, @@ -78,7 +79,7 @@ pub(crate) fn execution_write_miss_error( conn: &Connection, sandbox_id: SandboxId, incoming_generation: Option, -) -> rusqlite::Error { +) -> Error { match conn.query_row( "SELECT EXISTS (SELECT 1 FROM sandboxes WHERE id = ?1), @@ -95,39 +96,35 @@ pub(crate) fn execution_write_miss_error( ], |row| Ok((row.get::<_, bool>(0)?, row.get::<_, bool>(1)?)), ) { - Ok((false, _)) => rusqlite::Error::QueryReturnedNoRows, - Ok((true, true)) => { - rusqlite::Error::ToSqlConversionFailure(Box::new(PendingReceiptOwnsExecution)) - } - Ok((true, false)) => { - rusqlite::Error::ToSqlConversionFailure(Box::new(LifecycleOwnsExecution)) - } + Ok((false, _)) => Error::no_rows(), + Ok((true, true)) => Error::to_sql_conversion(PendingReceiptOwnsExecution), + Ok((true, false)) => Error::to_sql_conversion(LifecycleOwnsExecution), Err(error) => error, } } -pub(crate) fn is_pending_receipt_execution_fence(error: &rusqlite::Error) -> bool { +pub(crate) fn is_pending_receipt_execution_fence(error: &Error) -> bool { matches!( - error, + error.raw(), rusqlite::Error::ToSqlConversionFailure(source) if source.downcast_ref::().is_some() ) } -pub(crate) fn is_lifecycle_execution_fence(error: &rusqlite::Error) -> bool { +pub(crate) fn is_lifecycle_execution_fence(error: &Error) -> bool { matches!( - error, + error.raw(), rusqlite::Error::ToSqlConversionFailure(source) if source.downcast_ref::().is_some() ) } -pub(crate) fn map_pending_receipt_delete_error(error: rusqlite::Error) -> rusqlite::Error { - match &error { +pub(crate) fn map_pending_receipt_delete_error(error: Error) -> Error { + match error.raw() { rusqlite::Error::SqliteFailure(_, Some(message)) if message == PendingReceiptOwnsDeletion::DIAGNOSTIC => { - rusqlite::Error::ToSqlConversionFailure(Box::new(PendingReceiptOwnsDeletion)) + Error::to_sql_conversion(PendingReceiptOwnsDeletion) } _ => error, } @@ -142,7 +139,7 @@ pub(crate) struct ReceiptEvidenceConflict { #[derive(Debug, thiserror::Error)] pub(crate) enum InsertPendingReceiptError { #[error(transparent)] - Database(#[from] rusqlite::Error), + Database(#[from] Error), #[error(transparent)] Conflict(#[from] ReceiptEvidenceConflict), } @@ -150,7 +147,7 @@ pub(crate) enum InsertPendingReceiptError { #[derive(Debug, thiserror::Error)] pub(crate) enum CompletePendingReceiptError { #[error(transparent)] - Database(#[from] rusqlite::Error), + Database(#[from] Error), #[error(transparent)] Conflict(#[from] ReceiptEvidenceConflict), } @@ -158,7 +155,7 @@ pub(crate) enum CompletePendingReceiptError { #[derive(Debug, thiserror::Error)] pub(crate) enum AutoLifecycleDeleteEvidenceError { #[error(transparent)] - Database(#[from] rusqlite::Error), + Database(#[from] Error), #[error("auto-lifecycle policy changed before delete admission")] PolicyChanged, #[error("auto-lifecycle authority changed from {expected} to {actual} before delete admission")] @@ -239,19 +236,16 @@ pub(crate) fn classify_and_record_admission( retained_generation_substrate, )) = values else { - return Err(AutoLifecycleDeleteEvidenceError::Database( - rusqlite::Error::QueryReturnedNoRows, - )); + return Err(AutoLifecycleDeleteEvidenceError::Database(Error::no_rows())); }; let policy: pplx_space_common_core::types::AutoLifecyclePolicy = - serde_json::from_str(&policy_json).map_err(|error| { - rusqlite::Error::FromSqlConversionFailure(1, Type::Text, Box::new(error)) - })?; + serde_json::from_str(&policy_json) + .map_err(|error| crate::db::Error::decode(1, Type::Text, error))?; if AutoLifecyclePolicyFingerprint::for_policy(&policy) != expectation.policy_fingerprint { return Err(AutoLifecycleDeleteEvidenceError::PolicyChanged); } - let authority: PolicyAuthority = parse_text(&authority, 0)?; + let authority: PolicyAuthority = parse_text(&authority, 0).map_err(crate::db::Error::from)?; if authority != expectation.authority { return Err(AutoLifecycleDeleteEvidenceError::AuthorityChanged { expected: expectation.authority, @@ -554,7 +548,7 @@ pub(crate) fn record_retained_route_evidence( conn: &Connection, sandbox_id: SandboxId, evidence: RouteEvidence, -) -> rusqlite::Result { +) -> crate::db::Result { // Activation attempts are UUIDv7. Their canonical text order therefore // prevents a delayed same-generation write from replacing a later dispatch. let updated = conn.execute( @@ -603,7 +597,7 @@ pub(crate) fn record_retained_route_evidence( pub(crate) fn route_identity_adoption_metadata( conn: &Connection, sandbox_id: SandboxId, -) -> rusqlite::Result { +) -> crate::db::Result { conn.query_row( "SELECT space_api_route_identity_provenance, last_observed_execution_generation, @@ -649,7 +643,7 @@ pub(crate) fn route_identity_adoption_metadata( ) } -fn clear_route_evidence_block(conn: &Connection, sandbox_id: SandboxId) -> rusqlite::Result<()> { +fn clear_route_evidence_block(conn: &Connection, sandbox_id: SandboxId) -> crate::db::Result<()> { let reason = conn .query_row( "SELECT auto_lifecycle_delete_block_reason @@ -674,7 +668,7 @@ pub(super) fn discard_pending_receipt_superseded_by_generation( conn: &Connection, sandbox_id: SandboxId, current_generation: ExecutionGeneration, -) -> rusqlite::Result> { +) -> crate::db::Result> { conn.query_row( "DELETE FROM auto_lifecycle_delete_outbox WHERE sandbox_id = ?1 @@ -703,7 +697,7 @@ pub(crate) fn discard_pending_receipt_for_execution_advance( conn: &Connection, sandbox_id: SandboxId, superseding_generation: Option, -) -> rusqlite::Result> { +) -> crate::db::Result> { let Some(superseding_generation) = superseding_generation else { return Ok(None); }; @@ -726,7 +720,7 @@ pub(crate) fn discard_pending_receipt_for_execution_advance( pub(crate) fn pending_receipt_matches_observed_execution( conn: &Connection, sandbox_id: SandboxId, -) -> rusqlite::Result { +) -> crate::db::Result { conn.query_row( "SELECT NOT EXISTS ( SELECT 1 @@ -780,27 +774,23 @@ fn record_block( [sandbox_id.to_string()], |row| Ok((row.get(0)?, row.get(1)?)), )?; - let policy = serde_json::from_str(&policy_json).map_err(|error| { - rusqlite::Error::FromSqlConversionFailure(0, Type::Text, Box::new(error)) - })?; + let policy = serde_json::from_str(&policy_json) + .map_err(|error| crate::db::Error::decode(0, Type::Text, error))?; let mut runtime_state: pplx_space_common_core::types::AutoLifecycleRuntimeState = - serde_json::from_str(&runtime_state_json).map_err(|error| { - rusqlite::Error::FromSqlConversionFailure(1, Type::Text, Box::new(error)) - })?; + serde_json::from_str(&runtime_state_json) + .map_err(|error| crate::db::Error::decode(1, Type::Text, error))?; let retry_delay = i64::try_from(AUTO_LIFECYCLE_DELETE_RETRY_DELAY.as_secs()) - .map_err(|error| rusqlite::Error::ToSqlConversionFailure(Box::new(error)))?; + .map_err(crate::db::Error::to_sql_conversion)?; let retry_at = crate::db::now_epoch_i64()?.saturating_add(retry_delay); crate::auto_lifecycle::mark_error( &mut runtime_state, &policy, format!("auto-lifecycle delete blocked: {reason}"), ); - runtime_state.next_auto_event_at = Some( - u64::try_from(retry_at) - .map_err(|error| rusqlite::Error::ToSqlConversionFailure(Box::new(error)))?, - ); - let runtime_state_json = serde_json::to_string(&runtime_state) - .map_err(|error| rusqlite::Error::ToSqlConversionFailure(Box::new(error)))?; + runtime_state.next_auto_event_at = + Some(u64::try_from(retry_at).map_err(crate::db::Error::to_sql_conversion)?); + let runtime_state_json = + serde_json::to_string(&runtime_state).map_err(crate::db::Error::to_sql_conversion)?; conn.execute( "UPDATE sandboxes SET auto_lifecycle_delete_block_reason = ?1, @@ -826,8 +816,8 @@ fn record_block( pub(crate) fn record_receipt_conflict_retry( conn: &Connection, storage_id: &str, -) -> rusqlite::Result { - let transaction = crate::db::immediate_transaction(conn)?; +) -> crate::db::Result { + let transaction = crate::db::Transaction::immediate(conn)?; clear_block_by_storage_id(&transaction, storage_id)?; let (policy_json, runtime_state_json): (String, String) = transaction.query_row( "SELECT auto_lifecycle_json, auto_lifecycle_state_json @@ -866,11 +856,11 @@ pub(crate) fn record_receipt_conflict_retry( Ok(changed) } -fn clear_block(conn: &Connection, sandbox_id: SandboxId) -> rusqlite::Result<()> { +fn clear_block(conn: &Connection, sandbox_id: SandboxId) -> crate::db::Result<()> { clear_block_by_storage_id(conn, &sandbox_id.to_string()) } -fn clear_block_by_storage_id(conn: &Connection, storage_id: &str) -> rusqlite::Result<()> { +fn clear_block_by_storage_id(conn: &Connection, storage_id: &str) -> crate::db::Result<()> { let values = conn .query_row( "SELECT auto_lifecycle_json, auto_lifecycle_state_json @@ -933,7 +923,7 @@ pub(crate) fn insert_pending_receipt( } let (existing, existing_state) = receipt_record_for_sandbox(conn, receipt.sandbox_id)? - .ok_or(rusqlite::Error::QueryReturnedNoRows)?; + .ok_or_else(crate::db::Error::no_rows)?; if existing_state == AutoLifecycleDeleteOutboxState::Pending && existing.execution_generation == receipt.execution_generation && existing.route_activation_id == receipt.route_activation_id @@ -1004,10 +994,10 @@ pub(crate) fn complete_pending_receipt( pub(crate) fn list_completed_receipts( conn: &Connection, -) -> rusqlite::Result> { +) -> crate::db::Result> { let batch_size = i64::try_from(RECEIPT_BATCH_SIZE) .map_err(|error| rusqlite::Error::ToSqlConversionFailure(Box::new(error)))?; - let transaction = crate::db::immediate_transaction(conn)?; + let transaction = crate::db::Transaction::immediate(conn)?; let rows = { // Interleave fresh and retried receipts by rank within each class. // Fresh completions cannot starve deferred evidence, and a repeatedly @@ -1083,8 +1073,8 @@ pub(crate) fn list_completed_receipts( pub(crate) fn acknowledge_completed_receipts( conn: &Connection, receipt_ids: &[AutoLifecycleDeleteReceiptId], -) -> rusqlite::Result> { - let transaction = crate::db::immediate_transaction(conn)?; +) -> crate::db::Result> { + let transaction = crate::db::Transaction::immediate(conn)?; let mut acknowledged = Vec::new(); for receipt_id in receipt_ids { if transaction.execute( @@ -1100,7 +1090,7 @@ pub(crate) fn acknowledge_completed_receipts( Ok(acknowledged) } -pub(crate) fn outbox_stats(conn: &Connection) -> rusqlite::Result { +pub(crate) fn outbox_stats(conn: &Connection) -> crate::db::Result { let now = crate::db::now_epoch_i64()?; let (pending, completed, attempts, oldest_pending, oldest_completed) = conn.query_row( "SELECT @@ -1143,7 +1133,7 @@ pub(crate) fn outbox_stats(conn: &Connection) -> rusqlite::Result rusqlite::Result> { +) -> crate::db::Result> { let now = crate::db::now_epoch_i64()?; let mut statement = conn.prepare( "SELECT auto_lifecycle_delete_block_reason, @@ -1170,13 +1160,14 @@ pub(crate) fn blocked_stats( oldest_age_secs, }) })? - .collect() + .collect::>() + .map_err(crate::db::Error::from) } pub(crate) fn pending_receipt_for_sandbox( conn: &Connection, sandbox_id: SandboxId, -) -> rusqlite::Result> { +) -> crate::db::Result> { Ok( receipt_record_for_sandbox(conn, sandbox_id)?.and_then(|(receipt, state)| { (state == AutoLifecycleDeleteOutboxState::Pending).then_some(receipt) @@ -1187,7 +1178,7 @@ pub(crate) fn pending_receipt_for_sandbox( fn receipt_record_for_sandbox( conn: &Connection, sandbox_id: SandboxId, -) -> rusqlite::Result> { +) -> crate::db::Result> { conn.query_row( "SELECT receipt_id, execution_generation, route_activation_id, auto_lifecycle_policy_fingerprint, state diff --git a/crates/libs/sbx/mgr/server/src/db/auto_lifecycle_delete_receipts/tests.rs b/crates/libs/sbx/mgr/server/src/db/auto_lifecycle_delete_receipts/tests.rs index 650b8ea56..bf87e7410 100644 --- a/crates/libs/sbx/mgr/server/src/db/auto_lifecycle_delete_receipts/tests.rs +++ b/crates/libs/sbx/mgr/server/src/db/auto_lifecycle_delete_receipts/tests.rs @@ -1,5 +1,6 @@ use std::collections::BTreeMap; +use crate::db::{Connection, OptionalExtension}; use pplx_space_common_core::types::{ AutoLifecycleConditionSet, AutoLifecycleConditionSetId, AutoLifecycleDeleteReceiptId, AutoLifecycleIdleAction, AutoLifecyclePolicy, AutoLifecycleStage, AutoLifecycleWakePolicy, @@ -8,7 +9,6 @@ use pplx_space_common_core::types::{ SpaceApiRouteIdentity, }; use pplx_space_common_svc::types::AutoLifecyclePolicyFingerprint; -use rusqlite::{Connection, OptionalExtension}; use super::*; use crate::auto_lifecycle_delete_receipts::*; @@ -989,7 +989,7 @@ fn pending_receipt_guard_aborts_only_the_delete_statement() { ) .unwrap(); - let tx = crate::db::immediate_transaction(&conn).unwrap(); + let tx = crate::db::Transaction::immediate(&conn).unwrap(); tx.execute( "UPDATE sandboxes SET failure_reason = 'before guarded delete' WHERE id = ?1", [sandbox_id.to_string()], @@ -1002,7 +1002,7 @@ fn pending_receipt_guard_aborts_only_the_delete_statement() { ) .unwrap_err(); let error = map_pending_receipt_delete_error(error); - let rusqlite::Error::ToSqlConversionFailure(error) = error else { + let rusqlite::Error::ToSqlConversionFailure(error) = error.raw() else { panic!("pending-receipt guard must map to a typed database error") }; assert!(error.downcast_ref::().is_some()); @@ -1306,10 +1306,7 @@ fn pending_receipt_fence_is_distinct_from_a_missing_sandbox() { RouteIdentityProvenance::ClusterFenced, ) .unwrap_err(); - assert!(matches!( - missing_error, - rusqlite::Error::QueryReturnedNoRows - )); + assert!(missing_error.is_no_rows()); } #[test] diff --git a/crates/libs/sbx/mgr/server/src/db/cleanup_episodes.rs b/crates/libs/sbx/mgr/server/src/db/cleanup_episodes.rs index 20774ed10..7838a162e 100644 --- a/crates/libs/sbx/mgr/server/src/db/cleanup_episodes.rs +++ b/crates/libs/sbx/mgr/server/src/db/cleanup_episodes.rs @@ -18,7 +18,7 @@ pub enum CompleteCleanupEpisodeError { #[error("row-deleting cleanup cannot complete as a resident state")] DeleteHasNoTargetState, #[error(transparent)] - Db(#[from] rusqlite::Error), + Db(#[from] Error), #[error("cleanup episode cannot restore suspended without a registered full suspend artifact")] MissingSuspendArtifact, #[error("cleanup episode sandbox row disappeared")] @@ -32,7 +32,7 @@ pub enum CompleteCleanupEpisodeError { #[derive(Debug, thiserror::Error)] pub enum InsertSandboxWithCleanupEpisodeError { #[error(transparent)] - Db(#[from] rusqlite::Error), + Db(#[from] Error), #[error( "sandbox state {sandbox_state} does not match cleanup activation state {activation_state}" )] @@ -45,7 +45,7 @@ pub enum InsertSandboxWithCleanupEpisodeError { #[derive(Debug, thiserror::Error)] pub enum RequestCleanupEpisodeError { #[error(transparent)] - Db(#[from] rusqlite::Error), + Db(#[from] Error), #[error("cleanup episode phase update affected {changed} rows instead of one")] PhaseUpdateInvariant { changed: usize }, } @@ -174,9 +174,7 @@ pub(crate) fn insert_sandbox_with_cleanup_episode_and_route_evidence( if let Some(route_evidence) = route_evidence { auto_lifecycle_delete_receipts::record_retained_route_evidence( &tx, - sandbox_id - .parse() - .map_err(|error| rusqlite::Error::ToSqlConversionFailure(Box::new(error)))?, + sandbox_id.parse().map_err(Error::to_sql_conversion)?, route_evidence, )?; } @@ -633,7 +631,7 @@ enum QuarantineRecoveryMatch { } fn validate_cleanup_completion_artifact( - tx: &rusqlite::Transaction<'_>, + tx: &Transaction<'_>, sandbox_id: &str, episode: &CleanupEpisode, ) -> std::result::Result<(), CompleteCleanupEpisodeError> { @@ -649,7 +647,7 @@ fn validate_cleanup_completion_artifact( } fn match_quarantine_recovery( - tx: &rusqlite::Transaction<'_>, + tx: &Transaction<'_>, sandbox_id: &str, cleanup_id: SandboxCleanupEpisodeId, episode: &CleanupEpisode, @@ -755,7 +753,7 @@ fn transition_receipt_for_completion( } fn apply_cleanup_state( - tx: &rusqlite::Transaction<'_>, + tx: &Transaction<'_>, sandbox_id: &str, target_state: SandboxState, receipt: Option>, @@ -894,7 +892,7 @@ fn apply_cleanup_state( } fn complete_state_cleanup( - tx: &rusqlite::Transaction<'_>, + tx: &Transaction<'_>, sandbox_id: &str, cleanup_id: SandboxCleanupEpisodeId, episode: &CleanupEpisode, @@ -940,7 +938,7 @@ fn complete_state_cleanup( } fn retire_quarantine_recovery( - tx: &rusqlite::Transaction<'_>, + tx: &Transaction<'_>, sandbox_id: &str, recovery: &QuarantineRecoveryCompletion, ) -> std::result::Result<(), CompleteCleanupEpisodeError> { @@ -1127,7 +1125,7 @@ pub(crate) enum CompleteRowIntentCleanupError { #[error("row-deleting cleanup cannot complete as a resident state")] DeleteHasNoTargetState, #[error(transparent)] - Database(#[from] rusqlite::Error), + Database(#[from] Error), #[error(transparent)] ReceiptCompletion(#[from] auto_lifecycle_delete_receipts::CompletePendingReceiptError), #[error( @@ -1242,15 +1240,11 @@ pub(crate) fn complete_row_intent_cleanup_for_job( job_id: Option<&JobId>, ) -> std::result::Result { if published_suspend_artifact.is_some() && completion != CleanupCompletion::MarkSuspended { - return Err(rusqlite::Error::InvalidQuery.into()); + return Err(Error::invalid_query().into()); } let tx = immediate_transaction(conn)?; let parsed_delete_sandbox_id = (completion.deletes_row()) - .then(|| { - sandbox_id - .parse() - .map_err(|error| rusqlite::Error::ToSqlConversionFailure(Box::new(error))) - }) + .then(|| sandbox_id.parse().map_err(Error::to_sql_conversion)) .transpose()?; let pending_delete_receipt = parsed_delete_sandbox_id .map(|sandbox_id| { @@ -1373,7 +1367,7 @@ pub(crate) fn complete_row_intent_cleanup_for_job( failure_reason, published_suspend_artifact, )? { - return Err(rusqlite::Error::QueryReturnedNoRows.into()); + return Err(Error::no_rows().into()); } if missing_suspend_artifact { let timestamp = now()?; @@ -1915,13 +1909,13 @@ pub(crate) fn cleanup_recovery_page( next_cursor: None, }); }; - let tx = conn.unchecked_transaction()?; + let tx = Transaction::deferred(conn)?; let mut inventory = tx.prepare(sql!( "SELECT rowid FROM sandboxes WHERE rowid >= ?1 ORDER BY rowid LIMIT ?2" ))?; let range: Vec = inventory .query_map(params![first, sql_limit], |row| row.get(0))? - .collect::>()?; + .collect::>()?; let Some(&last) = range.last() else { return Ok(CleanupRecoveryPage { ids: Vec::new(), @@ -1967,7 +1961,7 @@ pub(crate) fn cleanup_recovery_page( )?; let ids = statement .query_map(params![first, last], |row| row.get::<_, String>(0))? - .collect::>()?; + .collect::>()?; let next_cursor = (range.len() == limit).then_some(last); drop(statement); drop(inventory); diff --git a/crates/libs/sbx/mgr/server/src/db/cleanup_jobs.rs b/crates/libs/sbx/mgr/server/src/db/cleanup_jobs.rs index 7849a8510..989bc3856 100644 --- a/crates/libs/sbx/mgr/server/src/db/cleanup_jobs.rs +++ b/crates/libs/sbx/mgr/server/src/db/cleanup_jobs.rs @@ -44,20 +44,27 @@ pub(crate) fn finish( conn: &Connection, id: &JobId, outcome: &CleanupOutcome, -) -> rusqlite::Result { +) -> crate::db::Result { match outcome { - CleanupOutcome::Applied | CleanupOutcome::AlreadyApplied => jobs::complete_job(conn, id, 0), + CleanupOutcome::Applied | CleanupOutcome::AlreadyApplied => { + Ok(jobs::complete_job(conn.raw(), id, 0)?) + } CleanupOutcome::Superseded => { - jobs::request_job_cancellation(conn, id)?; - jobs::mark_job_cancelled(conn, id) + jobs::request_job_cancellation(conn.raw(), id)?; + Ok(jobs::mark_job_cancelled(conn.raw(), id)?) } CleanupOutcome::Quarantined(message) | CleanupOutcome::Blocked(message) => { let error = serde_json::to_string(&CleanupFailure { code: 9, // gRPC FailedPrecondition; decoded at the service boundary. message: message.clone(), }) - .map_err(|error| rusqlite::Error::ToSqlConversionFailure(Box::new(error)))?; - jobs::fail_running_job_permanent(conn, id, &error, None) + .map_err(Error::to_sql_conversion)?; + Ok(jobs::fail_running_job_permanent( + conn.raw(), + id, + &error, + None, + )?) } } } @@ -66,11 +73,11 @@ pub(crate) fn finish_in_transaction( conn: &Connection, id: Option<&JobId>, outcome: &CleanupOutcome, -) -> rusqlite::Result<()> { +) -> crate::db::Result<()> { if let Some(id) = id && !finish(conn, id, outcome)? { - return Err(rusqlite::Error::QueryReturnedNoRows); + return Err(Error::no_rows()); } Ok(()) } @@ -80,7 +87,7 @@ pub(crate) fn finish_in_transaction( pub(crate) fn eligibility( conn: &Connection, work: &CleanupWork, -) -> rusqlite::Result { +) -> crate::db::Result { let id = work.sandbox_id.to_string(); let state = current_state_label(conn, &id)?; if state.is_none() @@ -196,7 +203,7 @@ impl CleanupWork { #[derive(Debug, thiserror::Error)] pub(crate) enum CleanupJobError { #[error("cleanup scheduling database: {0}")] - Database(#[from] rusqlite::Error), + Database(#[from] Error), #[error("cleanup scheduling payload: {0}")] Payload(#[from] serde_json::Error), #[error("unsupported cleanup scheduling version {0}")] @@ -220,8 +227,10 @@ pub(crate) fn persist( mut work: CleanupWork, ) -> std::result::Result { let subject = work.sandbox_id.to_string(); - if let Some(id) = jobs::find_active_job(conn, KIND, &subject)? { - let existing = jobs::get_job(conn, &id)?.ok_or(CleanupJobError::Missing)?; + if let Some(id) = jobs::find_active_job(conn.raw(), KIND, &subject).map_err(Error::from)? { + let existing = jobs::get_job(conn.raw(), &id) + .map_err(Error::from)? + .ok_or(CleanupJobError::Missing)?; let previous = decode(&existing.payload)?; // A recovery hint has no new publication evidence. Keep evidence already // attached to this same execution and completion obligation. @@ -237,24 +246,25 @@ pub(crate) fn persist( work.immediate_kill |= previous.immediate_kill; work.guest_flush = work.guest_flush.merge(previous.guest_flush); jobs::update_active_job_payload( - conn, + conn.raw(), &id, &serde_json::to_string(&work)?, work.priority(), - )?; + ) + .map_err(Error::from)?; return Ok(id); } // The domain fence excludes a mutating attempt. A dispatched attempt // still waiting for the fence rechecks status before touching resources. - jobs::request_job_cancellation(conn, &id)?; - jobs::mark_job_cancelled(conn, &id)?; + jobs::request_job_cancellation(conn.raw(), &id).map_err(Error::from)?; + jobs::mark_job_cancelled(conn.raw(), &id).map_err(Error::from)?; } let id = JobId::new(); // The persisting caller's context, which an attempt no caller waits for // links to. A job coalesced into an active one keeps that job's context. let traceparent = pplx_space_observability::trace_propagation::current_traceparent(); jobs::insert_job( - conn, + conn.raw(), jobs::InsertJob { job_id: &id, kind: KIND, @@ -265,11 +275,12 @@ pub(crate) fn persist( traceparent: traceparent.as_deref(), priority: work.priority(), }, - )?; + ) + .map_err(Error::from)?; Ok(id) } -pub(crate) fn current(conn: &Connection, work: &CleanupWork) -> rusqlite::Result { +pub(crate) fn current(conn: &Connection, work: &CleanupWork) -> crate::db::Result { let id = work.sandbox_id.to_string(); if runtime_execution_generation(conn, &id)? != work.execution_generation { return Ok(false); @@ -288,6 +299,55 @@ pub(crate) fn current(conn: &Connection, work: &CleanupWork) -> rusqlite::Result cleanup_intent_current(conn, &id, work.completion) } +/// Requeue every running cleanup job: none survives a restart. +pub(crate) fn reset_running(conn: &Connection) -> Result { + Ok(jobs::reset_running_kind(conn.raw(), KIND)?) +} + +/// Requeue running cleanup jobs whose subject has no active attempt. +pub(crate) fn reset_inactive_running( + conn: &Connection, + active_subjects: &[String], +) -> Result { + Ok(jobs::reset_inactive_running_kind( + conn.raw(), + KIND, + active_subjects, + )?) +} + +pub(crate) fn get_job( + conn: &Connection, + job_id: &JobId, +) -> Result> { + Ok(jobs::get_job(conn.raw(), job_id)?) +} + +/// Claim a due job; `false` if another claimant won it. +pub(crate) fn mark_running(conn: &Connection, job_id: &JobId) -> Result { + Ok(jobs::mark_job_running(conn.raw(), job_id)?) +} + +/// Return a running job to the queue without spending an attempt. +pub(crate) fn yield_running(conn: &Connection, job_id: &JobId) -> Result { + Ok(jobs::yield_running_job(conn.raw(), job_id)?) +} + +/// Schedule a running job's retry after `delay_secs`. +pub(crate) fn retry_running( + conn: &Connection, + job_id: &JobId, + error: &str, + delay_secs: u64, +) -> Result { + Ok(jobs::retry_running_job( + conn.raw(), + job_id, + error, + delay_secs, + )?) +} + /// Indexed retry readiness. Fenced admission coalesces each sandbox to one active /// job, so exclusion costs at most the bounded number of active subjects. pub(crate) fn due_job_ids( @@ -295,7 +355,7 @@ pub(crate) fn due_job_ids( priority: JobPriority, active_subjects: &[String], limit: usize, -) -> rusqlite::Result> { +) -> crate::db::Result> { if limit == 0 { return Ok(Vec::new()); } @@ -314,7 +374,8 @@ pub(crate) fn due_job_ids( .query_map(params![KIND, priority.get(), excluded, limit], |row| { row.get(0) })? - .collect() + .collect::>() + .map_err(Error::from) } #[cfg(test)] diff --git a/crates/libs/sbx/mgr/server/src/db/cleanup_jobs/tests.rs b/crates/libs/sbx/mgr/server/src/db/cleanup_jobs/tests.rs index 5333311e9..fe02f7515 100644 --- a/crates/libs/sbx/mgr/server/src/db/cleanup_jobs/tests.rs +++ b/crates/libs/sbx/mgr/server/src/db/cleanup_jobs/tests.rs @@ -19,47 +19,47 @@ fn cleanup_job_admission_is_atomic_deduplicated_and_strength_preserving() { published_artifact: Some((LocalArtifactId::new(), "/cache/suspend".to_owned())), }; { - let tx = conn.unchecked_transaction().unwrap(); + let tx = Transaction::deferred(&conn).unwrap(); persist(&tx, work.clone()).unwrap(); } assert!( - jobs::find_active_job(&conn, KIND, &sandbox_id.to_string()) + jobs::find_active_job(conn.raw(), KIND, &sandbox_id.to_string()) .unwrap() .is_none() ); - let tx = conn.unchecked_transaction().unwrap(); + let tx = Transaction::deferred(&conn).unwrap(); let id = persist(&tx, work.clone()).unwrap(); tx.commit().unwrap(); let mut kill = work.clone(); kill.immediate_kill = true; - let tx = conn.unchecked_transaction().unwrap(); + let tx = Transaction::deferred(&conn).unwrap(); assert_eq!(persist(&tx, kill).unwrap(), id); assert_eq!(persist(&tx, work.clone()).unwrap(), id); tx.commit().unwrap(); assert!( - decode(&jobs::get_job(&conn, &id).unwrap().unwrap().payload) + decode(&jobs::get_job(conn.raw(), &id).unwrap().unwrap().payload) .unwrap() .immediate_kill ); let mut recovery = work.clone(); recovery.published_artifact = None; - let tx = conn.unchecked_transaction().unwrap(); + let tx = Transaction::deferred(&conn).unwrap(); assert_eq!(persist(&tx, recovery).unwrap(), id); tx.commit().unwrap(); assert_eq!( - decode(&jobs::get_job(&conn, &id).unwrap().unwrap().payload) + decode(&jobs::get_job(conn.raw(), &id).unwrap().unwrap().payload) .unwrap() .published_artifact, work.published_artifact ); let mut replacement = work; replacement.execution_generation = Some(ExecutionGeneration::new(2).unwrap()); - let tx = conn.unchecked_transaction().unwrap(); + let tx = Transaction::deferred(&conn).unwrap(); let next = persist(&tx, replacement).unwrap(); tx.commit().unwrap(); assert_ne!(next, id); assert_eq!( - jobs::get_job(&conn, &id).unwrap().unwrap().status, + jobs::get_job(conn.raw(), &id).unwrap().unwrap().status, JobStatus::Cancelled.as_str() ); } @@ -84,11 +84,11 @@ fn a_stops_guest_flush_persists_and_a_forced_stop_overrides_it() { published_artifact: None, }; let persisted = |id: &JobId| { - decode(&jobs::get_job(&conn, id).unwrap().unwrap().payload) + decode(&jobs::get_job(conn.raw(), id).unwrap().unwrap().payload) .unwrap() .guest_flush }; - let tx = conn.unchecked_transaction().unwrap(); + let tx = Transaction::deferred(&conn).unwrap(); let id = persist(&tx, stop(GuestFlush::BeforeTermination, false)).unwrap(); assert_eq!( persist(&tx, stop(GuestFlush::Unrequested, false)).unwrap(), @@ -97,7 +97,7 @@ fn a_stops_guest_flush_persists_and_a_forced_stop_overrides_it() { tx.commit().unwrap(); assert_eq!(persisted(&id), GuestFlush::BeforeTermination); - let tx = conn.unchecked_transaction().unwrap(); + let tx = Transaction::deferred(&conn).unwrap(); assert_eq!(persist(&tx, stop(GuestFlush::Forbidden, true)).unwrap(), id); assert_eq!( persist(&tx, stop(GuestFlush::BeforeTermination, false)).unwrap(), @@ -267,7 +267,7 @@ fn bounded_dispatch_preserves_retry_priority_and_other_workers() { ), ] { insert_job( - &conn, + conn.raw(), InsertJob { job_id: &id, kind, @@ -282,24 +282,24 @@ fn bounded_dispatch_preserves_retry_priority_and_other_workers() { .unwrap(); } mark_job_running( - &conn, + conn.raw(), &"019d84bd-280d-7331-99c6-000000000001".parse().unwrap(), ) .unwrap(); retry_running_job( - &conn, + conn.raw(), &"019d84bd-280d-7331-99c6-000000000001".parse().unwrap(), "busy", 3600, ) .unwrap(); mark_job_running( - &conn, + conn.raw(), &"019d84bd-280d-7331-99c6-000000000002".parse().unwrap(), ) .unwrap(); mark_job_running( - &conn, + conn.raw(), &"019d84bd-280d-7331-99c6-000000000005".parse().unwrap(), ) .unwrap(); @@ -315,10 +315,10 @@ fn bounded_dispatch_preserves_retry_priority_and_other_workers() { .unwrap() .is_empty() ); - reset_inactive_running_kind(&conn, KIND, &active).unwrap(); + reset_inactive_running_kind(conn.raw(), KIND, &active).unwrap(); assert_eq!( get_job( - &conn, + conn.raw(), &"019d84bd-280d-7331-99c6-000000000002".parse().unwrap() ) .unwrap() @@ -327,7 +327,7 @@ fn bounded_dispatch_preserves_retry_priority_and_other_workers() { "running" ); update_active_job_payload( - &conn, + conn.raw(), &"019d84bd-280d-7331-99c6-000000000001".parse().unwrap(), "{\"kill\":true}", JobPriority::new(1), @@ -336,10 +336,10 @@ fn bounded_dispatch_preserves_retry_priority_and_other_workers() { let promoted = due_job_ids(&conn, JobPriority::new(1), &[], 2).unwrap(); assert_eq!(promoted.len(), 2); assert!(promoted.contains(&"019d84bd-280d-7331-99c6-000000000001".parse().unwrap())); - assert_eq!(reset_running_kind(&conn, KIND).unwrap(), 1); + assert_eq!(reset_running_kind(conn.raw(), KIND).unwrap(), 1); assert_eq!( get_job( - &conn, + conn.raw(), &"019d84bd-280d-7331-99c6-000000000002".parse().unwrap() ) .unwrap() @@ -349,7 +349,7 @@ fn bounded_dispatch_preserves_retry_priority_and_other_workers() { ); assert_eq!( get_job( - &conn, + conn.raw(), &"019d84bd-280d-7331-99c6-000000000005".parse().unwrap() ) .unwrap() diff --git a/crates/libs/sbx/mgr/server/src/db/cleanup_target_scan.rs b/crates/libs/sbx/mgr/server/src/db/cleanup_target_scan.rs index 2f338b68c..d654cb6da 100644 --- a/crates/libs/sbx/mgr/server/src/db/cleanup_target_scan.rs +++ b/crates/libs/sbx/mgr/server/src/db/cleanup_target_scan.rs @@ -1,9 +1,10 @@ //! Bounded identity discovery for operator selection, not mutation authority. use super::inventory_read::{InventoryReadError, check_row}; +use crate::db::Connection; use pplx_space_common_core::inventory::{InventoryBudget, MAX_ENCODED_BYTES}; use pplx_space_common_core::types::{SandboxId, SandboxOwner, SandboxRuntimeLabel}; -use rusqlite::{Connection, types::Type}; +use rusqlite::types::Type; use super::CleanupTarget; diff --git a/crates/libs/sbx/mgr/server/src/db/discovery_indexes_tests.rs b/crates/libs/sbx/mgr/server/src/db/discovery_indexes_tests.rs index bb1943e6f..0576cb4c2 100644 --- a/crates/libs/sbx/mgr/server/src/db/discovery_indexes_tests.rs +++ b/crates/libs/sbx/mgr/server/src/db/discovery_indexes_tests.rs @@ -42,7 +42,7 @@ fn host_process_and_artifact_path_lookups_use_nonunique_indexes() { .unwrap() .query_map([], |row| row.get::<_, String>(3)) .unwrap() - .collect::>>() + .collect::>>() .unwrap(); assert!( details diff --git a/crates/libs/sbx/mgr/server/src/db/eviction.rs b/crates/libs/sbx/mgr/server/src/db/eviction.rs index 7475c8a12..24b1ad416 100644 --- a/crates/libs/sbx/mgr/server/src/db/eviction.rs +++ b/crates/libs/sbx/mgr/server/src/db/eviction.rs @@ -308,7 +308,7 @@ pub fn claim_retired_teardown_execution_for_release( fn cancel_live_upload_job(conn: &Connection, artifact: LocalArtifactId) -> Result<()> { use pplx_space_common_jobs::{JobStatus, db as jobs}; let Some(job) = jobs::find_job_by_dedup_key( - conn, + conn.raw(), super::snapshot_upload_jobs::SNAPSHOT_UPLOAD_JOB_KIND, &artifact.to_string(), )? @@ -318,9 +318,9 @@ fn cancel_live_upload_job(conn: &Connection, artifact: LocalArtifactId) -> Resul if job.is_terminal() { return Ok(()); } - jobs::request_job_cancellation(conn, &job.job_id)?; + jobs::request_job_cancellation(conn.raw(), &job.job_id)?; if job.status == JobStatus::Pending.as_str() { - jobs::mark_job_cancelled(conn, &job.job_id)?; + jobs::mark_job_cancelled(conn.raw(), &job.job_id)?; } Ok(()) } @@ -343,12 +343,12 @@ pub fn other_local_artifacts_awaiting_backup( AND backup_completed_at IS NULL ORDER BY local_artifact_id", )?; - statement + Ok(statement .query_map( params![id, staged_artifact.to_string()], row_to_local_artifact, )? - .collect() + .collect::>()?) } /// Whether this generation's teardown-suspend job is still running: the other diff --git a/crates/libs/sbx/mgr/server/src/db/eviction_scan.rs b/crates/libs/sbx/mgr/server/src/db/eviction_scan.rs index c23167607..1d5492861 100644 --- a/crates/libs/sbx/mgr/server/src/db/eviction_scan.rs +++ b/crates/libs/sbx/mgr/server/src/db/eviction_scan.rs @@ -1,8 +1,9 @@ //! Suspended-sandbox reads behind `crate::sandbox_eviction`'s candidate //! scans and local eviction evidence. +use crate::db::{Connection, OptionalExtension, Result}; use pplx_space_common_svc::sql::sqlite::SqliteU64; -use rusqlite::{Connection, OptionalExtension, Result, params}; +use rusqlite::params; use crate::db::timestamps::EpochSeconds; @@ -107,7 +108,7 @@ pub(crate) fn idle_suspended_candidates( Ok((sandbox_id, idle_witness_secs)) }, )? - .collect::>()?; + .collect::>()?; Ok(rows) } @@ -145,7 +146,7 @@ pub(crate) fn legacy_reupload_candidates( params![SqliteU64(idle_cutoff_epoch_secs), limit as i64], |r| r.get(0), )? - .collect::>()?; + .collect::>()?; Ok(rows) } diff --git a/crates/libs/sbx/mgr/server/src/db/guest_health_scan.rs b/crates/libs/sbx/mgr/server/src/db/guest_health_scan.rs index 29f3ae51e..bdcf1c696 100644 --- a/crates/libs/sbx/mgr/server/src/db/guest_health_scan.rs +++ b/crates/libs/sbx/mgr/server/src/db/guest_health_scan.rs @@ -1,7 +1,8 @@ //! Sandbox rows the watchdog's guest-health and probe passes read. +use crate::db::{Connection, Error, Result}; +use rusqlite::OptionalExtension as _; use rusqlite::types::Type; -use rusqlite::{Connection, OptionalExtension, Result}; use pplx_space_common_core::sql; @@ -37,7 +38,7 @@ pub(crate) fn list_guest_health_rows(conn: &Connection) -> Result>()?) } /// One sandbox's `state` label, `None` without a row. @@ -56,7 +57,8 @@ pub(crate) fn state_labels_for_ids<'a>( .map(|sandbox_id| { let label = statement .query_row([sandbox_id], |row| row.get::<_, String>(0)) - .optional(); + .optional() + .map_err(Error::from); (sandbox_id, label) }) .collect()) diff --git a/crates/libs/sbx/mgr/server/src/db/handle.rs b/crates/libs/sbx/mgr/server/src/db/handle.rs new file mode 100644 index 000000000..8d049d1a1 --- /dev/null +++ b/crates/libs/sbx/mgr/server/src/db/handle.rs @@ -0,0 +1,327 @@ +//! The store's handle types: the shared connection and its lock guard, the +//! connection view every `db` function takes, transactions, and the error +//! they return. Callers outside `db` name only these; the rusqlite types they +//! wrap stay inside the store. + +use std::ops::Deref; +use std::sync::{Arc, LockResult, Mutex, MutexGuard, PoisonError}; + +use ref_cast::{RefCastCustom, ref_cast_custom}; + +/// A method that returns or takes rusqlite values: crate-visible in test +/// builds, for fixtures and tripwires outside `db`, and private to `db` +/// otherwise, so production code outside the store cannot reach a raw +/// connection, statement or error. +macro_rules! db_private_outside_tests { + ($(#[$attr:meta])* fn $name:ident $($rest:tt)*) => { + $(#[$attr])* + #[cfg(test)] + pub(crate) fn $name $($rest)* + + $(#[$attr])* + #[cfg(not(test))] + pub(in crate::db) fn $name $($rest)* + }; +} + +/// A failed SQLite operation. `Display` and `source` are the underlying +/// rusqlite error's, unchanged. +#[derive(Debug, thiserror::Error)] +#[error(transparent)] +pub struct Error(#[from] rusqlite::Error); + +pub type Result = std::result::Result; + +impl Error { + /// A guarded write's precondition failed: the store's "stale or + /// conflicting" refusal. + pub fn invalid_query() -> Self { + Self(rusqlite::Error::InvalidQuery) + } + + /// The value in `column` of SQL type `kind` did not decode. + pub(in crate::db) fn decode( + column: usize, + kind: rusqlite::types::Type, + error: impl Into>, + ) -> Self { + Self(rusqlite::Error::FromSqlConversionFailure( + column, + kind, + error.into(), + )) + } + + /// The statement matched no row. + pub fn no_rows() -> Self { + Self(rusqlite::Error::QueryReturnedNoRows) + } + + /// A lock protecting store state was poisoned; `message` says which. + pub fn poisoned(message: String) -> Self { + Self(rusqlite::Error::InvalidParameterName(message)) + } + + /// A value could not be converted for storage. + pub fn to_sql_conversion(error: impl Into>) -> Self { + Self(rusqlite::Error::ToSqlConversionFailure(error.into())) + } + + /// The statement matched no row. + pub fn is_no_rows(&self) -> bool { + matches!(self.0, rusqlite::Error::QueryReturnedNoRows) + } + + /// SQLite reported the database busy. + #[cfg(test)] + pub(crate) fn is_database_busy(&self) -> bool { + self.0.sqlite_error_code() == Some(rusqlite::ErrorCode::DatabaseBusy) + } + + /// The column and SQL type of a value that failed to decode. + #[cfg(test)] + pub(crate) fn conversion_failure(&self) -> Option<(usize, rusqlite::types::Type)> { + match &self.0 { + rusqlite::Error::FromSqlConversionFailure(column, kind, _) => Some((*column, *kind)), + _ => None, + } + } + + /// See [`Self::invalid_query`]. + pub fn is_invalid_query(&self) -> bool { + matches!(self.0, rusqlite::Error::InvalidQuery) + } + + db_private_outside_tests! { + fn raw(&self) -> &rusqlite::Error { + &self.0 + } + } + + #[cfg(test)] + pub(crate) fn into_raw(self) -> rusqlite::Error { + self.0 + } +} + +/// [`rusqlite::OptionalExtension`] for the store's [`Result`]. +pub(crate) trait OptionalExtension { + /// `Ok(None)` when the statement matched no row. + fn optional(self) -> Result>; +} + +impl OptionalExtension for Result { + fn optional(self) -> Result> { + match self { + Ok(value) => Ok(Some(value)), + Err(error) if error.is_no_rows() => Ok(None), + Err(error) => Err(error), + } + } +} + +/// A connection to sbx-mgr's database, as `db` functions take it: `&Connection` +/// views the connection held through a [`ConnectionGuard`] or a +/// [`Transaction`], and statements run through it join any transaction open on +/// that connection. +#[derive(RefCastCustom)] +#[repr(transparent)] +pub struct Connection(rusqlite::Connection); + +impl Connection { + #[ref_cast_custom] + fn view(raw: &rusqlite::Connection) -> &Self; + + pub(in crate::db) fn from_raw(raw: rusqlite::Connection) -> Self { + Self(raw) + } + + /// An empty in-memory database, without the schema. + #[cfg(test)] + pub(crate) fn open_in_memory() -> Self { + Self(rusqlite::Connection::open_in_memory().expect("open an in-memory SQLite database")) + } + + /// A plain connection to the database file at `path`, without the + /// production open's settings. + #[cfg(test)] + pub(crate) fn open_at(path: impl AsRef) -> Result { + Ok(Self(rusqlite::Connection::open(path)?)) + } + + #[cfg(test)] + pub(crate) fn pragma_update( + &self, + schema: Option<&str>, + name: &str, + value: V, + ) -> Result<()> { + Ok(self.0.pragma_update(schema, name, value)?) + } + + #[cfg(test)] + pub(crate) fn pragma_query_value( + &self, + schema: Option<&str>, + name: &str, + f: F, + ) -> Result + where + F: FnOnce(&rusqlite::Row<'_>) -> rusqlite::Result, + { + Ok(self.0.pragma_query_value(schema, name, f)?) + } + + #[cfg(test)] + pub(crate) fn busy_timeout(&self, timeout: std::time::Duration) -> Result<()> { + Ok(self.0.busy_timeout(timeout)?) + } + + /// [`rusqlite::Connection::trace_v2`], for tests that observe the + /// statements a store operation runs. + #[cfg(test)] + pub(crate) fn trace_v2( + &self, + mask: rusqlite::trace::TraceEventCodes, + trace: Option)>, + ) { + self.0.trace_v2(mask, trace); + } + + db_private_outside_tests! { + fn raw(&self) -> &rusqlite::Connection { + &self.0 + } + } + + db_private_outside_tests! { + fn execute(&self, sql: &str, params: P) -> Result { + Ok(self.0.execute(sql, params)?) + } + } + + #[cfg(test)] + pub(crate) fn execute_batch(&self, sql: &str) -> Result<()> { + Ok(self.0.execute_batch(sql)?) + } + + db_private_outside_tests! { + fn prepare(&self, sql: &str) -> Result> { + Ok(self.0.prepare(sql)?) + } + } + + db_private_outside_tests! { + fn prepare_cached(&self, sql: &str) -> Result> { + Ok(self.0.prepare_cached(sql)?) + } + } + + db_private_outside_tests! { + fn query_row(&self, sql: &str, params: P, f: F) -> Result + where + P: rusqlite::Params, + F: FnOnce(&rusqlite::Row<'_>) -> rusqlite::Result, + { + Ok(self.0.query_row(sql, params, f)?) + } + } + + pub(crate) fn is_autocommit(&self) -> bool { + self.0.is_autocommit() + } +} + +/// A transaction on a [`Connection`], rolled back when dropped uncommitted. +/// It derefs to the connection, so `db` functions run inside it. +pub struct Transaction<'c>(rusqlite::Transaction<'c>); + +impl<'c> Transaction<'c> { + /// `BEGIN IMMEDIATE`: takes the write lock up front. + pub fn immediate(conn: &'c Connection) -> Result { + Ok(Self(rusqlite::Transaction::new_unchecked( + &conn.0, + rusqlite::TransactionBehavior::Immediate, + )?)) + } + + /// `BEGIN DEFERRED`: takes locks as its statements need them. + pub fn deferred(conn: &'c Connection) -> Result { + Ok(Self(conn.0.unchecked_transaction()?)) + } + + pub fn commit(self) -> Result<()> { + Ok(self.0.commit()?) + } + + pub fn rollback(self) -> Result<()> { + Ok(self.0.rollback()?) + } +} + +impl Deref for Transaction<'_> { + type Target = Connection; + + fn deref(&self) -> &Connection { + Connection::view(&self.0) + } +} + +/// The process's connection to sbx-mgr's database, shared behind one mutex +/// by every writer, including the jobs worker. A panic while it is locked +/// poisons it, as for any [`Mutex`]. +#[derive(Clone)] +pub struct SharedConnection(Arc>); + +impl SharedConnection { + pub fn new(conn: Connection) -> Self { + Self(Arc::new(Mutex::new(conn.0))) + } + + /// [`Mutex::lock`]: a poisoned mutex still yields its guard through the + /// [`PoisonError`]. + pub fn lock(&self) -> LockResult> { + match self.0.lock() { + Ok(guard) => Ok(ConnectionGuard(guard)), + Err(poisoned) => Err(PoisonError::new(ConnectionGuard(poisoned.into_inner()))), + } + } + + /// [`Mutex::is_poisoned`]. + pub fn is_poisoned(&self) -> bool { + self.0.is_poisoned() + } + + /// [`Mutex::try_lock`]. + #[cfg(test)] + pub(crate) fn try_lock(&self) -> std::sync::TryLockResult> { + use std::sync::TryLockError; + match self.0.try_lock() { + Ok(guard) => Ok(ConnectionGuard(guard)), + Err(TryLockError::Poisoned(poisoned)) => Err(TryLockError::Poisoned(PoisonError::new( + ConnectionGuard(poisoned.into_inner()), + ))), + Err(TryLockError::WouldBlock) => Err(TryLockError::WouldBlock), + } + } + + /// A jobs worker over this connection, sharing its mutex. + pub fn background_worker( + &self, + executor: E, + config: pplx_space_common_jobs::WorkerConfig, + ) -> pplx_space_common_jobs::BackgroundWorker { + pplx_space_common_jobs::BackgroundWorker::new(Arc::clone(&self.0), executor, config) + } +} + +/// A held [`SharedConnection`] lock. +pub struct ConnectionGuard<'a>(MutexGuard<'a, rusqlite::Connection>); + +impl Deref for ConnectionGuard<'_> { + type Target = Connection; + + fn deref(&self) -> &Connection { + Connection::view(&self.0) + } +} diff --git a/crates/libs/sbx/mgr/server/src/db/inventory.rs b/crates/libs/sbx/mgr/server/src/db/inventory.rs index dc7af194a..4dd712310 100644 --- a/crates/libs/sbx/mgr/server/src/db/inventory.rs +++ b/crates/libs/sbx/mgr/server/src/db/inventory.rs @@ -1,8 +1,8 @@ //! Bounded local discovery. A batch is never a complete external inventory. -use rusqlite::{Connection, Result}; +use crate::db::{Connection, Result}; -use super::{SandboxRow, describe_error, get_sandbox, is_row_decode_error}; +use super::{SandboxRow, describe_error, describe_raw_error, get_sandbox, is_row_decode_error}; pub(crate) const INVENTORY_BATCH_SIZE: usize = 128; @@ -57,15 +57,15 @@ impl SandboxInventoryScan { let id = match row.get::<_, String>(1) { Ok(id) => id, Err(error) if is_row_decode_error(&error) => { - tracing::warn!(rowid, error = %describe_error(&error), "preserving undecodable inventory identity"); + tracing::warn!(rowid, error = %describe_raw_error(&error), "preserving undecodable inventory identity"); continue; } - Err(error) => return Err(error), + Err(error) => return Err(crate::db::Error::from(error)), }; match get_sandbox(conn, &id) { Ok(Some(row)) => batch.push(row), Ok(None) => {} - Err(error) if is_row_decode_error(&error) => { + Err(error) if is_row_decode_error(error.raw()) => { tracing::warn!(sandbox_id = %id, error = %describe_error(&error), "preserving undecodable inventory row"); } Err(error) => return Err(error), @@ -136,9 +136,9 @@ impl RestoreTransferScopeScan { match decoded { Ok(scope) => batch.push(scope), Err(error) if is_row_decode_error(&error) => { - tracing::warn!(rowid, error = %describe_error(&error), "preserving malformed orphan transfer scope"); + tracing::warn!(rowid, error = %describe_raw_error(&error), "preserving malformed orphan transfer scope"); } - Err(error) => return Err(error), + Err(error) => return Err(error.into()), } if batch.len() == 8 { return Ok(batch); diff --git a/crates/libs/sbx/mgr/server/src/db/inventory_read.rs b/crates/libs/sbx/mgr/server/src/db/inventory_read.rs index 7bf8f1ece..27594aa22 100644 --- a/crates/libs/sbx/mgr/server/src/db/inventory_read.rs +++ b/crates/libs/sbx/mgr/server/src/db/inventory_read.rs @@ -2,21 +2,28 @@ //! Unpaged ListBuildReferencingSandboxes is the exception: it reports overruns //! instead. +use crate::db::{Connection, Error}; use pplx_space_common_core::inventory::{ InventoryBudget, InventoryLimitExceeded, MAX_ITEMS, MAX_RAW_BYTES, }; -use rusqlite::{Connection, Row, types::ValueRef}; +use rusqlite::{Row, types::ValueRef}; #[derive(Debug, thiserror::Error)] pub(crate) enum InventoryReadError { #[error(transparent)] - Database(#[from] rusqlite::Error), + Database(#[from] Error), #[error(transparent)] Limit(#[from] InventoryLimitExceeded), #[error("ambiguous sandbox selector")] Ambiguous, } +impl From for InventoryReadError { + fn from(error: rusqlite::Error) -> Self { + Self::Database(error.into()) + } +} + impl InventoryReadError { pub(crate) fn into_status(self) -> tonic::Status { match self { @@ -40,10 +47,10 @@ fn row_raw_bytes(row: &Row<'_>) -> rusqlite::Result { Ok(bytes) } -pub(crate) fn check_row( +pub(in crate::db) fn check_row( row: &Row<'_>, budget: &mut InventoryBudget, -) -> Result<(), InventoryReadError> { +) -> rusqlite::Result<(), InventoryReadError> { budget.charge(row_raw_bytes(row)?)?; Ok(()) } @@ -114,7 +121,7 @@ pub(crate) enum ListPageRow { Undecodable { id: String, state: Option, - error: rusqlite::Error, + error: Error, }, } @@ -165,7 +172,7 @@ pub(crate) fn list_page( Err(error) => ListPageRow::Undecodable { id: row.get("id")?, state: row.get::<_, String>("state").ok(), - error, + error: error.into(), }, }); } diff --git a/crates/libs/sbx/mgr/server/src/db/inventory_read/tests.rs b/crates/libs/sbx/mgr/server/src/db/inventory_read/tests.rs index 66d3319d2..a43280a36 100644 --- a/crates/libs/sbx/mgr/server/src/db/inventory_read/tests.rs +++ b/crates/libs/sbx/mgr/server/src/db/inventory_read/tests.rs @@ -21,7 +21,7 @@ fn state_metadata_batches_share_the_callers_budget() { #[test] fn raw_value_limit_precedes_decoding_and_rejects_whole_collection() { - let conn = Connection::open_in_memory().unwrap(); + let conn = Connection::open_in_memory(); let mut stmt = conn .prepare("SELECT 'valid' UNION ALL SELECT zeroblob(6)") .unwrap(); @@ -37,8 +37,8 @@ fn raw_value_limit_precedes_decoding_and_rejects_whole_collection() { #[test] fn unary_list_serves_the_complete_inventory_past_the_item_budget() { use crate::service::tests::service_tests::insert_test_sandbox; - let mut conn = crate::db::test_conn(); - let tx = conn.transaction().unwrap(); + let conn = crate::db::test_conn(); + let tx = crate::db::Transaction::deferred(&conn).unwrap(); for _ in 0..=MAX_ITEMS { insert_test_sandbox( &tx, diff --git a/crates/libs/sbx/mgr/server/src/db/lifecycle_validation_tests.rs b/crates/libs/sbx/mgr/server/src/db/lifecycle_validation_tests.rs index ee100f3de..dc4f5260a 100644 --- a/crates/libs/sbx/mgr/server/src/db/lifecycle_validation_tests.rs +++ b/crates/libs/sbx/mgr/server/src/db/lifecycle_validation_tests.rs @@ -1,8 +1,9 @@ -use rusqlite::{Connection, params, types::Value}; +use crate::db::Connection; +use rusqlite::{params, types::Value}; use super::{SandboxState, seed_historical_lifecycle_rows, test_conn}; -fn insert(conn: &Connection, id: &str, state: &Value) -> rusqlite::Result { +fn insert(conn: &Connection, id: &str, state: &Value) -> crate::db::Result { conn.execute( "INSERT INTO sandboxes (id, uuid8, state, template, created_at, updated_at) VALUES (?1, 'sandbox1', ?2, 'template', '0', '0')", @@ -10,10 +11,10 @@ fn insert(conn: &Connection, id: &str, state: &Value) -> rusqlite::Result ) } -fn assert_lifecycle_rejected(result: rusqlite::Result) { +fn assert_lifecycle_rejected(result: crate::db::Result) { let error = result.unwrap_err(); assert_eq!( - error.sqlite_error().unwrap().extended_code, + error.raw().sqlite_error().unwrap().extended_code, rusqlite::ffi::SQLITE_CONSTRAINT_TRIGGER, ); } @@ -126,7 +127,7 @@ fn lifecycle_validation_preserves_native_not_null_conflict_handling() { } else { let error = result.unwrap_err(); assert_eq!( - error.sqlite_error().unwrap().extended_code, + error.raw().sqlite_error().unwrap().extended_code, rusqlite::ffi::SQLITE_CONSTRAINT_NOTNULL, "{sql}" ); @@ -144,10 +145,10 @@ fn lifecycle_validation_preserves_native_not_null_conflict_handling() { #[test] fn lifecycle_validation_aborts_the_whole_statement_without_poisoning_the_transaction() { - let mut conn = test_conn(); + let conn = test_conn(); insert(&conn, "first", &Value::Text("started".into())).unwrap(); insert(&conn, "second", &Value::Text("started".into())).unwrap(); - let tx = conn.transaction().unwrap(); + let tx = crate::db::Transaction::deferred(&conn).unwrap(); tx.execute("UPDATE sandboxes SET template = 'retained'", []) .unwrap(); assert_lifecycle_rejected(tx.execute( diff --git a/crates/libs/sbx/mgr/server/src/db/local_artifact_leases.rs b/crates/libs/sbx/mgr/server/src/db/local_artifact_leases.rs index df8b54e6e..9a9299f01 100644 --- a/crates/libs/sbx/mgr/server/src/db/local_artifact_leases.rs +++ b/crates/libs/sbx/mgr/server/src/db/local_artifact_leases.rs @@ -2,10 +2,11 @@ //! restore and delete leases. Each runs on the connection or transaction its //! caller holds, so a checkout's reads and its CAS share one transaction. +use crate::db::{Connection, Error, OptionalExtension, Result}; use pplx_space_common_core::sql; use pplx_space_common_core::types::{ArtifactKind, LocalArtifactId, SnapshotName}; use pplx_space_common_svc::sql::sqlite::SqliteU64; -use rusqlite::{Connection, OptionalExtension, Result, params}; +use rusqlite::params; use crate::db::timestamps::EpochSeconds; @@ -120,7 +121,8 @@ pub(crate) fn list_pending_backups(conn: &Connection) -> Result>() + .map_err(crate::db::Error::from) } /// A sandbox's named-snapshot artifacts, within the inventory read budget. @@ -260,12 +262,13 @@ pub(crate) fn reset_restoring_artifacts(conn: &Connection) -> Result { /// `(local_artifact_id, local_path)` of every `deleting` row. pub(crate) fn list_deleting_artifacts(conn: &Connection) -> Result> { - conn.prepare( - "SELECT local_artifact_id, local_path FROM local_artifacts + Ok(conn + .prepare( + "SELECT local_artifact_id, local_path FROM local_artifacts WHERE state = 'deleting'", - )? - .query_map([], |r| Ok((r.get::<_, String>(0)?, r.get::<_, String>(1)?)))? - .collect::>() + )? + .query_map([], |r| Ok((r.get::<_, String>(0)?, r.get::<_, String>(1)?)))? + .collect::>()?) } /// CAS `deleting` → `ready`; the number of rows changed. @@ -348,20 +351,22 @@ pub(crate) fn orphan_scan_rows( through: i64, limit: i64, ) -> Result> { - conn.prepare( - "SELECT la.rowid, la.local_artifact_id, la.local_path, + Ok(conn + .prepare( + "SELECT la.rowid, la.local_artifact_id, la.local_path, EXISTS (SELECT 1 FROM sandboxes s WHERE s.id = la.sandbox_id) FROM local_artifacts la WHERE la.rowid >= ?1 AND la.rowid <= ?2 ORDER BY la.rowid LIMIT ?3", - )? - .query_map(params![next, through, limit], |row| { - let decoded = (|| -> rusqlite::Result<(String, String, bool)> { - Ok((row.get(1)?, row.get(2)?, row.get(3)?)) - })(); - Ok((row.get::<_, i64>(0)?, decoded)) - })? - .collect::>>() + )? + .query_map(params![next, through, limit], |row| { + let decoded = (|| -> rusqlite::Result<(String, String, bool)> { + Ok((row.get(1)?, row.get(2)?, row.get(3)?)) + })() + .map_err(Error::from); + Ok((row.get::<_, i64>(0)?, decoded)) + })? + .collect::>>()?) } /// Delete the artifact row if its sandbox row is gone; the number deleted. diff --git a/crates/libs/sbx/mgr/server/src/db/local_artifacts.rs b/crates/libs/sbx/mgr/server/src/db/local_artifacts.rs index 1b364915c..b13e2afae 100644 --- a/crates/libs/sbx/mgr/server/src/db/local_artifacts.rs +++ b/crates/libs/sbx/mgr/server/src/db/local_artifacts.rs @@ -97,7 +97,7 @@ pub fn pin_suspended_artifact_for_cleanup( ], )?; if changed != 1 { - return Err(rusqlite::Error::QueryReturnedNoRows); + return Err(Error::no_rows()); } tx.commit()?; Ok(()) @@ -234,7 +234,7 @@ pub fn publish_restored_suspended_artifact( ], )?; if changed != 1 { - return Err(rusqlite::Error::QueryReturnedNoRows); + return Err(Error::no_rows()); } retire_restore_transfer_scope(&tx, id)?; tx.commit()?; @@ -261,7 +261,7 @@ pub fn publish_restored_suspended_without_artifact(conn: &Connection, id: &str) params![now()?, id], )?; if changed != 1 { - return Err(rusqlite::Error::QueryReturnedNoRows); + return Err(Error::no_rows()); } retire_restore_transfer_scope(&tx, id)?; tx.commit()?; @@ -286,7 +286,7 @@ pub fn publish_restored_disk_as_new(conn: &Connection, id: &str) -> Result<()> { params![now()?, id], )?; if changed != 1 { - return Err(rusqlite::Error::QueryReturnedNoRows); + return Err(Error::no_rows()); } clear_runtime_fields(&tx, id)?; retire_restore_transfer_scope(&tx, id)?; @@ -461,7 +461,7 @@ pub fn publish_stopped_artifact( ], )?; if changed != 1 { - return Err(rusqlite::Error::QueryReturnedNoRows); + return Err(Error::no_rows()); } // Clear the runtime first so the retention trigger observes its complete // identity, then close the exact attempt in the same transaction. @@ -576,7 +576,7 @@ pub fn delete_named_local_artifact( Ok(()) } -pub(super) fn row_to_local_artifact(row: &rusqlite::Row<'_>) -> Result { +pub(super) fn row_to_local_artifact(row: &rusqlite::Row<'_>) -> rusqlite::Result { Ok(LocalArtifactRow { local_artifact_id: row.get::<_, String>(0)?.parse().map_err(|error| { rusqlite::Error::FromSqlConversionFailure(0, Type::Text, Box::new(error)) diff --git a/crates/libs/sbx/mgr/server/src/db/network_policy.rs b/crates/libs/sbx/mgr/server/src/db/network_policy.rs index 0d2128b2a..f7b1fd1fb 100644 --- a/crates/libs/sbx/mgr/server/src/db/network_policy.rs +++ b/crates/libs/sbx/mgr/server/src/db/network_policy.rs @@ -317,7 +317,7 @@ pub fn enqueue_network_policy_reconcile_if_live( } fn enqueue_network_policy_reconcile_if_live_tx( - tx: &rusqlite::Transaction<'_>, + tx: &Transaction<'_>, sandbox_id: &str, ) -> Result { let ts = now()?; @@ -339,7 +339,7 @@ fn enqueue_network_policy_reconcile_if_live_tx( } fn enqueue_node_dirty_network_policy_sandbox_tx( - tx: &rusqlite::Transaction<'_>, + tx: &Transaction<'_>, sandbox_id: &str, ) -> Result { enqueue_node_dirty_ref_tx( @@ -357,10 +357,7 @@ pub fn enqueue_node_dirty_sandbox_inventory(conn: &Connection, sandbox_id: &str) Ok(queued) } -fn enqueue_node_dirty_sandbox_inventory_tx( - tx: &rusqlite::Transaction<'_>, - sandbox_id: &str, -) -> Result { +fn enqueue_node_dirty_sandbox_inventory_tx(tx: &Transaction<'_>, sandbox_id: &str) -> Result { enqueue_node_dirty_ref_tx( tx, RECONCILE_FACET_SANDBOX_INVENTORY, @@ -370,7 +367,7 @@ fn enqueue_node_dirty_sandbox_inventory_tx( } fn enqueue_node_dirty_ref_tx( - tx: &rusqlite::Transaction<'_>, + tx: &Transaction<'_>, facet: i32, element_kind: &str, element_value: &str, @@ -412,7 +409,7 @@ pub fn list_node_dirty_refs(conn: &Connection, limit: usize) -> Result>()?) } /// Acknowledge only the obligations read before sending. A newer enqueue, even diff --git a/crates/libs/sbx/mgr/server/src/db/pool.rs b/crates/libs/sbx/mgr/server/src/db/pool.rs index 7213c4ed5..b270f11cf 100644 --- a/crates/libs/sbx/mgr/server/src/db/pool.rs +++ b/crates/libs/sbx/mgr/server/src/db/pool.rs @@ -31,7 +31,7 @@ pub fn find_pool_sandbox( ], row_to_sandbox, )?; - rows.next().transpose() + rows.next().transpose().map_err(Error::from) } pub fn claim_pool_sandbox( diff --git a/crates/libs/sbx/mgr/server/src/db/quarantine.rs b/crates/libs/sbx/mgr/server/src/db/quarantine.rs index 497e20b62..6c688c763 100644 --- a/crates/libs/sbx/mgr/server/src/db/quarantine.rs +++ b/crates/libs/sbx/mgr/server/src/db/quarantine.rs @@ -43,7 +43,7 @@ pub struct RawSandboxStatePublicationMetadata { } pub type RawSandboxStatePublicationMetadataRead = - std::result::Result; + std::result::Result; #[derive(Clone, Copy, Debug, Eq, PartialEq)] pub enum SandboxStatePublicationMetadataField { @@ -256,7 +256,7 @@ fn parse_published_quarantine_reason(value: String) -> PublishedQuarantineReason #[derive(Debug, thiserror::Error)] pub enum MarkQuarantinedError { #[error(transparent)] - Db(#[from] rusqlite::Error), + Db(#[from] Error), #[error("unknown current sandbox state '{state}'")] UnknownCurrentState { state: String }, #[error("illegal transition into quarantined: {0}")] @@ -307,7 +307,7 @@ fn insert_quarantine_history( /// episodes need an explicit owner outside automatic legacy recovery. Older /// binaries already exclude unknown owners from their recovery scans. pub(super) fn insert_operator_quarantine_episode( - conn: &rusqlite::Transaction<'_>, + conn: &Transaction<'_>, episode: &QuarantineEpisode<'_>, ) -> Result<()> { insert_new_quarantine_episode(conn, episode)?; @@ -554,6 +554,7 @@ pub fn current_quarantine_reason_wire_value(conn: &Connection, id: &str) -> Resu Ok(value.to_string()) }) .transpose() + .map_err(Error::from) } #[derive(Clone, Copy, Debug, Eq, PartialEq)] @@ -581,7 +582,7 @@ impl PauseFailedRecoveryTarget { #[derive(Debug, thiserror::Error)] pub(crate) enum PauseFailedRecoveryError { #[error("pause-failed recovery database operation failed: {0}")] - Database(#[from] rusqlite::Error), + Database(#[from] Error), #[error("pause-failed sandbox state CAS affected {changed} rows for {sandbox_id}")] SandboxStateInvariant { sandbox_id: SandboxId, @@ -761,7 +762,7 @@ pub fn sandbox_state_publication_metadata( } Ok(None) => {} Err(error) => { - publications.insert(sandbox_id.clone(), Err(error)); + publications.insert(sandbox_id.clone(), Err(error.into())); } } } @@ -819,7 +820,7 @@ fn mark_quarantined_inner( let starts_new_episode = previous_state.as_deref() != Some(SandboxState::Quarantined.as_str()); let ts = now()?; - let tx = conn.unchecked_transaction()?; + let tx = Transaction::deferred(conn)?; let changed = tx.execute( "UPDATE sandboxes SET state = 'quarantined', diff --git a/crates/libs/sbx/mgr/server/src/db/quarantine_history.rs b/crates/libs/sbx/mgr/server/src/db/quarantine_history.rs index 981555ad3..55b4fc7be 100644 --- a/crates/libs/sbx/mgr/server/src/db/quarantine_history.rs +++ b/crates/libs/sbx/mgr/server/src/db/quarantine_history.rs @@ -2,7 +2,8 @@ use pplx_space_common_core::types::SandboxId; -use rusqlite::{Connection, Result, params, types::Value}; +use crate::db::{Connection, Result}; +use rusqlite::{params, types::Value}; const ROWS_PER_BATCH: usize = 128; const RETENTION_SECS: u64 = 30 * 24 * 60 * 60; @@ -63,7 +64,7 @@ impl QuarantineHistoryPruner { row.get::<_, Value>(2)?, )) })? - .collect::>>()? + .collect::>>()? }; let mut report = HistoryPruneBatch { examined: rows.len(), diff --git a/crates/libs/sbx/mgr/server/src/db/quarantine_recovery.rs b/crates/libs/sbx/mgr/server/src/db/quarantine_recovery.rs index 3603da4e4..f5a650445 100644 --- a/crates/libs/sbx/mgr/server/src/db/quarantine_recovery.rs +++ b/crates/libs/sbx/mgr/server/src/db/quarantine_recovery.rs @@ -171,7 +171,7 @@ pub struct RawQuarantineRecoveryObservation { } pub type RawQuarantineRecoveryObservationRead = - std::result::Result; + std::result::Result; pub fn is_orphan_quarantine_recovery_state( conn: &Connection, @@ -319,7 +319,7 @@ pub fn quarantine_recovery_observations( } Ok(Some((_, None)) | None) => {} Err(error) => { - observations.insert(sandbox_id.clone(), Err(error)); + observations.insert(sandbox_id.clone(), Err(error.into())); } } } @@ -419,7 +419,7 @@ pub fn scan_quarantine_recovery_candidates(conn: &Connection) -> Result, current_epoch: i64, -) -> Result { +) -> rusqlite::Result { // Read beside the decoded columns rather than among them: `last_error` is // diagnostic text with no validation, so a value it cannot parse only // means no stop is recorded, never a malformed row. @@ -740,7 +740,7 @@ pub fn rearm_stopped_quarantine_recoveries(conn: &Connection) -> Result { )) }, )?; - rows.collect::>>()? + rows.collect::>>()? }; let updated_at = now_epoch_i64()?; let mut rearmed = 0; @@ -1108,8 +1108,8 @@ pub fn quarantine_recovery_claim_is_current( fence.quarantine_row_predicate("qs."), fence.sandbox_guards("qs.sandbox_id"), ); - conn.prepare_cached(&sql)? - .query_row(&fence.binds()[..], |row| row.get(0)) + let mut statement = conn.prepare_cached(&sql)?; + Ok(statement.query_row(&fence.binds()[..], |row| row.get(0))?) } fn quarantine_recovery_backoff(local_quarantine_id: uuid::Uuid, attempt_count: u32) -> Duration { diff --git a/crates/libs/sbx/mgr/server/src/db/quarantine_scan.rs b/crates/libs/sbx/mgr/server/src/db/quarantine_scan.rs index a1aefc418..c8f31959e 100644 --- a/crates/libs/sbx/mgr/server/src/db/quarantine_scan.rs +++ b/crates/libs/sbx/mgr/server/src/db/quarantine_scan.rs @@ -2,7 +2,8 @@ use std::time::{Duration, Instant}; -use rusqlite::{Connection, Result, params}; +use crate::db::{Connection, Result}; +use rusqlite::params; use super::{ QuarantineRecoveryCandidate, QuarantineRecoveryOwner, QuarantineRecoveryRow, diff --git a/crates/libs/sbx/mgr/server/src/db/restore_transfer_scopes.rs b/crates/libs/sbx/mgr/server/src/db/restore_transfer_scopes.rs index 9414e1445..366f6a2ad 100644 --- a/crates/libs/sbx/mgr/server/src/db/restore_transfer_scopes.rs +++ b/crates/libs/sbx/mgr/server/src/db/restore_transfer_scopes.rs @@ -82,10 +82,10 @@ pub fn rowless_restore_transfer_scopes( Ok(scope) => scopes.push(scope), Err(error) if is_row_decode_error(&error) => tracing::warn!( sandbox_id = ?row.get_ref(0)?, - error = %describe_error(&error), + error = %describe_raw_error(&error), "retaining malformed rowless restore scope; operator repair required" ), - Err(error) => return Err(error), + Err(error) => return Err(error.into()), } } Ok(scopes) diff --git a/crates/libs/sbx/mgr/server/src/db/route_activation_claims.rs b/crates/libs/sbx/mgr/server/src/db/route_activation_claims.rs index a220902b4..338458ffe 100644 --- a/crates/libs/sbx/mgr/server/src/db/route_activation_claims.rs +++ b/crates/libs/sbx/mgr/server/src/db/route_activation_claims.rs @@ -1,12 +1,13 @@ //! Persistence of open Kubernetes route activation claims //! (`sandbox_open_route_activations`). +use crate::db::Connection; use pplx_space_common_core::types::{ ExecutionGeneration, NodeId, NodeManagedBy, PodUid, RouteGeneration, SandboxActivationAttemptId, SpaceApiRouteIdentity, }; use pplx_space_common_svc::sql::sqlite::SqliteU64; -use rusqlite::{Connection, params}; +use rusqlite::params; use crate::auto_lifecycle_delete_receipts::RouteIdentityProvenance; @@ -55,7 +56,7 @@ fn write_claim( conn: &Connection, sandbox_id: &str, claim: &OpenRouteActivationClaim, -) -> rusqlite::Result<()> { +) -> crate::db::Result<()> { conn.execute( "INSERT INTO sandbox_open_route_activations ( sandbox_id, route_generation, node_id, execution_generation, @@ -88,7 +89,7 @@ pub(crate) fn insert_create_claim( conn: &Connection, sandbox_id: &str, claim: &OpenRouteActivationClaim, -) -> rusqlite::Result<()> { +) -> crate::db::Result<()> { write_claim(conn, sandbox_id, claim) } @@ -96,7 +97,7 @@ pub(crate) fn claim_matches( conn: &Connection, sandbox_id: &str, claim: &OpenRouteActivationClaim, -) -> rusqlite::Result { +) -> crate::db::Result { conn.query_row( "SELECT EXISTS ( SELECT 1 @@ -123,7 +124,7 @@ pub(crate) fn claim_matches( ) } -pub(crate) fn close_claim(conn: &Connection, sandbox_id: &str) -> rusqlite::Result { +pub(crate) fn close_claim(conn: &Connection, sandbox_id: &str) -> crate::db::Result { Ok(conn.execute( "DELETE FROM sandbox_open_route_activations WHERE sandbox_id = ?1", [sandbox_id], @@ -131,7 +132,7 @@ pub(crate) fn close_claim(conn: &Connection, sandbox_id: &str) -> rusqlite::Resu } #[cfg(test)] -pub(crate) fn claim_exists(conn: &Connection, sandbox_id: &str) -> rusqlite::Result { +pub(crate) fn claim_exists(conn: &Connection, sandbox_id: &str) -> crate::db::Result { conn.query_row( "SELECT EXISTS ( SELECT 1 FROM sandbox_open_route_activations WHERE sandbox_id = ?1 diff --git a/crates/libs/sbx/mgr/server/src/db/sandbox_row/tests.rs b/crates/libs/sbx/mgr/server/src/db/sandbox_row/tests.rs index 48b54a979..fb225a90c 100644 --- a/crates/libs/sbx/mgr/server/src/db/sandbox_row/tests.rs +++ b/crates/libs/sbx/mgr/server/src/db/sandbox_row/tests.rs @@ -1,4 +1,5 @@ use super::*; +use crate::db::Connection; use crate::db::{InsertSandboxParams, insert_sandbox, test_conn}; #[test] @@ -165,7 +166,7 @@ fn absent_or_malformed_counters_fail_closed_instead_of_decoding_zero() { /// Test-only projection builder. Production queries keep their own complete /// statements; this exists to drop exactly one column from an otherwise whole /// projection. -fn sandbox_projection(conn: &rusqlite::Connection, omitted: Option<&str>) -> String { +fn sandbox_projection(conn: &Connection, omitted: Option<&str>) -> String { let mut statement = conn .prepare("SELECT name FROM pragma_table_info('sandboxes')") .unwrap(); diff --git a/crates/libs/sbx/mgr/server/src/db/sandboxes.rs b/crates/libs/sbx/mgr/server/src/db/sandboxes.rs index 8538c7d4b..130d501f6 100644 --- a/crates/libs/sbx/mgr/server/src/db/sandboxes.rs +++ b/crates/libs/sbx/mgr/server/src/db/sandboxes.rs @@ -71,7 +71,7 @@ pub struct InsertSandboxParams<'a> { #[derive(Debug, thiserror::Error)] pub enum RestoreAdoptOrphanInsertError { #[error(transparent)] - Sqlite(#[from] rusqlite::Error), + Sqlite(#[from] Error), #[error("restore sandbox row state must be restoring, got {state}")] NonRestoringState { state: SandboxState }, #[error("existing sandbox row for id {id} is not a recoverable synthetic orphan")] @@ -81,7 +81,7 @@ pub enum RestoreAdoptOrphanInsertError { #[derive(Debug, thiserror::Error)] pub enum InsertRecoveredSuspendedSandboxError { #[error(transparent)] - Db(#[from] rusqlite::Error), + Db(#[from] Error), #[error("recovered suspended sandbox insert must use suspended state, got {state}")] NonSuspendedInsert { state: SandboxState }, #[error("recovered suspended sandbox artifact pin updated {changed} rows instead of one")] @@ -110,7 +110,7 @@ pub(crate) fn sandbox_lifecycle_counts(conn: &Connection) -> Result(1)?.0; Ok(SandboxLifecycleCount { state, count }) })?; - rows.collect() + Ok(rows.collect::>()?) } pub(crate) fn current_state_label(conn: &Connection, id: &str) -> Result> { @@ -275,7 +275,7 @@ pub(crate) fn node_execution_fence( previous.map(parse).transpose()?, ))), None if previous.is_none() => Ok(None), - None => Err(rusqlite::Error::InvalidQuery), + None => Err(Error::invalid_query()), } } @@ -288,14 +288,15 @@ pub(crate) fn unbound_node_executions(conn: &Connection) -> Result> [pplx_space_node_mgr_client::execution_fence::EXECUTION_PREFLIGHT_LIMIT as i64], |row| row.get(0), )? - .collect() + .collect::>() + .map_err(Error::from) } /// Import receiver authority only while inserting a previously absent orphan /// row in this transaction. Never use inspection to re-authorize a rejected /// cleanup of an existing row: the receiver may own a successor execution. pub(crate) fn inherit_orphan_node_execution( - tx: &rusqlite::Transaction<'_>, + tx: &Transaction<'_>, sandbox_id: &str, fence: pplx_space_node_mgr_client::execution_fence::ExecutionFence, ) -> Result<()> { @@ -310,7 +311,7 @@ pub(crate) fn inherit_orphan_node_execution( ], )?; if changed != 1 { - return Err(rusqlite::Error::QueryReturnedNoRows); + return Err(Error::no_rows()); } Ok(()) } @@ -324,7 +325,7 @@ pub(crate) fn acknowledge_node_cleanup( retained: Option, ) -> Result { if retained.is_some_and(|id| !expected.owns(id)) { - return Err(rusqlite::Error::InvalidQuery); + return Err(Error::invalid_query()); } Ok(conn.execute( "UPDATE sandboxes SET node_execution_id = ?4, previous_node_execution_id = NULL, @@ -365,11 +366,9 @@ pub(super) fn upsert_sandbox_execution_metadata_in_transaction( .parse::() .is_ok_and(|runtime| runtime == execution.runtime().kind); if !runtime_matches { - return Err(rusqlite::Error::ToSqlConversionFailure(Box::new( - std::io::Error::new( - std::io::ErrorKind::InvalidData, - "sandbox runtime column disagrees with execution metadata", - ), + return Err(Error::to_sql_conversion(std::io::Error::new( + std::io::ErrorKind::InvalidData, + "sandbox runtime column disagrees with execution metadata", ))); } @@ -472,19 +471,18 @@ pub(crate) fn restore_adopt_orphan_or_insert_sandbox_in_transaction( return Err(RestoreAdoptOrphanInsertError::NonRestoringState { state }); } let ts = now()?; - let process_env_json = serde_json::to_string(process_env) - .map_err(|error| rusqlite::Error::ToSqlConversionFailure(Box::new(error)))?; - let auto_lifecycle_json = serde_json::to_string(auto_lifecycle) - .map_err(|error| rusqlite::Error::ToSqlConversionFailure(Box::new(error)))?; + let process_env_json = serde_json::to_string(process_env).map_err(Error::to_sql_conversion)?; + let auto_lifecycle_json = + serde_json::to_string(auto_lifecycle).map_err(Error::to_sql_conversion)?; let auto_lifecycle_state = initial_auto_lifecycle_state(auto_lifecycle, state); - let auto_lifecycle_state_json = serde_json::to_string(&auto_lifecycle_state) - .map_err(|error| rusqlite::Error::ToSqlConversionFailure(Box::new(error)))?; + let auto_lifecycle_state_json = + serde_json::to_string(&auto_lifecycle_state).map_err(Error::to_sql_conversion)?; let network_policy_override_json = network_policy_override .map(serde_json::to_string) .transpose() - .map_err(|error| rusqlite::Error::ToSqlConversionFailure(Box::new(error)))?; + .map_err(Error::to_sql_conversion)?; let orphan_auto_lifecycle_json = serde_json::to_string(&AutoLifecyclePolicy::disabled()) - .map_err(|error| rusqlite::Error::ToSqlConversionFailure(Box::new(error)))?; + .map_err(Error::to_sql_conversion)?; let rows_affected = conn.execute( "INSERT INTO sandboxes (id, uuid8, state, template, template_hash, user, runtime, snapshots_enabled, rootfs_size_mb, pool_template, process_env_json, auto_lifecycle_json, auto_lifecycle_state_json, next_auto_event_at, network_policy_override_json, network_policy_generation, network_policy_authority, auto_lifecycle_authority, volume_encryption, created_at, updated_at, state_changed_at, restore_origin, restore_phase, restore_storage_uri, disk_io_max_operations_per_sec, disk_io_max_bandwidth_mib_per_sec, node_execution_id, node_execution_retired) @@ -682,7 +680,7 @@ pub fn sandbox_state_changed_at( let rows = statement.query_map(rusqlite::params_from_iter(sandbox_ids), |row| { Ok((row.get(0)?, timestamps::diagnostic_column(row, 1)?)) })?; - rows.collect() + Ok(rows.collect::>()?) } /// Read one row's `state_changed_at` exactly as stored. @@ -1024,9 +1022,7 @@ pub fn runtime_execution_generation( generation .map(ExecutionGeneration::new) .transpose() - .map_err(|error| { - rusqlite::Error::FromSqlConversionFailure(0, Type::Integer, Box::new(error)) - }) + .map_err(|error| Error::decode(0, Type::Integer, error)) } #[derive(Clone, Copy, Debug, Default, Eq, PartialEq)] @@ -1042,7 +1038,7 @@ pub struct ActivationRouteProvenance { fn activation_route_provenance_from_row( row: &rusqlite::Row<'_>, offset: usize, -) -> Result { +) -> rusqlite::Result { let values = ( row.get::<_, Option>(offset)? .map(|value| value.0), @@ -1145,10 +1141,10 @@ pub fn activation_route_provenance_for_sandboxes( let rows = statement.query_map(rusqlite::params_from_iter(sandbox_ids), |row| { Ok(( row.get::<_, String>(0)?, - activation_route_provenance_from_row(row, 1), + activation_route_provenance_from_row(row, 1).map_err(Error::from), )) })?; - rows.collect() + Ok(rows.collect::>()?) } pub fn clear_runtime_fields(conn: &Connection, id: &str) -> Result<()> { @@ -1185,7 +1181,7 @@ pub fn get_sandbox(conn: &Connection, id: &str) -> Result> { // SELECT each time dominated that work. let mut stmt = conn.prepare_cached(GET_SANDBOX_SQL)?; let mut rows = stmt.query_map(params![id], row_to_sandbox)?; - rows.next().transpose() + rows.next().transpose().map_err(Error::from) } /// Select at most two identities: two matches prove ambiguity without decoding @@ -1214,7 +1210,7 @@ const LIST_SANDBOXES_SQL: &str = pub fn list_sandboxes(conn: &Connection) -> Result> { let mut stmt = conn.prepare(LIST_SANDBOXES_SQL)?; let rows = stmt.query_map([], row_to_sandbox)?; - rows.collect() + Ok(rows.collect::>()?) } pub fn list_sandboxes_for_recovery(conn: &Connection) -> Result> { @@ -1226,10 +1222,10 @@ pub fn list_sandboxes_for_recovery(conn: &Connection) -> Result> Ok(row) => valid.push(row), Err(error) if is_row_decode_error(&error) => tracing::warn!( sandbox_id = ?row.get_ref(0)?, - error = %describe_error(&error), + error = %describe_raw_error(&error), "skipping undecodable sandbox row during local recovery" ), - Err(error) => return Err(error), + Err(error) => return Err(Error::from(error)), } } Ok(valid) @@ -1296,7 +1292,7 @@ pub(crate) fn periodic_lifecycle_recovery_batch( (Err(error), _) | (_, Err(error)) => { tracing::warn!( rowid, - error = %describe_error(&error), + error = %describe_raw_error(&error), "skipping undecodable sandbox key/state during periodic recovery" ); continue; @@ -1331,7 +1327,7 @@ pub(crate) fn periodic_lifecycle_recovery_batch( pub fn list_sandbox_id_states(conn: &Connection) -> Result> { let mut statement = conn.prepare("SELECT id, state FROM sandboxes ORDER BY created_at")?; let rows = statement.query_map([], |row| Ok((row.get(0)?, row.get(1)?)))?; - rows.collect() + Ok(rows.collect::>()?) } /// Host-control net-gateway auth lookup: rows whose registered @@ -1359,7 +1355,7 @@ pub fn list_by_netgtw_pid_uid( LIMIT 2", )?; let rows = stmt.query_map(params![netgtw_pid, uid], row_to_sandbox)?; - rows.collect() + Ok(rows.collect::>()?) } /// Host-control spaced-host auth lookup: rows whose registered @@ -1377,14 +1373,12 @@ pub fn list_by_spaced_host_pid_uid( )?; let sandbox_ids = stmt .query_map(params![spaced_host_pid, uid], |row| row.get::<_, String>(0))? - .collect::>>()?; + .collect::>>()?; drop(stmt); sandbox_ids .into_iter() - .map(|sandbox_id| { - get_sandbox(conn, &sandbox_id)?.ok_or(rusqlite::Error::QueryReturnedNoRows) - }) + .map(|sandbox_id| get_sandbox(conn, &sandbox_id)?.ok_or(Error::no_rows())) .collect() } @@ -1395,7 +1389,7 @@ pub fn list_by_state(conn: &Connection, state: SandboxState) -> Result>()?) } pub fn delete_sandbox(conn: &Connection, id: &str) -> Result<()> { @@ -1626,7 +1620,7 @@ pub fn update_netgtw_pid(conn: &Connection, id: &str, uid: u32, netgtw_pid: u32) params![uid, netgtw_pid, now()?, id], )?; if changed != 1 { - return Err(rusqlite::Error::QueryReturnedNoRows); + return Err(Error::no_rows()); } Ok(()) } @@ -1651,7 +1645,7 @@ pub fn replace_netgtw_pid( params![netgtw_pid, now()?, id, uid, expected_netgtw_pid], )?; if changed != 1 { - return Err(rusqlite::Error::QueryReturnedNoRows); + return Err(Error::no_rows()); } Ok(()) } @@ -1677,7 +1671,7 @@ pub fn update_spaced_host_pid( params![spaced_host_pid, now()?, id, uid, expected_spaced_host_pid], )?; if changed != 1 { - return Err(rusqlite::Error::QueryReturnedNoRows); + return Err(Error::no_rows()); } Ok(()) } diff --git a/crates/libs/sbx/mgr/server/src/db/snapshot_gc.rs b/crates/libs/sbx/mgr/server/src/db/snapshot_gc.rs index d4b25b8fd..be6ceefcc 100644 --- a/crates/libs/sbx/mgr/server/src/db/snapshot_gc.rs +++ b/crates/libs/sbx/mgr/server/src/db/snapshot_gc.rs @@ -1,8 +1,9 @@ //! Bounded, progressing artifact discovery; deletion authority stays with its lease. +use crate::db::{Connection, Result}; use pplx_space_common_core::inventory::{InventoryBudget, MAX_RAW_BYTES}; use pplx_space_common_core::sql; -use rusqlite::{Connection, Result, params}; +use rusqlite::params; use super::inventory_read::{InventoryReadError, check_row}; use super::timestamps::EpochSeconds; diff --git a/crates/libs/sbx/mgr/server/src/db/snapshot_gc/tests.rs b/crates/libs/sbx/mgr/server/src/db/snapshot_gc/tests.rs index f8a3063ab..3e9ee4839 100644 --- a/crates/libs/sbx/mgr/server/src/db/snapshot_gc/tests.rs +++ b/crates/libs/sbx/mgr/server/src/db/snapshot_gc/tests.rs @@ -77,7 +77,7 @@ fn pin_projection_uses_indexed_lookup_instead_of_scanning_sandboxes() { row.get::<_, String>(3) }) .unwrap() - .collect::>>() + .collect::>>() .unwrap(); assert!( plan.iter().any(|step| step diff --git a/crates/libs/sbx/mgr/server/src/db/snapshot_upload_jobs.rs b/crates/libs/sbx/mgr/server/src/db/snapshot_upload_jobs.rs index 7b4c8c33f..4588208fd 100644 --- a/crates/libs/sbx/mgr/server/src/db/snapshot_upload_jobs.rs +++ b/crates/libs/sbx/mgr/server/src/db/snapshot_upload_jobs.rs @@ -1,8 +1,9 @@ //! `jobs` rows of the snapshot-upload pipeline, and the backup markers its //! completed jobs prove. +use crate::db::{Connection, Result}; use pplx_space_common_core::types::JobId; -use rusqlite::{Connection, Result, params}; +use rusqlite::params; /// The `jobs.kind` of the snapshot-upload pipeline's jobs. pub const SNAPSHOT_UPLOAD_JOB_KIND: &str = "snapshot_upload"; @@ -58,7 +59,11 @@ pub(crate) fn visit_completed_upload_payloads( let mut rows = statement.query(params![after, through, limit])?; let now = crate::db::now()?; while let Some(row) = rows.next()? { - visit(&now, row.get(0)?, row.get::<_, String>(1))?; + visit( + &now, + row.get(0)?, + row.get::<_, String>(1).map_err(crate::db::Error::from), + )?; } Ok(()) } diff --git a/crates/libs/sbx/mgr/server/src/db/startup_recovery.rs b/crates/libs/sbx/mgr/server/src/db/startup_recovery.rs index 132c9682b..b7eaa4bfa 100644 --- a/crates/libs/sbx/mgr/server/src/db/startup_recovery.rs +++ b/crates/libs/sbx/mgr/server/src/db/startup_recovery.rs @@ -61,7 +61,7 @@ pub(crate) fn settle_startup_resident( | SandboxState::Resuming | SandboxState::Checkpointing ) { - return Err(rusqlite::Error::InvalidQuery); + return Err(Error::invalid_query()); } let (target, reason, error) = match &settlement { StartupResidentSettlement::Started => ( @@ -212,7 +212,7 @@ pub fn recover_interrupted_startup_states( if row.state.is_transient() { let episode = match cleanup_episode(conn, &row.id) { Ok(episode) => episode, - Err(error) if is_row_decode_error(&error) => { + Err(error) if is_row_decode_error(error.raw()) => { tracing::warn!(sandbox_id = %row.id, error = %describe_error(&error), "retaining malformed startup cleanup ownership; operator repair required"); recovered.push(StartupRecoveryRecord { id: row.id, @@ -366,7 +366,7 @@ pub fn recover_interrupted_startup_states( #[derive(Debug, thiserror::Error)] pub enum RecoverStartupStatesError { #[error("startup state recovery database operation failed: {0}")] - Database(#[from] rusqlite::Error), + Database(#[from] Error), #[error("failed to hand interrupted activation to cleanup: {0}")] CleanupRequest(#[from] RequestCleanupEpisodeError), #[error("armed cleanup episode has unsupported activation state {0}")] diff --git a/crates/libs/sbx/mgr/server/src/db/state_transitions.rs b/crates/libs/sbx/mgr/server/src/db/state_transitions.rs index 7ab22008a..4a9337799 100644 --- a/crates/libs/sbx/mgr/server/src/db/state_transitions.rs +++ b/crates/libs/sbx/mgr/server/src/db/state_transitions.rs @@ -20,7 +20,7 @@ impl ActivationKind { #[derive(Debug, thiserror::Error)] pub enum BeginActivationError { #[error(transparent)] - Db(#[from] rusqlite::Error), + Db(#[from] Error), #[error("sandbox state {state} cannot begin a {kind:?} activation")] UnsupportedState { kind: ActivationKind, @@ -31,7 +31,7 @@ pub enum BeginActivationError { #[derive(Debug, thiserror::Error)] pub enum RollbackActivationEpisodeError { #[error(transparent)] - Db(#[from] rusqlite::Error), + Db(#[from] Error), #[error("sandbox state {state} is not an activation state")] UnsupportedActivationState { state: SandboxState }, #[error("an activation episode cannot roll back to quarantined")] @@ -41,7 +41,7 @@ pub enum RollbackActivationEpisodeError { #[derive(Debug, thiserror::Error)] pub enum SupersedeActivationError { #[error(transparent)] - Db(#[from] rusqlite::Error), + Db(#[from] Error), #[error("sandbox quarantine recovery is already in progress")] QuarantineRecoveryInProgress, #[error(transparent)] @@ -54,7 +54,7 @@ pub(crate) fn restore_checkpoint_state( stable_state: SandboxState, ) -> Result { if !matches!(stable_state, SandboxState::Started | SandboxState::Paused) { - return Err(rusqlite::Error::InvalidQuery); + return Err(Error::invalid_query()); } let changed = conn.execute( "UPDATE sandboxes @@ -101,7 +101,7 @@ pub(crate) fn restore_resident_resume_state( stable_state: SandboxState, ) -> Result { if !matches!(stable_state, SandboxState::Started | SandboxState::Paused) { - return Err(rusqlite::Error::InvalidQuery); + return Err(Error::invalid_query()); } let tx = immediate_transaction(conn)?; let changed = rollback_activation_state(&tx, id, SandboxState::Resuming, stable_state)?; @@ -315,10 +315,7 @@ pub(crate) fn begin_activation_with_route_evidence( install_activation_auto_lifecycle(&tx, id, policy)?; } let sandbox_id = (route_admission.evidence.is_some() || execution_generation.is_some()) - .then(|| { - id.parse::() - .map_err(|error| rusqlite::Error::ToSqlConversionFailure(Box::new(error))) - }) + .then(|| id.parse::().map_err(Error::to_sql_conversion)) .transpose()?; match route_admission.open_claim { Some(open_route_activation) => { @@ -331,7 +328,7 @@ pub(crate) fn begin_activation_with_route_evidence( } } if let Some(route_evidence) = route_admission.evidence { - let sandbox_id = sandbox_id.ok_or(rusqlite::Error::InvalidQuery)?; + let sandbox_id = sandbox_id.ok_or_else(Error::invalid_query)?; auto_lifecycle_delete_receipts::record_retained_route_evidence( &tx, sandbox_id, @@ -426,7 +423,7 @@ pub fn mark_cross_node_restore_staging_failed( #[derive(Debug, thiserror::Error)] pub(crate) enum ActivationOperationError { #[error("activation receipt database operation failed: {0}")] - Database(#[from] rusqlite::Error), + Database(#[from] Error), #[error("sandbox no longer exists")] MissingSandbox, #[error("activation route evidence does not match the selected execution")] @@ -516,10 +513,9 @@ pub(crate) fn complete_resident_wake_operation( if previous.0 == Some(generation.get()) && let Some(previous_id) = previous.1 { - let previous_operation: SandboxActivationAttemptId = - previous_id.parse().map_err(|error| { - rusqlite::Error::FromSqlConversionFailure(1, Type::Text, Box::new(error)) - })?; + let previous_operation: SandboxActivationAttemptId = previous_id + .parse() + .map_err(|error| Error::decode(1, Type::Text, error))?; if previous_operation > operation { // Do not replace a potentially newer completion. UUID ordering is // only a conservative receipt-retention policy: PostgreSQL clears @@ -570,7 +566,7 @@ fn activation_completed_target(state: SandboxState) -> Option, + tx: &Transaction<'_>, sandbox_id: &str, cleanup_id: SandboxCleanupEpisodeId, target_state: SandboxState, @@ -747,7 +743,7 @@ pub fn mark_restore_succeeded( cleanup_id: Option, ) -> Result { if !matches!(final_state, SandboxState::Started | SandboxState::Paused) { - return Err(rusqlite::Error::InvalidQuery); + return Err(Error::invalid_query()); } let tx = immediate_transaction(conn)?; let changed = tx.execute( @@ -888,7 +884,7 @@ pub fn begin_teardown_suspend( params![now()?, id], )?; if changed != 1 { - return Err(rusqlite::Error::QueryReturnedNoRows); + return Err(Error::no_rows()); } tx.commit()?; emit_teardown_suspend_lease_claim(id, execution_generation, claim); @@ -1015,7 +1011,7 @@ fn rollback_activation_episode( }); } if restored_state == SandboxState::Failed { - return Err(rusqlite::Error::InvalidQuery.into()); + return Err(Error::invalid_query().into()); } if restored_state == SandboxState::Quarantined { return Err(RollbackActivationEpisodeError::QuarantinedTarget); @@ -1069,7 +1065,7 @@ fn rollback_activation_state( restored_state, SandboxState::Quarantined | SandboxState::Failed ) { - return Err(rusqlite::Error::InvalidQuery); + return Err(Error::invalid_query()); } let changed = conn.execute( "UPDATE sandboxes diff --git a/crates/libs/sbx/mgr/server/src/db/state_write.rs b/crates/libs/sbx/mgr/server/src/db/state_write.rs index c1384f73b..66e69792d 100644 --- a/crates/libs/sbx/mgr/server/src/db/state_write.rs +++ b/crates/libs/sbx/mgr/server/src/db/state_write.rs @@ -545,7 +545,7 @@ pub fn update_state_from( edge: &'static Edge, ) -> Result { if edge.from.is_empty() { - return Err(rusqlite::Error::InvalidQuery); + return Err(Error::invalid_query()); } write_state_from(conn, id, edge.from, edge.to) } @@ -576,7 +576,7 @@ pub(super) fn write_cleanup_admission_state( target: SandboxState, ) -> Result> { if !matches!(target, SandboxState::Stopping | SandboxState::Deleting) { - return Err(rusqlite::Error::InvalidQuery); + return Err(Error::invalid_query()); } match write_state_from(conn, id, StateSet::ALL, target)? { StateWriteOutcome::Applied { .. } => Ok(Ok(())), @@ -603,7 +603,7 @@ pub(super) fn write_cleanup_admission_state( StateWriteOutcome::Refused { refusal: StateWriteRefusal::FromState, .. - } => Err(rusqlite::Error::StatementChangedRows(0)), + } => Err(Error::from(rusqlite::Error::StatementChangedRows(0))), StateWriteOutcome::Missing => Ok(Err(SandboxStateWriteNotApplied::Missing)), } } @@ -615,7 +615,7 @@ fn write_state_from( to: SandboxState, ) -> Result { if matches!(to, SandboxState::Quarantined | SandboxState::Failed) { - return Err(rusqlite::Error::InvalidQuery); + return Err(Error::invalid_query()); } if conn.is_autocommit() { let tx = immediate_transaction(conn)?; @@ -688,7 +688,7 @@ fn write_state_from_in_transaction( // The row was read as `found` in this transaction, so only a source // set that excludes it can miss. if from.contains(found) { - return Err(rusqlite::Error::StatementChangedRows(0)); + return Err(Error::from(rusqlite::Error::StatementChangedRows(0))); } return Ok(StateWriteOutcome::Refused { found, diff --git a/crates/libs/sbx/mgr/server/src/db/teardown_jobs.rs b/crates/libs/sbx/mgr/server/src/db/teardown_jobs.rs index 35458d27c..0d25d16f4 100644 --- a/crates/libs/sbx/mgr/server/src/db/teardown_jobs.rs +++ b/crates/libs/sbx/mgr/server/src/db/teardown_jobs.rs @@ -6,11 +6,12 @@ //! active teardown jobs per tick, excluding unrelated and retained job history. use super::{active_teardown_suspend_operation, immediate_transaction}; +use crate::db::{Connection, Result}; use pplx_space_common_core::{ sql, types::{ExecutionGeneration, JobId, LocalArtifactId}, }; -use rusqlite::{Connection, Result, params}; +use rusqlite::params; #[derive(Debug, Clone, Copy, PartialEq, Eq)] pub(crate) enum TeardownSuspendFailure { @@ -115,7 +116,7 @@ fn fail_operation( super::mark_teardown_suspend_backup_permanently_failed(&tx, id, failed_artifact)?; } pplx_space_common_jobs::db::fail_job_permanent( - &tx, + tx.raw(), &operation.job_id, reason.as_str(), None, @@ -139,7 +140,7 @@ pub(super) fn fail_active( ) -> Result<()> { if let Some(operation) = active_teardown_suspend_operation(conn, id)? { pplx_space_common_jobs::db::fail_job_permanent( - conn, + conn.raw(), &operation.job_id, reason.as_str(), None, @@ -181,11 +182,11 @@ pub(crate) fn reconcile_terminal_teardown_suspend_jobs(conn: &Connection) -> Res .query_map(params![super::TEARDOWN_SUSPEND_JOB_KIND], |row| { row.get::<_, JobId>(0) })? - .collect::>>()?; + .collect::>>()?; drop(statement); for job in &jobs { pplx_space_common_jobs::db::fail_job_permanent( - &tx, + tx.raw(), job, TeardownSuspendFailure::Reconciled.as_str(), None, diff --git a/crates/libs/sbx/mgr/server/src/db/teardown_suspend.rs b/crates/libs/sbx/mgr/server/src/db/teardown_suspend.rs index 3dcddcdb7..1aa79d235 100644 --- a/crates/libs/sbx/mgr/server/src/db/teardown_suspend.rs +++ b/crates/libs/sbx/mgr/server/src/db/teardown_suspend.rs @@ -133,7 +133,7 @@ pub fn bind_suspended_teardown_operation( |row| row.get::<_, bool>(0), )?; if !has_suspended_artifact { - return Err(rusqlite::Error::QueryReturnedNoRows); + return Err(Error::no_rows()); } let claim = ensure_teardown_suspend_operation(&tx, id, execution_generation)?; mark_teardown_suspend_operation_running(&tx, id, execution_generation)?; @@ -149,29 +149,29 @@ pub(super) fn ensure_teardown_suspend_operation( ) -> Result { if runtime_execution_generation(conn, id)?.is_some_and(|current| current > execution_generation) { - return Err(rusqlite::Error::InvalidQuery); + return Err(Error::invalid_query()); } if latest_teardown_suspend_operation(conn, id)? .is_some_and(|latest| latest.execution_generation > execution_generation) { - return Err(rusqlite::Error::InvalidQuery); + return Err(Error::invalid_query()); } if latest_teardown_suspend_operation(conn, id)?.is_some_and(|latest| { latest.execution_generation == execution_generation && latest.status == TeardownSuspendOperationStatus::Failed }) && !teardown_suspend_retry_admissible(conn, id, execution_generation)? { - return Err(rusqlite::Error::QueryReturnedNoRows); + return Err(Error::no_rows()); } let mut superseded_generation = None; if let Some(active) = active_teardown_suspend_operation(conn, id)? { if active.execution_generation > execution_generation { - return Err(rusqlite::Error::InvalidQuery); + return Err(Error::invalid_query()); } if active.execution_generation < execution_generation { superseded_generation = Some(active.execution_generation); pplx_space_common_jobs::db::fail_job_permanent( - conn, + conn.raw(), &active.job_id, TEARDOWN_SUSPEND_SUPERSEDED_REASON, None, @@ -187,7 +187,7 @@ pub(super) fn ensure_teardown_suspend_operation( .map_err(|error| rusqlite::Error::ToSqlConversionFailure(Box::new(error)))?; let dedup_key = format!("{id}:{execution_generation}"); pplx_space_common_jobs::db::insert_job( - conn, + conn.raw(), pplx_space_common_jobs::db::InsertJob { job_id: &job_id, kind: TEARDOWN_SUSPEND_JOB_KIND, @@ -259,10 +259,10 @@ pub(super) fn mark_teardown_suspend_operation_running( let operation = active_teardown_suspend_operation(conn, id)?.ok_or(rusqlite::Error::QueryReturnedNoRows)?; if operation.execution_generation != execution_generation { - return Err(rusqlite::Error::InvalidQuery); + return Err(Error::invalid_query()); } if operation.status == TeardownSuspendOperationStatus::Pending { - pplx_space_common_jobs::db::mark_job_running(conn, &operation.job_id)?; + pplx_space_common_jobs::db::mark_job_running(conn.raw(), &operation.job_id)?; } Ok(()) } @@ -278,7 +278,7 @@ pub fn stage_teardown_suspend_artifact( let operation = active_teardown_suspend_operation(&tx, id)?.ok_or(rusqlite::Error::QueryReturnedNoRows)?; if operation.execution_generation != execution_generation { - return Err(rusqlite::Error::QueryReturnedNoRows); + return Err(Error::no_rows()); } upsert_suspend_local_artifact(&tx, id, local_artifact_id, local_path)?; let changed = tx.execute( @@ -293,7 +293,7 @@ pub fn stage_teardown_suspend_artifact( params![local_artifact_id.to_string(), now()?, id], )?; if changed != 1 { - return Err(rusqlite::Error::QueryReturnedNoRows); + return Err(Error::no_rows()); } tx.commit()?; Ok(()) @@ -307,14 +307,14 @@ pub(super) fn advance_teardown_suspend_lease( if runtime_execution_generation(conn, sandbox_id)? .is_some_and(|current| current > execution_generation) { - return Err(rusqlite::Error::InvalidQuery); + return Err(Error::invalid_query()); } if latest_teardown_suspend_operation(conn, sandbox_id)?.is_some_and(|latest| { latest.execution_generation > execution_generation || (latest.execution_generation == execution_generation && latest.status == TeardownSuspendOperationStatus::Completed) }) { - return Err(rusqlite::Error::InvalidQuery); + return Err(Error::invalid_query()); } let mut superseded_generation = None; @@ -324,7 +324,7 @@ pub(super) fn advance_teardown_suspend_lease( } superseded_generation = Some(active.execution_generation); pplx_space_common_jobs::db::fail_job_permanent( - conn, + conn.raw(), &active.job_id, TEARDOWN_SUSPEND_SUPERSEDED_REASON, None, @@ -355,21 +355,22 @@ pub(super) fn active_teardown_suspend_operation( row.get::<_, String>(2)?, )) })? - .collect::>>()?; + .collect::>>()?; if rows.len() > 1 { - return Err(rusqlite::Error::FromSqlConversionFailure( + return Err(Error::from(rusqlite::Error::FromSqlConversionFailure( 0, Type::Text, Box::new(std::io::Error::new( std::io::ErrorKind::InvalidData, format!("multiple active teardown suspend jobs for sandbox {sandbox_id}"), )), - )); + ))); } rows.into_iter() .next() .map(|(job_id, payload, status)| parse_teardown_suspend_operation(job_id, payload, status)) .transpose() + .map_err(Error::from) } pub(crate) fn latest_teardown_suspend_operation( @@ -393,7 +394,7 @@ fn parse_teardown_suspend_operation( job_id: JobId, payload: String, status: String, -) -> Result { +) -> rusqlite::Result { let payload: TeardownSuspendJobPayload = serde_json::from_str(&payload).map_err(|error| { rusqlite::Error::FromSqlConversionFailure(1, Type::Text, Box::new(error)) })?; @@ -422,7 +423,7 @@ pub(crate) fn teardown_release_evidence( sandbox_id: &str, execution_generation: ExecutionGeneration, ) -> Result { - let tx = conn.unchecked_transaction()?; + let tx = Transaction::deferred(conn)?; let runtime_generation = runtime_execution_generation(&tx, sandbox_id)?; let suspend = teardown_suspend_status(&tx, sandbox_id)?; let deleted = execution_delete_receipt_covers(&tx, sandbox_id, execution_generation)?; @@ -641,8 +642,8 @@ pub fn complete_teardown_suspend( )?; } mark_teardown_suspend_operation_running(&tx, sandbox_id, execution_generation)?; - if !pplx_space_common_jobs::db::complete_job(&tx, &operation.job_id, 0)? { - return Err(rusqlite::Error::QueryReturnedNoRows); + if !pplx_space_common_jobs::db::complete_job(tx.raw(), &operation.job_id, 0)? { + return Err(Error::no_rows()); } tx.commit()?; Ok(true) @@ -715,7 +716,7 @@ fn bind_teardown_suspend_artifact( params![payload, job_id], )? != 1 { - return Err(rusqlite::Error::QueryReturnedNoRows); + return Err(Error::no_rows()); } Ok(()) } diff --git a/crates/libs/sbx/mgr/server/src/db/tests.rs b/crates/libs/sbx/mgr/server/src/db/tests.rs index 5a7683b19..2ba24a0d4 100644 --- a/crates/libs/sbx/mgr/server/src/db/tests.rs +++ b/crates/libs/sbx/mgr/server/src/db/tests.rs @@ -145,7 +145,7 @@ fn persisted_clock_rejects_pre_epoch_instead_of_fabricating_zero() { for timestamp in ["1969-12-31T23:59:59Z", "1969-12-31T23:59:59.999999999Z"] { let error = persisted_epoch_seconds(timestamp.parse().unwrap()).unwrap_err(); assert!(matches!( - error, + error.raw(), rusqlite::Error::ToSqlConversionFailure(cause) if cause.is::() )); @@ -841,7 +841,7 @@ fn a_type_mismatched_column_makes_one_row_malformed_not_the_whole_scan() { fn quarantine_recovery_candidate_scan_propagates_statement_failures() { let directory = tempfile::tempdir().unwrap(); let database_path = directory.path().join("sandbox-manager.db"); - let mut conn = Connection::open(&database_path).unwrap(); + let mut conn = Connection::open_at(&database_path).unwrap(); create_test_schema(&mut conn); setup_quarantine_recovery_episode( &conn, @@ -850,13 +850,13 @@ fn quarantine_recovery_candidate_scan_propagates_statement_failures() { // A real SQLITE_BUSY, which is what a contended node-local database // produces. Reported as an empty or partial candidate list it makes a pass // that saw none of the backlog indistinguishable from an empty backlog. - let blocker = Connection::open(&database_path).unwrap(); + let blocker = Connection::open_at(&database_path).unwrap(); blocker.execute_batch("BEGIN EXCLUSIVE").unwrap(); conn.busy_timeout(std::time::Duration::ZERO).unwrap(); let error = scan_quarantine_recovery_candidates(&conn) .expect_err("a statement-level SQLite failure must abort the pass"); - let rusqlite::Error::SqliteFailure(failure, _) = &error else { + let rusqlite::Error::SqliteFailure(failure, _) = error.raw() else { panic!("expected the underlying SQLite failure, got {error:?}"); }; assert_eq!(failure.code, rusqlite::ErrorCode::DatabaseBusy); @@ -2203,7 +2203,7 @@ fn runtime_attachment_rejects_a_missing_sandbox() { ) .expect_err("runtime ownership cannot attach to a missing sandbox"); - assert!(matches!(error, rusqlite::Error::QueryReturnedNoRows)); + assert!(error.is_no_rows()); } #[test] @@ -2228,7 +2228,7 @@ fn terminal_teardown_generation_rejects_stale_runtime_attachment() { runtime_fields(ExecutionGeneration::new(1).unwrap()), ) .expect_err("a terminal newer lease is a durable generation high-water mark"); - assert!(matches!(error, rusqlite::Error::InvalidQuery)); + assert!(error.is_invalid_query()); let latest = latest_teardown_suspend_operation(&conn, &sandbox_id_string) .unwrap() @@ -2253,7 +2253,7 @@ fn failed_teardown_generation_allows_same_runtime_reattachment() { .unwrap() .expect("the teardown operation must exist"); pplx_space_common_jobs::db::fail_job_permanent( - &conn, + conn.raw(), &operation.job_id, "injected teardown failure", None, @@ -6742,7 +6742,7 @@ fn start_arms_access_idle_only_after_guest_readiness() { fn legacy_equal_route_and_operation_receipt_survives_restart_without_split_field() { let dir = tempfile::tempdir().unwrap(); let path = dir.path().join("manager.sqlite"); - let mut conn = Connection::open(&path).unwrap(); + let mut conn = Connection::open_at(&path).unwrap(); create_test_schema(&mut conn); let sandbox_id = SandboxId::new().to_string(); insert_test_sandbox( @@ -6765,7 +6765,7 @@ fn legacy_equal_route_and_operation_receipt_survives_restart_without_split_field assert_eq!(episode.activation_attempt_id, Some(route_and_operation)); drop(conn); - let conn = Connection::open(&path).unwrap(); + let conn = Connection::open_at(&path).unwrap(); assert!(mark_start_succeeded(&conn, &sandbox_id, Some(episode.id)).unwrap()); let metadata = sandbox_state_publication_metadata(&conn, std::slice::from_ref(&sandbox_id)) .unwrap() @@ -6788,7 +6788,7 @@ fn split_operation_receipts_survive_restart_and_preserve_route_ownership() { for (joined, rollback) in [(false, false), (true, false), (false, true), (true, true)] { let dir = tempfile::tempdir().unwrap(); let path = dir.path().join("manager.sqlite"); - let mut conn = Connection::open(&path).unwrap(); + let mut conn = Connection::open_at(&path).unwrap(); create_test_schema(&mut conn); let id = uuid::Uuid::now_v7().to_string(); insert_test_sandbox( @@ -6833,7 +6833,7 @@ fn split_operation_receipts_survive_restart_and_preserve_route_ownership() { )); } drop(conn); // No request future or in-memory owner survives this restart. - let conn = Connection::open(&path).unwrap(); + let conn = Connection::open_at(&path).unwrap(); let stored = cleanup_episode(&conn, &id).unwrap().unwrap(); assert_eq!(stored.id, episode.id); assert_eq!(stored.activation_attempt_id, Some(route_attempt)); @@ -6915,7 +6915,7 @@ fn generic_lifecycle_writers_reject_failed_targets() { ); assert!(matches!( rollback_starting(&conn, "starting", SandboxState::Failed), - Err(rusqlite::Error::InvalidQuery) + Err(error) if error.is_invalid_query() )); assert_eq!( get_sandbox(&conn, "starting").unwrap().unwrap().state, @@ -7290,7 +7290,7 @@ fn update_netgtw_pid_errors_when_sandbox_row_is_missing() { let error = update_netgtw_pid(&conn, "missing-sandbox", 10_042, 4_242) .expect_err("registration write for a deleted sandbox must error, not no-op"); - assert!(matches!(error, rusqlite::Error::QueryReturnedNoRows)); + assert!(error.is_no_rows()); } #[test] @@ -7323,7 +7323,7 @@ fn replace_netgtw_pid_rejects_stale_restart_authority() { .expect("a stale observer may confirm the already-committed replacement"); let error = replace_netgtw_pid(&conn, "sandbox-live", 10_042, Some(4_242), 4_244) .expect_err("stale restart must not overwrite the committed replacement"); - assert!(matches!(error, rusqlite::Error::QueryReturnedNoRows)); + assert!(error.is_no_rows()); assert_eq!( get_sandbox(&conn, "sandbox-live") .unwrap() @@ -7362,7 +7362,7 @@ fn update_spaced_host_pid_is_guarded_by_persisted_owner() { let error = update_spaced_host_pid(&conn, "sandbox-live", 10_043, Some(4_243), 4_244) .expect_err("stale owner must not replace endpoint authority"); - assert!(matches!(error, rusqlite::Error::QueryReturnedNoRows)); + assert!(error.is_no_rows()); let row = get_sandbox(&conn, "sandbox-live").unwrap().unwrap(); assert_eq!(row.spaced_host_pid, Some(4_243)); } @@ -7387,7 +7387,7 @@ fn update_spaced_host_pid_rejects_stale_restart_for_same_owner() { .unwrap(); let error = update_spaced_host_pid(&conn, "sandbox-live", 10_042, Some(4_242), 4_243) .expect_err("stale restart must not overwrite a newer activation for the same uid"); - assert!(matches!(error, rusqlite::Error::QueryReturnedNoRows)); + assert!(error.is_no_rows()); let row = get_sandbox(&conn, "sandbox-live").unwrap().unwrap(); assert_eq!(row.spaced_host_pid, Some(5_000)); } @@ -7505,7 +7505,7 @@ fn startup_resident_settlement_keeps_only_current_receipts_atomically() { |row| { (0..6) .map(|column| row.get::<_, rusqlite::types::Value>(column)) - .collect::>>() + .collect::>>() }, ) .unwrap() @@ -10001,7 +10001,7 @@ fn teardown_suspend_reconciliation_skips_healthy_and_retained_jobs_and_bounds_cl let generation = ExecutionGeneration::new(7).unwrap(); let unrelated_job = JobId::new(); pplx_space_common_jobs::db::insert_job( - &conn, + conn.raw(), pplx_space_common_jobs::db::InsertJob { job_id: &unrelated_job, kind: "synthetic_other_job", @@ -10040,7 +10040,7 @@ fn teardown_suspend_reconciliation_skips_healthy_and_retained_jobs_and_bounds_cl .unwrap() .unwrap(); pplx_space_common_jobs::db::fail_job_permanent( - &conn, + conn.raw(), &retained.job_id, "synthetic retained failure", None, @@ -10058,7 +10058,7 @@ fn teardown_suspend_reconciliation_skips_healthy_and_retained_jobs_and_bounds_cl assert_eq!(reconcile_terminal_teardown_suspend_jobs(&conn).unwrap(), 5); assert_eq!(reconcile_terminal_teardown_suspend_jobs(&conn).unwrap(), 0); assert_eq!( - pplx_space_common_jobs::db::get_job(&conn, &retained.job_id) + pplx_space_common_jobs::db::get_job(conn.raw(), &retained.job_id) .unwrap() .unwrap() .error @@ -10066,7 +10066,7 @@ fn teardown_suspend_reconciliation_skips_healthy_and_retained_jobs_and_bounds_cl Some("synthetic retained failure") ); assert!( - !pplx_space_common_jobs::db::get_job(&conn, &unrelated_job) + !pplx_space_common_jobs::db::get_job(conn.raw(), &unrelated_job) .unwrap() .unwrap() .is_terminal() @@ -10082,7 +10082,7 @@ fn teardown_suspend_reconciliation_skips_healthy_and_retained_jobs_and_bounds_cl let operation = latest_teardown_suspend_operation(&conn, &format!("sandbox-{index}")) .unwrap() .unwrap(); - let job = pplx_space_common_jobs::db::get_job(&conn, &operation.job_id) + let job = pplx_space_common_jobs::db::get_job(conn.raw(), &operation.job_id) .unwrap() .unwrap(); assert_eq!( diff --git a/crates/libs/sbx/mgr/server/src/db/tests/accepted_shapes_cleanup_episodes.rs b/crates/libs/sbx/mgr/server/src/db/tests/accepted_shapes_cleanup_episodes.rs index 6f3dddfd6..4e965e663 100644 --- a/crates/libs/sbx/mgr/server/src/db/tests/accepted_shapes_cleanup_episodes.rs +++ b/crates/libs/sbx/mgr/server/src/db/tests/accepted_shapes_cleanup_episodes.rs @@ -9,11 +9,12 @@ //! column-identical. Auto-lifecycle is disabled on every seeded row, so //! arming after activation writes nothing here; other tests cover arming. +use crate::db::Connection; use pplx_space_common_core::types::{ LocalArtifactId, SandboxActivationAttemptId, SandboxCleanupEpisodeId, SandboxQuarantineReasonCode, }; -use rusqlite::{Connection, params}; +use rusqlite::params; use super::state_write_fixtures::{ Cell, ExpectedDiff, NULL, apply_edge, assert_accepts, backdate, decorate, generation, diff --git a/crates/libs/sbx/mgr/server/src/db/tests/accepted_shapes_quarantine.rs b/crates/libs/sbx/mgr/server/src/db/tests/accepted_shapes_quarantine.rs index 43eae0b93..80a1e7e91 100644 --- a/crates/libs/sbx/mgr/server/src/db/tests/accepted_shapes_quarantine.rs +++ b/crates/libs/sbx/mgr/server/src/db/tests/accepted_shapes_quarantine.rs @@ -8,12 +8,13 @@ //! shared helper must stay column-identical. `thin_writer_coverage` covers the //! adoption statement itself. +use crate::db::Connection; use pplx_space_common_core::types::{ ArtifactKind, ArtifactOrigin, LocalArtifactId, SandboxCleanupEpisodeId, SandboxId, SandboxQuarantineNextAction, SandboxQuarantineReasonCode, SandboxQuarantineSource, TransferScopeId, }; -use rusqlite::{Connection, params}; +use rusqlite::params; use super::state_write_fixtures::{ Cell, ExpectedDiff, NULL, as_synthetic_orphan, assert_accepts, backdate, decorate, diff --git a/crates/libs/sbx/mgr/server/src/db/tests/accepted_shapes_single_statement.rs b/crates/libs/sbx/mgr/server/src/db/tests/accepted_shapes_single_statement.rs index e4478a5e4..36b6b2e87 100644 --- a/crates/libs/sbx/mgr/server/src/db/tests/accepted_shapes_single_statement.rs +++ b/crates/libs/sbx/mgr/server/src/db/tests/accepted_shapes_single_statement.rs @@ -10,11 +10,11 @@ //! writes nothing here; `started_transition_arms_disarmed_lifecycle_for_rollback_writers` //! and `start_arms_access_idle_only_after_guest_readiness` cover arming. +use crate::db::Connection; use pplx_space_common_core::types::{ ArtifactKind, ArtifactOrigin, LocalArtifactId, SandboxId, SandboxQuarantineNextAction, SandboxQuarantineReasonCode, SandboxQuarantineSource, TransferScopeId, }; -use rusqlite::Connection; use super::state_write_fixtures::{ Cell, ExpectedDiff, NULL, assert_accepts, backdate, decorate, generation, insert_row, diff --git a/crates/libs/sbx/mgr/server/src/db/tests/cleanup_quarantine_refusal.rs b/crates/libs/sbx/mgr/server/src/db/tests/cleanup_quarantine_refusal.rs index 06dbf7bf5..cc42937bd 100644 --- a/crates/libs/sbx/mgr/server/src/db/tests/cleanup_quarantine_refusal.rs +++ b/crates/libs/sbx/mgr/server/src/db/tests/cleanup_quarantine_refusal.rs @@ -851,7 +851,10 @@ fn cleanup_episode_decoder_rejects_rows_it_cannot_represent() { let result = cleanup_episode(&conn, "sandbox"); assert!( - matches!(result, Err(rusqlite::Error::FromSqlConversionFailure(..))), + matches!( + result.as_ref().map_err(Error::raw), + Err(rusqlite::Error::FromSqlConversionFailure(..)) + ), "{case}: {result:?}" ); } diff --git a/crates/libs/sbx/mgr/server/src/db/tests/local_network_policy_clear.rs b/crates/libs/sbx/mgr/server/src/db/tests/local_network_policy_clear.rs index 7e96a6064..dbb67fe85 100644 --- a/crates/libs/sbx/mgr/server/src/db/tests/local_network_policy_clear.rs +++ b/crates/libs/sbx/mgr/server/src/db/tests/local_network_policy_clear.rs @@ -139,6 +139,6 @@ fn clearing_a_local_network_policy_hands_authority_back_to_the_cluster() { let conn = test_conn(); assert!(matches!( clear_local_network_policy(&conn, "missing", true, true), - Err(rusqlite::Error::QueryReturnedNoRows) + Err(ref error) if error.is_no_rows() )); } diff --git a/crates/libs/sbx/mgr/server/src/db/tests/state_write_fixtures.rs b/crates/libs/sbx/mgr/server/src/db/tests/state_write_fixtures.rs index db91698a2..53df9dc7d 100644 --- a/crates/libs/sbx/mgr/server/src/db/tests/state_write_fixtures.rs +++ b/crates/libs/sbx/mgr/server/src/db/tests/state_write_fixtures.rs @@ -8,13 +8,14 @@ use std::collections::BTreeMap; +use crate::db::Connection; use pplx_space_common_core::types::{ Arch, AutoLifecyclePolicy, ExecutionGeneration, LinuxPid, LinuxUid, LocalArtifactId, RuntimeKind, SandboxActivationAttemptId, SandboxCleanupEpisodeId, SandboxId, SandboxOwner, SandboxQuarantineReasonCode, SandboxRuntimeLabel, VsockCid, }; +use rusqlite::params; use rusqlite::types::Value; -use rusqlite::{Connection, params}; use super::{TestSandboxInsert, insert_test_sandbox}; use crate::db::{ diff --git a/crates/libs/sbx/mgr/server/src/db/tests/state_writer_refusal.rs b/crates/libs/sbx/mgr/server/src/db/tests/state_writer_refusal.rs index 3cfb95024..47ce9aeef 100644 --- a/crates/libs/sbx/mgr/server/src/db/tests/state_writer_refusal.rs +++ b/crates/libs/sbx/mgr/server/src/db/tests/state_writer_refusal.rs @@ -10,12 +10,13 @@ use std::collections::BTreeMap; use super::database_rows; +use crate::db::Connection; use pplx_space_common_core::types::{ ArtifactKind, ArtifactOrigin, AutoLifecyclePolicy, ExactStorageUri, ExecutionGeneration, LocalArtifactId, SandboxCleanupEpisodeId, SandboxId, SandboxOwner, SandboxQuarantineNextAction, SandboxQuarantineReasonCode, SandboxQuarantineSource, SandboxRuntimeLabel, TransferScopeId, }; -use rusqlite::{Connection, params}; +use rusqlite::params; use crate::db::{ ActivationKind, CleanupCompletion, CleanupEpisode, CleanupEpisodeRequestOutcome, @@ -50,9 +51,9 @@ fn assert_unchanged(conn: &Connection, before: &BTreeMap>, c ); } -fn assert_no_rows(result: rusqlite::Result<()>, case: &str) { +fn assert_no_rows(result: crate::db::Result<()>, case: &str) { assert!( - matches!(result, Err(rusqlite::Error::QueryReturnedNoRows)), + matches!(result, Err(ref error) if error.is_no_rows()), "{case}: expected the guarded write to refuse with QueryReturnedNoRows, got {result:?}" ); } @@ -585,9 +586,7 @@ fn activation_episode_rollback_requires_the_activation_state_and_its_armed_episo )); assert!(matches!( rollback_starting_episode(&conn, &starting, SandboxState::Failed, start_episode.id), - Err(RollbackActivationEpisodeError::Db( - rusqlite::Error::InvalidQuery - )) + Err(RollbackActivationEpisodeError::Db(ref error)) if error.is_invalid_query() )); assert_unchanged(&conn, &before, "rejected rollback target"); @@ -658,7 +657,7 @@ fn resident_resume_rollback_settles_only_a_resuming_row() { assert!( matches!( restore_resident_resume_state(&conn, &resuming, target), - Err(rusqlite::Error::InvalidQuery) + Err(error) if error.is_invalid_query() ), "{target}" ); @@ -734,7 +733,7 @@ impl RestoredSuspendedPublication { conn: &Connection, id: &str, artifact: LocalArtifactId, - ) -> rusqlite::Result<()> { + ) -> crate::db::Result<()> { match self { Self::WithArtifact => { publish_restored_suspended_artifact(conn, id, artifact, &snapshot_path(id)) diff --git a/crates/libs/sbx/mgr/server/src/db/tests/stopped_artifact_pin.rs b/crates/libs/sbx/mgr/server/src/db/tests/stopped_artifact_pin.rs index a60b81377..8687b695c 100644 --- a/crates/libs/sbx/mgr/server/src/db/tests/stopped_artifact_pin.rs +++ b/crates/libs/sbx/mgr/server/src/db/tests/stopped_artifact_pin.rs @@ -9,17 +9,16 @@ use pplx_space_common_core::types::{ ArtifactKind, ArtifactOrigin, LocalArtifactId, SandboxQuarantineReasonCode, }; -use rusqlite::Connection; use super::state_write_fixtures::{ Cell, ExpectedDiff, admit_stop, assert_accepts, backdate, begin_cold_start, complete_stop, force_state, insert_row, insert_suspended_with_artifact, seed_runtime, text, unpinned, }; use crate::db::{ - StateWriteOutcome, complete_cleanup_episode, drop_stopped_artifact_pin, get_local_artifact, - get_sandbox, mark_restore_succeeded, publish_stopped_artifact, request_cleanup_episode, - restage_for_warm_start, rollback_starting_episode, stop_suspended_for_auto_lifecycle, - test_conn, + Connection, StateWriteOutcome, complete_cleanup_episode, drop_stopped_artifact_pin, + get_local_artifact, get_sandbox, mark_restore_succeeded, publish_stopped_artifact, + request_cleanup_episode, restage_for_warm_start, rollback_starting_episode, + stop_suspended_for_auto_lifecycle, test_conn, }; use crate::state::SandboxState; diff --git a/crates/libs/sbx/mgr/server/src/db/tests/thin_writer_coverage.rs b/crates/libs/sbx/mgr/server/src/db/tests/thin_writer_coverage.rs index affa14506..27f002ba4 100644 --- a/crates/libs/sbx/mgr/server/src/db/tests/thin_writer_coverage.rs +++ b/crates/libs/sbx/mgr/server/src/db/tests/thin_writer_coverage.rs @@ -2,11 +2,12 @@ //! thin coverage: each one's accepting case and each refusing predicate in its //! own case, every refusal starting from a row the write would otherwise change. +use crate::db::Connection; use pplx_space_common_core::types::{ LocalArtifactId, RuntimeKind, SandboxCleanupEpisodeId, SandboxId, SandboxQuarantineNextAction, SandboxQuarantineReasonCode, SandboxQuarantineSource, TransferScopeId, }; -use rusqlite::{Connection, params}; +use rusqlite::params; use super::accepted_shapes_quarantine::{adoptable, insert_claimed_adoptable}; use super::state_write_fixtures::{ @@ -68,7 +69,7 @@ fn checkpoint_rollback_requires_a_checkpointing_row_and_a_resident_target() { &case, &conn, || restore_checkpoint_state(&conn, &id, target), - |result| matches!(result, Err(rusqlite::Error::InvalidQuery)), + |result| matches!(result, Err(error) if error.is_invalid_query()), ); } } @@ -388,7 +389,7 @@ fn startup_resident_settlement_refuses_any_change_to_the_observed_row() { &format!("observed {state}"), &conn, || settle_startup_resident(&conn, &observed, StartupResidentSettlement::Started), - |result| matches!(result, Err(rusqlite::Error::InvalidQuery)), + |result| matches!(result, Err(error) if error.is_invalid_query()), ); } diff --git a/crates/libs/sbx/mgr/server/src/db/tests/update_state_from.rs b/crates/libs/sbx/mgr/server/src/db/tests/update_state_from.rs index 1513fea2a..89298e3a4 100644 --- a/crates/libs/sbx/mgr/server/src/db/tests/update_state_from.rs +++ b/crates/libs/sbx/mgr/server/src/db/tests/update_state_from.rs @@ -96,7 +96,7 @@ fn a_from_set_without_the_row_state_refuses_and_keeps_every_row() { assert_unchanged(&case, &before, &conn); } } - (None, Err(rusqlite::Error::InvalidQuery)) => { + (None, Err(error)) if error.is_invalid_query() => { assert_unchanged(&case, &before, &conn); } (expected, result) => { @@ -158,7 +158,7 @@ fn insert_and_rejected_target_edges_are_refused() { ); let before = snapshot(&conn); match update_state_from(&conn, &id, edge) { - Err(rusqlite::Error::InvalidQuery) => assert_unchanged(&case, &before, &conn), + Err(error) if error.is_invalid_query() => assert_unchanged(&case, &before, &conn), other => panic!("{case}: expected InvalidQuery, got {other:?}"), } } @@ -182,7 +182,8 @@ fn a_missed_write_from_a_source_state_is_an_invariant_error() { ) .unwrap(); let before = snapshot(&conn); - match update_state_from(&conn, &id, &edges::PAUSE_ROLLBACK) { + match update_state_from(&conn, &id, &edges::PAUSE_ROLLBACK).map_err(crate::db::Error::into_raw) + { Err(rusqlite::Error::StatementChangedRows(0)) => {} other => panic!("expected an invariant error, got {other:?}"), } @@ -207,7 +208,9 @@ fn a_standalone_deletion_rolls_back_when_its_job_failure_does() { .unwrap(); let before = snapshot(&conn); assert!(conn.is_autocommit()); - match update_state_from(&conn, &id, &edges::DELETE_ADMISSION) { + match update_state_from(&conn, &id, &edges::DELETE_ADMISSION) + .map_err(crate::db::Error::into_raw) + { Err(rusqlite::Error::SqliteFailure(_, Some(message))) => { assert!(message.contains("job update refused"), "{message}"); } diff --git a/crates/libs/sbx/mgr/server/src/db/tests/update_state_matrix.rs b/crates/libs/sbx/mgr/server/src/db/tests/update_state_matrix.rs index 3be1b01ab..a4c8d47d6 100644 --- a/crates/libs/sbx/mgr/server/src/db/tests/update_state_matrix.rs +++ b/crates/libs/sbx/mgr/server/src/db/tests/update_state_matrix.rs @@ -9,10 +9,10 @@ //! derived from `SandboxState::can_transition_to`: the grids record which //! writes the guards admit, including edges the state machine forbids. +use crate::db::{Connection, Transaction}; use pplx_space_common_core::types::{ SandboxCleanupEpisodeId, SandboxId, SandboxQuarantineReasonCode, }; -use rusqlite::Connection; use super::state_write_fixtures::{ Cell, ExpectedDiff, NULL, Snapshot, assert_diff, assert_unchanged, backdate, force_state, @@ -378,7 +378,7 @@ impl Writer { refusal: StateWriteRefusal::FromState, }) => panic!("{case}: every state is expected, yet {found} was refused"), Ok(StateWriteOutcome::Missing) => (None, Expected::Missing), - Err(rusqlite::Error::InvalidQuery) => return Expected::Rejected, + Err(error) if error.is_invalid_query() => return Expected::Rejected, Err(error) => panic!("{case}: {error}"), }; assert_eq!(observed, seeded, "{case}: the outcome names the seeded row"); @@ -394,11 +394,11 @@ pub(super) fn write_from( from: StateSet, to: SandboxState, in_transaction: bool, -) -> rusqlite::Result { +) -> crate::db::Result { if !in_transaction { return update_state_from_states(conn, id, from, to); } - let tx = conn.unchecked_transaction()?; + let tx = Transaction::deferred(conn)?; let outcome = update_state_from_states(&tx, id, from, to)?; tx.commit()?; Ok(outcome) diff --git a/crates/libs/sbx/mgr/server/src/db/timestamps.rs b/crates/libs/sbx/mgr/server/src/db/timestamps.rs index 050e5c6b2..72b1e4020 100644 --- a/crates/libs/sbx/mgr/server/src/db/timestamps.rs +++ b/crates/libs/sbx/mgr/server/src/db/timestamps.rs @@ -3,8 +3,9 @@ use std::fmt; +use crate::db::{Connection, Result}; +use rusqlite::params; use rusqlite::types::{FromSql, FromSqlError, FromSqlResult, Value, ValueRef}; -use rusqlite::{Connection, Result, params}; #[derive(Clone, Copy, Debug, Eq, PartialEq)] pub(crate) struct EpochSeconds(i64); @@ -31,7 +32,7 @@ impl EpochSeconds { .ok_or(InvalidTimestamp) } - pub(crate) fn decode(value: &Value) -> FromSqlResult { + pub(in crate::db) fn decode(value: &Value) -> FromSqlResult { Self::column_result(ValueRef::from(value)) } @@ -65,7 +66,7 @@ impl fmt::Display for EpochSeconds { /// Diagnostic paths preserve corrupt text for degraded reporting instead of /// making a malformed timestamp hide the sandbox's raw lifecycle state. -pub(crate) fn diagnostic_text(value: Value) -> Option { +pub(in crate::db) fn diagnostic_text(value: Value) -> Option { match EpochSeconds::decode(&value) { Ok(value) => Some(value.to_string()), Err(_) => match value { @@ -76,7 +77,7 @@ pub(crate) fn diagnostic_text(value: Value) -> Option { } } -pub(super) fn diagnostic_column(row: &rusqlite::Row<'_>, index: usize) -> Result { +pub(super) fn diagnostic_column(row: &rusqlite::Row<'_>, index: usize) -> rusqlite::Result { let value: Value = row.get(index)?; let data_type = value.data_type(); diagnostic_text(value).ok_or_else(|| { @@ -132,7 +133,7 @@ impl RestoreScopeTimestampScan { .query_map(params![self.next, through], |row| { Ok((row.get::<_, i64>(0)?, row.get::<_, Value>(1)?)) })? - .collect::>>()? + .collect::>>()? }; let mut changed = 0; for (rowid, raw) in &rows { diff --git a/crates/libs/sbx/mgr/server/src/db/transfer.rs b/crates/libs/sbx/mgr/server/src/db/transfer.rs index f5a686357..11d498019 100644 --- a/crates/libs/sbx/mgr/server/src/db/transfer.rs +++ b/crates/libs/sbx/mgr/server/src/db/transfer.rs @@ -11,7 +11,7 @@ use super::*; #[derive(Debug, thiserror::Error)] pub(crate) enum ReserveImportError { #[error("reserve sandbox import: {0}")] - Database(#[from] rusqlite::Error), + Database(#[from] Error), #[error("claim sandbox cache: {0}")] Claim(#[from] std::io::Error), } diff --git a/crates/libs/sbx/mgr/server/src/lifecycle_metrics.rs b/crates/libs/sbx/mgr/server/src/lifecycle_metrics.rs index 5890d3814..d4187b10a 100644 --- a/crates/libs/sbx/mgr/server/src/lifecycle_metrics.rs +++ b/crates/libs/sbx/mgr/server/src/lifecycle_metrics.rs @@ -1,8 +1,6 @@ use std::collections::BTreeSet; -use std::sync::{Arc, Mutex}; use std::time::Duration; -use rusqlite::Connection; use tokio_util::sync::CancellationToken; use crate::db::SandboxLifecycleCount; @@ -49,7 +47,7 @@ impl LifecycleGaugeSampler { } } -pub(crate) async fn run(db: Arc>, shutdown_token: CancellationToken) { +pub(crate) async fn run(db: crate::db::SharedConnection, shutdown_token: CancellationToken) { let mut sampler = LifecycleGaugeSampler::default(); let mut interval = tokio::time::interval(SAMPLE_INTERVAL); interval.set_missed_tick_behavior(tokio::time::MissedTickBehavior::Delay); diff --git a/crates/libs/sbx/mgr/server/src/local_artifacts.rs b/crates/libs/sbx/mgr/server/src/local_artifacts.rs index db5970302..c520d1cf7 100644 --- a/crates/libs/sbx/mgr/server/src/local_artifacts.rs +++ b/crates/libs/sbx/mgr/server/src/local_artifacts.rs @@ -59,10 +59,9 @@ //! gate with explicit deletion, whose semantics do not require idle time. use std::path::Path; -use std::sync::{Arc, Mutex, MutexGuard}; +use std::sync::{Arc, Mutex}; use pplx_space_common_core::types::{ArtifactKind, LocalArtifactId, SnapshotName}; -use rusqlite::Connection; use crate::db::LocalArtifactRow; use crate::db::local_artifact_leases as leases; @@ -111,7 +110,7 @@ pub enum CheckoutError { raw_state: String, }, #[error("database error: {0}")] - Db(#[from] rusqlite::Error), + Db(#[from] crate::db::Error), } #[derive(Clone, Copy)] @@ -129,13 +128,13 @@ pub(crate) enum GcDeleteCheckout { } /// All `local_artifacts` operations route through this handle. The -/// underlying connection is the same `Arc>` +/// underlying connection is the same `crate::db::SharedConnection` /// that drives the rest of sbx-mgr's SQLite state, so transactions /// here serialize against every other path that touches that /// connection. #[derive(Clone)] pub struct LocalArtifacts { - db: Arc>, + db: crate::db::SharedConnection, orphan_scan: Arc>, } @@ -155,7 +154,7 @@ impl Default for OrphanScan { } impl LocalArtifacts { - pub fn new(db: Arc>) -> Self { + pub fn new(db: crate::db::SharedConnection) -> Self { Self { db, orphan_scan: Arc::new(Mutex::new(OrphanScan::default())), @@ -169,7 +168,7 @@ impl LocalArtifacts { pub fn get( &self, local_artifact_id: LocalArtifactId, - ) -> rusqlite::Result> { + ) -> crate::db::Result> { let conn = self.db_conn()?; crate::db::get_local_artifact(&conn, local_artifact_id) } @@ -180,7 +179,7 @@ impl LocalArtifacts { &self, sandbox_id: &str, snapshot_name: &SnapshotName, - ) -> rusqlite::Result> { + ) -> crate::db::Result> { let conn = self.db_conn()?; crate::db::get_named_local_artifact(&conn, sandbox_id, snapshot_name) } @@ -292,7 +291,7 @@ impl LocalArtifacts { /// hand the caller a set that looks complete and is short by exactly /// the artifact nobody can name; a failure instead leaves the caller /// with no report, which every consumer already has to handle. - pub fn list_pending_backups(&self) -> rusqlite::Result> { + pub fn list_pending_backups(&self) -> crate::db::Result> { let conn = self.db_conn()?; leases::list_pending_backups(&conn) } @@ -313,7 +312,7 @@ impl LocalArtifacts { pub fn mark_backup_completed( &self, local_artifact_id: LocalArtifactId, - ) -> rusqlite::Result<()> { + ) -> crate::db::Result<()> { let conn = self.db_conn()?; crate::db::mark_local_artifact_backup_completed(&conn, &local_artifact_id.to_string()) } @@ -329,7 +328,7 @@ impl LocalArtifacts { expected_execution: Option, local_artifact_id: LocalArtifactId, local_path: &str, - ) -> rusqlite::Result<()> { + ) -> crate::db::Result<()> { let conn = self.db_conn()?; crate::db::publish_stopped_artifact( &conn, @@ -350,7 +349,7 @@ impl LocalArtifacts { artifact_kind: ArtifactKind, snapshot_name: &SnapshotName, local_path: &str, - ) -> rusqlite::Result<()> { + ) -> crate::db::Result<()> { let conn = self.db_conn()?; crate::db::publish_named_local_artifact( &conn, @@ -372,7 +371,7 @@ impl LocalArtifacts { sandbox_id: &str, local_artifact_id: LocalArtifactId, local_path: &str, - ) -> rusqlite::Result { + ) -> crate::db::Result { let conn = self.db_conn()?; crate::db::adopt_suspended_local_artifact(&conn, sandbox_id, local_artifact_id, local_path) } @@ -384,7 +383,7 @@ impl LocalArtifacts { sandbox_id: &str, local_artifact_id: LocalArtifactId, local_path: &str, - ) -> rusqlite::Result { + ) -> crate::db::Result { let conn = self.db_conn()?; crate::db::adopt_suspended_local_artifact_preserving_updated_at( &conn, @@ -407,8 +406,7 @@ impl LocalArtifacts { local_artifact_id: LocalArtifactId, ) -> Result { let conn = self.db_conn()?; - let tx = - rusqlite::Transaction::new_unchecked(&conn, rusqlite::TransactionBehavior::Immediate)?; + let tx = crate::db::Transaction::immediate(&conn)?; let id_str = local_artifact_id.to_string(); let row = leases::artifact_path_and_state(&tx, &id_str)?; let Some((local_path, state)) = row else { @@ -441,7 +439,7 @@ impl LocalArtifacts { Ok(RestoreLease { id: local_artifact_id, local_path, - db: Arc::clone(&self.db), + db: self.db.clone(), released: false, }) } @@ -459,15 +457,14 @@ impl LocalArtifacts { local_artifact_id: LocalArtifactId, ) -> Result { let conn = self.db_conn()?; - let tx = - rusqlite::Transaction::new_unchecked(&conn, rusqlite::TransactionBehavior::Immediate)?; + let tx = crate::db::Transaction::immediate(&conn)?; let local_path = Self::checkout_delete_in_transaction(&tx, local_artifact_id)?; tx.commit()?; drop(conn); Ok(DeleteLease { id: local_artifact_id, local_path, - db: Arc::clone(&self.db), + db: self.db.clone(), committed: false, }) } @@ -481,8 +478,7 @@ impl LocalArtifacts { eligibility: GcEligibility, ) -> Result { let conn = self.db_conn()?; - let tx = - rusqlite::Transaction::new_unchecked(&conn, rusqlite::TransactionBehavior::Immediate)?; + let tx = crate::db::Transaction::immediate(&conn)?; let row = leases::gc_checkout_evidence(&tx, &local_artifact_id.to_string(), sandbox_id)?; let Some((created_at, touched_at, pinned, backed_up)) = row else { return Err(CheckoutError::NotFound(local_artifact_id)); @@ -509,13 +505,13 @@ impl LocalArtifacts { Ok(GcDeleteCheckout::Leased(DeleteLease { id: local_artifact_id, local_path, - db: Arc::clone(&self.db), + db: self.db.clone(), committed: false, })) } fn checkout_delete_in_transaction( - tx: &rusqlite::Transaction<'_>, + tx: &crate::db::Transaction<'_>, local_artifact_id: LocalArtifactId, ) -> Result { let id_str = local_artifact_id.to_string(); @@ -616,10 +612,9 @@ impl LocalArtifacts { /// from a pin: adoption can reuse an existing artifact ID. /// Explicit restore or cleanup owns progress; this recovery /// does not promise an automatic repair deadline. - pub fn recover_stuck_states(&self) -> rusqlite::Result { + pub fn recover_stuck_states(&self) -> crate::db::Result { let conn = self.db_conn()?; - let tx = - rusqlite::Transaction::new_unchecked(&conn, rusqlite::TransactionBehavior::Immediate)?; + let tx = crate::db::Transaction::immediate(&conn)?; let restoring_reset = leases::reset_restoring_artifacts(&tx)?; let stuck_deleting = leases::list_deleting_artifacts(&tx)?; let mut deleting_rolled_back = 0usize; @@ -663,7 +658,7 @@ impl LocalArtifacts { /// GC staging is disposable only after both its registry and pin are gone. /// Retain rows in every state, including historical deletion conflicts. - pub(crate) fn gc_staging_is_unowned(&self, id: LocalArtifactId) -> rusqlite::Result { + pub(crate) fn gc_staging_is_unowned(&self, id: LocalArtifactId) -> crate::db::Result { let conn = self.db_conn()?; leases::gc_staging_is_unowned(&conn, &id.to_string()) } @@ -672,18 +667,17 @@ impl LocalArtifacts { /// malformed, or unreadable entries. A fixed rowid ceiling makes each sweep /// finite under insertion churn; retained rows are reconsidered after wrap. /// Probes and guarded deletion retain the same immediate transaction. - pub fn prune_missing_orphan_rows(&self, max_records: usize) -> rusqlite::Result { + pub fn prune_missing_orphan_rows(&self, max_records: usize) -> crate::db::Result { if max_records == 0 { return Ok(0); } let conn = self.db_conn()?; let mut cursor = self.orphan_scan.lock().map_err(|error| { - rusqlite::Error::InvalidParameterName(format!("artifact scan mutex poisoned: {error}")) + crate::db::Error::poisoned(format!("artifact scan mutex poisoned: {error}")) })?; let mut scan = *cursor; - let tx = - rusqlite::Transaction::new_unchecked(&conn, rusqlite::TransactionBehavior::Immediate)?; + let tx = crate::db::Transaction::immediate(&conn)?; if scan.through.is_none() { scan.through = leases::max_artifact_rowid(&tx)?; } @@ -731,9 +725,9 @@ impl LocalArtifacts { Ok(deleted) } - fn db_conn(&self) -> rusqlite::Result> { + fn db_conn(&self) -> crate::db::Result> { self.db.lock().map_err(|error| { - rusqlite::Error::InvalidParameterName(format!("database mutex poisoned: {error}")) + crate::db::Error::poisoned(format!("database mutex poisoned: {error}")) }) } } @@ -774,7 +768,7 @@ impl StuckStateRecovery { pub struct RestoreLease { id: LocalArtifactId, local_path: String, - db: Arc>, + db: crate::db::SharedConnection, released: bool, } @@ -795,13 +789,13 @@ impl RestoreLease { /// this point. Prefer this over relying on `Drop` so failures /// surface as `Result` instead of being swallowed, and so the /// idle-clock reset actually happens. - pub fn release(mut self) -> rusqlite::Result<()> { + pub fn release(mut self) -> crate::db::Result<()> { let result = self.release_with_touch(); self.released = true; result } - fn release_with_touch(&self) -> rusqlite::Result<()> { + fn release_with_touch(&self) -> crate::db::Result<()> { let conn = lock_db(&self.db)?; leases::release_restoring_artifact_with_touch(&conn, &self.id.to_string())?; Ok(()) @@ -812,7 +806,7 @@ impl RestoreLease { /// early `?` or panic before `release()`), and the caller may /// not have actually read the artifact's bytes. The reaper's /// idle clock should reflect that. - fn release_without_touch(&self) -> rusqlite::Result<()> { + fn release_without_touch(&self) -> crate::db::Result<()> { let conn = lock_db(&self.db)?; leases::release_restoring_artifact(&conn, &self.id.to_string())?; Ok(()) @@ -841,7 +835,7 @@ impl Drop for RestoreLease { pub struct DeleteLease { id: LocalArtifactId, local_path: String, - db: Arc>, + db: crate::db::SharedConnection, committed: bool, } @@ -858,7 +852,7 @@ impl DeleteLease { /// `AND state = 'deleting'` makes this idempotent: a second /// commit (e.g. after rollback already ran) is a no-op rather /// than a foot-gun. - pub fn commit(mut self) -> rusqlite::Result<()> { + pub fn commit(mut self) -> crate::db::Result<()> { let conn = lock_db(&self.db)?; let deleted = leases::delete_deleting_artifact(&conn, &self.id.to_string())?; drop(conn); @@ -899,10 +893,9 @@ impl Drop for DeleteLease { } } -fn lock_db(db: &Arc>) -> rusqlite::Result> { - db.lock().map_err(|error| { - rusqlite::Error::InvalidParameterName(format!("database mutex poisoned: {error}")) - }) +fn lock_db(db: &crate::db::SharedConnection) -> crate::db::Result> { + db.lock() + .map_err(|error| crate::db::Error::poisoned(format!("database mutex poisoned: {error}"))) } #[cfg(test)] diff --git a/crates/libs/sbx/mgr/server/src/local_artifacts/refusal_tests.rs b/crates/libs/sbx/mgr/server/src/local_artifacts/refusal_tests.rs index 8d9e76790..8bdf0f49c 100644 --- a/crates/libs/sbx/mgr/server/src/local_artifacts/refusal_tests.rs +++ b/crates/libs/sbx/mgr/server/src/local_artifacts/refusal_tests.rs @@ -3,7 +3,8 @@ use std::sync::PoisonError; -use rusqlite::{OptionalExtension, params}; +use crate::db::fixture::params; +use crate::db::{Connection, OptionalExtension}; use super::*; @@ -21,9 +22,9 @@ fn artifact_id(id: &str) -> LocalArtifactId { id.parse().unwrap() } -fn shared(conn: Connection) -> (Arc>, LocalArtifacts) { - let db = Arc::new(Mutex::new(conn)); - let store = LocalArtifacts::new(Arc::clone(&db)); +fn shared(conn: Connection) -> (crate::db::SharedConnection, LocalArtifacts) { + let db = crate::db::SharedConnection::new(conn); + let store = LocalArtifacts::new(db.clone()); (db, store) } @@ -42,7 +43,7 @@ fn insert_row(conn: &Connection, id: &str, sandbox_id: &str, local_path: &str) { } /// Reads through a poisoned mutex too, so failure tests can inspect the row. -fn row_state(db: &Arc>, id: &str) -> Option { +fn row_state(db: &crate::db::SharedConnection, id: &str) -> Option { db.lock() .unwrap_or_else(PoisonError::into_inner) .query_row( @@ -54,7 +55,7 @@ fn row_state(db: &Arc>, id: &str) -> Option { .unwrap() } -fn last_touched_at(db: &Arc>, id: &str) -> String { +fn last_touched_at(db: &crate::db::SharedConnection, id: &str) -> String { db.lock() .unwrap() .query_row( @@ -336,7 +337,7 @@ fn lease_resolution_failure_is_reported_and_leaves_the_row_held() { .unwrap(); match failure { ResolutionFailure::PoisonedConnection => { - let poisoner = Arc::clone(&db); + let poisoner = db.clone(); std::thread::spawn(move || { let _held = poisoner.lock().unwrap(); panic!("poison the shared connection"); @@ -377,8 +378,7 @@ fn lease_resolution_failure_is_reported_and_leaves_the_row_held() { } fn insert_named_rows(conn: &Connection, sandbox_id: &str, count: usize) { - let tx = rusqlite::Transaction::new_unchecked(conn, rusqlite::TransactionBehavior::Immediate) - .unwrap(); + let tx = crate::db::Transaction::immediate(conn).unwrap(); for index in 0..count { crate::db::publish_named_local_artifact( &tx, diff --git a/crates/libs/sbx/mgr/server/src/local_artifacts/tests.rs b/crates/libs/sbx/mgr/server/src/local_artifacts/tests.rs index 3dcac7287..4ed7b3067 100644 --- a/crates/libs/sbx/mgr/server/src/local_artifacts/tests.rs +++ b/crates/libs/sbx/mgr/server/src/local_artifacts/tests.rs @@ -1,5 +1,6 @@ +use crate::db::Connection; +use crate::db::fixture::params; use pplx_space_common_core::types::SandboxId; -use rusqlite::params; use super::*; @@ -12,8 +13,8 @@ thread_local! { static GC_READ_RACE: std::cell::RefCell> = const { std::cell::RefCell::new(None) }; } -fn race_restore_release_at_gc_read(event: rusqlite::trace::TraceEvent<'_>) { - if let rusqlite::trace::TraceEvent::Row(statement) = event +fn race_restore_release_at_gc_read(event: crate::db::fixture::TraceEvent<'_>) { + if let crate::db::fixture::TraceEvent::Row(statement) = event && statement .sql() .starts_with("SELECT created_at, COALESCE(last_touched_at, created_at)") @@ -52,7 +53,7 @@ fn artifact_id(s: &str) -> LocalArtifactId { fn gc_checkout_serializes_eligibility_with_a_concurrent_restore_release() { let root = tempfile::TempDir::new().unwrap(); let path = root.path().join("artifacts.db"); - let mut conn = Connection::open(&path).unwrap(); + let mut conn = Connection::open_at(&path).unwrap(); crate::db::create_test_schema(&mut conn); conn.pragma_update(None, "journal_mode", "WAL").unwrap(); let id = "01939e3e-1111-7000-8000-000000000001"; @@ -62,9 +63,9 @@ fn gc_checkout_serializes_eligibility_with_a_concurrent_restore_release() { [id], ) .unwrap(); - let competing = Connection::open(&path).unwrap(); + let competing = Connection::open_at(&path).unwrap(); competing.busy_timeout(std::time::Duration::ZERO).unwrap(); - let competing_store = LocalArtifacts::new(Arc::new(Mutex::new(competing))); + let competing_store = LocalArtifacts::new(crate::db::SharedConnection::new(competing)); let restore = competing_store .check_out_for_restore(artifact_id(id)) .unwrap(); @@ -77,10 +78,10 @@ fn gc_checkout_serializes_eligibility_with_a_concurrent_restore_release() { }) }); conn.trace_v2( - rusqlite::trace::TraceEventCodes::SQLITE_TRACE_ROW, + crate::db::fixture::TraceEventCodes::SQLITE_TRACE_ROW, Some(race_restore_release_at_gc_read), ); - let store = LocalArtifacts::new(Arc::new(Mutex::new(conn))); + let store = LocalArtifacts::new(crate::db::SharedConnection::new(conn)); let writer = std::thread::spawn(move || { start_rx .recv_timeout(std::time::Duration::from_secs(5)) @@ -105,9 +106,7 @@ fn gc_checkout_serializes_eligibility_with_a_concurrent_restore_release() { .. }) )); - assert!( - matches!(writer.join().unwrap(), Err(rusqlite::Error::SqliteFailure(error, _)) if error.code == rusqlite::ErrorCode::DatabaseBusy) - ); + assert!(matches!(writer.join().unwrap(), Err(error) if error.is_database_busy())); assert!( GC_READ_RACE.with(|race| race.borrow().is_none()), "the competing writer must run at the eligibility read" @@ -119,8 +118,8 @@ fn gc_checkout_requires_current_backup_pin_and_time_evidence() { let conn = open_test_db(); let id = "01939e3e-1111-7000-8000-000000000001"; insert_row(&conn, id, "sbx", "/artifact"); - let shared = Arc::new(Mutex::new(conn)); - let store = LocalArtifacts::new(Arc::clone(&shared)); + let shared = crate::db::SharedConnection::new(conn); + let store = LocalArtifacts::new(shared.clone()); let eligibility = GcEligibility { now_secs: 100, idle_secs: 0, @@ -231,7 +230,7 @@ fn named_publication_inventory_reports_state_without_taking_a_lease() { "other", "/tmp/other", ); - let store = LocalArtifacts::new(Arc::new(Mutex::new(conn))); + let store = LocalArtifacts::new(crate::db::SharedConnection::new(conn)); let check = |state| { let publications = store.list_named_publications("sbx").unwrap(); assert_eq!(publications.len(), 1); @@ -256,7 +255,7 @@ fn check_out_for_restore_flips_state_and_releases_back() { let conn = open_test_db(); let id = "01939e3e-1111-7000-8000-000000000001"; insert_row(&conn, id, "sbx-1", "/tmp/art"); - let store = LocalArtifacts::new(Arc::new(Mutex::new(conn))); + let store = LocalArtifacts::new(crate::db::SharedConnection::new(conn)); let lease = store.check_out_for_restore(artifact_id(id)).unwrap(); // While the lease is held, a second checkout (restore or @@ -281,7 +280,7 @@ fn check_out_for_delete_blocks_concurrent_restore_and_vice_versa() { let conn = open_test_db(); let id = "01939e3e-2222-7000-8000-000000000001"; insert_row(&conn, id, "sbx-2", "/tmp/art2"); - let store = LocalArtifacts::new(Arc::new(Mutex::new(conn))); + let store = LocalArtifacts::new(crate::db::SharedConnection::new(conn)); let del = store.check_out_for_delete(artifact_id(id)).unwrap(); let restore_attempt = store.check_out_for_restore(artifact_id(id)); @@ -304,7 +303,7 @@ fn check_out_for_delete_refuses_shared_local_path() { let id_b = "01939e3e-3333-7000-8000-000000000002"; insert_row(&conn, id_a, "sbx-3", "/tmp/shared"); insert_row(&conn, id_b, "sbx-3", "/tmp/shared"); - let store = LocalArtifacts::new(Arc::new(Mutex::new(conn))); + let store = LocalArtifacts::new(crate::db::SharedConnection::new(conn)); let attempt = store.check_out_for_delete(artifact_id(id_a)); assert!(matches!( @@ -321,7 +320,7 @@ fn delete_lease_commit_removes_row() { let conn = open_test_db(); let id = "01939e3e-4444-7000-8000-000000000001"; insert_row(&conn, id, "sbx-4", "/tmp/art4"); - let store = LocalArtifacts::new(Arc::new(Mutex::new(conn))); + let store = LocalArtifacts::new(crate::db::SharedConnection::new(conn)); let lease = store.check_out_for_delete(artifact_id(id)).unwrap(); lease.commit().unwrap(); @@ -335,7 +334,7 @@ fn restore_lease_drop_without_release_rolls_back_to_ready() { let conn = open_test_db(); let id = "01939e3e-5555-7000-8000-000000000001"; insert_row(&conn, id, "sbx-5", "/tmp/art5"); - let store = LocalArtifacts::new(Arc::new(Mutex::new(conn))); + let store = LocalArtifacts::new(crate::db::SharedConnection::new(conn)); { let _lease = store.check_out_for_restore(artifact_id(id)).unwrap(); @@ -369,7 +368,7 @@ fn restore_lease_release_bumps_last_touched_at_but_drop_does_not() { .unwrap(); // Two distinct sandboxes so the dedupe in publish_suspended doesn't apply // — these are direct-SQL inserts so dedupe never fires anyway. Fine. - let store = LocalArtifacts::new(Arc::new(Mutex::new(conn))); + let store = LocalArtifacts::new(crate::db::SharedConnection::new(conn)); // Path A: release() — must bump last_touched_at. let lease_a = store.check_out_for_restore(artifact_id(id_a)).unwrap(); @@ -413,7 +412,7 @@ fn restore_lease_release_bumps_last_touched_at_but_drop_does_not() { #[test] fn check_out_returns_not_found_for_missing_row() { let conn = open_test_db(); - let store = LocalArtifacts::new(Arc::new(Mutex::new(conn))); + let store = LocalArtifacts::new(crate::db::SharedConnection::new(conn)); let attempt = store.check_out_for_restore(artifact_id("01939e3e-6666-7000-8000-000000000001")); assert!(matches!(attempt, Err(CheckoutError::NotFound(_)))); } @@ -432,7 +431,7 @@ fn artifact_state_round_trips_via_from_str() { .expect_err("bogus artifact state must fail to parse"); } -fn force_state(conn: &Arc>, id: &str, state: &str) { +fn force_state(conn: &crate::db::SharedConnection, id: &str, state: &str) { conn.lock() .unwrap() .execute( @@ -450,8 +449,8 @@ fn recover_resets_restoring_rows_to_ready() { let conn = open_test_db(); let id = "01939e3e-8881-7000-8000-000000000001"; insert_row(&conn, id, "sbx-r1", "/tmp/recover-restoring"); - let db = Arc::new(Mutex::new(conn)); - let store = LocalArtifacts::new(Arc::clone(&db)); + let db = crate::db::SharedConnection::new(conn); + let store = LocalArtifacts::new(db.clone()); force_state(&db, id, "restoring"); let report = store.recover_stuck_states().unwrap(); @@ -474,8 +473,8 @@ fn recover_rolls_back_deleting_when_original_bytes_intact() { let id = "01939e3e-8882-7000-8000-000000000001"; insert_row(&conn, id, "sbx-r2", &local_path); - let db = Arc::new(Mutex::new(conn)); - let store = LocalArtifacts::new(Arc::clone(&db)); + let db = crate::db::SharedConnection::new(conn); + let store = LocalArtifacts::new(db.clone()); force_state(&db, id, "deleting"); let report = store.recover_stuck_states().unwrap(); @@ -499,8 +498,8 @@ fn recover_finishes_delete_when_staging_dir_present() { std::fs::create_dir_all(format!("{local_path}.gc-pending-{id}")).unwrap(); insert_row(&conn, id, "sbx-r3", &local_path); - let db = Arc::new(Mutex::new(conn)); - let store = LocalArtifacts::new(Arc::clone(&db)); + let db = crate::db::SharedConnection::new(conn); + let store = LocalArtifacts::new(db.clone()); force_state(&db, id, "deleting"); let report = store.recover_stuck_states().unwrap(); @@ -525,8 +524,8 @@ fn recover_finishes_delete_when_neither_path_present() { let id = "01939e3e-8884-7000-8000-000000000001"; insert_row(&conn, id, "sbx-r4", &local_path); - let db = Arc::new(Mutex::new(conn)); - let store = LocalArtifacts::new(Arc::clone(&db)); + let db = crate::db::SharedConnection::new(conn); + let store = LocalArtifacts::new(db.clone()); force_state(&db, id, "deleting"); let report = store.recover_stuck_states().unwrap(); @@ -567,8 +566,8 @@ fn prune_missing_orphan_rows_deletes_only_missing_unowned_rows() { [], ) .unwrap(); - let db = Arc::new(Mutex::new(conn)); - let store = LocalArtifacts::new(Arc::clone(&db)); + let db = crate::db::SharedConnection::new(conn); + let store = LocalArtifacts::new(db.clone()); let pruned = store.prune_missing_orphan_rows(10).unwrap(); @@ -585,8 +584,8 @@ fn prune_missing_orphan_rows_obeys_batch_limit() { let second = "01939e3e-8891-7000-8000-000000000002"; insert_row(&conn, first, "sbx-orphan-1", "/tmp/space-orphan-1"); insert_row(&conn, second, "sbx-orphan-2", "/tmp/space-orphan-2"); - let db = Arc::new(Mutex::new(conn)); - let store = LocalArtifacts::new(Arc::clone(&db)); + let db = crate::db::SharedConnection::new(conn); + let store = LocalArtifacts::new(db.clone()); assert_eq!(store.prune_missing_orphan_rows(1).unwrap(), 1); assert_eq!(store.prune_missing_orphan_rows(1).unwrap(), 1); @@ -629,8 +628,8 @@ fn prune_missing_orphan_rows_advances_past_preserved_rows_and_wraps() { "missing", tmp.path().join("missing").to_str().unwrap(), ); - let db = Arc::new(Mutex::new(conn)); - let store = LocalArtifacts::new(Arc::clone(&db)); + let db = crate::db::SharedConnection::new(conn); + let store = LocalArtifacts::new(db.clone()); assert_eq!(store.prune_missing_orphan_rows(1).unwrap(), 0); insert_row( @@ -665,7 +664,7 @@ fn prune_missing_orphan_rows_preserves_unreadable_paths() { loop_path.try_exists().unwrap_err(); let id = "01939e3e-8893-7000-8000-000000000001"; insert_row(&conn, id, "unreadable", loop_path.to_str().unwrap()); - let store = LocalArtifacts::new(Arc::new(Mutex::new(conn))); + let store = LocalArtifacts::new(crate::db::SharedConnection::new(conn)); assert_eq!(store.prune_missing_orphan_rows(1).unwrap(), 0); assert!(store.get(artifact_id(id)).unwrap().is_some()); } @@ -705,8 +704,8 @@ fn recover_pinned_deletion_requires_unambiguous_original_bytes() { params![id], ) .unwrap(); - let db = Arc::new(Mutex::new(conn)); - let store = LocalArtifacts::new(Arc::clone(&db)); + let db = crate::db::SharedConnection::new(conn); + let store = LocalArtifacts::new(db.clone()); force_state(&db, id, "deleting"); let report = store.recover_stuck_states().unwrap(); @@ -923,7 +922,7 @@ fn list_pending_backups_returns_exactly_the_unbacked_pinned_artifacts() { // A running sandbox pins nothing and holds nothing. insert_pinned_sandbox(&conn, "sbx-running", None); - let store = LocalArtifacts::new(Arc::new(Mutex::new(conn))); + let store = LocalArtifacts::new(crate::db::SharedConnection::new(conn)); let pending = store.list_pending_backups().unwrap(); @@ -956,8 +955,8 @@ fn list_pending_backups_follows_the_pin_not_the_sandbox_id() { Some("2026-09-19T00:00:00Z"), ); insert_pinned_sandbox(&conn, "sbx-repin", Some((pinned, "full", "suspend"))); - let db = Arc::new(Mutex::new(conn)); - let store = LocalArtifacts::new(Arc::clone(&db)); + let db = crate::db::SharedConnection::new(conn); + let store = LocalArtifacts::new(db.clone()); assert_eq!(store.list_pending_backups().unwrap(), Vec::new()); @@ -1005,7 +1004,7 @@ fn list_pending_backups_reports_a_pin_owned_by_another_sandbox() { "sbx-borrower", Some((owned_by_other, "full", "suspend")), ); - let store = LocalArtifacts::new(Arc::new(Mutex::new(conn))); + let store = LocalArtifacts::new(crate::db::SharedConnection::new(conn)); assert_eq!( store.list_pending_backups().unwrap(), @@ -1040,7 +1039,7 @@ fn list_pending_backups_reports_a_pin_whose_artifact_row_is_gone() { Some("2026-09-19T00:00:00Z"), ); insert_pinned_sandbox(&conn, "sbx-healthy", Some((healthy, "full", "suspend"))); - let store = LocalArtifacts::new(Arc::new(Mutex::new(conn))); + let store = LocalArtifacts::new(crate::db::SharedConnection::new(conn)); assert_eq!( store.list_pending_backups().unwrap(), @@ -1085,12 +1084,12 @@ fn list_pending_backups_fails_rather_than_dropping_an_undecodable_row() { ); conn.pragma_update(None, "ignore_check_constraints", false) .unwrap(); - let store = LocalArtifacts::new(Arc::new(Mutex::new(conn))); + let store = LocalArtifacts::new(crate::db::SharedConnection::new(conn)); let error = store.list_pending_backups().unwrap_err(); assert!( - matches!(error, rusqlite::Error::FromSqlConversionFailure(..)), + error.conversion_failure().is_some(), "expected a decode failure, got {error:?}" ); } diff --git a/crates/libs/sbx/mgr/server/src/node_state_sync.rs b/crates/libs/sbx/mgr/server/src/node_state_sync.rs index eb7c93963..8268df572 100644 --- a/crates/libs/sbx/mgr/server/src/node_state_sync.rs +++ b/crates/libs/sbx/mgr/server/src/node_state_sync.rs @@ -6,10 +6,9 @@ use pplx_space_node_api_client::proto::{ FacetRef, MarkFacetDirtyRequest, NetworkPolicyTargetRef, ReconcileFacet, facet_ref, network_policy_target_ref, }; -use rusqlite::Connection; use std::collections::hash_map::DefaultHasher; use std::hash::{Hash, Hasher}; -use std::sync::{Arc, Mutex, MutexGuard, OnceLock}; +use std::sync::OnceLock; use tokio::sync::Notify; use tokio_util::sync::CancellationToken; use tokio_util::task::TaskTracker; @@ -30,8 +29,8 @@ pub(crate) struct DatabaseMutexPoisoned; /// supervised outbox worker, which observes the same poisoned connection and /// terminates the process through the normal ownership-preserving drain. pub(crate) fn connection_or_signal_failure( - db: &Mutex, -) -> Result, DatabaseMutexPoisoned> { + db: &crate::db::SharedConnection, +) -> Result, DatabaseMutexPoisoned> { db.lock().map_err(|_| { tracing::error!("sandbox database mutex poisoned; notifying supervised outbox worker"); database_poison_notify().notify_one(); @@ -47,7 +46,7 @@ pub(crate) fn should_skip_node_publish(request: &Request) -> bool { .is_some_and(|value| value.eq_ignore_ascii_case("true") || value == "1") } -pub(crate) fn mark_sandbox_dirty(db: &Arc>, sandbox_id: &str) { +pub(crate) fn mark_sandbox_dirty(db: &crate::db::SharedConnection, sandbox_id: &str) { if sandbox_id.trim().is_empty() { return; } @@ -56,7 +55,7 @@ pub(crate) fn mark_sandbox_dirty(db: &Arc>, sandbox_id: &str) } } -pub(crate) fn mark_sandboxes_dirty(db: &Arc>, sandbox_ids: I) +pub(crate) fn mark_sandboxes_dirty(db: &crate::db::SharedConnection, sandbox_ids: I) where I: IntoIterator, { @@ -92,7 +91,7 @@ where } } -fn enqueue_sandbox_inventory_dirty(db: &Arc>, sandbox_id: &str) -> bool { +fn enqueue_sandbox_inventory_dirty(db: &crate::db::SharedConnection, sandbox_id: &str) -> bool { let Ok(db) = connection_or_signal_failure(db) else { return false; }; @@ -120,7 +119,7 @@ fn database_poison_notify() -> &'static Notify { } pub(crate) fn start_dirty_outbox_worker( - db: Arc>, + db: crate::db::SharedConnection, shutdown_token: CancellationToken, task_tracker: &TaskTracker, critical: crate::service::drain::CriticalTaskGuard, diff --git a/crates/libs/sbx/mgr/server/src/route_activation_claims.rs b/crates/libs/sbx/mgr/server/src/route_activation_claims.rs index 6c49856c5..984fd1b13 100644 --- a/crates/libs/sbx/mgr/server/src/route_activation_claims.rs +++ b/crates/libs/sbx/mgr/server/src/route_activation_claims.rs @@ -1,11 +1,11 @@ -use rusqlite::Connection; +use crate::db::Connection; use crate::db::route_activation_claims::{OpenRouteActivationClaim, claim_matches}; #[derive(Debug, thiserror::Error)] pub(crate) enum OpenRouteActivationAdmissionError { #[error(transparent)] - Database(#[from] rusqlite::Error), + Database(#[from] crate::db::Error), #[error( "runtime activation identity generation is not newer than retained generation {retained}" )] diff --git a/crates/libs/sbx/mgr/server/src/run.rs b/crates/libs/sbx/mgr/server/src/run.rs index b46346649..9d37bf64a 100644 --- a/crates/libs/sbx/mgr/server/src/run.rs +++ b/crates/libs/sbx/mgr/server/src/run.rs @@ -38,8 +38,6 @@ use tonic::service::interceptor::InterceptedService; use tonic::{Request, Status, service::Interceptor}; use tower::{BoxError, Layer, Service}; -use pplx_space_sbxmgr_migrations::{COMMON_SCHEMA_MIGRATOR, SANDBOX_MANAGER_MIGRATOR}; - use crate::proto::sandbox_manager_server::SandboxManagerServer; use crate::service::{ EnableDependencyRestartDrainError, ReleaseDependencyRestartDrainError, SandboxManagerService, @@ -77,20 +75,6 @@ const POST_HANDOFF_RPC_DRAIN_TIMEOUT: Duration = DEFAULT_DRAIN_TIMEOUT; const DRAIN_BLOCKER_WARNING_THRESHOLD: Duration = Duration::from_secs(10); const STARTUP_RECOVERY_ADMISSION_TIMEOUT: Duration = DEFAULT_SERVICE_MUTEX_TIMEOUT; -async fn migrate_sandbox_manager_db( - db_path: &std::path::Path, -) -> Result<(), pplx_space_common_svc::sql::sqlite::SqliteError> { - pplx_space_common_svc::sql::sqlite::migrate_embedded( - "sandbox-manager", - db_path, - pplx_space_common_svc::sql::sqlite::EmbeddedMigrators { - service: &SANDBOX_MANAGER_MIGRATOR, - common: Some(&COMMON_SCHEMA_MIGRATOR), - }, - ) - .await -} - type GrpcServerHandle = JoinHandle>; type GrpcServerJoinResult = Result, tokio::task::JoinError>; @@ -584,9 +568,9 @@ pub enum RunSbxMgrError { #[error("failed to acquire the sandbox-manager service mutex: {0}")] ServiceMutex(#[source] pplx_space_common_svc::service_mutex::AcquireError), #[error("failed to migrate the sandbox-manager database: {0}")] - Migrate(#[source] pplx_space_common_svc::sql::sqlite::SqliteError), + Migrate(#[source] crate::db::OpenError), #[error("failed to open the sandbox-manager database: {0}")] - OpenDatabase(#[source] pplx_space_common_svc::sql::sqlite::SqliteError), + OpenDatabase(#[source] crate::db::OpenError), #[error("invalid {SPACE_NODE_ROLE_ENV} {role:?}: {source}")] NodeRole { role: String, @@ -610,7 +594,7 @@ pub enum RunSbxMgrError { #[error("failed to persist sandbox startup recovery: {0}")] StartupRecovery(#[source] StartupRecoveryError), #[error("failed to recover local_artifacts lease states: {0}")] - LocalArtifactRecovery(#[source] rusqlite::Error), + LocalArtifactRecovery(#[source] crate::db::Error), #[error("failed to refund interrupted snapshot upload attempts: {0}")] RefundSnapshotUploadAttempts( #[source] crate::service::RefundInterruptedSnapshotUploadAttemptsError, @@ -815,11 +799,9 @@ pub async fn run( ); health_server.mark_starting(); - migrate_sandbox_manager_db(std::path::Path::new( - pplx_space_common_core::host::linux::paths::SANDBOX_MANAGER_DB, - )) - .await - .map_err(RunSbxMgrError::Migrate)?; + crate::db::migrate() + .await + .map_err(RunSbxMgrError::Migrate)?; let conn = crate::db::open().map_err(RunSbxMgrError::OpenDatabase)?; let node_role = match std::env::var(SPACE_NODE_ROLE_ENV) { diff --git a/crates/libs/sbx/mgr/server/src/run/tests.rs b/crates/libs/sbx/mgr/server/src/run/tests.rs index 0446d94f0..851924ded 100644 --- a/crates/libs/sbx/mgr/server/src/run/tests.rs +++ b/crates/libs/sbx/mgr/server/src/run/tests.rs @@ -4,7 +4,7 @@ use std::sync::{Arc, Mutex}; use super::*; use async_trait::async_trait; -use pplx_space_common_jobs::{BackgroundWorker, Job, JobError, JobExecutor, WorkerConfig}; +use pplx_space_common_jobs::{Job, JobError, JobExecutor, WorkerConfig}; use pplx_space_common_svc::{ServiceMutex, UnixSocketOptions}; use tonic::codegen::Bytes; use tonic::transport::Channel; @@ -369,25 +369,13 @@ impl JobExecutor for SuspendFinalizationExecutor { } } -fn create_jobs_schema(connection: &rusqlite::Connection) { - for migration in COMMON_SCHEMA_MIGRATOR - .iter() - .filter(|migration| migration.migration_type.is_up_migration()) - { - connection.execute_batch(migration.sql.as_ref()).unwrap(); - } -} - #[tokio::test(start_paused = true)] async fn snapshot_worker_restart_yield_allows_inflight_suspend_finalization_to_finish() { assert!(SNAPSHOT_WORKER_DRAIN_TIMEOUT < PRE_HANDOFF_DRAIN_TIMEOUT); - let connection = rusqlite::Connection::open_in_memory().expect("open jobs database"); - create_jobs_schema(&connection); let started = Arc::new(tokio::sync::Notify::new()); let completed = Arc::new(AtomicBool::new(false)); - let worker = BackgroundWorker::new( - Arc::new(Mutex::new(connection)), + let worker = crate::db::SharedConnection::new(crate::db::test_conn()).background_worker( SuspendFinalizationExecutor { started: Arc::clone(&started), completed: Arc::clone(&completed), @@ -774,7 +762,7 @@ async fn grpc_writer_during_pre_handoff_drain_returns_retry_on_successor() { let listener = pplx_space_common_svc::bind_unix_socket(&socket_path, UnixSocketOptions::new(0o600)) .unwrap(); - let service = SandboxManagerService::for_test(rusqlite::Connection::open_in_memory().unwrap()); + let service = SandboxManagerService::for_test(crate::db::Connection::open_in_memory()); let rpc_drain_token = CancellationToken::new(); let shutdown_token = CancellationToken::new(); let server_shutdown = shutdown_token.clone(); @@ -821,7 +809,7 @@ async fn grpc_in_flight_request_interrupted_at_handoff_is_not_marked_replayable( let listener = pplx_space_common_svc::bind_unix_socket(&socket_path, UnixSocketOptions::new(0o600)) .unwrap(); - let service = SandboxManagerService::for_test(rusqlite::Connection::open_in_memory().unwrap()); + let service = SandboxManagerService::for_test(crate::db::Connection::open_in_memory()); let rpc_drain_token = CancellationToken::new(); let shutdown_token = CancellationToken::new(); let server_shutdown = shutdown_token.clone(); @@ -1270,10 +1258,10 @@ async fn daemon_migrations_converge_jobs_from_each_supported_schema() { let dir = tempfile::tempdir().unwrap(); let path = dir.path().join("sandbox-manager.db"); if schema != "fresh" { - migrate_sandbox_manager_db(&path).await.unwrap(); - let conn = rusqlite::Connection::open(&path).unwrap(); + crate::db::migrate_at(&path).await.unwrap(); + let conn = crate::db::Connection::open_at(&path).unwrap(); jobs::insert_job( - &conn, + conn.raw(), jobs::InsertJob { job_id: &"019d84bd-280d-7331-99c6-000000000001".parse().unwrap(), kind: "sandbox_cleanup", @@ -1287,13 +1275,13 @@ async fn daemon_migrations_converge_jobs_from_each_supported_schema() { ) .unwrap(); jobs::mark_job_running( - &conn, + conn.raw(), &"019d84bd-280d-7331-99c6-000000000001".parse().unwrap(), ) .unwrap(); } - migrate_sandbox_manager_db(&path).await.unwrap(); - let conn = rusqlite::Connection::open(&path).unwrap(); + crate::db::migrate_at(&path).await.unwrap(); + let conn = crate::db::Connection::open_at(&path).unwrap(); conn.prepare("SELECT activation_attempt_id, operation_activation_attempt_id FROM sandbox_cleanup_episodes") .expect("real daemon migrations install independent operation ownership"); assert_eq!( @@ -1307,7 +1295,7 @@ async fn daemon_migrations_converge_jobs_from_each_supported_schema() { if schema != "fresh" { let retained = jobs::get_job( - &conn, + conn.raw(), &"019d84bd-280d-7331-99c6-000000000001".parse().unwrap(), ) .unwrap() @@ -1317,7 +1305,7 @@ async fn daemon_migrations_converge_jobs_from_each_supported_schema() { assert_eq!(retained.attempts, 1, "{schema}"); } jobs::insert_job( - &conn, + conn.raw(), jobs::InsertJob { job_id: &"019d84bd-280d-7331-99c6-000000000002".parse().unwrap(), kind: "sandbox_cleanup", @@ -1331,25 +1319,25 @@ async fn daemon_migrations_converge_jobs_from_each_supported_schema() { ) .unwrap(); jobs::mark_job_running( - &conn, + conn.raw(), &"019d84bd-280d-7331-99c6-000000000002".parse().unwrap(), ) .unwrap(); jobs::request_job_cancellation( - &conn, + conn.raw(), &"019d84bd-280d-7331-99c6-000000000002".parse().unwrap(), ) .unwrap(); assert!( jobs::mark_job_cancelled( - &conn, + conn.raw(), &"019d84bd-280d-7331-99c6-000000000002".parse().unwrap() ) .unwrap() ); assert_eq!( jobs::get_job( - &conn, + conn.raw(), &"019d84bd-280d-7331-99c6-000000000002".parse().unwrap() ) .unwrap() @@ -1359,8 +1347,8 @@ async fn daemon_migrations_converge_jobs_from_each_supported_schema() { "{schema}" ); drop(conn); - migrate_sandbox_manager_db(&path).await.unwrap(); - let conn = rusqlite::Connection::open(&path).unwrap(); + crate::db::migrate_at(&path).await.unwrap(); + let conn = crate::db::Connection::open_at(&path).unwrap(); assert_eq!( conn.query_row( "SELECT count(*) FROM sqlite_schema WHERE type = 'index' AND name = ?1", @@ -1373,7 +1361,7 @@ async fn daemon_migrations_converge_jobs_from_each_supported_schema() { ); assert_eq!( jobs::get_job( - &conn, + conn.raw(), &"019d84bd-280d-7331-99c6-000000000002".parse().unwrap() ) .unwrap() @@ -1392,11 +1380,11 @@ async fn daemon_migration_failure_preserves_durable_jobs() { for history in ["_sqlx_migrations", "_space_common_schema_migrations"] { let dir = tempfile::tempdir().unwrap(); let path = dir.path().join("sandbox-manager.db"); - migrate_sandbox_manager_db(&path).await.unwrap(); - let conn = rusqlite::Connection::open(&path).unwrap(); + crate::db::migrate_at(&path).await.unwrap(); + let conn = crate::db::Connection::open_at(&path).unwrap(); jobs::insert_job( - &conn, + conn.raw(), jobs::InsertJob { job_id: &"019d84bd-280d-7331-99c6-000000000001".parse().unwrap(), kind: "sandbox_cleanup", @@ -1413,14 +1401,11 @@ async fn daemon_migration_failure_preserves_durable_jobs() { .unwrap(); drop(conn); - assert!( - migrate_sandbox_manager_db(&path).await.is_err(), - "{history}" - ); - let conn = rusqlite::Connection::open(&path).unwrap(); + assert!(crate::db::migrate_at(&path).await.is_err(), "{history}"); + let conn = crate::db::Connection::open_at(&path).unwrap(); assert_eq!( jobs::get_job( - &conn, + conn.raw(), &"019d84bd-280d-7331-99c6-000000000001".parse().unwrap() ) .unwrap() diff --git a/crates/libs/sbx/mgr/server/src/runtime_reconciliation.rs b/crates/libs/sbx/mgr/server/src/runtime_reconciliation.rs index 9a97668a1..bdb69b504 100644 --- a/crates/libs/sbx/mgr/server/src/runtime_reconciliation.rs +++ b/crates/libs/sbx/mgr/server/src/runtime_reconciliation.rs @@ -1,5 +1,3 @@ -use std::sync::{Arc, Mutex}; - use pplx_space_common_core::types::{ SandboxId, SandboxQuarantineNextAction, SandboxQuarantineReasonCode, SandboxQuarantineSource, }; @@ -105,7 +103,7 @@ pub(crate) async fn observe_firecracker_vm_state( } pub(crate) fn mark_quarantined_best_effort( - db: &Arc>, + db: &crate::db::SharedConnection, sandbox_id: &str, expected_state: SandboxState, reason_code: SandboxQuarantineReasonCode, diff --git a/crates/libs/sbx/mgr/server/src/sandbox_eviction.rs b/crates/libs/sbx/mgr/server/src/sandbox_eviction.rs index 89666b80d..d87aaadaa 100644 --- a/crates/libs/sbx/mgr/server/src/sandbox_eviction.rs +++ b/crates/libs/sbx/mgr/server/src/sandbox_eviction.rs @@ -90,7 +90,7 @@ use std::collections::HashMap; use std::path::Path; use std::time::{Duration, Instant}; -use rusqlite::Connection; +use crate::db::Connection; use tokio_util::sync::CancellationToken; use tracing::{info, warn}; use uuid::Uuid; @@ -522,7 +522,7 @@ fn query_candidates_after( idle_cutoff_epoch_secs: u64, cursor: Option<&CandidateCursor>, limit: usize, -) -> rusqlite::Result> { +) -> crate::db::Result> { let cursor_witness = cursor.map(|cursor| i64::try_from(cursor.idle_witness_secs).unwrap_or(i64::MAX)); let cursor_sandbox_id = cursor.map(|cursor| cursor.sandbox_id.as_str()); @@ -555,7 +555,7 @@ pub(crate) fn list_candidates( idle_cutoff_epoch_secs: u64, cursor: Option<&CandidateCursor>, limit: usize, -) -> rusqlite::Result> { +) -> crate::db::Result> { let candidates = query_candidates_after(conn, idle_cutoff_epoch_secs, cursor, limit)?; if candidates.is_empty() && cursor.is_some() { return query_candidates_after(conn, idle_cutoff_epoch_secs, None, limit); @@ -572,7 +572,7 @@ pub(crate) fn list_legacy_reupload_candidates( conn: &Connection, idle_cutoff_epoch_secs: u64, limit: usize, -) -> rusqlite::Result> { +) -> crate::db::Result> { crate::db::eviction_scan::legacy_reupload_candidates(conn, idle_cutoff_epoch_secs, limit) } @@ -591,7 +591,7 @@ pub(crate) fn list_legacy_reupload_candidates( pub(crate) fn gather_local_evidence( conn: &Connection, sandbox_id: &str, -) -> rusqlite::Result> { +) -> crate::db::Result> { let row = crate::db::eviction_scan::artifact_pin_and_updated_at(conn, sandbox_id)?; let Some((pin, updated_at)) = row else { return Ok(None); diff --git a/crates/libs/sbx/mgr/server/src/sandbox_eviction/tests.rs b/crates/libs/sbx/mgr/server/src/sandbox_eviction/tests.rs index a2ac37a8d..4f772da1f 100644 --- a/crates/libs/sbx/mgr/server/src/sandbox_eviction/tests.rs +++ b/crates/libs/sbx/mgr/server/src/sandbox_eviction/tests.rs @@ -1,5 +1,5 @@ +use crate::db::fixture::params; use pplx_space_common_core::types::{LocalArtifactId, SnapshotId}; -use rusqlite::params; use super::*; @@ -1297,10 +1297,10 @@ fn gather_evidence_rejects_malformed_updated_at() { let error = gather_local_evidence(&conn, "sbx-malformed").unwrap_err(); - assert!(matches!( - error, - rusqlite::Error::FromSqlConversionFailure(1, rusqlite::types::Type::Text, _) - )); + assert_eq!( + error.conversion_failure(), + Some((1, crate::db::fixture::Type::Text)) + ); } /// The version-bound claim used by the cluster-deleted eviction RPC: diff --git a/crates/libs/sbx/mgr/server/src/service/activation_support.rs b/crates/libs/sbx/mgr/server/src/service/activation_support.rs index 947a6fe2f..ad6edcc1c 100644 --- a/crates/libs/sbx/mgr/server/src/service/activation_support.rs +++ b/crates/libs/sbx/mgr/server/src/service/activation_support.rs @@ -397,7 +397,7 @@ impl SandboxManagerService { } Some(provenance) } - Err(rusqlite::Error::QueryReturnedNoRows) => None, + Err(error) if error.is_no_rows() => None, Err(error) => { return Err(Status::internal(format!("db error: {error}"))); } @@ -449,7 +449,7 @@ impl SandboxManagerService { let db = self.db_conn()?; match crate::db::activation_route_provenance(&db, sandbox_id) { Ok(provenance) => provenance, - Err(rusqlite::Error::QueryReturnedNoRows) => { + Err(error) if error.is_no_rows() => { return Err( pplx_space_sbx_mgr_client::authoritative_route_identity_required_status( "identity-free activation has no local route provenance; supply an authoritative route identity", @@ -981,7 +981,7 @@ fn activation_env_matches( } pub(super) async fn configure_process_env_for_activation( - db: &Arc>, + db: &crate::db::SharedConnection, sandbox_id: &str, process_env: &BTreeMap, phase: &'static str, @@ -1096,7 +1096,7 @@ fn activation_completion_lost_status(activation_state: SandboxState) -> Status { } fn activation_failure_status_if_current( - db: &Arc>, + db: &crate::db::SharedConnection, sandbox_id: &str, activation_state: SandboxState, cleanup_id: Option, @@ -1142,7 +1142,7 @@ pub(super) fn evaluate_start_gate( } pub(super) async fn wait_ready_and_mark_started( - db: Arc>, + db: crate::db::SharedConnection, sandbox_id: &str, process_env: &BTreeMap, ready_timeout_secs: u32, @@ -1184,7 +1184,7 @@ struct StartReadiness { } async fn complete_start_after_readiness( - db: Arc>, + db: crate::db::SharedConnection, sandbox_id: &str, process_env: &BTreeMap, ready_timeout_secs: u32, @@ -1395,7 +1395,7 @@ fn emit_guest_observed_specs( /// 30s readiness budget followed by the existing 60s network budget. A paused /// restore cannot answer guest RPCs; its guest check runs on resident resume. pub(super) async fn complete_restore_and_mark_final( - db: Arc>, + db: crate::db::SharedConnection, sandbox_id: &str, process_env: &BTreeMap, final_state: SandboxState, @@ -1460,7 +1460,7 @@ pub(super) async fn complete_restore_and_mark_final( } async fn mark_restore_after_network_ready( - db: &Arc>, + db: &crate::db::SharedConnection, sandbox_id: &str, final_state: SandboxState, cleanup_id: Option, @@ -1574,7 +1574,7 @@ pub(super) struct StartStateTransition { pub(super) fn spawn_wait_ready_state_transition( service: &SandboxManagerService, - db: Arc>, + db: crate::db::SharedConnection, transition: StartStateTransition, cleanup_guard: ActivationCleanupGuard, ) -> Result<(), Status> { @@ -1632,7 +1632,7 @@ pub(super) fn spawn_restore_state_transition( mut cleanup_guard: ActivationCleanupGuard, cleanup_id: SandboxCleanupEpisodeId, ) -> Result<(), Status> { - let db = Arc::clone(&service.db); + let db = service.db.clone(); let node_manager = service.node_manager.clone(); let span_sandbox_id = sandbox_id.clone(); service.spawn_detached_state_writer("RestoreFinalizer", &span_sandbox_id, async move { diff --git a/crates/libs/sbx/mgr/server/src/service/activation_support/tests.rs b/crates/libs/sbx/mgr/server/src/service/activation_support/tests.rs index 1f8b3690b..127608028 100644 --- a/crates/libs/sbx/mgr/server/src/service/activation_support/tests.rs +++ b/crates/libs/sbx/mgr/server/src/service/activation_support/tests.rs @@ -76,7 +76,7 @@ async fn restore_completion_requires_gateway_readiness_only_when_starting() { let sandbox_id = SandboxId::new().to_string(); insert_test_sandbox(&conn, &sandbox_id, SandboxState::Restoring); crate::db::set_pool_template(&conn, &sandbox_id, pool_template).unwrap(); - let db = Arc::new(Mutex::new(conn)); + let db = crate::db::SharedConnection::new(conn); // No gateway exists. A restored snapshot alone must not authorize // publishing Started, while a drain restore may remain Paused. @@ -128,7 +128,7 @@ async fn restore_defers_guest_readiness_only_while_paused() { // Exclude the network gate: even a pool restore needs guest support // before Started. No guest exists to answer this synthetic sandbox. crate::db::set_pool_template(&conn, &id, Some("seed")).unwrap(); - let db = Arc::new(Mutex::new(conn)); + let db = crate::db::SharedConnection::new(conn); let result = complete_restore_and_mark_final(db.clone(), &id, &BTreeMap::new(), final_state, None) .await; @@ -173,7 +173,7 @@ async fn start_guest_failure_preserves_terminal_status_and_activation_ownership( SandboxState::Starting }, ); - let db = Arc::new(Mutex::new(conn)); + let db = crate::db::SharedConnection::new(conn); let status = complete_start_after_readiness( db.clone(), &id, @@ -232,7 +232,7 @@ async fn restore_publication_waits_for_network_readiness_and_preserves_activatio ) .unwrap() .unwrap(); - let db = Arc::new(Mutex::new(conn)); + let db = crate::db::SharedConnection::new(conn); let (ready_tx, ready_rx) = tokio::sync::oneshot::channel(); let completion = mark_restore_after_network_ready( &db, @@ -528,7 +528,7 @@ fn locally_built_failures_stay_internal_even_when_terminal_shaped() { #[tokio::test] async fn restore_final_state_db_error_is_propagated() { - let db = Arc::new(Mutex::new(rusqlite::Connection::open_in_memory().unwrap())); + let db = crate::db::SharedConnection::new(crate::db::Connection::open_in_memory()); let result = complete_restore_and_mark_final( db, @@ -551,7 +551,7 @@ async fn restore_final_state_db_error_is_propagated() { #[tokio::test] async fn restore_final_state_requires_a_restoring_row() { let conn = crate::db::test_conn(); - let db = Arc::new(Mutex::new(conn)); + let db = crate::db::SharedConnection::new(conn); let result = complete_restore_and_mark_final( db, @@ -586,7 +586,7 @@ async fn restore_finalizer_retains_cleanup_until_state_persistence_succeeds() { (conn, episode.id) } else { ( - rusqlite::Connection::open_in_memory().unwrap(), + crate::db::Connection::open_in_memory(), SandboxCleanupEpisodeId::new(), ) }; @@ -717,7 +717,7 @@ async fn activation_env_failure_keeps_setup_context_for_callers_and_cleanup() { let cleanup_id = crate::db::cleanup_episode(&conn, &sandbox_id) .unwrap() .map(|episode| episode.id); - let db = Arc::new(Mutex::new(conn)); + let db = crate::db::SharedConnection::new(conn); let (cleanup_tx, cleanup_rx) = std::sync::mpsc::channel(); let mut cleanup = ActivationCleanupGuard::with_interruption_reason( SandboxRuntimeLabel::parse("runtime1").unwrap(), @@ -865,7 +865,7 @@ async fn activation_env_rejects_requested_or_unowned_cleanup() { ) .unwrap(); } - let db = Arc::new(Mutex::new(conn)); + let db = crate::db::SharedConnection::new(conn); for owner in [None, Some(SandboxCleanupEpisodeId::new()), Some(episode.id)] { if !requested && owner == Some(episode.id) { continue; // The setup-context test covers the current owner. diff --git a/crates/libs/sbx/mgr/server/src/service/auth_lookup.rs b/crates/libs/sbx/mgr/server/src/service/auth_lookup.rs index 1aecfdcdf..ee73b6491 100644 --- a/crates/libs/sbx/mgr/server/src/service/auth_lookup.rs +++ b/crates/libs/sbx/mgr/server/src/service/auth_lookup.rs @@ -1010,8 +1010,7 @@ impl SandboxManagerService { recompute_runtime_state(&mut runtime_state, &policy); let db = self.db_conn()?; - let transaction = db - .unchecked_transaction() + let transaction = crate::db::Transaction::deferred(&db) .map_err(|e| Status::internal(format!("db transaction failed: {e}")))?; crate::db::update_auto_lifecycle_with_state( &transaction, @@ -1230,7 +1229,7 @@ impl SandboxManagerService { } fn resident_resume_connect_error( - db: &Arc>, + db: &crate::db::SharedConnection, sandbox_id: &str, error: super::tail_support::NodeManagerConnectError, ) -> Status { @@ -1244,7 +1243,7 @@ fn resident_resume_connect_error( } fn restore_resident_resume_prior_state( - db: &Arc>, + db: &crate::db::SharedConnection, sandbox_id: &str, ) -> Result<(), Status> { let restored = { @@ -1264,7 +1263,7 @@ fn restore_resident_resume_prior_state( } pub(super) fn finish_resident_resume_after_guest_setup( - db: &Arc>, + db: &crate::db::SharedConnection, sandbox_id: &str, setup_result: Result<(), Status>, rollback_vm_state: Option, @@ -1322,7 +1321,7 @@ pub(super) fn finish_resident_resume_after_guest_setup( } pub(super) fn finish_resident_resume_after_rpc_error( - db: &Arc>, + db: &crate::db::SharedConnection, sandbox_id: &str, status: Status, vm_state: Option, diff --git a/crates/libs/sbx/mgr/server/src/service/auth_lookup/resume_begin/tests.rs b/crates/libs/sbx/mgr/server/src/service/auth_lookup/resume_begin/tests.rs index 824b67210..902481bdd 100644 --- a/crates/libs/sbx/mgr/server/src/service/auth_lookup/resume_begin/tests.rs +++ b/crates/libs/sbx/mgr/server/src/service/auth_lookup/resume_begin/tests.rs @@ -90,7 +90,7 @@ fn expected(found: Found) -> Expected { /// A paused warm-pool member whose runtime columns are populated: the test /// process's pid, route generation 3 and execution generation 7. Nothing in /// the resume path probes them before its write. -fn insert_paused_pool_member(conn: &rusqlite::Connection, sandbox_id: SandboxId, scratch: &str) { +fn insert_paused_pool_member(conn: &crate::db::Connection, sandbox_id: SandboxId, scratch: &str) { let id = sandbox_id.to_string(); crate::db::insert_sandbox( conn, @@ -145,7 +145,7 @@ fn insert_paused_pool_member(conn: &rusqlite::Connection, sandbox_id: SandboxId, /// the writers Stop runs, which take neither the wake nor the artifact lock: a /// non-blocking Stop's admission of the paused VM, then its completion. Every /// other state is written directly, and `Missing` deletes the row. -fn move_row(conn: &rusqlite::Connection, id: &str, found: Found) { +fn move_row(conn: &crate::db::Connection, id: &str, found: Found) { let state = match found { Found::Missing => { crate::db::delete_sandbox(conn, id).unwrap(); @@ -165,7 +165,7 @@ fn move_row(conn: &rusqlite::Connection, id: &str, found: Found) { restore_origin = CASE WHEN ?1 = 'restoring' THEN 'local' END, restore_phase = CASE WHEN ?1 = 'restoring' THEN 'activation' END WHERE id = ?2", - rusqlite::params![state.as_str(), id], + crate::db::fixture::params![state.as_str(), id], ) .unwrap(); } diff --git a/crates/libs/sbx/mgr/server/src/service/auth_lookup/tests.rs b/crates/libs/sbx/mgr/server/src/service/auth_lookup/tests.rs index 0eaafa8ed..ff5734424 100644 --- a/crates/libs/sbx/mgr/server/src/service/auth_lookup/tests.rs +++ b/crates/libs/sbx/mgr/server/src/service/auth_lookup/tests.rs @@ -9,7 +9,7 @@ async fn gateway_identity_mapping_preserves_exact_pid_and_lifecycle_authority() "INSERT INTO sandboxes (id,uuid8,state,template,user,runtime,created_at,updated_at) VALUES (?1,'gateway1','started','seed','deploy','fc','1','1')" ), - rusqlite::params![id], + crate::db::fixture::params![id], ) .unwrap(); let owner = 10_042; @@ -63,10 +63,10 @@ fn connection_failure_rolls_back_only_the_admitted_resume() { conn.execute( "INSERT INTO sandboxes (id,uuid8,state,template,user,runtime,created_at,updated_at) VALUES (?1,'sandbox1',?2,'seed','deploy','fc','1','1')", - rusqlite::params![id, state.as_str()], + crate::db::fixture::params![id, state.as_str()], ) .unwrap(); - let db = Arc::new(Mutex::new(conn)); + let db = crate::db::SharedConnection::new(conn); let error = resident_resume_connect_error( &db, &id, diff --git a/crates/libs/sbx/mgr/server/src/service/auth_lookup/wait_ready_policy_tests.rs b/crates/libs/sbx/mgr/server/src/service/auth_lookup/wait_ready_policy_tests.rs index 095d9de72..6a27e71a0 100644 --- a/crates/libs/sbx/mgr/server/src/service/auth_lookup/wait_ready_policy_tests.rs +++ b/crates/libs/sbx/mgr/server/src/service/auth_lookup/wait_ready_policy_tests.rs @@ -7,7 +7,7 @@ use pplx_space_common_svc::space_api_route::{ }; fn service() -> SandboxManagerService { - SandboxManagerService::for_test(rusqlite::Connection::open_in_memory().unwrap()) + SandboxManagerService::for_test(crate::db::Connection::open_in_memory()) } fn node_api_peer() -> AuthorizedUdsPeer { diff --git a/crates/libs/sbx/mgr/server/src/service/auth_lookup_tests.rs b/crates/libs/sbx/mgr/server/src/service/auth_lookup_tests.rs index ddad9fc8a..7c7e5c7b5 100644 --- a/crates/libs/sbx/mgr/server/src/service/auth_lookup_tests.rs +++ b/crates/libs/sbx/mgr/server/src/service/auth_lookup_tests.rs @@ -70,7 +70,7 @@ fn fixture_with_claims( ) .unwrap(); let service = SandboxManagerService::new_with_options( - rusqlite::Connection::open_in_memory().unwrap(), + crate::db::Connection::open_in_memory(), SandboxManagerServiceOptions { route_ed25519_verification_bundle: Some(Arc::new(bundle)), ..SandboxManagerServiceOptions::for_test() diff --git a/crates/libs/sbx/mgr/server/src/service/cache_cleanup.rs b/crates/libs/sbx/mgr/server/src/service/cache_cleanup.rs index 48876adc4..cd5cdc022 100644 --- a/crates/libs/sbx/mgr/server/src/service/cache_cleanup.rs +++ b/crates/libs/sbx/mgr/server/src/service/cache_cleanup.rs @@ -244,7 +244,7 @@ pub(super) enum OrphanedSandboxRowMaterializationError { #[error("cannot inspect sandbox filesystem evidence: {0}")] Filesystem(#[from] std::io::Error), #[error(transparent)] - Sqlite(#[from] rusqlite::Error), + Sqlite(#[from] crate::db::Error), #[error(transparent)] Quarantine(#[from] crate::db::MarkQuarantinedError), #[error(transparent)] @@ -498,7 +498,7 @@ impl CommittedOrphanRowMaterialization { } fn insert_orphaned_sandbox_row_in_transaction( - conn: &rusqlite::Connection, + conn: &crate::db::Connection, sandbox_id: SandboxId, has_cache_root: bool, has_live_runtime: bool, @@ -707,7 +707,7 @@ pub(super) fn reclaim_abandoned_restore_metadata_staging( pub(super) trait InventoryDbAccess { fn with_connection( &self, - operation: impl FnOnce(&rusqlite::Connection) -> T, + operation: impl FnOnce(&crate::db::Connection) -> T, ) -> Result; } @@ -716,7 +716,7 @@ pub enum InventoryError { #[error("inventory database mutex is poisoned")] DatabaseMutexPoisoned, #[error(transparent)] - Database(#[from] rusqlite::Error), + Database(#[from] crate::db::Error), } enum OrphanRowMaterialization { @@ -732,19 +732,19 @@ enum StaleRowInventoryMutation { Database(crate::db::InventoryStaleRowMutation), } -impl InventoryDbAccess for rusqlite::Connection { +impl InventoryDbAccess for crate::db::Connection { fn with_connection( &self, - operation: impl FnOnce(&rusqlite::Connection) -> T, + operation: impl FnOnce(&crate::db::Connection) -> T, ) -> Result { Ok(operation(self)) } } -impl InventoryDbAccess for Mutex { +impl InventoryDbAccess for crate::db::SharedConnection { fn with_connection( &self, - operation: impl FnOnce(&rusqlite::Connection) -> T, + operation: impl FnOnce(&crate::db::Connection) -> T, ) -> Result { let conn = self .lock() @@ -1230,7 +1230,7 @@ where if SandboxInventoryPresence::inspect(paths, *sandbox_id)? != observed_presence { return Ok(OrphanRowMaterialization::InventoryChanged); } - let tx = crate::db::immediate_transaction(conn)?; + let tx = crate::db::Transaction::immediate(conn)?; if crate::db::get_sandbox(&tx, &sandbox_id_string)?.is_some() { return Ok(OrphanRowMaterialization::DatabaseRowAppeared); } @@ -1357,7 +1357,7 @@ pub(super) fn inventory_execution_for_test(_: SandboxId) -> Result rusqlite::Result<()> { + ) -> crate::db::Result<()> { // The caller keeps SQLite locked through retirement, excluding a // same-ID insertion and its new policy obligation. if !crate::db::sandbox_exists(db, sandbox_id)? { diff --git a/crates/libs/sbx/mgr/server/src/service/cache_cleanup/periodic.rs b/crates/libs/sbx/mgr/server/src/service/cache_cleanup/periodic.rs index daf6d759b..8b9e3d873 100644 --- a/crates/libs/sbx/mgr/server/src/service/cache_cleanup/periodic.rs +++ b/crates/libs/sbx/mgr/server/src/service/cache_cleanup/periodic.rs @@ -221,7 +221,7 @@ impl SandboxManagerService { let report = self.apply_route_identity_mutations("periodic_inventory_reconcile", || { let observe_execution = self.orphan_execution_observer(); let report = reconcile_inventory_batch( - self.db.as_ref(), + &self.db, paths, discovery, ReconcileSource::Periodic, diff --git a/crates/libs/sbx/mgr/server/src/service/cache_cleanup/tests.rs b/crates/libs/sbx/mgr/server/src/service/cache_cleanup/tests.rs index 058aa886d..bb352ac89 100644 --- a/crates/libs/sbx/mgr/server/src/service/cache_cleanup/tests.rs +++ b/crates/libs/sbx/mgr/server/src/service/cache_cleanup/tests.rs @@ -4,7 +4,7 @@ use super::*; #[test] fn poisoned_inventory_connection_returns_error_without_running_the_mutation() { - let db = std::sync::Arc::new(std::sync::Mutex::new(crate::db::test_conn())); + let db = crate::db::SharedConnection::new(crate::db::test_conn()); let poison = db.clone(); assert!( std::thread::spawn(move || { @@ -46,7 +46,7 @@ fn orphan_materialization_requires_observed_authority_without_overwriting_a_new_ (false, inventory_execution_for_test(SandboxId::new()), false), (false, inventory_execution_for_test(SandboxId::new()), true), ] { - let db = Mutex::new(crate::db::test_conn()); + let db = crate::db::SharedConnection::new(crate::db::test_conn()); let cache = tempfile::tempdir().unwrap(); let run = tempfile::tempdir().unwrap(); let id = SandboxId::new(); @@ -128,7 +128,7 @@ async fn orphan_authority_reaches_cleanup_and_is_never_refreshed_on_rejection() std::fs::create_dir(&cache).unwrap(); std::fs::create_dir_all(run.join(id.to_string())).unwrap(); std::os::unix::fs::symlink(&cache, run.join(id.to_string()).join("fc.pid")).unwrap(); - let db = Arc::new(Mutex::new(crate::db::test_conn())); + let db = crate::db::SharedConnection::new(crate::db::test_conn()); let receiver = Arc::new(Mutex::new(Err(Status::unavailable("receiver restarting")))); let inspections = Arc::new(std::sync::atomic::AtomicUsize::new(0)); let inspect = tower::service_fn({ @@ -203,7 +203,7 @@ async fn orphan_authority_reaches_cleanup_and_is_never_refreshed_on_rejection() tokio::task::spawn_blocking(move || { let observe = orphan_execution_observer(client, CancellationToken::new()); reconcile_sandbox_inventory_ids( - db.as_ref(), + &db, SandboxInventoryPaths::new(&cache, &run), Vec::new(), vec![id], @@ -315,18 +315,18 @@ fn inventory_does_not_treat_an_unreadable_cache_path_as_absence() { } struct SecondCallMutatingInventoryDb { - conn: std::cell::RefCell, + conn: std::cell::RefCell, calls: std::cell::Cell, mutation: Mutation, } impl InventoryDbAccess for SecondCallMutatingInventoryDb where - Mutation: Fn(&rusqlite::Connection), + Mutation: Fn(&crate::db::Connection), { fn with_connection( &self, - operation: impl FnOnce(&rusqlite::Connection) -> T, + operation: impl FnOnce(&crate::db::Connection) -> T, ) -> Result { let call = self.calls.get(); self.calls.set(call + 1); @@ -483,7 +483,7 @@ async fn inventory_retires_registries_atomically_and_retries_busy_registrations( let wake_guard = wake.lock().await; let reconcile = || { reconcile_sandbox_inventory_ids( - service.db.as_ref(), + &service.db, SandboxInventoryPaths::new(cache.path(), run.path()), Vec::new(), Vec::new(), @@ -599,7 +599,7 @@ fn orphan_materialization_rechecks_filesystem_after_acquiring_database_mutex() { let db = SecondCallMutatingInventoryDb { conn: std::cell::RefCell::new(conn), calls: std::cell::Cell::new(0), - mutation: move |_: &rusqlite::Connection| { + mutation: move |_: &crate::db::Connection| { std::fs::remove_dir_all(&sandbox_root).expect("remove stale filesystem evidence"); }, }; @@ -640,7 +640,7 @@ fn stale_row_rechecks_filesystem_after_acquiring_database_mutex() { let db = SecondCallMutatingInventoryDb { conn: std::cell::RefCell::new(conn), calls: std::cell::Cell::new(0), - mutation: move |_: &rusqlite::Connection| { + mutation: move |_: &crate::db::Connection| { std::fs::create_dir_all(&sandbox_root).expect("restore filesystem evidence"); }, }; @@ -686,7 +686,7 @@ fn nested_materialization_database_failure_is_classified_as_database_error() { let db = SecondCallMutatingInventoryDb { conn: std::cell::RefCell::new(conn), calls: std::cell::Cell::new(0), - mutation: |conn: &rusqlite::Connection| { + mutation: |conn: &crate::db::Connection| { conn.execute_batch("DROP TABLE local_artifacts") .expect("break nested artifact registration"); }, @@ -975,7 +975,7 @@ fn inventory_keeps_a_quarantined_row_without_local_evidence() { // the candidates: skipping the hold before the fence is what keeps a pass // from contending for it. let fenced = std::sync::Mutex::new(Vec::new()); - let reconcile = |conn: &rusqlite::Connection| { + let reconcile = |conn: &crate::db::Connection| { reconcile_sandbox_inventory_ids( conn, paths, diff --git a/crates/libs/sbx/mgr/server/src/service/cleanup.rs b/crates/libs/sbx/mgr/server/src/service/cleanup.rs index 64d5581b6..f15d01c12 100644 --- a/crates/libs/sbx/mgr/server/src/service/cleanup.rs +++ b/crates/libs/sbx/mgr/server/src/service/cleanup.rs @@ -330,7 +330,7 @@ enum QuarantineRecoveryArtifactError { #[error("database mutex poisoned")] DatabaseMutexPoisoned, #[error("load cleanup episode failed: {0}")] - Database(#[from] rusqlite::Error), + Database(#[from] crate::db::Error), #[error("quarantine recovery episode is missing its artifact/generation fence")] MissingRecoveryFence, #[error(transparent)] @@ -375,7 +375,7 @@ pub(super) enum CleanupCompletionError { #[error("database mutex poisoned")] DatabaseMutexPoisoned, #[error("failed to persist cleanup completion")] - Database(#[from] rusqlite::Error), + Database(#[from] crate::db::Error), #[error(transparent)] RowIntent(#[from] crate::db::CompleteRowIntentCleanupError), } @@ -830,7 +830,7 @@ impl SandboxManagerService { } pub(super) fn check_cleanup_selection( - db: &rusqlite::Connection, + db: &crate::db::Connection, sandbox_id: &str, expected_template: Option<&str>, require_template_owner: bool, @@ -992,9 +992,9 @@ impl SandboxManagerService { })?; let transaction = match if mode.requires_immediate_transaction() || require_selection_check { - crate::db::immediate_transaction(&db) + crate::db::Transaction::immediate(&db) } else { - db.unchecked_transaction() + crate::db::Transaction::deferred(&db) } { Ok(transaction) => transaction, Err(error) => { @@ -1449,7 +1449,7 @@ impl SandboxManagerService { } pub(super) async fn dispatch_cleanup_if_generation_current( - db: &Arc>, + db: &crate::db::SharedConnection, sandbox_id: &str, expected_execution_generation: Option, cleanup_fence: &(impl CleanupFenceAuthorization + ?Sized), @@ -1685,7 +1685,7 @@ impl SandboxManagerService { } pub(super) fn cleanup_attempt_still_current( - db: &Arc>, + db: &crate::db::SharedConnection, sandbox_id: &str, completion: CleanupCompletion, ) -> Result { @@ -1711,7 +1711,7 @@ impl SandboxManagerService { } fn async_cleanup_attempt_still_current( - db: &Arc>, + db: &crate::db::SharedConnection, sandbox_id: &str, completion: CleanupCompletion, cleanup_id: Option, @@ -1728,7 +1728,7 @@ impl SandboxManagerService { } fn validate_quarantine_recovery_artifact( - db: &Arc>, + db: &crate::db::SharedConnection, sandbox_id: &str, cleanup_id: SandboxCleanupEpisodeId, verdicts: &crate::retained_source::RetainedSourceVerdicts, @@ -1874,7 +1874,7 @@ impl SandboxManagerService { #[cfg(test)] pub(super) fn complete_async_cleanup( - db: &Arc>, + db: &crate::db::SharedConnection, sandbox_id: &str, completion: CleanupCompletion, published_artifact: Option<(LocalArtifactId, String)>, @@ -1894,7 +1894,7 @@ impl SandboxManagerService { } pub(super) fn complete_cleanup_after_node_release( - db: &Arc>, + db: &crate::db::SharedConnection, sandbox_id: &str, completion: CleanupCompletion, expected_execution_generation: Option, @@ -1915,7 +1915,7 @@ impl SandboxManagerService { } pub(super) fn complete_cleanup_after_node_release_with_hook( - db: &Arc>, + db: &crate::db::SharedConnection, sandbox_id: &str, completion: CleanupCompletion, expected_execution_generation: Option, @@ -1943,7 +1943,7 @@ impl SandboxManagerService { } fn complete_fenced_row_cleanup( - conn: &rusqlite::Connection, + conn: &crate::db::Connection, request: RowCleanupCompletion<'_>, after_generation_check: impl FnOnce(), ) -> Result { @@ -1985,7 +1985,7 @@ impl SandboxManagerService { } pub(super) fn mark_async_cleanup_failed( - db: &Arc>, + db: &crate::db::SharedConnection, sandbox_id: &str, keep_filesystem: bool, completion: CleanupCompletion, @@ -2835,7 +2835,7 @@ pub(super) enum StopFlushError { #[error("cannot read the execution fence: {0}")] Database(#[source] Status), #[error("cannot read the execution fence: {0}")] - Fence(#[source] rusqlite::Error), + Fence(#[source] crate::db::Error), #[error("cannot connect to node-manager: {0}")] NodeManager(#[source] NodeManagerConnectError), #[error("cannot resume the paused VM to flush it: {0}")] diff --git a/crates/libs/sbx/mgr/server/src/service/cleanup/stop_admission_tests.rs b/crates/libs/sbx/mgr/server/src/service/cleanup/stop_admission_tests.rs index 39b79f627..aebaf8876 100644 --- a/crates/libs/sbx/mgr/server/src/service/cleanup/stop_admission_tests.rs +++ b/crates/libs/sbx/mgr/server/src/service/cleanup/stop_admission_tests.rs @@ -40,7 +40,7 @@ const WAIT: Duration = Duration::from_secs(30); /// A test service whose host dependencies resolve under `scratch`, with node /// cleanup held by a test dispatch. fn isolated_service( - conn: rusqlite::Connection, + conn: crate::db::Connection, scratch: &std::path::Path, ) -> SandboxManagerService { let service = SandboxManagerService::new_with_options( @@ -69,7 +69,7 @@ async fn shutdown(service: SandboxManagerService) { } /// A row in `state`, with a local restore's columns when it is `restoring`. -fn insert_row(conn: &rusqlite::Connection, state: SandboxState) -> SandboxId { +fn insert_row(conn: &crate::db::Connection, state: SandboxState) -> SandboxId { let sandbox_id = SandboxId::new(); let id = sandbox_id.to_string(); insert_test_sandbox(conn, &id, state); @@ -292,7 +292,7 @@ async fn sweep_every_source_state(cleanup: Cleanup) { ); assert!( pplx_space_common_jobs::db::find_active_job( - &db, + db.raw(), crate::db::cleanup_jobs::KIND, &id ) diff --git a/crates/libs/sbx/mgr/server/src/service/cleanup_executor.rs b/crates/libs/sbx/mgr/server/src/service/cleanup_executor.rs index 449780eb2..6e2a5f5f5 100644 --- a/crates/libs/sbx/mgr/server/src/service/cleanup_executor.rs +++ b/crates/libs/sbx/mgr/server/src/service/cleanup_executor.rs @@ -9,7 +9,7 @@ use super::*; use crate::db::cleanup_jobs::{ self, CleanupEligibility, CleanupFailure, CleanupOutcome, CleanupWork, }; -use pplx_space_common_jobs::{JobPriority, JobStatus, db as jobs}; +use pplx_space_common_jobs::{JobPriority, JobStatus}; type AttemptResult = Result<(), Status>; type CleanupResult = Result; @@ -261,7 +261,7 @@ impl SandboxManagerService { .db .lock() .map_err(|_| Status::internal("cleanup database poisoned"))?; - let tx = db.unchecked_transaction().map_err(storage_status)?; + let tx = crate::db::Transaction::deferred(&db).map_err(storage_status)?; if request.persist_intent && !crate::db::set_cleanup_intent(&tx, &id, request.keep_filesystem, request.completion) .map_err(storage_status)? @@ -342,7 +342,7 @@ impl SandboxManagerService { .storage(move |db| { let _admission = admission; let _fence = fence; - let tx = db.unchecked_transaction().map_err(storage_status)?; + let tx = crate::db::Transaction::deferred(db).map_err(storage_status)?; let generation = crate::db::runtime_execution_generation(&tx, &sandbox_id.to_string()) .map_err(storage_status)?; @@ -412,7 +412,7 @@ impl SandboxManagerService { loop { match self .storage(|db| { - jobs::reset_running_kind(db, cleanup_jobs::KIND) + cleanup_jobs::reset_running(db) .map_err(|error| Status::internal(error.to_string())) }) .await @@ -492,10 +492,9 @@ impl SandboxManagerService { foreground_first = !foreground_first; let jobs = self .storage(move |db| { - let tx = db.unchecked_transaction().map_err(storage_status)?; + let tx = crate::db::Transaction::deferred(db).map_err(storage_status)?; let mut subjects: Vec<_> = active.into_iter().map(|(id, _)| id).collect(); - jobs::reset_inactive_running_kind(&tx, cleanup_jobs::KIND, &subjects) - .map_err(storage_status)?; + cleanup_jobs::reset_inactive_running(&tx, &subjects).map_err(storage_status)?; let mut selected = Vec::new(); // Neither class can occupy the other's final slot. for (priority, count) in classes { @@ -511,7 +510,9 @@ impl SandboxManagerService { ) .map_err(storage_status)? { - let Some(job) = jobs::get_job(&tx, &id).map_err(storage_status)? else { + let Some(job) = + cleanup_jobs::get_job(&tx, &id).map_err(storage_status)? + else { continue; }; let Some(subject) = job.subject_id.clone() else { @@ -520,7 +521,7 @@ impl SandboxManagerService { if subjects.contains(&subject) { continue; } - if jobs::mark_job_running(&tx, &id).map_err(storage_status)? { + if cleanup_jobs::mark_running(&tx, &id).map_err(storage_status)? { subjects.push(subject.clone()); // `mark_job_running` counted this attempt. let attempt = job.attempts.saturating_add(1); @@ -619,7 +620,7 @@ impl SandboxManagerService { let job_id = id.to_owned(); let selected = self .storage(move |db| { - let job = jobs::get_job(db, &job_id) + let job = cleanup_jobs::get_job(db, &job_id) .map_err(storage_status)? .ok_or_else(|| Status::internal("cleanup job disappeared"))?; if job.status != JobStatus::Running.as_str() { @@ -630,7 +631,7 @@ impl SandboxManagerService { if let CleanupEligibility::Finished(outcome) = cleanup_jobs::eligibility(db, &work).map_err(storage_status)? { - let tx = db.unchecked_transaction().map_err(storage_status)?; + let tx = crate::db::Transaction::deferred(db).map_err(storage_status)?; cleanup_jobs::finish(&tx, &job_id, &outcome).map_err(storage_status)?; tx.commit().map_err(storage_status)?; return Ok(None); @@ -653,7 +654,7 @@ impl SandboxManagerService { // a failed attempt or a completion notification to callers. let job_id = id.to_owned(); self.storage(move |db| { - jobs::yield_running_job(db, &job_id).map_err(storage_status)?; + cleanup_jobs::yield_running(db, &job_id).map_err(storage_status)?; Ok(()) }) .await?; @@ -707,11 +708,11 @@ impl SandboxManagerService { // outcome must already share the lifecycle transaction; // completing it here would reopen the crash ambiguity. if matches!(outcome, CleanupOutcome::Superseded) { - let tx = db.unchecked_transaction().map_err(storage_status)?; + let tx = crate::db::Transaction::deferred(db).map_err(storage_status)?; cleanup_jobs::finish(&tx, &id, outcome).map_err(storage_status)?; tx.commit().map_err(storage_status)?; } else { - let stored = jobs::get_job(db, &id) + let stored = cleanup_jobs::get_job(db, &id) .map_err(storage_status)? .ok_or_else(|| Status::internal("cleanup job disappeared"))?; if stored.status != outcome.job_status().as_str() { @@ -722,7 +723,7 @@ impl SandboxManagerService { } } Err(error) => { - let attempts = jobs::get_job(db, &id) + let attempts = cleanup_jobs::get_job(db, &id) .map_err(storage_status)? .map_or(1, |job| job.attempts); let delay = CLEANUP_RETRY_INITIAL_DELAY_SECS @@ -740,7 +741,8 @@ impl SandboxManagerService { message: error.message().to_owned(), }) .map_err(|error| Status::internal(format!("encode cleanup result: {error}")))?; - jobs::retry_running_job(db, &id, &failure, delay).map_err(storage_status)?; + cleanup_jobs::retry_running(db, &id, &failure, delay) + .map_err(storage_status)?; pplx_space_metrics::sandbox::worker::cleanup_retry(); } } @@ -805,7 +807,7 @@ impl SandboxManagerService { loop { let read_id = id; let stored = self - .storage(move |db| jobs::get_job(db, &read_id).map_err(storage_status)) + .storage(move |db| cleanup_jobs::get_job(db, &read_id).map_err(storage_status)) .await? .ok_or_else(|| Status::internal("cleanup job disappeared"))?; #[cfg(test)] @@ -866,7 +868,7 @@ impl SandboxManagerService { } } -fn storage_status(error: rusqlite::Error) -> Status { +fn storage_status(error: crate::db::Error) -> Status { Status::internal(format!("cleanup storage: {error}")) } diff --git a/crates/libs/sbx/mgr/server/src/service/cleanup_executor/tests.rs b/crates/libs/sbx/mgr/server/src/service/cleanup_executor/tests.rs index 09b0f834b..a94b45605 100644 --- a/crates/libs/sbx/mgr/server/src/service/cleanup_executor/tests.rs +++ b/crates/libs/sbx/mgr/server/src/service/cleanup_executor/tests.rs @@ -5,8 +5,9 @@ use crate::service::cleanup_support::{ CleanupCompletion, CleanupLockGuard, CleanupLockTryResult, DEFAULT_CLEANUP_CONCURRENCY_CAP, }; use crate::service::tests::service_tests::{insert_test_sandbox, service_for_teardown_test}; +use pplx_space_common_jobs::db as jobs; -fn work(conn: &rusqlite::Connection, kill: bool) -> (JobId, CleanupWork) { +fn work(conn: &crate::db::Connection, kill: bool) -> (JobId, CleanupWork) { let sandbox_id = SandboxId::new(); insert_test_sandbox(conn, &sandbox_id.to_string(), SandboxState::Deleting); let work = CleanupWork { @@ -21,7 +22,7 @@ fn work(conn: &rusqlite::Connection, kill: bool) -> (JobId, CleanupWork) { guest_flush: crate::db::cleanup_jobs::GuestFlush::Unrequested, published_artifact: None, }; - let tx = conn.unchecked_transaction().unwrap(); + let tx = crate::db::Transaction::deferred(conn).unwrap(); let id = cleanup_jobs::persist(&tx, work.clone()).unwrap(); tx.commit().unwrap(); (id, work) @@ -129,7 +130,7 @@ async fn stop_waiter_bounds_retries_without_losing_the_durable_job() { ] { let conn = crate::db::test_conn(); let (id, _) = work(&conn, true); - jobs::mark_job_running(&conn, &id).unwrap(); + jobs::mark_job_running(conn.raw(), &id).unwrap(); let failure = if code == Code::Unknown { "legacy cleanup failure".to_owned() } else { @@ -141,14 +142,14 @@ async fn stop_waiter_bounds_retries_without_losing_the_durable_job() { }; match state { JobStatus::Pending => { - jobs::retry_running_job(&conn, &id, &failure, 30).unwrap(); + jobs::retry_running_job(conn.raw(), &id, &failure, 30).unwrap(); } JobStatus::Failed => { - jobs::fail_job_permanent(&conn, &id, &failure, None).unwrap(); + jobs::fail_job_permanent(conn.raw(), &id, &failure, None).unwrap(); } JobStatus::Cancelled => { - jobs::request_job_cancellation(&conn, &id).unwrap(); - jobs::mark_job_cancelled(&conn, &id).unwrap(); + jobs::request_job_cancellation(conn.raw(), &id).unwrap(); + jobs::mark_job_cancelled(conn.raw(), &id).unwrap(); } _ => unreachable!(), } @@ -168,7 +169,7 @@ async fn stop_waiter_bounds_retries_without_losing_the_durable_job() { assert_eq!(original.message(), failure); } assert_eq!( - jobs::get_job(&service.db.lock().unwrap(), &id) + jobs::get_job(service.db.lock().unwrap().raw(), &id) .unwrap() .unwrap() .status, @@ -276,7 +277,7 @@ async fn restarted_claim_retries_without_blocking_other_completions() { let conn = crate::db::test_conn(); let (failed_id, failed) = work(&conn, false); let (successful_id, _) = work(&conn, true); - jobs::mark_job_running(&conn, &failed_id).unwrap(); + jobs::mark_job_running(conn.raw(), &failed_id).unwrap(); let service = service_for_teardown_test(conn); let failed_subject = failed.sandbox_id.to_string(); let (attempted, mut attempts) = tokio::sync::mpsc::channel(2); @@ -304,7 +305,7 @@ async fn restarted_claim_retries_without_blocking_other_completions() { loop { let done = { let db = service.db.lock().unwrap(); - jobs::get_job(&db, &failed_id).unwrap().unwrap().status + jobs::get_job(db.raw(), &failed_id).unwrap().unwrap().status == JobStatus::Pending.as_str() }; if done { @@ -414,10 +415,10 @@ async fn lifecycle_and_job_completion_commit_together_and_recover( .unwrap() .unwrap() .state; - let tx = conn.unchecked_transaction().unwrap(); + let tx = crate::db::Transaction::deferred(&conn).unwrap(); let id = cleanup_jobs::persist(&tx, work).unwrap(); tx.commit().unwrap(); - jobs::mark_job_running(&conn, &id).unwrap(); + jobs::mark_job_running(conn.raw(), &id).unwrap(); conn.execute_batch("CREATE TRIGGER reject_cleanup_completion BEFORE UPDATE ON jobs WHEN NEW.status IN ('completed', 'failed') BEGIN SELECT RAISE(FAIL, 'completion storage unavailable'); END;").unwrap(); let service = service_for_teardown_test(conn); @@ -470,7 +471,7 @@ async fn lifecycle_and_job_completion_commit_together_and_recover( episode ); assert_eq!( - jobs::get_job(&db, &id).unwrap().unwrap().status, + jobs::get_job(db.raw(), &id).unwrap().unwrap().status, JobStatus::Pending.as_str() ); db.execute_batch("DROP TRIGGER reject_cleanup_completion") @@ -500,7 +501,7 @@ async fn lifecycle_and_job_completion_commit_together_and_recover( }; tokio::time::timeout(Duration::from_secs(5), async { loop { - if jobs::get_job(&service.db.lock().unwrap(), &id) + if jobs::get_job(service.db.lock().unwrap().raw(), &id) .unwrap() .unwrap() .status @@ -760,7 +761,7 @@ async fn failed_job_admission_rolls_back_claim_and_preserves_emergency_teardown( ); assert!( jobs::find_active_job( - &service.db.lock().unwrap(), + service.db.lock().unwrap().raw(), cleanup_jobs::KIND, &sandbox_id.to_string() ) @@ -820,7 +821,7 @@ async fn promoted_selected_job_requeues_without_stealing_graceful_capacity() { promoted.immediate_kill = true; { let db = service.db.lock().unwrap(); - let tx = db.unchecked_transaction().unwrap(); + let tx = crate::db::Transaction::deferred(&db).unwrap(); assert_eq!(cleanup_jobs::persist(&tx, promoted).unwrap(), promoted_id); tx.commit().unwrap(); } @@ -841,7 +842,7 @@ async fn promoted_selected_job_requeues_without_stealing_graceful_capacity() { assert!(!waiter.is_finished(), "requeue is not caller completion"); { let db = service.db.lock().unwrap(); - let job = jobs::get_job(&db, &promoted_id).unwrap().unwrap(); + let job = jobs::get_job(db.raw(), &promoted_id).unwrap().unwrap(); assert_eq!(job.status, JobStatus::Pending.as_str()); assert_eq!( job.attempts, 0, @@ -895,14 +896,14 @@ async fn non_execution_outcomes_preserve_durable_authority() { ) .unwrap(); jobs::update_active_job_payload( - &conn, + conn.raw(), &id, &serde_json::to_string(&work).unwrap(), work.priority(), ) .unwrap(); } - jobs::mark_job_running(&conn, &id).unwrap(); + jobs::mark_job_running(conn.raw(), &id).unwrap(); let service = service_for_teardown_test(conn); let subject = work.sandbox_id.to_string(); let fence = CleanupLockGuard::new( @@ -927,8 +928,8 @@ async fn non_execution_outcomes_preserve_durable_authority() { let conn = service.db.lock().unwrap(); match case { "cancelled" => { - jobs::request_job_cancellation(&conn, &id).unwrap(); - jobs::mark_job_cancelled(&conn, &id).unwrap(); + jobs::request_job_cancellation(conn.raw(), &id).unwrap(); + jobs::mark_job_cancelled(conn.raw(), &id).unwrap(); } "replacement_generation" => { conn.execute("UPDATE sandboxes SET space_api_route_execution_generation = 2 WHERE id = ?1", [&subject]).unwrap(); @@ -939,7 +940,7 @@ async fn non_execution_outcomes_preserve_durable_authority() { } else { "quarantined" }; - let update = |conn: &rusqlite::Connection| { + let update = |conn: &crate::db::Connection| { conn.execute( "UPDATE sandboxes SET state = ?1 WHERE id = ?2", [state, &subject], @@ -973,7 +974,7 @@ async fn non_execution_outcomes_preserve_durable_authority() { expected_code, "{case}" ); - let status = jobs::get_job(&service.db.lock().unwrap(), &id) + let status = jobs::get_job(service.db.lock().unwrap().raw(), &id) .unwrap() .unwrap() .status; @@ -984,7 +985,7 @@ async fn non_execution_outcomes_preserve_durable_authority() { .await .unwrap(); assert_eq!( - jobs::get_job(&service.db.lock().unwrap(), &id) + jobs::get_job(service.db.lock().unwrap().raw(), &id) .unwrap() .unwrap() .status, @@ -1007,7 +1008,7 @@ async fn selected_cancellation_cannot_publish_success_after_a_stale_waiter_read( ) .unwrap(); jobs::update_active_job_payload( - &conn, + conn.raw(), &id, &serde_json::to_string(&work).unwrap(), work.priority(), @@ -1048,7 +1049,7 @@ async fn selected_cancellation_cannot_publish_success_after_a_stale_waiter_read( .unwrap(); let replacement_id = { let db = service.db.lock().unwrap(); - let tx = db.unchecked_transaction().unwrap(); + let tx = crate::db::Transaction::deferred(&db).unwrap(); // A different obligation replaces the selected job under its fence. let mut replacement = work; replacement.completion = CleanupCompletion::DeleteRow; @@ -1085,7 +1086,7 @@ async fn selected_cancellation_cannot_publish_success_after_a_stale_waiter_read( service.task_tracker.close(); service.task_tracker.wait().await; assert_eq!( - jobs::get_job(&service.db.lock().unwrap(), &id) + jobs::get_job(service.db.lock().unwrap().raw(), &id) .unwrap() .unwrap() .status, diff --git a/crates/libs/sbx/mgr/server/src/service/cleanup_support.rs b/crates/libs/sbx/mgr/server/src/service/cleanup_support.rs index c2546b8f3..40320508d 100644 --- a/crates/libs/sbx/mgr/server/src/service/cleanup_support.rs +++ b/crates/libs/sbx/mgr/server/src/service/cleanup_support.rs @@ -7,7 +7,7 @@ use pplx_space_events::{ }; pub(super) async fn cleanup_sandbox_via_node( - db: &Arc>, + db: &crate::db::SharedConnection, node_client: &mut NodeManagerGrpcClient, sandbox_id: &str, keep_filesystem: bool, @@ -26,7 +26,7 @@ pub(super) async fn cleanup_sandbox_via_node( } pub(super) async fn cleanup_sandbox_via_node_with_options( - db: &Arc>, + db: &crate::db::SharedConnection, node_client: &mut NodeManagerGrpcClient, sandbox_id: &str, keep_filesystem: bool, @@ -92,7 +92,7 @@ pub(super) async fn cleanup_sandbox_via_node_with_options( } pub(super) fn read_node_execution_fence( - db: &Arc>, + db: &crate::db::SharedConnection, sandbox_id: &str, ) -> Result, Status> { let db = db @@ -103,7 +103,7 @@ pub(super) fn read_node_execution_fence( } pub(super) fn admit_prepared_clone_disposal_attempt( - db: &Arc>, + db: &crate::db::SharedConnection, id: &str, job_id: Option<&JobId>, ) -> Result<(), Status> { @@ -150,7 +150,7 @@ pub(super) struct NodeCleanupOptions<'a> { /// cleanup after persistence errors. Losing the lookup is not permission to /// fall back to ordinary pathname-based cleanup. pub(super) async fn service_cleanup_via_node_with_options( - db: &Arc>, + db: &crate::db::SharedConnection, node_client: &mut NodeManagerGrpcClient, sandbox_id: &str, options: NodeCleanupOptions<'_>, @@ -229,7 +229,7 @@ pub(super) use crate::db::CleanupCompletion; /// reports the state another transition left the row in, and a generic-guard /// refusal or a missing row renders [`crate::db::SandboxStateWriteNotApplied`]. pub(super) fn require_state_write_from( - result: rusqlite::Result, + result: crate::db::Result, context: &str, ) -> Result<(), Status> { let outcome = result.map_err(|error| Status::internal(format!("{context}: {error}")))?; @@ -295,7 +295,7 @@ impl StateWriteRejected { /// marks the sandbox dirty. `None` is a poisoned connection or a /// database error. pub(super) fn update_state_from_best_effort( - db: &Arc>, + db: &crate::db::SharedConnection, sandbox_id: &str, edge: &'static crate::db::Edge, ) -> Option { diff --git a/crates/libs/sbx/mgr/server/src/service/cleanup_support/tests.rs b/crates/libs/sbx/mgr/server/src/service/cleanup_support/tests.rs index 6a2400147..3e3a736b9 100644 --- a/crates/libs/sbx/mgr/server/src/service/cleanup_support/tests.rs +++ b/crates/libs/sbx/mgr/server/src/service/cleanup_support/tests.rs @@ -4,7 +4,7 @@ //! listed by a match without a wildcard, so a new outcome or refusal does not //! compile until it has a status here. -use std::sync::{Arc, Mutex}; +use std::sync::Arc; use pplx_space_common_core::types::{ SandboxId, SandboxQuarantineNextAction, SandboxQuarantineReasonCode, SandboxQuarantineSource, @@ -134,7 +134,7 @@ fn every_state_write_outcome_has_its_rejection_and_status() { fn a_database_error_is_internal() { assert_eq!( code_and_message(require_state_write_from( - Err(rusqlite::Error::InvalidQuery), + Err(crate::db::Error::invalid_query()), CONTEXT, )), Err(( @@ -147,7 +147,7 @@ fn a_database_error_is_internal() { /// A row each generic guard refuses `target` over, or no row. struct GuardedRow { name: &'static str, - seed: fn(&rusqlite::Connection, &str), + seed: fn(&crate::db::Connection, &str), target: SandboxState, rejected: fn(SandboxState) -> StateWriteRejected, message: &'static str, @@ -247,7 +247,7 @@ fn guard_refusals_and_a_missing_row_keep_their_statuses() { } } -fn dirty_sandboxes(conn: &rusqlite::Connection) -> Vec { +fn dirty_sandboxes(conn: &crate::db::Connection) -> Vec { crate::db::list_node_dirty_refs(conn, 100) .unwrap() .into_iter() @@ -258,7 +258,7 @@ fn dirty_sandboxes(conn: &rusqlite::Connection) -> Vec { #[test] fn best_effort_write_applies_and_marks_only_that_sandbox_dirty() { - let db = Arc::new(Mutex::new(crate::db::test_conn())); + let db = crate::db::SharedConnection::new(crate::db::test_conn()); let id = SandboxId::new().to_string(); insert_test_sandbox(&db.lock().unwrap(), &id, SandboxState::Pausing); let before = dirty_sandboxes(&db.lock().unwrap()); @@ -301,7 +301,7 @@ fn best_effort_refusals_change_nothing() { ), ("missing row", None, StateWriteOutcome::Missing), ] { - let db = Arc::new(Mutex::new(crate::db::test_conn())); + let db = crate::db::SharedConnection::new(crate::db::test_conn()); let id = SandboxId::new().to_string(); if let Some(state) = state { insert_test_sandbox(&db.lock().unwrap(), &id, state); @@ -320,7 +320,7 @@ fn best_effort_refusals_change_nothing() { #[test] fn best_effort_database_error_is_none_and_changes_nothing() { let edge: &'static Edge = &edges::ORPHAN_SUSPENDED_INSERT; - let db = Arc::new(Mutex::new(crate::db::test_conn())); + let db = crate::db::SharedConnection::new(crate::db::test_conn()); let id = SandboxId::new().to_string(); insert_test_sandbox(&db.lock().unwrap(), &id, SandboxState::Stopped); let before = snapshot(&db.lock().unwrap()); @@ -337,7 +337,7 @@ fn best_effort_database_error_is_none_and_changes_nothing() { /// does. #[tokio::test(start_paused = true)] async fn best_effort_write_over_a_poisoned_connection_signals_the_failure() { - let db = Arc::new(Mutex::new(crate::db::test_conn())); + let db = crate::db::SharedConnection::new(crate::db::test_conn()); let id = SandboxId::new().to_string(); insert_test_sandbox(&db.lock().unwrap(), &id, SandboxState::Pausing); let before = snapshot(&db.lock().unwrap()); @@ -345,7 +345,7 @@ async fn best_effort_write_over_a_poisoned_connection_signals_the_failure() { let worker_failed = CancellationToken::new(); let tracker = TaskTracker::new(); crate::node_state_sync::start_dirty_outbox_worker( - Arc::clone(&db), + db.clone(), shutdown.clone(), &tracker, CriticalTaskGuard::new( @@ -356,7 +356,7 @@ async fn best_effort_write_over_a_poisoned_connection_signals_the_failure() { ), ); tokio::task::yield_now().await; - let poisoner = Arc::clone(&db); + let poisoner = db.clone(); std::thread::spawn(move || { let _conn = poisoner.lock().unwrap(); panic!("synthetic panic while holding the database"); diff --git a/crates/libs/sbx/mgr/server/src/service/cleanup_targets.rs b/crates/libs/sbx/mgr/server/src/service/cleanup_targets.rs index 8de30f877..1a7f693b4 100644 --- a/crates/libs/sbx/mgr/server/src/service/cleanup_targets.rs +++ b/crates/libs/sbx/mgr/server/src/service/cleanup_targets.rs @@ -210,7 +210,7 @@ impl SandboxManagerService { .clone() .unwrap_or_else(|| PathBuf::from(RUN_DIR)); let cache_dir = self.sandbox_cache_root(); - let db = Arc::clone(&self.db); + let db = self.db.clone(); let template = template.map(str::to_owned); tokio::task::spawn_blocking(move || { let _permit = permit; @@ -234,7 +234,7 @@ impl SandboxManagerService { } fn resolve_lifecycle_target_blocking( - db: &Mutex, + db: &crate::db::SharedConnection, cache_dir: &Path, sandbox_id: SandboxId, template: Option<&str>, diff --git a/crates/libs/sbx/mgr/server/src/service/cleanup_targets/tests.rs b/crates/libs/sbx/mgr/server/src/service/cleanup_targets/tests.rs index 42dc627dc..5934f0765 100644 --- a/crates/libs/sbx/mgr/server/src/service/cleanup_targets/tests.rs +++ b/crates/libs/sbx/mgr/server/src/service/cleanup_targets/tests.rs @@ -79,7 +79,7 @@ async fn selected_cleanup_rechecks_template_and_owner_after_acquiring_admission_ insert_test_sandbox(&conn, &id.to_string(), SandboxState::Started); conn.execute( "UPDATE sandboxes SET user = ?1 WHERE id = ?2", - rusqlite::params![ + crate::db::fixture::params![ SandboxOwnerKind::TemplateManager .reserved_owner_label() .unwrap(), @@ -260,7 +260,7 @@ fn runtime_probe_does_not_hold_database_mutex() { let conn = crate::db::test_conn(); let id = SandboxId::new().to_string(); insert_test_sandbox(&conn, &id, SandboxState::Started); - let db = Mutex::new(conn); + let db = crate::db::SharedConnection::new(conn); let cache = tempfile::tempdir().unwrap(); let targets = resolve_lifecycle_target_blocking( &db, @@ -360,7 +360,7 @@ async fn normalized_selectors_reach_fenced_stop_and_delete_completion() { #[test] fn exact_requests_use_indexed_lookups_including_disk_only_and_absent_targets() { static QUERIES: Mutex> = Mutex::new(Vec::new()); - use rusqlite::trace::{TraceEvent, TraceEventCodes}; + use crate::db::fixture::{TraceEvent, TraceEventCodes}; fn trace(event: TraceEvent<'_>) { if let TraceEvent::Stmt(statement, _) = event { QUERIES @@ -399,7 +399,7 @@ fn exact_requests_use_indexed_lookups_including_disk_only_and_absent_targets() { std::fs::create_dir(cache.path().join(&target)).unwrap(); } conn.trace_v2(TraceEventCodes::SQLITE_TRACE_STMT, Some(trace)); - let db = Mutex::new(conn); + let db = crate::db::SharedConnection::new(conn); for selector in [&target, &target.to_uppercase(), &target.replace('-', "")] { let resolved = resolve_lifecycle_target_blocking( &db, @@ -413,7 +413,7 @@ fn exact_requests_use_indexed_lookups_including_disk_only_and_absent_targets() { .unwrap(); assert_eq!(resolved.is_some(), metadata || disk); } - let conn = db.into_inner().unwrap(); + let conn = db.lock().unwrap(); conn.trace_v2(TraceEventCodes::empty(), None); for sql in QUERIES.lock().unwrap().drain(..) { let mut plan = conn.prepare(&format!("EXPLAIN QUERY PLAN {sql}")).unwrap(); @@ -441,7 +441,7 @@ fn exact_cleanup_rejects_target_metadata_damage_and_database_failure() { [&broken], ) .unwrap(); - let db = Mutex::new(conn); + let db = crate::db::SharedConnection::new(conn); let cache = tempfile::tempdir().unwrap(); for running in [false, true] { let resolve = |id: &str| { @@ -502,13 +502,13 @@ async fn a_stops_guest_flush_is_decided_from_the_state_it_supersedes() { let persisted = { let db = service.db_conn().unwrap(); let job = pplx_space_common_jobs::db::find_active_job( - &db, + db.raw(), cleanup_jobs::KIND, &id.to_string(), ) .unwrap() .expect("the stop's cleanup is persisted"); - let payload = pplx_space_common_jobs::db::get_job(&db, &job) + let payload = pplx_space_common_jobs::db::get_job(db.raw(), &job) .unwrap() .unwrap() .payload; diff --git a/crates/libs/sbx/mgr/server/src/service/create.rs b/crates/libs/sbx/mgr/server/src/service/create.rs index 002df47be..1fbbf6a23 100644 --- a/crates/libs/sbx/mgr/server/src/service/create.rs +++ b/crates/libs/sbx/mgr/server/src/service/create.rs @@ -1350,7 +1350,7 @@ impl SandboxManagerService { let final_state = target_state; if blocking { if let Err(error) = complete_restore_and_mark_final( - Arc::clone(&self.db), + self.db.clone(), id, &sandbox.process_env, final_state, diff --git a/crates/libs/sbx/mgr/server/src/service/create_support.rs b/crates/libs/sbx/mgr/server/src/service/create_support.rs index 09b66e62f..28fb3e046 100644 --- a/crates/libs/sbx/mgr/server/src/service/create_support.rs +++ b/crates/libs/sbx/mgr/server/src/service/create_support.rs @@ -300,7 +300,7 @@ pub(super) fn template_create_hash( /// `creating → created`. Create holds no lock between its insert and this /// write, so a row a Stop moved to `stopping` refuses it, and the Stop keeps /// the row. -pub(super) fn write_create_completed(db: &rusqlite::Connection, id: &str) -> Result<(), Status> { +pub(super) fn write_create_completed(db: &crate::db::Connection, id: &str) -> Result<(), Status> { let edge = &crate::db::edges::CREATE_COMPLETE; require_state_write_from( crate::db::update_state_from(db, id, edge), @@ -312,7 +312,7 @@ pub(super) fn write_create_completed(db: &rusqlite::Connection, id: &str) -> Res /// own `created` write, `created → suspended`, refused like /// [`write_create_completed`]. pub(super) fn write_base_snapshot_create_completed( - db: &rusqlite::Connection, + db: &crate::db::Connection, id: &str, ) -> Result<(), Status> { let edge = &crate::db::edges::CREATE_BASE_SNAPSHOT_COMPLETE; @@ -358,7 +358,10 @@ pub(super) fn admit_warm_start(sandbox: &crate::db::SandboxRow) -> Result<(), St /// A transient row answers `FailedPrecondition`, where a cold Start answers /// `Unavailable`, deliberately: warm start does not wait out a transition, it /// refuses a row it cannot restage. -pub(super) fn write_warm_start_restaged(db: &rusqlite::Connection, id: &str) -> Result<(), Status> { +pub(super) fn write_warm_start_restaged( + db: &crate::db::Connection, + id: &str, +) -> Result<(), Status> { const CONTEXT: &str = "prepare recovered sandbox restore"; let outcome = crate::db::restage_for_warm_start(db, id) .map_err(|error| Status::internal(format!("{CONTEXT}: {error}")))?; @@ -493,7 +496,7 @@ pub(super) fn requested_sandbox_create_auto_lifecycle_changed( } pub(super) fn update_existing_sandbox_create_auto_lifecycle( - conn: &rusqlite::Connection, + conn: &crate::db::Connection, sandbox_id: &str, state: SandboxState, policy: &AutoLifecyclePolicy, @@ -505,8 +508,7 @@ pub(super) fn update_existing_sandbox_create_auto_lifecycle( }; recompute_runtime_state(&mut runtime_state, policy); - let transaction = conn - .unchecked_transaction() + let transaction = crate::db::Transaction::deferred(conn) .map_err(|e| Status::internal(format!("db transaction failed: {e}")))?; crate::db::update_auto_lifecycle_with_state( &transaction, diff --git a/crates/libs/sbx/mgr/server/src/service/create_support/state_write_tests.rs b/crates/libs/sbx/mgr/server/src/service/create_support/state_write_tests.rs index ef511c6fe..ff3415086 100644 --- a/crates/libs/sbx/mgr/server/src/service/create_support/state_write_tests.rs +++ b/crates/libs/sbx/mgr/server/src/service/create_support/state_write_tests.rs @@ -18,11 +18,11 @@ use crate::db::{ CompleteRowIntentCleanupDisposition, }; -fn state(conn: &rusqlite::Connection, id: &str) -> SandboxState { +fn state(conn: &crate::db::Connection, id: &str) -> SandboxState { crate::db::get_sandbox(conn, id).unwrap().unwrap().state } -type Write = fn(&rusqlite::Connection, &str) -> Result<(), Status>; +type Write = fn(&crate::db::Connection, &str) -> Result<(), Status>; /// A writer's expected answer for the row with an id in each source state. type Outcomes = fn(&str, SandboxState) -> Expected; @@ -229,7 +229,7 @@ fn warm_start_restaged(id: &str, state: SandboxState) -> Expected { } } -fn check(case: &str, conn: &rusqlite::Connection, id: &str, write: Write, expected: Expected) { +fn check(case: &str, conn: &crate::db::Connection, id: &str, write: Write, expected: Expected) { let before = snapshot(conn); let result = write(conn, id); match expected { diff --git a/crates/libs/sbx/mgr/server/src/service/gateway_authority/tests.rs b/crates/libs/sbx/mgr/server/src/service/gateway_authority/tests.rs index 7bf47330a..bf7ce3bf8 100644 --- a/crates/libs/sbx/mgr/server/src/service/gateway_authority/tests.rs +++ b/crates/libs/sbx/mgr/server/src/service/gateway_authority/tests.rs @@ -91,7 +91,7 @@ impl Fixture { .unwrap() .execute( "UPDATE sandboxes SET state = ?1 WHERE id = ?2", - rusqlite::params![state.as_str(), self.sandbox_id.to_string()], + crate::db::fixture::params![state.as_str(), self.sandbox_id.to_string()], ) .unwrap(); } @@ -111,7 +111,7 @@ impl Fixture { space_api_route_activation_attempt_id = ?6, space_api_route_identity_provenance = 'unfenced' WHERE id = ?7", - rusqlite::params![ + crate::db::fixture::params![ identity .route_generation .and_then(|generation| i64::try_from(generation.get()).ok()), diff --git a/crates/libs/sbx/mgr/server/src/service/grpc_core/create_after_lookup_tests.rs b/crates/libs/sbx/mgr/server/src/service/grpc_core/create_after_lookup_tests.rs index 97b92fef5..55c8e3ba8 100644 --- a/crates/libs/sbx/mgr/server/src/service/grpc_core/create_after_lookup_tests.rs +++ b/crates/libs/sbx/mgr/server/src/service/grpc_core/create_after_lookup_tests.rs @@ -54,7 +54,7 @@ struct FencedRow { } /// Refuse every new `sandboxes` row, reporting the row in the refusal. -fn install_insert_fence(conn: &rusqlite::Connection) { +fn install_insert_fence(conn: &crate::db::Connection) { conn.execute_batch(&format!( "CREATE TEMP TRIGGER create_after_lookup_insert_fence BEFORE INSERT ON main.sandboxes @@ -165,7 +165,7 @@ impl Fixture { /// A service over `conn`, with the insert fence installed, whose template /// lookups go to `templates` and whose node-manager is `node_manager`. fn new( - conn: rusqlite::Connection, + conn: crate::db::Connection, templates: Arc, node_manager: FakeNodeManager, ) -> Self { @@ -241,7 +241,7 @@ fn create_request() -> CreateRequest { /// Insert a row the way create writes one, with [`row_env`], before the fence /// is installed. fn insert_created_row( - conn: &rusqlite::Connection, + conn: &crate::db::Connection, id: &str, state: SandboxState, template_hash: &str, @@ -446,7 +446,7 @@ async fn autostart_create_checks_ready_network_slots_before_the_row_insert() { } /// A paused pool donor at `build`, as the pool leaves it for adoption. -fn insert_pool_donor(conn: &rusqlite::Connection, build: &str) -> String { +fn insert_pool_donor(conn: &crate::db::Connection, build: &str) -> String { let donor = SandboxId::new(); let id = donor.to_string(); insert_created_row( @@ -628,7 +628,7 @@ async fn requested_id_create_reuses_only_a_compatible_settled_row() { ); conn.execute( "UPDATE sandboxes SET state = ?1 WHERE id = ?2", - rusqlite::params![case.state.as_str(), &id], + crate::db::fixture::params![case.state.as_str(), &id], ) .unwrap(); }); @@ -827,7 +827,7 @@ async fn selected_create_persists_the_binding_used_by_first_start() { let db_path = database.path().join("sandbox.sqlite"); crate::db::create_test_schema_on_disk(&db_path); let mut fixture = Fixture::new( - rusqlite::Connection::open(&db_path).unwrap(), + crate::db::Connection::open_at(&db_path).unwrap(), build.resolver(BUILD), node, ); @@ -847,7 +847,7 @@ async fn selected_create_persists_the_binding_used_by_first_start() { created.environment.as_deref(), Some(definition.reference().to_string().as_str()) ); - let reopened = rusqlite::Connection::open(&db_path).unwrap(); + let reopened = crate::db::Connection::open_at(&db_path).unwrap(); let row = crate::db::get_sandbox(&reopened, &created.sandbox_id) .unwrap() .unwrap(); diff --git a/crates/libs/sbx/mgr/server/src/service/grpc_core/tests.rs b/crates/libs/sbx/mgr/server/src/service/grpc_core/tests.rs index 193800287..ce5cbd4aa 100644 --- a/crates/libs/sbx/mgr/server/src/service/grpc_core/tests.rs +++ b/crates/libs/sbx/mgr/server/src/service/grpc_core/tests.rs @@ -33,7 +33,7 @@ fn enter_isolated_test_process() -> bool { /// A service whose host dependencies all resolve under `scratch`; see /// [`SandboxManagerServiceOptions::for_test_in`]. fn isolated_service( - conn: rusqlite::Connection, + conn: crate::db::Connection, scratch: &std::path::Path, node_role: Option, ) -> SandboxManagerService { @@ -309,12 +309,12 @@ struct CleanupFixture { } impl CleanupFixture { - fn new(conn: rusqlite::Connection, dispatch_permits: usize) -> Self { + fn new(conn: crate::db::Connection, dispatch_permits: usize) -> Self { Self::with_node_role(conn, dispatch_permits, None) } fn with_node_role( - conn: rusqlite::Connection, + conn: crate::db::Connection, dispatch_permits: usize, node_role: Option, ) -> Self { @@ -356,7 +356,7 @@ impl CleanupFixture { fn active_cleanup_job(&self, id: &str) -> Option { pplx_space_common_jobs::db::find_active_job( - &self.service.db_conn().unwrap(), + self.service.db_conn().unwrap().raw(), crate::db::cleanup_jobs::KIND, id, ) @@ -455,7 +455,7 @@ impl CleanupRpc { } } -fn insert_owned_sandbox(conn: &rusqlite::Connection, id: &str, owner: &str) { +fn insert_owned_sandbox(conn: &crate::db::Connection, id: &str, owner: &str) { let sandbox_id = SandboxId::from_str(id).unwrap(); crate::db::insert_sandbox( conn, @@ -974,7 +974,7 @@ async fn auto_lifecycle_delete_requires_one_canonical_id_and_the_expected_policy /// Insert a pool clone whose publication still owns an armed disposal /// episode, as `PoolWaitClone` leaves it before publication completes. fn insert_armed_pool_clone( - conn: &rusqlite::Connection, + conn: &crate::db::Connection, owner: &str, ) -> (SandboxId, SandboxCleanupEpisodeId) { let id = SandboxId::new(); @@ -1308,7 +1308,7 @@ async fn delete_removes_a_quarantine_whose_automatic_recovery_stopped() { // Due by its retry time and still not offered: recovery stopped. conn.execute( "UPDATE sandbox_quarantine_state SET next_action_after = NULL WHERE sandbox_id = ?1", - rusqlite::params![id], + crate::db::fixture::params![id], ) .unwrap(); assert!( diff --git a/crates/libs/sbx/mgr/server/src/service/grpc_core/unpersisted_cleanup_tests.rs b/crates/libs/sbx/mgr/server/src/service/grpc_core/unpersisted_cleanup_tests.rs index d5fe6c0d8..6b98ea8cd 100644 --- a/crates/libs/sbx/mgr/server/src/service/grpc_core/unpersisted_cleanup_tests.rs +++ b/crates/libs/sbx/mgr/server/src/service/grpc_core/unpersisted_cleanup_tests.rs @@ -17,7 +17,7 @@ use pplx_space_node_mgr_client::proto::{CleanupSandboxRequest, CleanupSandboxRes const CLAIM_REFUSAL: &str = "scripted lifecycle claim refusal"; /// Refuse every write of a sandbox's `stopping` or `deleting` claim. -fn refuse_lifecycle_claims(conn: &rusqlite::Connection) { +fn refuse_lifecycle_claims(conn: &crate::db::Connection) { conn.execute_batch(&format!( "CREATE TEMP TRIGGER unpersisted_cleanup_claim_refusal BEFORE UPDATE OF state ON main.sandboxes @@ -190,7 +190,7 @@ async fn unpersisted_stop_and_delete_clean_up_through_node_manager_and_report_th ); assert!( pplx_space_common_jobs::db::find_active_job( - &db, + db.raw(), crate::db::cleanup_jobs::KIND, &id ) diff --git a/crates/libs/sbx/mgr/server/src/service/grpc_lifecycle/backup_policy_tests.rs b/crates/libs/sbx/mgr/server/src/service/grpc_lifecycle/backup_policy_tests.rs index 5688fd9db..b5921a41b 100644 --- a/crates/libs/sbx/mgr/server/src/service/grpc_lifecycle/backup_policy_tests.rs +++ b/crates/libs/sbx/mgr/server/src/service/grpc_lifecycle/backup_policy_tests.rs @@ -63,7 +63,7 @@ const STATIC_TOKEN: &str = "characterization-token"; fn snapshot_upload_jobs(service: &SandboxManagerService, sandbox_id: &str) -> usize { let db = service.db_conn().unwrap(); - pplx_space_common_jobs::db::list_jobs(&db, Some(sandbox_id), None, 0) + pplx_space_common_jobs::db::list_jobs(db.raw(), Some(sandbox_id), None, 0) .unwrap() .into_iter() .filter(|job| job.kind == crate::snapshot_upload_jobs::SNAPSHOT_UPLOAD_JOB_KIND) @@ -660,7 +660,7 @@ fn wire_authority(authority: PolicyAuthority) -> i32 { /// from template-manager. Callers pause the clock so the entry cannot reach /// its refresh deadline mid-test. async fn service_with_template_policy( - conn: rusqlite::Connection, + conn: crate::db::Connection, egress: Egress, generation: u64, ) -> SandboxManagerService { @@ -939,7 +939,7 @@ async fn get_network_policy_reports_each_sandbox_policy_source() { ); conn.execute( "UPDATE sandboxes SET template = ?1 WHERE id = ?2", - rusqlite::params![ + crate::db::fixture::params![ pplx_space_common_core::types::ORPHANED_SANDBOX_TEMPLATE, orphaned ], @@ -1068,9 +1068,13 @@ async fn kill_refuses_signals_it_cannot_deliver_before_cleanup_admission() { let row = crate::db::get_sandbox(&db, id).unwrap(); assert_eq!(row.map(|row| row.state), state, "{case}"); assert!( - pplx_space_common_jobs::db::find_active_job(&db, crate::db::cleanup_jobs::KIND, id) - .unwrap() - .is_none(), + pplx_space_common_jobs::db::find_active_job( + db.raw(), + crate::db::cleanup_jobs::KIND, + id + ) + .unwrap() + .is_none(), "{case}" ); assert!( diff --git a/crates/libs/sbx/mgr/server/src/service/grpc_lifecycle/pause_resume.rs b/crates/libs/sbx/mgr/server/src/service/grpc_lifecycle/pause_resume.rs index 0871472e7..d7fad317c 100644 --- a/crates/libs/sbx/mgr/server/src/service/grpc_lifecycle/pause_resume.rs +++ b/crates/libs/sbx/mgr/server/src/service/grpc_lifecycle/pause_resume.rs @@ -27,7 +27,7 @@ fn superseded(rejected: StateWriteRejected, edge: &Edge) -> bool { /// Roll a `pausing` row back to `started`, best effort. Returns the rejection /// when another operation moved the row out of `pausing`. fn roll_back_pause_best_effort( - db: &Arc>, + db: &crate::db::SharedConnection, sandbox_id: &str, ) -> Option { let edge = &edges::PAUSE_ROLLBACK; @@ -125,7 +125,7 @@ async fn reconcile_state_after_unconfirmed_pause( pub(super) async fn pause_with_reconciliation( node_client: &mut NodeManagerGrpcClient, execution_fence: Option, - db: &Arc>, + db: &crate::db::SharedConnection, sandbox_id: &SandboxId, reason_context: &str, guest_preparation_budget: GuestSnapshotPreparationBudget, diff --git a/crates/libs/sbx/mgr/server/src/service/grpc_lifecycle/pause_resume/supersession_tests.rs b/crates/libs/sbx/mgr/server/src/service/grpc_lifecycle/pause_resume/supersession_tests.rs index 138e790a4..31f0993ca 100644 --- a/crates/libs/sbx/mgr/server/src/service/grpc_lifecycle/pause_resume/supersession_tests.rs +++ b/crates/libs/sbx/mgr/server/src/service/grpc_lifecycle/pause_resume/supersession_tests.rs @@ -151,7 +151,7 @@ enum Interleave { } impl Interleave { - fn apply(self, conn: &rusqlite::Connection, id: &str) { + fn apply(self, conn: &crate::db::Connection, id: &str) { match self { Self::Stop => stop(conn, id), Self::Force(SandboxState::Pausing) => {} @@ -281,7 +281,7 @@ async fn check_pause(exit: Exit, interleave: Interleave, found: SandboxState) { let interleaved = Arc::new(Mutex::new(None::)); let prepare: GuestPreparation = { let (calls, interleaved) = (Arc::clone(&calls), Arc::clone(&interleaved)); - let db = Arc::clone(&service.db); + let db = service.db.clone(); Arc::new(move |sandbox_id| { let call = calls.fetch_add(1, Ordering::SeqCst) + 1; let last = call == exit.interleaved_preparation(); @@ -347,7 +347,7 @@ fn assert_outcome( case: &str, expected: Expected, result: Result, Status>, - db: &rusqlite::Connection, + db: &crate::db::Connection, id: &str, interleaved: &Snapshot, ) { diff --git a/crates/libs/sbx/mgr/server/src/service/grpc_lifecycle/retired_teardown_tests.rs b/crates/libs/sbx/mgr/server/src/service/grpc_lifecycle/retired_teardown_tests.rs index e7cda14c7..ea11fee69 100644 --- a/crates/libs/sbx/mgr/server/src/service/grpc_lifecycle/retired_teardown_tests.rs +++ b/crates/libs/sbx/mgr/server/src/service/grpc_lifecycle/retired_teardown_tests.rs @@ -29,14 +29,14 @@ fn generation(value: i64) -> ExecutionGeneration { } fn insert_upload_job( - conn: &rusqlite::Connection, + conn: &crate::db::Connection, sandbox_id: &str, staged: LocalArtifactId, payload: &str, ) -> JobId { let job_id = JobId::new(); pplx_space_common_jobs::db::insert_job( - conn, + conn.raw(), pplx_space_common_jobs::db::InsertJob { job_id: &job_id, kind: crate::snapshot_upload_jobs::SNAPSHOT_UPLOAD_JOB_KIND, @@ -78,7 +78,7 @@ async fn refused_teardown_with(payload: impl FnOnce(LocalArtifactId) -> String) insert_stranded_teardown_suspend_sandbox(&conn, sandbox_id, generation(GENERATION)); let job_id = insert_upload_job(&conn, &sandbox_id_string, staged, &payload(staged)); pplx_space_common_jobs::db::fail_job_permanent( - &conn, + conn.raw(), &job_id, "cluster-api rejected begin_artifact_backup (FailedPrecondition): execution retired", None, @@ -121,7 +121,7 @@ async fn refused_teardown_with(payload: impl FnOnce(LocalArtifactId) -> String) type Dispatched = Arc>>; /// A service whose cleanup dispatcher records each runtime teardown. -fn service_recording_cleanup(conn: rusqlite::Connection) -> (SandboxManagerService, Dispatched) { +fn service_recording_cleanup(conn: crate::db::Connection) -> (SandboxManagerService, Dispatched) { let service = service_with_runtime_probe(conn, false); let dispatched = Dispatched::default(); let observed = Arc::clone(&dispatched); @@ -284,7 +284,7 @@ async fn release_for_a_row_that_moved_on_records_the_drive_failure() { .unwrap() .execute( "UPDATE sandboxes SET state = 'quarantined' WHERE id = ?1", - rusqlite::params![sandbox_id_string], + crate::db::fixture::params![sandbox_id_string], ) .unwrap(); @@ -389,7 +389,7 @@ fn insert_unstamped_checkpoint( local_artifact_id, sandbox_id, artifact_kind, artifact_origin, snapshot_name, local_path, created_at ) VALUES (?1, ?2, 'full', 'snapshot', 'checkpoint', ?3, '0')", - rusqlite::params![ + crate::db::fixture::params![ named.to_string(), sandbox_id_string, format!("/var/lib/space/sandboxes/{sandbox_id_string}/snapshots/checkpoint"), @@ -407,7 +407,7 @@ fn checkpoint_stamped(service: &SandboxManagerService, artifact: LocalArtifactId .query_row( "SELECT backup_completed_at IS NOT NULL FROM local_artifacts WHERE local_artifact_id = ?1", - rusqlite::params![artifact.to_string()], + crate::db::fixture::params![artifact.to_string()], |row| row.get(0), ) .unwrap() @@ -553,7 +553,7 @@ fn release_claim_marks_a_running_upload_to_finish_cancelled() { let staged = insert_stranded_teardown_suspend_sandbox(&conn, sandbox_id, generation(GENERATION)); let job_id = insert_upload_job(&conn, &sandbox_id_string, staged, "{}"); - assert!(pplx_space_common_jobs::db::mark_job_running(&conn, &job_id).unwrap()); + assert!(pplx_space_common_jobs::db::mark_job_running(conn.raw(), &job_id).unwrap()); assert!( crate::db::claim_retired_teardown_execution_for_release( @@ -564,7 +564,7 @@ fn release_claim_marks_a_running_upload_to_finish_cancelled() { ) .unwrap() ); - let job = pplx_space_common_jobs::db::get_job(&conn, &job_id) + let job = pplx_space_common_jobs::db::get_job(conn.raw(), &job_id) .unwrap() .unwrap(); assert_eq!( @@ -681,7 +681,7 @@ async fn only_an_upload_bound_to_the_generation_is_probed() { ); } pplx_space_common_jobs::db::fail_job_permanent( - &service.db.lock().unwrap(), + service.db.lock().unwrap().raw(), &job_id, "refused", None, @@ -720,7 +720,7 @@ fn release_claim_waits_for_other_local_artifacts_to_back_up() { local_artifact_id, sandbox_id, artifact_kind, artifact_origin, snapshot_name, local_path, created_at ) VALUES (?1, ?2, 'full', 'snapshot', 'checkpoint', ?3, '0')", - rusqlite::params![ + crate::db::fixture::params![ named.to_string(), sandbox_id_string, format!("/var/lib/space/sandboxes/{sandbox_id_string}/snapshots/checkpoint"), @@ -1157,7 +1157,7 @@ async fn direct_refusal_off_its_node_releases_on_the_first_drive() { ); let upload = { let db = service.db.lock().unwrap(); - pplx_space_common_jobs::db::get_job(&db, &upload_job) + pplx_space_common_jobs::db::get_job(db.raw(), &upload_job) .unwrap() .unwrap() }; @@ -1224,7 +1224,7 @@ async fn node_local_suspend_of_a_superseded_execution_rolls_back() { .unwrap() .query_row( "SELECT payload FROM jobs WHERE kind = ?1 AND subject_id = ?2", - rusqlite::params![ + crate::db::fixture::params![ crate::snapshot_upload_jobs::SNAPSHOT_UPLOAD_JOB_KIND, sandbox_id ], @@ -1278,7 +1278,10 @@ async fn backup_rerequested_by_artifact_keeps_its_execution_generation() { .unwrap() .execute( "UPDATE local_artifacts SET local_path = ?1 WHERE local_artifact_id = ?2", - rusqlite::params![snapshot_dir.to_str().unwrap(), refused.staged.to_string()], + crate::db::fixture::params![ + snapshot_dir.to_str().unwrap(), + refused.staged.to_string() + ], ) .unwrap(); @@ -1359,7 +1362,10 @@ async fn rerequested_admitted_backup_keeps_its_execution_generation() { .unwrap() .execute( "UPDATE local_artifacts SET local_path = ?1 WHERE local_artifact_id = ?2", - rusqlite::params![snapshot_dir.to_str().unwrap(), refused.staged.to_string()], + crate::db::fixture::params![ + snapshot_dir.to_str().unwrap(), + refused.staged.to_string() + ], ) .unwrap(); @@ -1441,7 +1447,7 @@ async fn first_request_of_a_staged_capture_carries_the_teardown_generation() { ); conn.execute( "UPDATE local_artifacts SET local_path = ?1 WHERE local_artifact_id = ?2", - rusqlite::params![snapshot_dir.to_str().unwrap(), staged.to_string()], + crate::db::fixture::params![snapshot_dir.to_str().unwrap(), staged.to_string()], ) .unwrap(); // The route names another generation, so only the teardown's @@ -1450,7 +1456,7 @@ async fn first_request_of_a_staged_capture_carries_the_teardown_generation() { "UPDATE sandboxes SET state = ?1, space_api_route_execution_generation = ?2 WHERE id = ?3", - rusqlite::params![state, GENERATION + 92, sandbox_id_string], + crate::db::fixture::params![state, GENERATION + 92, sandbox_id_string], ) .unwrap(); let (service, _dispatched) = service_recording_cleanup(conn); diff --git a/crates/libs/sbx/mgr/server/src/service/grpc_lifecycle/suspend/tests.rs b/crates/libs/sbx/mgr/server/src/service/grpc_lifecycle/suspend/tests.rs index f65798c8d..1f93ac910 100644 --- a/crates/libs/sbx/mgr/server/src/service/grpc_lifecycle/suspend/tests.rs +++ b/crates/libs/sbx/mgr/server/src/service/grpc_lifecycle/suspend/tests.rs @@ -143,7 +143,7 @@ fn expected(exit: Exit, found: SandboxState) -> Expected { /// Move the row to `state` with no other column changed, and install the /// tripwire. The state the auto-pause left at the exit is left as it is. -fn interleave(conn: &rusqlite::Connection, id: &str, exit: Exit, state: SandboxState) -> Snapshot { +fn interleave(conn: &crate::db::Connection, id: &str, exit: Exit, state: SandboxState) -> Snapshot { let left = match exit { Exit::Begin => SandboxState::Started, Exit::Rollback | Exit::Complete => SandboxState::Pausing, @@ -197,7 +197,7 @@ async fn check_suspend(exit: Exit, state: SandboxState) { let interleaved = Arc::new(Mutex::new(None::)); let prepare: GuestPreparation = { let (calls, interleaved) = (Arc::clone(&calls), Arc::clone(&interleaved)); - let db = Arc::clone(&service.db); + let db = service.db.clone(); Arc::new(move |sandbox_id| { calls.fetch_add(1, Ordering::SeqCst); if !matches!(exit, Exit::Begin) { diff --git a/crates/libs/sbx/mgr/server/src/service/grpc_lifecycle/teardown_backup_tests.rs b/crates/libs/sbx/mgr/server/src/service/grpc_lifecycle/teardown_backup_tests.rs index fd57e20e4..87a68e4e3 100644 --- a/crates/libs/sbx/mgr/server/src/service/grpc_lifecycle/teardown_backup_tests.rs +++ b/crates/libs/sbx/mgr/server/src/service/grpc_lifecycle/teardown_backup_tests.rs @@ -99,7 +99,7 @@ async fn teardown_reuses_ready_backup_but_waits_for_active_local_upload() { .unwrap(); if active_upload { pplx_space_common_jobs::db::insert_job( - &conn, + conn.raw(), pplx_space_common_jobs::db::InsertJob { job_id: &JobId::new(), kind: crate::snapshot_upload_jobs::SNAPSHOT_UPLOAD_JOB_KIND, @@ -160,10 +160,15 @@ async fn teardown_reuses_ready_backup_but_waits_for_active_local_upload() { .unwrap(); assert!(status.backup_completed); assert!( - pplx_space_common_jobs::db::list_jobs(&db, Some(&sandbox_id.to_string()), None, 0) - .unwrap() - .iter() - .all(|job| job.kind != crate::snapshot_upload_jobs::SNAPSHOT_UPLOAD_JOB_KIND) + pplx_space_common_jobs::db::list_jobs( + db.raw(), + Some(&sandbox_id.to_string()), + None, + 0 + ) + .unwrap() + .iter() + .all(|job| job.kind != crate::snapshot_upload_jobs::SNAPSHOT_UPLOAD_JOB_KIND) ); } } diff --git a/crates/libs/sbx/mgr/server/src/service/grpc_lifecycle/teardown_suspend.rs b/crates/libs/sbx/mgr/server/src/service/grpc_lifecycle/teardown_suspend.rs index d721540b4..530d8e4ee 100644 --- a/crates/libs/sbx/mgr/server/src/service/grpc_lifecycle/teardown_suspend.rs +++ b/crates/libs/sbx/mgr/server/src/service/grpc_lifecycle/teardown_suspend.rs @@ -342,7 +342,7 @@ impl SandboxManagerService { execution_generation, ) { Ok(_) => {} - Err(rusqlite::Error::InvalidQuery) => { + Err(error) if error.is_invalid_query() => { return Ok(teardown_suspend_prepare_response( TeardownSuspendOutcome::Superseded, Some("a newer local execution owns generation suspend".to_string()), @@ -377,7 +377,7 @@ impl SandboxManagerService { execution_generation, ) { Ok(_) => {} - Err(rusqlite::Error::InvalidQuery) => { + Err(error) if error.is_invalid_query() => { return Ok(teardown_suspend_prepare_response( TeardownSuspendOutcome::Superseded, Some("a newer local execution owns generation suspend".to_string()), diff --git a/crates/libs/sbx/mgr/server/src/service/grpc_lifecycle/tests.rs b/crates/libs/sbx/mgr/server/src/service/grpc_lifecycle/tests.rs index c51eb371e..069fd47bd 100644 --- a/crates/libs/sbx/mgr/server/src/service/grpc_lifecycle/tests.rs +++ b/crates/libs/sbx/mgr/server/src/service/grpc_lifecycle/tests.rs @@ -179,7 +179,7 @@ async fn template_notification_matches_inherited_sandboxes_of_both_authorities() conn.execute( "UPDATE sandboxes SET template = ?1, pool_template = ?2, network_policy_override_json = ?3, network_policy_authority = ?4 WHERE id = ?5", - rusqlite::params![template, pool, override_policy, authority, id], + crate::db::fixture::params![template, pool, override_policy, authority, id], ) .unwrap(); } @@ -728,7 +728,7 @@ async fn retained_source_backup_precondition_follows_node_manager() { assert!(!retry.message().contains(REUPLOAD_ARTIFACT_MISSING_MARKER)); } -fn insert_suspended_test_sandbox(conn: &rusqlite::Connection, sandbox_id: &str) { +fn insert_suspended_test_sandbox(conn: &crate::db::Connection, sandbox_id: &str) { let uuid8 = SandboxRuntimeLabel::parse("sandbox1").unwrap(); let owner = SandboxOwner::parse("user").unwrap(); let process_env = BTreeMap::new(); @@ -1256,7 +1256,7 @@ async fn permanently_refused_teardown_backup_marks_the_suspending_row_terminal() insert_stranded_teardown_suspend_sandbox(&conn, sandbox_id, execution_generation); let backup_job_id = JobId::new(); pplx_space_common_jobs::db::insert_job( - &conn, + conn.raw(), pplx_space_common_jobs::db::InsertJob { job_id: &backup_job_id, kind: crate::snapshot_upload_jobs::SNAPSHOT_UPLOAD_JOB_KIND, @@ -1272,7 +1272,7 @@ async fn permanently_refused_teardown_backup_marks_the_suspending_row_terminal() match failure { pplx_space_common_jobs::JobFailure::Permanent { reason } => { pplx_space_common_jobs::db::fail_job_permanent( - &conn, + conn.raw(), &backup_job_id, "cluster-api rejected BeginArtifactBackup (NotFound): sandbox not found", reason, @@ -1281,7 +1281,7 @@ async fn permanently_refused_teardown_backup_marks_the_suspending_row_terminal() } pplx_space_common_jobs::JobFailure::Retriable => { pplx_space_common_jobs::db::fail_job_retriable_exhausted( - &conn, + conn.raw(), &backup_job_id, "cluster-api BeginArtifactBackup (Unavailable): connection refused", ) @@ -1403,7 +1403,7 @@ async fn teardown_suspend_terminal_driver_failures_stop_polling_without_release_ if let Some(state) = state { conn.execute( "UPDATE sandboxes SET state = ?1 WHERE id = ?2", - rusqlite::params![state.as_str(), id], + crate::db::fixture::params![state.as_str(), id], ) .unwrap(); } else { @@ -1444,7 +1444,7 @@ async fn teardown_suspend_terminal_driver_failures_stop_polling_without_release_ /// A started sandbox whose generation-7 suspend job is running, moved to /// Deleting by the production transition, which closes that job. fn deleting_sandbox_with_closed_teardown_suspend( - conn: &rusqlite::Connection, + conn: &crate::db::Connection, ) -> (String, pplx_space_common_core::types::ExecutionGeneration) { use crate::service::tests::service_tests::insert_test_sandbox; use pplx_space_common_core::types::ExecutionGeneration; @@ -1489,7 +1489,7 @@ fn deleting_sandbox_with_closed_teardown_suspend( /// The production delete: it commits the receipt and removes the row in one /// transaction. -fn commit_sandbox_delete(conn: &rusqlite::Connection, id: &str) { +fn commit_sandbox_delete(conn: &crate::db::Connection, id: &str) { crate::db::set_cleanup_intent(conn, id, false, crate::db::CleanupCompletion::DeleteRow) .unwrap(); crate::db::complete_row_intent_cleanup(conn, id, crate::db::CleanupCompletion::DeleteRow, None) @@ -1934,14 +1934,14 @@ async fn teardown_suspend_pause_failure_recovery_allows_same_generation_retry() for observed_generation in [None, Some(6), Some(8)] { db.execute( "UPDATE sandboxes SET space_api_route_execution_generation = ?1 WHERE id = ?2", - rusqlite::params![observed_generation, id], + crate::db::fixture::params![observed_generation, id], ) .unwrap(); crate::db::begin_teardown_suspend_operation(&db, &id, generation).unwrap_err(); } db.execute( "UPDATE sandboxes SET space_api_route_execution_generation = ?1 WHERE id = ?2", - rusqlite::params![generation.get(), id], + crate::db::fixture::params![generation.get(), id], ) .unwrap(); crate::db::begin_teardown_suspend_operation(&db, &id, generation).unwrap(); @@ -1955,7 +1955,7 @@ async fn teardown_suspend_pause_failure_recovery_allows_same_generation_retry() ); assert_ne!(retry.job_id, original.job_id); assert_eq!( - pplx_space_common_jobs::db::get_job(&db, &original.job_id) + pplx_space_common_jobs::db::get_job(db.raw(), &original.job_id) .unwrap() .unwrap() .status, @@ -1974,7 +1974,7 @@ fn stored_sandbox(service: &SandboxManagerService, id: &str) -> Option>() + .collect::>() .unwrap() } @@ -642,7 +642,7 @@ fn snapshot_publication_refuses_a_catalog_entry_it_does_not_own() { .db .execute( "UPDATE local_artifacts SET state = ?1 WHERE sandbox_id = ?2", - rusqlite::params![state.as_str(), fixture.id], + crate::db::fixture::params![state.as_str(), fixture.id], ) .unwrap(); diff --git a/crates/libs/sbx/mgr/server/src/service/grpc_spaced_process_snapshots/rollback_state.rs b/crates/libs/sbx/mgr/server/src/service/grpc_spaced_process_snapshots/rollback_state.rs index f6a7e4a52..1bc0c5986 100644 --- a/crates/libs/sbx/mgr/server/src/service/grpc_spaced_process_snapshots/rollback_state.rs +++ b/crates/libs/sbx/mgr/server/src/service/grpc_spaced_process_snapshots/rollback_state.rs @@ -2,8 +2,8 @@ //! `sandboxes.state`. use super::super::cleanup_support::{StateWriteRejected, state_write_result}; +use crate::db::Connection; use crate::db::edges; -use rusqlite::Connection; use tonic::Status; /// Restage a full process-snapshot rollback's `stopped` row as `suspended`, so diff --git a/crates/libs/sbx/mgr/server/src/service/grpc_spaced_process_snapshots/tests.rs b/crates/libs/sbx/mgr/server/src/service/grpc_spaced_process_snapshots/tests.rs index 9107b5199..70a496850 100644 --- a/crates/libs/sbx/mgr/server/src/service/grpc_spaced_process_snapshots/tests.rs +++ b/crates/libs/sbx/mgr/server/src/service/grpc_spaced_process_snapshots/tests.rs @@ -345,7 +345,7 @@ fn rollback_replaces_optional_sidecars_or_reports_unremovable_stale_evidence() { } } -fn sandbox_state(conn: &rusqlite::Connection, id: &str) -> SandboxState { +fn sandbox_state(conn: &crate::db::Connection, id: &str) -> SandboxState { crate::db::get_sandbox(conn, id).unwrap().unwrap().state } diff --git a/crates/libs/sbx/mgr/server/src/service/grpc_state_snapshot.rs b/crates/libs/sbx/mgr/server/src/service/grpc_state_snapshot.rs index 575e29bd1..41cb15753 100644 --- a/crates/libs/sbx/mgr/server/src/service/grpc_state_snapshot.rs +++ b/crates/libs/sbx/mgr/server/src/service/grpc_state_snapshot.rs @@ -70,7 +70,7 @@ fn sqlite_timestamp(value: &str) -> Option { #[derive(Debug, thiserror::Error)] enum BuildQuarantineRecoveryObservationError { #[error("read quarantine recovery observation: {0}")] - Read(#[source] rusqlite::Error), + Read(#[source] crate::db::Error), #[error("invalid local quarantine id {value:?}: {source}")] LocalQuarantineId { value: String, diff --git a/crates/libs/sbx/mgr/server/src/service/grpc_state_snapshot/tests.rs b/crates/libs/sbx/mgr/server/src/service/grpc_state_snapshot/tests.rs index 592ebe543..f63023d20 100644 --- a/crates/libs/sbx/mgr/server/src/service/grpc_state_snapshot/tests.rs +++ b/crates/libs/sbx/mgr/server/src/service/grpc_state_snapshot/tests.rs @@ -147,7 +147,7 @@ async fn candidate_snapshot_admission_uses_durable_execution_before_checkpointin insert_test_sandbox(&conn, &id, SandboxState::Paused); conn.execute( "UPDATE sandboxes SET user = ?1 WHERE id = ?2", - rusqlite::params![ + crate::db::fixture::params![ SandboxOwnerKind::TemplateManager .reserved_owner_label() .unwrap(), @@ -189,11 +189,11 @@ async fn candidate_snapshot_admission_uses_durable_execution_before_checkpointin ); } -fn sandbox_state(conn: &rusqlite::Connection, id: &str) -> SandboxState { +fn sandbox_state(conn: &crate::db::Connection, id: &str) -> SandboxState { crate::db::get_sandbox(conn, id).unwrap().unwrap().state } -fn sandbox_inventory_dirty(conn: &rusqlite::Connection, id: &str) -> bool { +fn sandbox_inventory_dirty(conn: &crate::db::Connection, id: &str) -> bool { crate::db::list_node_dirty_refs(conn, 1000) .unwrap() .iter() @@ -221,7 +221,7 @@ fn template_base_snapshot_writes_leave_a_stop_that_completed_during_capture_stop crate::db::tests::state_write_fixtures::stop(&conn, &id); assert_eq!(sandbox_state(&conn, &id), SandboxState::Stopped); let before = crate::db::tests::database_rows(&conn); - let db = Arc::new(Mutex::new(conn)); + let db = crate::db::SharedConnection::new(conn); if completion { let error = complete_template_base_snapshot_state(&db.lock().unwrap(), &id) @@ -291,7 +291,7 @@ fn template_base_snapshot_writes_return_only_a_checkpointing_row_to_paused() { ); crate::db::tests::state_write_fixtures::decorate(&conn, &id, SandboxState::Started); let before = crate::db::tests::database_rows(&conn); - let db = Arc::new(Mutex::new(conn)); + let db = crate::db::SharedConnection::new(conn); let result = if completion { complete_template_base_snapshot_state(&db.lock().unwrap(), &id) diff --git a/crates/libs/sbx/mgr/server/src/service/host_control/tests.rs b/crates/libs/sbx/mgr/server/src/service/host_control/tests.rs index 0f55eec0b..a135f7d21 100644 --- a/crates/libs/sbx/mgr/server/src/service/host_control/tests.rs +++ b/crates/libs/sbx/mgr/server/src/service/host_control/tests.rs @@ -83,7 +83,7 @@ fn policy_request() -> Request { Request::new(GetNetworkPolicyRequest {}) } -fn set_generation(conn: &rusqlite::Connection, sandbox_id: SandboxId, generation: Option) { +fn set_generation(conn: &crate::db::Connection, sandbox_id: SandboxId, generation: Option) { crate::db::update_space_api_route_identity( conn, sandbox_id, diff --git a/crates/libs/sbx/mgr/server/src/service/mod.rs b/crates/libs/sbx/mgr/server/src/service/mod.rs index e58d6610e..f141d0e22 100644 --- a/crates/libs/sbx/mgr/server/src/service/mod.rs +++ b/crates/libs/sbx/mgr/server/src/service/mod.rs @@ -5,7 +5,7 @@ use std::path::{Path, PathBuf}; use std::pin::Pin; use std::str::FromStr; use std::sync::atomic::{AtomicBool, AtomicUsize, Ordering}; -use std::sync::{Arc, Mutex, MutexGuard}; +use std::sync::{Arc, Mutex}; use std::time::{Duration, Instant, SystemTime}; use arc_swap::ArcSwapOption; @@ -351,7 +351,7 @@ impl Drop for TeardownSuspendDriverSlot { #[derive(Clone)] pub struct SandboxManagerService { - db: Arc>, + db: crate::db::SharedConnection, local_node_id: Option, local_artifacts: crate::local_artifacts::LocalArtifacts, watchdog: Arc, @@ -922,7 +922,7 @@ fn optional_identity_field_is_enrichment(current: &Option, incoming: & } fn persisted_activation_owns_route_identity( - db: &rusqlite::Connection, + db: &crate::db::Connection, sandbox_id: SandboxId, cleanup_id: SandboxCleanupEpisodeId, incoming: &RouteStreamEpochIdentity, @@ -948,7 +948,7 @@ fn persisted_activation_owns_route_identity( && sandbox.cleanup_keep_filesystem == Some(episode.keep_filesystem)) } -fn map_runtime_fields_update_error(error: rusqlite::Error) -> Status { +fn map_runtime_fields_update_error(error: crate::db::Error) -> Status { if crate::db::auto_lifecycle_delete_receipts::is_pending_receipt_execution_fence(&error) { return Status::aborted("pending auto-lifecycle delete receipt owns the sandbox execution"); } @@ -956,14 +956,14 @@ fn map_runtime_fields_update_error(error: rusqlite::Error) -> Status { return Status::aborted("sandbox lifecycle or cleanup owns the sandbox execution"); } match error { - rusqlite::Error::InvalidQuery => Status::aborted("runtime execution generation is stale"), - rusqlite::Error::QueryReturnedNoRows => Status::not_found("sandbox not found"), + e if e.is_invalid_query() => Status::aborted("runtime execution generation is stale"), + e if e.is_no_rows() => Status::not_found("sandbox not found"), error => Status::internal(format!("db error: {error}")), } } fn route_identity_adoption_write_rejection_reason( - error: &rusqlite::Error, + error: &crate::db::Error, ) -> pplx_space_metrics::sandbox::worker::RouteIdentityAdoptionReason { use pplx_space_metrics::sandbox::worker::RouteIdentityAdoptionReason; if crate::db::auto_lifecycle_delete_receipts::is_pending_receipt_execution_fence(error) { @@ -972,8 +972,8 @@ fn route_identity_adoption_write_rejection_reason( RouteIdentityAdoptionReason::LifecycleOwnsExecution } else { match error { - rusqlite::Error::InvalidQuery => RouteIdentityAdoptionReason::StaleOrConflict, - rusqlite::Error::QueryReturnedNoRows => RouteIdentityAdoptionReason::NotFound, + error if error.is_invalid_query() => RouteIdentityAdoptionReason::StaleOrConflict, + error if error.is_no_rows() => RouteIdentityAdoptionReason::NotFound, _ => RouteIdentityAdoptionReason::Internal, } } @@ -1058,11 +1058,11 @@ pub enum RefundInterruptedSnapshotUploadAttemptsError { #[error("sandbox database mutex is poisoned")] DatabaseMutexPoisoned, #[error("failed to refund interrupted snapshot upload attempts: {0}")] - Database(#[from] rusqlite::Error), + Database(#[from] crate::db::Error), } impl SandboxManagerService { - pub(crate) fn db_conn(&self) -> Result, Status> { + pub(crate) fn db_conn(&self) -> Result, Status> { self.db.lock().map_err(|_| { // Poison is not recoverable connection health. Keep ownership // through the normal drain and let the supervisor restart us. @@ -1635,14 +1635,14 @@ impl SandboxManagerService { } pub fn new_with_options( - conn: rusqlite::Connection, + conn: crate::db::Connection, options: SandboxManagerServiceOptions, ) -> Self { Self::new_with_config(conn, options, crate::config::SandboxConfig::default()) } pub fn new_with_config( - conn: rusqlite::Connection, + conn: crate::db::Connection, options: SandboxManagerServiceOptions, config: crate::config::SandboxConfig, ) -> Self { @@ -1668,7 +1668,7 @@ impl SandboxManagerService { ), } } - let db = Arc::new(Mutex::new(conn)); + let db = crate::db::SharedConnection::new(conn); let auto_lifecycle = AutoLifecycleCoordinator::default(); let network_policy_reconcile_queue = NetworkPolicyReconcileQueue::default(); let feature_flags = pplx_space_feature_flags::FeatureFlagClient::from_env(); @@ -1693,7 +1693,7 @@ impl SandboxManagerService { ), ); let watchdog = Arc::new(crate::watchdog::SandboxWatchdog::new( - Arc::clone(&db), + db.clone(), network_policy_reconcile_queue.clone(), snapshot_scheduler.clone(), auto_lifecycle.clone(), @@ -1708,11 +1708,11 @@ impl SandboxManagerService { watchdog.set_expected_route_verification_key_generation(generation); } let snapshot_upload_worker = crate::snapshot_upload_jobs::build_worker( - Arc::clone(&db), + db.clone(), Arc::clone(&drain_state), options.node_manager.clone(), ); - let local_artifacts = crate::local_artifacts::LocalArtifacts::new(Arc::clone(&db)); + let local_artifacts = crate::local_artifacts::LocalArtifacts::new(db.clone()); let teardown_suspend_driver_concurrency = TEARDOWN_SUSPEND_DRIVER_CONCURRENCY; pplx_space_metrics::sandbox::worker::teardown_suspend_driver_capacity( teardown_suspend_driver_concurrency, @@ -1830,7 +1830,7 @@ impl SandboxManagerService { /// RPC path can acquire a fresh local-artifact lease. pub fn recover_local_artifact_stuck_states( &self, - ) -> rusqlite::Result { + ) -> crate::db::Result { self.local_artifacts.recover_stuck_states() } @@ -1848,7 +1848,7 @@ impl SandboxManagerService { shutdown: tokio_util::sync::CancellationToken, tracker: &tokio_util::task::TaskTracker, ) { - let db = Arc::clone(&self.db); + let db = self.db.clone(); let worker = self.snapshot_upload_worker.clone(); let drain = Arc::clone(&self.drain_state); let critical = self.critical_task_guard("snapshot-upload-maintenance", &shutdown); @@ -1876,7 +1876,7 @@ impl SandboxManagerService { shutdown_token: tokio_util::sync::CancellationToken, task_tracker: &tokio_util::task::TaskTracker, ) { - let db = Arc::clone(&self.db); + let db = self.db.clone(); let artifacts = self.local_artifacts.clone(); let active_fork_copy_operations = Arc::clone(&self.active_fork_copy_operations); let cfg = crate::snapshot_gc::GcConfig::from_env(); @@ -1916,7 +1916,7 @@ impl SandboxManagerService { task_tracker: &tokio_util::task::TaskTracker, ) { task_tracker.spawn(crate::lifecycle_metrics::run( - Arc::clone(&self.db), + self.db.clone(), shutdown_token, )); } diff --git a/crates/libs/sbx/mgr/server/src/service/network_runtime.rs b/crates/libs/sbx/mgr/server/src/service/network_runtime.rs index 6a282f685..47d937366 100644 --- a/crates/libs/sbx/mgr/server/src/service/network_runtime.rs +++ b/crates/libs/sbx/mgr/server/src/service/network_runtime.rs @@ -477,7 +477,7 @@ pub(super) async fn launch_net_gateway_via_node( /// the write, so task cancellation cannot observe a launched-but-unregistered /// gateway from this helper. pub(super) async fn register_net_gateway_launch( - db: &Arc>, + db: &crate::db::SharedConnection, sandbox_id: &str, uid: u32, launch_netgtw: N, @@ -501,7 +501,7 @@ where /// RPCs and backoff. A shorter caller deadline still wins. Channels are reused /// between polls; the gateway owns all configured readiness dependencies. pub(super) async fn refresh_network_policy( - db: &Arc>, + db: &crate::db::SharedConnection, sandbox_id: &str, expected_policy_ref: Option<&EffectiveNetworkPolicyRef>, reason: &'static str, @@ -524,7 +524,7 @@ pub(super) async fn refresh_network_policy( } pub(super) async fn refresh_network_policy_with_request( - db: &Arc>, + db: &crate::db::SharedConnection, sandbox_id: &str, expected_policy_ref: Option<&EffectiveNetworkPolicyRef>, reason: &'static str, @@ -721,7 +721,7 @@ pub(super) enum RefreshAbandonReason { /// keeps retrying: a transient DB error must not abandon a refresh the gateway /// could still confirm. fn refresh_abandon_reason( - db: &Arc>, + db: &crate::db::SharedConnection, sandbox_id: &str, ) -> Result, crate::node_state_sync::DatabaseMutexPoisoned> { let db = crate::node_state_sync::connection_or_signal_failure(db)?; @@ -743,7 +743,7 @@ fn refresh_abandon_reason( } pub(super) fn spawn_network_policy_refresh( - db: Arc>, + db: crate::db::SharedConnection, sandbox_id: String, expected_policy_ref: EffectiveNetworkPolicyRef, reason: &'static str, diff --git a/crates/libs/sbx/mgr/server/src/service/network_runtime/tests.rs b/crates/libs/sbx/mgr/server/src/service/network_runtime/tests.rs index 771480aab..e2a3f9b86 100644 --- a/crates/libs/sbx/mgr/server/src/service/network_runtime/tests.rs +++ b/crates/libs/sbx/mgr/server/src/service/network_runtime/tests.rs @@ -339,7 +339,7 @@ async fn refresh_waits_for_delayed_readiness_but_bounds_pending_and_hung_request use super::super::tests::service_tests::insert_test_sandbox; use pplx_space_common_core::types::SandboxId; use pplx_space_net_gateway_client::proto::{NetworkState, RefreshNetworkPolicyResponse}; - use std::sync::{Arc, Mutex}; + use std::time::Duration; for (ready_after_secs, request_delay_secs, matching_ref, expect_ready) in [ @@ -356,7 +356,7 @@ async fn refresh_waits_for_delayed_readiness_but_bounds_pending_and_hung_request let conn = crate::db::test_conn(); let sandbox_id = SandboxId::new().to_string(); insert_test_sandbox(&conn, &sandbox_id, SandboxState::Restoring); - let db = Arc::new(Mutex::new(conn)); + let db = crate::db::SharedConnection::new(conn); let expected_ref = format!( "cluster:sandbox:{sandbox_id}:generation:1:sha256:{}", "a".repeat(64) @@ -422,14 +422,14 @@ async fn refresh_rechecks_deletion_after_rpc_and_honors_a_shorter_caller_deadlin use super::super::tests::service_tests::insert_test_sandbox; use pplx_space_common_core::types::SandboxId; use pplx_space_net_gateway_client::proto::{NetworkState, RefreshNetworkPolicyResponse}; - use std::sync::{Arc, Mutex}; + use std::time::Duration; for delete_during_rpc in [false, true] { let conn = crate::db::test_conn(); let sandbox_id = SandboxId::new().to_string(); insert_test_sandbox(&conn, &sandbox_id, SandboxState::Restoring); - let db = Arc::new(Mutex::new(conn)); + let db = crate::db::SharedConnection::new(conn); let started = tokio::time::Instant::now(); let result = tokio::time::timeout( super::NETWORK_POLICY_SYNC_REFRESH_TIMEOUT, @@ -467,7 +467,7 @@ async fn refresh_rechecks_deletion_after_rpc_and_honors_a_shorter_caller_deadlin #[tokio::test(start_paused = true)] async fn refresh_abandons_schedule_when_sandbox_row_is_gone() { let conn = crate::db::test_conn(); - let db = std::sync::Arc::new(std::sync::Mutex::new(conn)); + let db = crate::db::SharedConnection::new(conn); let started = tokio::time::Instant::now(); let error = super::refresh_network_policy( @@ -772,13 +772,13 @@ async fn unavailable_interception_dependencies_still_converge_the_start_gate() { use pplx_space_net_gateway_client::proto::{ InterceptionReadiness, NetworkState, RefreshNetworkPolicyResponse, }; - use std::sync::{Arc, Mutex}; + use std::time::Duration; let conn = crate::db::test_conn(); let sandbox_id = SandboxId::new().to_string(); insert_test_sandbox(&conn, &sandbox_id, SandboxState::Restoring); - let db = Arc::new(Mutex::new(conn)); + let db = crate::db::SharedConnection::new(conn); let expected_ref = format!( "cluster:sandbox:{sandbox_id}:generation:1:sha256:{}", "a".repeat(64) diff --git a/crates/libs/sbx/mgr/server/src/service/reconcile.rs b/crates/libs/sbx/mgr/server/src/service/reconcile.rs index bc30b1c05..2b7b56b3d 100644 --- a/crates/libs/sbx/mgr/server/src/service/reconcile.rs +++ b/crates/libs/sbx/mgr/server/src/service/reconcile.rs @@ -899,7 +899,7 @@ impl SandboxManagerService { ) { let guard = self.critical_task_guard("node dirty outbox", &shutdown_token); crate::node_state_sync::start_dirty_outbox_worker( - Arc::clone(&self.db), + self.db.clone(), shutdown_token, task_tracker, guard, diff --git a/crates/libs/sbx/mgr/server/src/service/reconcile/tests.rs b/crates/libs/sbx/mgr/server/src/service/reconcile/tests.rs index 6eec651eb..262f054f4 100644 --- a/crates/libs/sbx/mgr/server/src/service/reconcile/tests.rs +++ b/crates/libs/sbx/mgr/server/src/service/reconcile/tests.rs @@ -12,7 +12,7 @@ async fn dirty_outbox_poison_requests_fatal_service_drain_without_unwinding_call ..SandboxManagerServiceOptions::for_test() }, ); - let db = Arc::clone(&service.db); + let db = service.db.clone(); assert!( std::thread::spawn(move || { let _guard = db.lock().unwrap(); @@ -283,13 +283,14 @@ async fn auto_lifecycle_error_takes_the_storage_keyed_wake_lock() { #[tokio::test] async fn auto_lifecycle_error_after_row_removal_does_not_scan_inventory() { - use rusqlite::trace::{TraceEvent, TraceEventCodes}; + use crate::db::fixture::{TraceEvent, TraceEventCodes}; use std::sync::atomic::{AtomicUsize, Ordering}; static STEPS: AtomicUsize = AtomicUsize::new(0); fn trace(event: TraceEvent<'_>) { if let TraceEvent::Profile(statement, _) = event { STEPS.fetch_add( - usize::try_from(statement.get_status(rusqlite::StatementStatus::VmStep)).unwrap(), + usize::try_from(statement.get_status(crate::db::fixture::StatementStatus::VmStep)) + .unwrap(), Ordering::Relaxed, ); } @@ -805,7 +806,7 @@ const STOP_WRITE_WAIT: Duration = Duration::from_secs(30); async fn stop_suspended_with_concurrent_write( service: &SandboxManagerService, id: &str, - move_row: impl FnOnce(&rusqlite::Connection), + move_row: impl FnOnce(&crate::db::Connection), ) -> ( Result<(), Status>, crate::db::tests::state_write_fixtures::Snapshot, @@ -917,7 +918,7 @@ async fn stop_admitted_during_auto_lifecycle_stop_keeps_its_completion() { let (result, before) = stop_suspended_with_concurrent_write(&service, &id, |db| { // Stop admission's state write. A blocking Stop persists no intent. - let admission = crate::db::immediate_transaction(db).unwrap(); + let admission = crate::db::Transaction::immediate(db).unwrap(); crate::db::update_state_superseding_activation_in_transaction( &admission, &id, @@ -967,7 +968,7 @@ async fn delete_admitted_during_auto_lifecycle_stop_keeps_its_row() { let (result, before) = stop_suspended_with_concurrent_write(&service, &id, |db| { // A non-blocking Delete's admission transaction. - let admission = crate::db::immediate_transaction(db).unwrap(); + let admission = crate::db::Transaction::immediate(db).unwrap(); crate::db::update_state_superseding_activation_in_transaction( &admission, &id, diff --git a/crates/libs/sbx/mgr/server/src/service/recovery.rs b/crates/libs/sbx/mgr/server/src/service/recovery.rs index 3382a1d12..e942a460b 100644 --- a/crates/libs/sbx/mgr/server/src/service/recovery.rs +++ b/crates/libs/sbx/mgr/server/src/service/recovery.rs @@ -186,7 +186,7 @@ pub enum StartupRecoveryError { #[error("sandbox database mutex is poisoned")] DatabaseMutexPoisoned, #[error("startup recovery database operation failed: {0}")] - Database(#[from] rusqlite::Error), + Database(#[from] crate::db::Error), #[error("failed to inspect interrupted suspend snapshot publication: {0}")] SuspendSnapshot(#[from] suspend_snapshot_publication::SuspendSnapshotPublicationError), #[error("failed to quarantine sandbox during startup recovery: {0}")] @@ -1289,7 +1289,7 @@ impl SandboxManagerService { pub(super) fn startup_recovery_db( &self, - ) -> Result, StartupRecoveryError> { + ) -> Result, StartupRecoveryError> { self.db_conn() .map_err(|_| StartupRecoveryError::DatabaseMutexPoisoned) } @@ -2055,7 +2055,7 @@ impl SandboxManagerService { } pub(super) fn recover_required_cleanup_targets( - db: &Arc>, + db: &crate::db::SharedConnection, authority: CleanupRecoveryAuthority, ) -> Result, StartupRecoveryError> { let rows = { @@ -2069,7 +2069,7 @@ impl SandboxManagerService { } fn recover_required_cleanup_targets_for_ids( - db: &Arc>, + db: &crate::db::SharedConnection, sandbox_ids: Vec, ) -> Result, StartupRecoveryError> { let rows = { @@ -2151,7 +2151,7 @@ impl SandboxManagerService { } fn recover_required_cleanup_targets_from_rows( - db: &Arc>, + db: &crate::db::SharedConnection, rows: Vec, authority: CleanupRecoveryAuthority, ) -> Result, StartupRecoveryError> { diff --git a/crates/libs/sbx/mgr/server/src/service/recovery/staging_cleanup/tests.rs b/crates/libs/sbx/mgr/server/src/service/recovery/staging_cleanup/tests.rs index 011d7bfa2..e80a4f8d0 100644 --- a/crates/libs/sbx/mgr/server/src/service/recovery/staging_cleanup/tests.rs +++ b/crates/libs/sbx/mgr/server/src/service/recovery/staging_cleanup/tests.rs @@ -261,7 +261,7 @@ async fn sweep_owns_the_hold_of_a_rolled_back_publication() { current_local_artifact_kind = 'full', current_local_artifact_origin = 'suspend' WHERE id = ?1", - rusqlite::params![pinned, old_id.to_string()], + crate::db::fixture::params![pinned, old_id.to_string()], ) .unwrap(); @@ -306,7 +306,7 @@ async fn sweep_owns_the_hold_of_a_rolled_back_publication() { current_local_artifact_kind = 'full', current_local_artifact_origin = 'suspend' WHERE id = ?1", - rusqlite::params![interrupted, old_id.to_string()], + crate::db::fixture::params![interrupted, old_id.to_string()], ) .unwrap(); let interrupted_dir = cache.path().join(&interrupted); @@ -370,7 +370,7 @@ async fn sweep_keeps_holds_without_durable_supersession() { current_local_artifact_kind = 'full', current_local_artifact_origin = 'suspend' WHERE id = ?1", - rusqlite::params![id, a.to_string()], + crate::db::fixture::params![id, a.to_string()], ) .unwrap(); } @@ -428,7 +428,7 @@ async fn sweep_leaves_holds_alone_while_a_publication_may_be_in_flight() { current_local_artifact_kind = 'full', current_local_artifact_origin = 'suspend' WHERE id = ?1", - rusqlite::params![id, new_id.to_string()], + crate::db::fixture::params![id, new_id.to_string()], ) .unwrap(); suspend_snapshot_publication::write_full_suspend_artifact( @@ -493,7 +493,7 @@ async fn sweep_releases_holds_once_a_canonical_snapshot_exists() { current_local_artifact_kind = 'full', current_local_artifact_origin = 'suspend' WHERE id = ?1", - rusqlite::params![committed, c.to_string()], + crate::db::fixture::params![committed, c.to_string()], ) .unwrap(); let committed_dir = cache.path().join(&committed); diff --git a/crates/libs/sbx/mgr/server/src/service/recovery/tests.rs b/crates/libs/sbx/mgr/server/src/service/recovery/tests.rs index 85f49f1e6..a96581adb 100644 --- a/crates/libs/sbx/mgr/server/src/service/recovery/tests.rs +++ b/crates/libs/sbx/mgr/server/src/service/recovery/tests.rs @@ -59,7 +59,7 @@ fn restore_source(sandbox_id: &str) -> pplx_space_common_core::types::ExactStora .unwrap() } -fn insert_suspended_test_sandbox(conn: &rusqlite::Connection, id: &str) { +fn insert_suspended_test_sandbox(conn: &crate::db::Connection, id: &str) { let sandbox_id = SandboxId::from_str(id).unwrap(); let uuid8 = SandboxRuntimeLabel::from_sandbox_id(sandbox_id); let user = SandboxOwner::parse("user").unwrap(); @@ -175,7 +175,7 @@ fn cross_node_restore_row_lifecycle_stays_restoring_until_rootfs_stage_succeeds( let persisted_storage_uri = conn .query_row( "SELECT restore_storage_uri FROM sandboxes WHERE id = ?1", - rusqlite::params![sandbox_id], + crate::db::fixture::params![sandbox_id], |row| row.get::<_, Option>(0), ) .unwrap(); @@ -394,7 +394,7 @@ fn orphan_restore_terminal_outcomes_are_reported_distinctly() { // storage while this node's SQLite is the thing refusing writes. ( crate::snapshot_restore::RestoreError::RenewQuarantineRecoveryClaim { - source: rusqlite::Error::QueryReturnedNoRows, + source: crate::db::Error::no_rows(), }, crate::db::QuarantineRecoveryNoProgressReason::ClaimRenewalFailed, ), @@ -465,7 +465,7 @@ fn a_lost_claim_is_reported_without_a_durable_retry_write() { // changed lines in this file was 11%. // --------------------------------------------------------------------------- -fn recovery_test_service(conn: rusqlite::Connection) -> SandboxManagerService { +fn recovery_test_service(conn: crate::db::Connection) -> SandboxManagerService { crate::service::tests::service_tests::service_with_runtime_probe(conn, false) } @@ -483,7 +483,7 @@ fn retry_state(service: &SandboxManagerService, sandbox_id: SandboxId) -> (i64, let db = service.db.lock().unwrap(); db.query_row( "SELECT attempt_count, last_error FROM sandbox_quarantine_state WHERE sandbox_id = ?1", - rusqlite::params![sandbox_id.to_string()], + crate::db::fixture::params![sandbox_id.to_string()], |row| Ok((row.get(0)?, row.get(1)?)), ) .unwrap() @@ -751,7 +751,7 @@ async fn an_orphan_restore_runs_outside_the_inventory_pass_under_the_node_wide_b SET claim_expires_at = 0, next_action_after = 0 WHERE sandbox_id = ?1", - rusqlite::params![sandbox_id.to_string()], + crate::db::fixture::params![sandbox_id.to_string()], ) .unwrap(); } @@ -795,12 +795,12 @@ async fn background_dispatch_defers_a_second_orphan_while_the_first_holds_capaci let second_id = crate::db::tests::setup_quarantine_recovery_episode(&conn, reason_code); conn.execute( "UPDATE sandbox_quarantine_state SET entered_at = '1' WHERE sandbox_id = ?1", - rusqlite::params![first_id.to_string()], + crate::db::fixture::params![first_id.to_string()], ) .unwrap(); conn.execute( "UPDATE sandbox_quarantine_state SET entered_at = '2' WHERE sandbox_id = ?1", - rusqlite::params![second_id.to_string()], + crate::db::fixture::params![second_id.to_string()], ) .unwrap(); let service = recovery_test_service(conn); @@ -924,7 +924,7 @@ async fn the_dispatch_path_supervises_the_task_it_spawns() { } conn.execute( "UPDATE sandbox_quarantine_state SET next_action_after = NULL WHERE sandbox_id = ?1", - rusqlite::params![sandbox_id.to_string()], + crate::db::fixture::params![sandbox_id.to_string()], ) .unwrap(); let service = bound_test_service(conn); @@ -1019,7 +1019,7 @@ async fn the_dispatch_path_supervises_the_task_it_spawns() { "UPDATE sandbox_quarantine_state SET claim_expires_at = '1', next_action_after = NULL WHERE sandbox_id = ?1", - rusqlite::params![sandbox_id.to_string()], + crate::db::fixture::params![sandbox_id.to_string()], ) .unwrap(); service @@ -1253,13 +1253,13 @@ async fn capacity_deferred_orphans_do_not_consume_attempts_or_admission_budget() for sandbox_id in &orphan_ids { conn.execute( "UPDATE sandbox_quarantine_state SET entered_at = '1' WHERE sandbox_id = ?1", - rusqlite::params![sandbox_id.to_string()], + crate::db::fixture::params![sandbox_id.to_string()], ) .unwrap(); } conn.execute( "UPDATE sandbox_quarantine_state SET entered_at = '2' WHERE sandbox_id = ?1", - rusqlite::params![artifact_id.to_string()], + crate::db::fixture::params![artifact_id.to_string()], ) .unwrap(); let service = recovery_test_service(conn); @@ -1336,7 +1336,7 @@ async fn capacity_released_mid_pass_does_not_leapfrog_an_older_orphan() { ] { conn.execute( "UPDATE sandbox_quarantine_state SET entered_at = ?1 WHERE sandbox_id = ?2", - rusqlite::params![entered_at, sandbox_id.to_string()], + crate::db::fixture::params![entered_at, sandbox_id.to_string()], ) .unwrap(); } @@ -1471,7 +1471,7 @@ async fn a_candidate_that_stops_being_admissible_is_reported_without_a_write() { // ordering that reaches the acquire CAS. Inserting it before the pass — as this // test used to — excludes the row from the scan instead, so the pass never claims // anything and every assertion below holds because nothing ran. - let db_for_probe = std::sync::Arc::clone(&service.db); + let db_for_probe = service.db.clone(); let probe = move |window: crate::service::recovery::RecoveryPassWindow| { if window != crate::service::recovery::RecoveryPassWindow::Scanned { return; @@ -1482,7 +1482,7 @@ async fn a_candidate_that_stops_being_admissible_is_reported_without_a_write() { sandbox_id, cleanup_id, completion, keep_filesystem, phase, activation_state, created_at ) VALUES (?1, ?2, 'mark_suspended', TRUE, 'armed', 'restoring', 0)", - rusqlite::params![ + crate::db::fixture::params![ sandbox_id.to_string(), pplx_space_common_core::types::SandboxCleanupEpisodeId::new().to_string() ], @@ -1737,7 +1737,7 @@ async fn a_validated_artifact_is_adopted_into_durable_cleanup() { let state: String = db .query_row( "SELECT state FROM sandboxes WHERE id = ?1", - rusqlite::params![sandbox_id.to_string()], + crate::db::fixture::params![sandbox_id.to_string()], |row| row.get(0), ) .unwrap(); @@ -1760,7 +1760,7 @@ async fn a_validated_artifact_is_adopted_into_durable_cleanup() { .query_row( "SELECT claim_token, claim_expires_at FROM sandbox_quarantine_state WHERE sandbox_id = ?1", - rusqlite::params![sandbox_id.to_string()], + crate::db::fixture::params![sandbox_id.to_string()], |row| Ok((row.get(0)?, row.get(1)?)), ) .unwrap(); @@ -1772,7 +1772,7 @@ async fn a_validated_artifact_is_adopted_into_durable_cleanup() { let queued: i64 = db .query_row( "SELECT COUNT(*) FROM node_dirty_refs WHERE element_value = ?1", - rusqlite::params![sandbox_id.to_string()], + crate::db::fixture::params![sandbox_id.to_string()], |row| row.get(0), ) .unwrap(); @@ -2177,7 +2177,7 @@ async fn a_stale_claim_reaches_the_sandbox_event_timeline() { // Another claimant takes the episode, so this claim's write will miss. conn.execute( "UPDATE sandbox_quarantine_state SET claim_token = ?1 WHERE sandbox_id = ?2", - rusqlite::params![uuid::Uuid::now_v7().to_string(), sandbox_id.to_string()], + crate::db::fixture::params![uuid::Uuid::now_v7().to_string(), sandbox_id.to_string()], ) .unwrap(); @@ -2238,7 +2238,7 @@ impl PassAdversary { Self::BumpAttemptCount, ]; - fn apply(self, conn: &rusqlite::Connection, sandbox_id: SandboxId, token: uuid::Uuid) { + fn apply(self, conn: &crate::db::Connection, sandbox_id: SandboxId, token: uuid::Uuid) { let id = sandbox_id.to_string(); match self { Self::RotateClaimToken => { @@ -2247,7 +2247,7 @@ impl PassAdversary { SET claim_token = ?1, claim_expires_at = CAST(strftime('%s','now') AS INTEGER) + 900 WHERE sandbox_id = ?2", - rusqlite::params![token.to_string(), id], + crate::db::fixture::params![token.to_string(), id], ) .unwrap(); } @@ -2257,7 +2257,7 @@ impl PassAdversary { sandbox_id, cleanup_id, completion, keep_filesystem, phase, activation_state, created_at ) VALUES (?1, ?2, 'mark_suspended', TRUE, 'armed', 'restoring', 0)", - rusqlite::params![ + crate::db::fixture::params![ id, pplx_space_common_core::types::SandboxCleanupEpisodeId::new().to_string() ], @@ -2267,14 +2267,14 @@ impl PassAdversary { Self::LeaveActivatableState => { conn.execute( "UPDATE sandboxes SET state = 'started' WHERE id = ?1", - rusqlite::params![id], + crate::db::fixture::params![id], ) .unwrap(); } Self::DeleteQuarantineRow => { conn.execute( "DELETE FROM sandbox_quarantine_state WHERE sandbox_id = ?1", - rusqlite::params![id], + crate::db::fixture::params![id], ) .unwrap(); } @@ -2283,7 +2283,7 @@ impl PassAdversary { "UPDATE sandbox_quarantine_state SET attempt_count = attempt_count + 7 WHERE sandbox_id = ?1", - rusqlite::params![id], + crate::db::fixture::params![id], ) .unwrap(); } @@ -2440,7 +2440,7 @@ async fn no_interleaving_in_any_named_window_lets_the_pass_write_under_a_moved_f }; let foreign_token = uuid::Uuid::now_v7(); - let db_for_probe = std::sync::Arc::clone(&service.db); + let db_for_probe = service.db.clone(); let fired = std::sync::Arc::new(std::sync::atomic::AtomicBool::new(false)); let fired_for_probe = std::sync::Arc::clone(&fired); // The row exactly as the adversary left it. Anything different at the @@ -2489,7 +2489,7 @@ async fn no_interleaving_in_any_named_window_lets_the_pass_write_under_a_moved_f let state: String = db .query_row( "SELECT state FROM sandboxes WHERE id = ?1", - rusqlite::params![sandbox_id.to_string()], + crate::db::fixture::params![sandbox_id.to_string()], |row| row.get(0), ) .unwrap(); @@ -2607,7 +2607,7 @@ struct QuarantineRow { failure_reason: Option, } -fn quarantine_row(conn: &rusqlite::Connection, sandbox_id: SandboxId) -> Option { +fn quarantine_row(conn: &crate::db::Connection, sandbox_id: SandboxId) -> Option { conn.query_row( "SELECT qs.claim_token, qs.claim_expires_at, qs.attempt_count, qs.next_action_after, qs.last_error, qs.next_action_owner, s.space_api_route_execution_generation, @@ -2615,7 +2615,7 @@ fn quarantine_row(conn: &rusqlite::Connection, sandbox_id: SandboxId) -> Option< FROM sandbox_quarantine_state AS qs JOIN sandboxes AS s ON s.id = qs.sandbox_id WHERE qs.sandbox_id = ?1", - rusqlite::params![sandbox_id.to_string()], + crate::db::fixture::params![sandbox_id.to_string()], |row| { Ok(QuarantineRow { claim_token: row.get(0)?, @@ -2641,11 +2641,11 @@ struct SandboxSideRow { failure_reason: Option, } -fn sandbox_side_row(conn: &rusqlite::Connection, sandbox_id: SandboxId) -> Option { +fn sandbox_side_row(conn: &crate::db::Connection, sandbox_id: SandboxId) -> Option { conn.query_row( "SELECT state, space_api_route_execution_generation, failure_reason FROM sandboxes WHERE id = ?1", - rusqlite::params![sandbox_id.to_string()], + crate::db::fixture::params![sandbox_id.to_string()], |row| { Ok(SandboxSideRow { state: row.get(0)?, @@ -2695,7 +2695,7 @@ async fn the_interleaving_matrix_fixture_adopts_when_nobody_interferes() { let state: String = db .query_row( "SELECT state FROM sandboxes WHERE id = ?1", - rusqlite::params![sandbox_id.to_string()], + crate::db::fixture::params![sandbox_id.to_string()], |row| row.get(0), ) .unwrap(); @@ -2997,7 +2997,10 @@ async fn the_cleanup_contention_release_reports_only_a_real_loss() { if supersede { conn.execute( "UPDATE sandbox_quarantine_state SET claim_token = ?1 WHERE sandbox_id = ?2", - rusqlite::params![uuid::Uuid::now_v7().to_string(), sandbox_id.to_string()], + crate::db::fixture::params![ + uuid::Uuid::now_v7().to_string(), + sandbox_id.to_string() + ], ) .unwrap(); } @@ -3072,7 +3075,7 @@ async fn two_actors_in_two_windows_still_leave_every_fenced_write_missing() { let plan = plan_for(Some(artifact_id), reason_code); let foreign_token = uuid::Uuid::now_v7(); - let db_for_probe = std::sync::Arc::clone(&service.db); + let db_for_probe = service.db.clone(); // Whatever the most recent actor left behind. Every fenced write from the // first move onwards has to miss, so this is what the row must still be. let latest: std::sync::Arc>> = @@ -3127,7 +3130,7 @@ async fn two_actors_in_two_windows_still_leave_every_fenced_write_missing() { let state: String = db .query_row( "SELECT state FROM sandboxes WHERE id = ?1", - rusqlite::params![sandbox_id.to_string()], + crate::db::fixture::params![sandbox_id.to_string()], |row| row.get(0), ) .unwrap(); @@ -3215,7 +3218,7 @@ async fn a_claim_lost_during_the_orphan_restore_is_not_written_up() { let plan = plan_for(None, reason_code); let foreign_token = uuid::Uuid::now_v7(); - let db_for_probe = std::sync::Arc::clone(&service.db); + let db_for_probe = service.db.clone(); let after_rotation: std::sync::Arc>> = std::sync::Arc::new(std::sync::Mutex::new(None)); let slot = std::sync::Arc::clone(&after_rotation); @@ -3274,7 +3277,7 @@ async fn closed_capacity_is_read_off_the_semaphore_not_the_walk() { "UPDATE sandbox_quarantine_state SET entered_at = (SELECT MAX(entered_at) + 60 FROM sandbox_quarantine_state) WHERE sandbox_id = ?1", - rusqlite::params![orphan_id.to_string()], + crate::db::fixture::params![orphan_id.to_string()], ) .unwrap(); let service = recovery_test_service(conn); @@ -3335,12 +3338,12 @@ async fn closed_capacity_is_read_off_the_semaphore_not_the_walk() { ); } -fn insert_quarantined_orphan(conn: &rusqlite::Connection, sandbox_id: &str) { +fn insert_quarantined_orphan(conn: &crate::db::Connection, sandbox_id: &str) { insert_suspended_test_sandbox(conn, sandbox_id); conn.execute( "UPDATE sandboxes SET template = ?1, template_hash = NULL, user = ?2, pool_template = NULL, process_env_json = '{}' WHERE id = ?3", - rusqlite::params![ + crate::db::fixture::params![ pplx_space_common_core::types::ORPHANED_SANDBOX_TEMPLATE, pplx_space_common_core::types::ORPHANED_SANDBOX_OWNER.as_str(), sandbox_id @@ -3392,7 +3395,7 @@ fn cross_node_restore_leaves_failed_synthetic_orphan_untouched() { // --------------------------------------------------------------------------- fn discover_cleanup_targets( - db: &Arc>, + db: &crate::db::SharedConnection, authority: CleanupRecoveryAuthority, ) -> Vec { SandboxManagerService::recover_required_cleanup_targets(db, authority).unwrap() @@ -3423,7 +3426,7 @@ fn startup_requests_interrupted_activation_cleanup_with_the_activation_reason() let episode = crate::db::begin_activation(&conn, &id, entered_from, kind, None, None) .unwrap() .expect("activation arms a cleanup episode"); - let db = Arc::new(Mutex::new(conn)); + let db = crate::db::SharedConnection::new(conn); let targets = discover_cleanup_targets(&db, CleanupRecoveryAuthority::Startup); @@ -3472,7 +3475,7 @@ fn startup_retires_an_armed_cleanup_whose_row_left_the_activation_state() { [&id], ) .unwrap(); - let db = Arc::new(Mutex::new(conn)); + let db = crate::db::SharedConnection::new(conn); assert!(discover_cleanup_targets(&db, CleanupRecoveryAuthority::Startup).is_empty()); @@ -3498,7 +3501,7 @@ fn startup_persists_the_inferred_cleanup_intent_of_an_interrupted_start() { [&id], ) .unwrap(); - let db = Arc::new(Mutex::new(conn)); + let db = crate::db::SharedConnection::new(conn); let [target] = discover_cleanup_targets(&db, CleanupRecoveryAuthority::Startup) .try_into() @@ -3544,7 +3547,7 @@ fn requested_cleanup_discovery_yields_to_an_intent_recorded_after_listing() { .unwrap(); let listed = vec![crate::db::get_sandbox(&conn, &id).unwrap().unwrap()]; crate::db::set_cleanup_intent(&conn, &id, true, CleanupCompletion::MarkStopped).unwrap(); - let db = Arc::new(Mutex::new(conn)); + let db = crate::db::SharedConnection::new(conn); let targets = SandboxManagerService::recover_required_cleanup_targets_from_rows( &db, @@ -3586,7 +3589,7 @@ fn startup_cleanup_discovery_skips_rows_with_malformed_ids() { [sandbox_id.to_string()], ) .unwrap(); - let db = Arc::new(Mutex::new(conn)); + let db = crate::db::SharedConnection::new(conn); let [target] = discover_cleanup_targets(&db, CleanupRecoveryAuthority::Startup) .try_into() @@ -3708,7 +3711,7 @@ async fn pass_with_every_episode_due( } /// Every field explicit, so no fixture reads the host's node identity. -fn bound_test_service(conn: rusqlite::Connection) -> SandboxManagerService { +fn bound_test_service(conn: crate::db::Connection) -> SandboxManagerService { crate::service::tests::service_tests::service_with_runtime_probe(conn, false) } diff --git a/crates/libs/sbx/mgr/server/src/service/recovery_cross_node.rs b/crates/libs/sbx/mgr/server/src/service/recovery_cross_node.rs index 2f9d95eae..5ce9480be 100644 --- a/crates/libs/sbx/mgr/server/src/service/recovery_cross_node.rs +++ b/crates/libs/sbx/mgr/server/src/service/recovery_cross_node.rs @@ -406,7 +406,7 @@ fn log_interrupted_restore_completion(canonical_id: &str, storage_uri: &str) { /// stamp `failure_reason` onto a row whose new owner is mid-restore — the state guard /// alone does not stop it, because the new owner has put the row back in `restoring`. fn persist_cross_node_staging_failure( - conn: &rusqlite::Connection, + conn: &crate::db::Connection, sandbox_id: SandboxId, stage: &'static str, error: &crate::snapshot_restore::RestoreError, @@ -465,7 +465,7 @@ fn select_interrupted_restore_source( } fn persist_selected_interrupted_restore_source( - conn: &rusqlite::Connection, + conn: &crate::db::Connection, sandbox_id: SandboxId, persisted_source: &ExactStorageUri, selected_source: &ExactStorageUri, @@ -650,7 +650,7 @@ impl<'a> RestoringRowInput<'a> { /// episode. The two are one transaction now, so a refused CAS rolls the adoption /// back with it and a stale task leaves no trace. fn adopt_restoring_row_under_recovery_claim( - conn: &rusqlite::Connection, + conn: &crate::db::Connection, sandbox_id: SandboxId, canonical_id: &str, row: RestoringRowInput<'_>, @@ -679,7 +679,7 @@ fn adopt_restoring_row_under_recovery_claim( ) }; } - let tx = crate::db::immediate_transaction(conn) + let tx = crate::db::Transaction::immediate(conn) .map_err(|source| crate::snapshot_restore::RestoreError::AdvanceRestorePhase { source })?; // Check recovery authority against the state observed before the upsert. // The generation CAS below sees the newly written Restoring state, so it @@ -769,7 +769,7 @@ fn adopt_restoring_row_under_recovery_claim( /// advances the recovery generation together, so this uses the in-transaction /// variant rather than opening and committing its own. pub(super) fn insert_restoring_sandbox_row_from_snapshot( - conn: &rusqlite::Connection, + conn: &crate::db::Connection, canonical_id: &str, meta: &crate::snapshot_upload::SnapshotMeta, restore_source: &pplx_space_common_core::types::ExactStorageUri, @@ -817,7 +817,7 @@ pub(super) fn insert_restoring_sandbox_row_from_snapshot( } pub(super) fn transition_restored_sandbox_to_suspended( - conn: &rusqlite::Connection, + conn: &crate::db::Connection, canonical_id: &str, meta: &crate::snapshot_upload::SnapshotMeta, ) -> Result<(), crate::snapshot_restore::RestoreError> { @@ -831,7 +831,7 @@ pub(super) fn transition_restored_sandbox_to_suspended( } fn transition_restored_sandbox_to_suspended_parts( - conn: &rusqlite::Connection, + conn: &crate::db::Connection, canonical_id: &str, local_artifact_id: Option, artifact_kind: Option, @@ -867,7 +867,7 @@ fn transition_restored_sandbox_to_suspended_parts( } fn finalize_recovered_published_restore( - conn: &rusqlite::Connection, + conn: &crate::db::Connection, canonical_id: &str, sandbox_dir: &std::path::Path, meta: &crate::snapshot_upload::SnapshotMeta, @@ -886,9 +886,9 @@ fn finalize_recovered_published_restore( } async fn finalize_exchanged_restore_after_cleanup( - db: &Arc>, + db: &crate::db::SharedConnection, cleanup: impl std::future::Future>, - finalize: impl FnOnce(&rusqlite::Connection) -> Result, + finalize: impl FnOnce(&crate::db::Connection) -> Result, ) -> Result { // Finalization retires the transfer scope. Keep that durable cleanup owner // until every fallible external and filesystem cleanup step has succeeded. @@ -900,7 +900,7 @@ async fn finalize_exchanged_restore_after_cleanup( } fn claim_exchanged_restore_cleanup( - db: &Arc>, + db: &crate::db::SharedConnection, sandbox_id: &str, existing: &crate::db::RestoreTransferScopeRow, execution_generation: pplx_space_common_core::types::ExecutionGeneration, @@ -2009,7 +2009,7 @@ impl SandboxManagerService { ), ); - let finalize_restore = |db: &rusqlite::Connection| { + let finalize_restore = |db: &crate::db::Connection| { if let Some(recovery) = recovery { let local_artifact_id = validate_quarantine_recovery_artifact_identity( &canonical_id, @@ -2027,7 +2027,7 @@ impl SandboxManagerService { .map_err(|source| RestoreError::PersistOrphanQuarantineRecovery { source })? { // Same class as the generation CAS above, and the typed - // rusqlite error is preserved rather than formatted away. + // SQLite error is preserved rather than formatted away. // // This one refuses on strictly more than a lost claim: also on // an execution-generation mismatch, on the sandbox no longer diff --git a/crates/libs/sbx/mgr/server/src/service/recovery_cross_node/tests.rs b/crates/libs/sbx/mgr/server/src/service/recovery_cross_node/tests.rs index 5ea6f9760..cb349ec50 100644 --- a/crates/libs/sbx/mgr/server/src/service/recovery_cross_node/tests.rs +++ b/crates/libs/sbx/mgr/server/src/service/recovery_cross_node/tests.rs @@ -98,7 +98,7 @@ fn interrupted_restore_persists_new_source_for_uri_less_retry() { restore_origin, restore_phase, restore_storage_uri ) VALUES (?1, 'sandbox1', 'restoring', 'seed', '1785456000', '1785456000', 'remote', 'metadata_staging', ?2)", - rusqlite::params![sandbox_id.to_string(), persisted.as_wire()], + crate::db::fixture::params![sandbox_id.to_string(), persisted.as_wire()], ) .unwrap(); @@ -244,7 +244,7 @@ async fn exchanged_restore_cleanup_failure_retains_scope_until_retry() { ) .unwrap() ); - let db = std::sync::Arc::new(std::sync::Mutex::new(conn)); + let db = crate::db::SharedConnection::new(conn); let failed = finalize_exchanged_restore_after_cleanup( &db, @@ -741,12 +741,12 @@ fn restore_artifact_as_new_publishes_execution_metadata_for_cold_start() { /// lose the claim or fail against SQLite. struct RenewalStub { calls: std::sync::atomic::AtomicUsize, - verdict: Result, + verdict: Result, fail_first: u32, } impl RenewalStub { - fn new(verdict: Result) -> Self { + fn new(verdict: Result) -> Self { Self { calls: std::sync::atomic::AtomicUsize::new(0), verdict, @@ -770,7 +770,7 @@ impl RenewalStub { if u32::try_from(call).unwrap_or(u32::MAX) < self.fail_first { return Err( crate::snapshot_restore::RestoreError::RenewQuarantineRecoveryClaim { - source: rusqlite::Error::QueryReturnedNoRows, + source: crate::db::Error::no_rows(), }, ); } @@ -778,7 +778,7 @@ impl RenewalStub { Ok(renewed) => Ok(*renewed), Err(_) => Err( crate::snapshot_restore::RestoreError::RenewQuarantineRecoveryClaim { - source: rusqlite::Error::QueryReturnedNoRows, + source: crate::db::Error::no_rows(), }, ), } @@ -871,7 +871,7 @@ async fn claimed_operation_aborts_when_the_claim_is_lost() { #[tokio::test(start_paused = true)] async fn claimed_operation_propagates_a_failed_renewal_as_a_typed_error() { let sandbox_id = SandboxId::new(); - let renewal = RenewalStub::new(Err(rusqlite::Error::QueryReturnedNoRows)); + let renewal = RenewalStub::new(Err(crate::db::Error::no_rows())); let error = drive_under_quarantine_recovery_claim( async { std::future::pending::<()>().await; @@ -1029,7 +1029,7 @@ fn a_superseded_recovery_claim_leaves_no_adopted_row() { "UPDATE sandbox_quarantine_state SET claim_token = ?1, claim_expires_at = ?2, next_action_after = NULL WHERE sandbox_id = ?3", - rusqlite::params![ + crate::db::fixture::params![ uuid::Uuid::now_v7().to_string(), (std::time::SystemTime::now() .duration_since(std::time::UNIX_EPOCH) @@ -1145,11 +1145,11 @@ fn an_adoption_inside_the_claim_transaction_commits_the_row() { fn an_ordinary_cross_node_retry_takes_no_write_lock() { let dir = tempfile::tempdir().unwrap(); let path = dir.path().join("sbxmgr.db"); - let mut conn = rusqlite::Connection::open(&path).unwrap(); + let mut conn = crate::db::Connection::open_at(&path).unwrap(); crate::db::create_test_schema(&mut conn); conn.busy_timeout(std::time::Duration::ZERO).unwrap(); - let blocker = rusqlite::Connection::open(&path).unwrap(); + let blocker = crate::db::Connection::open_at(&path).unwrap(); blocker.busy_timeout(std::time::Duration::ZERO).unwrap(); blocker.execute_batch("BEGIN EXCLUSIVE").unwrap(); @@ -1301,7 +1301,7 @@ fn a_superseded_claim_cannot_persist_a_canonical_publication_failure() { .expect("claiming a fresh episode should succeed"); conn.execute( "UPDATE sandboxes SET state = 'restoring' WHERE id = ?1", - rusqlite::params![sandbox_id.to_string()], + crate::db::fixture::params![sandbox_id.to_string()], ) .unwrap(); @@ -1318,7 +1318,7 @@ fn a_superseded_claim_cannot_persist_a_canonical_publication_failure() { let owned: Option = conn .query_row( "SELECT failure_reason FROM sandboxes WHERE id = ?1", - rusqlite::params![sandbox_id.to_string()], + crate::db::fixture::params![sandbox_id.to_string()], |row| row.get(0), ) .unwrap(); @@ -1327,7 +1327,7 @@ fn a_superseded_claim_cannot_persist_a_canonical_publication_failure() { // The episode moves to another claimant while the transfer is still running. conn.execute( "UPDATE sandbox_quarantine_state SET claim_token = ?1 WHERE sandbox_id = ?2", - rusqlite::params![uuid::Uuid::now_v7().to_string(), sandbox_id.to_string()], + crate::db::fixture::params![uuid::Uuid::now_v7().to_string(), sandbox_id.to_string()], ) .unwrap(); // A different stage, so the two writes are distinguishable. With the same stage the @@ -1352,7 +1352,7 @@ fn a_superseded_claim_cannot_persist_a_canonical_publication_failure() { let after: Option = conn .query_row( "SELECT failure_reason FROM sandboxes WHERE id = ?1", - rusqlite::params![sandbox_id.to_string()], + crate::db::fixture::params![sandbox_id.to_string()], |row| row.get(0), ) .unwrap(); @@ -1467,7 +1467,7 @@ const UNOWNED_STAGING_DIR: &str = "/space/cache/restore-staging/not-owned-by-thi /// Persist the row an interrupted remote restore leaves behind: `restoring`, /// remote origin, metadata staging, no runtime fields and no artifact. -fn insert_interrupted_remote_restore(conn: &rusqlite::Connection) -> (String, ExactStorageUri) { +fn insert_interrupted_remote_restore(conn: &crate::db::Connection) -> (String, ExactStorageUri) { let sandbox_id = SandboxId::new().to_string(); let (mut meta, _) = disk_artifact_meta(); meta.sandbox_id = sandbox_id.clone(); @@ -1499,10 +1499,10 @@ fn transfer_scope( /// Every persisted column of a `sandboxes` row, so "untouched" means the whole /// row and not only the fields a test thought to name. fn sandbox_columns( - conn: &rusqlite::Connection, + conn: &crate::db::Connection, sandbox_id: &str, -) -> Option> { - use rusqlite::OptionalExtension as _; +) -> Option> { + use crate::db::OptionalExtension as _; conn.query_row( "SELECT * FROM sandboxes WHERE id = ?1", [sandbox_id], @@ -1919,7 +1919,7 @@ async fn rowless_restore_keeps_a_superseded_scope_it_cannot_retire() { } fn claim_orphan_episode( - conn: &rusqlite::Connection, + conn: &crate::db::Connection, ) -> (SandboxId, crate::db::ClaimedQuarantineRecoveryCandidate) { let sandbox_id = crate::db::tests::setup_quarantine_recovery_episode( conn, @@ -2074,7 +2074,7 @@ async fn orphan_restore_declines_a_row_that_now_holds_a_local_artifact() { current_local_artifact_kind = 'full', current_local_artifact_origin = 'suspend' WHERE id = ?2", - rusqlite::params![LocalArtifactId::new().to_string(), sandbox_id.to_string()], + crate::db::fixture::params![LocalArtifactId::new().to_string(), sandbox_id.to_string()], ) .unwrap(); let service = service_with_runtime_probe(conn, true); diff --git a/crates/libs/sbx/mgr/server/src/service/route_identity_tests.rs b/crates/libs/sbx/mgr/server/src/service/route_identity_tests.rs index cd9cddc27..47ff32c65 100644 --- a/crates/libs/sbx/mgr/server/src/service/route_identity_tests.rs +++ b/crates/libs/sbx/mgr/server/src/service/route_identity_tests.rs @@ -66,7 +66,7 @@ struct RouteTestService { } impl RouteTestService { - fn new(conn: rusqlite::Connection) -> Self { + fn new(conn: crate::db::Connection) -> Self { let sockets = tempfile::tempdir().unwrap(); let shutdown = CancellationToken::new(); let tasks = TaskTracker::new(); @@ -102,7 +102,7 @@ impl std::ops::Deref for RouteTestService { } fn insert_sandbox( - conn: &rusqlite::Connection, + conn: &crate::db::Connection, sandbox_id: SandboxId, state: SandboxState, pool_template: Option<&str>, @@ -166,7 +166,7 @@ fn kubernetes_identity(sandbox_id: SandboxId, generation: u64) -> SpaceApiRouteI /// A started Kubernetes row whose complete route identity was recorded /// without cluster fencing. -fn started_unfenced_sandbox(conn: &rusqlite::Connection) -> (SandboxId, SpaceApiRouteIdentity) { +fn started_unfenced_sandbox(conn: &crate::db::Connection) -> (SandboxId, SpaceApiRouteIdentity) { let sandbox_id = SandboxId::new(); insert_sandbox( conn, @@ -684,7 +684,7 @@ async fn resident_resume_refusal_status_for_every_state() { .unwrap(); conn.execute( "UPDATE sandboxes SET state = ?1 WHERE id = ?2", - rusqlite::params![state.as_str(), sandbox_id.to_string()], + crate::db::fixture::params![state.as_str(), sandbox_id.to_string()], ) .unwrap(); let service = RouteTestService::new(conn); diff --git a/crates/libs/sbx/mgr/server/src/service/start.rs b/crates/libs/sbx/mgr/server/src/service/start.rs index c511a3b2f..bef9eb51c 100644 --- a/crates/libs/sbx/mgr/server/src/service/start.rs +++ b/crates/libs/sbx/mgr/server/src/service/start.rs @@ -2093,7 +2093,7 @@ impl SandboxManagerService { ); let route_identity = normalized_route_identity.identity; let netgtw_route_identity = route_identity.clone(); - let netgtw_db = Arc::clone(&self.db); + let netgtw_db = self.db.clone(); let netgtw_launch_config = net_gateway_launch_config(&self.feature_flags, &netgtw_sandbox_id); let netgtw_task = tokio_util::task::AbortOnDropHandle::new(tokio::spawn( @@ -2518,7 +2518,7 @@ impl SandboxManagerService { let apply_network_policy = !Self::sandbox_is_pool_member(&sandbox); if req.blocking { if let Err(error) = wait_ready_and_mark_started( - Arc::clone(&self.db), + self.db.clone(), id, &sandbox.process_env, ready_timeout_secs, @@ -2534,7 +2534,7 @@ impl SandboxManagerService { } else { spawn_wait_ready_state_transition( self, - Arc::clone(&self.db), + self.db.clone(), StartStateTransition { sandbox_id: id.clone(), process_env: sandbox.process_env.clone(), diff --git a/crates/libs/sbx/mgr/server/src/service/start/tests.rs b/crates/libs/sbx/mgr/server/src/service/start/tests.rs index 715db4049..1d8bc3126 100644 --- a/crates/libs/sbx/mgr/server/src/service/start/tests.rs +++ b/crates/libs/sbx/mgr/server/src/service/start/tests.rs @@ -662,7 +662,7 @@ async fn restore_releases_artifact_lock_while_waiting_for_cleanup_completion() { conn.execute("INSERT INTO sandbox_cleanup_episodes (sandbox_id, cleanup_id, completion, keep_filesystem, phase, activation_state, created_at, requested_at, quarantine_reason_code) VALUES (?1, ?2, 'mark_suspended', TRUE, 'requested', 'restoring', 0, 0, ?3)", - rusqlite::params![id, cleanup_id.to_string(), SandboxQuarantineReasonCode::RestoreActivationInterrupted.as_str()]).unwrap(); + crate::db::fixture::params![id, cleanup_id.to_string(), SandboxQuarantineReasonCode::RestoreActivationInterrupted.as_str()]).unwrap(); let service = service_for_teardown_test(conn); *service.cleanup_executor.dispatch.lock().unwrap() = Some(Arc::new(|_, _| Box::pin(async { Ok(()) }))); diff --git a/crates/libs/sbx/mgr/server/src/service/start_support.rs b/crates/libs/sbx/mgr/server/src/service/start_support.rs index abf971f19..fab5cb0d6 100644 --- a/crates/libs/sbx/mgr/server/src/service/start_support.rs +++ b/crates/libs/sbx/mgr/server/src/service/start_support.rs @@ -154,7 +154,7 @@ impl Drop for ActivationCleanupGuard { } pub(super) fn persist_allocated_runtime_label( - db: &Arc>, + db: &crate::db::SharedConnection, sandbox_id: &str, current: &mut SandboxRuntimeLabel, cleanup: &mut ActivationCleanupGuard, diff --git a/crates/libs/sbx/mgr/server/src/service/start_support/tests.rs b/crates/libs/sbx/mgr/server/src/service/start_support/tests.rs index b5a59b986..b5c15f50b 100644 --- a/crates/libs/sbx/mgr/server/src/service/start_support/tests.rs +++ b/crates/libs/sbx/mgr/server/src/service/start_support/tests.rs @@ -251,7 +251,7 @@ async fn activation_cleanup_uses_updated_runtime_label() { #[tokio::test] async fn allocated_runtime_label_owns_cleanup_before_database_persistence() { - let db = Arc::new(Mutex::new(rusqlite::Connection::open_in_memory().unwrap())); + let db = crate::db::SharedConnection::new(crate::db::Connection::open_in_memory()); let (cleanup_tx, cleanup_rx) = tokio::sync::oneshot::channel(); let mut cleanup_guard = ActivationCleanupGuard::with_interruption_reason( SandboxRuntimeLabel::parse("runtime1").unwrap(), diff --git a/crates/libs/sbx/mgr/server/src/service/startup_resident.rs b/crates/libs/sbx/mgr/server/src/service/startup_resident.rs index fcfb79e69..d29046b0e 100644 --- a/crates/libs/sbx/mgr/server/src/service/startup_resident.rs +++ b/crates/libs/sbx/mgr/server/src/service/startup_resident.rs @@ -130,7 +130,7 @@ impl SandboxManagerService { SandboxState::Paused => SandboxQuarantineReasonCode::RuntimeProbeInconsistent, SandboxState::Resuming => SandboxQuarantineReasonCode::ResidentResumeFailed, SandboxState::Checkpointing => SandboxQuarantineReasonCode::CheckpointResumeFailed, - _ => return Err(rusqlite::Error::InvalidQuery.into()), + _ => return Err(crate::db::Error::invalid_query().into()), }; // Resume uses the wake lock; teardown/activation uses the cleanup // fence. Hold both through setup so a stale recovery cannot configure diff --git a/crates/libs/sbx/mgr/server/src/service/storage.rs b/crates/libs/sbx/mgr/server/src/service/storage.rs index b91d0c745..f59476ccc 100644 --- a/crates/libs/sbx/mgr/server/src/service/storage.rs +++ b/crates/libs/sbx/mgr/server/src/service/storage.rs @@ -35,7 +35,7 @@ impl SandboxManagerService { pub(super) async fn storage( &self, - operation: impl FnOnce(&rusqlite::Connection) -> Result + Send + 'static, + operation: impl FnOnce(&crate::db::Connection) -> Result + Send + 'static, ) -> Result { self.blocking_io(move |service| { let db = service.db_conn()?; diff --git a/crates/libs/sbx/mgr/server/src/service/storage/tests.rs b/crates/libs/sbx/mgr/server/src/service/storage/tests.rs index cb66ab001..dc3f32894 100644 --- a/crates/libs/sbx/mgr/server/src/service/storage/tests.rs +++ b/crates/libs/sbx/mgr/server/src/service/storage/tests.rs @@ -198,7 +198,7 @@ async fn storage_waits_off_runtime_and_preserves_transaction_rollback_and_poison let error = service .storage(|db| { - let tx = db.unchecked_transaction().unwrap(); + let tx = crate::db::Transaction::deferred(db).unwrap(); insert_test_sandbox(&tx, ID, crate::state::SandboxState::Created); Err::<(), _>(Status::aborted("abort transaction")) }) diff --git a/crates/libs/sbx/mgr/server/src/service/template_snapshot_support.rs b/crates/libs/sbx/mgr/server/src/service/template_snapshot_support.rs index 20dd4add1..111767477 100644 --- a/crates/libs/sbx/mgr/server/src/service/template_snapshot_support.rs +++ b/crates/libs/sbx/mgr/server/src/service/template_snapshot_support.rs @@ -142,7 +142,7 @@ pub(super) fn publish_template_base_snapshot_dir( /// `sandboxes.state` from `paused` to `checkpointing`. The caller checks the /// row under the same connection guard. pub(super) fn begin_template_base_snapshot_state( - db: &rusqlite::Connection, + db: &crate::db::Connection, sandbox_id: &str, ) -> Result<(), Status> { require_state_write_from( @@ -157,7 +157,7 @@ pub(super) fn begin_template_base_snapshot_state( /// moved it to; the completion reports that it was superseded instead of /// overwriting the row. pub(super) fn complete_template_base_snapshot_state( - db: &rusqlite::Connection, + db: &crate::db::Connection, sandbox_id: &str, ) -> Result<(), Status> { require_state_write_from( @@ -174,7 +174,7 @@ pub(super) fn complete_template_base_snapshot_state( /// on a best-effort basis. A row another transition moved, such as a Stop, /// keeps its state. pub(super) fn rollback_template_base_snapshot_state( - db: &Arc>, + db: &crate::db::SharedConnection, sandbox_id: &str, ) { update_state_from_best_effort( diff --git a/crates/libs/sbx/mgr/server/src/service/test_options.rs b/crates/libs/sbx/mgr/server/src/service/test_options.rs index c56111563..a5137e17d 100644 --- a/crates/libs/sbx/mgr/server/src/service/test_options.rs +++ b/crates/libs/sbx/mgr/server/src/service/test_options.rs @@ -80,7 +80,7 @@ impl SandboxManagerServiceOptions { impl SandboxManagerService { /// A service built from [`SandboxManagerServiceOptions::for_test`]. - pub(crate) fn for_test(conn: rusqlite::Connection) -> Self { + pub(crate) fn for_test(conn: crate::db::Connection) -> Self { Self::new_with_options(conn, SandboxManagerServiceOptions::for_test()) } } diff --git a/crates/libs/sbx/mgr/server/src/service/tests.rs b/crates/libs/sbx/mgr/server/src/service/tests.rs index 1d76da9b9..65ec1943e 100644 --- a/crates/libs/sbx/mgr/server/src/service/tests.rs +++ b/crates/libs/sbx/mgr/server/src/service/tests.rs @@ -82,7 +82,7 @@ pub(crate) fn closed_drain_for_test() -> Arc { #[test] fn startup_cleanup_discovery_reports_database_failure_instead_of_empty_inventory() { - let db = Arc::new(Mutex::new(crate::db::test_conn())); + let db = crate::db::SharedConnection::new(crate::db::test_conn()); db.lock() .unwrap() .execute_batch("DROP TABLE sandboxes") @@ -672,7 +672,7 @@ pub(crate) mod service_tests { assert_eq!(adopt.route_identity, Some(route_identity)); } - fn insert_pool_donor(conn: &rusqlite::Connection, sandbox_id: SandboxId) { + fn insert_pool_donor(conn: &crate::db::Connection, sandbox_id: SandboxId) { let sandbox_id_string = sandbox_id.to_string(); let uuid8 = SandboxRuntimeLabel::from_sandbox_id(sandbox_id); crate::db::insert_sandbox( @@ -1189,7 +1189,7 @@ pub(crate) mod service_tests { last_observed_execution_generation = 7, last_observed_route_generation_substrate_kind = 'space' WHERE id = ?1", - rusqlite::params![sandbox_id.to_string()], + crate::db::fixture::params![sandbox_id.to_string()], ) .unwrap(); let service = service_for_teardown_test(conn); @@ -1447,7 +1447,7 @@ pub(crate) mod service_tests { last_observed_execution_generation = 7, last_observed_route_generation_substrate_kind = NULL WHERE id = ?1", - rusqlite::params![sandbox_id.to_string()], + crate::db::fixture::params![sandbox_id.to_string()], ) .unwrap(); let service = service_for_teardown_test(conn); @@ -1479,7 +1479,7 @@ pub(crate) mod service_tests { last_observed_execution_generation = 7, last_observed_route_generation_substrate_kind = 'space' WHERE id = ?1", - rusqlite::params![sandbox_id.to_string()], + crate::db::fixture::params![sandbox_id.to_string()], ) .unwrap(); let service = service_for_teardown_test(conn); @@ -1512,7 +1512,7 @@ pub(crate) mod service_tests { last_observed_execution_generation = 7, last_observed_route_generation_substrate_kind = NULL WHERE id = ?1", - rusqlite::params![sandbox_id.to_string()], + crate::db::fixture::params![sandbox_id.to_string()], ) .unwrap(); let service = service_for_teardown_test(conn); @@ -2080,7 +2080,7 @@ pub(crate) mod service_tests { { let dir = tempfile::tempdir().unwrap(); let path = dir.path().join("manager.sqlite"); - let mut conn = rusqlite::Connection::open(&path).unwrap(); + let mut conn = crate::db::Connection::open_at(&path).unwrap(); crate::db::create_test_schema(&mut conn); let sandbox_id = SandboxId::new(); let id = sandbox_id.to_string(); @@ -2153,7 +2153,8 @@ pub(crate) mod service_tests { .unwrap(); } drop(service); - let recovered = service_for_teardown_test(rusqlite::Connection::open(&path).unwrap()); + let recovered = + service_for_teardown_test(crate::db::Connection::open_at(&path).unwrap()); recovered .recover_interrupted_startup_states() .await @@ -2250,7 +2251,7 @@ pub(crate) mod service_tests { // makes a rolled-back manager reject the same legitimate route installation. conn.execute( "UPDATE sandbox_cleanup_episodes SET activation_attempt_id = ?2 WHERE sandbox_id = ?1", - rusqlite::params![id, operation_b.to_string()], + crate::db::fixture::params![id, operation_b.to_string()], ) .unwrap(); assert!( @@ -3675,23 +3676,19 @@ pub(crate) mod service_tests { use pplx_space_metrics::sandbox::worker::RouteIdentityAdoptionReason; assert_eq!( - route_identity_adoption_write_rejection_reason(&rusqlite::Error::QueryReturnedNoRows), + route_identity_adoption_write_rejection_reason(&crate::db::Error::no_rows()), RouteIdentityAdoptionReason::NotFound ); assert_eq!( - route_identity_adoption_write_rejection_reason( - &rusqlite::Error::ToSqlConversionFailure(Box::new( - crate::db::auto_lifecycle_delete_receipts::PendingReceiptOwnsExecution, - )), - ), + route_identity_adoption_write_rejection_reason(&crate::db::Error::to_sql_conversion( + crate::db::auto_lifecycle_delete_receipts::PendingReceiptOwnsExecution, + ),), RouteIdentityAdoptionReason::PendingReceiptOwnsExecution ); assert_eq!( - route_identity_adoption_write_rejection_reason( - &rusqlite::Error::ToSqlConversionFailure(Box::new( - crate::db::auto_lifecycle_delete_receipts::LifecycleOwnsExecution, - )), - ), + route_identity_adoption_write_rejection_reason(&crate::db::Error::to_sql_conversion( + crate::db::auto_lifecycle_delete_receipts::LifecycleOwnsExecution, + ),), RouteIdentityAdoptionReason::LifecycleOwnsExecution ); } @@ -3700,7 +3697,7 @@ pub(crate) mod service_tests { async fn list_build_referencing_sandboxes_includes_user_sandboxes_and_scopes_out_deleting() { let conn = crate::db::test_conn(); - let insert = |conn: &rusqlite::Connection, + let insert = |conn: &crate::db::Connection, id: &str, state: SandboxState, hash: &str, @@ -3884,7 +3881,7 @@ pub(crate) mod service_tests { // are empty for a template-manager peer yet must continue the scan. conn.execute( "UPDATE sandboxes SET user = ?2 WHERE id = ?1", - rusqlite::params![ + crate::db::fixture::params![ ids[4], pplx_space_common_core::types::TEMPLATE_MANAGER_SANDBOX_OWNER.as_str() ], @@ -3944,7 +3941,7 @@ pub(crate) mod service_tests { for id in &ids[..2] { conn.execute( "UPDATE sandboxes SET template = ?2 WHERE id = ?1", - rusqlite::params![id, template], + crate::db::fixture::params![id, template], ) .unwrap(); } @@ -4299,7 +4296,7 @@ pub(crate) mod service_tests { insert_test_sandbox(&conn, &sandbox_id, SandboxState::Creating); conn.execute( "UPDATE sandboxes SET network_policy_override_json = ?1 WHERE id = ?2", - rusqlite::params![ + crate::db::fixture::params![ "{\"version\":\"v9\",\"egress\":{\"default_action\":\"deny\"}}", &sandbox_id ], @@ -4327,7 +4324,7 @@ pub(crate) mod service_tests { let conn = crate::db::test_conn(); let sandbox_id = SandboxId::new().to_string(); insert_test_sandbox(&conn, &sandbox_id, SandboxState::Restoring); - let db = Arc::new(Mutex::new(conn)); + let db = crate::db::SharedConnection::new(conn); let sbx_uid = 10042; let netgtw_pid = 4242; @@ -4347,7 +4344,7 @@ pub(crate) mod service_tests { // Fake jailer launch: still in flight when the gateway arm completes. // It observes what host-control auth would see for a policy pull // issued by the just-launched gateway at this point. - let jailer_db = Arc::clone(&db); + let jailer_db = db.clone(); let jailer_arm = async move { gateway_arm_done_rx.await.expect("gateway arm dropped"); let conn = jailer_db.lock().expect("mutex poisoned"); @@ -5109,7 +5106,7 @@ pub(crate) mod service_tests { let conn = crate::db::test_conn(); let sandbox_id = SandboxId::new().to_string(); insert_test_sandbox(&conn, &sandbox_id, SandboxState::Resuming); - let db = Arc::new(std::sync::Mutex::new(conn)); + let db = crate::db::SharedConnection::new(conn); let error = super::super::auth_lookup::finish_resident_resume_after_guest_setup( &db, @@ -5134,7 +5131,7 @@ pub(crate) mod service_tests { let conn = crate::db::test_conn(); let sandbox_id = SandboxId::new().to_string(); insert_test_sandbox(&conn, &sandbox_id, SandboxState::Resuming); - let db = Arc::new(std::sync::Mutex::new(conn)); + let db = crate::db::SharedConnection::new(conn); let error = super::super::auth_lookup::finish_resident_resume_after_guest_setup( &db, @@ -5175,7 +5172,7 @@ pub(crate) mod service_tests { let conn = crate::db::test_conn(); let sandbox_id = SandboxId::new().to_string(); insert_test_sandbox(&conn, &sandbox_id, SandboxState::Resuming); - let db = Arc::new(std::sync::Mutex::new(conn)); + let db = crate::db::SharedConnection::new(conn); let result = super::super::auth_lookup::finish_resident_resume_after_rpc_error( &db, @@ -5396,10 +5393,10 @@ pub(crate) mod service_tests { crate::db::CleanupCompletion::DeleteRow, ) .unwrap(); - let db = Arc::new(Mutex::new(conn)); + let db = crate::db::SharedConnection::new(conn); let result = wait_ready_and_mark_started( - Arc::clone(&db), + db.clone(), &sandbox_id, &BTreeMap::new(), 1, @@ -5503,7 +5500,7 @@ pub(crate) mod service_tests { JOIN sandbox_quarantine_history AS history ON history.quarantine_id = current.quarantine_id WHERE current.sandbox_id = ?1", - rusqlite::params![sandbox_id_string], + crate::db::fixture::params![sandbox_id_string], |row| Ok((row.get(0)?, row.get(1)?, row.get(2)?)), ) .unwrap(); @@ -6030,7 +6027,7 @@ pub(crate) mod service_tests { #[test] fn reconcile_inventory_does_not_delete_row_changed_after_inventory_snapshot() { struct MutatingInventoryDb { - conn: std::cell::RefCell, + conn: std::cell::RefCell, calls: std::cell::Cell, sandbox_id: String, } @@ -6038,7 +6035,7 @@ pub(crate) mod service_tests { impl InventoryDbAccess for MutatingInventoryDb { fn with_connection( &self, - operation: impl FnOnce(&rusqlite::Connection) -> T, + operation: impl FnOnce(&crate::db::Connection) -> T, ) -> Result { let call = self.calls.get(); self.calls.set(call + 1); @@ -6048,7 +6045,7 @@ pub(crate) mod service_tests { "UPDATE sandboxes SET state = 'started', uid = 10000, pid = 1234, updated_at = 'new' WHERE id = ?1", - rusqlite::params![&self.sandbox_id], + crate::db::fixture::params![&self.sandbox_id], )?; } Ok(operation(&conn)) @@ -6149,7 +6146,7 @@ pub(crate) mod service_tests { #[tokio::test] async fn reconcile_inventory_staging_lock_protects_active_and_reclaims_abandoned_restore() { let conn = crate::db::test_conn(); - let db = std::sync::Mutex::new(conn); + let db = crate::db::SharedConnection::new(conn); let cache_dir = tempfile::tempdir().unwrap(); let run_dir = tempfile::tempdir().unwrap(); let sandbox_id = SandboxId::new(); @@ -6481,7 +6478,7 @@ pub(crate) mod service_tests { if index == 0 || index == READ_BATCH_ITEMS { conn.execute( "UPDATE sandboxes SET process_env_json = ?2 WHERE id = ?1", - rusqlite::params![id, env], + crate::db::fixture::params![id, env], ) .unwrap(); } @@ -6528,7 +6525,11 @@ pub(crate) mod service_tests { "INSERT INTO sandbox_cleanup_episodes (sandbox_id,cleanup_id,completion,keep_filesystem,phase,activation_state,created_at) VALUES (?1,?2,?3,0,'armed','creating',1)", - rusqlite::params![id, SandboxCleanupEpisodeId::new().to_string(), completion], + crate::db::fixture::params![ + id, + SandboxCleanupEpisodeId::new().to_string(), + completion + ], ) .unwrap(); let service = service_for_teardown_test(conn); @@ -6658,25 +6659,28 @@ pub(crate) mod service_tests { } } - pub(crate) fn insert_test_sandbox(conn: &rusqlite::Connection, id: &str, state: SandboxState) { + pub(crate) fn insert_test_sandbox(conn: &crate::db::Connection, id: &str, state: SandboxState) { insert_test_sandbox_with_policy(conn, id, state, &AutoLifecyclePolicy::disabled()); } - fn mark_test_remote_restore_artifact_staging(conn: &rusqlite::Connection, id: &str) { + fn mark_test_remote_restore_artifact_staging(conn: &crate::db::Connection, id: &str) { conn.execute( "UPDATE sandboxes SET restore_origin = 'remote', restore_phase = 'metadata_staging', restore_storage_uri = ?2 WHERE id = ?1 AND state = 'restoring'", - rusqlite::params![id, format!("s3://snapshots/suspend/{id}?region=us-east-1")], + crate::db::fixture::params![ + id, + format!("s3://snapshots/suspend/{id}?region=us-east-1") + ], ) .unwrap(); assert!(crate::db::record_remote_restore_artifact_staging(conn, id).unwrap()); } pub(crate) fn insert_test_sandbox_with_runtime( - conn: &rusqlite::Connection, + conn: &crate::db::Connection, id: &str, state: SandboxState, ) { @@ -6704,19 +6708,19 @@ pub(crate) mod service_tests { if state != SandboxState::Started { conn.execute( "UPDATE sandboxes SET state = ?1 WHERE id = ?2", - rusqlite::params![state.as_str(), id], + crate::db::fixture::params![state.as_str(), id], ) .unwrap(); } } - fn insert_started_test_sandbox(conn: &rusqlite::Connection, id: &str) { + fn insert_started_test_sandbox(conn: &crate::db::Connection, id: &str) { insert_test_sandbox_with_runtime(conn, id, SandboxState::Started); } /// Move a live `started` row to `restoring`, keeping its empty restore /// columns. No production edge writes this. - fn restage_started_as_restoring(conn: &rusqlite::Connection, id: &str) { + fn restage_started_as_restoring(conn: &crate::db::Connection, id: &str) { assert_eq!( crate::db::update_state_from_states( conn, @@ -6731,13 +6735,13 @@ pub(crate) mod service_tests { ); } - pub(crate) fn complete_runtime_endpoint_identity(conn: &rusqlite::Connection, id: &str) { + pub(crate) fn complete_runtime_endpoint_identity(conn: &crate::db::Connection, id: &str) { conn.execute( "UPDATE sandboxes SET space_api_route_execution_generation = 1, space_api_route_activation_attempt_id = ?1 WHERE id = ?2", - rusqlite::params![SandboxActivationAttemptId::new().to_string(), id], + crate::db::fixture::params![SandboxActivationAttemptId::new().to_string(), id], ) .unwrap(); } @@ -6782,7 +6786,7 @@ pub(crate) mod service_tests { } fn insert_platform_owned_sandbox( - conn: &rusqlite::Connection, + conn: &crate::db::Connection, id: &str, state: SandboxState, pool_template: Option<&str>, @@ -7002,7 +7006,7 @@ pub(crate) mod service_tests { insert_platform_owned_sandbox(&conn, &sandbox_id, SandboxState::Restoring, None); conn.execute( "UPDATE sandboxes SET updated_at = '0' WHERE id = ?1", - rusqlite::params![sandbox_id], + crate::db::fixture::params![sandbox_id], ) .unwrap(); let service = service_with_runtime_probe(conn, false); @@ -7028,7 +7032,7 @@ pub(crate) mod service_tests { ); conn.execute( "UPDATE sandboxes SET updated_at = '0' WHERE id = ?1", - rusqlite::params![sandbox_id], + crate::db::fixture::params![sandbox_id], ) .unwrap(); let service = service_with_runtime_probe(conn, false); @@ -7083,7 +7087,7 @@ pub(crate) mod service_tests { } conn.execute( "UPDATE sandboxes SET updated_at = '0' WHERE id = ?1", - rusqlite::params![sandbox_id], + crate::db::fixture::params![sandbox_id], ) .unwrap(); if state == SandboxState::Quarantined { @@ -7155,7 +7159,7 @@ pub(crate) mod service_tests { insert_platform_owned_sandbox(&conn, &sandbox_id, SandboxState::Stopped, None); conn.execute( "UPDATE sandboxes SET updated_at = '0' WHERE id = ?1", - rusqlite::params![sandbox_id], + crate::db::fixture::params![sandbox_id], ) .unwrap(); let service = service_with_runtime_probe(conn, false); @@ -7236,7 +7240,7 @@ pub(crate) mod service_tests { } conn.execute( "UPDATE sandboxes SET updated_at = '0' WHERE id = ?1", - rusqlite::params![sandbox_id], + crate::db::fixture::params![sandbox_id], ) .unwrap(); let service = service_with_runtime_probe(conn, false); @@ -7354,7 +7358,7 @@ pub(crate) mod service_tests { conn.execute( "UPDATE sandboxes SET updated_at = ?1, restore_origin = 'remote', \ restore_phase = 'metadata_staging', restore_storage_uri = ?3 WHERE id = ?2", - rusqlite::params![ + crate::db::fixture::params![ decided_version, sandbox_id, format!("s3://snapshots/suspend/{sandbox_id}?region=us-east-1") @@ -7397,7 +7401,7 @@ pub(crate) mod service_tests { /// Assembled out of the production writers rather than raw SQL so the /// fixture cannot drift from the shape those writers actually produce. pub(crate) fn insert_stranded_teardown_suspend_sandbox( - conn: &rusqlite::Connection, + conn: &crate::db::Connection, sandbox_id: SandboxId, execution_generation: ExecutionGeneration, ) -> LocalArtifactId { @@ -7433,22 +7437,22 @@ pub(crate) mod service_tests { local_artifact_id } - fn set_test_row_version(conn: &rusqlite::Connection, sandbox_id: &str, updated_at: &str) { + fn set_test_row_version(conn: &crate::db::Connection, sandbox_id: &str, updated_at: &str) { conn.execute( "UPDATE sandboxes SET updated_at = ?1 WHERE id = ?2", - rusqlite::params![updated_at, sandbox_id], + crate::db::fixture::params![updated_at, sandbox_id], ) .unwrap(); } fn set_test_state_changed_at( - conn: &rusqlite::Connection, + conn: &crate::db::Connection, sandbox_id: &str, state_changed_at: &str, ) { conn.execute( "UPDATE sandboxes SET state_changed_at = ?1 WHERE id = ?2", - rusqlite::params![state_changed_at, sandbox_id], + crate::db::fixture::params![state_changed_at, sandbox_id], ) .unwrap(); } @@ -7547,10 +7551,7 @@ pub(crate) mod service_tests { impl RowToucher { /// Start touching; returns once the first touch has landed. - async fn start( - db: Arc>, - sandbox_id: String, - ) -> Self { + async fn start(db: crate::db::SharedConnection, sandbox_id: String) -> Self { let stop = Arc::new(std::sync::atomic::AtomicBool::new(false)); let flag = Arc::clone(&stop); let (landed, first_touch) = tokio::sync::oneshot::channel(); @@ -7563,7 +7564,7 @@ pub(crate) mod service_tests { touches += db .execute( "UPDATE sandboxes SET updated_at = ?1 WHERE id = ?2", - rusqlite::params![ + crate::db::fixture::params![ crate::sandbox_eviction::now_secs().to_string(), sandbox_id ], @@ -7667,7 +7668,7 @@ pub(crate) mod service_tests { // above and may move again at any point during the RPC. Nothing here // forces a write into the gap between the decision and the claim; // the claim must commit whether or not one lands there. - let toucher = RowToucher::start(Arc::clone(&service.db), sandbox_id_string.clone()).await; + let toucher = RowToucher::start(service.db.clone(), sandbox_id_string.clone()).await; let evicted = tokio::time::timeout( Duration::from_secs(5), service.evict_cluster_deleted_sandbox(root_peer_request(&sandbox_id_string)), @@ -7820,7 +7821,7 @@ pub(crate) mod service_tests { current_local_artifact_kind = NULL, current_local_artifact_origin = NULL WHERE id = ?1", - rusqlite::params![sandbox_id_string], + crate::db::fixture::params![sandbox_id_string], ) .unwrap(); set_test_state_changed_at(&conn, &sandbox_id_string, "1000000000"); @@ -7859,7 +7860,7 @@ pub(crate) mod service_tests { if let Some(reason) = failure_reason { conn.execute( "UPDATE sandboxes SET failure_reason = ?1 WHERE id = ?2", - rusqlite::params![reason, sandbox_id_string], + crate::db::fixture::params![reason, sandbox_id_string], ) .unwrap(); } @@ -7880,7 +7881,7 @@ pub(crate) mod service_tests { .unwrap() .query_row( "SELECT current_local_artifact_id IS NOT NULL FROM sandboxes WHERE id = ?1", - rusqlite::params![sandbox_id_string], + crate::db::fixture::params![sandbox_id_string], |row| row.get::<_, bool>(0), ) .unwrap(), @@ -7979,7 +7980,7 @@ pub(crate) mod service_tests { ); } - fn state_changed_at(conn: &rusqlite::Connection, sandbox_id: &str) -> String { + fn state_changed_at(conn: &crate::db::Connection, sandbox_id: &str) -> String { crate::db::sandbox_state_changed_at_witness(conn, sandbox_id) .unwrap() .unwrap() @@ -7995,7 +7996,7 @@ pub(crate) mod service_tests { // artifact, and `state_changed_at` — a tuple nothing can leave intact // while moving the sandbox on. let execution_generation = ExecutionGeneration::new(7).unwrap(); - let claim_after = |mutate: &dyn Fn(&rusqlite::Connection, &str, LocalArtifactId)| { + let claim_after = |mutate: &dyn Fn(&crate::db::Connection, &str, LocalArtifactId)| { let conn = crate::db::test_conn(); let sandbox_id = SandboxId::new(); let sandbox_id_string = sandbox_id.to_string(); @@ -8033,7 +8034,7 @@ pub(crate) mod service_tests { !claim_after(&|conn, id, _| { conn.execute( "UPDATE sandboxes SET current_local_artifact_id = ?1 WHERE id = ?2", - rusqlite::params![LocalArtifactId::new().to_string(), id], + crate::db::fixture::params![LocalArtifactId::new().to_string(), id], ) .unwrap(); }), @@ -8043,7 +8044,7 @@ pub(crate) mod service_tests { !claim_after(&|conn, id, _| { conn.execute( "UPDATE sandboxes SET failure_reason = NULL WHERE id = ?1", - rusqlite::params![id], + crate::db::fixture::params![id], ) .unwrap(); }), @@ -8053,7 +8054,7 @@ pub(crate) mod service_tests { !claim_after(&|conn, id, _| { conn.execute( "UPDATE sandboxes SET state = 'suspended' WHERE id = ?1", - rusqlite::params![id], + crate::db::fixture::params![id], ) .unwrap(); }), @@ -8166,7 +8167,7 @@ pub(crate) mod service_tests { insert_platform_owned_sandbox(&conn, &sandbox_id, SandboxState::Suspended, None); conn.execute( "UPDATE sandboxes SET updated_at = 'not-a-timestamp' WHERE id = ?1", - rusqlite::params![sandbox_id], + crate::db::fixture::params![sandbox_id], ) .unwrap(); let service = service_with_runtime_probe(conn, false); @@ -8187,7 +8188,7 @@ pub(crate) mod service_tests { insert_platform_owned_sandbox(&conn, &sandbox_id, SandboxState::Suspended, None); conn.execute( "UPDATE sandboxes SET updated_at = '0' WHERE id = ?1", - rusqlite::params![sandbox_id], + crate::db::fixture::params![sandbox_id], ) .unwrap(); let service = service_with_runtime_probe(conn, false); @@ -8337,7 +8338,7 @@ pub(crate) mod service_tests { assert!( !SandboxManagerService::cleanup_attempt_still_current( - &std::sync::Arc::new(std::sync::Mutex::new(conn)), + &crate::db::SharedConnection::new(conn), &sandbox_id.to_string(), CleanupCompletion::DeleteRow, ) @@ -8394,7 +8395,7 @@ pub(crate) mod service_tests { SET cleanup_completion = 'mark_failed', cleanup_keep_filesystem = TRUE WHERE id = ?1", - rusqlite::params![sandbox_id], + crate::db::fixture::params![sandbox_id], ) .unwrap(); conn.execute( @@ -8404,7 +8405,7 @@ pub(crate) mod service_tests { quarantine_reason_code, failure_message, created_at ) VALUES (?1, ?2, 'mark_failed', TRUE, 'requested', 'restoring', 'restoring', 1, 'restore_prepare_failed', 'cleanup requested', 1)", - rusqlite::params![sandbox_id, cleanup_id.to_string()], + crate::db::fixture::params![sandbox_id, cleanup_id.to_string()], ) .unwrap(); let capture = SandboxEventCapture::default(); @@ -8544,7 +8545,7 @@ pub(crate) mod service_tests { /// A service whose runtime probe answers `runtime_live` for every sandbox /// instead of reading the host's run directory. pub(crate) fn service_with_runtime_probe( - conn: rusqlite::Connection, + conn: crate::db::Connection, runtime_live: bool, ) -> SandboxManagerService { SandboxManagerService::new_with_options( @@ -8558,7 +8559,7 @@ pub(crate) mod service_tests { /// Teardown tests compare route identities against this node's identity, /// [`crate::service::test_options::test_node_id`]. - pub(crate) fn service_for_teardown_test(conn: rusqlite::Connection) -> SandboxManagerService { + pub(crate) fn service_for_teardown_test(conn: crate::db::Connection) -> SandboxManagerService { SandboxManagerService::for_test(conn) } @@ -8978,7 +8979,7 @@ pub(crate) mod service_tests { current_local_artifact_kind = NULL, current_local_artifact_origin = NULL WHERE id = ?1", - rusqlite::params![sandbox_id.to_string()], + crate::db::fixture::params![sandbox_id.to_string()], ) .unwrap(); let legacy = crate::db::get_sandbox(&conn, &sandbox_id.to_string()) @@ -9095,7 +9096,7 @@ pub(crate) mod service_tests { // the actual recovery writers before attempting runtime publication. conn.execute( "UPDATE sandboxes SET state = 'restoring' WHERE id = ?1", - rusqlite::params![sandbox_id.to_string()], + crate::db::fixture::params![sandbox_id.to_string()], ) .unwrap(); let producing_generation = ExecutionGeneration::new(7).unwrap(); @@ -9176,7 +9177,7 @@ pub(crate) mod service_tests { "UPDATE sandboxes SET current_local_artifact_id = NULL, current_local_artifact_kind = NULL, current_local_artifact_origin = NULL WHERE id = ?1", - rusqlite::params![sandbox_id.to_string()], + crate::db::fixture::params![sandbox_id.to_string()], ) .unwrap(); Some(tonic::Code::FailedPrecondition) @@ -9184,7 +9185,10 @@ pub(crate) mod service_tests { Case::PriorNodeIdentity => { conn.execute( "UPDATE sandboxes SET space_api_route_node_id = ?1 WHERE id = ?2", - rusqlite::params![NodeId::new().to_string(), sandbox_id.to_string()], + crate::db::fixture::params![ + NodeId::new().to_string(), + sandbox_id.to_string() + ], ) .unwrap(); Some(tonic::Code::FailedPrecondition) @@ -9693,7 +9697,7 @@ pub(crate) mod service_tests { let current_generation = ExecutionGeneration::new(2).unwrap(); { - let mut conn = rusqlite::Connection::open(&db_path).unwrap(); + let mut conn = crate::db::Connection::open_at(&db_path).unwrap(); crate::db::create_test_schema(&mut conn); insert_test_sandbox(&conn, &sandbox_id.to_string(), SandboxState::Paused); let service = service_for_teardown_test(conn); @@ -9702,7 +9706,7 @@ pub(crate) mod service_tests { .unwrap(); } - let service = service_for_teardown_test(rusqlite::Connection::open(&db_path).unwrap()); + let service = service_for_teardown_test(crate::db::Connection::open_at(&db_path).unwrap()); let response = service .prepare_teardown_suspend(internal_request(prepare_teardown_suspend_request( sandbox_id.to_string(), @@ -10002,7 +10006,7 @@ pub(crate) mod service_tests { WHERE sandbox_id = ?1 AND state = 'pending' AND execution_generation < ?2", - rusqlite::params![sandbox_id.to_string(), 5], + crate::db::fixture::params![sandbox_id.to_string(), 5], ) .unwrap(); let service = service_for_teardown_test(conn); @@ -11027,8 +11031,7 @@ pub(crate) mod service_tests { #[tokio::test] async fn unpersisted_delete_clears_activity_without_replacing_live_lock_identity() { - let service = - SandboxManagerService::for_test(rusqlite::Connection::open_in_memory().unwrap()); + let service = SandboxManagerService::for_test(crate::db::Connection::open_in_memory()); let sandbox_id = "sbx-delete-persistence-failure"; let wake_before = service.auto_lifecycle.wake_lock(sandbox_id); service.auto_lifecycle.record_wake_completed(sandbox_id); @@ -11088,7 +11091,7 @@ pub(crate) mod service_tests { { let conn = service.db.lock().unwrap(); let bookkeeping = crate::snapshot_upload_jobs::spawn_backup_bookkeeping( - Arc::clone(&service.db), + service.db.clone(), &service.drain_state, &payloads[0], Arc::new(crate::retained_source::FakeInspector::new( @@ -11110,7 +11113,7 @@ pub(crate) mod service_tests { .expect("blocking bookkeeping must release ownership when it completes"); assert!( crate::snapshot_upload_jobs::spawn_backup_bookkeeping( - Arc::clone(&service.db), + service.db.clone(), &service.drain_state, &payloads[1], Arc::new(crate::retained_source::FakeInspector::new( @@ -11133,14 +11136,14 @@ pub(crate) mod service_tests { assert_eq!(stamped, completed); } pplx_space_common_jobs::db::prune_terminal_jobs( - &conn, + conn.raw(), pplx_space_common_jobs::COMPLETED_JOB_RETENTION, pplx_space_common_jobs::FAILED_JOB_RETENTION, ) .unwrap(); assert!( pplx_space_common_jobs::db::get_job( - &conn, + conn.raw(), &"019d84bd-280d-7331-99c6-b63377f3a3a2".parse().unwrap() ) .unwrap() @@ -11260,7 +11263,7 @@ pub(crate) mod service_tests { db.execute( "UPDATE local_artifacts SET backup_completed_at = NULL WHERE local_artifact_id = ?1", - rusqlite::params![local_artifact_id.to_string()], + crate::db::fixture::params![local_artifact_id.to_string()], ) .unwrap(); } @@ -11281,7 +11284,7 @@ pub(crate) mod service_tests { } fn delete_sandbox_with_generation_receipt( - conn: &rusqlite::Connection, + conn: &crate::db::Connection, sandbox_id: &str, deleted_generation: ExecutionGeneration, ) { @@ -11397,7 +11400,7 @@ pub(crate) mod service_tests { let generation = ExecutionGeneration::new(7).unwrap(); { crate::db::create_test_schema_on_disk(&db_path); - let conn = rusqlite::Connection::open(&db_path).unwrap(); + let conn = crate::db::Connection::open_at(&db_path).unwrap(); insert_test_sandbox(&conn, &id, target); let fields = runtime_fields(generation, 7); let episode = crate::db::begin_activation( @@ -11451,7 +11454,7 @@ pub(crate) mod service_tests { .operation .unwrap(); pplx_space_common_jobs::db::fail_job_permanent( - &conn, + conn.raw(), &operation.job_id, "suspend publication failed", None, @@ -11491,7 +11494,7 @@ pub(crate) mod service_tests { .unwrap(); if case == "cleanup_episode" { // A conflicting episode must block even with a dormant row. - conn.execute("UPDATE sandboxes SET state = ?2, cleanup_completion = NULL, cleanup_keep_filesystem = NULL WHERE id = ?1", rusqlite::params![id, target.as_str()]).unwrap(); + conn.execute("UPDATE sandboxes SET state = ?2, cleanup_completion = NULL, cleanup_keep_filesystem = NULL WHERE id = ?1", crate::db::fixture::params![id, target.as_str()]).unwrap(); } else { crate::db::update_runtime_fields_for_activation( &conn, @@ -11512,11 +11515,12 @@ pub(crate) mod service_tests { } } // A retained historical receipt must not override a new owner. - conn.execute("UPDATE sandboxes SET transition_receipt_id = ?2, transition_receipt_kind = ?3, transition_receipt_target_state = ?4, transition_receipt_execution_generation = ?5, transition_receipt_reason_code = ?6, transition_receipt_activation_attempt_id = ?7 WHERE id = ?1", rusqlite::params![id, old_receipt.transition_receipt_id, old_receipt.transition_receipt_kind, old_receipt.transition_receipt_target_state, old_receipt.transition_receipt_execution_generation, old_receipt.transition_receipt_reason_code, old_receipt.transition_receipt_activation_attempt_id]).unwrap(); + conn.execute("UPDATE sandboxes SET transition_receipt_id = ?2, transition_receipt_kind = ?3, transition_receipt_target_state = ?4, transition_receipt_execution_generation = ?5, transition_receipt_reason_code = ?6, transition_receipt_activation_attempt_id = ?7 WHERE id = ?1", crate::db::fixture::params![id, old_receipt.transition_receipt_id, old_receipt.transition_receipt_kind, old_receipt.transition_receipt_target_state, old_receipt.transition_receipt_execution_generation, old_receipt.transition_receipt_reason_code, old_receipt.transition_receipt_activation_attempt_id]).unwrap(); } } - let service = - service_for_teardown_test(rusqlite::Connection::open(&db_path).unwrap()); + let service = service_for_teardown_test( + crate::db::Connection::open_at(&db_path).unwrap(), + ); let before = crate::db::get_sandbox(&service.db.lock().unwrap(), &id) .unwrap() .unwrap(); @@ -11634,11 +11638,11 @@ pub(crate) mod service_tests { let sandbox_id = SandboxId::new().to_string(); let deleted_generation = ExecutionGeneration::new(2).unwrap(); { - let mut conn = rusqlite::Connection::open(&db_path).unwrap(); + let mut conn = crate::db::Connection::open_at(&db_path).unwrap(); crate::db::create_test_schema(&mut conn); delete_sandbox_with_generation_receipt(&conn, &sandbox_id, deleted_generation); } - let service = service_for_teardown_test(rusqlite::Connection::open(&db_path).unwrap()); + let service = service_for_teardown_test(crate::db::Connection::open_at(&db_path).unwrap()); let deleted = service .prepare_teardown_suspend(internal_request(prepare_teardown_suspend_request( @@ -11683,12 +11687,12 @@ pub(crate) mod service_tests { .as_secs() as i64; conn.execute( "UPDATE sandbox_execution_delete_receipts SET deleted_at = ?1 WHERE sandbox_id = ?2", - rusqlite::params![now - 7 * 24 * 60 * 60 + 60, retained_sandbox_id], + crate::db::fixture::params![now - 7 * 24 * 60 * 60 + 60, retained_sandbox_id], ) .unwrap(); conn.execute( "UPDATE sandbox_execution_delete_receipts SET deleted_at = ?1 WHERE sandbox_id = ?2", - rusqlite::params![now - 7 * 24 * 60 * 60 - 1, expired_sandbox_id], + crate::db::fixture::params![now - 7 * 24 * 60 * 60 - 1, expired_sandbox_id], ) .unwrap(); let service = service_for_teardown_test(conn); @@ -12436,7 +12440,7 @@ pub(crate) mod service_tests { /// [`service_with_runtime_probe`] with its host dependencies under the /// caller's `sockets` directory. pub(crate) fn service_with_runtime_liveness_and_absent_sockets( - conn: rusqlite::Connection, + conn: crate::db::Connection, runtime_live: bool, sockets: &std::path::Path, ) -> SandboxManagerService { @@ -12449,7 +12453,7 @@ pub(crate) mod service_tests { ) } - fn insert_suspending_resuspend(conn: &rusqlite::Connection, id: &str) -> LocalArtifactId { + fn insert_suspending_resuspend(conn: &crate::db::Connection, id: &str) -> LocalArtifactId { insert_test_sandbox(conn, id, SandboxState::Paused); let predecessor = LocalArtifactId::new(); conn.execute( @@ -12458,7 +12462,7 @@ pub(crate) mod service_tests { current_local_artifact_kind = 'full', current_local_artifact_origin = 'suspend' WHERE id = ?2", - rusqlite::params![predecessor.to_string(), id], + crate::db::fixture::params![predecessor.to_string(), id], ) .unwrap(); assert!(crate::db::begin_suspend(conn, id).unwrap()); @@ -13103,7 +13107,7 @@ pub(crate) mod service_tests { insert_test_sandbox(&conn, &sandbox_id, SandboxState::Suspending); conn.execute( "UPDATE sandboxes SET uid = ?1, spaced_host_pid = ?2, netgtw_pid = ?3 WHERE id = ?4", - rusqlite::params![20_001, 30_001, 30_002, sandbox_id], + crate::db::fixture::params![20_001, 30_001, 30_002, sandbox_id], ) .unwrap(); @@ -13120,7 +13124,7 @@ pub(crate) mod service_tests { let db = service.db.lock().expect("mutex poisoned"); db.execute( "UPDATE sandboxes SET spaced_host_pid = ?1, netgtw_pid = ?2 WHERE id = ?3", - rusqlite::params![40_001, 40_002, sandbox_id], + crate::db::fixture::params![40_001, 40_002, sandbox_id], ) .unwrap(); } @@ -13296,7 +13300,7 @@ pub(crate) mod service_tests { assert!(row.failure_reason.is_none()); } - fn insert_orphaned_test_sandbox(conn: &rusqlite::Connection, id: &str, state: SandboxState) { + fn insert_orphaned_test_sandbox(conn: &crate::db::Connection, id: &str, state: SandboxState) { let sandbox_id = SandboxId::from_str(id).unwrap(); let uuid8 = SandboxRuntimeLabel::from_sandbox_id(sandbox_id); crate::db::insert_sandbox( @@ -13326,7 +13330,7 @@ pub(crate) mod service_tests { } pub(crate) fn insert_test_sandbox_with_policy( - conn: &rusqlite::Connection, + conn: &crate::db::Connection, id: &str, state: SandboxState, auto_lifecycle: &AutoLifecyclePolicy, @@ -13425,8 +13429,8 @@ pub(crate) mod service_tests { #[tokio::test] async fn dependency_drain_after_cleanup_commit_releases_rpc_and_recovers_job() { + use crate::db::OptionalExtension as _; use pplx_space_common_jobs::db as jobs; - use rusqlite::OptionalExtension as _; let conn = crate::db::test_conn(); let sandbox_id = SandboxId::new().to_string(); @@ -13505,7 +13509,7 @@ pub(crate) mod service_tests { .unwrap(); let work_file = locks.open_work(); try_lock_exclusive(&work_file).expect("replacement must acquire the work gate"); - let job = jobs::get_job(&service.db.lock().unwrap(), &job_id) + let job = jobs::get_job(service.db.lock().unwrap().raw(), &job_id) .unwrap() .unwrap(); assert_eq!(job.status, "pending"); @@ -13518,7 +13522,7 @@ pub(crate) mod service_tests { drop(admission); // Advance the persisted retry deadline without waiting for backoff. jobs::retry_job( - &service.db.lock().unwrap(), + service.db.lock().unwrap().raw(), &job_id, job.error.as_deref().unwrap(), 0, @@ -13527,7 +13531,7 @@ pub(crate) mod service_tests { service.ensure_cleanup_executor(); tokio::time::timeout(Duration::from_secs(5), async { loop { - if jobs::get_job(&service.db.lock().unwrap(), &job_id) + if jobs::get_job(service.db.lock().unwrap().raw(), &job_id) .unwrap() .unwrap() .status @@ -14178,7 +14182,7 @@ pub(crate) mod service_tests { conn.execute("INSERT INTO sandbox_cleanup_episodes (sandbox_id, cleanup_id, completion, keep_filesystem, phase, activation_state, created_at) VALUES (?1, ?1, 'mark_stopped', 1, 'armed', 'starting', 0)", [id]).unwrap(); conn.execute( "UPDATE sandbox_cleanup_episodes SET cleanup_id = ?1 WHERE sandbox_id = ?2", - rusqlite::params![format!("malformed-{id}"), id], + crate::db::fixture::params![format!("malformed-{id}"), id], ) .unwrap(); } @@ -14240,7 +14244,7 @@ pub(crate) mod service_tests { for (position, candidate) in cohort.iter().enumerate() { conn.execute( "UPDATE sandboxes SET created_at = ?1 WHERE id = ?2", - rusqlite::params![format!("{position:03}"), candidate], + crate::db::fixture::params![format!("{position:03}"), candidate], ) .unwrap(); } @@ -14616,7 +14620,7 @@ pub(crate) mod service_tests { insert_test_sandbox(&conn, &original_id, SandboxState::Pausing); conn.execute( "UPDATE sandboxes SET id = ?1, pid = ?2 WHERE id = ?3", - rusqlite::params![malformed_id, std::process::id(), original_id], + crate::db::fixture::params![malformed_id, std::process::id(), original_id], ) .unwrap(); let stopping_id = SandboxId::new().to_string(); @@ -14944,7 +14948,7 @@ pub(crate) mod service_tests { for id in [&bad, &good] { insert_test_sandbox(&conn, id, SandboxState::Suspending); } - conn.execute("INSERT INTO jobs (job_id, kind, subject_id, payload) VALUES (?1, 'teardown_suspend', ?2, 'malformed')", rusqlite::params![uuid::Uuid::now_v7().to_string(), bad]).unwrap(); + conn.execute("INSERT INTO jobs (job_id, kind, subject_id, payload) VALUES (?1, 'teardown_suspend', ?2, 'malformed')", crate::db::fixture::params![uuid::Uuid::now_v7().to_string(), bad]).unwrap(); let cache = tempfile::tempdir().unwrap(); let artifact = LocalArtifactId::new(); let staging = suspend_snapshot_staging_dir(&cache.path().join(&good), artifact); @@ -15746,7 +15750,7 @@ pub(crate) mod service_tests { } fn seed_due_access_idle_sandbox( - conn: &rusqlite::Connection, + conn: &crate::db::Connection, sandbox_id: &str, timeout_secs: u64, idle_for_secs: u64, @@ -15852,14 +15856,14 @@ pub(crate) mod service_tests { } fn route_authorized_service( - conn: rusqlite::Connection, + conn: crate::db::Connection, sandbox_id: SandboxId, ) -> (SandboxManagerService, SpaceApiRouteEd25519Token) { route_authorized_service_for_port(conn, sandbox_id, None) } fn route_authorized_service_for_port( - conn: rusqlite::Connection, + conn: crate::db::Connection, sandbox_id: SandboxId, port: Option, ) -> (SandboxManagerService, SpaceApiRouteEd25519Token) { @@ -16126,7 +16130,7 @@ pub(crate) mod service_tests { .unwrap() .execute( "UPDATE sandboxes SET state = 'suspended' WHERE id = ?1", - rusqlite::params![sandbox_id], + crate::db::fixture::params![sandbox_id], ) .unwrap(); service.begin_draining_for_restart(); @@ -16989,7 +16993,7 @@ pub(crate) mod service_tests { "UPDATE sandboxes SET state = 'restoring', \ failure_reason = 'cross-node restore artifact staging failed: test', \ updated_at = ?1 WHERE id = ?2", - rusqlite::params![stale_updated_at, sandbox_id], + crate::db::fixture::params![stale_updated_at, sandbox_id], ) .unwrap(); let service = SandboxManagerService::for_test(conn); @@ -17076,7 +17080,7 @@ pub(crate) mod service_tests { "UPDATE sandboxes SET state = 'restoring', \ failure_reason = 'cross-node restore artifact staging failed: test', \ next_auto_event_at = 1, updated_at = ?1 WHERE id = ?2", - rusqlite::params![stale_updated_at, sandbox_id], + crate::db::fixture::params![stale_updated_at, sandbox_id], ) .unwrap(); let service = SandboxManagerService::for_test(conn); @@ -17517,7 +17521,7 @@ pub(crate) mod service_tests { ) .unwrap() .unwrap(); - let db = Arc::new(Mutex::new(conn)); + let db = crate::db::SharedConnection::new(conn); let targets = SandboxManagerService::recover_required_cleanup_targets( &db, @@ -17590,7 +17594,7 @@ pub(crate) mod service_tests { [&sandbox_id], ) .unwrap(); - let db = Arc::new(Mutex::new(conn)); + let db = crate::db::SharedConnection::new(conn); for authority in [ CleanupRecoveryAuthority::Startup, CleanupRecoveryAuthority::RequestedOnly, @@ -17663,7 +17667,7 @@ pub(crate) mod service_tests { current_local_artifact_kind = 'full', current_local_artifact_origin = 'suspend' WHERE id = ?2", - rusqlite::params![Uuid::now_v7().to_string(), &sandbox_id_string], + crate::db::fixture::params![Uuid::now_v7().to_string(), &sandbox_id_string], ) .unwrap(); let service = SandboxManagerService::for_test(conn); @@ -17993,7 +17997,7 @@ pub(crate) mod service_tests { } pub(in crate::service) fn register_full_suspend_artifact( - conn: &rusqlite::Connection, + conn: &crate::db::Connection, sandbox_id: &str, ) { // `adopt_suspended_local_artifact` registers-and-pins exactly the @@ -18223,7 +18227,7 @@ pub(crate) mod service_tests { current_local_artifact_kind = 'full', current_local_artifact_origin = 'suspend' WHERE id = ?2", - rusqlite::params![LocalArtifactId::new().to_string(), &sandbox_id], + crate::db::fixture::params![LocalArtifactId::new().to_string(), &sandbox_id], ) .unwrap(); // The test service's node-manager refuses every dial, so the @@ -18434,7 +18438,7 @@ pub(crate) mod service_tests { let staging_failure = "cross-node restore artifact staging failed: test"; conn.execute( "UPDATE sandboxes SET failure_reason = ?1 WHERE id = ?2", - rusqlite::params![staging_failure, sandbox_id], + crate::db::fixture::params![staging_failure, sandbox_id], ) .unwrap(); let restoring = crate::db::get_sandbox(&conn, &sandbox_id) @@ -18452,7 +18456,7 @@ pub(crate) mod service_tests { "UPDATE sandboxes SET cleanup_completion = 'delete_row', cleanup_keep_filesystem = 0 WHERE id = ?1", - rusqlite::params![sandbox_id], + crate::db::fixture::params![sandbox_id], ) .unwrap(); let pending_delete = crate::db::get_sandbox(&conn, &sandbox_id) @@ -19658,13 +19662,13 @@ pub(crate) mod service_tests { insert_test_sandbox(&conn, &failed_runtime_id, SandboxState::Failed); conn.execute( "UPDATE sandboxes SET uid = 10000, slot = 0 WHERE id = ?1", - rusqlite::params![&failed_runtime_id], + crate::db::fixture::params![&failed_runtime_id], ) .unwrap(); insert_test_sandbox(&conn, &started_runtime_id, SandboxState::Started); conn.execute( "UPDATE sandboxes SET uid = 10001, slot = 1, pid = ?1 WHERE id = ?2", - rusqlite::params![std::process::id(), &started_runtime_id], + crate::db::fixture::params![std::process::id(), &started_runtime_id], ) .unwrap(); insert_test_sandbox(&conn, &started_stale_intent_id, SandboxState::Started); @@ -19685,7 +19689,7 @@ pub(crate) mod service_tests { .unwrap(); }); - let db = Arc::new(Mutex::new(conn)); + let db = crate::db::SharedConnection::new(conn); let mut targets = SandboxManagerService::recover_required_cleanup_targets( &db, super::recovery::CleanupRecoveryAuthority::RequestedOnly, @@ -19815,7 +19819,7 @@ pub(crate) mod service_tests { transition_receipt_reason_code = 'cold_activation_interrupted', transition_receipt_activation_attempt_id = ?2 WHERE id = ?3", - rusqlite::params![&receipt_id, Uuid::now_v7().to_string(), &stop_id], + crate::db::fixture::params![&receipt_id, Uuid::now_v7().to_string(), &stop_id], ) .unwrap(); insert_test_sandbox(&conn, &delete_id, SandboxState::Created); @@ -19837,7 +19841,7 @@ pub(crate) mod service_tests { ) .unwrap(); - let db = Arc::new(Mutex::new(conn)); + let db = crate::db::SharedConnection::new(conn); SandboxManagerService::complete_async_cleanup( &db, &stop_id, @@ -19872,7 +19876,7 @@ pub(crate) mod service_tests { let retained_receipt = db .query_row( "SELECT transition_receipt_id FROM sandboxes WHERE id = ?1", - rusqlite::params![&stop_id], + crate::db::fixture::params![&stop_id], |row| row.get::<_, Option>(0), ) .unwrap(); @@ -19918,7 +19922,7 @@ pub(crate) mod service_tests { ) .unwrap(); - let db = Arc::new(Mutex::new(conn)); + let db = crate::db::SharedConnection::new(conn); SandboxManagerService::complete_async_cleanup( &db, &sandbox_id, @@ -19957,7 +19961,7 @@ pub(crate) mod service_tests { current_local_artifact_kind = 'full', current_local_artifact_origin = 'suspend' WHERE id = ?2", - rusqlite::params![local_artifact_id.to_string(), &sandbox_id], + crate::db::fixture::params![local_artifact_id.to_string(), &sandbox_id], ) .unwrap(); crate::db::set_cleanup_intent( @@ -19968,7 +19972,7 @@ pub(crate) mod service_tests { ) .unwrap(); - let db = Arc::new(Mutex::new(conn)); + let db = crate::db::SharedConnection::new(conn); SandboxManagerService::complete_async_cleanup( &db, &sandbox_id, @@ -20013,7 +20017,7 @@ pub(crate) mod service_tests { ) .unwrap(); - let db = Arc::new(Mutex::new(conn)); + let db = crate::db::SharedConnection::new(conn); let local_artifact_id = LocalArtifactId::new(); let local_path = format!("/space/cache/{sandbox_id}/snapshot"); SandboxManagerService::complete_async_cleanup( diff --git a/crates/libs/sbx/mgr/server/src/service/tests/cancelled_cleanup_rpc_tests.rs b/crates/libs/sbx/mgr/server/src/service/tests/cancelled_cleanup_rpc_tests.rs index 858086d16..d72797e4d 100644 --- a/crates/libs/sbx/mgr/server/src/service/tests/cancelled_cleanup_rpc_tests.rs +++ b/crates/libs/sbx/mgr/server/src/service/tests/cancelled_cleanup_rpc_tests.rs @@ -79,13 +79,13 @@ async fn cancelled_cleanup_rpcs_preserve_admitted_termination_strength() { let (job_id, persisted_strength) = { let db = service.db.lock().unwrap(); let job_id = pplx_space_common_jobs::db::find_active_job( - &db, + db.raw(), crate::db::cleanup_jobs::KIND, &id, ) .unwrap() .unwrap(); - let job = pplx_space_common_jobs::db::get_job(&db, &job_id) + let job = pplx_space_common_jobs::db::get_job(db.raw(), &job_id) .unwrap() .unwrap(); ( diff --git a/crates/libs/sbx/mgr/server/src/service/tests/local_backup_inventory_tests.rs b/crates/libs/sbx/mgr/server/src/service/tests/local_backup_inventory_tests.rs index 8d852885c..5d7b1f315 100644 --- a/crates/libs/sbx/mgr/server/src/service/tests/local_backup_inventory_tests.rs +++ b/crates/libs/sbx/mgr/server/src/service/tests/local_backup_inventory_tests.rs @@ -17,7 +17,7 @@ fn pin_artifact( local_artifact_id, sandbox_id, artifact_kind, artifact_origin, snapshot_name, local_path, created_at, backup_completed_at ) VALUES (?1, ?2, ?3, ?4, NULL, ?5, '0', ?6)", - rusqlite::params![ + crate::db::fixture::params![ artifact_id.to_string(), sandbox_id, kind, @@ -33,7 +33,7 @@ fn pin_artifact( current_local_artifact_kind = ?2, current_local_artifact_origin = ?3 WHERE id = ?4", - rusqlite::params![artifact_id.to_string(), kind, origin, sandbox_id], + crate::db::fixture::params![artifact_id.to_string(), kind, origin, sandbox_id], ) .unwrap(); } diff --git a/crates/libs/sbx/mgr/server/src/service/tests/node_manager_transport_tests.rs b/crates/libs/sbx/mgr/server/src/service/tests/node_manager_transport_tests.rs index e9e9e905a..76db5dcd3 100644 --- a/crates/libs/sbx/mgr/server/src/service/tests/node_manager_transport_tests.rs +++ b/crates/libs/sbx/mgr/server/src/service/tests/node_manager_transport_tests.rs @@ -23,7 +23,7 @@ use pplx_space_node_mgr_client::proto::{ /// A test service whose node-manager clients all come from `node_manager`. pub(crate) fn service_with_node_manager( - conn: rusqlite::Connection, + conn: crate::db::Connection, node_manager: NodeManagerConnector, ) -> SandboxManagerService { SandboxManagerService::new_with_options( @@ -124,7 +124,7 @@ async fn fatal_kill_signal_failure_still_schedules_admitted_cleanup() { /// A suspending encrypted row whose capture was recorded `age` ago and never /// sealed: its staging holds only the record, built from real metadata. fn record_encrypted_capture( - conn: &rusqlite::Connection, + conn: &crate::db::Connection, cache: &Path, id: &str, age: std::time::Duration, @@ -454,7 +454,7 @@ async fn stats_answers_without_cgroup_figures_while_node_manager_is_down() { "UPDATE sandboxes SET uid = 10000, slot = 7, network_tx_bytes = 1234, network_rx_bytes = 5678 WHERE id = ?1", - rusqlite::params![sandbox_id], + crate::db::fixture::params![sandbox_id], ) .unwrap(); let service = service_with_node_manager( @@ -520,7 +520,7 @@ async fn stats_answers_without_cgroup_figures_when_node_manager_stalls() { insert_test_sandbox(&conn, &sandbox_id, SandboxState::Started); conn.execute( "UPDATE sandboxes SET uid = 10000, slot = 7 WHERE id = ?1", - rusqlite::params![sandbox_id], + crate::db::fixture::params![sandbox_id], ) .unwrap(); let node_manager = StalledNodeManager::bind(); diff --git a/crates/libs/sbx/mgr/server/src/snapshot_gc.rs b/crates/libs/sbx/mgr/server/src/snapshot_gc.rs index 09194167e..eb07823ab 100644 --- a/crates/libs/sbx/mgr/server/src/snapshot_gc.rs +++ b/crates/libs/sbx/mgr/server/src/snapshot_gc.rs @@ -59,12 +59,11 @@ use std::collections::HashSet; use std::os::unix::fs::MetadataExt; use std::path::{Path, PathBuf}; -use std::sync::{Arc, Mutex}; +use std::sync::Arc; use std::time::{Duration, Instant}; use pplx_space_common_core::types::LocalArtifactId; use pplx_space_common_lru_reaper::{self as lru_reaper, EvictionInput}; -use rusqlite::Connection; use tokio_util::sync::CancellationToken; use tracing::{debug, info, warn}; @@ -366,7 +365,7 @@ fn execute_one( async fn gc_tick( artifacts: LocalArtifacts, - db: Arc>, + db: crate::db::SharedConnection, cfg: GcConfig, now: u64, disk: crate::disk_usage::DiskUsage, @@ -384,7 +383,7 @@ async fn gc_tick( let need_to_free = disk.bytes_over_target(cfg.target_fraction); let idle_secs = cfg.idle.as_secs(); - let db_for_snap = Arc::clone(&db); + let db_for_snap = db.clone(); let mut next_scan = *scan; let (next_scan, page) = tokio::task::spawn_blocking(move || { let conn = crate::node_state_sync::connection_or_signal_failure(&db_for_snap)?; @@ -776,7 +775,7 @@ fn gc_pending_is_unowned(artifacts: &LocalArtifacts, path: &Path) -> bool { /// `state = 'restoring'`. pub(crate) async fn run_loop( artifacts: LocalArtifacts, - db: Arc>, + db: crate::db::SharedConnection, active_fork_copy_operations: Arc, cfg: GcConfig, ownership: GcOwnership, @@ -847,7 +846,7 @@ pub(crate) async fn run_loop( // or cancellation of this supervising future. gc_tick( artifacts.clone(), - Arc::clone(&db), + db.clone(), cfg, now_secs(), disk, diff --git a/crates/libs/sbx/mgr/server/src/snapshot_gc/tests.rs b/crates/libs/sbx/mgr/server/src/snapshot_gc/tests.rs index 564ea4c22..619ff7b25 100644 --- a/crates/libs/sbx/mgr/server/src/snapshot_gc/tests.rs +++ b/crates/libs/sbx/mgr/server/src/snapshot_gc/tests.rs @@ -1,7 +1,9 @@ use super::*; -use rusqlite::params; +use crate::db::Connection; +use crate::db::fixture::params; use std::fs; use std::io::Write as _; +use std::sync::{Arc, Mutex}; use tempfile::TempDir; fn open_test_db() -> Connection { @@ -138,8 +140,8 @@ async fn malformed_artifact_times_are_retained_without_blocking_valid_siblings() [TEST_ART_ID], ) .unwrap(); - let db = Arc::new(Mutex::new(conn)); - let artifacts = LocalArtifacts::new(Arc::clone(&db)); + let db = crate::db::SharedConnection::new(conn); + let artifacts = LocalArtifacts::new(db.clone()); // A timestamp corrupted between planning and execution must also fail closed. execute_one( &artifacts, @@ -218,8 +220,8 @@ async fn reaps_idle_uploaded_unpinned_artifact() { }) .serve(); - let db = Arc::new(Mutex::new(conn)); - let artifacts = LocalArtifacts::new(Arc::clone(&db)); + let db = crate::db::SharedConnection::new(conn); + let artifacts = LocalArtifacts::new(db.clone()); let ownership = GcOwnership { node_manager: node_manager.connector(), ..test_ownership() @@ -227,7 +229,7 @@ async fn reaps_idle_uploaded_unpinned_artifact() { let mut scan = ArtifactScan::default(); let report = within_node_manager_wait(gc_tick( artifacts, - Arc::clone(&db), + db.clone(), test_cfg(), now_secs(), full_disk(), @@ -264,8 +266,8 @@ async fn does_not_reap_below_engage_fraction() { insert_artifact_row(&conn, TEST_ART_ID, "sbx1", &path, 0, 0, Some(0)); insert_sandbox_row(&conn, "sbx1", None); - let db = Arc::new(Mutex::new(conn)); - let artifacts = LocalArtifacts::new(Arc::clone(&db)); + let db = crate::db::SharedConnection::new(conn); + let artifacts = LocalArtifacts::new(db.clone()); let mut cfg = test_cfg(); cfg.engage_fraction = 0.50; cfg.target_fraction = 0.40; @@ -275,7 +277,7 @@ async fn does_not_reap_below_engage_fraction() { }; let report = gc_tick( artifacts, - Arc::clone(&db), + db.clone(), cfg, now_secs(), disk, @@ -302,11 +304,11 @@ async fn does_not_reap_when_backup_not_completed() { insert_artifact_row(&conn, "art1", "sbx1", &path, 0, 0, None); insert_sandbox_row(&conn, "sbx1", None); - let db = Arc::new(Mutex::new(conn)); - let artifacts = LocalArtifacts::new(Arc::clone(&db)); + let db = crate::db::SharedConnection::new(conn); + let artifacts = LocalArtifacts::new(db.clone()); let report = gc_tick( artifacts, - Arc::clone(&db), + db.clone(), test_cfg(), now_secs(), full_disk(), @@ -329,11 +331,11 @@ async fn does_not_reap_committed_or_suspend_predecessor_pin() { insert_artifact_row(&conn, TEST_ART_ID, "sbx1", &path, 0, 0, Some(0)); insert_sandbox_row(&conn, "sbx1", Some(TEST_ART_ID)); - let db = Arc::new(Mutex::new(conn)); - let artifacts = LocalArtifacts::new(Arc::clone(&db)); + let db = crate::db::SharedConnection::new(conn); + let artifacts = LocalArtifacts::new(db.clone()); let report = gc_tick( artifacts, - Arc::clone(&db), + db.clone(), test_cfg(), now_secs(), full_disk(), @@ -362,10 +364,10 @@ async fn does_not_reap_committed_or_suspend_predecessor_pin() { .unwrap(); assert_eq!(page.artifacts.len(), 1); assert!(page.artifacts[0].pinned_current); - let artifacts = LocalArtifacts::new(Arc::clone(&db)); + let artifacts = LocalArtifacts::new(db.clone()); let report = gc_tick( artifacts, - Arc::clone(&db), + db.clone(), test_cfg(), now_secs(), full_disk(), @@ -389,13 +391,13 @@ async fn does_not_reap_when_not_idle() { insert_artifact_row(&conn, "art1", "sbx1", &path, now, now, Some(now)); insert_sandbox_row(&conn, "sbx1", None); - let db = Arc::new(Mutex::new(conn)); - let artifacts = LocalArtifacts::new(Arc::clone(&db)); + let db = crate::db::SharedConnection::new(conn); + let artifacts = LocalArtifacts::new(db.clone()); let mut cfg = test_cfg(); cfg.idle = Duration::from_secs(3600); let report = gc_tick( artifacts, - Arc::clone(&db), + db.clone(), cfg, now, full_disk(), @@ -422,8 +424,8 @@ async fn run_loop_startup_does_not_reset_live_restore_lease() { insert_artifact_row(&conn, TEST_ART_ID, "sbx1", &path, 0, 0, Some(0)); insert_sandbox_row(&conn, "sbx1", Some(TEST_ART_ID)); - let db = Arc::new(Mutex::new(conn)); - let artifacts = LocalArtifacts::new(Arc::clone(&db)); + let db = crate::db::SharedConnection::new(conn); + let artifacts = LocalArtifacts::new(db.clone()); let artifact_id = TEST_ART_ID.parse::().unwrap(); let lease = artifacts.check_out_for_restore(artifact_id).unwrap(); @@ -433,7 +435,7 @@ async fn run_loop_startup_does_not_reset_live_restore_lease() { let shutdown = ownership.shutdown.clone(); let reaper = run_loop( artifacts.clone(), - Arc::clone(&db), + db.clone(), Arc::new(crate::service::fork_artifact::CopyOperationReservations::default()), cfg, ownership, @@ -512,8 +514,8 @@ fn sweep_discovers_gc_pending_orphans_and_skips_unrelated() { #[test] fn gc_pending_reaping_requires_absent_registry_and_pin() { - let db = Arc::new(Mutex::new(open_test_db())); - let artifacts = LocalArtifacts::new(Arc::clone(&db)); + let db = crate::db::SharedConnection::new(open_test_db()); + let artifacts = LocalArtifacts::new(db.clone()); let root = TempDir::new().unwrap(); let path = root .path() @@ -643,8 +645,8 @@ async fn completed_restore_after_planning_still_honors_grace_when_idle_is_zero() let now = now_secs(); let observed = ArtifactScan::default().next_page(&conn).unwrap(); assert_eq!(observed.artifacts[0].last_touched_at_secs, 0); - let db = Arc::new(Mutex::new(conn)); - let artifacts = LocalArtifacts::new(Arc::clone(&db)); + let db = crate::db::SharedConnection::new(conn); + let artifacts = LocalArtifacts::new(db.clone()); artifacts .check_out_for_restore(TEST_ART_ID.parse().unwrap()) .unwrap() @@ -672,8 +674,8 @@ async fn gc_defers_to_publication_staging_drain_and_shutdown_ownership() { let tmp = TempDir::new().unwrap(); let path = make_artifact_dir(&tmp, "artifact", 4096); insert_artifact_row(&conn, TEST_ART_ID, "sbx1", &path, 0, 0, Some(0)); - let db = Arc::new(Mutex::new(conn)); - let artifacts = LocalArtifacts::new(Arc::clone(&db)); + let db = crate::db::SharedConnection::new(conn); + let artifacts = LocalArtifacts::new(db.clone()); let ownership = test_ownership(); for lock in [ ownership.coordinator.artifact_lock("sbx1"), @@ -682,7 +684,7 @@ async fn gc_defers_to_publication_staging_drain_and_shutdown_ownership() { let guard = lock.lock_owned().await; let report = gc_tick( artifacts.clone(), - Arc::clone(&db), + db.clone(), test_cfg(), now_secs(), full_disk(), @@ -704,7 +706,7 @@ async fn gc_defers_to_publication_staging_drain_and_shutdown_ownership() { }; let report = gc_tick( artifacts.clone(), - Arc::clone(&db), + db.clone(), test_cfg(), now_secs(), full_disk(), @@ -718,7 +720,7 @@ async fn gc_defers_to_publication_staging_drain_and_shutdown_ownership() { ownership.shutdown.cancel(); let report = gc_tick( artifacts.clone(), - Arc::clone(&db), + db.clone(), test_cfg(), now_secs(), full_disk(), diff --git a/crates/libs/sbx/mgr/server/src/snapshot_restore.rs b/crates/libs/sbx/mgr/server/src/snapshot_restore.rs index 35ee3c1ec..e5802b4ae 100644 --- a/crates/libs/sbx/mgr/server/src/snapshot_restore.rs +++ b/crates/libs/sbx/mgr/server/src/snapshot_restore.rs @@ -200,7 +200,7 @@ pub enum RestoreError { #[error("read sandbox row before remote restore: {source}")] ReadSandboxRow { #[source] - source: rusqlite::Error, + source: crate::db::Error, }, #[error("persist restoring sandbox row: {source}")] PersistRestoringRow { @@ -215,7 +215,7 @@ pub enum RestoreError { #[error("advance remote restore to artifact staging: {source}")] AdvanceRestorePhase { #[source] - source: rusqlite::Error, + source: crate::db::Error, }, #[error("sandbox {sandbox_id} left restoring before artifact staging was recorded")] RestorePhaseTransitionLost { sandbox_id: SandboxId }, @@ -224,17 +224,17 @@ pub enum RestoreError { #[error("read quarantine recovery claim: {source}")] ReadQuarantineRecoveryClaim { #[source] - source: rusqlite::Error, + source: crate::db::Error, }, #[error("renew quarantine recovery claim: {source}")] RenewQuarantineRecoveryClaim { #[source] - source: rusqlite::Error, + source: crate::db::Error, }, #[error("persist orphan quarantine recovery: {source}")] PersistOrphanQuarantineRecovery { #[source] - source: rusqlite::Error, + source: crate::db::Error, }, /// A `GetQuarantineRecoveryPlan` call failed with a status this release does /// not map. The code and the server's message are separate fields on purpose: @@ -396,7 +396,7 @@ impl RestoreError { Some(format!("plan_rpc_status={code:?}")) } // Safe: payloads this process built — typed ids, generations, - // timeouts, paths it constructed, and wrapped rusqlite/io sources. + // timeouts, paths it constructed, and wrapped SQLite/io sources. // Nothing here holds a string supplied by a caller, a peer, or // downloaded metadata. Self::SnapshotNotRegistered(..) diff --git a/crates/libs/sbx/mgr/server/src/snapshot_scheduler.rs b/crates/libs/sbx/mgr/server/src/snapshot_scheduler.rs index 59c714ead..ef53236b7 100644 --- a/crates/libs/sbx/mgr/server/src/snapshot_scheduler.rs +++ b/crates/libs/sbx/mgr/server/src/snapshot_scheduler.rs @@ -7,7 +7,7 @@ use std::collections::HashMap; use std::io::Write; -use std::sync::{Arc, MutexGuard}; +use std::sync::Arc; use std::time::Duration; use tokio::sync::Mutex; @@ -33,7 +33,7 @@ use pplx_space_node_runtime_core::fc_api::FirecrackerVmState; const TICK_INTERVAL: Duration = Duration::from_secs(60); -fn lock_db(ctx: &CheckpointContext) -> Result, String> { +fn lock_db(ctx: &CheckpointContext) -> Result, String> { ctx.db .lock() .map_err(|error| format!("database mutex poisoned: {error}")) @@ -45,7 +45,7 @@ fn lock_db(ctx: &CheckpointContext) -> Result Result<(), String> { +fn begin_checkpoint_state(db: &crate::db::Connection, sandbox_id: &str) -> Result<(), String> { use crate::db::{SandboxStateWriteNotApplied, StateWriteOutcome, StateWriteRefusal}; let edge = &crate::db::edges::CHECKPOINT_BEGIN; let not_applied = match crate::db::update_state_from(db, sandbox_id, edge) @@ -261,7 +261,7 @@ impl CheckpointTier { /// Shared context for checkpoint operations. pub struct CheckpointContext { - pub db: Arc>, + pub db: crate::db::SharedConnection, pub auto_lifecycle: crate::auto_lifecycle::AutoLifecycleCoordinator, pub health_lifecycle: crate::watchdog::HealthLifecycleEpochs, pub node_manager: crate::node_manager_transport::NodeManagerConnector, @@ -589,7 +589,7 @@ fn publish_named_artifact( sandbox_id, snapshot_name.as_str(), ); - crate::local_artifacts::LocalArtifacts::new(std::sync::Arc::clone(&ctx.db)) + crate::local_artifacts::LocalArtifacts::new(ctx.db.clone()) .publish_named_snapshot( sandbox_id, local_artifact_id, diff --git a/crates/libs/sbx/mgr/server/src/snapshot_scheduler/tests.rs b/crates/libs/sbx/mgr/server/src/snapshot_scheduler/tests.rs index 0e5c9e8cb..8ae33a59c 100644 --- a/crates/libs/sbx/mgr/server/src/snapshot_scheduler/tests.rs +++ b/crates/libs/sbx/mgr/server/src/snapshot_scheduler/tests.rs @@ -135,12 +135,12 @@ fn checkpoint_context(state: SandboxState) -> (CheckpointContext, String) { "INSERT INTO sandboxes (id, uuid8, state, template, user, created_at, updated_at) VALUES (?1, 'runtime1', ?2, 'seed', 'user:00000000-0000-0000-0000-000000000001', '1', '1')", - rusqlite::params![sandbox_id, state.as_str()], + crate::db::fixture::params![sandbox_id, state.as_str()], ) .unwrap(); ( CheckpointContext { - db: Arc::new(std::sync::Mutex::new(conn)), + db: crate::db::SharedConnection::new(conn), auto_lifecycle: crate::auto_lifecycle::AutoLifecycleCoordinator::default(), health_lifecycle: crate::watchdog::HealthLifecycleEpochs::default(), node_manager: crate::node_manager_transport::fake::unbound_in_absent_dir(), diff --git a/crates/libs/sbx/mgr/server/src/snapshot_upload_jobs.rs b/crates/libs/sbx/mgr/server/src/snapshot_upload_jobs.rs index 0fef00ab8..9952c2aba 100644 --- a/crates/libs/sbx/mgr/server/src/snapshot_upload_jobs.rs +++ b/crates/libs/sbx/mgr/server/src/snapshot_upload_jobs.rs @@ -61,7 +61,7 @@ const MAX_ATTEMPTS: i32 = 5; const TRANSFER_PROBE_TIMEOUT: Duration = Duration::from_secs(10); /// Build a `BackgroundWorker` configured for the snapshot-upload pipeline. -/// Caller owns the `rusqlite::Connection` (it lives alongside the rest of +/// Caller owns the `crate::db::Connection` (it lives alongside the rest of /// sbx-mgr's SQLite state) and is responsible for calling /// [`BackgroundWorker::recover_and_log`] at startup and /// [`BackgroundWorker::yield_for_restart`] at exit. @@ -70,7 +70,7 @@ const TRANSFER_PROBE_TIMEOUT: Duration = Duration::from_secs(10); /// sandbox-manager gives local finalization a bounded deploy-shutdown drain, /// while durable remote-upload work can resume after startup recovery. pub(crate) fn build_worker( - db: std::sync::Arc>, + db: crate::db::SharedConnection, drain_state: std::sync::Arc, node_manager: crate::node_manager_transport::NodeManagerConnector, ) -> BackgroundWorker { @@ -78,17 +78,19 @@ pub(crate) fn build_worker( .with_kind_concurrency(SNAPSHOT_UPLOAD_JOB_KIND, DEFAULT_CONCURRENCY) .with_recovery_job_kind(SNAPSHOT_UPLOAD_JOB_KIND); let executor = SnapshotUploadExecutor { - db: std::sync::Arc::clone(&db), + db: db.clone(), drain_state, retained_source_inspector: std::sync::Arc::new( crate::retained_source::NodeManagerSourceInspector(node_manager.clone()), ), node_manager, }; - BackgroundWorker::new(db, executor, config) + db.background_worker(executor, config) } -pub fn refund_interrupted_running_attempts(conn: &rusqlite::Connection) -> rusqlite::Result { +pub fn refund_interrupted_running_attempts( + conn: &crate::db::Connection, +) -> crate::db::Result { crate::db::snapshot_upload_jobs::refund_interrupted_running_attempts( conn, SNAPSHOT_UPLOAD_JOB_KIND, @@ -104,7 +106,7 @@ pub fn refund_interrupted_running_attempts(conn: &rusqlite::Connection) -> rusql /// same lock hold. #[derive(Clone)] pub struct SnapshotUploadExecutor { - db: std::sync::Arc>, + db: crate::db::SharedConnection, drain_state: std::sync::Arc, /// Sizes a retained source before its stream is reclaimed, so the local /// sidecar can switch to counting the image the artifact keeps. @@ -379,7 +381,7 @@ impl SnapshotUploadExecutor { // handle. Failure or panic leaves completed-job repair evidence; // maintenance retries the marker, while stream reclaim is optional. drop(spawn_backup_bookkeeping( - std::sync::Arc::clone(&self.db), + self.db.clone(), &self.drain_state, payload, std::sync::Arc::clone(&self.retained_source_inspector), @@ -420,11 +422,11 @@ async fn retained_image_for_reclaim( } pub(crate) fn spawn_backup_bookkeeping( - db: std::sync::Arc>, + db: crate::db::SharedConnection, drain_state: &std::sync::Arc, payload: &SnapshotUploadJobs, inspector: std::sync::Arc, -) -> Option>> { +) -> Option>> { // Admission is capped before spawning, so returning the upload worker's // slot cannot accumulate unbounded queued writers. Refusal defers repair. let writer = match crate::service::drain::acquire_state_writer( @@ -452,15 +454,15 @@ pub(crate) fn spawn_backup_bookkeeping( // writer's last mutation. let _writer = writer; let join_failed = |join: tokio::task::JoinError| { - rusqlite::Error::ToSqlConversionFailure(Box::new(std::io::Error::other(format!( + crate::db::Error::to_sql_conversion(std::io::Error::other(format!( "backup bookkeeping writer panicked: {join}" - )))) + ))) }; // The durability marker first, waiting on nothing but the database. // Whether a reclaim is due is read in the same hold, so node-manager // is asked only when the stream exists and the pin still stands. let reclaim_due = { - let db = std::sync::Arc::clone(&db); + let db = db.clone(); let payload = payload.clone(); tokio::task::spawn_blocking(move || { let conn = db.lock().unwrap_or_else(std::sync::PoisonError::into_inner); @@ -501,7 +503,7 @@ pub(crate) fn spawn_backup_bookkeeping( } pub(crate) fn persist_submitted_transfer_ids( - db: &std::sync::Arc>, + db: &crate::db::SharedConnection, job_id: &JobId, payload: &SnapshotUploadJobs, ) -> Result<(), JobError> { @@ -765,20 +767,20 @@ fn stream_reclaim_applies(meta: &SnapshotMeta) -> bool { /// The two bookkeeping steps in one hold, for tests of their combined effect. #[cfg(test)] fn stamp_backup_completed_and_reclaim_stream( - conn: &rusqlite::Connection, + conn: &crate::db::Connection, payload: &SnapshotUploadJobs, backup_attempt_id: &str, reclaim_image: Option, -) -> Result<(), rusqlite::Error> { +) -> Result<(), crate::db::Error> { stamp_backup_completed(conn, payload)?; reclaim_stream(conn, payload, backup_attempt_id, reclaim_image) } /// Stamp the artifact's durability marker. Nothing else waits on this. fn stamp_backup_completed( - conn: &rusqlite::Connection, + conn: &crate::db::Connection, payload: &SnapshotUploadJobs, -) -> Result<(), rusqlite::Error> { +) -> Result<(), crate::db::Error> { let Some(local_artifact_id) = payload.meta.local_artifact_id else { return Ok(()); }; @@ -790,9 +792,9 @@ fn stamp_backup_completed( /// reclaimable artifact that fails either check is counted as skipped here, /// because no later step sees it. fn stream_reclaim_due( - conn: &rusqlite::Connection, + conn: &crate::db::Connection, payload: &SnapshotUploadJobs, -) -> Result { +) -> Result { let Some(local_artifact_id) = payload.meta.local_artifact_id else { return Ok(false); }; @@ -828,11 +830,11 @@ fn stream_reclaim_due( /// Reclaim the uploaded stream under the connection hold that rechecks the /// pin, so a wake that moved it cannot race the unlink. fn reclaim_stream( - conn: &rusqlite::Connection, + conn: &crate::db::Connection, payload: &SnapshotUploadJobs, backup_attempt_id: &str, reclaim_image: Option, -) -> Result<(), rusqlite::Error> { +) -> Result<(), crate::db::Error> { let Some(local_artifact_id) = payload.meta.local_artifact_id else { return Ok(()); }; @@ -1417,9 +1419,9 @@ const BACKUP_MARKER_BATCH: usize = 128; /// remain unmarked and unreapable: operators must establish remote durability /// or arrange a fresh backup, never infer success from age or sandbox identity. pub fn backfill_backup_completed_markers( - conn: &rusqlite::Connection, + conn: &crate::db::Connection, scan: &mut BackupMarkerScan, -) -> rusqlite::Result { +) -> crate::db::Result { let mut report = BackupMarkerBackfill::default(); let through = match scan.through { Some(through) => through, @@ -1503,7 +1505,7 @@ pub fn backfill_backup_completed_markers( /// Each blocking pass is awaited through shutdown so no local writer escapes /// the daemon's task drain. The pruning trigger protects unvisited obligations. pub(crate) async fn run_maintenance( - db: std::sync::Arc>, + db: crate::db::SharedConnection, worker: BackgroundWorker, drain: std::sync::Arc, shutdown: tokio_util::sync::CancellationToken, @@ -1524,7 +1526,7 @@ pub(crate) async fn run_maintenance( else { continue; }; - let db = std::sync::Arc::clone(&db); + let db = db.clone(); let worker = worker.clone(); let prune = last_prune.is_none_or(|last: tokio::time::Instant| { last.elapsed() >= pplx_space_common_jobs::TERMINAL_JOB_PRUNE_INTERVAL @@ -1535,9 +1537,7 @@ pub(crate) async fn run_maintenance( let conn = crate::node_state_sync::connection_or_signal_failure(&db)?; let result = backfill_backup_completed_markers(&conn, &mut scan); let history_result = crate::db::now_epoch_i64().and_then(|now| { - let now = u64::try_from(now).map_err(|error| { - rusqlite::Error::ToSqlConversionFailure(Box::new(error)) - })?; + let now = u64::try_from(now).map_err(crate::db::Error::to_sql_conversion)?; history_scan.next_batch(&conn, now) }); let timestamp_result = timestamp_scan.next_batch(&conn); diff --git a/crates/libs/sbx/mgr/server/src/snapshot_upload_jobs/tests.rs b/crates/libs/sbx/mgr/server/src/snapshot_upload_jobs/tests.rs index 1361c699e..3f6738fba 100644 --- a/crates/libs/sbx/mgr/server/src/snapshot_upload_jobs/tests.rs +++ b/crates/libs/sbx/mgr/server/src/snapshot_upload_jobs/tests.rs @@ -35,7 +35,7 @@ async fn upload_executor_converges_unstaged_metadata_failure() { payload.includes_metadata_job = false; payload.metadata_staging_dir = Some(metadata_dir.clone()); let executor = SnapshotUploadExecutor { - db: std::sync::Arc::new(std::sync::Mutex::new(open_backfill_test_db())), + db: crate::db::SharedConnection::new(open_backfill_test_db()), drain_state: crate::service::tests::closed_drain_for_test(), node_manager: crate::node_manager_transport::fake::unbound_in_absent_dir(), retained_source_inspector: std::sync::Arc::new(crate::retained_source::FakeInspector::new( @@ -130,7 +130,7 @@ async fn stalled_transfer_probe_is_bounded_without_cancelling_remote_work() { #[test] fn refund_interrupted_running_attempts_only_touches_running_snapshot_upload_jobs() { - let conn = rusqlite::Connection::open_in_memory().unwrap(); + let conn = crate::db::Connection::open_in_memory(); conn.execute_batch( "CREATE TABLE jobs ( job_id TEXT PRIMARY KEY, @@ -167,7 +167,7 @@ fn refund_interrupted_running_attempts_only_touches_running_snapshot_upload_jobs let attempts = |job_id: &str| -> i32 { conn.query_row( "SELECT attempts FROM jobs WHERE job_id = ?1", - rusqlite::params![job_id], + crate::db::fixture::params![job_id], |row| row.get(0), ) .unwrap() @@ -181,9 +181,7 @@ fn refund_interrupted_running_attempts_only_touches_running_snapshot_upload_jobs #[tokio::test] async fn executor_rejects_non_snapshot_upload_job_kind() { let executor = SnapshotUploadExecutor { - db: std::sync::Arc::new(std::sync::Mutex::new( - rusqlite::Connection::open_in_memory().unwrap(), - )), + db: crate::db::SharedConnection::new(crate::db::Connection::open_in_memory()), drain_state: std::sync::Arc::new(crate::service::drain::DrainState::default()), node_manager: crate::node_manager_transport::fake::unbound_in_absent_dir(), retained_source_inspector: std::sync::Arc::new(crate::retained_source::FakeInspector::new( @@ -214,9 +212,7 @@ async fn executor_requires_begin_before_volume_staging() { return; } let executor = SnapshotUploadExecutor { - db: std::sync::Arc::new(std::sync::Mutex::new( - rusqlite::Connection::open_in_memory().unwrap(), - )), + db: crate::db::SharedConnection::new(crate::db::Connection::open_in_memory()), drain_state: std::sync::Arc::new(crate::service::drain::DrainState::default()), node_manager: crate::node_manager_transport::fake::unbound_in_absent_dir(), retained_source_inspector: std::sync::Arc::new(crate::retained_source::FakeInspector::new( @@ -307,7 +303,7 @@ fn finalise_total_saturates_instead_of_overflowing() { assert_eq!(vm_artifact_total_bytes(&polled, 2), u64::MAX); } -fn open_backfill_test_db() -> rusqlite::Connection { +fn open_backfill_test_db() -> crate::db::Connection { crate::db::test_conn() } @@ -339,7 +335,7 @@ fn ready_backup_keeps_completed_evidence_when_bookkeeping_misses_execution_deadl let artifact = "01939e3e-aaaa-7000-8000-000000000003"; let conn = open_backfill_test_db(); insert_artifact(&conn, artifact, None); - let db = std::sync::Arc::new(std::sync::Mutex::new(conn)); + let db = crate::db::SharedConnection::new(conn); let payload = reclaim_fixture_payload(artifact, "/unused".into()); let runtime = tokio::runtime::Builder::new_current_thread() .enable_all() @@ -358,11 +354,10 @@ fn ready_backup_keeps_completed_evidence_when_bookkeeping_misses_execution_deadl }); ready.await.unwrap(); let phase = std::sync::Arc::new(tokio::sync::Notify::new()); - let worker = BackgroundWorker::new( - std::sync::Arc::clone(&db), + let worker = db.background_worker( ReadyBackupExecutor { executor: SnapshotUploadExecutor { - db: std::sync::Arc::clone(&db), + db: db.clone(), drain_state: std::sync::Arc::new(crate::service::drain::DrainState::default()), node_manager: crate::node_manager_transport::fake::unbound_in_absent_dir(), retained_source_inspector: std::sync::Arc::new( @@ -400,12 +395,12 @@ fn ready_backup_keeps_completed_evidence_when_bookkeeping_misses_execution_deadl ) .unwrap(); let pruned = pplx_space_common_jobs::db::prune_terminal_jobs( - &conn, + conn.raw(), Duration::from_secs(1), Duration::from_secs(1), ) .unwrap(); - let retained = pplx_space_common_jobs::db::get_job(&conn, &id) + let retained = pplx_space_common_jobs::db::get_job(conn.raw(), &id) .unwrap() .is_some(); (job, marker_missing, pruned, retained) @@ -451,20 +446,15 @@ async fn maintenance_retries_failed_pruning_on_the_next_tick() { END;", ) .unwrap(); - let db = std::sync::Arc::new(std::sync::Mutex::new(conn)); + let db = crate::db::SharedConnection::new(conn); let drain = std::sync::Arc::new(crate::service::drain::DrainState::default()); let worker = build_worker( - std::sync::Arc::clone(&db), + db.clone(), std::sync::Arc::clone(&drain), crate::node_manager_transport::fake::unbound_in_absent_dir(), ); let shutdown = tokio_util::sync::CancellationToken::new(); - let maintenance = tokio::spawn(run_maintenance( - std::sync::Arc::clone(&db), - worker, - drain, - shutdown.clone(), - )); + let maintenance = tokio::spawn(run_maintenance(db.clone(), worker, drain, shutdown.clone())); // RAISE(FAIL) preserves the trigger's attempt witness, but not the delete. // Yield without advancing virtual time while the blocking pass finishes. @@ -489,7 +479,7 @@ async fn maintenance_retries_failed_pruning_on_the_next_tick() { let deadline = Instant::now() + Duration::from_secs(5); let deleted = loop { if pplx_space_common_jobs::db::get_job( - &db.lock().unwrap(), + db.lock().unwrap().raw(), &"019d84bd-280d-7331-99c6-000000000002".parse().unwrap(), ) .unwrap() @@ -533,7 +523,7 @@ fn pruning_retains_failed_backup_marker_evidence_until_repaired() { .unwrap(); let prune = || { pplx_space_common_jobs::db::prune_terminal_jobs( - &conn, + conn.raw(), Duration::from_secs(1), Duration::from_secs(1), ) @@ -617,22 +607,22 @@ fn backup_marker_repair_bounds_discovery_and_advances_past_retained_evidence() { assert!(backup_marker(&conn, inserted_later).is_some()); } -fn insert_artifact(conn: &rusqlite::Connection, id: &str, backup_completed_at: Option<&str>) { +fn insert_artifact(conn: &crate::db::Connection, id: &str, backup_completed_at: Option<&str>) { conn.execute( "INSERT INTO local_artifacts ( local_artifact_id, sandbox_id, artifact_kind, artifact_origin, snapshot_name, local_path, created_at, backup_completed_at ) VALUES (?1, ?1, 'full', 'suspend', NULL, ?1, '0', ?2)", - rusqlite::params![id, backup_completed_at], + crate::db::fixture::params![id, backup_completed_at], ) .unwrap(); } -fn insert_job(conn: &rusqlite::Connection, job_id: &str, status: &str, payload: &str) { +fn insert_job(conn: &crate::db::Connection, job_id: &str, status: &str, payload: &str) { conn.execute( "INSERT INTO jobs (job_id, kind, status, payload, subject_id) VALUES (?1, ?2, ?3, ?4, 'some-sandbox-id')", - rusqlite::params![job_id, SNAPSHOT_UPLOAD_JOB_KIND, status, payload], + crate::db::fixture::params![job_id, SNAPSHOT_UPLOAD_JOB_KIND, status, payload], ) .unwrap(); } @@ -727,11 +717,11 @@ fn stream_reclaim_applies_only_to_pinned_btrfs_send_suspend_artifacts() { ))); } -fn open_reclaim_test_db() -> rusqlite::Connection { +fn open_reclaim_test_db() -> crate::db::Connection { crate::db::test_conn() } -fn insert_reclaim_sandbox(conn: &rusqlite::Connection, id: &str, state: &str, pin: &str) { +fn insert_reclaim_sandbox(conn: &crate::db::Connection, id: &str, state: &str, pin: &str) { conn.execute( "INSERT INTO sandboxes ( id, uuid8, state, template, user, runtime, created_at, updated_at, @@ -740,7 +730,7 @@ fn insert_reclaim_sandbox(conn: &rusqlite::Connection, id: &str, state: &str, pi current_local_artifact_origin ) VALUES (?1, 'uuid8', ?2, 'seed', 'u', 'fc', '0', '0', '{}', '{}', '{}', ?3, 'full', 'suspend')", - rusqlite::params![id, state, pin], + crate::db::fixture::params![id, state, pin], ) .unwrap(); } @@ -803,7 +793,7 @@ fn submitted_transfer_ids_replace_the_durable_prepared_intent() { payload.deferred_metadata = true; payload.meta.rootfs_send_source_id = Some(uuid::Uuid::now_v7()); payload.job_ids = vec![JobId::new(), JobId::new(), JobId::new()]; - let db = std::sync::Arc::new(std::sync::Mutex::new(conn)); + let db = crate::db::SharedConnection::new(conn); for job_id in [ "019d84bd-280d-7331-99c6-000000000003", @@ -867,10 +857,10 @@ fn only_exact_prepared_intents_begin_in_the_worker() { assert!(!prepared_upload_requires_begin(&legacy)); } -fn backup_marker(conn: &rusqlite::Connection, artifact_id: &str) -> Option { +fn backup_marker(conn: &crate::db::Connection, artifact_id: &str) -> Option { conn.query_row( "SELECT backup_completed_at FROM local_artifacts WHERE local_artifact_id = ?1", - rusqlite::params![artifact_id], + crate::db::fixture::params![artifact_id], |r| r.get(0), ) .unwrap() @@ -1013,7 +1003,7 @@ fn backfill_stamps_unmarked_artifacts_and_buckets_the_rest() { let stamped_at: Option = conn .query_row( "SELECT backup_completed_at FROM local_artifacts WHERE local_artifact_id = ?1", - rusqlite::params![unmarked], + crate::db::fixture::params![unmarked], |r| r.get(0), ) .unwrap(); @@ -1023,7 +1013,7 @@ fn backfill_stamps_unmarked_artifacts_and_buckets_the_rest() { let preserved: String = conn .query_row( "SELECT backup_completed_at FROM local_artifacts WHERE local_artifact_id = ?1", - rusqlite::params![marked], + crate::db::fixture::params![marked], |r| r.get(0), ) .unwrap(); @@ -1033,7 +1023,7 @@ fn backfill_stamps_unmarked_artifacts_and_buckets_the_rest() { let untouched: Option = conn .query_row( "SELECT backup_completed_at FROM local_artifacts WHERE local_artifact_id = ?1", - rusqlite::params![pending_only], + crate::db::fixture::params![pending_only], |r| r.get(0), ) .unwrap(); @@ -1133,10 +1123,10 @@ fn ready_backup_returns_before_the_retained_source_is_inspected() { let vm = crate::snapshot_upload::vm_artifacts_for_format(SuspendSnapshotFormat::BtrfsSend); crate::snapshot_upload::write_snapshot_metadata(&payload.meta, &snapshot_dir, &vm, None) .unwrap(); - let db = std::sync::Arc::new(std::sync::Mutex::new(conn)); + let db = crate::db::SharedConnection::new(conn); let gate = std::sync::Arc::new(tokio::sync::Notify::new()); let executor = SnapshotUploadExecutor { - db: std::sync::Arc::clone(&db), + db: db.clone(), drain_state: std::sync::Arc::new(crate::service::drain::DrainState::default()), node_manager: crate::node_manager_transport::fake::unbound_in_absent_dir(), retained_source_inspector: std::sync::Arc::new(GatedInspector { diff --git a/crates/libs/sbx/mgr/server/src/watchdog.rs b/crates/libs/sbx/mgr/server/src/watchdog.rs index 030d49009..66d1721f1 100644 --- a/crates/libs/sbx/mgr/server/src/watchdog.rs +++ b/crates/libs/sbx/mgr/server/src/watchdog.rs @@ -129,7 +129,7 @@ enum NetGatewayRelaunchArgsError { LoadSandbox { sandbox_id: String, #[source] - source: rusqlite::Error, + source: crate::db::Error, }, #[error("sandbox {0} missing from local DB")] MissingSandbox(String), @@ -158,7 +158,7 @@ enum SpacedHostRestartError { #[error("sandbox database mutex poisoned")] DatabaseMutexPoisoned, #[error("persist spaced-host replacement pid: {0}")] - Persist(#[source] rusqlite::Error), + Persist(#[source] crate::db::Error), } #[derive(Debug, thiserror::Error)] @@ -672,7 +672,7 @@ impl HealthComponents { pub struct SandboxWatchdog { state: Arc>>, sidecars: Arc>>, - db: Arc>, + db: crate::db::SharedConnection, network_policy_reconcile_queue: NetworkPolicyReconcileQueue, expected_route_verification_key_generation: Arc, feature_flags: pplx_space_feature_flags::FeatureFlagClient, @@ -711,7 +711,7 @@ impl SandboxWatchdog { reason = "the constructor names each long-lived watchdog dependency explicitly" )] pub(crate) fn new( - db: Arc>, + db: crate::db::SharedConnection, network_policy_reconcile_queue: NetworkPolicyReconcileQueue, snapshot_scheduler: Arc, auto_lifecycle: AutoLifecycleCoordinator, @@ -793,7 +793,7 @@ impl SandboxWatchdog { #[cfg(test)] fn new_for_tests( - db: Arc>, + db: crate::db::SharedConnection, network_policy_reconcile_queue: NetworkPolicyReconcileQueue, shutdown_token: CancellationToken, task_tracker: &TaskTracker, @@ -1265,7 +1265,7 @@ fn system_time_to_timestamp(value: SystemTime) -> Option async fn sidecar_loop( state: Arc>>, sidecars: Arc>>, - db: Arc>, + db: crate::db::SharedConnection, network_policy_reconcile_queue: NetworkPolicyReconcileQueue, expected_route_verification_key_generation: Arc, feature_flags: pplx_space_feature_flags::FeatureFlagClient, @@ -1455,7 +1455,7 @@ async fn fill_sidecar_restart_slots( queue: &mut SidecarRestartQueue, futures: &mut FuturesUnordered, sidecars: &Arc>>, - db: &Arc>, + db: &crate::db::SharedConnection, network_policy_reconcile_queue: &NetworkPolicyReconcileQueue, state: &Arc>>, feature_flags: &pplx_space_feature_flags::FeatureFlagClient, @@ -1483,7 +1483,7 @@ async fn fill_sidecar_restart_slots( let sandbox_id = request.sandbox_id().to_string(); let operation = request.operation(); - let db = Arc::clone(db); + let db = db.clone(); let sidecars = Arc::clone(sidecars); let network_policy_reconcile_queue = network_policy_reconcile_queue.clone(); let state = Arc::clone(state); @@ -1546,7 +1546,7 @@ fn finish_sidecar_restart(queue: &mut SidecarRestartQueue, outcome: SidecarResta } } -fn emit_auto_lifecycle_delete_gauges(db: &Arc>) { +fn emit_auto_lifecycle_delete_gauges(db: &crate::db::SharedConnection) { use pplx_space_metrics::sandbox::worker::AutoLifecycleDeleteOutboxState; let (outbox, blocked) = { @@ -1603,9 +1603,7 @@ fn emit_auto_lifecycle_delete_gauges(db: &Arc>, -) -> Option> { +fn load_guest_health_rows(db: &crate::db::SharedConnection) -> Option> { let db = match db.lock() { Ok(db) => db, Err(error) => { @@ -1886,7 +1884,7 @@ async fn emit_net_gateway_route_verification_gauges( } fn sandbox_states_for_ids( - db: &Arc>, + db: &crate::db::SharedConnection, sandbox_ids: &[String], ) -> Option> { let db = match db.lock() { @@ -2018,7 +2016,7 @@ async fn spawn_net_gateway_stats_probe( sandbox_id: String, netgtw_pid: u32, state: Arc>>, - db: Arc>, + db: crate::db::SharedConnection, ) { if pending.len() >= NET_GATEWAY_STATS_CONCURRENCY { drain_net_gateway_stats_probe(pending).await; @@ -2053,7 +2051,7 @@ async fn probe_net_gateway_stats( sandbox_id: &str, netgtw_pid: u32, state: &Arc>>, - db: &Arc>, + db: &crate::db::SharedConnection, ) { // Every registered sandbox has the UUID id create assigned it; an id that // does not parse fails the probe as an absent gateway socket would. @@ -2143,7 +2141,7 @@ async fn probe_net_gateway_stats( async fn composed_health_loop( state: Arc>>, sidecars: Arc>>, - db: Arc>, + db: crate::db::SharedConnection, snapshot_scheduler: Arc, auto_lifecycle: AutoLifecycleCoordinator, lifecycle_epochs: HealthLifecycleEpochs, @@ -2587,7 +2585,7 @@ fn update_legacy_health_fields( async fn apply_and_reconcile_composed_health_result( state: &Arc>>, - db: &Arc>, + db: &crate::db::SharedConnection, snapshot_scheduler: &SnapshotScheduler, auto_lifecycle: &AutoLifecycleCoordinator, lifecycle_epochs: &HealthLifecycleEpochs, @@ -2622,7 +2620,7 @@ async fn apply_and_reconcile_composed_health_result( async fn apply_composed_health_result( state: &Arc>>, - db: &Arc>, + db: &crate::db::SharedConnection, result: Option, ) -> Option { let Some(Ok((sandbox_id, lifecycle, lifecycle_epoch, result))) = result else { @@ -2711,7 +2709,7 @@ async fn apply_composed_health_result( } async fn reconcile_pause_failed_quarantine( - db: &Arc>, + db: &crate::db::SharedConnection, snapshot_scheduler: &SnapshotScheduler, auto_lifecycle: &AutoLifecycleCoordinator, lifecycle_epochs: &HealthLifecycleEpochs, @@ -2877,7 +2875,7 @@ fn classify_composed_health( async fn restart_spaced_host( info: &SidecarInfo, - db: Arc>, + db: crate::db::SharedConnection, sidecars: Arc>>, node_manager: &NodeManagerConnector, ) -> Result<(), SpacedHostRestartError> { @@ -2975,7 +2973,7 @@ fn spaced_host_restart_args(sandbox_id: &str, vsock_uds_path: String) -> Vec>, + db: crate::db::SharedConnection, sidecars: Arc>>, network_policy_reconcile_queue: NetworkPolicyReconcileQueue, state: &Arc>>, @@ -3006,7 +3004,7 @@ async fn restart_netgtw( /// forking a duplicate. async fn relaunch_netgtw( info: &SidecarInfo, - db: &Arc>, + db: &crate::db::SharedConnection, sidecars: &Arc>>, network_policy_reconcile_queue: &NetworkPolicyReconcileQueue, state: &Arc>>, @@ -3056,7 +3054,7 @@ async fn commit_netgtw_replacement( uid: u32, observed_netgtw_pid: Option, pid: u32, - db: &Arc>, + db: &crate::db::SharedConnection, sidecars: &Arc>>, state: &Arc>>, ) -> Result<(), String> { @@ -3086,7 +3084,7 @@ async fn commit_netgtw_replacement( } fn net_gateway_relaunch_args( - db: &Arc>, + db: &crate::db::SharedConnection, sandbox_id: &str, feature_flags: &pplx_space_feature_flags::FeatureFlagClient, ) -> Result, NetGatewayRelaunchArgsError> { diff --git a/crates/libs/sbx/mgr/server/src/watchdog/diagnostics/tests.rs b/crates/libs/sbx/mgr/server/src/watchdog/diagnostics/tests.rs index d6254c568..78ddf1175 100644 --- a/crates/libs/sbx/mgr/server/src/watchdog/diagnostics/tests.rs +++ b/crates/libs/sbx/mgr/server/src/watchdog/diagnostics/tests.rs @@ -10,7 +10,7 @@ use super::*; async fn apply_guest_outcome( state: &Arc>>, - db: &Arc>, + db: &crate::db::SharedConnection, sandbox_id: &str, guest: HealthProbeOutcome, ) -> Option { @@ -44,7 +44,7 @@ async fn only_degrading_transitions_gather_evidence_and_console_tails_are_rate_l sandbox_id.clone(), test_health(), )]))); - let db = Arc::new(std::sync::Mutex::new(crate::db::test_conn())); + let db = crate::db::SharedConnection::new(crate::db::test_conn()); let mut transitions = Vec::new(); for guest in [ HealthProbeOutcome::Success, diff --git a/crates/libs/sbx/mgr/server/src/watchdog/tests.rs b/crates/libs/sbx/mgr/server/src/watchdog/tests.rs index f54e51cbb..a7de3b777 100644 --- a/crates/libs/sbx/mgr/server/src/watchdog/tests.rs +++ b/crates/libs/sbx/mgr/server/src/watchdog/tests.rs @@ -24,7 +24,7 @@ async fn gateway_metric_registry_forgets_retired_builds() { assert!(published.is_empty()); } -fn insert_test_sandbox(conn: &rusqlite::Connection, id: &str, state: crate::state::SandboxState) { +fn insert_test_sandbox(conn: &crate::db::Connection, id: &str, state: crate::state::SandboxState) { let uuid8 = id .parse::() .map(SandboxRuntimeLabel::from_sandbox_id) @@ -102,7 +102,7 @@ fn test_route_identity() -> SpaceApiRouteIdentity { } fn set_runtime_fields( - conn: &rusqlite::Connection, + conn: &crate::db::Connection, id: &str, space_api_route_identity: SpaceApiRouteIdentity, ) { @@ -224,7 +224,7 @@ fn net_gateway_relaunch_args_include_current_route_identity() { let identity = test_route_identity(); set_runtime_fields(&conn, id, identity.clone()); - let db = Arc::new(std::sync::Mutex::new(conn)); + let db = crate::db::SharedConnection::new(conn); let flags = pplx_space_feature_flags::FeatureFlagClient::disabled(); let args = net_gateway_relaunch_args(&db, id, &flags).expect("relaunch args build"); @@ -249,12 +249,12 @@ fn net_gateway_relaunch_args_keep_pool_gateway_no_identity() { insert_test_sandbox(&conn, id, crate::state::SandboxState::Started); conn.execute( "UPDATE sandboxes SET pool_template = 'seed' WHERE id = ?1", - rusqlite::params![id], + crate::db::fixture::params![id], ) .expect("pool template update"); set_runtime_fields(&conn, id, test_route_identity()); - let db = Arc::new(std::sync::Mutex::new(conn)); + let db = crate::db::SharedConnection::new(conn); let flags = pplx_space_feature_flags::FeatureFlagClient::disabled(); let args = net_gateway_relaunch_args(&db, id, &flags).expect("relaunch args build"); @@ -281,7 +281,7 @@ fn net_gateway_relaunch_args_keep_pool_gateway_no_identity() { async fn clear_netgtw_probe_state_drops_cached_process_observations() { let conn = crate::db::test_conn(); let watchdog = SandboxWatchdog::new_for_tests( - Arc::new(std::sync::Mutex::new(conn)), + crate::db::SharedConnection::new(conn), NetworkPolicyReconcileQueue::default(), CancellationToken::new(), &TaskTracker::new(), @@ -342,10 +342,10 @@ async fn watchdog_with_committed_gateway( let conn = crate::db::test_conn(); insert_test_sandbox(&conn, sandbox_id, crate::state::SandboxState::Started); set_runtime_fields(&conn, sandbox_id, test_route_identity()); - let db = Arc::new(std::sync::Mutex::new(conn)); + let db = crate::db::SharedConnection::new(conn); let tracker = tokio_util::task::TaskTracker::new(); let watchdog = SandboxWatchdog::new_for_tests( - Arc::clone(&db), + db.clone(), NetworkPolicyReconcileQueue::default(), CancellationToken::new(), &tracker, @@ -453,7 +453,7 @@ async fn rejected_gateway_commit_preserves_probe_state() { .unwrap() .execute( "UPDATE sandboxes SET netgtw_pid = ?1 WHERE id = ?2", - rusqlite::params![50_003, sandbox_id], + crate::db::fixture::params![50_003, sandbox_id], ) .unwrap(); @@ -484,7 +484,7 @@ fn test_watchdog_with_row(sandbox_id: &str, state: crate::state::SandboxState) - let conn = crate::db::test_conn(); insert_test_sandbox(&conn, sandbox_id, state); SandboxWatchdog::new_for_tests( - Arc::new(std::sync::Mutex::new(conn)), + crate::db::SharedConnection::new(conn), NetworkPolicyReconcileQueue::default(), CancellationToken::new(), &TaskTracker::new(), @@ -546,7 +546,7 @@ async fn recover_all_skips_running_vm_without_db_row() { let sandbox_id = SandboxId::new().to_string(); let conn = crate::db::test_conn(); let watchdog = SandboxWatchdog::new_for_tests( - Arc::new(std::sync::Mutex::new(conn)), + crate::db::SharedConnection::new(conn), NetworkPolicyReconcileQueue::default(), CancellationToken::new(), &TaskTracker::new(), @@ -1069,7 +1069,7 @@ fn pause_failed_recovery_requires_two_matching_firecracker_states() { async fn pause_failed_recovery_fixture( firecracker_state: FirecrackerVmState, ) -> ( - Arc>, + crate::db::SharedConnection, Arc, AutoLifecycleCoordinator, HealthLifecycleEpochs, @@ -1095,7 +1095,7 @@ async fn pause_failed_recovery_fixture( ) .expect("pause-failed quarantine must be recorded") ); - let db = Arc::new(std::sync::Mutex::new(conn)); + let db = crate::db::SharedConnection::new(conn); let auto_lifecycle = AutoLifecycleCoordinator::default(); let lifecycle_epochs = HealthLifecycleEpochs::default(); let snapshot_scheduler = Arc::new(SnapshotScheduler::new(Arc::new( @@ -1275,7 +1275,7 @@ async fn lifecycle_edge_preserves_status_and_discards_in_flight_probe() { } let conn = crate::db::test_conn(); - let db = Arc::new(std::sync::Mutex::new(conn)); + let db = crate::db::SharedConnection::new(conn); let observations = std::array::from_fn(|_| sample(HealthProbeOutcome::Success)); apply_composed_health_result( &state, @@ -1315,7 +1315,7 @@ async fn missing_guest_uptime_preserves_last_observation() { let conn = crate::db::test_conn(); insert_test_sandbox(&conn, &sandbox_id, crate::state::SandboxState::Started); crate::db::update_guest_uptime(&conn, &sandbox_id, 10).expect("initial uptime persists"); - let db = Arc::new(std::sync::Mutex::new(conn)); + let db = crate::db::SharedConnection::new(conn); apply_composed_health_result( &state, @@ -1408,13 +1408,13 @@ async fn successful_health_probes_leave_failed_lifecycle_unchanged() { insert_test_sandbox(&conn, &sandbox_id, crate::state::SandboxState::Failed); conn.execute( "UPDATE sandboxes SET failure_reason = ?1 WHERE id = ?2", - rusqlite::params![ + crate::db::fixture::params![ "guest bridge health probe failed after 3 consecutive failures: timeout", &sandbox_id ], ) .unwrap(); - let db = Arc::new(std::sync::Mutex::new(conn)); + let db = crate::db::SharedConnection::new(conn); let state = Arc::new(Mutex::new(HashMap::from([( sandbox_id.clone(), test_health(),