diff --git a/application/src/main/resources/thingsboard.yml b/application/src/main/resources/thingsboard.yml
index 003cee2eb7..64b9e4e58e 100644
--- a/application/src/main/resources/thingsboard.yml
+++ b/application/src/main/resources/thingsboard.yml
@@ -614,6 +614,13 @@ actors:
#
- QoS level 0: This feature does not apply, as no acknowledgment is expected, and therefore no timeout is triggered.
# - QoS level 1: This feature applies, as an acknowledgment is expected.
# - QoS level 2: Unsupported.
+ # For MQTT Gateway (connected sub-devices): the same QoS rules as MQTT transport apply
+ # (only a QoS level 1 PUBACK triggers a delivery timeout). However, the gateway's own transport
+ # session is never closed, so the gateway connection and its other sub-devices stay unaffected.
+ # Only the sub-device session is deregistered and the RPC is reverted to the queued state. It is
+ # redelivered automatically once the sub-device session is re-registered, which happens on the next
+ # gateway message for that device (connect, telemetry, or attribute publish): the RPC subscription
+ # is reapplied and the queued RPC is resent. Until then the RPC remains queued.
# For CoAP transport:
# - Confirmable requests: This feature applies, as delivery confirmation is expected.
# - Non-confirmable requests: This feature does not apply, as no delivery acknowledgment is expected.
diff --git a/application/src/test/java/org/thingsboard/server/transport/mqtt/mqttv3/rpc/AbstractMqttServerSideRpcIntegrationTest.java b/application/src/test/java/org/thingsboard/server/transport/mqtt/mqttv3/rpc/AbstractMqttServerSideRpcIntegrationTest.java
index 75d529d4b1..bb1037f481 100644
--- a/application/src/test/java/org/thingsboard/server/transport/mqtt/mqttv3/rpc/AbstractMqttServerSideRpcIntegrationTest.java
+++ b/application/src/test/java/org/thingsboard/server/transport/mqtt/mqttv3/rpc/AbstractMqttServerSideRpcIntegrationTest.java
@@ -38,6 +38,7 @@ import org.thingsboard.server.common.data.device.profile.MqttDeviceProfileTransp
import org.thingsboard.server.common.data.device.profile.ProtoTransportPayloadConfiguration;
import org.thingsboard.server.common.data.device.profile.TransportPayloadTypeConfiguration;
import org.thingsboard.server.common.data.rpc.Rpc;
+import org.thingsboard.server.common.data.rpc.RpcStatus;
import org.thingsboard.server.common.msg.session.FeatureType;
import org.thingsboard.server.gen.transport.TransportApiProtos;
import org.thingsboard.server.transport.mqtt.AbstractMqttIntegrationTest;
@@ -331,6 +332,62 @@ public abstract class AbstractMqttServerSideRpcIntegrationTest extends AbstractM
assertEquals(MqttQoS.AT_MOST_ONCE.value(), callback.getMessageArrivedQoS());
}
+ protected void validateGatewayPersistentRpcDelivered(String deviceName) throws Exception {
+ GatewayRpcSession session = connectGatewayAndSubscribeForRpc(deviceName, false);
+
+ long expirationTime = System.currentTimeMillis() + TimeUnit.DAYS.toMillis(1);
+ String rpcRequest = "{\"method\":\"toggle_gpio\",\"params\":{\"pin\":1},\"persistent\":true,\"expirationTime\":" + expirationTime + "}";
+ String response = doPostAsync("/api/rpc/twoway/" + session.device().getId().getId().toString(), rpcRequest, String.class, status().isOk());
+ String rpcId = JacksonUtil.toJsonNode(response).get("rpcId").asText();
+
+ session.callback().getSubscribeLatch().await(DEFAULT_WAIT_TIMEOUT_SECONDS, TimeUnit.SECONDS);
+ // Confirm the downlink arrived at QoS1 — otherwise no PUBACK is produced and the test could
+ // pass for the wrong reason if a regression downgraded the gateway RPC to QoS0.
+ assertEquals(MqttQoS.AT_LEAST_ONCE.value(), session.callback().getMessageArrivedQoS());
+
+ awaitRpcStatus(rpcId, RpcStatus.DELIVERED, 200, 100);
+
+ session.client().disconnect();
+ }
+
+ /**
+ * Connects a gateway, provisions the sub-device via CONNECT, and subscribes to the gateway RPC topic
+ * at QoS1. Two-step subscribe (client.subscribeAndWait + awaitForDeviceActorToReceiveSubscription, 1)
+ * rather than the 5-arg subscribeAndWait, which waits for subscription count+1 — the gateway CONNECT
+ * already registered one RPC subscription, so it would hang.
+ */
+ protected GatewayRpcSession connectGatewayAndSubscribeForRpc(String deviceName, boolean manualAcks) throws Exception {
+ MqttTestClient client = new MqttTestClient();
+ if (manualAcks) {
+ client.enableManualAcks();
+ }
+ client.connectAndWait(gatewayAccessToken);
+
+ String connectPayload = "{\"device\": \"" + deviceName + "\", \"type\": \"" + TransportPayloadType.JSON.name() + "\"}";
+ client.publish(GATEWAY_CONNECT_TOPIC, connectPayload.getBytes());
+
+ Device device = doExecuteWithRetriesAndInterval(() -> getDeviceByName(deviceName), 20, 100);
+ assertNotNull(device);
+
+ MqttTestCallback callback = new MqttTestSubscribeOnTopicCallback(GATEWAY_RPC_TOPIC);
+ client.setCallback(callback);
+ client.subscribeAndWait(GATEWAY_RPC_TOPIC, MqttQoS.AT_LEAST_ONCE);
+ awaitForDeviceActorToReceiveSubscription(device.getId(), FeatureType.RPC, 1);
+ return new GatewayRpcSession(client, device, callback);
+ }
+
+ protected Rpc awaitRpcStatus(String rpcId, RpcStatus expectedStatus, int retries, int intervalMs) throws Exception {
+ Rpc rpc = doExecuteWithRetriesAndInterval(() -> {
+ Rpc current = doGet("/api/rpc/persistent/" + rpcId, Rpc.class);
+ return expectedStatus.equals(current.getStatus()) ? current : null;
+ }, retries, intervalMs);
+ assertNotNull(rpc);
+ assertEquals(expectedStatus, rpc.getStatus());
+ return rpc;
+ }
+
+ protected record GatewayRpcSession(MqttTestClient client, Device device, MqttTestCallback callback) {}
+
protected void validateProtoTwoWayRpcGatewayResponse(String deviceName, MqttTestClient client, byte[] connectPayloadBytes) throws Exception {
client.publish(GATEWAY_CONNECT_TOPIC, connectPayloadBytes);
@@ -353,7 +410,7 @@ public abstract class AbstractMqttServerSideRpcIntegrationTest extends AbstractM
assertEquals(MqttQoS.AT_MOST_ONCE.value(), callback.getMessageArrivedQoS());
}
- private Device getDeviceByName(String deviceName) throws Exception {
+ protected Device getDeviceByName(String deviceName) throws Exception {
return doGet("/api/tenant/devices?deviceName=" + deviceName, Device.class);
}
diff --git a/application/src/test/java/org/thingsboard/server/transport/mqtt/mqttv3/rpc/MqttGatewayServerSideRpcDeliveryTimeoutIntegrationTest.java b/application/src/test/java/org/thingsboard/server/transport/mqtt/mqttv3/rpc/MqttGatewayServerSideRpcDeliveryTimeoutIntegrationTest.java
new file mode 100644
index 0000000000..9c314339e5
--- /dev/null
+++ b/application/src/test/java/org/thingsboard/server/transport/mqtt/mqttv3/rpc/MqttGatewayServerSideRpcDeliveryTimeoutIntegrationTest.java
@@ -0,0 +1,75 @@
+/**
+ * Copyright © 2016-2026 The Thingsboard Authors
+ *
+ * Licensed 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.thingsboard.server.transport.mqtt.mqttv3.rpc;
+
+import io.netty.handler.codec.mqtt.MqttQoS;
+import lombok.extern.slf4j.Slf4j;
+import org.junit.Before;
+import org.junit.Test;
+import org.springframework.test.context.TestPropertySource;
+import org.thingsboard.common.util.JacksonUtil;
+import org.thingsboard.server.common.data.TransportPayloadType;
+import org.thingsboard.server.common.data.rpc.RpcStatus;
+import org.thingsboard.server.dao.service.DaoSqlTest;
+import org.thingsboard.server.transport.mqtt.MqttTestConfigProperties;
+
+import java.util.concurrent.TimeUnit;
+
+import static org.junit.Assert.assertEquals;
+import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
+
+@Slf4j
+@DaoSqlTest
+@TestPropertySource(properties = {
+ "actors.rpc.close_session_on_rpc_delivery_timeout=true",
+ "transport.mqtt.timeout=100",
+})
+public class MqttGatewayServerSideRpcDeliveryTimeoutIntegrationTest extends AbstractMqttServerSideRpcIntegrationTest {
+
+ @Before
+ public void beforeTest() throws Exception {
+ MqttTestConfigProperties configProperties = MqttTestConfigProperties.builder()
+ .deviceName("RPC timeout test device")
+ .gatewayName("RPC timeout test gateway")
+ .transportPayloadType(TransportPayloadType.JSON)
+ .build();
+ processBeforeTest(configProperties);
+ }
+
+ @Test
+ public void testGatewayPersistentRpcTimeoutWhenNoPuback() throws Exception {
+ // manualAcks = true → the gateway client never PUBACKs the QoS1 downlink.
+ GatewayRpcSession session = connectGatewayAndSubscribeForRpc("Gateway Device Persistent RPC Timeout Json", true);
+
+ long expirationTime = System.currentTimeMillis() + TimeUnit.DAYS.toMillis(1);
+ String rpcRequest = "{\"method\":\"toggle_gpio\",\"params\":{\"pin\":1},\"persistent\":true,\"retries\":0,\"expirationTime\":" + expirationTime + "}";
+ String response = doPostAsync("/api/rpc/twoway/" + session.device().getId().getId().toString(), rpcRequest, String.class, status().isOk());
+ String rpcId = JacksonUtil.toJsonNode(response).get("rpcId").asText();
+
+ session.callback().getSubscribeLatch().await(DEFAULT_WAIT_TIMEOUT_SECONDS, TimeUnit.SECONDS);
+ // Confirm the downlink arrived at QoS1 — a QoS0 downlink would produce no PUBACK path at all,
+ // so the timeout would fire for the wrong reason and mask a regression.
+ assertEquals(MqttQoS.AT_LEAST_ONCE.value(), session.callback().getMessageArrivedQoS());
+
+ // No PUBACK is ever sent. The gateway awaiting-ack scheduler emits TIMEOUT after
+ // transport.mqtt.timeout (100 ms); the RPC status reverts to QUEUED (re-queued via the
+ // close_session_on_rpc_delivery_timeout path).
+ awaitRpcStatus(rpcId, RpcStatus.QUEUED, 50, 100);
+
+ session.client().disconnect();
+ }
+
+}
diff --git a/application/src/test/java/org/thingsboard/server/transport/mqtt/mqttv3/rpc/MqttServerSideRpcJsonIntegrationTest.java b/application/src/test/java/org/thingsboard/server/transport/mqtt/mqttv3/rpc/MqttServerSideRpcJsonIntegrationTest.java
index 10ef549c87..842904baef 100644
--- a/application/src/test/java/org/thingsboard/server/transport/mqtt/mqttv3/rpc/MqttServerSideRpcJsonIntegrationTest.java
+++ b/application/src/test/java/org/thingsboard/server/transport/mqtt/mqttv3/rpc/MqttServerSideRpcJsonIntegrationTest.java
@@ -81,6 +81,11 @@ public class MqttServerSideRpcJsonIntegrationTest extends AbstractMqttServerSide
processJsonTwoWayRpcTestGateway("Gateway Device TwoWay RPC Json");
}
+ @Test
+ public void testGatewayServerMqttPersistentRpcDeliveredOnPuback() throws Exception {
+ validateGatewayPersistentRpcDelivered("Gateway Device Persistent RPC Json");
+ }
+
protected void processJsonOneWayRpcTestGateway(String deviceName) throws Exception {
MqttTestClient client = new MqttTestClient();
client.connectAndWait(gatewayAccessToken);
diff --git a/common/transport/mqtt/src/main/java/org/thingsboard/server/transport/mqtt/MqttTransportHandler.java b/common/transport/mqtt/src/main/java/org/thingsboard/server/transport/mqtt/MqttTransportHandler.java
index f753579292..7b321dc5f1 100644
--- a/common/transport/mqtt/src/main/java/org/thingsboard/server/transport/mqtt/MqttTransportHandler.java
+++ b/common/transport/mqtt/src/main/java/org/thingsboard/server/transport/mqtt/MqttTransportHandler.java
@@ -88,6 +88,7 @@ import org.thingsboard.server.transport.mqtt.session.GatewaySessionHandler;
import org.thingsboard.server.transport.mqtt.session.MqttTopicMatcher;
import org.thingsboard.server.transport.mqtt.session.SparkplugDeviceSessionContext;
import org.thingsboard.server.transport.mqtt.session.SparkplugNodeSessionHandler;
+import org.thingsboard.server.transport.mqtt.util.MqttRpcStatusUtil;
import org.thingsboard.server.transport.mqtt.util.ReturnCodeResolver;
import org.thingsboard.server.transport.mqtt.util.sparkplug.SparkplugMessageType;
import org.thingsboard.server.transport.mqtt.util.sparkplug.SparkplugRpcRequestHeader;
@@ -385,6 +386,8 @@ public class MqttTransportHandler extends ChannelInboundHandlerAdapter implement
TransportProtos.ToDeviceRpcRequestMsg rpcRequest = rpcAwaitingAck.remove(msgId);
if (rpcRequest != null) {
transportService.process(deviceSessionCtx.getSessionInfo(), rpcRequest, RpcStatus.DELIVERED, true, TransportServiceCallback.EMPTY);
+ } else if (gatewaySessionHandler != null) {
+ gatewaySessionHandler.onPubAck(msgId);
}
break;
default:
@@ -1521,9 +1524,11 @@ public class MqttTransportHandler extends ChannelInboundHandlerAdapter implement
}
public void sendToDeviceRpcRequest(MqttMessage payload, TransportProtos.ToDeviceRpcRequestMsg rpcRequest, TransportProtos.SessionInfoProto sessionInfo) {
- int msgId = ((MqttPublishMessage) payload).variableHeader().packetId();
+ MqttPublishMessage publishMsg = (MqttPublishMessage) payload;
+ int msgId = publishMsg.variableHeader().packetId();
int requestId = rpcRequest.getRequestId();
- if (isAckExpected(payload)) {
+ boolean requireDeliveryTracking = MqttRpcStatusUtil.requireDeliveryTracking(rpcRequest);
+ if (requireDeliveryTracking && isAckExpected(publishMsg)) {
rpcAwaitingAck.put(msgId, rpcRequest);
context.getScheduler().schedule(() -> {
TransportProtos.ToDeviceRpcRequestMsg msg = rpcAwaitingAck.remove(msgId);
@@ -1533,7 +1538,7 @@ public class MqttTransportHandler extends ChannelInboundHandlerAdapter implement
}
}, Math.max(0, Math.min(deviceSessionCtx.getContext().getTimeout(), rpcRequest.getExpirationTime() - System.currentTimeMillis())), TimeUnit.MILLISECONDS);
}
- var cf = publish(payload, deviceSessionCtx);
+ var cf = publish(publishMsg, deviceSessionCtx);
cf.addListener(result -> {
Throwable throwable = result.cause();
if (throwable != null) {
@@ -1542,9 +1547,11 @@ public class MqttTransportHandler extends ChannelInboundHandlerAdapter implement
ThingsboardErrorCode.INVALID_ARGUMENTS, " Failed send To Device Rpc Request: " + rpcRequest.getMethodName());
return;
}
- if (!isAckExpected(payload)) {
- log.trace("[{}][{}][{}] Going to send to device actor RPC request DELIVERED status update ...", deviceSessionCtx.getDeviceId(), sessionId, requestId);
- transportService.process(sessionInfo, rpcRequest, RpcStatus.DELIVERED, TransportServiceCallback.EMPTY);
+ if (!isAckExpected(publishMsg)) {
+ if (requireDeliveryTracking) {
+ log.trace("[{}][{}][{}] Going to send to device actor RPC request DELIVERED status update ...", deviceSessionCtx.getDeviceId(), sessionId, requestId);
+ transportService.process(sessionInfo, rpcRequest, RpcStatus.DELIVERED, TransportServiceCallback.EMPTY);
+ }
} else if (rpcRequest.getPersisted()) {
log.trace("[{}][{}][{}] Going to send to device actor RPC request SENT status update ...", deviceSessionCtx.getDeviceId(), sessionId, requestId);
transportService.process(sessionInfo, rpcRequest, RpcStatus.SENT, TransportServiceCallback.EMPTY);
diff --git a/common/transport/mqtt/src/main/java/org/thingsboard/server/transport/mqtt/session/AbstractGatewayDeviceSessionContext.java b/common/transport/mqtt/src/main/java/org/thingsboard/server/transport/mqtt/session/AbstractGatewayDeviceSessionContext.java
index a1162f1990..5b0f8c15be 100644
--- a/common/transport/mqtt/src/main/java/org/thingsboard/server/transport/mqtt/session/AbstractGatewayDeviceSessionContext.java
+++ b/common/transport/mqtt/src/main/java/org/thingsboard/server/transport/mqtt/session/AbstractGatewayDeviceSessionContext.java
@@ -17,6 +17,7 @@ package org.thingsboard.server.transport.mqtt.session;
import io.netty.channel.ChannelFuture;
import io.netty.handler.codec.mqtt.MqttMessage;
+import io.netty.handler.codec.mqtt.MqttPublishMessage;
import lombok.Getter;
import lombok.ToString;
import lombok.extern.slf4j.Slf4j;
@@ -29,6 +30,7 @@ import org.thingsboard.server.common.transport.TransportServiceCallback;
import org.thingsboard.server.common.transport.auth.TransportDeviceInfo;
import org.thingsboard.server.gen.transport.TransportProtos;
import org.thingsboard.server.gen.transport.TransportProtos.SessionInfoProto;
+import org.thingsboard.server.transport.mqtt.util.MqttRpcStatusUtil;
import java.util.UUID;
import java.util.concurrent.ConcurrentMap;
@@ -106,19 +108,23 @@ public abstract class AbstractGatewayDeviceSessionContext {
- ChannelFuture channelFuture = parent.writeAndFlush(payload);
- if (request.getPersisted()) {
- channelFuture.addListener(result -> {
- if (result.cause() == null) {
- if (!isAckExpected(payload)) {
+ MqttPublishMessage publishMsg = (MqttPublishMessage) payload;
+ boolean requireDeliveryTracking = MqttRpcStatusUtil.requireDeliveryTracking(request);
+ if (requireDeliveryTracking && isAckExpected(publishMsg)) {
+ parent.registerRpcAwaitingAck(publishMsg.variableHeader().packetId(), getSessionInfo(), request);
+ }
+ ChannelFuture channelFuture = parent.writeAndFlush(publishMsg);
+ channelFuture.addListener(result -> {
+ if (result.cause() == null) {
+ if (!isAckExpected(publishMsg)) {
+ if (requireDeliveryTracking) {
transportService.process(getSessionInfo(), request, RpcStatus.DELIVERED, TransportServiceCallback.EMPTY);
- } else if (request.getPersisted()) {
- transportService.process(getSessionInfo(), request, RpcStatus.SENT, TransportServiceCallback.EMPTY);
-
}
+ } else if (request.getPersisted()) {
+ transportService.process(getSessionInfo(), request, RpcStatus.SENT, TransportServiceCallback.EMPTY);
}
- });
- }
+ }
+ });
}
);
} catch (Exception e) {
diff --git a/common/transport/mqtt/src/main/java/org/thingsboard/server/transport/mqtt/session/AbstractGatewaySessionHandler.java b/common/transport/mqtt/src/main/java/org/thingsboard/server/transport/mqtt/session/AbstractGatewaySessionHandler.java
index 8ea6480142..0a15d5d255 100644
--- a/common/transport/mqtt/src/main/java/org/thingsboard/server/transport/mqtt/session/AbstractGatewaySessionHandler.java
+++ b/common/transport/mqtt/src/main/java/org/thingsboard/server/transport/mqtt/session/AbstractGatewaySessionHandler.java
@@ -50,6 +50,7 @@ import org.thingsboard.server.common.data.Device;
import org.thingsboard.server.common.data.DeviceProfile;
import org.thingsboard.server.common.data.StringUtils;
import org.thingsboard.server.common.data.id.DeviceId;
+import org.thingsboard.server.common.data.rpc.RpcStatus;
import org.thingsboard.server.common.data.util.TbPair;
import org.thingsboard.server.common.msg.gateway.metrics.GatewayMetadata;
import org.thingsboard.server.common.msg.tools.TbRateLimitsException;
@@ -80,6 +81,7 @@ import java.util.Set;
import java.util.UUID;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ConcurrentMap;
+import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.concurrent.locks.Lock;
@@ -123,6 +125,10 @@ public abstract class AbstractGatewaySessionHandler rpcAwaitingAck = new ConcurrentHashMap<>();
+
+ private record RpcAwaitingAck(SessionInfoProto deviceSessionInfo, TransportProtos.ToDeviceRpcRequestMsg rpc) {}
+
@Getter
@Setter
private boolean overwriteDevicesActivity = false;
@@ -201,6 +207,10 @@ public abstract class AbstractGatewaySessionHandler {
Futures.addCallback(future, new FutureCallback() {
@@ -252,6 +262,28 @@ public abstract class AbstractGatewaySessionHandler {
+ RpcAwaitingAck pending = rpcAwaitingAck.remove(msgId);
+ if (pending != null) {
+ log.trace("[{}] Gateway RPC [{}] PUBACK timeout, sending TIMEOUT status", sessionId, rpc.getRequestId());
+ transportService.process(pending.deviceSessionInfo(), pending.rpc(), RpcStatus.TIMEOUT, TransportServiceCallback.EMPTY);
+ }
+ }, delay, TimeUnit.MILLISECONDS);
+ }
+
+ public void onPubAck(int msgId) {
+ RpcAwaitingAck pending = rpcAwaitingAck.remove(msgId);
+ if (pending == null) {
+ return;
+ }
+ log.trace("[{}] Gateway RPC [{}] PUBACK received, sending DELIVERED status", sessionId, pending.rpc().getRequestId());
+ transportService.process(pending.deviceSessionInfo(), pending.rpc(), RpcStatus.DELIVERED, true, TransportServiceCallback.EMPTY);
+ }
+
protected boolean isJsonPayloadType() {
return deviceSessionCtx.isJsonPayloadType();
}
diff --git a/common/transport/mqtt/src/main/java/org/thingsboard/server/transport/mqtt/util/MqttRpcStatusUtil.java b/common/transport/mqtt/src/main/java/org/thingsboard/server/transport/mqtt/util/MqttRpcStatusUtil.java
new file mode 100644
index 0000000000..7aeb468d86
--- /dev/null
+++ b/common/transport/mqtt/src/main/java/org/thingsboard/server/transport/mqtt/util/MqttRpcStatusUtil.java
@@ -0,0 +1,35 @@
+/**
+ * Copyright © 2016-2026 The Thingsboard Authors
+ *
+ * Licensed 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.thingsboard.server.transport.mqtt.util;
+
+import org.thingsboard.server.gen.transport.TransportProtos.ToDeviceRpcRequestMsg;
+
+public final class MqttRpcStatusUtil {
+
+ private MqttRpcStatusUtil() {
+ }
+
+ /**
+ * Whether the transport should track and emit a delivery status (DELIVERED / TIMEOUT) for the given RPC.
+ * Non-persistent one-way RPCs self-complete on send in the device actor and are never added to its pending
+ * map, so a delivery status for them lands on nothing and only produces a benign
+ * "RPC has already been removed from pending map" warning. Every other RPC is tracked.
+ */
+ public static boolean requireDeliveryTracking(ToDeviceRpcRequestMsg rpc) {
+ return !(rpc.getOneway() && !rpc.getPersisted());
+ }
+
+}