The window worker keeps its window over a chunk that doesn't inflate
A live reply cut short (the server's compressed data ending at ".\r\n" at a read boundary; the spool keeps what Corpus.write refuses) made the worker give the group back without a snapshot, losing the window and the batch since its last post (six times in the first 40 minutes on homelab). Now the batch before the chunk goes out, the chunk is reported (fetched again, plain the second time) and the range is asked for again from that chunk (RangeOutcome::Again): the stream waits for the new copy and the window stays. Test: the coordinator hands the same range back at its resume point after a corrupt report. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
This commit is contained in:
1 parent
1f6e3316ec
commit
29e0cc8f6e
4 files changed
+34
-8
No files matched your search
+5
-1
@@ -539,7 +539,11 @@ releases, a third of them one poster's image dump (27.7 M rows, LOG.md
|
||||
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).
|
||||
ranges (the two-range release one, the album joined). Deployed and
|
||||
started 2026-10-26 22:37 (LOG.md (118)). A chunk that doesn't inflate
|
||||
(a live reply cut short; the spool doesn't check) is reported after
|
||||
the batch before it went out and the range asked for again from that
|
||||
chunk: the window stays (the first run lost it, and the open batch).
|
||||
|
||||
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
|
||||
|
||||
+3
-1
@@ -256,7 +256,9 @@ COORDINATOR_URL=http://coordinator:3334 NODE_TOKEN=… ./target/release/edge-nod
|
||||
409 means the lease is lost (the range stops, isn't given back); a
|
||||
batch once 500k articles or 100k files wait (a picture group's batches
|
||||
went over the server's 256 MB limit); a chunk that doesn't inflate is
|
||||
reported (the coordinator fetches it plain next time); SIGTERM/SIGINT
|
||||
reported (the coordinator fetches it plain next time) after the batch
|
||||
before it went out, and a held group asks for the range again from
|
||||
that chunk, its window kept; SIGTERM/SIGINT
|
||||
give each range back after its current chunk; `--max-mbit` is the
|
||||
node's download cap, shared by its threads. Plus a heartbeat when no
|
||||
batch went out for a minute.
|
||||
|
||||
@@ -536,6 +536,9 @@ fn run_range(api: &Arc<Api>, lease: LeaseJson, config: &Config, stopping: &Atomi
|
||||
/// What one range of a held group came to.
|
||||
enum RangeOutcome {
|
||||
Done,
|
||||
/// A chunk didn't inflate: reported (the coordinator fetches it again, plain the second time) after the batch
|
||||
/// before it went out, and the range is asked for again from there; the window stays.
|
||||
Again,
|
||||
/// The group is given back (a stop, a lost lease, a failure): no more ranges.
|
||||
Stop,
|
||||
}
|
||||
@@ -626,17 +629,25 @@ fn run_group_range(api: &Arc<Api>, group_id: i64, group_name: &str, range: Range
|
||||
}
|
||||
}
|
||||
}
|
||||
// Stopped mid-range: the batch so far goes out, so the snapshot stands at a stored resume point.
|
||||
if !reached_end && corrupt.is_none() && failure.is_none() && post_error.lock().unwrap().is_none() && indexer.next() > batch_from {
|
||||
// Stopped mid-range (or at a chunk that doesn't inflate): the batch so far goes out, so the snapshot stands at
|
||||
// a stored resume point, and a chunk reported corrupt is read again from its own start.
|
||||
if !reached_end && failure.is_none() && post_error.lock().unwrap().is_none() && indexer.next() > batch_from {
|
||||
let _ = post_tx.send(indexer.take());
|
||||
}
|
||||
drop(post_tx);
|
||||
let _ = poster.join();
|
||||
let post_error = post_error.lock().unwrap().take();
|
||||
if let Some((from, to, e)) = corrupt {
|
||||
log("warn", &format!("{group_name} {}–{}: chunk {from}–{to} doesn't inflate ({e}); reported, the group goes back", range.start, range.end));
|
||||
let _ = api.corrupt(range.id, from);
|
||||
return RangeOutcome::Stop;
|
||||
// The server's compressed reply cut short (LOG.md 2026-10-11): the coordinator fetches the chunk again,
|
||||
// plain the second time, and sets the range aside after that; the window carries on either way.
|
||||
log("warn", &format!("{group_name} {}–{}: chunk {from}–{to} doesn't inflate ({e}); reported, the range is asked for again", range.start, range.end));
|
||||
if let Err(e) = api.corrupt(range.id, from) {
|
||||
log("warn", &format!("{group_name} {}–{}: reporting the chunk: {e}; the group goes back", range.start, range.end));
|
||||
return RangeOutcome::Stop;
|
||||
}
|
||||
if post_error.is_none() {
|
||||
return RangeOutcome::Again;
|
||||
}
|
||||
}
|
||||
match post_error {
|
||||
Some(ApiError::LeaseLost(m)) => {
|
||||
@@ -758,6 +769,8 @@ fn run_group(api: &Arc<Api>, lease: GroupLeaseJson, config: &Config, stopping: &
|
||||
}
|
||||
match run_group_range(api, group_id, &group_name, range, &mut indexer, config, stopping, &totals) {
|
||||
RangeOutcome::Done => ranges += 1,
|
||||
// The same range again from the reported chunk: the stream waits for its new copy.
|
||||
RangeOutcome::Again => {}
|
||||
RangeOutcome::Stop => break,
|
||||
}
|
||||
if snapshotted.elapsed() >= config.snapshot_every {
|
||||
|
||||
@@ -6,7 +6,7 @@ import { tmpdir } from 'node:os'
|
||||
import { join } from 'node:path'
|
||||
import coordinatorConfig from '#config/coordinator'
|
||||
import GroupLeases from '#services/group_leases'
|
||||
import { LeaseLost } from '#services/leases'
|
||||
import Leases, { LeaseLost } from '#services/leases'
|
||||
import { Readable } from 'node:stream'
|
||||
|
||||
async function node(name: string) {
|
||||
@@ -71,6 +71,13 @@ test.group('Group leases', (g) => {
|
||||
assert.equal((await GroupLeases.next(b, n1))?.range.id, b1)
|
||||
await db.from('ranges').where('id', b1).update({ status: 'done', next_id: 101 })
|
||||
assert.equal((await GroupLeases.next(b, n1))?.range.id, b2)
|
||||
// A chunk of b2 that doesn't inflate, reported after the batch before it: the range is asked for again and
|
||||
// comes back at its resume point (the chunk fetched plain next time).
|
||||
await db.from('ranges').where('id', b2).update({ next_id: 151 })
|
||||
await Leases.corrupt(b2, n1, 151)
|
||||
assert.include(await rangeRow(b2), { status: 'planned', node_id: null })
|
||||
assert.deepEqual((await GroupLeases.next(b, n1))?.range, { id: b2, start: 101, end: 200, next: 151 })
|
||||
assert.deepEqual((await rangeRow(b2)).plain_chunks.map(Number), [151])
|
||||
await db.from('ranges').where('id', b2).update({ status: 'done', next_id: 201 })
|
||||
assert.isNull(await GroupLeases.next(b, n1), 'caught up')
|
||||
|
||||
|
||||
Reference in new issue
Block a user