"""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 ]