|
|
@@ -259,11 +259,11 @@ namespace ClankerSaddle.Services {
|
|
|
|
|
|
/**
|
|
|
* Computed display states for a batch of tasks (all lists share
|
|
|
- * this), keyed by task id string. Unanswered review blocks load
|
|
|
- * once, not per task. Plan 16 S5 (U3): a goal-run task with a
|
|
|
- * pending review-family block (plan/completion/review) reads
|
|
|
- * "awaiting your review" — the run-task card agrees with its
|
|
|
- * goal.
|
|
|
+ * this), keyed by task id string. Unanswered review-family
|
|
|
+ * blocks load in ONE batched query over the wanted tasks, not
|
|
|
+ * per task. Plan 16 S5 (U3): a goal-run task with a pending
|
|
|
+ * review-family block (plan/completion/review) reads "awaiting
|
|
|
+ * your review" — the run-task card agrees with its goal.
|
|
|
*/
|
|
|
public async Dictionary<string, TaskDisplay> display_map_for(
|
|
|
Lot<TaskEntity> tasks) throws Error {
|
|
|
@@ -278,9 +278,9 @@ namespace ClankerSaddle.Services {
|
|
|
|
|
|
var review_pending = new Invercargill.DataStructures.HashSet<string>();
|
|
|
var goal_review_pending = new Invercargill.DataStructures.HashSet<string>();
|
|
|
- foreach (var family in new string[] { BLOCK_REVIEW, BLOCK_COMPLETION, BLOCK_PLAN }) {
|
|
|
+ if (wanted.length > 0) {
|
|
|
foreach (var block in yield db.query<TaskBlock>()
|
|
|
- .where(expr("type == $0", elem(family)))
|
|
|
+ .where(id_in("task_id", wanted))
|
|
|
.materialise_async()) {
|
|
|
if (block.answered_at != null
|
|
|
|| !wanted.has(block.task_id.to_string())) {
|
|
|
@@ -289,8 +289,12 @@ namespace ClankerSaddle.Services {
|
|
|
if (block.block_type == BLOCK_REVIEW) {
|
|
|
review_pending.add(block.task_id.to_string());
|
|
|
}
|
|
|
- if (goal_runs.has(block.task_id.to_string())) {
|
|
|
- goal_review_pending.add(block.task_id.to_string());
|
|
|
+ if (block.block_type == BLOCK_REVIEW
|
|
|
+ || block.block_type == BLOCK_COMPLETION
|
|
|
+ || block.block_type == BLOCK_PLAN) {
|
|
|
+ if (goal_runs.has(block.task_id.to_string())) {
|
|
|
+ goal_review_pending.add(block.task_id.to_string());
|
|
|
+ }
|
|
|
}
|
|
|
}
|
|
|
}
|
|
|
@@ -330,29 +334,37 @@ namespace ClankerSaddle.Services {
|
|
|
|
|
|
/**
|
|
|
* Lists tasks, optionally filtered by status and/or project, ordered
|
|
|
- * by priority (1 first) then creation time.
|
|
|
+ * by priority (1 first) then creation time — filtered and sorted
|
|
|
+ * in the query, one materialisation.
|
|
|
*/
|
|
|
public async ImmutableLot<TaskEntity> list_tasks(string? status, int64? project_id) throws Error {
|
|
|
- var tasks = yield db.query<TaskEntity>().materialise_async();
|
|
|
- var rows = new Vector<TaskEntity>();
|
|
|
- foreach (var task in tasks) {
|
|
|
- if (status != null && status.length > 0 && task.status != status) {
|
|
|
- continue;
|
|
|
- }
|
|
|
- if (project_id != null && task.project_id != project_id) {
|
|
|
- continue;
|
|
|
- }
|
|
|
- rows.add(task);
|
|
|
+ var query = db.query<TaskEntity>();
|
|
|
+ if (status != null && status.length > 0) {
|
|
|
+ query.where(expr("status == $0", elem(status)));
|
|
|
}
|
|
|
- var sorted = rows.sort((a, b) => {
|
|
|
- if (a.priority != b.priority) {
|
|
|
- return (int)(a.priority - b.priority);
|
|
|
- }
|
|
|
- int64 a_created = a.created_at != null ? ((!)a.created_at).to_unix() : 0;
|
|
|
- int64 b_created = b.created_at != null ? ((!)b.created_at).to_unix() : 0;
|
|
|
- return (int)(a_created - b_created);
|
|
|
- });
|
|
|
- return sorted.to_immutable_buffer();
|
|
|
+ if (project_id != null) {
|
|
|
+ query.where(expr("project_id == $0", elem<int64?>(project_id)));
|
|
|
+ }
|
|
|
+ return yield query
|
|
|
+ .order_by(expr("priority"))
|
|
|
+ .order_by(expr("created_at"))
|
|
|
+ .materialise_async();
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * Every standalone human-workable task row (kind task, no
|
|
|
+ * goal), any status, ordered like {@link list_tasks} — the
|
|
|
+ * auto-enqueue-after dropdowns' option list, queried for
|
|
|
+ * exactly the rows they show instead of every task in the
|
|
|
+ * table.
|
|
|
+ */
|
|
|
+ public async ImmutableLot<TaskEntity> list_standalone_tasks() throws Error {
|
|
|
+ return yield db.query<TaskEntity>()
|
|
|
+ .where(expr("kind == $0 && goal_id == $1",
|
|
|
+ elem(TASK_KIND_TASK), elem<int64?>(0)))
|
|
|
+ .order_by(expr("priority"))
|
|
|
+ .order_by(expr("created_at"))
|
|
|
+ .materialise_async();
|
|
|
}
|
|
|
|
|
|
/**
|
|
|
@@ -1044,28 +1056,88 @@ namespace ClankerSaddle.Services {
|
|
|
* Task ids currently `assigned`/`working` whose open session sits
|
|
|
* on an offline or fenced saddle — the queue marks them
|
|
|
* "saddle unreachable — requeueing" while recovery runs.
|
|
|
+ * Batched: the candidate tasks load once, their sessions in
|
|
|
+ * one query, the saddles in another.
|
|
|
*/
|
|
|
public async Invercargill.DataStructures.HashSet<string> unreachable_task_ids() throws Error {
|
|
|
var ids = new Invercargill.DataStructures.HashSet<string>();
|
|
|
+ var candidate_ids = new Invercargill.DataStructures.HashSet<string>();
|
|
|
foreach (var status in new string[] { STATUS_ASSIGNED, STATUS_WORKING }) {
|
|
|
foreach (var task in yield db.query<TaskEntity>()
|
|
|
.where(expr("status == $0", elem(status)))
|
|
|
.materialise_async()) {
|
|
|
- var session = yield open_session_for_task(task.id);
|
|
|
- if (session == null) {
|
|
|
- continue;
|
|
|
- }
|
|
|
- var saddle = yield db.query<Saddle>()
|
|
|
- .where(expr("id == $0", elem<int64?>(((!)session).saddle_id)))
|
|
|
- .first_async();
|
|
|
- if (saddle != null && ((!)saddle).status == SADDLE_OFFLINE) {
|
|
|
- ids.add(task.id.to_string());
|
|
|
- }
|
|
|
+ candidate_ids.add(task.id.to_string());
|
|
|
+ }
|
|
|
+ }
|
|
|
+ if (candidate_ids.length == 0) {
|
|
|
+ return ids;
|
|
|
+ }
|
|
|
+
|
|
|
+ // The newest open session per task: sessions ordered by id
|
|
|
+ // ascending, the LAST open row per task wins (the
|
|
|
+ // pre-batch semantics) — walked backwards, first hit per
|
|
|
+ // task keeps the slot.
|
|
|
+ var row_tasks = new Vector<string>();
|
|
|
+ var row_saddles = new Vector<string>();
|
|
|
+ foreach (var session in yield db.query<Session>()
|
|
|
+ .where(id_in("task_id", candidate_ids))
|
|
|
+ .order_by(expr("id"))
|
|
|
+ .materialise_async()) {
|
|
|
+ if (session.ended_at == null) {
|
|
|
+ row_tasks.add(session.task_id.to_string());
|
|
|
+ row_saddles.add(session.saddle_id.to_string());
|
|
|
+ }
|
|
|
+ }
|
|
|
+ var open_task_ids = new Vector<string>();
|
|
|
+ var open_saddle_ids = new Vector<string>();
|
|
|
+ var taken_tasks = new Invercargill.DataStructures.HashSet<string>();
|
|
|
+ for (int index = (int) row_tasks.length - 1; index >= 0; index--) {
|
|
|
+ if (taken_tasks.add(row_tasks.get(index))) {
|
|
|
+ open_task_ids.add(row_tasks.get(index));
|
|
|
+ open_saddle_ids.add(row_saddles.get(index));
|
|
|
+ }
|
|
|
+ }
|
|
|
+ if (open_task_ids.length == 0) {
|
|
|
+ return ids;
|
|
|
+ }
|
|
|
+
|
|
|
+ var saddle_ids = new Invercargill.DataStructures.HashSet<string>();
|
|
|
+ foreach (var saddle_id in open_saddle_ids) {
|
|
|
+ saddle_ids.add(saddle_id);
|
|
|
+ }
|
|
|
+ var offline = new Invercargill.DataStructures.HashSet<string>();
|
|
|
+ foreach (var saddle in yield db.query<Saddle>()
|
|
|
+ .where(id_in("id", saddle_ids))
|
|
|
+ .materialise_async()) {
|
|
|
+ if (saddle.status == SADDLE_OFFLINE) {
|
|
|
+ offline.add(saddle.id.to_string());
|
|
|
+ }
|
|
|
+ }
|
|
|
+ for (int index = 0; index < open_task_ids.length; index++) {
|
|
|
+ if (offline.has(open_saddle_ids.get(index))) {
|
|
|
+ ids.add(open_task_ids.get(index));
|
|
|
}
|
|
|
}
|
|
|
return ids;
|
|
|
}
|
|
|
|
|
|
+ /**
|
|
|
+ * An OR-chain of `column == id` comparisons for a batch of ids
|
|
|
+ * (the grammar has no IN operator), or null for an empty
|
|
|
+ * batch — the WHERE-in the batched lookups build.
|
|
|
+ */
|
|
|
+ private static Expression? id_in(string column,
|
|
|
+ Invercargill.DataStructures.HashSet<string> ids) throws Error {
|
|
|
+ Expression? chain = null;
|
|
|
+ foreach (var id in ids) {
|
|
|
+ var comparison = expr(column + " == $0", elem(id));
|
|
|
+ chain = chain == null
|
|
|
+ ? comparison : new BinaryExpression((!)chain, comparison,
|
|
|
+ BinaryOperator.OR);
|
|
|
+ }
|
|
|
+ return chain;
|
|
|
+ }
|
|
|
+
|
|
|
/**
|
|
|
* The resume_notice wording for an answered block (the plan-11
|
|
|
* stable verdicts): review and plan answers are already stored
|
|
|
@@ -1403,13 +1475,30 @@ namespace ClankerSaddle.Services {
|
|
|
/**
|
|
|
* Queued tasks whose auto-enqueue predecessor (if any) is
|
|
|
* done, ordered by priority (1 first) then creation time —
|
|
|
- * the dispatcher's candidate list.
|
|
|
+ * the dispatcher's candidate list. Only the CANDIDATES'
|
|
|
+ * predecessors are queried (not every task row).
|
|
|
*/
|
|
|
public async ImmutableLot<TaskEntity> get_ready_tasks() throws Error {
|
|
|
var queued = yield db.query<TaskEntity>()
|
|
|
.where(expr("status == $0", elem(STATUS_QUEUED)))
|
|
|
+ .order_by(expr("priority"))
|
|
|
+ .order_by(expr("created_at"))
|
|
|
.materialise_async();
|
|
|
- var all_tasks = yield db.query<TaskEntity>().materialise_async();
|
|
|
+
|
|
|
+ var predecessor_ids = new Invercargill.DataStructures.HashSet<string>();
|
|
|
+ foreach (var task in queued) {
|
|
|
+ if (task.enqueue_after_task_id > 0) {
|
|
|
+ predecessor_ids.add(task.enqueue_after_task_id.to_string());
|
|
|
+ }
|
|
|
+ }
|
|
|
+ var predecessor_status = new Dictionary<string, string>();
|
|
|
+ if (predecessor_ids.length > 0) {
|
|
|
+ foreach (var predecessor in yield db.query<TaskEntity>()
|
|
|
+ .where(id_in("id", predecessor_ids))
|
|
|
+ .materialise_async()) {
|
|
|
+ predecessor_status.set(predecessor.id.to_string(), predecessor.status);
|
|
|
+ }
|
|
|
+ }
|
|
|
|
|
|
var rows = new Vector<TaskEntity>();
|
|
|
foreach (var task in queued) {
|
|
|
@@ -1418,29 +1507,17 @@ namespace ClankerSaddle.Services {
|
|
|
// The auto-enqueue predecessor also gates dispatch:
|
|
|
// a dependent that reached the queue some other way
|
|
|
// (a predecessor set after a manual enqueue) still
|
|
|
- // waits for it to finish.
|
|
|
- ready = false;
|
|
|
- foreach (var other in all_tasks) {
|
|
|
- if (other.id == task.enqueue_after_task_id) {
|
|
|
- ready = other.status == STATUS_DONE;
|
|
|
- break;
|
|
|
- }
|
|
|
- }
|
|
|
+ // waits for it to finish. A predecessor row that
|
|
|
+ // does not resolve keeps the dependent waiting.
|
|
|
+ string? status = predecessor_status.get_or_default(
|
|
|
+ task.enqueue_after_task_id.to_string());
|
|
|
+ ready = status != null && status == STATUS_DONE;
|
|
|
}
|
|
|
if (ready) {
|
|
|
rows.add(task);
|
|
|
}
|
|
|
}
|
|
|
-
|
|
|
- var sorted = rows.sort((a, b) => {
|
|
|
- if (a.priority != b.priority) {
|
|
|
- return (int)(a.priority - b.priority);
|
|
|
- }
|
|
|
- int64 a_created = a.created_at != null ? ((!)a.created_at).to_unix() : 0;
|
|
|
- int64 b_created = b.created_at != null ? ((!)b.created_at).to_unix() : 0;
|
|
|
- return (int)(a_created - b_created);
|
|
|
- });
|
|
|
- return sorted.to_immutable_buffer();
|
|
|
+ return rows.to_immutable_buffer();
|
|
|
}
|
|
|
|
|
|
/** Comments on a task, oldest first. */
|