This is an automated email from the ASF dual-hosted git repository. Caideyipi pushed a commit to branch feat/air-gap-strict-unidirectional in repository https://gitbox.apache.org/repos/asf/iotdb.git
commit f03440874c1b404808b4f512795d64c092bea9f1 Author: Caideyipi <[email protected]> AuthorDate: Fri Oct 30 15:28:38 2026 +0800 Support strictly unidirectional UDP air-gap transfer --- .../protocol/airgap/IoTDBAirGapReceiver.java | 4 +- .../protocol/airgap/IoTDBAirGapReceiverTest.java | 56 +++++++++ .../airgap/IoTDBDataRegionAirGapSinkTest.java | 137 +++++++++++++++++++++ .../apache/iotdb/commons/i18n/PipeMessages.java | 6 + .../apache/iotdb/commons/i18n/PipeMessages.java | 6 + .../pipe/config/constant/PipeSinkConstant.java | 6 + .../sink/payload/airgap/AirGapUdpProtocol.java | 29 +++++ .../pipe/sink/protocol/IoTDBAirGapSink.java | 64 +++++++++- 8 files changed, 303 insertions(+), 5 deletions(-) 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 e3413b009b2..1a9cb1616d0 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 @@ -25,6 +25,7 @@ import org.apache.iotdb.commons.pipe.config.PipeConfig; 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.airgap.AirGapPseudoTPipeTransferRequest; +import org.apache.iotdb.commons.pipe.sink.payload.airgap.AirGapUdpProtocol; import org.apache.iotdb.db.i18n.DataNodePipeMessages; import org.apache.iotdb.db.pipe.agent.PipeDataNodeAgent; import org.apache.iotdb.db.pipe.receiver.protocol.thrift.IoTDBDataNodeReceiverAgent; @@ -296,7 +297,8 @@ public class IoTDBAirGapReceiver extends WrappedRunnable { private void respond(final byte[] response) throws IOException { if (currentDatagramSocket != null && currentRemoteSocketAddress != null) { - if (currentDatagramRequestId == null) { + if (currentDatagramRequestId == null + || (currentDatagramRequestId & AirGapUdpProtocol.NO_ACK_REQUEST_ID_MASK) != 0) { return; } final byte[] responseBytes = 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 57cacb38799..86481ce3517 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 @@ -27,6 +27,7 @@ import org.apache.iotdb.commons.pipe.receiver.runtime.PipeReceiverRuntimeRegistr 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.airgap.AirGapPseudoTPipeTransferRequest; +import org.apache.iotdb.commons.pipe.sink.payload.airgap.AirGapUdpProtocol; 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; @@ -202,6 +203,61 @@ public class IoTDBAirGapReceiverTest { } } + @Test + public void testStrictUnidirectionalUdpNeverRespondsToSuccessOrFailure() throws Exception { + for (final TSStatusCode status : + new TSStatusCode[] { + TSStatusCode.SUCCESS_STATUS, TSStatusCode.PIPE_RECEIVER_USER_CONFLICT_EXCEPTION + }) { + try (final DatagramSocket receiverSocket = new NoResponseDatagramSocket()) { + final byte[] request = + toUdpDatagram(AirGapUdpProtocol.NO_ACK_REQUEST_ID_MASK | 73L, toRawRequestBytes()); + final DatagramPacket packet = + new DatagramPacket(request, request.length, InetAddress.getLoopbackAddress(), 12345); + final IoTDBAirGapReceiver receiver = new IoTDBAirGapReceiver(new RecordingSocket(), 5L); + final StubIoTDBDataNodeReceiverAgent stubAgent = new StubIoTDBDataNodeReceiverAgent(); + final AtomicInteger receivedRequests = new AtomicInteger(); + stubAgent.setStubReceiver( + "udp-client", + new StubReceiver(new TSStatus(status.getStatusCode())) { + @Override + public TPipeTransferResp receive(final TPipeTransferReq req) { + receivedRequests.incrementAndGet(); + return super.receive(req); + } + }); + setField(receiver, "agent", stubAgent); + receiver.receiveUdp(receiverSocket, packet, "udp-client", request, () -> {}); + Assert.assertEquals(1, receivedRequests.get()); + } + } + } + + @Test + public void testStrictUnidirectionalUdpDoesNotRespondToCorruptRequest() throws Exception { + try (final DatagramSocket receiverSocket = new NoResponseDatagramSocket()) { + final byte[] request = + toUdpDatagram(AirGapUdpProtocol.NO_ACK_REQUEST_ID_MASK | 74L, toRawRequestBytes()); + request[request.length - 1] ^= 1; + final DatagramPacket packet = + new DatagramPacket(request, request.length, InetAddress.getLoopbackAddress(), 12345); + final AtomicBoolean registered = new AtomicBoolean(); + new IoTDBAirGapReceiver(new RecordingSocket(), 6L) + .receiveUdp(receiverSocket, packet, "udp-client", request, () -> registered.set(true)); + Assert.assertFalse(registered.get()); + } + } + + private static class NoResponseDatagramSocket extends DatagramSocket { + + private NoResponseDatagramSocket() throws SocketException {} + + @Override + public void send(final DatagramPacket packet) { + Assert.fail("Strictly unidirectional requests must never send an ACK or NACK"); + } + } + @Test public void testTemporaryUnavailableRetryUsesFreshRequestBody() throws Exception { assertTemporaryUnavailableRetryUsesFreshRequestBody(null); 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 82000564cfe..cdc0a300067 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 @@ -24,6 +24,7 @@ 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.airgap.AirGapUdpProtocol; 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; @@ -32,6 +33,7 @@ import org.apache.iotdb.db.pipe.sink.payload.evolvable.request.PipeTransferTsFil 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.PipeParameterNotValidException; import org.apache.iotdb.service.rpc.thrift.TPipeTransferReq; import org.apache.tsfile.enums.TSDataType; @@ -60,6 +62,137 @@ import java.util.zip.CRC32; public class IoTDBDataRegionAirGapSinkTest { + @Test + public void testStrictUnidirectionalRequiresUdpAndBooleanValue() throws Exception { + try (final IoTDBDataRegionAirGapSink sink = new IoTDBDataRegionAirGapSink()) { + for (final String key : + Arrays.asList( + PipeSinkConstant.CONNECTOR_AIR_GAP_STRICT_UNIDIRECTIONAL_KEY, + PipeSinkConstant.SINK_AIR_GAP_STRICT_UNIDIRECTIONAL_KEY)) { + final PipeParameters parameters = buildParameters(false); + parameters.getAttribute().put(key, "true"); + Assert.assertThrows( + PipeParameterNotValidException.class, + () -> sink.validate(new PipeParameterValidator(parameters))); + parameters.getAttribute().put(PipeSinkConstant.CONNECTOR_AIR_GAP_TRANSPORT_KEY, "udp"); + sink.validate(new PipeParameterValidator(parameters)); + parameters.getAttribute().put(key, "invalid"); + Assert.assertThrows( + PipeParameterNotValidException.class, + () -> sink.validate(new PipeParameterValidator(parameters))); + } + } + } + + @Test + public void testStrictUnidirectionalUdpSendsAllSlicesWithoutAcknowledgements() throws Exception { + for (final String key : + Arrays.asList( + PipeSinkConstant.CONNECTOR_AIR_GAP_STRICT_UNIDIRECTIONAL_KEY, + PipeSinkConstant.SINK_AIR_GAP_STRICT_UNIDIRECTIONAL_KEY)) { + try (final DatagramSocket receiverSocket = new DatagramSocket(0); + final UdpTestingIoTDBDataRegionAirGapSink sink = + new UdpTestingIoTDBDataRegionAirGapSink()) { + final PipeParameters parameters = buildParameters(false); + parameters.getAttribute().put(PipeSinkConstant.CONNECTOR_AIR_GAP_TRANSPORT_KEY, "udp"); + parameters + .getAttribute() + .put(PipeSinkConstant.CONNECTOR_AIR_GAP_UDP_PACKET_SIZE_KEY, "1024"); + parameters.getAttribute().put(key, "true"); + sink.validate(new PipeParameterValidator(parameters)); + sink.customize( + parameters, + new PipeTaskRuntimeConfiguration(new PipeTaskSinkRuntimeEnvironment("pipe", 1L, 1))); + + final byte[] request = new byte[2500]; + request[0] = 1; + request[2] = (byte) PipeRequestType.TRANSFER_TABLET_RAW.getType(); + // Nothing receives or acknowledges until sendUdp has returned. + Assert.assertTrue(sink.sendUdp(receiverSocket.getLocalPort(), request)); + + receiverSocket.setSoTimeout(1000); + for (int i = 0; i < 3; i++) { + final byte[] buffer = new byte[1025]; + final DatagramPacket packet = new DatagramPacket(buffer, buffer.length); + receiverSocket.receive(packet); + Assert.assertTrue(packet.getLength() <= 1024); + final long requestId = ByteBuffer.wrap(buffer).getLong(); + Assert.assertEquals(AirGapUdpProtocol.NO_ACK_REQUEST_ID_MASK, requestId & Long.MIN_VALUE); + Assert.assertEquals(i + 1, requestId & Long.MAX_VALUE); + Assert.assertEquals( + PipeRequestType.TRANSFER_SLICE.getType(), + decodeAirGapDatagram(buffer, packet.getLength()).type); + } + } + } + } + + @Test + public void testStrictUnidirectionalHandshakeIsRefreshedBeforeEachEvent() throws Exception { + try (final DatagramSocket receiverSocket = new DatagramSocket(0); + final UdpTestingIoTDBDataRegionAirGapSink sink = + new UdpTestingIoTDBDataRegionAirGapSink()) { + final PipeParameters parameters = buildParameters(false); + parameters.getAttribute().put(PipeSinkConstant.CONNECTOR_AIR_GAP_TRANSPORT_KEY, "udp"); + parameters + .getAttribute() + .put(PipeSinkConstant.SINK_AIR_GAP_STRICT_UNIDIRECTIONAL_KEY, "true"); + parameters + .getAttribute() + .put( + PipeSinkConstant.CONNECTOR_IOTDB_NODE_URLS_KEY, + "127.0.0.1:" + receiverSocket.getLocalPort()); + sink.validate(new PipeParameterValidator(parameters)); + sink.customize( + parameters, + new PipeTaskRuntimeConfiguration(new PipeTaskSinkRuntimeEnvironment("pipe", 1L, 1))); + sink.handshake(); + final byte[] request = new byte[] {1, 0, 11}; + sink.sendEvent(request); + sink.sendEvent(request); + + receiverSocket.setSoTimeout(1000); + for (final short type : + new short[] { + PipeRequestType.HANDSHAKE_DATANODE_V2.getType(), + PipeRequestType.HANDSHAKE_DATANODE_V2.getType(), + 11, + PipeRequestType.HANDSHAKE_DATANODE_V2.getType(), + 11 + }) { + final byte[] buffer = new byte[65_507]; + final DatagramPacket packet = new DatagramPacket(buffer, buffer.length); + receiverSocket.receive(packet); + Assert.assertEquals(type, decodeAirGapDatagram(buffer, packet.getLength()).type); + Assert.assertTrue(ByteBuffer.wrap(buffer).getLong() < 0); + } + } + } + + @Test + public void testUdpDefaultAndExplicitFalseRequireAcknowledgement() throws Exception { + for (final String strictValue : Arrays.asList(null, "false")) { + try (final DatagramSocket receiverSocket = new DatagramSocket(0); + final UdpTestingIoTDBDataRegionAirGapSink sink = + new UdpTestingIoTDBDataRegionAirGapSink()) { + final PipeParameters parameters = buildParameters(false); + parameters.getAttribute().put(PipeSinkConstant.CONNECTOR_AIR_GAP_TRANSPORT_KEY, "udp"); + if (strictValue != null) { + parameters + .getAttribute() + .put(PipeSinkConstant.SINK_AIR_GAP_STRICT_UNIDIRECTIONAL_KEY, strictValue); + } + sink.validate(new PipeParameterValidator(parameters)); + sink.customize( + parameters, + new PipeTaskRuntimeConfiguration(new PipeTaskSinkRuntimeEnvironment("pipe", 1L, 1))); + Assert.assertThrows( + SocketTimeoutException.class, + () -> sink.sendUdp(receiverSocket.getLocalPort(), new byte[] {1, 0, 11})); + } + } + } + @Test public void testCompletionBarrierRetriesUnacknowledgedBatch() throws Exception { try (final RecordingIoTDBDataRegionAirGapSink sink = new RecordingIoTDBDataRegionAirGapSink()) { @@ -455,6 +588,10 @@ public class IoTDBDataRegionAirGapSinkTest { private static class UdpTestingIoTDBDataRegionAirGapSink extends IoTDBDataRegionAirGapSink { + private void sendEvent(final byte[] request) throws IOException { + Assert.assertTrue(sendBytes(sockets.get(nextSocketIndex()), request)); + } + 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) { 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 437e4dd68de..7cdf30ca6e6 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 @@ -20,6 +20,12 @@ package org.apache.iotdb.commons.i18n; public final class PipeMessages { + public static final String EXCEPTION_STRICTLY_UNIDIRECTIONAL_AIR_GAP_TRANSPORT_REQUIRES_UDP_78921AF9 = + "Strictly unidirectional air-gap transport requires UDP."; + public static final String LOG_STRICTLY_UNIDIRECTIONAL_UDP_TRANSPORT_IS_ENABLED_REQUESTS_ARE_SENT_WITHOUT_ACKNOWLEDGEMENTS_DELIVERY_AND_RECEIVER_ERRORS_CANNOT_BE_CONFIRMED_0A2F4382 = + "Strictly unidirectional UDP transport is enabled. Requests are sent without acknowledgements; delivery and receiver errors cannot be confirmed."; + public static final String LOG_AIR_GAP_HANDSHAKE_REQUEST_SENT_ON_SOCKET_ARG_WITHOUT_WAITING_FOR_ACKNOWLEDGEMENT_E1ECD582 = + "Air gap handshake request sent on socket {} without waiting for acknowledgement."; // ===================== Common / Shared ===================== 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 f67f0a351ac..7b57a22fa37 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 @@ -20,6 +20,12 @@ package org.apache.iotdb.commons.i18n; public final class PipeMessages { + public static final String EXCEPTION_STRICTLY_UNIDIRECTIONAL_AIR_GAP_TRANSPORT_REQUIRES_UDP_78921AF9 = + "严格单向的 air-gap 传输必须使用 UDP。"; + public static final String LOG_STRICTLY_UNIDIRECTIONAL_UDP_TRANSPORT_IS_ENABLED_REQUESTS_ARE_SENT_WITHOUT_ACKNOWLEDGEMENTS_DELIVERY_AND_RECEIVER_ERRORS_CANNOT_BE_CONFIRMED_0A2F4382 = + "已启用严格单向 UDP 传输。请求发送后不等待确认;无法确认送达情况和接收端错误。"; + public static final String LOG_AIR_GAP_HANDSHAKE_REQUEST_SENT_ON_SOCKET_ARG_WITHOUT_WAITING_FOR_ACKNOWLEDGEMENT_E1ECD582 = + "已通过 socket {} 发送 air-gap 握手请求,不等待确认。"; // ===================== Common / Shared ===================== 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 e7a36577a06..c04f6b87c79 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 @@ -198,6 +198,12 @@ public class PipeSinkConstant { Arrays.asList( CONNECTOR_AIR_GAP_TRANSPORT_TCP_VALUE, CONNECTOR_AIR_GAP_TRANSPORT_UDP_VALUE))); + public static final String CONNECTOR_AIR_GAP_STRICT_UNIDIRECTIONAL_KEY = + "connector.air-gap.strict-unidirectional"; + public static final String SINK_AIR_GAP_STRICT_UNIDIRECTIONAL_KEY = + "sink.air-gap.strict-unidirectional"; + public static final boolean CONNECTOR_AIR_GAP_STRICT_UNIDIRECTIONAL_DEFAULT_VALUE = false; + 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 = diff --git a/iotdb-core/node-commons/src/main/java/org/apache/iotdb/commons/pipe/sink/payload/airgap/AirGapUdpProtocol.java b/iotdb-core/node-commons/src/main/java/org/apache/iotdb/commons/pipe/sink/payload/airgap/AirGapUdpProtocol.java new file mode 100644 index 00000000000..783c6af94ff --- /dev/null +++ b/iotdb-core/node-commons/src/main/java/org/apache/iotdb/commons/pipe/sink/payload/airgap/AirGapUdpProtocol.java @@ -0,0 +1,29 @@ +/* + * 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.commons.pipe.sink.payload.airgap; + +public final class AirGapUdpProtocol { + + // Reserve the request ID's sign bit for requests that must not produce any reverse traffic. + // The remaining 63 bits identify the request; acknowledged requests keep their existing envelope. + public static final long NO_ACK_REQUEST_ID_MASK = Long.MIN_VALUE; + + private AirGapUdpProtocol() {} +} 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 bdffcfe4734..cd3c151d36f 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,6 +26,7 @@ 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.airgap.AirGapUdpProtocol; 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; @@ -69,6 +70,8 @@ import static org.apache.iotdb.commons.pipe.config.constant.PipeSinkConstant.CON 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_STRICT_UNIDIRECTIONAL_DEFAULT_VALUE; +import static org.apache.iotdb.commons.pipe.config.constant.PipeSinkConstant.CONNECTOR_AIR_GAP_STRICT_UNIDIRECTIONAL_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; @@ -82,6 +85,7 @@ import static org.apache.iotdb.commons.pipe.config.constant.PipeSinkConstant.CON 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_STRICT_UNIDIRECTIONAL_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; @@ -182,6 +186,7 @@ public abstract class IoTDBAirGapSink extends IoTDBSink { private boolean eLanguageEnable; private boolean useUdpTransport; + private boolean strictUnidirectional; private int udpPacketSizeInBytes; // The air gap connector does not use clientManager thus we put handshake type here @@ -209,6 +214,21 @@ public abstract class IoTDBAirGapSink extends IoTDBSink { CONNECTOR_AIR_GAP_TRANSPORT_SET, airGapTransport), airGapTransport); + validator + .validateAttributeValueRange( + CONNECTOR_AIR_GAP_STRICT_UNIDIRECTIONAL_KEY, true, "true", "false") + .validateAttributeValueRange(SINK_AIR_GAP_STRICT_UNIDIRECTIONAL_KEY, true, "true", "false") + .validate( + arg -> + !parameters.getBooleanOrDefault( + Arrays.asList( + CONNECTOR_AIR_GAP_STRICT_UNIDIRECTIONAL_KEY, + SINK_AIR_GAP_STRICT_UNIDIRECTIONAL_KEY), + CONNECTOR_AIR_GAP_STRICT_UNIDIRECTIONAL_DEFAULT_VALUE) + || CONNECTOR_AIR_GAP_TRANSPORT_UDP_VALUE.equals(airGapTransport), + PipeMessages.EXCEPTION_STRICTLY_UNIDIRECTIONAL_AIR_GAP_TRANSPORT_REQUIRES_UDP_78921AF9, + airGapTransport); + final int packetSize = parameters.getIntOrDefault( Arrays.asList(CONNECTOR_AIR_GAP_UDP_PACKET_SIZE_KEY, SINK_AIR_GAP_UDP_PACKET_SIZE_KEY), @@ -273,6 +293,17 @@ public abstract class IoTDBAirGapSink extends IoTDBSink { CONNECTOR_AIR_GAP_TRANSPORT_DEFAULT_VALUE) .trim() .toLowerCase()); + strictUnidirectional = + parameters.getBooleanOrDefault( + Arrays.asList( + CONNECTOR_AIR_GAP_STRICT_UNIDIRECTIONAL_KEY, + SINK_AIR_GAP_STRICT_UNIDIRECTIONAL_KEY), + CONNECTOR_AIR_GAP_STRICT_UNIDIRECTIONAL_DEFAULT_VALUE); + if (strictUnidirectional) { + LOGGER.warn( + PipeMessages + .LOG_STRICTLY_UNIDIRECTIONAL_UDP_TRANSPORT_IS_ENABLED_REQUESTS_ARE_SENT_WITHOUT_ACKNOWLEDGEMENTS_DELIVERY_AND_RECEIVER_ERRORS_CANNOT_BE_CONFIRMED_0A2F4382); + } udpPacketSizeInBytes = parameters.getIntOrDefault( Arrays.asList(CONNECTOR_AIR_GAP_UDP_PACKET_SIZE_KEY, SINK_AIR_GAP_UDP_PACKET_SIZE_KEY), @@ -370,7 +401,14 @@ public abstract class IoTDBAirGapSink extends IoTDBSink { supportModsIfIsDataNodeReceiver = true; } socket.setSoTimeout(PIPE_CONFIG.getPipeSinkTransferTimeoutMs()); - LOGGER.info(PipeMessages.HANDSHAKE_SUCCESS_SOCKET, socket); + if (strictUnidirectional) { + LOGGER.debug( + PipeMessages + .LOG_AIR_GAP_HANDSHAKE_REQUEST_SENT_ON_SOCKET_ARG_WITHOUT_WAITING_FOR_ACKNOWLEDGEMENT_E1ECD582, + socket); + } else { + LOGGER.info(PipeMessages.HANDSHAKE_SUCCESS_SOCKET, socket); + } } protected abstract byte[] generateHandShakeV1Payload() throws IOException; @@ -444,8 +482,19 @@ public abstract class IoTDBAirGapSink extends IoTDBSink { protected abstract byte[] getTransferMultiFilePieceBytes( final String fileName, final long position, final byte[] payLoad) throws IOException; - protected int nextSocketIndex() { - return loadBalancer.nextSocketIndex(); + protected int nextSocketIndex() throws IOException { + final int socketIndex = loadBalancer.nextSocketIndex(); + if (strictUnidirectional) { + // There is no feedback when a receiver restarts or its idle session expires. Re-establish + // the session before each event, never between the pieces/slices of the same file/request. + try { + sendHandshakeReq(sockets.get(socketIndex)); + } catch (final IOException e) { + isSocketAlive.set(socketIndex, false); + throw e; + } + } + return socketIndex; } protected boolean send( @@ -488,7 +537,9 @@ public abstract class IoTDBAirGapSink extends IoTDBSink { private boolean sendOneDatagram(final AirGapSocket socket, final TPipeTransferReq request) throws IOException { - final long requestId = udpRequestIdGenerator.incrementAndGet(); + final long requestId = + (udpRequestIdGenerator.incrementAndGet() & Long.MAX_VALUE) + | (strictUnidirectional ? AirGapUdpProtocol.NO_ACK_REQUEST_ID_MASK : 0); final ByteBuffer body = request.body.duplicate(); final int requestSize = Byte.BYTES + Short.BYTES + body.remaining(); final int eLanguageSize = @@ -536,6 +587,11 @@ public abstract class IoTDBAirGapSink extends IoTDBSink { socket.getDatagramAddress(), socket.getEndPoint().getPort())); + if (strictUnidirectional) { + // This only confirms a local send. A one-way link cannot report delivery or remote errors. + return true; + } + final int originalTimeoutMs = datagramSocket.getSoTimeout(); final long deadlineNanos = originalTimeoutMs == 0
