| 1 | package engines |
| 2 | |
| 3 | import ( |
| 4 | "fmt" |
| 5 | "sort" |
| 6 | "strings" |
| 7 | ) |
| 8 | |
| 9 | // Classification, minted groups and the operator clear (spec §5.1 "The |
| 10 | // column", "Platform-minted rows", "Becoming public is never automatic", |
| 11 | // "Classification before serving"; §6.3). |
| 12 | |
| 13 | // ---------- minted groups ---------- |
| 14 | |
| 15 | // followsOwner: the row is a platform-minted row that takes its execution |
| 16 | // owner's mark and barrier. Only minted instance rows do: a minted static or |
| 17 | // service row is 0 by construction (static-minted-public). |
| 18 | func followsOwner(r *row) bool { |
| 19 | if !r.Platform || !on("minted-follow-owner") { |
| 20 | return false |
| 21 | } |
| 22 | return r.Kind == "instance" || (r.Kind == "static" && !on("static-minted-public")) |
| 23 | } |
| 24 | |
| 25 | // resolveOwner is the edge's (and the agent's) owner for a minted row, under |
| 26 | // the rules in force. |
| 27 | func (b *box) resolveOwner(r *row) (string, bool) { |
| 28 | return b.resolveOwnerWith(r, on("owner-nonempty-unique")) |
| 29 | } |
| 30 | |
| 31 | // resolveOwnerWith: the recorded owner, if its row exists; else the |
| 32 | // non-platform instance row with the same account and app and the same |
| 33 | // backend. strict is the spec's fallback: empty values never match, and a |
| 34 | // match that is not unique counts as no match. Without it, empty values |
| 35 | // match and the first match wins. |
| 36 | func (b *box) resolveOwnerWith(r *row, strict bool) (string, bool) { |
| 37 | if r.Owner != "" { |
| 38 | if o := b.Rows[r.Owner]; o != nil && !o.Platform { |
| 39 | return r.Owner, true |
| 40 | } |
| 41 | return "", false |
| 42 | } |
| 43 | var found []string |
| 44 | for _, h := range hostsOf(b) { |
| 45 | c := b.Rows[h] |
| 46 | if c.Platform || c.Kind != "instance" || c.App != r.App { |
| 47 | continue |
| 48 | } |
| 49 | cs, ca, rs, ra := socketOf(c), addrOf(c), socketOf(r), addrOf(r) |
| 50 | if strict && ((cs != "" && cs == rs) || (ca != "" && ca == ra)) || |
| 51 | !strict && (cs == rs || ca == ra) { |
| 52 | found = append(found, h) |
| 53 | } |
| 54 | } |
| 55 | if len(found) == 1 || (!strict && len(found) > 0) { |
| 56 | return found[0], true |
| 57 | } |
| 58 | return "", false |
| 59 | } |
| 60 | |
| 61 | // socketOf and addrOf split a backend into the registry's Socket and Addr. |
| 62 | func socketOf(r *row) string { |
| 63 | if strings.HasPrefix(r.Backend, "/") { |
| 64 | return r.Backend |
| 65 | } |
| 66 | return "" |
| 67 | } |
| 68 | |
| 69 | func addrOf(r *row) string { |
| 70 | if r.Backend != "" && !strings.HasPrefix(r.Backend, "/") { |
| 71 | return r.Backend |
| 72 | } |
| 73 | return "" |
| 74 | } |
| 75 | |
| 76 | func hostsOf(b *box) []string { |
| 77 | hosts := make([]string, 0, len(b.Rows)) |
| 78 | for h := range b.Rows { |
| 79 | hosts = append(hosts, h) |
| 80 | } |
| 81 | sort.Strings(hosts) |
| 82 | return hosts |
| 83 | } |
| 84 | |
| 85 | // truthGroup is the group a row really serves: a minted row's source, else |
| 86 | // its own host. The oracle reads it; the protocol never does. |
| 87 | func truthGroup(b *box, host string) string { |
| 88 | if r := b.Rows[host]; r != nil && r.Platform && r.Source != "" { |
| 89 | return r.Source |
| 90 | } |
| 91 | return host |
| 92 | } |
| 93 | |
| 94 | // mint is the domains reconciler minting a platform row from src: it copies |
| 95 | // the source's kind, backend and app (no version), and, being a row of the |
| 96 | // same group, the group's mark. recordOwner is whether it writes |
| 97 | // routes.owner (rows minted before the column was added have none until the |
| 98 | // reconciler's next pass). |
| 99 | func (w *world) mint(b *box, src, host string, recordOwner bool) { |
| 100 | s := b.Rows[src] |
| 101 | m := w.groupMark(b, w.group(b, src)) |
| 102 | r := &row{Host: host, Kind: s.Kind, App: s.App, Backend: s.Backend, Platform: true, Source: src, PathTo: map[string]string{}} |
| 103 | if recordOwner { |
| 104 | r.Owner = src |
| 105 | } |
| 106 | b.Rows[host] = r |
| 107 | b.Mark[host] = m |
| 108 | b.Wrote++ |
| 109 | w.logf("box%d: registry change %d: minted %s %s from %s (owner %q, mark %v)", b.ID, b.Wrote, r.Kind, host, src, r.Owner, m) |
| 110 | } |
| 111 | |
| 112 | // reconcileOwners is the reconciler's backfill pass: it records the owner of |
| 113 | // every minted instance row that has none. The row takes its owner group's |
| 114 | // mark (one group, one mark). It reports whether it recorded any. |
| 115 | func (w *world) reconcileOwners(b *box) bool { |
| 116 | did := false |
| 117 | for _, h := range hostsOf(b) { |
| 118 | r := b.Rows[h] |
| 119 | if !r.Platform || r.Kind != "instance" || r.Owner != "" || b.Rows[r.Source] == nil { |
| 120 | continue |
| 121 | } |
| 122 | m := w.groupMark(b, w.group(b, r.Source)) |
| 123 | r.Owner = r.Source |
| 124 | b.Mark[h] = m |
| 125 | b.Wrote++ |
| 126 | did = true |
| 127 | w.logf("box%d: registry change %d: owner of %s recorded as %s (mark %v)", b.ID, b.Wrote, h, r.Owner, m) |
| 128 | } |
| 129 | return did |
| 130 | } |
| 131 | |
| 132 | // provision creates instance route host (a new route: NULL, unclassified), |
| 133 | // then its first adoption classifies it before it is served: a declaring |
| 134 | // build through the gate, a readable plain one as 0 (spec §5.1, "A route the |
| 135 | // agent creates later ... is classified by its first adoption"). |
| 136 | func (w *world) provision(b *box, host, app, backend string, ver int) bool { |
| 137 | b.Rows[host] = &row{Host: host, Kind: "instance", App: app, Backend: backend, PathTo: map[string]string{}} |
| 138 | b.Wrote++ |
| 139 | w.logf("box%d: registry change %d: provisioned %s (%s, %s), NULL until its first adoption", b.ID, b.Wrote, host, app, backend) |
| 140 | ok := w.adopt(b, host, ver, nil) |
| 141 | if w.groupMark(b, host) == markNull { |
| 142 | if m := w.classify(b, host); m != markNull { |
| 143 | w.writeMark(b, host, m) |
| 144 | } |
| 145 | } |
| 146 | return ok |
| 147 | } |
| 148 | |
| 149 | // addStaticApp creates static app name with its minted platform row and a |
| 150 | // static canary on its own channel (domains/reconcile.go:368). |
| 151 | func (w *world) addStaticApp(b *box, name string) (minted, canary string) { |
| 152 | b.Rows[name] = &row{Host: name, Kind: "static", App: "acct/" + name, PathTo: map[string]string{}} |
| 153 | b.Mark[name] = markPublic // 0 by construction |
| 154 | b.Wrote++ |
| 155 | w.logf("box%d: registry change %d: static app %s", b.ID, b.Wrote, name) |
| 156 | minted, canary = name+".acct.apps.example", "canary-"+name+".acct.apps.example" |
| 157 | w.mint(b, name, minted, true) |
| 158 | w.mint(b, name, canary, true) |
| 159 | return minted, canary |
| 160 | } |
| 161 | |
| 162 | // ---------- grants ---------- |
| 163 | |
| 164 | // groupHasGrant: a row of group g holds the doc-engine grant. With |
| 165 | // grant-marks-group, any row of the group; without it, only the owner's own |
| 166 | // row (a grant on a minted row is ignored, R6-1). |
| 167 | func (w *world) groupHasGrant(b *box, g string) bool { |
| 168 | if !on("grant-marks-group") { |
| 169 | r := b.Rows[g] |
| 170 | return r != nil && r.Grant |
| 171 | } |
| 172 | for _, h := range w.groupHosts(b, g) { |
| 173 | if b.Rows[h].Grant { |
| 174 | return true |
| 175 | } |
| 176 | } |
| 177 | return false |
| 178 | } |
| 179 | |
| 180 | // grantLoaded is the oracle's grant: a row whose true group is g holds the |
| 181 | // grant and the edge has loaded the change that recorded it. It returns that |
| 182 | // row, or "". |
| 183 | func (w *world) grantLoaded(b *box, g string) string { |
| 184 | for _, h := range hostsOf(b) { |
| 185 | r := b.Rows[h] |
| 186 | if r.Grant && truthGroup(b, h) == g && b.Fenced >= r.GrantAt { |
| 187 | return h |
| 188 | } |
| 189 | } |
| 190 | return "" |
| 191 | } |
| 192 | |
| 193 | // grant is `carlos route --host <host> --grant doc-engine`: it records the |
| 194 | // grant, writes 1 on the group if the group now holds it, and returns once |
| 195 | // the edge has loaded the change (or says it timed out). On a minted row |
| 196 | // whose owner cannot be resolved it is refused: the CLI cannot find the |
| 197 | // group to mark, and the row itself stays NULL and refused. |
| 198 | func (w *world) grant(b *box, host string) bool { |
| 199 | r := b.Rows[host] |
| 200 | if r == nil || r.Kind != "instance" || r.Grant { |
| 201 | return false |
| 202 | } |
| 203 | if followsOwner(r) { |
| 204 | if _, ok := b.resolveOwner(r); !ok { |
| 205 | w.logf("box%d: --grant doc-engine on %s: refused: the row's owner is not recorded and cannot be found uniquely; the row stays NULL and refused", b.ID, host) |
| 206 | return false |
| 207 | } |
| 208 | } |
| 209 | b.Wrote++ |
| 210 | r.Grant, r.GrantAt = true, b.Wrote |
| 211 | g := w.group(b, host) |
| 212 | w.logf("box%d: registry change %d: --grant doc-engine on %s (group %s)", b.ID, b.Wrote, host, g) |
| 213 | if w.groupHasGrant(b, g) && w.groupMark(b, g) != markInternal { |
| 214 | w.writeMark(b, g, markInternal) |
| 215 | } |
| 216 | want := b.Wrote |
| 217 | for i := 0; i < fenceWaitTicks && b.Fenced < want; i++ { |
| 218 | w.reload(b) |
| 219 | } |
| 220 | if b.Fenced < want { |
| 221 | w.logf("box%d: --grant on %s: the edge has not loaded it; timed out", b.ID, host) |
| 222 | return false |
| 223 | } |
| 224 | return true |
| 225 | } |
| 226 | |
| 227 | // revoke is --revoke doc-engine on any row of a group: the grant goes and |
| 228 | // the group's mark stays (becoming public is never automatic; only --public |
| 229 | // writes 0). Like --grant it runs the barrier for the group: it returns once |
| 230 | // the edge has loaded the change, so the group's mark is fenced, or says it |
| 231 | // timed out. Refused on a minted row whose owner cannot be resolved. |
| 232 | func (w *world) revoke(b *box, host string) bool { |
| 233 | r := b.Rows[host] |
| 234 | if r == nil || !r.Grant { |
| 235 | return false |
| 236 | } |
| 237 | if followsOwner(r) { |
| 238 | if _, ok := b.resolveOwner(r); !ok { |
| 239 | w.logf("box%d: --revoke doc-engine on %s: refused: the row's owner cannot be resolved", b.ID, host) |
| 240 | return false |
| 241 | } |
| 242 | } |
| 243 | g := w.group(b, host) |
| 244 | b.Wrote++ |
| 245 | r.Grant = false |
| 246 | want := b.Wrote |
| 247 | w.logf("box%d: registry change %d: --revoke doc-engine on %s (group %s); the mark stays %v", b.ID, b.Wrote, host, g, w.groupMark(b, g)) |
| 248 | for i := 0; i < fenceWaitTicks && b.Fenced < want; i++ { |
| 249 | w.reload(b) |
| 250 | } |
| 251 | if b.Fenced < want { |
| 252 | w.logf("box%d: --revoke on %s: the edge has not loaded it; timed out", b.ID, host) |
| 253 | return false |
| 254 | } |
| 255 | return true |
| 256 | } |
| 257 | |
| 258 | // ---------- classification before serving ---------- |
| 259 | |
| 260 | // classify is the upgrade's classification of group g: 1 if a row of the |
| 261 | // group holds the grant, the live manifest declares, or the manifest of any |
| 262 | // other version of the app staged on the box declares, whichever instance of |
| 263 | // the app staged it (classify-staged); 0 if the live manifest is readable and |
| 264 | // none of those holds; else NULL. |
| 265 | func (w *world) classify(b *box, g string) mark { |
| 266 | if w.groupHasGrant(b, g) { |
| 267 | return markInternal |
| 268 | } |
| 269 | live, liveOK := w.builds[b.Live[g]] |
| 270 | liveOK = liveOK && b.Live[g] != 0 && live.ManifestOK |
| 271 | if liveOK && live.Declares { |
| 272 | return markInternal |
| 273 | } |
| 274 | if on("classify-staged") { |
| 275 | for _, v := range w.stagedForApp(b, g) { |
| 276 | if bd := w.builds[v]; bd.ManifestOK && bd.Declares { |
| 277 | return markInternal |
| 278 | } |
| 279 | } |
| 280 | } |
| 281 | if liveOK { |
| 282 | return markPublic |
| 283 | } |
| 284 | return markNull |
| 285 | } |
| 286 | |
| 287 | // classifiedGroups is every group with a row the classification covers: an |
| 288 | // instance row, or a row that follows an owner. |
| 289 | func (w *world) classifiedGroups(b *box) []string { |
| 290 | seen := map[string]bool{} |
| 291 | var out []string |
| 292 | for _, h := range hostsOf(b) { |
| 293 | r := b.Rows[h] |
| 294 | if r.Kind != "instance" && !followsOwner(r) { |
| 295 | continue |
| 296 | } |
| 297 | if g := w.group(b, h); !seen[g] { |
| 298 | seen[g] = true |
| 299 | out = append(out, g) |
| 300 | } |
| 301 | } |
| 302 | return out |
| 303 | } |
| 304 | |
| 305 | // upgrade is the agent's first start on the build that adds routes.internal. |
| 306 | // The column arrives NULL on every instance route; before any edge listener |
| 307 | // opens, the agent classifies every NULL group. The edge then starts on the |
| 308 | // classified map: in-flight requests did not survive the restart, and the |
| 309 | // fence covers everything written. Running processes are untouched. |
| 310 | func (w *world) upgrade(b *box) { |
| 311 | if w.unwrittenManifestAcrossUpgrade { |
| 312 | if why, procs := w.upgradeResidual(b); why != "" { |
| 313 | for _, p := range procs { |
| 314 | p.Residual = "unwrittenManifestAcrossUpgrade" |
| 315 | } |
| 316 | w.logf("box%d: upgrade: the stated residual (%s); its processes' breaches are counted as residual", b.ID, why) |
| 317 | } |
| 318 | } |
| 319 | gs := w.classifiedGroups(b) |
| 320 | w.logf("box%d: upgrade: routes.internal arrives NULL on %v", b.ID, gs) |
| 321 | for _, g := range gs { |
| 322 | w.writeMark(b, g, markNull) |
| 323 | } |
| 324 | for _, g := range gs { |
| 325 | if m := w.classify(b, g); m != markNull { |
| 326 | w.writeMark(b, g, m) |
| 327 | } else { |
| 328 | w.logf("box%d: upgrade: %s stays NULL (live manifest unreadable): refused publicly, named every pass", b.ID, g) |
| 329 | } |
| 330 | } |
| 331 | b.loadEdge() |
| 332 | b.Epoch++ |
| 333 | if len(b.Admitted) > 0 { |
| 334 | w.logf("box%d: upgrade: %d in-flight requests ended with the restart", b.ID, len(b.Admitted)) |
| 335 | } |
| 336 | b.Admitted = nil |
| 337 | b.Fenced = b.Wrote |
| 338 | // the restart loaded every group's mark and drained the whole box: a |
| 339 | // fence for every closed group, NULL included |
| 340 | for _, g := range gs { |
| 341 | if w.closed(b, g) && !w.recorded(b, g) { |
| 342 | b.SetAt[g] = b.Wrote |
| 343 | } |
| 344 | } |
| 345 | w.logf("box%d: upgrade: listeners open on the classified map, epoch %d, fenced %d", b.ID, b.Epoch, b.Fenced) |
| 346 | } |
| 347 | |
| 348 | // residualAtUpgrade names the spec's stated residual if an upgrade of b now |
| 349 | // would produce it: a running declaring build whose manifest cannot be read, |
| 350 | // in a group the (correct) classification would mark 0 because the live |
| 351 | // manifest reads and does not declare. The default world does not generate |
| 352 | // it; Task 5's flag (unwrittenManifestAcrossUpgrade) does. |
| 353 | func (w *world) residualAtUpgrade(b *box) string { |
| 354 | why, _ := w.upgradeResidual(b) |
| 355 | return why |
| 356 | } |
| 357 | |
| 358 | // upgradeResidual is residualAtUpgrade with the processes it names: every |
| 359 | // running unreadable declaring process of each group it applies to. |
| 360 | func (w *world) upgradeResidual(b *box) (string, []*proc) { |
| 361 | first := "" |
| 362 | var procs []*proc |
| 363 | for _, g := range w.groups(b) { |
| 364 | var unreadable *proc |
| 365 | var group []*proc |
| 366 | for _, p := range b.Procs { |
| 367 | if bd := w.builds[p.Ver]; p.Group == g && bd.Declares && !bd.ManifestOK { |
| 368 | unreadable = p |
| 369 | group = append(group, p) |
| 370 | } |
| 371 | } |
| 372 | if unreadable == nil { |
| 373 | continue |
| 374 | } |
| 375 | granted := false |
| 376 | for _, h := range hostsOf(b) { |
| 377 | granted = granted || (b.Rows[h].Grant && truthGroup(b, h) == g) |
| 378 | } |
| 379 | live, ok := w.builds[b.Live[g]] |
| 380 | if granted || b.Live[g] == 0 || !ok || !live.ManifestOK || live.Declares { |
| 381 | continue |
| 382 | } |
| 383 | staged := false |
| 384 | for _, v := range w.stagedForApp(b, g) { |
| 385 | if bd := w.builds[v]; bd.ManifestOK && bd.Declares { |
| 386 | staged = true |
| 387 | } |
| 388 | } |
| 389 | if !staged { |
| 390 | if first == "" { |
| 391 | first = fmt.Sprintf("%s runs v%d (declares, manifest unreadable) while live v%d reads as plain", g, unreadable.Ver, b.Live[g]) |
| 392 | } |
| 393 | procs = append(procs, group...) |
| 394 | } |
| 395 | } |
| 396 | return first, procs |
| 397 | } |
| 398 | |
| 399 | // ---------- the operator clear ---------- |
| 400 | |
| 401 | // operatorPublic is `carlos route --host <host> --public` on group g (spec |
| 402 | // §5.1, "Becoming public is never automatic"). Holding the group's lock for |
| 403 | // its whole run (adoption's repoint honours it, public-holds-lock; so does |
| 404 | // every launch through the gate, clear-launch-lock): |
| 405 | // 1. refuse unless the live manifest is readable and does not declare |
| 406 | // (public-checks-live), and no row of the group holds the grant; |
| 407 | // 2. stop the whole group (public-stops-group); |
| 408 | // 3. refuse unless every cgroup of the group is empty (public-confirms-empty); |
| 409 | // 4. write 0 and release the lock. |
| 410 | // |
| 411 | // Step 3 is what catches a stop that did not complete (w.clearStopMisses). |
| 412 | // |
| 413 | // w.clearHook, if set, runs after step 1 ("locked"), after step 2 |
| 414 | // ("stopped") and after step 3 ("confirmed"). |
| 415 | func (w *world) operatorPublic(b *box, g string) bool { |
| 416 | w.logf("box%d: --public %s: taking the group's lock", b.ID, g) |
| 417 | b.Locked[g] = true |
| 418 | defer delete(b.Locked, g) |
| 419 | if on("public-checks-live") { |
| 420 | live, ok := w.builds[b.Live[g]] |
| 421 | if b.Live[g] == 0 || !ok || !live.ManifestOK || live.Declares { |
| 422 | w.logf("box%d: --public %s: refused: the live release v%d is unreadable or declares doc_engine", b.ID, g, b.Live[g]) |
| 423 | return false |
| 424 | } |
| 425 | } |
| 426 | if w.groupHasGrant(b, g) { |
| 427 | w.logf("box%d: --public %s: refused: a row of the group holds the doc-engine grant", b.ID, g) |
| 428 | return false |
| 429 | } |
| 430 | w.hook(b, g, "locked") |
| 431 | if on("public-stops-group") { |
| 432 | w.clearStop(b, g) |
| 433 | } |
| 434 | w.hook(b, g, "stopped") |
| 435 | if on("public-confirms-empty") && w.running(b, g) { |
| 436 | w.logf("box%d: --public %s: refused: a cgroup of the group is not empty", b.ID, g) |
| 437 | return false |
| 438 | } |
| 439 | w.hook(b, g, "confirmed") |
| 440 | w.writeMark(b, g, markPublic) |
| 441 | w.logf("box%d: --public %s: 0 written; the lock is released", b.ID, g) |
| 442 | return true |
| 443 | } |
| 444 | |
| 445 | // clearStop is step 2: stop every process of group g, except, when |
| 446 | // w.clearStopMisses is set, the first one, whose stop did not complete. |
| 447 | func (w *world) clearStop(b *box, g string) { |
| 448 | var left *proc |
| 449 | if w.clearStopMisses { |
| 450 | for _, p := range b.Procs { |
| 451 | if p.Group == g { |
| 452 | left = p |
| 453 | break |
| 454 | } |
| 455 | } |
| 456 | } |
| 457 | w.stopGroup(b, g) |
| 458 | if left != nil { |
| 459 | b.Procs = append(b.Procs, left) |
| 460 | w.logf("box%d: --public %s: the stop did not complete: process %d (v%d) still runs", b.ID, g, left.ID, left.Ver) |
| 461 | return |
| 462 | } |
| 463 | w.logf("box%d: --public %s: the group is stopped", b.ID, g) |
| 464 | } |
| 465 | |
| 466 | func (w *world) hook(b *box, g, stage string) { |
| 467 | if w.clearHook != nil { |
| 468 | w.clearHook(b, g, stage) |
| 469 | } |
| 470 | } |
| 471 | |
| 472 | // ---------- the random walk ---------- |
| 473 | |
| 474 | // pickInstanceRow is any instance row of b, minted included, or "". |
| 475 | func (w *world) pickInstanceRow(b *box) string { |
| 476 | var hs []string |
| 477 | for _, h := range hostsOf(b) { |
| 478 | if b.Rows[h].Kind == "instance" { |
| 479 | hs = append(hs, h) |
| 480 | } |
| 481 | } |
| 482 | if len(hs) == 0 { |
| 483 | return "" |
| 484 | } |
| 485 | return hs[w.rng.Intn(len(hs))] |
| 486 | } |
| 487 | |
| 488 | // maxRows bounds a box's table in the walk. |
| 489 | const maxRows = 24 |
| 490 | |
| 491 | func init() { |
| 492 | actions = append(actions, |
| 493 | action{name: "upgrade", weight: 1, run: func(w *world) bool { |
| 494 | b := w.pickBox() |
| 495 | if why := w.residualAtUpgrade(b); why != "" && !w.unwrittenManifestAcrossUpgrade { |
| 496 | w.logf("box%d: upgrade not generated: the stated residual (%s)", b.ID, why) |
| 497 | return false |
| 498 | } |
| 499 | w.upgrade(b) |
| 500 | return true |
| 501 | }}, |
| 502 | action{name: "grant", weight: 1, run: func(w *world) bool { |
| 503 | b := w.pickBox() |
| 504 | h := w.pickInstanceRow(b) |
| 505 | if h == "" || b.Rows[h].Grant { |
| 506 | return false |
| 507 | } |
| 508 | w.grant(b, h) |
| 509 | return true |
| 510 | }}, |
| 511 | action{name: "revoke", weight: 1, run: func(w *world) bool { |
| 512 | b := w.pickBox() |
| 513 | h := w.pickInstanceRow(b) |
| 514 | if h == "" || !b.Rows[h].Grant { |
| 515 | return false |
| 516 | } |
| 517 | w.revoke(b, h) |
| 518 | return true |
| 519 | }}, |
| 520 | action{name: "operator-public", weight: 2, run: func(w *world) bool { |
| 521 | b := w.pickBox() |
| 522 | g := w.pickGroup(b) |
| 523 | if g == "" { |
| 524 | return false |
| 525 | } |
| 526 | switch w.rng.Intn(5) { |
| 527 | case 1: // an adoption tries to run during the clear |
| 528 | w.clearHook = func(hb *box, hg, stage string) { |
| 529 | if stage == "locked" { |
| 530 | w.adopt(hb, hg, w.pickVer(), nil) |
| 531 | } |
| 532 | } |
| 533 | case 2: // an awake repoint tries to run during the clear |
| 534 | w.clearHook = func(hb *box, hg, stage string) { |
| 535 | if stage == "locked" { |
| 536 | w.repointAwake(hb, hg, w.pickVer()) |
| 537 | } |
| 538 | } |
| 539 | case 3: // a swap's incoming generation races the stop |
| 540 | w.clearHook = func(hb *box, hg, stage string) { |
| 541 | if stage == "stopped" { |
| 542 | w.launch(hb, hg, w.pickVer(), "swap") |
| 543 | } |
| 544 | } |
| 545 | case 4: // a launch races the write of 0, after the empty check |
| 546 | w.clearHook = func(hb *box, hg, stage string) { |
| 547 | if stage == "confirmed" { |
| 548 | w.launch(hb, hg, w.pickVer(), []string{"cmd", "recovery", "seed"}[w.rng.Intn(3)]) |
| 549 | } |
| 550 | } |
| 551 | } |
| 552 | w.clearStopMisses = w.rng.Intn(4) == 0 |
| 553 | w.operatorPublic(b, g) |
| 554 | w.clearHook, w.clearStopMisses = nil, false |
| 555 | return true |
| 556 | }}, |
| 557 | action{name: "adopt-awake", weight: 2, run: func(w *world) bool { |
| 558 | b := w.pickBox() |
| 559 | g := w.pickGroup(b) |
| 560 | if g == "" { |
| 561 | return false |
| 562 | } |
| 563 | w.repointAwake(b, g, w.pickVer()) |
| 564 | return true |
| 565 | }}, |
| 566 | action{name: "add-tcp-instance", weight: 1, run: func(w *world) bool { |
| 567 | b := w.pickBox() |
| 568 | if len(b.Rows) >= maxRows { |
| 569 | return false |
| 570 | } |
| 571 | w.nextID++ |
| 572 | host := fmt.Sprintf("t%d", w.nextID) |
| 573 | app := []string{"acct/shop", "acct/tcp"}[w.rng.Intn(2)] |
| 574 | addr := fmt.Sprintf("10.0.%d.1:%d", b.ID, 9100+w.nextID) // unique: no ambiguous owner in the walk |
| 575 | w.provision(b, host, app, addr, w.pickVer()) |
| 576 | w.mint(b, host, host+".acct.apps.example", w.rng.Intn(2) == 0) |
| 577 | return true |
| 578 | }}, |
| 579 | action{name: "add-static-app", weight: 1, run: func(w *world) bool { |
| 580 | b := w.pickBox() |
| 581 | if len(b.Rows) >= maxRows { |
| 582 | return false |
| 583 | } |
| 584 | w.nextID++ |
| 585 | w.addStaticApp(b, fmt.Sprintf("s%d", w.nextID)) |
| 586 | return true |
| 587 | }}, |
| 588 | action{name: "reconcile-owners", weight: 1, run: func(w *world) bool { |
| 589 | return w.reconcileOwners(w.pickBox()) |
| 590 | }}, |
| 591 | ) |
| 592 | } |
| 593 | |