oc_orchestrator
Loading...
Searching...
No Matches
work_queue.py
Go to the documentation of this file.
1#!/usr/bin/env python3
2# -*- coding: utf-8 -*-
3# oc_orchestrator/work_queue.py
4
5# OCT-LINT: disable=tests_exist reason="Covered by oc_orchestrator/tests/test_258_work_queue.py"
6"""
7The work queue's contract: one work item, one operation set, two backends.
8
9Purpose
10-------
11Plan 258 P1, from the factory-decomposition design note §5. The unit of
12work is a work item WITH A KIND -- ``plan`` is the first kind and the
13only one this release accepts -- and the queue has one set of
14operations that two backends implement: a file backend that is the
15default and the mode the package is tested in, and a registry backend
16that inherits signing, audit and checkpoints from the registry because
17that is where those already live. Neither backend is a special case,
18so the operations, the item, the state and the operation record are
19declared exactly once, here, and neither backend is the place where
20the shape gets decided.
21
22This module is deliberately a LEAF: it imports nothing from any
23consumer -- no registry, no chat client -- and nothing outside the
24standard library besides the diagnostics runtime. A witness measures
25that with the committed import-graph extractor rather than asserting
26it by hand. Plan 271 moved it here from ``oc_chat/core/work_queue.py``,
27which is now a rebinding shim over this module.
28
29Responsibilities
30----------------
31- :class:`WorkItem` -- the unit of work: a kind, an id, and three
32 optional free-text fields.
33- :func:`check_kind` -- the ONE place a kind is checked, so a second
34 kind is one edit to :data:`SUPPORTED_KINDS`.
35- :class:`QueueBackend` -- the six operations, as a runtime-checkable
36 :class:`typing.Protocol`, so a backend is a plain class.
37- :class:`QueueState` -- three ordered tuples: pending, in progress,
38 completed.
39- :class:`QueueOp` -- one operation, as the log records it.
40- :class:`QueueError` and its named subclasses -- every refusal.
41
42Diagnostics
43-----------
44Domain: OC_CHAT.MAIN
45Levels:
46 L2 - lifecycle events (module load)
47 L3 - semantic details (a refusal, with what it refused)
48 L4 - deep tracing
49
50Contracts
51---------
52- ``kind`` is a plain string checked by name against
53 :data:`SUPPORTED_KINDS`, read at call time. Widening that set is the
54 whole edit a second kind needs in this module. The design note names
55 the registry's plan-shaped ``related_plan_id`` column as the seam a
56 second kind will also find; that is named, not built.
57- ``item_id`` for a ``plan`` is the plan id, ``NNN-stem``. ``lane`` is
58 a free-text preference such as a model family or a harness name and
59 is NEVER a roster letter -- the letter model was retired by plan 223
60 and is not revived. ``instance`` is the ``agent-NNN`` identity
61 allocated at dispatch; identity is plan-AND-instance, so an item may
62 be re-dispatched and the log then shows both instances.
63- Positions are 1-BASED, matching the registry's ``queue_position``.
64- :class:`QueueOp.at` is ISO 8601 with an explicit offset.
65 :class:`QueueOp.actor` is free text and is NEVER signed here: a
66 backend that signs (the registry one) inherits signing from the
67 registry, and a backend that does not (the file one) must not
68 pretend to.
69- Every refusal is a named exception under :class:`QueueError`.
70 :class:`QueueBackendError` is the separate arm a caller maps to its
71 own "the backend said no" exit code; the other three are semantic.
72
73Dependencies
74------------
75- oc_diagnostics (structured debug logging via _dbg)
76"""
77
78from __future__ import annotations
79
80from collections.abc import Mapping
81from dataclasses import dataclass, field
82from typing import Protocol, runtime_checkable
83
84from oc_diagnostics import _dbg
85
86_dbg("OC_CHAT.MAIN", "module", "work_queue loaded", 4)
87
88
89# ── Kinds ────────────────────────────────────────────────────────────────
90
91
92#: The first kind, and the only one this plan accepts.
93KIND_PLAN = "plan"
94
95#: The kinds a :class:`WorkItem` may carry. Widening this set is the
96#: whole edit a second kind needs here -- :func:`check_kind` reads it at
97#: call time and every construction path goes through that function.
98SUPPORTED_KINDS: frozenset[str] = frozenset({KIND_PLAN})
99
100
101# ── Operation names ──────────────────────────────────────────────────────
102
103
104OP_ENQUEUE = "enqueue"
105OP_MOVE = "move"
106OP_DEQUEUE = "dequeue"
107OP_COMPLETE = "complete"
108
109#: The four op names, in lifecycle order. Both backends report these, so
110#: a history read through one is comparable with a history read through
111#: the other -- which is what the plan-258 equivalence witness asserts.
112OP_NAMES: tuple[str, ...] = (OP_ENQUEUE, OP_MOVE, OP_DEQUEUE, OP_COMPLETE)
113
114
115# ── Refusals ─────────────────────────────────────────────────────────────
116
117
118class QueueError(Exception):
119 """Base class for every refusal the queue makes."""
120
121
123 """A work item named a kind this release does not accept."""
124
125
127 """An operation named an item the queue does not hold."""
128
129
131 """An enqueue named an item the queue already holds."""
132
133
135 """A position is not a 1-based index into the pending list."""
136
137
139 """The BACKEND refused, rather than the operation being meaningless.
140
141 A corrupt log line, a storage constraint, a missing argument the
142 storage demands: things a different backend would not have refused.
143 Callers that map refusals onto exit codes give this its own code.
144 """
145
146
147def check_kind(kind: str) -> None:
148 """Refuse *kind* by name unless it is in :data:`SUPPORTED_KINDS`.
149
150 The single check site. :data:`SUPPORTED_KINDS` is read here, at call
151 time, so widening it is the whole edit a second kind needs.
152
153 Raises:
154 UnknownKindError: VALIDATION_ERROR (L3) -- *kind* is not
155 accepted; the message names it and the accepted set.
156 """
157 func = "check_kind"
158 if kind not in SUPPORTED_KINDS:
159 accepted = ", ".join(sorted(SUPPORTED_KINDS))
160 _dbg("OC_CHAT.MAIN", func, f"VALIDATION_ERROR: kind {kind!r}", 3)
161 raise UnknownKindError(
162 f"unknown work-item kind {kind!r}; this release accepts: {accepted}"
163 )
164
165
166# ── Value types ──────────────────────────────────────────────────────────
167
168
169@dataclass(frozen=True)
171 """One unit of work: a kind, an id, and three free-text fields."""
172
173 #: ``"plan"`` today; checked by name in :func:`check_kind`.
174 kind: str
175 #: For a plan, the plan id ``NNN-stem``.
176 item_id: str
177 #: A free-text preference -- a model family, a harness name. NEVER a
178 #: roster letter; the letter model is retired and is not revived.
179 lane: str | None = None
180 #: The ``agent-NNN`` identity, once dispatched. Identity is
181 #: plan-AND-instance: a re-dispatch sets a second instance and the
182 #: log shows both.
183 instance: str | None = None
184 #: Free text for the operator.
185 note: str | None = None
186
187 def __post_init__(self) -> None:
188 """Refuse an unaccepted kind at construction, by name."""
190
191
192@dataclass(frozen=True)
194 """The queue's three buckets, each in its own order."""
195
196 pending: tuple[WorkItem, ...] = ()
197 in_progress: tuple[WorkItem, ...] = ()
198 completed: tuple[WorkItem, ...] = ()
199
200
201@dataclass(frozen=True)
203 """One recorded operation, in the shape the log stores."""
204
205 #: 1-based, monotonic within one backend's log.
206 seq: int
207 #: ISO 8601 with an explicit offset.
208 at: str
209 #: One of :data:`OP_NAMES`.
210 op: str
211 #: The item the operation was about.
212 item_id: str
213 #: Whatever the operation carried; never interpreted here.
214 payload: Mapping[str, object] = field(default_factory=dict)
215 #: Free text. NEVER signed here -- see the module contract.
216 actor: str = ""
217
218
219# ── The operation set ────────────────────────────────────────────────────
220
221
222@runtime_checkable
223class QueueBackend(Protocol):
224 """The six operations, defined once, implemented by both backends."""
225
226 def enqueue(self, item: WorkItem, position: int | None = None) -> QueueState:
227 """Put *item* into pending at *position*, or at the end."""
228
229 def move(self, item_id: str, position: int, *, reason: str) -> QueueState:
230 """Move a pending item to *position*, for a stated *reason*."""
231
232 def dequeue(self, item_id: str, *, instance: str, reason: str) -> QueueState:
233 """Take an item out of pending into progress, naming *instance*."""
234
235 def complete(self, item_id: str, *, reason: str) -> QueueState:
236 """Finish an item, for a stated *reason*."""
237
238 def status(self) -> QueueState:
239 """The three buckets as they stand."""
240
241 def history(self, item_id: str) -> tuple[QueueOp, ...]:
242 """Every recorded operation about *item_id*, oldest first."""
243
244
245__all__ = [
246 "KIND_PLAN",
247 "OP_COMPLETE",
248 "OP_DEQUEUE",
249 "OP_ENQUEUE",
250 "OP_MOVE",
251 "OP_NAMES",
252 "SUPPORTED_KINDS",
253 "BadPositionError",
254 "DuplicateItemError",
255 "QueueBackend",
256 "QueueBackendError",
257 "QueueError",
258 "QueueOp",
259 "QueueState",
260 "UnknownItemError",
261 "UnknownKindError",
262 "WorkItem",
263 "check_kind",
264]
QueueState enqueue(self, WorkItem item, int|None position=None)
QueueState dequeue(self, str item_id, *, str instance, str reason)
QueueState move(self, str item_id, int position, *, str reason)
tuple[QueueOp,...] history(self, str item_id)
QueueState complete(self, str item_id, *, str reason)
None check_kind(str kind)