---- MODULE MirroredImage ---- EXTENDS Naturals, FiniteSets, TLC CONSTANTS Tagged, Safe, AllowRemoval, AllowFailure, AllowRetry, RetryStageSet, MaxEpoch, MaxRetries Stages == {"prepare", "promotion", "cleanup"} Repos == {"candidate", "mirror"} Children == {"amd64", "arm64"} Phases == {"absent", "pending", "queued", "running", "published", "succeeded", "failed"} Terminal(p) == p \in {"succeeded", "failed"} VARIABLES desired, managed, retired, attempts, workflows, scanned, verdict, stored, temporary, durableTags, promoted, copies, retryCount, retryAck, cleanupObserved, grace, epoch, retryStage, badPromotion vars == <> Available(r) == stored[r] /\ (temporary[r] \/ durableTags[r] # {}) Active == \E s \in Stages : attempts[s] # "absent" /\ ~Terminal(attempts[s]) Init == /\ desired = TRUE /\ managed = FALSE /\ retired = FALSE /\ attempts = [s \in Stages |-> "absent"] /\ workflows = [s \in Stages |-> "absent"] /\ scanned = {} /\ verdict = "pending" /\ stored = [r \in Repos |-> FALSE] /\ temporary = [r \in Repos |-> FALSE] /\ durableTags = [r \in Repos |-> {}] /\ promoted = FALSE /\ copies = 0 /\ retryCount = 0 /\ retryAck = FALSE /\ cleanupObserved = FALSE /\ grace = [r \in Repos |-> FALSE] /\ epoch = 1 /\ retryStage = "none" /\ badPromotion = FALSE Manage == /\ desired /\ ~managed /\ ~retired /\ managed' = TRUE /\ UNCHANGED <> Plan(s) == /\ managed /\ ~Active /\ attempts[s] = "absent" /\ CASE s = "prepare" -> desired /\ attempts["cleanup"] = "absent" [] s = "promotion" -> desired /\ attempts["cleanup"] = "absent" /\ attempts["prepare"] = "succeeded" /\ verdict = "accepted" [] s = "cleanup" -> ~desired /\ attempts' = [attempts EXCEPT ![s] = "pending"] /\ UNCHANGED <> Submit(s) == /\ attempts[s] = "pending" /\ workflows[s] = "absent" /\ s = "cleanup" => \A t \in {"prepare", "promotion"} : attempts[t] = "absent" \/ Terminal(attempts[t]) /\ workflows' = [workflows EXCEPT ![s] = "queued"] /\ retryAck' = (retryAck \/ (retryCount > 0 /\ s = retryStage)) /\ UNCHANGED <> Start(s) == /\ workflows[s] = "queued" /\ ~\E t \in Stages : workflows[t] \in {"running", "published"} /\ workflows' = [workflows EXCEPT ![s] = "running"] /\ UNCHANGED <> PartialCopy(s) == /\ s \in {"prepare", "promotion"} /\ workflows[s] = "running" /\ s = "promotion" => Available("candidate") /\ LET r == IF s = "prepare" THEN "candidate" ELSE "mirror" IN /\ ~stored[r] /\ stored' = [stored EXCEPT ![r] = TRUE] /\ temporary' = [temporary EXCEPT ![r] = TRUE] /\ grace' = [grace EXCEPT ![r] = FALSE] /\ badPromotion' = (badPromotion \/ (s = "promotion" /\ (~Safe \/ verdict # "accepted" \/ scanned # Children))) /\ UNCHANGED <> Publish(s) == /\ s \in {"prepare", "promotion"} /\ workflows[s] = "running" /\ s = "promotion" => Available("candidate") /\ LET r == IF s = "prepare" THEN "candidate" ELSE "mirror" IN /\ stored' = [stored EXCEPT ![r] = TRUE] /\ temporary' = [temporary EXCEPT ![r] = TRUE] /\ durableTags' = [durableTags EXCEPT ![r] = @ \cup {"retention"}] /\ grace' = [grace EXCEPT ![r] = FALSE] /\ promoted' = (promoted \/ (s = "promotion" /\ attempts["cleanup"] = "absent")) /\ copies' = IF s = "promotion" THEN copies + 1 ELSE copies /\ badPromotion' = (badPromotion \/ (s = "promotion" /\ (~Safe \/ verdict # "accepted" \/ scanned # Children))) /\ workflows' = [workflows EXCEPT ![s] = "published"] /\ UNCHANGED <> Alias(s) == /\ Tagged /\ s \in {"prepare", "promotion"} /\ workflows[s] = "published" /\ LET r == IF s = "prepare" THEN "candidate" ELSE "mirror" IN /\ "declared" \notin durableTags[r] /\ durableTags' = [durableTags EXCEPT ![r] = @ \cup {"declared"}] /\ UNCHANGED <> Delete(r) == /\ workflows["cleanup"] = "running" /\ durableTags[r] # {} /\ durableTags' = [durableTags EXCEPT ![r] = {}] /\ UNCHANGED <> Finish(s, success) == /\ workflows[s] \in {"running", "published"} /\ success \/ AllowFailure \/ (s = "promotion" /\ ~Available("candidate")) /\ success => IF s = "cleanup" THEN \A r \in Repos : durableTags[r] = {} ELSE /\ workflows[s] = "published" /\ Tagged => "declared" \in durableTags[IF s = "prepare" THEN "candidate" ELSE "mirror"] /\ workflows' = [workflows EXCEPT ![s] = IF success THEN "succeeded" ELSE "failed"] /\ UNCHANGED <> Observe(s) == /\ attempts[s] = "pending" /\ Terminal(workflows[s]) /\ attempts' = [attempts EXCEPT ![s] = workflows[s]] /\ UNCHANGED <> Scan(c) == /\ desired /\ attempts["prepare"] = "succeeded" /\ attempts["cleanup"] = "absent" /\ verdict = "pending" /\ Available("candidate") /\ c \notin scanned /\ scanned' = scanned \cup {c} /\ verdict' = IF ~Safe /\ c = "arm64" THEN "rejected" ELSE IF scanned' = Children THEN "accepted" ELSE "pending" /\ UNCHANGED <> Expire(r) == /\ temporary[r] /\ temporary' = [temporary EXCEPT ![r] = FALSE] /\ UNCHANGED <> Age(r) == /\ stored[r] /\ ~Available(r) /\ ~grace[r] /\ grace' = [grace EXCEPT ![r] = TRUE] /\ UNCHANGED <> Collect(r) == /\ stored[r] /\ ~Available(r) /\ grace[r] /\ stored' = [stored EXCEPT ![r] = FALSE] /\ UNCHANGED <> Remove == /\ AllowRemoval /\ desired /\ desired' = FALSE /\ UNCHANGED <> Forget == /\ managed /\ ~Active /\ attempts["cleanup"] = "succeeded" /\ \A r \in Repos : ~stored[r] /\ managed' = desired /\ retired' = ~desired /\ cleanupObserved' = ~desired /\ attempts' = IF desired THEN [s \in Stages |-> "absent"] ELSE attempts /\ workflows' = IF desired THEN [s \in Stages |-> "absent"] ELSE workflows /\ scanned' = IF desired THEN {} ELSE scanned /\ verdict' = IF desired THEN "pending" ELSE verdict /\ promoted' = IF desired THEN FALSE ELSE promoted /\ retryAck' = IF desired THEN FALSE ELSE retryAck /\ UNCHANGED <> Readd == /\ ~desired /\ epoch < MaxEpoch /\ desired' = TRUE /\ epoch' = epoch + 1 /\ retired' = FALSE /\ promoted' = FALSE /\ cleanupObserved' = FALSE /\ attempts' = IF managed THEN attempts ELSE [s \in Stages |-> "absent"] /\ workflows' = IF managed THEN workflows ELSE [s \in Stages |-> "absent"] /\ scanned' = IF managed THEN scanned ELSE {} /\ verdict' = IF managed THEN verdict ELSE "pending" /\ retryAck' = IF managed THEN retryAck ELSE FALSE /\ UNCHANGED <> ObservePromoted == /\ desired /\ ~promoted /\ attempts["promotion"] = "succeeded" /\ attempts["cleanup"] = "absent" /\ Available("mirror") /\ promoted' = TRUE /\ UNCHANGED <> Retry(s) == /\ AllowRetry /\ retryCount < MaxRetries /\ managed /\ ~Active /\ Terminal(attempts[s]) /\ attempts' = [t \in Stages |-> IF t = s THEN "pending" ELSE IF s = "prepare" /\ t = "promotion" THEN "absent" ELSE IF t = "cleanup" /\ attempts[t] # "absent" THEN "pending" ELSE attempts[t]] /\ workflows' = [t \in Stages |-> IF t = s \/ (s = "prepare" /\ t = "promotion") \/ t = "cleanup" THEN "absent" ELSE workflows[t]] /\ scanned' = IF s = "prepare" THEN {} ELSE scanned /\ verdict' = IF s = "prepare" THEN "pending" ELSE verdict /\ retryCount' = retryCount + 1 /\ retryStage' = s /\ retryAck' = FALSE /\ UNCHANGED <> Next == \/ Manage \/ \E s \in Stages : Plan(s) \/ Submit(s) \/ Start(s) \/ Observe(s) \/ \E success \in BOOLEAN : Finish(s, success) \/ \E s \in {"prepare", "promotion"} : PartialCopy(s) \/ Publish(s) \/ Alias(s) \/ \E r \in Repos : Delete(r) \/ Expire(r) \/ Age(r) \/ Collect(r) \/ \E c \in Children : Scan(c) \/ Remove \/ Forget \/ (\E s \in RetryStageSet : Retry(s)) \/ Readd \/ ObservePromoted Spec == Init /\ [][Next]_vars FairSpec == Spec /\ WF_vars(Manage) /\ WF_vars(Forget) /\ WF_vars(ObservePromoted) /\ (\A s \in Stages : WF_vars(Plan(s)) /\ WF_vars(Submit(s)) /\ WF_vars(Start(s)) /\ WF_vars(Observe(s)) /\ WF_vars(Finish(s, TRUE) \/ Finish(s, FALSE))) /\ (\A s \in {"prepare", "promotion"} : WF_vars(Publish(s)) /\ WF_vars(Alias(s))) /\ (\A r \in Repos : WF_vars(Delete(r)) /\ WF_vars(Expire(r)) /\ WF_vars(Age(r)) /\ WF_vars(Collect(r))) /\ (\A c \in Children : WF_vars(Scan(c))) TypeOK == /\ desired \in BOOLEAN /\ managed \in BOOLEAN /\ retired \in BOOLEAN /\ attempts \in [Stages -> Phases] /\ workflows \in [Stages -> Phases] /\ scanned \subseteq Children /\ verdict \in {"pending", "accepted", "rejected"} /\ stored \in [Repos -> BOOLEAN] /\ temporary \in [Repos -> BOOLEAN] /\ durableTags \in [Repos -> SUBSET {"retention", "declared", "retry"}] /\ promoted \in BOOLEAN /\ copies \in Nat /\ epoch \in 1..MaxEpoch /\ retryCount \in 0..MaxRetries /\ retryAck \in BOOLEAN /\ retryStage \in Stages \cup {"none"} /\ badPromotion \in BOOLEAN /\ cleanupObserved \in BOOLEAN /\ grace \in [Repos -> BOOLEAN] PromotionRequiresAcceptance == ~badPromotion DeclaredPromotionRetained == desired /\ promoted => Available("mirror") DeclaredCandidateRetained == desired /\ attempts["cleanup"] = "absent" /\ attempts["prepare"] = "succeeded" => Available("candidate") CleanupObservedMeansGone == cleanupObserved => \A r \in Repos : ~stored[r] DeclaredAliasOnlyWhenRequested == ~Tagged => \A r \in Repos : "declared" \notin durableTags[r] RetryPreservesTagIdentity == \A r \in Repos : durableTags[r] \subseteq {"retention", "declared"} RetryAcknowledgedAfterCreation == retryAck => retryStage # "none" /\ workflows[retryStage] # "absent" CleanupExcludesCopy == workflows["cleanup"] \in {"running", "published"} => \A s \in {"prepare", "promotion"} : workflows[s] \notin {"running", "published"} EventuallyPromoted == (desired /\ managed) ~> (~desired \/ promoted) EventuallyRetired == (~desired /\ managed) ~> (retired \/ desired) EventuallyRejected == <>(verdict = "rejected") ====