Browse Source

Merge pull request #10977 from thingsboard/feature/attr_tskv_version

Add versioning for attributes, latest timeseries and relations
pull/11360/head
Andrew Shvayka 2 years ago
committed by GitHub
parent
commit
ea662bc26a
No known key found for this signature in database GPG Key ID: B5690EEEBB952194
  1. 16
      application/src/main/data/upgrade/3.7.0/schema_update.sql
  2. 40
      application/src/main/java/org/thingsboard/server/controller/EntityRelationController.java
  3. 2
      application/src/main/java/org/thingsboard/server/service/device/DeviceProvisionServiceImpl.java
  4. 6
      application/src/main/java/org/thingsboard/server/service/edge/rpc/EdgeGrpcSession.java
  5. 16
      application/src/main/java/org/thingsboard/server/service/entitiy/entity/relation/DefaultTbEntityRelationService.java
  6. 4
      application/src/main/java/org/thingsboard/server/service/entitiy/entity/relation/TbEntityRelationService.java
  7. 2
      application/src/main/java/org/thingsboard/server/service/install/DefaultSystemDataLoaderService.java
  8. 6
      application/src/main/java/org/thingsboard/server/service/install/update/DefaultCacheCleanupService.java
  9. 6
      application/src/main/java/org/thingsboard/server/service/telemetry/DefaultTelemetrySubscriptionService.java
  10. 7
      application/src/main/resources/thingsboard.yml
  11. 18
      application/src/test/java/org/thingsboard/server/controller/EntityRelationControllerTest.java
  12. 11
      application/src/test/java/org/thingsboard/server/edge/RelationEdgeTest.java
  13. 3
      application/src/test/java/org/thingsboard/server/service/sync/vc/VersionControlTest.java
  14. 21
      application/src/test/java/org/thingsboard/server/transport/mqtt/sparkplug/connection/AbstractMqttV5ClientSparkplugConnectionTest.java
  15. 17
      application/src/test/java/org/thingsboard/server/transport/mqtt/sparkplug/timeseries/AbstractMqttV5ClientSparkplugTelemetryTest.java
  16. 57
      build.sh
  17. 4
      common/cache/src/main/java/org/thingsboard/server/cache/CaffeineTbCacheTransaction.java
  18. 34
      common/cache/src/main/java/org/thingsboard/server/cache/CaffeineTbTransactionalCache.java
  19. 5
      common/cache/src/main/java/org/thingsboard/server/cache/RedisTbCacheTransaction.java
  20. 50
      common/cache/src/main/java/org/thingsboard/server/cache/RedisTbTransactionalCache.java
  21. 2
      common/cache/src/main/java/org/thingsboard/server/cache/TbCacheTransaction.java
  22. 35
      common/cache/src/main/java/org/thingsboard/server/cache/TbJavaRedisSerializer.java
  23. 6
      common/cache/src/main/java/org/thingsboard/server/cache/TbTransactionalCache.java
  24. 95
      common/cache/src/main/java/org/thingsboard/server/cache/VersionedCaffeineTbCache.java
  25. 181
      common/cache/src/main/java/org/thingsboard/server/cache/VersionedRedisTbCache.java
  26. 53
      common/cache/src/main/java/org/thingsboard/server/cache/VersionedTbCache.java
  27. 45
      common/cache/src/test/java/org/thingsboard/server/cache/TsLatestRedisCacheTest.java
  28. 22
      common/dao-api/src/main/java/org/thingsboard/server/dao/attributes/AttributesService.java
  29. 4
      common/dao-api/src/main/java/org/thingsboard/server/dao/relation/RelationService.java
  30. 2
      common/dao-api/src/main/java/org/thingsboard/server/dao/timeseries/TimeseriesService.java
  31. 26
      common/dao-api/src/main/java/org/thingsboard/server/dao/util/SqlTsLatestAnyDaoCachedRedis.java
  32. 1
      common/data/src/main/java/org/thingsboard/server/common/data/CacheConstants.java
  33. 15
      common/data/src/main/java/org/thingsboard/server/common/data/HasVersion.java
  34. 4
      common/data/src/main/java/org/thingsboard/server/common/data/kv/AttributeKvEntry.java
  35. 43
      common/data/src/main/java/org/thingsboard/server/common/data/kv/BaseAttributeKvEntry.java
  36. 38
      common/data/src/main/java/org/thingsboard/server/common/data/kv/BasicTsKvEntry.java
  37. 3
      common/data/src/main/java/org/thingsboard/server/common/data/kv/TsKvEntry.java
  38. 11
      common/data/src/main/java/org/thingsboard/server/common/data/kv/TsKvLatestRemovingResult.java
  39. 54
      common/data/src/main/java/org/thingsboard/server/common/data/relation/EntityRelation.java
  40. 8
      common/data/src/main/java/org/thingsboard/server/common/data/util/CollectionsUtil.java
  41. 14
      common/proto/src/main/java/org/thingsboard/server/common/util/KvProtoUtil.java
  42. 98
      common/proto/src/main/java/org/thingsboard/server/common/util/ProtoUtils.java
  43. 2
      common/proto/src/main/proto/queue.proto
  44. 5
      dao/pom.xml
  45. 130
      dao/src/main/java/org/thingsboard/server/dao/AbstractVersionedInsertRepository.java
  46. 4
      dao/src/main/java/org/thingsboard/server/dao/attributes/AttributeCaffeineCache.java
  47. 69
      dao/src/main/java/org/thingsboard/server/dao/attributes/AttributeRedisCache.java
  48. 5
      dao/src/main/java/org/thingsboard/server/dao/attributes/AttributesDao.java
  49. 42
      dao/src/main/java/org/thingsboard/server/dao/attributes/BaseAttributesService.java
  50. 123
      dao/src/main/java/org/thingsboard/server/dao/attributes/CachedAttributesService.java
  51. 20
      dao/src/main/java/org/thingsboard/server/dao/model/BaseVersionedEntity.java
  52. 1
      dao/src/main/java/org/thingsboard/server/dao/model/ModelConstants.java
  53. 7
      dao/src/main/java/org/thingsboard/server/dao/model/sql/AbstractTsKvEntity.java
  54. 6
      dao/src/main/java/org/thingsboard/server/dao/model/sql/AttributeKvEntity.java
  55. 5
      dao/src/main/java/org/thingsboard/server/dao/model/sql/RelationEntity.java
  56. 11
      dao/src/main/java/org/thingsboard/server/dao/model/sqlts/latest/TsKvLatestEntity.java
  57. 123
      dao/src/main/java/org/thingsboard/server/dao/relation/BaseRelationService.java
  58. 24
      dao/src/main/java/org/thingsboard/server/dao/relation/RelationDao.java
  59. 4
      dao/src/main/java/org/thingsboard/server/dao/sql/JpaAbstractDaoListeningExecutorService.java
  60. 47
      dao/src/main/java/org/thingsboard/server/dao/sql/TbSqlBlockingQueue.java
  61. 1
      dao/src/main/java/org/thingsboard/server/dao/sql/TbSqlBlockingQueueParams.java
  62. 15
      dao/src/main/java/org/thingsboard/server/dao/sql/TbSqlBlockingQueueWrapper.java
  63. 8
      dao/src/main/java/org/thingsboard/server/dao/sql/TbSqlQueue.java
  64. 6
      dao/src/main/java/org/thingsboard/server/dao/sql/TbSqlQueueElement.java
  65. 228
      dao/src/main/java/org/thingsboard/server/dao/sql/attributes/AttributeKvInsertRepository.java
  66. 5
      dao/src/main/java/org/thingsboard/server/dao/sql/attributes/AttributeKvRepository.java
  67. 32
      dao/src/main/java/org/thingsboard/server/dao/sql/attributes/JpaAttributeDao.java
  68. 2
      dao/src/main/java/org/thingsboard/server/dao/sql/edge/JpaBaseEdgeEventDao.java
  69. 2
      dao/src/main/java/org/thingsboard/server/dao/sql/event/JpaBaseEventDao.java
  70. 157
      dao/src/main/java/org/thingsboard/server/dao/sql/relation/JpaRelationDao.java
  71. 2
      dao/src/main/java/org/thingsboard/server/dao/sql/relation/RelationInsertRepository.java
  72. 3
      dao/src/main/java/org/thingsboard/server/dao/sql/relation/RelationRepository.java
  73. 50
      dao/src/main/java/org/thingsboard/server/dao/sql/relation/SqlRelationInsertRepository.java
  74. 2
      dao/src/main/java/org/thingsboard/server/dao/sqlts/AbstractChunkedAggregationTimeseriesDao.java
  75. 170
      dao/src/main/java/org/thingsboard/server/dao/sqlts/CachedRedisSqlTimeseriesLatestDao.java
  76. 62
      dao/src/main/java/org/thingsboard/server/dao/sqlts/SqlTimeseriesLatestDao.java
  77. 2
      dao/src/main/java/org/thingsboard/server/dao/sqlts/insert/latest/InsertLatestTsRepository.java
  78. 224
      dao/src/main/java/org/thingsboard/server/dao/sqlts/insert/latest/sql/SqlLatestInsertTsRepository.java
  79. 2
      dao/src/main/java/org/thingsboard/server/dao/sqlts/latest/SearchTsKvLatestRepository.java
  80. 2
      dao/src/main/java/org/thingsboard/server/dao/sqlts/timescale/TimescaleTimeseriesDao.java
  81. 4
      dao/src/main/java/org/thingsboard/server/dao/timeseries/BaseTimeseriesService.java
  82. 4
      dao/src/main/java/org/thingsboard/server/dao/timeseries/CassandraBaseTimeseriesLatestDao.java
  83. 2
      dao/src/main/java/org/thingsboard/server/dao/timeseries/TimeseriesLatestDao.java
  84. 40
      dao/src/main/java/org/thingsboard/server/dao/timeseries/TsLatestCacheKey.java
  85. 55
      dao/src/main/java/org/thingsboard/server/dao/timeseries/TsLatestRedisCache.java
  86. 9
      dao/src/main/resources/sql/schema-entities.sql
  87. 3
      dao/src/main/resources/sql/schema-timescale.sql
  88. 10
      dao/src/main/resources/sql/schema-ts-latest-psql.sql
  89. 91
      dao/src/test/java/org/thingsboard/server/dao/AbstractRedisClusterContainer.java
  90. 31
      dao/src/test/java/org/thingsboard/server/dao/RedisClusterSqlTestSuite.java
  91. 64
      dao/src/test/java/org/thingsboard/server/dao/RedisJUnit5Test.java
  92. 4
      dao/src/test/java/org/thingsboard/server/dao/service/AlarmServiceTest.java
  93. 16
      dao/src/test/java/org/thingsboard/server/dao/service/EntityServiceTest.java
  94. 6
      dao/src/test/java/org/thingsboard/server/dao/service/RelationCacheTest.java
  95. 92
      dao/src/test/java/org/thingsboard/server/dao/service/RelationServiceTest.java
  96. 44
      dao/src/test/java/org/thingsboard/server/dao/service/attributes/BaseAttributesServiceTest.java
  97. 114
      dao/src/test/java/org/thingsboard/server/dao/service/attributes/sql/AttributeCacheServiceSqlTest.java
  98. 119
      dao/src/test/java/org/thingsboard/server/dao/service/timeseries/BaseTimeseriesServiceTest.java
  99. 153
      dao/src/test/java/org/thingsboard/server/dao/service/timeseries/sql/LatestTimeseriesPerformanceTest.java
  100. 17
      dao/src/test/resources/application-test.properties

16
application/src/main/data/upgrade/3.7.0/schema_update.sql

@ -14,3 +14,19 @@
-- limitations under the License. -- limitations under the License.
-- --
-- KV VERSIONING UPDATE START
CREATE SEQUENCE IF NOT EXISTS attribute_kv_version_seq cache 1;
CREATE SEQUENCE IF NOT EXISTS ts_kv_latest_version_seq cache 1;
ALTER TABLE attribute_kv ADD COLUMN version bigint default 0;
ALTER TABLE ts_kv_latest ADD COLUMN version bigint default 0;
-- KV VERSIONING UPDATE END
-- RELATION VERSIONING UPDATE START
CREATE SEQUENCE IF NOT EXISTS relation_version_seq cache 1;
ALTER TABLE relation ADD COLUMN version bigint default 0;
-- RELATION VERSIONING UPDATE END

40
application/src/main/java/org/thingsboard/server/controller/EntityRelationController.java

@ -79,7 +79,23 @@ public class EntityRelationController extends BaseController {
@RequestMapping(value = "/relation", method = RequestMethod.POST) @RequestMapping(value = "/relation", method = RequestMethod.POST)
@ResponseStatus(value = HttpStatus.OK) @ResponseStatus(value = HttpStatus.OK)
public void saveRelation(@Parameter(description = "A JSON value representing the relation.", required = true) public void saveRelation(@Parameter(description = "A JSON value representing the relation.", required = true)
@RequestBody EntityRelation relation) throws ThingsboardException { @RequestBody EntityRelation relation) throws ThingsboardException {
doSave(relation);
}
@ApiOperation(value = "Create Relation (saveRelationV2)",
notes = "Creates or updates a relation between two entities in the platform. " +
"Relations unique key is a combination of from/to entity id and relation type group and relation type. " +
SECURITY_CHECKS_ENTITIES_DESCRIPTION)
@PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN', 'CUSTOMER_USER')")
@RequestMapping(value = "/v2/relation", method = RequestMethod.POST)
@ResponseStatus(value = HttpStatus.OK)
public EntityRelation saveRelationV2(@Parameter(description = "A JSON value representing the relation.", required = true)
@RequestBody EntityRelation relation) throws ThingsboardException {
return doSave(relation);
}
private EntityRelation doSave(EntityRelation relation) throws ThingsboardException {
checkNotNull(relation); checkNotNull(relation);
checkCanCreateRelation(relation.getFrom()); checkCanCreateRelation(relation.getFrom());
checkCanCreateRelation(relation.getTo()); checkCanCreateRelation(relation.getTo());
@ -87,7 +103,7 @@ public class EntityRelationController extends BaseController {
relation.setTypeGroup(RelationTypeGroup.COMMON); relation.setTypeGroup(RelationTypeGroup.COMMON);
} }
tbEntityRelationService.save(getTenantId(), getCurrentUser().getCustomerId(), relation, getCurrentUser()); return tbEntityRelationService.save(getTenantId(), getCurrentUser().getCustomerId(), relation, getCurrentUser());
} }
@ApiOperation(value = "Delete Relation (deleteRelation)", @ApiOperation(value = "Delete Relation (deleteRelation)",
@ -101,6 +117,24 @@ public class EntityRelationController extends BaseController {
@Parameter(description = RELATION_TYPE_GROUP_PARAM_DESCRIPTION) @RequestParam(value = "relationTypeGroup", required = false) String strRelationTypeGroup, @Parameter(description = RELATION_TYPE_GROUP_PARAM_DESCRIPTION) @RequestParam(value = "relationTypeGroup", required = false) String strRelationTypeGroup,
@Parameter(description = ENTITY_ID_PARAM_DESCRIPTION, required = true) @RequestParam(TO_ID) String strToId, @Parameter(description = ENTITY_ID_PARAM_DESCRIPTION, required = true) @RequestParam(TO_ID) String strToId,
@Parameter(description = ENTITY_TYPE_PARAM_DESCRIPTION, required = true) @RequestParam(TO_TYPE) String strToType) throws ThingsboardException { @Parameter(description = ENTITY_TYPE_PARAM_DESCRIPTION, required = true) @RequestParam(TO_TYPE) String strToType) throws ThingsboardException {
doDelete(strFromId, strFromType, strRelationType, strRelationTypeGroup, strToId, strToType);
}
@ApiOperation(value = "Delete Relation (deleteRelationV2)",
notes = "Deletes a relation between two entities in the platform. " + SECURITY_CHECKS_ENTITIES_DESCRIPTION)
@PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN', 'CUSTOMER_USER')")
@RequestMapping(value = "/v2/relation", method = RequestMethod.DELETE, params = {FROM_ID, FROM_TYPE, RELATION_TYPE, TO_ID, TO_TYPE})
@ResponseStatus(value = HttpStatus.OK)
public EntityRelation deleteRelationV2(@Parameter(description = ENTITY_ID_PARAM_DESCRIPTION, required = true) @RequestParam(FROM_ID) String strFromId,
@Parameter(description = ENTITY_TYPE_PARAM_DESCRIPTION, required = true) @RequestParam(FROM_TYPE) String strFromType,
@Parameter(description = RELATION_TYPE_PARAM_DESCRIPTION, required = true) @RequestParam(RELATION_TYPE) String strRelationType,
@Parameter(description = RELATION_TYPE_GROUP_PARAM_DESCRIPTION) @RequestParam(value = "relationTypeGroup", required = false) String strRelationTypeGroup,
@Parameter(description = ENTITY_ID_PARAM_DESCRIPTION, required = true) @RequestParam(TO_ID) String strToId,
@Parameter(description = ENTITY_TYPE_PARAM_DESCRIPTION, required = true) @RequestParam(TO_TYPE) String strToType) throws ThingsboardException {
return doDelete(strFromId, strFromType, strRelationType, strRelationTypeGroup, strToId, strToType);
}
private EntityRelation doDelete(String strFromId, String strFromType, String strRelationType, String strRelationTypeGroup, String strToId, String strToType) throws ThingsboardException {
checkParameter(FROM_ID, strFromId); checkParameter(FROM_ID, strFromId);
checkParameter(FROM_TYPE, strFromType); checkParameter(FROM_TYPE, strFromType);
checkParameter(RELATION_TYPE, strRelationType); checkParameter(RELATION_TYPE, strRelationType);
@ -113,7 +147,7 @@ public class EntityRelationController extends BaseController {
RelationTypeGroup relationTypeGroup = parseRelationTypeGroup(strRelationTypeGroup, RelationTypeGroup.COMMON); RelationTypeGroup relationTypeGroup = parseRelationTypeGroup(strRelationTypeGroup, RelationTypeGroup.COMMON);
EntityRelation relation = new EntityRelation(fromId, toId, strRelationType, relationTypeGroup); EntityRelation relation = new EntityRelation(fromId, toId, strRelationType, relationTypeGroup);
tbEntityRelationService.delete(getTenantId(), getCurrentUser().getCustomerId(), relation, getCurrentUser()); return tbEntityRelationService.delete(getTenantId(), getCurrentUser().getCustomerId(), relation, getCurrentUser());
} }
@ApiOperation(value = "Delete common relations (deleteCommonRelations)", @ApiOperation(value = "Delete common relations (deleteCommonRelations)",

2
application/src/main/java/org/thingsboard/server/service/device/DeviceProvisionServiceImpl.java

@ -240,7 +240,7 @@ public class DeviceProvisionServiceImpl implements DeviceProvisionService {
return deviceCredentialsService.updateDeviceCredentials(tenantId, deviceCredentials); return deviceCredentialsService.updateDeviceCredentials(tenantId, deviceCredentials);
} }
private ListenableFuture<List<String>> saveProvisionStateAttribute(Device device) { private ListenableFuture<List<Long>> saveProvisionStateAttribute(Device device) {
return attributesService.save(device.getTenantId(), device.getId(), AttributeScope.SERVER_SCOPE, return attributesService.save(device.getTenantId(), device.getId(), AttributeScope.SERVER_SCOPE,
Collections.singletonList(new BaseAttributeKvEntry(new StringDataEntry(DEVICE_PROVISION_STATE, PROVISIONED_STATE), Collections.singletonList(new BaseAttributeKvEntry(new StringDataEntry(DEVICE_PROVISION_STATE, PROVISIONED_STATE),
System.currentTimeMillis()))); System.currentTimeMillis())));

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

@ -371,10 +371,10 @@ public final class EdgeGrpcSession implements Closeable {
@Override @Override
public void onSuccess(@Nullable Pair<Long, Long> newStartTsAndSeqId) { public void onSuccess(@Nullable Pair<Long, Long> newStartTsAndSeqId) {
if (newStartTsAndSeqId != null) { if (newStartTsAndSeqId != null) {
ListenableFuture<List<String>> updateFuture = updateQueueStartTsAndSeqId(newStartTsAndSeqId); ListenableFuture<List<Long>> updateFuture = updateQueueStartTsAndSeqId(newStartTsAndSeqId);
Futures.addCallback(updateFuture, new FutureCallback<>() { Futures.addCallback(updateFuture, new FutureCallback<>() {
@Override @Override
public void onSuccess(@Nullable List<String> list) { public void onSuccess(@Nullable List<Long> list) {
log.debug("[{}][{}] queue offset was updated [{}]", tenantId, sessionId, newStartTsAndSeqId); log.debug("[{}][{}] queue offset was updated [{}]", tenantId, sessionId, newStartTsAndSeqId);
if (fetcher.isSeqIdNewCycleStarted()) { if (fetcher.isSeqIdNewCycleStarted()) {
seqIdEnd = fetcher.getSeqIdEnd(); seqIdEnd = fetcher.getSeqIdEnd();
@ -626,7 +626,7 @@ public final class EdgeGrpcSession implements Closeable {
return startSeqId; return startSeqId;
} }
private ListenableFuture<List<String>> updateQueueStartTsAndSeqId(Pair<Long, Long> pair) { private ListenableFuture<List<Long>> updateQueueStartTsAndSeqId(Pair<Long, Long> pair) {
this.newStartTs = pair.getFirst(); this.newStartTs = pair.getFirst();
this.newStartSeqId = pair.getSecond(); this.newStartSeqId = pair.getSecond();
log.trace("[{}] updateQueueStartTsAndSeqId [{}][{}][{}]", this.sessionId, edge.getId(), this.newStartTs, this.newStartSeqId); log.trace("[{}] updateQueueStartTsAndSeqId [{}][{}][{}]", this.sessionId, edge.getId(), this.newStartTs, this.newStartSeqId);

16
application/src/main/java/org/thingsboard/server/service/entitiy/entity/relation/DefaultTbEntityRelationService.java

@ -39,12 +39,13 @@ public class DefaultTbEntityRelationService extends AbstractTbEntityService impl
private final RelationService relationService; private final RelationService relationService;
@Override @Override
public void save(TenantId tenantId, CustomerId customerId, EntityRelation relation, User user) throws ThingsboardException { public EntityRelation save(TenantId tenantId, CustomerId customerId, EntityRelation relation, User user) throws ThingsboardException {
ActionType actionType = ActionType.RELATION_ADD_OR_UPDATE; ActionType actionType = ActionType.RELATION_ADD_OR_UPDATE;
try { try {
relationService.saveRelation(tenantId, relation); var savedRelation = relationService.saveRelation(tenantId, relation);
logEntityActionService.logEntityRelationAction(tenantId, customerId, logEntityActionService.logEntityRelationAction(tenantId, customerId,
relation, user, actionType, null, relation); savedRelation, user, actionType, null, savedRelation);
return savedRelation;
} catch (Exception e) { } catch (Exception e) {
logEntityActionService.logEntityRelationAction(tenantId, customerId, logEntityActionService.logEntityRelationAction(tenantId, customerId,
relation, user, actionType, e, relation); relation, user, actionType, e, relation);
@ -53,14 +54,15 @@ public class DefaultTbEntityRelationService extends AbstractTbEntityService impl
} }
@Override @Override
public void delete(TenantId tenantId, CustomerId customerId, EntityRelation relation, User user) throws ThingsboardException { public EntityRelation delete(TenantId tenantId, CustomerId customerId, EntityRelation relation, User user) throws ThingsboardException {
ActionType actionType = ActionType.RELATION_DELETED; ActionType actionType = ActionType.RELATION_DELETED;
try { try {
boolean found = relationService.deleteRelation(tenantId, relation.getFrom(), relation.getTo(), relation.getType(), relation.getTypeGroup()); var found = relationService.deleteRelation(tenantId, relation.getFrom(), relation.getTo(), relation.getType(), relation.getTypeGroup());
if (!found) { if (found == null) {
throw new ThingsboardException("Requested item wasn't found!", ThingsboardErrorCode.ITEM_NOT_FOUND); throw new ThingsboardException("Requested item wasn't found!", ThingsboardErrorCode.ITEM_NOT_FOUND);
} }
logEntityActionService.logEntityRelationAction(tenantId, customerId, relation, user, actionType, null, relation); logEntityActionService.logEntityRelationAction(tenantId, customerId, found, user, actionType, null, found);
return found;
} catch (Exception e) { } catch (Exception e) {
logEntityActionService.logEntityRelationAction(tenantId, customerId, logEntityActionService.logEntityRelationAction(tenantId, customerId,
relation, user, actionType, e, relation); relation, user, actionType, e, relation);

4
application/src/main/java/org/thingsboard/server/service/entitiy/entity/relation/TbEntityRelationService.java

@ -24,9 +24,9 @@ import org.thingsboard.server.common.data.relation.EntityRelation;
public interface TbEntityRelationService { public interface TbEntityRelationService {
void save(TenantId tenantId, CustomerId customerId, EntityRelation entity, User user) throws ThingsboardException; EntityRelation save(TenantId tenantId, CustomerId customerId, EntityRelation entity, User user) throws ThingsboardException;
void delete(TenantId tenantId, CustomerId customerId, EntityRelation entity, User user) throws ThingsboardException; EntityRelation delete(TenantId tenantId, CustomerId customerId, EntityRelation entity, User user) throws ThingsboardException;
void deleteCommonRelations(TenantId tenantId, CustomerId customerId, EntityId entityId, User user) throws ThingsboardException; void deleteCommonRelations(TenantId tenantId, CustomerId customerId, EntityId entityId, User user) throws ThingsboardException;

2
application/src/main/java/org/thingsboard/server/service/install/DefaultSystemDataLoaderService.java

@ -577,7 +577,7 @@ public class DefaultSystemDataLoaderService implements SystemDataLoaderService {
Collections.singletonList(new BasicTsKvEntry(System.currentTimeMillis(), new BooleanDataEntry(key, value))), 0L); Collections.singletonList(new BasicTsKvEntry(System.currentTimeMillis(), new BooleanDataEntry(key, value))), 0L);
addTsCallback(saveFuture, new TelemetrySaveCallback<>(deviceId, key, value)); addTsCallback(saveFuture, new TelemetrySaveCallback<>(deviceId, key, value));
} else { } else {
ListenableFuture<List<String>> saveFuture = attributesService.save(TenantId.SYS_TENANT_ID, deviceId, AttributeScope.SERVER_SCOPE, ListenableFuture<List<Long>> saveFuture = attributesService.save(TenantId.SYS_TENANT_ID, deviceId, AttributeScope.SERVER_SCOPE,
Collections.singletonList(new BaseAttributeKvEntry(new BooleanDataEntry(key, value) Collections.singletonList(new BaseAttributeKvEntry(new BooleanDataEntry(key, value)
, System.currentTimeMillis()))); , System.currentTimeMillis())));
addTsCallback(saveFuture, new TelemetrySaveCallback<>(deviceId, key, value)); addTsCallback(saveFuture, new TelemetrySaveCallback<>(deviceId, key, value));

6
application/src/main/java/org/thingsboard/server/service/install/update/DefaultCacheCleanupService.java

@ -61,6 +61,10 @@ public class DefaultCacheCleanupService implements CacheCleanupService {
log.info("Clearing cache to upgrade from version 3.6.4 to 3.7.0"); log.info("Clearing cache to upgrade from version 3.6.4 to 3.7.0");
clearAll(); clearAll();
break; break;
case "3.7.0":
log.info("Clearing cache to upgrade from version 3.7.0 to 3.7.1");
clearAll();
break;
default: default:
//Do nothing, since cache cleanup is optional. //Do nothing, since cache cleanup is optional.
} }
@ -81,7 +85,7 @@ public class DefaultCacheCleanupService implements CacheCleanupService {
if (redisTemplate.isPresent()) { if (redisTemplate.isPresent()) {
log.info("Flushing all caches"); log.info("Flushing all caches");
redisTemplate.get().execute((RedisCallback<Object>) connection -> { redisTemplate.get().execute((RedisCallback<Object>) connection -> {
connection.flushAll(); connection.serverCommands().flushAll();
return null; return null;
}); });
return; return;

6
application/src/main/java/org/thingsboard/server/service/telemetry/DefaultTelemetrySubscriptionService.java

@ -260,14 +260,14 @@ public class DefaultTelemetrySubscriptionService extends AbstractSubscriptionSer
@Override @Override
public void saveAndNotifyInternal(TenantId tenantId, EntityId entityId, String scope, List<AttributeKvEntry> attributes, boolean notifyDevice, FutureCallback<Void> callback) { public void saveAndNotifyInternal(TenantId tenantId, EntityId entityId, String scope, List<AttributeKvEntry> attributes, boolean notifyDevice, FutureCallback<Void> callback) {
ListenableFuture<List<String>> saveFuture = attrService.save(tenantId, entityId, scope, attributes); ListenableFuture<List<Long>> saveFuture = attrService.save(tenantId, entityId, scope, attributes);
addVoidCallback(saveFuture, callback); addVoidCallback(saveFuture, callback);
addWsCallback(saveFuture, success -> onAttributesUpdate(tenantId, entityId, scope, attributes, notifyDevice)); addWsCallback(saveFuture, success -> onAttributesUpdate(tenantId, entityId, scope, attributes, notifyDevice));
} }
@Override @Override
public void saveAndNotifyInternal(TenantId tenantId, EntityId entityId, AttributeScope scope, List<AttributeKvEntry> attributes, boolean notifyDevice, FutureCallback<Void> callback) { public void saveAndNotifyInternal(TenantId tenantId, EntityId entityId, AttributeScope scope, List<AttributeKvEntry> attributes, boolean notifyDevice, FutureCallback<Void> callback) {
ListenableFuture<List<String>> saveFuture = attrService.save(tenantId, entityId, scope, attributes); ListenableFuture<List<Long>> saveFuture = attrService.save(tenantId, entityId, scope, attributes);
addVoidCallback(saveFuture, callback); addVoidCallback(saveFuture, callback);
addWsCallback(saveFuture, success -> onAttributesUpdate(tenantId, entityId, scope.name(), attributes, notifyDevice)); addWsCallback(saveFuture, success -> onAttributesUpdate(tenantId, entityId, scope.name(), attributes, notifyDevice));
} }
@ -280,7 +280,7 @@ public class DefaultTelemetrySubscriptionService extends AbstractSubscriptionSer
@Override @Override
public void saveLatestAndNotifyInternal(TenantId tenantId, EntityId entityId, List<TsKvEntry> ts, FutureCallback<Void> callback) { public void saveLatestAndNotifyInternal(TenantId tenantId, EntityId entityId, List<TsKvEntry> ts, FutureCallback<Void> callback) {
ListenableFuture<List<Void>> saveFuture = tsService.saveLatest(tenantId, entityId, ts); ListenableFuture<List<Long>> saveFuture = tsService.saveLatest(tenantId, entityId, ts);
addVoidCallback(saveFuture, callback); addVoidCallback(saveFuture, callback);
addWsCallback(saveFuture, success -> onTimeSeriesUpdate(tenantId, entityId, ts)); addWsCallback(saveFuture, success -> onTimeSeriesUpdate(tenantId, entityId, ts));
} }

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

@ -491,6 +491,10 @@ cache:
attributes: attributes:
# make sure that if cache.type is 'redis' and cache.attributes.enabled is 'true' if you change 'maxmemory-policy' Redis config property to 'allkeys-lru', 'allkeys-lfu' or 'allkeys-random' # make sure that if cache.type is 'redis' and cache.attributes.enabled is 'true' if you change 'maxmemory-policy' Redis config property to 'allkeys-lru', 'allkeys-lfu' or 'allkeys-random'
enabled: "${CACHE_ATTRIBUTES_ENABLED:true}" enabled: "${CACHE_ATTRIBUTES_ENABLED:true}"
ts_latest:
# Will enable cache-aside strategy for SQL timeseries latest DAO.
# make sure that if cache.type is 'redis' and cache.ts_latest.enabled is 'true' if you change 'maxmemory-policy' Redis config property to 'allkeys-lru', 'allkeys-lfu' or 'allkeys-random'
enabled: "${CACHE_TS_LATEST_ENABLED:true}"
specs: specs:
relations: relations:
timeToLiveInMinutes: "${CACHE_SPECS_RELATIONS_TTL:1440}" # Relations cache TTL timeToLiveInMinutes: "${CACHE_SPECS_RELATIONS_TTL:1440}" # Relations cache TTL
@ -547,6 +551,9 @@ cache:
attributes: attributes:
timeToLiveInMinutes: "${CACHE_SPECS_ATTRIBUTES_TTL:1440}" # Attributes cache TTL timeToLiveInMinutes: "${CACHE_SPECS_ATTRIBUTES_TTL:1440}" # Attributes cache TTL
maxSize: "${CACHE_SPECS_ATTRIBUTES_MAX_SIZE:100000}" # 0 means the cache is disabled maxSize: "${CACHE_SPECS_ATTRIBUTES_MAX_SIZE:100000}" # 0 means the cache is disabled
tsLatest:
timeToLiveInMinutes: "${CACHE_SPECS_TS_LATEST_TTL:1440}" # Timeseries latest cache TTL
maxSize: "${CACHE_SPECS_TS_LATEST_MAX_SIZE:100000}" # 0 means the cache is disabled
userSessionsInvalidation: userSessionsInvalidation:
# The value of this TTL is ignored and replaced by the JWT refresh token expiration time # The value of this TTL is ignored and replaced by the JWT refresh token expiration time
timeToLiveInMinutes: "0" timeToLiveInMinutes: "0"

18
application/src/test/java/org/thingsboard/server/controller/EntityRelationControllerTest.java

@ -103,7 +103,7 @@ public class EntityRelationControllerTest extends AbstractControllerTest {
Mockito.reset(tbClusterService, auditLogService); Mockito.reset(tbClusterService, auditLogService);
doPost("/api/relation", relation).andExpect(status().isOk()); relation = doPost("/api/v2/relation", relation, EntityRelation.class);
String url = String.format("/api/relation?fromId=%s&fromType=%s&relationType=%s&toId=%s&toType=%s", String url = String.format("/api/relation?fromId=%s&fromType=%s&relationType=%s&toId=%s&toType=%s",
mainDevice.getUuidId(), EntityType.DEVICE, mainDevice.getUuidId(), EntityType.DEVICE,
@ -315,7 +315,7 @@ public class EntityRelationControllerTest extends AbstractControllerTest {
Device device = buildSimpleDevice("Test device 1"); Device device = buildSimpleDevice("Test device 1");
EntityRelation relation = createFromRelation(mainDevice, device, "CONTAINS"); EntityRelation relation = createFromRelation(mainDevice, device, "CONTAINS");
doPost("/api/relation", relation).andExpect(status().isOk()); relation = doPost("/api/v2/relation", relation, EntityRelation.class);
String url = String.format("/api/relation?fromId=%s&fromType=%s&relationType=%s&toId=%s&toType=%s", String url = String.format("/api/relation?fromId=%s&fromType=%s&relationType=%s&toId=%s&toType=%s",
mainDevice.getUuidId(), EntityType.DEVICE, mainDevice.getUuidId(), EntityType.DEVICE,
@ -329,11 +329,15 @@ public class EntityRelationControllerTest extends AbstractControllerTest {
Mockito.reset(tbClusterService, auditLogService); Mockito.reset(tbClusterService, auditLogService);
doDelete(url).andExpect(status().isOk()); String deleteUrl = String.format("/api/v2/relation?fromId=%s&fromType=%s&relationType=%s&toId=%s&toType=%s",
mainDevice.getUuidId(), EntityType.DEVICE,
"CONTAINS", device.getUuidId(), EntityType.DEVICE
);
var deletedRelation = doDelete(deleteUrl, EntityRelation.class);
testNotifyEntityAllOneTimeRelation(foundRelation, testNotifyEntityAllOneTimeRelation(deletedRelation,
savedTenant.getId(), tenantAdmin.getCustomerId(), tenantAdmin.getId(), tenantAdmin.getEmail(), savedTenant.getId(), tenantAdmin.getCustomerId(), tenantAdmin.getId(), tenantAdmin.getEmail(),
ActionType.RELATION_DELETED, foundRelation); ActionType.RELATION_DELETED, deletedRelation);
doGet(url).andExpect(status().is4xxClientError()); doGet(url).andExpect(status().is4xxClientError());
} }
@ -523,7 +527,7 @@ public class EntityRelationControllerTest extends AbstractControllerTest {
@Test @Test
public void testCreateRelationFromTenantToDevice() throws Exception { public void testCreateRelationFromTenantToDevice() throws Exception {
EntityRelation relation = new EntityRelation(tenantAdmin.getTenantId(), mainDevice.getId(), "CONTAINS"); EntityRelation relation = new EntityRelation(tenantAdmin.getTenantId(), mainDevice.getId(), "CONTAINS");
doPost("/api/relation", relation).andExpect(status().isOk()); relation = doPost("/api/v2/relation", relation, EntityRelation.class);
String url = String.format("/api/relation?fromId=%s&fromType=%s&relationType=%s&toId=%s&toType=%s", String url = String.format("/api/relation?fromId=%s&fromType=%s&relationType=%s&toId=%s&toType=%s",
tenantAdmin.getTenantId(), EntityType.TENANT, tenantAdmin.getTenantId(), EntityType.TENANT,
@ -539,7 +543,7 @@ public class EntityRelationControllerTest extends AbstractControllerTest {
@Test @Test
public void testCreateRelationFromDeviceToTenant() throws Exception { public void testCreateRelationFromDeviceToTenant() throws Exception {
EntityRelation relation = new EntityRelation(mainDevice.getId(), tenantAdmin.getTenantId(), "CONTAINS"); EntityRelation relation = new EntityRelation(mainDevice.getId(), tenantAdmin.getTenantId(), "CONTAINS");
doPost("/api/relation", relation).andExpect(status().isOk()); relation = doPost("/api/v2/relation", relation, EntityRelation.class);
String url = String.format("/api/relation?fromId=%s&fromType=%s&relationType=%s&toId=%s&toType=%s", String url = String.format("/api/relation?fromId=%s&fromType=%s&relationType=%s&toId=%s&toType=%s",
mainDevice.getUuidId(), EntityType.DEVICE, mainDevice.getUuidId(), EntityType.DEVICE,

11
application/src/test/java/org/thingsboard/server/edge/RelationEdgeTest.java

@ -48,7 +48,7 @@ public class RelationEdgeTest extends AbstractEdgeTest {
relation.setTo(asset.getId()); relation.setTo(asset.getId());
relation.setTypeGroup(RelationTypeGroup.COMMON); relation.setTypeGroup(RelationTypeGroup.COMMON);
edgeImitator.expectMessageAmount(1); edgeImitator.expectMessageAmount(1);
doPost("/api/relation", relation); relation = doPost("/api/v2/relation", relation, EntityRelation.class);
Assert.assertTrue(edgeImitator.waitForMessages()); Assert.assertTrue(edgeImitator.waitForMessages());
AbstractMessage latestMessage = edgeImitator.getLatestMessage(); AbstractMessage latestMessage = edgeImitator.getLatestMessage();
Assert.assertTrue(latestMessage instanceof RelationUpdateMsg); Assert.assertTrue(latestMessage instanceof RelationUpdateMsg);
@ -60,21 +60,20 @@ public class RelationEdgeTest extends AbstractEdgeTest {
// delete relation // delete relation
edgeImitator.expectMessageAmount(1); edgeImitator.expectMessageAmount(1);
doDelete("/api/relation?" + var deletedRelation = doDelete("/api/v2/relation?" +
"fromId=" + relation.getFrom().getId().toString() + "fromId=" + relation.getFrom().getId().toString() +
"&fromType=" + relation.getFrom().getEntityType().name() + "&fromType=" + relation.getFrom().getEntityType().name() +
"&relationType=" + relation.getType() + "&relationType=" + relation.getType() +
"&relationTypeGroup=" + relation.getTypeGroup().name() + "&relationTypeGroup=" + relation.getTypeGroup().name() +
"&toId=" + relation.getTo().getId().toString() + "&toId=" + relation.getTo().getId().toString() +
"&toType=" + relation.getTo().getEntityType().name()) "&toType=" + relation.getTo().getEntityType().name(), EntityRelation.class);
.andExpect(status().isOk());
Assert.assertTrue(edgeImitator.waitForMessages()); Assert.assertTrue(edgeImitator.waitForMessages());
latestMessage = edgeImitator.getLatestMessage(); latestMessage = edgeImitator.getLatestMessage();
Assert.assertTrue(latestMessage instanceof RelationUpdateMsg); Assert.assertTrue(latestMessage instanceof RelationUpdateMsg);
relationUpdateMsg = (RelationUpdateMsg) latestMessage; relationUpdateMsg = (RelationUpdateMsg) latestMessage;
entityRelation = JacksonUtil.fromString(relationUpdateMsg.getEntity(), EntityRelation.class, true); entityRelation = JacksonUtil.fromString(relationUpdateMsg.getEntity(), EntityRelation.class, true);
Assert.assertNotNull(entityRelation); Assert.assertNotNull(entityRelation);
Assert.assertEquals(relation, entityRelation); Assert.assertEquals(deletedRelation, entityRelation);
Assert.assertEquals(UpdateMsgType.ENTITY_DELETED_RPC_MESSAGE, relationUpdateMsg.getMsgType()); Assert.assertEquals(UpdateMsgType.ENTITY_DELETED_RPC_MESSAGE, relationUpdateMsg.getMsgType());
} }
@ -119,7 +118,7 @@ public class RelationEdgeTest extends AbstractEdgeTest {
deviceToAssetRelation.setTypeGroup(RelationTypeGroup.COMMON); deviceToAssetRelation.setTypeGroup(RelationTypeGroup.COMMON);
edgeImitator.expectMessageAmount(1); edgeImitator.expectMessageAmount(1);
doPost("/api/relation", deviceToAssetRelation); deviceToAssetRelation = doPost("/api/v2/relation", deviceToAssetRelation, EntityRelation.class);
Assert.assertTrue(edgeImitator.waitForMessages()); Assert.assertTrue(edgeImitator.waitForMessages());
EntityRelation assetToTenantRelation = new EntityRelation(); EntityRelation assetToTenantRelation = new EntityRelation();

3
application/src/test/java/org/thingsboard/server/service/sync/vc/VersionControlTest.java

@ -937,8 +937,7 @@ public class VersionControlTest extends AbstractControllerTest {
relation.setType(EntityRelation.MANAGES_TYPE); relation.setType(EntityRelation.MANAGES_TYPE);
relation.setAdditionalInfo(JacksonUtil.newObjectNode().set("a", new TextNode("b"))); relation.setAdditionalInfo(JacksonUtil.newObjectNode().set("a", new TextNode("b")));
relation.setTypeGroup(RelationTypeGroup.COMMON); relation.setTypeGroup(RelationTypeGroup.COMMON);
doPost("/api/relation", relation).andExpect(status().isOk()); return doPost("/api/v2/relation", relation, EntityRelation.class);
return relation;
} }
protected void checkImportedRuleChainData(RuleChain initialRuleChain, RuleChainMetaData initialMetaData, RuleChain importedRuleChain, RuleChainMetaData importedMetaData) { protected void checkImportedRuleChainData(RuleChain initialRuleChain, RuleChainMetaData initialMetaData, RuleChain importedRuleChain, RuleChainMetaData importedMetaData) {

21
application/src/test/java/org/thingsboard/server/transport/mqtt/sparkplug/connection/AbstractMqttV5ClientSparkplugConnectionTest.java

@ -63,7 +63,9 @@ public abstract class AbstractMqttV5ClientSparkplugConnectionTest extends Abstra
return finalFuture.get().get().isPresent(); return finalFuture.get().get().isPresent();
}); });
TsKvEntry actualTsKvEntry = finalFuture.get().get().get(); TsKvEntry actualTsKvEntry = finalFuture.get().get().get();
Assert.assertEquals(expectedTsKvEntry, actualTsKvEntry); Assert.assertEquals(expectedTsKvEntry.getKey(), actualTsKvEntry.getKey());
Assert.assertEquals(expectedTsKvEntry.getValue(), actualTsKvEntry.getValue());
Assert.assertEquals(expectedTsKvEntry.getTs(), actualTsKvEntry.getTs());
} }
protected void processClientWithCorrectNodeAccessTokenWithoutNDEATH_Test() throws Exception { protected void processClientWithCorrectNodeAccessTokenWithoutNDEATH_Test() throws Exception {
@ -95,20 +97,27 @@ public abstract class AbstractMqttV5ClientSparkplugConnectionTest extends Abstra
List<Device> devices = connectClientWithCorrectAccessTokenWithNDEATHCreatedDevices(cntDevices, ts); List<Device> devices = connectClientWithCorrectAccessTokenWithNDEATHCreatedDevices(cntDevices, ts);
TsKvEntry tsKvEntry = new BasicTsKvEntry(ts, new StringDataEntry(messageName(STATE), ONLINE.name())); TsKvEntry tsKvEntry = new BasicTsKvEntry(ts, new StringDataEntry(messageName(STATE), ONLINE.name()));
AtomicReference<ListenableFuture<List<TsKvEntry>>> finalFuture = new AtomicReference<>();
await(alias + messageName(STATE) + ", device: " + savedGateway.getName()) await(alias + messageName(STATE) + ", device: " + savedGateway.getName())
.atMost(40, TimeUnit.SECONDS) .atMost(40, TimeUnit.SECONDS)
.until(() -> { .until(() -> {
finalFuture.set(tsService.findAllLatest(tenantId, savedGateway.getId())); var foundEntry = tsService.findAllLatest(tenantId, savedGateway.getId()).get().stream()
return finalFuture.get().get().contains(tsKvEntry); .filter(tsKv -> tsKv.getKey().equals(tsKvEntry.getKey()))
.filter(tsKv -> tsKv.getValue().equals(tsKvEntry.getValue()))
.filter(tsKv -> tsKv.getTs() == tsKvEntry.getTs())
.findFirst();
return foundEntry.isPresent();
}); });
for (Device device : devices) { for (Device device : devices) {
await(alias + messageName(STATE) + ", device: " + device.getName()) await(alias + messageName(STATE) + ", device: " + device.getName())
.atMost(40, TimeUnit.SECONDS) .atMost(40, TimeUnit.SECONDS)
.until(() -> { .until(() -> {
finalFuture.set(tsService.findAllLatest(tenantId, device.getId())); var foundEntry = tsService.findAllLatest(tenantId, device.getId()).get().stream()
return finalFuture.get().get().contains(tsKvEntry); .filter(tsKv -> tsKv.getKey().equals(tsKvEntry.getKey()))
.filter(tsKv -> tsKv.getValue().equals(tsKvEntry.getValue()))
.filter(tsKv -> tsKv.getTs() == tsKvEntry.getTs())
.findFirst();
return foundEntry.isPresent();
}); });
} }
} }

17
application/src/test/java/org/thingsboard/server/transport/mqtt/sparkplug/timeseries/AbstractMqttV5ClientSparkplugTelemetryTest.java

@ -78,7 +78,7 @@ public abstract class AbstractMqttV5ClientSparkplugTelemetryTest extends Abstrac
finalFuture.set(tsService.findAllLatest(tenantId, savedGateway.getId())); finalFuture.set(tsService.findAllLatest(tenantId, savedGateway.getId()));
return finalFuture.get().get().size() == (listTsKvEntry.size() + 1); return finalFuture.get().get().size() == (listTsKvEntry.size() + 1);
}); });
Assert.assertTrue("Actual tsKvEntrys is not containsAll Expected tsKvEntrys", finalFuture.get().get().containsAll(listTsKvEntry)); Assert.assertTrue("Actual tsKvEntries is not containsAll Expected tsKvEntries", containsIgnoreVersion(finalFuture.get().get(), listTsKvEntry));
} }
protected void processClientWithCorrectAccessTokenPushNodeMetricBuildArraysPrimitiveSimple() throws Exception { protected void processClientWithCorrectAccessTokenPushNodeMetricBuildArraysPrimitiveSimple() throws Exception {
@ -107,7 +107,20 @@ public abstract class AbstractMqttV5ClientSparkplugTelemetryTest extends Abstrac
finalFuture.set(tsService.findAllLatest(tenantId, savedGateway.getId())); finalFuture.set(tsService.findAllLatest(tenantId, savedGateway.getId()));
return finalFuture.get().get().size() == (listTsKvEntry.size() + 1); return finalFuture.get().get().size() == (listTsKvEntry.size() + 1);
}); });
Assert.assertTrue("Actual tsKvEntrys is not containsAll Expected tsKvEntrys", finalFuture.get().get().containsAll(listTsKvEntry)); Assert.assertTrue("Actual tsKvEntries is not containsAll Expected tsKvEntries", containsIgnoreVersion(finalFuture.get().get(), listTsKvEntry));
} }
private static boolean containsIgnoreVersion(List<TsKvEntry> expected, List<TsKvEntry> actual) {
for (TsKvEntry actualEntry : actual) {
var found = expected.stream()
.filter(tsKv -> tsKv.getKey().equals(actualEntry.getKey()))
.filter(tsKv -> tsKv.getValue().equals(actualEntry.getValue()))
.filter(tsKv -> tsKv.getTs() == actualEntry.getTs())
.findFirst();
if (found.isEmpty()) {
return false;
}
}
return true;
}
} }

57
build.sh

@ -0,0 +1,57 @@
#!/bin/bash
#
# Copyright © 2016-2024 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.
#
set -e # exit on any error
#PROJECTS="msa/tb-node,msa/web-ui,rule-engine-pe/rule-node-twilio-sms"
PROJECTS=""
if [ "$1" ]; then
PROJECTS="--projects $1"
fi
echo "Building and pushing [amd64,arm64] projects '$PROJECTS' ..."
echo "HELP: usage ./build.sh [projects]"
echo "HELP: example ./build.sh msa/web-ui,msa/web-report"
java -version
#echo "Cleaning ui-ngx/node_modules" && rm -rf ui-ngx/node_modules
MAVEN_OPTS="-Xmx1024m" NODE_OPTIONS="--max_old_space_size=4096" DOCKER_CLI_EXPERIMENTAL=enabled DOCKER_BUILDKIT=0 \
mvn -T2 license:format clean install -DskipTests \
$PROJECTS --also-make
# \
# -Dpush-docker-amd-arm-images
# -Ddockerfile.skip=false -Dpush-docker-image=true
# --offline
# --projects '!msa/web-report' --also-make
# push all
# mvn -T 1C license:format clean install -DskipTests -Ddockerfile.skip=false -Dpush-docker-image=true
## Build and push AMD and ARM docker images using docker buildx
## Reference to article how to setup docker miltiplatform build environment: https://medium.com/@artur.klauser/building-multi-architecture-docker-images-with-buildx-27d80f7e2408
## install docker-ce from docker repo https://docs.docker.com/engine/install/ubuntu/
# sudo apt install -y qemu-user-static binfmt-support
# export DOCKER_CLI_EXPERIMENTAL=enabled
# docker version
# docker run --rm --privileged multiarch/qemu-user-static --reset -p yes
# docker buildx create --name mybuilder
# docker buildx use mybuilder
# docker buildx inspect --bootstrap
# docker buildx ls
# mvn clean install -P push-docker-amd-arm-images

4
common/cache/src/main/java/org/thingsboard/server/cache/CaffeineTbCacheTransaction.java

@ -38,10 +38,10 @@ public class CaffeineTbCacheTransaction<K extends Serializable, V extends Serial
@Setter @Setter
private boolean failed; private boolean failed;
private final Map<Object, Object> pendingPuts = new LinkedHashMap<>(); private final Map<K, V> pendingPuts = new LinkedHashMap<>();
@Override @Override
public void putIfAbsent(K key, V value) { public void put(K key, V value) {
pendingPuts.put(key, value); pendingPuts.put(key, value);
} }

34
common/cache/src/main/java/org/thingsboard/server/cache/CaffeineTbTransactionalCache.java

@ -17,6 +17,7 @@ package org.thingsboard.server.cache;
import lombok.Getter; import lombok.Getter;
import lombok.RequiredArgsConstructor; import lombok.RequiredArgsConstructor;
import org.springframework.cache.Cache;
import org.springframework.cache.CacheManager; import org.springframework.cache.CacheManager;
import java.io.Serializable; import java.io.Serializable;
@ -26,6 +27,7 @@ import java.util.HashMap;
import java.util.HashSet; import java.util.HashSet;
import java.util.List; import java.util.List;
import java.util.Map; import java.util.Map;
import java.util.Optional;
import java.util.Set; import java.util.Set;
import java.util.UUID; import java.util.UUID;
import java.util.concurrent.locks.Lock; import java.util.concurrent.locks.Lock;
@ -34,17 +36,27 @@ import java.util.concurrent.locks.ReentrantLock;
@RequiredArgsConstructor @RequiredArgsConstructor
public abstract class CaffeineTbTransactionalCache<K extends Serializable, V extends Serializable> implements TbTransactionalCache<K, V> { public abstract class CaffeineTbTransactionalCache<K extends Serializable, V extends Serializable> implements TbTransactionalCache<K, V> {
private final CacheManager cacheManager;
@Getter @Getter
private final String cacheName; protected final String cacheName;
protected final Cache cache;
private final Lock lock = new ReentrantLock(); protected final Lock lock = new ReentrantLock();
private final Map<K, Set<UUID>> objectTransactions = new HashMap<>(); private final Map<K, Set<UUID>> objectTransactions = new HashMap<>();
private final Map<UUID, CaffeineTbCacheTransaction<K, V>> transactions = new HashMap<>(); private final Map<UUID, CaffeineTbCacheTransaction<K, V>> transactions = new HashMap<>();
public CaffeineTbTransactionalCache(CacheManager cacheManager, String cacheName) {
this.cacheName = cacheName;
this.cache = Optional.ofNullable(cacheManager.getCache(cacheName))
.orElseThrow(() -> new IllegalArgumentException("Cache '" + cacheName + "' is not configured"));
}
@Override @Override
public TbCacheValueWrapper<V> get(K key) { public TbCacheValueWrapper<V> get(K key) {
return SimpleTbCacheValueWrapper.wrap(cacheManager.getCache(cacheName).get(key)); return SimpleTbCacheValueWrapper.wrap(cache.get(key));
}
@Override
public TbCacheValueWrapper<V> get(K key, boolean transactionMode) {
return get(key);
} }
@Override @Override
@ -52,7 +64,7 @@ public abstract class CaffeineTbTransactionalCache<K extends Serializable, V ext
lock.lock(); lock.lock();
try { try {
failAllTransactionsByKey(key); failAllTransactionsByKey(key);
cacheManager.getCache(cacheName).put(key, value); cache.put(key, value);
} finally { } finally {
lock.unlock(); lock.unlock();
} }
@ -109,12 +121,12 @@ public abstract class CaffeineTbTransactionalCache<K extends Serializable, V ext
return newTransaction(keys); return newTransaction(keys);
} }
void doPutIfAbsent(Object key, Object value) { void doPutIfAbsent(K key, V value) {
cacheManager.getCache(cacheName).putIfAbsent(key, value); cache.putIfAbsent(key, value);
} }
void doEvict(K key) { void doEvict(K key) {
cacheManager.getCache(cacheName).evict(key); cache.evict(key);
} }
TbCacheTransaction<K, V> newTransaction(List<K> keys) { TbCacheTransaction<K, V> newTransaction(List<K> keys) {
@ -132,7 +144,7 @@ public abstract class CaffeineTbTransactionalCache<K extends Serializable, V ext
} }
} }
public boolean commit(UUID trId, Map<Object, Object> pendingPuts) { public boolean commit(UUID trId, Map<K, V> pendingPuts) {
lock.lock(); lock.lock();
try { try {
var tr = transactions.get(trId); var tr = transactions.get(trId);
@ -181,7 +193,7 @@ public abstract class CaffeineTbTransactionalCache<K extends Serializable, V ext
} }
} }
private void failAllTransactionsByKey(K key) { protected void failAllTransactionsByKey(K key) {
Set<UUID> transactionsIds = objectTransactions.get(key); Set<UUID> transactionsIds = objectTransactions.get(key);
if (transactionsIds != null) { if (transactionsIds != null) {
for (UUID otherTrId : transactionsIds) { for (UUID otherTrId : transactionsIds) {

5
common/cache/src/main/java/org/thingsboard/server/cache/RedisTbCacheTransaction.java

@ -18,7 +18,6 @@ package org.thingsboard.server.cache;
import lombok.RequiredArgsConstructor; import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j; import lombok.extern.slf4j.Slf4j;
import org.springframework.data.redis.connection.RedisConnection; import org.springframework.data.redis.connection.RedisConnection;
import org.springframework.data.redis.connection.RedisStringCommands;
import java.io.Serializable; import java.io.Serializable;
import java.util.Objects; import java.util.Objects;
@ -31,8 +30,8 @@ public class RedisTbCacheTransaction<K extends Serializable, V extends Serializa
private final RedisConnection connection; private final RedisConnection connection;
@Override @Override
public void putIfAbsent(K key, V value) { public void put(K key, V value) {
cache.put(connection, key, value, RedisStringCommands.SetOption.UPSERT); cache.put(key, value, connection, true);
} }
@Override @Override

50
common/cache/src/main/java/org/thingsboard/server/cache/RedisTbTransactionalCache.java

@ -45,7 +45,7 @@ import java.util.function.Supplier;
@Slf4j @Slf4j
public abstract class RedisTbTransactionalCache<K extends Serializable, V extends Serializable> implements TbTransactionalCache<K, V> { public abstract class RedisTbTransactionalCache<K extends Serializable, V extends Serializable> implements TbTransactionalCache<K, V> {
private static final byte[] BINARY_NULL_VALUE = RedisSerializer.java().serialize(NullValue.INSTANCE); static final byte[] BINARY_NULL_VALUE = RedisSerializer.java().serialize(NullValue.INSTANCE);
static final JedisPool MOCK_POOL = new JedisPool(); //non-null pool required for JedisConnection to trigger closing jedis connection static final JedisPool MOCK_POOL = new JedisPool(); //non-null pool required for JedisConnection to trigger closing jedis connection
@Autowired @Autowired
@ -53,12 +53,13 @@ public abstract class RedisTbTransactionalCache<K extends Serializable, V extend
@Getter @Getter
private final String cacheName; private final String cacheName;
@Getter
private final JedisConnectionFactory connectionFactory; private final JedisConnectionFactory connectionFactory;
private final RedisSerializer<String> keySerializer = StringRedisSerializer.UTF_8; private final RedisSerializer<String> keySerializer = StringRedisSerializer.UTF_8;
private final TbRedisSerializer<K, V> valueSerializer; private final TbRedisSerializer<K, V> valueSerializer;
private final Expiration evictExpiration; protected final Expiration evictExpiration;
private final Expiration cacheTtl; protected final Expiration cacheTtl;
private final boolean cacheEnabled; protected final boolean cacheEnabled;
public RedisTbTransactionalCache(String cacheName, public RedisTbTransactionalCache(String cacheName,
CacheSpecsMap cacheSpecsMap, CacheSpecsMap cacheSpecsMap,
@ -85,13 +86,18 @@ public abstract class RedisTbTransactionalCache<K extends Serializable, V extend
@Override @Override
public TbCacheValueWrapper<V> get(K key) { public TbCacheValueWrapper<V> get(K key) {
return get(key, false);
}
@Override
public TbCacheValueWrapper<V> get(K key, boolean transactionMode) {
if (!cacheEnabled) { if (!cacheEnabled) {
return null; return null;
} }
try (var connection = connectionFactory.getConnection()) { try (var connection = connectionFactory.getConnection()) {
byte[] rawKey = getRawKey(key); byte[] rawKey = getRawKey(key);
byte[] rawValue = connection.get(rawKey); byte[] rawValue = doGet(connection, rawKey, transactionMode);
if (rawValue == null) { if (rawValue == null || rawValue.length == 0) {
return null; return null;
} else if (Arrays.equals(rawValue, BINARY_NULL_VALUE)) { } else if (Arrays.equals(rawValue, BINARY_NULL_VALUE)) {
return SimpleTbCacheValueWrapper.empty(); return SimpleTbCacheValueWrapper.empty();
@ -107,16 +113,24 @@ public abstract class RedisTbTransactionalCache<K extends Serializable, V extend
} }
} }
protected byte[] doGet(RedisConnection connection, byte[] rawKey, boolean transactionMode) {
return connection.stringCommands().get(rawKey);
}
@Override @Override
public void put(K key, V value) { public void put(K key, V value) {
if (!cacheEnabled) { if (!cacheEnabled) {
return; return;
} }
try (var connection = connectionFactory.getConnection()) { try (var connection = connectionFactory.getConnection()) {
put(connection, key, value, RedisStringCommands.SetOption.UPSERT); put(key, value, connection, false);
} }
} }
public void put(K key, V value, RedisConnection connection, boolean transactionMode) {
put(connection, key, value, RedisStringCommands.SetOption.UPSERT);
}
@Override @Override
public void putIfAbsent(K key, V value) { public void putIfAbsent(K key, V value) {
if (!cacheEnabled) { if (!cacheEnabled) {
@ -133,7 +147,7 @@ public abstract class RedisTbTransactionalCache<K extends Serializable, V extend
return; return;
} }
try (var connection = connectionFactory.getConnection()) { try (var connection = connectionFactory.getConnection()) {
connection.del(getRawKey(key)); connection.keyCommands().del(getRawKey(key));
} }
} }
@ -147,7 +161,7 @@ public abstract class RedisTbTransactionalCache<K extends Serializable, V extend
return; return;
} }
try (var connection = connectionFactory.getConnection()) { try (var connection = connectionFactory.getConnection()) {
connection.del(keys.stream().map(this::getRawKey).toArray(byte[][]::new)); connection.keyCommands().del(keys.stream().map(this::getRawKey).toArray(byte[][]::new));
} }
} }
@ -158,10 +172,10 @@ public abstract class RedisTbTransactionalCache<K extends Serializable, V extend
} }
try (var connection = connectionFactory.getConnection()) { try (var connection = connectionFactory.getConnection()) {
var rawKey = getRawKey(key); var rawKey = getRawKey(key);
var records = connection.del(rawKey); var records = connection.keyCommands().del(rawKey);
if (records == null || records == 0) { if (records == null || records == 0) {
//We need to put the value in case of Redis, because evict will NOT cancel concurrent transaction used to "get" the missing value from cache. //We need to put the value in case of Redis, because evict will NOT cancel concurrent transaction used to "get" the missing value from cache.
connection.set(rawKey, getRawValue(value), evictExpiration, RedisStringCommands.SetOption.UPSERT); connection.stringCommands().set(rawKey, getRawValue(value), evictExpiration, RedisStringCommands.SetOption.UPSERT);
} }
} }
} }
@ -187,7 +201,7 @@ public abstract class RedisTbTransactionalCache<K extends Serializable, V extend
return TbTransactionalCache.super.getAndPutInTransaction(key, dbCall, cacheValueToResult, dbValueToCacheValue, cacheNullValue); return TbTransactionalCache.super.getAndPutInTransaction(key, dbCall, cacheValueToResult, dbValueToCacheValue, cacheNullValue);
} }
private RedisConnection getConnection(byte[] rawKey) { protected RedisConnection getConnection(byte[] rawKey) {
if (!connectionFactory.isRedisClusterAware()) { if (!connectionFactory.isRedisClusterAware()) {
return connectionFactory.getConnection(); return connectionFactory.getConnection();
} }
@ -202,7 +216,7 @@ public abstract class RedisTbTransactionalCache<K extends Serializable, V extend
return jedisConnection; return jedisConnection;
} }
private RedisConnection watch(byte[][] rawKeysList) { protected RedisConnection watch(byte[][] rawKeysList) {
RedisConnection connection = getConnection(rawKeysList[0]); RedisConnection connection = getConnection(rawKeysList[0]);
try { try {
connection.watch(rawKeysList); connection.watch(rawKeysList);
@ -214,7 +228,7 @@ public abstract class RedisTbTransactionalCache<K extends Serializable, V extend
return connection; return connection;
} }
private byte[] getRawKey(K key) { protected byte[] getRawKey(K key) {
String keyString = cacheName + key.toString(); String keyString = cacheName + key.toString();
byte[] rawKey; byte[] rawKey;
try { try {
@ -230,7 +244,7 @@ public abstract class RedisTbTransactionalCache<K extends Serializable, V extend
return rawKey; return rawKey;
} }
private byte[] getRawValue(V value) { protected byte[] getRawValue(V value) {
if (value == null) { if (value == null) {
return BINARY_NULL_VALUE; return BINARY_NULL_VALUE;
} else { } else {
@ -252,8 +266,12 @@ public abstract class RedisTbTransactionalCache<K extends Serializable, V extend
return; return;
} }
byte[] rawKey = getRawKey(key); byte[] rawKey = getRawKey(key);
put(connection, rawKey, value, setOption);
}
public void put(RedisConnection connection, byte[] rawKey, V value, RedisStringCommands.SetOption setOption) {
byte[] rawValue = getRawValue(value); byte[] rawValue = getRawValue(value);
connection.set(rawKey, rawValue, cacheTtl, setOption); connection.stringCommands().set(rawKey, rawValue, this.cacheTtl, setOption);
} }
} }

2
common/cache/src/main/java/org/thingsboard/server/cache/TbCacheTransaction.java

@ -17,7 +17,7 @@ package org.thingsboard.server.cache;
public interface TbCacheTransaction<K, V> { public interface TbCacheTransaction<K, V> {
void putIfAbsent(K key, V value); void put(K key, V value);
boolean commit(); boolean commit();

35
common/cache/src/main/java/org/thingsboard/server/cache/TbJavaRedisSerializer.java

@ -0,0 +1,35 @@
/**
* Copyright © 2016-2024 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.cache;
import org.springframework.data.redis.serializer.RedisSerializer;
import org.springframework.data.redis.serializer.SerializationException;
public class TbJavaRedisSerializer<K, V> implements TbRedisSerializer<K, V> {
final RedisSerializer<Object> serializer = RedisSerializer.java();
@Override
public byte[] serialize(V value) throws SerializationException {
return serializer.serialize(value);
}
@Override
public V deserialize(K key, byte[] bytes) throws SerializationException {
return (V) serializer.deserialize(bytes);
}
}

6
common/cache/src/main/java/org/thingsboard/server/cache/TbTransactionalCache.java

@ -27,6 +27,8 @@ public interface TbTransactionalCache<K extends Serializable, V extends Serializ
TbCacheValueWrapper<V> get(K key); TbCacheValueWrapper<V> get(K key);
TbCacheValueWrapper<V> get(K key, boolean transactionMode);
void put(K key, V value); void put(K key, V value);
void putIfAbsent(K key, V value); void putIfAbsent(K key, V value);
@ -64,7 +66,7 @@ public interface TbTransactionalCache<K extends Serializable, V extends Serializ
} }
default <R> R getAndPutInTransaction(K key, Supplier<R> dbCall, Function<V, R> cacheValueToResult, Function<R, V> dbValueToCacheValue, boolean cacheNullValue) { default <R> R getAndPutInTransaction(K key, Supplier<R> dbCall, Function<V, R> cacheValueToResult, Function<R, V> dbValueToCacheValue, boolean cacheNullValue) {
TbCacheValueWrapper<V> cacheValueWrapper = get(key); TbCacheValueWrapper<V> cacheValueWrapper = get(key, true);
if (cacheValueWrapper != null) { if (cacheValueWrapper != null) {
V cacheValue = cacheValueWrapper.get(); V cacheValue = cacheValueWrapper.get();
return cacheValue != null ? cacheValueToResult.apply(cacheValue) : null; return cacheValue != null ? cacheValueToResult.apply(cacheValue) : null;
@ -73,7 +75,7 @@ public interface TbTransactionalCache<K extends Serializable, V extends Serializ
try { try {
R dbValue = dbCall.get(); R dbValue = dbCall.get();
if (dbValue != null || cacheNullValue) { if (dbValue != null || cacheNullValue) {
cacheTransaction.putIfAbsent(key, dbValueToCacheValue.apply(dbValue)); cacheTransaction.put(key, dbValueToCacheValue.apply(dbValue));
cacheTransaction.commit(); cacheTransaction.commit();
return dbValue; return dbValue;
} else { } else {

95
common/cache/src/main/java/org/thingsboard/server/cache/VersionedCaffeineTbCache.java

@ -0,0 +1,95 @@
/**
* Copyright © 2016-2024 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.cache;
import org.springframework.cache.Cache;
import org.springframework.cache.CacheManager;
import org.thingsboard.server.common.data.HasVersion;
import org.thingsboard.server.common.data.util.TbPair;
import java.io.Serializable;
public abstract class VersionedCaffeineTbCache<K extends Serializable, V extends Serializable & HasVersion> extends CaffeineTbTransactionalCache<K, V> implements VersionedTbCache<K, V> {
public VersionedCaffeineTbCache(CacheManager cacheManager, String cacheName) {
super(cacheManager, cacheName);
}
@Override
public TbCacheValueWrapper<V> get(K key) {
TbPair<Long, V> versionValuePair = doGet(key);
if (versionValuePair != null) {
return SimpleTbCacheValueWrapper.wrap(versionValuePair.getSecond());
}
return null;
}
@Override
public void put(K key, V value) {
Long version = value != null ? value.getVersion() : 0;
doPut(key, value, version);
}
private void doPut(K key, V value, Long version) {
if (version == null) {
return;
}
lock.lock();
try {
TbPair<Long, V> versionValuePair = doGet(key);
if (versionValuePair == null || version > versionValuePair.getFirst()) {
failAllTransactionsByKey(key);
cache.put(key, wrapValue(value, version));
}
} finally {
lock.unlock();
}
}
private TbPair<Long, V> doGet(K key) {
Cache.ValueWrapper source = cache.get(key);
return source == null ? null : (TbPair<Long, V>) source.get();
}
@Override
public void evict(K key) {
lock.lock();
try {
failAllTransactionsByKey(key);
cache.evict(key);
} finally {
lock.unlock();
}
}
@Override
public void evict(K key, Long version) {
if (version == null) {
return;
}
doPut(key, null, version);
}
@Override
void doPutIfAbsent(K key, V value) {
cache.putIfAbsent(key, wrapValue(value, value != null ? value.getVersion() : 0));
}
private TbPair<Long, V> wrapValue(V value, Long version) {
return TbPair.of(version, value);
}
}

181
common/cache/src/main/java/org/thingsboard/server/cache/VersionedRedisTbCache.java

@ -0,0 +1,181 @@
/**
* Copyright © 2016-2024 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.cache;
import jakarta.annotation.PostConstruct;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.NotImplementedException;
import org.springframework.dao.InvalidDataAccessApiUsageException;
import org.springframework.data.redis.connection.RedisConnection;
import org.springframework.data.redis.connection.RedisConnectionFactory;
import org.springframework.data.redis.connection.ReturnType;
import org.springframework.data.redis.core.types.Expiration;
import org.springframework.data.redis.serializer.StringRedisSerializer;
import org.thingsboard.server.common.data.HasVersion;
import java.io.Serializable;
import java.util.Arrays;
@Slf4j
public abstract class VersionedRedisTbCache<K extends Serializable, V extends Serializable & HasVersion> extends RedisTbTransactionalCache<K, V> implements VersionedTbCache<K, V> {
private static final int VERSION_SIZE = 8;
private static final int VALUE_END_OFFSET = -1;
static final byte[] SET_VERSIONED_VALUE_LUA_SCRIPT = StringRedisSerializer.UTF_8.serialize("""
local key = KEYS[1]
local newValue = ARGV[1]
local newVersion = tonumber(ARGV[2])
local expiration = tonumber(ARGV[3])
local function setNewValue()
local newValueWithVersion = struct.pack(">I8", newVersion) .. newValue
redis.call('SET', key, newValueWithVersion, 'EX', expiration)
end
local function bytes_to_number(bytes)
local n = 0
for i = 1, 8 do
n = n * 256 + string.byte(bytes, i)
end
return n
end
-- Get the current version (first 8 bytes) of the current value
local currentVersionBytes = redis.call('GETRANGE', key, 0, 7)
if currentVersionBytes and #currentVersionBytes == 8 then
local currentVersion = bytes_to_number(currentVersionBytes)
if newVersion > currentVersion then
setNewValue()
end
else
-- If the current value is absent or the current version is not found, set the new value
setNewValue()
end
""");
static final byte[] SET_VERSIONED_VALUE_SHA = StringRedisSerializer.UTF_8.serialize("80e56cbbbb4bd9cb150d6537f1e7d8df4fddb252");
public VersionedRedisTbCache(String cacheName, CacheSpecsMap cacheSpecsMap, RedisConnectionFactory connectionFactory, TBRedisCacheConfiguration configuration, TbRedisSerializer<K, V> valueSerializer) {
super(cacheName, cacheSpecsMap, connectionFactory, configuration, valueSerializer);
}
@PostConstruct
public void init() {
try (var connection = getConnection(SET_VERSIONED_VALUE_SHA)) {
log.debug("Loading LUA with expected SHA[{}], connection [{}]", new String(SET_VERSIONED_VALUE_SHA), connection.getNativeConnection());
String sha = connection.scriptingCommands().scriptLoad(SET_VERSIONED_VALUE_LUA_SCRIPT);
if (!Arrays.equals(SET_VERSIONED_VALUE_SHA, StringRedisSerializer.UTF_8.serialize(sha))) {
log.error("SHA for SET_VERSIONED_VALUE_LUA_SCRIPT wrong! Expected [{}], but actual [{}], connection [{}]", new String(SET_VERSIONED_VALUE_SHA), sha, connection.getNativeConnection());
}
} catch (Throwable t) {
log.error("Error on Redis versioned cache init", t);
}
}
@Override
protected byte[] doGet(RedisConnection connection, byte[] rawKey, boolean transactionMode) {
if (transactionMode) {
return super.doGet(connection, rawKey, true);
}
return connection.stringCommands().getRange(rawKey, VERSION_SIZE, VALUE_END_OFFSET);
}
@Override
public void put(K key, V value) {
Long version = getVersion(value);
if (version == null) {
return;
}
doPut(key, value, version, cacheTtl);
}
@Override
public void put(K key, V value, RedisConnection connection, boolean transactionMode) {
if (transactionMode) {
super.put(key, value, connection, true); // because scripting commands are not supported in transaction mode
return;
}
Long version = getVersion(value);
if (version == null) {
return;
}
byte[] rawKey = getRawKey(key);
doPut(rawKey, value, version, cacheTtl, connection);
}
private void doPut(K key, V value, Long version, Expiration expiration) {
if (!cacheEnabled) {
return;
}
log.trace("put [{}][{}][{}]", key, value, version);
final byte[] rawKey = getRawKey(key);
try (var connection = getConnection(rawKey)) {
doPut(rawKey, value, version, expiration, connection);
}
}
private void doPut(byte[] rawKey, V value, Long version, Expiration expiration, RedisConnection connection) {
byte[] rawValue = getRawValue(value);
byte[] rawVersion = StringRedisSerializer.UTF_8.serialize(String.valueOf(version));
byte[] rawExpiration = StringRedisSerializer.UTF_8.serialize(String.valueOf(expiration.getExpirationTimeInSeconds()));
try {
connection.scriptingCommands().evalSha(SET_VERSIONED_VALUE_SHA, ReturnType.VALUE, 1, rawKey, rawValue, rawVersion, rawExpiration);
} catch (InvalidDataAccessApiUsageException e) {
log.debug("loading LUA [{}]", connection.getNativeConnection());
String sha = connection.scriptingCommands().scriptLoad(SET_VERSIONED_VALUE_LUA_SCRIPT);
if (!Arrays.equals(SET_VERSIONED_VALUE_SHA, StringRedisSerializer.UTF_8.serialize(sha))) {
log.error("SHA for SET_VERSIONED_VALUE_LUA_SCRIPT wrong! Expected [{}], but actual [{}]", new String(SET_VERSIONED_VALUE_SHA), sha);
}
try {
connection.scriptingCommands().evalSha(SET_VERSIONED_VALUE_SHA, ReturnType.VALUE, 1, rawKey, rawValue, rawVersion, rawExpiration);
} catch (InvalidDataAccessApiUsageException ignored) {
log.debug("Slowly executing eval instead of fast evalsha");
connection.scriptingCommands().eval(SET_VERSIONED_VALUE_LUA_SCRIPT, ReturnType.VALUE, 1, rawKey, rawValue, rawVersion, rawExpiration);
}
}
}
@Override
public void evict(K key, Long version) {
log.trace("evict [{}][{}]", key, version);
if (version != null) {
doPut(key, null, version, evictExpiration);
}
}
@Override
public void putIfAbsent(K key, V value) {
throw new NotImplementedException("putIfAbsent is not supported by versioned cache");
}
@Override
public void evictOrPut(K key, V value) {
throw new NotImplementedException("evictOrPut is not supported by versioned cache");
}
private Long getVersion(V value) {
if (value == null) {
return 0L;
} else if (value.getVersion() != null) {
return value.getVersion();
} else {
return null;
}
}
}

53
common/cache/src/main/java/org/thingsboard/server/cache/VersionedTbCache.java

@ -0,0 +1,53 @@
/**
* Copyright © 2016-2024 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.cache;
import org.thingsboard.server.common.data.HasVersion;
import java.io.Serializable;
import java.util.Collection;
import java.util.Optional;
import java.util.function.Supplier;
public interface VersionedTbCache<K extends Serializable, V extends Serializable & HasVersion> extends TbTransactionalCache<K, V> {
TbCacheValueWrapper<V> get(K key);
default V get(K key, Supplier<V> supplier) {
return get(key, supplier, true);
}
default V get(K key, Supplier<V> supplier, boolean putToCache) {
return Optional.ofNullable(get(key))
.map(TbCacheValueWrapper::get)
.orElseGet(() -> {
V value = supplier.get();
if (putToCache) {
put(key, value);
}
return value;
});
}
void put(K key, V value);
void evict(K key);
void evict(Collection<K> keys);
void evict(K key, Long version);
}

45
common/cache/src/test/java/org/thingsboard/server/cache/TsLatestRedisCacheTest.java

@ -0,0 +1,45 @@
/**
* Copyright © 2016-2024 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.cache;
import lombok.SneakyThrows;
import org.junit.jupiter.api.Test;
import java.security.MessageDigest;
import static org.assertj.core.api.Assertions.assertThat;
class VersionedRedisTbCacheTest {
@Test
void testUpsertTsLatestLUAScriptHash() {
assertThat(getSHA1(VersionedRedisTbCache.SET_VERSIONED_VALUE_LUA_SCRIPT)).isEqualTo(new String(VersionedRedisTbCache.SET_VERSIONED_VALUE_SHA));
}
@SneakyThrows
String getSHA1(byte[] script) {
MessageDigest md = MessageDigest.getInstance("SHA-1");
byte[] hash = md.digest(script);
StringBuilder sb = new StringBuilder();
for (byte b : hash) {
sb.append(String.format("%02x", b));
}
return sb.toString();
}
}

22
common/dao-api/src/main/java/org/thingsboard/server/dao/attributes/AttributesService.java

@ -17,7 +17,6 @@ package org.thingsboard.server.dao.attributes;
import com.google.common.util.concurrent.ListenableFuture; import com.google.common.util.concurrent.ListenableFuture;
import org.thingsboard.server.common.data.AttributeScope; import org.thingsboard.server.common.data.AttributeScope;
import org.thingsboard.server.common.data.EntityType;
import org.thingsboard.server.common.data.id.DeviceProfileId; import org.thingsboard.server.common.data.id.DeviceProfileId;
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;
@ -32,30 +31,18 @@ import java.util.Optional;
*/ */
public interface AttributesService { public interface AttributesService {
@Deprecated(since = "3.7.0")
ListenableFuture<Optional<AttributeKvEntry>> find(TenantId tenantId, EntityId entityId, String scope, String attributeKey);
ListenableFuture<Optional<AttributeKvEntry>> find(TenantId tenantId, EntityId entityId, AttributeScope scope, String attributeKey); ListenableFuture<Optional<AttributeKvEntry>> find(TenantId tenantId, EntityId entityId, AttributeScope scope, String attributeKey);
@Deprecated(since = "3.7.0")
ListenableFuture<List<AttributeKvEntry>> find(TenantId tenantId, EntityId entityId, String scope, Collection<String> attributeKeys);
ListenableFuture<List<AttributeKvEntry>> find(TenantId tenantId, EntityId entityId, AttributeScope scope, Collection<String> attributeKeys); ListenableFuture<List<AttributeKvEntry>> find(TenantId tenantId, EntityId entityId, AttributeScope scope, Collection<String> attributeKeys);
@Deprecated(since = "3.7.0")
ListenableFuture<List<AttributeKvEntry>> findAll(TenantId tenantId, EntityId entityId, String scope);
ListenableFuture<List<AttributeKvEntry>> findAll(TenantId tenantId, EntityId entityId, AttributeScope scope); ListenableFuture<List<AttributeKvEntry>> findAll(TenantId tenantId, EntityId entityId, AttributeScope scope);
@Deprecated(since = "3.7.0") @Deprecated(since = "3.7.0")
ListenableFuture<List<String>> save(TenantId tenantId, EntityId entityId, String scope, List<AttributeKvEntry> attributes); ListenableFuture<List<Long>> save(TenantId tenantId, EntityId entityId, String scope, List<AttributeKvEntry> attributes);
ListenableFuture<List<String>> save(TenantId tenantId, EntityId entityId, AttributeScope scope, List<AttributeKvEntry> attributes);
@Deprecated(since = "3.7.0") ListenableFuture<List<Long>> save(TenantId tenantId, EntityId entityId, AttributeScope scope, List<AttributeKvEntry> attributes);
ListenableFuture<String> save(TenantId tenantId, EntityId entityId, String scope, AttributeKvEntry attribute);
ListenableFuture<String> save(TenantId tenantId, EntityId entityId, AttributeScope scope, AttributeKvEntry attribute); ListenableFuture<Long> save(TenantId tenantId, EntityId entityId, AttributeScope scope, AttributeKvEntry attribute);
@Deprecated(since = "3.7.0") @Deprecated(since = "3.7.0")
ListenableFuture<List<String>> removeAll(TenantId tenantId, EntityId entityId, String scope, List<String> attributeKeys); ListenableFuture<List<String>> removeAll(TenantId tenantId, EntityId entityId, String scope, List<String> attributeKeys);
@ -64,9 +51,6 @@ public interface AttributesService {
List<String> findAllKeysByDeviceProfileId(TenantId tenantId, DeviceProfileId deviceProfileId); List<String> findAllKeysByDeviceProfileId(TenantId tenantId, DeviceProfileId deviceProfileId);
@Deprecated(since = "3.7.0")
List<String> findAllKeysByEntityIds(TenantId tenantId, EntityType entityType, List<EntityId> entityIds);
List<String> findAllKeysByEntityIds(TenantId tenantId, List<EntityId> entityIds); List<String> findAllKeysByEntityIds(TenantId tenantId, List<EntityId> entityIds);
List<String> findAllKeysByEntityIds(TenantId tenantId, List<EntityId> entityIds, String scope); List<String> findAllKeysByEntityIds(TenantId tenantId, List<EntityId> entityIds, String scope);

4
common/dao-api/src/main/java/org/thingsboard/server/dao/relation/RelationService.java

@ -37,7 +37,7 @@ public interface RelationService {
EntityRelation getRelation(TenantId tenantId, EntityId from, EntityId to, String relationType, RelationTypeGroup typeGroup); EntityRelation getRelation(TenantId tenantId, EntityId from, EntityId to, String relationType, RelationTypeGroup typeGroup);
boolean saveRelation(TenantId tenantId, EntityRelation relation); EntityRelation saveRelation(TenantId tenantId, EntityRelation relation);
void saveRelations(TenantId tenantId, List<EntityRelation> relations); void saveRelations(TenantId tenantId, List<EntityRelation> relations);
@ -47,7 +47,7 @@ public interface RelationService {
ListenableFuture<Boolean> deleteRelationAsync(TenantId tenantId, EntityRelation relation); ListenableFuture<Boolean> deleteRelationAsync(TenantId tenantId, EntityRelation relation);
boolean deleteRelation(TenantId tenantId, EntityId from, EntityId to, String relationType, RelationTypeGroup typeGroup); EntityRelation deleteRelation(TenantId tenantId, EntityId from, EntityId to, String relationType, RelationTypeGroup typeGroup);
ListenableFuture<Boolean> deleteRelationAsync(TenantId tenantId, EntityId from, EntityId to, String relationType, RelationTypeGroup typeGroup); ListenableFuture<Boolean> deleteRelationAsync(TenantId tenantId, EntityId from, EntityId to, String relationType, RelationTypeGroup typeGroup);

2
common/dao-api/src/main/java/org/thingsboard/server/dao/timeseries/TimeseriesService.java

@ -50,7 +50,7 @@ public interface TimeseriesService {
ListenableFuture<Integer> saveWithoutLatest(TenantId tenantId, EntityId entityId, List<TsKvEntry> tsKvEntry, long ttl); ListenableFuture<Integer> saveWithoutLatest(TenantId tenantId, EntityId entityId, List<TsKvEntry> tsKvEntry, long ttl);
ListenableFuture<List<Void>> saveLatest(TenantId tenantId, EntityId entityId, List<TsKvEntry> tsKvEntry); ListenableFuture<List<Long>> saveLatest(TenantId tenantId, EntityId entityId, List<TsKvEntry> tsKvEntry);
ListenableFuture<List<TsKvLatestRemovingResult>> remove(TenantId tenantId, EntityId entityId, List<DeleteTsKvQuery> queries); ListenableFuture<List<TsKvLatestRemovingResult>> remove(TenantId tenantId, EntityId entityId, List<DeleteTsKvQuery> queries);

26
common/dao-api/src/main/java/org/thingsboard/server/dao/util/SqlTsLatestAnyDaoCachedRedis.java

@ -0,0 +1,26 @@
/**
* Copyright © 2016-2024 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.util;
import org.springframework.boot.autoconfigure.condition.ConditionalOnExpression;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
@Retention(RetentionPolicy.RUNTIME)
@ConditionalOnExpression("('${database.ts_latest.type}'=='sql' || '${database.ts_latest.type}'=='timescale') && '${cache.ts_latest.enabled:false}'=='true' && '${cache.type:caffeine}'=='redis' ")
public @interface SqlTsLatestAnyDaoCachedRedis {
}

1
common/data/src/main/java/org/thingsboard/server/common/data/CacheConstants.java

@ -36,6 +36,7 @@ public class CacheConstants {
public static final String ASSET_PROFILE_CACHE = "assetProfiles"; public static final String ASSET_PROFILE_CACHE = "assetProfiles";
public static final String ATTRIBUTES_CACHE = "attributes"; public static final String ATTRIBUTES_CACHE = "attributes";
public static final String TS_LATEST_CACHE = "tsLatest";
public static final String USERS_SESSION_INVALIDATION_CACHE = "userSessionsInvalidation"; public static final String USERS_SESSION_INVALIDATION_CACHE = "userSessionsInvalidation";
public static final String OTA_PACKAGE_CACHE = "otaPackages"; public static final String OTA_PACKAGE_CACHE = "otaPackages";
public static final String OTA_PACKAGE_DATA_CACHE = "otaPackagesData"; public static final String OTA_PACKAGE_DATA_CACHE = "otaPackagesData";

15
dao/src/main/java/org/thingsboard/server/dao/sql/attributes/SqlAttributesInsertRepository.java → common/data/src/main/java/org/thingsboard/server/common/data/HasVersion.java

@ -13,15 +13,8 @@
* See the License for the specific language governing permissions and * See the License for the specific language governing permissions and
* limitations under the License. * limitations under the License.
*/ */
package org.thingsboard.server.dao.sql.attributes; package org.thingsboard.server.common.data;
import org.springframework.stereotype.Repository; public interface HasVersion {
import org.springframework.transaction.annotation.Transactional; Long getVersion();
import org.thingsboard.server.dao.util.SqlDao; }
@Repository
@Transactional
@SqlDao
public class SqlAttributesInsertRepository extends AttributeKvInsertRepository {
}

4
common/data/src/main/java/org/thingsboard/server/common/data/kv/AttributeKvEntry.java

@ -15,10 +15,12 @@
*/ */
package org.thingsboard.server.common.data.kv; package org.thingsboard.server.common.data.kv;
import org.thingsboard.server.common.data.HasVersion;
/** /**
* @author Andrew Shvayka * @author Andrew Shvayka
*/ */
public interface AttributeKvEntry extends KvEntry { public interface AttributeKvEntry extends KvEntry, HasVersion {
long getLastUpdateTs(); long getLastUpdateTs();

43
common/data/src/main/java/org/thingsboard/server/common/data/kv/BaseAttributeKvEntry.java

@ -16,12 +16,13 @@
package org.thingsboard.server.common.data.kv; package org.thingsboard.server.common.data.kv;
import jakarta.validation.Valid; import jakarta.validation.Valid;
import lombok.Data;
import java.util.Optional; import java.util.Optional;
/** /**
* @author Andrew Shvayka * @author Andrew Shvayka
*/ */
@Data
public class BaseAttributeKvEntry implements AttributeKvEntry { public class BaseAttributeKvEntry implements AttributeKvEntry {
private static final long serialVersionUID = -6460767583563159407L; private static final long serialVersionUID = -6460767583563159407L;
@ -30,18 +31,22 @@ public class BaseAttributeKvEntry implements AttributeKvEntry {
@Valid @Valid
private final KvEntry kv; private final KvEntry kv;
private final Long version;
public BaseAttributeKvEntry(KvEntry kv, long lastUpdateTs) { public BaseAttributeKvEntry(KvEntry kv, long lastUpdateTs) {
this.kv = kv; this.kv = kv;
this.lastUpdateTs = lastUpdateTs; this.lastUpdateTs = lastUpdateTs;
this.version = null;
} }
public BaseAttributeKvEntry(long lastUpdateTs, KvEntry kv) { public BaseAttributeKvEntry(KvEntry kv, long lastUpdateTs, Long version) {
this(kv, lastUpdateTs); this.kv = kv;
this.lastUpdateTs = lastUpdateTs;
this.version = version;
} }
@Override public BaseAttributeKvEntry(long lastUpdateTs, KvEntry kv) {
public long getLastUpdateTs() { this(kv, lastUpdateTs);
return lastUpdateTs;
} }
@Override @Override
@ -89,30 +94,4 @@ public class BaseAttributeKvEntry implements AttributeKvEntry {
return kv.getValue(); return kv.getValue();
} }
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
BaseAttributeKvEntry that = (BaseAttributeKvEntry) o;
if (lastUpdateTs != that.lastUpdateTs) return false;
return kv.equals(that.kv);
}
@Override
public int hashCode() {
int result = (int) (lastUpdateTs ^ (lastUpdateTs >>> 32));
result = 31 * result + kv.hashCode();
return result;
}
@Override
public String toString() {
return "BaseAttributeKvEntry{" +
"lastUpdateTs=" + lastUpdateTs +
", kv=" + kv +
'}';
}
} }

38
common/data/src/main/java/org/thingsboard/server/common/data/kv/BasicTsKvEntry.java

@ -16,19 +16,30 @@
package org.thingsboard.server.common.data.kv; package org.thingsboard.server.common.data.kv;
import jakarta.validation.Valid; import jakarta.validation.Valid;
import lombok.Data;
import java.util.Objects; import java.util.Objects;
import java.util.Optional; import java.util.Optional;
@Data
public class BasicTsKvEntry implements TsKvEntry { public class BasicTsKvEntry implements TsKvEntry {
private static final int MAX_CHARS_PER_DATA_POINT = 512; private static final int MAX_CHARS_PER_DATA_POINT = 512;
protected final long ts; protected final long ts;
@Valid @Valid
private final KvEntry kv; private final KvEntry kv;
private final Long version;
public BasicTsKvEntry(long ts, KvEntry kv) { public BasicTsKvEntry(long ts, KvEntry kv) {
this.ts = ts; this.ts = ts;
this.kv = kv; this.kv = kv;
this.version = null;
}
public BasicTsKvEntry(long ts, KvEntry kv, Long version) {
this.ts = ts;
this.kv = kv;
this.version = version;
} }
@Override @Override
@ -71,33 +82,6 @@ public class BasicTsKvEntry implements TsKvEntry {
return kv.getValue(); return kv.getValue();
} }
@Override
public long getTs() {
return ts;
}
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (!(o instanceof BasicTsKvEntry)) return false;
BasicTsKvEntry that = (BasicTsKvEntry) o;
return getTs() == that.getTs() &&
Objects.equals(kv, that.kv);
}
@Override
public int hashCode() {
return Objects.hash(getTs(), kv);
}
@Override
public String toString() {
return "BasicTsKvEntry{" +
"ts=" + ts +
", kv=" + kv +
'}';
}
@Override @Override
public String getValueAsString() { public String getValueAsString() {
return kv.getValueAsString(); return kv.getValueAsString();

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

@ -16,6 +16,7 @@
package org.thingsboard.server.common.data.kv; package org.thingsboard.server.common.data.kv;
import com.fasterxml.jackson.annotation.JsonIgnore; import com.fasterxml.jackson.annotation.JsonIgnore;
import org.thingsboard.server.common.data.HasVersion;
import org.thingsboard.server.common.data.query.TsValue; import org.thingsboard.server.common.data.query.TsValue;
/** /**
@ -24,7 +25,7 @@ import org.thingsboard.server.common.data.query.TsValue;
* @author ashvayka * @author ashvayka
* *
*/ */
public interface TsKvEntry extends KvEntry { public interface TsKvEntry extends KvEntry, HasVersion {
long getTs(); long getTs();

11
common/data/src/main/java/org/thingsboard/server/common/data/kv/TsKvLatestRemovingResult.java

@ -22,15 +22,22 @@ public class TsKvLatestRemovingResult {
private String key; private String key;
private TsKvEntry data; private TsKvEntry data;
private boolean removed; private boolean removed;
private Long version;
public TsKvLatestRemovingResult(TsKvEntry data) { public TsKvLatestRemovingResult(String key, boolean removed) {
this(key, removed, null);
}
public TsKvLatestRemovingResult(TsKvEntry data, Long version) {
this.key = data.getKey(); this.key = data.getKey();
this.data = data; this.data = data;
this.removed = true; this.removed = true;
this.version = version;
} }
public TsKvLatestRemovingResult(String key, boolean removed) { public TsKvLatestRemovingResult(String key, boolean removed, Long version) {
this.key = key; this.key = key;
this.removed = removed; this.removed = removed;
this.version = version;
} }
} }

54
common/data/src/main/java/org/thingsboard/server/common/data/relation/EntityRelation.java

@ -18,8 +18,13 @@ package org.thingsboard.server.common.data.relation;
import com.fasterxml.jackson.annotation.JsonIgnore; import com.fasterxml.jackson.annotation.JsonIgnore;
import com.fasterxml.jackson.databind.JsonNode; import com.fasterxml.jackson.databind.JsonNode;
import io.swagger.v3.oas.annotations.media.Schema; import io.swagger.v3.oas.annotations.media.Schema;
import lombok.EqualsAndHashCode;
import lombok.Getter;
import lombok.Setter;
import lombok.ToString;
import lombok.extern.slf4j.Slf4j; import lombok.extern.slf4j.Slf4j;
import org.thingsboard.server.common.data.BaseDataWithAdditionalInfo; import org.thingsboard.server.common.data.BaseDataWithAdditionalInfo;
import org.thingsboard.server.common.data.HasVersion;
import org.thingsboard.server.common.data.id.EntityId; import org.thingsboard.server.common.data.id.EntityId;
import org.thingsboard.server.common.data.validation.Length; import org.thingsboard.server.common.data.validation.Length;
@ -27,7 +32,9 @@ import java.io.Serializable;
@Slf4j @Slf4j
@Schema @Schema
public class EntityRelation implements Serializable { @EqualsAndHashCode(exclude = "additionalInfoBytes")
@ToString(exclude = {"additionalInfoBytes"})
public class EntityRelation implements HasVersion, Serializable {
private static final long serialVersionUID = 2807343040519543363L; private static final long serialVersionUID = 2807343040519543363L;
@ -35,11 +42,18 @@ public class EntityRelation implements Serializable {
public static final String CONTAINS_TYPE = "Contains"; public static final String CONTAINS_TYPE = "Contains";
public static final String MANAGES_TYPE = "Manages"; public static final String MANAGES_TYPE = "Manages";
@Setter
private EntityId from; private EntityId from;
@Setter
private EntityId to; private EntityId to;
@Setter
@Length(fieldName = "type") @Length(fieldName = "type")
private String type; private String type;
@Setter
private RelationTypeGroup typeGroup; private RelationTypeGroup typeGroup;
@Getter
@Setter
private Long version;
private transient JsonNode additionalInfo; private transient JsonNode additionalInfo;
@JsonIgnore @JsonIgnore
private byte[] additionalInfoBytes; private byte[] additionalInfoBytes;
@ -70,6 +84,7 @@ public class EntityRelation implements Serializable {
this.type = entityRelation.getType(); this.type = entityRelation.getType();
this.typeGroup = entityRelation.getTypeGroup(); this.typeGroup = entityRelation.getTypeGroup();
this.additionalInfo = entityRelation.getAdditionalInfo(); this.additionalInfo = entityRelation.getAdditionalInfo();
this.version = entityRelation.getVersion();
} }
@Schema(description = "JSON object with [from] Entity Id.", accessMode = Schema.AccessMode.READ_ONLY) @Schema(description = "JSON object with [from] Entity Id.", accessMode = Schema.AccessMode.READ_ONLY)
@ -77,37 +92,21 @@ public class EntityRelation implements Serializable {
return from; return from;
} }
public void setFrom(EntityId from) {
this.from = from;
}
@Schema(description = "JSON object with [to] Entity Id.", accessMode = Schema.AccessMode.READ_ONLY) @Schema(description = "JSON object with [to] Entity Id.", accessMode = Schema.AccessMode.READ_ONLY)
public EntityId getTo() { public EntityId getTo() {
return to; return to;
} }
public void setTo(EntityId to) {
this.to = to;
}
@Schema(description = "String value of relation type.", example = "Contains") @Schema(description = "String value of relation type.", example = "Contains")
public String getType() { public String getType() {
return type; return type;
} }
public void setType(String type) {
this.type = type;
}
@Schema(description = "Represents the type group of the relation.", example = "COMMON") @Schema(description = "Represents the type group of the relation.", example = "COMMON")
public RelationTypeGroup getTypeGroup() { public RelationTypeGroup getTypeGroup() {
return typeGroup; return typeGroup;
} }
public void setTypeGroup(RelationTypeGroup typeGroup) {
this.typeGroup = typeGroup;
}
@Schema(description = "Additional parameters of the relation",implementation = com.fasterxml.jackson.databind.JsonNode.class) @Schema(description = "Additional parameters of the relation",implementation = com.fasterxml.jackson.databind.JsonNode.class)
public JsonNode getAdditionalInfo() { public JsonNode getAdditionalInfo() {
return BaseDataWithAdditionalInfo.getJson(() -> additionalInfo, () -> additionalInfoBytes); return BaseDataWithAdditionalInfo.getJson(() -> additionalInfo, () -> additionalInfoBytes);
@ -117,25 +116,4 @@ public class EntityRelation implements Serializable {
BaseDataWithAdditionalInfo.setJson(addInfo, json -> this.additionalInfo = json, bytes -> this.additionalInfoBytes = bytes); BaseDataWithAdditionalInfo.setJson(addInfo, json -> this.additionalInfo = json, bytes -> this.additionalInfoBytes = bytes);
} }
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
EntityRelation that = (EntityRelation) o;
if (from != null ? !from.equals(that.from) : that.from != null) return false;
if (to != null ? !to.equals(that.to) : that.to != null) return false;
if (type != null ? !type.equals(that.type) : that.type != null) return false;
return typeGroup == that.typeGroup;
}
@Override
public int hashCode() {
int result = from != null ? from.hashCode() : 0;
result = 31 * result + (to != null ? to.hashCode() : 0);
result = 31 * result + (type != null ? type.hashCode() : 0);
result = 31 * result + (typeGroup != null ? typeGroup.hashCode() : 0);
return result;
}
} }

8
common/data/src/main/java/org/thingsboard/server/common/data/util/CollectionsUtil.java

@ -17,6 +17,7 @@ package org.thingsboard.server.common.data.util;
import java.util.Collection; import java.util.Collection;
import java.util.HashMap; import java.util.HashMap;
import java.util.List;
import java.util.Map; import java.util.Map;
import java.util.Set; import java.util.Set;
import java.util.stream.Collectors; import java.util.stream.Collectors;
@ -37,6 +38,13 @@ public class CollectionsUtil {
return b.stream().filter(p -> !a.contains(p)).collect(Collectors.toSet()); return b.stream().filter(p -> !a.contains(p)).collect(Collectors.toSet());
} }
/**
* Returns new list with elements that are present in list B(new) but absent in list A(old).
*/
public static <T> List<T> diffLists(List<T> a, List<T> b) {
return b.stream().filter(p -> !a.contains(p)).collect(Collectors.toList());
}
public static <T> boolean contains(Collection<T> collection, T element) { public static <T> boolean contains(Collection<T> collection, T element) {
return isNotEmpty(collection) && collection.contains(element); return isNotEmpty(collection) && collection.contains(element);
} }

14
common/proto/src/main/java/org/thingsboard/server/common/util/KvProtoUtil.java

@ -86,8 +86,20 @@ public class KvProtoUtil {
.setKv(KvProtoUtil.toKeyValueTypeProto(kvEntry)).build(); .setKv(KvProtoUtil.toKeyValueTypeProto(kvEntry)).build();
} }
public static TransportProtos.TsKvProto toTsKvProto(long ts, KvEntry kvEntry, Long version) {
var builder = TransportProtos.TsKvProto.newBuilder()
.setTs(ts)
.setKv(KvProtoUtil.toKeyValueTypeProto(kvEntry));
if (version != null) {
builder.setVersion(version);
}
return builder.build();
}
public static TsKvEntry fromTsKvProto(TransportProtos.TsKvProto proto) { public static TsKvEntry fromTsKvProto(TransportProtos.TsKvProto proto) {
return new BasicTsKvEntry(proto.getTs(), fromTsKvProto(proto.getKv())); return new BasicTsKvEntry(proto.getTs(), fromTsKvProto(proto.getKv()), proto.hasVersion() ? proto.getVersion() : null);
} }
public static TransportProtos.KeyValueProto toKeyValueTypeProto(KvEntry kvEntry) { public static TransportProtos.KeyValueProto toKeyValueTypeProto(KvEntry kvEntry) {

98
common/proto/src/main/java/org/thingsboard/server/common/util/ProtoUtils.java

@ -17,7 +17,9 @@ package org.thingsboard.server.common.util;
import com.fasterxml.jackson.core.JsonProcessingException; import com.fasterxml.jackson.core.JsonProcessingException;
import com.google.protobuf.ByteString; import com.google.protobuf.ByteString;
import com.google.protobuf.InvalidProtocolBufferException;
import lombok.extern.slf4j.Slf4j; import lombok.extern.slf4j.Slf4j;
import org.jetbrains.annotations.Nullable;
import org.thingsboard.common.util.JacksonUtil; import org.thingsboard.common.util.JacksonUtil;
import org.thingsboard.server.common.data.ApiUsageState; import org.thingsboard.server.common.data.ApiUsageState;
import org.thingsboard.server.common.data.ApiUsageStateValue; import org.thingsboard.server.common.data.ApiUsageStateValue;
@ -256,42 +258,51 @@ public class ProtoUtils {
if (msg.getValues() != null) { if (msg.getValues() != null) {
for (AttributeKvEntry attributeKvEntry : msg.getValues()) { for (AttributeKvEntry attributeKvEntry : msg.getValues()) {
TransportProtos.AttributeValueProto.Builder attributeValueBuilder = TransportProtos.AttributeValueProto.newBuilder() builder.addValues(toProto(attributeKvEntry));
.setLastUpdateTs(attributeKvEntry.getLastUpdateTs())
.setKey(attributeKvEntry.getKey());
switch (attributeKvEntry.getDataType()) {
case BOOLEAN -> {
attributeKvEntry.getBooleanValue().ifPresent(attributeValueBuilder::setBoolV);
attributeValueBuilder.setHasV(attributeKvEntry.getBooleanValue().isPresent());
attributeValueBuilder.setType(TransportProtos.KeyValueType.BOOLEAN_V);
}
case STRING -> {
attributeKvEntry.getStrValue().ifPresent(attributeValueBuilder::setStringV);
attributeValueBuilder.setHasV(attributeKvEntry.getStrValue().isPresent());
attributeValueBuilder.setType(TransportProtos.KeyValueType.STRING_V);
}
case DOUBLE -> {
attributeKvEntry.getDoubleValue().ifPresent(attributeValueBuilder::setDoubleV);
attributeValueBuilder.setHasV(attributeKvEntry.getDoubleValue().isPresent());
attributeValueBuilder.setType(TransportProtos.KeyValueType.DOUBLE_V);
}
case LONG -> {
attributeKvEntry.getLongValue().ifPresent(attributeValueBuilder::setLongV);
attributeValueBuilder.setHasV(attributeKvEntry.getLongValue().isPresent());
attributeValueBuilder.setType(TransportProtos.KeyValueType.LONG_V);
}
case JSON -> {
attributeKvEntry.getJsonValue().ifPresent(attributeValueBuilder::setJsonV);
attributeValueBuilder.setHasV(attributeKvEntry.getJsonValue().isPresent());
attributeValueBuilder.setType(TransportProtos.KeyValueType.JSON_V);
}
}
builder.addValues(attributeValueBuilder.build());
} }
} }
return builder.build(); return builder.build();
} }
public static TransportProtos.AttributeValueProto toProto(AttributeKvEntry attributeKvEntry) {
TransportProtos.AttributeValueProto.Builder builder = TransportProtos.AttributeValueProto.newBuilder()
.setLastUpdateTs(attributeKvEntry.getLastUpdateTs())
.setKey(attributeKvEntry.getKey());
switch (attributeKvEntry.getDataType()) {
case BOOLEAN:
attributeKvEntry.getBooleanValue().ifPresent(builder::setBoolV);
builder.setHasV(attributeKvEntry.getBooleanValue().isPresent());
builder.setType(TransportProtos.KeyValueType.BOOLEAN_V);
break;
case STRING:
attributeKvEntry.getStrValue().ifPresent(builder::setStringV);
builder.setHasV(attributeKvEntry.getStrValue().isPresent());
builder.setType(TransportProtos.KeyValueType.STRING_V);
break;
case DOUBLE:
attributeKvEntry.getDoubleValue().ifPresent(builder::setDoubleV);
builder.setHasV(attributeKvEntry.getDoubleValue().isPresent());
builder.setType(TransportProtos.KeyValueType.DOUBLE_V);
break;
case LONG:
attributeKvEntry.getLongValue().ifPresent(builder::setLongV);
builder.setHasV(attributeKvEntry.getLongValue().isPresent());
builder.setType(TransportProtos.KeyValueType.LONG_V);
break;
case JSON:
attributeKvEntry.getJsonValue().ifPresent(builder::setJsonV);
builder.setHasV(attributeKvEntry.getJsonValue().isPresent());
builder.setType(TransportProtos.KeyValueType.JSON_V);
break;
}
if (attributeKvEntry.getVersion() != null) {
builder.setVersion(attributeKvEntry.getVersion());
}
return builder.build();
}
private static ToDeviceActorNotificationMsg fromProto(TransportProtos.DeviceAttributesEventMsgProto proto) { private static ToDeviceActorNotificationMsg fromProto(TransportProtos.DeviceAttributesEventMsgProto proto) {
return new DeviceAttributesEventNotificationMsg( return new DeviceAttributesEventNotificationMsg(
TenantId.fromUUID(new UUID(proto.getTenantIdMSB(), proto.getTenantIdLSB())), TenantId.fromUUID(new UUID(proto.getTenantIdMSB(), proto.getTenantIdLSB())),
@ -500,20 +511,25 @@ public class ProtoUtils {
} }
List<AttributeKvEntry> result = new ArrayList<>(); List<AttributeKvEntry> result = new ArrayList<>();
for (TransportProtos.AttributeValueProto kvEntry : valuesList) { for (TransportProtos.AttributeValueProto kvEntry : valuesList) {
boolean hasValue = kvEntry.getHasV(); result.add(fromProto(kvEntry));
KvEntry entry = switch (kvEntry.getType()) {
case BOOLEAN_V -> new BooleanDataEntry(kvEntry.getKey(), hasValue ? kvEntry.getBoolV() : null);
case LONG_V -> new LongDataEntry(kvEntry.getKey(), hasValue ? kvEntry.getLongV() : null);
case DOUBLE_V -> new DoubleDataEntry(kvEntry.getKey(), hasValue ? kvEntry.getDoubleV() : null);
case STRING_V -> new StringDataEntry(kvEntry.getKey(), hasValue ? kvEntry.getStringV() : null);
case JSON_V -> new JsonDataEntry(kvEntry.getKey(), hasValue ? kvEntry.getJsonV() : null);
default -> null;
};
result.add(new BaseAttributeKvEntry(kvEntry.getLastUpdateTs(), entry));
} }
return result; return result;
} }
public static AttributeKvEntry fromProto(TransportProtos.AttributeValueProto proto) {
boolean hasValue = proto.getHasV();
String key = proto.getKey();
KvEntry entry = switch (proto.getType()) {
case BOOLEAN_V -> new BooleanDataEntry(key, hasValue ? proto.getBoolV() : null);
case LONG_V -> new LongDataEntry(key, hasValue ? proto.getLongV() : null);
case DOUBLE_V -> new DoubleDataEntry(key, hasValue ? proto.getDoubleV() : null);
case STRING_V -> new StringDataEntry(key, hasValue ? proto.getStringV() : null);
case JSON_V -> new JsonDataEntry(key, hasValue ? proto.getJsonV() : null);
default -> null;
};
return new BaseAttributeKvEntry(entry, proto.getLastUpdateTs(), proto.hasVersion() ? proto.getVersion() : null);
}
public static TransportProtos.DeviceProto toProto(Device device) { public static TransportProtos.DeviceProto toProto(Device device) {
var builder = TransportProtos.DeviceProto.newBuilder() var builder = TransportProtos.DeviceProto.newBuilder()
.setTenantIdMSB(device.getTenantId().getId().getMostSignificantBits()) .setTenantIdMSB(device.getTenantId().getId().getMostSignificantBits())

2
common/proto/src/main/proto/queue.proto

@ -163,11 +163,13 @@ message AttributeValueProto {
string string_v = 7; string string_v = 7;
string json_v = 8; string json_v = 8;
optional string key = 9; optional string key = 9;
optional int64 version = 10;
} }
message TsKvProto { message TsKvProto {
int64 ts = 1; int64 ts = 1;
KeyValueProto kv = 2; KeyValueProto kv = 2;
optional int64 version = 3;
} }
message TsKvListProto { message TsKvListProto {

5
dao/pom.xml

@ -216,6 +216,11 @@
<artifactId>jdbc</artifactId> <artifactId>jdbc</artifactId>
<scope>test</scope> <scope>test</scope>
</dependency> </dependency>
<dependency>
<groupId>org.testcontainers</groupId>
<artifactId>junit-jupiter</artifactId>
<scope>test</scope>
</dependency>
<dependency> <dependency>
<groupId>org.springframework</groupId> <groupId>org.springframework</groupId>
<artifactId>spring-context-support</artifactId> <artifactId>spring-context-support</artifactId>

130
dao/src/main/java/org/thingsboard/server/dao/AbstractVersionedInsertRepository.java

@ -0,0 +1,130 @@
/**
* Copyright © 2016-2024 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;
import org.springframework.jdbc.core.BatchPreparedStatementSetter;
import org.springframework.jdbc.core.PreparedStatementCreator;
import org.springframework.jdbc.core.SqlProvider;
import org.springframework.jdbc.support.GeneratedKeyHolder;
import org.springframework.jdbc.support.KeyHolder;
import org.thingsboard.server.dao.sqlts.insert.AbstractInsertRepository;
import java.sql.Connection;
import java.sql.PreparedStatement;
import java.sql.SQLException;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import static org.thingsboard.server.dao.model.ModelConstants.VERSION_COLUMN;
public abstract class AbstractVersionedInsertRepository<T> extends AbstractInsertRepository {
public List<Long> saveOrUpdate(List<T> entities) {
return transactionTemplate.execute(status -> {
List<Long> seqNumbers = new ArrayList<>(entities.size());
KeyHolder keyHolder = new GeneratedKeyHolder();
int[] updateResult = onBatchUpdate(entities, keyHolder);
List<Map<String, Object>> seqNumbersList = keyHolder.getKeyList();
int notUpdatedCount = entities.size() - seqNumbersList.size();
List<Integer> toInsertIndexes = new ArrayList<>(notUpdatedCount);
List<T> insertEntities = new ArrayList<>(notUpdatedCount);
int keyHolderIndex = 0;
for (int i = 0; i < updateResult.length; i++) {
if (updateResult[i] == 0) {
insertEntities.add(entities.get(i));
seqNumbers.add(null);
toInsertIndexes.add(i);
} else {
seqNumbers.add((Long) seqNumbersList.get(keyHolderIndex).get(VERSION_COLUMN));
keyHolderIndex++;
}
}
if (insertEntities.isEmpty()) {
return seqNumbers;
}
int[] insertResult = onInsertOrUpdate(insertEntities, keyHolder);
seqNumbersList = keyHolder.getKeyList();
for (int i = 0; i < insertResult.length; i++) {
if (insertResult[i] != 0) {
seqNumbers.set(toInsertIndexes.get(i), (Long) seqNumbersList.get(i).get(VERSION_COLUMN));
}
}
return seqNumbers;
});
}
private int[] onBatchUpdate(List<T> entities, KeyHolder keyHolder) {
return jdbcTemplate.batchUpdate(new SequencePreparedStatementCreator(getBatchUpdateQuery()), new BatchPreparedStatementSetter() {
@Override
public void setValues(PreparedStatement ps, int i) throws SQLException {
setOnBatchUpdateValues(ps, i, entities);
}
@Override
public int getBatchSize() {
return entities.size();
}
}, keyHolder);
}
private int[] onInsertOrUpdate(List<T> insertEntities, KeyHolder keyHolder) {
return jdbcTemplate.batchUpdate(new SequencePreparedStatementCreator(getInsertOrUpdateQuery()), new BatchPreparedStatementSetter() {
@Override
public void setValues(PreparedStatement ps, int i) throws SQLException {
setOnInsertOrUpdateValues(ps, i, insertEntities);
}
@Override
public int getBatchSize() {
return insertEntities.size();
}
}, keyHolder);
}
protected abstract void setOnBatchUpdateValues(PreparedStatement ps, int i, List<T> entities) throws SQLException;
protected abstract void setOnInsertOrUpdateValues(PreparedStatement ps, int i, List<T> entities) throws SQLException;
protected abstract String getBatchUpdateQuery();
protected abstract String getInsertOrUpdateQuery();
private record SequencePreparedStatementCreator(String sql) implements PreparedStatementCreator, SqlProvider {
private static final String[] COLUMNS = {VERSION_COLUMN};
@Override
public PreparedStatement createPreparedStatement(Connection con) throws SQLException {
return con.prepareStatement(sql, COLUMNS);
}
@Override
public String getSql() {
return this.sql;
}
}
}

4
dao/src/main/java/org/thingsboard/server/dao/attributes/AttributeCaffeineCache.java

@ -18,13 +18,13 @@ package org.thingsboard.server.dao.attributes;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.cache.CacheManager; import org.springframework.cache.CacheManager;
import org.springframework.stereotype.Service; import org.springframework.stereotype.Service;
import org.thingsboard.server.cache.CaffeineTbTransactionalCache; import org.thingsboard.server.cache.VersionedCaffeineTbCache;
import org.thingsboard.server.common.data.CacheConstants; import org.thingsboard.server.common.data.CacheConstants;
import org.thingsboard.server.common.data.kv.AttributeKvEntry; import org.thingsboard.server.common.data.kv.AttributeKvEntry;
@ConditionalOnProperty(prefix = "cache", value = "type", havingValue = "caffeine", matchIfMissing = true) @ConditionalOnProperty(prefix = "cache", value = "type", havingValue = "caffeine", matchIfMissing = true)
@Service("AttributeCache") @Service("AttributeCache")
public class AttributeCaffeineCache extends CaffeineTbTransactionalCache<AttributeCacheKey, AttributeKvEntry> { public class AttributeCaffeineCache extends VersionedCaffeineTbCache<AttributeCacheKey, AttributeKvEntry> {
public AttributeCaffeineCache(CacheManager cacheManager) { public AttributeCaffeineCache(CacheManager cacheManager) {
super(cacheManager, CacheConstants.ATTRIBUTES_CACHE); super(cacheManager, CacheConstants.ATTRIBUTES_CACHE);

69
dao/src/main/java/org/thingsboard/server/dao/attributes/AttributeRedisCache.java

@ -21,88 +21,29 @@ import org.springframework.data.redis.connection.RedisConnectionFactory;
import org.springframework.data.redis.serializer.SerializationException; import org.springframework.data.redis.serializer.SerializationException;
import org.springframework.stereotype.Service; import org.springframework.stereotype.Service;
import org.thingsboard.server.cache.CacheSpecsMap; import org.thingsboard.server.cache.CacheSpecsMap;
import org.thingsboard.server.cache.RedisTbTransactionalCache;
import org.thingsboard.server.cache.TBRedisCacheConfiguration; import org.thingsboard.server.cache.TBRedisCacheConfiguration;
import org.thingsboard.server.cache.TbRedisSerializer; import org.thingsboard.server.cache.TbRedisSerializer;
import org.thingsboard.server.cache.VersionedRedisTbCache;
import org.thingsboard.server.common.data.CacheConstants; import org.thingsboard.server.common.data.CacheConstants;
import org.thingsboard.server.common.data.kv.AttributeKvEntry; import org.thingsboard.server.common.data.kv.AttributeKvEntry;
import org.thingsboard.server.common.data.kv.BaseAttributeKvEntry; import org.thingsboard.server.common.util.ProtoUtils;
import org.thingsboard.server.common.data.kv.BooleanDataEntry;
import org.thingsboard.server.common.data.kv.DoubleDataEntry;
import org.thingsboard.server.common.data.kv.JsonDataEntry;
import org.thingsboard.server.common.data.kv.KvEntry;
import org.thingsboard.server.common.data.kv.LongDataEntry;
import org.thingsboard.server.common.data.kv.StringDataEntry;
import org.thingsboard.server.gen.transport.TransportProtos.AttributeValueProto; import org.thingsboard.server.gen.transport.TransportProtos.AttributeValueProto;
import org.thingsboard.server.gen.transport.TransportProtos.KeyValueType;
@ConditionalOnProperty(prefix = "cache", value = "type", havingValue = "redis") @ConditionalOnProperty(prefix = "cache", value = "type", havingValue = "redis")
@Service("AttributeCache") @Service("AttributeCache")
public class AttributeRedisCache extends RedisTbTransactionalCache<AttributeCacheKey, AttributeKvEntry> { public class AttributeRedisCache extends VersionedRedisTbCache<AttributeCacheKey, AttributeKvEntry> {
public AttributeRedisCache(TBRedisCacheConfiguration configuration, CacheSpecsMap cacheSpecsMap, RedisConnectionFactory connectionFactory) { public AttributeRedisCache(TBRedisCacheConfiguration configuration, CacheSpecsMap cacheSpecsMap, RedisConnectionFactory connectionFactory) {
super(CacheConstants.ATTRIBUTES_CACHE, cacheSpecsMap, connectionFactory, configuration, new TbRedisSerializer<>() { super(CacheConstants.ATTRIBUTES_CACHE, cacheSpecsMap, connectionFactory, configuration, new TbRedisSerializer<>() {
@Override @Override
public byte[] serialize(AttributeKvEntry attributeKvEntry) throws SerializationException { public byte[] serialize(AttributeKvEntry attributeKvEntry) throws SerializationException {
AttributeValueProto.Builder builder = AttributeValueProto.newBuilder() return ProtoUtils.toProto(attributeKvEntry).toByteArray();
.setLastUpdateTs(attributeKvEntry.getLastUpdateTs());
switch (attributeKvEntry.getDataType()) {
case BOOLEAN:
attributeKvEntry.getBooleanValue().ifPresent(builder::setBoolV);
builder.setHasV(attributeKvEntry.getBooleanValue().isPresent());
builder.setType(KeyValueType.BOOLEAN_V);
break;
case STRING:
attributeKvEntry.getStrValue().ifPresent(builder::setStringV);
builder.setHasV(attributeKvEntry.getStrValue().isPresent());
builder.setType(KeyValueType.STRING_V);
break;
case DOUBLE:
attributeKvEntry.getDoubleValue().ifPresent(builder::setDoubleV);
builder.setHasV(attributeKvEntry.getDoubleValue().isPresent());
builder.setType(KeyValueType.DOUBLE_V);
break;
case LONG:
attributeKvEntry.getLongValue().ifPresent(builder::setLongV);
builder.setHasV(attributeKvEntry.getLongValue().isPresent());
builder.setType(KeyValueType.LONG_V);
break;
case JSON:
attributeKvEntry.getJsonValue().ifPresent(builder::setJsonV);
builder.setHasV(attributeKvEntry.getJsonValue().isPresent());
builder.setType(KeyValueType.JSON_V);
break;
}
return builder.build().toByteArray();
} }
@Override @Override
public AttributeKvEntry deserialize(AttributeCacheKey key, byte[] bytes) throws SerializationException { public AttributeKvEntry deserialize(AttributeCacheKey key, byte[] bytes) throws SerializationException {
try { try {
AttributeValueProto proto = AttributeValueProto.parseFrom(bytes); return ProtoUtils.fromProto(AttributeValueProto.parseFrom(bytes));
boolean hasValue = proto.getHasV();
KvEntry entry;
switch (proto.getType()) {
case BOOLEAN_V:
entry = new BooleanDataEntry(key.getKey(), hasValue ? proto.getBoolV() : null);
break;
case LONG_V:
entry = new LongDataEntry(key.getKey(), hasValue ? proto.getLongV() : null);
break;
case DOUBLE_V:
entry = new DoubleDataEntry(key.getKey(), hasValue ? proto.getDoubleV() : null);
break;
case STRING_V:
entry = new StringDataEntry(key.getKey(), hasValue ? proto.getStringV() : null);
break;
case JSON_V:
entry = new JsonDataEntry(key.getKey(), hasValue ? proto.getJsonV() : null);
break;
default:
throw new InvalidProtocolBufferException("Unrecognized type: " + proto.getType() + " !");
}
return new BaseAttributeKvEntry(proto.getLastUpdateTs(), entry);
} catch (InvalidProtocolBufferException e) { } catch (InvalidProtocolBufferException e) {
throw new SerializationException(e.getMessage()); throw new SerializationException(e.getMessage());
} }

5
dao/src/main/java/org/thingsboard/server/dao/attributes/AttributesDao.java

@ -22,6 +22,7 @@ import org.thingsboard.server.common.data.id.DeviceProfileId;
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 org.thingsboard.server.common.data.kv.AttributeKvEntry; import org.thingsboard.server.common.data.kv.AttributeKvEntry;
import org.thingsboard.server.common.data.util.TbPair;
import java.util.Collection; import java.util.Collection;
import java.util.List; import java.util.List;
@ -38,10 +39,12 @@ public interface AttributesDao {
List<AttributeKvEntry> findAll(TenantId tenantId, EntityId entityId, AttributeScope attributeScope); List<AttributeKvEntry> findAll(TenantId tenantId, EntityId entityId, AttributeScope attributeScope);
ListenableFuture<String> save(TenantId tenantId, EntityId entityId, AttributeScope attributeScope, AttributeKvEntry attribute); ListenableFuture<Long> save(TenantId tenantId, EntityId entityId, AttributeScope attributeScope, AttributeKvEntry attribute);
List<ListenableFuture<String>> removeAll(TenantId tenantId, EntityId entityId, AttributeScope attributeScope, List<String> keys); List<ListenableFuture<String>> removeAll(TenantId tenantId, EntityId entityId, AttributeScope attributeScope, List<String> keys);
List<ListenableFuture<TbPair<String, Long>>> removeAllWithVersions(TenantId tenantId, EntityId entityId, AttributeScope attributeScope, List<String> keys);
List<String> findAllKeysByDeviceProfileId(TenantId tenantId, DeviceProfileId deviceProfileId); List<String> findAllKeysByDeviceProfileId(TenantId tenantId, DeviceProfileId deviceProfileId);
List<String> findAllKeysByEntityIds(TenantId tenantId, List<EntityId> entityIds); List<String> findAllKeysByEntityIds(TenantId tenantId, List<EntityId> entityIds);

42
dao/src/main/java/org/thingsboard/server/dao/attributes/BaseAttributesService.java

@ -56,13 +56,6 @@ public class BaseAttributesService implements AttributesService {
this.attributesDao = attributesDao; this.attributesDao = attributesDao;
} }
@Override
public ListenableFuture<Optional<AttributeKvEntry>> find(TenantId tenantId, EntityId entityId, String scope, String attributeKey) {
validate(entityId, scope);
Validator.validateString(attributeKey, k -> "Incorrect attribute key " + k);
return Futures.immediateFuture(attributesDao.find(tenantId, entityId, AttributeScope.valueOf(scope), attributeKey));
}
@Override @Override
public ListenableFuture<Optional<AttributeKvEntry>> find(TenantId tenantId, EntityId entityId, AttributeScope scope, String attributeKey) { public ListenableFuture<Optional<AttributeKvEntry>> find(TenantId tenantId, EntityId entityId, AttributeScope scope, String attributeKey) {
validate(entityId, scope); validate(entityId, scope);
@ -70,13 +63,6 @@ public class BaseAttributesService implements AttributesService {
return Futures.immediateFuture(attributesDao.find(tenantId, entityId, scope, attributeKey)); return Futures.immediateFuture(attributesDao.find(tenantId, entityId, scope, attributeKey));
} }
@Override
public ListenableFuture<List<AttributeKvEntry>> find(TenantId tenantId, EntityId entityId, String scope, Collection<String> attributeKeys) {
validate(entityId, scope);
attributeKeys.forEach(attributeKey -> Validator.validateString(attributeKey, k -> "Incorrect attribute key " + k));
return Futures.immediateFuture(attributesDao.find(tenantId, entityId, AttributeScope.valueOf(scope), attributeKeys));
}
@Override @Override
public ListenableFuture<List<AttributeKvEntry>> find(TenantId tenantId, EntityId entityId, AttributeScope scope, Collection<String> attributeKeys) { public ListenableFuture<List<AttributeKvEntry>> find(TenantId tenantId, EntityId entityId, AttributeScope scope, Collection<String> attributeKeys) {
validate(entityId, scope); validate(entityId, scope);
@ -84,12 +70,6 @@ public class BaseAttributesService implements AttributesService {
return Futures.immediateFuture(attributesDao.find(tenantId, entityId, scope, attributeKeys)); return Futures.immediateFuture(attributesDao.find(tenantId, entityId, scope, attributeKeys));
} }
@Override
public ListenableFuture<List<AttributeKvEntry>> findAll(TenantId tenantId, EntityId entityId, String scope) {
validate(entityId, scope);
return Futures.immediateFuture(attributesDao.findAll(tenantId, entityId, AttributeScope.valueOf(scope)));
}
@Override @Override
public ListenableFuture<List<AttributeKvEntry>> findAll(TenantId tenantId, EntityId entityId, AttributeScope scope) { public ListenableFuture<List<AttributeKvEntry>> findAll(TenantId tenantId, EntityId entityId, AttributeScope scope) {
validate(entityId, scope); validate(entityId, scope);
@ -101,11 +81,6 @@ public class BaseAttributesService implements AttributesService {
return attributesDao.findAllKeysByDeviceProfileId(tenantId, deviceProfileId); return attributesDao.findAllKeysByDeviceProfileId(tenantId, deviceProfileId);
} }
@Override
public List<String> findAllKeysByEntityIds(TenantId tenantId, EntityType entityType, List<EntityId> entityIds) {
return attributesDao.findAllKeysByEntityIds(tenantId, entityIds);
}
@Override @Override
public List<String> findAllKeysByEntityIds(TenantId tenantId, List<EntityId> entityIds) { public List<String> findAllKeysByEntityIds(TenantId tenantId, List<EntityId> entityIds) {
return attributesDao.findAllKeysByEntityIds(tenantId, entityIds); return attributesDao.findAllKeysByEntityIds(tenantId, entityIds);
@ -121,32 +96,25 @@ public class BaseAttributesService implements AttributesService {
} }
@Override @Override
public ListenableFuture<String> save(TenantId tenantId, EntityId entityId, String scope, AttributeKvEntry attribute) { public ListenableFuture<Long> save(TenantId tenantId, EntityId entityId, AttributeScope scope, AttributeKvEntry attribute) {
validate(entityId, scope);
AttributeUtils.validate(attribute, valueNoXssValidation);
return attributesDao.save(tenantId, entityId, AttributeScope.valueOf(scope), attribute);
}
@Override
public ListenableFuture<String> save(TenantId tenantId, EntityId entityId, AttributeScope scope, AttributeKvEntry attribute) {
validate(entityId, scope); validate(entityId, scope);
AttributeUtils.validate(attribute, valueNoXssValidation); AttributeUtils.validate(attribute, valueNoXssValidation);
return attributesDao.save(tenantId, entityId, scope, attribute); return attributesDao.save(tenantId, entityId, scope, attribute);
} }
@Override @Override
public ListenableFuture<List<String>> save(TenantId tenantId, EntityId entityId, String scope, List<AttributeKvEntry> attributes) { public ListenableFuture<List<Long>> save(TenantId tenantId, EntityId entityId, String scope, List<AttributeKvEntry> attributes) {
validate(entityId, scope); validate(entityId, scope);
AttributeUtils.validate(attributes, valueNoXssValidation); AttributeUtils.validate(attributes, valueNoXssValidation);
List<ListenableFuture<String>> saveFutures = attributes.stream().map(attribute -> attributesDao.save(tenantId, entityId, AttributeScope.valueOf(scope), attribute)).collect(Collectors.toList()); List<ListenableFuture<Long>> saveFutures = attributes.stream().map(attribute -> attributesDao.save(tenantId, entityId, AttributeScope.valueOf(scope), attribute)).collect(Collectors.toList());
return Futures.allAsList(saveFutures); return Futures.allAsList(saveFutures);
} }
@Override @Override
public ListenableFuture<List<String>> save(TenantId tenantId, EntityId entityId, AttributeScope scope, List<AttributeKvEntry> attributes) { public ListenableFuture<List<Long>> save(TenantId tenantId, EntityId entityId, AttributeScope scope, List<AttributeKvEntry> attributes) {
validate(entityId, scope); validate(entityId, scope);
AttributeUtils.validate(attributes, valueNoXssValidation); AttributeUtils.validate(attributes, valueNoXssValidation);
List<ListenableFuture<String>> saveFutures = attributes.stream().map(attribute -> attributesDao.save(tenantId, entityId, scope, attribute)).collect(Collectors.toList()); List<ListenableFuture<Long>> saveFutures = attributes.stream().map(attribute -> attributesDao.save(tenantId, entityId, scope, attribute)).collect(Collectors.toList());
return Futures.allAsList(saveFutures); return Futures.allAsList(saveFutures);
} }

123
dao/src/main/java/org/thingsboard/server/dao/attributes/CachedAttributesService.java

@ -27,14 +27,15 @@ import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.context.annotation.Primary; import org.springframework.context.annotation.Primary;
import org.springframework.stereotype.Service; import org.springframework.stereotype.Service;
import org.thingsboard.server.cache.TbCacheValueWrapper; import org.thingsboard.server.cache.TbCacheValueWrapper;
import org.thingsboard.server.cache.TbTransactionalCache; import org.thingsboard.server.cache.VersionedTbCache;
import org.thingsboard.server.common.data.AttributeScope; import org.thingsboard.server.common.data.AttributeScope;
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.common.data.id.DeviceProfileId; import org.thingsboard.server.common.data.id.DeviceProfileId;
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 org.thingsboard.server.common.data.kv.AttributeKvEntry; import org.thingsboard.server.common.data.kv.AttributeKvEntry;
import org.thingsboard.server.common.data.kv.BaseAttributeKvEntry;
import org.thingsboard.server.common.data.util.TbPair;
import org.thingsboard.server.common.stats.DefaultCounter; import org.thingsboard.server.common.stats.DefaultCounter;
import org.thingsboard.server.common.stats.StatsFactory; import org.thingsboard.server.common.stats.StatsFactory;
import org.thingsboard.server.dao.cache.CacheExecutorService; import org.thingsboard.server.dao.cache.CacheExecutorService;
@ -68,7 +69,7 @@ public class CachedAttributesService implements AttributesService {
private final CacheExecutorService cacheExecutorService; private final CacheExecutorService cacheExecutorService;
private final DefaultCounter hitCounter; private final DefaultCounter hitCounter;
private final DefaultCounter missCounter; private final DefaultCounter missCounter;
private final TbTransactionalCache<AttributeCacheKey, AttributeKvEntry> cache; private final VersionedTbCache<AttributeCacheKey, AttributeKvEntry> cache;
private ListeningExecutorService cacheExecutor; private ListeningExecutorService cacheExecutor;
@Value("${cache.type:caffeine}") @Value("${cache.type:caffeine}")
@ -80,7 +81,7 @@ public class CachedAttributesService implements AttributesService {
JpaExecutorService jpaExecutorService, JpaExecutorService jpaExecutorService,
StatsFactory statsFactory, StatsFactory statsFactory,
CacheExecutorService cacheExecutorService, CacheExecutorService cacheExecutorService,
TbTransactionalCache<AttributeCacheKey, AttributeKvEntry> cache) { VersionedTbCache<AttributeCacheKey, AttributeKvEntry> cache) {
this.attributesDao = attributesDao; this.attributesDao = attributesDao;
this.jpaExecutorService = jpaExecutorService; this.jpaExecutorService = jpaExecutorService;
this.cacheExecutorService = cacheExecutorService; this.cacheExecutorService = cacheExecutorService;
@ -109,12 +110,6 @@ public class CachedAttributesService implements AttributesService {
return cacheExecutorService.executor(); return cacheExecutorService.executor();
} }
@Override
public ListenableFuture<Optional<AttributeKvEntry>> find(TenantId tenantId, EntityId entityId, String scope, String attributeKey) {
return find(tenantId, entityId, AttributeScope.valueOf(scope), attributeKey);
}
@Override @Override
public ListenableFuture<Optional<AttributeKvEntry>> find(TenantId tenantId, EntityId entityId, AttributeScope scope, String attributeKey) { public ListenableFuture<Optional<AttributeKvEntry>> find(TenantId tenantId, EntityId entityId, AttributeScope scope, String attributeKey) {
validate(entityId, scope); validate(entityId, scope);
@ -129,31 +124,18 @@ public class CachedAttributesService implements AttributesService {
return Optional.ofNullable(cachedAttributeKvEntry); return Optional.ofNullable(cachedAttributeKvEntry);
} else { } else {
missCounter.increment(); missCounter.increment();
var cacheTransaction = cache.newTransactionForKey(attributeCacheKey); Optional<AttributeKvEntry> result = attributesDao.find(tenantId, entityId, scope, attributeKey);
try { cache.put(attributeCacheKey, result.orElse(null));
Optional<AttributeKvEntry> result = attributesDao.find(tenantId, entityId, scope, attributeKey); return result;
cacheTransaction.putIfAbsent(attributeCacheKey, result.orElse(null));
cacheTransaction.commit();
return result;
} catch (Throwable e) {
cacheTransaction.rollback();
log.debug("Could not find attribute from cache: [{}] [{}] [{}]", entityId, scope, attributeKey, e);
throw e;
}
} }
}); });
} }
@Override
public ListenableFuture<List<AttributeKvEntry>> find(TenantId tenantId, EntityId entityId, String scope, final Collection<String> attributeKeysNonUnique) {
return find(tenantId, entityId, AttributeScope.valueOf(scope), attributeKeysNonUnique);
}
@Override @Override
public ListenableFuture<List<AttributeKvEntry>> find(TenantId tenantId, EntityId entityId, AttributeScope scope, final Collection<String> attributeKeysNonUnique) { public ListenableFuture<List<AttributeKvEntry>> find(TenantId tenantId, EntityId entityId, AttributeScope scope, final Collection<String> attributeKeysNonUnique) {
validate(entityId, scope); validate(entityId, scope);
final var attributeKeys = new LinkedHashSet<>(attributeKeysNonUnique); // deduplicate the attributes final var attributeKeys = new LinkedHashSet<>(attributeKeysNonUnique); // deduplicate the attributes
attributeKeys.forEach(attributeKey -> Validator.validateString(attributeKey, k ->"Incorrect attribute key " + k)); attributeKeys.forEach(attributeKey -> Validator.validateString(attributeKey, k -> "Incorrect attribute key " + k));
//CacheExecutor for Redis or DirectExecutor for local Caffeine //CacheExecutor for Redis or DirectExecutor for local Caffeine
return Futures.transformAsync(cacheExecutor.submit(() -> findCachedAttributes(entityId, scope, attributeKeys)), return Futures.transformAsync(cacheExecutor.submit(() -> findCachedAttributes(entityId, scope, attributeKeys)),
@ -175,28 +157,19 @@ public class CachedAttributesService implements AttributesService {
// DB call should run in DB executor, not in cache-related executor // DB call should run in DB executor, not in cache-related executor
return jpaExecutorService.submit(() -> { return jpaExecutorService.submit(() -> {
var cacheTransaction = cache.newTransactionForKeys(notFoundKeys); log.trace("[{}][{}] Lookup attributes from db: {}", entityId, scope, notFoundAttributeKeys);
try { List<AttributeKvEntry> result = attributesDao.find(tenantId, entityId, scope, notFoundAttributeKeys);
log.trace("[{}][{}] Lookup attributes from db: {}", entityId, scope, notFoundAttributeKeys); for (AttributeKvEntry foundInDbAttribute : result) {
List<AttributeKvEntry> result = attributesDao.find(tenantId, entityId, scope, notFoundAttributeKeys); put(entityId, scope, foundInDbAttribute);
for (AttributeKvEntry foundInDbAttribute : result) { notFoundAttributeKeys.remove(foundInDbAttribute.getKey());
AttributeCacheKey attributeCacheKey = new AttributeCacheKey(scope, entityId, foundInDbAttribute.getKey()); }
cacheTransaction.putIfAbsent(attributeCacheKey, foundInDbAttribute); for (String key : notFoundAttributeKeys) {
notFoundAttributeKeys.remove(foundInDbAttribute.getKey()); cache.put(new AttributeCacheKey(scope, entityId, key), null);
}
for (String key : notFoundAttributeKeys) {
cacheTransaction.putIfAbsent(new AttributeCacheKey(scope, entityId, key), null);
}
List<AttributeKvEntry> mergedAttributes = new ArrayList<>(cachedAttributes);
mergedAttributes.addAll(result);
cacheTransaction.commit();
log.trace("[{}][{}] Commit cache transaction: {}", entityId, scope, notFoundAttributeKeys);
return mergedAttributes;
} catch (Throwable e) {
cacheTransaction.rollback();
log.debug("Could not find attributes from cache: [{}] [{}] [{}]", entityId, scope, notFoundAttributeKeys, e);
throw e;
} }
List<AttributeKvEntry> mergedAttributes = new ArrayList<>(cachedAttributes);
mergedAttributes.addAll(result);
log.trace("[{}][{}] Commit cache transaction: {}", entityId, scope, notFoundAttributeKeys);
return mergedAttributes;
}); });
}, MoreExecutors.directExecutor()); // cacheExecutor analyse and returns results or submit to DB executor }, MoreExecutors.directExecutor()); // cacheExecutor analyse and returns results or submit to DB executor
@ -216,11 +189,6 @@ public class CachedAttributesService implements AttributesService {
return cachedAttributes; return cachedAttributes;
} }
@Override
public ListenableFuture<List<AttributeKvEntry>> findAll(TenantId tenantId, EntityId entityId, String scope) {
return findAll(tenantId, entityId, AttributeScope.valueOf(scope));
}
@Override @Override
public ListenableFuture<List<AttributeKvEntry>> findAll(TenantId tenantId, EntityId entityId, AttributeScope scope) { public ListenableFuture<List<AttributeKvEntry>> findAll(TenantId tenantId, EntityId entityId, AttributeScope scope) {
validate(entityId, scope); validate(entityId, scope);
@ -233,11 +201,6 @@ public class CachedAttributesService implements AttributesService {
return attributesDao.findAllKeysByDeviceProfileId(tenantId, deviceProfileId); return attributesDao.findAllKeysByDeviceProfileId(tenantId, deviceProfileId);
} }
@Override
public List<String> findAllKeysByEntityIds(TenantId tenantId, EntityType entityType, List<EntityId> entityIds) {
return findAllKeysByEntityIds(tenantId, entityIds);
}
@Override @Override
public List<String> findAllKeysByEntityIds(TenantId tenantId, List<EntityId> entityIds) { public List<String> findAllKeysByEntityIds(TenantId tenantId, List<EntityId> entityIds) {
return attributesDao.findAllKeysByEntityIds(tenantId, entityIds); return attributesDao.findAllKeysByEntityIds(tenantId, entityIds);
@ -253,42 +216,43 @@ public class CachedAttributesService implements AttributesService {
} }
@Override @Override
public ListenableFuture<String> save(TenantId tenantId, EntityId entityId, String scope, AttributeKvEntry attribute) { public ListenableFuture<Long> save(TenantId tenantId, EntityId entityId, AttributeScope scope, AttributeKvEntry attribute) {
return save(tenantId, entityId, AttributeScope.valueOf(scope), attribute);
}
@Override
public ListenableFuture<String> save(TenantId tenantId, EntityId entityId, AttributeScope scope, AttributeKvEntry attribute) {
validate(entityId, scope); validate(entityId, scope);
AttributeUtils.validate(attribute, valueNoXssValidation); AttributeUtils.validate(attribute, valueNoXssValidation);
ListenableFuture<String> future = attributesDao.save(tenantId, entityId, scope, attribute); return doSave(tenantId, entityId, scope, attribute);
return Futures.transform(future, key -> evict(entityId, scope, attribute, key), cacheExecutor);
} }
@Override @Override
public ListenableFuture<List<String>> save(TenantId tenantId, EntityId entityId, String scope, List<AttributeKvEntry> attributes) { public ListenableFuture<List<Long>> save(TenantId tenantId, EntityId entityId, String scope, List<AttributeKvEntry> attributes) {
return save(tenantId, entityId, AttributeScope.valueOf(scope), attributes); return save(tenantId, entityId, AttributeScope.valueOf(scope), attributes);
} }
@Override @Override
public ListenableFuture<List<String>> save(TenantId tenantId, EntityId entityId, AttributeScope scope, List<AttributeKvEntry> attributes) { public ListenableFuture<List<Long>> save(TenantId tenantId, EntityId entityId, AttributeScope scope, List<AttributeKvEntry> attributes) {
validate(entityId, scope); validate(entityId, scope);
AttributeUtils.validate(attributes, valueNoXssValidation); AttributeUtils.validate(attributes, valueNoXssValidation);
List<ListenableFuture<String>> futures = new ArrayList<>(attributes.size()); List<ListenableFuture<Long>> futures = new ArrayList<>(attributes.size());
for (var attribute : attributes) { for (var attribute : attributes) {
ListenableFuture<String> future = attributesDao.save(tenantId, entityId, scope, attribute); futures.add(doSave(tenantId, entityId, scope, attribute));
futures.add(Futures.transform(future, key -> evict(entityId, scope, attribute, key), cacheExecutor));
} }
return Futures.allAsList(futures); return Futures.allAsList(futures);
} }
private String evict(EntityId entityId, AttributeScope scope, AttributeKvEntry attribute, String key) { private ListenableFuture<Long> doSave(TenantId tenantId, EntityId entityId, AttributeScope scope, AttributeKvEntry attribute) {
log.trace("[{}][{}][{}] Before cache evict: {}", entityId, scope, key, attribute); ListenableFuture<Long> future = attributesDao.save(tenantId, entityId, scope, attribute);
cache.evictOrPut(new AttributeCacheKey(scope, entityId, key), attribute); return Futures.transform(future, version -> {
log.trace("[{}][{}][{}] after cache evict.", entityId, scope, key); put(entityId, scope, new BaseAttributeKvEntry(((BaseAttributeKvEntry)attribute).getKv(), attribute.getLastUpdateTs(), version));
return key; return version;
}, cacheExecutor);
}
private void put(EntityId entityId, AttributeScope scope, AttributeKvEntry attribute) {
String key = attribute.getKey();
log.trace("[{}][{}][{}] Before cache put: {}", entityId, scope, key, attribute);
cache.put(new AttributeCacheKey(scope, entityId, key), attribute);
log.trace("[{}][{}][{}] after cache put.", entityId, scope, key);
} }
@Override @Override
@ -299,9 +263,10 @@ public class CachedAttributesService implements AttributesService {
@Override @Override
public ListenableFuture<List<String>> removeAll(TenantId tenantId, EntityId entityId, AttributeScope scope, List<String> attributeKeys) { public ListenableFuture<List<String>> removeAll(TenantId tenantId, EntityId entityId, AttributeScope scope, List<String> attributeKeys) {
validate(entityId, scope); validate(entityId, scope);
List<ListenableFuture<String>> futures = attributesDao.removeAll(tenantId, entityId, scope, attributeKeys); List<ListenableFuture<TbPair<String, Long>>> futures = attributesDao.removeAllWithVersions(tenantId, entityId, scope, attributeKeys);
return Futures.allAsList(futures.stream().map(future -> Futures.transform(future, key -> { return Futures.allAsList(futures.stream().map(future -> Futures.transform(future, keyVersionPair -> {
cache.evict(new AttributeCacheKey(scope, entityId, key)); String key = keyVersionPair.getFirst();
cache.evict(new AttributeCacheKey(scope, entityId, key), keyVersionPair.getSecond());
return key; return key;
}, cacheExecutor)).collect(Collectors.toList())); }, cacheExecutor)).collect(Collectors.toList()));
} }

20
dao/src/main/java/org/thingsboard/server/dao/model/BaseVersionedEntity.java

@ -0,0 +1,20 @@
/**
* Copyright © 2016-2024 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.model;
public interface BaseVersionedEntity {
long getVersion();
}

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

@ -56,6 +56,7 @@ public class ModelConstants {
public static final String ATTRIBUTE_TYPE_COLUMN = "attribute_type"; public static final String ATTRIBUTE_TYPE_COLUMN = "attribute_type";
public static final String ATTRIBUTE_KEY_COLUMN = "attribute_key"; public static final String ATTRIBUTE_KEY_COLUMN = "attribute_key";
public static final String LAST_UPDATE_TS_COLUMN = "last_update_ts"; public static final String LAST_UPDATE_TS_COLUMN = "last_update_ts";
public static final String VERSION_COLUMN = "version";
/** /**
* User constants. * User constants.

7
dao/src/main/java/org/thingsboard/server/dao/model/sql/AbstractTsKvEntity.java

@ -119,10 +119,13 @@ public abstract class AbstractTsKvEntity implements ToData<TsKvEntry> {
} }
if (aggValuesCount == null) { if (aggValuesCount == null) {
return new BasicTsKvEntry(ts, kvEntry); return new BasicTsKvEntry(ts, kvEntry, getVersion());
} else { } else {
return new AggTsKvEntry(ts, kvEntry, aggValuesCount); return new AggTsKvEntry(ts, kvEntry, aggValuesCount);
} }
} }
} public Long getVersion() {
return null;
}
}

6
dao/src/main/java/org/thingsboard/server/dao/model/sql/AttributeKvEntity.java

@ -39,6 +39,7 @@ import static org.thingsboard.server.dao.model.ModelConstants.JSON_VALUE_COLUMN;
import static org.thingsboard.server.dao.model.ModelConstants.LAST_UPDATE_TS_COLUMN; import static org.thingsboard.server.dao.model.ModelConstants.LAST_UPDATE_TS_COLUMN;
import static org.thingsboard.server.dao.model.ModelConstants.LONG_VALUE_COLUMN; import static org.thingsboard.server.dao.model.ModelConstants.LONG_VALUE_COLUMN;
import static org.thingsboard.server.dao.model.ModelConstants.STRING_VALUE_COLUMN; import static org.thingsboard.server.dao.model.ModelConstants.STRING_VALUE_COLUMN;
import static org.thingsboard.server.dao.model.ModelConstants.VERSION_COLUMN;
@Data @Data
@Entity @Entity
@ -66,6 +67,9 @@ public class AttributeKvEntity implements ToData<AttributeKvEntry>, Serializable
@Column(name = LAST_UPDATE_TS_COLUMN) @Column(name = LAST_UPDATE_TS_COLUMN)
private Long lastUpdateTs; private Long lastUpdateTs;
@Column(name = VERSION_COLUMN)
private Long version;
@Transient @Transient
protected String strKey; protected String strKey;
@ -84,6 +88,6 @@ public class AttributeKvEntity implements ToData<AttributeKvEntry>, Serializable
kvEntry = new JsonDataEntry(strKey, jsonValue); kvEntry = new JsonDataEntry(strKey, jsonValue);
} }
return new BaseAttributeKvEntry(kvEntry, lastUpdateTs); return new BaseAttributeKvEntry(kvEntry, lastUpdateTs, version);
} }
} }

5
dao/src/main/java/org/thingsboard/server/dao/model/sql/RelationEntity.java

@ -39,6 +39,7 @@ import static org.thingsboard.server.dao.model.ModelConstants.RELATION_TO_ID_PRO
import static org.thingsboard.server.dao.model.ModelConstants.RELATION_TO_TYPE_PROPERTY; import static org.thingsboard.server.dao.model.ModelConstants.RELATION_TO_TYPE_PROPERTY;
import static org.thingsboard.server.dao.model.ModelConstants.RELATION_TYPE_GROUP_PROPERTY; import static org.thingsboard.server.dao.model.ModelConstants.RELATION_TYPE_GROUP_PROPERTY;
import static org.thingsboard.server.dao.model.ModelConstants.RELATION_TYPE_PROPERTY; import static org.thingsboard.server.dao.model.ModelConstants.RELATION_TYPE_PROPERTY;
import static org.thingsboard.server.dao.model.ModelConstants.VERSION_COLUMN;
@Data @Data
@Entity @Entity
@ -70,6 +71,9 @@ public final class RelationEntity implements ToData<EntityRelation> {
@Column(name = RELATION_TYPE_PROPERTY) @Column(name = RELATION_TYPE_PROPERTY)
private String relationType; private String relationType;
@Column(name = VERSION_COLUMN)
private Long version;
@Convert(converter = JsonConverter.class) @Convert(converter = JsonConverter.class)
@Column(name = ADDITIONAL_INFO_PROPERTY) @Column(name = ADDITIONAL_INFO_PROPERTY)
private JsonNode additionalInfo; private JsonNode additionalInfo;
@ -103,6 +107,7 @@ public final class RelationEntity implements ToData<EntityRelation> {
} }
relation.setType(relationType); relation.setType(relationType);
relation.setTypeGroup(RelationTypeGroup.valueOf(relationTypeGroup)); relation.setTypeGroup(RelationTypeGroup.valueOf(relationTypeGroup));
relation.setVersion(version);
relation.setAdditionalInfo(additionalInfo); relation.setAdditionalInfo(additionalInfo);
return relation; return relation;
} }

11
dao/src/main/java/org/thingsboard/server/dao/model/sqlts/latest/TsKvLatestEntity.java

@ -15,6 +15,7 @@
*/ */
package org.thingsboard.server.dao.model.sqlts.latest; package org.thingsboard.server.dao.model.sqlts.latest;
import jakarta.persistence.Column;
import jakarta.persistence.ColumnResult; import jakarta.persistence.ColumnResult;
import jakarta.persistence.ConstructorResult; import jakarta.persistence.ConstructorResult;
import jakarta.persistence.Entity; import jakarta.persistence.Entity;
@ -30,6 +31,8 @@ import org.thingsboard.server.dao.sqlts.latest.SearchTsKvLatestRepository;
import java.util.UUID; import java.util.UUID;
import static org.thingsboard.server.dao.model.ModelConstants.VERSION_COLUMN;
@Data @Data
@Entity @Entity
@Table(name = "ts_kv_latest") @Table(name = "ts_kv_latest")
@ -50,7 +53,7 @@ import java.util.UUID;
@ColumnResult(name = "doubleValue", type = Double.class), @ColumnResult(name = "doubleValue", type = Double.class),
@ColumnResult(name = "jsonValue", type = String.class), @ColumnResult(name = "jsonValue", type = String.class),
@ColumnResult(name = "ts", type = Long.class), @ColumnResult(name = "ts", type = Long.class),
@ColumnResult(name = "version", type = Long.class)
} }
), ),
}) })
@ -65,6 +68,9 @@ import java.util.UUID;
}) })
public final class TsKvLatestEntity extends AbstractTsKvEntity { public final class TsKvLatestEntity extends AbstractTsKvEntity {
@Column(name = VERSION_COLUMN)
private Long version;
@Override @Override
public boolean isNotEmpty() { public boolean isNotEmpty() {
return strValue != null || longValue != null || doubleValue != null || booleanValue != null || jsonValue != null; return strValue != null || longValue != null || doubleValue != null || booleanValue != null || jsonValue != null;
@ -73,7 +79,7 @@ public final class TsKvLatestEntity extends AbstractTsKvEntity {
public TsKvLatestEntity() { public TsKvLatestEntity() {
} }
public TsKvLatestEntity(UUID entityId, Integer key, String strKey, String strValue, Boolean boolValue, Long longValue, Double doubleValue, String jsonValue, Long ts) { public TsKvLatestEntity(UUID entityId, Integer key, String strKey, String strValue, Boolean boolValue, Long longValue, Double doubleValue, String jsonValue, Long ts, Long version) {
this.entityId = entityId; this.entityId = entityId;
this.key = key; this.key = key;
this.ts = ts; this.ts = ts;
@ -83,5 +89,6 @@ public final class TsKvLatestEntity extends AbstractTsKvEntity {
this.booleanValue = boolValue; this.booleanValue = boolValue;
this.jsonValue = jsonValue; this.jsonValue = jsonValue;
this.strKey = strKey; this.strKey = strKey;
this.version = version;
} }
} }

123
dao/src/main/java/org/thingsboard/server/dao/relation/BaseRelationService.java

@ -28,7 +28,6 @@ import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Value; import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.ApplicationEventPublisher; import org.springframework.context.ApplicationEventPublisher;
import org.springframework.context.annotation.Lazy; import org.springframework.context.annotation.Lazy;
import org.springframework.dao.ConcurrencyFailureException;
import org.springframework.stereotype.Service; import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional; import org.springframework.transaction.annotation.Transactional;
import org.springframework.transaction.event.TransactionalEventListener; import org.springframework.transaction.event.TransactionalEventListener;
@ -149,16 +148,16 @@ public class BaseRelationService implements RelationService {
return relationDao.getRelation(tenantId, from, to, relationType, typeGroup); return relationDao.getRelation(tenantId, from, to, relationType, typeGroup);
}, },
RelationCacheValue::getRelation, RelationCacheValue::getRelation,
relations -> RelationCacheValue.builder().relation(relations).build(), false); relation -> RelationCacheValue.builder().relation(relation).build(), false);
} }
@Override @Override
public boolean saveRelation(TenantId tenantId, EntityRelation relation) { public EntityRelation saveRelation(TenantId tenantId, EntityRelation relation) {
log.trace("Executing saveRelation [{}]", relation); log.trace("Executing saveRelation [{}]", relation);
validate(relation); validate(relation);
var result = relationDao.saveRelation(tenantId, relation); var result = relationDao.saveRelation(tenantId, relation);
publishEvictEvent(EntityRelationEvent.from(relation)); publishEvictEvent(EntityRelationEvent.from(result));
eventPublisher.publishEvent(new RelationActionEvent(tenantId, relation, ActionType.RELATION_ADD_OR_UPDATE)); eventPublisher.publishEvent(new RelationActionEvent(tenantId, result, ActionType.RELATION_ADD_OR_UPDATE));
return result; return result;
} }
@ -168,10 +167,11 @@ public class BaseRelationService implements RelationService {
for (EntityRelation relation : relations) { for (EntityRelation relation : relations) {
validate(relation); validate(relation);
} }
List<EntityRelation> savedRelations = new ArrayList<>(relations.size());
for (List<EntityRelation> partition : Lists.partition(relations, 1024)) { for (List<EntityRelation> partition : Lists.partition(relations, 1024)) {
relationDao.saveRelations(tenantId, partition); savedRelations.addAll(relationDao.saveRelations(tenantId, partition));
} }
for (EntityRelation relation : relations) { for (EntityRelation relation : savedRelations) {
publishEvictEvent(EntityRelationEvent.from(relation)); publishEvictEvent(EntityRelationEvent.from(relation));
eventPublisher.publishEvent(new RelationActionEvent(tenantId, relation, ActionType.RELATION_ADD_OR_UPDATE)); eventPublisher.publishEvent(new RelationActionEvent(tenantId, relation, ActionType.RELATION_ADD_OR_UPDATE));
} }
@ -182,11 +182,13 @@ public class BaseRelationService implements RelationService {
log.trace("Executing saveRelationAsync [{}]", relation); log.trace("Executing saveRelationAsync [{}]", relation);
validate(relation); validate(relation);
var future = relationDao.saveRelationAsync(tenantId, relation); var future = relationDao.saveRelationAsync(tenantId, relation);
future.addListener(() -> { return Futures.transform(future, savedRelation -> {
handleEvictEvent(EntityRelationEvent.from(relation)); if (savedRelation != null) {
eventPublisher.publishEvent(new RelationActionEvent(tenantId, relation, ActionType.RELATION_ADD_OR_UPDATE)); handleEvictEvent(EntityRelationEvent.from(savedRelation));
eventPublisher.publishEvent(new RelationActionEvent(tenantId, savedRelation, ActionType.RELATION_ADD_OR_UPDATE));
}
return savedRelation != null;
}, MoreExecutors.directExecutor()); }, MoreExecutors.directExecutor());
return future;
} }
@Override @Override
@ -194,10 +196,11 @@ public class BaseRelationService implements RelationService {
log.trace("Executing DeleteRelation [{}]", relation); log.trace("Executing DeleteRelation [{}]", relation);
validate(relation); validate(relation);
var result = relationDao.deleteRelation(tenantId, relation); var result = relationDao.deleteRelation(tenantId, relation);
//TODO: evict cache only if the relation was deleted. Note: relationDao.deleteRelation requires improvement. if (result != null) {
publishEvictEvent(EntityRelationEvent.from(relation)); publishEvictEvent(EntityRelationEvent.from(result));
eventPublisher.publishEvent(new RelationActionEvent(tenantId, relation, ActionType.RELATION_DELETED)); eventPublisher.publishEvent(new RelationActionEvent(tenantId, result, ActionType.RELATION_DELETED));
return result; }
return result != null;
} }
@Override @Override
@ -205,22 +208,24 @@ public class BaseRelationService implements RelationService {
log.trace("Executing deleteRelationAsync [{}]", relation); log.trace("Executing deleteRelationAsync [{}]", relation);
validate(relation); validate(relation);
var future = relationDao.deleteRelationAsync(tenantId, relation); var future = relationDao.deleteRelationAsync(tenantId, relation);
future.addListener(() -> { return Futures.transform(future, deletedRelation -> {
handleEvictEvent(EntityRelationEvent.from(relation)); if (deletedRelation != null) {
eventPublisher.publishEvent(new RelationActionEvent(tenantId, relation, ActionType.RELATION_DELETED)); handleEvictEvent(EntityRelationEvent.from(deletedRelation));
eventPublisher.publishEvent(new RelationActionEvent(tenantId, deletedRelation, ActionType.RELATION_DELETED));
}
return deletedRelation != null;
}, MoreExecutors.directExecutor()); }, MoreExecutors.directExecutor());
return future;
} }
@Override @Override
public boolean deleteRelation(TenantId tenantId, EntityId from, EntityId to, String relationType, RelationTypeGroup typeGroup) { public EntityRelation deleteRelation(TenantId tenantId, EntityId from, EntityId to, String relationType, RelationTypeGroup typeGroup) {
log.trace("Executing deleteRelation [{}][{}][{}][{}]", from, to, relationType, typeGroup); log.trace("Executing deleteRelation [{}][{}][{}][{}]", from, to, relationType, typeGroup);
validate(from, to, relationType, typeGroup); validate(from, to, relationType, typeGroup);
var result = relationDao.deleteRelation(tenantId, from, to, relationType, typeGroup); var result = relationDao.deleteRelation(tenantId, from, to, relationType, typeGroup);
//TODO: evict cache only if the relation was deleted. Note: relationDao.deleteRelation requires improvement. if (result != null) {
EntityRelation entityRelation = new EntityRelation(from, to, relationType, typeGroup); publishEvictEvent(EntityRelationEvent.from(result));
publishEvictEvent(EntityRelationEvent.from(entityRelation)); eventPublisher.publishEvent(new RelationActionEvent(tenantId, result, ActionType.RELATION_DELETED));
eventPublisher.publishEvent(new RelationActionEvent(tenantId, entityRelation, ActionType.RELATION_DELETED)); }
return result; return result;
} }
@ -229,9 +234,12 @@ public class BaseRelationService implements RelationService {
log.trace("Executing deleteRelationAsync [{}][{}][{}][{}]", from, to, relationType, typeGroup); log.trace("Executing deleteRelationAsync [{}][{}][{}][{}]", from, to, relationType, typeGroup);
validate(from, to, relationType, typeGroup); validate(from, to, relationType, typeGroup);
var future = relationDao.deleteRelationAsync(tenantId, from, to, relationType, typeGroup); var future = relationDao.deleteRelationAsync(tenantId, from, to, relationType, typeGroup);
EntityRelationEvent event = new EntityRelationEvent(from, to, relationType, typeGroup); return Futures.transform(future, deletedEvent -> {
future.addListener(() -> handleEvictEvent(event), MoreExecutors.directExecutor()); if (deletedEvent != null) {
return future; handleEvictEvent(EntityRelationEvent.from(deletedEvent));
}
return deletedEvent != null;
}, MoreExecutors.directExecutor());
} }
@Transactional @Transactional
@ -250,60 +258,27 @@ public class BaseRelationService implements RelationService {
public void deleteEntityRelations(TenantId tenantId, EntityId entityId, RelationTypeGroup relationTypeGroup) { public void deleteEntityRelations(TenantId tenantId, EntityId entityId, RelationTypeGroup relationTypeGroup) {
log.trace("Executing deleteEntityRelations [{}]", entityId); log.trace("Executing deleteEntityRelations [{}]", entityId);
validate(entityId); validate(entityId);
List<EntityRelation> inboundRelations = relationTypeGroup == null
? relationDao.findAllByTo(tenantId, entityId)
: relationDao.findAllByTo(tenantId, entityId, relationTypeGroup);
List<EntityRelation> outboundRelations = relationTypeGroup == null
? relationDao.findAllByFrom(tenantId, entityId)
: relationDao.findAllByFrom(tenantId, entityId, relationTypeGroup);
if (!inboundRelations.isEmpty()) {
try {
if (relationTypeGroup == null) {
relationDao.deleteInboundRelations(tenantId, entityId);
} else {
relationDao.deleteInboundRelations(tenantId, entityId, relationTypeGroup);
}
} catch (ConcurrencyFailureException e) {
log.debug("Concurrency exception while deleting relations [{}]", inboundRelations, e);
}
for (EntityRelation relation : inboundRelations) { List<EntityRelation> inboundRelations;
eventPublisher.publishEvent(EntityRelationEvent.from(relation)); if (relationTypeGroup == null) {
} inboundRelations = relationDao.deleteInboundRelations(tenantId, entityId);
} else {
inboundRelations = relationDao.deleteInboundRelations(tenantId, entityId, relationTypeGroup);
} }
if (!outboundRelations.isEmpty()) { for (EntityRelation relation : inboundRelations) {
if (relationTypeGroup == null) { eventPublisher.publishEvent(EntityRelationEvent.from(relation));
relationDao.deleteOutboundRelations(tenantId, entityId);
} else {
relationDao.deleteOutboundRelations(tenantId, entityId, relationTypeGroup);
}
for (EntityRelation relation : outboundRelations) {
eventPublisher.publishEvent(EntityRelationEvent.from(relation));
}
} }
}
private List<ListenableFuture<Boolean>> deleteRelationGroupsAsync(TenantId tenantId, List<List<EntityRelation>> relations, boolean deleteFromDb) { List<EntityRelation> outboundRelations;
List<ListenableFuture<Boolean>> results = new ArrayList<>(); if (relationTypeGroup == null) {
for (List<EntityRelation> relationList : relations) { outboundRelations = relationDao.deleteOutboundRelations(tenantId, entityId);
relationList.forEach(relation -> results.add(deleteAsync(tenantId, relation, deleteFromDb))); } else {
outboundRelations = relationDao.deleteOutboundRelations(tenantId, entityId, relationTypeGroup);
} }
return results;
}
private ListenableFuture<Boolean> deleteAsync(TenantId tenantId, EntityRelation relation, boolean deleteFromDb) { for (EntityRelation relation : outboundRelations) {
if (deleteFromDb) { eventPublisher.publishEvent(EntityRelationEvent.from(relation));
return Futures.transform(relationDao.deleteRelationAsync(tenantId, relation),
bool -> {
handleEvictEvent(EntityRelationEvent.from(relation));
return bool;
}, MoreExecutors.directExecutor());
} else {
handleEvictEvent(EntityRelationEvent.from(relation));
return Futures.immediateFuture(false);
} }
} }

24
dao/src/main/java/org/thingsboard/server/dao/relation/RelationDao.java

@ -48,29 +48,27 @@ public interface RelationDao {
EntityRelation getRelation(TenantId tenantId, EntityId from, EntityId to, String relationType, RelationTypeGroup typeGroup); EntityRelation getRelation(TenantId tenantId, EntityId from, EntityId to, String relationType, RelationTypeGroup typeGroup);
boolean saveRelation(TenantId tenantId, EntityRelation relation); EntityRelation saveRelation(TenantId tenantId, EntityRelation relation);
void saveRelations(TenantId tenantId, Collection<EntityRelation> relations); List<EntityRelation> saveRelations(TenantId tenantId, List<EntityRelation> relations);
ListenableFuture<Boolean> saveRelationAsync(TenantId tenantId, EntityRelation relation); ListenableFuture<EntityRelation> saveRelationAsync(TenantId tenantId, EntityRelation relation);
boolean deleteRelation(TenantId tenantId, EntityRelation relation); EntityRelation deleteRelation(TenantId tenantId, EntityRelation relation);
ListenableFuture<Boolean> deleteRelationAsync(TenantId tenantId, EntityRelation relation); ListenableFuture<EntityRelation> deleteRelationAsync(TenantId tenantId, EntityRelation relation);
boolean deleteRelation(TenantId tenantId, EntityId from, EntityId to, String relationType, RelationTypeGroup typeGroup); EntityRelation deleteRelation(TenantId tenantId, EntityId from, EntityId to, String relationType, RelationTypeGroup typeGroup);
ListenableFuture<Boolean> deleteRelationAsync(TenantId tenantId, EntityId from, EntityId to, String relationType, RelationTypeGroup typeGroup); ListenableFuture<EntityRelation> deleteRelationAsync(TenantId tenantId, EntityId from, EntityId to, String relationType, RelationTypeGroup typeGroup);
void deleteOutboundRelations(TenantId tenantId, EntityId entity); List<EntityRelation> deleteOutboundRelations(TenantId tenantId, EntityId entity);
void deleteOutboundRelations(TenantId tenantId, EntityId entity, RelationTypeGroup relationTypeGroup); List<EntityRelation> deleteOutboundRelations(TenantId tenantId, EntityId entity, RelationTypeGroup relationTypeGroup);
void deleteInboundRelations(TenantId tenantId, EntityId entity); List<EntityRelation> deleteInboundRelations(TenantId tenantId, EntityId entity);
void deleteInboundRelations(TenantId tenantId, EntityId entity, RelationTypeGroup relationTypeGroup); List<EntityRelation> deleteInboundRelations(TenantId tenantId, EntityId entity, RelationTypeGroup relationTypeGroup);
ListenableFuture<Boolean> deleteOutboundRelationsAsync(TenantId tenantId, EntityId entity);
List<EntityRelation> findRuleNodeToRuleChainRelations(RuleChainType ruleChainType, int limit); List<EntityRelation> findRuleNodeToRuleChainRelations(RuleChainType ruleChainType, int limit);
} }

4
dao/src/main/java/org/thingsboard/server/dao/sql/JpaAbstractDaoListeningExecutorService.java

@ -18,6 +18,7 @@ package org.thingsboard.server.dao.sql;
import lombok.extern.slf4j.Slf4j; import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.jdbc.core.JdbcTemplate; import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.transaction.support.TransactionTemplate;
import javax.sql.DataSource; import javax.sql.DataSource;
import java.sql.SQLException; import java.sql.SQLException;
@ -36,6 +37,9 @@ public abstract class JpaAbstractDaoListeningExecutorService {
@Autowired @Autowired
protected JdbcTemplate jdbcTemplate; protected JdbcTemplate jdbcTemplate;
@Autowired
protected TransactionTemplate transactionTemplate;
protected void printWarnings(Statement statement) throws SQLException { protected void printWarnings(Statement statement) throws SQLException {
SQLWarning warnings = statement.getWarnings(); SQLWarning warnings = statement.getWarnings();
if (warnings != null) { if (warnings != null) {

47
dao/src/main/java/org/thingsboard/server/dao/sql/TbSqlBlockingQueue.java

@ -19,6 +19,7 @@ import com.google.common.util.concurrent.ListenableFuture;
import com.google.common.util.concurrent.SettableFuture; import com.google.common.util.concurrent.SettableFuture;
import lombok.extern.slf4j.Slf4j; import lombok.extern.slf4j.Slf4j;
import org.thingsboard.common.util.ThingsBoardThreadFactory; import org.thingsboard.common.util.ThingsBoardThreadFactory;
import org.thingsboard.server.common.data.util.CollectionsUtil;
import org.thingsboard.server.common.stats.MessagesStats; import org.thingsboard.server.common.stats.MessagesStats;
import java.util.ArrayList; import java.util.ArrayList;
@ -29,14 +30,13 @@ import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors; import java.util.concurrent.Executors;
import java.util.concurrent.LinkedBlockingQueue; import java.util.concurrent.LinkedBlockingQueue;
import java.util.concurrent.TimeUnit; import java.util.concurrent.TimeUnit;
import java.util.function.Consumer; import java.util.function.Function;
import java.util.stream.Collectors; import java.util.stream.Collectors;
import java.util.stream.Stream;
@Slf4j @Slf4j
public class TbSqlBlockingQueue<E> implements TbSqlQueue<E> { public class TbSqlBlockingQueue<E, R> implements TbSqlQueue<E, R> {
private final BlockingQueue<TbSqlQueueElement<E>> queue = new LinkedBlockingQueue<>(); private final BlockingQueue<TbSqlQueueElement<E, R>> queue = new LinkedBlockingQueue<>();
private final TbSqlBlockingQueueParams params; private final TbSqlBlockingQueueParams params;
private ExecutorService executor; private ExecutorService executor;
@ -48,17 +48,17 @@ public class TbSqlBlockingQueue<E> implements TbSqlQueue<E> {
} }
@Override @Override
public void init(ScheduledLogExecutorComponent logExecutor, Consumer<List<E>> saveFunction, Comparator<E> batchUpdateComparator, int index) { public void init(ScheduledLogExecutorComponent logExecutor, Function<List<E>, List<R>> saveFunction, Comparator<E> batchUpdateComparator, Function<List<TbSqlQueueElement<E, R>>, List<TbSqlQueueElement<E, R>>> filter, int index) {
executor = Executors.newSingleThreadExecutor(ThingsBoardThreadFactory.forName("sql-queue-" + index + "-" + params.getLogName().toLowerCase())); executor = Executors.newSingleThreadExecutor(ThingsBoardThreadFactory.forName("sql-queue-" + index + "-" + params.getLogName().toLowerCase()));
executor.submit(() -> { executor.submit(() -> {
String logName = params.getLogName(); String logName = params.getLogName();
int batchSize = params.getBatchSize(); int batchSize = params.getBatchSize();
long maxDelay = params.getMaxDelay(); long maxDelay = params.getMaxDelay();
final List<TbSqlQueueElement<E>> entities = new ArrayList<>(batchSize); final List<TbSqlQueueElement<E, R>> entities = new ArrayList<>(batchSize);
while (!Thread.interrupted()) { while (!Thread.interrupted()) {
try { try {
long currentTs = System.currentTimeMillis(); long currentTs = System.currentTimeMillis();
TbSqlQueueElement<E> attr = queue.poll(maxDelay, TimeUnit.MILLISECONDS); TbSqlQueueElement<E, R> attr = queue.poll(maxDelay, TimeUnit.MILLISECONDS);
if (attr == null) { if (attr == null) {
continue; continue;
} else { } else {
@ -70,12 +70,27 @@ public class TbSqlBlockingQueue<E> implements TbSqlQueue<E> {
log.debug("[{}] Going to save {} entities", logName, entities.size()); log.debug("[{}] Going to save {} entities", logName, entities.size());
log.trace("[{}] Going to save entities: {}", logName, entities); log.trace("[{}] Going to save entities: {}", logName, entities);
} }
Stream<E> entitiesStream = entities.stream().map(TbSqlQueueElement::getEntity);
saveFunction.accept( List<TbSqlQueueElement<E, R>> entitiesToSave = filter.apply(entities);
(params.isBatchSortEnabled() ? entitiesStream.sorted(batchUpdateComparator) : entitiesStream)
.collect(Collectors.toList()) if (params.isBatchSortEnabled()) {
); entitiesToSave = entitiesToSave.stream().sorted((o1, o2) -> batchUpdateComparator.compare(o1.getEntity(), o2.getEntity())).toList();
entities.forEach(v -> v.getFuture().set(null)); }
List<R> result = saveFunction.apply(entitiesToSave.stream().map(TbSqlQueueElement::getEntity).collect(Collectors.toList()));
if (params.isWithResponse()) {
for (int i = 0; i < entitiesToSave.size(); i++) {
entitiesToSave.get(i).getFuture().set(result.get(i));
}
if (entities.size() > entitiesToSave.size()) {
CollectionsUtil.diffLists(entitiesToSave, entities).forEach(v -> v.getFuture().set(null));
}
} else {
entities.forEach(v -> v.getFuture().set(null));
}
stats.incrementSuccessful(entities.size()); stats.incrementSuccessful(entities.size());
if (!fullPack) { if (!fullPack) {
long remainingDelay = maxDelay - (System.currentTimeMillis() - currentTs); long remainingDelay = maxDelay - (System.currentTimeMillis() - currentTs);
@ -104,7 +119,7 @@ public class TbSqlBlockingQueue<E> implements TbSqlQueue<E> {
}); });
logExecutor.scheduleAtFixedRate(() -> { logExecutor.scheduleAtFixedRate(() -> {
if (queue.size() > 0 || stats.getTotal() > 0 || stats.getSuccessful() > 0 || stats.getFailed() > 0) { if (!queue.isEmpty() || stats.getTotal() > 0 || stats.getSuccessful() > 0 || stats.getFailed() > 0) {
log.info("Queue-{} [{}] queueSize [{}] totalAdded [{}] totalSaved [{}] totalFailed [{}]", index, log.info("Queue-{} [{}] queueSize [{}] totalAdded [{}] totalSaved [{}] totalFailed [{}]", index,
params.getLogName(), queue.size(), stats.getTotal(), stats.getSuccessful(), stats.getFailed()); params.getLogName(), queue.size(), stats.getTotal(), stats.getSuccessful(), stats.getFailed());
stats.reset(); stats.reset();
@ -120,8 +135,8 @@ public class TbSqlBlockingQueue<E> implements TbSqlQueue<E> {
} }
@Override @Override
public ListenableFuture<Void> add(E element) { public ListenableFuture<R> add(E element) {
SettableFuture<Void> future = SettableFuture.create(); SettableFuture<R> future = SettableFuture.create();
queue.add(new TbSqlQueueElement<>(future, element)); queue.add(new TbSqlQueueElement<>(future, element));
stats.incrementTotal(); stats.incrementTotal();
return future; return future;

1
dao/src/main/java/org/thingsboard/server/dao/sql/TbSqlBlockingQueueParams.java

@ -30,4 +30,5 @@ public class TbSqlBlockingQueueParams {
private final long statsPrintIntervalMs; private final long statsPrintIntervalMs;
private final String statsNamePrefix; private final String statsNamePrefix;
private final boolean batchSortEnabled; private final boolean batchSortEnabled;
private final boolean withResponse;
} }

15
dao/src/main/java/org/thingsboard/server/dao/sql/TbSqlBlockingQueueWrapper.java

@ -29,10 +29,9 @@ import java.util.function.Function;
@Slf4j @Slf4j
@Data @Data
public class TbSqlBlockingQueueWrapper<E> { public class TbSqlBlockingQueueWrapper<E, R> {
private final CopyOnWriteArrayList<TbSqlBlockingQueue<E>> queues = new CopyOnWriteArrayList<>(); private final CopyOnWriteArrayList<TbSqlBlockingQueue<E, R>> queues = new CopyOnWriteArrayList<>();
private final TbSqlBlockingQueueParams params; private final TbSqlBlockingQueueParams params;
private ScheduledLogExecutorComponent logExecutor;
private final Function<E, Integer> hashCodeFunction; private final Function<E, Integer> hashCodeFunction;
private final int maxThreads; private final int maxThreads;
private final StatsFactory statsFactory; private final StatsFactory statsFactory;
@ -46,15 +45,19 @@ public class TbSqlBlockingQueueWrapper<E> {
* NOTE: you must use all of primary key parts in your comparator * NOTE: you must use all of primary key parts in your comparator
*/ */
public void init(ScheduledLogExecutorComponent logExecutor, Consumer<List<E>> saveFunction, Comparator<E> batchUpdateComparator) { public void init(ScheduledLogExecutorComponent logExecutor, Consumer<List<E>> saveFunction, Comparator<E> batchUpdateComparator) {
init(logExecutor, l -> { saveFunction.accept(l); return null; }, batchUpdateComparator, l -> l);
}
public void init(ScheduledLogExecutorComponent logExecutor, Function<List<E>, List<R>> saveFunction, Comparator<E> batchUpdateComparator, Function<List<TbSqlQueueElement<E, R>>, List<TbSqlQueueElement<E, R>>> filter) {
for (int i = 0; i < maxThreads; i++) { for (int i = 0; i < maxThreads; i++) {
MessagesStats stats = statsFactory.createMessagesStats(params.getStatsNamePrefix() + ".queue." + i); MessagesStats stats = statsFactory.createMessagesStats(params.getStatsNamePrefix() + ".queue." + i);
TbSqlBlockingQueue<E> queue = new TbSqlBlockingQueue<>(params, stats); TbSqlBlockingQueue<E, R> queue = new TbSqlBlockingQueue<>(params, stats);
queues.add(queue); queues.add(queue);
queue.init(logExecutor, saveFunction, batchUpdateComparator, i); queue.init(logExecutor, saveFunction, batchUpdateComparator, filter, i);
} }
} }
public ListenableFuture<Void> add(E element) { public ListenableFuture<R> add(E element) {
int queueIndex = element != null ? (hashCodeFunction.apply(element) & 0x7FFFFFFF) % maxThreads : 0; int queueIndex = element != null ? (hashCodeFunction.apply(element) & 0x7FFFFFFF) % maxThreads : 0;
return queues.get(queueIndex).add(element); return queues.get(queueIndex).add(element);
} }

8
dao/src/main/java/org/thingsboard/server/dao/sql/TbSqlQueue.java

@ -19,13 +19,13 @@ import com.google.common.util.concurrent.ListenableFuture;
import java.util.Comparator; import java.util.Comparator;
import java.util.List; import java.util.List;
import java.util.function.Consumer; import java.util.function.Function;
public interface TbSqlQueue<E> { public interface TbSqlQueue<E, R> {
void init(ScheduledLogExecutorComponent logExecutor, Consumer<List<E>> saveFunction, Comparator<E> batchUpdateComparator, int queueIndex); void init(ScheduledLogExecutorComponent logExecutor, Function<List<E>, List<R>> saveFunction, Comparator<E> batchUpdateComparator, Function<List<TbSqlQueueElement<E, R>>, List<TbSqlQueueElement<E, R>>> filter, int queueIndex);
void destroy(); void destroy();
ListenableFuture<Void> add(E element); ListenableFuture<R> add(E element);
} }

6
dao/src/main/java/org/thingsboard/server/dao/sql/TbSqlQueueElement.java

@ -20,13 +20,13 @@ import lombok.Getter;
import lombok.ToString; import lombok.ToString;
@ToString(exclude = "future") @ToString(exclude = "future")
public final class TbSqlQueueElement<E> { public final class TbSqlQueueElement<E, R> {
@Getter @Getter
private final SettableFuture<Void> future; private final SettableFuture<R> future;
@Getter @Getter
private final E entity; private final E entity;
public TbSqlQueueElement(SettableFuture<Void> future, E entity) { public TbSqlQueueElement(SettableFuture<R> future, E entity) {
this.future = future; this.future = future;
this.entity = entity; this.entity = entity;
} }

228
dao/src/main/java/org/thingsboard/server/dao/sql/attributes/AttributeKvInsertRepository.java

@ -15,162 +15,110 @@
*/ */
package org.thingsboard.server.dao.sql.attributes; package org.thingsboard.server.dao.sql.attributes;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.jdbc.core.BatchPreparedStatementSetter;
import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.stereotype.Repository; import org.springframework.stereotype.Repository;
import org.springframework.transaction.TransactionStatus; import org.springframework.transaction.annotation.Transactional;
import org.springframework.transaction.support.TransactionCallbackWithoutResult; import org.thingsboard.server.dao.AbstractVersionedInsertRepository;
import org.springframework.transaction.support.TransactionTemplate;
import org.thingsboard.server.dao.model.sql.AttributeKvEntity; import org.thingsboard.server.dao.model.sql.AttributeKvEntity;
import org.thingsboard.server.dao.util.SqlDao; import org.thingsboard.server.dao.util.SqlDao;
import java.sql.PreparedStatement; import java.sql.PreparedStatement;
import java.sql.SQLException; import java.sql.SQLException;
import java.sql.Types; import java.sql.Types;
import java.util.ArrayList;
import java.util.List; import java.util.List;
import java.util.regex.Pattern;
@Repository @Repository
@Slf4j @Transactional
@SqlDao @SqlDao
public abstract class AttributeKvInsertRepository { public class AttributeKvInsertRepository extends AbstractVersionedInsertRepository<AttributeKvEntity> {
private static final ThreadLocal<Pattern> PATTERN_THREAD_LOCAL = ThreadLocal.withInitial(() -> Pattern.compile(String.valueOf(Character.MIN_VALUE))); private static final String BATCH_UPDATE = "UPDATE attribute_kv SET str_v = ?, long_v = ?, dbl_v = ?, bool_v = ?, json_v = cast(? AS json), last_update_ts = ?, version = nextval('attribute_kv_version_seq') " +
private static final String EMPTY_STR = ""; "WHERE entity_id = ? and attribute_type =? and attribute_key = ? RETURNING version;";
private static final String BATCH_UPDATE = "UPDATE attribute_kv SET str_v = ?, long_v = ?, dbl_v = ?, bool_v = ?, json_v = cast(? AS json), last_update_ts = ? " +
"WHERE entity_id = ? and attribute_type =? and attribute_key = ?;";
private static final String INSERT_OR_UPDATE = private static final String INSERT_OR_UPDATE =
"INSERT INTO attribute_kv (entity_id, attribute_type, attribute_key, str_v, long_v, dbl_v, bool_v, json_v, last_update_ts) " + "INSERT INTO attribute_kv (entity_id, attribute_type, attribute_key, str_v, long_v, dbl_v, bool_v, json_v, last_update_ts, version) " +
"VALUES(?, ?, ?, ?, ?, ?, ?, cast(? AS json), ?) " + "VALUES(?, ?, ?, ?, ?, ?, ?, cast(? AS json), ?, nextval('attribute_kv_version_seq')) " +
"ON CONFLICT (entity_id, attribute_type, attribute_key) " + "ON CONFLICT (entity_id, attribute_type, attribute_key) " +
"DO UPDATE SET str_v = ?, long_v = ?, dbl_v = ?, bool_v = ?, json_v = cast(? AS json), last_update_ts = ?;"; "DO UPDATE SET str_v = ?, long_v = ?, dbl_v = ?, bool_v = ?, json_v = cast(? AS json), last_update_ts = ?, version = nextval('attribute_kv_version_seq') RETURNING version;";
@Autowired @Override
protected JdbcTemplate jdbcTemplate; protected void setOnBatchUpdateValues(PreparedStatement ps, int i, List<AttributeKvEntity> entities) throws SQLException {
AttributeKvEntity kvEntity = entities.get(i);
@Autowired ps.setString(1, replaceNullChars(kvEntity.getStrValue()));
private TransactionTemplate transactionTemplate;
if (kvEntity.getLongValue() != null) {
@Value("${sql.remove_null_chars:true}") ps.setLong(2, kvEntity.getLongValue());
private boolean removeNullChars; } else {
ps.setNull(2, Types.BIGINT);
public void saveOrUpdate(List<AttributeKvEntity> entities) { }
transactionTemplate.execute(new TransactionCallbackWithoutResult() {
@Override if (kvEntity.getDoubleValue() != null) {
protected void doInTransactionWithoutResult(TransactionStatus status) { ps.setDouble(3, kvEntity.getDoubleValue());
int[] result = jdbcTemplate.batchUpdate(BATCH_UPDATE, new BatchPreparedStatementSetter() { } else {
@Override ps.setNull(3, Types.DOUBLE);
public void setValues(PreparedStatement ps, int i) throws SQLException { }
AttributeKvEntity kvEntity = entities.get(i);
ps.setString(1, replaceNullChars(kvEntity.getStrValue())); if (kvEntity.getBooleanValue() != null) {
ps.setBoolean(4, kvEntity.getBooleanValue());
if (kvEntity.getLongValue() != null) { } else {
ps.setLong(2, kvEntity.getLongValue()); ps.setNull(4, Types.BOOLEAN);
} else { }
ps.setNull(2, Types.BIGINT);
} ps.setString(5, replaceNullChars(kvEntity.getJsonValue()));
if (kvEntity.getDoubleValue() != null) { ps.setLong(6, kvEntity.getLastUpdateTs());
ps.setDouble(3, kvEntity.getDoubleValue()); ps.setObject(7, kvEntity.getId().getEntityId());
} else { ps.setInt(8, kvEntity.getId().getAttributeType());
ps.setNull(3, Types.DOUBLE); ps.setInt(9, kvEntity.getId().getAttributeKey());
}
if (kvEntity.getBooleanValue() != null) {
ps.setBoolean(4, kvEntity.getBooleanValue());
} else {
ps.setNull(4, Types.BOOLEAN);
}
ps.setString(5, replaceNullChars(kvEntity.getJsonValue()));
ps.setLong(6, kvEntity.getLastUpdateTs());
ps.setObject(7, kvEntity.getId().getEntityId());
ps.setInt(8, kvEntity.getId().getAttributeType());
ps.setInt(9, kvEntity.getId().getAttributeKey());
}
@Override
public int getBatchSize() {
return entities.size();
}
});
int updatedCount = 0;
for (int i = 0; i < result.length; i++) {
if (result[i] == 0) {
updatedCount++;
}
}
List<AttributeKvEntity> insertEntities = new ArrayList<>(updatedCount);
for (int i = 0; i < result.length; i++) {
if (result[i] == 0) {
insertEntities.add(entities.get(i));
}
}
jdbcTemplate.batchUpdate(INSERT_OR_UPDATE, new BatchPreparedStatementSetter() {
@Override
public void setValues(PreparedStatement ps, int i) throws SQLException {
AttributeKvEntity kvEntity = insertEntities.get(i);
ps.setObject(1, kvEntity.getId().getEntityId());
ps.setInt(2, kvEntity.getId().getAttributeType());
ps.setInt(3, kvEntity.getId().getAttributeKey());
ps.setString(4, replaceNullChars(kvEntity.getStrValue()));
ps.setString(10, replaceNullChars(kvEntity.getStrValue()));
if (kvEntity.getLongValue() != null) {
ps.setLong(5, kvEntity.getLongValue());
ps.setLong(11, kvEntity.getLongValue());
} else {
ps.setNull(5, Types.BIGINT);
ps.setNull(11, Types.BIGINT);
}
if (kvEntity.getDoubleValue() != null) {
ps.setDouble(6, kvEntity.getDoubleValue());
ps.setDouble(12, kvEntity.getDoubleValue());
} else {
ps.setNull(6, Types.DOUBLE);
ps.setNull(12, Types.DOUBLE);
}
if (kvEntity.getBooleanValue() != null) {
ps.setBoolean(7, kvEntity.getBooleanValue());
ps.setBoolean(13, kvEntity.getBooleanValue());
} else {
ps.setNull(7, Types.BOOLEAN);
ps.setNull(13, Types.BOOLEAN);
}
ps.setString(8, replaceNullChars(kvEntity.getJsonValue()));
ps.setString(14, replaceNullChars(kvEntity.getJsonValue()));
ps.setLong(9, kvEntity.getLastUpdateTs());
ps.setLong(15, kvEntity.getLastUpdateTs());
}
@Override
public int getBatchSize() {
return insertEntities.size();
}
});
}
});
} }
private String replaceNullChars(String strValue) { @Override
if (removeNullChars && strValue != null) { protected void setOnInsertOrUpdateValues(PreparedStatement ps, int i, List<AttributeKvEntity> insertEntities) throws SQLException {
return PATTERN_THREAD_LOCAL.get().matcher(strValue).replaceAll(EMPTY_STR); AttributeKvEntity kvEntity = insertEntities.get(i);
ps.setObject(1, kvEntity.getId().getEntityId());
ps.setInt(2, kvEntity.getId().getAttributeType());
ps.setInt(3, kvEntity.getId().getAttributeKey());
ps.setString(4, replaceNullChars(kvEntity.getStrValue()));
ps.setString(10, replaceNullChars(kvEntity.getStrValue()));
if (kvEntity.getLongValue() != null) {
ps.setLong(5, kvEntity.getLongValue());
ps.setLong(11, kvEntity.getLongValue());
} else {
ps.setNull(5, Types.BIGINT);
ps.setNull(11, Types.BIGINT);
} }
return strValue;
if (kvEntity.getDoubleValue() != null) {
ps.setDouble(6, kvEntity.getDoubleValue());
ps.setDouble(12, kvEntity.getDoubleValue());
} else {
ps.setNull(6, Types.DOUBLE);
ps.setNull(12, Types.DOUBLE);
}
if (kvEntity.getBooleanValue() != null) {
ps.setBoolean(7, kvEntity.getBooleanValue());
ps.setBoolean(13, kvEntity.getBooleanValue());
} else {
ps.setNull(7, Types.BOOLEAN);
ps.setNull(13, Types.BOOLEAN);
}
ps.setString(8, replaceNullChars(kvEntity.getJsonValue()));
ps.setString(14, replaceNullChars(kvEntity.getJsonValue()));
ps.setLong(9, kvEntity.getLastUpdateTs());
ps.setLong(15, kvEntity.getLastUpdateTs());
}
@Override
protected String getBatchUpdateQuery() {
return BATCH_UPDATE;
}
@Override
protected String getInsertOrUpdateQuery() {
return INSERT_OR_UPDATE;
} }
} }

5
dao/src/main/java/org/thingsboard/server/dao/sql/attributes/AttributeKvRepository.java

@ -30,8 +30,8 @@ public interface AttributeKvRepository extends JpaRepository<AttributeKvEntity,
@Query("SELECT a FROM AttributeKvEntity a WHERE a.id.entityId = :entityId " + @Query("SELECT a FROM AttributeKvEntity a WHERE a.id.entityId = :entityId " +
"AND a.id.attributeType = :attributeType") "AND a.id.attributeType = :attributeType")
List<AttributeKvEntity> findAllEntityIdAndAttributeType(@Param("entityId") UUID entityId, List<AttributeKvEntity> findAllByEntityIdAndAttributeType(@Param("entityId") UUID entityId,
@Param("attributeType") int attributeType); @Param("attributeType") int attributeType);
@Transactional @Transactional
@Modifying @Modifying
@ -60,4 +60,3 @@ public interface AttributeKvRepository extends JpaRepository<AttributeKvEntity,
List<Integer> findAllKeysByEntityIdsAndAttributeType(@Param("entityIds") List<UUID> entityIds, List<Integer> findAllKeysByEntityIdsAndAttributeType(@Param("entityIds") List<UUID> entityIds,
@Param("attributeType") int attributeType); @Param("attributeType") int attributeType);
} }

32
dao/src/main/java/org/thingsboard/server/dao/sql/attributes/JpaAttributeDao.java

@ -16,9 +16,7 @@
package org.thingsboard.server.dao.sql.attributes; package org.thingsboard.server.dao.sql.attributes;
import com.google.common.collect.Lists; import com.google.common.collect.Lists;
import com.google.common.util.concurrent.Futures;
import com.google.common.util.concurrent.ListenableFuture; import com.google.common.util.concurrent.ListenableFuture;
import com.google.common.util.concurrent.MoreExecutors;
import jakarta.annotation.PostConstruct; import jakarta.annotation.PostConstruct;
import jakarta.annotation.PreDestroy; import jakarta.annotation.PreDestroy;
import lombok.extern.slf4j.Slf4j; import lombok.extern.slf4j.Slf4j;
@ -32,6 +30,7 @@ import org.thingsboard.server.common.data.id.DeviceProfileId;
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 org.thingsboard.server.common.data.kv.AttributeKvEntry; import org.thingsboard.server.common.data.kv.AttributeKvEntry;
import org.thingsboard.server.common.data.util.TbPair;
import org.thingsboard.server.common.stats.StatsFactory; import org.thingsboard.server.common.stats.StatsFactory;
import org.thingsboard.server.dao.DaoUtil; import org.thingsboard.server.dao.DaoUtil;
import org.thingsboard.server.dao.attributes.AttributesDao; import org.thingsboard.server.dao.attributes.AttributesDao;
@ -88,7 +87,7 @@ public class JpaAttributeDao extends JpaAbstractDaoListeningExecutorService impl
@Value("${sql.batch_sort:true}") @Value("${sql.batch_sort:true}")
private boolean batchSortEnabled; private boolean batchSortEnabled;
private TbSqlBlockingQueueWrapper<AttributeKvEntity> queue; private TbSqlBlockingQueueWrapper<AttributeKvEntity, Long> queue;
@PostConstruct @PostConstruct
private void init() { private void init() {
@ -99,6 +98,7 @@ public class JpaAttributeDao extends JpaAbstractDaoListeningExecutorService impl
.statsPrintIntervalMs(statsPrintIntervalMs) .statsPrintIntervalMs(statsPrintIntervalMs)
.statsNamePrefix("attributes") .statsNamePrefix("attributes")
.batchSortEnabled(batchSortEnabled) .batchSortEnabled(batchSortEnabled)
.withResponse(true)
.build(); .build();
Function<AttributeKvEntity, Integer> hashcodeFunction = entity -> entity.getId().getEntityId().hashCode(); Function<AttributeKvEntity, Integer> hashcodeFunction = entity -> entity.getId().getEntityId().hashCode();
@ -106,7 +106,7 @@ public class JpaAttributeDao extends JpaAbstractDaoListeningExecutorService impl
queue.init(logExecutor, v -> attributeKvInsertRepository.saveOrUpdate(v), queue.init(logExecutor, v -> attributeKvInsertRepository.saveOrUpdate(v),
Comparator.comparing((AttributeKvEntity attributeKvEntity) -> attributeKvEntity.getId().getEntityId()) Comparator.comparing((AttributeKvEntity attributeKvEntity) -> attributeKvEntity.getId().getEntityId())
.thenComparing(attributeKvEntity -> attributeKvEntity.getId().getAttributeType()) .thenComparing(attributeKvEntity -> attributeKvEntity.getId().getAttributeType())
.thenComparing(attributeKvEntity -> attributeKvEntity.getId().getAttributeKey()) .thenComparing(attributeKvEntity -> attributeKvEntity.getId().getAttributeKey()), l -> l
); );
} }
@ -145,7 +145,7 @@ public class JpaAttributeDao extends JpaAbstractDaoListeningExecutorService impl
@Override @Override
public List<AttributeKvEntry> findAll(TenantId tenantId, EntityId entityId, AttributeScope attributeScope) { public List<AttributeKvEntry> findAll(TenantId tenantId, EntityId entityId, AttributeScope attributeScope) {
List<AttributeKvEntity> attributes = attributeKvRepository.findAllEntityIdAndAttributeType( List<AttributeKvEntity> attributes = attributeKvRepository.findAllByEntityIdAndAttributeType(
entityId.getId(), entityId.getId(),
attributeScope.getId()); attributeScope.getId());
attributes.forEach(attributeKvEntity -> attributeKvEntity.setStrKey(keyDictionaryDao.getKey(attributeKvEntity.getId().getAttributeKey()))); attributes.forEach(attributeKvEntity -> attributeKvEntity.setStrKey(keyDictionaryDao.getKey(attributeKvEntity.getId().getAttributeKey())));
@ -178,7 +178,7 @@ public class JpaAttributeDao extends JpaAbstractDaoListeningExecutorService impl
} }
@Override @Override
public ListenableFuture<String> save(TenantId tenantId, EntityId entityId, AttributeScope attributeScope, AttributeKvEntry attribute) { public ListenableFuture<Long> save(TenantId tenantId, EntityId entityId, AttributeScope attributeScope, AttributeKvEntry attribute) {
AttributeKvEntity entity = new AttributeKvEntity(); AttributeKvEntity entity = new AttributeKvEntity();
entity.setId(new AttributeKvCompositeKey(entityId.getId(), attributeScope.getId(), keyDictionaryDao.getOrSaveKeyId(attribute.getKey()))); entity.setId(new AttributeKvCompositeKey(entityId.getId(), attributeScope.getId(), keyDictionaryDao.getOrSaveKeyId(attribute.getKey())));
entity.setLastUpdateTs(attribute.getLastUpdateTs()); entity.setLastUpdateTs(attribute.getLastUpdateTs());
@ -187,11 +187,11 @@ public class JpaAttributeDao extends JpaAbstractDaoListeningExecutorService impl
entity.setLongValue(attribute.getLongValue().orElse(null)); entity.setLongValue(attribute.getLongValue().orElse(null));
entity.setBooleanValue(attribute.getBooleanValue().orElse(null)); entity.setBooleanValue(attribute.getBooleanValue().orElse(null));
entity.setJsonValue(attribute.getJsonValue().orElse(null)); entity.setJsonValue(attribute.getJsonValue().orElse(null));
return addToQueue(entity, attribute.getKey()); return addToQueue(entity);
} }
private ListenableFuture<String> addToQueue(AttributeKvEntity entity, String key) { private ListenableFuture<Long> addToQueue(AttributeKvEntity entity) {
return Futures.transform(queue.add(entity), v -> key, MoreExecutors.directExecutor()); return queue.add(entity);
} }
@Override @Override
@ -206,6 +206,20 @@ public class JpaAttributeDao extends JpaAbstractDaoListeningExecutorService impl
return futuresList; return futuresList;
} }
@Override
public List<ListenableFuture<TbPair<String, Long>>> removeAllWithVersions(TenantId tenantId, EntityId entityId, AttributeScope attributeScope, List<String> keys) {
List<ListenableFuture<TbPair<String, Long>>> futuresList = new ArrayList<>(keys.size());
for (String key : keys) {
futuresList.add(service.submit(() -> {
Long version = transactionTemplate.execute(status -> jdbcTemplate.query("DELETE FROM attribute_kv WHERE entity_id = ? AND attribute_type = ? " +
"AND attribute_key = ? RETURNING nextval('attribute_kv_version_seq')",
rs -> rs.next() ? rs.getLong(1) : null, entityId.getId(), attributeScope.getId(), keyDictionaryDao.getOrSaveKeyId(key)));
return TbPair.of(key, version);
}));
}
return futuresList;
}
@Transactional @Transactional
@Override @Override
public List<Pair<AttributeScope, String>> removeAllByEntityId(TenantId tenantId, EntityId entityId) { public List<Pair<AttributeScope, String>> removeAllByEntityId(TenantId tenantId, EntityId entityId) {

2
dao/src/main/java/org/thingsboard/server/dao/sql/edge/JpaBaseEdgeEventDao.java

@ -90,7 +90,7 @@ public class JpaBaseEdgeEventDao extends JpaPartitionedAbstractDao<EdgeEventEnti
private static final String TABLE_NAME = ModelConstants.EDGE_EVENT_TABLE_NAME; private static final String TABLE_NAME = ModelConstants.EDGE_EVENT_TABLE_NAME;
private TbSqlBlockingQueueWrapper<EdgeEventEntity> queue; private TbSqlBlockingQueueWrapper<EdgeEventEntity, Void> queue;
@Override @Override
protected Class<EdgeEventEntity> getEntityClass() { protected Class<EdgeEventEntity> getEntityClass() {

2
dao/src/main/java/org/thingsboard/server/dao/sql/event/JpaBaseEventDao.java

@ -110,7 +110,7 @@ public class JpaBaseEventDao implements EventDao {
@Value("${sql.batch_sort:true}") @Value("${sql.batch_sort:true}")
private boolean batchSortEnabled; private boolean batchSortEnabled;
private TbSqlBlockingQueueWrapper<Event> queue; private TbSqlBlockingQueueWrapper<Event, Void> queue;
private final Map<EventType, EventRepository<?, ?>> repositories = new ConcurrentHashMap<>(); private final Map<EventType, EventRepository<?, ?>> repositories = new ConcurrentHashMap<>();

157
dao/src/main/java/org/thingsboard/server/dao/sql/relation/JpaRelationDao.java

@ -18,11 +18,11 @@ package org.thingsboard.server.dao.sql.relation;
import com.google.common.util.concurrent.ListenableFuture; import com.google.common.util.concurrent.ListenableFuture;
import lombok.extern.slf4j.Slf4j; import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.dao.ConcurrencyFailureException;
import org.springframework.dao.DataAccessException;
import org.springframework.data.domain.PageRequest; import org.springframework.data.domain.PageRequest;
import org.springframework.stereotype.Component; import org.springframework.stereotype.Component;
import org.springframework.util.CollectionUtils;
import org.thingsboard.server.common.data.id.EntityId; import org.thingsboard.server.common.data.id.EntityId;
import org.thingsboard.server.common.data.id.EntityIdFactory;
import org.thingsboard.server.common.data.id.TenantId; import org.thingsboard.server.common.data.id.TenantId;
import org.thingsboard.server.common.data.relation.EntityRelation; import org.thingsboard.server.common.data.relation.EntityRelation;
import org.thingsboard.server.common.data.relation.RelationTypeGroup; import org.thingsboard.server.common.data.relation.RelationTypeGroup;
@ -36,11 +36,19 @@ import org.thingsboard.server.dao.util.SqlDao;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.Arrays; import java.util.Arrays;
import java.util.Collection;
import java.util.Collections; import java.util.Collections;
import java.util.List; import java.util.List;
import java.util.UUID;
import java.util.stream.Collectors; import java.util.stream.Collectors;
import static org.thingsboard.server.dao.model.ModelConstants.RELATION_FROM_ID_PROPERTY;
import static org.thingsboard.server.dao.model.ModelConstants.RELATION_FROM_TYPE_PROPERTY;
import static org.thingsboard.server.dao.model.ModelConstants.RELATION_TO_ID_PROPERTY;
import static org.thingsboard.server.dao.model.ModelConstants.RELATION_TO_TYPE_PROPERTY;
import static org.thingsboard.server.dao.model.ModelConstants.RELATION_TYPE_GROUP_PROPERTY;
import static org.thingsboard.server.dao.model.ModelConstants.RELATION_TYPE_PROPERTY;
import static org.thingsboard.server.dao.model.ModelConstants.VERSION_COLUMN;
/** /**
* Created by Valerii Sosliuk on 5/29/2017. * Created by Valerii Sosliuk on 5/29/2017.
*/ */
@ -50,6 +58,8 @@ import java.util.stream.Collectors;
public class JpaRelationDao extends JpaAbstractDaoListeningExecutorService implements RelationDao { public class JpaRelationDao extends JpaAbstractDaoListeningExecutorService implements RelationDao {
private static final List<String> ALL_TYPE_GROUP_NAMES = new ArrayList<>(); private static final List<String> ALL_TYPE_GROUP_NAMES = new ArrayList<>();
private static final String RETURNING = "RETURNING from_id, from_type, to_id, to_type, relation_type, relation_type_group, nextval('relation_version_seq') as version";
private static final String DELETE_QUERY = "DELETE FROM relation WHERE from_id = ? AND from_type = ? AND to_id = ? AND to_type = ? AND relation_type = ? AND relation_type_group = ? " + RETURNING;
static { static {
Arrays.stream(RelationTypeGroup.values()).map(RelationTypeGroup::name).forEach(ALL_TYPE_GROUP_NAMES::add); Arrays.stream(RelationTypeGroup.values()).map(RelationTypeGroup::name).forEach(ALL_TYPE_GROUP_NAMES::add);
@ -144,107 +154,138 @@ public class JpaRelationDao extends JpaAbstractDaoListeningExecutorService imple
} }
@Override @Override
public boolean saveRelation(TenantId tenantId, EntityRelation relation) { public EntityRelation saveRelation(TenantId tenantId, EntityRelation relation) {
return relationInsertRepository.saveOrUpdate(new RelationEntity(relation)) != null; return DaoUtil.getData(relationInsertRepository.saveOrUpdate(new RelationEntity(relation)));
} }
@Override @Override
public void saveRelations(TenantId tenantId, Collection<EntityRelation> relations) { public List<EntityRelation> saveRelations(TenantId tenantId, List<EntityRelation> relations) {
List<RelationEntity> entities = relations.stream().map(RelationEntity::new).collect(Collectors.toList()); List<RelationEntity> entities = relations.stream().map(RelationEntity::new).collect(Collectors.toList());
relationInsertRepository.saveOrUpdate(entities); return DaoUtil.convertDataList(relationInsertRepository.saveOrUpdate(entities));
} }
@Override @Override
public ListenableFuture<Boolean> saveRelationAsync(TenantId tenantId, EntityRelation relation) { public ListenableFuture<EntityRelation> saveRelationAsync(TenantId tenantId, EntityRelation relation) {
return service.submit(() -> relationInsertRepository.saveOrUpdate(new RelationEntity(relation)) != null); return service.submit(() -> DaoUtil.getData(relationInsertRepository.saveOrUpdate(new RelationEntity(relation))));
} }
@Override @Override
public boolean deleteRelation(TenantId tenantId, EntityRelation relation) { public EntityRelation deleteRelation(TenantId tenantId, EntityRelation relation) {
RelationCompositeKey key = new RelationCompositeKey(relation); RelationCompositeKey key = new RelationCompositeKey(relation);
return deleteRelationIfExists(key); return deleteRelationIfExists(key);
} }
@Override @Override
public ListenableFuture<Boolean> deleteRelationAsync(TenantId tenantId, EntityRelation relation) { public ListenableFuture<EntityRelation> deleteRelationAsync(TenantId tenantId, EntityRelation relation) {
RelationCompositeKey key = new RelationCompositeKey(relation); RelationCompositeKey key = new RelationCompositeKey(relation);
return service.submit( return service.submit(
() -> deleteRelationIfExists(key)); () -> deleteRelationIfExists(key));
} }
@Override @Override
public boolean deleteRelation(TenantId tenantId, EntityId from, EntityId to, String relationType, RelationTypeGroup typeGroup) { public EntityRelation deleteRelation(TenantId tenantId, EntityId from, EntityId to, String relationType, RelationTypeGroup typeGroup) {
RelationCompositeKey key = getRelationCompositeKey(from, to, relationType, typeGroup); RelationCompositeKey key = getRelationCompositeKey(from, to, relationType, typeGroup);
return deleteRelationIfExists(key); return deleteRelationIfExists(key);
} }
@Override @Override
public ListenableFuture<Boolean> deleteRelationAsync(TenantId tenantId, EntityId from, EntityId to, String relationType, RelationTypeGroup typeGroup) { public ListenableFuture<EntityRelation> deleteRelationAsync(TenantId tenantId, EntityId from, EntityId to, String relationType, RelationTypeGroup typeGroup) {
RelationCompositeKey key = getRelationCompositeKey(from, to, relationType, typeGroup); RelationCompositeKey key = getRelationCompositeKey(from, to, relationType, typeGroup);
return service.submit( return service.submit(
() -> deleteRelationIfExists(key)); () -> deleteRelationIfExists(key));
} }
private boolean deleteRelationIfExists(RelationCompositeKey key) { private EntityRelation deleteRelationIfExists(RelationCompositeKey key) {
boolean relationExistsBeforeDelete = relationRepository.existsById(key); return jdbcTemplate.query(DELETE_QUERY, rs -> {
if (relationExistsBeforeDelete) { if (!rs.next()) {
try { return null;
relationRepository.deleteById(key);
} catch (DataAccessException e) {
log.debug("[{}] Concurrency exception while deleting relation", key, e);
} }
} EntityRelation relation = new EntityRelation();
return relationExistsBeforeDelete;
var fromId = rs.getObject(RELATION_FROM_ID_PROPERTY, UUID.class);
var fromType = rs.getString(RELATION_FROM_TYPE_PROPERTY);
var toId = rs.getObject(RELATION_TO_ID_PROPERTY, UUID.class);
var toType = rs.getString(RELATION_TO_TYPE_PROPERTY);
var relationTypeGroup = rs.getString(RELATION_TYPE_GROUP_PROPERTY);
var relationType = rs.getString(RELATION_TYPE_PROPERTY);
var version = rs.getLong(VERSION_COLUMN);
//additionalInfo ignored (no need to send extra data for delete events)
relation.setTo(EntityIdFactory.getByTypeAndUuid(toType, toId));
relation.setFrom(EntityIdFactory.getByTypeAndUuid(fromType, fromId));
relation.setType(relationType);
relation.setTypeGroup(RelationTypeGroup.valueOf(relationTypeGroup));
relation.setVersion(version);
return relation;
}, key.getFromId(), key.getFromType(), key.getToId(), key.getToType(), key.getRelationType(), key.getRelationTypeGroup());
} }
@Override @Override
public void deleteOutboundRelations(TenantId tenantId, EntityId entity) { public List<EntityRelation> deleteOutboundRelations(TenantId tenantId, EntityId entity) {
try { return deleteRelations(entity, null, false);
relationRepository.deleteByFromIdAndFromType(entity.getId(), entity.getEntityType().name());
} catch (ConcurrencyFailureException e) {
log.debug("Concurrency exception while deleting relations [{}]", entity, e);
}
} }
@Override @Override
public void deleteOutboundRelations(TenantId tenantId, EntityId entity, RelationTypeGroup relationTypeGroup) { public List<EntityRelation> deleteOutboundRelations(TenantId tenantId, EntityId entity, RelationTypeGroup relationTypeGroup) {
try { return deleteRelations(entity, Collections.singletonList(relationTypeGroup.name()), false);
relationRepository.deleteByFromIdAndFromTypeAndRelationTypeGroupIn(entity.getId(), entity.getEntityType().name(), Collections.singletonList(relationTypeGroup.name()));
} catch (ConcurrencyFailureException e) {
log.debug("Concurrency exception while deleting relations [{}]", entity, e);
}
} }
@Override @Override
public void deleteInboundRelations(TenantId tenantId, EntityId entity) { public List<EntityRelation> deleteInboundRelations(TenantId tenantId, EntityId entity) {
try { return deleteRelations(entity, ALL_TYPE_GROUP_NAMES, true);
relationRepository.deleteByToIdAndToTypeAndRelationTypeGroupIn(entity.getId(), entity.getEntityType().name(), ALL_TYPE_GROUP_NAMES);
} catch (ConcurrencyFailureException e) {
log.debug("Concurrency exception while deleting relations [{}]", entity, e);
}
} }
@Override @Override
public void deleteInboundRelations(TenantId tenantId, EntityId entity, RelationTypeGroup relationTypeGroup) { public List<EntityRelation> deleteInboundRelations(TenantId tenantId, EntityId entity, RelationTypeGroup relationTypeGroup) {
try { return deleteRelations(entity, Collections.singletonList(relationTypeGroup.name()), true);
relationRepository.deleteByToIdAndToTypeAndRelationTypeGroupIn(entity.getId(), entity.getEntityType().name(), Collections.singletonList(relationTypeGroup.name()));
} catch (ConcurrencyFailureException e) {
log.debug("Concurrency exception while deleting relations [{}]", entity, e);
}
} }
@Override private List<EntityRelation> deleteRelations(EntityId entityId, List<String> relationTypeGroups, boolean inbound) {
public ListenableFuture<Boolean> deleteOutboundRelationsAsync(TenantId tenantId, EntityId entity) { List<Object> params = new ArrayList<>();
return service.submit( params.add(entityId.getId());
() -> { params.add(entityId.getEntityType().name());
boolean relationExistsBeforeDelete = relationRepository
.findAllByFromIdAndFromType(entity.getId(), entity.getEntityType().name()) StringBuilder sqlBuilder = new StringBuilder("DELETE FROM relation WHERE ");
.size() > 0; if (inbound) {
if (relationExistsBeforeDelete) { sqlBuilder.append("to_id = ? AND to_type = ? ");
relationRepository.deleteByFromIdAndFromType(entity.getId(), entity.getEntityType().name()); } else {
} sqlBuilder.append("from_id = ? AND from_type = ? ");
return relationExistsBeforeDelete; }
});
if (!CollectionUtils.isEmpty(relationTypeGroups)) {
sqlBuilder.append("AND relation_type_group IN (?");
for (int i = 1; i < relationTypeGroups.size(); i++) {
sqlBuilder.append(", ?");
}
sqlBuilder.append(")");
params.addAll(relationTypeGroups);
}
sqlBuilder.append(RETURNING);
return jdbcTemplate.queryForList(sqlBuilder.toString(), params.toArray()).stream()
.map(row -> {
EntityRelation relation = new EntityRelation();
var fromId = row.get(RELATION_FROM_ID_PROPERTY);
var fromType = row.get(RELATION_FROM_TYPE_PROPERTY);
var toId = row.get(RELATION_TO_ID_PROPERTY);
var toType = row.get(RELATION_TO_TYPE_PROPERTY);
var relationTypeGroup = row.get(RELATION_TYPE_GROUP_PROPERTY);
var relationType = row.get(RELATION_TYPE_PROPERTY);
var version = row.get(VERSION_COLUMN);
//additionalInfo ignored (no need to send extra data for delete events)
relation.setTo(EntityIdFactory.getByTypeAndUuid((String) toType, (UUID) toId));
relation.setFrom(EntityIdFactory.getByTypeAndUuid((String) fromType, (UUID) fromId));
relation.setType((String) relationType);
relation.setTypeGroup(RelationTypeGroup.valueOf((String) relationTypeGroup));
relation.setVersion((Long) version);
return relation;
})
.collect(Collectors.toList());
} }
@Override @Override

2
dao/src/main/java/org/thingsboard/server/dao/sql/relation/RelationInsertRepository.java

@ -23,6 +23,6 @@ public interface RelationInsertRepository {
RelationEntity saveOrUpdate(RelationEntity entity); RelationEntity saveOrUpdate(RelationEntity entity);
void saveOrUpdate(List<RelationEntity> entities); List<RelationEntity> saveOrUpdate(List<RelationEntity> entities);
} }

3
dao/src/main/java/org/thingsboard/server/dao/sql/relation/RelationRepository.java

@ -58,9 +58,6 @@ public interface RelationRepository
String relationType, String relationType,
String relationTypeGroup); String relationTypeGroup);
List<RelationEntity> findAllByFromIdAndFromType(UUID fromId,
String fromType);
@Query("SELECT r FROM RelationEntity r WHERE " + @Query("SELECT r FROM RelationEntity r WHERE " +
"r.relationTypeGroup = 'RULE_NODE' AND r.toType = 'RULE_CHAIN' " + "r.relationTypeGroup = 'RULE_NODE' AND r.toType = 'RULE_CHAIN' " +
"AND r.toId in (SELECT id from RuleChainEntity where type = :ruleChainType )") "AND r.toId in (SELECT id from RuleChainEntity where type = :ruleChainType )")

50
dao/src/main/java/org/thingsboard/server/dao/sql/relation/SqlRelationInsertRepository.java

@ -21,27 +21,33 @@ import jakarta.persistence.Query;
import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.jdbc.core.BatchPreparedStatementSetter; import org.springframework.jdbc.core.BatchPreparedStatementSetter;
import org.springframework.jdbc.core.JdbcTemplate; import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.jdbc.core.PreparedStatementCreator;
import org.springframework.jdbc.core.SqlProvider;
import org.springframework.jdbc.support.GeneratedKeyHolder;
import org.springframework.jdbc.support.KeyHolder;
import org.springframework.stereotype.Repository; import org.springframework.stereotype.Repository;
import org.springframework.transaction.annotation.Transactional; import org.springframework.transaction.annotation.Transactional;
import org.thingsboard.common.util.JacksonUtil; import org.thingsboard.common.util.JacksonUtil;
import org.thingsboard.server.dao.model.sql.RelationEntity; import org.thingsboard.server.dao.model.sql.RelationEntity;
import java.sql.Connection;
import java.sql.PreparedStatement; import java.sql.PreparedStatement;
import java.sql.SQLException; import java.sql.SQLException;
import java.util.List; import java.util.List;
import static org.thingsboard.server.dao.model.ModelConstants.VERSION_COLUMN;
@Repository @Repository
@Transactional @Transactional
public class SqlRelationInsertRepository implements RelationInsertRepository { public class SqlRelationInsertRepository implements RelationInsertRepository {
private static final String INSERT_ON_CONFLICT_DO_UPDATE_JPA = "INSERT INTO relation (from_id, from_type, to_id, to_type, relation_type_group, relation_type, additional_info)" + private static final String INSERT_ON_CONFLICT_DO_UPDATE_JPA = "INSERT INTO relation (from_id, from_type, to_id, to_type, relation_type_group, relation_type, version, additional_info)" +
" VALUES (:fromId, :fromType, :toId, :toType, :relationTypeGroup, :relationType, :additionalInfo) " + " VALUES (:fromId, :fromType, :toId, :toType, :relationTypeGroup, :relationType, nextval('relation_version_seq'), :additionalInfo) " +
"ON CONFLICT (from_id, from_type, relation_type_group, relation_type, to_id, to_type) DO UPDATE SET additional_info = :additionalInfo returning *"; "ON CONFLICT (from_id, from_type, relation_type_group, relation_type, to_id, to_type) DO UPDATE SET additional_info = :additionalInfo, version = nextval('relation_version_seq') returning *";
private static final String INSERT_ON_CONFLICT_DO_UPDATE_JDBC = "INSERT INTO relation (from_id, from_type, to_id, to_type, relation_type_group, relation_type, additional_info)" +
" VALUES (?, ?, ?, ?, ?, ?, ?) " +
"ON CONFLICT (from_id, from_type, relation_type_group, relation_type, to_id, to_type) DO UPDATE SET additional_info = ?";
private static final String INSERT_ON_CONFLICT_DO_UPDATE_JDBC = "INSERT INTO relation (from_id, from_type, to_id, to_type, relation_type_group, relation_type, version, additional_info)" +
" VALUES (?, ?, ?, ?, ?, ?, nextval('relation_version_seq'), ?) " +
"ON CONFLICT (from_id, from_type, relation_type_group, relation_type, to_id, to_type) DO UPDATE SET additional_info = ?, version = nextval('relation_version_seq')";
@PersistenceContext @PersistenceContext
protected EntityManager entityManager; protected EntityManager entityManager;
@ -71,8 +77,9 @@ public class SqlRelationInsertRepository implements RelationInsertRepository {
} }
@Override @Override
public void saveOrUpdate(List<RelationEntity> entities) { public List<RelationEntity> saveOrUpdate(List<RelationEntity> entities) {
jdbcTemplate.batchUpdate(INSERT_ON_CONFLICT_DO_UPDATE_JDBC, new BatchPreparedStatementSetter() { KeyHolder keyHolder = new GeneratedKeyHolder();
jdbcTemplate.batchUpdate(new SequencePreparedStatementCreator(INSERT_ON_CONFLICT_DO_UPDATE_JDBC), new BatchPreparedStatementSetter() {
@Override @Override
public void setValues(PreparedStatement ps, int i) throws SQLException { public void setValues(PreparedStatement ps, int i) throws SQLException {
RelationEntity relation = entities.get(i); RelationEntity relation = entities.get(i);
@ -98,7 +105,30 @@ public class SqlRelationInsertRepository implements RelationInsertRepository {
public int getBatchSize() { public int getBatchSize() {
return entities.size(); return entities.size();
} }
}); }, keyHolder);
var seqNumbers = keyHolder.getKeyList();
for (int i = 0; i < entities.size(); i++) {
entities.get(i).setVersion((Long) seqNumbers.get(i).get(VERSION_COLUMN));
}
return entities;
}
private record SequencePreparedStatementCreator(String sql) implements PreparedStatementCreator, SqlProvider {
private static final String[] COLUMNS = {VERSION_COLUMN};
@Override
public PreparedStatement createPreparedStatement(Connection con) throws SQLException {
return con.prepareStatement(sql, COLUMNS);
}
@Override
public String getSql() {
return this.sql;
}
} }
} }

2
dao/src/main/java/org/thingsboard/server/dao/sqlts/AbstractChunkedAggregationTimeseriesDao.java

@ -60,7 +60,7 @@ public abstract class AbstractChunkedAggregationTimeseriesDao extends AbstractSq
@Autowired @Autowired
protected InsertTsRepository<TsKvEntity> insertRepository; protected InsertTsRepository<TsKvEntity> insertRepository;
protected TbSqlBlockingQueueWrapper<TsKvEntity> tsQueue; protected TbSqlBlockingQueueWrapper<TsKvEntity, Void> tsQueue;
@Autowired @Autowired
private StatsFactory statsFactory; private StatsFactory statsFactory;

170
dao/src/main/java/org/thingsboard/server/dao/sqlts/CachedRedisSqlTimeseriesLatestDao.java

@ -0,0 +1,170 @@
/**
* Copyright © 2016-2024 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.sqlts;
import com.google.common.util.concurrent.FutureCallback;
import com.google.common.util.concurrent.Futures;
import com.google.common.util.concurrent.ListenableFuture;
import com.google.common.util.concurrent.MoreExecutors;
import jakarta.annotation.PostConstruct;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.context.annotation.Primary;
import org.springframework.stereotype.Component;
import org.thingsboard.server.cache.TbCacheValueWrapper;
import org.thingsboard.server.cache.VersionedTbCache;
import org.thingsboard.server.common.data.id.DeviceProfileId;
import org.thingsboard.server.common.data.id.EntityId;
import org.thingsboard.server.common.data.id.TenantId;
import org.thingsboard.server.common.data.kv.BasicTsKvEntry;
import org.thingsboard.server.common.data.kv.DeleteTsKvQuery;
import org.thingsboard.server.common.data.kv.TsKvEntry;
import org.thingsboard.server.common.data.kv.TsKvLatestRemovingResult;
import org.thingsboard.server.common.stats.DefaultCounter;
import org.thingsboard.server.common.stats.StatsFactory;
import org.thingsboard.server.dao.cache.CacheExecutorService;
import org.thingsboard.server.dao.timeseries.TimeseriesLatestDao;
import org.thingsboard.server.dao.timeseries.TsLatestCacheKey;
import org.thingsboard.server.dao.util.SqlTsLatestAnyDaoCachedRedis;
import java.util.List;
import java.util.Optional;
@Slf4j
@Component
@SqlTsLatestAnyDaoCachedRedis
@RequiredArgsConstructor
@Primary
public class CachedRedisSqlTimeseriesLatestDao extends BaseAbstractSqlTimeseriesDao implements TimeseriesLatestDao {
public static final String STATS_NAME = "ts_latest.cache";
final CacheExecutorService cacheExecutorService;
final SqlTimeseriesLatestDao sqlDao;
final StatsFactory statsFactory;
final VersionedTbCache<TsLatestCacheKey, TsKvEntry> cache;
DefaultCounter hitCounter;
DefaultCounter missCounter;
@PostConstruct
public void init() {
log.info("Init Redis cache-aside SQL Timeseries Latest DAO");
this.hitCounter = statsFactory.createDefaultCounter(STATS_NAME, "result", "hit");
this.missCounter = statsFactory.createDefaultCounter(STATS_NAME, "result", "miss");
}
@Override
public ListenableFuture<Long> saveLatest(TenantId tenantId, EntityId entityId, TsKvEntry tsKvEntry) {
ListenableFuture<Long> future = sqlDao.saveLatest(tenantId, entityId, tsKvEntry);
future = Futures.transform(future, version -> {
cache.put(new TsLatestCacheKey(entityId, tsKvEntry.getKey()), new BasicTsKvEntry(tsKvEntry.getTs(), ((BasicTsKvEntry) tsKvEntry).getKv(), version));
return version;
},
cacheExecutorService);
if (log.isTraceEnabled()) {
Futures.addCallback(future, new FutureCallback<>() {
@Override
public void onSuccess(Long result) {
log.trace("saveLatest onSuccess [{}][{}][{}]", entityId, tsKvEntry.getKey(), tsKvEntry);
}
@Override
public void onFailure(Throwable t) {
log.info("saveLatest onFailure [{}][{}][{}]", entityId, tsKvEntry.getKey(), tsKvEntry, t);
}
}, MoreExecutors.directExecutor());
}
return future;
}
@Override
public ListenableFuture<TsKvLatestRemovingResult> removeLatest(TenantId tenantId, EntityId entityId, DeleteTsKvQuery query) {
ListenableFuture<TsKvLatestRemovingResult> future = sqlDao.removeLatest(tenantId, entityId, query);
future = Futures.transform(future, x -> {
if (x.isRemoved()) {
TsLatestCacheKey key = new TsLatestCacheKey(entityId, query.getKey());
Long version = x.getVersion();
TsKvEntry newTsKvEntry = x.getData();
if (newTsKvEntry != null) {
cache.put(key, new BasicTsKvEntry(newTsKvEntry.getTs(), ((BasicTsKvEntry) newTsKvEntry).getKv(), version));
} else {
cache.evict(key, version);
}
}
return x;
},
cacheExecutorService);
if (log.isTraceEnabled()) {
Futures.addCallback(future, new FutureCallback<>() {
@Override
public void onSuccess(TsKvLatestRemovingResult result) {
log.trace("removeLatest onSuccess [{}][{}][{}]", entityId, query.getKey(), query);
}
@Override
public void onFailure(Throwable t) {
log.info("removeLatest onFailure [{}][{}][{}]", entityId, query.getKey(), query, t);
}
}, MoreExecutors.directExecutor());
}
return future;
}
@Override
public ListenableFuture<Optional<TsKvEntry>> findLatestOpt(TenantId tenantId, EntityId entityId, String key) {
log.trace("findLatestOpt");
return doFindLatest(tenantId, entityId, key);
}
@Override
public ListenableFuture<TsKvEntry> findLatest(TenantId tenantId, EntityId entityId, String key) {
return Futures.transform(doFindLatest(tenantId, entityId, key), x -> sqlDao.wrapNullTsKvEntry(key, x.orElse(null)), MoreExecutors.directExecutor());
}
public ListenableFuture<Optional<TsKvEntry>> doFindLatest(TenantId tenantId, EntityId entityId, String key) {
final TsLatestCacheKey cacheKey = new TsLatestCacheKey(entityId, key);
ListenableFuture<TbCacheValueWrapper<TsKvEntry>> cacheFuture = cacheExecutorService.submit(() -> cache.get(cacheKey));
return Futures.transformAsync(cacheFuture, (cacheValueWrap) -> {
if (cacheValueWrap != null) {
final TsKvEntry tsKvEntry = cacheValueWrap.get();
log.debug("findLatest cache hit [{}][{}][{}]", entityId, key, tsKvEntry);
return Futures.immediateFuture(Optional.ofNullable(tsKvEntry));
}
log.debug("findLatest cache miss [{}][{}]", entityId, key);
ListenableFuture<Optional<TsKvEntry>> daoFuture = sqlDao.findLatestOpt(tenantId, entityId, key);
return Futures.transform(daoFuture, daoValue -> {
cache.put(cacheKey, daoValue.orElse(null));
return daoValue;
}, MoreExecutors.directExecutor());
}, MoreExecutors.directExecutor());
}
@Override
public ListenableFuture<List<TsKvEntry>> findAllLatest(TenantId tenantId, EntityId entityId) {
return sqlDao.findAllLatest(tenantId, entityId);
}
@Override
public List<String> findAllKeysByDeviceProfileId(TenantId tenantId, DeviceProfileId deviceProfileId) {
return sqlDao.findAllKeysByDeviceProfileId(tenantId, deviceProfileId);
}
@Override
public List<String> findAllKeysByEntityIds(TenantId tenantId, List<EntityId> entityIds) {
return sqlDao.findAllKeysByEntityIds(tenantId, entityIds);
}
}

62
dao/src/main/java/org/thingsboard/server/dao/sqlts/SqlTimeseriesLatestDao.java

@ -46,6 +46,7 @@ import org.thingsboard.server.dao.model.sqlts.latest.TsKvLatestEntity;
import org.thingsboard.server.dao.sql.ScheduledLogExecutorComponent; import org.thingsboard.server.dao.sql.ScheduledLogExecutorComponent;
import org.thingsboard.server.dao.sql.TbSqlBlockingQueueParams; import org.thingsboard.server.dao.sql.TbSqlBlockingQueueParams;
import org.thingsboard.server.dao.sql.TbSqlBlockingQueueWrapper; import org.thingsboard.server.dao.sql.TbSqlBlockingQueueWrapper;
import org.thingsboard.server.dao.sql.TbSqlQueueElement;
import org.thingsboard.server.dao.sqlts.insert.latest.InsertLatestTsRepository; import org.thingsboard.server.dao.sqlts.insert.latest.InsertLatestTsRepository;
import org.thingsboard.server.dao.sqlts.latest.SearchTsKvLatestRepository; import org.thingsboard.server.dao.sqlts.latest.SearchTsKvLatestRepository;
import org.thingsboard.server.dao.sqlts.latest.TsKvLatestRepository; import org.thingsboard.server.dao.sqlts.latest.TsKvLatestRepository;
@ -81,7 +82,7 @@ public class SqlTimeseriesLatestDao extends BaseAbstractSqlTimeseriesDao impleme
@Autowired @Autowired
private InsertLatestTsRepository insertLatestTsRepository; private InsertLatestTsRepository insertLatestTsRepository;
private TbSqlBlockingQueueWrapper<TsKvLatestEntity> tsLatestQueue; private TbSqlBlockingQueueWrapper<TsKvLatestEntity, Long> tsLatestQueue;
@Value("${sql.ts_latest.batch_size:1000}") @Value("${sql.ts_latest.batch_size:1000}")
private int tsLatestBatchSize; private int tsLatestBatchSize;
@ -115,25 +116,26 @@ public class SqlTimeseriesLatestDao extends BaseAbstractSqlTimeseriesDao impleme
.maxDelay(tsLatestMaxDelay) .maxDelay(tsLatestMaxDelay)
.statsPrintIntervalMs(tsLatestStatsPrintIntervalMs) .statsPrintIntervalMs(tsLatestStatsPrintIntervalMs)
.statsNamePrefix("ts.latest") .statsNamePrefix("ts.latest")
.batchSortEnabled(false) .batchSortEnabled(batchSortEnabled)
.withResponse(true)
.build(); .build();
java.util.function.Function<TsKvLatestEntity, Integer> hashcodeFunction = entity -> entity.getEntityId().hashCode(); java.util.function.Function<TsKvLatestEntity, Integer> hashcodeFunction = entity -> entity.getEntityId().hashCode();
tsLatestQueue = new TbSqlBlockingQueueWrapper<>(tsLatestParams, hashcodeFunction, tsLatestBatchThreads, statsFactory); tsLatestQueue = new TbSqlBlockingQueueWrapper<>(tsLatestParams, hashcodeFunction, tsLatestBatchThreads, statsFactory);
tsLatestQueue.init(logExecutor, v -> { tsLatestQueue.init(logExecutor,
Map<TsKey, TsKvLatestEntity> trueLatest = new HashMap<>(); v -> insertLatestTsRepository.saveOrUpdate(v),
v.forEach(ts -> { Comparator.comparing((Function<TsKvLatestEntity, UUID>) AbstractTsKvEntity::getEntityId)
TsKey key = new TsKey(ts.getEntityId(), ts.getKey()); .thenComparingInt(AbstractTsKvEntity::getKey),
trueLatest.merge(key, ts, (oldTs, newTs) -> oldTs.getTs() <= newTs.getTs() ? newTs : oldTs); v -> {
}); Map<TsKey, TbSqlQueueElement<TsKvLatestEntity, Long>> trueLatest = new HashMap<>();
List<TsKvLatestEntity> latestEntities = new ArrayList<>(trueLatest.values()); v.forEach(element -> {
if (batchSortEnabled) { var entity = element.getEntity();
latestEntities.sort(Comparator.comparing((Function<TsKvLatestEntity, UUID>) AbstractTsKvEntity::getEntityId) TsKey key = new TsKey(entity.getEntityId(), entity.getKey());
.thenComparingInt(AbstractTsKvEntity::getKey)); trueLatest.merge(key, element, (oldElement, newElement) -> oldElement.getEntity().getTs() <= newElement.getEntity().getTs() ? newElement : oldElement);
} });
insertLatestTsRepository.saveOrUpdate(latestEntities); return new ArrayList<>(trueLatest.values());
}, (l, r) -> 0); });
} }
@PreDestroy @PreDestroy
@ -144,7 +146,7 @@ public class SqlTimeseriesLatestDao extends BaseAbstractSqlTimeseriesDao impleme
} }
@Override @Override
public ListenableFuture<Void> saveLatest(TenantId tenantId, EntityId entityId, TsKvEntry tsKvEntry) { public ListenableFuture<Long> saveLatest(TenantId tenantId, EntityId entityId, TsKvEntry tsKvEntry) {
return getSaveLatestFuture(entityId, tsKvEntry); return getSaveLatestFuture(entityId, tsKvEntry);
} }
@ -155,12 +157,13 @@ public class SqlTimeseriesLatestDao extends BaseAbstractSqlTimeseriesDao impleme
@Override @Override
public ListenableFuture<Optional<TsKvEntry>> findLatestOpt(TenantId tenantId, EntityId entityId, String key) { public ListenableFuture<Optional<TsKvEntry>> findLatestOpt(TenantId tenantId, EntityId entityId, String key) {
return service.submit(() -> Optional.ofNullable(doFindLatest(entityId, key))); return service.submit(() -> Optional.ofNullable(doFindLatestSync(entityId, key)));
} }
@Override @Override
public ListenableFuture<TsKvEntry> findLatest(TenantId tenantId, EntityId entityId, String key) { public ListenableFuture<TsKvEntry> findLatest(TenantId tenantId, EntityId entityId, String key) {
return service.submit(() -> getLatestTsKvEntry(entityId, key)); log.trace("findLatest [{}][{}][{}]", tenantId, entityId, key);
return service.submit(() -> wrapNullTsKvEntry(key, doFindLatestSync(entityId, key)));
} }
@Override @Override
@ -187,7 +190,7 @@ public class SqlTimeseriesLatestDao extends BaseAbstractSqlTimeseriesDao impleme
return Futures.transformAsync(future, entryList -> { return Futures.transformAsync(future, entryList -> {
if (entryList.size() == 1) { if (entryList.size() == 1) {
TsKvEntry entry = entryList.get(0); TsKvEntry entry = entryList.get(0);
return Futures.transform(getSaveLatestFuture(entityId, entry), v -> new TsKvLatestRemovingResult(entry), MoreExecutors.directExecutor()); return Futures.transform(getSaveLatestFuture(entityId, entry), v -> new TsKvLatestRemovingResult(entry, v), MoreExecutors.directExecutor());
} else { } else {
log.trace("Could not find new latest value for [{}], key - {}", entityId, query.getKey()); log.trace("Could not find new latest value for [{}], key - {}", entityId, query.getKey());
} }
@ -204,7 +207,7 @@ public class SqlTimeseriesLatestDao extends BaseAbstractSqlTimeseriesDao impleme
ReadTsKvQueryResult::getData, MoreExecutors.directExecutor()); ReadTsKvQueryResult::getData, MoreExecutors.directExecutor());
} }
protected TsKvEntry doFindLatest(EntityId entityId, String key) { protected TsKvEntry doFindLatestSync(EntityId entityId, String key) {
TsKvLatestCompositeKey compositeKey = TsKvLatestCompositeKey compositeKey =
new TsKvLatestCompositeKey( new TsKvLatestCompositeKey(
entityId.getId(), entityId.getId(),
@ -220,24 +223,24 @@ public class SqlTimeseriesLatestDao extends BaseAbstractSqlTimeseriesDao impleme
} }
protected ListenableFuture<TsKvLatestRemovingResult> getRemoveLatestFuture(TenantId tenantId, EntityId entityId, DeleteTsKvQuery query) { protected ListenableFuture<TsKvLatestRemovingResult> getRemoveLatestFuture(TenantId tenantId, EntityId entityId, DeleteTsKvQuery query) {
ListenableFuture<TsKvEntry> latestFuture = service.submit(() -> doFindLatest(entityId, query.getKey())); ListenableFuture<TsKvEntry> latestFuture = service.submit(() -> doFindLatestSync(entityId, query.getKey()));
return Futures.transformAsync(latestFuture, latest -> { return Futures.transformAsync(latestFuture, latest -> {
if (latest == null) { if (latest == null) {
return Futures.immediateFuture(new TsKvLatestRemovingResult(query.getKey(), false)); return Futures.immediateFuture(new TsKvLatestRemovingResult(query.getKey(), false));
} }
boolean isRemoved = false; boolean isRemoved = false;
Long version = null;
long ts = latest.getTs(); long ts = latest.getTs();
if (ts >= query.getStartTs() && ts < query.getEndTs()) { if (ts >= query.getStartTs() && ts < query.getEndTs()) {
TsKvLatestEntity latestEntity = new TsKvLatestEntity(); version = transactionTemplate.execute(status -> jdbcTemplate.query("DELETE FROM ts_kv_latest WHERE entity_id = ? " +
latestEntity.setEntityId(entityId.getId()); "AND key = ? RETURNING nextval('ts_kv_latest_version_seq')",
latestEntity.setKey(keyDictionaryDao.getOrSaveKeyId(query.getKey())); rs -> rs.next() ? rs.getLong(1) : null, entityId.getId(), keyDictionaryDao.getOrSaveKeyId(query.getKey())));
tsKvLatestRepository.delete(latestEntity);
isRemoved = true; isRemoved = true;
if (query.getRewriteLatestIfDeleted()) { if (query.getRewriteLatestIfDeleted()) {
return getNewLatestEntryFuture(tenantId, entityId, query); return getNewLatestEntryFuture(tenantId, entityId, query);
} }
} }
return Futures.immediateFuture(new TsKvLatestRemovingResult(query.getKey(), isRemoved)); return Futures.immediateFuture(new TsKvLatestRemovingResult(query.getKey(), isRemoved, version));
}, MoreExecutors.directExecutor()); }, MoreExecutors.directExecutor());
} }
@ -247,7 +250,7 @@ public class SqlTimeseriesLatestDao extends BaseAbstractSqlTimeseriesDao impleme
searchTsKvLatestRepository.findAllByEntityId(entityId.getId())))); searchTsKvLatestRepository.findAllByEntityId(entityId.getId()))));
} }
protected ListenableFuture<Void> getSaveLatestFuture(EntityId entityId, TsKvEntry tsKvEntry) { protected ListenableFuture<Long> getSaveLatestFuture(EntityId entityId, TsKvEntry tsKvEntry) {
TsKvLatestEntity latestEntity = new TsKvLatestEntity(); TsKvLatestEntity latestEntity = new TsKvLatestEntity();
latestEntity.setEntityId(entityId.getId()); latestEntity.setEntityId(entityId.getId());
latestEntity.setTs(tsKvEntry.getTs()); latestEntity.setTs(tsKvEntry.getTs());
@ -261,10 +264,9 @@ public class SqlTimeseriesLatestDao extends BaseAbstractSqlTimeseriesDao impleme
return tsLatestQueue.add(latestEntity); return tsLatestQueue.add(latestEntity);
} }
private TsKvEntry getLatestTsKvEntry(EntityId entityId, String key) { protected TsKvEntry wrapNullTsKvEntry(final String key, final TsKvEntry latest) {
TsKvEntry latest = doFindLatest(entityId, key);
if (latest == null) { if (latest == null) {
latest = new BasicTsKvEntry(System.currentTimeMillis(), new StringDataEntry(key, null)); return new BasicTsKvEntry(System.currentTimeMillis(), new StringDataEntry(key, null));
} }
return latest; return latest;
} }

2
dao/src/main/java/org/thingsboard/server/dao/sqlts/insert/latest/InsertLatestTsRepository.java

@ -21,6 +21,6 @@ import java.util.List;
public interface InsertLatestTsRepository { public interface InsertLatestTsRepository {
void saveOrUpdate(List<TsKvLatestEntity> entities); List<Long> saveOrUpdate(List<TsKvLatestEntity> entities);
} }

224
dao/src/main/java/org/thingsboard/server/dao/sqlts/insert/latest/sql/SqlLatestInsertTsRepository.java

@ -15,14 +15,12 @@
*/ */
package org.thingsboard.server.dao.sqlts.insert.latest.sql; package org.thingsboard.server.dao.sqlts.insert.latest.sql;
import jakarta.annotation.PostConstruct;
import org.springframework.beans.factory.annotation.Value; import org.springframework.beans.factory.annotation.Value;
import org.springframework.jdbc.core.BatchPreparedStatementSetter;
import org.springframework.stereotype.Repository; import org.springframework.stereotype.Repository;
import org.springframework.transaction.TransactionStatus;
import org.springframework.transaction.annotation.Transactional; import org.springframework.transaction.annotation.Transactional;
import org.springframework.transaction.support.TransactionCallbackWithoutResult; import org.thingsboard.server.dao.AbstractVersionedInsertRepository;
import org.thingsboard.server.dao.model.sqlts.latest.TsKvLatestEntity; import org.thingsboard.server.dao.model.sqlts.latest.TsKvLatestEntity;
import org.thingsboard.server.dao.sqlts.insert.AbstractInsertRepository;
import org.thingsboard.server.dao.sqlts.insert.latest.InsertLatestTsRepository; import org.thingsboard.server.dao.sqlts.insert.latest.InsertLatestTsRepository;
import org.thingsboard.server.dao.util.SqlDao; import org.thingsboard.server.dao.util.SqlDao;
import org.thingsboard.server.dao.util.SqlTsLatestAnyDao; import org.thingsboard.server.dao.util.SqlTsLatestAnyDao;
@ -30,143 +28,123 @@ import org.thingsboard.server.dao.util.SqlTsLatestAnyDao;
import java.sql.PreparedStatement; import java.sql.PreparedStatement;
import java.sql.SQLException; import java.sql.SQLException;
import java.sql.Types; import java.sql.Types;
import java.util.ArrayList;
import java.util.List; import java.util.List;
@SqlTsLatestAnyDao @SqlTsLatestAnyDao
@Repository @Repository
@Transactional @Transactional
@SqlDao @SqlDao
public class SqlLatestInsertTsRepository extends AbstractInsertRepository implements InsertLatestTsRepository { public class SqlLatestInsertTsRepository extends AbstractVersionedInsertRepository<TsKvLatestEntity> implements InsertLatestTsRepository {
@Value("${sql.ts_latest.update_by_latest_ts:true}") @Value("${sql.ts_latest.update_by_latest_ts:true}")
private Boolean updateByLatestTs; private Boolean updateByLatestTs;
private static final String BATCH_UPDATE = private static final String BATCH_UPDATE =
"UPDATE ts_kv_latest SET ts = ?, bool_v = ?, str_v = ?, long_v = ?, dbl_v = ?, json_v = cast(? AS json) WHERE entity_id = ? AND key = ?"; "UPDATE ts_kv_latest SET ts = ?, bool_v = ?, str_v = ?, long_v = ?, dbl_v = ?, json_v = cast(? AS json), version = nextval('ts_kv_latest_version_seq') WHERE entity_id = ? AND key = ?";
private static final String INSERT_OR_UPDATE = private static final String INSERT_OR_UPDATE =
"INSERT INTO ts_kv_latest (entity_id, key, ts, bool_v, str_v, long_v, dbl_v, json_v) VALUES(?, ?, ?, ?, ?, ?, ?, cast(? AS json)) " + "INSERT INTO ts_kv_latest (entity_id, key, ts, bool_v, str_v, long_v, dbl_v, json_v, version) VALUES(?, ?, ?, ?, ?, ?, ?, cast(? AS json), nextval('ts_kv_latest_version_seq')) " +
"ON CONFLICT (entity_id, key) DO UPDATE SET ts = ?, bool_v = ?, str_v = ?, long_v = ?, dbl_v = ?, json_v = cast(? AS json)"; "ON CONFLICT (entity_id, key) DO UPDATE SET ts = ?, bool_v = ?, str_v = ?, long_v = ?, dbl_v = ?, json_v = cast(? AS json), version = nextval('ts_kv_latest_version_seq')";
private static final String BATCH_UPDATE_BY_LATEST_TS = BATCH_UPDATE + " AND ts_kv_latest.ts <= ?"; private static final String BATCH_UPDATE_BY_LATEST_TS = BATCH_UPDATE + " AND ts_kv_latest.ts <= ?";
private static final String INSERT_OR_UPDATE_BY_LATEST_TS = INSERT_OR_UPDATE + " WHERE ts_kv_latest.ts <= ?"; private static final String INSERT_OR_UPDATE_BY_LATEST_TS = INSERT_OR_UPDATE + " WHERE ts_kv_latest.ts <= ?";
private static final String RETURNING = " RETURNING version";
private String batchUpdateQuery;
private String insertOrUpdateQuery;
@PostConstruct
private void init() {
this.batchUpdateQuery = (updateByLatestTs ? BATCH_UPDATE_BY_LATEST_TS : BATCH_UPDATE) + RETURNING;
this.insertOrUpdateQuery = (updateByLatestTs ? INSERT_OR_UPDATE_BY_LATEST_TS : INSERT_OR_UPDATE) + RETURNING;
}
@Override
protected void setOnBatchUpdateValues(PreparedStatement ps, int i, List<TsKvLatestEntity> entities) throws SQLException {
TsKvLatestEntity tsKvLatestEntity = entities.get(i);
ps.setLong(1, tsKvLatestEntity.getTs());
if (tsKvLatestEntity.getBooleanValue() != null) {
ps.setBoolean(2, tsKvLatestEntity.getBooleanValue());
} else {
ps.setNull(2, Types.BOOLEAN);
}
ps.setString(3, replaceNullChars(tsKvLatestEntity.getStrValue()));
if (tsKvLatestEntity.getLongValue() != null) {
ps.setLong(4, tsKvLatestEntity.getLongValue());
} else {
ps.setNull(4, Types.BIGINT);
}
if (tsKvLatestEntity.getDoubleValue() != null) {
ps.setDouble(5, tsKvLatestEntity.getDoubleValue());
} else {
ps.setNull(5, Types.DOUBLE);
}
ps.setString(6, replaceNullChars(tsKvLatestEntity.getJsonValue()));
ps.setObject(7, tsKvLatestEntity.getEntityId());
ps.setInt(8, tsKvLatestEntity.getKey());
if (updateByLatestTs) {
ps.setLong(9, tsKvLatestEntity.getTs());
}
}
@Override
protected void setOnInsertOrUpdateValues(PreparedStatement ps, int i, List<TsKvLatestEntity> insertEntities) throws SQLException {
TsKvLatestEntity tsKvLatestEntity = insertEntities.get(i);
ps.setObject(1, tsKvLatestEntity.getEntityId());
ps.setInt(2, tsKvLatestEntity.getKey());
ps.setLong(3, tsKvLatestEntity.getTs());
ps.setLong(9, tsKvLatestEntity.getTs());
if (updateByLatestTs) {
ps.setLong(15, tsKvLatestEntity.getTs());
}
if (tsKvLatestEntity.getBooleanValue() != null) {
ps.setBoolean(4, tsKvLatestEntity.getBooleanValue());
ps.setBoolean(10, tsKvLatestEntity.getBooleanValue());
} else {
ps.setNull(4, Types.BOOLEAN);
ps.setNull(10, Types.BOOLEAN);
}
ps.setString(5, replaceNullChars(tsKvLatestEntity.getStrValue()));
ps.setString(11, replaceNullChars(tsKvLatestEntity.getStrValue()));
if (tsKvLatestEntity.getLongValue() != null) {
ps.setLong(6, tsKvLatestEntity.getLongValue());
ps.setLong(12, tsKvLatestEntity.getLongValue());
} else {
ps.setNull(6, Types.BIGINT);
ps.setNull(12, Types.BIGINT);
}
if (tsKvLatestEntity.getDoubleValue() != null) {
ps.setDouble(7, tsKvLatestEntity.getDoubleValue());
ps.setDouble(13, tsKvLatestEntity.getDoubleValue());
} else {
ps.setNull(7, Types.DOUBLE);
ps.setNull(13, Types.DOUBLE);
}
ps.setString(8, replaceNullChars(tsKvLatestEntity.getJsonValue()));
ps.setString(14, replaceNullChars(tsKvLatestEntity.getJsonValue()));
}
@Override
protected String getBatchUpdateQuery() {
return batchUpdateQuery;
}
@Override @Override
public void saveOrUpdate(List<TsKvLatestEntity> entities) { protected String getInsertOrUpdateQuery() {
transactionTemplate.execute(new TransactionCallbackWithoutResult() { return insertOrUpdateQuery;
@Override
protected void doInTransactionWithoutResult(TransactionStatus status) {
String batchUpdateQuery = updateByLatestTs ? BATCH_UPDATE_BY_LATEST_TS : BATCH_UPDATE;
String insertOrUpdateQuery = updateByLatestTs ? INSERT_OR_UPDATE_BY_LATEST_TS : INSERT_OR_UPDATE;
int[] result = jdbcTemplate.batchUpdate(batchUpdateQuery, new BatchPreparedStatementSetter() {
@Override
public void setValues(PreparedStatement ps, int i) throws SQLException {
TsKvLatestEntity tsKvLatestEntity = entities.get(i);
ps.setLong(1, tsKvLatestEntity.getTs());
if (tsKvLatestEntity.getBooleanValue() != null) {
ps.setBoolean(2, tsKvLatestEntity.getBooleanValue());
} else {
ps.setNull(2, Types.BOOLEAN);
}
ps.setString(3, replaceNullChars(tsKvLatestEntity.getStrValue()));
if (tsKvLatestEntity.getLongValue() != null) {
ps.setLong(4, tsKvLatestEntity.getLongValue());
} else {
ps.setNull(4, Types.BIGINT);
}
if (tsKvLatestEntity.getDoubleValue() != null) {
ps.setDouble(5, tsKvLatestEntity.getDoubleValue());
} else {
ps.setNull(5, Types.DOUBLE);
}
ps.setString(6, replaceNullChars(tsKvLatestEntity.getJsonValue()));
ps.setObject(7, tsKvLatestEntity.getEntityId());
ps.setInt(8, tsKvLatestEntity.getKey());
if (updateByLatestTs) {
ps.setLong(9, tsKvLatestEntity.getTs());
}
}
@Override
public int getBatchSize() {
return entities.size();
}
});
int updatedCount = 0;
for (int i = 0; i < result.length; i++) {
if (result[i] == 0) {
updatedCount++;
}
}
List<TsKvLatestEntity> insertEntities = new ArrayList<>(updatedCount);
for (int i = 0; i < result.length; i++) {
if (result[i] == 0) {
insertEntities.add(entities.get(i));
}
}
jdbcTemplate.batchUpdate(insertOrUpdateQuery, new BatchPreparedStatementSetter() {
@Override
public void setValues(PreparedStatement ps, int i) throws SQLException {
TsKvLatestEntity tsKvLatestEntity = insertEntities.get(i);
ps.setObject(1, tsKvLatestEntity.getEntityId());
ps.setInt(2, tsKvLatestEntity.getKey());
ps.setLong(3, tsKvLatestEntity.getTs());
ps.setLong(9, tsKvLatestEntity.getTs());
if (updateByLatestTs) {
ps.setLong(15, tsKvLatestEntity.getTs());
}
if (tsKvLatestEntity.getBooleanValue() != null) {
ps.setBoolean(4, tsKvLatestEntity.getBooleanValue());
ps.setBoolean(10, tsKvLatestEntity.getBooleanValue());
} else {
ps.setNull(4, Types.BOOLEAN);
ps.setNull(10, Types.BOOLEAN);
}
ps.setString(5, replaceNullChars(tsKvLatestEntity.getStrValue()));
ps.setString(11, replaceNullChars(tsKvLatestEntity.getStrValue()));
if (tsKvLatestEntity.getLongValue() != null) {
ps.setLong(6, tsKvLatestEntity.getLongValue());
ps.setLong(12, tsKvLatestEntity.getLongValue());
} else {
ps.setNull(6, Types.BIGINT);
ps.setNull(12, Types.BIGINT);
}
if (tsKvLatestEntity.getDoubleValue() != null) {
ps.setDouble(7, tsKvLatestEntity.getDoubleValue());
ps.setDouble(13, tsKvLatestEntity.getDoubleValue());
} else {
ps.setNull(7, Types.DOUBLE);
ps.setNull(13, Types.DOUBLE);
}
ps.setString(8, replaceNullChars(tsKvLatestEntity.getJsonValue()));
ps.setString(14, replaceNullChars(tsKvLatestEntity.getJsonValue()));
}
@Override
public int getBatchSize() {
return insertEntities.size();
}
});
}
});
} }
} }

2
dao/src/main/java/org/thingsboard/server/dao/sqlts/latest/SearchTsKvLatestRepository.java

@ -31,7 +31,7 @@ public class SearchTsKvLatestRepository {
public static final String FIND_ALL_BY_ENTITY_ID = "findAllByEntityId"; public static final String FIND_ALL_BY_ENTITY_ID = "findAllByEntityId";
public static final String FIND_ALL_BY_ENTITY_ID_QUERY = "SELECT ts_kv_latest.entity_id AS entityId, ts_kv_latest.key AS key, key_dictionary.key AS strKey, ts_kv_latest.str_v AS strValue," + public static final String FIND_ALL_BY_ENTITY_ID_QUERY = "SELECT ts_kv_latest.entity_id AS entityId, ts_kv_latest.key AS key, key_dictionary.key AS strKey, ts_kv_latest.str_v AS strValue," +
" ts_kv_latest.bool_v AS boolValue, ts_kv_latest.long_v AS longValue, ts_kv_latest.dbl_v AS doubleValue, ts_kv_latest.json_v AS jsonValue, ts_kv_latest.ts AS ts FROM ts_kv_latest " + " ts_kv_latest.bool_v AS boolValue, ts_kv_latest.long_v AS longValue, ts_kv_latest.dbl_v AS doubleValue, ts_kv_latest.json_v AS jsonValue, ts_kv_latest.ts AS ts, ts_kv_latest.version AS version FROM ts_kv_latest " +
"INNER JOIN key_dictionary ON ts_kv_latest.key = key_dictionary.key_id WHERE ts_kv_latest.entity_id = cast(:id AS uuid)"; "INNER JOIN key_dictionary ON ts_kv_latest.key = key_dictionary.key_id WHERE ts_kv_latest.entity_id = cast(:id AS uuid)";
@PersistenceContext @PersistenceContext

2
dao/src/main/java/org/thingsboard/server/dao/sqlts/timescale/TimescaleTimeseriesDao.java

@ -75,7 +75,7 @@ public class TimescaleTimeseriesDao extends AbstractSqlTimeseriesDao implements
@Autowired @Autowired
protected KeyDictionaryDao keyDictionaryDao; protected KeyDictionaryDao keyDictionaryDao;
protected TbSqlBlockingQueueWrapper<TimescaleTsKvEntity> tsQueue; protected TbSqlBlockingQueueWrapper<TimescaleTsKvEntity, Void> tsQueue;
@PostConstruct @PostConstruct
protected void init() { protected void init() {

4
dao/src/main/java/org/thingsboard/server/dao/timeseries/BaseTimeseriesService.java

@ -187,8 +187,8 @@ public class BaseTimeseriesService implements TimeseriesService {
} }
@Override @Override
public ListenableFuture<List<Void>> saveLatest(TenantId tenantId, EntityId entityId, List<TsKvEntry> tsKvEntries) { public ListenableFuture<List<Long>> saveLatest(TenantId tenantId, EntityId entityId, List<TsKvEntry> tsKvEntries) {
List<ListenableFuture<Void>> futures = new ArrayList<>(tsKvEntries.size()); List<ListenableFuture<Long>> futures = new ArrayList<>(tsKvEntries.size());
for (TsKvEntry tsKvEntry : tsKvEntries) { for (TsKvEntry tsKvEntry : tsKvEntries) {
futures.add(timeseriesLatestDao.saveLatest(tenantId, entityId, tsKvEntry)); futures.add(timeseriesLatestDao.saveLatest(tenantId, entityId, tsKvEntry));
} }

4
dao/src/main/java/org/thingsboard/server/dao/timeseries/CassandraBaseTimeseriesLatestDao.java

@ -100,7 +100,7 @@ public class CassandraBaseTimeseriesLatestDao extends AbstractCassandraBaseTimes
} }
@Override @Override
public ListenableFuture<Void> saveLatest(TenantId tenantId, EntityId entityId, TsKvEntry tsKvEntry) { public ListenableFuture<Long> saveLatest(TenantId tenantId, EntityId entityId, TsKvEntry tsKvEntry) {
BoundStatementBuilder stmtBuilder = new BoundStatementBuilder(getLatestStmt().bind()); BoundStatementBuilder stmtBuilder = new BoundStatementBuilder(getLatestStmt().bind());
stmtBuilder.setString(0, entityId.getEntityType().name()) stmtBuilder.setString(0, entityId.getEntityType().name())
.setUuid(1, entityId.getId()) .setUuid(1, entityId.getId())
@ -161,7 +161,7 @@ public class CassandraBaseTimeseriesLatestDao extends AbstractCassandraBaseTimes
var entryList = result.getData(); var entryList = result.getData();
if (entryList.size() == 1) { if (entryList.size() == 1) {
TsKvEntry entry = entryList.get(0); TsKvEntry entry = entryList.get(0);
return Futures.transform(saveLatest(tenantId, entityId, entryList.get(0)), v -> new TsKvLatestRemovingResult(entry), MoreExecutors.directExecutor()); return Futures.transform(saveLatest(tenantId, entityId, entryList.get(0)), v -> new TsKvLatestRemovingResult(entry, v), MoreExecutors.directExecutor());
} else { } else {
log.trace("Could not find new latest value for [{}], key - {}", entityId, query.getKey()); log.trace("Could not find new latest value for [{}], key - {}", entityId, query.getKey());
} }

2
dao/src/main/java/org/thingsboard/server/dao/timeseries/TimeseriesLatestDao.java

@ -42,7 +42,7 @@ public interface TimeseriesLatestDao {
ListenableFuture<List<TsKvEntry>> findAllLatest(TenantId tenantId, EntityId entityId); ListenableFuture<List<TsKvEntry>> findAllLatest(TenantId tenantId, EntityId entityId);
ListenableFuture<Void> saveLatest(TenantId tenantId, EntityId entityId, TsKvEntry tsKvEntry); ListenableFuture<Long> saveLatest(TenantId tenantId, EntityId entityId, TsKvEntry tsKvEntry);
ListenableFuture<TsKvLatestRemovingResult> removeLatest(TenantId tenantId, EntityId entityId, DeleteTsKvQuery query); ListenableFuture<TsKvLatestRemovingResult> removeLatest(TenantId tenantId, EntityId entityId, DeleteTsKvQuery query);

40
dao/src/main/java/org/thingsboard/server/dao/timeseries/TsLatestCacheKey.java

@ -0,0 +1,40 @@
/**
* Copyright © 2016-2024 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.timeseries;
import lombok.AllArgsConstructor;
import lombok.EqualsAndHashCode;
import lombok.Getter;
import org.thingsboard.server.common.data.AttributeScope;
import org.thingsboard.server.common.data.id.EntityId;
import java.io.Serial;
import java.io.Serializable;
@EqualsAndHashCode
@Getter
@AllArgsConstructor
public class TsLatestCacheKey implements Serializable {
private static final long serialVersionUID = 2024369077925351881L;
private final EntityId entityId;
private final String key;
@Override
public String toString() {
return "{" + entityId + "}" + key;
}
}

55
dao/src/main/java/org/thingsboard/server/dao/timeseries/TsLatestRedisCache.java

@ -0,0 +1,55 @@
/**
* Copyright © 2016-2024 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.timeseries;
import com.google.protobuf.InvalidProtocolBufferException;
import lombok.extern.slf4j.Slf4j;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.data.redis.connection.RedisConnectionFactory;
import org.springframework.data.redis.serializer.SerializationException;
import org.springframework.stereotype.Service;
import org.thingsboard.server.cache.CacheSpecsMap;
import org.thingsboard.server.cache.TBRedisCacheConfiguration;
import org.thingsboard.server.cache.TbRedisSerializer;
import org.thingsboard.server.cache.VersionedRedisTbCache;
import org.thingsboard.server.common.data.CacheConstants;
import org.thingsboard.server.common.data.kv.TsKvEntry;
import org.thingsboard.server.common.util.KvProtoUtil;
import org.thingsboard.server.gen.transport.TransportProtos;
@ConditionalOnProperty(prefix = "cache", value = "type", havingValue = "redis")
@Service("TsLatestCache")
@Slf4j
public class TsLatestRedisCache extends VersionedRedisTbCache<TsLatestCacheKey, TsKvEntry> {
public TsLatestRedisCache(TBRedisCacheConfiguration configuration, CacheSpecsMap cacheSpecsMap, RedisConnectionFactory connectionFactory) {
super(CacheConstants.TS_LATEST_CACHE, cacheSpecsMap, connectionFactory, configuration, new TbRedisSerializer<>() {
@Override
public byte[] serialize(TsKvEntry tsKvEntry) throws SerializationException {
return KvProtoUtil.toTsKvProto(tsKvEntry.getTs(), tsKvEntry, tsKvEntry.getVersion()).toByteArray();
}
@Override
public TsKvEntry deserialize(TsLatestCacheKey key, byte[] bytes) throws SerializationException {
try {
return KvProtoUtil.fromTsKvProto(TransportProtos.TsKvProto.parseFrom(bytes));
} catch (InvalidProtocolBufferException e) {
throw new SerializationException(e.getMessage());
}
}
});
}
}

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

@ -102,6 +102,8 @@ CREATE TABLE IF NOT EXISTS audit_log (
action_failure_details varchar(1000000) action_failure_details varchar(1000000)
) PARTITION BY RANGE (created_time); ) PARTITION BY RANGE (created_time);
CREATE SEQUENCE IF NOT EXISTS attribute_kv_version_seq cache 1;
CREATE TABLE IF NOT EXISTS attribute_kv ( CREATE TABLE IF NOT EXISTS attribute_kv (
entity_id uuid, entity_id uuid,
attribute_type int, attribute_type int,
@ -112,6 +114,7 @@ CREATE TABLE IF NOT EXISTS attribute_kv (
dbl_v double precision, dbl_v double precision,
json_v json, json_v json,
last_update_ts bigint, last_update_ts bigint,
version bigint default 0,
CONSTRAINT attribute_kv_pkey PRIMARY KEY (entity_id, attribute_type, attribute_key) CONSTRAINT attribute_kv_pkey PRIMARY KEY (entity_id, attribute_type, attribute_key)
); );
@ -417,6 +420,8 @@ CREATE TABLE IF NOT EXISTS error_event (
e_error varchar e_error varchar
) PARTITION BY RANGE (ts); ) PARTITION BY RANGE (ts);
CREATE SEQUENCE IF NOT EXISTS relation_version_seq cache 1;
CREATE TABLE IF NOT EXISTS relation ( CREATE TABLE IF NOT EXISTS relation (
from_id uuid, from_id uuid,
from_type varchar(255), from_type varchar(255),
@ -425,6 +430,7 @@ CREATE TABLE IF NOT EXISTS relation (
relation_type_group varchar(255), relation_type_group varchar(255),
relation_type varchar(255), relation_type varchar(255),
additional_info varchar, additional_info varchar,
version bigint default 0,
CONSTRAINT relation_pkey PRIMARY KEY (from_id, from_type, relation_type_group, relation_type, to_id, to_type) CONSTRAINT relation_pkey PRIMARY KEY (from_id, from_type, relation_type_group, relation_type, to_id, to_type)
); );
@ -538,6 +544,8 @@ CREATE TABLE IF NOT EXISTS entity_view (
CONSTRAINT entity_view_external_id_unq_key UNIQUE (tenant_id, external_id) CONSTRAINT entity_view_external_id_unq_key UNIQUE (tenant_id, external_id)
); );
CREATE SEQUENCE IF NOT EXISTS ts_kv_latest_version_seq cache 1;
CREATE TABLE IF NOT EXISTS ts_kv_latest CREATE TABLE IF NOT EXISTS ts_kv_latest
( (
entity_id uuid NOT NULL, entity_id uuid NOT NULL,
@ -548,6 +556,7 @@ CREATE TABLE IF NOT EXISTS ts_kv_latest
long_v bigint, long_v bigint,
dbl_v double precision, dbl_v double precision,
json_v json, json_v json,
version bigint default 0,
CONSTRAINT ts_kv_latest_pkey PRIMARY KEY (entity_id, key) CONSTRAINT ts_kv_latest_pkey PRIMARY KEY (entity_id, key)
); );

3
dao/src/main/resources/sql/schema-timescale.sql

@ -34,6 +34,8 @@ CREATE TABLE IF NOT EXISTS key_dictionary (
CONSTRAINT key_dictionary_id_pkey PRIMARY KEY (key) CONSTRAINT key_dictionary_id_pkey PRIMARY KEY (key)
); );
CREATE SEQUENCE IF NOT EXISTS ts_kv_latest_version_seq cache 1;
CREATE TABLE IF NOT EXISTS ts_kv_latest ( CREATE TABLE IF NOT EXISTS ts_kv_latest (
entity_id uuid NOT NULL, entity_id uuid NOT NULL,
key int NOT NULL, key int NOT NULL,
@ -43,6 +45,7 @@ CREATE TABLE IF NOT EXISTS ts_kv_latest (
long_v bigint, long_v bigint,
dbl_v double precision, dbl_v double precision,
json_v json, json_v json,
version bigint default 0,
CONSTRAINT ts_kv_latest_pkey PRIMARY KEY (entity_id, key) CONSTRAINT ts_kv_latest_pkey PRIMARY KEY (entity_id, key)
); );

10
dao/src/main/resources/sql/schema-ts-latest-psql.sql

@ -14,6 +14,8 @@
-- limitations under the License. -- limitations under the License.
-- --
CREATE SEQUENCE IF NOT EXISTS ts_kv_latest_version_seq cache 1;
CREATE TABLE IF NOT EXISTS ts_kv_latest CREATE TABLE IF NOT EXISTS ts_kv_latest
( (
entity_id uuid NOT NULL, entity_id uuid NOT NULL,
@ -24,12 +26,6 @@ CREATE TABLE IF NOT EXISTS ts_kv_latest
long_v bigint, long_v bigint,
dbl_v double precision, dbl_v double precision,
json_v json, json_v json,
version bigint default 0,
CONSTRAINT ts_kv_latest_pkey PRIMARY KEY (entity_id, key) CONSTRAINT ts_kv_latest_pkey PRIMARY KEY (entity_id, key)
); );
CREATE TABLE IF NOT EXISTS ts_kv_dictionary
(
key varchar(255) NOT NULL,
key_id serial UNIQUE,
CONSTRAINT ts_key_id_pkey PRIMARY KEY (key)
);

91
dao/src/test/java/org/thingsboard/server/dao/AbstractRedisClusterContainer.java

@ -0,0 +1,91 @@
/**
* Copyright © 2016-2024 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;
import lombok.extern.slf4j.Slf4j;
import org.junit.ClassRule;
import org.junit.rules.ExternalResource;
import org.testcontainers.containers.GenericContainer;
import org.testcontainers.containers.Network;
import org.testcontainers.containers.output.OutputFrame;
import java.util.List;
import java.util.concurrent.TimeUnit;
@Slf4j
public class AbstractRedisClusterContainer {
static final String nodes = "127.0.0.1:6371,127.0.0.1:6372,127.0.0.1:6373,127.0.0.1:6374,127.0.0.1:6375,127.0.0.1:6376";
@ClassRule(order = 0)
public static Network network = Network.newNetwork();
@ClassRule(order = 1)
public static GenericContainer redis1 = new GenericContainer("bitnami/redis-cluster:latest").withEnv("REDIS_PORT_NUMBER", "6371").withNetworkMode("host").withLogConsumer(x -> log.warn("{}", ((OutputFrame) x).getUtf8StringWithoutLineEnding())).withEnv("ALLOW_EMPTY_PASSWORD", "yes").withEnv("REDIS_NODES", nodes);
@ClassRule(order = 2)
public static GenericContainer redis2 = new GenericContainer("bitnami/redis-cluster:latest").withEnv("REDIS_PORT_NUMBER", "6372").withNetworkMode("host").withLogConsumer(x -> log.warn("{}", ((OutputFrame) x).getUtf8StringWithoutLineEnding())).withEnv("ALLOW_EMPTY_PASSWORD", "yes").withEnv("REDIS_NODES", nodes);
@ClassRule(order = 3)
public static GenericContainer redis3 = new GenericContainer("bitnami/redis-cluster:latest").withEnv("REDIS_PORT_NUMBER", "6373").withNetworkMode("host").withLogConsumer(x -> log.warn("{}", ((OutputFrame) x).getUtf8StringWithoutLineEnding())).withEnv("ALLOW_EMPTY_PASSWORD", "yes").withEnv("REDIS_NODES", nodes);
@ClassRule(order = 4)
public static GenericContainer redis4 = new GenericContainer("bitnami/redis-cluster:latest").withEnv("REDIS_PORT_NUMBER", "6374").withNetworkMode("host").withLogConsumer(x -> log.warn("{}", ((OutputFrame) x).getUtf8StringWithoutLineEnding())).withEnv("ALLOW_EMPTY_PASSWORD", "yes").withEnv("REDIS_NODES", nodes);
@ClassRule(order = 5)
public static GenericContainer redis5 = new GenericContainer("bitnami/redis-cluster:latest").withEnv("REDIS_PORT_NUMBER", "6375").withNetworkMode("host").withLogConsumer(x -> log.warn("{}", ((OutputFrame) x).getUtf8StringWithoutLineEnding())).withEnv("ALLOW_EMPTY_PASSWORD", "yes").withEnv("REDIS_NODES", nodes);
@ClassRule(order = 6)
public static GenericContainer redis6 = new GenericContainer("bitnami/redis-cluster:latest").withEnv("REDIS_PORT_NUMBER", "6376").withNetworkMode("host").withLogConsumer(x -> log.warn("{}", ((OutputFrame) x).getUtf8StringWithoutLineEnding())).withEnv("ALLOW_EMPTY_PASSWORD", "yes").withEnv("REDIS_NODES", nodes);
@ClassRule(order = 100)
public static ExternalResource resource = new ExternalResource() {
@Override
protected void before() throws Throwable {
redis1.start();
redis2.start();
redis3.start();
redis4.start();
redis5.start();
redis6.start();
Thread.sleep(TimeUnit.SECONDS.toMillis(5)); // otherwise not all containers have time to start
String clusterCreateCommand = "echo yes | redis-cli --cluster create " +
"127.0.0.1:6371 127.0.0.1:6372 127.0.0.1:6373 127.0.0.1:6374 127.0.0.1:6375 127.0.0.1:6376 " +
"--cluster-replicas 1";
log.warn("Command to init Redis Cluster: {}", clusterCreateCommand);
var result = redis6.execInContainer("/bin/sh", "-c", clusterCreateCommand);
log.warn("Init cluster result: {}", result);
Thread.sleep(TimeUnit.SECONDS.toMillis(5)); // otherwise cluster not always ready
log.warn("Connect to nodes: {}", nodes);
System.setProperty("cache.type", "redis");
System.setProperty("redis.connection.type", "cluster");
System.setProperty("redis.cluster.nodes", nodes);
System.setProperty("redis.cluster.useDefaultPoolConfig", "false");
}
@Override
protected void after() {
redis1.stop();
redis2.stop();
redis3.stop();
redis4.stop();
redis5.stop();
redis6.stop();
List.of("cache.type", "redis.connection.type", "redis.cluster.nodes", "redis.cluster.useDefaultPoolConfig\"")
.forEach(System.getProperties()::remove);
}
};
}

31
dao/src/test/java/org/thingsboard/server/dao/RedisClusterSqlTestSuite.java

@ -0,0 +1,31 @@
/**
* Copyright © 2016-2024 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;
import org.junit.extensions.cpsuite.ClasspathSuite;
import org.junit.extensions.cpsuite.ClasspathSuite.ClassnameFilters;
import org.junit.runner.RunWith;
@RunWith(ClasspathSuite.class)
@ClassnameFilters(
//All the same tests using redis instead of caffeine.
{
"org.thingsboard.server.dao.service.*ServiceSqlTest",
}
)
public class RedisClusterSqlTestSuite extends AbstractRedisClusterContainer {
}

64
dao/src/test/java/org/thingsboard/server/dao/RedisJUnit5Test.java

@ -0,0 +1,64 @@
/**
* Copyright © 2016-2024 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;
import lombok.extern.slf4j.Slf4j;
import org.junit.jupiter.api.AfterAll;
import org.junit.jupiter.api.BeforeAll;
import org.junit.jupiter.api.Test;
import org.testcontainers.containers.GenericContainer;
import org.testcontainers.containers.output.OutputFrame;
import org.testcontainers.junit.jupiter.Container;
import org.testcontainers.junit.jupiter.Testcontainers;
import java.util.List;
import static org.assertj.core.api.Assertions.assertThat;
@Testcontainers
@Slf4j
public class RedisJUnit5Test {
@Container
private static final GenericContainer REDIS = new GenericContainer("redis:7.2-bookworm")
.withLogConsumer(s -> log.error(((OutputFrame) s).getUtf8String().trim()))
.withExposedPorts(6379);
@BeforeAll
static void beforeAll() {
log.warn("Starting redis...");
REDIS.start();
System.setProperty("cache.type", "redis");
System.setProperty("redis.connection.type", "standalone");
System.setProperty("redis.standalone.host", REDIS.getHost());
System.setProperty("redis.standalone.port", String.valueOf(REDIS.getMappedPort(6379)));
}
@AfterAll
static void afterAll() {
List.of("cache.type", "redis.connection.type", "redis.standalone.host", "redis.standalone.port")
.forEach(System.getProperties()::remove);
REDIS.stop();
log.warn("Redis is stopped");
}
@Test
void test() {
assertThat(REDIS.isRunning()).isTrue();
}
}

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

@ -340,7 +340,7 @@ public class AlarmServiceTest extends AbstractServiceTest {
EntityRelation relation = new EntityRelation(parentId, childId, EntityRelation.CONTAINS_TYPE); EntityRelation relation = new EntityRelation(parentId, childId, EntityRelation.CONTAINS_TYPE);
Assert.assertTrue(relationService.saveRelation(tenantId, relation)); Assert.assertNotNull(relationService.saveRelation(tenantId, relation));
long ts = System.currentTimeMillis(); long ts = System.currentTimeMillis();
AlarmApiCallResult result = alarmService.createAlarm(AlarmCreateOrUpdateActiveRequest.builder() AlarmApiCallResult result = alarmService.createAlarm(AlarmCreateOrUpdateActiveRequest.builder()
@ -877,7 +877,7 @@ public class AlarmServiceTest extends AbstractServiceTest {
EntityRelation relation = new EntityRelation(parentId, childId, EntityRelation.CONTAINS_TYPE); EntityRelation relation = new EntityRelation(parentId, childId, EntityRelation.CONTAINS_TYPE);
Assert.assertTrue(relationService.saveRelation(tenantId, relation)); Assert.assertNotNull(relationService.saveRelation(tenantId, relation));
long ts = System.currentTimeMillis(); long ts = System.currentTimeMillis();
AlarmApiCallResult result = alarmService.createAlarm(AlarmCreateOrUpdateActiveRequest.builder() AlarmApiCallResult result = alarmService.createAlarm(AlarmCreateOrUpdateActiveRequest.builder()

16
dao/src/test/java/org/thingsboard/server/dao/service/EntityServiceTest.java

@ -412,7 +412,7 @@ public class EntityServiceTest extends AbstractServiceTest {
List<Long> highTemperatures = new ArrayList<>(); List<Long> highTemperatures = new ArrayList<>();
createTestHierarchy(tenantId, assets, devices, new ArrayList<>(), new ArrayList<>(), temperatures, highTemperatures); createTestHierarchy(tenantId, assets, devices, new ArrayList<>(), new ArrayList<>(), temperatures, highTemperatures);
List<ListenableFuture<List<String>>> attributeFutures = new ArrayList<>(); List<ListenableFuture<List<Long>>> attributeFutures = new ArrayList<>();
for (int i = 0; i < devices.size(); i++) { for (int i = 0; i < devices.size(); i++) {
Device device = devices.get(i); Device device = devices.get(i);
attributeFutures.add(saveLongAttribute(device.getId(), "temperature", temperatures.get(i), AttributeScope.CLIENT_SCOPE)); attributeFutures.add(saveLongAttribute(device.getId(), "temperature", temperatures.get(i), AttributeScope.CLIENT_SCOPE));
@ -591,7 +591,7 @@ public class EntityServiceTest extends AbstractServiceTest {
List<Long> highTemperatures = new ArrayList<>(); List<Long> highTemperatures = new ArrayList<>();
createTestHierarchy(tenantId, assets, devices, new ArrayList<>(), new ArrayList<>(), temperatures, highTemperatures); createTestHierarchy(tenantId, assets, devices, new ArrayList<>(), new ArrayList<>(), temperatures, highTemperatures);
List<ListenableFuture<List<String>>> attributeFutures = new ArrayList<>(); List<ListenableFuture<List<Long>>> attributeFutures = new ArrayList<>();
for (int i = 0; i < devices.size(); i++) { for (int i = 0; i < devices.size(); i++) {
Device device = devices.get(i); Device device = devices.get(i);
attributeFutures.add(saveLongAttribute(device.getId(), "temperature", temperatures.get(i), AttributeScope.CLIENT_SCOPE)); attributeFutures.add(saveLongAttribute(device.getId(), "temperature", temperatures.get(i), AttributeScope.CLIENT_SCOPE));
@ -666,7 +666,7 @@ public class EntityServiceTest extends AbstractServiceTest {
List<Long> highConsumptions = new ArrayList<>(); List<Long> highConsumptions = new ArrayList<>();
createTestHierarchy(tenantId, assets, devices, consumptions, highConsumptions, new ArrayList<>(), new ArrayList<>()); createTestHierarchy(tenantId, assets, devices, consumptions, highConsumptions, new ArrayList<>(), new ArrayList<>());
List<ListenableFuture<List<String>>> attributeFutures = new ArrayList<>(); List<ListenableFuture<List<Long>>> attributeFutures = new ArrayList<>();
for (int i = 0; i < assets.size(); i++) { for (int i = 0; i < assets.size(); i++) {
Asset asset = assets.get(i); Asset asset = assets.get(i);
attributeFutures.add(saveLongAttribute(asset.getId(), "consumption", consumptions.get(i), AttributeScope.SERVER_SCOPE)); attributeFutures.add(saveLongAttribute(asset.getId(), "consumption", consumptions.get(i), AttributeScope.SERVER_SCOPE));
@ -1586,7 +1586,7 @@ public class EntityServiceTest extends AbstractServiceTest {
} }
} }
List<ListenableFuture<List<String>>> attributeFutures = new ArrayList<>(); List<ListenableFuture<List<Long>>> attributeFutures = new ArrayList<>();
for (int i = 0; i < devices.size(); i++) { for (int i = 0; i < devices.size(); i++) {
Device device = devices.get(i); Device device = devices.get(i);
for (AttributeScope currentScope : AttributeScope.values()) { for (AttributeScope currentScope : AttributeScope.values()) {
@ -1688,7 +1688,7 @@ public class EntityServiceTest extends AbstractServiceTest {
} }
} }
List<ListenableFuture<List<String>>> attributeFutures = new ArrayList<>(); List<ListenableFuture<List<Long>>> attributeFutures = new ArrayList<>();
for (int i = 0; i < devices.size(); i++) { for (int i = 0; i < devices.size(); i++) {
Device device = devices.get(i); Device device = devices.get(i);
attributeFutures.add(saveLongAttribute(device.getId(), "temperature", temperatures.get(i), AttributeScope.CLIENT_SCOPE)); attributeFutures.add(saveLongAttribute(device.getId(), "temperature", temperatures.get(i), AttributeScope.CLIENT_SCOPE));
@ -1966,7 +1966,7 @@ public class EntityServiceTest extends AbstractServiceTest {
} }
} }
List<ListenableFuture<List<String>>> attributeFutures = new ArrayList<>(); List<ListenableFuture<List<Long>>> attributeFutures = new ArrayList<>();
for (int i = 0; i < devices.size(); i++) { for (int i = 0; i < devices.size(); i++) {
Device device = devices.get(i); Device device = devices.get(i);
attributeFutures.add(saveStringAttribute(device.getId(), "attributeString", attributeStrings.get(i), AttributeScope.CLIENT_SCOPE)); attributeFutures.add(saveStringAttribute(device.getId(), "attributeString", attributeStrings.get(i), AttributeScope.CLIENT_SCOPE));
@ -2428,13 +2428,13 @@ public class EntityServiceTest extends AbstractServiceTest {
return filter; return filter;
} }
private ListenableFuture<List<String>> saveLongAttribute(EntityId entityId, String key, long value, AttributeScope scope) { private ListenableFuture<List<Long>> saveLongAttribute(EntityId entityId, String key, long value, AttributeScope scope) {
KvEntry attrValue = new LongDataEntry(key, value); KvEntry attrValue = new LongDataEntry(key, value);
AttributeKvEntry attr = new BaseAttributeKvEntry(attrValue, 42L); AttributeKvEntry attr = new BaseAttributeKvEntry(attrValue, 42L);
return attributesService.save(SYSTEM_TENANT_ID, entityId, scope, Collections.singletonList(attr)); return attributesService.save(SYSTEM_TENANT_ID, entityId, scope, Collections.singletonList(attr));
} }
private ListenableFuture<List<String>> saveStringAttribute(EntityId entityId, String key, String value, AttributeScope scope) { private ListenableFuture<List<Long>> saveStringAttribute(EntityId entityId, String key, String value, AttributeScope scope) {
KvEntry attrValue = new StringDataEntry(key, value); KvEntry attrValue = new StringDataEntry(key, value);
AttributeKvEntry attr = new BaseAttributeKvEntry(attrValue, 42L); AttributeKvEntry attr = new BaseAttributeKvEntry(attrValue, 42L);
return attributesService.save(SYSTEM_TENANT_ID, entityId, scope, Collections.singletonList(attr)); return attributesService.save(SYSTEM_TENANT_ID, entityId, scope, Collections.singletonList(attr));

6
dao/src/test/java/org/thingsboard/server/dao/service/RelationCacheTest.java

@ -85,8 +85,12 @@ public class RelationCacheTest extends AbstractServiceTest {
@Test @Test
public void testDeleteRelations_EvictsCache() { public void testDeleteRelations_EvictsCache() {
EntityRelation relation = new EntityRelation(ENTITY_ID_FROM, ENTITY_ID_TO, RELATION_TYPE);
when(relationDao.getRelation(SYSTEM_TENANT_ID, ENTITY_ID_FROM, ENTITY_ID_TO, RELATION_TYPE, RelationTypeGroup.COMMON)) when(relationDao.getRelation(SYSTEM_TENANT_ID, ENTITY_ID_FROM, ENTITY_ID_TO, RELATION_TYPE, RelationTypeGroup.COMMON))
.thenReturn(new EntityRelation(ENTITY_ID_FROM, ENTITY_ID_TO, RELATION_TYPE)); .thenReturn(relation);
when(relationDao.deleteRelation(SYSTEM_TENANT_ID, ENTITY_ID_FROM, ENTITY_ID_TO, RELATION_TYPE, RelationTypeGroup.COMMON))
.thenReturn(relation);
relationService.getRelation(SYSTEM_TENANT_ID, ENTITY_ID_FROM, ENTITY_ID_TO, RELATION_TYPE, RelationTypeGroup.COMMON); relationService.getRelation(SYSTEM_TENANT_ID, ENTITY_ID_FROM, ENTITY_ID_TO, RELATION_TYPE, RelationTypeGroup.COMMON);
relationService.getRelation(SYSTEM_TENANT_ID, ENTITY_ID_FROM, ENTITY_ID_TO, RELATION_TYPE, RelationTypeGroup.COMMON); relationService.getRelation(SYSTEM_TENANT_ID, ENTITY_ID_FROM, ENTITY_ID_TO, RELATION_TYPE, RelationTypeGroup.COMMON);

92
dao/src/test/java/org/thingsboard/server/dao/service/RelationServiceTest.java

@ -57,13 +57,13 @@ public class RelationServiceTest extends AbstractServiceTest {
} }
@Test @Test
public void testSaveRelation() throws ExecutionException, InterruptedException { public void testSaveRelation() {
AssetId parentId = new AssetId(Uuids.timeBased()); AssetId parentId = new AssetId(Uuids.timeBased());
AssetId childId = new AssetId(Uuids.timeBased()); AssetId childId = new AssetId(Uuids.timeBased());
EntityRelation relation = new EntityRelation(parentId, childId, EntityRelation.CONTAINS_TYPE); EntityRelation relation = new EntityRelation(parentId, childId, EntityRelation.CONTAINS_TYPE);
Assert.assertTrue(saveRelation(relation)); Assert.assertNotNull(saveRelation(relation));
Assert.assertTrue(relationService.checkRelation(SYSTEM_TENANT_ID, parentId, childId, EntityRelation.CONTAINS_TYPE, RelationTypeGroup.COMMON)); Assert.assertTrue(relationService.checkRelation(SYSTEM_TENANT_ID, parentId, childId, EntityRelation.CONTAINS_TYPE, RelationTypeGroup.COMMON));
@ -204,8 +204,8 @@ public class RelationServiceTest extends AbstractServiceTest {
Assert.assertEquals(0, relations.size()); Assert.assertEquals(0, relations.size());
} }
private Boolean saveRelation(EntityRelation relationA1) { private EntityRelation saveRelation(EntityRelation relation) {
return relationService.saveRelation(SYSTEM_TENANT_ID, relationA1); return relationService.saveRelation(SYSTEM_TENANT_ID, relation);
} }
@Test @Test
@ -265,9 +265,9 @@ public class RelationServiceTest extends AbstractServiceTest {
EntityRelation relationB = new EntityRelation(assetB, assetC, EntityRelation.CONTAINS_TYPE); EntityRelation relationB = new EntityRelation(assetB, assetC, EntityRelation.CONTAINS_TYPE);
EntityRelation relationC = new EntityRelation(assetC, assetA, EntityRelation.CONTAINS_TYPE); EntityRelation relationC = new EntityRelation(assetC, assetA, EntityRelation.CONTAINS_TYPE);
saveRelation(relationA); relationA = saveRelation(relationA);
saveRelation(relationB); relationB = saveRelation(relationB);
saveRelation(relationC); relationC = saveRelation(relationC);
EntityRelationsQuery query = new EntityRelationsQuery(); EntityRelationsQuery query = new EntityRelationsQuery();
query.setParameters(new RelationsSearchParameters(assetA, EntitySearchDirection.FROM, -1, false)); query.setParameters(new RelationsSearchParameters(assetA, EntitySearchDirection.FROM, -1, false));
@ -299,8 +299,8 @@ public class RelationServiceTest extends AbstractServiceTest {
EntityRelation relationBD = new EntityRelation(assetB, deviceD, EntityRelation.CONTAINS_TYPE); EntityRelation relationBD = new EntityRelation(assetB, deviceD, EntityRelation.CONTAINS_TYPE);
saveRelation(relationAB); relationAB = saveRelation(relationAB);
saveRelation(relationBC); relationBC = saveRelation(relationBC);
saveRelation(relationBD); saveRelation(relationBD);
EntityRelationsQuery query = new EntityRelationsQuery(); EntityRelationsQuery query = new EntityRelationsQuery();
@ -329,26 +329,20 @@ public class RelationServiceTest extends AbstractServiceTest {
EntityRelation relationAB = new EntityRelation(root, left, EntityRelation.CONTAINS_TYPE); EntityRelation relationAB = new EntityRelation(root, left, EntityRelation.CONTAINS_TYPE);
EntityRelation relationBC = new EntityRelation(root, right, EntityRelation.CONTAINS_TYPE); EntityRelation relationBC = new EntityRelation(root, right, EntityRelation.CONTAINS_TYPE);
saveRelation(relationAB); expected.add(saveRelation(relationAB));
expected.add(relationAB); expected.add(saveRelation(relationBC));
saveRelation(relationBC);
expected.add(relationBC);
for (int i = 0; i < maxLevel; i++) { for (int i = 0; i < maxLevel; i++) {
var newLeft = new AssetId(Uuids.timeBased()); var newLeft = new AssetId(Uuids.timeBased());
var newRight = new AssetId(Uuids.timeBased()); var newRight = new AssetId(Uuids.timeBased());
EntityRelation relationLeft = new EntityRelation(left, newLeft, EntityRelation.CONTAINS_TYPE); EntityRelation relationLeft = new EntityRelation(left, newLeft, EntityRelation.CONTAINS_TYPE);
EntityRelation relationRight = new EntityRelation(right, newRight, EntityRelation.CONTAINS_TYPE); EntityRelation relationRight = new EntityRelation(right, newRight, EntityRelation.CONTAINS_TYPE);
saveRelation(relationLeft); expected.add(saveRelation(relationLeft));
expected.add(relationLeft); expected.add(saveRelation(relationRight));
saveRelation(relationRight);
expected.add(relationRight);
left = newLeft; left = newLeft;
right = newRight; right = newRight;
} }
EntityRelationsQuery query = new EntityRelationsQuery(); EntityRelationsQuery query = new EntityRelationsQuery();
query.setParameters(new RelationsSearchParameters(root, EntitySearchDirection.FROM, -1, false)); query.setParameters(new RelationsSearchParameters(root, EntitySearchDirection.FROM, -1, false));
query.setFilters(Collections.singletonList(new RelationEntityTypeFilter(EntityRelation.CONTAINS_TYPE, Collections.singletonList(EntityType.ASSET)))); query.setFilters(Collections.singletonList(new RelationEntityTypeFilter(EntityRelation.CONTAINS_TYPE, Collections.singletonList(EntityType.ASSET))));
@ -372,7 +366,7 @@ public class RelationServiceTest extends AbstractServiceTest {
relation.setTo(new AssetId(Uuids.timeBased())); relation.setTo(new AssetId(Uuids.timeBased()));
relation.setType(EntityRelation.CONTAINS_TYPE); relation.setType(EntityRelation.CONTAINS_TYPE);
Assertions.assertThrows(DataValidationException.class, () -> { Assertions.assertThrows(DataValidationException.class, () -> {
Assert.assertTrue(saveRelation(relation)); Assert.assertNotNull(saveRelation(relation));
}); });
} }
@ -382,7 +376,7 @@ public class RelationServiceTest extends AbstractServiceTest {
relation.setFrom(new AssetId(Uuids.timeBased())); relation.setFrom(new AssetId(Uuids.timeBased()));
relation.setType(EntityRelation.CONTAINS_TYPE); relation.setType(EntityRelation.CONTAINS_TYPE);
Assertions.assertThrows(DataValidationException.class, () -> { Assertions.assertThrows(DataValidationException.class, () -> {
Assert.assertTrue(saveRelation(relation)); Assert.assertNotNull(saveRelation(relation));
}); });
} }
@ -392,7 +386,7 @@ public class RelationServiceTest extends AbstractServiceTest {
relation.setFrom(new AssetId(Uuids.timeBased())); relation.setFrom(new AssetId(Uuids.timeBased()));
relation.setTo(new AssetId(Uuids.timeBased())); relation.setTo(new AssetId(Uuids.timeBased()));
Assertions.assertThrows(DataValidationException.class, () -> { Assertions.assertThrows(DataValidationException.class, () -> {
Assert.assertTrue(saveRelation(relation)); Assert.assertNotNull(saveRelation(relation));
}); });
} }
@ -414,10 +408,10 @@ public class RelationServiceTest extends AbstractServiceTest {
EntityRelation relationC = new EntityRelation(assetC, assetD, EntityRelation.CONTAINS_TYPE); EntityRelation relationC = new EntityRelation(assetC, assetD, EntityRelation.CONTAINS_TYPE);
EntityRelation relationD = new EntityRelation(assetC, assetE, EntityRelation.CONTAINS_TYPE); EntityRelation relationD = new EntityRelation(assetC, assetE, EntityRelation.CONTAINS_TYPE);
saveRelation(relationA); relationA = saveRelation(relationA);
saveRelation(relationB); relationB = saveRelation(relationB);
saveRelation(relationC); relationC = saveRelation(relationC);
saveRelation(relationD); relationD = saveRelation(relationD);
EntityRelationsQuery query = new EntityRelationsQuery(); EntityRelationsQuery query = new EntityRelationsQuery();
query.setParameters(new RelationsSearchParameters(assetA, EntitySearchDirection.FROM, -1, true)); query.setParameters(new RelationsSearchParameters(assetA, EntitySearchDirection.FROM, -1, true));
@ -450,9 +444,9 @@ public class RelationServiceTest extends AbstractServiceTest {
EntityRelation relationB = new EntityRelation(assetB, assetC, EntityRelation.CONTAINS_TYPE); EntityRelation relationB = new EntityRelation(assetB, assetC, EntityRelation.CONTAINS_TYPE);
EntityRelation relationC = new EntityRelation(assetC, assetD, EntityRelation.CONTAINS_TYPE); EntityRelation relationC = new EntityRelation(assetC, assetD, EntityRelation.CONTAINS_TYPE);
saveRelation(relationA); relationA = saveRelation(relationA);
saveRelation(relationB); relationB = saveRelation(relationB);
saveRelation(relationC); relationC = saveRelation(relationC);
EntityRelationsQuery query = new EntityRelationsQuery(); EntityRelationsQuery query = new EntityRelationsQuery();
query.setParameters(new RelationsSearchParameters(assetA, EntitySearchDirection.FROM, -1, true)); query.setParameters(new RelationsSearchParameters(assetA, EntitySearchDirection.FROM, -1, true));
@ -494,12 +488,12 @@ public class RelationServiceTest extends AbstractServiceTest {
EntityRelation relationE = new EntityRelation(assetD, assetF, EntityRelation.CONTAINS_TYPE); EntityRelation relationE = new EntityRelation(assetD, assetF, EntityRelation.CONTAINS_TYPE);
EntityRelation relationF = new EntityRelation(assetD, assetG, EntityRelation.CONTAINS_TYPE); EntityRelation relationF = new EntityRelation(assetD, assetG, EntityRelation.CONTAINS_TYPE);
saveRelation(relationA); relationA = saveRelation(relationA);
saveRelation(relationB); relationB = saveRelation(relationB);
saveRelation(relationC); relationC = saveRelation(relationC);
saveRelation(relationD); relationD = saveRelation(relationD);
saveRelation(relationE); relationE = saveRelation(relationE);
saveRelation(relationF); relationF = saveRelation(relationF);
EntityRelationsQuery query = new EntityRelationsQuery(); EntityRelationsQuery query = new EntityRelationsQuery();
query.setParameters(new RelationsSearchParameters(assetA, EntitySearchDirection.FROM, 2, true)); query.setParameters(new RelationsSearchParameters(assetA, EntitySearchDirection.FROM, 2, true));
@ -547,12 +541,12 @@ public class RelationServiceTest extends AbstractServiceTest {
EntityRelation relationE = new EntityRelation(assetD, assetF, EntityRelation.CONTAINS_TYPE); EntityRelation relationE = new EntityRelation(assetD, assetF, EntityRelation.CONTAINS_TYPE);
EntityRelation relationF = new EntityRelation(assetD, assetG, EntityRelation.CONTAINS_TYPE); EntityRelation relationF = new EntityRelation(assetD, assetG, EntityRelation.CONTAINS_TYPE);
saveRelation(relationA); relationA = saveRelation(relationA);
saveRelation(relationB); relationB = saveRelation(relationB);
saveRelation(relationC); relationC = saveRelation(relationC);
saveRelation(relationD); relationD = saveRelation(relationD);
saveRelation(relationE); relationE = saveRelation(relationE);
saveRelation(relationF); relationF = saveRelation(relationF);
EntityRelationsQuery query = new EntityRelationsQuery(); EntityRelationsQuery query = new EntityRelationsQuery();
query.setParameters(new RelationsSearchParameters(assetA, EntitySearchDirection.FROM, 2, false)); query.setParameters(new RelationsSearchParameters(assetA, EntitySearchDirection.FROM, 2, false));
@ -600,12 +594,12 @@ public class RelationServiceTest extends AbstractServiceTest {
EntityRelation relationE = new EntityRelation(assetD, assetF, EntityRelation.CONTAINS_TYPE); EntityRelation relationE = new EntityRelation(assetD, assetF, EntityRelation.CONTAINS_TYPE);
EntityRelation relationF = new EntityRelation(assetD, assetG, EntityRelation.CONTAINS_TYPE); EntityRelation relationF = new EntityRelation(assetD, assetG, EntityRelation.CONTAINS_TYPE);
saveRelation(relationA); relationA = saveRelation(relationA);
saveRelation(relationB); relationB = saveRelation(relationB);
saveRelation(relationC); relationC = saveRelation(relationC);
saveRelation(relationD); relationD = saveRelation(relationD);
saveRelation(relationE); relationE = saveRelation(relationE);
saveRelation(relationF); relationF = saveRelation(relationF);
EntityRelationsQuery query = new EntityRelationsQuery(); EntityRelationsQuery query = new EntityRelationsQuery();
query.setParameters(new RelationsSearchParameters(assetA, EntitySearchDirection.FROM, -1, false)); query.setParameters(new RelationsSearchParameters(assetA, EntitySearchDirection.FROM, -1, false));
@ -670,8 +664,8 @@ public class RelationServiceTest extends AbstractServiceTest {
EntityRelation firstRelation = new EntityRelation(rootAsset, firstAsset, EntityRelation.CONTAINS_TYPE); EntityRelation firstRelation = new EntityRelation(rootAsset, firstAsset, EntityRelation.CONTAINS_TYPE);
EntityRelation secondRelation = new EntityRelation(rootAsset, secondAsset, EntityRelation.CONTAINS_TYPE); EntityRelation secondRelation = new EntityRelation(rootAsset, secondAsset, EntityRelation.CONTAINS_TYPE);
saveRelation(firstRelation); firstRelation = saveRelation(firstRelation);
saveRelation(secondRelation); secondRelation = saveRelation(secondRelation);
if (!lastLvlOnly || lvl == 1) { if (!lastLvlOnly || lvl == 1) {
entityRelations.add(firstRelation); entityRelations.add(firstRelation);

44
dao/src/test/java/org/thingsboard/server/dao/service/attributes/BaseAttributesServiceTest.java

@ -26,7 +26,6 @@ import org.junit.Assert;
import org.junit.Before; import org.junit.Before;
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.server.cache.TbTransactionalCache;
import org.thingsboard.server.common.data.AttributeScope; import org.thingsboard.server.common.data.AttributeScope;
import org.thingsboard.server.common.data.id.DeviceId; import org.thingsboard.server.common.data.id.DeviceId;
import org.thingsboard.server.common.data.id.TenantId; import org.thingsboard.server.common.data.id.TenantId;
@ -34,7 +33,6 @@ import org.thingsboard.server.common.data.kv.AttributeKvEntry;
import org.thingsboard.server.common.data.kv.BaseAttributeKvEntry; import org.thingsboard.server.common.data.kv.BaseAttributeKvEntry;
import org.thingsboard.server.common.data.kv.KvEntry; import org.thingsboard.server.common.data.kv.KvEntry;
import org.thingsboard.server.common.data.kv.StringDataEntry; import org.thingsboard.server.common.data.kv.StringDataEntry;
import org.thingsboard.server.dao.attributes.AttributeCacheKey;
import org.thingsboard.server.dao.attributes.AttributesService; import org.thingsboard.server.dao.attributes.AttributesService;
import org.thingsboard.server.dao.service.AbstractServiceTest; import org.thingsboard.server.dao.service.AbstractServiceTest;
@ -59,9 +57,6 @@ public abstract class BaseAttributesServiceTest extends AbstractServiceTest {
private static final String OLD_VALUE = "OLD VALUE"; private static final String OLD_VALUE = "OLD VALUE";
private static final String NEW_VALUE = "NEW VALUE"; private static final String NEW_VALUE = "NEW VALUE";
@Autowired
private TbTransactionalCache<AttributeCacheKey, AttributeKvEntry> cache;
@Autowired @Autowired
private AttributesService attributesService; private AttributesService attributesService;
@ -77,7 +72,7 @@ public abstract class BaseAttributesServiceTest extends AbstractServiceTest {
attributesService.save(SYSTEM_TENANT_ID, deviceId, AttributeScope.CLIENT_SCOPE, Collections.singletonList(attr)).get(); attributesService.save(SYSTEM_TENANT_ID, deviceId, AttributeScope.CLIENT_SCOPE, Collections.singletonList(attr)).get();
Optional<AttributeKvEntry> saved = attributesService.find(SYSTEM_TENANT_ID, deviceId, AttributeScope.CLIENT_SCOPE, attr.getKey()).get(); Optional<AttributeKvEntry> saved = attributesService.find(SYSTEM_TENANT_ID, deviceId, AttributeScope.CLIENT_SCOPE, attr.getKey()).get();
Assert.assertTrue(saved.isPresent()); Assert.assertTrue(saved.isPresent());
Assert.assertEquals(attr, saved.get()); equalsIgnoreVersion(attr, saved.get());
} }
@Test @Test
@ -90,14 +85,15 @@ public abstract class BaseAttributesServiceTest extends AbstractServiceTest {
Optional<AttributeKvEntry> saved = attributesService.find(SYSTEM_TENANT_ID, deviceId, AttributeScope.CLIENT_SCOPE, attrOld.getKey()).get(); Optional<AttributeKvEntry> saved = attributesService.find(SYSTEM_TENANT_ID, deviceId, AttributeScope.CLIENT_SCOPE, attrOld.getKey()).get();
Assert.assertTrue(saved.isPresent()); Assert.assertTrue(saved.isPresent());
Assert.assertEquals(attrOld, saved.get()); equalsIgnoreVersion(attrOld, saved.get());
KvEntry attrNewValue = new StringDataEntry("attribute1", "value2"); KvEntry attrNewValue = new StringDataEntry("attribute1", "value2");
AttributeKvEntry attrNew = new BaseAttributeKvEntry(attrNewValue, 73L); AttributeKvEntry attrNew = new BaseAttributeKvEntry(attrNewValue, 73L);
attributesService.save(SYSTEM_TENANT_ID, deviceId, AttributeScope.CLIENT_SCOPE, Collections.singletonList(attrNew)).get(); attributesService.save(SYSTEM_TENANT_ID, deviceId, AttributeScope.CLIENT_SCOPE, Collections.singletonList(attrNew)).get();
saved = attributesService.find(SYSTEM_TENANT_ID, deviceId, AttributeScope.CLIENT_SCOPE, attrOld.getKey()).get(); saved = attributesService.find(SYSTEM_TENANT_ID, deviceId, AttributeScope.CLIENT_SCOPE, attrOld.getKey()).get();
Assert.assertEquals(attrNew, saved.get()); Assert.assertTrue(saved.isPresent());
equalsIgnoreVersion(attrNew, saved.get());
} }
@Test @Test
@ -120,8 +116,8 @@ public abstract class BaseAttributesServiceTest extends AbstractServiceTest {
Assert.assertNotNull(saved); Assert.assertNotNull(saved);
Assert.assertEquals(2, saved.size()); Assert.assertEquals(2, saved.size());
Assert.assertEquals(attrANew, saved.get(0)); equalsIgnoreVersion(attrANew, saved.get(0));
Assert.assertEquals(attrBNew, saved.get(1)); equalsIgnoreVersion(attrBNew, saved.get(1));
} }
@Test @Test
@ -132,24 +128,6 @@ public abstract class BaseAttributesServiceTest extends AbstractServiceTest {
Assert.assertTrue(result.isEmpty()); Assert.assertTrue(result.isEmpty());
} }
@Test
public void testConcurrentTransaction() throws Exception {
var tenantId = new TenantId(UUID.randomUUID());
var deviceId = new DeviceId(UUID.randomUUID());
var scope = AttributeScope.SERVER_SCOPE;
var key = "TEST";
var attrKey = new AttributeCacheKey(scope, deviceId, "TEST");
var oldValue = new BaseAttributeKvEntry(System.currentTimeMillis(), new StringDataEntry(key, OLD_VALUE));
var newValue = new BaseAttributeKvEntry(System.currentTimeMillis(), new StringDataEntry(key, NEW_VALUE));
var trx = cache.newTransactionForKey(attrKey);
cache.putIfAbsent(attrKey, newValue);
trx.putIfAbsent(attrKey, oldValue);
Assert.assertFalse(trx.commit());
Assert.assertEquals(NEW_VALUE, getAttributeValue(tenantId, deviceId, scope, key));
}
@Test @Test
public void testConcurrentFetchAndUpdate() throws Exception { public void testConcurrentFetchAndUpdate() throws Exception {
var tenantId = new TenantId(UUID.randomUUID()); var tenantId = new TenantId(UUID.randomUUID());
@ -274,6 +252,11 @@ public abstract class BaseAttributesServiceTest extends AbstractServiceTest {
})); }));
futures.add(pool.submit(() -> saveAttribute(tenantId, deviceId, scope, key, NEW_VALUE))); futures.add(pool.submit(() -> saveAttribute(tenantId, deviceId, scope, key, NEW_VALUE)));
Futures.allAsList(futures).get(10, TimeUnit.SECONDS); Futures.allAsList(futures).get(10, TimeUnit.SECONDS);
String attributeValue = getAttributeValue(tenantId, deviceId, scope, key);
if (!NEW_VALUE.equals(attributeValue)) {
System.out.println();
}
Assert.assertEquals(NEW_VALUE, getAttributeValue(tenantId, deviceId, scope, key)); Assert.assertEquals(NEW_VALUE, getAttributeValue(tenantId, deviceId, scope, key));
} }
@ -330,5 +313,10 @@ public abstract class BaseAttributesServiceTest extends AbstractServiceTest {
} }
} }
private void equalsIgnoreVersion(AttributeKvEntry expected, AttributeKvEntry actual) {
Assert.assertEquals(expected.getKey(), actual.getKey());
Assert.assertEquals(expected.getValue(), actual.getValue());
Assert.assertEquals(expected.getLastUpdateTs(), actual.getLastUpdateTs());
}
} }

114
dao/src/test/java/org/thingsboard/server/dao/service/attributes/sql/AttributeCacheServiceSqlTest.java

@ -0,0 +1,114 @@
/**
* Copyright © 2016-2024 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.service.attributes.sql;
import org.junit.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.thingsboard.server.cache.TbCacheValueWrapper;
import org.thingsboard.server.cache.VersionedTbCache;
import org.thingsboard.server.common.data.AttributeScope;
import org.thingsboard.server.common.data.id.DeviceId;
import org.thingsboard.server.common.data.kv.AttributeKvEntry;
import org.thingsboard.server.common.data.kv.BaseAttributeKvEntry;
import org.thingsboard.server.common.data.kv.StringDataEntry;
import org.thingsboard.server.dao.attributes.AttributeCacheKey;
import org.thingsboard.server.dao.service.AbstractServiceTest;
import org.thingsboard.server.dao.service.DaoSqlTest;
import java.util.UUID;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertNotNull;
import static org.junit.Assert.assertNull;
@DaoSqlTest
public class AttributeCacheServiceSqlTest extends AbstractServiceTest {
private static final String TEST_KEY = "key";
private static final String TEST_VALUE = "value";
private static final DeviceId DEVICE_ID = new DeviceId(UUID.randomUUID());
@Autowired
VersionedTbCache<AttributeCacheKey, AttributeKvEntry> cache;
@Test
public void testPutAndGet() {
AttributeCacheKey testKey = new AttributeCacheKey(AttributeScope.CLIENT_SCOPE, DEVICE_ID, TEST_KEY);
AttributeKvEntry testValue = new BaseAttributeKvEntry(new StringDataEntry(TEST_KEY, TEST_VALUE), 1, 1L);
cache.put(testKey, testValue);
TbCacheValueWrapper<AttributeKvEntry> wrapper = cache.get(testKey);
assertNotNull(wrapper);
assertEquals(testValue, wrapper.get());
AttributeKvEntry testValue2 = new BaseAttributeKvEntry(new StringDataEntry(TEST_KEY, TEST_VALUE), 1, 2L);
cache.put(testKey, testValue2);
wrapper = cache.get(testKey);
assertNotNull(wrapper);
assertEquals(testValue2, wrapper.get());
AttributeKvEntry testValue3 = new BaseAttributeKvEntry(new StringDataEntry(TEST_KEY, TEST_VALUE), 1, 0L);
cache.put(testKey, testValue3);
wrapper = cache.get(testKey);
assertNotNull(wrapper);
assertEquals(testValue2, wrapper.get());
cache.evict(testKey);
}
@Test
public void testEvictWithVersion() {
AttributeCacheKey testKey = new AttributeCacheKey(AttributeScope.CLIENT_SCOPE, DEVICE_ID, TEST_KEY);
AttributeKvEntry testValue = new BaseAttributeKvEntry(new StringDataEntry(TEST_KEY, TEST_VALUE), 1, 1L);
cache.put(testKey, testValue);
TbCacheValueWrapper<AttributeKvEntry> wrapper = cache.get(testKey);
assertNotNull(wrapper);
assertEquals(testValue, wrapper.get());
cache.evict(testKey, 2L);
wrapper = cache.get(testKey);
assertNotNull(wrapper);
assertNull(wrapper.get());
cache.evict(testKey);
}
@Test
public void testEvict() {
AttributeCacheKey testKey = new AttributeCacheKey(AttributeScope.CLIENT_SCOPE, DEVICE_ID, TEST_KEY);
AttributeKvEntry testValue = new BaseAttributeKvEntry(new StringDataEntry(TEST_KEY, TEST_VALUE), 1, 1L);
cache.put(testKey, testValue);
TbCacheValueWrapper<AttributeKvEntry> wrapper = cache.get(testKey);
assertNotNull(wrapper);
assertEquals(testValue, wrapper.get());
cache.evict(testKey);
wrapper = cache.get(testKey);
assertNull(wrapper);
}
}

119
dao/src/test/java/org/thingsboard/server/dao/service/timeseries/BaseTimeseriesServiceTest.java

@ -17,11 +17,13 @@ package org.thingsboard.server.dao.service.timeseries;
import com.datastax.oss.driver.api.core.uuid.Uuids; import com.datastax.oss.driver.api.core.uuid.Uuids;
import lombok.extern.slf4j.Slf4j; import lombok.extern.slf4j.Slf4j;
import org.assertj.core.data.Offset;
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.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.thingsboard.server.common.data.EntityView; import org.thingsboard.server.common.data.EntityView;
import org.thingsboard.server.common.data.Tenant; import org.thingsboard.server.common.data.Tenant;
import org.thingsboard.server.common.data.id.DeviceId; import org.thingsboard.server.common.data.id.DeviceId;
@ -32,6 +34,7 @@ import org.thingsboard.server.common.data.kv.BaseDeleteTsKvQuery;
import org.thingsboard.server.common.data.kv.BaseReadTsKvQuery; import org.thingsboard.server.common.data.kv.BaseReadTsKvQuery;
import org.thingsboard.server.common.data.kv.BasicTsKvEntry; import org.thingsboard.server.common.data.kv.BasicTsKvEntry;
import org.thingsboard.server.common.data.kv.BooleanDataEntry; import org.thingsboard.server.common.data.kv.BooleanDataEntry;
import org.thingsboard.server.common.data.kv.DataType;
import org.thingsboard.server.common.data.kv.DoubleDataEntry; import org.thingsboard.server.common.data.kv.DoubleDataEntry;
import org.thingsboard.server.common.data.kv.JsonDataEntry; import org.thingsboard.server.common.data.kv.JsonDataEntry;
import org.thingsboard.server.common.data.kv.KvEntry; import org.thingsboard.server.common.data.kv.KvEntry;
@ -50,15 +53,14 @@ import java.util.Arrays;
import java.util.Collections; import java.util.Collections;
import java.util.Comparator; import java.util.Comparator;
import java.util.List; import java.util.List;
import java.util.Optional;
import java.util.concurrent.ExecutionException; import java.util.concurrent.ExecutionException;
import java.util.concurrent.TimeUnit; import java.util.concurrent.TimeUnit;
import java.util.concurrent.TimeoutException; import java.util.concurrent.TimeoutException;
import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThat;
import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertNotNull; import static org.junit.Assert.assertNotNull;
import static org.junit.Assert.assertTrue;
/** /**
* @author Andrew Shvayka * @author Andrew Shvayka
@ -73,6 +75,9 @@ public abstract class BaseTimeseriesServiceTest extends AbstractServiceTest {
@Autowired @Autowired
EntityViewService entityViewService; EntityViewService entityViewService;
@Value("${database.ts.type}")
String databaseTsLatestType;
protected static final int MAX_TIMEOUT = 30; protected static final int MAX_TIMEOUT = 30;
private static final String STRING_KEY = "stringKey"; private static final String STRING_KEY = "stringKey";
@ -89,6 +94,7 @@ public abstract class BaseTimeseriesServiceTest extends AbstractServiceTest {
KvEntry booleanKvEntry = new BooleanDataEntry(BOOLEAN_KEY, Boolean.TRUE); KvEntry booleanKvEntry = new BooleanDataEntry(BOOLEAN_KEY, Boolean.TRUE);
protected TenantId tenantId; protected TenantId tenantId;
DeviceId deviceId = new DeviceId(Uuids.timeBased());
@Before @Before
public void before() { public void before() {
@ -106,8 +112,6 @@ public abstract class BaseTimeseriesServiceTest extends AbstractServiceTest {
@Test @Test
public void testFindAllLatest() throws Exception { public void testFindAllLatest() throws Exception {
DeviceId deviceId = new DeviceId(Uuids.timeBased());
saveEntries(deviceId, TS - 2); saveEntries(deviceId, TS - 2);
saveEntries(deviceId, TS - 1); saveEntries(deviceId, TS - 1);
saveEntries(deviceId, TS); saveEntries(deviceId, TS);
@ -133,7 +137,12 @@ public abstract class BaseTimeseriesServiceTest extends AbstractServiceTest {
toTsEntry(TS, booleanKvEntry)); toTsEntry(TS, booleanKvEntry));
Collections.sort(expected, Comparator.comparing(KvEntry::getKey)); Collections.sort(expected, Comparator.comparing(KvEntry::getKey));
assertEquals(expected, tsList); for (int i = 0; i < expected.size(); i++) {
var expectedEntry = expected.get(i);
var actualEntry = tsList.get(i);
equalsIgnoreVersion(expectedEntry, actualEntry);
}
} }
private EntityView saveAndCreateEntityView(DeviceId deviceId, List<String> timeseries) { private EntityView saveAndCreateEntityView(DeviceId deviceId, List<String> timeseries) {
@ -150,34 +159,96 @@ public abstract class BaseTimeseriesServiceTest extends AbstractServiceTest {
@Test @Test
public void testFindLatest() throws Exception { public void testFindLatest() throws Exception {
DeviceId deviceId = new DeviceId(Uuids.timeBased());
saveEntries(deviceId, TS - 2); saveEntries(deviceId, TS - 2);
saveEntries(deviceId, TS - 1); saveEntries(deviceId, TS - 1);
saveEntries(deviceId, TS); saveEntries(deviceId, TS);
List<TsKvEntry> entries = tsService.findLatest(tenantId, deviceId, Collections.singleton(STRING_KEY)).get(MAX_TIMEOUT, TimeUnit.SECONDS); List<TsKvEntry> entries = tsService.findLatest(tenantId, deviceId, Collections.singleton(STRING_KEY)).get(MAX_TIMEOUT, TimeUnit.SECONDS);
Assert.assertEquals(1, entries.size()); Assert.assertEquals(1, entries.size());
Assert.assertEquals(toTsEntry(TS, stringKvEntry), entries.get(0)); equalsIgnoreVersion(toTsEntry(TS, stringKvEntry), entries.get(0));
} }
@Test @Test
public void testFindLatestWithoutLatestUpdate() throws Exception { public void testFindLatestOpt_givenSaveWithHistoricalNonOrderedTS() throws Exception {
DeviceId deviceId = new DeviceId(Uuids.timeBased()); if (databaseTsLatestType.equals("cassandra")) {
return;
}
save(tenantId, deviceId, toTsEntry(TS - 1, stringKvEntry));
save(tenantId, deviceId, toTsEntry(TS, stringKvEntry));
save(tenantId, deviceId, toTsEntry(TS - 10, stringKvEntry));
save(tenantId, deviceId, toTsEntry(TS - 11, stringKvEntry));
Optional<TsKvEntry> entryOpt = tsService.findLatest(tenantId, deviceId, STRING_KEY).get(MAX_TIMEOUT, TimeUnit.SECONDS);
assertThat(entryOpt).isNotNull().isPresent();
equalsIgnoreVersion(toTsEntry(TS, stringKvEntry), entryOpt.get());
}
@Test
public void testFindLatestOpt_givenSaveWithSameTSOverwriteValue() throws Exception {
save(tenantId, deviceId, toTsEntry(TS, new StringDataEntry(STRING_KEY, "old")));
save(tenantId, deviceId, toTsEntry(TS, new StringDataEntry(STRING_KEY, "new")));
Optional<TsKvEntry> entryOpt = tsService.findLatest(tenantId, deviceId, STRING_KEY).get(MAX_TIMEOUT, TimeUnit.SECONDS);
assertThat(entryOpt).isNotNull().isPresent();
equalsIgnoreVersion(toTsEntry(TS, new StringDataEntry(STRING_KEY, "new")), entryOpt.get());
}
public void testFindLatestOpt_givenSaveWithSameTSOverwriteTypeAndValue() throws Exception {
save(tenantId, deviceId, toTsEntry(TS, new JsonDataEntry("temp", "{\"hello\":\"world\"}")));
save(tenantId, deviceId, toTsEntry(TS, new BooleanDataEntry("temp", true)));
save(tenantId, deviceId, toTsEntry(TS, new LongDataEntry("temp", 100L)));
save(tenantId, deviceId, toTsEntry(TS, new DoubleDataEntry("temp", Math.PI)));
save(tenantId, deviceId, toTsEntry(TS, new StringDataEntry("temp", "NOOP")));
Optional<TsKvEntry> entryOpt = tsService.findLatest(tenantId, deviceId, STRING_KEY).get(MAX_TIMEOUT, TimeUnit.SECONDS);
assertThat(entryOpt).isNotNull().isPresent();
Assert.assertEquals(toTsEntry(TS, new StringDataEntry("temp", "NOOP")), entryOpt.orElse(null));
}
@Test
public void testFindLatestOpt() throws Exception {
saveEntries(deviceId, TS - 2);
saveEntries(deviceId, TS - 1);
saveEntries(deviceId, TS);
Optional<TsKvEntry> entryOpt = tsService.findLatest(tenantId, deviceId, STRING_KEY).get(MAX_TIMEOUT, TimeUnit.SECONDS);
assertThat(entryOpt).isNotNull().isPresent();
equalsIgnoreVersion(toTsEntry(TS, stringKvEntry), entryOpt.get());
}
@Test
public void testFindLatest_NotFound() throws Exception {
List<TsKvEntry> entries = tsService.findLatest(tenantId, deviceId, Collections.singleton(STRING_KEY)).get(MAX_TIMEOUT, TimeUnit.SECONDS);
assertThat(entries).hasSize(1);
TsKvEntry tsKvEntry = entries.get(0);
assertThat(tsKvEntry).isNotNull();
// null ts latest representation
assertThat(tsKvEntry.getKey()).isEqualTo(STRING_KEY);
assertThat(tsKvEntry.getDataType()).isEqualTo(DataType.STRING);
assertThat(tsKvEntry.getValue()).isNull();
assertThat(tsKvEntry.getTs()).isCloseTo(System.currentTimeMillis(), Offset.offset(TimeUnit.MINUTES.toMillis(1)));
}
@Test
public void testFindLatestOpt_NotFound() throws Exception {
Optional<TsKvEntry> entryOpt = tsService.findLatest(tenantId, deviceId, STRING_KEY).get(MAX_TIMEOUT, TimeUnit.SECONDS);
assertThat(entryOpt).isNotNull().isNotPresent();
}
@Test
public void testFindLatestWithoutLatestUpdate() throws Exception {
saveEntries(deviceId, TS - 2); saveEntries(deviceId, TS - 2);
saveEntries(deviceId, TS - 1); saveEntries(deviceId, TS - 1);
saveEntriesWithoutLatest(deviceId, TS); saveEntriesWithoutLatest(deviceId, TS);
List<TsKvEntry> entries = tsService.findLatest(tenantId, deviceId, Collections.singleton(STRING_KEY)).get(MAX_TIMEOUT, TimeUnit.SECONDS); List<TsKvEntry> entries = tsService.findLatest(tenantId, deviceId, Collections.singleton(STRING_KEY)).get(MAX_TIMEOUT, TimeUnit.SECONDS);
Assert.assertEquals(1, entries.size()); Assert.assertEquals(1, entries.size());
Assert.assertEquals(toTsEntry(TS - 1, stringKvEntry), entries.get(0)); equalsIgnoreVersion(toTsEntry(TS - 1, stringKvEntry), entries.get(0));
} }
@Test @Test
public void testFindByQueryAscOrder() throws Exception { public void testFindByQueryAscOrder() throws Exception {
DeviceId deviceId = new DeviceId(Uuids.timeBased());
saveEntries(deviceId, TS - 3); saveEntries(deviceId, TS - 3);
saveEntries(deviceId, TS - 2); saveEntries(deviceId, TS - 2);
saveEntries(deviceId, TS - 1); saveEntries(deviceId, TS - 1);
@ -202,7 +273,6 @@ public abstract class BaseTimeseriesServiceTest extends AbstractServiceTest {
@Test @Test
public void testFindByQuery_whenPeriodEqualsOneMilisecondPeriod() throws Exception { public void testFindByQuery_whenPeriodEqualsOneMilisecondPeriod() throws Exception {
DeviceId deviceId = new DeviceId(Uuids.timeBased());
saveEntries(deviceId, TS - 1L); saveEntries(deviceId, TS - 1L);
saveEntries(deviceId, TS); saveEntries(deviceId, TS);
saveEntries(deviceId, TS + 1L); saveEntries(deviceId, TS + 1L);
@ -222,7 +292,6 @@ public abstract class BaseTimeseriesServiceTest extends AbstractServiceTest {
@Test @Test
public void testFindByQuery_whenPeriodEqualsInterval() throws Exception { public void testFindByQuery_whenPeriodEqualsInterval() throws Exception {
DeviceId deviceId = new DeviceId(Uuids.timeBased());
saveEntries(deviceId, TS - 1L); saveEntries(deviceId, TS - 1L);
for (long i = TS; i <= TS + 100L; i += 10L) { for (long i = TS; i <= TS + 100L; i += 10L) {
saveEntries(deviceId, i); saveEntries(deviceId, i);
@ -244,7 +313,6 @@ public abstract class BaseTimeseriesServiceTest extends AbstractServiceTest {
@Test @Test
public void testFindByQuery_whenPeriodHaveTwoIntervalWithEqualsLength() throws Exception { public void testFindByQuery_whenPeriodHaveTwoIntervalWithEqualsLength() throws Exception {
DeviceId deviceId = new DeviceId(Uuids.timeBased());
saveEntries(deviceId, TS - 1L); saveEntries(deviceId, TS - 1L);
for (long i = TS; i <= TS + 100000L; i += 10000L) { for (long i = TS; i <= TS + 100000L; i += 10000L) {
saveEntries(deviceId, i); saveEntries(deviceId, i);
@ -268,7 +336,6 @@ public abstract class BaseTimeseriesServiceTest extends AbstractServiceTest {
@Test @Test
public void testFindByQuery_whenPeriodHaveTwoInterval_whereSecondShorterThanFirst() throws Exception { public void testFindByQuery_whenPeriodHaveTwoInterval_whereSecondShorterThanFirst() throws Exception {
DeviceId deviceId = new DeviceId(Uuids.timeBased());
saveEntries(deviceId, TS - 1L); saveEntries(deviceId, TS - 1L);
for (long i = TS; i <= TS + 80000L; i += 10000L) { for (long i = TS; i <= TS + 80000L; i += 10000L) {
saveEntries(deviceId, i); saveEntries(deviceId, i);
@ -292,7 +359,6 @@ public abstract class BaseTimeseriesServiceTest extends AbstractServiceTest {
@Test @Test
public void testFindByQuery_whenPeriodHaveTwoIntervalWithEqualsLength_whereNotAllEntriesInRange() throws Exception { public void testFindByQuery_whenPeriodHaveTwoIntervalWithEqualsLength_whereNotAllEntriesInRange() throws Exception {
DeviceId deviceId = new DeviceId(Uuids.timeBased());
for (long i = TS - 1L; i <= TS + 100000L + 1L; i += 10000) { for (long i = TS - 1L; i <= TS + 100000L + 1L; i += 10000) {
saveEntries(deviceId, i); saveEntries(deviceId, i);
} }
@ -314,7 +380,6 @@ public abstract class BaseTimeseriesServiceTest extends AbstractServiceTest {
@Test @Test
public void testFindByQuery_whenPeriodHaveTwoInterval_whereSecondShorterThanFirst_andNotAllEntriesInRange() throws Exception { public void testFindByQuery_whenPeriodHaveTwoInterval_whereSecondShorterThanFirst_andNotAllEntriesInRange() throws Exception {
DeviceId deviceId = new DeviceId(Uuids.timeBased());
for (long i = TS - 1L; i <= TS + 100000L + 1L; i += 10000L) { for (long i = TS - 1L; i <= TS + 100000L + 1L; i += 10000L) {
saveEntries(deviceId, i); saveEntries(deviceId, i);
} }
@ -336,8 +401,6 @@ public abstract class BaseTimeseriesServiceTest extends AbstractServiceTest {
@Test @Test
public void testFindByQueryDescOrder() throws Exception { public void testFindByQueryDescOrder() throws Exception {
DeviceId deviceId = new DeviceId(Uuids.timeBased());
saveEntries(deviceId, TS - 3); saveEntries(deviceId, TS - 3);
saveEntries(deviceId, TS - 2); saveEntries(deviceId, TS - 2);
saveEntries(deviceId, TS - 1); saveEntries(deviceId, TS - 1);
@ -362,7 +425,6 @@ public abstract class BaseTimeseriesServiceTest extends AbstractServiceTest {
@Test @Test
public void testFindAllByQueries_verifyQueryId() throws Exception { public void testFindAllByQueries_verifyQueryId() throws Exception {
DeviceId deviceId = new DeviceId(Uuids.timeBased());
saveEntries(deviceId, TS); saveEntries(deviceId, TS);
saveEntries(deviceId, TS - 2); saveEntries(deviceId, TS - 2);
saveEntries(deviceId, TS - 10); saveEntries(deviceId, TS - 10);
@ -373,7 +435,6 @@ public abstract class BaseTimeseriesServiceTest extends AbstractServiceTest {
@Test @Test
public void testFindAllByQueries_verifyQueryId_forEntityView() throws Exception { public void testFindAllByQueries_verifyQueryId_forEntityView() throws Exception {
DeviceId deviceId = new DeviceId(Uuids.timeBased());
saveEntries(deviceId, TS); saveEntries(deviceId, TS);
saveEntries(deviceId, TS - 2); saveEntries(deviceId, TS - 2);
saveEntries(deviceId, TS - 12); saveEntries(deviceId, TS - 12);
@ -392,8 +453,6 @@ public abstract class BaseTimeseriesServiceTest extends AbstractServiceTest {
@Test @Test
public void testDeleteDeviceTsDataWithOverwritingLatest() throws Exception { public void testDeleteDeviceTsDataWithOverwritingLatest() throws Exception {
DeviceId deviceId = new DeviceId(Uuids.timeBased());
saveEntries(deviceId, 10000); saveEntries(deviceId, 10000);
saveEntries(deviceId, 20000); saveEntries(deviceId, 20000);
saveEntries(deviceId, 30000); saveEntries(deviceId, 30000);
@ -412,7 +471,6 @@ public abstract class BaseTimeseriesServiceTest extends AbstractServiceTest {
@Test @Test
public void testFindDeviceTsData() throws Exception { public void testFindDeviceTsData() throws Exception {
DeviceId deviceId = new DeviceId(Uuids.timeBased());
List<TsKvEntry> entries = new ArrayList<>(); List<TsKvEntry> entries = new ArrayList<>();
entries.add(save(deviceId, 5000, 100)); entries.add(save(deviceId, 5000, 100));
@ -563,7 +621,6 @@ public abstract class BaseTimeseriesServiceTest extends AbstractServiceTest {
@Test @Test
public void testFindDeviceLongAndDoubleTsData() throws Exception { public void testFindDeviceLongAndDoubleTsData() throws Exception {
DeviceId deviceId = new DeviceId(Uuids.timeBased());
List<TsKvEntry> entries = new ArrayList<>(); List<TsKvEntry> entries = new ArrayList<>();
entries.add(save(deviceId, 5000, 100)); entries.add(save(deviceId, 5000, 100));
@ -654,8 +711,6 @@ public abstract class BaseTimeseriesServiceTest extends AbstractServiceTest {
@Test @Test
public void testSaveTs_RemoveTs_AndSaveTsAgain() throws Exception { public void testSaveTs_RemoveTs_AndSaveTsAgain() throws Exception {
DeviceId deviceId = new DeviceId(Uuids.timeBased());
save(deviceId, 2000000L, 95); save(deviceId, 2000000L, 95);
save(deviceId, 4000000L, 100); save(deviceId, 4000000L, 100);
save(deviceId, 6000000L, 105); save(deviceId, 6000000L, 105);
@ -686,7 +741,6 @@ public abstract class BaseTimeseriesServiceTest extends AbstractServiceTest {
BasicTsKvEntry jsonEntry = new BasicTsKvEntry(TimeUnit.MINUTES.toMillis(5), new JsonDataEntry("test", "{\"test\":\"testValue\"}")); BasicTsKvEntry jsonEntry = new BasicTsKvEntry(TimeUnit.MINUTES.toMillis(5), new JsonDataEntry("test", "{\"test\":\"testValue\"}"));
List<TsKvEntry> timeseries = List.of(booleanEntry, stringEntry, longEntry, doubleEntry, jsonEntry); List<TsKvEntry> timeseries = List.of(booleanEntry, stringEntry, longEntry, doubleEntry, jsonEntry);
DeviceId deviceId = new DeviceId(Uuids.timeBased());
for (TsKvEntry tsKvEntry : timeseries) { for (TsKvEntry tsKvEntry : timeseries) {
save(tenantId, deviceId, tsKvEntry); save(tenantId, deviceId, tsKvEntry);
} }
@ -745,5 +799,10 @@ public abstract class BaseTimeseriesServiceTest extends AbstractServiceTest {
return new BasicTsKvEntry(ts, entry); return new BasicTsKvEntry(ts, entry);
} }
private static void equalsIgnoreVersion(TsKvEntry expected, TsKvEntry actual) {
assertEquals(expected.getKey(), actual.getKey());
assertEquals(expected.getValue(), actual.getValue());
assertEquals(expected.getTs(), actual.getTs());
}
} }

153
dao/src/test/java/org/thingsboard/server/dao/service/timeseries/sql/LatestTimeseriesPerformanceTest.java

@ -0,0 +1,153 @@
/**
* Copyright © 2016-2024 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.service.timeseries.sql;
import com.google.common.util.concurrent.Futures;
import com.google.common.util.concurrent.ListenableFuture;
import com.google.common.util.concurrent.ListeningExecutorService;
import com.google.common.util.concurrent.MoreExecutors;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.RandomStringUtils;
import org.junit.After;
import org.junit.Assert;
import org.junit.Before;
import org.junit.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.thingsboard.common.util.ThingsBoardThreadFactory;
import org.thingsboard.server.common.data.Tenant;
import org.thingsboard.server.common.data.id.DeviceId;
import org.thingsboard.server.common.data.id.EntityId;
import org.thingsboard.server.common.data.kv.BasicTsKvEntry;
import org.thingsboard.server.common.data.kv.BooleanDataEntry;
import org.thingsboard.server.common.data.kv.DoubleDataEntry;
import org.thingsboard.server.common.data.kv.LongDataEntry;
import org.thingsboard.server.common.data.kv.StringDataEntry;
import org.thingsboard.server.common.data.kv.TsKvEntry;
import org.thingsboard.server.dao.service.AbstractServiceTest;
import org.thingsboard.server.dao.service.DaoSqlTest;
import org.thingsboard.server.dao.timeseries.TimeseriesLatestDao;
import java.util.ArrayList;
import java.util.List;
import java.util.Random;
import java.util.UUID;
import java.util.concurrent.Executors;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicLong;
@DaoSqlTest
@Slf4j
public class LatestTimeseriesPerformanceTest extends AbstractServiceTest {
private static final String STRING_KEY = "stringKey";
private static final String LONG_KEY = "longKey";
private static final String DOUBLE_KEY = "doubleKey";
private static final String BOOLEAN_KEY = "booleanKey";
public static final int AMOUNT_OF_UNIQ_KEY = 10000;
private final Random random = new Random();
@Autowired
private TimeseriesLatestDao timeseriesLatestDao;
private ListeningExecutorService testExecutor;
private EntityId entityId;
private AtomicLong saveCounter;
@Before
public void before() {
Tenant tenant = new Tenant();
tenant.setTitle("My tenant");
Tenant savedTenant = tenantService.saveTenant(tenant);
Assert.assertNotNull(savedTenant);
tenantId = savedTenant.getId();
entityId = new DeviceId(UUID.randomUUID());
saveCounter = new AtomicLong(0);
testExecutor = MoreExecutors.listeningDecorator(Executors.newFixedThreadPool(200, ThingsBoardThreadFactory.forName(getClass().getSimpleName() + "-test-scope")));
}
@After
public void after() {
tenantService.deleteTenant(tenantId);
if (testExecutor != null) {
testExecutor.shutdownNow();
}
}
@Test
public void test_save_latest_timeseries() throws Exception {
warmup();
saveCounter.set(0);
long startTime = System.currentTimeMillis();
List<ListenableFuture<?>> futures = new ArrayList<>();
for (int i = 0; i < 25_000; i++) {
futures.add(save(generateStrEntry(getRandomKey())));
futures.add(save(generateLngEntry(getRandomKey())));
futures.add(save(generateDblEntry(getRandomKey())));
futures.add(save(generateBoolEntry(getRandomKey())));
}
Futures.allAsList(futures).get(60, TimeUnit.SECONDS);
long endTime = System.currentTimeMillis();
long totalTime = endTime - startTime;
log.info("Total time: {}", totalTime);
log.info("Saved count: {}", saveCounter.get());
log.warn("Saved per 1 sec: {}", saveCounter.get() * 1000 / totalTime);
}
private void warmup() throws Exception {
List<ListenableFuture<?>> futures = new ArrayList<>();
for (int i = 0; i < AMOUNT_OF_UNIQ_KEY; i++) {
futures.add(save(generateStrEntry(i)));
futures.add(save(generateLngEntry(i)));
futures.add(save(generateDblEntry(i)));
futures.add(save(generateBoolEntry(i)));
}
Futures.allAsList(futures).get(60, TimeUnit.SECONDS);
}
private ListenableFuture<?> save(TsKvEntry tsKvEntry) {
return Futures.transformAsync(testExecutor.submit(() -> timeseriesLatestDao.saveLatest(tenantId, entityId, tsKvEntry)), result -> {
saveCounter.incrementAndGet();
return result;
}, testExecutor);
}
private TsKvEntry generateStrEntry(int keyIndex) {
return new BasicTsKvEntry(System.currentTimeMillis(), new StringDataEntry(STRING_KEY + keyIndex, RandomStringUtils.random(10)));
}
private TsKvEntry generateLngEntry(int keyIndex) {
return new BasicTsKvEntry(System.currentTimeMillis(), new LongDataEntry(LONG_KEY + keyIndex, random.nextLong()));
}
private TsKvEntry generateDblEntry(int keyIndex) {
return new BasicTsKvEntry(System.currentTimeMillis(), new DoubleDataEntry(DOUBLE_KEY + keyIndex, random.nextDouble()));
}
private TsKvEntry generateBoolEntry(int keyIndex) {
return new BasicTsKvEntry(System.currentTimeMillis(), new BooleanDataEntry(BOOLEAN_KEY + keyIndex, random.nextBoolean()));
}
private int getRandomKey() {
return random.nextInt(AMOUNT_OF_UNIQ_KEY);
}
}

17
dao/src/test/resources/application-test.properties

@ -10,6 +10,7 @@ audit-log.sink.type=none
#cache.type=caffeine # will be injected redis by RedisContainer or will be default (caffeine) #cache.type=caffeine # will be injected redis by RedisContainer or will be default (caffeine)
cache.maximumPoolSize=16 cache.maximumPoolSize=16
cache.attributes.enabled=true cache.attributes.enabled=true
cache.ts_latest.enabled=true
cache.specs.relations.timeToLiveInMinutes=1440 cache.specs.relations.timeToLiveInMinutes=1440
cache.specs.relations.maxSize=100000 cache.specs.relations.maxSize=100000
@ -59,8 +60,11 @@ cache.specs.assetProfiles.maxSize=100000
cache.specs.attributes.timeToLiveInMinutes=1440 cache.specs.attributes.timeToLiveInMinutes=1440
cache.specs.attributes.maxSize=100000 cache.specs.attributes.maxSize=100000
cache.specs.tokensOutdatageTime.timeToLiveInMinutes=1440 cache.specs.tsLatest.timeToLiveInMinutes=1440
cache.specs.tokensOutdatageTime.maxSize=100000 cache.specs.tsLatest.maxSize=100000
cache.specs.userSessionsInvalidation.timeToLiveInMinutes=1440
cache.specs.userSessionsInvalidation.maxSize=10000
cache.specs.otaPackages.timeToLiveInMinutes=1440 cache.specs.otaPackages.timeToLiveInMinutes=1440
cache.specs.otaPackages.maxSize=100000 cache.specs.otaPackages.maxSize=100000
@ -89,6 +93,15 @@ cache.specs.resourceInfo.maxSize=10000
cache.specs.alarmTypes.timeToLiveInMinutes=60 cache.specs.alarmTypes.timeToLiveInMinutes=60
cache.specs.alarmTypes.maxSize=10000 cache.specs.alarmTypes.maxSize=10000
cache.specs.userSettings.timeToLiveInMinutes=1440
cache.specs.userSettings.maxSize=10000
cache.specs.mobileAppSettings.timeToLiveInMinutes=1440
cache.specs.mobileAppSettings.maxSize=10000
cache.specs.mobileSecretKey.timeToLiveInMinutes=1440
cache.specs.mobileSecretKey.maxSize=10000
redis.connection.host=localhost redis.connection.host=localhost
redis.connection.port=6379 redis.connection.port=6379
redis.connection.db=0 redis.connection.db=0

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

Loading…
Cancel
Save