Skip to content

Commit 0300e6d

Browse files
authored
fix(sandbox): order sidecar provider updates by generation (#2849)
Closes #2847 Separate sidecar delivery ordering from opaque provider environment fingerprints and cover live bind/unbind convergence. Signed-off-by: John Myers <9696606+johntmyers@users.noreply.github.com>
1 parent 6c34a3c commit 0300e6d

4 files changed

Lines changed: 309 additions & 18 deletions

File tree

architecture/sandbox.md

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -121,6 +121,13 @@ partially active or last-known-good static set. Invalid metadata preserves the
121121
supplied dynamic snapshot, while a fetch failure preserves the currently active
122122
dynamic snapshot.
123123

124+
In the Kubernetes sidecar topology, the provider environment revision remains
125+
an opaque content fingerprint and has no numeric ordering semantics. The
126+
network supervisor assigns a separate, connection-local monotonic generation
127+
to each distinct environment it publishes. The process supervisor applies only
128+
newer generations, which accepts descending fingerprint values while rejecting
129+
duplicate or delayed sidecar messages.
130+
124131
Gateway-managed refresh credentials use an opaque workload handle derived from
125132
the sandbox, provider identity, credential key, refresh authorization epoch,
126133
and canonical endpoint boundary. The handle remains stable while the gateway

crates/openshell-sandbox/src/lib.rs

Lines changed: 83 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -363,6 +363,9 @@ pub async fn run_sandbox(
363363
.as_ref()
364364
.map(|connection| connection.writer.clone());
365365
let process_exit_ack = Arc::new(tokio::sync::Mutex::new(None));
366+
let initial_provider_env_generation = sidecar_bootstrap
367+
.as_ref()
368+
.map_or(0, |bootstrap| bootstrap.provider_env_generation);
366369
let mut process_control_closed = None;
367370
if let Some(connection) = process_control_connection {
368371
process_control_closed = Some(connection.closed);
@@ -371,6 +374,7 @@ pub async fn run_sandbox(
371374
provider_credentials.clone(),
372375
agent_proposals.clone(),
373376
Arc::clone(&process_exit_ack),
377+
initial_provider_env_generation,
374378
);
375379
}
376380

@@ -569,6 +573,7 @@ pub async fn run_sandbox(
569573
sidecar_control::BootstrapData {
570574
policy_proto: proto.clone(),
571575
provider_env_revision: provider_credentials.snapshot().revision,
576+
provider_env_generation: 0,
572577
provider_child_env: provider_env.clone(),
573578
agent_proposals_enabled: agent_proposals.enabled(),
574579
proxy_ca_cert_path: ca_paths.as_ref().map(|paths| paths.0.clone()),
@@ -1072,25 +1077,29 @@ fn spawn_sidecar_control_update_watcher(
10721077
provider_credentials: ProviderCredentialState,
10731078
agent_proposals: AgentProposals,
10741079
exit_ack: MainProcessExitAckWaiter,
1080+
mut provider_env_generation: u64,
10751081
) -> tokio::task::JoinHandle<()> {
10761082
tokio::spawn(async move {
10771083
while let Some(update) = updates.recv().await {
10781084
match update {
10791085
sidecar_control::ControlUpdate::ProviderEnv {
10801086
revision,
1087+
generation,
10811088
provider_child_env,
10821089
} => {
1083-
if revision <= provider_credentials.snapshot().revision {
1090+
if generation <= provider_env_generation {
10841091
continue;
10851092
}
10861093
let env_count = provider_credentials
10871094
.install_child_env_snapshot(revision, provider_child_env);
1095+
provider_env_generation = generation;
10881096
ocsf_emit!(
10891097
ConfigStateChangeBuilder::new(ocsf_ctx())
10901098
.severity(SeverityId::Informational)
10911099
.status(StatusId::Success)
10921100
.state(StateId::Enabled, "loaded")
10931101
.unmapped("provider_env_revision", serde_json::json!(revision))
1102+
.unmapped("provider_env_generation", serde_json::json!(generation))
10941103
.message(format!(
10951104
"Sidecar provider environment refreshed [revision:{revision} env_count:{env_count}]"
10961105
))
@@ -4350,21 +4359,24 @@ mod tests {
43504359
}
43514360

43524361
#[tokio::test]
4353-
async fn sidecar_control_provider_env_update_installs_newer_revision() {
4362+
async fn sidecar_control_provider_env_update_orders_by_generation() {
43544363
let (tx, rx) = tokio::sync::mpsc::unbounded_channel();
43554364
let provider_credentials = ProviderCredentialState::from_child_env_snapshot(
4356-
1,
4365+
u64::MAX,
43574366
std::collections::HashMap::from([("TOKEN".to_string(), "old".to_string())]),
43584367
);
4368+
let agent_proposals = AgentProposals::new(true);
43594369
let handle = spawn_sidecar_control_update_watcher(
43604370
rx,
43614371
provider_credentials.clone(),
4362-
AgentProposals::default(),
4372+
agent_proposals.clone(),
43634373
Arc::new(tokio::sync::Mutex::new(None)),
4374+
10,
43644375
);
43654376

43664377
tx.send(sidecar_control::ControlUpdate::ProviderEnv {
4367-
revision: 2,
4378+
revision: 1,
4379+
generation: 11,
43684380
provider_child_env: std::collections::HashMap::from([(
43694381
"TOKEN".to_string(),
43704382
"new".to_string(),
@@ -4374,7 +4386,7 @@ mod tests {
43744386

43754387
timeout(Duration::from_secs(1), async {
43764388
loop {
4377-
if provider_credentials.snapshot().revision == 2 {
4389+
if provider_credentials.snapshot().revision == 1 {
43784390
break;
43794391
}
43804392
tokio::time::sleep(Duration::from_millis(10)).await;
@@ -4383,21 +4395,33 @@ mod tests {
43834395
.await
43844396
.unwrap();
43854397
let snapshot = provider_credentials.snapshot();
4386-
assert_eq!(snapshot.revision, 2);
4398+
assert_eq!(snapshot.revision, 1);
43874399
assert_eq!(
43884400
snapshot.child_env.get("TOKEN").map(String::as_str),
43894401
Some("new")
43904402
);
43914403

43924404
tx.send(sidecar_control::ControlUpdate::ProviderEnv {
4393-
revision: 1,
4405+
revision: 2,
4406+
generation: 11,
43944407
provider_child_env: std::collections::HashMap::from([(
43954408
"TOKEN".to_string(),
4396-
"stale".to_string(),
4409+
"duplicate-generation".to_string(),
43974410
)]),
43984411
})
43994412
.unwrap();
4400-
tokio::time::sleep(Duration::from_millis(20)).await;
4413+
tx.send(sidecar_control::ControlUpdate::AgentProposals {
4414+
enabled: false,
4415+
config_revision: 1,
4416+
})
4417+
.unwrap();
4418+
timeout(Duration::from_secs(1), async {
4419+
while agent_proposals.enabled() {
4420+
tokio::time::sleep(Duration::from_millis(10)).await;
4421+
}
4422+
})
4423+
.await
4424+
.unwrap();
44014425
assert_eq!(
44024426
provider_credentials
44034427
.snapshot()
@@ -4406,6 +4430,54 @@ mod tests {
44064430
.map(String::as_str),
44074431
Some("new")
44084432
);
4433+
4434+
tx.send(sidecar_control::ControlUpdate::ProviderEnv {
4435+
revision: 2,
4436+
generation: 12,
4437+
provider_child_env: std::collections::HashMap::from([(
4438+
"TOKEN".to_string(),
4439+
"newest".to_string(),
4440+
)]),
4441+
})
4442+
.unwrap();
4443+
timeout(Duration::from_secs(1), async {
4444+
loop {
4445+
if provider_credentials.snapshot().revision == 2 {
4446+
break;
4447+
}
4448+
tokio::time::sleep(Duration::from_millis(10)).await;
4449+
}
4450+
})
4451+
.await
4452+
.unwrap();
4453+
4454+
tx.send(sidecar_control::ControlUpdate::ProviderEnv {
4455+
revision: u64::MAX,
4456+
generation: 11,
4457+
provider_child_env: std::collections::HashMap::from([(
4458+
"TOKEN".to_string(),
4459+
"stale".to_string(),
4460+
)]),
4461+
})
4462+
.unwrap();
4463+
tx.send(sidecar_control::ControlUpdate::AgentProposals {
4464+
enabled: true,
4465+
config_revision: 2,
4466+
})
4467+
.unwrap();
4468+
timeout(Duration::from_secs(1), async {
4469+
while !agent_proposals.enabled() {
4470+
tokio::time::sleep(Duration::from_millis(10)).await;
4471+
}
4472+
})
4473+
.await
4474+
.unwrap();
4475+
let snapshot = provider_credentials.snapshot();
4476+
assert_eq!(snapshot.revision, 2);
4477+
assert_eq!(
4478+
snapshot.child_env.get("TOKEN").map(String::as_str),
4479+
Some("newest")
4480+
);
44094481
handle.abort();
44104482
}
44114483

@@ -4420,6 +4492,7 @@ mod tests {
44204492
provider_credentials,
44214493
agent_proposals.clone(),
44224494
Arc::new(tokio::sync::Mutex::new(None)),
4495+
0,
44234496
);
44244497

44254498
tx.send(sidecar_control::ControlUpdate::AgentProposals {

0 commit comments

Comments
 (0)