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 574d5eb6278 Implement UDP transport for air-gap sink (#17954)
574d5eb6278 is described below
commit 574d5eb6278e6f34aea9d92280e126bdc297bb38
Author: Caideyipi <[email protected]>
AuthorDate: Fri Oct 30 09:31:01 2026 +0800
Implement UDP transport for air-gap sink (#17954)
* Implement UDP transport for air-gap sink
* Update IoTDBDataRegionAirGapSinkTest.java
* Reuse thread pool
* Fix air-gap receiver Sonar issues
* Fix air gap receiver retry test
* Fix air-gap UDP ACK correlation and session cleanup
* Address air-gap UDP review comments
---
.../apache/iotdb/db/i18n/DataNodePipeMessages.java | 4 +
.../apache/iotdb/db/i18n/DataNodePipeMessages.java | 4 +
.../protocol/airgap/IoTDBAirGapReceiver.java | 163 +++++++++---
.../protocol/airgap/IoTDBAirGapReceiverAgent.java | 177 ++++++++++++-
.../thrift/IoTDBDataNodeReceiverAgent.java | 24 +-
.../airgap/IoTDBAirGapReceiverAgentTest.java | 120 +++++++++
.../protocol/airgap/IoTDBAirGapReceiverTest.java | 110 +++++++-
.../airgap/IoTDBDataRegionAirGapSinkTest.java | 238 ++++++++++++++++-
.../apache/iotdb/commons/i18n/PipeMessages.java | 8 +
.../apache/iotdb/commons/i18n/PipeMessages.java | 8 +
.../pipe/config/constant/PipeSinkConstant.java | 20 ++
.../pipe/sink/protocol/IoTDBAirGapSink.java | 290 ++++++++++++++++++++-
12 files changed, 1113 insertions(+), 53 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 cf67838025e..2c33424cf99 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
@@ -1323,6 +1323,10 @@ public final class DataNodePipeMessages {
"Pipe air gap receiver {} started. Socket: {}";
public static final String PIPE_AIR_GAP_RECEIVER_TEMPORARY_UNAVAILABLE_RETRY
=
"Pipe air gap receiver {}: Temporary unavailable retry timed out,
returning FAIL to sender.";
+ public static final String
EXCEPTION_AIR_GAP_UDP_PACKET_IS_MISSING_ITS_REQUEST_ID_C4228CC8 =
+ "Air-gap UDP packet is missing its request ID.";
+ public static final String
EXCEPTION_AIR_GAP_UDP_PACKET_CONTAINS_TRAILING_BYTES_2D34D418 =
+ "Air-gap UDP packet contains trailing bytes.";
public static final String PIPE_DATA_TRANSPORT_ERROR = "Pipe data transport
error, {}";
public static final String PIPE_INSERTING_ROW_CASTING_TYPE_FROM =
"Pipe: Inserting row. Casting type from {} to {}.";
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 4e1e7a0f2fd..064fb8b4032 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
@@ -1242,6 +1242,10 @@ public final class DataNodePipeMessages {
"Pipe air gap receiver {}:监听数据时 socket {} 关闭。原因:{}";
public static final String PIPE_AIR_GAP_RECEIVER_STARTED_SOCKET =
"Pipe air gap receiver {} 已启动。Socket:{}";
+ public static final String
EXCEPTION_AIR_GAP_UDP_PACKET_IS_MISSING_ITS_REQUEST_ID_C4228CC8 =
+ "Air-gap UDP 数据包缺少 request ID。";
+ public static final String
EXCEPTION_AIR_GAP_UDP_PACKET_CONTAINS_TRAILING_BYTES_2D34D418 =
+ "Air-gap UDP 数据包包含多余字节。";
public static final String PIPE_AIR_GAP_RECEIVER_TEMPORARY_UNAVAILABLE_RETRY
=
"Pipe air gap receiver {}:临时不可用重试超时,向 sender 返回 FAIL。";
public static final String PIPE_DATA_TRANSPORT_ERROR = "Pipe 数据传输错误,{}";
diff --git
a/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/pipe/receiver/protocol/airgap/IoTDBAirGapReceiver.java
b/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/pipe/receiver/protocol/airgap/IoTDBAirGapReceiver.java
index 48fb42d27c2..e3413b009b2 100644
---
a/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/pipe/receiver/protocol/airgap/IoTDBAirGapReceiver.java
+++
b/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/pipe/receiver/protocol/airgap/IoTDBAirGapReceiver.java
@@ -40,11 +40,15 @@ import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.io.BufferedInputStream;
+import java.io.ByteArrayInputStream;
import java.io.FilterInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
+import java.net.DatagramPacket;
+import java.net.DatagramSocket;
import java.net.Socket;
+import java.net.SocketAddress;
import java.net.SocketTimeoutException;
import java.nio.ByteBuffer;
import java.util.Arrays;
@@ -63,6 +67,10 @@ public class IoTDBAirGapReceiver extends WrappedRunnable {
private final IoTDBDataNodeReceiverAgent agent;
private boolean isELanguagePayload;
+ private OutputStream currentOutputStream;
+ private DatagramSocket currentDatagramSocket;
+ private SocketAddress currentRemoteSocketAddress;
+ private Long currentDatagramRequestId;
public IoTDBAirGapReceiver(final Socket socket, final long receiverId) {
this.socket = socket;
@@ -105,33 +113,11 @@ public class IoTDBAirGapReceiver extends WrappedRunnable {
try {
final byte[] data = readData(inputStream);
+ currentOutputStream = socket.getOutputStream();
- // If check sum failed, it indicates that the length we read may not be
correct.
- // Namely, there may be remaining bytes in the socket stream, which will
fail any subsequent
- // attempts to read from that.
- // We directly close the socket here.
- if (!checkSum(data)) {
- LOGGER.warn(
- DataNodePipeMessages.PIPE_AIR_GAP_RECEIVER_CLOSED_BECAUSE_OF,
receiverId, socket);
- try {
- fail();
- } finally {
- socket.close();
- }
- return;
+ if (!receive(data, null)) {
+ socket.close();
}
-
- // Removed the used checksum
- final ByteBuffer byteBuffer = ByteBuffer.wrap(data, LONG_LEN,
data.length - LONG_LEN);
-
- // Pseudo request, to reuse logic in IoTDBThriftReceiverAgent
- final AirGapPseudoTPipeTransferRequest req =
- (AirGapPseudoTPipeTransferRequest)
- new AirGapPseudoTPipeTransferRequest()
- .setVersion(ReadWriteIOUtils.readByte(byteBuffer))
- .setType(ReadWriteIOUtils.readShort(byteBuffer))
- .setBody(byteBuffer.slice());
- handleReq(req, System.currentTimeMillis());
} catch (final SocketTimeoutException e) {
// It is normal for an air gap sender to remain idle. Only close the
connection when the
// timeout occurs after a request has started, because the stream can no
longer be decoded
@@ -161,9 +147,103 @@ public class IoTDBAirGapReceiver extends WrappedRunnable {
}
}
- private void handleReq(final AirGapPseudoTPipeTransferRequest req, final
long startTime)
+ boolean receive(final byte[] data, final String receiverKey) throws
IOException {
+ return receive(data, receiverKey, null);
+ }
+
+ private boolean receive(
+ final byte[] data, final String receiverKey, final Runnable
onValidRequest)
throws IOException {
- final TPipeTransferResp resp = agent.receive(duplicateReq(req));
+ try {
+ // If check sum failed, it indicates that the length we read may not be
correct.
+ // Namely, there may be remaining bytes in the socket stream, which will
fail any subsequent
+ // attempts to read from that.
+ if (!checkSum(data)) {
+ LOGGER.warn(
+ DataNodePipeMessages.PIPE_AIR_GAP_RECEIVER_CLOSED_BECAUSE_OF,
receiverId, socket);
+ fail();
+ return false;
+ }
+
+ if (onValidRequest != null) {
+ onValidRequest.run();
+ }
+ handleReq(toTPipeTransferReq(data), System.currentTimeMillis(),
receiverKey);
+ return true;
+ } catch (final IOException e) {
+ throw e;
+ } catch (final Exception e) {
+ if (currentDatagramSocket != null) {
+ fail();
+ }
+ throw e;
+ } finally {
+ currentOutputStream = null;
+ currentDatagramSocket = null;
+ currentRemoteSocketAddress = null;
+ currentDatagramRequestId = null;
+ }
+ }
+
+ void receiveUdp(
+ final DatagramSocket datagramSocket,
+ final DatagramPacket packet,
+ final String receiverKey,
+ final byte[] buffer,
+ final Runnable onValidRequest)
+ throws IOException {
+ isELanguagePayload = false;
+ currentDatagramSocket = datagramSocket;
+ currentRemoteSocketAddress = packet.getSocketAddress();
+ boolean requestHandlingStarted = false;
+ try {
+ if (packet.getLength() < Long.BYTES) {
+ throw new IOException(
+
DataNodePipeMessages.EXCEPTION_AIR_GAP_UDP_PACKET_IS_MISSING_ITS_REQUEST_ID_C4228CC8);
+ }
+ currentDatagramRequestId = ByteBuffer.wrap(buffer, 0,
Long.BYTES).getLong();
+ try (final ByteArrayInputStream inputStream =
+ new ByteArrayInputStream(buffer, Long.BYTES, packet.getLength() -
Long.BYTES)) {
+ final byte[] data = readData(inputStream);
+ if (inputStream.available() != 0) {
+ throw new IOException(
+
DataNodePipeMessages.EXCEPTION_AIR_GAP_UDP_PACKET_CONTAINS_TRAILING_BYTES_2D34D418);
+ }
+ requestHandlingStarted = true;
+ receive(data, receiverKey, onValidRequest);
+ }
+ } catch (final Exception e) {
+ if (!requestHandlingStarted) {
+ fail();
+ }
+ throw e;
+ } finally {
+ currentOutputStream = null;
+ currentDatagramSocket = null;
+ currentRemoteSocketAddress = null;
+ currentDatagramRequestId = null;
+ }
+ }
+
+ private AirGapPseudoTPipeTransferRequest toTPipeTransferReq(final byte[]
data) {
+ // Removed the used checksum
+ final ByteBuffer byteBuffer = ByteBuffer.wrap(data, LONG_LEN, data.length
- LONG_LEN);
+
+ // Pseudo request, to reuse logic in IoTDBThriftReceiverAgent
+ return (AirGapPseudoTPipeTransferRequest)
+ new AirGapPseudoTPipeTransferRequest()
+ .setVersion(ReadWriteIOUtils.readByte(byteBuffer))
+ .setType(ReadWriteIOUtils.readShort(byteBuffer))
+ .setBody(byteBuffer.slice());
+ }
+
+ private void handleReq(
+ final AirGapPseudoTPipeTransferRequest req, final long startTime, final
String receiverKey)
+ throws IOException {
+ final TPipeTransferResp resp =
+ receiverKey == null
+ ? agent.receive(duplicateReq(req))
+ : agent.receive(receiverKey, duplicateReq(req));
final TSStatus status = resp.getStatus();
if (status.getCode() == TSStatusCode.SUCCESS_STATUS.getStatusCode()) {
@@ -181,7 +261,7 @@ public class IoTDBAirGapReceiver extends WrappedRunnable {
}
if (System.currentTimeMillis() - startTime
< PipeConfig.getInstance().getPipeAirGapRetryMaxMs()) {
- handleReq(req, startTime);
+ handleReq(req, startTime, receiverKey);
} else {
LOGGER.warn(
DataNodePipeMessages.PIPE_AIR_GAP_RECEIVER_TEMPORARY_UNAVAILABLE_RETRY,
receiverId);
@@ -207,14 +287,31 @@ public class IoTDBAirGapReceiver extends WrappedRunnable {
}
private void ok() throws IOException {
- final OutputStream outputStream = socket.getOutputStream();
- outputStream.write(AirGapOneByteResponse.OK);
- outputStream.flush();
+ respond(AirGapOneByteResponse.OK);
}
private void fail() throws IOException {
- final OutputStream outputStream = socket.getOutputStream();
- outputStream.write(AirGapOneByteResponse.FAIL);
+ respond(AirGapOneByteResponse.FAIL);
+ }
+
+ private void respond(final byte[] response) throws IOException {
+ if (currentDatagramSocket != null && currentRemoteSocketAddress != null) {
+ if (currentDatagramRequestId == null) {
+ return;
+ }
+ final byte[] responseBytes =
+ ByteBuffer.allocate(Long.BYTES + response.length)
+ .putLong(currentDatagramRequestId)
+ .put(response)
+ .array();
+ currentDatagramSocket.send(
+ new DatagramPacket(responseBytes, responseBytes.length,
currentRemoteSocketAddress));
+ return;
+ }
+
+ final OutputStream outputStream =
+ currentOutputStream != null ? currentOutputStream :
socket.getOutputStream();
+ outputStream.write(response);
outputStream.flush();
}
diff --git
a/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/pipe/receiver/protocol/airgap/IoTDBAirGapReceiverAgent.java
b/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/pipe/receiver/protocol/airgap/IoTDBAirGapReceiverAgent.java
index c14ce2054b7..87683081d9b 100644
---
a/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/pipe/receiver/protocol/airgap/IoTDBAirGapReceiverAgent.java
+++
b/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/pipe/receiver/protocol/airgap/IoTDBAirGapReceiverAgent.java
@@ -26,30 +26,46 @@ import org.apache.iotdb.commons.pipe.config.PipeConfig;
import org.apache.iotdb.commons.service.IService;
import org.apache.iotdb.commons.service.ServiceType;
import org.apache.iotdb.db.i18n.DataNodePipeMessages;
+import org.apache.iotdb.db.pipe.agent.PipeDataNodeAgent;
+import org.apache.iotdb.db.protocol.session.ClientSession;
+import org.apache.iotdb.db.protocol.session.SessionManager;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.io.IOException;
+import java.net.DatagramPacket;
+import java.net.DatagramSocket;
+import java.net.InetAddress;
import java.net.ServerSocket;
import java.net.Socket;
+import java.net.SocketTimeoutException;
+import java.util.Map;
import java.util.Objects;
+import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ExecutorService;
+import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.concurrent.atomic.AtomicLong;
public class IoTDBAirGapReceiverAgent implements IService {
private static final Logger LOGGER =
LoggerFactory.getLogger(IoTDBAirGapReceiverAgent.class);
+ private static final int UDP_PACKET_MAX_SIZE_IN_BYTES = 65_507;
+ private static final int UDP_SESSION_CLEANUP_INTERVAL_MS = 60_000;
+ private static final int UDP_MAX_CLIENT_SESSIONS = 1_024;
+ private static final int LISTENER_THREAD_COUNT = 2;
private final ExecutorService listenExecutor =
- IoTDBThreadPoolFactory.newSingleThreadExecutor(
- ThreadName.PIPE_RECEIVER_AIR_GAP_AGENT.getName());
+ IoTDBThreadPoolFactory.newFixedThreadPool(
+ LISTENER_THREAD_COUNT,
ThreadName.PIPE_RECEIVER_AIR_GAP_AGENT.getName());
private final AtomicBoolean allowSubmitListen = new AtomicBoolean(false);
private ServerSocket serverSocket;
+ private DatagramSocket datagramSocket;
private final AtomicLong receiverId = new AtomicLong(0);
+ final Map<String, UdpClientSession> udpClientSessions = new
ConcurrentHashMap<>();
public void listen() {
try {
@@ -61,7 +77,9 @@ public class IoTDBAirGapReceiverAgent implements IService {
ThreadName.PIPE_AIR_GAP_RECEIVER.getName() + "-" + airGapReceiverId);
airGapReceiverThread.start();
} catch (final IOException e) {
-
LOGGER.warn(DataNodePipeMessages.UNHANDLED_EXCEPTION_DURING_PIPE_AIR_GAP_RECEIVER,
e);
+ if (allowSubmitListen.get()) {
+
LOGGER.warn(DataNodePipeMessages.UNHANDLED_EXCEPTION_DURING_PIPE_AIR_GAP_RECEIVER,
e);
+ }
}
if (allowSubmitListen.get()) {
@@ -69,31 +87,150 @@ public class IoTDBAirGapReceiverAgent implements IService {
}
}
+ public void listenUdp() {
+ while (allowSubmitListen.get() && !datagramSocket.isClosed()) {
+ removeExpiredUdpClientSessions(System.nanoTime());
+ final byte[] buffer = new byte[UDP_PACKET_MAX_SIZE_IN_BYTES];
+ final DatagramPacket packet = new DatagramPacket(buffer, buffer.length);
+ try {
+ datagramSocket.receive(packet);
+
+ final long airGapReceiverId = receiverId.incrementAndGet();
+ final IoTDBAirGapReceiver receiver =
+ new IoTDBAirGapReceiver(new Socket(), airGapReceiverId);
+ final String receiverKey = packet.getSocketAddress().toString();
+ final boolean[] registeredSession = {false};
+ try {
+ receiver.receiveUdp(
+ datagramSocket,
+ packet,
+ receiverKey,
+ buffer,
+ () -> registeredSession[0] =
registerUdpSessionIfNecessary(packet));
+ } finally {
+ if (registeredSession[0]) {
+ SessionManager.getInstance().removeCurrSession();
+ }
+ }
+ } catch (final SocketTimeoutException e) {
+ removeExpiredUdpClientSessions(System.nanoTime());
+ } catch (final IOException e) {
+ if (allowSubmitListen.get()) {
+
LOGGER.warn(DataNodePipeMessages.UNHANDLED_EXCEPTION_DURING_PIPE_AIR_GAP_RECEIVER,
e);
+ }
+ } catch (final Exception e) {
+
LOGGER.warn(DataNodePipeMessages.UNHANDLED_EXCEPTION_DURING_PIPE_AIR_GAP_RECEIVER,
e);
+ }
+ }
+ }
+
+ boolean registerUdpSessionIfNecessary(final DatagramPacket packet) {
+ final String receiverKey = packet.getSocketAddress().toString();
+ final long now = System.nanoTime();
+ removeExpiredUdpClientSessions(now);
+
+ UdpClientSession clientSession = udpClientSessions.get(receiverKey);
+ if (clientSession == null) {
+ ensureUdpClientSessionCapacity();
+ clientSession =
+ new UdpClientSession(
+ new ClientSession(new DatagramClientSocket(packet.getAddress(),
packet.getPort())),
+ now);
+ final UdpClientSession existing =
udpClientSessions.putIfAbsent(receiverKey, clientSession);
+ if (existing != null) {
+ clientSession = existing;
+ }
+ }
+ clientSession.lastAccessTimeNanos = now;
+ return SessionManager.getInstance().registerSession(clientSession.session);
+ }
+
+ private void ensureUdpClientSessionCapacity() {
+ if (udpClientSessions.size() < UDP_MAX_CLIENT_SESSIONS) {
+ return;
+ }
+
+ String leastRecentlyUsedKey = null;
+ UdpClientSession leastRecentlyUsedSession = null;
+ for (final Map.Entry<String, UdpClientSession> entry :
udpClientSessions.entrySet()) {
+ if (leastRecentlyUsedSession == null
+ || entry.getValue().lastAccessTimeNanos <
leastRecentlyUsedSession.lastAccessTimeNanos) {
+ leastRecentlyUsedKey = entry.getKey();
+ leastRecentlyUsedSession = entry.getValue();
+ }
+ }
+ if (leastRecentlyUsedKey != null) {
+ removeUdpClientSession(leastRecentlyUsedKey, leastRecentlyUsedSession);
+ }
+ }
+
+ void removeExpiredUdpClientSessions(final long nowNanos) {
+ final long idleTimeoutNanos =
+ TimeUnit.MILLISECONDS.toNanos(
+ Math.max(1,
PipeConfig.getInstance().getPipeSinkTransferTimeoutMs()));
+ udpClientSessions.forEach(
+ (key, session) -> {
+ if (nowNanos - session.lastAccessTimeNanos >= idleTimeoutNanos) {
+ removeUdpClientSession(key, session);
+ }
+ });
+ }
+
+ private void removeUdpClientSession(final String key, final UdpClientSession
session) {
+ if (!udpClientSessions.remove(key, session)) {
+ return;
+ }
+
+ final boolean registeredSession =
SessionManager.getInstance().registerSession(session.session);
+ try {
+ PipeDataNodeAgent.receiver().thrift().handleClientExit(key);
+ } finally {
+ if (registeredSession) {
+ SessionManager.getInstance().removeCurrSession();
+ }
+ }
+ }
+
@Override
public void start() throws StartupException {
try {
serverSocket = new
ServerSocket(PipeConfig.getInstance().getPipeAirGapReceiverPort());
+ datagramSocket = new
DatagramSocket(PipeConfig.getInstance().getPipeAirGapReceiverPort());
+ datagramSocket.setSoTimeout(UDP_SESSION_CLEANUP_INTERVAL_MS);
} catch (final IOException e) {
+ if (Objects.nonNull(serverSocket)) {
+ try {
+ serverSocket.close();
+ } catch (final IOException closeException) {
+ e.addSuppressed(closeException);
+ }
+ }
throw new StartupException(e);
}
allowSubmitListen.set(true);
listenExecutor.submit(this::listen);
+ listenExecutor.submit(this::listenUdp);
LOGGER.info(DataNodePipeMessages.IOTDBAIRGAPRECEIVERAGENT_STARTED,
serverSocket);
}
@Override
public void stop() {
+ allowSubmitListen.set(false);
+
try {
if (Objects.nonNull(serverSocket)) {
serverSocket.close();
}
+ if (Objects.nonNull(datagramSocket)) {
+ datagramSocket.close();
+ }
} catch (final IOException e) {
LOGGER.warn(DataNodePipeMessages.FAILED_TO_CLOSE_IOTDBAIRGAPRECEIVERAGENT_S_SERVER_SOCKET,
e);
}
- allowSubmitListen.set(false);
+ udpClientSessions.forEach(this::removeUdpClientSession);
listenExecutor.shutdown();
LOGGER.info(DataNodePipeMessages.IOTDBAIRGAPRECEIVERAGENT_STOPPED,
serverSocket);
@@ -103,4 +240,36 @@ public class IoTDBAirGapReceiverAgent implements IService {
public ServiceType getID() {
return ServiceType.AIR_GAP_SERVICE;
}
+
+ private static class DatagramClientSocket extends Socket {
+
+ private final InetAddress address;
+ private final int port;
+
+ private DatagramClientSocket(final InetAddress address, final int port) {
+ this.address = address;
+ this.port = port;
+ }
+
+ @Override
+ public InetAddress getInetAddress() {
+ return address;
+ }
+
+ @Override
+ public int getPort() {
+ return port;
+ }
+ }
+
+ static class UdpClientSession {
+
+ private final ClientSession session;
+ volatile long lastAccessTimeNanos;
+
+ UdpClientSession(final ClientSession session, final long
lastAccessTimeNanos) {
+ this.session = session;
+ this.lastAccessTimeNanos = lastAccessTimeNanos;
+ }
+ }
}
diff --git
a/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/pipe/receiver/protocol/thrift/IoTDBDataNodeReceiverAgent.java
b/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/pipe/receiver/protocol/thrift/IoTDBDataNodeReceiverAgent.java
index 8a995f7656d..57ed64ec520 100644
---
a/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/pipe/receiver/protocol/thrift/IoTDBDataNodeReceiverAgent.java
+++
b/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/pipe/receiver/protocol/thrift/IoTDBDataNodeReceiverAgent.java
@@ -24,9 +24,13 @@ import
org.apache.iotdb.commons.pipe.receiver.IoTDBReceiverAgent;
import
org.apache.iotdb.commons.pipe.sink.payload.thrift.request.IoTDBSinkRequestVersion;
import
org.apache.iotdb.db.pipe.processor.twostage.exchange.receiver.TwoStageAggregateReceiver;
+import java.util.Map;
+import java.util.concurrent.ConcurrentHashMap;
+
public class IoTDBDataNodeReceiverAgent extends IoTDBReceiverAgent {
private final ThreadLocal<IoTDBReceiver> receiverThreadLocal = new
ThreadLocal<>();
+ private final Map<String, IoTDBReceiver> receiverMap = new
ConcurrentHashMap<>();
@Override
protected void initConstructors() {
@@ -37,17 +41,25 @@ public class IoTDBDataNodeReceiverAgent extends
IoTDBReceiverAgent {
}
@Override
- protected IoTDBReceiver getReceiverWithSpecifiedClient(final String ignore) {
- return receiverThreadLocal.get();
+ protected IoTDBReceiver getReceiverWithSpecifiedClient(final String key) {
+ return key == null ? receiverThreadLocal.get() : receiverMap.get(key);
}
@Override
- protected void setReceiverWithSpecifiedClient(final String ignore, final
IoTDBReceiver receiver) {
- receiverThreadLocal.set(receiver);
+ protected void setReceiverWithSpecifiedClient(final String key, final
IoTDBReceiver receiver) {
+ if (key == null) {
+ receiverThreadLocal.set(receiver);
+ } else {
+ receiverMap.put(key, receiver);
+ }
}
@Override
- protected void removeReceiverWithSpecifiedClient(final String ignore) {
- receiverThreadLocal.remove();
+ protected void removeReceiverWithSpecifiedClient(final String key) {
+ if (key == null) {
+ receiverThreadLocal.remove();
+ } else {
+ receiverMap.remove(key);
+ }
}
}
diff --git
a/iotdb-core/datanode/src/test/java/org/apache/iotdb/db/pipe/receiver/protocol/airgap/IoTDBAirGapReceiverAgentTest.java
b/iotdb-core/datanode/src/test/java/org/apache/iotdb/db/pipe/receiver/protocol/airgap/IoTDBAirGapReceiverAgentTest.java
new file mode 100644
index 00000000000..ffbebb5a668
--- /dev/null
+++
b/iotdb-core/datanode/src/test/java/org/apache/iotdb/db/pipe/receiver/protocol/airgap/IoTDBAirGapReceiverAgentTest.java
@@ -0,0 +1,120 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.apache.iotdb.db.pipe.receiver.protocol.airgap;
+
+import org.apache.iotdb.common.rpc.thrift.TSStatus;
+import org.apache.iotdb.commons.pipe.config.PipeConfig;
+import org.apache.iotdb.commons.pipe.receiver.IoTDBReceiver;
+import
org.apache.iotdb.commons.pipe.sink.payload.thrift.request.IoTDBSinkRequestVersion;
+import org.apache.iotdb.db.pipe.agent.PipeDataNodeAgent;
+import
org.apache.iotdb.db.pipe.receiver.protocol.thrift.IoTDBDataNodeReceiverAgent;
+import org.apache.iotdb.db.protocol.session.ClientSession;
+import org.apache.iotdb.rpc.TSStatusCode;
+import org.apache.iotdb.service.rpc.thrift.TPipeTransferReq;
+import org.apache.iotdb.service.rpc.thrift.TPipeTransferResp;
+
+import org.junit.Assert;
+import org.junit.Test;
+
+import java.io.IOException;
+import java.lang.reflect.Field;
+import java.net.DatagramPacket;
+import java.net.DatagramSocket;
+import java.net.InetAddress;
+import java.net.Socket;
+import java.util.Map;
+import java.util.concurrent.TimeUnit;
+import java.util.concurrent.atomic.AtomicBoolean;
+
+public class IoTDBAirGapReceiverAgentTest {
+
+ @Test
+ public void testExpiredUdpClientSessionIsRemoved() throws Exception {
+ final IoTDBAirGapReceiverAgent agent = new IoTDBAirGapReceiverAgent();
+ final String sessionKey = "expired-client";
+ final IoTDBAirGapReceiverAgent.UdpClientSession session =
+ new IoTDBAirGapReceiverAgent.UdpClientSession(new ClientSession(new
Socket()), 0L);
+ agent.udpClientSessions.put(sessionKey, session);
+
+ final AtomicBoolean receiverExited = new AtomicBoolean(false);
+ final IoTDBDataNodeReceiverAgent receiverAgent =
PipeDataNodeAgent.receiver().thrift();
+ final Field receiverMapField =
IoTDBDataNodeReceiverAgent.class.getDeclaredField("receiverMap");
+ receiverMapField.setAccessible(true);
+ @SuppressWarnings("unchecked")
+ final Map<String, IoTDBReceiver> receiverMap =
+ (Map<String, IoTDBReceiver>) receiverMapField.get(receiverAgent);
+ receiverMap.put(
+ sessionKey,
+ new IoTDBReceiver() {
+ @Override
+ public TPipeTransferResp receive(final TPipeTransferReq req) {
+ return new TPipeTransferResp(new
TSStatus(TSStatusCode.SUCCESS_STATUS.getStatusCode()));
+ }
+
+ @Override
+ public void handleExit() {
+ receiverExited.set(true);
+ }
+
+ @Override
+ public IoTDBSinkRequestVersion getVersion() {
+ return IoTDBSinkRequestVersion.VERSION_1;
+ }
+ });
+
+ try {
+ final long timeoutNanos =
+
TimeUnit.MILLISECONDS.toNanos(PipeConfig.getInstance().getPipeSinkTransferTimeoutMs());
+ agent.removeExpiredUdpClientSessions(timeoutNanos + 1);
+
+ Assert.assertTrue(agent.udpClientSessions.isEmpty());
+ Assert.assertTrue(receiverExited.get());
+ } finally {
+ receiverMap.remove(sessionKey);
+ }
+ }
+
+ @Test
+ public void testMalformedUdpPacketDoesNotCreateClientSession() throws
Exception {
+ try (final DatagramSocket datagramSocket = new DatagramSocket()) {
+ final IoTDBAirGapReceiver receiver = new IoTDBAirGapReceiver(new
Socket(), 1L);
+ final byte[] malformedPacket = new byte[] {1};
+ final DatagramPacket packet =
+ new DatagramPacket(
+ malformedPacket,
+ malformedPacket.length,
+ InetAddress.getLoopbackAddress(),
+ datagramSocket.getLocalPort());
+ final AtomicBoolean sessionRegistrationAttempted = new
AtomicBoolean(false);
+
+ Assert.assertThrows(
+ IOException.class,
+ () ->
+ receiver.receiveUdp(
+ datagramSocket,
+ packet,
+ "invalid-client",
+ malformedPacket,
+ () -> sessionRegistrationAttempted.set(true)));
+
+ Assert.assertFalse(sessionRegistrationAttempted.get());
+ }
+ }
+}
diff --git
a/iotdb-core/datanode/src/test/java/org/apache/iotdb/db/pipe/receiver/protocol/airgap/IoTDBAirGapReceiverTest.java
b/iotdb-core/datanode/src/test/java/org/apache/iotdb/db/pipe/receiver/protocol/airgap/IoTDBAirGapReceiverTest.java
index d7e69b51cfd..57cacb38799 100644
---
a/iotdb-core/datanode/src/test/java/org/apache/iotdb/db/pipe/receiver/protocol/airgap/IoTDBAirGapReceiverTest.java
+++
b/iotdb-core/datanode/src/test/java/org/apache/iotdb/db/pipe/receiver/protocol/airgap/IoTDBAirGapReceiverTest.java
@@ -36,6 +36,7 @@ import org.apache.iotdb.service.rpc.thrift.TPipeTransferReq;
import org.apache.iotdb.service.rpc.thrift.TPipeTransferResp;
import org.apache.tsfile.utils.BytesUtils;
+import org.apache.tsfile.utils.ReadWriteIOUtils;
import org.junit.After;
import org.junit.Assert;
import org.junit.Before;
@@ -43,18 +44,23 @@ import org.junit.Test;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
+import java.io.DataOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.lang.reflect.Field;
import java.lang.reflect.Method;
+import java.net.DatagramPacket;
+import java.net.DatagramSocket;
import java.net.InetAddress;
import java.net.Socket;
import java.net.SocketException;
import java.net.SocketTimeoutException;
import java.nio.ByteBuffer;
+import java.util.Arrays;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.concurrent.atomic.AtomicInteger;
+import java.util.zip.CRC32;
public class IoTDBAirGapReceiverTest {
@@ -136,9 +142,9 @@ public class IoTDBAirGapReceiverTest {
final Method handleReq =
IoTDBAirGapReceiver.class.getDeclaredMethod(
- "handleReq", AirGapPseudoTPipeTransferRequest.class, long.class);
+ "handleReq", AirGapPseudoTPipeTransferRequest.class, long.class,
String.class);
handleReq.setAccessible(true);
- handleReq.invoke(receiver, req, System.currentTimeMillis() - 10_000L);
+ handleReq.invoke(receiver, req, System.currentTimeMillis() - 10_000L,
null);
Assert.assertArrayEquals(AirGapOneByteResponse.FAIL,
socket.getWrittenBytes());
} finally {
@@ -147,8 +153,68 @@ public class IoTDBAirGapReceiverTest {
}
}
+ @Test
+ public void testReceiveAirGapPayloadWithSpecifiedReceiverKey() throws
Exception {
+ final RecordingSocket socket = new RecordingSocket();
+ final IoTDBAirGapReceiver receiver = new IoTDBAirGapReceiver(socket, 4L);
+ final StubIoTDBDataNodeReceiverAgent stubAgent = new
StubIoTDBDataNodeReceiverAgent();
+ stubAgent.setStubReceiver(
+ "udp-client", new StubReceiver(new
TSStatus(TSStatusCode.SUCCESS_STATUS.getStatusCode())));
+ setField(receiver, "agent", stubAgent);
+
+ Assert.assertTrue(receiver.receive(toAirGapData(toRawRequestBytes()),
"udp-client"));
+ Assert.assertArrayEquals(AirGapOneByteResponse.OK,
socket.getWrittenBytes());
+ }
+
+ @Test
+ public void testUdpResponseContainsRequestId() throws Exception {
+ try (final DatagramSocket receiverSocket = new DatagramSocket(0);
+ final DatagramSocket senderSocket = new DatagramSocket()) {
+ final long requestId = 73L;
+ final byte[] request = toUdpDatagram(requestId, toRawRequestBytes());
+ senderSocket.send(
+ new DatagramPacket(
+ request,
+ request.length,
+ InetAddress.getLoopbackAddress(),
+ receiverSocket.getLocalPort()));
+
+ final byte[] buffer = new byte[65_507];
+ final DatagramPacket packet = new DatagramPacket(buffer, buffer.length);
+ receiverSocket.receive(packet);
+
+ final RecordingSocket socket = new RecordingSocket();
+ final IoTDBAirGapReceiver receiver = new IoTDBAirGapReceiver(socket, 5L);
+ final StubIoTDBDataNodeReceiverAgent stubAgent = new
StubIoTDBDataNodeReceiverAgent();
+ stubAgent.setStubReceiver(
+ "udp-client",
+ new StubReceiver(new
TSStatus(TSStatusCode.SUCCESS_STATUS.getStatusCode())));
+ setField(receiver, "agent", stubAgent);
+ receiver.receiveUdp(receiverSocket, packet, "udp-client", buffer, () ->
{});
+
+ final byte[] response = new byte[Long.BYTES + 2];
+ final DatagramPacket responsePacket = new DatagramPacket(response,
response.length);
+ senderSocket.setSoTimeout(1000);
+ senderSocket.receive(responsePacket);
+ Assert.assertEquals(Long.BYTES + 1, responsePacket.getLength());
+ Assert.assertEquals(requestId, ByteBuffer.wrap(response).getLong());
+ Assert.assertEquals(AirGapOneByteResponse.OK[0], response[Long.BYTES]);
+ }
+ }
+
@Test
public void testTemporaryUnavailableRetryUsesFreshRequestBody() throws
Exception {
+ assertTemporaryUnavailableRetryUsesFreshRequestBody(null);
+ }
+
+ @Test
+ public void
testTemporaryUnavailableRetryUsesFreshRequestBodyWithSpecifiedReceiverKey()
+ throws Exception {
+ assertTemporaryUnavailableRetryUsesFreshRequestBody("udp-client");
+ }
+
+ private void assertTemporaryUnavailableRetryUsesFreshRequestBody(final
String receiverKey)
+ throws Exception {
final CommonConfig commonConfig =
CommonDescriptor.getInstance().getConfig();
final long originalRetryLocalIntervalMs =
commonConfig.getPipeAirGapRetryLocalIntervalMs();
final long originalRetryMaxMs = commonConfig.getPipeAirGapRetryMaxMs();
@@ -163,6 +229,7 @@ public class IoTDBAirGapReceiverTest {
final byte[] expectedBody = new byte[] {1, 2, 3};
final AtomicInteger receiveCount = new AtomicInteger();
stubAgent.setStubReceiver(
+ receiverKey,
new IoTDBReceiver() {
@Override
public TPipeTransferResp receive(final TPipeTransferReq req) {
@@ -196,9 +263,9 @@ public class IoTDBAirGapReceiverTest {
final Method handleReq =
IoTDBAirGapReceiver.class.getDeclaredMethod(
- "handleReq", AirGapPseudoTPipeTransferRequest.class, long.class);
+ "handleReq", AirGapPseudoTPipeTransferRequest.class, long.class,
String.class);
handleReq.setAccessible(true);
- handleReq.invoke(receiver, req, System.currentTimeMillis());
+ handleReq.invoke(receiver, req, System.currentTimeMillis(), receiverKey);
Assert.assertEquals(2, receiveCount.get());
Assert.assertEquals(0, req.body.position());
@@ -239,7 +306,7 @@ public class IoTDBAirGapReceiverTest {
@Test
public void testIdleReadTimeoutDoesNotRespondOrCloseSocket() throws
Exception {
final RecordingSocket socket = new RecordingSocket(new
TimeoutInputStream(new byte[0]));
- invokeReceive(new IoTDBAirGapReceiver(socket, 4L));
+ invokeReceive(new IoTDBAirGapReceiver(socket, 5L));
Assert.assertArrayEquals(new byte[0], socket.getWrittenBytes());
Assert.assertFalse(socket.isClosed());
@@ -249,7 +316,7 @@ public class IoTDBAirGapReceiverTest {
public void testPartialRequestReadTimeoutClosesSocketWithoutResponse()
throws Exception {
final RecordingSocket socket =
new RecordingSocket(new TimeoutInputStream(new byte[] {(byte) 0xFF}));
- invokeReceive(new IoTDBAirGapReceiver(socket, 5L));
+ invokeReceive(new IoTDBAirGapReceiver(socket, 6L));
Assert.assertArrayEquals(new byte[0], socket.getWrittenBytes());
Assert.assertTrue(socket.isClosed());
@@ -387,6 +454,10 @@ public class IoTDBAirGapReceiverTest {
void setStubReceiver(final IoTDBReceiver receiver) {
setReceiverWithSpecifiedClient(null, receiver);
}
+
+ void setStubReceiver(final String key, final IoTDBReceiver receiver) {
+ setReceiverWithSpecifiedClient(key, receiver);
+ }
}
private static class StubReceiver implements IoTDBReceiver {
@@ -413,6 +484,33 @@ public class IoTDBAirGapReceiverTest {
}
}
+ private static byte[] toRawRequestBytes() throws IOException {
+ try (final ByteArrayOutputStream byteArrayOutputStream = new
ByteArrayOutputStream();
+ final DataOutputStream outputStream = new
DataOutputStream(byteArrayOutputStream)) {
+ ReadWriteIOUtils.write(IoTDBSinkRequestVersion.VERSION_1.getVersion(),
outputStream);
+ ReadWriteIOUtils.write((short) 0, outputStream);
+ return byteArrayOutputStream.toByteArray();
+ }
+ }
+
+ private static byte[] toAirGapData(final byte[] rawRequestBytes) {
+ final CRC32 crc32 = new CRC32();
+ crc32.update(rawRequestBytes, 0, rawRequestBytes.length);
+ return BytesUtils.concatByteArrayList(
+ Arrays.asList(BytesUtils.longToBytes(crc32.getValue()),
rawRequestBytes));
+ }
+
+ private static byte[] toUdpDatagram(final long requestId, final byte[]
rawRequestBytes) {
+ final byte[] data = toAirGapData(rawRequestBytes);
+ final ByteBuffer datagram =
+ ByteBuffer.allocate(Long.BYTES + 2 * Integer.BYTES + data.length)
+ .putLong(requestId)
+ .putInt(data.length)
+ .putInt(data.length)
+ .put(data);
+ return datagram.array();
+ }
+
private static class RuntimeCleanupReceiver implements IoTDBReceiver {
private final String sessionKey;
diff --git
a/iotdb-core/datanode/src/test/java/org/apache/iotdb/db/pipe/sink/protocol/airgap/IoTDBDataRegionAirGapSinkTest.java
b/iotdb-core/datanode/src/test/java/org/apache/iotdb/db/pipe/sink/protocol/airgap/IoTDBDataRegionAirGapSinkTest.java
index ca88f30fcc5..82000564cfe 100644
---
a/iotdb-core/datanode/src/test/java/org/apache/iotdb/db/pipe/sink/protocol/airgap/IoTDBDataRegionAirGapSinkTest.java
+++
b/iotdb-core/datanode/src/test/java/org/apache/iotdb/db/pipe/sink/protocol/airgap/IoTDBDataRegionAirGapSinkTest.java
@@ -23,6 +23,7 @@ import
org.apache.iotdb.commons.pipe.agent.plugin.builtin.BuiltinPipePlugin;
import org.apache.iotdb.commons.pipe.config.constant.PipeSinkConstant;
import
org.apache.iotdb.commons.pipe.config.plugin.configuraion.PipeTaskRuntimeConfiguration;
import
org.apache.iotdb.commons.pipe.config.plugin.env.PipeTaskSinkRuntimeEnvironment;
+import org.apache.iotdb.commons.pipe.sink.payload.airgap.AirGapOneByteResponse;
import
org.apache.iotdb.commons.pipe.sink.payload.thrift.request.PipeRequestType;
import org.apache.iotdb.db.pipe.event.common.heartbeat.PipeHeartbeatEvent;
import
org.apache.iotdb.db.pipe.event.common.tablet.PipeRawTabletInsertionEvent;
@@ -42,13 +43,20 @@ import org.junit.Assert;
import org.junit.Test;
import java.io.IOException;
+import java.net.DatagramPacket;
+import java.net.DatagramSocket;
+import java.net.Socket;
+import java.net.SocketTimeoutException;
import java.nio.ByteBuffer;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
+import java.util.concurrent.atomic.AtomicBoolean;
import java.util.concurrent.atomic.AtomicLong;
+import java.util.concurrent.atomic.AtomicReference;
+import java.util.zip.CRC32;
public class IoTDBDataRegionAirGapSinkTest {
@@ -140,7 +148,196 @@ public class IoTDBDataRegionAirGapSinkTest {
}
}
+ @Test
+ public void testUdpAirGapSlicing() throws Exception {
+ try (final DatagramSocket receiverSocket = new DatagramSocket(0);
+ final UdpTestingIoTDBDataRegionAirGapSink sink =
+ new UdpTestingIoTDBDataRegionAirGapSink()) {
+ final Map<String, String> attributes = buildParameterAttributes(false);
+ attributes.put(PipeSinkConstant.CONNECTOR_AIR_GAP_TRANSPORT_KEY, "udp");
+ attributes.put(PipeSinkConstant.CONNECTOR_AIR_GAP_UDP_PACKET_SIZE_KEY,
"1024");
+
+ final PipeParameters parameters = new PipeParameters(attributes);
+ sink.validate(new PipeParameterValidator(parameters));
+ sink.customize(
+ parameters,
+ new PipeTaskRuntimeConfiguration(new
PipeTaskSinkRuntimeEnvironment("pipe", 1L, 1)));
+
+ final List<TPipeTransferReq> receivedRequests = new ArrayList<>();
+ final List<Integer> receivedPacketSizes = new ArrayList<>();
+ final AtomicBoolean senderFinished = new AtomicBoolean(false);
+ final AtomicReference<Throwable> receiverException = new
AtomicReference<>();
+ final Thread receiverThread =
+ new Thread(
+ () -> {
+ try {
+ receiverSocket.setSoTimeout(100);
+ while (!senderFinished.get()) {
+ final byte[] buffer = new byte[2048];
+ final DatagramPacket packet = new DatagramPacket(buffer,
buffer.length);
+ try {
+ receiverSocket.receive(packet);
+ receivedPacketSizes.add(packet.getLength());
+ receivedRequests.add(decodeAirGapDatagram(buffer,
packet.getLength()));
+ final ByteBuffer responseBuffer =
+ ByteBuffer.allocate(Long.BYTES +
AirGapOneByteResponse.OK.length)
+ .putLong(ByteBuffer.wrap(buffer, 0,
Long.BYTES).getLong())
+ .put(AirGapOneByteResponse.OK);
+ receiverSocket.send(
+ new DatagramPacket(
+ responseBuffer.array(),
+ responseBuffer.array().length,
+ packet.getAddress(),
+ packet.getPort()));
+ } catch (final SocketTimeoutException ignored) {
+ // Poll until the sender has finished all datagrams.
+ }
+ }
+ } catch (final Throwable ignored) {
+ receiverException.set(ignored);
+ }
+ });
+ receiverThread.start();
+
+ final byte[] request = new byte[2500];
+ request[0] = 1;
+ final byte[] type =
org.apache.tsfile.utils.BytesUtils.shortToBytes((short) 11);
+ request[1] = type[0];
+ request[2] = type[1];
+
+ try {
+ Assert.assertTrue(sink.sendUdp(receiverSocket.getLocalPort(),
request));
+ } finally {
+ senderFinished.set(true);
+ }
+
+ receiverThread.join(5000);
+ Assert.assertFalse(receiverThread.isAlive());
+ Assert.assertNull(receiverException.get());
+ Assert.assertTrue(receivedRequests.size() > 1);
+ for (final int packetSize : receivedPacketSizes) {
+ Assert.assertTrue(packetSize <= 1024);
+ }
+ for (final TPipeTransferReq receivedRequest : receivedRequests) {
+ Assert.assertEquals(PipeRequestType.TRANSFER_SLICE.getType(),
receivedRequest.type);
+ Assert.assertTrue(receivedRequest.body.remaining() < 1024);
+ }
+ }
+ }
+
+ @Test
+ public void testUdpAirGapRejectsOversizedAcknowledgement() throws Exception {
+ try (final DatagramSocket receiverSocket = new DatagramSocket(0);
+ final UdpTestingIoTDBDataRegionAirGapSink sink =
+ new UdpTestingIoTDBDataRegionAirGapSink()) {
+ final PipeParameters parameters = new
PipeParameters(buildParameterAttributes(false));
+
parameters.getAttribute().put(PipeSinkConstant.CONNECTOR_AIR_GAP_TRANSPORT_KEY,
"udp");
+ sink.validate(new PipeParameterValidator(parameters));
+ sink.customize(
+ parameters,
+ new PipeTaskRuntimeConfiguration(new
PipeTaskSinkRuntimeEnvironment("pipe", 1L, 1)));
+
+ final AtomicBoolean oversizedAcknowledgementSent = new
AtomicBoolean(false);
+ final AtomicReference<Throwable> receiverException = new
AtomicReference<>();
+ final Thread receiverThread =
+ new Thread(
+ () -> {
+ try {
+ final byte[] buffer = new byte[2048];
+ final DatagramPacket packet = new DatagramPacket(buffer,
buffer.length);
+ receiverSocket.receive(packet);
+ final long requestId = ByteBuffer.wrap(buffer, 0,
Long.BYTES).getLong();
+ final byte[] oversizedAck =
+ ByteBuffer.allocate(Long.BYTES +
AirGapOneByteResponse.OK.length + 1)
+ .putLong(requestId)
+ .put(AirGapOneByteResponse.OK)
+ .put((byte) 1)
+ .array();
+ receiverSocket.send(
+ new DatagramPacket(
+ oversizedAck,
+ oversizedAck.length,
+ packet.getAddress(),
+ packet.getPort()));
+ oversizedAcknowledgementSent.set(true);
+ } catch (final Throwable t) {
+ receiverException.set(t);
+ }
+ });
+ receiverThread.start();
+
+ try {
+ Assert.assertThrows(
+ SocketTimeoutException.class,
+ () -> sink.sendUdp(receiverSocket.getLocalPort(), new byte[] {1,
0, 1}));
+ } finally {
+ receiverSocket.close();
+ }
+
+ receiverThread.join(5000);
+ Assert.assertFalse(receiverThread.isAlive());
+ Assert.assertNull(receiverException.get());
+ Assert.assertTrue(oversizedAcknowledgementSent.get());
+ }
+ }
+
+ @Test
+ public void testUdpAirGapIgnoresStaleAcknowledgement() throws Exception {
+ try (final DatagramSocket receiverSocket = new DatagramSocket(0);
+ final UdpTestingIoTDBDataRegionAirGapSink sink =
+ new UdpTestingIoTDBDataRegionAirGapSink()) {
+ final PipeParameters parameters = new
PipeParameters(buildParameterAttributes(false));
+
parameters.getAttribute().put(PipeSinkConstant.CONNECTOR_AIR_GAP_TRANSPORT_KEY,
"udp");
+
parameters.getAttribute().put(PipeSinkConstant.CONNECTOR_AIR_GAP_UDP_PACKET_SIZE_KEY,
"1024");
+ sink.validate(new PipeParameterValidator(parameters));
+ sink.customize(
+ parameters,
+ new PipeTaskRuntimeConfiguration(new
PipeTaskSinkRuntimeEnvironment("pipe", 1L, 1)));
+
+ final AtomicBoolean secondPacketReceived = new AtomicBoolean(false);
+ final AtomicReference<Throwable> receiverException = new
AtomicReference<>();
+ final Thread receiverThread =
+ new Thread(
+ () -> {
+ try {
+ final byte[] buffer = new byte[2048];
+ final DatagramPacket packet = new DatagramPacket(buffer,
buffer.length);
+ receiverSocket.receive(packet);
+ final long requestId = ByteBuffer.wrap(buffer, 0,
Long.BYTES).getLong();
+ sendUdpAck(receiverSocket, packet, requestId);
+ receiverSocket.receive(packet);
+ sendUdpAck(receiverSocket, packet, requestId);
+ secondPacketReceived.set(true);
+ } catch (final Throwable t) {
+ receiverException.set(t);
+ }
+ });
+ receiverThread.start();
+
+ try {
+ final byte[] request = new byte[2500];
+ request[0] = 1;
+ request[1] = 0;
+ request[2] = 1;
+ Assert.assertThrows(
+ SocketTimeoutException.class,
+ () -> sink.sendUdp(receiverSocket.getLocalPort(), request));
+ } finally {
+ receiverSocket.close();
+ }
+
+ receiverThread.join(5000);
+ Assert.assertFalse(receiverThread.isAlive());
+ Assert.assertNull(receiverException.get());
+ Assert.assertTrue(secondPacketReceived.get());
+ }
+ }
+
private PipeParameters buildParameters(final boolean useTsFileBatch) {
+ return new PipeParameters(buildParameterAttributes(useTsFileBatch));
+ }
+
+ private Map<String, String> buildParameterAttributes(final boolean
useTsFileBatch) {
final Map<String, String> attributes = new HashMap<>();
attributes.put(
PipeSinkConstant.CONNECTOR_KEY,
@@ -151,7 +348,7 @@ public class IoTDBDataRegionAirGapSinkTest {
if (useTsFileBatch) {
attributes.put(PipeSinkConstant.CONNECTOR_FORMAT_KEY, "tsfile");
}
- return new PipeParameters(attributes);
+ return attributes;
}
private PipeRawTabletInsertionEvent createPipeRawTabletInsertionEvent(
@@ -185,6 +382,34 @@ public class IoTDBDataRegionAirGapSinkTest {
return req;
}
+ private static TPipeTransferReq decodeAirGapDatagram(final byte[] datagram,
final int length) {
+ final ByteBuffer buffer = ByteBuffer.wrap(datagram, Long.BYTES, length -
Long.BYTES);
+ final int payloadLength = ReadWriteIOUtils.readInt(buffer);
+ Assert.assertEquals(payloadLength, ReadWriteIOUtils.readInt(buffer));
+ Assert.assertEquals(length - Long.BYTES - 2 * Integer.BYTES,
payloadLength);
+
+ final long expectedChecksum = ReadWriteIOUtils.readLong(buffer);
+ final byte[] payload = new byte[payloadLength - Long.BYTES];
+ buffer.get(payload);
+
+ final CRC32 crc32 = new CRC32();
+ crc32.update(payload, 0, payload.length);
+ Assert.assertEquals(expectedChecksum, crc32.getValue());
+ return toTPipeTransferReq(payload);
+ }
+
+ private static void sendUdpAck(
+ final DatagramSocket socket, final DatagramPacket requestPacket, final
long requestId)
+ throws IOException {
+ final byte[] ack =
+ ByteBuffer.allocate(Long.BYTES + AirGapOneByteResponse.OK.length)
+ .putLong(requestId)
+ .put(AirGapOneByteResponse.OK)
+ .array();
+ socket.send(
+ new DatagramPacket(ack, ack.length, requestPacket.getAddress(),
requestPacket.getPort()));
+ }
+
private static class RecordingIoTDBDataRegionAirGapSink extends
IoTDBDataRegionAirGapSink {
private final List<byte[]> sentRequests = new ArrayList<>();
@@ -227,4 +452,15 @@ public class IoTDBDataRegionAirGapSinkTest {
}
}
}
+
+ private static class UdpTestingIoTDBDataRegionAirGapSink extends
IoTDBDataRegionAirGapSink {
+
+ private boolean sendUdp(final int port, final byte[] request) throws
Exception {
+ final AirGapSocket socket = new AirGapSocket("127.0.0.1", port);
+ try (final Socket ignored = socket) {
+ socket.connectUdp(1000);
+ return sendBytes(socket, request);
+ }
+ }
+ }
}
diff --git
a/iotdb-core/node-commons/src/main/i18n/en/org/apache/iotdb/commons/i18n/PipeMessages.java
b/iotdb-core/node-commons/src/main/i18n/en/org/apache/iotdb/commons/i18n/PipeMessages.java
index 0aafc358427..c09c42a188f 100644
---
a/iotdb-core/node-commons/src/main/i18n/en/org/apache/iotdb/commons/i18n/PipeMessages.java
+++
b/iotdb-core/node-commons/src/main/i18n/en/org/apache/iotdb/commons/i18n/PipeMessages.java
@@ -508,6 +508,14 @@ public final class PipeMessages {
"IoTDBAirGapConnector is customized with handshakeTimeoutMs: {}.";
public static final String AIR_GAP_CUSTOMIZED_E_LANGUAGE =
"IoTDBAirGapConnector is customized with eLanguageEnable: {}.";
+ public static final String
+
EXCEPTION_AIR_GAP_UDP_DATAGRAM_SIZE_ARG_EXCEEDS_CONFIGURED_PACKET_SIZE_ARG_559D0654
=
+ "Air gap UDP datagram size %d exceeds configured packet size %d.";
+ public static final String
EXCEPTION_TIMED_OUT_WAITING_FOR_THE_MATCHING_AIR_GAP_ACK_DD2F3706 =
+ "Timed out waiting for the matching air-gap ACK.";
+ public static final String
+
EXCEPTION_AIR_GAP_UDP_PACKET_SIZE_ARG_IS_TOO_SMALL_TO_TRANSFER_SLICED_REQUESTS_722B32AB
=
+ "Air gap UDP packet size %d is too small to transfer sliced
requests.";
public static final String CONNECTED_TO_TARGET_SERVER =
"Successfully connected to target server ip: {}, port: {}.";
public static final String FAILED_TO_CLOSE_SOCKET =
diff --git
a/iotdb-core/node-commons/src/main/i18n/zh/org/apache/iotdb/commons/i18n/PipeMessages.java
b/iotdb-core/node-commons/src/main/i18n/zh/org/apache/iotdb/commons/i18n/PipeMessages.java
index 9746b71500d..b8fd1db6729 100644
---
a/iotdb-core/node-commons/src/main/i18n/zh/org/apache/iotdb/commons/i18n/PipeMessages.java
+++
b/iotdb-core/node-commons/src/main/i18n/zh/org/apache/iotdb/commons/i18n/PipeMessages.java
@@ -486,6 +486,14 @@ public final class PipeMessages {
public static final String AIR_GAP_CUSTOMIZED_HANDSHAKE_TIMEOUT =
"IoTDBAirGapConnector 自定义握手超时时间:{}。";
+ public static final String
+
EXCEPTION_AIR_GAP_UDP_DATAGRAM_SIZE_ARG_EXCEEDS_CONFIGURED_PACKET_SIZE_ARG_559D0654
=
+ "Air gap UDP 数据报大小 %d 超过配置的包大小 %d。";
+ public static final String
EXCEPTION_TIMED_OUT_WAITING_FOR_THE_MATCHING_AIR_GAP_ACK_DD2F3706 =
+ "等待匹配的 air-gap ACK 超时。";
+ public static final String
+
EXCEPTION_AIR_GAP_UDP_PACKET_SIZE_ARG_IS_TOO_SMALL_TO_TRANSFER_SLICED_REQUESTS_722B32AB
=
+ "Air gap UDP 包大小 %d 太小,无法传输切片请求。";
public static final String AIR_GAP_CUSTOMIZED_E_LANGUAGE =
"IoTDBAirGapConnector 自定义 eLanguageEnable:{}。";
public static final String CONNECTED_TO_TARGET_SERVER =
diff --git
a/iotdb-core/node-commons/src/main/java/org/apache/iotdb/commons/pipe/config/constant/PipeSinkConstant.java
b/iotdb-core/node-commons/src/main/java/org/apache/iotdb/commons/pipe/config/constant/PipeSinkConstant.java
index 04a744611c0..e7a36577a06 100644
---
a/iotdb-core/node-commons/src/main/java/org/apache/iotdb/commons/pipe/config/constant/PipeSinkConstant.java
+++
b/iotdb-core/node-commons/src/main/java/org/apache/iotdb/commons/pipe/config/constant/PipeSinkConstant.java
@@ -186,6 +186,26 @@ public class PipeSinkConstant {
"sink.air-gap.handshake-timeout-ms";
public static final int CONNECTOR_AIR_GAP_HANDSHAKE_TIMEOUT_MS_DEFAULT_VALUE
= 5000;
+ public static final String CONNECTOR_AIR_GAP_TRANSPORT_KEY =
"connector.air-gap.transport";
+ public static final String SINK_AIR_GAP_TRANSPORT_KEY =
"sink.air-gap.transport";
+ public static final String CONNECTOR_AIR_GAP_TRANSPORT_TCP_VALUE = "tcp";
+ public static final String CONNECTOR_AIR_GAP_TRANSPORT_UDP_VALUE = "udp";
+ public static final String CONNECTOR_AIR_GAP_TRANSPORT_DEFAULT_VALUE =
+ CONNECTOR_AIR_GAP_TRANSPORT_TCP_VALUE;
+ public static final Set<String> CONNECTOR_AIR_GAP_TRANSPORT_SET =
+ Collections.unmodifiableSet(
+ new HashSet<>(
+ Arrays.asList(
+ CONNECTOR_AIR_GAP_TRANSPORT_TCP_VALUE,
CONNECTOR_AIR_GAP_TRANSPORT_UDP_VALUE)));
+
+ public static final String CONNECTOR_AIR_GAP_UDP_PACKET_SIZE_KEY =
+ "connector.air-gap.udp.packet-size-bytes";
+ public static final String SINK_AIR_GAP_UDP_PACKET_SIZE_KEY =
+ "sink.air-gap.udp.packet-size-bytes";
+ public static final int CONNECTOR_AIR_GAP_UDP_PACKET_SIZE_DEFAULT_VALUE = 60
* 1024;
+ public static final int CONNECTOR_AIR_GAP_UDP_PACKET_SIZE_MIN_VALUE = 1024;
+ public static final int CONNECTOR_AIR_GAP_UDP_PACKET_SIZE_MAX_VALUE = 65_507;
+
public static final String CONNECTOR_IOTDB_SYNC_CONNECTOR_VERSION_KEY =
"connector.version";
public static final String SINK_IOTDB_SYNC_CONNECTOR_VERSION_KEY =
"sink.version";
public static final String
CONNECTOR_IOTDB_SYNC_CONNECTOR_VERSION_DEFAULT_VALUE = "1.1";
diff --git
a/iotdb-core/node-commons/src/main/java/org/apache/iotdb/commons/pipe/sink/protocol/IoTDBAirGapSink.java
b/iotdb-core/node-commons/src/main/java/org/apache/iotdb/commons/pipe/sink/protocol/IoTDBAirGapSink.java
index 6919b3b87ce..bdffcfe4734 100644
---
a/iotdb-core/node-commons/src/main/java/org/apache/iotdb/commons/pipe/sink/protocol/IoTDBAirGapSink.java
+++
b/iotdb-core/node-commons/src/main/java/org/apache/iotdb/commons/pipe/sink/protocol/IoTDBAirGapSink.java
@@ -26,16 +26,20 @@ import org.apache.iotdb.commons.pipe.config.PipeConfig;
import org.apache.iotdb.commons.pipe.resource.log.PipeLogger;
import
org.apache.iotdb.commons.pipe.sink.payload.airgap.AirGapELanguageConstant;
import org.apache.iotdb.commons.pipe.sink.payload.airgap.AirGapOneByteResponse;
+import
org.apache.iotdb.commons.pipe.sink.payload.thrift.common.PipeTransferSliceReqBuilder;
import org.apache.iotdb.commons.utils.ErrorHandlingCommonUtils;
import org.apache.iotdb.pipe.api.annotation.TableModel;
import org.apache.iotdb.pipe.api.annotation.TreeModel;
import
org.apache.iotdb.pipe.api.customizer.configuration.PipeConnectorRuntimeConfiguration;
+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.PipeConnectionException;
import org.apache.iotdb.pipe.api.exception.PipeException;
import org.apache.iotdb.rpc.TSStatusCode;
+import org.apache.iotdb.service.rpc.thrift.TPipeTransferReq;
import org.apache.tsfile.utils.BytesUtils;
+import org.apache.tsfile.utils.ReadWriteIOUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
@@ -44,34 +48,59 @@ import java.io.DataInputStream;
import java.io.File;
import java.io.IOException;
import java.io.RandomAccessFile;
+import java.net.DatagramPacket;
+import java.net.DatagramSocket;
+import java.net.InetAddress;
import java.net.InetSocketAddress;
import java.net.Socket;
import java.net.SocketException;
+import java.net.SocketTimeoutException;
+import java.nio.ByteBuffer;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
+import java.util.concurrent.TimeUnit;
+import java.util.concurrent.atomic.AtomicLong;
import java.util.zip.CRC32;
import static
org.apache.iotdb.commons.pipe.config.constant.PipeSinkConstant.CONNECTOR_AIR_GAP_E_LANGUAGE_ENABLE_DEFAULT_VALUE;
import static
org.apache.iotdb.commons.pipe.config.constant.PipeSinkConstant.CONNECTOR_AIR_GAP_E_LANGUAGE_ENABLE_KEY;
import static
org.apache.iotdb.commons.pipe.config.constant.PipeSinkConstant.CONNECTOR_AIR_GAP_HANDSHAKE_TIMEOUT_MS_DEFAULT_VALUE;
import static
org.apache.iotdb.commons.pipe.config.constant.PipeSinkConstant.CONNECTOR_AIR_GAP_HANDSHAKE_TIMEOUT_MS_KEY;
+import static
org.apache.iotdb.commons.pipe.config.constant.PipeSinkConstant.CONNECTOR_AIR_GAP_TRANSPORT_DEFAULT_VALUE;
+import static
org.apache.iotdb.commons.pipe.config.constant.PipeSinkConstant.CONNECTOR_AIR_GAP_TRANSPORT_KEY;
+import static
org.apache.iotdb.commons.pipe.config.constant.PipeSinkConstant.CONNECTOR_AIR_GAP_TRANSPORT_SET;
+import static
org.apache.iotdb.commons.pipe.config.constant.PipeSinkConstant.CONNECTOR_AIR_GAP_TRANSPORT_UDP_VALUE;
+import static
org.apache.iotdb.commons.pipe.config.constant.PipeSinkConstant.CONNECTOR_AIR_GAP_UDP_PACKET_SIZE_DEFAULT_VALUE;
+import static
org.apache.iotdb.commons.pipe.config.constant.PipeSinkConstant.CONNECTOR_AIR_GAP_UDP_PACKET_SIZE_KEY;
+import static
org.apache.iotdb.commons.pipe.config.constant.PipeSinkConstant.CONNECTOR_AIR_GAP_UDP_PACKET_SIZE_MAX_VALUE;
+import static
org.apache.iotdb.commons.pipe.config.constant.PipeSinkConstant.CONNECTOR_AIR_GAP_UDP_PACKET_SIZE_MIN_VALUE;
import static
org.apache.iotdb.commons.pipe.config.constant.PipeSinkConstant.CONNECTOR_LOAD_BALANCE_PRIORITY_STRATEGY;
import static
org.apache.iotdb.commons.pipe.config.constant.PipeSinkConstant.CONNECTOR_LOAD_BALANCE_RANDOM_STRATEGY;
import static
org.apache.iotdb.commons.pipe.config.constant.PipeSinkConstant.CONNECTOR_LOAD_BALANCE_ROUND_ROBIN_STRATEGY;
import static
org.apache.iotdb.commons.pipe.config.constant.PipeSinkConstant.SINK_AIR_GAP_E_LANGUAGE_ENABLE_KEY;
import static
org.apache.iotdb.commons.pipe.config.constant.PipeSinkConstant.SINK_AIR_GAP_HANDSHAKE_TIMEOUT_MS_KEY;
+import static
org.apache.iotdb.commons.pipe.config.constant.PipeSinkConstant.SINK_AIR_GAP_TRANSPORT_KEY;
+import static
org.apache.iotdb.commons.pipe.config.constant.PipeSinkConstant.SINK_AIR_GAP_UDP_PACKET_SIZE_KEY;
import static org.apache.iotdb.commons.utils.BasicStructureSerDeUtil.LONG_LEN;
@TreeModel
@TableModel
public abstract class IoTDBAirGapSink extends IoTDBSink {
+ private static final int UDP_ENVELOPE_SIZE = 2 * Integer.BYTES + Long.BYTES;
+ private static final int UDP_REQUEST_ID_SIZE = Long.BYTES;
+ private static final int UDP_ACK_SIZE = UDP_REQUEST_ID_SIZE + 1;
+ private static final int UDP_SLICE_REQUEST_SERIALIZATION_RESERVED_SIZE = 64;
+
protected static class AirGapSocket extends Socket {
private final TEndPoint endPoint;
+ private DatagramSocket datagramSocket;
+ private InetAddress datagramAddress;
+ private boolean isUdp;
public AirGapSocket(final String ip, final int port) {
this.endPoint = new TEndPoint(ip, port);
@@ -81,9 +110,60 @@ public abstract class IoTDBAirGapSink extends IoTDBSink {
return endPoint;
}
+ public void connectUdp(final int timeoutMs) throws IOException {
+ datagramAddress = InetAddress.getByName(endPoint.getIp());
+ datagramSocket = new DatagramSocket();
+ datagramSocket.connect(datagramAddress, endPoint.getPort());
+ datagramSocket.setSoTimeout(timeoutMs);
+ isUdp = true;
+ }
+
+ public InetAddress getDatagramAddress() {
+ return datagramAddress;
+ }
+
+ public DatagramSocket getDatagramSocket() {
+ return datagramSocket;
+ }
+
+ public boolean isUdp() {
+ return isUdp;
+ }
+
+ @Override
+ public boolean isConnected() {
+ return isUdp
+ ? datagramSocket != null && datagramSocket.isConnected() &&
!datagramSocket.isClosed()
+ : super.isConnected();
+ }
+
+ @Override
+ public synchronized void setSoTimeout(final int timeout) throws
SocketException {
+ if (isUdp && datagramSocket != null) {
+ datagramSocket.setSoTimeout(timeout);
+ } else {
+ super.setSoTimeout(timeout);
+ }
+ }
+
+ @Override
+ public synchronized void close() throws IOException {
+ if (datagramSocket != null) {
+ datagramSocket.close();
+ }
+ super.close();
+ }
+
@Override
public String toString() {
- return "AirGapSocket{" + "endPoint=" + endPoint + "} (" +
super.toString() + ")";
+ return "AirGapSocket{"
+ + "endPoint="
+ + endPoint
+ + ", transport="
+ + (isUdp ? "udp" : "tcp")
+ + "} ("
+ + super.toString()
+ + ")";
}
}
@@ -101,11 +181,49 @@ public abstract class IoTDBAirGapSink extends IoTDBSink {
private int handshakeTimeoutMs;
private boolean eLanguageEnable;
+ private boolean useUdpTransport;
+ private int udpPacketSizeInBytes;
// The air gap connector does not use clientManager thus we put handshake
type here
protected boolean supportModsIfIsDataNodeReceiver = true;
private final Map<TEndPoint, Long> failLogTimes = new HashMap<>();
+ private final AtomicLong udpRequestIdGenerator = new AtomicLong();
+
+ @Override
+ public void validate(final PipeParameterValidator validator) throws
Exception {
+ super.validate(validator);
+
+ final PipeParameters parameters = validator.getParameters();
+ final String airGapTransport =
+ parameters
+ .getStringOrDefault(
+ Arrays.asList(CONNECTOR_AIR_GAP_TRANSPORT_KEY,
SINK_AIR_GAP_TRANSPORT_KEY),
+ CONNECTOR_AIR_GAP_TRANSPORT_DEFAULT_VALUE)
+ .trim()
+ .toLowerCase();
+ validator.validate(
+ arg -> CONNECTOR_AIR_GAP_TRANSPORT_SET.contains(airGapTransport),
+ String.format(
+ "Air gap transport should be one of %s, but got %s.",
+ CONNECTOR_AIR_GAP_TRANSPORT_SET, airGapTransport),
+ airGapTransport);
+
+ final int packetSize =
+ parameters.getIntOrDefault(
+ Arrays.asList(CONNECTOR_AIR_GAP_UDP_PACKET_SIZE_KEY,
SINK_AIR_GAP_UDP_PACKET_SIZE_KEY),
+ CONNECTOR_AIR_GAP_UDP_PACKET_SIZE_DEFAULT_VALUE);
+ validator.validate(
+ arg ->
+ packetSize >= CONNECTOR_AIR_GAP_UDP_PACKET_SIZE_MIN_VALUE
+ && packetSize <= CONNECTOR_AIR_GAP_UDP_PACKET_SIZE_MAX_VALUE,
+ String.format(
+ "UDP packet size should be in the range [%d, %d], but got %d.",
+ CONNECTOR_AIR_GAP_UDP_PACKET_SIZE_MIN_VALUE,
+ CONNECTOR_AIR_GAP_UDP_PACKET_SIZE_MAX_VALUE,
+ packetSize),
+ packetSize);
+ }
@Override
public void customize(
@@ -146,6 +264,23 @@ public abstract class IoTDBAirGapSink extends IoTDBSink {
CONNECTOR_AIR_GAP_E_LANGUAGE_ENABLE_KEY,
SINK_AIR_GAP_E_LANGUAGE_ENABLE_KEY),
CONNECTOR_AIR_GAP_E_LANGUAGE_ENABLE_DEFAULT_VALUE);
LOGGER.info(PipeMessages.AIR_GAP_CUSTOMIZED_E_LANGUAGE, eLanguageEnable);
+
+ useUdpTransport =
+ CONNECTOR_AIR_GAP_TRANSPORT_UDP_VALUE.equals(
+ parameters
+ .getStringOrDefault(
+ Arrays.asList(CONNECTOR_AIR_GAP_TRANSPORT_KEY,
SINK_AIR_GAP_TRANSPORT_KEY),
+ CONNECTOR_AIR_GAP_TRANSPORT_DEFAULT_VALUE)
+ .trim()
+ .toLowerCase());
+ udpPacketSizeInBytes =
+ parameters.getIntOrDefault(
+ Arrays.asList(CONNECTOR_AIR_GAP_UDP_PACKET_SIZE_KEY,
SINK_AIR_GAP_UDP_PACKET_SIZE_KEY),
+ CONNECTOR_AIR_GAP_UDP_PACKET_SIZE_DEFAULT_VALUE);
+ LOGGER.info(
+ "Air gap transport is {}, udp packet size is {} bytes.",
+ useUdpTransport ? "udp" : "tcp",
+ udpPacketSizeInBytes);
}
@Override
@@ -180,8 +315,12 @@ public abstract class IoTDBAirGapSink extends IoTDBSink {
final AirGapSocket socket = new AirGapSocket(ip, port);
try {
- socket.connect(new InetSocketAddress(ip, port), handshakeTimeoutMs);
- socket.setKeepAlive(true);
+ if (useUdpTransport) {
+ socket.connectUdp(handshakeTimeoutMs);
+ } else {
+ socket.connect(new InetSocketAddress(ip, port), handshakeTimeoutMs);
+ socket.setKeepAlive(true);
+ }
sockets.set(i, socket);
LOGGER.debug(PipeMessages.CONNECTED_TO_TARGET_SERVER, ip, port);
failLogTimes.remove(nodeUrls.get(i));
@@ -324,6 +463,10 @@ public abstract class IoTDBAirGapSink extends IoTDBSink {
PipeMessages.EXCEPTION_SOCKET_ARG_CLOSED_WILL_TRY_HANDSHAKE_02562BF1, socket));
}
+ if (socket.isUdp()) {
+ return sendBytesByUdp(socket, bytes);
+ }
+
final BufferedOutputStream outputStream = new
BufferedOutputStream(socket.getOutputStream());
bytes = enrichWithLengthAndChecksum(bytes);
outputStream.write(eLanguageEnable ? enrichWithELanguage(bytes) : bytes);
@@ -334,6 +477,147 @@ public abstract class IoTDBAirGapSink extends IoTDBSink {
return Arrays.equals(AirGapOneByteResponse.OK, response);
}
+ private boolean sendBytesByUdp(final AirGapSocket socket, final byte[]
bytes) throws IOException {
+ for (final TPipeTransferReq request : sliceIfNeededForUdp(bytes)) {
+ if (!sendOneDatagram(socket, request)) {
+ return false;
+ }
+ }
+ return true;
+ }
+
+ private boolean sendOneDatagram(final AirGapSocket socket, final
TPipeTransferReq request)
+ throws IOException {
+ final long requestId = udpRequestIdGenerator.incrementAndGet();
+ final ByteBuffer body = request.body.duplicate();
+ final int requestSize = Byte.BYTES + Short.BYTES + body.remaining();
+ final int eLanguageSize =
+ eLanguageEnable
+ ? AirGapELanguageConstant.E_LANGUAGE_PREFIX.length
+ + AirGapELanguageConstant.E_LANGUAGE_SUFFIX.length
+ : 0;
+ final int datagramSize = UDP_REQUEST_ID_SIZE + UDP_ENVELOPE_SIZE +
requestSize + eLanguageSize;
+ if (datagramSize > udpPacketSizeInBytes) {
+ throw new IOException(
+ String.format(
+ PipeMessages
+
.EXCEPTION_AIR_GAP_UDP_DATAGRAM_SIZE_ARG_EXCEEDS_CONFIGURED_PACKET_SIZE_ARG_559D0654,
+ datagramSize,
+ udpPacketSizeInBytes));
+ }
+
+ final CRC32 crc32 = new CRC32();
+ crc32.update(request.version);
+ crc32.update((request.type >>> Byte.SIZE) & 0xFF);
+ crc32.update(request.type & 0xFF);
+ crc32.update(body.duplicate());
+
+ final ByteBuffer datagramBuffer =
ByteBuffer.allocate(datagramSize).putLong(requestId);
+ if (eLanguageEnable) {
+ datagramBuffer.put(AirGapELanguageConstant.E_LANGUAGE_PREFIX);
+ }
+ datagramBuffer
+ .putInt(requestSize + Long.BYTES)
+ .putInt(requestSize + Long.BYTES)
+ .putLong(crc32.getValue())
+ .put(request.version)
+ .putShort(request.type)
+ .put(body);
+ if (eLanguageEnable) {
+ datagramBuffer.put(AirGapELanguageConstant.E_LANGUAGE_SUFFIX);
+ }
+ final byte[] datagramBytes = datagramBuffer.array();
+
+ final DatagramSocket datagramSocket = socket.getDatagramSocket();
+ datagramSocket.send(
+ new DatagramPacket(
+ datagramBytes,
+ datagramBytes.length,
+ socket.getDatagramAddress(),
+ socket.getEndPoint().getPort()));
+
+ final int originalTimeoutMs = datagramSocket.getSoTimeout();
+ final long deadlineNanos =
+ originalTimeoutMs == 0
+ ? Long.MAX_VALUE
+ : System.nanoTime() +
TimeUnit.MILLISECONDS.toNanos(originalTimeoutMs);
+ // One extra byte lets us detect datagrams that receive() would otherwise
silently truncate.
+ final byte[] response = new byte[UDP_ACK_SIZE + 1];
+ final DatagramPacket responsePacket = new DatagramPacket(response,
response.length);
+ try {
+ while (true) {
+ if (deadlineNanos != Long.MAX_VALUE) {
+ final long remainingNanos = deadlineNanos - System.nanoTime();
+ if (remainingNanos <= 0) {
+ throw new SocketTimeoutException(
+
PipeMessages.EXCEPTION_TIMED_OUT_WAITING_FOR_THE_MATCHING_AIR_GAP_ACK_DD2F3706);
+ }
+ datagramSocket.setSoTimeout(
+ (int) Math.max(1,
TimeUnit.NANOSECONDS.toMillis(remainingNanos)));
+ }
+
+ responsePacket.setLength(response.length);
+ datagramSocket.receive(responsePacket);
+ if (responsePacket.getLength() != UDP_ACK_SIZE) {
+ continue;
+ }
+
+ final ByteBuffer responseBuffer = ByteBuffer.wrap(response, 0,
responsePacket.getLength());
+ if (responseBuffer.getLong() != requestId) {
+ continue;
+ }
+ return responseBuffer.get() == AirGapOneByteResponse.OK[0];
+ }
+ } finally {
+ datagramSocket.setSoTimeout(originalTimeoutMs);
+ }
+ }
+
+ private List<TPipeTransferReq> sliceIfNeededForUdp(final byte[]
requestBytes) throws IOException {
+ final TPipeTransferReq request = toTPipeTransferReq(requestBytes);
+ final int rawPayloadSizeLimit =
+ udpPacketSizeInBytes
+ - UDP_ENVELOPE_SIZE
+ - UDP_REQUEST_ID_SIZE
+ - (eLanguageEnable
+ ? AirGapELanguageConstant.E_LANGUAGE_PREFIX.length
+ + AirGapELanguageConstant.E_LANGUAGE_SUFFIX.length
+ : 0);
+ if (Byte.BYTES + Short.BYTES + request.body.remaining() <=
rawPayloadSizeLimit) {
+ return Arrays.asList(request);
+ }
+
+ final int sliceBodySizeLimit =
+ rawPayloadSizeLimit - UDP_SLICE_REQUEST_SERIALIZATION_RESERVED_SIZE;
+ if (sliceBodySizeLimit <= 0) {
+ throw new IOException(
+ String.format(
+ PipeMessages
+
.EXCEPTION_AIR_GAP_UDP_PACKET_SIZE_ARG_IS_TOO_SMALL_TO_TRANSFER_SLICED_REQUESTS_722B32AB,
+ udpPacketSizeInBytes));
+ }
+
+ final int sliceOrderId = PipeTransferSliceReqBuilder.nextSliceOrderId();
+ final int sliceCount = PipeTransferSliceReqBuilder.getSliceCount(request,
sliceBodySizeLimit);
+
+ final List<TPipeTransferReq> slicedRequests = new ArrayList<>(sliceCount);
+ for (int i = 0; i < sliceCount; i++) {
+ slicedRequests.add(
+ PipeTransferSliceReqBuilder.buildSliceReq(
+ request, sliceOrderId, i, sliceCount, sliceBodySizeLimit));
+ }
+ return slicedRequests;
+ }
+
+ private TPipeTransferReq toTPipeTransferReq(final byte[] requestBytes) {
+ final ByteBuffer byteBuffer = ByteBuffer.wrap(requestBytes);
+ final TPipeTransferReq request = new TPipeTransferReq();
+ request.version = ReadWriteIOUtils.readByte(byteBuffer);
+ request.type = ReadWriteIOUtils.readShort(byteBuffer);
+ request.body = byteBuffer.slice();
+ return request;
+ }
+
protected boolean send(final AirGapSocket socket, final byte[] bytes) throws
IOException {
return send(null, 0, socket, bytes);
}