Fix xdist database-is-locked flake in the mypy regression tests - #74
Merged
Conversation
Two of the three in-process mypy runs in tests/integration/test_regression.py fell back to mypy's default .mypy_cache/, so under pytest-xdist they could open the same sqlite incremental-cache database from separate worker processes. mypy 2.x defaults sqlite_cache to True and the main process opens each cache shard with PRAGMA journal_mode=WAL, which needs an exclusive lock and raises immediately rather than waiting on the busy timeout, so whichever test lost the race died with sqlite3.OperationalError: database is locked. Route all three tests through a _run_mypy helper that injects a --cache-dir derived from the test method name. That keeps the stable-path cache reuse #65 introduced for test_should_catch_errors_with_plugin while making it structurally impossible for a newly added test in this file to fall back to the shared default dir again. Claude-Session: https://claude.ai/code/session_013nRj6HvRbwkzAoj3QPbiSj
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Fixes the intermittent
sqlite3.OperationalError: database is lockedintests/integration/test_regression.pythat has shown up on at least two recent PRs.Root cause (confirmed, not assumed)
Reproduced locally and captured the trace. Every failure was identical:
The chain:
mypy/options.py:305setsself.sqlite_cache = True, anddefaults.SQLITE_NUM_SHARDS = 16, so a cache dir holdscache.0.db…cache.15.dbrather than the old JSON metastore.build.py:create_metastorepassesset_journal_mode=not parallel_worker, andconnect_dbthen runsPRAGMA journal_mode=WAL. Changing journal mode needs an exclusive lock and fails immediately instead of waiting on the sqlite busy timeout.test_should_not_catch_errors_without_pluginandtest_should_accept_polarsframe_with_type_argumentpassed no--cache-dir, so both landed on.mypy_cache/.test_should_catch_errors_with_pluginalready had its own dir from Fix slow mypy plugin regression test #65.-n autois inaddopts, so those two tests can run in different xdist worker processes at the same time. mypy also spawns its own worker processes per run, which hold locks on those same shard files. Whichever test's main process reaches the WAL pragma second raises.Two corrections to the original hypothesis, both verified:
.mypy_cache/plugin_regression_test. That dir is a distinct sqlite file set and never appeared in a failure; the third test never failed once across the whole baseline.uv run inv lint.inv lintruns ruff/ty/bandit/complexipy/cargo — no mypy at all, and the pre-commit config has no mypy hook either.grep -rn "mypy_run\|from mypy.api" tests/confirms this one file holds the only in-process mypy invocations in the suite, so the race is purely test-vs-test.The failure is symmetric — either of the two sharing tests can lose the race. The reported flake was on
test_should_accept_polarsframe_with_type_argument; in the baseline below it failed 4 times and the no-plugin test failed 3.Fix
All three tests now go through a
_run_mypyhelper that injects--cache-dir .mypy_cache/<test method name>.Why a derived path rather than more hand-written dedicated paths: adding two more fixed
--cache-dirstrings would fix today's flake but leaves the same trap for the next test added to this file — the failure mode is silent until it bites in CI. Deriving the path inside the single helper the tests call makes the correct behaviour the default and unittest's method-name uniqueness guarantees no two tests collide.Why not
tempfile.mkdtemp()per run: that defeats mypy's incremental cache and reintroduces exactly the problem #65 fixed — a full cold typeshed/stdlib check on every run. The derived path is stable across runs, so cache reuse is preserved while cross-test isolation is total. This is #65's reasoning generalised, not reversed.Side effect:
test_should_catch_errors_with_pluginmoves from.mypy_cache/plugin_regression_testto.mypy_cache/test_should_catch_errors_with_plugin, costing one cold run. The explanatory comment #65 added moves into the helper docstring, expanded with the sqlite/WAL detail.Stress-test evidence
Cold cache was produced by moving the repo-local
.mypy_cache/aside into a scratch dir between iterations — no deletes anywhere. Machine: macOS, 18 cores, so-n autogives 18 workers.Before the fix — 9 failures / 40 runs, all
database is lockedatmetastore.py:174:test_regression.pyonly,-n autotests/,-n autoAfter the fix — 0 failures / 65 runs:
test_regression.pyonly,-n autotest_regression.pyonly,-n autotests/,-n autoChecks
uv run inv allgreen: 301 passed, coverageTOTAL 969 stmts / 316 branches / 100%,coverage-thresholdSuccess, Rust suite ok (172 + 13 tests). No new lint-ignore rules and no bandit skips.https://claude.ai/code/session_013nRj6HvRbwkzAoj3QPbiSj