Skip to content
Merged
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
46 changes: 40 additions & 6 deletions crates/engram-coordinator/src/boot_materializer.rs
Original file line number Diff line number Diff line change
Expand Up @@ -390,16 +390,30 @@ pub(crate) async fn materialize_snapshot_resume(
})
}

/// The placement-budget slice of the cold-boot shape, for callers that
/// need `(memory_mib, vcpus)` and nothing else (the resume verb's
/// `ScheduleContext`, ADR 0078). Best-effort by contract: `None` on any
/// miss keeps the pre-0072 capacity-soft posture — deliberately NOT
/// `materialize_cold_boot`, which does per-session slot resolution this
/// caller would throw away.
/// The `(memory_mib, vcpus)` a resume, a queued resume, or a move of
/// `session` reserves: the budget the session records
/// (`MetadataStore::session_budget`). That budget is the size of the VM
/// its snapshot restores and the figure the host accounting holds for the
/// session. The image's current config decides only for a session with no
/// recorded budget: a config that changed after the create does not
/// resize the VM. `None` when neither source resolves; the caller then
/// places without a capacity check.
pub(crate) async fn resolve_resume_budget(
meta: &std::sync::Arc<dyn engram_core::traits::MetadataStore>,
session: &Session,
) -> Option<(u32, u32)> {
match meta.session_budget(session.id).await {
Ok(Some((mem_mib, vcpus))) => {
if let (Ok(mem_mib), Ok(vcpus)) = (u32::try_from(mem_mib), u32::try_from(vcpus)) {
return Some((mem_mib, vcpus));
}
}
Ok(None) => {}
Err(e) => {
tracing::warn!(session_id = %session.id, error = %e,
"session budget lookup failed; the image config decides the resume budget");
}
}
match meta.get_enabled_image(&session.image).await {
Ok(Some(row)) => {
let config = row.effective_config();
Expand Down Expand Up @@ -687,6 +701,26 @@ mod readiness_tests {
(state, meta, host, session, sandbox)
}

/// A resume reserves what the session records, not what the image's
/// config says now: the two differ after an image config change, and
/// the host accounting holds the recorded figure.
#[tokio::test]
async fn the_resume_budget_is_the_sessions_recorded_budget() {
let (state, meta, _host, session, _sandbox) = fixture().await;
let from_image = resolve_resume_budget(&state.services.meta, &session).await;
assert_ne!(from_image, Some((24_576, 6)));

meta.with_db_mut(|db| {
let row = db.sessions.get_mut(&session.id).expect("session row");
row.mem_budget_mib = 24_576;
row.cpu_budget_vcpus = 6;
});
assert_eq!(
resolve_resume_budget(&state.services.meta, &session).await,
Some((24_576, 6))
);
}

#[tokio::test]
async fn resolution_error_never_becomes_active() {
let (state, meta, host, session, sandbox) = fixture().await;
Expand Down
8 changes: 8 additions & 0 deletions crates/engram-core/src/traits/metadata.rs
Original file line number Diff line number Diff line change
Expand Up @@ -736,6 +736,14 @@ pub trait MetadataStore: Send + Sync {
.collect())
}

/// The `(mem_budget_mib, cpu_budget_vcpus)` the session reserves: the
/// figures its VM was created with, which every later restore of that
/// VM keeps. `None` when the row records no memory budget. The default
/// is `None` (mocks carry no budgets).
async fn session_budget(&self, _id: SessionId) -> Result<Option<(i64, i32)>, MetaError> {
Ok(None)
}

/// ADR 0048 C8: the RESIDENT assignments on `host_id` WITH their
/// reservation budgets, for the admin drain (the don't-strand guard
/// pre-checks that some survivor fits each session's budgets before
Expand Down
14 changes: 14 additions & 0 deletions crates/engram-postgres/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2754,6 +2754,20 @@ impl MetadataStore for PostgresStore {
Ok(out)
}

async fn session_budget(&self, id: SessionId) -> Result<Option<(i64, i32)>, MetaError> {
let row: Option<(i64, i32)> = sqlx::query_as(
r#"
SELECT COALESCE(mem_budget_mib, 0)::BIGINT, COALESCE(cpu_budget_vcpus, 0)
FROM sessions WHERE id = $1
"#,
)
.bind(id.as_uuid())
.fetch_optional(&self.pool)
.await
.map_err(db_err)?;
Ok(row.filter(|(mem, _)| *mem > 0))
}

async fn list_resident_assignments_with_budgets_on_host(
&self,
host_id: HostId,
Expand Down
10 changes: 10 additions & 0 deletions crates/engram-sim/src/meta/store_impl.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4611,6 +4611,16 @@ impl MetadataStore for SimMetadataStore {
panic!("SimMeta: latest_capture_job_for_enable not implemented — add it plus a conformance case (ADR 0098 D4)")
}

async fn session_budget(&self, id: SessionId) -> Result<Option<(i64, i32)>, MetaError> {
self.gate()?;
let db = self.db.lock();
Ok(db
.sessions
.get(&id)
.map(|r| (r.mem_budget_mib, r.cpu_budget_vcpus))
.filter(|(mem, _)| *mem > 0))
}

async fn list_resident_assignments_with_budgets_on_host(
&self,
host_id: HostId,
Expand Down
51 changes: 51 additions & 0 deletions crates/engram-sim/tests/meta_conformance.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3023,6 +3023,53 @@ async fn a_host_without_reported_cores_has_no_cpu_gate(ctx: &Ctx) {
));
}

/// `session_budget` is the reservation the session was created with. A
/// session with no recorded budget, and an unknown session, have none.
async fn session_budget_is_the_create_time_reservation(ctx: &Ctx) {
let meta = &ctx.meta;
let now = ctx.clock.now_utc();
let host = HostId::new();
meta.upsert_host(host_record(host, "conf-budget", now))
.await
.unwrap();
let budgeted = SessionId::new();
let placed = meta
.reserve_and_persist_create(
engram_core::traits::metadata::SessionCreateWriteSet {
session_id: budgeted,
spec: spec("conf:budget"),
mem_budget_mib: 4096,
cpu_budget_vcpus: 2,
sealed_secrets: None,
capabilities: Vec::new(),
integration_policy_json: None,
runtime_spec: engram_core::types::runtime_spec::RuntimeSpec::new(
Vec::new(),
None,
None,
Vec::new(),
),
oauth_binding: None,
},
&[host],
0,
)
.await
.unwrap();
assert!(matches!(
placed,
engram_core::traits::metadata::CreateDisposition::Placed(_)
));
assert_eq!(
meta.session_budget(budgeted).await.unwrap(),
Some((4096, 2))
);

let unbudgeted = meta.create_session(spec("conf:no-budget")).await.unwrap();
assert_eq!(meta.session_budget(unbudgeted).await.unwrap(), None);
assert_eq!(meta.session_budget(SessionId::new()).await.unwrap(), None);
}

/// The register-time rehydrate list and the reconcile assignment list are
/// one listing: every VM-resident state with a bound sandbox on the host.
/// A rung-parked `Evicting` survivor and a `Created` survivor are as
Expand Down Expand Up @@ -5596,6 +5643,10 @@ conformance!(
t_a_host_without_reported_cores_has_no_cpu_gate,
super::a_host_without_reported_cores_has_no_cpu_gate
);
conformance!(
t_session_budget_is_the_create_time_reservation,
super::session_budget_is_the_create_time_reservation
);
conformance!(
t_enabled_image_config_update,
super::enabled_image_config_update
Expand Down
Loading