Files
PacketStation/server/packetstation/drawings/queue.py
T

114 lines
4.7 KiB
Python
Raw Normal View History

"""Which drawings the drawing job looks up this run, in order.
Over live packets made from VISUAL that are not filed, have a drawing number, and sit in a department in scope
(feed setting `departments`; empty = every department). One item per drawing number (trimmed), the best priority
winning:
1. the VISUAL rev changed since the last lookup (`drawing_lookups.visual_rev_seen`), or a pending feed change
order waits for a drawing (the item is the CO's target drawing and rev; at most every 15 minutes);
2. drafts still needing a drawing (`drawing_state = 'needed'`, or never looked up), unless a failed fetch set a
later `next_check_at`;
3. failed lookups whose backoff ran out (`ok = false and next_check_at <= now`);
4. rechecks due (`checked_at` older than `drawing_recheck_hours`).
A packet with a hand drawing joins only through (1) or (4) once the feed has taken the hand drawing off; while
`drawing_state = 'hand'` it is skipped.
"""
from __future__ import annotations
from dataclasses import dataclass
from datetime import datetime, timedelta
from typing import Any
BACKOFF_FIRST = timedelta(minutes=15)
BACKOFF_MAX = timedelta(hours=24)
CO_RECHECK = timedelta(minutes=15) # a change order's target drawing is looked up at most this often
QUEUE_SQL = """
WITH own AS (
SELECT p.id AS packet_id, btrim(p.drawing) AS dwg, coalesce(btrim(p.rev), '') AS visual_rev, NULL::bigint AS co_id,
CASE
WHEN d.dwg IS NOT NULL AND d.checked_at IS NOT NULL
AND d.visual_rev_seen IS DISTINCT FROM coalesce(btrim(p.rev), '') THEN 1
WHEN p.status = 'draft' AND (d.dwg IS NULL OR p.drawing_state = 'needed')
AND (d.next_check_at IS NULL OR d.next_check_at <= %(now)s) THEN 2
WHEN d.ok = false AND (d.next_check_at IS NULL OR d.next_check_at <= %(now)s) THEN 3
WHEN d.checked_at IS NULL OR d.checked_at < %(recheck_before)s THEN 4
END AS priority,
d.checked_at
FROM packets p
LEFT JOIN drawing_lookups d ON d.dwg = btrim(p.drawing)
WHERE p.removed_at IS NULL AND p.source_kind = 'visual' AND p.status <> 'filed'
AND coalesce(btrim(p.drawing), '') <> '' AND p.drawing_state <> 'hand'
AND (cardinality(%(depts)s::text[]) = 0 OR p.dept_code = ANY(%(depts)s::text[]))
),
co AS (
SELECT p.id AS packet_id, btrim(c.waiting->>'dwg') AS dwg, coalesce(btrim(c.waiting->>'rev'), '') AS visual_rev,
c.id AS co_id, 1 AS priority, d.checked_at
FROM change_orders c
JOIN packets p ON p.id = c.packet_id
LEFT JOIN drawing_lookups d ON d.dwg = btrim(c.waiting->>'dwg')
WHERE c.review = 'pending' AND c.origin IN ('visual', 'drawing') AND c.waiting IS NOT NULL
AND jsonb_typeof(c.waiting) = 'object' AND coalesce(btrim(c.waiting->>'dwg'), '') <> ''
AND p.removed_at IS NULL AND p.status NOT IN ('draft', 'filed')
AND (d.checked_at IS NULL OR d.checked_at < %(co_before)s)
AND (d.ok IS NOT false OR d.next_check_at IS NULL OR d.next_check_at <= %(now)s)
AND (cardinality(%(depts)s::text[]) = 0 OR p.dept_code = ANY(%(depts)s::text[]))
),
items AS (
SELECT * FROM own WHERE priority IS NOT NULL
UNION ALL
SELECT * FROM co
),
best AS (
SELECT DISTINCT ON (dwg) packet_id, dwg, visual_rev, priority, co_id, checked_at
FROM items
ORDER BY dwg, priority, co_id NULLS LAST, packet_id
)
SELECT packet_id, dwg, visual_rev, priority, co_id
FROM best
ORDER BY priority, checked_at NULLS FIRST, dwg
"""
@dataclass(frozen=True, slots=True)
class QueueItem:
packet_id: int
dwg: str
visual_rev: str
priority: int
co_id: int | None
def backoff(fail_count: int) -> timedelta:
"""15 minutes x 2^(fail_count - 1), at most 24 hours. fail_count counts this failure (1 = first)."""
n = max(0, int(fail_count) - 1)
if n >= 10:
return BACKOFF_MAX
return min(BACKOFF_FIRST * (2**n), BACKOFF_MAX)
def params(feed_settings: Any, now: datetime) -> dict[str, Any]:
hours = int(getattr(feed_settings, "drawing_recheck_hours", 4))
depts = [str(d) for d in (getattr(feed_settings, "departments", None) or [])]
return {
"now": now,
"recheck_before": now - timedelta(hours=hours),
"co_before": now - CO_RECHECK,
"depts": depts,
}
def queue(conn: Any, feed_settings: Any, now: datetime, *, limit: int | None = None) -> list[QueueItem]:
"""The ordered lookup list. `limit` cuts it (the job passes the budget)."""
sql = QUEUE_SQL
p = params(feed_settings, now)
if limit is not None:
sql += " LIMIT %(limit)s"
p["limit"] = max(0, int(limit))
rows = conn.execute(sql, p).fetchall()
return [
QueueItem(int(r[0]), str(r[1]), str(r[2] or ""), int(r[3]), None if r[4] is None else int(r[4])) for r in rows
]