@@ -60,59 +60,41 @@ describe("process administration", () => {
6060 it ( "atomically releases every claim owned by a stale process" , async ( ) => {
6161 runtime = configuredRuntime ( )
6262 await runtime . install ( )
63- const message = await ProcessActor . ref ( "claimed" ) . send . run ( )
64- await runtime . repository . registerProcess ( "stale" , "worker" )
65- const turn = await runtime . repository . claim ( "stale" )
66- expect ( turn ?. message . id ) . toBe ( message . id )
67- await runtime . settings . database . connection ( async ( connection ) => {
68- await connection . run (
69- `INSERT INTO ${ runtime ?. repository . table ( "effects" ) }
70- (id, message_id, instance_id, name, arguments, status, max_attempts, available_at_ms, claimed_by)
71- VALUES ('effect', ?, ?, 'test', '{}', 'processing', 1, 0, 'stale')` ,
72- [ message . id , turn ?. instance . id ] ,
73- )
74- await connection . run (
75- `INSERT INTO ${ runtime ?. repository . table ( "reminders" ) }
76- (id, instance_id, operation, run_at_ms, arguments, missed_policy, status, claimed_by, claimed_at_ms)
77- VALUES ('reminder', ?, 'run', 0, '{}', 'latest', 'scheduled', 'stale', 0)` ,
78- [ turn ?. instance . id ] ,
79- )
80- await connection . run (
81- `INSERT INTO ${ runtime ?. repository . table ( "broadcasts" ) }
82- (id, message_id, instance_id, actor_type, actor_id, state_revision, observables,
83- status, available_at_ms, claimed_by)
84- VALUES ('broadcast', ?, ?, ?, 'claimed', 1, '{}', 'processing', 0, 'stale')` ,
85- [ message . id , turn ?. instance . id , ProcessActor . actorType ] ,
86- )
87- } )
63+ const message = await claimEveryRole ( "stale" )
8864 await staleProcess ( "stale" )
8965
9066 const result = await runtime . processes . cleanup ( )
9167
9268 expect ( result . cleaned ) . toBe ( 1 )
93- expect ( await message . status ( ) ) . toBe ( "ready" )
94- const rows = await runtime . settings . database . connection ( async ( connection ) => ( {
95- process : await connection . get < { shutdown_state : string } > (
96- `SELECT shutdown_state FROM ${ runtime ?. repository . table ( "processes" ) } WHERE id = 'stale'` ,
97- ) ,
98- instance : await connection . get < { activation_owner_id : string | null } > (
99- `SELECT activation_owner_id FROM ${ runtime ?. repository . table ( "instances" ) } ` ,
100- ) ,
101- effect : await connection . get < { status : string ; claimed_by : string | null } > (
102- `SELECT status, claimed_by FROM ${ runtime ?. repository . table ( "effects" ) } ` ,
103- ) ,
104- reminder : await connection . get < { claimed_by : string | null } > (
105- `SELECT claimed_by FROM ${ runtime ?. repository . table ( "reminders" ) } ` ,
106- ) ,
107- broadcast : await connection . get < { status : string ; claimed_by : string | null } > (
108- `SELECT status, claimed_by FROM ${ runtime ?. repository . table ( "broadcasts" ) } ` ,
69+ await expectOwnershipReleased ( "stale" , message )
70+ } )
71+
72+ it ( "atomically releases every claim during graceful shutdown" , async ( ) => {
73+ runtime = configuredRuntime ( )
74+ await runtime . install ( )
75+ const message = await claimEveryRole ( "stopping" )
76+
77+ await runtime . repository . stopProcess ( "stopping" )
78+
79+ await expectOwnershipReleased ( "stopping" , message )
80+ } )
81+
82+ it ( "recovers a stale draining process" , async ( ) => {
83+ runtime = configuredRuntime ( )
84+ await runtime . install ( )
85+ const message = await claimEveryRole ( "draining" )
86+ await runtime . settings . database . connection ( ( connection ) =>
87+ connection . run (
88+ `UPDATE ${ runtime ?. repository . table ( "processes" ) }
89+ SET shutdown_state = 'draining', heartbeat_at_ms = 0 WHERE id = 'draining'` ,
10990 ) ,
110- } ) )
111- expect ( rows . process ?. shutdown_state ) . toBe ( "stopped" )
112- expect ( rows . instance ?. activation_owner_id ) . toBeNull ( )
113- expect ( rows . effect ) . toEqual ( { status : "pending" , claimed_by : null } )
114- expect ( rows . reminder ?. claimed_by ) . toBeNull ( )
115- expect ( rows . broadcast ) . toEqual ( { status : "pending" , claimed_by : null } )
91+ )
92+
93+ expect ( await runtime . processes . all ( ) ) . toEqual ( [
94+ expect . objectContaining ( { id : "draining" , shutdownState : "draining" , stale : true } ) ,
95+ ] )
96+ expect ( await runtime . processes . cleanup ( ) ) . toEqual ( { cleaned : 1 } )
97+ await expectOwnershipReleased ( "draining" , message )
11698 } )
11799
118100 it ( "leaves live process ownership unchanged" , async ( ) => {
@@ -151,3 +133,67 @@ async function staleProcess(id: string): Promise<void> {
151133 ) ,
152134 )
153135}
136+
137+ async function claimEveryRole ( processId : string ) {
138+ const message = await ProcessActor . ref ( `claimed-${ processId } ` ) . send . run ( )
139+ await runtime ?. repository . registerProcess ( processId , "worker" )
140+ const turn = await runtime ?. repository . claim ( processId )
141+ expect ( turn ?. message . id ) . toBe ( message . id )
142+ await runtime ?. settings . database . connection ( async ( connection ) => {
143+ await connection . run (
144+ `INSERT INTO ${ runtime ?. repository . table ( "effects" ) }
145+ (id, message_id, instance_id, name, arguments, status, max_attempts, available_at_ms, claimed_by)
146+ VALUES (?, ?, ?, 'test', '{}', 'processing', 1, 0, ?)` ,
147+ [ `effect-${ processId } ` , message . id , turn ?. instance . id , processId ] ,
148+ )
149+ await connection . run (
150+ `INSERT INTO ${ runtime ?. repository . table ( "reminders" ) }
151+ (id, instance_id, operation, run_at_ms, arguments, missed_policy, status, claimed_by,
152+ claimed_at_ms)
153+ VALUES (?, ?, 'run', 0, '{}', 'latest', 'scheduled', ?, 0)` ,
154+ [ `reminder-${ processId } ` , turn ?. instance . id , processId ] ,
155+ )
156+ await connection . run (
157+ `INSERT INTO ${ runtime ?. repository . table ( "broadcasts" ) }
158+ (id, message_id, instance_id, actor_type, actor_id, state_revision, observables,
159+ status, available_at_ms, claimed_by)
160+ VALUES (?, ?, ?, ?, 'claimed', 1, '{}', 'processing', 0, ?)` ,
161+ [ `broadcast-${ processId } ` , message . id , turn ?. instance . id , ProcessActor . actorType , processId ] ,
162+ )
163+ } )
164+ return message
165+ }
166+
167+ async function expectOwnershipReleased (
168+ processId : string ,
169+ message : Awaited < ReturnType < typeof claimEveryRole > > ,
170+ ) {
171+ expect ( await message . status ( ) ) . toBe ( "ready" )
172+ const rows = await runtime ?. settings . database . connection ( async ( connection ) => ( {
173+ process : await connection . get < { shutdown_state : string } > (
174+ `SELECT shutdown_state FROM ${ runtime ?. repository . table ( "processes" ) } WHERE id = ?` ,
175+ [ processId ] ,
176+ ) ,
177+ instance : await connection . get < { activation_owner_id : string | null } > (
178+ `SELECT activation_owner_id FROM ${ runtime ?. repository . table ( "instances" ) } WHERE actor_id = ?` ,
179+ [ `claimed-${ processId } ` ] ,
180+ ) ,
181+ effect : await connection . get < { status : string ; claimed_by : string | null } > (
182+ `SELECT status, claimed_by FROM ${ runtime ?. repository . table ( "effects" ) } WHERE id = ?` ,
183+ [ `effect-${ processId } ` ] ,
184+ ) ,
185+ reminder : await connection . get < { claimed_by : string | null } > (
186+ `SELECT claimed_by FROM ${ runtime ?. repository . table ( "reminders" ) } WHERE id = ?` ,
187+ [ `reminder-${ processId } ` ] ,
188+ ) ,
189+ broadcast : await connection . get < { status : string ; claimed_by : string | null } > (
190+ `SELECT status, claimed_by FROM ${ runtime ?. repository . table ( "broadcasts" ) } WHERE id = ?` ,
191+ [ `broadcast-${ processId } ` ] ,
192+ ) ,
193+ } ) )
194+ expect ( rows ?. process ?. shutdown_state ) . toBe ( "stopped" )
195+ expect ( rows ?. instance ?. activation_owner_id ) . toBeNull ( )
196+ expect ( rows ?. effect ) . toEqual ( { status : "pending" , claimed_by : null } )
197+ expect ( rows ?. reminder ?. claimed_by ) . toBeNull ( )
198+ expect ( rows ?. broadcast ) . toEqual ( { status : "pending" , claimed_by : null } )
199+ }
0 commit comments