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 fea5bd2490a Fix oversized subscription requests with slicing (#18695)
fea5bd2490a is described below
commit fea5bd2490a8274cebf6fb197744d49321859de2
Author: Caideyipi <[email protected]>
AuthorDate: Mon Oct 12 10:39:19 2026 +0800
Fix oversized subscription requests with slicing (#18695)
---
.../subscription/i18n/SubscriptionMessages.java | 4 +
.../subscription/i18n/SubscriptionMessages.java | 4 +
.../payload/request/PipeSubscribeRequestType.java | 1 +
.../payload/request/PipeSubscribeSliceReq.java | 169 +++++++++++++++
.../request/PipeSubscribeSliceReqBuilder.java | 80 +++++++
.../request/PipeSubscribeSliceReqHandler.java | 141 +++++++++++++
.../SubscriptionSessionConnection.java | 8 +
.../base/AbstractSubscriptionProvider.java | 186 ++++++++++++++++-
.../payload/request/PipeSubscribeSliceReqTest.java | 203 ++++++++++++++++++
.../base/SubscriptionProviderSlicingTest.java | 232 +++++++++++++++++++++
.../receiver/SubscriptionReceiverV1.java | 52 +++++
.../receiver/SubscriptionReceiverV1Test.java | 55 +++++
12 files changed, 1134 insertions(+), 1 deletion(-)
diff --git
a/iotdb-client/subscription/src/main/i18n/en/org/apache/iotdb/rpc/subscription/i18n/SubscriptionMessages.java
b/iotdb-client/subscription/src/main/i18n/en/org/apache/iotdb/rpc/subscription/i18n/SubscriptionMessages.java
index 41bcc525d18..a161eea5151 100644
---
a/iotdb-client/subscription/src/main/i18n/en/org/apache/iotdb/rpc/subscription/i18n/SubscriptionMessages.java
+++
b/iotdb-client/subscription/src/main/i18n/en/org/apache/iotdb/rpc/subscription/i18n/SubscriptionMessages.java
@@ -21,6 +21,10 @@ package org.apache.iotdb.rpc.subscription.i18n;
public final class SubscriptionMessages {
+ public static final String
+
EXCEPTION_SUBSCRIPTION_REQUEST_EXCEEDS_THE_NEGOTIATED_FRAME_LIMIT_CA8DEB50 =
+ "Subscription request exceeds the negotiated frame limit";
+
// --- TopicConstant / ConsumerConstant ---
public static final String UTILITY_CLASS = "Utility class";
diff --git
a/iotdb-client/subscription/src/main/i18n/zh/org/apache/iotdb/rpc/subscription/i18n/SubscriptionMessages.java
b/iotdb-client/subscription/src/main/i18n/zh/org/apache/iotdb/rpc/subscription/i18n/SubscriptionMessages.java
index 89e4d59750a..75f3c62bdf9 100644
---
a/iotdb-client/subscription/src/main/i18n/zh/org/apache/iotdb/rpc/subscription/i18n/SubscriptionMessages.java
+++
b/iotdb-client/subscription/src/main/i18n/zh/org/apache/iotdb/rpc/subscription/i18n/SubscriptionMessages.java
@@ -21,6 +21,10 @@ package org.apache.iotdb.rpc.subscription.i18n;
public final class SubscriptionMessages {
+ public static final String
+
EXCEPTION_SUBSCRIPTION_REQUEST_EXCEEDS_THE_NEGOTIATED_FRAME_LIMIT_CA8DEB50 =
+ "订阅请求超过协商的帧大小限制";
+
// --- TopicConstant / ConsumerConstant ---
public static final String UTILITY_CLASS = "工具类";
diff --git
a/iotdb-client/subscription/src/main/java/org/apache/iotdb/rpc/subscription/payload/request/PipeSubscribeRequestType.java
b/iotdb-client/subscription/src/main/java/org/apache/iotdb/rpc/subscription/payload/request/PipeSubscribeRequestType.java
index 9fcc1d86b0c..5a6c414695c 100644
---
a/iotdb-client/subscription/src/main/java/org/apache/iotdb/rpc/subscription/payload/request/PipeSubscribeRequestType.java
+++
b/iotdb-client/subscription/src/main/java/org/apache/iotdb/rpc/subscription/payload/request/PipeSubscribeRequestType.java
@@ -32,6 +32,7 @@ public enum PipeSubscribeRequestType {
SUBSCRIBE((short) 5),
UNSUBSCRIBE((short) 6),
SEEK((short) 7),
+ SLICE((short) 8),
;
private final short type;
diff --git
a/iotdb-client/subscription/src/main/java/org/apache/iotdb/rpc/subscription/payload/request/PipeSubscribeSliceReq.java
b/iotdb-client/subscription/src/main/java/org/apache/iotdb/rpc/subscription/payload/request/PipeSubscribeSliceReq.java
new file mode 100644
index 00000000000..d783e93bf34
--- /dev/null
+++
b/iotdb-client/subscription/src/main/java/org/apache/iotdb/rpc/subscription/payload/request/PipeSubscribeSliceReq.java
@@ -0,0 +1,169 @@
+/*
+ * 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.rpc.subscription.payload.request;
+
+import org.apache.iotdb.service.rpc.thrift.TPipeSubscribeReq;
+
+import org.apache.tsfile.utils.Binary;
+import org.apache.tsfile.utils.PublicBAOS;
+import org.apache.tsfile.utils.ReadWriteIOUtils;
+
+import java.io.DataOutputStream;
+import java.io.IOException;
+import java.nio.ByteBuffer;
+import java.util.Arrays;
+import java.util.Objects;
+
+public class PipeSubscribeSliceReq extends TPipeSubscribeReq {
+
+ private transient int orderId;
+ private transient byte originReqVersion;
+ private transient short originReqType;
+ private transient int originBodySize;
+ private transient int sliceIndex;
+ private transient int sliceCount;
+ private transient byte[] sliceBody;
+
+ public int getOrderId() {
+ return orderId;
+ }
+
+ public byte getOriginReqVersion() {
+ return originReqVersion;
+ }
+
+ public short getOriginReqType() {
+ return originReqType;
+ }
+
+ public int getOriginBodySize() {
+ return originBodySize;
+ }
+
+ public int getSliceIndex() {
+ return sliceIndex;
+ }
+
+ public int getSliceCount() {
+ return sliceCount;
+ }
+
+ public byte[] getSliceBody() {
+ return sliceBody;
+ }
+
+ public static PipeSubscribeSliceReq toTPipeSubscribeReq(
+ final TPipeSubscribeReq originReq,
+ final int orderId,
+ final int sliceIndex,
+ final int sliceCount,
+ final int bodySizeLimit)
+ throws IOException {
+ final ByteBuffer originBody = originReq.body.duplicate();
+ final int originPosition = originBody.position();
+ final int originBodySize = originBody.remaining();
+ final int startOffset = sliceIndex * bodySizeLimit;
+ final int endOffset = startOffset + Math.min(bodySizeLimit, originBodySize
- startOffset);
+
+ final PipeSubscribeSliceReq sliceReq = new PipeSubscribeSliceReq();
+ sliceReq.orderId = orderId;
+ sliceReq.originReqVersion = originReq.getVersion();
+ sliceReq.originReqType = originReq.getType();
+ sliceReq.originBodySize = originBodySize;
+ sliceReq.sliceIndex = sliceIndex;
+ sliceReq.sliceCount = sliceCount;
+ sliceReq.sliceBody = new byte[endOffset - startOffset];
+ originBody.position(originPosition + startOffset);
+ originBody.get(sliceReq.sliceBody);
+
+ sliceReq.version = PipeSubscribeRequestVersion.VERSION_1.getVersion();
+ sliceReq.type = PipeSubscribeRequestType.SLICE.getType();
+ try (final PublicBAOS byteArrayOutputStream = new PublicBAOS();
+ final DataOutputStream outputStream = new
DataOutputStream(byteArrayOutputStream)) {
+ ReadWriteIOUtils.write(sliceReq.orderId, outputStream);
+ ReadWriteIOUtils.write(sliceReq.originReqVersion, outputStream);
+ ReadWriteIOUtils.write(sliceReq.originReqType, outputStream);
+ ReadWriteIOUtils.write(sliceReq.originBodySize, outputStream);
+ ReadWriteIOUtils.write(sliceReq.sliceIndex, outputStream);
+ ReadWriteIOUtils.write(sliceReq.sliceCount, outputStream);
+ ReadWriteIOUtils.write(new Binary(sliceReq.sliceBody), outputStream);
+ sliceReq.body =
+ ByteBuffer.wrap(byteArrayOutputStream.getBuf(), 0,
byteArrayOutputStream.size());
+ }
+ return sliceReq;
+ }
+
+ public static PipeSubscribeSliceReq fromTPipeSubscribeReq(final
TPipeSubscribeReq thriftReq) {
+ final PipeSubscribeSliceReq sliceReq = new PipeSubscribeSliceReq();
+ final ByteBuffer body = thriftReq.body.duplicate();
+ sliceReq.orderId = ReadWriteIOUtils.readInt(body);
+ sliceReq.originReqVersion = ReadWriteIOUtils.readByte(body);
+ sliceReq.originReqType = ReadWriteIOUtils.readShort(body);
+ sliceReq.originBodySize = ReadWriteIOUtils.readInt(body);
+ sliceReq.sliceIndex = ReadWriteIOUtils.readInt(body);
+ sliceReq.sliceCount = ReadWriteIOUtils.readInt(body);
+ final int sliceBodySize = ReadWriteIOUtils.readInt(body);
+ if (sliceBodySize < 0 || sliceBodySize != body.remaining()) {
+ throw new IllegalArgumentException();
+ }
+ sliceReq.sliceBody = new byte[sliceBodySize];
+ body.get(sliceReq.sliceBody);
+ sliceReq.version = thriftReq.version;
+ sliceReq.type = thriftReq.type;
+ sliceReq.body = thriftReq.body;
+ return sliceReq;
+ }
+
+ @Override
+ public boolean equals(final Object obj) {
+ if (this == obj) {
+ return true;
+ }
+ if (obj == null || getClass() != obj.getClass()) {
+ return false;
+ }
+ final PipeSubscribeSliceReq that = (PipeSubscribeSliceReq) obj;
+ return orderId == that.orderId
+ && originReqVersion == that.originReqVersion
+ && originReqType == that.originReqType
+ && originBodySize == that.originBodySize
+ && sliceIndex == that.sliceIndex
+ && sliceCount == that.sliceCount
+ && Arrays.equals(sliceBody, that.sliceBody)
+ && version == that.version
+ && type == that.type
+ && Objects.equals(body, that.body);
+ }
+
+ @Override
+ public int hashCode() {
+ return Objects.hash(
+ orderId,
+ originReqVersion,
+ originReqType,
+ originBodySize,
+ sliceIndex,
+ sliceCount,
+ Arrays.hashCode(sliceBody),
+ version,
+ type,
+ body);
+ }
+}
diff --git
a/iotdb-client/subscription/src/main/java/org/apache/iotdb/rpc/subscription/payload/request/PipeSubscribeSliceReqBuilder.java
b/iotdb-client/subscription/src/main/java/org/apache/iotdb/rpc/subscription/payload/request/PipeSubscribeSliceReqBuilder.java
new file mode 100644
index 00000000000..2a743cfbebd
--- /dev/null
+++
b/iotdb-client/subscription/src/main/java/org/apache/iotdb/rpc/subscription/payload/request/PipeSubscribeSliceReqBuilder.java
@@ -0,0 +1,80 @@
+/*
+ * 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.rpc.subscription.payload.request;
+
+import org.apache.iotdb.service.rpc.thrift.TPipeSubscribeReq;
+
+import java.io.IOException;
+import java.util.Objects;
+import java.util.concurrent.atomic.AtomicInteger;
+
+public final class PipeSubscribeSliceReqBuilder {
+
+ private static final AtomicInteger ORDER_ID_GENERATOR = new AtomicInteger(0);
+
+ private PipeSubscribeSliceReqBuilder() {}
+
+ public static boolean shouldSlice(final TPipeSubscribeReq req, final int
bodySizeLimit) {
+ return bodySizeLimit > 0
+ && Objects.nonNull(req)
+ && req.getVersion() ==
PipeSubscribeRequestVersion.VERSION_1.getVersion()
+ && req.isSetBody()
+ && req.body.remaining() > bodySizeLimit
+ && isSliceableRequestType(req.getType());
+ }
+
+ public static boolean isSliceableRequestType(final short type) {
+ return type != PipeSubscribeRequestType.HANDSHAKE.getType()
+ && type != PipeSubscribeRequestType.CLOSE.getType()
+ && type != PipeSubscribeRequestType.SLICE.getType()
+ && PipeSubscribeRequestType.isValidatedRequestType(type);
+ }
+
+ public static int nextOrderId() {
+ return ORDER_ID_GENERATOR.getAndIncrement();
+ }
+
+ public static int getSliceCount(final TPipeSubscribeReq req, final int
bodySizeLimit) {
+ if (bodySizeLimit <= 0 || Objects.isNull(req) || !req.isSetBody()) {
+ throw new IllegalArgumentException();
+ }
+ final int bodySize = req.body.remaining();
+ return bodySize == 0 ? 1 : (bodySize - 1) / bodySizeLimit + 1;
+ }
+
+ public static PipeSubscribeSliceReq buildSliceReq(
+ final TPipeSubscribeReq originReq,
+ final int orderId,
+ final int sliceIndex,
+ final int sliceCount,
+ final int bodySizeLimit)
+ throws IOException {
+ if (bodySizeLimit <= 0
+ || sliceCount <= 1
+ || sliceIndex < 0
+ || sliceIndex >= sliceCount
+ || !shouldSlice(originReq, bodySizeLimit)
+ || getSliceCount(originReq, bodySizeLimit) != sliceCount) {
+ throw new IllegalArgumentException();
+ }
+ return PipeSubscribeSliceReq.toTPipeSubscribeReq(
+ originReq, orderId, sliceIndex, sliceCount, bodySizeLimit);
+ }
+}
diff --git
a/iotdb-client/subscription/src/main/java/org/apache/iotdb/rpc/subscription/payload/request/PipeSubscribeSliceReqHandler.java
b/iotdb-client/subscription/src/main/java/org/apache/iotdb/rpc/subscription/payload/request/PipeSubscribeSliceReqHandler.java
new file mode 100644
index 00000000000..e62983aa4c6
--- /dev/null
+++
b/iotdb-client/subscription/src/main/java/org/apache/iotdb/rpc/subscription/payload/request/PipeSubscribeSliceReqHandler.java
@@ -0,0 +1,141 @@
+/*
+ * 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.rpc.subscription.payload.request;
+
+import org.apache.iotdb.service.rpc.thrift.TPipeSubscribeReq;
+
+import org.apache.tsfile.utils.PublicBAOS;
+
+import java.nio.ByteBuffer;
+import java.util.Objects;
+import java.util.Optional;
+
+public class PipeSubscribeSliceReqHandler {
+
+ private static final int INITIAL_BUFFER_SIZE = 8 * 1024;
+
+ private final int maxOriginBodySize;
+
+ private int orderId;
+ private byte originReqVersion = -1;
+ private short originReqType = -1;
+ private int originBodySize = -1;
+ private int sliceCount = -1;
+ private int nextSliceIndex;
+ private int receivedBodySize;
+ private PublicBAOS assembledBody;
+
+ public PipeSubscribeSliceReqHandler(final int maxOriginBodySize) {
+ if (maxOriginBodySize <= 0) {
+ throw new IllegalArgumentException();
+ }
+ this.maxOriginBodySize = maxOriginBodySize;
+ }
+
+ public synchronized boolean receiveSlice(final PipeSubscribeSliceReq req) {
+ if (!isValidSlice(req)) {
+ clear();
+ return false;
+ }
+
+ if (assembledBody == null) {
+ if (req.getSliceIndex() != 0) {
+ return false;
+ }
+ orderId = req.getOrderId();
+ originReqVersion = req.getOriginReqVersion();
+ originReqType = req.getOriginReqType();
+ originBodySize = req.getOriginBodySize();
+ sliceCount = req.getSliceCount();
+ assembledBody = new PublicBAOS(Math.min(INITIAL_BUFFER_SIZE,
originBodySize));
+ }
+
+ if (orderId != req.getOrderId()
+ || originReqVersion != req.getOriginReqVersion()
+ || originReqType != req.getOriginReqType()
+ || originBodySize != req.getOriginBodySize()
+ || sliceCount != req.getSliceCount()
+ || nextSliceIndex != req.getSliceIndex()) {
+ clear();
+ return false;
+ }
+
+ final byte[] sliceBody = req.getSliceBody();
+ if (receivedBodySize > originBodySize - sliceBody.length) {
+ clear();
+ return false;
+ }
+ assembledBody.write(sliceBody, 0, sliceBody.length);
+ receivedBodySize += sliceBody.length;
+ nextSliceIndex++;
+
+ if (nextSliceIndex < sliceCount && receivedBodySize >= originBodySize) {
+ clear();
+ return false;
+ }
+ if (nextSliceIndex == sliceCount && receivedBodySize != originBodySize) {
+ clear();
+ return false;
+ }
+ return true;
+ }
+
+ private boolean isValidSlice(final PipeSubscribeSliceReq req) {
+ return Objects.nonNull(req)
+ && req.getVersion() ==
PipeSubscribeRequestVersion.VERSION_1.getVersion()
+ && req.getType() == PipeSubscribeRequestType.SLICE.getType()
+ && req.getOriginReqVersion() ==
PipeSubscribeRequestVersion.VERSION_1.getVersion()
+ &&
PipeSubscribeSliceReqBuilder.isSliceableRequestType(req.getOriginReqType())
+ && req.getOriginBodySize() > 0
+ && req.getOriginBodySize() <= maxOriginBodySize
+ && req.getSliceCount() > 1
+ && req.getSliceIndex() >= 0
+ && req.getSliceIndex() < req.getSliceCount()
+ && Objects.nonNull(req.getSliceBody())
+ && req.getSliceBody().length > 0
+ && req.getSliceBody().length <= req.getOriginBodySize();
+ }
+
+ public synchronized Optional<TPipeSubscribeReq> makeReqIfComplete() {
+ if (assembledBody == null
+ || nextSliceIndex != sliceCount
+ || receivedBodySize != originBodySize) {
+ return Optional.empty();
+ }
+
+ final TPipeSubscribeReq req = new TPipeSubscribeReq();
+ req.version = originReqVersion;
+ req.type = originReqType;
+ req.body = ByteBuffer.wrap(assembledBody.getBuf(), 0,
assembledBody.size());
+ clear();
+ return Optional.of(req);
+ }
+
+ public synchronized void clear() {
+ orderId = 0;
+ originReqVersion = -1;
+ originReqType = -1;
+ originBodySize = -1;
+ sliceCount = -1;
+ nextSliceIndex = 0;
+ receivedBodySize = 0;
+ assembledBody = null;
+ }
+}
diff --git
a/iotdb-client/subscription/src/main/java/org/apache/iotdb/session/subscription/SubscriptionSessionConnection.java
b/iotdb-client/subscription/src/main/java/org/apache/iotdb/session/subscription/SubscriptionSessionConnection.java
index df3de1331e9..baff5f6034a 100644
---
a/iotdb-client/subscription/src/main/java/org/apache/iotdb/session/subscription/SubscriptionSessionConnection.java
+++
b/iotdb-client/subscription/src/main/java/org/apache/iotdb/session/subscription/SubscriptionSessionConnection.java
@@ -21,6 +21,7 @@ package org.apache.iotdb.session.subscription;
import org.apache.iotdb.common.rpc.thrift.TEndPoint;
import org.apache.iotdb.rpc.IoTDBConnectionException;
+import org.apache.iotdb.service.rpc.thrift.ServerProperties;
import org.apache.iotdb.service.rpc.thrift.TPipeSubscribeReq;
import org.apache.iotdb.service.rpc.thrift.TPipeSubscribeResp;
import org.apache.iotdb.session.Session;
@@ -59,6 +60,13 @@ public class SubscriptionSessionConnection extends
SessionConnection {
return client.pipeSubscribe(req);
}
+ public int getServerThriftMaxFrameSize(final int fallbackMaxFrameSize)
throws TException {
+ final ServerProperties properties = client.getProperties();
+ return properties.isSetThriftMaxFrameSize()
+ ? properties.getThriftMaxFrameSize()
+ : fallbackMaxFrameSize;
+ }
+
public boolean setTimeout(final int timeoutInMs) {
return setTransportTimeout(timeoutInMs);
}
diff --git
a/iotdb-client/subscription/src/main/java/org/apache/iotdb/session/subscription/consumer/base/AbstractSubscriptionProvider.java
b/iotdb-client/subscription/src/main/java/org/apache/iotdb/session/subscription/consumer/base/AbstractSubscriptionProvider.java
index 477a7ae45cb..db0ded818ec 100644
---
a/iotdb-client/subscription/src/main/java/org/apache/iotdb/session/subscription/consumer/base/AbstractSubscriptionProvider.java
+++
b/iotdb-client/subscription/src/main/java/org/apache/iotdb/session/subscription/consumer/base/AbstractSubscriptionProvider.java
@@ -22,6 +22,7 @@ package org.apache.iotdb.session.subscription.consumer.base;
import org.apache.iotdb.common.rpc.thrift.TEndPoint;
import org.apache.iotdb.common.rpc.thrift.TSStatus;
import org.apache.iotdb.rpc.IoTDBConnectionException;
+import org.apache.iotdb.rpc.TSStatusCode;
import org.apache.iotdb.rpc.subscription.config.ConsumerConfig;
import org.apache.iotdb.rpc.subscription.config.ConsumerConstant;
import org.apache.iotdb.rpc.subscription.config.TopicConfig;
@@ -36,6 +37,7 @@ import
org.apache.iotdb.rpc.subscription.i18n.SubscriptionMessages;
import org.apache.iotdb.rpc.subscription.payload.poll.PollFilePayload;
import org.apache.iotdb.rpc.subscription.payload.poll.PollPayload;
import org.apache.iotdb.rpc.subscription.payload.poll.PollTabletsPayload;
+import org.apache.iotdb.rpc.subscription.payload.poll.RegionProgress;
import
org.apache.iotdb.rpc.subscription.payload.poll.SubscriptionCommitContext;
import org.apache.iotdb.rpc.subscription.payload.poll.SubscriptionPollRequest;
import
org.apache.iotdb.rpc.subscription.payload.poll.SubscriptionPollRequestType;
@@ -45,6 +47,7 @@ import
org.apache.iotdb.rpc.subscription.payload.request.PipeSubscribeCloseReq;
import
org.apache.iotdb.rpc.subscription.payload.request.PipeSubscribeCommitReq;
import
org.apache.iotdb.rpc.subscription.payload.request.PipeSubscribeHandshakeReq;
import org.apache.iotdb.rpc.subscription.payload.request.PipeSubscribePollReq;
+import
org.apache.iotdb.rpc.subscription.payload.request.PipeSubscribeSliceReqBuilder;
import
org.apache.iotdb.rpc.subscription.payload.request.PipeSubscribeSubscribeReq;
import
org.apache.iotdb.rpc.subscription.payload.request.PipeSubscribeUnsubscribeReq;
import
org.apache.iotdb.rpc.subscription.payload.request.SubscriptionHeartbeatReq;
@@ -66,8 +69,11 @@ import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.io.IOException;
+import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
+import java.util.LinkedHashMap;
+import java.util.LinkedHashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
@@ -79,6 +85,9 @@ public abstract class AbstractSubscriptionProvider {
private static final Logger LOGGER =
LoggerFactory.getLogger(AbstractSubscriptionProvider.class);
private static final int CLOSE_TIMEOUT_IN_MS = 5_000;
+ private static final int REQUEST_FRAME_RESERVED_BYTES = 1024;
+ // A commit response echoes accepted contexts and may also carry committed
progress.
+ private static final int COMMIT_RESPONSE_HEADROOM_FACTOR = 3;
private static final String STATUS_FORMATTER = "Status code is [%s], status
message is [%s].";
private static final String INTERNAL_ERROR_FORMATTER =
@@ -101,6 +110,9 @@ public abstract class AbstractSubscriptionProvider {
private final AtomicBoolean isAvailable = new AtomicBoolean(false);
private final ReentrantLock rpcLock = new ReentrantLock();
+ private final int localThriftMaxFrameSize;
+ private volatile int requestBodySizeLimit;
+
private final TEndPoint endPoint;
private int dataNodeId;
@@ -149,6 +161,8 @@ public abstract class AbstractSubscriptionProvider {
this.username = username;
this.password = password;
this.encryptedPassword = encryptedPassword;
+ this.localThriftMaxFrameSize = thriftMaxFrameSize;
+ this.requestBodySizeLimit = getRequestBodySizeLimit(thriftMaxFrameSize);
this.heartbeatIntervalMs = heartbeatIntervalMs;
this.connectionTimeoutInMs = connectionTimeoutInMs;
}
@@ -161,12 +175,66 @@ public abstract class AbstractSubscriptionProvider {
throws TException, IoTDBConnectionException {
rpcLock.lock();
try {
- return getSessionConnection().pipeSubscribe(req);
+ final SubscriptionSessionConnection connection = getSessionConnection();
+ return pipeSubscribeWithOptionalSlicing(req, requestBodySizeLimit,
connection::pipeSubscribe);
} finally {
rpcLock.unlock();
}
}
+ static TPipeSubscribeResp pipeSubscribeWithOptionalSlicing(
+ final TPipeSubscribeReq req,
+ final int bodySizeLimit,
+ final PipeSubscribeRequestSender requestSender)
+ throws TException {
+ if (!PipeSubscribeSliceReqBuilder.shouldSlice(req, bodySizeLimit)) {
+ if (bodySizeLimit <= 0 || (req.isSetBody() && req.body.remaining() >
bodySizeLimit)) {
+ throw new TException(
+ SubscriptionMessages
+
.EXCEPTION_SUBSCRIPTION_REQUEST_EXCEEDS_THE_NEGOTIATED_FRAME_LIMIT_CA8DEB50);
+ }
+ return requestSender.send(req);
+ }
+
+ final int orderId = PipeSubscribeSliceReqBuilder.nextOrderId();
+ final int sliceCount = PipeSubscribeSliceReqBuilder.getSliceCount(req,
bodySizeLimit);
+ TPipeSubscribeResp resp = null;
+ for (int sliceIndex = 0; sliceIndex < sliceCount; sliceIndex++) {
+ try {
+ resp =
+ requestSender.send(
+ PipeSubscribeSliceReqBuilder.buildSliceReq(
+ req, orderId, sliceIndex, sliceCount, bodySizeLimit));
+ } catch (final IOException e) {
+ throw new TException(e);
+ }
+ if (sliceIndex < sliceCount - 1
+ && (resp == null
+ || resp.getStatus() == null
+ || resp.getStatus().getCode() !=
TSStatusCode.SUCCESS_STATUS.getStatusCode())) {
+ return resp;
+ }
+ }
+ return resp;
+ }
+
+ private static int getRequestBodySizeLimit(final int maxFrameSize) {
+ return Math.max(0, maxFrameSize - REQUEST_FRAME_RESERVED_BYTES);
+ }
+
+ private void updateRequestBodySizeLimit() {
+ try {
+ final int serverMaxFrameSize =
+
getSessionConnection().getServerThriftMaxFrameSize(localThriftMaxFrameSize);
+ if (serverMaxFrameSize > 0) {
+ requestBodySizeLimit =
+ getRequestBodySizeLimit(Math.min(localThriftMaxFrameSize,
serverMaxFrameSize));
+ }
+ } catch (final TException | IoTDBConnectionException ignored) {
+ // Keep the local limit for servers that do not expose their frame size.
+ }
+ }
+
boolean isAvailable() {
return isAvailable.get();
}
@@ -207,6 +275,7 @@ public abstract class AbstractSubscriptionProvider {
}
session.open(); // throw IoTDBConnectionException
+ updateRequestBodySizeLimit();
// TODO: pass the complete consumer parameter configuration to the server
final Map<String, String> consumerAttributes = new HashMap<>();
@@ -371,6 +440,40 @@ public abstract class AbstractSubscriptionProvider {
PipeSubscribeHeartbeatResp heartbeat(
final List<SubscriptionCommitContext> processorBufferedCommitContexts)
throws SubscriptionException {
+ final List<List<SubscriptionCommitContext>> batches;
+ try {
+ batches =
+ partitionCommitContexts(
+ processorBufferedCommitContexts, requestBodySizeLimit,
Integer.BYTES);
+ } catch (final IOException e) {
+ LOGGER.warn(
+ SubscriptionMessages
+
.LOG_IOEXCEPTION_OCCURRED_SUBSCRIPTIONPROVIDER_ARG_SERIALIZE_HEARTBEAT_REQUEST_ARG_634C8333,
+ this,
+ processorBufferedCommitContexts,
+ e);
+ throw new SubscriptionRuntimeNonCriticalException(e.getMessage(), e);
+ }
+
+ PipeSubscribeHeartbeatResp latestResp = null;
+ final Set<String> topicNamesToUnsubscribe = new LinkedHashSet<>();
+ rpcLock.lock();
+ try {
+ for (final List<SubscriptionCommitContext> batch : batches) {
+ latestResp = heartbeatOnce(batch);
+
topicNamesToUnsubscribe.addAll(latestResp.getTopicNamesToUnsubscribe());
+ }
+ } finally {
+ rpcLock.unlock();
+ }
+ latestResp.getTopicNamesToUnsubscribe().clear();
+ latestResp.getTopicNamesToUnsubscribe().addAll(topicNamesToUnsubscribe);
+ return latestResp;
+ }
+
+ private PipeSubscribeHeartbeatResp heartbeatOnce(
+ final List<SubscriptionCommitContext> processorBufferedCommitContexts)
+ throws SubscriptionException {
final SubscriptionHeartbeatReq req;
try {
req =
SubscriptionHeartbeatReq.toThriftReq(processorBufferedCommitContexts);
@@ -634,6 +737,41 @@ public abstract class AbstractSubscriptionProvider {
CommitResult commit(
final List<SubscriptionCommitContext> subscriptionCommitContexts, final
boolean nack)
throws SubscriptionException {
+ final List<List<SubscriptionCommitContext>> batches;
+ try {
+ batches =
+ partitionCommitContexts(
+ subscriptionCommitContexts,
+ Math.max(1, requestBodySizeLimit /
COMMIT_RESPONSE_HEADROOM_FACTOR),
+ Integer.BYTES + Byte.BYTES);
+ } catch (final IOException e) {
+ LOGGER.warn(
+ SubscriptionMessages
+
.LOG_IOEXCEPTION_OCCURRED_SUBSCRIPTIONPROVIDER_ARG_SERIALIZE_COMMIT_REQUEST_ARG_D5335538,
+ this,
+ subscriptionCommitContexts,
+ e);
+ throw new SubscriptionRuntimeNonCriticalException(e.getMessage(), e);
+ }
+
+ final List<SubscriptionCommitContext> acceptedCommitContexts = new
ArrayList<>();
+ final Map<String, TopicProgress> committedProgressByTopic = new
LinkedHashMap<>();
+ rpcLock.lock();
+ try {
+ for (final List<SubscriptionCommitContext> batch : batches) {
+ final CommitResult result = commitOnce(batch, nack);
+ acceptedCommitContexts.addAll(result.getAcceptedCommitContexts());
+ mergeTopicProgress(committedProgressByTopic,
result.getCommittedProgressByTopic());
+ }
+ } finally {
+ rpcLock.unlock();
+ }
+ return new CommitResult(acceptedCommitContexts, committedProgressByTopic);
+ }
+
+ private CommitResult commitOnce(
+ final List<SubscriptionCommitContext> subscriptionCommitContexts, final
boolean nack)
+ throws SubscriptionException {
final PipeSubscribeCommitReq req;
try {
req =
PipeSubscribeCommitReq.toTPipeSubscribeReq(subscriptionCommitContexts, nack);
@@ -666,6 +804,52 @@ public abstract class AbstractSubscriptionProvider {
commitResp.getAcceptedCommitContexts(),
commitResp.getCommittedProgressByTopic());
}
+ static List<List<SubscriptionCommitContext>> partitionCommitContexts(
+ final List<SubscriptionCommitContext> commitContexts,
+ final int maxBodySize,
+ final int fixedBodySize)
+ throws IOException {
+ if (commitContexts == null || commitContexts.isEmpty()) {
+ return Collections.singletonList(Collections.emptyList());
+ }
+
+ final List<List<SubscriptionCommitContext>> batches = new ArrayList<>();
+ List<SubscriptionCommitContext> currentBatch = new ArrayList<>();
+ long currentBodySize = fixedBodySize;
+ for (final SubscriptionCommitContext commitContext : commitContexts) {
+ final int contextSize =
SubscriptionCommitContext.serialize(commitContext).remaining();
+ if (!currentBatch.isEmpty() && currentBodySize + contextSize >
maxBodySize) {
+ batches.add(currentBatch);
+ currentBatch = new ArrayList<>();
+ currentBodySize = fixedBodySize;
+ }
+ currentBatch.add(commitContext);
+ currentBodySize += contextSize;
+ }
+ batches.add(currentBatch);
+ return batches;
+ }
+
+ static void mergeTopicProgress(
+ final Map<String, TopicProgress> target, final Map<String,
TopicProgress> source) {
+ source.forEach(
+ (topicName, newProgress) ->
+ target.merge(
+ topicName,
+ newProgress,
+ (oldProgress, ignored) -> {
+ final Map<String, RegionProgress> mergedRegionProgress =
+ new LinkedHashMap<>(oldProgress.getRegionProgress());
+ mergedRegionProgress.putAll(newProgress.getRegionProgress());
+ return new TopicProgress(mergedRegionProgress);
+ }));
+ }
+
+ @FunctionalInterface
+ interface PipeSubscribeRequestSender {
+ TPipeSubscribeResp send(TPipeSubscribeReq req) throws TException;
+ }
+
static final class CommitResult {
private static final CommitResult EMPTY =
new CommitResult(Collections.emptyList(), Collections.emptyMap());
diff --git
a/iotdb-client/subscription/src/test/java/org/apache/iotdb/rpc/subscription/payload/request/PipeSubscribeSliceReqTest.java
b/iotdb-client/subscription/src/test/java/org/apache/iotdb/rpc/subscription/payload/request/PipeSubscribeSliceReqTest.java
new file mode 100644
index 00000000000..4a29123b70b
--- /dev/null
+++
b/iotdb-client/subscription/src/test/java/org/apache/iotdb/rpc/subscription/payload/request/PipeSubscribeSliceReqTest.java
@@ -0,0 +1,203 @@
+/*
+ * 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.rpc.subscription.payload.request;
+
+import org.apache.iotdb.service.rpc.thrift.TPipeSubscribeReq;
+
+import org.apache.tsfile.utils.Binary;
+import org.apache.tsfile.utils.PublicBAOS;
+import org.apache.tsfile.utils.ReadWriteIOUtils;
+import org.junit.Assert;
+import org.junit.Test;
+
+import java.io.DataOutputStream;
+import java.io.IOException;
+import java.nio.ByteBuffer;
+import java.util.Optional;
+
+public class PipeSubscribeSliceReqTest {
+
+ @Test
+ public void testBuildAndReassembleSlicesFromNonZeroPosition() throws
Exception {
+ final byte[] bytes = new byte[] {99, 98, 0, 1, 2, 3, 4, 5, 6, 97};
+ final ByteBuffer body = ByteBuffer.wrap(bytes);
+ body.position(2);
+ body.limit(9);
+ final TPipeSubscribeReq req = request(PipeSubscribeRequestType.POLL, body);
+ final int originalPosition = body.position();
+ final int originalLimit = body.limit();
+
+ final int bodySizeLimit = 3;
+ final int sliceCount = PipeSubscribeSliceReqBuilder.getSliceCount(req,
bodySizeLimit);
+ Assert.assertEquals(3, sliceCount);
+ Assert.assertTrue(PipeSubscribeSliceReqBuilder.shouldSlice(req,
bodySizeLimit));
+
+ final PipeSubscribeSliceReqHandler handler = new
PipeSubscribeSliceReqHandler(10);
+ for (int sliceIndex = 0; sliceIndex < sliceCount; sliceIndex++) {
+ final PipeSubscribeSliceReq thriftSlice =
+ PipeSubscribeSliceReqBuilder.buildSliceReq(
+ req, -1, sliceIndex, sliceCount, bodySizeLimit);
+ final int sliceBodyPosition = thriftSlice.body.position();
+ final PipeSubscribeSliceReq slice =
PipeSubscribeSliceReq.fromTPipeSubscribeReq(thriftSlice);
+
+ Assert.assertEquals(sliceBodyPosition, thriftSlice.body.position());
+ Assert.assertEquals(-1, slice.getOrderId());
+ Assert.assertEquals(sliceIndex, slice.getSliceIndex());
+ Assert.assertEquals(sliceCount, slice.getSliceCount());
+ Assert.assertTrue(handler.receiveSlice(slice));
+ if (sliceIndex < sliceCount - 1) {
+ Assert.assertFalse(handler.makeReqIfComplete().isPresent());
+ }
+ }
+
+ final Optional<TPipeSubscribeReq> reassembled =
handler.makeReqIfComplete();
+ Assert.assertTrue(reassembled.isPresent());
+ Assert.assertEquals(req.getVersion(), reassembled.get().getVersion());
+ Assert.assertEquals(req.getType(), reassembled.get().getType());
+ Assert.assertArrayEquals(
+ new byte[] {0, 1, 2, 3, 4, 5, 6},
remainingBytes(reassembled.get().body));
+ Assert.assertEquals(originalPosition, body.position());
+ Assert.assertEquals(originalLimit, body.limit());
+ }
+
+ @Test
+ public void testRejectOutOfOrderSliceAndAcceptFreshRequest() throws
Exception {
+ final TPipeSubscribeReq req =
+ request(PipeSubscribeRequestType.SEEK, ByteBuffer.wrap(new byte[] {0,
1, 2, 3, 4, 5}));
+ final int sliceCount = PipeSubscribeSliceReqBuilder.getSliceCount(req, 2);
+ final PipeSubscribeSliceReqHandler handler = new
PipeSubscribeSliceReqHandler(10);
+
+ Assert.assertTrue(
+ handler.receiveSlice(PipeSubscribeSliceReqBuilder.buildSliceReq(req,
1, 0, sliceCount, 2)));
+ Assert.assertFalse(
+ handler.receiveSlice(PipeSubscribeSliceReqBuilder.buildSliceReq(req,
1, 2, sliceCount, 2)));
+ Assert.assertFalse(handler.makeReqIfComplete().isPresent());
+
+ for (int sliceIndex = 0; sliceIndex < sliceCount; sliceIndex++) {
+ Assert.assertTrue(
+ handler.receiveSlice(
+ PipeSubscribeSliceReqBuilder.buildSliceReq(req, 2, sliceIndex,
sliceCount, 2)));
+ }
+ Assert.assertArrayEquals(
+ new byte[] {0, 1, 2, 3, 4, 5},
remainingBytes(handler.makeReqIfComplete().get().body));
+ }
+
+ @Test
+ public void testRejectChangedMetadataAndBodySizeViolations() throws
Exception {
+ final PipeSubscribeSliceReqHandler handler = new
PipeSubscribeSliceReqHandler(10);
+
+ Assert.assertTrue(
+ handler.receiveSlice(slice(1, PipeSubscribeRequestType.POLL, 4, 0, 2,
new byte[] {0, 1})));
+ Assert.assertFalse(
+ handler.receiveSlice(slice(2, PipeSubscribeRequestType.POLL, 4, 1, 2,
new byte[] {2, 3})));
+
+ Assert.assertFalse(
+ handler.receiveSlice(slice(1, PipeSubscribeRequestType.POLL, 11, 0, 2,
new byte[] {0})));
+ Assert.assertFalse(
+ handler.receiveSlice(slice(1, PipeSubscribeRequestType.POLL, 2, 0, 2,
new byte[] {0, 1})));
+
+ Assert.assertTrue(
+ handler.receiveSlice(slice(1, PipeSubscribeRequestType.POLL, 3, 0, 2,
new byte[] {0})));
+ Assert.assertFalse(
+ handler.receiveSlice(slice(1, PipeSubscribeRequestType.POLL, 3, 1, 2,
new byte[] {1})));
+ Assert.assertFalse(handler.makeReqIfComplete().isPresent());
+ }
+
+ @Test
+ public void testRejectUnsliceableOriginRequestTypes() {
+ for (final PipeSubscribeRequestType type :
+ new PipeSubscribeRequestType[] {
+ PipeSubscribeRequestType.HANDSHAKE,
+ PipeSubscribeRequestType.CLOSE,
+ PipeSubscribeRequestType.SLICE
+ }) {
+ final TPipeSubscribeReq req = request(type, ByteBuffer.wrap(new byte[]
{0, 1}));
+ Assert.assertFalse(PipeSubscribeSliceReqBuilder.shouldSlice(req, 1));
+ Assert.assertThrows(
+ IllegalArgumentException.class,
+ () -> PipeSubscribeSliceReqBuilder.buildSliceReq(req, 1, 0, 2, 1));
+ }
+
+ final PipeSubscribeSliceReqHandler handler = new
PipeSubscribeSliceReqHandler(10);
+ Assert.assertFalse(
+ handler.receiveSlice(slice(1, PipeSubscribeRequestType.SLICE, 2, 0, 2,
new byte[] {0})));
+ }
+
+ @Test
+ public void testRejectMalformedSliceEnvelopeBeforeAllocatingBody() throws
Exception {
+ final TPipeSubscribeReq req =
+ request(PipeSubscribeRequestType.POLL, ByteBuffer.wrap(new byte[] {0,
1}));
+ final PipeSubscribeSliceReq malformedSlice =
+ PipeSubscribeSliceReqBuilder.buildSliceReq(req, 1, 0, 2, 1);
+ malformedSlice.body.putInt(malformedSlice.body.position() + 19,
Integer.MAX_VALUE);
+
+ Assert.assertThrows(
+ IllegalArgumentException.class,
+ () -> PipeSubscribeSliceReq.fromTPipeSubscribeReq(malformedSlice));
+
+ final PipeSubscribeSliceReq invalidVersionSlice =
+ PipeSubscribeSliceReqBuilder.buildSliceReq(req, 1, 0, 2, 1);
+ invalidVersionSlice.version = (byte) -1;
+ Assert.assertFalse(new
PipeSubscribeSliceReqHandler(10).receiveSlice(invalidVersionSlice));
+ }
+
+ private static TPipeSubscribeReq request(
+ final PipeSubscribeRequestType type, final ByteBuffer body) {
+ final TPipeSubscribeReq req = new TPipeSubscribeReq();
+ req.version = PipeSubscribeRequestVersion.VERSION_1.getVersion();
+ req.type = type.getType();
+ req.body = body;
+ return req;
+ }
+
+ private static PipeSubscribeSliceReq slice(
+ final int orderId,
+ final PipeSubscribeRequestType originType,
+ final int originBodySize,
+ final int sliceIndex,
+ final int sliceCount,
+ final byte[] sliceBody) {
+ final TPipeSubscribeReq thriftReq = new TPipeSubscribeReq();
+ thriftReq.version = PipeSubscribeRequestVersion.VERSION_1.getVersion();
+ thriftReq.type = PipeSubscribeRequestType.SLICE.getType();
+ try (final PublicBAOS byteArrayOutputStream = new PublicBAOS();
+ final DataOutputStream outputStream = new
DataOutputStream(byteArrayOutputStream)) {
+ ReadWriteIOUtils.write(orderId, outputStream);
+
ReadWriteIOUtils.write(PipeSubscribeRequestVersion.VERSION_1.getVersion(),
outputStream);
+ ReadWriteIOUtils.write(originType.getType(), outputStream);
+ ReadWriteIOUtils.write(originBodySize, outputStream);
+ ReadWriteIOUtils.write(sliceIndex, outputStream);
+ ReadWriteIOUtils.write(sliceCount, outputStream);
+ ReadWriteIOUtils.write(new Binary(sliceBody), outputStream);
+ thriftReq.body =
+ ByteBuffer.wrap(byteArrayOutputStream.getBuf(), 0,
byteArrayOutputStream.size());
+ } catch (final IOException e) {
+ throw new AssertionError(e);
+ }
+ return PipeSubscribeSliceReq.fromTPipeSubscribeReq(thriftReq);
+ }
+
+ private static byte[] remainingBytes(final ByteBuffer buffer) {
+ final ByteBuffer duplicate = buffer.duplicate();
+ final byte[] bytes = new byte[duplicate.remaining()];
+ duplicate.get(bytes);
+ return bytes;
+ }
+}
diff --git
a/iotdb-client/subscription/src/test/java/org/apache/iotdb/session/subscription/consumer/base/SubscriptionProviderSlicingTest.java
b/iotdb-client/subscription/src/test/java/org/apache/iotdb/session/subscription/consumer/base/SubscriptionProviderSlicingTest.java
new file mode 100644
index 00000000000..31db29408bc
--- /dev/null
+++
b/iotdb-client/subscription/src/test/java/org/apache/iotdb/session/subscription/consumer/base/SubscriptionProviderSlicingTest.java
@@ -0,0 +1,232 @@
+/*
+ * 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.session.subscription.consumer.base;
+
+import org.apache.iotdb.common.rpc.thrift.TSStatus;
+import org.apache.iotdb.rpc.TSStatusCode;
+import org.apache.iotdb.rpc.subscription.payload.poll.RegionProgress;
+import
org.apache.iotdb.rpc.subscription.payload.poll.SubscriptionCommitContext;
+import org.apache.iotdb.rpc.subscription.payload.poll.TopicProgress;
+import org.apache.iotdb.rpc.subscription.payload.poll.WriterId;
+import org.apache.iotdb.rpc.subscription.payload.poll.WriterProgress;
+import
org.apache.iotdb.rpc.subscription.payload.request.PipeSubscribeRequestType;
+import
org.apache.iotdb.rpc.subscription.payload.request.PipeSubscribeRequestVersion;
+import org.apache.iotdb.rpc.subscription.payload.request.PipeSubscribeSliceReq;
+import
org.apache.iotdb.rpc.subscription.payload.request.SubscriptionHeartbeatReq;
+import
org.apache.iotdb.rpc.subscription.payload.response.PipeSubscribeResponseType;
+import
org.apache.iotdb.rpc.subscription.payload.response.PipeSubscribeResponseVersion;
+import org.apache.iotdb.service.rpc.thrift.TPipeSubscribeReq;
+import org.apache.iotdb.service.rpc.thrift.TPipeSubscribeResp;
+
+import org.apache.thrift.TException;
+import org.junit.Assert;
+import org.junit.Test;
+
+import java.nio.ByteBuffer;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.LinkedHashMap;
+import java.util.List;
+import java.util.Map;
+
+public class SubscriptionProviderSlicingTest {
+
+ @Test
+ public void testSendSlicesInOrderAndReturnFinalResponse() throws Exception {
+ final TPipeSubscribeReq req = request(new byte[] {0, 1, 2, 3, 4, 5, 6});
+ final List<TPipeSubscribeReq> sentRequests = new ArrayList<>();
+ final TPipeSubscribeResp finalResp = response(TSStatusCode.SUCCESS_STATUS);
+
+ final TPipeSubscribeResp actualResp =
+ AbstractSubscriptionProvider.pipeSubscribeWithOptionalSlicing(
+ req,
+ 3,
+ sentReq -> {
+ sentRequests.add(sentReq);
+ return sentRequests.size() == 3 ? finalResp :
response(TSStatusCode.SUCCESS_STATUS);
+ });
+
+ Assert.assertSame(finalResp, actualResp);
+ Assert.assertEquals(3, sentRequests.size());
+ Integer orderId = null;
+ for (int sliceIndex = 0; sliceIndex < sentRequests.size(); sliceIndex++) {
+ final PipeSubscribeSliceReq slice =
+
PipeSubscribeSliceReq.fromTPipeSubscribeReq(sentRequests.get(sliceIndex));
+ if (orderId == null) {
+ orderId = slice.getOrderId();
+ }
+ Assert.assertEquals(orderId.intValue(), slice.getOrderId());
+ Assert.assertEquals(sliceIndex, slice.getSliceIndex());
+ Assert.assertEquals(sentRequests.size(), slice.getSliceCount());
+ }
+ }
+
+ @Test
+ public void testStopSendingAfterIntermediateSliceError() throws Exception {
+ final TPipeSubscribeReq req = request(new byte[] {0, 1, 2, 3, 4});
+ final List<TPipeSubscribeReq> sentRequests = new ArrayList<>();
+ final TPipeSubscribeResp errorResp =
response(TSStatusCode.SUBSCRIPTION_TYPE_ERROR);
+
+ final TPipeSubscribeResp actualResp =
+ AbstractSubscriptionProvider.pipeSubscribeWithOptionalSlicing(
+ req,
+ 2,
+ sentReq -> {
+ sentRequests.add(sentReq);
+ return sentRequests.size() == 2 ? errorResp :
response(TSStatusCode.SUCCESS_STATUS);
+ });
+
+ Assert.assertSame(errorResp, actualResp);
+ Assert.assertEquals(2, sentRequests.size());
+ }
+
+ @Test
+ public void testSendSmallRequestWithoutWrapping() throws Exception {
+ final TPipeSubscribeReq req = request(new byte[] {0, 1, 2});
+ final List<TPipeSubscribeReq> sentRequests = new ArrayList<>();
+ final TPipeSubscribeResp expectedResp =
response(TSStatusCode.SUCCESS_STATUS);
+
+ final TPipeSubscribeResp actualResp =
+ AbstractSubscriptionProvider.pipeSubscribeWithOptionalSlicing(
+ req,
+ 3,
+ sentReq -> {
+ sentRequests.add(sentReq);
+ return expectedResp;
+ });
+
+ Assert.assertSame(expectedResp, actualResp);
+ Assert.assertEquals(Collections.singletonList(req), sentRequests);
+ }
+
+ @Test
+ public void testRejectOversizedUnsliceableRequest() {
+ for (final PipeSubscribeRequestType type :
+ new PipeSubscribeRequestType[] {
+ PipeSubscribeRequestType.HANDSHAKE, PipeSubscribeRequestType.CLOSE
+ }) {
+ final TPipeSubscribeReq req = request(new byte[] {0, 1, 2, 3});
+ req.type = type.getType();
+ Assert.assertThrows(
+ TException.class,
+ () ->
+ AbstractSubscriptionProvider.pipeSubscribeWithOptionalSlicing(
+ req,
+ 3,
+ ignored -> {
+ Assert.fail();
+ return null;
+ }));
+ }
+ Assert.assertThrows(
+ TException.class,
+ () ->
+ AbstractSubscriptionProvider.pipeSubscribeWithOptionalSlicing(
+ request(new byte[] {0}),
+ 0,
+ ignored -> {
+ Assert.fail();
+ return null;
+ }));
+ }
+
+ @Test
+ public void testPartitionCommitContextsBySerializedSize() throws Exception {
+ final SubscriptionCommitContext first = commitContext("a", 1);
+ final SubscriptionCommitContext second = commitContext("bb", 2);
+ final SubscriptionCommitContext third = commitContext("ccc", 3);
+ final int fixedBodySize = Integer.BYTES;
+ final int maxBodySize =
+ fixedBodySize
+ + SubscriptionCommitContext.serialize(first).remaining()
+ + SubscriptionCommitContext.serialize(second).remaining();
+
+ final List<List<SubscriptionCommitContext>> batches =
+ AbstractSubscriptionProvider.partitionCommitContexts(
+ Arrays.asList(first, second, third), maxBodySize, fixedBodySize);
+
+ Assert.assertEquals(2, batches.size());
+ Assert.assertEquals(Arrays.asList(first, second), batches.get(0));
+ Assert.assertEquals(Collections.singletonList(third), batches.get(1));
+ for (final List<SubscriptionCommitContext> batch : batches) {
+ Assert.assertTrue(
+ SubscriptionHeartbeatReq.toThriftReq(batch).body.remaining() <=
maxBodySize);
+ }
+ Assert.assertEquals(
+ Collections.singletonList(Collections.emptyList()),
+ AbstractSubscriptionProvider.partitionCommitContexts(
+ Collections.emptyList(), maxBodySize, fixedBodySize));
+ }
+
+ @Test
+ public void testMergeTopicProgressAcrossBatches() {
+ final RegionProgress firstRegionProgress = regionProgress("1_1", 1, 1);
+ final RegionProgress updatedFirstRegionProgress = regionProgress("1_1", 1,
2);
+ final RegionProgress secondRegionProgress = regionProgress("1_2", 2, 3);
+ final Map<String, RegionProgress> initialRegions = new LinkedHashMap<>();
+ initialRegions.put("1_1", firstRegionProgress);
+ final Map<String, TopicProgress> target = new LinkedHashMap<>();
+ target.put("topic", new TopicProgress(initialRegions));
+
+ final Map<String, RegionProgress> newRegions = new LinkedHashMap<>();
+ newRegions.put("1_1", updatedFirstRegionProgress);
+ newRegions.put("1_2", secondRegionProgress);
+ final Map<String, TopicProgress> source = new LinkedHashMap<>();
+ source.put("topic", new TopicProgress(newRegions));
+ source.put(
+ "another-topic", new TopicProgress(Collections.singletonMap("2_1",
secondRegionProgress)));
+
+ AbstractSubscriptionProvider.mergeTopicProgress(target, source);
+
+ Assert.assertEquals(2, target.size());
+ Assert.assertEquals(
+ updatedFirstRegionProgress,
target.get("topic").getRegionProgress().get("1_1"));
+ Assert.assertEquals(secondRegionProgress,
target.get("topic").getRegionProgress().get("1_2"));
+ Assert.assertEquals(source.get("another-topic"),
target.get("another-topic"));
+ }
+
+ private static TPipeSubscribeReq request(final byte[] body) {
+ final TPipeSubscribeReq req = new TPipeSubscribeReq();
+ req.version = PipeSubscribeRequestVersion.VERSION_1.getVersion();
+ req.type = PipeSubscribeRequestType.POLL.getType();
+ req.body = ByteBuffer.wrap(body);
+ return req;
+ }
+
+ private static TPipeSubscribeResp response(final TSStatusCode statusCode) {
+ return new TPipeSubscribeResp(
+ new TSStatus(statusCode.getStatusCode()),
+ PipeSubscribeResponseVersion.VERSION_1.getVersion(),
+ PipeSubscribeResponseType.ACK.getType());
+ }
+
+ private static SubscriptionCommitContext commitContext(
+ final String topicName, final long commitId) {
+ return new SubscriptionCommitContext(1, 1, topicName, "group", commitId);
+ }
+
+ private static RegionProgress regionProgress(
+ final String regionId, final int nodeId, final long localSequence) {
+ return new RegionProgress(
+ Collections.singletonMap(
+ new WriterId(regionId, nodeId), new WriterProgress(1L,
localSequence)));
+ }
+}
diff --git
a/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/subscription/receiver/SubscriptionReceiverV1.java
b/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/subscription/receiver/SubscriptionReceiverV1.java
index 2c2b00eca12..dd897f3a162 100644
---
a/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/subscription/receiver/SubscriptionReceiverV1.java
+++
b/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/subscription/receiver/SubscriptionReceiverV1.java
@@ -67,6 +67,8 @@ import
org.apache.iotdb.rpc.subscription.payload.request.PipeSubscribeHandshakeR
import org.apache.iotdb.rpc.subscription.payload.request.PipeSubscribePollReq;
import
org.apache.iotdb.rpc.subscription.payload.request.PipeSubscribeRequestType;
import
org.apache.iotdb.rpc.subscription.payload.request.PipeSubscribeRequestVersion;
+import org.apache.iotdb.rpc.subscription.payload.request.PipeSubscribeSliceReq;
+import
org.apache.iotdb.rpc.subscription.payload.request.PipeSubscribeSliceReqHandler;
import
org.apache.iotdb.rpc.subscription.payload.request.PipeSubscribeSubscribeReq;
import
org.apache.iotdb.rpc.subscription.payload.request.PipeSubscribeUnsubscribeReq;
import
org.apache.iotdb.rpc.subscription.payload.request.SubscriptionHeartbeatReq;
@@ -110,6 +112,8 @@ public class SubscriptionReceiverV1 implements
SubscriptionReceiver {
private static final double POLL_PAYLOAD_SIZE_EXCEED_THRESHOLD = 0.9;
private static final long HEARTBEAT_TIMEOUT_MULTIPLIER = 3L;
+ private static final long MAX_REASSEMBLED_REQUEST_BODY_SIZE = 256L * 1024 *
1024;
+ private static final int MAX_REASSEMBLED_REQUEST_FRAME_COUNT = 4;
private static final IClientManager<ConfigRegionId, ConfigNodeClient>
CONFIG_NODE_CLIENT_MANAGER =
ConfigNodeClientManager.getInstance();
@@ -131,6 +135,18 @@ public class SubscriptionReceiverV1 implements
SubscriptionReceiver {
PipeSubscribeResponseVersion.VERSION_1.getVersion(),
PipeSubscribeResponseType.ACK.getType());
+ private static final TPipeSubscribeResp SUBSCRIPTION_SLICE_ACK_RESP =
+ new TPipeSubscribeResp(
+ new TSStatus(TSStatusCode.SUCCESS_STATUS.getStatusCode()),
+ PipeSubscribeResponseVersion.VERSION_1.getVersion(),
+ PipeSubscribeResponseType.ACK.getType());
+
+ private static final TPipeSubscribeResp SUBSCRIPTION_SLICE_ERROR_RESP =
+ new TPipeSubscribeResp(
+ new TSStatus(TSStatusCode.SUBSCRIPTION_TYPE_ERROR.getStatusCode()),
+ PipeSubscribeResponseVersion.VERSION_1.getVersion(),
+ PipeSubscribeResponseType.ACK.getType());
+
private final ThreadLocal<ConsumerConfig> consumerConfigThreadLocal = new
ThreadLocal<>();
private final ThreadLocal<PollTimer> pollTimerThreadLocal = new
ThreadLocal<>();
private volatile String authenticatedUsername;
@@ -139,6 +155,8 @@ public class SubscriptionReceiverV1 implements
SubscriptionReceiver {
private volatile boolean consumerFenced;
private volatile long lastActivityTimeMs = System.currentTimeMillis();
private final AtomicLong inFlightRequestCount = new AtomicLong(0);
+ private final PipeSubscribeSliceReqHandler sliceReqHandler =
+ new PipeSubscribeSliceReqHandler(getMaxReassembledRequestBodySize());
private long consumerStateVersion;
private static final String SQL_DIALECT_TABLE_VALUE = "table";
@@ -146,6 +164,9 @@ public class SubscriptionReceiverV1 implements
SubscriptionReceiver {
@Override
public final TPipeSubscribeResp handle(final TPipeSubscribeReq req) {
final short reqType = req.getType();
+ if (PipeSubscribeRequestType.SLICE.getType() != reqType) {
+ sliceReqHandler.clear();
+ }
final boolean isFencedRequest = beforeHandle(reqType);
try {
if (isFencedRequest) {
@@ -172,6 +193,8 @@ public class SubscriptionReceiverV1 implements
SubscriptionReceiver {
return
handlePipeSubscribeClose(PipeSubscribeCloseReq.fromTPipeSubscribeReq(req));
case SEEK:
return
handleSubscriptionSeek(SubscriptionSeekReq.fromThriftReq(req));
+ case SLICE:
+ return handlePipeSubscribeSlice(req);
default:
break;
}
@@ -228,6 +251,7 @@ public class SubscriptionReceiverV1 implements
SubscriptionReceiver {
public void invalidateConsumer() {
synchronized (this) {
consumerFenced = true;
+ sliceReqHandler.clear();
clearSharedConsumerState();
}
}
@@ -264,6 +288,7 @@ public class SubscriptionReceiverV1 implements
SubscriptionReceiver {
}
}
} finally {
+ sliceReqHandler.clear();
consumerConfigThreadLocal.remove();
pollTimerThreadLocal.remove();
authenticatedUsername = null;
@@ -271,6 +296,33 @@ public class SubscriptionReceiverV1 implements
SubscriptionReceiver {
}
}
+ private TPipeSubscribeResp handlePipeSubscribeSlice(final TPipeSubscribeReq
thriftReq) {
+ final PipeSubscribeSliceReq sliceReq;
+ try {
+ sliceReq = PipeSubscribeSliceReq.fromTPipeSubscribeReq(thriftReq);
+ } catch (final RuntimeException e) {
+ sliceReqHandler.clear();
+ return SUBSCRIPTION_SLICE_ERROR_RESP;
+ }
+ if (!sliceReqHandler.receiveSlice(sliceReq)) {
+ return SUBSCRIPTION_SLICE_ERROR_RESP;
+ }
+ return sliceReqHandler
+ .makeReqIfComplete()
+ .map(this::handle)
+ .orElse(SUBSCRIPTION_SLICE_ACK_RESP);
+ }
+
+ private static int getMaxReassembledRequestBodySize() {
+ final long maxFrameSize =
IoTDBDescriptor.getInstance().getConfig().getThriftMaxFrameSize();
+ final long desiredSize =
+ Math.min(
+ MAX_REASSEMBLED_REQUEST_BODY_SIZE, maxFrameSize *
MAX_REASSEMBLED_REQUEST_FRAME_COUNT);
+ final long memoryLimit = Math.max(maxFrameSize,
Runtime.getRuntime().maxMemory() / 16);
+ return (int)
+ Math.min(Integer.MAX_VALUE, Math.max(maxFrameSize,
Math.min(desiredSize, memoryLimit)));
+ }
+
@Override
public void handleTimeout() {
synchronized (this) {
diff --git
a/iotdb-core/datanode/src/test/java/org/apache/iotdb/db/subscription/receiver/SubscriptionReceiverV1Test.java
b/iotdb-core/datanode/src/test/java/org/apache/iotdb/db/subscription/receiver/SubscriptionReceiverV1Test.java
index 8b2bd022241..a8c7dc7841f 100644
---
a/iotdb-core/datanode/src/test/java/org/apache/iotdb/db/subscription/receiver/SubscriptionReceiverV1Test.java
+++
b/iotdb-core/datanode/src/test/java/org/apache/iotdb/db/subscription/receiver/SubscriptionReceiverV1Test.java
@@ -30,7 +30,10 @@ import
org.apache.iotdb.rpc.subscription.config.ConsumerConstant;
import org.apache.iotdb.rpc.subscription.config.TopicConstant;
import
org.apache.iotdb.rpc.subscription.payload.poll.SubscriptionCommitContext;
import
org.apache.iotdb.rpc.subscription.payload.request.PipeSubscribeHandshakeReq;
+import
org.apache.iotdb.rpc.subscription.payload.request.PipeSubscribeSliceReqBuilder;
import
org.apache.iotdb.rpc.subscription.payload.request.SubscriptionHeartbeatReq;
+import org.apache.iotdb.service.rpc.thrift.TPipeSubscribeReq;
+import org.apache.iotdb.service.rpc.thrift.TPipeSubscribeResp;
import org.junit.Assert;
import org.junit.Test;
@@ -221,6 +224,58 @@ public class SubscriptionReceiverV1Test {
receiver.handle(SubscriptionHeartbeatReq.toThriftReq()).getStatus().getCode());
}
+ @Test
+ public void testHandleSlicesDispatchesReassembledRequest() throws Exception {
+ final SubscriptionReceiverV1 receiver = new SubscriptionReceiverV1();
+ final TPipeSubscribeReq heartbeatReq =
+ SubscriptionHeartbeatReq.toThriftReq(Collections.emptyList());
+ final int bodySizeLimit = 2;
+ final int sliceCount =
PipeSubscribeSliceReqBuilder.getSliceCount(heartbeatReq, bodySizeLimit);
+
+ final TPipeSubscribeResp firstResp =
+ receiver.handle(
+ PipeSubscribeSliceReqBuilder.buildSliceReq(
+ heartbeatReq, 1, 0, sliceCount, bodySizeLimit));
+ Assert.assertEquals(
+ TSStatusCode.SUCCESS_STATUS.getStatusCode(),
firstResp.getStatus().getCode());
+
+ final TPipeSubscribeResp lastResp =
+ receiver.handle(
+ PipeSubscribeSliceReqBuilder.buildSliceReq(
+ heartbeatReq, 1, 1, sliceCount, bodySizeLimit));
+ Assert.assertEquals(
+ TSStatusCode.SUBSCRIPTION_MISSING_CONSUMER.getStatusCode(),
lastResp.getStatus().getCode());
+ }
+
+ @Test
+ public void testNonSliceRequestClearsIncompleteSlices() throws Exception {
+ final SubscriptionReceiverV1 receiver = new SubscriptionReceiverV1();
+ final TPipeSubscribeReq heartbeatReq =
+ SubscriptionHeartbeatReq.toThriftReq(Collections.emptyList());
+ final int bodySizeLimit = 2;
+ final int sliceCount =
PipeSubscribeSliceReqBuilder.getSliceCount(heartbeatReq, bodySizeLimit);
+
+ Assert.assertEquals(
+ TSStatusCode.SUCCESS_STATUS.getStatusCode(),
+ receiver
+ .handle(
+ PipeSubscribeSliceReqBuilder.buildSliceReq(
+ heartbeatReq, 1, 0, sliceCount, bodySizeLimit))
+ .getStatus()
+ .getCode());
+ Assert.assertEquals(
+ TSStatusCode.SUBSCRIPTION_MISSING_CONSUMER.getStatusCode(),
+
receiver.handle(SubscriptionHeartbeatReq.toThriftReq()).getStatus().getCode());
+ Assert.assertEquals(
+ TSStatusCode.SUBSCRIPTION_TYPE_ERROR.getStatusCode(),
+ receiver
+ .handle(
+ PipeSubscribeSliceReqBuilder.buildSliceReq(
+ heartbeatReq, 1, 1, sliceCount, bodySizeLimit))
+ .getStatus()
+ .getCode());
+ }
+
@Test
public void testCalculateConsumerInactivityTimeoutUsesDefaultTimeout()
throws Exception {
final SubscriptionReceiverV1 receiver = new SubscriptionReceiverV1();