Repository navigation
Read a subscribed record once per update instead of once per subscriber - #2921
Merged
Merged
Conversation
Live delivery checked each subscriber's event against the record's current version with its own primaryStore.getEntry call, so an update to a record with N subscribers on a thread did N reads of the same entry. Memoize the read on the audit-record object, which every subscriber of the key on the thread receives for that log entry. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…e record id too Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
kriszyp
marked this pull request as ready for review
September 30, 2026 01:24
github-actions
Bot
requested review from
Ethan-Arrowood,
cb1kenobi and
heskew
September 30, 2026 01:24
Contributor
There was a problem hiding this comment.
Code Review
This pull request optimizes subscription version selection in Table.ts by introducing a memoization mechanism (currentEntryForAudit) that ensures the current entry is read only once per audit record during a synchronous notify pass, rather than once per subscriber. The changes also include corresponding updates to the design documentation and a new unit test to verify the optimization. The review feedback correctly identifies a TypeScript type definition issue where memoizedEntryId is declared as Id but is initialized and reset to undefined, which would cause compilation errors under strict null checks.
Contributor
|
Reviewed; no blockers found. |
This was referenced Sep 30, 2026
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.
Live subscription delivery now reads a record once per update instead of once per subscriber. Every subscriber to a record checked its event against the record's current version with its own
primaryStore.getEntrycall (eventFromAuditinresources/Table.ts), so an update to a record with N subscribers on a thread did N identical reads. The read is now memoized bycurrentEntryForAuditon the audit record every one of those subscribers receives, for the duration of that synchronous notify pass. The invariant it relies on is recorded inresources/DESIGN.md(subscription version selection) and its root index entry. For a record with many subscribers this cuts CPU per delivery by roughly 10–15% and moves the saturation point for update-driven fan-out: at the rate wheremaindelivers 58–86% of messages with seconds of p99 latency, this branch delivers 97–99% with p99 under 400 ms.For the human reviewer
getEntryas the largest single JavaScript cost (~10% of worker CPU in a hot-record scenario). The change is warranted for any "many clients watching one record" workload (live scores, dashboards). It is independent of other in-flight work and small enough to land on its own.Table.ts, keyed on audit-record identity, store and record id, cleared by aqueueMicrotaskafter the pass. Alternatives: a per-record slot on the audit-record object (adds a field to every decoded audit entry on every read path, replication included), or an entry passed from the notify loop intransactionBroadcast.ts(couples the two modules). Easy to change later; a "no" costs only the refactor.queueMicrotaskper synchronous turn that fills the memo, even when a key has a single subscriber and saves nothing. It is per turn, not per record or subscriber. The zero-allocation alternative is the caller-owned clear from item 2.HARPER_STORAGE_ENGINE=lmdb, subscribers of a key now receive one sharedevent.valueobject instead of one decode each (RocksDB already shared it through its record cache). ArowFilterfreezes that value, so a listener that mutatesevent.valuewould now throw on LMDB as it already could on RocksDB. Records are immutable by contract.includeSupersededpatch reconstruction (durable MQTT receiving patches) still reads once per subscriber; covering it would be an additive follow-up.main). The benefit applies to deployments running 5.2/5.3 today; backporting is a customer-need call.Verification
subscriptionEntryRead.test.jscountsgetEntrycalls for the record while 1 and then 20 subscribers receive an update, and asserts the count does not grow with subscriber count and that every subscriber gets the new value. Fails onmain(22 reads for 20 subscribers vs 3 for one), passes here. Subscription suites (subscriptionEntryRead,subscriptionSuperseded,subscriptionValueIdentity,subscriptionReplay) pass on RocksDB (64 passing) and LMDB (75 passing).827247cd9(freshnpm ci):test:unit:main6289 passing;test:unit:resources3707 passing;test:integration:all2285 passing, 0 failing, 6 cancelled — theintegrationTests/server/ollama-backend.test.tssuite, which only runs when a live Ollama server is present and fails at module load (systemSchema.json needs an import attribute) identically onmain(726dd1e).main/branch pairs. CPU per delivery at 2 updates/s: 18.0 / 17.1 µs onmainvs 16.2 / 14.7 µs here; at 5 updates/s: 17.3 / 17.5 µs vs 15.6 / 14.1 µs. At 10 updates/s (the saturation point)maindelivered 58% / 86% of the fan-out with p99 5.0 / 0.96 s, this branch 97% / 99% with p99 0.37 / 0.31 s. The load generator is a benchmark harness to be submitted separately.Complexity: medium
Review-Coverage: authored=claude; ran=gemini,cursor-composer,codex; adjudicated=domain; declined=cursor-grok,cursor-kimi,cursor-muse; rounds=2; full=1 @ afc9b2f
Human-Review-Need: 3 (decisions: memo-lifetime-owner, memo-scope-default-path-only) @ ca64a6e