This is an automated email from the ASF dual-hosted git repository.
danny0405 pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/hudi.git
The following commit(s) were added to refs/heads/master by this push:
new 17e36507ab1c fix(timeline-service): skip redundant refreshes for
trailing cleans (#19946)
17e36507ab1c is described below
commit 17e36507ab1c71e8b3561282f6a6f4798d296ca6
Author: Goutam Adwant <[email protected]>
AuthorDate: Thu Oct 8 21:18:35 2026 -0700
fix(timeline-service): skip redundant refreshes for trailing cleans (#19946)
* perf(timeline-service): avoid refreshing exact timeline extensions
* refactor(timeline-service): check trailing clean before view lag
---------
Co-authored-by: danny0405 <[email protected]>
---
.../hudi/timeline/service/RequestHandler.java | 10 +-
.../timeline/service/TestTimelineViewRefresh.java | 452 +++++++++++++++++++++
2 files changed, 460 insertions(+), 2 deletions(-)
diff --git
a/hudi-timeline-service/src/main/java/org/apache/hudi/timeline/service/RequestHandler.java
b/hudi-timeline-service/src/main/java/org/apache/hudi/timeline/service/RequestHandler.java
index 827cd268f507..bd34295d51e9 100644
---
a/hudi-timeline-service/src/main/java/org/apache/hudi/timeline/service/RequestHandler.java
+++
b/hudi-timeline-service/src/main/java/org/apache/hudi/timeline/service/RequestHandler.java
@@ -750,7 +750,8 @@ public class RequestHandler {
}
String localTimelineHash = localTimeline.getTimelineHash();
- // refresh if timeline hash mismatches
+ // Keep the final consistency check strict so server-ahead responses
still activate
+ // PriorityBasedFileSystemView's local fallback (apart from the
trailing-clean exception).
if (!localTimelineHash.equals(timelineHashFromClient)) {
return true;
}
@@ -766,9 +767,14 @@ public class RequestHandler {
String basePath =
ctx.queryParam(RemoteHoodieTableFileSystemView.BASEPATH_PARAM);
SyncableFileSystemView view = viewManager.getFileSystemView(basePath);
synchronized (view) {
+ HoodieTimeline localTimeline = view.getTimeline();
+ // The final check already accepts a trailing clean with an otherwise
matching timeline.
+ // Other extensions still need a sync: restore may have deleted their
extra instants.
+ if (!shouldThrowExceptionIfLocalViewBehind(localTimeline,
getTimelineHashParam(ctx))) {
+ return false;
+ }
if (isLocalViewBehind(ctx)) {
String lastKnownInstantFromClient = getLastInstantTsParam(ctx);
- HoodieTimeline localTimeline =
viewManager.getFileSystemView(basePath).getTimeline();
if (log.isInfoEnabled()) {
log.info("Syncing view as client passed last known instant {} as
last known instant but server has the following last instant on timeline: {}",
lastKnownInstantFromClient, localTimeline.lastInstant());
diff --git
a/hudi-timeline-service/src/test/java/org/apache/hudi/timeline/service/TestTimelineViewRefresh.java
b/hudi-timeline-service/src/test/java/org/apache/hudi/timeline/service/TestTimelineViewRefresh.java
new file mode 100644
index 000000000000..3ad679dc0756
--- /dev/null
+++
b/hudi-timeline-service/src/test/java/org/apache/hudi/timeline/service/TestTimelineViewRefresh.java
@@ -0,0 +1,452 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you 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.apache.hudi.timeline.service;
+
+import org.apache.hudi.common.engine.HoodieLocalEngineContext;
+import org.apache.hudi.common.model.HoodieBaseFile;
+import org.apache.hudi.common.table.timeline.HoodieInstant;
+import org.apache.hudi.common.table.timeline.HoodieTimeline;
+import org.apache.hudi.common.table.timeline.TimelineServiceClient;
+import org.apache.hudi.common.table.timeline.dto.BaseFileDTO;
+import org.apache.hudi.common.table.view.FileSystemViewManager;
+import org.apache.hudi.common.table.view.FileSystemViewStorageConfig;
+import org.apache.hudi.common.table.view.HoodieTableFileSystemView;
+import org.apache.hudi.common.table.view.PriorityBasedFileSystemView;
+import org.apache.hudi.common.table.view.RemoteHoodieTableFileSystemView;
+import org.apache.hudi.common.table.view.SyncableFileSystemView;
+import org.apache.hudi.common.testutils.FileCreateUtils;
+import org.apache.hudi.common.testutils.HoodieCommonTestHarness;
+import org.apache.hudi.common.testutils.HoodieTestUtils;
+import org.apache.hudi.common.util.CompactionUtils;
+import org.apache.hudi.common.util.Option;
+import org.apache.hudi.common.util.collection.Pair;
+import org.apache.hudi.storage.HoodieStorage;
+import org.apache.hudi.storage.StoragePath;
+
+import com.fasterxml.jackson.core.type.TypeReference;
+import org.junit.jupiter.api.AfterEach;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.params.ParameterizedTest;
+import org.junit.jupiter.params.provider.ValueSource;
+
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.concurrent.ExecutorService;
+import java.util.concurrent.Executors;
+import java.util.concurrent.Future;
+import java.util.concurrent.TimeUnit;
+import java.util.stream.Collectors;
+import java.util.stream.Stream;
+
+import static
org.apache.hudi.common.table.timeline.HoodieInstant.State.COMPLETED;
+import static
org.apache.hudi.common.table.timeline.HoodieInstant.State.INFLIGHT;
+import static
org.apache.hudi.common.table.timeline.HoodieInstant.State.REQUESTED;
+import static
org.apache.hudi.common.table.timeline.HoodieTimeline.CLEAN_ACTION;
+import static
org.apache.hudi.common.table.timeline.HoodieTimeline.COMMIT_ACTION;
+import static
org.apache.hudi.common.table.timeline.HoodieTimeline.COMPACTION_ACTION;
+import static
org.apache.hudi.common.table.timeline.HoodieTimeline.INVALID_INSTANT_TS;
+import static
org.apache.hudi.common.table.timeline.HoodieTimeline.LOG_COMPACTION_ACTION;
+import static
org.apache.hudi.common.table.timeline.HoodieTimeline.REPLACE_COMMIT_ACTION;
+import static
org.apache.hudi.common.table.timeline.TimelineServiceClientBase.RequestMethod.GET;
+import static
org.apache.hudi.common.table.view.RemoteHoodieTableFileSystemView.BASEPATH_PARAM;
+import static
org.apache.hudi.common.table.view.RemoteHoodieTableFileSystemView.LAST_INSTANT_TS;
+import static
org.apache.hudi.common.table.view.RemoteHoodieTableFileSystemView.LATEST_PARTITION_DATA_FILES_URL;
+import static
org.apache.hudi.common.table.view.RemoteHoodieTableFileSystemView.PARTITION_PARAM;
+import static
org.apache.hudi.common.table.view.RemoteHoodieTableFileSystemView.TIMELINE_HASH;
+import static
org.apache.hudi.common.testutils.HoodieTestUtils.INSTANT_GENERATOR;
+import static
org.apache.hudi.common.testutils.HoodieTestUtils.TIMELINE_FACTORY;
+import static org.junit.jupiter.api.Assertions.assertAll;
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertThrows;
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.ArgumentMatchers.anyString;
+import static org.mockito.Mockito.clearInvocations;
+import static org.mockito.Mockito.doAnswer;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.never;
+import static org.mockito.Mockito.spy;
+import static org.mockito.Mockito.times;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
+
+class TestTimelineViewRefresh extends HoodieCommonTestHarness {
+ private TimelineService server;
+ private TimelineServiceClient client;
+ private FileSystemViewManager manager;
+ private SyncableFileSystemView view;
+
+ @BeforeEach
+ void setUp() throws IOException {
+ metaClient = HoodieTestUtils.init(tempDir.toAbsolutePath().toString());
+ basePath = metaClient.getBasePath().toString();
+ manager = mock(FileSystemViewManager.class);
+ view = mock(SyncableFileSystemView.class);
+ when(manager.getFileSystemView(basePath)).thenAnswer(invocation -> view);
+ when(view.getLatestBaseFiles(anyString())).thenAnswer(invocation ->
Stream.empty());
+ server = new TimelineService(metaClient.getStorageConf(),
TimelineService.Config.builder().serverPort(0).build(), manager);
+ server.startService();
+ client = new TimelineServiceClient(FileSystemViewStorageConfig.newBuilder()
+
.withRemoteServerHost("localhost").withRemoteServerPort(server.getServerPort()).build());
+ }
+
+ @AfterEach
+ void tearDown() {
+ server.close();
+ view.close();
+ }
+
+ @Test
+ void testConcurrentRequestsWithTrailingCleanDoNotSync() throws Exception {
+ HoodieTimeline clientTimeline = timeline(instant(COMPLETED, COMMIT_ACTION,
"001"));
+ when(view.getTimeline()).thenReturn(timeline(instant(COMPLETED,
COMMIT_ACTION, "001"), instant(COMPLETED, CLEAN_ACTION, "002")));
+ ExecutorService executor = Executors.newFixedThreadPool(8);
+ try {
+ List<Future<List<String>>> requests = new ArrayList<>();
+ for (int i = 0; i < 32; i++) {
+ requests.add(executor.submit(() -> request(clientTimeline, "001",
"partition")));
+ }
+ for (Future<List<String>> request : requests) {
+ assertEquals(0, request.get(60, TimeUnit.SECONDS).size());
+ }
+ } finally {
+ executor.shutdownNow();
+ }
+ verify(view, never()).sync();
+ }
+
+ @ParameterizedTest
+ @ValueSource(strings = {COMMIT_ACTION, REPLACE_COMMIT_ACTION,
COMPACTION_ACTION, LOG_COMPACTION_ACTION})
+ void testOtherExtensionsStillSyncAndFailFinalCheck(String action) {
+ HoodieTimeline clientTimeline = timeline(instant(COMPLETED, COMMIT_ACTION,
"001"));
+ when(view.getTimeline()).thenReturn(timeline(instant(COMPLETED,
COMMIT_ACTION, "001"), instant(COMPLETED, action, "002")));
+ assertThrows(IOException.class, () -> request(clientTimeline, "001",
"partition"));
+ verify(view).sync();
+ }
+
+ @Test
+ void testAllActionsAtClientBoundaryAreCompared() throws IOException {
+ HoodieInstant commit = instant(COMPLETED, COMMIT_ACTION, "001");
+ HoodieInstant clean = instant(COMPLETED, CLEAN_ACTION, "001");
+ HoodieTimeline clientTimeline = timeline(commit, clean);
+ when(view.getTimeline()).thenReturn(timeline(commit, clean,
instant(COMPLETED, CLEAN_ACTION, "002")));
+ request(clientTimeline, "001", "partition");
+ verify(view, never()).sync();
+ when(view.getTimeline()).thenReturn(timeline(commit, instant(COMPLETED,
CLEAN_ACTION, "002")));
+ doAnswer(invocation -> {
+ when(view.getTimeline()).thenReturn(clientTimeline);
+ return null;
+ }).when(view).sync();
+ request(clientTimeline, "001", "partition");
+ verify(view).sync();
+ }
+
+ @Test
+ void testExtensionDuringRequestFailsFinalCheck() {
+ HoodieTimeline clientTimeline = timeline(instant(COMPLETED, COMMIT_ACTION,
"001"));
+ when(view.getTimeline()).thenReturn(clientTimeline);
+ when(view.getLatestBaseFiles(anyString())).thenAnswer(invocation -> {
+ when(view.getTimeline()).thenReturn(timeline(instant(COMPLETED,
COMMIT_ACTION, "001"), instant(COMPLETED, COMMIT_ACTION, "002")));
+ return Stream.empty();
+ });
+ assertThrows(IOException.class, () -> request(clientTimeline, "001",
"partition"));
+ verify(view, never()).sync();
+ }
+
+ @Test
+ void testDivergentPrefixesStillSync() throws IOException {
+ HoodieInstant first = instant(COMPLETED, COMMIT_ACTION, "001");
+ HoodieInstant boundary = instant(COMPLETED, CLEAN_ACTION, "003");
+ HoodieInstant newer = instant(COMPLETED, CLEAN_ACTION, "004");
+ HoodieTimeline clientTimeline = timeline(first, boundary);
+ List<HoodieTimeline> divergent = Arrays.asList(
+ timeline(first), // server behind
+ timeline(boundary, newer), // archived first instant
+ timeline(first, instant(COMPLETED, COMMIT_ACTION, "002"), boundary,
newer), // out-of-order completion
+ timeline(instant(COMPLETED, REPLACE_COMMIT_ACTION, "001"), boundary,
newer), // action changed
+ timeline(instant(INFLIGHT, COMPACTION_ACTION, "001"), boundary,
newer));
+ for (HoodieTimeline serverTimeline : divergent) {
+ when(view.getTimeline()).thenReturn(serverTimeline);
+ doAnswer(invocation -> {
+ when(view.getTimeline()).thenReturn(clientTimeline);
+ return null;
+ }).when(view).sync();
+ request(clientTimeline, "003", "partition");
+ }
+ verify(view, times(divergent.size())).sync();
+ }
+
+ @Test
+ void testCompactionStateAndFiltering() throws IOException {
+ HoodieInstant commit = instant(COMPLETED, COMMIT_ACTION, "001");
+ HoodieInstant clean = instant(COMPLETED, CLEAN_ACTION, "003");
+ for (String action : Arrays.asList(COMPACTION_ACTION,
LOG_COMPACTION_ACTION)) {
+ HoodieTimeline clientTimeline = timeline(commit, instant(REQUESTED,
action, "002"), clean);
+ when(view.getTimeline()).thenReturn(timeline(commit, instant(INFLIGHT,
action, "002"), clean, instant(COMPLETED, COMMIT_ACTION, "004")));
+ doAnswer(invocation -> {
+ when(view.getTimeline()).thenReturn(clientTimeline);
+ return null;
+ }).when(view).sync();
+ request(clientTimeline, "003", "partition");
+ }
+ verify(view, times(2)).sync();
+ clearInvocations(view);
+ HoodieTimeline rawClient = timeline(commit, instant(INFLIGHT,
COMMIT_ACTION, "002"), clean);
+ HoodieTimeline filteredClient =
rawClient.filterCompletedAndCompactionInstants();
+ when(view.getTimeline()).thenReturn(timeline(commit, instant(REQUESTED,
CLEAN_ACTION, "002"), clean, instant(COMPLETED, COMMIT_ACTION, "004")));
+ doAnswer(invocation -> {
+ when(view.getTimeline()).thenReturn(filteredClient);
+ return null;
+ }).when(view).sync();
+ request(filteredClient, "003", "partition");
+ verify(view).sync();
+ clearInvocations(view);
+
+ // A pending log compaction is included by the server's existing filter
but not the remote client's.
+ // It must not be silently dropped from the prefix to manufacture a match.
+ when(view.getTimeline()).thenReturn(timeline(commit, instant(REQUESTED,
LOG_COMPACTION_ACTION, "002"), clean));
+ doAnswer(invocation -> {
+ when(view.getTimeline()).thenReturn(filteredClient);
+ return null;
+ }).when(view).sync();
+ request(filteredClient, "003", "partition");
+ verify(view).sync();
+ }
+
+ @Test
+ void testInvalidOrMissingBoundaryDoesNotProveExtension() {
+ HoodieTimeline clientTimeline = timeline(instant(COMPLETED, COMMIT_ACTION,
"001"));
+ when(view.getTimeline()).thenReturn(timeline(instant(COMPLETED,
COMMIT_ACTION, "001"), instant(COMPLETED, COMMIT_ACTION, "003")));
+ for (String boundary : Arrays.asList(null, "", INVALID_INSTANT_TS, "000",
"002", "004", "invalid")) {
+ assertThrows(IOException.class, () -> request(clientTimeline, boundary,
"partition"));
+ }
+ verify(view, times(7)).sync();
+ }
+
+ @Test
+ void testEmptyAndEqualTimelines() throws IOException {
+ when(view.getTimeline()).thenReturn(timeline());
+ request(timeline(), null, "partition");
+ HoodieTimeline same = timeline(instant(COMPLETED, COMMIT_ACTION, "001"));
+ when(view.getTimeline()).thenReturn(same);
+ request(same, "001", "partition");
+ verify(view, never()).sync();
+ assertThrows(IOException.class, () -> request(same, "002", "partition"));
+ verify(view).sync();
+ }
+
+ @Test
+ void testDivergenceDuringRequestIsRejected() {
+ HoodieTimeline clientTimeline = timeline(instant(COMPLETED, COMMIT_ACTION,
"001"));
+ when(view.getTimeline()).thenReturn(clientTimeline);
+ when(view.getLatestBaseFiles(anyString())).thenAnswer(invocation -> {
+ when(view.getTimeline()).thenReturn(timeline(instant(COMPLETED,
REPLACE_COMMIT_ACTION, "001"), instant(COMPLETED, COMMIT_ACTION, "002")));
+ return Stream.empty();
+ });
+ assertThrows(IOException.class, () -> request(clientTimeline, "001",
"partition"));
+ verify(view, never()).sync();
+ }
+
+ @Test
+ void testRealViewRefreshesBeforeReturningNewBaseFile() throws Exception {
+ FileCreateUtils.createCommit(metaClient, "001");
+ FileCreateUtils.createBaseFile(metaClient, "partition", "001", "file");
+ HoodieTimeline initial = metaClient.reloadActiveTimeline();
+ view = spy(HoodieTableFileSystemView.fileListingBasedFileSystemView(new
HoodieLocalEngineContext(metaClient.getStorageConf()), metaClient, initial));
+ assertEquals(Arrays.asList("001"), request(initial, "001", "partition"));
+ FileCreateUtils.createCommit(metaClient, "002");
+ FileCreateUtils.createBaseFile(metaClient, "partition", "002", "file");
+ HoodieTimeline updated = metaClient.reloadActiveTimeline();
+ assertEquals(Arrays.asList("002"), request(updated, "002", "partition"));
+ verify(view).sync();
+ }
+
+ @Test
+ void testRealViewRefreshesAfterCommitIsDeleted() throws Exception {
+ FileCreateUtils.createCommit(metaClient, "001");
+ FileCreateUtils.createBaseFile(metaClient, "partition", "001", "file");
+ FileCreateUtils.createCommit(metaClient, "002");
+ FileCreateUtils.createBaseFile(metaClient, "partition", "002", "file");
+ HoodieTimeline initial = metaClient.reloadActiveTimeline();
+ view = spy(HoodieTableFileSystemView.fileListingBasedFileSystemView(new
HoodieLocalEngineContext(metaClient.getStorageConf()), metaClient, initial));
+ assertEquals(Arrays.asList("002"), request(initial, "002", "partition"));
+
+ // Restore can delete completed instants without publishing each rollback.
The server's
+ // extra commit is now stale, even though the client's timeline is an
exact prefix.
+ FileCreateUtils.deleteCommit(metaClient, "002");
+ HoodieTimeline restored = metaClient.reloadActiveTimeline();
+ assertEquals(Arrays.asList("001"), request(restored, "001", "partition"));
+ verify(view).sync();
+ }
+
+ @Test
+ void testExactExtensionPreservesLocalFallback() throws Exception {
+ FileCreateUtils.createCommit(metaClient, "001");
+ FileCreateUtils.createBaseFile(metaClient, "partition", "001", "file");
+ HoodieTimeline initial = metaClient.reloadActiveTimeline();
+ HoodieLocalEngineContext context = new
HoodieLocalEngineContext(metaClient.getStorageConf());
+ RemoteHoodieTableFileSystemView remoteView = new
RemoteHoodieTableFileSystemView("localhost", server.getServerPort(),
metaClient);
+ HoodieTableFileSystemView secondary =
HoodieTableFileSystemView.fileListingBasedFileSystemView(context, metaClient,
initial);
+ FileCreateUtils.createCommit(metaClient, "002");
+ FileCreateUtils.createBaseFile(metaClient, "partition", "002", "file");
+ view =
spy(HoodieTableFileSystemView.fileListingBasedFileSystemView(context,
metaClient, metaClient.reloadActiveTimeline()));
+ PriorityBasedFileSystemView priorityView = new
PriorityBasedFileSystemView(remoteView, ignored -> secondary, context);
+ try {
+ assertEquals("001", secondary.getLatestBaseFile("partition",
"file").get().getCommitTime());
+ assertEquals("001", priorityView.getLatestBaseFile("partition",
"file").get().getCommitTime());
+ assertEquals(Arrays.asList("001"),
priorityView.getLatestBaseFiles("partition")
+ .map(HoodieBaseFile::getCommitTime).collect(Collectors.toList()));
+ assertEquals("001", priorityView.getLatestFileSlice("partition",
"file").get().getBaseInstantTime());
+ assertEquals(Arrays.asList("001"),
priorityView.getLatestFileSlices("partition")
+ .map(slice ->
slice.getBaseInstantTime()).collect(Collectors.toList()));
+ assertEquals(Arrays.asList("001"),
priorityView.getLatestBaseFilesBeforeOrOn("partition", "001")
+ .map(HoodieBaseFile::getCommitTime).collect(Collectors.toList()));
+ assertEquals(Arrays.asList("001"),
priorityView.getLatestFileSlicesBeforeOrOn("partition", "001", false)
+ .map(slice ->
slice.getBaseInstantTime()).collect(Collectors.toList()));
+ verify(view).sync();
+ } finally {
+ priorityView.close();
+ secondary.close();
+ }
+ }
+
+ @Test
+ void testBoundedSelectionPreservesClientPendingCompactionView() throws
Exception {
+ FileCreateUtils.createCommit(metaClient, "001");
+ FileCreateUtils.createBaseFile(metaClient, "partition", "001", "file");
+ HoodieTimeline initial = metaClient.reloadActiveTimeline();
+ HoodieLocalEngineContext context = new
HoodieLocalEngineContext(metaClient.getStorageConf());
+ RemoteHoodieTableFileSystemView remoteView = new
RemoteHoodieTableFileSystemView("localhost", server.getServerPort(),
metaClient);
+ HoodieTableFileSystemView secondary =
HoodieTableFileSystemView.fileListingBasedFileSystemView(context, metaClient,
initial);
+
metaClient.getActiveTimeline().saveToCompactionRequested(instant(REQUESTED,
COMPACTION_ACTION, "002"),
+ CompactionUtils.buildFromFileSlices(Arrays.asList(Pair.of("partition",
secondary.getLatestFileSlice("partition", "file").get())),
+ Option.empty(), Option.empty()));
+ view =
spy(HoodieTableFileSystemView.fileListingBasedFileSystemView(context,
metaClient, metaClient.reloadActiveTimeline()));
+ PriorityBasedFileSystemView priorityView = new
PriorityBasedFileSystemView(remoteView, ignored -> secondary, context);
+ try {
+ assertEquals(1, secondary.getLatestFileSlicesBeforeOrOn("partition",
"001", false).count());
+ assertEquals(1, priorityView.getLatestFileSlicesBeforeOrOn("partition",
"001", false).count());
+ assertEquals(Arrays.asList("001"),
priorityView.getLatestFileSlicesBeforeOrOn("partition", "001", true)
+ .map(slice ->
slice.getBaseInstantTime()).collect(Collectors.toList()));
+ verify(view).sync();
+ } finally {
+ priorityView.close();
+ secondary.close();
+ }
+ }
+
+ @Test
+ void testDivergentTimelineRetainsStickyFallback() throws Exception {
+ FileCreateUtils.createCommit(metaClient, "001");
+ FileCreateUtils.createBaseFile(metaClient, "partition", "001", "file");
+ HoodieTimeline initial = metaClient.reloadActiveTimeline();
+ HoodieLocalEngineContext context = new
HoodieLocalEngineContext(metaClient.getStorageConf());
+ RemoteHoodieTableFileSystemView remoteView = new
RemoteHoodieTableFileSystemView("localhost", server.getServerPort(),
metaClient);
+ HoodieTableFileSystemView secondary =
spy(HoodieTableFileSystemView.fileListingBasedFileSystemView(context,
metaClient, initial));
+ when(view.getTimeline()).thenReturn(timeline(instant(COMPLETED,
REPLACE_COMMIT_ACTION, "001"), instant(COMPLETED, COMMIT_ACTION, "002")));
+ PriorityBasedFileSystemView priorityView = new
PriorityBasedFileSystemView(remoteView, ignored -> secondary, context);
+ try {
+ assertEquals(Arrays.asList("001"),
priorityView.getLatestBaseFiles("partition")
+ .map(HoodieBaseFile::getCommitTime).collect(Collectors.toList()));
+ // Even if the server subsequently matches, the previous rejection keeps
routing locally.
+ when(view.getTimeline()).thenReturn(initial);
+ assertEquals(Arrays.asList("001"),
priorityView.getLatestBaseFiles("partition")
+ .map(HoodieBaseFile::getCommitTime).collect(Collectors.toList()));
+ verify(view).sync();
+ verify(view).getLatestBaseFiles("partition");
+ verify(secondary, times(2)).getLatestBaseFiles("partition");
+ } finally {
+ priorityView.close();
+ secondary.close();
+ }
+ }
+
+ @Test
+ void testRealViewAvoidsRedundantReloadsAndListings() throws Exception {
+ FileCreateUtils.createCommit(metaClient, "001");
+ for (int i = 0; i < 8; i++) {
+ FileCreateUtils.createBaseFile(metaClient, "partition-" + i, "001",
"file");
+ }
+ metaClient.reloadActiveTimeline();
+ RemoteHoodieTableFileSystemView remoteView = new
RemoteHoodieTableFileSystemView("localhost", server.getServerPort(),
metaClient);
+ FileCreateUtils.createCleanFile(metaClient, "002", Option.empty(), null,
true);
+ HoodieTimeline serverTimeline = metaClient.reloadActiveTimeline();
+ metaClient = spy(metaClient);
+ HoodieStorage storage = spy(metaClient.getStorage());
+ when(metaClient.getStorage()).thenReturn(storage);
+ view = spy(HoodieTableFileSystemView.fileListingBasedFileSystemView(new
HoodieLocalEngineContext(metaClient.getStorageConf()), metaClient,
serverTimeline));
+ for (int i = 0; i < 8; i++) {
+ view.getLatestBaseFiles("partition-" + i).count();
+ }
+ clearInvocations(view, metaClient, storage);
+ try {
+ for (int i = 0; i < 32; i++) {
+ assertEquals(Arrays.asList("001"),
remoteView.getLatestFileSlices("partition-" + i % 8)
+ .map(slice ->
slice.getBaseFile().get().getCommitTime()).collect(Collectors.toList()));
+ }
+ } finally {
+ remoteView.close();
+ }
+ assertAll(
+ () -> verify(view, never()).sync(),
+ () -> verify(metaClient, never()).reloadActiveTimeline(),
+ () -> verify(storage,
never()).listDirectEntries(any(StoragePath.class), any()));
+ }
+
+ @Test
+ void testRealViewRefreshesOutOfOrderCompletion() throws Exception {
+ FileCreateUtils.createCommit(metaClient, "001");
+ FileCreateUtils.createBaseFile(metaClient, "partition", "001", "file");
+ FileCreateUtils.createInflightCommit(metaClient, "002");
+ FileCreateUtils.createCleanFile(metaClient, "003", Option.empty(), null,
true);
+ HoodieTimeline initial = metaClient.reloadActiveTimeline();
+ view = spy(HoodieTableFileSystemView.fileListingBasedFileSystemView(new
HoodieLocalEngineContext(metaClient.getStorageConf()), metaClient, initial));
+ assertEquals(Arrays.asList("001"),
request(initial.filterCompletedAndCompactionInstants(), "003", "partition"));
+ FileCreateUtils.createCommit(metaClient, "002");
+ FileCreateUtils.createBaseFile(metaClient, "partition", "002", "file");
+ HoodieTimeline updated =
metaClient.reloadActiveTimeline().filterCompletedAndCompactionInstants();
+ assertEquals(Arrays.asList("002"), request(updated, "003", "partition"));
+ verify(view).sync();
+ }
+
+ private List<String> request(HoodieTimeline timeline, String boundary,
String partition) throws IOException {
+ Map<String, String> params = new HashMap<>();
+ params.put(BASEPATH_PARAM, basePath);
+ params.put(PARTITION_PARAM, partition);
+ params.put(TIMELINE_HASH, timeline.getTimelineHash());
+ if (boundary != null) {
+ params.put(LAST_INSTANT_TS, boundary);
+ }
+ List<BaseFileDTO> result =
client.makeRequest(TimelineServiceClient.Request.newBuilder(GET,
LATEST_PARTITION_DATA_FILES_URL)
+ .addQueryParams(params).build()).getDecodedContent(new
TypeReference<List<BaseFileDTO>>() {});
+ return
result.stream().map(BaseFileDTO::toHoodieBaseFile).map(HoodieBaseFile::getCommitTime).collect(Collectors.toList());
+ }
+
+ private HoodieTimeline timeline(HoodieInstant... instants) {
+ return TIMELINE_FACTORY.createDefaultTimeline(Arrays.stream(instants),
metaClient.getActiveTimeline());
+ }
+
+ private HoodieInstant instant(HoodieInstant.State state, String action,
String timestamp) {
+ return INSTANT_GENERATOR.createNewInstant(state, action, timestamp);
+ }
+}