diff --git a/ARCHITECTURE.toml b/ARCHITECTURE.toml index 9eae39872..2ac4582aa 100644 --- a/ARCHITECTURE.toml +++ b/ARCHITECTURE.toml @@ -245,17 +245,20 @@ mode = "required" mode = "required" # sbx-mgr's node-local SQLite database (`sandboxes` and every other sbx-mgr -# table) is opened, migrated and queried only by its store, `src/db` of the -# server crate. Within sbx-mgr's crates, naming rusqlite, opening a -# 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; 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. +# table) is opened, migrated and queried only by its store, the +# `pplx-space-sbx-mgr-store` crate. The guarantee is that crate's boundary: +# `dependency_fence.sbxmgr_rusqlite` below keeps rusqlite out of every other +# sbx-mgr crate, and outside its `test-support` feature the store's API hands +# out no rusqlite connection, transaction, statement or row. (Its `FromSql` +# impl for a column type can return rusqlite's `FromSqlError`, an error type +# rather than a database capability, to a caller holding a rusqlite value.) +# This ratchet is a line-based tripwire on top of it, within sbx-mgr's +# crates: naming rusqlite, opening a 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. Aliases, wrapped +# imports and calls through names imported earlier escape it. Other services' crates use rusqlite for their own databases, so +# `paths` limits the rule to sbx-mgr's. [rules.pattern_ratchet.sbxmgr_sqlite] patterns = [ '\brusqlite\s*::', @@ -272,10 +275,7 @@ patterns = [ '^\s*(?:SELECT\s|INSERT\s+(?:OR\s+[A-Z]+\s+)?INTO\s|UPDATE\s+(?:OR\s+[A-Z]+\s+)?\w+(?:\s+SET\s|\s*$)|DELETE\s+FROM\s|WITH\s+(?:RECURSIVE\s+)?\w+\s+AS\b)', ] paths = ["crates/libs/sbx/mgr/**", "crates/apps/sbx/**"] -allow = [ - "crates/libs/sbx/mgr/server/src/db.rs", - "crates/libs/sbx/mgr/server/src/db/**", -] +allow = ["crates/libs/sbx/mgr/store/src/**"] [rules.pattern_ratchet.sbxmgr_sqlite.no_crossing] mode = "required" @@ -294,8 +294,9 @@ mode = "required" mode = "required" # The same database's schema and location, in any crate: its migrations, its -# path and file name, and the migrator's default-path helper for it belong to -# the store, which opens and migrates it. The migrations crate defines the +# path and file name, and a default-path helper for it belong to the store, +# which opens and migrates it; the database migrator takes them from the +# store's `db::migration::target()`. The migrations crate defines the # migrators and common-core the path. These names are # specific enough to check workspace-wide, unlike `sbxmgr_sqlite`'s patterns, # which other services' crates match for their own databases. @@ -308,8 +309,7 @@ patterns = [ '\bdb/sandbox-manager\b', ] allow = [ - "crates/libs/sbx/mgr/server/src/db.rs", - "crates/libs/sbx/mgr/server/src/db/**", + "crates/libs/sbx/mgr/store/src/**", "crates/libs/sbx/mgr/migrations/src/**", "crates/libs/common/core/src/host/linux.rs", ] @@ -317,9 +317,9 @@ allow = [ [rules.pattern_ratchet.sbxmgr_sqlite_schema.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_schema.no_new_entries] -mode = "advisory" +mode = "required" [rules.pattern_ratchet.sbxmgr_sqlite_schema.no_raised_caps] mode = "required" @@ -330,6 +330,28 @@ mode = "required" [rules.pattern_ratchet.sbxmgr_sqlite_schema.stale_entries] mode = "required" +# Only the store may depend on the SQLite drivers that can open sbx-mgr's +# database, so no other sbx-mgr crate can name rusqlite: no normal, dev or +# build dependency, for any target, under any name. +[rules.dependency_fence.sbxmgr_rusqlite] +packages = ["libsqlite3-sys", "rusqlite"] +members = ["crates/libs/sbx/mgr/**", "crates/apps/sbx/**"] +allow = ["crates/libs/sbx/mgr/store"] + +[rules.dependency_fence.sbxmgr_rusqlite.no_crossing] +mode = "required" + +# sqlx opens SQLite too, directly or through its core and SQLite driver +# crates. The migrations crate embeds the migrations with it, and the store +# names their `Migrator` type. +[rules.dependency_fence.sbxmgr_sqlx] +packages = ["sqlx", "sqlx-core", "sqlx-sqlite"] +members = ["crates/libs/sbx/mgr/**", "crates/apps/sbx/**"] +allow = ["crates/libs/sbx/mgr/store", "crates/libs/sbx/mgr/migrations"] + +[rules.dependency_fence.sbxmgr_sqlx.no_crossing] +mode = "required" + [rules.crate_names] # A crate matches if a part of its package or library name, lowercased and # split on `-` and `_`, is one of these. diff --git a/Cargo.lock b/Cargo.lock index b6b5e7638..53537c138 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -5723,7 +5723,7 @@ dependencies = [ "pplx-space-common-jobs", "pplx-space-common-svc", "pplx-space-db-test-fixtures", - "pplx-space-sbxmgr-migrations", + "pplx-space-sbx-mgr-store", "pplx-space-test-env", "rusqlite", "sqlx", @@ -6691,7 +6691,7 @@ dependencies = [ "pplx-space-node-runtime-core", "pplx-space-observability", "pplx-space-sbx-mgr-client", - "pplx-space-sbxmgr-migrations", + "pplx-space-sbx-mgr-store", "pplx-space-spaced-common", "pplx-space-spaced-host-client", "pplx-space-template-mgr-client", @@ -6700,15 +6700,12 @@ dependencies = [ "prost", "prost-types", "rcgen", - "ref-cast", "rstest", - "rusqlite", "rustix", "rustls", "serde", "serde_json", "sha2 0.11.0", - "sqlx", "tempfile", "thiserror 2.0.20", "tokio", @@ -6767,6 +6764,39 @@ dependencies = [ "uuid", ] +[[package]] +name = "pplx-space-sbx-mgr-store" +version = "0.8.0" +dependencies = [ + "dashmap", + "futures-util", + "jiff", + "pplx-space-common-core", + "pplx-space-common-jobs", + "pplx-space-common-svc", + "pplx-space-events", + "pplx-space-metrics", + "pplx-space-net-common", + "pplx-space-node-mgr-client", + "pplx-space-node-runtime-core", + "pplx-space-observability", + "pplx-space-sbx-mgr-client", + "pplx-space-sbxmgr-migrations", + "prost-types", + "ref-cast", + "rusqlite", + "rustix", + "serde", + "serde_json", + "sqlx", + "tempfile", + "thiserror 2.0.20", + "tokio", + "tonic", + "tracing", + "uuid", +] + [[package]] name = "pplx-space-sbxmgr-migrations" version = "0.8.0" diff --git a/Cargo.toml b/Cargo.toml index 5d01f07a8..238ae6fd2 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -52,6 +52,7 @@ members = [ "crates/libs/sbx/mgr/client", "crates/libs/sbx/mgr/migrations", "crates/libs/sbx/mgr/server", + "crates/libs/sbx/mgr/store", "crates/libs/node/api/client", "crates/libs/node/api/server", "crates/libs/node/ctl/server", @@ -344,6 +345,7 @@ pplx-space-snapshot-store = { path = "crates/libs/snapshot/store" } pplx-space-sbxmgr-migrations = { path = "crates/libs/sbx/mgr/migrations" } pplx-space-sbx-mgr = { path = "crates/libs/sbx/mgr/server" } pplx-space-sbx-mgr-client = { path = "crates/libs/sbx/mgr/client" } +pplx-space-sbx-mgr-store = { path = "crates/libs/sbx/mgr/store" } pplx-space-spaced-build = { path = "crates/libs/spaced/build" } pplx-space-spaced-guest-protocol = { path = "crates/libs/spaced/guest/protocol" } pplx-space-spaced-guest-execution = { path = "crates/libs/spaced/guest/execution" } diff --git a/architecture-baseline.toml b/architecture-baseline.toml index 7fa0f514c..2d19b4e4e 100644 --- a/architecture-baseline.toml +++ b/architecture-baseline.toml @@ -75,11 +75,6 @@ "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" = 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" = 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 @@ -101,6 +96,11 @@ "crates/libs/sbx/mgr/server/src/snapshot_restore.rs" = 2150 "crates/libs/sbx/mgr/server/src/snapshot_upload_jobs.rs" = 1210 "crates/libs/sbx/mgr/server/src/watchdog.rs" = 2767 +"crates/libs/sbx/mgr/store/src/db/auto_lifecycle_delete_receipts.rs" = 1147 +"crates/libs/sbx/mgr/store/src/db/cleanup_episodes.rs" = 1830 +"crates/libs/sbx/mgr/store/src/db/quarantine_recovery.rs" = 1042 +"crates/libs/sbx/mgr/store/src/db/sandboxes.rs" = 1535 +"crates/libs/sbx/mgr/store/src/db/state_transitions.rs" = 1012 "crates/libs/spaced/host/client/src/lib.rs" = 1040 "crates/libs/spaced/host/server/src/bridge.rs" = 1249 "crates/libs/template/mgr/src/db.rs" = 1276 @@ -149,10 +149,9 @@ # transaction and error types, so no file is listed. [pattern_ratchet.sbxmgr_sqlite] -# Code outside the store that still names sbx-mgr's migrations or database -# path: the database migrator, until it calls the store. +# No code outside the store names sbx-mgr's migrations or database path; the +# database migrator takes them from the store. [pattern_ratchet.sbxmgr_sqlite_schema] -"crates/libs/db/migrator/src/lib.rs" = 5 # [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/ci/rust-artifact-inputs.json b/ci/rust-artifact-inputs.json index ee9785fba..f9cd73084 100644 --- a/ci/rust-artifact-inputs.json +++ b/ci/rust-artifact-inputs.json @@ -36,6 +36,7 @@ "crates/libs/common/core", "crates/libs/common/grpc", "crates/libs/common/grpc-lb", + "crates/libs/common/jobs", "crates/libs/common/proto-build", "crates/libs/common/svc", "crates/libs/common/work-coalescer", @@ -59,6 +60,7 @@ "crates/libs/sandbox/teardown", "crates/libs/sbx/mgr/client", "crates/libs/sbx/mgr/migrations", + "crates/libs/sbx/mgr/store", "crates/libs/spaced/build", "crates/libs/spaced/common", "crates/libs/template/mgr-client", @@ -81,11 +83,13 @@ "crates/libs/common/core", "crates/libs/common/grpc", "crates/libs/common/grpc-lb", + "crates/libs/common/jobs", "crates/libs/common/proto-build", "crates/libs/common/svc", "crates/libs/credential/injection/core", "crates/libs/credential/mgr/client", "crates/libs/db/migrator", + "crates/libs/events", "crates/libs/execution/common", "crates/libs/metrics", "crates/libs/net/common", @@ -96,6 +100,7 @@ "crates/libs/observability", "crates/libs/sbx/mgr/client", "crates/libs/sbx/mgr/migrations", + "crates/libs/sbx/mgr/store", "crates/libs/spaced/build", "crates/libs/spaced/common", "crates/libs/template/mgr-client" @@ -258,6 +263,7 @@ "crates/libs/sbx/mgr/client", "crates/libs/sbx/mgr/migrations", "crates/libs/sbx/mgr/server", + "crates/libs/sbx/mgr/store", "crates/libs/snapshot/store", "crates/libs/spaced/build", "crates/libs/spaced/common", diff --git a/ci/scripts/run-rust-checks.sh b/ci/scripts/run-rust-checks.sh index bf4a20699..e9ae6d1e4 100755 --- a/ci/scripts/run-rust-checks.sh +++ b/ci/scripts/run-rust-checks.sh @@ -240,6 +240,52 @@ run_fmt_check() { cargo fmt --all -- --check } +# sbx-mgr's SQLite store makes its raw connection and statement methods +# public only under its `test-support` feature, which the workspace pass's +# `--all-features` turns on. Lint sbx-mgr's production libraries and +# binaries without dev targets in every feature configuration its images +# are built with, so production code that calls a gated store method fails +# before merge, even behind a feature such as `otel`: no features, the +# release images' set (docker/Dockerfile.rust-build-common) and the Kind +# images' set (docker/Dockerfile.rust-build-kind). The migrator has no +# features of its own. +run_sbxmgr_production_checks() { + local target_dir="$1" + local sbxmgr_feature_sets=( + "" + "pplx-space-sbxmgr-svc/otel,pplx-space-sbxmgr-svc/datadog" + "pplx-space-sbxmgr-svc/otel,pplx-space-sbxmgr-svc/datadog,pplx-space-sbxmgr-svc/test-hooks" + ) + local sbxmgr_features sbxmgr_feature_args sbxmgr_graph + for sbxmgr_features in "${sbxmgr_feature_sets[@]}"; do + sbxmgr_feature_args=() + if [ -n "${sbxmgr_features}" ]; then + sbxmgr_feature_args=(--features "${sbxmgr_features}") + fi + echo "--- :rust: Running sbx-mgr production clippy without test-support (features: ${sbxmgr_features:-none})" + CARGO_TARGET_DIR="${target_dir}" cargo clippy \ + --locked \ + -p pplx-space-sbx-mgr-store \ + -p pplx-space-sbx-mgr \ + -p pplx-space-sbxmgr-svc \ + -p pplx-space-db-migrate \ + -p pplx-space-db-migrator \ + "${sbxmgr_feature_args[@]}" \ + --lib --bins \ + -- \ + -D warnings + sbxmgr_graph="$(cargo tree --locked \ + -p pplx-space-sbx-mgr -p pplx-space-sbxmgr-svc \ + -p pplx-space-db-migrate -p pplx-space-db-migrator \ + "${sbxmgr_feature_args[@]}" \ + -e normal,features --prefix none)" + if grep -q '^pplx-space-sbx-mgr-store feature "test-support"' <<<"${sbxmgr_graph}"; then + echo "sbx-mgr's production graph (features: ${sbxmgr_features:-none}) must not enable the store's test-support" >&2 + return 1 + fi + done +} + run_clippy_checks() { local target_dir="target/buildkite-lint" @@ -276,6 +322,8 @@ run_clippy_checks() { -- \ -D warnings + run_sbxmgr_production_checks "${target_dir}" + # feature-flags must lint clean with no vendor SDK compiled in; the matching # dependency-graph assertion lives in run_feature_isolation_checks. # `--all-targets` so the SDK-free test targets lint too; run_feature_isolation_checks diff --git a/crates/libs/db/migrator/Cargo.toml b/crates/libs/db/migrator/Cargo.toml index be3c1048f..a3edc306e 100644 --- a/crates/libs/db/migrator/Cargo.toml +++ b/crates/libs/db/migrator/Cargo.toml @@ -21,7 +21,7 @@ tracing = { workspace = true } # internal pplx-space-common-svc = { workspace = true, features = ["sqlite"] } -pplx-space-sbxmgr-migrations = { workspace = true } +pplx-space-sbx-mgr-store = { workspace = true } [dev-dependencies] pplx-space-common-jobs = { workspace = true } diff --git a/crates/libs/db/migrator/build.rs b/crates/libs/db/migrator/build.rs index 20c1c59f6..8fdf216c9 100644 --- a/crates/libs/db/migrator/build.rs +++ b/crates/libs/db/migrator/build.rs @@ -3,7 +3,6 @@ fn main() { // also invalidate the embedded schema. watch("../../../../db/postgres"); watch("../../../../db/common"); - watch("../../../../db/sandbox-manager"); watch("../../../../db/template-manager"); watch("../../../../db/node-manager"); watch("../../../../db/node-ctl"); diff --git a/crates/libs/db/migrator/src/lib.rs b/crates/libs/db/migrator/src/lib.rs index 05197132c..3456e8a9b 100644 --- a/crates/libs/db/migrator/src/lib.rs +++ b/crates/libs/db/migrator/src/lib.rs @@ -34,7 +34,6 @@ mod pg_test_support; pub mod user_password; static POSTGRES_MIGRATOR: Migrator = sqlx::migrate!("../../../../db/postgres"); -use pplx_space_sbxmgr_migrations::SANDBOX_MANAGER_MIGRATOR; static TEMPLATE_MANAGER_MIGRATOR: Migrator = sqlx::migrate!("../../../../db/template-manager"); static NODE_MANAGER_MIGRATOR: Migrator = sqlx::migrate!("../../../../db/node-manager"); static NODE_CTL_MIGRATOR: Migrator = sqlx::migrate!("../../../../db/node-ctl"); @@ -218,7 +217,13 @@ pub async fn run(config: DbMigrateRunConfig) -> Result<(), RunDbMigrateError> { Database::ControlPlane(args) => run_control_plane(args).await, Database::BootstrapRoles(args) => Ok(bootstrap_roles::run(args).await?), Database::SetUserPassword(args) => Ok(user_password::run(args).await?), - Database::SandboxManager(args) => run_sqlite(sqlite_sandbox_manager(), args).await, + Database::SandboxManager(args) => { + run_sqlite( + pplx_space_sbx_mgr_store::db::migration::target().into(), + args, + ) + .await + } Database::TemplateManager(args) => run_sqlite(sqlite_template_manager(), args).await, Database::NodeManager(args) => run_sqlite(sqlite_node_manager(), args).await, Database::NodeCtl(args) => run_sqlite(sqlite_node_ctl(), args).await, @@ -228,12 +233,16 @@ pub async fn run(config: DbMigrateRunConfig) -> Result<(), RunDbMigrateError> { } } -fn sqlite_sandbox_manager() -> SqliteDatabase { - SqliteDatabase { - name: "sandbox-manager", - default_path: pplx_space_common_svc::core::host::linux::paths::SANDBOX_MANAGER_DB, - migrator: &SANDBOX_MANAGER_MIGRATOR, - common_schema: true, +/// sbx-mgr's database, as its store describes it. It carries the shared +/// schema, which `run_sqlite` applies from [`COMMON_SCHEMA_MIGRATOR`]. +impl From for SqliteDatabase { + fn from(target: pplx_space_sbx_mgr_store::db::migration::MigrationTarget) -> Self { + Self { + name: target.name, + default_path: target.default_path, + migrator: target.service, + common_schema: true, + } } } diff --git a/crates/libs/db/migrator/src/tests.rs b/crates/libs/db/migrator/src/tests.rs index 5980caefb..5fc3ba461 100644 --- a/crates/libs/db/migrator/src/tests.rs +++ b/crates/libs/db/migrator/src/tests.rs @@ -5,7 +5,10 @@ use super::*; static ALL_MIGRATORS: &[(&str, &Migrator)] = &[ ("common", &COMMON_SCHEMA_MIGRATOR), ("postgres", &POSTGRES_MIGRATOR), - ("sandbox-manager", &SANDBOX_MANAGER_MIGRATOR), + ( + "sandbox-manager", + pplx_space_sbx_mgr_store::db::migration::target().service, + ), ("template-manager", &TEMPLATE_MANAGER_MIGRATOR), ("node-manager", &NODE_MANAGER_MIGRATOR), ("node-ctl", &NODE_CTL_MIGRATOR), @@ -18,7 +21,7 @@ static ALL_MIGRATORS: &[(&str, &Migrator)] = &[ async fn sandbox_baseline_refuses_down_without_changing_shared_jobs_or_history() { let directory = tempfile::tempdir().unwrap(); let path = directory.path().join("sandbox.sqlite"); - let database = sqlite_sandbox_manager(); + let database = SqliteDatabase::from(pplx_space_sbx_mgr_store::db::migration::target()); migrate_sqlite_up(database, &path).await.unwrap(); let conn = Connection::open(&path).unwrap(); conn.execute( @@ -135,7 +138,7 @@ async fn common_schema_converges_on_fresh_consumer_databases() { fn sqlite_databases_with_common_schema() -> [SqliteDatabase; 5] { [ - sqlite_sandbox_manager(), + SqliteDatabase::from(pplx_space_sbx_mgr_store::db::migration::target()), sqlite_template_manager(), sqlite_volume_manager(), sqlite_build_manager(), diff --git a/crates/libs/internal/api/server/src/node_local_backup_pending.rs b/crates/libs/internal/api/server/src/node_local_backup_pending.rs index 7d6e7f88f..1f86374ff 100644 --- a/crates/libs/internal/api/server/src/node_local_backup_pending.rs +++ b/crates/libs/internal/api/server/src/node_local_backup_pending.rs @@ -335,7 +335,7 @@ pub(super) enum PendingLocalBackupCensus { /// hand, and the set of node-side states that end up publishing a marker is /// wider than the obvious two — sandbox-manager pins its own artifact while /// setting `cleanup_required` -/// (`sbx/mgr/server/src/db/local_artifacts.rs`'s `pin_suspended_artifact_for_cleanup`, a +/// (`sbx/mgr/store/src/db/local_artifacts.rs`'s `pin_suspended_artifact_for_cleanup`, a /// SQLite writer), and the teardown-suspend path pins one mid-transition, /// and each of those eventually reaches this column through /// `publish_clear_or_preserve_current_local_artifact_marker_assignments`. A diff --git a/crates/libs/sbx/mgr/server/AGENTS.md b/crates/libs/sbx/mgr/server/AGENTS.md index 1c956e6f1..d39cb3be1 100644 --- a/crates/libs/sbx/mgr/server/AGENTS.md +++ b/crates/libs/sbx/mgr/server/AGENTS.md @@ -2,20 +2,13 @@ ## Changes -- Write the node-local SQLite `sandboxes.state` only through `src/db`: every - persisted write goes through `update_state_from` with an explicit edge, or - through a named specialized writer. A new edge in `src/db/state_write.rs` - needs a kind and a doc comment naming its writer; add any new disagreement - with `can_transition_to`, with a reason, to `ALLOWLIST` in - `src/db/tests/state_write_table.rs`. Nothing checks the table against the - writers; the `&'static Edge` argument does that at call sites. A refused - write must leave every row unchanged. Map an `update_state_from` outcome to - a status with `require_state_write_from`, or write it best effort with +- The node-local SQLite database belongs to `pplx-space-sbx-mgr-store` + (`../store`); its `AGENTS.md` has the state-write and child-table rules. + Map an `update_state_from` outcome to a status with + `require_state_write_from`, or write it best effort with `update_state_from_best_effort` (`src/service/cleanup_support.rs`); a caller that answers one case differently matches `state_write_result` and passes the rest to `into_status`. -- Foreign keys are off: register a new sandbox child table in - `delete_sandbox_child_rows` (`src/db.rs`). - `src/service/adapter.rs` implements the gRPC trait; most handler bodies live in `src/service/grpc_core.rs`, `src/service/grpc_lifecycle/` and the other `src/service/grpc_*.rs` modules. diff --git a/crates/libs/sbx/mgr/server/Cargo.toml b/crates/libs/sbx/mgr/server/Cargo.toml index 2b6d98b5f..576462135 100644 --- a/crates/libs/sbx/mgr/server/Cargo.toml +++ b/crates/libs/sbx/mgr/server/Cargo.toml @@ -41,12 +41,9 @@ 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 } sha2 = { workspace = true } -sqlx = { workspace = true, features = ["macros", "migrate"] } tempfile = { workspace = true } thiserror = { workspace = true } tokio = { workspace = true, features = ["fs", "io-util", "macros", "net", "process", "rt", "sync", "time"] } @@ -64,7 +61,7 @@ pplx-space-common-core = { workspace = true, features = ["serde"] } pplx-space-common-grpc = { workspace = true } pplx-space-common-jobs = { workspace = true } pplx-space-common-lru-reaper = { workspace = true } -pplx-space-common-svc = { workspace = true, features = ["sqlite"] } +pplx-space-common-svc = { workspace = true } pplx-space-credential-injection-core = { workspace = true } pplx-space-credmgr-client = { workspace = true } pplx-space-events = { workspace = true } @@ -81,22 +78,18 @@ pplx-space-spaced-common = { workspace = true } pplx-space-spaced-host-client = { workspace = true } pplx-space-observability = { workspace = true, default-features = false } pplx-space-sbx-mgr-client = { workspace = true } -pplx-space-sbxmgr-migrations = { workspace = true } +pplx-space-sbx-mgr-store = { workspace = true } pplx-space-template-mgr-client = { workspace = true } [dev-dependencies] rstest = { workspace = true } pplx-space-common-core = { workspace = true, features = ["serde", "test-hooks"] } -# `serialize` exposes `sqlite3_serialize`, which the test-schema helpers use to -# migrate once per test process and stamp the resulting image into each fixture -# database instead of replaying every migration per connection. -rusqlite = { workspace = true, features = ["serialize"] } +pplx-space-sbx-mgr-store = { workspace = true, features = ["test-support"] } pplx-space-net-gateway = { workspace = true, features = ["test-support"] } opentelemetry-proto = { workspace = true, features = ["gen-tonic", "metrics"] } pplx-space-credmgr = { workspace = true } rcgen = { workspace = true, features = ["aws_lc_rs", "x509-parser"] } rustls = { workspace = true, features = ["aws_lc_rs", "std", "tls12"] } -pplx-space-sbxmgr-migrations = { workspace = true } # `test-util` enables `#[tokio::test(start_paused = true)]` so retry-backoff # loops auto-advance virtual time instead of sleeping in real time. # `rt-multi-thread` lets lock-ordering regression tests run waiters in real diff --git a/crates/libs/sbx/mgr/server/src/lib.rs b/crates/libs/sbx/mgr/server/src/lib.rs index 74ce11e11..5e7067e9d 100644 --- a/crates/libs/sbx/mgr/server/src/lib.rs +++ b/crates/libs/sbx/mgr/server/src/lib.rs @@ -3,14 +3,10 @@ #![forbid(unsafe_code)] #![cfg_attr(not(test), deny(clippy::expect_used))] -pub mod auto_lifecycle; -pub(crate) mod auto_lifecycle_delete_receipts; pub(crate) mod btrfs_via_nodemgr; pub(crate) mod byok; pub mod config; -pub mod db; pub mod disk_usage; -pub mod endpoint_proof; mod firecracker_via_nodemgr; mod guest_filesystem_flush; mod inventory_budget; @@ -19,7 +15,6 @@ pub mod local_artifacts; mod net_gateway_clients; pub mod node_manager_transport; mod node_state_sync; -pub mod restore_operation; pub(crate) mod retained_source; pub(crate) mod rootfs_encryption; pub(crate) mod route_activation_claims; @@ -35,7 +30,6 @@ pub mod snapshot_restore; pub mod snapshot_scheduler; pub mod snapshot_upload; pub mod snapshot_upload_jobs; -pub mod state; pub mod template_resolver; mod transfer_directory; mod vm_diagnostics; @@ -44,4 +38,6 @@ pub mod watchdog; pub use pplx_space_sbx_mgr_client::{ SOCKET_PATH, SandboxManagerGrpcClient, connect, encode_optional_sandbox_id, proto, }; +pub(crate) use pplx_space_sbx_mgr_store::auto_lifecycle_delete_receipts; +pub use pplx_space_sbx_mgr_store::{auto_lifecycle, db, endpoint_proof, restore_operation, state}; pub use run::{RunSbxMgrError, SERVICE_NAME, SbxMgrRunConfig, run}; 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 902481bdd..8020d9ce8 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 @@ -19,7 +19,7 @@ use pplx_space_node_mgr_client::proto::{ResumeSandboxRequest, ResumeSandboxRespo use tonic::{Code, Status}; use super::BEGIN_BARRIER; -use crate::db::tests::state_write_fixtures::snapshot; +use crate::db::test_fixtures::state_write_fixtures::snapshot; use crate::node_manager_transport::fake::FakeNodeManager; use crate::proto::ResumeRequest; use crate::service::activation_support::internal_request; @@ -156,7 +156,7 @@ fn move_row(conn: &crate::db::Connection, id: &str, found: Found) { }; match state { SandboxState::Paused => {} - SandboxState::Stopped => crate::db::tests::state_write_fixtures::stop(conn, id), + SandboxState::Stopped => crate::db::test_fixtures::state_write_fixtures::stop(conn, id), state => { // The schema ties the restore columns to `restoring`. conn.execute( 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 ff5734424..d407d9c54 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 @@ -43,7 +43,11 @@ async fn gateway_identity_mapping_preserves_exact_pid_and_lifecycle_authority() crate::db::get_sandbox(&conn, &id).unwrap().unwrap().uid, Some(owner) ); - crate::db::tests::state_write_fixtures::force_state(&conn, &id, SandboxState::Stopped); + crate::db::test_fixtures::state_write_fixtures::force_state( + &conn, + &id, + SandboxState::Stopped, + ); } service .lookup_sandbox_by_net_gateway_process(4242, isolated) diff --git a/crates/libs/sbx/mgr/server/src/service/cache_cleanup/periodic/tests.rs b/crates/libs/sbx/mgr/server/src/service/cache_cleanup/periodic/tests.rs index 1d90aaf1b..de0c65be4 100644 --- a/crates/libs/sbx/mgr/server/src/service/cache_cleanup/periodic/tests.rs +++ b/crates/libs/sbx/mgr/server/src/service/cache_cleanup/periodic/tests.rs @@ -1,5 +1,5 @@ use super::*; -use crate::db::tests::database_rows; +use crate::db::test_fixtures::database_rows; use crate::service::tests::service_tests::insert_test_sandbox; #[tokio::test] 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 bb352ac89..a85d84d6d 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 @@ -951,7 +951,7 @@ fn inventory_keeps_a_quarantined_row_without_local_evidence() { // A quarantine is held for operator or cluster disposition, so missing // cache and runtime evidence is not proof that its row is stale. let conn = crate::db::test_conn(); - let sandbox_id = crate::db::tests::setup_quarantine_recovery_episode( + let sandbox_id = crate::db::test_fixtures::setup_quarantine_recovery_episode( &conn, SandboxQuarantineReasonCode::RestoreFirecrackerLaunchFailed, ); @@ -967,7 +967,7 @@ fn inventory_keeps_a_quarantined_row_without_local_evidence() { .as_deref(), Some(SandboxQuarantineReasonCode::RestoreFirecrackerLaunchFailed.as_str()) ); - let before = crate::db::tests::database_rows(&conn); + let before = crate::db::test_fixtures::database_rows(&conn); let cache_dir = tempfile::tempdir().expect("create cache directory"); let run_dir = tempfile::tempdir().expect("create run directory"); let paths = SandboxInventoryPaths::new(cache_dir.path(), run_dir.path()); @@ -1003,7 +1003,7 @@ fn inventory_keeps_a_quarantined_row_without_local_evidence() { "inventory must not take the quarantined row's cleanup fence" ); assert_eq!( - crate::db::tests::database_rows(&conn), + crate::db::test_fixtures::database_rows(&conn), before, "inventory must keep the quarantined row, its hold, history and artifact pin" ); @@ -1016,7 +1016,7 @@ fn inventory_keeps_a_quarantined_row_without_local_evidence() { crate::db::InventoryOwnershipReason::LifecycleOwned ) ); - assert_eq!(crate::db::tests::database_rows(&conn), before); + assert_eq!(crate::db::test_fixtures::database_rows(&conn), before); // The control: a stale row beside it is fenced and removed. let stale = SandboxId::new(); crate::service::tests::service_tests::insert_test_sandbox( 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 aebaf8876..1bb81bc95 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 @@ -31,7 +31,7 @@ use super::{ StopRequest, }; use crate::db::CleanupCompletion; -use crate::db::tests::state_write_fixtures::{assert_unchanged, snapshot}; +use crate::db::test_fixtures::state_write_fixtures::{assert_unchanged, snapshot}; use crate::service::tests::service_tests::insert_test_sandbox; use crate::state::SandboxState; 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 6e2a5f5f5..bda46f2c1 100644 --- a/crates/libs/sbx/mgr/server/src/service/cleanup_executor.rs +++ b/crates/libs/sbx/mgr/server/src/service/cleanup_executor.rs @@ -24,14 +24,12 @@ pub(super) enum CleanupWait { Stop, } -impl CleanupOutcome { - fn into_caller_result(self) -> AttemptResult { - match self { - Self::Applied | Self::AlreadyApplied => Ok(()), - Self::Superseded => Err(Status::aborted("cleanup superseded")), - Self::Quarantined(message) | Self::Blocked(message) => { - Err(Status::failed_precondition(message)) - } +fn caller_result(outcome: CleanupOutcome) -> AttemptResult { + match outcome { + CleanupOutcome::Applied | CleanupOutcome::AlreadyApplied => Ok(()), + CleanupOutcome::Superseded => Err(Status::aborted("cleanup superseded")), + CleanupOutcome::Quarantined(message) | CleanupOutcome::Blocked(message) => { + Err(Status::failed_precondition(message)) } } } @@ -353,7 +351,7 @@ impl SandboxManagerService { if let CleanupEligibility::Finished(outcome) = cleanup_jobs::eligibility(&tx, &work).map_err(storage_status)? { - outcome.into_caller_result()?; + caller_result(outcome)?; return Ok(None); } let job = cleanup_jobs::persist(&tx, work) 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 3e3a736b9..4ad61bf98 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 @@ -16,7 +16,7 @@ use tonic::Code; use super::{ StateWriteRejected, require_state_write_from, state_write_result, update_state_from_best_effort, }; -use crate::db::tests::state_write_fixtures::{assert_unchanged, snapshot}; +use crate::db::test_fixtures::state_write_fixtures::{assert_unchanged, snapshot}; use crate::db::{Edge, StateSet, StateWriteGuard, StateWriteOutcome, StateWriteRefusal, edges}; use crate::service::drain::{CriticalTaskGuard, DrainState}; use crate::service::tests::service_tests::insert_test_sandbox; 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 ff3415086..0884392ef 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 @@ -9,7 +9,7 @@ use pplx_space_common_core::types::SandboxQuarantineReasonCode; use super::*; -use crate::db::tests::state_write_fixtures::{ +use crate::db::test_fixtures::state_write_fixtures::{ admit_stop, assert_unchanged, backdate, force_state, insert_creating, insert_row, seed_receipt, set_failure_reason, snapshot, }; 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 bf7ce3bf8..312bd7eff 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 @@ -458,7 +458,7 @@ async fn gateway_authority_rechecks_ownership_after_await_but_not_ordinary_polic crate::db::update_netgtw_pid(&db, &id.to_string(), sandbox.uid.unwrap(), sandbox.netgtw_pid.unwrap() + 1).unwrap(); } "pool" => { crate::db::set_pool_template(&db, &id.to_string(), Some("warm-pool")).unwrap(); } - "stopping" => crate::db::tests::state_write_fixtures::apply_edge(&db, &id.to_string(), &crate::db::edges::STOP_ADMISSION), + "stopping" => crate::db::test_fixtures::state_write_fixtures::apply_edge(&db, &id.to_string(), &crate::db::edges::STOP_ADMISSION), "cleanup" => { crate::db::mark_cleanup_required(&db, &id.to_string(), true, crate::db::CleanupCompletion::MarkSuspended, "restore failed").unwrap(); } "policy" => { crate::db::update_network_policy_override(&db, &id.to_string(), Some(&NetworkPolicyV1::deny_all()), 2, crate::db::PolicyAuthority::Cluster, false, false).unwrap(); } _ => unreachable!(), 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 ce5cbd4aa..6d0917862 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 @@ -77,7 +77,7 @@ async fn legacy_delete_after_durable_delete_does_not_dispatch_node_cleanup() { ); { let db = service.db.lock().unwrap(); - crate::db::tests::state_write_fixtures::apply_edge( + crate::db::test_fixtures::state_write_fixtures::apply_edge( &db, &id, &crate::db::edges::DELETE_ADMISSION, @@ -1293,13 +1293,13 @@ async fn delete_removes_a_quarantine_whose_automatic_recovery_stopped() { return; } let conn = crate::db::test_conn(); - let sandbox_id = crate::db::tests::setup_quarantine_recovery_episode( + let sandbox_id = crate::db::test_fixtures::setup_quarantine_recovery_episode( &conn, SandboxQuarantineReasonCode::OrphanedSandboxRowMissing, ); let id = sandbox_id.to_string(); for _ in 0..crate::db::QUARANTINE_RECOVERY_PLAN_REJECTION_BOUND { - crate::db::tests::record_quarantine_recovery_attempt( + crate::db::test_fixtures::record_quarantine_recovery_attempt( &conn, sandbox_id, crate::db::QuarantineRecoveryNoProgressReason::PlanIneligible, 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 31f0993ca..cc471a50c 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 @@ -16,7 +16,7 @@ use std::sync::atomic::{AtomicUsize, Ordering}; use super::*; -use crate::db::tests::state_write_fixtures::{ +use crate::db::test_fixtures::state_write_fixtures::{ Snapshot, assert_unchanged, force_state, snapshot, stop, }; use crate::guest_filesystem_flush::GuestFlushError; 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 1f93ac910..74dbd660f 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 @@ -16,7 +16,9 @@ use std::sync::atomic::{AtomicUsize, Ordering}; use super::super::pause_resume::{GUEST_PREPARATION, GuestPreparation}; use super::*; -use crate::db::tests::state_write_fixtures::{Snapshot, assert_unchanged, force_state, snapshot}; +use crate::db::test_fixtures::state_write_fixtures::{ + Snapshot, assert_unchanged, force_state, snapshot, +}; use crate::guest_filesystem_flush::GuestFlushError; use crate::node_manager_transport::fake::FakeNodeManager; use crate::node_manager_transport::{ 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 069fd47bd..6ac2d8c86 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 @@ -214,7 +214,7 @@ async fn artifact_alias_waits_on_canonical_lock_and_rereads_after_admission() { let pending = service.lock_artifact_for_sandbox("01939e3e1111"); tokio::pin!(pending); assert!(futures_util::poll!(&mut pending).is_pending()); - crate::db::tests::state_write_fixtures::apply_edge( + crate::db::test_fixtures::state_write_fixtures::apply_edge( &service.db_conn().unwrap(), id, &crate::db::edges::AUTO_LIFECYCLE_STOP_SUSPENDED, @@ -1471,7 +1471,7 @@ fn deleting_sandbox_with_closed_teardown_suspend( .status, crate::db::TeardownSuspendOperationStatus::Running ); - crate::db::tests::state_write_fixtures::apply_edge( + crate::db::test_fixtures::state_write_fixtures::apply_edge( conn, &id, &crate::db::edges::DELETE_ADMISSION, @@ -1887,7 +1887,7 @@ async fn teardown_suspend_pause_failure_recovery_allows_same_generation_retry() let original = crate::db::latest_teardown_suspend_operation(&conn, &id) .unwrap() .unwrap(); - crate::db::tests::state_write_fixtures::apply_edge( + crate::db::test_fixtures::state_write_fixtures::apply_edge( &conn, &id, &crate::db::edges::PAUSE_BEGIN, @@ -2356,7 +2356,7 @@ async fn suspend_completion_refuses_a_superseded_row_before_runtime_cleanup() { match supersession { Supersession::CleanupActive => {} Supersession::Deleted => { - crate::db::tests::state_write_fixtures::apply_edge( + crate::db::test_fixtures::state_write_fixtures::apply_edge( &conn, &id, &crate::db::edges::DELETE_ADMISSION, diff --git a/crates/libs/sbx/mgr/server/src/service/grpc_pool_wait_clone/tests.rs b/crates/libs/sbx/mgr/server/src/service/grpc_pool_wait_clone/tests.rs index ab54e5c31..9d5ec02d9 100644 --- a/crates/libs/sbx/mgr/server/src/service/grpc_pool_wait_clone/tests.rs +++ b/crates/libs/sbx/mgr/server/src/service/grpc_pool_wait_clone/tests.rs @@ -340,7 +340,7 @@ async fn pool_clone_and_adoption_keep_donor_ownership_exclusive() { { let db = service.db.lock().unwrap(); assert!(crate::db::complete_pool_clone(&db, &id, cleanup_id).unwrap()); - crate::db::tests::state_write_fixtures::force_state(&db, &id, SandboxState::Paused); + crate::db::test_fixtures::state_write_fixtures::force_state(&db, &id, SandboxState::Paused); crate::db::update_user(&db, &id, &TEMPLATE_MANAGER_SANDBOX_OWNER).unwrap(); crate::db::set_pool_template(&db, &id, Some("seed")).unwrap(); } 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 70a496850..e29e6472b 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 @@ -1,5 +1,5 @@ use super::*; -use crate::db::tests::state_write_fixtures::{admit_stop, complete_stop}; +use crate::db::test_fixtures::state_write_fixtures::{admit_stop, complete_stop}; #[test] fn snapshot_inventory_does_not_return_partial_success_for_oversized_sidecars() { @@ -367,7 +367,7 @@ fn full_rollback_restage_leaves_a_stop_admitted_after_its_own_stop() { complete_stop(&conn, &id); assert_eq!(sandbox_state(&conn, &id), SandboxState::Stopped); admit_stop(&conn, &id); - let before = crate::db::tests::database_rows(&conn); + let before = crate::db::test_fixtures::database_rows(&conn); let error = restage_rolled_back_full_snapshot(&conn, &id) .expect_err("the restage must not overwrite a concurrent Stop"); @@ -377,7 +377,7 @@ fn full_rollback_restage_leaves_a_stop_admitted_after_its_own_stop() { error.message(), "complete process snapshot suspension: sandbox state write was superseded: sandbox is stopping" ); - assert_eq!(crate::db::tests::database_rows(&conn), before); + assert_eq!(crate::db::test_fixtures::database_rows(&conn), before); complete_stop(&conn, &id); let row = crate::db::get_sandbox(&conn, &id).unwrap().unwrap(); assert_eq!(row.state, SandboxState::Stopped); @@ -432,8 +432,8 @@ fn full_rollback_restage_writes_only_over_a_stopped_row() { let conn = crate::db::test_conn(); let id = SandboxId::new().to_string(); crate::service::tests::service_tests::insert_test_sandbox_with_runtime(&conn, &id, state); - crate::db::tests::state_write_fixtures::decorate(&conn, &id, SandboxState::Started); - let before = crate::db::tests::database_rows(&conn); + crate::db::test_fixtures::state_write_fixtures::decorate(&conn, &id, SandboxState::Started); + let before = crate::db::test_fixtures::database_rows(&conn); let result = restage_rolled_back_full_snapshot(&conn, &id); @@ -444,13 +444,21 @@ fn full_rollback_restage_writes_only_over_a_stopped_row() { } Expected::AlreadySuspended => { result.unwrap(); - assert_eq!(crate::db::tests::database_rows(&conn), before, "{state}"); + assert_eq!( + crate::db::test_fixtures::database_rows(&conn), + before, + "{state}" + ); } Expected::Refused(message) => { let error = result.expect_err("a refused restage must fail"); assert_eq!(error.code(), tonic::Code::Aborted, "{state}"); assert_eq!(error.message(), message, "{state}"); - assert_eq!(crate::db::tests::database_rows(&conn), before, "{state}"); + assert_eq!( + crate::db::test_fixtures::database_rows(&conn), + before, + "{state}" + ); } } } 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 f63023d20..3d755c0f4 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 @@ -218,9 +218,9 @@ fn template_base_snapshot_writes_leave_a_stop_that_completed_during_capture_stop ); begin_template_base_snapshot_state(&conn, &id).unwrap(); assert_eq!(sandbox_state(&conn, &id), SandboxState::Checkpointing); - crate::db::tests::state_write_fixtures::stop(&conn, &id); + crate::db::test_fixtures::state_write_fixtures::stop(&conn, &id); assert_eq!(sandbox_state(&conn, &id), SandboxState::Stopped); - let before = crate::db::tests::database_rows(&conn); + let before = crate::db::test_fixtures::database_rows(&conn); let db = crate::db::SharedConnection::new(conn); if completion { @@ -236,7 +236,7 @@ fn template_base_snapshot_writes_leave_a_stop_that_completed_during_capture_stop } assert_eq!( - crate::db::tests::database_rows(&db.lock().unwrap()), + crate::db::test_fixtures::database_rows(&db.lock().unwrap()), before, "completion={completion}" ); @@ -289,8 +289,12 @@ fn template_base_snapshot_writes_return_only_a_checkpointing_row_to_paused() { crate::service::tests::service_tests::insert_test_sandbox_with_runtime( &conn, &id, state, ); - crate::db::tests::state_write_fixtures::decorate(&conn, &id, SandboxState::Started); - let before = crate::db::tests::database_rows(&conn); + crate::db::test_fixtures::state_write_fixtures::decorate( + &conn, + &id, + SandboxState::Started, + ); + let before = crate::db::test_fixtures::database_rows(&conn); let db = crate::db::SharedConnection::new(conn); let result = if completion { @@ -318,7 +322,7 @@ fn template_base_snapshot_writes_return_only_a_checkpointing_row_to_paused() { assert_eq!(error.message(), message, "{state}"); } assert_eq!( - crate::db::tests::database_rows(&conn), + crate::db::test_fixtures::database_rows(&conn), before, "{state}, completion={completion}" ); @@ -369,7 +373,7 @@ fn template_base_snapshot_admission_writes_only_over_a_paused_row() { let conn = crate::db::test_conn(); let id = SandboxId::new().to_string(); crate::service::tests::service_tests::insert_test_sandbox_with_runtime(&conn, &id, state); - let before = crate::db::tests::database_rows(&conn); + let before = crate::db::test_fixtures::database_rows(&conn); let result = begin_template_base_snapshot_state(&conn, &id); @@ -382,7 +386,11 @@ fn template_base_snapshot_admission_writes_only_over_a_paused_row() { let error = result.expect_err("a refused admission must fail"); assert_eq!(error.code(), tonic::Code::Aborted, "{state}"); assert_eq!(error.message(), message, "{state}"); - assert_eq!(crate::db::tests::database_rows(&conn), before, "{state}"); + assert_eq!( + crate::db::test_fixtures::database_rows(&conn), + before, + "{state}" + ); } } } 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 262f054f4..eeb1d9ee6 100644 --- a/crates/libs/sbx/mgr/server/src/service/reconcile/tests.rs +++ b/crates/libs/sbx/mgr/server/src/service/reconcile/tests.rs @@ -809,7 +809,7 @@ async fn stop_suspended_with_concurrent_write( move_row: impl FnOnce(&crate::db::Connection), ) -> ( Result<(), Status>, - crate::db::tests::state_write_fixtures::Snapshot, + crate::db::test_fixtures::state_write_fixtures::Snapshot, ) { let barrier = Arc::new(tokio::sync::Barrier::new(2)); *service @@ -828,7 +828,7 @@ async fn stop_suspended_with_concurrent_write( let before = { let db = service.db.lock().unwrap(); move_row(&db); - crate::db::tests::state_write_fixtures::snapshot(&db) + crate::db::test_fixtures::state_write_fixtures::snapshot(&db) }; tokio::time::timeout(STOP_WRITE_WAIT, barrier.wait()) .await @@ -884,7 +884,7 @@ async fn auto_lifecycle_stop_writes_only_over_a_still_suspended_row() { insert_due_suspended_sandbox(&service, &id, 10); let (result, before) = stop_suspended_with_concurrent_write(&service, &id, |db| { - crate::db::tests::state_write_fixtures::force_state(db, &id, moved_to); + crate::db::test_fixtures::state_write_fixtures::force_state(db, &id, moved_to); }) .await; @@ -898,7 +898,7 @@ async fn auto_lifecycle_stop_writes_only_over_a_still_suspended_row() { "{case}: the Stop stage is recorded as fired" ); } else { - crate::db::tests::state_write_fixtures::assert_unchanged(&case, &before, &db); + crate::db::test_fixtures::state_write_fixtures::assert_unchanged(&case, &before, &db); } } } @@ -914,7 +914,7 @@ async fn stop_admitted_during_auto_lifecycle_stop_keeps_its_completion() { let service = isolated_service(scratch.path()); let id = SandboxId::new().to_string(); insert_due_suspended_sandbox(&service, &id, 10); - crate::db::tests::state_write_fixtures::seed_runtime(&service.db.lock().unwrap(), &id); + crate::db::test_fixtures::state_write_fixtures::seed_runtime(&service.db.lock().unwrap(), &id); let (result, before) = stop_suspended_with_concurrent_write(&service, &id, |db| { // Stop admission's state write. A blocking Stop persists no intent. @@ -931,7 +931,7 @@ async fn stop_admitted_during_auto_lifecycle_stop_keeps_its_completion() { assert!(result.is_ok(), "{result:?}"); let db = service.db.lock().unwrap(); - crate::db::tests::state_write_fixtures::assert_unchanged( + crate::db::test_fixtures::state_write_fixtures::assert_unchanged( "auto-lifecycle Stop after Stop admission", &before, &db, @@ -990,7 +990,7 @@ async fn delete_admitted_during_auto_lifecycle_stop_keeps_its_row() { assert!(result.is_ok(), "{result:?}"); let db = service.db.lock().unwrap(); - crate::db::tests::state_write_fixtures::assert_unchanged( + crate::db::test_fixtures::state_write_fixtures::assert_unchanged( "auto-lifecycle Stop after Delete admission", &before, &db, 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 e80a4f8d0..11a1b1a49 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 @@ -140,7 +140,7 @@ async fn staging_cleanup_rediscovery_survives_rollback_failed_deletion_and_resta SandboxState::Started, SandboxState::Paused, ] { - crate::db::tests::state_write_fixtures::force_state( + crate::db::test_fixtures::state_write_fixtures::force_state( &service.db.lock().unwrap(), &id, state, @@ -166,7 +166,7 @@ async fn staging_cleanup_rediscovery_survives_rollback_failed_deletion_and_resta fresh_id, b"new attempt", ); - crate::db::tests::state_write_fixtures::force_state( + crate::db::test_fixtures::state_write_fixtures::force_state( &service.db.lock().unwrap(), &id, SandboxState::Suspending, 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 a96581adb..d2943b061 100644 --- a/crates/libs/sbx/mgr/server/src/service/recovery/tests.rs +++ b/crates/libs/sbx/mgr/server/src/service/recovery/tests.rs @@ -602,7 +602,7 @@ pub(super) async fn wait_for_orphan_restore_capacity( #[tokio::test] async fn a_plan_that_reports_no_progress_charges_the_attempt_and_backs_off() { let conn = crate::db::test_conn(); - let sandbox_id = crate::db::tests::setup_quarantine_recovery_episode( + let sandbox_id = crate::db::test_fixtures::setup_quarantine_recovery_episode( &conn, pplx_space_common_core::types::SandboxQuarantineReasonCode::RestoreFirecrackerLaunchFailed, ); @@ -636,7 +636,7 @@ async fn a_plan_that_reports_no_progress_charges_the_attempt_and_backs_off() { #[tokio::test] async fn a_plan_for_a_different_reason_is_recorded_as_a_local_mismatch() { let conn = crate::db::test_conn(); - let sandbox_id = crate::db::tests::setup_quarantine_recovery_episode( + let sandbox_id = crate::db::test_fixtures::setup_quarantine_recovery_episode( &conn, pplx_space_common_core::types::SandboxQuarantineReasonCode::RestoreFirecrackerLaunchFailed, ); @@ -668,7 +668,7 @@ async fn a_plan_for_a_different_reason_is_recorded_as_a_local_mismatch() { #[tokio::test] async fn a_failed_plan_lookup_is_recorded_rather_than_retried_immediately() { let conn = crate::db::test_conn(); - let sandbox_id = crate::db::tests::setup_quarantine_recovery_episode( + let sandbox_id = crate::db::test_fixtures::setup_quarantine_recovery_episode( &conn, pplx_space_common_core::types::SandboxQuarantineReasonCode::RestoreFirecrackerLaunchFailed, ); @@ -699,7 +699,8 @@ async fn an_orphan_restore_runs_outside_the_inventory_pass_under_the_node_wide_b let reason_code = pplx_space_common_core::types::SandboxQuarantineReasonCode::OrphanedSandboxRowMissing; let conn = crate::db::test_conn(); - let sandbox_id = crate::db::tests::setup_quarantine_recovery_episode(&conn, reason_code); + let sandbox_id = + crate::db::test_fixtures::setup_quarantine_recovery_episode(&conn, reason_code); let service = recovery_test_service(conn); // The dispatched restore's own vol-manager transfer is not what this asserts // — the claim, the permit and the pass's early return are — and against an @@ -791,8 +792,8 @@ async fn background_dispatch_defers_a_second_orphan_while_the_first_holds_capaci let reason_code = pplx_space_common_core::types::SandboxQuarantineReasonCode::OrphanedSandboxRowMissing; let conn = crate::db::test_conn(); - let first_id = crate::db::tests::setup_quarantine_recovery_episode(&conn, reason_code); - let second_id = crate::db::tests::setup_quarantine_recovery_episode(&conn, reason_code); + let first_id = crate::db::test_fixtures::setup_quarantine_recovery_episode(&conn, reason_code); + let second_id = crate::db::test_fixtures::setup_quarantine_recovery_episode(&conn, reason_code); conn.execute( "UPDATE sandbox_quarantine_state SET entered_at = '1' WHERE sandbox_id = ?1", crate::db::fixture::params![first_id.to_string()], @@ -912,11 +913,12 @@ async fn the_dispatch_path_supervises_the_task_it_spawns() { let reason_code = pplx_space_common_core::types::SandboxQuarantineReasonCode::OrphanedSandboxRowMissing; let conn = crate::db::test_conn(); - let sandbox_id = crate::db::tests::setup_quarantine_recovery_episode(&conn, reason_code); + let sandbox_id = + crate::db::test_fixtures::setup_quarantine_recovery_episode(&conn, reason_code); // One short of the bound, so the accepted plan below is all that keeps the // next rejection from completing a run that the plan interrupted. for _ in 1..crate::db::QUARANTINE_RECOVERY_PLAN_REJECTION_BOUND { - crate::db::tests::record_quarantine_recovery_attempt( + crate::db::test_fixtures::record_quarantine_recovery_attempt( &conn, sandbox_id, crate::db::QuarantineRecoveryNoProgressReason::PlanIneligible, @@ -1058,7 +1060,8 @@ async fn a_momentarily_busy_staging_tree_does_not_charge_the_episode() { let reason_code = pplx_space_common_core::types::SandboxQuarantineReasonCode::OrphanedSandboxRowMissing; let conn = crate::db::test_conn(); - let sandbox_id = crate::db::tests::setup_quarantine_recovery_episode(&conn, reason_code); + let sandbox_id = + crate::db::test_fixtures::setup_quarantine_recovery_episode(&conn, reason_code); // After admission, stop at the live-runtime guard before touching the host // filesystem or calling volume-manager. Those operations use real I/O and // cannot finish reliably against the helper's auto-advancing virtual timeout. @@ -1106,7 +1109,8 @@ async fn a_busy_staging_tree_releases_node_capacity_and_backs_off_the_episode() let reason_code = pplx_space_common_core::types::SandboxQuarantineReasonCode::OrphanedSandboxRowMissing; let conn = crate::db::test_conn(); - let sandbox_id = crate::db::tests::setup_quarantine_recovery_episode(&conn, reason_code); + let sandbox_id = + crate::db::test_fixtures::setup_quarantine_recovery_episode(&conn, reason_code); let service = recovery_test_service(conn); let staging_lock = service.auto_lifecycle.staging_lock(&sandbox_id.to_string()); let staging_guard = staging_lock.lock_owned().await; @@ -1152,13 +1156,14 @@ async fn closed_capacity_is_not_reported_as_busy_capacity() { let orphan_reason = pplx_space_common_core::types::SandboxQuarantineReasonCode::OrphanedSandboxRowMissing; let conn = crate::db::test_conn(); - let artifact_id = crate::db::tests::setup_quarantine_recovery_episode(&conn, artifact_reason); + let artifact_id = + crate::db::test_fixtures::setup_quarantine_recovery_episode(&conn, artifact_reason); // Orphans behind the closed semaphore. Seeding none — as this test first did — // makes the `deferred == 0` assertion below true for the wrong reason: it cannot // tell "closed capacity is not reported as busy" from "there was nothing to // report either way", which is exactly the conflation it exists to catch. for _ in 0..3 { - crate::db::tests::setup_quarantine_recovery_episode(&conn, orphan_reason); + crate::db::test_fixtures::setup_quarantine_recovery_episode(&conn, orphan_reason); } let service = recovery_test_service(conn); service.quarantine_recovery_restore_sem.close(); @@ -1212,7 +1217,7 @@ async fn closed_capacity_is_not_reported_as_busy_capacity() { // And the other direction: an open semaphore must say nothing under this reason. let open_conn = crate::db::test_conn(); - crate::db::tests::setup_quarantine_recovery_episode(&open_conn, artifact_reason); + crate::db::test_fixtures::setup_quarantine_recovery_episode(&open_conn, artifact_reason); let open_service = recovery_test_service(open_conn); let counters = pplx_space_metrics::test_capture::capture_counters_guard(); open_service @@ -1247,9 +1252,10 @@ async fn capacity_deferred_orphans_do_not_consume_attempts_or_admission_budget() pplx_space_common_core::types::SandboxQuarantineReasonCode::RestorePrepareFailed; let conn = crate::db::test_conn(); let orphan_ids = (0..QUARANTINE_RECOVERY_ADMISSIONS_PER_PASS + 1) - .map(|_| crate::db::tests::setup_quarantine_recovery_episode(&conn, orphan_reason)) + .map(|_| crate::db::test_fixtures::setup_quarantine_recovery_episode(&conn, orphan_reason)) .collect::>(); - let artifact_id = crate::db::tests::setup_quarantine_recovery_episode(&conn, artifact_reason); + let artifact_id = + crate::db::test_fixtures::setup_quarantine_recovery_episode(&conn, artifact_reason); for sandbox_id in &orphan_ids { conn.execute( "UPDATE sandbox_quarantine_state SET entered_at = '1' WHERE sandbox_id = ?1", @@ -1326,9 +1332,12 @@ async fn capacity_released_mid_pass_does_not_leapfrog_an_older_orphan() { let artifact_reason = pplx_space_common_core::types::SandboxQuarantineReasonCode::RestorePrepareFailed; let conn = crate::db::test_conn(); - let first_orphan_id = crate::db::tests::setup_quarantine_recovery_episode(&conn, orphan_reason); - let artifact_id = crate::db::tests::setup_quarantine_recovery_episode(&conn, artifact_reason); - let later_orphan_id = crate::db::tests::setup_quarantine_recovery_episode(&conn, orphan_reason); + let first_orphan_id = + crate::db::test_fixtures::setup_quarantine_recovery_episode(&conn, orphan_reason); + let artifact_id = + crate::db::test_fixtures::setup_quarantine_recovery_episode(&conn, artifact_reason); + let later_orphan_id = + crate::db::test_fixtures::setup_quarantine_recovery_episode(&conn, orphan_reason); for (sandbox_id, entered_at) in [ (first_orphan_id, "1"), (artifact_id, "2"), @@ -1429,7 +1438,8 @@ async fn orphan_capacity_is_released_when_plan_lookup_fails_before_spawn() { let reason_code = pplx_space_common_core::types::SandboxQuarantineReasonCode::OrphanedSandboxRowMissing; let conn = crate::db::test_conn(); - let sandbox_id = crate::db::tests::setup_quarantine_recovery_episode(&conn, reason_code); + let sandbox_id = + crate::db::test_fixtures::setup_quarantine_recovery_episode(&conn, reason_code); let service = recovery_test_service(conn); service @@ -1461,7 +1471,7 @@ async fn orphan_capacity_is_released_when_plan_lookup_fails_before_spawn() { #[tokio::test] async fn a_candidate_that_stops_being_admissible_is_reported_without_a_write() { let conn = crate::db::test_conn(); - let sandbox_id = crate::db::tests::setup_quarantine_recovery_episode( + let sandbox_id = crate::db::test_fixtures::setup_quarantine_recovery_episode( &conn, pplx_space_common_core::types::SandboxQuarantineReasonCode::RestoreFirecrackerLaunchFailed, ); @@ -1558,7 +1568,7 @@ fn plan_for( #[tokio::test] async fn a_plan_naming_a_different_artifact_retains_the_quarantine() { let conn = crate::db::test_conn(); - let sandbox_id = crate::db::tests::setup_quarantine_recovery_episode( + let sandbox_id = crate::db::test_fixtures::setup_quarantine_recovery_episode( &conn, pplx_space_common_core::types::SandboxQuarantineReasonCode::RestoreFirecrackerLaunchFailed, ); @@ -1586,7 +1596,7 @@ async fn a_plan_naming_a_different_artifact_retains_the_quarantine() { #[tokio::test] async fn an_unreadable_local_artifact_retains_the_quarantine() { let conn = crate::db::test_conn(); - let sandbox_id = crate::db::tests::setup_quarantine_recovery_episode( + let sandbox_id = crate::db::test_fixtures::setup_quarantine_recovery_episode( &conn, pplx_space_common_core::types::SandboxQuarantineReasonCode::RestoreFirecrackerLaunchFailed, ); @@ -1665,7 +1675,7 @@ fn write_recoverable_suspend_snapshot( #[tokio::test] async fn a_snapshot_on_disk_naming_a_different_artifact_retains_the_quarantine() { let conn = crate::db::test_conn(); - let sandbox_id = crate::db::tests::setup_quarantine_recovery_episode( + let sandbox_id = crate::db::test_fixtures::setup_quarantine_recovery_episode( &conn, pplx_space_common_core::types::SandboxQuarantineReasonCode::RestoreFirecrackerLaunchFailed, ); @@ -1707,7 +1717,7 @@ async fn a_snapshot_on_disk_naming_a_different_artifact_retains_the_quarantine() #[tokio::test] async fn a_validated_artifact_is_adopted_into_durable_cleanup() { let conn = crate::db::test_conn(); - let sandbox_id = crate::db::tests::setup_quarantine_recovery_episode( + let sandbox_id = crate::db::test_fixtures::setup_quarantine_recovery_episode( &conn, pplx_space_common_core::types::SandboxQuarantineReasonCode::RestoreFirecrackerLaunchFailed, ); @@ -1856,7 +1866,8 @@ async fn a_deferred_orphan_restore_never_logs_the_storage_uri() { let reason_code = pplx_space_common_core::types::SandboxQuarantineReasonCode::OrphanedSandboxRowMissing; let conn = crate::db::test_conn(); - let sandbox_id = crate::db::tests::setup_quarantine_recovery_episode(&conn, reason_code); + let sandbox_id = + crate::db::test_fixtures::setup_quarantine_recovery_episode(&conn, reason_code); let candidate = crate::db::scan_quarantine_recovery_candidates(&conn) .unwrap() .candidates @@ -1984,7 +1995,8 @@ async fn abandoning_at_shutdown_schedules_the_retry_one_backoff_away() { let reason_code = pplx_space_common_core::types::SandboxQuarantineReasonCode::OrphanedSandboxRowMissing; let conn = crate::db::test_conn(); - let sandbox_id = crate::db::tests::setup_quarantine_recovery_episode(&conn, reason_code); + let sandbox_id = + crate::db::test_fixtures::setup_quarantine_recovery_episode(&conn, reason_code); let candidate = crate::db::scan_quarantine_recovery_candidates(&conn) .unwrap() .candidates @@ -1999,7 +2011,7 @@ async fn abandoning_at_shutdown_schedules_the_retry_one_backoff_away() { .unwrap() .expect("claiming a fresh episode should succeed"); let (_, attempts_after_claim, scheduled_by_claim, claim_after_claim, _) = - crate::db::tests::quarantine_claim_state(&conn, sandbox_id); + crate::db::test_fixtures::quarantine_claim_state(&conn, sandbox_id); assert_eq!( attempts_after_claim, 1, "the claim charges the first attempt" @@ -2026,7 +2038,7 @@ async fn abandoning_at_shutdown_schedules_the_retry_one_backoff_away() { .as_secs() as i64; let db = service.db.lock().expect("mutex poisoned"); let (_, attempts, scheduled, claim_token, claim_expiry) = - crate::db::tests::quarantine_claim_state(&db, sandbox_id); + crate::db::test_fixtures::quarantine_claim_state(&db, sandbox_id); let scheduled: i64 = scheduled .expect("a retry must be scheduled") .parse() @@ -2079,7 +2091,7 @@ async fn cancelling_shutdown_mid_pass_stops_the_next_candidate() { let conn = crate::db::test_conn(); let mut episodes = Vec::new(); for _ in 0..3 { - episodes.push(crate::db::tests::setup_quarantine_recovery_episode( + episodes.push(crate::db::test_fixtures::setup_quarantine_recovery_episode( &conn, reason_code, )); @@ -2133,7 +2145,7 @@ async fn cancelling_shutdown_mid_pass_stops_the_next_candidate() { .iter() .filter(|sandbox_id| { let (_, attempts, _, _, _) = - crate::db::tests::quarantine_claim_state(&db, **sandbox_id); + crate::db::test_fixtures::quarantine_claim_state(&db, **sandbox_id); attempts > 0 }) .count(); @@ -2160,7 +2172,8 @@ async fn a_stale_claim_reaches_the_sandbox_event_timeline() { let reason_code = pplx_space_common_core::types::SandboxQuarantineReasonCode::OrphanedSandboxRowMissing; let conn = crate::db::test_conn(); - let sandbox_id = crate::db::tests::setup_quarantine_recovery_episode(&conn, reason_code); + let sandbox_id = + crate::db::test_fixtures::setup_quarantine_recovery_episode(&conn, reason_code); let candidate = crate::db::scan_quarantine_recovery_candidates(&conn) .unwrap() .candidates @@ -2424,7 +2437,7 @@ async fn no_interleaving_in_any_named_window_lets_the_pass_write_under_a_moved_f let reason_code = fixture.reason_code(); let conn = crate::db::test_conn(); let sandbox_id = - crate::db::tests::setup_quarantine_recovery_episode(&conn, reason_code); + crate::db::test_fixtures::setup_quarantine_recovery_episode(&conn, reason_code); let service = recovery_test_service(conn); if fixture == PassFixture::Orphan { cancel_orphan_restore_operation(&service); @@ -2668,7 +2681,8 @@ async fn the_interleaving_matrix_fixture_adopts_when_nobody_interferes() { let reason_code = pplx_space_common_core::types::SandboxQuarantineReasonCode::RestoreFirecrackerLaunchFailed; let conn = crate::db::test_conn(); - let sandbox_id = crate::db::tests::setup_quarantine_recovery_episode(&conn, reason_code); + let sandbox_id = + crate::db::test_fixtures::setup_quarantine_recovery_episode(&conn, reason_code); let service = recovery_test_service(conn); let artifact_id = candidate_artifact_id(&service); let cache = tempfile::tempdir().unwrap(); @@ -2732,7 +2746,8 @@ async fn the_orphan_branch_reaches_the_restore_window() { let reason_code = pplx_space_common_core::types::SandboxQuarantineReasonCode::OrphanedSandboxRowMissing; let conn = crate::db::test_conn(); - let sandbox_id = crate::db::tests::setup_quarantine_recovery_episode(&conn, reason_code); + let sandbox_id = + crate::db::test_fixtures::setup_quarantine_recovery_episode(&conn, reason_code); let service = recovery_test_service(conn); cancel_orphan_restore_operation(&service); let cache = tempfile::tempdir().unwrap(); @@ -2980,7 +2995,8 @@ async fn the_cleanup_contention_release_reports_only_a_real_loss() { pplx_space_common_core::types::SandboxQuarantineReasonCode::OrphanedSandboxRowMissing; for supersede in [false, true] { let conn = crate::db::test_conn(); - let sandbox_id = crate::db::tests::setup_quarantine_recovery_episode(&conn, reason_code); + let sandbox_id = + crate::db::test_fixtures::setup_quarantine_recovery_episode(&conn, reason_code); let candidate = crate::db::scan_quarantine_recovery_candidates(&conn) .unwrap() .candidates @@ -3067,7 +3083,7 @@ async fn two_actors_in_two_windows_still_leave_every_fenced_write_missing() { } let conn = crate::db::test_conn(); let sandbox_id = - crate::db::tests::setup_quarantine_recovery_episode(&conn, reason_code); + crate::db::test_fixtures::setup_quarantine_recovery_episode(&conn, reason_code); let service = recovery_test_service(conn); let artifact_id = candidate_artifact_id(&service); let cache = tempfile::tempdir().unwrap(); @@ -3160,7 +3176,7 @@ async fn a_token_cancelled_after_a_claim_stops_the_next_candidate_before_its_loo pplx_space_common_core::types::SandboxQuarantineReasonCode::OrphanedSandboxRowMissing; let conn = crate::db::test_conn(); for _ in 0..3 { - crate::db::tests::setup_quarantine_recovery_episode(&conn, reason_code); + crate::db::test_fixtures::setup_quarantine_recovery_episode(&conn, reason_code); } let service = recovery_test_service(conn); let shutdown = tokio_util::sync::CancellationToken::new(); @@ -3211,7 +3227,8 @@ async fn a_claim_lost_during_the_orphan_restore_is_not_written_up() { let reason_code = pplx_space_common_core::types::SandboxQuarantineReasonCode::OrphanedSandboxRowMissing; let conn = crate::db::test_conn(); - let sandbox_id = crate::db::tests::setup_quarantine_recovery_episode(&conn, reason_code); + let sandbox_id = + crate::db::test_fixtures::setup_quarantine_recovery_episode(&conn, reason_code); let service = recovery_test_service(conn); cancel_orphan_restore_operation(&service); let cache = tempfile::tempdir().unwrap(); @@ -3269,9 +3286,12 @@ async fn closed_capacity_is_read_off_the_semaphore_not_the_walk() { let conn = crate::db::test_conn(); // Exactly the admission cap, so the walk breaks on the candidate after them. let adoptions: Vec<_> = (0..super::QUARANTINE_RECOVERY_ADMISSIONS_PER_PASS) - .map(|_| crate::db::tests::setup_quarantine_recovery_episode(&conn, artifact_reason)) + .map(|_| { + crate::db::test_fixtures::setup_quarantine_recovery_episode(&conn, artifact_reason) + }) .collect(); - let orphan_id = crate::db::tests::setup_quarantine_recovery_episode(&conn, orphan_reason); + let orphan_id = + crate::db::test_fixtures::setup_quarantine_recovery_episode(&conn, orphan_reason); // The orphan was inserted after the adoptions, so it falls on the next page. conn.execute( "UPDATE sandbox_quarantine_state @@ -3609,7 +3629,8 @@ async fn a_held_cleanup_fence_defers_adoption_and_charges_the_attempt() { let reason_code = pplx_space_common_core::types::SandboxQuarantineReasonCode::RestoreFirecrackerLaunchFailed; let conn = crate::db::test_conn(); - let sandbox_id = crate::db::tests::setup_quarantine_recovery_episode(&conn, reason_code); + let sandbox_id = + crate::db::test_fixtures::setup_quarantine_recovery_episode(&conn, reason_code); let service = recovery_test_service(conn); let artifact_id = candidate_artifact_id(&service); let cache_dir = tempfile::tempdir().unwrap(); @@ -3722,7 +3743,7 @@ fn bound() -> i64 { #[tokio::test] async fn plan_rejections_stop_recovery_at_the_bound_and_later_passes_never_look_it_up() { let conn = crate::db::test_conn(); - let sandbox_id = crate::db::tests::setup_quarantine_recovery_episode( + let sandbox_id = crate::db::test_fixtures::setup_quarantine_recovery_episode( &conn, pplx_space_common_core::types::SandboxQuarantineReasonCode::RestoreFirecrackerLaunchFailed, ); @@ -3763,7 +3784,7 @@ async fn stopped_holds_take_no_admission_from_a_recoverable_hold() { let conn = crate::db::test_conn(); // More stopped holds than a pass admits, all older than the recoverable one. let stopped = (0..QUARANTINE_RECOVERY_ADMISSIONS_PER_PASS + 1) - .map(|_| crate::db::tests::setup_quarantine_recovery_episode(&conn, reason_code)) + .map(|_| crate::db::test_fixtures::setup_quarantine_recovery_episode(&conn, reason_code)) .collect::>(); let service = bound_test_service(conn); let cache = tempfile::tempdir().unwrap(); @@ -3781,7 +3802,7 @@ async fn stopped_holds_take_no_admission_from_a_recoverable_hold() { assert_eq!(retry_state(&service, *sandbox_id).0, bound()); } - let recoverable = crate::db::tests::setup_quarantine_recovery_episode( + let recoverable = crate::db::test_fixtures::setup_quarantine_recovery_episode( &service.db.lock().unwrap(), reason_code, ); @@ -3805,7 +3826,8 @@ async fn a_restart_allows_a_stopped_hold_one_more_rejection_and_a_plan_answer_re let reason_code = pplx_space_common_core::types::SandboxQuarantineReasonCode::RestoreFirecrackerLaunchFailed; let conn = crate::db::test_conn(); - let sandbox_id = crate::db::tests::setup_quarantine_recovery_episode(&conn, reason_code); + let sandbox_id = + crate::db::test_fixtures::setup_quarantine_recovery_episode(&conn, reason_code); let service = bound_test_service(conn); let cache = tempfile::tempdir().unwrap(); let lookups = Arc::new(std::sync::Mutex::new(Vec::new())); 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 cb349ec50..ba2284101 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 @@ -1008,7 +1008,8 @@ fn a_superseded_recovery_claim_leaves_no_adopted_row() { let conn = crate::db::test_conn(); let reason_code = pplx_space_common_core::types::SandboxQuarantineReasonCode::OrphanedSandboxRowMissing; - let sandbox_id = crate::db::tests::setup_quarantine_recovery_episode(&conn, reason_code); + let sandbox_id = + crate::db::test_fixtures::setup_quarantine_recovery_episode(&conn, reason_code); let candidate = crate::db::scan_quarantine_recovery_candidates(&conn) .unwrap() .candidates @@ -1187,7 +1188,8 @@ fn a_sandbox_that_left_an_activatable_state_is_refused_before_adoption() { let conn = crate::db::test_conn(); let reason_code = pplx_space_common_core::types::SandboxQuarantineReasonCode::OrphanedSandboxRowMissing; - let sandbox_id = crate::db::tests::setup_quarantine_recovery_episode(&conn, reason_code); + let sandbox_id = + crate::db::test_fixtures::setup_quarantine_recovery_episode(&conn, reason_code); let candidate = crate::db::scan_quarantine_recovery_candidates(&conn) .unwrap() .candidates @@ -1291,7 +1293,8 @@ fn the_renewal_budget_aborts_a_full_heartbeat_inside_the_lease() { #[test] fn a_superseded_claim_cannot_persist_a_canonical_publication_failure() { let conn = crate::db::test_conn(); - let (sandbox_id, candidate) = crate::db::tests::setup_quarantine_recovery_candidate(&conn); + let (sandbox_id, candidate) = + crate::db::test_fixtures::setup_quarantine_recovery_candidate(&conn); let claimed = crate::db::claim_quarantine_recovery_candidate( &conn, &candidate, @@ -1921,7 +1924,7 @@ async fn rowless_restore_keeps_a_superseded_scope_it_cannot_retire() { fn claim_orphan_episode( conn: &crate::db::Connection, ) -> (SandboxId, crate::db::ClaimedQuarantineRecoveryCandidate) { - let sandbox_id = crate::db::tests::setup_quarantine_recovery_episode( + let sandbox_id = crate::db::test_fixtures::setup_quarantine_recovery_episode( conn, pplx_space_common_core::types::SandboxQuarantineReasonCode::OrphanedSandboxRowMissing, ); 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 47ff32c65..27cb63298 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 @@ -307,7 +307,7 @@ async fn identity_equal_adoption_does_not_overwrite_an_owned_execution() { SandboxState::Started } ExecutionOwner::LifecycleTransition => { - crate::db::tests::state_write_fixtures::apply_edge( + crate::db::test_fixtures::state_write_fixtures::apply_edge( &conn, &id, &crate::db::edges::STOP_ADMISSION, diff --git a/crates/libs/sbx/mgr/server/src/service/tests.rs b/crates/libs/sbx/mgr/server/src/service/tests.rs index 65ec1943e..57a41c5f6 100644 --- a/crates/libs/sbx/mgr/server/src/service/tests.rs +++ b/crates/libs/sbx/mgr/server/src/service/tests.rs @@ -1907,12 +1907,12 @@ pub(crate) mod service_tests { None, ) .unwrap(); - crate::db::tests::state_write_fixtures::apply_edge( + crate::db::test_fixtures::state_write_fixtures::apply_edge( &db, &sandbox_id_string, &crate::db::edges::CREATE_COMPLETE, ); - crate::db::tests::state_write_fixtures::apply_edge( + crate::db::test_fixtures::state_write_fixtures::apply_edge( &db, &sandbox_id_string, &crate::db::edges::CREATE_BASE_SNAPSHOT_COMPLETE, @@ -5381,7 +5381,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::Starting); - crate::db::tests::state_write_fixtures::apply_edge( + crate::db::test_fixtures::state_write_fixtures::apply_edge( &conn, &sandbox_id, &crate::db::edges::DELETE_ADMISSION, @@ -6659,9 +6659,7 @@ pub(crate) mod service_tests { } } - pub(crate) fn insert_test_sandbox(conn: &crate::db::Connection, id: &str, state: SandboxState) { - insert_test_sandbox_with_policy(conn, id, state, &AutoLifecyclePolicy::disabled()); - } + pub(crate) use crate::db::fixture::{insert_test_sandbox, insert_test_sandbox_with_policy}; fn mark_test_remote_restore_artifact_staging(conn: &crate::db::Connection, id: &str) { conn.execute( @@ -9075,7 +9073,7 @@ pub(crate) mod service_tests { Case::PriorNodeIdentity, ] { let conn = crate::db::test_conn(); - let sandbox_id = crate::db::tests::setup_quarantine_recovery_episode( + let sandbox_id = crate::db::test_fixtures::setup_quarantine_recovery_episode( &conn, SandboxQuarantineReasonCode::OrphanedSandboxRowMissing, ); @@ -10473,7 +10471,7 @@ pub(crate) mod service_tests { .expect("delete must reach the cleanup claim"); { let db = service.db.lock().expect("mutex poisoned"); - crate::db::tests::state_write_fixtures::force_state( + crate::db::test_fixtures::state_write_fixtures::force_state( &db, &sandbox_id_string, SandboxState::Suspending, @@ -10947,7 +10945,7 @@ pub(crate) mod service_tests { let sandbox_id = SandboxId::new(); let sandbox_id_string = sandbox_id.to_string(); insert_test_sandbox(&conn, &sandbox_id_string, SandboxState::Started); - crate::db::tests::state_write_fixtures::force_state( + crate::db::test_fixtures::state_write_fixtures::force_state( &conn, &sandbox_id_string, SandboxState::Paused, @@ -11168,7 +11166,7 @@ pub(crate) mod service_tests { runtime_fields(ExecutionGeneration::new(2).unwrap(), 7), ) .unwrap(); - crate::db::tests::state_write_fixtures::apply_edge( + crate::db::test_fixtures::state_write_fixtures::apply_edge( &conn, &sandbox_id, &crate::db::edges::STOP_ADMISSION, @@ -11295,7 +11293,7 @@ pub(crate) mod service_tests { runtime_fields(deleted_generation, 7), ) .unwrap(); - crate::db::tests::state_write_fixtures::apply_edge( + crate::db::test_fixtures::state_write_fixtures::apply_edge( conn, sandbox_id, &crate::db::edges::DELETE_ADMISSION, @@ -12011,7 +12009,7 @@ pub(crate) mod service_tests { ExecutionGeneration::new(1).unwrap(), ) .unwrap(); - crate::db::tests::state_write_fixtures::apply_edge( + crate::db::test_fixtures::state_write_fixtures::apply_edge( &conn, &sandbox_id.to_string(), &crate::db::edges::SUSPEND_ROLLBACK, @@ -12497,6 +12495,37 @@ pub(crate) mod service_tests { ); } + #[test] + fn a_successful_cold_start_captures_a_fresh_memory_volume() { + let conn = crate::db::test_conn(); + let sandbox_id = SandboxId::new().to_string(); + insert_test_sandbox(&conn, &sandbox_id, SandboxState::Starting); + conn.execute( + "UPDATE sandboxes + SET current_local_artifact_id = ?1, + current_local_artifact_kind = 'full', + current_local_artifact_origin = 'suspend' + WHERE id = ?2", + crate::db::fixture::params![ + pplx_space_common_core::types::LocalArtifactId::new().to_string(), + sandbox_id + ], + ) + .unwrap(); + let before = crate::db::get_sandbox(&conn, &sandbox_id).unwrap().unwrap(); + assert!(!crate::snapshot_encryption::capture_volume_is_fresh( + &before + )); + + assert!(crate::db::mark_start_succeeded(&conn, &sandbox_id, None).unwrap()); + + let started = crate::db::get_sandbox(&conn, &sandbox_id).unwrap().unwrap(); + assert!( + crate::snapshot_encryption::capture_volume_is_fresh(&started), + "a successful cold start must not reuse an older suspend memory image" + ); + } + #[test] fn failed_encrypted_capture_keeps_live_memory_base_mounted() { assert!(!super::grpc_lifecycle::failed_encrypted_capture_may_release_volume(true)); @@ -13329,41 +13358,6 @@ pub(crate) mod service_tests { .unwrap(); } - pub(crate) fn insert_test_sandbox_with_policy( - conn: &crate::db::Connection, - id: &str, - state: SandboxState, - auto_lifecycle: &AutoLifecyclePolicy, - ) { - let sandbox_id = SandboxId::from_str(id).unwrap(); - let uuid8 = SandboxRuntimeLabel::from_sandbox_id(sandbox_id); - let user = SandboxOwner::parse("user").unwrap(); - crate::db::insert_sandbox( - conn, - crate::db::InsertSandboxParams { - id, - uuid8: &uuid8, - state, - template: "seed", - template_hash: Some("hash"), - user: &user, - runtime: "fc", - snapshots_enabled: false, - rootfs_size_mb: None, - disk_io_limits: Default::default(), - pool_template: None, - volume_encryption: false, - process_env: &BTreeMap::new(), - auto_lifecycle, - network_policy_override: None, - network_policy_generation: 0, - network_policy_authority: crate::db::PolicyAuthority::Cluster, - auto_lifecycle_authority: crate::db::PolicyAuthority::Cluster, - }, - ) - .unwrap(); - } - #[tokio::test] async fn restart_drain_rejects_new_writers_and_waits_for_active_transitions() { let conn = crate::db::test_conn(); @@ -15148,7 +15142,7 @@ pub(crate) mod service_tests { }; { let db = service.db.lock().expect("mutex poisoned"); - crate::db::tests::state_write_fixtures::apply_edge( + crate::db::test_fixtures::state_write_fixtures::apply_edge( &db, &sandbox_id, &crate::db::edges::SUSPEND_ROLLBACK, @@ -16767,7 +16761,7 @@ pub(crate) mod service_tests { let service = SandboxManagerService::for_test(conn); { let db = service.db.lock().expect("mutex poisoned"); - crate::db::tests::state_write_fixtures::force_state( + crate::db::test_fixtures::state_write_fixtures::force_state( &db, &sandbox_id, SandboxState::Suspended, @@ -18092,7 +18086,7 @@ pub(crate) mod service_tests { let sandbox_id = SandboxId::new().to_string(); insert_test_sandbox(&conn, &sandbox_id, SandboxState::Suspended); register_full_suspend_artifact(&conn, &sandbox_id); - crate::db::tests::state_write_fixtures::apply_edge( + crate::db::test_fixtures::state_write_fixtures::apply_edge( &conn, &sandbox_id, &crate::db::edges::RESTORE_BEGIN, 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 8ae33a59c..1b2130834 100644 --- a/crates/libs/sbx/mgr/server/src/snapshot_scheduler/tests.rs +++ b/crates/libs/sbx/mgr/server/src/snapshot_scheduler/tests.rs @@ -184,7 +184,7 @@ fn checkpoint_admission_writes_only_over_a_started_or_paused_row() { }; let (ctx, sandbox_id) = checkpoint_context(state); let db = ctx.db.lock().unwrap(); - let before = crate::db::tests::database_rows(&db); + let before = crate::db::test_fixtures::database_rows(&db); let result = begin_checkpoint_state(&db, &sandbox_id); @@ -196,7 +196,11 @@ fn checkpoint_admission_writes_only_over_a_started_or_paused_row() { } Some(message) => { assert_eq!(result.unwrap_err(), message, "{state}"); - assert_eq!(crate::db::tests::database_rows(&db), before, "{state}"); + assert_eq!( + crate::db::test_fixtures::database_rows(&db), + before, + "{state}" + ); } } } @@ -206,13 +210,13 @@ fn checkpoint_admission_writes_only_over_a_started_or_paused_row() { #[test] fn checkpoint_admission_of_a_missing_row_reports_it() { let db = crate::db::test_conn(); - let before = crate::db::tests::database_rows(&db); + let before = crate::db::test_fixtures::database_rows(&db); assert_eq!( begin_checkpoint_state(&db, &SandboxId::new().to_string()).unwrap_err(), "sandbox state write was not applied: missing" ); - assert_eq!(crate::db::tests::database_rows(&db), before); + assert_eq!(crate::db::test_fixtures::database_rows(&db), before); } #[tokio::test] diff --git a/crates/libs/sbx/mgr/store/AGENTS.md b/crates/libs/sbx/mgr/store/AGENTS.md new file mode 100644 index 000000000..a4de517b8 --- /dev/null +++ b/crates/libs/sbx/mgr/store/AGENTS.md @@ -0,0 +1,30 @@ +# `pplx-space-sbx-mgr-store` + +sbx-mgr's node-local SQLite store: `sandboxes` and every other sbx-mgr table, +not the control-plane Postgres. Only this crate opens, migrates or queries that +database. `dependency_fence.sbxmgr_rusqlite` in `ARCHITECTURE.toml` keeps +rusqlite out of every other sbx-mgr crate. + +## Changes + +- Outside the `test-support` feature, no public item may return, expose or + re-export a rusqlite connection, transaction, statement, row or error + value; the one exception is the `FromSqlError` that `FromSql` impls return + by design. A method that hands one out is declared through + `db_private_outside_tests!` (`src/db/handle.rs`), which makes it public only + with `test-support`. +- Write `sandboxes.state` only through `update_state_from` with an explicit + edge, or through a named specialized writer. A new edge in + `src/db/state_write.rs` needs a kind and a doc comment naming its writer; + add any new disagreement with `can_transition_to`, with a reason, to + `ALLOWLIST` in `src/db/tests/state_write_table.rs`. Nothing checks the table + against the writers; the `&'static Edge` argument does that at call sites. A + refused write must leave every row unchanged. +- Foreign keys are off: register a new sandbox child table in + `delete_sandbox_child_rows` (`src/db.rs`). +- Fixtures that sbx-mgr's other test suites share live in `db::fixture` and + `db::test_fixtures`, behind `test-support`; fixtures only this crate's tests + use stay in the test modules. + +Module moves change log targets: see "Log targets" in +`docs/rust/space-conventions.md`. diff --git a/crates/libs/sbx/mgr/store/CLAUDE.md b/crates/libs/sbx/mgr/store/CLAUDE.md new file mode 120000 index 000000000..47dc3e3d8 --- /dev/null +++ b/crates/libs/sbx/mgr/store/CLAUDE.md @@ -0,0 +1 @@ +AGENTS.md \ No newline at end of file diff --git a/crates/libs/sbx/mgr/store/Cargo.toml b/crates/libs/sbx/mgr/store/Cargo.toml new file mode 100644 index 000000000..b833e9942 --- /dev/null +++ b/crates/libs/sbx/mgr/store/Cargo.toml @@ -0,0 +1,57 @@ +[package] +name = "pplx-space-sbx-mgr-store" +version.workspace = true +edition.workspace = true +rust-version.workspace = true +authors.workspace = true +license.workspace = true +publish.workspace = true + +[lints] +workspace = true + +[features] +default = [] +# Exposes the raw connection and the SQL fixtures (test connections, schema +# images, `db::fixture`) to sbx-mgr's test suites, which enable it as a +# dev-dependency. Unit tests in this crate get them under `cfg(test)`. +# `serialize` provides `sqlite3_serialize`, which the test-schema helpers use +# to migrate once per test process and stamp the resulting image into each +# fixture database instead of replaying every migration per connection. +test-support = ["rusqlite/serialize"] + +[dependencies] +dashmap = { workspace = true } +jiff = { workspace = true } +ref-cast = { workspace = true } +rustix = { workspace = true, features = ["fs"] } +rusqlite = { workspace = true } +serde = { workspace = true, features = ["derive"] } +serde_json = { workspace = true } +sqlx = { workspace = true, features = ["migrate"] } +thiserror = { workspace = true } +tokio = { workspace = true, features = ["sync"] } +tonic = { workspace = true } +tracing = { workspace = true } +uuid = { workspace = true, features = ["v7"] } + +# internal +pplx-space-common-core = { workspace = true, features = ["serde"] } +pplx-space-common-jobs = { workspace = true } +pplx-space-common-svc = { workspace = true, features = ["sqlite"] } +pplx-space-events = { workspace = true } +pplx-space-metrics = { workspace = true } +pplx-space-net-common = { workspace = true, features = ["serde"] } +pplx-space-node-mgr-client = { workspace = true } +pplx-space-observability = { workspace = true, default-features = false } +pplx-space-sbx-mgr-client = { workspace = true } +pplx-space-sbxmgr-migrations = { workspace = true } + +[dev-dependencies] +futures-util = { workspace = true } +pplx-space-common-core = { workspace = true, features = ["serde", "test-hooks"] } +pplx-space-node-runtime-core = { workspace = true } +prost-types = { workspace = true } +rusqlite = { workspace = true, features = ["serialize"] } +tempfile = { workspace = true } +tokio = { workspace = true, features = ["macros", "rt", "rt-multi-thread", "sync", "test-util", "time"] } diff --git a/crates/libs/sbx/mgr/server/src/auto_lifecycle.rs b/crates/libs/sbx/mgr/store/src/auto_lifecycle.rs similarity index 98% rename from crates/libs/sbx/mgr/server/src/auto_lifecycle.rs rename to crates/libs/sbx/mgr/store/src/auto_lifecycle.rs index def569f9a..ef019ed18 100644 --- a/crates/libs/sbx/mgr/server/src/auto_lifecycle.rs +++ b/crates/libs/sbx/mgr/store/src/auto_lifecycle.rs @@ -11,7 +11,7 @@ use pplx_space_common_core::types::{ }; use tonic::Status; -use crate::proto; +use pplx_space_sbx_mgr_client::proto; pub const ACCESS_IDLE_CONDITION_SET: AutoLifecycleConditionSetId = AutoLifecycleConditionSetId::AccessIdle; @@ -20,12 +20,13 @@ pub const COMMAND_IDLE_CONDITION_SET: AutoLifecycleConditionSetId = pub const GUEST_ACTIVITY_CONDITION_SET: AutoLifecycleConditionSetId = AutoLifecycleConditionSetId::GuestActivity; -#[cfg(test)] -pub(crate) const TEST_GUEST_ACTIVITY_HOLD_CAP: std::time::Duration = +#[cfg(any(test, feature = "test-support"))] +pub const TEST_GUEST_ACTIVITY_HOLD_CAP: std::time::Duration = std::time::Duration::from_secs(5 * 60); -#[cfg(test)] -pub(crate) fn test_guest_activity_hold_cap() -> pplx_space_common_core::types::SimpleDuration { +#[cfg(any(test, feature = "test-support"))] +#[allow(clippy::unwrap_used)] +pub fn test_guest_activity_hold_cap() -> pplx_space_common_core::types::SimpleDuration { TEST_GUEST_ACTIVITY_HOLD_CAP.try_into().unwrap() } @@ -130,8 +131,8 @@ pub struct AutoLifecycleCoordinator { last_wake_completed: Arc>, /// A test's two-phase rendezvous before auto-lifecycle Stop writes a /// `suspended` row, so the test can move the row in between. - #[cfg(test)] - pub(crate) suspended_stop_barrier: Arc>>>, + #[cfg(any(test, feature = "test-support"))] + pub suspended_stop_barrier: Arc>>>, } impl AutoLifecycleCoordinator { diff --git a/crates/libs/sbx/mgr/server/src/auto_lifecycle/tests.rs b/crates/libs/sbx/mgr/store/src/auto_lifecycle/tests.rs similarity index 99% rename from crates/libs/sbx/mgr/server/src/auto_lifecycle/tests.rs rename to crates/libs/sbx/mgr/store/src/auto_lifecycle/tests.rs index 704f056b2..10a148cc5 100644 --- a/crates/libs/sbx/mgr/server/src/auto_lifecycle/tests.rs +++ b/crates/libs/sbx/mgr/store/src/auto_lifecycle/tests.rs @@ -14,7 +14,7 @@ use super::{ policy_from_proto, recompute_runtime_state, test_guest_activity_hold_cap, touch_access, touch_command_and_access, validate_v1_policy, }; -use crate::proto; +use pplx_space_sbx_mgr_client::proto; fn sample_policy() -> AutoLifecyclePolicy { let mut policy = AutoLifecyclePolicy { diff --git a/crates/libs/sbx/mgr/server/src/auto_lifecycle_delete_receipts/mod.rs b/crates/libs/sbx/mgr/store/src/auto_lifecycle_delete_receipts/mod.rs similarity index 83% rename from crates/libs/sbx/mgr/server/src/auto_lifecycle_delete_receipts/mod.rs rename to crates/libs/sbx/mgr/store/src/auto_lifecycle_delete_receipts/mod.rs index 392aeeeb6..9d58a8b13 100644 --- a/crates/libs/sbx/mgr/server/src/auto_lifecycle_delete_receipts/mod.rs +++ b/crates/libs/sbx/mgr/store/src/auto_lifecycle_delete_receipts/mod.rs @@ -24,7 +24,7 @@ impl RouteIdentityProvenance { } } - pub(crate) fn for_activation_identity(identity: &SpaceApiRouteIdentity) -> Self { + pub fn for_activation_identity(identity: &SpaceApiRouteIdentity) -> Self { if identity.effective_substrate_kind() == Some(NodeManagedBy::Kubernetes) && identity.pod_uid.is_some() && identity.activation_attempt_id.is_some() @@ -43,7 +43,7 @@ impl RouteIdentityProvenance { /// `incoming` and the latter from `current` prevents both promotion of a /// locally synthesized attempt and provenance that describes a different /// tuple from the one persisted by the merge. - pub(crate) fn for_authoritative_adoption( + pub fn for_authoritative_adoption( current: &SpaceApiRouteIdentity, incoming: &SpaceApiRouteIdentity, ) -> Self { @@ -82,7 +82,7 @@ impl FromStr for RouteIdentityProvenance { } #[derive(Clone, Copy, Debug, Eq, PartialEq)] -pub(crate) enum AutoLifecycleDeleteBlockReason { +pub enum AutoLifecycleDeleteBlockReason { NodeRoleUnavailable, NodeRoleNotWorker, PartialCurrentRoute, @@ -95,7 +95,7 @@ pub(crate) enum AutoLifecycleDeleteBlockReason { } impl AutoLifecycleDeleteBlockReason { - pub(crate) const ALL: [Self; 9] = [ + pub const ALL: [Self; 9] = [ Self::NodeRoleUnavailable, Self::NodeRoleNotWorker, Self::PartialCurrentRoute, @@ -107,7 +107,7 @@ impl AutoLifecycleDeleteBlockReason { Self::UnfencedRoute, ]; - pub(crate) const fn as_str(self) -> &'static str { + pub const fn as_str(self) -> &'static str { match self { Self::NodeRoleUnavailable => "node_role_unavailable", Self::NodeRoleNotWorker => "node_role_not_worker", @@ -121,7 +121,7 @@ impl AutoLifecycleDeleteBlockReason { } } - pub(crate) const fn metric_reason( + pub const fn metric_reason( self, ) -> pplx_space_metrics::sandbox::worker::AutoLifecycleDeleteAdmissionReason { use pplx_space_metrics::sandbox::worker::AutoLifecycleDeleteAdmissionReason as Reason; @@ -179,7 +179,7 @@ impl FromStr for AutoLifecycleDeleteBlockReason { #[derive(Debug, thiserror::Error)] #[error("unknown auto-lifecycle delete block reason {0:?}")] -pub(crate) struct AutoLifecycleDeleteBlockReasonParseError(String); +pub struct AutoLifecycleDeleteBlockReasonParseError(String); #[derive(Clone, Copy, Debug, Eq, PartialEq)] pub(crate) enum AutoLifecycleDeleteOutboxState { @@ -220,14 +220,14 @@ pub(crate) struct AutoLifecycleDeleteOutboxStateParseError(String); #[derive(Clone, Copy, Debug, Eq, PartialEq)] pub struct RouteEvidence { - pub(crate) execution_generation: ExecutionGeneration, - pub(crate) activation_attempt_id: SandboxActivationAttemptId, - pub(crate) substrate: NodeManagedBy, - pub(crate) provenance: RouteIdentityProvenance, + pub execution_generation: ExecutionGeneration, + pub activation_attempt_id: SandboxActivationAttemptId, + pub substrate: NodeManagedBy, + pub provenance: RouteIdentityProvenance, } impl RouteEvidence { - pub(crate) fn from_identity( + pub fn from_identity( identity: &SpaceApiRouteIdentity, provenance: RouteIdentityProvenance, ) -> Option { @@ -241,38 +241,38 @@ impl RouteEvidence { } #[derive(Clone, Copy, Debug, Eq, PartialEq)] -pub(crate) struct AutoLifecycleDeleteExpectation { - pub(crate) policy_fingerprint: AutoLifecyclePolicyFingerprint, - pub(crate) authority: pplx_space_common_core::types::PolicyAuthority, +pub struct AutoLifecycleDeleteExpectation { + pub policy_fingerprint: AutoLifecyclePolicyFingerprint, + pub authority: pplx_space_common_core::types::PolicyAuthority, } #[derive(Clone, Copy, Debug, Eq, PartialEq)] -pub(crate) struct AutoLifecycleDeleteReceipt { - pub(crate) receipt_id: AutoLifecycleDeleteReceiptId, - pub(crate) sandbox_id: SandboxId, - pub(crate) execution_generation: ExecutionGeneration, - pub(crate) route_activation_id: SandboxActivationAttemptId, - pub(crate) policy_fingerprint: AutoLifecyclePolicyFingerprint, +pub struct AutoLifecycleDeleteReceipt { + pub receipt_id: AutoLifecycleDeleteReceiptId, + pub sandbox_id: SandboxId, + pub execution_generation: ExecutionGeneration, + pub route_activation_id: SandboxActivationAttemptId, + pub policy_fingerprint: AutoLifecyclePolicyFingerprint, } #[derive(Clone, Copy, Debug, Default, Eq, PartialEq)] -pub(crate) struct AutoLifecycleDeleteOutboxStats { - pub(crate) pending_count: u64, - pub(crate) completed_count: u64, - pub(crate) max_attempt_count: u64, - pub(crate) oldest_pending_age_secs: u64, - pub(crate) oldest_completed_age_secs: u64, +pub struct AutoLifecycleDeleteOutboxStats { + pub pending_count: u64, + pub completed_count: u64, + pub max_attempt_count: u64, + pub oldest_pending_age_secs: u64, + pub oldest_completed_age_secs: u64, } #[derive(Clone, Copy, Debug, Eq, PartialEq)] -pub(crate) struct AutoLifecycleDeleteBlockedStats { - pub(crate) reason: AutoLifecycleDeleteBlockReason, - pub(crate) count: u64, - pub(crate) oldest_age_secs: u64, +pub struct AutoLifecycleDeleteBlockedStats { + pub reason: AutoLifecycleDeleteBlockReason, + pub count: u64, + pub oldest_age_secs: u64, } #[derive(Clone, Copy, Debug, Eq, PartialEq)] -pub(crate) enum AutoLifecycleDeleteAdmission { +pub enum AutoLifecycleDeleteAdmission { Ordinary(AutoLifecycleDeleteOrdinaryReason), Receipt(RouteEvidence), Blocked { @@ -282,20 +282,20 @@ pub(crate) enum AutoLifecycleDeleteAdmission { } #[derive(Clone, Copy, Debug, Eq, PartialEq)] -pub(crate) enum AutoLifecycleDeleteOrdinaryReason { +pub enum AutoLifecycleDeleteOrdinaryReason { LocalAuthority, DirectUnsupported, } impl AutoLifecycleDeleteOrdinaryReason { - pub(crate) const fn as_str(self) -> &'static str { + pub const fn as_str(self) -> &'static str { match self { Self::LocalAuthority => "local_authority", Self::DirectUnsupported => "direct_unsupported", } } - pub(crate) const fn metric_reason( + pub const fn metric_reason( self, ) -> pplx_space_metrics::sandbox::worker::AutoLifecycleDeleteAdmissionReason { use pplx_space_metrics::sandbox::worker::AutoLifecycleDeleteAdmissionReason as Reason; diff --git a/crates/libs/sbx/mgr/server/src/auto_lifecycle_delete_receipts/tests.rs b/crates/libs/sbx/mgr/store/src/auto_lifecycle_delete_receipts/tests.rs similarity index 100% rename from crates/libs/sbx/mgr/server/src/auto_lifecycle_delete_receipts/tests.rs rename to crates/libs/sbx/mgr/store/src/auto_lifecycle_delete_receipts/tests.rs diff --git a/crates/libs/sbx/mgr/server/src/db.rs b/crates/libs/sbx/mgr/store/src/db.rs similarity index 85% rename from crates/libs/sbx/mgr/server/src/db.rs rename to crates/libs/sbx/mgr/store/src/db.rs index d9edf661f..9e76b9074 100644 --- a/crates/libs/sbx/mgr/server/src/db.rs +++ b/crates/libs/sbx/mgr/store/src/db.rs @@ -33,23 +33,24 @@ use pplx_space_common_core::sql; use rusqlite::{OptionalExtension as _, params}; mod handle; -pub(crate) use handle::OptionalExtension; +pub use handle::OptionalExtension; pub use handle::{Connection, ConnectionGuard, Error, Result, SharedConnection, Transaction}; -pub(crate) mod auto_lifecycle_delete_receipts; -pub(crate) mod cleanup_jobs; -pub(crate) mod cleanup_target_scan; -pub(crate) mod eviction_scan; -pub(crate) mod guest_health_scan; -pub(crate) mod inventory; -pub(crate) mod inventory_read; -pub(crate) mod local_artifact_leases; -pub(crate) mod quarantine_history; -pub(crate) mod route_activation_claims; -pub(crate) mod snapshot_gc; -pub(crate) mod snapshot_upload_jobs; -pub(crate) mod timestamps; -pub(crate) mod transfer; +pub mod auto_lifecycle_delete_receipts; +pub mod cleanup_jobs; +pub mod cleanup_target_scan; +pub mod eviction_scan; +pub mod guest_health_scan; +pub mod inventory; +pub mod inventory_read; +pub mod local_artifact_leases; +pub mod migration; +pub mod quarantine_history; +pub mod route_activation_claims; +pub mod snapshot_gc; +pub mod snapshot_upload_jobs; +pub mod timestamps; +pub mod transfer; use crate::endpoint_proof::SpacedEndpointProof; use crate::restore_operation::{RemoteRestoreOperation, RestoreOrigin}; @@ -62,15 +63,15 @@ mod quarantine_scan; mod sandbox_row; mod teardown_jobs; use auto_lifecycle_delete_receipts::discard_pending_receipt_for_execution_advance; -pub(crate) use teardown_jobs::{ +pub use teardown_jobs::{ TeardownSuspendFailure, defer_teardown_suspend_retry, fail_teardown_suspend_backup_operation, fail_teardown_suspend_operation, reconcile_terminal_teardown_suspend_jobs, teardown_suspend_retry_admissible, teardown_suspend_retry_due, }; -#[cfg(test)] +#[cfg(any(test, feature = "test-support"))] mod test_schema_image_cache; -pub(crate) use quarantine_scan::PeriodicQuarantineRecoveryScan; -pub(crate) use sandbox_row::SandboxListRow; +pub use quarantine_scan::PeriodicQuarantineRecoveryScan; +pub use sandbox_row::SandboxListRow; pub use sandbox_row::SandboxRow; use sandbox_row::{row_to_list_sandbox, row_to_sandbox}; @@ -235,12 +236,13 @@ pub async fn migrate() -> Result<(), OpenError> { /// [`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}; + use pplx_space_sbxmgr_migrations::COMMON_SCHEMA_MIGRATOR; + let target = migration::target(); Ok(pplx_space_common_svc::sql::sqlite::migrate_embedded( - "sandbox-manager", + target.name, db_path, pplx_space_common_svc::sql::sqlite::EmbeddedMigrators { - service: &SANDBOX_MANAGER_MIGRATOR, + service: target.service, common: Some(&COMMON_SCHEMA_MIGRATOR), }, ) @@ -258,7 +260,7 @@ pub fn open() -> Result { Ok(Connection::from_raw(conn)) } -pub(crate) fn describe_error(error: &Error) -> String { +pub fn describe_error(error: &Error) -> String { describe_raw_error(error.raw()) } @@ -282,7 +284,7 @@ pub(crate) fn now() -> Result { now_epoch_i64().map(|seconds| seconds.to_string()) } -pub(crate) fn now_epoch_i64() -> Result { +pub fn now_epoch_i64() -> Result { persisted_epoch_seconds(jiff::Timestamp::now()) } @@ -366,7 +368,7 @@ fn emit_auto_lifecycle_delete_receipt_superseded( ); } -pub(crate) fn emit_auto_lifecycle_delete_receipt_superseded_if_present( +pub fn emit_auto_lifecycle_delete_receipt_superseded_if_present( sandbox_id: SandboxId, supersession: Option, ) { @@ -478,8 +480,9 @@ fn is_row_decode_error(error: &rusqlite::Error) -> bool { ) } -#[cfg(test)] -pub(crate) fn create_test_schema(conn: &mut Connection) { +#[cfg(any(test, feature = "test-support"))] +#[allow(clippy::unwrap_used)] +pub fn create_test_schema(conn: &mut Connection) { use pplx_space_sbxmgr_migrations::{COMMON_SCHEMA_MIGRATOR, SANDBOX_MANAGER_MIGRATOR}; for migration in COMMON_SCHEMA_MIGRATOR .iter() @@ -505,7 +508,8 @@ pub(crate) fn create_test_schema(conn: &mut Connection) { /// into it — so the first process of a run to build the image leaves a copy /// beside the executable, and later processes of the same run and build read /// that copy (see the `test_schema_image_cache` module). -#[cfg(test)] +#[cfg(any(test, feature = "test-support"))] +#[allow(clippy::unwrap_used)] fn test_schema_image() -> &'static [u8] { static IMAGE: std::sync::OnceLock> = std::sync::OnceLock::new(); IMAGE.get_or_init(|| { @@ -528,8 +532,9 @@ fn test_schema_image() -> &'static [u8] { /// For tests that need durability across a close and reopen: the file left /// behind is the one `create_test_schema` would have produced on that path, so /// the connections the test then opens are ordinary file connections. -#[cfg(test)] -pub(crate) fn create_test_schema_on_disk(db_path: &std::path::Path) { +#[cfg(any(test, feature = "test-support"))] +#[allow(clippy::unwrap_used)] +pub fn create_test_schema_on_disk(db_path: &std::path::Path) { std::fs::write(db_path, test_schema_image()).unwrap(); } @@ -541,8 +546,9 @@ pub(crate) fn create_test_schema_on_disk(db_path: &std::path::Path) { /// connections affordable. Nothing else is shared: each call owns its pages, /// and the per-connection settings are the ones `Connection::open_in_memory` /// installs, exactly as a replay would leave them. -#[cfg(test)] -pub(crate) fn test_conn() -> Connection { +#[cfg(any(test, feature = "test-support"))] +#[allow(clippy::unwrap_used)] +pub fn test_conn() -> Connection { let image = test_schema_image(); let mut conn = rusqlite::Connection::open_in_memory().unwrap(); conn.deserialize_read_exact(rusqlite::MAIN_DB, image, image.len(), false) @@ -557,8 +563,9 @@ pub(crate) fn test_conn() -> Connection { /// from a pinned migration, so a later migration that replaces them is restored /// as it actually exists. A rename is a loud failure here, not a silent /// reinstatement of a superseded vocabulary in one test's database. -#[cfg(test)] -pub(crate) fn seed_historical_lifecycle_rows( +#[cfg(any(test, feature = "test-support"))] +#[allow(clippy::unwrap_used)] +pub fn seed_historical_lifecycle_rows( conn: &Connection, seed: impl FnOnce(&Connection) -> T, ) -> T { @@ -593,13 +600,71 @@ pub(crate) fn seed_historical_lifecycle_rows( /// 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(any(test, feature = "test-support"))] +#[allow(clippy::unwrap_used)] +pub mod fixture { + use std::collections::BTreeMap; + use std::str::FromStr; + + use pplx_space_common_core::types::{ + AutoLifecyclePolicy, SandboxId, SandboxOwner, SandboxRuntimeLabel, + }; + + use crate::state::SandboxState; + + pub use rusqlite::trace::{TraceEvent, TraceEventCodes}; + pub use rusqlite::types::{Type, Value}; + pub use rusqlite::{StatementStatus, params}; + + pub fn insert_test_sandbox(conn: &crate::db::Connection, id: &str, state: SandboxState) { + insert_test_sandbox_with_policy(conn, id, state, &AutoLifecyclePolicy::disabled()); + } + + pub fn insert_test_sandbox_with_policy( + conn: &crate::db::Connection, + id: &str, + state: SandboxState, + auto_lifecycle: &AutoLifecyclePolicy, + ) { + let sandbox_id = SandboxId::from_str(id).unwrap(); + let uuid8 = SandboxRuntimeLabel::from_sandbox_id(sandbox_id); + let user = SandboxOwner::parse("user").unwrap(); + crate::db::insert_sandbox( + conn, + crate::db::InsertSandboxParams { + id, + uuid8: &uuid8, + state, + template: "seed", + template_hash: Some("hash"), + user: &user, + runtime: "fc", + snapshots_enabled: false, + rootfs_size_mb: None, + disk_io_limits: Default::default(), + pool_template: None, + volume_encryption: false, + process_env: &BTreeMap::new(), + auto_lifecycle, + network_policy_override: None, + network_policy_generation: 0, + network_policy_authority: crate::db::PolicyAuthority::Cluster, + auto_lifecycle_authority: crate::db::PolicyAuthority::Cluster, + }, + ) + .unwrap(); + } } +#[cfg(any(test, feature = "test-support"))] +#[allow( + clippy::unwrap_used, + clippy::expect_used, + clippy::indexing_slicing, + clippy::panic +)] +pub mod test_fixtures; + #[cfg(test)] pub(crate) mod tests; diff --git a/crates/libs/sbx/mgr/server/src/db/auto_lifecycle.rs b/crates/libs/sbx/mgr/store/src/db/auto_lifecycle.rs similarity index 97% rename from crates/libs/sbx/mgr/server/src/db/auto_lifecycle.rs rename to crates/libs/sbx/mgr/store/src/db/auto_lifecycle.rs index ba04f9292..0d466cfdf 100644 --- a/crates/libs/sbx/mgr/server/src/db/auto_lifecycle.rs +++ b/crates/libs/sbx/mgr/store/src/db/auto_lifecycle.rs @@ -6,7 +6,7 @@ use super::*; /// past unchanged failures and rows without due work; the deadline lane reserves prompt /// admission for newly due work. Neither lane counts the full backlog. #[derive(Clone, Copy)] -pub(crate) struct AutoLifecycleScan { +pub struct AutoLifecycleScan { pub(super) after: i64, pub(super) fresh_turn: bool, } @@ -22,7 +22,7 @@ impl Default for AutoLifecycleScan { // Preserve the selected database key: parsing a UUID validates its identity, // but formatting it again need not reproduce the stored key. -pub(crate) struct DueAutoLifecycleSandbox { +pub struct DueAutoLifecycleSandbox { pub storage_id: String, pub sandbox_id: SandboxId, } @@ -50,11 +50,7 @@ const AUTO_LIFECYCLE_NEVER_ACTIONABLE_STATES_SQL: &str = "('deleting')"; /// state JSON (which still carries the true deadline) stay as they were. Any /// later state write rewrites the column from the JSON, so the deferral is /// self-healing and never outlives the next real transition. -pub(crate) fn defer_auto_lifecycle_deadline( - conn: &Connection, - id: &str, - until_secs: u64, -) -> Result<()> { +pub fn defer_auto_lifecycle_deadline(conn: &Connection, id: &str, until_secs: u64) -> Result<()> { conn.execute( "UPDATE sandboxes SET next_auto_event_at = ?1 WHERE id = ?2", params![SqliteU64(until_secs), id], @@ -74,7 +70,7 @@ pub(crate) fn defer_auto_lifecycle_deadline( /// eviction's quiescence clock on exactly that retained row). `Delete` /// follows the state machine's force-delete contract and acts wherever /// `Deleting` is reachable. `None` is a no-op that may always be recorded. -pub(crate) fn auto_lifecycle_action_can_act( +pub fn auto_lifecycle_action_can_act( action: pplx_space_common_core::types::AutoLifecycleIdleAction, state: SandboxState, ) -> bool { @@ -92,13 +88,13 @@ pub(crate) fn auto_lifecycle_action_can_act( /// failed cross-node restore retains — every write bumps `updated_at`, the /// quiescence clock the cluster-deleted eviction of that row waits on, /// and the in-flight transition owns the row anyway. -pub(crate) fn auto_lifecycle_state_is_settled(state: SandboxState) -> bool { +pub fn auto_lifecycle_state_is_settled(state: SandboxState) -> bool { // The state machine owns the transient set; a state added there is // non-settled here without a second list to update. !state.is_transient() } -pub(crate) fn due_auto_lifecycle_batch( +pub fn due_auto_lifecycle_batch( conn: &Connection, now_secs: u64, active: &[SandboxId], diff --git a/crates/libs/sbx/mgr/server/src/db/auto_lifecycle_delete_receipts.rs b/crates/libs/sbx/mgr/store/src/db/auto_lifecycle_delete_receipts.rs similarity index 97% rename from crates/libs/sbx/mgr/server/src/db/auto_lifecycle_delete_receipts.rs rename to crates/libs/sbx/mgr/store/src/db/auto_lifecycle_delete_receipts.rs index 7d62a839c..d0f563410 100644 --- a/crates/libs/sbx/mgr/server/src/db/auto_lifecycle_delete_receipts.rs +++ b/crates/libs/sbx/mgr/store/src/db/auto_lifecycle_delete_receipts.rs @@ -16,8 +16,8 @@ use crate::auto_lifecycle_delete_receipts::{ }; #[derive(Clone, Copy, Debug, Eq, PartialEq)] -pub(crate) struct RouteIdentityAdoptionMetadata { - pub(crate) current_provenance: Option, +pub struct RouteIdentityAdoptionMetadata { + pub current_provenance: Option, retained_evidence: Option, } @@ -29,13 +29,13 @@ pub(crate) struct SupersededPendingReceipt { #[derive(Clone, Copy, Debug, Eq, PartialEq)] #[must_use = "receipt supersession must be emitted after the transaction commits"] -pub(crate) struct PendingReceiptSupersession { +pub struct PendingReceiptSupersession { pub(crate) receipt: SupersededPendingReceipt, pub(crate) superseding_generation: ExecutionGeneration, } impl RouteIdentityAdoptionMetadata { - pub(crate) fn is_current( + pub fn is_current( self, identity: &pplx_space_common_core::types::SpaceApiRouteIdentity, provenance: RouteIdentityProvenance, @@ -69,11 +69,11 @@ impl PendingReceiptOwnsDeletion { #[derive(Debug, thiserror::Error)] #[error("pending auto-lifecycle delete receipt owns the sandbox execution")] -pub(crate) struct PendingReceiptOwnsExecution; +pub struct PendingReceiptOwnsExecution; #[derive(Debug, thiserror::Error)] #[error("sandbox lifecycle or cleanup intent owns the sandbox execution")] -pub(crate) struct LifecycleOwnsExecution; +pub struct LifecycleOwnsExecution; pub(crate) fn execution_write_miss_error( conn: &Connection, @@ -103,7 +103,7 @@ pub(crate) fn execution_write_miss_error( } } -pub(crate) fn is_pending_receipt_execution_fence(error: &Error) -> bool { +pub fn is_pending_receipt_execution_fence(error: &Error) -> bool { matches!( error.raw(), rusqlite::Error::ToSqlConversionFailure(source) @@ -111,7 +111,7 @@ pub(crate) fn is_pending_receipt_execution_fence(error: &Error) -> bool { ) } -pub(crate) fn is_lifecycle_execution_fence(error: &Error) -> bool { +pub fn is_lifecycle_execution_fence(error: &Error) -> bool { matches!( error.raw(), rusqlite::Error::ToSqlConversionFailure(source) @@ -132,12 +132,12 @@ pub(crate) fn map_pending_receipt_delete_error(error: Error) -> Error { #[derive(Debug, thiserror::Error)] #[error("auto-lifecycle delete receipt conflicts with existing evidence for sandbox {sandbox_id}")] -pub(crate) struct ReceiptEvidenceConflict { +pub struct ReceiptEvidenceConflict { sandbox_id: SandboxId, } #[derive(Debug, thiserror::Error)] -pub(crate) enum InsertPendingReceiptError { +pub enum InsertPendingReceiptError { #[error(transparent)] Database(#[from] Error), #[error(transparent)] @@ -145,7 +145,7 @@ pub(crate) enum InsertPendingReceiptError { } #[derive(Debug, thiserror::Error)] -pub(crate) enum CompletePendingReceiptError { +pub enum CompletePendingReceiptError { #[error(transparent)] Database(#[from] Error), #[error(transparent)] @@ -153,7 +153,7 @@ pub(crate) enum CompletePendingReceiptError { } #[derive(Debug, thiserror::Error)] -pub(crate) enum AutoLifecycleDeleteEvidenceError { +pub enum AutoLifecycleDeleteEvidenceError { #[error(transparent)] Database(#[from] Error), #[error("auto-lifecycle policy changed before delete admission")] @@ -173,7 +173,7 @@ struct PersistedRouteEvidence<'a> { provenance: &'a str, } -pub(crate) fn classify_and_record_admission( +pub fn classify_and_record_admission( conn: &Connection, sandbox_id: SandboxId, node_role: Option, @@ -544,7 +544,7 @@ pub(crate) fn classify_and_record_admission( })) } -pub(crate) fn record_retained_route_evidence( +pub fn record_retained_route_evidence( conn: &Connection, sandbox_id: SandboxId, evidence: RouteEvidence, @@ -594,7 +594,7 @@ pub(crate) fn record_retained_route_evidence( Ok(updated) } -pub(crate) fn route_identity_adoption_metadata( +pub fn route_identity_adoption_metadata( conn: &Connection, sandbox_id: SandboxId, ) -> crate::db::Result { @@ -717,7 +717,7 @@ pub(crate) fn discard_pending_receipt_for_execution_advance( /// writer: local cleanup must not dispatch after either the current route or /// its retained high-water mark has advanced to a different generation or /// same-generation activation attempt. -pub(crate) fn pending_receipt_matches_observed_execution( +pub fn pending_receipt_matches_observed_execution( conn: &Connection, sandbox_id: SandboxId, ) -> crate::db::Result { @@ -813,7 +813,7 @@ fn record_block( Ok(AutoLifecycleDeleteAdmission::Blocked { reason, changed }) } -pub(crate) fn record_receipt_conflict_retry( +pub fn record_receipt_conflict_retry( conn: &Connection, storage_id: &str, ) -> crate::db::Result { @@ -899,7 +899,7 @@ fn clear_block_by_storage_id(conn: &Connection, storage_id: &str) -> crate::db:: Ok(()) } -pub(crate) fn insert_pending_receipt( +pub fn insert_pending_receipt( conn: &Connection, receipt: AutoLifecycleDeleteReceipt, ) -> Result { @@ -992,7 +992,7 @@ pub(crate) fn complete_pending_receipt( Err(ReceiptEvidenceConflict { sandbox_id }.into()) } -pub(crate) fn list_completed_receipts( +pub fn list_completed_receipts( conn: &Connection, ) -> crate::db::Result> { let batch_size = i64::try_from(RECEIPT_BATCH_SIZE) @@ -1070,7 +1070,7 @@ pub(crate) fn list_completed_receipts( Ok(receipts) } -pub(crate) fn acknowledge_completed_receipts( +pub fn acknowledge_completed_receipts( conn: &Connection, receipt_ids: &[AutoLifecycleDeleteReceiptId], ) -> crate::db::Result> { @@ -1090,7 +1090,7 @@ pub(crate) fn acknowledge_completed_receipts( Ok(acknowledged) } -pub(crate) fn outbox_stats(conn: &Connection) -> crate::db::Result { +pub 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 @@ -1131,9 +1131,7 @@ pub(crate) fn outbox_stats(conn: &Connection) -> crate::db::Result crate::db::Result> { +pub fn blocked_stats(conn: &Connection) -> crate::db::Result> { let now = crate::db::now_epoch_i64()?; let mut statement = conn.prepare( "SELECT auto_lifecycle_delete_block_reason, @@ -1164,7 +1162,7 @@ pub(crate) fn blocked_stats( .map_err(crate::db::Error::from) } -pub(crate) fn pending_receipt_for_sandbox( +pub fn pending_receipt_for_sandbox( conn: &Connection, sandbox_id: SandboxId, ) -> crate::db::Result> { diff --git a/crates/libs/sbx/mgr/server/src/db/auto_lifecycle_delete_receipts/tests.rs b/crates/libs/sbx/mgr/store/src/db/auto_lifecycle_delete_receipts/tests.rs similarity index 100% rename from crates/libs/sbx/mgr/server/src/db/auto_lifecycle_delete_receipts/tests.rs rename to crates/libs/sbx/mgr/store/src/db/auto_lifecycle_delete_receipts/tests.rs diff --git a/crates/libs/sbx/mgr/server/src/db/cleanup_discovery_tests.rs b/crates/libs/sbx/mgr/store/src/db/cleanup_discovery_tests.rs similarity index 98% rename from crates/libs/sbx/mgr/server/src/db/cleanup_discovery_tests.rs rename to crates/libs/sbx/mgr/store/src/db/cleanup_discovery_tests.rs index b700123a9..6c17720e9 100644 --- a/crates/libs/sbx/mgr/server/src/db/cleanup_discovery_tests.rs +++ b/crates/libs/sbx/mgr/store/src/db/cleanup_discovery_tests.rs @@ -1,5 +1,5 @@ use super::*; -use crate::service::tests::service_tests::insert_test_sandbox; +use crate::db::fixture::insert_test_sandbox; #[test] fn recovery_inventory_retains_malformed_keys_without_hiding_siblings() { @@ -358,7 +358,7 @@ fn discovery_excludes_active_and_not_yet_due_work() { fn lifecycle_deadline_write_rejects_out_of_range_without_partial_update() { let conn = test_conn(); let id = SandboxId::new().to_string(); - crate::service::tests::service_tests::insert_test_sandbox(&conn, &id, SandboxState::Started); + crate::db::fixture::insert_test_sandbox(&conn, &id, SandboxState::Started); let mut state = AutoLifecycleRuntimeState { next_auto_event_at: Some(10), ..Default::default() diff --git a/crates/libs/sbx/mgr/server/src/db/cleanup_episodes.rs b/crates/libs/sbx/mgr/store/src/db/cleanup_episodes.rs similarity index 98% rename from crates/libs/sbx/mgr/server/src/db/cleanup_episodes.rs rename to crates/libs/sbx/mgr/store/src/db/cleanup_episodes.rs index 7838a162e..58e48a6f9 100644 --- a/crates/libs/sbx/mgr/server/src/db/cleanup_episodes.rs +++ b/crates/libs/sbx/mgr/store/src/db/cleanup_episodes.rs @@ -63,7 +63,7 @@ pub struct CleanupEpisodeSpec { } #[derive(Clone, Copy, Debug, Eq, PartialEq, serde::Serialize, serde::Deserialize)] -pub(crate) struct PreparedCloneRoot { +pub struct PreparedCloneRoot { pub device: u64, pub inode: u64, } @@ -72,7 +72,7 @@ pub(crate) struct PreparedCloneRoot { pub struct CleanupEpisode { /// Receipt owner; route authorization continues to use activation_attempt_id. pub operation_activation_attempt_id: Option, - pub(crate) prepared_clone_root: Option, + pub prepared_clone_root: Option, pub id: SandboxCleanupEpisodeId, pub completion: CleanupCompletion, pub keep_filesystem: bool, @@ -149,7 +149,7 @@ pub(super) fn insert_cleanup_episode( Ok(()) } -pub(crate) fn insert_sandbox_with_cleanup_episode_and_route_evidence( +pub fn insert_sandbox_with_cleanup_episode_and_route_evidence( conn: &Connection, params: InsertSandboxParams<'_>, episode: CleanupEpisodeSpec, @@ -187,7 +187,7 @@ pub(crate) fn insert_sandbox_with_cleanup_episode_and_route_evidence( } /// Charge before connecting or dispatching so crashes cannot reset the budget. -pub(crate) fn charge_prepared_clone_disposal_attempt( +pub fn charge_prepared_clone_disposal_attempt( conn: &Connection, id: &str, cleanup_id: SandboxCleanupEpisodeId, @@ -204,7 +204,7 @@ pub(crate) fn charge_prepared_clone_disposal_attempt( /// An explicit delete retries a held disposal with its original identity. /// It never grants ownership of the replacement now occupying the pathname. -pub(crate) fn rearm_prepared_clone_disposal( +pub fn rearm_prepared_clone_disposal( conn: &Connection, id: &str, cleanup_id: SandboxCleanupEpisodeId, @@ -230,7 +230,7 @@ pub(crate) fn rearm_prepared_clone_disposal( /// Bind disposal authority before any clone RPC can mutate the prepared tree. /// A missing binding after a crash permits only absence, never pathname deletion. -pub(crate) fn bind_prepared_clone_root( +pub fn bind_prepared_clone_root( conn: &Connection, id: &str, cleanup_id: SandboxCleanupEpisodeId, @@ -248,7 +248,7 @@ pub(crate) fn bind_prepared_clone_root( /// Presence is authoritative even when a newer binary wrote an intent this /// reader cannot decode. Startup must not promote or recreate its resources. -pub(crate) fn has_cleanup_episode(conn: &Connection, id: &str) -> Result { +pub fn has_cleanup_episode(conn: &Connection, id: &str) -> Result { conn.query_row( "SELECT EXISTS(SELECT 1 FROM sandbox_cleanup_episodes WHERE sandbox_id = ?1)", [id], @@ -994,7 +994,7 @@ pub fn complete_cleanup_episode( complete_cleanup_episode_for_job(conn, sandbox_id, cleanup_id, None) } -pub(crate) fn complete_cleanup_episode_for_job( +pub fn complete_cleanup_episode_for_job( conn: &Connection, sandbox_id: &str, cleanup_id: SandboxCleanupEpisodeId, @@ -1084,7 +1084,7 @@ pub(crate) fn complete_cleanup_episode_for_job( /// Why ordinary cleanup may not erase a sandbox row and its filesystem. #[derive(Clone, Copy, Debug, Eq, PartialEq)] -pub(crate) enum SandboxEraseSafety { +pub enum SandboxEraseSafety { Safe, SuspendPublicationPending, RetainedFilesystemOnlyCopy, @@ -1096,7 +1096,7 @@ pub(crate) enum SandboxEraseSafety { /// publish. A terminal missing-artifact failure cannot publish, but its /// retained rootfs may be the only copy of the sandbox's latest work. The /// failure reason is the durable witness for that distinct safety condition. -pub(crate) fn sandbox_erase_safety( +pub fn sandbox_erase_safety( state: SandboxState, cleanup_completion: Option, failure_reason: Option<&str>, @@ -1121,7 +1121,7 @@ pub(crate) fn sandbox_erase_safety( /// row. Routine stop, delete, kill, suspend, and startup recovery use this /// path; activation compensation uses [`complete_cleanup_episode`] instead. #[derive(Debug, thiserror::Error)] -pub(crate) enum CompleteRowIntentCleanupError { +pub enum CompleteRowIntentCleanupError { #[error("row-deleting cleanup cannot complete as a resident state")] DeleteHasNoTargetState, #[error(transparent)] @@ -1137,7 +1137,7 @@ pub(crate) enum CompleteRowIntentCleanupError { } #[derive(Clone, Copy, Debug, Eq, PartialEq)] -pub(crate) enum CompleteRowIntentCleanupDisposition { +pub enum CompleteRowIntentCleanupDisposition { Applied, Quarantined, Superseded, @@ -1196,8 +1196,8 @@ fn emit_auto_lifecycle_delete_receipt_completion_superseded( ); } -#[cfg(test)] -pub(crate) fn complete_row_intent_cleanup( +#[cfg(any(test, feature = "test-support"))] +pub fn complete_row_intent_cleanup( conn: &Connection, sandbox_id: &str, completion: CleanupCompletion, @@ -1213,8 +1213,8 @@ pub(crate) fn complete_row_intent_cleanup( .map(|_| ()) } -#[cfg(test)] -pub(crate) fn complete_row_intent_cleanup_with_receipt( +#[cfg(any(test, feature = "test-support"))] +pub fn complete_row_intent_cleanup_with_receipt( conn: &Connection, sandbox_id: &str, completion: CleanupCompletion, @@ -1231,7 +1231,7 @@ pub(crate) fn complete_row_intent_cleanup_with_receipt( ) } -pub(crate) fn complete_row_intent_cleanup_for_job( +pub fn complete_row_intent_cleanup_for_job( conn: &Connection, sandbox_id: &str, completion: CleanupCompletion, @@ -1437,7 +1437,7 @@ pub fn quarantine_cleanup_episode( quarantine_cleanup_episode_for_job(conn, sandbox_id, cleanup_id, unsafe_evidence, None) } -pub(crate) fn quarantine_cleanup_episode_for_job( +pub fn quarantine_cleanup_episode_for_job( conn: &Connection, sandbox_id: &str, cleanup_id: SandboxCleanupEpisodeId, @@ -1746,7 +1746,7 @@ pub fn mark_cleanup_failed( Ok(changed != 0) } -#[cfg(test)] +#[cfg(any(test, feature = "test-support"))] pub fn clear_cleanup_intent(conn: &Connection, id: &str) -> Result<()> { conn.execute( "UPDATE sandboxes @@ -1761,13 +1761,13 @@ pub fn clear_cleanup_intent(conn: &Connection, id: &str) -> Result<()> { /// Identity and authorization fields needed to target a cleanup. Optional /// launch and snapshot metadata must not make stop/delete unavailable. -pub(crate) struct CleanupTarget { +pub struct CleanupTarget { pub uuid8: SandboxRuntimeLabel, pub template: String, pub user: SandboxOwner, } -pub(crate) fn cleanup_target(conn: &Connection, id: &str) -> Result> { +pub fn cleanup_target(conn: &Connection, id: &str) -> Result> { conn.query_row( sql!("SELECT uuid8, template, user FROM sandboxes WHERE id = ?1"), [id], @@ -1786,10 +1786,7 @@ pub(crate) fn cleanup_target(conn: &Connection, id: &str) -> Result Result> { +pub fn cleanup_runtime_label(conn: &Connection, id: &str) -> Result> { conn.query_row( sql!("SELECT uuid8 FROM sandboxes WHERE id = ?1"), [id], @@ -1802,7 +1799,7 @@ pub(crate) fn cleanup_runtime_label( .optional() } -pub(crate) fn cleanup_intent_current( +pub fn cleanup_intent_current( conn: &Connection, id: &str, completion: CleanupCompletion, @@ -1832,7 +1829,7 @@ pub(crate) fn cleanup_intent_current( /// Sandboxes with durable cleanup evidence that a live reconciliation pass may /// safely re-drive after acquiring the per-sandbox cleanup fence. -#[cfg(test)] +#[cfg(any(test, feature = "test-support"))] pub fn list_requested_cleanup_recovery_candidate_ids(conn: &Connection) -> Result> { let mut statement = conn.prepare( "SELECT sandbox.id @@ -1890,12 +1887,12 @@ pub fn list_requested_cleanup_recovery_candidate_ids(conn: &Connection) -> Resul /// Discover unscheduled obligations without decoding the entire local inventory. /// Limit the inventory range first, so a sparse backlog cannot turn one page /// into an inventory-sized scan. The cursor advances even when none qualify. -pub(crate) struct CleanupRecoveryPage { +pub struct CleanupRecoveryPage { pub ids: Vec, pub next_cursor: Option, } -pub(crate) fn cleanup_recovery_page( +pub fn cleanup_recovery_page( conn: &Connection, after: Option, limit: usize, diff --git a/crates/libs/sbx/mgr/server/src/db/cleanup_jobs.rs b/crates/libs/sbx/mgr/store/src/db/cleanup_jobs.rs similarity index 90% rename from crates/libs/sbx/mgr/server/src/db/cleanup_jobs.rs rename to crates/libs/sbx/mgr/store/src/db/cleanup_jobs.rs index 989bc3856..c6abe2837 100644 --- a/crates/libs/sbx/mgr/server/src/db/cleanup_jobs.rs +++ b/crates/libs/sbx/mgr/store/src/db/cleanup_jobs.rs @@ -4,11 +4,11 @@ use pplx_space_common_core::types::JobId; use super::*; use pplx_space_common_jobs::{JobPriority, JobStatus, db as jobs}; -pub(crate) const KIND: &str = "sandbox_cleanup"; +pub const KIND: &str = "sandbox_cleanup"; /// A terminal domain outcome is distinct from an attempt that did no work. #[derive(Clone, Debug, PartialEq, Eq)] -pub(crate) enum CleanupOutcome { +pub enum CleanupOutcome { Applied, AlreadyApplied, Superseded, @@ -17,7 +17,7 @@ pub(crate) enum CleanupOutcome { } impl CleanupOutcome { - pub(crate) fn job_status(&self) -> JobStatus { + pub fn job_status(&self) -> JobStatus { match self { Self::Applied | Self::AlreadyApplied => JobStatus::Completed, Self::Superseded => JobStatus::Cancelled, @@ -26,13 +26,13 @@ impl CleanupOutcome { } } -pub(crate) enum CleanupEligibility { +pub enum CleanupEligibility { Runnable, Finished(CleanupOutcome), } #[derive(serde::Serialize, serde::Deserialize)] -pub(crate) struct CleanupFailure { +pub struct CleanupFailure { pub code: i32, pub message: String, } @@ -40,11 +40,7 @@ pub(crate) struct CleanupFailure { /// Called inside the transaction which applies lifecycle completion. A failed /// job write rolls back lifecycle finalization too; recovery may repeat the /// idempotent host cleanup, but cannot mistake lost authority for success. -pub(crate) fn finish( - conn: &Connection, - id: &JobId, - outcome: &CleanupOutcome, -) -> crate::db::Result { +pub fn finish(conn: &Connection, id: &JobId, outcome: &CleanupOutcome) -> crate::db::Result { match outcome { CleanupOutcome::Applied | CleanupOutcome::AlreadyApplied => { Ok(jobs::complete_job(conn.raw(), id, 0)?) @@ -84,10 +80,7 @@ pub(crate) fn finish_in_transaction( /// Absence of authority does not prove completion. Only an execution-scoped /// delete receipt can establish prior application by a different binary. -pub(crate) fn eligibility( - conn: &Connection, - work: &CleanupWork, -) -> crate::db::Result { +pub fn eligibility(conn: &Connection, work: &CleanupWork) -> crate::db::Result { let id = work.sandbox_id.to_string(); let state = current_state_label(conn, &id)?; if state.is_none() @@ -125,7 +118,7 @@ pub(crate) fn eligibility( /// guest acknowledged but has not yet committed to its disk would be lost. #[derive(Clone, Copy, Debug, Default, PartialEq, Eq, serde::Serialize, serde::Deserialize)] #[serde(rename_all = "snake_case")] -pub(crate) enum GuestFlush { +pub enum GuestFlush { /// This request asks nothing; merged with pending work, that work's /// choice stands. Work persisted before this field existed reads as this, /// and does not flush. @@ -146,7 +139,7 @@ impl GuestFlush { /// pause flushes before it freezes the VM, and a write in progress then /// can complete after that flush, so cleanup resumes a paused VM to /// flush it. - pub(crate) fn for_superseded_state(self, state: Option) -> Self { + pub fn for_superseded_state(self, state: Option) -> Self { match (self, state) { ( Self::BeforeTermination, @@ -173,7 +166,7 @@ impl GuestFlush { } #[derive(Clone, Debug, PartialEq, Eq, serde::Serialize, serde::Deserialize)] -pub(crate) struct CleanupWork { +pub struct CleanupWork { pub version: u8, pub sandbox_id: SandboxId, pub uuid8: SandboxRuntimeLabel, @@ -201,7 +194,7 @@ impl CleanupWork { } #[derive(Debug, thiserror::Error)] -pub(crate) enum CleanupJobError { +pub enum CleanupJobError { #[error("cleanup scheduling database: {0}")] Database(#[from] Error), #[error("cleanup scheduling payload: {0}")] @@ -212,7 +205,7 @@ pub(crate) enum CleanupJobError { Missing, } -pub(crate) fn decode(payload: &str) -> std::result::Result { +pub fn decode(payload: &str) -> std::result::Result { let work: CleanupWork = serde_json::from_str(payload)?; if work.version != 1 { return Err(CleanupJobError::UnsupportedVersion(work.version)); @@ -222,7 +215,7 @@ pub(crate) fn decode(payload: &str) -> std::result::Result std::result::Result { @@ -280,7 +273,7 @@ pub(crate) fn persist( Ok(id) } -pub(crate) fn current(conn: &Connection, work: &CleanupWork) -> crate::db::Result { +pub 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); @@ -300,15 +293,12 @@ pub(crate) fn current(conn: &Connection, work: &CleanupWork) -> crate::db::Resul } /// Requeue every running cleanup job: none survives a restart. -pub(crate) fn reset_running(conn: &Connection) -> Result { +pub 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 { +pub fn reset_inactive_running(conn: &Connection, active_subjects: &[String]) -> Result { Ok(jobs::reset_inactive_running_kind( conn.raw(), KIND, @@ -316,25 +306,22 @@ pub(crate) fn reset_inactive_running( )?) } -pub(crate) fn get_job( - conn: &Connection, - job_id: &JobId, -) -> Result> { +pub 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 { +pub 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 { +pub 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( +pub fn retry_running( conn: &Connection, job_id: &JobId, error: &str, @@ -350,7 +337,7 @@ pub(crate) fn retry_running( /// 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( +pub fn due_job_ids( conn: &Connection, priority: JobPriority, active_subjects: &[String], diff --git a/crates/libs/sbx/mgr/server/src/db/cleanup_jobs/tests.rs b/crates/libs/sbx/mgr/store/src/db/cleanup_jobs/tests.rs similarity index 99% rename from crates/libs/sbx/mgr/server/src/db/cleanup_jobs/tests.rs rename to crates/libs/sbx/mgr/store/src/db/cleanup_jobs/tests.rs index fe02f7515..e414a32d8 100644 --- a/crates/libs/sbx/mgr/server/src/db/cleanup_jobs/tests.rs +++ b/crates/libs/sbx/mgr/store/src/db/cleanup_jobs/tests.rs @@ -182,7 +182,7 @@ fn recovery_cursor_crosses_empty_pages_without_skipping_obligations() { } else { SandboxState::Suspended }; - crate::service::tests::service_tests::insert_test_sandbox(&conn, id, state); + crate::db::fixture::insert_test_sandbox(&conn, id, state); } // A later sweep must revisit intent added behind the prior cursor. for obligations in [vec![15, 31, 39], vec![15, 20, 31, 39]] { diff --git a/crates/libs/sbx/mgr/server/src/db/cleanup_target_scan.rs b/crates/libs/sbx/mgr/store/src/db/cleanup_target_scan.rs similarity index 92% rename from crates/libs/sbx/mgr/server/src/db/cleanup_target_scan.rs rename to crates/libs/sbx/mgr/store/src/db/cleanup_target_scan.rs index d654cb6da..5e86cd5f8 100644 --- a/crates/libs/sbx/mgr/server/src/db/cleanup_target_scan.rs +++ b/crates/libs/sbx/mgr/store/src/db/cleanup_target_scan.rs @@ -8,26 +8,26 @@ use rusqlite::types::Type; use super::CleanupTarget; -pub(crate) const BATCH_SIZE: usize = 128; +pub const BATCH_SIZE: usize = 128; #[derive(Debug, Default)] -pub(crate) struct CleanupTargetScan { - next: Option, - through: Option, - complete: bool, +pub struct CleanupTargetScan { + pub next: Option, + pub through: Option, + pub complete: bool, } -pub(crate) struct CleanupTargetObservation { +pub struct CleanupTargetObservation { pub raw_id: String, pub metadata: Option, } impl CleanupTargetScan { - pub(crate) fn complete(&self) -> bool { + pub fn complete(&self) -> bool { self.complete } - pub(crate) fn next_batch( + pub fn next_batch( &mut self, conn: &Connection, ) -> Result, InventoryReadError> { diff --git a/crates/libs/sbx/mgr/server/src/db/cleanup_target_scan/tests.rs b/crates/libs/sbx/mgr/store/src/db/cleanup_target_scan/tests.rs similarity index 96% rename from crates/libs/sbx/mgr/server/src/db/cleanup_target_scan/tests.rs rename to crates/libs/sbx/mgr/store/src/db/cleanup_target_scan/tests.rs index 1918c504c..b13280b9f 100644 --- a/crates/libs/sbx/mgr/server/src/db/cleanup_target_scan/tests.rs +++ b/crates/libs/sbx/mgr/store/src/db/cleanup_target_scan/tests.rs @@ -1,5 +1,5 @@ use super::*; -use crate::service::tests::service_tests::insert_test_sandbox; +use crate::db::fixture::insert_test_sandbox; use crate::state::SandboxState; #[test] diff --git a/crates/libs/sbx/mgr/server/src/db/dirty_outbox_tests.rs b/crates/libs/sbx/mgr/store/src/db/dirty_outbox_tests.rs similarity index 100% rename from crates/libs/sbx/mgr/server/src/db/dirty_outbox_tests.rs rename to crates/libs/sbx/mgr/store/src/db/dirty_outbox_tests.rs diff --git a/crates/libs/sbx/mgr/server/src/db/discovery_indexes_tests.rs b/crates/libs/sbx/mgr/store/src/db/discovery_indexes_tests.rs similarity index 97% rename from crates/libs/sbx/mgr/server/src/db/discovery_indexes_tests.rs rename to crates/libs/sbx/mgr/store/src/db/discovery_indexes_tests.rs index 0576cb4c2..99a6c0435 100644 --- a/crates/libs/sbx/mgr/server/src/db/discovery_indexes_tests.rs +++ b/crates/libs/sbx/mgr/store/src/db/discovery_indexes_tests.rs @@ -1,5 +1,5 @@ use super::*; -use crate::service::tests::service_tests::insert_test_sandbox; +use crate::db::fixture::insert_test_sandbox; #[test] fn prefix_selection_ignores_unrelated_metadata_and_detects_ambiguity_before_decode() { diff --git a/crates/libs/sbx/mgr/server/src/db/eviction.rs b/crates/libs/sbx/mgr/store/src/db/eviction.rs similarity index 100% rename from crates/libs/sbx/mgr/server/src/db/eviction.rs rename to crates/libs/sbx/mgr/store/src/db/eviction.rs diff --git a/crates/libs/sbx/mgr/server/src/db/eviction_scan.rs b/crates/libs/sbx/mgr/store/src/db/eviction_scan.rs similarity index 95% rename from crates/libs/sbx/mgr/server/src/db/eviction_scan.rs rename to crates/libs/sbx/mgr/store/src/db/eviction_scan.rs index 1d5492861..a19b839bb 100644 --- a/crates/libs/sbx/mgr/server/src/db/eviction_scan.rs +++ b/crates/libs/sbx/mgr/store/src/db/eviction_scan.rs @@ -63,7 +63,7 @@ const IDLE_WITNESS_SQL: &str = "COALESCE( /// `(sandbox_id, idle witness)` of cold suspended sandboxes after the /// `(cursor_witness, cursor_sandbox_id)` keyset position, in witness then id /// order. -pub(crate) fn idle_suspended_candidates( +pub fn idle_suspended_candidates( conn: &Connection, cutoff: i64, cursor_witness: Option, @@ -114,7 +114,7 @@ pub(crate) fn idle_suspended_candidates( /// Ids of cold suspended sandboxes with neither a pinned artifact nor an /// upload job carrying a snapshot id, oldest witness first. -pub(crate) fn legacy_reupload_candidates( +pub fn legacy_reupload_candidates( conn: &Connection, idle_cutoff_epoch_secs: u64, limit: usize, @@ -150,7 +150,7 @@ pub(crate) fn legacy_reupload_candidates( Ok(rows) } -pub(crate) fn count_malformed_suspended_updated_at(conn: &Connection) -> Result { +pub fn count_malformed_suspended_updated_at(conn: &Connection) -> Result { let sql = format!( "SELECT COUNT(*) FROM sandboxes s WHERE s.state = 'suspended' @@ -164,7 +164,7 @@ pub(crate) fn count_malformed_suspended_updated_at(conn: &Connection) -> Result< /// Every local `sandboxes` row, whatever its state. node-ctl's full census /// publishes one entry per row, so this is the count that sizes it. -pub(crate) fn count_resident_rows(conn: &Connection) -> Result { +pub fn count_resident_rows(conn: &Connection) -> Result { conn.query_row("SELECT COUNT(*) FROM sandboxes", [], |row| { let count: i64 = row.get(0)?; usize::try_from(count).map_err(|_| rusqlite::Error::IntegralValueOutOfRange(0, count)) @@ -172,7 +172,7 @@ pub(crate) fn count_resident_rows(conn: &Connection) -> Result { } /// The sandbox's pinned artifact id and `updated_at`, `None` without a row. -pub(crate) fn artifact_pin_and_updated_at( +pub fn artifact_pin_and_updated_at( conn: &Connection, sandbox_id: &str, ) -> Result, EpochSeconds)>> { @@ -186,7 +186,7 @@ pub(crate) fn artifact_pin_and_updated_at( /// `(artifact_kind, local_artifact_id, backed up)` of the artifact `pin` /// names, only if `sandbox_id` owns it. -pub(crate) fn owned_artifact_evidence( +pub fn owned_artifact_evidence( conn: &Connection, pin: &str, sandbox_id: &str, @@ -203,7 +203,7 @@ pub(crate) fn owned_artifact_evidence( /// The payload `snapshot_id` and `created_at` of the sandbox's newest /// snapshot-upload job, whatever its status. -pub(crate) fn latest_upload_job( +pub fn latest_upload_job( conn: &Connection, sandbox_id: &str, ) -> Result, String)>> { diff --git a/crates/libs/sbx/mgr/server/src/db/guest_health_scan.rs b/crates/libs/sbx/mgr/store/src/db/guest_health_scan.rs similarity index 86% rename from crates/libs/sbx/mgr/server/src/db/guest_health_scan.rs rename to crates/libs/sbx/mgr/store/src/db/guest_health_scan.rs index bdcf1c696..b46030584 100644 --- a/crates/libs/sbx/mgr/server/src/db/guest_health_scan.rs +++ b/crates/libs/sbx/mgr/store/src/db/guest_health_scan.rs @@ -7,10 +7,10 @@ use rusqlite::types::Type; use pplx_space_common_core::sql; #[derive(Clone, Debug, PartialEq, Eq)] -pub(crate) struct SandboxGuestHealthRow { - pub(crate) id: String, - pub(crate) state: crate::state::SandboxState, - pub(crate) template_content_hash: Option, +pub struct SandboxGuestHealthRow { + pub id: String, + pub state: crate::state::SandboxState, + pub template_content_hash: Option, } #[derive(Clone, Debug, PartialEq, Eq, thiserror::Error)] @@ -19,7 +19,7 @@ struct SandboxStateParseError { value: String, } -pub(crate) fn list_guest_health_rows(conn: &Connection) -> Result> { +pub fn list_guest_health_rows(conn: &Connection) -> Result> { let mut stmt = conn.prepare(sql!("SELECT id, state, template_hash FROM sandboxes"))?; let rows = stmt.query_map([], |row| { let state_value = row.get::<_, String>(1)?; @@ -47,7 +47,7 @@ pub(crate) type StateLabelRead = Result>; /// The `state` label of each of `sandbox_ids` through one prepared /// statement, `None` for a sandbox without a row. Only preparing the /// statement fails the whole lookup; each id's read fails on its own. -pub(crate) fn state_labels_for_ids<'a>( +pub fn state_labels_for_ids<'a>( conn: &Connection, sandbox_ids: &'a [String], ) -> Result> { diff --git a/crates/libs/sbx/mgr/server/src/db/guest_health_scan/tests.rs b/crates/libs/sbx/mgr/store/src/db/guest_health_scan/tests.rs similarity index 93% rename from crates/libs/sbx/mgr/server/src/db/guest_health_scan/tests.rs rename to crates/libs/sbx/mgr/store/src/db/guest_health_scan/tests.rs index de5401770..25ed7e110 100644 --- a/crates/libs/sbx/mgr/server/src/db/guest_health_scan/tests.rs +++ b/crates/libs/sbx/mgr/store/src/db/guest_health_scan/tests.rs @@ -1,6 +1,6 @@ use pplx_space_common_core::types::SandboxId; -use crate::service::tests::service_tests::insert_test_sandbox; +use crate::db::fixture::insert_test_sandbox; use crate::state::SandboxState; use super::*; diff --git a/crates/libs/sbx/mgr/server/src/db/handle.rs b/crates/libs/sbx/mgr/store/src/db/handle.rs similarity index 83% rename from crates/libs/sbx/mgr/server/src/db/handle.rs rename to crates/libs/sbx/mgr/store/src/db/handle.rs index 8d049d1a1..d61ca1136 100644 --- a/crates/libs/sbx/mgr/server/src/db/handle.rs +++ b/crates/libs/sbx/mgr/store/src/db/handle.rs @@ -8,18 +8,18 @@ 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. +/// A method that returns or takes rusqlite values: public with the +/// `test-support` feature, for sbx-mgr's fixtures and tripwires outside the +/// store, 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)* + #[cfg(any(test, feature = "test-support"))] + pub fn $name $($rest)* $(#[$attr])* - #[cfg(not(test))] + #[cfg(not(any(test, feature = "test-support")))] pub(in crate::db) fn $name $($rest)* }; } @@ -73,14 +73,14 @@ impl Error { } /// SQLite reported the database busy. - #[cfg(test)] - pub(crate) fn is_database_busy(&self) -> bool { + #[cfg(any(test, feature = "test-support"))] + pub 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)> { + #[cfg(any(test, feature = "test-support"))] + pub fn conversion_failure(&self) -> Option<(usize, rusqlite::types::Type)> { match &self.0 { rusqlite::Error::FromSqlConversionFailure(column, kind, _) => Some((*column, *kind)), _ => None, @@ -98,14 +98,14 @@ impl Error { } } - #[cfg(test)] - pub(crate) fn into_raw(self) -> rusqlite::Error { + #[cfg(any(test, feature = "test-support"))] + pub fn into_raw(self) -> rusqlite::Error { self.0 } } /// [`rusqlite::OptionalExtension`] for the store's [`Result`]. -pub(crate) trait OptionalExtension { +pub trait OptionalExtension { /// `Ok(None)` when the statement matched no row. fn optional(self) -> Result>; } @@ -137,20 +137,21 @@ impl Connection { } /// An empty in-memory database, without the schema. - #[cfg(test)] - pub(crate) fn open_in_memory() -> Self { + #[cfg(any(test, feature = "test-support"))] + #[allow(clippy::expect_used)] + pub 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 { + #[cfg(any(test, feature = "test-support"))] + pub fn open_at(path: impl AsRef) -> Result { Ok(Self(rusqlite::Connection::open(path)?)) } - #[cfg(test)] - pub(crate) fn pragma_update( + #[cfg(any(test, feature = "test-support"))] + pub fn pragma_update( &self, schema: Option<&str>, name: &str, @@ -159,28 +160,23 @@ impl Connection { 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 + #[cfg(any(test, feature = "test-support"))] + pub 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<()> { + #[cfg(any(test, feature = "test-support"))] + pub 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( + #[cfg(any(test, feature = "test-support"))] + pub fn trace_v2( &self, mask: rusqlite::trace::TraceEventCodes, trace: Option)>, @@ -200,8 +196,8 @@ impl Connection { } } - #[cfg(test)] - pub(crate) fn execute_batch(&self, sql: &str) -> Result<()> { + #[cfg(any(test, feature = "test-support"))] + pub fn execute_batch(&self, sql: &str) -> Result<()> { Ok(self.0.execute_batch(sql)?) } @@ -227,7 +223,7 @@ impl Connection { } } - pub(crate) fn is_autocommit(&self) -> bool { + pub fn is_autocommit(&self) -> bool { self.0.is_autocommit() } } @@ -293,8 +289,8 @@ impl SharedConnection { } /// [`Mutex::try_lock`]. - #[cfg(test)] - pub(crate) fn try_lock(&self) -> std::sync::TryLockResult> { + #[cfg(any(test, feature = "test-support"))] + pub fn try_lock(&self) -> std::sync::TryLockResult> { use std::sync::TryLockError; match self.0.try_lock() { Ok(guard) => Ok(ConnectionGuard(guard)), diff --git a/crates/libs/sbx/mgr/server/src/db/inventory.rs b/crates/libs/sbx/mgr/store/src/db/inventory.rs similarity index 95% rename from crates/libs/sbx/mgr/server/src/db/inventory.rs rename to crates/libs/sbx/mgr/store/src/db/inventory.rs index 4dd712310..65f9b038e 100644 --- a/crates/libs/sbx/mgr/server/src/db/inventory.rs +++ b/crates/libs/sbx/mgr/store/src/db/inventory.rs @@ -10,7 +10,7 @@ pub(crate) const INVENTORY_BATCH_SIZE: usize = 128; /// Rowids are positions, not identities or mutation authority. Consumers must /// recheck each observed row under its operation-specific ownership fence. #[derive(Debug)] -pub(crate) struct SandboxInventoryScan { +pub struct SandboxInventoryScan { next: i64, through: Option, } @@ -25,11 +25,11 @@ impl Default for SandboxInventoryScan { } impl SandboxInventoryScan { - pub(crate) fn sweep_complete(&self) -> bool { + pub fn sweep_complete(&self) -> bool { self.through.is_none() } - pub(crate) fn next_batch(&mut self, conn: &Connection) -> Result> { + pub fn next_batch(&mut self, conn: &Connection) -> Result> { if self.through.is_none() { self.through = conn.query_row("SELECT MAX(rowid) FROM sandboxes", [], |row| row.get(0))?; @@ -81,7 +81,7 @@ mod tests; /// Periodic orphan-scope discovery, including live and malformed rows in its /// scan budget so retained evidence cannot permanently occupy the first page. #[derive(Debug)] -pub(crate) struct RestoreTransferScopeScan { +pub struct RestoreTransferScopeScan { next: i64, through: Option, } @@ -96,7 +96,7 @@ impl Default for RestoreTransferScopeScan { } impl RestoreTransferScopeScan { - pub(crate) fn next_batch( + pub fn next_batch( &mut self, conn: &Connection, ) -> Result> { diff --git a/crates/libs/sbx/mgr/server/src/db/inventory/tests.rs b/crates/libs/sbx/mgr/store/src/db/inventory/tests.rs similarity index 97% rename from crates/libs/sbx/mgr/server/src/db/inventory/tests.rs rename to crates/libs/sbx/mgr/store/src/db/inventory/tests.rs index 012f6636c..f6ee45a71 100644 --- a/crates/libs/sbx/mgr/server/src/db/inventory/tests.rs +++ b/crates/libs/sbx/mgr/store/src/db/inventory/tests.rs @@ -1,5 +1,5 @@ use super::*; -use crate::service::tests::service_tests::insert_test_sandbox; +use crate::db::fixture::insert_test_sandbox; use crate::state::SandboxState; use pplx_space_common_core::types::SandboxId; diff --git a/crates/libs/sbx/mgr/server/src/db/inventory_read.rs b/crates/libs/sbx/mgr/store/src/db/inventory_read.rs similarity index 96% rename from crates/libs/sbx/mgr/server/src/db/inventory_read.rs rename to crates/libs/sbx/mgr/store/src/db/inventory_read.rs index 27594aa22..2afbe2dc1 100644 --- a/crates/libs/sbx/mgr/server/src/db/inventory_read.rs +++ b/crates/libs/sbx/mgr/store/src/db/inventory_read.rs @@ -9,7 +9,7 @@ use pplx_space_common_core::inventory::{ use rusqlite::{Row, types::ValueRef}; #[derive(Debug, thiserror::Error)] -pub(crate) enum InventoryReadError { +pub enum InventoryReadError { #[error(transparent)] Database(#[from] Error), #[error(transparent)] @@ -25,7 +25,7 @@ impl From for InventoryReadError { } impl InventoryReadError { - pub(crate) fn into_status(self) -> tonic::Status { + pub fn into_status(self) -> tonic::Status { match self { Self::Limit(error) => tonic::Status::resource_exhausted(error.to_string()), Self::Database(error) => tonic::Status::internal(format!("read inventory: {error}")), @@ -96,7 +96,7 @@ impl UnaryListUsage { const LIST_COLUMNS: &str = "id, state, template, template_hash, user, runtime, created_at, pool_template"; -pub(crate) fn list( +pub fn list( conn: &Connection, rpc: &'static str, ) -> Result, InventoryReadError> { @@ -116,7 +116,7 @@ pub(crate) fn list( /// One scanned List row. An undecodable row keeps its identity so the caller /// can degrade it rather than hide it. -pub(crate) enum ListPageRow { +pub enum ListPageRow { Decoded(super::SandboxListRow), Undecodable { id: String, @@ -126,7 +126,7 @@ pub(crate) enum ListPageRow { } impl ListPageRow { - pub(crate) fn id(&self) -> &str { + pub fn id(&self) -> &str { match self { Self::Decoded(row) => &row.id, Self::Undecodable { id, .. } => id, @@ -134,7 +134,7 @@ impl ListPageRow { } } -pub(crate) struct ListPage { +pub struct ListPage { pub rows: Vec, /// Rows remain after the last scanned row. pub more: bool, @@ -144,7 +144,7 @@ pub(crate) struct ListPage { /// early rather than exceed the raw budget; only a single row over the budget /// is an error. Keying on the immutable primary key keeps every row present for /// the whole scan on exactly one page. -pub(crate) fn list_page( +pub fn list_page( conn: &Connection, after: &str, limit: usize, @@ -182,7 +182,7 @@ pub(crate) fn list_page( }) } -pub(crate) fn get( +pub fn get( conn: &Connection, id: &str, budget: &mut InventoryBudget, @@ -197,7 +197,7 @@ pub(crate) fn get( .transpose() } -pub(crate) fn lookup( +pub fn lookup( conn: &Connection, selector: &str, ) -> Result, InventoryReadError> { @@ -232,7 +232,7 @@ pub(crate) fn lookup( /// Check auxiliary State fields before the existing typed readers allocate them. /// The caller retains the same SQLite mutex across preflight and those reads. -pub(crate) fn check_state_metadata( +pub fn check_state_metadata( conn: &Connection, ids: &[String], budget: &mut InventoryBudget, diff --git a/crates/libs/sbx/mgr/server/src/db/inventory_read/tests.rs b/crates/libs/sbx/mgr/store/src/db/inventory_read/tests.rs similarity index 94% rename from crates/libs/sbx/mgr/server/src/db/inventory_read/tests.rs rename to crates/libs/sbx/mgr/store/src/db/inventory_read/tests.rs index a43280a36..c4a76ba4f 100644 --- a/crates/libs/sbx/mgr/server/src/db/inventory_read/tests.rs +++ b/crates/libs/sbx/mgr/store/src/db/inventory_read/tests.rs @@ -4,7 +4,7 @@ use super::*; #[test] fn state_metadata_batches_share_the_callers_budget() { - use crate::service::tests::service_tests::insert_test_sandbox; + use crate::db::fixture::insert_test_sandbox; let conn = crate::db::test_conn(); let mut budget = InventoryBudget::new(1, MAX_RAW_BYTES); for index in 0..2 { @@ -36,7 +36,7 @@ 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; + use crate::db::fixture::insert_test_sandbox; let conn = crate::db::test_conn(); let tx = crate::db::Transaction::deferred(&conn).unwrap(); for _ in 0..=MAX_ITEMS { @@ -52,7 +52,7 @@ fn unary_list_serves_the_complete_inventory_past_the_item_budget() { #[test] fn unary_list_serves_the_complete_inventory_past_the_raw_budget() { - use crate::service::tests::service_tests::insert_test_sandbox; + use crate::db::fixture::insert_test_sandbox; let conn = crate::db::test_conn(); let template = "t".repeat(MAX_RAW_BYTES / 2); for _ in 0..3 { @@ -68,7 +68,7 @@ fn unary_list_serves_the_complete_inventory_past_the_raw_budget() { } fn insert_listed(conn: &Connection, count: usize) -> Vec { - use crate::service::tests::service_tests::insert_test_sandbox; + use crate::db::fixture::insert_test_sandbox; let mut ids = (0..count) .map(|_| { let id = SandboxId::new().to_string(); diff --git a/crates/libs/sbx/mgr/server/src/db/lifecycle_validation_tests.rs b/crates/libs/sbx/mgr/store/src/db/lifecycle_validation_tests.rs similarity index 100% rename from crates/libs/sbx/mgr/server/src/db/lifecycle_validation_tests.rs rename to crates/libs/sbx/mgr/store/src/db/lifecycle_validation_tests.rs diff --git a/crates/libs/sbx/mgr/server/src/db/local_artifact_leases.rs b/crates/libs/sbx/mgr/store/src/db/local_artifact_leases.rs similarity index 89% rename from crates/libs/sbx/mgr/server/src/db/local_artifact_leases.rs rename to crates/libs/sbx/mgr/store/src/db/local_artifact_leases.rs index 9a9299f01..811893820 100644 --- a/crates/libs/sbx/mgr/server/src/db/local_artifact_leases.rs +++ b/crates/libs/sbx/mgr/store/src/db/local_artifact_leases.rs @@ -81,7 +81,7 @@ impl std::fmt::Display for ArtifactState { } /// Point-in-time publication evidence for a named snapshot, not a checkout lease. -pub(crate) struct NamedArtifactPublication { +pub struct NamedArtifactPublication { pub snapshot_name: SnapshotName, pub local_path: String, pub artifact_kind: ArtifactKind, @@ -91,7 +91,7 @@ pub(crate) struct NamedArtifactPublication { /// Every sandbox whose pinned artifact has no backup-completed stamp, or /// whose pin names no row it owns. The caller, /// `LocalArtifacts::list_pending_backups`, documents the contract. -pub(crate) fn list_pending_backups(conn: &Connection) -> Result> { +pub fn list_pending_backups(conn: &Connection) -> Result> { conn.prepare( "SELECT s.id, s.current_local_artifact_id, s.current_local_artifact_kind FROM sandboxes AS s @@ -126,7 +126,7 @@ pub(crate) fn list_pending_backups(conn: &Connection) -> Result std::result::Result, crate::db::inventory_read::InventoryReadError> @@ -170,7 +170,7 @@ pub(crate) fn list_named_publications( } /// `(local_path, state)` of an artifact row, as stored. -pub(crate) fn artifact_path_and_state( +pub fn artifact_path_and_state( conn: &Connection, local_artifact_id: &str, ) -> Result> { @@ -183,7 +183,7 @@ pub(crate) fn artifact_path_and_state( } /// CAS `ready` → `restoring`; the number of rows changed. -pub(crate) fn mark_artifact_restoring(conn: &Connection, local_artifact_id: &str) -> Result { +pub fn mark_artifact_restoring(conn: &Connection, local_artifact_id: &str) -> Result { conn.execute( "UPDATE local_artifacts SET state = 'restoring' WHERE local_artifact_id = ?1 AND state = 'ready'", @@ -192,7 +192,7 @@ pub(crate) fn mark_artifact_restoring(conn: &Connection, local_artifact_id: &str } /// CAS `ready` → `deleting`; the number of rows changed. -pub(crate) fn mark_artifact_deleting(conn: &Connection, local_artifact_id: &str) -> Result { +pub fn mark_artifact_deleting(conn: &Connection, local_artifact_id: &str) -> Result { conn.execute( "UPDATE local_artifacts SET state = 'deleting' WHERE local_artifact_id = ?1 AND state = 'ready'", @@ -203,7 +203,7 @@ pub(crate) fn mark_artifact_deleting(conn: &Connection, local_artifact_id: &str) /// The GC checkout's evidence for an artifact `sandbox_id` owns: its /// creation and last-touch times, whether a sandbox pins it, and whether its /// backup completed. -pub(crate) fn gc_checkout_evidence( +pub fn gc_checkout_evidence( conn: &Connection, local_artifact_id: &str, sandbox_id: &str, @@ -232,7 +232,7 @@ pub(crate) fn gc_checkout_evidence( /// Whether a sandbox pins the artifact as its current or suspend-predecessor /// artifact. -pub(crate) fn artifact_is_pinned(conn: &Connection, local_artifact_id: &str) -> Result { +pub fn artifact_is_pinned(conn: &Connection, local_artifact_id: &str) -> Result { conn.query_row( sql!( "SELECT EXISTS(SELECT 1 FROM sandboxes @@ -244,7 +244,7 @@ pub(crate) fn artifact_is_pinned(conn: &Connection, local_artifact_id: &str) -> } /// The number of artifact rows at `local_path`. -pub(crate) fn count_artifacts_at_path(conn: &Connection, local_path: &str) -> Result { +pub fn count_artifacts_at_path(conn: &Connection, local_path: &str) -> Result { conn.query_row( "SELECT COUNT(*) FROM local_artifacts WHERE local_path = ?1", params![local_path], @@ -253,7 +253,7 @@ pub(crate) fn count_artifacts_at_path(conn: &Connection, local_path: &str) -> Re } /// Reset every `restoring` row to `ready`; the number reset. -pub(crate) fn reset_restoring_artifacts(conn: &Connection) -> Result { +pub fn reset_restoring_artifacts(conn: &Connection) -> Result { conn.execute( "UPDATE local_artifacts SET state = 'ready' WHERE state = 'restoring'", [], @@ -261,7 +261,7 @@ 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> { +pub fn list_deleting_artifacts(conn: &Connection) -> Result> { Ok(conn .prepare( "SELECT local_artifact_id, local_path FROM local_artifacts @@ -272,10 +272,7 @@ pub(crate) fn list_deleting_artifacts(conn: &Connection) -> Result Result { +pub fn roll_back_deleting_artifact(conn: &Connection, local_artifact_id: &str) -> Result { conn.execute( "UPDATE local_artifacts SET state = 'ready' WHERE local_artifact_id = ?1 AND state = 'deleting'", @@ -284,10 +281,7 @@ pub(crate) fn roll_back_deleting_artifact( } /// Delete the row if it is still `deleting`; the number deleted. -pub(crate) fn delete_deleting_artifact( - conn: &Connection, - local_artifact_id: &str, -) -> Result { +pub fn delete_deleting_artifact(conn: &Connection, local_artifact_id: &str) -> Result { conn.execute( "DELETE FROM local_artifacts WHERE local_artifact_id = ?1 AND state = 'deleting'", @@ -296,7 +290,7 @@ pub(crate) fn delete_deleting_artifact( } /// CAS `restoring` → `ready`, touching the artifact now. -pub(crate) fn release_restoring_artifact_with_touch( +pub fn release_restoring_artifact_with_touch( conn: &Connection, local_artifact_id: &str, ) -> Result { @@ -309,10 +303,7 @@ pub(crate) fn release_restoring_artifact_with_touch( } /// CAS `restoring` → `ready`, leaving the touch time alone. -pub(crate) fn release_restoring_artifact( - conn: &Connection, - local_artifact_id: &str, -) -> Result { +pub fn release_restoring_artifact(conn: &Connection, local_artifact_id: &str) -> Result { conn.execute( "UPDATE local_artifacts SET state = 'ready' WHERE local_artifact_id = ?1 AND state = 'restoring'", @@ -321,7 +312,7 @@ pub(crate) fn release_restoring_artifact( } /// Whether neither an artifact row nor a sandbox pin names the id. -pub(crate) fn gc_staging_is_unowned(conn: &Connection, local_artifact_id: &str) -> Result { +pub fn gc_staging_is_unowned(conn: &Connection, local_artifact_id: &str) -> Result { conn.query_row( sql!( "SELECT NOT EXISTS(SELECT 1 FROM local_artifacts WHERE local_artifact_id = ?1) @@ -334,7 +325,7 @@ pub(crate) fn gc_staging_is_unowned(conn: &Connection, local_artifact_id: &str) } /// The highest artifact rowid, `None` for an empty table. -pub(crate) fn max_artifact_rowid(conn: &Connection) -> Result> { +pub fn max_artifact_rowid(conn: &Connection) -> Result> { conn.query_row("SELECT MAX(rowid) FROM local_artifacts", [], |r| r.get(0)) } @@ -345,7 +336,7 @@ pub(crate) type OrphanScanRow = Result<(String, String, bool)>; /// Up to `limit` artifact rows with rowids in `next..=through`, in rowid /// order: each rowid, and its `(local_artifact_id, local_path, owned)` or the /// error decoding them, so one malformed row does not fail the scan. -pub(crate) fn orphan_scan_rows( +pub fn orphan_scan_rows( conn: &Connection, next: i64, through: i64, @@ -370,10 +361,7 @@ pub(crate) fn orphan_scan_rows( } /// Delete the artifact row if its sandbox row is gone; the number deleted. -pub(crate) fn delete_orphan_artifact_row( - conn: &Connection, - local_artifact_id: &str, -) -> Result { +pub fn delete_orphan_artifact_row(conn: &Connection, local_artifact_id: &str) -> Result { conn.execute( "DELETE FROM local_artifacts WHERE local_artifact_id = ?1 diff --git a/crates/libs/sbx/mgr/server/src/db/local_artifacts.rs b/crates/libs/sbx/mgr/store/src/db/local_artifacts.rs similarity index 100% rename from crates/libs/sbx/mgr/server/src/db/local_artifacts.rs rename to crates/libs/sbx/mgr/store/src/db/local_artifacts.rs diff --git a/crates/libs/sbx/mgr/store/src/db/migration.rs b/crates/libs/sbx/mgr/store/src/db/migration.rs new file mode 100644 index 000000000..04261a11b --- /dev/null +++ b/crates/libs/sbx/mgr/store/src/db/migration.rs @@ -0,0 +1,33 @@ +//! sbx-mgr's database as a migration runner sees it. The service migrates it +//! at startup with [`super::migrate`]; the database migrator's +//! `sandbox-manager` subcommand runs the same migrations, and its `down`, +//! `status` and `reset`, from [`target`], so neither names the migrations or +//! the path itself. + +use sqlx::migrate::Migrator; + +/// What a migration runner needs to migrate sbx-mgr's database. +/// +/// The database also carries the shared `db/common` schema (the `jobs` +/// table and its kin). Every runner applies that from its own embedded copy +/// of `db/common`, the one it applies to each database with the shared +/// schema, so the target names only the service's migrations. +#[derive(Debug, Clone, Copy)] +pub struct MigrationTarget { + /// The name the shared SQLite helpers log and report the database under. + pub name: &'static str, + /// Where the service opens the database, and so where a runner migrates + /// it unless told otherwise. + pub default_path: &'static str, + /// The service's own migrations. + pub service: &'static Migrator, +} + +/// sbx-mgr's database and its migrations. +pub const fn target() -> MigrationTarget { + MigrationTarget { + name: "sandbox-manager", + default_path: super::DB_PATH, + service: &pplx_space_sbxmgr_migrations::SANDBOX_MANAGER_MIGRATOR, + } +} diff --git a/crates/libs/sbx/mgr/server/src/db/network_policy.rs b/crates/libs/sbx/mgr/store/src/db/network_policy.rs similarity index 100% rename from crates/libs/sbx/mgr/server/src/db/network_policy.rs rename to crates/libs/sbx/mgr/store/src/db/network_policy.rs diff --git a/crates/libs/sbx/mgr/server/src/db/pool.rs b/crates/libs/sbx/mgr/store/src/db/pool.rs similarity index 100% rename from crates/libs/sbx/mgr/server/src/db/pool.rs rename to crates/libs/sbx/mgr/store/src/db/pool.rs diff --git a/crates/libs/sbx/mgr/server/src/db/quarantine.rs b/crates/libs/sbx/mgr/store/src/db/quarantine.rs similarity index 99% rename from crates/libs/sbx/mgr/server/src/db/quarantine.rs rename to crates/libs/sbx/mgr/store/src/db/quarantine.rs index 6c688c763..143df725e 100644 --- a/crates/libs/sbx/mgr/server/src/db/quarantine.rs +++ b/crates/libs/sbx/mgr/store/src/db/quarantine.rs @@ -460,7 +460,7 @@ pub(super) fn insert_new_quarantine_episode( ) } -pub(crate) fn insert_quarantined_sandbox_in_transaction( +pub fn insert_quarantined_sandbox_in_transaction( conn: &Connection, params: InsertSandboxParams<'_>, source: SandboxQuarantineSource, @@ -497,7 +497,7 @@ pub(crate) fn insert_quarantined_sandbox_in_transaction( Ok(()) } -pub(crate) fn emit_insert_quarantined_sandbox_event( +pub fn emit_insert_quarantined_sandbox_event( sandbox_id: &str, reason_code: SandboxQuarantineReasonCode, ) { @@ -510,7 +510,7 @@ pub(crate) fn emit_insert_quarantined_sandbox_event( ); } -#[cfg(test)] +#[cfg(any(test, feature = "test-support"))] pub fn mark_quarantined( conn: &Connection, id: &str, @@ -558,13 +558,13 @@ pub fn current_quarantine_reason_wire_value(conn: &Connection, id: &str) -> Resu } #[derive(Clone, Copy, Debug, Eq, PartialEq)] -pub(crate) enum PauseFailedRecoveryTarget { +pub enum PauseFailedRecoveryTarget { Started, Paused, } impl PauseFailedRecoveryTarget { - pub(crate) const fn sandbox_state(self) -> SandboxState { + pub const fn sandbox_state(self) -> SandboxState { match self { Self::Started => SandboxState::Started, Self::Paused => SandboxState::Paused, @@ -580,7 +580,7 @@ impl PauseFailedRecoveryTarget { } #[derive(Debug, thiserror::Error)] -pub(crate) enum PauseFailedRecoveryError { +pub enum PauseFailedRecoveryError { #[error("pause-failed recovery database operation failed: {0}")] Database(#[from] Error), #[error("pause-failed sandbox state CAS affected {changed} rows for {sandbox_id}")] @@ -602,7 +602,7 @@ pub(crate) enum PauseFailedRecoveryError { /// Recover an sbx-manager pause failure after the caller establishes /// definitive runtime state. -pub(crate) fn recover_pause_failed_quarantine( +pub fn recover_pause_failed_quarantine( conn: &Connection, id: &SandboxId, target: PauseFailedRecoveryTarget, @@ -769,7 +769,7 @@ pub fn sandbox_state_publication_metadata( Ok(publications) } -pub(crate) fn mark_quarantined_from_state( +pub fn mark_quarantined_from_state( conn: &Connection, id: &str, expected_state: SandboxState, diff --git a/crates/libs/sbx/mgr/server/src/db/quarantine_history.rs b/crates/libs/sbx/mgr/store/src/db/quarantine_history.rs similarity index 96% rename from crates/libs/sbx/mgr/server/src/db/quarantine_history.rs rename to crates/libs/sbx/mgr/store/src/db/quarantine_history.rs index 55b4fc7be..82c1d7934 100644 --- a/crates/libs/sbx/mgr/server/src/db/quarantine_history.rs +++ b/crates/libs/sbx/mgr/store/src/db/quarantine_history.rs @@ -10,7 +10,7 @@ const RETENTION_SECS: u64 = 30 * 24 * 60 * 60; /// New history waits for the next finite sweep; retained rows cannot starve it. #[derive(Debug)] -pub(crate) struct QuarantineHistoryPruner { +pub struct QuarantineHistoryPruner { next: i64, through: Option, } @@ -25,13 +25,13 @@ impl Default for QuarantineHistoryPruner { } #[derive(Debug, Default)] -pub(crate) struct HistoryPruneBatch { +pub struct HistoryPruneBatch { pub examined: usize, pub deleted: usize, } impl QuarantineHistoryPruner { - pub(crate) fn next_batch(&mut self, conn: &Connection, now: u64) -> Result { + pub fn next_batch(&mut self, conn: &Connection, now: u64) -> Result { let result = self.prune_batch(conn, now); if result.is_err() { *self = Self::default(); diff --git a/crates/libs/sbx/mgr/server/src/db/quarantine_history/tests.rs b/crates/libs/sbx/mgr/store/src/db/quarantine_history/tests.rs similarity index 100% rename from crates/libs/sbx/mgr/server/src/db/quarantine_history/tests.rs rename to crates/libs/sbx/mgr/store/src/db/quarantine_history/tests.rs diff --git a/crates/libs/sbx/mgr/server/src/db/quarantine_recovery.rs b/crates/libs/sbx/mgr/store/src/db/quarantine_recovery.rs similarity index 99% rename from crates/libs/sbx/mgr/server/src/db/quarantine_recovery.rs rename to crates/libs/sbx/mgr/store/src/db/quarantine_recovery.rs index f5a650445..6f2aa12af 100644 --- a/crates/libs/sbx/mgr/server/src/db/quarantine_recovery.rs +++ b/crates/libs/sbx/mgr/store/src/db/quarantine_recovery.rs @@ -111,7 +111,7 @@ pub struct ClaimedQuarantineRecoveryCandidate { } #[derive(Clone, Copy, Debug, Eq, PartialEq)] -pub(crate) enum QuarantineRecoveryOwner { +pub enum QuarantineRecoveryOwner { LegacyRecovery, OperatorAttention, } @@ -127,7 +127,7 @@ impl QuarantineRecoveryOwner { #[derive(Debug, thiserror::Error)] #[error("unknown quarantine recovery owner {value:?}")] -pub(crate) struct QuarantineRecoveryOwnerParseError { +pub struct QuarantineRecoveryOwnerParseError { value: String, } @@ -400,7 +400,7 @@ fn required_stored_integer(value: rusqlite::types::Value) -> Option { /// derived from the same rows. Rows whose columns fail validation are counted /// and skipped; a statement-level SQLite failure aborts the scan so a caller /// can never mistake a partial read for an empty backlog. -#[cfg(test)] +#[cfg(any(test, feature = "test-support"))] pub fn scan_quarantine_recovery_candidates(conn: &Connection) -> Result { // Tests that need a whole cohort still exercise production eligibility, // paging and decoding. Stop at exactly one finite sweep. @@ -579,9 +579,9 @@ pub(super) fn decode_quarantine_recovery_row( }) } -pub(crate) const QUARANTINE_RECOVERY_CLAIM_LEASE: Duration = Duration::from_secs(15 * 60); +pub const QUARANTINE_RECOVERY_CLAIM_LEASE: Duration = Duration::from_secs(15 * 60); -pub(crate) const QUARANTINE_RECOVERY_BACKOFF_BASE: Duration = Duration::from_secs(60); +pub const QUARANTINE_RECOVERY_BACKOFF_BASE: Duration = Duration::from_secs(60); pub(super) const QUARANTINE_RECOVERY_BACKOFF_MAX: Duration = Duration::from_secs(15 * 60); @@ -600,7 +600,7 @@ pub(super) const QUARANTINE_RECOVERY_BACKOFF_MAX: Duration = Duration::from_secs /// API, or a node republishing its quarantine after an outage, resolves in /// minutes. The outage itself reports lookup failures or timeouts, which leave /// a run where it was instead of extending it. -pub(crate) const QUARANTINE_RECOVERY_PLAN_REJECTION_BOUND: u32 = 16; +pub const QUARANTINE_RECOVERY_PLAN_REJECTION_BOUND: u32 = 16; /// How the node-local SQLite `sandbox_quarantine_state.last_error` carries a /// run of plan rejections. A first rejection records its bare reason, as every diff --git a/crates/libs/sbx/mgr/server/src/db/quarantine_scan.rs b/crates/libs/sbx/mgr/store/src/db/quarantine_scan.rs similarity index 96% rename from crates/libs/sbx/mgr/server/src/db/quarantine_scan.rs rename to crates/libs/sbx/mgr/store/src/db/quarantine_scan.rs index c8f31959e..0d469d43d 100644 --- a/crates/libs/sbx/mgr/server/src/db/quarantine_scan.rs +++ b/crates/libs/sbx/mgr/store/src/db/quarantine_scan.rs @@ -14,7 +14,7 @@ const RAW_ROWS_PER_BATCH: usize = 128; const CANDIDATES_PER_BATCH: usize = 8; #[derive(Debug)] -pub(crate) struct QuarantineRecoveryBatch { +pub struct QuarantineRecoveryBatch { pub candidates: Vec, /// A completed sweep's observations, not a transactionally consistent snapshot. /// Absent on partial batches; consumers must not publish page counts as totals. @@ -24,7 +24,7 @@ pub(crate) struct QuarantineRecoveryBatch { /// New rows wait for a later sweep. Busy ownership, slow I/O and skipped ticks /// can postpone discovery, so this cursor does not promise a recovery deadline. #[derive(Debug)] -pub(crate) struct PeriodicQuarantineRecoveryScan { +pub struct PeriodicQuarantineRecoveryScan { next: i64, through: Option, summary: QuarantineRecoveryScan, @@ -50,7 +50,7 @@ impl PeriodicQuarantineRecoveryScan { /// Every completion renews freshness, even after a slow sweep. Discovery /// stops after either 128 raw rows or eight due candidates per tick; tick /// work and admission delays prevent a fixed population-to-latency bound. - pub(crate) fn has_recent_completed_sweep(&self) -> bool { + pub fn has_recent_completed_sweep(&self) -> bool { self.has_recent_completed_sweep_at(Instant::now()) } @@ -60,7 +60,7 @@ impl PeriodicQuarantineRecoveryScan { }) } - pub(crate) fn next_batch(&mut self, conn: &Connection) -> Result { + pub fn next_batch(&mut self, conn: &Connection) -> Result { let batch = self.read_batch(conn); if batch.is_err() { // Never complete a backlog sample after an unread section of a sweep. diff --git a/crates/libs/sbx/mgr/server/src/db/quarantine_scan/tests.rs b/crates/libs/sbx/mgr/store/src/db/quarantine_scan/tests.rs similarity index 100% rename from crates/libs/sbx/mgr/server/src/db/quarantine_scan/tests.rs rename to crates/libs/sbx/mgr/store/src/db/quarantine_scan/tests.rs diff --git a/crates/libs/sbx/mgr/server/src/db/restore_transfer_scopes.rs b/crates/libs/sbx/mgr/store/src/db/restore_transfer_scopes.rs similarity index 100% rename from crates/libs/sbx/mgr/server/src/db/restore_transfer_scopes.rs rename to crates/libs/sbx/mgr/store/src/db/restore_transfer_scopes.rs diff --git a/crates/libs/sbx/mgr/server/src/db/route_activation_claims.rs b/crates/libs/sbx/mgr/store/src/db/route_activation_claims.rs similarity index 92% rename from crates/libs/sbx/mgr/server/src/db/route_activation_claims.rs rename to crates/libs/sbx/mgr/store/src/db/route_activation_claims.rs index 338458ffe..81fe6959c 100644 --- a/crates/libs/sbx/mgr/server/src/db/route_activation_claims.rs +++ b/crates/libs/sbx/mgr/store/src/db/route_activation_claims.rs @@ -17,7 +17,7 @@ use crate::auto_lifecycle_delete_receipts::RouteIdentityProvenance; /// This is not an exclusive lease: the matching pod and attempt may continue /// after a manager restart. A later activation is fenced by the retained /// generation recorded when that activation actually begins. -pub(crate) struct OpenRouteActivationClaim { +pub struct OpenRouteActivationClaim { route_generation: RouteGeneration, node_id: NodeId, execution_generation: ExecutionGeneration, @@ -26,7 +26,7 @@ pub(crate) struct OpenRouteActivationClaim { } impl OpenRouteActivationClaim { - pub(crate) fn for_cluster_fenced_identity( + pub fn for_cluster_fenced_identity( identity: &SpaceApiRouteIdentity, provenance: RouteIdentityProvenance, ) -> Option { @@ -44,7 +44,7 @@ impl OpenRouteActivationClaim { }) } - pub(crate) fn requested_generation(&self) -> u64 { + pub fn requested_generation(&self) -> u64 { // ExecutionGeneration admits only positive i64 values. self.route_generation .get() @@ -93,7 +93,7 @@ pub(crate) fn insert_create_claim( write_claim(conn, sandbox_id, claim) } -pub(crate) fn claim_matches( +pub fn claim_matches( conn: &Connection, sandbox_id: &str, claim: &OpenRouteActivationClaim, @@ -124,15 +124,15 @@ pub(crate) fn claim_matches( ) } -pub(crate) fn close_claim(conn: &Connection, sandbox_id: &str) -> crate::db::Result { +pub 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], )? == 1) } -#[cfg(test)] -pub(crate) fn claim_exists(conn: &Connection, sandbox_id: &str) -> crate::db::Result { +#[cfg(any(test, feature = "test-support"))] +pub 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.rs b/crates/libs/sbx/mgr/store/src/db/sandbox_row.rs similarity index 99% rename from crates/libs/sbx/mgr/server/src/db/sandbox_row.rs rename to crates/libs/sbx/mgr/store/src/db/sandbox_row.rs index 44d2ea14a..7378bb987 100644 --- a/crates/libs/sbx/mgr/server/src/db/sandbox_row.rs +++ b/crates/libs/sbx/mgr/store/src/db/sandbox_row.rs @@ -76,7 +76,7 @@ pub struct SandboxRow { } impl SandboxRow { - pub(crate) fn node_execution_fence( + pub fn node_execution_fence( &self, ) -> Option { self.node_execution_id.map(|id| { @@ -88,7 +88,7 @@ impl SandboxRow { /// The persisted fields List returns or filters on. List reads only these /// columns, so its inventory budget measures what it serves rather than every /// runtime and policy document on the row. -pub(crate) struct SandboxListRow { +pub struct SandboxListRow { pub id: String, pub state: SandboxState, pub template: String, diff --git a/crates/libs/sbx/mgr/server/src/db/sandbox_row/tests.rs b/crates/libs/sbx/mgr/store/src/db/sandbox_row/tests.rs similarity index 100% rename from crates/libs/sbx/mgr/server/src/db/sandbox_row/tests.rs rename to crates/libs/sbx/mgr/store/src/db/sandbox_row/tests.rs diff --git a/crates/libs/sbx/mgr/server/src/db/sandboxes.rs b/crates/libs/sbx/mgr/store/src/db/sandboxes.rs similarity index 98% rename from crates/libs/sbx/mgr/server/src/db/sandboxes.rs rename to crates/libs/sbx/mgr/store/src/db/sandboxes.rs index 130d501f6..08d2ad880 100644 --- a/crates/libs/sbx/mgr/server/src/db/sandboxes.rs +++ b/crates/libs/sbx/mgr/store/src/db/sandboxes.rs @@ -5,15 +5,12 @@ use super::*; /// Minimal lifecycle evidence that remains readable when optional metadata or /// a newer lifecycle label cannot be decoded by this binary. #[derive(Debug)] -pub(crate) struct RawSandboxLifecycle { +pub struct RawSandboxLifecycle { pub state: String, pub state_changed_at: String, } -pub(crate) fn raw_sandbox_lifecycle( - conn: &Connection, - id: &str, -) -> Result> { +pub fn raw_sandbox_lifecycle(conn: &Connection, id: &str) -> Result> { conn.query_row( "SELECT state, state_changed_at FROM sandboxes WHERE id = ?1", [id], @@ -28,7 +25,7 @@ pub(crate) fn raw_sandbox_lifecycle( } #[derive(Clone, Debug, Eq, PartialEq)] -pub(crate) struct SandboxLifecycleCount { +pub struct SandboxLifecycleCount { pub state: SandboxState, pub count: u64, } @@ -91,7 +88,7 @@ pub enum InsertRecoveredSuspendedSandboxError { /// Count every durable sandbox-manager row by its bounded lifecycle. This stays /// in SQL so the sampler's memory use is independent of the number of sandboxes /// on the node. -pub(crate) fn sandbox_lifecycle_counts(conn: &Connection) -> Result> { +pub fn sandbox_lifecycle_counts(conn: &Connection) -> Result> { let mut statement = conn.prepare( "SELECT state, COUNT(*) FROM sandboxes @@ -113,7 +110,7 @@ pub(crate) fn sandbox_lifecycle_counts(conn: &Connection) -> Result>()?) } -pub(crate) fn current_state_label(conn: &Connection, id: &str) -> Result> { +pub fn current_state_label(conn: &Connection, id: &str) -> Result> { conn.query_row( "SELECT state FROM sandboxes WHERE id = ?1", params![id], @@ -250,7 +247,7 @@ pub fn insert_sandbox(conn: &Connection, params: InsertSandboxParams<'_>) -> Res Ok(()) } -pub(crate) fn node_execution_fence( +pub fn node_execution_fence( conn: &Connection, sandbox_id: &str, ) -> Result> { @@ -280,7 +277,7 @@ pub(crate) fn node_execution_fence( } /// Bounded startup preflight; legacy NULL rows require the adoption baseline. -pub(crate) fn unbound_node_executions(conn: &Connection) -> Result> { +pub fn unbound_node_executions(conn: &Connection) -> Result> { let mut statement = conn .prepare("SELECT id FROM sandboxes WHERE node_execution_id IS NULL ORDER BY id LIMIT ?1")?; statement @@ -295,7 +292,7 @@ pub(crate) fn unbound_node_executions(conn: &Connection) -> Result> /// 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( +pub fn inherit_orphan_node_execution( tx: &Transaction<'_>, sandbox_id: &str, fence: pplx_space_node_mgr_client::execution_fence::ExecutionFence, @@ -318,7 +315,7 @@ pub(crate) fn inherit_orphan_node_execution( /// A cleanup reply may acknowledge the predecessor when allocation never /// reached the node. Never overwrite an activation that superseded this request. -pub(crate) fn acknowledge_node_cleanup( +pub fn acknowledge_node_cleanup( conn: &Connection, sandbox_id: &str, expected: pplx_space_node_mgr_client::execution_fence::ExecutionFence, @@ -384,7 +381,7 @@ pub(super) fn upsert_sandbox_execution_metadata_in_transaction( Ok(()) } -pub(crate) fn insert_recovered_suspended_sandbox_in_transaction( +pub fn insert_recovered_suspended_sandbox_in_transaction( conn: &Connection, params: InsertSandboxParams<'_>, execution: &ExecutionMetadata, @@ -441,7 +438,7 @@ pub fn restore_adopt_orphan_or_insert_sandbox( Ok(()) } -pub(crate) fn restore_adopt_orphan_or_insert_sandbox_in_transaction( +pub fn restore_adopt_orphan_or_insert_sandbox_in_transaction( conn: &Connection, params: InsertSandboxParams<'_>, restore_operation: &RemoteRestoreOperation, @@ -625,10 +622,7 @@ pub fn update_snapshots_enabled(conn: &Connection, id: &str, enabled: bool) -> R /// Check the observation against the current incarnation before replaying /// guest setup. The caller holds the wake and cleanup fences across setup. -pub(crate) fn startup_resident_witness_current( - conn: &Connection, - observed: &SandboxRow, -) -> Result { +pub fn startup_resident_witness_current(conn: &Connection, observed: &SandboxRow) -> Result { let Some(current) = get_sandbox(conn, &observed.id)? else { return Ok(false); }; @@ -750,7 +744,7 @@ pub fn update_space_api_route_identity( Ok(()) } -pub(crate) fn update_space_api_route_identity_in_transaction( +pub fn update_space_api_route_identity_in_transaction( conn: &Connection, sandbox_id: SandboxId, identity: &SpaceApiRouteIdentity, @@ -827,7 +821,7 @@ pub(crate) fn update_space_api_route_identity_in_transaction( Ok(receipt_supersession) } -pub(crate) fn update_runtime_fields( +pub fn update_runtime_fields( conn: &Connection, sandbox_id: SandboxId, fields: RuntimeFields<'_>, @@ -1236,7 +1230,7 @@ pub fn list_sandboxes_for_recovery(conn: &Connection) -> Result> /// New candidates wait for a later sweep to reach their rowid; admission pressure /// and recovery failures can delay progress further. No recovery deadline is promised. #[derive(Debug)] -pub(crate) struct PeriodicLifecycleRecoveryScan { +pub struct PeriodicLifecycleRecoveryScan { next: i64, through: Option, } @@ -1257,7 +1251,7 @@ impl Default for PeriodicLifecycleRecoveryScan { /// Advance past malformed/ineligible rows as well as candidates, so a retained /// or busy first page cannot starve the rest. Rowid is only a scan position; /// each consumer must revalidate the execution witness under its own fences. -pub(crate) fn periodic_lifecycle_recovery_batch( +pub fn periodic_lifecycle_recovery_batch( conn: &Connection, scan: &mut PeriodicLifecycleRecoveryScan, ) -> Result> { @@ -1424,7 +1418,7 @@ fn inventory_deletion_witness_matches(current: &SandboxRow, observed: &SandboxRo } #[derive(Clone, Copy, Debug, Eq, PartialEq)] -pub(crate) enum InventoryOwnershipReason { +pub enum InventoryOwnershipReason { DurableCleanupEpisode, RowCleanupIntent, MalformedCleanupIntent, @@ -1433,7 +1427,7 @@ pub(crate) enum InventoryOwnershipReason { } #[derive(Clone, Copy, Debug, Eq, PartialEq)] -pub(crate) enum InventoryStaleRowMutation { +pub enum InventoryStaleRowMutation { Removed, Missing, WitnessChanged, @@ -1444,7 +1438,7 @@ pub(crate) enum InventoryStaleRowMutation { /// is unchanged and no durable lifecycle owner exists. The immediate /// transaction makes the ownership reads and manual child-row cascade /// indivisible from concurrent lifecycle writers. -pub(crate) fn delete_sandbox_if_inventory_witness_matches( +pub fn delete_sandbox_if_inventory_witness_matches( conn: &Connection, observed: &SandboxRow, ) -> Result { diff --git a/crates/libs/sbx/mgr/server/src/db/snapshot_gc.rs b/crates/libs/sbx/mgr/store/src/db/snapshot_gc.rs similarity index 87% rename from crates/libs/sbx/mgr/server/src/db/snapshot_gc.rs rename to crates/libs/sbx/mgr/store/src/db/snapshot_gc.rs index be6ceefcc..4f1dc244e 100644 --- a/crates/libs/sbx/mgr/server/src/db/snapshot_gc.rs +++ b/crates/libs/sbx/mgr/store/src/db/snapshot_gc.rs @@ -8,35 +8,35 @@ use rusqlite::params; use super::inventory_read::{InventoryReadError, check_row}; use super::timestamps::EpochSeconds; -pub(crate) const PAGE_SIZE: usize = 128; +pub const PAGE_SIZE: usize = 128; #[derive(Debug)] -pub(crate) struct ArtifactObservation { - pub(crate) local_artifact_id: String, - pub(crate) sandbox_id: String, - pub(crate) artifact_origin: String, - pub(crate) artifact_kind: String, - pub(crate) local_path: String, - pub(crate) created_at_secs: u64, - pub(crate) last_touched_at_secs: u64, - pub(crate) backup_completed: bool, - pub(crate) is_ready_state: bool, - pub(crate) pinned_current: bool, +pub struct ArtifactObservation { + pub local_artifact_id: String, + pub sandbox_id: String, + pub artifact_origin: String, + pub artifact_kind: String, + pub local_path: String, + pub created_at_secs: u64, + pub last_touched_at_secs: u64, + pub backup_completed: bool, + pub is_ready_state: bool, + pub pinned_current: bool, } #[derive(Debug, Default)] -pub(crate) struct ArtifactPage { - pub(crate) artifacts: Vec, - pub(crate) examined: usize, +pub struct ArtifactPage { + pub artifacts: Vec, + pub examined: usize, } /// Fixed-ceiling sweeps visit malformed and protected rows without allowing /// either those rows or concurrent insertion to starve later candidates. /// This is page-local LRU discovery, not a globally ordered inventory. #[derive(Clone, Copy, Debug)] -pub(crate) struct ArtifactScan { - next: i64, - through: Option, +pub struct ArtifactScan { + pub next: i64, + pub through: Option, } impl Default for ArtifactScan { @@ -48,7 +48,7 @@ impl Default for ArtifactScan { } } -const PAGE_SQL: &str = sql!( +pub const PAGE_SQL: &str = sql!( "SELECT a.rowid, a.local_artifact_id, a.sandbox_id, a.artifact_origin, a.artifact_kind, a.local_path, a.created_at, COALESCE(a.last_touched_at, a.created_at), @@ -61,7 +61,7 @@ const PAGE_SQL: &str = sql!( ); impl ArtifactScan { - pub(crate) fn next_page( + pub fn next_page( &mut self, conn: &Connection, ) -> std::result::Result { diff --git a/crates/libs/sbx/mgr/server/src/db/snapshot_gc/tests.rs b/crates/libs/sbx/mgr/store/src/db/snapshot_gc/tests.rs similarity index 100% rename from crates/libs/sbx/mgr/server/src/db/snapshot_gc/tests.rs rename to crates/libs/sbx/mgr/store/src/db/snapshot_gc/tests.rs diff --git a/crates/libs/sbx/mgr/server/src/db/snapshot_upload_jobs.rs b/crates/libs/sbx/mgr/store/src/db/snapshot_upload_jobs.rs similarity index 87% rename from crates/libs/sbx/mgr/server/src/db/snapshot_upload_jobs.rs rename to crates/libs/sbx/mgr/store/src/db/snapshot_upload_jobs.rs index 4588208fd..4e60d9983 100644 --- a/crates/libs/sbx/mgr/server/src/db/snapshot_upload_jobs.rs +++ b/crates/libs/sbx/mgr/store/src/db/snapshot_upload_jobs.rs @@ -10,7 +10,7 @@ pub const SNAPSHOT_UPLOAD_JOB_KIND: &str = "snapshot_upload"; /// Give back the attempt a restart interrupted: every running job of `kind` /// with attempts left loses one; the number refunded. -pub(crate) fn refund_interrupted_running_attempts(conn: &Connection, kind: &str) -> Result { +pub fn refund_interrupted_running_attempts(conn: &Connection, kind: &str) -> Result { conn.execute( "UPDATE jobs SET attempts = attempts - 1 @@ -20,7 +20,7 @@ pub(crate) fn refund_interrupted_running_attempts(conn: &Connection, kind: &str) } /// Replace a pending or running job's payload; the number of rows changed. -pub(crate) fn replace_active_job_payload( +pub fn replace_active_job_payload( conn: &Connection, payload: &str, job_id: &JobId, @@ -34,7 +34,7 @@ pub(crate) fn replace_active_job_payload( } /// The highest job rowid, `None` for an empty table. -pub(crate) fn max_job_rowid(conn: &Connection) -> Result> { +pub fn max_job_rowid(conn: &Connection) -> Result> { conn.query_row("SELECT MAX(rowid) FROM jobs", [], |r| r.get(0)) } @@ -44,7 +44,7 @@ pub(crate) fn max_job_rowid(conn: &Connection) -> Result> { /// statement is bound, before the first row is read. The statement stays open /// while `visit` runs, so `visit` may write through the same connection; its /// error stops the scan. -pub(crate) fn visit_completed_upload_payloads( +pub fn visit_completed_upload_payloads( conn: &Connection, after: i64, through: i64, @@ -70,7 +70,7 @@ pub(crate) fn visit_completed_upload_payloads( /// Stamp `backup_completed_at` on an artifact that has none; the number of /// rows changed. -pub(crate) fn stamp_backup_completed_if_unset( +pub fn stamp_backup_completed_if_unset( conn: &Connection, now: &str, local_artifact_id: &str, @@ -83,7 +83,7 @@ pub(crate) fn stamp_backup_completed_if_unset( } /// Whether an artifact row exists. -pub(crate) fn local_artifact_exists(conn: &Connection, local_artifact_id: &str) -> Result { +pub fn local_artifact_exists(conn: &Connection, local_artifact_id: &str) -> Result { conn.query_row( "SELECT EXISTS(SELECT 1 FROM local_artifacts WHERE local_artifact_id = ?1)", params![local_artifact_id], diff --git a/crates/libs/sbx/mgr/server/src/db/startup_recovery.rs b/crates/libs/sbx/mgr/store/src/db/startup_recovery.rs similarity index 99% rename from crates/libs/sbx/mgr/server/src/db/startup_recovery.rs rename to crates/libs/sbx/mgr/store/src/db/startup_recovery.rs index b7eaa4bfa..d23056fc0 100644 --- a/crates/libs/sbx/mgr/server/src/db/startup_recovery.rs +++ b/crates/libs/sbx/mgr/store/src/db/startup_recovery.rs @@ -38,7 +38,7 @@ pub struct StartupRecoveryRecord { /// The settlement determines both durable state and event semantics. Healthy /// outcomes cannot carry quarantine reasons or errors. -pub(crate) enum StartupResidentSettlement<'a> { +pub enum StartupResidentSettlement<'a> { Started, Paused, Quarantined { @@ -49,7 +49,7 @@ pub(crate) enum StartupResidentSettlement<'a> { /// Commit lifecycle and quarantine evidence together. A changed row or durable /// cleanup owner supersedes the entire observation. -pub(crate) fn settle_startup_resident( +pub fn settle_startup_resident( conn: &Connection, observed: &SandboxRow, settlement: StartupResidentSettlement<'_>, diff --git a/crates/libs/sbx/mgr/server/src/db/state_transitions.rs b/crates/libs/sbx/mgr/store/src/db/state_transitions.rs similarity index 97% rename from crates/libs/sbx/mgr/server/src/db/state_transitions.rs rename to crates/libs/sbx/mgr/store/src/db/state_transitions.rs index 4a9337799..002063604 100644 --- a/crates/libs/sbx/mgr/server/src/db/state_transitions.rs +++ b/crates/libs/sbx/mgr/store/src/db/state_transitions.rs @@ -48,7 +48,7 @@ pub enum SupersedeActivationError { StateWriteNotApplied(#[from] SandboxStateWriteNotApplied), } -pub(crate) fn restore_checkpoint_state( +pub fn restore_checkpoint_state( conn: &Connection, id: &str, stable_state: SandboxState, @@ -73,7 +73,7 @@ pub(crate) fn restore_checkpoint_state( /// [`edges::RESUME_BEGIN`] refuses any other state. An absent generation /// matches only an identity-free legacy resident; it is never a wildcard for an /// execution that has acquired a generation. -pub(crate) fn begin_resident_resume_with_policy( +pub fn begin_resident_resume_with_policy( conn: &Connection, id: &str, generation: Option, @@ -95,7 +95,7 @@ pub(crate) fn begin_resident_resume_with_policy( Ok(true) } -pub(crate) fn restore_resident_resume_state( +pub fn restore_resident_resume_state( conn: &Connection, id: &str, stable_state: SandboxState, @@ -112,7 +112,7 @@ pub(crate) fn restore_resident_resume_state( Ok(changed) } -pub(crate) fn recover_interrupted_pause_state( +pub fn recover_interrupted_pause_state( conn: &Connection, id: &str, target: PauseFailedRecoveryTarget, @@ -134,7 +134,7 @@ pub(crate) fn recover_interrupted_pause_state( /// Transfer lifecycle ownership from an armed activation to an explicit /// stop/delete teardown. The caller is responsible for completing that /// teardown; the activation guard becomes a superseded no-op. -#[cfg(test)] +#[cfg(any(test, feature = "test-support"))] fn update_state_superseding_activation( conn: &Connection, id: &str, @@ -150,7 +150,7 @@ fn update_state_superseding_activation( Ok(()) } -pub(crate) fn update_state_superseding_activation_in_transaction( +pub fn update_state_superseding_activation_in_transaction( conn: &Connection, id: &str, state: SandboxState, @@ -185,7 +185,7 @@ pub(crate) fn update_state_superseding_activation_in_transaction( Ok(()) } -#[cfg(test)] +#[cfg(any(test, feature = "test-support"))] pub fn mark_stopping_superseding_activation( conn: &Connection, id: &str, @@ -193,7 +193,7 @@ pub fn mark_stopping_superseding_activation( update_state_superseding_activation(conn, id, SandboxState::Stopping) } -#[cfg(test)] +#[cfg(any(test, feature = "test-support"))] pub fn mark_deleting_superseding_activation( conn: &Connection, id: &str, @@ -225,14 +225,14 @@ pub fn begin_activation( } #[derive(Default)] -pub(crate) struct ActivationRouteAdmission<'a> { - pub(crate) auto_lifecycle: Option<&'a AutoLifecyclePolicy>, - pub(crate) operation_activation_attempt_id: Option, - pub(crate) evidence: Option, - pub(crate) open_claim: Option<&'a route_activation_claims::OpenRouteActivationClaim>, +pub struct ActivationRouteAdmission<'a> { + pub auto_lifecycle: Option<&'a AutoLifecyclePolicy>, + pub operation_activation_attempt_id: Option, + pub evidence: Option, + pub open_claim: Option<&'a route_activation_claims::OpenRouteActivationClaim>, } -pub(crate) fn begin_activation_with_route_evidence( +pub fn begin_activation_with_route_evidence( conn: &Connection, id: &str, expected_state: SandboxState, @@ -421,7 +421,7 @@ pub fn mark_cross_node_restore_staging_failed( } #[derive(Debug, thiserror::Error)] -pub(crate) enum ActivationOperationError { +pub enum ActivationOperationError { #[error("activation receipt database operation failed: {0}")] Database(#[from] Error), #[error("sandbox no longer exists")] @@ -439,7 +439,7 @@ pub(crate) enum ActivationOperationError { /// Join an armed activation without changing its route-ownership fence. The /// update is durable before a Wake waiter can disappear; cleanup will report /// the joined operation even if the original worker finishes asynchronously. -pub(crate) fn bind_activation_operation( +pub fn bind_activation_operation( conn: &Connection, id: &str, route: &SpaceApiRouteIdentity, @@ -473,7 +473,7 @@ pub(crate) fn bind_activation_operation( /// while its wake lock is held, after identity adoption and before fallible /// gateway sync. Lifecycle completion does not depend on that network call. This /// also closes a join whose activation completed before we bound its episode. -pub(crate) fn complete_resident_wake_operation( +pub fn complete_resident_wake_operation( conn: &Connection, id: &str, route: &SpaceApiRouteIdentity, @@ -661,7 +661,7 @@ pub fn mark_start_succeeded( /// Publish a disposable clone only while its original cleanup episode is armed. /// Publication and retiring deletion ownership are one transaction. -pub(crate) fn complete_pool_clone( +pub fn complete_pool_clone( conn: &Connection, id: &str, cleanup_id: SandboxCleanupEpisodeId, @@ -931,7 +931,7 @@ pub fn mark_suspending_quarantined( ) } -#[cfg(test)] +#[cfg(any(test, feature = "test-support"))] pub fn mark_starting_quarantined( conn: &Connection, id: &str, @@ -949,7 +949,7 @@ pub fn mark_starting_quarantined( ) } -#[cfg(test)] +#[cfg(any(test, feature = "test-support"))] pub fn mark_restoring_quarantined( conn: &Connection, id: &str, @@ -967,12 +967,12 @@ pub fn mark_restoring_quarantined( ) } -#[cfg(test)] +#[cfg(any(test, feature = "test-support"))] pub fn rollback_starting(conn: &Connection, id: &str, state: SandboxState) -> Result { rollback_activation_state(conn, id, SandboxState::Starting, state) } -#[cfg(test)] +#[cfg(any(test, feature = "test-support"))] pub fn rollback_restoring(conn: &Connection, id: &str, state: SandboxState) -> Result { rollback_activation_state(conn, id, SandboxState::Restoring, state) } diff --git a/crates/libs/sbx/mgr/server/src/db/state_write.rs b/crates/libs/sbx/mgr/store/src/db/state_write.rs similarity index 99% rename from crates/libs/sbx/mgr/server/src/db/state_write.rs rename to crates/libs/sbx/mgr/store/src/db/state_write.rs index 66e69792d..97e93c8d8 100644 --- a/crates/libs/sbx/mgr/server/src/db/state_write.rs +++ b/crates/libs/sbx/mgr/store/src/db/state_write.rs @@ -552,8 +552,8 @@ pub fn update_state_from( /// [`update_state_from`] over an arbitrary from-set, for tests that sweep /// from-sets no edge has. -#[cfg(test)] -pub(crate) fn update_state_from_states( +#[cfg(any(test, feature = "test-support"))] +pub fn update_state_from_states( conn: &Connection, id: &str, from: StateSet, diff --git a/crates/libs/sbx/mgr/server/src/db/teardown_jobs.rs b/crates/libs/sbx/mgr/store/src/db/teardown_jobs.rs similarity index 94% rename from crates/libs/sbx/mgr/server/src/db/teardown_jobs.rs rename to crates/libs/sbx/mgr/store/src/db/teardown_jobs.rs index 0d25d16f4..6aed20bb5 100644 --- a/crates/libs/sbx/mgr/server/src/db/teardown_jobs.rs +++ b/crates/libs/sbx/mgr/store/src/db/teardown_jobs.rs @@ -14,7 +14,7 @@ use pplx_space_common_core::{ use rusqlite::params; #[derive(Debug, Clone, Copy, PartialEq, Eq)] -pub(crate) enum TeardownSuspendFailure { +pub enum TeardownSuspendFailure { Missing, Deleted, Deleting, @@ -25,7 +25,7 @@ pub(crate) enum TeardownSuspendFailure { } impl TeardownSuspendFailure { - pub(crate) const fn as_str(self) -> &'static str { + pub const fn as_str(self) -> &'static str { match self { Self::Missing => "sandbox record no longer exists", Self::Deleted => "sandbox was deleted", @@ -48,7 +48,7 @@ impl std::fmt::Display for TeardownSuspendFailure { /// A recovered resident may start a fresh attempt; retained artifacts and /// in-flight cleanup cannot supply that authority, even for the same generation. -pub(crate) fn teardown_suspend_retry_admissible( +pub fn teardown_suspend_retry_admissible( conn: &Connection, id: &str, generation: ExecutionGeneration, @@ -70,7 +70,7 @@ pub(crate) fn teardown_suspend_retry_admissible( ) } -pub(crate) fn fail_teardown_suspend_operation( +pub fn fail_teardown_suspend_operation( conn: &Connection, id: &str, generation: ExecutionGeneration, @@ -79,7 +79,7 @@ pub(crate) fn fail_teardown_suspend_operation( fail_operation(conn, id, generation, reason, None) } -pub(crate) fn fail_teardown_suspend_backup_operation( +pub fn fail_teardown_suspend_backup_operation( conn: &Connection, id: &str, generation: ExecutionGeneration, @@ -152,7 +152,7 @@ pub(super) fn fail_active( /// Use the active-kind index to avoid retained history. Limit closures per tick /// so a backlog cannot monopolize SQLite. Recheck candidates after taking the /// write lock; the read-only fast path takes no write transaction when idle. -pub(crate) fn reconcile_terminal_teardown_suspend_jobs(conn: &Connection) -> Result { +pub fn reconcile_terminal_teardown_suspend_jobs(conn: &Connection) -> Result { // Include the complete partial-index predicate, then exclude cancellation: // SQLite does not infer that the narrower two-status IN implies this index. let has_candidates: bool = conn.query_row( @@ -200,7 +200,7 @@ const TEARDOWN_SUSPEND_RETRY_DELAY_SECS: i64 = 30; /// Persist retry pacing on the generation's existing job, so daemon restarts /// and frequent shim polls cannot turn a blocked backup into a tight loop. -pub(crate) fn defer_teardown_suspend_retry( +pub fn defer_teardown_suspend_retry( conn: &Connection, id: &str, generation: ExecutionGeneration, @@ -220,7 +220,7 @@ pub(crate) fn defer_teardown_suspend_retry( Ok(()) } -pub(crate) fn teardown_suspend_retry_due( +pub fn teardown_suspend_retry_due( conn: &Connection, id: &str, generation: ExecutionGeneration, diff --git a/crates/libs/sbx/mgr/server/src/db/teardown_suspend.rs b/crates/libs/sbx/mgr/store/src/db/teardown_suspend.rs similarity index 99% rename from crates/libs/sbx/mgr/server/src/db/teardown_suspend.rs rename to crates/libs/sbx/mgr/store/src/db/teardown_suspend.rs index 1aa79d235..e1a63566e 100644 --- a/crates/libs/sbx/mgr/server/src/db/teardown_suspend.rs +++ b/crates/libs/sbx/mgr/store/src/db/teardown_suspend.rs @@ -4,7 +4,7 @@ use super::*; pub const TEARDOWN_SUSPEND_JOB_KIND: &str = "teardown_suspend"; -pub(crate) const PLANNED_TEARDOWN_SUSPEND_OPERATION: &str = "planned_teardown"; +pub const PLANNED_TEARDOWN_SUSPEND_OPERATION: &str = "planned_teardown"; const TEARDOWN_SUSPEND_SUPERSEDED_REASON: &str = "superseded by a newer local execution lease"; @@ -373,7 +373,7 @@ pub(super) fn active_teardown_suspend_operation( .map_err(Error::from) } -pub(crate) fn latest_teardown_suspend_operation( +pub fn latest_teardown_suspend_operation( conn: &Connection, sandbox_id: &str, ) -> Result> { @@ -409,7 +409,7 @@ fn parse_teardown_suspend_operation( }) } -pub(crate) struct TeardownReleaseEvidence { +pub struct TeardownReleaseEvidence { pub runtime_generation: Option, pub suspend: Option, pub deleted: bool, @@ -418,7 +418,7 @@ pub(crate) struct TeardownReleaseEvidence { /// Release evidence must describe one SQLite snapshot: a historical rollback /// cannot be combined with ownership from before a replacement activation. -pub(crate) fn teardown_release_evidence( +pub fn teardown_release_evidence( conn: &Connection, sandbox_id: &str, execution_generation: ExecutionGeneration, diff --git a/crates/libs/sbx/mgr/store/src/db/test_fixtures.rs b/crates/libs/sbx/mgr/store/src/db/test_fixtures.rs new file mode 100644 index 000000000..026999780 --- /dev/null +++ b/crates/libs/sbx/mgr/store/src/db/test_fixtures.rs @@ -0,0 +1,244 @@ +//! Seeds and whole-database readers shared by the store's tests and +//! sbx-mgr's other test suites, exposed through the `test-support` feature. +use super::*; + +pub mod state_write_fixtures; + +/// Build a quarantine-recovery episode for the loop's mechanics to act on. +/// +/// For any reason other than `orphaned_sandbox_row_missing` this both names a +/// full/suspend artifact on the sandbox row and registers it in +/// `local_artifacts`. Production reaches the same *loop-visible* state by a +/// different route: the registry row is reaped while the sandbox row keeps +/// naming the artifact, a restore then fails, and its cleanup quarantines +/// because it cannot account for the artifact. The scan and the claim read only +/// the sandbox row's columns, which are identical either way, and the adoption +/// upserts the registry row rather than requiring it. What no writer produces is +/// the divergence that would let a *failed restore* arrive in this shape — see +/// `a_failed_restore_recovers_only_if_the_two_artifact_stores_diverge`. So tests +/// built on this helper cover the fence, the backoff and the adoption, which are +/// reason-agnostic; do not cite one as evidence that a failed restore recovers. +pub fn setup_quarantine_recovery_episode( + conn: &Connection, + reason_code: SandboxQuarantineReasonCode, +) -> SandboxId { + let sandbox_id = SandboxId::new(); + let sandbox_id_string = sandbox_id.to_string(); + insert_test_sandbox( + conn, + TestSandboxInsert::new( + &sandbox_id_string, + SandboxRuntimeLabel::from_sandbox_id(sandbox_id).to_string(), + SandboxState::Restoring, + ), + ); + if reason_code != SandboxQuarantineReasonCode::OrphanedSandboxRowMissing { + let local_artifact_id = LocalArtifactId::new(); + insert_local_artifact( + conn, + &sandbox_id_string, + local_artifact_id, + ArtifactKind::Full, + ArtifactOrigin::Suspend, + None, + "/space/cache/sandboxes/test/quarantine-recovery", + ) + .unwrap(); + conn.execute( + "UPDATE sandboxes + SET current_local_artifact_id = ?1, + current_local_artifact_kind = 'full', + current_local_artifact_origin = 'suspend', + space_api_route_execution_generation = 7 + WHERE id = ?2", + params![local_artifact_id.to_string(), sandbox_id_string], + ) + .unwrap(); + } + assert!( + mark_restoring_quarantined( + conn, + &sandbox_id_string, + reason_code, + "restore rollout interrupted", + ) + .unwrap() + ); + sandbox_id +} + +/// `(next_action_owner, attempt_count, next_action_after, claim_token, claim_expires_at)` +/// for one quarantine episode — the fields legacy recovery leases against. +pub fn quarantine_claim_state( + conn: &Connection, + sandbox_id: SandboxId, +) -> ( + Option, + i64, + Option, + Option, + Option, +) { + conn.query_row( + "SELECT next_action_owner, attempt_count, next_action_after, + claim_token, claim_expires_at + FROM sandbox_quarantine_state + WHERE sandbox_id = ?1", + params![sandbox_id.to_string()], + |row| { + Ok(( + row.get(0)?, + row.get(1)?, + row.get(2)?, + row.get(3)?, + row.get(4)?, + )) + }, + ) + .unwrap() +} + +pub fn setup_quarantine_recovery_candidate( + conn: &Connection, +) -> (SandboxId, QuarantineRecoveryCandidate) { + let sandbox_id = setup_quarantine_recovery_episode( + conn, + SandboxQuarantineReasonCode::RestoreFirecrackerLaunchFailed, + ); + let [candidate] = scan_quarantine_recovery_candidates(conn) + .unwrap() + .candidates + .try_into() + .unwrap(); + (sandbox_id, candidate) +} + +/// Every row of every table, order-insensitive. +pub fn database_rows(conn: &Connection) -> std::collections::BTreeMap> { + let tables: Vec = conn + .prepare("SELECT name FROM sqlite_master WHERE type = 'table' AND name NOT LIKE 'sqlite_%'") + .unwrap() + .query_map([], |row| row.get(0)) + .unwrap() + .collect::>() + .unwrap(); + tables + .into_iter() + .map(|table| { + let mut statement = conn.prepare(&format!("SELECT * FROM \"{table}\"")).unwrap(); + let width = statement.column_count(); + let mut rows: Vec = statement + .query_map([], |row| { + (0..width) + .map(|column| row.get::<_, rusqlite::types::Value>(column)) + .collect::>>() + .map(|values| format!("{values:?}")) + }) + .unwrap() + .collect::>() + .unwrap(); + rows.sort(); + (table, rows) + }) + .collect() +} + +pub struct TestSandboxInsert { + pub(crate) id: String, + pub(crate) uuid8: String, + pub(crate) state: SandboxState, + pub(crate) user: String, + pub(crate) rootfs_size_mb: Option, + pub(crate) disk_io_limits: pplx_space_common_core::types::DiskIoLimits, + pub(crate) pool_template: Option, + pub(crate) volume_encryption: bool, + pub(crate) process_env: BTreeMap, + pub(crate) auto_lifecycle: AutoLifecyclePolicy, + pub(crate) network_policy_override: Option, + pub(crate) network_policy_generation: u64, + pub(crate) network_policy_authority: PolicyAuthority, + pub(crate) auto_lifecycle_authority: PolicyAuthority, +} + +impl TestSandboxInsert { + pub(crate) fn new( + id: impl Into, + uuid8: impl Into, + state: SandboxState, + ) -> Self { + Self { + id: id.into(), + uuid8: uuid8.into(), + state, + user: "user".to_string(), + rootfs_size_mb: None, + disk_io_limits: Default::default(), + pool_template: None, + volume_encryption: false, + process_env: BTreeMap::new(), + auto_lifecycle: AutoLifecyclePolicy::disabled(), + network_policy_override: None, + network_policy_generation: 0, + network_policy_authority: PolicyAuthority::Cluster, + auto_lifecycle_authority: PolicyAuthority::Cluster, + } + } + + pub(crate) fn insert_params<'a>( + &'a self, + uuid8: &'a SandboxRuntimeLabel, + user: &'a SandboxOwner, + ) -> InsertSandboxParams<'a> { + InsertSandboxParams { + id: &self.id, + uuid8, + state: self.state, + template: "seed", + template_hash: Some("hash"), + user, + runtime: "fc", + snapshots_enabled: false, + rootfs_size_mb: self.rootfs_size_mb, + disk_io_limits: self.disk_io_limits, + pool_template: self.pool_template.as_deref(), + volume_encryption: self.volume_encryption, + process_env: &self.process_env, + auto_lifecycle: &self.auto_lifecycle, + network_policy_override: self.network_policy_override.as_ref(), + network_policy_generation: self.network_policy_generation, + network_policy_authority: self.network_policy_authority, + auto_lifecycle_authority: self.auto_lifecycle_authority, + } + } +} + +pub fn insert_test_sandbox(conn: &Connection, params: TestSandboxInsert) { + let uuid8 = SandboxRuntimeLabel::parse(params.uuid8.clone()).unwrap(); + let user = SandboxOwner::parse(params.user.clone()).unwrap(); + insert_sandbox(conn, params.insert_params(&uuid8, &user)).unwrap(); +} + +/// Claim the episode's due candidate and record `reason` against the claim, +/// as one recovery pass does: the claim carries the reason's effect on the run +/// of plan rejections, and the no-progress write records the reason. +pub fn record_quarantine_recovery_attempt( + conn: &Connection, + sandbox_id: SandboxId, + reason: QuarantineRecoveryNoProgressReason, +) -> QuarantineRecoveryNoProgressOutcome { + conn.execute( + "UPDATE sandbox_quarantine_state SET next_action_after = NULL WHERE sandbox_id = ?1", + params![sandbox_id.to_string()], + ) + .unwrap(); + let [candidate] = scan_quarantine_recovery_candidates(conn) + .unwrap() + .candidates + .try_into() + .expect("a due episode is a candidate"); + let claimed = + claim_quarantine_recovery_candidate(conn, &candidate, PlanRejectionRunEffect::of(reason)) + .unwrap() + .expect("a due candidate is claimable"); + record_quarantine_recovery_no_progress(conn, &claimed, reason).unwrap() +} diff --git a/crates/libs/sbx/mgr/server/src/db/tests/state_write_fixtures.rs b/crates/libs/sbx/mgr/store/src/db/test_fixtures/state_write_fixtures.rs similarity index 88% rename from crates/libs/sbx/mgr/server/src/db/tests/state_write_fixtures.rs rename to crates/libs/sbx/mgr/store/src/db/test_fixtures/state_write_fixtures.rs index 53df9dc7d..755fde81f 100644 --- a/crates/libs/sbx/mgr/server/src/db/tests/state_write_fixtures.rs +++ b/crates/libs/sbx/mgr/store/src/db/test_fixtures/state_write_fixtures.rs @@ -29,15 +29,15 @@ use crate::db::{ }; use crate::state::SandboxState; -pub(super) type Row = BTreeMap; +pub type Row = BTreeMap; /// Every row of every table, keyed by table name and primary key (`rowid` /// for a table without one), so a row deleted and re-inserted under a new key /// reads as a deletion and an insertion even when SQLite reuses its `rowid`. #[derive(Clone, Debug, PartialEq)] -pub(crate) struct Snapshot(BTreeMap>); +pub struct Snapshot(BTreeMap>); -pub(crate) fn snapshot(conn: &Connection) -> Snapshot { +pub fn snapshot(conn: &Connection) -> Snapshot { let tables: Vec = conn .prepare("SELECT name FROM sqlite_master WHERE type = 'table' AND name NOT LIKE 'sqlite_%'") .unwrap() @@ -101,7 +101,7 @@ pub(crate) fn snapshot(conn: &Connection) -> Snapshot { /// What one column of an expected row must hold after the write. #[derive(Clone, Debug)] -pub(super) enum Cell { +pub enum Cell { Is(Value), /// An epoch-seconds timestamp written by this write: text holding a value /// no earlier than the test's start. Seeds backdate every timestamp, so a @@ -114,15 +114,15 @@ pub(super) enum Cell { Fresh, } -pub(super) fn text(value: impl Into) -> Cell { +pub fn text(value: impl Into) -> Cell { Cell::Is(Value::Text(value.into())) } -pub(super) fn int(value: i64) -> Cell { +pub fn int(value: i64) -> Cell { Cell::Is(Value::Integer(value)) } -pub(super) const NULL: Cell = Cell::Is(Value::Null); +pub const NULL: Cell = Cell::Is(Value::Null); #[derive(Clone, Debug)] enum Change { @@ -143,18 +143,18 @@ enum Change { /// The exact set of row changes one write may make. #[derive(Clone, Debug, Default)] -pub(super) struct ExpectedDiff { +pub struct ExpectedDiff { changes: Vec, } impl ExpectedDiff { - pub(super) fn new() -> Self { + pub fn new() -> Self { Self::default() } /// The row of `table` whose `key.0` column equals `key.1` changes exactly /// the listed columns. - pub(super) fn updated( + pub fn updated( mut self, table: &'static str, key: (&'static str, &str), @@ -169,16 +169,12 @@ impl ExpectedDiff { } /// The `sandboxes` row `id` changes exactly the listed columns. - pub(super) fn sandbox( - self, - id: &str, - cells: impl IntoIterator, - ) -> Self { + pub fn sandbox(self, id: &str, cells: impl IntoIterator) -> Self { self.updated("sandboxes", ("id", id), cells) } /// One new row of `table`; every column must be listed. - pub(super) fn inserted( + pub fn inserted( mut self, table: &'static str, cells: impl IntoIterator, @@ -190,7 +186,7 @@ impl ExpectedDiff { self } - pub(super) fn deleted(mut self, table: &'static str, key: (&'static str, &str)) -> Self { + pub fn deleted(mut self, table: &'static str, key: (&'static str, &str)) -> Self { self.changes.push(Change::Deleted { table, key: (key.0, Value::Text(key.1.to_owned())), @@ -287,12 +283,12 @@ fn row_mismatch( /// Seconds since the epoch, captured before a write so [`Cell::NowText`] can /// tell a fresh timestamp from a seeded one. -pub(super) fn test_start() -> i64 { +pub fn test_start() -> i64 { jiff::Timestamp::now().as_second() } /// Assert that `after` differs from `before` by exactly `expected`. -pub(super) fn assert_diff( +pub fn assert_diff( case: &str, before: &Snapshot, after: &Snapshot, @@ -351,7 +347,7 @@ pub(super) fn assert_diff( } /// Assert that a refused write left every row of every table as it was. -pub(crate) fn assert_unchanged(case: &str, before: &Snapshot, conn: &Connection) { +pub fn assert_unchanged(case: &str, before: &Snapshot, conn: &Connection) { let after = snapshot(conn); let changed = actual_changes(before, &after); assert!( @@ -361,7 +357,7 @@ pub(crate) fn assert_unchanged(case: &str, before: &Snapshot, conn: &Connection) } /// Insert a plain sandbox row in `state`. -pub(crate) fn insert_row(conn: &Connection, state: SandboxState) -> String { +pub fn insert_row(conn: &Connection, state: SandboxState) -> String { let sandbox_id = SandboxId::new(); let id = sandbox_id.to_string(); insert_test_sandbox( @@ -378,7 +374,7 @@ pub(crate) fn insert_row(conn: &Connection, state: SandboxState) -> String { /// Move a seeded row to `state` without any writer's side effects. A /// `restoring` row carries a remote restore's three restore columns, which the /// table's `CHECK` ties to that state. -pub(crate) fn force_state(conn: &Connection, id: &str, state: SandboxState) { +pub fn force_state(conn: &Connection, id: &str, state: SandboxState) { let restoring = state == SandboxState::Restoring; conn.execute( "UPDATE sandboxes @@ -393,7 +389,7 @@ pub(crate) fn force_state(conn: &Connection, id: &str, state: SandboxState) { } /// Write `edge` with `update_state_from`, which must apply. -pub(crate) fn apply_edge(conn: &Connection, id: &str, edge: &'static Edge) { +pub fn apply_edge(conn: &Connection, id: &str, edge: &'static Edge) { let outcome = update_state_from(conn, id, edge).unwrap(); assert!( matches!(outcome, StateWriteOutcome::Applied { .. }), @@ -405,7 +401,7 @@ pub(crate) fn apply_edge(conn: &Connection, id: &str, edge: &'static Edge) { /// A non-blocking Stop's admission transaction, as the cleanup fence runs it: /// supersede any activation, write `stopping` and persist the `mark_stopped` /// intent. -pub(crate) fn admit_stop(conn: &Connection, id: &str) { +pub fn admit_stop(conn: &Connection, id: &str) { let tx = immediate_transaction(conn).unwrap(); update_state_superseding_activation_in_transaction(&tx, id, SandboxState::Stopping).unwrap(); assert!(set_cleanup_intent(&tx, id, true, CleanupCompletion::MarkStopped).unwrap()); @@ -413,26 +409,26 @@ pub(crate) fn admit_stop(conn: &Connection, id: &str) { } /// Stop's row-intent completion of [`admit_stop`]: `stopping` to `stopped`. -pub(crate) fn complete_stop(conn: &Connection, id: &str) { +pub fn complete_stop(conn: &Connection, id: &str) { complete_row_intent_cleanup_for_job(conn, id, CleanupCompletion::MarkStopped, None, None, None) .unwrap(); } /// A non-blocking Stop from admission to completion. -pub(crate) fn stop(conn: &Connection, id: &str) { +pub fn stop(conn: &Connection, id: &str) { admit_stop(conn, id); complete_stop(conn, id); } -pub(super) const RESTORE_STORAGE_URI: &str = "s3://snapshots/suspend/restore?region=us-east-1"; +pub const RESTORE_STORAGE_URI: &str = "s3://snapshots/suspend/restore?region=us-east-1"; /// The remote snapshot a seeded cross-node restore stages from. -pub(super) fn restore_source() -> pplx_space_common_core::types::ExactStorageUri { +pub fn restore_source() -> pplx_space_common_core::types::ExactStorageUri { RESTORE_STORAGE_URI.parse().unwrap() } /// Backdate every timestamp a writer may stamp, so a fresh stamp is visible. -pub(crate) fn backdate(conn: &Connection, id: &str) { +pub fn backdate(conn: &Connection, id: &str) { conn.execute( "UPDATE sandboxes SET state_changed_at = '1', updated_at = '1' WHERE id = ?1", params![id], @@ -443,12 +439,12 @@ pub(crate) fn backdate(conn: &Connection, id: &str) { /// A live runtime with its spaced endpoint proof and complete route identity: /// the columns a teardown writer clears and the route-retention trigger /// observes. -pub(super) const RUNTIME_ROUTE_GENERATION: i64 = 3; -pub(super) const RUNTIME_EXECUTION_GENERATION: i64 = 7; -pub(super) const RUNTIME_ACTIVATION_ATTEMPT: &str = "019a0000-0000-7000-8000-000000000001"; -pub(super) const RUNTIME_POD_UID: &str = "pod-uid-1"; +pub const RUNTIME_ROUTE_GENERATION: i64 = 3; +pub const RUNTIME_EXECUTION_GENERATION: i64 = 7; +pub const RUNTIME_ACTIVATION_ATTEMPT: &str = "019a0000-0000-7000-8000-000000000001"; +pub const RUNTIME_POD_UID: &str = "pod-uid-1"; -pub(crate) fn seed_runtime(conn: &Connection, id: &str) { +pub fn seed_runtime(conn: &Connection, id: &str) { conn.execute( "UPDATE sandboxes SET uid = 2001, slot = 1, guest_cid = 3, pid = 4242, @@ -479,7 +475,7 @@ pub(crate) fn seed_runtime(conn: &Connection, id: &str) { /// The columns a write that clears [`seed_runtime`]'s runtime and route fields /// changes, including the route-retention trigger's observation, with /// `retained` naming the route columns the write keeps. -pub(super) fn runtime_cleared(retained: &[&str]) -> Vec<(&'static str, Cell)> { +pub fn runtime_cleared(retained: &[&str]) -> Vec<(&'static str, Cell)> { [ ("uid", NULL), ("slot", NULL), @@ -525,7 +521,7 @@ pub(super) fn runtime_cleared(retained: &[&str]) -> Vec<(&'static str, Cell)> { /// A transition receipt targeting `target`, in the kind the table's `CHECK` /// admits for it: an activation completion for created, started and paused, /// otherwise an activation rollback. -pub(crate) fn seed_receipt(conn: &Connection, id: &str, target: SandboxState) { +pub fn seed_receipt(conn: &Connection, id: &str, target: SandboxState) { let (kind, reason) = match target { SandboxState::Created | SandboxState::Started | SandboxState::Paused => { ("activation_completed", None) @@ -551,7 +547,7 @@ pub(crate) fn seed_receipt(conn: &Connection, id: &str, target: SandboxState) { /// The receipt columns a write that clears [`seed_receipt`]'s receipt for /// `target` changes: an activation completion carries no reason code. -pub(super) fn receipt_cleared(target: SandboxState) -> Vec<(&'static str, Cell)> { +pub fn receipt_cleared(target: SandboxState) -> Vec<(&'static str, Cell)> { let mut cells = vec![ ("transition_receipt_id", NULL), ("transition_receipt_kind", NULL), @@ -565,7 +561,7 @@ pub(super) fn receipt_cleared(target: SandboxState) -> Vec<(&'static str, Cell)> cells } -pub(crate) fn set_failure_reason(conn: &Connection, id: &str, reason: &str) { +pub fn set_failure_reason(conn: &Connection, id: &str, reason: &str) { conn.execute( "UPDATE sandboxes SET failure_reason = ?2 WHERE id = ?1", params![id, reason], @@ -573,34 +569,29 @@ pub(crate) fn set_failure_reason(conn: &Connection, id: &str, reason: &str) { .unwrap(); } -pub(super) fn generation(value: i64) -> ExecutionGeneration { +pub fn generation(value: i64) -> ExecutionGeneration { ExecutionGeneration::new(value).unwrap() } /// Run `write` and assert it changed exactly `expected`. -pub(super) fn assert_accepts( - case: &str, - conn: &Connection, - write: impl FnOnce(), - expected: ExpectedDiff, -) { +pub fn assert_accepts(case: &str, conn: &Connection, write: impl FnOnce(), expected: ExpectedDiff) { let before = snapshot(conn); let start = test_start(); write(); assert_diff(case, &before, &snapshot(conn), start, &expected); } -pub(super) fn snapshot_path(id: &str) -> String { +pub fn snapshot_path(id: &str) -> String { format!("/cache/{id}/snapshot") } /// An active teardown-suspend job: a `deleting` claim must close it in the /// same transaction. -pub(super) fn seed_teardown_job(conn: &Connection, id: &str) { +pub fn seed_teardown_job(conn: &Connection, id: &str) { ensure_teardown_suspend_operation(conn, id, generation(7)).unwrap(); } -pub(super) fn teardown_job_failed(id: &str, error: &str) -> ExpectedDiff { +pub fn teardown_job_failed(id: &str, error: &str) -> ExpectedDiff { ExpectedDiff::new().updated( "jobs", ("subject_id", id), @@ -614,14 +605,11 @@ pub(super) fn teardown_job_failed(id: &str, error: &str) -> ExpectedDiff { } /// A suspend artifact row the writer inserts for `id`. -pub(super) fn suspend_artifact_inserted( - id: &str, - artifact: LocalArtifactId, -) -> Vec<(&'static str, Cell)> { +pub fn suspend_artifact_inserted(id: &str, artifact: LocalArtifactId) -> Vec<(&'static str, Cell)> { local_artifact_inserted(id, artifact, "full", "suspend", &snapshot_path(id)) } -pub(super) fn local_artifact_inserted( +pub fn local_artifact_inserted( id: &str, artifact: LocalArtifactId, kind: &str, @@ -642,11 +630,7 @@ pub(super) fn local_artifact_inserted( ] } -pub(super) fn pinned( - artifact: LocalArtifactId, - kind: &str, - origin: &str, -) -> Vec<(&'static str, Cell)> { +pub fn pinned(artifact: LocalArtifactId, kind: &str, origin: &str) -> Vec<(&'static str, Cell)> { vec![ ("current_local_artifact_id", text(artifact.to_string())), ("current_local_artifact_kind", text(kind)), @@ -654,7 +638,7 @@ pub(super) fn pinned( ] } -pub(super) fn unpinned() -> Vec<(&'static str, Cell)> { +pub fn unpinned() -> Vec<(&'static str, Cell)> { vec![ ("current_local_artifact_id", NULL), ("current_local_artifact_kind", NULL), @@ -664,7 +648,7 @@ pub(super) fn unpinned() -> Vec<(&'static str, Cell)> { /// Pin a suspend artifact without its `local_artifacts` row, as a re-suspend's /// predecessor carries it. -pub(super) fn set_suspend_pin(conn: &Connection, id: &str) -> LocalArtifactId { +pub fn set_suspend_pin(conn: &Connection, id: &str) -> LocalArtifactId { let pin = LocalArtifactId::new(); conn.execute( "UPDATE sandboxes @@ -680,7 +664,7 @@ pub(super) fn set_suspend_pin(conn: &Connection, id: &str) -> LocalArtifactId { /// Arm a cold start of a `stopped` row, as Start admits it, returning the /// activation's cleanup episode. -pub(crate) fn begin_cold_start(conn: &Connection, id: &str) -> CleanupEpisode { +pub fn begin_cold_start(conn: &Connection, id: &str) -> CleanupEpisode { begin_activation_with_route_evidence( conn, id, @@ -695,7 +679,7 @@ pub(crate) fn begin_cold_start(conn: &Connection, id: &str) -> CleanupEpisode { } /// A `suspended` row pinning its published suspend artifact. -pub(super) fn insert_suspended_with_artifact(conn: &Connection) -> (String, LocalArtifactId) { +pub fn insert_suspended_with_artifact(conn: &Connection) -> (String, LocalArtifactId) { let id = insert_row(conn, SandboxState::Paused); assert!(begin_suspend(conn, &id).unwrap()); let artifact = LocalArtifactId::new(); @@ -711,7 +695,7 @@ pub(super) fn insert_suspended_with_artifact(conn: &Connection) -> (String, Loca (id, artifact) } -pub(crate) fn decorate(conn: &Connection, id: &str, receipt: SandboxState) { +pub fn decorate(conn: &Connection, id: &str, receipt: SandboxState) { set_failure_reason(conn, id, "seeded failure"); seed_receipt(conn, id, receipt); backdate(conn, id); @@ -719,7 +703,7 @@ pub(crate) fn decorate(conn: &Connection, id: &str, receipt: SandboxState) { /// Build the insert parameters of an ordinary sandbox `id` in `state`, let /// `customize` adjust them, and pass them to `insert`. -pub(super) fn with_sandbox_params( +pub fn with_sandbox_params( id: &str, state: SandboxState, customize: impl FnOnce(&mut InsertSandboxParams<'_>), @@ -755,7 +739,7 @@ pub(super) fn with_sandbox_params( /// The synthetic row the orphan reconciler materializes for a cache it cannot /// match to a sandbox. -pub(super) fn as_synthetic_orphan(params: &mut InsertSandboxParams<'_>) { +pub fn as_synthetic_orphan(params: &mut InsertSandboxParams<'_>) { params.template = ORPHANED_SANDBOX_TEMPLATE; params.template_hash = None; params.user = &ORPHANED_SANDBOX_OWNER; @@ -764,7 +748,7 @@ pub(super) fn as_synthetic_orphan(params: &mut InsertSandboxParams<'_>) { /// A creating row owning an armed create episode with `completion`, bound to /// `execution` when the create carries a route identity. -pub(super) fn insert_creating_with( +pub fn insert_creating_with( conn: &Connection, completion: CleanupCompletion, execution: Option<(ExecutionGeneration, SandboxActivationAttemptId)>, @@ -798,7 +782,7 @@ pub(super) fn insert_creating_with( (id, cleanup_id) } -pub(crate) fn insert_creating( +pub fn insert_creating( conn: &Connection, completion: CleanupCompletion, ) -> (String, SandboxCleanupEpisodeId) { @@ -806,7 +790,7 @@ pub(crate) fn insert_creating( } /// Every column of a fresh `sandbox_quarantine_state` row. -pub(super) fn quarantine_state_entered( +pub fn quarantine_state_entered( id: &str, from: &str, reason: SandboxQuarantineReasonCode, @@ -833,7 +817,7 @@ pub(super) fn quarantine_state_entered( } /// Every column of the history row a fresh quarantine episode records. -pub(super) fn quarantine_history_entered( +pub fn quarantine_history_entered( id: &str, reason: SandboxQuarantineReasonCode, message: &str, @@ -858,7 +842,7 @@ pub(super) fn quarantine_history_entered( /// A spaced endpoint proof for `sandbox_id`, matching [`seed_runtime`]'s /// runtime and route identity. -pub(super) fn endpoint_proof(sandbox_id: SandboxId) -> crate::endpoint_proof::SpacedEndpointProof { +pub fn endpoint_proof(sandbox_id: SandboxId) -> crate::endpoint_proof::SpacedEndpointProof { use crate::endpoint_proof::{ ProcessActivationIdentity, SpacedEndpointProof, VsockActivationIdentity, }; @@ -888,6 +872,6 @@ pub(super) fn endpoint_proof(sandbox_id: SandboxId) -> crate::endpoint_proof::Sp } /// The execution metadata quarantine adoption records. -pub(super) fn execution_metadata() -> crate::db::ExecutionMetadata { +pub fn execution_metadata() -> crate::db::ExecutionMetadata { crate::db::ExecutionMetadata::initial_firecracker(Arch::Aarch64, vec![]) } diff --git a/crates/libs/sbx/mgr/server/src/db/test_schema_image_cache.rs b/crates/libs/sbx/mgr/store/src/db/test_schema_image_cache.rs similarity index 100% rename from crates/libs/sbx/mgr/server/src/db/test_schema_image_cache.rs rename to crates/libs/sbx/mgr/store/src/db/test_schema_image_cache.rs diff --git a/crates/libs/sbx/mgr/server/src/db/tests.rs b/crates/libs/sbx/mgr/store/src/db/tests.rs similarity index 97% rename from crates/libs/sbx/mgr/server/src/db/tests.rs rename to crates/libs/sbx/mgr/store/src/db/tests.rs index 2ba24a0d4..c79d463c3 100644 --- a/crates/libs/sbx/mgr/server/src/db/tests.rs +++ b/crates/libs/sbx/mgr/store/src/db/tests.rs @@ -1,5 +1,10 @@ mod state_writer_refusal; +pub(crate) use super::test_fixtures::{ + TestSandboxInsert, database_rows, insert_test_sandbox, quarantine_claim_state, + record_quarantine_recovery_attempt, setup_quarantine_recovery_candidate, + setup_quarantine_recovery_episode, state_write_fixtures, +}; use super::*; use crate::restore_operation::RestoreOperation; @@ -521,145 +526,6 @@ fn retained_route_provenance_columns( .unwrap() } -/// Build a quarantine-recovery episode for the loop's mechanics to act on. -/// -/// For any reason other than `orphaned_sandbox_row_missing` this both names a -/// full/suspend artifact on the sandbox row and registers it in -/// `local_artifacts`. Production reaches the same *loop-visible* state by a -/// different route: the registry row is reaped while the sandbox row keeps -/// naming the artifact, a restore then fails, and its cleanup quarantines -/// because it cannot account for the artifact. The scan and the claim read only -/// the sandbox row's columns, which are identical either way, and the adoption -/// upserts the registry row rather than requiring it. What no writer produces is -/// the divergence that would let a *failed restore* arrive in this shape — see -/// `a_failed_restore_recovers_only_if_the_two_artifact_stores_diverge`. So tests -/// built on this helper cover the fence, the backoff and the adoption, which are -/// reason-agnostic; do not cite one as evidence that a failed restore recovers. -pub(crate) fn setup_quarantine_recovery_episode( - conn: &Connection, - reason_code: SandboxQuarantineReasonCode, -) -> SandboxId { - let sandbox_id = SandboxId::new(); - let sandbox_id_string = sandbox_id.to_string(); - insert_test_sandbox( - conn, - TestSandboxInsert::new( - &sandbox_id_string, - SandboxRuntimeLabel::from_sandbox_id(sandbox_id).to_string(), - SandboxState::Restoring, - ), - ); - if reason_code != SandboxQuarantineReasonCode::OrphanedSandboxRowMissing { - let local_artifact_id = LocalArtifactId::new(); - insert_local_artifact( - conn, - &sandbox_id_string, - local_artifact_id, - ArtifactKind::Full, - ArtifactOrigin::Suspend, - None, - "/space/cache/sandboxes/test/quarantine-recovery", - ) - .unwrap(); - conn.execute( - "UPDATE sandboxes - SET current_local_artifact_id = ?1, - current_local_artifact_kind = 'full', - current_local_artifact_origin = 'suspend', - space_api_route_execution_generation = 7 - WHERE id = ?2", - params![local_artifact_id.to_string(), sandbox_id_string], - ) - .unwrap(); - } - assert!( - mark_restoring_quarantined( - conn, - &sandbox_id_string, - reason_code, - "restore rollout interrupted", - ) - .unwrap() - ); - sandbox_id -} - -/// `(next_action_owner, attempt_count, next_action_after, claim_token, claim_expires_at)` -/// for one quarantine episode — the fields legacy recovery leases against. -pub(crate) fn quarantine_claim_state( - conn: &Connection, - sandbox_id: SandboxId, -) -> ( - Option, - i64, - Option, - Option, - Option, -) { - conn.query_row( - "SELECT next_action_owner, attempt_count, next_action_after, - claim_token, claim_expires_at - FROM sandbox_quarantine_state - WHERE sandbox_id = ?1", - params![sandbox_id.to_string()], - |row| { - Ok(( - row.get(0)?, - row.get(1)?, - row.get(2)?, - row.get(3)?, - row.get(4)?, - )) - }, - ) - .unwrap() -} - -pub(crate) fn setup_quarantine_recovery_candidate( - conn: &Connection, -) -> (SandboxId, QuarantineRecoveryCandidate) { - let sandbox_id = setup_quarantine_recovery_episode( - conn, - SandboxQuarantineReasonCode::RestoreFirecrackerLaunchFailed, - ); - let [candidate] = scan_quarantine_recovery_candidates(conn) - .unwrap() - .candidates - .try_into() - .unwrap(); - (sandbox_id, candidate) -} - -/// Every row of every table, order-insensitive. -pub(crate) fn database_rows(conn: &Connection) -> std::collections::BTreeMap> { - let tables: Vec = conn - .prepare("SELECT name FROM sqlite_master WHERE type = 'table' AND name NOT LIKE 'sqlite_%'") - .unwrap() - .query_map([], |row| row.get(0)) - .unwrap() - .collect::>() - .unwrap(); - tables - .into_iter() - .map(|table| { - let mut statement = conn.prepare(&format!("SELECT * FROM \"{table}\"")).unwrap(); - let width = statement.column_count(); - let mut rows: Vec = statement - .query_map([], |row| { - (0..width) - .map(|column| row.get::<_, rusqlite::types::Value>(column)) - .collect::>>() - .map(|values| format!("{values:?}")) - }) - .unwrap() - .collect::>() - .unwrap(); - rows.sort(); - (table, rows) - }) - .collect() -} - #[test] fn quarantine_recovery_candidates_follow_the_complete_reason_policy() { let reasons = [ @@ -1168,77 +1034,6 @@ fn access_idle_policy() -> AutoLifecyclePolicy { .normalized() } -struct TestSandboxInsert { - id: String, - uuid8: String, - state: SandboxState, - user: String, - rootfs_size_mb: Option, - disk_io_limits: pplx_space_common_core::types::DiskIoLimits, - pool_template: Option, - volume_encryption: bool, - process_env: BTreeMap, - auto_lifecycle: AutoLifecyclePolicy, - network_policy_override: Option, - network_policy_generation: u64, - network_policy_authority: PolicyAuthority, - auto_lifecycle_authority: PolicyAuthority, -} - -impl TestSandboxInsert { - fn new(id: impl Into, uuid8: impl Into, state: SandboxState) -> Self { - Self { - id: id.into(), - uuid8: uuid8.into(), - state, - user: "user".to_string(), - rootfs_size_mb: None, - disk_io_limits: Default::default(), - pool_template: None, - volume_encryption: false, - process_env: BTreeMap::new(), - auto_lifecycle: AutoLifecyclePolicy::disabled(), - network_policy_override: None, - network_policy_generation: 0, - network_policy_authority: PolicyAuthority::Cluster, - auto_lifecycle_authority: PolicyAuthority::Cluster, - } - } - - fn insert_params<'a>( - &'a self, - uuid8: &'a SandboxRuntimeLabel, - user: &'a SandboxOwner, - ) -> InsertSandboxParams<'a> { - InsertSandboxParams { - id: &self.id, - uuid8, - state: self.state, - template: "seed", - template_hash: Some("hash"), - user, - runtime: "fc", - snapshots_enabled: false, - rootfs_size_mb: self.rootfs_size_mb, - disk_io_limits: self.disk_io_limits, - pool_template: self.pool_template.as_deref(), - volume_encryption: self.volume_encryption, - process_env: &self.process_env, - auto_lifecycle: &self.auto_lifecycle, - network_policy_override: self.network_policy_override.as_ref(), - network_policy_generation: self.network_policy_generation, - network_policy_authority: self.network_policy_authority, - auto_lifecycle_authority: self.auto_lifecycle_authority, - } - } -} - -fn insert_test_sandbox(conn: &Connection, params: TestSandboxInsert) { - let uuid8 = SandboxRuntimeLabel::parse(params.uuid8.clone()).unwrap(); - let user = SandboxOwner::parse(params.user.clone()).unwrap(); - insert_sandbox(conn, params.insert_params(&uuid8, &user)).unwrap(); -} - #[test] fn sandbox_lifecycle_counts_groups_durable_rows() { let conn = test_conn(); @@ -4506,31 +4301,6 @@ fn quarantine_recovery_no_progress_schedules_the_capped_doubling_backoff() { } } -/// Claim the episode's due candidate and record `reason` against the claim, -/// as one recovery pass does: the claim carries the reason's effect on the run -/// of plan rejections, and the no-progress write records the reason. -pub(crate) fn record_quarantine_recovery_attempt( - conn: &Connection, - sandbox_id: SandboxId, - reason: QuarantineRecoveryNoProgressReason, -) -> QuarantineRecoveryNoProgressOutcome { - conn.execute( - "UPDATE sandbox_quarantine_state SET next_action_after = NULL WHERE sandbox_id = ?1", - params![sandbox_id.to_string()], - ) - .unwrap(); - let [candidate] = scan_quarantine_recovery_candidates(conn) - .unwrap() - .candidates - .try_into() - .expect("a due episode is a candidate"); - let claimed = - claim_quarantine_recovery_candidate(conn, &candidate, PlanRejectionRunEffect::of(reason)) - .unwrap() - .expect("a due candidate is claimable"); - record_quarantine_recovery_no_progress(conn, &claimed, reason).unwrap() -} - /// This test's own classification of every no-progress reason, with a distinct /// index per variant so `ALL_NO_PROGRESS_REASONS` is checked for completeness. /// Wildcard-free: a new reason does not compile until it is placed here. @@ -6620,10 +6390,6 @@ fn mark_start_succeeded_requires_starting_state() { assert!(started.current_local_artifact_id.is_none()); assert!(started.current_local_artifact_kind.is_none()); assert!(started.current_local_artifact_origin.is_none()); - assert!( - crate::snapshot_encryption::capture_volume_is_fresh(&started), - "a successful cold start must not reuse an older suspend memory image" - ); assert_eq!(deleting.state, SandboxState::Deleting); let receipt = sandbox_state_publication_metadata(&conn, std::slice::from_ref(&receipt_sandbox_id)) @@ -8563,7 +8329,8 @@ struct DbModuleFile { /// The `db` module's non-test source files, read from the crate at test time: /// `db.rs` and every `.rs` file under `db/` except test modules (`tests.rs`, -/// `*_tests.rs`, `test_*.rs` and anything under a `tests/` directory). Listing +/// `*_tests.rs`, `test_*.rs` and anything under a `tests/` or `test_*/` +/// directory, such as the `test-support` fixtures). Listing /// the directory rather than naming the files means a new module is scanned by /// the source-shape guards below without anyone registering it. fn db_module_files() -> Vec { @@ -8578,7 +8345,11 @@ fn db_module_files() -> Vec { for entry in std::fs::read_dir(dir).expect("read db module directory") { let path = entry.expect("read db module directory entry").path(); if path.is_dir() { - if path.file_name().is_some_and(|name| name != "tests") { + if path + .file_name() + .and_then(|name| name.to_str()) + .is_some_and(|name| name != "tests" && !name.starts_with("test_")) + { collect(&path, into); } } else if path.extension().is_some_and(|extension| extension == "rs") @@ -10097,7 +9868,6 @@ mod accepted_shapes_quarantine; mod accepted_shapes_single_statement; mod cleanup_quarantine_refusal; mod local_network_policy_clear; -pub(crate) mod state_write_fixtures; mod state_write_table; mod stopped_artifact_pin; mod thin_writer_coverage; diff --git a/crates/libs/sbx/mgr/server/src/db/tests/accepted_shapes_cleanup_episodes.rs b/crates/libs/sbx/mgr/store/src/db/tests/accepted_shapes_cleanup_episodes.rs similarity index 100% rename from crates/libs/sbx/mgr/server/src/db/tests/accepted_shapes_cleanup_episodes.rs rename to crates/libs/sbx/mgr/store/src/db/tests/accepted_shapes_cleanup_episodes.rs diff --git a/crates/libs/sbx/mgr/server/src/db/tests/accepted_shapes_quarantine.rs b/crates/libs/sbx/mgr/store/src/db/tests/accepted_shapes_quarantine.rs similarity index 100% rename from crates/libs/sbx/mgr/server/src/db/tests/accepted_shapes_quarantine.rs rename to crates/libs/sbx/mgr/store/src/db/tests/accepted_shapes_quarantine.rs diff --git a/crates/libs/sbx/mgr/server/src/db/tests/accepted_shapes_single_statement.rs b/crates/libs/sbx/mgr/store/src/db/tests/accepted_shapes_single_statement.rs similarity index 100% rename from crates/libs/sbx/mgr/server/src/db/tests/accepted_shapes_single_statement.rs rename to crates/libs/sbx/mgr/store/src/db/tests/accepted_shapes_single_statement.rs diff --git a/crates/libs/sbx/mgr/server/src/db/tests/cleanup_quarantine_refusal.rs b/crates/libs/sbx/mgr/store/src/db/tests/cleanup_quarantine_refusal.rs similarity index 100% rename from crates/libs/sbx/mgr/server/src/db/tests/cleanup_quarantine_refusal.rs rename to crates/libs/sbx/mgr/store/src/db/tests/cleanup_quarantine_refusal.rs diff --git a/crates/libs/sbx/mgr/server/src/db/tests/execution_binding.rs b/crates/libs/sbx/mgr/store/src/db/tests/execution_binding.rs similarity index 100% rename from crates/libs/sbx/mgr/server/src/db/tests/execution_binding.rs rename to crates/libs/sbx/mgr/store/src/db/tests/execution_binding.rs diff --git a/crates/libs/sbx/mgr/server/src/db/tests/local_network_policy_clear.rs b/crates/libs/sbx/mgr/store/src/db/tests/local_network_policy_clear.rs similarity index 100% rename from crates/libs/sbx/mgr/server/src/db/tests/local_network_policy_clear.rs rename to crates/libs/sbx/mgr/store/src/db/tests/local_network_policy_clear.rs diff --git a/crates/libs/sbx/mgr/server/src/db/tests/state_write_table.rs b/crates/libs/sbx/mgr/store/src/db/tests/state_write_table.rs similarity index 100% rename from crates/libs/sbx/mgr/server/src/db/tests/state_write_table.rs rename to crates/libs/sbx/mgr/store/src/db/tests/state_write_table.rs diff --git a/crates/libs/sbx/mgr/server/src/db/tests/state_writer_refusal.rs b/crates/libs/sbx/mgr/store/src/db/tests/state_writer_refusal.rs similarity index 100% rename from crates/libs/sbx/mgr/server/src/db/tests/state_writer_refusal.rs rename to crates/libs/sbx/mgr/store/src/db/tests/state_writer_refusal.rs diff --git a/crates/libs/sbx/mgr/server/src/db/tests/stopped_artifact_pin.rs b/crates/libs/sbx/mgr/store/src/db/tests/stopped_artifact_pin.rs similarity index 100% rename from crates/libs/sbx/mgr/server/src/db/tests/stopped_artifact_pin.rs rename to crates/libs/sbx/mgr/store/src/db/tests/stopped_artifact_pin.rs diff --git a/crates/libs/sbx/mgr/server/src/db/tests/thin_writer_coverage.rs b/crates/libs/sbx/mgr/store/src/db/tests/thin_writer_coverage.rs similarity index 100% rename from crates/libs/sbx/mgr/server/src/db/tests/thin_writer_coverage.rs rename to crates/libs/sbx/mgr/store/src/db/tests/thin_writer_coverage.rs diff --git a/crates/libs/sbx/mgr/server/src/db/tests/update_state_from.rs b/crates/libs/sbx/mgr/store/src/db/tests/update_state_from.rs similarity index 100% rename from crates/libs/sbx/mgr/server/src/db/tests/update_state_from.rs rename to crates/libs/sbx/mgr/store/src/db/tests/update_state_from.rs diff --git a/crates/libs/sbx/mgr/server/src/db/tests/update_state_matrix.rs b/crates/libs/sbx/mgr/store/src/db/tests/update_state_matrix.rs similarity index 100% rename from crates/libs/sbx/mgr/server/src/db/tests/update_state_matrix.rs rename to crates/libs/sbx/mgr/store/src/db/tests/update_state_matrix.rs diff --git a/crates/libs/sbx/mgr/server/src/db/timestamps.rs b/crates/libs/sbx/mgr/store/src/db/timestamps.rs similarity index 93% rename from crates/libs/sbx/mgr/server/src/db/timestamps.rs rename to crates/libs/sbx/mgr/store/src/db/timestamps.rs index 72b1e4020..a593c8dbd 100644 --- a/crates/libs/sbx/mgr/server/src/db/timestamps.rs +++ b/crates/libs/sbx/mgr/store/src/db/timestamps.rs @@ -8,14 +8,14 @@ use rusqlite::params; use rusqlite::types::{FromSql, FromSqlError, FromSqlResult, Value, ValueRef}; #[derive(Clone, Copy, Debug, Eq, PartialEq)] -pub(crate) struct EpochSeconds(i64); +pub struct EpochSeconds(i64); #[derive(Debug, thiserror::Error)] #[error("expected whole nonnegative Unix seconds or a whole-second UTC timestamp")] -pub(crate) struct InvalidTimestamp; +pub struct InvalidTimestamp; impl EpochSeconds { - pub(crate) fn parse(text: &str) -> std::result::Result { + pub fn parse(text: &str) -> std::result::Result { if !text.is_empty() && text.bytes().all(|byte| byte.is_ascii_digit()) { return text.parse().map(Self).map_err(|_| InvalidTimestamp); } @@ -36,11 +36,11 @@ impl EpochSeconds { Self::column_result(ValueRef::from(value)) } - pub(crate) fn get(self) -> i64 { + pub fn get(self) -> i64 { self.0 } - pub(crate) fn unsigned(self) -> u64 { + pub fn unsigned(self) -> u64 { self.0.cast_unsigned() } } @@ -89,7 +89,7 @@ pub(super) fn diagnostic_column(row: &rusqlite::Row<'_>, index: usize) -> rusqli /// default. A fixed rowid ceiling prevents new scopes extending a sweep forever; /// malformed evidence is retained and cannot prevent later rows being visited. #[derive(Debug)] -pub(crate) struct RestoreScopeTimestampScan { +pub struct RestoreScopeTimestampScan { next: i64, through: Option, } @@ -104,7 +104,7 @@ impl Default for RestoreScopeTimestampScan { } impl RestoreScopeTimestampScan { - pub(crate) fn next_batch(&mut self, conn: &Connection) -> Result { + pub fn next_batch(&mut self, conn: &Connection) -> Result { let result = self.normalize_batch(conn); if result.is_err() { *self = Self::default(); diff --git a/crates/libs/sbx/mgr/server/src/db/timestamps/tests.rs b/crates/libs/sbx/mgr/store/src/db/timestamps/tests.rs similarity index 100% rename from crates/libs/sbx/mgr/server/src/db/timestamps/tests.rs rename to crates/libs/sbx/mgr/store/src/db/timestamps/tests.rs diff --git a/crates/libs/sbx/mgr/server/src/db/transfer.rs b/crates/libs/sbx/mgr/store/src/db/transfer.rs similarity index 95% rename from crates/libs/sbx/mgr/server/src/db/transfer.rs rename to crates/libs/sbx/mgr/store/src/db/transfer.rs index 11d498019..77fc4b830 100644 --- a/crates/libs/sbx/mgr/server/src/db/transfer.rs +++ b/crates/libs/sbx/mgr/store/src/db/transfer.rs @@ -9,14 +9,14 @@ use super::*; #[derive(Debug, thiserror::Error)] -pub(crate) enum ReserveImportError { +pub enum ReserveImportError { #[error("reserve sandbox import: {0}")] Database(#[from] Error), #[error("claim sandbox cache: {0}")] Claim(#[from] std::io::Error), } -pub(crate) struct ImportReservation<'a> { +pub struct ImportReservation<'a> { pub params: InsertSandboxParams<'a>, pub episode: CleanupEpisodeSpec, pub execution: &'a ExecutionMetadata, @@ -25,7 +25,7 @@ pub(crate) struct ImportReservation<'a> { pub cache_root: &'a Path, } -pub(crate) fn reserve_import( +pub fn reserve_import( conn: &Connection, reservation: ImportReservation<'_>, ) -> std::result::Result<(), ReserveImportError> { @@ -54,7 +54,7 @@ pub(crate) fn reserve_import( Ok(()) } -pub(crate) fn complete_import( +pub fn complete_import( conn: &Connection, id: &str, cleanup_id: SandboxCleanupEpisodeId, @@ -102,7 +102,7 @@ pub(crate) fn preserve_import_timestamps( Ok(()) } -pub(crate) struct SnapshotPublication<'a> { +pub struct SnapshotPublication<'a> { pub id: &'a str, pub template: &'a str, pub hash: &'a str, @@ -112,7 +112,7 @@ pub(crate) struct SnapshotPublication<'a> { pub verified_existing: bool, } -pub(crate) fn publish_snapshot( +pub fn publish_snapshot( conn: &Connection, publication: SnapshotPublication<'_>, ) -> std::result::Result { @@ -242,14 +242,14 @@ pub(crate) fn publish_snapshot( } #[derive(serde::Serialize, serde::Deserialize, PartialEq, Eq)] -pub(crate) struct TransferBinding { +pub struct TransferBinding { pub sandbox_id: String, pub template: String, pub content_hash: String, pub stream_fingerprint: String, } -pub(crate) fn validate_snapshot_binding(path: &Path, sandbox: &SandboxRow) -> std::io::Result<()> { +pub fn validate_snapshot_binding(path: &Path, sandbox: &SandboxRow) -> std::io::Result<()> { let bytes = match std::fs::read(path.join(".transfer.json")) { Ok(bytes) => bytes, Err(error) if error.kind() == std::io::ErrorKind::NotFound => return Ok(()), @@ -269,7 +269,7 @@ pub(crate) fn validate_snapshot_binding(path: &Path, sandbox: &SandboxRow) -> st Ok(()) } -pub(crate) fn sync_import_directory(path: &Path) -> std::io::Result<()> { +pub fn sync_import_directory(path: &Path) -> std::io::Result<()> { for entry in std::fs::read_dir(path)? { let entry = entry?; let metadata = entry.file_type()?; @@ -287,7 +287,7 @@ pub(crate) fn sync_import_directory(path: &Path) -> std::io::Result<()> { } /// Publication must not replace even an empty destination or dangling symlink. -pub(crate) fn rename_unoccupied(source: &Path, destination: &Path) -> std::io::Result<()> { +pub fn rename_unoccupied(source: &Path, destination: &Path) -> std::io::Result<()> { #[cfg(any(target_os = "linux", target_os = "macos"))] { rustix::fs::renameat_with( diff --git a/crates/libs/sbx/mgr/server/src/endpoint_proof.rs b/crates/libs/sbx/mgr/store/src/endpoint_proof.rs similarity index 100% rename from crates/libs/sbx/mgr/server/src/endpoint_proof.rs rename to crates/libs/sbx/mgr/store/src/endpoint_proof.rs diff --git a/crates/libs/sbx/mgr/server/src/endpoint_proof/tests.rs b/crates/libs/sbx/mgr/store/src/endpoint_proof/tests.rs similarity index 100% rename from crates/libs/sbx/mgr/server/src/endpoint_proof/tests.rs rename to crates/libs/sbx/mgr/store/src/endpoint_proof/tests.rs diff --git a/crates/libs/sbx/mgr/store/src/lib.rs b/crates/libs/sbx/mgr/store/src/lib.rs new file mode 100644 index 000000000..8c61e80e9 --- /dev/null +++ b/crates/libs/sbx/mgr/store/src/lib.rs @@ -0,0 +1,25 @@ +//! sbx-mgr's node-local SQLite store: the `sandboxes` table and every other +//! sbx-mgr table, not the control-plane Postgres. +//! +//! This crate is the only code that opens, migrates or queries that +//! database. [`db`] holds its statements, its handle, transaction and error +//! types, and its migration entry point; the other modules are the domain +//! types its rows decode into. Callers hold only the store's own types: outside +//! the `test-support` feature, no public item returns, exposes or re-exports a +//! rusqlite connection, transaction, statement or row, so a caller cannot run +//! SQL of its own. The rusqlite types left in the public API are trait impls: +//! the `From` conversions into the store's errors, which take +//! a rusqlite error in, and `FromSql` for a timestamp column type, whose +//! `column_result` returns rusqlite's `FromSqlError` to a caller that already +//! holds a rusqlite `ValueRef`: an error type going out, not a database +//! capability. + +#![forbid(unsafe_code)] +#![cfg_attr(not(test), deny(clippy::expect_used))] + +pub mod auto_lifecycle; +pub mod auto_lifecycle_delete_receipts; +pub mod db; +pub mod endpoint_proof; +pub mod restore_operation; +pub mod state; diff --git a/crates/libs/sbx/mgr/server/src/restore_operation.rs b/crates/libs/sbx/mgr/store/src/restore_operation.rs similarity index 100% rename from crates/libs/sbx/mgr/server/src/restore_operation.rs rename to crates/libs/sbx/mgr/store/src/restore_operation.rs diff --git a/crates/libs/sbx/mgr/server/src/state.rs b/crates/libs/sbx/mgr/store/src/state.rs similarity index 99% rename from crates/libs/sbx/mgr/server/src/state.rs rename to crates/libs/sbx/mgr/store/src/state.rs index 9fa44ec00..5594f5e16 100644 --- a/crates/libs/sbx/mgr/server/src/state.rs +++ b/crates/libs/sbx/mgr/store/src/state.rs @@ -139,7 +139,7 @@ impl SandboxState { } /// Both `Started` and `Paused` sandboxes may have associated network gateways. - pub(crate) const fn can_cycle_net_gateway(self) -> bool { + pub const fn can_cycle_net_gateway(self) -> bool { matches!(self, Self::Started | Self::Paused) } @@ -386,7 +386,7 @@ impl std::str::FromStr for SandboxState { } } -impl From for crate::proto::SandboxLifecycleState { +impl From for pplx_space_sbx_mgr_client::proto::SandboxLifecycleState { fn from(state: SandboxState) -> Self { match state { SandboxState::Creating => Self::Creating, diff --git a/crates/libs/sbx/mgr/server/src/state/tests.rs b/crates/libs/sbx/mgr/store/src/state/tests.rs similarity index 96% rename from crates/libs/sbx/mgr/server/src/state/tests.rs rename to crates/libs/sbx/mgr/store/src/state/tests.rs index ad4419f04..052f29677 100644 --- a/crates/libs/sbx/mgr/server/src/state/tests.rs +++ b/crates/libs/sbx/mgr/store/src/state/tests.rs @@ -21,14 +21,15 @@ fn pause_admission_retries_in_flight_lifecycle_but_rejects_invalid_stable_states #[test] fn every_local_lifecycle_round_trips_through_typed_observations() { for state in super::SandboxState::all() { - let wire = crate::proto::SandboxLifecycleState::from(state); + let wire = pplx_space_sbx_mgr_client::proto::SandboxLifecycleState::from(state); assert_eq!(wire.label(), state.as_str()); assert_eq!( state.as_str().parse::().unwrap(), state ); assert_eq!( - crate::proto::SandboxLifecycleState::from_label(state.as_str()).unwrap(), + pplx_space_sbx_mgr_client::proto::SandboxLifecycleState::from_label(state.as_str()) + .unwrap(), wire ); } diff --git a/crates/tools/archcheck/README.md b/crates/tools/archcheck/README.md index 3853138c6..59f6b5bcf 100644 --- a/crates/tools/archcheck/README.md +++ b/crates/tools/archcheck/README.md @@ -261,16 +261,42 @@ history, a module's reach is limited to its crate, `file_size` already bounds how much one can hold, and a check on module names is easy to evade (`util.rs` becomes `support.rs`) without changing what the module holds. +## Dependency fences + +`dependency_fence` keeps packages out of workspace members that must not +depend on them. Each `[rules.dependency_fence.]` lists Cargo +`packages`, the `members` it checks and the members it `allow`s, as globs over +member directories: + +```toml +[rules.dependency_fence.sbxmgr_rusqlite] +packages = ["libsqlite3-sys", "rusqlite"] +members = ["crates/libs/sbx/mgr/**", "crates/apps/sbx/**"] +allow = ["crates/libs/sbx/mgr/store"] + +[rules.dependency_fence.sbxmgr_rusqlite.no_crossing] +mode = "required" +``` + +`no_crossing` reports each dependency a checked member outside `allow` +declares on a listed package: normal, dev or build, for any target, and under +any name. The records come from `cargo metadata`, which names the depended-on +package even when `package = "…"` renames it, so a fence does not depend on +how a manifest is written. Each finding points at the manifest line that +declares the dependency. There is no baseline: an existing dependency is +removed or its member allowed. An empty `packages` or `members`, or a +`members` or `allow` glob that matches no workspace member, exits 2. + ## Configuration and modes Each behaviour has a `mode`: `advisory` findings are reported, `required` ones also fail the check, so behaviours can graduate one at a time. Unknown keys, sections or modes, a missing file, bad TOML, an invalid regex, glob or instance -name, an empty `patterns` or `paths` list, a baseline table for an instance -that is not configured, and a failing `cargo metadata` exit 2. So does a -`--base-baseline`, `--renames` or `--base-architecture` that can't be read, or -one combined with `--write-baseline`, and `--renames` input that is not git's -(see [Renamed files](#renamed-files)). +name, an empty `patterns`, `paths`, `packages` or `members` list, a baseline +table for an instance that is not configured, and a failing `cargo metadata` +exit 2. So does a `--base-baseline`, `--renames` or `--base-architecture` that +can't be read, or one combined with `--write-baseline`, and `--renames` input +that is not git's (see [Renamed files](#renamed-files)). ## Baseline diff --git a/crates/tools/archcheck/src/config.rs b/crates/tools/archcheck/src/config.rs index fb400d9e1..ffb716918 100644 --- a/crates/tools/archcheck/src/config.rs +++ b/crates/tools/archcheck/src/config.rs @@ -11,7 +11,7 @@ use std::{ path::Path, }; -use globset::{GlobBuilder, GlobSet, GlobSetBuilder}; +use globset::{GlobBuilder, GlobMatcher, GlobSet, GlobSetBuilder}; use regex::RegexSet; use serde::Deserialize; use thiserror::Error; @@ -39,6 +39,9 @@ pub struct RulesConfig { #[serde(default)] pub pattern_ratchet: BTreeMap, pub crate_names: CrateNamesConfig, + /// `[rules.dependency_fence.]` tables; none when absent. + #[serde(default)] + pub dependency_fence: BTreeMap, } #[derive(Debug, Clone, Deserialize)] @@ -75,6 +78,119 @@ pub struct PatternRatchetConfig { pub stale_entries: BehaviourConfig, } +/// One named instance of `dependency_fence`. +#[derive(Debug, Clone, Deserialize)] +#[serde(deny_unknown_fields)] +pub struct DependencyFenceConfig { + /// Cargo package names the fenced members may not depend on. + pub packages: Packages, + /// Globs over workspace member directories, repository-relative: the + /// members the instance checks. + pub members: MemberGlobs, + /// Globs over member directories that may depend on the packages. + pub allow: MemberGlobList, + pub no_crossing: BehaviourConfig, +} + +/// Package names, at least one. +#[derive(Debug, Clone, Deserialize)] +#[serde(try_from = "Vec")] +pub struct Packages(BTreeSet); + +#[derive(Debug, Error)] +#[error("packages must list at least one package name")] +pub struct PackagesError; + +impl Packages { + pub fn contains(&self, package: &str) -> bool { + self.0.contains(package) + } +} + +impl TryFrom> for Packages { + type Error = PackagesError; + + fn try_from(packages: Vec) -> Result { + if packages.is_empty() { + return Err(PackagesError); + } + Ok(Self(packages.into_iter().collect())) + } +} + +/// Globs over workspace member directories, each kept with its text so that +/// one matching no member can be named. +#[derive(Debug, Clone, Deserialize)] +#[serde(try_from = "Vec")] +pub struct MemberGlobList(Vec<(String, GlobMatcher)>); + +impl MemberGlobList { + pub fn is_match(&self, member: &str) -> bool { + self.0.iter().any(|(_, glob)| glob.is_match(member)) + } + + /// The first glob that matches none of `members`. + pub fn unmatched<'a>(&self, members: impl Iterator + Clone) -> Option<&str> { + self.0 + .iter() + .find(|(_, glob)| !members.clone().any(|member| glob.is_match(member))) + .map(|(text, _)| text.as_str()) + } +} + +impl TryFrom> for MemberGlobList { + type Error = globset::Error; + + fn try_from(globs: Vec) -> Result { + globs + .into_iter() + .map(|text| { + let glob = GlobBuilder::new(&text) + .literal_separator(true) + .build()? + .compile_matcher(); + Ok((text, glob)) + }) + .collect::>() + .map(Self) + } +} + +/// A `dependency_fence` instance's `members`: a [`MemberGlobList`] with at +/// least one glob, since an empty list would check nothing. +#[derive(Debug, Clone, Deserialize)] +#[serde(try_from = "Vec")] +pub struct MemberGlobs(MemberGlobList); + +#[derive(Debug, Error)] +pub enum MemberGlobsError { + #[error("members must list at least one glob")] + Empty, + #[error(transparent)] + Glob(#[from] globset::Error), +} + +impl MemberGlobs { + pub fn is_match(&self, member: &str) -> bool { + self.0.is_match(member) + } + + pub fn globs(&self) -> &MemberGlobList { + &self.0 + } +} + +impl TryFrom> for MemberGlobs { + type Error = MemberGlobsError; + + fn try_from(globs: Vec) -> Result { + if globs.is_empty() { + return Err(MemberGlobsError::Empty); + } + Ok(Self(MemberGlobList::try_from(globs)?)) + } +} + /// The name of a rule instance, as it appears in finding codes and baseline /// table names: lowercase ASCII letters, digits and underscores, starting with /// a letter. diff --git a/crates/tools/archcheck/src/context.rs b/crates/tools/archcheck/src/context.rs index 5567f38f6..837bd2088 100644 --- a/crates/tools/archcheck/src/context.rs +++ b/crates/tools/archcheck/src/context.rs @@ -67,6 +67,17 @@ pub enum LoadError { /// Debug-formatted, so the invalid bytes are shown escaped. #[error("{path:?} is not valid UTF-8")] NonUtf8Path { path: PathBuf }, + /// A glob that matches no member checks nothing, so it is a typo or a + /// member that moved. + #[error( + "[rules.dependency_fence.{instance}] {key} in {CONFIG_FILE} has {glob:?}, which matches \ + no workspace member; fix or remove it" + )] + UnmatchedMemberGlob { + instance: String, + key: &'static str, + glob: String, + }, #[error("no Cargo.toml with a [workspace] table found at or above {start}")] NoWorkspaceRoot { start: PathBuf }, /// The wording is for a rule with instances: every other rule's @@ -116,6 +127,24 @@ pub struct Crate { pub name_line: usize, /// The 1-based line of `[lib] name`, when the manifest sets it. pub lib_name_line: Option, + /// Every dependency its manifest declares, of any kind and target. + pub dependencies: Vec, +} + +/// A dependency a member's manifest declares, as `cargo metadata` reports +/// it. +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct Dependency { + /// The depended-on package's name, whatever the manifest calls it. + pub package: String, + /// The name the manifest gives it with `package = "…"`, if it renames it. + pub rename: Option, + /// `None` for `[dependencies]`; `dev` or `build` otherwise. + pub kind: Option, + /// The `cfg(…)` or target triple of a `[target..…]` table. + pub target: Option, + /// The 1-based line that names it in the manifest, or 1 if none does. + pub line: usize, } impl Crate { @@ -190,6 +219,18 @@ impl Context { return Err(LoadError::AllowedAndListed { name: name.clone() }); } let crates = workspace_crates(root)?; + let members = crates.iter().map(|krate| krate.member.as_str()); + for (instance, fence) in &config.rules.dependency_fence { + for (key, globs) in [("members", fence.members.globs()), ("allow", &fence.allow)] { + if let Some(glob) = globs.unmatched(members.clone()) { + return Err(LoadError::UnmatchedMemberGlob { + instance: instance.to_string(), + key, + glob: glob.to_owned(), + }); + } + } + } let files = production_files(root, &crates)?; Ok(Self { config, @@ -299,6 +340,15 @@ struct MetadataPackage { name: String, manifest_path: PathBuf, targets: Vec, + dependencies: Vec, +} + +#[derive(Deserialize)] +struct MetadataDependency { + name: String, + rename: Option, + kind: Option, + target: Option, } #[derive(Deserialize)] @@ -390,15 +440,55 @@ fn load_crate(workspace_root: &Path, package: MetadataPackage) -> Result usize { + let quoted = format!("\"{key}\""); + let names_key = |rest: &str| { + [key, quoted.as_str()].iter().any(|name| { + rest.strip_prefix(name) + .is_some_and(|after| after.trim_start().starts_with(['=', '.', ']'])) + }) + }; + manifest + .lines() + .position(|line| { + let line = line.trim_start(); + names_key(line) + || line.starts_with('[') + && line + .split_once("dependencies.") + .is_some_and(|(_, rest)| names_key(rest)) + }) + .map_or(1, |index| index + 1) +} + fn production_files(root: &Path, crates: &[Crate]) -> Result, LoadError> { let mut paths = Vec::new(); for krate in crates { diff --git a/crates/tools/archcheck/src/rules.rs b/crates/tools/archcheck/src/rules.rs index bd837e149..6d06573ae 100644 --- a/crates/tools/archcheck/src/rules.rs +++ b/crates/tools/archcheck/src/rules.rs @@ -1,11 +1,13 @@ //! The rule interface and the registry every check runs from. mod crate_names; +mod dependency_fence; mod file_size; mod pattern_ratchet; mod ratchet; pub(crate) use crate_names::CrateNames; +pub(crate) use dependency_fence::DependencyFence; pub(crate) use file_size::FileSize; pub(crate) use pattern_ratchet::PatternRatchet; pub(crate) use ratchet::CountRule; @@ -92,5 +94,6 @@ pub fn registered() -> Vec> { Box::new(FileSize), Box::new(PatternRatchet), Box::new(CrateNames), + Box::new(DependencyFence), ] } diff --git a/crates/tools/archcheck/src/rules/dependency_fence.rs b/crates/tools/archcheck/src/rules/dependency_fence.rs new file mode 100644 index 000000000..baed4246a --- /dev/null +++ b/crates/tools/archcheck/src/rules/dependency_fence.rs @@ -0,0 +1,85 @@ +//! Workspace members that may not depend on certain packages. +//! +//! Each `[rules.dependency_fence.]` lists Cargo package names and +//! the members it checks. A checked member outside its `allow` globs that +//! declares a dependency on one of the packages violates it (`no_crossing`), +//! whatever the dependency's kind or target and whatever the manifest calls +//! it: the dependency records come from `cargo metadata`, which names the +//! depended-on package even when `package = "…"` renames it. There is no +//! baseline; an existing dependency is removed or allowed. + +use crate::{ + config::{CONFIG_FILE, DependencyFenceConfig, InstanceName}, + context::{Context, Crate, Dependency}, + finding::{Finding, Location}, + rules::Rule, +}; + +pub struct DependencyFence; + +const RULE: &str = "dependency_fence"; +const NO_CROSSING: &str = "no_crossing"; + +impl Rule for DependencyFence { + fn check(&self, ctx: &Context) -> Vec { + ctx.config + .rules + .dependency_fence + .iter() + .flat_map(|(instance, config)| { + ctx.crates + .iter() + .filter(|krate| { + config.members.is_match(&krate.member) + && !config.allow.is_match(&krate.member) + }) + .flat_map(move |krate| { + krate + .dependencies + .iter() + .filter(|dependency| config.packages.contains(&dependency.package)) + .map(move |dependency| crossing(instance, config, krate, dependency)) + }) + }) + .collect() + } +} + +fn crossing( + instance: &InstanceName, + config: &DependencyFenceConfig, + krate: &Crate, + dependency: &Dependency, +) -> Finding { + let manifest = krate.manifest(); + let mut declared = Vec::new(); + if let Some(rename) = &dependency.rename { + declared.push(format!("renamed {rename}")); + } + declared.push(match dependency.kind.as_deref() { + None => "a normal dependency".to_owned(), + Some(kind) => format!("a {kind}-dependency"), + }); + if let Some(target) = &dependency.target { + declared.push(format!("for target {target}")); + } + Finding { + rule: RULE, + instance: Some(instance.to_string()), + behaviour: NO_CROSSING, + detail: format!( + ": {} depends on {} ({}), which only the members [rules.{RULE}.{instance}] in \ + {CONFIG_FILE} allows may. Remove the dependency, or use what it provides through \ + an allowed member.", + krate.name, + dependency.package, + declared.join(", "), + ), + path: manifest.clone(), + mode: config.no_crossing.mode, + location: Location { + path: manifest, + line: dependency.line, + }, + } +} diff --git a/crates/tools/archcheck/tests/cli.rs b/crates/tools/archcheck/tests/cli.rs index dbb0dc0eb..71587f0ca 100644 --- a/crates/tools/archcheck/tests/cli.rs +++ b/crates/tools/archcheck/tests/cli.rs @@ -2009,6 +2009,151 @@ fn pattern_ratchet_paths() { ); } +/// A fenced member may not depend on a listed package by any name, of any +/// kind or for any target; an allowed or unchecked member may, and a package +/// whose name only starts with a listed one is not listed. +#[test] +fn dependency_fence() { + let fence = "[rules.dependency_fence.driver]\n\ + packages = [\"rusqlite\"]\n\ + members = [\"crates/a\", \"crates/store\"]\n\ + allow = [\"crates/store\"]\n\ + [rules.dependency_fence.driver.no_crossing]\nmode = \"required\"\n"; + let manifest = |name: &str, dependencies: &str| format!("{}{dependencies}", package(name)); + type Case<'a> = (&'a str, &'a str, &'a [(&'a str, usize)]); + let cases: &[Case] = &[ + ( + "a normal dependency", + "[dependencies]\nrusqlite = \"0.39\"\n", + &[("a depends on rusqlite (a normal dependency), which only", 6)], + ), + ( + "a renamed dependency", + "[dependencies]\nsqlite = { package = \"rusqlite\", version = \"0.39\" }\n", + &[( + "a depends on rusqlite (renamed sqlite, a normal dependency)", + 6, + )], + ), + ( + "a dev-dependency in a table of its own", + "[dev-dependencies.rusqlite]\nversion = \"0.39\"\n", + &[("a depends on rusqlite (a dev-dependency)", 5)], + ), + ( + "a build-dependency", + "[build-dependencies]\nrusqlite = \"0.39\"\n", + &[("a depends on rusqlite (a build-dependency)", 6)], + ), + ( + "a target's dependency", + "[target.'cfg(unix)'.dependencies]\nrusqlite = \"0.39\"\n", + &[( + "a depends on rusqlite (a normal dependency, for target cfg(unix))", + 6, + )], + ), + ( + "a renamed dev-dependency for one target", + "[target.'cfg(unix)'.dev-dependencies]\nsqlite = { package = \"rusqlite\", version = \"0.39\" }\n", + &[( + "a depends on rusqlite (renamed sqlite, a dev-dependency, for target cfg(unix))", + 6, + )], + ), + ( + "another package with a similar name", + "[dependencies]\nrusqlite-macros = \"0.1\"\n", + &[], + ), + ]; + for (name, dependencies, expected) in cases { + let ws = crate_workspace( + &[("crates/store", "store")], + &[ + ("crates/a/Cargo.toml", manifest("a", dependencies)), + ("crates/b/Cargo.toml", manifest("b", dependencies)), + ("crates/store/Cargo.toml", manifest("store", dependencies)), + ("ARCHITECTURE.toml", format!("{}{fence}", config(ADVISORY))), + ], + ); + let found: Vec<_> = check(&load(ws.path(), None)) + .into_iter() + .filter(|finding| finding.rule == "dependency_fence") + .collect(); + assert_eq!(found.len(), expected.len(), "{name}: {found:#?}"); + for (finding, (message, line)) in found.iter().zip(*expected) { + assert_eq!( + finding.code(), + "dependency_fence.driver.no_crossing", + "{name}" + ); + assert!(finding.mode.is_required(), "{name}"); + assert!( + finding + .message() + .starts_with(&format!("crates/a/Cargo.toml: {message}")), + "{name}: {}", + finding.message() + ); + assert_eq!(finding.location.path, "crates/a/Cargo.toml", "{name}"); + assert_eq!(finding.location.line, *line, "{name}"); + } + } + + // Each list must name something, or the instance would check nothing. + for (key, error) in [ + ("packages", "packages must list at least one package name"), + ("members", "members must list at least one glob"), + ] { + let line = fence + .lines() + .find(|line| line.starts_with(&format!("{key} = "))) + .expect("the fence has the key"); + let ws = workspace(&[( + "ARCHITECTURE.toml", + format!( + "{}{}", + config(ADVISORY), + fence.replace(line, &format!("{key} = []")) + ), + )]); + let found = Context::load(ws.path()).expect_err("an empty list is rejected"); + assert!(found.to_string().contains(error), "{key}: {found}"); + } + + // A glob that matches no workspace member checks or allows nothing, so it + // is rejected; here `crates/store` is not a member. + let matching = fence + .replace( + "members = [\"crates/a\", \"crates/store\"]", + "members = [\"crates/a\"]", + ) + .replace("allow = [\"crates/store\"]", "allow = [\"crates/a\"]"); + for key in ["members", "allow"] { + let line = matching + .lines() + .find(|line| line.starts_with(&format!("{key} = "))) + .expect("the fence has the key"); + let ws = workspace(&[( + "ARCHITECTURE.toml", + format!( + "{}{}", + config(ADVISORY), + matching.replace(line, &format!("{key} = [\"crates/a\", \"crates/store\"]")) + ), + )]); + let found = Context::load(ws.path()).expect_err("an unmatched glob is rejected"); + assert!( + found.to_string().contains(&format!( + "[rules.dependency_fence.driver] {key} in ARCHITECTURE.toml has \"crates/store\", \ + which matches no workspace member" + )), + "{key}: {found}" + ); + } +} + /// One finding per behaviour, each reported in its own mode; only required /// findings exit 1. Without `--base-baseline`, no_new_entries is skipped and /// the summary says so. diff --git a/crates/tools/archcheck/tests/repository.rs b/crates/tools/archcheck/tests/repository.rs index 32f1e69ff..9652c08bd 100644 --- a/crates/tools/archcheck/tests/repository.rs +++ b/crates/tools/archcheck/tests/repository.rs @@ -121,6 +121,12 @@ fn ratchet_findings( .and_then(toml::Value::as_table_mut) .expect("ARCHITECTURE.toml has [rules.pattern_ratchet]") .retain(|name, _| name == instance); + // The fences name members this one-crate workspace does not have. + config + .get_mut("rules") + .and_then(toml::Value::as_table_mut) + .expect("ARCHITECTURE.toml has [rules]") + .remove("dependency_fence"); let dir = tempfile::tempdir().expect("create a temporary workspace"); let member = src.trim_end_matches("/src");