Skip to content

Commit b9972d5

Browse files
pimlockclaude
andcommitted
fix(server): hold pushed updates until the bootstrap result arrives
A publication that raced a session's bootstrap rebuilt both components and sent them before the supervisor reported its bootstrap result, so the gateway could not tell they matched what the supervisor already had. In e2e runs a third of sandbox configuration and provider environment deliveries were applied as IGNORED_DUPLICATE. Treat the bootstrap like an update in flight: a rebuild that arrives first waits as the pending snapshot, and is dropped when the bootstrap result acknowledges the same snapshot or sent otherwise. The hold is bounded by the same acknowledgement timeout as live updates. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Signed-off-by: Piotr Mlocek <pmlocek@nvidia.com>
1 parent 470ce8b commit b9972d5

3 files changed

Lines changed: 258 additions & 41 deletions

File tree

‎crates/openshell-server/src/config_delivery/mod.rs‎

Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1027,6 +1027,10 @@ mod tests {
10271027
}
10281028

10291029
async fn put_provider(state: &ServerState, name: &str) {
1030+
put_provider_with_token(state, name, None).await;
1031+
}
1032+
1033+
async fn put_provider_with_token(state: &ServerState, name: &str, token: Option<&str>) {
10301034
state
10311035
.store
10321036
.put_message(&Provider {
@@ -1037,12 +1041,39 @@ mod tests {
10371041
..Default::default()
10381042
}),
10391043
r#type: "github".into(),
1044+
credentials: token
1045+
.map(|token| HashMap::from([("GITHUB_TOKEN".to_string(), token.to_string())]))
1046+
.unwrap_or_default(),
10401047
..Default::default()
10411048
})
10421049
.await
10431050
.unwrap();
10441051
}
10451052

1053+
/// Commit a new policy revision, so the next sandbox configuration build
1054+
/// differs from the bootstrap the session acknowledged.
1055+
async fn change_policy(state: &ServerState, sandbox_id: &str, version: i64) {
1056+
use crate::policy_store::PolicyStoreExt as _;
1057+
use prost::Message as _;
1058+
1059+
let policy = openshell_core::proto::SandboxPolicy {
1060+
version: 1,
1061+
..Default::default()
1062+
};
1063+
state
1064+
.store
1065+
.put_policy_revision(
1066+
&uuid::Uuid::new_v4().to_string(),
1067+
sandbox_id,
1068+
"default",
1069+
version,
1070+
&policy.encode_to_vec(),
1071+
&crate::grpc::policy::deterministic_policy_hash(&policy),
1072+
)
1073+
.await
1074+
.unwrap();
1075+
}
1076+
10461077
/// Connect a streamed-apply supervisor. Without image policy discovery the
10471078
/// gateway skips startup preparation and accepts it with its bootstrap.
10481079
async fn accepted_session(
@@ -1060,6 +1091,28 @@ mod tests {
10601091
let Some(gateway_message::Payload::SessionAccepted(accepted)) = first.payload else {
10611092
panic!("expected SessionAccepted");
10621093
};
1094+
// Live updates wait for the bootstrap result, as from a real
1095+
// supervisor; the gateway answers it with the admission.
1096+
if let Some(bootstrap) = accepted.bootstrap {
1097+
harness
1098+
.outbound
1099+
.send(openshell_core::proto::SupervisorMessage {
1100+
payload: Some(
1101+
openshell_core::proto::supervisor_message::Payload::ConfigBootstrapResult(
1102+
session::applied_bootstrap_result(&bootstrap),
1103+
),
1104+
),
1105+
})
1106+
.await
1107+
.unwrap();
1108+
let admission = next_update(&mut harness, Duration::from_secs(5))
1109+
.await
1110+
.expect("configuration admission");
1111+
assert!(matches!(
1112+
admission.payload,
1113+
Some(gateway_message::Payload::ConfigurationAdmission(_))
1114+
));
1115+
}
10631116
(harness, accepted.session_id)
10641117
}
10651118

@@ -1119,6 +1172,7 @@ mod tests {
11191172
let state = push_state().await;
11201173
put_sandbox(&state, "owned-sandbox", &[]).await;
11211174
let (mut harness, session_id) = accepted_session(&state, "owned-sandbox").await;
1175+
change_policy(&state, "owned-sandbox", 1).await;
11221176

11231177
let response =
11241178
handle_peer_notify_config_update(&state, peer_notify("owned-sandbox", &session_id))
@@ -1139,6 +1193,7 @@ mod tests {
11391193
put_sandbox(&state, "unattached", &[]).await;
11401194
let (mut attached, _) = accepted_session(&state, "attached").await;
11411195
let (mut unattached, _) = accepted_session(&state, "unattached").await;
1196+
put_provider_with_token(&state, "github", Some("rotated")).await;
11421197

11431198
publish_provider_components(&state, "default", "github", ConfigComponents::ALL);
11441199

@@ -1154,6 +1209,7 @@ mod tests {
11541209
.is_none()
11551210
);
11561211

1212+
change_policy(&state, "unattached", 1).await;
11571213
publish_workspace_components(&state, "default", ConfigComponents::SANDBOX_CONFIG);
11581214
assert!(
11591215
next_update(&mut unattached, Duration::from_secs(5))
@@ -1306,6 +1362,7 @@ mod tests {
13061362
put_sandbox(&state, "sandbox", &[]).await;
13071363
let (mut harness, _) = accepted_session(&state, "sandbox").await;
13081364
assert!(has_recipient(&state, "sandbox"));
1365+
change_policy(&state, "sandbox", 1).await;
13091366

13101367
publish_sandbox_components(&state, "sandbox", ConfigComponents::SANDBOX_CONFIG);
13111368
let update = next_update(&mut harness, Duration::from_secs(5))

0 commit comments

Comments
 (0)