Browse Source

Merge pull request #6536 from thingsboard/bug/cache-issues

Fix misc cache issues with race conditions
pull/6555/head
Andrew Shvayka 4 years ago
committed by GitHub
parent
commit
ba634ada27
No known key found for this signature in database GPG Key ID: 4AEE18F83AFDEB23
  1. 3
      application/pom.xml
  2. 8
      application/src/main/java/org/thingsboard/server/actors/device/DeviceActorMessageProcessor.java
  3. 2
      application/src/main/java/org/thingsboard/server/service/device/DeviceProvisionServiceImpl.java
  4. 6
      application/src/main/java/org/thingsboard/server/service/edge/rpc/EdgeGrpcSession.java
  5. 6
      application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/TelemetryEdgeProcessor.java
  6. 2
      application/src/main/java/org/thingsboard/server/service/install/DefaultSystemDataLoaderService.java
  7. 4
      application/src/main/java/org/thingsboard/server/service/install/update/DefaultCacheCleanupService.java
  8. 17
      application/src/main/java/org/thingsboard/server/service/session/DefaultDeviceSessionCacheService.java
  9. 4
      application/src/main/java/org/thingsboard/server/service/session/DeviceSessionCacheService.java
  10. 34
      application/src/main/java/org/thingsboard/server/service/session/SessionCaffeineCache.java
  11. 52
      application/src/main/java/org/thingsboard/server/service/session/SessionRedisCache.java
  12. 4
      application/src/main/java/org/thingsboard/server/service/telemetry/DefaultTelemetrySubscriptionService.java
  13. 4
      application/src/main/resources/thingsboard.yml
  14. 8
      application/src/test/java/org/thingsboard/server/cache/CaffeineCacheDefaultConfigurationTest.java
  15. 12
      application/src/test/java/org/thingsboard/server/controller/AbstractRuleEngineControllerTest.java
  16. 33
      application/src/test/java/org/thingsboard/server/controller/ControllerSqlTestSuite.java
  17. 125
      application/src/test/java/org/thingsboard/server/rules/lifecycle/AbstractRuleEngineLifecycleIntegrationTest.java
  18. 1
      application/src/test/java/org/thingsboard/server/rules/lifecycle/sql/RuleEngineLifecycleSqlIntegrationTest.java
  19. 35
      application/src/test/java/org/thingsboard/server/transport/TransportSqlTestSuite.java
  20. 4
      common/cache/pom.xml
  21. 27
      common/cache/src/main/java/org/thingsboard/server/cache/CacheSpecsMap.java
  22. 59
      common/cache/src/main/java/org/thingsboard/server/cache/CaffeineTbCacheTransaction.java
  23. 193
      common/cache/src/main/java/org/thingsboard/server/cache/CaffeineTbTransactionalCache.java
  24. 58
      common/cache/src/main/java/org/thingsboard/server/cache/RedisTbCacheTransaction.java
  25. 180
      common/cache/src/main/java/org/thingsboard/server/cache/RedisTbTransactionalCache.java
  26. 44
      common/cache/src/main/java/org/thingsboard/server/cache/SimpleTbCacheValueWrapper.java
  27. 27
      common/cache/src/main/java/org/thingsboard/server/cache/TBRedisCacheConfiguration.java
  28. 10
      common/cache/src/main/java/org/thingsboard/server/cache/TBRedisClusterConfiguration.java
  29. 20
      common/cache/src/main/java/org/thingsboard/server/cache/TBRedisStandaloneConfiguration.java
  30. 16
      common/cache/src/main/java/org/thingsboard/server/cache/TbCacheTransaction.java
  31. 13
      common/cache/src/main/java/org/thingsboard/server/cache/TbCacheValueWrapper.java
  32. 19
      common/cache/src/main/java/org/thingsboard/server/cache/TbCaffeineCacheConfiguration.java
  33. 34
      common/cache/src/main/java/org/thingsboard/server/cache/TbRedisSerializer.java
  34. 89
      common/cache/src/main/java/org/thingsboard/server/cache/TbTransactionalCache.java
  35. 16
      common/cache/src/main/java/org/thingsboard/server/cache/device/DeviceCacheEvictEvent.java
  36. 54
      common/cache/src/main/java/org/thingsboard/server/cache/device/DeviceCacheKey.java
  37. 33
      common/cache/src/main/java/org/thingsboard/server/cache/device/DeviceCaffeineCache.java
  38. 35
      common/cache/src/main/java/org/thingsboard/server/cache/device/DeviceRedisCache.java
  39. 1
      common/cache/src/main/java/org/thingsboard/server/cache/ota/CaffeineOtaPackageCache.java
  40. 1
      common/cache/src/main/java/org/thingsboard/server/cache/ota/RedisOtaPackageDataCache.java
  41. 12
      common/cache/src/test/java/org/thingsboard/server/cache/CacheSpecsMapTest.java
  42. 4
      common/dao-api/src/main/java/org/thingsboard/server/dao/attributes/AttributesService.java
  43. 4
      common/dao-api/src/main/java/org/thingsboard/server/dao/edge/EdgeEventService.java
  44. 2
      common/dao-api/src/main/java/org/thingsboard/server/dao/relation/RelationService.java
  45. 4
      dao/pom.xml
  46. 1
      dao/src/main/java/org/thingsboard/server/dao/DaoUtil.java
  47. 2
      dao/src/main/java/org/thingsboard/server/dao/JpaDaoConfig.java
  48. 30
      dao/src/main/java/org/thingsboard/server/dao/asset/AssetCacheEvictEvent.java
  49. 42
      dao/src/main/java/org/thingsboard/server/dao/asset/AssetCacheKey.java
  50. 33
      dao/src/main/java/org/thingsboard/server/dao/asset/AssetCaffeineCache.java
  51. 1
      dao/src/main/java/org/thingsboard/server/dao/asset/AssetDao.java
  52. 35
      dao/src/main/java/org/thingsboard/server/dao/asset/AssetRedisCache.java
  53. 40
      dao/src/main/java/org/thingsboard/server/dao/asset/BaseAssetService.java
  54. 33
      dao/src/main/java/org/thingsboard/server/dao/attributes/AttributeCaffeineCache.java
  55. 35
      dao/src/main/java/org/thingsboard/server/dao/attributes/AttributeRedisCache.java
  56. 63
      dao/src/main/java/org/thingsboard/server/dao/attributes/AttributesCacheWrapper.java
  57. 10
      dao/src/main/java/org/thingsboard/server/dao/attributes/AttributesDao.java
  58. 17
      dao/src/main/java/org/thingsboard/server/dao/attributes/BaseAttributesService.java
  59. 141
      dao/src/main/java/org/thingsboard/server/dao/attributes/CachedAttributesService.java
  60. 64
      dao/src/main/java/org/thingsboard/server/dao/cache/EntitiesCacheManagerImpl.java
  61. 46
      dao/src/main/java/org/thingsboard/server/dao/cache/PreviousDeviceCredentialsIdKeyGenerator.java
  62. 2
      dao/src/main/java/org/thingsboard/server/dao/customer/CustomerServiceImpl.java
  63. 33
      dao/src/main/java/org/thingsboard/server/dao/device/DeviceCredentialsCaffeineCache.java
  64. 18
      dao/src/main/java/org/thingsboard/server/dao/device/DeviceCredentialsEvictEvent.java
  65. 35
      dao/src/main/java/org/thingsboard/server/dao/device/DeviceCredentialsRedisCache.java
  66. 42
      dao/src/main/java/org/thingsboard/server/dao/device/DeviceCredentialsServiceImpl.java
  67. 63
      dao/src/main/java/org/thingsboard/server/dao/device/DeviceProfileCacheKey.java
  68. 33
      dao/src/main/java/org/thingsboard/server/dao/device/DeviceProfileCaffeineCache.java
  69. 31
      dao/src/main/java/org/thingsboard/server/dao/device/DeviceProfileEvictEvent.java
  70. 35
      dao/src/main/java/org/thingsboard/server/dao/device/DeviceProfileRedisCache.java
  71. 115
      dao/src/main/java/org/thingsboard/server/dao/device/DeviceProfileServiceImpl.java
  72. 126
      dao/src/main/java/org/thingsboard/server/dao/device/DeviceServiceImpl.java
  73. 30
      dao/src/main/java/org/thingsboard/server/dao/edge/EdgeCacheEvictEvent.java
  74. 40
      dao/src/main/java/org/thingsboard/server/dao/edge/EdgeCacheKey.java
  75. 33
      dao/src/main/java/org/thingsboard/server/dao/edge/EdgeCaffeineCache.java
  76. 35
      dao/src/main/java/org/thingsboard/server/dao/edge/EdgeRedisCache.java
  77. 45
      dao/src/main/java/org/thingsboard/server/dao/edge/EdgeServiceImpl.java
  78. 43
      dao/src/main/java/org/thingsboard/server/dao/entity/AbstractCachedEntityService.java
  79. 3
      dao/src/main/java/org/thingsboard/server/dao/entity/AbstractEntityService.java
  80. 67
      dao/src/main/java/org/thingsboard/server/dao/entityview/EntityViewCacheKey.java
  81. 36
      dao/src/main/java/org/thingsboard/server/dao/entityview/EntityViewCacheValue.java
  82. 32
      dao/src/main/java/org/thingsboard/server/dao/entityview/EntityViewCaffeineCache.java
  83. 2
      dao/src/main/java/org/thingsboard/server/dao/entityview/EntityViewDao.java
  84. 35
      dao/src/main/java/org/thingsboard/server/dao/entityview/EntityViewEvictEvent.java
  85. 34
      dao/src/main/java/org/thingsboard/server/dao/entityview/EntityViewRedisCache.java
  86. 99
      dao/src/main/java/org/thingsboard/server/dao/entityview/EntityViewServiceImpl.java
  87. 2
      dao/src/main/java/org/thingsboard/server/dao/event/EventDao.java
  88. 1
      dao/src/main/java/org/thingsboard/server/dao/model/sql/AbstractEdgeEntity.java
  89. 1
      dao/src/main/java/org/thingsboard/server/dao/model/sql/AbstractEntityViewEntity.java
  90. 3
      dao/src/main/java/org/thingsboard/server/dao/model/sql/AbstractTenantEntity.java
  91. 2
      dao/src/main/java/org/thingsboard/server/dao/model/sql/ApiUsageStateEntity.java
  92. 15
      dao/src/main/java/org/thingsboard/server/dao/model/sql/OAuth2ClientRegistrationTemplateEntity.java
  93. 4
      dao/src/main/java/org/thingsboard/server/dao/model/sql/OtaPackageEntity.java
  94. 5
      dao/src/main/java/org/thingsboard/server/dao/model/sql/OtaPackageInfoEntity.java
  95. 2
      dao/src/main/java/org/thingsboard/server/dao/model/sql/RuleNodeStateEntity.java
  96. 2
      dao/src/main/java/org/thingsboard/server/dao/model/sql/TenantProfileEntity.java
  97. 1
      dao/src/main/java/org/thingsboard/server/dao/model/sql/WidgetTypeEntity.java
  98. 1
      dao/src/main/java/org/thingsboard/server/dao/oauth2/OAuth2Configuration.java
  99. 14
      dao/src/main/java/org/thingsboard/server/dao/oauth2/OAuth2Utils.java
  100. 52
      dao/src/main/java/org/thingsboard/server/dao/ota/BaseOtaPackageService.java

3
application/pom.xml

@ -369,14 +369,11 @@
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-surefire-plugin</artifactId>
<version>${surfire.version}</version>
<configuration>
<systemPropertyVariables>
<spring.config.name>thingsboard</spring.config.name>
</systemPropertyVariables>
<excludes>
<exclude>**/sql/*Test.java</exclude>
<exclude>**/psql/*Test.java</exclude>
<exclude>**/nosql/*Test.java</exclude>
</excludes>
<includes>

8
application/src/main/java/org/thingsboard/server/actors/device/DeviceActorMessageProcessor.java

@ -884,10 +884,10 @@ class DeviceActorMessageProcessor extends AbstractContextAwareMsgProcessor {
return;
}
log.debug("[{}] Restoring sessions from cache", deviceId);
DeviceSessionsCacheEntry sessionsDump = null;
DeviceSessionsCacheEntry sessionsDump;
try {
sessionsDump = DeviceSessionsCacheEntry.parseFrom(systemContext.getDeviceSessionCacheService().get(deviceId));
} catch (InvalidProtocolBufferException e) {
sessionsDump = systemContext.getDeviceSessionCacheService().get(deviceId);
} catch (Exception e) {
log.warn("[{}] Failed to decode device sessions from cache", deviceId);
return;
}
@ -942,7 +942,7 @@ class DeviceActorMessageProcessor extends AbstractContextAwareMsgProcessor {
});
systemContext.getDeviceSessionCacheService()
.put(deviceId, DeviceSessionsCacheEntry.newBuilder()
.addAllSessions(sessionsList).build().toByteArray());
.addAllSessions(sessionsList).build());
}
void init(TbActorCtx ctx) {

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

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

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

@ -309,10 +309,10 @@ public final class EdgeGrpcSession implements Closeable {
public void onSuccess(@Nullable UUID ifOffset) {
if (ifOffset != null) {
Long newStartTs = Uuids.unixTimestamp(ifOffset);
ListenableFuture<List<Void>> updateFuture = updateQueueStartTs(newStartTs);
ListenableFuture<List<String>> updateFuture = updateQueueStartTs(newStartTs);
Futures.addCallback(updateFuture, new FutureCallback<>() {
@Override
public void onSuccess(@Nullable List<Void> list) {
public void onSuccess(@Nullable List<String> list) {
log.debug("[{}] queue offset was updated [{}][{}]", sessionId, ifOffset, newStartTs);
result.set(null);
}
@ -496,7 +496,7 @@ public final class EdgeGrpcSession implements Closeable {
}, ctx.getGrpcCallbackExecutorService());
}
private ListenableFuture<List<Void>> updateQueueStartTs(Long newStartTs) {
private ListenableFuture<List<String>> updateQueueStartTs(Long newStartTs) {
log.trace("[{}] updating QueueStartTs [{}][{}]", this.sessionId, edge.getId(), newStartTs);
List<AttributeKvEntry> attributes = Collections.singletonList(
new BaseAttributeKvEntry(

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

@ -206,10 +206,10 @@ public class TelemetryEdgeProcessor extends BaseEdgeProcessor {
SettableFuture<Void> futureToSet = SettableFuture.create();
JsonObject json = JsonUtils.getJsonObject(msg.getKvList());
Set<AttributeKvEntry> attributes = JsonConverter.convertToAttributes(json);
ListenableFuture<List<Void>> future = attributesService.save(tenantId, entityId, metaData.getValue("scope"), new ArrayList<>(attributes));
Futures.addCallback(future, new FutureCallback<List<Void>>() {
ListenableFuture<List<String>> future = attributesService.save(tenantId, entityId, metaData.getValue("scope"), new ArrayList<>(attributes));
Futures.addCallback(future, new FutureCallback<>() {
@Override
public void onSuccess(@Nullable List<Void> voids) {
public void onSuccess(@Nullable List<String> keys) {
Pair<String, RuleChainId> defaultQueueAndRuleChain = getDefaultQueueNameAndRuleChainId(tenantId, entityId);
String queueName = defaultQueueAndRuleChain.getKey();
RuleChainId ruleChainId = defaultQueueAndRuleChain.getValue();

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

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

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

@ -68,6 +68,10 @@ public class DefaultCacheCleanupService implements CacheCleanupService {
log.info("Clear cache to upgrade from version 3.3.3 to 3.3.4 ...");
clearAll();
break;
case "3.3.4":
log.info("Clear cache to upgrade from version 3.3.4 to 3.4.0 ...");
clearAll();
break;
default:
//Do nothing, since cache cleanup is optional.
}

17
application/src/main/java/org/thingsboard/server/service/session/DefaultDeviceSessionCacheService.java

@ -15,14 +15,18 @@
*/
package org.thingsboard.server.service.session;
import com.google.protobuf.InvalidProtocolBufferException;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.cache.annotation.CachePut;
import org.springframework.cache.annotation.Cacheable;
import org.springframework.stereotype.Service;
import org.thingsboard.server.cache.TbTransactionalCache;
import org.thingsboard.server.common.data.id.DeviceId;
import org.thingsboard.server.gen.transport.TransportProtos.DeviceSessionsCacheEntry;
import org.thingsboard.server.queue.util.TbCoreComponent;
import java.io.Serializable;
import java.util.Collections;
import static org.thingsboard.server.common.data.CacheConstants.SESSIONS_CACHE;
@ -35,17 +39,20 @@ import static org.thingsboard.server.common.data.CacheConstants.SESSIONS_CACHE;
@Slf4j
public class DefaultDeviceSessionCacheService implements DeviceSessionCacheService {
@Autowired
protected TbTransactionalCache<DeviceId, DeviceSessionsCacheEntry> cache;
@Override
@Cacheable(cacheNames = SESSIONS_CACHE, key = "#deviceId.toString()")
public byte[] get(DeviceId deviceId) {
public DeviceSessionsCacheEntry get(DeviceId deviceId) {
log.debug("[{}] Fetching session data from cache", deviceId);
return DeviceSessionsCacheEntry.newBuilder().addAllSessions(Collections.emptyList()).build().toByteArray();
return cache.getAndPutInTransaction(deviceId, () ->
DeviceSessionsCacheEntry.newBuilder().addAllSessions(Collections.emptyList()).build(), false);
}
@Override
@CachePut(cacheNames = SESSIONS_CACHE, key = "#deviceId.toString()")
public byte[] put(DeviceId deviceId, byte[] sessions) {
public DeviceSessionsCacheEntry put(DeviceId deviceId, DeviceSessionsCacheEntry sessions) {
log.debug("[{}] Pushing session data to cache: {}", deviceId, sessions);
cache.putIfAbsent(deviceId, sessions);
return sessions;
}
}

4
application/src/main/java/org/thingsboard/server/service/session/DeviceSessionCacheService.java

@ -23,8 +23,8 @@ import org.thingsboard.server.gen.transport.TransportProtos.DeviceSessionsCacheE
*/
public interface DeviceSessionCacheService {
byte[] get(DeviceId deviceId);
DeviceSessionsCacheEntry get(DeviceId deviceId);
byte[] put(DeviceId deviceId, byte[] sessions);
DeviceSessionsCacheEntry put(DeviceId deviceId, DeviceSessionsCacheEntry sessions);
}

34
application/src/main/java/org/thingsboard/server/service/session/SessionCaffeineCache.java

@ -0,0 +1,34 @@
/**
* 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.service.session;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.cache.CacheManager;
import org.springframework.stereotype.Service;
import org.thingsboard.server.common.data.CacheConstants;
import org.thingsboard.server.common.data.id.DeviceId;
import org.thingsboard.server.cache.CaffeineTbTransactionalCache;
import org.thingsboard.server.gen.transport.TransportProtos;
@ConditionalOnProperty(prefix = "cache", value = "type", havingValue = "caffeine", matchIfMissing = true)
@Service("SessionCache")
public class SessionCaffeineCache extends CaffeineTbTransactionalCache<DeviceId, TransportProtos.DeviceSessionsCacheEntry> {
public SessionCaffeineCache(CacheManager cacheManager) {
super(cacheManager, CacheConstants.SESSIONS_CACHE);
}
}

52
application/src/main/java/org/thingsboard/server/service/session/SessionRedisCache.java

@ -0,0 +1,52 @@
/**
* 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.service.session;
import com.google.protobuf.InvalidProtocolBufferException;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.data.redis.connection.RedisConnectionFactory;
import org.springframework.data.redis.serializer.RedisSerializer;
import org.springframework.data.redis.serializer.SerializationException;
import org.springframework.stereotype.Service;
import org.thingsboard.server.cache.CacheSpecsMap;
import org.thingsboard.server.cache.TBRedisCacheConfiguration;
import org.thingsboard.server.common.data.CacheConstants;
import org.thingsboard.server.common.data.id.DeviceId;
import org.thingsboard.server.cache.RedisTbTransactionalCache;
import org.thingsboard.server.gen.transport.TransportProtos;
@ConditionalOnProperty(prefix = "cache", value = "type", havingValue = "redis")
@Service("SessionCache")
public class SessionRedisCache extends RedisTbTransactionalCache<DeviceId, TransportProtos.DeviceSessionsCacheEntry> {
public SessionRedisCache(TBRedisCacheConfiguration configuration, CacheSpecsMap cacheSpecsMap, RedisConnectionFactory connectionFactory) {
super(CacheConstants.ASSET_CACHE, cacheSpecsMap, connectionFactory, configuration, new RedisSerializer<>() {
@Override
public byte[] serialize(TransportProtos.DeviceSessionsCacheEntry deviceSessionsCacheEntry) throws SerializationException {
return deviceSessionsCacheEntry.toByteArray();
}
@Override
public TransportProtos.DeviceSessionsCacheEntry deserialize(byte[] bytes) throws SerializationException {
try {
return TransportProtos.DeviceSessionsCacheEntry.parseFrom(bytes);
} catch (InvalidProtocolBufferException e) {
throw new RuntimeException("Failed to deserialize session cache entry");
}
}
});
}
}

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

@ -243,7 +243,7 @@ public class DefaultTelemetrySubscriptionService extends AbstractSubscriptionSer
@Override
public void saveAndNotifyInternal(TenantId tenantId, EntityId entityId, String scope, List<AttributeKvEntry> attributes, boolean notifyDevice, FutureCallback<Void> callback) {
ListenableFuture<List<Void>> saveFuture = attrService.save(tenantId, entityId, scope, attributes);
ListenableFuture<List<String>> saveFuture = attrService.save(tenantId, entityId, scope, attributes);
addVoidCallback(saveFuture, callback);
addWsCallback(saveFuture, success -> onAttributesUpdate(tenantId, entityId, scope, attributes, notifyDevice));
}
@ -269,7 +269,7 @@ public class DefaultTelemetrySubscriptionService extends AbstractSubscriptionSer
@Override
public void deleteAndNotifyInternal(TenantId tenantId, EntityId entityId, String scope, List<String> keys, FutureCallback<Void> callback) {
ListenableFuture<List<Void>> deleteFuture = attrService.removeAll(tenantId, entityId, scope, keys);
ListenableFuture<List<String>> deleteFuture = attrService.removeAll(tenantId, entityId, scope, keys);
addVoidCallback(deleteFuture, callback);
addWsCallback(deleteFuture, success -> onAttributesDelete(tenantId, entityId, scope, keys));
}

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

@ -387,8 +387,6 @@ cache:
attributes:
# make sure that if cache.type is 'redis' and cache.attributes.enabled is 'true' that you change 'maxmemory-policy' Redis config property to 'allkeys-lru', 'allkeys-lfu' or 'allkeys-random'
enabled: "${CACHE_ATTRIBUTES_ENABLED:true}"
caffeine:
specs:
relations:
timeToLiveInMinutes: "${CACHE_SPECS_RELATIONS_TTL:1440}"
@ -475,6 +473,8 @@ redis:
maxWaitMills: "${REDIS_POOL_CONFIG_MAX_WAIT_MS:60000}"
numberTestsPerEvictionRun: "${REDIS_POOL_CONFIG_NUMBER_TESTS_PER_EVICTION_RUN:3}"
blockWhenExhausted: "${REDIS_POOL_CONFIG_BLOCK_WHEN_EXHAUSTED:true}"
# TTL for short-living SET commands that are used to replace DEL in order to enable transaction support
evictTtlInMs: "${REDIS_EVICT_TTL_MS:60000}"
# Check new version updates parameters
updates:

8
application/src/test/java/org/thingsboard/server/cache/CaffeineCacheDefaultConfigurationTest.java

@ -36,15 +36,15 @@ import static org.assertj.core.api.Assertions.assertThat;
public class CaffeineCacheDefaultConfigurationTest {
@Autowired
CaffeineCacheConfiguration caffeineCacheConfiguration;
CacheSpecsMap cacheSpecsMap;
@Test
public void verifyTransactionAwareCacheManagerProxy() {
assertThat(caffeineCacheConfiguration.getSpecs()).as("specs").isNotNull();
caffeineCacheConfiguration.getSpecs().forEach((name, cacheSpecs)->assertThat(cacheSpecs).as("cache %s specs", name).isNotNull());
assertThat(cacheSpecsMap.getSpecs()).as("specs").isNotNull();
cacheSpecsMap.getSpecs().forEach((name, cacheSpecs)->assertThat(cacheSpecs).as("cache %s specs", name).isNotNull());
SoftAssertions softly = new SoftAssertions();
caffeineCacheConfiguration.getSpecs().forEach((name, cacheSpecs)->{
cacheSpecsMap.getSpecs().forEach((name, cacheSpecs)->{
softly.assertThat(name).as("cache name").isNotEmpty();
softly.assertThat(cacheSpecs.getTimeToLiveInMinutes()).as("cache %s time to live", name).isGreaterThan(0);
softly.assertThat(cacheSpecs.getMaxSize()).as("cache %s max size", name).isGreaterThan(0);

12
application/src/test/java/org/thingsboard/server/controller/AbstractRuleEngineControllerTest.java

@ -18,6 +18,7 @@ package org.thingsboard.server.controller;
import com.fasterxml.jackson.core.type.TypeReference;
import com.fasterxml.jackson.databind.JsonNode;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.test.context.TestPropertySource;
import org.thingsboard.server.common.data.DataConstants;
import org.thingsboard.server.common.data.Event;
import org.thingsboard.server.common.data.id.EntityId;
@ -35,6 +36,9 @@ import java.util.function.Predicate;
/**
* Created by ashvayka on 20.03.18.
*/
@TestPropertySource(properties = {
"js.evaluator=mock",
})
public abstract class AbstractRuleEngineControllerTest extends AbstractControllerTest {
@Autowired
@ -57,12 +61,18 @@ public abstract class AbstractRuleEngineControllerTest extends AbstractControlle
}
protected PageData<Event> getDebugEvents(TenantId tenantId, EntityId entityId, int limit) throws Exception {
return getEvents(tenantId, entityId, DataConstants.DEBUG_RULE_NODE, limit);
}
protected PageData<Event> getEvents(TenantId tenantId, EntityId entityId, String eventType, int limit) throws Exception {
TimePageLink pageLink = new TimePageLink(limit);
return doGetTypedWithTimePageLink("/api/events/{entityType}/{entityId}/{eventType}?tenantId={tenantId}&",
new TypeReference<PageData<Event>>() {
}, pageLink, entityId.getEntityType(), entityId.getId(), DataConstants.DEBUG_RULE_NODE, tenantId.getId());
}, pageLink, entityId.getEntityType(), entityId.getId(), eventType, tenantId.getId());
}
protected JsonNode getMetadata(Event outEvent) {
String metaDataStr = outEvent.getBody().get("metadata").asText();
try {

33
application/src/test/java/org/thingsboard/server/controller/ControllerSqlTestSuite.java

@ -1,33 +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.controller;
import org.junit.BeforeClass;
import org.junit.extensions.cpsuite.ClasspathSuite;
import org.junit.runner.RunWith;
import org.thingsboard.server.queue.memory.InMemoryStorage;
@RunWith(ClasspathSuite.class)
@ClasspathSuite.ClassnameFilters({
// "org.thingsboard.server.controller.sql.WebsocketApiSqlTest",
// "org.thingsboard.server.controller.sql.EntityQueryControllerSqlTest",
// "org.thingsboard.server.controller.sql.TbResourceControllerSqlTest",
// "org.thingsboard.server.controller.sql.DeviceProfileControllerSqlTest",
"org.thingsboard.server.controller.sql.*Test",
})
public class ControllerSqlTestSuite {
}

125
application/src/test/java/org/thingsboard/server/rules/lifecycle/AbstractRuleEngineLifecycleIntegrationTest.java

@ -15,30 +15,28 @@
*/
package org.thingsboard.server.rules.lifecycle;
import com.google.common.util.concurrent.ListenableFuture;
import lombok.extern.slf4j.Slf4j;
import org.awaitility.Awaitility;
import org.junit.After;
import org.junit.Assert;
import org.junit.Before;
import org.junit.ClassRule;
import org.junit.Test;
import org.mockito.Mockito;
import org.mockito.stubbing.Answer;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.test.util.ReflectionTestUtils;
import org.springframework.context.ConfigurableApplicationContext;
import org.springframework.test.context.support.TestPropertySourceUtils;
import org.testcontainers.containers.GenericContainer;
import org.thingsboard.rule.engine.metadata.TbGetAttributesNodeConfiguration;
import org.thingsboard.server.actors.ActorSystemContext;
import org.thingsboard.server.common.data.DataConstants;
import org.thingsboard.server.common.data.Device;
import org.thingsboard.server.common.data.Event;
import org.thingsboard.server.common.data.Tenant;
import org.thingsboard.server.common.data.User;
import org.thingsboard.server.common.data.kv.BaseAttributeKvEntry;
import org.thingsboard.server.common.data.kv.StringDataEntry;
import org.thingsboard.server.common.data.page.PageData;
import org.thingsboard.server.common.data.rule.RuleChain;
import org.thingsboard.server.common.data.rule.RuleChainMetaData;
import org.thingsboard.server.common.data.rule.RuleNode;
import org.thingsboard.server.common.data.security.Authority;
import org.thingsboard.server.common.msg.TbMsg;
import org.thingsboard.server.common.msg.TbMsgMetaData;
import org.thingsboard.server.common.msg.queue.QueueToRuleEngineMsg;
@ -46,16 +44,13 @@ import org.thingsboard.server.common.msg.queue.TbMsgCallback;
import org.thingsboard.server.controller.AbstractRuleEngineControllerTest;
import org.thingsboard.server.dao.attributes.AttributesService;
import org.thingsboard.server.dao.event.EventService;
import org.thingsboard.server.queue.memory.InMemoryStorage;
import java.util.Collections;
import java.util.List;
import java.util.concurrent.TimeUnit;
import java.util.stream.Collectors;
import static org.awaitility.Awaitility.await;
import static org.mockito.Mockito.spy;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
import static java.util.concurrent.TimeUnit.MILLISECONDS;
/**
* @author Valerii Sosliuk
@ -63,9 +58,6 @@ import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.
@Slf4j
public abstract class AbstractRuleEngineLifecycleIntegrationTest extends AbstractRuleEngineControllerTest {
protected Tenant savedTenant;
protected User tenantAdmin;
@Autowired
protected ActorSystemContext actorSystem;
@ -75,50 +67,14 @@ public abstract class AbstractRuleEngineLifecycleIntegrationTest extends Abstrac
@Autowired
protected EventService eventService;
@Autowired
protected InMemoryStorage storage;
@Before
public void beforeTest() throws Exception {
EventService spyEventService = spy(eventService);
Mockito.doAnswer((Answer<ListenableFuture<Void>>) invocation -> {
Object[] args = invocation.getArguments();
Event event = (Event) args[0];
ListenableFuture<Void> future = eventService.saveAsync(event);
try {
future.get();
} catch (Exception e) {}
return future;
}).when(spyEventService).saveAsync(Mockito.any(Event.class));
ReflectionTestUtils.setField(actorSystem, "eventService", spyEventService);
loginSysAdmin();
Tenant tenant = new Tenant();
tenant.setTitle("My tenant");
savedTenant = doPost("/api/tenant", tenant, Tenant.class);
Assert.assertNotNull(savedTenant);
ruleChainService.deleteRuleChainsByTenantId(savedTenant.getId());
tenantAdmin = new User();
tenantAdmin.setAuthority(Authority.TENANT_ADMIN);
tenantAdmin.setTenantId(savedTenant.getId());
tenantAdmin.setEmail("tenant2@thingsboard.org");
tenantAdmin.setFirstName("Joe");
tenantAdmin.setLastName("Downs");
createUserAndLogin(tenantAdmin, "testPassword1");
loginTenantAdmin();
ruleChainService.deleteRuleChainsByTenantId(tenantId);
}
@After
public void afterTest() throws Exception {
loginSysAdmin();
if (savedTenant != null) {
doDelete("/api/tenant/" + savedTenant.getId().getId().toString()).andExpect(status().isOk());
}
}
@Test
@ -126,7 +82,7 @@ public abstract class AbstractRuleEngineLifecycleIntegrationTest extends Abstrac
// Creating Rule Chain
RuleChain ruleChain = new RuleChain();
ruleChain.setName("Simple Rule Chain");
ruleChain.setTenantId(savedTenant.getId());
ruleChain.setTenantId(tenantId);
ruleChain.setRoot(true);
ruleChain.setDebugMode(true);
ruleChain = saveRuleChain(ruleChain);
@ -149,8 +105,24 @@ public abstract class AbstractRuleEngineLifecycleIntegrationTest extends Abstrac
metaData = saveRuleChainMetaData(metaData);
Assert.assertNotNull(metaData);
ruleChain = getRuleChain(ruleChain.getId());
Assert.assertNotNull(ruleChain.getFirstRuleNodeId());
final RuleChain ruleChainFinal = getRuleChain(ruleChain.getId());
Assert.assertNotNull(ruleChainFinal.getFirstRuleNodeId());
//TODO find out why RULE_NODE update event did not appear all the time
List<Event> rcEvents = Awaitility.await("Rule Node started successfully")
.pollInterval(10, MILLISECONDS)
.atMost(TIMEOUT, TimeUnit.SECONDS)
.until(() -> {
List<Event> debugEvents = getEvents(tenantId, ruleChainFinal.getFirstRuleNodeId(), DataConstants.LC_EVENT, 1000)
.getData().stream().filter(e -> {
var body = e.getBody();
return body.has("event") && body.get("event").asText().equals("STARTED")
&& body.has("success") && body.get("success").asBoolean();
}).collect(Collectors.toList());
debugEvents.forEach((e) -> log.trace("event: {}", e));
return debugEvents;
},
x -> x.size() == 1);
// Saving the device
Device device = new Device();
@ -158,35 +130,46 @@ public abstract class AbstractRuleEngineLifecycleIntegrationTest extends Abstrac
device.setType("default");
device = doPost("/api/device", device, Device.class);
log.warn("before update attr");
attributesService.save(device.getTenantId(), device.getId(), DataConstants.SERVER_SCOPE,
Collections.singletonList(new BaseAttributeKvEntry(new StringDataEntry("serverAttributeKey", "serverAttributeValue"), System.currentTimeMillis())));
await("total inMemory queue lag is empty").atMost(30, TimeUnit.SECONDS)
.until(() -> storage.getLagTotal() == 0);
Thread.sleep(1000);
Collections.singletonList(new BaseAttributeKvEntry(new StringDataEntry("serverAttributeKey", "serverAttributeValue"), System.currentTimeMillis())))
.get(TIMEOUT, TimeUnit.SECONDS);
log.warn("attr updated");
TbMsgCallback tbMsgCallback = Mockito.mock(TbMsgCallback.class);
Mockito.when(tbMsgCallback.isMsgValid()).thenReturn(true);
TbMsg tbMsg = TbMsg.newMsg("CUSTOM", device.getId(), new TbMsgMetaData(), "{}", tbMsgCallback);
QueueToRuleEngineMsg qMsg = new QueueToRuleEngineMsg(savedTenant.getId(), tbMsg, null, null);
QueueToRuleEngineMsg qMsg = new QueueToRuleEngineMsg(tenantId, tbMsg, null, null);
// Pushing Message to the system
log.warn("before tell tbMsgCallback");
actorSystem.tell(qMsg);
Mockito.verify(tbMsgCallback, Mockito.timeout(10000)).onSuccess();
PageData<Event> eventsPage = getDebugEvents(savedTenant.getId(), ruleChain.getFirstRuleNodeId(), 1000);
List<Event> events = eventsPage.getData().stream().filter(filterByCustomEvent()).collect(Collectors.toList());
Assert.assertEquals(2, events.size());
log.warn("awaiting tbMsgCallback");
Mockito.verify(tbMsgCallback, Mockito.timeout(TimeUnit.SECONDS.toMillis(TIMEOUT))).onSuccess();
log.warn("awaiting events");
List<Event> events = Awaitility.await("get debug by custom event")
.pollInterval(10, MILLISECONDS)
.atMost(TIMEOUT, TimeUnit.SECONDS)
.until(() -> {
List<Event> debugEvents = getDebugEvents(tenantId, ruleChainFinal.getFirstRuleNodeId(), 1000)
.getData().stream().filter(filterByCustomEvent()).collect(Collectors.toList());
log.warn("filtered debug events [{}]", debugEvents.size());
debugEvents.forEach((e) -> log.warn("event: {}", e));
return debugEvents;
},
x -> x.size() == 2);
log.warn("asserting..");
Event inEvent = events.stream().filter(e -> e.getBody().get("type").asText().equals(DataConstants.IN)).findFirst().get();
Assert.assertEquals(ruleChain.getFirstRuleNodeId(), inEvent.getEntityId());
Assert.assertEquals(ruleChainFinal.getFirstRuleNodeId(), inEvent.getEntityId());
Assert.assertEquals(device.getId().getId().toString(), inEvent.getBody().get("entityId").asText());
Event outEvent = events.stream().filter(e -> e.getBody().get("type").asText().equals(DataConstants.OUT)).findFirst().get();
Assert.assertEquals(ruleChain.getFirstRuleNodeId(), outEvent.getEntityId());
Assert.assertEquals(ruleChainFinal.getFirstRuleNodeId(), outEvent.getEntityId());
Assert.assertEquals(device.getId().getId().toString(), outEvent.getBody().get("entityId").asText());
log.warn("OUT event {}", outEvent);
log.warn("OUT event metadata {}", getMetadata(outEvent));
Assert.assertNotNull("metadata has ss_serverAttributeKey", getMetadata(outEvent).get("ss_serverAttributeKey"));
Assert.assertEquals("serverAttributeValue", getMetadata(outEvent).get("ss_serverAttributeKey").asText());
}

1
application/src/test/java/org/thingsboard/server/rules/lifecycle/sql/RuleEngineLifecycleSqlIntegrationTest.java

@ -16,7 +16,6 @@
package org.thingsboard.server.rules.lifecycle.sql;
import org.thingsboard.server.dao.service.DaoSqlTest;
import org.thingsboard.server.rules.flow.AbstractRuleEngineFlowIntegrationTest;
import org.thingsboard.server.rules.lifecycle.AbstractRuleEngineLifecycleIntegrationTest;
/**

35
application/src/test/java/org/thingsboard/server/transport/TransportSqlTestSuite.java

@ -1,35 +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.transport;
import org.junit.extensions.cpsuite.ClasspathSuite;
import org.junit.runner.RunWith;
@RunWith(ClasspathSuite.class)
@ClasspathSuite.ClassnameFilters({
"org.thingsboard.server.transport.*.rpc.*Test",
"org.thingsboard.server.transport.*.telemetry.timeseries.sql.*Test",
"org.thingsboard.server.transport.*.telemetry.attributes.*Test",
"org.thingsboard.server.transport.*.attributes.updates.*Test",
"org.thingsboard.server.transport.*.attributes.request.*Test",
"org.thingsboard.server.transport.*.claim.*Test",
"org.thingsboard.server.transport.*.provision.*Test",
"org.thingsboard.server.transport.*.credentials.*Test",
"org.thingsboard.server.transport.lwm2m.*.sql.*Test"
})
public class TransportSqlTestSuite {
}

4
common/cache/pom.xml

@ -56,6 +56,10 @@
<groupId>com.github.ben-manes.caffeine</groupId>
<artifactId>caffeine</artifactId>
</dependency>
<dependency>
<groupId>javax.annotation</groupId>
<artifactId>javax.annotation-api</artifactId>
</dependency>
<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-lang3</artifactId>

27
application/src/test/java/org/thingsboard/server/system/SystemSqlTestSuite.java → common/cache/src/main/java/org/thingsboard/server/cache/CacheSpecsMap.java

@ -13,20 +13,21 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.thingsboard.server.system;
package org.thingsboard.server.cache;
import org.junit.BeforeClass;
import org.junit.extensions.cpsuite.ClasspathSuite;
import org.junit.runner.RunWith;
import org.thingsboard.server.queue.memory.InMemoryStorage;
import lombok.Data;
import lombok.Getter;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.context.annotation.Configuration;
/**
* Created by Valerii Sosliuk on 6/27/2017.
*/
@RunWith(ClasspathSuite.class)
@ClasspathSuite.ClassnameFilters({
"org.thingsboard.server.system.sql.*SqlTest",
})
public class SystemSqlTestSuite {
import java.util.Map;
@Configuration
@ConfigurationProperties(prefix = "cache")
@Data
public class CacheSpecsMap {
@Getter
private Map<String, CacheSpecs> specs;
}

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

@ -0,0 +1,59 @@
/**
* 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.cache;
import lombok.Getter;
import lombok.RequiredArgsConstructor;
import lombok.Setter;
import lombok.extern.slf4j.Slf4j;
import java.io.Serializable;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.UUID;
@Slf4j
@RequiredArgsConstructor
public class CaffeineTbCacheTransaction<K extends Serializable, V extends Serializable> implements TbCacheTransaction<K, V> {
@Getter
private final UUID id = UUID.randomUUID();
private final CaffeineTbTransactionalCache<K, V> cache;
@Getter
private final List<K> keys;
@Getter
@Setter
private boolean failed;
private final Map<Object, Object> 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);
}
}

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

@ -0,0 +1,193 @@
/**
* 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.cache;
import lombok.Getter;
import lombok.RequiredArgsConstructor;
import org.springframework.cache.CacheManager;
import java.io.Serializable;
import java.util.Collection;
import java.util.Collections;
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
public abstract class CaffeineTbTransactionalCache<K extends Serializable, V extends Serializable> implements TbTransactionalCache<K, V> {
private final CacheManager cacheManager;
@Getter
private final String cacheName;
private final Lock lock = new ReentrantLock();
private final Map<K, Set<UUID>> objectTransactions = new HashMap<>();
private final Map<UUID, CaffeineTbCacheTransaction<K, V>> transactions = new HashMap<>();
@Override
public TbCacheValueWrapper<V> get(K key) {
return SimpleTbCacheValueWrapper.wrap(cacheManager.getCache(cacheName).get(key));
}
@Override
public void put(K key, V value) {
lock.lock();
try {
failAllTransactionsByKey(key);
cacheManager.getCache(cacheName).put(key, value);
} finally {
lock.unlock();
}
}
@Override
public void putIfAbsent(K key, V value) {
lock.lock();
try {
failAllTransactionsByKey(key);
doPutIfAbsent(key, value);
} finally {
lock.unlock();
}
}
@Override
public void evict(K key) {
lock.lock();
try {
failAllTransactionsByKey(key);
doEvict(key);
} finally {
lock.unlock();
}
}
@Override
public void evict(Collection<K> keys) {
lock.lock();
try {
keys.forEach(key -> {
failAllTransactionsByKey(key);
doEvict(key);
});
} finally {
lock.unlock();
}
}
@Override
public void evictOrPut(K key, V value) {
//No need to put the value in case of Caffeine, because evict will cancel concurrent transaction used to "get" the missing value from cache.
evict(key);
}
@Override
public TbCacheTransaction<K, V> newTransactionForKey(K key) {
return newTransaction(Collections.singletonList(key));
}
@Override
public TbCacheTransaction<K, V> newTransactionForKeys(List<K> keys) {
return newTransaction(keys);
}
void doPutIfAbsent(Object key, Object value) {
cacheManager.getCache(cacheName).putIfAbsent(key, value);
}
void doEvict(K key) {
cacheManager.getCache(cacheName).evict(key);
}
TbCacheTransaction<K, V> 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();
}
}
public boolean commit(UUID trId, Map<Object, Object> pendingPuts) {
lock.lock();
try {
var tr = transactions.get(trId);
var success = !tr.isFailed();
if (success) {
for (K 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(this::doPutIfAbsent);
}
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<K, V> 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 void failAllTransactionsByKey(K key) {
Set<UUID> transactionsIds = objectTransactions.get(key);
if (transactionsIds != null) {
for (UUID otherTrId : transactionsIds) {
transactions.get(otherTrId).setFailed(true);
}
}
}
}

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

@ -0,0 +1,58 @@
/**
* 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.cache;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.data.redis.connection.RedisConnection;
import org.springframework.data.redis.connection.RedisStringCommands;
import java.io.Serializable;
import java.util.Objects;
@Slf4j
@RequiredArgsConstructor
public class RedisTbCacheTransaction<K extends Serializable, V extends Serializable> implements TbCacheTransaction<K, V> {
private final RedisTbTransactionalCache<K, V> cache;
private final RedisConnection connection;
@Override
public void putIfAbsent(K key, V value) {
cache.put(connection, key, value, RedisStringCommands.SetOption.UPSERT);
}
@Override
public boolean commit() {
try {
var execResult = connection.exec();
var result = execResult != null && execResult.stream().anyMatch(Objects::nonNull);
return result;
} finally {
connection.close();
}
}
@Override
public void rollback() {
try {
connection.discard();
} finally {
connection.close();
}
}
}

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

@ -0,0 +1,180 @@
/**
* 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.cache;
import lombok.Getter;
import lombok.extern.slf4j.Slf4j;
import org.springframework.cache.support.NullValue;
import org.springframework.data.redis.connection.RedisConnection;
import org.springframework.data.redis.connection.RedisConnectionFactory;
import org.springframework.data.redis.connection.RedisStringCommands;
import org.springframework.data.redis.core.types.Expiration;
import org.springframework.data.redis.serializer.RedisSerializer;
import org.springframework.data.redis.serializer.StringRedisSerializer;
import java.io.Serializable;
import java.util.Arrays;
import java.util.Collection;
import java.util.List;
import java.util.concurrent.TimeUnit;
@Slf4j
public abstract class RedisTbTransactionalCache<K extends Serializable, V extends Serializable> implements TbTransactionalCache<K, V> {
private static final byte[] BINARY_NULL_VALUE = RedisSerializer.java().serialize(NullValue.INSTANCE);
@Getter
private final String cacheName;
private final RedisConnectionFactory connectionFactory;
private final RedisSerializer<String> keySerializer = new StringRedisSerializer();
private final RedisSerializer<V> valueSerializer;
private final Expiration evictExpiration;
private final Expiration cacheTtl;
public RedisTbTransactionalCache(String cacheName,
CacheSpecsMap cacheSpecsMap,
RedisConnectionFactory connectionFactory,
TBRedisCacheConfiguration configuration,
RedisSerializer<V> valueSerializer) {
this.cacheName = cacheName;
this.connectionFactory = connectionFactory;
this.valueSerializer = valueSerializer;
this.evictExpiration = Expiration.from(configuration.getEvictTtlInMs(), TimeUnit.MILLISECONDS);
if (cacheSpecsMap.getSpecs() != null && cacheSpecsMap.getSpecs().get(cacheName) != null) {
CacheSpecs cacheSpecs = cacheSpecsMap.getSpecs().get(cacheName);
this.cacheTtl = Expiration.from(cacheSpecs.getTimeToLiveInMinutes(), TimeUnit.MINUTES);
} else {
this.cacheTtl = Expiration.persistent();
}
}
@Override
public TbCacheValueWrapper<V> get(K key) {
try (var connection = connectionFactory.getConnection()) {
byte[] rawKey = getRawKey(key);
byte[] rawValue = connection.get(rawKey);
if (rawValue == null) {
return null;
} else if (Arrays.equals(rawValue, BINARY_NULL_VALUE)) {
return SimpleTbCacheValueWrapper.empty();
} else {
V value = valueSerializer.deserialize(rawValue);
return SimpleTbCacheValueWrapper.wrap(value);
}
}
}
@Override
public void put(K key, V value) {
try (var connection = connectionFactory.getConnection()) {
put(connection, key, value, RedisStringCommands.SetOption.UPSERT);
}
}
@Override
public void putIfAbsent(K key, V value) {
try (var connection = connectionFactory.getConnection()) {
put(connection, key, value, RedisStringCommands.SetOption.SET_IF_ABSENT);
}
}
@Override
public void evict(K key) {
try (var connection = connectionFactory.getConnection()) {
connection.del(getRawKey(key));
}
}
@Override
public void evict(Collection<K> keys) {
try (var connection = connectionFactory.getConnection()) {
connection.del(keys.stream().map(this::getRawKey).toArray(byte[][]::new));
}
}
@Override
public void evictOrPut(K key, V value) {
try (var connection = connectionFactory.getConnection()) {
var rawKey = getRawKey(key);
var records = connection.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);
}
}
}
@Override
public TbCacheTransaction<K, V> newTransactionForKey(K key) {
byte[][] rawKey = new byte[][]{getRawKey(key)};
RedisConnection connection = watch(rawKey);
return new RedisTbCacheTransaction<>(this, connection);
}
@Override
public TbCacheTransaction<K, V> newTransactionForKeys(List<K> keys) {
RedisConnection connection = watch(keys.stream().map(this::getRawKey).toArray(byte[][]::new));
return new RedisTbCacheTransaction<>(this, connection);
}
private RedisConnection watch(byte[][] rawKeysList) {
var connection = connectionFactory.getConnection();
try {
connection.watch(rawKeysList);
connection.multi();
} catch (Exception e) {
connection.close();
throw e;
}
return connection;
}
private byte[] getRawKey(K key) {
String keyString = cacheName + key.toString();
byte[] rawKey;
try {
rawKey = keySerializer.serialize(keyString);
} catch (Exception e) {
log.warn("Failed to serialize the cache key: {}", key, e);
throw new RuntimeException(e);
}
if (rawKey == null) {
log.warn("Failed to serialize the cache key: {}", key);
throw new IllegalArgumentException("Failed to serialize the cache key!");
}
return rawKey;
}
private byte[] getRawValue(V value) {
if (value == null) {
return BINARY_NULL_VALUE;
} else {
try {
return valueSerializer.serialize(value);
} catch (Exception e) {
log.warn("Failed to serialize the cache value: {}", value, e);
throw new RuntimeException(e);
}
}
}
public void put(RedisConnection connection, K key, V value, RedisStringCommands.SetOption setOption) {
byte[] rawKey = getRawKey(key);
byte[] rawValue = getRawValue(value);
connection.set(rawKey, rawValue, cacheTtl, setOption);
}
}

44
common/cache/src/main/java/org/thingsboard/server/cache/SimpleTbCacheValueWrapper.java

@ -0,0 +1,44 @@
/**
* 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.cache;
import lombok.AccessLevel;
import lombok.RequiredArgsConstructor;
import org.springframework.cache.Cache;
@RequiredArgsConstructor(access = AccessLevel.PRIVATE)
public class SimpleTbCacheValueWrapper<T> implements TbCacheValueWrapper<T> {
private final T value;
@Override
public T get() {
return value;
}
public static <T> SimpleTbCacheValueWrapper<T> empty() {
return new SimpleTbCacheValueWrapper<>(null);
}
public static <T> SimpleTbCacheValueWrapper<T> wrap(T value) {
return new SimpleTbCacheValueWrapper<>(value);
}
@SuppressWarnings("unchecked")
public static <T> SimpleTbCacheValueWrapper<T> wrap(Cache.ValueWrapper source) {
return source == null ? null : new SimpleTbCacheValueWrapper<>((T) source.get());
}
}

27
common/cache/src/main/java/org/thingsboard/server/cache/TBRedisCacheConfiguration.java

@ -36,42 +36,45 @@ 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.evictTtlInMs:60000}")
private int evictTtlInMs;
@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

10
common/cache/src/main/java/org/thingsboard/server/cache/TBRedisClusterConfiguration.java

@ -29,23 +29,23 @@ import java.util.Collections;
import java.util.List;
@Configuration
@ConditionalOnMissingBean(CaffeineCacheConfiguration.class)
@ConditionalOnMissingBean(TbCaffeineCacheConfiguration.class)
@ConditionalOnProperty(prefix = "redis.connection", value = "type", havingValue = "cluster")
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() {

20
common/cache/src/main/java/org/thingsboard/server/cache/TBRedisStandaloneConfiguration.java

@ -26,35 +26,35 @@ import org.springframework.data.redis.connection.jedis.JedisConnectionFactory;
import java.time.Duration;
@Configuration
@ConditionalOnMissingBean(CaffeineCacheConfiguration.class)
@ConditionalOnMissingBean(TbCaffeineCacheConfiguration.class)
@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() {

16
dao/src/test/java/org/thingsboard/server/dao/JpaDaoTestSuite.java → common/cache/src/main/java/org/thingsboard/server/cache/TbCacheTransaction.java

@ -13,16 +13,14 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.thingsboard.server.dao;
package org.thingsboard.server.cache;
import org.junit.extensions.cpsuite.ClasspathSuite;
import org.junit.extensions.cpsuite.ClasspathSuite.ClassnameFilters;
import org.junit.runner.RunWith;
public interface TbCacheTransaction<K, V> {
@RunWith(ClasspathSuite.class)
@ClassnameFilters({
"org.thingsboard.server.dao.sql.*Test"
})
public class JpaDaoTestSuite {
void putIfAbsent(K key, V value);
boolean commit();
void rollback();
}

13
application/src/test/java/org/thingsboard/server/edge/EdgeSqlTestSuite.java → common/cache/src/main/java/org/thingsboard/server/cache/TbCacheValueWrapper.java

@ -13,17 +13,10 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.thingsboard.server.edge;
package org.thingsboard.server.cache;
import org.junit.BeforeClass;
import org.junit.extensions.cpsuite.ClasspathSuite;
import org.junit.runner.RunWith;
import org.thingsboard.server.queue.memory.InMemoryStorage;
public interface TbCacheValueWrapper<T> {
@RunWith(ClasspathSuite.class)
@ClasspathSuite.ClassnameFilters({
"org.thingsboard.server.edge.sql.*Test",
})
public class EdgeSqlTestSuite {
T get();
}

19
common/cache/src/main/java/org/thingsboard/server/cache/CaffeineCacheConfiguration.java → common/cache/src/main/java/org/thingsboard/server/cache/TbCaffeineCacheConfiguration.java

@ -19,10 +19,8 @@ import com.github.benmanes.caffeine.cache.Caffeine;
import com.github.benmanes.caffeine.cache.RemovalCause;
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.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.cache.CacheManager;
import org.springframework.cache.annotation.EnableCaching;
import org.springframework.cache.caffeine.CaffeineCache;
@ -34,20 +32,20 @@ import org.springframework.context.annotation.Configuration;
import java.util.Arrays;
import java.util.Collection;
import java.util.List;
import java.util.Map;
import java.util.concurrent.TimeUnit;
import java.util.stream.Collectors;
@Configuration
@ConditionalOnProperty(prefix = "cache", value = "type", havingValue = "caffeine", matchIfMissing = true)
@ConfigurationProperties(prefix = "caffeine")
@EnableCaching
@Data
@Slf4j
public class CaffeineCacheConfiguration {
public class TbCaffeineCacheConfiguration {
private Map<String, CacheSpecs> specs;
private final CacheSpecsMap configuration;
public TbCaffeineCacheConfiguration(CacheSpecsMap configuration) {
this.configuration = configuration;
}
/**
* Transaction aware CaffeineCache implementation with TransactionAwareCacheManagerProxy
@ -55,11 +53,11 @@ public class CaffeineCacheConfiguration {
*/
@Bean
public CacheManager cacheManager() {
log.trace("Initializing cache: {} specs {}", Arrays.toString(RemovalCause.values()), specs);
log.trace("Initializing cache: {} specs {}", Arrays.toString(RemovalCause.values()), configuration.getSpecs());
SimpleCacheManager manager = new SimpleCacheManager();
if (specs != null) {
if (configuration.getSpecs() != null) {
List<CaffeineCache> caches =
specs.entrySet().stream()
configuration.getSpecs().entrySet().stream()
.map(entry -> buildCache(entry.getKey(),
entry.getValue()))
.collect(Collectors.toList());
@ -95,4 +93,5 @@ public class CaffeineCacheConfiguration {
return 1;
};
}
}

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

@ -0,0 +1,34 @@
/**
* 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.cache;
import org.springframework.data.redis.serializer.RedisSerializer;
import org.springframework.data.redis.serializer.SerializationException;
public class TbRedisSerializer<T> implements RedisSerializer<T> {
private final RedisSerializer<Object> java = RedisSerializer.java();
@Override
public byte[] serialize(T t) throws SerializationException {
return java.serialize(t);
}
@Override
public T deserialize(byte[] bytes) throws SerializationException {
return (T) java.deserialize(bytes);
}
}

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

@ -0,0 +1,89 @@
/**
* 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.cache;
import java.io.Serializable;
import java.util.Collection;
import java.util.List;
import java.util.function.Function;
import java.util.function.Supplier;
public interface TbTransactionalCache<K extends Serializable, V extends Serializable> {
String getCacheName();
TbCacheValueWrapper<V> get(K key);
void put(K key, V value);
void putIfAbsent(K key, V value);
void evict(K key);
void evict(Collection<K> keys);
void evictOrPut(K key, V value);
TbCacheTransaction<K, V> newTransactionForKey(K key);
TbCacheTransaction<K, V> newTransactionForKeys(List<K> keys);
default V getAndPutInTransaction(K key, Supplier<V> dbCall, boolean cacheNullValue) {
TbCacheValueWrapper<V> cacheValueWrapper = get(key);
if (cacheValueWrapper != null) {
return cacheValueWrapper.get();
}
var cacheTransaction = newTransactionForKey(key);
try {
V dbValue = dbCall.get();
if (dbValue != null || cacheNullValue) {
cacheTransaction.putIfAbsent(key, dbValue);
cacheTransaction.commit();
return dbValue;
} else {
cacheTransaction.rollback();
return null;
}
} catch (Throwable e) {
cacheTransaction.rollback();
throw e;
}
}
default <R> R getAndPutInTransaction(K key, Supplier<R> dbCall, Function<V, R> cacheValueToResult, Function<R, V> dbValueToCacheValue, boolean cacheNullValue) {
TbCacheValueWrapper<V> cacheValueWrapper = get(key);
if (cacheValueWrapper != null) {
var cacheValue = cacheValueWrapper.get();
return cacheValue == null ? null : cacheValueToResult.apply(cacheValue);
}
var cacheTransaction = newTransactionForKey(key);
try {
R dbValue = dbCall.get();
if (dbValue != null || cacheNullValue) {
cacheTransaction.putIfAbsent(key, dbValueToCacheValue.apply(dbValue));
cacheTransaction.commit();
return dbValue;
} else {
cacheTransaction.rollback();
return null;
}
} catch (Throwable e) {
cacheTransaction.rollback();
throw e;
}
}
}

16
dao/src/main/java/org/thingsboard/server/dao/cache/EntitiesCacheManager.java → common/cache/src/main/java/org/thingsboard/server/cache/device/DeviceCacheEvictEvent.java

@ -13,18 +13,18 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.thingsboard.server.dao.cache;
package org.thingsboard.server.cache.device;
import lombok.Data;
import org.thingsboard.server.common.data.id.DeviceId;
import org.thingsboard.server.common.data.id.TenantId;
public interface EntitiesCacheManager {
@Data
public class DeviceCacheEvictEvent {
void removeDeviceFromCacheByName(TenantId tenantId, String name);
private final TenantId tenantId;
private final DeviceId deviceId;
private final String newName;
private final String oldName;
void removeDeviceFromCacheById(TenantId tenantId, DeviceId deviceId);
void removeAssetFromCacheByName(TenantId tenantId, String name);
void removeEdgeFromCacheByName(TenantId tenantId, String name);
}

54
common/cache/src/main/java/org/thingsboard/server/cache/device/DeviceCacheKey.java

@ -0,0 +1,54 @@
/**
* 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.cache.device;
import lombok.Builder;
import lombok.EqualsAndHashCode;
import lombok.Getter;
import lombok.RequiredArgsConstructor;
import org.thingsboard.server.common.data.id.DeviceId;
import org.thingsboard.server.common.data.id.TenantId;
import java.io.Serializable;
@Getter
@EqualsAndHashCode
@RequiredArgsConstructor
@Builder
public class DeviceCacheKey implements Serializable {
private final TenantId tenantId;
private final DeviceId deviceId;
private final String deviceName;
public DeviceCacheKey(TenantId tenantId, DeviceId deviceId) {
this(tenantId, deviceId, null);
}
public DeviceCacheKey(TenantId tenantId, String deviceName) {
this(tenantId, null, deviceName);
}
@Override
public String toString() {
if (deviceId != null) {
return tenantId + "_" + deviceId;
} else {
return tenantId + "_n_" + deviceName;
}
}
}

33
common/cache/src/main/java/org/thingsboard/server/cache/device/DeviceCaffeineCache.java

@ -0,0 +1,33 @@
/**
* 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.cache.device;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.cache.CacheManager;
import org.springframework.stereotype.Service;
import org.thingsboard.server.cache.CaffeineTbTransactionalCache;
import org.thingsboard.server.common.data.CacheConstants;
import org.thingsboard.server.common.data.Device;
@ConditionalOnProperty(prefix = "cache", value = "type", havingValue = "caffeine", matchIfMissing = true)
@Service("DeviceCache")
public class DeviceCaffeineCache extends CaffeineTbTransactionalCache<DeviceCacheKey, Device> {
public DeviceCaffeineCache(CacheManager cacheManager) {
super(cacheManager, CacheConstants.DEVICE_CACHE);
}
}

35
common/cache/src/main/java/org/thingsboard/server/cache/device/DeviceRedisCache.java

@ -0,0 +1,35 @@
/**
* 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.cache.device;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.data.redis.connection.RedisConnectionFactory;
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.TbRedisSerializer;
import org.thingsboard.server.common.data.CacheConstants;
import org.thingsboard.server.common.data.Device;
@ConditionalOnProperty(prefix = "cache", value = "type", havingValue = "redis")
@Service("DeviceCache")
public class DeviceRedisCache extends RedisTbTransactionalCache<DeviceCacheKey, Device> {
public DeviceRedisCache(TBRedisCacheConfiguration configuration, CacheSpecsMap cacheSpecsMap, RedisConnectionFactory connectionFactory) {
super(CacheConstants.DEVICE_CACHE, cacheSpecsMap, connectionFactory, configuration, new TbRedisSerializer<>());
}
}

1
common/cache/src/main/java/org/thingsboard/server/cache/ota/CaffeineOtaPackageCache.java

@ -20,7 +20,6 @@ import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.cache.CacheManager;
import org.springframework.stereotype.Service;
import static org.thingsboard.server.common.data.CacheConstants.OTA_PACKAGE_CACHE;
import static org.thingsboard.server.common.data.CacheConstants.OTA_PACKAGE_DATA_CACHE;
@Service

1
common/cache/src/main/java/org/thingsboard/server/cache/ota/RedisOtaPackageDataCache.java

@ -21,7 +21,6 @@ import org.springframework.data.redis.connection.RedisConnection;
import org.springframework.data.redis.connection.RedisConnectionFactory;
import org.springframework.stereotype.Service;
import static org.thingsboard.server.common.data.CacheConstants.OTA_PACKAGE_CACHE;
import static org.thingsboard.server.common.data.CacheConstants.OTA_PACKAGE_DATA_CACHE;
@Service

12
common/cache/src/test/java/org/thingsboard/server/cache/CaffeineCacheConfigurationTest.java → common/cache/src/test/java/org/thingsboard/server/cache/CacheSpecsMapTest.java

@ -30,16 +30,16 @@ import org.springframework.test.context.junit.jupiter.SpringExtension;
import static org.assertj.core.api.Assertions.assertThat;
@ExtendWith(SpringExtension.class)
@ContextConfiguration(classes = CaffeineCacheConfiguration.class)
@ContextConfiguration(classes = {CacheSpecsMap.class, TbCaffeineCacheConfiguration.class})
@EnableConfigurationProperties
@TestPropertySource(properties = {
"cache.type=caffeine",
"caffeine.specs.relations.timeToLiveInMinutes=1440",
"caffeine.specs.relations.maxSize=0",
"caffeine.specs.devices.timeToLiveInMinutes=60",
"caffeine.specs.devices.maxSize=100"})
"cache.specs.relations.timeToLiveInMinutes=1440",
"cache.specs.relations.maxSize=0",
"cache.specs.devices.timeToLiveInMinutes=60",
"cache.specs.devices.maxSize=100"})
@Slf4j
public class CaffeineCacheConfigurationTest {
public class CacheSpecsMapTest {
@Autowired
CacheManager cacheManager;

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

@ -37,9 +37,9 @@ public interface AttributesService {
ListenableFuture<List<AttributeKvEntry>> findAll(TenantId tenantId, EntityId entityId, String scope);
ListenableFuture<List<Void>> save(TenantId tenantId, EntityId entityId, String scope, List<AttributeKvEntry> attributes);
ListenableFuture<List<String>> save(TenantId tenantId, EntityId entityId, String scope, List<AttributeKvEntry> attributes);
ListenableFuture<List<Void>> removeAll(TenantId tenantId, EntityId entityId, String scope, List<String> attributeKeys);
ListenableFuture<List<String>> removeAll(TenantId tenantId, EntityId entityId, String scope, List<String> attributeKeys);
List<String> findAllKeysByDeviceProfileId(TenantId tenantId, DeviceProfileId deviceProfileId);

4
common/dao-api/src/main/java/org/thingsboard/server/dao/edge/EdgeEventService.java

@ -28,5 +28,9 @@ public interface EdgeEventService {
PageData<EdgeEvent> findEdgeEvents(TenantId tenantId, EdgeId edgeId, TimePageLink pageLink, boolean withTsUpdate);
/**
* Executes stored procedure to cleanup old edge events.
* @param ttl the ttl for edge events in seconds
*/
void cleanupEvents(long ttl);
}

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

@ -38,8 +38,6 @@ public interface RelationService {
EntityRelation getRelation(TenantId tenantId, EntityId from, EntityId to, String relationType, RelationTypeGroup typeGroup);
ListenableFuture<EntityRelation> getRelationAsync(TenantId tenantId, EntityId from, EntityId to, String relationType, RelationTypeGroup typeGroup);
boolean saveRelation(TenantId tenantId, EntityRelation relation);
ListenableFuture<Boolean> saveRelationAsync(TenantId tenantId, EntityRelation relation);

4
dao/pom.xml

@ -234,12 +234,8 @@
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-surefire-plugin</artifactId>
<version>${surfire.version}</version>
<configuration>
<excludes>
<exclude>**/sql/*Test.java</exclude>
<exclude>**/sql/*/*DaoTest.java</exclude>
<exclude>**/psql/*Test.java</exclude>
<exclude>**/nosql/*Test.java</exclude>
</excludes>
<includes>

1
dao/src/main/java/org/thingsboard/server/dao/DaoUtil.java

@ -18,7 +18,6 @@ package org.thingsboard.server.dao;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.PageRequest;
import org.springframework.data.domain.Pageable;
import org.thingsboard.server.common.data.id.UUIDBased;
import org.thingsboard.server.common.data.page.PageData;
import org.thingsboard.server.common.data.page.PageLink;

2
dao/src/main/java/org/thingsboard/server/dao/JpaDaoConfig.java

@ -27,7 +27,7 @@ import org.thingsboard.server.dao.util.TbAutoConfiguration;
*/
@Configuration
@TbAutoConfiguration
@ComponentScan("org.thingsboard.server.dao.sql")
@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

30
dao/src/main/java/org/thingsboard/server/dao/asset/AssetCacheEvictEvent.java

@ -0,0 +1,30 @@
/**
* 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.asset;
import lombok.Data;
import lombok.RequiredArgsConstructor;
import org.thingsboard.server.common.data.id.TenantId;
@Data
@RequiredArgsConstructor
class AssetCacheEvictEvent {
private final TenantId tenantId;
private final String newName;
private final String oldName;
}

42
dao/src/main/java/org/thingsboard/server/dao/asset/AssetCacheKey.java

@ -0,0 +1,42 @@
/**
* 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.asset;
import lombok.Builder;
import lombok.EqualsAndHashCode;
import lombok.Getter;
import lombok.RequiredArgsConstructor;
import org.thingsboard.server.common.data.id.TenantId;
import java.io.Serializable;
@Getter
@EqualsAndHashCode
@RequiredArgsConstructor
@Builder
public class AssetCacheKey implements Serializable {
private static final long serialVersionUID = 4196610233744512673L;
private final TenantId tenantId;
private final String name;
@Override
public String toString() {
return tenantId + "_" + name;
}
}

33
dao/src/main/java/org/thingsboard/server/dao/asset/AssetCaffeineCache.java

@ -0,0 +1,33 @@
/**
* 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.asset;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.cache.CacheManager;
import org.springframework.stereotype.Service;
import org.thingsboard.server.common.data.CacheConstants;
import org.thingsboard.server.common.data.asset.Asset;
import org.thingsboard.server.cache.CaffeineTbTransactionalCache;
@ConditionalOnProperty(prefix = "cache", value = "type", havingValue = "caffeine", matchIfMissing = true)
@Service("AssetCache")
public class AssetCaffeineCache extends CaffeineTbTransactionalCache<AssetCacheKey, Asset> {
public AssetCaffeineCache(CacheManager cacheManager) {
super(cacheManager, CacheConstants.ASSET_CACHE);
}
}

1
dao/src/main/java/org/thingsboard/server/dao/asset/AssetDao.java

@ -22,7 +22,6 @@ import org.thingsboard.server.common.data.asset.AssetInfo;
import org.thingsboard.server.common.data.id.TenantId;
import org.thingsboard.server.common.data.page.PageData;
import org.thingsboard.server.common.data.page.PageLink;
import org.thingsboard.server.common.data.page.TimePageLink;
import org.thingsboard.server.dao.Dao;
import org.thingsboard.server.dao.TenantEntityDao;

35
dao/src/main/java/org/thingsboard/server/dao/asset/AssetRedisCache.java

@ -0,0 +1,35 @@
/**
* 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.asset;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.data.redis.connection.RedisConnectionFactory;
import org.springframework.stereotype.Service;
import org.thingsboard.server.cache.CacheSpecsMap;
import org.thingsboard.server.cache.TBRedisCacheConfiguration;
import org.thingsboard.server.common.data.CacheConstants;
import org.thingsboard.server.common.data.asset.Asset;
import org.thingsboard.server.cache.RedisTbTransactionalCache;
import org.thingsboard.server.cache.TbRedisSerializer;
@ConditionalOnProperty(prefix = "cache", value = "type", havingValue = "redis")
@Service("AssetCache")
public class AssetRedisCache extends RedisTbTransactionalCache<AssetCacheKey, Asset> {
public AssetRedisCache(TBRedisCacheConfiguration configuration, CacheSpecsMap cacheSpecsMap, RedisConnectionFactory connectionFactory) {
super(CacheConstants.ASSET_CACHE, cacheSpecsMap, connectionFactory, configuration, new TbRedisSerializer<>());
}
}

40
dao/src/main/java/org/thingsboard/server/dao/asset/BaseAssetService.java

@ -22,12 +22,14 @@ import com.google.common.util.concurrent.MoreExecutors;
import lombok.extern.slf4j.Slf4j;
import org.hibernate.exception.ConstraintViolationException;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.cache.annotation.CacheEvict;
import org.springframework.cache.annotation.Cacheable;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Propagation;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.transaction.event.TransactionalEventListener;
import org.thingsboard.server.common.data.EntitySubtype;
import org.thingsboard.server.common.data.EntityType;
import org.thingsboard.server.common.data.EntityView;
import org.thingsboard.server.common.data.StringUtils;
import org.thingsboard.server.common.data.asset.Asset;
import org.thingsboard.server.common.data.asset.AssetInfo;
import org.thingsboard.server.common.data.asset.AssetSearchQuery;
@ -42,8 +44,7 @@ import org.thingsboard.server.common.data.page.PageLink;
import org.thingsboard.server.common.data.relation.EntityRelation;
import org.thingsboard.server.common.data.relation.EntitySearchDirection;
import org.thingsboard.server.common.data.relation.RelationTypeGroup;
import org.thingsboard.server.dao.cache.EntitiesCacheManager;
import org.thingsboard.server.dao.entity.AbstractEntityService;
import org.thingsboard.server.dao.entity.AbstractCachedEntityService;
import org.thingsboard.server.dao.exception.DataValidationException;
import org.thingsboard.server.dao.service.DataValidator;
import org.thingsboard.server.dao.service.PaginatedRemover;
@ -55,7 +56,6 @@ import java.util.List;
import java.util.concurrent.ExecutionException;
import java.util.stream.Collectors;
import static org.thingsboard.server.common.data.CacheConstants.ASSET_CACHE;
import static org.thingsboard.server.dao.DaoUtil.toUUIDs;
import static org.thingsboard.server.dao.service.Validator.validateId;
import static org.thingsboard.server.dao.service.Validator.validateIds;
@ -64,7 +64,7 @@ import static org.thingsboard.server.dao.service.Validator.validateString;
@Service
@Slf4j
public class BaseAssetService extends AbstractEntityService implements AssetService {
public class BaseAssetService extends AbstractCachedEntityService<AssetCacheKey, Asset, AssetCacheEvictEvent> implements AssetService {
public static final String INCORRECT_TENANT_ID = "Incorrect tenantId ";
public static final String INCORRECT_CUSTOMER_ID = "Incorrect customerId ";
@ -74,12 +74,20 @@ public class BaseAssetService extends AbstractEntityService implements AssetServ
@Autowired
private AssetDao assetDao;
@Autowired
private EntitiesCacheManager cacheManager;
@Autowired
private DataValidator<Asset> assetValidator;
@TransactionalEventListener(classes = AssetCacheEvictEvent.class)
@Override
public void handleEvictEvent(AssetCacheEvictEvent event) {
List<AssetCacheKey> keys = new ArrayList<>(2);
keys.add(new AssetCacheKey(event.getTenantId(), event.getNewName()));
if (StringUtils.isNotEmpty(event.getOldName()) && !event.getOldName().equals(event.getNewName())) {
keys.add(new AssetCacheKey(event.getTenantId(), event.getOldName()));
}
cache.evict(keys);
}
@Override
public AssetInfo findAssetInfoById(TenantId tenantId, AssetId assetId) {
log.trace("Executing findAssetInfoById [{}]", assetId);
@ -101,24 +109,26 @@ public class BaseAssetService extends AbstractEntityService implements AssetServ
return assetDao.findByIdAsync(tenantId, assetId.getId());
}
@Cacheable(cacheNames = ASSET_CACHE, key = "{#tenantId, #name}")
@Override
public Asset findAssetByTenantIdAndName(TenantId tenantId, String name) {
log.trace("Executing findAssetByTenantIdAndName [{}][{}]", tenantId, name);
validateId(tenantId, INCORRECT_TENANT_ID + tenantId);
return assetDao.findAssetsByTenantIdAndName(tenantId.getId(), name)
.orElse(null);
return cache.getAndPutInTransaction(new AssetCacheKey(tenantId, name),
() -> assetDao.findAssetsByTenantIdAndName(tenantId.getId(), name)
.orElse(null), true);
}
@CacheEvict(cacheNames = ASSET_CACHE, key = "{#asset.tenantId, #asset.name}")
@Override
public Asset saveAsset(Asset asset) {
log.trace("Executing saveAsset [{}]", asset);
assetValidator.validate(asset, Asset::getTenantId);
Asset oldAsset = assetValidator.validate(asset, Asset::getTenantId);
Asset savedAsset;
AssetCacheEvictEvent evictEvent = new AssetCacheEvictEvent(asset.getTenantId(), asset.getName(), oldAsset != null ? oldAsset.getName() : null);
try {
savedAsset = assetDao.save(asset.getTenantId(), asset);
publishEvictEvent(evictEvent);
} catch (Exception t) {
handleEvictEvent(evictEvent);
ConstraintViolationException e = extractConstraintViolationException(t).orElse(null);
if (e != null && e.getConstraintName() != null && e.getConstraintName().equalsIgnoreCase("asset_name_unq_key")) {
throw new DataValidationException("Asset with such name already exists!");
@ -160,7 +170,7 @@ public class BaseAssetService extends AbstractEntityService implements AssetServ
throw new RuntimeException("Exception while finding entity views for assetId [" + assetId + "]", e);
}
cacheManager.removeAssetFromCacheByName(asset.getTenantId(), asset.getName());
publishEvictEvent(new AssetCacheEvictEvent(asset.getTenantId(), asset.getName(), null));
assetDao.removeById(tenantId, assetId.getId());
}

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

@ -0,0 +1,33 @@
/**
* 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 org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.cache.CacheManager;
import org.springframework.stereotype.Service;
import org.thingsboard.server.common.data.CacheConstants;
import org.thingsboard.server.common.data.kv.AttributeKvEntry;
import org.thingsboard.server.cache.CaffeineTbTransactionalCache;
@ConditionalOnProperty(prefix = "cache", value = "type", havingValue = "caffeine", matchIfMissing = true)
@Service("AttributeCache")
public class AttributeCaffeineCache extends CaffeineTbTransactionalCache<AttributeCacheKey, AttributeKvEntry> {
public AttributeCaffeineCache(CacheManager cacheManager) {
super(cacheManager, CacheConstants.ATTRIBUTES_CACHE);
}
}

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

@ -0,0 +1,35 @@
/**
* 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 org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.data.redis.connection.RedisConnectionFactory;
import org.springframework.stereotype.Service;
import org.thingsboard.server.cache.CacheSpecsMap;
import org.thingsboard.server.cache.TBRedisCacheConfiguration;
import org.thingsboard.server.common.data.CacheConstants;
import org.thingsboard.server.common.data.kv.AttributeKvEntry;
import org.thingsboard.server.cache.RedisTbTransactionalCache;
import org.thingsboard.server.cache.TbRedisSerializer;
@ConditionalOnProperty(prefix = "cache", value = "type", havingValue = "redis")
@Service("AttributeCache")
public class AttributeRedisCache extends RedisTbTransactionalCache<AttributeCacheKey, AttributeKvEntry> {
public AttributeRedisCache(TBRedisCacheConfiguration configuration, CacheSpecsMap cacheSpecsMap, RedisConnectionFactory connectionFactory) {
super(CacheConstants.ATTRIBUTES_CACHE, cacheSpecsMap, connectionFactory, configuration, new TbRedisSerializer<>());
}
}

63
dao/src/main/java/org/thingsboard/server/dao/attributes/AttributesCacheWrapper.java

@ -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 AttributesCacheWrapper {
private final Cache attributesCache;
public AttributesCacheWrapper(CacheManager cacheManager) {
this.attributesCache = cacheManager.getCache(ATTRIBUTES_CACHE);
}
public Cache.ValueWrapper get(AttributeCacheKey attributeCacheKey) {
try {
return attributesCache.get(attributeCacheKey);
} catch (Exception e) {
log.debug("Failed to retrieve element from cache for key {}. Reason - {}.", attributeCacheKey, e.getMessage());
return null;
}
}
public void put(AttributeCacheKey attributeCacheKey, AttributeKvEntry attributeKvEntry) {
try {
attributesCache.put(attributeCacheKey, attributeKvEntry);
} catch (Exception e) {
log.debug("Failed to put element from cache for key {}. Reason - {}.", attributeCacheKey, e.getMessage());
}
}
public void evict(AttributeCacheKey attributeCacheKey) {
try {
attributesCache.evict(attributeCacheKey);
} catch (Exception e) {
log.debug("Failed to evict element from cache for key {}. Reason - {}.", attributeCacheKey, e.getMessage());
}
}
}

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

@ -31,15 +31,15 @@ import java.util.Optional;
*/
public interface AttributesDao {
ListenableFuture<Optional<AttributeKvEntry>> find(TenantId tenantId, EntityId entityId, String attributeType, String attributeKey);
Optional<AttributeKvEntry> find(TenantId tenantId, EntityId entityId, String attributeType, String attributeKey);
ListenableFuture<List<AttributeKvEntry>> find(TenantId tenantId, EntityId entityId, String attributeType, Collection<String> attributeKey);
List<AttributeKvEntry> find(TenantId tenantId, EntityId entityId, String attributeType, Collection<String> attributeKey);
ListenableFuture<List<AttributeKvEntry>> findAll(TenantId tenantId, EntityId entityId, String attributeType);
List<AttributeKvEntry> findAll(TenantId tenantId, EntityId entityId, String attributeType);
ListenableFuture<Void> save(TenantId tenantId, EntityId entityId, String attributeType, AttributeKvEntry attribute);
ListenableFuture<String> save(TenantId tenantId, EntityId entityId, String attributeType, AttributeKvEntry attribute);
ListenableFuture<List<Void>> removeAll(TenantId tenantId, EntityId entityId, String attributeType, List<String> keys);
List<ListenableFuture<String>> removeAll(TenantId tenantId, EntityId entityId, String attributeType, List<String> keys);
List<String> findAllKeysByDeviceProfileId(TenantId tenantId, DeviceProfileId deviceProfileId);

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

@ -53,20 +53,20 @@ public class BaseAttributesService implements AttributesService {
public ListenableFuture<Optional<AttributeKvEntry>> find(TenantId tenantId, EntityId entityId, String scope, String attributeKey) {
validate(entityId, scope);
Validator.validateString(attributeKey, "Incorrect attribute key " + attributeKey);
return attributesDao.find(tenantId, entityId, scope, attributeKey);
return Futures.immediateFuture(attributesDao.find(tenantId, entityId, scope, attributeKey));
}
@Override
public ListenableFuture<List<AttributeKvEntry>> find(TenantId tenantId, EntityId entityId, String scope, Collection<String> attributeKeys) {
validate(entityId, scope);
attributeKeys.forEach(attributeKey -> Validator.validateString(attributeKey, "Incorrect attribute key " + attributeKey));
return attributesDao.find(tenantId, entityId, scope, attributeKeys);
return Futures.immediateFuture(attributesDao.find(tenantId, entityId, scope, attributeKeys));
}
@Override
public ListenableFuture<List<AttributeKvEntry>> findAll(TenantId tenantId, EntityId entityId, String scope) {
validate(entityId, scope);
return attributesDao.findAll(tenantId, entityId, scope);
return Futures.immediateFuture(attributesDao.findAll(tenantId, entityId, scope));
}
@Override
@ -80,17 +80,16 @@ public class BaseAttributesService implements AttributesService {
}
@Override
public ListenableFuture<List<Void>> save(TenantId tenantId, EntityId entityId, String scope, List<AttributeKvEntry> attributes) {
public ListenableFuture<List<String>> save(TenantId tenantId, EntityId entityId, String scope, List<AttributeKvEntry> attributes) {
validate(entityId, scope);
attributes.forEach(attribute -> validate(attribute));
List<ListenableFuture<Void>> saveFutures = attributes.stream().map(attribute -> attributesDao.save(tenantId, entityId, scope, attribute)).collect(Collectors.toList());
attributes.forEach(AttributeUtils::validate);
List<ListenableFuture<String>> saveFutures = attributes.stream().map(attribute -> attributesDao.save(tenantId, entityId, scope, attribute)).collect(Collectors.toList());
return Futures.allAsList(saveFutures);
}
@Override
public ListenableFuture<List<Void>> removeAll(TenantId tenantId, EntityId entityId, String scope, List<String> attributeKeys) {
public ListenableFuture<List<String>> removeAll(TenantId tenantId, EntityId entityId, String scope, List<String> attributeKeys) {
validate(entityId, scope);
return attributesDao.removeAll(tenantId, entityId, scope, attributeKeys);
return Futures.allAsList(attributesDao.removeAll(tenantId, entityId, scope, attributeKeys));
}
}

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

@ -17,20 +17,21 @@ package org.thingsboard.server.dao.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 lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
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.cache.TbCacheValueWrapper;
import org.thingsboard.server.cache.TbTransactionalCache;
import org.thingsboard.server.common.data.EntityType;
import org.thingsboard.server.common.data.StringUtils;
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.AttributeKvEntry;
import org.thingsboard.server.common.data.kv.KvEntry;
import org.thingsboard.server.common.stats.DefaultCounter;
import org.thingsboard.server.common.stats.StatsFactory;
import org.thingsboard.server.dao.cache.CacheExecutorService;
@ -46,7 +47,6 @@ import java.util.Map;
import java.util.Objects;
import java.util.Optional;
import java.util.Set;
import java.util.concurrent.Executor;
import java.util.stream.Collectors;
import static org.thingsboard.server.dao.attributes.AttributeUtils.validate;
@ -60,22 +60,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 Executor cacheExecutor;
private final TbTransactionalCache<AttributeCacheKey, AttributeKvEntry> cache;
private ListeningExecutorService cacheExecutor;
@Value("${cache.type}")
private String cacheType;
public CachedAttributesService(AttributesDao attributesDao,
AttributesCacheWrapper cacheWrapper,
StatsFactory statsFactory,
CacheExecutorService cacheExecutorService) {
CacheExecutorService cacheExecutorService,
TbTransactionalCache<AttributeCacheKey, AttributeKvEntry> 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");
@ -88,37 +88,44 @@ public class CachedAttributesService implements AttributesService {
/**
* Will return:
* - for the <b>local</b> cache type (cache.type="coffeine"): directExecutor (run callback immediately in the same thread)
* - for the <b>remote</b> cache: dedicated thread pool for the cache IO calls to unblock any caller thread
* */
Executor getExecutor(String cacheType, CacheExecutorService cacheExecutorService) {
* - for the <b>local</b> cache type (cache.type="coffeine"): directExecutor (run callback immediately in the same thread)
* - for the <b>remote</b> cache: dedicated thread pool for the cache IO calls to unblock any caller thread
*/
ListeningExecutorService getExecutor(String cacheType, CacheExecutorService cacheExecutorService) {
if (StringUtils.isEmpty(cacheType) || LOCAL_CACHE_TYPE.equals(cacheType)) {
log.info("Going to use directExecutor for the local cache type {}", cacheType);
return MoreExecutors.directExecutor();
return MoreExecutors.newDirectExecutorService();
}
log.info("Going to use cacheExecutorService for the remote cache type {}", cacheType);
return cacheExecutorService;
return cacheExecutorService.executor();
}
@Override
public ListenableFuture<Optional<AttributeKvEntry>> 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);
TbCacheValueWrapper<AttributeKvEntry> cachedAttributeValue = cache.get(attributeCacheKey);
if (cachedAttributeValue != null) {
hitCounter.increment();
AttributeKvEntry cachedAttributeKvEntry = (AttributeKvEntry) cachedAttributeValue.get();
AttributeKvEntry cachedAttributeKvEntry = cachedAttributeValue.get();
return Futures.immediateFuture(Optional.ofNullable(cachedAttributeKvEntry));
} else {
missCounter.increment();
ListenableFuture<Optional<AttributeKvEntry>> result = attributesDao.find(tenantId, entityId, scope, attributeKey);
return Futures.transform(result, foundAttrKvEntry -> {
// TODO: think if it's a good idea to store 'empty' attributes
cacheWrapper.put(attributeCacheKey, foundAttrKvEntry.orElse(null));
return foundAttrKvEntry;
}, cacheExecutor);
return cacheExecutor.submit(() -> {
var cacheTransaction = cache.newTransactionForKey(attributeCacheKey);
try {
Optional<AttributeKvEntry> result = attributesDao.find(tenantId, entityId, scope, attributeKey);
cacheTransaction.putIfAbsent(attributeCacheKey, result.orElse(null));
cacheTransaction.commit();
return result;
} catch (Throwable e) {
cacheTransaction.rollback();
throw e;
}
});
}
}
@ -127,10 +134,10 @@ public class CachedAttributesService implements AttributesService {
validate(entityId, scope);
attributeKeys.forEach(attributeKey -> Validator.validateString(attributeKey, "Incorrect attribute key " + attributeKey));
Map<String, Cache.ValueWrapper> wrappedCachedAttributes = findCachedAttributes(entityId, scope, attributeKeys);
Map<String, TbCacheValueWrapper<AttributeKvEntry>> wrappedCachedAttributes = findCachedAttributes(entityId, scope, attributeKeys);
List<AttributeKvEntry> cachedAttributes = wrappedCachedAttributes.values().stream()
.map(wrappedCachedAttribute -> (AttributeKvEntry) wrappedCachedAttribute.get())
.map(TbCacheValueWrapper::get)
.filter(Objects::nonNull)
.collect(Collectors.toList());
if (wrappedCachedAttributes.size() == attributeKeys.size()) {
@ -140,15 +147,35 @@ public class CachedAttributesService implements AttributesService {
Set<String> notFoundAttributeKeys = new HashSet<>(attributeKeys);
notFoundAttributeKeys.removeAll(wrappedCachedAttributes.keySet());
ListenableFuture<List<AttributeKvEntry>> result = attributesDao.find(tenantId, entityId, scope, notFoundAttributeKeys);
return Futures.transform(result, foundInDbAttributes -> mergeDbAndCacheAttributes(entityId, scope, cachedAttributes, notFoundAttributeKeys, foundInDbAttributes), cacheExecutor);
List<AttributeCacheKey> notFoundKeys = notFoundAttributeKeys.stream().map(k -> new AttributeCacheKey(scope, entityId, k)).collect(Collectors.toList());
return cacheExecutor.submit(() -> {
var cacheTransaction = cache.newTransactionForKeys(notFoundKeys);
try {
List<AttributeKvEntry> result = attributesDao.find(tenantId, entityId, scope, notFoundAttributeKeys);
for (AttributeKvEntry foundInDbAttribute : result) {
AttributeCacheKey attributeCacheKey = new AttributeCacheKey(scope, entityId, foundInDbAttribute.getKey());
cacheTransaction.putIfAbsent(attributeCacheKey, foundInDbAttribute);
notFoundAttributeKeys.remove(foundInDbAttribute.getKey());
}
for (String key : notFoundAttributeKeys) {
cacheTransaction.putIfAbsent(new AttributeCacheKey(scope, entityId, key), null);
}
List<AttributeKvEntry> mergedAttributes = new ArrayList<>(cachedAttributes);
mergedAttributes.addAll(result);
cacheTransaction.commit();
return mergedAttributes;
} catch (Throwable e) {
cacheTransaction.rollback();
throw e;
}
});
}
private Map<String, Cache.ValueWrapper> findCachedAttributes(EntityId entityId, String scope, Collection<String> attributeKeys) {
Map<String, Cache.ValueWrapper> cachedAttributes = new HashMap<>();
private Map<String, TbCacheValueWrapper<AttributeKvEntry>> findCachedAttributes(EntityId entityId, String scope, Collection<String> attributeKeys) {
Map<String, TbCacheValueWrapper<AttributeKvEntry>> cachedAttributes = new HashMap<>();
for (String attributeKey : attributeKeys) {
Cache.ValueWrapper cachedAttributeValue = cacheWrapper.get(new AttributeCacheKey(scope, entityId, attributeKey));
var cachedAttributeValue = cache.get(new AttributeCacheKey(scope, entityId, attributeKey));
if (cachedAttributeValue != null) {
hitCounter.increment();
cachedAttributes.put(attributeKey, cachedAttributeValue);
@ -159,24 +186,10 @@ public class CachedAttributesService implements AttributesService {
return cachedAttributes;
}
private List<AttributeKvEntry> mergeDbAndCacheAttributes(EntityId entityId, String scope, List<AttributeKvEntry> cachedAttributes, Set<String> notFoundAttributeKeys, List<AttributeKvEntry> foundInDbAttributes) {
for (AttributeKvEntry foundInDbAttribute : foundInDbAttributes) {
AttributeCacheKey attributeCacheKey = new AttributeCacheKey(scope, entityId, foundInDbAttribute.getKey());
cacheWrapper.put(attributeCacheKey, foundInDbAttribute);
notFoundAttributeKeys.remove(foundInDbAttribute.getKey());
}
for (String key : notFoundAttributeKeys){
cacheWrapper.put(new AttributeCacheKey(scope, entityId, key), null);
}
List<AttributeKvEntry> mergedAttributes = new ArrayList<>(cachedAttributes);
mergedAttributes.addAll(foundInDbAttributes);
return mergedAttributes;
}
@Override
public ListenableFuture<List<AttributeKvEntry>> findAll(TenantId tenantId, EntityId entityId, String scope) {
validate(entityId, scope);
return attributesDao.findAll(tenantId, entityId, scope);
return Futures.immediateFuture(attributesDao.findAll(tenantId, entityId, scope));
}
@Override
@ -190,34 +203,30 @@ public class CachedAttributesService implements AttributesService {
}
@Override
public ListenableFuture<List<Void>> save(TenantId tenantId, EntityId entityId, String scope, List<AttributeKvEntry> attributes) {
public ListenableFuture<List<String>> save(TenantId tenantId, EntityId entityId, String scope, List<AttributeKvEntry> attributes) {
validate(entityId, scope);
attributes.forEach(AttributeUtils::validate);
List<ListenableFuture<Void>> saveFutures = attributes.stream().map(attribute -> attributesDao.save(tenantId, entityId, scope, attribute)).collect(Collectors.toList());
ListenableFuture<List<Void>> future = Futures.allAsList(saveFutures);
List<ListenableFuture<String>> futures = new ArrayList<>(attributes.size());
for (var attribute : attributes) {
ListenableFuture<String> future = attributesDao.save(tenantId, entityId, scope, attribute);
futures.add(Futures.transform(future, key -> {
cache.evictOrPut(new AttributeCacheKey(scope, entityId, key), attribute);
return key;
}, cacheExecutor));
}
// TODO: can do if (attributesCache.get() != null) attributesCache.put() instead, but will be more twice more requests to cache
List<String> attributeKeys = attributes.stream().map(KvEntry::getKey).collect(Collectors.toList());
future.addListener(() -> evictAttributesFromCache(tenantId, entityId, scope, attributeKeys), cacheExecutor);
return future;
return Futures.allAsList(futures);
}
@Override
public ListenableFuture<List<Void>> removeAll(TenantId tenantId, EntityId entityId, String scope, List<String> attributeKeys) {
public ListenableFuture<List<String>> removeAll(TenantId tenantId, EntityId entityId, String scope, List<String> attributeKeys) {
validate(entityId, scope);
ListenableFuture<List<Void>> future = attributesDao.removeAll(tenantId, entityId, scope, attributeKeys);
future.addListener(() -> evictAttributesFromCache(tenantId, entityId, scope, attributeKeys), cacheExecutor);
return future;
List<ListenableFuture<String>> futures = attributesDao.removeAll(tenantId, entityId, scope, attributeKeys);
return Futures.allAsList(futures.stream().map(future -> Futures.transform(future, key -> {
cache.evict(new AttributeCacheKey(scope, entityId, key));
return key;
}, cacheExecutor)).collect(Collectors.toList()));
}
private void evictAttributesFromCache(TenantId tenantId, EntityId entityId, String scope, List<String> attributeKeys) {
try {
for (String attributeKey : attributeKeys) {
cacheWrapper.evict(new AttributeCacheKey(scope, entityId, attributeKey));
}
} catch (Exception e) {
log.error("[{}][{}] Failed to remove values from cache.", tenantId, entityId, e);
}
}
}

64
dao/src/main/java/org/thingsboard/server/dao/cache/EntitiesCacheManagerImpl.java

@ -1,64 +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.cache;
import lombok.AllArgsConstructor;
import org.springframework.cache.Cache;
import org.springframework.cache.CacheManager;
import org.springframework.stereotype.Component;
import org.thingsboard.server.common.data.id.DeviceId;
import org.thingsboard.server.common.data.id.TenantId;
import java.util.Arrays;
import static org.thingsboard.server.common.data.CacheConstants.ASSET_CACHE;
import static org.thingsboard.server.common.data.CacheConstants.DEVICE_CACHE;
import static org.thingsboard.server.common.data.CacheConstants.EDGE_CACHE;
@Component
@AllArgsConstructor
public class EntitiesCacheManagerImpl implements EntitiesCacheManager {
private final CacheManager cacheManager;
@Override
public void removeDeviceFromCacheByName(TenantId tenantId, String name) {
Cache cache = cacheManager.getCache(DEVICE_CACHE);
cache.evict(Arrays.asList(tenantId, name));
}
@Override
public void removeDeviceFromCacheById(TenantId tenantId, DeviceId deviceId) {
if (deviceId == null) {
return;
}
Cache cache = cacheManager.getCache(DEVICE_CACHE);
cache.evict(Arrays.asList(tenantId, deviceId));
}
@Override
public void removeAssetFromCacheByName(TenantId tenantId, String name) {
Cache cache = cacheManager.getCache(ASSET_CACHE);
cache.evict(Arrays.asList(tenantId, name));
}
@Override
public void removeEdgeFromCacheByName(TenantId tenantId, String name) {
Cache cache = cacheManager.getCache(EDGE_CACHE);
cache.evict(Arrays.asList(tenantId, name));
}
}

46
dao/src/main/java/org/thingsboard/server/dao/cache/PreviousDeviceCredentialsIdKeyGenerator.java

@ -1,46 +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.cache;
import org.springframework.cache.interceptor.KeyGenerator;
import org.springframework.stereotype.Component;
import org.thingsboard.server.common.data.id.TenantId;
import org.thingsboard.server.common.data.security.DeviceCredentials;
import org.thingsboard.server.dao.device.DeviceCredentialsService;
import java.lang.reflect.Method;
import static org.thingsboard.server.common.data.CacheConstants.DEVICE_CREDENTIALS_CACHE;
@Component("previousDeviceCredentialsId")
public class PreviousDeviceCredentialsIdKeyGenerator implements KeyGenerator {
private static final String NOT_VALID_DEVICE = DEVICE_CREDENTIALS_CACHE + "_notValidDeviceCredentialsId";
@Override
public Object generate(Object o, Method method, Object... objects) {
DeviceCredentialsService deviceCredentialsService = (DeviceCredentialsService) o;
TenantId tenantId = (TenantId) objects[0];
DeviceCredentials deviceCredentials = (DeviceCredentials) objects[1];
if (deviceCredentials.getDeviceId() != null) {
DeviceCredentials oldDeviceCredentials = deviceCredentialsService.findDeviceCredentialsByDeviceId(tenantId, deviceCredentials.getDeviceId());
if (oldDeviceCredentials != null) {
return DEVICE_CREDENTIALS_CACHE + "_" + oldDeviceCredentials.getCredentialsId();
}
}
return NOT_VALID_DEVICE;
}
}

2
dao/src/main/java/org/thingsboard/server/dao/customer/CustomerServiceImpl.java

@ -20,6 +20,7 @@ import com.fasterxml.jackson.databind.ObjectMapper;
import com.google.common.util.concurrent.ListenableFuture;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Lazy;
import org.springframework.stereotype.Service;
import org.thingsboard.server.common.data.Customer;
import org.thingsboard.server.common.data.id.CustomerId;
@ -69,6 +70,7 @@ public class CustomerServiceImpl extends AbstractEntityService implements Custom
@Autowired
private DashboardService dashboardService;
@Lazy
@Autowired
private ApiUsageStateService apiUsageStateService;

33
dao/src/main/java/org/thingsboard/server/dao/device/DeviceCredentialsCaffeineCache.java

@ -0,0 +1,33 @@
/**
* 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.device;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.cache.CacheManager;
import org.springframework.stereotype.Service;
import org.thingsboard.server.common.data.CacheConstants;
import org.thingsboard.server.common.data.security.DeviceCredentials;
import org.thingsboard.server.cache.CaffeineTbTransactionalCache;
@ConditionalOnProperty(prefix = "cache", value = "type", havingValue = "caffeine", matchIfMissing = true)
@Service("DeviceCredentialsCache")
public class DeviceCredentialsCaffeineCache extends CaffeineTbTransactionalCache<String, DeviceCredentials> {
public DeviceCredentialsCaffeineCache(CacheManager cacheManager) {
super(cacheManager, CacheConstants.DEVICE_CREDENTIALS_CACHE);
}
}

18
application/src/test/java/org/thingsboard/server/service/ServiceSqlTestSuite.java → dao/src/main/java/org/thingsboard/server/dao/device/DeviceCredentialsEvictEvent.java

@ -13,18 +13,14 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.thingsboard.server.service;
package org.thingsboard.server.dao.device;
import org.junit.BeforeClass;
import org.junit.extensions.cpsuite.ClasspathSuite;
import org.junit.runner.RunWith;
import org.thingsboard.server.queue.memory.InMemoryStorage;
import lombok.Data;
@RunWith(ClasspathSuite.class)
@ClasspathSuite.ClassnameFilters({
"org.thingsboard.server.service.resource.sql.*Test",
"org.thingsboard.server.service.sql.*Test"
})
public class ServiceSqlTestSuite {
@Data
class DeviceCredentialsEvictEvent {
private final String newCedentialsId;
private final String oldCredentialsId;
}

35
dao/src/main/java/org/thingsboard/server/dao/device/DeviceCredentialsRedisCache.java

@ -0,0 +1,35 @@
/**
* 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.device;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.data.redis.connection.RedisConnectionFactory;
import org.springframework.stereotype.Service;
import org.thingsboard.server.cache.CacheSpecsMap;
import org.thingsboard.server.cache.TBRedisCacheConfiguration;
import org.thingsboard.server.common.data.CacheConstants;
import org.thingsboard.server.common.data.security.DeviceCredentials;
import org.thingsboard.server.cache.RedisTbTransactionalCache;
import org.thingsboard.server.cache.TbRedisSerializer;
@ConditionalOnProperty(prefix = "cache", value = "type", havingValue = "redis")
@Service("DeviceCredentialsCache")
public class DeviceCredentialsRedisCache extends RedisTbTransactionalCache<String, DeviceCredentials> {
public DeviceCredentialsRedisCache(TBRedisCacheConfiguration configuration, CacheSpecsMap cacheSpecsMap, RedisConnectionFactory connectionFactory) {
super(CacheConstants.DEVICE_CREDENTIALS_CACHE, cacheSpecsMap, connectionFactory, configuration, new TbRedisSerializer<>());
}
}

42
dao/src/main/java/org/thingsboard/server/dao/device/DeviceCredentialsServiceImpl.java

@ -15,15 +15,15 @@
*/
package org.thingsboard.server.dao.device;
import lombok.extern.slf4j.Slf4j;
import org.eclipse.leshan.core.SecurityMode;
import org.eclipse.leshan.core.util.SecurityUtil;
import org.hibernate.exception.ConstraintViolationException;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.cache.annotation.CacheEvict;
import org.springframework.cache.annotation.Cacheable;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Propagation;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.transaction.event.TransactionalEventListener;
import org.thingsboard.common.util.JacksonUtil;
import org.thingsboard.server.common.data.StringUtils;
import org.thingsboard.server.common.data.device.credentials.BasicMqttCredentials;
@ -41,18 +41,17 @@ import org.thingsboard.server.common.data.id.DeviceId;
import org.thingsboard.server.common.data.id.TenantId;
import org.thingsboard.server.common.data.security.DeviceCredentials;
import org.thingsboard.server.common.msg.EncryptionUtil;
import org.thingsboard.server.dao.entity.AbstractEntityService;
import org.thingsboard.server.dao.entity.AbstractCachedEntityService;
import org.thingsboard.server.dao.exception.DataValidationException;
import org.thingsboard.server.dao.exception.DeviceCredentialsValidationException;
import org.thingsboard.server.dao.service.DataValidator;
import static org.thingsboard.server.common.data.CacheConstants.DEVICE_CREDENTIALS_CACHE;
import static org.thingsboard.server.dao.service.Validator.validateId;
import static org.thingsboard.server.dao.service.Validator.validateString;
@Service
@Slf4j
public class DeviceCredentialsServiceImpl extends AbstractEntityService implements DeviceCredentialsService {
public class DeviceCredentialsServiceImpl extends AbstractCachedEntityService<String, DeviceCredentials, DeviceCredentialsEvictEvent> implements DeviceCredentialsService {
@Autowired
private DeviceCredentialsDao deviceCredentialsDao;
@ -60,6 +59,15 @@ public class DeviceCredentialsServiceImpl extends AbstractEntityService implemen
@Autowired
private DataValidator<DeviceCredentials> credentialsValidator;
@TransactionalEventListener(classes = DeviceCredentialsEvictEvent.class)
@Override
public void handleEvictEvent(DeviceCredentialsEvictEvent event) {
cache.evict(event.getNewCedentialsId());
if (StringUtils.isNotEmpty(event.getOldCredentialsId()) && !event.getNewCedentialsId().equals(event.getOldCredentialsId())) {
cache.evict(event.getOldCredentialsId());
}
}
@Override
public DeviceCredentials findDeviceCredentialsByDeviceId(TenantId tenantId, DeviceId deviceId) {
log.trace("Executing findDeviceCredentialsByDeviceId [{}]", deviceId);
@ -68,15 +76,15 @@ public class DeviceCredentialsServiceImpl extends AbstractEntityService implemen
}
@Override
@Cacheable(cacheNames = DEVICE_CREDENTIALS_CACHE, key = "'deviceCredentials_' + #credentialsId", unless = "#result == null")
public DeviceCredentials findDeviceCredentialsByCredentialsId(String credentialsId) {
log.trace("Executing findDeviceCredentialsByCredentialsId [{}]", credentialsId);
validateString(credentialsId, "Incorrect credentialsId " + credentialsId);
return deviceCredentialsDao.findByCredentialsId(TenantId.SYS_TENANT_ID, credentialsId);
return cache.getAndPutInTransaction(credentialsId,
() -> deviceCredentialsDao.findByCredentialsId(TenantId.SYS_TENANT_ID, credentialsId),
false);
}
@Override
@CacheEvict(cacheNames = DEVICE_CREDENTIALS_CACHE, keyGenerator = "previousDeviceCredentialsId", beforeInvocation = true)
public DeviceCredentials updateDeviceCredentials(TenantId tenantId, DeviceCredentials deviceCredentials) {
return saveOrUpdate(tenantId, deviceCredentials);
}
@ -93,9 +101,16 @@ public class DeviceCredentialsServiceImpl extends AbstractEntityService implemen
formatCredentials(deviceCredentials);
log.trace("Executing updateDeviceCredentials [{}]", deviceCredentials);
credentialsValidator.validate(deviceCredentials, id -> tenantId);
DeviceCredentials oldDeviceCredentials = null;
if (deviceCredentials.getDeviceId() != null) {
oldDeviceCredentials = deviceCredentialsDao.findByDeviceId(tenantId, deviceCredentials.getDeviceId().getId());
}
try {
return deviceCredentialsDao.saveAndFlush(tenantId, deviceCredentials);
var value = deviceCredentialsDao.saveAndFlush(tenantId, deviceCredentials);
publishEvictEvent(new DeviceCredentialsEvictEvent(value.getCredentialsId(), oldDeviceCredentials != null ? oldDeviceCredentials.getCredentialsId() : null));
return value;
} catch (Exception t) {
handleEvictEvent(new DeviceCredentialsEvictEvent(deviceCredentials.getCredentialsId(), oldDeviceCredentials != null ? oldDeviceCredentials.getCredentialsId() : null));
ConstraintViolationException e = extractConstraintViolationException(t).orElse(null);
if (e != null && e.getConstraintName() != null
&& (e.getConstraintName().equalsIgnoreCase("device_credentials_id_unq_key") || e.getConstraintName().equalsIgnoreCase("device_credentials_device_id_unq_key"))) {
@ -183,8 +198,7 @@ public class DeviceCredentialsServiceImpl extends AbstractEntityService implemen
deviceCredentials.setCredentialsValue(JacksonUtil.toString(lwM2MCredentials));
X509ClientCredential x509ClientConfig = (X509ClientCredential) clientCredentials;
if ((StringUtils.isNotBlank(x509ClientConfig.getCert()))) {
String sha3Hash = EncryptionUtil.getSha3Hash(x509ClientConfig.getCert());
credentialsId = sha3Hash;
credentialsId = EncryptionUtil.getSha3Hash(x509ClientConfig.getCert());
} else {
credentialsId = x509ClientConfig.getEndpoint();
}
@ -252,7 +266,7 @@ public class DeviceCredentialsServiceImpl extends AbstractEntityService implemen
throw new DeviceCredentialsValidationException("LwM2M client PSK key must be random sequence in hex encoding!");
}
if (pskKey.length()% 32 != 0 || pskKey.length() > 128) {
if (pskKey.length() % 32 != 0 || pskKey.length() > 128) {
throw new DeviceCredentialsValidationException("LwM2M client PSK key length = " + pskKey.length() + ". Key must be HexDec format: 32, 64, 128 characters!");
}
@ -368,10 +382,10 @@ public class DeviceCredentialsServiceImpl extends AbstractEntityService implemen
}
@Override
@CacheEvict(cacheNames = DEVICE_CREDENTIALS_CACHE, key = "'deviceCredentials_' + #deviceCredentials.credentialsId")
public void deleteDeviceCredentials(TenantId tenantId, DeviceCredentials deviceCredentials) {
log.trace("Executing deleteDeviceCredentials [{}]", deviceCredentials);
deviceCredentialsDao.removeById(tenantId, deviceCredentials.getUuidId());
publishEvictEvent(new DeviceCredentialsEvictEvent(deviceCredentials.getCredentialsId(), null));
}
}

63
dao/src/main/java/org/thingsboard/server/dao/device/DeviceProfileCacheKey.java

@ -0,0 +1,63 @@
/**
* 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.device;
import lombok.Data;
import org.thingsboard.server.common.data.id.DeviceProfileId;
import org.thingsboard.server.common.data.id.TenantId;
import java.io.Serializable;
@Data
public class DeviceProfileCacheKey implements Serializable {
private static final long serialVersionUID = 8220455917177676472L;
private final TenantId tenantId;
private final String name;
private final DeviceProfileId deviceProfileId;
private final boolean defaultProfile;
private DeviceProfileCacheKey(TenantId tenantId, String name, DeviceProfileId deviceProfileId, boolean defaultProfile) {
this.tenantId = tenantId;
this.name = name;
this.deviceProfileId = deviceProfileId;
this.defaultProfile = defaultProfile;
}
public static DeviceProfileCacheKey fromName(TenantId tenantId, String name) {
return new DeviceProfileCacheKey(tenantId, name, null, false);
}
public static DeviceProfileCacheKey fromId(DeviceProfileId id) {
return new DeviceProfileCacheKey(null, null, id, false);
}
public static DeviceProfileCacheKey defaultProfile(TenantId tenantId) {
return new DeviceProfileCacheKey(tenantId, null, null, true);
}
@Override
public String toString() {
if (deviceProfileId != null) {
return deviceProfileId.toString();
} else if (defaultProfile) {
return tenantId.toString();
} else {
return tenantId + "_" + name;
}
}
}

33
dao/src/main/java/org/thingsboard/server/dao/device/DeviceProfileCaffeineCache.java

@ -0,0 +1,33 @@
/**
* 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.device;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.cache.CacheManager;
import org.springframework.stereotype.Service;
import org.thingsboard.server.common.data.CacheConstants;
import org.thingsboard.server.common.data.DeviceProfile;
import org.thingsboard.server.cache.CaffeineTbTransactionalCache;
@ConditionalOnProperty(prefix = "cache", value = "type", havingValue = "caffeine", matchIfMissing = true)
@Service("DeviceProfileCache")
public class DeviceProfileCaffeineCache extends CaffeineTbTransactionalCache<DeviceProfileCacheKey, DeviceProfile> {
public DeviceProfileCaffeineCache(CacheManager cacheManager) {
super(cacheManager, CacheConstants.DEVICE_PROFILE_CACHE);
}
}

31
dao/src/main/java/org/thingsboard/server/dao/device/DeviceProfileEvictEvent.java

@ -0,0 +1,31 @@
/**
* 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.device;
import lombok.Data;
import org.thingsboard.server.common.data.id.DeviceProfileId;
import org.thingsboard.server.common.data.id.TenantId;
@Data
public class DeviceProfileEvictEvent {
private final TenantId tenantId;
private final String newName;
private final String oldName;
private final DeviceProfileId deviceProfileId;
private final boolean defaultProfile;
}

35
dao/src/main/java/org/thingsboard/server/dao/device/DeviceProfileRedisCache.java

@ -0,0 +1,35 @@
/**
* 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.device;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.data.redis.connection.RedisConnectionFactory;
import org.springframework.stereotype.Service;
import org.thingsboard.server.cache.CacheSpecsMap;
import org.thingsboard.server.cache.TBRedisCacheConfiguration;
import org.thingsboard.server.common.data.CacheConstants;
import org.thingsboard.server.common.data.DeviceProfile;
import org.thingsboard.server.cache.RedisTbTransactionalCache;
import org.thingsboard.server.cache.TbRedisSerializer;
@ConditionalOnProperty(prefix = "cache", value = "type", havingValue = "redis")
@Service("DeviceProfileCache")
public class DeviceProfileRedisCache extends RedisTbTransactionalCache<DeviceProfileCacheKey, DeviceProfile> {
public DeviceProfileRedisCache(TBRedisCacheConfiguration configuration, CacheSpecsMap cacheSpecsMap, RedisConnectionFactory connectionFactory) {
super(CacheConstants.DEVICE_PROFILE_CACHE, cacheSpecsMap, connectionFactory, configuration, new TbRedisSerializer<>());
}
}

115
dao/src/main/java/org/thingsboard/server/dao/device/DeviceProfileServiceImpl.java

@ -18,16 +18,17 @@ package org.thingsboard.server.dao.device;
import lombok.extern.slf4j.Slf4j;
import org.hibernate.exception.ConstraintViolationException;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.cache.Cache;
import org.springframework.cache.CacheManager;
import org.springframework.cache.annotation.Cacheable;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Propagation;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.transaction.event.TransactionalEventListener;
import org.thingsboard.server.common.data.Device;
import org.thingsboard.server.common.data.DeviceProfile;
import org.thingsboard.server.common.data.DeviceProfileInfo;
import org.thingsboard.server.common.data.DeviceProfileProvisionType;
import org.thingsboard.server.common.data.DeviceProfileType;
import org.thingsboard.server.common.data.DeviceTransportType;
import org.thingsboard.server.common.data.StringUtils;
import org.thingsboard.server.common.data.device.profile.DefaultDeviceProfileConfiguration;
import org.thingsboard.server.common.data.device.profile.DefaultDeviceProfileTransportConfiguration;
import org.thingsboard.server.common.data.device.profile.DeviceProfileData;
@ -36,27 +37,27 @@ import org.thingsboard.server.common.data.id.DeviceProfileId;
import org.thingsboard.server.common.data.id.TenantId;
import org.thingsboard.server.common.data.page.PageData;
import org.thingsboard.server.common.data.page.PageLink;
import org.thingsboard.server.dao.entity.AbstractEntityService;
import org.thingsboard.server.dao.entity.AbstractCachedEntityService;
import org.thingsboard.server.dao.exception.DataValidationException;
import org.thingsboard.server.dao.service.DataValidator;
import org.thingsboard.server.dao.service.PaginatedRemover;
import org.thingsboard.server.dao.service.Validator;
import java.util.Arrays;
import java.util.Collections;
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
import static org.thingsboard.server.common.data.CacheConstants.DEVICE_PROFILE_CACHE;
import static org.thingsboard.server.dao.service.Validator.validateId;
@Service
@Slf4j
public class DeviceProfileServiceImpl extends AbstractEntityService implements DeviceProfileService {
public class DeviceProfileServiceImpl extends AbstractCachedEntityService<DeviceProfileCacheKey, DeviceProfile, DeviceProfileEvictEvent> implements DeviceProfileService {
private static final String INCORRECT_TENANT_ID = "Incorrect tenantId ";
private static final String INCORRECT_DEVICE_PROFILE_ID = "Incorrect deviceProfileId ";
private static final String INCORRECT_DEVICE_PROFILE_NAME = "Incorrect deviceProfileName ";
private static final String DEVICE_PROFILE_WITH_SUCH_NAME_ALREADY_EXISTS = "Device profile with such name already exists!";
@Autowired
private DeviceProfileDao deviceProfileDao;
@ -67,66 +68,73 @@ public class DeviceProfileServiceImpl extends AbstractEntityService implements D
@Autowired
private DeviceService deviceService;
@Autowired
private CacheManager cacheManager;
@Autowired
private DataValidator<DeviceProfile> deviceProfileValidator;
private final Lock findOrCreateLock = new ReentrantLock();
@Cacheable(cacheNames = DEVICE_PROFILE_CACHE, key = "{#deviceProfileId.id}")
@TransactionalEventListener(classes = DeviceProfileEvictEvent.class)
@Override
public void handleEvictEvent(DeviceProfileEvictEvent event) {
List<DeviceProfileCacheKey> keys = new ArrayList<>(2);
keys.add(DeviceProfileCacheKey.fromName(event.getTenantId(), event.getNewName()));
if (event.getDeviceProfileId() != null) {
keys.add(DeviceProfileCacheKey.fromId(event.getDeviceProfileId()));
}
if (event.isDefaultProfile()) {
keys.add(DeviceProfileCacheKey.defaultProfile(event.getTenantId()));
}
if (StringUtils.isNotEmpty(event.getOldName()) && !event.getOldName().equals(event.getNewName())) {
keys.add(DeviceProfileCacheKey.fromName(event.getTenantId(), event.getOldName()));
}
cache.evict(keys);
}
@Override
public DeviceProfile findDeviceProfileById(TenantId tenantId, DeviceProfileId deviceProfileId) {
log.trace("Executing findDeviceProfileById [{}]", deviceProfileId);
Validator.validateId(deviceProfileId, INCORRECT_DEVICE_PROFILE_ID + deviceProfileId);
return deviceProfileDao.findById(tenantId, deviceProfileId.getId());
return cache.getAndPutInTransaction(DeviceProfileCacheKey.fromId(deviceProfileId),
() -> deviceProfileDao.findById(tenantId, deviceProfileId.getId()), true);
}
@Override
public DeviceProfile findDeviceProfileByName(TenantId tenantId, String profileName) {
log.trace("Executing findDeviceProfileByName [{}][{}]", tenantId, profileName);
Validator.validateString(profileName, INCORRECT_DEVICE_PROFILE_NAME + profileName);
return deviceProfileDao.findByName(tenantId, profileName);
return cache.getAndPutInTransaction(DeviceProfileCacheKey.fromName(tenantId, profileName),
() -> deviceProfileDao.findByName(tenantId, profileName), true);
}
@Cacheable(cacheNames = DEVICE_PROFILE_CACHE, key = "{'info', #deviceProfileId.id}")
@Override
public DeviceProfileInfo findDeviceProfileInfoById(TenantId tenantId, DeviceProfileId deviceProfileId) {
log.trace("Executing findDeviceProfileById [{}]", deviceProfileId);
Validator.validateId(deviceProfileId, INCORRECT_DEVICE_PROFILE_ID + deviceProfileId);
return deviceProfileDao.findDeviceProfileInfoById(tenantId, deviceProfileId.getId());
return toDeviceProfileInfo(findDeviceProfileById(tenantId, deviceProfileId));
}
@Override
public DeviceProfile saveDeviceProfile(DeviceProfile deviceProfile) {
log.trace("Executing saveDeviceProfile [{}]", deviceProfile);
deviceProfileValidator.validate(deviceProfile, DeviceProfile::getTenantId);
DeviceProfile oldDeviceProfile = null;
if (deviceProfile.getId() != null) {
oldDeviceProfile = deviceProfileDao.findById(deviceProfile.getTenantId(), deviceProfile.getId().getId());
}
DeviceProfile oldDeviceProfile = deviceProfileValidator.validate(deviceProfile, DeviceProfile::getTenantId);
DeviceProfile savedDeviceProfile;
try {
savedDeviceProfile = deviceProfileDao.saveAndFlush(deviceProfile.getTenantId(), deviceProfile);
publishEvictEvent(new DeviceProfileEvictEvent(savedDeviceProfile.getTenantId(), savedDeviceProfile.getName(),
oldDeviceProfile != null ? oldDeviceProfile.getName() : null, savedDeviceProfile.getId(), savedDeviceProfile.isDefault()));
} catch (Exception t) {
handleEvictEvent(new DeviceProfileEvictEvent(deviceProfile.getTenantId(), deviceProfile.getName(),
oldDeviceProfile != null ? oldDeviceProfile.getName() : null, null, deviceProfile.isDefault()));
ConstraintViolationException e = extractConstraintViolationException(t).orElse(null);
if (e != null && e.getConstraintName() != null && e.getConstraintName().equalsIgnoreCase("device_profile_name_unq_key")) {
throw new DataValidationException("Device profile with such name already exists!");
//TODO: refactor this to return existing device profile. If they are equal - no need to throw exception. Then we can make this call @Transactional and tests will not fail.
throw new DataValidationException(DEVICE_PROFILE_WITH_SUCH_NAME_ALREADY_EXISTS);
} else if (e != null && e.getConstraintName() != null && e.getConstraintName().equalsIgnoreCase("device_provision_key_unq_key")) {
throw new DataValidationException("Device profile with such provision device key already exists!");
} else {
throw t;
}
}
Cache cache = cacheManager.getCache(DEVICE_PROFILE_CACHE);
cache.evict(Collections.singletonList(savedDeviceProfile.getId().getId()));
cache.evict(Arrays.asList("info", savedDeviceProfile.getId().getId()));
cache.evict(Arrays.asList(deviceProfile.getTenantId().getId(), deviceProfile.getName()));
if (savedDeviceProfile.isDefault()) {
cache.evict(Arrays.asList("default", savedDeviceProfile.getTenantId().getId()));
cache.evict(Arrays.asList("default", "info", savedDeviceProfile.getTenantId().getId()));
}
if (oldDeviceProfile != null && !oldDeviceProfile.getName().equals(deviceProfile.getName())) {
PageLink pageLink = new PageLink(100);
PageData<Device> pageData;
@ -157,6 +165,8 @@ public class DeviceProfileServiceImpl extends AbstractEntityService implements D
DeviceProfileId deviceProfileId = deviceProfile.getId();
try {
deviceProfileDao.removeById(tenantId, deviceProfileId.getId());
publishEvictEvent(new DeviceProfileEvictEvent(deviceProfile.getTenantId(), deviceProfile.getName(),
null, deviceProfile.getId(), deviceProfile.isDefault()));
} catch (Exception t) {
ConstraintViolationException e = extractConstraintViolationException(t).orElse(null);
if (e != null && e.getConstraintName() != null && e.getConstraintName().equalsIgnoreCase("fk_device_profile")) {
@ -166,10 +176,6 @@ public class DeviceProfileServiceImpl extends AbstractEntityService implements D
}
}
deleteEntityRelations(tenantId, deviceProfileId);
Cache cache = cacheManager.getCache(DEVICE_PROFILE_CACHE);
cache.evict(Collections.singletonList(deviceProfileId.getId()));
cache.evict(Arrays.asList("info", deviceProfileId.getId()));
cache.evict(Arrays.asList(tenantId.getId(), deviceProfile.getName()));
}
@Override
@ -188,20 +194,19 @@ public class DeviceProfileServiceImpl extends AbstractEntityService implements D
return deviceProfileDao.findDeviceProfileInfos(tenantId, pageLink, transportType);
}
@Cacheable(cacheNames = DEVICE_PROFILE_CACHE, key = "{#tenantId.id, #name}")
@Override
public DeviceProfile findOrCreateDeviceProfile(TenantId tenantId, String name) {
log.trace("Executing findOrCreateDefaultDeviceProfile");
DeviceProfile deviceProfile = findDeviceProfileByName(tenantId, name);
if (deviceProfile == null) {
findOrCreateLock.lock();
try {
deviceProfile = findDeviceProfileByName(tenantId, name);
if (deviceProfile == null) {
deviceProfile = this.doCreateDefaultDeviceProfile(tenantId, name, name.equals("default"));
deviceProfile = this.doCreateDefaultDeviceProfile(tenantId, name, name.equals("default"));
} catch (DataValidationException e) {
if (DEVICE_PROFILE_WITH_SUCH_NAME_ALREADY_EXISTS.equals(e.getMessage())) {
deviceProfile = findDeviceProfileByName(tenantId, name);
} else {
throw e;
}
} finally {
findOrCreateLock.unlock();
}
}
return deviceProfile;
@ -234,20 +239,19 @@ public class DeviceProfileServiceImpl extends AbstractEntityService implements D
return saveDeviceProfile(deviceProfile);
}
@Cacheable(cacheNames = DEVICE_PROFILE_CACHE, key = "{'default', #tenantId.id}")
@Override
public DeviceProfile findDefaultDeviceProfile(TenantId tenantId) {
log.trace("Executing findDefaultDeviceProfile tenantId [{}]", tenantId);
validateId(tenantId, INCORRECT_TENANT_ID + tenantId);
return deviceProfileDao.findDefaultDeviceProfile(tenantId);
return cache.getAndPutInTransaction(DeviceProfileCacheKey.defaultProfile(tenantId),
() -> deviceProfileDao.findDefaultDeviceProfile(tenantId), true);
}
@Cacheable(cacheNames = DEVICE_PROFILE_CACHE, key = "{'default', 'info', #tenantId.id}")
@Override
public DeviceProfileInfo findDefaultDeviceProfileInfo(TenantId tenantId) {
log.trace("Executing findDefaultDeviceProfileInfo tenantId [{}]", tenantId);
validateId(tenantId, INCORRECT_TENANT_ID + tenantId);
return deviceProfileDao.findDefaultDeviceProfileInfo(tenantId);
return toDeviceProfileInfo(findDefaultDeviceProfile(tenantId));
}
@Override
@ -256,29 +260,21 @@ public class DeviceProfileServiceImpl extends AbstractEntityService implements D
Validator.validateId(deviceProfileId, INCORRECT_DEVICE_PROFILE_ID + deviceProfileId);
DeviceProfile deviceProfile = deviceProfileDao.findById(tenantId, deviceProfileId.getId());
if (!deviceProfile.isDefault()) {
Cache cache = cacheManager.getCache(DEVICE_PROFILE_CACHE);
deviceProfile.setDefault(true);
DeviceProfile previousDefaultDeviceProfile = findDefaultDeviceProfile(tenantId);
boolean changed = false;
if (previousDefaultDeviceProfile == null) {
deviceProfileDao.save(tenantId, deviceProfile);
publishEvictEvent(new DeviceProfileEvictEvent(deviceProfile.getTenantId(), deviceProfile.getName(), null, deviceProfile.getId(), true));
changed = true;
} else if (!previousDefaultDeviceProfile.getId().equals(deviceProfile.getId())) {
previousDefaultDeviceProfile.setDefault(false);
deviceProfileDao.save(tenantId, previousDefaultDeviceProfile);
deviceProfileDao.save(tenantId, deviceProfile);
cache.evict(Collections.singletonList(previousDefaultDeviceProfile.getId().getId()));
cache.evict(Arrays.asList("info", previousDefaultDeviceProfile.getId().getId()));
cache.evict(Arrays.asList(tenantId.getId(), previousDefaultDeviceProfile.getName()));
publishEvictEvent(new DeviceProfileEvictEvent(previousDefaultDeviceProfile.getTenantId(), previousDefaultDeviceProfile.getName(), null, previousDefaultDeviceProfile.getId(), false));
publishEvictEvent(new DeviceProfileEvictEvent(deviceProfile.getTenantId(), deviceProfile.getName(), null, deviceProfile.getId(), true));
changed = true;
}
if (changed) {
cache.evict(Collections.singletonList(deviceProfile.getId().getId()));
cache.evict(Arrays.asList("info", deviceProfile.getId().getId()));
cache.evict(Arrays.asList("default", tenantId.getId()));
cache.evict(Arrays.asList("default", "info", tenantId.getId()));
cache.evict(Arrays.asList(tenantId.getId(), deviceProfile.getName()));
}
return changed;
}
return false;
@ -292,7 +288,7 @@ public class DeviceProfileServiceImpl extends AbstractEntityService implements D
}
private PaginatedRemover<TenantId, DeviceProfile> tenantDeviceProfilesRemover =
new PaginatedRemover<TenantId, DeviceProfile>() {
new PaginatedRemover<>() {
@Override
protected PageData<DeviceProfile> findEntities(TenantId tenantId, TenantId id, PageLink pageLink) {
@ -305,4 +301,9 @@ public class DeviceProfileServiceImpl extends AbstractEntityService implements D
}
};
private DeviceProfileInfo toDeviceProfileInfo(DeviceProfile profile) {
return profile == null ? null : new DeviceProfileInfo(profile.getId(), profile.getName(), profile.getImage(),
profile.getDefaultDashboardId(), profile.getType(), profile.getTransportType());
}
}

126
dao/src/main/java/org/thingsboard/server/dao/device/DeviceServiceImpl.java

@ -23,21 +23,22 @@ import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.RandomStringUtils;
import org.hibernate.exception.ConstraintViolationException;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.cache.annotation.CacheEvict;
import org.springframework.cache.annotation.Cacheable;
import org.springframework.cache.annotation.Caching;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.transaction.event.TransactionalEventListener;
import org.springframework.util.CollectionUtils;
import org.springframework.util.StringUtils;
import org.thingsboard.common.util.JacksonUtil;
import org.thingsboard.server.cache.device.DeviceCacheEvictEvent;
import org.thingsboard.server.cache.device.DeviceCacheKey;
import org.thingsboard.server.common.data.Device;
import org.thingsboard.server.common.data.DeviceInfo;
import org.thingsboard.server.common.data.DeviceProfile;
import org.thingsboard.server.common.data.DeviceProfileType;
import org.thingsboard.server.common.data.DeviceTransportType;
import org.thingsboard.server.common.data.EntitySubtype;
import org.thingsboard.server.common.data.EntityType;
import org.thingsboard.server.common.data.EntityView;
import org.thingsboard.server.common.data.StringUtils;
import org.thingsboard.server.common.data.device.DeviceSearchQuery;
import org.thingsboard.server.common.data.device.credentials.BasicMqttCredentials;
import org.thingsboard.server.common.data.device.data.CoapDeviceTransportConfiguration;
@ -62,11 +63,10 @@ import org.thingsboard.server.common.data.relation.EntitySearchDirection;
import org.thingsboard.server.common.data.relation.RelationTypeGroup;
import org.thingsboard.server.common.data.security.DeviceCredentials;
import org.thingsboard.server.common.data.security.DeviceCredentialsType;
import org.thingsboard.server.dao.cache.EntitiesCacheManager;
import org.thingsboard.server.dao.device.provision.ProvisionFailedException;
import org.thingsboard.server.dao.device.provision.ProvisionRequest;
import org.thingsboard.server.dao.device.provision.ProvisionResponseStatus;
import org.thingsboard.server.dao.entity.AbstractEntityService;
import org.thingsboard.server.dao.entity.AbstractCachedEntityService;
import org.thingsboard.server.dao.event.EventService;
import org.thingsboard.server.dao.exception.DataValidationException;
import org.thingsboard.server.dao.service.DataValidator;
@ -77,12 +77,10 @@ import java.util.ArrayList;
import java.util.Collections;
import java.util.Comparator;
import java.util.List;
import java.util.Optional;
import java.util.UUID;
import java.util.concurrent.ExecutionException;
import java.util.stream.Collectors;
import static org.thingsboard.server.common.data.CacheConstants.DEVICE_CACHE;
import static org.thingsboard.server.dao.DaoUtil.toUUIDs;
import static org.thingsboard.server.dao.service.Validator.validateId;
import static org.thingsboard.server.dao.service.Validator.validateIds;
@ -91,7 +89,7 @@ import static org.thingsboard.server.dao.service.Validator.validateString;
@Service
@Slf4j
public class DeviceServiceImpl extends AbstractEntityService implements DeviceService {
public class DeviceServiceImpl extends AbstractCachedEntityService<DeviceCacheKey, Device, DeviceCacheEvictEvent> implements DeviceService {
public static final String INCORRECT_TENANT_ID = "Incorrect tenantId ";
public static final String INCORRECT_DEVICE_PROFILE_ID = "Incorrect deviceProfileId ";
@ -109,9 +107,6 @@ public class DeviceServiceImpl extends AbstractEntityService implements DeviceSe
@Autowired
private DeviceProfileService deviceProfileService;
@Autowired
private EntitiesCacheManager cacheManager;
@Autowired
private EventService eventService;
@ -125,16 +120,19 @@ public class DeviceServiceImpl extends AbstractEntityService implements DeviceSe
return deviceDao.findDeviceInfoById(tenantId, deviceId.getId());
}
@Cacheable(cacheNames = DEVICE_CACHE, key = "{#tenantId, #deviceId}")
@Override
public Device findDeviceById(TenantId tenantId, DeviceId deviceId) {
log.trace("Executing findDeviceById [{}]", deviceId);
validateId(deviceId, INCORRECT_DEVICE_ID + deviceId);
if (TenantId.SYS_TENANT_ID.equals(tenantId)) {
return deviceDao.findById(tenantId, deviceId.getId());
} else {
return deviceDao.findDeviceByTenantIdAndId(tenantId, deviceId.getId());
}
return cache.getAndPutInTransaction(new DeviceCacheKey(tenantId, deviceId),
() -> {
//TODO: possible bug source since sometimes we need to clear cache by tenant id and sometimes by sys tenant id?
if (TenantId.SYS_TENANT_ID.equals(tenantId)) {
return deviceDao.findById(tenantId, deviceId.getId());
} else {
return deviceDao.findDeviceByTenantIdAndId(tenantId, deviceId.getId());
}
}, true);
}
@Override
@ -148,47 +146,29 @@ public class DeviceServiceImpl extends AbstractEntityService implements DeviceSe
}
}
@Cacheable(cacheNames = DEVICE_CACHE, key = "{#tenantId, #name}")
@Override
public Device findDeviceByTenantIdAndName(TenantId tenantId, String name) {
log.trace("Executing findDeviceByTenantIdAndName [{}][{}]", tenantId, name);
validateId(tenantId, INCORRECT_TENANT_ID + tenantId);
Optional<Device> deviceOpt = deviceDao.findDeviceByTenantIdAndName(tenantId.getId(), name);
return deviceOpt.orElse(null);
return cache.getAndPutInTransaction(new DeviceCacheKey(tenantId, name),
() -> deviceDao.findDeviceByTenantIdAndName(tenantId.getId(), name).orElse(null), true);
}
@Caching(evict= {
@CacheEvict(cacheNames = DEVICE_CACHE, key = "{#device.tenantId, #device.name}"),
@CacheEvict(cacheNames = DEVICE_CACHE, key = "{#device.tenantId, #device.id}")
})
@Transactional
@Override
public Device saveDeviceWithAccessToken(Device device, String accessToken) {
return doSaveDevice(device, accessToken, true);
}
@Caching(evict= {
@CacheEvict(cacheNames = DEVICE_CACHE, key = "{#device.tenantId, #device.name}"),
@CacheEvict(cacheNames = DEVICE_CACHE, key = "{#device.tenantId, #device.id}")
})
@Override
public Device saveDevice(Device device, boolean doValidate) {
return doSaveDevice(device, null, doValidate);
}
@Caching(evict= {
@CacheEvict(cacheNames = DEVICE_CACHE, key = "{#device.tenantId, #device.name}"),
@CacheEvict(cacheNames = DEVICE_CACHE, key = "{#device.tenantId, #device.id}")
})
@Override
public Device saveDevice(Device device) {
return doSaveDevice(device, null, true);
}
@Caching(evict= {
@CacheEvict(cacheNames = DEVICE_CACHE, key = "{#device.tenantId, #device.name}"),
@CacheEvict(cacheNames = DEVICE_CACHE, key = "{#device.tenantId, #device.id}")
})
@Transactional
@Override
public Device saveDeviceWithCredentials(Device device, DeviceCredentials deviceCredentials) {
@ -226,9 +206,13 @@ public class DeviceServiceImpl extends AbstractEntityService implements DeviceSe
private Device saveDeviceWithoutCredentials(Device device, boolean doValidate) {
log.trace("Executing saveDevice [{}]", device);
Device oldDevice = null;
if (doValidate) {
deviceValidator.validate(device, Device::getTenantId);
oldDevice = deviceValidator.validate(device, Device::getTenantId);
} else if (device.getId() != null) {
oldDevice = findDeviceById(device.getTenantId(), device.getId());
}
DeviceCacheEvictEvent deviceCacheEvictEvent = new DeviceCacheEvictEvent(device.getTenantId(), device.getId(), device.getName(), oldDevice != null ? oldDevice.getName() : null);
try {
DeviceProfile deviceProfile;
if (device.getDeviceProfileId() == null) {
@ -246,13 +230,13 @@ public class DeviceServiceImpl extends AbstractEntityService implements DeviceSe
}
device.setType(deviceProfile.getName());
device.setDeviceData(syncDeviceData(deviceProfile, device.getDeviceData()));
return deviceDao.saveAndFlush(device.getTenantId(), device);
Device result = deviceDao.saveAndFlush(device.getTenantId(), device);
publishEvictEvent(deviceCacheEvictEvent);
return result;
} catch (Exception t) {
handleEvictEvent(deviceCacheEvictEvent);
ConstraintViolationException e = extractConstraintViolationException(t).orElse(null);
if (e != null && e.getConstraintName() != null && e.getConstraintName().equalsIgnoreCase("device_name_unq_key")) {
// remove device from cache in case null value cached in the distributed redis.
cacheManager.removeDeviceFromCacheByName(device.getTenantId(), device.getName());
cacheManager.removeDeviceFromCacheById(device.getTenantId(), device.getId());
throw new DataValidationException("Device with such name already exists!");
} else {
throw t;
@ -260,15 +244,27 @@ public class DeviceServiceImpl extends AbstractEntityService implements DeviceSe
}
}
@TransactionalEventListener(classes = DeviceCacheEvictEvent.class)
@Override
public void handleEvictEvent(DeviceCacheEvictEvent event) {
List<DeviceCacheKey> keys = new ArrayList<>(3);
keys.add(new DeviceCacheKey(event.getTenantId(), event.getNewName()));
if (event.getDeviceId() != null) {
keys.add(new DeviceCacheKey(event.getTenantId(), event.getDeviceId()));
}
if (StringUtils.isNotEmpty(event.getOldName()) && !event.getOldName().equals(event.getNewName())) {
keys.add(new DeviceCacheKey(event.getTenantId(), event.getOldName()));
}
cache.evict(keys);
}
private DeviceData syncDeviceData(DeviceProfile deviceProfile, DeviceData deviceData) {
if (deviceData == null) {
deviceData = new DeviceData();
}
if (deviceData.getConfiguration() == null || !deviceProfile.getType().equals(deviceData.getConfiguration().getType())) {
switch (deviceProfile.getType()) {
case DEFAULT:
deviceData.setConfiguration(new DefaultDeviceConfiguration());
break;
if (deviceProfile.getType() == DeviceProfileType.DEFAULT) {
deviceData.setConfiguration(new DefaultDeviceConfiguration());
}
}
if (deviceData.getTransportConfiguration() == null || !deviceProfile.getTransportType().equals(deviceData.getTransportConfiguration().getType())) {
@ -293,24 +289,20 @@ public class DeviceServiceImpl extends AbstractEntityService implements DeviceSe
return deviceData;
}
@Transactional
@Override
public Device assignDeviceToCustomer(TenantId tenantId, DeviceId deviceId, CustomerId customerId) {
Device device = findDeviceById(tenantId, deviceId);
device.setCustomerId(customerId);
Device savedDevice = saveDevice(device);
cacheManager.removeDeviceFromCacheByName(tenantId, device.getName());
cacheManager.removeDeviceFromCacheById(tenantId, device.getId());
return savedDevice;
return saveDevice(device);
}
@Transactional
@Override
public Device unassignDeviceFromCustomer(TenantId tenantId, DeviceId deviceId) {
Device device = findDeviceById(tenantId, deviceId);
device.setCustomerId(null);
Device savedDevice = saveDevice(device);
cacheManager.removeDeviceFromCacheByName(tenantId, device.getName());
cacheManager.removeDeviceFromCacheById(tenantId, device.getId());
return savedDevice;
return saveDevice(device);
}
@Transactional
@ -320,7 +312,7 @@ public class DeviceServiceImpl extends AbstractEntityService implements DeviceSe
validateId(deviceId, INCORRECT_DEVICE_ID + deviceId);
Device device = deviceDao.findById(tenantId, deviceId.getId());
final String deviceName = device.getName();
DeviceCacheEvictEvent deviceCacheEvictEvent = new DeviceCacheEvictEvent(device.getTenantId(), device.getId(), device.getName(), null);
try {
List<EntityView> entityViews = entityViewService.findEntityViewsByTenantIdAndEntityIdAsync(device.getTenantId(), deviceId).get();
if (entityViews != null && !entityViews.isEmpty()) {
@ -339,12 +331,10 @@ public class DeviceServiceImpl extends AbstractEntityService implements DeviceSe
deviceDao.removeById(tenantId, deviceId.getId());
cacheManager.removeDeviceFromCacheByName(tenantId, deviceName);
cacheManager.removeDeviceFromCacheById(tenantId, deviceId);
publishEvictEvent(deviceCacheEvictEvent);
}
@Override
public PageData<Device> findDevicesByTenantId(TenantId tenantId, PageLink pageLink) {
log.trace("Executing findDevicesByTenantId, tenantId [{}], pageLink [{}]", tenantId, pageLink);
@ -417,7 +407,7 @@ public class DeviceServiceImpl extends AbstractEntityService implements DeviceSe
return deviceDao.findDevicesByTenantIdAndIdsAsync(tenantId.getId(), toUUIDs(deviceIds));
}
@Transactional
@Override
public void deleteDevicesByTenantId(TenantId tenantId) {
log.trace("Executing deleteDevicesByTenantId, tenantId [{}]", tenantId);
@ -506,7 +496,7 @@ public class DeviceServiceImpl extends AbstractEntityService implements DeviceSe
return Futures.successfulAsList(futures);
}, MoreExecutors.directExecutor());
devices = Futures.transform(devices, new Function<List<Device>, List<Device>>() {
devices = Futures.transform(devices, new Function<>() {
@Nullable
@Override
public List<Device> apply(@Nullable List<Device> deviceList) {
@ -533,7 +523,6 @@ public class DeviceServiceImpl extends AbstractEntityService implements DeviceSe
@Override
public Device assignDeviceToTenant(TenantId tenantId, Device device) {
log.trace("Executing assignDeviceToTenant [{}][{}]", tenantId, device);
try {
List<EntityView> entityViews = entityViewService.findEntityViewsByTenantIdAndEntityIdAsync(device.getTenantId(), device.getId()).get();
if (!CollectionUtils.isEmpty(entityViews)) {
@ -554,16 +543,18 @@ public class DeviceServiceImpl extends AbstractEntityService implements DeviceSe
device.setCustomerId(null);
Device savedDevice = doSaveDevice(device, null, true);
DeviceCacheEvictEvent oldTenantEvent = new DeviceCacheEvictEvent(oldTenantId, device.getId(), device.getName(), null);
DeviceCacheEvictEvent newTenantEvent = new DeviceCacheEvictEvent(savedDevice.getTenantId(), device.getId(), device.getName(), null);
// explicitly remove device with previous tenant id from cache
// result device object will have different tenant id and will not remove entity from cache
cacheManager.removeDeviceFromCacheByName(oldTenantId, device.getName());
cacheManager.removeDeviceFromCacheById(oldTenantId, device.getId());
publishEvictEvent(oldTenantEvent);
publishEvictEvent(newTenantEvent);
return savedDevice;
}
@Override
@CacheEvict(cacheNames = DEVICE_CACHE, key = "{#profile.tenantId, #provisionRequest.deviceName}")
@Transactional
public Device saveDevice(ProvisionRequest provisionRequest, DeviceProfile profile) {
Device device = new Device();
@ -605,7 +596,8 @@ public class DeviceServiceImpl extends AbstractEntityService implements DeviceSe
throw new ProvisionFailedException(ProvisionResponseStatus.FAILURE.name());
}
}
cacheManager.removeDeviceFromCacheById(savedDevice.getTenantId(), savedDevice.getId()); // eviction by name is described as annotation @CacheEvict above
publishEvictEvent(new DeviceCacheEvictEvent(savedDevice.getTenantId(), savedDevice.getId(), provisionRequest.getDeviceName(), null));
return savedDevice;
}
@ -677,7 +669,7 @@ public class DeviceServiceImpl extends AbstractEntityService implements DeviceSe
}
private PaginatedRemover<TenantId, Device> tenantDevicesRemover =
new PaginatedRemover<TenantId, Device>() {
new PaginatedRemover<>() {
@Override
protected PageData<Device> findEntities(TenantId tenantId, TenantId id, PageLink pageLink) {

30
dao/src/main/java/org/thingsboard/server/dao/edge/EdgeCacheEvictEvent.java

@ -0,0 +1,30 @@
/**
* 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.edge;
import lombok.Data;
import lombok.RequiredArgsConstructor;
import org.thingsboard.server.common.data.id.TenantId;
@Data
@RequiredArgsConstructor
class EdgeCacheEvictEvent {
private final TenantId tenantId;
private final String newName;
private final String oldName;
}

40
dao/src/main/java/org/thingsboard/server/dao/edge/EdgeCacheKey.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.edge;
import lombok.Builder;
import lombok.EqualsAndHashCode;
import lombok.Getter;
import lombok.RequiredArgsConstructor;
import org.thingsboard.server.common.data.id.TenantId;
import java.io.Serializable;
@Getter
@EqualsAndHashCode
@RequiredArgsConstructor
@Builder
public class EdgeCacheKey implements Serializable {
private final TenantId tenantId;
private final String name;
@Override
public String toString() {
return tenantId + "_" + name;
}
}

33
dao/src/main/java/org/thingsboard/server/dao/edge/EdgeCaffeineCache.java

@ -0,0 +1,33 @@
/**
* 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.edge;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.cache.CacheManager;
import org.springframework.stereotype.Service;
import org.thingsboard.server.common.data.CacheConstants;
import org.thingsboard.server.common.data.edge.Edge;
import org.thingsboard.server.cache.CaffeineTbTransactionalCache;
@ConditionalOnProperty(prefix = "cache", value = "type", havingValue = "caffeine", matchIfMissing = true)
@Service("EdgeCache")
public class EdgeCaffeineCache extends CaffeineTbTransactionalCache<EdgeCacheKey, Edge> {
public EdgeCaffeineCache(CacheManager cacheManager) {
super(cacheManager, CacheConstants.EDGE_CACHE);
}
}

35
dao/src/main/java/org/thingsboard/server/dao/edge/EdgeRedisCache.java

@ -0,0 +1,35 @@
/**
* 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.edge;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.data.redis.connection.RedisConnectionFactory;
import org.springframework.stereotype.Service;
import org.thingsboard.server.cache.CacheSpecsMap;
import org.thingsboard.server.cache.TBRedisCacheConfiguration;
import org.thingsboard.server.common.data.CacheConstants;
import org.thingsboard.server.common.data.edge.Edge;
import org.thingsboard.server.cache.RedisTbTransactionalCache;
import org.thingsboard.server.cache.TbRedisSerializer;
@ConditionalOnProperty(prefix = "cache", value = "type", havingValue = "redis")
@Service("EdgeCache")
public class EdgeRedisCache extends RedisTbTransactionalCache<EdgeCacheKey, Edge> {
public EdgeRedisCache(TBRedisCacheConfiguration configuration, CacheSpecsMap cacheSpecsMap, RedisConnectionFactory connectionFactory) {
super(CacheConstants.EDGE_CACHE, cacheSpecsMap, connectionFactory, configuration, new TbRedisSerializer<>());
}
}

45
dao/src/main/java/org/thingsboard/server/dao/edge/EdgeServiceImpl.java

@ -25,11 +25,13 @@ import com.google.common.util.concurrent.MoreExecutors;
import lombok.extern.slf4j.Slf4j;
import org.hibernate.exception.ConstraintViolationException;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.cache.annotation.CacheEvict;
import org.springframework.cache.annotation.Cacheable;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Propagation;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.transaction.event.TransactionalEventListener;
import org.thingsboard.server.common.data.EntitySubtype;
import org.thingsboard.server.common.data.EntityType;
import org.thingsboard.server.common.data.StringUtils;
import org.thingsboard.server.common.data.User;
import org.thingsboard.server.common.data.edge.Edge;
import org.thingsboard.server.common.data.edge.EdgeInfo;
@ -47,8 +49,7 @@ import org.thingsboard.server.common.data.relation.EntityRelation;
import org.thingsboard.server.common.data.relation.EntitySearchDirection;
import org.thingsboard.server.common.data.rule.RuleChain;
import org.thingsboard.server.common.data.rule.RuleChainConnectionInfo;
import org.thingsboard.server.dao.cache.EntitiesCacheManager;
import org.thingsboard.server.dao.entity.AbstractEntityService;
import org.thingsboard.server.dao.entity.AbstractCachedEntityService;
import org.thingsboard.server.dao.exception.DataValidationException;
import org.thingsboard.server.dao.relation.RelationService;
import org.thingsboard.server.dao.rule.RuleChainService;
@ -65,7 +66,6 @@ import java.util.List;
import java.util.Optional;
import java.util.stream.Collectors;
import static org.thingsboard.server.common.data.CacheConstants.EDGE_CACHE;
import static org.thingsboard.server.dao.DaoUtil.toUUIDs;
import static org.thingsboard.server.dao.service.Validator.validateId;
import static org.thingsboard.server.dao.service.Validator.validateIds;
@ -74,7 +74,7 @@ import static org.thingsboard.server.dao.service.Validator.validateString;
@Service
@Slf4j
public class EdgeServiceImpl extends AbstractEntityService implements EdgeService {
public class EdgeServiceImpl extends AbstractCachedEntityService<EdgeCacheKey, Edge, EdgeCacheEvictEvent> implements EdgeService {
public static final String INCORRECT_TENANT_ID = "Incorrect tenantId ";
public static final String INCORRECT_CUSTOMER_ID = "Incorrect customerId ";
@ -90,9 +90,6 @@ public class EdgeServiceImpl extends AbstractEntityService implements EdgeServic
@Autowired
private UserService userService;
@Autowired
private EntitiesCacheManager cacheManager;
@Autowired
private RuleChainService ruleChainService;
@ -102,6 +99,17 @@ public class EdgeServiceImpl extends AbstractEntityService implements EdgeServic
@Autowired
private DataValidator<Edge> edgeValidator;
@TransactionalEventListener(classes = EdgeCacheEvictEvent.class)
@Override
public void handleEvictEvent(EdgeCacheEvictEvent event) {
List<EdgeCacheKey> keys = new ArrayList<>(2);
keys.add(new EdgeCacheKey(event.getTenantId(), event.getNewName()));
if (StringUtils.isNotEmpty(event.getOldName()) && !event.getOldName().equals(event.getNewName())) {
keys.add(new EdgeCacheKey(event.getTenantId(), event.getOldName()));
}
cache.evict(keys);
}
@Override
public Edge findEdgeById(TenantId tenantId, EdgeId edgeId) {
log.trace("Executing findEdgeById [{}]", edgeId);
@ -123,13 +131,13 @@ public class EdgeServiceImpl extends AbstractEntityService implements EdgeServic
return edgeDao.findByIdAsync(tenantId, edgeId.getId());
}
@Cacheable(cacheNames = EDGE_CACHE, key = "{#tenantId, #name}")
@Override
public Edge findEdgeByTenantIdAndName(TenantId tenantId, String name) {
log.trace("Executing findEdgeByTenantIdAndName [{}][{}]", tenantId, name);
validateId(tenantId, INCORRECT_TENANT_ID + tenantId);
Optional<Edge> edgeOpt = edgeDao.findEdgeByTenantIdAndName(tenantId.getId(), name);
return edgeOpt.orElse(null);
return cache.getAndPutInTransaction(new EdgeCacheKey(tenantId, name),
() -> edgeDao.findEdgeByTenantIdAndName(tenantId.getId(), name)
.orElse(null), true);
}
@Override
@ -139,14 +147,17 @@ public class EdgeServiceImpl extends AbstractEntityService implements EdgeServic
return edgeDao.findByRoutingKey(tenantId.getId(), routingKey);
}
@CacheEvict(cacheNames = EDGE_CACHE, key = "{#edge.tenantId, #edge.name}")
@Override
public Edge saveEdge(Edge edge) {
log.trace("Executing saveEdge [{}]", edge);
edgeValidator.validate(edge, Edge::getTenantId);
Edge oldEdge = edgeValidator.validate(edge, Edge::getTenantId);
EdgeCacheEvictEvent evictEvent = new EdgeCacheEvictEvent(edge.getTenantId(), edge.getName(), oldEdge != null ? oldEdge.getName() : null);
try {
return edgeDao.save(edge.getTenantId(), edge);
var savedEdge = edgeDao.save(edge.getTenantId(), edge);
publishEvictEvent(evictEvent);
return savedEdge;
} catch (Exception t) {
handleEvictEvent(evictEvent);
ConstraintViolationException e = extractConstraintViolationException(t).orElse(null);
if (e != null && e.getConstraintName() != null
&& e.getConstraintName().equalsIgnoreCase("edge_name_unq_key")) {
@ -182,9 +193,9 @@ public class EdgeServiceImpl extends AbstractEntityService implements EdgeServic
deleteEntityRelations(tenantId, edgeId);
cacheManager.removeEdgeFromCacheByName(edge.getTenantId(), edge.getName());
edgeDao.removeById(tenantId, edgeId.getId());
publishEvictEvent(new EdgeCacheEvictEvent(edge.getTenantId(), edge.getName(), null));
}
@Override

43
dao/src/main/java/org/thingsboard/server/dao/entity/AbstractCachedEntityService.java

@ -0,0 +1,43 @@
/**
* 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.entity;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.ApplicationEventPublisher;
import org.springframework.transaction.support.TransactionSynchronizationManager;
import org.thingsboard.server.cache.TbTransactionalCache;
import java.io.Serializable;
public abstract class AbstractCachedEntityService<K extends Serializable, V extends Serializable, E> extends AbstractEntityService {
@Autowired
protected TbTransactionalCache<K, V> cache;
@Autowired
private ApplicationEventPublisher eventPublisher;
protected void publishEvictEvent(E event) {
if (TransactionSynchronizationManager.isActualTransactionActive()) {
eventPublisher.publishEvent(event);
} else {
handleEvictEvent(event);
}
}
public abstract void handleEvictEvent(E event);
}

3
dao/src/main/java/org/thingsboard/server/dao/entity/AbstractEntityService.java

@ -40,6 +40,7 @@ public abstract class AbstractEntityService {
public static final String INCORRECT_EDGE_ID = "Incorrect edgeId ";
public static final String INCORRECT_PAGE_LINK = "Incorrect page link ";
@Lazy
@Autowired
protected RelationService relationService;
@ -47,9 +48,11 @@ public abstract class AbstractEntityService {
@Autowired
protected AlarmService alarmService;
@Lazy
@Autowired
protected EntityViewService entityViewService;
@Lazy
@Autowired(required = false)
protected EdgeService edgeService;

67
dao/src/main/java/org/thingsboard/server/dao/entityview/EntityViewCacheKey.java

@ -0,0 +1,67 @@
/**
* 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.entityview;
import lombok.Builder;
import lombok.EqualsAndHashCode;
import lombok.Getter;
import org.thingsboard.server.common.data.id.EntityId;
import org.thingsboard.server.common.data.id.EntityViewId;
import org.thingsboard.server.common.data.id.TenantId;
import java.io.Serializable;
@Getter
@EqualsAndHashCode
@Builder
public class EntityViewCacheKey implements Serializable {
private final TenantId tenantId;
private final String name;
private final EntityId entityId;
private final EntityViewId entityViewId;
private EntityViewCacheKey(TenantId tenantId, String name, EntityId entityId, EntityViewId entityViewId) {
this.tenantId = tenantId;
this.name = name;
this.entityId = entityId;
this.entityViewId = entityViewId;
}
public static EntityViewCacheKey byName(TenantId tenantId, String name) {
return new EntityViewCacheKey(tenantId, name, null, null);
}
public static EntityViewCacheKey byEntityId(TenantId tenantId, EntityId entityId) {
return new EntityViewCacheKey(tenantId, null, entityId, null);
}
public static EntityViewCacheKey byId(EntityViewId id) {
return new EntityViewCacheKey(null, null, null, id);
}
@Override
public String toString() {
if (entityViewId != null) {
return entityViewId.toString();
} else if (entityId != null) {
return tenantId + "_" + entityId;
} else {
return tenantId + "_n_" + name;
}
}
}

36
dao/src/main/java/org/thingsboard/server/dao/entityview/EntityViewCacheValue.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.entityview;
import lombok.Builder;
import lombok.EqualsAndHashCode;
import lombok.Getter;
import org.thingsboard.server.common.data.EntityView;
import java.io.Serializable;
import java.util.List;
@Getter
@EqualsAndHashCode
@Builder
public class EntityViewCacheValue implements Serializable {
private static final long serialVersionUID = 1959004642076413174L;
private final EntityView entityView;
private final List<EntityView> entityViews;
}

32
dao/src/main/java/org/thingsboard/server/dao/entityview/EntityViewCaffeineCache.java

@ -0,0 +1,32 @@
/**
* 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.entityview;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.cache.CacheManager;
import org.springframework.stereotype.Service;
import org.thingsboard.server.common.data.CacheConstants;
import org.thingsboard.server.cache.CaffeineTbTransactionalCache;
@ConditionalOnProperty(prefix = "cache", value = "type", havingValue = "caffeine", matchIfMissing = true)
@Service("EntityViewCache")
public class EntityViewCaffeineCache extends CaffeineTbTransactionalCache<EntityViewCacheKey, EntityViewCacheValue> {
public EntityViewCaffeineCache(CacheManager cacheManager) {
super(cacheManager, CacheConstants.ENTITY_VIEW_CACHE);
}
}

2
dao/src/main/java/org/thingsboard/server/dao/entityview/EntityViewDao.java

@ -144,7 +144,7 @@ public interface EntityViewDao extends Dao<EntityView> {
*/
PageData<EntityViewInfo> findEntityViewInfosByTenantIdAndCustomerIdAndType(UUID tenantId, UUID customerId, String type, PageLink pageLink);
ListenableFuture<List<EntityView>> findEntityViewsByTenantIdAndEntityIdAsync(UUID tenantId, UUID entityId);
List<EntityView> findEntityViewsByTenantIdAndEntityId(UUID tenantId, UUID entityId);
/**
* Find tenants entity view types.

35
dao/src/main/java/org/thingsboard/server/dao/entityview/EntityViewEvictEvent.java

@ -0,0 +1,35 @@
/**
* 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.entityview;
import lombok.Data;
import lombok.RequiredArgsConstructor;
import org.thingsboard.server.common.data.id.EntityId;
import org.thingsboard.server.common.data.id.EntityViewId;
import org.thingsboard.server.common.data.id.TenantId;
@Data
@RequiredArgsConstructor
class EntityViewEvictEvent {
private final TenantId tenantId;
private final EntityViewId id;
private final EntityId newEntityId;
private final EntityId oldEntityId;
private final String newName;
private final String oldName;
}

34
dao/src/main/java/org/thingsboard/server/dao/entityview/EntityViewRedisCache.java

@ -0,0 +1,34 @@
/**
* 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.entityview;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.data.redis.connection.RedisConnectionFactory;
import org.springframework.stereotype.Service;
import org.thingsboard.server.cache.CacheSpecsMap;
import org.thingsboard.server.cache.TBRedisCacheConfiguration;
import org.thingsboard.server.common.data.CacheConstants;
import org.thingsboard.server.cache.RedisTbTransactionalCache;
import org.thingsboard.server.cache.TbRedisSerializer;
@ConditionalOnProperty(prefix = "cache", value = "type", havingValue = "redis")
@Service("EntityViewCache")
public class EntityViewRedisCache extends RedisTbTransactionalCache<EntityViewCacheKey, EntityViewCacheValue> {
public EntityViewRedisCache(TBRedisCacheConfiguration configuration, CacheSpecsMap cacheSpecsMap, RedisConnectionFactory connectionFactory) {
super(CacheConstants.ENTITY_VIEW_CACHE, cacheSpecsMap, connectionFactory, configuration, new TbRedisSerializer<>());
}
}

99
dao/src/main/java/org/thingsboard/server/dao/entityview/EntityViewServiceImpl.java

@ -16,20 +16,20 @@
package org.thingsboard.server.dao.entityview;
import com.google.common.base.Function;
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 lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.cache.Cache;
import org.springframework.cache.CacheManager;
import org.springframework.cache.annotation.Cacheable;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Propagation;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.transaction.event.TransactionalEventListener;
import org.thingsboard.server.common.data.EntitySubtype;
import org.thingsboard.server.common.data.EntityType;
import org.thingsboard.server.common.data.EntityView;
import org.thingsboard.server.common.data.EntityViewInfo;
import org.thingsboard.server.common.data.StringUtils;
import org.thingsboard.server.common.data.edge.Edge;
import org.thingsboard.server.common.data.entityview.EntityViewSearchQuery;
import org.thingsboard.server.common.data.id.CustomerId;
@ -42,22 +42,20 @@ import org.thingsboard.server.common.data.page.PageLink;
import org.thingsboard.server.common.data.relation.EntityRelation;
import org.thingsboard.server.common.data.relation.EntitySearchDirection;
import org.thingsboard.server.common.data.relation.RelationTypeGroup;
import org.thingsboard.server.dao.entity.AbstractEntityService;
import org.thingsboard.server.dao.entity.AbstractCachedEntityService;
import org.thingsboard.server.dao.exception.DataValidationException;
import org.thingsboard.server.dao.service.DataValidator;
import org.thingsboard.server.dao.service.PaginatedRemover;
import org.thingsboard.server.dao.sql.JpaExecutorService;
import javax.annotation.Nullable;
import java.util.ArrayList;
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.stream.Collectors;
import static org.thingsboard.server.common.data.CacheConstants.ENTITY_VIEW_CACHE;
import static org.thingsboard.server.dao.service.Validator.validateId;
import static org.thingsboard.server.dao.service.Validator.validatePageLink;
import static org.thingsboard.server.dao.service.Validator.validateString;
@ -67,7 +65,7 @@ import static org.thingsboard.server.dao.service.Validator.validateString;
*/
@Service
@Slf4j
public class EntityViewServiceImpl extends AbstractEntityService implements EntityViewService {
public class EntityViewServiceImpl extends AbstractCachedEntityService<EntityViewCacheKey, EntityViewCacheValue, EntityViewEvictEvent> implements EntityViewService {
public static final String INCORRECT_TENANT_ID = "Incorrect tenantId ";
public static final String INCORRECT_PAGE_LINK = "Incorrect page link ";
@ -79,28 +77,39 @@ public class EntityViewServiceImpl extends AbstractEntityService implements Enti
private EntityViewDao entityViewDao;
@Autowired
private CacheManager cacheManager;
private DataValidator<EntityView> entityViewValidator;
@Autowired
private DataValidator<EntityView> entityViewValidator;
protected JpaExecutorService service;
@TransactionalEventListener(classes = EntityViewEvictEvent.class)
@Override
public EntityView saveEntityView(EntityView entityView) {
log.trace("Executing save entity view [{}]", entityView);
entityViewValidator.validate(entityView, EntityView::getTenantId);
if (entityView.getId() != null) {
EntityView oldEntityView = entityViewDao.findById(entityView.getTenantId(), entityView.getUuidId());
evictCache(oldEntityView);
public void handleEvictEvent(EntityViewEvictEvent event) {
List<EntityViewCacheKey> keys = new ArrayList<>(5);
keys.add(EntityViewCacheKey.byName(event.getTenantId(), event.getNewName()));
keys.add(EntityViewCacheKey.byId(event.getId()));
keys.add(EntityViewCacheKey.byEntityId(event.getTenantId(), event.getNewEntityId()));
if (event.getOldEntityId() != null && !event.getOldEntityId().equals(event.getNewEntityId())) {
keys.add(EntityViewCacheKey.byEntityId(event.getTenantId(), event.getOldEntityId()));
}
if (StringUtils.isNotEmpty(event.getOldName()) && !event.getOldName().equals(event.getNewName())) {
keys.add(EntityViewCacheKey.byName(event.getTenantId(), event.getOldName()));
}
cache.evict(keys);
}
return entityViewDao.save(entityView.getTenantId(), entityView);
@Override
public EntityView saveEntityView(EntityView entityView) {
log.trace("Executing save entity view [{}]", entityView);
EntityView old = entityViewValidator.validate(entityView, EntityView::getTenantId);
EntityView saved = entityViewDao.save(entityView.getTenantId(), entityView);
publishEvictEvent(new EntityViewEvictEvent(saved.getTenantId(), saved.getId(), saved.getEntityId(), old != null ? old.getEntityId() : null, saved.getName(), old != null ? old.getName() : null));
return saved;
}
@Override
public EntityView assignEntityViewToCustomer(TenantId tenantId, EntityViewId entityViewId, CustomerId customerId) {
EntityView entityView = findEntityViewById(tenantId, entityViewId);
evictCache(entityView);
entityView.setCustomerId(customerId);
return saveEntityView(entityView);
}
@ -108,7 +117,6 @@ public class EntityViewServiceImpl extends AbstractEntityService implements Enti
@Override
public EntityView unassignEntityViewFromCustomer(TenantId tenantId, EntityViewId entityViewId) {
EntityView entityView = findEntityViewById(tenantId, entityViewId);
evictCache(entityView);
entityView.setCustomerId(null);
return saveEntityView(entityView);
}
@ -128,21 +136,23 @@ public class EntityViewServiceImpl extends AbstractEntityService implements Enti
return entityViewDao.findEntityViewInfoById(tenantId, entityViewId.getId());
}
@Cacheable(cacheNames = ENTITY_VIEW_CACHE, key = "{#entityViewId}")
@Override
public EntityView findEntityViewById(TenantId tenantId, EntityViewId entityViewId) {
log.trace("Executing findEntityViewById [{}]", entityViewId);
validateId(entityViewId, INCORRECT_ENTITY_VIEW_ID + entityViewId);
return entityViewDao.findById(tenantId, entityViewId.getId());
return cache.getAndPutInTransaction(EntityViewCacheKey.byId(entityViewId),
() -> entityViewDao.findById(tenantId, entityViewId.getId())
, EntityViewCacheValue::getEntityView, v -> new EntityViewCacheValue(v, null), true);
}
@Cacheable(cacheNames = ENTITY_VIEW_CACHE, key = "{#tenantId, #name}")
@Override
public EntityView findEntityViewByTenantIdAndName(TenantId tenantId, String name) {
log.trace("Executing findEntityViewByTenantIdAndName [{}][{}]", tenantId, name);
validateId(tenantId, INCORRECT_TENANT_ID + tenantId);
Optional<EntityView> entityViewOpt = entityViewDao.findEntityViewByTenantIdAndName(tenantId.getId(), name);
return entityViewOpt.orElse(null);
return cache.getAndPutInTransaction(EntityViewCacheKey.byName(tenantId, name),
() -> entityViewDao.findEntityViewByTenantIdAndName(tenantId.getId(), name).orElse(null)
, EntityViewCacheValue::getEntityView, v -> new EntityViewCacheValue(v, null), true);
}
@Override
@ -265,31 +275,9 @@ public class EntityViewServiceImpl extends AbstractEntityService implements Enti
validateId(tenantId, INCORRECT_TENANT_ID + tenantId);
validateId(entityId.getId(), "Incorrect entityId" + entityId);
List<Object> tenantIdAndEntityId = new ArrayList<>();
tenantIdAndEntityId.add(tenantId);
tenantIdAndEntityId.add(entityId);
Cache cache = cacheManager.getCache(ENTITY_VIEW_CACHE);
@SuppressWarnings("unchecked")
List<EntityView> fromCache = cache.get(tenantIdAndEntityId, List.class);
if (fromCache != null) {
return Futures.immediateFuture(fromCache);
} else {
ListenableFuture<List<EntityView>> entityViewsFuture = entityViewDao.findEntityViewsByTenantIdAndEntityIdAsync(tenantId.getId(), entityId.getId());
Futures.addCallback(entityViewsFuture,
new FutureCallback<List<EntityView>>() {
@Override
public void onSuccess(@Nullable List<EntityView> result) {
cache.putIfAbsent(tenantIdAndEntityId, result);
}
@Override
public void onFailure(Throwable t) {
log.error("Error while finding entity views by tenantId and entityId", t);
}
}, MoreExecutors.directExecutor());
return entityViewsFuture;
}
return service.submit(() -> cache.getAndPutInTransaction(EntityViewCacheKey.byEntityId(tenantId, entityId),
() -> entityViewDao.findEntityViewsByTenantIdAndEntityId(tenantId.getId(), entityId.getId()),
EntityViewCacheValue::getEntityViews, v -> new EntityViewCacheValue(null, v), true));
}
@Override
@ -298,8 +286,8 @@ public class EntityViewServiceImpl extends AbstractEntityService implements Enti
validateId(entityViewId, INCORRECT_ENTITY_VIEW_ID + entityViewId);
deleteEntityRelations(tenantId, entityViewId);
EntityView entityView = entityViewDao.findById(tenantId, entityViewId.getId());
evictCache(entityView);
entityViewDao.removeById(tenantId, entityViewId.getId());
publishEvictEvent(new EntityViewEvictEvent(entityView.getTenantId(), entityView.getId(), entityView.getEntityId(), null, entityView.getName(), null));
}
@Override
@ -410,11 +398,4 @@ public class EntityViewServiceImpl extends AbstractEntityService implements Enti
unassignEntityViewFromCustomer(tenantId, new EntityViewId(entity.getUuidId()));
}
};
private void evictCache(EntityView entityView) {
Cache cache = cacheManager.getCache(ENTITY_VIEW_CACHE);
cache.evict(Arrays.asList(entityView.getTenantId(), entityView.getEntityId()));
cache.evict(Arrays.asList(entityView.getTenantId(), entityView.getName()));
cache.evict(Arrays.asList(entityView.getId()));
}
}

2
dao/src/main/java/org/thingsboard/server/dao/event/EventDao.java

@ -19,13 +19,11 @@ import com.google.common.util.concurrent.ListenableFuture;
import org.thingsboard.server.common.data.Event;
import org.thingsboard.server.common.data.event.EventFilter;
import org.thingsboard.server.common.data.id.EntityId;
import org.thingsboard.server.common.data.id.TenantId;
import org.thingsboard.server.common.data.page.PageData;
import org.thingsboard.server.common.data.page.TimePageLink;
import org.thingsboard.server.dao.Dao;
import java.util.List;
import java.util.Optional;
import java.util.UUID;
/**

1
dao/src/main/java/org/thingsboard/server/dao/model/sql/AbstractEdgeEntity.java

@ -15,7 +15,6 @@
*/
package org.thingsboard.server.dao.model.sql;
import com.datastax.oss.driver.api.core.uuid.Uuids;
import com.fasterxml.jackson.databind.JsonNode;
import lombok.Data;
import lombok.EqualsAndHashCode;

1
dao/src/main/java/org/thingsboard/server/dao/model/sql/AbstractEntityViewEntity.java

@ -25,7 +25,6 @@ import org.hibernate.annotations.TypeDef;
import org.thingsboard.server.common.data.EntityType;
import org.thingsboard.server.common.data.EntityView;
import org.thingsboard.server.common.data.id.CustomerId;
import org.thingsboard.server.common.data.id.EdgeId;
import org.thingsboard.server.common.data.id.EntityIdFactory;
import org.thingsboard.server.common.data.id.EntityViewId;
import org.thingsboard.server.common.data.id.TenantId;

3
dao/src/main/java/org/thingsboard/server/dao/model/sql/AbstractTenantEntity.java

@ -21,7 +21,6 @@ import lombok.EqualsAndHashCode;
import org.hibernate.annotations.Type;
import org.hibernate.annotations.TypeDef;
import org.thingsboard.server.common.data.Tenant;
import org.thingsboard.server.common.data.id.CustomerId;
import org.thingsboard.server.common.data.id.TenantId;
import org.thingsboard.server.common.data.id.TenantProfileId;
import org.thingsboard.server.dao.model.BaseSqlEntity;
@ -30,9 +29,7 @@ import org.thingsboard.server.dao.model.SearchTextEntity;
import org.thingsboard.server.dao.util.mapping.JsonStringType;
import javax.persistence.Column;
import javax.persistence.Entity;
import javax.persistence.MappedSuperclass;
import javax.persistence.Table;
import java.util.UUID;
@Data

2
dao/src/main/java/org/thingsboard/server/dao/model/sql/ApiUsageStateEntity.java

@ -20,9 +20,9 @@ import lombok.EqualsAndHashCode;
import org.hibernate.annotations.TypeDef;
import org.thingsboard.server.common.data.ApiUsageState;
import org.thingsboard.server.common.data.ApiUsageStateValue;
import org.thingsboard.server.common.data.id.ApiUsageStateId;
import org.thingsboard.server.common.data.id.EntityIdFactory;
import org.thingsboard.server.common.data.id.TenantId;
import org.thingsboard.server.common.data.id.ApiUsageStateId;
import org.thingsboard.server.dao.model.BaseEntity;
import org.thingsboard.server.dao.model.BaseSqlEntity;
import org.thingsboard.server.dao.model.ModelConstants;

15
dao/src/main/java/org/thingsboard/server/dao/model/sql/OAuth2ClientRegistrationTemplateEntity.java

@ -21,16 +21,21 @@ import lombok.EqualsAndHashCode;
import org.hibernate.annotations.Type;
import org.hibernate.annotations.TypeDef;
import org.thingsboard.server.common.data.id.OAuth2ClientRegistrationTemplateId;
import org.thingsboard.server.common.data.id.TenantId;
import org.thingsboard.server.common.data.oauth2.*;
import org.thingsboard.server.common.data.oauth2.MapperType;
import org.thingsboard.server.common.data.oauth2.OAuth2BasicMapperConfig;
import org.thingsboard.server.common.data.oauth2.OAuth2ClientRegistrationTemplate;
import org.thingsboard.server.common.data.oauth2.OAuth2MapperConfig;
import org.thingsboard.server.common.data.oauth2.TenantNameStrategyType;
import org.thingsboard.server.dao.model.BaseSqlEntity;
import org.thingsboard.server.dao.model.ModelConstants;
import org.thingsboard.server.dao.util.mapping.JsonStringType;
import org.thingsboard.server.common.data.oauth2.OAuth2ClientRegistrationTemplate;
import javax.persistence.*;
import javax.persistence.Column;
import javax.persistence.Entity;
import javax.persistence.EnumType;
import javax.persistence.Enumerated;
import javax.persistence.Table;
import java.util.Arrays;
import java.util.UUID;
@Data
@EqualsAndHashCode(callSuper = true)

4
dao/src/main/java/org/thingsboard/server/dao/model/sql/OtaPackageEntity.java

@ -21,11 +21,11 @@ import lombok.EqualsAndHashCode;
import org.hibernate.annotations.Type;
import org.hibernate.annotations.TypeDef;
import org.thingsboard.server.common.data.OtaPackage;
import org.thingsboard.server.common.data.ota.ChecksumAlgorithm;
import org.thingsboard.server.common.data.ota.OtaPackageType;
import org.thingsboard.server.common.data.id.DeviceProfileId;
import org.thingsboard.server.common.data.id.OtaPackageId;
import org.thingsboard.server.common.data.id.TenantId;
import org.thingsboard.server.common.data.ota.ChecksumAlgorithm;
import org.thingsboard.server.common.data.ota.OtaPackageType;
import org.thingsboard.server.dao.model.BaseSqlEntity;
import org.thingsboard.server.dao.model.ModelConstants;
import org.thingsboard.server.dao.model.SearchTextEntity;

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

@ -22,12 +22,11 @@ import org.hibernate.annotations.Type;
import org.hibernate.annotations.TypeDef;
import org.thingsboard.common.util.JacksonUtil;
import org.thingsboard.server.common.data.OtaPackageInfo;
import org.thingsboard.server.common.data.StringUtils;
import org.thingsboard.server.common.data.ota.ChecksumAlgorithm;
import org.thingsboard.server.common.data.ota.OtaPackageType;
import org.thingsboard.server.common.data.id.DeviceProfileId;
import org.thingsboard.server.common.data.id.OtaPackageId;
import org.thingsboard.server.common.data.id.TenantId;
import org.thingsboard.server.common.data.ota.ChecksumAlgorithm;
import org.thingsboard.server.common.data.ota.OtaPackageType;
import org.thingsboard.server.dao.model.BaseSqlEntity;
import org.thingsboard.server.dao.model.ModelConstants;
import org.thingsboard.server.dao.model.SearchTextEntity;

2
dao/src/main/java/org/thingsboard/server/dao/model/sql/RuleNodeStateEntity.java

@ -15,10 +15,8 @@
*/
package org.thingsboard.server.dao.model.sql;
import com.fasterxml.jackson.databind.JsonNode;
import lombok.Data;
import lombok.EqualsAndHashCode;
import org.hibernate.annotations.Type;
import org.hibernate.annotations.TypeDef;
import org.thingsboard.server.common.data.id.EntityIdFactory;
import org.thingsboard.server.common.data.id.RuleNodeId;

2
dao/src/main/java/org/thingsboard/server/dao/model/sql/TenantProfileEntity.java

@ -21,13 +21,13 @@ import lombok.Data;
import lombok.EqualsAndHashCode;
import org.hibernate.annotations.Type;
import org.hibernate.annotations.TypeDef;
import org.thingsboard.common.util.JacksonUtil;
import org.thingsboard.server.common.data.TenantProfile;
import org.thingsboard.server.common.data.id.TenantProfileId;
import org.thingsboard.server.common.data.tenant.profile.TenantProfileData;
import org.thingsboard.server.dao.model.BaseSqlEntity;
import org.thingsboard.server.dao.model.ModelConstants;
import org.thingsboard.server.dao.model.SearchTextEntity;
import org.thingsboard.common.util.JacksonUtil;
import org.thingsboard.server.dao.util.mapping.JsonBinaryType;
import javax.persistence.Column;

1
dao/src/main/java/org/thingsboard/server/dao/model/sql/WidgetTypeEntity.java

@ -28,7 +28,6 @@ import org.thingsboard.server.dao.util.mapping.JsonStringType;
import javax.persistence.Column;
import javax.persistence.Entity;
import javax.persistence.Table;
import java.util.UUID;
@Data
@EqualsAndHashCode(callSuper = true)

1
dao/src/main/java/org/thingsboard/server/dao/oauth2/OAuth2Configuration.java

@ -16,7 +16,6 @@
package org.thingsboard.server.dao.oauth2;
import lombok.Data;
import lombok.extern.slf4j.Slf4j;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.context.annotation.Configuration;

14
dao/src/main/java/org/thingsboard/server/dao/oauth2/OAuth2Utils.java

@ -18,9 +18,19 @@ package org.thingsboard.server.dao.oauth2;
import org.thingsboard.server.common.data.BaseData;
import org.thingsboard.server.common.data.id.OAuth2ParamsId;
import org.thingsboard.server.common.data.id.TenantId;
import org.thingsboard.server.common.data.oauth2.*;
import org.thingsboard.server.common.data.oauth2.OAuth2ClientInfo;
import org.thingsboard.server.common.data.oauth2.OAuth2Domain;
import org.thingsboard.server.common.data.oauth2.OAuth2DomainInfo;
import org.thingsboard.server.common.data.oauth2.OAuth2Info;
import org.thingsboard.server.common.data.oauth2.OAuth2Mobile;
import org.thingsboard.server.common.data.oauth2.OAuth2MobileInfo;
import org.thingsboard.server.common.data.oauth2.OAuth2Params;
import org.thingsboard.server.common.data.oauth2.OAuth2ParamsInfo;
import org.thingsboard.server.common.data.oauth2.OAuth2Registration;
import org.thingsboard.server.common.data.oauth2.OAuth2RegistrationInfo;
import java.util.*;
import java.util.Comparator;
import java.util.List;
import java.util.stream.Collectors;
public class OAuth2Utils {

52
dao/src/main/java/org/thingsboard/server/dao/ota/BaseOtaPackageService.java

@ -21,10 +21,10 @@ import com.google.common.util.concurrent.ListenableFuture;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.hibernate.exception.ConstraintViolationException;
import org.springframework.cache.Cache;
import org.springframework.cache.CacheManager;
import org.springframework.cache.annotation.Cacheable;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Propagation;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.transaction.event.TransactionalEventListener;
import org.thingsboard.server.cache.ota.OtaPackageDataCache;
import org.thingsboard.server.common.data.OtaPackage;
import org.thingsboard.server.common.data.OtaPackageInfo;
@ -36,6 +36,7 @@ import org.thingsboard.server.common.data.ota.ChecksumAlgorithm;
import org.thingsboard.server.common.data.ota.OtaPackageType;
import org.thingsboard.server.common.data.page.PageData;
import org.thingsboard.server.common.data.page.PageLink;
import org.thingsboard.server.dao.entity.AbstractCachedEntityService;
import org.thingsboard.server.dao.exception.DataValidationException;
import org.thingsboard.server.dao.service.DataValidator;
import org.thingsboard.server.dao.service.PaginatedRemover;
@ -45,40 +46,47 @@ import java.util.Collections;
import java.util.List;
import java.util.Optional;
import static org.thingsboard.server.common.data.CacheConstants.OTA_PACKAGE_CACHE;
import static org.thingsboard.server.dao.service.Validator.validateId;
import static org.thingsboard.server.dao.service.Validator.validatePageLink;
@Service
@Slf4j
@RequiredArgsConstructor
public class BaseOtaPackageService implements OtaPackageService {
public class BaseOtaPackageService extends AbstractCachedEntityService<OtaPackageCacheKey, OtaPackageInfo, OtaPackageCacheEvictEvent> implements OtaPackageService {
public static final String INCORRECT_OTA_PACKAGE_ID = "Incorrect otaPackageId ";
public static final String INCORRECT_TENANT_ID = "Incorrect tenantId ";
private final OtaPackageDao otaPackageDao;
private final OtaPackageInfoDao otaPackageInfoDao;
private final CacheManager cacheManager;
private final OtaPackageDataCache otaPackageDataCache;
private final DataValidator<OtaPackageInfo> otaPackageInfoValidator;
private final DataValidator<OtaPackage> otaPackageValidator;
@TransactionalEventListener(classes = OtaPackageCacheEvictEvent.class)
@Override
public void handleEvictEvent(OtaPackageCacheEvictEvent event) {
cache.evict(new OtaPackageCacheKey(event.getId()));
otaPackageDataCache.evict(event.getId().toString());
}
@Override
public OtaPackageInfo saveOtaPackageInfo(OtaPackageInfo otaPackageInfo, boolean isUrl) {
log.trace("Executing saveOtaPackageInfo [{}]", otaPackageInfo);
if(isUrl && (StringUtils.isEmpty(otaPackageInfo.getUrl()) || otaPackageInfo.getUrl().trim().length() == 0)) {
if (isUrl && (StringUtils.isEmpty(otaPackageInfo.getUrl()) || otaPackageInfo.getUrl().trim().length() == 0)) {
throw new DataValidationException("Ota package URL should be specified!");
}
otaPackageInfoValidator.validate(otaPackageInfo, OtaPackageInfo::getTenantId);
OtaPackageId otaPackageId = otaPackageInfo.getId();
try {
OtaPackageId otaPackageId = otaPackageInfo.getId();
var result = otaPackageInfoDao.save(otaPackageInfo.getTenantId(), otaPackageInfo);
if (otaPackageId != null) {
Cache cache = cacheManager.getCache(OTA_PACKAGE_CACHE);
cache.evict(toOtaPackageInfoKey(otaPackageId));
otaPackageDataCache.evict(otaPackageId.toString());
publishEvictEvent(new OtaPackageCacheEvictEvent(otaPackageId));
}
return otaPackageInfoDao.save(otaPackageInfo.getTenantId(), otaPackageInfo);
return result;
} catch (Exception t) {
if (otaPackageId != null) {
handleEvictEvent(new OtaPackageCacheEvictEvent(otaPackageId));
}
ConstraintViolationException e = extractConstraintViolationException(t).orElse(null);
if (e != null && e.getConstraintName() != null && e.getConstraintName().equalsIgnoreCase("ota_package_tenant_title_version_unq_key")) {
throw new DataValidationException("OtaPackage with such title and version already exists!");
@ -92,15 +100,17 @@ public class BaseOtaPackageService implements OtaPackageService {
public OtaPackage saveOtaPackage(OtaPackage otaPackage) {
log.trace("Executing saveOtaPackage [{}]", otaPackage);
otaPackageValidator.validate(otaPackage, OtaPackageInfo::getTenantId);
OtaPackageId otaPackageId = otaPackage.getId();
try {
OtaPackageId otaPackageId = otaPackage.getId();
var result = otaPackageDao.save(otaPackage.getTenantId(), otaPackage);
if (otaPackageId != null) {
Cache cache = cacheManager.getCache(OTA_PACKAGE_CACHE);
cache.evict(toOtaPackageInfoKey(otaPackageId));
otaPackageDataCache.evict(otaPackageId.toString());
publishEvictEvent(new OtaPackageCacheEvictEvent(otaPackageId));
}
return otaPackageDao.save(otaPackage.getTenantId(), otaPackage);
return result;
} catch (Exception t) {
if (otaPackageId != null) {
handleEvictEvent(new OtaPackageCacheEvictEvent(otaPackageId));
}
ConstraintViolationException e = extractConstraintViolationException(t).orElse(null);
if (e != null && e.getConstraintName() != null && e.getConstraintName().equalsIgnoreCase("ota_package_tenant_title_version_unq_key")) {
throw new DataValidationException("OtaPackage with such title and version already exists!");
@ -149,11 +159,11 @@ public class BaseOtaPackageService implements OtaPackageService {
}
@Override
@Cacheable(cacheNames = OTA_PACKAGE_CACHE, key = "{#otaPackageId}")
public OtaPackageInfo findOtaPackageInfoById(TenantId tenantId, OtaPackageId otaPackageId) {
log.trace("Executing findOtaPackageInfoById [{}]", otaPackageId);
validateId(otaPackageId, INCORRECT_OTA_PACKAGE_ID + otaPackageId);
return otaPackageInfoDao.findById(tenantId, otaPackageId.getId());
return cache.getAndPutInTransaction(new OtaPackageCacheKey(otaPackageId),
() -> otaPackageInfoDao.findById(tenantId, otaPackageId.getId()), true);
}
@Override
@ -184,10 +194,8 @@ public class BaseOtaPackageService implements OtaPackageService {
log.trace("Executing deleteOtaPackage [{}]", otaPackageId);
validateId(otaPackageId, INCORRECT_OTA_PACKAGE_ID + otaPackageId);
try {
Cache cache = cacheManager.getCache(OTA_PACKAGE_CACHE);
cache.evict(toOtaPackageInfoKey(otaPackageId));
otaPackageDataCache.evict(otaPackageId.toString());
otaPackageDao.removeById(tenantId, otaPackageId.getId());
publishEvictEvent(new OtaPackageCacheEvictEvent(otaPackageId));
} catch (Exception t) {
ConstraintViolationException e = extractConstraintViolationException(t).orElse(null);
if (e != null && e.getConstraintName() != null && e.getConstraintName().equalsIgnoreCase("fk_firmware_device")) {

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

Loading…
Cancel
Save