Repository navigation
Conversation
Signed-off-by: Drew Newberry <anewberry@nvidia.com>
|
Auto-sync is disabled for draft pull requests in this repository. Workflows must be run manually. Contributors can view more details about this message here. |
|
🌿 Preview your docs: https://nvidia-preview-pr-4864.docs.buildwithfern.com/openshell |
Signed-off-by: Drew Newberry <anewberry@nvidia.com>
Signed-off-by: Drew Newberry <anewberry@nvidia.com>
Signed-off-by: Drew Newberry <anewberry@nvidia.com>
Signed-off-by: Drew Newberry <anewberry@nvidia.com>
matthewgrossman
left a comment
There was a problem hiding this comment.
Review of head 06cd2e2 from a multi-agent review panel (security, correctness, API/UX, and scope reviewers, plus a verification and triage step). I checked the one blocking finding against the code by hand. The reviewers read the code but didn't build it or run tests.
Summary: a large but coherent HA feature for #4860. The security reviewer found no new problems with the takeover grants, credential-epoch fencing, or the state preserved during adoption. One correctness issue should be fixed before merge. Everything else is polish or follow-up.
Fix before merge
- A refused pool launch leaves its assignment record and launch material behind, and another replica can later give it a takeover owner (
supervisor_pool.rs:1731).
Small fixes
- A config error message contains runs of spaces (
lib.rs:1876). - The pool/non-pool mismatch errors don't name the setting to change (
controller.rs:1037). - The HA docs don't say what an upgrade does to running sandboxes (
high-availability.mdx).
Follow-ups (fine as separate issues)
- Member freshness and report ordering compare wall-clock times written by different replicas.
- A design record (RFC) for pool ownership, takeover grants, and epoch fencing.
- Decide whether the non-pool shared-supervisor mode stays, since Helm always enables the pool.
Considered and not raised: splitting this ~24k-line PR into smaller PRs. It's one coherent, tested feature, so that's your call.
Details are in the inline comments.
| } | ||
| } | ||
|
|
||
| fn fresh(&self, record: &ObjectRecord) -> bool { |
There was a problem hiding this comment.
Follow-up (non-blocking): cross-replica clock comparisons
fresh() compares this replica's now_ms() with an updated_at_ms that another replica wrote from its own clock. write_member and record_reports also order streams by connected_at_ms from different replicas. With a 15 s TTL, a few seconds of skew between gateway replicas could make a live member look expired, which would trigger an unneeded takeover, or could let a stale stream's report win.
Options: use database time (now() in the UPDATE) for freshness and ordering across both the postgres and sqlite stores, or document a clock-sync requirement for multi-replica gateways. Fine as a follow-up issue.
| > **Implementation note ([#4860](https://github.com/NVIDIA/OpenShell/issues/4860)).** | ||
| > The built-in OpenShell runtime adds an explicit, capability-gated exception | ||
| > for highly available shared supervisors | ||
| > (`openshell.sandbox.supervisor-takeover`, protocol 1.1). A replacement |
There was a problem hiding this comment.
Follow-up (non-blocking): design record for pool ownership
This PR adds durable membership and ownership with CAS terms, gateway-signed takeover grants, credential-epoch fencing, a Sandbox Protocol minor bump, and a Helm default change. The RFC side is this short note. A short RFC, or an extension of RFC 0012, recording the trust model, the invariants, and the non-goals (multi-gateway partitions, node loss) would make later changes easier to review. It doesn't need to block this PR.
Signed-off-by: Drew Newberry <anewberry@nvidia.com>
Compatibility review (advisory)New or updated breaking changes; see the full report for all findings. 1. cli — No explicit risk acceptance foundThe existing
2. rust-sdk — No explicit risk acceptance found
Review details
Advisory review; publication gates are unchanged. |
TaylorMutch
left a comment
There was a problem hiding this comment.
Reviewed head abffde246 locally. I found two correctness issues, each reproduced with a focused Rust test against this commit; details and reproduction steps are inline.
Validation: 30 supervisor Helm tests, the render-only 1/2/3-replica scaling check, 13 sandbox authentication tests, and the original 12 supervisor-control tests passed. The two additional regression assertions fail at the expected behavior checks. Temporary source changes were restored. These reproductions exercise the Rust controller and SQLite-backed pool; I did not run live Kubernetes recovery E2Es or PostgreSQL validation.
| self.complete_takeover(record.clone()).await | ||
| } | ||
| Some(record) if takeovers_allowed && !owner_alive(&record.payload, &members) => { | ||
| self.take_over(record, &ring, &members, &charges).await |
There was a problem hiding this comment.
Confirm the owner's current membership before taking it over
This pass reads members before it lists owners. A healthy member can join and another gateway can admit a sandbox onto it between those reads. The owner is then visible here, but its member is absent from the earlier membership result, so owner_alive returns false and the pass takes over a healthy launch. This unnecessarily advances the credential epoch and revokes the launch owner's credentials, disrupting a sandbox whose supervisor did not fail.
Reproduced on abffde246 using the existing fixture and listing hook: start with m1; pause reconcile_once in the report listing after membership was read (fill one LIST_LIMIT page and use pause_listing_after_first_page(REPORT_OBJECT_TYPE)); register m2; use another replica's place_launch for a sandbox the ring places on m2; resume reconciliation. The assertion that ownership remains (m2, epoch 1) fails: it becomes (m1, epoch 2), with a granted takeover and previous_member_id = m2. The focused test runs to that assertion in 0.17 seconds.
Please confirm the specific owner's member record, including its process, freshness, and leaving state, before recording an intent based on apparent failure, and retain a regression for this ordering.
| @@ -390,16 +786,14 @@ impl SupervisorHostController { | |||
| { | |||
| return ObservedInstance::Reported(instance.clone()); | |||
There was a problem hiding this comment.
Fence local observations by the acknowledged ownership term
When a route returns to the same still-running supervisor process at a later ownership term, this returns the process's earlier status before checking whether it acknowledged the current assignment. member_reports() correctly rejects the earlier term, but local observed() reads still accept it. Consequently a recovering sandbox can be reported ready before its new owner has applied the takeover; an earlier terminal status can instead become Unavailable through SharedSupervision::supervision() and cause the Kubernetes driver to suspend the workload.
Reproduced on abffde246 by adding assert_eq!(controller.observed("rt-a"), ObservedInstance::Pending); to reports_count_only_for_the_term_their_stream_acknowledged, immediately after the term-3 snapshot is sent, the term-1 status is recorded, and terms().is_empty() is asserted. It fails with Reported(SandboxInstanceStatus { phase: Supervising, ready: true, ... }) instead of Pending. The existing test already models the ownership transition from m1/p1 to another member and back before either snapshot was applied.
Please apply the current-term acknowledgement fencing to local observations as well, and extend that regression to assert observed() remains pending until the later term is acknowledged.
Summary
Add high availability for the shared supervisor introduced by #4702. A hash ring places new sandbox generations, while durable ownership and gateway-signed takeover grants let a replacement supervisor adopt existing runtimes, processes, and memory without restarting workloads. Shared supervisors always use pool coordination, including at one replica.
Compute drivers are unchanged from base
9a85667d0. Coordination remains in the gateway and supervisor control layer.Related Issue
Closes #4860
Changes
[openshell.gateway.supervisor_pool]optionally tunes the recovery deadline (60 seconds by default); member identity is required, and configuration errors name the setting to fix. Per-sandbox supervision remains available.supervisor.replicasscales 1 → 2 → 1 without changing gateway configuration or replacing surviving members.Release Notes
Shared supervisors always run as a pool; set
supervisor.replicasto change capacity. One replica has no spare for immediate failover. During takeover, workloads can briefly freeze and existing proxied connections or interactive sessions must reconnect. Workloads terminate if supervision is not restored within the recovery deadline.Testing
helm:test, rendered-TOML loader validation, count-independent 1/2/3 renders, Helm lint/docs, affected Clippy with warnings denied, formatting, and Markdown lint passed.abffde246: 1 → 2 → 1 passed (85.35 s), plus all seven helper tests. Workload Pod UID/generation/PID/start ticks/in-memory counter and gateway/surviving-supervisor identity stayed unchanged; a new sandbox worked after downscale. The handed-off workload had two transient exec and network probe failures, with a 6.38 s maximum gap between successful probes (including the 2 s sampling interval); the unaffected workload had no failed probes.Live Kubernetes validation uses one local k3d node. Physical node loss and multi-gateway partitions are not exercised. Cross-replica wall-clock comparisons and a standalone ownership design record remain follow-ups.
Pool members share the supervisor credential, so declared member identity is within that trust boundary. Identity-bound provider placeholders without stable handles can fail closed after takeover if their provider revision changed. Service discovery adds bounded periodic probes per pool sandbox. A crash between credential creation and candidate staging can leave orphaned material, as in the existing SSH host-key path.
Checklist