Replay harness: a --to bound and the homelab window driver
Test and deploy / test (push) Successful in 52s
Test and deploy / edge (push) Successful in 2m7s
Test and deploy / deploy (push) Skipped

edge-bench --group and replay:corpus take --to <article> next to --from, so a
replay covers one window of a group's history while the corpus holds others
(the 2016 ranges next to 2021's and 2025's). deploy/replay-window.sh runs a
window on homelab: every group whose window ranges are all stored is replayed
once into usenet_replay through the voyager-3 app image, 4 groups at a time,
looping while the corpus fill completes the rest.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
This commit is contained in:
Daan MeijerandClaude Fable 5.1 committed 2026-10-26 07:56:39 +01:00
1 parent 72e973d006
commit ae735361a0
5 files changed
+99 -12

No files matched your search

+11 -5
View File
@@ -628,7 +628,7 @@ window holds a day of a group's segments in memory (the busiest group's
### The replay harness (built 2026-10-26) ### The replay harness (built 2026-10-26)
`node ace replay:corpus --group <name> [--dir CORPUS_DIR] [--from article] `node ace replay:corpus --group <name> [--dir CORPUS_DIR] [--from article]
[--limit ranges] [--batch-chunks 5] [--no-finish]` (commands/ [--to article] [--limit ranges] [--batch-chunks 5] [--no-finish]` (commands/
replay_corpus.ts; design 8): a group's complete corpus ranges in article replay_corpus.ts; design 8): a group's complete corpus ranges in article
order through one window in the Rust edge (`edge-bench --group <dir> order through one window in the Rust edge (`edge-bench --group <dir>
--rules rules.json --wire -`: the window carried from range to range, the --rules rules.json --wire -`: the window carried from range to range, the
@@ -646,10 +646,16 @@ headers; bytes per header of the NVMe tables (releases, release_texts,
posters, group_sets, rule counts and samples) and of those on posters, group_sets, rule counts and samples) and of those on
CHUNK_TABLESPACE (release_chunks, probe_candidates, release_inspections). CHUNK_TABLESPACE (release_chunks, probe_candidates, release_inspections).
The app image carries `edge-bench` for it (Dockerfile); on homelab the The app image carries `edge-bench` for it (Dockerfile); on homelab the
scratch catalog is a second database on bulk (`usenet_replay`, migrated scratch catalog is a second database (`usenet_replay`, default tablespace
with the voyager-3 image, `groups` and `rule_sets` copied over), the `nvme` like the catalog, the chunk tables on `pg_default`; migrated with
corpus mounted read-only: not run there yet (LOG.md 2026-10-26 (111) has the voyager-3 image built from the deploy cache's repo, `groups` and
the first local numbers). `rule_sets` copied over), the corpus mounted read-only.
`deploy/replay-window.sh` runs a window there: each group whose window
ranges are all stored is replayed once with the window's bounds as
`--from`/`--to` (the corpus holds other windows of the same group), 4
groups at a time, logs and `.done` markers in `.cache/replay/`, looping
until the window is done (LOG.md 2026-10-26 (111) has the first local
numbers, (112) the homelab run).
Next: the replay on homelab over the 2016 window (the NVMe bytes per Next: the replay on homelab over the 2016 window (the NVMe bytes per
header for real), albums in the edge (design 3), the ordered range leases header for real), albums in the edge (design 3), the ordered range leases
+10 -3
View File
@@ -31,7 +31,7 @@ interface StoredRange {
* at its resume point, the range finished when its last batch is in. Then a report: releases finished, complete, * at its resume point, the range finished when its last batch is in. Then a report: releases finished, complete,
* partial and provisional, and the catalog's bytes per header (the NVMe tables apart from those on CHUNK_TABLESPACE). * partial and provisional, and the catalog's bytes per header (the NVMe tables apart from those on CHUNK_TABLESPACE).
* *
* node ace replay:corpus --group alt.binaries.ath [--dir CORPUS_DIR] [--limit N] [--batch-chunks 5] * node ace replay:corpus --group alt.binaries.ath [--dir CORPUS_DIR] [--from A] [--to B] [--limit N] [--batch-chunks 5]
* *
* Refuses a database not named *_replay or *_scratch. The rules are the catalog's current rule set * Refuses a database not named *_replay or *_scratch. The rules are the catalog's current rule set
* (rule_sets, the groups' priorities): copy `groups` and `rule_sets` from the catalog for a faithful replay. * (rule_sets, the groups' priorities): copy `groups` and `rule_sets` from the catalog for a faithful replay.
@@ -54,6 +54,11 @@ export default class ReplayCorpus extends BaseCommand {
@flags.number({ description: 'Skip ranges ending before this article (continue a replay)' }) @flags.number({ description: 'Skip ranges ending before this article (continue a replay)' })
declare from?: number declare from?: number
@flags.number({
description: "Skip ranges starting after this article (one window of the group's history)",
})
declare to?: number
@flags.number({ @flags.number({
description: 'Chunks of 100k articles per batch, as the worker posts them', description: 'Chunks of 100k articles per batch, as the worker posts them',
default: 5, default: 5,
@@ -112,7 +117,7 @@ export default class ReplayCorpus extends BaseCommand {
`${this.group}: ${ranges.length} ranges (${ranges[0].start}–${ranges[ranges.length - 1].end}) through ${bench}, rules v${rules.version}, into ${database}.` `${this.group}: ${ranges.length} ranges (${ranges[0].start}–${ranges[ranges.length - 1].end}) through ${bench}, rules v${rules.version}, into ${database}.`
) )
// edge-bench walks the group directory in article order itself; --from and --limit cut its list as ours. // edge-bench walks the group directory in article order itself; --from, --to and --limit cut its list as ours.
const args = [ const args = [
'--group', '--group',
groupDir, groupDir,
@@ -124,6 +129,7 @@ export default class ReplayCorpus extends BaseCommand {
'-', '-',
] ]
if (this.from) args.push('--from', String(this.from)) if (this.from) args.push('--from', String(this.from))
if (this.to) args.push('--to', String(this.to))
if (this.limit) args.push('--limit', String(this.limit)) if (this.limit) args.push('--limit', String(this.limit))
if (this.noFinish) args.push('--no-finish') if (this.noFinish) args.push('--no-finish')
const started = Date.now() const started = Date.now()
@@ -215,7 +221,7 @@ export default class ReplayCorpus extends BaseCommand {
return null return null
} }
/** The group directory's complete ranges (a range.json), in article order, cut by --from and --limit. */ /** The group directory's complete ranges (a range.json), in article order, cut by --from, --to and --limit. */
private async storedRanges(groupDir: string): Promise<{ start: number; end: number }[]> { private async storedRanges(groupDir: string): Promise<{ start: number; end: number }[]> {
const names = await readdir(groupDir).catch(() => [] as string[]) const names = await readdir(groupDir).catch(() => [] as string[])
const ranges: { start: number; end: number }[] = [] const ranges: { start: number; end: number }[] = []
@@ -228,6 +234,7 @@ export default class ReplayCorpus extends BaseCommand {
} }
ranges.sort((a, b) => a.start - b.start) ranges.sort((a, b) => a.start - b.start)
let chosen = this.from ? ranges.filter((r) => r.end >= this.from!) : ranges let chosen = this.from ? ranges.filter((r) => r.end >= this.from!) : ranges
if (this.to) chosen = chosen.filter((r) => r.start <= this.to!)
if (this.limit) chosen = chosen.slice(0, this.limit) if (this.limit) chosen = chosen.slice(0, this.limit)
return chosen return chosen
} }
+69
View File
@@ -0,0 +1,69 @@
#!/bin/bash
# A corpus window replayed on homelab (REBUILD.md, "The replay harness"): every group whose window ranges (the
# ranges marked `capture` at PRIORITY in the catalog, 0 for 2016) are all stored in the corpus is replayed once
# into the scratch catalog usenet_replay through the voyager-3 app image, PARALLEL groups at a time, the
# window's bounds passed as --from/--to (the corpus holds other windows of the same group). The group's log
# ends in .cache/replay/<group>.log, a .done marker next to it (.failed when the replay failed: remove it to
# retry). Loops until every group of the window is done (the corpus fill runs meanwhile), so run it detached
# from the deployment root:
# setsid nohup deploy/replay-window.sh > .cache/replay/driver.log 2>&1 &
# No new group starts while the nightly pause is on (.cache/paused); the running ones finish.
set -u
ROOT=${ROOT:-/mnt/bulk/usenet-rebuild}
PRIORITY=${PRIORITY:-0}
PARALLEL=${PARALLEL:-4}
MEMORY=${MEMORY:-12g}
IMAGE=${IMAGE:-usenet-voyager3:latest}
OUT=$ROOT/.cache/replay
mkdir -p "$OUT"
cd "$ROOT" || exit 1
say() { echo "$(date -Is) $*"; }
catalog() { docker compose exec -T postgres psql -U rebuild -d usenet_rebuild -X -q -At -F ' ' -c "$1"; }
# The window's groups: name, first article, last article, whether every range is stored; smallest first.
window() {
catalog "SELECT g.name, min(r.start_id), max(r.end_id), (count(*) = count(r.stored_at))::int
FROM ranges r JOIN groups g ON g.id = r.group_id
WHERE r.capture AND r.priority = $PRIORITY
GROUP BY g.name ORDER BY sum(r.end_id - r.start_id + 1)"
}
replay() {
local group=$1 from=$2 to=$3
say "$group: replaying $from–$to"
if docker run --rm --name "replay-${group//./-}" --network usenet-rebuild --env-file .env \
-e NODE_ENV=production -e DB_HOST=postgres -e DB_PORT=5432 -e DB_DATABASE=usenet_replay -e CORPUS_DIR=/corpus \
-v "$ROOT/corpus:/corpus:ro" --memory "$MEMORY" "$IMAGE" \
node ace replay:corpus --group "$group" --from "$from" --to "$to" > "$OUT/$group.log" 2>&1; then
touch "$OUT/$group.done"
say "$group: done ($(grep -o 'Releases [0-9,]*' "$OUT/$group.log" | tail -1))"
else
touch "$OUT/$group.failed"
say "$group: FAILED (see $OUT/$group.log)"
fi
rm -f "$OUT/$group.running"
}
while true; do
todo=0
while read -r group from to complete; do
[ -n "$group" ] || continue
[ -e "$OUT/$group.done" ] && continue
[ -e "$OUT/$group.failed" ] && continue
todo=$((todo + 1))
[ "$complete" = 1 ] || continue
[ -e "$OUT/$group.running" ] && continue
[ -e "$ROOT/.cache/paused" ] && continue
while [ "$(find "$OUT" -name '*.running' | wc -l)" -ge "$PARALLEL" ]; do sleep 30; done
touch "$OUT/$group.running"
replay "$group" "$from" "$to" &
sleep 5
done <<< "$(window)"
if [ "$todo" = 0 ]; then
wait
say "every group of the window at priority $PRIORITY is replayed"
exit 0
fi
sleep 300
done
+2 -1
View File
@@ -122,7 +122,8 @@ replay harness"):
```sh ```sh
./target/release/edge-bench --group /corpus/alt.binaries.ath --rules rules.json --batch-chunks 5 --wire - > batches.jsonl ./target/release/edge-bench --group /corpus/alt.binaries.ath --rules rules.json --batch-chunks 5 --wire - > batches.jsonl
# --from <article> skips ranges ending before it, --limit <n> stops after n ranges, --no-finish leaves the window open # --from <article> skips ranges ending before it, --to <article> ranges starting after it (one window of the
# group's history), --limit <n> stops after n ranges, --no-finish leaves the window open
``` ```
`--rules` takes the lease's rules JSON (`src/lease.rs`: the catalog's rule `--rules` takes the lease's rules JSON (`src/lease.rs`: the catalog's rule
+7 -3
View File
@@ -4,7 +4,7 @@
//! aggregate files, finish releases through the window, and every N chunks judge and encode them. `--dump` //! aggregate files, finish releases through the window, and every N chunks judge and encode them. `--dump`
//! writes every output as JSON lines; `--wire` the batches as the worker posts them (`-`: stdout). //! writes every output as JSON lines; `--wire` the batches as the worker posts them (`-`: stdout).
//! //!
//! edge-bench <range dir> | --group <group dir> [--from article] [--limit ranges] [--rules rules.json] //! edge-bench <range dir> | --group <group dir> [--from article] [--to article] [--limit ranges] [--rules rules.json]
//! [--batch-chunks 10] [--idle 5] [--no-finish] [--dump out.jsonl] [--wire batches.jsonl|-] [--releases out.jsonl] //! [--batch-chunks 10] [--idle 5] [--no-finish] [--dump out.jsonl] [--wire batches.jsonl|-] [--releases out.jsonl]
use base64::Engine; use base64::Engine;
@@ -211,7 +211,7 @@ const MAX_PENDING_FILES: usize = 100_000;
fn main() { fn main() {
let args: Vec<String> = std::env::args().collect(); let args: Vec<String> = std::env::args().collect();
let usage = "usage: edge-bench <range dir> | --group <group dir> [--from article] [--limit ranges] [--rules rules.json] [--batch-chunks N] [--idle N] [--no-finish] [--dump file] [--wire file|-] [--releases file]"; let usage = "usage: edge-bench <range dir> | --group <group dir> [--from article] [--to article] [--limit ranges] [--rules rules.json] [--batch-chunks N] [--idle N] [--no-finish] [--dump file] [--wire file|-] [--releases file]";
let flag = |name: &str| args.iter().position(|a| a == name).and_then(|i| args.get(i + 1)).cloned(); let flag = |name: &str| args.iter().position(|a| a == name).and_then(|i| args.get(i + 1)).cloned();
let has = |name: &str| args.iter().any(|a| a == name); let has = |name: &str| args.iter().any(|a| a == name);
let batch_chunks: usize = flag("--batch-chunks").map(|v| v.parse().unwrap()).unwrap_or(10); let batch_chunks: usize = flag("--batch-chunks").map(|v| v.parse().unwrap()).unwrap_or(10);
@@ -233,10 +233,14 @@ fn main() {
let (group, ranges) = match flag("--group") { let (group, ranges) = match flag("--group") {
Some(dir) => { Some(dir) => {
let (group, mut ranges) = ranges_in(&dir); let (group, mut ranges) = ranges_in(&dir);
// --from: ranges ending before it are skipped (a replay continued); --limit: this many at most. // --from: ranges ending before it are skipped (a replay continued); --to: ranges starting after it (one
// window of the group's history, the corpus holding others); --limit: this many at most.
if let Some(from) = flag("--from").map(|v| v.parse::<i64>().unwrap()) { if let Some(from) = flag("--from").map(|v| v.parse::<i64>().unwrap()) {
ranges.retain(|r| r.end >= from); ranges.retain(|r| r.end >= from);
} }
if let Some(to) = flag("--to").map(|v| v.parse::<i64>().unwrap()) {
ranges.retain(|r| r.start <= to);
}
if let Some(limit) = flag("--limit").map(|v| v.parse::<usize>().unwrap()) { if let Some(limit) = flag("--limit").map(|v| v.parse::<usize>().unwrap()) {
ranges.truncate(limit); ranges.truncate(limit);
} }