Browse Source

Merge branch 'lts_4.3/cacheCleanupFix' into cfCacheCleanupFix

pull/15280/head
dashevchenko 2 months ago
parent
commit
0b101958fa
  1. 52
      application/src/main/java/org/thingsboard/server/service/cf/DefaultCalculatedFieldCache.java
  2. 2
      application/src/main/java/org/thingsboard/server/service/cf/ctx/state/CalculatedFieldCtx.java
  3. 23
      application/src/main/java/org/thingsboard/server/service/profile/DefaultTbAssetProfileCache.java
  4. 23
      application/src/main/java/org/thingsboard/server/service/profile/DefaultTbDeviceProfileCache.java
  5. 183
      application/src/test/java/org/thingsboard/server/service/cf/DefaultCalculatedFieldCacheTest.java
  6. 1
      application/src/test/java/org/thingsboard/server/service/profile/DefaultTbAssetProfileCacheTest.java

52
application/src/main/java/org/thingsboard/server/service/cf/DefaultCalculatedFieldCache.java

@ -18,7 +18,6 @@ package org.thingsboard.server.service.cf;
import lombok.Getter;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.jspecify.annotations.Nullable;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Lazy;
import org.springframework.context.event.EventListener;
@ -358,41 +357,40 @@ public class DefaultCalculatedFieldCache implements CalculatedFieldCache {
}
private void evictTenantCfs(TenantId tenantId) {
var removedCfIds = new HashSet<CalculatedFieldId>();
var removedCfEntityIds = new HashSet<EntityId>();
var removedLinkEntityIds = new HashSet<EntityId>();
for (Map.Entry<CalculatedFieldId, CalculatedField> entry : calculatedFields.entrySet()) {
CalculatedFieldId cfId = entry.getKey();
CalculatedField cf = entry.getValue();
if (cf.getTenantId().equals(tenantId)) {
calculatedFields.remove(cfId);
List<CalculatedFieldLink> links = calculatedFieldLinks.remove(cfId);
if (links != null) {
links.forEach(link -> removedLinkEntityIds.add(link.entityId()));
}
calculatedFieldsCtx.remove(cfId);
removedCfIds.add(cfId);
var toRemove = calculatedFields.entrySet().stream()
.filter(e -> e.getValue().getTenantId().equals(tenantId))
.map(Map.Entry::getKey)
.collect(Collectors.toSet());
toRemove.forEach(cfId -> {
CalculatedField cf = calculatedFields.remove(cfId);
List<CalculatedFieldLink> links = calculatedFieldLinks.remove(cfId);
if (links != null) {
links.forEach(link -> removedLinkEntityIds.add(link.entityId()));
}
calculatedFieldsCtx.remove(cfId);
if (cf != null) {
removedCfEntityIds.add(cf.getEntityId());
log.debug("[{}] evict calculated field from cache on tenant deletion: {}", cfId, cf);
}
}
});
removedCfEntityIds.forEach(entityId -> {
List<CalculatedField> cfs = entityIdCalculatedFields.get(entityId);
if (cfs != null) {
cfs.removeIf(cf -> removedCfIds.contains(cf.getId()));
if (cfs.isEmpty()) {
entityIdCalculatedFields.remove(entityId);
entityIdCalculatedFields.compute(entityId, (k, cfs) -> {
if (cfs != null) {
cfs.removeIf(cf -> toRemove.contains(cf.getId()));
return cfs.isEmpty() ? null : cfs;
}
}
return null;
});
});
removedLinkEntityIds.forEach(entityId -> {
List<CalculatedFieldLink> entityLinks = entityIdCalculatedFieldLinks.get(entityId);
if (entityLinks != null) {
entityLinks.removeIf(link -> removedCfIds.contains(link.calculatedFieldId()));
if (entityLinks.isEmpty()) {
entityIdCalculatedFieldLinks.remove(entityId);
entityIdCalculatedFieldLinks.compute(entityId, ((entityId1, links) -> {
if (links != null) {
links.removeIf(link -> toRemove.contains(link.calculatedFieldId()));
return links.isEmpty() ? null : links;
}
}
return null;
}));
});
}

2
application/src/main/java/org/thingsboard/server/service/cf/ctx/state/CalculatedFieldCtx.java

@ -305,7 +305,7 @@ public class CalculatedFieldCtx implements Closeable {
public void setTenantProfileProperties() {
TenantProfile tenantProfile = systemContext.getTenantProfileCache().get(tenantId);
if (tenantProfile == null) {
log.warn("Tenant Profile not found for tenant: {}. Using default values for CF configuration.", tenantId);
log.warn("[{}][{}][{}] Tenant Profile not found for tenant: {}. CF limits and thresholds will not be updated.", tenantId, entityId, cfId, tenantId);
return;
}
tenantProfile.getProfileConfiguration().ifPresent(config -> {

23
application/src/main/java/org/thingsboard/server/service/profile/DefaultTbAssetProfileCache.java

@ -29,14 +29,14 @@ import org.thingsboard.server.common.msg.plugin.ComponentLifecycleMsg;
import org.thingsboard.server.dao.asset.AssetProfileService;
import org.thingsboard.server.dao.asset.AssetService;
import java.util.HashSet;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ConcurrentMap;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
import java.util.function.BiConsumer;
import java.util.function.Consumer;
import java.util.stream.Collectors;
@Service
@Slf4j
@ -154,19 +154,12 @@ public class DefaultTbAssetProfileCache implements TbAssetProfileCache {
case TENANT:
if (event.getEvent() == ComponentLifecycleEvent.DELETED) {
TenantId tenantId = event.getTenantId();
var removedProfileIds = new HashSet<AssetProfileId>();
for (Map.Entry<AssetProfileId, AssetProfile> entry : assetProfilesMap.entrySet()) {
if (entry.getValue().getTenantId().equals(tenantId)) {
assetProfilesMap.remove(entry.getKey());
removedProfileIds.add(entry.getKey());
log.debug("[{}] evict asset profile from cache: {}", entry.getKey(), entry.getValue());
}
}
for (Map.Entry<AssetId, AssetProfileId> entry : assetsMap.entrySet()) {
if (removedProfileIds.contains(entry.getValue())) {
assetsMap.remove(entry.getKey());
}
}
Set<AssetProfileId> toRemove = assetProfilesMap.values().stream()
.filter(assetProfile -> assetProfile.getTenantId().equals(tenantId))
.map(AssetProfile::getId)
.collect(Collectors.toSet());
assetProfilesMap.keySet().removeAll(toRemove);
assetsMap.entrySet().removeIf(entry -> toRemove.contains(entry.getValue()));
profileListeners.remove(tenantId);
assetProfileListeners.remove(tenantId);
}

23
application/src/main/java/org/thingsboard/server/service/profile/DefaultTbDeviceProfileCache.java

@ -29,14 +29,14 @@ import org.thingsboard.server.common.msg.plugin.ComponentLifecycleMsg;
import org.thingsboard.server.dao.device.DeviceProfileService;
import org.thingsboard.server.dao.device.DeviceService;
import java.util.HashSet;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ConcurrentMap;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
import java.util.function.BiConsumer;
import java.util.function.Consumer;
import java.util.stream.Collectors;
@Service
@Slf4j
@ -154,19 +154,12 @@ public class DefaultTbDeviceProfileCache implements TbDeviceProfileCache {
case TENANT:
if (event.getEvent() == ComponentLifecycleEvent.DELETED) {
TenantId tenantId = event.getTenantId();
var removedProfileIds = new HashSet<DeviceProfileId>();
for (Map.Entry<DeviceProfileId, DeviceProfile> entry : deviceProfilesMap.entrySet()) {
if (entry.getValue().getTenantId().equals(tenantId)) {
deviceProfilesMap.remove(entry.getKey());
removedProfileIds.add(entry.getKey());
log.debug("[{}] evict device profile from cache: {}", entry.getKey(), entry.getValue());
}
}
for (Map.Entry<DeviceId, DeviceProfileId> entry : devicesMap.entrySet()) {
if (removedProfileIds.contains(entry.getValue())) {
devicesMap.remove(entry.getKey());
}
}
Set<DeviceProfileId> toRemove = deviceProfilesMap.values().stream()
.filter(deviceProfile -> deviceProfile.getTenantId().equals(tenantId))
.map(DeviceProfile::getId)
.collect(Collectors.toSet());
deviceProfilesMap.keySet().removeAll(toRemove);
devicesMap.entrySet().removeIf(entry -> toRemove.contains(entry.getValue()));
profileListeners.remove(tenantId);
deviceProfileListeners.remove(tenantId);
}

183
application/src/test/java/org/thingsboard/server/service/cf/DefaultCalculatedFieldCacheTest.java

@ -26,11 +26,14 @@ import org.thingsboard.server.common.data.cf.CalculatedFieldLink;
import org.thingsboard.server.common.data.cf.CalculatedFieldType;
import org.thingsboard.server.common.data.cf.configuration.CalculatedFieldConfiguration;
import org.thingsboard.server.common.data.id.AssetId;
import org.thingsboard.server.common.data.id.AssetProfileId;
import org.thingsboard.server.common.data.id.CalculatedFieldId;
import org.thingsboard.server.common.data.id.CustomerId;
import org.thingsboard.server.common.data.id.DeviceId;
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.id.TenantProfileId;
import org.thingsboard.server.common.data.page.PageData;
import org.thingsboard.server.common.data.plugin.ComponentLifecycleEvent;
import org.thingsboard.server.common.msg.plugin.ComponentLifecycleMsg;
@ -51,6 +54,9 @@ import static org.assertj.core.api.Assertions.assertThat;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.eq;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.never;
import static org.mockito.Mockito.spy;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
@ExtendWith(MockitoExtension.class)
@ -225,6 +231,112 @@ public class DefaultCalculatedFieldCacheTest {
assertThat(cache.getDynamicEntities(tenant, customer)).doesNotContain(device);
}
// --- DeviceProfile/AssetProfile deletion tests ---
@Test
public void onComponentLifecycleEvent_deviceProfileDeleted_evictsCfsForThatProfile() {
TenantId tenant = new TenantId(UUID.randomUUID());
DeviceProfileId profileId = new DeviceProfileId(UUID.randomUUID());
CalculatedField cf = addCfToCache(tenant, profileId);
cache.onComponentLifecycleEvent(new ComponentLifecycleMsg(tenant, profileId, ComponentLifecycleEvent.DELETED));
assertThat(cache.getCalculatedField(cf.getId())).isNull();
assertThat(cache.getCalculatedFieldsByEntityId(profileId)).isEmpty();
}
@Test
public void onComponentLifecycleEvent_deviceProfileDeleted_removesLinksForLinkedEntities() {
TenantId tenant = new TenantId(UUID.randomUUID());
DeviceProfileId profileId = new DeviceProfileId(UUID.randomUUID());
DeviceId linkedDevice = new DeviceId(UUID.randomUUID());
addCfToCache(tenant, profileId, linkedDevice);
cache.onComponentLifecycleEvent(new ComponentLifecycleMsg(tenant, profileId, ComponentLifecycleEvent.DELETED));
assertThat(cache.getCalculatedFieldLinksByEntityId(linkedDevice)).isEmpty();
}
@Test
public void onComponentLifecycleEvent_deviceProfileDeleted_doesNotEvictOtherProfilesCfs() {
TenantId tenant = new TenantId(UUID.randomUUID());
DeviceProfileId profile1 = new DeviceProfileId(UUID.randomUUID());
DeviceProfileId profile2 = new DeviceProfileId(UUID.randomUUID());
CalculatedField cf1 = addCfToCache(tenant, profile1);
CalculatedField cf2 = addCfToCache(tenant, profile2);
cache.onComponentLifecycleEvent(new ComponentLifecycleMsg(tenant, profile1, ComponentLifecycleEvent.DELETED));
assertThat(cache.getCalculatedField(cf1.getId())).isNull();
assertThat(cache.getCalculatedFieldsByEntityId(profile1)).isEmpty();
assertThat(cache.getCalculatedField(cf2.getId())).isEqualTo(cf2);
assertThat(cache.getCalculatedFieldsByEntityId(profile2)).containsExactly(cf2);
}
@Test
public void onComponentLifecycleEvent_deviceProfileUpdated_doesNotEvictCfs() {
TenantId tenant = new TenantId(UUID.randomUUID());
DeviceProfileId profileId = new DeviceProfileId(UUID.randomUUID());
CalculatedField cf = addCfToCache(tenant, profileId);
cache.onComponentLifecycleEvent(new ComponentLifecycleMsg(tenant, profileId, ComponentLifecycleEvent.UPDATED));
assertThat(cache.getCalculatedField(cf.getId())).isEqualTo(cf);
assertThat(cache.getCalculatedFieldsByEntityId(profileId)).containsExactly(cf);
}
@Test
public void onComponentLifecycleEvent_assetProfileDeleted_evictsCfsForThatProfile() {
TenantId tenant = new TenantId(UUID.randomUUID());
AssetProfileId profileId = new AssetProfileId(UUID.randomUUID());
CalculatedField cf = addCfToCache(tenant, profileId);
cache.onComponentLifecycleEvent(new ComponentLifecycleMsg(tenant, profileId, ComponentLifecycleEvent.DELETED));
assertThat(cache.getCalculatedField(cf.getId())).isNull();
assertThat(cache.getCalculatedFieldsByEntityId(profileId)).isEmpty();
}
@Test
public void onComponentLifecycleEvent_assetProfileDeleted_removesLinksForLinkedEntities() {
TenantId tenant = new TenantId(UUID.randomUUID());
AssetProfileId profileId = new AssetProfileId(UUID.randomUUID());
AssetId linkedAsset = new AssetId(UUID.randomUUID());
addCfToCache(tenant, profileId, linkedAsset);
cache.onComponentLifecycleEvent(new ComponentLifecycleMsg(tenant, profileId, ComponentLifecycleEvent.DELETED));
assertThat(cache.getCalculatedFieldLinksByEntityId(linkedAsset)).isEmpty();
}
@Test
public void onComponentLifecycleEvent_assetProfileDeleted_doesNotEvictOtherProfilesCfs() {
TenantId tenant = new TenantId(UUID.randomUUID());
AssetProfileId profile1 = new AssetProfileId(UUID.randomUUID());
AssetProfileId profile2 = new AssetProfileId(UUID.randomUUID());
CalculatedField cf1 = addCfToCache(tenant, profile1);
CalculatedField cf2 = addCfToCache(tenant, profile2);
cache.onComponentLifecycleEvent(new ComponentLifecycleMsg(tenant, profile1, ComponentLifecycleEvent.DELETED));
assertThat(cache.getCalculatedField(cf1.getId())).isNull();
assertThat(cache.getCalculatedFieldsByEntityId(profile1)).isEmpty();
assertThat(cache.getCalculatedField(cf2.getId())).isEqualTo(cf2);
assertThat(cache.getCalculatedFieldsByEntityId(profile2)).containsExactly(cf2);
}
@Test
public void onComponentLifecycleEvent_assetProfileUpdated_doesNotEvictCfs() {
TenantId tenant = new TenantId(UUID.randomUUID());
AssetProfileId profileId = new AssetProfileId(UUID.randomUUID());
CalculatedField cf = addCfToCache(tenant, profileId);
cache.onComponentLifecycleEvent(new ComponentLifecycleMsg(tenant, profileId, ComponentLifecycleEvent.UPDATED));
assertThat(cache.getCalculatedField(cf.getId())).isEqualTo(cf);
assertThat(cache.getCalculatedFieldsByEntityId(profileId)).containsExactly(cf);
}
// --- CalculatedField lifecycle tests ---
@Test
@ -268,6 +380,77 @@ public class DefaultCalculatedFieldCacheTest {
assertThat(cache.getCalculatedField(cf.getId())).isEqualTo(updatedCf);
}
// --- evictOwner recursive traversal tests ---
@Test
public void evictOwner_customerDeleted_recursivelyEvictsDevicesOwnedByThatCustomer() {
TenantId tenant = new TenantId(UUID.randomUUID());
CustomerId customer = new CustomerId(UUID.randomUUID());
DeviceId device = new DeviceId(UUID.randomUUID());
stubDeviceOwner(tenant, device, customer);
when(customerService.findCustomersByTenantId(any(), any())).thenReturn(PageData.emptyPageData());
// tenant owns customer (getOwner for CUSTOMER returns tenantId)
cache.addOwnerEntity(tenant, customer); // ownerEntities[tenant] = {customer}
cache.addOwnerEntity(tenant, device); // ownerEntities[customer] = {device}
assertThat(cache.getDynamicEntities(tenant, tenant)).contains(customer);
assertThat(cache.getDynamicEntities(tenant, customer)).contains(device);
// deleting the customer evicts the customer key and recursively cleans its owned set
cache.onComponentLifecycleEvent(new ComponentLifecycleMsg(tenant, customer, ComponentLifecycleEvent.DELETED));
assertThat(cache.getDynamicEntities(tenant, customer)).doesNotContain(device);
}
@Test
public void evictOwner_tenantDeleted_recursivelyEvictsCustomerAndItsOwnedDevices() {
TenantId tenant = new TenantId(UUID.randomUUID());
CustomerId customer = new CustomerId(UUID.randomUUID());
DeviceId device = new DeviceId(UUID.randomUUID());
stubDeviceOwner(tenant, device, customer);
when(customerService.findCustomersByTenantId(any(), any())).thenReturn(PageData.emptyPageData());
cache.addOwnerEntity(tenant, customer); // ownerEntities[tenant] = {customer}
cache.addOwnerEntity(tenant, device); // ownerEntities[customer] = {device}
assertThat(cache.getDynamicEntities(tenant, tenant)).contains(customer);
assertThat(cache.getDynamicEntities(tenant, customer)).contains(device);
// deleting the tenant: evictOwner(tenant) finds customer (CUSTOMER type) and recurses into it
cache.onComponentLifecycleEvent(new ComponentLifecycleMsg(tenant, tenant, ComponentLifecycleEvent.DELETED));
// both levels must be gone
assertThat(cache.getDynamicEntities(tenant, tenant)).doesNotContain(customer);
assertThat(cache.getDynamicEntities(tenant, customer)).doesNotContain(device);
}
// --- TenantProfile lifecycle tests ---
@Test
public void onComponentLifecycleEvent_tenantProfileUpdated_callsHandleTenantProfileUpdate() {
TenantId tenant = new TenantId(UUID.randomUUID());
TenantProfileId profileId = new TenantProfileId(UUID.randomUUID());
DefaultCalculatedFieldCache spyCache = spy(cache);
spyCache.onComponentLifecycleEvent(new ComponentLifecycleMsg(tenant, profileId, ComponentLifecycleEvent.UPDATED));
verify(spyCache).handleTenantProfileUpdate(profileId);
}
@Test
public void onComponentLifecycleEvent_tenantProfileDeleted_doesNotCallHandleTenantProfileUpdate() {
TenantId tenant = new TenantId(UUID.randomUUID());
TenantProfileId profileId = new TenantProfileId(UUID.randomUUID());
DefaultCalculatedFieldCache spyCache = spy(cache);
spyCache.onComponentLifecycleEvent(new ComponentLifecycleMsg(tenant, profileId, ComponentLifecycleEvent.DELETED));
verify(spyCache, never()).handleTenantProfileUpdate(any());
}
// --- Helpers ---
private void stubDeviceOwner(TenantId tenantId, DeviceId deviceId, EntityId ownerId) {

1
application/src/test/java/org/thingsboard/server/service/profile/DefaultTbAssetProfileCacheTest.java

@ -70,7 +70,6 @@ public class DefaultTbAssetProfileCacheTest {
// After deletion tenant1 profile should be reloaded from service on next get
when(assetProfileService.findAssetProfileById(any(), any())).thenReturn(null);
assertThat(cache.get(tenant1, profileId1)).isNull();
// tenant2 profile should still be served from cache (no extra service call)
verify(assetProfileService, times(1)).findAssetProfileById(tenant2, profileId2);
}

Loading…
Cancel
Save