Skip to content

Commit c383d6e

Browse files
authored
Reduce task query and polling overhead
1 parent 8ca546b commit c383d6e

9 files changed

Lines changed: 641 additions & 102 deletions

File tree

backend/src/api.ts

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1423,8 +1423,9 @@ async function handleGet(
14231423
}
14241424

14251425
if (path === "/api/tasks") {
1426+
const summary = url.searchParams.get("mode") === "summary";
14261427
return jsonResponse(
1427-
ctx.db.get_all_tasks().map((t) => attachDependencyMetadata(ctx.db, t)),
1428+
ctx.db.get_all_tasks_with_dependencies(summary),
14281429
200,
14291430
origin,
14301431
);
@@ -1512,7 +1513,7 @@ async function handleGet(
15121513
return jsonResponse({ csrf_token: CSRF_TOKEN }, 200, origin);
15131514
if (path === "/api/health")
15141515
return jsonResponse(
1515-
{ status: "ok", tasks: ctx.db.get_all_tasks().length },
1516+
{ status: "ok", tasks: ctx.db.count_tasks() },
15161517
200,
15171518
origin,
15181519
);

backend/src/db.ts

Lines changed: 79 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -106,6 +106,10 @@ export class TaskDB {
106106
this._migrate("ALTER TABLE tasks ADD COLUMN image_paths TEXT DEFAULT '[]'");
107107
this._migrate("ALTER TABLE tasks ADD COLUMN notify_slack_channel TEXT");
108108
this._migrate("ALTER TABLE tasks ADD COLUMN notify_telegram_chat_id TEXT");
109+
this.conn.run(`
110+
CREATE INDEX IF NOT EXISTS idx_tasks_status_next_run
111+
ON tasks(status, next_run_at)
112+
`);
109113

110114
this.conn.run(`
111115
CREATE TABLE IF NOT EXISTS settings (
@@ -174,6 +178,10 @@ export class TaskDB {
174178
)
175179
`);
176180
this._migrate("ALTER TABLE task_runs ADD COLUMN raw_output TEXT");
181+
this.conn.run(`
182+
CREATE INDEX IF NOT EXISTS idx_task_runs_task_started
183+
ON task_runs(task_id, started_at DESC)
184+
`);
177185

178186
this.conn.run(`
179187
CREATE TABLE IF NOT EXISTS heartbeats (
@@ -265,6 +273,10 @@ export class TaskDB {
265273
CREATE INDEX IF NOT EXISTS idx_task_output_events_timestamp
266274
ON task_output_events(timestamp)
267275
`);
276+
this.conn.run(`
277+
CREATE INDEX IF NOT EXISTS idx_task_output_events_task_timestamp
278+
ON task_output_events(task_id, timestamp DESC)
279+
`);
268280

269281
// DAG dependency table
270282
this.conn.run(`
@@ -1286,6 +1298,73 @@ export class TaskDB {
12861298
return rows.map((r) => this._deserialize_task(r));
12871299
}
12881300

1301+
/**
1302+
* Return the task list with dependency metadata using three fixed queries.
1303+
*
1304+
* The legacy list keeps the same shape as get_task() plus dependencies and
1305+
* dependents. Summary mode omits large detail-only columns and truncates the
1306+
* prompt for TaskCard rendering.
1307+
*/
1308+
get_all_tasks_with_dependencies(summary: boolean = false): Row[] {
1309+
const taskSql = summary
1310+
? `SELECT id, title, substr(prompt, 1, 240) AS prompt_preview,
1311+
status, schedule_type, cron_expr, delay_seconds, next_run_at,
1312+
last_run_at, run_count, max_runs, tags, agent, dag_id
1313+
FROM tasks ORDER BY created_at DESC`
1314+
: "SELECT * FROM tasks ORDER BY created_at DESC";
1315+
const rows = this.conn.query(taskSql).all() as Row[];
1316+
if (rows.length === 0) return [];
1317+
1318+
const taskIds = new Set(rows.map((row) => Number(row["id"])));
1319+
const dependenciesByTask = new Map<number, Row[]>();
1320+
const dependentsByTask = new Map<number, number[]>();
1321+
const dependencies = this.conn
1322+
.query(
1323+
`SELECT td.*, t.title AS depends_on_title, t.status AS depends_on_status
1324+
FROM task_dependencies td
1325+
JOIN tasks t ON t.id = td.depends_on_task_id`,
1326+
)
1327+
.all() as Row[];
1328+
const dependents = this.conn
1329+
.query(
1330+
`SELECT td.*, t.title AS task_title, t.status AS task_status
1331+
FROM task_dependencies td
1332+
JOIN tasks t ON t.id = td.task_id`,
1333+
)
1334+
.all() as Row[];
1335+
1336+
for (const dependency of dependencies) {
1337+
const taskId = Number(dependency["task_id"]);
1338+
if (!taskIds.has(taskId)) continue;
1339+
const value = summary
1340+
? { depends_on_task_id: dependency["depends_on_task_id"] }
1341+
: { ...dependency };
1342+
const current = dependenciesByTask.get(taskId) ?? [];
1343+
current.push(value);
1344+
dependenciesByTask.set(taskId, current);
1345+
}
1346+
for (const dependent of dependents) {
1347+
const upstreamId = Number(dependent["depends_on_task_id"]);
1348+
if (!taskIds.has(upstreamId)) continue;
1349+
const current = dependentsByTask.get(upstreamId) ?? [];
1350+
current.push(Number(dependent["task_id"]));
1351+
dependentsByTask.set(upstreamId, current);
1352+
}
1353+
1354+
return rows.map((row) => ({
1355+
...(summary ? row : this._deserialize_task(row)),
1356+
dependencies: dependenciesByTask.get(Number(row["id"])) ?? [],
1357+
dependents: dependentsByTask.get(Number(row["id"])) ?? [],
1358+
}));
1359+
}
1360+
1361+
count_tasks(): number {
1362+
const row = this.conn
1363+
.query("SELECT COUNT(*) AS count FROM tasks")
1364+
.get() as Row;
1365+
return Number(row["count"]);
1366+
}
1367+
12891368
get_due_tasks(): Row[] {
12901369
const rows = this.conn
12911370
.query(

backend/tests/api-handler.test.ts

Lines changed: 71 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -158,6 +158,77 @@ describe("api handler", () => {
158158
expect(tasks[0]["dependents"]).toEqual([]);
159159
});
160160

161+
test("GET /api/tasks batches dependency metadata without per-task queries", async () => {
162+
const upstream = db.add_task(
163+
makeTask({ title: "Upstream", prompt: "prepare", working_dir: "." }),
164+
);
165+
const downstream = db.add_task(
166+
makeTask({ title: "Downstream", prompt: "finish", working_dir: "." }),
167+
);
168+
db.add_dependency(downstream, upstream, true);
169+
db.get_dependencies = () => {
170+
throw new Error("per-task dependency query should not run");
171+
};
172+
db.get_dependents = () => {
173+
throw new Error("per-task dependent query should not run");
174+
};
175+
176+
const tasks = await json(new Request("http://127.0.0.1:9712/api/tasks"));
177+
178+
expect(tasks).toHaveLength(2);
179+
expect(
180+
tasks.find((task: Record<string, any>) => task.id === upstream)
181+
.dependents,
182+
).toEqual([downstream]);
183+
expect(
184+
tasks.find((task: Record<string, any>) => task.id === downstream)
185+
.dependencies[0],
186+
).toMatchObject({
187+
task_id: downstream,
188+
depends_on_task_id: upstream,
189+
inject_result: 1,
190+
depends_on_title: "Upstream",
191+
});
192+
expect(
193+
tasks.find((task: Record<string, any>) => task.id === upstream).prompt,
194+
).toBe("prepare");
195+
});
196+
197+
test("GET /api/tasks summary returns only board fields", async () => {
198+
db.add_task(
199+
makeTask({
200+
title: "Summary",
201+
prompt: "x".repeat(400),
202+
working_dir: "/private/project",
203+
}),
204+
);
205+
206+
const tasks = await json(
207+
new Request("http://127.0.0.1:9712/api/tasks?mode=summary"),
208+
);
209+
const task = tasks[0];
210+
211+
expect(task.prompt_preview).toHaveLength(240);
212+
expect(task.dependencies).toEqual([]);
213+
expect(task.dependents).toEqual([]);
214+
expect(task).not.toHaveProperty("prompt");
215+
expect(task).not.toHaveProperty("working_dir");
216+
expect(task).not.toHaveProperty("result");
217+
expect(task).not.toHaveProperty("error");
218+
expect(task).not.toHaveProperty("prompt_images");
219+
});
220+
221+
test("GET /api/health counts tasks without loading task rows", async () => {
222+
db.add_task(makeTask({ title: "Count me", prompt: "p" }));
223+
db.get_all_tasks = () => {
224+
throw new Error("health should use COUNT");
225+
};
226+
227+
const health = await json(new Request("http://127.0.0.1:9712/api/health"));
228+
229+
expect(health).toEqual({ status: "ok", tasks: 1 });
230+
});
231+
161232
test("GET task output falls back to latest persisted raw output", async () => {
162233
const created = await json(
163234
new Request("http://127.0.0.1:9712/api/tasks", {

backend/tests/taskdb.test.ts

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,29 @@ describe("TaskDB", () => {
4949
expect(db.get_setting("k")).toBe("v2");
5050
});
5151

52+
test("test_task_count_and_polling_indexes_are_idempotent", () => {
53+
db.add_task(makeTask({ title: "one", prompt: "p" }));
54+
expect(db.count_tasks()).toBe(1);
55+
56+
const dbPath = db.db_path;
57+
db.conn.close();
58+
db = new TaskDB(dbPath);
59+
60+
const indexNames = (table: string) =>
61+
new Set(
62+
(
63+
db.conn.query(`PRAGMA index_list('${table}')`).all() as Array<{
64+
name: string;
65+
}>
66+
).map((row) => row.name),
67+
);
68+
expect(indexNames("tasks")).toContain("idx_tasks_status_next_run");
69+
expect(indexNames("task_runs")).toContain("idx_task_runs_task_started");
70+
expect(indexNames("task_output_events")).toContain(
71+
"idx_task_output_events_task_timestamp",
72+
);
73+
});
74+
5275
// ── run history ────────────────────────────────────────────────────────────
5376
test("test_run_lifecycle_and_ordering", () => {
5477
const tid = db.add_task(

0 commit comments

Comments
 (0)