mirror of
https://github.com/nestriness/nestri.git
synced 2026-09-19 09:15:19 +03:00
feat(api): hold a run to the attempt that claimed it
The agent side sends a claim token on every write; this side rejected the field outright, so every state report and every ticket publish answered 400. Both bodies now take it. Underneath that, nothing compared a holder. A run was reachable by any caller on the right machine, and a box names exactly one machine — so two attempts polling the same job presented identical credentials and were told apart only by which one's select landed first. That is timing, not a rule, and no caller could be told which case it was in. The row now remembers which attempt holds it. Taking a claim requires there to be no holder; every write after it requires the caller to be the holder. The same state reported by a different attempt is a lost race and not a retry, and is refused whatever the state is - which is the only thing that separates the two 200s from the 409s. The ticket is held to the claim too, for a worse reason than a double start: the client re-reads the address rather than keeping the first, so a ticket written by a losing attempt produces a client that connects, successfully, to a machine running nothing. The holder is never cleared, including on a terminal state, so a settled claim cannot be replayed and a finished run still records which attempt ran it. It is not in what goes out - holding one permits writing to a run, and the owner reading their own session is not the holder.
This commit is contained in:
@@ -56,12 +56,24 @@ export namespace SessionApi {
|
||||
return actor.properties.userID;
|
||||
}
|
||||
|
||||
/**
|
||||
* The value that says which attempt is speaking.
|
||||
*
|
||||
* Described rather than explained. This description is served publicly, so
|
||||
* it says what to send and not what it defends against.
|
||||
*/
|
||||
const ClaimTokenField = Session.ClaimToken.meta({
|
||||
description: 'The token this attempt claimed the run with',
|
||||
example: Examples.Session.claimToken
|
||||
});
|
||||
|
||||
const StateReport = z
|
||||
.object({
|
||||
state: Session.ReportableState.meta({
|
||||
description: 'Where the run has got to',
|
||||
example: 'starting'
|
||||
}),
|
||||
claimToken: ClaimTokenField,
|
||||
errorMessage: z.string().max(1024).nullable().optional().meta({
|
||||
description: 'Why it failed. Kept only for a run that did',
|
||||
example: Examples.Session.errorMessage
|
||||
@@ -281,6 +293,7 @@ export namespace SessionApi {
|
||||
id: c.req.valid('param').id,
|
||||
machineId: Actor.machineID,
|
||||
state: body.state,
|
||||
claimToken: body.claimToken,
|
||||
errorMessage: body.errorMessage ?? null
|
||||
});
|
||||
|
||||
@@ -289,6 +302,12 @@ export namespace SessionApi {
|
||||
notYours();
|
||||
case 'illegal':
|
||||
conflict(`A run in state ${result.session?.state} cannot become ${body.state}`);
|
||||
case 'notHolder':
|
||||
// The same answer whatever the state, including the state the
|
||||
// run is already in. A report from an attempt that does not
|
||||
// hold the run is that attempt losing a race, and telling
|
||||
// that apart from a retry is the entire job of the token.
|
||||
conflict('Another attempt holds this run');
|
||||
case 'lost':
|
||||
conflict('Another caller moved this run first');
|
||||
default:
|
||||
@@ -324,7 +343,8 @@ export namespace SessionApi {
|
||||
ticket: z.string().min(1).meta({
|
||||
description: 'The current connect ticket',
|
||||
example: Examples.Session.ticket
|
||||
})
|
||||
}),
|
||||
claimToken: ClaimTokenField
|
||||
})
|
||||
.strict()
|
||||
),
|
||||
@@ -332,6 +352,7 @@ export namespace SessionApi {
|
||||
const result = await Session.publishTicket({
|
||||
id: c.req.valid('param').id,
|
||||
machineId: Actor.machineID,
|
||||
claimToken: c.req.valid('json').claimToken,
|
||||
ticket: c.req.valid('json').ticket
|
||||
});
|
||||
|
||||
@@ -340,6 +361,8 @@ export namespace SessionApi {
|
||||
notYours();
|
||||
case 'unclaimed':
|
||||
conflict('Claim this run by reporting `starting` before publishing an address');
|
||||
case 'notHolder':
|
||||
conflict('Another attempt holds this run');
|
||||
case 'closed':
|
||||
conflict('That run has stopped, so it has no address to publish');
|
||||
default:
|
||||
|
||||
Reference in New Issue
Block a user