The window worker: group leases, the stream serving the corpus, the snapshot through the API (design 1 and 5)
Test and deploy / test (push) Successful in 55s
Test and deploy / edge (push) Successful in 2m9s
Test and deploy / deploy (push) Successful in 1m16s

A worker thread leases a group and takes its ranges in article order through
one carried window; the coordinator keeps one cursor a group (group_leases,
migration 6500) and hands out the next range, the rules and nothing about
where the chunks are: the range stream serves a stored range's chunks from the
corpus file and a live one's from the spool. The window's snapshot (bincode:
the frontier, the open files, releases and albums, the upload numbers, the
batch-counter table) goes to the coordinator every EDGE_SNAPSHOT_MINUTES and
when the group is given back, and the next holder restores it; a takeover
after LEASE_TTL_SECONDS plans the ranges from the snapshot's article again.
The prefetcher runs ahead of held groups' cursors and skips chunks the corpus
has. LEASE_MIN_PRIORITY..LEASE_MAX_PRIORITY bound what is leased. edge-node
holds groups by default; --ranges keeps the old per-range path.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
This commit is contained in:
Daan MeijerandClaude Fable 5.1 committed 2026-10-26 18:22:18 +01:00
1 parent 3c4fe324a1
commit 1f6e3316ec
19 files changed
+1358 -31

No files matched your search

+56 -5
View File
@@ -409,6 +409,31 @@ releases, a third of them one poster's image dump (27.7 M rows, LOG.md
consecutive ranges of a.b.ath (2016): one range is 15 hours of posting,
so without the carry no release would finish inside a range (LOG.md
2026-10-26 (111)).
**The group lease (2026-10-26, the user's question in LOG.md (117)):**
a worker thread leases a *group*, not a range, and asks the coordinator
for the group's next range each time it finished one; the coordinator
keeps one cursor a group (`group_leases`: the holder, its heartbeat,
the next range in article order, the snapshot it stands at). The
answer is the range (id, span, resume point), the rules version and the
stream URL — nothing about where the chunks are: the stream serves a
stored range's chunks from the corpus file and a live one's from the
spool, so a worker needs no mount and no corpus layout, and a remote
worker is the same binary with another URL. The overhead is a copy
through the node API: the corpus is ~30 B an article compressed (2016;
16–18 for 2021 and 2025), eight local workers at the bench's rate are
~170 MB/s, which the HDD under the corpus bounds either way; a remote
worker is bounded by its link (100 Mbit ≈ 400k articles/s). History
therefore no longer waits for the corpus fill: a group's stored ranges
are read from disk, an unstored one is fetched live and captured into
the corpus on the way, and the prefetcher runs ahead of each held
group's cursor (plus the capture work left). A group with no planned
range left is the tail: the worker keeps the group and asks again
every minute, the planner's tail ranges fall into its order. A lease
whose holder went quiet for LEASE_TTL_SECONDS is taken over: the new
holder downloads the group's snapshot (5) and redoes the ranges since
it; the batch store's keys make the redo idempotent. The replay
harness (8) stays for offline runs; the driver on homelab goes once the
worker runs.
2. **The window.** The worker reads a block in article order and keeps its
open files and releases in memory (counters, names, sizes; the segment
@@ -490,8 +515,31 @@ releases, a third of them one poster's image dump (27.7 M rows, LOG.md
of writes, so the state is snapshotted at a lease's end and on a timer,
to a file on bulk, and a crash redoes the ranges since the last snapshot
from the corpus or the prefetch cache: the deterministic keys make the
redo idempotent. Not built yet; the replay harness (8) keeps a group's
window in one process.
redo idempotent. **The snapshot goes through the API** (2026-10-26):
`PUT /node/groups/:id/snapshot` with the serialized window (the
frontier, the open files and releases, the open albums, the upload
numbers, the batch-counter table; gzip) and the article it stands at
(the last batch's `next`), kept by the coordinator on bulk as
`<snapshot dir>/<group id>`; `GET` gives it back to the next holder. A
worker snapshots when it gives a group back (a stop) and every
SNAPSHOT_MINUTES while it holds one, always right after a batch was
stored, so the snapshot's article is a resume point. The replay
harness (8) keeps a group's window in one process without one.
**Built 2026-10-26** (LOG.md (117)): migration 6500 `group_leases`;
`app/services/group_leases.ts` (lease by the most urgent planned
range within LEASE_MIN_PRIORITY..LEASE_MAX_PRIORITY, the longest free
first; takeover after LEASE_TTL_SECONDS, the ranges from the snapshot's
article on planned again; next; heartbeat; release; the snapshot
streamed to `SNAPSHOT_DIR/<group id>.gz`); the node API's
`groups/lease`, `groups/:id/next|heartbeat|release|snapshot`; the
range stream serving a stored range's chunks from the corpus; the
prefetcher ahead of held groups' cursors, skipping chunks the corpus
has; `RangeIndexer::for_group/begin_range/snapshot/restore` (bincode,
the files a table, SNAPSHOT_VERSION 1); `edge-node` holding groups by
default (`--ranges` keeps the old path). Tests: the lease order,
takeover and redo, the endpoints with a corpus-served stream and the
snapshot through the API, the snapshot restoring a window between two
ranges (the two-range release one, the album joined).
6. **The corpus stays as it is.** Measured on 300 unfiltered chunks
(30 M lines, LOG.md (102)): 43% of lines lack a "(k/N)" counter, and of
@@ -701,9 +749,12 @@ working on it as it fills; the scratch run's numbers (63 groups) were
read before (LOG.md (115)). The tail waits for the window worker.
Next: the replay on homelab over the 2016 window (the NVMe bytes per
header for real), the ordered range leases with the carried window in
the edge-node and its snapshot (design 1 and 5), the prober writing
`release_texts` kind 2, the spot/NZB match on pending keys (decision f). Gone from the list: block
header for real), the window worker (design 1 and 5: group leases, the
stream serving stored chunks, the snapshot through the API; being built,
LOG.md (117)), the prober writing `release_texts` kind 2, the spot/NZB
match on pending keys (decision f), the tail's provisional writes at a
range end (the window's open releases shown while they upload: the files
sent provisionally marked, the finish sending the rest). Gone from the list: block
leases, the planner and the block merger (design 1, amended).
## Decisions