Skip to content

feat(server): add HA capacity metrics, scoped locks, and graceful drain - #3710

Open
EmilienM wants to merge 1 commit into
NVIDIA:mainfrom
EmilienM:feat/3528-ha-scaling-signals/EmilienM
Open

EmilienM wants to merge 1 commit into
NVIDIA:mainfrom
EmilienM:feat/3528-ha-scaling-signals/EmilienM

Conversation

@EmilienM

@EmilienM EmilienM commented Sep 25, 2026 •

Copy link
Copy Markdown
Contributor

Summary

Multi-replica gateways need capacity signals, bounded database contention, and a paced shutdown path. This PR adds per-replica metrics, scoped mutation locks, batched watch polling, graceful supervisor-session draining, and an optional Helm HPA.

The PR was initially drafted by Opus 5.5 xhigh and reviewed by GPT-6-Astra xhigh through independent adversarial code and architecture reviews.

Related Issue

Part of #3528: implements its capacity, scoped-lock, batched-watch, and planned-drain work. Draining moves sessions off a stopping replica; it does not balance established sessions after scale-out or decide which replica receives a reconnect. #3661 covers connect-time placement.

The issue has no acceptance or agent-workflow labels. Implementation and review were directly requested; labels are unchanged.

Changes

  • On shutdown, close supervisor admission and report 503 draining on /readyz and /health; /healthz stays healthy. PostgreSQL gateways with a peer endpoint wait 3 seconds for endpoint propagation, then pace session closes over at most 12 seconds while the listener remains available. Ownership cleanup has a separate 10-second timeout. Compute-driver cleanup, trace export, and the gateway OCSF JSONL flush add time, so these budgets do not guarantee exit within the chart's existing 30-second termination grace period.
  • Reset supervisor reconnect backoff after an accepted session and jitter retries. Queue SessionAccepted before exposing a session to relay requests. A draining owner rejects relays promptly after its session becomes unusable.
  • Export bounded-label session, drain, relay capacity/rejection/expiry/latency, peer RPC, mutation-lock, and watch-poller metrics. New latency series use histograms; existing summaries remain compatible.
  • Replace the fleet-wide mutation lock with global/workspace/sandbox intention locks. Provider and workspace-profile writes exclude sandbox mutations in that workspace; global policy/settings and platform-profile writes exclude all mutation scopes. Lifecycle paths use local locks and cross-replica compare-and-swap. The legacy global key preserves mutual exclusion during mixed-version rollouts.
  • Use a dedicated four-connection PostgreSQL lock pool, with deadlines for acquisition and complete connection return, including SQLx's final ping. Lock timeouts return retryable UNAVAILABLE with reason MUTATION_LOCK_TIMEOUT. Provider refresh validates expiry before staging credentials and cleans up staged handles when lock acquisition fails. Each replica can open 10 data connections plus 4 lock connections.
  • Batch watched sandbox version reads in groups of 1,000 IDs. Reconcile startup endpoint status with bounded concurrency and per-sandbox guards; use keyset pagination for restart sweeps.
  • Add optional autoscaling/v2 HPA support with CPU/memory targets, custom metrics, and configurable scale-down behavior. With HPA enabled, omit workload spec.replicas and validate against maxReplicas. Give certgen hook pods separate labels so workload selectors and HPA exclude them.
  • Add PostgreSQL, contention, relay saturation, large-watch-set, and rollout coverage. Update the HA guide, metrics reference, API error reference, and related skills. No protobuf changes or new gateway TOML settings.

Operational limits:

  • A reconnecting sandbox can briefly report Provisioning, making new exec/SSH/forward requests fail with FAILED_PRECONDITION. On Kubernetes, a slower reconnect can also report DependenciesNotReady while the workload continues running. Streams through an exiting gateway disconnect. Existing sandboxes retain old supervisor reconnect behavior until recreated.
  • A lone PostgreSQL replica with a peer endpoint still drains, adding downtime to a StatefulSet restart. helm upgrade --reuse-values retains an older termination grace period unless explicitly overridden.
  • Enabling HPA on an existing release can briefly reset the workload to one replica while HPA takes ownership; the chart README describes migration. HPA behavior with a metrics server/adapter and the PodMonitor example remain unverified.
  • Short drains may be missed at a 30-second scrape interval; the metrics guide recommends 5 seconds for observing them.

Testing

Base: main at 33a8eac1. Fresh verification for this review:

  • Independent adversarial code and architecture reviews by GPT-6-Astra xhigh found no confirmed critical/high/medium findings across the complete 50-file diff. Corrected description claims about scope, shutdown guarantees, pagination, and existing chart defaults. Runtime code is unchanged by this review.
  • cargo test -p openshell-server --features test-support: 1,949 unit tests and 31 integration tests passed; 24 tests ignored in this run.
  • mise run test:rust:postgres: all 17 selected PostgreSQL tests passed against disposable PostgreSQL 17.10, including cancellation cleanup, mixed-version exclusion, contention, batched polling, and stalled-connection recovery.
  • cargo test -p openshell-supervisor-process: 83 passed.
  • mise run helm:test: 242 gateway and 6 workspace tests passed; chart ownership check passed.
  • shellcheck -x tasks/scripts/run-postgres-tests.sh and git diff --check passed.
  • mise run pre-commit: passed, including workspace/e2e/example Clippy, formatting, lockfile checks, Helm lint/docs checks, Markdown/Mermaid, protobuf, Python, TypeScript, and license checks.

Previously reported integration results for the same runtime code; Kubernetes HA, Prometheus, and full CI were not rerun during this review:

  • Kubernetes HA e2e on local kind with rootless Podman, two replicas, Envoy, and PostgreSQL 17: 4 of 5 tests passed. sandbox_file_sync_survives_gateway_pod_rolls failed once when immediate post-roll exec saw Provisioning, then passed five isolated reruns. The rollout test checks drain signaling, eventual Ready state, session accounting, and exec through replacement pods. Its observed exit intervals do not prove every pod avoided SIGKILL or handed off ownership within its termination grace period.
  • Prometheus 3.15.0 discovered and scraped all nine observed gateway pods with namespace/pod labels. The 15 new metric families had at most four label sets per pod; no relay rejections, lock timeouts, or poller errors were observed. promtool check metrics accepted the new metrics and flagged two pre-existing readiness metrics for missing help text. HPA and PodMonitor were not exercised.
  • A previous mise run ci stopped in two unchanged openshell-binary-identity tests because /proc/<pid>/exe access was denied. This is not a full CI pass.

The HA CI lane requires test:e2e-kubernetes, which is not currently applied; PostgreSQL-specific tests have no CI job. Required GitHub checks still depend on maintainer validation of the fork PR.

  • mise run pre-commit passes.
  • Unit tests added/updated.
  • E2E tests added/updated.

Checklist

  • Follows Conventional Commits.
  • Single commit, signed off for DCO.
  • Lock architecture documented in rustdoc; operator behavior documented in published guides (architecture/ was removed from the repository).
  • Related skills updated.

Merge coordination

With #3661 (consistent-hash placement), whichever PR lands second:

  • Membership during drain: pass draining_rx to spawn_membership_worker, so membership is removed when drain starts rather than after its 15-second budget. Peer ring refresh can take 10 seconds, longer than the 3-second propagation delay. After a redirected attempt fails before SessionAccepted, preserve the redirected flag for the fallback hello (redirected = redirected && !accepted) to avoid another redirect to the draining replica.
  • TLS redirects: peer endpoints use pod IPs while gateway certificates cover Service DNS names. Preserve the gateway TLS server name when supervisors dial redirected pod IPs and add a TLS-enabled integration test. OPENSHELL_PEER_TLS_SERVER_NAME configures gateway-to-gateway calls, not the supervisor client.
  • Reconnect loop: keep this PR's single accepted read before the match and ReconnectBackoff::next_delay; retain feat(server): place supervisor sessions by consistent hash #3661's redirect arm and target reset, and drop its per-arm readiness reset.
  • Store API: replace feat(server): place supervisor sessions by consistent hash #3661's removed Store::list_by_type call with list_by_type_after(MEMBER_OBJECT_TYPE, None, MEMBER_LIST_LIMIT). A textual merge alone does not catch this compile failure.
  • Session setup: perform the redirect check before track_session, then use register_awaiting_accept and mark_accepts_relays. New SupervisorHello test literals already use ..Default::default().

#3384 may overlap in Helm values, helpers, and docs, including this PR's new top-level autoscaling.* keys and documentation for the existing podLifecycle.* settings.

@copy-pr-bot

copy-pr-bot Bot commented Sep 25, 2026

Copy link
Copy Markdown

This pull request requires additional validation before any workflows can run on NVIDIA's runners.

Pull request vetters can view their responsibilities here.

Contributors can view more details about this message here.

@EmilienM

Copy link
Copy Markdown
Contributor Author

cc @drew

@mrunalp

mrunalp commented Sep 25, 2026

Copy link
Copy Markdown
Collaborator

/ok to test 14df55d

@mrunalp mrunalp added the test:e2e Requires end-to-end coverage label Sep 25, 2026
@mrunalp mrunalp added this to the OpenShell 0.1.1 milestone Sep 25, 2026
@github-actions

Copy link
Copy Markdown

Label test:e2e applied, but pull-request/3710 does not exist yet. A maintainer needs to comment /ok to test 14df55d7941a7c7e3f4ca6200fd16f15c6d79e46 to mirror this PR. Once the mirror exists, re-apply the label or re-run Branch E2E Checks from the Actions tab.

@sjenning

Copy link
Copy Markdown
Collaborator

/ok to test 14df55d

@FrostGod

FrostGod commented Sep 25, 2026 •

Copy link
Copy Markdown
Contributor

just a heads up, there is slight design change to optimize the current gateway HA workflow.

#3661
please do ensure this PR takes the new changes into account. most likely it still works
and will review this PR myself, and please feel free to review posted PR.

@EmilienM
EmilienM force-pushed the feat/3528-ha-scaling-signals/EmilienM branch from 14df55d to 4097490 Compare September 25, 2026 20:10
@EmilienM

EmilienM commented Sep 25, 2026 •

Copy link
Copy Markdown
Contributor Author

@FrostGod Thanks for the heads-up! I rebased on main (with #3644) and did a local test merge with #3661. They work together fine. There are a few small things for whoever lands second, and I listed them under "Merge coordination" in the description. The main one: #3661's membership worker should stop on draining_rx rather than shutdown_rx, so peers stop redirecting supervisors to a pod that's draining. Let me know if your newer changes affect any of that.

@EmilienM
EmilienM force-pushed the feat/3528-ha-scaling-signals/EmilienM branch from 4097490 to 42a0efe Compare September 25, 2026 23:09
@EmilienM

Copy link
Copy Markdown
Contributor Author

I ran another round of adversarial code and architecture reviews with GPT-6-Astra xhigh on this iteration and addressed the findings. The PR is now ready for review.

@drew drew modified the milestones: OpenShell 0.1.1, OpenShell 0.1.5 Sep 28, 2026
@sjenning

Copy link
Copy Markdown
Collaborator

/ok to test 42a0efe

@mrunalp mrunalp added test:e2e Requires end-to-end coverage and removed test:e2e Requires end-to-end coverage labels Sep 28, 2026
@github-actions

Copy link
Copy Markdown

Label test:e2e applied, but pull-request/3710 does not exist yet. A maintainer needs to comment /ok to test 42a0efe983a0ea0b9282347304c33c04688a9a47 to mirror this PR. Once the mirror exists, re-apply the label or re-run Branch E2E Checks from the Actions tab.

@mrunalp

mrunalp commented Sep 28, 2026

Copy link
Copy Markdown
Collaborator

/ok to test 42a0efe

@EmilienM
EmilienM force-pushed the feat/3528-ha-scaling-signals/EmilienM branch 3 times, most recently from 3f58e4d to a8be556 Compare September 29, 2026 19:53
Multi-replica gateways need capacity signals, paced supervisor handoff,
and bounded shared-database contention for production scaling.

On SIGTERM every gateway reports 503 "draining" on /readyz and /health.
Peer-routed PostgreSQL gateways then drain their supervisor sessions
before stopping their listener. Supervisors reset their reconnect
backoff after an accepted session and jitter retries, and a new session
receives relays only after SessionAccepted is queued. Expose
per-replica session, relay, peer-RPC, mutation-lock, and watch-poller
metrics with bounded labels.

Replace the fleet-wide mutation guard with global/workspace/sandbox
intention locks while preserving the legacy global key for rolling
upgrades. Use a dedicated four-connection PostgreSQL lock pool with
bounded acquisition and return. Lock timeouts fail with UNAVAILABLE,
reason MUTATION_LOCK_TIMEOUT, and a one-second retry delay. Provider
refresh validates the credential expiry before staging and removes
staged credentials when lock acquisition fails. Reconcile startup
endpoint status per sandbox and batch watched resource-version reads in
groups of 1,000.

Add optional autoscaling/v2 HPA support and document drain budgets against
the chart's existing 30-second termination grace default. Give certgen
hook pods their own labels so gateway selectors and the HPA no longer
match them. Document placement, rollout, connection-pool, and HPA
limitations. Add PostgreSQL concurrency and stalled-connection tests,
credential cleanup coverage, Kubernetes rollout tests, and a
test:rust:postgres task.

Part of NVIDIA#3528

Signed-off-by: Emilien Macchi <emacchi@redhat.com>
@EmilienM
EmilienM force-pushed the feat/3528-ha-scaling-signals/EmilienM branch from a8be556 to dbe29e6 Compare September 29, 2026 20:08
@mrunalp

mrunalp commented Sep 29, 2026

Copy link
Copy Markdown
Collaborator

/ok to test dbe29e6

@EmilienM

Copy link
Copy Markdown
Contributor Author

Can someone add the test:e2e-kubernetes label for running more tests?

@mrunalp mrunalp added the test:e2e-kubernetes Requires Kubernetes end-to-end coverage label Sep 29, 2026
@github-actions

Copy link
Copy Markdown

Label test:e2e-kubernetes applied for dbe29e6. Open the existing run and click Re-run all jobs to execute with the label set. The run will execute Kubernetes HA and credential-driver E2E after building the required gateway, sandbox, and supervisor images once. This is an optional proof-of-life suite; failures are visible in the workflow run but do not publish a required CI gate status.

@EmilienM

Copy link
Copy Markdown
Contributor Author

The Kubernetes HA e2e lane passed on dbe29e63 (job): all 5 tests in kubernetes_ha_rebalancing ran and passed in about 7 minutes, with no retries. That includes the ones this PR adds:

  • supervisor_sessions_redistribute_across_gateway_pod_rolls scales the gateway to two replicas, creates four sandboxes, and checks that the per-pod openshell_server_supervisor_sessions gauges add up to the Ready sandboxes. It then runs kubectl rollout restart and watches the old pods: at least one reports openshell_server_draining=1, only old pods drain, and each exits within 28 seconds, so the drain finished instead of being cut off by the 30-second grace period. Afterwards every sandbox is Ready with exactly one session on the new pods, and exec works through each new pod and through the regular gateway endpoint.
  • prometheus_count_reads_unlabelled_samples_only and terminating_pod_exit_time_runs_from_first_sighting_to_first_listing_without_it are small unit tests for the helpers the rollout test uses to read /metrics and time terminating pods.

The PR also makes sandbox_exec_rebalances_across_gateway_scale_and_rollout wait for the removed pod to finish draining before it execs, since a moved sandbox briefly reports Provisioning. That test and sandbox_file_sync_survives_gateway_pod_rolls passed too.

Thanks for adding the label!

@FrostGod

FrostGod commented Sep 30, 2026 •

Copy link
Copy Markdown
Contributor

Could we split this into smaller PRs, particularly separating the scoped-lock redesign, graceful draining and the observability additions?

The lock changes alter concurrency guarantees across provider, policy, sandbox, and lifecycle operations,

while draining changes shutdown and reconnect behavior and needs coordination with #3661.

Reviewing those independently would make the invariants, validation, and rollback boundaries much clearer.

Metrics can land first where they observe existing behavior; feature-specific metrics can accompany their implementation. HPA also looks suitable within the Metrics PR.

pods, and checks that the new pods' `openshell_server_supervisor_sessions` add
up to the Ready sandbox count. It scrapes each pod through
`kubectl get --raw /api/v1/namespaces/<ns>/pods/<pod>:9090/proxy/metrics`.
Gateway pods take up to 30 seconds to terminate because each drains its

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[general obs] A lone PostgreSQL replica still drains even though no peer can receive its sessions, adding up to 15 seconds.

propagation_delay_ms = supervisor_session::DRAIN_PROPAGATION_DELAY.as_millis(),
max_close_window_ms = supervisor_session::DRAIN_CLOSE_WINDOW.as_millis(),
"Draining supervisor sessions before stopping the gateway listener"
);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Having peer routing configured does not guarantee another gateway is available. On a single-replica PostgreSQL StatefulSet, this adds shutdown delay without a destination for the sessions. Could we skip the paced drain when no other healthy replica is available?


// Gauges
pub const SUPERVISOR_SESSIONS: &str = "openshell_server_supervisor_sessions";
pub const DRAINING: &str = "openshell_server_draining";

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do we need a separate draining metric? Short drains may finish between scrapes.

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

test:e2e Requires end-to-end coverage test:e2e-kubernetes Requires Kubernetes end-to-end coverage

Projects

None yet

Development

Successfully merging this pull request may close these issues.

5 participants