Model API mutation replay semantics
This commit is contained in:
parent
a44b687897
commit
e5ab04d49c
4 changed files with 201 additions and 1 deletions
29
docs/protocol/RemoraApiMutation.cfg
Normal file
29
docs/protocol/RemoraApiMutation.cfg
Normal file
|
|
@ -0,0 +1,29 @@
|
||||||
|
SPECIFICATION Spec
|
||||||
|
|
||||||
|
CHECK_DEADLOCK TRUE
|
||||||
|
|
||||||
|
CONSTANTS
|
||||||
|
OperationScopes = {scope1}
|
||||||
|
EligibilityClaims = {eligibility1, eligibility2}
|
||||||
|
OperationIds = {op1, op2}
|
||||||
|
MutationKinds = {submit, renew, progress, success, failure}
|
||||||
|
Fingerprints = {fp1, fp2}
|
||||||
|
NoEligibility = NoEligibility
|
||||||
|
NoRecord = NoRecord
|
||||||
|
NoObservation = NoObservation
|
||||||
|
Accepted = Accepted
|
||||||
|
PreconditionFailed = PreconditionFailed
|
||||||
|
FirstObservation = FirstObservation
|
||||||
|
ReplayObservation = ReplayObservation
|
||||||
|
ConflictObservation = ConflictObservation
|
||||||
|
|
||||||
|
INVARIANTS
|
||||||
|
TypeOK
|
||||||
|
RecordEffectConsistency
|
||||||
|
RevisionCountsEffects
|
||||||
|
ObservationConsistent
|
||||||
|
|
||||||
|
PROPERTIES
|
||||||
|
RecordsAppendOnly
|
||||||
|
EffectsApplyAtMostOnce
|
||||||
|
RevisionMonotonic
|
||||||
163
docs/protocol/RemoraApiMutation.tla
Normal file
163
docs/protocol/RemoraApiMutation.tla
Normal file
|
|
@ -0,0 +1,163 @@
|
||||||
|
-------------------------- MODULE RemoraApiMutation --------------------------
|
||||||
|
EXTENDS Naturals, FiniteSets
|
||||||
|
|
||||||
|
\* This model covers the durable API operation ledger around an abstract
|
||||||
|
\* JobProtocol effect; JobProtocol remains the sole phase-transition model.
|
||||||
|
|
||||||
|
CONSTANTS
|
||||||
|
OperationScopes, EligibilityClaims, OperationIds, MutationKinds,
|
||||||
|
Fingerprints, NoEligibility, NoRecord, NoObservation,
|
||||||
|
Accepted, PreconditionFailed,
|
||||||
|
FirstObservation, ReplayObservation, ConflictObservation
|
||||||
|
|
||||||
|
ASSUME
|
||||||
|
/\ OperationScopes # {}
|
||||||
|
/\ EligibilityClaims # {}
|
||||||
|
/\ OperationIds # {}
|
||||||
|
/\ MutationKinds # {}
|
||||||
|
/\ Fingerprints # {}
|
||||||
|
|
||||||
|
Identities == OperationScopes \X OperationIds
|
||||||
|
|
||||||
|
Requests ==
|
||||||
|
[scope : OperationScopes,
|
||||||
|
operation : OperationIds,
|
||||||
|
eligibility : EligibilityClaims,
|
||||||
|
kind : MutationKinds,
|
||||||
|
fingerprint : Fingerprints]
|
||||||
|
|
||||||
|
Responses ==
|
||||||
|
[status : {Accepted, PreconditionFailed},
|
||||||
|
revision : 0..Cardinality(Identities)]
|
||||||
|
|
||||||
|
Records == [request : Requests, response : Responses]
|
||||||
|
|
||||||
|
Observations ==
|
||||||
|
[request : Requests,
|
||||||
|
response : Responses,
|
||||||
|
disposition : {FirstObservation, ReplayObservation,
|
||||||
|
ConflictObservation}]
|
||||||
|
|
||||||
|
VARIABLES currentEligibility, records, effectCount, revision, observation
|
||||||
|
|
||||||
|
vars == <<currentEligibility, records, effectCount, revision, observation>>
|
||||||
|
|
||||||
|
Identity(request) == <<request.scope, request.operation>>
|
||||||
|
|
||||||
|
Eligible(request) ==
|
||||||
|
/\ currentEligibility # NoEligibility
|
||||||
|
/\ currentEligibility = request.eligibility
|
||||||
|
|
||||||
|
Init ==
|
||||||
|
/\ currentEligibility \in EligibilityClaims
|
||||||
|
/\ records = [identity \in Identities |-> NoRecord]
|
||||||
|
/\ effectCount = [identity \in Identities |-> 0]
|
||||||
|
/\ revision = 0
|
||||||
|
/\ observation = NoObservation
|
||||||
|
|
||||||
|
FirstRequest(request) ==
|
||||||
|
LET identity == Identity(request)
|
||||||
|
accepted == Eligible(request)
|
||||||
|
nextRevision == IF accepted THEN revision + 1 ELSE revision
|
||||||
|
response ==
|
||||||
|
[status |-> IF accepted THEN Accepted ELSE PreconditionFailed,
|
||||||
|
revision |-> nextRevision]
|
||||||
|
record == [request |-> request, response |-> response]
|
||||||
|
IN /\ records[identity] = NoRecord
|
||||||
|
/\ records' = [records EXCEPT ![identity] = record]
|
||||||
|
/\ effectCount' =
|
||||||
|
[effectCount EXCEPT ![identity] = IF accepted THEN 1 ELSE 0]
|
||||||
|
/\ revision' = nextRevision
|
||||||
|
/\ observation' =
|
||||||
|
[request |-> request,
|
||||||
|
response |-> response,
|
||||||
|
disposition |-> FirstObservation]
|
||||||
|
/\ UNCHANGED currentEligibility
|
||||||
|
|
||||||
|
ReplayRequest(request) ==
|
||||||
|
LET identity == Identity(request)
|
||||||
|
record == records[identity]
|
||||||
|
IN /\ record # NoRecord
|
||||||
|
/\ record.request = request
|
||||||
|
/\ observation' =
|
||||||
|
[request |-> request,
|
||||||
|
response |-> record.response,
|
||||||
|
disposition |-> ReplayObservation]
|
||||||
|
/\ UNCHANGED <<currentEligibility, records, effectCount, revision>>
|
||||||
|
|
||||||
|
ConflictRequest(request) ==
|
||||||
|
LET identity == Identity(request)
|
||||||
|
record == records[identity]
|
||||||
|
response ==
|
||||||
|
[status |-> PreconditionFailed, revision |-> revision]
|
||||||
|
IN /\ record # NoRecord
|
||||||
|
/\ record.request # request
|
||||||
|
/\ observation' =
|
||||||
|
[request |-> request,
|
||||||
|
response |-> response,
|
||||||
|
disposition |-> ConflictObservation]
|
||||||
|
/\ UNCHANGED <<currentEligibility, records, effectCount, revision>>
|
||||||
|
|
||||||
|
ChangeEligibility(nextEligibility) ==
|
||||||
|
/\ nextEligibility \in EligibilityClaims \cup {NoEligibility}
|
||||||
|
/\ nextEligibility # currentEligibility
|
||||||
|
/\ currentEligibility' = nextEligibility
|
||||||
|
/\ observation' = NoObservation
|
||||||
|
/\ UNCHANGED <<records, effectCount, revision>>
|
||||||
|
|
||||||
|
Next ==
|
||||||
|
\/ \E request \in Requests : FirstRequest(request)
|
||||||
|
\/ \E request \in Requests : ReplayRequest(request)
|
||||||
|
\/ \E request \in Requests : ConflictRequest(request)
|
||||||
|
\/ \E nextEligibility \in EligibilityClaims \cup {NoEligibility} :
|
||||||
|
ChangeEligibility(nextEligibility)
|
||||||
|
|
||||||
|
Spec == Init /\ [][Next]_vars
|
||||||
|
|
||||||
|
TypeOK ==
|
||||||
|
/\ currentEligibility \in EligibilityClaims \cup {NoEligibility}
|
||||||
|
/\ records \in [Identities -> Records \cup {NoRecord}]
|
||||||
|
/\ effectCount \in [Identities -> 0..1]
|
||||||
|
/\ revision \in 0..Cardinality(Identities)
|
||||||
|
/\ observation \in Observations \cup {NoObservation}
|
||||||
|
|
||||||
|
RecordEffectConsistency ==
|
||||||
|
\A identity \in Identities :
|
||||||
|
effectCount[identity] = 1
|
||||||
|
<=> /\ records[identity] # NoRecord
|
||||||
|
/\ records[identity].response.status = Accepted
|
||||||
|
|
||||||
|
RevisionCountsEffects ==
|
||||||
|
revision = Cardinality(
|
||||||
|
{identity \in Identities : effectCount[identity] = 1})
|
||||||
|
|
||||||
|
ObservationConsistent ==
|
||||||
|
observation # NoObservation =>
|
||||||
|
LET identity == Identity(observation.request)
|
||||||
|
record == records[identity]
|
||||||
|
IN /\ record # NoRecord
|
||||||
|
/\ CASE observation.disposition = FirstObservation ->
|
||||||
|
/\ record.request = observation.request
|
||||||
|
/\ record.response = observation.response
|
||||||
|
[] observation.disposition = ReplayObservation ->
|
||||||
|
/\ record.request = observation.request
|
||||||
|
/\ record.response = observation.response
|
||||||
|
[] observation.disposition = ConflictObservation ->
|
||||||
|
/\ record.request # observation.request
|
||||||
|
/\ observation.response.status = PreconditionFailed
|
||||||
|
|
||||||
|
RecordsAppendOnlyAction ==
|
||||||
|
\A identity \in Identities :
|
||||||
|
records[identity] # NoRecord => records'[identity] = records[identity]
|
||||||
|
|
||||||
|
EffectsApplyAtMostOnceAction ==
|
||||||
|
\A identity \in Identities :
|
||||||
|
effectCount[identity] = 1 => effectCount'[identity] = 1
|
||||||
|
|
||||||
|
RevisionMonotonicAction == revision' >= revision
|
||||||
|
|
||||||
|
RecordsAppendOnly == [][RecordsAppendOnlyAction]_vars
|
||||||
|
EffectsApplyAtMostOnce == [][EffectsApplyAtMostOnceAction]_vars
|
||||||
|
RevisionMonotonic == [][RevisionMonotonicAction]_vars
|
||||||
|
|
||||||
|
=============================================================================
|
||||||
|
|
@ -26,6 +26,10 @@ and `operation_id`. Each operation identity retains a request fingerprint. An
|
||||||
exact replay has the original effect; a changed worker, fence, payload, stale,
|
exact replay has the original effect; a changed worker, fence, payload, stale,
|
||||||
or non-authoritative tuple returns `precondition_failed` without mutation.
|
or non-authoritative tuple returns `precondition_failed` without mutation.
|
||||||
|
|
||||||
|
[`RemoraApiMutation`](../../protocol/RemoraApiMutation.tla) models this durable
|
||||||
|
operation ledger around an abstract successful `JobProtocol` effect. It does
|
||||||
|
not redefine job phases or attempt-authority transitions.
|
||||||
|
|
||||||
## Authorization and errors
|
## Authorization and errors
|
||||||
|
|
||||||
Public operations apply the authorization rules in the architecture. List is
|
Public operations apply the authorization rules in the architecture. List is
|
||||||
|
|
|
||||||
|
|
@ -222,7 +222,7 @@ paths:
|
||||||
content:
|
content:
|
||||||
application/json:
|
application/json:
|
||||||
schema:
|
schema:
|
||||||
$ref: '#/components/schemas/AttemptMutation'
|
$ref: '#/components/schemas/AttemptRenewRequest'
|
||||||
responses:
|
responses:
|
||||||
'204':
|
'204':
|
||||||
description: Renewed or duplicate operation accepted.
|
description: Renewed or duplicate operation accepted.
|
||||||
|
|
@ -721,6 +721,10 @@ components:
|
||||||
minimum: 1
|
minimum: 1
|
||||||
worker_incarnation:
|
worker_incarnation:
|
||||||
$ref: '#/components/schemas/OpaqueId'
|
$ref: '#/components/schemas/OpaqueId'
|
||||||
|
AttemptRenewRequest:
|
||||||
|
unevaluatedProperties: false
|
||||||
|
allOf:
|
||||||
|
- $ref: '#/components/schemas/AttemptMutation'
|
||||||
AttemptProgressRequest:
|
AttemptProgressRequest:
|
||||||
unevaluatedProperties: false
|
unevaluatedProperties: false
|
||||||
allOf:
|
allOf:
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue