Repository navigation
Conversation
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
- Count live check routines per group; the feeder closes its channel and the
divider quits ("checkersGone") once a group has none left.
- A failed STAT is retried once, then recorded as an error on the group so the
item still counts as checked; a check routine quits after 3 items without
a connection.
- Replace the -verbose-only watchdog (which killed progressing runs that had
dead segments) with an always-on 3 min no-progress check that also counts
failed attempts.
- GetConn failures in the downs/reups routines now flag the item, so it no
longer stays "in flight" forever; uploads give up a provider after 3 fails,
and the FlagError retry delay works (.Unix(), retryOn set).
- Failed uploads keep their mem slot: returning it deadlocked downloads and
re-queued uploads on MemLockWait.
- After the stop, queued items are dropped and GetConn no longer dials.
- A provider whose pool gave up on dial errors is skipped for the rest of the
session (never in proxy mode).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…tines - CloseConn: nil-safe; the open counters drop once per conn (a second close drifted openConns negative), the shared-conn token is always sent. - KillConnPool: no global lock, bounded wait (5 s), only idle conns closed. The unused ConnPools map and PoolsLock are gone (and the lock inversion). - Worker tick goroutines are time.Tickers; WatchOpenConnsThread and the per-pool Speedmeter stop when KillConnPool closes the pool's stop channel. - readDotLines: early returns close the rapidyenc pipe / decodeBodyChan and wait for the decoder goroutine (with -yenctest=4 the decoder was released while still in use); the async decoder keeps draining after an error. 4 web jobs in a row: 122 goroutines left before, 10 after. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
… second lock MemReturn checked memlocked and returned the slot in separate steps, so two concurrent returns of one item handed back two slots (or hit log.Fatalf on a full chan). memdata is now the single ownership record; it changes together with item.memlocked under m.mux, and only once the slot is taken. A second return is a logged no-op. A second MemLockWait on an item still waiting for a slot waits for the first one instead of calling os.Exit(1). MemAvail keeps its meaning (bound the downloads waiting for a slot): gating on free slots cost 25% throughput with -mem 4. waiting is atomic so MemAvail takes no lock, since pushDL calls it while holding item.mux. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
GoBootWorkers wrote them under globalmux while LaunchSession and closeSession use s.mux, and the web stats poll read active with no session lock. All now use s.mux; the poll reads active together with providerList. waitDivider.Done is deferred before the early returns. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
connect() read the greeting and AUTHINFO replies with no deadline (UseNoDeadline makes ExtendConn a no-op), so a server that accepts and then stays silent blocked GetConn forever. The handshake now has a 30 s deadline, cleared before the conn is handed out; a timeout counts as a failed dial so the provider can be marked down for the session. checkCapabilities held provider.mux across two unbounded reads. It now has its own deadline and takes the lock only to store the result. The greeting was read with ReadCodeLine(200): a 201 greeting came back with an error and, without a login, the conn was leaked. Accept any 2xx. clearSession read len(sessMap) without the processor lock. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…under locks StartWebServer binds and loads the TLS cert before serving and returns the error; main exits on it (a busy port used to leave the process running without a web server). The job queue moves to webjobs.go. Readers get copies and never write. Each job creates and keeps its own session (SessionID is set), so nothing matches sessions by path. GoWorkDivider publishes its per-pass counts in s.progress; running jobs read them and finished jobs keep the final counts, also when nobody polled. AddJob parses outside the lock and rejects broken NZBs. collectStats no longer holds globalmux or walks segment lists. New waiting state, panic recovery on the job goroutine, queue caps, 404/409 status codes. Per-job settings (D2): the config endpoints return 501 with the startup settings, /upload ignores the fields, and the dialogs are read-only. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Login (D1): HTTP basic auth against its own -webpasswd file, users via -webadduser. Without it the server only starts on loopback. Verified logins are cached briefly, the file is reloaded when it changes. The passwd parsing moves to passwd.go and is shared with the proxy, which keeps its own map: a reload swaps in a new map only when it parsed and has users (it used to clear the live map first), and unknown users cost a bcrypt comparison like wrong passwords (timing oracle), expiry checked. Every state-changing request needs X-NZBreX-Request: 1, a same-site Sec-Fetch-Site and a matching Origin. Without a login the Host header is checked against an allowlist (DNS rebinding). Server timeouts, 128 MiB upload cap, no trusted proxies. Uploads go to a private MkdirTemp dir under unique names, written with O_EXCL instead of gin's SaveUploadedFile, and are deleted with their job. Bootstrap is embedded (Debian libjs-bootstrap5 5.3.5) instead of loaded from a CDN. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…ject broken modes Jobs ran in random order: each started job had its own goroutine racing for a mutex. One runner now takes the oldest waiting job. POST /api/jobs/:id/stop cancels a waiting job or stops a running one. The engine now reacts to s.stop() before the work is done: the divider quits (close case "stopped"), the check feeders stop and the check routines drain without checking; the rest tears down as usual. SIGINT/SIGTERM in web mode shuts the server down, stops the running job, removes the upload dir and exits 0. D3: -nzb with -web or the proxy, -web with the proxy, and -nzbdir (with anything, or alone while it is unfinished) are rejected at startup. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…y articles on broken yEnc Hangs and exhaustion (5.1): per-command read deadlines (60 s to log in, 5 min idle), write deadlines, 400 before closing, a cap of 64 connections that haven't logged in, 512-byte command lines, upstream conns parked before writing to the client, 64 MiB cap on buffered replies, a bounded negative cache keyed by group for numbers, accept-loop backoff (EMFILE used to spin at 100% CPU and log millions of lines), shutdown on signal. Correctness (5.2): only QUIT, an expired login or a failed client write close the connection; syntax errors and unknown commands are answered. Every client write is checked. NEXT/LAST forward the upstream reply. GROUP selects the first article, message-ID requests keep the group, the pointer moves only on success, numeric replies carry the real number and message-ID. Listeners return errors instead of log.Fatalf and start only after the providers are loaded. Shared readDotLines: a broken yEnc article returns bad crc (99932) instead of success with no content (empty 220s to proxy clients, empty articles to workers); a plain-text article in mode 4 is delivered as is. The proxy relays articles unchanged (no Path/header rewriting). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- -chansize <= 0 is sized per session instead of written back to cfg.opt - yEnc CPU limiters are set up once at startup, never resized mid-run - -log redirects only for NZB sessions (no logs/.log for web/proxy) and closes the previous log file - /api/stats lists the startup providers while no job runs - WEB_JOB_QUEUE.md rewritten to match the code (plan 6.3, D2) Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…the send - newBaseSession builds the proxy/web sessions with all their fields - ParkConn only locks for its debug line - dl/up queue counts go up before the channel send and back down if it was full, so a fast worker can't decrement first and leave them at 1 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
… via -yencgo - ydec.DecodeIncremental is a drop-in for rapidyenc_decode_incremental: a state machine matching rapidyenc's scalar reference, plus vector loops with GOEXPERIMENT=simd on amd64 (VPSHUFB tables on AVX/AVX2, VPCOMPRESSB on AVX-512 VBMI2); plain Go elsewhere - tested against the C library: random and chunked inputs, encoded articles, sample files, fuzzing; rapidyenc's decoder tests and the broken-yEnc readDotLines tests also run with it - rapidyenc.UseGoDecoder / -yencgo switch the Decoder's yEnc step only; C stays the default - fix: -rapidyencbufsize had no effect (AcquireDecoder used a copy of DefaultBufSize made at init) Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- ydec moved to github.com/go-while/go-yenc-simd (package yencsimd), imported via a replace to the sibling checkout until it is published - the C cross-check tests and benchmarks move to rapidyenc - go mod tidy merged the require blocks (no version changes) Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…se subjectparser fork - the segment list was sized by TotalSegments, the count the subjects claim: "(1/9999999999999999999)" panicked (makeslice), a large plausible count allocated gigabytes. Sized by the NZB's segments now (web job count too) - -yencmerge joined the filename from the subject into the target path: "../../../x" wrote outside the cache dir. Only its last element is used now (safeFileName); unusable names are refused - github.com/Tensai75/subjectparser is replaced by go-while's fork (../subjectparser, branch review-fixes): filenames of subjects without segment numbers, overflowing numbers, regexps compiled once Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- github.com/Tensai75/nzbparser -> github.com/javi11/nzbparser (active fork with its own subject parser), replaced by ../nzbparser (branch review-fixes) until the fixes are published or merged; NzbSegment.Id is ID there. The subjectparser fork is no longer imported. - Segment ids are normalised to "<...>" and must be valid RFC 3977 message-ids (printable ASCII, no spaces, at most 250 bytes) before they are used in NNTP command lines; invalid ones are skipped with an error. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- github.com/go-while/go-yenc-simd is on GitHub: required at 50d56c3 (pseudo-version), the replace to ../go-yenc-simd is gone - github.com/javi11/nzbparser is replaced by the pushed review-fixes branch of github.com/go-while/nzbparser (7c20501, open as javi11/nzbparser#2) instead of ../nzbparser A fresh clone builds without sibling checkouts. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- rapidyenc/decoder_c.go (cgo, the C DecodeIncremental, DecodeKernel) and encoder.go build only with -tags rapidyenc; without it, decoder_purego.go decodes with go-yenc-simd and nothing needs cgo - State/End constants have fixed Go values, checked against the C headers in the C build (TestStateValuesMatchC) - rapidyenc.Backend() names the decoder; -yenctest=4 logs it, -yencgo only matters in the C build - C-only tests carry the tag; a test-only Go encoder serves the others - local_build_linux-amd64.sh builds with -tags rapidyenc; README build section covers both variants Both builds pass the race tests; CGO_ENABLED=0 cross-compiles to linux/windows/darwin amd64+arm64 and freebsd/amd64. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- ci.yml (push, PR): go vet + go test -race for both variants, with and without GOEXPERIMENT=simd; purego cross-compiles for every target; a native macOS check of the C variant. Read-only permissions. - release.yml (v* tags, or by hand for a tag): purego for linux/windows/ darwin amd64+arm64 and freebsd/amd64 from one runner; rapidyenc for linux amd64/arm64, windows amd64, darwin arm64; one publish job (the only one with contents: write) uploads everything with SHA256SUMS via gh - scripts/build-release.sh builds and packs one target (flat tar.gz/zip with README, LICENSE, samples; .deb "nzbrex" with docs in /usr/share/doc), the same locally and in CI - build_and_publish_test.yml is removed: its musl build linked glibc, its "purego" assets used cgo, no checksums reached the release, the .deb broke Debian naming, and every matrix job wrote to the release Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- routes with Go 1.22+ ServeMux patterns ("POST /api/jobs/{id}/start"),
the Host/CSRF guard and the login as handler wrappers, writeJSON,
pages rendered into a buffer, no directory listings for /static/,
panic recovery (500), access log with -debug only
- same routes, JSON, status codes and content types; a wrong method is
405 now (was 404)
- webserver_test.go covers every route, uploads, login, CSRF and Host;
written against the gin router first, passes unchanged
- binary 33.5 -> 17.1 MB, third-party packages 114 -> 27
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…7/v8) checkout@v4 and setup-go@v5 target Node 20, which GitHub is retiring (forced onto Node 24, with a warning per job). No breaking change applies: setup-go keeps go-version-file, release.yml downloads all artifacts with merge-multiple (not by ID), and download-artifact@v8 now fails on a checksum mismatch instead of warning. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
GitHub replaces '~' with '.' in release asset names, so the .deb names in SHA256SUMS didn't match the uploaded files (v0.0.7-test-p1). The file name now uses the tag's version (nzbrex_0.0.7-test-p1_amd64_purego.deb); the Debian Version field keeps '~' (0.0.7~test~p1) so test releases sort before the release. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
v0.0.7-test-p1 got GitHub's generated notes (unrelated PRs merged into main, compared against v0.0.5-rapidyenc) and targeted main. Now the text is the annotated tag's message, the commits since the previous release (next lower version tag, collapsed, at most 100), and .github/release-notes.md (which file to download, how to check it). The release targets testing when the tagged commit is on it, otherwise main. A re-run updates the text and target too. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A successful upload sets uploadedTo, not availableOn, but the divider's done test needed availableOn on every provider. Uploaded articles were never done: each pass counted them as "no upload target", and every refresh run ended through the fallback close case #8. Done is now onAllProviders(item): on every provider, confirmed there or uploaded there by this session. availableOn keeps meaning "confirmed" (pushDL picks download sources from it). The web's ArticlesDone is done alone; it added uploaded to make up for this. On the local servers: a normal run ends through #0 (was #8), one with damaged articles through #6 (was #8); result tables unchanged. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Download-only runs (-cd, -yencout, no provider taking uploads) never downloaded: pushUP's "no target"/"not allowed" error made the divider skip pushDL, and with -cd/-yencout the articles weren't retired either, so the run ended in the "impossible closeCase" safety exit. - -dlmissing: download the articles missing on any provider into the cache (e.g. to re-upload them later) - -dlall: download every article not yet cached (fill the cache, or get the files with -crc32 -yencout -yencmerge) - both need -cd, not together, not with -checkonly - pushUP only runs for items with an upload target; -dlall keeps done items until downloaded, then returns their mem slot - without a flag such articles are retired and the run ends cleanly, with a hint naming the flags Local servers, both NoUpload, 60 articles (15 missing on one): no flag ends in 5 s with the hint; -dlmissing caches 15; -dlall with -yencmerge caches 60 and merges 3 files matching their yEnc size and CRC32. Normal re-upload runs are unchanged. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
After 10 failed logins within 10 minutes an address gets 429 with Retry-After for 10 minutes, before any bcrypt check. A success resets the count. Logins already verified (cached) pass during a block, so other users behind the same address (reverse proxy) keep working. The table of addresses is capped. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…rk can return - every provider conn is an idleConn: each read and write gets a fresh 60 s deadline, so a provider that stops answering mid-command fails that command instead of blocking the worker forever, while a slow but steady article is not cut off. Off during the handshake and the CAPABILITIES exchange, which have their own deadline - download and upload routines exited after ~15 s without work; a retry that came back later (after another provider timed out) found no worker, and the run ended with that work undone. They now end when the session closes their channel or stops Against a server that answers STAT but stalls every ARTICLE, with a second download source: the old build was still blocked after 10 min; now the run finishes in 71 s with every missing article copied. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Every -dirrefresh seconds (default 60, minimum 15) the watcher takes the .nzb/.nzb.gz files that are unchanged since the previous scan (not ones still being copied in), oldest first, runs each in its own session and removes the session afterwards. Then the file moves to done/, or failed/ if the run couldn't start; a name already there gets a timestamp. SIGINT/SIGTERM stops the running NZB, which stays for the next start. The old watchDirThread/refreshDir/processorThread stubs are gone (the scan never ran: nothing set its interval). -nzbdir alone is allowed now; with -nzb, -web or the proxy it is still refused. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
NNTP proxy, web job queue, review fixes, pure-Go builds (
testing→main)This PR started in June 2025 as the first version of the NNTP proxy (
Server.go). Since thentestinggained the web job queue, and in October 2026 a full review of the codebase with fixes for everything it found (the plan and every result are inreview-the-codebase-for-jolly-petal.md). This description covers the state oftestingnow.All verification ran against local go-while/nntp-server instances with generated test articles (
tools/nntpseed), never against live Usenet providers. CI ontestingis green.📡 NNTP proxy (
-proxytcp/-proxytls)Protocol
CAPABILITIES,AUTHINFO USER/PASS,MODE READER,DATE,QUIT,GROUP,NEXT,LAST,LIST,XOVER/OVER,XHDR/HDR, andARTICLE/BODY/HEAD/STATby message-id, by number, or for the current article.GROUPselects the first article, the article pointer only moves on success, replies to requests by number carry the real number and message-id,NEXT/LASTforward the server's answer,DATEuses the RFC format.QUIT, a failed login or a failed write close it. Every write is checked.Path:, no stripped headers). A damaged or bad-CRC article from one provider is tried on the next; the client gets430instead of an empty article.Limits and robustness
400), at most 64 connections waiting to log in.🔐 Users
AUTHINFO PASS pw junkno longer logs in aspw.Proxy flags
-proxytcp(int): start the proxy on this TCP port, e.g.-proxytcp=1119. Default0(off).-proxytls(int): start the proxy on this TLS port, e.g.-proxytls=1563. Default0(off).-tlscrt/-tlskey(string): TLS certificate and key for the proxy. Defaultfullchain.pem/privkey.pemin the current directory.-proxyadduser(string): add a proxy user,'username|password|maxconns|expires|(no)post'. The password is given in cleartext and hashed when written. Expiry is a Unix timestamp or a duration (7d,12h,30m). Example:-proxyadduser="HelloWorld|NotAsecurePassword|5|42d|nopost"(5 connections, expires in 42 days).-proxypasswdfile(string): the proxy's passwd file. Default.proxypasswd.🌐 Web interface and job queue (
-web).nzb,.nzb.gz, up to 128 MiB), queue them, start one or all, stop a running job or cancel a waiting one, delete queued or finished jobs. Jobs run one at a time in queue order:queued→waiting→running→completed/failed./jobs,/stats,/api/jobs,/api/stats). Each job's counters come from its own session.-webpasswd, users via-webadduser; proxy users get no web access). Without a login the server refuses to start on anything but a loopback address.429(withRetry-After) for 10 minutes, answered before any password check. Logins that already succeeded keep working, so users behind the same address (reverse proxy) aren't locked out.X-NZBreX-Request: 1header and a same-siteOrigin/Sec-Fetch-Site(CSRF); theHostheader is checked against DNS rebinding (-webhosts). Uploads go to a private temp dir with unique names andO_EXCL.501, and the docs say so (WEB_JOB_QUEUE.md, rewritten from the code).net/http(no gin): binary 33.5 → 17.1 MB, third-party packages 114 → 27.Web flags:
-web,-webaddr(default127.0.0.1:11911),-webssl/-webssl-crt/-webssl-key,-webpasswd,-webadduser 'user|password'(orauto|auto),-webhosts.🛠 Engine fixes (races, hangs, leaks)
Every fix was checked with the race detector, most against the old build as well.
MaxConns: 0, a436on IHAVE, or a-nzbdirwatcher; blocking waits can be stopped; the NNTP handshake andCAPABILITIEShave timeouts (a silent server hung > 180 s, now gives up at 105 s).MemLimiter: one owner record per slot, no double return), connection counts, goroutines, the decoder pool (use after release in-yenctest=4), one log file per-logsession.-chansize, the yEnc CPU limiters or the log output for the whole process.CAPABILITIES,-crc32in the proxy, a nil connection pool in the stats, aretryIncomputed from the seconds field instead of a timestamp.-nzbdirwith-web, the proxy with-web, ...) exit with an error instead of half-starting.-dlmissing(articles missing on any provider) and-dlall(every article) download into the cache (-cd), also without an upload provider;-dlall -crc32 -yencout -yencmergerebuilds the files. Without a flag such a run now ends cleanly with a hint (it used to download nothing and end in a safety exit).-nzbdirworks: it watches a directory (-dirrefresh, default 60 s) and processes its NZBs one at a time, oldest first, once a file is unchanged between two scans; afterwards the file moves todone/orfailed/. SIGINT/SIGTERM stops the running NZB, which stays for the next start.⚡ yEnc decoding: pure Go by default, C optional
GOEXPERIMENT=simd, plain Go otherwise. Same results as rapidyenc's C code, call for call (golden outputs, differential tests, fuzzing). Through NZBreX's full decoder it reaches 61–81 % of the C speed, far above download rates.-yenctest=4decodergo build .go build -tags rapidyenc .-yencgoswitches to go-yenc-simd-yenctest=4logs the decoder in use. Fixed:-rapidyencbufsizehad no effect.🧱 Crafted NZBs and dependencies
(1/9999999999999999999)crashed NZBreX (makeslice); the segment list is now sized by the segments the NZB really has.-yencmergebuilt the output path from the subject's filename, so"../../x"wrote outside the cache directory; only the last path element is used now.review-fixes, open upstream as fix: well-formed Write after Parse, trimmed ids, overflow-safe subject numbers, regexps compiled once javi11/nzbparser#2 (well-formedWrite, trimmed ids, overflow-safe numbers, 3× faster subject parsing). Fixes for Tensai75/subjectparser are open as Keep the filename without segment numbers, ignore overflowing numbers, compile regexps once Tensai75/subjectparser#1.📦 Build, CI and releases
animetosho/rapidyenc, pinned).ci.yml, every push and PR):go vetandgo test -racefor both variants with and without SIMD, purego cross builds for linux/windows/darwin amd64+arm64 and freebsd/amd64, and a native macOS build and test of the C variant.release.yml, onv*tags): purego for all targets from one runner, rapidyenc for linux amd64/arm64, windows amd64 and macOS arm64; one publish job (the only one with write access) uploads everything withSHA256SUMS.scripts/build-release.shbuilds and packs a target the same way locally: flat archives with README, LICENSE and samples, and a.debnamednzbrex.build_and_publish_test.yml, whose "musl" build was linked against glibc, whose "purego" assets used cgo, and whose release had no checksum files.🧪 Tests
New tests for the proxy, web routes (every route, login, CSRF, Host, uploads), job queue, auth, memory limiter, connection pool, teardown and the decoders;
go test -race ./...passes for both variants. Local test setup and tools:tools/nntpseed(generates yEnc articles, including damaged ones, and feeds them to local servers) andnzbs/LOCAL_TESTING.md.📝 Known issues (not fixed here)
🤖 Generated with Claude Code