Browse Source

merged with master

# Conflicts:
#	common/data/src/main/java/org/thingsboard/server/common/data/notification/rule/NotificationRuleRecipientsConfig.java
pull/15196/head
dashevchenko 7 months ago
parent
commit
37a1ad77fc
  1. 2
      .gitignore
  2. 5
      application/src/main/data/upgrade/basic/schema_update.sql
  3. 3
      application/src/main/java/org/thingsboard/server/config/WebSocketConfiguration.java
  4. 28
      application/src/main/java/org/thingsboard/server/controller/plugin/TbWebSocketHandler.java
  5. 8
      application/src/main/java/org/thingsboard/server/service/edge/EdgeContextComponent.java
  6. 16
      application/src/main/java/org/thingsboard/server/service/edge/EdgeMsgConstructorUtils.java
  7. 40
      application/src/main/java/org/thingsboard/server/service/edge/rpc/EdgeGrpcService.java
  8. 11
      application/src/main/java/org/thingsboard/server/service/edge/rpc/EdgeGrpcSession.java
  9. 2
      application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/BaseEdgeProcessor.java
  10. 105
      application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/apikey/ApiKeyEdgeProcessor.java
  11. 28
      application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/apikey/ApiKeyProcessor.java
  12. 63
      application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/apikey/BaseApiKeyProcessor.java
  13. 7
      application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/user/UserEdgeProcessor.java
  14. 2
      application/src/main/java/org/thingsboard/server/service/security/auth/pat/ApiKeyAuthenticationProvider.java
  15. 23
      application/src/main/java/org/thingsboard/server/service/subscription/DefaultTbEntityDataSubscriptionService.java
  16. 4
      application/src/main/java/org/thingsboard/server/service/subscription/DefaultTbLocalSubscriptionService.java
  17. 4
      application/src/main/java/org/thingsboard/server/service/ttl/AbstractCleanUpService.java
  18. 75
      application/src/main/java/org/thingsboard/server/service/ttl/NotificationsCleanUpService.java
  19. 94
      application/src/main/java/org/thingsboard/server/service/ttl/rpc/RpcCleanUpService.java
  20. 3
      application/src/main/java/org/thingsboard/server/service/ws/AuthCmd.java
  21. 76
      application/src/main/resources/thingsboard.yml
  22. 11
      application/src/test/java/org/thingsboard/server/controller/AbstractControllerTest.java
  23. 9
      application/src/test/java/org/thingsboard/server/controller/AbstractWebTest.java
  24. 71
      application/src/test/java/org/thingsboard/server/controller/ApiKeyWebSocketApiTest.java
  25. 39
      application/src/test/java/org/thingsboard/server/controller/AuditLogControllerTest.java
  26. 4
      application/src/test/java/org/thingsboard/server/controller/CalculatedFieldControllerTest.java
  27. 12
      application/src/test/java/org/thingsboard/server/controller/EntityViewControllerTest.java
  28. 24
      application/src/test/java/org/thingsboard/server/controller/TbTestWebSocketClient.java
  29. 266
      application/src/test/java/org/thingsboard/server/controller/WebSocketApiTest.java
  30. 106
      application/src/test/java/org/thingsboard/server/controller/plugin/TbWebSocketHandlerTest.java
  31. 238
      application/src/test/java/org/thingsboard/server/edge/ApiKeyEdgeTest.java
  32. 3
      application/src/test/java/org/thingsboard/server/edge/CalculatedFieldEdgeTest.java
  33. 6
      application/src/test/java/org/thingsboard/server/edge/imitator/EdgeImitator.java
  34. 272
      application/src/test/java/org/thingsboard/server/service/edge/rpc/EdgeGrpcSslTest.java
  35. 8
      application/src/test/java/org/thingsboard/server/service/entitiy/EntityServiceTest.java
  36. 10
      application/src/test/java/org/thingsboard/server/service/housekeeper/HousekeeperServiceTest.java
  37. 144
      application/src/test/java/org/thingsboard/server/service/ttl/NotificationsCleanUpServiceTest.java
  38. 136
      application/src/test/java/org/thingsboard/server/service/ttl/rpc/RpcCleanUpServiceTest.java
  39. 11
      application/src/test/java/org/thingsboard/server/transport/lwm2m/client/FwLwM2MDevice.java
  40. 7
      application/src/test/java/org/thingsboard/server/transport/lwm2m/rpc/sql/RpcLwm2mIntegrationReadCollectedValueTest.java
  41. 1
      application/src/test/java/org/thingsboard/server/transport/mqtt/mqttv3/attributes/AbstractMqttAttributesIntegrationTest.java
  42. 2
      application/src/test/java/org/thingsboard/server/transport/mqtt/sparkplug/AbstractMqttV5ClientSparkplugTest.java
  43. 2
      application/src/test/java/org/thingsboard/server/transport/mqtt/sparkplug/attributes/AbstractMqttV5ClientSparkplugAttributesTest.java
  44. 1
      application/src/test/resources/application-test.properties
  45. 8
      common/dao-api/src/main/java/org/thingsboard/server/dao/pat/ApiKeyService.java
  46. 31
      common/data/src/main/java/org/thingsboard/server/common/data/cf/CalculatedField.java
  47. 3
      common/data/src/main/java/org/thingsboard/server/common/data/edge/EdgeEventType.java
  48. 4
      common/data/src/main/java/org/thingsboard/server/common/data/housekeeper/AlarmsDeletionHousekeeperTask.java
  49. 4
      common/data/src/main/java/org/thingsboard/server/common/data/housekeeper/AlarmsUnassignHousekeeperTask.java
  50. 4
      common/data/src/main/java/org/thingsboard/server/common/data/housekeeper/EntitiesDeletionHousekeeperTask.java
  51. 4
      common/data/src/main/java/org/thingsboard/server/common/data/housekeeper/HousekeeperTask.java
  52. 5
      common/data/src/main/java/org/thingsboard/server/common/data/housekeeper/LatestTsDeletionHousekeeperTask.java
  53. 5
      common/data/src/main/java/org/thingsboard/server/common/data/housekeeper/TenantEntitiesDeletionHousekeeperTask.java
  54. 5
      common/data/src/main/java/org/thingsboard/server/common/data/housekeeper/TsHistoryDeletionHousekeeperTask.java
  55. 1
      common/data/src/main/java/org/thingsboard/server/common/data/id/EntityIdFactory.java
  56. 5
      common/data/src/main/java/org/thingsboard/server/common/data/kv/TsKvEntry.java
  57. 2
      common/data/src/main/java/org/thingsboard/server/common/data/notification/rule/NotificationRuleRecipientsConfig.java
  58. 11
      common/edge-api/src/main/proto/edge.proto
  59. 18
      common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/server/store/TbLwM2mRedisRegistrationStore.java
  60. 14
      common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/server/store/TbRedisLwM2MClientStore.java
  61. 18
      common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/server/store/TbRedisLwM2MModelConfigStore.java
  62. 137
      common/transport/lwm2m/src/test/java/org/thingsboard/server/transport/lwm2m/server/store/TbRedisLwM2MClientStoreTest.java
  63. 22
      common/transport/mqtt/src/main/java/org/thingsboard/server/transport/mqtt/MqttTransportContext.java
  64. 31
      common/transport/mqtt/src/main/java/org/thingsboard/server/transport/mqtt/MqttTransportHandler.java
  65. 2
      common/transport/transport-api/src/main/java/org/thingsboard/server/common/transport/TransportService.java
  66. 18
      common/transport/transport-api/src/main/java/org/thingsboard/server/common/transport/service/DefaultTransportService.java
  67. 1
      dao/pom.xml
  68. 1
      dao/src/main/java/org/thingsboard/server/dao/model/ModelConstants.java
  69. 11
      dao/src/main/java/org/thingsboard/server/dao/model/sql/CalculatedFieldEntity.java
  70. 2
      dao/src/main/java/org/thingsboard/server/dao/notification/NotificationRequestDao.java
  71. 58
      dao/src/main/java/org/thingsboard/server/dao/ota/BaseOtaPackageService.java
  72. 7
      dao/src/main/java/org/thingsboard/server/dao/ota/OtaPackageDao.java
  73. 7
      dao/src/main/java/org/thingsboard/server/dao/pat/ApiKeyDao.java
  74. 44
      dao/src/main/java/org/thingsboard/server/dao/pat/ApiKeyServiceImpl.java
  75. 3
      dao/src/main/java/org/thingsboard/server/dao/rpc/RpcDao.java
  76. 2
      dao/src/main/java/org/thingsboard/server/dao/sql/cf/DefaultNativeCalculatedFieldRepository.java
  77. 4
      dao/src/main/java/org/thingsboard/server/dao/sql/notification/JpaNotificationRequestDao.java
  78. 8
      dao/src/main/java/org/thingsboard/server/dao/sql/notification/NotificationRequestRepository.java
  79. 10
      dao/src/main/java/org/thingsboard/server/dao/sql/ota/JpaOtaPackageDao.java
  80. 10
      dao/src/main/java/org/thingsboard/server/dao/sql/ota/OtaPackageRepository.java
  81. 7
      dao/src/main/java/org/thingsboard/server/dao/sql/pat/ApiKeyRepository.java
  82. 13
      dao/src/main/java/org/thingsboard/server/dao/sql/pat/JpaApiKeyDao.java
  83. 4
      dao/src/main/java/org/thingsboard/server/dao/sql/rpc/JpaRpcDao.java
  84. 11
      dao/src/main/java/org/thingsboard/server/dao/sql/rpc/RpcRepository.java
  85. 1
      dao/src/main/resources/sql/schema-entities.sql
  86. 4
      dao/src/test/java/org/thingsboard/server/dao/service/CalculatedFieldServiceTest.java
  87. 52
      dao/src/test/java/org/thingsboard/server/dao/service/OtaPackageServiceTest.java
  88. 2
      dao/src/test/java/org/thingsboard/server/dao/service/timeseries/nosql/TimeseriesServiceNoSqlTest.java
  89. 133
      dao/src/test/java/org/thingsboard/server/dao/sql/notification/JpaNotificationRequestDaoTest.java
  90. 7
      dao/src/test/java/org/thingsboard/server/dao/sql/rpc/JpaRpcDaoTest.java
  91. 458
      docker/monitoring/grafana/provisioning/dashboards/transport_connections.json
  92. 14
      edqs/src/main/resources/edqs.yml
  93. 64
      msa/js-executor/config/default.yml
  94. 24
      msa/vc-executor/src/main/resources/tb-vc-executor.yml
  95. 1
      netty-mqtt/pom.xml
  96. 6
      pom.xml
  97. 30
      transport/coap/src/main/resources/tb-coap-transport.yml
  98. 30
      transport/http/src/main/resources/tb-http-transport.yml
  99. 30
      transport/lwm2m/src/main/resources/tb-lwm2m-transport.yml
  100. 30
      transport/mqtt/src/main/resources/tb-mqtt-transport.yml

2
.gitignore

@ -37,4 +37,4 @@ rebuild-docker.sh
*/.run/** */.run/**
.run/** .run/**
.run .run
.claude/ .claude

5
application/src/main/data/upgrade/basic/schema_update.sql

@ -14,3 +14,8 @@
-- limitations under the License. -- limitations under the License.
-- --
-- CALCULATED FIELD ADDITIONAL INFO ADDITION START
ALTER TABLE calculated_field ADD COLUMN IF NOT EXISTS additional_info varchar;
-- CALCULATED FIELD ADDITIONAL INFO ADDITION END

3
application/src/main/java/org/thingsboard/server/config/WebSocketConfiguration.java

@ -60,7 +60,8 @@ public class WebSocketConfiguration implements WebSocketConfigurer {
log.error("TbWebSocketHandler expected but [{}] provided", wsHandler); log.error("TbWebSocketHandler expected but [{}] provided", wsHandler);
throw new RuntimeException("TbWebSocketHandler expected but " + wsHandler + " provided"); throw new RuntimeException("TbWebSocketHandler expected but " + wsHandler + " provided");
} }
registry.addHandler(wsHandler, WS_API_MAPPING).setAllowedOriginPatterns("*"); registry.addHandler(wsHandler, WS_API_MAPPING)
.setAllowedOriginPatterns("*");
} }
} }

28
application/src/main/java/org/thingsboard/server/controller/plugin/TbWebSocketHandler.java

@ -52,6 +52,7 @@ import org.thingsboard.server.config.WebSocketConfiguration;
import org.thingsboard.server.dao.tenant.TbTenantProfileCache; import org.thingsboard.server.dao.tenant.TbTenantProfileCache;
import org.thingsboard.server.queue.util.TbCoreComponent; import org.thingsboard.server.queue.util.TbCoreComponent;
import org.thingsboard.server.service.security.auth.jwt.JwtAuthenticationProvider; import org.thingsboard.server.service.security.auth.jwt.JwtAuthenticationProvider;
import org.thingsboard.server.service.security.auth.pat.ApiKeyAuthenticationProvider;
import org.thingsboard.server.service.security.exception.JwtExpiredTokenException; import org.thingsboard.server.service.security.exception.JwtExpiredTokenException;
import org.thingsboard.server.service.security.model.SecurityUser; import org.thingsboard.server.service.security.model.SecurityUser;
import org.thingsboard.server.service.security.model.UserPrincipal; import org.thingsboard.server.service.security.model.UserPrincipal;
@ -100,6 +101,8 @@ public class TbWebSocketHandler extends TextWebSocketHandler implements WebSocke
private RateLimitService rateLimitService; private RateLimitService rateLimitService;
@Autowired @Autowired
private JwtAuthenticationProvider authenticationProvider; private JwtAuthenticationProvider authenticationProvider;
@Autowired
private ApiKeyAuthenticationProvider apiKeyAuthenticationProvider;
@Value("${server.ws.send_timeout:5000}") @Value("${server.ws.send_timeout:5000}")
private long sendTimeout; private long sendTimeout;
@ -194,7 +197,11 @@ public class TbWebSocketHandler extends TextWebSocketHandler implements WebSocke
log.trace("{} Authenticating session", sessionRef); log.trace("{} Authenticating session", sessionRef);
SecurityUser securityCtx; SecurityUser securityCtx;
try { try {
securityCtx = authenticationProvider.authenticate(authCmd.getToken()); if (StringUtils.isNotEmpty(authCmd.getApiKey())) {
securityCtx = apiKeyAuthenticationProvider.authenticate(authCmd.getApiKey());
} else {
securityCtx = authenticationProvider.authenticate(authCmd.getToken());
}
} catch (Exception e) { } catch (Exception e) {
close(sessionRef, CloseStatus.BAD_DATA.withReason(e.getMessage())); close(sessionRef, CloseStatus.BAD_DATA.withReason(e.getMessage()));
return; return;
@ -328,9 +335,12 @@ public class TbWebSocketHandler extends TextWebSocketHandler implements WebSocke
} }
SecurityUser securityCtx = null; SecurityUser securityCtx = null;
String token = StringUtils.substringAfter(session.getUri().getQuery(), "token="); String query = session.getUri().getQuery();
if (StringUtils.isNotEmpty(token)) { if (query != null) {
securityCtx = authenticationProvider.authenticate(token); String token = extractQueryParam(query, "token");
if (StringUtils.isNotEmpty(token)) {
securityCtx = authenticationProvider.authenticate(token);
}
} }
return WebSocketSessionRef.builder() return WebSocketSessionRef.builder()
.sessionId(UUID.randomUUID().toString()) .sessionId(UUID.randomUUID().toString())
@ -341,6 +351,15 @@ public class TbWebSocketHandler extends TextWebSocketHandler implements WebSocke
.build(); .build();
} }
private String extractQueryParam(String query, String paramName) {
for (String param : query.split("&")) {
if (param.startsWith(paramName + "=")) {
return param.substring(paramName.length() + 1);
}
}
return null;
}
private SessionMetaData getSessionMd(String internalSessionId) { private SessionMetaData getSessionMd(String internalSessionId) {
SessionMetaData sessionMd = internalSessionMap.get(internalSessionId); SessionMetaData sessionMd = internalSessionMap.get(internalSessionId);
if (sessionMd == null) { if (sessionMd == null) {
@ -482,6 +501,7 @@ public class TbWebSocketHandler extends TextWebSocketHandler implements WebSocke
} }
} }
} }
} }
@Override @Override

8
application/src/main/java/org/thingsboard/server/service/edge/EdgeContextComponent.java

@ -25,6 +25,7 @@ import org.thingsboard.server.cluster.TbClusterService;
import org.thingsboard.server.common.data.edge.EdgeEventType; import org.thingsboard.server.common.data.edge.EdgeEventType;
import org.thingsboard.server.common.msg.notification.NotificationRuleProcessor; import org.thingsboard.server.common.msg.notification.NotificationRuleProcessor;
import org.thingsboard.server.dao.ai.AiModelService; import org.thingsboard.server.dao.ai.AiModelService;
import org.thingsboard.server.dao.pat.ApiKeyService;
import org.thingsboard.server.dao.alarm.AlarmCommentService; import org.thingsboard.server.dao.alarm.AlarmCommentService;
import org.thingsboard.server.dao.alarm.AlarmService; import org.thingsboard.server.dao.alarm.AlarmService;
import org.thingsboard.server.dao.asset.AssetProfileService; import org.thingsboard.server.dao.asset.AssetProfileService;
@ -61,6 +62,7 @@ import org.thingsboard.server.service.edge.rpc.EdgeEventStorageSettings;
import org.thingsboard.server.service.edge.rpc.EdgeRpcService; import org.thingsboard.server.service.edge.rpc.EdgeRpcService;
import org.thingsboard.server.service.edge.rpc.processor.EdgeProcessor; import org.thingsboard.server.service.edge.rpc.processor.EdgeProcessor;
import org.thingsboard.server.service.edge.rpc.processor.ai.AiModelProcessor; import org.thingsboard.server.service.edge.rpc.processor.ai.AiModelProcessor;
import org.thingsboard.server.service.edge.rpc.processor.apikey.ApiKeyProcessor;
import org.thingsboard.server.service.edge.rpc.processor.alarm.AlarmProcessor; import org.thingsboard.server.service.edge.rpc.processor.alarm.AlarmProcessor;
import org.thingsboard.server.service.edge.rpc.processor.alarm.comment.AlarmCommentProcessor; import org.thingsboard.server.service.edge.rpc.processor.alarm.comment.AlarmCommentProcessor;
import org.thingsboard.server.service.edge.rpc.processor.asset.AssetEdgeProcessor; import org.thingsboard.server.service.edge.rpc.processor.asset.AssetEdgeProcessor;
@ -273,6 +275,12 @@ public class EdgeContextComponent {
@Autowired @Autowired
private AiModelProcessor aiModelProcessor; private AiModelProcessor aiModelProcessor;
@Autowired
private ApiKeyService apiKeyService;
@Autowired
private ApiKeyProcessor apiKeyProcessor;
@Autowired @Autowired
private UserProcessor userProcessor; private UserProcessor userProcessor;

16
application/src/main/java/org/thingsboard/server/service/edge/EdgeMsgConstructorUtils.java

@ -49,6 +49,7 @@ import org.thingsboard.server.common.data.Tenant;
import org.thingsboard.server.common.data.TenantProfile; import org.thingsboard.server.common.data.TenantProfile;
import org.thingsboard.server.common.data.User; import org.thingsboard.server.common.data.User;
import org.thingsboard.server.common.data.ai.AiModel; import org.thingsboard.server.common.data.ai.AiModel;
import org.thingsboard.server.common.data.pat.ApiKey;
import org.thingsboard.server.common.data.alarm.Alarm; import org.thingsboard.server.common.data.alarm.Alarm;
import org.thingsboard.server.common.data.alarm.AlarmComment; import org.thingsboard.server.common.data.alarm.AlarmComment;
import org.thingsboard.server.common.data.asset.Asset; import org.thingsboard.server.common.data.asset.Asset;
@ -61,6 +62,7 @@ import org.thingsboard.server.common.data.edge.Edge;
import org.thingsboard.server.common.data.edge.EdgeEvent; import org.thingsboard.server.common.data.edge.EdgeEvent;
import org.thingsboard.server.common.data.edge.EdgeEventActionType; import org.thingsboard.server.common.data.edge.EdgeEventActionType;
import org.thingsboard.server.common.data.id.AiModelId; import org.thingsboard.server.common.data.id.AiModelId;
import org.thingsboard.server.common.data.id.ApiKeyId;
import org.thingsboard.server.common.data.id.AssetId; import org.thingsboard.server.common.data.id.AssetId;
import org.thingsboard.server.common.data.id.AssetProfileId; import org.thingsboard.server.common.data.id.AssetProfileId;
import org.thingsboard.server.common.data.id.CalculatedFieldId; import org.thingsboard.server.common.data.id.CalculatedFieldId;
@ -98,6 +100,7 @@ import org.thingsboard.server.common.data.widget.WidgetTypeDetails;
import org.thingsboard.server.common.data.widget.WidgetsBundle; import org.thingsboard.server.common.data.widget.WidgetsBundle;
import org.thingsboard.server.common.transport.util.JsonUtils; import org.thingsboard.server.common.transport.util.JsonUtils;
import org.thingsboard.server.gen.edge.v1.AiModelUpdateMsg; import org.thingsboard.server.gen.edge.v1.AiModelUpdateMsg;
import org.thingsboard.server.gen.edge.v1.ApiKeyUpdateMsg;
import org.thingsboard.server.gen.edge.v1.AlarmCommentUpdateMsg; import org.thingsboard.server.gen.edge.v1.AlarmCommentUpdateMsg;
import org.thingsboard.server.gen.edge.v1.AlarmUpdateMsg; import org.thingsboard.server.gen.edge.v1.AlarmUpdateMsg;
import org.thingsboard.server.gen.edge.v1.AssetProfileUpdateMsg; import org.thingsboard.server.gen.edge.v1.AssetProfileUpdateMsg;
@ -741,6 +744,19 @@ public class EdgeMsgConstructorUtils {
.setIdLSB(aiModelId.getId().getLeastSignificantBits()).build(); .setIdLSB(aiModelId.getId().getLeastSignificantBits()).build();
} }
public static ApiKeyUpdateMsg constructApiKeyUpdatedMsg(UpdateMsgType msgType, ApiKey apiKey) {
return ApiKeyUpdateMsg.newBuilder().setMsgType(msgType).setEntity(JacksonUtil.toString(apiKey))
.setIdMSB(apiKey.getId().getId().getMostSignificantBits())
.setIdLSB(apiKey.getId().getId().getLeastSignificantBits()).build();
}
public static ApiKeyUpdateMsg constructApiKeyDeleteMsg(ApiKeyId apiKeyId) {
return ApiKeyUpdateMsg.newBuilder()
.setMsgType(UpdateMsgType.ENTITY_DELETED_RPC_MESSAGE)
.setIdMSB(apiKeyId.getId().getMostSignificantBits())
.setIdLSB(apiKeyId.getId().getLeastSignificantBits()).build();
}
public static List<EdgeEvent> mergeAndFilterDownlinkDuplicates(List<EdgeEvent> edgeEvents) { public static List<EdgeEvent> mergeAndFilterDownlinkDuplicates(List<EdgeEvent> edgeEvents) {
try { try {
edgeEvents = removeDownlinkDuplicates(edgeEvents); edgeEvents = removeDownlinkDuplicates(edgeEvents);

40
application/src/main/java/org/thingsboard/server/service/edge/rpc/EdgeGrpcService.java

@ -19,7 +19,10 @@ import com.fasterxml.jackson.databind.node.ObjectNode;
import com.google.common.util.concurrent.FutureCallback; import com.google.common.util.concurrent.FutureCallback;
import com.google.common.util.concurrent.Futures; import com.google.common.util.concurrent.Futures;
import io.grpc.Server; import io.grpc.Server;
import io.grpc.netty.shaded.io.grpc.netty.GrpcSslContexts;
import io.grpc.netty.shaded.io.grpc.netty.NettyServerBuilder; import io.grpc.netty.shaded.io.grpc.netty.NettyServerBuilder;
import io.grpc.netty.shaded.io.netty.handler.ssl.SslContext;
import io.grpc.netty.shaded.io.netty.handler.ssl.SslContextBuilder;
import io.grpc.stub.StreamObserver; import io.grpc.stub.StreamObserver;
import jakarta.annotation.Nullable; import jakarta.annotation.Nullable;
import jakarta.annotation.PreDestroy; import jakarta.annotation.PreDestroy;
@ -37,7 +40,8 @@ import org.thingsboard.server.cache.TbTransactionalCache;
import org.thingsboard.server.cluster.TbClusterService; import org.thingsboard.server.cluster.TbClusterService;
import org.thingsboard.server.common.data.AttributeScope; import org.thingsboard.server.common.data.AttributeScope;
import org.thingsboard.server.common.data.DataConstants; import org.thingsboard.server.common.data.DataConstants;
import org.thingsboard.server.common.data.ResourceUtils; import org.thingsboard.server.common.data.StringUtils;
import org.thingsboard.server.common.transport.config.ssl.PemSslCredentials;
import org.thingsboard.server.common.data.edge.Edge; import org.thingsboard.server.common.data.edge.Edge;
import org.thingsboard.server.common.data.edge.EdgeEvent; import org.thingsboard.server.common.data.edge.EdgeEvent;
import org.thingsboard.server.common.data.id.EdgeId; import org.thingsboard.server.common.data.id.EdgeId;
@ -67,7 +71,6 @@ import org.thingsboard.server.service.edge.EdgeContextComponent;
import org.thingsboard.server.service.telemetry.TelemetrySubscriptionService; import org.thingsboard.server.service.telemetry.TelemetrySubscriptionService;
import java.io.IOException; import java.io.IOException;
import java.io.InputStream;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.Collection; import java.util.Collection;
import java.util.HashMap; import java.util.HashMap;
@ -112,6 +115,8 @@ public class EdgeGrpcService extends EdgeRpcServiceGrpc.EdgeRpcServiceImplBase i
private String certFileResource; private String certFileResource;
@Value("${edges.rpc.ssl.private_key}") @Value("${edges.rpc.ssl.private_key}")
private String privateKeyResource; private String privateKeyResource;
@Value("${edges.rpc.ssl.key_password:}")
private String keyPassword;
@Value("${edges.state.persistToTelemetry:false}") @Value("${edges.state.persistToTelemetry:false}")
private boolean persistToTelemetry; private boolean persistToTelemetry;
@Value("${edges.rpc.client_max_keep_alive_time_sec:1}") @Value("${edges.rpc.client_max_keep_alive_time_sec:1}")
@ -173,9 +178,7 @@ public class EdgeGrpcService extends EdgeRpcServiceGrpc.EdgeRpcServiceImplBase i
.addService(this); .addService(this);
if (sslEnabled) { if (sslEnabled) {
try { try {
InputStream certFileIs = ResourceUtils.getInputStream(this, certFileResource); setupSsl(builder);
InputStream privateKeyFileIs = ResourceUtils.getInputStream(this, privateKeyResource);
builder.useTransportSecurity(certFileIs, privateKeyFileIs);
} catch (Exception e) { } catch (Exception e) {
log.error("Unable to set up SSL context. Reason: " + e.getMessage(), e); log.error("Unable to set up SSL context. Reason: " + e.getMessage(), e);
throw new RuntimeException("Unable to set up SSL context!", e); throw new RuntimeException("Unable to set up SSL context!", e);
@ -196,6 +199,33 @@ public class EdgeGrpcService extends EdgeRpcServiceGrpc.EdgeRpcServiceImplBase i
log.info("Edge RPC service initialized!"); log.info("Edge RPC service initialized!");
} }
/**
* Configures TLS for the Edge gRPC server.
* <p>
* Delegates PEM parsing and key management to {@link PemSslCredentials} — the same
* class used by MQTT, CoAP, and LwM2M transports — which supports:
* <ul>
* <li>Separate certificate and private key files (classic two-file setup)</li>
* <li>Combined PEM: certificate chain + private key in a single {@code cert} file
* ({@code private_key} left empty)</li>
* <li>Encrypted private keys (password supplied via {@code key_password})</li>
* </ul>
* Path resolution (for both {@code cert} and {@code private_key}) is handled by
* {@link org.thingsboard.server.common.data.ResourceUtils#getInputStream ResourceUtils}:
* absolute path → relative / working-dir → classpath → {@code classpath:} prefix.
*/
void setupSsl(NettyServerBuilder builder) throws Exception {
PemSslCredentials credentials = new PemSslCredentials();
credentials.setCertFile(certFileResource);
credentials.setKeyFile(StringUtils.isEmpty(privateKeyResource) ? null : privateKeyResource);
credentials.setKeyPassword(keyPassword);
credentials.init(false);
SslContext sslContext = GrpcSslContexts.configure(
SslContextBuilder.forServer(credentials.createKeyManagerFactory())).build();
builder.sslContext(sslContext);
}
@PreDestroy @PreDestroy
public void destroy() { public void destroy() {
if (server != null) { if (server != null) {

11
application/src/main/java/org/thingsboard/server/service/edge/rpc/EdgeGrpcSession.java

@ -49,6 +49,7 @@ import org.thingsboard.server.common.data.page.TimePageLink;
import org.thingsboard.server.common.msg.edge.EdgeEventUpdateMsg; import org.thingsboard.server.common.msg.edge.EdgeEventUpdateMsg;
import org.thingsboard.server.dao.edge.stats.EdgeStatsKey; import org.thingsboard.server.dao.edge.stats.EdgeStatsKey;
import org.thingsboard.server.gen.edge.v1.AiModelUpdateMsg; import org.thingsboard.server.gen.edge.v1.AiModelUpdateMsg;
import org.thingsboard.server.gen.edge.v1.ApiKeyUpdateMsg;
import org.thingsboard.server.gen.edge.v1.AlarmCommentUpdateMsg; import org.thingsboard.server.gen.edge.v1.AlarmCommentUpdateMsg;
import org.thingsboard.server.gen.edge.v1.AlarmUpdateMsg; import org.thingsboard.server.gen.edge.v1.AlarmUpdateMsg;
import org.thingsboard.server.gen.edge.v1.AssetProfileUpdateMsg; import org.thingsboard.server.gen.edge.v1.AssetProfileUpdateMsg;
@ -988,6 +989,16 @@ public abstract class EdgeGrpcSession implements Closeable {
} }
} }
} }
if (uplinkMsg.getApiKeyUpdateMsgCount() > 0) {
for (ApiKeyUpdateMsg apiKeyUpdateMsg : uplinkMsg.getApiKeyUpdateMsgList()) {
sequenceDependencyLock.lock();
try {
result.add(ctx.getApiKeyProcessor().processApiKeyMsgFromEdge(edge.getTenantId(), edge, apiKeyUpdateMsg));
} finally {
sequenceDependencyLock.unlock();
}
}
}
} catch (Exception e) { } catch (Exception e) {
String failureMsg = String.format("Can't process uplink msg [%s] from edge", uplinkMsg); String failureMsg = String.format("Can't process uplink msg [%s] from edge", uplinkMsg);
log.trace("[{}][{}] Can't process uplink msg [{}]", tenantId, edge.getId(), uplinkMsg, e); log.trace("[{}][{}] Can't process uplink msg [{}]", tenantId, edge.getId(), uplinkMsg, e);

2
application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/BaseEdgeProcessor.java

@ -146,7 +146,7 @@ public abstract class BaseEdgeProcessor implements EdgeProcessor {
UPDATED_COMMENT, DELETED -> true; UPDATED_COMMENT, DELETED -> true;
default -> switch (type) { default -> switch (type) {
case ALARM, ALARM_COMMENT, RULE_CHAIN, RULE_CHAIN_METADATA, USER, CUSTOMER, TENANT, TENANT_PROFILE, case ALARM, ALARM_COMMENT, RULE_CHAIN, RULE_CHAIN_METADATA, USER, CUSTOMER, TENANT, TENANT_PROFILE,
WIDGETS_BUNDLE, WIDGET_TYPE, ADMIN_SETTINGS, OTA_PACKAGE, QUEUE, RELATION, CALCULATED_FIELD, AI_MODEL, NOTIFICATION_TEMPLATE, WIDGETS_BUNDLE, WIDGET_TYPE, ADMIN_SETTINGS, OTA_PACKAGE, QUEUE, RELATION, CALCULATED_FIELD, AI_MODEL, API_KEY, NOTIFICATION_TEMPLATE,
NOTIFICATION_TARGET, NOTIFICATION_RULE -> true; NOTIFICATION_TARGET, NOTIFICATION_RULE -> true;
default -> false; default -> false;
}; };

105
application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/apikey/ApiKeyEdgeProcessor.java

@ -0,0 +1,105 @@
/**
* 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.service.edge.rpc.processor.apikey;
import com.google.common.util.concurrent.Futures;
import com.google.common.util.concurrent.ListenableFuture;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Component;
import org.thingsboard.server.common.data.EdgeUtils;
import org.thingsboard.server.common.data.edge.Edge;
import org.thingsboard.server.common.data.edge.EdgeEvent;
import org.thingsboard.server.common.data.edge.EdgeEventType;
import org.thingsboard.server.common.data.id.ApiKeyId;
import org.thingsboard.server.common.data.id.TenantId;
import org.thingsboard.server.common.data.msg.TbMsgType;
import org.thingsboard.server.common.data.pat.ApiKey;
import org.thingsboard.server.exception.DataValidationException;
import org.thingsboard.server.gen.edge.v1.ApiKeyUpdateMsg;
import org.thingsboard.server.gen.edge.v1.DownlinkMsg;
import org.thingsboard.server.gen.edge.v1.EdgeVersion;
import org.thingsboard.server.gen.edge.v1.UpdateMsgType;
import org.thingsboard.server.queue.util.TbCoreComponent;
import org.thingsboard.server.service.edge.EdgeMsgConstructorUtils;
import java.util.UUID;
@Slf4j
@Component
@TbCoreComponent
public class ApiKeyEdgeProcessor extends BaseApiKeyProcessor implements ApiKeyProcessor {
@Override
public ListenableFuture<Void> processApiKeyMsgFromEdge(TenantId tenantId, Edge edge, ApiKeyUpdateMsg apiKeyUpdateMsg) {
ApiKeyId apiKeyId = new ApiKeyId(new UUID(apiKeyUpdateMsg.getIdMSB(), apiKeyUpdateMsg.getIdLSB()));
try {
edgeSynchronizationManager.getEdgeId().set(edge.getId());
return switch (apiKeyUpdateMsg.getMsgType()) {
case ENTITY_CREATED_RPC_MESSAGE, ENTITY_UPDATED_RPC_MESSAGE -> {
boolean created = saveOrUpdateApiKey(tenantId, apiKeyId, apiKeyUpdateMsg);
if (created) {
ApiKey apiKey = edgeCtx.getApiKeyService().findApiKeyById(tenantId, apiKeyId);
if (apiKey != null) {
pushEntityEventToRuleEngine(tenantId, edge, apiKey, TbMsgType.ENTITY_CREATED);
}
}
yield Futures.immediateFuture(null);
}
case ENTITY_DELETED_RPC_MESSAGE -> {
deleteApiKey(tenantId, edge, apiKeyId);
yield Futures.immediateFuture(null);
}
default -> handleUnsupportedMsgType(apiKeyUpdateMsg.getMsgType());
};
} catch (DataValidationException e) {
return Futures.immediateFailedFuture(e);
} finally {
edgeSynchronizationManager.getEdgeId().remove();
}
}
@Override
public DownlinkMsg convertEdgeEventToDownlink(EdgeEvent edgeEvent, EdgeVersion edgeVersion) {
ApiKeyId apiKeyId = new ApiKeyId(edgeEvent.getEntityId());
switch (edgeEvent.getAction()) {
case ADDED, UPDATED -> {
ApiKey apiKey = edgeCtx.getApiKeyService().findApiKeyById(edgeEvent.getTenantId(), apiKeyId);
if (apiKey != null) {
UpdateMsgType msgType = getUpdateMsgType(edgeEvent.getAction());
ApiKeyUpdateMsg apiKeyUpdateMsg = EdgeMsgConstructorUtils.constructApiKeyUpdatedMsg(msgType, apiKey);
return DownlinkMsg.newBuilder()
.setDownlinkMsgId(EdgeUtils.nextPositiveInt())
.addApiKeyUpdateMsg(apiKeyUpdateMsg)
.build();
}
}
case DELETED -> {
ApiKeyUpdateMsg apiKeyUpdateMsg = EdgeMsgConstructorUtils.constructApiKeyDeleteMsg(apiKeyId);
return DownlinkMsg.newBuilder()
.setDownlinkMsgId(EdgeUtils.nextPositiveInt())
.addApiKeyUpdateMsg(apiKeyUpdateMsg)
.build();
}
}
return null;
}
@Override
public EdgeEventType getEdgeEventType() {
return EdgeEventType.API_KEY;
}
}

28
application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/apikey/ApiKeyProcessor.java

@ -0,0 +1,28 @@
/**
* 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.service.edge.rpc.processor.apikey;
import com.google.common.util.concurrent.ListenableFuture;
import org.thingsboard.server.common.data.edge.Edge;
import org.thingsboard.server.common.data.id.TenantId;
import org.thingsboard.server.gen.edge.v1.ApiKeyUpdateMsg;
import org.thingsboard.server.service.edge.rpc.processor.EdgeProcessor;
public interface ApiKeyProcessor extends EdgeProcessor {
ListenableFuture<Void> processApiKeyMsgFromEdge(TenantId tenantId, Edge edge, ApiKeyUpdateMsg apiKeyUpdateMsg);
}

63
application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/apikey/BaseApiKeyProcessor.java

@ -0,0 +1,63 @@
/**
* 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.service.edge.rpc.processor.apikey;
import com.datastax.oss.driver.api.core.uuid.Uuids;
import lombok.extern.slf4j.Slf4j;
import org.thingsboard.common.util.JacksonUtil;
import org.thingsboard.server.common.data.edge.Edge;
import org.thingsboard.server.common.data.id.ApiKeyId;
import org.thingsboard.server.common.data.id.TenantId;
import org.thingsboard.server.common.data.msg.TbMsgType;
import org.thingsboard.server.common.data.pat.ApiKey;
import org.thingsboard.server.gen.edge.v1.ApiKeyUpdateMsg;
import org.thingsboard.server.service.edge.rpc.processor.BaseEdgeProcessor;
@Slf4j
public abstract class BaseApiKeyProcessor extends BaseEdgeProcessor {
protected boolean saveOrUpdateApiKey(TenantId tenantId, ApiKeyId apiKeyId, ApiKeyUpdateMsg apiKeyUpdateMsg) {
boolean isCreated = false;
try {
ApiKey apiKey = JacksonUtil.fromString(apiKeyUpdateMsg.getEntity(), ApiKey.class, true);
if (apiKey == null) {
throw new RuntimeException("[{" + tenantId + "}] apiKeyUpdateMsg {" + apiKeyUpdateMsg + " } cannot be converted to apiKey");
}
ApiKey existingApiKey = edgeCtx.getApiKeyService().findApiKeyById(tenantId, apiKeyId);
if (existingApiKey == null) {
apiKey.setCreatedTime(Uuids.unixTimestamp(apiKeyId.getId()));
isCreated = true;
}
apiKey.setId(apiKeyId);
edgeCtx.getApiKeyService().saveApiKey(tenantId, apiKey, apiKey.getValue(), false);
} catch (Exception e) {
log.error("[{}] Failed to process apiKey update msg [{}]", tenantId, apiKeyUpdateMsg, e);
throw e;
}
return isCreated;
}
protected void deleteApiKey(TenantId tenantId, Edge edge, ApiKeyId apiKeyId) {
ApiKey apiKey = edgeCtx.getApiKeyService().findApiKeyById(tenantId, apiKeyId);
if (apiKey != null) {
edgeCtx.getApiKeyService().deleteApiKey(tenantId, apiKey, false);
pushEntityEventToRuleEngine(tenantId, edge, apiKey, TbMsgType.ENTITY_DELETED);
}
}
}

7
application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/user/UserEdgeProcessor.java

@ -23,6 +23,7 @@ import org.springframework.stereotype.Component;
import org.thingsboard.common.util.JacksonUtil; import org.thingsboard.common.util.JacksonUtil;
import org.thingsboard.server.common.data.EdgeUtils; import org.thingsboard.server.common.data.EdgeUtils;
import org.thingsboard.server.common.data.User; import org.thingsboard.server.common.data.User;
import org.thingsboard.server.common.data.pat.ApiKey;
import org.thingsboard.server.common.data.edge.Edge; import org.thingsboard.server.common.data.edge.Edge;
import org.thingsboard.server.common.data.edge.EdgeEvent; import org.thingsboard.server.common.data.edge.EdgeEvent;
import org.thingsboard.server.common.data.edge.EdgeEventActionType; import org.thingsboard.server.common.data.edge.EdgeEventActionType;
@ -34,6 +35,7 @@ import org.thingsboard.server.common.data.msg.TbMsgType;
import org.thingsboard.server.common.data.security.UserCredentials; import org.thingsboard.server.common.data.security.UserCredentials;
import org.thingsboard.server.common.msg.TbMsgMetaData; import org.thingsboard.server.common.msg.TbMsgMetaData;
import org.thingsboard.server.exception.DataValidationException; import org.thingsboard.server.exception.DataValidationException;
import org.thingsboard.server.gen.edge.v1.ApiKeyUpdateMsg;
import org.thingsboard.server.gen.edge.v1.DownlinkMsg; import org.thingsboard.server.gen.edge.v1.DownlinkMsg;
import org.thingsboard.server.gen.edge.v1.EdgeVersion; import org.thingsboard.server.gen.edge.v1.EdgeVersion;
import org.thingsboard.server.gen.edge.v1.UpdateMsgType; import org.thingsboard.server.gen.edge.v1.UpdateMsgType;
@ -42,6 +44,7 @@ import org.thingsboard.server.gen.edge.v1.UserUpdateMsg;
import org.thingsboard.server.queue.util.TbCoreComponent; import org.thingsboard.server.queue.util.TbCoreComponent;
import org.thingsboard.server.service.edge.EdgeMsgConstructorUtils; import org.thingsboard.server.service.edge.EdgeMsgConstructorUtils;
import java.util.List;
import java.util.UUID; import java.util.UUID;
@Slf4j @Slf4j
@ -135,6 +138,10 @@ public class UserEdgeProcessor extends BaseUserProcessor implements UserProcesso
if (userCredentialsByUserId != null) { if (userCredentialsByUserId != null) {
builder.addUserCredentialsUpdateMsg(EdgeMsgConstructorUtils.constructUserCredentialsUpdatedMsg(userCredentialsByUserId)); builder.addUserCredentialsUpdateMsg(EdgeMsgConstructorUtils.constructUserCredentialsUpdatedMsg(userCredentialsByUserId));
} }
List<ApiKey> apiKeys = edgeCtx.getApiKeyService().findApiKeysByUserId(edgeEvent.getTenantId(), userId);
for (ApiKey apiKey : apiKeys) {
builder.addApiKeyUpdateMsg(EdgeMsgConstructorUtils.constructApiKeyUpdatedMsg(msgType, apiKey));
}
return builder.build(); return builder.build();
} }
} }

2
application/src/main/java/org/thingsboard/server/service/security/auth/pat/ApiKeyAuthenticationProvider.java

@ -46,7 +46,7 @@ public class ApiKeyAuthenticationProvider extends AbstractAuthenticationProvider
return new ApiKeyAuthenticationToken(securityUser); return new ApiKeyAuthenticationToken(securityUser);
} }
private SecurityUser authenticate(String key) { public SecurityUser authenticate(String key) {
if (StringUtils.isEmpty(key)) { if (StringUtils.isEmpty(key)) {
throw new BadCredentialsException("Empty API key"); throw new BadCredentialsException("Empty API key");
} }

23
application/src/main/java/org/thingsboard/server/service/subscription/DefaultTbEntityDataSubscriptionService.java

@ -34,6 +34,7 @@ import org.springframework.web.socket.CloseStatus;
import org.thingsboard.common.util.ThingsBoardExecutors; import org.thingsboard.common.util.ThingsBoardExecutors;
import org.thingsboard.common.util.ThingsBoardThreadFactory; import org.thingsboard.common.util.ThingsBoardThreadFactory;
import org.thingsboard.server.common.data.id.EntityId; import org.thingsboard.server.common.data.id.EntityId;
import org.thingsboard.server.dao.nosql.ResultSetSizeLimitExceededException;
import org.thingsboard.server.common.data.kv.BaseReadTsKvQuery; import org.thingsboard.server.common.data.kv.BaseReadTsKvQuery;
import org.thingsboard.server.common.data.kv.ReadTsKvQuery; import org.thingsboard.server.common.data.kv.ReadTsKvQuery;
import org.thingsboard.server.common.data.kv.ReadTsKvQueryResult; import org.thingsboard.server.common.data.kv.ReadTsKvQueryResult;
@ -242,7 +243,10 @@ public class DefaultTbEntityDataSubscriptionService implements TbEntityDataSubsc
@Override @Override
public void onFailure(Throwable t) { public void onFailure(Throwable t) {
log.warn("[{}][{}] Failed to process command", finalCtx.getSessionId(), finalCtx.getCmdId()); log.warn("[{}][{}] Failed to process command", finalCtx.getSessionId(), finalCtx.getCmdId(), t);
if (t instanceof ResultSetSizeLimitExceededException) {
sendError(finalCtx, t);
}
} }
}, wsCallBackExecutor); }, wsCallBackExecutor);
} }
@ -258,7 +262,18 @@ public class DefaultTbEntityDataSubscriptionService implements TbEntityDataSubsc
handleLatestCmd(ctx, cmd.getLatestCmd()); handleLatestCmd(ctx, cmd.getLatestCmd());
} }
if (cmd.getTsCmd() != null) { if (cmd.getTsCmd() != null) {
handleTimeSeriesCmd(ctx, cmd.getTsCmd()); Futures.addCallback(handleTimeSeriesCmd(ctx, cmd.getTsCmd()), new FutureCallback<>() {
@Override
public void onSuccess(TbEntityDataSubCtx result) {}
@Override
public void onFailure(Throwable t) {
log.warn("[{}][{}] Failed to process timeseries command", ctx.getSessionId(), ctx.getCmdId(), t);
if (t instanceof ResultSetSizeLimitExceededException) {
sendError(ctx, t);
}
}
}, wsCallBackExecutor);
} }
} else { } else {
checkAndSendInitialData(ctx); checkAndSendInitialData(ctx);
@ -268,6 +283,10 @@ public class DefaultTbEntityDataSubscriptionService implements TbEntityDataSubsc
} }
} }
private void sendError(TbEntityDataSubCtx ctx, Throwable t) {
ctx.sendWsMsg(new EntityDataUpdate(ctx.getCmdId(), SubscriptionErrorCode.INTERNAL_ERROR.getCode(), t.getMessage()));
}
private void checkAndSendInitialData(@Nullable TbEntityDataSubCtx theCtx) { private void checkAndSendInitialData(@Nullable TbEntityDataSubCtx theCtx) {
if (!theCtx.isInitialDataSent()) { if (!theCtx.isInitialDataSent()) {
EntityDataUpdate update = new EntityDataUpdate(theCtx.getCmdId(), theCtx.getData(), null, theCtx.getMaxEntitiesPerDataSubscription()); EntityDataUpdate update = new EntityDataUpdate(theCtx.getCmdId(), theCtx.getData(), null, theCtx.getMaxEntitiesPerDataSubscription());

4
application/src/main/java/org/thingsboard/server/service/subscription/DefaultTbLocalSubscriptionService.java

@ -348,7 +348,7 @@ public class DefaultTbLocalSubscriptionService implements TbLocalSubscriptionSer
if (sub.isLatestValues()) { if (sub.isLatestValues()) {
for (TsKvEntry kv : data) { for (TsKvEntry kv : data) {
Long stateTs = keyStates.get(kv.getKey()); Long stateTs = keyStates.get(kv.getKey());
if (stateTs == null || kv.getTs() >= stateTs) { if (stateTs == null || kv.getTs() >= stateTs || kv.isDeletedEntry()) {
if (updateData == null) { if (updateData == null) {
updateData = new ArrayList<>(); updateData = new ArrayList<>();
} }
@ -362,7 +362,7 @@ public class DefaultTbLocalSubscriptionService implements TbLocalSubscriptionSer
for (TsKvEntry kv : data) { for (TsKvEntry kv : data) {
Long stateTs = keyStates.get(kv.getKey()); Long stateTs = keyStates.get(kv.getKey());
if (stateTs != null) { if (stateTs != null) {
if (!sub.isLatestValues() || kv.getTs() >= stateTs) { if (!sub.isLatestValues() || kv.getTs() >= stateTs || kv.isDeletedEntry()) {
if (updateData == null) { if (updateData == null) {
updateData = new ArrayList<>(); updateData = new ArrayList<>();
} }

4
application/src/main/java/org/thingsboard/server/service/ttl/AbstractCleanUpService.java

@ -32,4 +32,8 @@ public abstract class AbstractCleanUpService {
return partitionService.resolve(ServiceType.TB_CORE, TenantId.SYS_TENANT_ID, TenantId.SYS_TENANT_ID).isMyPartition(); return partitionService.resolve(ServiceType.TB_CORE, TenantId.SYS_TENANT_ID, TenantId.SYS_TENANT_ID).isMyPartition();
} }
protected boolean isTenantPartitionMine(TenantId tenantId) {
return partitionService.resolve(ServiceType.TB_CORE, tenantId, tenantId).isMyPartition();
}
} }

75
application/src/main/java/org/thingsboard/server/service/ttl/NotificationsCleanUpService.java

@ -20,33 +20,41 @@ import org.springframework.beans.factory.annotation.Value;
import org.springframework.boot.autoconfigure.condition.ConditionalOnExpression; import org.springframework.boot.autoconfigure.condition.ConditionalOnExpression;
import org.springframework.scheduling.annotation.Scheduled; import org.springframework.scheduling.annotation.Scheduled;
import org.springframework.stereotype.Service; import org.springframework.stereotype.Service;
import org.thingsboard.server.common.data.id.TenantId;
import org.thingsboard.server.common.data.notification.NotificationRequestConfig; import org.thingsboard.server.common.data.notification.NotificationRequestConfig;
import org.thingsboard.server.common.data.page.PageDataIterable;
import org.thingsboard.server.dao.notification.NotificationRequestDao; import org.thingsboard.server.dao.notification.NotificationRequestDao;
import org.thingsboard.server.dao.sqlts.insert.sql.SqlPartitioningRepository; import org.thingsboard.server.dao.sqlts.insert.sql.SqlPartitioningRepository;
import org.thingsboard.server.dao.tenant.TenantService;
import org.thingsboard.server.queue.discovery.PartitionService; import org.thingsboard.server.queue.discovery.PartitionService;
import java.time.Instant;
import java.util.concurrent.TimeUnit; import java.util.concurrent.TimeUnit;
import static org.thingsboard.server.dao.model.ModelConstants.NOTIFICATION_TABLE_NAME; import static org.thingsboard.server.dao.model.ModelConstants.NOTIFICATION_TABLE_NAME;
@Slf4j
@Service @Service
@ConditionalOnExpression("${sql.ttl.notifications.enabled:true} && ${sql.ttl.notifications.ttl:0} > 0") @ConditionalOnExpression("${sql.ttl.notifications.enabled:true} && ${sql.ttl.notifications.ttl:0} > 0")
@Slf4j
public class NotificationsCleanUpService extends AbstractCleanUpService { public class NotificationsCleanUpService extends AbstractCleanUpService {
private final SqlPartitioningRepository partitioningRepository; private final SqlPartitioningRepository partitioningRepository;
private final NotificationRequestDao notificationRequestDao; private final NotificationRequestDao notificationRequestDao;
private final TenantService tenantService;
@Value("${sql.ttl.notifications.ttl:2592000}") @Value("${sql.ttl.notifications.ttl:2592000}")
private long ttlInSec; private long ttlInSec;
@Value("${sql.notifications.partition_size:168}") @Value("${sql.notifications.partition_size:168}")
private int partitionSizeInHours; private int partitionSizeInHours;
@Value("${sql.ttl.notifications.removal_batch_size:10000}")
private int removalBatchSize;
public NotificationsCleanUpService(PartitionService partitionService, SqlPartitioningRepository partitioningRepository, public NotificationsCleanUpService(PartitionService partitionService, SqlPartitioningRepository partitioningRepository,
NotificationRequestDao notificationRequestDao) { NotificationRequestDao notificationRequestDao, TenantService tenantService) {
super(partitionService); super(partitionService);
this.partitioningRepository = partitioningRepository; this.partitioningRepository = partitioningRepository;
this.notificationRequestDao = notificationRequestDao; this.notificationRequestDao = notificationRequestDao;
this.tenantService = tenantService;
} }
@Scheduled(initialDelayString = "#{T(org.apache.commons.lang3.RandomUtils).nextLong(0, ${sql.ttl.notifications.checking_interval_ms:86400000})}", @Scheduled(initialDelayString = "#{T(org.apache.commons.lang3.RandomUtils).nextLong(0, ${sql.ttl.notifications.checking_interval_ms:86400000})}",
@ -54,18 +62,65 @@ public class NotificationsCleanUpService extends AbstractCleanUpService {
public void cleanUp() { public void cleanUp() {
long expTime = System.currentTimeMillis() - TimeUnit.SECONDS.toMillis(ttlInSec); long expTime = System.currentTimeMillis() - TimeUnit.SECONDS.toMillis(ttlInSec);
long partitionDurationMs = TimeUnit.HOURS.toMillis(partitionSizeInHours); long partitionDurationMs = TimeUnit.HOURS.toMillis(partitionSizeInHours);
if (!isSystemTenantPartitionMine()) { if (isSystemTenantPartitionMine()) {
partitioningRepository.dropPartitionsBefore(NOTIFICATION_TABLE_NAME, expTime, partitionDurationMs);
} else {
partitioningRepository.cleanupPartitionsCache(NOTIFICATION_TABLE_NAME, expTime, partitionDurationMs); partitioningRepository.cleanupPartitionsCache(NOTIFICATION_TABLE_NAME, expTime, partitionDurationMs);
return;
} }
long lastRemovedNotificationTs = partitioningRepository.dropPartitionsBefore(NOTIFICATION_TABLE_NAME, expTime, partitionDurationMs); long gap = TimeUnit.MINUTES.toMillis(10);
if (lastRemovedNotificationTs > 0) { long requestExpTime = expTime - TimeUnit.SECONDS.toMillis(NotificationRequestConfig.MAX_SENDING_DELAY) - gap;
long gap = TimeUnit.MINUTES.toMillis(10); cleanUpNotificationRequests(requestExpTime);
long requestExpTime = lastRemovedNotificationTs - TimeUnit.SECONDS.toMillis(NotificationRequestConfig.MAX_SENDING_DELAY) - gap; }
int removed = notificationRequestDao.removeAllByCreatedTimeBefore(requestExpTime);
log.info("Removed {} outdated notification requests older than {}", removed, requestExpTime); private void cleanUpNotificationRequests(long expirationTime) {
log.info("Starting notification requests cleanup for records older than {}", Instant.ofEpochMilli(expirationTime));
int totalRemoved = 0;
int tenantsProcessed = 0;
// Clean up SYSADMIN's notification requests on the system node only
if (isSystemTenantPartitionMine()) {
try {
totalRemoved += cleanUpByTenant(TenantId.SYS_TENANT_ID, expirationTime);
} catch (Exception e) {
log.warn("Failed to clean up notification requests for sysadmin {}", TenantId.SYS_TENANT_ID, e);
}
}
// Each node cleans up notification requests for its own tenants
PageDataIterable<TenantId> tenants = new PageDataIterable<>(tenantService::findTenantsIds, 10_000);
for (TenantId tenantId : tenants) {
try {
if (!isTenantPartitionMine(tenantId)) {
continue;
}
int tenantRemoved = cleanUpByTenant(tenantId, expirationTime);
totalRemoved += tenantRemoved;
tenantsProcessed++;
if (tenantRemoved > 0) {
log.trace("Removed {} notification requests for tenant {}", tenantRemoved, tenantId);
}
} catch (Exception e) {
log.warn("Failed to clean up notification requests for tenant {}", tenantId, e);
}
} }
log.info("Notification requests cleanup completed. Processed {} tenants, removed {} total records older than {}", tenantsProcessed, totalRemoved, Instant.ofEpochMilli(expirationTime));
}
private int cleanUpByTenant(TenantId tenantId, long expirationTime) {
int totalRemoved = 0;
int batchRemoved;
do {
batchRemoved = notificationRequestDao.removeByTenantIdAndCreatedTimeBeforeBatch(tenantId, expirationTime, removalBatchSize);
totalRemoved += batchRemoved;
if (batchRemoved > 0) {
log.trace("Removed {} notification requests in batch for tenant {}", batchRemoved, tenantId);
}
} while (batchRemoved >= removalBatchSize);
return totalRemoved;
} }
} }

94
application/src/main/java/org/thingsboard/server/service/ttl/rpc/RpcCleanUpService.java

@ -15,69 +15,87 @@
*/ */
package org.thingsboard.server.service.ttl.rpc; package org.thingsboard.server.service.ttl.rpc;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j; import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Value; import org.springframework.beans.factory.annotation.Value;
import org.springframework.boot.autoconfigure.condition.ConditionalOnExpression;
import org.springframework.scheduling.annotation.Scheduled; import org.springframework.scheduling.annotation.Scheduled;
import org.springframework.stereotype.Service; import org.springframework.stereotype.Service;
import org.thingsboard.server.common.data.id.TenantId; import org.thingsboard.server.common.data.id.TenantId;
import org.thingsboard.server.common.data.page.PageData; import org.thingsboard.server.common.data.page.PageDataIterable;
import org.thingsboard.server.common.data.page.PageLink;
import org.thingsboard.server.common.data.tenant.profile.DefaultTenantProfileConfiguration; import org.thingsboard.server.common.data.tenant.profile.DefaultTenantProfileConfiguration;
import org.thingsboard.server.common.msg.queue.ServiceType;
import org.thingsboard.server.dao.rpc.RpcDao; import org.thingsboard.server.dao.rpc.RpcDao;
import org.thingsboard.server.dao.tenant.TbTenantProfileCache; import org.thingsboard.server.dao.tenant.TbTenantProfileCache;
import org.thingsboard.server.dao.tenant.TenantService; import org.thingsboard.server.dao.tenant.TenantService;
import org.thingsboard.server.queue.discovery.PartitionService; import org.thingsboard.server.queue.discovery.PartitionService;
import org.thingsboard.server.queue.util.TbCoreComponent; import org.thingsboard.server.queue.util.TbCoreComponent;
import org.thingsboard.server.service.ttl.AbstractCleanUpService;
import java.util.Date; import java.time.Instant;
import java.util.Optional; import java.util.Optional;
import java.util.concurrent.TimeUnit; import java.util.concurrent.TimeUnit;
@TbCoreComponent
@Service
@Slf4j @Slf4j
@RequiredArgsConstructor @Service
public class RpcCleanUpService { @TbCoreComponent
@Value("${sql.ttl.rpc.enabled}") @ConditionalOnExpression("${sql.ttl.rpc.enabled:true}")
private boolean ttlTaskExecutionEnabled; public class RpcCleanUpService extends AbstractCleanUpService {
@Value("${sql.ttl.rpc.removal_batch_size:10000}")
private int removalBatchSize;
private final RpcDao rpcDao;
private final TenantService tenantService; private final TenantService tenantService;
private final PartitionService partitionService;
private final TbTenantProfileCache tenantProfileCache; private final TbTenantProfileCache tenantProfileCache;
private final RpcDao rpcDao;
public RpcCleanUpService(TenantService tenantService, PartitionService partitionService, TbTenantProfileCache tenantProfileCache, RpcDao rpcDao) {
super(partitionService);
this.tenantService = tenantService;
this.tenantProfileCache = tenantProfileCache;
this.rpcDao = rpcDao;
}
@Scheduled(initialDelayString = "#{T(org.apache.commons.lang3.RandomUtils).nextLong(0, ${sql.ttl.rpc.checking_interval})}", fixedDelayString = "${sql.ttl.rpc.checking_interval}") @Scheduled(initialDelayString = "#{T(org.apache.commons.lang3.RandomUtils).nextLong(0, ${sql.ttl.rpc.checking_interval})}", fixedDelayString = "${sql.ttl.rpc.checking_interval}")
public void cleanUp() { public void cleanUp() {
if (ttlTaskExecutionEnabled) { PageDataIterable<TenantId> tenants = new PageDataIterable<>(tenantService::findTenantsIds, 10_000);
PageLink tenantsBatchRequest = new PageLink(10_000, 0); for (TenantId tenantId : tenants) {
PageData<TenantId> tenantsIds; try {
do { if (!isTenantPartitionMine(tenantId)) {
tenantsIds = tenantService.findTenantsIds(tenantsBatchRequest); continue;
for (TenantId tenantId : tenantsIds.getData()) {
if (!partitionService.resolve(ServiceType.TB_CORE, tenantId, tenantId).isMyPartition()) {
continue;
}
Optional<DefaultTenantProfileConfiguration> tenantProfileConfiguration = tenantProfileCache.get(tenantId).getProfileConfiguration();
if (tenantProfileConfiguration.isEmpty() || tenantProfileConfiguration.get().getRpcTtlDays() == 0) {
continue;
}
long ttl = TimeUnit.DAYS.toMillis(tenantProfileConfiguration.get().getRpcTtlDays());
long expirationTime = System.currentTimeMillis() - ttl;
int totalRemoved = rpcDao.deleteOutdatedRpcByTenantId(tenantId, expirationTime);
if (totalRemoved > 0) {
log.info("Removed {} outdated rpc(s) for tenant {} older than {}", totalRemoved, tenantId, new Date(expirationTime));
}
} }
tenantsBatchRequest = tenantsBatchRequest.nextPageLink(); Optional<DefaultTenantProfileConfiguration> tenantProfileConfiguration = tenantProfileCache.get(tenantId).getProfileConfiguration();
} while (tenantsIds.hasNext()); if (tenantProfileConfiguration.isEmpty() || tenantProfileConfiguration.get().getRpcTtlDays() == 0) {
continue;
}
long ttl = TimeUnit.DAYS.toMillis(tenantProfileConfiguration.get().getRpcTtlDays());
long expirationTime = System.currentTimeMillis() - ttl;
int totalRemoved = cleanUpByTenant(tenantId, expirationTime);
if (totalRemoved > 0) {
log.info("Removed {} outdated rpc(s) for tenant {} older than {}", totalRemoved, tenantId, Instant.ofEpochMilli(expirationTime));
}
} catch (Exception e) {
log.warn("Failed to clean up rpc by ttl for tenant {}", tenantId, e);
}
} }
} }
private int cleanUpByTenant(TenantId tenantId, long expirationTime) {
int totalRemoved = 0;
int batchRemoved;
do {
batchRemoved = rpcDao.deleteOutdatedRpcByTenantIdBatch(tenantId, expirationTime, removalBatchSize);
totalRemoved += batchRemoved;
if (batchRemoved > 0) {
log.trace("Removed {} rpc in batch for tenant {}", batchRemoved, tenantId);
}
} while (batchRemoved >= removalBatchSize);
return totalRemoved;
}
} }

3
application/src/main/java/org/thingsboard/server/service/ws/AuthCmd.java

@ -23,11 +23,14 @@ import lombok.NoArgsConstructor;
@NoArgsConstructor @NoArgsConstructor
@AllArgsConstructor @AllArgsConstructor
public class AuthCmd implements WsCmd { public class AuthCmd implements WsCmd {
private int cmdId; private int cmdId;
private String token; private String token;
private String apiKey;
@Override @Override
public WsCmdType getType() { public WsCmdType getType() {
return WsCmdType.AUTH; return WsCmdType.AUTH;
} }
} }

76
application/src/main/resources/thingsboard.yml

@ -15,6 +15,7 @@
# #
# Server common parameters # Server common parameters
# Configures HTTP/HTTPS bind address, port, SSL, WebSocket, and REST API settings.
server: server:
# Server bind-address # Server bind-address
address: "${HTTP_BIND_ADDRESS:0.0.0.0}" address: "${HTTP_BIND_ADDRESS:0.0.0.0}"
@ -109,23 +110,25 @@ server:
response_timeout: "${DEFAULT_RULE_ENGINE_RESPONSE_TIMEOUT:10000}" response_timeout: "${DEFAULT_RULE_ENGINE_RESPONSE_TIMEOUT:10000}"
# Application info parameters # Application info parameters
# Exposes application metadata such as version string injected at build time.
app: app:
# Application version # Application version
version: "@project.version@" version: "@project.version@"
# Zookeeper connection parameters # ZooKeeper connection parameters
# Controls ZooKeeper-based service discovery and cluster coordination for microservice deployments.
zk: zk:
# Enable/disable zookeeper discovery service. # Enable/disable ZooKeeper discovery service.
enabled: "${ZOOKEEPER_ENABLED:false}" enabled: "${ZOOKEEPER_ENABLED:false}"
# Zookeeper connect string # ZooKeeper connect string
url: "${ZOOKEEPER_URL:localhost:2181}" url: "${ZOOKEEPER_URL:localhost:2181}"
# Zookeeper retry interval in milliseconds # ZooKeeper retry interval in milliseconds
retry_interval_ms: "${ZOOKEEPER_RETRY_INTERVAL_MS:3000}" retry_interval_ms: "${ZOOKEEPER_RETRY_INTERVAL_MS:3000}"
# Zookeeper connection timeout in milliseconds # ZooKeeper connection timeout in milliseconds
connection_timeout_ms: "${ZOOKEEPER_CONNECTION_TIMEOUT_MS:3000}" connection_timeout_ms: "${ZOOKEEPER_CONNECTION_TIMEOUT_MS:3000}"
# Zookeeper session timeout in milliseconds # ZooKeeper session timeout in milliseconds
session_timeout_ms: "${ZOOKEEPER_SESSION_TIMEOUT_MS:3000}" session_timeout_ms: "${ZOOKEEPER_SESSION_TIMEOUT_MS:3000}"
# Name of the directory in zookeeper 'filesystem' # Name of the directory in ZooKeeper 'filesystem'
zk_dir: "${ZOOKEEPER_NODES_DIR:/thingsboard}" zk_dir: "${ZOOKEEPER_NODES_DIR:/thingsboard}"
# The recalculate_delay property is recommended in a microservices architecture setup for rule-engine services. # The recalculate_delay property is recommended in a microservices architecture setup for rule-engine services.
# This property provides a pause to ensure that when a rule-engine service is restarted, other nodes don't immediately attempt to recalculate their partitions. # This property provides a pause to ensure that when a rule-engine service is restarted, other nodes don't immediately attempt to recalculate their partitions.
@ -133,6 +136,7 @@ zk:
recalculate_delay: "${ZOOKEEPER_RECALCULATE_DELAY_MS:0}" recalculate_delay: "${ZOOKEEPER_RECALCULATE_DELAY_MS:0}"
# Cluster parameters # Cluster parameters
# Controls cluster statistics — tracks the number of messages exchanged between cluster nodes.
cluster: cluster:
stats: stats:
# Enable/Disable the cluster statistics. Calculates the number of messages sent between cluster nodes based on each type # Enable/Disable the cluster statistics. Calculates the number of messages sent between cluster nodes based on each type
@ -141,11 +145,13 @@ cluster:
print_interval_ms: "${TB_CLUSTER_STATS_PRINT_INTERVAL_MS:10000}" print_interval_ms: "${TB_CLUSTER_STATS_PRINT_INTERVAL_MS:10000}"
# Plugins configuration parameters # Plugins configuration parameters
# Defines classpath scan packages used to discover and register ThingsBoard extension plugins.
plugins: plugins:
# Comma-separated package list used during classpath scanning for plugins # Comma-separated package list used during classpath scanning for plugins
scan_packages: "${PLUGINS_SCAN_PACKAGES:org.thingsboard.server.extensions,org.thingsboard.rule.engine}" scan_packages: "${PLUGINS_SCAN_PACKAGES:org.thingsboard.server.extensions,org.thingsboard.rule.engine}"
# Security parameters # Security parameters
# Configures JWT tokens, user login, device claiming, OAuth2, and CA certificate trust store.
security: security:
# JWT Token parameters # JWT Token parameters
jwt: # Since 3.4.2 values are persisted in the database during installation or upgrade. On Install, the key will be generated randomly if no custom value set. You can change it later from Web UI under SYS_ADMIN jwt: # Since 3.4.2 values are persisted in the database during installation or upgrade. On Install, the key will be generated randomly if no custom value set. You can change it later from Web UI under SYS_ADMIN
@ -184,6 +190,7 @@ security:
password: "${SECURITY_JAVA_CACERTS_PASSWORD:changeit}" password: "${SECURITY_JAVA_CACERTS_PASSWORD:changeit}"
# Mail settings parameters # Mail settings parameters
# Configures mail service OAuth2 token refresh and per-tenant sending rate limits.
mail: mail:
oauth2: oauth2:
# Interval for checking refresh token expiration in seconds(by default, 1 day). # Interval for checking refresh token expiration in seconds(by default, 1 day).
@ -192,6 +199,7 @@ mail:
per_tenant_rate_limits: "${MAIL_PER_TENANT_RATE_LIMITS:}" per_tenant_rate_limits: "${MAIL_PER_TENANT_RATE_LIMITS:}"
# Usage statistics parameters # Usage statistics parameters
# Controls collection and reporting intervals for API usage stats at system, tenant, and customer levels.
usage: usage:
stats: stats:
report: report:
@ -215,6 +223,7 @@ usage:
report_interval: "${DEVICES_STATS_REPORT_INTERVAL:60}" report_interval: "${DEVICES_STATS_REPORT_INTERVAL:60}"
# UI settings parameters # UI settings parameters
# Configures dashboard data limits and base URL for UI help assets.
ui: ui:
# Dashboard parameters # Dashboard parameters
dashboard: dashboard:
@ -226,6 +235,7 @@ ui:
base-url: "${UI_HELP_BASE_URL:https://raw.githubusercontent.com/thingsboard/thingsboard-ui-help/release-4.4}" base-url: "${UI_HELP_BASE_URL:https://raw.githubusercontent.com/thingsboard/thingsboard-ui-help/release-4.4}"
# Database telemetry parameters # Database telemetry parameters
# Selects the storage backend (SQL, Cassandra, or TimescaleDB) for time-series and latest telemetry data.
database: database:
ts_max_intervals: "${DATABASE_TS_MAX_INTERVALS:700}" # Max number of DB queries generated by a single API call to fetch telemetry records ts_max_intervals: "${DATABASE_TS_MAX_INTERVALS:700}" # Max number of DB queries generated by a single API call to fetch telemetry records
ts: ts:
@ -234,6 +244,7 @@ database:
type: "${DATABASE_TS_LATEST_TYPE:sql}" # cassandra, sql, or timescale (for hybrid mode, DATABASE_TS_TYPE value should be cassandra, or timescale) type: "${DATABASE_TS_LATEST_TYPE:sql}" # cassandra, sql, or timescale (for hybrid mode, DATABASE_TS_TYPE value should be cassandra, or timescale)
# Cassandra driver configuration parameters # Cassandra driver configuration parameters
# Configures cluster name, keyspace, SSL, credentials, socket, and query settings for the Cassandra driver.
cassandra: cassandra:
# Thingsboard cluster name # Thingsboard cluster name
cluster_name: "${CASSANDRA_CLUSTER_NAME:Thingsboard Cluster}" cluster_name: "${CASSANDRA_CLUSTER_NAME:Thingsboard Cluster}"
@ -352,6 +363,7 @@ cassandra:
print_tenant_names: "${CASSANDRA_QUERY_TENANT_RATE_LIMITS_PRINT_TENANT_NAMES:false}" print_tenant_names: "${CASSANDRA_QUERY_TENANT_RATE_LIMITS_PRINT_TENANT_NAMES:false}"
# SQL configuration parameters # SQL configuration parameters
# Tunes batch sizes, delays, thread counts, TTL, and partitioning for SQL-backed persistence of telemetry, events, and audit logs.
sql: sql:
# Specify batch size for persisting attribute updates # Specify batch size for persisting attribute updates
attributes: attributes:
@ -432,10 +444,11 @@ sql:
edge_events_ttl: "${SQL_TTL_EDGE_EVENTS_TTL:2628000}" # Number of seconds. The current value corresponds to one month edge_events_ttl: "${SQL_TTL_EDGE_EVENTS_TTL:2628000}" # Number of seconds. The current value corresponds to one month
alarms: alarms:
checking_interval: "${SQL_ALARMS_TTL_CHECKING_INTERVAL:7200000}" # Number of milliseconds. The current value corresponds to two hours checking_interval: "${SQL_ALARMS_TTL_CHECKING_INTERVAL:7200000}" # Number of milliseconds. The current value corresponds to two hours
removal_batch_size: "${SQL_ALARMS_TTL_REMOVAL_BATCH_SIZE:3000}" # To delete outdated alarms not all at once but in batches removal_batch_size: "${SQL_ALARMS_TTL_REMOVAL_BATCH_SIZE:3000}" # Batch size for records removal
rpc: rpc:
enabled: "${SQL_TTL_RPC_ENABLED:true}" # Enable/disable TTL (Time To Live) for rpc call records enabled: "${SQL_TTL_RPC_ENABLED:true}" # Enable/disable TTL (Time To Live) for rpc call records
checking_interval: "${SQL_RPC_TTL_CHECKING_INTERVAL:7200000}" # Number of milliseconds. The current value corresponds to two hours checking_interval: "${SQL_RPC_TTL_CHECKING_INTERVAL:7200000}" # Number of milliseconds. The current value corresponds to two hours
removal_batch_size: "${SQL_RPC_TTL_REMOVAL_BATCH_SIZE:10000}" # Batch size for records removal
audit_logs: audit_logs:
enabled: "${SQL_TTL_AUDIT_LOGS_ENABLED:true}" # Enable/disable TTL (Time To Live) for audit log records enabled: "${SQL_TTL_AUDIT_LOGS_ENABLED:true}" # Enable/disable TTL (Time To Live) for audit log records
ttl: "${SQL_TTL_AUDIT_LOGS_SECS:0}" # Disabled by default. The accuracy of the cleanup depends on the sql.audit_logs.partition_size ttl: "${SQL_TTL_AUDIT_LOGS_SECS:0}" # Disabled by default. The accuracy of the cleanup depends on the sql.audit_logs.partition_size
@ -444,6 +457,7 @@ sql:
enabled: "${SQL_TTL_NOTIFICATIONS_ENABLED:true}" # Enable/disable TTL (Time To Live) for notification center records enabled: "${SQL_TTL_NOTIFICATIONS_ENABLED:true}" # Enable/disable TTL (Time To Live) for notification center records
ttl: "${SQL_TTL_NOTIFICATIONS_SECS:2592000}" # Default value - 30 days ttl: "${SQL_TTL_NOTIFICATIONS_SECS:2592000}" # Default value - 30 days
checking_interval_ms: "${SQL_TTL_NOTIFICATIONS_CHECKING_INTERVAL_MS:86400000}" # Default value - 1 day checking_interval_ms: "${SQL_TTL_NOTIFICATIONS_CHECKING_INTERVAL_MS:86400000}" # Default value - 1 day
removal_batch_size: "${SQL_TTL_NOTIFICATIONS_REMOVAL_BATCH_SIZE:10000}" # Batch size for records removal
api_keys: api_keys:
enabled: "${SQL_TTL_API_KEYS_ENABLED:true}" # Enable/disable TTL (Time To Live) for expired api keys records enabled: "${SQL_TTL_API_KEYS_ENABLED:true}" # Enable/disable TTL (Time To Live) for expired api keys records
checking_interval_ms: "${SQL_TTL_API_KEYS_CHECKING_INTERVAL_MS:86400000}" # Default value - 1 day checking_interval_ms: "${SQL_TTL_API_KEYS_CHECKING_INTERVAL_MS:86400000}" # Default value - 1 day
@ -453,6 +467,7 @@ sql:
query_timeout: "${SQL_RELATIONS_QUERY_TIMEOUT_SEC:20}" # This value has to be reasonably small to prevent the relation query from blocking all other DB calls query_timeout: "${SQL_RELATIONS_QUERY_TIMEOUT_SEC:20}" # This value has to be reasonably small to prevent the relation query from blocking all other DB calls
# Actor system parameters # Actor system parameters
# Configures thread pools, timeouts, and behavior for the internal actor system processing devices, rules, and RPCs.
actors: actors:
system: system:
throughput: "${ACTORS_SYSTEM_THROUGHPUT:5}" # Number of messages the actor system will process per actor before switching to processing of messages for the next actor throughput: "${ACTORS_SYSTEM_THROUGHPUT:5}" # Number of messages the actor system will process per actor before switching to processing of messages for the next actor
@ -553,6 +568,8 @@ actors:
# Time in seconds to receive calculation result. # Time in seconds to receive calculation result.
calculation_timeout: "${ACTORS_CALCULATION_TIMEOUT_SEC:5}" calculation_timeout: "${ACTORS_CALCULATION_TIMEOUT_SEC:5}"
# Debug settings parameters
# Configures the global default duration for debug mode used by rule chains and calculated fields.
debug: debug:
settings: settings:
# Default duration (in minutes) for debug mode. Min value is 1 minute. Tenant profile settings override this one. # Default duration (in minutes) for debug mode. Min value is 1 minute. Tenant profile settings override this one.
@ -560,6 +577,7 @@ debug:
default_duration: "${DEBUG_SETTINGS_DEFAULT_DURATION_MINUTES:15}" default_duration: "${DEBUG_SETTINGS_DEFAULT_DURATION_MINUTES:15}"
# Cache settings parameters # Cache settings parameters
# Configures cache type (Caffeine or Redis), pool size, and per-entity TTL and max-size limits for all caches.
cache: cache:
# caffeine or redis(7.2 - latest compatible version) # caffeine or redis(7.2 - latest compatible version)
type: "${CACHE_TYPE:caffeine}" type: "${CACHE_TYPE:caffeine}"
@ -719,6 +737,7 @@ cache:
spring.data.redis.repositories.enabled: false # Disable this because it is not required. spring.data.redis.repositories.enabled: false # Disable this because it is not required.
# Redis/Valkey configuration parameters # Redis/Valkey configuration parameters
# Configures standalone, cluster, or sentinel Redis connection, SSL, and connection pool settings used for caching.
redis: redis:
# standalone or cluster or sentinel # standalone or cluster or sentinel
connection: connection:
@ -755,9 +774,9 @@ redis:
password: "${REDIS_SENTINEL_PASSWORD:}" password: "${REDIS_SENTINEL_PASSWORD:}"
# If set false will be used pool config build from values of the pool config section # If set false will be used pool config build from values of the pool config section
useDefaultPoolConfig: "${REDIS_USE_DEFAULT_POOL_CONFIG:true}" useDefaultPoolConfig: "${REDIS_USE_DEFAULT_POOL_CONFIG:true}"
# db index # Redis logical database index to select after connecting.
db: "${REDIS_DB:0}" db: "${REDIS_DB:0}"
# db password # Password for Redis authentication (leave empty if not required).
password: "${REDIS_PASSWORD:}" password: "${REDIS_PASSWORD:}"
# Redis username for ACL authentication (Redis 6.0+). Leave empty for legacy password-only auth # Redis username for ACL authentication (Redis 6.0+). Leave empty for legacy password-only auth
username: "${REDIS_USERNAME:}" username: "${REDIS_USERNAME:}"
@ -802,11 +821,13 @@ redis:
# Update version parameters # Update version parameters
# Controls whether the platform periodically checks for new ThingsBoard releases.
updates: updates:
# Enable/disable checks for the new version # Enable/disable checks for the new version
enabled: "${UPDATES_ENABLED:true}" enabled: "${UPDATES_ENABLED:true}"
# Spring CORS configuration parameters # Spring CORS configuration parameters
# Defines allowed origins, methods, headers, and credentials for cross-origin REST API requests.
spring.mvc.cors: spring.mvc.cors:
mappings: mappings:
# Intercept path # Intercept path
@ -823,6 +844,7 @@ spring.mvc.cors:
allow-credentials: "true" allow-credentials: "true"
# General spring parameters # General spring parameters
# Miscellaneous Spring Boot settings for circular references, Freemarker, MVC, multipart, and JPA dialect.
spring.main.allow-circular-references: "true" # Spring Boot configuration property that controls whether circular dependencies between beans are allowed. spring.main.allow-circular-references: "true" # Spring Boot configuration property that controls whether circular dependencies between beans are allowed.
spring.freemarker.checkTemplateLocation: "false" # spring freemarker configuration spring.freemarker.checkTemplateLocation: "false" # spring freemarker configuration
spring.mvc.async.request-timeout: "${SPRING_MVC_ASYNC_REQUEST_TIMEOUT:30000}" # The default timeout for asynchronous requests in milliseconds spring.mvc.async.request-timeout: "${SPRING_MVC_ASYNC_REQUEST_TIMEOUT:30000}" # The default timeout for asynchronous requests in milliseconds
@ -841,6 +863,7 @@ spring.jpa.properties.hibernate.order_by.default_null_ordering: "${SPRING_JPA_PR
spring.jpa.properties.hibernate.dialect: "${SPRING_JPA_DIALECT:org.thingsboard.server.dao.ThingsboardPostgreSQLDialect}" # we use custom dialect that contains ilike(arg1, arg2) function (is interpreted to postgres ILIKE operator) spring.jpa.properties.hibernate.dialect: "${SPRING_JPA_DIALECT:org.thingsboard.server.dao.ThingsboardPostgreSQLDialect}" # we use custom dialect that contains ilike(arg1, arg2) function (is interpreted to postgres ILIKE operator)
# SQL DAO Configuration parameters # SQL DAO Configuration parameters
# Configures Spring JPA, Hibernate DDL, and HikariCP datasource connections for the primary and events databases.
spring: spring:
data: data:
jpa: jpa:
@ -891,6 +914,7 @@ spring:
registerMbeans: "${SPRING_EVENTS_DATASOURCE_HIKARI_REGISTER_MBEANS:false}" registerMbeans: "${SPRING_EVENTS_DATASOURCE_HIKARI_REGISTER_MBEANS:false}"
# Audit log parameters # Audit log parameters
# Configures audit logging levels per entity type and optional forwarding to an external sink such as Elasticsearch.
audit-log: audit-log:
# Enable/disable audit log functionality. # Enable/disable audit log functionality.
enabled: "${AUDIT_LOG_ENABLED:true}" enabled: "${AUDIT_LOG_ENABLED:true}"
@ -933,6 +957,7 @@ audit-log:
password: "${AUDIT_LOG_SINK_PASSWORD:}" # Password used to access external sink system password: "${AUDIT_LOG_SINK_PASSWORD:}" # Password used to access external sink system
# Device state parameters # Device state parameters
# Configures device inactivity detection, state persistence strategy, and rate limits for state rule nodes.
state: state:
# Device inactivity timeout is a global configuration parameter that defines when the device will be marked as "inactive" by the server. # Device inactivity timeout is a global configuration parameter that defines when the device will be marked as "inactive" by the server.
# The parameter value is in seconds. A user can overwrite this parameter for an individual device by setting the “inactivityTimeout” server-side attribute (NOTE: expects value in milliseconds). # The parameter value is in seconds. A user can overwrite this parameter for an individual device by setting the “inactivityTimeout” server-side attribute (NOTE: expects value in milliseconds).
@ -963,7 +988,8 @@ state:
# Refill is set to be greedy. Please refer to Bucket4j library documentation for more details. # Refill is set to be greedy. Please refer to Bucket4j library documentation for more details.
rateLimit: "${DEVICE_STATE_NODE_RATE_LIMIT_CONFIGURATION:1:1,30:60,60:3600}" rateLimit: "${DEVICE_STATE_NODE_RATE_LIMIT_CONFIGURATION:1:1,30:60,60:3600}"
# Tbel parameters # TBEL parameters
# Configures the ThingsBoard Expression Language (TBEL) engine: limits, timeouts, thread pool, and blacklisting.
tbel: tbel:
# Enable/Disable TBEL feature. # Enable/Disable TBEL feature.
enabled: "${TBEL_ENABLED:true}" enabled: "${TBEL_ENABLED:true}"
@ -992,6 +1018,7 @@ tbel:
print_interval_ms: "${TB_TBEL_STATS_PRINT_INTERVAL_MS:10000}" print_interval_ms: "${TB_TBEL_STATS_PRINT_INTERVAL_MS:10000}"
# JS parameters # JS parameters
# Configures the JavaScript execution engine (local Nashorn or remote Node.js): limits, sandboxing, and thread pools.
js: js:
# local (Nashorn Engine, deprecated) OR remote JS-Executors (NodeJS) # local (Nashorn Engine, deprecated) OR remote JS-Executors (NodeJS)
evaluator: "${JS_EVALUATOR:local}" evaluator: "${JS_EVALUATOR:local}"
@ -1039,6 +1066,7 @@ js:
print_interval_ms: "${TB_JS_REMOTE_STATS_PRINT_INTERVAL_MS:10000}" print_interval_ms: "${TB_JS_REMOTE_STATS_PRINT_INTERVAL_MS:10000}"
# Transport configuration parameters # Transport configuration parameters
# Configures session management, rate limits, and protocol-specific settings for HTTP, MQTT, CoAP, LwM2M, and SNMP transports.
transport: transport:
sessions: sessions:
# Session inactivity timeout is a global configuration parameter that defines how long the device transport session will be opened after the last message arrives from the device. # Session inactivity timeout is a global configuration parameter that defines how long the device transport session will be opened after the last message arrives from the device.
@ -1349,6 +1377,7 @@ transport:
fetch_frequency: "${TB_GATEWAY_DASHBOARD_SYNC_FETCH_FREQUENCY:24}" fetch_frequency: "${TB_GATEWAY_DASHBOARD_SYNC_FETCH_FREQUENCY:24}"
# CoAP server parameters # CoAP server parameters
# Configures the standalone CoAP server bind address, port, DTLS encryption, and credential settings.
coap: coap:
server: server:
# Enable/disable coap server. # Enable/disable coap server.
@ -1433,6 +1462,7 @@ coap:
dtls_session_report_timeout: "${TB_COAP_X509_DTLS_SESSION_REPORT_TIMEOUT:1800000}" dtls_session_report_timeout: "${TB_COAP_X509_DTLS_SESSION_REPORT_TIMEOUT:1800000}"
# Device connectivity parameters # Device connectivity parameters
# Specifies the hosts, ports, and credentials exposed to the UI for generating device connection check commands.
device: device:
connectivity: connectivity:
http: http:
@ -1486,6 +1516,7 @@ device:
image_version: "${DEVICE_CONNECTIVITY_GATEWAY_IMAGE_VERSION:3.8-stable}" image_version: "${DEVICE_CONNECTIVITY_GATEWAY_IMAGE_VERSION:3.8-stable}"
# Edges parameters # Edges parameters
# Controls Edge instance gRPC communication, event storage, state persistence, and statistics reporting.
edges: edges:
# Enable/disable Edge instance # Enable/disable Edge instance
enabled: "${EDGES_ENABLED:true}" enabled: "${EDGES_ENABLED:true}"
@ -1507,10 +1538,17 @@ edges:
ssl: ssl:
# Enable/disable SSL support # Enable/disable SSL support
enabled: "${EDGES_RPC_SSL_ENABLED:false}" enabled: "${EDGES_RPC_SSL_ENABLED:false}"
# Cert file to be used during TLS connectivity to the cloud # Path to the server certificate file (holds server certificate or certificate chain, may include server private key).
# Accepts an absolute filesystem path (e.g. /etc/thingsboard/certChainFile.pem),
# a relative path resolved against the working directory first then the classpath,
# or a classpath resource with the explicit "classpath:" prefix (e.g. classpath:conf/certChainFile.pem).
cert: "${EDGES_RPC_SSL_CERT:certChainFile.pem}" cert: "${EDGES_RPC_SSL_CERT:certChainFile.pem}"
# Private key file associated with the Cert certificate. This key is used in the encryption process during a secure connection # Path to the server certificate private key file. Optional if the private key is already present in the cert file above.
# Supports the same path resolution as 'cert': absolute, relative/classpath, or "classpath:" prefix.
# Leave empty when using a combined PEM cert that already contains the private key.
private_key: "${EDGES_RPC_SSL_PRIVATE_KEY:privateKeyFile.pem}" private_key: "${EDGES_RPC_SSL_PRIVATE_KEY:privateKeyFile.pem}"
# Server certificate private key password (optional). Leave empty if the key is not encrypted.
key_password: "${EDGES_RPC_SSL_KEY_PASSWORD:}"
# Maximum size (in bytes) of inbound messages the cloud can handle from the edge. By default, it can handle messages up to 4 Megabytes # Maximum size (in bytes) of inbound messages the cloud can handle from the edge. By default, it can handle messages up to 4 Megabytes
max_inbound_message_size: "${EDGES_RPC_MAX_INBOUND_MESSAGE_SIZE:4194304}" max_inbound_message_size: "${EDGES_RPC_MAX_INBOUND_MESSAGE_SIZE:4194304}"
# Maximum length of telemetry (time-series and attributes) message the cloud sends to the edge. By default, there is no limitation. # Maximum length of telemetry (time-series and attributes) message the cloud sends to the edge. By default, there is no limitation.
@ -1547,6 +1585,7 @@ edges:
report-interval-millis: "${EDGES_STATS_REPORT_INTERVAL_MS:600000}" report-interval-millis: "${EDGES_STATS_REPORT_INTERVAL_MS:600000}"
# Spring doc common parameters # Spring doc common parameters
# Enables or disables the OpenAPI/Swagger documentation endpoint and sets the default media type.
springdoc: springdoc:
# If false swagger API docs will be unavailable # If false swagger API docs will be unavailable
api-docs.enabled: "${SWAGGER_ENABLED:true}" api-docs.enabled: "${SWAGGER_ENABLED:true}"
@ -1554,6 +1593,7 @@ springdoc:
default-produces-media-type: "${SWAGGER_DEFAULT_PRODUCES_MEDIA_TYPE:application/json}" default-produces-media-type: "${SWAGGER_DEFAULT_PRODUCES_MEDIA_TYPE:application/json}"
# Swagger common parameters # Swagger common parameters
# Configures Swagger UI metadata: title, description, contact, license, version, and API path patterns.
swagger: swagger:
# General swagger match pattern of swagger UI links # General swagger match pattern of swagger UI links
api_path: "${SWAGGER_API_PATH:/api/**}" api_path: "${SWAGGER_API_PATH:/api/**}"
@ -1585,6 +1625,7 @@ swagger:
doc_expansion: "${SWAGGER_DOC_EXPANSION:list}" doc_expansion: "${SWAGGER_DOC_EXPANSION:list}"
# Queue configuration parameters # Queue configuration parameters
# Configures the message queue backend (in-memory or Kafka) and all topic/partition/consumer settings for each service.
queue: queue:
type: "${TB_QUEUE_TYPE:in-memory}" # in-memory or kafka (Apache Kafka) type: "${TB_QUEUE_TYPE:in-memory}" # in-memory or kafka (Apache Kafka)
prefix: "${TB_QUEUE_PREFIX:}" # Global queue prefix. If specified, prefix is added before default topic name: 'prefix.default_topic_name'. Prefix is applied to all topics (and consumer groups for kafka). prefix: "${TB_QUEUE_PREFIX:}" # Global queue prefix. If specified, prefix is added before default topic name: 'prefix.default_topic_name'. Prefix is applied to all topics (and consumer groups for kafka).
@ -1998,12 +2039,14 @@ queue:
processing_interval: "${TB_QUEUE_TASKS_STATS_PROCESSING_INTERVAL_MS:1000}" processing_interval: "${TB_QUEUE_TASKS_STATS_PROCESSING_INTERVAL_MS:1000}"
# Event configuration parameters # Event configuration parameters
# Controls limits on debug event content size stored for rule chain and rule node execution.
event: event:
debug: debug:
# Maximum number of symbols per debug event. The event content will be truncated if needed # Maximum number of symbols per debug event. The event content will be truncated if needed
max-symbols: "${TB_MAX_DEBUG_EVENT_SYMBOLS:4096}" max-symbols: "${TB_MAX_DEBUG_EVENT_SYMBOLS:4096}"
# General service parameters # General service parameters
# Sets the deployment type (monolith or microservice) and assigns tenant profiles to specific Rule Engine instances.
service: service:
type: "${TB_SERVICE_TYPE:monolith}" # monolith or tb-core or tb-rule-engine type: "${TB_SERVICE_TYPE:monolith}" # monolith or tb-core or tb-rule-engine
# Unique id for this service (autogenerated if empty) # Unique id for this service (autogenerated if empty)
@ -2017,6 +2060,7 @@ service:
executor_thread_pool_size: "${TB_RULE_ENGINE_PUBSUB_EXECUTOR_THREAD_POOL_SIZE:0}" executor_thread_pool_size: "${TB_RULE_ENGINE_PUBSUB_EXECUTOR_THREAD_POOL_SIZE:0}"
# Metrics parameters # Metrics parameters
# Enables actuator metrics endpoint and configures system info (CPU, memory) persistence frequency and TTL.
metrics: metrics:
# Enable/disable actuator metrics. # Enable/disable actuator metrics.
enabled: "${METRICS_ENABLED:false}" enabled: "${METRICS_ENABLED:false}"
@ -2030,6 +2074,7 @@ metrics:
ttl: "${METRICS_SYSTEM_INFO_TTL_DAYS:7}" ttl: "${METRICS_SYSTEM_INFO_TTL_DAYS:7}"
# Version control parameters # Version control parameters
# Configures thread pools and Git repository folder for entity version control (export/import) operations.
vc: vc:
# Pool size for handling export tasks # Pool size for handling export tasks
thread_pool_size: "${TB_VC_POOL_SIZE:6}" thread_pool_size: "${TB_VC_POOL_SIZE:6}"
@ -2040,6 +2085,7 @@ vc:
repositories-folder: "${TB_VC_GIT_REPOSITORIES_FOLDER:${java.io.tmpdir}/repositories}" repositories-folder: "${TB_VC_GIT_REPOSITORIES_FOLDER:${java.io.tmpdir}/repositories}"
# Notification system parameters # Notification system parameters
# Configures thread pool size and deduplication intervals for the notification rule processing engine.
notification_system: notification_system:
# Specify thread pool size for Notification System processing notification rules and notification sending. Recommend value <= 10 # Specify thread pool size for Notification System processing notification rules and notification sending. Recommend value <= 10
thread_pool_size: "${TB_NOTIFICATION_SYSTEM_THREAD_POOL_SIZE:10}" thread_pool_size: "${TB_NOTIFICATION_SYSTEM_THREAD_POOL_SIZE:10}"
@ -2048,6 +2094,7 @@ notification_system:
deduplication_durations: "${TB_NOTIFICATION_RULES_DEDUPLICATION_DURATIONS:NEW_PLATFORM_VERSION:0;RATE_LIMITS:14400000;}" deduplication_durations: "${TB_NOTIFICATION_RULES_DEDUPLICATION_DURATIONS:NEW_PLATFORM_VERSION:0;RATE_LIMITS:14400000;}"
# General management parameters # General management parameters
# Configures Spring Boot Actuator endpoint exposure and health indicator settings.
management: management:
endpoints: endpoints:
web: web:
@ -2060,6 +2107,7 @@ management:
enabled: "false" enabled: "false"
# Mobile application settings for Thingsboard mobile application # Mobile application settings for Thingsboard mobile application
# Specifies domain name and store links used for the ThingsBoard Live mobile application QR code and deep links.
mobileApp: mobileApp:
# Server domain name for Thingsboard Live mobile application # Server domain name for Thingsboard Live mobile application
domain: "${TB_MOBILE_APP_DOMAIN:demo.thingsboard.io}" domain: "${TB_MOBILE_APP_DOMAIN:demo.thingsboard.io}"
@ -2068,6 +2116,8 @@ mobileApp:
# Link to App Store for Thingsboard Live mobile application # Link to App Store for Thingsboard Live mobile application
appStoreLink: "${TB_MOBILE_APP_APP_STORE_LINK:https://apps.apple.com/us/app/thingsboard-live/id1594355695}" appStoreLink: "${TB_MOBILE_APP_APP_STORE_LINK:https://apps.apple.com/us/app/thingsboard-live/id1594355695}"
# MQTT client parameters
# Configures MQTT client retransmission behavior including max attempts, initial delay, and jitter factor.
mqtt: mqtt:
# MQTT client configuration parameters # MQTT client configuration parameters
client: client:

11
application/src/test/java/org/thingsboard/server/controller/AbstractControllerTest.java

@ -86,12 +86,12 @@ public abstract class AbstractControllerTest extends AbstractNotifyEntityTest {
} }
@Before @Before
public void beforeWsTest() throws Exception { public void beforeWsTest() {
// placeholder // placeholder
} }
@After @After
public void afterWsTest() throws Exception { public void afterWsTest() {
if (wsClient != null) { if (wsClient != null) {
wsClient.close(); wsClient.close();
} }
@ -113,4 +113,11 @@ public abstract class AbstractControllerTest extends AbstractNotifyEntityTest {
return wsClient; return wsClient;
} }
protected TbTestWebSocketClient buildAndConnectWebSocketClientWithApiKey(String apiKey) throws URISyntaxException, InterruptedException {
TbTestWebSocketClient wsClient = new TbTestWebSocketClient(new URI(WS_URL + wsPort + "/api/ws"));
assertThat(wsClient.connectBlocking(TIMEOUT, TimeUnit.SECONDS)).isTrue();
wsClient.authenticateWithApiKey(apiKey);
return wsClient;
}
} }

9
application/src/test/java/org/thingsboard/server/controller/AbstractWebTest.java

@ -425,6 +425,10 @@ public abstract class AbstractWebTest extends AbstractInMemoryStorageTest {
public void teardownWebTest() throws Exception { public void teardownWebTest() throws Exception {
log.debug("Executing web test teardown"); log.debug("Executing web test teardown");
// Drain any pending housekeeper work left by the test body (e.g., bulk tenant deletes)
// before proceeding with teardown deletions, to avoid 90s per-tenant wait timing out.
awaitHousekeeperDrained();
loginSysAdmin(); loginSysAdmin();
deleteTenant(tenantId); deleteTenant(tenantId);
deleteDifferentTenant(); deleteDifferentTenant();
@ -457,6 +461,11 @@ public abstract class AbstractWebTest extends AbstractInMemoryStorageTest {
.until(() -> storage.getLag("tb_housekeeper") == 0); .until(() -> storage.getLag("tb_housekeeper") == 0);
} }
protected void awaitHousekeeperDrained() {
Awaitility.await("housekeeper drained").atMost(5, TimeUnit.MINUTES).during(300, TimeUnit.MILLISECONDS)
.until(() -> storage.getLag("tb_housekeeper") == 0);
}
private List<Tenant> getAllTenants() throws Exception { private List<Tenant> getAllTenants() throws Exception {
List<Tenant> loadedTenants = new ArrayList<>(); List<Tenant> loadedTenants = new ArrayList<>();
PageLink pageLink = new PageLink(10); PageLink pageLink = new PageLink(10);

71
application/src/test/java/org/thingsboard/server/controller/ApiKeyWebSocketApiTest.java

@ -0,0 +1,71 @@
/**
* 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.controller;
import lombok.extern.slf4j.Slf4j;
import org.junit.After;
import org.junit.Before;
import org.junit.Test;
import org.thingsboard.server.common.data.pat.ApiKey;
import org.thingsboard.server.common.data.pat.ApiKeyInfo;
import org.thingsboard.server.dao.service.DaoSqlTest;
import java.net.URI;
import java.net.URISyntaxException;
import java.util.concurrent.TimeUnit;
import static org.assertj.core.api.Assertions.assertThat;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
@Slf4j
@DaoSqlTest
public class ApiKeyWebSocketApiTest extends WebSocketApiTest {
private ApiKey apiKey;
@Before
public void setUpApiKey() {
ApiKeyInfo apiKeyInfo = new ApiKeyInfo();
apiKeyInfo.setDescription("WS test API key");
apiKeyInfo.setEnabled(true);
apiKeyInfo.setUserId(tenantAdminUserId);
apiKey = doPost("/api/apiKey", apiKeyInfo, ApiKey.class);
}
@After
public void tearDownApiKey() throws Exception {
loginTenantAdmin();
doDelete("/api/apiKey/" + apiKey.getId()).andExpect(status().isOk());
}
@Override
protected TbTestWebSocketClient buildAndConnectWebSocketClient() throws URISyntaxException, InterruptedException {
return buildAndConnectWebSocketClientWithApiKey(apiKey.getValue());
}
@Test
public void testInvalidApiKeyAuthCmd_connectionClosed() throws Exception {
TbTestWebSocketClient client = new TbTestWebSocketClient(new URI(WS_URL + wsPort + "/api/ws"));
assertThat(client.connectBlocking(TIMEOUT, TimeUnit.SECONDS)).isTrue();
try {
client.authenticateWithApiKey("invalid-key");
assertThat(client.waitForClose()).isTrue();
} finally {
client.close();
}
}
}

39
application/src/test/java/org/thingsboard/server/controller/AuditLogControllerTest.java

@ -19,6 +19,7 @@ import com.datastax.oss.driver.api.core.uuid.Uuids;
import com.fasterxml.jackson.core.type.TypeReference; import com.fasterxml.jackson.core.type.TypeReference;
import lombok.Getter; import lombok.Getter;
import lombok.extern.slf4j.Slf4j; import lombok.extern.slf4j.Slf4j;
import org.awaitility.Awaitility;
import org.junit.After; import org.junit.After;
import org.junit.Assert; import org.junit.Assert;
import org.junit.Before; import org.junit.Before;
@ -61,6 +62,9 @@ import static org.mockito.Mockito.verify;
@DaoSqlTest @DaoSqlTest
public class AuditLogControllerTest extends AbstractControllerTest { public class AuditLogControllerTest extends AbstractControllerTest {
// Small enough to force multiple pages, verifying pagination loop correctness
private static final int SMALL_PAGE_SIZE = 5;
private Tenant savedTenant; private Tenant savedTenant;
private User tenantAdmin; private User tenantAdmin;
@ -112,17 +116,14 @@ public class AuditLogControllerTest extends AbstractControllerTest {
doPost("/api/device", device, Device.class); doPost("/api/device", device, Device.class);
} }
List<AuditLog> loadedAuditLogs = getAuditLogs(5, "/api/audit/logs?"); Awaitility.await().atMost(TIMEOUT, TimeUnit.SECONDS).untilAsserted(() ->
assertThat(getAuditLogs(SMALL_PAGE_SIZE, "/api/audit/logs?")).hasSize(11 + 1));
Assert.assertEquals(11 + 1, loadedAuditLogs.size());
loadedAuditLogs = getAuditLogs(5, "/api/audit/logs/customer/" + ModelConstants.NULL_UUID + "?");
Assert.assertEquals(11 + 1, loadedAuditLogs.size());
loadedAuditLogs = getAuditLogs(5, "/api/audit/logs/user/" + tenantAdmin.getId().getId().toString() + "?"); Awaitility.await().atMost(TIMEOUT, TimeUnit.SECONDS).untilAsserted(() ->
assertThat(getAuditLogs(SMALL_PAGE_SIZE, "/api/audit/logs/customer/" + ModelConstants.NULL_UUID + "?")).hasSize(11 + 1));
Assert.assertEquals(11 + 1, loadedAuditLogs.size()); Awaitility.await().atMost(TIMEOUT, TimeUnit.SECONDS).untilAsserted(() ->
assertThat(getAuditLogs(SMALL_PAGE_SIZE, "/api/audit/logs/user/" + tenantAdmin.getId().getId().toString() + "?")).hasSize(11 + 1));
} }
@Test @Test
@ -136,9 +137,10 @@ public class AuditLogControllerTest extends AbstractControllerTest {
doPost("/api/tenantProfile", tenantProfile, TenantProfile.class); doPost("/api/tenantProfile", tenantProfile, TenantProfile.class);
} }
List<AuditLog> loadedAuditLogs = getAuditLogs(100, "/api/audit/logs?"); int expectedSize = loadedAuditLogsBefore.size() + 3;
Awaitility.await().atMost(TIMEOUT, TimeUnit.SECONDS).untilAsserted(() ->
Assert.assertEquals("Have X audit log before this test + New tenant profiles in the test", loadedAuditLogsBefore.size() + 3, loadedAuditLogs.size()); Assert.assertEquals("Have X audit log before this test + New tenant profiles in the test",
expectedSize, getAuditLogs(100, "/api/audit/logs?").size()));
} }
private List<AuditLog> getAuditLogs(int pageSize, String urlTemplate) throws Exception { private List<AuditLog> getAuditLogs(int pageSize, String urlTemplate) throws Exception {
@ -168,9 +170,9 @@ public class AuditLogControllerTest extends AbstractControllerTest {
savedDevice = doPost("/api/device", savedDevice, Device.class); savedDevice = doPost("/api/device", savedDevice, Device.class);
} }
List<AuditLog> loadedAuditLogs = getAuditLogs(5, "/api/audit/logs/entity/DEVICE/" + savedDevice.getId().getId() + "?"); Device finalSavedDevice = savedDevice;
Awaitility.await().atMost(TIMEOUT, TimeUnit.SECONDS).untilAsserted(() ->
Assert.assertEquals(11 + 1, loadedAuditLogs.size()); assertThat(getAuditLogs(SMALL_PAGE_SIZE, "/api/audit/logs/entity/DEVICE/" + finalSavedDevice.getId().getId() + "?")).hasSize(11 + 1));
} }
@Test @Test
@ -186,9 +188,10 @@ public class AuditLogControllerTest extends AbstractControllerTest {
tenantProfile.setName(tenantProfile.getName() + "(old)"); tenantProfile.setName(tenantProfile.getName() + "(old)");
tenantProfile = doPost("/api/tenantProfile", tenantProfile, TenantProfile.class); tenantProfile = doPost("/api/tenantProfile", tenantProfile, TenantProfile.class);
List<AuditLog> loadedAuditLogs = getAuditLogs(5, "/api/audit/logs/entity/" +tenantProfile.getId().getEntityType()+ "/" + tenantProfile.getId().getId() + "?"); TenantProfile finalTenantProfile = tenantProfile;
Awaitility.await().atMost(TIMEOUT, TimeUnit.SECONDS).untilAsserted(() ->
Assert.assertEquals("Audit logs count by Tenant Profile entity", 2, loadedAuditLogs.size()); assertThat(getAuditLogs(SMALL_PAGE_SIZE, "/api/audit/logs/entity/" + finalTenantProfile.getId().getEntityType() + "/" + finalTenantProfile.getId().getId() + "?"))
.as("Audit logs count by Tenant Profile entity").hasSize(2));
//cleanup //cleanup
doDelete("/api/tenantProfile/" + tenantProfile.getId().getId().toString()); doDelete("/api/tenantProfile/" + tenantProfile.getId().getId().toString());

4
application/src/test/java/org/thingsboard/server/controller/CalculatedFieldControllerTest.java

@ -18,6 +18,7 @@ package org.thingsboard.server.controller;
import org.junit.After; import org.junit.After;
import org.junit.Before; import org.junit.Before;
import org.junit.Test; import org.junit.Test;
import org.thingsboard.common.util.JacksonUtil;
import org.thingsboard.server.common.data.Device; import org.thingsboard.server.common.data.Device;
import org.thingsboard.server.common.data.DeviceProfile; import org.thingsboard.server.common.data.DeviceProfile;
import org.thingsboard.server.common.data.EntityType; import org.thingsboard.server.common.data.EntityType;
@ -108,6 +109,7 @@ public class CalculatedFieldControllerTest extends AbstractControllerTest {
assertThat(savedCalculatedField.getType()).isEqualTo(calculatedField.getType()); assertThat(savedCalculatedField.getType()).isEqualTo(calculatedField.getType());
assertThat(savedCalculatedField.getName()).isEqualTo(calculatedField.getName()); assertThat(savedCalculatedField.getName()).isEqualTo(calculatedField.getName());
assertThat(savedCalculatedField.getConfiguration()).isEqualTo(getSimpleCalculatedFieldConfig()); assertThat(savedCalculatedField.getConfiguration()).isEqualTo(getSimpleCalculatedFieldConfig());
assertThat(savedCalculatedField.getAdditionalInfo()).isEqualTo(calculatedField.getAdditionalInfo());
assertThat(savedCalculatedField.getVersion()).isEqualTo(1L); assertThat(savedCalculatedField.getVersion()).isEqualTo(1L);
savedCalculatedField.setName("Test CF"); savedCalculatedField.setName("Test CF");
@ -115,6 +117,7 @@ public class CalculatedFieldControllerTest extends AbstractControllerTest {
CalculatedField updatedCalculatedField = doPost("/api/calculatedField", savedCalculatedField, CalculatedField.class); CalculatedField updatedCalculatedField = doPost("/api/calculatedField", savedCalculatedField, CalculatedField.class);
assertThat(updatedCalculatedField.getName()).isEqualTo(savedCalculatedField.getName()); assertThat(updatedCalculatedField.getName()).isEqualTo(savedCalculatedField.getName());
assertThat(updatedCalculatedField.getAdditionalInfo()).isEqualTo(savedCalculatedField.getAdditionalInfo());
assertThat(updatedCalculatedField.getVersion()).isEqualTo(savedCalculatedField.getVersion() + 1); assertThat(updatedCalculatedField.getVersion()).isEqualTo(savedCalculatedField.getVersion() + 1);
doDelete("/api/calculatedField/" + savedCalculatedField.getId().getId().toString()) doDelete("/api/calculatedField/" + savedCalculatedField.getId().getId().toString())
@ -322,6 +325,7 @@ public class CalculatedFieldControllerTest extends AbstractControllerTest {
calculatedField.setType(cfType); calculatedField.setType(cfType);
calculatedField.setName("Test Calculated Field for " + entityId); calculatedField.setName("Test Calculated Field for " + entityId);
calculatedField.setConfigurationVersion(1); calculatedField.setConfigurationVersion(1);
calculatedField.setAdditionalInfo(JacksonUtil.newObjectNode());
if (customConfiguration != null) { if (customConfiguration != null) {
calculatedField.setConfiguration(customConfiguration); calculatedField.setConfiguration(customConfiguration);
} else switch (cfType) { } else switch (cfType) {

12
application/src/test/java/org/thingsboard/server/controller/EntityViewControllerTest.java

@ -41,6 +41,7 @@ import org.springframework.test.context.ContextConfiguration;
import org.springframework.test.context.DynamicPropertyRegistry; import org.springframework.test.context.DynamicPropertyRegistry;
import org.springframework.test.context.DynamicPropertySource; import org.springframework.test.context.DynamicPropertySource;
import org.springframework.test.context.TestPropertySource; import org.springframework.test.context.TestPropertySource;
import org.springframework.test.util.TestSocketUtils;
import org.springframework.test.web.servlet.ResultActions; import org.springframework.test.web.servlet.ResultActions;
import org.thingsboard.common.util.ThingsBoardExecutors; import org.thingsboard.common.util.ThingsBoardExecutors;
import org.thingsboard.server.common.data.Customer; import org.thingsboard.server.common.data.Customer;
@ -90,8 +91,6 @@ import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertNotNull; import static org.junit.Assert.assertNotNull;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
import static org.thingsboard.server.dao.model.ModelConstants.NULL_UUID; import static org.thingsboard.server.dao.model.ModelConstants.NULL_UUID;
import static org.thingsboard.server.transport.mqtt.AbstractMqttIntegrationTest.MQTT_PORT;
import static org.thingsboard.server.transport.mqtt.AbstractMqttIntegrationTest.MQTT_URL;
@TestPropertySource(properties = { @TestPropertySource(properties = {
"transport.mqtt.enabled=true", "transport.mqtt.enabled=true",
@ -101,6 +100,15 @@ import static org.thingsboard.server.transport.mqtt.AbstractMqttIntegrationTest.
@ContextConfiguration(classes = {EntityViewControllerTest.Config.class}) @ContextConfiguration(classes = {EntityViewControllerTest.Config.class})
@DaoSqlTest @DaoSqlTest
public class EntityViewControllerTest extends AbstractControllerTest { public class EntityViewControllerTest extends AbstractControllerTest {
// Must NOT be imported from AbstractMqttIntegrationTest. That field is a static final initialized
// once per JVM. Other test classes (e.g. MqttGatewayRateLimitsTest, DeviceEdgeTest) share the same
// constant but produce a different Spring context cache key, so Spring creates a separate
// ApplicationContext for each of them. Every context starts its own MqttTransportService and tries
// to bind the same port, causing BindException when tests run in the same Surefire JVM fork.
// Declaring the port here gives this context its own independently allocated port.
static final int MQTT_PORT = TestSocketUtils.findAvailableTcpPort();
static final String MQTT_URL = "tcp://localhost:" + MQTT_PORT;
@DynamicPropertySource @DynamicPropertySource
static void props(DynamicPropertyRegistry registry) { static void props(DynamicPropertyRegistry registry) {
log.warn("transport.mqtt.bind_port = {}", MQTT_PORT); log.warn("transport.mqtt.bind_port = {}", MQTT_PORT);

24
application/src/test/java/org/thingsboard/server/controller/TbTestWebSocketClient.java

@ -39,7 +39,6 @@ import org.thingsboard.server.service.ws.telemetry.cmd.v2.EntityDataUpdate;
import org.thingsboard.server.service.ws.telemetry.cmd.v2.EntityHistoryCmd; import org.thingsboard.server.service.ws.telemetry.cmd.v2.EntityHistoryCmd;
import org.thingsboard.server.service.ws.telemetry.cmd.v2.LatestValueCmd; import org.thingsboard.server.service.ws.telemetry.cmd.v2.LatestValueCmd;
import org.thingsboard.server.service.ws.telemetry.cmd.v2.TimeSeriesCmd; import org.thingsboard.server.service.ws.telemetry.cmd.v2.TimeSeriesCmd;
import org.thingsboard.server.service.ws.telemetry.sub.TelemetrySubscriptionUpdate;
import java.net.URI; import java.net.URI;
import java.nio.channels.NotYetConnectedException; import java.nio.channels.NotYetConnectedException;
@ -53,6 +52,8 @@ public class TbTestWebSocketClient extends WebSocketClient {
private static final long TIMEOUT = TimeUnit.SECONDS.toMillis(30); private static final long TIMEOUT = TimeUnit.SECONDS.toMillis(30);
private final CountDownLatch closeLatch = new CountDownLatch(1);
@Getter @Getter
private volatile String lastMsg; private volatile String lastMsg;
private volatile CountDownLatch reply; private volatile CountDownLatch reply;
@ -62,6 +63,7 @@ public class TbTestWebSocketClient extends WebSocketClient {
super(serverUri); super(serverUri);
} }
@Override @Override
public void onOpen(ServerHandshake serverHandshake) { public void onOpen(ServerHandshake serverHandshake) {
@ -69,7 +71,13 @@ public class TbTestWebSocketClient extends WebSocketClient {
public void authenticate(String token) { public void authenticate(String token) {
WsCommandsWrapper cmdsWrapper = new WsCommandsWrapper(); WsCommandsWrapper cmdsWrapper = new WsCommandsWrapper();
cmdsWrapper.setAuthCmd(new AuthCmd(1, token)); cmdsWrapper.setAuthCmd(new AuthCmd(1, token, null));
send(JacksonUtil.toString(cmdsWrapper));
}
public void authenticateWithApiKey(String apiKey) {
WsCommandsWrapper cmdsWrapper = new WsCommandsWrapper();
cmdsWrapper.setAuthCmd(new AuthCmd(1, null, apiKey));
send(JacksonUtil.toString(cmdsWrapper)); send(JacksonUtil.toString(cmdsWrapper));
} }
@ -88,6 +96,16 @@ public class TbTestWebSocketClient extends WebSocketClient {
@Override @Override
public void onClose(int i, String s, boolean b) { public void onClose(int i, String s, boolean b) {
log.info("CLOSED."); log.info("CLOSED.");
closeLatch.countDown();
}
public boolean waitForClose() {
try {
return closeLatch.await(TIMEOUT, TimeUnit.MILLISECONDS);
} catch (InterruptedException e) {
log.warn("Failed to await close", e);
return false;
}
} }
@Override @Override
@ -275,7 +293,7 @@ public class TbTestWebSocketClient extends WebSocketClient {
public JsonNode sendTimeseriesCmd(EntityId entityId, String scope) { public JsonNode sendTimeseriesCmd(EntityId entityId, String scope) {
log.warn("sendTimeseriesCmd entityId: {}, scope: {}", entityId, scope); log.warn("sendTimeseriesCmd entityId: {}, scope: {}", entityId, scope);
TimeseriesSubscriptionCmd cmd = new TimeseriesSubscriptionCmd(0, 0, 0, 10, null); TimeseriesSubscriptionCmd cmd = new TimeseriesSubscriptionCmd(0, 0, 0, 10, null);
cmd.setEntityId(entityId.getId().toString()); cmd.setEntityId(entityId.getId().toString());
cmd.setEntityType(entityId.getEntityType().toString()); cmd.setEntityType(entityId.getEntityType().toString());
cmd.setCmdId(1); cmd.setCmdId(1);

266
application/src/test/java/org/thingsboard/server/controller/WebsocketApiTest.java → application/src/test/java/org/thingsboard/server/controller/WebSocketApiTest.java

@ -19,13 +19,16 @@ import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.node.ArrayNode; import com.fasterxml.jackson.databind.node.ArrayNode;
import com.fasterxml.jackson.databind.node.ObjectNode; import com.fasterxml.jackson.databind.node.ObjectNode;
import com.google.common.util.concurrent.FutureCallback; import com.google.common.util.concurrent.FutureCallback;
import com.google.common.util.concurrent.Futures;
import lombok.extern.slf4j.Slf4j; import lombok.extern.slf4j.Slf4j;
import org.checkerframework.checker.nullness.qual.Nullable; import org.checkerframework.checker.nullness.qual.Nullable;
import org.junit.After; import org.junit.After;
import org.junit.Assert; import org.junit.Assert;
import org.junit.Before; import org.junit.Before;
import org.junit.Test; import org.junit.Test;
import org.mockito.Mockito;
import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.mock.mockito.SpyBean;
import org.springframework.test.context.TestPropertySource; import org.springframework.test.context.TestPropertySource;
import org.testcontainers.shaded.org.apache.commons.lang3.RandomStringUtils; import org.testcontainers.shaded.org.apache.commons.lang3.RandomStringUtils;
import org.thingsboard.common.util.JacksonUtil; import org.thingsboard.common.util.JacksonUtil;
@ -60,7 +63,9 @@ import org.thingsboard.server.common.data.query.NumericFilterPredicate;
import org.thingsboard.server.common.data.query.SingleEntityFilter; import org.thingsboard.server.common.data.query.SingleEntityFilter;
import org.thingsboard.server.common.data.query.TsValue; import org.thingsboard.server.common.data.query.TsValue;
import org.thingsboard.server.common.data.relation.EntityRelation; import org.thingsboard.server.common.data.relation.EntityRelation;
import org.thingsboard.server.dao.nosql.ResultSetSizeLimitExceededException;
import org.thingsboard.server.dao.service.DaoSqlTest; import org.thingsboard.server.dao.service.DaoSqlTest;
import org.thingsboard.server.dao.timeseries.TimeseriesService;
import org.thingsboard.server.service.subscription.SubscriptionErrorCode; import org.thingsboard.server.service.subscription.SubscriptionErrorCode;
import org.thingsboard.server.service.subscription.TbAttributeSubscriptionScope; import org.thingsboard.server.service.subscription.TbAttributeSubscriptionScope;
import org.thingsboard.server.service.telemetry.TelemetrySubscriptionService; import org.thingsboard.server.service.telemetry.TelemetrySubscriptionService;
@ -71,13 +76,11 @@ import org.thingsboard.server.service.ws.telemetry.cmd.v2.AlarmStatusUpdate;
import org.thingsboard.server.service.ws.telemetry.cmd.v2.EntityCountCmd; import org.thingsboard.server.service.ws.telemetry.cmd.v2.EntityCountCmd;
import org.thingsboard.server.service.ws.telemetry.cmd.v2.EntityCountUpdate; import org.thingsboard.server.service.ws.telemetry.cmd.v2.EntityCountUpdate;
import org.thingsboard.server.service.ws.telemetry.cmd.v2.EntityDataUpdate; import org.thingsboard.server.service.ws.telemetry.cmd.v2.EntityDataUpdate;
import org.thingsboard.server.service.ws.telemetry.sub.TelemetrySubscriptionUpdate;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.Arrays; import java.util.Arrays;
import java.util.Collections; import java.util.Collections;
import java.util.List; import java.util.List;
import java.util.Map;
import java.util.concurrent.CountDownLatch; import java.util.concurrent.CountDownLatch;
import java.util.concurrent.TimeUnit; import java.util.concurrent.TimeUnit;
@ -91,10 +94,14 @@ import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.
"server.ws.alarms_per_alarm_status_subscription_cache_size=5", "server.ws.alarms_per_alarm_status_subscription_cache_size=5",
"server.ws.dynamic_page_link.refresh_interval=15" "server.ws.dynamic_page_link.refresh_interval=15"
}) })
public class WebsocketApiTest extends AbstractControllerTest { public class WebSocketApiTest extends AbstractControllerTest {
@Autowired @Autowired
private TelemetrySubscriptionService tsService; private TelemetrySubscriptionService tsService;
@SpyBean
private TimeseriesService timeseriesService;
Device device; Device device;
DeviceTypeFilter dtf; DeviceTypeFilter dtf;
@ -128,8 +135,8 @@ public class WebsocketApiTest extends AbstractControllerTest {
PageData<EntityData> pageData = update.getData(); PageData<EntityData> pageData = update.getData();
Assert.assertNotNull(pageData); Assert.assertNotNull(pageData);
Assert.assertEquals(1, pageData.getData().size()); Assert.assertEquals(1, pageData.getData().size());
Assert.assertEquals(device.getId(), pageData.getData().get(0).getEntityId()); Assert.assertEquals(device.getId(), pageData.getData().getFirst().getEntityId());
Assert.assertEquals(0, pageData.getData().get(0).getTimeseries().get("temperature").length); Assert.assertEquals(0, pageData.getData().getFirst().getTimeseries().get("temperature").length);
TsKvEntry dataPoint1 = new BasicTsKvEntry(now - TimeUnit.MINUTES.toMillis(1), new LongDataEntry("temperature", 42L)); TsKvEntry dataPoint1 = new BasicTsKvEntry(now - TimeUnit.MINUTES.toMillis(1), new LongDataEntry("temperature", 42L));
TsKvEntry dataPoint2 = new BasicTsKvEntry(now - TimeUnit.MINUTES.toMillis(2), new LongDataEntry("temperature", 42L)); TsKvEntry dataPoint2 = new BasicTsKvEntry(now - TimeUnit.MINUTES.toMillis(2), new LongDataEntry("temperature", 42L));
@ -144,8 +151,8 @@ public class WebsocketApiTest extends AbstractControllerTest {
List<EntityData> dataList = update.getUpdate(); List<EntityData> dataList = update.getUpdate();
Assert.assertNotNull(dataList); Assert.assertNotNull(dataList);
Assert.assertEquals(1, dataList.size()); Assert.assertEquals(1, dataList.size());
Assert.assertEquals(device.getId(), dataList.get(0).getEntityId()); Assert.assertEquals(device.getId(), dataList.getFirst().getEntityId());
TsValue[] tsArray = dataList.get(0).getTimeseries().get("temperature"); TsValue[] tsArray = dataList.getFirst().getTimeseries().get("temperature");
Assert.assertEquals(3, tsArray.length); Assert.assertEquals(3, tsArray.length);
Assert.assertEquals(new TsValue(dataPoint1.getTs(), dataPoint1.getValueAsString()), tsArray[0]); Assert.assertEquals(new TsValue(dataPoint1.getTs(), dataPoint1.getValueAsString()), tsArray[0]);
Assert.assertEquals(new TsValue(dataPoint2.getTs(), dataPoint2.getValueAsString()), tsArray[1]); Assert.assertEquals(new TsValue(dataPoint2.getTs(), dataPoint2.getValueAsString()), tsArray[1]);
@ -162,7 +169,7 @@ public class WebsocketApiTest extends AbstractControllerTest {
PageData<EntityData> pageData = update.getData(); PageData<EntityData> pageData = update.getData();
Assert.assertNotNull(pageData); Assert.assertNotNull(pageData);
Assert.assertEquals(1, pageData.getData().size()); Assert.assertEquals(1, pageData.getData().size());
Assert.assertEquals(device.getId(), pageData.getData().get(0).getEntityId()); Assert.assertEquals(device.getId(), pageData.getData().getFirst().getEntityId());
TsKvEntry dataPoint1 = new BasicTsKvEntry(now - TimeUnit.MINUTES.toMillis(1), new LongDataEntry("temperature", 42L)); TsKvEntry dataPoint1 = new BasicTsKvEntry(now - TimeUnit.MINUTES.toMillis(1), new LongDataEntry("temperature", 42L));
TsKvEntry dataPoint2 = new BasicTsKvEntry(now - TimeUnit.MINUTES.toMillis(2), new LongDataEntry("temperature", 43L)); TsKvEntry dataPoint2 = new BasicTsKvEntry(now - TimeUnit.MINUTES.toMillis(2), new LongDataEntry("temperature", 43L));
@ -176,8 +183,8 @@ public class WebsocketApiTest extends AbstractControllerTest {
List<EntityData> listData = update.getUpdate(); List<EntityData> listData = update.getUpdate();
Assert.assertNotNull(listData); Assert.assertNotNull(listData);
Assert.assertEquals(1, listData.size()); Assert.assertEquals(1, listData.size());
Assert.assertEquals(device.getId(), listData.get(0).getEntityId()); Assert.assertEquals(device.getId(), listData.getFirst().getEntityId());
TsValue[] tsArray = listData.get(0).getTimeseries().get("temperature"); TsValue[] tsArray = listData.getFirst().getTimeseries().get("temperature");
Assert.assertEquals(3, tsArray.length); Assert.assertEquals(3, tsArray.length);
Assert.assertEquals(new TsValue(dataPoint1.getTs(), dataPoint1.getValueAsString()), tsArray[0]); Assert.assertEquals(new TsValue(dataPoint1.getTs(), dataPoint1.getValueAsString()), tsArray[0]);
Assert.assertEquals(new TsValue(dataPoint2.getTs(), dataPoint2.getValueAsString()), tsArray[1]); Assert.assertEquals(new TsValue(dataPoint2.getTs(), dataPoint2.getValueAsString()), tsArray[1]);
@ -186,7 +193,7 @@ public class WebsocketApiTest extends AbstractControllerTest {
now = System.currentTimeMillis(); now = System.currentTimeMillis();
TsKvEntry dataPoint4 = new BasicTsKvEntry(now, new LongDataEntry("temperature", 45L)); TsKvEntry dataPoint4 = new BasicTsKvEntry(now, new LongDataEntry("temperature", 45L));
getWsClient().registerWaitForUpdate(); getWsClient().registerWaitForUpdate();
sendTelemetry(device, Arrays.asList(dataPoint4)); sendTelemetry(device, List.of(dataPoint4));
String msg = getWsClient().waitForUpdate(); String msg = getWsClient().waitForUpdate();
update = JacksonUtil.fromString(msg, EntityDataUpdate.class); update = JacksonUtil.fromString(msg, EntityDataUpdate.class);
@ -194,9 +201,9 @@ public class WebsocketApiTest extends AbstractControllerTest {
List<EntityData> eData = update.getUpdate(); List<EntityData> eData = update.getUpdate();
Assert.assertNotNull(eData); Assert.assertNotNull(eData);
Assert.assertEquals(1, eData.size()); Assert.assertEquals(1, eData.size());
Assert.assertEquals(device.getId(), eData.get(0).getEntityId()); Assert.assertEquals(device.getId(), eData.getFirst().getEntityId());
Assert.assertNotNull(eData.get(0).getTimeseries()); Assert.assertNotNull(eData.getFirst().getTimeseries());
TsValue[] tsValues = eData.get(0).getTimeseries().get("temperature"); TsValue[] tsValues = eData.getFirst().getTimeseries().get("temperature");
Assert.assertNotNull(tsValues); Assert.assertNotNull(tsValues);
Assert.assertEquals(new TsValue(dataPoint4.getTs(), dataPoint4.getValueAsString()), tsValues[0]); Assert.assertEquals(new TsValue(dataPoint4.getTs(), dataPoint4.getValueAsString()), tsValues[0]);
} }
@ -521,7 +528,7 @@ public class WebsocketApiTest extends AbstractControllerTest {
String msg = getWsClient().waitForUpdate(TimeUnit.SECONDS.toMillis(1)); String msg = getWsClient().waitForUpdate(TimeUnit.SECONDS.toMillis(1));
Assert.assertNull(msg); Assert.assertNull(msg);
// check device // check a device
AlarmStatusCmd deviceCmd = new AlarmStatusCmd(2, device.getId(), null, List.of(AlarmSeverity.CRITICAL)); AlarmStatusCmd deviceCmd = new AlarmStatusCmd(2, device.getId(), null, List.of(AlarmSeverity.CRITICAL));
getWsClient().send(deviceCmd); getWsClient().send(deviceCmd);
@ -589,13 +596,13 @@ public class WebsocketApiTest extends AbstractControllerTest {
PageData<EntityData> pageData = update.getData(); PageData<EntityData> pageData = update.getData();
Assert.assertNotNull(pageData); Assert.assertNotNull(pageData);
Assert.assertEquals(1, pageData.getData().size()); Assert.assertEquals(1, pageData.getData().size());
Assert.assertEquals(device.getId(), pageData.getData().get(0).getEntityId()); Assert.assertEquals(device.getId(), pageData.getData().getFirst().getEntityId());
Assert.assertNotNull(pageData.getData().get(0).getLatest().get(EntityKeyType.TIME_SERIES).get("temperature")); Assert.assertNotNull(pageData.getData().getFirst().getLatest().get(EntityKeyType.TIME_SERIES).get("temperature"));
Assert.assertEquals(0, pageData.getData().get(0).getLatest().get(EntityKeyType.TIME_SERIES).get("temperature").getTs()); Assert.assertEquals(0, pageData.getData().getFirst().getLatest().get(EntityKeyType.TIME_SERIES).get("temperature").getTs());
Assert.assertEquals("", pageData.getData().get(0).getLatest().get(EntityKeyType.TIME_SERIES).get("temperature").getValue()); Assert.assertEquals("", pageData.getData().getFirst().getLatest().get(EntityKeyType.TIME_SERIES).get("temperature").getValue());
TsKvEntry dataPoint1 = new BasicTsKvEntry(now - TimeUnit.MINUTES.toMillis(1), new LongDataEntry("temperature", 42L)); TsKvEntry dataPoint1 = new BasicTsKvEntry(now - TimeUnit.MINUTES.toMillis(1), new LongDataEntry("temperature", 42L));
List<TsKvEntry> tsData = Arrays.asList(dataPoint1); List<TsKvEntry> tsData = List.of(dataPoint1);
sendTelemetry(device, tsData); sendTelemetry(device, tsData);
update = getWsClient().subscribeLatestUpdate(keys); update = getWsClient().subscribeLatestUpdate(keys);
@ -605,36 +612,36 @@ public class WebsocketApiTest extends AbstractControllerTest {
List<EntityData> listData = update.getUpdate(); List<EntityData> listData = update.getUpdate();
Assert.assertNotNull(listData); Assert.assertNotNull(listData);
Assert.assertEquals(1, listData.size()); Assert.assertEquals(1, listData.size());
Assert.assertEquals(device.getId(), listData.get(0).getEntityId()); Assert.assertEquals(device.getId(), listData.getFirst().getEntityId());
Assert.assertNotNull(listData.get(0).getLatest().get(EntityKeyType.TIME_SERIES)); Assert.assertNotNull(listData.getFirst().getLatest().get(EntityKeyType.TIME_SERIES));
TsValue tsValue = listData.get(0).getLatest().get(EntityKeyType.TIME_SERIES).get("temperature"); TsValue tsValue = listData.getFirst().getLatest().get(EntityKeyType.TIME_SERIES).get("temperature");
Assert.assertEquals(new TsValue(dataPoint1.getTs(), dataPoint1.getValueAsString()), tsValue); Assert.assertEquals(new TsValue(dataPoint1.getTs(), dataPoint1.getValueAsString()), tsValue);
now = System.currentTimeMillis(); now = System.currentTimeMillis();
TsKvEntry dataPoint2 = new BasicTsKvEntry(now, new LongDataEntry("temperature", 52L)); TsKvEntry dataPoint2 = new BasicTsKvEntry(now, new LongDataEntry("temperature", 52L));
getWsClient().registerWaitForUpdate(); getWsClient().registerWaitForUpdate();
sendTelemetry(device, Arrays.asList(dataPoint2)); sendTelemetry(device, List.of(dataPoint2));
update = getWsClient().parseDataReply(getWsClient().waitForUpdate()); update = getWsClient().parseDataReply(getWsClient().waitForUpdate());
Assert.assertEquals(1, update.getCmdId()); Assert.assertEquals(1, update.getCmdId());
List<EntityData> eData = update.getUpdate(); List<EntityData> eData = update.getUpdate();
Assert.assertNotNull(eData); Assert.assertNotNull(eData);
Assert.assertEquals(1, eData.size()); Assert.assertEquals(1, eData.size());
Assert.assertEquals(device.getId(), eData.get(0).getEntityId()); Assert.assertEquals(device.getId(), eData.getFirst().getEntityId());
Assert.assertNotNull(eData.get(0).getLatest().get(EntityKeyType.TIME_SERIES)); Assert.assertNotNull(eData.getFirst().getLatest().get(EntityKeyType.TIME_SERIES));
tsValue = eData.get(0).getLatest().get(EntityKeyType.TIME_SERIES).get("temperature"); tsValue = eData.getFirst().getLatest().get(EntityKeyType.TIME_SERIES).get("temperature");
Assert.assertEquals(new TsValue(dataPoint2.getTs(), dataPoint2.getValueAsString()), tsValue); Assert.assertEquals(new TsValue(dataPoint2.getTs(), dataPoint2.getValueAsString()), tsValue);
//Sending update from the past, while latest value has new timestamp; //Sending update from the past, while latest value has new timestamp;
getWsClient().registerWaitForUpdate(); getWsClient().registerWaitForUpdate();
sendTelemetry(device, Arrays.asList(dataPoint1)); sendTelemetry(device, List.of(dataPoint1));
String msg = getWsClient().waitForUpdate(TimeUnit.SECONDS.toMillis(1)); String msg = getWsClient().waitForUpdate(TimeUnit.SECONDS.toMillis(1));
Assert.assertNull(msg); Assert.assertNull(msg);
//Sending duplicate update again //Sending duplicate update again
getWsClient().registerWaitForUpdate(); getWsClient().registerWaitForUpdate();
sendTelemetry(device, Arrays.asList(dataPoint2)); sendTelemetry(device, List.of(dataPoint2));
msg = getWsClient().waitForUpdate(TimeUnit.SECONDS.toMillis(1)); msg = getWsClient().waitForUpdate(TimeUnit.SECONDS.toMillis(1));
Assert.assertNull(msg); Assert.assertNull(msg);
} }
@ -677,14 +684,14 @@ public class WebsocketApiTest extends AbstractControllerTest {
PageData<EntityData> pageData = update.getData(); PageData<EntityData> pageData = update.getData();
Assert.assertNotNull(pageData); Assert.assertNotNull(pageData);
Assert.assertEquals(1, pageData.getData().size()); Assert.assertEquals(1, pageData.getData().size());
Assert.assertEquals(device.getId(), pageData.getData().get(0).getEntityId()); Assert.assertEquals(device.getId(), pageData.getData().getFirst().getEntityId());
Assert.assertNotNull(pageData.getData().get(0).getLatest().get(EntityKeyType.TIME_SERIES).get("temperature")); Assert.assertNotNull(pageData.getData().getFirst().getLatest().get(EntityKeyType.TIME_SERIES).get("temperature"));
Assert.assertEquals(0, pageData.getData().get(0).getLatest().get(EntityKeyType.TIME_SERIES).get("temperature").getTs()); Assert.assertEquals(0, pageData.getData().getFirst().getLatest().get(EntityKeyType.TIME_SERIES).get("temperature").getTs());
Assert.assertEquals("", pageData.getData().get(0).getLatest().get(EntityKeyType.TIME_SERIES).get("temperature").getValue()); Assert.assertEquals("", pageData.getData().getFirst().getLatest().get(EntityKeyType.TIME_SERIES).get("temperature").getValue());
getWsClient().registerWaitForUpdate(); getWsClient().registerWaitForUpdate();
TsKvEntry dataPoint1 = new BasicTsKvEntry(now - TimeUnit.MINUTES.toMillis(1), new LongDataEntry("temperature", 42L)); TsKvEntry dataPoint1 = new BasicTsKvEntry(now - TimeUnit.MINUTES.toMillis(1), new LongDataEntry("temperature", 42L));
List<TsKvEntry> tsData = Arrays.asList(dataPoint1); List<TsKvEntry> tsData = List.of(dataPoint1);
sendTelemetry(device, tsData); sendTelemetry(device, tsData);
update = getWsClient().parseDataReply(getWsClient().waitForUpdate()); update = getWsClient().parseDataReply(getWsClient().waitForUpdate());
@ -694,38 +701,73 @@ public class WebsocketApiTest extends AbstractControllerTest {
List<EntityData> listData = update.getUpdate(); List<EntityData> listData = update.getUpdate();
Assert.assertNotNull(listData); Assert.assertNotNull(listData);
Assert.assertEquals(1, listData.size()); Assert.assertEquals(1, listData.size());
Assert.assertEquals(device.getId(), listData.get(0).getEntityId()); Assert.assertEquals(device.getId(), listData.getFirst().getEntityId());
Assert.assertNotNull(listData.get(0).getLatest().get(EntityKeyType.TIME_SERIES)); Assert.assertNotNull(listData.getFirst().getLatest().get(EntityKeyType.TIME_SERIES));
TsValue tsValue = listData.get(0).getLatest().get(EntityKeyType.TIME_SERIES).get("temperature"); TsValue tsValue = listData.getFirst().getLatest().get(EntityKeyType.TIME_SERIES).get("temperature");
Assert.assertEquals(new TsValue(dataPoint1.getTs(), dataPoint1.getValueAsString()), tsValue); Assert.assertEquals(new TsValue(dataPoint1.getTs(), dataPoint1.getValueAsString()), tsValue);
now = System.currentTimeMillis(); now = System.currentTimeMillis();
TsKvEntry dataPoint2 = new BasicTsKvEntry(now, new LongDataEntry("temperature", 52L)); TsKvEntry dataPoint2 = new BasicTsKvEntry(now, new LongDataEntry("temperature", 52L));
getWsClient().registerWaitForUpdate(); getWsClient().registerWaitForUpdate();
sendTelemetry(device, Arrays.asList(dataPoint2)); sendTelemetry(device, List.of(dataPoint2));
update = getWsClient().parseDataReply(getWsClient().waitForUpdate()); update = getWsClient().parseDataReply(getWsClient().waitForUpdate());
Assert.assertEquals(1, update.getCmdId()); Assert.assertEquals(1, update.getCmdId());
List<EntityData> eData = update.getUpdate(); List<EntityData> eData = update.getUpdate();
Assert.assertNotNull(eData); Assert.assertNotNull(eData);
Assert.assertEquals(1, eData.size()); Assert.assertEquals(1, eData.size());
Assert.assertEquals(device.getId(), eData.get(0).getEntityId()); Assert.assertEquals(device.getId(), eData.getFirst().getEntityId());
Assert.assertNotNull(eData.get(0).getLatest().get(EntityKeyType.TIME_SERIES)); Assert.assertNotNull(eData.getFirst().getLatest().get(EntityKeyType.TIME_SERIES));
tsValue = eData.get(0).getLatest().get(EntityKeyType.TIME_SERIES).get("temperature"); tsValue = eData.getFirst().getLatest().get(EntityKeyType.TIME_SERIES).get("temperature");
Assert.assertEquals(new TsValue(dataPoint2.getTs(), dataPoint2.getValueAsString()), tsValue); Assert.assertEquals(new TsValue(dataPoint2.getTs(), dataPoint2.getValueAsString()), tsValue);
//Sending update from the past, while latest value has new timestamp; //Sending update from the past, while the latest value has new timestamp;
getWsClient().registerWaitForUpdate(); getWsClient().registerWaitForUpdate();
sendTelemetry(device, Arrays.asList(dataPoint1)); sendTelemetry(device, List.of(dataPoint1));
String msg = getWsClient().waitForUpdate(TimeUnit.SECONDS.toMillis(1)); String msg = getWsClient().waitForUpdate(TimeUnit.SECONDS.toMillis(1));
Assert.assertNull(msg); Assert.assertNull(msg);
//Sending duplicate update again //Sending duplicate update again
getWsClient().registerWaitForUpdate(); getWsClient().registerWaitForUpdate();
sendTelemetry(device, Arrays.asList(dataPoint2)); sendTelemetry(device, List.of(dataPoint2));
msg = getWsClient().waitForUpdate(TimeUnit.SECONDS.toMillis(1)); msg = getWsClient().waitForUpdate(TimeUnit.SECONDS.toMillis(1));
Assert.assertNull(msg); Assert.assertNull(msg);
} }
@Test
public void testShouldSendWsUpdateMessageWhenTelemetryWasDeleted() throws Exception {
long now = System.currentTimeMillis() - 100;
TsKvEntry dataPoint = new BasicTsKvEntry(now, new LongDataEntry("temperature", 42L));
List<TsKvEntry> tsData = List.of(dataPoint);
sendTelemetry(device, tsData);
List<EntityKey> keys = List.of(new EntityKey(EntityKeyType.TIME_SERIES, "temperature"));
EntityDataUpdate update = getWsClient().subscribeLatestUpdate(keys, dtf);
Assert.assertEquals(1, update.getCmdId());
PageData<EntityData> pageData = update.getData();
Assert.assertNotNull(pageData);
Assert.assertEquals(1, pageData.getData().size());
Assert.assertEquals(device.getId(), pageData.getData().get(0).getEntityId());
Assert.assertNotNull(pageData.getData().get(0).getLatest().get(EntityKeyType.TIME_SERIES).get("temperature"));
Assert.assertEquals(now, pageData.getData().get(0).getLatest().get(EntityKeyType.TIME_SERIES).get("temperature").getTs());
Assert.assertEquals("42", pageData.getData().get(0).getLatest().get(EntityKeyType.TIME_SERIES).get("temperature").getValue());
// delete telemetry
getWsClient().registerWaitForUpdate();
doDeleteAsync("/api/plugins/telemetry/DEVICE/" + device.getId() + "/timeseries/delete?keys=temperature&deleteAllDataForKeys=true", String.class);
update = getWsClient().parseDataReply(getWsClient().waitForUpdate());
Assert.assertEquals(1, update.getCmdId());
List<EntityData> listData = update.getUpdate();
Assert.assertNotNull(listData);
Assert.assertEquals(1, listData.size());
Assert.assertEquals(device.getId(), listData.get(0).getEntityId());
Assert.assertNotNull(listData.get(0).getLatest().get(EntityKeyType.TIME_SERIES));
TsValue tsValue = listData.get(0).getLatest().get(EntityKeyType.TIME_SERIES).get("temperature");
Assert.assertEquals(new TsValue(0, ""), tsValue);
}
@Test @Test
public void testEntityDataLatestAttrWsCmd() throws Exception { public void testEntityDataLatestAttrWsCmd() throws Exception {
long now = System.currentTimeMillis(); long now = System.currentTimeMillis();
@ -736,21 +778,21 @@ public class WebsocketApiTest extends AbstractControllerTest {
PageData<EntityData> pageData = update.getData(); PageData<EntityData> pageData = update.getData();
Assert.assertNotNull(pageData); Assert.assertNotNull(pageData);
Assert.assertEquals(1, pageData.getData().size()); Assert.assertEquals(1, pageData.getData().size());
Assert.assertEquals(device.getId(), pageData.getData().get(0).getEntityId()); Assert.assertEquals(device.getId(), pageData.getData().getFirst().getEntityId());
Assert.assertNotNull(pageData.getData().get(0).getLatest().get(EntityKeyType.SERVER_ATTRIBUTE).get("serverAttributeKey")); Assert.assertNotNull(pageData.getData().getFirst().getLatest().get(EntityKeyType.SERVER_ATTRIBUTE).get("serverAttributeKey"));
Assert.assertEquals(0, pageData.getData().get(0).getLatest().get(EntityKeyType.SERVER_ATTRIBUTE).get("serverAttributeKey").getTs()); Assert.assertEquals(0, pageData.getData().getFirst().getLatest().get(EntityKeyType.SERVER_ATTRIBUTE).get("serverAttributeKey").getTs());
Assert.assertEquals("", pageData.getData().get(0).getLatest().get(EntityKeyType.SERVER_ATTRIBUTE).get("serverAttributeKey").getValue()); Assert.assertEquals("", pageData.getData().getFirst().getLatest().get(EntityKeyType.SERVER_ATTRIBUTE).get("serverAttributeKey").getValue());
getWsClient().registerWaitForUpdate(); getWsClient().registerWaitForUpdate();
// Pushing update with wrong scope and make sure it will not arrive. // Pushing update with the wrong scope and make sure it will not arrive.
AttributeKvEntry invalidDataPoint = new BaseAttributeKvEntry(now - TimeUnit.MINUTES.toMillis(1), new LongDataEntry("serverAttributeKey", 55L)); AttributeKvEntry invalidDataPoint = new BaseAttributeKvEntry(now - TimeUnit.MINUTES.toMillis(1), new LongDataEntry("serverAttributeKey", 55L));
sendAttributes(device, TbAttributeSubscriptionScope.CLIENT_SCOPE, Arrays.asList(invalidDataPoint)); sendAttributes(device, TbAttributeSubscriptionScope.CLIENT_SCOPE, List.of(invalidDataPoint));
Assert.assertNull(getWsClient().waitForUpdate(3000)); Assert.assertNull(getWsClient().waitForUpdate(3000));
AttributeKvEntry dataPoint1 = new BaseAttributeKvEntry(now - TimeUnit.MINUTES.toMillis(1), new LongDataEntry("serverAttributeKey", 42L)); AttributeKvEntry dataPoint1 = new BaseAttributeKvEntry(now - TimeUnit.MINUTES.toMillis(1), new LongDataEntry("serverAttributeKey", 42L));
List<AttributeKvEntry> tsData = Arrays.asList(dataPoint1); List<AttributeKvEntry> tsData = List.of(dataPoint1);
sendAttributes(device, TbAttributeSubscriptionScope.SERVER_SCOPE, tsData); sendAttributes(device, TbAttributeSubscriptionScope.SERVER_SCOPE, tsData);
String msg = getWsClient().waitForUpdate(); String msg = getWsClient().waitForUpdate();
@ -761,16 +803,16 @@ public class WebsocketApiTest extends AbstractControllerTest {
List<EntityData> listData = update.getUpdate(); List<EntityData> listData = update.getUpdate();
Assert.assertNotNull(listData); Assert.assertNotNull(listData);
Assert.assertEquals(1, listData.size()); Assert.assertEquals(1, listData.size());
Assert.assertEquals(device.getId(), listData.get(0).getEntityId()); Assert.assertEquals(device.getId(), listData.getFirst().getEntityId());
Assert.assertNotNull(listData.get(0).getLatest().get(EntityKeyType.SERVER_ATTRIBUTE)); Assert.assertNotNull(listData.getFirst().getLatest().get(EntityKeyType.SERVER_ATTRIBUTE));
TsValue tsValue = listData.get(0).getLatest().get(EntityKeyType.SERVER_ATTRIBUTE).get("serverAttributeKey"); TsValue tsValue = listData.getFirst().getLatest().get(EntityKeyType.SERVER_ATTRIBUTE).get("serverAttributeKey");
Assert.assertEquals(new TsValue(dataPoint1.getLastUpdateTs(), dataPoint1.getValueAsString()), tsValue); Assert.assertEquals(new TsValue(dataPoint1.getLastUpdateTs(), dataPoint1.getValueAsString()), tsValue);
now = System.currentTimeMillis(); now = System.currentTimeMillis();
AttributeKvEntry dataPoint2 = new BaseAttributeKvEntry(now, new LongDataEntry("serverAttributeKey", 52L)); AttributeKvEntry dataPoint2 = new BaseAttributeKvEntry(now, new LongDataEntry("serverAttributeKey", 52L));
getWsClient().registerWaitForUpdate(); getWsClient().registerWaitForUpdate();
sendAttributes(device, TbAttributeSubscriptionScope.SERVER_SCOPE, Arrays.asList(dataPoint2)); sendAttributes(device, TbAttributeSubscriptionScope.SERVER_SCOPE, List.of(dataPoint2));
msg = getWsClient().waitForUpdate(); msg = getWsClient().waitForUpdate();
Assert.assertNotNull(msg); Assert.assertNotNull(msg);
@ -779,20 +821,20 @@ public class WebsocketApiTest extends AbstractControllerTest {
List<EntityData> eData = update.getUpdate(); List<EntityData> eData = update.getUpdate();
Assert.assertNotNull(eData); Assert.assertNotNull(eData);
Assert.assertEquals(1, eData.size()); Assert.assertEquals(1, eData.size());
Assert.assertEquals(device.getId(), eData.get(0).getEntityId()); Assert.assertEquals(device.getId(), eData.getFirst().getEntityId());
Assert.assertNotNull(eData.get(0).getLatest().get(EntityKeyType.SERVER_ATTRIBUTE)); Assert.assertNotNull(eData.getFirst().getLatest().get(EntityKeyType.SERVER_ATTRIBUTE));
tsValue = eData.get(0).getLatest().get(EntityKeyType.SERVER_ATTRIBUTE).get("serverAttributeKey"); tsValue = eData.getFirst().getLatest().get(EntityKeyType.SERVER_ATTRIBUTE).get("serverAttributeKey");
Assert.assertEquals(new TsValue(dataPoint2.getLastUpdateTs(), dataPoint2.getValueAsString()), tsValue); Assert.assertEquals(new TsValue(dataPoint2.getLastUpdateTs(), dataPoint2.getValueAsString()), tsValue);
//Sending update from the past, while latest value has new timestamp; //Sending update from the past, while the latest value has new timestamp;
getWsClient().registerWaitForUpdate(); getWsClient().registerWaitForUpdate();
sendAttributes(device, TbAttributeSubscriptionScope.SERVER_SCOPE, Arrays.asList(dataPoint1)); sendAttributes(device, TbAttributeSubscriptionScope.SERVER_SCOPE, List.of(dataPoint1));
msg = getWsClient().waitForUpdate(TimeUnit.SECONDS.toMillis(1)); msg = getWsClient().waitForUpdate(TimeUnit.SECONDS.toMillis(1));
Assert.assertNull(msg); Assert.assertNull(msg);
//Sending duplicate update again //Sending duplicate update again
getWsClient().registerWaitForUpdate(); getWsClient().registerWaitForUpdate();
sendAttributes(device, TbAttributeSubscriptionScope.SERVER_SCOPE, Arrays.asList(dataPoint2)); sendAttributes(device, TbAttributeSubscriptionScope.SERVER_SCOPE, List.of(dataPoint2));
msg = getWsClient().waitForUpdate(TimeUnit.SECONDS.toMillis(1)); msg = getWsClient().waitForUpdate(TimeUnit.SECONDS.toMillis(1));
Assert.assertNull(msg); Assert.assertNull(msg);
} }
@ -812,23 +854,23 @@ public class WebsocketApiTest extends AbstractControllerTest {
PageData<EntityData> pageData = update.getData(); PageData<EntityData> pageData = update.getData();
Assert.assertNotNull(pageData); Assert.assertNotNull(pageData);
Assert.assertEquals(1, pageData.getData().size()); Assert.assertEquals(1, pageData.getData().size());
Assert.assertEquals(device.getId(), pageData.getData().get(0).getEntityId()); Assert.assertEquals(device.getId(), pageData.getData().getFirst().getEntityId());
Assert.assertNotNull(pageData.getData().get(0).getLatest().get(EntityKeyType.SERVER_ATTRIBUTE).get("serverAttributeKey")); Assert.assertNotNull(pageData.getData().getFirst().getLatest().get(EntityKeyType.SERVER_ATTRIBUTE).get("serverAttributeKey"));
Assert.assertEquals(0, pageData.getData().get(0).getLatest().get(EntityKeyType.SERVER_ATTRIBUTE).get("serverAttributeKey").getTs()); Assert.assertEquals(0, pageData.getData().getFirst().getLatest().get(EntityKeyType.SERVER_ATTRIBUTE).get("serverAttributeKey").getTs());
Assert.assertEquals("", pageData.getData().get(0).getLatest().get(EntityKeyType.SERVER_ATTRIBUTE).get("serverAttributeKey").getValue()); Assert.assertEquals("", pageData.getData().getFirst().getLatest().get(EntityKeyType.SERVER_ATTRIBUTE).get("serverAttributeKey").getValue());
Assert.assertNotNull(pageData.getData().get(0).getLatest().get(EntityKeyType.CLIENT_ATTRIBUTE).get("clientAttributeKey")); Assert.assertNotNull(pageData.getData().getFirst().getLatest().get(EntityKeyType.CLIENT_ATTRIBUTE).get("clientAttributeKey"));
Assert.assertEquals(0, pageData.getData().get(0).getLatest().get(EntityKeyType.CLIENT_ATTRIBUTE).get("clientAttributeKey").getTs()); Assert.assertEquals(0, pageData.getData().getFirst().getLatest().get(EntityKeyType.CLIENT_ATTRIBUTE).get("clientAttributeKey").getTs());
Assert.assertEquals("", pageData.getData().get(0).getLatest().get(EntityKeyType.CLIENT_ATTRIBUTE).get("clientAttributeKey").getValue()); Assert.assertEquals("", pageData.getData().getFirst().getLatest().get(EntityKeyType.CLIENT_ATTRIBUTE).get("clientAttributeKey").getValue());
Assert.assertNotNull(pageData.getData().get(0).getLatest().get(EntityKeyType.SHARED_ATTRIBUTE).get("sharedAttributeKey")); Assert.assertNotNull(pageData.getData().getFirst().getLatest().get(EntityKeyType.SHARED_ATTRIBUTE).get("sharedAttributeKey"));
Assert.assertEquals(0, pageData.getData().get(0).getLatest().get(EntityKeyType.SHARED_ATTRIBUTE).get("sharedAttributeKey").getTs()); Assert.assertEquals(0, pageData.getData().getFirst().getLatest().get(EntityKeyType.SHARED_ATTRIBUTE).get("sharedAttributeKey").getTs());
Assert.assertEquals("", pageData.getData().get(0).getLatest().get(EntityKeyType.SHARED_ATTRIBUTE).get("sharedAttributeKey").getValue()); Assert.assertEquals("", pageData.getData().getFirst().getLatest().get(EntityKeyType.SHARED_ATTRIBUTE).get("sharedAttributeKey").getValue());
Assert.assertNotNull(pageData.getData().get(0).getLatest().get(EntityKeyType.ATTRIBUTE).get("anyAttributeKey")); Assert.assertNotNull(pageData.getData().getFirst().getLatest().get(EntityKeyType.ATTRIBUTE).get("anyAttributeKey"));
Assert.assertEquals(0, pageData.getData().get(0).getLatest().get(EntityKeyType.ATTRIBUTE).get("anyAttributeKey").getTs()); Assert.assertEquals(0, pageData.getData().getFirst().getLatest().get(EntityKeyType.ATTRIBUTE).get("anyAttributeKey").getTs());
Assert.assertEquals("", pageData.getData().get(0).getLatest().get(EntityKeyType.ATTRIBUTE).get("anyAttributeKey").getValue()); Assert.assertEquals("", pageData.getData().getFirst().getLatest().get(EntityKeyType.ATTRIBUTE).get("anyAttributeKey").getValue());
getWsClient().registerWaitForUpdate(); getWsClient().registerWaitForUpdate();
AttributeKvEntry dataPoint1 = new BaseAttributeKvEntry(now - TimeUnit.MINUTES.toMillis(1), new LongDataEntry("serverAttributeKey", 42L)); AttributeKvEntry dataPoint1 = new BaseAttributeKvEntry(now - TimeUnit.MINUTES.toMillis(1), new LongDataEntry("serverAttributeKey", 42L));
List<AttributeKvEntry> tsData = Arrays.asList(dataPoint1); List<AttributeKvEntry> tsData = List.of(dataPoint1);
sendAttributes(device, TbAttributeSubscriptionScope.SERVER_SCOPE, tsData); sendAttributes(device, TbAttributeSubscriptionScope.SERVER_SCOPE, tsData);
@ -839,78 +881,78 @@ public class WebsocketApiTest extends AbstractControllerTest {
List<EntityData> eData = update.getUpdate(); List<EntityData> eData = update.getUpdate();
Assert.assertNotNull(eData); Assert.assertNotNull(eData);
Assert.assertEquals(1, eData.size()); Assert.assertEquals(1, eData.size());
Assert.assertEquals(device.getId(), eData.get(0).getEntityId()); Assert.assertEquals(device.getId(), eData.getFirst().getEntityId());
Assert.assertNotNull(eData.get(0).getLatest().get(EntityKeyType.SERVER_ATTRIBUTE)); Assert.assertNotNull(eData.getFirst().getLatest().get(EntityKeyType.SERVER_ATTRIBUTE));
TsValue attrValue = eData.get(0).getLatest().get(EntityKeyType.SERVER_ATTRIBUTE).get("serverAttributeKey"); TsValue attrValue = eData.getFirst().getLatest().get(EntityKeyType.SERVER_ATTRIBUTE).get("serverAttributeKey");
Assert.assertEquals(new TsValue(dataPoint1.getLastUpdateTs(), dataPoint1.getValueAsString()), attrValue); Assert.assertEquals(new TsValue(dataPoint1.getLastUpdateTs(), dataPoint1.getValueAsString()), attrValue);
//Sending update from the past, while latest value has new timestamp; //Sending update from the past, while the latest value has new timestamp;
getWsClient().registerWaitForUpdate(); getWsClient().registerWaitForUpdate();
sendAttributes(device, TbAttributeSubscriptionScope.SHARED_SCOPE, Arrays.asList(dataPoint1)); sendAttributes(device, TbAttributeSubscriptionScope.SHARED_SCOPE, List.of(dataPoint1));
msg = getWsClient().waitForUpdate(TimeUnit.SECONDS.toMillis(1)); msg = getWsClient().waitForUpdate(TimeUnit.SECONDS.toMillis(1));
Assert.assertNull(msg); Assert.assertNull(msg);
//Sending duplicate update again //Sending duplicate update again
getWsClient().registerWaitForUpdate(); getWsClient().registerWaitForUpdate();
sendAttributes(device, TbAttributeSubscriptionScope.CLIENT_SCOPE, Arrays.asList(dataPoint1)); sendAttributes(device, TbAttributeSubscriptionScope.CLIENT_SCOPE, List.of(dataPoint1));
msg = getWsClient().waitForUpdate(TimeUnit.SECONDS.toMillis(1)); msg = getWsClient().waitForUpdate(TimeUnit.SECONDS.toMillis(1));
Assert.assertNull(msg); Assert.assertNull(msg);
//Sending update from the past, while latest value has new timestamp; //Sending update from the past, while latest value has new timestamp;
getWsClient().registerWaitForUpdate(); getWsClient().registerWaitForUpdate();
AttributeKvEntry dataPoint2 = new BaseAttributeKvEntry(now, new LongDataEntry("sharedAttributeKey", 42L)); AttributeKvEntry dataPoint2 = new BaseAttributeKvEntry(now, new LongDataEntry("sharedAttributeKey", 42L));
sendAttributes(device, TbAttributeSubscriptionScope.SHARED_SCOPE, Arrays.asList(dataPoint2)); sendAttributes(device, TbAttributeSubscriptionScope.SHARED_SCOPE, List.of(dataPoint2));
msg = getWsClient().waitForUpdate(TimeUnit.SECONDS.toMillis(1)); msg = getWsClient().waitForUpdate(TimeUnit.SECONDS.toMillis(1));
update = JacksonUtil.fromString(msg, EntityDataUpdate.class); update = JacksonUtil.fromString(msg, EntityDataUpdate.class);
Assert.assertEquals(1, update.getCmdId()); Assert.assertEquals(1, update.getCmdId());
eData = update.getUpdate(); eData = update.getUpdate();
Assert.assertNotNull(eData); Assert.assertNotNull(eData);
Assert.assertEquals(1, eData.size()); Assert.assertEquals(1, eData.size());
Assert.assertEquals(device.getId(), eData.get(0).getEntityId()); Assert.assertEquals(device.getId(), eData.getFirst().getEntityId());
Assert.assertNotNull(eData.get(0).getLatest().get(EntityKeyType.SHARED_ATTRIBUTE)); Assert.assertNotNull(eData.getFirst().getLatest().get(EntityKeyType.SHARED_ATTRIBUTE));
attrValue = eData.get(0).getLatest().get(EntityKeyType.SHARED_ATTRIBUTE).get("sharedAttributeKey"); attrValue = eData.getFirst().getLatest().get(EntityKeyType.SHARED_ATTRIBUTE).get("sharedAttributeKey");
Assert.assertEquals(new TsValue(dataPoint2.getLastUpdateTs(), dataPoint2.getValueAsString()), attrValue); Assert.assertEquals(new TsValue(dataPoint2.getLastUpdateTs(), dataPoint2.getValueAsString()), attrValue);
getWsClient().registerWaitForUpdate(); getWsClient().registerWaitForUpdate();
AttributeKvEntry dataPoint3 = new BaseAttributeKvEntry(now, new LongDataEntry("clientAttributeKey", 42L)); AttributeKvEntry dataPoint3 = new BaseAttributeKvEntry(now, new LongDataEntry("clientAttributeKey", 42L));
sendAttributes(device, TbAttributeSubscriptionScope.CLIENT_SCOPE, Arrays.asList(dataPoint3)); sendAttributes(device, TbAttributeSubscriptionScope.CLIENT_SCOPE, List.of(dataPoint3));
msg = getWsClient().waitForUpdate(TimeUnit.SECONDS.toMillis(1)); msg = getWsClient().waitForUpdate(TimeUnit.SECONDS.toMillis(1));
update = JacksonUtil.fromString(msg, EntityDataUpdate.class); update = JacksonUtil.fromString(msg, EntityDataUpdate.class);
Assert.assertEquals(1, update.getCmdId()); Assert.assertEquals(1, update.getCmdId());
eData = update.getUpdate(); eData = update.getUpdate();
Assert.assertNotNull(eData); Assert.assertNotNull(eData);
Assert.assertEquals(1, eData.size()); Assert.assertEquals(1, eData.size());
Assert.assertEquals(device.getId(), eData.get(0).getEntityId()); Assert.assertEquals(device.getId(), eData.getFirst().getEntityId());
Assert.assertNotNull(eData.get(0).getLatest().get(EntityKeyType.CLIENT_ATTRIBUTE)); Assert.assertNotNull(eData.getFirst().getLatest().get(EntityKeyType.CLIENT_ATTRIBUTE));
attrValue = eData.get(0).getLatest().get(EntityKeyType.CLIENT_ATTRIBUTE).get("clientAttributeKey"); attrValue = eData.getFirst().getLatest().get(EntityKeyType.CLIENT_ATTRIBUTE).get("clientAttributeKey");
Assert.assertEquals(new TsValue(dataPoint3.getLastUpdateTs(), dataPoint3.getValueAsString()), attrValue); Assert.assertEquals(new TsValue(dataPoint3.getLastUpdateTs(), dataPoint3.getValueAsString()), attrValue);
getWsClient().registerWaitForUpdate(); getWsClient().registerWaitForUpdate();
AttributeKvEntry dataPoint4 = new BaseAttributeKvEntry(now, new LongDataEntry("anyAttributeKey", 42L)); AttributeKvEntry dataPoint4 = new BaseAttributeKvEntry(now, new LongDataEntry("anyAttributeKey", 42L));
sendAttributes(device, TbAttributeSubscriptionScope.CLIENT_SCOPE, Arrays.asList(dataPoint4)); sendAttributes(device, TbAttributeSubscriptionScope.CLIENT_SCOPE, List.of(dataPoint4));
msg = getWsClient().waitForUpdate(TimeUnit.SECONDS.toMillis(1)); msg = getWsClient().waitForUpdate(TimeUnit.SECONDS.toMillis(1));
update = JacksonUtil.fromString(msg, EntityDataUpdate.class); update = JacksonUtil.fromString(msg, EntityDataUpdate.class);
Assert.assertEquals(1, update.getCmdId()); Assert.assertEquals(1, update.getCmdId());
eData = update.getUpdate(); eData = update.getUpdate();
Assert.assertNotNull(eData); Assert.assertNotNull(eData);
Assert.assertEquals(1, eData.size()); Assert.assertEquals(1, eData.size());
Assert.assertEquals(device.getId(), eData.get(0).getEntityId()); Assert.assertEquals(device.getId(), eData.getFirst().getEntityId());
Assert.assertNotNull(eData.get(0).getLatest().get(EntityKeyType.ATTRIBUTE)); Assert.assertNotNull(eData.getFirst().getLatest().get(EntityKeyType.ATTRIBUTE));
attrValue = eData.get(0).getLatest().get(EntityKeyType.ATTRIBUTE).get("anyAttributeKey"); attrValue = eData.getFirst().getLatest().get(EntityKeyType.ATTRIBUTE).get("anyAttributeKey");
Assert.assertEquals(new TsValue(dataPoint4.getLastUpdateTs(), dataPoint4.getValueAsString()), attrValue); Assert.assertEquals(new TsValue(dataPoint4.getLastUpdateTs(), dataPoint4.getValueAsString()), attrValue);
getWsClient().registerWaitForUpdate(); getWsClient().registerWaitForUpdate();
AttributeKvEntry dataPoint5 = new BaseAttributeKvEntry(now, new LongDataEntry("anyAttributeKey", 43L)); AttributeKvEntry dataPoint5 = new BaseAttributeKvEntry(now, new LongDataEntry("anyAttributeKey", 43L));
sendAttributes(device, TbAttributeSubscriptionScope.SERVER_SCOPE, Arrays.asList(dataPoint5)); sendAttributes(device, TbAttributeSubscriptionScope.SERVER_SCOPE, List.of(dataPoint5));
msg = getWsClient().waitForUpdate(TimeUnit.SECONDS.toMillis(1)); msg = getWsClient().waitForUpdate(TimeUnit.SECONDS.toMillis(1));
update = JacksonUtil.fromString(msg, EntityDataUpdate.class); update = JacksonUtil.fromString(msg, EntityDataUpdate.class);
Assert.assertEquals(1, update.getCmdId()); Assert.assertEquals(1, update.getCmdId());
eData = update.getUpdate(); eData = update.getUpdate();
Assert.assertNotNull(eData); Assert.assertNotNull(eData);
Assert.assertEquals(1, eData.size()); Assert.assertEquals(1, eData.size());
Assert.assertEquals(device.getId(), eData.get(0).getEntityId()); Assert.assertEquals(device.getId(), eData.getFirst().getEntityId());
Assert.assertNotNull(eData.get(0).getLatest().get(EntityKeyType.ATTRIBUTE)); Assert.assertNotNull(eData.getFirst().getLatest().get(EntityKeyType.ATTRIBUTE));
attrValue = eData.get(0).getLatest().get(EntityKeyType.ATTRIBUTE).get("anyAttributeKey"); attrValue = eData.getFirst().getLatest().get(EntityKeyType.ATTRIBUTE).get("anyAttributeKey");
Assert.assertEquals(new TsValue(dataPoint5.getLastUpdateTs(), dataPoint5.getValueAsString()), attrValue); Assert.assertEquals(new TsValue(dataPoint5.getLastUpdateTs(), dataPoint5.getValueAsString()), attrValue);
} }
@ -965,13 +1007,41 @@ public class WebsocketApiTest extends AbstractControllerTest {
} }
@Test
public void testHistoryCmdSendsWsErrorOnResultSetSizeLimitExceeded() throws Exception {
ResultSetSizeLimitExceededException exception = new ResultSetSizeLimitExceededException(100L, 200L);
Mockito.doReturn(Futures.immediateFailedFuture(exception))
.when(timeseriesService).findAllByQueries(Mockito.any(), Mockito.any(), Mockito.any());
List<String> keys = List.of("temperature");
long now = System.currentTimeMillis();
EntityDataUpdate errorUpdate = getWsClient().sendHistoryCmd(keys, now, TimeUnit.HOURS.toMillis(1), dtf);
assertThat(errorUpdate.getErrorCode()).isEqualTo(SubscriptionErrorCode.INTERNAL_ERROR.getCode());
assertThat(errorUpdate.getErrorMsg()).isEqualTo(exception.getMessage());
}
@Test
public void testTimeSeriesCmdSendsWsErrorOnResultSetSizeLimitExceeded() throws Exception {
ResultSetSizeLimitExceededException exception = new ResultSetSizeLimitExceededException(100L, 200L);
Mockito.doReturn(Futures.immediateFailedFuture(exception))
.when(timeseriesService).findAllByQueries(Mockito.any(), Mockito.any(), Mockito.any());
List<String> keys = List.of("temperature");
long now = System.currentTimeMillis();
EntityDataUpdate errorUpdate = getWsClient().subscribeTsUpdate(keys, now, TimeUnit.HOURS.toMillis(1), dtf);
assertThat(errorUpdate.getErrorCode()).isEqualTo(SubscriptionErrorCode.INTERNAL_ERROR.getCode());
assertThat(errorUpdate.getErrorMsg()).isEqualTo(exception.getMessage());
}
private void sendTelemetry(Device device, List<TsKvEntry> tsData) throws InterruptedException { private void sendTelemetry(Device device, List<TsKvEntry> tsData) throws InterruptedException {
CountDownLatch latch = new CountDownLatch(1); CountDownLatch latch = new CountDownLatch(1);
tsService.saveTimeseries(TimeseriesSaveRequest.builder() tsService.saveTimeseries(TimeseriesSaveRequest.builder()
.tenantId(device.getTenantId()) .tenantId(device.getTenantId())
.entityId(device.getId()) .entityId(device.getId())
.entries(tsData) .entries(tsData)
.callback(new FutureCallback<Void>() { .callback(new FutureCallback<>() {
@Override @Override
public void onSuccess(@Nullable Void result) { public void onSuccess(@Nullable Void result) {
log.debug("sendTelemetry callback onSuccess"); log.debug("sendTelemetry callback onSuccess");

106
application/src/test/java/org/thingsboard/server/controller/plugin/TbWebSocketHandlerTest.java

@ -25,16 +25,25 @@ import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test; import org.junit.jupiter.api.Test;
import org.mockito.Mockito; import org.mockito.Mockito;
import org.springframework.test.util.ReflectionTestUtils;
import org.springframework.web.socket.CloseStatus; import org.springframework.web.socket.CloseStatus;
import org.springframework.web.socket.adapter.NativeWebSocketSession; import org.springframework.web.socket.adapter.NativeWebSocketSession;
import org.thingsboard.common.util.ThingsBoardThreadFactory; import org.thingsboard.common.util.ThingsBoardThreadFactory;
import org.thingsboard.server.dao.tenant.TbTenantProfileCache;
import org.thingsboard.server.service.security.auth.jwt.JwtAuthenticationProvider;
import org.thingsboard.server.service.security.auth.pat.ApiKeyAuthenticationProvider;
import org.thingsboard.server.service.security.model.SecurityUser;
import org.thingsboard.server.service.ws.WebSocketService;
import org.thingsboard.server.service.ws.WebSocketSessionRef; import org.thingsboard.server.service.ws.WebSocketSessionRef;
import org.thingsboard.server.service.ws.WebSocketSessionType;
import java.io.IOException; import java.io.IOException;
import java.lang.reflect.Method;
import java.util.Collection; import java.util.Collection;
import java.util.Deque; import java.util.Deque;
import java.util.List; import java.util.List;
import java.util.Random; import java.util.Random;
import java.util.UUID;
import java.util.concurrent.ConcurrentLinkedDeque; import java.util.concurrent.ConcurrentLinkedDeque;
import java.util.concurrent.ConcurrentLinkedQueue; import java.util.concurrent.ConcurrentLinkedQueue;
import java.util.concurrent.CountDownLatch; import java.util.concurrent.CountDownLatch;
@ -184,4 +193,101 @@ class TbWebSocketHandlerTest {
assertThat(msgs).map(Integer::parseInt).doesNotHaveDuplicates().hasSize(100); assertThat(msgs).map(Integer::parseInt).doesNotHaveDuplicates().hasSize(100);
} }
private AuthTestFixture createAuthTestFixture() throws IOException {
TbWebSocketHandler handler = spy(new TbWebSocketHandler());
willDoNothing().given(handler).close(any(), any());
ApiKeyAuthenticationProvider apiKeyProvider = mock(ApiKeyAuthenticationProvider.class);
JwtAuthenticationProvider jwtProvider = mock(JwtAuthenticationProvider.class);
WebSocketService wsService = mock(WebSocketService.class);
ReflectionTestUtils.setField(handler, "apiKeyAuthenticationProvider", apiKeyProvider);
ReflectionTestUtils.setField(handler, "authenticationProvider", jwtProvider);
ReflectionTestUtils.setField(handler, "webSocketService", wsService);
ReflectionTestUtils.setField(handler, "tenantProfileCache", mock(TbTenantProfileCache.class));
ReflectionTestUtils.setField(handler, "authTimeoutMs", 10000);
ReflectionTestUtils.invokeMethod(handler, "init");
WebSocketSessionRef ref = WebSocketSessionRef.builder()
.sessionId(UUID.randomUUID().toString())
.sessionType(WebSocketSessionType.GENERAL)
.build();
NativeWebSocketSession wsSession = mock(NativeWebSocketSession.class);
Session nativeSess = mock(Session.class);
willReturn(nativeSess).given(wsSession).getNativeSession(Session.class);
RemoteEndpoint.Async async = mock(RemoteEndpoint.Async.class);
willReturn(async).given(nativeSess).getAsyncRemote();
willReturn("test-session-id").given(wsSession).getId();
TbWebSocketHandler.SessionMetaData sessionMd = handler.new SessionMetaData(wsSession, ref);
return new AuthTestFixture(handler, apiKeyProvider, jwtProvider, ref, sessionMd);
}
@Test
void processMsg_authenticatesWithApiKey() throws Exception {
AuthTestFixture f = createAuthTestFixture();
SecurityUser securityUser = mock(SecurityUser.class, Mockito.RETURNS_DEEP_STUBS);
willReturn(securityUser).given(f.apiKeyProvider).authenticate("my-api-key");
String msg = "{\"authCmd\":{\"cmdId\":1,\"apiKey\":\"my-api-key\"},\"cmds\":[]}";
f.handler.processMsg(f.sessionMd, msg);
verify(f.apiKeyProvider).authenticate("my-api-key");
verify(f.jwtProvider, never()).authenticate(anyString());
assertThat(f.ref.getSecurityCtx()).isSameAs(securityUser);
}
@Test
void processMsg_authenticatesWithJwtToken() throws Exception {
AuthTestFixture f = createAuthTestFixture();
SecurityUser securityUser = mock(SecurityUser.class, Mockito.RETURNS_DEEP_STUBS);
willReturn(securityUser).given(f.jwtProvider).authenticate("my-jwt-token");
String msg = "{\"authCmd\":{\"cmdId\":1,\"token\":\"my-jwt-token\"},\"cmds\":[]}";
f.handler.processMsg(f.sessionMd, msg);
verify(f.jwtProvider).authenticate("my-jwt-token");
verify(f.apiKeyProvider, never()).authenticate(anyString());
assertThat(f.ref.getSecurityCtx()).isSameAs(securityUser);
}
@Test
void processMsg_apiKeyTakesPrecedenceOverToken() throws Exception {
AuthTestFixture f = createAuthTestFixture();
SecurityUser securityUser = mock(SecurityUser.class, Mockito.RETURNS_DEEP_STUBS);
willReturn(securityUser).given(f.apiKeyProvider).authenticate("my-api-key");
String msg = "{\"authCmd\":{\"cmdId\":1,\"apiKey\":\"my-api-key\",\"token\":\"my-jwt-token\"},\"cmds\":[]}";
f.handler.processMsg(f.sessionMd, msg);
verify(f.apiKeyProvider).authenticate("my-api-key");
verify(f.jwtProvider, never()).authenticate(anyString());
assertThat(f.ref.getSecurityCtx()).isSameAs(securityUser);
}
@Test
void extractQueryParam_parsesCorrectly() throws Exception {
TbWebSocketHandler handler = new TbWebSocketHandler();
Method method = TbWebSocketHandler.class.getDeclaredMethod("extractQueryParam", String.class, String.class);
method.setAccessible(true);
assertThat(method.invoke(handler, "token=jwt123", "token")).isEqualTo("jwt123");
assertThat(method.invoke(handler, "token=jwt123&other=abc123", "token")).isEqualTo("jwt123");
assertThat(method.invoke(handler, "other=value", "token")).isNull();
assertThat(method.invoke(handler, "tokenExtra=value", "token")).isNull();
}
private record AuthTestFixture(
TbWebSocketHandler handler,
ApiKeyAuthenticationProvider apiKeyProvider,
JwtAuthenticationProvider jwtProvider,
WebSocketSessionRef ref,
TbWebSocketHandler.SessionMetaData sessionMd
) {}
} }

238
application/src/test/java/org/thingsboard/server/edge/ApiKeyEdgeTest.java

@ -0,0 +1,238 @@
/**
* 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.edge;
import com.datastax.oss.driver.api.core.uuid.Uuids;
import com.google.protobuf.AbstractMessage;
import com.google.protobuf.InvalidProtocolBufferException;
import org.junit.Assert;
import org.junit.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.thingsboard.common.util.JacksonUtil;
import org.thingsboard.server.common.data.User;
import org.thingsboard.server.common.data.id.ApiKeyId;
import org.thingsboard.server.common.data.pat.ApiKey;
import org.thingsboard.server.common.data.pat.ApiKeyInfo;
import org.thingsboard.server.common.data.security.Authority;
import org.thingsboard.server.dao.pat.ApiKeyService;
import org.thingsboard.server.dao.service.DaoSqlTest;
import org.thingsboard.server.gen.edge.v1.ApiKeyUpdateMsg;
import org.thingsboard.server.gen.edge.v1.UpdateMsgType;
import org.thingsboard.server.gen.edge.v1.UplinkMsg;
import org.thingsboard.server.gen.edge.v1.UplinkResponseMsg;
import org.thingsboard.server.gen.edge.v1.UserCredentialsUpdateMsg;
import org.thingsboard.server.gen.edge.v1.UserUpdateMsg;
import java.util.Optional;
import java.util.UUID;
import java.util.concurrent.TimeUnit;
import static org.awaitility.Awaitility.await;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
import static org.thingsboard.server.gen.edge.v1.UpdateMsgType.ENTITY_DELETED_RPC_MESSAGE;
@DaoSqlTest
public class ApiKeyEdgeTest extends AbstractEdgeTest {
@Autowired
private ApiKeyService apiKeyService;
private static final String DEFAULT_API_KEY_DESCRIPTION = "Edge Test ApiKey";
private static final String UPDATED_API_KEY_DESCRIPTION = "Updated Edge Test ApiKey";
@Test
public void testApiKey_create_update_delete_fromCloud() throws Exception {
// create ApiKey
ApiKeyInfo apiKeyInfo = createSimpleApiKeyInfo(DEFAULT_API_KEY_DESCRIPTION);
edgeImitator.expectMessageAmount(1);
ApiKey savedApiKey = doPost("/api/apiKey", apiKeyInfo, ApiKey.class);
Assert.assertTrue(edgeImitator.waitForMessages());
AbstractMessage latestMessage = edgeImitator.getLatestMessage();
Assert.assertTrue(latestMessage instanceof ApiKeyUpdateMsg);
ApiKeyUpdateMsg apiKeyUpdateMsg = (ApiKeyUpdateMsg) latestMessage;
Assert.assertEquals(UpdateMsgType.ENTITY_CREATED_RPC_MESSAGE, apiKeyUpdateMsg.getMsgType());
Assert.assertEquals(savedApiKey.getUuidId().getMostSignificantBits(), apiKeyUpdateMsg.getIdMSB());
Assert.assertEquals(savedApiKey.getUuidId().getLeastSignificantBits(), apiKeyUpdateMsg.getIdLSB());
ApiKey apiKeyFromMsg = JacksonUtil.fromString(apiKeyUpdateMsg.getEntity(), ApiKey.class, true);
Assert.assertNotNull(apiKeyFromMsg);
Assert.assertEquals(DEFAULT_API_KEY_DESCRIPTION, apiKeyFromMsg.getDescription());
Assert.assertEquals(savedApiKey.getTenantId(), apiKeyFromMsg.getTenantId());
// update ApiKey
edgeImitator.expectMessageAmount(1);
savedApiKey.setDescription(UPDATED_API_KEY_DESCRIPTION);
savedApiKey = doPost("/api/apiKey", new ApiKeyInfo(savedApiKey), ApiKey.class);
Assert.assertTrue(edgeImitator.waitForMessages());
latestMessage = edgeImitator.getLatestMessage();
Assert.assertTrue(latestMessage instanceof ApiKeyUpdateMsg);
apiKeyUpdateMsg = (ApiKeyUpdateMsg) latestMessage;
apiKeyFromMsg = JacksonUtil.fromString(apiKeyUpdateMsg.getEntity(), ApiKey.class, true);
Assert.assertNotNull(apiKeyFromMsg);
Assert.assertEquals(UpdateMsgType.ENTITY_UPDATED_RPC_MESSAGE, apiKeyUpdateMsg.getMsgType());
Assert.assertEquals(UPDATED_API_KEY_DESCRIPTION, apiKeyFromMsg.getDescription());
// delete ApiKey
edgeImitator.expectMessageAmount(1);
doDelete("/api/apiKey/" + savedApiKey.getUuidId())
.andExpect(status().isOk());
Assert.assertTrue(edgeImitator.waitForMessages());
latestMessage = edgeImitator.getLatestMessage();
Assert.assertTrue(latestMessage instanceof ApiKeyUpdateMsg);
apiKeyUpdateMsg = (ApiKeyUpdateMsg) latestMessage;
Assert.assertEquals(UpdateMsgType.ENTITY_DELETED_RPC_MESSAGE, apiKeyUpdateMsg.getMsgType());
Assert.assertEquals(savedApiKey.getUuidId().getMostSignificantBits(), apiKeyUpdateMsg.getIdMSB());
Assert.assertEquals(savedApiKey.getUuidId().getLeastSignificantBits(), apiKeyUpdateMsg.getIdLSB());
}
@Test
public void testApiKey_create_update_delete_toCloud() throws Exception {
// create
ApiKey apiKey = createSimpleApiKey(DEFAULT_API_KEY_DESCRIPTION);
UUID uuid = Uuids.timeBased();
UplinkMsg uplinkMsg = getUplinkMsg(uuid, apiKey, UpdateMsgType.ENTITY_CREATED_RPC_MESSAGE);
checkApiKeyOnCloud(uplinkMsg, uuid, apiKey.getDescription());
// update
apiKey.setDescription(UPDATED_API_KEY_DESCRIPTION);
UplinkMsg updatedUplinkMsg = getUplinkMsg(uuid, apiKey, UpdateMsgType.ENTITY_UPDATED_RPC_MESSAGE);
checkApiKeyOnCloud(updatedUplinkMsg, uuid, apiKey.getDescription());
// delete
UplinkMsg deleteUplinkMsg = getDeleteUplinkMsg(uuid);
edgeImitator.expectResponsesAmount(1);
edgeImitator.sendUplinkMsg(deleteUplinkMsg);
Assert.assertTrue(edgeImitator.waitForResponses());
ApiKeyId apiKeyId = new ApiKeyId(uuid);
await().atMost(30, TimeUnit.SECONDS).untilAsserted(() ->
Assert.assertNull(apiKeyService.findApiKeyById(tenantId, apiKeyId))
);
}
@Test
public void testApiKey_pushedDuringUserSync() throws Exception {
// create tenant admin user - expect 3 messages: 1 UserUpdateMsg + 2 UserCredentialsUpdateMsg
User user = new User();
user.setAuthority(Authority.TENANT_ADMIN);
user.setTenantId(tenantId);
user.setEmail("apiKeyTestUser@thingsboard.org");
user.setFirstName("ApiKey");
user.setLastName("TestUser");
edgeImitator.expectMessageAmount(3);
User savedUser = createUser(user, "tenant");
Assert.assertTrue(edgeImitator.waitForMessages());
Assert.assertEquals(1, edgeImitator.findAllMessagesByType(UserUpdateMsg.class).size());
Assert.assertEquals(2, edgeImitator.findAllMessagesByType(UserCredentialsUpdateMsg.class).size());
// create API key for this user - expect 1 ApiKeyUpdateMsg
ApiKeyInfo apiKeyInfo = new ApiKeyInfo();
apiKeyInfo.setTenantId(tenantId);
apiKeyInfo.setUserId(savedUser.getId());
apiKeyInfo.setDescription("Test API Key for user sync");
apiKeyInfo.setEnabled(true);
edgeImitator.expectMessageAmount(1);
doPost("/api/apiKey", apiKeyInfo, ApiKey.class);
Assert.assertTrue(edgeImitator.waitForMessages());
Assert.assertEquals(1, edgeImitator.findAllMessagesByType(ApiKeyUpdateMsg.class).size());
// update user - expect 3 messages: UserUpdateMsg + UserCredentialsUpdateMsg + ApiKeyUpdateMsg
savedUser.setLastName("UpdatedLastName");
edgeImitator.expectMessageAmount(3);
doPost("/api/user", savedUser, User.class);
Assert.assertTrue(edgeImitator.waitForMessages());
Assert.assertEquals(1, edgeImitator.findAllMessagesByType(UserUpdateMsg.class).size());
Assert.assertEquals(1, edgeImitator.findAllMessagesByType(UserCredentialsUpdateMsg.class).size());
Assert.assertEquals(1, edgeImitator.findAllMessagesByType(ApiKeyUpdateMsg.class).size());
Optional<ApiKeyUpdateMsg> apiKeyUpdateMsgOpt = edgeImitator.findMessageByType(ApiKeyUpdateMsg.class);
Assert.assertTrue(apiKeyUpdateMsgOpt.isPresent());
ApiKeyUpdateMsg apiKeyUpdateMsg = apiKeyUpdateMsgOpt.get();
Assert.assertEquals(UpdateMsgType.ENTITY_UPDATED_RPC_MESSAGE, apiKeyUpdateMsg.getMsgType());
}
private ApiKeyInfo createSimpleApiKeyInfo(String description) {
ApiKeyInfo apiKeyInfo = new ApiKeyInfo();
apiKeyInfo.setTenantId(tenantId);
apiKeyInfo.setUserId(tenantAdminUserId);
apiKeyInfo.setDescription(description);
apiKeyInfo.setEnabled(true);
return apiKeyInfo;
}
private ApiKey createSimpleApiKey(String description) {
ApiKey apiKey = new ApiKey();
apiKey.setTenantId(tenantId);
apiKey.setUserId(tenantAdminUserId);
apiKey.setDescription(description);
apiKey.setEnabled(true);
apiKey.setValue("test-api-key-value-" + UUID.randomUUID());
return apiKey;
}
private UplinkMsg getDeleteUplinkMsg(UUID uuid) throws InvalidProtocolBufferException {
UplinkMsg.Builder upLinkMsgBuilder = UplinkMsg.newBuilder();
ApiKeyUpdateMsg.Builder apiKeyDeleteMsgBuilder = ApiKeyUpdateMsg.newBuilder();
apiKeyDeleteMsgBuilder.setMsgType(ENTITY_DELETED_RPC_MESSAGE);
apiKeyDeleteMsgBuilder.setIdMSB(uuid.getMostSignificantBits());
apiKeyDeleteMsgBuilder.setIdLSB(uuid.getLeastSignificantBits());
testAutoGeneratedCodeByProtobuf(apiKeyDeleteMsgBuilder);
upLinkMsgBuilder.addApiKeyUpdateMsg(apiKeyDeleteMsgBuilder.build());
testAutoGeneratedCodeByProtobuf(upLinkMsgBuilder);
return upLinkMsgBuilder.build();
}
private UplinkMsg getUplinkMsg(UUID uuid, ApiKey apiKey, UpdateMsgType updateMsgType) throws InvalidProtocolBufferException {
UplinkMsg.Builder uplinkMsgBuilder = UplinkMsg.newBuilder();
ApiKeyUpdateMsg.Builder apiKeyUpdateMsgBuilder = ApiKeyUpdateMsg.newBuilder();
apiKeyUpdateMsgBuilder.setIdMSB(uuid.getMostSignificantBits());
apiKeyUpdateMsgBuilder.setIdLSB(uuid.getLeastSignificantBits());
apiKeyUpdateMsgBuilder.setEntity(JacksonUtil.toString(apiKey));
apiKeyUpdateMsgBuilder.setMsgType(updateMsgType);
testAutoGeneratedCodeByProtobuf(apiKeyUpdateMsgBuilder);
uplinkMsgBuilder.addApiKeyUpdateMsg(apiKeyUpdateMsgBuilder.build());
testAutoGeneratedCodeByProtobuf(uplinkMsgBuilder);
return uplinkMsgBuilder.build();
}
private void checkApiKeyOnCloud(UplinkMsg uplinkMsg, UUID uuid, String description) throws Exception {
edgeImitator.expectResponsesAmount(1);
edgeImitator.sendUplinkMsg(uplinkMsg);
Assert.assertTrue(edgeImitator.waitForResponses());
UplinkResponseMsg latestResponseMsg = edgeImitator.getLatestResponseMsg();
Assert.assertTrue(latestResponseMsg.getSuccess());
ApiKey apiKey = apiKeyService.findApiKeyById(tenantId, new ApiKeyId(uuid));
Assert.assertNotNull(apiKey);
Assert.assertEquals(description, apiKey.getDescription());
}
}

3
application/src/test/java/org/thingsboard/server/edge/CalculatedFieldEdgeTest.java

@ -73,6 +73,7 @@ public class CalculatedFieldEdgeTest extends AbstractEdgeTest {
Assert.assertEquals(DEFAULT_CF_NAME, calculatedFieldFromMsg.getName()); Assert.assertEquals(DEFAULT_CF_NAME, calculatedFieldFromMsg.getName());
Assert.assertEquals(savedDevice.getId(), calculatedFieldFromMsg.getEntityId()); Assert.assertEquals(savedDevice.getId(), calculatedFieldFromMsg.getEntityId());
Assert.assertEquals(config, calculatedFieldFromMsg.getConfiguration()); Assert.assertEquals(config, calculatedFieldFromMsg.getConfiguration());
Assert.assertEquals(calculatedField.getAdditionalInfo(), calculatedFieldFromMsg.getAdditionalInfo());
edgeImitator.expectMessageAmount(1); edgeImitator.expectMessageAmount(1);
savedCalculatedField.setName(UPDATED_CF_NAME); savedCalculatedField.setName(UPDATED_CF_NAME);
@ -229,6 +230,7 @@ public class CalculatedFieldEdgeTest extends AbstractEdgeTest {
config.setOutput(output); config.setOutput(output);
calculatedField.setConfiguration(config); calculatedField.setConfiguration(config);
calculatedField.setAdditionalInfo(JacksonUtil.newObjectNode());
return calculatedField; return calculatedField;
} }
@ -260,6 +262,7 @@ public class CalculatedFieldEdgeTest extends AbstractEdgeTest {
CalculatedField calculatedField = doGet("/api/calculatedField/" + uuid, CalculatedField.class); CalculatedField calculatedField = doGet("/api/calculatedField/" + uuid, CalculatedField.class);
Assert.assertNotNull(calculatedField); Assert.assertNotNull(calculatedField);
Assert.assertEquals(resourceTitle, calculatedField.getName()); Assert.assertEquals(resourceTitle, calculatedField.getName());
Assert.assertEquals(JacksonUtil.newObjectNode(), calculatedField.getAdditionalInfo());
} }
} }

6
application/src/test/java/org/thingsboard/server/edge/imitator/EdgeImitator.java

@ -30,6 +30,7 @@ import org.thingsboard.edge.rpc.EdgeRpcClient;
import org.thingsboard.server.controller.AbstractWebTest; import org.thingsboard.server.controller.AbstractWebTest;
import org.thingsboard.server.gen.edge.v1.AdminSettingsUpdateMsg; import org.thingsboard.server.gen.edge.v1.AdminSettingsUpdateMsg;
import org.thingsboard.server.gen.edge.v1.AiModelUpdateMsg; import org.thingsboard.server.gen.edge.v1.AiModelUpdateMsg;
import org.thingsboard.server.gen.edge.v1.ApiKeyUpdateMsg;
import org.thingsboard.server.gen.edge.v1.AlarmCommentUpdateMsg; import org.thingsboard.server.gen.edge.v1.AlarmCommentUpdateMsg;
import org.thingsboard.server.gen.edge.v1.AlarmUpdateMsg; import org.thingsboard.server.gen.edge.v1.AlarmUpdateMsg;
import org.thingsboard.server.gen.edge.v1.AssetProfileUpdateMsg; import org.thingsboard.server.gen.edge.v1.AssetProfileUpdateMsg;
@ -375,6 +376,11 @@ public class EdgeImitator {
result.add(saveDownlinkMsg(aiModelUpdateMsg)); result.add(saveDownlinkMsg(aiModelUpdateMsg));
} }
} }
if (downlinkMsg.getApiKeyUpdateMsgCount() > 0) {
for (ApiKeyUpdateMsg apiKeyUpdateMsg : downlinkMsg.getApiKeyUpdateMsgList()) {
result.add(saveDownlinkMsg(apiKeyUpdateMsg));
}
}
if (downlinkMsg.hasEdgeConfiguration()) { if (downlinkMsg.hasEdgeConfiguration()) {
result.add(saveDownlinkMsg(downlinkMsg.getEdgeConfiguration())); result.add(saveDownlinkMsg(downlinkMsg.getEdgeConfiguration()));
} }

272
application/src/test/java/org/thingsboard/server/service/edge/rpc/EdgeGrpcSslTest.java

@ -0,0 +1,272 @@
/**
* 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.service.edge.rpc;
import io.grpc.ManagedChannel;
import io.grpc.Server;
import io.grpc.netty.shaded.io.grpc.netty.GrpcSslContexts;
import io.grpc.netty.shaded.io.grpc.netty.NettyChannelBuilder;
import io.grpc.netty.shaded.io.grpc.netty.NettyServerBuilder;
import org.bouncycastle.asn1.x500.X500Name;
import org.bouncycastle.cert.jcajce.JcaX509CertificateConverter;
import org.bouncycastle.cert.jcajce.JcaX509v3CertificateBuilder;
import org.bouncycastle.jce.provider.BouncyCastleProvider;
import org.bouncycastle.openssl.jcajce.JcaPEMWriter;
import org.bouncycastle.openssl.jcajce.JcePEMEncryptorBuilder;
import org.bouncycastle.operator.jcajce.JcaContentSignerBuilder;
import org.bouncycastle.util.io.pem.PemObject;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.EnumSource;
import org.springframework.test.util.ReflectionTestUtils;
import org.thingsboard.server.controller.AbstractWebTest;
import org.thingsboard.server.gen.edge.v1.EdgeRpcServiceGrpc;
import java.io.ByteArrayInputStream;
import java.math.BigInteger;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.Path;
import java.security.KeyPair;
import java.security.KeyPairGenerator;
import java.security.PrivateKey;
import java.security.Security;
import java.security.cert.X509Certificate;
import java.security.spec.ECGenParameterSpec;
import java.util.ArrayList;
import java.util.Date;
import java.util.List;
import java.util.concurrent.TimeUnit;
import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatThrownBy;
import static org.awaitility.Awaitility.await;
/**
* Tests for Edge gRPC SSL setup using the production {@link EdgeGrpcService#setupSsl} method.
* <p>
* Covers:
* 1. Separate cert and key PEM inputs
* 2. Combined PEM (cert + key in one file)
* 3. Encrypted private key with password
* 4. Missing key in combined PEM → error
* <p>
* Each scenario is parameterized across key types: RSA-2048, RSA-4096, EC P-256, EC P-384.
*/
class EdgeGrpcSslTest {
static {
if (Security.getProvider(BouncyCastleProvider.PROVIDER_NAME) == null) {
Security.addProvider(new BouncyCastleProvider());
}
}
enum KeyType {
RSA_2048("RSA", 2048, null, "SHA256withRSA"),
RSA_4096("RSA", 4096, null, "SHA256withRSA"),
EC_P256("EC", 256, "secp256r1", "SHA256withECDSA"),
EC_P384("EC", 384, "secp384r1", "SHA384withECDSA");
final String algorithm;
final int size;
final String curve;
final String sigAlg;
KeyType(String algorithm, int size, String curve, String sigAlg) {
this.algorithm = algorithm;
this.size = size;
this.curve = curve;
this.sigAlg = sigAlg;
}
KeyPair generateKeyPair() throws Exception {
KeyPairGenerator kpg = KeyPairGenerator.getInstance(algorithm);
if (curve != null) {
kpg.initialize(new ECGenParameterSpec(curve));
} else {
kpg.initialize(size);
}
return kpg.generateKeyPair();
}
}
private final List<Path> tempFiles = new ArrayList<>();
private Server server;
private ManagedChannel channel;
@AfterEach
void cleanup() throws Exception {
if (channel != null) {
channel.shutdownNow().awaitTermination(2, TimeUnit.SECONDS);
}
if (server != null) {
server.shutdownNow().awaitTermination(2, TimeUnit.SECONDS);
}
for (Path p : tempFiles) {
Files.deleteIfExists(p);
}
}
@ParameterizedTest(name = "separateCertAndKey_{0}")
@EnumSource(KeyType.class)
void separateCertAndKey(KeyType keyType) throws Exception {
KeyPair kp = keyType.generateKeyPair();
X509Certificate cert = generateSelfSignedCert(kp, keyType.sigAlg);
Path certFile = writeTempPem("cert", cert);
Path keyFile = writeTempPem("key", kp.getPrivate());
server = startServer(certFile.toString(), keyFile.toString(), null);
assertTlsConnectivity(cert);
}
@ParameterizedTest(name = "combinedPemWithCertAndKey_{0}")
@EnumSource(KeyType.class)
void combinedPemWithCertAndKey(KeyType keyType) throws Exception {
KeyPair kp = keyType.generateKeyPair();
X509Certificate cert = generateSelfSignedCert(kp, keyType.sigAlg);
Path combinedFile = writeTempPem("combined", cert, kp.getPrivate());
server = startServer(combinedFile.toString(), "", null);
assertTlsConnectivity(cert);
}
// RSA-only: BouncyCastle writes encrypted EC keys in traditional PEM format (BEGIN EC PRIVATE KEY),
// which after decryption produces a PEMKeyPair without public key info — causing PemSslCredentials
// to fail with "Cannot invoke SubjectPublicKeyInfo.getEncoded() because getPublicKeyInfo() is null".
@ParameterizedTest(name = "encryptedPrivateKey_{0}")
@EnumSource(value = KeyType.class, names = {"RSA_2048", "RSA_4096"})
void encryptedPrivateKey(KeyType keyType) throws Exception {
KeyPair kp = keyType.generateKeyPair();
X509Certificate cert = generateSelfSignedCert(kp, keyType.sigAlg);
String password = "test-password";
Path combinedFile = writeTempPemEncrypted("enc-combined", password, cert, kp.getPrivate());
server = startServer(combinedFile.toString(), "", password);
assertTlsConnectivity(cert);
}
@ParameterizedTest(name = "combinedPemWithCertOnly_throwsException_{0}")
@EnumSource(KeyType.class)
void combinedPemWithCertOnly_throwsException(KeyType keyType) throws Exception {
KeyPair kp = keyType.generateKeyPair();
X509Certificate cert = generateSelfSignedCert(kp, keyType.sigAlg);
Path certOnlyFile = writeTempPem("cert-only", cert);
assertThatThrownBy(() -> startServer(certOnlyFile.toString(), "", null))
.isInstanceOf(IllegalArgumentException.class);
}
// --- Server startup using production EdgeGrpcService.setupSsl() ---
private Server startServer(String certFileResource, String privateKeyResource, String keyPassword) throws Exception {
EdgeGrpcService edgeGrpcService = new EdgeGrpcService();
ReflectionTestUtils.setField(edgeGrpcService, "certFileResource", certFileResource);
ReflectionTestUtils.setField(edgeGrpcService, "privateKeyResource", privateKeyResource);
ReflectionTestUtils.setField(edgeGrpcService, "keyPassword", keyPassword != null ? keyPassword : "");
NettyServerBuilder builder = NettyServerBuilder.forPort(0)
.addService(new EdgeRpcServiceGrpc.EdgeRpcServiceImplBase() {});
edgeGrpcService.setupSsl(builder);
return builder.build().start();
}
private void assertTlsConnectivity(X509Certificate trustedCert) throws Exception {
String certPem = toPem(trustedCert);
var clientSsl = GrpcSslContexts.forClient()
.trustManager(new ByteArrayInputStream(certPem.getBytes(StandardCharsets.UTF_8)))
.build();
channel = NettyChannelBuilder.forAddress("localhost", server.getPort())
.sslContext(clientSsl)
.build();
channel.getState(true); // trigger connection attempt
await().atMost(AbstractWebTest.TIMEOUT, TimeUnit.SECONDS)
.pollInterval(50, TimeUnit.MILLISECONDS)
.untilAsserted(() -> {
var state = channel.getState(false);
if (state == io.grpc.ConnectivityState.TRANSIENT_FAILURE) {
throw new AssertionError("TLS handshake failed: channel in TRANSIENT_FAILURE");
}
assertThat(state).isEqualTo(io.grpc.ConnectivityState.READY);
});
}
// --- Cert/key generation ---
private X509Certificate generateSelfSignedCert(KeyPair kp, String sigAlg) throws Exception {
X500Name subject = new X500Name("CN=localhost");
Date now = new Date();
return new JcaX509CertificateConverter().getCertificate(
new JcaX509v3CertificateBuilder(
subject, BigInteger.ONE, now,
new Date(now.getTime() + TimeUnit.DAYS.toMillis(1)),
subject, kp.getPublic())
.build(new JcaContentSignerBuilder(sigAlg).build(kp.getPrivate())));
}
// --- PEM file helpers ---
private String toPem(Object obj) throws Exception {
java.io.StringWriter sw = new java.io.StringWriter();
try (JcaPEMWriter w = new JcaPEMWriter(sw)) {
w.writeObject(obj);
}
return sw.toString();
}
private Path writeTempPem(String prefix, Object... objects) throws Exception {
Path p = Files.createTempFile(prefix + "-", ".pem");
tempFiles.add(p);
try (JcaPEMWriter w = new JcaPEMWriter(Files.newBufferedWriter(p))) {
for (Object o : objects) {
w.writeObject(toPkcs8IfKey(o));
}
}
return p;
}
private Path writeTempPemEncrypted(String prefix, String password, Object... objects) throws Exception {
Path p = Files.createTempFile(prefix + "-", ".pem");
tempFiles.add(p);
var encryptor = new JcePEMEncryptorBuilder("AES-256-CBC")
.setProvider(BouncyCastleProvider.PROVIDER_NAME)
.build(password.toCharArray());
try (JcaPEMWriter w = new JcaPEMWriter(Files.newBufferedWriter(p))) {
for (Object o : objects) {
if (o instanceof PrivateKey) {
w.writeObject(o, encryptor);
} else {
w.writeObject(o);
}
}
}
return p;
}
private Object toPkcs8IfKey(Object o) {
if (o instanceof PrivateKey pk) {
return new PemObject("PRIVATE KEY", pk.getEncoded());
}
return o;
}
}

8
application/src/test/java/org/thingsboard/server/service/entitiy/EntityServiceTest.java

@ -115,6 +115,8 @@ import java.util.Map;
import java.util.Random; import java.util.Random;
import java.util.UUID; import java.util.UUID;
import java.util.concurrent.ExecutionException; import java.util.concurrent.ExecutionException;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.TimeoutException;
import java.util.stream.Collectors; import java.util.stream.Collectors;
import java.util.stream.Stream; import java.util.stream.Stream;
@ -1749,13 +1751,13 @@ public class EntityServiceTest extends AbstractControllerTest {
} }
@Test @Test
public void testFindTenantTelemetry() { public void testFindTenantTelemetry() throws ExecutionException, InterruptedException, TimeoutException {
// save timeseries by sys admin // save timeseries by sys admin
BasicTsKvEntry timeseries = new BasicTsKvEntry(42L, new DoubleDataEntry("temperature", 45.5)); BasicTsKvEntry timeseries = new BasicTsKvEntry(42L, new DoubleDataEntry("temperature", 45.5));
timeseriesService.save(TenantId.SYS_TENANT_ID, tenantId, timeseries); timeseriesService.save(TenantId.SYS_TENANT_ID, tenantId, timeseries).get(TIMEOUT, TimeUnit.SECONDS);
AttributeKvEntry attr = new BaseAttributeKvEntry(new LongDataEntry("attr", 10L), 42L); AttributeKvEntry attr = new BaseAttributeKvEntry(new LongDataEntry("attr", 10L), 42L);
attributesService.save(TenantId.SYS_TENANT_ID, tenantId, SERVER_SCOPE, List.of(attr)); attributesService.save(TenantId.SYS_TENANT_ID, tenantId, SERVER_SCOPE, List.of(attr)).get(TIMEOUT, TimeUnit.SECONDS);
SingleEntityFilter singleEntityFilter = new SingleEntityFilter(); SingleEntityFilter singleEntityFilter = new SingleEntityFilter();
singleEntityFilter.setSingleEntity(AliasEntityId.fromEntityId(tenantId)); singleEntityFilter.setSingleEntity(AliasEntityId.fromEntityId(tenantId));

10
application/src/test/java/org/thingsboard/server/service/housekeeper/HousekeeperServiceTest.java

@ -23,8 +23,8 @@ import org.junit.Test;
import org.mockito.ArgumentMatcher; import org.mockito.ArgumentMatcher;
import org.mockito.Mockito; import org.mockito.Mockito;
import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.mock.mockito.SpyBean;
import org.springframework.test.context.TestPropertySource; import org.springframework.test.context.TestPropertySource;
import org.springframework.test.context.bean.override.mockito.MockitoSpyBean;
import org.testcontainers.shaded.org.apache.commons.lang3.RandomStringUtils; import org.testcontainers.shaded.org.apache.commons.lang3.RandomStringUtils;
import org.thingsboard.common.util.JacksonUtil; import org.thingsboard.common.util.JacksonUtil;
import org.thingsboard.rule.engine.metadata.TbGetAttributesNode; import org.thingsboard.rule.engine.metadata.TbGetAttributesNode;
@ -127,10 +127,12 @@ import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.
}) })
public class HousekeeperServiceTest extends AbstractControllerTest { public class HousekeeperServiceTest extends AbstractControllerTest {
@SpyBean @MockitoSpyBean
private HousekeeperService housekeeperService; private HousekeeperService housekeeperService;
@SpyBean @MockitoSpyBean
private HousekeeperReprocessingService housekeeperReprocessingService; private HousekeeperReprocessingService housekeeperReprocessingService;
@MockitoSpyBean
private TsHistoryDeletionTaskProcessor tsHistoryDeletionTaskProcessor;
@Autowired @Autowired
private EventService eventService; private EventService eventService;
@Autowired @Autowired
@ -153,8 +155,6 @@ public class HousekeeperServiceTest extends AbstractControllerTest {
private CustomerService customerService; private CustomerService customerService;
@Autowired @Autowired
private DashboardService dashboardService; private DashboardService dashboardService;
@SpyBean
private TsHistoryDeletionTaskProcessor tsHistoryDeletionTaskProcessor;
private TenantId tenantId; private TenantId tenantId;

144
application/src/test/java/org/thingsboard/server/service/ttl/NotificationsCleanUpServiceTest.java

@ -0,0 +1,144 @@
/**
* 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.service.ttl;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.Mock;
import org.mockito.junit.jupiter.MockitoExtension;
import org.springframework.test.util.ReflectionTestUtils;
import org.thingsboard.server.common.data.id.TenantId;
import org.thingsboard.server.common.data.page.PageData;
import org.thingsboard.server.common.msg.queue.TopicPartitionInfo;
import org.thingsboard.server.dao.notification.NotificationRequestDao;
import org.thingsboard.server.dao.sqlts.insert.sql.SqlPartitioningRepository;
import org.thingsboard.server.dao.tenant.TenantService;
import org.thingsboard.server.queue.discovery.PartitionService;
import java.util.List;
import java.util.UUID;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.anyInt;
import static org.mockito.ArgumentMatchers.anyLong;
import static org.mockito.ArgumentMatchers.anyString;
import static org.mockito.ArgumentMatchers.eq;
import static org.mockito.Mockito.never;
import static org.mockito.Mockito.times;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
@ExtendWith(MockitoExtension.class)
public class NotificationsCleanUpServiceTest {
@Mock
private PartitionService partitionService;
@Mock
private SqlPartitioningRepository partitioningRepository;
@Mock
private NotificationRequestDao notificationRequestDao;
@Mock
private TenantService tenantService;
private NotificationsCleanUpService cleanUpService;
private static final int BATCH_SIZE = 3;
@BeforeEach
public void setUp() {
cleanUpService = new NotificationsCleanUpService(partitionService, partitioningRepository, notificationRequestDao, tenantService);
ReflectionTestUtils.setField(cleanUpService, "ttlInSec", 2592000L);
ReflectionTestUtils.setField(cleanUpService, "partitionSizeInHours", 168);
ReflectionTestUtils.setField(cleanUpService, "removalBatchSize", BATCH_SIZE);
}
@Test
public void testBatchLoopCallsDaoMultipleTimes() {
TopicPartitionInfo myPartition = TopicPartitionInfo.builder().topic("tb_core").myPartition(true).build();
when(partitionService.resolve(any(), any(), any())).thenReturn(myPartition);
when(partitioningRepository.dropPartitionsBefore(anyString(), anyLong(), anyLong()))
.thenReturn(System.currentTimeMillis());
TenantId tenantId = TenantId.fromUUID(UUID.randomUUID());
when(tenantService.findTenantsIds(any()))
.thenReturn(new PageData<>(List.of(tenantId), 1, 1, false));
// Sysadmin: returns 3 (full batch), then 1 (partial) -> 2 calls
when(notificationRequestDao.removeByTenantIdAndCreatedTimeBeforeBatch(eq(TenantId.SYS_TENANT_ID), anyLong(), eq(BATCH_SIZE)))
.thenReturn(BATCH_SIZE)
.thenReturn(1);
// Tenant: returns 3, 3, 0 -> 3 calls
when(notificationRequestDao.removeByTenantIdAndCreatedTimeBeforeBatch(eq(tenantId), anyLong(), eq(BATCH_SIZE)))
.thenReturn(BATCH_SIZE)
.thenReturn(BATCH_SIZE)
.thenReturn(0);
cleanUpService.cleanUp();
verify(notificationRequestDao, times(2))
.removeByTenantIdAndCreatedTimeBeforeBatch(eq(TenantId.SYS_TENANT_ID), anyLong(), eq(BATCH_SIZE));
verify(notificationRequestDao, times(3))
.removeByTenantIdAndCreatedTimeBeforeBatch(eq(tenantId), anyLong(), eq(BATCH_SIZE));
}
@Test
public void testSkipsTenantNotOnMyPartition() {
TopicPartitionInfo myPartition = TopicPartitionInfo.builder().topic("tb_core").myPartition(true).build();
TopicPartitionInfo notMyPartition = TopicPartitionInfo.builder().topic("tb_core").myPartition(false).build();
when(partitionService.resolve(any(), eq(TenantId.SYS_TENANT_ID), eq(TenantId.SYS_TENANT_ID)))
.thenReturn(myPartition);
when(partitioningRepository.dropPartitionsBefore(anyString(), anyLong(), anyLong()))
.thenReturn(System.currentTimeMillis());
// Sysadmin: no records
when(notificationRequestDao.removeByTenantIdAndCreatedTimeBeforeBatch(eq(TenantId.SYS_TENANT_ID), anyLong(), eq(BATCH_SIZE)))
.thenReturn(0);
TenantId myTenant = TenantId.fromUUID(UUID.randomUUID());
TenantId otherTenant = TenantId.fromUUID(UUID.randomUUID());
when(tenantService.findTenantsIds(any()))
.thenReturn(new PageData<>(List.of(myTenant, otherTenant), 2, 1, false));
when(partitionService.resolve(any(), eq(myTenant), eq(myTenant))).thenReturn(myPartition);
when(partitionService.resolve(any(), eq(otherTenant), eq(otherTenant))).thenReturn(notMyPartition);
when(notificationRequestDao.removeByTenantIdAndCreatedTimeBeforeBatch(eq(myTenant), anyLong(), eq(BATCH_SIZE)))
.thenReturn(0);
cleanUpService.cleanUp();
verify(notificationRequestDao).removeByTenantIdAndCreatedTimeBeforeBatch(eq(myTenant), anyLong(), eq(BATCH_SIZE));
verify(notificationRequestDao, never()).removeByTenantIdAndCreatedTimeBeforeBatch(eq(otherTenant), anyLong(), anyInt());
}
@Test
public void testNoPartitionsDropped_stillCleansUpRequests() {
TopicPartitionInfo myPartition = TopicPartitionInfo.builder().topic("tb_core").myPartition(true).build();
when(partitionService.resolve(any(), any(), any())).thenReturn(myPartition);
when(partitioningRepository.dropPartitionsBefore(anyString(), anyLong(), anyLong()))
.thenReturn(0L);
when(notificationRequestDao.removeByTenantIdAndCreatedTimeBeforeBatch(eq(TenantId.SYS_TENANT_ID), anyLong(), eq(BATCH_SIZE)))
.thenReturn(0);
when(tenantService.findTenantsIds(any()))
.thenReturn(new PageData<>(List.of(), 0, 0, false));
cleanUpService.cleanUp();
verify(notificationRequestDao).removeByTenantIdAndCreatedTimeBeforeBatch(eq(TenantId.SYS_TENANT_ID), anyLong(), eq(BATCH_SIZE));
}
}

136
application/src/test/java/org/thingsboard/server/service/ttl/rpc/RpcCleanUpServiceTest.java

@ -0,0 +1,136 @@
/**
* 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.service.ttl.rpc;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.Mock;
import org.mockito.junit.jupiter.MockitoExtension;
import org.springframework.test.util.ReflectionTestUtils;
import org.thingsboard.server.common.data.TenantProfile;
import org.thingsboard.server.common.data.id.TenantId;
import org.thingsboard.server.common.data.page.PageData;
import org.thingsboard.server.common.data.tenant.profile.DefaultTenantProfileConfiguration;
import org.thingsboard.server.common.data.tenant.profile.TenantProfileData;
import org.thingsboard.server.common.msg.queue.TopicPartitionInfo;
import org.thingsboard.server.dao.rpc.RpcDao;
import org.thingsboard.server.dao.tenant.TbTenantProfileCache;
import org.thingsboard.server.dao.tenant.TenantService;
import org.thingsboard.server.queue.discovery.PartitionService;
import java.util.List;
import java.util.UUID;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.anyInt;
import static org.mockito.ArgumentMatchers.anyLong;
import static org.mockito.ArgumentMatchers.eq;
import static org.mockito.Mockito.never;
import static org.mockito.Mockito.times;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
@ExtendWith(MockitoExtension.class)
public class RpcCleanUpServiceTest {
@Mock
private PartitionService partitionService;
@Mock
private RpcDao rpcDao;
@Mock
private TenantService tenantService;
@Mock
private TbTenantProfileCache tenantProfileCache;
private RpcCleanUpService cleanUpService;
private static final int BATCH_SIZE = 3;
@BeforeEach
public void setUp() {
cleanUpService = new RpcCleanUpService(tenantService, partitionService, tenantProfileCache, rpcDao);
ReflectionTestUtils.setField(cleanUpService, "removalBatchSize", BATCH_SIZE);
}
@Test
public void testBatchLoopCallsDaoMultipleTimes() {
TenantId tenantId = TenantId.fromUUID(UUID.randomUUID());
setupTenant(tenantId, 7);
// Returns 3 (full batch), 3 (full batch), 1 (partial) -> 3 calls
when(rpcDao.deleteOutdatedRpcByTenantIdBatch(eq(tenantId), anyLong(), eq(BATCH_SIZE)))
.thenReturn(BATCH_SIZE)
.thenReturn(BATCH_SIZE)
.thenReturn(1);
cleanUpService.cleanUp();
verify(rpcDao, times(3)).deleteOutdatedRpcByTenantIdBatch(eq(tenantId), anyLong(), eq(BATCH_SIZE));
}
@Test
public void testSkipsTenantNotOnMyPartition() {
TenantId myTenant = TenantId.fromUUID(UUID.randomUUID());
TenantId otherTenant = TenantId.fromUUID(UUID.randomUUID());
TopicPartitionInfo myPartition = TopicPartitionInfo.builder().topic("tb_core").myPartition(true).build();
TopicPartitionInfo notMyPartition = TopicPartitionInfo.builder().topic("tb_core").myPartition(false).build();
when(tenantService.findTenantsIds(any()))
.thenReturn(new PageData<>(List.of(myTenant, otherTenant), 2, 1, false));
when(partitionService.resolve(any(), eq(myTenant), eq(myTenant))).thenReturn(myPartition);
when(partitionService.resolve(any(), eq(otherTenant), eq(otherTenant))).thenReturn(notMyPartition);
setupTenantProfile(myTenant, 7);
when(rpcDao.deleteOutdatedRpcByTenantIdBatch(eq(myTenant), anyLong(), eq(BATCH_SIZE)))
.thenReturn(0);
cleanUpService.cleanUp();
verify(rpcDao).deleteOutdatedRpcByTenantIdBatch(eq(myTenant), anyLong(), eq(BATCH_SIZE));
verify(rpcDao, never()).deleteOutdatedRpcByTenantIdBatch(eq(otherTenant), anyLong(), anyInt());
}
@Test
public void testSkipsTenantWithZeroTtl() {
TenantId tenantId = TenantId.fromUUID(UUID.randomUUID());
setupTenant(tenantId, 0);
cleanUpService.cleanUp();
verify(rpcDao, never()).deleteOutdatedRpcByTenantIdBatch(any(), anyLong(), anyInt());
}
private void setupTenant(TenantId tenantId, int rpcTtlDays) {
TopicPartitionInfo myPartition = TopicPartitionInfo.builder().topic("tb_core").myPartition(true).build();
when(partitionService.resolve(any(), eq(tenantId), eq(tenantId))).thenReturn(myPartition);
when(tenantService.findTenantsIds(any()))
.thenReturn(new PageData<>(List.of(tenantId), 1, 1, false));
setupTenantProfile(tenantId, rpcTtlDays);
}
private void setupTenantProfile(TenantId tenantId, int rpcTtlDays) {
TenantProfile profile = new TenantProfile();
TenantProfileData profileData = new TenantProfileData();
DefaultTenantProfileConfiguration config = new DefaultTenantProfileConfiguration();
config.setRpcTtlDays(rpcTtlDays);
profileData.setConfiguration(config);
profile.setProfileData(profileData);
when(tenantProfileCache.get(tenantId)).thenReturn(profile);
}
}

11
application/src/test/java/org/thingsboard/server/transport/lwm2m/client/FwLwM2MDevice.java

@ -171,7 +171,14 @@ public class FwLwM2MDevice extends BaseInstanceEnabler implements Destroyable {
if (this.leshanClient != null) { if (this.leshanClient != null) {
log.info("Stop/reboot LwM2M client {}", this.leshanClient.getEndpoint(identity)); log.info("Stop/reboot LwM2M client {}", this.leshanClient.getEndpoint(identity));
this.leshanClient.stop(false); try {
this.leshanClient.stop(false);
} catch (Exception stopEx) {
// Leshan may throw NPE during CoAP observe-relation cleanup when the server
// reference is null (race condition in NotificationDataStore.toKey()).
// The client is still considered stopped at this point — proceed with restart.
log.warn("Exception during LwM2M client stop, proceeding with restart: {}", stopEx.getMessage());
}
log.info("Start after update fw LwM2M client {}", this.leshanClient.getEndpoint(identity)); log.info("Start after update fw LwM2M client {}", this.leshanClient.getEndpoint(identity));
this.leshanClient.start(); this.leshanClient.start();
@ -193,7 +200,7 @@ public class FwLwM2MDevice extends BaseInstanceEnabler implements Destroyable {
} catch (Exception e) { } catch (Exception e) {
log.error("Error during firmware update", e); log.error("Error during firmware update", e);
} }
}, 0, TimeUnit.SECONDS); // start immediately, without further delay }, 1, TimeUnit.SECONDS); // delay 1 sec to allow CoAP Execute response to be delivered before client stops
} }
protected void setLeshanClient(LeshanClient leshanClient) { protected void setLeshanClient(LeshanClient leshanClient) {

7
application/src/test/java/org/thingsboard/server/transport/lwm2m/rpc/sql/RpcLwm2mIntegrationReadCollectedValueTest.java

@ -21,6 +21,7 @@ import com.fasterxml.jackson.databind.node.ObjectNode;
import lombok.extern.slf4j.Slf4j; import lombok.extern.slf4j.Slf4j;
import org.junit.Before; import org.junit.Before;
import org.junit.Test; import org.junit.Test;
import org.thingsboard.server.transport.lwm2m.Lwm2mTestHelper;
import org.thingsboard.server.transport.lwm2m.rpc.AbstractRpcLwM2MIntegrationTest; import org.thingsboard.server.transport.lwm2m.rpc.AbstractRpcLwM2MIntegrationTest;
import java.util.concurrent.atomic.AtomicReference; import java.util.concurrent.atomic.AtomicReference;
import static java.util.concurrent.TimeUnit.SECONDS; import static java.util.concurrent.TimeUnit.SECONDS;
@ -37,6 +38,12 @@ import static org.thingsboard.server.transport.lwm2m.Lwm2mTestHelper.RESOURCE_ID
@Slf4j @Slf4j
public class RpcLwm2mIntegrationReadCollectedValueTest extends AbstractRpcLwM2MIntegrationTest { public class RpcLwm2mIntegrationReadCollectedValueTest extends AbstractRpcLwM2MIntegrationTest {
@Before
public void resetCollectedValueTimestamps() {
Lwm2mTestHelper.RESOURCE_ID_3303_12_5700_TS_0 = 0;
Lwm2mTestHelper.RESOURCE_ID_3303_12_5700_TS_1 = 0;
}
/** /**
* Read {"id":"/3303/12/5700"} * Read {"id":"/3303/12/5700"}
* Trigger a Send operation from the client with multiple values for the same resource as a payload * Trigger a Send operation from the client with multiple values for the same resource as a payload

1
application/src/test/java/org/thingsboard/server/transport/mqtt/mqttv3/attributes/AbstractMqttAttributesIntegrationTest.java

@ -420,6 +420,7 @@ public abstract class AbstractMqttAttributesIntegrationTest extends AbstractMqtt
Awaitility.await() Awaitility.await()
.atMost(10, TimeUnit.SECONDS) .atMost(10, TimeUnit.SECONDS)
.ignoreExceptions()
.until(() -> { .until(() -> {
List<Map<String, Object>> attributes = doGetAsyncTyped(attributeValuesUrl, new TypeReference<>() { List<Map<String, Object>> attributes = doGetAsyncTyped(attributeValuesUrl, new TypeReference<>() {
}); });

2
application/src/test/java/org/thingsboard/server/transport/mqtt/sparkplug/AbstractMqttV5ClientSparkplugTest.java

@ -191,6 +191,7 @@ public abstract class AbstractMqttV5ClientSparkplugTest extends AbstractMqttInte
AtomicReference<Device> device = new AtomicReference<>(); AtomicReference<Device> device = new AtomicReference<>();
await(alias + "find device [" + deviceName + "] after created") await(alias + "find device [" + deviceName + "] after created")
.atMost(200, TimeUnit.SECONDS) .atMost(200, TimeUnit.SECONDS)
.ignoreExceptions()
.until(() -> { .until(() -> {
device.set(doGet("/api/tenant/devices?deviceName=" + deviceName, Device.class)); device.set(doGet("/api/tenant/devices?deviceName=" + deviceName, Device.class));
return device.get() != null; return device.get() != null;
@ -236,6 +237,7 @@ public abstract class AbstractMqttV5ClientSparkplugTest extends AbstractMqttInte
AtomicReference<Device> device = new AtomicReference<>(); AtomicReference<Device> device = new AtomicReference<>();
await(alias + "find device [" + deviceName + "] after created") await(alias + "find device [" + deviceName + "] after created")
.atMost(200, TimeUnit.SECONDS) .atMost(200, TimeUnit.SECONDS)
.ignoreExceptions()
.until(() -> { .until(() -> {
device.set(doGet("/api/tenant/devices?deviceName=" + deviceName, Device.class)); device.set(doGet("/api/tenant/devices?deviceName=" + deviceName, Device.class));
return device.get() != null; return device.get() != null;

2
application/src/test/java/org/thingsboard/server/transport/mqtt/sparkplug/attributes/AbstractMqttV5ClientSparkplugAttributesTest.java

@ -468,6 +468,7 @@ public abstract class AbstractMqttV5ClientSparkplugAttributesTest extends Abstra
AtomicReference<List<String>> actualKeys = new AtomicReference<>(); AtomicReference<List<String>> actualKeys = new AtomicReference<>();
await(alias + SparkplugMessageType.NBIRTH.name()) await(alias + SparkplugMessageType.NBIRTH.name())
.atMost(40, TimeUnit.SECONDS) .atMost(40, TimeUnit.SECONDS)
.ignoreExceptions()
.until(() -> { .until(() -> {
actualKeys.set(doGetAsyncTyped(urlTemplate, new TypeReference<>() { actualKeys.set(doGetAsyncTyped(urlTemplate, new TypeReference<>() {
})); }));
@ -483,6 +484,7 @@ public abstract class AbstractMqttV5ClientSparkplugAttributesTest extends Abstra
AtomicReference<List<String>> actualKeys = new AtomicReference<>(); AtomicReference<List<String>> actualKeys = new AtomicReference<>();
await(alias + SparkplugMessageType.DBIRTH.name()) await(alias + SparkplugMessageType.DBIRTH.name())
.atMost(40, TimeUnit.SECONDS) .atMost(40, TimeUnit.SECONDS)
.ignoreExceptions()
.until(() -> { .until(() -> {
actualKeys.set(doGetAsyncTyped(urlTemplate, new TypeReference<>() { actualKeys.set(doGetAsyncTyped(urlTemplate, new TypeReference<>() {
})); }));

1
application/src/test/resources/application-test.properties

@ -44,6 +44,7 @@ queue.transport_api.response_poll_interval=5
queue.transport.poll_interval=5 queue.transport.poll_interval=5
queue.core.poll-interval=5 queue.core.poll-interval=5
queue.core.partitions=2 queue.core.partitions=2
queue.core.housekeeper.task-reprocessing-delay-ms=0
queue.rule-engine.poll-interval=5 queue.rule-engine.poll-interval=5
queue.rule-engine.stats.enabled=true queue.rule-engine.stats.enabled=true

8
common/dao-api/src/main/java/org/thingsboard/server/dao/pat/ApiKeyService.java

@ -24,10 +24,14 @@ import org.thingsboard.server.common.data.pat.ApiKey;
import org.thingsboard.server.common.data.pat.ApiKeyInfo; import org.thingsboard.server.common.data.pat.ApiKeyInfo;
import org.thingsboard.server.dao.entity.EntityDaoService; import org.thingsboard.server.dao.entity.EntityDaoService;
import java.util.List;
public interface ApiKeyService extends EntityDaoService { public interface ApiKeyService extends EntityDaoService {
ApiKey saveApiKey(TenantId tenantId, ApiKeyInfo apiKey); ApiKey saveApiKey(TenantId tenantId, ApiKeyInfo apiKey);
ApiKey saveApiKey(TenantId tenantId, ApiKeyInfo apiKeyInfo, String value, boolean doValidate);
void deleteApiKey(TenantId tenantId, ApiKey apiKey, boolean force); void deleteApiKey(TenantId tenantId, ApiKey apiKey, boolean force);
void deleteByUserId(TenantId tenantId, UserId userId); void deleteByUserId(TenantId tenantId, UserId userId);
@ -38,4 +42,8 @@ public interface ApiKeyService extends EntityDaoService {
PageData<ApiKeyInfo> findApiKeysByUserId(TenantId tenantId, UserId userId, PageLink pageLink); PageData<ApiKeyInfo> findApiKeysByUserId(TenantId tenantId, UserId userId, PageLink pageLink);
List<ApiKey> findApiKeysByUserId(TenantId tenantId, UserId userId);
PageData<ApiKey> findApiKeysByTenantId(TenantId tenantId, PageLink pageLink);
} }

31
common/data/src/main/java/org/thingsboard/server/common/data/cf/CalculatedField.java

@ -17,15 +17,15 @@ package org.thingsboard.server.common.data.cf;
import com.fasterxml.jackson.annotation.JsonIgnore; import com.fasterxml.jackson.annotation.JsonIgnore;
import com.fasterxml.jackson.annotation.JsonSetter; import com.fasterxml.jackson.annotation.JsonSetter;
import com.fasterxml.jackson.databind.JsonNode;
import io.swagger.v3.oas.annotations.media.Schema; import io.swagger.v3.oas.annotations.media.Schema;
import jakarta.validation.Valid; import jakarta.validation.Valid;
import jakarta.validation.constraints.NotNull; import jakarta.validation.constraints.NotNull;
import lombok.Data; import lombok.Data;
import lombok.EqualsAndHashCode; import lombok.EqualsAndHashCode;
import lombok.Getter;
import lombok.Setter;
import org.thingsboard.server.common.data.BaseData; import org.thingsboard.server.common.data.BaseData;
import org.thingsboard.server.common.data.EntityType; import org.thingsboard.server.common.data.EntityType;
import org.thingsboard.server.common.data.HasAdditionalInfo;
import org.thingsboard.server.common.data.HasDebugSettings; import org.thingsboard.server.common.data.HasDebugSettings;
import org.thingsboard.server.common.data.HasName; import org.thingsboard.server.common.data.HasName;
import org.thingsboard.server.common.data.HasTenantId; import org.thingsboard.server.common.data.HasTenantId;
@ -39,7 +39,6 @@ import org.thingsboard.server.common.data.id.TenantId;
import org.thingsboard.server.common.data.validation.Length; import org.thingsboard.server.common.data.validation.Length;
import org.thingsboard.server.common.data.validation.NoXss; import org.thingsboard.server.common.data.validation.NoXss;
import java.io.Serial;
import java.util.Collections; import java.util.Collections;
import java.util.EnumSet; import java.util.EnumSet;
import java.util.Map; import java.util.Map;
@ -48,10 +47,7 @@ import java.util.Set;
@Schema @Schema
@Data @Data
@EqualsAndHashCode(callSuper = true) @EqualsAndHashCode(callSuper = true)
public class CalculatedField extends BaseData<CalculatedFieldId> implements HasName, HasTenantId, HasVersion, HasDebugSettings { public class CalculatedField extends BaseData<CalculatedFieldId> implements HasName, HasTenantId, HasVersion, HasDebugSettings, HasAdditionalInfo {
@Serial
private static final long serialVersionUID = 4491966747773381420L;
public static final Map<EntityType, Set<CalculatedFieldType>> SUPPORTED_ENTITIES = Map.of( public static final Map<EntityType, Set<CalculatedFieldType>> SUPPORTED_ENTITIES = Map.of(
EntityType.DEVICE, CalculatedFieldType.all, EntityType.DEVICE, CalculatedFieldType.all,
@ -90,28 +86,17 @@ public class CalculatedField extends BaseData<CalculatedFieldId> implements HasN
@Valid @Valid
@NotNull @NotNull
private CalculatedFieldConfiguration configuration; private CalculatedFieldConfiguration configuration;
@Getter
@Setter
private Long version; private Long version;
@NoXss
@Schema(description = "Additional parameters of the calculated field")
private JsonNode additionalInfo;
public CalculatedField() { public CalculatedField() {}
super();
}
public CalculatedField(CalculatedFieldId id) { public CalculatedField(CalculatedFieldId id) {
super(id); super(id);
} }
public CalculatedField(TenantId tenantId, EntityId entityId, CalculatedFieldType type, String name, int configurationVersion, CalculatedFieldConfiguration configuration, Long version) {
this.tenantId = tenantId;
this.entityId = entityId;
this.type = type;
this.name = name;
this.configurationVersion = configurationVersion;
this.configuration = configuration;
this.version = version;
}
public CalculatedField(CalculatedField calculatedField) { public CalculatedField(CalculatedField calculatedField) {
super(calculatedField); super(calculatedField);
this.tenantId = calculatedField.tenantId; this.tenantId = calculatedField.tenantId;
@ -123,6 +108,7 @@ public class CalculatedField extends BaseData<CalculatedFieldId> implements HasN
this.configurationVersion = calculatedField.configurationVersion; this.configurationVersion = calculatedField.configurationVersion;
this.configuration = calculatedField.configuration; this.configuration = calculatedField.configuration;
this.version = calculatedField.version; this.version = calculatedField.version;
this.additionalInfo = calculatedField.additionalInfo;
} }
@Schema(description = "JSON object with the Calculated Field Id. Referencing non-existing Calculated Field Id will cause error.") @Schema(description = "JSON object with the Calculated Field Id. Referencing non-existing Calculated Field Id will cause error.")
@ -159,6 +145,7 @@ public class CalculatedField extends BaseData<CalculatedFieldId> implements HasN
.append(", name='").append(name) .append(", name='").append(name)
.append(", configurationVersion=").append(configurationVersion) .append(", configurationVersion=").append(configurationVersion)
.append(", configuration=").append(configuration) .append(", configuration=").append(configuration)
.append(", additionalInfo=").append(additionalInfo)
.append(", version=").append(version) .append(", version=").append(version)
.append(", createdTime=").append(createdTime) .append(", createdTime=").append(createdTime)
.append(", id=").append(id).append(']') .append(", id=").append(id).append(']')

3
common/data/src/main/java/org/thingsboard/server/common/data/edge/EdgeEventType.java

@ -48,7 +48,8 @@ public enum EdgeEventType {
OAUTH2_CLIENT(true, EntityType.OAUTH2_CLIENT), OAUTH2_CLIENT(true, EntityType.OAUTH2_CLIENT),
DOMAIN(true, EntityType.DOMAIN), DOMAIN(true, EntityType.DOMAIN),
CALCULATED_FIELD(false, EntityType.CALCULATED_FIELD), CALCULATED_FIELD(false, EntityType.CALCULATED_FIELD),
AI_MODEL(true, EntityType.AI_MODEL); AI_MODEL(true, EntityType.AI_MODEL),
API_KEY(true, EntityType.API_KEY);
private final boolean allEdgesRelated; private final boolean allEdgesRelated;

4
common/data/src/main/java/org/thingsboard/server/common/data/housekeeper/AlarmsDeletionHousekeeperTask.java

@ -23,6 +23,7 @@ import lombok.ToString;
import org.thingsboard.server.common.data.id.EntityId; import org.thingsboard.server.common.data.id.EntityId;
import org.thingsboard.server.common.data.id.TenantId; import org.thingsboard.server.common.data.id.TenantId;
import java.io.Serial;
import java.util.List; import java.util.List;
import java.util.UUID; import java.util.UUID;
@ -32,6 +33,9 @@ import java.util.UUID;
@NoArgsConstructor(access = AccessLevel.PROTECTED) @NoArgsConstructor(access = AccessLevel.PROTECTED)
public class AlarmsDeletionHousekeeperTask extends HousekeeperTask { public class AlarmsDeletionHousekeeperTask extends HousekeeperTask {
@Serial
private static final long serialVersionUID = 9214680001573764374L;
private List<UUID> alarms; private List<UUID> alarms;
public AlarmsDeletionHousekeeperTask(TenantId tenantId, EntityId entityId) { public AlarmsDeletionHousekeeperTask(TenantId tenantId, EntityId entityId) {

4
common/data/src/main/java/org/thingsboard/server/common/data/housekeeper/AlarmsUnassignHousekeeperTask.java

@ -24,6 +24,7 @@ import org.thingsboard.server.common.data.User;
import org.thingsboard.server.common.data.id.TenantId; import org.thingsboard.server.common.data.id.TenantId;
import org.thingsboard.server.common.data.id.UserId; import org.thingsboard.server.common.data.id.UserId;
import java.io.Serial;
import java.util.List; import java.util.List;
import java.util.UUID; import java.util.UUID;
@ -33,6 +34,9 @@ import java.util.UUID;
@NoArgsConstructor(access = AccessLevel.PROTECTED) @NoArgsConstructor(access = AccessLevel.PROTECTED)
public class AlarmsUnassignHousekeeperTask extends HousekeeperTask { public class AlarmsUnassignHousekeeperTask extends HousekeeperTask {
@Serial
private static final long serialVersionUID = 9156667024462937756L;
private String userTitle; private String userTitle;
private List<UUID> alarms; private List<UUID> alarms;

4
common/data/src/main/java/org/thingsboard/server/common/data/housekeeper/EntitiesDeletionHousekeeperTask.java

@ -23,6 +23,7 @@ import lombok.ToString;
import org.thingsboard.server.common.data.EntityType; import org.thingsboard.server.common.data.EntityType;
import org.thingsboard.server.common.data.id.TenantId; import org.thingsboard.server.common.data.id.TenantId;
import java.io.Serial;
import java.util.List; import java.util.List;
import java.util.UUID; import java.util.UUID;
@ -32,6 +33,9 @@ import java.util.UUID;
@NoArgsConstructor @NoArgsConstructor
public class EntitiesDeletionHousekeeperTask extends HousekeeperTask { public class EntitiesDeletionHousekeeperTask extends HousekeeperTask {
@Serial
private static final long serialVersionUID = 9009068831061529286L;
private EntityType entityType; private EntityType entityType;
private List<UUID> entities; private List<UUID> entities;

4
common/data/src/main/java/org/thingsboard/server/common/data/housekeeper/HousekeeperTask.java

@ -29,6 +29,7 @@ import org.thingsboard.server.common.data.User;
import org.thingsboard.server.common.data.id.EntityId; import org.thingsboard.server.common.data.id.EntityId;
import org.thingsboard.server.common.data.id.TenantId; import org.thingsboard.server.common.data.id.TenantId;
import java.io.Serial;
import java.io.Serializable; import java.io.Serializable;
@JsonIgnoreProperties(ignoreUnknown = true) @JsonIgnoreProperties(ignoreUnknown = true)
@ -45,6 +46,9 @@ import java.io.Serializable;
@NoArgsConstructor(access = AccessLevel.PROTECTED) @NoArgsConstructor(access = AccessLevel.PROTECTED)
public class HousekeeperTask implements Serializable { public class HousekeeperTask implements Serializable {
@Serial
private static final long serialVersionUID = -2585974110832225152L;
private TenantId tenantId; private TenantId tenantId;
private EntityId entityId; private EntityId entityId;
private HousekeeperTaskType taskType; private HousekeeperTaskType taskType;

5
common/data/src/main/java/org/thingsboard/server/common/data/housekeeper/LatestTsDeletionHousekeeperTask.java

@ -23,12 +23,17 @@ import lombok.ToString;
import org.thingsboard.server.common.data.id.EntityId; import org.thingsboard.server.common.data.id.EntityId;
import org.thingsboard.server.common.data.id.TenantId; import org.thingsboard.server.common.data.id.TenantId;
import java.io.Serial;
@Data @Data
@ToString(callSuper = true) @ToString(callSuper = true)
@EqualsAndHashCode(callSuper = true) @EqualsAndHashCode(callSuper = true)
@NoArgsConstructor(access = AccessLevel.PROTECTED) @NoArgsConstructor(access = AccessLevel.PROTECTED)
public class LatestTsDeletionHousekeeperTask extends HousekeeperTask { public class LatestTsDeletionHousekeeperTask extends HousekeeperTask {
@Serial
private static final long serialVersionUID = 5193191938513490138L;
private String key; private String key;
public LatestTsDeletionHousekeeperTask(TenantId tenantId, EntityId entityId, String key) { public LatestTsDeletionHousekeeperTask(TenantId tenantId, EntityId entityId, String key) {

5
common/data/src/main/java/org/thingsboard/server/common/data/housekeeper/TenantEntitiesDeletionHousekeeperTask.java

@ -23,12 +23,17 @@ import lombok.ToString;
import org.thingsboard.server.common.data.EntityType; import org.thingsboard.server.common.data.EntityType;
import org.thingsboard.server.common.data.id.TenantId; import org.thingsboard.server.common.data.id.TenantId;
import java.io.Serial;
@Data @Data
@ToString(callSuper = true) @ToString(callSuper = true)
@EqualsAndHashCode(callSuper = true) @EqualsAndHashCode(callSuper = true)
@NoArgsConstructor @NoArgsConstructor
public class TenantEntitiesDeletionHousekeeperTask extends HousekeeperTask { public class TenantEntitiesDeletionHousekeeperTask extends HousekeeperTask {
@Serial
private static final long serialVersionUID = -8033108795318393447L;
private EntityType entityType; private EntityType entityType;
public TenantEntitiesDeletionHousekeeperTask(TenantId tenantId, EntityType entityType) { public TenantEntitiesDeletionHousekeeperTask(TenantId tenantId, EntityType entityType) {

5
common/data/src/main/java/org/thingsboard/server/common/data/housekeeper/TsHistoryDeletionHousekeeperTask.java

@ -23,12 +23,17 @@ import lombok.ToString;
import org.thingsboard.server.common.data.id.EntityId; import org.thingsboard.server.common.data.id.EntityId;
import org.thingsboard.server.common.data.id.TenantId; import org.thingsboard.server.common.data.id.TenantId;
import java.io.Serial;
@Data @Data
@ToString(callSuper = true) @ToString(callSuper = true)
@EqualsAndHashCode(callSuper = true) @EqualsAndHashCode(callSuper = true)
@NoArgsConstructor(access = AccessLevel.PROTECTED) @NoArgsConstructor(access = AccessLevel.PROTECTED)
public class TsHistoryDeletionHousekeeperTask extends HousekeeperTask { public class TsHistoryDeletionHousekeeperTask extends HousekeeperTask {
@Serial
private static final long serialVersionUID = 4573851542705079043L;
private String key; private String key;
public TsHistoryDeletionHousekeeperTask(TenantId tenantId, EntityId entityId, String key) { public TsHistoryDeletionHousekeeperTask(TenantId tenantId, EntityId entityId, String key) {

1
common/data/src/main/java/org/thingsboard/server/common/data/id/EntityIdFactory.java

@ -114,6 +114,7 @@ public class EntityIdFactory {
case DOMAIN -> new DomainId(uuid); case DOMAIN -> new DomainId(uuid);
case CALCULATED_FIELD -> new CalculatedFieldId(uuid); case CALCULATED_FIELD -> new CalculatedFieldId(uuid);
case AI_MODEL -> new AiModelId(uuid); case AI_MODEL -> new AiModelId(uuid);
case API_KEY -> new ApiKeyId(uuid);
case ADMIN_SETTINGS -> new AdminSettingsId(uuid); case ADMIN_SETTINGS -> new AdminSettingsId(uuid);
default -> throw new IllegalArgumentException("EdgeEventType " + edgeEventType + " is not supported!"); default -> throw new IllegalArgumentException("EdgeEventType " + edgeEventType + " is not supported!");
}; };

5
common/data/src/main/java/org/thingsboard/server/common/data/kv/TsKvEntry.java

@ -37,4 +37,9 @@ public interface TsKvEntry extends KvEntry, HasVersion {
return new TsValue(getTs(), getValueAsString()); return new TsValue(getTs(), getValueAsString());
} }
@JsonIgnore
default boolean isDeletedEntry() {
return getTs() == 0 && (getValue() == null || getValueAsString().isEmpty());
}
} }

2
common/data/src/main/java/org/thingsboard/server/common/data/notification/rule/NotificationRuleRecipientsConfig.java

@ -16,6 +16,7 @@
package org.thingsboard.server.common.data.notification.rule; package org.thingsboard.server.common.data.notification.rule;
import com.fasterxml.jackson.annotation.JsonIgnore; import com.fasterxml.jackson.annotation.JsonIgnore;
import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
import com.fasterxml.jackson.annotation.JsonSubTypes; import com.fasterxml.jackson.annotation.JsonSubTypes;
import com.fasterxml.jackson.annotation.JsonSubTypes.Type; import com.fasterxml.jackson.annotation.JsonSubTypes.Type;
import com.fasterxml.jackson.annotation.JsonTypeInfo; import com.fasterxml.jackson.annotation.JsonTypeInfo;
@ -46,6 +47,7 @@ import java.util.UUID;
@DiscriminatorMapping(value = "TASK_PROCESSING_FAILURE", schema = DefaultNotificationRuleRecipientsConfig.TaskProcessingFailureRecipientsConfig.class), @DiscriminatorMapping(value = "TASK_PROCESSING_FAILURE", schema = DefaultNotificationRuleRecipientsConfig.TaskProcessingFailureRecipientsConfig.class),
@DiscriminatorMapping(value = "RESOURCES_SHORTAGE", schema = DefaultNotificationRuleRecipientsConfig.ResourceShortageRecipientsConfig.class) @DiscriminatorMapping(value = "RESOURCES_SHORTAGE", schema = DefaultNotificationRuleRecipientsConfig.ResourceShortageRecipientsConfig.class)
}) })
@JsonIgnoreProperties(ignoreUnknown = true)
@JsonTypeInfo(use = JsonTypeInfo.Id.NAME, property = "triggerType", include = JsonTypeInfo.As.EXISTING_PROPERTY) @JsonTypeInfo(use = JsonTypeInfo.Id.NAME, property = "triggerType", include = JsonTypeInfo.As.EXISTING_PROPERTY)
@JsonSubTypes({ @JsonSubTypes({
@Type(name = "ALARM", value = EscalatedNotificationRuleRecipientsConfig.class), @Type(name = "ALARM", value = EscalatedNotificationRuleRecipientsConfig.class),

11
common/edge-api/src/main/proto/edge.proto

@ -47,8 +47,10 @@ enum EdgeVersion {
V_4_3_0 = 13; V_4_3_0 = 13;
V_4_2_1_2 = 14; V_4_2_1_2 = 14;
V_4_2_2 = 4220; V_4_2_2 = 4220;
V_4_2_2_1 = 4221;
V_4_3_0_1 = 15; V_4_3_0_1 = 15;
V_4_3_1 = 4310; V_4_3_1 = 4310;
V_4_3_1_1 = 4311;
V_4_4_0 = 4400; V_4_4_0 = 4400;
V_LATEST = 99999; V_LATEST = 99999;
@ -146,6 +148,13 @@ message AiModelUpdateMsg{
string entity = 4; string entity = 4;
} }
message ApiKeyUpdateMsg{
UpdateMsgType msgType = 1;
int64 idMSB = 2;
int64 idLSB = 3;
string entity = 4;
}
message EntityDataProto { message EntityDataProto {
int64 entityIdMSB = 1; int64 entityIdMSB = 1;
int64 entityIdLSB = 2; int64 entityIdLSB = 2;
@ -456,6 +465,7 @@ message UplinkMsg {
repeated AiModelUpdateMsg aiModelUpdateMsg = 27; repeated AiModelUpdateMsg aiModelUpdateMsg = 27;
repeated UserUpdateMsg userUpdateMsg = 28; repeated UserUpdateMsg userUpdateMsg = 28;
repeated UserCredentialsUpdateMsg userCredentialsUpdateMsg = 29; repeated UserCredentialsUpdateMsg userCredentialsUpdateMsg = 29;
repeated ApiKeyUpdateMsg apiKeyUpdateMsg = 30;
} }
message UplinkResponseMsg { message UplinkResponseMsg {
@ -507,4 +517,5 @@ message DownlinkMsg {
repeated OAuth2DomainUpdateMsg oAuth2DomainUpdateMsg = 34; repeated OAuth2DomainUpdateMsg oAuth2DomainUpdateMsg = 34;
repeated CalculatedFieldUpdateMsg calculatedFieldUpdateMsg = 35; repeated CalculatedFieldUpdateMsg calculatedFieldUpdateMsg = 35;
repeated AiModelUpdateMsg aiModelUpdateMsg = 36; repeated AiModelUpdateMsg aiModelUpdateMsg = 36;
repeated ApiKeyUpdateMsg apiKeyUpdateMsg = 37;
} }

18
common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/server/store/TbLwM2mRedisRegistrationStore.java

@ -323,22 +323,24 @@ public class TbLwM2mRedisRegistrationStore implements RegistrationStore, Startab
@Override @Override
public Iterator<Registration> getAllRegistrations() { public Iterator<Registration> getAllRegistrations() {
try (var connection = connectionFactory.getConnection()) { try (var scanConnection = connectionFactory.getConnection();
var getConnection = connectionFactory.getConnection()) {
Collection<Registration> list = new LinkedList<>(); Collection<Registration> list = new LinkedList<>();
ScanOptions scanOptions = ScanOptions.scanOptions().count(100).match(REG_EP + "*").build(); ScanOptions scanOptions = ScanOptions.scanOptions().count(100).match(REG_EP + "*").build();
List<Cursor<byte[]>> scans = new ArrayList<>(); List<Cursor<byte[]>> scans = new ArrayList<>();
if (connection instanceof RedisClusterConnection) { if (scanConnection instanceof RedisClusterConnection clusterConnection) {
((RedisClusterConnection) connection).clusterGetNodes().forEach(node -> { clusterConnection.clusterGetNodes().forEach(node ->
scans.add(((RedisClusterConnection) connection).scan(node, scanOptions)); scans.add(clusterConnection.scan(node, scanOptions)));
});
} else { } else {
scans.add(connection.scan(scanOptions)); scans.add(scanConnection.scan(scanOptions));
} }
scans.forEach(scan -> { scans.forEach(scan -> {
scan.forEachRemaining(key -> { scan.forEachRemaining(key -> {
byte[] element = connection.get(key); byte[] element = getConnection.get(key);
list.add(deserializeReg(element)); if (element != null) {
list.add(deserializeReg(element));
}
}); });
}); });
return list.iterator(); return list.iterator();

14
common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/server/store/TbRedisLwM2MClientStore.java

@ -61,21 +61,21 @@ public class TbRedisLwM2MClientStore implements TbLwM2MClientStore {
@Override @Override
public Set<LwM2mClient> getAll() { public Set<LwM2mClient> getAll() {
try (var connection = connectionFactory.getConnection()) { try (var scanConnection = connectionFactory.getConnection();
var getConnection = connectionFactory.getConnection()) {
Set<LwM2mClient> clients = new HashSet<>(); Set<LwM2mClient> clients = new HashSet<>();
ScanOptions scanOptions = ScanOptions.scanOptions().count(100).match(CLIENT_EP + "*").build(); ScanOptions scanOptions = ScanOptions.scanOptions().count(100).match(CLIENT_EP + "*").build();
List<Cursor<byte[]>> scans = new ArrayList<>(); List<Cursor<byte[]>> scans = new ArrayList<>();
if (connection instanceof RedisClusterConnection) { if (scanConnection instanceof RedisClusterConnection clusterConnection) {
((RedisClusterConnection) connection).clusterGetNodes().forEach(node -> { clusterConnection.clusterGetNodes().forEach(node ->
scans.add(((RedisClusterConnection) connection).scan(node, scanOptions)); scans.add(clusterConnection.scan(node, scanOptions)));
});
} else { } else {
scans.add(connection.scan(scanOptions)); scans.add(scanConnection.scan(scanOptions));
} }
scans.forEach(scan -> { scans.forEach(scan -> {
scan.forEachRemaining(key -> { scan.forEachRemaining(key -> {
byte[] element = connection.get(key); byte[] element = getConnection.get(key);
if (element != null) { if (element != null) {
try { try {
clients.add(deserialize(element)); clients.add(deserialize(element));

18
common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/server/store/TbRedisLwM2MModelConfigStore.java

@ -35,22 +35,24 @@ public class TbRedisLwM2MModelConfigStore implements TbLwM2MModelConfigStore {
@Override @Override
public List<LwM2MModelConfig> getAll() { public List<LwM2MModelConfig> getAll() {
try (var connection = connectionFactory.getConnection()) { try (var scanConnection = connectionFactory.getConnection();
var getConnection = connectionFactory.getConnection()) {
List<LwM2MModelConfig> configs = new ArrayList<>(); List<LwM2MModelConfig> configs = new ArrayList<>();
ScanOptions scanOptions = ScanOptions.scanOptions().count(100).match(MODEL_EP + "*").build(); ScanOptions scanOptions = ScanOptions.scanOptions().count(100).match(MODEL_EP + "*").build();
List<Cursor<byte[]>> scans = new ArrayList<>(); List<Cursor<byte[]>> scans = new ArrayList<>();
if (connection instanceof RedisClusterConnection) { if (scanConnection instanceof RedisClusterConnection clusterConnection) {
((RedisClusterConnection) connection).clusterGetNodes().forEach(node -> { clusterConnection.clusterGetNodes().forEach(node ->
scans.add(((RedisClusterConnection) connection).scan(node, scanOptions)); scans.add(clusterConnection.scan(node, scanOptions)));
});
} else { } else {
scans.add(connection.scan(scanOptions)); scans.add(scanConnection.scan(scanOptions));
} }
scans.forEach(scan -> { scans.forEach(scan -> {
scan.forEachRemaining(key -> { scan.forEachRemaining(key -> {
byte[] element = connection.get(key); byte[] element = getConnection.get(key);
configs.add(JacksonUtil.fromBytes(element, LwM2MModelConfig.class)); if (element != null) {
configs.add(JacksonUtil.fromBytes(element, LwM2MModelConfig.class));
}
}); });
}); });
return configs; return configs;

137
common/transport/lwm2m/src/test/java/org/thingsboard/server/transport/lwm2m/server/store/TbRedisLwM2MClientStoreTest.java

@ -0,0 +1,137 @@
/**
* 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.lwm2m.server.store;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.Mock;
import org.mockito.junit.jupiter.MockitoExtension;
import org.springframework.data.redis.connection.RedisConnection;
import org.springframework.data.redis.connection.RedisConnectionFactory;
import org.springframework.data.redis.core.Cursor;
import org.springframework.data.redis.core.ScanOptions;
import org.thingsboard.server.transport.lwm2m.server.client.LwM2MClientState;
import org.thingsboard.server.transport.lwm2m.server.client.LwM2mClient;
import java.util.List;
import java.util.Set;
import java.util.function.Consumer;
import static org.assertj.core.api.Assertions.assertThat;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.Mockito.doAnswer;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.never;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
import static org.thingsboard.server.transport.lwm2m.server.store.util.LwM2MClientSerDes.serialize;
/**
* Verifies that {@link TbRedisLwM2MClientStore#getAll()} uses separate connections for
* SCAN and GET operations to prevent Jedis 5.x response-ordering corruption that occurs
* when both commands share the same connection.
*/
@ExtendWith(MockitoExtension.class)
class TbRedisLwM2MClientStoreTest {
@Mock
RedisConnectionFactory connectionFactory;
@Mock
RedisConnection scanConnection;
@Mock
RedisConnection getConnection;
TbRedisLwM2MClientStore store;
@BeforeEach
void setUp() {
// First getConnection() call → scanConnection, second → getConnection
when(connectionFactory.getConnection())
.thenReturn(scanConnection)
.thenReturn(getConnection);
store = new TbRedisLwM2MClientStore(connectionFactory);
}
@Test
void getAll_returnsSingleClient() {
LwM2mClient client = new LwM2mClient("nodeId", "testEndpoint");
client.setState(LwM2MClientState.REGISTERED);
byte[] key = "CLIENT#EP#testEndpoint".getBytes();
byte[] value = serialize(client);
// Cursor created before thenReturn to avoid Mockito unfinished-stubbing error
Cursor<byte[]> cursor = cursorOf(key);
when(scanConnection.scan(any(ScanOptions.class))).thenReturn(cursor);
when(getConnection.get(key)).thenReturn(value);
Set<LwM2mClient> result = store.getAll();
assertThat(result).hasSize(1);
assertThat(result.iterator().next().getEndpoint()).isEqualTo("testEndpoint");
}
@Test
void getAll_getIsNeverCalledOnScanConnection() {
Cursor<byte[]> cursor = cursorOf();
when(scanConnection.scan(any(ScanOptions.class))).thenReturn(cursor);
store.getAll();
verify(scanConnection, never()).get(any(byte[].class));
}
@Test
void getAll_scanIsNeverCalledOnGetConnection() {
Cursor<byte[]> cursor = cursorOf();
when(scanConnection.scan(any(ScanOptions.class))).thenReturn(cursor);
store.getAll();
verify(getConnection, never()).scan(any(ScanOptions.class));
}
@Test
void getAll_skipsKeyWhenValueIsNull() {
byte[] key = "CLIENT#EP#gone".getBytes();
Cursor<byte[]> cursor = cursorOf(key);
when(scanConnection.scan(any(ScanOptions.class))).thenReturn(cursor);
// getConnection.get(key) returns null by default — no stubbing needed
Set<LwM2mClient> result = store.getAll();
assertThat(result).isEmpty();
}
/**
* Creates a mock {@link Cursor} that iterates over the given keys via {@code forEachRemaining}.
* The cursor is created separately (not inside a {@code thenReturn()} argument) to avoid
* Mockito's "unfinished stubbing" error caused by nested {@code when()} calls.
*/
@SuppressWarnings("unchecked")
private static Cursor<byte[]> cursorOf(byte[]... keys) {
Cursor<byte[]> cursor = mock(Cursor.class);
List<byte[]> keyList = List.of(keys);
doAnswer(inv -> {
Consumer<byte[]> action = inv.getArgument(0);
keyList.forEach(action);
return null;
}).when(cursor).forEachRemaining(any(Consumer.class));
return cursor;
}
}

22
common/transport/mqtt/src/main/java/org/thingsboard/server/transport/mqtt/MqttTransportContext.java

@ -88,20 +88,30 @@ public class MqttTransportContext extends TransportContext {
@Value("${transport.mqtt.proxy_enabled:false}") @Value("${transport.mqtt.proxy_enabled:false}")
private boolean proxyEnabled; private boolean proxyEnabled;
private final AtomicInteger connectionsCounter = new AtomicInteger(); private final AtomicInteger connectionsActiveCounterMQTT = new AtomicInteger();
private final AtomicInteger connectionsActiveCounterMQTTS = new AtomicInteger();
@PostConstruct @PostConstruct
public void init() { public void init() {
super.init(); super.init();
transportService.createGaugeStats("openConnections", connectionsCounter); transportService.createGaugeStats("connections_active", connectionsActiveCounterMQTT, "protocol", "MQTT");
transportService.createGaugeStats("connections_active", connectionsActiveCounterMQTTS, "protocol", "MQTTS");
} }
public void channelRegistered() { public void channelRegistered(boolean isSSL) {
connectionsCounter.incrementAndGet(); if (isSSL) {
connectionsActiveCounterMQTTS.incrementAndGet();
} else {
connectionsActiveCounterMQTT.incrementAndGet();
}
} }
public void channelUnregistered() { public void channelUnregistered(boolean isSSL) {
connectionsCounter.decrementAndGet(); if (isSSL) {
connectionsActiveCounterMQTTS.decrementAndGet();
} else {
connectionsActiveCounterMQTT.decrementAndGet();
}
} }
public boolean checkAddress(InetSocketAddress address) { public boolean checkAddress(InetSocketAddress address) {

31
common/transport/mqtt/src/main/java/org/thingsboard/server/transport/mqtt/MqttTransportHandler.java

@ -180,16 +180,20 @@ public class MqttTransportHandler extends ChannelInboundHandlerAdapter implement
this.rpcAwaitingAck = new ConcurrentHashMap<>(); this.rpcAwaitingAck = new ConcurrentHashMap<>();
} }
boolean isSSL() {
return sslHandler != null;
}
@Override @Override
public void channelRegistered(ChannelHandlerContext ctx) throws Exception { public void channelRegistered(ChannelHandlerContext ctx) throws Exception {
super.channelRegistered(ctx); super.channelRegistered(ctx);
context.channelRegistered(); context.channelRegistered(isSSL());
} }
@Override @Override
public void channelUnregistered(ChannelHandlerContext ctx) throws Exception { public void channelUnregistered(ChannelHandlerContext ctx) throws Exception {
super.channelUnregistered(ctx); super.channelUnregistered(ctx);
context.channelUnregistered(); context.channelUnregistered(isSSL());
} }
@Override @Override
@ -254,6 +258,17 @@ public class MqttTransportHandler extends ChannelInboundHandlerAdapter implement
} }
} }
String getClientAddr(ChannelHandlerContext ctx) {
try {
InetSocketAddress remote = getAddress(ctx);
if (remote == null) return "unknown";
String host = remote.getAddress() != null ? remote.getAddress().getHostAddress() : remote.getHostString();
return host + ":" + remote.getPort();
} catch (Exception ignored) {
return "unknown";
}
}
InetSocketAddress getAddress(ChannelHandlerContext ctx) { InetSocketAddress getAddress(ChannelHandlerContext ctx) {
var address = ctx.channel().attr(MqttTransportService.ADDRESS).get(); var address = ctx.channel().attr(MqttTransportService.ADDRESS).get();
if (address == null) { if (address == null) {
@ -1150,19 +1165,25 @@ public class MqttTransportHandler extends ChannelInboundHandlerAdapter implement
public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) { public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) {
if (cause instanceof IOException) { if (cause instanceof IOException) {
if (log.isDebugEnabled()) { if (log.isDebugEnabled()) {
log.debug("[{}][{}][{}] IOException: {}", sessionId, String clientAddr = getClientAddr(ctx);
log.debug("[{}][{}][{}][{}] {}: {}", sessionId,
Optional.ofNullable(this.deviceSessionCtx.getDeviceInfo()).map(TransportDeviceInfo::getDeviceId).orElse(null), Optional.ofNullable(this.deviceSessionCtx.getDeviceInfo()).map(TransportDeviceInfo::getDeviceId).orElse(null),
Optional.ofNullable(this.deviceSessionCtx.getDeviceInfo()).map(TransportDeviceInfo::getDeviceName).orElse(""), Optional.ofNullable(this.deviceSessionCtx.getDeviceInfo()).map(TransportDeviceInfo::getDeviceName).orElse(""),
clientAddr,
cause.getClass().getSimpleName(),
cause.getMessage(), cause.getMessage(),
cause); cause);
} else if (log.isInfoEnabled()) { } else if (log.isInfoEnabled()) {
log.info("[{}][{}][{}] IOException: {}", sessionId, String clientAddr = getClientAddr(ctx);
log.info("[{}][{}][{}][{}] {}: {}", sessionId,
Optional.ofNullable(this.deviceSessionCtx.getDeviceInfo()).map(TransportDeviceInfo::getDeviceId).orElse(null), Optional.ofNullable(this.deviceSessionCtx.getDeviceInfo()).map(TransportDeviceInfo::getDeviceId).orElse(null),
Optional.ofNullable(this.deviceSessionCtx.getDeviceInfo()).map(TransportDeviceInfo::getDeviceName).orElse(""), Optional.ofNullable(this.deviceSessionCtx.getDeviceInfo()).map(TransportDeviceInfo::getDeviceName).orElse(""),
clientAddr,
cause.getClass().getSimpleName(),
cause.getMessage()); cause.getMessage());
} }
} else { } else {
log.error("[{}] Unexpected Exception", sessionId, cause); log.error("[{}][{}] Unexpected Exception", sessionId, getClientAddr(ctx), cause);
} }
closeCtx(ctx, MqttReasonCodes.Disconnect.SERVER_SHUTTING_DOWN); closeCtx(ctx, MqttReasonCodes.Disconnect.SERVER_SHUTTING_DOWN);

2
common/transport/transport-api/src/main/java/org/thingsboard/server/common/transport/TransportService.java

@ -161,5 +161,5 @@ public interface TransportService {
boolean hasSession(SessionInfoProto sessionInfo); boolean hasSession(SessionInfoProto sessionInfo);
void createGaugeStats(String openConnections, AtomicInteger connectionsCounter); void createGaugeStats(String statsName, AtomicInteger number, String... tags);
} }

18
common/transport/transport-api/src/main/java/org/thingsboard/server/common/transport/service/DefaultTransportService.java

@ -1257,9 +1257,21 @@ public class DefaultTransportService extends TransportActivityManager implements
} }
@Override @Override
public void createGaugeStats(String statsName, AtomicInteger number) { public void createGaugeStats(String statsName, AtomicInteger number, String... tags) {
statsFactory.createGauge(StatsType.TRANSPORT + "." + statsName, number); String key = "thingsboard" + "." + StatsType.TRANSPORT.getName() + "." + statsName;
statsMap.put(statsName, number); statsFactory.createGauge(key, number, tags);
statsMap.put(statsName + tagsKey(tags), number);
}
private String tagsKey(String... tags) {
if (tags == null || tags.length < 2) return "";
StringBuilder sb = new StringBuilder("[");
for (int i = 0; i < tags.length; i += 2) {
if (i > 0) sb.append(',');
sb.append(tags[i]).append('=').append(i + 1 < tags.length ? tags[i + 1] : "");
}
sb.append(']');
return sb.toString();
} }
@Scheduled(fixedDelayString = "${transport.stats.print-interval-ms:60000}") @Scheduled(fixedDelayString = "${transport.stats.print-interval-ms:60000}")

1
dao/pom.xml

@ -280,7 +280,6 @@
<plugin> <plugin>
<groupId>org.apache.maven.plugins</groupId> <groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-jar-plugin</artifactId> <artifactId>maven-jar-plugin</artifactId>
<version>${jar-plugin.version}</version>
<executions> <executions>
<execution> <execution>
<goals> <goals>

1
dao/src/main/java/org/thingsboard/server/dao/model/ModelConstants.java

@ -730,6 +730,7 @@ public class ModelConstants {
public static final String CALCULATED_FIELD_CONFIGURATION_VERSION = "configuration_version"; public static final String CALCULATED_FIELD_CONFIGURATION_VERSION = "configuration_version";
public static final String CALCULATED_FIELD_CONFIGURATION = "configuration"; public static final String CALCULATED_FIELD_CONFIGURATION = "configuration";
public static final String CALCULATED_FIELD_VERSION = "version"; public static final String CALCULATED_FIELD_VERSION = "version";
public static final String CALCULATED_FIELD_ADDITIONAL_INFO = ADDITIONAL_INFO_PROPERTY;
/** /**
* Tasks constants. * Tasks constants.

11
dao/src/main/java/org/thingsboard/server/dao/model/sql/CalculatedFieldEntity.java

@ -36,6 +36,7 @@ import org.thingsboard.server.dao.util.mapping.JsonConverter;
import java.util.UUID; import java.util.UUID;
import static org.thingsboard.server.dao.model.ModelConstants.CALCULATED_FIELD_ADDITIONAL_INFO;
import static org.thingsboard.server.dao.model.ModelConstants.CALCULATED_FIELD_CONFIGURATION; import static org.thingsboard.server.dao.model.ModelConstants.CALCULATED_FIELD_CONFIGURATION;
import static org.thingsboard.server.dao.model.ModelConstants.CALCULATED_FIELD_CONFIGURATION_VERSION; import static org.thingsboard.server.dao.model.ModelConstants.CALCULATED_FIELD_CONFIGURATION_VERSION;
import static org.thingsboard.server.dao.model.ModelConstants.CALCULATED_FIELD_ENTITY_ID; import static org.thingsboard.server.dao.model.ModelConstants.CALCULATED_FIELD_ENTITY_ID;
@ -81,9 +82,11 @@ public class CalculatedFieldEntity extends BaseVersionedEntity<CalculatedField>
@Column(name = DEBUG_SETTINGS) @Column(name = DEBUG_SETTINGS)
private String debugSettings; private String debugSettings;
public CalculatedFieldEntity() { @Convert(converter = JsonConverter.class)
super(); @Column(name = CALCULATED_FIELD_ADDITIONAL_INFO)
} private JsonNode additionalInfo;
public CalculatedFieldEntity() {}
public CalculatedFieldEntity(CalculatedField calculatedField) { public CalculatedFieldEntity(CalculatedField calculatedField) {
this.setUuid(calculatedField.getUuidId()); this.setUuid(calculatedField.getUuidId());
@ -97,6 +100,7 @@ public class CalculatedFieldEntity extends BaseVersionedEntity<CalculatedField>
this.configuration = JacksonUtil.valueToTree(calculatedField.getConfiguration()); this.configuration = JacksonUtil.valueToTree(calculatedField.getConfiguration());
this.version = calculatedField.getVersion(); this.version = calculatedField.getVersion();
this.debugSettings = JacksonUtil.toString(calculatedField.getDebugSettings()); this.debugSettings = JacksonUtil.toString(calculatedField.getDebugSettings());
this.additionalInfo = calculatedField.getAdditionalInfo();
} }
@Override @Override
@ -111,6 +115,7 @@ public class CalculatedFieldEntity extends BaseVersionedEntity<CalculatedField>
calculatedField.setConfiguration(JacksonUtil.treeToValue(configuration, CalculatedFieldConfiguration.class)); calculatedField.setConfiguration(JacksonUtil.treeToValue(configuration, CalculatedFieldConfiguration.class));
calculatedField.setVersion(version); calculatedField.setVersion(version);
calculatedField.setDebugSettings(JacksonUtil.fromString(debugSettings, DebugSettings.class)); calculatedField.setDebugSettings(JacksonUtil.fromString(debugSettings, DebugSettings.class));
calculatedField.setAdditionalInfo(additionalInfo);
return calculatedField; return calculatedField;
} }

2
dao/src/main/java/org/thingsboard/server/dao/notification/NotificationRequestDao.java

@ -50,7 +50,7 @@ public interface NotificationRequestDao extends Dao<NotificationRequest> {
boolean existsByTenantIdAndStatusAndTemplateId(TenantId tenantId, NotificationRequestStatus status, NotificationTemplateId templateId); boolean existsByTenantIdAndStatusAndTemplateId(TenantId tenantId, NotificationRequestStatus status, NotificationTemplateId templateId);
int removeAllByCreatedTimeBefore(long ts); int removeByTenantIdAndCreatedTimeBeforeBatch(TenantId tenantId, long ts, int batchSize);
NotificationRequestInfo findInfoById(TenantId tenantId, NotificationRequestId id); NotificationRequestInfo findInfoById(TenantId tenantId, NotificationRequestId id);

58
dao/src/main/java/org/thingsboard/server/dao/ota/BaseOtaPackageService.java

@ -196,7 +196,9 @@ public class BaseOtaPackageService extends AbstractCachedEntityService<OtaPackag
log.trace("Executing deleteOtaPackage [{}]", otaPackageId); log.trace("Executing deleteOtaPackage [{}]", otaPackageId);
validateId(otaPackageId, id -> INCORRECT_OTA_PACKAGE_ID + id); validateId(otaPackageId, id -> INCORRECT_OTA_PACKAGE_ID + id);
try { try {
Long oid = getDataOidById(tenantId, otaPackageId);
otaPackageDao.removeById(tenantId, otaPackageId.getId()); otaPackageDao.removeById(tenantId, otaPackageId.getId());
unlinkDataIfPresent(tenantId, otaPackageId, oid);
publishEvictEvent(new OtaPackageCacheEvictEvent(otaPackageId)); publishEvictEvent(new OtaPackageCacheEvictEvent(otaPackageId));
eventPublisher.publishEvent(DeleteEntityEvent.builder().tenantId(tenantId).entityId(otaPackageId).build()); eventPublisher.publishEvent(DeleteEntityEvent.builder().tenantId(tenantId).entityId(otaPackageId).build());
} catch (Exception t) { } catch (Exception t) {
@ -215,6 +217,30 @@ public class BaseOtaPackageService extends AbstractCachedEntityService<OtaPackag
} }
} }
private Long getDataOidById(TenantId tenantId, OtaPackageId otaPackageId) {
try {
log.trace("Executing getDataOidById tenantId [{}], otaPackageId [{}]", tenantId, otaPackageId);
return otaPackageDao.getDataOidById(otaPackageId.getId());
} catch (Exception e) {
log.warn("[{}][{}] Failed to retrieve OID before deletion", tenantId, otaPackageId, e);
}
return null;
}
private void unlinkDataIfPresent(TenantId tenantId, OtaPackageId otaPackageId, Long oid) {
if (oid == null) {
return;
}
try {
Integer result = otaPackageDao.unlinkLargeObject(oid);
if (result != 1) {
log.warn("[{}][{}] Failed to delete large object (OID: {}). Result code: {}", tenantId, otaPackageId, oid, result);
}
} catch (Exception e) {
log.warn("[{}][{}] Failed to delete large object (OID: {})", tenantId, otaPackageId, oid, e);
}
}
@Override @Override
public void deleteEntity(TenantId tenantId, EntityId id, boolean force) { public void deleteEntity(TenantId tenantId, EntityId id, boolean force) {
deleteOtaPackage(tenantId, (OtaPackageId) id); deleteOtaPackage(tenantId, (OtaPackageId) id);
@ -225,6 +251,11 @@ public class BaseOtaPackageService extends AbstractCachedEntityService<OtaPackag
return otaPackageDao.sumDataSizeByTenantId(tenantId); return otaPackageDao.sumDataSizeByTenantId(tenantId);
} }
@Override
public void deleteByTenantId(TenantId tenantId) {
deleteOtaPackagesByTenantId(tenantId);
}
@Override @Override
public void deleteOtaPackagesByTenantId(TenantId tenantId) { public void deleteOtaPackagesByTenantId(TenantId tenantId) {
log.trace("Executing deleteOtaPackagesByTenantId, tenantId [{}]", tenantId); log.trace("Executing deleteOtaPackagesByTenantId, tenantId [{}]", tenantId);
@ -232,24 +263,17 @@ public class BaseOtaPackageService extends AbstractCachedEntityService<OtaPackag
tenantOtaPackageRemover.removeEntities(tenantId, tenantId); tenantOtaPackageRemover.removeEntities(tenantId, tenantId);
} }
@Override private final PaginatedRemover<TenantId, OtaPackageInfo> tenantOtaPackageRemover = new PaginatedRemover<>() {
public void deleteByTenantId(TenantId tenantId) { @Override
deleteOtaPackagesByTenantId(tenantId); protected PageData<OtaPackageInfo> findEntities(TenantId tenantId, TenantId id, PageLink pageLink) {
} return otaPackageInfoDao.findOtaPackageInfoByTenantId(id, pageLink);
}
private PaginatedRemover<TenantId, OtaPackageInfo> tenantOtaPackageRemover =
new PaginatedRemover<>() {
@Override
protected PageData<OtaPackageInfo> findEntities(TenantId tenantId, TenantId id, PageLink pageLink) {
return otaPackageInfoDao.findOtaPackageInfoByTenantId(id, pageLink);
}
@Override @Override
protected void removeEntity(TenantId tenantId, OtaPackageInfo entity) { protected void removeEntity(TenantId tenantId, OtaPackageInfo entity) {
deleteOtaPackage(tenantId, entity.getId()); deleteOtaPackage(tenantId, entity.getId());
} }
}; };
@Override @Override
public Optional<HasId<?>> findEntity(TenantId tenantId, EntityId entityId) { public Optional<HasId<?>> findEntity(TenantId tenantId, EntityId entityId) {

7
dao/src/main/java/org/thingsboard/server/dao/ota/OtaPackageDao.java

@ -18,15 +18,20 @@ package org.thingsboard.server.dao.ota;
import org.thingsboard.server.common.data.OtaPackage; import org.thingsboard.server.common.data.OtaPackage;
import org.thingsboard.server.common.data.id.OtaPackageId; import org.thingsboard.server.common.data.id.OtaPackageId;
import org.thingsboard.server.common.data.id.TenantId; import org.thingsboard.server.common.data.id.TenantId;
import org.thingsboard.server.common.data.ota.OtaPackageType;
import org.thingsboard.server.dao.Dao; import org.thingsboard.server.dao.Dao;
import org.thingsboard.server.dao.ExportableEntityDao; import org.thingsboard.server.dao.ExportableEntityDao;
import org.thingsboard.server.dao.TenantEntityWithDataDao; import org.thingsboard.server.dao.TenantEntityWithDataDao;
import java.util.UUID;
public interface OtaPackageDao extends Dao<OtaPackage>, TenantEntityWithDataDao, ExportableEntityDao<OtaPackageId, OtaPackage> { public interface OtaPackageDao extends Dao<OtaPackage>, TenantEntityWithDataDao, ExportableEntityDao<OtaPackageId, OtaPackage> {
Long sumDataSizeByTenantId(TenantId tenantId); Long sumDataSizeByTenantId(TenantId tenantId);
OtaPackage findOtaPackageByTenantIdAndTitleAndVersion(TenantId tenantId, String title, String version); OtaPackage findOtaPackageByTenantIdAndTitleAndVersion(TenantId tenantId, String title, String version);
Long getDataOidById(UUID id);
Integer unlinkLargeObject(Long dataOid);
} }

7
dao/src/main/java/org/thingsboard/server/dao/pat/ApiKeyDao.java

@ -17,15 +17,22 @@ package org.thingsboard.server.dao.pat;
import org.thingsboard.server.common.data.id.TenantId; import org.thingsboard.server.common.data.id.TenantId;
import org.thingsboard.server.common.data.id.UserId; import org.thingsboard.server.common.data.id.UserId;
import org.thingsboard.server.common.data.page.PageData;
import org.thingsboard.server.common.data.page.PageLink;
import org.thingsboard.server.common.data.pat.ApiKey; import org.thingsboard.server.common.data.pat.ApiKey;
import org.thingsboard.server.dao.Dao; import org.thingsboard.server.dao.Dao;
import java.util.List;
import java.util.Set; import java.util.Set;
public interface ApiKeyDao extends Dao<ApiKey> { public interface ApiKeyDao extends Dao<ApiKey> {
ApiKey findByValue(String value); ApiKey findByValue(String value);
PageData<ApiKey> findByTenantId(TenantId tenantId, PageLink pageLink);
List<ApiKey> findByTenantIdAndUserId(TenantId tenantId, UserId userId);
Set<String> deleteByTenantId(TenantId tenantId); Set<String> deleteByTenantId(TenantId tenantId);
Set<String> deleteByUserId(TenantId tenantId, UserId userId); Set<String> deleteByUserId(TenantId tenantId, UserId userId);

44
dao/src/main/java/org/thingsboard/server/dao/pat/ApiKeyServiceImpl.java

@ -24,6 +24,7 @@ import org.springframework.stereotype.Service;
import org.springframework.transaction.event.TransactionalEventListener; import org.springframework.transaction.event.TransactionalEventListener;
import org.thingsboard.server.common.data.EntityType; import org.thingsboard.server.common.data.EntityType;
import org.thingsboard.server.common.data.StringUtils; import org.thingsboard.server.common.data.StringUtils;
import org.thingsboard.server.exception.DataValidationException;
import org.thingsboard.server.common.data.id.ApiKeyId; import org.thingsboard.server.common.data.id.ApiKeyId;
import org.thingsboard.server.common.data.id.EntityId; import org.thingsboard.server.common.data.id.EntityId;
import org.thingsboard.server.common.data.id.HasId; import org.thingsboard.server.common.data.id.HasId;
@ -34,9 +35,11 @@ import org.thingsboard.server.common.data.page.PageLink;
import org.thingsboard.server.common.data.pat.ApiKey; import org.thingsboard.server.common.data.pat.ApiKey;
import org.thingsboard.server.common.data.pat.ApiKeyInfo; import org.thingsboard.server.common.data.pat.ApiKeyInfo;
import org.thingsboard.server.dao.entity.AbstractCachedEntityService; import org.thingsboard.server.dao.entity.AbstractCachedEntityService;
import org.thingsboard.server.dao.eventsourcing.DeleteEntityEvent;
import org.thingsboard.server.dao.eventsourcing.SaveEntityEvent; import org.thingsboard.server.dao.eventsourcing.SaveEntityEvent;
import org.thingsboard.server.dao.service.validator.ApiKeyDataValidator; import org.thingsboard.server.dao.service.validator.ApiKeyDataValidator;
import java.util.List;
import java.util.Optional; import java.util.Optional;
import java.util.Set; import java.util.Set;
import java.util.UUID; import java.util.UUID;
@ -73,13 +76,20 @@ public class ApiKeyServiceImpl extends AbstractCachedEntityService<ApiKeyCacheKe
@Override @Override
public ApiKey saveApiKey(TenantId tenantId, ApiKeyInfo apiKeyInfo) { public ApiKey saveApiKey(TenantId tenantId, ApiKeyInfo apiKeyInfo) {
return saveApiKey(tenantId, apiKeyInfo, null, true);
}
@Override
public ApiKey saveApiKey(TenantId tenantId, ApiKeyInfo apiKeyInfo, String value, boolean doValidate) {
log.trace("Executing saveApiKey [{}]", apiKeyInfo); log.trace("Executing saveApiKey [{}]", apiKeyInfo);
try { try {
var apiKey = new ApiKey(apiKeyInfo); var apiKey = new ApiKey(apiKeyInfo);
var old = apiKeyValidator.validate(apiKey, ApiKeyInfo::getTenantId); ApiKey old = doValidate ? apiKeyValidator.validate(apiKey, ApiKeyInfo::getTenantId) :
if (old == null) { (apiKey.getId() != null ? apiKeyDao.findById(tenantId, apiKey.getUuidId()) : null);
String value = generateApiKeySecret(); if (value != null) {
apiKey.setValue(value); apiKey.setValue(value);
} else if (old == null) {
apiKey.setValue(generateApiKeySecret());
} else { } else {
apiKey.setValue(old.getValue()); apiKey.setValue(old.getValue());
} }
@ -109,6 +119,13 @@ public class ApiKeyServiceImpl extends AbstractCachedEntityService<ApiKeyCacheKe
return apiKeyInfoDao.findByUserId(tenantId, userId, pageLink); return apiKeyInfoDao.findByUserId(tenantId, userId, pageLink);
} }
@Override
public List<ApiKey> findApiKeysByUserId(TenantId tenantId, UserId userId) {
log.trace("Executing findApiKeysByUserId [{}][{}]", tenantId, userId);
validateId(userId, id -> INCORRECT_USER_ID + id);
return apiKeyDao.findByTenantIdAndUserId(tenantId, userId);
}
@Override @Override
public Optional<HasId<?>> findEntity(TenantId tenantId, EntityId entityId) { public Optional<HasId<?>> findEntity(TenantId tenantId, EntityId entityId) {
return Optional.ofNullable(findApiKeyById(tenantId, new ApiKeyId(entityId.getId()))); return Optional.ofNullable(findApiKeyById(tenantId, new ApiKeyId(entityId.getId())));
@ -126,6 +143,20 @@ public class ApiKeyServiceImpl extends AbstractCachedEntityService<ApiKeyCacheKe
validateId(apiKeyId, id -> INCORRECT_API_KEY_ID + id); validateId(apiKeyId, id -> INCORRECT_API_KEY_ID + id);
apiKeyDao.removeById(tenantId, apiKeyId); apiKeyDao.removeById(tenantId, apiKeyId);
publishEvictEvent(new ApiKeyEvictEvent(apiKey.getValue())); publishEvictEvent(new ApiKeyEvictEvent(apiKey.getValue()));
eventPublisher.publishEvent(DeleteEntityEvent.builder().tenantId(tenantId).entityId(apiKey.getId()).build());
}
@Override
public void deleteEntity(TenantId tenantId, EntityId id, boolean force) {
ApiKey apiKey = findApiKeyById(tenantId, new ApiKeyId(id.getId()));
if (apiKey == null) {
if (force) {
return;
} else {
throw new DataValidationException("Unable to delete non-existent API key.");
}
}
deleteApiKey(tenantId, apiKey, force);
} }
@Override @Override
@ -144,6 +175,13 @@ public class ApiKeyServiceImpl extends AbstractCachedEntityService<ApiKeyCacheKe
values.forEach(value -> publishEvictEvent(new ApiKeyEvictEvent(value))); values.forEach(value -> publishEvictEvent(new ApiKeyEvictEvent(value)));
} }
@Override
public PageData<ApiKey> findApiKeysByTenantId(TenantId tenantId, PageLink pageLink) {
log.trace("Executing findApiKeysByTenantId [{}]", tenantId);
validateId(tenantId, id -> INCORRECT_TENANT_ID + id);
return apiKeyDao.findByTenantId(tenantId, pageLink);
}
@Override @Override
public ApiKey findApiKeyByValue(String value) { public ApiKey findApiKeyByValue(String value) {
log.trace("Executing findApiKeyByValue [{}]", value); log.trace("Executing findApiKeyByValue [{}]", value);

3
dao/src/main/java/org/thingsboard/server/dao/rpc/RpcDao.java

@ -24,12 +24,13 @@ import org.thingsboard.server.common.data.rpc.RpcStatus;
import org.thingsboard.server.dao.Dao; import org.thingsboard.server.dao.Dao;
public interface RpcDao extends Dao<Rpc> { public interface RpcDao extends Dao<Rpc> {
PageData<Rpc> findAllByDeviceId(TenantId tenantId, DeviceId deviceId, PageLink pageLink); PageData<Rpc> findAllByDeviceId(TenantId tenantId, DeviceId deviceId, PageLink pageLink);
PageData<Rpc> findAllByDeviceIdAndStatus(TenantId tenantId, DeviceId deviceId, RpcStatus rpcStatus, PageLink pageLink); PageData<Rpc> findAllByDeviceIdAndStatus(TenantId tenantId, DeviceId deviceId, RpcStatus rpcStatus, PageLink pageLink);
PageData<Rpc> findAllRpcByTenantId(TenantId tenantId, PageLink pageLink); PageData<Rpc> findAllRpcByTenantId(TenantId tenantId, PageLink pageLink);
int deleteOutdatedRpcByTenantId(TenantId tenantId, Long expirationTime); int deleteOutdatedRpcByTenantIdBatch(TenantId tenantId, Long expirationTime, int batchSize);
} }

2
dao/src/main/java/org/thingsboard/server/dao/sql/cf/DefaultNativeCalculatedFieldRepository.java

@ -74,6 +74,7 @@ public class DefaultNativeCalculatedFieldRepository implements NativeCalculatedF
JsonNode configuration = JacksonUtil.toJsonNode((String) row.get("configuration")); JsonNode configuration = JacksonUtil.toJsonNode((String) row.get("configuration"));
long version = row.get("version") != null ? (long) row.get("version") : 0; long version = row.get("version") != null ? (long) row.get("version") : 0;
String debugSettings = (String) row.get("debug_settings"); String debugSettings = (String) row.get("debug_settings");
JsonNode additionalInfo = JacksonUtil.toJsonNode((String) row.get("additional_info"));
CalculatedField calculatedField = new CalculatedField(); CalculatedField calculatedField = new CalculatedField();
calculatedField.setId(new CalculatedFieldId(id)); calculatedField.setId(new CalculatedFieldId(id));
@ -91,6 +92,7 @@ public class DefaultNativeCalculatedFieldRepository implements NativeCalculatedF
} }
calculatedField.setVersion(version); calculatedField.setVersion(version);
calculatedField.setDebugSettings(JacksonUtil.fromString(debugSettings, DebugSettings.class)); calculatedField.setDebugSettings(JacksonUtil.fromString(debugSettings, DebugSettings.class));
calculatedField.setAdditionalInfo(additionalInfo);
return calculatedField; return calculatedField;
}).collect(Collectors.toList()); }).collect(Collectors.toList());

4
dao/src/main/java/org/thingsboard/server/dao/sql/notification/JpaNotificationRequestDao.java

@ -98,8 +98,8 @@ public class JpaNotificationRequestDao extends JpaAbstractDao<NotificationReques
} }
@Override @Override
public int removeAllByCreatedTimeBefore(long ts) { public int removeByTenantIdAndCreatedTimeBeforeBatch(TenantId tenantId, long ts, int batchSize) {
return notificationRequestRepository.deleteAllByCreatedTimeBefore(ts); return notificationRequestRepository.deleteByTenantIdAndCreatedTimeBeforeBatch(tenantId.getId(), ts, batchSize);
} }
@Override @Override

8
dao/src/main/java/org/thingsboard/server/dao/sql/notification/NotificationRequestRepository.java

@ -71,8 +71,12 @@ public interface NotificationRequestRepository extends JpaRepository<Notificatio
@Transactional @Transactional
@Modifying @Modifying
@Query("DELETE FROM NotificationRequestEntity r WHERE r.createdTime < :ts") @Query(value = "DELETE FROM notification_request WHERE id IN " +
int deleteAllByCreatedTimeBefore(@Param("ts") long ts); "(SELECT id FROM notification_request WHERE tenant_id = :tenantId AND created_time < :ts LIMIT :batchSize)",
nativeQuery = true)
int deleteByTenantIdAndCreatedTimeBeforeBatch(@Param("tenantId") UUID tenantId,
@Param("ts") long ts,
@Param("batchSize") int batchSize);
@Transactional @Transactional
@Modifying @Modifying

10
dao/src/main/java/org/thingsboard/server/dao/sql/ota/JpaOtaPackageDao.java

@ -77,6 +77,16 @@ public class JpaOtaPackageDao extends JpaAbstractDao<OtaPackageEntity, OtaPackag
return DaoUtil.getData(otaPackageRepository.findByTenantIdAndExternalId(tenantId, externalId)); return DaoUtil.getData(otaPackageRepository.findByTenantIdAndExternalId(tenantId, externalId));
} }
@Override
public Long getDataOidById(UUID id) {
return otaPackageRepository.getDataOidById(id);
}
@Override
public Integer unlinkLargeObject(Long dataOid) {
return otaPackageRepository.unlinkLargeObject(dataOid);
}
@Override @Override
public OtaPackageId getExternalIdByInternal(OtaPackageId internalId) { public OtaPackageId getExternalIdByInternal(OtaPackageId internalId) {
return DaoUtil.toEntityId(otaPackageRepository.getExternalIdById(internalId.getId()), OtaPackageId::new); return DaoUtil.toEntityId(otaPackageRepository.getExternalIdById(internalId.getId()), OtaPackageId::new);

10
dao/src/main/java/org/thingsboard/server/dao/sql/ota/OtaPackageRepository.java

@ -20,7 +20,7 @@ import org.springframework.data.domain.Pageable;
import org.springframework.data.jpa.repository.JpaRepository; import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.data.jpa.repository.Query; import org.springframework.data.jpa.repository.Query;
import org.springframework.data.repository.query.Param; import org.springframework.data.repository.query.Param;
import org.thingsboard.server.common.data.ota.OtaPackageType; import org.springframework.transaction.annotation.Transactional;
import org.thingsboard.server.dao.ExportableEntityRepository; import org.thingsboard.server.dao.ExportableEntityRepository;
import org.thingsboard.server.dao.model.sql.OtaPackageEntity; import org.thingsboard.server.dao.model.sql.OtaPackageEntity;
@ -41,4 +41,12 @@ public interface OtaPackageRepository extends JpaRepository<OtaPackageEntity, UU
@Query("SELECT r.id FROM OtaPackageEntity r WHERE r.tenantId = :tenantId") @Query("SELECT r.id FROM OtaPackageEntity r WHERE r.tenantId = :tenantId")
Page<UUID> findIdsByTenantId(@Param("tenantId") UUID tenantId, Pageable pageable); Page<UUID> findIdsByTenantId(@Param("tenantId") UUID tenantId, Pageable pageable);
// The 'data' column is of type OID (PostgreSQL large object reference), so it returns the OID as Long
@Query(value = "SELECT data FROM ota_package WHERE id = :id AND data IS NOT NULL", nativeQuery = true)
Long getDataOidById(@Param("id") UUID id);
@Transactional
@Query(value = "SELECT lo_unlink(:oid)", nativeQuery = true)
Integer unlinkLargeObject(@Param("oid") Long oid);
} }

7
dao/src/main/java/org/thingsboard/server/dao/sql/pat/ApiKeyRepository.java

@ -15,6 +15,8 @@
*/ */
package org.thingsboard.server.dao.sql.pat; package org.thingsboard.server.dao.sql.pat;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.Pageable;
import org.springframework.data.jpa.repository.JpaRepository; import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.data.jpa.repository.Modifying; import org.springframework.data.jpa.repository.Modifying;
import org.springframework.data.jpa.repository.Query; import org.springframework.data.jpa.repository.Query;
@ -22,6 +24,7 @@ import org.springframework.data.repository.query.Param;
import org.springframework.transaction.annotation.Transactional; import org.springframework.transaction.annotation.Transactional;
import org.thingsboard.server.dao.model.sql.ApiKeyEntity; import org.thingsboard.server.dao.model.sql.ApiKeyEntity;
import java.util.List;
import java.util.Set; import java.util.Set;
import java.util.UUID; import java.util.UUID;
@ -29,6 +32,10 @@ public interface ApiKeyRepository extends JpaRepository<ApiKeyEntity, UUID> {
ApiKeyEntity findByValue(String value); ApiKeyEntity findByValue(String value);
Page<ApiKeyEntity> findByTenantId(UUID tenantId, Pageable pageable);
List<ApiKeyEntity> findByTenantIdAndUserId(UUID tenantId, UUID userId);
@Transactional @Transactional
@Modifying @Modifying
@Query(value = """ @Query(value = """

13
dao/src/main/java/org/thingsboard/server/dao/sql/pat/JpaApiKeyDao.java

@ -22,6 +22,8 @@ import org.springframework.stereotype.Component;
import org.thingsboard.server.common.data.EntityType; import org.thingsboard.server.common.data.EntityType;
import org.thingsboard.server.common.data.id.TenantId; import org.thingsboard.server.common.data.id.TenantId;
import org.thingsboard.server.common.data.id.UserId; import org.thingsboard.server.common.data.id.UserId;
import org.thingsboard.server.common.data.page.PageData;
import org.thingsboard.server.common.data.page.PageLink;
import org.thingsboard.server.common.data.pat.ApiKey; import org.thingsboard.server.common.data.pat.ApiKey;
import org.thingsboard.server.dao.DaoUtil; import org.thingsboard.server.dao.DaoUtil;
import org.thingsboard.server.dao.model.sql.ApiKeyEntity; import org.thingsboard.server.dao.model.sql.ApiKeyEntity;
@ -29,6 +31,7 @@ import org.thingsboard.server.dao.pat.ApiKeyDao;
import org.thingsboard.server.dao.sql.JpaAbstractDao; import org.thingsboard.server.dao.sql.JpaAbstractDao;
import org.thingsboard.server.dao.util.SqlDao; import org.thingsboard.server.dao.util.SqlDao;
import java.util.List;
import java.util.Set; import java.util.Set;
import java.util.UUID; import java.util.UUID;
@ -45,6 +48,16 @@ public class JpaApiKeyDao extends JpaAbstractDao<ApiKeyEntity, ApiKey> implement
return DaoUtil.getData(apiKeyRepository.findByValue(value)); return DaoUtil.getData(apiKeyRepository.findByValue(value));
} }
@Override
public PageData<ApiKey> findByTenantId(TenantId tenantId, PageLink pageLink) {
return DaoUtil.toPageData(apiKeyRepository.findByTenantId(tenantId.getId(), DaoUtil.toPageable(pageLink)));
}
@Override
public List<ApiKey> findByTenantIdAndUserId(TenantId tenantId, UserId userId) {
return DaoUtil.convertDataList(apiKeyRepository.findByTenantIdAndUserId(tenantId.getId(), userId.getId()));
}
@Override @Override
public Set<String> deleteByTenantId(TenantId tenantId) { public Set<String> deleteByTenantId(TenantId tenantId) {
return apiKeyRepository.deleteByTenantId(tenantId.getId()); return apiKeyRepository.deleteByTenantId(tenantId.getId());

4
dao/src/main/java/org/thingsboard/server/dao/sql/rpc/JpaRpcDao.java

@ -71,8 +71,8 @@ public class JpaRpcDao extends JpaAbstractDao<RpcEntity, Rpc> implements RpcDao,
@Transactional @Transactional
@Override @Override
public int deleteOutdatedRpcByTenantId(TenantId tenantId, Long expirationTime) { public int deleteOutdatedRpcByTenantIdBatch(TenantId tenantId, Long expirationTime, int batchSize) {
return rpcRepository.deleteOutdatedRpcByTenantId(tenantId.getId(), expirationTime); return rpcRepository.deleteOutdatedRpcByTenantIdBatch(tenantId.getId(), expirationTime, batchSize);
} }
@Override @Override

11
dao/src/main/java/org/thingsboard/server/dao/sql/rpc/RpcRepository.java

@ -21,20 +21,27 @@ import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.data.jpa.repository.Modifying; import org.springframework.data.jpa.repository.Modifying;
import org.springframework.data.jpa.repository.Query; import org.springframework.data.jpa.repository.Query;
import org.springframework.data.repository.query.Param; import org.springframework.data.repository.query.Param;
import org.springframework.transaction.annotation.Transactional;
import org.thingsboard.server.common.data.rpc.RpcStatus; import org.thingsboard.server.common.data.rpc.RpcStatus;
import org.thingsboard.server.dao.model.sql.RpcEntity; import org.thingsboard.server.dao.model.sql.RpcEntity;
import java.util.UUID; import java.util.UUID;
public interface RpcRepository extends JpaRepository<RpcEntity, UUID> { public interface RpcRepository extends JpaRepository<RpcEntity, UUID> {
Page<RpcEntity> findAllByTenantIdAndDeviceId(UUID tenantId, UUID deviceId, Pageable pageable); Page<RpcEntity> findAllByTenantIdAndDeviceId(UUID tenantId, UUID deviceId, Pageable pageable);
Page<RpcEntity> findAllByTenantIdAndDeviceIdAndStatus(UUID tenantId, UUID deviceId, RpcStatus status, Pageable pageable); Page<RpcEntity> findAllByTenantIdAndDeviceIdAndStatus(UUID tenantId, UUID deviceId, RpcStatus status, Pageable pageable);
Page<RpcEntity> findAllByTenantId(UUID tenantId, Pageable pageable); Page<RpcEntity> findAllByTenantId(UUID tenantId, Pageable pageable);
@Transactional
@Modifying @Modifying
@Query(value = "DELETE FROM rpc WHERE tenant_id = :tenantId AND created_time < :expirationTime", @Query(value = "DELETE FROM rpc WHERE id IN " +
"(SELECT id FROM rpc WHERE tenant_id = :tenantId AND created_time < :expirationTime LIMIT :batchSize)",
nativeQuery = true) nativeQuery = true)
int deleteOutdatedRpcByTenantId(@Param("tenantId") UUID tenantId, @Param("expirationTime") Long expirationTime); int deleteOutdatedRpcByTenantIdBatch(@Param("tenantId") UUID tenantId,
@Param("expirationTime") Long expirationTime,
@Param("batchSize") int batchSize);
} }

1
dao/src/main/resources/sql/schema-entities.sql

@ -935,6 +935,7 @@ CREATE TABLE IF NOT EXISTS calculated_field (
configuration varchar(1000000), configuration varchar(1000000),
version BIGINT DEFAULT 1, version BIGINT DEFAULT 1,
debug_settings varchar(1024), debug_settings varchar(1024),
additional_info varchar,
CONSTRAINT calculated_field_unq_key UNIQUE (entity_id, type, name) CONSTRAINT calculated_field_unq_key UNIQUE (entity_id, type, name)
); );

4
dao/src/test/java/org/thingsboard/server/dao/service/CalculatedFieldServiceTest.java

@ -18,6 +18,7 @@ package org.thingsboard.server.dao.service;
import org.apache.commons.lang3.RandomUtils; import org.apache.commons.lang3.RandomUtils;
import org.junit.Test; import org.junit.Test;
import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Autowired;
import org.thingsboard.common.util.JacksonUtil;
import org.thingsboard.server.common.data.AttributeScope; import org.thingsboard.server.common.data.AttributeScope;
import org.thingsboard.server.common.data.Device; import org.thingsboard.server.common.data.Device;
import org.thingsboard.server.common.data.TenantProfile; import org.thingsboard.server.common.data.TenantProfile;
@ -83,6 +84,7 @@ public class CalculatedFieldServiceTest extends AbstractServiceTest {
assertThat(savedCalculatedField.getType()).isEqualTo(calculatedField.getType()); assertThat(savedCalculatedField.getType()).isEqualTo(calculatedField.getType());
assertThat(savedCalculatedField.getName()).isEqualTo(calculatedField.getName()); assertThat(savedCalculatedField.getName()).isEqualTo(calculatedField.getName());
assertThat(savedCalculatedField.getConfiguration()).isEqualTo(calculatedField.getConfiguration()); assertThat(savedCalculatedField.getConfiguration()).isEqualTo(calculatedField.getConfiguration());
assertThat(savedCalculatedField.getAdditionalInfo()).isEqualTo(calculatedField.getAdditionalInfo());
assertThat(savedCalculatedField.getVersion()).isEqualTo(1L); assertThat(savedCalculatedField.getVersion()).isEqualTo(1L);
savedCalculatedField.setName("Test CF"); savedCalculatedField.setName("Test CF");
@ -90,6 +92,7 @@ public class CalculatedFieldServiceTest extends AbstractServiceTest {
CalculatedField updatedCalculatedField = calculatedFieldService.save(savedCalculatedField); CalculatedField updatedCalculatedField = calculatedFieldService.save(savedCalculatedField);
assertThat(updatedCalculatedField.getName()).isEqualTo(savedCalculatedField.getName()); assertThat(updatedCalculatedField.getName()).isEqualTo(savedCalculatedField.getName());
assertThat(updatedCalculatedField.getAdditionalInfo()).isEqualTo(savedCalculatedField.getAdditionalInfo());
assertThat(updatedCalculatedField.getVersion()).isEqualTo(savedCalculatedField.getVersion() + 1); assertThat(updatedCalculatedField.getVersion()).isEqualTo(savedCalculatedField.getVersion() + 1);
} }
@ -495,6 +498,7 @@ public class CalculatedFieldServiceTest extends AbstractServiceTest {
calculatedField.setName("Test Calculated Field"); calculatedField.setName("Test Calculated Field");
calculatedField.setConfigurationVersion(1); calculatedField.setConfigurationVersion(1);
calculatedField.setConfiguration(getCalculatedFieldConfig(referencedEntityId)); calculatedField.setConfiguration(getCalculatedFieldConfig(referencedEntityId));
calculatedField.setAdditionalInfo(JacksonUtil.newObjectNode());
return calculatedField; return calculatedField;
} }

52
dao/src/test/java/org/thingsboard/server/dao/service/OtaPackageServiceTest.java

@ -36,14 +36,14 @@ import org.thingsboard.server.common.data.page.PageLink;
import org.thingsboard.server.common.data.tenant.profile.DefaultTenantProfileConfiguration; import org.thingsboard.server.common.data.tenant.profile.DefaultTenantProfileConfiguration;
import org.thingsboard.server.dao.device.DeviceProfileService; import org.thingsboard.server.dao.device.DeviceProfileService;
import org.thingsboard.server.dao.device.DeviceService; import org.thingsboard.server.dao.device.DeviceService;
import org.thingsboard.server.exception.DataValidationException; import org.thingsboard.server.dao.ota.OtaPackageDao;
import org.thingsboard.server.dao.ota.OtaPackageService; import org.thingsboard.server.dao.ota.OtaPackageService;
import org.thingsboard.server.dao.tenant.TbTenantProfileCache; import org.thingsboard.server.dao.tenant.TbTenantProfileCache;
import org.thingsboard.server.dao.tenant.TenantProfileService; import org.thingsboard.server.dao.tenant.TenantProfileService;
import org.thingsboard.server.exception.DataValidationException;
import java.nio.ByteBuffer; import java.nio.ByteBuffer;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.Collections;
import java.util.List; import java.util.List;
import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThat;
@ -77,6 +77,8 @@ public class OtaPackageServiceTest extends AbstractServiceTest {
@Autowired @Autowired
TenantProfileService tenantProfileService; TenantProfileService tenantProfileService;
@Autowired @Autowired
OtaPackageDao otaPackageDao;
@Autowired
TbTenantProfileCache tenantProfileCache; TbTenantProfileCache tenantProfileCache;
@Before @Before
@ -118,10 +120,8 @@ public class OtaPackageServiceTest extends AbstractServiceTest {
Assert.assertEquals(1, otaPackageService.sumDataSizeByTenantId(tenantId)); Assert.assertEquals(1, otaPackageService.sumDataSizeByTenantId(tenantId));
int maxSumDataSize = 8; int maxSumDataSize = 8;
List<OtaPackage> packages = new ArrayList<>(maxSumDataSize);
for (int i = 2; i <= maxSumDataSize; i++) { for (int i = 2; i <= maxSumDataSize; i++) {
packages.add(createAndSaveFirmware(tenantId, "0." + i)); createAndSaveFirmware(tenantId, "0." + i);
Assert.assertEquals(i, otaPackageService.sumDataSizeByTenantId(tenantId)); Assert.assertEquals(i, otaPackageService.sumDataSizeByTenantId(tenantId));
} }
@ -533,6 +533,39 @@ public class OtaPackageServiceTest extends AbstractServiceTest {
Assert.assertNull(foundFirmware); Assert.assertNull(foundFirmware);
} }
@Test
public void testDeleteOtaPackageWithoutData() {
OtaPackageInfo firmwareInfo = new OtaPackageInfo();
firmwareInfo.setTenantId(tenantId);
firmwareInfo.setDeviceProfileId(deviceProfileId);
firmwareInfo.setType(FIRMWARE);
firmwareInfo.setTitle(TITLE);
firmwareInfo.setVersion(VERSION);
OtaPackageInfo savedFirmwareInfo = otaPackageService.saveOtaPackageInfo(firmwareInfo, false);
Assert.assertNotNull(savedFirmwareInfo);
Assert.assertNotNull(savedFirmwareInfo.getId());
// Should not throw NPE when deleting package without data (OID is null)
otaPackageService.deleteOtaPackage(tenantId, savedFirmwareInfo.getId());
OtaPackageInfo foundFirmware = otaPackageService.findOtaPackageInfoById(tenantId, savedFirmwareInfo.getId());
Assert.assertNull(foundFirmware);
}
@Test
public void testDeleteOtaPackageUnlinksLargeObject() {
OtaPackage savedFirmware = createAndSaveFirmware(tenantId, VERSION);
Long oid = otaPackageDao.getDataOidById(savedFirmware.getId().getId());
Assert.assertNotNull(oid);
otaPackageService.deleteOtaPackage(tenantId, savedFirmware.getId());
// Verify the large object was unlinked - PostgreSQL throws an exception when the object doesn't exist
assertThatThrownBy(() -> otaPackageDao.unlinkLargeObject(oid)).hasMessageContaining("large object " + oid + " does not exist");
}
@Test @Test
public void testFindTenantFirmwaresByTenantId() { public void testFindTenantFirmwaresByTenantId() {
List<OtaPackageInfo> firmwares = new ArrayList<>(); List<OtaPackageInfo> firmwares = new ArrayList<>();
@ -567,8 +600,8 @@ public class OtaPackageServiceTest extends AbstractServiceTest {
} }
} while (pageData.hasNext()); } while (pageData.hasNext());
Collections.sort(firmwares, idComparator); firmwares.sort(idComparator);
Collections.sort(loadedFirmwares, idComparator); loadedFirmwares.sort(idComparator);
assertThat(firmwares).isEqualTo(loadedFirmwares); assertThat(firmwares).isEqualTo(loadedFirmwares);
@ -622,8 +655,8 @@ public class OtaPackageServiceTest extends AbstractServiceTest {
} }
} while (pageData.hasNext()); } while (pageData.hasNext());
Collections.sort(firmwares, idComparator); firmwares.sort(idComparator);
Collections.sort(loadedFirmwares, idComparator); loadedFirmwares.sort(idComparator);
assertThat(firmwares).isEqualTo(loadedFirmwares); assertThat(firmwares).isEqualTo(loadedFirmwares);
@ -726,4 +759,5 @@ public class OtaPackageServiceTest extends AbstractServiceTest {
firmware.setDataSize(DATA_SIZE); firmware.setDataSize(DATA_SIZE);
return firmware; return firmware;
} }
} }

2
dao/src/test/java/org/thingsboard/server/dao/service/timeseries/nosql/TimeseriesServiceNoSqlTest.java

@ -65,7 +65,7 @@ public class TimeseriesServiceNoSqlTest extends BaseTimeseriesServiceTest {
new BasicTsKvEntry(TimeUnit.MINUTES.toMillis(5), new JsonDataEntry("test", "{\"test\":\"testValue\"}"))); new BasicTsKvEntry(TimeUnit.MINUTES.toMillis(5), new JsonDataEntry("test", "{\"test\":\"testValue\"}")));
DeviceId deviceId = new DeviceId(Uuids.timeBased()); DeviceId deviceId = new DeviceId(Uuids.timeBased());
tsService.save(tenantId, deviceId, timeseries, ttlInSec); tsService.save(tenantId, deviceId, timeseries, ttlInSec).get(MAX_TIMEOUT, TimeUnit.SECONDS);
List<TsKvEntry> fullList = tsService.findAll(tenantId, deviceId, Collections.singletonList(new BaseReadTsKvQuery("test", 0L, List<TsKvEntry> fullList = tsService.findAll(tenantId, deviceId, Collections.singletonList(new BaseReadTsKvQuery("test", 0L,
TimeUnit.MINUTES.toMillis(6), 1000, 10, Aggregation.NONE))).get(MAX_TIMEOUT, TimeUnit.SECONDS); TimeUnit.MINUTES.toMillis(6), 1000, 10, Aggregation.NONE))).get(MAX_TIMEOUT, TimeUnit.SECONDS);

133
dao/src/test/java/org/thingsboard/server/dao/sql/notification/JpaNotificationRequestDaoTest.java

@ -0,0 +1,133 @@
/**
* 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.dao.sql.notification;
import org.junit.After;
import org.junit.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.thingsboard.server.common.data.id.NotificationRequestId;
import org.thingsboard.server.common.data.id.TenantId;
import org.thingsboard.server.common.data.notification.NotificationRequest;
import org.thingsboard.server.common.data.notification.NotificationRequestStatus;
import org.thingsboard.server.dao.AbstractJpaDaoTest;
import java.util.ArrayList;
import java.util.List;
import java.util.UUID;
import java.util.concurrent.TimeUnit;
import static org.assertj.core.api.Assertions.assertThat;
public class JpaNotificationRequestDaoTest extends AbstractJpaDaoTest {
@Autowired
JpaNotificationRequestDao notificationRequestDao;
private final List<NotificationRequest> createdRequests = new ArrayList<>();
@After
public void tearDown() {
for (NotificationRequest request : createdRequests) {
notificationRequestDao.removeById(request.getTenantId(), request.getId().getId());
}
createdRequests.clear();
}
@Test
public void testBatchDeletion() {
TenantId sysTenantId = TenantId.SYS_TENANT_ID;
long now = System.currentTimeMillis();
long oldTimestamp = now - TimeUnit.DAYS.toMillis(30);
NotificationRequest oldRequest1 = createNotificationRequest(sysTenantId, oldTimestamp);
notificationRequestDao.save(sysTenantId, oldRequest1);
NotificationRequest oldRequest2 = createNotificationRequest(sysTenantId, oldTimestamp);
notificationRequestDao.save(sysTenantId, oldRequest2);
NotificationRequest freshRequest = createNotificationRequest(sysTenantId, now);
notificationRequestDao.save(sysTenantId, freshRequest);
TenantId tenant2Id = TenantId.fromUUID(UUID.fromString("3d193a7a-774b-4c05-84d5-f7fdcf7a37cf"));
NotificationRequest tenant2Request = createNotificationRequest(tenant2Id, oldTimestamp);
notificationRequestDao.save(tenant2Id, tenant2Request);
int batchSize = 10_000;
assertThat(notificationRequestDao.removeByTenantIdAndCreatedTimeBeforeBatch(sysTenantId, oldTimestamp - 1, batchSize)).isEqualTo(0);
long expirationTime = now - TimeUnit.DAYS.toMillis(15);
assertThat(notificationRequestDao.removeByTenantIdAndCreatedTimeBeforeBatch(sysTenantId, expirationTime, batchSize)).isEqualTo(2);
assertThat(notificationRequestDao.findById(sysTenantId, freshRequest.getId().getId())).isNotNull();
assertThat(notificationRequestDao.removeByTenantIdAndCreatedTimeBeforeBatch(tenant2Id, now + 1, batchSize)).isEqualTo(1);
}
@Test
public void testBatchDeletionWithSmallBatchSize() {
TenantId tenantId = TenantId.SYS_TENANT_ID;
long oldTimestamp = System.currentTimeMillis() - TimeUnit.DAYS.toMillis(30);
for (int i = 0; i < 10; i++) {
NotificationRequest request = createNotificationRequest(tenantId, oldTimestamp);
notificationRequestDao.save(tenantId, request);
}
int batchSize = 3;
long expirationTime = System.currentTimeMillis();
assertThat(notificationRequestDao.removeByTenantIdAndCreatedTimeBeforeBatch(tenantId, expirationTime, batchSize)).isEqualTo(3);
assertThat(notificationRequestDao.removeByTenantIdAndCreatedTimeBeforeBatch(tenantId, expirationTime, batchSize)).isEqualTo(3);
assertThat(notificationRequestDao.removeByTenantIdAndCreatedTimeBeforeBatch(tenantId, expirationTime, batchSize)).isEqualTo(3);
assertThat(notificationRequestDao.removeByTenantIdAndCreatedTimeBeforeBatch(tenantId, expirationTime, batchSize)).isEqualTo(1);
assertThat(notificationRequestDao.removeByTenantIdAndCreatedTimeBeforeBatch(tenantId, expirationTime, batchSize)).isEqualTo(0);
}
@Test
public void testBatchDeletionIsolationBetweenTenants() {
TenantId tenant1 = TenantId.SYS_TENANT_ID;
TenantId tenant2 = TenantId.fromUUID(UUID.fromString("3d193a7a-774b-4c05-84d5-f7fdcf7a37cf"));
long oldTimestamp = System.currentTimeMillis() - TimeUnit.DAYS.toMillis(30);
for (int i = 0; i < 5; i++) {
NotificationRequest request = createNotificationRequest(tenant1, oldTimestamp);
notificationRequestDao.save(tenant1, request);
}
for (int i = 0; i < 3; i++) {
NotificationRequest request = createNotificationRequest(tenant2, oldTimestamp);
notificationRequestDao.save(tenant2, request);
}
int batchSize = 10_000;
long expirationTime = System.currentTimeMillis();
assertThat(notificationRequestDao.removeByTenantIdAndCreatedTimeBeforeBatch(tenant1, expirationTime, batchSize)).isEqualTo(5);
assertThat(notificationRequestDao.removeByTenantIdAndCreatedTimeBeforeBatch(tenant2, expirationTime, batchSize)).isEqualTo(3);
}
private NotificationRequest createNotificationRequest(TenantId tenantId, long createdTime) {
NotificationRequest request = new NotificationRequest();
request.setId(new NotificationRequestId(UUID.randomUUID()));
request.setTenantId(tenantId);
request.setCreatedTime(createdTime);
request.setTargets(List.of(UUID.randomUUID()));
request.setStatus(NotificationRequestStatus.SENT);
createdRequests.add(request);
return request;
}
}

7
dao/src/test/java/org/thingsboard/server/dao/sql/rpc/JpaRpcDaoTest.java

@ -51,9 +51,10 @@ public class JpaRpcDaoTest extends AbstractJpaDaoTest {
rpc.setDeviceId(new DeviceId(UUID.randomUUID())); rpc.setDeviceId(new DeviceId(UUID.randomUUID()));
rpcDao.saveAndFlush(rpc.getTenantId(), rpc); rpcDao.saveAndFlush(rpc.getTenantId(), rpc);
assertThat(rpcDao.deleteOutdatedRpcByTenantId(TenantId.SYS_TENANT_ID, 0L)).isEqualTo(0); int batchSize = 10_000;
assertThat(rpcDao.deleteOutdatedRpcByTenantId(TenantId.SYS_TENANT_ID, Long.MAX_VALUE)).isEqualTo(2); assertThat(rpcDao.deleteOutdatedRpcByTenantIdBatch(TenantId.SYS_TENANT_ID, 0L, batchSize)).isEqualTo(0);
assertThat(rpcDao.deleteOutdatedRpcByTenantId(tenantId, System.currentTimeMillis() + 1)).isEqualTo(1); assertThat(rpcDao.deleteOutdatedRpcByTenantIdBatch(TenantId.SYS_TENANT_ID, Long.MAX_VALUE, batchSize)).isEqualTo(2);
assertThat(rpcDao.deleteOutdatedRpcByTenantIdBatch(tenantId, System.currentTimeMillis() + 1, batchSize)).isEqualTo(1);
} }
} }

458
docker/monitoring/grafana/provisioning/dashboards/transport_connections.json

@ -0,0 +1,458 @@
{
"annotations": {
"list": [
{
"builtIn": 1,
"datasource": {
"type": "datasource",
"uid": "grafana"
},
"enable": true,
"hide": true,
"iconColor": "rgba(0, 211, 255, 1)",
"name": "Annotations & Alerts",
"type": "dashboard"
}
]
},
"editable": true,
"fiscalYearStartMonth": 0,
"graphTooltip": 0,
"id": null,
"links": [],
"panels": [
{
"datasource": {
"type": "prometheus",
"uid": "prometheus"
},
"description": "",
"fieldConfig": {
"defaults": {
"color": {
"mode": "palette-classic"
},
"custom": {
"axisBorderShow": false,
"axisCenteredZero": false,
"axisColorMode": "text",
"axisLabel": "",
"axisPlacement": "auto",
"barAlignment": 0,
"drawStyle": "line",
"fillOpacity": 10,
"gradientMode": "none",
"hideFrom": {
"legend": false,
"tooltip": false,
"viz": false
},
"insertNulls": false,
"lineInterpolation": "linear",
"lineWidth": 1,
"pointSize": 5,
"scaleDistribution": {
"type": "linear"
},
"showPoints": "never",
"spanNulls": true,
"stacking": {
"group": "A",
"mode": "none"
},
"thresholdsStyle": {
"mode": "off"
}
},
"mappings": [],
"thresholds": {
"mode": "absolute",
"steps": [
{
"color": "dark-orange",
"value": null
}
]
},
"unit": "short"
},
"overrides": [
{
"matcher": {
"id": "byName",
"options": "Total connections"
},
"properties": [
{
"id": "color",
"value": {
"fixedColor": "light-orange",
"mode": "fixed"
}
}
]
}
]
},
"gridPos": {
"h": 15,
"w": 19,
"x": 0,
"y": 0
},
"id": 19,
"options": {
"legend": {
"calcs": [],
"displayMode": "list",
"placement": "bottom",
"showLegend": true
},
"tooltip": {
"mode": "multi",
"sort": "none"
}
},
"targets": [
{
"datasource": {
"type": "prometheus",
"uid": "prometheus"
},
"editorMode": "code",
"exemplar": false,
"expr": "sum by (protocol) (thingsboard_transport_connections_active{})",
"format": "time_series",
"instant": false,
"interval": "",
"intervalFactor": 1,
"legendFormat": "{{protocol}}",
"refId": "A"
}
],
"title": "Active Connections by Protocol",
"type": "timeseries"
},
{
"datasource": {
"type": "prometheus",
"uid": "prometheus"
},
"fieldConfig": {
"defaults": {
"color": {
"mode": "thresholds"
},
"mappings": [],
"thresholds": {
"mode": "absolute",
"steps": [
{
"color": "rgba(209, 138, 78, 1)",
"value": null
}
]
}
},
"overrides": []
},
"gridPos": {
"h": 15,
"w": 5,
"x": 19,
"y": 0
},
"id": 21,
"options": {
"colorMode": "value",
"graphMode": "none",
"justifyMode": "auto",
"orientation": "auto",
"percentChangeColorMode": "standard",
"reduceOptions": {
"calcs": [
"lastNotNull"
],
"fields": "",
"values": false
},
"showPercentChange": false,
"text": {},
"textMode": "auto",
"wideLayout": true
},
"targets": [
{
"datasource": {
"type": "prometheus",
"uid": "prometheus"
},
"editorMode": "code",
"exemplar": true,
"expr": "sum (thingsboard_transport_connections_active)",
"instant": true,
"interval": "",
"legendFormat": "",
"refId": "A"
}
],
"title": "Total Connections",
"type": "stat"
},
{
"datasource": {
"type": "prometheus",
"uid": "prometheus"
},
"description": "",
"fieldConfig": {
"defaults": {
"color": {
"mode": "palette-classic"
},
"custom": {
"axisBorderShow": false,
"axisCenteredZero": false,
"axisColorMode": "text",
"axisLabel": "",
"axisPlacement": "auto",
"barAlignment": 0,
"drawStyle": "line",
"fillOpacity": 10,
"gradientMode": "none",
"hideFrom": {
"legend": false,
"tooltip": false,
"viz": false
},
"insertNulls": false,
"lineInterpolation": "linear",
"lineWidth": 1,
"pointSize": 5,
"scaleDistribution": {
"type": "linear"
},
"showPoints": "never",
"spanNulls": true,
"stacking": {
"group": "A",
"mode": "none"
},
"thresholdsStyle": {
"mode": "off"
}
},
"mappings": [],
"thresholds": {
"mode": "absolute",
"steps": [
{
"color": "green",
"value": null
},
{
"color": "red",
"value": 80
}
]
},
"unit": "short"
},
"overrides": []
},
"gridPos": {
"h": 15,
"w": 19,
"x": 0,
"y": 15
},
"id": 22,
"options": {
"legend": {
"calcs": [],
"displayMode": "list",
"placement": "bottom",
"showLegend": true
},
"tooltip": {
"mode": "multi",
"sort": "none"
}
},
"targets": [
{
"datasource": {
"type": "prometheus",
"uid": "prometheus"
},
"editorMode": "code",
"exemplar": true,
"expr": "thingsboard_transport_connections_active",
"format": "time_series",
"interval": "",
"intervalFactor": 1,
"legendFormat": "{{protocol}} {{pod_name}}",
"range": true,
"refId": "A"
}
],
"title": "Active Connections by Pod",
"type": "timeseries"
},
{
"datasource": {
"type": "prometheus",
"uid": "prometheus"
},
"fieldConfig": {
"defaults": {
"color": {
"mode": "thresholds"
},
"custom": {
"align": "auto",
"cellOptions": {
"type": "auto"
},
"filterable": false,
"inspect": false
},
"mappings": [],
"min": 0,
"thresholds": {
"mode": "absolute",
"steps": [
{
"color": "rgba(209, 138, 78, 1)",
"value": null
}
]
}
},
"overrides": [
{
"matcher": {
"id": "byName",
"options": "Connections"
},
"properties": [
{
"id": "custom.cellOptions",
"value": {
"mode": "gradient",
"type": "gauge"
}
}
]
}
]
},
"gridPos": {
"h": 15,
"w": 5,
"x": 19,
"y": 15
},
"id": 23,
"options": {
"cellHeight": "sm",
"footer": {
"countRows": false,
"fields": "",
"reducer": [
"sum"
],
"show": false
},
"showHeader": true,
"sortBy": []
},
"targets": [
{
"datasource": {
"type": "prometheus",
"uid": "prometheus"
},
"editorMode": "code",
"exemplar": true,
"expr": "sum by (pod_name) (thingsboard_transport_connections_active)",
"format": "table",
"instant": true,
"interval": "",
"legendFormat": "__auto",
"queryType": "randomWalk",
"refId": "A"
}
],
"title": "Connection Count by Pod",
"transformations": [
{
"id": "organize",
"options": {
"excludeByName": {
"Time": true,
"__name__": true,
"app": true,
"controller_revision_hash": true,
"instance": true,
"job": true,
"kubernetes_name": true,
"kubernetes_namespace": true,
"node_name": true,
"pod_name": false,
"statefulset_kubernetes_io_pod_name": true
},
"indexByName": {},
"renameByName": {
"Time": "",
"Value": "Connections",
"pod_name": "Pod"
}
}
},
{
"id": "sortBy",
"options": {
"fields": {},
"sort": [
{
"field": "Pod"
}
]
}
}
],
"type": "table"
}
],
"refresh": "30s",
"schemaVersion": 39,
"tags": [
"thingsboard"
],
"templating": {
"list": []
},
"time": {
"from": "now-1h",
"to": "now"
},
"timepicker": {
"refresh_intervals": [
"10s",
"30s",
"1m",
"5m",
"15m",
"30m",
"1h",
"2h",
"1d"
]
},
"timezone": "",
"title": "Transport Connections",
"uid": "lewbrlwjerwkj3",
"version": 1,
"weekStart": ""
}

14
edqs/src/main/resources/edqs.yml

@ -25,19 +25,19 @@ app:
# Application version # Application version
version: "@project.version@" version: "@project.version@"
# Zookeeper connection parameters # ZooKeeper connection parameters
zk: zk:
# Enable/disable zookeeper discovery service. # Enable/disable ZooKeeper discovery service.
enabled: "${ZOOKEEPER_ENABLED:true}" enabled: "${ZOOKEEPER_ENABLED:true}"
# Zookeeper connect string # ZooKeeper connect string
url: "${ZOOKEEPER_URL:localhost:2181}" url: "${ZOOKEEPER_URL:localhost:2181}"
# Zookeeper retry interval in milliseconds # ZooKeeper retry interval in milliseconds
retry_interval_ms: "${ZOOKEEPER_RETRY_INTERVAL_MS:3000}" retry_interval_ms: "${ZOOKEEPER_RETRY_INTERVAL_MS:3000}"
# Zookeeper connection timeout in milliseconds # ZooKeeper connection timeout in milliseconds
connection_timeout_ms: "${ZOOKEEPER_CONNECTION_TIMEOUT_MS:3000}" connection_timeout_ms: "${ZOOKEEPER_CONNECTION_TIMEOUT_MS:3000}"
# Zookeeper session timeout in milliseconds # ZooKeeper session timeout in milliseconds
session_timeout_ms: "${ZOOKEEPER_SESSION_TIMEOUT_MS:3000}" session_timeout_ms: "${ZOOKEEPER_SESSION_TIMEOUT_MS:3000}"
# Name of the directory in zookeeper 'filesystem' # Name of the directory in ZooKeeper 'filesystem'
zk_dir: "${ZOOKEEPER_NODES_DIR:/thingsboard}" zk_dir: "${ZOOKEEPER_NODES_DIR:/thingsboard}"
# The recalculate_delay property is recommended in a microservices architecture setup for rule-engine services. # The recalculate_delay property is recommended in a microservices architecture setup for rule-engine services.
# This property provides a pause to ensure that when a rule-engine service is restarted, other nodes don't immediately attempt to recalculate their partitions. # This property provides a pause to ensure that when a rule-engine service is restarted, other nodes don't immediately attempt to recalculate their partitions.

64
msa/js-executor/config/default.yml

@ -14,46 +14,56 @@
# limitations under the License. # limitations under the License.
# #
queue_type: "kafka" # Queue configuration
request_topic: "js_eval.requests" # Defines the messaging queue backend used to dispatch JS evaluation requests to the executor and receive results.
queue_prefix: "" queue_type: "kafka" # Queue type used for JS evaluation requests: kafka or in-memory
http_port: "8888" # /livenessProbe request_topic: "js_eval.requests" # Kafka topic name for incoming JS evaluation requests
queue_prefix: "" # Optional prefix applied to all queue/topic names; useful for environment isolation
http_port: "8888" # HTTP port exposing the /livenessProbe health check endpoint
# JavaScript execution configuration
# Controls response polling behavior and output size limits for individual JS script executions.
js: js:
response_poll_interval: "25" response_poll_interval: "25" # Interval in milliseconds to poll for JS execution responses
max_result_size: "300000" max_result_size: "300000" # Maximum allowed size in bytes of a JS execution result
# Kafka configuration
# Connection, producer, and topic settings for the Kafka message broker used as the JS evaluation queue.
kafka: kafka:
bootstrap: bootstrap:
# Kafka Bootstrap Servers # Kafka Bootstrap Servers
servers: "localhost:9092" servers: "localhost:9092"
replication_factor: "1" replication_factor: "1" # Number of replicas for each Kafka topic partition
acks: "1" # -1 = all; 0 = no acknowledgments; 1 = only waits for the leader to acknowledge acks: "1" # -1 = all; 0 = no acknowledgments; 1 = only waits for the leader to acknowledge
batch_size: "128" # for producer batch_size: "128" # Maximum number of messages to accumulate before triggering a batch send
linger_ms: "5" # for producer linger_ms: "5" # Time in milliseconds the producer waits before sending a batch, allowing more messages to accumulate
partitions_consumed_concurrently: "1" # (EXPERIMENTAL) increase this value if you are planning to handle more than one partition (scale up, scale down) - this will decrease the latency partitions_consumed_concurrently: "1" # (EXPERIMENTAL) increase this value if you are planning to handle more than one partition (scale up, scale down) - this will decrease the latency
requestTimeout: "30000" # The default value in kafkajs is: 30000 requestTimeout: "30000" # Time in milliseconds to wait for a response to a Kafka request before timing out
connectionTimeout: "1000" # The default value in kafkajs is: 1000 connectionTimeout: "1000" # Time in milliseconds to wait when establishing a connection to a Kafka broker
compression: "none" # gzip or uncompressed compression: "none" # Message compression codec for the producer: none, gzip
topic_properties: "retention.ms:604800000;segment.bytes:52428800;retention.bytes:104857600;partitions:100;min.insync.replicas:1" topic_properties: "retention.ms:604800000;segment.bytes:52428800;retention.bytes:104857600;partitions:100;min.insync.replicas:1" # Semicolon-separated Kafka topic configuration properties applied on topic creation
use_confluent_cloud: false use_confluent_cloud: false # Set to true to enable Confluent Cloud-specific configuration (SSL + SASL)
client_id: "kafkajs" #inject pod name to easy identify the client using /opt/kafka/bin/kafka-consumer-groups.sh client_id: "kafkajs" # Kafka client identifier; inject the pod name to easily identify the client via kafka-consumer-groups.sh
ssl: ssl:
enabled: false enabled: false # Enable SSL/TLS for Kafka broker connections
confluent: confluent:
sasl: sasl:
mechanism: "PLAIN" mechanism: "PLAIN" # SASL mechanism for Confluent Cloud authentication: PLAIN, SCRAM-SHA-256, or SCRAM-SHA-512
# Logging configuration
# Controls log verbosity, output directory, and log file naming pattern for the JS executor service.
logger: logger:
level: "info" level: "info" # Log level: trace, debug, info, warn, or error
path: "logs" path: "logs" # Directory path where log files are written
filename: "tb-js-executor-%DATE%.log" filename: "tb-js-executor-%DATE%.log" # Log file name pattern; %DATE% is replaced with the current date
# JavaScript execution and monitoring configuration
# Sandboxing, script caching, and observability settings for the JS script runtime.
script: script:
use_sandbox: "true" use_sandbox: "true" # Run scripts inside an isolated sandbox to prevent access to Node.js internals
memory_usage_trace_frequency: "1000" memory_usage_trace_frequency: "1000" # Log memory usage every N script executions
script_body_trace_frequency: "10000" script_body_trace_frequency: "10000" # Log the script body every N script executions
stat_print_frequency: "10000" stat_print_frequency: "10000" # Print execution statistics every N script executions
max_active_scripts: "1000" max_active_scripts: "1000" # Maximum number of compiled scripts kept in the cache
slow_query_log_ms: "5.000000" #millis slow_query_log_ms: "5.000000" # Execution time threshold in milliseconds above which a script is considered slow and logged
slow_query_log_body: "false" slow_query_log_body: "false" # Log the script body when execution time exceeds slow_query_log_ms threshold

24
msa/vc-executor/src/main/resources/tb-vc-executor.yml

@ -15,30 +15,33 @@
# #
# Spring common parameters # Spring common parameters
# Controls Spring Boot startup mode, web environment type, and bean wiring behaviour.
spring.main.web-environment: "${WEB_APPLICATION_ENABLE:false}" # If you enabled process metrics you should also enable 'web-environment'. spring.main.web-environment: "${WEB_APPLICATION_ENABLE:false}" # If you enabled process metrics you should also enable 'web-environment'.
spring.main.web-application-type: "${WEB_APPLICATION_TYPE:none}" # If you enabled process metrics you should set 'web-application-type' to 'servlet' value. spring.main.web-application-type: "${WEB_APPLICATION_TYPE:none}" # If you enabled process metrics you should set 'web-application-type' to 'servlet' value.
spring.main.allow-circular-references: "true" # Spring Boot configuration property that controls whether circular dependencies between beans are allowed. spring.main.allow-circular-references: "true" # Spring Boot configuration property that controls whether circular dependencies between beans are allowed.
# Server common parameters # Server common parameters
# Configures the embedded HTTP server bind address and port used for actuator and metrics endpoints.
server: server:
# Server bind address (has no effect if web-environment is disabled). # Server bind address (has no effect if web-environment is disabled).
address: "${HTTP_BIND_ADDRESS:0.0.0.0}" address: "${HTTP_BIND_ADDRESS:0.0.0.0}"
# Server bind port (has no effect if web-environment is disabled). # Server bind port (has no effect if web-environment is disabled).
port: "${HTTP_BIND_PORT:8086}" port: "${HTTP_BIND_PORT:8086}"
# Zookeeper connection parameters. Used for service discovery. # ZooKeeper connection parameters
# Controls ZooKeeper-based service discovery and cluster coordination for microservice deployments.
zk: zk:
# Enable/disable zookeeper discovery service. # Enable/disable ZooKeeper discovery service.
enabled: "${ZOOKEEPER_ENABLED:true}" enabled: "${ZOOKEEPER_ENABLED:true}"
# Zookeeper connect string # ZooKeeper connect string
url: "${ZOOKEEPER_URL:localhost:2181}" url: "${ZOOKEEPER_URL:localhost:2181}"
# Zookeeper retry interval in milliseconds # ZooKeeper retry interval in milliseconds
retry_interval_ms: "${ZOOKEEPER_RETRY_INTERVAL_MS:3000}" retry_interval_ms: "${ZOOKEEPER_RETRY_INTERVAL_MS:3000}"
# Zookeeper connection timeout in milliseconds # ZooKeeper connection timeout in milliseconds
connection_timeout_ms: "${ZOOKEEPER_CONNECTION_TIMEOUT_MS:3000}" connection_timeout_ms: "${ZOOKEEPER_CONNECTION_TIMEOUT_MS:3000}"
# Zookeeper session timeout in milliseconds # ZooKeeper session timeout in milliseconds
session_timeout_ms: "${ZOOKEEPER_SESSION_TIMEOUT_MS:3000}" session_timeout_ms: "${ZOOKEEPER_SESSION_TIMEOUT_MS:3000}"
# Name of the directory in zookeeper 'filesystem' # Name of the directory in ZooKeeper 'filesystem'
zk_dir: "${ZOOKEEPER_NODES_DIR:/thingsboard}" zk_dir: "${ZOOKEEPER_NODES_DIR:/thingsboard}"
# The recalculate_delay property is recommended in a microservices architecture setup for rule-engine services. # The recalculate_delay property is recommended in a microservices architecture setup for rule-engine services.
# This property provides a pause to ensure that when a rule-engine service is restarted, other nodes don't immediately attempt to recalculate their partitions. # This property provides a pause to ensure that when a rule-engine service is restarted, other nodes don't immediately attempt to recalculate their partitions.
@ -46,6 +49,7 @@ zk:
recalculate_delay: "${ZOOKEEPER_RECALCULATE_DELAY_MS:0}" recalculate_delay: "${ZOOKEEPER_RECALCULATE_DELAY_MS:0}"
# Queue configuration parameters # Queue configuration parameters
# Defines the message queue type (Kafka or in-memory) and all related producer, consumer, and topic settings.
queue: queue:
type: "${TB_QUEUE_TYPE:kafka}" # kafka (Apache Kafka) type: "${TB_QUEUE_TYPE:kafka}" # kafka (Apache Kafka)
prefix: "${TB_QUEUE_PREFIX:}" # Global queue prefix. If specified, prefix is added before default topic name: 'prefix.default_topic_name'. Prefix is applied to all topics (and consumer groups for kafka). prefix: "${TB_QUEUE_PREFIX:}" # Global queue prefix. If specified, prefix is added before default topic name: 'prefix.default_topic_name'. Prefix is applied to all topics (and consumer groups for kafka).
@ -196,6 +200,7 @@ queue:
msg-chunk-size: "${TB_QUEUE_VC_MSG_CHUNK_SIZE:250000}" msg-chunk-size: "${TB_QUEUE_VC_MSG_CHUNK_SIZE:250000}"
# Version control parameters # Version control parameters
# Configures the Git-based version control executor, including thread pools and local repository storage.
vc: vc:
# Pool size for handling export tasks # Pool size for handling export tasks
thread_pool_size: "${TB_VC_POOL_SIZE:6}" thread_pool_size: "${TB_VC_POOL_SIZE:6}"
@ -206,6 +211,7 @@ vc:
repositories-folder: "${TB_VC_GIT_REPOSITORIES_FOLDER:${java.io.tmpdir}/repositories}" repositories-folder: "${TB_VC_GIT_REPOSITORIES_FOLDER:${java.io.tmpdir}/repositories}"
# Usage statistics parameters # Usage statistics parameters
# Controls collection and reporting of API usage statistics at system, tenant, and customer levels.
usage: usage:
stats: stats:
report: report:
@ -221,6 +227,7 @@ usage:
pack_size: "${USAGE_STATS_REPORT_PACK_SIZE:1024}" pack_size: "${USAGE_STATS_REPORT_PACK_SIZE:1024}"
# Metrics parameters # Metrics parameters
# Enables actuator-based metrics collection and configures histogram/timer percentile exports.
metrics: metrics:
# Enable/disable actuator metrics. # Enable/disable actuator metrics.
enabled: "${METRICS_ENABLED:false}" enabled: "${METRICS_ENABLED:false}"
@ -229,6 +236,7 @@ metrics:
percentiles: "${METRICS_TIMER_PERCENTILES:0.5}" percentiles: "${METRICS_TIMER_PERCENTILES:0.5}"
# General management parameters # General management parameters
# Configures Spring Boot Actuator endpoint exposure, including the metrics and info endpoints.
management: management:
endpoints: endpoints:
web: web:
@ -237,12 +245,14 @@ management:
include: "${METRICS_ENDPOINTS_EXPOSE:info}" include: "${METRICS_ENDPOINTS_EXPOSE:info}"
# Service common properties # Service common properties
# Identifies this microservice instance by type and optional unique ID for cluster coordination.
service: service:
type: "${TB_SERVICE_TYPE:tb-vc-executor}" # service type type: "${TB_SERVICE_TYPE:tb-vc-executor}" # service type
# Unique id for this service (autogenerated if empty) # Unique id for this service (autogenerated if empty)
id: "${TB_SERVICE_ID:}" id: "${TB_SERVICE_ID:}"
# Notification system parameters # Notification system parameters
# Defines deduplication rules for notification triggers to prevent repeated alerts within a time window.
notification_system: notification_system:
rules: rules:
# Semicolon-separated deduplication durations (in millis) for trigger types. Format: 'NotificationRuleTriggerType1:123;NotificationRuleTriggerType2:456' # Semicolon-separated deduplication durations (in millis) for trigger types. Format: 'NotificationRuleTriggerType1:123;NotificationRuleTriggerType2:456'

1
netty-mqtt/pom.xml

@ -125,7 +125,6 @@
<plugin> <plugin>
<groupId>org.apache.maven.plugins</groupId> <groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-jar-plugin</artifactId> <artifactId>maven-jar-plugin</artifactId>
<version>3.1.1</version>
<configuration> <configuration>
<archive> <archive>
<manifest> <manifest>

6
pom.xml

@ -72,7 +72,7 @@
<paho.mqttv5.client.version>1.2.5</paho.mqttv5.client.version> <paho.mqttv5.client.version>1.2.5</paho.mqttv5.client.version>
<os-maven-plugin.version>1.7.1</os-maven-plugin.version> <os-maven-plugin.version>1.7.1</os-maven-plugin.version>
<surefire.version>3.5.4</surefire.version> <surefire.version>3.5.4</surefire.version>
<jar-plugin.version>3.4.0</jar-plugin.version> <jar-plugin.version>3.5.0</jar-plugin.version>
<springdoc-swagger.version>2.8.8TB</springdoc-swagger.version> <springdoc-swagger.version>2.8.8TB</springdoc-swagger.version>
<swagger-annotations.version>2.2.30</swagger-annotations.version> <swagger-annotations.version>2.2.30</swagger-annotations.version>
<spatial4j.version>0.8</spatial4j.version> <spatial4j.version>0.8</spatial4j.version>
@ -636,7 +636,7 @@
<plugin> <plugin>
<groupId>org.apache.maven.plugins</groupId> <groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-jar-plugin</artifactId> <artifactId>maven-jar-plugin</artifactId>
<version>3.1.1</version> <version>${jar-plugin.version}</version>
</plugin> </plugin>
<plugin> <plugin>
<groupId>org.apache.maven.plugins</groupId> <groupId>org.apache.maven.plugins</groupId>
@ -656,7 +656,7 @@
<plugin> <plugin>
<groupId>com.github.eirslett</groupId> <groupId>com.github.eirslett</groupId>
<artifactId>frontend-maven-plugin</artifactId> <artifactId>frontend-maven-plugin</artifactId>
<version>1.12.0</version> <version>2.0.0</version>
</plugin> </plugin>
<plugin> <plugin>
<groupId>org.apache.maven.plugins</groupId> <groupId>org.apache.maven.plugins</groupId>

30
transport/coap/src/main/resources/tb-coap-transport.yml

@ -15,30 +15,33 @@
# #
# Spring common parameters # Spring common parameters
# Controls core Spring Boot application settings such as web environment and application type.
spring.main.web-environment: "${WEB_APPLICATION_ENABLE:false}" # If you enabled process metrics you should also enable 'web-environment'. spring.main.web-environment: "${WEB_APPLICATION_ENABLE:false}" # If you enabled process metrics you should also enable 'web-environment'.
spring.main.web-application-type: "${WEB_APPLICATION_TYPE:none}" # If you enabled process metrics you should set 'web-application-type' to 'servlet' value. spring.main.web-application-type: "${WEB_APPLICATION_TYPE:none}" # If you enabled process metrics you should set 'web-application-type' to 'servlet' value.
spring.main.allow-circular-references: "true" # Spring Boot configuration property that controls whether circular dependencies between beans are allowed. spring.main.allow-circular-references: "true" # Spring Boot configuration property that controls whether circular dependencies between beans are allowed.
# Server common parameters # Server common parameters
# Configures the HTTP server bind address and port for the actuator/metrics web endpoint.
server: server:
# Server bind address (has no effect if web-environment is disabled). # Server bind address (has no effect if web-environment is disabled).
address: "${HTTP_BIND_ADDRESS:0.0.0.0}" address: "${HTTP_BIND_ADDRESS:0.0.0.0}"
# Server bind port (has no effect if web-environment is disabled). # Server bind port (has no effect if web-environment is disabled).
port: "${HTTP_BIND_PORT:8083}" port: "${HTTP_BIND_PORT:8083}"
# Zookeeper connection parameters. Used for service discovery. # ZooKeeper connection parameters. Used for service discovery.
# Defines how this service registers itself and discovers other nodes via Apache ZooKeeper.
zk: zk:
# Enable/disable zookeeper discovery service. # Enable/disable ZooKeeper discovery service.
enabled: "${ZOOKEEPER_ENABLED:false}" enabled: "${ZOOKEEPER_ENABLED:false}"
# Zookeeper connect string # ZooKeeper connect string
url: "${ZOOKEEPER_URL:localhost:2181}" url: "${ZOOKEEPER_URL:localhost:2181}"
# Zookeeper retry interval in milliseconds # ZooKeeper retry interval in milliseconds
retry_interval_ms: "${ZOOKEEPER_RETRY_INTERVAL_MS:3000}" retry_interval_ms: "${ZOOKEEPER_RETRY_INTERVAL_MS:3000}"
# Zookeeper connection timeout in milliseconds # ZooKeeper connection timeout in milliseconds
connection_timeout_ms: "${ZOOKEEPER_CONNECTION_TIMEOUT_MS:3000}" connection_timeout_ms: "${ZOOKEEPER_CONNECTION_TIMEOUT_MS:3000}"
# Zookeeper session timeout in milliseconds # ZooKeeper session timeout in milliseconds
session_timeout_ms: "${ZOOKEEPER_SESSION_TIMEOUT_MS:3000}" session_timeout_ms: "${ZOOKEEPER_SESSION_TIMEOUT_MS:3000}"
# Name of the directory in zookeeper 'filesystem' # Name of the directory in ZooKeeper 'filesystem'
zk_dir: "${ZOOKEEPER_NODES_DIR:/thingsboard}" zk_dir: "${ZOOKEEPER_NODES_DIR:/thingsboard}"
# The recalculate_delay property is recommended in a microservices architecture setup for rule-engine services. # The recalculate_delay property is recommended in a microservices architecture setup for rule-engine services.
# This property provides a pause to ensure that when a rule-engine service is restarted, other nodes don't immediately attempt to recalculate their partitions. # This property provides a pause to ensure that when a rule-engine service is restarted, other nodes don't immediately attempt to recalculate their partitions.
@ -46,6 +49,7 @@ zk:
recalculate_delay: "${ZOOKEEPER_RECALCULATE_DELAY_MS:0}" recalculate_delay: "${ZOOKEEPER_RECALCULATE_DELAY_MS:0}"
# Cache parameters # Cache parameters
# Configures the caching backend and per-entity cache settings used by the transport service.
cache: cache:
# caffeine or redis # caffeine or redis
type: "${CACHE_TYPE:redis}" type: "${CACHE_TYPE:redis}"
@ -55,6 +59,7 @@ cache:
maxSize: "${CACHE_SPECS_ENTITY_LIMITS_MAX_SIZE:100000}" # 0 means the cache is disabled maxSize: "${CACHE_SPECS_ENTITY_LIMITS_MAX_SIZE:100000}" # 0 means the cache is disabled
# Redis/Valkey configuration parameters # Redis/Valkey configuration parameters
# Defines connection mode, authentication, SSL, and connection pool settings for Redis or Valkey.
redis: redis:
connection: connection:
# standalone or cluster or sentinel # standalone or cluster or sentinel
@ -90,9 +95,9 @@ redis:
password: "${REDIS_SENTINEL_PASSWORD:}" password: "${REDIS_SENTINEL_PASSWORD:}"
# if set false will be used pool config build from values of the pool config section # if set false will be used pool config build from values of the pool config section
useDefaultPoolConfig: "${REDIS_USE_DEFAULT_POOL_CONFIG:true}" useDefaultPoolConfig: "${REDIS_USE_DEFAULT_POOL_CONFIG:true}"
# db index # Redis logical database index to select after connecting.
db: "${REDIS_DB:0}" db: "${REDIS_DB:0}"
# db password # Password for Redis authentication (leave empty if not required).
password: "${REDIS_PASSWORD:}" password: "${REDIS_PASSWORD:}"
# Redis username for ACL authentication (Redis 6.0+). Leave empty for legacy password-only auth # Redis username for ACL authentication (Redis 6.0+). Leave empty for legacy password-only auth
username: "${REDIS_USERNAME:}" username: "${REDIS_USERNAME:}"
@ -172,6 +177,7 @@ transport:
print-interval-ms: "${TB_TRANSPORT_STATS_PRINT_INTERVAL_MS:60000}" print-interval-ms: "${TB_TRANSPORT_STATS_PRINT_INTERVAL_MS:60000}"
# CoAP server parameters # CoAP server parameters
# Configures the CoAP and CoAP/DTLS server endpoints, including bind addresses, timeouts, and TLS credentials.
coap: coap:
# CoAP bind-address # CoAP bind-address
bind_address: "${COAP_BIND_ADDRESS:0.0.0.0}" bind_address: "${COAP_BIND_ADDRESS:0.0.0.0}"
@ -253,6 +259,7 @@ coap:
dtls_session_report_timeout: "${TB_COAP_X509_DTLS_SESSION_REPORT_TIMEOUT:1800000}" dtls_session_report_timeout: "${TB_COAP_X509_DTLS_SESSION_REPORT_TIMEOUT:1800000}"
# Queue configuration parameters # Queue configuration parameters
# Defines the message queue backend (Kafka) and all topic, partition, and consumer settings for inter-service communication.
queue: queue:
type: "${TB_QUEUE_TYPE:kafka}" # kafka (Apache Kafka) type: "${TB_QUEUE_TYPE:kafka}" # kafka (Apache Kafka)
prefix: "${TB_QUEUE_PREFIX:}" # Global queue prefix. If specified, prefix is added before default topic name: 'prefix.default_topic_name'. Prefix is applied to all topics (and consumer groups for kafka). prefix: "${TB_QUEUE_PREFIX:}" # Global queue prefix. If specified, prefix is added before default topic name: 'prefix.default_topic_name'. Prefix is applied to all topics (and consumer groups for kafka).
@ -406,12 +413,14 @@ queue:
poll_interval: "${TB_QUEUE_TRANSPORT_NOTIFICATIONS_POLL_INTERVAL_MS:25}" poll_interval: "${TB_QUEUE_TRANSPORT_NOTIFICATIONS_POLL_INTERVAL_MS:25}"
# Service common properties # Service common properties
# Identifies this service instance within the ThingsBoard cluster.
service: service:
type: "${TB_SERVICE_TYPE:tb-transport}" # service type type: "${TB_SERVICE_TYPE:tb-transport}" # service type
# Unique id for this service (autogenerated if empty) # Unique id for this service (autogenerated if empty)
id: "${TB_SERVICE_ID:}" id: "${TB_SERVICE_ID:}"
# Usage statistics parameters # Usage statistics parameters
# Controls reporting of API usage metrics at system, tenant, and customer levels.
usage: usage:
stats: stats:
report: report:
@ -427,11 +436,13 @@ usage:
pack_size: "${USAGE_STATS_REPORT_PACK_SIZE:1024}" pack_size: "${USAGE_STATS_REPORT_PACK_SIZE:1024}"
# Metrics parameters # Metrics parameters
# Enables Micrometer/Actuator metrics collection and configures the exposed management endpoints.
metrics: metrics:
# Enable/disable actuator metrics. # Enable/disable actuator metrics.
enabled: "${METRICS_ENABLED:false}" enabled: "${METRICS_ENABLED:false}"
# General management parameters # General management parameters
# Configures Spring Boot Actuator endpoint exposure for health checks and metrics scraping.
management: management:
endpoints: endpoints:
web: web:
@ -440,6 +451,7 @@ management:
include: "${METRICS_ENDPOINTS_EXPOSE:info}" include: "${METRICS_ENDPOINTS_EXPOSE:info}"
# Notification system parameters # Notification system parameters
# Configures deduplication rules for platform notifications to prevent repeated alerts within a time window.
notification_system: notification_system:
rules: rules:
# Semicolon-separated deduplication durations (in millis) for trigger types. Format: 'NotificationRuleTriggerType1:123;NotificationRuleTriggerType2:456' # Semicolon-separated deduplication durations (in millis) for trigger types. Format: 'NotificationRuleTriggerType1:123;NotificationRuleTriggerType2:456'

30
transport/http/src/main/resources/tb-http-transport.yml

@ -15,6 +15,7 @@
# #
# Server common properties # Server common properties
# Configures the HTTP server bind address, port, SSL/TLS settings, and HTTP/2 support.
server: server:
# Server bind address # Server bind address
address: "${HTTP_BIND_ADDRESS:0.0.0.0}" address: "${HTTP_BIND_ADDRESS:0.0.0.0}"
@ -54,23 +55,25 @@ server:
enabled: "${HTTP2_ENABLED:true}" enabled: "${HTTP2_ENABLED:true}"
# Spring common parameters # Spring common parameters
# Controls Spring Boot framework-level settings such as circular references and multipart upload limits.
spring.main.allow-circular-references: "true" # Spring Boot configuration property that controls whether circular dependencies between beans are allowed. spring.main.allow-circular-references: "true" # Spring Boot configuration property that controls whether circular dependencies between beans are allowed.
spring.servlet.multipart.max-file-size: "${SPRING_SERVLET_MULTIPART_MAX_FILE_SIZE:50MB}" # Total file size cannot exceed 50MB when configuring file uploads spring.servlet.multipart.max-file-size: "${SPRING_SERVLET_MULTIPART_MAX_FILE_SIZE:50MB}" # Total file size cannot exceed 50MB when configuring file uploads
spring.servlet.multipart.max-request-size: "${SPRING_SERVLET_MULTIPART_MAX_REQUEST_SIZE:50MB}" # Total request size for a multipart/form-data cannot exceed 50MB spring.servlet.multipart.max-request-size: "${SPRING_SERVLET_MULTIPART_MAX_REQUEST_SIZE:50MB}" # Total request size for a multipart/form-data cannot exceed 50MB
# Zookeeper connection parameters. Used for service discovery. # ZooKeeper connection parameters
# Controls ZooKeeper-based service discovery and cluster coordination for microservice deployments.
zk: zk:
# Enable/disable zookeeper discovery service. # Enable/disable ZooKeeper discovery service.
enabled: "${ZOOKEEPER_ENABLED:false}" enabled: "${ZOOKEEPER_ENABLED:false}"
# Zookeeper connect string # ZooKeeper connect string
url: "${ZOOKEEPER_URL:localhost:2181}" url: "${ZOOKEEPER_URL:localhost:2181}"
# Zookeeper retry interval in milliseconds # ZooKeeper retry interval in milliseconds
retry_interval_ms: "${ZOOKEEPER_RETRY_INTERVAL_MS:3000}" retry_interval_ms: "${ZOOKEEPER_RETRY_INTERVAL_MS:3000}"
# Zookeeper connection timeout in milliseconds # ZooKeeper connection timeout in milliseconds
connection_timeout_ms: "${ZOOKEEPER_CONNECTION_TIMEOUT_MS:3000}" connection_timeout_ms: "${ZOOKEEPER_CONNECTION_TIMEOUT_MS:3000}"
# Zookeeper session timeout in milliseconds # ZooKeeper session timeout in milliseconds
session_timeout_ms: "${ZOOKEEPER_SESSION_TIMEOUT_MS:3000}" session_timeout_ms: "${ZOOKEEPER_SESSION_TIMEOUT_MS:3000}"
# Name of the directory in zookeeper 'filesystem' # Name of the directory in ZooKeeper 'filesystem'
zk_dir: "${ZOOKEEPER_NODES_DIR:/thingsboard}" zk_dir: "${ZOOKEEPER_NODES_DIR:/thingsboard}"
# The recalculate_delay property is recommended in a microservices architecture setup for rule-engine services. # The recalculate_delay property is recommended in a microservices architecture setup for rule-engine services.
# This property provides a pause to ensure that when a rule-engine service is restarted, other nodes don't immediately attempt to recalculate their partitions. # This property provides a pause to ensure that when a rule-engine service is restarted, other nodes don't immediately attempt to recalculate their partitions.
@ -78,6 +81,7 @@ zk:
recalculate_delay: "${ZOOKEEPER_RECALCULATE_DELAY_MS:0}" recalculate_delay: "${ZOOKEEPER_RECALCULATE_DELAY_MS:0}"
# Cache parameters # Cache parameters
# Defines the cache backend type and per-entity cache configuration such as TTL and maximum size.
cache: cache:
# caffeine or redis # caffeine or redis
type: "${CACHE_TYPE:redis}" type: "${CACHE_TYPE:redis}"
@ -87,6 +91,7 @@ cache:
maxSize: "${CACHE_SPECS_ENTITY_LIMITS_MAX_SIZE:100000}" # 0 means the cache is disabled maxSize: "${CACHE_SPECS_ENTITY_LIMITS_MAX_SIZE:100000}" # 0 means the cache is disabled
# Redis/Valkey configuration parameters # Redis/Valkey configuration parameters
# Configures connection mode (standalone, cluster, sentinel), credentials, SSL, and connection pool settings.
redis: redis:
# standalone or cluster or sentinel # standalone or cluster or sentinel
connection: connection:
@ -123,9 +128,9 @@ redis:
password: "${REDIS_SENTINEL_PASSWORD:}" password: "${REDIS_SENTINEL_PASSWORD:}"
# if set false will be used pool config build from values of the pool config section # if set false will be used pool config build from values of the pool config section
useDefaultPoolConfig: "${REDIS_USE_DEFAULT_POOL_CONFIG:true}" useDefaultPoolConfig: "${REDIS_USE_DEFAULT_POOL_CONFIG:true}"
# db index # Redis logical database index to select after connecting.
db: "${REDIS_DB:0}" db: "${REDIS_DB:0}"
# db password # Password for Redis authentication (leave empty if not required).
password: "${REDIS_PASSWORD:}" password: "${REDIS_PASSWORD:}"
# Redis username for ACL authentication (Redis 6.0+). Leave empty for legacy password-only auth # Redis username for ACL authentication (Redis 6.0+). Leave empty for legacy password-only auth
username: "${REDIS_USERNAME:}" username: "${REDIS_USERNAME:}"
@ -166,6 +171,7 @@ redis:
blockWhenExhausted: "${REDIS_POOL_CONFIG_BLOCK_WHEN_EXHAUSTED:true}" blockWhenExhausted: "${REDIS_POOL_CONFIG_BLOCK_WHEN_EXHAUSTED:true}"
# HTTP server parameters # HTTP server parameters
# Configures HTTP transport request timeouts, payload limits, session management, JSON processing, logging, and statistics.
transport: transport:
http: http:
# HTTP request processing timeout in milliseconds # HTTP request processing timeout in milliseconds
@ -203,6 +209,7 @@ transport:
print-interval-ms: "${TB_TRANSPORT_STATS_PRINT_INTERVAL_MS:60000}" print-interval-ms: "${TB_TRANSPORT_STATS_PRINT_INTERVAL_MS:60000}"
# Queue configuration parameters # Queue configuration parameters
# Configures the messaging queue backend (Kafka), topic definitions, partitioning, and per-service consumer settings.
queue: queue:
type: "${TB_QUEUE_TYPE:kafka}" # kafka (Apache Kafka) type: "${TB_QUEUE_TYPE:kafka}" # kafka (Apache Kafka)
prefix: "${TB_QUEUE_PREFIX:}" # Global queue prefix. If specified, prefix is added before default topic name: 'prefix.default_topic_name'. Prefix is applied to all topics (and consumer groups for kafka) . prefix: "${TB_QUEUE_PREFIX:}" # Global queue prefix. If specified, prefix is added before default topic name: 'prefix.default_topic_name'. Prefix is applied to all topics (and consumer groups for kafka) .
@ -355,12 +362,14 @@ queue:
poll_interval: "${TB_QUEUE_TRANSPORT_NOTIFICATIONS_POLL_INTERVAL_MS:25}" poll_interval: "${TB_QUEUE_TRANSPORT_NOTIFICATIONS_POLL_INTERVAL_MS:25}"
# General service parameters # General service parameters
# Defines the service type and unique identifier used for service discovery and cluster coordination.
service: service:
type: "${TB_SERVICE_TYPE:tb-transport}" # type of service type: "${TB_SERVICE_TYPE:tb-transport}" # type of service
# Unique id for this service (autogenerated if empty) # Unique id for this service (autogenerated if empty)
id: "${TB_SERVICE_ID:}" id: "${TB_SERVICE_ID:}"
# Usage statistics parameters # Usage statistics parameters
# Controls reporting of API usage statistics at the system, tenant, and optional customer level.
usage: usage:
stats: stats:
report: report:
@ -376,11 +385,13 @@ usage:
pack_size: "${USAGE_STATS_REPORT_PACK_SIZE:1024}" pack_size: "${USAGE_STATS_REPORT_PACK_SIZE:1024}"
# Metrics parameters # Metrics parameters
# Enables or disables actuator metrics collection and exposure for monitoring integrations such as Prometheus.
metrics: metrics:
# Enable/disable actuator metrics. # Enable/disable actuator metrics.
enabled: "${METRICS_ENABLED:false}" enabled: "${METRICS_ENABLED:false}"
# General management parameters # General management parameters
# Configures Spring Boot Actuator management endpoints, including which endpoints are exposed over HTTP.
management: management:
endpoints: endpoints:
web: web:
@ -389,6 +400,7 @@ management:
include: "${METRICS_ENDPOINTS_EXPOSE:info}" include: "${METRICS_ENDPOINTS_EXPOSE:info}"
# Notification system parameters # Notification system parameters
# Configures notification rules, including deduplication windows to prevent repeated alerts for the same trigger.
notification_system: notification_system:
rules: rules:
# Semicolon-separated deduplication durations (in millis) for trigger types. Format: 'NotificationRuleTriggerType1:123;NotificationRuleTriggerType2:456' # Semicolon-separated deduplication durations (in millis) for trigger types. Format: 'NotificationRuleTriggerType1:123;NotificationRuleTriggerType2:456'

30
transport/lwm2m/src/main/resources/tb-lwm2m-transport.yml

@ -15,30 +15,33 @@
# #
# Spring common parameters # Spring common parameters
# Controls Spring Boot web environment and application type settings used for optional metrics exposure.
spring.main.web-environment: "${WEB_APPLICATION_ENABLE:false}" # If you enabled process metrics you should also enable 'web-environment'. spring.main.web-environment: "${WEB_APPLICATION_ENABLE:false}" # If you enabled process metrics you should also enable 'web-environment'.
spring.main.web-application-type: "${WEB_APPLICATION_TYPE:none}" # If you enabled process metrics you should set 'web-application-type' to 'servlet' value. spring.main.web-application-type: "${WEB_APPLICATION_TYPE:none}" # If you enabled process metrics you should set 'web-application-type' to 'servlet' value.
spring.main.allow-circular-references: "true" # Spring Boot configuration property that controls whether circular dependencies between beans are allowed. spring.main.allow-circular-references: "true" # Spring Boot configuration property that controls whether circular dependencies between beans are allowed.
# Server common parameters # Server common parameters
# HTTP server bind address and port, effective only when the web environment is enabled.
server: server:
# Server bind address (has no effect if web-environment is disabled). # Server bind address (has no effect if web-environment is disabled).
address: "${HTTP_BIND_ADDRESS:0.0.0.0}" address: "${HTTP_BIND_ADDRESS:0.0.0.0}"
# Server bind port (has no effect if web-environment is disabled). # Server bind port (has no effect if web-environment is disabled).
port: "${HTTP_BIND_PORT:8083}" port: "${HTTP_BIND_PORT:8083}"
# Zookeeper connection parameters. Used for service discovery. # ZooKeeper connection parameters
# Controls ZooKeeper-based service discovery and cluster coordination for microservice deployments.
zk: zk:
# Enable/disable zookeeper discovery service. # Enable/disable ZooKeeper discovery service.
enabled: "${ZOOKEEPER_ENABLED:false}" enabled: "${ZOOKEEPER_ENABLED:false}"
# Zookeeper connect string # ZooKeeper connect string
url: "${ZOOKEEPER_URL:localhost:2181}" url: "${ZOOKEEPER_URL:localhost:2181}"
# Zookeeper retry interval in milliseconds # ZooKeeper retry interval in milliseconds
retry_interval_ms: "${ZOOKEEPER_RETRY_INTERVAL_MS:3000}" retry_interval_ms: "${ZOOKEEPER_RETRY_INTERVAL_MS:3000}"
# Zookeeper connection timeout in milliseconds # ZooKeeper connection timeout in milliseconds
connection_timeout_ms: "${ZOOKEEPER_CONNECTION_TIMEOUT_MS:3000}" connection_timeout_ms: "${ZOOKEEPER_CONNECTION_TIMEOUT_MS:3000}"
# Zookeeper session timeout in milliseconds # ZooKeeper session timeout in milliseconds
session_timeout_ms: "${ZOOKEEPER_SESSION_TIMEOUT_MS:3000}" session_timeout_ms: "${ZOOKEEPER_SESSION_TIMEOUT_MS:3000}"
# Name of the directory in zookeeper 'filesystem' # Name of the directory in ZooKeeper 'filesystem'
zk_dir: "${ZOOKEEPER_NODES_DIR:/thingsboard}" zk_dir: "${ZOOKEEPER_NODES_DIR:/thingsboard}"
# The recalculate_delay property is recommended in a microservices architecture setup for rule-engine services. # The recalculate_delay property is recommended in a microservices architecture setup for rule-engine services.
# This property provides a pause to ensure that when a rule-engine service is restarted, other nodes don't immediately attempt to recalculate their partitions. # This property provides a pause to ensure that when a rule-engine service is restarted, other nodes don't immediately attempt to recalculate their partitions.
@ -46,6 +49,7 @@ zk:
recalculate_delay: "${ZOOKEEPER_RECALCULATE_DELAY_MS:0}" recalculate_delay: "${ZOOKEEPER_RECALCULATE_DELAY_MS:0}"
# Cache parameters # Cache parameters
# Configures the cache backend type and per-entity cache specifications such as TTL and maximum size.
cache: cache:
# caffeine or redis # caffeine or redis
type: "${CACHE_TYPE:redis}" type: "${CACHE_TYPE:redis}"
@ -55,6 +59,7 @@ cache:
maxSize: "${CACHE_SPECS_ENTITY_LIMITS_MAX_SIZE:100000}" # 0 means the cache is disabled maxSize: "${CACHE_SPECS_ENTITY_LIMITS_MAX_SIZE:100000}" # 0 means the cache is disabled
# Redis/Valkey configuration parameters # Redis/Valkey configuration parameters
# Connection, authentication, SSL, and connection pool settings for the Redis or Valkey cache backend.
redis: redis:
connection: connection:
# standalone or cluster or sentinel # standalone or cluster or sentinel
@ -90,9 +95,9 @@ redis:
password: "${REDIS_SENTINEL_PASSWORD:}" password: "${REDIS_SENTINEL_PASSWORD:}"
# if set false will be used pool config build from values of the pool config section # if set false will be used pool config build from values of the pool config section
useDefaultPoolConfig: "${REDIS_USE_DEFAULT_POOL_CONFIG:true}" useDefaultPoolConfig: "${REDIS_USE_DEFAULT_POOL_CONFIG:true}"
# db index # Redis logical database index to select after connecting.
db: "${REDIS_DB:0}" db: "${REDIS_DB:0}"
# db password # Password for Redis authentication (leave empty if not required).
password: "${REDIS_PASSWORD:}" password: "${REDIS_PASSWORD:}"
# Redis username for ACL authentication (Redis 6.0+). Leave empty for legacy password-only auth # Redis username for ACL authentication (Redis 6.0+). Leave empty for legacy password-only auth
username: "${REDIS_USERNAME:}" username: "${REDIS_USERNAME:}"
@ -133,6 +138,7 @@ redis:
blockWhenExhausted: "${REDIS_POOL_CONFIG_BLOCK_WHEN_EXHAUSTED:true}" blockWhenExhausted: "${REDIS_POOL_CONFIG_BLOCK_WHEN_EXHAUSTED:true}"
# LWM2M server parameters # LWM2M server parameters
# Session, JSON processing, RPC, logging, and LwM2M-specific transport settings including DTLS, bootstrap, and OTA.
transport: transport:
sessions: sessions:
# Session inactivity timeout is a global configuration parameter that defines how long the device transport session will be opened after the last message arrives from the device. # Session inactivity timeout is a global configuration parameter that defines how long the device transport session will be opened after the last message arrives from the device.
@ -303,6 +309,7 @@ transport:
print-interval-ms: "${TB_TRANSPORT_STATS_PRINT_INTERVAL_MS:60000}" print-interval-ms: "${TB_TRANSPORT_STATS_PRINT_INTERVAL_MS:60000}"
# Queue configuration properties # Queue configuration properties
# Defines the message queue backend (Kafka) and per-topic settings for transport, core, rule-engine, and JS evaluation.
queue: queue:
type: "${TB_QUEUE_TYPE:kafka}" # kafka (Apache Kafka) type: "${TB_QUEUE_TYPE:kafka}" # kafka (Apache Kafka)
prefix: "${TB_QUEUE_PREFIX:}" # Global queue prefix. If specified, prefix is added before default topic name: 'prefix.default_topic_name'. Prefix is applied to all topics (and consumer groups for kafka). prefix: "${TB_QUEUE_PREFIX:}" # Global queue prefix. If specified, prefix is added before default topic name: 'prefix.default_topic_name'. Prefix is applied to all topics (and consumer groups for kafka).
@ -456,12 +463,14 @@ queue:
poll_interval: "${TB_QUEUE_TRANSPORT_NOTIFICATIONS_POLL_INTERVAL_MS:25}" poll_interval: "${TB_QUEUE_TRANSPORT_NOTIFICATIONS_POLL_INTERVAL_MS:25}"
# Service common parameters # Service common parameters
# Identifies the service type and its unique instance ID within a ThingsBoard cluster.
service: service:
type: "${TB_SERVICE_TYPE:tb-transport}" # service type type: "${TB_SERVICE_TYPE:tb-transport}" # service type
# Unique id for this service (autogenerated if empty) # Unique id for this service (autogenerated if empty)
id: "${TB_SERVICE_ID:}" id: "${TB_SERVICE_ID:}"
# Usage statistics parameters # Usage statistics parameters
# Controls reporting of API usage statistics at system, tenant, and optionally customer level.
usage: usage:
stats: stats:
report: report:
@ -477,11 +486,13 @@ usage:
pack_size: "${USAGE_STATS_REPORT_PACK_SIZE:1024}" pack_size: "${USAGE_STATS_REPORT_PACK_SIZE:1024}"
# Metrics parameters # Metrics parameters
# Enables or disables Actuator-based metrics collection (e.g., Prometheus endpoint).
metrics: metrics:
# Enable/disable actuator metrics. # Enable/disable actuator metrics.
enabled: "${METRICS_ENABLED:false}" enabled: "${METRICS_ENABLED:false}"
# General management parameters # General management parameters
# Configures Spring Boot Actuator endpoint exposure, e.g. for Prometheus metrics scraping.
management: management:
endpoints: endpoints:
web: web:
@ -490,6 +501,7 @@ management:
include: "${METRICS_ENDPOINTS_EXPOSE:info}" include: "${METRICS_ENDPOINTS_EXPOSE:info}"
# Notification system parameters # Notification system parameters
# Configures deduplication rules for notification triggers to prevent repeated alerts within a time window.
notification_system: notification_system:
rules: rules:
# Semicolon-separated deduplication durations (in millis) for trigger types. Format: 'NotificationRuleTriggerType1:123;NotificationRuleTriggerType2:456' # Semicolon-separated deduplication durations (in millis) for trigger types. Format: 'NotificationRuleTriggerType1:123;NotificationRuleTriggerType2:456'

30
transport/mqtt/src/main/resources/tb-mqtt-transport.yml

@ -15,30 +15,33 @@
# #
# Spring common parameters # Spring common parameters
# Controls Spring Boot application type, web environment, and bean dependency settings.
spring.main.web-environment: "${WEB_APPLICATION_ENABLE:false}" # If you enabled process metrics you should also enable 'web-environment'. spring.main.web-environment: "${WEB_APPLICATION_ENABLE:false}" # If you enabled process metrics you should also enable 'web-environment'.
spring.main.web-application-type: "${WEB_APPLICATION_TYPE:none}" # If you enabled process metrics you should set 'web-application-type' to 'servlet' value. spring.main.web-application-type: "${WEB_APPLICATION_TYPE:none}" # If you enabled process metrics you should set 'web-application-type' to 'servlet' value.
spring.main.allow-circular-references: "true" # Spring Boot configuration property that controls whether circular dependencies between beans are allowed. spring.main.allow-circular-references: "true" # Spring Boot configuration property that controls whether circular dependencies between beans are allowed.
# Server common parameters # Server common parameters
# Configures the embedded HTTP server bind address and port for actuator/metrics endpoints.
server: server:
# Server bind address (has no effect if web-environment is disabled). # Server bind address (has no effect if web-environment is disabled).
address: "${HTTP_BIND_ADDRESS:0.0.0.0}" address: "${HTTP_BIND_ADDRESS:0.0.0.0}"
# Server bind port (has no effect if web-environment is disabled). # Server bind port (has no effect if web-environment is disabled).
port: "${HTTP_BIND_PORT:8083}" port: "${HTTP_BIND_PORT:8083}"
# Zookeeper connection parameters. Used for service discovery. # ZooKeeper connection parameters. Used for service discovery.
# Defines connection, session, retry settings and the node directory path for ZooKeeper-based cluster coordination.
zk: zk:
# Enable/disable zookeeper discovery service. # Enable/disable ZooKeeper discovery service.
enabled: "${ZOOKEEPER_ENABLED:false}" enabled: "${ZOOKEEPER_ENABLED:false}"
# Zookeeper connect string # ZooKeeper connect string
url: "${ZOOKEEPER_URL:localhost:2181}" url: "${ZOOKEEPER_URL:localhost:2181}"
# Zookeeper retry interval in milliseconds # ZooKeeper retry interval in milliseconds
retry_interval_ms: "${ZOOKEEPER_RETRY_INTERVAL_MS:3000}" retry_interval_ms: "${ZOOKEEPER_RETRY_INTERVAL_MS:3000}"
# Zookeeper connection timeout in milliseconds # ZooKeeper connection timeout in milliseconds
connection_timeout_ms: "${ZOOKEEPER_CONNECTION_TIMEOUT_MS:3000}" connection_timeout_ms: "${ZOOKEEPER_CONNECTION_TIMEOUT_MS:3000}"
# Zookeeper session timeout in milliseconds # ZooKeeper session timeout in milliseconds
session_timeout_ms: "${ZOOKEEPER_SESSION_TIMEOUT_MS:3000}" session_timeout_ms: "${ZOOKEEPER_SESSION_TIMEOUT_MS:3000}"
# Name of the directory in zookeeper 'filesystem' # Name of the directory in ZooKeeper 'filesystem'
zk_dir: "${ZOOKEEPER_NODES_DIR:/thingsboard}" zk_dir: "${ZOOKEEPER_NODES_DIR:/thingsboard}"
# The recalculate_delay property is recommended in a microservices architecture setup for rule-engine services. # The recalculate_delay property is recommended in a microservices architecture setup for rule-engine services.
# This property provides a pause to ensure that when a rule-engine service is restarted, other nodes don't immediately attempt to recalculate their partitions. # This property provides a pause to ensure that when a rule-engine service is restarted, other nodes don't immediately attempt to recalculate their partitions.
@ -46,6 +49,7 @@ zk:
recalculate_delay: "${ZOOKEEPER_RECALCULATE_DELAY_MS:0}" recalculate_delay: "${ZOOKEEPER_RECALCULATE_DELAY_MS:0}"
# Cache parameters # Cache parameters
# Configures the cache backend type (Caffeine or Redis) and per-cache TTL and size limits.
cache: cache:
# caffeine or redis # caffeine or redis
type: "${CACHE_TYPE:redis}" type: "${CACHE_TYPE:redis}"
@ -55,6 +59,7 @@ cache:
maxSize: "${CACHE_SPECS_ENTITY_LIMITS_MAX_SIZE:100000}" # 0 means the cache is disabled maxSize: "${CACHE_SPECS_ENTITY_LIMITS_MAX_SIZE:100000}" # 0 means the cache is disabled
# Redis/Valkey configuration parameters # Redis/Valkey configuration parameters
# Covers connection mode (standalone, cluster, sentinel), SSL, credentials, and connection pool tuning.
redis: redis:
# standalone or cluster or sentinel # standalone or cluster or sentinel
connection: connection:
@ -91,9 +96,9 @@ redis:
password: "${REDIS_SENTINEL_PASSWORD:}" password: "${REDIS_SENTINEL_PASSWORD:}"
# if set false will be used pool config build from values of the pool config section # if set false will be used pool config build from values of the pool config section
useDefaultPoolConfig: "${REDIS_USE_DEFAULT_POOL_CONFIG:true}" useDefaultPoolConfig: "${REDIS_USE_DEFAULT_POOL_CONFIG:true}"
# db index # Redis logical database index to select after connecting.
db: "${REDIS_DB:0}" db: "${REDIS_DB:0}"
# db password # Password for Redis authentication (leave empty if not required).
password: "${REDIS_PASSWORD:}" password: "${REDIS_PASSWORD:}"
# Redis username for ACL authentication (Redis 6.0+). Leave empty for legacy password-only auth # Redis username for ACL authentication (Redis 6.0+). Leave empty for legacy password-only auth
username: "${REDIS_USERNAME:}" username: "${REDIS_USERNAME:}"
@ -134,6 +139,7 @@ redis:
blockWhenExhausted: "${REDIS_POOL_CONFIG_BLOCK_WHEN_EXHAUSTED:true}" blockWhenExhausted: "${REDIS_POOL_CONFIG_BLOCK_WHEN_EXHAUSTED:true}"
# MQTT server parameters # MQTT server parameters
# Configures the MQTT transport layer including bind address/port, SSL, Netty tuning, session management, rate limits, and message processing options.
transport: transport:
mqtt: mqtt:
# MQTT bind-address # MQTT bind-address
@ -236,6 +242,7 @@ transport:
ip_block_timeout: "${TB_TRANSPORT_IP_BLOCK_TIMEOUT:60000}" ip_block_timeout: "${TB_TRANSPORT_IP_BLOCK_TIMEOUT:60000}"
# Queue configuration parameters # Queue configuration parameters
# Defines the messaging queue backend (Kafka), topic names, partitioning, and poll/processing settings for all platform microservices.
queue: queue:
type: "${TB_QUEUE_TYPE:kafka}" # kafka (Apache Kafka) type: "${TB_QUEUE_TYPE:kafka}" # kafka (Apache Kafka)
prefix: "${TB_QUEUE_PREFIX:}" # Global queue prefix. If specified, prefix is added before default topic name: 'prefix.default_topic_name'. Prefix is applied to all topics (and consumer groups for kafka). prefix: "${TB_QUEUE_PREFIX:}" # Global queue prefix. If specified, prefix is added before default topic name: 'prefix.default_topic_name'. Prefix is applied to all topics (and consumer groups for kafka).
@ -389,12 +396,14 @@ queue:
poll_interval: "${TB_QUEUE_TRANSPORT_NOTIFICATIONS_POLL_INTERVAL_MS:25}" poll_interval: "${TB_QUEUE_TRANSPORT_NOTIFICATIONS_POLL_INTERVAL_MS:25}"
# Service common properties # Service common properties
# Identifies this service instance within the cluster by type and optional unique ID.
service: service:
type: "${TB_SERVICE_TYPE:tb-transport}" # service type type: "${TB_SERVICE_TYPE:tb-transport}" # service type
# Unique id for this service (autogenerated if empty) # Unique id for this service (autogenerated if empty)
id: "${TB_SERVICE_ID:}" id: "${TB_SERVICE_ID:}"
# Usage statistics parameters # Usage statistics parameters
# Controls reporting of API usage statistics at system, tenant, and customer levels with configurable intervals.
usage: usage:
stats: stats:
report: report:
@ -410,11 +419,13 @@ usage:
pack_size: "${USAGE_STATS_REPORT_PACK_SIZE:1024}" pack_size: "${USAGE_STATS_REPORT_PACK_SIZE:1024}"
# Metrics parameters # Metrics parameters
# Enables or disables Micrometer actuator metrics collection for monitoring this service.
metrics: metrics:
# Enable/disable actuator metrics. # Enable/disable actuator metrics.
enabled: "${METRICS_ENABLED:false}" enabled: "${METRICS_ENABLED:false}"
# General management parameters # General management parameters
# Configures Spring Boot Actuator endpoint exposure, including which endpoints are accessible over HTTP.
management: management:
endpoints: endpoints:
web: web:
@ -423,6 +434,7 @@ management:
include: "${METRICS_ENDPOINTS_EXPOSE:info}" include: "${METRICS_ENDPOINTS_EXPOSE:info}"
# Notification system parameters # Notification system parameters
# Configures deduplication rules for notification triggers to prevent repeated alerts within a defined time window.
notification_system: notification_system:
rules: rules:
# Semicolon-separated deduplication durations (in millis) for trigger types. Format: 'NotificationRuleTriggerType1:123;NotificationRuleTriggerType2:456' # Semicolon-separated deduplication durations (in millis) for trigger types. Format: 'NotificationRuleTriggerType1:123;NotificationRuleTriggerType2:456'

Some files were not shown because too many files changed in this diff

Loading…
Cancel
Save