ghostworking.py 4.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106
  1. """The ghostworking bundle (goal 28): the single-saddle ghost 'working' row.
  2. A saddle reboot can kill a round without any transition push; on a
  3. single-saddle fleet the recovery passes that COULD clear the row never
  4. fire — requeue_tasks_on_offline_saddles needs the saddle row offline
  5. (the rebooted saddle reads online while running something else), and
  6. requeue_interrupted_sessions rides an /next-task ask (the one saddle
  7. is busy running another session, so no ask ever comes). The dispatcher
  8. tick's ghost-working sweep requeues a stale open session on an
  9. assigned/working task after STALE_WORKING_S without either crutch.
  10. """
  11. from harness import * # noqa: F401,F403 — the shared wire helpers
  12. # TaskService.STALE_WORKING_S is 20 minutes; the sweep reads it from
  13. # SADDLE_STALE_WORKING_S so the test can compress the window.
  14. STALE_ENV = {"SADDLE_STALE_WORKING_S": "6"}
  15. def run(ctx):
  16. auth, fake, fake_id, fake_token = ctx.auth, ctx.fake, ctx.fake_id, ctx.fake_token
  17. print("== goal 28: a ghost 'working' row requeues from the dispatcher tick ==")
  18. saddles_state = slot_public(auth, "/saddles", "saddles")
  19. act(auth, saddles_state["create_capability"], {"name": "ghostwake"})
  20. cap_id = int([c["id"] for c in slot_public(auth, "/saddles", "saddles")["capabilities"]
  21. if c["name"] == "ghostwake"][0])
  22. # The ghost: a daemon that claims a task, pushes working, then
  23. # DIES silently (no further pushes, no transition — a reboot).
  24. ghost = FakeSaddle()
  25. ghost.ask_on_register = False
  26. ghost.hold_state = True
  27. ghost.serve()
  28. act(auth, slot_public(auth, "/saddles", "saddles")["register"],
  29. {"name": "ghost-daemon", "host": "127.0.0.1:%d" % ghost.port})
  30. act(auth, saddle_card(auth, "ghost-daemon")["attach_capability"],
  31. {"capability_id": str(cap_id)})
  32. task_id = create_queued(auth, {
  33. "project_id": "1", "title": "Ghost: dies without a transition",
  34. "priority": "2", "capability_id": [str(cap_id)]})
  35. ask_status, assignment = ghost.ask_now()
  36. check(ask_status == 200 and int(assignment["task"]["id"]) == task_id,
  37. "the ask claims the queued task")
  38. ghost.state = "working"
  39. check(ghost.push_results("working") == 200, "the working push opens the round")
  40. conn = db()
  41. row = conn.execute(
  42. "select status from tasks where id = ?", (task_id,)).fetchone()
  43. check(row is not None and row[0] == "working", "the task row reads working")
  44. # The LIVE peer on another saddle: pushes keep coming, so the sweep
  45. # must never touch it (the seeded clock + fresh pushes protect it).
  46. live = FakeSaddle()
  47. live.ask_on_register = False
  48. live.hold_state = True
  49. live.serve()
  50. act(auth, slot_public(auth, "/saddles", "saddles")["register"],
  51. {"name": "live-daemon", "host": "127.0.0.1:%d" % live.port})
  52. act(auth, saddle_card(auth, "live-daemon")["attach_capability"],
  53. {"capability_id": str(cap_id)})
  54. live_task = create_queued(auth, {
  55. "project_id": "1", "title": "Live: keeps pushing", "priority": "1",
  56. "capability_id": [str(cap_id)]})
  57. ask_status, live_assignment = live.ask_now()
  58. check(ask_status == 200 and int(live_assignment["task"]["id"]) == live_task,
  59. "the live peer claims its task")
  60. live.state = "working"
  61. check(live.push_results("working") == 200, "the live round pushes working")
  62. # Silence the ghost: no pushes for the (compressed) stale window
  63. # while the live daemon keeps pushing. The sweep must requeue ONLY
  64. # the ghost's row, and the requeued task must be claimable again.
  65. deadline = time.time() + 40
  66. requeued = False
  67. while time.time() < deadline:
  68. live.state = "working"
  69. live.push_results("working")
  70. row = conn.execute(
  71. "select status from tasks where id = ?", (task_id,)).fetchone()
  72. if row is not None and row[0] == "queued":
  73. requeued = True
  74. break
  75. time.sleep(2.0)
  76. check(requeued,
  77. "the ghost 'working' row requeues once its silence passes the stale window")
  78. row = conn.execute(
  79. "select status from tasks where id = ?", (live_task,)).fetchone()
  80. check(row is not None and row[0] == "working",
  81. "the live, still-pushing round is NOT touched by the sweep")
  82. # The requeued task is claimable again — the fleet is unwedged.
  83. claim_status, reclaim = ghost.ask_now()
  84. check(claim_status == 200 and int(reclaim["task"]["id"]) == task_id,
  85. "the requeued ghost task is claimable again")
  86. ghost.state = "done"
  87. ghost.push_results("done", summary="# done")
  88. live.state = "done"
  89. live.push_results("done", summary="# done")
  90. wait_for(lambda: task_state(auth, task_id).get("status") == "done"
  91. or False, timeout=30, what="ghost task done")
  92. wait_for(lambda: task_state(auth, live_task).get("status") == "done"
  93. or False, timeout=30, what="live task done")