added tenant cache

This commit is contained in:
YevhenBondarenko 2022-06-16 22:04:26 +02:00
parent e61bfea55e
commit 2b18b44a52
11 changed files with 265 additions and 46 deletions

View File

@ -15,7 +15,8 @@
*/
package org.thingsboard.server.dao.service.validator;
import lombok.AllArgsConstructor;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Lazy;
import org.springframework.stereotype.Component;
import org.thingsboard.server.common.data.ApiUsageState;
import org.thingsboard.server.common.data.EntityType;
@ -25,10 +26,11 @@ import org.thingsboard.server.dao.service.DataValidator;
import org.thingsboard.server.dao.tenant.TenantService;
@Component
@AllArgsConstructor
public class ApiUsageDataValidator extends DataValidator<ApiUsageState> {
private final TenantService tenantService;
@Lazy
@Autowired
private TenantService tenantService;
@Override
protected void validateDataImpl(TenantId requestTenantId, ApiUsageState apiUsageState) {

View File

@ -21,7 +21,6 @@ import org.springframework.context.annotation.Lazy;
import org.springframework.stereotype.Component;
import org.thingsboard.server.common.data.Customer;
import org.thingsboard.server.common.data.EntityType;
import org.thingsboard.server.common.data.Tenant;
import org.thingsboard.server.common.data.User;
import org.thingsboard.server.common.data.id.CustomerId;
import org.thingsboard.server.common.data.id.TenantId;
@ -32,7 +31,6 @@ import org.thingsboard.server.dao.exception.DataValidationException;
import org.thingsboard.server.dao.model.ModelConstants;
import org.thingsboard.server.dao.service.DataValidator;
import org.thingsboard.server.dao.tenant.TbTenantProfileCache;
import org.thingsboard.server.dao.tenant.TenantDao;
import org.thingsboard.server.dao.tenant.TenantService;
import org.thingsboard.server.dao.user.UserDao;
import org.thingsboard.server.dao.user.UserService;
@ -55,6 +53,7 @@ public class UserDataValidator extends DataValidator<User> {
private TbTenantProfileCache tenantProfileCache;
@Autowired
@Lazy
private TenantService tenantService;
@Override

View File

@ -0,0 +1,45 @@
/**
* 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.tenant;
import lombok.AllArgsConstructor;
import lombok.Data;
import org.thingsboard.server.common.data.id.TenantId;
import org.thingsboard.server.common.data.id.TenantProfileId;
import java.io.Serializable;
@Data
@AllArgsConstructor
public class TenantCacheKey implements Serializable {
private static final long serialVersionUID = -121787454251592384L;
private final TenantId tenantId;
private final TenantCacheKeyPrefix keyPrefix;
public static TenantCacheKey fromId(TenantId tenantId) {
return new TenantCacheKey(tenantId, TenantCacheKeyPrefix.TENANT);
}
public static TenantCacheKey fromIdExists(TenantId tenantId) {
return new TenantCacheKey(tenantId, TenantCacheKeyPrefix.EXISTS);
}
public enum TenantCacheKeyPrefix {
TENANT, EXISTS
}
}

View File

@ -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.tenant;
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.Tenant;
@ConditionalOnProperty(prefix = "cache", value = "type", havingValue = "caffeine", matchIfMissing = true)
@Service("TenantCache")
public class TenantCaffeineCache extends CaffeineTbTransactionalCache<TenantCacheKey, Tenant> {
public TenantCaffeineCache(CacheManager cacheManager) {
super(cacheManager, CacheConstants.TENANTS_CACHE);
}
}

View File

@ -0,0 +1,26 @@
/**
* 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.tenant;
import lombok.Data;
import org.thingsboard.server.common.data.id.TenantId;
@Data
public class TenantEvictEvent {
private final TenantId tenantId;
// for exists tenant cache
private final boolean isExistsTenant;
}

View File

@ -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.tenant;
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.Tenant;
@ConditionalOnProperty(prefix = "cache", value = "type", havingValue = "caffeine", matchIfMissing = true)
@Service("TenantExistsCache")
public class TenantExistsCaffeineCache extends CaffeineTbTransactionalCache<TenantCacheKey, Boolean> {
public TenantExistsCaffeineCache(CacheManager cacheManager) {
super(cacheManager, CacheConstants.TENANTS_CACHE);
}
}

View File

@ -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.tenant;
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.Tenant;
@ConditionalOnProperty(prefix = "cache", value = "type", havingValue = "redis")
@Service("TenantExistsCache")
public class TenantExistsRedisCache extends RedisTbTransactionalCache<TenantCacheKey, Boolean> {
public TenantExistsRedisCache(TBRedisCacheConfiguration configuration, CacheSpecsMap cacheSpecsMap, RedisConnectionFactory connectionFactory) {
super(CacheConstants.TENANTS_CACHE, cacheSpecsMap, connectionFactory, configuration, new TbRedisSerializer<>());
}
}

View File

@ -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.tenant;
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.Tenant;
@ConditionalOnProperty(prefix = "cache", value = "type", havingValue = "redis")
@Service("TenantCache")
public class TenantRedisCache extends RedisTbTransactionalCache<TenantCacheKey, Tenant> {
public TenantRedisCache(TBRedisCacheConfiguration configuration, CacheSpecsMap cacheSpecsMap, RedisConnectionFactory connectionFactory) {
super(CacheConstants.TENANTS_CACHE, cacheSpecsMap, connectionFactory, configuration, new TbRedisSerializer<>());
}
}

View File

@ -18,12 +18,11 @@ package org.thingsboard.server.dao.tenant;
import com.google.common.util.concurrent.ListenableFuture;
import lombok.extern.slf4j.Slf4j;
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.context.annotation.Lazy;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.transaction.event.TransactionalEventListener;
import org.thingsboard.server.cache.TbTransactionalCache;
import org.thingsboard.server.common.data.Tenant;
import org.thingsboard.server.common.data.TenantInfo;
import org.thingsboard.server.common.data.TenantProfile;
@ -36,7 +35,7 @@ import org.thingsboard.server.dao.customer.CustomerService;
import org.thingsboard.server.dao.dashboard.DashboardService;
import org.thingsboard.server.dao.device.DeviceProfileService;
import org.thingsboard.server.dao.device.DeviceService;
import org.thingsboard.server.dao.entity.AbstractEntityService;
import org.thingsboard.server.dao.entity.AbstractCachedEntityService;
import org.thingsboard.server.dao.ota.OtaPackageService;
import org.thingsboard.server.dao.queue.QueueService;
import org.thingsboard.server.dao.resource.ResourceService;
@ -51,12 +50,11 @@ import org.thingsboard.server.dao.widget.WidgetsBundleService;
import java.util.List;
import static org.thingsboard.server.common.data.CacheConstants.TENANTS_CACHE;
import static org.thingsboard.server.dao.service.Validator.validateId;
@Service
@Slf4j
public class TenantServiceImpl extends AbstractEntityService implements TenantService {
public class TenantServiceImpl extends AbstractCachedEntityService<TenantCacheKey, Tenant, TenantEvictEvent> implements TenantService {
private static final String DEFAULT_TENANT_REGION = "Global";
public static final String INCORRECT_TENANT_ID = "Incorrect tenantId ";
@ -83,6 +81,7 @@ public class TenantServiceImpl extends AbstractEntityService implements TenantSe
@Autowired
private DeviceProfileService deviceProfileService;
@Lazy
@Autowired
private ApiUsageStateService apiUsageStateService;
@ -110,12 +109,26 @@ public class TenantServiceImpl extends AbstractEntityService implements TenantSe
@Autowired
private QueueService queueService;
@Autowired
protected TbTransactionalCache<TenantCacheKey, Boolean> existsTenantCache;
@TransactionalEventListener(classes = TenantEvictEvent.class)
@Override
public void handleEvictEvent(TenantEvictEvent event) {
TenantId tenantId = event.getTenantId();
cache.evict(TenantCacheKey.fromId(tenantId));
if (event.isExistsTenant()) {
existsTenantCache.evict(TenantCacheKey.fromIdExists(tenantId));
}
}
@Override
@Cacheable(cacheNames = TENANTS_CACHE, key = "{#tenantId, 'TENANT'}")
public Tenant findTenantById(TenantId tenantId) {
log.trace("Executing findTenantById [{}]", tenantId);
Validator.validateId(tenantId, INCORRECT_TENANT_ID + tenantId);
return tenantDao.findById(tenantId, tenantId.getId());
return cache.getAndPutInTransaction(TenantCacheKey.fromId(tenantId),
() -> tenantDao.findById(tenantId, tenantId.getId()), true);
}
@Override
@ -134,7 +147,6 @@ public class TenantServiceImpl extends AbstractEntityService implements TenantSe
@Override
@Transactional
@CacheEvict(cacheNames = TENANTS_CACHE, key = "{#tenant.id, 'TENANT'}", condition = "#tenant.id!=null")
public Tenant saveTenant(Tenant tenant) {
log.trace("Executing saveTenant [{}]", tenant);
tenant.setRegion(DEFAULT_TENANT_REGION);
@ -144,6 +156,7 @@ public class TenantServiceImpl extends AbstractEntityService implements TenantSe
}
tenantValidator.validate(tenant, Tenant::getId);
Tenant savedTenant = tenantDao.save(tenant.getId(), tenant);
publishEvictEvent(new TenantEvictEvent(savedTenant.getId(), false));
if (tenant.getId() == null) {
deviceProfileService.createDefaultDeviceProfile(savedTenant.getId());
apiUsageStateService.createDefaultApiUsageState(savedTenant.getId(), null);
@ -153,10 +166,6 @@ public class TenantServiceImpl extends AbstractEntityService implements TenantSe
@Override
@Transactional(timeout = 60 * 60)
@Caching(evict = {
@CacheEvict(cacheNames = TENANTS_CACHE, key = "{#tenantId, 'TENANT'}"),
@CacheEvict(cacheNames = TENANTS_CACHE, key = "{#tenantId, 'EXISTS'}")
})
public void deleteTenant(TenantId tenantId) {
log.trace("Executing deleteTenant [{}]", tenantId);
Validator.validateId(tenantId, INCORRECT_TENANT_ID + tenantId);
@ -176,6 +185,7 @@ public class TenantServiceImpl extends AbstractEntityService implements TenantSe
rpcService.deleteAllRpcByTenantId(tenantId);
queueService.deleteQueuesByTenantId(tenantId);
tenantDao.removeById(tenantId, tenantId.getId());
publishEvictEvent(new TenantEvictEvent(tenantId, true));
deleteEntityRelations(tenantId, tenantId);
}
@ -212,9 +222,10 @@ public class TenantServiceImpl extends AbstractEntityService implements TenantSe
return tenantDao.findTenantsIds(pageLink);
}
@Cacheable(cacheNames = TENANTS_CACHE, key = "{#tenantId, 'EXISTS'}")
@Override
public boolean tenantExists(TenantId tenantId) {
return tenantDao.existsById(tenantId, tenantId.getId());
return existsTenantCache.getAndPutInTransaction(TenantCacheKey.fromIdExists(tenantId),
() -> tenantDao.existsById(tenantId, tenantId.getId()), false);
}
private PaginatedRemover<TenantId, Tenant> tenantsRemover = new PaginatedRemover<>() {

View File

@ -25,6 +25,8 @@ import org.springframework.boot.test.mock.mockito.SpyBean;
import org.springframework.cache.Cache;
import org.springframework.cache.CacheManager;
import org.thingsboard.common.util.JacksonUtil;
import org.thingsboard.server.cache.TbCacheValueWrapper;
import org.thingsboard.server.cache.TbTransactionalCache;
import org.thingsboard.server.common.data.CacheConstants;
import org.thingsboard.server.common.data.Customer;
import org.thingsboard.server.common.data.Dashboard;
@ -59,6 +61,7 @@ import org.thingsboard.server.common.data.tenant.profile.DefaultTenantProfileCon
import org.thingsboard.server.common.data.tenant.profile.TenantProfileData;
import org.thingsboard.server.common.data.widget.WidgetsBundle;
import org.thingsboard.server.dao.exception.DataValidationException;
import org.thingsboard.server.dao.tenant.TenantCacheKey;
import org.thingsboard.server.dao.tenant.TenantDao;
import java.util.ArrayList;
@ -80,14 +83,10 @@ public abstract class BaseTenantServiceTest extends AbstractServiceTest {
protected TenantDao tenantDao;
@Autowired
CacheManager cacheManager;
protected TbTransactionalCache<TenantCacheKey, Tenant> cache;
private Cache tenantCache;
@Before
public void setup() {
tenantCache = cacheManager.getCache(CacheConstants.TENANTS_CACHE);
}
@Autowired
protected TbTransactionalCache<TenantCacheKey, Boolean> existsTenantCache;
@Test
public void testSaveTenant() {
@ -155,7 +154,6 @@ public abstract class BaseTenantServiceTest extends AbstractServiceTest {
@Test
public void testFindTenants() {
List<Tenant> tenants = new ArrayList<>();
PageLink pageLink = new PageLink(17);
PageData<Tenant> pageData = tenantService.findTenants(pageLink);
@ -335,15 +333,14 @@ public abstract class BaseTenantServiceTest extends AbstractServiceTest {
Tenant savedTenant = tenantService.saveTenant(tenant);
Mockito.reset(tenantDao);
Objects.requireNonNull(tenantCache, "Tenant cache manager is null").evict(List.of(savedTenant.getId(), "TENANT"));
verify(tenantDao, Mockito.times(0)).findById(any(), any());
tenantService.findTenantById(savedTenant.getId());
verify(tenantDao, Mockito.times(1)).findById(eq(savedTenant.getId()), eq(savedTenant.getId().getId()));
Cache.ValueWrapper cachedTenant =
Objects.requireNonNull(tenantCache, "Cache manager is null!").get(List.of(savedTenant.getId(), "TENANT"));
var cachedTenant = cache.get(TenantCacheKey.fromId(savedTenant.getId()));
Assert.assertNotNull("Getting an existing Tenant doesn't add it to the cache!", cachedTenant);
Assert.assertEquals(savedTenant, cachedTenant.get());
for (int i = 0; i < 100; i++) {
tenantService.findTenantById(savedTenant.getId());
@ -360,15 +357,15 @@ public abstract class BaseTenantServiceTest extends AbstractServiceTest {
Tenant savedTenant = tenantService.saveTenant(tenant);
Mockito.reset(tenantDao);
tenantCache.clear();
//fromIdExists invoked from device profile validator
existsTenantCache.evict(TenantCacheKey.fromIdExists(savedTenant.getTenantId()));
verify(tenantDao, Mockito.times(0)).existsById(any(), any());
tenantService.tenantExists(savedTenant.getId());
verify(tenantDao, Mockito.times(1)).existsById(eq(savedTenant.getId()), eq(savedTenant.getId().getId()));
Cache.ValueWrapper cachedExists =
Objects.requireNonNull(tenantCache, "Cache manager is null!").get(List.of(savedTenant.getId(), "EXISTS"));
Assert.assertNotNull("Getting an existing Tenant doesn't add it to the cache!", cachedExists);
var isExists = existsTenantCache.get(TenantCacheKey.fromIdExists(savedTenant.getId()));
Assert.assertNotNull("Getting an existing Tenant doesn't add it to the cache!", isExists);
for (int i = 0; i < 100; i++) {
tenantService.tenantExists(savedTenant.getId());
@ -384,16 +381,18 @@ public abstract class BaseTenantServiceTest extends AbstractServiceTest {
tenant.setTitle("My tenant");
Tenant savedTenant = tenantService.saveTenant(tenant);
Cache.ValueWrapper cachedTenant =
Objects.requireNonNull(tenantCache, "Cache manager is null!").get(List.of(savedTenant.getId(), "TENANT"));
tenantService.findTenantById(savedTenant.getId());
var cachedTenant = cache.get(TenantCacheKey.fromId(savedTenant.getId()));
Assert.assertNotNull("Saving a Tenant doesn't add it to the cache!", cachedTenant);
Assert.assertEquals(savedTenant, cachedTenant.get());
savedTenant.setTitle("My new tenant");
savedTenant = tenantService.saveTenant(savedTenant);
Mockito.reset(tenantDao);
cachedTenant = Objects.requireNonNull(tenantCache, "Cache manager is null!").get(List.of(savedTenant.getId(), "TENANT"));
cachedTenant = cache.get(TenantCacheKey.fromId(savedTenant.getId()));
Assert.assertNull("Updating a Tenant doesn't evict the cache!", cachedTenant);
verify(tenantDao, Mockito.times(0)).findById(any(), any());
@ -409,20 +408,21 @@ public abstract class BaseTenantServiceTest extends AbstractServiceTest {
tenant.setTitle("My tenant");
Tenant savedTenant = tenantService.saveTenant(tenant);
tenantService.findTenantById(savedTenant.getId());
tenantService.tenantExists(savedTenant.getId());
Cache.ValueWrapper cachedTenant =
Objects.requireNonNull(tenantCache, "Cache manager is null!").get(List.of(savedTenant.getId(), "TENANT"));
Cache.ValueWrapper cachedExists =
Objects.requireNonNull(tenantCache, "Cache manager is null!").get(List.of(savedTenant.getId(), "EXISTS"));
var cachedTenant =
cache.get(TenantCacheKey.fromId(savedTenant.getId()));
var cachedExists =
existsTenantCache.get(TenantCacheKey.fromIdExists(savedTenant.getId()));
Assert.assertNotNull("Saving a Tenant doesn't add it to the cache!", cachedTenant);
Assert.assertNotNull("Saving a Tenant doesn't add it to the cache!", cachedExists);
tenantService.deleteTenant(savedTenant.getId());
cachedTenant =
Objects.requireNonNull(tenantCache, "Cache manager is null!").get(List.of(savedTenant.getId(), "TENANT"));
cache.get(TenantCacheKey.fromId(savedTenant.getId()));
cachedExists =
Objects.requireNonNull(tenantCache, "Cache manager is null!").get(List.of(savedTenant.getId(), "EXISTS"));
existsTenantCache.get(TenantCacheKey.fromIdExists(savedTenant.getId()));
Assert.assertNull("Removing a Tenant doesn't evict the cache!", cachedTenant);

View File

@ -35,8 +35,8 @@ cache.specs.entityViews.maxSize=100000
cache.specs.claimDevices.timeToLiveInMinutes=1440
cache.specs.claimDevices.maxSize=100000
caffeine.specs.tenants.timeToLiveInMinutes=1440
caffeine.specs.tenants.maxSize=100000
cache.specs.tenants.timeToLiveInMinutes=1440
cache.specs.tenants.maxSize=100000
cache.specs.securitySettings.timeToLiveInMinutes=1440
cache.specs.securitySettings.maxSize=100000