Browse Source

Merge remote-tracking branch 'origin/feature/attr_tskv_version' into feature/entities-version

# Conflicts:
#	common/data/src/main/java/org/thingsboard/server/common/data/HasVersion.java
pull/11112/head
ViacheslavKlimov 2 years ago
parent
commit
7fb5f268c7
  1. 2
      application/src/main/java/org/thingsboard/server/service/device/DeviceProvisionServiceImpl.java
  2. 6
      application/src/main/java/org/thingsboard/server/service/edge/rpc/EdgeGrpcSession.java
  3. 2
      application/src/main/java/org/thingsboard/server/service/install/DefaultSystemDataLoaderService.java
  4. 6
      application/src/main/java/org/thingsboard/server/service/telemetry/DefaultTelemetrySubscriptionService.java
  5. 7
      application/src/main/resources/thingsboard.yml
  6. 57
      build.sh
  7. 38
      common/cache/src/main/java/org/thingsboard/server/cache/RedisTbTransactionalCache.java
  8. 35
      common/cache/src/main/java/org/thingsboard/server/cache/TbJavaRedisSerializer.java
  9. 94
      common/cache/src/main/java/org/thingsboard/server/cache/VersionedCaffeineTbCache.java
  10. 170
      common/cache/src/main/java/org/thingsboard/server/cache/VersionedRedisTbCache.java
  11. 33
      common/cache/src/main/java/org/thingsboard/server/cache/VersionedTbCache.java
  12. 45
      common/cache/src/test/java/org/thingsboard/server/cache/TsLatestRedisCacheTest.java
  13. 22
      common/dao-api/src/main/java/org/thingsboard/server/dao/attributes/AttributesService.java
  14. 2
      common/dao-api/src/main/java/org/thingsboard/server/dao/timeseries/TimeseriesService.java
  15. 26
      common/dao-api/src/main/java/org/thingsboard/server/dao/util/SqlTsLatestAnyDaoCachedRedis.java
  16. 1
      common/data/src/main/java/org/thingsboard/server/common/data/CacheConstants.java
  17. 2
      common/data/src/main/java/org/thingsboard/server/common/data/HasVersion.java
  18. 4
      common/data/src/main/java/org/thingsboard/server/common/data/kv/AttributeKvEntry.java
  19. 14
      common/data/src/main/java/org/thingsboard/server/common/data/kv/BaseAttributeKvEntry.java
  20. 13
      common/data/src/main/java/org/thingsboard/server/common/data/kv/BasicTsKvEntry.java
  21. 3
      common/data/src/main/java/org/thingsboard/server/common/data/kv/TsKvEntry.java
  22. 11
      common/data/src/main/java/org/thingsboard/server/common/data/kv/TsKvLatestRemovingResult.java
  23. 8
      common/data/src/main/java/org/thingsboard/server/common/data/util/CollectionsUtil.java
  24. 5
      dao/pom.xml
  25. 130
      dao/src/main/java/org/thingsboard/server/dao/AbstractVersionedInsertRepository.java
  26. 4
      dao/src/main/java/org/thingsboard/server/dao/attributes/AttributeCaffeineCache.java
  27. 33
      dao/src/main/java/org/thingsboard/server/dao/attributes/AttributeRedisCache.java
  28. 5
      dao/src/main/java/org/thingsboard/server/dao/attributes/AttributesDao.java
  29. 42
      dao/src/main/java/org/thingsboard/server/dao/attributes/BaseAttributesService.java
  30. 117
      dao/src/main/java/org/thingsboard/server/dao/attributes/CachedAttributesService.java
  31. 15
      dao/src/main/java/org/thingsboard/server/dao/model/BaseVersionedEntity.java
  32. 1
      dao/src/main/java/org/thingsboard/server/dao/model/ModelConstants.java
  33. 7
      dao/src/main/java/org/thingsboard/server/dao/model/sql/AbstractTsKvEntity.java
  34. 6
      dao/src/main/java/org/thingsboard/server/dao/model/sql/AttributeKvEntity.java
  35. 30
      dao/src/main/java/org/thingsboard/server/dao/model/sql/VersionedEntity.java
  36. 11
      dao/src/main/java/org/thingsboard/server/dao/model/sqlts/latest/TsKvLatestEntity.java
  37. 4
      dao/src/main/java/org/thingsboard/server/dao/sql/JpaAbstractDaoListeningExecutorService.java
  38. 47
      dao/src/main/java/org/thingsboard/server/dao/sql/TbSqlBlockingQueue.java
  39. 1
      dao/src/main/java/org/thingsboard/server/dao/sql/TbSqlBlockingQueueParams.java
  40. 15
      dao/src/main/java/org/thingsboard/server/dao/sql/TbSqlBlockingQueueWrapper.java
  41. 8
      dao/src/main/java/org/thingsboard/server/dao/sql/TbSqlQueue.java
  42. 6
      dao/src/main/java/org/thingsboard/server/dao/sql/TbSqlQueueElement.java
  43. 228
      dao/src/main/java/org/thingsboard/server/dao/sql/attributes/AttributeKvInsertRepository.java
  44. 5
      dao/src/main/java/org/thingsboard/server/dao/sql/attributes/AttributeKvRepository.java
  45. 32
      dao/src/main/java/org/thingsboard/server/dao/sql/attributes/JpaAttributeDao.java
  46. 2
      dao/src/main/java/org/thingsboard/server/dao/sql/edge/JpaBaseEdgeEventDao.java
  47. 2
      dao/src/main/java/org/thingsboard/server/dao/sql/event/JpaBaseEventDao.java
  48. 2
      dao/src/main/java/org/thingsboard/server/dao/sqlts/AbstractChunkedAggregationTimeseriesDao.java
  49. 168
      dao/src/main/java/org/thingsboard/server/dao/sqlts/CachedRedisSqlTimeseriesLatestDao.java
  50. 62
      dao/src/main/java/org/thingsboard/server/dao/sqlts/SqlTimeseriesLatestDao.java
  51. 2
      dao/src/main/java/org/thingsboard/server/dao/sqlts/insert/latest/InsertLatestTsRepository.java
  52. 224
      dao/src/main/java/org/thingsboard/server/dao/sqlts/insert/latest/sql/SqlLatestInsertTsRepository.java
  53. 2
      dao/src/main/java/org/thingsboard/server/dao/sqlts/latest/SearchTsKvLatestRepository.java
  54. 8
      dao/src/main/java/org/thingsboard/server/dao/sqlts/timescale/TimescaleTimeseriesDao.java
  55. 4
      dao/src/main/java/org/thingsboard/server/dao/timeseries/BaseTimeseriesService.java
  56. 4
      dao/src/main/java/org/thingsboard/server/dao/timeseries/CassandraBaseTimeseriesLatestDao.java
  57. 2
      dao/src/main/java/org/thingsboard/server/dao/timeseries/TimeseriesLatestDao.java
  58. 40
      dao/src/main/java/org/thingsboard/server/dao/timeseries/TsLatestCacheKey.java
  59. 55
      dao/src/main/java/org/thingsboard/server/dao/timeseries/TsLatestRedisCache.java
  60. 6
      dao/src/main/resources/sql/schema-entities.sql
  61. 3
      dao/src/main/resources/sql/schema-timescale.sql
  62. 10
      dao/src/main/resources/sql/schema-ts-latest-psql.sql
  63. 90
      dao/src/test/java/org/thingsboard/server/dao/AbstractRedisClusterContainer.java
  64. 31
      dao/src/test/java/org/thingsboard/server/dao/RedisClusterSqlTestSuite.java
  65. 64
      dao/src/test/java/org/thingsboard/server/dao/RedisJUnit5Test.java
  66. 16
      dao/src/test/java/org/thingsboard/server/dao/service/EntityServiceTest.java
  67. 23
      dao/src/test/java/org/thingsboard/server/dao/service/attributes/BaseAttributesServiceTest.java
  68. 103
      dao/src/test/java/org/thingsboard/server/dao/service/timeseries/BaseTimeseriesServiceTest.java
  69. 153
      dao/src/test/java/org/thingsboard/server/dao/service/timeseries/sql/LatestTimeseriesPerformanceTest.java
  70. 4
      dao/src/test/resources/application-test.properties
  71. 6
      dao/src/test/resources/logback-test.xml
  72. 2
      dao/src/test/resources/sql/psql/drop-all-tables.sql
  73. 12
      pom.xml

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);

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/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"

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

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

@ -29,7 +29,6 @@ import org.springframework.data.redis.core.types.Expiration;
import org.springframework.data.redis.serializer.RedisSerializer; import org.springframework.data.redis.serializer.RedisSerializer;
import org.springframework.data.redis.serializer.StringRedisSerializer; import org.springframework.data.redis.serializer.StringRedisSerializer;
import org.thingsboard.server.common.data.FstStatsService; import org.thingsboard.server.common.data.FstStatsService;
import redis.clients.jedis.Connection;
import redis.clients.jedis.Jedis; import redis.clients.jedis.Jedis;
import redis.clients.jedis.JedisPool; import redis.clients.jedis.JedisPool;
import redis.clients.jedis.util.JedisClusterCRC16; import redis.clients.jedis.util.JedisClusterCRC16;
@ -44,7 +43,7 @@ import java.util.concurrent.TimeUnit;
@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
@ -52,11 +51,12 @@ 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;
public RedisTbTransactionalCache(String cacheName, public RedisTbTransactionalCache(String cacheName,
CacheSpecsMap cacheSpecsMap, CacheSpecsMap cacheSpecsMap,
@ -79,8 +79,8 @@ public abstract class RedisTbTransactionalCache<K extends Serializable, V extend
public TbCacheValueWrapper<V> get(K key) { public TbCacheValueWrapper<V> get(K key) {
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);
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();
@ -96,6 +96,10 @@ public abstract class RedisTbTransactionalCache<K extends Serializable, V extend
} }
} }
protected byte[] doGet(RedisConnection connection, byte[] rawKey) {
return connection.stringCommands().get(rawKey);
}
@Override @Override
public void put(K key, V value) { public void put(K key, V value) {
try (var connection = connectionFactory.getConnection()) { try (var connection = connectionFactory.getConnection()) {
@ -113,7 +117,7 @@ public abstract class RedisTbTransactionalCache<K extends Serializable, V extend
@Override @Override
public void evict(K key) { public void evict(K key) {
try (var connection = connectionFactory.getConnection()) { try (var connection = connectionFactory.getConnection()) {
connection.del(getRawKey(key)); connection.keyCommands().del(getRawKey(key));
} }
} }
@ -124,7 +128,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));
} }
} }
@ -132,10 +136,10 @@ public abstract class RedisTbTransactionalCache<K extends Serializable, V extend
public void evictOrPut(K key, V value) { public void evictOrPut(K key, V value) {
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);
} }
} }
} }
@ -153,7 +157,7 @@ public abstract class RedisTbTransactionalCache<K extends Serializable, V extend
return new RedisTbCacheTransaction<>(this, connection); return new RedisTbCacheTransaction<>(this, connection);
} }
private RedisConnection getConnection(byte[] rawKey) { protected RedisConnection getConnection(byte[] rawKey) {
if (!connectionFactory.isRedisClusterAware()) { if (!connectionFactory.isRedisClusterAware()) {
return connectionFactory.getConnection(); return connectionFactory.getConnection();
} }
@ -168,7 +172,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);
@ -180,7 +184,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 {
@ -196,7 +200,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 {
@ -215,8 +219,12 @@ public abstract class RedisTbTransactionalCache<K extends Serializable, V extend
public void put(RedisConnection connection, K key, V value, RedisStringCommands.SetOption setOption) { public void put(RedisConnection connection, K key, V value, RedisStringCommands.SetOption setOption) {
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);
} }
} }

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);
}
}

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

@ -0,0 +1,94 @@
/**
* 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.RequiredArgsConstructor;
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;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
@RequiredArgsConstructor
public abstract class VersionedCaffeineTbCache<K extends Serializable, V extends Serializable & HasVersion> implements VersionedTbCache<K, V> {
private final CacheManager cacheManager;
private final String cacheName;
private final Lock lock = new ReentrantLock();
@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;
put(key, value, version);
}
@Override
public void put(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()) {
cacheManager.getCache(cacheName).put(key, TbPair.of(version, value));
}
} finally {
lock.unlock();
}
}
private TbPair<Long, V> doGet(K key) {
Cache.ValueWrapper source = cacheManager.getCache(cacheName).get(key);
return source == null ? null : (TbPair<Long, V>) source.get();
}
@Override
public void evict(K key) {
lock.lock();
try {
cacheManager.getCache(cacheName).evict(key);
} finally {
lock.unlock();
}
}
@Override
public void evict(K key, Long version) {
if (version == null) {
return;
}
lock.lock();
try {
put(key, null, version);
} finally {
lock.unlock();
}
}
}

170
common/cache/src/main/java/org/thingsboard/server/cache/VersionedRedisTbCache.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.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;
import java.util.Collection;
import java.util.List;
@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 or newVersion == 1 and currentVersion > 1 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("05a09f34f523429c96c6eaabbe6f2595f5cba2c3");
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) {
return connection.stringCommands().getRange(rawKey, VERSION_SIZE, VALUE_END_OFFSET);
}
@Override
public void put(K key, V value) {
Long version = value != null ? value.getVersion() : 0;
put(key, value, version);
}
@Override
public void put(K key, V value, Long version) {
log.trace("put [{}][{}][{}]", key, value, version);
doPut(key, value, version, cacheTtl);
}
private void doPut(K key, V value, Long version, Expiration expiration) {
if (version == null) {
return;
}
final byte[] rawKey = getRawKey(key);
byte[] rawValue = getRawValue(value);
byte[] rawVersion = StringRedisSerializer.UTF_8.serialize(String.valueOf(version));
byte[] rawExpiration = StringRedisSerializer.UTF_8.serialize(String.valueOf(expiration.getExpirationTimeInSeconds()));
try (var connection = getConnection(rawKey)) {
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) {
log.trace("putIfAbsent [{}][{}]", key, value);
throw new NotImplementedException("putIfAbsent is not supported by versioned cache");
}
@Override
public void evict(Collection<K> keys) {
throw new NotImplementedException("evict by many keys is not supported by versioned cache");
}
@Override
public void evictOrPut(K key, V value) {
throw new NotImplementedException("evictOrPut is not supported by versioned cache");
}
@Override
public TbCacheTransaction<K, V> newTransactionForKey(K key) {
throw new NotImplementedException("newTransactionForKey is not supported by versioned cache");
}
@Override
public TbCacheTransaction<K, V> newTransactionForKeys(List<K> keys) {
throw new NotImplementedException("newTransactionForKeys is not supported by versioned cache");
}
}

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

@ -0,0 +1,33 @@
/**
* 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;
public interface VersionedTbCache<K extends Serializable, V extends Serializable & HasVersion> {
TbCacheValueWrapper<V> get(K key);
void put(K key, V value);
void put(K key, V value, Long version);
void evict(K key);
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);

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";

2
common/data/src/main/java/org/thingsboard/server/common/data/HasVersion.java

@ -17,7 +17,7 @@ package org.thingsboard.server.common.data;
public interface HasVersion { public interface HasVersion {
Integer getVersion(); Long getVersion();
void setVersion(Integer version); void setVersion(Integer version);

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();

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

@ -29,9 +29,18 @@ 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(KvEntry kv, long lastUpdateTs, Long version) {
this.kv = kv;
this.lastUpdateTs = lastUpdateTs;
this.version = version;
} }
public BaseAttributeKvEntry(long lastUpdateTs, KvEntry kv) { public BaseAttributeKvEntry(long lastUpdateTs, KvEntry kv) {
@ -88,6 +97,11 @@ public class BaseAttributeKvEntry implements AttributeKvEntry {
return kv.getValue(); return kv.getValue();
} }
@Override
public Long getVersion() {
return version;
}
@Override @Override
public boolean equals(Object o) { public boolean equals(Object o) {
if (this == o) return true; if (this == o) return true;

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

@ -25,9 +25,18 @@ public class BasicTsKvEntry implements TsKvEntry {
@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
@ -118,4 +127,8 @@ public class BasicTsKvEntry implements TsKvEntry {
return Math.max(1, (length + MAX_CHARS_PER_DATA_POINT - 1) / MAX_CHARS_PER_DATA_POINT); return Math.max(1, (length + MAX_CHARS_PER_DATA_POINT - 1) / MAX_CHARS_PER_DATA_POINT);
} }
@Override
public Long getVersion() {
return version;
}
} }

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;
} }
} }

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);
} }

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);

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

@ -21,9 +21,9 @@ 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.data.kv.BaseAttributeKvEntry;
@ -38,7 +38,7 @@ 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<>() {
@ -82,26 +82,15 @@ public class AttributeRedisCache extends RedisTbTransactionalCache<AttributeCach
try { try {
AttributeValueProto proto = AttributeValueProto.parseFrom(bytes); AttributeValueProto proto = AttributeValueProto.parseFrom(bytes);
boolean hasValue = proto.getHasV(); boolean hasValue = proto.getHasV();
KvEntry entry; KvEntry entry = switch (proto.getType()) {
switch (proto.getType()) { case BOOLEAN_V -> new BooleanDataEntry(key.getKey(), hasValue ? proto.getBoolV() : null);
case BOOLEAN_V: case LONG_V -> new LongDataEntry(key.getKey(), hasValue ? proto.getLongV() : null);
entry = new BooleanDataEntry(key.getKey(), hasValue ? proto.getBoolV() : null); case DOUBLE_V -> new DoubleDataEntry(key.getKey(), hasValue ? proto.getDoubleV() : null);
break; case STRING_V -> new StringDataEntry(key.getKey(), hasValue ? proto.getStringV() : null);
case LONG_V: case JSON_V -> new JsonDataEntry(key.getKey(), hasValue ? proto.getJsonV() : null);
entry = new LongDataEntry(key.getKey(), hasValue ? proto.getLongV() : null); default ->
break; throw new InvalidProtocolBufferException("Unrecognized type: " + proto.getType() + " !");
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); 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);
} }

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

@ -27,14 +27,14 @@ 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.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 +68,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 +80,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 +109,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 +123,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 +156,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, foundInDbAttribute.getVersion());
for (AttributeKvEntry foundInDbAttribute : result) { notFoundAttributeKeys.remove(foundInDbAttribute.getKey());
AttributeCacheKey attributeCacheKey = new AttributeCacheKey(scope, entityId, foundInDbAttribute.getKey());
cacheTransaction.putIfAbsent(attributeCacheKey, foundInDbAttribute);
notFoundAttributeKeys.remove(foundInDbAttribute.getKey());
}
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;
} }
for (String key : notFoundAttributeKeys) {
cache.put(new AttributeCacheKey(scope, entityId, key), null);
}
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 +188,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 +200,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 +215,38 @@ 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); ListenableFuture<Long> future = attributesDao.save(tenantId, entityId, scope, attribute);
return Futures.transform(future, key -> evict(entityId, scope, attribute, key), cacheExecutor); return Futures.transform(future, version -> put(entityId, scope, attribute, version), 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); ListenableFuture<Long> future = attributesDao.save(tenantId, entityId, scope, attribute);
futures.add(Futures.transform(future, key -> evict(entityId, scope, attribute, key), cacheExecutor)); futures.add(Futures.transform(future, version -> put(entityId, scope, attribute, version), cacheExecutor));
} }
return Futures.allAsList(futures); return Futures.allAsList(futures);
} }
private String evict(EntityId entityId, AttributeScope scope, AttributeKvEntry attribute, String key) { private Long put(EntityId entityId, AttributeScope scope, AttributeKvEntry attribute, Long version) {
log.trace("[{}][{}][{}] Before cache evict: {}", entityId, scope, key, attribute); String key = attribute.getKey();
cache.evictOrPut(new AttributeCacheKey(scope, entityId, key), attribute); log.trace("[{}][{}][{}] Before cache put: {}", entityId, scope, key, attribute);
log.trace("[{}][{}][{}] after cache evict.", entityId, scope, key); cache.put(new AttributeCacheKey(scope, entityId, key), attribute, version);
return key; log.trace("[{}][{}][{}] after cache put.", entityId, scope, key);
return version;
} }
@Override @Override
@ -299,9 +257,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()));
} }

15
dao/src/main/java/org/thingsboard/server/dao/sql/attributes/SqlAttributesInsertRepository.java → dao/src/main/java/org/thingsboard/server/dao/model/BaseVersionedEntity.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.dao.model;
import org.springframework.stereotype.Repository; public interface BaseVersionedEntity {
import org.springframework.transaction.annotation.Transactional; long getVersion();
import org.thingsboard.server.dao.util.SqlDao; }
@Repository
@Transactional
@SqlDao
public class SqlAttributesInsertRepository extends AttributeKvInsertRepository {
}

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

@ -57,6 +57,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

@ -16,6 +16,7 @@
package org.thingsboard.server.dao.model.sql; package org.thingsboard.server.dao.model.sql;
import lombok.Data; import lombok.Data;
import lombok.EqualsAndHashCode;
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.kv.BaseAttributeKvEntry;
import org.thingsboard.server.common.data.kv.BooleanDataEntry; import org.thingsboard.server.common.data.kv.BooleanDataEntry;
@ -41,9 +42,10 @@ 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;
@Data @Data
@EqualsAndHashCode(callSuper = true)
@Entity @Entity
@Table(name = "attribute_kv") @Table(name = "attribute_kv")
public class AttributeKvEntity implements ToData<AttributeKvEntry>, Serializable { public class AttributeKvEntity extends VersionedEntity implements ToData<AttributeKvEntry>, Serializable {
@EmbeddedId @EmbeddedId
private AttributeKvCompositeKey id; private AttributeKvCompositeKey id;
@ -84,6 +86,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);
} }
} }

30
dao/src/main/java/org/thingsboard/server/dao/model/sql/VersionedEntity.java

@ -0,0 +1,30 @@
/**
* 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.sql;
import jakarta.persistence.Column;
import jakarta.persistence.MappedSuperclass;
import lombok.Data;
import static org.thingsboard.server.dao.model.ModelConstants.VERSION_COLUMN;
@Data
@MappedSuperclass
public abstract class VersionedEntity {
@Column(name = VERSION_COLUMN)
protected Long version;
}

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;
} }
} }

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<>();

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;

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

@ -0,0 +1,168 @@
/**
* 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.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, x -> {
cache.put(new TsLatestCacheKey(entityId, tsKvEntry.getKey()), tsKvEntry, x);
return x;
},
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();
if (x.getData() != null) {
cache.put(key, x.getData(), 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

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

@ -18,8 +18,9 @@ package org.thingsboard.server.dao.sqlts.timescale;
import com.google.common.util.concurrent.Futures; 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 com.google.common.util.concurrent.MoreExecutors;
import jakarta.annotation.PostConstruct;
import jakarta.annotation.PreDestroy;
import lombok.extern.slf4j.Slf4j; import lombok.extern.slf4j.Slf4j;
import org.jetbrains.annotations.NotNull;
import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.domain.PageRequest; import org.springframework.data.domain.PageRequest;
import org.springframework.data.domain.Sort; import org.springframework.data.domain.Sort;
@ -38,7 +39,6 @@ import org.thingsboard.server.dao.DaoUtil;
import org.thingsboard.server.dao.dictionary.KeyDictionaryDao; import org.thingsboard.server.dao.dictionary.KeyDictionaryDao;
import org.thingsboard.server.dao.model.sql.AbstractTsKvEntity; import org.thingsboard.server.dao.model.sql.AbstractTsKvEntity;
import org.thingsboard.server.dao.model.sqlts.timescale.ts.TimescaleTsKvEntity; import org.thingsboard.server.dao.model.sqlts.timescale.ts.TimescaleTsKvEntity;
import org.thingsboard.server.dao.model.sqlts.ts.TsKvEntity;
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.sqlts.AbstractSqlTimeseriesDao; import org.thingsboard.server.dao.sqlts.AbstractSqlTimeseriesDao;
@ -47,8 +47,6 @@ import org.thingsboard.server.dao.timeseries.TimeseriesDao;
import org.thingsboard.server.dao.util.TimeUtils; import org.thingsboard.server.dao.util.TimeUtils;
import org.thingsboard.server.dao.util.TimescaleDBTsDao; import org.thingsboard.server.dao.util.TimescaleDBTsDao;
import jakarta.annotation.PostConstruct;
import jakarta.annotation.PreDestroy;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.Collections; import java.util.Collections;
import java.util.Comparator; import java.util.Comparator;
@ -77,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).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());
}
}
});
}
}

6
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 1000;
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)
); );
@ -550,6 +553,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 1000;
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,
@ -560,6 +565,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 1000;
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 1000;
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)
);

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

@ -0,0 +1,90 @@
/**
* 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 redis.clients.jedis.Jedis;
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);
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();
}
}

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));

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

@ -26,7 +26,7 @@ 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.cache.VersionedTbCache;
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;
@ -59,9 +59,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;
@ -132,24 +129,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());

103
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);
@ -150,8 +154,6 @@ 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);
@ -162,9 +164,75 @@ public abstract class BaseTimeseriesServiceTest extends AbstractServiceTest {
} }
@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();
Assert.assertEquals(toTsEntry(TS, stringKvEntry), entryOpt.orElse(null));
}
@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();
Assert.assertEquals(toTsEntry(TS, new StringDataEntry(STRING_KEY, "new")), entryOpt.orElse(null));
}
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();
Assert.assertEquals(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);
@ -176,8 +244,6 @@ public abstract class BaseTimeseriesServiceTest extends AbstractServiceTest {
@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 +268,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 +287,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 +308,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 +331,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 +354,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 +375,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 +396,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 +420,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 +430,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 +448,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 +466,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 +616,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 +706,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 +736,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);
} }

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);
}
}

4
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,6 +60,9 @@ 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.tsLatest.timeToLiveInMinutes=1440
cache.specs.tsLatest.maxSize=100000
cache.specs.tokensOutdatageTime.timeToLiveInMinutes=1440 cache.specs.tokensOutdatageTime.timeToLiveInMinutes=1440
cache.specs.tokensOutdatageTime.maxSize=100000 cache.specs.tokensOutdatageTime.maxSize=100000

6
dao/src/test/resources/logback.xml → dao/src/test/resources/logback-test.xml

@ -9,9 +9,15 @@
<logger name="org.thingsboard.server.dao" level="WARN"/> <logger name="org.thingsboard.server.dao" level="WARN"/>
<logger name="org.testcontainers" level="INFO" /> <logger name="org.testcontainers" level="INFO" />
<logger name="org.thingsboard.server.dao.sqlts" level="INFO" />
<logger name="org.thingsboard.server.dao.sqlts.CachedRedisSqlTimeseriesLatestDao" level="DEBUG" />
<logger name="org.thingsboard.server.dao.sqlts.SqlTimeseriesLatestDao" level="TRACE" />
<logger name="org.thingsboard.server.dao.timeseries.TsLatestRedisCache" level="TRACE" />
<!-- Log Hibernate SQL queries --> <!-- Log Hibernate SQL queries -->
<!-- <logger name="org.hibernate.SQL" level="DEBUG"/> --> <!-- <logger name="org.hibernate.SQL" level="DEBUG"/> -->
<!-- <logger name="org.springframework.jdbc.core.JdbcTemplate" level="DEBUG" /> -->
<root level="WARN"> <root level="WARN">
<appender-ref ref="console"/> <appender-ref ref="console"/>

2
dao/src/test/resources/sql/psql/drop-all-tables.sql

@ -23,6 +23,7 @@ DROP TABLE IF EXISTS alarm_type;
DROP TABLE IF EXISTS asset; DROP TABLE IF EXISTS asset;
DROP TABLE IF EXISTS audit_log; DROP TABLE IF EXISTS audit_log;
DROP TABLE IF EXISTS attribute_kv; DROP TABLE IF EXISTS attribute_kv;
DROP SEQUENCE IF EXISTS attribute_kv_version_seq;
DROP TABLE IF EXISTS component_descriptor; DROP TABLE IF EXISTS component_descriptor;
DROP TABLE IF EXISTS customer; DROP TABLE IF EXISTS customer;
DROP TABLE IF EXISTS device; DROP TABLE IF EXISTS device;
@ -36,6 +37,7 @@ DROP TABLE IF EXISTS relation;
DROP TABLE IF EXISTS tenant; DROP TABLE IF EXISTS tenant;
DROP TABLE IF EXISTS ts_kv; DROP TABLE IF EXISTS ts_kv;
DROP TABLE IF EXISTS ts_kv_latest; DROP TABLE IF EXISTS ts_kv_latest;
DROP SEQUENCE IF EXISTS ts_kv_latest_version_seq;
DROP TABLE IF EXISTS ts_kv_dictionary; DROP TABLE IF EXISTS ts_kv_dictionary;
DROP TABLE IF EXISTS user_credentials; DROP TABLE IF EXISTS user_credentials;
DROP TABLE IF EXISTS widgets_bundle_widget; DROP TABLE IF EXISTS widgets_bundle_widget;

12
pom.xml

@ -2162,6 +2162,18 @@
<version>${testcontainers-junit4-mock.version}</version> <version>${testcontainers-junit4-mock.version}</version>
<scope>test</scope> <scope>test</scope>
</dependency> </dependency>
<dependency>
<groupId>org.testcontainers</groupId>
<artifactId>junit-jupiter</artifactId>
<version>${testcontainers.version}</version>
<scope>test</scope>
<exclusions>
<exclusion>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
</exclusion>
</exclusions>
</dependency>
<dependency> <dependency>
<groupId>org.zeroturnaround</groupId> <groupId>org.zeroturnaround</groupId>
<artifactId>zt-exec</artifactId> <artifactId>zt-exec</artifactId>

Loading…
Cancel
Save