github.com/kaisenlinux/docker.io@v0.0.0-20230510090727-ea55db55fac7/swarmkit/design/tla/SwarmKit.tla (about) 1 This is a TLA+ model of SwarmKit. Even if you don't know TLA+, you should be able to 2 get the general idea. This section gives a very brief overview of the syntax. 3 4 Declare `x' to be something that changes as the system runs: 5 6 VARIABLE x 7 8 Define `Init' to be a state predicate (== means ``is defined to be''): 9 10 Init == 11 x = 0 12 13 `Init' is true for states in which `x = 0'. This can be used to define 14 the possible initial states of the system. For example, the state 15 [ x |-> 0, y |-> 2, ... ] satisfies this. 16 17 Define `Next' to be an action: 18 19 Next == 20 /\ x' \in Nat 21 /\ x' > x 22 23 An action takes a pair of states, representing an atomic step of the system. 24 Unprimed expressions (e.g. `x') refer to the old state, and primed ones to 25 the new state. This example says that a step is a `Next' step iff the new 26 value of `x' is a natural number and greater than the previous value. 27 For example, [ x |-> 3, ... ] -> [x |-> 10, ... ] is a `Next' step. 28 29 /\ is logical ``and''. This example uses TLA's ``bulleted-list'' syntax, which makes 30 writing these easier. It is indentation-sensitive. TLA also has \/ lists (``or''). 31 32 See `.http://lamport.azurewebsites.net/tla/summary.pdf.' for a more complete summary 33 of the syntax. 34 35 This specification can be read as documentation, but it can also be executed by the TLC 36 model checker. See the model checking section below for details about that. 37 38 The rest of the document is organised as follows: 39 40 1. Parameters to the model 41 2. Types and definitions 42 3. How to run the model checker 43 4. Actions performed by the user 44 5. Actions performed by the components of SwarmKit 45 6. The complete system 46 7. Properties of the system 47 48 -------------------------------- MODULE SwarmKit -------------------------------- 49 50 (* Import some libraries we use. 51 Common SwarmKit types are defined in Types.tla. You should probably read that before continuing. *) 52 EXTENDS Integers, TLC, FiniteSets, \* From the TLA+ standard library 53 Types, \* SwarmKit types 54 Tasks, \* The `tasks' variable 55 WorkerSpec, \* High-level spec for worker nodes 56 EventCounter \* Event limiting, for modelling purposes 57 58 (* The maximum number of terminated tasks to keep for each slot. *) 59 CONSTANT maxTerminated 60 ASSUME maxTerminated \in Nat 61 62 (* In the model, we share taskIDs (see ModelTaskId), which means that 63 we can cover most behaviours with only enough task IDs 64 for one running task and maxTerminated finished ones. *) 65 ASSUME Cardinality(TaskId) >= 1 + maxTerminated 66 67 ------------------------------------------------------------------------------- 68 \* Services 69 70 VARIABLE services \* A map of currently-allocated services, indexed by ServiceId 71 72 (* A replicated service is one that specifies some number of replicas it wants. *) 73 IsReplicated(sid) == 74 services[sid].replicas \in Nat 75 76 (* A global service is one that wants one task running on each node. *) 77 IsGlobal(sid) == 78 services[sid].replicas = global 79 80 (* TasksOf(sid) is the set of tasks for service `sid'. *) 81 TasksOf(sid) == 82 { t \in tasks : t.service = sid } 83 84 (* All tasks of service `sid' in `vslot'. *) 85 TasksOfVSlot(sid, vslot) == 86 { t \in TasksOf(sid) : VSlot(t) = vslot } 87 88 (* All vslots of service `sid'. *) 89 VSlotsOf(sid) == 90 { VSlot(t) : t \in TasksOf(sid) } 91 92 ------------------------------------------------------------------------------- 93 \* Types 94 95 (* The expected type of each variable. TLA+ is an untyped language, but the model checker 96 can check that TypeOK is true for every reachable state. *) 97 TypeOK == 98 \* `services' is a mapping from service IDs to ServiceSpecs: 99 /\ DOMAIN services \subseteq ServiceId 100 /\ services \in [ DOMAIN services -> ServiceSpec ] 101 /\ TasksTypeOK \* Defined in Types.tla 102 /\ WorkerTypeOK \* Defined in WorkerSpec.tla 103 104 ------------------------------------------------------------------------------- 105 (* 106 `^ \textbf{Model checking} ^' 107 108 You can test this specification using the TLC model checker. 109 This section describes how to do that. If you don't want to run TLC, 110 you can skip this section. 111 112 To use TLC, load this specification file in the TLA+ toolbox (``Open Spec'') 113 and create a new model using the menu. 114 115 You will be prompted to enter values for the various CONSTANTS. 116 A suitable set of initial values is: 117 118 `. 119 Node <- [ model value ] {n1} 120 ServiceId <- [ model value ] {s1} 121 TaskId <- [ model value ] {t1, t2} 122 maxReplicas <- 1 123 maxTerminated <- 1 124 .' 125 126 For the [ model value ] ones, select `Set of model values'. 127 128 This says that we have one node, `n1', at most one service, and at most 129 two tasks per vslot. TLC can explore all possible behaviours of this system 130 in a couple of seconds on my laptop. 131 132 You should also specify some things to check (under ``What to check?''): 133 134 - Add `TypeOK' and `Inv' under ``Invariants'' 135 - Add `TransitionsOK' and `EventuallyAsDesired' under ``Properties'' 136 137 Running the model should report ``No errors''. 138 139 If the model fails, TLC will show you an example sequence of actions that lead to 140 the failure and you can inspect the state at each step. You can try this out by 141 commenting out any important-looking condition in the model (e.g. the requirement 142 in UpdateService that you can't change the mode of an existing service). 143 144 Although the above model is very small, it should detect most errors that you might 145 accidentally introduce when modifying the specification. Increasing the number of nodes, 146 services, replicas or terminated tasks will check more behaviours of the system, 147 but will be MUCH slower. 148 149 The rest of this section describes techniques to make model checking faster by reducing 150 the number of states that must be considered in various ways. Feel free to skip it. 151 152 `^ \textbf{Symmetry sets} ^' 153 154 You should configure any model sets (e.g. `TaskId') as `symmetry sets'. 155 For example, if you have a model with two nodes {n1, n2} then this tells TLC that 156 two states which are the same except that n1 and n2 are swapped are equivalent 157 and it only needs to continue exploring from one of them. 158 TLC will warn that checking temporal properties may not work correctly, 159 but it's much faster and I haven't had any problems with it. 160 161 `^ \textbf{Limiting the maximum number of setbacks to consider} ^' 162 163 Another way to speed things up is to reduce the number of failures that TLC must consider. 164 By default, it checks every possible combination of failures at every point, which 165 is very expensive. 166 In the `Advanced Options' panel of the model, add a ``Definition Override'' of e.g. 167 `maxEvents = 2'. Actions that represent unnecessary extra work (such as the user 168 changing the configuration or a worker node going down) are tagged with `CountEvent'. 169 Any run of the system cannot have more than `maxEvents' such events. 170 171 See `EventCounter.tla' for details. 172 173 `^ \textbf{Preventing certain failures} ^' 174 175 If you're not interested in some actions then you can block them. For example, 176 adding these two constraints in the ``Action Constraint'' box of the 177 ``Advanced Options'' tab tells TLC not to consider workers going down or 178 workers rejecting tasks as possible actions: 179 180 /\ ~WorkerDown 181 /\ ~RejectTask 182 *) 183 184 (* 185 `^ \textbf{Combining task states} ^' 186 187 A finished task can be either in the `complete' or `failed' state, depending on 188 its exit status. If we have 4 finished tasks, that's 16 different states. For 189 modelling, we might not care about exit codes and we can treat this as a single 190 state with another definition override: 191 192 `.failed <- complete.' 193 194 In a similar way, we can combine { assigned, accepted, preparing, ready } into a single 195 state: 196 197 `.accepted <- assigned 198 preparing <- assigned 199 ready <- assigned.' 200 *) 201 202 ---------------------------- MODULE User -------------------------------------------- 203 \* Actions performed by users 204 205 (* Create a new service with any ServiceSpec. 206 207 This says that a single atomic step of the system from an old state 208 to a new one is a CreateService step iff `tasks', `nodes' and `nEvents' don't change 209 and the new value of `services' is the same as before except that some 210 service ID that wasn't used in the old state is now mapped to some 211 ServiceSpec. 212 213 Note: A \ B means { x \in A : x \notin B } -- 214 i.e. the set A with all elements in B removed. 215 *) 216 CreateService == 217 /\ UNCHANGED << tasks, nodes, nEvents >> 218 /\ \E sid \in ServiceId \ DOMAIN services, \* `sid' is an unused ServiceId 219 spec \in ServiceSpec : \* `spec' is any ServiceSpec 220 /\ spec.remove = FALSE \* (not flagged for removal) 221 /\ services' = services @@ sid :> spec \* Add `sid |-> spec' to `services' 222 223 (* Update an existing service's spec. *) 224 UpdateService == 225 /\ UNCHANGED << tasks, nodes >> 226 /\ CountEvent \* Flag as an event for model-checking purposes 227 /\ \E sid \in DOMAIN services, \* `sid' is an existing ServiceId 228 newSpec \in ServiceSpec : \* `newSpec' is any `ServiceSpec' 229 /\ services[sid].remove = FALSE \* We weren't trying to remove sid 230 /\ newSpec.remove = FALSE \* and we still aren't. 231 \* You can't change a service's mode: 232 /\ (services[sid].replicas = global) <=> (newSpec.replicas = global) 233 /\ services' = [ services EXCEPT ![sid] = newSpec ] 234 235 (* The user removes a service. 236 237 Note: Currently, SwarmKit deletes the service from its records immediately. 238 However, this isn't right because we need to wait for service-level resources 239 such as Virtual IPs to be freed. 240 Here we model the proposed fix, in which we just flag the service for removal. *) 241 RemoveService == 242 /\ UNCHANGED << nodes >> 243 /\ CountEvent 244 /\ \E sid \in DOMAIN services : \* sid is some existing service 245 \* Flag service for removal: 246 /\ services' = [services EXCEPT ![sid].remove = TRUE] 247 \* Flag every task of the service for removal: 248 /\ UpdateTasks([ t \in TasksOf(sid) |-> 249 [t EXCEPT !.desired_state = remove] ]) 250 251 (* A user action is one of these. *) 252 User == 253 \/ CreateService 254 \/ UpdateService 255 \/ RemoveService 256 257 ============================================================================= 258 259 ---------------------------- MODULE Orchestrator ---------------------------- 260 261 \* Actions performed the orchestrator 262 263 \* Note: This is by far the most complicated component in the model. 264 \* You might want to read this section last... 265 266 (* The set of tasks for service `sid' that should be considered as active. 267 This is any task that is running or on its way to running. *) 268 RunnableTasks(sid) == 269 { t \in TasksOf(sid) : Runnable(t) } 270 271 (* Candidates for shutting down when we have too many. We don't want to count tasks that are shutting down 272 towards the total count when deciding whether we need to kill anything. *) 273 RunnableWantedTasks(sid) == 274 { t \in RunnableTasks(sid) : t.desired_state \preceq running } 275 276 (* The set of possible new vslots for `sid'. *) 277 UnusedVSlot(sid) == 278 IF IsReplicated(sid) THEN Slot \ VSlotsOf(sid) 279 ELSE Node \ VSlotsOf(sid) 280 281 (* The set of possible IDs for a new task in a vslot. 282 283 The complexity here is just a side-effect of the modelling (where we need to 284 share and reuse task IDs for performance). 285 In the real system, choosing an unused ID is easy. *) 286 UnusedId(sid, vslot) == 287 LET swarmTaskIds == { t.id : t \in TasksOfVSlot(sid, vslot) } 288 IN TaskId \ swarmTaskIds 289 290 (* Create a new task/slot if the number of runnable tasks is less than the number requested. *) 291 CreateSlot == 292 /\ UNCHANGED << services, nodes, nEvents >> 293 /\ \E sid \in DOMAIN services : \* `sid' is an existing service 294 /\ ~services[sid].remove \* that we're not trying to remove 295 (* For replicated tasks, only create as many slots as we need. 296 For global tasks, we want all possible vslots (nodes). *) 297 /\ IsReplicated(sid) => 298 services[sid].replicas > Cardinality(VSlotsOf(sid)) \* Desired > actual 299 /\ \E slot \in UnusedVSlot(sid) : 300 \E id \in UnusedId(sid, slot) : 301 tasks' = tasks \union { NewTask(sid, slot, id, running) } 302 303 (* Add a task if a slot exists, contains no runnable tasks, and we weren't trying to remove it. 304 Note: if we are trying to remove it, the slot will eventually disappear and CreateSlot will 305 then make a new one if we later need it again. 306 307 Currently in SwarmKit, slots do not actually exist as objects in the store. 308 Instead, we just infer that a slot exists because there exists a task with that slot ID. 309 This has the odd effect that if `maxTerminated = 0' then we may create new slots rather than reusing 310 existing ones, depending on exactly when the reaper runs. 311 *) 312 ReplaceTask == 313 /\ UNCHANGED << services, nodes, nEvents >> 314 /\ \E sid \in DOMAIN services : 315 \E slot \in VSlotsOf(sid) : 316 /\ \A task \in TasksOfVSlot(sid, slot) : \* If all tasks in `slot' are 317 ~Runnable(task) \* dead (not runnable) and 318 /\ \E task \in TasksOfVSlot(sid, slot) : \* there is some task that 319 task.desired_state # remove \* we're not trying to remove, 320 /\ \E id \in UnusedId(sid, slot) : \* then create a replacement task: 321 tasks' = tasks \union { NewTask(sid, slot, id, running) } 322 323 (* If we have more replicas than the spec asks for, remove one of them. *) 324 RequestRemoval == 325 /\ UNCHANGED << services, nodes, nEvents >> 326 /\ \E sid \in DOMAIN services : 327 LET current == RunnableWantedTasks(sid) 328 IN \* Note: `current' excludes tasks we're already trying to kill 329 /\ IsReplicated(sid) 330 /\ services[sid].replicas < Cardinality(current) \* We have too many replicas 331 /\ \E slot \in { t.slot : t \in current } : \* Choose an allocated slot 332 \* Mark all tasks for that slot for removal: 333 UpdateTasks( [ t \in TasksOfVSlot(sid, slot) |-> 334 [t EXCEPT !.desired_state = remove] ] ) 335 336 (* Mark a terminated task for removal if we already have `maxTerminated' terminated tasks for this slot. *) 337 CleanupTerminated == 338 /\ UNCHANGED << services, nodes, nEvents >> 339 /\ \E sid \in DOMAIN services : 340 \E slot \in VSlotsOf(sid) : 341 LET termTasksInSlot == { t \in TasksOfVSlot(sid, slot) : 342 State(t) \in { complete, shutdown, failed, rejected } } 343 IN 344 /\ Cardinality(termTasksInSlot) > maxTerminated \* Too many tasks for slot 345 /\ \E t \in termTasksInSlot : \* Pick a victim to remove 346 UpdateTasks(t :> [t EXCEPT !.desired_state = remove]) 347 348 (* We don't model the updater explicitly, but we allow any task to be restarted (perhaps with 349 a different image) at any time, which should cover the behaviours of the restart supervisor. 350 351 TODO: SwarmKit also allows ``start-first'' mode updates where we first get the new task to 352 `running' and then mark the old task for shutdown. Add this to the model. *) 353 RestartTask == 354 /\ UNCHANGED << services, nodes >> 355 /\ CountEvent 356 /\ \E oldT \in tasks : 357 \E newId \in UnusedId(oldT.service, VSlot(oldT)) : 358 /\ Runnable(oldT) \* Victim must be runnable 359 /\ oldT.desired_state \prec shutdown \* and we're not trying to kill it 360 \* Create the new task in the `ready' state (see ReleaseReady below): 361 /\ LET replacement == NewTask(oldT.service, VSlot(oldT), newId, ready) 362 IN tasks' = 363 (tasks \ {oldT}) \union { 364 [oldT EXCEPT !.desired_state = shutdown], 365 replacement 366 } 367 368 (* A task is set to wait at `ready' and the previous task for that slot has now finished. 369 Allow it to proceed to `running'. *) 370 ReleaseReady == 371 /\ UNCHANGED << services, nodes, nEvents >> 372 /\ \E t \in tasks : 373 /\ t.desired_state = ready \* (and not e.g. `remove') 374 /\ State(t) = ready 375 /\ \A other \in TasksOfVSlot(t.service, VSlot(t)) \ {t} : 376 ~Runnable(other) \* All other tasks have finished 377 /\ UpdateTasks(t :> [t EXCEPT !.desired_state = running]) 378 379 (* The user asked to remove a service, and now all its tasks have been cleaned up. *) 380 CleanupService == 381 /\ UNCHANGED << tasks, nodes, nEvents >> 382 /\ \E sid \in DOMAIN services : 383 /\ services[sid].remove = TRUE 384 /\ TasksOf(sid) = {} 385 /\ services' = [ i \in DOMAIN services \ {sid} |-> services[i] ] 386 387 (* Tasks that the orchestrator must always do eventually if it can: *) 388 OrchestratorProgress == 389 \/ CreateSlot 390 \/ ReplaceTask 391 \/ RequestRemoval 392 \/ CleanupTerminated 393 \/ ReleaseReady 394 \/ CleanupService 395 396 (* All actions that the orchestrator can perform *) 397 Orchestrator == 398 \/ OrchestratorProgress 399 \/ RestartTask 400 401 ============================================================================= 402 403 ---------------------------- MODULE Allocator ------------------------------- 404 \* Actions performed the allocator 405 406 (* Pick a `new' task and move it to `pending'. 407 408 The spec says the allocator will ``allocate resources such as network attachments 409 which are necessary for the tasks to run''. However, we don't model any resources here. *) 410 AllocateTask == 411 /\ UNCHANGED << services, nodes, nEvents >> 412 /\ \E t \in tasks : 413 /\ State(t) = new 414 /\ UpdateTasks(t :> [t EXCEPT !.status.state = pending]) 415 416 AllocatorProgress == 417 \/ AllocateTask 418 419 Allocator == 420 \/ AllocatorProgress 421 422 ============================================================================= 423 424 ---------------------------- MODULE Scheduler ------------------------------- 425 426 \* Actions performed by the scheduler 427 428 (* The scheduler assigns a node to a `pending' task and moves it to `assigned' 429 once sufficient resources are available (we don't model resources here). *) 430 Scheduler == 431 /\ UNCHANGED << services, nodes, nEvents >> 432 /\ \E t \in tasks : 433 /\ State(t) = pending 434 /\ LET candidateNodes == IF t.node = unassigned 435 THEN Node \* (all nodes) 436 ELSE { t.node } 437 IN 438 \E node \in candidateNodes : 439 UpdateTasks(t :> [t EXCEPT !.status.state = assigned, 440 !.node = node ]) 441 442 ============================================================================= 443 444 ---------------------------- MODULE Reaper ---------------------------------- 445 446 \* Actions performed by the reaper 447 448 (* Forget about tasks in remove or orphan states. 449 450 Orphaned tasks belong to nodes that we are assuming are lost forever (or have crashed 451 and will come up with nothing running, which is an equally fine outcome). *) 452 Reaper == 453 /\ UNCHANGED << services, nodes, nEvents >> 454 /\ \E t \in tasks : 455 /\ \/ /\ t.desired_state = remove 456 /\ (State(t) \prec assigned \/ ~Runnable(t)) \* Not owned by agent 457 \/ State(t) = orphaned 458 /\ tasks' = tasks \ {t} 459 460 ============================================================================= 461 462 \* The complete system 463 464 \* Import definitions from the various modules 465 INSTANCE User 466 INSTANCE Orchestrator 467 INSTANCE Allocator 468 INSTANCE Scheduler 469 INSTANCE Reaper 470 471 \* All the variables 472 vars == << tasks, services, nodes, nEvents >> 473 474 \* Initially there are no tasks and no services, and all nodes are up. 475 Init == 476 /\ tasks = {} 477 /\ services = << >> 478 /\ nodes = [ n \in Node |-> nodeUp ] 479 /\ InitEvents 480 481 (* WorkerSpec doesn't mention `services'. To combine it with this spec, we need to say 482 that every action of the agent leaves `services' unchanged. *) 483 AgentReal == 484 Agent /\ UNCHANGED services 485 486 (* Unfortunately, `AgentReal' causes TLC to report all problems of the agent 487 as simply `AgentReal' steps, which isn't very helpful. We can get better 488 diagnostics by expanding it, like this: *) 489 AgentTLC == 490 \/ (ProgressTask /\ UNCHANGED services) 491 \/ (ShutdownComplete /\ UNCHANGED services) 492 \/ (OrphanTasks /\ UNCHANGED services) 493 \/ (WorkerUp /\ UNCHANGED services) 494 \/ (RejectTask /\ UNCHANGED services) 495 \/ (ContainerExit /\ UNCHANGED services) 496 \/ (WorkerDown /\ UNCHANGED services) 497 498 (* To avoid the risk of `AgentTLC' getting out of sync, 499 TLAPS can check that the definitions are equivalent. *) 500 THEOREM AgentTLC = AgentReal 501 BY DEF AgentTLC, AgentReal, Agent, AgentProgress 502 503 (* A next step is one in which any of these sub-components takes a step: *) 504 Next == 505 \/ User 506 \/ Orchestrator 507 \/ Allocator 508 \/ Scheduler 509 \/ AgentTLC 510 \/ Reaper 511 \* For model checking: don't report deadlock if we're limiting events 512 \/ (nEvents = maxEvents /\ UNCHANGED vars) 513 514 (* This is a ``temporal formula''. It takes a sequence of states representing the 515 changing state of the world and evaluates to TRUE if that sequences of states is 516 a possible behaviour of SwarmKit. *) 517 Spec == 518 \* The first state in the behaviour must satisfy Init: 519 /\ Init 520 \* All consecutive pairs of states must satisfy Next or leave `vars' unchanged: 521 /\ [][Next]_vars 522 (* Some actions are required to happen eventually. For example, a behaviour in 523 which SwarmKit stops doing anything forever, even though it could advance some task 524 from the `new' state, isn't a valid behaviour of the system. 525 This property is called ``weak fairness''. *) 526 /\ WF_vars(OrchestratorProgress) 527 /\ WF_vars(AllocatorProgress) 528 /\ WF_vars(Scheduler) 529 /\ WF_vars(AgentProgress /\ UNCHANGED services) 530 /\ WF_vars(Reaper) 531 /\ WF_vars(WorkerUp /\ UNCHANGED services) 532 (* We don't require fairness of: 533 - User (we don't control them), 534 - RestartTask (services aren't required to be updated), 535 - RejectTask (tasks aren't required to be rejected), 536 - ContainerExit (we don't specify image behaviour) or 537 - WorkerDown (workers aren't required to fail). *) 538 539 ------------------------------------------------------------------------------- 540 \* Properties to verify 541 542 (* These are properties that should follow automatically if the system behaves as 543 described by `Spec' in the previous section. *) 544 545 \* A state invariant (things that should be true in every state). 546 Inv == 547 \A t \in tasks : 548 (* Every task has a service: 549 550 TODO: The spec says: ``In some cases, there are tasks that exist independent of any service. 551 These do not have a value set in service_id.''. Add an example of one. *) 552 /\ t.service \in DOMAIN services 553 \* Tasks have nodes once they reach `assigned', except maybe if rejected: 554 /\ assigned \preceq State(t) => t.node \in Node \/ State(t) = rejected 555 \* `remove' is only used as a desired state, not an actual one: 556 /\ State(t) # remove 557 \* Task IDs are unique 558 /\ \A t2 \in tasks : Id(t) = Id(t2) => t = t2 559 560 (* The state of task `i' in `S', or `null' if it doesn't exist *) 561 Get(S, i) == 562 LET cand == { x \in S : Id(x) = i } 563 IN IF cand = {} THEN null 564 ELSE State(CHOOSE x \in cand : TRUE) 565 566 (* An action in which all transitions were valid. *) 567 StepTransitionsOK == 568 LET permitted == { << x, x >> : x \in TaskState } \union \* No change is always OK 569 CASE Orchestrator -> Transitions.orchestrator 570 [] Allocator -> Transitions.allocator 571 [] Scheduler -> Transitions.scheduler 572 [] Agent -> Transitions.agent 573 [] Reaper -> Transitions.reaper 574 [] OTHER -> {} 575 oldIds == IdSet(tasks) 576 newIds == IdSet(tasks') 577 IN 578 \A id \in newIds \union oldIds : 579 << Get(tasks, id), Get(tasks', id) >> \in permitted 580 581 (* Some of the expressions below are ``temporal formulas''. Unlike state expressions and actions, 582 these look at a complete behaviour (sequence of states). Summary of notation: 583 584 [] means ``always''. e.g. []x=3 means that `x = 3' in all states. 585 586 <> means ``eventually''. e.g. <>x=3 means that `x = 3' in some state. 587 588 `x=3' on its own means that `x=3' in the initial state. 589 *) 590 591 \* A temporal formula that checks every step satisfies StepTransitionsOK (or `vars' is unchanged) 592 TransitionsOK == 593 [][StepTransitionsOK]_vars 594 595 (* Every service has the right number of running tasks (the system is in the desired state). *) 596 InDesiredState == 597 \A sid \in DOMAIN services : 598 \* We're not trying to remove the service: 599 /\ ~services[sid].remove 600 \* The service has the correct set of running replicas: 601 /\ LET runningTasks == { t \in TasksOf(sid) : State(t) = running } 602 nRunning == Cardinality(runningTasks) 603 IN 604 CASE IsReplicated(sid) -> 605 /\ nRunning = services[sid].replicas 606 [] IsGlobal(sid) -> 607 \* We have as many tasks as nodes: 608 /\ nRunning = Cardinality(Node) 609 \* We have a task for every node: 610 /\ { t.node : t \in runningTasks } = Node 611 \* The service does not have too many terminated tasks 612 /\ \A slot \in VSlotsOf(sid) : 613 LET terminated == { t \in TasksOfVSlot(sid, slot) : ~Runnable(t) } 614 IN Cardinality(terminated) <= maxTerminated 615 616 (* The main property we want to check. 617 618 []<> means ``always eventually'' (``infinitely-often'') 619 620 <>[] means ``eventually always'' (always true after some point) 621 622 This temporal formula says that if we only experience a finite number of 623 problems then the system will eventually settle on InDesiredState. 624 *) 625 EventuallyAsDesired == 626 \/ []<> <<User>>_vars \* Either the user keeps changing the configuration, 627 \/ []<> <<RestartTask>>_vars \* or restarting/updating tasks, 628 \/ []<> <<WorkerDown>>_vars \* or workers keep failing, 629 \/ []<> <<RejectTask>>_vars \* or workers keep rejecting tasks, 630 \/ []<> <<ContainerExit>>_vars \* or the containers keep exiting, 631 \/ <>[] InDesiredState \* or we eventually get to the desired state and stay there. 632 633 =============================================================================