Explorar el Código

fix: refresh maps parked daemon states (blocked/suspended) to logically-free saddles — only 'working' maps to busy, reconciled against the web's open-session records instead of corrupting busy bookkeeping

clanker hace 1 mes
padre
commit
40b7ad1713
Se han modificado 2 ficheros con 67 adiciones y 1 borrados
  1. 38 1
      src/Services/SaddleService.vala
  2. 29 0
      test/web_stack_test.py

+ 38 - 1
src/Services/SaddleService.vala

@@ -863,14 +863,51 @@ namespace ClankerSaddle.Services {
             try {
                 var status = yield request_saddle_json(((!)saddle).host, "GET", "/status", null);
                 string state = status.has_member("state") ? status.get_string_member_with_default("state", "") : "";
-                ((!)saddle).status = state == "idle" ? "idle" : "busy";
+                if (state == "working") {
+                    // Reconcile with the web's own records: busy only when
+                    // a live session truly holds this saddle.
+                    var session = yield open_session_on_saddle(id);
+                    ((!)saddle).status = "busy";
+                    ((!)saddle).current_task_id = session != null ? ((!)session).task_id : 0;
+                    ((!)saddle).allocated_at = session != null ? ((!)session).started_at : null;
+                } else {
+                    // Idle AND the parked states (blocked/suspended, plus
+                    // terminal and unknown states): the workspace is
+                    // parked, so the saddle is logically free — marking it
+                    // busy here used to wedge assignment against a daemon
+                    // that would never poll.
+                    ((!)saddle).status = "idle";
+                    ((!)saddle).current_task_id = 0;
+                    ((!)saddle).allocated_at = null;
+                }
             } catch (Error e) {
                 ((!)saddle).status = "offline";
+                ((!)saddle).current_task_id = 0;
+                ((!)saddle).allocated_at = null;
             }
             yield db.update_async<Saddle>((!)saddle);
             yield pulse.bump(0, "saddle status refreshed");
         }
 
+        /**
+         * The saddle's open (live or parked) session, or null — the
+         * reconciliation source for whether a daemon is genuinely
+         * running one of our tasks.
+         */
+        private async ClankerSaddle.Data.Session? open_session_on_saddle(int64 saddle_id) throws Error {
+            var sessions = yield db.query<ClankerSaddle.Data.Session>()
+                .where(expr("saddle_id == $0", elem<int64?>(saddle_id)))
+                .order_by(expr("id"))
+                .materialise_async();
+            ClankerSaddle.Data.Session? newest_open = null;
+            foreach (var session in sessions) {
+                if (session.ended_at == null) {
+                    newest_open = session;
+                }
+            }
+            return newest_open;
+        }
+
         /**
          * Answered blocks of the saddle's current task not yet handed over
          * the poll endpoint; marks them delivered.

+ 29 - 0
test/web_stack_test.py

@@ -1811,6 +1811,35 @@ def main():
         check("tick exceeded" not in app_log or "Dispatch tick failed" in app_log,
               "a hung request never freezes the tick latch")
 
+
+        print("== R6 F2: refresh must not corrupt parked bookkeeping ==")
+        fake.suspend_after_start = True
+        act(auth, slot_public(auth, "/tasks", "tasks")["create"], {
+            "project_id": "1",
+            "title": "Parked refresh parent",
+            "priority": "2",
+        })
+        parked = int([t["id"] for t in slot_public(auth, "/tasks", "tasks")["tasks"]
+                      if t["title"] == "Parked refresh parent"][0])
+        check(wait_for(lambda: task_state(auth, parked).get("status") == "suspended"
+                       or False, timeout=60, what="parked parent suspends") is not None,
+              "parent parks before the refresh")
+        fake_card = saddle_card("fake-saddle")
+        act(auth, fake_card["refresh"], {})
+        fake_card = saddle_card("fake-saddle")
+        check(fake_card["status"] == "idle" and int(fake_card["current_task_id"] or 0) == 0,
+              "refresh on a parked (suspended) daemon leaves the saddle idle")
+        # The parked task keeps its open session and resumes normally.
+        conn = db()
+        conn.execute("insert into tasks (project_id, title, status, priority, parent_task_id,"
+                     " summary, completed_at, created_at) values (1, 'Parked child', 'done', 5,"
+                     " ?, 'child finished', strftime('%s','now','+1 second'),"
+                     " strftime('%s','now'))", (parked,))
+        conn.commit()
+        conn.close()
+        check(wait_task_done(auth, parked, timeout=90) is not None,
+              "parked parent still resumes after the refresh")
+
     finally:
         # Exit-code diagnostic: distinguishes a crashed app (negative
         # returncode, e.g. -11 SIGSEGV) from a clean shutdown. When