|
21 | 21 |
|
22 | 22 | import datetime |
23 | 23 | import sys |
| 24 | +import threading |
| 25 | +from collections import deque |
24 | 26 | from pathlib import Path |
25 | 27 |
|
26 | | -from . import driver, leaves, publish, queue, signoff, state |
| 28 | +from . import driver, lane, leaves, publish, queue, signoff, state |
27 | 29 | from .config import Config |
28 | 30 |
|
29 | 31 |
|
@@ -166,6 +168,53 @@ def flow( |
166 | 168 | return final |
167 | 169 |
|
168 | 170 |
|
| 171 | +# ---------------------------------------------------------------------------- |
| 172 | +# The unattended band: advance every bundle through Do + Check (docs 09). Serial by |
| 173 | +# default; a worker pool of cfg.lanes lanes when configured (PDCA_LANES / [driver].lanes). |
| 174 | +# ---------------------------------------------------------------------------- |
| 175 | +def _build_all(cfg: Config, bundles: list[Path]) -> None: |
| 176 | + """Drive each bundle through the unattended Do+Check band to AWAITING_SIGNOFF / COMPLETE. |
| 177 | +
|
| 178 | + ``cfg.lanes <= 1`` keeps the original strictly-serial loop (Plan-if-unplanned then |
| 179 | + drive, per bundle). With ``cfg.lanes > 1`` the *drive* fans out across a worker pool: |
| 180 | + a serial Plan pre-pass runs first (an ``iterate-plan`` may have re-opened a bundle to |
| 181 | + UNPLANNED, and the Plan leaf is **interactive** — it must never enter the pool), then |
| 182 | + ``min(lanes, len(bundles))`` worker threads, each pinned to a fixed lane slot for its |
| 183 | + lifetime (so only ``lanes`` lane-scoped checkouts/runners are ever needed), pull |
| 184 | + bundles off a shared queue and run the unattended ``driver.run_issue``. |
| 185 | + """ |
| 186 | + if cfg.lanes <= 1 or len(bundles) <= 1: |
| 187 | + for d in bundles: |
| 188 | + def _build(d=d): |
| 189 | + _plan_if_unplanned(cfg, d, None) # iterate-plan may have re-opened it |
| 190 | + driver.run_issue(d, cfg) |
| 191 | + _isolate(d, "build/check", _build) |
| 192 | + return |
| 193 | + |
| 194 | + # Serial Plan pre-pass — the interactive Plan beat stays out of the pool. |
| 195 | + for d in bundles: |
| 196 | + _isolate(d, "plan", lambda d=d: _plan_if_unplanned(cfg, d, None)) |
| 197 | + # Pooled drive — fixed lane slot per worker; gates read it via lane.current(). |
| 198 | + work = deque(bundles) |
| 199 | + lock = threading.Lock() |
| 200 | + |
| 201 | + def worker(slot: int) -> None: |
| 202 | + lane.set_current(slot) |
| 203 | + while True: |
| 204 | + with lock: |
| 205 | + if not work: |
| 206 | + return |
| 207 | + d = work.popleft() |
| 208 | + _isolate(d, "build/check", lambda d=d: driver.run_issue(d, cfg)) |
| 209 | + |
| 210 | + threads = [threading.Thread(target=worker, args=(k,), name=f"pdca-lane{k}") |
| 211 | + for k in range(min(cfg.lanes, len(bundles)))] |
| 212 | + for t in threads: |
| 213 | + t.start() |
| 214 | + for t in threads: |
| 215 | + t.join() |
| 216 | + |
| 217 | + |
169 | 218 | # ---------------------------------------------------------------------------- |
170 | 219 | # Shared multi-bundle driver: build all → cheap-first sign-off → publish → Act once. |
171 | 220 | # ---------------------------------------------------------------------------- |
@@ -193,11 +242,8 @@ def _drive_and_act( |
193 | 242 | # Build-all (unattended): advance each bundle to AWAITING_SIGNOFF / COMPLETE. |
194 | 243 | # Each bundle is isolated — one that raises (a leaf left it half-written) is |
195 | 244 | # skipped this pass, never crashing the sweep and losing the others' progress. |
196 | | - for d in bundles: |
197 | | - def _build(d=d): |
198 | | - _plan_if_unplanned(cfg, d, None) # iterate-plan may have re-opened it |
199 | | - driver.run_issue(d, cfg) |
200 | | - _isolate(d, "build/check", _build) |
| 245 | + # Serial by default; fans out across cfg.lanes lanes when configured (docs 09). |
| 246 | + _build_all(cfg, bundles) |
201 | 247 | # Sign-off, cheap-first, restricted to this batch. ONE interactive session |
202 | 248 | # per chunk (≤ SIGNOFF_BATCH_SIZE) walks several bundles — like batch Plan — |
203 | 249 | # then every decision is recorded FIRST (apply_now=False) so an iterate-do |
|
0 commit comments