Start queued jobs in list order, whether pinned or "Any <model>" (#3200)

This commit is contained in:
maziggy
2026-09-30 15:55:05 +02:00
parent c446263774
commit e9627e44a2
16 changed files with 447 additions and 222 deletions
+17 -47
View File
@@ -3,17 +3,17 @@
*
* The backend decides this in SQL (`print_scheduler.check_queue`):
*
* ORDER BY printer_id, target_model,
* been_jumped DESC,
* ORDER BY been_jumped DESC,
* print_time_seconds ASC NULLS LAST,
* position
* position, id
*
* with the first two columns acting purely as a grouping — a row carries
* either a `printer_id` or a `target_model`, never both — and the rest
* deciding who goes first inside that group. Every UI surface that claims to
* show queue order has to reproduce it, and each one that reproduced it
* privately drifted: the timeline sorted by `position` alone and so ignored
* Shortest-Job-First entirely (#3043). One comparator, three callers.
* with the first two keys only when Shortest-Job-First is on. It is one order
* across the whole queue, not one per printer (#3200): a job pinned to a
* printer and an "Any <model>" job both want that printer, and the one higher
* in this order gets it. Every UI surface that claims to show queue order has
* to reproduce it, and each one that reproduced it privately drifted: the
* timeline sorted by `position` alone and so ignored Shortest-Job-First
* entirely (#3043).
*/
interface OrderableQueueItem {
@@ -22,12 +22,13 @@ interface OrderableQueueItem {
been_jumped?: boolean;
print_time_seconds?: number | null;
position: number;
id?: number;
}
/**
* Which dispatch group an item belongs to: a named printer, a printer model,
* or neither. Items only compete with others in their own group, so this is
* both the timeline's swimlane and the outer sort key of a flat pending list.
* Which swimlane an item is drawn in on the timeline: a named printer, a
* printer model, or neither. Display only -- lanes do not decide dispatch
* order, which is global (#3200).
*
* Returned as a string rather than a number because the group is a name, not
* a magnitude. The flat list used to fold `target_model` down to
@@ -41,7 +42,8 @@ export function queueLaneKey(item: OrderableQueueItem): string {
}
/**
* Order two items competing for the same printer or model.
* Order two pending items the way the scheduler takes them, whichever printer
* or model each is queued for.
*
* @param sjfEnabled the `queue_shortest_first` setting. When off, the
* scheduler orders by position alone and so does this.
@@ -65,38 +67,6 @@ export function compareQueueOrder(
if (aTime !== bTime) return aTime - bTime;
}
return a.position - b.position;
}
/**
* Order a flat list that spans several groups -- a pending list rather than a
* per-lane one. Groups stay contiguous; within each, the scheduler's own order
* applies.
*
* Groups themselves are ordered for reading, not to mirror the backend: named
* printers by id, then model lanes by name, then unassigned. The backend's own
* answer here is `ORDER BY printer_id` with a NULL in it, which SQLite sorts
* first and PostgreSQL sorts last -- nothing worth reproducing.
*/
export function compareQueueOrderAcrossLanes(
a: OrderableQueueItem,
b: OrderableQueueItem,
sjfEnabled: boolean,
): number {
const aLane = queueLaneKey(a);
const bLane = queueLaneKey(b);
if (aLane !== bLane) {
if (a.printer_id != null && b.printer_id != null) return a.printer_id - b.printer_id;
const aRank = laneRank(a);
const bRank = laneRank(b);
if (aRank !== bRank) return aRank - bRank;
return aLane < bLane ? -1 : 1;
}
return compareQueueOrder(a, b, sjfEnabled);
}
function laneRank(item: OrderableQueueItem): number {
if (item.printer_id != null) return 0;
if (item.target_model) return 1;
return 2;
if (a.position !== b.position) return a.position - b.position;
return (a.id ?? 0) - (b.id ?? 0);
}