---- MODULE ProjectDelivery ---- EXTENDS FiniteSets, Naturals CONSTANTS Deliveries, Templates, Identities, Revisions, Others, InitialTemplateRef, InitialMode Continuous == "Continuous" Point == "Point" Modes == {Continuous, Point} Absent == "Absent" Queued == "Queued" Running == "Running" Terminal == "Terminal" Collected == "Collected" \* Retry successors use fresh identities without consuming a source identity again. Attempts == Deliveries \X Modes \X Identities Slots == Attempts \cup Others Phases == {Absent, Queued, Running, Terminal, Collected} VARIABLES mode, templateRef, eligible, persisted, recorded, phase, attemptTemplate VARIABLES wantedRev, templateRev, settled, attemptRev, availableRev VARIABLES prepared, publishRev vars == <> ASSUME /\ Deliveries /= {} /\ Templates /= {} /\ Identities /= {} /\ Revisions /= {} /\ Attempts \cap Others = {} /\ InitialTemplateRef \in [Deliveries -> Templates] /\ InitialMode \in [Deliveries -> Modes] DeliveryOf(attempt) == attempt[1] ModeOf(attempt) == attempt[2] RetryPairs == {pair \in Attempts \X Attempts : /\ pair[1] /= pair[2] /\ DeliveryOf(pair[1]) = DeliveryOf(pair[2]) /\ ModeOf(pair[1]) = ModeOf(pair[2])} SelectedAttempt(delivery) == <> RunningFor(template) == {attempt \in Slots : /\ phase[attempt] = Running /\ attemptTemplate[attempt] = {template}} ActiveFor(template) == {attempt \in Slots : /\ phase[attempt] \in {Queued, Running} /\ attemptTemplate[attempt] = {template}} TypeOK == /\ mode \in [Deliveries -> Modes] /\ templateRef \in [Deliveries -> Templates] /\ eligible \in [Deliveries -> [Modes -> Identities]] /\ persisted \in SUBSET Attempts /\ recorded \in [Deliveries -> [Modes -> SUBSET Attempts]] /\ \A delivery \in Deliveries, sourceMode \in Modes : /\ Cardinality(recorded[delivery][sourceMode]) <= 1 /\ \A attempt \in recorded[delivery][sourceMode] : /\ DeliveryOf(attempt) = delivery /\ ModeOf(attempt) = sourceMode /\ attempt \in persisted /\ phase \in [Slots -> Phases] /\ availableRev \in [Templates -> SUBSET Revisions] /\ wantedRev \in [Templates -> Revisions] /\ templateRev \in [Templates -> Revisions] /\ settled \in [Templates -> BOOLEAN] /\ attemptRev \in [Slots -> SUBSET Revisions] /\ attemptTemplate \in [Slots -> SUBSET Templates] /\ prepared \in [Slots -> SUBSET (Templates \X Revisions)] /\ publishRev \in [Templates -> SUBSET Revisions] /\ \A template \in Templates : Cardinality(publishRev[template]) <= 1 /\ \A attempt \in Slots : /\ Cardinality(prepared[attempt]) <= 1 /\ (phase[attempt] = Absent) = (attemptTemplate[attempt] = {}) /\ phase[attempt] /= Absent => Cardinality(attemptTemplate[attempt]) = 1 /\ (phase[attempt] = Absent) = (attemptRev[attempt] = {}) /\ phase[attempt] /= Absent => Cardinality(attemptRev[attempt]) = 1 Init == /\ mode = InitialMode /\ templateRef = InitialTemplateRef /\ eligible = [delivery \in Deliveries |-> [sourceMode \in Modes |-> CHOOSE identity \in Identities : TRUE]] /\ persisted = {} /\ recorded = [delivery \in Deliveries |-> [sourceMode \in Modes |-> {}]] /\ phase = [attempt \in Slots |-> Absent] /\ attemptTemplate = [attempt \in Slots |-> {}] /\ templateRev = [template \in Templates |-> CHOOSE revision \in Revisions : TRUE] /\ availableRev = [template \in Templates |-> {templateRev[template]}] /\ wantedRev = templateRev /\ settled = [template \in Templates |-> TRUE] /\ attemptRev = [attempt \in Slots |-> {}] /\ prepared = [attempt \in Slots |-> {}] /\ publishRev = [template \in Templates |-> {}] ChangeEligibility(delivery, sourceMode, identity) == /\ identity /= eligible[delivery][sourceMode] /\ eligible' = [eligible EXCEPT ![delivery][sourceMode] = identity] /\ UNCHANGED <> ChangeMode(delivery, sourceMode) == /\ sourceMode /= mode[delivery] /\ mode' = [mode EXCEPT ![delivery] = sourceMode] /\ UNCHANGED <> ChangeTemplateRef(delivery, template) == /\ template /= templateRef[delivery] /\ templateRef' = [templateRef EXCEPT ![delivery] = template] /\ UNCHANGED <> CreationEnabled(delivery) == LET attempt == SelectedAttempt(delivery) IN /\ recorded[delivery][mode[delivery]] = {} /\ attempt \notin persisted /\ phase[attempt] \in {Absent, Collected} /\ prepared[attempt] = {} /\ wantedRev[templateRef[delivery]] = templateRev[templateRef[delivery]] /\ settled[templateRef[delivery]] Prepare(delivery) == LET attempt == SelectedAttempt(delivery) template == templateRef[delivery] IN /\ CreationEnabled(delivery) /\ prepared' = [prepared EXCEPT ![attempt] = {<>}] /\ UNCHANGED <> PrepareRetry(delivery, prior, attempt) == LET template == templateRef[delivery] IN /\ prior \in Attempts /\ attempt \in Attempts /\ DeliveryOf(prior) = delivery /\ DeliveryOf(attempt) = delivery /\ ModeOf(prior) = ModeOf(attempt) /\ prior \in persisted /\ phase[prior] \in {Terminal, Collected} /\ attempt \notin persisted /\ phase[attempt] \in {Absent, Collected} /\ prepared[attempt] = {} /\ \A sourceMode \in Modes : recorded[delivery][sourceMode] = {} /\ wantedRev[template] = templateRev[template] /\ settled[template] /\ prepared' = [prepared EXCEPT ![attempt] = {<>}] /\ UNCHANGED <> \* An API call can finish after a later observation without rereading admission gates. Create(attempt) == LET binding == CHOOSE value \in prepared[attempt] : TRUE IN /\ attempt \in Slots /\ prepared[attempt] /= {} /\ IF attempt \in Attempts THEN /\ phase[attempt] \in {Absent, Collected} /\ attempt \notin persisted ELSE phase[attempt] \in {Absent, Terminal, Collected} /\ phase' = [phase EXCEPT ![attempt] = Queued] /\ attemptTemplate' = [attemptTemplate EXCEPT ![attempt] = {binding[1]}] /\ attemptRev' = [attemptRev EXCEPT ![attempt] = {binding[2]}] /\ prepared' = [prepared EXCEPT ![attempt] = {}] /\ UNCHANGED <> Start(attempt) == /\ attempt \in Slots /\ phase[attempt] = Queued /\ \A template \in attemptTemplate[attempt] : RunningFor(template) = {} /\ phase' = [phase EXCEPT ![attempt] = Running] /\ UNCHANGED <> Persist(delivery, attempt) == /\ attempt \in Attempts /\ DeliveryOf(attempt) = delivery /\ attempt \notin persisted /\ phase[attempt] /= Absent /\ recorded[delivery][ModeOf(attempt)] = {} /\ persisted' = persisted \cup {attempt} /\ recorded' = [recorded EXCEPT ![delivery][ModeOf(attempt)] = {attempt}] /\ UNCHANGED <> Complete(attempt) == /\ attempt \in Slots /\ phase[attempt] = Running /\ phase' = [phase EXCEPT ![attempt] = Terminal] /\ UNCHANGED <> Reject(attempt) == /\ attempt \in Slots /\ phase[attempt] = Queued /\ phase' = [phase EXCEPT ![attempt] = Terminal] /\ UNCHANGED <> Release(delivery, sourceMode) == /\ recorded[delivery][sourceMode] /= {} /\ \A attempt \in recorded[delivery][sourceMode] : phase[attempt] \in {Terminal, Collected} /\ recorded' = [recorded EXCEPT ![delivery][sourceMode] = {}] /\ UNCHANGED <> Collect(attempt) == /\ attempt \in Attempts /\ phase[attempt] = Terminal /\ attempt \in persisted /\ phase' = [phase EXCEPT ![attempt] = Collected] /\ UNCHANGED <> \* Other slots represent fresh workflow executions and can be reused after completion. PrepareOther(attempt, template) == /\ attempt \in Others /\ phase[attempt] \in {Absent, Terminal, Collected} /\ prepared[attempt] = {} /\ wantedRev[template] = templateRev[template] /\ settled[template] /\ prepared' = [prepared EXCEPT ![attempt] = {<>}] /\ UNCHANGED <> \* Requests record observed bundles; writes preceding observation leave wantedRev unchanged. RequestTemplate(template, revision) == /\ revision /= wantedRev[template] /\ wantedRev' = [wantedRev EXCEPT ![template] = revision] /\ settled' = [settled EXCEPT ![template] = FALSE] /\ publishRev' = [publishRev EXCEPT ![template] = {}] /\ UNCHANGED <> \* Settle abstracts expiration of the configured quiet interval after the latest change. Settle(template) == /\ ~settled[template] /\ settled' = [settled EXCEPT ![template] = TRUE] /\ UNCHANGED <> PreparePublish(template) == /\ wantedRev[template] /= templateRev[template] /\ settled[template] /\ ActiveFor(template) = {} /\ publishRev[template] = {} /\ publishRev' = [publishRev EXCEPT ![template] = {wantedRev[template]}] /\ UNCHANGED <> Publish(template) == /\ wantedRev[template] /= templateRev[template] /\ settled[template] /\ publishRev[template] = {wantedRev[template]} /\ templateRev' = [templateRev EXCEPT ![template] = wantedRev[template]] /\ availableRev' = [availableRev EXCEPT ![template] = @ \cup {wantedRev[template]}] /\ publishRev' = [publishRev EXCEPT ![template] = {}] /\ UNCHANGED <> Next == \/ \E template \in Templates, revision \in Revisions : RequestTemplate(template, revision) \/ \E template \in Templates : Settle(template) \/ \E template \in Templates : PreparePublish(template) \/ \E template \in Templates : Publish(template) \/ \E delivery \in Deliveries, sourceMode \in Modes, identity \in Identities : ChangeEligibility(delivery, sourceMode, identity) \/ \E delivery \in Deliveries, sourceMode \in Modes : ChangeMode(delivery, sourceMode) \/ \E delivery \in Deliveries, template \in Templates : ChangeTemplateRef(delivery, template) \/ \E delivery \in Deliveries : Prepare(delivery) \/ \E pair \in RetryPairs : PrepareRetry(DeliveryOf(pair[1]), pair[1], pair[2]) \/ \E attempt \in Others, template \in Templates : PrepareOther(attempt, template) \/ \E attempt \in Slots : Create(attempt) \/ \E attempt \in Slots : Start(attempt) \/ \E delivery \in Deliveries, attempt \in Attempts : Persist(delivery, attempt) \/ \E attempt \in Slots : Complete(attempt) \/ \E attempt \in Slots : Reject(attempt) \/ \E delivery \in Deliveries, sourceMode \in Modes : Release(delivery, sourceMode) \/ \E attempt \in Attempts : Collect(attempt) Spec == Init /\ [][Next]_vars \* Argo Secure mode also reads the captured specification during artifact cleanup. TemplateAvailable == \A attempt \in Slots : phase[attempt] \notin {Absent, Collected} => \A template \in attemptTemplate[attempt] : attemptRev[attempt] \subseteq availableRev[template] CollectedSaved == \A attempt \in Attempts : phase[attempt] = Collected => attempt \in persisted NoConcurrentRuns == \A template \in Templates : Cardinality(RunningFor(template)) <= 1 TemplateStable == [][\A attempt \in Slots : phase[attempt] \in {Queued, Running} => UNCHANGED <>]_vars PreparedAvailable == \A attempt \in Slots : \A binding \in prepared[attempt] : binding[2] \in availableRev[binding[1]] AdmissionReady == [][\A attempt \in Slots : \A binding \in prepared'[attempt] \ prepared[attempt] : /\ settled[binding[1]] /\ binding[2] = wantedRev[binding[1]] /\ binding[2] = templateRev[binding[1]]]_vars PublishDrained == [][\A template \in Templates : publishRev'[template] \ publishRev[template] /= {} => /\ settled[template] /\ ActiveFor(template) = {} /\ publishRev'[template] = {wantedRev[template]}]_vars PublishLatest == [][\A template \in Templates : templateRev'[template] /= templateRev[template] => /\ settled[template] /\ publishRev[template] = {wantedRev[template]} /\ templateRev'[template] = wantedRev[template]]_vars ProgressFair == /\ Spec /\ \A template \in Templates : /\ WF_vars(Settle(template)) /\ WF_vars(PreparePublish(template)) /\ WF_vars(Publish(template)) /\ \A attempt \in Slots : /\ WF_vars(Create(attempt)) /\ WF_vars(Complete(attempt)) FairSpec == /\ ProgressFair /\ \A attempt \in Slots : WF_vars(Start(attempt)) PublishProgress == \A template \in Templates, revision \in Revisions : (<>[](wantedRev[template] = revision)) => <>[](templateRev[template] = revision) \* Strong fairness abstracts Argo's equal-priority mutex queue, ordered by creation time. LiveSpec == /\ ProgressFair /\ \A attempt \in Slots : SF_vars(Start(attempt)) /\ \A delivery \in Deliveries : WF_vars(Prepare(delivery)) /\ \A delivery \in Deliveries, attempt \in Attempts : WF_vars(Persist(delivery, attempt)) /\ \A delivery \in Deliveries, sourceMode \in Modes : WF_vars(Release(delivery, sourceMode)) DebounceProgress == \A template \in Templates, revision \in Revisions : (<>[](wantedRev[template] = revision)) => <>[]settled[template] DrainProgress == \A template \in Templates : (wantedRev[template] /= templateRev[template]) ~> (ActiveFor(template) = {} \/ wantedRev[template] = templateRev[template]) AttemptProgress == \A attempt \in Slots : (phase[attempt] \in {Queued, Running}) ~> (phase[attempt] \in {Terminal, Collected}) ReleaseProgress == \A delivery \in Deliveries, sourceMode \in Modes : (/\ recorded[delivery][sourceMode] /= {} /\ \A attempt \in recorded[delivery][sourceMode] : phase[attempt] \in {Terminal, Collected}) ~> (recorded[delivery][sourceMode] = {}) RecordOnce == [][\A delivery \in Deliveries, sourceMode \in Modes : (recorded'[delivery][sourceMode] \ recorded[delivery][sourceMode]) \subseteq (persisted' \ persisted)]_vars RecordProgress == \A attempt \in Attempts : (phase[attempt] /= Absent) ~> (attempt \in persisted) SelectedFor(delivery, attempt, template, revision) == /\ SelectedAttempt(delivery) = attempt /\ templateRef[delivery] = template /\ wantedRev[template] = revision DeliveryProgress == \A delivery \in Deliveries, attempt \in Attempts, template \in Templates, revision \in Revisions : (<>[]SelectedFor(delivery, attempt, template, revision)) => <>(attempt \in persisted /\ phase[attempt] \in {Terminal, Collected}) ContinuousOnly == \A delivery \in Deliveries : mode[delivery] = Continuous LiveDomain == /\ ContinuousOnly /\ \A delivery \in Deliveries : eligible[delivery][Point] = CHOOSE identity \in Identities : TRUE SharedTemplateRef == [delivery \in Deliveries |-> CHOOSE template \in Templates : TRUE] ContinuousMode == [delivery \in Deliveries |-> Continuous] ====