Merge pull request #9027 from ShvaykaD/feature/rpc-sequential-strategies
Rpc sequential strategies
This commit is contained in:
		
						commit
						ba48ed192f
					
				@ -528,9 +528,13 @@ public class ActorSystemContext {
 | 
			
		||||
    @Getter
 | 
			
		||||
    private String debugPerTenantLimitsConfiguration;
 | 
			
		||||
 | 
			
		||||
    @Value("${actors.rpc.sequential:false}")
 | 
			
		||||
    @Value("${actors.rpc.submit_strategy:BURST}")
 | 
			
		||||
    @Getter
 | 
			
		||||
    private boolean rpcSequential;
 | 
			
		||||
    private String rpcSubmitStrategy;
 | 
			
		||||
 | 
			
		||||
    @Value("${actors.rpc.response_timeout_ms:30000}")
 | 
			
		||||
    @Getter
 | 
			
		||||
    private long rpcResponseTimeout;
 | 
			
		||||
 | 
			
		||||
    @Value("${actors.rpc.max_retries:5}")
 | 
			
		||||
    @Getter
 | 
			
		||||
 | 
			
		||||
@ -89,6 +89,7 @@ import org.thingsboard.server.gen.transport.TransportProtos.TransportToDeviceAct
 | 
			
		||||
import org.thingsboard.server.gen.transport.TransportProtos.TsKvProto;
 | 
			
		||||
import org.thingsboard.server.service.rpc.FromDeviceRpcResponseActorMsg;
 | 
			
		||||
import org.thingsboard.server.service.rpc.RemoveRpcActorMsg;
 | 
			
		||||
import org.thingsboard.server.service.rpc.RpcSubmitStrategy;
 | 
			
		||||
import org.thingsboard.server.service.rpc.ToDeviceRpcRequestActorMsg;
 | 
			
		||||
import org.thingsboard.server.service.transport.msg.TransportToDeviceActorMsgWrapper;
 | 
			
		||||
 | 
			
		||||
@ -106,6 +107,9 @@ import java.util.Optional;
 | 
			
		||||
import java.util.Set;
 | 
			
		||||
import java.util.UUID;
 | 
			
		||||
import java.util.concurrent.ExecutionException;
 | 
			
		||||
import java.util.concurrent.ScheduledExecutorService;
 | 
			
		||||
import java.util.concurrent.ScheduledFuture;
 | 
			
		||||
import java.util.concurrent.TimeUnit;
 | 
			
		||||
import java.util.function.Consumer;
 | 
			
		||||
import java.util.stream.Collectors;
 | 
			
		||||
 | 
			
		||||
@ -124,22 +128,27 @@ public class DeviceActorMessageProcessor extends AbstractContextAwareMsgProcesso
 | 
			
		||||
    private final Map<UUID, SessionInfo> rpcSubscriptions;
 | 
			
		||||
    private final Map<Integer, ToDeviceRpcRequestMetadata> toDeviceRpcPendingMap;
 | 
			
		||||
    private final boolean rpcSequential;
 | 
			
		||||
    private final RpcSubmitStrategy rpcSubmitStrategy;
 | 
			
		||||
    private final ScheduledExecutorService scheduler;
 | 
			
		||||
 | 
			
		||||
    private int rpcSeq = 0;
 | 
			
		||||
    private String deviceName;
 | 
			
		||||
    private String deviceType;
 | 
			
		||||
    private TbMsgMetaData defaultMetaData;
 | 
			
		||||
    private EdgeId edgeId;
 | 
			
		||||
    private ScheduledFuture<?> awaitRpcResponseFuture;
 | 
			
		||||
 | 
			
		||||
    DeviceActorMessageProcessor(ActorSystemContext systemContext, TenantId tenantId, DeviceId deviceId) {
 | 
			
		||||
        super(systemContext);
 | 
			
		||||
        this.tenantId = tenantId;
 | 
			
		||||
        this.deviceId = deviceId;
 | 
			
		||||
        this.rpcSequential = systemContext.isRpcSequential();
 | 
			
		||||
        this.rpcSubmitStrategy = RpcSubmitStrategy.parse(systemContext.getRpcSubmitStrategy());
 | 
			
		||||
        this.rpcSequential = !rpcSubmitStrategy.equals(RpcSubmitStrategy.BURST);
 | 
			
		||||
        this.attributeSubscriptions = new HashMap<>();
 | 
			
		||||
        this.rpcSubscriptions = new HashMap<>();
 | 
			
		||||
        this.toDeviceRpcPendingMap = new LinkedHashMap<>();
 | 
			
		||||
        this.sessions = new LinkedHashMapRemoveEldest<>(systemContext.getMaxConcurrentSessionsPerDevice(), this::notifyTransportAboutClosedSessionMaxSessionsLimit);
 | 
			
		||||
        this.scheduler = systemContext.getScheduler();
 | 
			
		||||
        if (initAttributes()) {
 | 
			
		||||
            restoreSessions();
 | 
			
		||||
        }
 | 
			
		||||
@ -183,7 +192,7 @@ public class DeviceActorMessageProcessor extends AbstractContextAwareMsgProcesso
 | 
			
		||||
        ToDeviceRpcRequest request = msg.getMsg();
 | 
			
		||||
        UUID rpcId = request.getId();
 | 
			
		||||
        log.debug("[{}][{}] Received RPC request to process ...", deviceId, rpcId);
 | 
			
		||||
        ToDeviceRpcRequestMsg rpcRequest = creteToDeviceRpcRequestMsg(request);
 | 
			
		||||
        ToDeviceRpcRequestMsg rpcRequest = createToDeviceRpcRequestMsg(request);
 | 
			
		||||
 | 
			
		||||
        long timeout = request.getExpirationTime() - System.currentTimeMillis();
 | 
			
		||||
        boolean persisted = request.isPersisted();
 | 
			
		||||
@ -225,24 +234,28 @@ public class DeviceActorMessageProcessor extends AbstractContextAwareMsgProcesso
 | 
			
		||||
        if (persisted) {
 | 
			
		||||
            ObjectNode response = JacksonUtil.newObjectNode();
 | 
			
		||||
            response.put("rpcId", rpcId.toString());
 | 
			
		||||
            systemContext.getTbCoreDeviceRpcService().processRpcResponseFromDeviceActor(new FromDeviceRpcResponse(msg.getMsg().getId(), JacksonUtil.toString(response), null));
 | 
			
		||||
            systemContext.getTbCoreDeviceRpcService().processRpcResponseFromDeviceActor(new FromDeviceRpcResponse(rpcId, JacksonUtil.toString(response), null));
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        if (!persisted && request.isOneway() && sent) {
 | 
			
		||||
            log.debug("[{}] RPC command response sent [{}][{}]!", deviceId, rpcId, requestId);
 | 
			
		||||
            systemContext.getTbCoreDeviceRpcService().processRpcResponseFromDeviceActor(new FromDeviceRpcResponse(msg.getMsg().getId(), null, null));
 | 
			
		||||
            systemContext.getTbCoreDeviceRpcService().processRpcResponseFromDeviceActor(new FromDeviceRpcResponse(rpcId, null, null));
 | 
			
		||||
        } else {
 | 
			
		||||
            registerPendingRpcRequest(context, msg, sent, rpcRequest, timeout);
 | 
			
		||||
        }
 | 
			
		||||
        if (sent) {
 | 
			
		||||
            log.debug("[{}][{}][{}] RPC request is sent!", deviceId, rpcId, requestId);
 | 
			
		||||
        } else {
 | 
			
		||||
            log.debug("[{}][{}][{}] RPC request is NOT sent!", deviceId, rpcId, requestId);
 | 
			
		||||
        }
 | 
			
		||||
        String rpcSent = sent ? "sent!" : "NOT sent!";
 | 
			
		||||
        log.debug("[{}][{}][{}] RPC request is {}", deviceId, rpcId, requestId, rpcSent);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    private boolean isSendNewRpcAvailable() {
 | 
			
		||||
        return !rpcSequential || toDeviceRpcPendingMap.values().stream().filter(md -> !md.isDelivered()).findAny().isEmpty();
 | 
			
		||||
        switch (rpcSubmitStrategy) {
 | 
			
		||||
            case SEQUENTIAL_ON_ACK_FROM_DEVICE:
 | 
			
		||||
                return toDeviceRpcPendingMap.values().stream().filter(md -> !md.isDelivered()).findAny().isEmpty();
 | 
			
		||||
            case SEQUENTIAL_ON_RESPONSE_FROM_DEVICE:
 | 
			
		||||
                return toDeviceRpcPendingMap.values().stream().filter(ToDeviceRpcRequestMetadata::isDelivered).findAny().isEmpty();
 | 
			
		||||
            default:
 | 
			
		||||
                return true;
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    private void createRpc(ToDeviceRpcRequest request, RpcStatus status) {
 | 
			
		||||
@ -257,7 +270,7 @@ public class DeviceActorMessageProcessor extends AbstractContextAwareMsgProcesso
 | 
			
		||||
        systemContext.getTbRpcService().save(tenantId, rpc);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    private ToDeviceRpcRequestMsg creteToDeviceRpcRequestMsg(ToDeviceRpcRequest request) {
 | 
			
		||||
    private ToDeviceRpcRequestMsg createToDeviceRpcRequestMsg(ToDeviceRpcRequest request) {
 | 
			
		||||
        ToDeviceRpcRequestBody body = request.getBody();
 | 
			
		||||
        return ToDeviceRpcRequestMsg.newBuilder()
 | 
			
		||||
                .setRequestId(rpcSeq++)
 | 
			
		||||
@ -283,28 +296,31 @@ public class DeviceActorMessageProcessor extends AbstractContextAwareMsgProcesso
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    void processRemoveRpc(RemoveRpcActorMsg msg) {
 | 
			
		||||
        UUID requestId = msg.getRequestId();
 | 
			
		||||
        log.debug("[{}][{}] Received remove RPC request ...", deviceId, requestId);
 | 
			
		||||
        UUID rpcId = msg.getRequestId();
 | 
			
		||||
        log.debug("[{}][{}] Received remove RPC request ...", deviceId, rpcId);
 | 
			
		||||
        Map.Entry<Integer, ToDeviceRpcRequestMetadata> entry = null;
 | 
			
		||||
        for (Map.Entry<Integer, ToDeviceRpcRequestMetadata> e : toDeviceRpcPendingMap.entrySet()) {
 | 
			
		||||
            if (e.getValue().getMsg().getMsg().getId().equals(requestId)) {
 | 
			
		||||
            if (e.getValue().getMsg().getMsg().getId().equals(rpcId)) {
 | 
			
		||||
                entry = e;
 | 
			
		||||
                break;
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        if (entry != null) {
 | 
			
		||||
            Integer key = entry.getKey();
 | 
			
		||||
            Integer requestId = entry.getKey();
 | 
			
		||||
            if (entry.getValue().isDelivered()) {
 | 
			
		||||
                toDeviceRpcPendingMap.remove(key);
 | 
			
		||||
                toDeviceRpcPendingMap.remove(requestId);
 | 
			
		||||
                if (rpcSubmitStrategy.equals(RpcSubmitStrategy.SEQUENTIAL_ON_RESPONSE_FROM_DEVICE)) {
 | 
			
		||||
                    clearAwaitRpcResponseScheduler();
 | 
			
		||||
                    sendNextPendingRequest(rpcId, requestId, "Removed pending RPC!");
 | 
			
		||||
                }
 | 
			
		||||
            } else {
 | 
			
		||||
                Optional<Map.Entry<Integer, ToDeviceRpcRequestMetadata>> firstRpc = getFirstRpc();
 | 
			
		||||
                if (firstRpc.isPresent() && key.equals(firstRpc.get().getKey())) {
 | 
			
		||||
                    toDeviceRpcPendingMap.remove(key);
 | 
			
		||||
                    log.debug("[{}][{}][{}] Removed pending RPC! Going to send next pending request ...", deviceId, requestId, key);
 | 
			
		||||
                    sendNextPendingRequest();
 | 
			
		||||
                if (firstRpc.isPresent() && requestId.equals(firstRpc.get().getKey())) {
 | 
			
		||||
                    toDeviceRpcPendingMap.remove(requestId);
 | 
			
		||||
                    sendNextPendingRequest(rpcId, requestId, "Removed pending RPC!");
 | 
			
		||||
                } else {
 | 
			
		||||
                    toDeviceRpcPendingMap.remove(key);
 | 
			
		||||
                    toDeviceRpcPendingMap.remove(requestId);
 | 
			
		||||
                }
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
@ -321,9 +337,9 @@ public class DeviceActorMessageProcessor extends AbstractContextAwareMsgProcesso
 | 
			
		||||
 | 
			
		||||
    void processServerSideRpcTimeout(DeviceActorServerSideRpcTimeoutMsg msg) {
 | 
			
		||||
        Integer requestId = msg.getId();
 | 
			
		||||
        ToDeviceRpcRequestMetadata requestMd = toDeviceRpcPendingMap.remove(requestId);
 | 
			
		||||
        var requestMd = toDeviceRpcPendingMap.remove(requestId);
 | 
			
		||||
        if (requestMd != null) {
 | 
			
		||||
            ToDeviceRpcRequest toDeviceRpcRequest = requestMd.getMsg().getMsg();
 | 
			
		||||
            var toDeviceRpcRequest = requestMd.getMsg().getMsg();
 | 
			
		||||
            UUID rpcId = toDeviceRpcRequest.getId();
 | 
			
		||||
            log.debug("[{}][{}][{}] RPC request timeout detected!", deviceId, rpcId, requestId);
 | 
			
		||||
            if (toDeviceRpcRequest.isPersisted()) {
 | 
			
		||||
@ -332,8 +348,12 @@ public class DeviceActorMessageProcessor extends AbstractContextAwareMsgProcesso
 | 
			
		||||
            systemContext.getTbCoreDeviceRpcService().processRpcResponseFromDeviceActor(new FromDeviceRpcResponse(rpcId,
 | 
			
		||||
                    null, requestMd.isSent() ? RpcError.TIMEOUT : RpcError.NO_ACTIVE_CONNECTION));
 | 
			
		||||
            if (!requestMd.isDelivered()) {
 | 
			
		||||
                log.debug("[{}][{}][{}] Pending RPC timeout detected! Going to send next pending request ...", deviceId, rpcId, requestId);
 | 
			
		||||
                sendNextPendingRequest();
 | 
			
		||||
                sendNextPendingRequest(rpcId, requestId, "Pending RPC timeout detected!");
 | 
			
		||||
                return;
 | 
			
		||||
            }
 | 
			
		||||
            if (rpcSubmitStrategy.equals(RpcSubmitStrategy.SEQUENTIAL_ON_RESPONSE_FROM_DEVICE)) {
 | 
			
		||||
                clearAwaitRpcResponseScheduler();
 | 
			
		||||
                sendNextPendingRequest(rpcId, requestId, "Pending RPC timeout detected!");
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
@ -363,10 +383,25 @@ public class DeviceActorMessageProcessor extends AbstractContextAwareMsgProcesso
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    private Optional<Map.Entry<Integer, ToDeviceRpcRequestMetadata>> getFirstRpc() {
 | 
			
		||||
        if (rpcSubmitStrategy.equals(RpcSubmitStrategy.SEQUENTIAL_ON_RESPONSE_FROM_DEVICE)) {
 | 
			
		||||
            return toDeviceRpcPendingMap.entrySet().stream()
 | 
			
		||||
                    .findFirst().filter(entry -> {
 | 
			
		||||
                        var md = entry.getValue();
 | 
			
		||||
                        if (md.isDelivered()) {
 | 
			
		||||
                            if (awaitRpcResponseFuture == null || awaitRpcResponseFuture.isCancelled()) {
 | 
			
		||||
                                var toDeviceRpcRequest = md.getMsg().getMsg();
 | 
			
		||||
                                awaitRpcResponseFuture = scheduleAwaitRpcResponseFuture(toDeviceRpcRequest.getId(), entry.getKey());
 | 
			
		||||
                            }
 | 
			
		||||
                            return false;
 | 
			
		||||
                        }
 | 
			
		||||
                        return true;
 | 
			
		||||
                    });
 | 
			
		||||
        }
 | 
			
		||||
        return toDeviceRpcPendingMap.entrySet().stream().filter(e -> !e.getValue().isDelivered()).findFirst();
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    private void sendNextPendingRequest() {
 | 
			
		||||
    private void sendNextPendingRequest(UUID rpcId, int requestId, String logMessage) {
 | 
			
		||||
        log.debug("[{}][{}][{}] {} Going to send next pending request ...", deviceId, rpcId, requestId, logMessage);
 | 
			
		||||
        if (rpcSequential) {
 | 
			
		||||
            rpcSubscriptions.forEach((id, s) -> sendPendingRequests(id, s.getNodeId()));
 | 
			
		||||
        }
 | 
			
		||||
@ -591,12 +626,13 @@ public class DeviceActorMessageProcessor extends AbstractContextAwareMsgProcesso
 | 
			
		||||
        boolean success = requestMd != null;
 | 
			
		||||
        if (success) {
 | 
			
		||||
            ToDeviceRpcRequest toDeviceRequestMsg = requestMd.getMsg().getMsg();
 | 
			
		||||
            UUID rpcId = toDeviceRequestMsg.getId();
 | 
			
		||||
            boolean delivered = requestMd.isDelivered();
 | 
			
		||||
            boolean hasError = StringUtils.isNotEmpty(responseMsg.getError());
 | 
			
		||||
            try {
 | 
			
		||||
                String payload = hasError ? responseMsg.getError() : responseMsg.getPayload();
 | 
			
		||||
                systemContext.getTbCoreDeviceRpcService().processRpcResponseFromDeviceActor(
 | 
			
		||||
                        new FromDeviceRpcResponse(toDeviceRequestMsg.getId(), payload, null));
 | 
			
		||||
                        new FromDeviceRpcResponse(rpcId, payload, null));
 | 
			
		||||
                if (toDeviceRequestMsg.isPersisted()) {
 | 
			
		||||
                    RpcStatus status = hasError ? RpcStatus.FAILED : RpcStatus.SUCCESSFUL;
 | 
			
		||||
                    JsonNode response;
 | 
			
		||||
@ -605,13 +641,17 @@ public class DeviceActorMessageProcessor extends AbstractContextAwareMsgProcesso
 | 
			
		||||
                    } catch (IllegalArgumentException e) {
 | 
			
		||||
                        response = JacksonUtil.newObjectNode().put("error", payload);
 | 
			
		||||
                    }
 | 
			
		||||
                    systemContext.getTbRpcService().save(tenantId, new RpcId(toDeviceRequestMsg.getId()), status, response);
 | 
			
		||||
                    systemContext.getTbRpcService().save(tenantId, new RpcId(rpcId), status, response);
 | 
			
		||||
                }
 | 
			
		||||
            } finally {
 | 
			
		||||
                if (!delivered) {
 | 
			
		||||
                    String errorResponse = hasError ? "error" : "";
 | 
			
		||||
                    log.debug("[{}][{}][{}] Received {} response for undelivered RPC! Going to send next pending request ...", deviceId, sessionId, requestId, errorResponse);
 | 
			
		||||
                    sendNextPendingRequest();
 | 
			
		||||
                if (rpcSubmitStrategy.equals(RpcSubmitStrategy.SEQUENTIAL_ON_RESPONSE_FROM_DEVICE)) {
 | 
			
		||||
                    clearAwaitRpcResponseScheduler();
 | 
			
		||||
                    String errorResponse = hasError ? "error response" : "response";
 | 
			
		||||
                    String rpcState = delivered ? "" : "undelivered ";
 | 
			
		||||
                    sendNextPendingRequest(rpcId, requestId, String.format("Received %s for %sRPC!", errorResponse, rpcState));
 | 
			
		||||
                } else if (!delivered) {
 | 
			
		||||
                    String errorResponse = hasError ? "error response" : "response";
 | 
			
		||||
                    sendNextPendingRequest(rpcId, requestId, String.format("Received %s for undelivered RPC!", errorResponse));
 | 
			
		||||
                }
 | 
			
		||||
            }
 | 
			
		||||
        } else {
 | 
			
		||||
@ -626,36 +666,47 @@ public class DeviceActorMessageProcessor extends AbstractContextAwareMsgProcesso
 | 
			
		||||
        int requestId = responseMsg.getRequestId();
 | 
			
		||||
        log.debug("[{}][{}][{}][{}] Processing RPC command response status: [{}]", deviceId, sessionId, rpcId, requestId, status);
 | 
			
		||||
        ToDeviceRpcRequestMetadata md = toDeviceRpcPendingMap.get(requestId);
 | 
			
		||||
 | 
			
		||||
        if (md != null) {
 | 
			
		||||
            var toDeviceRpcRequest = md.getMsg().getMsg();
 | 
			
		||||
            boolean persisted = toDeviceRpcRequest.isPersisted();
 | 
			
		||||
            boolean oneWayRpc = toDeviceRpcRequest.isOneway();
 | 
			
		||||
            JsonNode response = null;
 | 
			
		||||
            if (status.equals(RpcStatus.DELIVERED)) {
 | 
			
		||||
                if (md.getMsg().getMsg().isOneway()) {
 | 
			
		||||
                if (oneWayRpc) {
 | 
			
		||||
                    toDeviceRpcPendingMap.remove(requestId);
 | 
			
		||||
                    if (rpcSequential) {
 | 
			
		||||
                        systemContext.getTbCoreDeviceRpcService().processRpcResponseFromDeviceActor(new FromDeviceRpcResponse(rpcId, null, null));
 | 
			
		||||
                        var fromDeviceRpcResponse = new FromDeviceRpcResponse(rpcId, null, null);
 | 
			
		||||
                        systemContext.getTbCoreDeviceRpcService().processRpcResponseFromDeviceActor(fromDeviceRpcResponse);
 | 
			
		||||
                    }
 | 
			
		||||
                } else {
 | 
			
		||||
                    md.setDelivered(true);
 | 
			
		||||
                    if (rpcSubmitStrategy.equals(RpcSubmitStrategy.SEQUENTIAL_ON_RESPONSE_FROM_DEVICE)) {
 | 
			
		||||
                        awaitRpcResponseFuture = scheduleAwaitRpcResponseFuture(rpcId, requestId);
 | 
			
		||||
                    }
 | 
			
		||||
                }
 | 
			
		||||
            } else if (status.equals(RpcStatus.TIMEOUT)) {
 | 
			
		||||
                Integer maxRpcRetries = md.getMsg().getMsg().getRetries();
 | 
			
		||||
                maxRpcRetries = maxRpcRetries == null ? systemContext.getMaxRpcRetries() : Math.min(maxRpcRetries, systemContext.getMaxRpcRetries());
 | 
			
		||||
                Integer maxRpcRetries = toDeviceRpcRequest.getRetries();
 | 
			
		||||
                maxRpcRetries = maxRpcRetries == null ?
 | 
			
		||||
                        systemContext.getMaxRpcRetries() : Math.min(maxRpcRetries, systemContext.getMaxRpcRetries());
 | 
			
		||||
                if (maxRpcRetries <= md.getRetries()) {
 | 
			
		||||
                    toDeviceRpcPendingMap.remove(requestId);
 | 
			
		||||
                    status = RpcStatus.FAILED;
 | 
			
		||||
                    response = JacksonUtil.newObjectNode().put("error", "There was a Timeout and all retry attempts have been exhausted. Retry attempts set: " + maxRpcRetries);
 | 
			
		||||
                    response = JacksonUtil.newObjectNode().put("error", "There was a Timeout and all retry " +
 | 
			
		||||
                            "attempts have been exhausted. Retry attempts set: " + maxRpcRetries);
 | 
			
		||||
                } else {
 | 
			
		||||
                    md.setRetries(md.getRetries() + 1);
 | 
			
		||||
                }
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            if (md.getMsg().getMsg().isPersisted()) {
 | 
			
		||||
            if (persisted) {
 | 
			
		||||
                systemContext.getTbRpcService().save(tenantId, new RpcId(rpcId), status, response);
 | 
			
		||||
            }
 | 
			
		||||
            if (status != RpcStatus.SENT) {
 | 
			
		||||
                log.debug("[{}][{}][{}][{}] RPC was {}! Going to send next pending request ...", deviceId, sessionId, rpcId, requestId, status.name().toLowerCase());
 | 
			
		||||
                sendNextPendingRequest();
 | 
			
		||||
            if (rpcSubmitStrategy.equals(RpcSubmitStrategy.SEQUENTIAL_ON_RESPONSE_FROM_DEVICE)
 | 
			
		||||
                    && status.equals(RpcStatus.DELIVERED) && !oneWayRpc) {
 | 
			
		||||
                return;
 | 
			
		||||
            }
 | 
			
		||||
            if (!status.equals(RpcStatus.SENT)) {
 | 
			
		||||
                sendNextPendingRequest(rpcId, requestId, String.format("RPC was %s!", status.name().toLowerCase()));
 | 
			
		||||
            }
 | 
			
		||||
        } else {
 | 
			
		||||
            log.warn("[{}][{}][{}][{}] RPC has already been removed from pending map.", deviceId, sessionId, rpcId, requestId);
 | 
			
		||||
@ -688,6 +739,7 @@ public class DeviceActorMessageProcessor extends AbstractContextAwareMsgProcesso
 | 
			
		||||
        if (subscribeCmd.getUnsubscribe()) {
 | 
			
		||||
            log.debug("[{}] Canceling RPC subscription for session: [{}]", deviceId, sessionId);
 | 
			
		||||
            rpcSubscriptions.remove(sessionId);
 | 
			
		||||
            clearAwaitRpcResponseScheduler();
 | 
			
		||||
        } else {
 | 
			
		||||
            SessionInfoMetaData sessionMD = sessions.get(sessionId);
 | 
			
		||||
            if (sessionMD == null) {
 | 
			
		||||
@ -722,6 +774,7 @@ public class DeviceActorMessageProcessor extends AbstractContextAwareMsgProcesso
 | 
			
		||||
            sessions.remove(sessionId);
 | 
			
		||||
            attributeSubscriptions.remove(sessionId);
 | 
			
		||||
            rpcSubscriptions.remove(sessionId);
 | 
			
		||||
            clearAwaitRpcResponseScheduler();
 | 
			
		||||
            if (sessions.isEmpty()) {
 | 
			
		||||
                reportSessionClose();
 | 
			
		||||
            }
 | 
			
		||||
@ -729,6 +782,27 @@ public class DeviceActorMessageProcessor extends AbstractContextAwareMsgProcesso
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    private ScheduledFuture<?> scheduleAwaitRpcResponseFuture(UUID rpcId, int requestId) {
 | 
			
		||||
        return scheduler.schedule(() -> {
 | 
			
		||||
            var md = toDeviceRpcPendingMap.remove(requestId);
 | 
			
		||||
            if (md == null) {
 | 
			
		||||
                return;
 | 
			
		||||
            }
 | 
			
		||||
            sendNextPendingRequest(rpcId, requestId, "RPC was removed from pending map due to await timeout on response from device!");
 | 
			
		||||
            var toDeviceRpcRequest = md.getMsg().getMsg();
 | 
			
		||||
            if (toDeviceRpcRequest.isPersisted()) {
 | 
			
		||||
                var responseAwaitTimeout = JacksonUtil.newObjectNode().put("error", "There was a timeout awaiting for RPC response from device.");
 | 
			
		||||
                systemContext.getTbRpcService().save(tenantId, new RpcId(rpcId), RpcStatus.FAILED, responseAwaitTimeout);
 | 
			
		||||
            }
 | 
			
		||||
        }, systemContext.getRpcResponseTimeout(), TimeUnit.MILLISECONDS);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    private void clearAwaitRpcResponseScheduler() {
 | 
			
		||||
        if (rpcSubmitStrategy.equals(RpcSubmitStrategy.SEQUENTIAL_ON_RESPONSE_FROM_DEVICE) && awaitRpcResponseFuture != null) {
 | 
			
		||||
            awaitRpcResponseFuture.cancel(true);
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    private void handleSessionActivity(SessionInfoProto sessionInfoProto, SubscriptionInfoProto subscriptionInfo) {
 | 
			
		||||
        UUID sessionId = getSessionId(sessionInfoProto);
 | 
			
		||||
        Objects.requireNonNull(sessionId);
 | 
			
		||||
@ -974,7 +1048,7 @@ public class DeviceActorMessageProcessor extends AbstractContextAwareMsgProcesso
 | 
			
		||||
                    rpc.setStatus(RpcStatus.EXPIRED);
 | 
			
		||||
                    systemContext.getTbRpcService().save(tenantId, rpc);
 | 
			
		||||
                } else {
 | 
			
		||||
                    registerPendingRpcRequest(ctx, new ToDeviceRpcRequestActorMsg(systemContext.getServiceId(), msg), false, creteToDeviceRpcRequestMsg(msg), timeout);
 | 
			
		||||
                    registerPendingRpcRequest(ctx, new ToDeviceRpcRequestActorMsg(systemContext.getServiceId(), msg), false, createToDeviceRpcRequestMsg(msg), timeout);
 | 
			
		||||
                }
 | 
			
		||||
            });
 | 
			
		||||
            if (pageData.hasNext()) {
 | 
			
		||||
 | 
			
		||||
@ -230,7 +230,7 @@ public class RpcV2Controller extends AbstractRpcController {
 | 
			
		||||
        Rpc rpc = checkRpcId(rpcId, Operation.DELETE);
 | 
			
		||||
 | 
			
		||||
        if (rpc != null) {
 | 
			
		||||
            if (rpc.getStatus().equals(RpcStatus.QUEUED)) {
 | 
			
		||||
            if (rpc.getStatus().isPushDeleteNotificationToCore()) {
 | 
			
		||||
                RemoveRpcActorMsg removeMsg = new RemoveRpcActorMsg(getTenantId(), rpc.getDeviceId(), rpc.getUuidId());
 | 
			
		||||
                log.trace("[{}] Forwarding msg {} to queue actor!", rpc.getDeviceId(), rpc);
 | 
			
		||||
                tbClusterService.pushMsgToCore(removeMsg, null);
 | 
			
		||||
 | 
			
		||||
@ -0,0 +1,30 @@
 | 
			
		||||
/**
 | 
			
		||||
 * Copyright © 2016-2023 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.service.rpc;
 | 
			
		||||
 | 
			
		||||
import java.util.Arrays;
 | 
			
		||||
 | 
			
		||||
public enum RpcSubmitStrategy {
 | 
			
		||||
 | 
			
		||||
    BURST, SEQUENTIAL_ON_ACK_FROM_DEVICE, SEQUENTIAL_ON_RESPONSE_FROM_DEVICE;
 | 
			
		||||
 | 
			
		||||
    public static RpcSubmitStrategy parse(String strategyStr) {
 | 
			
		||||
        return Arrays.stream(RpcSubmitStrategy.values())
 | 
			
		||||
                .filter(strategy -> strategy.name().equalsIgnoreCase(strategyStr))
 | 
			
		||||
                .findFirst()
 | 
			
		||||
                .orElse(BURST);
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
@ -405,8 +405,12 @@ actors:
 | 
			
		||||
      # Enqueue the result of external node processing as a separate message to the rule engine.
 | 
			
		||||
      force_ack: "${ACTORS_RULE_EXTERNAL_NODE_FORCE_ACK:false}"
 | 
			
		||||
  rpc:
 | 
			
		||||
    # Maximum number of persistent RPC call retries in case of failed requests delivery.
 | 
			
		||||
    max_retries: "${ACTORS_RPC_MAX_RETRIES:5}"
 | 
			
		||||
    sequential: "${ACTORS_RPC_SEQUENTIAL:false}"
 | 
			
		||||
    # RPC submit strategies. Allowed values: BURST, SEQUENTIAL_ON_ACK_FROM_DEVICE, SEQUENTIAL_ON_RESPONSE_FROM_DEVICE.
 | 
			
		||||
    submit_strategy: "${ACTORS_RPC_SUBMIT_STRATEGY_TYPE:BURST}"
 | 
			
		||||
    # Time in milliseconds for RPC to receive response after delivery. Used only for SEQUENTIAL_ON_RESPONSE_FROM_DEVICE submit strategy.
 | 
			
		||||
    response_timeout_ms: "${ACTORS_RPC_RESPONSE_TIMEOUT_MS:30000}"
 | 
			
		||||
  statistics:
 | 
			
		||||
    # Enable/disable actor statistics
 | 
			
		||||
    enabled: "${ACTORS_STATISTICS_ENABLED:true}"
 | 
			
		||||
 | 
			
		||||
@ -65,6 +65,7 @@ import org.thingsboard.server.actors.TbEntityActorId;
 | 
			
		||||
import org.thingsboard.server.actors.device.DeviceActor;
 | 
			
		||||
import org.thingsboard.server.actors.device.DeviceActorMessageProcessor;
 | 
			
		||||
import org.thingsboard.server.actors.device.SessionInfo;
 | 
			
		||||
import org.thingsboard.server.actors.device.ToDeviceRpcRequestMetadata;
 | 
			
		||||
import org.thingsboard.server.actors.service.DefaultActorService;
 | 
			
		||||
import org.thingsboard.server.common.data.Customer;
 | 
			
		||||
import org.thingsboard.server.common.data.Device;
 | 
			
		||||
@ -1008,6 +1009,15 @@ public abstract class AbstractWebTest extends AbstractInMemoryStorageTest {
 | 
			
		||||
        });
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    protected void awaitForDeviceActorToProcessAllRpcResponses(DeviceId deviceId) {
 | 
			
		||||
        DeviceActorMessageProcessor processor = getDeviceActorProcessor(deviceId);
 | 
			
		||||
        Map<Integer, ToDeviceRpcRequestMetadata> toDeviceRpcPendingMap = (Map<Integer, ToDeviceRpcRequestMetadata>) ReflectionTestUtils.getField(processor, "toDeviceRpcPendingMap");
 | 
			
		||||
        Awaitility.await("Device actor pending map is empty").atMost(5, TimeUnit.SECONDS).until(() -> {
 | 
			
		||||
            log.warn("device {}, toDeviceRpcPendingMap.size() == {}", deviceId, toDeviceRpcPendingMap.size());
 | 
			
		||||
            return toDeviceRpcPendingMap.isEmpty();
 | 
			
		||||
        });
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    protected static String getMapName(FeatureType featureType) {
 | 
			
		||||
        switch (featureType) {
 | 
			
		||||
            case ATTRIBUTES:
 | 
			
		||||
 | 
			
		||||
@ -0,0 +1,38 @@
 | 
			
		||||
/**
 | 
			
		||||
 * Copyright © 2016-2023 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.service.rpc;
 | 
			
		||||
 | 
			
		||||
import org.junit.jupiter.api.Test;
 | 
			
		||||
import org.thingsboard.server.common.data.StringUtils;
 | 
			
		||||
 | 
			
		||||
import static org.assertj.core.api.Assertions.assertThat;
 | 
			
		||||
 | 
			
		||||
class RpcSubmitStrategyTest {
 | 
			
		||||
 | 
			
		||||
    @Test
 | 
			
		||||
    void givenRandomString_whenParse_thenReturnBurstStrategy() {
 | 
			
		||||
        String randomString = StringUtils.randomAlphanumeric(10);
 | 
			
		||||
        RpcSubmitStrategy parsed = RpcSubmitStrategy.parse(randomString);
 | 
			
		||||
        assertThat(parsed).isEqualTo(RpcSubmitStrategy.BURST);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    @Test
 | 
			
		||||
    void givenNull_whenParse_thenReturnBurstStrategy() {
 | 
			
		||||
        RpcSubmitStrategy parsed = RpcSubmitStrategy.parse(null);
 | 
			
		||||
        assertThat(parsed).isEqualTo(RpcSubmitStrategy.BURST);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
}
 | 
			
		||||
@ -186,8 +186,12 @@ public abstract class AbstractMqttIntegrationTest extends AbstractTransportInteg
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    protected void subscribeAndWait(MqttTestClient client, String attrSubTopic, DeviceId deviceId, FeatureType featureType) throws MqttException {
 | 
			
		||||
        subscribeAndWait(client, attrSubTopic, deviceId, featureType, MqttQoS.AT_MOST_ONCE);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    protected void subscribeAndWait(MqttTestClient client, String attrSubTopic, DeviceId deviceId, FeatureType featureType, MqttQoS mqttQoS) throws MqttException {
 | 
			
		||||
        int subscriptionCount = getDeviceActorSubscriptionCount(deviceId, featureType);
 | 
			
		||||
        client.subscribeAndWait(attrSubTopic, MqttQoS.AT_MOST_ONCE);
 | 
			
		||||
        client.subscribeAndWait(attrSubTopic, mqttQoS);
 | 
			
		||||
        // TODO: This test awaits for the device actor to receive the subscription. Ideally it should not happen. See details below:
 | 
			
		||||
        // The transport layer acknowledge subscription request once the message about subscription is in the queue.
 | 
			
		||||
        // Test sends data immediately after acknowledgement.
 | 
			
		||||
 | 
			
		||||
@ -30,7 +30,7 @@ public class MqttTestCallback implements MqttCallback {
 | 
			
		||||
 | 
			
		||||
    protected CountDownLatch subscribeLatch;
 | 
			
		||||
    protected final CountDownLatch deliveryLatch;
 | 
			
		||||
    protected int qoS;
 | 
			
		||||
    protected int messageArrivedQoS;
 | 
			
		||||
    protected byte[] payloadBytes;
 | 
			
		||||
    protected boolean pubAckReceived;
 | 
			
		||||
 | 
			
		||||
@ -53,7 +53,7 @@ public class MqttTestCallback implements MqttCallback {
 | 
			
		||||
    @Override
 | 
			
		||||
    public void messageArrived(String requestTopic, MqttMessage mqttMessage) {
 | 
			
		||||
        log.warn("messageArrived on topic: {}", requestTopic);
 | 
			
		||||
        qoS = mqttMessage.getQos();
 | 
			
		||||
        messageArrivedQoS = mqttMessage.getQos();
 | 
			
		||||
        payloadBytes = mqttMessage.getPayload();
 | 
			
		||||
        subscribeLatch.countDown();
 | 
			
		||||
    }
 | 
			
		||||
@ -63,6 +63,5 @@ public class MqttTestCallback implements MqttCallback {
 | 
			
		||||
        log.warn("delivery complete: {}", iMqttDeliveryToken.getResponse());
 | 
			
		||||
        pubAckReceived = iMqttDeliveryToken.getResponse().getType() == MqttWireMessage.MESSAGE_TYPE_PUBACK;
 | 
			
		||||
        deliveryLatch.countDown();
 | 
			
		||||
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@ -36,7 +36,7 @@ public class MqttTestSubscribeOnTopicCallback extends MqttTestCallback {
 | 
			
		||||
    public void messageArrived(String requestTopic, MqttMessage mqttMessage) {
 | 
			
		||||
        log.warn("messageArrived on topic: {}, awaitSubTopic: {}", requestTopic, awaitSubTopic);
 | 
			
		||||
        if (awaitSubTopic.equals(requestTopic)) {
 | 
			
		||||
            qoS = mqttMessage.getQos();
 | 
			
		||||
            messageArrivedQoS = mqttMessage.getQos();
 | 
			
		||||
            payloadBytes = mqttMessage.getPayload();
 | 
			
		||||
            subscribeLatch.countDown();
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
@ -575,14 +575,14 @@ public abstract class AbstractMqttAttributesIntegrationTest extends AbstractMqtt
 | 
			
		||||
    protected void validateJsonResponse(MqttTestCallback callback, String expectedResponse) throws InterruptedException {
 | 
			
		||||
        assertThat(callback.getSubscribeLatch().await(DEFAULT_WAIT_TIMEOUT_SECONDS, TimeUnit.SECONDS))
 | 
			
		||||
                .as("await callback").isTrue();
 | 
			
		||||
        assertEquals(MqttQoS.AT_MOST_ONCE.value(), callback.getQoS());
 | 
			
		||||
        assertEquals(MqttQoS.AT_MOST_ONCE.value(), callback.getMessageArrivedQoS());
 | 
			
		||||
        assertEquals(JacksonUtil.toJsonNode(expectedResponse), JacksonUtil.fromBytes(callback.getPayloadBytes()));
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    protected void validateProtoResponse(MqttTestCallback callback, TransportProtos.GetAttributeResponseMsg expectedResponse) throws InterruptedException, InvalidProtocolBufferException {
 | 
			
		||||
        assertThat(callback.getSubscribeLatch().await(DEFAULT_WAIT_TIMEOUT_SECONDS, TimeUnit.SECONDS))
 | 
			
		||||
                .as("await callback").isTrue();
 | 
			
		||||
        assertEquals(MqttQoS.AT_MOST_ONCE.value(), callback.getQoS());
 | 
			
		||||
        assertEquals(MqttQoS.AT_MOST_ONCE.value(), callback.getMessageArrivedQoS());
 | 
			
		||||
        TransportProtos.GetAttributeResponseMsg actualAttributesResponse = TransportProtos.GetAttributeResponseMsg.parseFrom(callback.getPayloadBytes());
 | 
			
		||||
        assertEquals(expectedResponse.getRequestId(), actualAttributesResponse.getRequestId());
 | 
			
		||||
        List<TransportProtos.KeyValueProto> expectedClientKeyValueProtos = expectedResponse.getClientAttributeListList().stream().map(TransportProtos.TsKvProto::getKv).collect(Collectors.toList());
 | 
			
		||||
@ -606,7 +606,7 @@ public abstract class AbstractMqttAttributesIntegrationTest extends AbstractMqtt
 | 
			
		||||
    protected void validateJsonResponseGateway(MqttTestCallback callback, String deviceName, String expectedValues) throws InterruptedException {
 | 
			
		||||
        assertThat(callback.getSubscribeLatch().await(DEFAULT_WAIT_TIMEOUT_SECONDS, TimeUnit.SECONDS))
 | 
			
		||||
                .as("await callback").isTrue();
 | 
			
		||||
        assertEquals(MqttQoS.AT_LEAST_ONCE.value(), callback.getQoS());
 | 
			
		||||
        assertEquals(MqttQoS.AT_LEAST_ONCE.value(), callback.getMessageArrivedQoS());
 | 
			
		||||
        String expectedRequestPayload = "{\"id\":1,\"device\":\"" + deviceName + "\",\"values\":" + expectedValues + "}";
 | 
			
		||||
        assertEquals(JacksonUtil.toJsonNode(expectedRequestPayload), JacksonUtil.fromBytes(callback.getPayloadBytes()));
 | 
			
		||||
    }
 | 
			
		||||
@ -614,7 +614,7 @@ public abstract class AbstractMqttAttributesIntegrationTest extends AbstractMqtt
 | 
			
		||||
    protected void validateProtoClientResponseGateway(MqttTestCallback callback, String deviceName) throws InterruptedException, InvalidProtocolBufferException {
 | 
			
		||||
        assertThat(callback.getSubscribeLatch().await(DEFAULT_WAIT_TIMEOUT_SECONDS, TimeUnit.SECONDS))
 | 
			
		||||
                .as("await callback").isTrue();
 | 
			
		||||
        assertEquals(MqttQoS.AT_LEAST_ONCE.value(), callback.getQoS());
 | 
			
		||||
        assertEquals(MqttQoS.AT_LEAST_ONCE.value(), callback.getMessageArrivedQoS());
 | 
			
		||||
        TransportApiProtos.GatewayAttributeResponseMsg expectedGatewayAttributeResponseMsg = getExpectedGatewayAttributeResponseMsg(deviceName, true);
 | 
			
		||||
        TransportApiProtos.GatewayAttributeResponseMsg actualGatewayAttributeResponseMsg = TransportApiProtos.GatewayAttributeResponseMsg.parseFrom(callback.getPayloadBytes());
 | 
			
		||||
        assertEquals(expectedGatewayAttributeResponseMsg.getDeviceName(), actualGatewayAttributeResponseMsg.getDeviceName());
 | 
			
		||||
@ -631,7 +631,7 @@ public abstract class AbstractMqttAttributesIntegrationTest extends AbstractMqtt
 | 
			
		||||
    protected void validateProtoSharedResponseGateway(MqttTestCallback callback, String deviceName) throws InterruptedException, InvalidProtocolBufferException {
 | 
			
		||||
        assertThat(callback.getSubscribeLatch().await(DEFAULT_WAIT_TIMEOUT_SECONDS, TimeUnit.SECONDS))
 | 
			
		||||
                .as("await callback").isTrue();
 | 
			
		||||
        assertEquals(MqttQoS.AT_LEAST_ONCE.value(), callback.getQoS());
 | 
			
		||||
        assertEquals(MqttQoS.AT_LEAST_ONCE.value(), callback.getMessageArrivedQoS());
 | 
			
		||||
        TransportApiProtos.GatewayAttributeResponseMsg expectedGatewayAttributeResponseMsg = getExpectedGatewayAttributeResponseMsg(deviceName, false);
 | 
			
		||||
        TransportApiProtos.GatewayAttributeResponseMsg actualGatewayAttributeResponseMsg = TransportApiProtos.GatewayAttributeResponseMsg.parseFrom(callback.getPayloadBytes());
 | 
			
		||||
        assertEquals(expectedGatewayAttributeResponseMsg.getDeviceName(), actualGatewayAttributeResponseMsg.getDeviceName());
 | 
			
		||||
 | 
			
		||||
@ -37,12 +37,13 @@ import org.thingsboard.server.common.data.device.profile.DeviceProfileTransportC
 | 
			
		||||
import org.thingsboard.server.common.data.device.profile.MqttDeviceProfileTransportConfiguration;
 | 
			
		||||
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.msg.session.FeatureType;
 | 
			
		||||
import org.thingsboard.server.gen.transport.TransportApiProtos;
 | 
			
		||||
import org.thingsboard.server.transport.mqtt.AbstractMqttIntegrationTest;
 | 
			
		||||
import org.thingsboard.server.transport.mqtt.mqttv3.MqttTestCallback;
 | 
			
		||||
import org.thingsboard.server.transport.mqtt.mqttv3.MqttTestSubscribeOnTopicCallback;
 | 
			
		||||
import org.thingsboard.server.transport.mqtt.mqttv3.MqttTestClient;
 | 
			
		||||
import org.thingsboard.server.transport.mqtt.mqttv3.MqttTestSubscribeOnTopicCallback;
 | 
			
		||||
 | 
			
		||||
import java.util.ArrayList;
 | 
			
		||||
import java.util.List;
 | 
			
		||||
@ -61,7 +62,7 @@ import static org.thingsboard.server.common.data.device.profile.MqttTopics.GATEW
 | 
			
		||||
@Slf4j
 | 
			
		||||
public abstract class AbstractMqttServerSideRpcIntegrationTest extends AbstractMqttIntegrationTest {
 | 
			
		||||
 | 
			
		||||
    protected static final  String RPC_REQUEST_PROTO_SCHEMA = "syntax =\"proto3\";\n" +
 | 
			
		||||
    protected static final String RPC_REQUEST_PROTO_SCHEMA = "syntax =\"proto3\";\n" +
 | 
			
		||||
            "package rpc;\n" +
 | 
			
		||||
            "\n" +
 | 
			
		||||
            "message RpcRequestMsg {\n" +
 | 
			
		||||
@ -105,7 +106,7 @@ public abstract class AbstractMqttServerSideRpcIntegrationTest extends AbstractM
 | 
			
		||||
        } else {
 | 
			
		||||
            assertEquals(JacksonUtil.toJsonNode(setGpioRequest), JacksonUtil.fromBytes(callback.getPayloadBytes()));
 | 
			
		||||
        }
 | 
			
		||||
        assertEquals(MqttQoS.AT_MOST_ONCE.value(), callback.getQoS());
 | 
			
		||||
        assertEquals(MqttQoS.AT_MOST_ONCE.value(), callback.getMessageArrivedQoS());
 | 
			
		||||
        client.disconnect();
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
@ -176,9 +177,9 @@ public abstract class AbstractMqttServerSideRpcIntegrationTest extends AbstractM
 | 
			
		||||
        return builder.build();
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    protected void processSequenceTwoWayRpcTest() throws Exception {
 | 
			
		||||
        List<String> expected = new ArrayList<>();
 | 
			
		||||
        List<String> result = new ArrayList<>();
 | 
			
		||||
    protected void processSequenceOneWayRpcTest(MqttQoS mqttQoS) throws Exception {
 | 
			
		||||
        List<String> expectedRequest = new ArrayList<>();
 | 
			
		||||
        List<String> actualRequests = new ArrayList<>();
 | 
			
		||||
 | 
			
		||||
        String deviceId = savedDevice.getId().getId().toString();
 | 
			
		||||
 | 
			
		||||
@ -186,20 +187,67 @@ public abstract class AbstractMqttServerSideRpcIntegrationTest extends AbstractM
 | 
			
		||||
            ObjectNode request = JacksonUtil.newObjectNode();
 | 
			
		||||
            request.put("method", "test");
 | 
			
		||||
            request.put("params", i);
 | 
			
		||||
            expected.add(JacksonUtil.toString(request));
 | 
			
		||||
            expectedRequest.add(JacksonUtil.toString(request));
 | 
			
		||||
            request.put("persistent", true);
 | 
			
		||||
            doPostAsync("/api/rpc/twoway/" + deviceId, JacksonUtil.toString(request), String.class, status().isOk());
 | 
			
		||||
            doPostAsync("/api/rpc/oneway/" + deviceId, JacksonUtil.toString(request), String.class, status().isOk());
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        MqttTestClient client = new MqttTestClient();
 | 
			
		||||
        client.connectAndWait(accessToken);
 | 
			
		||||
        client.enableManualAcks();
 | 
			
		||||
        MqttTestSequenceCallback callback = new MqttTestSequenceCallback(client, 10, result);
 | 
			
		||||
        MqttTestOneWaySequenceCallback callback = new MqttTestOneWaySequenceCallback(client, 10, actualRequests);
 | 
			
		||||
        client.setCallback(callback);
 | 
			
		||||
        subscribeAndWait(client, DEVICE_RPC_REQUESTS_SUB_TOPIC, savedDevice.getId(), FeatureType.RPC);
 | 
			
		||||
        subscribeAndWait(client, DEVICE_RPC_REQUESTS_SUB_TOPIC, savedDevice.getId(), FeatureType.RPC, mqttQoS);
 | 
			
		||||
 | 
			
		||||
        callback.getSubscribeLatch().await(DEFAULT_WAIT_TIMEOUT_SECONDS, TimeUnit.SECONDS);
 | 
			
		||||
        assertEquals(expected, result);
 | 
			
		||||
        assertEquals(expectedRequest, actualRequests);
 | 
			
		||||
        client.disconnect();
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    protected void processSequenceTwoWayRpcTest(MqttQoS mqttQoS) throws Exception {
 | 
			
		||||
        processSequenceTwoWayRpcTest(mqttQoS, false);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    protected void processSequenceTwoWayRpcTest(MqttQoS mqttQoS, boolean manualAcksEnabled) throws Exception {
 | 
			
		||||
        List<String> expectedRequest = new ArrayList<>();
 | 
			
		||||
        List<String> actualRequests = new ArrayList<>();
 | 
			
		||||
 | 
			
		||||
        List<String> rpcIds = new ArrayList<>();
 | 
			
		||||
 | 
			
		||||
        List<String> expectedResponses = new ArrayList<>();
 | 
			
		||||
        List<String> actualResponses = new ArrayList<>();
 | 
			
		||||
 | 
			
		||||
        String deviceId = savedDevice.getId().getId().toString();
 | 
			
		||||
 | 
			
		||||
        for (int i = 0; i < 10; i++) {
 | 
			
		||||
            ObjectNode request = JacksonUtil.newObjectNode();
 | 
			
		||||
            request.put("method", "test");
 | 
			
		||||
            request.put("params", i);
 | 
			
		||||
            expectedRequest.add(JacksonUtil.toString(request));
 | 
			
		||||
            request.put("persistent", true);
 | 
			
		||||
            String response = doPostAsync("/api/rpc/twoway/" + deviceId, JacksonUtil.toString(request), String.class, status().isOk());
 | 
			
		||||
            var responseNode = JacksonUtil.toJsonNode(response);
 | 
			
		||||
            rpcIds.add(responseNode.get("rpcId").asText());
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        MqttTestClient client = new MqttTestClient();
 | 
			
		||||
        client.connectAndWait(accessToken);
 | 
			
		||||
        if (manualAcksEnabled) {
 | 
			
		||||
            client.enableManualAcks();
 | 
			
		||||
        }
 | 
			
		||||
        MqttTestTwoWaySequenceCallback callback = new MqttTestTwoWaySequenceCallback(
 | 
			
		||||
                client, 10, actualRequests, expectedResponses, manualAcksEnabled);
 | 
			
		||||
        client.setCallback(callback);
 | 
			
		||||
        subscribeAndWait(client, DEVICE_RPC_REQUESTS_SUB_TOPIC, savedDevice.getId(), FeatureType.RPC, mqttQoS);
 | 
			
		||||
 | 
			
		||||
        callback.getSubscribeLatch().await(DEFAULT_WAIT_TIMEOUT_SECONDS, TimeUnit.SECONDS);
 | 
			
		||||
        assertEquals(expectedRequest, actualRequests);
 | 
			
		||||
        awaitForDeviceActorToProcessAllRpcResponses(savedDevice.getId());
 | 
			
		||||
        for (String rpcId : rpcIds) {
 | 
			
		||||
            Rpc rpc = doGet("/api/rpc/persistent/" + rpcId, Rpc.class);
 | 
			
		||||
            actualResponses.add(JacksonUtil.toString(rpc.getResponse()));
 | 
			
		||||
        }
 | 
			
		||||
        assertEquals(expectedResponses, actualResponses);
 | 
			
		||||
        client.disconnect();
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    protected void processJsonTwoWayRpcTestGateway(String deviceName) throws Exception {
 | 
			
		||||
@ -222,7 +270,7 @@ public abstract class AbstractMqttServerSideRpcIntegrationTest extends AbstractM
 | 
			
		||||
        );
 | 
			
		||||
        assertNotNull(savedDevice);
 | 
			
		||||
 | 
			
		||||
        MqttTestCallback  callback = new MqttTestSubscribeOnTopicCallback(GATEWAY_RPC_TOPIC);
 | 
			
		||||
        MqttTestCallback callback = new MqttTestSubscribeOnTopicCallback(GATEWAY_RPC_TOPIC);
 | 
			
		||||
        client.setCallback(callback);
 | 
			
		||||
        subscribeAndCheckSubscription(client, GATEWAY_RPC_TOPIC, savedDevice.getId(), FeatureType.RPC);
 | 
			
		||||
 | 
			
		||||
@ -248,7 +296,7 @@ public abstract class AbstractMqttServerSideRpcIntegrationTest extends AbstractM
 | 
			
		||||
            JsonNode expectedJsonRequestData = getExpectedGatewayJsonRequestData(deviceName, setGpioRequest);
 | 
			
		||||
            assertEquals(expectedJsonRequestData, JacksonUtil.fromBytes(callback.getPayloadBytes()));
 | 
			
		||||
        }
 | 
			
		||||
        assertEquals(MqttQoS.AT_MOST_ONCE.value(), callback.getQoS());
 | 
			
		||||
        assertEquals(MqttQoS.AT_MOST_ONCE.value(), callback.getMessageArrivedQoS());
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    private JsonNode getExpectedGatewayJsonRequestData(String deviceName, String requestStr) {
 | 
			
		||||
@ -280,7 +328,7 @@ public abstract class AbstractMqttServerSideRpcIntegrationTest extends AbstractM
 | 
			
		||||
        callback.getSubscribeLatch().await(DEFAULT_WAIT_TIMEOUT_SECONDS, TimeUnit.SECONDS);
 | 
			
		||||
        log.warn("request payload: {}", JacksonUtil.fromBytes(callback.getPayloadBytes()));
 | 
			
		||||
        assertEquals("{\"success\":true}", actualRpcResponse);
 | 
			
		||||
        assertEquals(MqttQoS.AT_MOST_ONCE.value(), callback.getQoS());
 | 
			
		||||
        assertEquals(MqttQoS.AT_MOST_ONCE.value(), callback.getMessageArrivedQoS());
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    protected void validateProtoTwoWayRpcGatewayResponse(String deviceName, MqttTestClient client, byte[] connectPayloadBytes) throws Exception {
 | 
			
		||||
@ -302,7 +350,7 @@ public abstract class AbstractMqttServerSideRpcIntegrationTest extends AbstractM
 | 
			
		||||
        String actualRpcResponse = doPostAsync("/api/rpc/twoway/" + deviceId, setGpioRequest, String.class, status().isOk());
 | 
			
		||||
        callback.getSubscribeLatch().await(DEFAULT_WAIT_TIMEOUT_SECONDS, TimeUnit.SECONDS);
 | 
			
		||||
        assertEquals("{\"success\":true}", actualRpcResponse);
 | 
			
		||||
        assertEquals(MqttQoS.AT_MOST_ONCE.value(), callback.getQoS());
 | 
			
		||||
        assertEquals(MqttQoS.AT_MOST_ONCE.value(), callback.getMessageArrivedQoS());
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    private Device getDeviceByName(String deviceName) throws Exception {
 | 
			
		||||
@ -334,7 +382,7 @@ public abstract class AbstractMqttServerSideRpcIntegrationTest extends AbstractM
 | 
			
		||||
        public void messageArrived(String requestTopic, MqttMessage mqttMessage) {
 | 
			
		||||
            log.warn("messageArrived on topic: {}, awaitSubTopic: {}", requestTopic, awaitSubTopic);
 | 
			
		||||
            if (awaitSubTopic.equals(requestTopic)) {
 | 
			
		||||
                qoS = mqttMessage.getQos();
 | 
			
		||||
                messageArrivedQoS = mqttMessage.getQos();
 | 
			
		||||
                payloadBytes = mqttMessage.getPayload();
 | 
			
		||||
                String responseTopic;
 | 
			
		||||
                if (requestTopic.startsWith(BASE_DEVICE_API_TOPIC_V2)) {
 | 
			
		||||
@ -366,7 +414,7 @@ public abstract class AbstractMqttServerSideRpcIntegrationTest extends AbstractM
 | 
			
		||||
        public void messageArrived(String requestTopic, MqttMessage mqttMessage) {
 | 
			
		||||
            log.warn("messageArrived on topic: {}, awaitSubTopic: {}", requestTopic, awaitSubTopic);
 | 
			
		||||
            if (awaitSubTopic.equals(requestTopic)) {
 | 
			
		||||
                qoS = mqttMessage.getQos();
 | 
			
		||||
                messageArrivedQoS = mqttMessage.getQos();
 | 
			
		||||
                payloadBytes = mqttMessage.getPayload();
 | 
			
		||||
                String responseTopic;
 | 
			
		||||
                if (requestTopic.startsWith(BASE_DEVICE_API_TOPIC_V2)) {
 | 
			
		||||
@ -398,7 +446,7 @@ public abstract class AbstractMqttServerSideRpcIntegrationTest extends AbstractM
 | 
			
		||||
            try {
 | 
			
		||||
                DynamicMessage dynamicMessage = DynamicMessage.parseFrom(rpcRequestMsgDescriptor, requestPayload);
 | 
			
		||||
                List<Descriptors.FieldDescriptor> fields = rpcRequestMsgDescriptor.getFields();
 | 
			
		||||
                for (Descriptors.FieldDescriptor fieldDescriptor: fields) {
 | 
			
		||||
                for (Descriptors.FieldDescriptor fieldDescriptor : fields) {
 | 
			
		||||
                    assertTrue(dynamicMessage.hasField(fieldDescriptor));
 | 
			
		||||
                }
 | 
			
		||||
                ProtoFileElement rpcResponseProtoFileElement = DynamicProtoUtils.getProtoFileElement(protoTransportPayloadConfiguration.getDeviceRpcResponseProtoSchema());
 | 
			
		||||
@ -436,30 +484,69 @@ public abstract class AbstractMqttServerSideRpcIntegrationTest extends AbstractM
 | 
			
		||||
        return (ProtoTransportPayloadConfiguration) transportPayloadTypeConfiguration;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    protected class MqttTestSequenceCallback extends MqttTestCallback {
 | 
			
		||||
    protected static class MqttTestOneWaySequenceCallback extends MqttTestCallback {
 | 
			
		||||
 | 
			
		||||
        private final MqttTestClient client;
 | 
			
		||||
        private final List<String> expected;
 | 
			
		||||
        private final List<String> requests;
 | 
			
		||||
 | 
			
		||||
        MqttTestSequenceCallback(MqttTestClient client, int subscribeCount, List<String> expected) {
 | 
			
		||||
        MqttTestOneWaySequenceCallback(MqttTestClient client, int subscribeCount, List<String> requests) {
 | 
			
		||||
            super(subscribeCount);
 | 
			
		||||
            this.client = client;
 | 
			
		||||
            this.expected = expected;
 | 
			
		||||
            this.requests = requests;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        @Override
 | 
			
		||||
        public void messageArrived(String requestTopic, MqttMessage mqttMessage) {
 | 
			
		||||
            log.warn("messageArrived on topic: {}", requestTopic);
 | 
			
		||||
            expected.add(new String(mqttMessage.getPayload()));
 | 
			
		||||
            String responseTopic = requestTopic.replace("request", "response");
 | 
			
		||||
            qoS = mqttMessage.getQos();
 | 
			
		||||
            try {
 | 
			
		||||
                client.messageArrivedComplete(mqttMessage);
 | 
			
		||||
                client.publish(responseTopic, processJsonMessageArrived(requestTopic, mqttMessage));
 | 
			
		||||
            } catch (MqttException e) {
 | 
			
		||||
                log.warn("Failed to publish response on topic: {} due to: ", responseTopic, e);
 | 
			
		||||
            }
 | 
			
		||||
            requests.add(new String(mqttMessage.getPayload()));
 | 
			
		||||
            messageArrivedQoS = mqttMessage.getQos();
 | 
			
		||||
            subscribeLatch.countDown();
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    protected class MqttTestTwoWaySequenceCallback extends MqttTestCallback {
 | 
			
		||||
 | 
			
		||||
        private final MqttTestClient client;
 | 
			
		||||
        private final List<String> requests;
 | 
			
		||||
        private final List<String> responses;
 | 
			
		||||
        private final boolean manualAcksEnabled;
 | 
			
		||||
 | 
			
		||||
        MqttTestTwoWaySequenceCallback(MqttTestClient client, int subscribeCount, List<String> requests, List<String> responses, boolean manualAcksEnabled) {
 | 
			
		||||
            super(subscribeCount);
 | 
			
		||||
            this.client = client;
 | 
			
		||||
            this.requests = requests;
 | 
			
		||||
            this.responses = responses;
 | 
			
		||||
            this.manualAcksEnabled = manualAcksEnabled;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        @Override
 | 
			
		||||
        public void messageArrived(String requestTopic, MqttMessage mqttMessage) {
 | 
			
		||||
            log.warn("messageArrived on topic: {}", requestTopic);
 | 
			
		||||
            requests.add(new String(mqttMessage.getPayload()));
 | 
			
		||||
            messageArrivedQoS = mqttMessage.getQos();
 | 
			
		||||
            if (manualAcksEnabled) {
 | 
			
		||||
                try {
 | 
			
		||||
                    client.messageArrivedComplete(mqttMessage);
 | 
			
		||||
                } catch (MqttException e) {
 | 
			
		||||
                    log.warn("Failed to ack message delivery on topic: {} due to: ", requestTopic, e);
 | 
			
		||||
                } finally {
 | 
			
		||||
                    subscribeLatch.countDown();
 | 
			
		||||
                    processResponse(requestTopic, mqttMessage);
 | 
			
		||||
                }
 | 
			
		||||
                return;
 | 
			
		||||
            }
 | 
			
		||||
            subscribeLatch.countDown();
 | 
			
		||||
            processResponse(requestTopic, mqttMessage);
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        private void processResponse(String requestTopic, MqttMessage mqttMessage) {
 | 
			
		||||
            String responseTopic = requestTopic.replace("request", "response");
 | 
			
		||||
            byte[] responsePayload = processJsonMessageArrived(requestTopic, mqttMessage);
 | 
			
		||||
            responses.add(new String(responsePayload));
 | 
			
		||||
            try {
 | 
			
		||||
                client.publish(responseTopic, responsePayload);
 | 
			
		||||
            } catch (MqttException e) {
 | 
			
		||||
                log.warn("Failed to publish response on topic: {} due to: ", responseTopic, e);
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@ -110,11 +110,6 @@ public class MqttServerSideRpcDefaultIntegrationTest extends AbstractMqttServerS
 | 
			
		||||
        processJsonTwoWayRpcTest(DEVICE_RPC_REQUESTS_SUB_SHORT_JSON_TOPIC);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    @Test
 | 
			
		||||
    public void testSequenceServerMqttTwoWayRpc() throws Exception {
 | 
			
		||||
        processSequenceTwoWayRpcTest();
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    @Test
 | 
			
		||||
    public void testGatewayServerMqttOneWayRpc() throws Exception {
 | 
			
		||||
        processJsonOneWayRpcTestGateway("Gateway Device OneWay RPC");
 | 
			
		||||
 | 
			
		||||
@ -0,0 +1,72 @@
 | 
			
		||||
/**
 | 
			
		||||
 * Copyright © 2016-2023 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.server.dao.service.DaoSqlTest;
 | 
			
		||||
import org.thingsboard.server.transport.mqtt.MqttTestConfigProperties;
 | 
			
		||||
 | 
			
		||||
@Slf4j
 | 
			
		||||
@DaoSqlTest
 | 
			
		||||
@TestPropertySource(properties = {
 | 
			
		||||
        "actors.rpc.submit_strategy=SEQUENTIAL_ON_ACK_FROM_DEVICE",
 | 
			
		||||
})
 | 
			
		||||
public class MqttServerSideRpcSequenceOnAckIntegrationTest extends AbstractMqttServerSideRpcIntegrationTest {
 | 
			
		||||
 | 
			
		||||
    @Before
 | 
			
		||||
    public void beforeTest() throws Exception {
 | 
			
		||||
        MqttTestConfigProperties configProperties = MqttTestConfigProperties.builder()
 | 
			
		||||
                .deviceName("RPC test device")
 | 
			
		||||
                .gatewayName("RPC test gateway")
 | 
			
		||||
                .build();
 | 
			
		||||
        processBeforeTest(configProperties);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    @Test
 | 
			
		||||
    public void testSequenceServerMqttOneWayRpcQoSAtMostOnce() throws Exception {
 | 
			
		||||
        processSequenceOneWayRpcTest(MqttQoS.AT_MOST_ONCE);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    @Test
 | 
			
		||||
    public void testSequenceServerMqttOneWayRpcQoSAtLeastOnce() throws Exception {
 | 
			
		||||
        processSequenceOneWayRpcTest(MqttQoS.AT_LEAST_ONCE);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    @Test
 | 
			
		||||
    public void testSequenceServerMqttTwoWayRpcQoSAtMostOnce() throws Exception {
 | 
			
		||||
        processSequenceTwoWayRpcTest(MqttQoS.AT_MOST_ONCE);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    @Test
 | 
			
		||||
    public void testSequenceServerMqttTwoWayRpcQoSAtLeastOnce() throws Exception {
 | 
			
		||||
        processSequenceTwoWayRpcTest(MqttQoS.AT_LEAST_ONCE);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    @Test
 | 
			
		||||
    public void testSequenceServerMqttTwoWayRpcQoSAtMostOnceWithManualAcksEnabled() throws Exception {
 | 
			
		||||
        processSequenceTwoWayRpcTest(MqttQoS.AT_MOST_ONCE, true);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    @Test
 | 
			
		||||
    public void testSequenceServerMqttTwoWayRpcQoSAtLeastOnceWithoutManualAcksEnabled() throws Exception {
 | 
			
		||||
        processSequenceTwoWayRpcTest(MqttQoS.AT_LEAST_ONCE, true);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
}
 | 
			
		||||
@ -0,0 +1,73 @@
 | 
			
		||||
/**
 | 
			
		||||
 * Copyright © 2016-2023 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.server.dao.service.DaoSqlTest;
 | 
			
		||||
import org.thingsboard.server.transport.mqtt.MqttTestConfigProperties;
 | 
			
		||||
 | 
			
		||||
@Slf4j
 | 
			
		||||
@DaoSqlTest
 | 
			
		||||
@TestPropertySource(properties = {
 | 
			
		||||
        "actors.rpc.submit_strategy=SEQUENTIAL_ON_RESPONSE_FROM_DEVICE",
 | 
			
		||||
})
 | 
			
		||||
public class MqttServerSideRpcSequenceOnResponseIntegrationTest extends AbstractMqttServerSideRpcIntegrationTest {
 | 
			
		||||
 | 
			
		||||
    @Before
 | 
			
		||||
    public void beforeTest() throws Exception {
 | 
			
		||||
        MqttTestConfigProperties configProperties = MqttTestConfigProperties.builder()
 | 
			
		||||
                .deviceName("RPC test device")
 | 
			
		||||
                .gatewayName("RPC test gateway")
 | 
			
		||||
                .build();
 | 
			
		||||
        processBeforeTest(configProperties);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    @Test
 | 
			
		||||
    public void testSequenceServerMqttOneWayRpcQoSAtMostOnce() throws Exception {
 | 
			
		||||
        processSequenceOneWayRpcTest(MqttQoS.AT_MOST_ONCE);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    @Test
 | 
			
		||||
    public void testSequenceServerMqttOneWayRpcQoSAtLeastOnce() throws Exception {
 | 
			
		||||
        processSequenceOneWayRpcTest(MqttQoS.AT_LEAST_ONCE);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    @Test
 | 
			
		||||
    public void testSequenceServerMqttTwoWayRpcQoSAtMostOnce() throws Exception {
 | 
			
		||||
        processSequenceTwoWayRpcTest(MqttQoS.AT_MOST_ONCE);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    @Test
 | 
			
		||||
    public void testSequenceServerMqttTwoWayRpcQoSAtLeastOnce() throws Exception {
 | 
			
		||||
        processSequenceTwoWayRpcTest(MqttQoS.AT_LEAST_ONCE);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    @Test
 | 
			
		||||
    public void testSequenceServerMqttTwoWayRpcQoSAtMostOnceWithManualAcksEnabled() throws Exception {
 | 
			
		||||
        processSequenceTwoWayRpcTest(MqttQoS.AT_MOST_ONCE, true);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    @Test
 | 
			
		||||
    public void testSequenceServerMqttTwoWayRpcQoSAtLeastOnceWithoutManualAcksEnabled() throws Exception {
 | 
			
		||||
        processSequenceTwoWayRpcTest(MqttQoS.AT_LEAST_ONCE, true);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
}
 | 
			
		||||
@ -332,7 +332,7 @@ public abstract class AbstractMqttTimeseriesIntegrationTest extends AbstractMqtt
 | 
			
		||||
        doPostAsync("/api/plugins/telemetry/" + savedDevice.getId() + "/SHARED_SCOPE", payload, String.class, status().isOk());
 | 
			
		||||
        callback.getSubscribeLatch().await(DEFAULT_WAIT_TIMEOUT_SECONDS, TimeUnit.SECONDS);
 | 
			
		||||
        assertEquals(payload.getBytes(), callback.getPayloadBytes());
 | 
			
		||||
        assertEquals(MqttQoS.AT_MOST_ONCE.value(), callback.getQoS());
 | 
			
		||||
        assertEquals(MqttQoS.AT_MOST_ONCE.value(), callback.getMessageArrivedQoS());
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@ -13,7 +13,7 @@ transport.lwm2m.security.trust-credentials.keystore.store_file=lwm2m/credentials
 | 
			
		||||
edges.enabled=false
 | 
			
		||||
edges.storage.no_read_records_sleep=500
 | 
			
		||||
edges.storage.sleep_between_batches=500
 | 
			
		||||
actors.rpc.sequential=true
 | 
			
		||||
actors.rpc.submit_strategy=BURST
 | 
			
		||||
queue.rule-engine.stats.enabled=true
 | 
			
		||||
 | 
			
		||||
# Transports disabled to speed up the context init. Particular transport will be enabled with @TestPropertySource in respective tests
 | 
			
		||||
 | 
			
		||||
@ -15,6 +15,24 @@
 | 
			
		||||
 */
 | 
			
		||||
package org.thingsboard.server.common.data.rpc;
 | 
			
		||||
 | 
			
		||||
import lombok.Getter;
 | 
			
		||||
 | 
			
		||||
public enum RpcStatus {
 | 
			
		||||
    QUEUED, SENT, DELIVERED, SUCCESSFUL, TIMEOUT, EXPIRED, FAILED, DELETED
 | 
			
		||||
 | 
			
		||||
    QUEUED(true),
 | 
			
		||||
    SENT(true),
 | 
			
		||||
    DELIVERED(true),
 | 
			
		||||
    SUCCESSFUL(false),
 | 
			
		||||
    TIMEOUT(false),
 | 
			
		||||
    EXPIRED(false),
 | 
			
		||||
    FAILED(false),
 | 
			
		||||
    DELETED(false);
 | 
			
		||||
 | 
			
		||||
    @Getter
 | 
			
		||||
    private final boolean pushDeleteNotificationToCore;
 | 
			
		||||
 | 
			
		||||
    RpcStatus(boolean pushDeleteNotificationToCore) {
 | 
			
		||||
        this.pushDeleteNotificationToCore = pushDeleteNotificationToCore;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@ -0,0 +1,47 @@
 | 
			
		||||
/**
 | 
			
		||||
 * Copyright © 2016-2023 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.common.data.rpc;
 | 
			
		||||
 | 
			
		||||
import org.junit.jupiter.api.Test;
 | 
			
		||||
 | 
			
		||||
import java.util.List;
 | 
			
		||||
 | 
			
		||||
import static org.assertj.core.api.Assertions.assertThat;
 | 
			
		||||
import static org.thingsboard.server.common.data.rpc.RpcStatus.DELIVERED;
 | 
			
		||||
import static org.thingsboard.server.common.data.rpc.RpcStatus.QUEUED;
 | 
			
		||||
import static org.thingsboard.server.common.data.rpc.RpcStatus.SENT;
 | 
			
		||||
 | 
			
		||||
class RpcStatusTest {
 | 
			
		||||
 | 
			
		||||
    private static final List<RpcStatus> pushDeleteNotificationToCoreStatuses = List.of(
 | 
			
		||||
            QUEUED,
 | 
			
		||||
            SENT,
 | 
			
		||||
            DELIVERED
 | 
			
		||||
    );
 | 
			
		||||
 | 
			
		||||
    @Test
 | 
			
		||||
    void isPushDeleteNotificationToCoreStatusTest() {
 | 
			
		||||
        var rpcStatuses = RpcStatus.values();
 | 
			
		||||
        for (var status : rpcStatuses) {
 | 
			
		||||
            if (pushDeleteNotificationToCoreStatuses.contains(status)) {
 | 
			
		||||
                assertThat(status.isPushDeleteNotificationToCore()).isTrue();
 | 
			
		||||
            } else {
 | 
			
		||||
                assertThat(status.isPushDeleteNotificationToCore()).isFalse();
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
}
 | 
			
		||||
@ -27,12 +27,12 @@ import java.util.concurrent.ScheduledFuture;
 | 
			
		||||
import java.util.concurrent.TimeUnit;
 | 
			
		||||
 | 
			
		||||
@Component
 | 
			
		||||
public class DefaultSchedulerComponent implements SchedulerComponent{
 | 
			
		||||
public class DefaultSchedulerComponent implements SchedulerComponent {
 | 
			
		||||
 | 
			
		||||
    protected ScheduledExecutorService schedulerExecutor;
 | 
			
		||||
 | 
			
		||||
    @PostConstruct
 | 
			
		||||
    public void init(){
 | 
			
		||||
    public void init() {
 | 
			
		||||
        this.schedulerExecutor = Executors.newSingleThreadScheduledExecutor(ThingsBoardThreadFactory.forName("queue-scheduler"));
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
		Loading…
	
	
			
			x
			
			
		
	
		Reference in New Issue
	
	Block a user