Skip to content

Commit b08f5a4

Browse files
authored
fix: return full commit log when a commit tail is shorter than the fetch page (apache#5040)
Commits.commitLog paged through history by building a fixed-size ObjRef array of REVERSE_COMMIT_FETCH_SIZE and filling only tail.length entries. fetchMany returns null at the positions of the trailing null ids, and the iterator treats the first null as end-of-history, so a commit whose recent-ancestor tail is shorter than the fetch page size truncated the natural-order commit log at that point. With the default reference-previous-head-count of 20 (equal to the fetch size) the trailing null always coincided with genuine end-of-history, so the bug was not observable. With a smaller reference-previous-head-count the log stopped early, and because maintenance walks this log to decide retained objects, still-referenced objects could be dropped. Size the fetch array to min(REVERSE_COMMIT_FETCH_SIZE, tail.length) so no null padding reaches the iterator, mirroring the growing-list approach already used by commitLogReversed. Add a parameterized regression test that commits deeper than the tail with reference-previous-head-count below the fetch size and asserts the full log is returned. Signed-off-by: Anas Khan <83116240+anxkhn@users.noreply.github.com>
1 parent ac19147 commit b08f5a4

4 files changed

Lines changed: 75 additions & 2 deletions

File tree

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -92,6 +92,7 @@ request adding CHANGELOG notes for breaking (!) changes and possibly other secti
9292
- Deprecated `ALLOW_EXTERNAL_TABLE_LOCATION`. Use `ALLOW_EXTERNAL_METADATA_FILE_LOCATION` for external metadata file locations, including catalog config `polaris.config.allow.external.metadata.file.location`.
9393

9494
### Fixes
95+
- The NoSQL persistence commit log (`Commits.commitLog`) no longer stops early when a commit's recent-ancestor tail is shorter than the internal fetch page size. With a `polaris.persistence.reference-previous-head-count` smaller than the page size, the natural-order commit log previously truncated at the first short tail because trailing null entries in the fetch page were treated as end-of-history, which could also drop still-referenced objects during maintenance.
9596
- Python CLI REPL now shows a clear "Syntax error" message for malformed input instead of a generic "unexpected error" message.
9697
- Python CLI `setup apply` now exits with an error after any setup operation fails, while still attempting the remaining operations. Previously, individual failures were logged but the command reported success and exited with status 0.
9798
- Python CLI `tables list`, `tables get`, and `tables delete` commands now exit with status 1 when catalog API requests fail. Previously, these commands printed an error but exited successfully.

persistence/nosql/persistence/impl/build.gradle.kts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -83,6 +83,7 @@ dependencies {
8383
testFixturesCompileOnly(project(":polaris-immutables"))
8484
testFixturesAnnotationProcessor(project(":polaris-immutables", configuration = "processor"))
8585

86+
testFixturesImplementation(project(":polaris-idgen-api"))
8687
testFixturesImplementation(libs.guava)
8788

8889
testFixturesImplementation(libs.junit.pioneer)

persistence/nosql/persistence/impl/src/main/java/org/apache/polaris/persistence/nosql/impl/commits/CommitsImpl.java

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -216,8 +216,9 @@ protected C computeNext() {
216216
}
217217
lastCommit = null;
218218

219-
var ids = new ObjRef[REVERSE_COMMIT_FETCH_SIZE];
220-
for (var i = 0; i < REVERSE_COMMIT_FETCH_SIZE && i < tail.length; i++) {
219+
var pageSize = Math.min(REVERSE_COMMIT_FETCH_SIZE, tail.length);
220+
var ids = new ObjRef[pageSize];
221+
for (var i = 0; i < pageSize; i++) {
221222
ids[i] = objRef(type, tail[i], 1);
222223
}
223224

persistence/nosql/persistence/impl/src/testFixtures/java/org/apache/polaris/persistence/nosql/impl/commits/BaseTestCommitLogImpl.java

Lines changed: 70 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,15 +18,22 @@
1818
*/
1919
package org.apache.polaris.persistence.nosql.impl.commits;
2020

21+
import static java.util.function.Function.identity;
22+
2123
import java.util.ArrayList;
2224
import java.util.Collections;
2325
import java.util.Iterator;
2426
import java.util.List;
2527
import java.util.Optional;
2628
import java.util.OptionalLong;
29+
import java.util.UUID;
2730
import java.util.stream.Collectors;
2831
import java.util.stream.IntStream;
32+
import org.apache.polaris.ids.api.IdGenerator;
33+
import org.apache.polaris.ids.api.MonotonicClock;
2934
import org.apache.polaris.persistence.nosql.api.Persistence;
35+
import org.apache.polaris.persistence.nosql.api.PersistenceParams;
36+
import org.apache.polaris.persistence.nosql.api.backend.Backend;
3037
import org.apache.polaris.persistence.nosql.testextension.PersistenceTestExtension;
3138
import org.apache.polaris.persistence.nosql.testextension.PolarisPersistence;
3239
import org.assertj.core.api.SoftAssertions;
@@ -41,6 +48,9 @@
4148
public abstract class BaseTestCommitLogImpl {
4249
@InjectSoftAssertions protected SoftAssertions soft;
4350
@PolarisPersistence protected Persistence persistence;
51+
@PolarisPersistence protected Backend backend;
52+
@PolarisPersistence protected MonotonicClock clock;
53+
@PolarisPersistence protected IdGenerator idGenerator;
4454

4555
@ParameterizedTest
4656
@ValueSource(ints = {0, 1, 3, 19, 20, 21, 39, 40, 41, 255})
@@ -131,6 +141,66 @@ public void commitLogOffsets(int offsetIndex, TestInfo testInfo) throws Exceptio
131141
.containsExactlyElementsOf(chronological);
132142
}
133143

144+
/**
145+
* When a commit's {@code tail} is shorter than the number of commits fetched per page, the
146+
* "natural" commit log must not stop early. This exercises {@code referencePreviousHeadCount}
147+
* values below the internal reverse-fetch page size, so pages contain fewer entries than the page
148+
* size. That previously caused {@code commitLog} to truncate the history at the first commit of a
149+
* short tail.
150+
*/
151+
@ParameterizedTest
152+
@ValueSource(ints = {2, 3, 5})
153+
public void commitLogShortTail(int referencePreviousHeadCount, TestInfo testInfo)
154+
throws Exception {
155+
var refName =
156+
testInfo.getTestMethod().orElseThrow().getName() + "-" + referencePreviousHeadCount;
157+
var numCommits = 50;
158+
159+
var reducedPersistence =
160+
backend.newPersistence(
161+
identity(),
162+
PersistenceParams.BuildablePersistenceParams.builder()
163+
.referencePreviousHeadCount(referencePreviousHeadCount)
164+
.build(),
165+
UUID.randomUUID().toString(),
166+
clock,
167+
idGenerator);
168+
169+
reducedPersistence.createReference(refName, Optional.empty());
170+
171+
var committer =
172+
reducedPersistence.createCommitter(refName, SimpleCommitTestObj.class, String.class);
173+
for (int i = 0; i < numCommits; i++) {
174+
var payload = "commit #" + i;
175+
committer.commit(
176+
(state, refObjSupplier) ->
177+
state.commitResult(
178+
"foo",
179+
ImmutableSimpleCommitTestObj.builder().payload(payload),
180+
refObjSupplier.get()));
181+
}
182+
183+
var commits = reducedPersistence.commits();
184+
var expectedPayloads =
185+
IntStream.range(0, numCommits).mapToObj(i -> "commit #" + i).collect(Collectors.toList());
186+
187+
// "reversed" (most recent commit last) already used a growing page list and returned the full
188+
// history; assert it as a baseline.
189+
soft.assertThatIterator(commits.commitLogReversed(refName, 0L, SimpleCommitTestObj.class))
190+
.toIterable()
191+
.extracting(SimpleCommitTestObj::payload)
192+
.containsExactlyElementsOf(expectedPayloads);
193+
194+
// "natural" (most recent commit first) must return every commit, not stop at the first short
195+
// tail.
196+
Collections.reverse(expectedPayloads);
197+
soft.assertThatIterator(
198+
commits.commitLog(refName, OptionalLong.empty(), SimpleCommitTestObj.class))
199+
.toIterable()
200+
.extracting(SimpleCommitTestObj::payload)
201+
.containsExactlyElementsOf(expectedPayloads);
202+
}
203+
134204
private static List<SimpleCommitTestObj> toList(Iterator<SimpleCommitTestObj> iterator) {
135205
var result = new ArrayList<SimpleCommitTestObj>();
136206
iterator.forEachRemaining(result::add);

0 commit comments

Comments
 (0)