test(datastore): pin retirement against concurrent shred and rotation
Implements plan step 6 of
docs/plans/2026-08-03-s04b-rotation-tooling.md:
forced-interleaving coverage proving RetireKeyVersion serializes
against concurrent ShredNamespace and RotateKey through the store's
FOR UPDATE barrier.
Four tests extend the existing lock-wait harness
(pg_blocking_pids-gated, no sleeps) in
internal/datastore/namespace_encryption_keys_concurrency_integration_test.go,
covering both interleavings in both commit orders:
- shred commits first → retirement returns
ErrNamespaceShredded; tombstones survive. - retirement holds the barrier → the shred provably blocks, then tombstones the survivor.
- rotation-insert commits first → the resumed retirement re-resolves the active version fresh and still deletes its target (the version that was active at dispatch — the one target choice that makes the fresh post-barrier resolve falsifiable).
- retirement holds the barrier → the resumed insert lands the next version from MAX(version) over the surviving rows.
Test-only: no production code changes; the store under test merged with
!1343 (merged). Deterministic across repeated -race runs.
E2E scenario catalogs
No scenario added or affected: store-level concurrency coverage with no user-facing surface.
Database review
No new migration or query-producing method; the tests drive Step 1's store operations through the existing harness.
Spec coverage
Spec: docs/specs/S04-b-encryption-key-rotation-tooling.md
Acceptance criteria
| # | Criterion | Tests |
|---|---|---|
| AC-10 | RetireKeyVersion serializes against concurrent ShredNamespace and RotateKey through the shared FOR UPDATE barrier, proven by forced-interleaving integration tests; shred-commits-first yields ErrNamespaceShredded; retirement holding the barrier blocks a concurrent shred until commit |
TestNamespaceEncryptionKeyStore_RetireVsShred_RetireBlockedResumesShredded, TestNamespaceEncryptionKeyStore_RetireVsShred_ShredBlocksUntilRetireCommits, TestNamespaceEncryptionKeyStore_RetireVsRotate_RetireResumesOnFreshActive, TestNamespaceEncryptionKeyStore_RetireVsRotate_RotateBlocksThenLandsNext |
Error cases
| # | Condition | Tests |
|---|---|---|
| E-21 | Retire: namespace carries any tombstone → ErrNamespaceShredded; tombstones are never deleted |
Concurrency dimension: TestNamespaceEncryptionKeyStore_RetireVsShred_RetireBlockedResumesShredded (barrier-blocked retirement resumes onto committed tombstones). Committed-state input classes: Step 1's suite (namespace_encryption_keys_rotation_integration_test.go) |
| E-22 | Retire: version at or above the active key → ErrKeyVersionActive |
Boundary under concurrency: TestNamespaceEncryptionKeyStore_RetireVsRotate_RetireResumesOnFreshActive (the guard resolves post-barrier, so a concurrently-deactivated target is admitted). Refusal itself: Step 1's suite |
Security considerations
| # | Concern | Tests |
|---|---|---|
| — | No security-consideration row names retirement concurrency; the AC-10 barrier serialization above is the relevant control | Covered under AC-10 |
Related to https://gitlab.com/gitlab-org/ops/artifact-registry/-/issues/496