This is an automated email from the ASF dual-hosted git repository.
Jackie-Jiang pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/pinot.git
The following commit(s) were added to refs/heads/master by this push:
new de2f596815f Add skipUpsertDelete query option to view valid docs
instead of queryable docs (#19009)
de2f596815f is described below
commit de2f596815f69b6a1ae17e01f898fdab0f20814d
Author: Chaitanya Deepthi <[email protected]>
AuthorDate: Fri Jul 17 23:43:26 2026 -0700
Add skipUpsertDelete query option to view valid docs instead of queryable
docs (#19009)
---
.../common/utils/config/QueryOptionsUtils.java | 4 +
.../org/apache/pinot/core/plan/FilterPlanNode.java | 10 +-
.../core/query/pruner/SegmentPrunerService.java | 19 +--
.../apache/pinot/core/plan/FilterPlanNodeTest.java | 2 +-
...adataAndDictionaryAggregationPlanMakerTest.java | 2 +-
.../query/pruner/SegmentPrunerServiceTest.java | 36 ++++++
.../immutable/ImmutableSegmentImpl.java | 6 +
.../indexsegment/mutable/MutableSegmentImpl.java | 6 +
.../ConcurrentMapTableUpsertMetadataManager.java | 12 +-
.../pinot/segment/local/upsert/UpsertUtils.java | 30 +++++
.../segment/local/upsert/UpsertViewManager.java | 74 ++++++++----
.../BasePartitionUpsertMetadataManagerTest.java | 40 +++----
...oncurrentMapTableUpsertMetadataManagerTest.java | 130 +++++++++++++++++++++
.../segment/local/upsert/UpsertUtilsTest.java | 105 +++++++++++++++++
.../local/upsert/UpsertViewManagerTest.java | 73 +++++++++++-
.../org/apache/pinot/segment/spi/IndexSegment.java | 5 +
.../apache/pinot/segment/spi/SegmentContext.java | 20 +++-
.../apache/pinot/spi/utils/CommonConstants.java | 3 +
18 files changed, 514 insertions(+), 63 deletions(-)
diff --git
a/pinot-common/src/main/java/org/apache/pinot/common/utils/config/QueryOptionsUtils.java
b/pinot-common/src/main/java/org/apache/pinot/common/utils/config/QueryOptionsUtils.java
index 9702380739f..4a39ab5d8e7 100644
---
a/pinot-common/src/main/java/org/apache/pinot/common/utils/config/QueryOptionsUtils.java
+++
b/pinot-common/src/main/java/org/apache/pinot/common/utils/config/QueryOptionsUtils.java
@@ -165,6 +165,10 @@ public class QueryOptionsUtils {
return
Boolean.parseBoolean(queryOptions.get(QueryOptionKey.SKIP_UPSERT_VIEW));
}
+ public static boolean isSkipUpsertDelete(Map<String, String> queryOptions) {
+ return
Boolean.parseBoolean(queryOptions.get(QueryOptionKey.SKIP_UPSERT_DELETE));
+ }
+
public static boolean isTraceRuleProductions(Map<String, String>
queryOptions) {
return
Boolean.parseBoolean(queryOptions.get(QueryOptionKey.TRACE_RULE_PRODUCTIONS));
}
diff --git
a/pinot-core/src/main/java/org/apache/pinot/core/plan/FilterPlanNode.java
b/pinot-core/src/main/java/org/apache/pinot/core/plan/FilterPlanNode.java
index b69d77bcd2f..0a70e0e6008 100644
--- a/pinot-core/src/main/java/org/apache/pinot/core/plan/FilterPlanNode.java
+++ b/pinot-core/src/main/java/org/apache/pinot/core/plan/FilterPlanNode.java
@@ -97,20 +97,20 @@ public class FilterPlanNode implements PlanNode {
@Override
public BaseFilterOperator run() {
- MutableRoaringBitmap queryableDocIdsSnapshot =
_segmentContext.getQueryableDocIdsSnapshot();
+ MutableRoaringBitmap docIdsSnapshot = _segmentContext.getDocIdsSnapshot();
int numDocs = _indexSegment.getSegmentMetadata().getTotalDocs();
if (_filter != null) {
BaseFilterOperator filterOperator = constructPhysicalOperator(_filter,
numDocs);
- if (queryableDocIdsSnapshot != null) {
- BaseFilterOperator validDocFilter = new
BitmapBasedFilterOperator(queryableDocIdsSnapshot, false, numDocs);
+ if (docIdsSnapshot != null) {
+ BaseFilterOperator validDocFilter = new
BitmapBasedFilterOperator(docIdsSnapshot, false, numDocs);
return FilterOperatorUtils.getAndFilterOperator(_queryContext,
Arrays.asList(filterOperator, validDocFilter),
numDocs);
} else {
return filterOperator;
}
- } else if (queryableDocIdsSnapshot != null) {
- return new BitmapBasedFilterOperator(queryableDocIdsSnapshot, false,
numDocs);
+ } else if (docIdsSnapshot != null) {
+ return new BitmapBasedFilterOperator(docIdsSnapshot, false, numDocs);
} else {
return new MatchAllFilterOperator(numDocs);
}
diff --git
a/pinot-core/src/main/java/org/apache/pinot/core/query/pruner/SegmentPrunerService.java
b/pinot-core/src/main/java/org/apache/pinot/core/query/pruner/SegmentPrunerService.java
index 07aa5601e50..48af9b7395f 100644
---
a/pinot-core/src/main/java/org/apache/pinot/core/query/pruner/SegmentPrunerService.java
+++
b/pinot-core/src/main/java/org/apache/pinot/core/query/pruner/SegmentPrunerService.java
@@ -136,26 +136,31 @@ public class SegmentPrunerService {
* @param segments the list of segments to be pruned. This is a destructive
operation that may modify this list in an
* undefined way. Therefore, this list should not be used
after calling this method.
* @param query query context; when non-null and skipUpsert=true,
segments with 0 queryable/valid docs are not
- * treated as empty (they contribute replaced rows to the
result). When queryable doc ids exist,
- * emptiness is determined from them; otherwise {@link
IndexSegment#getValidDocIds()} is used.
+ * treated as empty (they contribute replaced rows to the
result). When skipUpsertDelete=true,
+ * emptiness is determined from valid docs (tombstones count
as non-empty); otherwise from
+ * queryable docs.
* @return the new list with filtered elements. This is the list that have
to be used.
*/
private static List<IndexSegment> removeEmptySegments(List<IndexSegment>
segments, QueryContext query) {
int selected = 0;
- boolean skipUpsert =
QueryOptionsUtils.isSkipUpsert(query.getQueryOptions());
+ Map<String, String> queryOptions = query.getQueryOptions();
+ boolean skipUpsert = QueryOptionsUtils.isSkipUpsert(queryOptions);
+ boolean skipUpsertDelete =
QueryOptionsUtils.isSkipUpsertDelete(queryOptions);
for (IndexSegment segment : segments) {
- if (!isEmptySegment(segment, skipUpsert)) {
+ if (!isEmptySegment(segment, skipUpsert, skipUpsertDelete)) {
segments.set(selected++, segment);
}
}
return segments.subList(0, selected);
}
- private static boolean isEmptySegment(IndexSegment segment, boolean
skipUpsert) {
+ private static boolean isEmptySegment(IndexSegment segment, boolean
skipUpsert, boolean skipUpsertDelete) {
if (segment.getSegmentMetadata().getTotalDocs() == 0) {
return true;
}
- // Check if the segment has 0 queryable docIds while skipUpsert=false
- return !skipUpsert && segment.hasNoQueryableDocs();
+ if (skipUpsert) {
+ return false;
+ }
+ return skipUpsertDelete ? segment.hasNoValidDocs() :
segment.hasNoQueryableDocs();
}
}
diff --git
a/pinot-core/src/test/java/org/apache/pinot/core/plan/FilterPlanNodeTest.java
b/pinot-core/src/test/java/org/apache/pinot/core/plan/FilterPlanNodeTest.java
index 3faced128bb..b505c2e8dfb 100644
---
a/pinot-core/src/test/java/org/apache/pinot/core/plan/FilterPlanNodeTest.java
+++
b/pinot-core/src/test/java/org/apache/pinot/core/plan/FilterPlanNodeTest.java
@@ -94,7 +94,7 @@ public class FilterPlanNodeTest {
// Result should be invariant - always exactly 3 docs
for (int i = 0; i < 10_000; i++) {
SegmentContext segmentContext = new SegmentContext(segment);
-
segmentContext.setQueryableDocIdsSnapshot(UpsertUtils.getQueryableDocIdsSnapshotFromSegment(segment));
+
segmentContext.setDocIdsSnapshot(UpsertUtils.getQueryableDocIdsSnapshotFromSegment(segment));
assertEquals(getNumberOfFilteredDocs(segmentContext, queryContext), 3);
}
diff --git
a/pinot-core/src/test/java/org/apache/pinot/core/plan/maker/MetadataAndDictionaryAggregationPlanMakerTest.java
b/pinot-core/src/test/java/org/apache/pinot/core/plan/maker/MetadataAndDictionaryAggregationPlanMakerTest.java
index 672f08a6fbd..769bd57b9d6 100644
---
a/pinot-core/src/test/java/org/apache/pinot/core/plan/maker/MetadataAndDictionaryAggregationPlanMakerTest.java
+++
b/pinot-core/src/test/java/org/apache/pinot/core/plan/maker/MetadataAndDictionaryAggregationPlanMakerTest.java
@@ -165,7 +165,7 @@ public class MetadataAndDictionaryAggregationPlanMakerTest {
assertTrue(operatorClass.isInstance(operator));
SegmentContext segmentContext = new SegmentContext(_upsertIndexSegment);
-
segmentContext.setQueryableDocIdsSnapshot(UpsertUtils.getQueryableDocIdsSnapshotFromSegment(_upsertIndexSegment));
+
segmentContext.setDocIdsSnapshot(UpsertUtils.getQueryableDocIdsSnapshotFromSegment(_upsertIndexSegment));
Operator<?> upsertOperator =
PLAN_MAKER.makeSegmentPlanNode(segmentContext, queryContext).run();
assertTrue(upsertOperatorClass.isInstance(upsertOperator));
}
diff --git
a/pinot-core/src/test/java/org/apache/pinot/core/query/pruner/SegmentPrunerServiceTest.java
b/pinot-core/src/test/java/org/apache/pinot/core/query/pruner/SegmentPrunerServiceTest.java
index f1ae4506907..7f5c6fa4a1a 100644
---
a/pinot-core/src/test/java/org/apache/pinot/core/query/pruner/SegmentPrunerServiceTest.java
+++
b/pinot-core/src/test/java/org/apache/pinot/core/query/pruner/SegmentPrunerServiceTest.java
@@ -186,6 +186,42 @@ public class SegmentPrunerServiceTest {
Assert.assertEquals(actual, segments);
}
+ @Test
+ public void emptyQueryableRetainedWithSkipUpsertDelete() {
+ SegmentPrunerService service = new SegmentPrunerService(_emptyPrunerConf);
+ ThreadSafeMutableRoaringBitmap valid = new
ThreadSafeMutableRoaringBitmap(0);
+ ThreadSafeMutableRoaringBitmap queryable = new
ThreadSafeMutableRoaringBitmap();
+ IndexSegment segment = mockUpsertIndexSegment(10, valid, queryable);
+
+ List<IndexSegment> segments = new ArrayList<>();
+ segments.add(segment);
+ QueryContext queryContext =
+ QueryContextConverterUtils.getQueryContext("select col1 from t1
option(skipUpsertDelete=true)");
+
+ List<IndexSegment> actual = service.prune(segments, queryContext, new
SegmentPrunerStatistics());
+
+ Assert.assertEquals(actual, segments);
+ }
+
+ /**
+ * skipUpsertDelete checks valid-docs emptiness specifically, not
skipUpsert's blanket bypass: a segment fully
+ * superseded elsewhere (0 valid docs) is still pruned.
+ */
+ @Test
+ public void emptyValidPrunedWithSkipUpsertDelete() {
+ SegmentPrunerService service = new SegmentPrunerService(_emptyPrunerConf);
+ IndexSegment segment = mockUpsertIndexSegment(10, new
ThreadSafeMutableRoaringBitmap(), null);
+
+ List<IndexSegment> segments = new ArrayList<>();
+ segments.add(segment);
+ QueryContext queryContext =
+ QueryContextConverterUtils.getQueryContext("select col1 from t1
option(skipUpsertDelete=true)");
+
+ List<IndexSegment> actual = service.prune(segments, queryContext, new
SegmentPrunerStatistics());
+
+ Assert.assertEquals(actual, List.of());
+ }
+
@Test
public void nonEmptyQueryableNotPruned() {
SegmentPrunerService service = new SegmentPrunerService(_emptyPrunerConf);
diff --git
a/pinot-segment-local/src/main/java/org/apache/pinot/segment/local/indexsegment/immutable/ImmutableSegmentImpl.java
b/pinot-segment-local/src/main/java/org/apache/pinot/segment/local/indexsegment/immutable/ImmutableSegmentImpl.java
index 9436a49ee35..c0a22c85fcd 100644
---
a/pinot-segment-local/src/main/java/org/apache/pinot/segment/local/indexsegment/immutable/ImmutableSegmentImpl.java
+++
b/pinot-segment-local/src/main/java/org/apache/pinot/segment/local/indexsegment/immutable/ImmutableSegmentImpl.java
@@ -40,6 +40,7 @@ import
org.apache.pinot.segment.local.segment.readers.PinotSegmentRecordReader;
import
org.apache.pinot.segment.local.segment.virtualcolumn.VirtualColumnContext;
import org.apache.pinot.segment.local.startree.v2.store.StarTreeIndexContainer;
import org.apache.pinot.segment.local.upsert.PartitionUpsertMetadataManager;
+import org.apache.pinot.segment.local.upsert.UpsertUtils;
import org.apache.pinot.segment.local.upsert.UpsertViewManager;
import org.apache.pinot.segment.spi.ColumnMetadata;
import org.apache.pinot.segment.spi.FetchContext;
@@ -382,6 +383,11 @@ public class ImmutableSegmentImpl implements
ImmutableSegment {
return validDocIds != null && validDocIds.isEmpty();
}
+ @Override
+ public boolean hasNoValidDocs() {
+ return UpsertUtils.hasNoValidDocs(_partitionUpsertMetadataManager, this);
+ }
+
@Override
public GenericRow getRecord(int docId, GenericRow reuse) {
try (PinotSegmentRecordReader recordReader = new
PinotSegmentRecordReader()) {
diff --git
a/pinot-segment-local/src/main/java/org/apache/pinot/segment/local/indexsegment/mutable/MutableSegmentImpl.java
b/pinot-segment-local/src/main/java/org/apache/pinot/segment/local/indexsegment/mutable/MutableSegmentImpl.java
index dbf8f935311..a8357d5779b 100644
---
a/pinot-segment-local/src/main/java/org/apache/pinot/segment/local/indexsegment/mutable/MutableSegmentImpl.java
+++
b/pinot-segment-local/src/main/java/org/apache/pinot/segment/local/indexsegment/mutable/MutableSegmentImpl.java
@@ -74,6 +74,7 @@ import
org.apache.pinot.segment.local.upsert.ComparisonColumns;
import org.apache.pinot.segment.local.upsert.PartitionUpsertMetadataManager;
import org.apache.pinot.segment.local.upsert.RecordInfo;
import org.apache.pinot.segment.local.upsert.UpsertContext;
+import org.apache.pinot.segment.local.upsert.UpsertUtils;
import org.apache.pinot.segment.local.upsert.UpsertViewManager;
import org.apache.pinot.segment.local.utils.FixedIntArrayOffHeapIdMap;
import org.apache.pinot.segment.local.utils.IdMap;
@@ -1242,6 +1243,11 @@ public class MutableSegmentImpl implements
MutableSegment {
return validDocIds != null && validDocIds.isEmpty();
}
+ @Override
+ public boolean hasNoValidDocs() {
+ return UpsertUtils.hasNoValidDocs(_partitionUpsertMetadataManager, this);
+ }
+
@Override
public GenericRow getRecord(int docId, GenericRow reuse) {
try (PinotSegmentRecordReader recordReader = new
PinotSegmentRecordReader()) {
diff --git
a/pinot-segment-local/src/main/java/org/apache/pinot/segment/local/upsert/ConcurrentMapTableUpsertMetadataManager.java
b/pinot-segment-local/src/main/java/org/apache/pinot/segment/local/upsert/ConcurrentMapTableUpsertMetadataManager.java
index 9f67a5d3d17..628104273a8 100644
---
a/pinot-segment-local/src/main/java/org/apache/pinot/segment/local/upsert/ConcurrentMapTableUpsertMetadataManager.java
+++
b/pinot-segment-local/src/main/java/org/apache/pinot/segment/local/upsert/ConcurrentMapTableUpsertMetadataManager.java
@@ -95,24 +95,28 @@ public class ConcurrentMapTableUpsertMetadataManager
extends BaseTableUpsertMeta
// Otherwise, get queryableDocIds bitmaps as kept by the segment objects
directly as before.
if (_context.getConsistencyMode() == UpsertConfig.ConsistencyMode.NONE ||
QueryOptionsUtils.isSkipUpsertView(
queryOptions)) {
+ // No shared upsert-view lock exists in this branch, so a direct read is
already safe here.
+ boolean skipUpsertDelete =
QueryOptionsUtils.isSkipUpsertDelete(queryOptions);
for (SegmentContext segmentContext : segmentContexts) {
IndexSegment segment = segmentContext.getIndexSegment();
-
segmentContext.setQueryableDocIdsSnapshot(UpsertUtils.getQueryableDocIdsSnapshotFromSegment(segment));
+ segmentContext.setDocIdsSnapshot(skipUpsertDelete
+ ? UpsertUtils.getValidDocIdsSnapshotFromSegment(segment)
+ : UpsertUtils.getQueryableDocIdsSnapshotFromSegment(segment));
}
return;
}
- // All segments should have been tracked by partitionMetadataManagers to
provide queries consistent upsert view.
+ // A consistency mode is active: UpsertViewManager knows the locking each
mode requires for skipUpsertDelete too.
_partitionMetadataManagerMap.forEach(
(partitionID, upsertMetadataManager) ->
upsertMetadataManager.getUpsertViewManager()
.setSegmentContexts(segmentContexts, queryOptions));
if (LOGGER.isDebugEnabled()) {
for (SegmentContext segmentContext : segmentContexts) {
IndexSegment segment = segmentContext.getIndexSegment();
- if (segmentContext.getQueryableDocIdsSnapshot() == null) {
+ if (segmentContext.getDocIdsSnapshot() == null) {
LOGGER.debug("No upsert view for segment: {}, type: {}, total: {}",
segment.getSegmentName(),
(segment instanceof ImmutableSegment ? "imm" : "mut"),
segment.getSegmentMetadata().getTotalDocs());
} else {
- int cardCnt =
segmentContext.getQueryableDocIdsSnapshot().getCardinality();
+ int cardCnt = segmentContext.getDocIdsSnapshot().getCardinality();
LOGGER.debug("Got upsert view of segment: {}, type: {}, total: {},
valid: {}", segment.getSegmentName(),
(segment instanceof ImmutableSegment ? "imm" : "mut"),
segment.getSegmentMetadata().getTotalDocs(),
cardCnt);
diff --git
a/pinot-segment-local/src/main/java/org/apache/pinot/segment/local/upsert/UpsertUtils.java
b/pinot-segment-local/src/main/java/org/apache/pinot/segment/local/upsert/UpsertUtils.java
index 3978044b878..f8af9bf12a6 100644
---
a/pinot-segment-local/src/main/java/org/apache/pinot/segment/local/upsert/UpsertUtils.java
+++
b/pinot-segment-local/src/main/java/org/apache/pinot/segment/local/upsert/UpsertUtils.java
@@ -57,6 +57,36 @@ public class UpsertUtils {
: (useEmptyForNull ? new MutableRoaringBitmap() : null);
}
+ /// Unlike [#getQueryableDocIdsSnapshotFromSegment], never falls back to
preferring queryable docs.
+ @Nullable
+ public static MutableRoaringBitmap
getValidDocIdsSnapshotFromSegment(IndexSegment segment) {
+ return getValidDocIdsSnapshotFromSegment(segment, false);
+ }
+
+ /// Shared by {@code ImmutableSegmentImpl}/{@code MutableSegmentImpl}'s
`hasNoValidDocs()`. Mirrors
+ /// `hasNoQueryableDocs()`'s consistency-mode-aware/live-fallback split,
against the valid-docs cache instead.
+ public static boolean hasNoValidDocs(@Nullable
PartitionUpsertMetadataManager partitionUpsertMetadataManager,
+ IndexSegment segment) {
+ if (partitionUpsertMetadataManager == null) {
+ return false;
+ }
+ UpsertViewManager viewManager =
partitionUpsertMetadataManager.getUpsertViewManager();
+ if (viewManager != null) {
+ MutableRoaringBitmap validDocIdsSnapshot =
viewManager.getValidDocIdsSnapshot(segment);
+ return validDocIdsSnapshot != null && validDocIdsSnapshot.isEmpty();
+ }
+ ThreadSafeMutableRoaringBitmap validDocIds = segment.getValidDocIds();
+ return validDocIds != null && validDocIds.isEmpty();
+ }
+
+ @Nullable
+ public static MutableRoaringBitmap
getValidDocIdsSnapshotFromSegment(IndexSegment segment,
+ boolean useEmptyForNull) {
+ ThreadSafeMutableRoaringBitmap validDocIds = segment.getValidDocIds();
+ return validDocIds != null ? validDocIds.getMutableRoaringBitmap()
+ : (useEmptyForNull ? new MutableRoaringBitmap() : null);
+ }
+
public static void doReplaceDocId(ThreadSafeMutableRoaringBitmap validDocIds,
@Nullable ThreadSafeMutableRoaringBitmap queryableDocIds, int oldDocId,
int newDocId, RecordInfo recordInfo) {
validDocIds.replace(oldDocId, newDocId);
diff --git
a/pinot-segment-local/src/main/java/org/apache/pinot/segment/local/upsert/UpsertViewManager.java
b/pinot-segment-local/src/main/java/org/apache/pinot/segment/local/upsert/UpsertViewManager.java
index 9b9d52ce2c2..b1df77bcfde 100644
---
a/pinot-segment-local/src/main/java/org/apache/pinot/segment/local/upsert/UpsertViewManager.java
+++
b/pinot-segment-local/src/main/java/org/apache/pinot/segment/local/upsert/UpsertViewManager.java
@@ -65,7 +65,12 @@ public class UpsertViewManager {
// The query threads always get _upsertViewTrackedSegmentsLock then
_upsertViewSegmentDocIdsLock to avoid deadlock.
// And the upsert threads never nest the two locks.
private final ReadWriteLock _upsertViewLock = new ReentrantReadWriteLock();
+ // When a segment has no delete column, the same MutableRoaringBitmap
instance is stored in both maps below
+ // (see doBatchRefreshUpsertView) to avoid cloning it twice; never mutate a
bitmap read from either map in place.
private volatile Map<IndexSegment, MutableRoaringBitmap>
_segmentQueryableDocIdsMap;
+ // Refreshed alongside _segmentQueryableDocIdsMap in the same locked pass,
so skipUpsertDelete queries and pruning
+ // can read a consistent cut lock-free instead of taking the write lock per
call.
+ private volatile Map<IndexSegment, MutableRoaringBitmap>
_segmentValidDocIdsMap;
// For SNAPSHOT mode, track segments that get new updates since last refresh
to reduce the overhead of refreshing.
private final Set<IndexSegment> _updatedSegmentsSinceLastRefresh =
ConcurrentHashMap.newKeySet();
@@ -164,10 +169,11 @@ public class UpsertViewManager {
* present, to avoid overwriting the contexts specified at the others places.
*/
public void setSegmentContexts(List<SegmentContext> segmentContexts,
Map<String, String> queryOptions) {
+ boolean skipUpsertDelete =
QueryOptionsUtils.isSkipUpsertDelete(queryOptions);
if (_consistencyMode == UpsertConfig.ConsistencyMode.SYNC) {
_upsertViewLock.readLock().lock();
try {
- setSegmentContexts(segmentContexts);
+ setSegmentContexts(segmentContexts, skipUpsertDelete);
return;
} finally {
_upsertViewLock.readLock().unlock();
@@ -184,21 +190,24 @@ public class UpsertViewManager {
upsertViewFreshnessMs = _upsertViewRefreshIntervalMs;
}
doBatchRefreshUpsertView(upsertViewFreshnessMs, false);
- Map<IndexSegment, MutableRoaringBitmap> currentUpsertView =
_segmentQueryableDocIdsMap;
+ Map<IndexSegment, MutableRoaringBitmap> currentUpsertView =
+ skipUpsertDelete ? _segmentValidDocIdsMap : _segmentQueryableDocIdsMap;
for (SegmentContext segmentContext : segmentContexts) {
IndexSegment segment = segmentContext.getIndexSegment();
MutableRoaringBitmap segmentView = currentUpsertView.get(segment);
if (segmentView != null) {
- segmentContext.setQueryableDocIdsSnapshot(segmentView);
+ segmentContext.setDocIdsSnapshot(segmentView);
}
}
}
- private void setSegmentContexts(List<SegmentContext> segmentContexts) {
+ private void setSegmentContexts(List<SegmentContext> segmentContexts,
boolean skipUpsertDelete) {
for (SegmentContext segmentContext : segmentContexts) {
IndexSegment segment = segmentContext.getIndexSegment();
if (_trackedSegments.contains(segment)) {
-
segmentContext.setQueryableDocIdsSnapshot(UpsertUtils.getQueryableDocIdsSnapshotFromSegment(segment,
true));
+ segmentContext.setDocIdsSnapshot(skipUpsertDelete
+ ? UpsertUtils.getValidDocIdsSnapshotFromSegment(segment, true)
+ : UpsertUtils.getQueryableDocIdsSnapshotFromSegment(segment,
true));
}
}
}
@@ -212,52 +221,65 @@ public class UpsertViewManager {
@VisibleForTesting
void doBatchRefreshUpsertView(long upsertViewFreshnessMs, boolean
forceRefresh) {
- // Always refresh if the current view is still empty.
- if (!forceRefresh && skipUpsertViewRefresh(upsertViewFreshnessMs) &&
_segmentQueryableDocIdsMap != null) {
+ // Always refresh if either view is still empty. The two maps are swapped
in via two separate volatile writes,
+ // not one atomic publish, so a concurrent reader can observe
_segmentValidDocIdsMap up to one refresh behind
+ // _segmentQueryableDocIdsMap; this is self-correcting next cycle and no
caller combines both maps at once.
+ if (!forceRefresh && skipUpsertViewRefresh(upsertViewFreshnessMs) &&
_segmentQueryableDocIdsMap != null
+ && _segmentValidDocIdsMap != null) {
return;
}
_upsertViewLock.writeLock().lock();
try {
- // Check again with lock, and always refresh if the current view is
still empty.
- Map<IndexSegment, MutableRoaringBitmap> current =
_segmentQueryableDocIdsMap;
- if (!forceRefresh && skipUpsertViewRefresh(upsertViewFreshnessMs) &&
current != null) {
+ // Check again with lock, and always refresh if either view is still
empty.
+ Map<IndexSegment, MutableRoaringBitmap> queryableDocIds =
_segmentQueryableDocIdsMap;
+ Map<IndexSegment, MutableRoaringBitmap> validDocs =
_segmentValidDocIdsMap;
+ if (!forceRefresh && skipUpsertViewRefresh(upsertViewFreshnessMs) &&
queryableDocIds != null
+ && validDocs != null) {
return;
}
if (LOGGER.isDebugEnabled()) {
- if (current == null) {
+ if (queryableDocIds == null) {
LOGGER.debug("Current upsert view is still null");
} else {
- current.forEach(
+ queryableDocIds.forEach(
(segment, bitmap) -> LOGGER.debug("Current upsert view of
segment: {}, type: {}, total: {}, valid: {}",
segment.getSegmentName(), (segment instanceof
ImmutableSegment ? "imm" : "mut"),
segment.getSegmentMetadata().getTotalDocs(),
bitmap.getCardinality()));
}
}
- Map<IndexSegment, MutableRoaringBitmap> updated = new HashMap<>();
+ Map<IndexSegment, MutableRoaringBitmap> updatedQueryable = new
HashMap<>();
+ Map<IndexSegment, MutableRoaringBitmap> updatedValid = new HashMap<>();
for (IndexSegment segment : _trackedSegments) {
// Update bitmap for segment updated since last refresh or not in the
view yet. This also handles segments
// that are tracked by _trackedSegments but not by
_updatedSegmentsSinceLastRefresh, like those didn't update
// any bitmaps as their docs simply lost all the upsert comparisons
with the existing docs.
- if (current == null || current.get(segment) == null ||
_updatedSegmentsSinceLastRefresh.contains(segment)) {
- updated.put(segment,
UpsertUtils.getQueryableDocIdsSnapshotFromSegment(segment, true));
+ if (queryableDocIds == null || queryableDocIds.get(segment) == null
+ || _updatedSegmentsSinceLastRefresh.contains(segment)) {
+ MutableRoaringBitmap validSnapshot =
UpsertUtils.getValidDocIdsSnapshotFromSegment(segment, true);
+ updatedValid.put(segment, validSnapshot);
+ // Without delete enabled, queryable docs equal valid docs; reuse
the clone instead of cloning twice.
+ updatedQueryable.put(segment, segment.getQueryableDocIds() != null
+ ? UpsertUtils.getQueryableDocIdsSnapshotFromSegment(segment,
true) : validSnapshot);
if (LOGGER.isDebugEnabled()) {
LOGGER.debug("Update upsert view of segment: {}, type: {}, total:
{}, valid: {}, reason: {}",
segment.getSegmentName(), (segment instanceof ImmutableSegment
? "imm" : "mut"),
- segment.getSegmentMetadata().getTotalDocs(),
updated.get(segment).getCardinality(),
- current == null || current.get(segment) == null ? "no view
yet" : "bitmap updated");
+ segment.getSegmentMetadata().getTotalDocs(),
updatedQueryable.get(segment).getCardinality(),
+ queryableDocIds == null || queryableDocIds.get(segment) ==
null ? "no view yet" : "bitmap updated");
}
} else {
- updated.put(segment, current.get(segment));
+ updatedQueryable.put(segment, queryableDocIds.get(segment));
+ updatedValid.put(segment, validDocs.get(segment));
}
}
// Swap in the new consistent set of bitmaps.
if (LOGGER.isDebugEnabled()) {
- updated.forEach(
+ updatedQueryable.forEach(
(segment, bitmap) -> LOGGER.debug("Updated upsert view of segment:
{}, type: {}, total: {}, valid: {}",
segment.getSegmentName(), (segment instanceof ImmutableSegment
? "imm" : "mut"),
segment.getSegmentMetadata().getTotalDocs(),
bitmap.getCardinality()));
}
- _segmentQueryableDocIdsMap = updated;
+ _segmentQueryableDocIdsMap = updatedQueryable;
+ _segmentValidDocIdsMap = updatedValid;
_updatedSegmentsSinceLastRefresh.clear();
_lastUpsertViewRefreshTimeMs = System.currentTimeMillis();
} finally {
@@ -307,6 +329,11 @@ public class UpsertViewManager {
return _segmentQueryableDocIdsMap;
}
+ @VisibleForTesting
+ Map<IndexSegment, MutableRoaringBitmap> getSegmentValidDocIdsMap() {
+ return _segmentValidDocIdsMap;
+ }
+
// Returns the queryable doc-id snapshot for the given segment from the most
recent refresh, or
// null if no refresh has happened yet or the segment is not in the current
view.
@Nullable
@@ -315,6 +342,13 @@ public class UpsertViewManager {
return view == null ? null : view.get(segment);
}
+ // Same as getQueryableDocIdsSnapshot, but from the valid-docs (tombstones
included) cache.
+ @Nullable
+ public MutableRoaringBitmap getValidDocIdsSnapshot(IndexSegment segment) {
+ Map<IndexSegment, MutableRoaringBitmap> view = _segmentValidDocIdsMap;
+ return view == null ? null : view.get(segment);
+ }
+
@VisibleForTesting
Set<IndexSegment> getUpdatedSegmentsSinceLastRefresh() {
return _updatedSegmentsSinceLastRefresh;
diff --git
a/pinot-segment-local/src/test/java/org/apache/pinot/segment/local/upsert/BasePartitionUpsertMetadataManagerTest.java
b/pinot-segment-local/src/test/java/org/apache/pinot/segment/local/upsert/BasePartitionUpsertMetadataManagerTest.java
index aefd1cfc895..c448fcb5c94 100644
---
a/pinot-segment-local/src/test/java/org/apache/pinot/segment/local/upsert/BasePartitionUpsertMetadataManagerTest.java
+++
b/pinot-segment-local/src/test/java/org/apache/pinot/segment/local/upsert/BasePartitionUpsertMetadataManagerTest.java
@@ -703,15 +703,15 @@ public class BasePartitionUpsertMetadataManagerTest {
ThreadSafeMutableRoaringBitmap validDocIds =
segmentQueryableDocIdsMap.get(sc.getIndexSegment());
assertNotNull(validDocIds);
// SegmentContext holds a clone of the original queryableDocIds held by
the segment object.
- assertNotSame(sc.getQueryableDocIdsSnapshot(),
validDocIds.getMutableRoaringBitmap());
- assertEquals(sc.getQueryableDocIdsSnapshot(),
validDocIds.getMutableRoaringBitmap());
+ assertNotSame(sc.getDocIdsSnapshot(),
validDocIds.getMutableRoaringBitmap());
+ assertEquals(sc.getDocIdsSnapshot(),
validDocIds.getMutableRoaringBitmap());
// docId=0 in seg01 got invalidated.
if (sc.getIndexSegment() == seg01) {
- assertFalse(sc.getQueryableDocIdsSnapshot().contains(0));
+ assertFalse(sc.getDocIdsSnapshot().contains(0));
}
// docId=12 in seg03 was newly added.
if (sc.getIndexSegment() == seg03) {
- assertTrue(sc.getQueryableDocIdsSnapshot().contains(12));
+ assertTrue(sc.getDocIdsSnapshot().contains(12));
}
}
}
@@ -796,21 +796,21 @@ public class BasePartitionUpsertMetadataManagerTest {
for (SegmentContext reuseSC : reuseSegmentContexts) {
for (SegmentContext sc : segmentContexts) {
if (reuseSC.getIndexSegment() == sc.getIndexSegment()) {
- assertSame(reuseSC.getQueryableDocIdsSnapshot(),
sc.getQueryableDocIdsSnapshot());
+ assertSame(reuseSC.getDocIdsSnapshot(), sc.getDocIdsSnapshot());
}
}
ThreadSafeMutableRoaringBitmap validDocIds =
segmentQueryableDocIdsMap.get(reuseSC.getIndexSegment());
assertNotNull(validDocIds);
// The upsert view holds a clone of the original queryableDocIds held by
the segment object.
- assertNotSame(reuseSC.getQueryableDocIdsSnapshot(),
validDocIds.getMutableRoaringBitmap());
- assertEquals(reuseSC.getQueryableDocIdsSnapshot(),
validDocIds.getMutableRoaringBitmap());
+ assertNotSame(reuseSC.getDocIdsSnapshot(),
validDocIds.getMutableRoaringBitmap());
+ assertEquals(reuseSC.getDocIdsSnapshot(),
validDocIds.getMutableRoaringBitmap());
// docId=0 in seg01 got invalidated.
if (reuseSC.getIndexSegment() == seg01) {
- assertFalse(reuseSC.getQueryableDocIdsSnapshot().contains(0));
+ assertFalse(reuseSC.getDocIdsSnapshot().contains(0));
}
// docId=12 in seg03 was newly added.
if (reuseSC.getIndexSegment() == seg03) {
- assertTrue(reuseSC.getQueryableDocIdsSnapshot().contains(12));
+ assertTrue(reuseSC.getDocIdsSnapshot().contains(12));
}
}
@@ -825,21 +825,21 @@ public class BasePartitionUpsertMetadataManagerTest {
for (SegmentContext refreshSC : refreshSegmentContexts) {
for (SegmentContext sc : segmentContexts) {
if (refreshSC.getIndexSegment() == sc.getIndexSegment()) {
- assertNotSame(refreshSC.getQueryableDocIdsSnapshot(),
sc.getQueryableDocIdsSnapshot());
+ assertNotSame(refreshSC.getDocIdsSnapshot(), sc.getDocIdsSnapshot());
}
}
ThreadSafeMutableRoaringBitmap validDocIds =
segmentQueryableDocIdsMap.get(refreshSC.getIndexSegment());
assertNotNull(validDocIds);
// The upsert view holds a clone of the original queryableDocIds held by
the segment object.
- assertNotSame(refreshSC.getQueryableDocIdsSnapshot(),
validDocIds.getMutableRoaringBitmap());
- assertEquals(refreshSC.getQueryableDocIdsSnapshot(),
validDocIds.getMutableRoaringBitmap());
+ assertNotSame(refreshSC.getDocIdsSnapshot(),
validDocIds.getMutableRoaringBitmap());
+ assertEquals(refreshSC.getDocIdsSnapshot(),
validDocIds.getMutableRoaringBitmap());
// docId=0 in seg01 got invalidated.
if (refreshSC.getIndexSegment() == seg01) {
- assertFalse(refreshSC.getQueryableDocIdsSnapshot().contains(0));
+ assertFalse(refreshSC.getDocIdsSnapshot().contains(0));
}
// docId=12 in seg03 was newly added.
if (refreshSC.getIndexSegment() == seg03) {
- assertTrue(refreshSC.getQueryableDocIdsSnapshot().contains(12));
+ assertTrue(refreshSC.getDocIdsSnapshot().contains(12));
}
}
}
@@ -896,22 +896,22 @@ public class BasePartitionUpsertMetadataManagerTest {
for (SegmentContext refreshSC : refreshSegmentContexts) {
for (SegmentContext sc : segmentContexts) {
if (refreshSC.getIndexSegment() == sc.getIndexSegment()) {
- assertNotSame(refreshSC.getQueryableDocIdsSnapshot(),
sc.getQueryableDocIdsSnapshot());
+ assertNotSame(refreshSC.getDocIdsSnapshot(), sc.getDocIdsSnapshot());
}
}
ThreadSafeMutableRoaringBitmap validDocIds =
segmentQueryableDocIdsMap.get(refreshSC.getIndexSegment());
assertNotNull(validDocIds);
// The upsert view holds a clone of the original queryableDocIds held by
the segment object.
- assertNotSame(refreshSC.getQueryableDocIdsSnapshot(),
validDocIds.getMutableRoaringBitmap());
- assertEquals(refreshSC.getQueryableDocIdsSnapshot(),
validDocIds.getMutableRoaringBitmap());
- assertNotNull(refreshSC.getQueryableDocIdsSnapshot());
+ assertNotSame(refreshSC.getDocIdsSnapshot(),
validDocIds.getMutableRoaringBitmap());
+ assertEquals(refreshSC.getDocIdsSnapshot(),
validDocIds.getMutableRoaringBitmap());
+ assertNotNull(refreshSC.getDocIdsSnapshot());
// docId=0 in seg01 got invalidated.
if (refreshSC.getIndexSegment() == seg01) {
- assertFalse(refreshSC.getQueryableDocIdsSnapshot().contains(0));
+ assertFalse(refreshSC.getDocIdsSnapshot().contains(0));
}
// docId=12 in seg03 was newly added.
if (refreshSC.getIndexSegment() == seg03) {
- assertTrue(refreshSC.getQueryableDocIdsSnapshot().contains(12));
+ assertTrue(refreshSC.getDocIdsSnapshot().contains(12));
}
}
}
diff --git
a/pinot-segment-local/src/test/java/org/apache/pinot/segment/local/upsert/ConcurrentMapTableUpsertMetadataManagerTest.java
b/pinot-segment-local/src/test/java/org/apache/pinot/segment/local/upsert/ConcurrentMapTableUpsertMetadataManagerTest.java
new file mode 100644
index 00000000000..9b912bfc721
--- /dev/null
+++
b/pinot-segment-local/src/test/java/org/apache/pinot/segment/local/upsert/ConcurrentMapTableUpsertMetadataManagerTest.java
@@ -0,0 +1,130 @@
+/**
+ * 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.pinot.segment.local.upsert;
+
+import java.io.File;
+import java.util.List;
+import java.util.Map;
+import org.apache.pinot.segment.local.data.manager.TableDataManager;
+import org.apache.pinot.segment.spi.IndexSegment;
+import org.apache.pinot.segment.spi.MutableSegment;
+import org.apache.pinot.segment.spi.SegmentContext;
+import
org.apache.pinot.segment.spi.index.mutable.ThreadSafeMutableRoaringBitmap;
+import org.apache.pinot.spi.config.table.TableConfig;
+import org.apache.pinot.spi.config.table.TableType;
+import org.apache.pinot.spi.config.table.UpsertConfig;
+import org.apache.pinot.spi.data.FieldSpec.DataType;
+import org.apache.pinot.spi.data.Schema;
+import org.apache.pinot.spi.env.PinotConfiguration;
+import
org.apache.pinot.spi.utils.CommonConstants.Broker.Request.QueryOptionKey;
+import org.apache.pinot.spi.utils.builder.TableConfigBuilder;
+import org.roaringbitmap.buffer.MutableRoaringBitmap;
+import org.testng.annotations.Test;
+
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.when;
+import static org.testng.Assert.assertNull;
+import static org.testng.Assert.assertSame;
+
+
+/**
+ * Covers the mode-dispatch branch that a prior review pass regressed:
NONE/skipUpsertView must take the direct,
+ * unlocked read; SYNC/SNAPSHOT must delegate to {@link UpsertViewManager},
which is the only path that respects
+ * segment tracking.
+ */
+public class ConcurrentMapTableUpsertMetadataManagerTest {
+ private static final String RAW_TABLE_NAME = "testTable";
+ private static final Schema SCHEMA = new Schema.SchemaBuilder()
+ .setSchemaName(RAW_TABLE_NAME)
+ .addSingleValueDimension("myCol", DataType.STRING)
+ .addDateTimeField("timeCol", DataType.LONG, "TIMESTAMP",
"1:MILLISECONDS")
+ .setPrimaryKeyColumns(List.of("myCol"))
+ .build();
+
+ private ConcurrentMapTableUpsertMetadataManager
createManager(UpsertConfig.ConsistencyMode consistencyMode) {
+ UpsertConfig upsertConfig = new UpsertConfig(UpsertConfig.Mode.FULL);
+ upsertConfig.setConsistencyMode(consistencyMode);
+ TableConfig tableConfig = new TableConfigBuilder(TableType.REALTIME)
+ .setTableName(RAW_TABLE_NAME)
+ .setTimeColumnName("timeCol")
+ .setUpsertConfig(upsertConfig)
+ .build();
+ TableDataManager tableDataManager = mock(TableDataManager.class);
+ when(tableDataManager.getTableDataDir()).thenReturn(new
File(RAW_TABLE_NAME));
+ return (ConcurrentMapTableUpsertMetadataManager)
TableUpsertMetadataManagerFactory.create(new PinotConfiguration(),
+ tableConfig, SCHEMA, tableDataManager, null);
+ }
+
+ private IndexSegment mockSegmentWithDistinctBitmaps() {
+ IndexSegment segment = mock(MutableSegment.class);
+ ThreadSafeMutableRoaringBitmap validBitmap =
mock(ThreadSafeMutableRoaringBitmap.class);
+ ThreadSafeMutableRoaringBitmap queryableBitmap =
mock(ThreadSafeMutableRoaringBitmap.class);
+ when(validBitmap.getMutableRoaringBitmap()).thenReturn(new
MutableRoaringBitmap());
+ when(queryableBitmap.getMutableRoaringBitmap()).thenReturn(new
MutableRoaringBitmap());
+ when(segment.getValidDocIds()).thenReturn(validBitmap);
+ when(segment.getQueryableDocIds()).thenReturn(queryableBitmap);
+ when(segment.getSegmentName()).thenReturn("seg1");
+ return segment;
+ }
+
+ @Test
+ public void testNoneModeTakesDirectReadEvenWhenUntracked() {
+ ConcurrentMapTableUpsertMetadataManager manager =
createManager(UpsertConfig.ConsistencyMode.NONE);
+ IndexSegment segment = mockSegmentWithDistinctBitmaps();
+ SegmentContext segmentContext = new SegmentContext(segment);
+
+ // Never tracked via any UpsertViewManager, yet a direct read still
populates the snapshot.
+ manager.setSegmentContexts(List.of(segmentContext),
Map.of(QueryOptionKey.SKIP_UPSERT_DELETE, "true"));
+ assertSame(segmentContext.getDocIdsSnapshot(),
segment.getValidDocIds().getMutableRoaringBitmap());
+ }
+
+ @Test
+ public void testSkipUpsertViewTakesDirectReadUnderSyncMode() {
+ ConcurrentMapTableUpsertMetadataManager manager =
createManager(UpsertConfig.ConsistencyMode.SYNC);
+ IndexSegment segment = mockSegmentWithDistinctBitmaps();
+ SegmentContext segmentContext = new SegmentContext(segment);
+
+ manager.setSegmentContexts(List.of(segmentContext),
+ Map.of(QueryOptionKey.SKIP_UPSERT_VIEW, "true",
QueryOptionKey.SKIP_UPSERT_DELETE, "true"));
+ assertSame(segmentContext.getDocIdsSnapshot(),
segment.getValidDocIds().getMutableRoaringBitmap());
+ }
+
+ /// ConcurrentMapTableUpsertMetadataManager's dispatch only special-cases
NONE (see the other tests above);
+ /// SYNC and SNAPSHOT both fall into this same delegate branch, so testing
one mode here is sufficient — their
+ /// differing internal behavior is UpsertViewManager's concern, covered by
UpsertViewManagerTest instead.
+ @Test
+ public void testDelegatesToUpsertViewManagerAndRespectsTracking() {
+ ConcurrentMapTableUpsertMetadataManager manager =
createManager(UpsertConfig.ConsistencyMode.SYNC);
+ IndexSegment segment = mockSegmentWithDistinctBitmaps();
+ SegmentContext segmentContext = new SegmentContext(segment);
+
+ // Force the partition manager (and its UpsertViewManager) to exist, but
deliberately don't track the segment:
+ // a correct delegation to UpsertViewManager leaves the snapshot unset,
since UpsertViewManager only serves
+ // tracked segments. A regression back to a direct-read bypass would
populate it regardless of tracking, as
+ // testNoneModeTakesDirectReadEvenWhenUntracked demonstrates. (Without
forcing the partition manager to exist
+ // first, this assertion would pass vacuously: setSegmentContexts would
iterate zero partition managers.)
+ manager.getOrCreatePartitionManager(0);
+ manager.setSegmentContexts(List.of(segmentContext),
Map.of(QueryOptionKey.SKIP_UPSERT_DELETE, "true"));
+ assertNull(segmentContext.getDocIdsSnapshot());
+
+
manager.getOrCreatePartitionManager(0).getUpsertViewManager().trackSegment(segment);
+ manager.setSegmentContexts(List.of(segmentContext),
Map.of(QueryOptionKey.SKIP_UPSERT_DELETE, "true"));
+ assertSame(segmentContext.getDocIdsSnapshot(),
segment.getValidDocIds().getMutableRoaringBitmap());
+ }
+}
diff --git
a/pinot-segment-local/src/test/java/org/apache/pinot/segment/local/upsert/UpsertUtilsTest.java
b/pinot-segment-local/src/test/java/org/apache/pinot/segment/local/upsert/UpsertUtilsTest.java
index de88a5b27d3..e60a397af28 100644
---
a/pinot-segment-local/src/test/java/org/apache/pinot/segment/local/upsert/UpsertUtilsTest.java
+++
b/pinot-segment-local/src/test/java/org/apache/pinot/segment/local/upsert/UpsertUtilsTest.java
@@ -20,6 +20,7 @@ package org.apache.pinot.segment.local.upsert;
import java.util.HashMap;
import
org.apache.pinot.segment.local.indexsegment.immutable.ImmutableSegmentImpl;
+import org.apache.pinot.segment.spi.IndexSegment;
import org.apache.pinot.segment.spi.index.metadata.SegmentMetadataImpl;
import
org.apache.pinot.segment.spi.index.mutable.ThreadSafeMutableRoaringBitmap;
import org.apache.pinot.segment.spi.store.SegmentDirectory;
@@ -30,11 +31,43 @@ import static org.mockito.ArgumentMatchers.any;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.when;
import static org.testng.Assert.assertFalse;
+import static org.testng.Assert.assertNull;
+import static org.testng.Assert.assertSame;
import static org.testng.Assert.assertTrue;
public class UpsertUtilsTest {
+ @Test
+ public void testGetValidDocIdsSnapshotFromSegmentReturnsValidDocsDirectly() {
+ IndexSegment segment = mock(IndexSegment.class);
+ ThreadSafeMutableRoaringBitmap validDocIds =
mock(ThreadSafeMutableRoaringBitmap.class);
+ MutableRoaringBitmap bitmap = new MutableRoaringBitmap();
+ when(validDocIds.getMutableRoaringBitmap()).thenReturn(bitmap);
+ when(segment.getValidDocIds()).thenReturn(validDocIds);
+ // Queryable docs must never be consulted by this method, even when
present.
+
when(segment.getQueryableDocIds()).thenReturn(mock(ThreadSafeMutableRoaringBitmap.class));
+
+ assertSame(UpsertUtils.getValidDocIdsSnapshotFromSegment(segment), bitmap);
+ }
+
+ @Test
+ public void
testGetValidDocIdsSnapshotFromSegmentNullWithoutUseEmptyForNull() {
+ IndexSegment segment = mock(IndexSegment.class);
+ when(segment.getValidDocIds()).thenReturn(null);
+
+ assertNull(UpsertUtils.getValidDocIdsSnapshotFromSegment(segment));
+ }
+
+ @Test
+ public void testGetValidDocIdsSnapshotFromSegmentEmptyWithUseEmptyForNull() {
+ IndexSegment segment = mock(IndexSegment.class);
+ when(segment.getValidDocIds()).thenReturn(null);
+
+ MutableRoaringBitmap result =
UpsertUtils.getValidDocIdsSnapshotFromSegment(segment, true);
+ assertTrue(result.isEmpty());
+ }
+
/// Non-upsert table: `_partitionUpsertMetadataManager` is never set, so
`hasNoQueryableDocs`
/// short-circuits to `false`.
@Test
@@ -135,6 +168,78 @@ public class UpsertUtilsTest {
assertFalse(segment.hasNoQueryableDocs());
}
+ // -------- hasNoValidDocs: mirrors hasNoQueryableDocs, but against the
valid-docs bitmap/cache. --------
+
+ @Test
+ public void testHasNoValidDocsNonUpsert() {
+ ImmutableSegmentImpl segment = newSegment();
+ assertFalse(segment.hasNoValidDocs());
+ }
+
+ @Test
+ public void testHasNoValidDocsLiveEmpty() {
+ ImmutableSegmentImpl segment = newSegment();
+ PartitionUpsertMetadataManager manager =
mock(PartitionUpsertMetadataManager.class);
+ when(manager.getUpsertViewManager()).thenReturn(null);
+ ThreadSafeMutableRoaringBitmap valid =
mock(ThreadSafeMutableRoaringBitmap.class);
+ when(valid.isEmpty()).thenReturn(true);
+ segment.enableUpsert(manager, valid, null);
+ assertTrue(segment.hasNoValidDocs());
+ }
+
+ @Test
+ public void testHasNoValidDocsLiveNonEmpty() {
+ ImmutableSegmentImpl segment = newSegment();
+ PartitionUpsertMetadataManager manager =
mock(PartitionUpsertMetadataManager.class);
+ when(manager.getUpsertViewManager()).thenReturn(null);
+ ThreadSafeMutableRoaringBitmap valid =
mock(ThreadSafeMutableRoaringBitmap.class);
+ when(valid.isEmpty()).thenReturn(false);
+ segment.enableUpsert(manager, valid, null);
+ assertFalse(segment.hasNoValidDocs());
+ }
+
+ @Test
+ public void testHasNoValidDocsConsistencyModeSnapshotEmpty() {
+ ImmutableSegmentImpl segment = newSegment();
+ PartitionUpsertMetadataManager manager =
mock(PartitionUpsertMetadataManager.class);
+ UpsertViewManager viewManager = mock(UpsertViewManager.class);
+ when(manager.getUpsertViewManager()).thenReturn(viewManager);
+ when(viewManager.getValidDocIdsSnapshot(any())).thenReturn(new
MutableRoaringBitmap());
+ ThreadSafeMutableRoaringBitmap liveValid =
mock(ThreadSafeMutableRoaringBitmap.class);
+ when(liveValid.isEmpty()).thenReturn(false);
+ segment.enableUpsert(manager, liveValid, null);
+ assertTrue(segment.hasNoValidDocs());
+ }
+
+ @Test
+ public void testHasNoValidDocsConsistencyModeSnapshotNonEmpty() {
+ ImmutableSegmentImpl segment = newSegment();
+ PartitionUpsertMetadataManager manager =
mock(PartitionUpsertMetadataManager.class);
+ UpsertViewManager viewManager = mock(UpsertViewManager.class);
+ MutableRoaringBitmap snapshot = new MutableRoaringBitmap();
+ snapshot.add(0);
+ when(manager.getUpsertViewManager()).thenReturn(viewManager);
+ when(viewManager.getValidDocIdsSnapshot(any())).thenReturn(snapshot);
+ ThreadSafeMutableRoaringBitmap liveValid =
mock(ThreadSafeMutableRoaringBitmap.class);
+ when(liveValid.isEmpty()).thenReturn(true);
+ segment.enableUpsert(manager, liveValid, null);
+ assertFalse(segment.hasNoValidDocs());
+ }
+
+ @Test
+ public void testHasNoValidDocsConsistencyModeSnapshotAbsent() {
+ // No refresh yet: don't claim empty even if the live bitmap currently is.
+ ImmutableSegmentImpl segment = newSegment();
+ PartitionUpsertMetadataManager manager =
mock(PartitionUpsertMetadataManager.class);
+ UpsertViewManager viewManager = mock(UpsertViewManager.class);
+ when(manager.getUpsertViewManager()).thenReturn(viewManager);
+ when(viewManager.getValidDocIdsSnapshot(any())).thenReturn(null);
+ ThreadSafeMutableRoaringBitmap liveValid =
mock(ThreadSafeMutableRoaringBitmap.class);
+ when(liveValid.isEmpty()).thenReturn(true);
+ segment.enableUpsert(manager, liveValid, null);
+ assertFalse(segment.hasNoValidDocs());
+ }
+
/// Returns a minimal {@link ImmutableSegmentImpl} usable for testing the
upsert-aware methods.
/// Mirrors the pattern used in {@code
BasePartitionUpsertMetadataManagerTest#createImmutableSegment}.
private static ImmutableSegmentImpl newSegment() {
diff --git
a/pinot-segment-local/src/test/java/org/apache/pinot/segment/local/upsert/UpsertViewManagerTest.java
b/pinot-segment-local/src/test/java/org/apache/pinot/segment/local/upsert/UpsertViewManagerTest.java
index 95d48b66780..3866ed23b43 100644
---
a/pinot-segment-local/src/test/java/org/apache/pinot/segment/local/upsert/UpsertViewManagerTest.java
+++
b/pinot-segment-local/src/test/java/org/apache/pinot/segment/local/upsert/UpsertViewManagerTest.java
@@ -20,12 +20,14 @@ package org.apache.pinot.segment.local.upsert;
import java.util.HashMap;
import java.util.List;
+import java.util.Map;
import java.util.Set;
import org.apache.pinot.segment.spi.IndexSegment;
import org.apache.pinot.segment.spi.MutableSegment;
import org.apache.pinot.segment.spi.SegmentContext;
import
org.apache.pinot.segment.spi.index.mutable.ThreadSafeMutableRoaringBitmap;
import org.apache.pinot.spi.config.table.UpsertConfig;
+import org.apache.pinot.spi.utils.CommonConstants;
import org.roaringbitmap.buffer.MutableRoaringBitmap;
import org.testng.annotations.Test;
@@ -52,12 +54,79 @@ public class UpsertViewManagerTest {
mgr.trackSegment(seg1);
assertEquals(mgr.getTrackedSegments(), Set.of(seg1));
mgr.setSegmentContexts(List.of(segCtx1), new HashMap<>());
- assertSame(segCtx1.getQueryableDocIdsSnapshot(), mutableRoaringBitmap);
+ assertSame(segCtx1.getDocIdsSnapshot(), mutableRoaringBitmap);
mgr.untrackSegment(seg1);
assertTrue(mgr.getTrackedSegments().isEmpty());
segCtx1 = new SegmentContext(seg1);
mgr.setSegmentContexts(List.of(segCtx1), new HashMap<>());
- assertNull(segCtx1.getQueryableDocIdsSnapshot());
+ assertNull(segCtx1.getDocIdsSnapshot());
+ }
+
+ @Test
+ public void testSkipUpsertDeleteReadsValidBitmapUnderSyncMode() {
+ UpsertViewManager mgr = new
UpsertViewManager(UpsertConfig.ConsistencyMode.SYNC, mock(UpsertContext.class));
+ IndexSegment seg1 = mockSegmentWithDistinctBitmaps();
+ mgr.trackSegment(seg1);
+
+ SegmentContext segCtx = new SegmentContext(seg1);
+ mgr.setSegmentContexts(List.of(segCtx),
Map.of(CommonConstants.Broker.Request.QueryOptionKey.SKIP_UPSERT_DELETE,
+ "true"));
+ assertSame(segCtx.getDocIdsSnapshot(), VALID_RESULT);
+
+ SegmentContext defaultCtx = new SegmentContext(seg1);
+ mgr.setSegmentContexts(List.of(defaultCtx), new HashMap<>());
+ assertSame(defaultCtx.getDocIdsSnapshot(), QUERYABLE_RESULT);
+ }
+
+ @Test
+ public void testSkipUpsertDeleteReadsValidBitmapUnderSnapshotMode() {
+ UpsertViewManager mgr = new
UpsertViewManager(UpsertConfig.ConsistencyMode.SNAPSHOT,
mock(UpsertContext.class));
+ IndexSegment seg1 = mockSegmentWithDistinctBitmaps();
+ mgr.trackSegment(seg1);
+
+ // skipUpsertDelete reads the cached valid-docs map, refreshed alongside
the queryable-docs one on track.
+ SegmentContext segCtx = new SegmentContext(seg1);
+ mgr.setSegmentContexts(List.of(segCtx),
Map.of(CommonConstants.Broker.Request.QueryOptionKey.SKIP_UPSERT_DELETE,
+ "true"));
+ assertSame(segCtx.getDocIdsSnapshot(), VALID_RESULT);
+
+ // Without the option, the cached queryable-docs map is used instead.
+ SegmentContext defaultCtx = new SegmentContext(seg1);
+ mgr.setSegmentContexts(List.of(defaultCtx), new HashMap<>());
+ assertSame(defaultCtx.getDocIdsSnapshot(), QUERYABLE_RESULT);
+ }
+
+ /// Without a delete column, queryable docs == valid docs; the refresh
reuses one clone for both caches instead
+ /// of cloning twice (see doBatchRefreshUpsertView), so both maps must
resolve to the same bitmap instance.
+ @Test
+ public void testNoDeleteColumnReusesSameBitmapInBothCaches() {
+ UpsertViewManager mgr = new
UpsertViewManager(UpsertConfig.ConsistencyMode.SNAPSHOT,
mock(UpsertContext.class));
+ IndexSegment seg1 = mock(MutableSegment.class);
+ ThreadSafeMutableRoaringBitmap validBitmap =
mock(ThreadSafeMutableRoaringBitmap.class);
+ when(validBitmap.getMutableRoaringBitmap()).thenReturn(new
MutableRoaringBitmap());
+ when(seg1.getValidDocIds()).thenReturn(validBitmap);
+ when(seg1.getQueryableDocIds()).thenReturn(null);
+ when(seg1.getSegmentName()).thenReturn("seg1");
+
+ mgr.trackSegment(seg1);
+ assertSame(mgr.getQueryableDocIdsSnapshot(seg1),
mgr.getValidDocIdsSnapshot(seg1));
+ }
+
+ private static final MutableRoaringBitmap VALID_RESULT = new
MutableRoaringBitmap();
+ private static final MutableRoaringBitmap QUERYABLE_RESULT = new
MutableRoaringBitmap();
+
+ /// Mocks a segment whose valid-docs and queryable-docs bitmaps resolve to
two different, identity-comparable
+ /// {@link MutableRoaringBitmap} instances, so tests can assert exactly
which one a code path picked.
+ private static IndexSegment mockSegmentWithDistinctBitmaps() {
+ IndexSegment segment = mock(MutableSegment.class);
+ ThreadSafeMutableRoaringBitmap validBitmap =
mock(ThreadSafeMutableRoaringBitmap.class);
+ ThreadSafeMutableRoaringBitmap queryableBitmap =
mock(ThreadSafeMutableRoaringBitmap.class);
+ when(validBitmap.getMutableRoaringBitmap()).thenReturn(VALID_RESULT);
+
when(queryableBitmap.getMutableRoaringBitmap()).thenReturn(QUERYABLE_RESULT);
+ when(segment.getValidDocIds()).thenReturn(validBitmap);
+ when(segment.getQueryableDocIds()).thenReturn(queryableBitmap);
+ when(segment.getSegmentName()).thenReturn("seg1");
+ return segment;
}
}
diff --git
a/pinot-segment-spi/src/main/java/org/apache/pinot/segment/spi/IndexSegment.java
b/pinot-segment-spi/src/main/java/org/apache/pinot/segment/spi/IndexSegment.java
index a491d79457c..acd90638b17 100644
---
a/pinot-segment-spi/src/main/java/org/apache/pinot/segment/spi/IndexSegment.java
+++
b/pinot-segment-spi/src/main/java/org/apache/pinot/segment/spi/IndexSegment.java
@@ -110,6 +110,11 @@ public interface IndexSegment {
return false;
}
+ /// Same as {@link #hasNoQueryableDocs}, against {@link #getValidDocIds}
instead.
+ default boolean hasNoValidDocs() {
+ return false;
+ }
+
/**
* Returns the record for the given document id. Virtual column values are
not returned.
* <p>NOTE: don't use this method for high performance code. Use
PinotSegmentRecordReader when reading multiple
diff --git
a/pinot-segment-spi/src/main/java/org/apache/pinot/segment/spi/SegmentContext.java
b/pinot-segment-spi/src/main/java/org/apache/pinot/segment/spi/SegmentContext.java
index f2c3b8f8372..993612b5871 100644
---
a/pinot-segment-spi/src/main/java/org/apache/pinot/segment/spi/SegmentContext.java
+++
b/pinot-segment-spi/src/main/java/org/apache/pinot/segment/spi/SegmentContext.java
@@ -24,8 +24,9 @@ import org.roaringbitmap.buffer.MutableRoaringBitmap;
public class SegmentContext {
private final IndexSegment _indexSegment;
+ /// Queryable-docs bitmap by default, or valid-docs (tombstones included)
when the query set `skipUpsertDelete`.
@Nullable
- private MutableRoaringBitmap _queryableDocIdsSnapshot = null;
+ private MutableRoaringBitmap _docIdsSnapshot = null;
public SegmentContext(IndexSegment indexSegment) {
_indexSegment = indexSegment;
@@ -35,12 +36,25 @@ public class SegmentContext {
return _indexSegment;
}
+ @Nullable
+ public MutableRoaringBitmap getDocIdsSnapshot() {
+ return _docIdsSnapshot;
+ }
+
+ public void setDocIdsSnapshot(@Nullable MutableRoaringBitmap docIdsSnapshot)
{
+ _docIdsSnapshot = docIdsSnapshot;
+ }
+
+ /// @deprecated Use {@link #getDocIdsSnapshot} instead; kept for binary
compatibility.
+ @Deprecated
@Nullable
public MutableRoaringBitmap getQueryableDocIdsSnapshot() {
- return _queryableDocIdsSnapshot;
+ return getDocIdsSnapshot();
}
+ /// @deprecated Use {@link #setDocIdsSnapshot} instead; kept for binary
compatibility.
+ @Deprecated
public void setQueryableDocIdsSnapshot(@Nullable MutableRoaringBitmap
queryableDocIdsSnapshot) {
- _queryableDocIdsSnapshot = queryableDocIdsSnapshot;
+ setDocIdsSnapshot(queryableDocIdsSnapshot);
}
}
diff --git
a/pinot-spi/src/main/java/org/apache/pinot/spi/utils/CommonConstants.java
b/pinot-spi/src/main/java/org/apache/pinot/spi/utils/CommonConstants.java
index f617e91b74b..c4b6fa71c90 100644
--- a/pinot-spi/src/main/java/org/apache/pinot/spi/utils/CommonConstants.java
+++ b/pinot-spi/src/main/java/org/apache/pinot/spi/utils/CommonConstants.java
@@ -741,6 +741,9 @@ public class CommonConstants {
public static final String SKIP_UPSERT = "skipUpsert";
public static final String SKIP_UPSERT_VIEW = "skipUpsertView";
public static final String UPSERT_VIEW_FRESHNESS_MS =
"upsertViewFreshnessMs";
+ /// Default `false` (queryable docs, tombstones excluded). When
`true`, skips the delete/tombstone
+ /// exclusion so rows deleted via `deleteRecordColumn` are included.
+ public static final String SKIP_UPSERT_DELETE = "skipUpsertDelete";
public static final String USE_STAR_TREE = "useStarTree";
/**
* When true, use index-based distinct operators when applicable. This
enables both
---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]