cshuo commented on code in PR #19553:
URL: https://github.com/apache/hudi/pull/19553#discussion_r3877815023
##########
hudi-common/src/main/java/org/apache/hudi/common/table/read/IncrementalQueryAnalyzer.java:
##########
@@ -167,51 +172,145 @@ public QueryContext analyze() {
return QueryContext.EMPTY;
}
HoodieTimeline filteredTimeline = getFilteredTimeline(this.metaClient);
- List<String> instantTimeList =
completionTimeQueryView.getInstantTimes(filteredTimeline, startCompletionTime,
endCompletionTime, rangeType);
- if (instantTimeList.isEmpty()) {
- // no instants completed within the give time range, returns early.
- return QueryContext.EMPTY;
- }
- // get hoodie instants
- Pair<List<String>, List<String>> splitInstantTime =
splitInstantByActiveness(instantTimeList, completionTimeQueryView);
- Set<String> instantTimeSet = new HashSet<>(instantTimeList);
- List<String> archivedInstantTimes = splitInstantTime.getLeft();
- List<String> activeInstantTimes = splitInstantTime.getRight();
- List<HoodieInstant> archivedInstants = new ArrayList<>();
- List<HoodieInstant> activeInstants = new ArrayList<>();
- HoodieTimeline archivedReadTimeline = null;
- if (!activeInstantTimes.isEmpty()) {
- activeInstants = filteredTimeline.getInstantsAsStream().filter(instant
->
instantTimeSet.contains(instant.requestedTime())).collect(Collectors.toList());
- if (limit > 0 && limit < activeInstants.size()) {
- // streaming read speed limit, limits the maximum number of commits
allowed to read for each run
- activeInstants = activeInstants.subList(0, limit);
- }
- }
- if (!archivedInstantTimes.isEmpty()) {
- archivedReadTimeline = getArchivedReadTimeline(metaClient,
archivedInstantTimes.get(0));
- archivedInstants =
archivedReadTimeline.getInstantsAsStream().filter(instant ->
instantTimeSet.contains(instant.requestedTime())).collect(Collectors.toList());
- }
- List<String> instants = Stream.concat(archivedInstants.stream(),
activeInstants.stream()).map(HoodieInstant::requestedTime).collect(Collectors.toList());
- if (instants.isEmpty()) {
- // no instants completed within the give time range, returns early.
- return QueryContext.EMPTY;
- }
- if (startCompletionTime.isEmpty() && endCompletionTime.isPresent()) {
- instants = Collections.singletonList(instants.get(instants.size() -
1));
+ if
(TimelineLayoutVersion.LAYOUT_VERSION_1.equals(metaClient.getTimelineLayoutVersion()))
{
+ return analyzeForV1Timeline(filteredTimeline, completionTimeQueryView);
}
- String lastInstant = instants.get(instants.size() - 1);
- // null => if starting from earliest, if no startCompletionTime is
specified, start from the latest instant like usual streaming read semantics.
- // if startCompletionTime is neither, then use the earliest instant as
the start instant.
- String startInstant =
START_COMMIT_EARLIEST.equalsIgnoreCase(startCompletionTime.orElse(null)) ? null
:
- startCompletionTime.isEmpty() ? lastInstant : instants.get(0);
- String endInstant = endCompletionTime.isEmpty() ? null : lastInstant;
- return QueryContext.create(startInstant, endInstant, instants,
archivedInstants, activeInstants, filteredTimeline, archivedReadTimeline);
+ return analyzeForV2Timeline(filteredTimeline, completionTimeQueryView);
} catch (Exception ex) {
log.error("Got exception when generating incremental query info", ex);
throw new HoodieException(ex);
}
}
+ private QueryContext analyzeForV1Timeline(
+ HoodieTimeline filteredTimeline,
+ CompletionTimeQueryView completionTimeQueryView) {
+ final boolean startFromEarliest =
START_COMMIT_EARLIEST.equalsIgnoreCase(startCompletionTime.orElse(null));
+ final Option<String> effectiveStart = startFromEarliest ? Option.empty() :
startCompletionTime;
+ final HoodieTimeline completedTimeline =
filteredTimeline.filterCompletedInstants();
+
+ if (effectiveStart.isEmpty() && endCompletionTime.isEmpty()) {
+ // (_, _) reads the latest completed instant while [earliest, +INF] is a
snapshot read.
+ List<HoodieInstant> activeInstants = completedTimeline.lastInstant()
+ .map(Collections::singletonList)
+ .orElse(Collections.emptyList());
+ return createQueryContext(Collections.emptyList(), activeInstants,
filteredTimeline, null);
+ }
+
+ List<HoodieInstant> archivedInstants = Collections.emptyList();
+ List<HoodieInstant> activeInstants;
+ HoodieTimeline archivedReadTimeline = null;
+
+ if (startCompletionTime.isEmpty() && endCompletionTime.isPresent()) {
+ // (_, end] returns the last eligible instant at or before end. Check
the filtered active
+ // timeline first and only load the archive when there is no active
match.
+ activeInstants = getLastInstantAtOrBefore(completedTimeline,
endCompletionTime.get());
+ if (activeInstants.isEmpty()) {
+ archivedReadTimeline = getArchivedReadTimeline(metaClient, "");
+ archivedInstants = getLastInstantAtOrBefore(archivedReadTimeline,
endCompletionTime.get());
+ }
+ } else {
+ InstantRange instantRange = InstantRange.builder()
+ .rangeType(rangeType)
+ .startInstant(effectiveStart.orElse(null))
+ .endInstant(endCompletionTime.orElse(null))
+ .nullableBoundary(true)
+ .build();
+ activeInstants = getInstantsInRange(completedTimeline, instantRange);
+
+ boolean needsArchivedInstants = startFromEarliest
+ || (effectiveStart.isPresent() &&
completionTimeQueryView.isArchived(effectiveStart.get()));
+ if (needsArchivedInstants) {
+ archivedReadTimeline = getArchivedReadTimeline(metaClient,
effectiveStart.orElse(""));
+ archivedInstants = getInstantsInRange(archivedReadTimeline,
instantRange);
+ }
+ }
+
+ return createQueryContext(
+ archivedInstants, activeInstants, filteredTimeline,
archivedReadTimeline);
+ }
+
+ private QueryContext analyzeForV2Timeline(
+ HoodieTimeline filteredTimeline,
+ CompletionTimeQueryView completionTimeQueryView) {
+ List<String> instantTimeList = completionTimeQueryView.getInstantTimes(
+ filteredTimeline, startCompletionTime, endCompletionTime, rangeType);
+ if (instantTimeList.isEmpty()) {
+ // no instants completed within the given time range, returns early.
+ return QueryContext.EMPTY;
+ }
+
+ Pair<List<String>, List<String>> splitInstantTime =
splitInstantByActiveness(
+ instantTimeList, completionTimeQueryView);
+ Set<String> instantTimeSet = new HashSet<>(instantTimeList);
+ List<String> archivedInstantTimes = splitInstantTime.getLeft();
+ List<String> activeInstantTimes = splitInstantTime.getRight();
+ List<HoodieInstant> archivedInstants = new ArrayList<>();
+ List<HoodieInstant> activeInstants = new ArrayList<>();
+ HoodieTimeline archivedReadTimeline = null;
+ if (!activeInstantTimes.isEmpty()) {
+ activeInstants = filteredTimeline.getInstantsAsStream()
+ .filter(instant -> instantTimeSet.contains(instant.requestedTime()))
+ .collect(Collectors.toList());
+ }
+ if (!archivedInstantTimes.isEmpty()) {
+ archivedReadTimeline = getArchivedReadTimeline(metaClient,
archivedInstantTimes.get(0));
+ archivedInstants = archivedReadTimeline.getInstantsAsStream()
+ .filter(instant -> instantTimeSet.contains(instant.requestedTime()))
+ .collect(Collectors.toList());
+ }
+ return createQueryContext(
+ archivedInstants, activeInstants, filteredTimeline,
archivedReadTimeline);
+ }
+
+ private QueryContext createQueryContext(
+ List<HoodieInstant> archivedInstants,
+ List<HoodieInstant> activeInstants,
+ HoodieTimeline filteredTimeline,
+ @Nullable HoodieTimeline archivedReadTimeline) {
+ if (limit > 0 && limit < activeInstants.size()) {
+ // streaming read speed limit, limits the maximum number of active
commits allowed per run
+ activeInstants = activeInstants.subList(0, limit);
+ }
+ List<String> instants = Stream.concat(archivedInstants.stream(),
activeInstants.stream())
Review Comment:
This concatenation does not guarantee global requested-time ordering. With
`hoodie.archive.beyond.savepoint=true`, an old savepointed commit may remain
active while later commits are archived. For example, archived `[102, 103,
104]` and active `[101]` produces `[102, 103, 104, 101]`. Since `lastInstant`
is taken from the final list element, the resulting end boundary regresses to
`101`, and downstream readers can omit `102`–`104`.
Please globally deduplicate and sort the combined requested-time list before
deriving `lastInstant`. The previous V1 candidate path explicitly applied both
`distinct()` and `sorted()`, so preserving those properties would also handle
overlap during concurrent archival.
##########
hudi-common/src/main/java/org/apache/hudi/common/table/read/IncrementalQueryAnalyzer.java:
##########
@@ -167,51 +172,145 @@ public QueryContext analyze() {
return QueryContext.EMPTY;
}
HoodieTimeline filteredTimeline = getFilteredTimeline(this.metaClient);
- List<String> instantTimeList =
completionTimeQueryView.getInstantTimes(filteredTimeline, startCompletionTime,
endCompletionTime, rangeType);
- if (instantTimeList.isEmpty()) {
- // no instants completed within the give time range, returns early.
- return QueryContext.EMPTY;
- }
- // get hoodie instants
- Pair<List<String>, List<String>> splitInstantTime =
splitInstantByActiveness(instantTimeList, completionTimeQueryView);
- Set<String> instantTimeSet = new HashSet<>(instantTimeList);
- List<String> archivedInstantTimes = splitInstantTime.getLeft();
- List<String> activeInstantTimes = splitInstantTime.getRight();
- List<HoodieInstant> archivedInstants = new ArrayList<>();
- List<HoodieInstant> activeInstants = new ArrayList<>();
- HoodieTimeline archivedReadTimeline = null;
- if (!activeInstantTimes.isEmpty()) {
- activeInstants = filteredTimeline.getInstantsAsStream().filter(instant
->
instantTimeSet.contains(instant.requestedTime())).collect(Collectors.toList());
- if (limit > 0 && limit < activeInstants.size()) {
- // streaming read speed limit, limits the maximum number of commits
allowed to read for each run
- activeInstants = activeInstants.subList(0, limit);
- }
- }
- if (!archivedInstantTimes.isEmpty()) {
- archivedReadTimeline = getArchivedReadTimeline(metaClient,
archivedInstantTimes.get(0));
- archivedInstants =
archivedReadTimeline.getInstantsAsStream().filter(instant ->
instantTimeSet.contains(instant.requestedTime())).collect(Collectors.toList());
- }
- List<String> instants = Stream.concat(archivedInstants.stream(),
activeInstants.stream()).map(HoodieInstant::requestedTime).collect(Collectors.toList());
- if (instants.isEmpty()) {
- // no instants completed within the give time range, returns early.
- return QueryContext.EMPTY;
- }
- if (startCompletionTime.isEmpty() && endCompletionTime.isPresent()) {
- instants = Collections.singletonList(instants.get(instants.size() -
1));
+ if
(TimelineLayoutVersion.LAYOUT_VERSION_1.equals(metaClient.getTimelineLayoutVersion()))
{
+ return analyzeForV1Timeline(filteredTimeline, completionTimeQueryView);
}
- String lastInstant = instants.get(instants.size() - 1);
- // null => if starting from earliest, if no startCompletionTime is
specified, start from the latest instant like usual streaming read semantics.
- // if startCompletionTime is neither, then use the earliest instant as
the start instant.
- String startInstant =
START_COMMIT_EARLIEST.equalsIgnoreCase(startCompletionTime.orElse(null)) ? null
:
- startCompletionTime.isEmpty() ? lastInstant : instants.get(0);
- String endInstant = endCompletionTime.isEmpty() ? null : lastInstant;
- return QueryContext.create(startInstant, endInstant, instants,
archivedInstants, activeInstants, filteredTimeline, archivedReadTimeline);
+ return analyzeForV2Timeline(filteredTimeline, completionTimeQueryView);
} catch (Exception ex) {
log.error("Got exception when generating incremental query info", ex);
throw new HoodieException(ex);
}
}
+ private QueryContext analyzeForV1Timeline(
+ HoodieTimeline filteredTimeline,
+ CompletionTimeQueryView completionTimeQueryView) {
+ final boolean startFromEarliest =
START_COMMIT_EARLIEST.equalsIgnoreCase(startCompletionTime.orElse(null));
+ final Option<String> effectiveStart = startFromEarliest ? Option.empty() :
startCompletionTime;
+ final HoodieTimeline completedTimeline =
filteredTimeline.filterCompletedInstants();
+
+ if (effectiveStart.isEmpty() && endCompletionTime.isEmpty()) {
+ // (_, _) reads the latest completed instant while [earliest, +INF] is a
snapshot read.
+ List<HoodieInstant> activeInstants = completedTimeline.lastInstant()
+ .map(Collections::singletonList)
+ .orElse(Collections.emptyList());
+ return createQueryContext(Collections.emptyList(), activeInstants,
filteredTimeline, null);
+ }
+
+ List<HoodieInstant> archivedInstants = Collections.emptyList();
+ List<HoodieInstant> activeInstants;
+ HoodieTimeline archivedReadTimeline = null;
+
+ if (startCompletionTime.isEmpty() && endCompletionTime.isPresent()) {
+ // (_, end] returns the last eligible instant at or before end. Check
the filtered active
+ // timeline first and only load the archive when there is no active
match.
+ activeInstants = getLastInstantAtOrBefore(completedTimeline,
endCompletionTime.get());
Review Comment:
This shortcut assumes that any eligible active instant is newer than every
archived instant, which is not guaranteed with
`hoodie.archive.beyond.savepoint=true`. A savepointed commit can remain active
while later commits are archived; for example, active `[101, 105]` and archived
`[102, 103, 104]`. For an end-only query ending at `104`, this returns active
instant `101` without checking the archive, omitting the actual latest eligible
instant `104`.
Please only use the active-only shortcut when the selected active instant is
on or after the archive boundary; otherwise compare the best active and
archived candidates. A regression test covering this savepoint-hole scenario
would also be helpful.
--
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.
To unsubscribe, e-mail: [email protected]
For queries about this service, please contact Infrastructure at:
[email protected]