Visitar URL original
fix(vector): rebuild after an admitted one is no longer deferred forever at 10M (#7260) by lvca · Pull Request #9289 · ArcadeData/arcadedb · GitHub
Skip to content

fix(vector): rebuild after an admitted one is no longer deferred forever at 10M (#7260) - #9289

Merged
lvca merged 7 commits into
mainfrom
fix/7260-vector-rebuild-deferral
Oct 6, 2026
Merged

lvca merged 7 commits into
mainfrom
fix/7260-vector-rebuild-deferral

Conversation

@lvca

@lvca lvca commented Oct 6, 2026 •

Copy link
Copy Markdown
Member

Fixes #7260.

Problem

The online rebuild gate charged the freshly built on-heap graph as resident for the whole next build. Once the buffered writes of a long first rebuild outgrew what a heap of ~2x the graph can hold beside a second graph, every later rebuild was deferred and the delta scan grew without bound.

Fix

  • When the estimate does not fit, admitOnlineRebuild swaps the resident on-heap graph for its certified persisted twin (same ordinals; identity tracked by persistedGraphSource), re-prices the build against what the twin costs, and admits it. Nothing is changed unless the rebuild is admitted, and searches keep working (reading topology from pages until the replacement publishes).
  • New stat residentGraphDemotions.
  • The deferral WARNING is logged once per streak (FINE afterwards).

Tests

  • New Issue7260ResidentGraphDemotionTest (heap figures pinned via a package-private overload, no 24 GB fixture).
  • Related vector rebuild/admission tests re-run green.

Summary by CodeRabbit

  • Improvements
    • Online vector-index rebuilds can now proceed when retaining the in-memory graph would exceed the available heap, provided a suitable persisted graph is available. The persisted graph can temporarily take the in-memory graph’s place during the rebuild.
    • Rebuilds still defer when available memory is insufficient, and the in-memory graph remains in place when the persisted graph is not suitable. Search results continue to work across graph replacements.

…instead of deferring a rebuild (#7260)

The online rebuild gate priced a rebuild as the new graph plus the old on-heap graph resident, so once the delta buffer outgrew a heap of ~2x the graph every later rebuild was deferred and the delta scan grew without bound. When that estimate does not fit, the gate now swaps the resident graph for its certified on-disk copy (same ordinals), re-prices the build against what the copy costs, and admits it. The deferral WARNING is also logged once per streak.
@lvca lvca added this to the 26.11.1 milestone Oct 6, 2026
@lvca lvca self-assigned this Oct 6, 2026
@mergify

mergify Bot commented Oct 6, 2026 •

Copy link
Copy Markdown
Contributor

This pull request does not currently match the merge queue conditions, so it cannot be queued from here. The box comes back if it matches again.

@claude

claude Bot commented Oct 6, 2026

Copy link
Copy Markdown

Review

Overall a well-reasoned fix: the swap only happens once the rebuild is admitted, the identity check via `persistedGraphSource` is a safe certification, and `persistedGraphSource` is cleared when a build publishes. Comments below, roughly by importance.

Correctness / risk

  1. Twin equivalence rests on ordinals only. The check is `persistedGraphSource == resident` plus `getIdUpperBound()` equality. If the on-heap graph can be mutated in place after the persist (incremental adds, or deletes tracked via the orphan set from LSM_VECTOR: a vector the build left unreachable is served from the delta scan until close(); after reopen it is gone from every search while totalVectors still counts it #7190), the twin is no longer the same graph, and `ordinalToVectorId` and the manifest orphan set could disagree with it. Please confirm nothing mutates `graphIndex` after publish, or clear `persistedGraphSource` on any such mutation path. A test that inserts/deletes after the build and then demotes would pin this.
  2. Admitted but never completed. After demotion, if the rebuild then fails, is cancelled, or loses the `REBUILD_SEMAPHORE` race, the index stays on the on-disk graph with `persistedGraphSource == null`, so searches pay page reads indefinitely and nothing promotes it back. Consider restoring the on-heap graph on a failed or aborted rebuild if it is still referenced, or at least logging that the demotion is now sticky.
  3. Lifecycle of the replaced on-heap graph and pooled searchers. `releasePooledSearchers()` is called under the write lock; please verify that searchers already checked out against the old `resident` stay valid and are not returned to the pool afterwards (stale searcher on the old graph).
  4. Post-swap credit. `creditedHeap = availableHeap + onHeapBytes` assumes the heap reading still counts the on-heap graph. If the reading was taken after a GC that already dropped it (e.g. a previous demotion attempt, or a concurrent publish), the heap is double counted and a build that does not fit gets admitted, which is the OOM case this gate exists to avoid. A comment on why this cannot happen would help.

Minor

  • The INFO log's "available heap that X allows" prints `availableHeap / 100 * min(percent, 90)`, but the decision used `availableHeap + onHeapBytes` (credited). The number shown is not the one compared, which will confuse operators. Also `availableHeap / 100 * p` loses precision versus `* p / 100` (overflow is not a realistic concern here).
  • `rebuildDeferralStreak++` on a volatile is non-atomic. You note the race is benign, which is fine, but an `AtomicInteger`/`getAndIncrement` costs nothing and removes the comment.
  • `forgetPersistedGraphSourceForTest()` adds a test hook to production code. Acceptable given the package-private style used here, just flagging it.
  • The `catch (Exception)` path logs at WARNING on every attempt with no streak suppression, so a persistent loadGraph failure would spam the log at every trigger.
  • Per CLAUDE.md, prefer a single `@Tag`-free test is fine here, but `withIndex(Consumer)` swallows nothing; nice.

Tests

  • Good coverage of the 4 main branches with pinned heap figures. Missing: (a) the race where `graphIndex != resident` at lock time, (b) reclaim of page cache failing (`reclaimReadCacheRAM < needed`) returning false without changing state, (c) the streak logging (WARNING once, FINE after, reset after a successful build), (d) `twin.getIdUpperBound() != resident.getIdUpperBound()` mismatch, (e) an end-to-end check that after demotion the actual rebuild completes and the index is back on an on-heap graph with `residentGraphDemotions == 1`.

Style follows the surrounding code and the `assertThat(...).isTrue()` convention. No new dependencies, nothing security-sensitive.

@codacy-production

codacy-production Bot commented Oct 6, 2026 •

Copy link
Copy Markdown

Up to standards ✅

🟢 Issues 0 issues

Results:
0 new issues

View in Codacy

🟢 Metrics 17 complexity

Metric Results
Complexity 17

View in Codacy

🟢 Coverage 81.91% diff coverage · -5.75% coverage variation

Metric Results
Coverage variation ✅ -5.75% coverage variation
Diff coverage ✅ 81.91% diff coverage

View coverage diff in Codacy

Coverage variation details
Coverable lines Covered lines Coverage
Common ancestor commit (cbf701d) 214016 182272 85.17%
Head commit (bedfadc) 246709 (+32693) 195926 (+13654) 79.42% (-5.75%)

Coverage variation is the difference between the coverage for the head and common ancestor commits of the pull request branch: <coverage of head commit> - <coverage of common ancestor commit>

Diff coverage details
Coverable lines Covered lines Diff coverage
Pull request (#9289) 94 77 81.91%

Diff coverage is the percentage of lines that are covered by tests out of the coverable lines that the pull request added or modified: <covered lines added or modified>/<coverable lines added or modified> * 100%

NEW Get contextual insights on your PRs based on Codacy's metrics, along with PR and Jira context, without leaving GitHub. Enable AI reviewer
TIP This summary will be updated as you push new changes.

@coderabbitai

coderabbitai Bot commented Oct 6, 2026 •

Copy link
Copy Markdown

Review in Change Stack →

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration
  • Configuration used: defaults
  • Review profile: CHILL
  • Plan: Advanced
  • Run ID: cbf987d6-b1b6-4f89-b8d2-508fb3f32e30
📥 Commits

Reviewing files that changed from the base of the PR and between cbf701d and bedfadc.

📒 Files selected for processing (5)
  • engine/src/main/java/com/arcadedb/index/vector/LSMVectorIndex.java
  • engine/src/main/java/com/arcadedb/index/vector/LSMVectorIndexMetrics.java
  • engine/src/main/java/com/arcadedb/index/vector/VectorHeapBudget.java
  • engine/src/test/java/com/arcadedb/index/vector/Issue6503VectorHeapBudgetTest.java
  • engine/src/test/java/com/arcadedb/index/vector/Issue7260ResidentGraphDemotionTest.java

Included review availability: This review used your included allowance. Your plan provides up to 4 included reviews per hour; 2 remain after this review.


📝 Walkthrough

Walkthrough

Online vector-index rebuild admission can replace an eligible resident on-heap graph with its certified persisted on-disk twin when the rebuild does not fit the ordinary heap estimate. Heap-budget calculations now accept supplied measurements, and metrics and regression tests cover demotion and deferral behavior.

Changes

Online Rebuild Admission

Layer / File(s) Summary
Certify graphs and calculate rebuild budgets
engine/src/main/java/com/arcadedb/index/vector/LSMVectorIndex.java, engine/src/main/java/com/arcadedb/index/vector/VectorHeapBudget.java, engine/src/test/java/com/arcadedb/index/vector/Issue6503VectorHeapBudgetTest.java
The index records when a published graph is certified by persistence. Heap-budget calculations accept supplied heap readings and separate measured graph size from bytes kept resident. Collector classification tests cover tenured and non-tenured collectors.
Admit rebuilds with a persisted twin
engine/src/main/java/com/arcadedb/index/vector/LSMVectorIndex.java, engine/src/main/java/com/arcadedb/index/vector/LSMVectorIndexMetrics.java
When the ordinary estimate does not fit, admission can load and validate the certified on-disk twin, reclaim needed cache bytes, and swap it in if the revised estimate fits. Successful demotions increment a reported and resettable metric. Deferrals and demotion failures use per-streak or per-index log levels.
Validate demotion and deferral behavior
engine/src/test/java/com/arcadedb/index/vector/Issue7260ResidentGraphDemotionTest.java
Tests cover demotion, subsequent rebuilds, heap-collection credit, retained on-heap graphs, deferrals, missing persisted sources, and unchanged search results.

Priority: ➖ Normal

Estimated code review effort: 4 (Complex) | ~45 minutes

Change: Bug fix · Severity of issue fixed: Medium

Sequence Diagram(s)

sequenceDiagram
  participant LSMVectorIndex
  participant VectorHeapBudget
  participant PersistedGraph
  participant LSMVectorIndexMetrics
  LSMVectorIndex->>VectorHeapBudget: Estimate rebuild with the resident graph
  alt Ordinary estimate does not fit
    LSMVectorIndex->>PersistedGraph: Load the certified on-disk twin
    PersistedGraph-->>LSMVectorIndex: Return the twin and node upper bound
    LSMVectorIndex->>VectorHeapBudget: Estimate rebuild with the twin retained
    alt Revised estimate fits and graph identity is unchanged
      LSMVectorIndex->>LSMVectorIndex: Swap the resident graph under the write lock
      LSMVectorIndex->>LSMVectorIndexMetrics: Increment resident graph demotions
    else Revised estimate does not fit or eligibility fails
      LSMVectorIndex->>LSMVectorIndex: Record a memory deferral
    end
  end
Loading

Suggested reviewers: robfrank

Merge Risk: ⚪ Minimal · up to bedfa

No actionable issue remains from this review; the change is mergeable after normal checks.

Security Architecture Review

Security architecture risk: 🟡 Moderate · up to bedfa

The change includes identity checks and failure cleanup, but a new reference can keep an old graph alive during a rebuild intended to release it first. This can weaken protection against memory exhaustion. No new access or privilege boundary was established.

Retained concerns

  • Medium · reliability · inferred: The new persistedGraphSource reference can defeat release-first graph reclamation. Successful persistence stores the built graph in this field, but the releaseResidentGraphFirst branch does not clear it when dropping the resident graph, cache, and pooled searchers. For a certified on-heap generation, the old graph can therefore remain reachable while its replacement is constructed. Clearing certification at later publication is too late to prevent that overlap, weakening the low-memory rebuild path's failure containment and potentially causing memory exhaustion in the shared JVM.
Security review details

Security Blast Radius

  • inferred — The supported exposure is resource availability in a shared JVM: a large index's rebuild can affect other databases sharing heap and page cache. The reviewed evidence does not establish deployment tenancy, externally available authority to trigger shutdown rebuilding, or an unauthenticated attack path.

Trust Boundaries and Controls

  • observed — The new graph transition relies on internal object identity and committed persistence certification, not caller-supplied certification. Replacement is serialized under the existing index write lock, and unsuccessful candidates are not published.

Resilience and Maintainability Implications

  • inferred — The added certification token must participate in every graph-release transition. Its omission from the release-first branch creates ownership drift between the online demotion path and the low-memory close-rebuild path, reducing failure containment even though online identity checks are sound.

Hardening Proposals

  • proposed — Invalidate the certification reference together with resident-graph release under the same lock, and verify that a certified generation is no longer retained before replacement construction, including when that construction fails.
🚥 Pre-merge checks | ✅ 4 | ❓ 1

❌ Failed checks (1 inconclusive)

Check name Status Explanation Resolution
Docstring Coverage ❓ Inconclusive Docstring coverage is 33.33% which is insufficient. The required threshold is 80.00%. Docstring coverage is scoped to functions touched by this diff. Analyzed 33 functions across 4 files. (1 skipped: … Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Linked Issues check ✅ Passed Issue #7260 requires rebuild admission to avoid indefinite deferral when the resident on-heap graph causes the estimate to exceed the limit. LSMVectorIndex.admitByDemotingResidentGraph checks for a …
Out of Scope Changes check ✅ Passed The changed heap-budget logic and collector classification support safe heap credit for issue #7260. The residentGraphDemotions metric and deferral logging support operation of the fallback. The new…
Title check ✅ Passed The title identifies the vector rebuild deferral fix and references the 10M scenario. It is somewhat awkwardly phrased, but it clearly describes the main change.
Description check ✅ Passed The description explains the problem, the fix, the related issue, and the tests. It does not use the template headings and omits the checklist and additional notes, but it provides the key information…
Full details: Docstring Coverage

Explanation

Docstring coverage is 33.33% which is insufficient. The required threshold is 80.00%. Docstring coverage is scoped to functions touched by this diff. Analyzed 33 functions across 4 files. (1 skipped: 1 too large.)

✨ Finishing Touches 💡 1
📝 Generate docstrings 💡
  • Commit to this branch
  • Create a new PR
🧪 Generate unit tests (beta)
  • Commit to this branch
  • Create a new PR
  • Autopilot · Keep fixing CodeRabbit findings and required CI, and resolving merge conflicts

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@claude

claude Bot commented Oct 6, 2026

Copy link
Copy Markdown

Review

Overall a well-reasoned fix with good comments and a pinned-heap test overload. A few points:

Correctness / risk

  1. Heap credit is optimistic. creditedHeap = min(availableHeap + onHeapBytes, maxHeap) assumes the post-GC reading still counts the on-heap graph. If no GC ran since publish, the credit double-counts and the gate admits a build that may OOM. The comment acknowledges this and relies on the OOM handler plus cooldown as backstop. Consider only crediting when the last GC timestamp is after the graph's publish time, or crediting a fraction.
  2. Race window after demotion. After graphIndex = twin, if the admitted rebuild is then not started or fails early (permit or other gate), searches stay on the slower page-read graph until the next trigger. Please confirm that every return true path is followed by an actual build attempt, and that a failed build leaves persistedGraphSource == null while a later re-promotion is not needed.
  3. Twin lifecycle. The OnDiskGraphIndex now published as graphIndex must be closed when the build replaces it (and on index close/drop). Please verify the existing publish path closes a previous on-disk graph, since before this PR graphIndex was only an on-disk graph in the storeVectorsInGraph path.
  4. twin.getIdUpperBound() != resident.getIdUpperBound() is a weak equivalence check, but persistedGraphSource identity covers the real guarantee. Fine, but worth a comment.
  5. reclaimReadCacheRAM is called before the write lock and the identity re-check. If the re-check fails, the cache was reclaimed for nothing. Minor.

Quality

  • The INFO log prints creditedHeap * min(percent,90) / 100 as "available heap that X allows", which is misleading since it includes the credit. Same for the "did not fit" wording.
  • demotionFailureLogged is never reset, so a failure after a long healthy period is silent (FINE). Consider resetting on a successful demotion.
  • forgetPersistedGraphSourceForTest is a test hook in production code; acceptable given the package-private convention here.
  • rebuildDeferralStreak is incremented in the deferral path and reset only on a successful build; a demotion-admitted rebuild that fails keeps the streak, which seems intended.
  • Per CLAUDE.md, prefer final on locals/params; OnDiskGraphIndex twin is intentionally non-final, fine.

Tests
Issue7260ResidentGraphDemotionTest covers the main path. Please also confirm there are cases for: persist-failed (no certified twin => defer), a build publishing between decision and swap (identity re-check), and a failing loadGraph() (WARNING once, graph untouched). No wall-clock assertions seen, good.

Security / performance: no security concerns. Search latency during demotion is the documented trade-off; the new residentGraphDemotions stat makes it observable.

Nice work; mainly worth checking items 1 and 3.

@claude

claude Bot commented Oct 6, 2026

Copy link
Copy Markdown

Review

The approach is sound. Demoting to the certified persisted twin, with identity tracked by persistedGraphSource, is a clean way to break the unbounded-deferral loop. The design is also safe: nothing changes unless the rebuild is admitted, the decision is rechecked under the write lock (graphIndex != resident), and searches keep working. Tests pin the heap figures through a package-private overload, which is a good pattern.

Potential issues

  1. lastCollectionEndMillis() may over-credit the resident graph (VectorHeapBudget).

    • It takes the latest getLastGcInfo() end time across all collectors. liveHeapBytes() is built from per-pool post-collection usage.
    • With G1, a young collection after the publish updates the collector's GcInfo, but the old-gen CollectionUsage is only refreshed by a mixed or full or concurrent cycle. A large graph normally lives in old gen.
    • So heapReadingCountsResident can be true while the old-gen reading still predates the graph. The code then adds onHeapBytes on top of a reading that never counted it, and the build is over-admitted. That is the OOM risk this gate exists to prevent.
    • Consider tying the check to the same pool(s) that liveHeapBytes() sums, for example the old-gen/tenured pool's collector. Alternatively, drop the credit unless a full or old-gen collection is known to have ended after the publish. Being conservative costs one more deferral. Over-admitting costs an OOM.
  2. twin.close() on the decline path.

    • The comment says published twins are never closed because they hold no resource. If that is true, closing here is harmless.
    • Please verify that OnDiskGraphIndex.close() and gf.loadGraph() do not share a reader or supplier with the graph file, or with an already-published graph. Closing a shared reader would break live searches on a declined path. A test that declines and then searches would pin this.
  3. Metadata mismatch check. The only check is getIdUpperBound() equality, and the real guarantee is persistedGraphSource identity. That is fine. But persistedGraphSource = builtGraph is set after writeGraphManifest outside the visible lock scope, while graphIndex could have been replaced by then. The identity comparison in admitByDemotingResidentGraph covers this, so it is OK. A short comment would help.

  4. Log text. The existing INFO message was reworded from "deferred" to "not admitted as it stands". Check that no test or operator doc greps for the old wording. The new INFO line also has a very long single string segment, so please wrap it per the surrounding style.

  5. Streak counter. rebuildDeferralStreak only resets when a build publishes. If a rebuild is admitted and then fails or aborts, the next deferral logs at FINE even though it is a new problem. This is minor, but resetting on admission would match the "per streak" intent more closely.

Style / minor

  • com.sun.management.GarbageCollectorMXBean and GcInfo are used fully qualified, against the CLAUDE.md guideline. The name clash with the java.lang.management import makes this understandable, but add a comment saying so. Also note this is a JDK-specific API, though the LinkageError catch handles it.
  • forgetPersistedGraphSourceForTest and demotionFailureLogged are fine. The heapReadingCountsResident javadoc @param alignment is slightly off.

Tests

Coverage of the happy path, the uncertified-twin path and the swap-not-applied path looks good. I would add:

  • a test for the stale-reading case (heapReadingCountsResident=false) asserting that deferral still happens when the estimate does not fit without the credit;
  • a decline-then-search test for item 2;
  • a concurrent-publish race test (graphIndex != resident returns false and leaves state untouched).

Overall: good fix. Item 1 is the one I would resolve before merging.

@claude

claude Bot commented Oct 6, 2026

Copy link
Copy Markdown

Review

The approach is sound: demoting to the certified persisted twin, tracked by reference identity, and changing nothing unless the rebuild is admitted. Most of the concerns below are about the heap-credit logic.

Potential bugs

  1. VectorHeapBudget.managesTenuredPool can credit the graph when only a young GC ran. With G1, the G1 Young Generation bean lists G1 Old Gen among its memory pools, so managesTenuredPool returns true for it. After any young collection, lastCollectionEndMillis() would then be later than graphPublishedAtMs, and heapReadingCountsResident would be true. That contradicts the Javadoc, which says a young collection does not refresh the old-gen reading. The graph's bytes could then be credited twice, and a build that does not fit could be admitted, which is the OOM case the gate exists to prevent. Consider keying on the collector name (Old, MarkSweep, Concurrent, Mixed) or on whether the old pool's CollectionUsage actually changed. Pool-name substring matching is also fragile across ZGC, Shenandoah, Parallel and Serial. Please add a unit test for it.
  2. A demoted index stays on the on-disk graph if the rebuild never runs. After admitOnlineRebuild returns true, the rebuild can still fail to get the JVM-wide permit, or be cancelled or fail. Searches then pay page reads indefinitely, and persistedGraphSource is already null. The log says the next trigger retries, which is fine. Please confirm that a failed or cancelled rebuild after demotion leaves a retryable state, and add a test for that path.
  3. The stale-decision check graphIndex != resident is done under the write lock, but loadGraph() and cache reclamation happen before it. If the check fails, the page cache has already been shrunk for nothing. This is minor.

Code quality

  • forgetPersistedGraphSourceForTest() is a test hook in production code. Setting the field via reflection or placing the test in the same package would avoid it. It is a style point only.
  • The demotion INFO message does a second Math.min(percent, 90) computation that duplicates the logic inside VectorHeapBudget. This can drift, so expose it from there. The format string also has an overlong line.
  • In the one-argument admitOnlineRebuild(), getPageManager() is fetched twice (once in the wrapper, once in the body, where it is still used for reclaim). Reuse one reference.
  • deferralLevel uses getAndIncrement() == 0, and the streak resets only on a build or a demotion. A streak that spans a long quiet period will stay at FINE. Consider also resetting on a time gap.
  • CLAUDE.md prefers imports over fully qualified names. The com.sun.management.GarbageCollectorMXBean one is justified by the name clash, but a comment is already there, so it is fine.

Tests

  • The new test class is good, and pinning the heap figures through the overload is a clean idea.
  • theRebuildAfterADemotionCompletes... busy-waits with Thread.onSpinWait() against a wall-clock deadline for up to 30s. Use Awaitility or a short sleep to avoid burning a core, and keep the deadline only as a hang detector.
  • It also invokes startAsyncGraphRebuild by reflection. A package-private accessor would be sturdier.
  • Missing: a test for the young-GC false positive (item 1), for a persist failure leaving no twin (the forgetPersistedGraphSourceForTest path is presumably covered), and for concurrent publish during demotion (the graphIndex != resident branch).
  • Per CLAUDE.md, consider whether this vector test belongs in the vector lane, since it waits on a background rebuild.

Performance and security

No security concerns. The page-read search latency during demotion is documented and exposed via the new residentGraphDemotions stat, which is good. The lastCollectionEndMillis() MXBean walk runs on every admission attempt. That is cheap, but it could be computed lazily, only when the first check fails.

Overall this looks good. I would fix item 1 before merging.

@claude

claude Bot commented Oct 6, 2026

Copy link
Copy Markdown

Review

The approach is sound. The demotion swaps the resident graph only once the rebuild is certain to be admitted. persistedGraphSource is a reference-identity check, so a stale or failed persist can never vouch for a graph. The write-lock re-check (graphIndex != resident) closes the race with a concurrent publish. The OnDiskGraphIndex is closed on every decline path, and the test seam (pinned heap figures) avoids a 24 GB fixture.

Points to consider

  1. Page cache reclaimed before the swap can still be abandoned. In admitByDemotingResidentGraph, reclaimReadCacheRAM(reclaimNeeded) runs before the write lock is taken. If graphIndex != resident then, the method returns false after the cache was already shrunk. This is harmless but wasteful. It could be worth moving the cheap identity check before the reclaim, in addition to the one under the lock.

  2. Demotion is sticky if the rebuild then fails. After the swap, persistedGraphSource is null. If the rebuild fails or is cancelled, the index serves from pages until the next successful rebuild. The log message says the next trigger retries, which is right. However, a failing rebuild now means a permanent latency regression, where previously it only meant a deferral. Consider whether residentGraphDemotions should be paired with a visible "serving from disk" indicator, or whether a failed rebuild after demotion should be logged at WARNING.

  3. lastCollectionEndMillis heuristics.

    • isTenuredCollector is a name-based blacklist: anything not containing "young/scavenge/copy/parnew/minor" counts as tenured. An unknown future collector name would count as tenured and grant the heap credit, which is the unsafe direction, even though the Javadoc says it is conservative. The -1 is only returned when the info is missing. A whitelist, or at least a note on this, would match the stated intent better.
    • jvmStart + info.getEndTime() is compared to System.currentTimeMillis(). This is fine, but clock adjustments can skew it. The effect is limited to one extra deferral or credit.
    • Fully qualified com.sun.management.* is justified by the comment, but it is a CLAUDE.md deviation. An import alias is not possible in Java, so this is acceptable. Note that com.sun.management is JDK-specific and the LinkageError catch covers it.
  4. Formatting. In the INFO log string in admitByDemotingResidentGraph, one line is well over the 160-column limit (... (%d MB on the heap), with about %d MB for the build. Searches read ...). The Javadoc @param alignment is also inconsistent in the new overload, and the new field declarations are misaligned (persistedGraphSource vs rebuildDeferralStreak).

  5. Wording change in an existing log. "deferred" became "not admitted as it stands" in the page-cache INFO message. Check that no existing test asserts on that text.

  6. Tests. There are new unit tests for the gate and for isTenuredCollector. I could not see an end-to-end test where a demoted index runs a search correctly and then gets its rebuild published. The searcher-pool release and the ordinalToVectorId validity after swap are the riskiest assumptions, so one such test would add real confidence. Also confirm the new test class carries the right @Tag if it is slow.

  7. Minor. demotionFailureLogged is never reset on a successful demotion path except at the swap, so the WARNING is once per index until a swap succeeds. That matches the comment. The catch (Exception) also covers RuntimeException from loadGraph, which is desired.

Overall: logic looks correct and well documented. Items 1, 3 and 6 are the ones I would address before merge.

🤖 Generated with Claude Code

@claude

claude Bot commented Oct 6, 2026

Copy link
Copy Markdown

Review of #9289 (resident graph demotion for deferred rebuilds)

Solid, carefully reasoned fix. Tracking the persisted twin by reference identity (persistedGraphSource, cleared on publish, set only after the manifest certifies) is a good design. Nothing changes unless the rebuild is admitted, and the tenured-collector whitelist fails safe (one more deferral, never an OOM).

Points to consider

  1. Wasted cache reclaim: in admitByDemotingResidentGraph, reclaimReadCacheRAM runs before the write lock. If the graphIndex != resident re-check then fails, the cache was evicted for nothing. Minor; re-checking right before reclaiming would narrow it.
  2. Failed rebuild after admission: the index stays on the on-disk twin until some rebuild succeeds, so searches pay a page read per visited node. This is documented. A test for 'admitted, build fails, next trigger retries against the twin' would pin it, if not already covered.
  3. Style: com.sun.management.GarbageCollectorMXBean/GcInfo are fully qualified in VectorHeapBudget.lastCollectionEndMillis. The name clash justifies it, so I accept the exception.
  4. forgetPersistedGraphSourceForTest and the package-private overload match existing patterns. Keep them package-private.
  5. The com.sun.management use is guarded by instanceof and LinkageError, so it degrades to -1 on other JVMs. A cheap test that lastCollectionEndMillis() doesn't throw would be nice.
  6. The INFO text changed from 'deferred' to 'not admitted as it stands'. Check that no test or doc greps for the old wording.

Tests: isTenuredCollector is well covered, including null and unknown names. Issue7260ResidentGraphDemotionTest pins heap figures through the overload, so no big fixture is needed.

No security concerns. Performance is fine: the path only runs on an admission miss, and costs one graph load plus a lock acquisition.

Nice work, only minor items above.

@codecov

codecov Bot commented Oct 6, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 73.40426% with 25 lines in your changes missing coverage. Please review.
✅ Project coverage is 76.93%. Comparing base (cbf701d) to head (bedfadc).

Files with missing lines Patch % Lines
...java/com/arcadedb/index/vector/LSMVectorIndex.java 68.18% 14 Missing and 7 partials ⚠️
...va/com/arcadedb/index/vector/VectorHeapBudget.java 86.95% 2 Missing and 1 partial ⚠️
...m/arcadedb/index/vector/LSMVectorIndexMetrics.java 80.00% 1 Missing ⚠️
Additional details and impacted files
@@            Coverage Diff            @@
##               main    #9289   +/-   ##
=========================================
  Coverage     76.93%   76.93%           
+ Complexity     3982     3981    -1     
=========================================
  Files          2128     2128           
  Lines        214016   214105   +89     
  Branches      45541    45560   +19     
=========================================
+ Hits         164656   164729   +73     
- Misses        31781    31789    +8     
- Partials      17579    17587    +8     

☔ View full report in Codecov by Harness.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

@lvca
lvca merged commit 0be1311 into main Oct 6, 2026
@lvca
lvca deleted the fix/7260-vector-rebuild-deferral branch October 6, 2026 12:12
tae898 added a commit to humemai/arcadedb-embedded-python that referenced this pull request Oct 6, 2026
…ted from the shipped 26.10.1 wheel (#228)

The 46 per-test-file pages under development/testing/ restated the tests' own names and docstrings and had drifted (a wrong auto-transaction default, a skip that does not exist, a marker table missing a test, 12 of 62 test files absent from the overview). They are replaced by one development/testing.md with a table of every test file, the markers, the zero-skips gate as it is now, what the docs tests run, and the section on writing a test.

Also removed: examples/basic.md, examples/import.md, examples/vectors.md (pointers that examples/index.md and the guide already cover) and getting-started/distributions.md (merged into installation.md; its two needles leave tests/test_docs_examples.py with it).

Known issues and the vectors guide now say what is true for the shipped wheel: upstream ArcadeData#9277, ArcadeData#9278, ArcadeData#9281, and ArcadeData#7260 are fixed upstream for 26.11.1 (PRs ArcadeData#9288 and ArcadeData#9289) and still present in 26.10.1; ArcadeData#9290 is still open. The JPype entries no longer say the wheel installs 1.7.1 (it declares jpype1>=1.5.0).

API, guide, and development pages: removed pre-26.10.1 workaround history for bugs the wheel ships fixed, sections that duplicated the guide, and wrong statements found against the source (schema of_type, set_wal_flush, troubleshooting entries, type-mapping table, wheel and server-stack sizes measured on the 26.10.1 wheel). Hunks that open PR #164 also touches are left alone.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Ubz3m6QXHESerCkehC1qgi
tae898 added a commit to humemai/arcadedb-embedded-python that referenced this pull request Oct 6, 2026
…ted from the shipped 26.10.1 wheel (#228)

The 46 per-test-file pages under development/testing/ restated the tests' own names and docstrings and had drifted (a wrong auto-transaction default, a skip that does not exist, a marker table missing a test, 12 of 62 test files absent from the overview). They are replaced by one development/testing.md with a table of every test file, the markers, the zero-skips gate as it is now, what the docs tests run, and the section on writing a test.

Also removed: examples/basic.md, examples/import.md, examples/vectors.md (pointers that examples/index.md and the guide already cover) and getting-started/distributions.md (merged into installation.md; its two needles leave tests/test_docs_examples.py with it).

Known issues and the vectors guide now say what is true for the shipped wheel: upstream ArcadeData#9277, ArcadeData#9278, ArcadeData#9281, and ArcadeData#7260 are fixed upstream for 26.11.1 (PRs ArcadeData#9288 and ArcadeData#9289) and still present in 26.10.1; ArcadeData#9290 is still open. The JPype entries no longer say the wheel installs 1.7.1 (it declares jpype1>=1.5.0).

API, guide, and development pages: removed pre-26.10.1 workaround history for bugs the wheel ships fixed, sections that duplicated the guide, and wrong statements found against the source (schema of_type, set_wal_flush, troubleshooting entries, type-mapping table, wheel and server-stack sizes measured on the 26.10.1 wheel). Hunks that open PR #164 also touches are left alone.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Ubz3m6QXHESerCkehC1qgi
tae898 added a commit to humemai/arcadedb-embedded-python that referenced this pull request Oct 6, 2026
…ted from the shipped 26.10.1 wheel (#229)

* docs: retire stale and duplicate Python bindings pages, fix what drifted from the shipped 26.10.1 wheel (#228)

The 46 per-test-file pages under development/testing/ restated the tests' own names and docstrings and had drifted (a wrong auto-transaction default, a skip that does not exist, a marker table missing a test, 12 of 62 test files absent from the overview). They are replaced by one development/testing.md with a table of every test file, the markers, the zero-skips gate as it is now, what the docs tests run, and the section on writing a test.

Also removed: examples/basic.md, examples/import.md, examples/vectors.md (pointers that examples/index.md and the guide already cover) and getting-started/distributions.md (merged into installation.md; its two needles leave tests/test_docs_examples.py with it).

Known issues and the vectors guide now say what is true for the shipped wheel: upstream ArcadeData#9277, ArcadeData#9278, ArcadeData#9281, and ArcadeData#7260 are fixed upstream for 26.11.1 (PRs ArcadeData#9288 and ArcadeData#9289) and still present in 26.10.1; ArcadeData#9290 is still open. The JPype entries no longer say the wheel installs 1.7.1 (it declares jpype1>=1.5.0).

API, guide, and development pages: removed pre-26.10.1 workaround history for bugs the wheel ships fixed, sections that duplicated the guide, and wrong statements found against the source (schema of_type, set_wal_flush, troubleshooting entries, type-mapping table, wheel and server-stack sizes measured on the 26.10.1 wheel). Hunks that open PR #164 also touches are left alone.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Ubz3m6QXHESerCkehC1qgi

* docs: testing.md says a strict xfail fires when the fix reaches the engine the suite runs on, which CI builds from upstream's snapshot jars (#228)

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Ubz3m6QXHESerCkehC1qgi

* docs: known issues says ArcadeData#9290 is fixed upstream (ArcadeData#9299, 26.11.1) and the tripwires are plain regression tests that fail on the 26.10.1 wheel (#228)

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Ubz3m6QXHESerCkehC1qgi

---------

Co-authored-by: Claude Sonnet 5.5 <noreply@anthropic.com>
tae898 added a commit to humemai/arcadedb-embedded-python that referenced this pull request Oct 18, 2026
…et" (#335)

The weekly docs pass (2026-10-19) found the bindings docs still describing six engine fixes as "fixed for 26.11.1, which is not released yet". Each fix's merge commit is an ancestor of the 26.10.2 tag (e5040d0), so 26.10.2 carries them all.

- known-issues.md: the six entries leave the page by its own rule and are listed under "Fixed in 26.10.2"; the ArcadeData#9228, ArcadeData#9289 and light_edges notes in the guide and API pages, the wheel sizes (70.2 MiB download, 97.8 MiB unpacked, computed from the PyPI file) and the test docstrings follow.
- test_bridge_fixes: from ArcadeData#10253 the engine's Result.toJSON writes an undeclared LocalDate as days since the epoch, as it already wrote a declared DATE on 26.10.2. to_json_list keeps its documented midnight-UTC milliseconds (the #116 fix); the test no longer requires the two to be equal, and results.md stops saying they are.
- sync-upstream.sh --help prints the real path lists (it named 1 of 5 fork-owned paths and 4 of 7 allowlisted workflows); release.md and upstream-bindings-pr.md: a dead anchor, a wrong step number, an optional argument shown as required.
- FAIRNESS.md: a probe path that is not in the repository; PAGE-SPEC.md: f3_sparse_perquery exists and is skipped, not unbuilt.

Closes #335. Closes #99: its cause, ArcadeData#9694, is fixed by ArcadeData#9703, which 26.10.2 carries.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
tae898 added a commit to humemai/arcadedb-embedded-python that referenced this pull request Oct 18, 2026
…et" (#335) (#336)

The weekly docs pass (2026-10-19) found the bindings docs still describing six engine fixes as "fixed for 26.11.1, which is not released yet". Each fix's merge commit is an ancestor of the 26.10.2 tag (e5040d0), so 26.10.2 carries them all.

- known-issues.md: the six entries leave the page by its own rule and are listed under "Fixed in 26.10.2"; the ArcadeData#9228, ArcadeData#9289 and light_edges notes in the guide and API pages, the wheel sizes (70.2 MiB download, 97.8 MiB unpacked, computed from the PyPI file) and the test docstrings follow.
- test_bridge_fixes: from ArcadeData#10253 the engine's Result.toJSON writes an undeclared LocalDate as days since the epoch, as it already wrote a declared DATE on 26.10.2. to_json_list keeps its documented midnight-UTC milliseconds (the #116 fix); the test no longer requires the two to be equal, and results.md stops saying they are.
- sync-upstream.sh --help prints the real path lists (it named 1 of 5 fork-owned paths and 4 of 7 allowlisted workflows); release.md and upstream-bindings-pr.md: a dead anchor, a wrong step number, an optional argument shown as required.
- FAIRNESS.md: a probe path that is not in the repository; PAGE-SPEC.md: f3_sparse_perquery exists and is skipped, not unbuilt.

Closes #335. Closes #99: its cause, ArcadeData#9694, is fixed by ArcadeData#9703, which 26.10.2 carries.

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

LSM_VECTOR: at 10M the rebuild after an admitted one is deferred again, because the estimate keeps the old graph resident (#7184 follow-up)

1 participant