Repository navigation
config: poll the chain every second and batch stream frames by time - #28
Conversation
Two defaults that cost latency and dropped frames on mainnet. SORTITION_POLL_INTERVAL 4s -> 1s. Both sortition watchers polled every 4 s against a 2 s block, so a session request or job waited up to two blocks before the worker saw it. STREAM_CHUNK_TOKENS 8 -> 32 and STREAM_CHUNK_INTERVAL 250ms -> 100ms. A frame every 8 tokens is 29 frames/s at 232 tok/s, above the relay's refill rate of 1000 messages/minute (~16.7/s) per consumer, past which it drops frames. With the interval as the binding limit the worker sends about 10 frames/s at any rate up to ~320 tok/s. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
|
Review notes, recorded after the merge. None of this blocked it; these are three side effects of the new defaults that the description does not cover. 1. Two retry budgets are counted in poll passes, so they now run out about four times sooner.
If either should keep its wall-clock length whatever the poll interval is, counting them in time instead of passes would do it. 2. Slower models send more frames than before. At 10 tok/s it is 5.0 frames/s (was 2.5), at 34 tok/s 6.8 (was 4.2). The relay bucket is per consumer address, so three parallel streams at 34 tok/s now add up to 20.4 frames/s, above the 16.7/s refill; before it was 12.6/s. The 1000-message burst absorbs about 4.5 minutes of that, so it only matters for a consumer with sustained parallel streams. 3. RPC load per worker is nearer three calls per second than two. Both watchers read the head every second, and each now runs Checked during the review
|
What this changes
Three defaults in
internal/config/config.go(and the matching fallbacks ininternal/pipeline/stream.go). No logic changes.SORTITION_POLL_INTERVAL4s1sSTREAM_CHUNK_TOKENS832STREAM_CHUNK_INTERVAL250ms100msOperators who set these explicitly are unaffected.
1. Poll interval: 4 s to 1 s
Both sortition watchers (
SessionWatcher,JobWatcher) tick on this interval. Mainnet blocks are 2 s, so at 4 s a session request or a submitted job waits up to two blocks before the worker looks.Measured on the mainnet fleet over 7 days at the 4 s default, time from the job's block timestamp to the worker starting stage 2:
The same interval gates session claims: over the same week, session request to claim was 8 s at p50, of which 4 s is the contract's two-block floor and 4 s is the claim transaction.
The nine mainnet workers in our fleet have run with
SORTITION_POLL_INTERVAL=1ssince 2026-10-13 14:30 UTC with no warnings or errors. Only two jobs had arrived when this was written, both picked up 1.3 s after their block, so the improvement is not yet proven with a real sample. The two testnet workers on A100-Workers have run at 2 s for longer.Cost: an idle pass is one
eth_blockNumber-class call per watcher, so about two cheap RPC calls per second per worker instead of one every two seconds. Each pending session request the worker is not yet eligible for adds agetRequestand aneligibleNowread per pass.2. Stream batching: by time, not by 8 tokens
The relay keeps a per-consumer token bucket of 1000 messages, refilled at 1000/minute, about 16.7 per second (
lightchain-relayinternal/ratelimit/limiter.go). Past it, frames are dropped. The chat client ignores the rate-limit notice and only repairs missing text from the terminalcompleteframe, which is rate-limited too.A frame every 8 tokens exceeds that refill rate on any fast model. Frame rate and first-frame delay, computed from the coalescer's logic at token speeds measured on the fleet:
With the new defaults the interval is the binding limit up to about 320 tok/s, where the 32-token cap takes over; the rate reaches the relay's refill rate only above about 530 tok/s.
Trade-off to be aware of: on fast models the first frame goes out about 70 ms later than before (30 to 47 ms becomes about 100 ms). On slow models it goes out sooner. The chat frontend already throttles rendering to 100 ms, so this should not be visible. Sending the very first delta immediately and batching the rest would remove that cost, but it changes the coalescer's behaviour and several handler tests, so it is left out of this PR.
Nothing in settlement changes: chunk frames are throwaway encryptions, and the terminal frame, the blob and
completeJobstill use the single full-response ciphertext.I did not measure dropped frames in production; the case for this change is the two rates above, not an observed incident.
Tests
TestConfigdefaults updated for the three values.TestTokenCoalescer_DefaultsStayUnderRelayRefillRate: drives the coalescer with a fake clock at 10, 34, 150, 232 and 320 tok/s and asserts the frame rate stays at or under 10.5 per second. It fails with the old defaults (150 tok/s and up) and passes with the new ones.Verification
Run locally with Go 1.26.9 in a workspace of this branch plus
lightchain-shared-pkgattestnet(58931c3), as CI does:go test ./internal/config/... ./internal/pipeline/... ./internal/sortition/...: pass.GOOS=linux go build ./...andGOOS=linux go vet ./...: clean.gofmt: clean on the four files.internal/releaseandinternal/servicetests, which do not compile on Windows (unix.Flock). CI covers them.Not in this PR
RECEIPT_POLL_INTERVALis parsed but never read; receipts are polled bybind.WaitMinedon a fixed 1 s ticker.🤖 Generated with Claude Code