Skip to content

Automatic reduction is a batch reduced as its files arrive - #196

Open
SimonHeybrock wants to merge 7 commits into
mainfrom
184-automatic-as-batch
Open

SimonHeybrock wants to merge 7 commits into
mainfrom
184-automatic-as-batch

Conversation

@SimonHeybrock

Copy link
Copy Markdown
Member

Step 4 of #184, first part: automatic reduction and batch reduction are one model. ess.apps.rules (Rule, RuleStatus, TriggerLoop) is removed. Automatic reduction is a Batch that ess.apps.batch.auto.Automatic reduces as its files arrive.

auto = Automatic(iofq_batch(client, label='auto/iofq'), at='sample', settings=lambda: layers('iofq'))
auto.step()      # scans, and reduces each row with a new file at the trigger column; records by key
auto.refused     # why each row with a new file did not run, by key
run([auto], interval=10.0)

Why

Rules predate the batch. A rule's submit(client, file) did three things in one function per file: pair the file with its partners, decide whether the file was handled, and submit. The batch has the first as a row source (a function from the files to rows, tested on plain data) and the third as Batch.reduce. So a technique wrote its pairing twice, once as a row source and once as a rule function, and automatic reduction had none of what the batch gives: a state per row that says why a run waits, a stage for what the rows share, and the views.

Which rows run

A person and the automatic reduction differ in one predicate over the same join of rows with the records under the batch's label:

A row runs if no result failed or cancelled a setting or another cell changed a new file at the trigger column
a view, "reduce remaining" yes yes yes (stale) yes
essapps-batch reduce --remaining yes yes no no
Automatic yes no no yes

Automatic reads which files the records of each row hold at the trigger column from the proposal's changes, so a restart needs nothing on disk. Records of any spec and any submitter count. A new label has no records, so it reduces from the first file.

What behaves differently from rules

  • A chain (a sample row that references the output of the transmission row) is two batches, the second with a source that reads the state of the first. It was one function call.
  • A row with a new file whose record is still running waits, and is submitted when that record finishes. Rules submitted a second record next to the running one.
  • One refused row does not hold back the others: Automatic drops the rows a refusal names and submits the rest. Batch.reduce stays all or none for a person.
  • A step that raises (the source, the settings, or the submission) loses nothing: what runs is worked out from the records at every step, and the refused rows only suppress a retry until their cells, their records, or the settings change.
  • A cell at the trigger column must hold the identity of its file, as a source gets it in FileInfo.ref. A cell that names a file, such as file(run=1), would never match what its record holds, and a client has no call to resolve the name. step raises ValueError for such a row after its first record, and at every later step before it submits. This takes the place of the check that a rule's record settles its file.

Changes to the batch

  • Batch.state(settings=None, keys=None) works out the state of chosen rows, so that reduce does not work out every row. It still reads every record under the label.
  • state.results does not filter by submitter: one folder or one service holds one history.
  • An unknown record in a cell affects the state of its own row only.

Cost

The requirement, from Simon: some instruments will write a file every few seconds, so 10 000 to 100 000 files per proposal. One step with one new file, toy workflow, fake file source, on a shared machine:

runs reduced best of ten mean of ten of which listing the files
3 000 15 ms 30 ms 5 ms
30 000 214 ms 401 ms 82 ms
100 000 822 ms 1.4 s 324 ms

A step is linear in the number of files: it lists them, runs the source over them, and checks each row against the files its records hold. TriggerLoop was linear too (272 ms at 100 000), since it listed all files at each step. A step costs the same for one new file or fifty, so the interval of run sets the load. The mean is about twice the best because of Python's garbage collector.

docs/developer/automatic-reduction.md lists what is open and not built: a file source that reports what is new since a position; two ways to make a step cost less than linear (a group per file that the source's author declares, or a source that makes one row by queries on an index that records what each row asked for); a range in Selector to start a new label at a given run; running old rows again for a new spec version; reading the records of chosen rows only. It also says why the history is not cut off by age or count to bound the cost.

Known limits

  • A view reads Batch.state, which counts results of the batch's spec version only, while Automatic counts any record under the label. After a new spec version a view would show old rows as not reduced while Automatic holds them. Related to What a batch is: how its definition is tied to its records #191.
  • Batch keeps each submitted request in memory for "stale" (What a batch is: how its definition is tied to its records #191), so a long-running Automatic holds one request per row.
  • A source that reads another batch's state() works out every row of that batch at each step.
  • Not measured: a folder workspace, a restart at 100 000 records, memory. run has no test.

Review

A fresh reviewer probed the branch and mutated the code. Fixed from it: a step that raised lost its rows until a restart; a trigger cell that named a file was submitted at every step; cost statements that were false; state(keys=...) differing from state() for a row; documented behaviours with no test that failed when they broke.

Remaining in step 4 of #184: one batch presenter, and one plot contract for technique packages (#149).

🤖 Generated with Claude Code

SimonHeybrock and others added 7 commits November 13, 2026 11:38
`Batch.state(settings, keys)` reads the records and statuses of the rows of
`keys` only, and `reduce`, `tweak`, and `cancel` use it, so that submitting a
few rows of a batch of 100k rows no longer works out the state of every row.
A table without edits returns the scan's rows as they are, without a copy per
row.

A row's result is the newest record of the batch's spec, label, and the row's
key, whoever submitted it: one folder or one service holds one history.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
`ess.apps.batch.auto.Automatic(batch, at=, settings=)` steps one batch: it
scans, and reduces each row whose cell at the trigger column `at` holds a file
that no record under the batch's label with the row's key holds at `at`. So a
failed record, a changed setting, or a new partner does not reduce a row
again, and a new file in a list at `at` does. Which files the records hold is
read from the proposal's changes, so a restart needs nothing on disk. A row
that is refused does not hold back the others, and `refused` says why. A step
looks at the rows whose cells or records changed, and at the refused rows when
the settings change. `run(autos, interval)` steps a list of them in order, so
that a batch whose source reads another's results comes after it.

`ess.apps.rules` (`Rule`, `RuleStatus`, `TriggerLoop`) is removed: a technique
writes its pairing once, as the source of a batch. The stories E and the test
of a moved folder use the batch. The check that a file at a list parameter is
refused moves to the tests of requests.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
automatic-reduction.md describes the loop over batches: which rows run and
why, what the loop keeps, refusals, chains across batches, the cost of a
step, and what is open (new files since a position, a group per file, a
range in Selector, old rows reduced again for a new spec version).

The batch guide gains a section on the automatic use, with the table of
which rows run for "reduce remaining", `essapps-batch reduce --remaining`,
and the loop, and says that a source may read another batch's state. A row's
result no longer depends on the submitter.

Stories E, the README, deployments, the ADRs, the prior-art notes, the
client guide, and the tours name the batch where they named rules and the
trigger loop.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A step worked out which rows to look at from what changed since the last
step, and stored that before it submitted, so an exception in the settings,
the source, or a submission left a row unreduced until it changed again. A
step now checks every row's cell at the trigger column against the files the
records hold, which come from the records only. The rows refused are kept as
they were then, and are tried again once their cells, their records, or the
settings change. The check reads the cell directly, a file or a list of
files, which costs less than comparing the rows with those of the last step.

`Automatic` refuses a batch without a source. `Batch.state(keys=...)` gives
the same state of a row as `Batch.state()` when a cell references an unknown
record: only the rows that reference it lose the status of the records they
read. A row given with `EMPTY` in its values has that cell empty.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Two file references are equal only when their identity strings are, so the
files held at the trigger column are kept as those strings. Checking a row
then hashes a string instead of a pydantic model.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
automatic-reduction.md states the scale that the cost is measured against,
how the interval of `run` sets the load, why the history is not cut off by
age or count, and what a step keeps now that it checks every row. It no
longer says that the state of chosen rows reads only their records. Open
gains: reading the records of chosen members, a source that makes a row from
one file by queries on an index of the files, the files held at the trigger
column for a page, and a trigger cell that names a file by run number.

The batch guide calls an `Automatic` the loop, keeps "automatic reduction"
for the activity, and says that which rows are reduced is defined by the
records under the label only, so a new label reduces from the first file
and a row whose key changes is reduced again.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A cell at the trigger column that names a file, such as `file(run=1)`, is
resolved by the workspace, so its record holds the file's identity and never
counts as holding the cell: the row was reduced at every step. A client has
no call that resolves a file name, so the loop checks, for the rows it
submitted, that each record holds the files of its row's cell, and raises
`ValueError` naming the row if one does not. Every later step raises too,
before it submits anything, until that cell changes.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant