Commit Graph
22 Commits
Author SHA1 Message Date
Daan MeijerandClaude Opus 5.5 18b9ffdca3 tools: obf_runs and obf_kinds, measuring the obfuscated subjects
Test and deploy / test (push) Successful in 47s
Test and deploy / edge (push) Successful in 1m47s
Test and deploy / deploy (push) Successful in 1m24s
obf_runs: how obfuscated articles come in runs (strict, and per poster
and tool with a time gap). obf_kinds: what they are (unquoted subjects
with a counter, random tokens equal to the message-id, other random
tokens, ...), with examples. LOG.md 2026-10-11 (5).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-11 20:43:03 +02:00
Daan MeijerandClaude Opus 5.5 5109481d67 The Rust worker is the product: edge/, built and checked in CI
Test and deploy / test (push) Successful in 46s
Test and deploy / edge (push) Successful in 1m54s
Test and deploy / deploy (push) Successful in 2m56s
User: Rust is the product (option a). tools/edge-bench-rs moves to edge/
(crate edge; binaries edge-node, the worker, and edge-bench, the replay).
CI's new job edge runs cargo test and edge/check-fixtures.sh: both
RangeIndexers on three committed captures (edge/fixtures, 25k overview
lines each of a.b.models, a.b.ath and a.b.boneless in 5 chunks, 3.5 MB),
compared field by field with check.ts; deploy waits for it. The
TypeScript RangeIndexer stays as check.ts's reference. README: what it
is now; the benchmark findings as history.

Random names: looksRandom missed long tokens of capitals and digits
("51294P1CUKR2R35X", release 74539068, readable and a Newznab title).
12+ capitals and digits switching 6+ times between them are random
(COD4MW2REMASTERED switches 4 times, BEATLES1964REMASTER twice); both
implementations, identical on the fixtures.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-11 20:01:18 +02:00
Daan MeijerandClaude Opus 5.5 18f5d27413 Prober: claim from random points of the queue, not in id order
Test and deploy / test (push) Successful in 45s
Test and deploy / deploy (push) Successful in 1m23s
In id order the prober went group by group, and some groups' articles
take 0.6-3 s each (a.b.zogg 2016; repeated fetches the same, pipelining
4 deep no faster), where a sample across all years takes 20-60 ms for
most: 7 articles/s on 6 connections, against 67/s in its first minutes.
A claim now takes 20 slices from random points (353 ms per 200).
tools/body_timing.ts: BODY time for the oldest and newest candidates.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-11 15:55:23 +02:00
Daan MeijerandClaude Opus 5.5 b87203e41a edge-node: a range whose batch is too large goes on in smaller batches
Test and deploy / test (push) Successful in 22s
Test and deploy / deploy (push) Successful in 2m25s
Range 154218 (a.b.hdtv.tv-episodes) looped 13 times on HTTP 413: a
500k-article batch over the server's 256 MB body limit, though under
100k files. On a 413 the worker gives the range back and posts that
range's batches at half the articles and files from then on (again on
another 413, down to 1/64); stored batches stay. The message says how
large the batch was.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-10 22:07:54 +02:00
Daan MeijerandClaude Opus 5.5 fc8ec9ff48 Rebuild: tools/compress_ab.ts, compressed vs plain XOVER side by side
Test and deploy / test (push) Successful in 24s
Test and deploy / deploy (push) Skipped
Each chunk fetched with and without XFEATURE COMPRESS GZIP on two
connections at the same moment. 30 chunks on homelab (24 bottom-500,
6 deferred): medians 2.9 s compressed, 3.2 s plain; 59 vs 350 bytes
per article. Slow fetches (27-52 s) hit either mode, rarely both.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-09 21:36:43 +02:00
Daan MeijerandClaude Opus 5.5 8442c4fc44 Rebuild: 2-minute NNTP reply timeout; the Rust worker runs on homelab
Test and deploy / test (push) Successful in 1m9s
Test and deploy / deploy (push) Skipped
Eweka stopped answering on all 40 of the coordinator's long-lived
connections (quiet sockets; a fresh connection fetched the same chunk in
20 s): with a 10-minute timeout the prefetcher fetched one chunk a
minute. The timeout (silence, not duration) is 2 minutes now: 365-411k
articles/s after the restart. tools/probe_chunk.ts times a chunk
compressed and plain.

README and REBUILD.md: edge-node, the Rust worker, is the one running
(8x less CPU per header, 20x less memory than edge:node in production).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-09 21:05:37 +02:00
Daan MeijerandClaude Opus 5.5 a8a139c6d9 edge-node: the edge worker in Rust
Test and deploy / test (push) Successful in 27s
Test and deploy / deploy (push) Skipped
tools/edge-bench-rs/src/bin/edge-node.rs: leases ranges, reads the
coordinator's framed stream (one thread reads and inflates a frame
ahead, libdeflate), indexes with the shared RangeIndexer and posts
batches as gzipped JSON in order on a thread of its own (one batch may
wait). As commands/edge_node.ts: retries for network trouble and 5xx,
409 = lease lost, a batch once 500k articles or 100k files wait, a
chunk that doesn't inflate reported (fetched plain next time), the
range given back on SIGTERM/SIGINT, --max-mbit shared by the threads;
plus a heartbeat when no batch went out for a minute. One range per
thread (--threads, default one per core).

End to end against the web server (a.b.models 2017215-4017214 from
the capture, spooled): the server stored the same from both workers
(38,228 releases with every column, chunks, 481 candidates, rule
counts, 57 fragments with their JSON). Rust 9 s / 5.1 CPU-s, TypeScript
17 s.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-09 20:42:09 +02:00
Daan MeijerandClaude Opus 5.5 dce9967806 edge-bench-rs: a library for the worker; rules from the lease; the wire format
Test and deploy / test (push) Successful in 26s
Test and deploy / deploy (push) Skipped
- src/lib.rs: the indexing modules as a library, for edge-bench-rs and
  the coming edge-node worker.
- Rules as a lease brings them (priorities, dropped signatures and
  tools, per-article parts, sample share, executables) instead of
  DEFAULT_RULES hard-coded; RangeIndexer takes them and a resume point.
- Fragments carry everything the TypeScript Fragment has (poster,
  subject, groups, release): the server stores them for the merger.
- wire.rs: a batch as toWire makes it (camelCase JSON, payloads in
  base64). edge-bench-rs --wire writes it; check.ts --wire compares it
  with toWire of the TypeScript batch: identical on models-a and
  ath-2013.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-09 20:27:17 +02:00
Daan MeijerandClaude Opus 5.5 7836436605 edge-bench-rs README: the a.b.models capture (image sets, re-uploads); where the captures are
Test and deploy / test (push) Successful in 21s
Test and deploy / deploy (push) Skipped
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-09 13:55:19 +02:00
Daan MeijerandClaude Opus 5.5 cfe85abc3e Rebuild: chunks the server's compression cuts short are fetched plain
Test and deploy / test (push) Successful in 23s
Test and deploy / deploy (push) Skipped
Eweka sends some ranges' compressed XOVER truncated, the same bytes
every time, while the plain reply is whole (a.b.models 3317215-3367214:
1,675,600 bytes that don't inflate, twice; 50,000 lines plain;
tools/false_end.ts). Not a false end of reply. The worker's corrupt
report removed the chunk and the range was fetched again, with the same
bytes: one range was reported 540 times. (Before this morning's change
the inflate check waited for more data until the 10-minute timeout,
like voyager 1's 'chunk never answered' ranges.)

A reported chunk's start goes into ranges.plain_chunks; the prefetcher
fetches those over one connection without XFEATURE COMPRESS GZIP
(NntpPool.usePlain, opened on demand) and deflates them here. A plain
chunk reported again sets the range aside (status failed, error). The
capture (edge:bench --capture) fetches such a chunk plain too.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-09 13:51:25 +02:00
Daan MeijerandClaude Opus 5.5 3737ea1542 Rebuild: tools to rank groups by real span (GROUP's low) and sample how full they are
Test and deploy / test (push) Successful in 22s
Test and deploy / deploy (push) Skipped
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-09 13:31:55 +02:00
Daan MeijerandClaude Opus 5.5 0613984de9 Rebuild: re-uploads are releases of their own
Test and deploy / test (push) Successful in 26s
Test and deploy / deploy (push) Skipped
A poster re-uploading under the same name made one release of all
uploads: in a sample, 24% of releases with 2+ files had a file name more
than once (4.3% of files extra copies), 70% of those copies posted a day
or more apart; their NZBs listed each copy.

- File assembly keeps a re-upload's copy apart: a part already in every
  open copy, posted more than 3 h from them, starts another copy; a
  missing part fills the nearest copy (a re-send of what was missing),
  a re-sent part near a copy that has it is ignored as before.
- A key's files in a batch split into uploads where posting pauses for
  more than 3 h, counted from the latest article posted so far (a file
  that takes hours to post doesn't split it); each upload is judged and
  chunked on its own (ReleaseChunk.upload, .postedLast; candidates carry
  their upload).
- The catalog matches a chunk to the release of its key whose articles
  the range's group owns and whose posting window is within 3 h, else
  starts one (key, key:2, …): releases get identity_key, group_id,
  last_posted_at. Keys are locked in order (advisory locks), so
  concurrent batches neither deadlock nor both start one; the merger
  writes in key order too. The same upload multi-posted to other groups
  is a release per group.
- The gap: 91% of uploads post within 3 h; 73% of re-uploaded copies
  come later (releases:upload-gaps). releases:duplicates measures the
  duplicate names.

Both implementations; check.ts compares the windows and uploads too
(identical on all captures; Rust tests mirror the TypeScript cases).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-09 13:31:35 +02:00
Daan MeijerandClaude Opus 5.5 d0f3497ec9 Rebuild: completeness as indexed, file totals from any counter, pausable tails, progress by articles
Test and deploy / test (push) Successful in 21s
Test and deploy / deploy (push) Skipped
- releases.complete is computed with every batch (files >= the
  counter's total) instead of only when a release settles after 6 quiet
  hours: the 'complete' filter matched nothing in a fresh catalog.
- A release's file total comes from any counter form (counterTotal:
  '(01/10)', 'File 27 of 132', …), not only [i/M]; the post rule's key
  keeps the [i/M] total, so keys are unchanged. Both implementations.
- groups.tail: a group's live tail can be paused (its history and owner
  priority stay), e.g. to let other groups' history go first.
- Stats: history by articles, and the current pass (history ranges
  before those deferred 2000 priorities behind) as its own figure; the
  header showed 0.1% of ranges counting 145k deferred ones.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-09 13:06:03 +02:00
Daan MeijerandClaude Opus 5.5 7b7175c229 Rebuild: an image without words around it is keyed as a stem
Test and deploy / test (push) Successful in 21s
Test and deploy / deploy (push) Skipped
So a cover 'x.jpg' joins 'x.part01.rar' in a stem-named post; an image
set like 'IMG_008' keys as the stem 'IMG'. Both implementations.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-09 12:23:47 +02:00
Daan MeijerandClaude Opus 5.5 14758a5b7a Rebuild: image sets, documents and audio albums as releases
Test and deploy / test (push) Successful in 22s
Test and deploy / deploy (push) Skipped
Picture groups made a release per image: 501k single-file releases in
the first 24 ranges of the bottom 500 groups (a.b.models 385k), ~120M
for all of them. Counters written out ('File 027 of 132', '[12of42]',
'104 of 472', '[2239-6389]', '02/17', '0097-1574'; only when the first
number is at most the second, so '[1948-12]' stays) and per-file sizes
('76.88 MB', '335555 Bytes') kept every subject unique.

- subjectRemainder removes those counters and sizes, and empty brackets.
- A subject-named release is bounded by its counter's total
  (counterTotal: the first form found): uploads a poster names alike
  stay apart, an upload's files (par2, nfo) together.
- Images and audio tracks don't take the poster-tag rule: the subject
  names the set or album. An image set without a counter: the image's
  name up to its last number, without numbers.
- Documents and comics (pdf, epub, mobi, azw3, djvu, cbr, cbz, cb7, chm,
  lit, doc, docx) are a release each: collections post hundreds under
  one subject ('cw-horror&mystery').

releases:regroup --group re-keys a group's indexed releases together:
cheerleaders 55,741 -> 3,165 releases, models 385,298 -> 40,313, German
audiobooks 4,106 -> 1,724, playstation 3,309 -> 2,345; comics and
e-books a release per item; boneless unchanged. Ported to edge-bench-rs;
check.ts: identical on all captures.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-09 12:21:00 +02:00
Daan MeijerandClaude Opus 5.5 59481037fb Rebuild: a poster's constant tag no longer makes one release of all their uploads
Test and deploy / test (push) Successful in 28s
Test and deploy / deploy (push) Skipped
Release 145637 held four albums: the poster puts '[KiLL.dA.FuCkIn.
EaStErRaBbIt]----' before every upload, and the text outside the quotes
(when it names a release) was the key with the poster. The same made
one release of a poster's albums under '(PowBeertje80 Posted)' (a post
id, with the same [i/M]), of masterposts' films, and of one volume of
every set in a DVD9 post.

Now, when the file's stem describes something itself (12+ letters and
digits, two words of 3+ letters) and the text outside the quotes shares
no word with it (4+ letters as part of the stem, 3-letter words and
numbers whole; boilerplate like www/net/posted not counted; no 5+ digit
per-post id), that text is taken for the poster's tag and the stem
says which release the file is. Albums of track files ('FTA … Ketchup'),
'DIE_ZEIT_35' with '35_China_dl', 'FTD769997' posts, scene names with
abbreviated file names ('grp-sm') and generic sidecars ('file_id.diz')
keep their grouping.

releases:regroup re-keys a sample of the catalog with the current rule:
on homelab's test data 145637 → 4 albums, 111566 → 17 albums of 27
files, the rest of the subject releases that change mostly lose an ad
image ('Flyer-download-2-Day.gif'); stem releases unchanged. Ported to
edge-bench-rs: check.ts still finds all captures identical (ath 2013:
16 → 76 releases on both sides).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-09 11:23:33 +02:00
Daan MeijerandClaude Opus 5.5 c5e0ad1f9f Rebuild: tools/xover_eras.ts, XOVER speed per era of a group
Test and deploy / test (push) Successful in 31s
Test and deploy / deploy (push) Skipped
Connections fetch chunks round-robin from several starting points of one
group at once, so each era sees the same server load; per era: seconds
per command (mean, median), articles/s per connection, bytes per article.
On a.b.boneless (2 connections, 10 min): 2008 24.9k, 2011 28.0k, 2019
22.6k, 2026 17.9k articles/s per connection; medians 2.3-3.0 s, means set
by a tail of slow commands (up to 41 s).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-09 09:57:19 +02:00
Daan MeijerandClaude Opus 5.5 122a054eab edge-bench-rs: README with the findings and how to compare the outputs
Test and deploy / test (push) Successful in 33s
Test and deploy / deploy (push) Skipped
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-09 09:40:45 +02:00
Daan MeijerandClaude Opus 5.5 c7b4ebe470 edge-bench-rs: libdeflate for chunks, memchr for lines and tabs
libdeflate inflates a chunk whole, in half the time of zlib-rs (1.09 ->
0.51 s per boneless range); memchr finds line ends and tabs with SIMD.
The per-article timers are gone. Boneless 2026 ~700k headers/s, ath 2013
~920k; all three captures still identical to the TypeScript.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-09 09:34:38 +02:00
Daan MeijerandClaude Opus 5.5 b1c2db5984 edge-bench-rs: upload tools, release-name checks and common Date headers by hand
Each with its regex/chrono version as the reference in tests; check.ts
still finds all three captures identical. Boneless 2026 ~500k headers/s,
ath 2013 ~650k.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-09 09:31:56 +02:00
Daan MeijerandClaude Opus 5.5 43e74199d4 edge-bench-rs: file stems and subject remainders by hand, mimalloc
The suffix and remainder regexes were most of a new file's description
(333k on boneless 2026): now plain byte matching, with the regex versions
kept as the reference in a test of tricky names and subjects. mimalloc
for the many small strings. Boneless 2026: 360k -> 495k headers/s;
check.ts still finds the outputs identical on all three captures.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-09 09:28:08 +02:00
Daan MeijerandClaude Opus 5.5 bacc9aa3b3 Rebuild: edge-bench-rs, the edge worker's indexing in Rust, for comparison
The RangeIndexer path of edge:bench (inflate, scan, owner rule, segments,
file aggregation, cut-off, rules, release identity, chunk encoding v2,
probe candidates) as a Rust binary over a captured range, with JS
semantics where they matter (\s, trim, \b, parseInt, lastIndexOf, UTF-16
lengths). check.ts runs the TypeScript RangeIndexer on the same capture
and compares every output: identical on all three captures (releases and
their decoded payloads, samples, candidates, rule counts, fragments),
after mapping Date headers ending in UTC to GMT (chrono follows RFC
5322; Date.parse takes UTC, a quarter of a.b.boneless). Compressed
payloads differ by <0.1% in size (zstd framing), not in content.

First run on boneless 2026: 360k headers/s and 228 MB peak (TypeScript:
~217k/s, ~0.7-0.8 GB).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-09 09:20:53 +02:00