Skip to content

Retires only per-host chart hashes in the conformance suite - #244

Merged
johnnyt merged 1 commit into
mainfrom
sp-gbyk-conformance-own-chart-hash
Sep 30, 2026
Merged

johnnyt merged 1 commit into
mainfrom
sp-gbyk-conformance-own-chart-hash

Conversation

@johnnyt

@johnnyt johnnyt commented Sep 30, 2026

Copy link
Copy Markdown
Member

What

The shipped conformance suite (StatifierPersistence.Testing.StorageConformance, in lib/) retired chart hashes that were the same for every module that used it. On Postgres the per-hash retirement lock is keyed on (namespace, hashtext(content_hash)) with no table, prefix or schema in it, so two async modules running the suite against one database met on one key. The tombstone-check case and the create-retired-after-first-check case each read a tombstone (shared lock) and then retire the same hash (exclusive lock) inside the test-long sandbox transaction; two copies of either case upgrade at once and Postgres answers 40P01. That is the flake the tombstone-check case showed on CI.

Classification: test sharing, not an adapter lock-order defect. retire_chart/3 takes the exclusive lock as the first statement of its transaction and holds nothing before it; fetch_retired_info/2 is the only shared-lock site. The deadlock needs a caller that composes a tombstone read and a retirement of one hash in one transaction of its own.

Change

  • @conformance_hash_suffix (a module attribute inside the using block): the first 16 hex characters of the SHA-256 of the using module's name. Private; no new public function, option or callback.
  • Cases changed, each now on a hash of the using module's own:
    • "facade: the tombstone check refuses a retired chart and lets a live one through": a copy of chart "a" with a comment naming the suffix (own_chart_a/0, private). Deadlocked before.
    • "facade: a create whose hash is retired after the first check refuses and writes nothing" and "facade: a migration whose to hash is retired after the first check refuses and writes nothing": the loan and renewed sources carry the same comment. The first deadlocked; the second has the same shape.
    • The adapter retirement block's @retire_hash, and the never-stored miss case's hash: these save a chart row and then retire it, and a row insert racing a shared read and a retirement on one key can also deadlock, so they no longer share a key across modules.
    • "facade: a retirement either runs or is declined at open" and "adapter: the tombstone read answers what the retired arm of fetch_chart/2 carries": each takes the exclusive lock on its hash.
  • Cases left: "adapter: the tombstone read answers nil for a live chart and for a hash never stored" (shared lock only, nothing retires those hashes), and every case that uses Charts.chart_a/0 or chart_b/0 without retiring (shared locks only; now that no case retires chart "a", no exclusive lock is taken on its hash).
  • Record: a dated foot Note on docs/adr/0012-retention-and-retirement.md, after its last Note. It states the upgrade hazard for a caller that reads, creates or migrates onto a hash and then retires it in one transaction, that no package path does this on its own, and that the lock key is database-wide. A Note decides nothing: no Status line, zero removed lines under docs/adr/.
  • changelog.d/sp-gbyk.md under ### Fixed: the suite ships in lib/ and a host running it asynchronously could hit the same flake.

Evidence

Seeded mix test loops against local Postgres 17:

  • Before the change, the three Ecto conformance modules together (ecto_conformance_test.exs, ecto_blob_type_conformance_test.exs, ecto_scoped_conformance_test.exs), seeds 1..15: 7 seeds failed, 11 failures, each a 40P01 in the tombstone-check case or the create case.
  • After the change, the same three modules together, seeds 1..150: 0 failed seeds, 0 failures, 244 tests per run, 213 s wall.
  • After the change, the full suite, seeds 1..50: 0 failed seeds, 0 failures, 1,347 tests per run, 380 s wall.
  • Sabotage of the fix: @conformance_hash_suffix redefined to one constant string for every module, the three modules together, seeds 1..30: 17 seeds failed, 25 failures, every one a 40P01 in the tombstone-check case or the create case. Restored from a copy (byte-equal), then touched and force-recompiled.

No new test: the change is to the suite's own fixtures, and the evidence is the repeated runs above and the sabotage row, which fails when the suffix stops differing per module.

Gate

Full local mix quality: green (format, compile with warnings as errors, credo, docs, doc links, dependencies, tests 1,347 of 1,347 at 95.8% coverage, dialyzer). The commit's tree is byte-identical to the tree that gate ran on.

Provenance

  • The fix's shape (per-module hashes rather than synchronous retirement cases, and a Note rather than an Amendment) follows the test-sharing classification, ruled by the operator, 2026-09-29.
  • Mechanism: a hash suffix derived from the module name rather than a per-run token, so the hashes stay deterministic from run to run; two modules of one BEAM never share one. An engineering choice inside the suite's private fixtures.

Refs: sp-gbyk

The conformance suite's retiring cases used literal chart hashes, so
two async modules running it against one Postgres database met on the
same database-wide advisory lock key. A case that reads a tombstone
and then retires its hash upgrades its shared lock inside the sandbox
transaction, and two copies of that case deadlocked (40P01).

Every hash a generated case retires now carries a suffix derived from
the using module's name: the adapter retirement block, the capability
and tombstone-read cases, the facade create and migration sources, and
the tombstone-check case's own copy of chart "a".

Adds a dated Note to the retention record stating the upgrade hazard
for a caller that reads and retires one hash in one transaction, and
that the lock key is database-wide, plus a Fixed changelog fragment.

Refs: sp-gbyk
@johnnyt
johnnyt merged commit de26253 into main Sep 30, 2026
1 check passed
@johnnyt
johnnyt deleted the sp-gbyk-conformance-own-chart-hash branch September 30, 2026 01:14
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant