Files
computer-playbook/cli/meta/ci/chunks.py
T
kevinveenbirkenbachandClaude Opus 5 c32bf320ff feat(ci): add a fourth deploy chunk so a short priority chunk strands no budget
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>
2026-09-11 23:46:48 +02:00

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)