Skip to content

Commit abffde2

Browse files
committed
fix(supervisor): fence refused launches and remove legacy mode
Signed-off-by: Drew Newberry <anewberry@nvidia.com>
1 parent 06cd2e2 commit abffde2

20 files changed

Lines changed: 1225 additions & 1250 deletions

File tree

‎.agents/skills/helm-dev-environment/SKILL.md‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -101,7 +101,7 @@ OPENSHELL_E2E_KUBE_IMAGE_PULL_SECRET=e2e-regcred \
101101

102102
The shared supervisor is always a pool of `supervisor.replicas` members
103103
(default 1). With the gateway topology the chart renders the gateway's
104-
`[openshell.gateway.supervisor_pool]` table (`enabled = true`, plus
104+
`[openshell.gateway.supervisor_pool]` tuning table (only
105105
`recovery_timeout_secs` from `supervisor.recoveryTimeoutSeconds`, default 60;
106106
the driver's `shared_supervisor` table stays unchanged), a
107107
RollingUpdate Deployment (`maxSurge: 1`, `maxUnavailable: 0`) with preferred

‎crates/openshell-server/README.md‎

Lines changed: 25 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -20,10 +20,11 @@ authorize the new one.
2020

2121
## Shared supervisor pool
2222

23-
With `[openshell.gateway.supervisor_pool] enabled = true`, the `SharedSupervision`
24-
the gateway hands its compute driver coordinates a pool of shared supervisor
25-
hosts; the driver itself is unchanged. The gateway refuses a pool when no
26-
in-process driver uses the shared supervisor. `supervisor_pool` keeps the
23+
The `SharedSupervision` the gateway hands a compute driver that asks for it
24+
always coordinates a pool of shared supervisor hosts, at any member count; the
25+
driver itself is unchanged. `[openshell.gateway.supervisor_pool]` only tunes it
26+
(`recovery_timeout_secs`), and the gateway refuses the table when no in-process
27+
driver uses the shared supervisor. `supervisor_pool` keeps the
2728
pool's durable state and reconciles it every two seconds on every gateway
2829
replica, independent of which replica runs the compute driver's watch:
2930

@@ -52,18 +53,29 @@ replica, independent of which replica runs the compute driver's watch:
5253
atomic store writes and holds no lock: it reserves the route with a row named
5354
by the route and identified by a fresh writer token, stores the material, has
5455
a `supervisor_instance_material_candidate` row own the handle, and publishes
55-
by advancing exactly its reservation's version. Collection deletes a
56-
candidate's material only once its recording no longer holds the reservation,
57-
after which that recording can never publish; a reservation that does not
58-
publish within two minutes is fenced by removing it. A withdrawal removes the
59-
material and then the record, or fences a reservation; an ended generation
56+
by advancing exactly its reservation's version. A launch publishes its
57+
record as `Admitting`, which is not desired (nothing places, charges, or
58+
grants it) and keeps its candidate; a driver recovery's backfill publishes
59+
directly. Collection deletes a candidate's material only once its recording
60+
no longer holds its reserved or admitting row, after which that recording
61+
can never publish or be admitted; a reservation or admitting record older
62+
than two minutes (a stalled, failed, or crashed launch) is fenced by
63+
removing it at its version, and its material is then collected. A
64+
withdrawal removes the material and then the record, or fences a reserved or
65+
admitting row; an ended generation
6066
(sandbox gone, replaced, stopped, or deleting) is withdrawn by the sweep; and
6167
the compute driver's recovery backfills instances it runs without a record and
6268
withdraws records published before its listing that it no longer lists.
63-
- **Placement and ownership.** A launch records its owner, the live member the
64-
consistent-hash ring prefers (`gateway_ring`), right after the instance; a
65-
member at its 256-instance capacity is skipped, and with no live member with
66-
room the launch is refused as a capacity refusal. One
69+
- **Placement and ownership.** A launch admits its instance on the live member
70+
the consistent-hash ring prefers (`gateway_ring`): one transaction creates
71+
the owner record, charges the member's capacity, and publishes the
72+
`Admitting` record, conditioned on that record's version, so an owner always
73+
has its published instance and a removed record never gains one. A member at
74+
its 256-instance capacity is skipped. With no live member with room the
75+
launch is refused as a capacity refusal: it removes the admitting record it
76+
created, only at the version it created (a concurrent launch of the same
77+
generation that admitted it first wins), and then that record's candidate
78+
and material; a removal that fails is left to fencing. One
6779
`supervisor_instance_owner` record per instance names the member, its
6880
process, and the ownership term (the sandbox's credential epoch). Records
6981
change only by compare-and-swap, and a live owner keeps its instances.

‎crates/openshell-server/src/config_file.rs‎

Lines changed: 19 additions & 41 deletions
Original file line numberDiff line numberDiff line change
@@ -192,7 +192,7 @@ pub struct GatewayFileSection {
192192
pub otlp: Option<OtlpConfig>,
193193
#[serde(default)]
194194
pub ocsf_log: Option<OcsfLogConfig>,
195-
/// Run the shared supervisor as a high-availability pool of replicas.
195+
/// Tuning of the shared supervisor pool.
196196
#[serde(default, skip_serializing_if = "Option::is_none")]
197197
pub supervisor_pool: Option<SupervisorPoolConfig>,
198198

@@ -205,19 +205,17 @@ pub struct GatewayFileSection {
205205
pub database_url: Option<String>,
206206
}
207207

208-
/// `[openshell.gateway.supervisor_pool]`: run the shared supervisor as a pool.
208+
/// `[openshell.gateway.supervisor_pool]`: optional tuning of the shared
209+
/// supervisor pool.
209210
///
210-
/// The gateway coordinates the shared supervisor's replicas for high
211-
/// availability: it places each sandbox instance on a live member by
211+
/// The shared supervisor always runs as a pool that the gateway coordinates
212+
/// for high availability: it places each sandbox instance on a live member by
212213
/// consistent hash, records its owner durably, and moves the instances of a
213214
/// failed or leaving member to another member, which adopts their running
214-
/// workloads.
215+
/// workloads. Without this table the defaults apply.
215216
#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Serialize, Deserialize)]
216217
#[serde(deny_unknown_fields)]
217218
pub struct SupervisorPoolConfig {
218-
/// Coordinate the shared supervisor's replicas as a pool.
219-
#[serde(default)]
220-
pub enabled: bool,
221219
/// Seconds a sandbox runtime keeps its workload frozen after losing its
222220
/// supervisor before terminating it, from 10 to 300. The pool's
223221
/// supervisors offer it to each runtime; unset offers
@@ -226,17 +224,6 @@ pub struct SupervisorPoolConfig {
226224
pub recovery_timeout_secs: Option<u32>,
227225
}
228226

229-
impl SupervisorPoolConfig {
230-
/// The recovery deadline the pool offers, when the pool is enabled.
231-
#[must_use]
232-
pub fn enabled_recovery_timeout_secs(&self) -> Option<u32> {
233-
self.enabled.then(|| {
234-
self.recovery_timeout_secs
235-
.unwrap_or(openshell_supervisor_control::POOL_RECOVERY_TIMEOUT_SECS)
236-
})
237-
}
238-
}
239-
240227
/// Gateway listener TLS fields accepted in `gateway.toml`.
241228
///
242229
/// Client-certificate handshake policy is derived from the presence of a
@@ -1779,24 +1766,14 @@ database_lock_max_connections = 0
17791766
}
17801767

17811768
#[test]
1782-
fn parses_the_supervisor_pool() {
1769+
fn parses_the_supervisor_pool_tuning() {
17831770
let pool = |table: &str| {
17841771
let tmp = write_tmp(&format!("[openshell.gateway.supervisor_pool]\n{table}"));
17851772
load(tmp.path()).map(|file| file.openshell.gateway.supervisor_pool)
17861773
};
1787-
// Enabled, a pool offers its default recovery deadline unless
1788-
// configured; disabled, it offers none.
1789-
let enabled = pool("enabled = true\n").unwrap().unwrap();
1790-
assert_eq!(
1791-
enabled.enabled_recovery_timeout_secs(),
1792-
Some(openshell_supervisor_control::POOL_RECOVERY_TIMEOUT_SECS)
1793-
);
1794-
let configured = pool("enabled = true\nrecovery_timeout_secs = 90\n")
1795-
.unwrap()
1796-
.unwrap();
1797-
assert_eq!(configured.enabled_recovery_timeout_secs(), Some(90));
1798-
let disabled = pool("recovery_timeout_secs = 90\n").unwrap().unwrap();
1799-
assert_eq!(disabled.enabled_recovery_timeout_secs(), None);
1774+
let configured = pool("recovery_timeout_secs = 90\n").unwrap().unwrap();
1775+
assert_eq!(configured.recovery_timeout_secs, Some(90));
1776+
assert_eq!(pool("").unwrap().unwrap().recovery_timeout_secs, None);
18001777
let tmp = write_tmp("[openshell.gateway]\n");
18011778
assert!(
18021779
load(tmp.path())
@@ -1806,22 +1783,23 @@ database_lock_max_connections = 0
18061783
.supervisor_pool
18071784
.is_none()
18081785
);
1809-
// Recovery stays bounded, and unknown keys are refused.
1786+
// Recovery stays bounded, and unknown keys, including the removed
1787+
// `enabled` switch, are refused.
18101788
for out_of_range in ["9", "301"] {
18111789
assert!(matches!(
1812-
pool(&format!(
1813-
"enabled = true\nrecovery_timeout_secs = {out_of_range}\n"
1814-
)),
1790+
pool(&format!("recovery_timeout_secs = {out_of_range}\n")),
18151791
Err(ConfigFileError::InvalidValue {
18161792
field: "openshell.gateway.supervisor_pool.recovery_timeout_secs",
18171793
..
18181794
})
18191795
));
18201796
}
1821-
assert!(matches!(
1822-
pool("enabled = true\nhigh_availability = true\n"),
1823-
Err(ConfigFileError::Parse { .. })
1824-
));
1797+
for unknown in ["enabled = true\n", "high_availability = true\n"] {
1798+
assert!(
1799+
matches!(pool(unknown), Err(ConfigFileError::Parse { .. })),
1800+
"{unknown}"
1801+
);
1802+
}
18251803
}
18261804

18271805
#[test]

‎crates/openshell-server/src/lib.rs‎

Lines changed: 45 additions & 37 deletions
Original file line numberDiff line numberDiff line change
@@ -821,17 +821,15 @@ pub(crate) async fn run_server(
821821
ttl_secs = ttl.as_secs(),
822822
"shared supervisor assignment stream enabled; minting supervisor tokens"
823823
);
824-
if controller.is_pool() {
825-
info!("shared supervisor pool enabled; reconciling pool membership and ownership");
826-
supervisor_pool::SupervisorPool::new(
827-
state.store.clone(),
828-
controller.clone(),
829-
authority.clone(),
830-
state.credentials.clone(),
831-
state.replica_id.clone(),
832-
)
833-
.start(shutdown_rx.clone());
834-
}
824+
info!("shared supervisor pool enabled; reconciling pool membership and ownership");
825+
supervisor_pool::SupervisorPool::new(
826+
state.store.clone(),
827+
controller.clone(),
828+
authority.clone(),
829+
state.credentials.clone(),
830+
state.replica_id.clone(),
831+
)
832+
.start(shutdown_rx.clone());
835833
spawn_supervisor_token_refresh(authority, ttl, controller.clone(), shutdown_rx.clone());
836834
}
837835

@@ -1708,25 +1706,25 @@ pub struct ComputeDriverBuildContext<'a> {
17081706
config: ComputeDriverConfigContext<'a>,
17091707
shutdown_rx: watch::Receiver<bool>,
17101708
shared_supervision: &'a OnceLock<openshell_supervisor_control::SharedSupervision>,
1711-
/// Recovery deadline of a configured supervisor pool.
1712-
supervisor_pool: Option<u32>,
1709+
/// Recovery deadline from `[openshell.gateway.supervisor_pool]`, when
1710+
/// configured.
1711+
supervisor_recovery_timeout_secs: Option<u32>,
17131712
}
17141713

17151714
impl ComputeDriverBuildContext<'_> {
1716-
/// The gateway's coordination of a shared supervisor host, for an
1715+
/// The gateway's coordination of the shared supervisor pool, for an
17171716
/// in-process driver whose configuration places new generations on it.
1718-
/// The gateway owns the desired set, serves it to the host over the
1719-
/// private assignment stream, mints the supervisor token, and publishes
1720-
/// each renewal for the driver to deliver. With
1721-
/// `[openshell.gateway.supervisor_pool]` enabled it coordinates a pool of
1722-
/// hosts. Created on first request; every call returns the same
1723-
/// coordination.
1717+
/// The gateway owns the desired set and ownership durably, serves each
1718+
/// member over the private assignment stream, mints the supervisor
1719+
/// token, and publishes each renewal for the driver to deliver.
1720+
/// `[openshell.gateway.supervisor_pool]` only tunes it. Created on first
1721+
/// request; every call returns the same coordination.
17241722
pub fn shared_supervision(&self) -> openshell_supervisor_control::SharedSupervision {
17251723
self.shared_supervision
17261724
.get_or_init(|| {
17271725
let supervision = openshell_supervisor_control::SharedSupervision::new();
1728-
if let Some(recovery_timeout_secs) = self.supervisor_pool {
1729-
supervision.enable_pool(recovery_timeout_secs);
1726+
if let Some(recovery_timeout_secs) = self.supervisor_recovery_timeout_secs {
1727+
supervision.set_recovery_timeout_secs(recovery_timeout_secs);
17301728
}
17311729
supervision
17321730
})
@@ -1850,8 +1848,7 @@ async fn build_compute_runtime(
18501848
info!(driver = %driver.name(), "Using compute driver");
18511849
let supervisor_pool = driver_startup
18521850
.file
1853-
.and_then(|file| file.openshell.gateway.supervisor_pool)
1854-
.and_then(|pool| pool.enabled_recovery_timeout_secs());
1851+
.and_then(|file| file.openshell.gateway.supervisor_pool);
18551852
let runtime = match driver {
18561853
ConfiguredComputeDriver::Registered(registration) => {
18571854
let shared_supervision = OnceLock::new();
@@ -1865,15 +1862,18 @@ async fn build_compute_runtime(
18651862
},
18661863
shutdown_rx,
18671864
shared_supervision: &shared_supervision,
1868-
supervisor_pool,
1865+
supervisor_recovery_timeout_secs: supervisor_pool
1866+
.and_then(|pool| pool.recovery_timeout_secs),
18691867
};
18701868
let instance = registration.factory.build(build_context).await?;
18711869
let supervisor_host = shared_supervision
18721870
.into_inner()
18731871
.map(|supervision| supervision.controller().clone());
18741872
if supervisor_pool.is_some() && supervisor_host.is_none() {
18751873
return Err(Error::config(
1876-
"openshell.gateway.supervisor_pool requires a compute driver that hosts sandboxes on the gateway's shared supervisor (Kubernetes supervisor_topology = \"gateway\")",
1874+
"openshell.gateway.supervisor_pool tunes the shared supervisor pool, but no \
1875+
compute driver hosts sandboxes on it; set the Kubernetes driver's \
1876+
supervisor_topology = \"gateway\" or remove the table",
18771877
));
18781878
}
18791879
match instance {
@@ -1916,8 +1916,8 @@ async fn build_compute_runtime(
19161916
}
19171917
ConfiguredComputeDriver::Remote { .. } if supervisor_pool.is_some() => {
19181918
return Err(Error::config(
1919-
"openshell.gateway.supervisor_pool requires an in-process compute driver that \
1920-
hosts sandboxes on the gateway's shared supervisor",
1919+
"openshell.gateway.supervisor_pool tunes the shared supervisor pool, which \
1920+
only an in-process compute driver can use; remove the table",
19211921
));
19221922
}
19231923
ConfiguredComputeDriver::Remote { name } => {
@@ -2343,10 +2343,16 @@ mod tests {
23432343
struct TestComputeDriverFactory;
23442344

23452345
#[test]
2346-
fn a_configured_supervisor_pool_makes_the_drivers_shared_supervision_a_pool() {
2346+
fn shared_supervision_is_always_a_pool_tuned_by_its_table() {
23472347
let endpoint_overrides = std::collections::BTreeMap::new();
23482348
let (_shutdown, shutdown_rx) = watch::channel(false);
2349-
for supervisor_pool in [None, Some(75)] {
2349+
for (configured, offered) in [
2350+
(
2351+
None,
2352+
openshell_supervisor_control::POOL_RECOVERY_TIMEOUT_SECS,
2353+
),
2354+
(Some(75), 75),
2355+
] {
23502356
let shared = std::sync::OnceLock::new();
23512357
let context = super::ComputeDriverBuildContext {
23522358
config: super::ComputeDriverConfigContext {
@@ -2364,19 +2370,21 @@ mod tests {
23642370
},
23652371
shutdown_rx: shutdown_rx.clone(),
23662372
shared_supervision: &shared,
2367-
supervisor_pool,
2373+
supervisor_recovery_timeout_secs: configured,
23682374
};
2369-
// The driver asks for shared supervision as before; the gateway's
2370-
// configuration alone decides whether it coordinates a pool.
2375+
// The driver asks for shared supervision as before; it is the
2376+
// pool, with the configured recovery deadline or the default.
23712377
let supervision = context.shared_supervision();
2372-
assert_eq!(supervision.is_pool(), supervisor_pool.is_some());
23732378
assert_eq!(
23742379
supervision.controller().pool_recovery_timeout_secs(),
2375-
supervisor_pool
2380+
offered
23762381
);
23772382
assert_eq!(
2378-
context.shared_supervision().is_pool(),
2379-
supervisor_pool.is_some()
2383+
context
2384+
.shared_supervision()
2385+
.controller()
2386+
.pool_recovery_timeout_secs(),
2387+
offered
23802388
);
23812389
assert!(shared.get().is_some());
23822390
}

0 commit comments

Comments
 (0)