|
|
@@ -1158,8 +1158,7 @@ namespace ClankerSaddle.Services {
|
|
|
|
|
|
yield mark_terminal((!)task, STATUS_DONE, summary, working_branch);
|
|
|
|
|
|
- yield close_open_sessions(task_id, outcome);
|
|
|
- yield free_saddles_running(task_id);
|
|
|
+ yield release_task(task_id, outcome, false);
|
|
|
yield pulse.bump(task_id, "task completed");
|
|
|
// Web Push (plan 11 task E): completion notifies the users
|
|
|
// holding a "Notify me" pref for this task.
|
|
|
@@ -1196,8 +1195,7 @@ namespace ClankerSaddle.Services {
|
|
|
|
|
|
yield mark_terminal((!)task, STATUS_FAILED, null, null, "Cancelled by user");
|
|
|
|
|
|
- yield close_open_sessions(task_id, "stopped");
|
|
|
- yield free_saddles_running(task_id);
|
|
|
+ yield release_task(task_id, "stopped", false);
|
|
|
yield pulse.bump(task_id, "task cancelled");
|
|
|
}
|
|
|
|
|
|
@@ -1229,8 +1227,7 @@ namespace ClankerSaddle.Services {
|
|
|
yield mark_terminal((!)task, STATUS_FAILED, summary, null,
|
|
|
"Saddle reported a failure");
|
|
|
|
|
|
- yield close_open_sessions(task_id, "failed");
|
|
|
- yield free_saddles_running(task_id);
|
|
|
+ yield release_task(task_id, "failed", false);
|
|
|
yield pulse.bump(task_id, "task failed");
|
|
|
// Web Push (plan 11 task E): an agent-reported failure
|
|
|
// notifies the opted-in users (a human cancellation does
|
|
|
@@ -1364,27 +1361,15 @@ namespace ClankerSaddle.Services {
|
|
|
/**
|
|
|
* Returns a non-terminal task to the queue: closes its open
|
|
|
* sessions (outcome 'stopped') and frees the saddle that was
|
|
|
- * running it. Plan 14 B1: a parked (suspended) task requeues
|
|
|
- * too — the goal page's requeue-to-resume reaches a goal
|
|
|
- * agent parked on a dead saddle; the suspension stamp clears
|
|
|
- * so the next dispatch starts a fresh parking window.
|
|
|
+ * running it — {@link release_task} with the requeue, the
|
|
|
+ * single owner of the bookkeeping. Plan 14 B1: a parked
|
|
|
+ * (suspended) task requeues too — the goal page's
|
|
|
+ * requeue-to-resume reaches a goal agent parked on a dead
|
|
|
+ * saddle; the suspension stamp clears so the next dispatch
|
|
|
+ * starts a fresh parking window.
|
|
|
*/
|
|
|
public async void requeue_task(int64 task_id) throws Error {
|
|
|
- var task = yield db.query<TaskEntity>()
|
|
|
- .where(expr("id == $0", elem<int64?>(task_id)))
|
|
|
- .first_async();
|
|
|
- if (task != null && (((!)task).status == STATUS_ASSIGNED
|
|
|
- || ((!)task).status == STATUS_WORKING || ((!)task).status == STATUS_BLOCKED
|
|
|
- || ((!)task).status == STATUS_REVIEW_REQUESTED
|
|
|
- || ((!)task).status == STATUS_SUSPENDED)) {
|
|
|
- ((!)task).status = STATUS_QUEUED;
|
|
|
- if (((!)task).suspended_at != null) {
|
|
|
- ((!)task).suspended_at = null;
|
|
|
- }
|
|
|
- yield db.update_async<TaskEntity>((!)task);
|
|
|
- }
|
|
|
- yield close_open_sessions(task_id, "stopped");
|
|
|
- yield free_saddles_running(task_id);
|
|
|
+ yield release_task(task_id, "stopped", true);
|
|
|
yield pulse.bump(task_id, "task requeued");
|
|
|
}
|
|
|
|
|
|
@@ -1396,8 +1381,9 @@ namespace ClankerSaddle.Services {
|
|
|
* its card; the next attempt writes a fresh one), so no
|
|
|
* summary→comment duplicate joins the thread. `completed_at`
|
|
|
* is withdrawn, any open session closes and its saddle frees —
|
|
|
- * the same bookkeeping as {@link requeue_task}, plus the audit
|
|
|
- * comment "Task retried by user".
|
|
|
+ * the same bookkeeping as {@link requeue_task} (retry's flip
|
|
|
+ * is unconditional: the attempt resets whatever it held),
|
|
|
+ * plus the audit comment "Task retried by user".
|
|
|
*/
|
|
|
public async void retry_task(int64 task_id) throws Error {
|
|
|
var task = yield db.query<TaskEntity>()
|
|
|
@@ -1413,8 +1399,7 @@ namespace ClankerSaddle.Services {
|
|
|
((!)task).completed_at = null;
|
|
|
yield db.update_async<TaskEntity>((!)task);
|
|
|
|
|
|
- yield close_open_sessions(task_id, "stopped");
|
|
|
- yield free_saddles_running(task_id);
|
|
|
+ yield release_task(task_id, "stopped", false);
|
|
|
yield add_system_comment(task_id, "Task retried by user");
|
|
|
yield pulse.bump(task_id, "task retried");
|
|
|
}
|
|
|
@@ -1540,7 +1525,47 @@ namespace ClankerSaddle.Services {
|
|
|
return null;
|
|
|
}
|
|
|
|
|
|
- private async void close_open_sessions(int64 task_id, string outcome) throws Error {
|
|
|
+ /**
|
|
|
+ * The ONE release path (plan 16 R2, arch item 13): every path
|
|
|
+ * that takes a task off its saddle runs through here. Closes
|
|
|
+ * the task's open sessions with `outcome` and frees whatever
|
|
|
+ * saddle row still runs it — one conditional UPDATE, atomic
|
|
|
+ * against a concurrent ask allocation claiming a new task for
|
|
|
+ * the saddle between a read and a full-row write. `requeue`
|
|
|
+ * first returns a non-terminal task to the queue
|
|
|
+ * (assigned/working/blocked/review_requested/suspended — the
|
|
|
+ * suspension stamp withdraws so the next dispatch opens a
|
|
|
+ * fresh parking window).
|
|
|
+ *
|
|
|
+ * The terminal callers (complete/cancel/fail) pass their own
|
|
|
+ * outcome with the status already stamped; requeue_task and
|
|
|
+ * the unregister path ride the requeue; retry_task does its
|
|
|
+ * own unconditional flip (summary and completed_at die with
|
|
|
+ * the attempt) and releases without one. The offline variant
|
|
|
+ * ({@link SaddleService.mark_saddle_unreachable}) keeps its
|
|
|
+ * own guarded OFFLINE write for the saddle row and calls this
|
|
|
+ * after it — the idle-free below no-ops against that row (its
|
|
|
+ * current_task_id already cleared under the entry-seen guard)
|
|
|
+ * while the session close applies; its keep-parked gate (an
|
|
|
+ * answered block awaiting delivery) skips the release
|
|
|
+ * entirely.
|
|
|
+ */
|
|
|
+ public async void release_task(int64 task_id, string outcome, bool requeue) throws Error {
|
|
|
+ if (requeue) {
|
|
|
+ var task = yield db.query<TaskEntity>()
|
|
|
+ .where(expr("id == $0", elem<int64?>(task_id)))
|
|
|
+ .first_async();
|
|
|
+ if (task != null && (((!)task).status == STATUS_ASSIGNED
|
|
|
+ || ((!)task).status == STATUS_WORKING || ((!)task).status == STATUS_BLOCKED
|
|
|
+ || ((!)task).status == STATUS_REVIEW_REQUESTED
|
|
|
+ || ((!)task).status == STATUS_SUSPENDED)) {
|
|
|
+ ((!)task).status = STATUS_QUEUED;
|
|
|
+ if (((!)task).suspended_at != null) {
|
|
|
+ ((!)task).suspended_at = null;
|
|
|
+ }
|
|
|
+ yield db.update_async<TaskEntity>((!)task);
|
|
|
+ }
|
|
|
+ }
|
|
|
var sessions = yield db.query<Session>()
|
|
|
.where(expr("task_id == $0", elem<int64?>(task_id)))
|
|
|
.materialise_async();
|
|
|
@@ -1552,12 +1577,6 @@ namespace ClankerSaddle.Services {
|
|
|
session.outcome = outcome;
|
|
|
yield db.update_async<Session>(session);
|
|
|
}
|
|
|
- }
|
|
|
-
|
|
|
- private async void free_saddles_running(int64 task_id) throws Error {
|
|
|
- // One conditional UPDATE, atomic against a concurrent ask
|
|
|
- // allocation claiming a new task for the saddle between a
|
|
|
- // read and a full-row write.
|
|
|
var command = connection.create_command(
|
|
|
"UPDATE saddles SET status = 'idle', current_task_id = 0, allocated_at = NULL"
|
|
|
+ " WHERE current_task_id = :task");
|