perf(posting): non-blocking UID cache warming, plus review follow-ups for #9809 - #2
Merged
gooohgb merged 3 commits intoSep 2, 2026
Conversation
…lock calculateUids held the published list's write lock across a full walk of the list, which for a multi-part list also reads every split from Badger. The commit path wants that same lock: UpdateCachedKeys -> updateItemInCache -> setMutationAfterCommit, called from commitOrAbort on the serial Raft apply loop, ahead of the ProcessDelta that releases waiting reads. One slow warm therefore stalled every commit for the group rather than only the readers of that key. Warm a private copy instead. A CAS elects one warmer per list, the walk runs on the copy readFromCache already makes, and the result is handed to the published list under a short write lock that drops it if a commit landed meanwhile. A reader that loses the election serves its read unwarmed, which is what every reader did before the optimization existed. Two related fixes on the same path: - A warm failure no longer fails the read. Warming is an optimization, so a transient Badger error reading a split part now logs and serves the list unmaterialized instead of turning a cache hit into a query error. - The re-set that refreshes the ristretto cost skips an entry that was evicted or dropped by a rollup during the warm, rather than resurrecting it. TestWarmCachedUidsWalksWithoutTheCachedListsWriteLock deadlocks against the previous behavior, so it fails when the walk is moved back onto the published list. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…ole walk A negative first has no early stop, so Uids() materializes the entire list before taking the last N off the end. Returning that as a view pinned the full []uint64 for the lifetime of the response: 8MB retained to hand back ten uids on a million-uid list. The retention only became reachable with the negative-pagination fix in 441d303. Before it, the opt.First != 0 stop check truncated the walk to a single posting, so the pinned array was one element long. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The uid_in branch still built ListOptions.First from int(q.First + q.Offset), the int32 arithmetic uidReadFirst was added to replace; MaxInt32 + offset wraps negative there. It is latent today, because calculatePaginationParams forces offset to zero whenever first is the unbounded sentinel and the branch only tests whether the intersection came back non-empty, but the invariant belongs in one place. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
3 tasks
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.
Follow-ups to the review on dgraph-io#9809, based on your
2b990b48. Three commits, each one independent — take, change, or drop any of them.Merging this into
fix-calculated-uids-materializationfolds it into dgraph-io#9809.1.
perf(posting): warm cached UID slices off the published list's write lockThis supersedes #1 — same shape you landed there, plus the reason I now think it should go in before dgraph-io#9809 merges rather than after.
calculateUidsheld the published list's write lock across a full walk, which for a multi-part list also reads every split from Badger (readListPart). The commit path wants that same lock:commitOrAbortruns onprocessApplyCh, which is explicitly serial, and theProcessDeltathat releases waiting reads comes afterUpdateCachedKeys. So one slow warm stalled every commit for the group rather than only the readers of that key.The fix is what you wrote in your PR: a CAS elects one warmer, the walk runs on the copy
readFromCachealready makes, and the result is handed over under a short write lock that drops it if a commit landed meanwhile. Differences from yours:needsUidWarm()is checked on the published list under the read lock thatreadFromCachealready holds. Your version checkedlCopy.mutationMap.currentEntries == nil, butclone()never copiescurrentEntries(posting/list.go:133-148), so that term was always true and said nothing about the cached list.publishCalculatedUidsrechecks it under the lock either way, so it was harmless — it just read as if it meant something.uidWarmState atomic.Int32broke the alignment of theListfield block, sogofmt -lflaggedposting/list.goandtrunk checkwould have failed. Realigned.warmCachedUids, andcalculateUidscarries a doc comment stating the ownership contract, so the walk doesn't drift back onto a shared list later.Two related fixes on the same path, folded into this commit because they touch the same lines:
readFromCachepropagated the error, so a transient Badger error reading a split part (orErrTsTooOldout ofiterate) turned a cache hit into a query error — something that path could not do before fix(query): avoid eager UID materialization on posting reads dgraph-io/dgraph#9809. Warming is an optimization, so it logs and serves the list unmaterialized. Same on the disk path, where the behavior predates this PR.ml.cache.set(key, cacheItem)re-inserted even ifupdateItemInCachecalledml.del(key)during the warm (the rollup branch,p.Pack != nil).resetIfCurrentonly re-sets while it is still the live entry. TheminTs <= readTs <= maxTsguard kept this from serving a wrong snapshot, so it was hygiene rather than correctness, but the window was as long as a warm.TestWarmCachedUidsWalksWithoutTheCachedListsWriteLockholds the published list's read lock across the walk, so it deadlocks against the old behavior — I confirmed it fails in 5s whenwarmCachedUidsis pointed back at the shared list.TestReadUidsWarmsCachedPostingListConcurrentlynow asserts what actually has to hold with a non-blocking warm: every one of the 32 readers returns all 4096 uids whether or not it won the election, and the cached entry is warm at the end.2.
perf(posting): copy the negative-first tail instead of pinning the whole walkYour negative-pagination fix is right, and it's what makes this reachable. A negative first has no early stop, so
Uids()materializes the whole list and then takes the last N off the end — as a view, which pinned the full[]uint64for the lifetime of the response. 8MB retained to hand back ten uids on a million-uid list, in a PR about not materializing UID slices. Before 441d303 theopt.First != 0stop check truncated the walk to one posting, so the pinned array was one element long.3.
fix(worker): route the uid_in read through uidReadFirstworker/task.go:971 still built
Firstfromint(q.First + q.Offset), the int32 arithmeticuidReadFirstexists to replace;MaxInt32 + offsetwraps negative. Latent today, sincecalculatePaginationParamsforces offset to zero whenever first is the unbounded sentinel and the branch only tests whether the intersection came back non-empty — but the invariant belongs in one place.worker/match.go:76andworker/trigram.go:29passint(q.First)with no offset, so they're unaffected.Verification
go build ./...,gofmtclean,go vetwith no new findings,go test ./posting/ -racegreen in full (176s),go test ./worker/green.One gap worth naming: the
uid_incall-site change in commit 3 has no test of its own.TestUidReadFirstcovers the arithmetic including the near-sentinel boundary, but the call site itself needs a live posting list.Also still open from the review, and deliberately left alone here: deleting the
cap()assertion rather than fixing it leaves the allocation-size bullet in the dgraph-io#9809 description untested. Your call whether that's worth a test.