mirror of
https://github.com/kevinveenbirkenbach/computer-playbook.git
synced 2026-11-06 02:54:26 +00:00
A sweep may deploy 210 rows (available) in chunks of 80. Three declared blocks hold exactly that when there is no priority line, but priority rows get a chunk of their own that is never topped up with regular rows. Nine priority rows therefore occupied a whole block, left the regular rows 2 x 80 = 160, and 42 rows of budget went unused. Adding a block to the YAML alone would not have changed that: matrix.chunks_of handed the split slots.chunk_count(), which is min(ceil(available / chunk size), INFINITO_CI_MAX_CHUNKS) and ignores the priority seam, so the plan still stopped at three blocks. call-orchestrator.yml now declares test-deploy-chunk-3, gated on chunks 0 to 2 like every chunk gates on all before it, and report-main-failures and done need it. INFINITO_CI_MAX_CHUNKS is 4, one above what the budget fills, and slots.chunk_blocks() returns it. chunks_of hands the split every declared block; available still caps the rows, and a chunk whose slice comes back empty skips its deploy. chunk_count() stays the "chunks filled" figure of the budget report. Tests: the split spends 9 + 80 + 80 + 41 = 210 rows with a spare block; chunks_of plans three chunks from chunk_blocks=3 while chunk_count=2 (the previous wiring yields [1, 10]); an integration guard requires the declared chunk jobs to equal INFINITO_CI_MAX_CHUNKS and to exceed ceil(available / chunk size) by one, and fails on the stale value 3. A shell that inherited INFINITO_CI_MAX_CHUNKS=3 keeps failing that guard until it is restarted; make dotenv-force rewrites .env. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
81 lines
3.4 KiB
Python
81 lines
3.4 KiB
Python
"""Split the ordered discovery rows into the serial chunks of one sweep.
|
|
|
|
A *sweep* is one orchestrator run. It deploys its rows in serial chunk
|
|
blocks, each small enough that its last runner wave still starts inside the
|
|
queue window (:mod:`cli.meta.ci.slots` sizes them). Two rules shape the split:
|
|
|
|
* **Priority first, never mixed.** Priority rows sort to the head of the list
|
|
and the split forces a chunk boundary at the priority/regular seam, so a
|
|
chunk is either all priority or all regular. The seam chunk stays short
|
|
rather than being topped up with regular rows -- that is what guarantees
|
|
every priority row is done before the first regular one starts. Priority is
|
|
never subject to the sweep offset: it leads every sweep.
|
|
|
|
* **The head leads unless an operator says otherwise.** Chunk 0 starts at the
|
|
top of the ranking. The list is ordered coverage-first, so its head is what a
|
|
run most needs proven, and a sweep that cannot afford the whole list leaves
|
|
the tail behind rather than sliding a window over it.
|
|
|
|
``offset`` moves that start deliberately: it is how an operator reaches the
|
|
tail without waiting for anything. It defaults to 0, so every run that does
|
|
not ask for it deploys the same head and can be compared against the last
|
|
one. This used to be derived from the sweep number instead, which bought tail
|
|
coverage at the price of the head -- chunk 0 began wherever the arithmetic
|
|
pointed, the rows that matter most ran only every few sweeps, and no two runs
|
|
were comparable.
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
from typing import TYPE_CHECKING
|
|
|
|
if TYPE_CHECKING:
|
|
from collections.abc import Sequence
|
|
from typing import TypeVar
|
|
|
|
T = TypeVar("T")
|
|
|
|
|
|
def slice_chunks(rows: Sequence[T], size: int) -> list[list[T]]:
|
|
"""Cut *rows* into consecutive blocks of at most *size*."""
|
|
if size < 1:
|
|
raise ValueError(f"chunk size must be >= 1, got {size}")
|
|
return [list(rows[start : start + size]) for start in range(0, len(rows), size)]
|
|
|
|
|
|
def plan(
|
|
priority: Sequence[T],
|
|
regular: Sequence[T],
|
|
*,
|
|
size: int,
|
|
blocks: int,
|
|
budget: int,
|
|
offset: int = 0,
|
|
) -> list[list[T]]:
|
|
"""The chunks one sweep deploys, priority blocks first.
|
|
|
|
Args:
|
|
priority: rows of the priority line, in query order.
|
|
regular: rows of the regular line, in query order.
|
|
size: rows one chunk may hold (``slots.chunk_size``).
|
|
blocks: chunk blocks the workflow declares (``slots.chunk_blocks``).
|
|
budget: rows the run job cap allows in total (``slots.available``).
|
|
offset: how many regular rows to skip before filling the chunks. ``0``
|
|
(the default) starts at the head of the ranking. A value past the
|
|
end of the list leaves the regular chunks empty rather than
|
|
wrapping: an operator who asks for a window beyond the tail gets an
|
|
empty run, not a silent restart at the top.
|
|
|
|
Returns:
|
|
at most *blocks* chunks, none larger than *size*, together holding at
|
|
most *budget* rows. Priority chunks come first and are never topped up
|
|
with regular rows.
|
|
"""
|
|
priority_chunks = slice_chunks(priority[:budget], size)[:blocks]
|
|
spent = sum(len(chunk) for chunk in priority_chunks)
|
|
capacity = max(min((blocks - len(priority_chunks)) * size, budget - spent), 0)
|
|
if not capacity or not regular:
|
|
return priority_chunks
|
|
start = max(offset, 0)
|
|
return priority_chunks + slice_chunks(regular[start : start + capacity], size)
|