114 lines
4.7 KiB
Python
114 lines
4.7 KiB
Python
"""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
|
||
|
|
]
|