Skip to content

Commit 8b10792

Browse files
authored
Fix unbounded async pipe sink retries (#18222) (#18225)
1 parent 780540c commit 8b10792

8 files changed

Lines changed: 346 additions & 30 deletions

File tree

iotdb-core/datanode/src/main/java/org/apache/iotdb/db/pipe/agent/task/subtask/sink/PipeSinkSubtask.java

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@
2121

2222
import org.apache.iotdb.commons.conf.CommonDescriptor;
2323
import org.apache.iotdb.commons.exception.pipe.PipeRuntimeException;
24+
import org.apache.iotdb.commons.exception.pipe.PipeRuntimeSinkNonReportTimeConfigurableException;
2425
import org.apache.iotdb.commons.pipe.agent.task.connection.UnboundedBlockingPendingQueue;
2526
import org.apache.iotdb.commons.pipe.agent.task.progress.CommitterKey;
2627
import org.apache.iotdb.commons.pipe.agent.task.subtask.PipeAbstractSinkSubtask;
@@ -170,6 +171,8 @@ private void transferHeartbeatEvent(final PipeHeartbeatEvent event) {
170171
})) {
171172
return;
172173
}
174+
} catch (final PipeRuntimeSinkNonReportTimeConfigurableException e) {
175+
throw e;
173176
} catch (final Exception e) {
174177
throw new PipeConnectionException(
175178
"PipeConnector: "

iotdb-core/datanode/src/main/java/org/apache/iotdb/db/pipe/sink/protocol/thrift/async/IoTDBDataRegionAsyncSink.java

Lines changed: 145 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@
2323
import org.apache.iotdb.common.rpc.thrift.TSStatus;
2424
import org.apache.iotdb.commons.client.ThriftClient;
2525
import org.apache.iotdb.commons.client.async.AsyncPipeDataTransferServiceClient;
26+
import org.apache.iotdb.commons.exception.pipe.PipeRuntimeSinkNonReportTimeConfigurableException;
2627
import org.apache.iotdb.commons.pipe.agent.task.progress.CommitterKey;
2728
import org.apache.iotdb.commons.pipe.config.PipeConfig;
2829
import org.apache.iotdb.commons.pipe.event.EnrichedEvent;
@@ -85,7 +86,6 @@
8586
import java.util.concurrent.LinkedBlockingQueue;
8687
import java.util.concurrent.atomic.AtomicBoolean;
8788
import java.util.concurrent.atomic.AtomicInteger;
88-
import java.util.concurrent.atomic.AtomicLong;
8989

9090
import static org.apache.iotdb.commons.pipe.config.constant.PipeSinkConstant.CONNECTOR_ENABLE_SEND_TSFILE_LIMIT;
9191
import static org.apache.iotdb.commons.pipe.config.constant.PipeSinkConstant.CONNECTOR_ENABLE_SEND_TSFILE_LIMIT_DEFAULT_VALUE;
@@ -539,6 +539,8 @@ private void logOnClientException(
539539
* @see PipeConnector#transfer(TsFileInsertionEvent) for more details.
540540
*/
541541
private void transferQueuedEventsIfNecessary(final boolean forced) {
542+
throwIfReceiverProbeIsDelayed();
543+
542544
if ((retryEventQueue.isEmpty() && retryTsFileQueue.isEmpty())
543545
|| (!forced
544546
&& retryEventQueueEventCounter.getTabletInsertionEventCount()
@@ -600,6 +602,8 @@ private void transferQueuedEventsIfNecessary(final boolean forced) {
600602
}
601603
}
602604

605+
throwIfReceiverProbeIsDelayed();
606+
603607
// Stop retrying if the execution time exceeds the threshold for better realtime performance
604608
if (System.currentTimeMillis() - retryStartTime
605609
> PipeConfig.getInstance().getPipeAsyncSinkMaxRetryExecutionTimeMsPerCall()) {
@@ -747,21 +751,58 @@ public void waitIfReceiverTemporarilyUnavailable(final TEndPoint endPoint) {
747751
return;
748752
}
749753

750-
while (!isClosed.get()) {
751-
final long waitTimeInMs = backoff.getRemainingWaitTimeInMs();
752-
if (waitTimeInMs <= 0) {
753-
return;
754+
while (!isClosed.get() && backoff.isActive()) {
755+
if (backoff.isRetryMaxDurationExceeded()) {
756+
final long probeDelayInMs = backoff.tryAcquireProbeAndGetDelayInMs();
757+
if (probeDelayInMs <= 0) {
758+
return;
759+
}
760+
throw createReceiverProbeDelayException(endPointKey, backoff);
754761
}
755762

756-
try {
757-
Thread.sleep(waitTimeInMs);
758-
} catch (final InterruptedException e) {
759-
Thread.currentThread().interrupt();
760-
return;
763+
final long retryTimeInMs = backoff.reserveNextRetryTimeInMs();
764+
while (!isClosed.get() && backoff.isActive()) {
765+
if (backoff.isRetryMaxDurationExceeded()) {
766+
break;
767+
}
768+
769+
final long waitTimeInMs = retryTimeInMs - System.currentTimeMillis();
770+
if (waitTimeInMs <= 0) {
771+
return;
772+
}
773+
774+
try {
775+
Thread.sleep(Math.min(waitTimeInMs, 1000L));
776+
} catch (final InterruptedException e) {
777+
Thread.currentThread().interrupt();
778+
return;
779+
}
761780
}
762781
}
763782
}
764783

784+
private void throwIfReceiverProbeIsDelayed() {
785+
for (final Map.Entry<String, ReceiverTemporaryUnavailableBackoff> entry :
786+
receiverBackoffMap.entrySet()) {
787+
final long probeDelayInMs = entry.getValue().getRemainingProbeDelayInMs();
788+
if (probeDelayInMs <= 0) {
789+
continue;
790+
}
791+
792+
throw createReceiverProbeDelayException(entry.getKey(), entry.getValue());
793+
}
794+
}
795+
796+
private static PipeRuntimeSinkNonReportTimeConfigurableException
797+
createReceiverProbeDelayException(
798+
final String endPointKey, final ReceiverTemporaryUnavailableBackoff backoff) {
799+
return new PipeRuntimeSinkNonReportTimeConfigurableException(
800+
String.format(
801+
"Receiver %s remained temporarily unavailable for more than %d ms, pause regular retries and probe every %d ms.",
802+
endPointKey, backoff.getRetryMaxDurationInMs(), backoff.getRetryProbeIntervalInMs()),
803+
Long.MAX_VALUE);
804+
}
805+
765806
public void recordReceiverStatus(final TEndPoint endPoint, final TSStatus status) {
766807
final String endPointKey = format(endPoint);
767808
if (Objects.isNull(endPointKey) || Objects.isNull(status)) {
@@ -781,10 +822,15 @@ public void recordReceiverStatus(final TEndPoint endPoint, final TSStatus status
781822
status);
782823
}
783824
} else if (isSuccess(status)) {
784-
final ReceiverTemporaryUnavailableBackoff backoff = receiverBackoffMap.get(endPointKey);
785-
if (Objects.nonNull(backoff) && backoff.getRemainingWaitTimeInMs() <= 0) {
786-
receiverBackoffMap.remove(endPointKey, backoff);
787-
}
825+
receiverBackoffMap.computeIfPresent(
826+
endPointKey,
827+
(key, backoff) -> {
828+
if (!backoff.shouldResetOnSuccess()) {
829+
return backoff;
830+
}
831+
backoff.markAvailable();
832+
return null;
833+
});
788834
}
789835
}
790836

@@ -994,23 +1040,90 @@ private static class ReceiverTemporaryUnavailableBackoff {
9941040

9951041
private final long maxBackoffTimeInMs =
9961042
Math.max(0, PipeConfig.getInstance().getPipeSinkSubtaskSleepIntervalMaxMs());
997-
private final AtomicLong currentBackoffTimeInMs =
998-
new AtomicLong(
999-
Math.min(
1000-
Math.max(1, PipeConfig.getInstance().getPipeSinkSubtaskSleepIntervalInitMs()),
1001-
maxBackoffTimeInMs));
1002-
private final AtomicLong nextAvailableTimeInMs = new AtomicLong(0);
1003-
1004-
private long markTemporarilyUnavailable() {
1005-
final long backoffTimeInMs = currentBackoffTimeInMs.get();
1006-
nextAvailableTimeInMs.updateAndGet(
1007-
current -> Math.max(current, System.currentTimeMillis() + backoffTimeInMs));
1008-
currentBackoffTimeInMs.updateAndGet(this::getNextBackoffTimeInMs);
1043+
private final long initialBackoffTimeInMs =
1044+
Math.min(
1045+
Math.max(1, PipeConfig.getInstance().getPipeSinkSubtaskSleepIntervalInitMs()),
1046+
maxBackoffTimeInMs);
1047+
private final long retryMaxDurationInMs =
1048+
PipeConfig.getInstance().getPipeAsyncSinkRetryMaxDurationMs();
1049+
private final long retryProbeIntervalInMs =
1050+
Math.max(1, PipeConfig.getInstance().getPipeAsyncSinkRetryProbeIntervalMs());
1051+
1052+
private boolean active = false;
1053+
private long firstUnavailableTimeInMs = 0;
1054+
private long currentBackoffTimeInMs = initialBackoffTimeInMs;
1055+
private long failureBackoffUntilInMs = 0;
1056+
private long nextReservedRetryTimeInMs = 0;
1057+
private long nextProbeTimeInMs = 0;
1058+
1059+
private synchronized long markTemporarilyUnavailable() {
1060+
final long currentTimeInMs = System.currentTimeMillis();
1061+
if (!active) {
1062+
active = true;
1063+
firstUnavailableTimeInMs = currentTimeInMs;
1064+
currentBackoffTimeInMs = initialBackoffTimeInMs;
1065+
failureBackoffUntilInMs = 0;
1066+
nextReservedRetryTimeInMs = 0;
1067+
nextProbeTimeInMs = 0;
1068+
}
1069+
1070+
final long backoffTimeInMs = currentBackoffTimeInMs;
1071+
failureBackoffUntilInMs =
1072+
Math.max(failureBackoffUntilInMs, safeAdd(currentTimeInMs, backoffTimeInMs));
1073+
nextReservedRetryTimeInMs = Math.max(nextReservedRetryTimeInMs, failureBackoffUntilInMs);
1074+
currentBackoffTimeInMs = getNextBackoffTimeInMs(currentBackoffTimeInMs);
10091075
return backoffTimeInMs;
10101076
}
10111077

1012-
private long getRemainingWaitTimeInMs() {
1013-
return nextAvailableTimeInMs.get() - System.currentTimeMillis();
1078+
private synchronized boolean isActive() {
1079+
return active;
1080+
}
1081+
1082+
private synchronized boolean isRetryMaxDurationExceeded() {
1083+
return active
1084+
&& retryMaxDurationInMs >= 0
1085+
&& System.currentTimeMillis() - firstUnavailableTimeInMs >= retryMaxDurationInMs;
1086+
}
1087+
1088+
private synchronized long reserveNextRetryTimeInMs() {
1089+
final long currentTimeInMs = System.currentTimeMillis();
1090+
final long retryTimeInMs =
1091+
Math.max(currentTimeInMs, Math.max(failureBackoffUntilInMs, nextReservedRetryTimeInMs));
1092+
nextReservedRetryTimeInMs = safeAdd(retryTimeInMs, currentBackoffTimeInMs);
1093+
return retryTimeInMs;
1094+
}
1095+
1096+
private synchronized long tryAcquireProbeAndGetDelayInMs() {
1097+
final long currentTimeInMs = System.currentTimeMillis();
1098+
if (currentTimeInMs >= nextProbeTimeInMs) {
1099+
nextProbeTimeInMs = safeAdd(currentTimeInMs, retryProbeIntervalInMs);
1100+
return 0;
1101+
}
1102+
return nextProbeTimeInMs - currentTimeInMs;
1103+
}
1104+
1105+
private synchronized long getRemainingProbeDelayInMs() {
1106+
return isRetryMaxDurationExceeded()
1107+
? Math.max(0, nextProbeTimeInMs - System.currentTimeMillis())
1108+
: 0;
1109+
}
1110+
1111+
private synchronized boolean shouldResetOnSuccess() {
1112+
return active
1113+
&& (isRetryMaxDurationExceeded()
1114+
|| failureBackoffUntilInMs - System.currentTimeMillis() <= 0);
1115+
}
1116+
1117+
private synchronized void markAvailable() {
1118+
active = false;
1119+
}
1120+
1121+
private long getRetryMaxDurationInMs() {
1122+
return retryMaxDurationInMs;
1123+
}
1124+
1125+
private long getRetryProbeIntervalInMs() {
1126+
return retryProbeIntervalInMs;
10141127
}
10151128

10161129
private long getNextBackoffTimeInMs(final long currentBackoffTimeInMs) {
@@ -1021,5 +1134,9 @@ private long getNextBackoffTimeInMs(final long currentBackoffTimeInMs) {
10211134
? maxBackoffTimeInMs
10221135
: currentBackoffTimeInMs << 1;
10231136
}
1137+
1138+
private static long safeAdd(final long left, final long right) {
1139+
return left >= Long.MAX_VALUE - right ? Long.MAX_VALUE : left + right;
1140+
}
10241141
}
10251142
}

iotdb-core/datanode/src/main/java/org/apache/iotdb/db/pipe/sink/protocol/thrift/async/handler/PipeTransferTrackableHandler.java

Lines changed: 27 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@
2121

2222
import org.apache.iotdb.commons.client.ThriftClient;
2323
import org.apache.iotdb.commons.client.async.AsyncPipeDataTransferServiceClient;
24+
import org.apache.iotdb.commons.exception.pipe.PipeRuntimeSinkNonReportTimeConfigurableException;
2425
import org.apache.iotdb.commons.pipe.resource.log.PipeLogger;
2526
import org.apache.iotdb.commons.pipe.sink.payload.thrift.common.PipeTransferSliceReqBuilder;
2627
import org.apache.iotdb.db.pipe.sink.protocol.thrift.async.IoTDBDataRegionAsyncSink;
@@ -92,7 +93,7 @@ public void onError(final Exception exception) {
9293
* @param client the client used for data transfer
9394
* @param req the request containing transfer details
9495
* @return {@code true} if the transfer was initiated successfully, {@code false} if the connector
95-
* is closed
96+
* is closed or the receiver probe is delayed
9697
* @throws TException if an error occurs during the transfer
9798
*/
9899
protected boolean tryTransfer(
@@ -106,7 +107,13 @@ protected boolean tryTransfer(
106107
if (returnFalseIfSinkIsClosed(client)) {
107108
return false;
108109
}
109-
sink.waitIfReceiverTemporarilyUnavailable(client.getEndPoint());
110+
try {
111+
sink.waitIfReceiverTemporarilyUnavailable(client.getEndPoint());
112+
} catch (final PipeRuntimeSinkNonReportTimeConfigurableException e) {
113+
returnClientToPool(client);
114+
onError(e);
115+
return false;
116+
}
110117
if (returnFalseIfSinkIsClosed(client)) {
111118
return false;
112119
}
@@ -136,6 +143,21 @@ private boolean returnFalseIfSinkIsClosed(final AsyncPipeDataTransferServiceClie
136143
return true;
137144
}
138145

146+
private void returnClientToPool(final AsyncPipeDataTransferServiceClient client) {
147+
client.setShouldReturnSelf(true);
148+
client.returnSelf(
149+
e -> {
150+
if (e instanceof IllegalStateException) {
151+
PipeLogger.log(
152+
LOGGER::info,
153+
"Illegal state when return the client to object pool, maybe the pool is already cleared. Will ignore.");
154+
return true;
155+
}
156+
return false;
157+
});
158+
this.client = null;
159+
}
160+
139161
/**
140162
* @return {@code true} if all transmissions corresponding to the handler have been completed,
141163
* {@code false} otherwise
@@ -276,6 +298,9 @@ private void fallbackToWholeRequest(
276298
return;
277299
}
278300
client.pipeTransfer(originalReq, this);
301+
} catch (final PipeRuntimeSinkNonReportTimeConfigurableException e) {
302+
returnClientToPool(client);
303+
PipeTransferTrackableHandler.this.onError(e);
279304
} catch (final Exception e) {
280305
PipeTransferTrackableHandler.this.onError(e);
281306
}

iotdb-core/datanode/src/test/java/org/apache/iotdb/db/pipe/agent/task/subtask/sink/PipeSinkSubtaskTest.java

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,9 +20,11 @@
2020
package org.apache.iotdb.db.pipe.agent.task.subtask.sink;
2121

2222
import org.apache.iotdb.commons.conf.CommonDescriptor;
23+
import org.apache.iotdb.commons.exception.pipe.PipeRuntimeSinkNonReportTimeConfigurableException;
2324
import org.apache.iotdb.commons.pipe.agent.task.connection.UnboundedBlockingPendingQueue;
2425
import org.apache.iotdb.commons.pipe.agent.task.progress.CommitterKey;
2526
import org.apache.iotdb.commons.pipe.sink.protocol.PipeConnectorWithEventDiscard;
27+
import org.apache.iotdb.db.pipe.event.common.heartbeat.PipeHeartbeatEvent;
2628
import org.apache.iotdb.pipe.api.PipeConnector;
2729
import org.apache.iotdb.pipe.api.customizer.configuration.PipeConnectorRuntimeConfiguration;
2830
import org.apache.iotdb.pipe.api.customizer.parameter.PipeParameterValidator;
@@ -39,9 +41,13 @@
3941
import java.util.concurrent.TimeUnit;
4042
import java.util.concurrent.atomic.AtomicBoolean;
4143

44+
import static org.mockito.ArgumentMatchers.any;
4245
import static org.mockito.Mockito.doAnswer;
46+
import static org.mockito.Mockito.doThrow;
4347
import static org.mockito.Mockito.mock;
48+
import static org.mockito.Mockito.never;
4449
import static org.mockito.Mockito.verify;
50+
import static org.mockito.Mockito.when;
4551
import static org.mockito.Mockito.withSettings;
4652

4753
public class PipeSinkSubtaskTest {
@@ -221,6 +227,46 @@ public void testCloseDoesNotWaitForeverForConnectorClose() throws Exception {
221227
}
222228
}
223229

230+
@Test
231+
public void testHeartbeatPreservesReceiverProbeDelayException() throws Exception {
232+
final long originalSleepIntervalInitMs =
233+
CommonDescriptor.getInstance().getConfig().getPipeSinkSubtaskSleepIntervalInitMs();
234+
final long originalSleepIntervalMaxMs =
235+
CommonDescriptor.getInstance().getConfig().getPipeSinkSubtaskSleepIntervalMaxMs();
236+
CommonDescriptor.getInstance().getConfig().setPipeSinkSubtaskSleepIntervalInitMs(1);
237+
CommonDescriptor.getInstance().getConfig().setPipeSinkSubtaskSleepIntervalMaxMs(2);
238+
239+
final PipeConnector connector = mock(PipeConnector.class);
240+
final UnboundedBlockingPendingQueue<Event> pendingQueue =
241+
mock(UnboundedBlockingPendingQueue.class);
242+
when(pendingQueue.waitedPoll()).thenReturn(new PipeHeartbeatEvent("1", false));
243+
doThrow(new PipeRuntimeSinkNonReportTimeConfigurableException("probe delayed", Long.MAX_VALUE))
244+
.when(connector)
245+
.transfer(any(Event.class));
246+
247+
final PipeSinkSubtask subtask =
248+
new PipeSinkSubtask(
249+
"PipeSinkSubtaskTest",
250+
System.currentTimeMillis(),
251+
"data_test",
252+
0,
253+
pendingQueue,
254+
connector);
255+
256+
try {
257+
Assert.assertTrue(subtask.executeOnce());
258+
verify(connector, never()).handshake();
259+
} finally {
260+
subtask.close();
261+
CommonDescriptor.getInstance()
262+
.getConfig()
263+
.setPipeSinkSubtaskSleepIntervalInitMs(originalSleepIntervalInitMs);
264+
CommonDescriptor.getInstance()
265+
.getConfig()
266+
.setPipeSinkSubtaskSleepIntervalMaxMs(originalSleepIntervalMaxMs);
267+
}
268+
}
269+
224270
private static class BlockingHandshakeConnector
225271
implements PipeConnector, PipeConnectorWithEventDiscard {
226272

0 commit comments

Comments
 (0)