-
-
Notifications
You must be signed in to change notification settings - Fork 95
feat: seek to the paging offset instead of scanning to it #1283
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| @@ -0,0 +1,219 @@ | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| /* | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| * Copyright (c) 2017-2021 Nitrite author or authors. | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| * | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| * Licensed under the Apache License, Version 2.0 (the "License"); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| * you may not use this file except in compliance with the License. | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| * You may obtain a copy of the License at | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| * | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| * http://www.apache.org/licenses/LICENSE-2.0 | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| * | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| * Unless required by applicable law or agreed to in writing, software | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| * distributed under the License is distributed on an "AS IS" BASIS, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| * See the License for the specific language governing permissions and | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| * limitations under the License. | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| * | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| */ | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| package org.dizitart.no2.integration; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| import org.dizitart.no2.Nitrite; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| import org.dizitart.no2.collection.Document; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| import org.dizitart.no2.collection.FindOptions; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| import org.dizitart.no2.collection.NitriteCollection; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| import org.dizitart.no2.common.SortOrder; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| import org.dizitart.no2.filters.Filter; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| import org.dizitart.no2.mvstore.MVStoreModule; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| import org.junit.After; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| import org.junit.Before; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| import org.junit.Test; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| import java.util.ArrayList; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| import java.util.List; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| import static org.dizitart.no2.filters.FluentFilter.where; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| import static org.junit.Assert.assertEquals; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| import static org.junit.Assert.assertTrue; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| /** | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| * End-to-end tests for paged reads on an MVStore-backed database: every plan | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| * shape must return exactly the same pages as slicing the full result list. | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| */ | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| public class PagedFindTest { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| private static final int SIZE = 5000; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| private final String fileName = TestUtil.getRandomTempDbFile(); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| private Nitrite db; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| private NitriteCollection collection; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| @Before | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| public void setUp() { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| MVStoreModule storeModule = MVStoreModule.withConfig() | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| .filePath(fileName) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| .build(); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| db = Nitrite.builder() | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| .fieldSeparator(".") | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| .loadModule(storeModule) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| .openOrCreate(); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| collection = db.getCollection("paged"); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| for (int i = 0; i < SIZE; i++) { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| collection.insert(Document.createDocument("value", i).put("group", i % 5)); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| @After | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| public void tearDown() { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| if (db != null && !db.isClosed()) { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| db.close(); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| TestUtil.deleteDb(fileName); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| private static List<Integer> values(Iterable<Document> documents) { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| List<Integer> values = new ArrayList<>(); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| for (Document document : documents) { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| values.add(document.get("value", Integer.class)); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| return values; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| private static FindOptions options(String sortField, SortOrder sortOrder) { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| return sortField == null ? new FindOptions() : FindOptions.orderBy(sortField, sortOrder); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| private void assertPagingMatchesFullResult(Filter filter, String sortField, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| SortOrder sortOrder, int pageSize) { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| List<Integer> expected = values(collection.find(filter, options(sortField, sortOrder))); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| List<Integer> paged = new ArrayList<>(); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| long offset = 0; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| while (true) { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| FindOptions pageOptions = options(sortField, sortOrder) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| .skip(offset) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| .limit((long) pageSize); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| List<Integer> page = values(collection.find(filter, pageOptions)); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| if (page.isEmpty()) { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| break; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| assertTrue("page must not exceed page size", page.size() <= pageSize); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| paged.addAll(page); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| offset += pageSize; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
Comment on lines
+91
to
+102
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🩺 Stability & Availability | 🟡 Minor | ⚡ Quick win Bound the paging loop so a regression fails instead of hangs. The loop ends only when a page comes back empty. That condition is exactly what the offset logic must guarantee. If 💚 Proposed fix List<Integer> paged = new ArrayList<>();
long offset = 0;
- while (true) {
+ long maxPages = (expected.size() / pageSize) + 2;
+ for (long pageIndex = 0; ; pageIndex++) {
+ assertTrue("paging did not terminate", pageIndex < maxPages);
FindOptions pageOptions = options(sortField, sortOrder)
.skip(offset)
.limit((long) pageSize);📝 Committable suggestion
Suggested change
🤖 Prompt for AI Agents |
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| assertEquals(expected, paged); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| @Test | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| public void testNaturalOrderPagedIterationMatchesFullIteration() { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| for (int pageSize : new int[]{1, 7, 100, SIZE, SIZE + 100}) { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| assertPagingMatchesFullResult(Filter.ALL, null, null, pageSize); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| @Test | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| public void testSkipBeyondEndIsEmpty() { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| assertTrue(collection.find(Filter.ALL, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| new FindOptions().skip(SIZE + 1).limit(10)).toList().isEmpty()); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| assertTrue(collection.find(Filter.ALL, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| new FindOptions().skip(SIZE).limit(10)).toList().isEmpty()); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| @Test | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| public void testLastPageIsPartial() { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| List<Integer> lastPage = values(collection.find(Filter.ALL, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| new FindOptions().skip(SIZE - 3).limit(10))); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| assertEquals(3, lastPage.size()); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| @Test | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| public void testPagingAfterRemovalsMatchesFullIteration() { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| collection.remove(where("group").eq(2)); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| assertPagingMatchesFullResult(Filter.ALL, null, null, 97); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| @Test | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| public void testPagingWithOrderByMatchesFullIteration() { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| assertPagingMatchesFullResult(Filter.ALL, "value", SortOrder.Descending, 131); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| @Test | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| public void testPagingWithIndexedFilterMatchesFullIteration() { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| collection.createIndex("value"); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| assertPagingMatchesFullResult(where("value").gte(SIZE / 2), null, null, 89); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| @Test | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| public void testPagingWithIndexedFilterAndIndexOrderMatchesFullIteration() { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| collection.createIndex("value"); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| assertPagingMatchesFullResult(where("value").gte(SIZE / 2), | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| "value", SortOrder.Ascending, 89); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| @Test | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| public void testPagingWithNonIndexedFilterMatchesFullIteration() { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| assertPagingMatchesFullResult(where("group").eq(3), null, null, 53); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| @Test | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| public void testPagingWithOrFilterMatchesFullIteration() { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| assertPagingMatchesFullResult( | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| Filter.or(where("group").eq(1), where("group").eq(4)), | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| null, null, 71); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| /** | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| * Paging through the whole collection must not be quadratic. The dataset is | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| * larger than the page cache, so without the store-level skip every page | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| * request re-reads and deserializes all skipped entries from disk (~25 full | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| * scans in total, measured at ~10x the duration of one full scan); with it, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| * the paged lap costs about one full scan plus per-page seek overhead | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| * (measured at ~1-2x). The 5x bound sits between the two, to stay robust | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| * on slow or busy machines. | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| */ | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| @Test | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| public void testPagedIterationIsNotQuadratic() { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| int size = 20_000; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| int pageSize = 400; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| String payload = "x".repeat(1024); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| String perfFileName = TestUtil.getRandomTempDbFile(); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| MVStoreModule storeModule = MVStoreModule.withConfig() | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| .filePath(perfFileName) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| .cacheSize(1) // MB, much smaller than the data, to defeat the page cache | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| .build(); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| try (Nitrite perfDb = Nitrite.builder() | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| .fieldSeparator(".") | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| .loadModule(storeModule) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| .openOrCreate()) { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| NitriteCollection perfCollection = perfDb.getCollection("paged-perf"); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| for (int i = 0; i < size; i++) { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| perfCollection.insert(Document.createDocument("value", i).put("payload", payload)); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| long fullScanStart = System.nanoTime(); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| int count = values(perfCollection.find()).size(); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| long fullScanNanos = System.nanoTime() - fullScanStart; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| assertEquals(size, count); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| long pagedStart = System.nanoTime(); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| int pagedCount = 0; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| for (long offset = 0; offset < size; offset += pageSize) { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| pagedCount += values(perfCollection.find(Filter.ALL, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| new FindOptions().skip(offset).limit((long) pageSize))).size(); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| long pagedNanos = System.nanoTime() - pagedStart; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| assertEquals(size, pagedCount); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| double ratio = (double) pagedNanos / fullScanNanos; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| System.out.printf("paged iteration took %.1fx of a full scan%n", ratio); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| assertTrue(String.format( | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| "paged iteration took %.1fx of a full scan; skip does not seem to be pushed down", | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| ratio), | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| ratio < 5); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } finally { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| TestUtil.deleteDb(perfFileName); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🩺 Stability & Availability | 🟠 Major | 🏗️ Heavy lift
🧩 Analysis chain
🏁 Script executed:
Repository: nitrite/nitrite-java
Length of output: 32192
🌐 Web query:
H2 MVStore registerVersionUsage retain version cursor RootReference chunk freed while reading💡 Result:
The error message or situation you are describing—often manifesting as a Chunk not found exception during read operations—occurs because the H2 MVStore is a multi-versioned storage engine [1]. When you perform a read (such as iterating over a map), you are reading a specific snapshot of the data [1][2]. If concurrent writes occur, the store advances to new versions, and older versions that are no longer needed are garbage collected (freed) to prevent the database file size from growing indefinitely [1][2]. If your read operation takes longer than the time it takes for those underlying chunks to be reclaimed, the store attempts to access data that no longer exists, resulting in the failure [1]. To resolve or prevent this issue, you must explicitly inform the MVStore that you are still using a specific version so that it is not garbage collected [3][1]. You do this by wrapping your read operations—such as map iterations or other slow reads—with the registerVersionUsage and deregisterVersionUsage methods [3][1]. Recommended Implementation: 1. Call store.registerVersionUsage before beginning the read operation [3][1]. 2. Perform your read operations (e.g., iterating through keys, using a cursor) [1]. 3. Call store.deregisterVersionUsage(txCounter) in a finally block to ensure the resource is released [3][1]. Example: MVStore.TxCounter txCounter = store.registerVersionUsage; try { // Perform your cursor/iterator/read operations here for (String key: map.keySet) { // process... } } finally { store.deregisterVersionUsage(txCounter); } Important Considerations: - Resource Management: Failing to call deregisterVersionUsage will prevent the MVStore from reclaiming space, leading to unbounded file growth [3][1]. - Performance: While this prevents the exception, holding a version for too long keeps older data alive on disk [3]. You should group your read operations efficiently and release the usage counter as soon as the read is complete [3]. - Configuration: While you can adjust settings like setVersionsToKeep or retention time, explicitly declaring version usage is the recommended and most robust solution for long-running read operations or concurrent environments [1][4].
Citations:
🏁 Script executed:
Repository: nitrite/nitrite-java
Length of output: 50377
🏁 Script executed:
Repository: nitrite/nitrite-java
Length of output: 50377
🏁 Script executed:
Repository: nitrite/nitrite-java
Length of output: 14510
🏁 Script executed:
Repository: nitrite/nitrite-java
Length of output: 2265
🌐 Web query:
H2 2.4.240 MVStore registerVersionUsage RootReference cursor source💡 Result:
In H2 Database version 2.4.240, the components you referenced interact to manage MVStore version retention and thread-safe data access [1][2]. registerVersionUsage This method, located in the MVStore class, is used to register an active operation (such as a transaction or a long-running read) [1]. It increments a usage counter for the current version of the MVStore [1]. By doing so, it ensures that the current version, and all subsequent versions, are not reclaimed or dropped by the store's background processes while the operation is in progress [1][3]. You must call this method before starting a long-running read operation and pair it with a corresponding call to deregisterVersionUsage in a finally block to release the counter once the operation is complete [3]. RootReference A RootReference is an immutable structure that represents the state of an MVMap (a B-Tree) as a whole at a specific point in time [4]. It holds the root page reference for that version of the map. When performing operations like creating a Cursor, a RootReference is often passed to ensure the cursor iterates over a consistent snapshot of the map [5][6]. Cursor The Cursor class is used to iterate over elements (keys and values) in an MVMap [5][6]. When a Cursor is initialized, it takes a RootReference to determine the starting state of the tree traversal [5]. Because the cursor depends on the structural integrity of the pages referenced by that root, it is essential that the version represented by the RootReference remains valid during the cursor's lifetime [5][3]. Synthesis When performing a long-running operation, such as iterating over a map with a Cursor, the pattern is to: 1. Obtain a RootReference for the desired version (e.g., via map.flushAndGetRoot) [7]. 2. Call store.registerVersionUsage to prevent the store from reclaiming the data pages associated with that version [1][3]. 3. Instantiate the Cursor using that RootReference [5][6]. 4. Perform the iteration [3]. 5. Call store.deregisterVersionUsage in a finally block to allow the store to reclaim those versions once the cursor is no longer needed [1][6][3].
Citations:
🌐 Web query:
site:github.com/h2database/h2database MVStore registerVersionUsage dropUnusedVersions RootReference💡 Result:
In the H2 database's MVStore engine, registerVersionUsage, deregisterVersionUsage, and dropUnusedVersions are critical components used for managing MVStore's multi-version concurrency control, garbage collection, and preventing "Chunk not found" errors during concurrent operations [1][2][3]. registerVersionUsage and deregisterVersionUsage These methods are used to demarcate operations (such as reads or map iterations) that require a consistent snapshot of the store [1][4]. When an application performs a "slow" read or traversal on an MVMap, it must wrap these operations between a registerVersionUsage and a corresponding deregisterVersionUsage call [1][3]. - When registerVersionUsage is called, it returns a TxCounter that increments the usage count for the current store version [5]. - This prevents the MVStore from garbage collecting (reclaiming space from) the chunks associated with that version, even if the store is modified by other concurrent transactions [1][2]. - Failure to use these methods around long-running read operations often leads to MVStoreException: Chunk not found errors, as the required pages may be overwritten or reclaimed by the engine [1][3]. dropUnusedVersions This is an internal housekeeping method responsible for reclaiming disk space or memory from store versions that are no longer in use [5][6]. - It examines the store to see which versions are still active (i.e., have a non-zero usage count) and which have expired based on configured retention policies (e.g., versionsToKeep or retention time) [5][2]. - It is typically invoked automatically by the MVStore during commit or other maintenance cycles to ensure that the store does not grow indefinitely [5][7]. RootReference While not a method, RootReference typically refers to the structure in the MVStore that tracks the root page of a map for a specific version. Because MVStore uses copy-on-write (CoW) semantics, different versions of the store effectively have different root pages. The version tracking mechanisms described above ensure that older root pages—and the data pages they reference—remain accessible as long as an operation is registered to use that version [2][8]. In summary, users of the MVStore API must explicitly manage version lifecycle via register/deregister calls to protect their read operations, while the engine uses internal mechanisms like dropUnusedVersions to maintain the integrity and size of the underlying storage file [1][2][4].
Citations:
Register version usage before capturing the root.
The lazy cursor can read chunks after compaction reclaims them. Call
registerVersionUsage()beforeflushAndGetRoot(), and deregister it when the iterator is exhausted or closed.🤖 Prompt for AI Agents