This is an automated email from the ASF dual-hosted git repository.
KKcorps 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 85db332cfca Reject consuming during download for upsert tables that
revert metadata in PROTECTED mode (#19710)
85db332cfca is described below
commit 85db332cfca365508212e712f6527d959e2ed172
Author: Kartik Khare <[email protected]>
AuthorDate: Tue Oct 6 17:01:53 2026 +0530
Reject consuming during download for upsert tables that revert metadata in
PROTECTED mode (#19710)
* Reject consuming during download for upsert tables that revert metadata
in PROTECTED mode
In PROTECTED mode, partial upsert and out-of-order tables revert keys the
replica consumed past the
committed end offset when the downloaded segment replaces the consuming
one. If the next consuming
segment starts during that download, its validDocIds snapshot runs before
the revert and misses the
rows the revert restores. Reject such policies in table config validation.
When the combination still
happens at runtime, the server logs an error and bumps a meter instead of
changing the policy.
Co-Authored-By: Claude Opus 5.5 (1M context) <[email protected]>
Claude-Session: https://claude.ai/code/session_01W5J32DeWRrd7a1e3z6Y5bN
* Rename the upsert revert consumption check and name the rejected setting
Address review: rename to validateConsumptionDuringUpsertRevert and start
the error with the policy or
flag that was rejected, so ALLOW_ALWAYS and ALLOW_DURING_DOWNLOAD_ONLY are
visible in the message.
Co-Authored-By: Claude Opus 5.5 (1M context) <[email protected]>
Claude-Session: https://claude.ai/code/session_01W5J32DeWRrd7a1e3z6Y5bN
* Reject ALLOW_DURING_BUILD_ONLY for upsert revert tables and flag the
build-to-download fallback
A local build under ALLOW_DURING_BUILD_ONLY releases the consumer semaphore
before the CRC check. On a failed build
or a CRC mismatch the server then downloads the committed copy and reverts
upsert metadata while the next consuming
segment can already be taking its snapshot. Validation now accepts only
DISALLOW_ALWAYS for these tables in
PROTECTED mode, except pauseless tables, which consume during build by
design. The server meter now also fires on
that fallback download.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
---------
Co-authored-by: Kartik Khare <[email protected]>
Co-authored-by: Claude Opus 5.5 (1M context) <[email protected]>
---
.../apache/pinot/common/metrics/ServerMeter.java | 1 +
.../realtime/RealtimeSegmentDataManager.java | 16 +++-
.../realtime/RealtimeSegmentDataManagerTest.java | 103 ++++++++++++++++++++-
.../upsert/BasePartitionUpsertMetadataManager.java | 1 +
.../upsert/PartitionUpsertMetadataManager.java | 5 +
.../segment/local/utils/TableConfigUtils.java | 32 +++++++
...rrentMapPartitionUpsertMetadataManagerTest.java | 22 +++++
.../segment/local/utils/TableConfigUtilsTest.java | 88 ++++++++++++++++++
8 files changed, 264 insertions(+), 4 deletions(-)
diff --git
a/pinot-common/src/main/java/org/apache/pinot/common/metrics/ServerMeter.java
b/pinot-common/src/main/java/org/apache/pinot/common/metrics/ServerMeter.java
index 91ca1327069..f862ed63aea 100644
---
a/pinot-common/src/main/java/org/apache/pinot/common/metrics/ServerMeter.java
+++
b/pinot-common/src/main/java/org/apache/pinot/common/metrics/ServerMeter.java
@@ -74,6 +74,7 @@ public enum ServerMeter implements AbstractMetrics.Meter {
UPSERT_MISSED_VALID_DOC_ID_SNAPSHOT_COUNT("segments", false),
UPSERT_MISSED_QUERYABLE_DOC_ID_SNAPSHOT_COUNT("segments", false),
UPSERT_PRELOAD_FAILURE("count", false),
+ UPSERT_REVERT_WITH_CONSUMPTION_DURING_DOWNLOAD("segments", false),
ROWS_WITH_ERRORS("rows", false),
LLC_CONTROLLER_RESPONSE_NOT_SENT("messages", true),
LLC_CONTROLLER_RESPONSE_COMMIT("messages", true),
diff --git
a/pinot-core/src/main/java/org/apache/pinot/core/data/manager/realtime/RealtimeSegmentDataManager.java
b/pinot-core/src/main/java/org/apache/pinot/core/data/manager/realtime/RealtimeSegmentDataManager.java
index f096ab6cc92..b330c781dff 100644
---
a/pinot-core/src/main/java/org/apache/pinot/core/data/manager/realtime/RealtimeSegmentDataManager.java
+++
b/pinot-core/src/main/java/org/apache/pinot/core/data/manager/realtime/RealtimeSegmentDataManager.java
@@ -1751,7 +1751,21 @@ public class RealtimeSegmentDataManager extends
SegmentDataManager {
protected void downloadSegmentAndReplace(SegmentZKMetadata segmentZKMetadata)
throws Exception {
- if (_parallelSegmentConsumptionPolicy.isAllowedDuringDownload()) {
+ boolean allowedDuringDownload =
_parallelSegmentConsumptionPolicy.isAllowedDuringDownload();
+ // A local build that failed or mismatched the committed CRC has already
released the semaphore when the policy
+ // allows consumption during build, so the next consuming segment can be
running during this download as well
+ boolean releasedDuringBuild =
+ _parallelSegmentConsumptionPolicy.isAllowedDuringBuild() &&
!_consumerSemaphoreAcquired.get();
+ if ((allowedDuringDownload || releasedDuringBuild) &&
_partitionUpsertMetadataManager != null
+ &&
_partitionUpsertMetadataManager.shouldRevertMetadataOnInconsistency(_realtimeSegment))
{
+ // Table config validation rejects this in PROTECTED mode. It still
happens when the mode is switched after the
+ // table is created, when the server-level default allows partial upsert
consumption during commit, or when a
+ // pauseless table falls back to a download after its local build fails.
+ _segmentLogger.error("Next consuming segment can run during download
while this replace reverts upsert "
+ + "metadata. Its snapshot can miss the rows the revert restores. Use
DISALLOW_ALWAYS");
+ _serverMetrics.addMeteredTableValue(_clientId,
ServerMeter.UPSERT_REVERT_WITH_CONSUMPTION_DURING_DOWNLOAD, 1L);
+ }
+ if (allowedDuringDownload) {
closeStreamConsumerAndReleaseSemaphore();
}
_realtimeTableDataManager.downloadAndReplaceConsumingSegment(segmentZKMetadata);
diff --git
a/pinot-core/src/test/java/org/apache/pinot/core/data/manager/realtime/RealtimeSegmentDataManagerTest.java
b/pinot-core/src/test/java/org/apache/pinot/core/data/manager/realtime/RealtimeSegmentDataManagerTest.java
index c778794ffb8..6dd607bb91c 100644
---
a/pinot-core/src/test/java/org/apache/pinot/core/data/manager/realtime/RealtimeSegmentDataManagerTest.java
+++
b/pinot-core/src/test/java/org/apache/pinot/core/data/manager/realtime/RealtimeSegmentDataManagerTest.java
@@ -40,6 +40,7 @@ import javax.annotation.Nullable;
import org.apache.commons.io.FileUtils;
import org.apache.helix.HelixManager;
import org.apache.pinot.common.metadata.segment.SegmentZKMetadata;
+import org.apache.pinot.common.metrics.ServerMeter;
import org.apache.pinot.common.metrics.ServerMetrics;
import org.apache.pinot.common.protocols.SegmentCompletionProtocol;
import org.apache.pinot.common.utils.LLCSegmentName;
@@ -56,6 +57,7 @@ import
org.apache.pinot.segment.local.segment.creator.Fixtures;
import
org.apache.pinot.segment.local.segment.creator.impl.SegmentIndexCreationDriverImpl;
import org.apache.pinot.segment.local.segment.index.loader.IndexLoadingConfig;
import org.apache.pinot.segment.local.segment.readers.GenericRowRecordReader;
+import org.apache.pinot.segment.local.upsert.PartitionUpsertMetadataManager;
import org.apache.pinot.segment.local.utils.SegmentLocks;
import org.apache.pinot.segment.local.utils.ServerReloadJobStatusCache;
import org.apache.pinot.segment.spi.creator.SegmentGeneratorConfig;
@@ -63,6 +65,7 @@ import
org.apache.pinot.segment.spi.index.metadata.SegmentMetadataImpl;
import org.apache.pinot.spi.config.instance.InstanceDataManagerConfig;
import org.apache.pinot.spi.config.table.TableConfig;
import org.apache.pinot.spi.config.table.ingestion.IngestionConfig;
+import
org.apache.pinot.spi.config.table.ingestion.ParallelSegmentConsumptionPolicy;
import org.apache.pinot.spi.config.table.ingestion.StreamIngestionConfig;
import org.apache.pinot.spi.data.Schema;
import org.apache.pinot.spi.data.readers.GenericRow;
@@ -81,6 +84,7 @@ import org.testng.annotations.BeforeClass;
import org.testng.annotations.Test;
import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.ArgumentMatchers.anyLong;
import static org.mockito.ArgumentMatchers.argThat;
import static org.mockito.ArgumentMatchers.eq;
import static org.mockito.ArgumentMatchers.same;
@@ -233,6 +237,14 @@ public class RealtimeSegmentDataManagerTest {
private FakeRealtimeSegmentDataManager createFakeSegmentManager(boolean
noUpsert, TimeSupplier timeSupplier,
@Nullable String maxRows, @Nullable String maxDuration, @Nullable
TableConfig tableConfig)
throws Exception {
+ return createFakeSegmentManager(noUpsert, timeSupplier, maxRows,
maxDuration, tableConfig,
+ new ServerMetrics(PinotMetricUtils.getPinotMetricsRegistry()));
+ }
+
+ private FakeRealtimeSegmentDataManager createFakeSegmentManager(boolean
noUpsert, TimeSupplier timeSupplier,
+ @Nullable String maxRows, @Nullable String maxDuration, @Nullable
TableConfig tableConfig,
+ ServerMetrics serverMetrics)
+ throws Exception {
SegmentZKMetadata segmentZKMetadata = createZkMetadata();
if (tableConfig == null) {
tableConfig = createTableConfig();
@@ -257,7 +269,6 @@ public class RealtimeSegmentDataManagerTest {
_partitionGroupIdToConsumerCoordinatorMap.putIfAbsent(PARTITION_GROUP_ID,
new ConsumerCoordinator(false, tableDataManager));
Schema schema = Fixtures.createSchema();
- ServerMetrics serverMetrics = new
ServerMetrics(PinotMetricUtils.getPinotMetricsRegistry());
return new FakeRealtimeSegmentDataManager(segmentZKMetadata, tableConfig,
tableDataManager,
new File(TEMP_DIR, REALTIME_TABLE_NAME).getAbsolutePath(), schema,
llcSegmentName,
_partitionGroupIdToConsumerCoordinatorMap, serverMetrics,
timeSupplier);
@@ -879,12 +890,77 @@ public class RealtimeSegmentDataManagerTest {
return metadata;
}
+ @Test
+ public void
testDownloadAfterLocalBuildFlagsUpsertRevertWhenConsumingDuringBuild()
+ throws Exception {
+ long finalOffsetValue = START_OFFSET_VALUE + 600;
+
+ // ALLOW_DURING_BUILD_ONLY: the local build releases the consumer
semaphore, its CRC mismatches the committed copy,
+ // and the download that follows reverts upsert metadata while the next
consuming segment can already be running.
+ _partitionGroupIdToConsumerCoordinatorMap.remove(PARTITION_GROUP_ID);
+ ServerMetrics serverMetrics = spy(new
ServerMetrics(PinotMetricUtils.getPinotMetricsRegistry()));
+ try (FakeRealtimeSegmentDataManager segmentDataManager =
createFakeSegmentManagerForUpsertRevert(
+ ParallelSegmentConsumptionPolicy.ALLOW_DURING_BUILD_ONLY,
serverMetrics)) {
+ Semaphore semaphore =
_partitionGroupIdToConsumerCoordinatorMap.get(PARTITION_GROUP_ID).getSemaphore();
+ Assert.assertTrue(semaphore.tryAcquire());
+ segmentDataManager._releaseSemaphoreOnBuild = true;
+ runGoOnlineForCrcGuard(segmentDataManager, crcMetadata(finalOffsetValue,
12345L), false, finalOffsetValue);
+ Assert.assertTrue(segmentDataManager._downloadAndReplaceCalled);
+ Assert.assertEquals(semaphore.availablePermits(), 1);
+ verify(serverMetrics).addMeteredTableValue(anyString(),
+ eq(ServerMeter.UPSERT_REVERT_WITH_CONSUMPTION_DURING_DOWNLOAD),
eq(1L));
+ }
+
+ // ALLOW_DURING_BUILD_ONLY without a local build: the download still holds
the semaphore, so nothing is flagged.
+ _partitionGroupIdToConsumerCoordinatorMap.remove(PARTITION_GROUP_ID);
+ serverMetrics = spy(new
ServerMetrics(PinotMetricUtils.getPinotMetricsRegistry()));
+ try (FakeRealtimeSegmentDataManager segmentDataManager =
createFakeSegmentManagerForUpsertRevert(
+ ParallelSegmentConsumptionPolicy.ALLOW_DURING_BUILD_ONLY,
serverMetrics)) {
+ Semaphore semaphore =
_partitionGroupIdToConsumerCoordinatorMap.get(PARTITION_GROUP_ID).getSemaphore();
+ Assert.assertTrue(semaphore.tryAcquire());
+ segmentDataManager.getConsumerSemaphoreAcquired().set(true);
+
segmentDataManager.downloadSegmentAndReplace(crcMetadata(finalOffsetValue,
12345L));
+ Assert.assertEquals(semaphore.availablePermits(), 0);
+ verify(serverMetrics, never()).addMeteredTableValue(anyString(),
+ eq(ServerMeter.UPSERT_REVERT_WITH_CONSUMPTION_DURING_DOWNLOAD),
anyLong());
+ }
+
+ // DISALLOW_ALWAYS never lets the next segment consume during the replace,
even when this segment never acquired the
+ // semaphore.
+ serverMetrics = spy(new
ServerMetrics(PinotMetricUtils.getPinotMetricsRegistry()));
+ try (FakeRealtimeSegmentDataManager segmentDataManager =
createFakeSegmentManagerForUpsertRevert(
+ ParallelSegmentConsumptionPolicy.DISALLOW_ALWAYS, serverMetrics)) {
+
segmentDataManager.downloadSegmentAndReplace(crcMetadata(finalOffsetValue,
12345L));
+ verify(serverMetrics, never()).addMeteredTableValue(anyString(),
+ eq(ServerMeter.UPSERT_REVERT_WITH_CONSUMPTION_DURING_DOWNLOAD),
anyLong());
+ }
+ }
+
+ private FakeRealtimeSegmentDataManager
createFakeSegmentManagerForUpsertRevert(
+ ParallelSegmentConsumptionPolicy policy, ServerMetrics serverMetrics)
+ throws Exception {
+ FakeRealtimeSegmentDataManager segmentDataManager =
createFakeSegmentManager(false, new TimeSupplier(), null, null,
+ createTableConfigWithStreamIngestion(false, policy), serverMetrics);
+ PartitionUpsertMetadataManager upsertMetadataManager =
mock(PartitionUpsertMetadataManager.class);
+
when(upsertMetadataManager.shouldRevertMetadataOnInconsistency(any())).thenReturn(true);
+
segmentDataManager.setPartitionUpsertMetadataManager(upsertMetadataManager);
+ segmentDataManager._useRealDownloadAndReplace = true;
+ return segmentDataManager;
+ }
+
private TableConfig createPauselessTableConfig()
throws Exception {
+ return createTableConfigWithStreamIngestion(true, null);
+ }
+
+ private TableConfig createTableConfigWithStreamIngestion(boolean pauseless,
+ @Nullable ParallelSegmentConsumptionPolicy policy)
+ throws Exception {
TableConfig tableConfig = createTableConfig();
StreamIngestionConfig streamIngestionConfig =
new
StreamIngestionConfig(List.of(tableConfig.getIndexingConfig().getStreamConfigs()));
- streamIngestionConfig.setPauselessConsumptionEnabled(true);
+ streamIngestionConfig.setPauselessConsumptionEnabled(pauseless);
+ streamIngestionConfig.setParallelSegmentConsumptionPolicy(policy);
IngestionConfig ingestionConfig = tableConfig.getIngestionConfig();
if (ingestionConfig == null) {
ingestionConfig = new IngestionConfig();
@@ -1500,6 +1576,11 @@ public class RealtimeSegmentDataManagerTest {
public boolean _localSegmentCrcMatchesZk = true;
// When set, isLocalSegmentCrcMatchingZk runs the real on-disk metadata
read + CRC comparison.
public boolean _useRealCrcCheck = false;
+ // When set, buildSegmentInternal releases the consumer semaphore like the
real build does when the parallel
+ // consumption policy allows consuming during build.
+ public boolean _releaseSemaphoreOnBuild = false;
+ // When set, downloadSegmentAndReplace runs the real implementation
against the mocked table data manager.
+ public boolean _useRealDownloadAndReplace = false;
private Field _streamMsgOffsetFactory;
public LinkedList<LongMsgOffset> _consumeOffsets = new LinkedList<>();
public LinkedList<SegmentCompletionProtocol.Response> _responses = new
LinkedList<>();
@@ -1569,6 +1650,15 @@ public class RealtimeSegmentDataManagerTest {
realtimeSegmentField.set(this, realtimeSegment);
}
+ /// Replaces the upsert metadata manager, which the constructor always
leaves null.
+ public void
setPartitionUpsertMetadataManager(PartitionUpsertMetadataManager
partitionUpsertMetadataManager)
+ throws Exception {
+ Field partitionUpsertMetadataManagerField =
+
RealtimeSegmentDataManager.class.getDeclaredField("_partitionUpsertMetadataManager");
+ partitionUpsertMetadataManagerField.setAccessible(true);
+ partitionUpsertMetadataManagerField.set(this,
partitionUpsertMetadataManager);
+ }
+
public boolean isStreamConsumerClosed()
throws Exception {
Field streamConsumerClosedField =
RealtimeSegmentDataManager.class.getDeclaredField("_streamConsumerClosed");
@@ -1717,6 +1807,9 @@ public class RealtimeSegmentDataManagerTest {
protected SegmentBuildDescriptor buildSegmentInternal(boolean forCommit) {
terminateLoopIfNecessary();
_buildSegmentCalled = true;
+ if (_releaseSemaphoreOnBuild) {
+ closeStreamConsumerAndReleaseSemaphore();
+ }
if (_failSegmentBuild) {
try {
_segmentBuildFailedWithDeterministicError.set(this, true);
@@ -1745,9 +1838,13 @@ public class RealtimeSegmentDataManagerTest {
}
@Override
- protected void downloadSegmentAndReplace(SegmentZKMetadata metadata) {
+ protected void downloadSegmentAndReplace(SegmentZKMetadata metadata)
+ throws Exception {
terminateLoopIfNecessary();
_downloadAndReplaceCalled = true;
+ if (_useRealDownloadAndReplace) {
+ super.downloadSegmentAndReplace(metadata);
+ }
}
@Override
diff --git
a/pinot-segment-local/src/main/java/org/apache/pinot/segment/local/upsert/BasePartitionUpsertMetadataManager.java
b/pinot-segment-local/src/main/java/org/apache/pinot/segment/local/upsert/BasePartitionUpsertMetadataManager.java
index 9c1351f6dc6..09a83c7becd 100644
---
a/pinot-segment-local/src/main/java/org/apache/pinot/segment/local/upsert/BasePartitionUpsertMetadataManager.java
+++
b/pinot-segment-local/src/main/java/org/apache/pinot/segment/local/upsert/BasePartitionUpsertMetadataManager.java
@@ -716,6 +716,7 @@ public abstract class BasePartitionUpsertMetadataManager
implements PartitionUps
///
/// @param oldSegment the old segment being replaced
/// @return true if metadata revert should be performed on inconsistency
+ @Override
public boolean shouldRevertMetadataOnInconsistency(IndexSegment oldSegment) {
return
ConsumingSegmentConsistencyModeListener.getInstance().getConsistencyMode()
== ConsumingSegmentConsistencyModeListener.Mode.PROTECTED
diff --git
a/pinot-segment-local/src/main/java/org/apache/pinot/segment/local/upsert/PartitionUpsertMetadataManager.java
b/pinot-segment-local/src/main/java/org/apache/pinot/segment/local/upsert/PartitionUpsertMetadataManager.java
index 4adc57b18a2..112b2c17f55 100644
---
a/pinot-segment-local/src/main/java/org/apache/pinot/segment/local/upsert/PartitionUpsertMetadataManager.java
+++
b/pinot-segment-local/src/main/java/org/apache/pinot/segment/local/upsert/PartitionUpsertMetadataManager.java
@@ -74,6 +74,11 @@ public interface PartitionUpsertMetadataManager extends
Closeable {
/// Replaces the upsert metadata for the old segment with the new immutable
segment.
void replaceSegment(ImmutableSegment segment, IndexSegment oldSegment);
+ /// Returns true when replacing the given segment reverts its keys to their
previous locations.
+ default boolean shouldRevertMetadataOnInconsistency(IndexSegment oldSegment)
{
+ return false;
+ }
+
/// Removes the upsert metadata for the given segment.
void removeSegment(IndexSegment segment);
diff --git
a/pinot-segment-local/src/main/java/org/apache/pinot/segment/local/utils/TableConfigUtils.java
b/pinot-segment-local/src/main/java/org/apache/pinot/segment/local/utils/TableConfigUtils.java
index 2ece7905724..c77b42930a4 100644
---
a/pinot-segment-local/src/main/java/org/apache/pinot/segment/local/utils/TableConfigUtils.java
+++
b/pinot-segment-local/src/main/java/org/apache/pinot/segment/local/utils/TableConfigUtils.java
@@ -91,6 +91,7 @@ import
org.apache.pinot.spi.config.table.ingestion.ComplexTypeConfig;
import org.apache.pinot.spi.config.table.ingestion.EnrichmentConfig;
import org.apache.pinot.spi.config.table.ingestion.FilterConfig;
import org.apache.pinot.spi.config.table.ingestion.IngestionConfig;
+import
org.apache.pinot.spi.config.table.ingestion.ParallelSegmentConsumptionPolicy;
import
org.apache.pinot.spi.config.table.ingestion.SchemaConformingTransformerConfig;
import org.apache.pinot.spi.config.table.ingestion.SourceFieldConfig;
import org.apache.pinot.spi.config.table.ingestion.StreamIngestionConfig;
@@ -108,6 +109,7 @@ import org.apache.pinot.spi.stream.StreamConfig;
import org.apache.pinot.spi.stream.StreamConfigProperties;
import org.apache.pinot.spi.utils.CommonConstants;
import org.apache.pinot.spi.utils.CommonConstants.Segment.AssignmentStrategy;
+import org.apache.pinot.spi.utils.ConsumingSegmentConsistencyModeListener;
import org.apache.pinot.spi.utils.DataSizeUtils;
import org.apache.pinot.spi.utils.Enablement;
import org.apache.pinot.spi.utils.IngestionConfigUtils;
@@ -1211,6 +1213,7 @@ public final class TableConfigUtils {
validateTTLForUpsertConfig(tableConfig, schema);
validatePartialUpsertStrategies(tableConfig, schema);
+ validateConsumptionDuringUpsertRevert(tableConfig);
Preconditions.checkState(
!(PinotMd5Mode.isPinotMd5Disabled() &&
upsertConfig.getHashFunction() == HashFunction.MD5),
"Upsert hash function MD5 is disabled via '%s=true'",
CommonConstants.CONFIG_OF_PINOT_MD5_DISABLED);
@@ -1225,6 +1228,35 @@ public final class TableConfigUtils {
}
}
+ /// Rejects consuming the next segment during a download on tables that
revert upsert metadata in PROTECTED mode,
+ /// because the next segment's snapshot would run before the revert and miss
the rows it restores.
+ @VisibleForTesting
+ static void validateConsumptionDuringUpsertRevert(TableConfig tableConfig) {
+ if (tableConfig.getTableType() != TableType.REALTIME ||
!isTableTypeInconsistentDuringConsumption(tableConfig)
+ ||
ConsumingSegmentConsistencyModeListener.getInstance().getConsistencyMode()
+ != ConsumingSegmentConsistencyModeListener.Mode.PROTECTED) {
+ return;
+ }
+ IngestionConfig ingestionConfig = tableConfig.getIngestionConfig();
+ StreamIngestionConfig streamIngestionConfig =
+ ingestionConfig != null ? ingestionConfig.getStreamIngestionConfig() :
null;
+ ParallelSegmentConsumptionPolicy policy =
+ streamIngestionConfig != null ?
streamIngestionConfig.getParallelSegmentConsumptionPolicy() : null;
+ boolean pauseless = streamIngestionConfig != null &&
streamIngestionConfig.isPauselessConsumptionEnabled();
+ // ALLOW_ALWAYS and ALLOW_DURING_DOWNLOAD_ONLY both allow it, and so does
the deprecated flag when no policy is set.
+ // ALLOW_DURING_BUILD_ONLY allows it too, because a local build that fails
or mismatches the committed CRC falls
+ // back to a download after the semaphore is released. Pauseless tables
consume during build by design and skip
+ // the CRC check, so they keep it.
+ boolean consumesDuringDownload = policy != null
+ ? policy.isAllowedDuringDownload() || (policy.isAllowedDuringBuild()
&& !pauseless)
+ :
tableConfig.getUpsertConfig().isAllowPartialUpsertConsumptionDuringCommit();
+ Preconditions.checkState(!consumesDuringDownload,
+ "%s lets the next segment consume during a segment download, but
tables with partial upsert, "
+ + "dropOutOfOrderRecord or outOfOrderRecordColumn revert upsert
metadata in PROTECTED consistency mode. "
+ + "Set parallelSegmentConsumptionPolicy to DISALLOW_ALWAYS",
+ policy != null ? "parallelSegmentConsumptionPolicy " + policy :
"allowPartialUpsertConsumptionDuringCommit");
+ }
+
/// Checks if a data type is valid for time-based comparison operations
(upsert/dedup).
/// Valid types include numeric types and types with stored data as a
numeric type:
/// e.g. TIMESTAMP which is stored as LONG internally.
diff --git
a/pinot-segment-local/src/test/java/org/apache/pinot/segment/local/upsert/ConcurrentMapPartitionUpsertMetadataManagerTest.java
b/pinot-segment-local/src/test/java/org/apache/pinot/segment/local/upsert/ConcurrentMapPartitionUpsertMetadataManagerTest.java
index d76c64d34c9..1571dfc24e3 100644
---
a/pinot-segment-local/src/test/java/org/apache/pinot/segment/local/upsert/ConcurrentMapPartitionUpsertMetadataManagerTest.java
+++
b/pinot-segment-local/src/test/java/org/apache/pinot/segment/local/upsert/ConcurrentMapPartitionUpsertMetadataManagerTest.java
@@ -2598,6 +2598,28 @@ public class
ConcurrentMapPartitionUpsertMetadataManagerTest {
upsertMetadataManager.close();
}
+ @Test
+ public void testShouldRevertMetadataOnlyForConsumingSegmentsInProtectedMode()
+ throws IOException {
+ PartitionUpsertMetadataManager upsertMetadataManager = new
ConcurrentMapPartitionUpsertMetadataManager(
+ REALTIME_TABLE_NAME, 0,
+
_contextBuilder.setConsistencyMode(UpsertConfig.ConsistencyMode.NONE).setDropOutOfOrderRecord(true).build());
+ MutableSegment mutableSegment = mock(MutableSegment.class);
+ ImmutableSegmentImpl immutableSegment = mock(ImmutableSegmentImpl.class);
+ ConsumingSegmentConsistencyModeListener consistencyModeListener =
+ ConsumingSegmentConsistencyModeListener.getInstance();
+ try {
+
assertFalse(upsertMetadataManager.shouldRevertMetadataOnInconsistency(mutableSegment));
+
consistencyModeListener.setMode(ConsumingSegmentConsistencyModeListener.Mode.PROTECTED);
+
assertTrue(upsertMetadataManager.shouldRevertMetadataOnInconsistency(mutableSegment));
+
assertFalse(upsertMetadataManager.shouldRevertMetadataOnInconsistency(immutableSegment));
+ } finally {
+ consistencyModeListener.reset();
+ upsertMetadataManager.stop();
+ upsertMetadataManager.close();
+ }
+ }
+
@Test
public void testNoRevertForImmutableSegmentReplacement()
throws IOException {
diff --git
a/pinot-segment-local/src/test/java/org/apache/pinot/segment/local/utils/TableConfigUtilsTest.java
b/pinot-segment-local/src/test/java/org/apache/pinot/segment/local/utils/TableConfigUtilsTest.java
index 97a0263b5a2..ae5af3891de 100644
---
a/pinot-segment-local/src/test/java/org/apache/pinot/segment/local/utils/TableConfigUtilsTest.java
+++
b/pinot-segment-local/src/test/java/org/apache/pinot/segment/local/utils/TableConfigUtilsTest.java
@@ -64,6 +64,7 @@ import
org.apache.pinot.spi.config.table.ingestion.BatchIngestionConfig;
import org.apache.pinot.spi.config.table.ingestion.ComplexTypeConfig;
import org.apache.pinot.spi.config.table.ingestion.FilterConfig;
import org.apache.pinot.spi.config.table.ingestion.IngestionConfig;
+import
org.apache.pinot.spi.config.table.ingestion.ParallelSegmentConsumptionPolicy;
import org.apache.pinot.spi.config.table.ingestion.SourceFieldConfig;
import org.apache.pinot.spi.config.table.ingestion.StreamIngestionConfig;
import org.apache.pinot.spi.config.table.ingestion.TransformConfig;
@@ -83,6 +84,7 @@ import org.apache.pinot.spi.stream.StreamMessageDecoder;
import org.apache.pinot.spi.stream.StreamMetadataProvider;
import org.apache.pinot.spi.utils.CommonConstants;
import org.apache.pinot.spi.utils.CommonConstants.Segment.AssignmentStrategy;
+import org.apache.pinot.spi.utils.ConsumingSegmentConsistencyModeListener;
import org.apache.pinot.spi.utils.Enablement;
import org.apache.pinot.spi.utils.JsonUtils;
import org.apache.pinot.spi.utils.PinotDataType;
@@ -2682,6 +2684,92 @@ public class TableConfigUtilsTest {
}
}
+ @Test
+ public void testRejectConsumptionDuringDownloadWhenUpsertMetadataReverts() {
+ UpsertConfig partialUpsertConfig = new
UpsertConfig(UpsertConfig.Mode.PARTIAL);
+ UpsertConfig allowDuringCommitConfig = new
UpsertConfig(UpsertConfig.Mode.PARTIAL);
+ allowDuringCommitConfig.setAllowPartialUpsertConsumptionDuringCommit(true);
+ UpsertConfig dropOutOfOrderConfig = new
UpsertConfig(UpsertConfig.Mode.FULL);
+ dropOutOfOrderConfig.setDropOutOfOrderRecord(true);
+ UpsertConfig fullUpsertConfig = new UpsertConfig(UpsertConfig.Mode.FULL);
+
+ ConsumingSegmentConsistencyModeListener consistencyModeListener =
+ ConsumingSegmentConsistencyModeListener.getInstance();
+ try {
+ // Without PROTECTED mode nothing is reverted, so every policy is allowed
+ checkConsumptionDuringUpsertRevert(partialUpsertConfig,
ParallelSegmentConsumptionPolicy.ALLOW_ALWAYS, false);
+
+
consistencyModeListener.setMode(ConsumingSegmentConsistencyModeListener.Mode.PROTECTED);
+ checkConsumptionDuringUpsertRevert(partialUpsertConfig,
ParallelSegmentConsumptionPolicy.ALLOW_ALWAYS, true);
+ checkConsumptionDuringUpsertRevert(partialUpsertConfig,
+ ParallelSegmentConsumptionPolicy.ALLOW_DURING_DOWNLOAD_ONLY, true);
+ checkConsumptionDuringUpsertRevert(dropOutOfOrderConfig,
ParallelSegmentConsumptionPolicy.ALLOW_ALWAYS, true);
+ checkConsumptionDuringUpsertRevert(allowDuringCommitConfig, null, true);
+ // A local build that fails or mismatches the committed CRC falls back
to a download after releasing the semaphore
+ checkConsumptionDuringUpsertRevert(partialUpsertConfig,
ParallelSegmentConsumptionPolicy.ALLOW_DURING_BUILD_ONLY,
+ true);
+ checkConsumptionDuringUpsertRevert(partialUpsertConfig,
ParallelSegmentConsumptionPolicy.DISALLOW_ALWAYS, false);
+ checkConsumptionDuringUpsertRevert(partialUpsertConfig, null, false);
+ // An explicit policy wins over the deprecated flag, and full upsert
without out-of-order handling never reverts
+ checkConsumptionDuringUpsertRevert(allowDuringCommitConfig,
ParallelSegmentConsumptionPolicy.DISALLOW_ALWAYS,
+ false);
+ checkConsumptionDuringUpsertRevert(fullUpsertConfig,
ParallelSegmentConsumptionPolicy.ALLOW_ALWAYS, false);
+ // Pauseless tables consume during build by design, so they keep
ALLOW_DURING_BUILD_ONLY but not download overlap
+
TableConfigUtils.validateConsumptionDuringUpsertRevert(createTableConfigWithConsumptionPolicy(partialUpsertConfig,
+ ParallelSegmentConsumptionPolicy.ALLOW_DURING_BUILD_ONLY, true));
+ expectThrows(IllegalStateException.class, () ->
TableConfigUtils.validateConsumptionDuringUpsertRevert(
+ createTableConfigWithConsumptionPolicy(partialUpsertConfig,
ParallelSegmentConsumptionPolicy.ALLOW_ALWAYS,
+ true)));
+
+ // The check is part of the upsert validation
+ Schema schema = new Schema.SchemaBuilder().setSchemaName(TABLE_NAME)
+ .addSingleValueDimension("myCol", DataType.STRING)
+ .setPrimaryKeyColumns(List.of("myCol"))
+ .build();
+ TableConfig tableConfig =
+ createTableConfigWithConsumptionPolicy(dropOutOfOrderConfig,
ParallelSegmentConsumptionPolicy.ALLOW_ALWAYS);
+ IllegalStateException e = expectThrows(IllegalStateException.class,
+ () -> TableConfigUtils.validateUpsertAndDedupConfig(tableConfig,
schema));
+ assertTrue(e.getMessage().endsWith("Set parallelSegmentConsumptionPolicy
to DISALLOW_ALWAYS"), e.getMessage());
+ } finally {
+ consistencyModeListener.reset();
+ }
+ }
+
+ private void checkConsumptionDuringUpsertRevert(UpsertConfig upsertConfig,
ParallelSegmentConsumptionPolicy policy,
+ boolean expectRejected) {
+ TableConfig tableConfig =
createTableConfigWithConsumptionPolicy(upsertConfig, policy);
+ if (expectRejected) {
+ IllegalStateException e = expectThrows(IllegalStateException.class,
+ () ->
TableConfigUtils.validateConsumptionDuringUpsertRevert(tableConfig));
+ assertTrue(e.getMessage().endsWith("Set parallelSegmentConsumptionPolicy
to DISALLOW_ALWAYS"), e.getMessage());
+ assertTrue(e.getMessage().startsWith(
+ policy != null ? "parallelSegmentConsumptionPolicy " + policy :
"allowPartialUpsertConsumptionDuringCommit"),
+ e.getMessage());
+ } else {
+ TableConfigUtils.validateConsumptionDuringUpsertRevert(tableConfig);
+ }
+ }
+
+ private TableConfig createTableConfigWithConsumptionPolicy(UpsertConfig
upsertConfig,
+ ParallelSegmentConsumptionPolicy policy) {
+ return createTableConfigWithConsumptionPolicy(upsertConfig, policy, false);
+ }
+
+ private TableConfig createTableConfigWithConsumptionPolicy(UpsertConfig
upsertConfig,
+ ParallelSegmentConsumptionPolicy policy, boolean pauseless) {
+ StreamIngestionConfig streamIngestionConfig = new
StreamIngestionConfig(List.of(getStreamConfigs()));
+ streamIngestionConfig.setParallelSegmentConsumptionPolicy(policy);
+ streamIngestionConfig.setPauselessConsumptionEnabled(pauseless);
+ IngestionConfig ingestionConfig = new IngestionConfig();
+ ingestionConfig.setStreamIngestionConfig(streamIngestionConfig);
+ return new TableConfigBuilder(TableType.REALTIME).setTableName(TABLE_NAME)
+ .setUpsertConfig(upsertConfig)
+ .setRoutingConfig(STRICT_REPLICA_ROUTING_CONFIG)
+ .setIngestionConfig(ingestionConfig)
+ .build();
+ }
+
@Test
public void testValidateUpsertConfigWithMd5Disabled() {
Schema schema = new Schema.SchemaBuilder().setSchemaName(TABLE_NAME)
---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]