diff --git a/common/cache/src/main/java/org/thingsboard/server/cache/CaffeineCacheConfiguration.java b/common/cache/src/main/java/org/thingsboard/server/cache/CaffeineCacheConfiguration.java index 373b730d41..ef8ef7d8b9 100644 --- a/common/cache/src/main/java/org/thingsboard/server/cache/CaffeineCacheConfiguration.java +++ b/common/cache/src/main/java/org/thingsboard/server/cache/CaffeineCacheConfiguration.java @@ -21,6 +21,8 @@ import com.github.benmanes.caffeine.cache.Ticker; import com.github.benmanes.caffeine.cache.Weigher; import lombok.Data; import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.beans.factory.annotation.Value; import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; import org.springframework.boot.context.properties.ConfigurationProperties; import org.springframework.cache.CacheManager; @@ -46,6 +48,9 @@ import java.util.stream.Collectors; @Slf4j public class CaffeineCacheConfiguration { + @Value("${cache.type}") + private String test; + private Map specs; diff --git a/common/cache/src/main/java/org/thingsboard/server/cache/CaffeineCacheTransactionStorage.java b/common/cache/src/main/java/org/thingsboard/server/cache/CaffeineCacheTransactionStorage.java new file mode 100644 index 0000000000..8a9a9850fc --- /dev/null +++ b/common/cache/src/main/java/org/thingsboard/server/cache/CaffeineCacheTransactionStorage.java @@ -0,0 +1,118 @@ +package org.thingsboard.server.cache; + +import lombok.RequiredArgsConstructor; + +import java.io.Serializable; +import java.util.HashMap; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.UUID; +import java.util.concurrent.locks.Lock; +import java.util.concurrent.locks.ReentrantLock; + +@RequiredArgsConstructor +class CaffeineCacheTransactionStorage { + + private final String cacheName; + private final CaffeineTbTransactionalCacheManager cache; + private final Lock lock = new ReentrantLock(); + private final Map> objectTransactions = new HashMap<>(); + private final Map transactions = new HashMap<>(); + + + TbCacheTransaction newTransaction(List keys) { + lock.lock(); + try { + var transaction = new CaffeineTbCacheTransaction(this, keys); + var transactionId = transaction.getId(); + for (K key : keys) { + objectTransactions.computeIfAbsent(key, k -> new HashSet<>()).add(transactionId); + } + transactions.put(transactionId, transaction); + return transaction; + } finally { + lock.unlock(); + } + } + + void putIfAbsent(K key, V value) { + lock.lock(); + try { + failAllTransactionsByKey(key); + cache.doPutIfAbsent(cacheName, key, value); + } finally { + lock.unlock(); + } + } + + public void evict(K key) { + lock.lock(); + try { + failAllTransactionsByKey(key); + cache.doEvict(cacheName, key); + } finally { + lock.unlock(); + } + } + + public boolean commit(UUID trId, Map pendingPuts) { + lock.lock(); + try { + var tr = transactions.get(trId); + var success = !tr.isFailed(); + if (success) { + for (Object key : tr.getKeys()) { + Set otherTransactions = objectTransactions.get(key); + if (otherTransactions != null) { + for (UUID otherTrId : otherTransactions) { + if (trId == null || !trId.equals(otherTrId)) { + transactions.get(otherTrId).setFailed(true); + } + } + } + } + pendingPuts.forEach((k, v) -> cache.doPutIfAbsent(cacheName, k, v)); + } + removeTransaction(trId); + return success; + } finally { + lock.unlock(); + } + } + + void rollback(UUID id) { + lock.lock(); + try { + removeTransaction(id); + } finally { + lock.unlock(); + } + } + + private void removeTransaction(UUID id) { + CaffeineTbCacheTransaction transaction = transactions.remove(id); + if (transaction != null) { + for (var key : transaction.getKeys()) { + Set transactions = objectTransactions.get(key); + if (transactions != null) { + transactions.remove(id); + if (transactions.isEmpty()) { + objectTransactions.remove(key); + } + } + } + } + } + + private void failAllTransactionsByKey(K key) { + Set transactionsIds = objectTransactions.get(key); + if (transactionsIds != null) { + for (UUID otherTrId : transactionsIds) { + transactions.get(otherTrId).setFailed(true); + } + } + } + +} diff --git a/common/cache/src/main/java/org/thingsboard/server/cache/CaffeineTbCacheTransaction.java b/common/cache/src/main/java/org/thingsboard/server/cache/CaffeineTbCacheTransaction.java new file mode 100644 index 0000000000..202d9c4007 --- /dev/null +++ b/common/cache/src/main/java/org/thingsboard/server/cache/CaffeineTbCacheTransaction.java @@ -0,0 +1,61 @@ +package org.thingsboard.server.cache; + +import com.google.common.util.concurrent.FutureCallback; +import com.google.common.util.concurrent.Futures; +import com.google.common.util.concurrent.ListenableFuture; +import lombok.Getter; +import lombok.RequiredArgsConstructor; +import lombok.Setter; +import lombok.extern.slf4j.Slf4j; +import org.checkerframework.checker.nullness.qual.Nullable; + +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; +import java.util.UUID; +import java.util.concurrent.Executor; + +@Slf4j +@RequiredArgsConstructor +public class CaffeineTbCacheTransaction implements TbCacheTransaction { + @Getter + private final UUID id = UUID.randomUUID(); + private final CaffeineCacheTransactionStorage cache; + @Getter + private final List keys; + @Getter @Setter + private boolean failed; + + private final Map pendingPuts = new LinkedHashMap<>(); + + @Override + public void putIfAbsent(K key, V value) { + pendingPuts.put(key, value); + } + + @Override + public boolean commit() { + return cache.commit(id, pendingPuts); + } + + @Override + public void rollback() { + cache.rollback(id); + } + + @Override + public void rollBackOnFailure(ListenableFuture future, Executor executor) { + Futures.addCallback(future, new FutureCallback() { + @Override + public void onSuccess(@Nullable T result) { + } + + @Override + public void onFailure(Throwable t) { + log.trace("[{}] Rollback transaction due to error", id, t); + rollback(); + } + }, executor); + } + +} diff --git a/common/cache/src/main/java/org/thingsboard/server/cache/CaffeineTbTransactionalCacheManager.java b/common/cache/src/main/java/org/thingsboard/server/cache/CaffeineTbTransactionalCacheManager.java new file mode 100644 index 0000000000..e9dba40cf1 --- /dev/null +++ b/common/cache/src/main/java/org/thingsboard/server/cache/CaffeineTbTransactionalCacheManager.java @@ -0,0 +1,59 @@ +package org.thingsboard.server.cache; + +import lombok.RequiredArgsConstructor; +import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; +import org.springframework.cache.Cache; +import org.springframework.cache.CacheManager; +import org.springframework.stereotype.Service; + +import java.io.Serializable; +import java.util.Collections; +import java.util.List; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.ConcurrentMap; + +@ConditionalOnProperty(prefix = "cache", value = "type", havingValue = "caffeine", matchIfMissing = true) +@Service +@RequiredArgsConstructor +public class CaffeineTbTransactionalCacheManager implements TbTransactionalCache { + + private final CacheManager cacheManager; + private final ConcurrentMap caches = new ConcurrentHashMap<>(); + + @Override + public Cache.ValueWrapper get(String cacheName, K key) { + return cacheManager.getCache(cacheName).get(key); + } + + @Override + public void putIfAbsent(String cacheName, K key, V value) { + getCache(cacheName).putIfAbsent(key, value); + } + + @Override + public void evict(String cacheName, K key) { + getCache(cacheName).evict(key); + } + + @Override + public TbCacheTransaction newTransactionForKey(String cacheName, K key) { + return getCache(cacheName).newTransaction(Collections.singletonList(key)); + } + + @Override + public TbCacheTransaction newTransactionForKeys(String cacheName, List keys) { + return getCache(cacheName).newTransaction(keys); + } + + private CaffeineCacheTransactionStorage getCache(String cacheName) { + return caches.computeIfAbsent(cacheName, cn -> new CaffeineCacheTransactionStorage(cacheName, this)); + } + + void doPutIfAbsent(String cacheName, Object key, Object value) { + cacheManager.getCache(cacheName).putIfAbsent(key, value); + } + + void doEvict(String cacheName, K key) { + cacheManager.getCache(cacheName).evict(key); + } +} diff --git a/common/cache/src/main/java/org/thingsboard/server/cache/RedisTbTransactionalCacheManager.java b/common/cache/src/main/java/org/thingsboard/server/cache/RedisTbTransactionalCacheManager.java new file mode 100644 index 0000000000..a4df25aeac --- /dev/null +++ b/common/cache/src/main/java/org/thingsboard/server/cache/RedisTbTransactionalCacheManager.java @@ -0,0 +1,42 @@ +package org.thingsboard.server.cache; + +import lombok.RequiredArgsConstructor; +import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; +import org.springframework.cache.Cache; +import org.springframework.cache.CacheManager; +import org.springframework.stereotype.Service; + +import java.io.Serializable; +import java.util.List; + +@ConditionalOnProperty(prefix = "cache", value = "type", havingValue = "redis") +@Service +@RequiredArgsConstructor +public class RedisTbTransactionalCacheManager implements TbTransactionalCache { + + private final CacheManager cacheManager; + + @Override + public Cache.ValueWrapper get(String cacheName, K key) { + return cacheManager.getCache(cacheName).get(key); + } + + @Override + public void putIfAbsent(String cacheName, K key, V value) { + } + + @Override + public void evict(String cacheName, K key) { + } + + @Override + public TbCacheTransaction newTransactionForKey(String cacheName, K key) { + return null; + } + + @Override + public TbCacheTransaction newTransactionForKeys(String cacheName, List keys) { + return null; + } + +} diff --git a/common/cache/src/main/java/org/thingsboard/server/cache/TBRedisCacheConfiguration.java b/common/cache/src/main/java/org/thingsboard/server/cache/TBRedisCacheConfiguration.java index ebf031c562..32127a5a39 100644 --- a/common/cache/src/main/java/org/thingsboard/server/cache/TBRedisCacheConfiguration.java +++ b/common/cache/src/main/java/org/thingsboard/server/cache/TBRedisCacheConfiguration.java @@ -36,42 +36,42 @@ import redis.clients.jedis.JedisPoolConfig; import java.time.Duration; @Configuration -@ConditionalOnProperty(prefix = "cache", value = "type", havingValue = "redis", matchIfMissing = false) +@ConditionalOnProperty(prefix = "cache", value = "type", havingValue = "redis") @EnableCaching @Data public abstract class TBRedisCacheConfiguration { - @Value("${redis.pool_config.maxTotal}") + @Value("${redis.pool_config.maxTotal:128}") private int maxTotal; - @Value("${redis.pool_config.maxIdle}") + @Value("${redis.pool_config.maxIdle:128}") private int maxIdle; - @Value("${redis.pool_config.minIdle}") + @Value("${redis.pool_config.minIdle:16}") private int minIdle; - @Value("${redis.pool_config.testOnBorrow}") + @Value("${redis.pool_config.testOnBorrow:true}") private boolean testOnBorrow; - @Value("${redis.pool_config.testOnReturn}") + @Value("${redis.pool_config.testOnReturn:true}") private boolean testOnReturn; - @Value("${redis.pool_config.testWhileIdle}") + @Value("${redis.pool_config.testWhileIdle:true}") private boolean testWhileIdle; - @Value("${redis.pool_config.minEvictableMs}") + @Value("${redis.pool_config.minEvictableMs:60000}") private long minEvictableMs; - @Value("${redis.pool_config.evictionRunsMs}") + @Value("${redis.pool_config.evictionRunsMs:30000}") private long evictionRunsMs; - @Value("${redis.pool_config.maxWaitMills}") + @Value("${redis.pool_config.maxWaitMills:60000}") private long maxWaitMills; - @Value("${redis.pool_config.numberTestsPerEvictionRun}") + @Value("${redis.pool_config.numberTestsPerEvictionRun:3}") private int numberTestsPerEvictionRun; - @Value("${redis.pool_config.blockWhenExhausted}") + @Value("${redis.pool_config.blockWhenExhausted:true}") private boolean blockWhenExhausted; @Bean diff --git a/common/cache/src/main/java/org/thingsboard/server/cache/TBRedisClusterConfiguration.java b/common/cache/src/main/java/org/thingsboard/server/cache/TBRedisClusterConfiguration.java index 3538b6e0d7..4925842ad3 100644 --- a/common/cache/src/main/java/org/thingsboard/server/cache/TBRedisClusterConfiguration.java +++ b/common/cache/src/main/java/org/thingsboard/server/cache/TBRedisClusterConfiguration.java @@ -36,16 +36,16 @@ public class TBRedisClusterConfiguration extends TBRedisCacheConfiguration { private static final String COMMA = ","; private static final String COLON = ":"; - @Value("${redis.cluster.nodes}") + @Value("${redis.cluster.nodes:}") private String clusterNodes; - @Value("${redis.cluster.max-redirects}") + @Value("${redis.cluster.max-redirects:12}") private Integer maxRedirects; - @Value("${redis.cluster.useDefaultPoolConfig}") + @Value("${redis.cluster.useDefaultPoolConfig:true}") private boolean useDefaultPoolConfig; - @Value("${redis.password}") + @Value("${redis.password:}") private String password; public JedisConnectionFactory loadFactory() { diff --git a/common/cache/src/main/java/org/thingsboard/server/cache/TBRedisStandaloneConfiguration.java b/common/cache/src/main/java/org/thingsboard/server/cache/TBRedisStandaloneConfiguration.java index 6a5b6e15e6..482c96d0da 100644 --- a/common/cache/src/main/java/org/thingsboard/server/cache/TBRedisStandaloneConfiguration.java +++ b/common/cache/src/main/java/org/thingsboard/server/cache/TBRedisStandaloneConfiguration.java @@ -30,31 +30,31 @@ import java.time.Duration; @ConditionalOnProperty(prefix = "redis.connection", value = "type", havingValue = "standalone") public class TBRedisStandaloneConfiguration extends TBRedisCacheConfiguration { - @Value("${redis.standalone.host}") + @Value("${redis.standalone.host:localhost}") private String host; - @Value("${redis.standalone.port}") + @Value("${redis.standalone.port:6379}") private Integer port; - @Value("${redis.standalone.clientName}") + @Value("${redis.standalone.clientName:standalone}") private String clientName; - @Value("${redis.standalone.connectTimeout}") + @Value("${redis.standalone.connectTimeout:30000}") private Long connectTimeout; - @Value("${redis.standalone.readTimeout}") + @Value("${redis.standalone.readTimeout:60000}") private Long readTimeout; - @Value("${redis.standalone.useDefaultClientConfig}") + @Value("${redis.standalone.useDefaultClientConfig:true}") private boolean useDefaultClientConfig; - @Value("${redis.standalone.usePoolConfig}") + @Value("${redis.standalone.usePoolConfig:false}") private boolean usePoolConfig; - @Value("${redis.db}") + @Value("${redis.db:0}") private Integer db; - @Value("${redis.password}") + @Value("${redis.password:}") private String password; public JedisConnectionFactory loadFactory() { diff --git a/common/cache/src/main/java/org/thingsboard/server/cache/TbCacheTransaction.java b/common/cache/src/main/java/org/thingsboard/server/cache/TbCacheTransaction.java new file mode 100644 index 0000000000..22bbec4489 --- /dev/null +++ b/common/cache/src/main/java/org/thingsboard/server/cache/TbCacheTransaction.java @@ -0,0 +1,16 @@ +package org.thingsboard.server.cache; + +import com.google.common.util.concurrent.ListenableFuture; + +import java.util.concurrent.Executor; + +public interface TbCacheTransaction { + + void putIfAbsent(K key, V value); + + boolean commit(); + + void rollback(); + + void rollBackOnFailure(ListenableFuture result, Executor cacheExecutor); +} diff --git a/common/cache/src/main/java/org/thingsboard/server/cache/TbTransactionalCache.java b/common/cache/src/main/java/org/thingsboard/server/cache/TbTransactionalCache.java new file mode 100644 index 0000000000..ad370f3d18 --- /dev/null +++ b/common/cache/src/main/java/org/thingsboard/server/cache/TbTransactionalCache.java @@ -0,0 +1,20 @@ +package org.thingsboard.server.cache; + +import org.springframework.cache.Cache; + +import java.io.Serializable; +import java.util.List; + +public interface TbTransactionalCache { + + Cache.ValueWrapper get(String cacheName, K key); + + void putIfAbsent(String cacheName, K key, V value); + + void evict(String cacheName, K key); + + TbCacheTransaction newTransactionForKey(String cacheName, K key); + + TbCacheTransaction newTransactionForKeys(String cacheName, List keys); + +} diff --git a/dao/src/main/java/org/thingsboard/server/dao/CacheDaoConfig.java b/dao/src/main/java/org/thingsboard/server/dao/CacheDaoConfig.java new file mode 100644 index 0000000000..f23ae62165 --- /dev/null +++ b/dao/src/main/java/org/thingsboard/server/dao/CacheDaoConfig.java @@ -0,0 +1,36 @@ +/** + * Copyright © 2016-2022 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.boot.autoconfigure.domain.EntityScan; +import org.springframework.context.annotation.ComponentScan; +import org.springframework.context.annotation.Configuration; +import org.springframework.data.jpa.repository.config.EnableJpaRepositories; +import org.springframework.transaction.annotation.EnableTransactionManagement; +import org.thingsboard.server.dao.util.TbAutoConfiguration; + +/** + * @author Valerii Sosliuk + */ +@Configuration +@TbAutoConfiguration +@ComponentScan({"org.thingsboard.server.dao.sql", "org.thingsboard.server.dao.attributes"}) +@EnableJpaRepositories("org.thingsboard.server.dao.sql") +@EntityScan("org.thingsboard.server.dao.model.sql") +@EnableTransactionManagement +public class CacheDaoConfig { + +} diff --git a/dao/src/main/java/org/thingsboard/server/dao/JpaServiceDaoConfig.java b/dao/src/main/java/org/thingsboard/server/dao/JpaServiceDaoConfig.java new file mode 100644 index 0000000000..46d067fc4a --- /dev/null +++ b/dao/src/main/java/org/thingsboard/server/dao/JpaServiceDaoConfig.java @@ -0,0 +1,38 @@ +/** + * Copyright © 2016-2022 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.boot.autoconfigure.domain.EntityScan; +import org.springframework.cache.annotation.EnableCaching; +import org.springframework.context.annotation.ComponentScan; +import org.springframework.context.annotation.Configuration; +import org.springframework.data.jpa.repository.config.EnableJpaRepositories; +import org.springframework.transaction.annotation.EnableTransactionManagement; +import org.thingsboard.server.dao.util.TbAutoConfiguration; + +/** + * @author Valerii Sosliuk + */ +@Configuration +@EnableCaching +@TbAutoConfiguration +@ComponentScan({"org.thingsboard.server.dao.sql", "org.thingsboard.server.dao.attributes", "org.thingsboard.server.dao.cache", "org.thingsboard.server.cache"}) +@EnableJpaRepositories("org.thingsboard.server.dao.sql") +@EntityScan("org.thingsboard.server.dao.model.sql") +@EnableTransactionManagement +public class JpaServiceDaoConfig { + +} diff --git a/dao/src/main/java/org/thingsboard/server/dao/attributes/AttributesCacheWrapper.java b/dao/src/main/java/org/thingsboard/server/dao/attributes/AttributesCacheWrapper.java deleted file mode 100644 index 466cd4b0f1..0000000000 --- a/dao/src/main/java/org/thingsboard/server/dao/attributes/AttributesCacheWrapper.java +++ /dev/null @@ -1,15 +0,0 @@ -package org.thingsboard.server.dao.attributes; - -import org.springframework.cache.Cache; -import org.thingsboard.server.common.data.kv.AttributeKvEntry; - -public interface AttributesCacheWrapper { - - Cache.ValueWrapper get(AttributeCacheKey attributeCacheKey); - - void put(AttributeCacheKey attributeCacheKey, AttributeKvEntry attributeKvEntry); - - void putIfAbsent(AttributeCacheKey attributeCacheKey, AttributeKvEntry attributeKvEntry); - - void evict(AttributeCacheKey attributeCacheKey); -} diff --git a/dao/src/main/java/org/thingsboard/server/dao/attributes/CachedAttributesService.java b/dao/src/main/java/org/thingsboard/server/dao/attributes/CachedAttributesService.java index abe741d6d7..d9e172c113 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/attributes/CachedAttributesService.java +++ b/dao/src/main/java/org/thingsboard/server/dao/attributes/CachedAttributesService.java @@ -25,6 +25,7 @@ import org.springframework.cache.Cache; import org.springframework.context.annotation.Primary; import org.springframework.stereotype.Service; import org.springframework.util.StringUtils; +import org.thingsboard.server.common.data.CacheConstants; import org.thingsboard.server.common.data.EntityType; import org.thingsboard.server.common.data.id.DeviceProfileId; import org.thingsboard.server.common.data.id.EntityId; @@ -33,6 +34,8 @@ import org.thingsboard.server.common.data.kv.AttributeKvEntry; 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.cache.TbCacheTransaction; +import org.thingsboard.server.cache.TbTransactionalCache; import org.thingsboard.server.dao.service.Validator; import javax.annotation.PostConstruct; @@ -59,22 +62,22 @@ public class CachedAttributesService implements AttributesService { public static final String LOCAL_CACHE_TYPE = "caffeine"; private final AttributesDao attributesDao; - private final AttributesCacheWrapper cacheWrapper; private final CacheExecutorService cacheExecutorService; private final DefaultCounter hitCounter; private final DefaultCounter missCounter; + private final TbTransactionalCache cache; private Executor cacheExecutor; @Value("${cache.type}") private String cacheType; public CachedAttributesService(AttributesDao attributesDao, - AttributesCacheWrapper cacheWrapper, StatsFactory statsFactory, - CacheExecutorService cacheExecutorService) { + CacheExecutorService cacheExecutorService, + TbTransactionalCache cache) { this.attributesDao = attributesDao; - this.cacheWrapper = cacheWrapper; this.cacheExecutorService = cacheExecutorService; + this.cache = cache; this.hitCounter = statsFactory.createDefaultCounter(STATS_NAME, "result", "hit"); this.missCounter = statsFactory.createDefaultCounter(STATS_NAME, "result", "miss"); @@ -99,23 +102,26 @@ public class CachedAttributesService implements AttributesService { return cacheExecutorService; } + @Override public ListenableFuture> find(TenantId tenantId, EntityId entityId, String scope, String attributeKey) { validate(entityId, scope); Validator.validateString(attributeKey, "Incorrect attribute key " + attributeKey); AttributeCacheKey attributeCacheKey = new AttributeCacheKey(scope, entityId, attributeKey); - Cache.ValueWrapper cachedAttributeValue = cacheWrapper.get(attributeCacheKey); + Cache.ValueWrapper cachedAttributeValue = cache.get(CacheConstants.ATTRIBUTES_CACHE, attributeCacheKey); if (cachedAttributeValue != null) { hitCounter.increment(); AttributeKvEntry cachedAttributeKvEntry = (AttributeKvEntry) cachedAttributeValue.get(); return Futures.immediateFuture(Optional.ofNullable(cachedAttributeKvEntry)); } else { missCounter.increment(); + TbCacheTransaction cacheTransaction = cache.newTransactionForKey(CacheConstants.ATTRIBUTES_CACHE, attributeCacheKey); ListenableFuture> result = attributesDao.find(tenantId, entityId, scope, attributeKey); + cacheTransaction.rollBackOnFailure(result, cacheExecutor); return Futures.transform(result, foundAttrKvEntry -> { - // TODO: think if it's a good idea to store 'empty' attributes - cacheWrapper.putIfAbsent(attributeCacheKey, foundAttrKvEntry.orElse(null)); + cacheTransaction.putIfAbsent(attributeCacheKey, foundAttrKvEntry.orElse(null)); + cacheTransaction.commit(); return foundAttrKvEntry; }, cacheExecutor); } @@ -139,15 +145,31 @@ public class CachedAttributesService implements AttributesService { Set notFoundAttributeKeys = new HashSet<>(attributeKeys); notFoundAttributeKeys.removeAll(wrappedCachedAttributes.keySet()); + List notFoundKeys = notFoundAttributeKeys.stream().map(k -> new AttributeCacheKey(scope, entityId, k)).collect(Collectors.toList()); + + TbCacheTransaction cacheTransaction = cache.newTransactionForKeys(CacheConstants.ATTRIBUTES_CACHE, notFoundKeys); ListenableFuture> result = attributesDao.find(tenantId, entityId, scope, notFoundAttributeKeys); - return Futures.transform(result, foundInDbAttributes -> mergeDbAndCacheAttributes(entityId, scope, cachedAttributes, notFoundAttributeKeys, foundInDbAttributes), cacheExecutor); + return Futures.transform(result, foundInDbAttributes -> { + for (AttributeKvEntry foundInDbAttribute : foundInDbAttributes) { + 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 mergedAttributes = new ArrayList<>(cachedAttributes); + mergedAttributes.addAll(foundInDbAttributes); + cacheTransaction.commit(); + return mergedAttributes; + }, cacheExecutor); } private Map findCachedAttributes(EntityId entityId, String scope, Collection attributeKeys) { Map cachedAttributes = new HashMap<>(); for (String attributeKey : attributeKeys) { - Cache.ValueWrapper cachedAttributeValue = cacheWrapper.get(new AttributeCacheKey(scope, entityId, attributeKey)); + Cache.ValueWrapper cachedAttributeValue = cache.get(CacheConstants.ATTRIBUTES_CACHE, new AttributeCacheKey(scope, entityId, attributeKey)); if (cachedAttributeValue != null) { hitCounter.increment(); cachedAttributes.put(attributeKey, cachedAttributeValue); @@ -158,20 +180,6 @@ public class CachedAttributesService implements AttributesService { return cachedAttributes; } - private List mergeDbAndCacheAttributes(EntityId entityId, String scope, List cachedAttributes, Set notFoundAttributeKeys, List foundInDbAttributes) { - for (AttributeKvEntry foundInDbAttribute : foundInDbAttributes) { - AttributeCacheKey attributeCacheKey = new AttributeCacheKey(scope, entityId, foundInDbAttribute.getKey()); - cacheWrapper.putIfAbsent(attributeCacheKey, foundInDbAttribute); - notFoundAttributeKeys.remove(foundInDbAttribute.getKey()); - } -// for (String key : notFoundAttributeKeys) { -// cacheWrapper.putIfAbsent(new AttributeCacheKey(scope, entityId, key), null); -// } - List mergedAttributes = new ArrayList<>(cachedAttributes); - mergedAttributes.addAll(foundInDbAttributes); - return mergedAttributes; - } - @Override public ListenableFuture> findAll(TenantId tenantId, EntityId entityId, String scope) { validate(entityId, scope); @@ -197,7 +205,7 @@ public class CachedAttributesService implements AttributesService { for (var attribute : attributes) { ListenableFuture future = attributesDao.save(tenantId, entityId, scope, attribute); futures.add(Futures.transform(future, key -> { - cacheWrapper.evict(new AttributeCacheKey(scope, entityId, key)); + cache.evict(CacheConstants.ATTRIBUTES_CACHE, new AttributeCacheKey(scope, entityId, key)); return key; }, cacheExecutor)); } @@ -210,7 +218,7 @@ public class CachedAttributesService implements AttributesService { validate(entityId, scope); List> futures = attributesDao.removeAll(tenantId, entityId, scope, attributeKeys); return Futures.allAsList(futures.stream().map(future -> Futures.transform(future, key -> { - cacheWrapper.evict(new AttributeCacheKey(scope, entityId, key)); + cache.evict(CacheConstants.ATTRIBUTES_CACHE, new AttributeCacheKey(scope, entityId, key)); return key; }, cacheExecutor)).collect(Collectors.toList())); } diff --git a/dao/src/main/java/org/thingsboard/server/dao/attributes/DefaultAttributesCacheWrapper.java b/dao/src/main/java/org/thingsboard/server/dao/attributes/DefaultAttributesCacheWrapper.java deleted file mode 100644 index aa0222551a..0000000000 --- a/dao/src/main/java/org/thingsboard/server/dao/attributes/DefaultAttributesCacheWrapper.java +++ /dev/null @@ -1,63 +0,0 @@ -/** - * Copyright © 2016-2022 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.attributes; - -import lombok.extern.slf4j.Slf4j; -import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; -import org.springframework.cache.Cache; -import org.springframework.cache.CacheManager; -import org.springframework.context.annotation.Primary; -import org.springframework.stereotype.Service; -import org.thingsboard.server.common.data.kv.AttributeKvEntry; - -import static org.thingsboard.server.common.data.CacheConstants.ATTRIBUTES_CACHE; - -@Service -@ConditionalOnProperty(prefix = "cache.attributes", value = "enabled", havingValue = "true") -@Primary -@Slf4j -public class DefaultAttributesCacheWrapper implements AttributesCacheWrapper { - private final Cache attributesCache; - - public DefaultAttributesCacheWrapper(CacheManager cacheManager) { - this.attributesCache = cacheManager.getCache(ATTRIBUTES_CACHE); - } - - @Override - public Cache.ValueWrapper get(AttributeCacheKey attributeCacheKey) { - var result = attributesCache.get(attributeCacheKey); - log.warn("[{}] Get = {}", attributeCacheKey, result); - return result; - } - - @Override - public void put(AttributeCacheKey attributeCacheKey, AttributeKvEntry attributeKvEntry) { - log.warn("[{}] Put = {}", attributeCacheKey, attributeKvEntry); - attributesCache.put(attributeCacheKey, attributeKvEntry); - } - - @Override - public void putIfAbsent(AttributeCacheKey attributeCacheKey, AttributeKvEntry attributeKvEntry) { - var result = attributesCache.putIfAbsent(attributeCacheKey, attributeKvEntry); - log.warn("[{}] Put if absent = {}, result = {}", attributeCacheKey, attributeKvEntry, result); - } - - @Override - public void evict(AttributeCacheKey attributeCacheKey) { - log.warn("[{}] Evict", attributeCacheKey); - attributesCache.evict(attributeCacheKey); - } -} diff --git a/dao/src/main/java/org/thingsboard/server/dao/relation/BaseRelationService.java b/dao/src/main/java/org/thingsboard/server/dao/relation/BaseRelationService.java index e62669586e..e2e1ba7d2b 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/relation/BaseRelationService.java +++ b/dao/src/main/java/org/thingsboard/server/dao/relation/BaseRelationService.java @@ -320,7 +320,7 @@ public class BaseRelationService implements RelationService { cache.evict(toTypeAndTypeGroup); } - @Cacheable(cacheNames = RELATIONS_CACHE, key = "{#from, #typeGroup, 'FROM'}") +// @Cacheable(cacheNames = RELATIONS_CACHE, key = "{#from, #typeGroup, 'FROM'}") @Transactional(propagation = Propagation.SUPPORTS) @Override public List findByFrom(TenantId tenantId, EntityId from, RelationTypeGroup typeGroup) { @@ -381,7 +381,7 @@ public class BaseRelationService implements RelationService { }, MoreExecutors.directExecutor()); } - @Cacheable(cacheNames = RELATIONS_CACHE, key = "{#from, #relationType, #typeGroup, 'FROM'}") +// @Cacheable(cacheNames = RELATIONS_CACHE, key = "{#from, #relationType, #typeGroup, 'FROM'}") @Override public List findByFromAndType(TenantId tenantId, EntityId from, String relationType, RelationTypeGroup typeGroup) { try { diff --git a/dao/src/test/java/org/thingsboard/server/dao/AbstractDaoServiceTest.java b/dao/src/test/java/org/thingsboard/server/dao/AbstractDaoServiceTest.java new file mode 100644 index 0000000000..25c423eddb --- /dev/null +++ b/dao/src/test/java/org/thingsboard/server/dao/AbstractDaoServiceTest.java @@ -0,0 +1,40 @@ +/** + * Copyright © 2016-2022 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.runner.RunWith; +import org.mockito.Answers; +import org.springframework.boot.test.mock.mockito.MockBean; +import org.springframework.test.context.ContextConfiguration; +import org.springframework.test.context.TestExecutionListeners; +import org.springframework.test.context.junit4.SpringRunner; +import org.springframework.test.context.support.DependencyInjectionTestExecutionListener; +import org.springframework.test.context.support.DirtiesContextTestExecutionListener; +import org.thingsboard.server.common.stats.StatsFactory; +import org.thingsboard.server.dao.service.DaoSqlTest; + +@RunWith(SpringRunner.class) +@ContextConfiguration(classes = {JpaServiceDaoConfig.class, PsqlTsDaoConfig.class, PsqlTsLatestDaoConfig.class, SqlTimeseriesDaoConfig.class}) +@DaoSqlTest +@TestExecutionListeners({ + DependencyInjectionTestExecutionListener.class, + DirtiesContextTestExecutionListener.class}) +public abstract class AbstractDaoServiceTest { + + @MockBean(answer = Answers.RETURNS_MOCKS) + StatsFactory statsFactory; + +} diff --git a/dao/src/test/java/org/thingsboard/server/dao/RedisTestSuite.java b/dao/src/test/java/org/thingsboard/server/dao/RedisTestSuite.java new file mode 100644 index 0000000000..21838ebc2c --- /dev/null +++ b/dao/src/test/java/org/thingsboard/server/dao/RedisTestSuite.java @@ -0,0 +1,50 @@ +/** + * Copyright © 2016-2022 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.ClassRule; +import org.junit.extensions.cpsuite.ClasspathSuite; +import org.junit.extensions.cpsuite.ClasspathSuite.ClassnameFilters; +import org.junit.runner.RunWith; +import org.springframework.context.ApplicationContextInitializer; +import org.springframework.context.ConfigurableApplicationContext; +import org.springframework.test.context.ContextConfiguration; +import org.springframework.test.context.support.TestPropertySourceUtils; +import org.testcontainers.containers.GenericContainer; + +@ContextConfiguration(initializers = RedisTestSuite.class) +@RunWith(ClasspathSuite.class) +@ClassnameFilters({ + "org.thingsboard.server.dao.sql.attributes.*ServiceTest", +}) +public class RedisTestSuite implements ApplicationContextInitializer { + + @ClassRule + public static GenericContainer redis = new GenericContainer("redis:4.0").withExposedPorts(6379); + + @Override + public void initialize(ConfigurableApplicationContext applicationContext) { + TestPropertySourceUtils.addInlinedPropertiesToEnvironment( + applicationContext, "cache.type=redis"); + TestPropertySourceUtils.addInlinedPropertiesToEnvironment( + applicationContext, "redis.connection.type=standalone"); + TestPropertySourceUtils.addInlinedPropertiesToEnvironment( + applicationContext, "redis.standalone.host=localhost"); + TestPropertySourceUtils.addInlinedPropertiesToEnvironment( + applicationContext, "redis.standalone.port=" + redis.getMappedPort(6379)); + } + +} diff --git a/dao/src/test/java/org/thingsboard/server/dao/attributes/CachedAttributesServiceTest.java b/dao/src/test/java/org/thingsboard/server/dao/attributes/CachedAttributesServiceTest.java index a45eb0b711..4f699912e3 100644 --- a/dao/src/test/java/org/thingsboard/server/dao/attributes/CachedAttributesServiceTest.java +++ b/dao/src/test/java/org/thingsboard/server/dao/attributes/CachedAttributesServiceTest.java @@ -17,6 +17,9 @@ package org.thingsboard.server.dao.attributes; import com.google.common.util.concurrent.MoreExecutors; import org.junit.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.thingsboard.server.dao.AbstractJpaDaoTest; +import org.thingsboard.server.dao.alarm.AlarmDao; import org.thingsboard.server.dao.cache.CacheExecutorService; import static org.hamcrest.CoreMatchers.is; @@ -25,7 +28,7 @@ import static org.mockito.ArgumentMatchers.any; import static org.mockito.BDDMockito.willCallRealMethod; import static org.mockito.Mockito.mock; -public class CachedAttributesServiceTest { +public class CachedAttributesServiceTest extends AbstractJpaDaoTest { public static final String REDIS = "redis"; @@ -57,7 +60,6 @@ public class CachedAttributesServiceTest { assertThat(cachedAttributesService.getExecutor(REDIS, cacheExecutorService), is(cacheExecutorService)); assertThat(cachedAttributesService.getExecutor("unknownCacheType", cacheExecutorService), is(cacheExecutorService)); - } } \ No newline at end of file diff --git a/dao/src/test/java/org/thingsboard/server/dao/sql/attributes/AttributeServiceTest.java b/dao/src/test/java/org/thingsboard/server/dao/sql/attributes/AttributeServiceTest.java new file mode 100644 index 0000000000..02b9ef54d6 --- /dev/null +++ b/dao/src/test/java/org/thingsboard/server/dao/sql/attributes/AttributeServiceTest.java @@ -0,0 +1,184 @@ +package org.thingsboard.server.dao.sql.attributes; + +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 org.junit.Assert; +import org.junit.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.thingsboard.server.common.data.DataConstants; +import org.thingsboard.server.common.data.id.DeviceId; +import org.thingsboard.server.common.data.id.TenantId; +import org.thingsboard.server.common.data.kv.AttributeKvEntry; +import org.thingsboard.server.common.data.kv.BaseAttributeKvEntry; +import org.thingsboard.server.common.data.kv.StringDataEntry; +import org.thingsboard.server.dao.AbstractDaoServiceTest; +import org.thingsboard.server.dao.attributes.CachedAttributesService; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collections; +import java.util.List; +import java.util.Optional; +import java.util.UUID; +import java.util.concurrent.Executors; +import java.util.concurrent.TimeUnit; +import java.util.stream.Collectors; + +public class AttributeServiceTest extends AbstractDaoServiceTest { + + private static final String OLD_VALUE = "OLD VALUE"; + private static final String NEW_VALUE = "NEW VALUE"; + + @Autowired + private CachedAttributesService attributesService; + + @Test + public void testDummyRequestWithEmptyResult() throws Exception { + var future = attributesService.find(new TenantId(UUID.randomUUID()), new DeviceId(UUID.randomUUID()), DataConstants.SERVER_SCOPE, "TEST"); + Assert.assertNotNull(future); + var result = future.get(10, TimeUnit.SECONDS); + Assert.assertTrue(result.isEmpty()); + } + + @Test + public void testConcurrentFetchAndUpdate() throws Exception { + var tenantId = new TenantId(UUID.randomUUID()); + ListeningExecutorService pool = MoreExecutors.listeningDecorator(Executors.newFixedThreadPool(2)); + try { + for (int i = 0; i < 100; i++) { + var deviceId = new DeviceId(UUID.randomUUID()); + testConcurrentFetchAndUpdate(tenantId, deviceId, pool); + } + } finally { + pool.shutdownNow(); + } + } + + @Test + public void testConcurrentFetchAndUpdateMulti() throws Exception { + var tenantId = new TenantId(UUID.randomUUID()); + ListeningExecutorService pool = MoreExecutors.listeningDecorator(Executors.newFixedThreadPool(2)); + try { + for (int i = 0; i < 100; i++) { + var deviceId = new DeviceId(UUID.randomUUID()); + testConcurrentFetchAndUpdateMulti(tenantId, deviceId, pool); + } + } finally { + pool.shutdownNow(); + } + } + + @Test + public void testFetchAndUpdateEmpty() throws Exception { + var tenantId = new TenantId(UUID.randomUUID()); + var deviceId = new DeviceId(UUID.randomUUID()); + var scope = DataConstants.SERVER_SCOPE; + var key = "TEST"; + + Optional emptyValue = attributesService.find(tenantId, deviceId, scope, key).get(10, TimeUnit.SECONDS); + Assert.assertTrue(emptyValue.isEmpty()); + + saveAttribute(tenantId, deviceId, scope, key, NEW_VALUE); + Assert.assertEquals(NEW_VALUE, getAttributeValue(tenantId, deviceId, scope, key)); + } + + @Test + public void testFetchAndUpdateMulti() throws Exception { + var tenantId = new TenantId(UUID.randomUUID()); + var deviceId = new DeviceId(UUID.randomUUID()); + var scope = DataConstants.SERVER_SCOPE; + var key1 = "TEST1"; + var key2 = "TEST2"; + + var value = getAttributeValues(tenantId, deviceId, scope, Arrays.asList(key1, key2)); + Assert.assertTrue(value.isEmpty()); + + saveAttribute(tenantId, deviceId, scope, key1, OLD_VALUE); + + value = getAttributeValues(tenantId, deviceId, scope, Arrays.asList(key1, key2)); + Assert.assertEquals(1, value.size()); + Assert.assertEquals(OLD_VALUE, value.get(0)); + + saveAttribute(tenantId, deviceId, scope, key2, NEW_VALUE); + + value = getAttributeValues(tenantId, deviceId, scope, Arrays.asList(key1, key2)); + Assert.assertEquals(2, value.size()); + Assert.assertTrue(value.contains(OLD_VALUE)); + Assert.assertTrue(value.contains(NEW_VALUE)); + + saveAttribute(tenantId, deviceId, scope, key1, NEW_VALUE); + + value = getAttributeValues(tenantId, deviceId, scope, Arrays.asList(key1, key2)); + Assert.assertEquals(2, value.size()); + Assert.assertEquals(NEW_VALUE, value.get(0)); + Assert.assertEquals(NEW_VALUE, value.get(1)); + } + + private void testConcurrentFetchAndUpdate(TenantId tenantId, DeviceId deviceId, ListeningExecutorService pool) throws Exception { + var scope = DataConstants.SERVER_SCOPE; + var key = "TEST"; + saveAttribute(tenantId, deviceId, scope, key, OLD_VALUE); + List> futures = new ArrayList<>(); + futures.add(pool.submit(() -> { + var value = getAttributeValue(tenantId, deviceId, scope, key); + Assert.assertTrue(value.equals(OLD_VALUE) || value.equals(NEW_VALUE)); + })); + futures.add(pool.submit(() -> saveAttribute(tenantId, deviceId, scope, key, NEW_VALUE))); + Futures.allAsList(futures).get(10, TimeUnit.SECONDS); + Assert.assertEquals(NEW_VALUE, getAttributeValue(tenantId, deviceId, scope, key)); + } + + private void testConcurrentFetchAndUpdateMulti(TenantId tenantId, DeviceId deviceId, ListeningExecutorService pool) throws Exception { + var scope = DataConstants.SERVER_SCOPE; + var key1 = "TEST1"; + var key2 = "TEST2"; + saveAttribute(tenantId, deviceId, scope, key1, OLD_VALUE); + saveAttribute(tenantId, deviceId, scope, key2, OLD_VALUE); + List> futures = new ArrayList<>(); + futures.add(pool.submit(() -> { + var value = getAttributeValues(tenantId, deviceId, scope, Arrays.asList(key1, key2)); + Assert.assertEquals(2, value.size()); + Assert.assertTrue(value.contains(OLD_VALUE) || value.contains(NEW_VALUE)); + })); + futures.add(pool.submit(() -> { + saveAttribute(tenantId, deviceId, scope, key1, NEW_VALUE); + saveAttribute(tenantId, deviceId, scope, key2, NEW_VALUE); + })); + Futures.allAsList(futures).get(10, TimeUnit.SECONDS); + var newResult = getAttributeValues(tenantId, deviceId, scope, Arrays.asList(key1, key2)); + Assert.assertEquals(2, newResult.size()); + Assert.assertEquals(NEW_VALUE, newResult.get(0)); + Assert.assertEquals(NEW_VALUE, newResult.get(1)); + } + + private String getAttributeValue(TenantId tenantId, DeviceId deviceId, String scope, String key) { + try { + Optional entry = attributesService.find(tenantId, deviceId, scope, key).get(10, TimeUnit.SECONDS); + return entry.orElseThrow(RuntimeException::new).getStrValue().orElse("Unknown"); + } catch (Exception e) { + throw new RuntimeException(e); + } + } + + private List getAttributeValues(TenantId tenantId, DeviceId deviceId, String scope, List keys) { + try { + List entry = attributesService.find(tenantId, deviceId, scope, keys).get(10, TimeUnit.SECONDS); + return entry.stream().map(e -> e.getStrValue().orElse(null)).collect(Collectors.toList()); + } catch (Exception e) { + throw new RuntimeException(e); + } + } + + private void saveAttribute(TenantId tenantId, DeviceId deviceId, String scope, String key, String s) { + try { + AttributeKvEntry newEntry = new BaseAttributeKvEntry(System.currentTimeMillis(), new StringDataEntry(key, s)); + attributesService.save(tenantId, deviceId, scope, Collections.singletonList(newEntry)).get(10, TimeUnit.SECONDS); + } catch (Exception e) { + Assert.assertNull(e); + } + } + + +} diff --git a/dao/src/test/java/org/thingsboard/server/dao/sql/attributes/RedisAttributeServiceTest.java b/dao/src/test/java/org/thingsboard/server/dao/sql/attributes/RedisAttributeServiceTest.java new file mode 100644 index 0000000000..09a0ccf9d4 --- /dev/null +++ b/dao/src/test/java/org/thingsboard/server/dao/sql/attributes/RedisAttributeServiceTest.java @@ -0,0 +1,30 @@ +package org.thingsboard.server.dao.sql.attributes; + +import lombok.extern.slf4j.Slf4j; +import org.junit.ClassRule; +import org.springframework.context.ApplicationContextInitializer; +import org.springframework.context.ConfigurableApplicationContext; +import org.springframework.test.context.ContextConfiguration; +import org.springframework.test.context.TestPropertySource; +import org.springframework.test.context.support.TestPropertySourceUtils; +import org.testcontainers.containers.GenericContainer; + +@TestPropertySource(properties = { + "cache.type=redis", "redis.connection.type=standalone" +}) +@ContextConfiguration(initializers = RedisAttributeServiceTest.class) +@Slf4j +public class RedisAttributeServiceTest extends AttributeServiceTest implements ApplicationContextInitializer { + + @ClassRule + public static GenericContainer redis = new GenericContainer("redis:4.0").withExposedPorts(6379); + + @Override + public void initialize(ConfigurableApplicationContext applicationContext) { + TestPropertySourceUtils.addInlinedPropertiesToEnvironment( + applicationContext, "redis.standalone.host=localhost"); + TestPropertySourceUtils.addInlinedPropertiesToEnvironment( + applicationContext, "redis.standalone.port=" + redis.getMappedPort(6379)); + } + +} diff --git a/dao/src/test/resources/application-test.properties b/dao/src/test/resources/application-test.properties index 5d02ae84d5..d61bd4eb65 100644 --- a/dao/src/test/resources/application-test.properties +++ b/dao/src/test/resources/application-test.properties @@ -11,6 +11,7 @@ audit-log.sink.type=none cache.type=caffeine cache.maximumPoolSize=16 +cache.attributes.enabled=true #cache.type=redis caffeine.specs.relations.timeToLiveInMinutes=1440 @@ -22,6 +23,9 @@ caffeine.specs.deviceCredentials.maxSize=100000 caffeine.specs.devices.timeToLiveInMinutes=1440 caffeine.specs.devices.maxSize=100000 +caffeine.specs.sessions.timeToLiveInMinutes=1440 +caffeine.specs.sessions.maxSize=100000 + caffeine.specs.assets.timeToLiveInMinutes=1440 caffeine.specs.assets.maxSize=100000 @@ -31,12 +35,21 @@ caffeine.specs.entityViews.maxSize=100000 caffeine.specs.claimDevices.timeToLiveInMinutes=1440 caffeine.specs.claimDevices.maxSize=100000 +caffeine.specs.securitySettings.timeToLiveInMinutes=1440 +caffeine.specs.securitySettings.maxSize=100000 + caffeine.specs.tenantProfiles.timeToLiveInMinutes=1440 caffeine.specs.tenantProfiles.maxSize=100000 caffeine.specs.deviceProfiles.timeToLiveInMinutes=1440 caffeine.specs.deviceProfiles.maxSize=100000 +caffeine.specs.attributes.timeToLiveInMinutes=1440 +caffeine.specs.attributes.maxSize=100000 + +caffeine.specs.tokensOutdatageTime.timeToLiveInMinutes=1440 +caffeine.specs.tokensOutdatageTime.maxSize=100000 + caffeine.specs.otaPackages.timeToLiveInMinutes=1440 caffeine.specs.otaPackages.maxSize=100000