Repository navigation
feat(server): scope mutation locks and bound lock waits - #4147
Conversation
26c5f0d to
71b9c52
Compare
|
Label |
|
Label |
|
/ok to test 71b9c52 |
PR Review StatusThe independent review of scoped mutation locks, bounded waits, and provider cleanup found no blocking findings. Current-head Branch Checks, Helm Lint, Trivy Changes, and E2E workflows are queued or running; Gator will monitor their results. Blocking findings: None. Gator metadata
|
71b9c52 to
51822ba
Compare
|
/ok to test 51822ba |
PR Review StatusThe independent follow-up review found no blocking findings in the author’s changes across the rebase, including settings row identity checks, provider-name validation, SSH identity lock-pool separation, and lifecycle lock adaptations. The stale test mirror has been refreshed, and current-head Branch Checks, Helm Lint, Trivy Changes, and E2E workflows are running; Gator will monitor their results. Blocking findings: None. Gator metadata
|
51822ba to
c00b632
Compare
|
/ok to test c00b632 |
The mutation lock ordering rules were prose only, so a nested guard on a different key passed every test and could only deadlock under contention. Debug builds now register each held guard with the Tokio task (or the thread, outside a task) that acquired it, and panic before waiting when a local key, a blocking lifecycle gate or the SSH identity lock would break rules 1, 4 or 5. Startup endpoint-status reconciliation and the tests that hold a guard while driving guarded code run under lock_order::branch(), which marks deliberate concurrency within one task. Release builds compile the check out. Part of NVIDIA#3528 Signed-off-by: Emilien Macchi <emacchi@redhat.com>
When a replacement supervisor session failed its endpoint-status reset, the spawned cleanup task kept the session lifetime guard through retry_endpoint_status_after_supervisor_disconnect. That retry only ends once the reset is durable, so a gateway shutdown during a database or lock outage waited the full 10 s and reported ownership cleanup as incomplete. The guard now covers only the lifecycle demotion, as in finish_supervisor_session, and a SQLite test checks that shutdown completes while the retry is still blocked. Part of NVIDIA#3528 Signed-off-by: Emilien Macchi <emacchi@redhat.com>
Platform-scope provider profile writers (import, update, delete) pass an
empty workspace on purpose and need X(global), because they validate
sandboxes in every workspace. MutationScope::profiles() now names that
escalation and picks Global for the platform scope, instead of relying
on MutationScope::Workspace("") to mean it. An empty workspace name now
trips a debug assertion, and release builds keep X(global) as a
fail-safe that over-locks rather than under-locks.
Part of NVIDIA#3528
Signed-off-by: Emilien Macchi <emacchi@redhat.com>
LocalMutationLocks swept its whole table on every key lookup, so a burst of N concurrent acquisitions on one replica cost O(N^2) under a blocking mutex. The table is now swept only when a new key grows it past the larger of 64 and twice the live entries left by the last sweep, which makes acquisition amortized O(1). Released entries, including those of cancelled waiters, stay bounded at about twice the peak live set. The lifecycle gate registry keeps its per-call sweep. Part of NVIDIA#3528 Signed-off-by: Emilien Macchi <emacchi@redhat.com>
Both stores hardcoded their pool ceiling: 10 connections for Postgres, 5 for on-disk SQLite. One pool is shared by every database-backed RPC, so that number is also the gateway's ceiling on concurrent database work. Once every connection is checked out, callers queue on acquire and sqlx logs "time to acquire exceeded slow threshold"; sandbox creates then time out and are retried, which adds load rather than shedding it. The right ceiling is deployment-specific — it depends on how many gateway replicas share the database and what max_connections the server itself allows — so it cannot be a single number baked into the binary. Expose it on the same config surface as the rest of the gateway's settings: --db-max-connections, OPENSHELL_DB_MAX_CONNECTIONS, and the TOML key database_max_connections, resolved in that precedence order. Helm deployments set the key through gatewayConfig. Omitting it keeps each backend's previous value, so existing deployments do not move. Values below 1 are rejected rather than silently replaced by the default: a zero pool would block every acquire, and quietly ignoring a typo would reproduce the ceiling the operator is trying to lift. An in-memory SQLite database stays pinned to one connection, since the database lives inside that connection. Refs: NVIDIA#2561 Signed-off-by: Bryce Wilkinson <22760097+bjw123@users.noreply.github.com> (cherry picked from commit 33517b3) [emacchi: dropped the server.dbMaxConnections Helm value; operators set the key through gatewayConfig] Signed-off-by: Emilien Macchi <emacchi@redhat.com>
The mutation lock pool was fixed at 4 connections per replica. That number rests on one reconnect-burst bench rather than scale testing, so a gateway that serves many more sandboxes needs a way to raise it without a rebuild. Add --db-lock-max-connections, OPENSHELL_DB_LOCK_MAX_CONNECTIONS and the database_lock_max_connections gateway.toml key next to the data pool knob, resolved in the same order; Helm deployments set it through gatewayConfig. The default stays 4, zero is rejected, SQLite ignores it with a warning, and the gateway logs both pool sizes at startup. PostgreSQL now needs at least 2 data connections, because the SSH identity lock keeps one while it queries the pool. The connection sizing docs, the HA guide formula and the debug skill now count data plus lock connections. Part of NVIDIA#3528 Signed-off-by: Emilien Macchi <emacchi@redhat.com>
A mutation lock acquisition that failed without timing out, such as a
lock connection PostgreSQL never opened or a failed lock statement,
only logged a warning, and readiness stays healthy because it pings the
data pool. Count those failures in
openshell_server_mutation_lock_errors_total{scope}, kept apart from
timeouts because they point at PostgreSQL capacity or connectivity
rather than contention. Export
openshell_server_mutation_lock_connections_in_use as a gauge slot held
by each checked-out lock connection, next to
openshell_server_mutation_lock_connections_capacity, which reads the
configured lock pool size; both exist only with PostgreSQL. The metrics
catalog, the HA guide's signals and PromQL examples, and the debug
skill cover the new series.
Part of NVIDIA#3528
Signed-off-by: Emilien Macchi <emacchi@redhat.com>
Nothing bounds how long a mutation guard is held, so a holder stuck in
a compute driver, credential backend or middleware call keeps its scope
locked while waiters time out. Each guard now records its hold in
openshell_server_mutation_lock_hold_seconds{scope} when it is released,
and logs a warning when the hold outlasted the lock wait timeout, so
operators can find the replica and scope behind those timeouts. A
PostgreSQL test with a stalled holder pins the current contract:
waiters on its sandbox, workspace and global scopes fail by their
deadline, other scopes proceed, the holder keeps its key, and a full
lock pool on one replica leaves the other working. The metrics catalog,
the HA guide and the debug skill describe the new signals, which
observe holds without bounding them.
Part of NVIDIA#3528
Signed-off-by: Emilien Macchi <emacchi@redhat.com>
189a96a to
9941f73
Compare
|
Thanks @pimlock, good catch on the startup lock timeout. Pushed 9941f73:
@FrostGod, another force push, sorry. If the four earlier threads look done to you, could you resolve them? My account can't. Could a maintainer take another look and |
|
/ok to test 9941f73 |
PR Review Status@EmilienM, I checked your startup-timeout update: lock timeouts now retry within the scan's shared deadline, with tests for a holder that releases and one that outlasts the retry window. The independent Critical-only review of the author-only changes found no new Critical defects. No carried Gator code findings remain. @pimlock, I verified that you accepted #4501, the PR's sole closing issue. Its accepted scope explicitly includes both database pool settings, which supplies the requested scope confirmation. Your earlier Helm feedback is reflected in the existing @johntmyers, your request to close out addressed feedback still needs human confirmation. @EmilienM has asked for help resolving the earlier threads because his account cannot do so. Gator cannot resolve human-owned review threads. Action required: a maintainer should confirm and resolve the addressed PostgreSQL CI, benchmark, first-settings-insert follow-up, nested-wait follow-up, and startup retry threads, or identify any remaining change. The stale PR Issue Acceptance failure also needs a rerun of run 38470182027, which failed before #4501 was accepted. Gator submitted an exact-run sandbox policy proposal for that rerun; it is approved but not yet loaded, so no rerun has been queued. Blocking findings: No new Critical findings. The test mirror matches the current head after @pimlock's Gator metadata
|
Monitoring CompleteMonitoring is complete because PR #4147 was merged by @pimlock. The last Gator review found no new Critical defects and no carried Gator code findings remained. Existing review history is preserved. I am removing the active Gator metadata
|
Resolve conflicts with main's scoped mutation locks (#4147) and configurable database pool size: - record_policy_apply_result takes the sandbox mutation guard by id in place of the removed sandbox_sync_guard, and returns NOT_FOUND when the sandbox is gone. The running-workload result test holds the same guard. - accept_supervisor_session takes main's endpoint-status reset failure path: when the session was current, demote the sandbox and retry the invalidation off the setup path. - backfill_legacy_policy_history pages sandboxes by keyset cursor, since list_all_messages was removed. - Keep both the config push and the mutation lock histograms. - Keep Store::max_connections for config delivery sizing; SQLite's test-only max_connections_for_test folds into it. - Main's new endpoint-status tests set the new SupervisorHello fields. Signed-off-by: Piotr Mlocek <pmlocek@nvidia.com>
Summary
Scope gateway mutation locks by workspace and sandbox and bound every lock wait, so a burst of supervisor reconnects (rollout, scale-down, redirects) no longer queues behind one fleet-wide lock.
Related Issue
Closes #4501. Part of #3528. Follows #3978, now merged. Carries #2828 by @bjw123. Follow-ups are tracked in #4307.
Changes
sync_lockand the single fleet-wide advisory key with global, workspace and sandbox lock scopes (shared/exclusive) held on a dedicated PostgreSQL lock pool. Replicas still on the old release keep taking the global key, so a mixed-version rollout stays serialized. Debug builds check the lock ordering rules before every wait.UNAVAILABLEwith reasonMUTATION_LOCK_TIMEOUTand retry info. A lock connection PostgreSQL never opens returnsINTERNAL, so it doesn't read as contention.NOT_FOUNDorABORTEDinstead of writing into the newer sandbox's settings.DeleteProviderholds the mutation guard while it deletes, and staged provider credentials are released when a refresh fails early. On kind, the delete race left a dangling provider reference that crashlooped every gateway at startup.database_max_connections) and a matchingdatabase_lock_max_connections, with flag and env forms. Helm sets both throughgatewayConfig. Defaults stay 10 and 4 (PostgreSQL needs at least 2 data connections), so each pod opens up to 14 PostgreSQL connections instead of 10; raisemax_connectionsbefore upgrading.mise run test:rust:postgresand a Branch Checks job, and document pool sizing. The reconnect-burst capacity check runs separately withmise run test:rust:postgres:bench, outside CI.Testing
mise run pre-commitandmise run docs:build:strictpass,mise run rust:lint, the Helm tests and the openshell-server tests pass, and every commit builds with its tests.mise run test:rust:postgres: 18 tests covering scope exclusion, interop with the old global key, cancellation, pool bounds and gauges, connection failures and a stalled holder.mise run test:rust:postgres:benchpasses on an idle machine.Checklist