@consiliency/agent-board-schema 1.50.0 → 1.51.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/fixtures/already-applied.json +4 -0
- package/migrations/20261002000100_agent_board_session_challenge_supersede.sql +383 -0
- package/package.json +1 -1
- package/schema-manifest.json +12 -3
- package/src/tooling.test.mjs +10 -2
- package/tests/agent_board_fk_lock_edges.test.sql +20 -0
- package/tests/agent_board_signing_challenge_pending_cap.test.sql +391 -36
- package/tests/isolation/expected/challenge_supersede_lock_order.out +213 -0
- package/tests/isolation/specs/challenge_supersede_lock_order.spec +183 -0
|
@@ -0,0 +1,213 @@
|
|
|
1
|
+
Parsed test spec with 3 sessions
|
|
2
|
+
|
|
3
|
+
starting permutation: mint_begin mint_lock_challenge issue_begin issue_at_cap mint_lock_key issue_commit mint_commit check
|
|
4
|
+
step mint_begin: BEGIN; SET LOCAL "request.jwt.claim.role" = 'service_role';
|
|
5
|
+
step mint_lock_challenge:
|
|
6
|
+
select count(*)::int as locked_challenges from (
|
|
7
|
+
select 1 from agent_board.agent_signing_key_challenges c
|
|
8
|
+
where c.id = 'f1c00000-0000-4000-8000-000000000001' for update) s;
|
|
9
|
+
|
|
10
|
+
locked_challenges
|
|
11
|
+
-----------------
|
|
12
|
+
1
|
|
13
|
+
(1 row)
|
|
14
|
+
|
|
15
|
+
step issue_begin: BEGIN; SET LOCAL "request.jwt.claim.role" = 'service_role';
|
|
16
|
+
step issue_at_cap:
|
|
17
|
+
select c.key_id, c.key_version
|
|
18
|
+
from agent_board.issue_signing_key_challenge(
|
|
19
|
+
'f1a00000-0000-4000-8000-000000000001', 'primary', interval '5 minutes') c;
|
|
20
|
+
|
|
21
|
+
key_id |key_version
|
|
22
|
+
-------+-----------
|
|
23
|
+
primary| 1
|
|
24
|
+
(1 row)
|
|
25
|
+
|
|
26
|
+
step mint_lock_key:
|
|
27
|
+
select count(*)::int as locked_keys from (
|
|
28
|
+
select 1 from agent_board.agent_signing_keys k
|
|
29
|
+
where k.id = 'f1b00000-0000-4000-8000-000000000001' for update) s;
|
|
30
|
+
<waiting ...>
|
|
31
|
+
step issue_commit: COMMIT;
|
|
32
|
+
step mint_lock_key: <... completed>
|
|
33
|
+
locked_keys
|
|
34
|
+
-----------
|
|
35
|
+
1
|
|
36
|
+
(1 row)
|
|
37
|
+
|
|
38
|
+
step mint_commit: COMMIT;
|
|
39
|
+
step check:
|
|
40
|
+
select
|
|
41
|
+
(select proof_state from agent_board.agent_signing_key_challenges
|
|
42
|
+
where id = 'f1c00000-0000-4000-8000-000000000001') as oldest_state,
|
|
43
|
+
(select proof_state from agent_board.agent_signing_key_challenges
|
|
44
|
+
where id = 'f1c00000-0000-4000-8000-000000000002') as second_oldest_state,
|
|
45
|
+
(select count(*)::int from agent_board.agent_signing_key_challenges
|
|
46
|
+
where signing_key_id = 'f1b00000-0000-4000-8000-000000000001'
|
|
47
|
+
and metadata ? 'superseded_by') as evicted_rows,
|
|
48
|
+
(select count(*)::int from agent_board.agent_signing_key_challenges
|
|
49
|
+
where signing_key_id = 'f1b00000-0000-4000-8000-000000000001'
|
|
50
|
+
and proof_state = 'pending' and consumed_at is null
|
|
51
|
+
and expires_at > now()) as pending_rows;
|
|
52
|
+
|
|
53
|
+
oldest_state|second_oldest_state|evicted_rows|pending_rows
|
|
54
|
+
------------+-------------------+------------+------------
|
|
55
|
+
pending |expired | 1| 16
|
|
56
|
+
(1 row)
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
starting permutation: issue_begin issue_at_cap mint_begin mint_lock_challenge issue_commit mint_lock_key mint_commit check
|
|
60
|
+
step issue_begin: BEGIN; SET LOCAL "request.jwt.claim.role" = 'service_role';
|
|
61
|
+
step issue_at_cap:
|
|
62
|
+
select c.key_id, c.key_version
|
|
63
|
+
from agent_board.issue_signing_key_challenge(
|
|
64
|
+
'f1a00000-0000-4000-8000-000000000001', 'primary', interval '5 minutes') c;
|
|
65
|
+
|
|
66
|
+
key_id |key_version
|
|
67
|
+
-------+-----------
|
|
68
|
+
primary| 1
|
|
69
|
+
(1 row)
|
|
70
|
+
|
|
71
|
+
step mint_begin: BEGIN; SET LOCAL "request.jwt.claim.role" = 'service_role';
|
|
72
|
+
step mint_lock_challenge:
|
|
73
|
+
select count(*)::int as locked_challenges from (
|
|
74
|
+
select 1 from agent_board.agent_signing_key_challenges c
|
|
75
|
+
where c.id = 'f1c00000-0000-4000-8000-000000000001' for update) s;
|
|
76
|
+
<waiting ...>
|
|
77
|
+
step issue_commit: COMMIT;
|
|
78
|
+
step mint_lock_challenge: <... completed>
|
|
79
|
+
locked_challenges
|
|
80
|
+
-----------------
|
|
81
|
+
1
|
|
82
|
+
(1 row)
|
|
83
|
+
|
|
84
|
+
step mint_lock_key:
|
|
85
|
+
select count(*)::int as locked_keys from (
|
|
86
|
+
select 1 from agent_board.agent_signing_keys k
|
|
87
|
+
where k.id = 'f1b00000-0000-4000-8000-000000000001' for update) s;
|
|
88
|
+
|
|
89
|
+
locked_keys
|
|
90
|
+
-----------
|
|
91
|
+
1
|
|
92
|
+
(1 row)
|
|
93
|
+
|
|
94
|
+
step mint_commit: COMMIT;
|
|
95
|
+
step check:
|
|
96
|
+
select
|
|
97
|
+
(select proof_state from agent_board.agent_signing_key_challenges
|
|
98
|
+
where id = 'f1c00000-0000-4000-8000-000000000001') as oldest_state,
|
|
99
|
+
(select proof_state from agent_board.agent_signing_key_challenges
|
|
100
|
+
where id = 'f1c00000-0000-4000-8000-000000000002') as second_oldest_state,
|
|
101
|
+
(select count(*)::int from agent_board.agent_signing_key_challenges
|
|
102
|
+
where signing_key_id = 'f1b00000-0000-4000-8000-000000000001'
|
|
103
|
+
and metadata ? 'superseded_by') as evicted_rows,
|
|
104
|
+
(select count(*)::int from agent_board.agent_signing_key_challenges
|
|
105
|
+
where signing_key_id = 'f1b00000-0000-4000-8000-000000000001'
|
|
106
|
+
and proof_state = 'pending' and consumed_at is null
|
|
107
|
+
and expires_at > now()) as pending_rows;
|
|
108
|
+
|
|
109
|
+
oldest_state|second_oldest_state|evicted_rows|pending_rows
|
|
110
|
+
------------+-------------------+------------+------------
|
|
111
|
+
expired |pending | 1| 16
|
|
112
|
+
(1 row)
|
|
113
|
+
|
|
114
|
+
|
|
115
|
+
starting permutation: issue_begin issue_at_cap issue_commit mint_begin mint_lock_challenge mint_lock_key mint_commit check
|
|
116
|
+
step issue_begin: BEGIN; SET LOCAL "request.jwt.claim.role" = 'service_role';
|
|
117
|
+
step issue_at_cap:
|
|
118
|
+
select c.key_id, c.key_version
|
|
119
|
+
from agent_board.issue_signing_key_challenge(
|
|
120
|
+
'f1a00000-0000-4000-8000-000000000001', 'primary', interval '5 minutes') c;
|
|
121
|
+
|
|
122
|
+
key_id |key_version
|
|
123
|
+
-------+-----------
|
|
124
|
+
primary| 1
|
|
125
|
+
(1 row)
|
|
126
|
+
|
|
127
|
+
step issue_commit: COMMIT;
|
|
128
|
+
step mint_begin: BEGIN; SET LOCAL "request.jwt.claim.role" = 'service_role';
|
|
129
|
+
step mint_lock_challenge:
|
|
130
|
+
select count(*)::int as locked_challenges from (
|
|
131
|
+
select 1 from agent_board.agent_signing_key_challenges c
|
|
132
|
+
where c.id = 'f1c00000-0000-4000-8000-000000000001' for update) s;
|
|
133
|
+
|
|
134
|
+
locked_challenges
|
|
135
|
+
-----------------
|
|
136
|
+
1
|
|
137
|
+
(1 row)
|
|
138
|
+
|
|
139
|
+
step mint_lock_key:
|
|
140
|
+
select count(*)::int as locked_keys from (
|
|
141
|
+
select 1 from agent_board.agent_signing_keys k
|
|
142
|
+
where k.id = 'f1b00000-0000-4000-8000-000000000001' for update) s;
|
|
143
|
+
|
|
144
|
+
locked_keys
|
|
145
|
+
-----------
|
|
146
|
+
1
|
|
147
|
+
(1 row)
|
|
148
|
+
|
|
149
|
+
step mint_commit: COMMIT;
|
|
150
|
+
step check:
|
|
151
|
+
select
|
|
152
|
+
(select proof_state from agent_board.agent_signing_key_challenges
|
|
153
|
+
where id = 'f1c00000-0000-4000-8000-000000000001') as oldest_state,
|
|
154
|
+
(select proof_state from agent_board.agent_signing_key_challenges
|
|
155
|
+
where id = 'f1c00000-0000-4000-8000-000000000002') as second_oldest_state,
|
|
156
|
+
(select count(*)::int from agent_board.agent_signing_key_challenges
|
|
157
|
+
where signing_key_id = 'f1b00000-0000-4000-8000-000000000001'
|
|
158
|
+
and metadata ? 'superseded_by') as evicted_rows,
|
|
159
|
+
(select count(*)::int from agent_board.agent_signing_key_challenges
|
|
160
|
+
where signing_key_id = 'f1b00000-0000-4000-8000-000000000001'
|
|
161
|
+
and proof_state = 'pending' and consumed_at is null
|
|
162
|
+
and expires_at > now()) as pending_rows;
|
|
163
|
+
|
|
164
|
+
oldest_state|second_oldest_state|evicted_rows|pending_rows
|
|
165
|
+
------------+-------------------+------------+------------
|
|
166
|
+
expired |pending | 1| 16
|
|
167
|
+
(1 row)
|
|
168
|
+
|
|
169
|
+
|
|
170
|
+
starting permutation: mint_begin mint_lock_all issue_begin issue_at_cap issue_commit mint_commit check
|
|
171
|
+
step mint_begin: BEGIN; SET LOCAL "request.jwt.claim.role" = 'service_role';
|
|
172
|
+
step mint_lock_all:
|
|
173
|
+
select count(*)::int as locked_challenges from (
|
|
174
|
+
select 1 from agent_board.agent_signing_key_challenges c
|
|
175
|
+
where c.signing_key_id = 'f1b00000-0000-4000-8000-000000000001'
|
|
176
|
+
and c.proof_state = 'pending'
|
|
177
|
+
and c.consumed_at is null
|
|
178
|
+
order by c.id
|
|
179
|
+
for update) s;
|
|
180
|
+
|
|
181
|
+
locked_challenges
|
|
182
|
+
-----------------
|
|
183
|
+
16
|
|
184
|
+
(1 row)
|
|
185
|
+
|
|
186
|
+
step issue_begin: BEGIN; SET LOCAL "request.jwt.claim.role" = 'service_role';
|
|
187
|
+
step issue_at_cap:
|
|
188
|
+
select c.key_id, c.key_version
|
|
189
|
+
from agent_board.issue_signing_key_challenge(
|
|
190
|
+
'f1a00000-0000-4000-8000-000000000001', 'primary', interval '5 minutes') c;
|
|
191
|
+
|
|
192
|
+
ERROR: challenge_pending_cap: too many pending signing-key challenges for this key (max 16)
|
|
193
|
+
step issue_commit: COMMIT;
|
|
194
|
+
step mint_commit: COMMIT;
|
|
195
|
+
step check:
|
|
196
|
+
select
|
|
197
|
+
(select proof_state from agent_board.agent_signing_key_challenges
|
|
198
|
+
where id = 'f1c00000-0000-4000-8000-000000000001') as oldest_state,
|
|
199
|
+
(select proof_state from agent_board.agent_signing_key_challenges
|
|
200
|
+
where id = 'f1c00000-0000-4000-8000-000000000002') as second_oldest_state,
|
|
201
|
+
(select count(*)::int from agent_board.agent_signing_key_challenges
|
|
202
|
+
where signing_key_id = 'f1b00000-0000-4000-8000-000000000001'
|
|
203
|
+
and metadata ? 'superseded_by') as evicted_rows,
|
|
204
|
+
(select count(*)::int from agent_board.agent_signing_key_challenges
|
|
205
|
+
where signing_key_id = 'f1b00000-0000-4000-8000-000000000001'
|
|
206
|
+
and proof_state = 'pending' and consumed_at is null
|
|
207
|
+
and expires_at > now()) as pending_rows;
|
|
208
|
+
|
|
209
|
+
oldest_state|second_oldest_state|evicted_rows|pending_rows
|
|
210
|
+
------------+-------------------+------------+------------
|
|
211
|
+
pending |pending | 0| 16
|
|
212
|
+
(1 row)
|
|
213
|
+
|
|
@@ -0,0 +1,183 @@
|
|
|
1
|
+
# F1 (code-review-2026-09-01) — issue_signing_key_challenge must be ACYCLIC against
|
|
2
|
+
# mint_fleet_agent_session once it supersedes at the pending-challenge cap.
|
|
3
|
+
#
|
|
4
|
+
# THE CYCLE THIS CHANGE WOULD HAVE INTRODUCED. Superseding at the cap makes
|
|
5
|
+
# issue_signing_key_challenge WRITE a challenge row while it already holds the key row:
|
|
6
|
+
# agent_signing_keys(FOR UPDATE, 20260719000100 / this migration)
|
|
7
|
+
# -> agent_signing_key_challenges(UPDATE, the eviction)
|
|
8
|
+
# mint_fleet_agent_session takes exactly that pair in the OPPOSITE order:
|
|
9
|
+
# agent_signing_key_challenges(FOR UPDATE, 20260718000600:73-76)
|
|
10
|
+
# -> agent_signing_keys(FOR UPDATE, 20260718000600:95-100)
|
|
11
|
+
# That is an ABBA between two honest callers, and not a rare one: the traffic this change
|
|
12
|
+
# is FOR — a flood driving evictions while legitimate agents mint — is precisely the
|
|
13
|
+
# traffic that produces it. agent_board_fk_lock_edges.test.sql FKL-10 named the pair the
|
|
14
|
+
# first time this migration was written without the fix; that assertion is a static
|
|
15
|
+
# detector and cannot see the fix, which is why the proof lives here.
|
|
16
|
+
#
|
|
17
|
+
# THE FIX this spec pins: the eviction's row selection carries FOR UPDATE SKIP LOCKED, so
|
|
18
|
+
# the issuing transaction NEVER WAITS on a challenge row. A row a mint already holds is
|
|
19
|
+
# passed over for the next-oldest. A transaction that cannot wait cannot close a cycle.
|
|
20
|
+
#
|
|
21
|
+
# WHAT THE GOLDEN SHOWS, AND WHAT WOULD CHANGE IF THE CLAUSE WERE REMOVED. In permutation
|
|
22
|
+
# 1 `mint_lock_challenge` takes the OLDEST pending row; `issue` then runs at the cap and
|
|
23
|
+
# COMPLETES WITHOUT A `<waiting ...>` LINE, evicting the SECOND-oldest instead. Delete
|
|
24
|
+
# `skip locked` and that step blocks — the trace gains `<waiting ...>`, and once
|
|
25
|
+
# `mint_lock_key` runs the two transactions are in a cycle and the server reports
|
|
26
|
+
# `ERROR: deadlock detected`. Either outcome is a byte diff against this golden.
|
|
27
|
+
#
|
|
28
|
+
# THE "mint" SESSION IS A LOCK-EQUIVALENT STAND-IN, and here is the equivalence.
|
|
29
|
+
# * It runs mint_fleet_agent_session's two cycle-relevant acquisitions, in its order and
|
|
30
|
+
# its modes: the challenge row FOR UPDATE (:73-76), then the key row FOR UPDATE
|
|
31
|
+
# (:95-100).
|
|
32
|
+
# * It omits that function's other work — _agent_principal_active's reads, the
|
|
33
|
+
# delegation_grants lookup, the auth_user_id read, the challenge consume UPDATE and the
|
|
34
|
+
# nonfleet_sessions insert. None of those touches agent_signing_keys, and the consume
|
|
35
|
+
# UPDATE re-writes a row this session already holds, so none can add an edge to the
|
|
36
|
+
# pair under test.
|
|
37
|
+
# * A real mint call was rejected only because it needs a valid PoP challenge, an
|
|
38
|
+
# auth_user_id-bearing principal and a live lineage inside a setup block, and because
|
|
39
|
+
# every one of its rejection paths RAISES, which would abort the session mid-permutation
|
|
40
|
+
# and turn a lock-order question into an error-message question. The claim under test is
|
|
41
|
+
# a LOCK ORDER, and these are the locks.
|
|
42
|
+
#
|
|
43
|
+
# THE ISSUE SESSION IS THE REAL FUNCTION, unmodified. That side is where the fix lives, so
|
|
44
|
+
# a stand-in there would be testing the spec instead of the schema.
|
|
45
|
+
|
|
46
|
+
setup
|
|
47
|
+
{
|
|
48
|
+
insert into agent_board.agents(id, agent_type, kind, handle, display_name, status)
|
|
49
|
+
values ('f1a00000-0000-4000-8000-000000000001','agent','agent','f1iso-owner','F1 ISO Owner','active');
|
|
50
|
+
|
|
51
|
+
insert into agent_board.agent_signing_keys(
|
|
52
|
+
id, agent_id, key_id, key_version, public_key, public_key_format, fingerprint, status, proof_state)
|
|
53
|
+
values ('f1b00000-0000-4000-8000-000000000001','f1a00000-0000-4000-8000-000000000001',
|
|
54
|
+
'primary', 1, public.test_pubkey('f1-iso'), 'raw_ed25519_base64url',
|
|
55
|
+
public.test_key_fingerprint('f1-iso'), 'active', 'proven');
|
|
56
|
+
|
|
57
|
+
-- Exactly sixteen pending challenges: the key is AT the cap, so the next issue must
|
|
58
|
+
-- evict. created_at is set explicitly so "oldest" and "second-oldest" are unambiguous
|
|
59
|
+
-- rather than depending on insert clock resolution.
|
|
60
|
+
insert into agent_board.agent_signing_key_challenges(
|
|
61
|
+
id, signing_key_id, agent_id, challenge_nonce, challenge_bytes, expires_at, created_at)
|
|
62
|
+
select
|
|
63
|
+
('f1c00000-0000-4000-8000-0000000000' || lpad(i::text, 2, '0'))::uuid,
|
|
64
|
+
'f1b00000-0000-4000-8000-000000000001',
|
|
65
|
+
'f1a00000-0000-4000-8000-000000000001',
|
|
66
|
+
'f1-iso-nonce-' || i,
|
|
67
|
+
'agent-signing-key-proof.v1:f1-iso:' || i,
|
|
68
|
+
now() + interval '5 minutes',
|
|
69
|
+
now() - make_interval(secs => (17 - i))
|
|
70
|
+
from generate_series(1, 16) i;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
teardown
|
|
74
|
+
{
|
|
75
|
+
delete from agent_board.agent_signing_key_challenges
|
|
76
|
+
where agent_id = 'f1a00000-0000-4000-8000-000000000001';
|
|
77
|
+
delete from agent_board.agent_signing_keys
|
|
78
|
+
where agent_id = 'f1a00000-0000-4000-8000-000000000001';
|
|
79
|
+
delete from agent_board.agents where handle like 'f1iso-%';
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
# mint_fleet_agent_session's two cycle-relevant locks, in its order and modes.
|
|
83
|
+
session "mint"
|
|
84
|
+
step "mint_begin" { BEGIN; SET LOCAL "request.jwt.claim.role" = 'service_role'; }
|
|
85
|
+
# The OLDEST pending row — the one the eviction would otherwise take.
|
|
86
|
+
step "mint_lock_challenge" {
|
|
87
|
+
select count(*)::int as locked_challenges from (
|
|
88
|
+
select 1 from agent_board.agent_signing_key_challenges c
|
|
89
|
+
where c.id = 'f1c00000-0000-4000-8000-000000000001' for update) s;
|
|
90
|
+
}
|
|
91
|
+
# ALL sixteen pending rows at once. This is the adversary for the no-evictable-row branch:
|
|
92
|
+
# SKIP LOCKED can only skip, so with every candidate held the eviction finds nothing.
|
|
93
|
+
step "mint_lock_all" {
|
|
94
|
+
select count(*)::int as locked_challenges from (
|
|
95
|
+
select 1 from agent_board.agent_signing_key_challenges c
|
|
96
|
+
where c.signing_key_id = 'f1b00000-0000-4000-8000-000000000001'
|
|
97
|
+
and c.proof_state = 'pending'
|
|
98
|
+
and c.consumed_at is null
|
|
99
|
+
order by c.id
|
|
100
|
+
for update) s;
|
|
101
|
+
}
|
|
102
|
+
step "mint_lock_key" {
|
|
103
|
+
select count(*)::int as locked_keys from (
|
|
104
|
+
select 1 from agent_board.agent_signing_keys k
|
|
105
|
+
where k.id = 'f1b00000-0000-4000-8000-000000000001' for update) s;
|
|
106
|
+
}
|
|
107
|
+
step "mint_commit" { COMMIT; }
|
|
108
|
+
|
|
109
|
+
# The real issuer, unmodified, at the cap. Only the deterministic columns are selected:
|
|
110
|
+
# challenge_id and the nonce inside `challenge` are fresh random values per run and must
|
|
111
|
+
# never reach a golden.
|
|
112
|
+
session "issue"
|
|
113
|
+
step "issue_begin" { BEGIN; SET LOCAL "request.jwt.claim.role" = 'service_role'; }
|
|
114
|
+
step "issue_at_cap" {
|
|
115
|
+
select c.key_id, c.key_version
|
|
116
|
+
from agent_board.issue_signing_key_challenge(
|
|
117
|
+
'f1a00000-0000-4000-8000-000000000001', 'primary', interval '5 minutes') c;
|
|
118
|
+
}
|
|
119
|
+
step "issue_commit" { COMMIT; }
|
|
120
|
+
|
|
121
|
+
session "check"
|
|
122
|
+
# WHICH row was evicted is the observable that distinguishes skipping from waiting: with
|
|
123
|
+
# SKIP LOCKED the mint-held oldest row is untouched and the SECOND-oldest carries the
|
|
124
|
+
# marker. The pending count proves the bound still holds.
|
|
125
|
+
step "check" {
|
|
126
|
+
select
|
|
127
|
+
(select proof_state from agent_board.agent_signing_key_challenges
|
|
128
|
+
where id = 'f1c00000-0000-4000-8000-000000000001') as oldest_state,
|
|
129
|
+
(select proof_state from agent_board.agent_signing_key_challenges
|
|
130
|
+
where id = 'f1c00000-0000-4000-8000-000000000002') as second_oldest_state,
|
|
131
|
+
(select count(*)::int from agent_board.agent_signing_key_challenges
|
|
132
|
+
where signing_key_id = 'f1b00000-0000-4000-8000-000000000001'
|
|
133
|
+
and metadata ? 'superseded_by') as evicted_rows,
|
|
134
|
+
(select count(*)::int from agent_board.agent_signing_key_challenges
|
|
135
|
+
where signing_key_id = 'f1b00000-0000-4000-8000-000000000001'
|
|
136
|
+
and proof_state = 'pending' and consumed_at is null
|
|
137
|
+
and expires_at > now()) as pending_rows;
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
# 1. THE CYCLE, in the order that would produce the deadlock. mint holds the oldest
|
|
141
|
+
# challenge row; issue then runs at the cap. It must NOT wait (no `<waiting ...>`), and
|
|
142
|
+
# mint's key lock must then be the only thing that blocks.
|
|
143
|
+
permutation "mint_begin" "mint_lock_challenge" "issue_begin" "issue_at_cap" "mint_lock_key" "issue_commit" "mint_commit" "check"
|
|
144
|
+
|
|
145
|
+
# 2. The reverse arrival order: issue arrives FIRST, evicts the true oldest, and mint then
|
|
146
|
+
# blocks on that row and proceeds once issue commits. This is the direction in which the
|
|
147
|
+
# eviction DOES cost a mint a wait, and it pins that the cost is a wait-then-proceed and
|
|
148
|
+
# not a deadlock.
|
|
149
|
+
#
|
|
150
|
+
# `issue_commit` MUST precede `mint_lock_key`. The first version of this permutation put
|
|
151
|
+
# the commit last, which does not test the schema at all: mint blocks on the challenge
|
|
152
|
+
# row, isolationtester will not run a blocked session's next step, and nothing in the
|
|
153
|
+
# permutation can release the lock — so the tester sat out its own 300-second cancel and
|
|
154
|
+
# baked `canceling step mint_lock_challenge after 300 seconds` into the golden. That is a
|
|
155
|
+
# property of the spec's step order, not of the lock order, and it made every iteration
|
|
156
|
+
# cost five minutes.
|
|
157
|
+
permutation "issue_begin" "issue_at_cap" "mint_begin" "mint_lock_challenge" "issue_commit" "mint_lock_key" "mint_commit" "check"
|
|
158
|
+
|
|
159
|
+
# 3. Fully serial: issue commits before mint starts. The baseline the two concurrent
|
|
160
|
+
# permutations are read against.
|
|
161
|
+
permutation "issue_begin" "issue_at_cap" "issue_commit" "mint_begin" "mint_lock_challenge" "mint_lock_key" "mint_commit" "check"
|
|
162
|
+
|
|
163
|
+
# 4. EVERY candidate locked -- the branch that exists only because SKIP LOCKED can skip.
|
|
164
|
+
#
|
|
165
|
+
# With all sixteen pending rows held by another session the eviction's
|
|
166
|
+
# `for update skip locked limit 1` returns NO ROW, so `v_evicted` is null. The function
|
|
167
|
+
# must then REFUSE (challenge_pending_cap) rather than issue: inserting a seventeenth
|
|
168
|
+
# would push the pending set past the bound the cap exists to hold, and the count that
|
|
169
|
+
# said the set was full was correct. This is the ONE path where the pre-change refusal
|
|
170
|
+
# survives, and the PR describes it as "confined to a window in which sixteen mints for
|
|
171
|
+
# one key are in flight at once" -- a claim that was visible in the SQL and asserted
|
|
172
|
+
# nowhere until this permutation.
|
|
173
|
+
#
|
|
174
|
+
# Three things the golden shows, and all three matter:
|
|
175
|
+
# * issue_at_cap ERRORS. It does not wait -- SKIP LOCKED means no `<waiting ...>`.
|
|
176
|
+
# * `evicted_rows = 0`. Nothing was superseded, so no legitimate challenge was
|
|
177
|
+
# destroyed on the way to refusing.
|
|
178
|
+
# * `pending_rows = 16`. No seventeenth row was inserted.
|
|
179
|
+
#
|
|
180
|
+
# MUTATION: delete the `if v_evicted is null then raise` guard and this permutation's
|
|
181
|
+
# golden changes -- the refusal disappears and pending_rows becomes 17, which is the
|
|
182
|
+
# bound being exceeded, i.e. exactly the failure the guard is there to prevent.
|
|
183
|
+
permutation "mint_begin" "mint_lock_all" "issue_begin" "issue_at_cap" "issue_commit" "mint_commit" "check"
|