This is an automated email from the ASF dual-hosted git repository.
jt2594838 pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/iotdb.git
The following commit(s) were added to refs/heads/master by this push:
new 4092a780097 Fix pipe TsFile transfer when mod file is missing (#18756)
4092a780097 is described below
commit 4092a78009765b2b2a9b92c663f140f7f05ec799
Author: Caideyipi <[email protected]>
AuthorDate: Tue Sep 29 15:55:20 2026 +0800
Fix pipe TsFile transfer when mod file is missing (#18756)
---
.../apache/iotdb/db/i18n/DataNodePipeMessages.java | 6 ++
.../apache/iotdb/db/i18n/DataNodePipeMessages.java | 6 ++
.../thrift/async/IoTDBDataRegionAsyncSink.java | 106 ++++++++++++++++++++-
.../async/handler/PipeTransferTsFileHandler.java | 100 ++++++++++++++++---
.../thrift/async/IoTDBDataRegionAsyncSinkTest.java | 49 ++++++++++
.../PipeTransferTsFileHandlerCleanupTest.java | 56 +++++++++++
6 files changed, 307 insertions(+), 16 deletions(-)
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 8fd63338892..1c0362b627b 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
@@ -820,10 +820,16 @@ public final class DataNodePipeMessages {
"Failed to transfer tsfile batch ({}).";
public static final String FAILED_TO_TRANSFER_TSFILE_EVENT_ASYNCHRONOUSLY =
"Failed to transfer tsfile event {} asynchronously.";
+ public static final String
+
EXCEPTION_FAILED_TO_TRANSFER_TSFILE_ARG_BECAUSE_FILE_ARG_IS_MISSING_AFTER_ARG_RETRIES_B656E84E
=
+ "Failed to transfer TsFile {} because file {} is missing after {}
retries.";
public static final String
FAILED_TO_TRANSFER_TSFILEINSERTIONEVENT_COMMITTER_KEY_COMMIT_ID =
"Failed to transfer TsFileInsertionEvent {} (committer key {}, commit id
{}).";
public static final String
FAILED_TO_TRANSFER_TSFILEINSERTIONEVENT_BATCHED_TABLE_EVENTS =
"Failed to transfer TsFileInsertionEvent {} (batched
TableInsertionEvents).";
+ public static final String
+
LOG_TSFILE_ARG_MODIFICATION_FILE_ARG_IS_MISSING_TRANSFER_THE_TSFILE_WITHOUT_MODIFICATIONS_8588D1E9
=
+ "TsFile {}: modification file {} is missing, transfer the TsFile
without modifications.";
public static final String FAILED_TO_UPDATE_LEADER_CACHE_FOR_DEVICE =
"Failed to update leader cache for device {} with endpoint {}:{}.";
public static final String FAILED_TO_WRITE = "Failed to write ";
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 62918788834..49096659ade 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
@@ -774,10 +774,16 @@ public final class DataNodePipeMessages {
public static final String FAILED_TO_TRANSFER_TSFILE_BATCH = "传输 tsfile
batch ({}) 失败。";
public static final String FAILED_TO_TRANSFER_TSFILE_EVENT_ASYNCHRONOUSLY =
"传输 tsfile event {} asynchronously 失败。";
+ public static final String
+
EXCEPTION_FAILED_TO_TRANSFER_TSFILE_ARG_BECAUSE_FILE_ARG_IS_MISSING_AFTER_ARG_RETRIES_B656E84E
=
+ "TsFile {} 传输失败,文件 {} 重试 {} 次后仍不存在。";
public static final String
FAILED_TO_TRANSFER_TSFILEINSERTIONEVENT_COMMITTER_KEY_COMMIT_ID =
"传输 TsFileInsertionEvent {}(committer key {},commit id {})失败。";
public static final String
FAILED_TO_TRANSFER_TSFILEINSERTIONEVENT_BATCHED_TABLE_EVENTS =
"传输 TsFileInsertionEvent {}(批量 TableInsertionEvent)失败。";
+ public static final String
+
LOG_TSFILE_ARG_MODIFICATION_FILE_ARG_IS_MISSING_TRANSFER_THE_TSFILE_WITHOUT_MODIFICATIONS_8588D1E9
=
+ "TsFile {}:修改文件 {} 不存在,将不带修改记录传输 TsFile。";
public static final String FAILED_TO_UPDATE_LEADER_CACHE_FOR_DEVICE =
"更新 leader cache for device {} with endpoint {}:{} 失败。";
public static final String FAILED_TO_WRITE = "写入失败 ";
diff --git
a/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/pipe/sink/protocol/thrift/async/IoTDBDataRegionAsyncSink.java
b/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/pipe/sink/protocol/thrift/async/IoTDBDataRegionAsyncSink.java
index 4b7f332dbe3..64f25919750 100644
---
a/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/pipe/sink/protocol/thrift/async/IoTDBDataRegionAsyncSink.java
+++
b/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/pipe/sink/protocol/thrift/async/IoTDBDataRegionAsyncSink.java
@@ -25,6 +25,7 @@ import org.apache.iotdb.commons.audit.UserEntity;
import org.apache.iotdb.commons.client.ThriftClient;
import
org.apache.iotdb.commons.client.async.AsyncPipeDataTransferServiceClient;
import
org.apache.iotdb.commons.exception.pipe.PipeRuntimeOutOfMemoryCriticalException;
+import
org.apache.iotdb.commons.exception.pipe.PipeRuntimeSinkCriticalException;
import
org.apache.iotdb.commons.exception.pipe.PipeRuntimeSinkNonReportTimeConfigurableException;
import
org.apache.iotdb.commons.exception.pipe.PipeRuntimeSinkResourceException;
import org.apache.iotdb.commons.pipe.agent.task.progress.CommitterKey;
@@ -82,6 +83,7 @@ import
org.apache.tsfile.exception.write.WriteProcessException;
import org.apache.tsfile.utils.Pair;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
+import org.slf4j.helpers.MessageFormatter;
import java.io.File;
import java.io.FileNotFoundException;
@@ -132,6 +134,7 @@ public class IoTDBDataRegionAsyncSink extends IoTDBSink
implements PipeSinkWithS
"Exception occurred while sending to receiver %s:%s.";
private static final boolean isSplitTSFileBatchModeEnabled = true;
+ private static final int MAX_FILE_NOT_FOUND_RETRY_TIMES = 5;
private final IoTDBDataRegionSyncSink syncSink = new
IoTDBDataRegionSyncSink();
@@ -143,6 +146,10 @@ public class IoTDBDataRegionAsyncSink extends IoTDBSink
implements PipeSinkWithS
// semantics because the same payload may compare equal.
private final Map<Event, PipeResourceFailureType>
retryEvent2ResourceFailureType =
new IdentityHashMap<>();
+ // File disappearance is permanent for a given TsFile event. Keep its retry
count separate from
+ // transient receiver failures so one missing file cannot retry forever in
the async queue.
+ private final Map<Event, Integer> missingFileRetryTimes = new
IdentityHashMap<>();
+ private volatile PipeRuntimeSinkCriticalException
missingFileRetryLimitException;
// Keep only the latest text to avoid retaining the complete exception chain
for every event.
private volatile String lastRetryFailureMessage;
// Events removed from the retry queue remain in this map while their next
transfer is being
@@ -576,6 +583,22 @@ public class IoTDBDataRegionAsyncSink extends IoTDBSink
implements PipeSinkWithS
throw new
FileNotFoundException(pipeTsFileInsertionEvent.getTsFile().getAbsolutePath());
}
+ final boolean transferMod =
+ pipeTsFileInsertionEvent.isWithMod()
+ && clientManager.supportModsIfIsDataNodeReceiver()
+ && pipeTsFileInsertionEvent.getModFile() != null
+ && pipeTsFileInsertionEvent.getModFile().exists();
+ if (pipeTsFileInsertionEvent.isWithMod()
+ && clientManager.supportModsIfIsDataNodeReceiver()
+ && pipeTsFileInsertionEvent.getModFile() != null
+ && !pipeTsFileInsertionEvent.getModFile().exists()) {
+ LOGGER.warn(
+ DataNodePipeMessages
+
.LOG_TSFILE_ARG_MODIFICATION_FILE_ARG_IS_MISSING_TRANSFER_THE_TSFILE_WITHOUT_MODIFICATIONS_8588D1E9,
+ pipeTsFileInsertionEvent.getTsFile(),
+ pipeTsFileInsertionEvent.getModFile());
+ }
+
pipeTransferTsFileHandler =
new PipeTransferTsFileHandler(
this,
@@ -589,8 +612,7 @@ public class IoTDBDataRegionAsyncSink extends IoTDBSink
implements PipeSinkWithS
new AtomicBoolean(false),
pipeTsFileInsertionEvent.getTsFile(),
pipeTsFileInsertionEvent.getModFile(),
- pipeTsFileInsertionEvent.isWithMod()
- && clientManager.supportModsIfIsDataNodeReceiver(),
+ transferMod,
pipeTsFileInsertionEvent.isTableModelEvent()
? pipeTsFileInsertionEvent.getTableModelDatabaseName()
: pipeTsFileInsertionEvent.getTreeModelDatabaseName());
@@ -775,6 +797,7 @@ public class IoTDBDataRegionAsyncSink extends IoTDBSink
implements PipeSinkWithS
* @see PipeConnector#transfer(TsFileInsertionEvent) for more details.
*/
private void transferQueuedEventsIfNecessary(final boolean forced) {
+ throwIfMissingFileRetryLimitExceeded();
throwIfReceiverProbeIsDelayed();
if ((retryEventQueue.isEmpty() && retryTsFileQueue.isEmpty())
@@ -838,6 +861,7 @@ public class IoTDBDataRegionAsyncSink extends IoTDBSink
implements PipeSinkWithS
}
}
+ throwIfMissingFileRetryLimitExceeded();
throwIfReceiverProbeIsDelayed();
// Stop retrying if the execution time exceeds the threshold for better
realtime performance
@@ -985,10 +1009,12 @@ public class IoTDBDataRegionAsyncSink extends IoTDBSink
implements PipeSinkWithS
final EnrichedEvent enrichedEvent = (EnrichedEvent) event;
if (enrichedEvent.isReleased()) {
retryingEvent2Handlers.remove(event);
+ missingFileRetryTimes.remove(event);
return;
}
if (isDroppedPipe(enrichedEvent)) {
retryingEvent2Handlers.remove(event);
+ missingFileRetryTimes.remove(event);
enrichedEvent.clearReferenceCount(IoTDBDataRegionAsyncSink.class.getName());
return;
}
@@ -996,6 +1022,7 @@ public class IoTDBDataRegionAsyncSink extends IoTDBSink
implements PipeSinkWithS
if (isClosed.get()) {
retryingEvent2Handlers.remove(event);
+ missingFileRetryTimes.remove(event);
if (event instanceof EnrichedEvent) {
((EnrichedEvent)
event).clearReferenceCount(IoTDBDataRegionAsyncSink.class.getName());
}
@@ -1030,6 +1057,14 @@ public class IoTDBDataRegionAsyncSink extends IoTDBSink
implements PipeSinkWithS
final PipeResourceFailureType previousResourceFailureType =
retryEvent2ResourceFailureType.get(event);
+ if (!alreadyInRetryQueue && isMissingFileFailure(event, e)) {
+ final int failureCount = missingFileRetryTimes.merge(event, 1,
Integer::sum);
+ if (failureCount > MAX_FILE_NOT_FOUND_RETRY_TIMES) {
+ reportMissingFileEventAfterRetryLimit(event, e, failureCount - 1);
+ return;
+ }
+ }
+
if (resourceFailureType != null
&& event instanceof EnrichedEvent
&& (!alreadyInRetryQueue || previousResourceFailureType !=
resourceFailureType)) {
@@ -1070,6 +1105,61 @@ public class IoTDBDataRegionAsyncSink extends IoTDBSink
implements PipeSinkWithS
}
}
+ private static boolean isMissingFileFailure(final Event event, final
Exception exception) {
+ if (!(event instanceof PipeTsFileInsertionEvent) || exception == null) {
+ return false;
+ }
+ return ErrorHandlingCommonUtils.getRootCause(exception) instanceof
FileNotFoundException;
+ }
+
+ private void reportMissingFileEventAfterRetryLimit(
+ final Event event, final Exception cause, final int retryTimes) {
+ final String missingFile = getMissingFile(cause);
+ final File tsFile = ((PipeTsFileInsertionEvent) event).getTsFile();
+ final String message =
+ MessageFormatter.arrayFormat(
+ DataNodePipeMessages
+
.EXCEPTION_FAILED_TO_TRANSFER_TSFILE_ARG_BECAUSE_FILE_ARG_IS_MISSING_AFTER_ARG_RETRIES_B656E84E,
+ new Object[] {tsFile, missingFile, retryTimes})
+ .getMessage();
+ final PipeRuntimeSinkCriticalException criticalException =
+ new PipeRuntimeSinkCriticalException(message, cause);
+
+ retryingEvent2Handlers.remove(event);
+ retryEvent2ResourceFailureType.put(event, null);
+ missingFileRetryTimes.remove(event);
+ lastRetryFailureMessage = message;
+ missingFileRetryLimitException = criticalException;
+ retryTsFileQueue.offer((PipeTsFileInsertionEvent) event);
+ retryEventQueueEventCounter.increaseEventCount(event);
+ if (event instanceof EnrichedEvent) {
+ final EnrichedEvent enrichedEvent = (EnrichedEvent) event;
+ PipeLogger.log(
+ LOGGER::error,
+ criticalException,
+ DataNodePipeMessages
+
.EXCEPTION_FAILED_TO_TRANSFER_TSFILE_ARG_BECAUSE_FILE_ARG_IS_MISSING_AFTER_ARG_RETRIES_B656E84E,
+ tsFile,
+ missingFile,
+ retryTimes);
+ PipeDataNodeAgent.runtime().report(enrichedEvent, criticalException);
+ } else {
+ LOGGER.error(message, cause);
+ }
+ }
+
+ private void throwIfMissingFileRetryLimitExceeded() {
+ final PipeRuntimeSinkCriticalException exception =
missingFileRetryLimitException;
+ if (exception != null) {
+ throw exception;
+ }
+ }
+
+ private static String getMissingFile(final Exception exception) {
+ final Throwable rootCause =
ErrorHandlingCommonUtils.getRootCause(exception);
+ return hasText(rootCause.getMessage()) ? rootCause.getMessage() :
rootCause.toString();
+ }
+
/**
* Add failure {@link EnrichedEvent}s to retry queue.
*
@@ -1121,6 +1211,12 @@ public class IoTDBDataRegionAsyncSink extends IoTDBSink
implements PipeSinkWithS
return lastRetryFailureMessage;
}
+ public synchronized void clearFileNotFoundRetryTimes(final Iterable<?
extends Event> events) {
+ for (final Event event : events) {
+ missingFileRetryTimes.remove(event);
+ }
+ }
+
public void addFailureEventsToRetryQueue(
final Iterable<EnrichedEvent> events,
final Exception e,
@@ -1289,6 +1385,7 @@ public class IoTDBDataRegionAsyncSink extends IoTDBSink
implements PipeSinkWithS
((EnrichedEvent)
event).clearReferenceCount(IoTDBDataRegionAsyncSink.class.getName());
retryEventQueueEventCounter.decreaseEventCount(event);
retryEvent2ResourceFailureType.remove(event);
+ missingFileRetryTimes.remove(event);
retryingEvent2Handlers.remove(event);
return true;
}
@@ -1302,6 +1399,7 @@ public class IoTDBDataRegionAsyncSink extends IoTDBSink
implements PipeSinkWithS
((EnrichedEvent)
event).clearReferenceCount(IoTDBDataRegionAsyncSink.class.getName());
retryEventQueueEventCounter.decreaseEventCount(event);
retryEvent2ResourceFailureType.remove(event);
+ missingFileRetryTimes.remove(event);
retryingEvent2Handlers.remove(event);
return true;
}
@@ -1315,6 +1413,7 @@ public class IoTDBDataRegionAsyncSink extends IoTDBSink
implements PipeSinkWithS
&& isDroppedPipe((EnrichedEvent) event, committerKey));
if (retryEventQueue.isEmpty() && retryTsFileQueue.isEmpty()) {
lastRetryFailureMessage = null;
+ missingFileRetryLimitException = null;
}
}
@@ -1397,12 +1496,15 @@ public class IoTDBDataRegionAsyncSink extends IoTDBSink
implements PipeSinkWithS
retryTsFileQueue.isEmpty() ? retryEventQueue.poll() :
retryTsFileQueue.poll();
retryEventQueueEventCounter.decreaseEventCount(event);
retryEvent2ResourceFailureType.remove(event);
+ missingFileRetryTimes.remove(event);
retryingEvent2Handlers.remove(event);
if (event instanceof EnrichedEvent) {
((EnrichedEvent)
event).clearReferenceCount(IoTDBDataRegionAsyncSink.class.getName());
}
}
retryEvent2ResourceFailureType.clear();
+ missingFileRetryTimes.clear();
+ missingFileRetryLimitException = null;
lastRetryFailureMessage = null;
retryingEvent2Handlers.clear();
}
diff --git
a/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/pipe/sink/protocol/thrift/async/handler/PipeTransferTsFileHandler.java
b/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/pipe/sink/protocol/thrift/async/handler/PipeTransferTsFileHandler.java
index 8bf1a1cede2..327cc570b00 100644
---
a/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/pipe/sink/protocol/thrift/async/handler/PipeTransferTsFileHandler.java
+++
b/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/pipe/sink/protocol/thrift/async/handler/PipeTransferTsFileHandler.java
@@ -51,6 +51,7 @@ import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.io.File;
+import java.io.FileNotFoundException;
import java.io.IOException;
import java.io.RandomAccessFile;
import java.util.Arrays;
@@ -79,14 +80,20 @@ public class PipeTransferTsFileHandler extends
PipeTransferTrackableHandler {
private File currentFile;
private final boolean transferMod;
+ // A mod file can be cleaned up after the event is created. Keep the
requested mode immutable,
+ // but downgrade the requests sent by this handler when the sidecar is no
longer available.
+ private boolean effectiveTransferMod;
private final String dataBaseName;
- private final String conversionTaskId;
+ private String conversionTaskId;
+ private final int outputIndex;
private final int readFileBufferSize;
private PipeTsFileMemoryBlock memoryBlock;
private byte[] readBuffer;
private long position;
+ private boolean transferringModFile;
+ private boolean transferringTsFile;
private RandomAccessFile reader;
@@ -141,13 +148,17 @@ public class PipeTransferTsFileHandler extends
PipeTransferTrackableHandler {
this.tsFile = tsFile;
this.modFile = modFile;
this.transferMod = transferMod;
+ this.effectiveTransferMod = transferMod && modFile != null &&
modFile.exists();
this.dataBaseName = dataBaseName;
+ this.outputIndex = outputIndex;
+ this.transferringModFile = false;
+ this.transferringTsFile = false;
conversionTaskId =
connector.shouldAsyncLoadTsFileOnTypeMismatch()
? PipeTransferTsFileSealWithModReq.generateConversionTaskId(
- connector.getSinkTaskId(), events, dataBaseName, outputIndex,
transferMod)
+ connector.getSinkTaskId(), events, dataBaseName, outputIndex,
effectiveTransferMod)
: null;
- currentFile = transferMod ? modFile : tsFile;
+ currentFile = effectiveTransferMod ? modFile : tsFile;
// NOTE: Waiting for resource enough for slicing here may cause deadlock!
// TsFile events are producing and consuming at the same time, and the
memory of a TsFile
@@ -157,7 +168,7 @@ public class PipeTransferTsFileHandler extends
PipeTransferTrackableHandler {
// will cause a deadlock.
waitForResourceEnough4Slicing((long) ((1 + Math.random()) * 20 * 1000));
// 20 - 40 seconds
final long maxFileLength =
- transferMod && Objects.nonNull(modFile)
+ effectiveTransferMod && Objects.nonNull(modFile)
? Math.max(tsFile.length(), modFile.length())
: tsFile.length();
readFileBufferSize =
@@ -204,7 +215,7 @@ public class PipeTransferTsFileHandler extends
PipeTransferTrackableHandler {
}
if (reader == null) {
- reader = transferMod ? new RandomAccessFile(modFile, "r") : new
RandomAccessFile(tsFile, "r");
+ openNextFile();
}
client.setShouldReturnSelf(false);
@@ -213,18 +224,15 @@ public class PipeTransferTsFileHandler extends
PipeTransferTrackableHandler {
final int readLength = readNextFilePiece(reader, readBuffer);
if (readLength == -1) {
- if (currentFile == modFile) {
- currentFile = tsFile;
- position = 0;
- reader.close();
- reader = null;
- reader = new RandomAccessFile(tsFile, "r");
+ reader.close();
+ reader = null;
+ if (openNextFile()) {
transfer(clientManager, client);
- } else if (currentFile == tsFile) {
+ } else {
isSealSignalSent.set(true);
final TPipeTransferReq uncompressedReq =
- transferMod
+ effectiveTransferMod
? PipeTransferTsFileSealWithModReq.toTPipeTransferReq(
modFile.getName(),
modFile.length(),
@@ -263,7 +271,7 @@ public class PipeTransferTsFileHandler extends
PipeTransferTrackableHandler {
? readBuffer
: Arrays.copyOfRange(readBuffer, 0, readLength);
final TPipeTransferReq uncompressedReq =
- transferMod
+ effectiveTransferMod
? PipeTransferTsFilePieceWithModReq.toTPipeTransferReq(
currentFile.getName(), position, payload)
: PipeTransferTsFilePieceReq.toTPipeTransferReq(
@@ -338,6 +346,7 @@ public class PipeTransferTsFileHandler extends
PipeTransferTrackableHandler {
try {
final int referenceCount = eventsReferenceCount.decrementAndGet();
if (referenceCount <= 0) {
+ sink.clearFileNotFoundRetryTimes(events);
events.forEach(
event ->
event.decreaseReferenceCount(PipeTransferTsFileHandler.class.getName(), true));
@@ -497,6 +506,69 @@ public class PipeTransferTsFileHandler extends
PipeTransferTrackableHandler {
events.forEach(event ->
event.clearReferenceCount(PipeTransferTsFileHandler.class.getName()));
}
+ private boolean advanceToNextFile() {
+ if (transferMod && !transferringModFile) {
+ transferringModFile = true;
+ if (modFile != null && modFile.exists()) {
+ effectiveTransferMod = true;
+ conversionTaskId = generateConversionTaskId();
+ currentFile = modFile;
+ position = 0;
+ return true;
+ }
+
+ // The mod file is an optional sidecar. It may have been cleaned up
after the event was
+ // created, so transfer the TsFile without modifications instead of
creating a WithMod
+ // request that the receiver can never finish.
+ effectiveTransferMod = false;
+ conversionTaskId = generateConversionTaskId();
+ if (modFile != null) {
+ LOGGER.warn(
+ DataNodePipeMessages
+
.LOG_TSFILE_ARG_MODIFICATION_FILE_ARG_IS_MISSING_TRANSFER_THE_TSFILE_WITHOUT_MODIFICATIONS_8588D1E9,
+ tsFile,
+ modFile);
+ }
+ }
+ if (!transferringTsFile) {
+ transferringTsFile = true;
+ currentFile = tsFile;
+ position = 0;
+ return true;
+ }
+ return false;
+ }
+
+ private boolean openNextFile() throws IOException {
+ while (advanceToNextFile()) {
+ try {
+ reader = new RandomAccessFile(currentFile, "r");
+ return true;
+ } catch (final FileNotFoundException e) {
+ if (currentFile == modFile && effectiveTransferMod) {
+ // Re-check after advanceToNextFile(). The sidecar can disappear in
this small window.
+ effectiveTransferMod = false;
+ conversionTaskId = generateConversionTaskId();
+ LOGGER.warn(
+ DataNodePipeMessages
+
.LOG_TSFILE_ARG_MODIFICATION_FILE_ARG_IS_MISSING_TRANSFER_THE_TSFILE_WITHOUT_MODIFICATIONS_8588D1E9,
+ tsFile,
+ modFile);
+ continue;
+ }
+ throw e;
+ }
+ }
+ return false;
+ }
+
+ private String generateConversionTaskId() {
+ return sink.shouldAsyncLoadTsFileOnTypeMismatch()
+ ? PipeTransferTsFileSealWithModReq.generateConversionTaskId(
+ sink.getSinkTaskId(), events, dataBaseName, outputIndex,
effectiveTransferMod)
+ : null;
+ }
+
@Override
public synchronized void close() {
try {
diff --git
a/iotdb-core/datanode/src/test/java/org/apache/iotdb/db/pipe/sink/protocol/thrift/async/IoTDBDataRegionAsyncSinkTest.java
b/iotdb-core/datanode/src/test/java/org/apache/iotdb/db/pipe/sink/protocol/thrift/async/IoTDBDataRegionAsyncSinkTest.java
index cd0213bc735..fb924b31e30 100644
---
a/iotdb-core/datanode/src/test/java/org/apache/iotdb/db/pipe/sink/protocol/thrift/async/IoTDBDataRegionAsyncSinkTest.java
+++
b/iotdb-core/datanode/src/test/java/org/apache/iotdb/db/pipe/sink/protocol/thrift/async/IoTDBDataRegionAsyncSinkTest.java
@@ -19,6 +19,8 @@
package org.apache.iotdb.db.pipe.sink.protocol.thrift.async;
+import
org.apache.iotdb.commons.exception.pipe.PipeRuntimeSinkCriticalException;
+import org.apache.iotdb.db.pipe.event.common.tsfile.PipeTsFileInsertionEvent;
import org.apache.iotdb.pipe.api.event.Event;
import org.apache.iotdb.pipe.api.exception.PipeException;
@@ -26,6 +28,11 @@ import org.junit.Assert;
import org.junit.Test;
import org.mockito.Mockito;
+import java.io.File;
+import java.io.FileNotFoundException;
+import java.lang.reflect.Field;
+import java.util.Map;
+
public class IoTDBDataRegionAsyncSinkTest {
@Test
@@ -63,4 +70,46 @@ public class IoTDBDataRegionAsyncSinkTest {
Assert.assertEquals("java.lang.NullPointerException",
sink.getLastRetryFailureMessage());
}
+
+ @Test
+ @SuppressWarnings("unchecked")
+ public void testMissingTsFileRetryLimitKeepsEventQueuedAndStopsSink() throws
Exception {
+ final IoTDBDataRegionAsyncSink sink = new IoTDBDataRegionAsyncSink();
+ final PipeTsFileInsertionEvent event =
Mockito.mock(PipeTsFileInsertionEvent.class);
+ final File missingTsFile = new
File("missing-retry-limit.tsfile").getAbsoluteFile();
+ Mockito.when(event.getTsFile()).thenReturn(missingTsFile);
+
+ final Field retryLimitField =
+
IoTDBDataRegionAsyncSink.class.getDeclaredField("MAX_FILE_NOT_FOUND_RETRY_TIMES");
+ retryLimitField.setAccessible(true);
+ final int retryLimit = retryLimitField.getInt(null);
+
+ final Field retryTimesField =
+
IoTDBDataRegionAsyncSink.class.getDeclaredField("missingFileRetryTimes");
+ retryTimesField.setAccessible(true);
+ ((Map<Event, Integer>) retryTimesField.get(sink)).put(event, retryLimit);
+
+ try {
+ sink.addFailureEventToRetryQueue(
+ event, new FileNotFoundException(missingTsFile.getAbsolutePath()));
+
+ Assert.assertEquals(1, sink.getRetryEventQueueSize());
+ Assert.assertTrue(
+
sink.getLastRetryFailureMessage().contains(missingTsFile.getAbsolutePath()));
+
Assert.assertTrue(sink.getLastRetryFailureMessage().contains(String.valueOf(retryLimit)));
+ Mockito.verify(event, Mockito.never())
+ .clearReferenceCount(IoTDBDataRegionAsyncSink.class.getName());
+
+ try {
+ sink.transfer(Mockito.mock(Event.class));
+ Assert.fail("The sink should stop transferring after the missing-file
retry limit.");
+ } catch (final PipeRuntimeSinkCriticalException e) {
+
Assert.assertTrue(e.getMessage().contains(missingTsFile.getAbsolutePath()));
+ }
+ } finally {
+ sink.clearRetryEventsReferenceCount();
+ }
+
+
Mockito.verify(event).clearReferenceCount(IoTDBDataRegionAsyncSink.class.getName());
+ }
}
diff --git
a/iotdb-core/datanode/src/test/java/org/apache/iotdb/db/pipe/sink/protocol/thrift/async/handler/PipeTransferTsFileHandlerCleanupTest.java
b/iotdb-core/datanode/src/test/java/org/apache/iotdb/db/pipe/sink/protocol/thrift/async/handler/PipeTransferTsFileHandlerCleanupTest.java
index 3cca3285cca..563961d983d 100644
---
a/iotdb-core/datanode/src/test/java/org/apache/iotdb/db/pipe/sink/protocol/thrift/async/handler/PipeTransferTsFileHandlerCleanupTest.java
+++
b/iotdb-core/datanode/src/test/java/org/apache/iotdb/db/pipe/sink/protocol/thrift/async/handler/PipeTransferTsFileHandlerCleanupTest.java
@@ -36,6 +36,7 @@ import java.io.File;
import java.io.IOException;
import java.io.RandomAccessFile;
import java.lang.reflect.Field;
+import java.lang.reflect.Method;
import java.nio.file.Files;
import java.util.Collections;
import java.util.concurrent.atomic.AtomicBoolean;
@@ -95,6 +96,47 @@ public class PipeTransferTsFileHandlerCleanupTest {
}
}
+ @Test
+ public void testModFileDisappearingBeforeOpenDowngradesToTsFileOnly() throws
Exception {
+ final File tsFile = Files.createTempFile("pipe-transfer-source",
".tsfile").toFile();
+ final File modFile =
+ new DisappearingFile(
+ Files.createTempDirectory("pipe-transfer-missing-mod")
+ .resolve("source.tsfile.mods2")
+ .toString());
+ final PipeTsFileInsertionEvent event =
Mockito.mock(PipeTsFileInsertionEvent.class);
+ final PipeTransferTsFileHandler handler =
+ new PipeTransferTsFileHandler(
+ Mockito.mock(IoTDBDataRegionAsyncSink.class),
+ Collections.emptyMap(),
+ Collections.singletonList(event),
+ new AtomicInteger(1),
+ new AtomicBoolean(false),
+ tsFile,
+ modFile,
+ true,
+ null);
+
+ try {
+ final Method openNextFile =
PipeTransferTsFileHandler.class.getDeclaredMethod("openNextFile");
+ openNextFile.setAccessible(true);
+ Assert.assertTrue((Boolean) openNextFile.invoke(handler));
+
+ final Field currentFile =
PipeTransferTsFileHandler.class.getDeclaredField("currentFile");
+ currentFile.setAccessible(true);
+ Assert.assertSame(tsFile, currentFile.get(handler));
+
+ final Field effectiveTransferMod =
+
PipeTransferTsFileHandler.class.getDeclaredField("effectiveTransferMod");
+ effectiveTransferMod.setAccessible(true);
+ Assert.assertFalse(effectiveTransferMod.getBoolean(handler));
+ } finally {
+ handler.close();
+ Assert.assertTrue(tsFile.delete());
+ Assert.assertTrue(modFile.getParentFile().delete());
+ }
+ }
+
@Test
public void testSealFailurePassesNestedReceiverMessageToRetryQueue() throws
Exception {
final File file = Files.createTempFile("pipe-transfer-seal-failure",
".tsfile").toFile();
@@ -160,4 +202,18 @@ public class PipeTransferTsFileHandlerCleanupTest {
false,
null);
}
+
+ private static final class DisappearingFile extends File {
+
+ private final AtomicInteger existenceCheckCount = new AtomicInteger();
+
+ private DisappearingFile(final String pathname) {
+ super(pathname);
+ }
+
+ @Override
+ public boolean exists() {
+ return existenceCheckCount.incrementAndGet() <= 2;
+ }
+ }
}