Repository navigation
Conversation
|
🌿 Preview your docs: https://nvidia-preview-pr-4950.docs.buildwithfern.com/openshell |
This comment was marked as outdated.
This comment was marked as outdated.
81d00f9 to
67c24b1
Compare
After the main process exits, the supervisor keeps its access plane, and SSH and port forwarding already reached Completed and Error sandboxes while it stayed connected. Exec still required Ready in both the CLI and the gateway, so it was refused on the same sandboxes. Exec, SSH sessions, and TCP forwarding now share one gate: a sandbox is reachable when it is Ready, or Completed/Error with a supervisor session on this replica or a fresh owner record naming a peer replica. The previous check only looked at the local session map, so on a multi-replica gateway a request landing on a non-owner replica was refused even though relay opening routes to the owner. The CLI exec pre-check and the service forward recheck accept Ready, Completed, and Error and leave the supervisor decision to the gateway. Signed-off-by: Piotr Mlocek <pmlocek@nvidia.com>
…upervisor Exposed-service HTTP routing still required Ready while port forwarding to the same sandbox kept working after the main process exited. Service routing now uses the shared relay reachability gate, so a retained Completed or Error sandbox serves its exposed services while its supervisor stays connected. Document what stays reachable after a retained sandbox's main process exits, and that --no-keep sandboxes are deleted once it does. Signed-off-by: Piotr Mlocek <pmlocek@nvidia.com>
Add two CLI conformance tests, each run for exit codes 0 and 3: an ephemeral (--no-keep) sandbox returns the exit code and is deleted, and a retained sandbox records Completed or Error with the exit code and still accepts exec. Signed-off-by: Piotr Mlocek <pmlocek@nvidia.com>
Add a phase diagram to the Sandbox Lifecycle section and describe what happens after the main process exits. A --no-keep sandbox is deleted. A retained sandbox keeps its compute and still accepts exec, file transfer, port forwarding and exposed services while its supervisor is connected. sandbox connect fails because it attaches to the exited main process, so the docs point to sandbox exec --tty for a shell. Signed-off-by: Piotr Mlocek <pmlocek@nvidia.com>
…gate Exec, interactive exec, SSH sessions, port forwarding, and exposed services now call sandbox_access::acquire and open their relays through the returned SandboxAccess handle. The phase rules live in one module, so anything that must happen before a sandbox can be used applies to every entry point. supervisor_session keeps only the replica-aware supervisor_connected check. Signed-off-by: Piotr Mlocek <pmlocek@nvidia.com>
Exec no longer checks the sandbox phase before calling the gateway; when the gateway refuses, the CLI names the phase it saw. Service forwarding stops only when the sandbox is gone or being deleted, and leaves each connection to the gateway's access check. Signed-off-by: Piotr Mlocek <pmlocek@nvidia.com>
Signed-off-by: Piotr Mlocek <pmlocek@nvidia.com>
Exposed services do not authenticate callers, so they now reach an Error sandbox only when its main process failed. Exec, SSH, and forwarding keep the debugging path into an infrastructure Error. The CLI service forward stops again once its sandbox is stopping, stopped, or being deleted, and the TypeScript SDK forward applies the same check instead of requiring Ready. Exec refusals name what makes the sandbox usable again. supervisor_connected skips the owner index on a single replica, the unused relay error variant is gone, and relay opening outside sandbox_access is crate-private. Signed-off-by: Piotr Mlocek <pmlocek@nvidia.com>
67c24b1 to
ce03ae9
Compare
| `sandbox connect` fails, because it attaches to the main process and that | ||
| process has exited. | ||
|
|
||
| An `Error` caused by an infrastructure failure, such as |
There was a problem hiding this comment.
Error covers two cases here: a main process that exited nonzero (MainProcessFailed) and an infrastructure failure. We considered giving a failed or exited main process its own phase, and also reusing Completed for every exit. We decided against both for this PR:
- Older SDKs treat
Completedas success inwait_ready, so a failed run would be reported as a successful one. - A new phase reaches older clients as an unknown value, and they keep polling until they time out.
This PR keeps the existing phases. The access rule doesn't need a new phase to be correct.
Signed-off-by: Piotr Mlocek <pmlocek@nvidia.com>
|
|
||
| /// Phases in which no forwarded connection can succeed until another command | ||
| /// starts or recreates the sandbox. | ||
| fn forward_ended_by_phase(phase: i32) -> bool { |
There was a problem hiding this comment.
Does this function not represent a decision by the CLI? Would adding a property to the sandbox or a filter on the GetSandbox request make sense?
elezar
left a comment
There was a problem hiding this comment.
Thanks for tackling this. A single acquire gate with a capability handle is a good fit for the problem, and the multi-replica owner-record handling reads correctly. I have no blockers on the gateway logic. My comments fall into two groups:
- Exposed services after exit. This is the one behavioural expansion I'd like an explicit decision on (comments on
service_routing.rsandsandbox_access.rs). - Clients re-implementing the gateway's rules. The CLI and SDK keep their own phase lists, which weakens the "gateway is the only place that decides" design. Carrying the refusal reason in the error would remove them (comments on
sandbox_access.rs,run.rs,client.ts). These are non-blocking.
Test coverage. CI conformance passes on Docker, Podman, k3s and k3s-ha (three replicas), which is good coverage, and the new retained-sandbox tests run there. Two gaps remain:
- It would be worth confirming that the k3s-ha run served a request on a replica other than the one holding the supervisor, or adding a test that forces it. Otherwise only the
peer_holds_sessionunit test covers the cross-replica path directly. - I don't see end-to-end coverage for exposed services on a
Completedsandbox. Only the unit testcompleted_sandbox_routes_services_only_while_supervisor_is_livecovers it.
| } | ||
| // Every request passes the access gate, including ones served from a | ||
| // pooled upstream connection. | ||
| let access = match sandbox_access::acquire_for_exposed_service(&state, &sandbox).await { |
There was a problem hiding this comment.
This is the one change I'd like an explicit decision on. Before, a service request to a Completed sandbox (or an Error one whose main process failed) was refused. Now it's relayed through the retained supervisor, and the gateway doesn't authenticate service callers (auth is the service's own, per ServiceAuthorizationMode). If anything a job left behind is still listening on the endpoint's port, anyone with the service URL can reach it after exit.
Two questions: (a) does the supervisor tear down the workload's process tree on main-process exit, so nothing can be listening? If so, a test or a note here would settle it. (b) What's the use case for serving after exit? #4949 needs exec, SSH and file transfer, which are all authenticated. If services don't need it, I'd limit acquire_for_exposed_service to Ready. Otherwise I'd make it opt-in per endpoint.
| sandbox: &Sandbox, | ||
| ) -> Result<SandboxAccess, AccessError> { | ||
| if sandbox.phase() == SandboxPhase::Error as i32 | ||
| && !crate::compute::is_failed_main_process_result(sandbox) |
There was a problem hiding this comment.
Related to the above. The failed-main-process Error carve-out for exposed services is the most permissive unauthenticated case, so it's worth stating the reasoning in the doc comment. If post-exit service access is deliberate, say why. If it isn't, this branch goes away together with the previous comment.
| #[derive(Debug)] | ||
| pub enum AccessError { | ||
| /// The sandbox is in a state that does not allow access. | ||
| NotReachable, |
There was a problem hiding this comment.
Suggestion: let NotReachable carry a reason and attach it as a google.rpc.ErrorInfo on the existing FailedPrecondition, keeping the message "sandbox is not ready". acquire already knows which branch it took. The repo already does this (openshell-core::rpc_error, and provider.rs keys on an ErrorInfo reason), so it follows convention.
Something like: PhaseNotServing, Stopped, Deleting, SupervisorDisconnected, ServiceInfraError, with the phase in metadata. That needs splitting Stopped/Deleting out of the _ => false arm. Older clients are unaffected, and newer clients against an older gateway fall back to the raw status.
|
|
||
| /// The gateway decides whether a sandbox can be used. When it refuses, name the | ||
| /// phase this command saw and what would make the sandbox usable again. | ||
| fn exec_start_error(status: Status, sandbox: &Sandbox) -> miette::Report { |
There was a problem hiding this comment.
This matches the gateway's message text ("sandbox is not ready") and picks a hint from a phase fetched earlier. Two problems: a reworded gateway message silently drops the hint, and "supervisor is no longer connected" can be wrong when the refusal had another cause (a Lookup error, or the phase changed since the fetch). With the reason from the sandbox_access.rs comment, this becomes a match on the validated ErrorInfo, in the style of provider_mutation_is_uncertain, and uses the gateway's phase.
|
|
||
| /// Phases in which no forwarded connection can succeed until another command | ||
| /// starts or recreates the sandbox. | ||
| fn forward_ended_by_phase(phase: i32) -> bool { |
There was a problem hiding this comment.
This and the SDK check (sdk/typescript/src/client.ts:1754) keep their own copy of "stopping, stopped or deleting means no connection can succeed", which is a reachability rule outside the gateway. It'll drift (Suspended already differs). Related: a forward against Starting or Suspended used to fail up front with "not ready". Now it binds and every connection fails at the gateway.
Suggestion: at startup, ask the gateway and act on the reason from the sandbox_access.rs comment. Give up on Stopped or Deleting. For phases that can still become reachable, wait with a bounded retry and then fail with the gateway's reason. That removes both phase lists and restores a clear startup error. If you'd rather keep the lists, a comment pointing at sandbox_access.rs and a parity test would reduce the drift risk.
| const ref = found.ref; | ||
| if (ref.phase !== 'ready') { | ||
| throw new SdkError('connect', `sandbox '${name}' is not ready (phase: ${ref.phase})`); | ||
| if (ref.phase === 'stopping' || ref.phase === 'stopped' || ref.phase === 'deleting') { |
There was a problem hiding this comment.
Same point as the run.rs comment on forward_ended_by_phase: this is a second copy of the reachability phase list, and it moves the failure for Starting/Suspended from startup to each connection. Reading the reason from the gateway would let both clients drop it.
| } | ||
|
|
||
| #[tokio::test] | ||
| async fn completed_sandbox_routes_services_only_while_supervisor_is_live() { |
There was a problem hiding this comment.
Good unit coverage for the gate. Since this is the expansion with the most security weight (see the comment on proxy_to_endpoint), an end-to-end check would be worth having, in either direction: that a service on a Completed sandbox is refused (if you take the Ready-only option), or that it's served and nothing unintended is reachable (if you keep it). Right now CI has no conformance test that touches it.
|
#5277 adds a If this PR lands first, update |
Summary
A retained sandbox stays reachable after its main process exits, but only through some of the ways into it. This PR puts every way in behind one access check, so they all agree on when a sandbox can be used.
Related Issue
Closes #4949
What's inconsistent today
Each entry point checks the sandbox phase on its own, and they don't agree:
ReadyCompleted/ErrorFor example, a job writes
result.txtand exits 0. The sandbox moves toCompletedand its supervisor stays connected. Then:With several gateway replicas, even the paths that do work after exit are unreliable. They only look for the supervisor on the replica that received the request, so the same
downloadworks or fails depending on where the load balancer sends it.What changes
One rule now covers every way in:
Ready: reachable.CompletedorError: reachable while the supervisor is connected to any replica.Exposed services have one extra limit. They don't authenticate callers, so they reach an
Errorsandbox only when its main process failed. A sandbox inErrorbecause of an infrastructure failure stays reachable for debugging only through the authenticated paths.The rule lives in a new gateway module,
sandbox_access. Every entry point callssandbox_access::acquireand opens its relay through the handle it returns, so none of them can skip the check:flowchart LR exec[ExecSandbox] --> acquire iexec[ExecSandboxInteractive] --> acquire ssh[CreateSshSession] --> acquire fwd[ForwardTcp<br/>SSH, file transfer, port forward] --> acquire svc[Exposed services<br/>every request] --> acquire acquire{"sandbox_access::acquire<br/>phase + supervisor on any replica"} -->|SandboxAccess| relay[open_relay] --> sup[Supervisor] acquire -->|refused| err[FailedPrecondition<br/>sandbox is not ready]The same example after this PR:
The gateway is the only place that decides. The CLI no longer checks the phase before exec. When the gateway refuses, the CLI error still names the phase it saw and what to do about it. A service forward in the CLI or the TypeScript SDK stops only when its sandbox is stopping, stopped or being deleted, and leaves the per-connection decision to the gateway.
New ways into a sandbox should go through
acquireas well. So should anything that has to happen before a sandbox can be used, such as resuming a suspended sandbox.Changes
sandbox_accessmodule. Exec, interactive exec, SSH sessions, port forwarding and exposed services go through it. Exposed services check access on every request, including requests served over a pooled upstream connection.supervisor_session::supervisor_connectedchecks all replicas through the owner record.--no-keepsandboxes are deleted.tests/suites/conformance/tests/cli.rs, each run for exit codes 0 and 3. A--no-keepsandbox is deleted. A retained sandbox records the exit code and still accepts exec.Testing
I ran the conformance checks against a local Docker gateway before the
sandbox_accessrefactor and before the conformance suite moved to testkit. Unit tests and the CLI exec integration tests cover the refactor. I haven't run the testkit version of the conformance tests, and I haven't tested Kubernetes, Podman or a real multi-replica gateway live.Still broken and tracked in #4949: after a Docker gateway restart,
CompletedandErrorsandboxes lose their supervisor and can't be reached.Checklist