Merge remote-tracking branch 'smatvienko-tb/feature/latest-ts-redis-cache-aside-dao' into feature/attr_tskv_version
This commit is contained in:
		
						commit
						6c417f209f
					
				@ -491,6 +491,10 @@ cache:
 | 
			
		||||
  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'
 | 
			
		||||
    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:
 | 
			
		||||
    relations:
 | 
			
		||||
      timeToLiveInMinutes: "${CACHE_SPECS_RELATIONS_TTL:1440}" # Relations cache TTL
 | 
			
		||||
@ -547,6 +551,9 @@ cache:
 | 
			
		||||
    attributes:
 | 
			
		||||
      timeToLiveInMinutes: "${CACHE_SPECS_ATTRIBUTES_TTL:1440}" # Attributes cache TTL
 | 
			
		||||
      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:
 | 
			
		||||
      # The value of this TTL is ignored and replaced by the JWT refresh token expiration time
 | 
			
		||||
      timeToLiveInMinutes: "0"
 | 
			
		||||
 | 
			
		||||
							
								
								
									
										57
									
								
								build.sh
									
									
									
									
									
										Executable file
									
								
							
							
						
						
									
										57
									
								
								build.sh
									
									
									
									
									
										Executable file
									
								
							@ -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
 | 
			
		||||
@ -51,6 +51,7 @@ public abstract class RedisTbTransactionalCache<K extends Serializable, V extend
 | 
			
		||||
 | 
			
		||||
    @Getter
 | 
			
		||||
    private final String cacheName;
 | 
			
		||||
    @Getter
 | 
			
		||||
    private final JedisConnectionFactory connectionFactory;
 | 
			
		||||
    private final RedisSerializer<String> keySerializer = StringRedisSerializer.UTF_8;
 | 
			
		||||
    private final TbRedisSerializer<K, V> valueSerializer;
 | 
			
		||||
@ -116,7 +117,7 @@ public abstract class RedisTbTransactionalCache<K extends Serializable, V extend
 | 
			
		||||
    @Override
 | 
			
		||||
    public void evict(K key) {
 | 
			
		||||
        try (var connection = connectionFactory.getConnection()) {
 | 
			
		||||
            connection.del(getRawKey(key));
 | 
			
		||||
            connection.keyCommands().del(getRawKey(key));
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
@ -127,7 +128,7 @@ public abstract class RedisTbTransactionalCache<K extends Serializable, V extend
 | 
			
		||||
            return;
 | 
			
		||||
        }
 | 
			
		||||
        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));
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
@ -135,10 +136,10 @@ public abstract class RedisTbTransactionalCache<K extends Serializable, V extend
 | 
			
		||||
    public void evictOrPut(K key, V value) {
 | 
			
		||||
        try (var connection = connectionFactory.getConnection()) {
 | 
			
		||||
            var rawKey = getRawKey(key);
 | 
			
		||||
            var records = connection.del(rawKey);
 | 
			
		||||
            var records = connection.keyCommands().del(rawKey);
 | 
			
		||||
            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.
 | 
			
		||||
                connection.set(rawKey, getRawValue(value), evictExpiration, RedisStringCommands.SetOption.UPSERT);
 | 
			
		||||
                connection.stringCommands().set(rawKey, getRawValue(value), evictExpiration, RedisStringCommands.SetOption.UPSERT);
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
@ -171,7 +172,7 @@ public abstract class RedisTbTransactionalCache<K extends Serializable, V extend
 | 
			
		||||
        return jedisConnection;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    private RedisConnection watch(byte[][] rawKeysList) {
 | 
			
		||||
    protected RedisConnection watch(byte[][] rawKeysList) {
 | 
			
		||||
        RedisConnection connection = getConnection(rawKeysList[0]);
 | 
			
		||||
        try {
 | 
			
		||||
            connection.watch(rawKeysList);
 | 
			
		||||
@ -218,8 +219,12 @@ public abstract class RedisTbTransactionalCache<K extends Serializable, V extend
 | 
			
		||||
 | 
			
		||||
    public void put(RedisConnection connection, K key, V value, RedisStringCommands.SetOption setOption) {
 | 
			
		||||
        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);
 | 
			
		||||
        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
									
									
									
									
										vendored
									
									
										Normal file
									
								
							
							
						
						
									
										35
									
								
								common/cache/src/main/java/org/thingsboard/server/cache/TbJavaRedisSerializer.java
									
									
									
									
										vendored
									
									
										Normal file
									
								
							@ -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);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
}
 | 
			
		||||
@ -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 {
 | 
			
		||||
}
 | 
			
		||||
@ -36,6 +36,7 @@ public class CacheConstants {
 | 
			
		||||
 | 
			
		||||
    public static final String ASSET_PROFILE_CACHE = "assetProfiles";
 | 
			
		||||
    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 OTA_PACKAGE_CACHE = "otaPackages";
 | 
			
		||||
    public static final String OTA_PACKAGE_DATA_CACHE = "otaPackagesData";
 | 
			
		||||
 | 
			
		||||
@ -216,6 +216,11 @@
 | 
			
		||||
            <artifactId>jdbc</artifactId>
 | 
			
		||||
            <scope>test</scope>
 | 
			
		||||
        </dependency>
 | 
			
		||||
        <dependency>
 | 
			
		||||
            <groupId>org.testcontainers</groupId>
 | 
			
		||||
            <artifactId>junit-jupiter</artifactId>
 | 
			
		||||
            <scope>test</scope>
 | 
			
		||||
        </dependency>
 | 
			
		||||
        <dependency>
 | 
			
		||||
            <groupId>org.springframework</groupId>
 | 
			
		||||
            <artifactId>spring-context-support</artifactId>
 | 
			
		||||
 | 
			
		||||
@ -0,0 +1,181 @@
 | 
			
		||||
/**
 | 
			
		||||
 * Copyright © 2016-2024 The Thingsboard Authors
 | 
			
		||||
 *
 | 
			
		||||
 * Licensed under the Apache License, Version 2.0 (the "License");
 | 
			
		||||
 * you may not use this file except in compliance with the License.
 | 
			
		||||
 * You may obtain a copy of the License at
 | 
			
		||||
 *
 | 
			
		||||
 *     http://www.apache.org/licenses/LICENSE-2.0
 | 
			
		||||
 *
 | 
			
		||||
 * Unless required by applicable law or agreed to in writing, software
 | 
			
		||||
 * distributed under the License is distributed on an "AS IS" BASIS,
 | 
			
		||||
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 | 
			
		||||
 * See the License for the specific language governing permissions and
 | 
			
		||||
 * limitations under the License.
 | 
			
		||||
 */
 | 
			
		||||
package org.thingsboard.server.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.TbTransactionalCache;
 | 
			
		||||
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 TbTransactionalCache<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<Void> saveLatest(TenantId tenantId, EntityId entityId, TsKvEntry tsKvEntry) {
 | 
			
		||||
        ListenableFuture<Void> future = sqlDao.saveLatest(tenantId, entityId, tsKvEntry);
 | 
			
		||||
        future = Futures.transform(future, x -> {
 | 
			
		||||
                    cache.put(new TsLatestCacheKey(entityId, tsKvEntry.getKey()), tsKvEntry);
 | 
			
		||||
                    return x;
 | 
			
		||||
                },
 | 
			
		||||
                cacheExecutorService);
 | 
			
		||||
        if (log.isTraceEnabled()) {
 | 
			
		||||
            Futures.addCallback(future, new FutureCallback<>() {
 | 
			
		||||
                @Override
 | 
			
		||||
                public void onSuccess(Void 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 -> {
 | 
			
		||||
                    cache.evict(new TsLatestCacheKey(entityId, query.getKey()));
 | 
			
		||||
                    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.transformAsync(daoFuture, (daoValue) -> {
 | 
			
		||||
 | 
			
		||||
                if (daoValue.isEmpty()) {
 | 
			
		||||
                    //TODO implement the cache logic if no latest found in TS DAO. Currently we are always getting from DB to stay on the safe side
 | 
			
		||||
                    return Futures.immediateFuture(daoValue);
 | 
			
		||||
                }
 | 
			
		||||
                ListenableFuture<Optional<TsKvEntry>> cachePutFuture = cacheExecutorService.submit(() -> {
 | 
			
		||||
                    cache.put(new TsLatestCacheKey(entityId, key), daoValue.get());
 | 
			
		||||
                    return daoValue;
 | 
			
		||||
                });
 | 
			
		||||
 | 
			
		||||
                Futures.addCallback(cachePutFuture, new FutureCallback<>() {
 | 
			
		||||
                    @Override
 | 
			
		||||
                    public void onSuccess(Optional<TsKvEntry> result) {
 | 
			
		||||
                        log.trace("saveLatest onSuccess [{}][{}][{}]", entityId, key, result);
 | 
			
		||||
                    }
 | 
			
		||||
 | 
			
		||||
                    @Override
 | 
			
		||||
                    public void onFailure(Throwable t) {
 | 
			
		||||
                        log.info("saveLatest onFailure [{}][{}][{}]", entityId, key, daoValue, t);
 | 
			
		||||
                    }
 | 
			
		||||
 | 
			
		||||
                }, MoreExecutors.directExecutor());
 | 
			
		||||
                return cachePutFuture;
 | 
			
		||||
            }, 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);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
}
 | 
			
		||||
@ -157,12 +157,13 @@ public class SqlTimeseriesLatestDao extends BaseAbstractSqlTimeseriesDao impleme
 | 
			
		||||
 | 
			
		||||
    @Override
 | 
			
		||||
    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
 | 
			
		||||
    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
 | 
			
		||||
@ -206,7 +207,7 @@ public class SqlTimeseriesLatestDao extends BaseAbstractSqlTimeseriesDao impleme
 | 
			
		||||
                ReadTsKvQueryResult::getData, MoreExecutors.directExecutor());
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    protected TsKvEntry doFindLatest(EntityId entityId, String key) {
 | 
			
		||||
   protected TsKvEntry doFindLatestSync(EntityId entityId, String key) {
 | 
			
		||||
        TsKvLatestCompositeKey compositeKey =
 | 
			
		||||
                new TsKvLatestCompositeKey(
 | 
			
		||||
                        entityId.getId(),
 | 
			
		||||
@ -222,7 +223,7 @@ public class SqlTimeseriesLatestDao extends BaseAbstractSqlTimeseriesDao impleme
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    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 -> {
 | 
			
		||||
            if (latest == null) {
 | 
			
		||||
                return Futures.immediateFuture(new TsKvLatestRemovingResult(query.getKey(), false));
 | 
			
		||||
@ -263,10 +264,9 @@ public class SqlTimeseriesLatestDao extends BaseAbstractSqlTimeseriesDao impleme
 | 
			
		||||
        return tsLatestQueue.add(latestEntity);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    private TsKvEntry getLatestTsKvEntry(EntityId entityId, String key) {
 | 
			
		||||
        TsKvEntry latest = doFindLatest(entityId, key);
 | 
			
		||||
    protected TsKvEntry wrapNullTsKvEntry(final String key, final TsKvEntry latest) {
 | 
			
		||||
        if (latest == null) {
 | 
			
		||||
            latest = new BasicTsKvEntry(System.currentTimeMillis(), new StringDataEntry(key, null));
 | 
			
		||||
            return new BasicTsKvEntry(System.currentTimeMillis(), new StringDataEntry(key, null));
 | 
			
		||||
        }
 | 
			
		||||
        return latest;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
@ -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;
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
@ -0,0 +1,148 @@
 | 
			
		||||
/**
 | 
			
		||||
 * 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 jakarta.annotation.PostConstruct;
 | 
			
		||||
import lombok.extern.slf4j.Slf4j;
 | 
			
		||||
import org.apache.commons.lang3.NotImplementedException;
 | 
			
		||||
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
 | 
			
		||||
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.serializer.StringRedisSerializer;
 | 
			
		||||
import org.springframework.stereotype.Service;
 | 
			
		||||
import org.thingsboard.server.cache.CacheSpecsMap;
 | 
			
		||||
import org.thingsboard.server.cache.RedisTbTransactionalCache;
 | 
			
		||||
import org.thingsboard.server.cache.TBRedisCacheConfiguration;
 | 
			
		||||
import org.thingsboard.server.cache.TbCacheTransaction;
 | 
			
		||||
import org.thingsboard.server.cache.TbCacheValueWrapper;
 | 
			
		||||
import org.thingsboard.server.cache.TbJavaRedisSerializer;
 | 
			
		||||
import org.thingsboard.server.common.data.CacheConstants;
 | 
			
		||||
import org.thingsboard.server.common.data.kv.TsKvEntry;
 | 
			
		||||
 | 
			
		||||
import java.io.Serializable;
 | 
			
		||||
import java.util.Arrays;
 | 
			
		||||
import java.util.Collection;
 | 
			
		||||
import java.util.List;
 | 
			
		||||
import java.util.Set;
 | 
			
		||||
 | 
			
		||||
@ConditionalOnProperty(prefix = "cache", value = "type", havingValue = "redis")
 | 
			
		||||
@Service("TsLatestCache")
 | 
			
		||||
@Slf4j
 | 
			
		||||
public class TsLatestRedisCache<K extends Serializable, V extends Serializable> extends RedisTbTransactionalCache<TsLatestCacheKey, TsKvEntry> {
 | 
			
		||||
 | 
			
		||||
    static final byte[] UPSERT_TS_LATEST_LUA_SCRIPT = StringRedisSerializer.UTF_8.serialize("" +
 | 
			
		||||
            "redis.call('ZREMRANGEBYSCORE', KEYS[1], ARGV[1], ARGV[1]); " +
 | 
			
		||||
            "redis.call('ZADD', KEYS[1], ARGV[1], ARGV[2]); " +
 | 
			
		||||
            "local current_size = redis.call('ZCARD', KEYS[1]); " +
 | 
			
		||||
            "if current_size > 1 then" +
 | 
			
		||||
            "  redis.call('ZREMRANGEBYRANK', KEYS[1], 0, -2) " +
 | 
			
		||||
            "end;");
 | 
			
		||||
    static final byte[] UPSERT_TS_LATEST_SHA = StringRedisSerializer.UTF_8.serialize("24e226c3ea34e3e850113e8eb1f3cd2b88171988");
 | 
			
		||||
 | 
			
		||||
    public TsLatestRedisCache(TBRedisCacheConfiguration configuration, CacheSpecsMap cacheSpecsMap, RedisConnectionFactory connectionFactory) {
 | 
			
		||||
        super(CacheConstants.TS_LATEST_CACHE, cacheSpecsMap, connectionFactory, configuration, new TbJavaRedisSerializer<>());
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    @PostConstruct
 | 
			
		||||
    public void init() {
 | 
			
		||||
        try (var connection = getConnection(UPSERT_TS_LATEST_SHA)) {
 | 
			
		||||
            log.debug("Loading LUA with expected SHA[{}], connection [{}]", new String(UPSERT_TS_LATEST_SHA), connection.getNativeConnection());
 | 
			
		||||
            String sha = connection.scriptingCommands().scriptLoad(UPSERT_TS_LATEST_LUA_SCRIPT);
 | 
			
		||||
            if (!Arrays.equals(UPSERT_TS_LATEST_SHA, StringRedisSerializer.UTF_8.serialize(sha))) {
 | 
			
		||||
                log.error("SHA for UPSERT_TS_LATEST_LUA_SCRIPT wrong! Expected [{}], but actual [{}], connection [{}]", new String(UPSERT_TS_LATEST_SHA), sha, connection.getNativeConnection());
 | 
			
		||||
            }
 | 
			
		||||
        } catch (Throwable t) {
 | 
			
		||||
            log.error("Error on Redis TS Latest cache init", t);
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    @Override
 | 
			
		||||
    public TbCacheValueWrapper<TsKvEntry> get(TsLatestCacheKey key) {
 | 
			
		||||
        log.debug("get [{}]", key);
 | 
			
		||||
        return super.get(key);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    @Override
 | 
			
		||||
    protected byte[] doGet(RedisConnection connection, byte[] rawKey) {
 | 
			
		||||
        log.trace("doGet [{}][{}]", connection, rawKey);
 | 
			
		||||
        Set<byte[]> values = connection.commands().zRange(rawKey, -1, -1);
 | 
			
		||||
        return values == null ? null : values.stream().findFirst().orElse(null);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    @Override
 | 
			
		||||
    public void put(TsLatestCacheKey key, TsKvEntry value) {
 | 
			
		||||
        log.trace("put [{}][{}]", key, value);
 | 
			
		||||
        final byte[] rawKey = getRawKey(key);
 | 
			
		||||
        try (var connection = getConnection(rawKey)) {
 | 
			
		||||
            byte[] rawValue = getRawValue(value);
 | 
			
		||||
            byte[] ts = StringRedisSerializer.UTF_8.serialize(String.valueOf(value.toTsValue().getTs()));
 | 
			
		||||
            try {
 | 
			
		||||
                connection.scriptingCommands().evalSha(UPSERT_TS_LATEST_SHA, ReturnType.VALUE, 1, rawKey, ts, rawValue);
 | 
			
		||||
            } catch (InvalidDataAccessApiUsageException e) {
 | 
			
		||||
                log.debug("loading LUA [{}]", connection.getNativeConnection());
 | 
			
		||||
                String sha = connection.scriptingCommands().scriptLoad(UPSERT_TS_LATEST_LUA_SCRIPT);
 | 
			
		||||
                if (!Arrays.equals(UPSERT_TS_LATEST_SHA, StringRedisSerializer.UTF_8.serialize(sha))) {
 | 
			
		||||
                    log.error("SHA for UPSERT_TS_LATEST_LUA_SCRIPT wrong! Expected [{}], but actual [{}]", new String(UPSERT_TS_LATEST_SHA), sha);
 | 
			
		||||
                }
 | 
			
		||||
                try {
 | 
			
		||||
                    connection.scriptingCommands().evalSha(UPSERT_TS_LATEST_SHA, ReturnType.VALUE, 1, rawKey, ts, rawValue);
 | 
			
		||||
                } catch (InvalidDataAccessApiUsageException ignored) {
 | 
			
		||||
                    log.debug("Slowly executing eval instead of fast evalsha");
 | 
			
		||||
                    connection.scriptingCommands().eval(UPSERT_TS_LATEST_LUA_SCRIPT, ReturnType.VALUE, 1, rawKey, ts, rawValue);
 | 
			
		||||
                }
 | 
			
		||||
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    @Override
 | 
			
		||||
    public void evict(TsLatestCacheKey key) {
 | 
			
		||||
        log.trace("evict [{}]", key);
 | 
			
		||||
        final byte[] rawKey = getRawKey(key);
 | 
			
		||||
        try (var connection = getConnection(rawKey)) {
 | 
			
		||||
            connection.keyCommands().del(rawKey);
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    @Override
 | 
			
		||||
    public void putIfAbsent(TsLatestCacheKey key, TsKvEntry value) {
 | 
			
		||||
        log.trace("putIfAbsent [{}][{}]", key, value);
 | 
			
		||||
        throw new NotImplementedException("putIfAbsent is not supported by TsLatestRedisCache");
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    @Override
 | 
			
		||||
    public void evict(Collection<TsLatestCacheKey> keys) {
 | 
			
		||||
        throw new NotImplementedException("evict by many keys is not supported by TsLatestRedisCache");
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    @Override
 | 
			
		||||
    public void evictOrPut(TsLatestCacheKey key, TsKvEntry value) {
 | 
			
		||||
        throw new NotImplementedException("evictOrPut is not supported by TsLatestRedisCache");
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    @Override
 | 
			
		||||
    public TbCacheTransaction<TsLatestCacheKey, TsKvEntry> newTransactionForKey(TsLatestCacheKey key) {
 | 
			
		||||
        throw new NotImplementedException("newTransactionForKey is not supported by TsLatestRedisCache");
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    @Override
 | 
			
		||||
    public TbCacheTransaction<TsLatestCacheKey, TsKvEntry> newTransactionForKeys(List<TsLatestCacheKey> keys) {
 | 
			
		||||
        throw new NotImplementedException("newTransactionForKeys is not supported by TsLatestRedisCache");
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
}
 | 
			
		||||
@ -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);
 | 
			
		||||
        }
 | 
			
		||||
    };
 | 
			
		||||
 | 
			
		||||
}
 | 
			
		||||
@ -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 {
 | 
			
		||||
 | 
			
		||||
}
 | 
			
		||||
@ -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();
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
}
 | 
			
		||||
@ -17,6 +17,7 @@ package org.thingsboard.server.dao.service.timeseries;
 | 
			
		||||
 | 
			
		||||
import com.datastax.oss.driver.api.core.uuid.Uuids;
 | 
			
		||||
import lombok.extern.slf4j.Slf4j;
 | 
			
		||||
import org.assertj.core.data.Offset;
 | 
			
		||||
import org.junit.After;
 | 
			
		||||
import org.junit.Assert;
 | 
			
		||||
import org.junit.Before;
 | 
			
		||||
@ -32,6 +33,7 @@ import org.thingsboard.server.common.data.kv.BaseDeleteTsKvQuery;
 | 
			
		||||
import org.thingsboard.server.common.data.kv.BaseReadTsKvQuery;
 | 
			
		||||
import org.thingsboard.server.common.data.kv.BasicTsKvEntry;
 | 
			
		||||
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.JsonDataEntry;
 | 
			
		||||
import org.thingsboard.server.common.data.kv.KvEntry;
 | 
			
		||||
@ -50,15 +52,14 @@ import java.util.Arrays;
 | 
			
		||||
import java.util.Collections;
 | 
			
		||||
import java.util.Comparator;
 | 
			
		||||
import java.util.List;
 | 
			
		||||
import java.util.Optional;
 | 
			
		||||
import java.util.concurrent.ExecutionException;
 | 
			
		||||
import java.util.concurrent.TimeUnit;
 | 
			
		||||
import java.util.concurrent.TimeoutException;
 | 
			
		||||
 | 
			
		||||
import static org.assertj.core.api.Assertions.assertThat;
 | 
			
		||||
import static org.junit.Assert.assertEquals;
 | 
			
		||||
import static org.junit.Assert.assertFalse;
 | 
			
		||||
import static org.junit.Assert.assertNotNull;
 | 
			
		||||
import static org.junit.Assert.assertTrue;
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * @author Andrew Shvayka
 | 
			
		||||
@ -89,6 +90,7 @@ public abstract class BaseTimeseriesServiceTest extends AbstractServiceTest {
 | 
			
		||||
    KvEntry booleanKvEntry = new BooleanDataEntry(BOOLEAN_KEY, Boolean.TRUE);
 | 
			
		||||
 | 
			
		||||
    protected TenantId tenantId;
 | 
			
		||||
    DeviceId deviceId = new DeviceId(Uuids.timeBased());
 | 
			
		||||
 | 
			
		||||
    @Before
 | 
			
		||||
    public void before() {
 | 
			
		||||
@ -106,8 +108,6 @@ public abstract class BaseTimeseriesServiceTest extends AbstractServiceTest {
 | 
			
		||||
 | 
			
		||||
    @Test
 | 
			
		||||
    public void testFindAllLatest() throws Exception {
 | 
			
		||||
        DeviceId deviceId = new DeviceId(Uuids.timeBased());
 | 
			
		||||
 | 
			
		||||
        saveEntries(deviceId, TS - 2);
 | 
			
		||||
        saveEntries(deviceId, TS - 1);
 | 
			
		||||
        saveEntries(deviceId, TS);
 | 
			
		||||
@ -150,8 +150,6 @@ public abstract class BaseTimeseriesServiceTest extends AbstractServiceTest {
 | 
			
		||||
 | 
			
		||||
    @Test
 | 
			
		||||
    public void testFindLatest() throws Exception {
 | 
			
		||||
        DeviceId deviceId = new DeviceId(Uuids.timeBased());
 | 
			
		||||
 | 
			
		||||
        saveEntries(deviceId, TS - 2);
 | 
			
		||||
        saveEntries(deviceId, TS - 1);
 | 
			
		||||
        saveEntries(deviceId, TS);
 | 
			
		||||
@ -162,9 +160,71 @@ public abstract class BaseTimeseriesServiceTest extends AbstractServiceTest {
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    @Test
 | 
			
		||||
    public void testFindLatestWithoutLatestUpdate() throws Exception {
 | 
			
		||||
        DeviceId deviceId = new DeviceId(Uuids.timeBased());
 | 
			
		||||
    public void testFindLatestOpt_givenSaveWithHistoricalNonOrderedTS() throws Exception {
 | 
			
		||||
        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 - 1);
 | 
			
		||||
        saveEntriesWithoutLatest(deviceId, TS);
 | 
			
		||||
@ -176,8 +236,6 @@ public abstract class BaseTimeseriesServiceTest extends AbstractServiceTest {
 | 
			
		||||
 | 
			
		||||
    @Test
 | 
			
		||||
    public void testFindByQueryAscOrder() throws Exception {
 | 
			
		||||
        DeviceId deviceId = new DeviceId(Uuids.timeBased());
 | 
			
		||||
 | 
			
		||||
        saveEntries(deviceId, TS - 3);
 | 
			
		||||
        saveEntries(deviceId, TS - 2);
 | 
			
		||||
        saveEntries(deviceId, TS - 1);
 | 
			
		||||
@ -202,7 +260,6 @@ public abstract class BaseTimeseriesServiceTest extends AbstractServiceTest {
 | 
			
		||||
 | 
			
		||||
    @Test
 | 
			
		||||
    public void testFindByQuery_whenPeriodEqualsOneMilisecondPeriod() throws Exception {
 | 
			
		||||
        DeviceId deviceId = new DeviceId(Uuids.timeBased());
 | 
			
		||||
        saveEntries(deviceId, TS - 1L);
 | 
			
		||||
        saveEntries(deviceId, TS);
 | 
			
		||||
        saveEntries(deviceId, TS + 1L);
 | 
			
		||||
@ -222,7 +279,6 @@ public abstract class BaseTimeseriesServiceTest extends AbstractServiceTest {
 | 
			
		||||
 | 
			
		||||
    @Test
 | 
			
		||||
    public void testFindByQuery_whenPeriodEqualsInterval() throws Exception {
 | 
			
		||||
        DeviceId deviceId = new DeviceId(Uuids.timeBased());
 | 
			
		||||
        saveEntries(deviceId, TS - 1L);
 | 
			
		||||
        for (long i = TS; i <= TS + 100L; i += 10L) {
 | 
			
		||||
            saveEntries(deviceId, i);
 | 
			
		||||
@ -244,7 +300,6 @@ public abstract class BaseTimeseriesServiceTest extends AbstractServiceTest {
 | 
			
		||||
 | 
			
		||||
    @Test
 | 
			
		||||
    public void testFindByQuery_whenPeriodHaveTwoIntervalWithEqualsLength() throws Exception {
 | 
			
		||||
        DeviceId deviceId = new DeviceId(Uuids.timeBased());
 | 
			
		||||
        saveEntries(deviceId, TS - 1L);
 | 
			
		||||
        for (long i = TS; i <= TS + 100000L; i += 10000L) {
 | 
			
		||||
            saveEntries(deviceId, i);
 | 
			
		||||
@ -268,7 +323,6 @@ public abstract class BaseTimeseriesServiceTest extends AbstractServiceTest {
 | 
			
		||||
 | 
			
		||||
    @Test
 | 
			
		||||
    public void testFindByQuery_whenPeriodHaveTwoInterval_whereSecondShorterThanFirst() throws Exception {
 | 
			
		||||
        DeviceId deviceId = new DeviceId(Uuids.timeBased());
 | 
			
		||||
        saveEntries(deviceId, TS - 1L);
 | 
			
		||||
        for (long i = TS; i <= TS + 80000L; i += 10000L) {
 | 
			
		||||
            saveEntries(deviceId, i);
 | 
			
		||||
@ -292,7 +346,6 @@ public abstract class BaseTimeseriesServiceTest extends AbstractServiceTest {
 | 
			
		||||
 | 
			
		||||
    @Test
 | 
			
		||||
    public void testFindByQuery_whenPeriodHaveTwoIntervalWithEqualsLength_whereNotAllEntriesInRange() throws Exception {
 | 
			
		||||
        DeviceId deviceId = new DeviceId(Uuids.timeBased());
 | 
			
		||||
        for (long i = TS - 1L; i <= TS + 100000L + 1L; i += 10000) {
 | 
			
		||||
            saveEntries(deviceId, i);
 | 
			
		||||
        }
 | 
			
		||||
@ -314,7 +367,6 @@ public abstract class BaseTimeseriesServiceTest extends AbstractServiceTest {
 | 
			
		||||
 | 
			
		||||
    @Test
 | 
			
		||||
    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) {
 | 
			
		||||
            saveEntries(deviceId, i);
 | 
			
		||||
        }
 | 
			
		||||
@ -336,8 +388,6 @@ public abstract class BaseTimeseriesServiceTest extends AbstractServiceTest {
 | 
			
		||||
 | 
			
		||||
    @Test
 | 
			
		||||
    public void testFindByQueryDescOrder() throws Exception {
 | 
			
		||||
        DeviceId deviceId = new DeviceId(Uuids.timeBased());
 | 
			
		||||
 | 
			
		||||
        saveEntries(deviceId, TS - 3);
 | 
			
		||||
        saveEntries(deviceId, TS - 2);
 | 
			
		||||
        saveEntries(deviceId, TS - 1);
 | 
			
		||||
@ -362,7 +412,6 @@ public abstract class BaseTimeseriesServiceTest extends AbstractServiceTest {
 | 
			
		||||
 | 
			
		||||
    @Test
 | 
			
		||||
    public void testFindAllByQueries_verifyQueryId() throws Exception {
 | 
			
		||||
        DeviceId deviceId = new DeviceId(Uuids.timeBased());
 | 
			
		||||
        saveEntries(deviceId, TS);
 | 
			
		||||
        saveEntries(deviceId, TS - 2);
 | 
			
		||||
        saveEntries(deviceId, TS - 10);
 | 
			
		||||
@ -373,7 +422,6 @@ public abstract class BaseTimeseriesServiceTest extends AbstractServiceTest {
 | 
			
		||||
 | 
			
		||||
    @Test
 | 
			
		||||
    public void testFindAllByQueries_verifyQueryId_forEntityView() throws Exception {
 | 
			
		||||
        DeviceId deviceId = new DeviceId(Uuids.timeBased());
 | 
			
		||||
        saveEntries(deviceId, TS);
 | 
			
		||||
        saveEntries(deviceId, TS - 2);
 | 
			
		||||
        saveEntries(deviceId, TS - 12);
 | 
			
		||||
@ -392,8 +440,6 @@ public abstract class BaseTimeseriesServiceTest extends AbstractServiceTest {
 | 
			
		||||
 | 
			
		||||
    @Test
 | 
			
		||||
    public void testDeleteDeviceTsDataWithOverwritingLatest() throws Exception {
 | 
			
		||||
        DeviceId deviceId = new DeviceId(Uuids.timeBased());
 | 
			
		||||
 | 
			
		||||
        saveEntries(deviceId, 10000);
 | 
			
		||||
        saveEntries(deviceId, 20000);
 | 
			
		||||
        saveEntries(deviceId, 30000);
 | 
			
		||||
@ -412,7 +458,6 @@ public abstract class BaseTimeseriesServiceTest extends AbstractServiceTest {
 | 
			
		||||
 | 
			
		||||
    @Test
 | 
			
		||||
    public void testFindDeviceTsData() throws Exception {
 | 
			
		||||
        DeviceId deviceId = new DeviceId(Uuids.timeBased());
 | 
			
		||||
        List<TsKvEntry> entries = new ArrayList<>();
 | 
			
		||||
 | 
			
		||||
        entries.add(save(deviceId, 5000, 100));
 | 
			
		||||
@ -563,7 +608,6 @@ public abstract class BaseTimeseriesServiceTest extends AbstractServiceTest {
 | 
			
		||||
 | 
			
		||||
    @Test
 | 
			
		||||
    public void testFindDeviceLongAndDoubleTsData() throws Exception {
 | 
			
		||||
        DeviceId deviceId = new DeviceId(Uuids.timeBased());
 | 
			
		||||
        List<TsKvEntry> entries = new ArrayList<>();
 | 
			
		||||
 | 
			
		||||
        entries.add(save(deviceId, 5000, 100));
 | 
			
		||||
@ -654,8 +698,6 @@ public abstract class BaseTimeseriesServiceTest extends AbstractServiceTest {
 | 
			
		||||
 | 
			
		||||
    @Test
 | 
			
		||||
    public void testSaveTs_RemoveTs_AndSaveTsAgain() throws Exception {
 | 
			
		||||
        DeviceId deviceId = new DeviceId(Uuids.timeBased());
 | 
			
		||||
 | 
			
		||||
        save(deviceId, 2000000L, 95);
 | 
			
		||||
        save(deviceId, 4000000L, 100);
 | 
			
		||||
        save(deviceId, 6000000L, 105);
 | 
			
		||||
@ -686,7 +728,6 @@ public abstract class BaseTimeseriesServiceTest extends AbstractServiceTest {
 | 
			
		||||
        BasicTsKvEntry jsonEntry = new BasicTsKvEntry(TimeUnit.MINUTES.toMillis(5), new JsonDataEntry("test", "{\"test\":\"testValue\"}"));
 | 
			
		||||
        List<TsKvEntry> timeseries = List.of(booleanEntry, stringEntry, longEntry, doubleEntry, jsonEntry);
 | 
			
		||||
 | 
			
		||||
        DeviceId deviceId = new DeviceId(Uuids.timeBased());
 | 
			
		||||
        for (TsKvEntry tsKvEntry : timeseries) {
 | 
			
		||||
            save(tenantId, deviceId, tsKvEntry);
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
@ -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.dao.timeseries;
 | 
			
		||||
 | 
			
		||||
import lombok.SneakyThrows;
 | 
			
		||||
import org.junit.jupiter.api.Test;
 | 
			
		||||
 | 
			
		||||
import java.security.MessageDigest;
 | 
			
		||||
 | 
			
		||||
import static org.assertj.core.api.Assertions.assertThat;
 | 
			
		||||
 | 
			
		||||
class TsLatestRedisCacheTest {
 | 
			
		||||
 | 
			
		||||
    @Test
 | 
			
		||||
    void testUpsertTsLatestLUAScriptHash() {
 | 
			
		||||
        assertThat(getSHA1(TsLatestRedisCache.UPSERT_TS_LATEST_LUA_SCRIPT)).isEqualTo(new String(TsLatestRedisCache.UPSERT_TS_LATEST_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();
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
}
 | 
			
		||||
@ -10,6 +10,7 @@ audit-log.sink.type=none
 | 
			
		||||
#cache.type=caffeine # will be injected redis by RedisContainer or will be default (caffeine)
 | 
			
		||||
cache.maximumPoolSize=16
 | 
			
		||||
cache.attributes.enabled=true
 | 
			
		||||
cache.ts_latest.enabled=true
 | 
			
		||||
 | 
			
		||||
cache.specs.relations.timeToLiveInMinutes=1440
 | 
			
		||||
cache.specs.relations.maxSize=100000
 | 
			
		||||
@ -59,6 +60,9 @@ cache.specs.assetProfiles.maxSize=100000
 | 
			
		||||
cache.specs.attributes.timeToLiveInMinutes=1440
 | 
			
		||||
cache.specs.attributes.maxSize=100000
 | 
			
		||||
 | 
			
		||||
cache.specs.tsLatest.timeToLiveInMinutes=1440
 | 
			
		||||
cache.specs.tsLatest.maxSize=100000
 | 
			
		||||
 | 
			
		||||
cache.specs.tokensOutdatageTime.timeToLiveInMinutes=1440
 | 
			
		||||
cache.specs.tokensOutdatageTime.maxSize=100000
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@ -9,6 +9,11 @@
 | 
			
		||||
 | 
			
		||||
    <logger name="org.thingsboard.server.dao" level="WARN"/>
 | 
			
		||||
    <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 -->
 | 
			
		||||
    <!-- <logger name="org.hibernate.SQL" level="DEBUG"/> -->
 | 
			
		||||
							
								
								
									
										12
									
								
								pom.xml
									
									
									
									
									
								
							
							
						
						
									
										12
									
								
								pom.xml
									
									
									
									
									
								
							@ -2162,6 +2162,18 @@
 | 
			
		||||
                <version>${testcontainers-junit4-mock.version}</version>
 | 
			
		||||
                <scope>test</scope>
 | 
			
		||||
            </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>
 | 
			
		||||
                <groupId>org.zeroturnaround</groupId>
 | 
			
		||||
                <artifactId>zt-exec</artifactId>
 | 
			
		||||
 | 
			
		||||
		Loading…
	
	
			
			x
			
			
		
	
		Reference in New Issue
	
	Block a user