This is an automated email from the ASF dual-hosted git repository. Caideyipi pushed a commit to branch codex/fix-history-pipe-flush-race-pr in repository https://gitbox.apache.org/repos/asf/iotdb.git
commit 159e3dd203724db8e7e81826aec54d18165882a4 Author: Caideyipi <[email protected]> AuthorDate: Mon Nov 2 18:52:21 2026 +0800 Fix history-only pipe replication and TsFile extraction races --- .../iotdb/consensus/i18n/IoTConsensusMessages.java | 2 + .../iotdb/consensus/i18n/IoTConsensusMessages.java | 3 + .../consensus/iot/IoTConsensusServerImpl.java | 41 +++- .../consensus/iot/IoTConsensusServerImplTest.java | 137 +++++++++++++ .../apache/iotdb/db/i18n/DataNodePipeMessages.java | 2 + .../apache/iotdb/db/i18n/DataNodePipeMessages.java | 2 + .../common/tsfile/PipeTsFileInsertionEvent.java | 1 + .../source/dataregion/IoTDBDataRegionSource.java | 61 ++++++ ...istoricalDataRegionTsFileAndDeletionSource.java | 23 ++- .../pipe/event/PipeTsFileInsertionEventTest.java | 32 +++ .../db/pipe/source/IoTDBDataRegionSourceTest.java | 190 ++++++++++++++++++ ...ricalDataRegionTsFileAndDeletionSourceTest.java | 222 +++++++++++++++++++++ .../pipe/config/constant/PipeSourceConstant.java | 6 + 13 files changed, 708 insertions(+), 14 deletions(-) diff --git a/iotdb-core/consensus/src/main/i18n/en/org/apache/iotdb/consensus/i18n/IoTConsensusMessages.java b/iotdb-core/consensus/src/main/i18n/en/org/apache/iotdb/consensus/i18n/IoTConsensusMessages.java index d07373015a8..1ec6587107e 100644 --- a/iotdb-core/consensus/src/main/i18n/en/org/apache/iotdb/consensus/i18n/IoTConsensusMessages.java +++ b/iotdb-core/consensus/src/main/i18n/en/org/apache/iotdb/consensus/i18n/IoTConsensusMessages.java @@ -154,6 +154,8 @@ public final class IoTConsensusMessages { "error when waiting %s to complete SyncLog. %s"; public static final String THREAD_INTERRUPTED_WAITING_SYNC_LOG = "thread interrupted when waiting %s to complete SyncLog. %s"; + public static final String EXCEPTION_TIMED_OUT_AFTER_ARG_MS_WAITING_FOR_ARG_TO_REPLICATE_THROUGH_INDEX_ARG_CURRENT_SYNC_INDEX_ARG_11DCFE73 = + "Timed out after %s ms waiting for %s to replicate through index %s (current sync index: %s)."; // ===================== Index controller ===================== diff --git a/iotdb-core/consensus/src/main/i18n/zh/org/apache/iotdb/consensus/i18n/IoTConsensusMessages.java b/iotdb-core/consensus/src/main/i18n/zh/org/apache/iotdb/consensus/i18n/IoTConsensusMessages.java index 0d908aa8613..4da1a3cab16 100644 --- a/iotdb-core/consensus/src/main/i18n/zh/org/apache/iotdb/consensus/i18n/IoTConsensusMessages.java +++ b/iotdb-core/consensus/src/main/i18n/zh/org/apache/iotdb/consensus/i18n/IoTConsensusMessages.java @@ -154,6 +154,9 @@ public final class IoTConsensusMessages { public static final String THREAD_INTERRUPTED_WAITING_SYNC_LOG = "等待 %s 完成 SyncLog 时线程被中断。%s"; + public static final String EXCEPTION_TIMED_OUT_AFTER_ARG_MS_WAITING_FOR_ARG_TO_REPLICATE_THROUGH_INDEX_ARG_CURRENT_SYNC_INDEX_ARG_11DCFE73 = + "等待 %s ms 后,%s 仍未复制到 index %s(当前同步 index:%s)。"; + // ===================== Index 控制器 ===================== public static final String UPDATE_INDEX = diff --git a/iotdb-core/consensus/src/main/java/org/apache/iotdb/consensus/iot/IoTConsensusServerImpl.java b/iotdb-core/consensus/src/main/java/org/apache/iotdb/consensus/iot/IoTConsensusServerImpl.java index df7126cde4f..b29e75ebd35 100644 --- a/iotdb-core/consensus/src/main/java/org/apache/iotdb/consensus/iot/IoTConsensusServerImpl.java +++ b/iotdb-core/consensus/src/main/java/org/apache/iotdb/consensus/iot/IoTConsensusServerImpl.java @@ -866,27 +866,58 @@ public class IoTConsensusServerImpl { public void waitTargetPeerUntilSyncLogCompleted(Peer targetPeer) throws ConsensusGroupModifyPeerException { - long checkIntervalInMs = 10_000L; + waitTargetPeerUntilSyncLogCompleted(targetPeer, Long.MAX_VALUE, 10_000L, false); + } + + /** Wait for writes accepted before the first check, without chasing subsequent writes. */ + public void waitTargetPeerUntilSyncLogCompleted(Peer targetPeer, long timeoutMs) + throws ConsensusGroupModifyPeerException { + waitTargetPeerUntilSyncLogCompleted(targetPeer, timeoutMs, 1000L, true); + } + + private void waitTargetPeerUntilSyncLogCompleted( + Peer targetPeer, long timeoutMs, long checkIntervalInMs, boolean useFixedBoundary) + throws ConsensusGroupModifyPeerException { + final long startNanos = System.nanoTime(); + final long timeoutNanos = TimeUnit.MILLISECONDS.toNanos(timeoutMs); try (SyncIoTConsensusServiceClient client = syncClientManager.borrowClient(targetPeer.getEndpoint())) { + long targetSearchIndex = -1; while (true) { TWaitSyncLogCompleteRes res = client.waitSyncLogComplete( new TWaitSyncLogCompleteReq(targetPeer.getGroupId().convertToTConsensusGroupId())); - if (res.complete) { + // Capture a finite replication boundary even if the writer continues accepting data. + if (targetSearchIndex < 0) { + targetSearchIndex = res.searchIndex; + } + final long currentTargetIndex = useFixedBoundary ? targetSearchIndex : res.searchIndex; + if (useFixedBoundary ? res.safeIndex >= targetSearchIndex : res.complete) { logger.info( IoTConsensusMessages.WAIT_SYNC_LOG_COMPLETED, targetPeer, - res.searchIndex, + currentTargetIndex, res.safeIndex); return; } logger.info( IoTConsensusMessages.WAIT_SYNC_LOG_IN_PROGRESS, targetPeer, - res.searchIndex, + currentTargetIndex, res.safeIndex); - Thread.sleep(checkIntervalInMs); + final long remainingNanos = timeoutNanos - (System.nanoTime() - startNanos); + if (remainingNanos <= 0) { + throw new ConsensusGroupModifyPeerException( + String.format( + IoTConsensusMessages + .EXCEPTION_TIMED_OUT_AFTER_ARG_MS_WAITING_FOR_ARG_TO_REPLICATE_THROUGH_INDEX_ARG_CURRENT_SYNC_INDEX_ARG_11DCFE73, + timeoutMs, + targetPeer, + targetSearchIndex, + res.safeIndex)); + } + TimeUnit.NANOSECONDS.sleep( + Math.min(TimeUnit.MILLISECONDS.toNanos(checkIntervalInMs), remainingNanos)); } } catch (ClientManagerException | TException e) { throw new ConsensusGroupModifyPeerException( diff --git a/iotdb-core/consensus/src/test/java/org/apache/iotdb/consensus/iot/IoTConsensusServerImplTest.java b/iotdb-core/consensus/src/test/java/org/apache/iotdb/consensus/iot/IoTConsensusServerImplTest.java index 291a8d99d1b..7d23b17d888 100644 --- a/iotdb-core/consensus/src/test/java/org/apache/iotdb/consensus/iot/IoTConsensusServerImplTest.java +++ b/iotdb-core/consensus/src/test/java/org/apache/iotdb/consensus/iot/IoTConsensusServerImplTest.java @@ -22,6 +22,7 @@ package org.apache.iotdb.consensus.iot; import org.apache.iotdb.common.rpc.thrift.TEndPoint; import org.apache.iotdb.common.rpc.thrift.TSStatus; import org.apache.iotdb.commons.audit.UserDataTransferAuditHandler; +import org.apache.iotdb.commons.client.IClientManager; import org.apache.iotdb.commons.consensus.ConsensusGroupId; import org.apache.iotdb.commons.consensus.DataRegionId; import org.apache.iotdb.commons.disk.strategy.DirectoryStrategyType; @@ -31,7 +32,11 @@ import org.apache.iotdb.consensus.common.request.DeserializedBatchIndexedConsens import org.apache.iotdb.consensus.common.request.IndexedConsensusRequest; import org.apache.iotdb.consensus.config.IoTConsensusConfig; import org.apache.iotdb.consensus.config.UserDataTransferAuditClassifier; +import org.apache.iotdb.consensus.exception.ConsensusGroupModifyPeerException; import org.apache.iotdb.consensus.i18n.ConsensusMessages; +import org.apache.iotdb.consensus.iot.client.SyncIoTConsensusServiceClient; +import org.apache.iotdb.consensus.iot.thrift.TWaitSyncLogCompleteReq; +import org.apache.iotdb.consensus.iot.thrift.TWaitSyncLogCompleteRes; import org.apache.iotdb.consensus.iot.util.TestEntry; import org.apache.iotdb.consensus.iot.util.TestStateMachine; import org.apache.iotdb.rpc.TSStatusCode; @@ -39,6 +44,7 @@ import org.apache.iotdb.rpc.TSStatusCode; import org.junit.Rule; import org.junit.Test; import org.junit.rules.TemporaryFolder; +import org.mockito.Mockito; import java.lang.reflect.Field; import java.util.Collections; @@ -57,6 +63,7 @@ import java.util.concurrent.atomic.AtomicReference; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertThrows; import static org.junit.Assert.assertTrue; public class IoTConsensusServerImplTest { @@ -67,6 +74,136 @@ public class IoTConsensusServerImplTest { @Rule public final TemporaryFolder temporaryFolder = new TemporaryFolder(); + @Test(timeout = 5000) + @SuppressWarnings("unchecked") + public void testReplicationWaitUsesFixedBoundaryWhileWriterContinues() throws Exception { + final ScheduledExecutorService backgroundTaskService = + Executors.newSingleThreadScheduledExecutor(); + final Peer thisNode = new Peer(new DataRegionId(1), 0, new TEndPoint("127.0.0.1", 6667)); + final Peer remotePeer = new Peer(new DataRegionId(1), 1, new TEndPoint("127.0.0.1", 6668)); + final IClientManager<TEndPoint, SyncIoTConsensusServiceClient> manager = + Mockito.mock(IClientManager.class); + final SyncIoTConsensusServiceClient client = Mockito.mock(SyncIoTConsensusServiceClient.class); + Mockito.when(manager.borrowClient(remotePeer.getEndpoint())).thenReturn(client); + Mockito.when(client.waitSyncLogComplete(Mockito.any(TWaitSyncLogCompleteReq.class))) + .thenReturn(new TWaitSyncLogCompleteRes(false, 10, 0)) + // The snapshot is replicated although newer writes have not caught up yet. + .thenReturn(new TWaitSyncLogCompleteRes(false, 11, 10)) + .thenThrow(new AssertionError("replication boundary moved with the writer")); + try { + final IoTConsensusServerImpl server = + new IoTConsensusServerImpl( + temporaryFolder.getRoot().getAbsolutePath(), + null, + DirectoryStrategyType.SEQUENCE_STRATEGY, + thisNode, + Collections.singletonList(thisNode), + new TestStateMachine(), + backgroundTaskService, + null, + manager, + IoTConsensusConfig.newBuilder().build()); + server.waitTargetPeerUntilSyncLogCompleted(remotePeer, 3000); + Mockito.verify(client, Mockito.times(2)) + .waitSyncLogComplete(Mockito.any(TWaitSyncLogCompleteReq.class)); + Mockito.verify(client).close(); + } finally { + backgroundTaskService.shutdownNow(); + } + } + + @Test(timeout = 5000) + public void testReplicationWaitTimesOutAndReleasesClient() throws Exception { + assertReplicationWaitFailure(false); + } + + @Test(timeout = 30000) + @SuppressWarnings("unchecked") + public void testPeerRemovalWaitStillRequiresReplicationComplete() throws Exception { + final ScheduledExecutorService backgroundTaskService = + Executors.newSingleThreadScheduledExecutor(); + final Peer thisNode = new Peer(new DataRegionId(1), 0, new TEndPoint("127.0.0.1", 6667)); + final Peer remotePeer = new Peer(new DataRegionId(1), 1, new TEndPoint("127.0.0.1", 6668)); + final IClientManager<TEndPoint, SyncIoTConsensusServiceClient> manager = + Mockito.mock(IClientManager.class); + final SyncIoTConsensusServiceClient client = Mockito.mock(SyncIoTConsensusServiceClient.class); + Mockito.when(manager.borrowClient(remotePeer.getEndpoint())).thenReturn(client); + Mockito.when(client.waitSyncLogComplete(Mockito.any(TWaitSyncLogCompleteReq.class))) + .thenReturn(new TWaitSyncLogCompleteRes(false, 10, 0)) + .thenReturn(new TWaitSyncLogCompleteRes(false, 11, 10)) + .thenReturn(new TWaitSyncLogCompleteRes(true, 11, 11)); + try { + final IoTConsensusServerImpl server = + new IoTConsensusServerImpl( + temporaryFolder.getRoot().getAbsolutePath(), + null, + DirectoryStrategyType.SEQUENCE_STRATEGY, + thisNode, + Collections.singletonList(thisNode), + new TestStateMachine(), + backgroundTaskService, + null, + manager, + IoTConsensusConfig.newBuilder().build()); + server.waitTargetPeerUntilSyncLogCompleted(remotePeer); + Mockito.verify(client, Mockito.times(3)) + .waitSyncLogComplete(Mockito.any(TWaitSyncLogCompleteReq.class)); + Mockito.verify(client).close(); + } finally { + backgroundTaskService.shutdownNow(); + } + } + + @Test(timeout = 5000) + public void testReplicationWaitPreservesInterruptAndReleasesClient() throws Exception { + assertReplicationWaitFailure(true); + } + + @SuppressWarnings("unchecked") + private void assertReplicationWaitFailure(final boolean interrupt) throws Exception { + final ScheduledExecutorService backgroundTaskService = + Executors.newSingleThreadScheduledExecutor(); + final Peer thisNode = new Peer(new DataRegionId(1), 0, new TEndPoint("127.0.0.1", 6667)); + final Peer remotePeer = new Peer(new DataRegionId(1), 1, new TEndPoint("127.0.0.1", 6668)); + final IClientManager<TEndPoint, SyncIoTConsensusServiceClient> manager = + Mockito.mock(IClientManager.class); + final SyncIoTConsensusServiceClient client = Mockito.mock(SyncIoTConsensusServiceClient.class); + Mockito.when(manager.borrowClient(remotePeer.getEndpoint())).thenReturn(client); + Mockito.when(client.waitSyncLogComplete(Mockito.any(TWaitSyncLogCompleteReq.class))) + .thenReturn(new TWaitSyncLogCompleteRes(false, 10, 0)); + try { + final IoTConsensusServerImpl server = + new IoTConsensusServerImpl( + temporaryFolder.getRoot().getAbsolutePath(), + null, + DirectoryStrategyType.SEQUENCE_STRATEGY, + thisNode, + Collections.singletonList(thisNode), + new TestStateMachine(), + backgroundTaskService, + null, + manager, + IoTConsensusConfig.newBuilder().build()); + if (interrupt) { + Thread.currentThread().interrupt(); + } + final ConsensusGroupModifyPeerException exception = + assertThrows( + ConsensusGroupModifyPeerException.class, + () -> server.waitTargetPeerUntilSyncLogCompleted(remotePeer, interrupt ? 3000 : 0)); + assertEquals(interrupt, Thread.currentThread().isInterrupted()); + if (interrupt) { + assertTrue(exception.getCause() instanceof InterruptedException); + } else { + assertTrue(exception.getMessage().contains(remotePeer.toString())); + } + Mockito.verify(client).close(); + } finally { + Thread.interrupted(); + backgroundTaskService.shutdownNow(); + } + } + @Test public void testSyncLogBecomesReadOnlyWhileWaiting() throws Exception { final ScheduledExecutorService backgroundTaskService = diff --git a/iotdb-core/datanode/src/main/i18n/en/org/apache/iotdb/db/i18n/DataNodePipeMessages.java b/iotdb-core/datanode/src/main/i18n/en/org/apache/iotdb/db/i18n/DataNodePipeMessages.java index f56589e52c0..078333ad7d2 100644 --- a/iotdb-core/datanode/src/main/i18n/en/org/apache/iotdb/db/i18n/DataNodePipeMessages.java +++ b/iotdb-core/datanode/src/main/i18n/en/org/apache/iotdb/db/i18n/DataNodePipeMessages.java @@ -593,6 +593,8 @@ public final class DataNodePipeMessages { public static final String FAILED_TO_LOAD_SNAPSHOT_FROM_BYTEBUFFER = "Failed to load snapshot from byteBuffer {}."; public static final String FAILED_TO_START_SOURCES = "failed to start sources."; + public static final String EXCEPTION_HISTORY_WAIT_FOR_REPLICATION_TIMEOUT_MS_MUST_BE_GREATER_THAN_0_4A4E80CB = + "history.wait-for-replication.timeout-ms must be greater than 0."; public static final String HEARTBEAT_EVENT_CAN_NOT_BE_SUPPLIED_BECAUSE = "Heartbeat Event {} can not be supplied because the reference count can not be increased"; public static final String EVENT_CAN_NOT_BE_SUPPLIED_BECAUSE_DATA_IS_LOST = diff --git a/iotdb-core/datanode/src/main/i18n/zh/org/apache/iotdb/db/i18n/DataNodePipeMessages.java b/iotdb-core/datanode/src/main/i18n/zh/org/apache/iotdb/db/i18n/DataNodePipeMessages.java index 1b8b488f30f..a7e4e69d95a 100644 --- a/iotdb-core/datanode/src/main/i18n/zh/org/apache/iotdb/db/i18n/DataNodePipeMessages.java +++ b/iotdb-core/datanode/src/main/i18n/zh/org/apache/iotdb/db/i18n/DataNodePipeMessages.java @@ -564,6 +564,8 @@ public final class DataNodePipeMessages { public static final String FAILED_TO_LOAD_SNAPSHOT_FROM_BYTEBUFFER = "加载 snapshot from byteBuffer {} 失败。"; public static final String FAILED_TO_START_SOURCES = "启动 sources 失败。"; + public static final String EXCEPTION_HISTORY_WAIT_FOR_REPLICATION_TIMEOUT_MS_MUST_BE_GREATER_THAN_0_4A4E80CB = + "history.wait-for-replication.timeout-ms 必须大于 0。"; public static final String HEARTBEAT_EVENT_CAN_NOT_BE_SUPPLIED_BECAUSE = "Heartbeat Event {} 无法被提供,因为其引用计数无法增加"; public static final String EVENT_CAN_NOT_BE_SUPPLIED_BECAUSE_DATA_IS_LOST = diff --git a/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/pipe/event/common/tsfile/PipeTsFileInsertionEvent.java b/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/pipe/event/common/tsfile/PipeTsFileInsertionEvent.java index 5d06cf577b4..92674ee8afe 100644 --- a/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/pipe/event/common/tsfile/PipeTsFileInsertionEvent.java +++ b/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/pipe/event/common/tsfile/PipeTsFileInsertionEvent.java @@ -967,6 +967,7 @@ public class PipeTsFileInsertionEvent extends PipeInsertionEvent public boolean shouldParseTime() { if (!isTimeParsed && Objects.nonNull(resource) + && (!isGeneratedByHistoricalExtractor || resource.isClosed()) && startTime <= resource.getFileStartTime() && resource.getFileEndTime() <= endTime) { isTimeParsed = true; diff --git a/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/pipe/source/dataregion/IoTDBDataRegionSource.java b/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/pipe/source/dataregion/IoTDBDataRegionSource.java index 9940127488a..02b37fca080 100644 --- a/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/pipe/source/dataregion/IoTDBDataRegionSource.java +++ b/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/pipe/source/dataregion/IoTDBDataRegionSource.java @@ -27,6 +27,12 @@ import org.apache.iotdb.commons.pipe.datastructure.pattern.IoTDBTreePatternOpera import org.apache.iotdb.commons.pipe.datastructure.pattern.TreePattern; import org.apache.iotdb.commons.pipe.source.IoTDBSource; import org.apache.iotdb.commons.queryengine.common.SqlDialect; +import org.apache.iotdb.consensus.IConsensus; +import org.apache.iotdb.consensus.common.Peer; +import org.apache.iotdb.consensus.exception.ConsensusGroupNotExistException; +import org.apache.iotdb.consensus.iot.IoTConsensus; +import org.apache.iotdb.consensus.iot.IoTConsensusServerImpl; +import org.apache.iotdb.db.consensus.DataRegionConsensusImpl; import org.apache.iotdb.db.i18n.DataNodePipeMessages; import org.apache.iotdb.db.pipe.event.common.heartbeat.PipeHeartbeatEvent; import org.apache.iotdb.db.pipe.metric.overview.PipeDataNodeSinglePipeMetrics; @@ -63,6 +69,7 @@ import javax.annotation.Nonnull; import java.time.ZoneId; import java.util.Arrays; import java.util.Objects; +import java.util.concurrent.TimeUnit; import java.util.concurrent.atomic.AtomicBoolean; import java.util.concurrent.atomic.AtomicReference; @@ -75,6 +82,8 @@ import static org.apache.iotdb.commons.pipe.config.constant.PipeSourceConstant.E import static org.apache.iotdb.commons.pipe.config.constant.PipeSourceConstant.EXTRACTOR_HISTORY_LOOSE_RANGE_KEY; import static org.apache.iotdb.commons.pipe.config.constant.PipeSourceConstant.EXTRACTOR_HISTORY_START_TIME_KEY; import static org.apache.iotdb.commons.pipe.config.constant.PipeSourceConstant.EXTRACTOR_HISTORY_TSFILE_ORDER_BY_QUERY_PRIORITY_KEY; +import static org.apache.iotdb.commons.pipe.config.constant.PipeSourceConstant.EXTRACTOR_HISTORY_WAIT_FOR_REPLICATION_TIMEOUT_MS_DEFAULT_VALUE; +import static org.apache.iotdb.commons.pipe.config.constant.PipeSourceConstant.EXTRACTOR_HISTORY_WAIT_FOR_REPLICATION_TIMEOUT_MS_KEY; import static org.apache.iotdb.commons.pipe.config.constant.PipeSourceConstant.EXTRACTOR_MODE_KEY; import static org.apache.iotdb.commons.pipe.config.constant.PipeSourceConstant.EXTRACTOR_MODE_SNAPSHOT_KEY; import static org.apache.iotdb.commons.pipe.config.constant.PipeSourceConstant.EXTRACTOR_MODE_STREAMING_DEFAULT_VALUE; @@ -111,6 +120,7 @@ import static org.apache.iotdb.commons.pipe.config.constant.PipeSourceConstant.S import static org.apache.iotdb.commons.pipe.config.constant.PipeSourceConstant.SOURCE_HISTORY_LOOSE_RANGE_KEY; import static org.apache.iotdb.commons.pipe.config.constant.PipeSourceConstant.SOURCE_HISTORY_START_TIME_KEY; import static org.apache.iotdb.commons.pipe.config.constant.PipeSourceConstant.SOURCE_HISTORY_TSFILE_ORDER_BY_QUERY_PRIORITY_KEY; +import static org.apache.iotdb.commons.pipe.config.constant.PipeSourceConstant.SOURCE_HISTORY_WAIT_FOR_REPLICATION_TIMEOUT_MS_KEY; import static org.apache.iotdb.commons.pipe.config.constant.PipeSourceConstant.SOURCE_MODE_KEY; import static org.apache.iotdb.commons.pipe.config.constant.PipeSourceConstant.SOURCE_MODE_SNAPSHOT_KEY; import static org.apache.iotdb.commons.pipe.config.constant.PipeSourceConstant.SOURCE_MODE_STREAMING_KEY; @@ -142,6 +152,9 @@ public class IoTDBDataRegionSource extends IoTDBSource { private boolean hasNoExtractionNeed = true; private boolean shouldExtractDeletion = false; + private boolean shouldWaitForHistoricalReplication = false; + private long historicalReplicationTimeoutMs = + EXTRACTOR_HISTORY_WAIT_FOR_REPLICATION_TIMEOUT_MS_DEFAULT_VALUE; private final AtomicBoolean completionSourceStarted = new AtomicBoolean(false); @Override @@ -253,6 +266,29 @@ public class IoTDBDataRegionSource extends IoTDBSource { constructHistoricalSource(); constructRealtimeSource(validator.getParameters()); + shouldWaitForHistoricalReplication = + realtimeSource instanceof PipeRealtimeDataRegionHeartbeatSource + && validator + .getParameters() + .getBooleanOrDefault( + Arrays.asList(EXTRACTOR_HISTORY_ENABLE_KEY, SOURCE_HISTORY_ENABLE_KEY), + EXTRACTOR_HISTORY_ENABLE_DEFAULT_VALUE); + if (shouldWaitForHistoricalReplication) { + historicalReplicationTimeoutMs = + validator + .getParameters() + .getLongOrDefault( + Arrays.asList( + EXTRACTOR_HISTORY_WAIT_FOR_REPLICATION_TIMEOUT_MS_KEY, + SOURCE_HISTORY_WAIT_FOR_REPLICATION_TIMEOUT_MS_KEY), + EXTRACTOR_HISTORY_WAIT_FOR_REPLICATION_TIMEOUT_MS_DEFAULT_VALUE); + validator.validate( + timeout -> (long) timeout > 0, + DataNodePipeMessages + .EXCEPTION_HISTORY_WAIT_FOR_REPLICATION_TIMEOUT_MS_MUST_BE_GREATER_THAN_0_4A4E80CB, + historicalReplicationTimeoutMs); + } + historicalSource.validate(validator); realtimeSource.validate(validator); } @@ -531,6 +567,31 @@ public class IoTDBDataRegionSource extends IoTDBSource { return; } + // A history-only source cannot recover writes that reach this replica after its scan. + // Wait outside the DataRegion write lock so consensus replication can apply those writes. + if (shouldWaitForHistoricalReplication) { + final IConsensus consensus = DataRegionConsensusImpl.getInstance(); + if (consensus instanceof IoTConsensus) { + final DataRegionId dataRegionId = new DataRegionId(regionId); + final IoTConsensusServerImpl impl = ((IoTConsensus) consensus).getImpl(dataRegionId); + if (impl == null) { + throw new ConsensusGroupNotExistException(dataRegionId); + } + final long replicationStartNanos = System.nanoTime(); + for (final Peer peer : impl.getConfiguration()) { + if (!peer.equals(impl.getThisNode())) { + impl.waitTargetPeerUntilSyncLogCompleted( + peer, + Math.max( + 0, + historicalReplicationTimeoutMs + - TimeUnit.NANOSECONDS.toMillis( + System.nanoTime() - replicationStartNanos))); + } + } + } + } + final long startTime = System.currentTimeMillis(); LOGGER.info( DataNodePipeMessages.PIPE_STARTING_HISTORICAL_SOURCE_AND_REALTIME_SOURCE, diff --git a/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/pipe/source/dataregion/historical/PipeHistoricalDataRegionTsFileAndDeletionSource.java b/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/pipe/source/dataregion/historical/PipeHistoricalDataRegionTsFileAndDeletionSource.java index 80730d8f156..fed95151942 100644 --- a/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/pipe/source/dataregion/historical/PipeHistoricalDataRegionTsFileAndDeletionSource.java +++ b/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/pipe/source/dataregion/historical/PipeHistoricalDataRegionTsFileAndDeletionSource.java @@ -68,6 +68,7 @@ import org.apache.iotdb.pipe.api.customizer.configuration.PipeRuntimeEnvironment import org.apache.iotdb.pipe.api.customizer.parameter.PipeParameterValidator; import org.apache.iotdb.pipe.api.customizer.parameter.PipeParameters; import org.apache.iotdb.pipe.api.event.Event; +import org.apache.iotdb.pipe.api.exception.PipeException; import org.apache.iotdb.pipe.api.exception.PipeParameterNotValidException; import org.apache.tsfile.file.metadata.IDeviceID; @@ -868,8 +869,8 @@ public class PipeHistoricalDataRegionTsFileAndDeletionSource return false; } - // Some resource may not be closed due to the control of PIPE_MIN_FLUSH_INTERVAL_IN_MS. We - // simply ignore them. + // Closing files may still have incomplete progress and time metadata. Select them + // conservatively; the event waits for the file to close before parsing or transfer. if (!resource.isClosed() && Optional.ofNullable(resource.getProcessor()) .map(TsFileProcessor::alreadyMarkedClosing) @@ -1057,7 +1058,10 @@ public class PipeHistoricalDataRegionTsFileAndDeletionSource } private boolean isTsFileResourceCoveredByTimeRange(final TsFileResource resource) { - return historicalDataExtractionStartTime <= resource.getFileStartTime() + // In particular, a sequence file's end time is not final until it is closed. Do not disable + // row-level filtering based on the bounds observed while an asynchronous close is in flight. + return resource.isClosed() + && historicalDataExtractionStartTime <= resource.getFileStartTime() && historicalDataExtractionEndTime >= resource.getFileEndTime(); } @@ -1318,16 +1322,17 @@ public class PipeHistoricalDataRegionTsFileAndDeletionSource event.increaseReferenceCount( PipeHistoricalDataRegionTsFileAndDeletionSource.class.getName()); if (!isReferenceCountIncreased) { - LOGGER.warn( - DataNodePipeMessages.PIPE_FAILED_TO_INCREASE_REFERENCE_COUNT_FOR_1, - pipeName, - dataRegionId, - event); + // Keep the queue entry, pin, and replicate index until an event owns the resource. A + // failed reference acquisition must not advance historical progress past this file. + throw new PipeException( + String.format( + DataNodePipeMessages.INCREASE_REFERENCE_COUNT_ERROR_HOLDER_FMT, + event.coreReportMessage())); } filteredTsFileResources2TableNames.remove(resource); shouldUnpinResource = true; shouldClearReplicateIndex = true; - return isReferenceCountIncreased ? event : null; + return event; } finally { if (shouldClearReplicateIndex) { clearReplicateIndexForResource(resource); diff --git a/iotdb-core/datanode/src/test/java/org/apache/iotdb/db/pipe/event/PipeTsFileInsertionEventTest.java b/iotdb-core/datanode/src/test/java/org/apache/iotdb/db/pipe/event/PipeTsFileInsertionEventTest.java index e905e201ec2..b1a88bf7506 100644 --- a/iotdb-core/datanode/src/test/java/org/apache/iotdb/db/pipe/event/PipeTsFileInsertionEventTest.java +++ b/iotdb-core/datanode/src/test/java/org/apache/iotdb/db/pipe/event/PipeTsFileInsertionEventTest.java @@ -78,6 +78,38 @@ import static org.apache.iotdb.commons.pipe.datastructure.pattern.TreePattern.bu import static org.apache.iotdb.db.auth.AuthorityChecker.NO_PERMISSION_PROMOTION; public class PipeTsFileInsertionEventTest { + @Test + public void testRealtimeTsFileTimeParsingRemainsUnchanged() { + final TsFileResource resource = new TsFileResource(new File("1-1-0-0.tsfile")); + final IDeviceID device = IDeviceID.Factory.DEFAULT_FACTORY.create(new String[] {"t1", "d1"}); + resource.updateStartTime(device, 0); + resource.updateEndTime(device, Long.MIN_VALUE); + resource.setStatusForTest(TsFileResourceStatus.UNCLOSED); + final PipeTsFileInsertionEvent event = + new PipeTsFileInsertionEvent( + true, + "db1", + resource, + null, + false, + false, + false, + Collections.singleton("t1"), + "realtime_time_filter", + 1L, + null, + null, + new TablePattern(true, null, null), + null, + null, + null, + true, + -10, + 10); + Assert.assertFalse(event.shouldParseTime()); + event.close(); + } + @Test public void testHistoricalModsSurviveOriginalFileRemoval() throws Exception { assertHistoricalModsAfterOriginalFileRemoval(true, true); diff --git a/iotdb-core/datanode/src/test/java/org/apache/iotdb/db/pipe/source/IoTDBDataRegionSourceTest.java b/iotdb-core/datanode/src/test/java/org/apache/iotdb/db/pipe/source/IoTDBDataRegionSourceTest.java index f660423b0b0..b90dc4abfe4 100644 --- a/iotdb-core/datanode/src/test/java/org/apache/iotdb/db/pipe/source/IoTDBDataRegionSourceTest.java +++ b/iotdb-core/datanode/src/test/java/org/apache/iotdb/db/pipe/source/IoTDBDataRegionSourceTest.java @@ -19,18 +19,208 @@ package org.apache.iotdb.db.pipe.source; +import org.apache.iotdb.common.rpc.thrift.TEndPoint; +import org.apache.iotdb.commons.consensus.DataRegionId; import org.apache.iotdb.commons.pipe.config.constant.PipeSourceConstant; +import org.apache.iotdb.commons.pipe.source.IoTDBSource; +import org.apache.iotdb.consensus.IConsensus; +import org.apache.iotdb.consensus.common.Peer; +import org.apache.iotdb.consensus.exception.ConsensusGroupModifyPeerException; +import org.apache.iotdb.consensus.iot.IoTConsensus; +import org.apache.iotdb.consensus.iot.IoTConsensusServerImpl; +import org.apache.iotdb.db.consensus.DataRegionConsensusImpl; import org.apache.iotdb.db.pipe.source.dataregion.IoTDBDataRegionSource; +import org.apache.iotdb.db.pipe.source.dataregion.historical.PipeHistoricalDataRegionSource; +import org.apache.iotdb.db.pipe.source.dataregion.realtime.PipeRealtimeDataRegionHeartbeatSource; +import org.apache.iotdb.db.pipe.source.dataregion.realtime.PipeRealtimeDataRegionSource; +import org.apache.iotdb.db.storageengine.StorageEngine; +import org.apache.iotdb.db.storageengine.dataregion.DataRegion; import org.apache.iotdb.pipe.api.customizer.parameter.PipeParameterValidator; import org.apache.iotdb.pipe.api.customizer.parameter.PipeParameters; import org.apache.iotdb.pipe.api.exception.PipeParameterNotValidException; import org.junit.Assert; import org.junit.Test; +import org.mockito.InOrder; +import org.mockito.Mockito; +import java.lang.reflect.Field; +import java.util.Arrays; import java.util.HashMap; +import java.util.Map; +import java.util.concurrent.atomic.AtomicBoolean; +import java.util.concurrent.atomic.AtomicInteger; public class IoTDBDataRegionSourceTest { + + @Test + public void testHistoryOnlyWaitsForReplicationBeforeLockingAndScanning() throws Exception { + assertReplicationBeforeHistoricalStart(false, false); + } + + @Test + public void testHistoryOnlyCanRetryAfterReplicationBarrierFailure() throws Exception { + assertReplicationBeforeHistoricalStart(false, true); + } + + @Test + public void testRealtimeSourceDoesNotWaitForReplicationBarrier() throws Exception { + for (final boolean historyEnabled : new boolean[] {false, true}) { + for (final String mode : new String[] {"hybrid", "log", "file", "forced-log"}) { + final Map<String, String> attributes = new HashMap<>(); + attributes.put( + PipeSourceConstant.SOURCE_HISTORY_ENABLE_KEY, String.valueOf(historyEnabled)); + attributes.put(PipeSourceConstant.SOURCE_REALTIME_ENABLE_KEY, "true"); + attributes.put(PipeSourceConstant.SOURCE_REALTIME_MODE_KEY, mode); + // This option belongs to history-only sources and must not affect realtime sources. + attributes.put( + PipeSourceConstant.SOURCE_HISTORY_WAIT_FOR_REPLICATION_TIMEOUT_MS_KEY, "unused"); + assertReplicationBeforeHistoricalStart(attributes, false, 30_000); + } + } + } + + @Test + public void testSnapshotSourceWaitsForReplicationBarrier() throws Exception { + final Map<String, String> attributes = new HashMap<>(); + attributes.put(PipeSourceConstant.SOURCE_MODE_KEY, "snapshot"); + assertReplicationBeforeHistoricalStart(attributes, false, 30_000); + } + + @Test + public void testHistoryOnlyReplicationTimeoutIsConfigurable() throws Exception { + for (final String key : + new String[] { + PipeSourceConstant.SOURCE_HISTORY_WAIT_FOR_REPLICATION_TIMEOUT_MS_KEY, + PipeSourceConstant.EXTRACTOR_HISTORY_WAIT_FOR_REPLICATION_TIMEOUT_MS_KEY, + "history.wait-for-replication.timeout-ms" + }) { + final Map<String, String> attributes = new HashMap<>(); + attributes.put(PipeSourceConstant.SOURCE_REALTIME_ENABLE_KEY, "false"); + attributes.put(key, "90000"); + assertReplicationBeforeHistoricalStart(attributes, false, 90_000); + } + } + + @Test + public void testHistoryOnlyRejectsNonPositiveReplicationTimeout() throws Exception { + for (final String value : new String[] {"0", "-1"}) { + final Map<String, String> attributes = new HashMap<>(); + attributes.put(PipeSourceConstant.SOURCE_REALTIME_ENABLE_KEY, "false"); + attributes.put(PipeSourceConstant.SOURCE_HISTORY_WAIT_FOR_REPLICATION_TIMEOUT_MS_KEY, value); + try (final IoTDBDataRegionSource source = new IoTDBDataRegionSource()) { + Assert.assertThrows( + PipeParameterNotValidException.class, + () -> source.validate(new PipeParameterValidator(new PipeParameters(attributes)))); + } + } + } + + private void assertReplicationBeforeHistoricalStart( + final boolean realtimeEnabled, final boolean failFirst) throws Exception { + final Map<String, String> attributes = new HashMap<>(); + attributes.put(PipeSourceConstant.SOURCE_REALTIME_ENABLE_KEY, String.valueOf(realtimeEnabled)); + assertReplicationBeforeHistoricalStart(attributes, failFirst, 30_000); + } + + @SuppressWarnings("unchecked") + private void assertReplicationBeforeHistoricalStart( + final Map<String, String> attributes, final boolean failFirst, final long expectedTimeoutMs) + throws Exception { + final IoTDBDataRegionSource source = new IoTDBDataRegionSource(); + source.validate(new PipeParameterValidator(new PipeParameters(attributes))); + final Field realtimeSourceField = + IoTDBDataRegionSource.class.getDeclaredField("realtimeSource"); + realtimeSourceField.setAccessible(true); + final PipeRealtimeDataRegionSource actualRealtimeSource = + (PipeRealtimeDataRegionSource) realtimeSourceField.get(source); + final boolean realtimeEnabled = + !(actualRealtimeSource instanceof PipeRealtimeDataRegionHeartbeatSource); + final IConsensus originalConsensus = DataRegionConsensusImpl.getInstance(); + final IoTConsensus consensus = Mockito.mock(IoTConsensus.class); + final IoTConsensusServerImpl impl = Mockito.mock(IoTConsensusServerImpl.class); + final DataRegionId regionId = new DataRegionId(2147483644); + final Peer localPeer = new Peer(regionId, 1, new TEndPoint("127.0.0.1", 6667)); + final Peer remotePeer = new Peer(regionId, 2, new TEndPoint("127.0.0.1", 6668)); + Mockito.when(consensus.getImpl(regionId)).thenReturn(impl); + Mockito.when(impl.getConfiguration()).thenReturn(Arrays.asList(localPeer, remotePeer)); + Mockito.when(impl.getThisNode()).thenReturn(localPeer); + final DataRegion region = Mockito.mock(DataRegion.class); + final PipeHistoricalDataRegionSource historical = + Mockito.mock(PipeHistoricalDataRegionSource.class); + final PipeRealtimeDataRegionSource realtime = Mockito.mock(actualRealtimeSource.getClass()); + setField(IoTDBDataRegionSource.class, source, "historicalSource", historical); + setField(IoTDBDataRegionSource.class, source, "realtimeSource", realtime); + setField(IoTDBSource.class, source, "regionId", regionId.getId()); + setField(IoTDBSource.class, source, "pipeName", "historical_replication_barrier"); + final AtomicBoolean replicated = new AtomicBoolean(); + final AtomicInteger attempts = new AtomicInteger(); + Mockito.doAnswer( + invocation -> { + Mockito.verify(region, Mockito.never()).writeLock(Mockito.anyString()); + final long timeoutMs = invocation.getArgument(1); + Assert.assertTrue( + timeoutMs >= expectedTimeoutMs / 2 && timeoutMs <= expectedTimeoutMs); + if (failFirst && attempts.getAndIncrement() == 0) { + throw new ConsensusGroupModifyPeerException("mock replication failure"); + } + replicated.set(true); + return null; + }) + .when(impl) + .waitTargetPeerUntilSyncLogCompleted(Mockito.eq(remotePeer), Mockito.anyLong()); + Mockito.doAnswer( + invocation -> { + Assert.assertTrue(realtimeEnabled || replicated.get()); + return null; + }) + .when(historical) + .start(); + + final Field regionsField = StorageEngine.class.getDeclaredField("dataRegionMap"); + regionsField.setAccessible(true); + final Map<DataRegionId, DataRegion> regions = + (Map<DataRegionId, DataRegion>) regionsField.get(StorageEngine.getInstance()); + Assert.assertFalse(regions.containsKey(regionId)); + regions.put(regionId, region); + DataRegionConsensusImpl.setInstance(consensus); + try { + if (failFirst) { + Assert.assertThrows(ConsensusGroupModifyPeerException.class, source::start); + Mockito.verify(historical, Mockito.never()).start(); + Mockito.verify(region, Mockito.never()).writeLock(Mockito.anyString()); + } + source.start(); + final InOrder order = Mockito.inOrder(impl, region, realtime, historical); + if (realtimeEnabled) { + Mockito.verify(consensus, Mockito.never()).getImpl(Mockito.any(DataRegionId.class)); + Mockito.verify(impl, Mockito.never()) + .waitTargetPeerUntilSyncLogCompleted(Mockito.any(Peer.class), Mockito.anyLong()); + } else { + order + .verify(impl, Mockito.times(failFirst ? 2 : 1)) + .waitTargetPeerUntilSyncLogCompleted(Mockito.eq(remotePeer), Mockito.anyLong()); + Mockito.verify(impl, Mockito.never()) + .waitTargetPeerUntilSyncLogCompleted(Mockito.eq(localPeer), Mockito.anyLong()); + } + order.verify(region).writeLock(Mockito.anyString()); + order.verify(realtime).start(); + order.verify(historical).start(); + order.verify(region).writeUnlock(); + } finally { + regions.remove(regionId); + DataRegionConsensusImpl.setInstance(originalConsensus); + } + } + + private static void setField( + final Class<?> declaringClass, final Object object, final String name, final Object value) + throws ReflectiveOperationException { + final Field field = declaringClass.getDeclaredField(name); + field.setAccessible(true); + field.set(object, value); + } + @Test public void testIoTDBDataRegionExtractor() { try (final IoTDBDataRegionSource extractor = new IoTDBDataRegionSource()) { diff --git a/iotdb-core/datanode/src/test/java/org/apache/iotdb/db/pipe/source/dataregion/historical/PipeHistoricalDataRegionTsFileAndDeletionSourceTest.java b/iotdb-core/datanode/src/test/java/org/apache/iotdb/db/pipe/source/dataregion/historical/PipeHistoricalDataRegionTsFileAndDeletionSourceTest.java index 991dab795f9..2ba204d4def 100644 --- a/iotdb-core/datanode/src/test/java/org/apache/iotdb/db/pipe/source/dataregion/historical/PipeHistoricalDataRegionTsFileAndDeletionSourceTest.java +++ b/iotdb-core/datanode/src/test/java/org/apache/iotdb/db/pipe/source/dataregion/historical/PipeHistoricalDataRegionTsFileAndDeletionSourceTest.java @@ -32,17 +32,24 @@ import org.apache.iotdb.commons.pipe.config.constant.SystemConstant; import org.apache.iotdb.commons.pipe.config.plugin.configuraion.PipeTaskRuntimeConfiguration; import org.apache.iotdb.commons.pipe.config.plugin.env.PipeTaskSourceRuntimeEnvironment; import org.apache.iotdb.commons.pipe.datastructure.pattern.PrefixTreePattern; +import org.apache.iotdb.commons.pipe.datastructure.pattern.TablePattern; import org.apache.iotdb.commons.pipe.datastructure.resource.PersistentResource; import org.apache.iotdb.commons.pipe.event.ProgressReportEvent; import org.apache.iotdb.commons.utils.FileUtils; +import org.apache.iotdb.db.pipe.agent.task.connection.PipeEventCollector; import org.apache.iotdb.db.pipe.consensus.ReplicateProgressDataNodeManager; +import org.apache.iotdb.db.pipe.event.common.tsfile.PipeTsFileInsertionEvent; +import org.apache.iotdb.db.pipe.resource.PipeDataNodeResourceManager; +import org.apache.iotdb.db.pipe.resource.tsfile.PipeTsFileResourceManager; import org.apache.iotdb.db.storageengine.dataregion.tsfile.TsFileResource; import org.apache.iotdb.db.storageengine.dataregion.tsfile.TsFileResourceStatus; import org.apache.iotdb.db.storageengine.dataregion.tsfile.timeindex.FileTimeIndex; import org.apache.iotdb.pipe.api.customizer.parameter.PipeParameterValidator; import org.apache.iotdb.pipe.api.customizer.parameter.PipeParameters; import org.apache.iotdb.pipe.api.event.Event; +import org.apache.iotdb.pipe.api.exception.PipeException; +import org.apache.tsfile.file.metadata.IDeviceID; import org.junit.Assert; import org.junit.Test; @@ -54,14 +61,203 @@ import java.nio.file.Files; import java.util.ArrayDeque; import java.util.ArrayList; import java.util.Arrays; +import java.util.Collections; import java.util.HashMap; import java.util.HashSet; import java.util.List; import java.util.Map; import java.util.Set; +import java.util.concurrent.atomic.AtomicBoolean; public class PipeHistoricalDataRegionTsFileAndDeletionSourceTest { + @Test + public void testClosingTsFileKeepsTimeFiltering() throws Exception { + // A closing sequence file can temporarily look wholly inside the extraction window even + // though the eventual file also contains rows outside it. + assertSuppliedTsFileTimeFiltering(false, 0, Long.MIN_VALUE, false, true); + } + + @Test + public void testClosedTsFileTimeFiltering() throws Exception { + assertSuppliedTsFileTimeFiltering(true, -10, 10, false, false); + assertSuppliedTsFileTimeFiltering(true, -20, 20, false, true); + } + + @Test + public void testClosingTsFileHonorsLooseTimeRange() throws Exception { + assertSuppliedTsFileTimeFiltering(false, 0, Long.MIN_VALUE, true, false); + } + + @Test + public void testHistoricalTsFileReferenceFailureRetriesWithoutAdvancingProgress() + throws Exception { + assertHistoricalTsFileReferenceFailure(true); + } + + @Test + public void testHistoricalTsFileReferenceFailureReleasesPinOnClose() throws Exception { + assertHistoricalTsFileReferenceFailure(false); + } + + @SuppressWarnings("unchecked") + private void assertHistoricalTsFileReferenceFailure(final boolean retry) throws Exception { + final File tempDir = Files.createTempDirectory("pipeHistoricalReferenceFailure").toFile(); + final String pipeName = "historical_reference_retry"; + final String resourcePipeName = + PipeTsFileResourceManager.getPipeTsFileResourcePipeName(pipeName, 1L); + final PipeTsFileResourceManager manager = PipeDataNodeResourceManager.tsfile(); + final PipeHistoricalDataRegionTsFileAndDeletionSource source = + new PipeHistoricalDataRegionTsFileAndDeletionSource(); + final AtomicBoolean failPathResolution = new AtomicBoolean(); + PipeTsFileInsertionEvent event = null; + try { + final File partitionDir = new File(tempDir, "sequence/db/2147483645/0"); + Assert.assertTrue(partitionDir.mkdirs()); + final TsFileResource resource = createTsFileResource(partitionDir, "1-1-0-0.tsfile"); + resource.setFile(new CanonicalPathFailingFile(resource.getTsFilePath(), failPathResolution)); + resource.setStatusForTest(TsFileResourceStatus.NORMAL); + resource.updateProgressIndex(new SimpleProgressIndex(0, 1)); + + setPrivateField(source, "pipeName", pipeName); + setPrivateField(source, "pipeNameWithCreationTime", resourcePipeName); + setPrivateField(source, "creationTime", 1L); + setPrivateField(source, "dataRegionId", 2147483645); + setPrivateField(source, "hasBeenStarted", true); + setPrivateField(source, "shouldExtractInsertion", true); + setPrivateField(source, "shouldOrderHistoricalTsFileByQueryPriority", true); + setPrivateField(source, "isModelDetected", true); + setPrivateField(source, "isTableModel", true); + setPrivateField(source, "tablePattern", new TablePattern(true, null, null)); + setPrivateField(source, "isDbNameCoveredByPattern", true); + final Map<TsFileResource, Set<String>> selectedResources = + (Map<TsFileResource, Set<String>>) + getPrivateField(source, "filteredTsFileResources2TableNames"); + selectedResources.put(resource, Collections.emptySet()); + final Set<PersistentResource> progressReportResources = + (Set<PersistentResource>) getPrivateField(source, "historicalProgressReportResources"); + progressReportResources.add(resource); + setPrivateField( + source, + "pendingQueue", + new ArrayDeque<PersistentResource>(Collections.singletonList(resource))); + + manager.pinTsFileResource(resource, false, resourcePipeName); + final File pinnedFile = + PipeTsFileResourceManager.getHardlinkOrCopiedFileInPipeDir( + resource.getTsFile(), resourcePipeName); + failPathResolution.set(true); + Assert.assertThrows(PipeException.class, source::supply); + Assert.assertEquals(1, source.getPendingQueueSize()); + Assert.assertTrue(selectedResources.containsKey(resource)); + Assert.assertTrue(progressReportResources.contains(resource)); + Assert.assertNull(getPrivateField(source, "pendingHistoricalProgressIndexToReport")); + Assert.assertEquals(1, manager.getFileReferenceCount(pinnedFile, resourcePipeName)); + + failPathResolution.set(false); + if (retry) { + event = (PipeTsFileInsertionEvent) source.supply(); + Assert.assertNotNull(event); + final Field resourceField = PipeTsFileInsertionEvent.class.getDeclaredField("resource"); + resourceField.setAccessible(true); + Assert.assertSame(resource, resourceField.get(event)); + Assert.assertEquals(0, source.getPendingQueueSize()); + Assert.assertFalse(selectedResources.containsKey(resource)); + // The event now owns the only remaining reference; the source has released its pin. + Assert.assertEquals(1, manager.getFileReferenceCount(pinnedFile, resourcePipeName)); + final Event progressEvent = source.supply(); + Assert.assertTrue(progressEvent instanceof ProgressReportEvent); + Assert.assertEquals( + new TimePartitionProgressIndex( + resource.getTimePartition(), resource.getMaxProgressIndex()), + ((ProgressReportEvent) progressEvent).getProgressIndex()); + } else { + source.close(); + Assert.assertEquals(0, manager.getFileReferenceCount(pinnedFile, resourcePipeName)); + } + } finally { + failPathResolution.set(false); + if (event != null) { + event.clearReferenceCount(getClass().getName()); + } + source.close(); + manager.cleanPipeTsFileDir(resourcePipeName); + FileUtils.deleteFileOrDirectory(tempDir); + } + } + + @SuppressWarnings("unchecked") + private void assertSuppliedTsFileTimeFiltering( + final boolean closed, + final long fileStartTime, + final long fileEndTime, + final boolean looseTimeRange, + final boolean shouldParseTime) + throws Exception { + final File tempDir = Files.createTempDirectory("pipeHistoricalClosingTimeFilter").toFile(); + final String pipeName = "closing_time_filter"; + final String resourcePipeName = + PipeTsFileResourceManager.getPipeTsFileResourcePipeName(pipeName, 1L); + final PipeTsFileResourceManager manager = PipeDataNodeResourceManager.tsfile(); + final PipeHistoricalDataRegionTsFileAndDeletionSource source = + new PipeHistoricalDataRegionTsFileAndDeletionSource(); + TsFileResource resource = null; + PipeTsFileInsertionEvent event = null; + boolean pinned = false; + try { + final File partitionDir = new File(tempDir, "sequence/db/2147483646/0"); + Assert.assertTrue(partitionDir.mkdirs()); + resource = createTsFileResource(partitionDir, "1-1-0-0.tsfile"); + final IDeviceID device = + IDeviceID.Factory.DEFAULT_FACTORY.create(new String[] {"table1", "device1"}); + resource.updateStartTime(device, fileStartTime); + resource.updateEndTime(device, fileEndTime); + resource.setStatusForTest( + closed ? TsFileResourceStatus.NORMAL : TsFileResourceStatus.UNCLOSED); + manager.pinTsFileResource(resource, false, resourcePipeName); + pinned = true; + + setPrivateField(source, "pipeName", pipeName); + setPrivateField(source, "pipeNameWithCreationTime", resourcePipeName); + setPrivateField(source, "creationTime", 1L); + setPrivateField(source, "dataRegionId", 2147483646); + setPrivateField(source, "isModelDetected", true); + setPrivateField(source, "isTableModel", true); + setPrivateField(source, "tablePattern", new TablePattern(true, null, null)); + setPrivateField(source, "isDbNameCoveredByPattern", true); + setPrivateField(source, "historicalDataExtractionStartTime", -10L); + setPrivateField(source, "historicalDataExtractionEndTime", 10L); + setPrivateField(source, "sloppyTimeRange", looseTimeRange); + ((Map<TsFileResource, Set<String>>) + getPrivateField(source, "filteredTsFileResources2TableNames")) + .put(resource, Collections.emptySet()); + + event = (PipeTsFileInsertionEvent) source.supplyTsFileEvent(resource); + pinned = false; + Assert.assertNotNull(event); + // The collector must not send this entire file without filtering after its close completes. + Assert.assertEquals(shouldParseTime, event.shouldParseTime()); + Assert.assertEquals(!shouldParseTime, PipeEventCollector.canSkipParsing4TsFileEvent(event)); + if (!closed && !looseTimeRange) { + resource.updateStartTime(device, -20); + resource.updateEndTime(device, 20); + resource.setStatusForTest(TsFileResourceStatus.NORMAL); + Assert.assertTrue(event.shouldParseTime()); + Assert.assertFalse(PipeEventCollector.canSkipParsing4TsFileEvent(event)); + } + } finally { + if (event != null) { + event.clearReferenceCount(getClass().getName()); + } + if (pinned) { + manager.unpinTsFileResource(resource, resourcePipeName, false); + } + source.close(); + manager.cleanPipeTsFileDir(resourcePipeName); + FileUtils.deleteFileOrDirectory(tempDir); + } + } + @Test public void testDeletionOnlyCustomizeInitializesSourceContext() throws Exception { final PipeHistoricalDataRegionTsFileAndDeletionSource source = @@ -865,6 +1061,32 @@ public class PipeHistoricalDataRegionTsFileAndDeletionSourceTest { return field.get(source); } + private static class CanonicalPathFailingFile extends File { + + private final AtomicBoolean failPathResolution; + + private CanonicalPathFailingFile(final String path, final AtomicBoolean failPathResolution) { + super(path); + this.failPathResolution = failPathResolution; + } + + @Override + public File getParentFile() { + final File parent = super.getParentFile(); + return parent == null + ? null + : new CanonicalPathFailingFile(parent.getPath(), failPathResolution); + } + + @Override + public String getCanonicalPath() throws IOException { + if (failPathResolution.get()) { + throw new IOException("mock path resolution failure"); + } + return super.getCanonicalPath(); + } + } + private static class TestablePipeHistoricalDataRegionTsFileAndDeletionSource extends PipeHistoricalDataRegionTsFileAndDeletionSource { diff --git a/iotdb-core/node-commons/src/main/java/org/apache/iotdb/commons/pipe/config/constant/PipeSourceConstant.java b/iotdb-core/node-commons/src/main/java/org/apache/iotdb/commons/pipe/config/constant/PipeSourceConstant.java index 70d452ae36a..cf83f3e1c32 100644 --- a/iotdb-core/node-commons/src/main/java/org/apache/iotdb/commons/pipe/config/constant/PipeSourceConstant.java +++ b/iotdb-core/node-commons/src/main/java/org/apache/iotdb/commons/pipe/config/constant/PipeSourceConstant.java @@ -98,6 +98,12 @@ public class PipeSourceConstant { public static final String EXTRACTOR_HISTORY_ENABLE_KEY = "extractor.history.enable"; public static final String SOURCE_HISTORY_ENABLE_KEY = "source.history.enable"; public static final boolean EXTRACTOR_HISTORY_ENABLE_DEFAULT_VALUE = true; + public static final String EXTRACTOR_HISTORY_WAIT_FOR_REPLICATION_TIMEOUT_MS_KEY = + "extractor.history.wait-for-replication.timeout-ms"; + public static final String SOURCE_HISTORY_WAIT_FOR_REPLICATION_TIMEOUT_MS_KEY = + "source.history.wait-for-replication.timeout-ms"; + public static final long EXTRACTOR_HISTORY_WAIT_FOR_REPLICATION_TIMEOUT_MS_DEFAULT_VALUE = + 30_000L; public static final String EXTRACTOR_HISTORY_START_TIME_KEY = "extractor.history.start-time"; public static final String SOURCE_HISTORY_START_TIME_KEY = "source.history.start-time"; public static final String EXTRACTOR_HISTORY_END_TIME_KEY = "extractor.history.end-time";
