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

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion architecture-baseline.toml
Original file line number Diff line number Diff line change
Expand Up @@ -73,7 +73,7 @@
"crates/libs/sbx/mgr/server/src/db/cleanup_episodes.rs" = 1841
"crates/libs/sbx/mgr/server/src/db/quarantine_recovery.rs" = 1042
"crates/libs/sbx/mgr/server/src/db/sandboxes.rs" = 1553
"crates/libs/sbx/mgr/server/src/db/state_transitions.rs" = 1156
"crates/libs/sbx/mgr/server/src/db/state_transitions.rs" = 1140
"crates/libs/sbx/mgr/server/src/run.rs" = 1101
"crates/libs/sbx/mgr/server/src/sandbox_eviction.rs" = 1273
"crates/libs/sbx/mgr/server/src/service/activation_support.rs" = 1592
Expand Down
17 changes: 13 additions & 4 deletions crates/libs/sbx/mgr/server/AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,19 @@

## Changes

- Write `sandboxes.state` only through `src/db` writers. A new transition uses
or adds a compare-and-set writer in `src/db`; no generic writer compares
against the caller's observed state. A refused write must leave every row
unchanged.
- Write `sandboxes.state` only through `src/db` writers. A new transition
calls `update_state_from` with its edge, or adds a specialized
compare-and-set writer in `src/db`. Do not add `update_state` callers: it
overwrites any state its cleanup and quarantine guards admit. Record the
edge in `src/db/state_write.rs`, and 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
`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
Expand Down
2 changes: 2 additions & 0 deletions crates/libs/sbx/mgr/server/src/db.rs
Original file line number Diff line number Diff line change
Expand Up @@ -75,6 +75,7 @@ mod restore_transfer_scopes;
mod sandboxes;
mod startup_recovery;
mod state_transitions;
mod state_write;
mod teardown_suspend;
pub use auto_lifecycle::*;
pub use cleanup_episodes::*;
Expand All @@ -88,6 +89,7 @@ pub use restore_transfer_scopes::*;
pub use sandboxes::*;
pub use startup_recovery::*;
pub use state_transitions::*;
pub use state_write::*;
pub use teardown_suspend::*;

// Most of the no-progress vocabulary is durable: its rendered form is persisted
Expand Down
60 changes: 19 additions & 41 deletions crates/libs/sbx/mgr/server/src/db/state_transitions.rs
Original file line number Diff line number Diff line change
Expand Up @@ -110,47 +110,25 @@ pub fn update_state(
let previous_state_value = previous_state
.as_deref()
.and_then(|state| state.parse::<SandboxState>().ok());
if generic_writer_transition_blocked(previous_state_value, state) {
tracing::warn!(
sandbox_id = %id,
target_state = %state,
"ignored generic sandbox state update because sandbox is quarantined"
);
return Ok(SandboxStateWriteOutcome::BlockedByQuarantine);
}
if cleanup_episode(conn, id)?
.is_some_and(|episode| !episode.completion.deletes_row() || episode.requested_at.is_some())
|| (matches!(
previous_state_value,
Some(
SandboxState::Deleting
| SandboxState::CleanupRequired
| SandboxState::CleanupFailed
)
) && state != SandboxState::Deleting
&& cleanup_intent_current(conn, id, CleanupCompletion::DeleteRow)?)
|| (matches!(
previous_state_value,
Some(
SandboxState::Stopping
| SandboxState::CleanupRequired
| SandboxState::CleanupFailed
)
) && matches!(
state,
SandboxState::Started
| SandboxState::Paused
| SandboxState::Pausing
| SandboxState::Resuming
| SandboxState::Checkpointing
))
{
tracing::warn!(
sandbox_id = %id,
target_state = %state,
"ignored generic sandbox state update because cleanup owns the transition"
);
return Ok(SandboxStateWriteOutcome::SupersededByCleanupEpisode);
match generic_state_write_refusal(conn, id, previous_state_value, state)? {
Some(StateWriteRefusal::Quarantined) => {
tracing::warn!(
sandbox_id = %id,
target_state = %state,
"ignored generic sandbox state update because sandbox is quarantined"
);
return Ok(SandboxStateWriteOutcome::BlockedByQuarantine);
}
// The generic guards never answer `FromState`.
Some(StateWriteRefusal::CleanupOwned | StateWriteRefusal::FromState) => {
tracing::warn!(
sandbox_id = %id,
target_state = %state,
"ignored generic sandbox state update because cleanup owns the transition"
);
return Ok(SandboxStateWriteOutcome::SupersededByCleanupEpisode);
}
None => {}
}

let changed = conn.execute(
Expand Down
Loading
Loading