Repository navigation
test: a sandboxed test takes the write lock before it reads - #115
Merged
Merged
Conversation
SCR-376, SCR-377 and SCR-378, the three findings left over from PR #113. The mechanism, already filed: Ecto.Adapters.SQL.Sandbox begins every test's transaction with an explicit mode: :transaction, which Exqlite.Connection resolves ahead of the configured default_transaction_mode: :immediate, and which is a plain DEFERRED BEGIN. So slice 005's fix never reached the suite. A test that reads and then writes is upgrading, and SQLite answers SQLITE_BUSY on an upgrade without consulting the busy handler, whatever busy_timeout says. Five failures across three tables came from that: gateway_identities twice, personas once, each time while the other run of the same commit passed. What was missing was not the mechanism but the premise for a fix. Measured now, as a third case in slice 005's own two-connection harness: a write that is the transaction's FIRST statement is not an upgrade, so the handler is consulted and the writer waits instead of failing. The case asserts both the success and, by elapsed time, that it genuinely contended rather than sailing through. So DataCase.setup_sandbox/1 issues one zero-row DELETE per repo before the test body runs. The table is the repo's own migration table read from its configuration, not assumed: Trinity.Repo.Receipts sets migration_source: "receipts_schema_migrations", and hard-coding schema_migrations failed every test using the template rather than only the receipts ones. SQLite only; Postgres has no SQLITE_BUSY and no upgrade path to guard. All five observed instances are covered. identities_test uses DataCase directly; approvals_test and subagents_test use SessionCase, which does `use Trinity.DataCase` internally, so they route through the same setup. I checked that rather than assuming it, having first read a grep that suggested the opposite. Cost: no change I can distinguish from this machine's run-to-run variance. 148.8 s before, 153.8 s after, on a suite that has reported times further apart than that between identical runs. The reason it is cheap is structural rather than lucky: every file using the template is async: false, so there is no concurrency to serialise. The only async: true occurrence of the template is its own documentation. SCR-378, separately. Trinity.SandboxTest's os.time and os.clock case inherited the default 1 s wall clock and failed once on a loaded runner, for a reason it was not about: it asserts the two functions are available. It now names a generous limit. That is not a weakening, because the limit's own binding is proven on its own by "an infinite loop is killed on wall clock and the caller is told which limit bound", which passes max_time_ms: 200 and asserts the refusal by value. A budget asserted in two places is a budget that gets loosened in the wrong one. mix gate: 952 passed, exit 0. Signed-off-by: Ayla Croft <aylacroft@proton.me>
Signed-off-by: Ayla Croft <aylacroft@proton.me>
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.
The three findings left over from #113: SCR-376, SCR-377, SCR-378.
The mechanism was already filed; the premise for a fix was not
Ecto.Adapters.SQL.Sandboxbegins every test's transaction with an explicitmode: :transaction, whichExqlite.Connectionresolves ahead of the configureddefault_transaction_mode: :immediate, and which is a plain DEFERREDBEGIN. Slice 005's fix never reached the suite. A test that reads then writes is upgrading, and SQLite answersSQLITE_BUSYon an upgrade without consulting the busy handler, whateverbusy_timeoutsays.Five failures across three tables came from that —
gateway_identitiestwice,personasonce — each time while the other run of the same commit passed.What was missing was the premise. Measured now, as a third case in slice 005's own two-connection harness:
The case asserts both the success and, by elapsed time, that it genuinely contended rather than sailing through. Without that second assertion it would pass on a race it never ran.
The fix
DataCase.setup_sandbox/1issues one zero-rowDELETEper repo before the test body runs. Two details that are not incidental:Trinity.Repo.Receiptssetsmigration_source: "receipts_schema_migrations", and hard-codingschema_migrationsfailed every test using the template rather than only the receipts ones. I found that by breaking it.SQLITE_BUSYand no upgrade path to guard, so taking a lock there would be cost with no reason.All five instances are covered, checked rather than assumed
identities_testusesDataCasedirectly.approvals_testandsubagents_testuseSessionCase— and a grep forsetup_sandboxdoes not listsession_case.ex, which initially suggested they were uncovered.SessionCasedoesuse Trinity.DataCaseinternally, so they route through the same setup. Worth stating because the grep was misleading.Cost
No change I can distinguish from this machine's run-to-run variance: 148.8 s before, 153.8 s after, on a suite that has reported times further apart than that between identical runs. I am not claiming 5 s; I am claiming it is small.
The reason it is cheap is structural rather than lucky: every file using the template is
async: false, so there is no concurrency to serialise. The onlyasync: trueoccurrence of the template is its own documentation.SCR-378, separately
Trinity.SandboxTest'sos.time/os.clockcase inherited the default 1 s wall clock and failed once on a loaded runner, for a reason it was not about: it asserts the two functions are available. It now names a generous limit.That is not a weakening. The limit's own binding is proven on its own by "an infinite loop is killed on wall clock and the caller is told which limit bound", which passes
max_time_ms: 200and asserts the refusal by value. A budget asserted in two places is a budget that gets loosened in the wrong one.Evidence
952 against 951 on
main: the one new harness case. Branched frommain, so it is independent of #114 and touches none of the same files.No
lib/change. No new slice id. Not merged.