Repository navigation
bug: managed-workspace E2E tests intermittently fail with SQLite lock contention #3679
Description
User Story
As an OpenShell contributor, I want the managed-workspace Kubernetes E2E suite to produce reliable results under its normal concurrency, so that unrelated PRs are not blocked by intermittent database-lock failures.
Problem Statement
Two tests in e2e/rust/tests/workspace_namespace_managed.rs failed while creating workspaces in PR #3651's CI run:
managed_full_lifecycle_with_multiple_sandboxes— assertion at line 664.managed_namespace_survives_with_remaining_sandboxes— assertion at line 279.
Both returned persist workspace failed: database error: error returned from database: (code: 5) database is locked after approximately 5.1 seconds. The suite reported 8 passed and 2 failed. Rerunning the failed job at the same PR head, without code changes, passed all 10 tests.
The observed symptom is SQLite lock contention during concurrent workspace creation. The lock holder and underlying cause have not been established; the successful rerun does not establish that the underlying problem is fixed.
Impact / Why This Matters
Priority: Medium (requested by the maintainer).
This failure blocks the required Core E2E gate on otherwise passing PRs and consumes contributor time investigating unrelated failures. Rerunning the job is the current workaround, but adds CI cost and delay and can obscure a real gateway concurrency defect. No production impact was established by this investigation.
Acceptance Criteria
- Identify the source of lock contention and determine whether the fix belongs in gateway persistence, test isolation, or the test runner.
- Add regression coverage or a repeatable diagnostic reproducer that exercises the failing concurrent workspace-creation behavior.
- Both named tests and the full managed-workspace suite pass repeated runs under the normal CI configuration; document the repetition count and results.
- Preserve the lifecycle and namespace-survival assertions; skipping the tests or adding blanket retries alone does not resolve the issue.
Reproduction Steps
- Use PR ci(qualification): add Ubuntu 24.04 k3s scenario #3651 head
c8e53de90ffb25e975872b022303f917394653a9and the existingtest:e2eCI configuration. - Run the
kubernetes-workspace-managed-e2e / Kubernetes E2E (workspace managed mode)job, which invokesmise run e2e:kubernetes:workspace-managedagainst its kind cluster using thee2e-kubernetesnextest profile. - Observe concurrent workspace creation in the two named tests. In the recorded failing attempt, both calls failed after approximately 5.1 seconds with SQLite code 5.
- Compare the unchanged-code rerun, which passed all 10 tests. Reproduction is intermittent; no deterministic local reproducer has yet been established.
Environment
- OpenShell PR head:
c8e53de90ffb25e975872b022303f917394653a9(ci(qualification): add Ubuntu 24.04 k3s scenario #3651). - Date: September 24, 2026.
- CI: Linux amd64 runner; Kubernetes kind cluster; managed workspace mode; SQLite gateway persistence.
- Workflow run:
36063204811, attempts 1 and 2. - Runner version in the failing attempt:
2.337.0. - Nextest profile:
e2e-kubernetes; 10 tests inworkspace_namespace_managed.
Logs
FAIL [5.106s] managed_full_lifecycle_with_multiple_sandboxes
FAIL [5.107s] managed_namespace_survives_with_remaining_sandboxes
workspace create failed: Error: code: 'Internal error', message:
"persist workspace failed: database error: error returned from database:
(code: 5) database is locked"
Summary [25.820s] 10 tests run: 8 passed, 2 failed, 0 skipped
Unchanged-code rerun:
Summary [34.614s] 10 tests run: 10 passed, 0 skipped
Evidence and Related Work
- Failing job, attempt 1.
- Passing job, attempt 2.
- Lifecycle test assertion.
- Namespace-survival test assertion.
- Related: Control-plane read failures during sandbox SSH-session cleanup; delete acknowledgment can precede durable cleanup #2999 reports other control-plane database-lock symptoms during SSH-session cleanup. A shared root cause has not been established.
- PR ci(qualification): add Ubuntu 24.04 k3s scenario #3651 changes the separate tmachine qualification path. Its new k3s conformance scenario passed; the failing managed-workspace test code and runner were unchanged by that PR.
Activity
kvnloo commentedon Sep 27, 2026
I can investigate this.
I would avoid adding retries first, because that could hide whether this is a gateway persistence bug or test-level database sharing.
My first pass would be:
nextestconcurrency;That should distinguish:
Stop condition for the investigation: identify the lock holder/path well enough that we can reproduce it deterministically.
I would not treat a blanket retry or a longer sleep as the fix.
fitzyracing1 commentedon Oct 4, 2026
@kvnloo, are you still working on this? If not, I'd like to pick it up. The ~5.1s failures match sqlx's default 5s
busy_timeout(the SQLite connect options inpersistence/sqlite.rsdon't set one), so theMustCreateinsert in the workspace create handler looks like it's waiting on another connection that holds the write lock for more than 5 seconds. My first step would be a persistence-level stress test that runs concurrent workspace creates alongside theBEGIN IMMEDIATEpaths and the relaxed pool, and records which one holds the lock, before touching any timeouts.kvnloo commentedon Oct 4, 2026
Thanks for checking, and for digging into this! Please go ahead! don't wait on me. I outlined an investigation here, but I don't have a patch or reproducer to hand over.
Your persistence-level stress test sounds like a useful next step, particularly identifying which path holds the write lock before changing timeouts. Thanks for picking this up, and sorry I didn't follow up sooner.
fitzyracing1 commentedon Oct 7, 2026
Thanks @kvnloo, I'll take it from here. I'll start with the persistence-level stress test to see which path holds the write lock, and I'll post what I find here before changing any timeouts.
fitzyracing1 commentedon Oct 7, 2026
Update from the persistence-level stress test. Short version: I don't think any single path holds the write lock for more than 5s. The failing waiter gets starved by a steady stream of short writes, and the failing build was still on the rollback journal.
What the code shows
busy_timeoutis never set inpersistence/sqlite.rs, so it's sqlx's implicit 5s (sqlx-sqlite 0.9.0,options/mod.rs:203). That lines up with the ~5.1s failures.create_if_workspace_count_belowandput_policy_revision_atomic(bothBEGIN IMMEDIATE, DB statements only). Everything else, including the workspaceMustCreateinsert, is a single autocommit statement.c8e53de, which doesn't contain perf(server): enable WAL for the SQLite store; relax sync only for SSH session issuance #3543. That gateway used the rollback journal (sqlx doesn't setjournal_mode),synchronous=FULL, and the 5-connection pool, with no relaxed pool. All tests in the binary share one SQLite file on the gateway PVC.CI history (Branch E2E, runs created Sep 24–Oct 7, every attempt of the workspace-managed job): 494 job instances, 4 failed with
database is locked. All 4 were SHAs without #3543 (2194310, e60098d, c8e53de, f155eeb), so 4 of the 21 pre-WAL instances. Of the 473 instances with WAL, none failed withdatabase is locked.Stress test. 8 concurrent simulated tests (workspace create, then template
BEGIN IMMEDIATE, sandbox create,put_policy_revision_atomic, status CAS updates, relaxed SSH-session create), plus readers and a batched cleanup delete. Each write is instrumented with a busy handler that copies SQLite's default backoff, so it records who waited and for how long. A slower CI disk is modeled by adding latency to each fsync through an LD_PRELOAD shim. That latency is injected, not measured from CI.Real
Store, failed workspace creates (max workspace-create latency in parentheses):The errors are the CI message exactly:
database error: error returned from database: (code: 5) database is locked.Lock attribution for one failed create (rollback journal, +20 ms/fsync): it waited 5005.7 ms over 60 busy-handler retries. In that window 61 other writes committed: 31 status CAS updates, 6 policy-atomic transactions, 6 template creates, 6 workspace creates, 6 sandbox creates, 5 SSH-session creates, 1 cleanup delete. Their holds sum to about 5.0 s, and the longest single hold was 83.9 ms. Each rollback-journal commit holds the lock for about 4 fsyncs (about 82 ms at +20 ms/fsync), and a run does about 11x as many fsyncs as on WAL. SQLite's busy handler is a polling sleep that doesn't preserve arrival order, so the waiter keeps losing.
A/B (standalone harness, same SQL, rollback journal, +20 ms/fsync, 5 runs × 8 tests):
database is locked(the contention just moves)On WAL, the same harness fails at +100 ms/fsync (8/24) and passes with the in-process write queue (0/24, max 1.2 s).
Takeaways
busy_timeoutonly hides the starvation. Isolating DBs per test would also hide a real gateway behavior.tokio::sync::MutexinSqliteStore. Then in-process writers wait in order instead of polling, and the busy timeout only covers contention from outside the process. With that on main: 0/24 at +100 ms/fsync (max 1.4 s), 0/40 at +50 ms (max 0.72 s), and the openshell-server lib tests pass. I'd also add the stress test as an#[ignore]d reproducer.I haven't run the kind-based E2E suite locally, so I don't have the repeated-run numbers the acceptance criteria ask for yet. I'd like a maintainer's view on the write-queue approach before opening a PR.
EmilienM commentedon Oct 13, 2026
Another data point: the new
sqlite_concurrent_writes_return_their_own_resource_versiontest from #4408 fails with the same(code: 5) database is lockedon both Windows MSVC lanes (x64 and arm64) in #4147's runs, about 8 s in. #4147 doesn't change that SQLite path, and these look like the firsttest:windowsruns since #4408 merged. Windows runners have slow fsync, so this looks like the starvation you measured at high fsync latency, and that test could double as a reproducer for the write-queue change.