Repository navigation
Make concurrent sourcedFrom misses share one source call when nothing gets cached - #3417
Merged
Merged
Conversation
Contributor
There was a problem hiding this comment.
Code Review
This pull request introduces a mechanism in getFromSource() where concurrent same-key waiters on the same thread share a resolved source fill (even if nothing was stored, such as for absent records or under noCacheStore) to prevent duplicate origin requests. This change is accompanied by comprehensive unit and integration tests, as well as updated design documentation. The review feedback highlights a potential deadlock risk in resources/Table.ts where a synchronous error thrown by transaction() could escape the Promise executor, preventing the record lock from being released. Wrapping the transaction call in a try...catch block is recommended to handle synchronous failures safely.
kriszyp
force-pushed
the
fix/sourcedfrom-negative-miss-coalescing
branch
from
October 27, 2026 00:29
83cd18a to
1e13349
Compare
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01T9ub6mcFSCB8x9He8FS35Z Dispatch-Task: harper-sourcedfrom-negative-miss-coalescing
A waiter on getFromSource's tryLock re-read the store after the leader's unlock and called the source again whenever nothing valid was stored, so concurrent misses whose source reported the record absent, or whose fill ran under noCacheStore, made one origin call per waiter in sequence. The leader now records its resolved entry once its fill commits with the write kept, and a same-thread waiter serves it when the store still has nothing valid. Failed and stale-on-error fills are not shared. Refs #1398 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01T9ub6mcFSCB8x9He8FS35Z Dispatch-Task: harper-sourcedfrom-negative-miss-coalescing
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01T9ub6mcFSCB8x9He8FS35Z Dispatch-Task: harper-sourcedfrom-negative-miss-coalescing
A source that sets noCacheStore on its own context has marked its response per-request, so that response is no longer shared with other waiters. An unstored record goes only to a waiter that would not have stored it either, so a caching waiter behind a no-store leader fills the cache itself. The shared entry is now taken only at resolve(), so a fill that fails while preparing the record (a non-extensible source object) is not shared. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01T9ub6mcFSCB8x9He8FS35Z Dispatch-Task: harper-sourcedfrom-negative-miss-coalescing
On a read-only node or a dropping table, noCacheStore is preset on the source context, so a source's own per-request opt-out cannot be told apart from it there; such fills are no longer shared. Widen the REST test's origin hold so all reads overlap one fill on a slow runner. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01T9ub6mcFSCB8x9He8FS35Z Dispatch-Task: harper-sourcedfrom-negative-miss-coalescing
…t bursts A table drop could abort a fill after its source write was allocated but before it was staged, and the fill was then published as stored. The fill's write now counts only once it is staged and its commit callback kept it. The unit and REST tests now hold the first source call of a burst until every other read has parked on the record lock, instead of relying on a fixed delay. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01T9ub6mcFSCB8x9He8FS35Z Dispatch-Task: harper-sourcedfrom-negative-miss-coalescing
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01T9ub6mcFSCB8x9He8FS35Z Dispatch-Task: harper-sourcedfrom-negative-miss-coalescing
Node 22's loose deepEqual does not treat null and undefined as equal, and an absent record reads back as null. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01T9ub6mcFSCB8x9He8FS35Z Dispatch-Task: harper-sourcedfrom-negative-miss-coalescing
A record whose headers is a Headers-like object is not shared: the response merge writes into it in place, which would leak one request's response headers into another's. A no-store leader that finds the origin reports a record absent while an older entry is stored now marks the fill unstored, so a caching waiter refetches and stages the delete instead of keeping the stale entry. The REST test's gate deadline is now below the lock timeout so its diagnostic wins, and the helper keeps the body of an unexpected status. Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com> Dispatch-Task: pr-maint-b559cdc403fbab978032ffbc8604051f
The Headers-like guard left two shapes open: a raw one-shot stream body without a status, and a record a response cannot copy (it assigns a Headers onto the shared object, which the next request then merges into in place). Eligibility is now an allow-list: a copyable record without a body or a Headers-like headers, or an absent answer. Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com> Dispatch-Task: pr-maint-b559cdc403fbab978032ffbc8604051f
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com> Dispatch-Task: pr-maint-b559cdc403fbab978032ffbc8604051f
Replace the per-shape guards (body, Headers-like headers, passthrough contentType, untracked records) with one invariant: a record goes to several requests only if every value in it is plain data, scanned within a bounded budget. That also covers a one-shot iterator held anywhere in the record, which the shape guards missed. A record of plain data with a text body or plain headers is now shared too. Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com> Dispatch-Task: pr-maint-b559cdc403fbab978032ffbc8604051f
The plain-data scan ran on every source fill and was read only when a waiter found the store invalid, so it moves to that waiter and is memoized on the fill; the default miss path pays nothing. The scan now walks own keys by descriptor, so a symbol-keyed iterator, a non-enumerable body and a getter all refuse sharing (without invoking the getter), and a sparse array is bounded by its length. Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com> Dispatch-Task: pr-maint-b559cdc403fbab978032ffbc8604051f
…ays by descriptor A revoked proxy or a throwing trap made the scan throw inside the lock waiter's callback, stranding that waiter; it now counts as not plain data. Arrays go through the same descriptor walk, so an own Symbol.iterator, an Array subclass or an index getter refuse sharing, and holes cost nothing because only present keys are visited. Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com> Dispatch-Task: pr-maint-b559cdc403fbab978032ffbc8604051f
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com> Dispatch-Task: pr-maint-b559cdc403fbab978032ffbc8604051f
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com> Dispatch-Task: pr-maint-b559cdc403fbab978032ffbc8604051f
kriszyp
force-pushed
the
fix/sourcedfrom-negative-miss-coalescing
branch
from
October 27, 2026 01:37
1e13349 to
88c71ad
Compare
kriszyp
marked this pull request as ready for review
October 27, 2026 03:09
kriszyp
added a commit
that referenced
this pull request
Oct 27, 2026
- In Table.prototype.allowRead, route select=['*'] for attribute-scoped
roles through the permitted-attribute expansion branch rather than
falling back whenever a brace sub-select filters to empty, keeping
child{*} and child[select(*)] consistent while returning {} when an
unpermitted attribute is selected
- Add regression assertions for attribute-scoped child{*},
child[select(*)], child{parentId}, and child[select(parentId)]
- Trim 10 wrapped lines in resources/DESIGN.md so the merged file stays
under the 1000-line check-design-docs budget after #3417
Dispatch-Task: harper-3221-rebase-after-3415
Co-Authored-By: Gemini <noreply@google.com>
kriszyp
added a commit
that referenced
this pull request
Oct 27, 2026
* Fix REST select/relationship projections and ops-API object get_attributes
- F-337: Mark single-record responses uncacheable (`UNCACHEABLE_TIMESTAMP`)
when `@relationship(from:)` resolves non-null foreign key IDs, mirroring
`@relationship(to:)` so single-record `select` or `.property` traversal
does not emit a stale parent ETag after related target rows change.
- F-338: Avoid double `transformForSelect` when `static loadAsInstance = true`
by delegating projection to `Resource.get`, and guard null/missing records
with `record?.[target.property]` when `static loadAsInstance = false` so
missing, deleted, or expired records return 404 instead of throwing 500.
- F-342: Preserve single named sub-select objects (`?select(child{id,name})`)
instead of unwrapping their inner array onto the parent query (#1415),
reject bracketed condition syntax inside `select(...)` (`?select(rel[a=b])`)
with a 400 `SyntaxViolation`, and use an identity transform in
`Resource.transformForSelect` for `{ name }` selectors without a nested
`select` array (e.g., `?select(id,children[sort(-id)])` on single-record GET).
- F-341: Allow nested `{ name, select }` objects in `search_by_hash` /
`search_by_id` `get_attributes` validation and recursively validate nested
relationship `select` entries against target table schemas across all four
search operations while keeping nested JSON (`Any`) sub-keys unchecked.
Dispatch-Task: harper-rest-select-projection-fixes
Co-Authored-By: Gemini <noreply@google.com>
* Address pre-push review findings for select and get_attributes projection
Dispatch-Task: harper-rest-select-projection-fixes
Co-Authored-By: Gemini <noreply@google.com>
* Address round 2 pre-push review findings for select sub-queries and validation
- Pre-flatten sort clauses and pre-compute comparable keys once per array in Resource.applySubQuery, sharing convertToComparableKeys with Table.ts.
- Apply sub-query offset/limit slicing in Table.transformEntryForSelect, guard dbIterator.return/throw for array iterators, and await nested async promises in handleResolvedValue.
- Support class constructor schemas in searchValidator visited tracking and refine unit tests.
Dispatch-Task: harper-rest-select-projection-fixes
Co-Authored-By: Gemini <noreply@google.com>
* Validate limit/offset bounds and harden parser hang probe
Reject negative or non-integer limit and offset values in parseBlock
and searchValidator, normalize nested offset/limit bounds to
non-negative integers in Resource.transformForSelect and
Table.transformEntryForSelect, and harden the subprocess probe in
query-parse.test.js by stripping --inspect flags, calling
process.exit(0), and raising the timeout to 45s.
Dispatch-Task: harper-rest-select-projection-fixes
Co-Authored-By: Gemini <noreply@google.com>
* Update design notes, count comments, and test titles for review findings
Clarify in resources/DESIGN.md that Resource.transformForSelect caches
nested sub-transforms in a WeakMap keyed by the selector object and that
relationship.to projections are uncacheable via Table.search. Clarify in
resources/Table.ts and queryCount.test.js that non-finite or negative
limits on the count path apply to programmatic targets. Remove the stale
TODO in Resource.ts and replace internal tracker IDs in test titles with
GitHub issue references (#1415, #3136).
Dispatch-Task: harper-rest-select-projection-fixes
Dispatch-Task: pr-maint-70caff910b7e4cb82c05ffb1adba931f
Co-Authored-By: Gemini <noreply@google.com>
* Treat nested wildcard sub-select as identity in transformForSelect
In Resource.transformForSelect's nested-selector branch, treat a subSelect
of '*' or ['*'] as an identity transform ((v) => v), matching
Table.transformEntryForSelect (Table.ts:6877). Previously,
REST GET /Table/id?select(rel{*}) and search_by_hash with
get_attributes: [{ name: 'rel', select: '*' | ['*'] }] recursed with
transformForSelect(['*'], targetResource) and tried to read a literal '*'
property on the related record, returning an empty object instead of all
related fields.
Dispatch-Task: harper-3221-nested-star-select
Co-Authored-By: Gemini <noreply@google.com>
* Address late review findings on searchValidator, nested sort, and allowRead
- Hoist collectUnknownAttributes, getVisitedSet, and findMatchingTableAttribute to module scope in validation/searchValidator.ts and allocate visited WeakMap/WeakSet lazily only when an object selector is encountered.
- Reject nameless or malformed object selectors in searchValidator without throwing TypeError on non-function startsWith/toString or bypassing catalog checks via attribute: '*'.
- Avoid per-item/per-clause selectFromObject closure allocations in Resource.ts nested sort via readPropertyFromObject and specialize single-clause sorts.
- Preserve nested sort, limit, and offset on relationship selectors without an inner select in Table.prototype.allowRead for non-superusers.
- Name Resource.transformForSelect explicitly for the selector-keyed WeakMap sub-transform cache in resources/DESIGN.md.
Dispatch-Task: late-review-kriszyp_harper_3221-993c671a4573-aa6bb452
Dispatch-Task: pr-maint-70caff910b7e4cb82c05ffb1adba931f
Co-Authored-By: Gemini <noreply@google.com>
* Normalize braced relationship sub-selects in Table.prototype.allowRead
Keep braced property-set arrays (e.g. select(child{id,label})) in Table.prototype.allowRead when a non-superuser has table-level read permission on the related table instead of collapsing to the bare relationship name. Also deduplicate node:child_process/node:path imports in query-parse.test.js and migrate #js/server/threads/manageThreads to #src/ in restSelectProjectionFixes.test.js after rebasing onto main.
Dispatch-Task: harper-3221-rebase-after-3415
Co-Authored-By: Gemini <noreply@google.com>
* Address pre-push review findings after rebase onto #3571
- Normalize braced sub-selects (property.select = Array.from(property))
before calling relatedResource.allowRead(user, property, context) and
verify nested sort paths against related table read permissions in
Table.prototype.allowRead
- Check query.parseError before calling resource.get(query) on
loadAsInstance = true resources
- Share index-sorted parallel-key sortAndSliceArray helper between
Resource.transformForSelect and TableResource.transformEntryForSelect
for plain-JSON array sub-selects
- Defer UNCACHEABLE_TIMESTAMP on @relationship(from:) until after empty
FK arrays short-circuit
- Avoid catching and doubling parseError in no-target parseQuery()
- Update resources/DESIGN.md and add regression tests
Dispatch-Task: harper-3221-rebase-after-3415
Co-Authored-By: Gemini <noreply@google.com>
* Fix scoped sub-select wildcard fallback and trim resources/DESIGN.md
- In Table.prototype.allowRead, route select=['*'] for attribute-scoped
roles through the permitted-attribute expansion branch rather than
falling back whenever a brace sub-select filters to empty, keeping
child{*} and child[select(*)] consistent while returning {} when an
unpermitted attribute is selected
- Add regression assertions for attribute-scoped child{*},
child[select(*)], child{parentId}, and child[select(parentId)]
- Trim 10 wrapped lines in resources/DESIGN.md so the merged file stays
under the 1000-line check-design-docs budget after #3417
Dispatch-Task: harper-3221-rebase-after-3415
Co-Authored-By: Gemini <noreply@google.com>
* fix(resources): preserve default permitted attributes when scalar select filters to empty
* Expand wildcard in-place in allowRead and deduplicate query-parse subprocess check
- Expand '*' in-place inside Table.prototype.allowRead's selectArray
flatMap when attribute permissions are active so select(*, rel{...})
and nested rel{*, subrel{...}} retain permitted scalar attributes
alongside authorized relationship sub-selects
- Fold the 'select(id)?limit(1)' subprocess timeout probe into the
existing '?' tokenizer child-process test in query-parse.test.js
- Replace PR-relative phrasing in resources/DESIGN.md with durable
wording
Dispatch-Task: harper-3221-rebase-after-3415
Co-Authored-By: Gemini <noreply@google.com>
* fix(resources): handle * in transformForSelect, scope braced sub-select fallback, and cache sub-select transforms
---------
Co-authored-by: Gemini <noreply@google.com>
This was referenced Nov 13, 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.
⊙ Problem
Refs #1398.
Concurrent reads of one key on a
sourcedFromtable coalesce only when the first read stores a record. When the source reports the record missing, or the read usesnoCacheStore, 20 concurrent reads make 20 origin calls, one after another.🤔 Is the requirement right?
Yes, with one exception. A response is not shared if its source set
noCacheStoreon its own context. That marks the response as per-request.💡 Solution
Headersor blob refetch: a response consumes or changes those.noCacheStorerecord goes only to waiters that also asked fornoCacheStore.🤔 Should concurrent
no-storerequests share one source answer?Yes, as the task asked. A caller-dependent source must set
noCacheStoreitself; the cache already shares its other answers. Reverting is one condition.⚖️ Alternatives
noCacheStore.🔧 Changes
resources/Table.ts: a per-tablesourceFillsmap of this thread's in-flight fills, keyed bywriteKeyIdso0and-0stay apart. Each caller captures the in-flight fill beforetryLock. The leader registers its fill in the same synchronous run as its successfultryLock. The waiter callback still reads the store first, then falls back to the fill's outcome.resources/DESIGN.md,DESIGN.md: the sharing rules and their index entry.✅ Verification
noCacheStoreand0/-0cases fail (20 calls where 1 is expected).no-storereads each make one origin request (base: 20).cachingandResource-get-contextpass.Suite numbers
Before the rebase,
test:unit:resources4519 passing, 2 failing;test:unit:main7425 passing, 2 failing (all four pass alone);test:integration:all2558 passing, 12 cancelled (6 Ollama models missing locally, 6 insql-not-predicate.test.ts, which passes alone).check:design-docsfails onmaintoo:resources/DESIGN.mdis over its 1000-line budget, and this PR adds 4 lines. Tests hold each burst's first source call until the other 19 reads park on the record lock.Unit cases
absent record → 1 call; no negative caching across bursts;
noCacheStore→ 1 call, nothing stored; no-store leader with a caching waiter → the waiter caches; source-setnoCacheStore→ 20 calls, writable and read-only; cached record → 1 call; failed call → retried, failure not shared; failure then absence → 2 calls; failure while preparing the record → not shared;Headers, stream body, passthrough stream, iterator, uncopyable record, symbol/hidden/hole/getter/dead-proxy shapes, over-budget record → not shared; nested plain data with a text body → shared; stale entry behind a no-store leader → caching waiter refetches;0/-0kept apart.Framing-Verdict: better-alternative-exists (23a399dda158) — adopted: the existing lock waiter callback carries the fill.
PR QA: skipped @ 88c71ad — dispatch unavailable (no worker token in this session).
Docs: no companion needed; no public API or configuration change.
🤖 Generated with Claude Code (Claude Opus 5.5; rebased and updated by Claude Sonnet 5.5); posted via @kriszyp.
https://claude.ai/code/session_01T9ub6mcFSCB8x9He8FS35Z
Release target: main — a performance fix with no known backport need; it changes shared source-fill behavior
Related PRs: #3490 overlaps (same lock-release and waiter code in getFromSource), 9 others independent
Complexity: medium
Deferred findings
a44a4a185298b74fnit correctness —resources/Table.ts— outcome served when stored record later invalidated, evicted or expired — NOT FILEDec56d2c461bfc09cnit documentation —resources/Table.ts— suggest comment that sourceFills is thread-local — NOT FILEDc4640d13051102danit test-coverage —unitTests/resources/sourceFillCoalescing.test.js— source-identity guard and mixed-policy burst untested — NOT FILED996d31ce76294bfanit security —resources/Table.ts— Concurrent no-store waiters receive the leader's unstored record without comparing requester identity — NOT FILEDbb1bbbbb0ec64391nit reliability —resources/Table.ts— Synchronous throw between sourceFills.set and when(commitPromise) attachment orphans the fill map entry — NOT FILED1f2fab373441427fnit maintainability —integrationTests/resources/sourcedfrom-negative-miss-coalescing.test.ts— '; runtime resolves fine' in the ts-expect-error directive adds no constraint — NOT FILED1b3008c1343391d1nit performance —resources/Table.ts— unstoredRecord counts a tombstone existingEntry, forcing caching waiters behind a no-store leader into one extra origin call — NOT FILED158e3ef374d7c494nit test-coverage —unitTests/resources/sourceFillCoalescing.test.js— unit retry leaders rely on a fixed delay(10) to outlast re-parking waiters — NOT FILEDbecf8ece6a59110dnit correctness —resources/Table.ts— waiters share the leader's live source object; Date and ArrayBuffer-view leaves skip own-property inspection, admitting an own one-shot iter — NOT FILED183bc5e8f8c1978enit documentation —resources/DESIGN.md— note leaves resources/DESIGN.md at 998 of 1000 lines without pruning — NOT FILEDDispatch: task
harper-sourcedfrom-negative-miss-coalescing· queued by unknown · ran by claude/opus/high · worker kzyp-xps-1Review-Coverage: authored=claude; ran=gemini,codex,cursor-composer,cursor-muse; adjudicated=domain; blocked=muse(failed); declined=cursor-grok,cursor-kimi; rounds=17; full=2 @ 88c71ad
Review-Attention: study ~20m (critical: Table.ts; decisions: do-less-alternative, same-thread-only, no-store-sharing-semantics, scan-allowlist-vs-clone) @ 88c71ad