22 changed files with 776 additions and 132 deletions
@ -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<Object, Set<UUID>> objectTransactions = new HashMap<>(); |
||||
|
private final Map<UUID, CaffeineTbCacheTransaction> transactions = new HashMap<>(); |
||||
|
|
||||
|
|
||||
|
<K extends Serializable> TbCacheTransaction newTransaction(List<K> 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(); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
<K extends Serializable, V extends Serializable> void putIfAbsent(K key, V value) { |
||||
|
lock.lock(); |
||||
|
try { |
||||
|
failAllTransactionsByKey(key); |
||||
|
cache.doPutIfAbsent(cacheName, key, value); |
||||
|
} finally { |
||||
|
lock.unlock(); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
public <K extends Serializable> void evict(K key) { |
||||
|
lock.lock(); |
||||
|
try { |
||||
|
failAllTransactionsByKey(key); |
||||
|
cache.doEvict(cacheName, key); |
||||
|
} finally { |
||||
|
lock.unlock(); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
public boolean commit(UUID trId, Map<Object, Object> pendingPuts) { |
||||
|
lock.lock(); |
||||
|
try { |
||||
|
var tr = transactions.get(trId); |
||||
|
var success = !tr.isFailed(); |
||||
|
if (success) { |
||||
|
for (Object key : tr.getKeys()) { |
||||
|
Set<UUID> 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<UUID> transactions = objectTransactions.get(key); |
||||
|
if (transactions != null) { |
||||
|
transactions.remove(id); |
||||
|
if (transactions.isEmpty()) { |
||||
|
objectTransactions.remove(key); |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
private <K extends Serializable> void failAllTransactionsByKey(K key) { |
||||
|
Set<UUID> transactionsIds = objectTransactions.get(key); |
||||
|
if (transactionsIds != null) { |
||||
|
for (UUID otherTrId : transactionsIds) { |
||||
|
transactions.get(otherTrId).setFailed(true); |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
} |
||||
@ -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<Object, Object> pendingPuts = new LinkedHashMap<>(); |
||||
|
|
||||
|
@Override |
||||
|
public <K, V> 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 <T> void rollBackOnFailure(ListenableFuture<T> future, Executor executor) { |
||||
|
Futures.addCallback(future, new FutureCallback<T>() { |
||||
|
@Override |
||||
|
public void onSuccess(@Nullable T result) { |
||||
|
} |
||||
|
|
||||
|
@Override |
||||
|
public void onFailure(Throwable t) { |
||||
|
log.trace("[{}] Rollback transaction due to error", id, t); |
||||
|
rollback(); |
||||
|
} |
||||
|
}, executor); |
||||
|
} |
||||
|
|
||||
|
} |
||||
@ -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<String, CaffeineCacheTransactionStorage> caches = new ConcurrentHashMap<>(); |
||||
|
|
||||
|
@Override |
||||
|
public <K extends Serializable> Cache.ValueWrapper get(String cacheName, K key) { |
||||
|
return cacheManager.getCache(cacheName).get(key); |
||||
|
} |
||||
|
|
||||
|
@Override |
||||
|
public <K extends Serializable, V extends Serializable> void putIfAbsent(String cacheName, K key, V value) { |
||||
|
getCache(cacheName).putIfAbsent(key, value); |
||||
|
} |
||||
|
|
||||
|
@Override |
||||
|
public <K extends Serializable> void evict(String cacheName, K key) { |
||||
|
getCache(cacheName).evict(key); |
||||
|
} |
||||
|
|
||||
|
@Override |
||||
|
public <K extends Serializable> TbCacheTransaction newTransactionForKey(String cacheName, K key) { |
||||
|
return getCache(cacheName).newTransaction(Collections.singletonList(key)); |
||||
|
} |
||||
|
|
||||
|
@Override |
||||
|
public <K extends Serializable> TbCacheTransaction newTransactionForKeys(String cacheName, List<K> keys) { |
||||
|
return getCache(cacheName).newTransaction(keys); |
||||
|
} |
||||
|
|
||||
|
private CaffeineCacheTransactionStorage getCache(String cacheName) { |
||||
|
return caches.computeIfAbsent(cacheName, cn -> new CaffeineCacheTransactionStorage(cacheName, this)); |
||||
|
} |
||||
|
|
||||
|
<K extends Serializable, V extends Serializable> void doPutIfAbsent(String cacheName, Object key, Object value) { |
||||
|
cacheManager.getCache(cacheName).putIfAbsent(key, value); |
||||
|
} |
||||
|
|
||||
|
<K extends Serializable> void doEvict(String cacheName, K key) { |
||||
|
cacheManager.getCache(cacheName).evict(key); |
||||
|
} |
||||
|
} |
||||
@ -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 <K extends Serializable> Cache.ValueWrapper get(String cacheName, K key) { |
||||
|
return cacheManager.getCache(cacheName).get(key); |
||||
|
} |
||||
|
|
||||
|
@Override |
||||
|
public <K extends Serializable, V extends Serializable> void putIfAbsent(String cacheName, K key, V value) { |
||||
|
} |
||||
|
|
||||
|
@Override |
||||
|
public <K extends Serializable> void evict(String cacheName, K key) { |
||||
|
} |
||||
|
|
||||
|
@Override |
||||
|
public <K extends Serializable> TbCacheTransaction newTransactionForKey(String cacheName, K key) { |
||||
|
return null; |
||||
|
} |
||||
|
|
||||
|
@Override |
||||
|
public <K extends Serializable> TbCacheTransaction newTransactionForKeys(String cacheName, List<K> keys) { |
||||
|
return null; |
||||
|
} |
||||
|
|
||||
|
} |
||||
@ -0,0 +1,16 @@ |
|||||
|
package org.thingsboard.server.cache; |
||||
|
|
||||
|
import com.google.common.util.concurrent.ListenableFuture; |
||||
|
|
||||
|
import java.util.concurrent.Executor; |
||||
|
|
||||
|
public interface TbCacheTransaction { |
||||
|
|
||||
|
<K,V> void putIfAbsent(K key, V value); |
||||
|
|
||||
|
boolean commit(); |
||||
|
|
||||
|
void rollback(); |
||||
|
|
||||
|
<T> void rollBackOnFailure(ListenableFuture<T> result, Executor cacheExecutor); |
||||
|
} |
||||
@ -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 { |
||||
|
|
||||
|
<K extends Serializable> Cache.ValueWrapper get(String cacheName, K key); |
||||
|
|
||||
|
<K extends Serializable, V extends Serializable> void putIfAbsent(String cacheName, K key, V value); |
||||
|
|
||||
|
<K extends Serializable> void evict(String cacheName, K key); |
||||
|
|
||||
|
<K extends Serializable> TbCacheTransaction newTransactionForKey(String cacheName, K key); |
||||
|
|
||||
|
<K extends Serializable> TbCacheTransaction newTransactionForKeys(String cacheName, List<K> keys); |
||||
|
|
||||
|
} |
||||
@ -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 { |
||||
|
|
||||
|
} |
||||
@ -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 { |
||||
|
|
||||
|
} |
||||
@ -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); |
|
||||
} |
|
||||
@ -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); |
|
||||
} |
|
||||
} |
|
||||
@ -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; |
||||
|
|
||||
|
} |
||||
@ -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<ConfigurableApplicationContext> { |
||||
|
|
||||
|
@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)); |
||||
|
} |
||||
|
|
||||
|
} |
||||
@ -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<AttributeKvEntry> 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<ListenableFuture<?>> 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<ListenableFuture<?>> 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<AttributeKvEntry> 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<String> getAttributeValues(TenantId tenantId, DeviceId deviceId, String scope, List<String> keys) { |
||||
|
try { |
||||
|
List<AttributeKvEntry> 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); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
|
||||
|
} |
||||
@ -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<ConfigurableApplicationContext> { |
||||
|
|
||||
|
@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)); |
||||
|
} |
||||
|
|
||||
|
} |
||||
Loading…
Reference in new issue