Browse Source

Merge pull request #15280 from dashevchenko/cfCacheCleanupFix

Fixed cache cleanup on tenant/entity deletion
pull/15424/head
Viacheslav Klimov 2 months ago
committed by GitHub
parent
commit
b2368302e9
No known key found for this signature in database GPG Key ID: B5690EEEBB952194
  1. 2
      application/src/main/java/org/thingsboard/server/service/cf/CalculatedFieldCache.java
  2. 126
      application/src/main/java/org/thingsboard/server/service/cf/DefaultCalculatedFieldCache.java
  3. 3
      application/src/main/java/org/thingsboard/server/service/cf/ctx/state/CalculatedFieldCtx.java
  4. 24
      application/src/main/java/org/thingsboard/server/service/profile/DefaultTbAssetProfileCache.java
  5. 24
      application/src/main/java/org/thingsboard/server/service/profile/DefaultTbDeviceProfileCache.java
  6. 4
      application/src/main/java/org/thingsboard/server/service/queue/DefaultTbCalculatedFieldConsumerService.java
  7. 4
      application/src/main/java/org/thingsboard/server/service/queue/DefaultTbCoreConsumerService.java
  8. 2
      application/src/main/java/org/thingsboard/server/service/queue/DefaultTbEdgeConsumerService.java
  9. 4
      application/src/main/java/org/thingsboard/server/service/queue/DefaultTbRuleEngineConsumerService.java
  10. 35
      application/src/main/java/org/thingsboard/server/service/queue/processing/AbstractConsumerService.java
  11. 4
      application/src/main/java/org/thingsboard/server/service/queue/processing/AbstractPartitionBasedConsumerService.java
  12. 517
      application/src/test/java/org/thingsboard/server/service/cf/DefaultCalculatedFieldCacheTest.java
  13. 159
      application/src/test/java/org/thingsboard/server/service/profile/DefaultTbAssetProfileCacheTest.java
  14. 160
      application/src/test/java/org/thingsboard/server/service/profile/DefaultTbDeviceProfileCacheTest.java

2
application/src/main/java/org/thingsboard/server/service/cf/CalculatedFieldCache.java

@ -61,7 +61,7 @@ public interface CalculatedFieldCache {
void addOwnerEntity(TenantId tenantId, EntityId entityId);
void evictEntity(EntityId entityId);
void evictOwnerEntity(EntityId entityId);
void evictOwner(EntityId owner);

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

@ -20,6 +20,7 @@ import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Lazy;
import org.springframework.context.event.EventListener;
import org.springframework.stereotype.Service;
import org.springframework.util.ConcurrentReferenceHashMap;
import org.thingsboard.server.actors.ActorSystemContext;
@ -37,6 +38,8 @@ import org.thingsboard.server.common.data.id.HasId;
import org.thingsboard.server.common.data.id.TenantId;
import org.thingsboard.server.common.data.id.TenantProfileId;
import org.thingsboard.server.common.data.page.PageDataIterable;
import org.thingsboard.server.common.data.plugin.ComponentLifecycleEvent;
import org.thingsboard.server.common.msg.plugin.ComponentLifecycleMsg;
import org.thingsboard.server.dao.cf.CalculatedFieldService;
import org.thingsboard.server.dao.tenant.TbTenantProfileCache;
import org.thingsboard.server.queue.util.AfterStartUp;
@ -45,7 +48,9 @@ import org.thingsboard.server.service.profile.TbAssetProfileCache;
import org.thingsboard.server.service.profile.TbDeviceProfileCache;
import java.util.Collections;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ConcurrentMap;
@ -53,6 +58,7 @@ import java.util.concurrent.CopyOnWriteArrayList;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
import java.util.function.Predicate;
import java.util.stream.Collectors;
import java.util.stream.Stream;
@Service
@ -268,18 +274,25 @@ public class DefaultCalculatedFieldCache implements CalculatedFieldCache {
@Override
public void updateOwnerEntity(TenantId tenantId, EntityId entityId) {
evictEntity(entityId);
evictOwnerEntity(entityId);
addOwnerEntity(tenantId, entityId);
}
@Override
public void evictEntity(EntityId entityId) {
public void evictOwnerEntity(EntityId entityId) {
ownerEntities.values().forEach(entities -> entities.remove(entityId));
}
@Override
public void evictOwner(EntityId owner) {
ownerEntities.remove(owner);
Set<EntityId> removedEntities = ownerEntities.remove(owner);
if (removedEntities != null) {
Set<EntityId> removedCustomers = removedEntities
.stream()
.filter(entityId -> entityId.getEntityType() == EntityType.CUSTOMER)
.collect(Collectors.toSet());
removedCustomers.forEach(this::evictOwner);
}
}
private Set<EntityId> getOwnedEntities(TenantId tenantId, EntityId ownerId) {
@ -290,6 +303,113 @@ public class DefaultCalculatedFieldCache implements CalculatedFieldCache {
});
}
@EventListener(ComponentLifecycleMsg.class)
public void onComponentLifecycleEvent(ComponentLifecycleMsg event) {
switch (event.getEntityId().getEntityType()) {
case TENANT_PROFILE:
if (event.getEvent() == ComponentLifecycleEvent.UPDATED) {
TenantProfileId tenantProfileId = new TenantProfileId(event.getEntityId().getId());
handleTenantProfileUpdate(tenantProfileId);
}
break;
case TENANT:
if (event.getEvent() == ComponentLifecycleEvent.DELETED) {
TenantId tenantId = event.getTenantId();
evictTenantCfs(tenantId);
evictOwner(tenantId);
}
break;
case CUSTOMER:
if (event.getEvent() == ComponentLifecycleEvent.CREATED) {
addOwnerEntity(event.getTenantId(), event.getEntityId());
} else if (event.getEvent() == ComponentLifecycleEvent.UPDATED && event.isOwnerChanged()) {
updateOwnerEntity(event.getTenantId(), event.getEntityId());
} else if (event.getEvent() == ComponentLifecycleEvent.DELETED) {
evictOwner(event.getEntityId());
evictOwnerEntity(event.getEntityId());
}
break;
case DEVICE, ASSET:
if (event.getEvent() == ComponentLifecycleEvent.CREATED) {
addOwnerEntity(event.getTenantId(), event.getEntityId());
} else if (event.getEvent() == ComponentLifecycleEvent.UPDATED && event.isOwnerChanged()) {
updateOwnerEntity(event.getTenantId(), event.getEntityId());
} else if (event.getEvent() == ComponentLifecycleEvent.DELETED) {
evictOwnerEntity(event.getEntityId());
evictEntityCfs(event.getEntityId());
}
break;
case DEVICE_PROFILE, ASSET_PROFILE:
if (event.getEvent() == ComponentLifecycleEvent.DELETED) {
evictEntityCfs(event.getEntityId());
}
break;
case CALCULATED_FIELD:
if (event.getEvent() == ComponentLifecycleEvent.CREATED) {
addCalculatedField(event.getTenantId(), (CalculatedFieldId) event.getEntityId());
} else if (event.getEvent() == ComponentLifecycleEvent.UPDATED) {
updateCalculatedField(event.getTenantId(), (CalculatedFieldId) event.getEntityId());
} else {
evict((CalculatedFieldId) event.getEntityId());
}
break;
}
}
private void evictTenantCfs(TenantId tenantId) {
var removedCfEntityIds = new HashSet<EntityId>();
var removedLinkEntityIds = new HashSet<EntityId>();
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());
}
});
removedCfEntityIds.forEach(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 -> {
entityIdCalculatedFieldLinks.compute(entityId, ((entityId1, links) -> {
if (links != null) {
links.removeIf(link -> toRemove.contains(link.calculatedFieldId()));
return links.isEmpty() ? null : links;
}
return null;
}));
});
}
private void evictEntityCfs(EntityId entityId) {
List<CalculatedField> cfs = entityIdCalculatedFields.remove(entityId);
if (cfs != null) {
var cfIds = new HashSet<CalculatedFieldId>();
cfs.forEach(cf -> {
calculatedFields.remove(cf.getId());
calculatedFieldLinks.remove(cf.getId());
calculatedFieldsCtx.remove(cf.getId());
cfIds.add(cf.getId());
log.debug("[{}] evict calculated field from cache on entity deletion: {}", cf.getId(), cf);
});
entityIdCalculatedFieldLinks.values().forEach(list -> list.removeIf(link -> cfIds.contains(link.calculatedFieldId())));
}
entityIdCalculatedFieldLinks.remove(entityId);
}
private Lock getFetchLock(CalculatedFieldId id) {
return calculatedFieldFetchLocks.computeIfAbsent(id, __ -> new ReentrantLock());
}

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

@ -305,7 +305,8 @@ public class CalculatedFieldCtx implements Closeable {
public void setTenantProfileProperties() {
TenantProfile tenantProfile = systemContext.getTenantProfileCache().get(tenantId);
if (tenantProfile == null) {
throw new IllegalStateException("Tenant Profile not found for tenant: " + 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 -> {
this.maxStateSize = config.getMaxStateSizeInKBytes() * 1024L;

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

@ -16,6 +16,7 @@
package org.thingsboard.server.service.profile;
import lombok.extern.slf4j.Slf4j;
import org.springframework.context.event.EventListener;
import org.springframework.stereotype.Service;
import org.thingsboard.server.common.data.asset.Asset;
import org.thingsboard.server.common.data.asset.AssetProfile;
@ -23,15 +24,19 @@ import org.thingsboard.server.common.data.id.AssetId;
import org.thingsboard.server.common.data.id.AssetProfileId;
import org.thingsboard.server.common.data.id.EntityId;
import org.thingsboard.server.common.data.id.TenantId;
import org.thingsboard.server.common.data.plugin.ComponentLifecycleEvent;
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.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
@ -143,6 +148,25 @@ public class DefaultTbAssetProfileCache implements TbAssetProfileCache {
}
}
@EventListener(ComponentLifecycleMsg.class)
public void onComponentLifecycleEvent(ComponentLifecycleMsg event) {
switch (event.getEntityId().getEntityType()) {
case TENANT:
if (event.getEvent() == ComponentLifecycleEvent.DELETED) {
TenantId tenantId = event.getTenantId();
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);
}
break;
}
}
private void notifyProfileListeners(AssetProfile profile) {
ConcurrentMap<EntityId, Consumer<AssetProfile>> tenantListeners = profileListeners.get(profile.getTenantId());
if (tenantListeners != null) {

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

@ -16,6 +16,7 @@
package org.thingsboard.server.service.profile;
import lombok.extern.slf4j.Slf4j;
import org.springframework.context.event.EventListener;
import org.springframework.stereotype.Service;
import org.thingsboard.server.common.data.Device;
import org.thingsboard.server.common.data.DeviceProfile;
@ -23,15 +24,19 @@ 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.plugin.ComponentLifecycleEvent;
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.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
@ -143,6 +148,25 @@ public class DefaultTbDeviceProfileCache implements TbDeviceProfileCache {
}
}
@EventListener(ComponentLifecycleMsg.class)
public void onComponentLifecycleEvent(ComponentLifecycleMsg event) {
switch (event.getEntityId().getEntityType()) {
case TENANT:
if (event.getEvent() == ComponentLifecycleEvent.DELETED) {
TenantId tenantId = event.getTenantId();
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);
}
break;
}
}
private void notifyProfileListeners(DeviceProfile profile) {
ConcurrentMap<EntityId, Consumer<DeviceProfile>> tenantListeners = profileListeners.get(profile.getTenantId());
if (tenantListeners != null) {

4
application/src/main/java/org/thingsboard/server/service/queue/DefaultTbCalculatedFieldConsumerService.java

@ -51,7 +51,6 @@ import org.thingsboard.server.queue.discovery.event.PartitionChangeEvent;
import org.thingsboard.server.queue.provider.TbRuleEngineQueueFactory;
import org.thingsboard.server.queue.util.TbRuleEngineComponent;
import org.thingsboard.server.service.apiusage.TbApiUsageStateService;
import org.thingsboard.server.service.cf.CalculatedFieldCache;
import org.thingsboard.server.service.cf.CalculatedFieldStateService;
import org.thingsboard.server.service.profile.TbAssetProfileCache;
import org.thingsboard.server.service.profile.TbDeviceProfileCache;
@ -91,9 +90,8 @@ public class DefaultTbCalculatedFieldConsumerService extends AbstractPartitionBa
PartitionService partitionService,
ApplicationEventPublisher eventPublisher,
JwtSettingsService jwtSettingsService,
CalculatedFieldCache calculatedFieldCache,
CalculatedFieldStateService stateService) {
super(actorContext, tenantProfileCache, deviceProfileCache, assetProfileCache, tbResourceDataCache, calculatedFieldCache, apiUsageStateService, partitionService,
super(actorContext, tenantProfileCache, deviceProfileCache, assetProfileCache, tbResourceDataCache, apiUsageStateService, partitionService,
eventPublisher, jwtSettingsService);
this.queueFactory = tbQueueFactory;
this.stateService = stateService;

4
application/src/main/java/org/thingsboard/server/service/queue/DefaultTbCoreConsumerService.java

@ -86,7 +86,6 @@ import org.thingsboard.server.queue.discovery.event.PartitionChangeEvent;
import org.thingsboard.server.queue.provider.TbCoreQueueFactory;
import org.thingsboard.server.queue.util.TbCoreComponent;
import org.thingsboard.server.service.apiusage.TbApiUsageStateService;
import org.thingsboard.server.service.cf.CalculatedFieldCache;
import org.thingsboard.server.service.notification.NotificationSchedulerService;
import org.thingsboard.server.service.ota.OtaPackageStateService;
import org.thingsboard.server.service.profile.TbAssetProfileCache;
@ -179,9 +178,8 @@ public class DefaultTbCoreConsumerService extends AbstractConsumerService<ToCore
TbImageService imageService,
TbResourceDataCache tbResourceDataCache,
RuleEngineCallService ruleEngineCallService,
CalculatedFieldCache calculatedFieldCache,
EdqsService edqsService) {
super(actorContext, tenantProfileCache, deviceProfileCache, assetProfileCache, tbResourceDataCache, calculatedFieldCache, apiUsageStateService, partitionService,
super(actorContext, tenantProfileCache, deviceProfileCache, assetProfileCache, tbResourceDataCache, apiUsageStateService, partitionService,
eventPublisher, jwtSettingsService);
this.stateService = stateService;
this.localSubscriptionService = localSubscriptionService;

2
application/src/main/java/org/thingsboard/server/service/queue/DefaultTbEdgeConsumerService.java

@ -87,7 +87,7 @@ public class DefaultTbEdgeConsumerService extends AbstractConsumerService<ToEdge
public DefaultTbEdgeConsumerService(TbCoreQueueFactory tbCoreQueueFactory, ActorSystemContext actorContext,
StatsFactory statsFactory, EdgeContextComponent edgeCtx) {
super(actorContext, null, null, null, null, null, null, null, null, null);
super(actorContext, null, null, null, null, null, null, null, null);
this.edgeCtx = edgeCtx;
this.stats = new EdgeConsumerStats(statsFactory);
this.queueFactory = tbCoreQueueFactory;

4
application/src/main/java/org/thingsboard/server/service/queue/DefaultTbRuleEngineConsumerService.java

@ -46,7 +46,6 @@ import org.thingsboard.server.queue.discovery.QueueKey;
import org.thingsboard.server.queue.discovery.event.PartitionChangeEvent;
import org.thingsboard.server.queue.util.TbRuleEngineComponent;
import org.thingsboard.server.service.apiusage.TbApiUsageStateService;
import org.thingsboard.server.service.cf.CalculatedFieldCache;
import org.thingsboard.server.service.profile.TbAssetProfileCache;
import org.thingsboard.server.service.profile.TbDeviceProfileCache;
import org.thingsboard.server.service.queue.processing.AbstractPartitionBasedConsumerService;
@ -86,9 +85,8 @@ public class DefaultTbRuleEngineConsumerService extends AbstractPartitionBasedCo
PartitionService partitionService,
ApplicationEventPublisher eventPublisher,
JwtSettingsService jwtSettingsService,
CalculatedFieldCache calculatedFieldCache,
TbMsgPackProcessingContextFactory packProcessingContextFactory) {
super(actorContext, tenantProfileCache, deviceProfileCache, assetProfileCache, tbResourceDataCache, calculatedFieldCache, apiUsageStateService, partitionService, eventPublisher, jwtSettingsService);
super(actorContext, tenantProfileCache, deviceProfileCache, assetProfileCache, tbResourceDataCache, apiUsageStateService, partitionService, eventPublisher, jwtSettingsService);
this.ctx = ctx;
this.tbDeviceRpcService = tbDeviceRpcService;
this.queueService = queueService;

35
application/src/main/java/org/thingsboard/server/service/queue/processing/AbstractConsumerService.java

@ -26,7 +26,6 @@ import org.thingsboard.server.actors.ActorSystemContext;
import org.thingsboard.server.common.data.EntityType;
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;
@ -47,7 +46,6 @@ import org.thingsboard.server.queue.discovery.TbApplicationEventListener;
import org.thingsboard.server.queue.discovery.event.PartitionChangeEvent;
import org.thingsboard.server.queue.util.AfterStartUp;
import org.thingsboard.server.service.apiusage.TbApiUsageStateService;
import org.thingsboard.server.service.cf.CalculatedFieldCache;
import org.thingsboard.server.service.profile.TbAssetProfileCache;
import org.thingsboard.server.service.profile.TbDeviceProfileCache;
import org.thingsboard.server.service.queue.TbPackCallback;
@ -75,7 +73,6 @@ public abstract class AbstractConsumerService<N extends com.google.protobuf.Gene
protected final TbDeviceProfileCache deviceProfileCache;
protected final TbAssetProfileCache assetProfileCache;
protected final TbResourceDataCache tbResourceDataCache;
protected final CalculatedFieldCache calculatedFieldCache;
protected final TbApiUsageStateService apiUsageStateService;
protected final PartitionService partitionService;
protected final ApplicationEventPublisher eventPublisher;
@ -166,7 +163,6 @@ public abstract class AbstractConsumerService<N extends com.google.protobuf.Gene
tenantProfileCache.evict(tenantProfileId);
if (componentLifecycleMsg.getEvent().equals(ComponentLifecycleEvent.UPDATED)) {
apiUsageStateService.onTenantProfileUpdate(tenantProfileId);
calculatedFieldCache.handleTenantProfileUpdate(tenantProfileId);
}
} else if (EntityType.TENANT.equals(componentLifecycleMsg.getEntityId().getEntityType())) {
if (TenantId.SYS_TENANT_ID.equals(tenantId)) {
@ -179,7 +175,6 @@ public abstract class AbstractConsumerService<N extends com.google.protobuf.Gene
apiUsageStateService.onTenantUpdate(tenantId);
} else if (componentLifecycleMsg.getEvent().equals(ComponentLifecycleEvent.DELETED)) {
apiUsageStateService.onTenantDelete(tenantId);
calculatedFieldCache.evictOwner(tenantId);
partitionService.removeTenant(tenantId);
}
}
@ -187,45 +182,17 @@ public abstract class AbstractConsumerService<N extends com.google.protobuf.Gene
deviceProfileCache.evict(tenantId, new DeviceProfileId(componentLifecycleMsg.getEntityId().getId()));
} else if (EntityType.DEVICE.equals(componentLifecycleMsg.getEntityId().getEntityType())) {
deviceProfileCache.evict(tenantId, new DeviceId(componentLifecycleMsg.getEntityId().getId()));
if (componentLifecycleMsg.getEvent().equals(ComponentLifecycleEvent.CREATED)) {
calculatedFieldCache.addOwnerEntity(tenantId, componentLifecycleMsg.getEntityId());
} else if (componentLifecycleMsg.getEvent().equals(ComponentLifecycleEvent.UPDATED) && componentLifecycleMsg.isOwnerChanged()) {
calculatedFieldCache.updateOwnerEntity(tenantId, componentLifecycleMsg.getEntityId());
} else if (componentLifecycleMsg.getEvent().equals(ComponentLifecycleEvent.DELETED)) {
calculatedFieldCache.evictEntity(componentLifecycleMsg.getEntityId());
}
} else if (EntityType.ASSET_PROFILE.equals(componentLifecycleMsg.getEntityId().getEntityType())) {
assetProfileCache.evict(tenantId, new AssetProfileId(componentLifecycleMsg.getEntityId().getId()));
} else if (EntityType.ASSET.equals(componentLifecycleMsg.getEntityId().getEntityType())) {
assetProfileCache.evict(tenantId, new AssetId(componentLifecycleMsg.getEntityId().getId()));
if (componentLifecycleMsg.getEvent().equals(ComponentLifecycleEvent.CREATED)) {
calculatedFieldCache.addOwnerEntity(tenantId, componentLifecycleMsg.getEntityId());
} else if (componentLifecycleMsg.getEvent().equals(ComponentLifecycleEvent.UPDATED) && componentLifecycleMsg.isOwnerChanged()) {
calculatedFieldCache.updateOwnerEntity(tenantId, componentLifecycleMsg.getEntityId());
} else if (componentLifecycleMsg.getEvent().equals(ComponentLifecycleEvent.DELETED)) {
calculatedFieldCache.evictEntity(componentLifecycleMsg.getEntityId());
}
} else if (EntityType.ENTITY_VIEW.equals(componentLifecycleMsg.getEntityId().getEntityType())) {
actorContext.getTbEntityViewService().onComponentLifecycleMsg(componentLifecycleMsg);
} else if (EntityType.API_USAGE_STATE.equals(componentLifecycleMsg.getEntityId().getEntityType())) {
apiUsageStateService.onApiUsageStateUpdate(tenantId);
} else if (EntityType.CUSTOMER.equals(componentLifecycleMsg.getEntityId().getEntityType())) {
if (componentLifecycleMsg.getEvent().equals(ComponentLifecycleEvent.CREATED)) {
calculatedFieldCache.addOwnerEntity(tenantId, componentLifecycleMsg.getEntityId());
} else if (componentLifecycleMsg.getEvent().equals(ComponentLifecycleEvent.UPDATED) && componentLifecycleMsg.isOwnerChanged()) {
calculatedFieldCache.updateOwnerEntity(tenantId, componentLifecycleMsg.getEntityId());
} else if (componentLifecycleMsg.getEvent() == ComponentLifecycleEvent.DELETED) {
if (componentLifecycleMsg.getEvent() == ComponentLifecycleEvent.DELETED) {
apiUsageStateService.onCustomerDelete((CustomerId) componentLifecycleMsg.getEntityId());
calculatedFieldCache.evictOwner(componentLifecycleMsg.getEntityId());
calculatedFieldCache.evictEntity(componentLifecycleMsg.getEntityId());
}
} else if (EntityType.CALCULATED_FIELD.equals(componentLifecycleMsg.getEntityId().getEntityType())) {
if (componentLifecycleMsg.getEvent() == ComponentLifecycleEvent.CREATED) {
calculatedFieldCache.addCalculatedField(tenantId, (CalculatedFieldId) componentLifecycleMsg.getEntityId());
} else if (componentLifecycleMsg.getEvent() == ComponentLifecycleEvent.UPDATED) {
calculatedFieldCache.updateCalculatedField(tenantId, (CalculatedFieldId) componentLifecycleMsg.getEntityId());
} else {
calculatedFieldCache.evict((CalculatedFieldId) componentLifecycleMsg.getEntityId());
}
} else if (EntityType.TB_RESOURCE.equals(componentLifecycleMsg.getEntityId().getEntityType())) {
tbResourceDataCache.evictResourceData(tenantId, new TbResourceId(componentLifecycleMsg.getEntityId().getId()));

4
application/src/main/java/org/thingsboard/server/service/queue/processing/AbstractPartitionBasedConsumerService.java

@ -24,7 +24,6 @@ import org.thingsboard.server.queue.discovery.PartitionService;
import org.thingsboard.server.queue.discovery.event.PartitionChangeEvent;
import org.thingsboard.server.queue.util.AfterStartUp;
import org.thingsboard.server.service.apiusage.TbApiUsageStateService;
import org.thingsboard.server.service.cf.CalculatedFieldCache;
import org.thingsboard.server.service.profile.TbAssetProfileCache;
import org.thingsboard.server.service.profile.TbDeviceProfileCache;
import org.thingsboard.server.service.security.auth.jwt.settings.JwtSettingsService;
@ -45,12 +44,11 @@ public abstract class AbstractPartitionBasedConsumerService<N extends com.google
TbDeviceProfileCache deviceProfileCache,
TbAssetProfileCache assetProfileCache,
TbResourceDataCache tbResourceDataCache,
CalculatedFieldCache calculatedFieldCache,
TbApiUsageStateService apiUsageStateService,
PartitionService partitionService,
ApplicationEventPublisher eventPublisher,
JwtSettingsService jwtSettingsService) {
super(actorContext, tenantProfileCache, deviceProfileCache, assetProfileCache, tbResourceDataCache, calculatedFieldCache, apiUsageStateService, partitionService, eventPublisher, jwtSettingsService);
super(actorContext, tenantProfileCache, deviceProfileCache, assetProfileCache, tbResourceDataCache, apiUsageStateService, partitionService, eventPublisher, jwtSettingsService);
}
@PostConstruct

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

@ -0,0 +1,517 @@
/**
* Copyright © 2016-2026 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.cf;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.Mock;
import org.mockito.junit.jupiter.MockitoExtension;
import org.thingsboard.server.common.data.Device;
import org.thingsboard.server.common.data.cf.CalculatedField;
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;
import org.thingsboard.server.dao.asset.AssetService;
import org.thingsboard.server.dao.cf.CalculatedFieldService;
import org.thingsboard.server.dao.customer.CustomerService;
import org.thingsboard.server.dao.device.DeviceService;
import org.thingsboard.server.dao.tenant.TbTenantProfileCache;
import org.thingsboard.server.service.profile.TbAssetProfileCache;
import org.thingsboard.server.service.profile.TbDeviceProfileCache;
import java.util.Collections;
import java.util.List;
import java.util.Set;
import java.util.UUID;
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)
public class DefaultCalculatedFieldCacheTest {
@Mock
private CalculatedFieldService calculatedFieldService;
@Mock
private TbAssetProfileCache assetProfileCache;
@Mock
private TbDeviceProfileCache deviceProfileCache;
@Mock
private TbTenantProfileCache tenantProfileCache;
@Mock
private DeviceService deviceService;
@Mock
private AssetService assetService;
@Mock
private CustomerService customerService;
private DefaultCalculatedFieldCache cache;
@BeforeEach
public void setUp() {
// ActorSystemContext is only used in getCalculatedFieldCtx (not tested here), so null is safe
OwnerService ownerService = new OwnerService(deviceService, assetService, customerService);
cache = new DefaultCalculatedFieldCache(calculatedFieldService, assetProfileCache,
deviceProfileCache, tenantProfileCache, null, ownerService);
}
// --- Tenant deletion tests ---
@Test
public void onComponentLifecycleEvent_tenantDeleted_evictsAllTenantCfsFromAllMaps() {
TenantId tenant1 = new TenantId(UUID.randomUUID());
TenantId tenant2 = new TenantId(UUID.randomUUID());
DeviceId device1 = new DeviceId(UUID.randomUUID());
DeviceId device2 = new DeviceId(UUID.randomUUID());
CalculatedField cf1 = addCfToCache(tenant1, device1);
CalculatedField cf2 = addCfToCache(tenant2, device2);
cache.onComponentLifecycleEvent(new ComponentLifecycleMsg(tenant1, tenant1, ComponentLifecycleEvent.DELETED));
assertThat(cache.getCalculatedField(cf1.getId())).isNull();
assertThat(cache.getCalculatedFieldsByEntityId(device1)).isEmpty();
assertThat(cache.getCalculatedField(cf2.getId())).isEqualTo(cf2);
assertThat(cache.getCalculatedFieldsByEntityId(device2)).containsExactly(cf2);
}
@Test
public void onComponentLifecycleEvent_tenantDeleted_evictsOwnerEntities() {
TenantId tenant = new TenantId(UUID.randomUUID());
DeviceId device = new DeviceId(UUID.randomUUID());
stubDeviceOwner(tenant, device, tenant);
cache.addOwnerEntity(tenant, device);
assertThat(cache.getDynamicEntities(tenant, tenant)).contains(device);
cache.onComponentLifecycleEvent(new ComponentLifecycleMsg(tenant, tenant, ComponentLifecycleEvent.DELETED));
// After eviction, getDynamicEntities triggers a fresh load from ownerService (empty)
assertThat(cache.getDynamicEntities(tenant, tenant)).doesNotContain(device);
}
@Test
public void onComponentLifecycleEvent_tenantDeleted_removesLinksToLinkedEntities() {
TenantId tenant = new TenantId(UUID.randomUUID());
DeviceId cfEntity = new DeviceId(UUID.randomUUID());
DeviceId linkedDevice = new DeviceId(UUID.randomUUID());
CalculatedField cf = addCfToCache(tenant, cfEntity, linkedDevice);
cache.onComponentLifecycleEvent(new ComponentLifecycleMsg(tenant, tenant, ComponentLifecycleEvent.DELETED));
assertThat(cache.getCalculatedFieldLinksByEntityId(linkedDevice)).isEmpty();
assertThat(cache.getCalculatedField(cf.getId())).isNull();
}
@Test
public void onComponentLifecycleEvent_tenantUpdated_doesNotEvictCfs() {
TenantId tenant = new TenantId(UUID.randomUUID());
DeviceId device = new DeviceId(UUID.randomUUID());
CalculatedField cf = addCfToCache(tenant, device);
cache.onComponentLifecycleEvent(new ComponentLifecycleMsg(tenant, tenant, ComponentLifecycleEvent.UPDATED));
assertThat(cache.getCalculatedField(cf.getId())).isEqualTo(cf);
}
// --- Device/Asset deletion tests ---
@Test
public void onComponentLifecycleEvent_deviceDeleted_evictsCfsForThatDevice() {
TenantId tenant = new TenantId(UUID.randomUUID());
DeviceId device = new DeviceId(UUID.randomUUID());
CalculatedField cf = addCfToCache(tenant, device);
cache.onComponentLifecycleEvent(new ComponentLifecycleMsg(tenant, device, ComponentLifecycleEvent.DELETED));
assertThat(cache.getCalculatedField(cf.getId())).isNull();
assertThat(cache.getCalculatedFieldsByEntityId(device)).isEmpty();
}
@Test
public void onComponentLifecycleEvent_deviceDeleted_removesLinksForLinkedEntities() {
TenantId tenant = new TenantId(UUID.randomUUID());
DeviceId device = new DeviceId(UUID.randomUUID());
DeviceId linkedDevice = new DeviceId(UUID.randomUUID());
addCfToCache(tenant, device, linkedDevice);
cache.onComponentLifecycleEvent(new ComponentLifecycleMsg(tenant, device, ComponentLifecycleEvent.DELETED));
assertThat(cache.getCalculatedFieldLinksByEntityId(linkedDevice)).isEmpty();
}
@Test
public void onComponentLifecycleEvent_deviceDeleted_evictsDeviceFromOwnerEntities() {
TenantId tenant = new TenantId(UUID.randomUUID());
CustomerId customer = new CustomerId(UUID.randomUUID());
DeviceId device = new DeviceId(UUID.randomUUID());
stubDeviceOwner(tenant, device, customer);
cache.addOwnerEntity(tenant, device);
assertThat(cache.getDynamicEntities(tenant, customer)).contains(device);
cache.onComponentLifecycleEvent(new ComponentLifecycleMsg(tenant, device, ComponentLifecycleEvent.DELETED));
assertThat(cache.getDynamicEntities(tenant, customer)).doesNotContain(device);
}
@Test
public void onComponentLifecycleEvent_assetDeleted_evictsCfsForThatAsset() {
TenantId tenant = new TenantId(UUID.randomUUID());
AssetId asset = new AssetId(UUID.randomUUID());
CalculatedField cf = addCfToCache(tenant, asset);
cache.onComponentLifecycleEvent(new ComponentLifecycleMsg(tenant, asset, ComponentLifecycleEvent.DELETED));
assertThat(cache.getCalculatedField(cf.getId())).isNull();
assertThat(cache.getCalculatedFieldsByEntityId(asset)).isEmpty();
}
@Test
public void onComponentLifecycleEvent_deviceCreated_addsDeviceToOwnerEntities() {
TenantId tenant = new TenantId(UUID.randomUUID());
CustomerId customer = new CustomerId(UUID.randomUUID());
DeviceId device = new DeviceId(UUID.randomUUID());
stubDeviceOwner(tenant, device, customer);
cache.onComponentLifecycleEvent(new ComponentLifecycleMsg(tenant, device, ComponentLifecycleEvent.CREATED));
assertThat(cache.getDynamicEntities(tenant, customer)).contains(device);
}
// --- Customer deletion tests ---
@Test
public void onComponentLifecycleEvent_customerDeleted_evictsCustomerOwnerEntries() {
TenantId tenant = new TenantId(UUID.randomUUID());
CustomerId customer = new CustomerId(UUID.randomUUID());
DeviceId device = new DeviceId(UUID.randomUUID());
stubDeviceOwner(tenant, device, customer);
cache.addOwnerEntity(tenant, device);
assertThat(cache.getDynamicEntities(tenant, customer)).contains(device);
cache.onComponentLifecycleEvent(new ComponentLifecycleMsg(tenant, customer, ComponentLifecycleEvent.DELETED));
// The customer's owned-entities entry is evicted; fresh load returns empty
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
public void onComponentLifecycleEvent_calculatedFieldCreated_addsCfToCache() {
TenantId tenant = new TenantId(UUID.randomUUID());
DeviceId device = new DeviceId(UUID.randomUUID());
CalculatedFieldId cfId = new CalculatedFieldId(UUID.randomUUID());
CalculatedField cf = buildCalculatedField(cfId, tenant, device, simpleCfConfig());
when(calculatedFieldService.findById(tenant, cfId)).thenReturn(cf);
cache.onComponentLifecycleEvent(new ComponentLifecycleMsg(tenant, cfId, ComponentLifecycleEvent.CREATED));
assertThat(cache.getCalculatedField(cfId)).isEqualTo(cf);
assertThat(cache.getCalculatedFieldsByEntityId(device)).containsExactly(cf);
}
@Test
public void onComponentLifecycleEvent_calculatedFieldDeleted_evictsCfFromCache() {
TenantId tenant = new TenantId(UUID.randomUUID());
DeviceId device = new DeviceId(UUID.randomUUID());
CalculatedField cf = addCfToCache(tenant, device);
cache.onComponentLifecycleEvent(new ComponentLifecycleMsg(tenant, cf.getId(), ComponentLifecycleEvent.DELETED));
assertThat(cache.getCalculatedField(cf.getId())).isNull();
assertThat(cache.getCalculatedFieldsByEntityId(device)).isEmpty();
}
@Test
public void onComponentLifecycleEvent_calculatedFieldUpdated_refreshesCfInCache() {
TenantId tenant = new TenantId(UUID.randomUUID());
DeviceId device = new DeviceId(UUID.randomUUID());
CalculatedField cf = addCfToCache(tenant, device);
CalculatedField updatedCf = buildCalculatedField(cf.getId(), tenant, device, simpleCfConfig());
updatedCf.setName("updated-name");
when(calculatedFieldService.findById(tenant, cf.getId())).thenReturn(updatedCf);
cache.onComponentLifecycleEvent(new ComponentLifecycleMsg(tenant, cf.getId(), ComponentLifecycleEvent.UPDATED));
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) {
Device device = new Device();
device.setId(deviceId);
device.setTenantId(tenantId);
if (ownerId instanceof CustomerId customerId) {
device.setCustomerId(customerId);
}
// If ownerId is a TenantId, leaving customerId null means getOwnerId() returns tenantId
when(deviceService.findDeviceById(tenantId, deviceId)).thenReturn(device);
// Stubs for getOwnedEntities iteration (empty pages — device is added explicitly)
when(deviceService.findDeviceInfosByFilter(any(), any())).thenReturn(PageData.emptyPageData());
when(assetService.findAssetsByTenantIdAndCustomerId(any(), any(), any())).thenReturn(PageData.emptyPageData());
if (ownerId instanceof TenantId) {
when(customerService.findCustomersByTenantId(any(), any())).thenReturn(PageData.emptyPageData());
}
}
private CalculatedField addCfToCache(TenantId tenantId, EntityId entityId) {
CalculatedFieldId cfId = new CalculatedFieldId(UUID.randomUUID());
CalculatedField cf = buildCalculatedField(cfId, tenantId, entityId, simpleCfConfig());
when(calculatedFieldService.findById(tenantId, cfId)).thenReturn(cf);
cache.addCalculatedField(tenantId, cfId);
return cf;
}
private CalculatedField addCfToCache(TenantId tenantId, EntityId entityId, EntityId linkedEntity) {
CalculatedFieldId cfId = new CalculatedFieldId(UUID.randomUUID());
CalculatedFieldConfiguration config = linkedEntityCfConfig(tenantId, cfId, linkedEntity);
CalculatedField cf = buildCalculatedField(cfId, tenantId, entityId, config);
when(calculatedFieldService.findById(tenantId, cfId)).thenReturn(cf);
cache.addCalculatedField(tenantId, cfId);
return cf;
}
private CalculatedField buildCalculatedField(CalculatedFieldId id, TenantId tenantId, EntityId entityId, CalculatedFieldConfiguration config) {
CalculatedField cf = new CalculatedField();
cf.setId(id);
cf.setTenantId(tenantId);
cf.setEntityId(entityId);
cf.setType(CalculatedFieldType.SIMPLE);
cf.setName("test-cf-" + id.getId());
cf.setConfiguration(config);
return cf;
}
private CalculatedFieldConfiguration simpleCfConfig() {
CalculatedFieldConfiguration config = mock(CalculatedFieldConfiguration.class);
when(config.getReferencedEntities()).thenReturn(Collections.emptySet());
when(config.buildCalculatedFieldLinks(any(), any(), any())).thenReturn(Collections.emptyList());
return config;
}
private CalculatedFieldConfiguration linkedEntityCfConfig(TenantId tenantId, CalculatedFieldId cfId, EntityId linkedEntity) {
CalculatedFieldConfiguration config = mock(CalculatedFieldConfiguration.class);
CalculatedFieldLink link = new CalculatedFieldLink(tenantId, linkedEntity, cfId);
when(config.getReferencedEntities()).thenReturn(Set.of(linkedEntity));
when(config.buildCalculatedFieldLinks(any(), any(), any())).thenReturn(List.of(link));
when(config.buildCalculatedFieldLink(any(), eq(linkedEntity), any())).thenReturn(link);
return config;
}
}

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

@ -0,0 +1,159 @@
/**
* Copyright © 2016-2026 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.profile;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.Mock;
import org.mockito.junit.jupiter.MockitoExtension;
import org.thingsboard.server.common.data.asset.Asset;
import org.thingsboard.server.common.data.asset.AssetProfile;
import org.thingsboard.server.common.data.id.AssetId;
import org.thingsboard.server.common.data.id.AssetProfileId;
import org.thingsboard.server.common.data.id.EntityId;
import org.thingsboard.server.common.data.id.TenantId;
import org.thingsboard.server.common.data.plugin.ComponentLifecycleEvent;
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.UUID;
import java.util.concurrent.atomic.AtomicInteger;
import static org.assertj.core.api.Assertions.assertThat;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.Mockito.times;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
@ExtendWith(MockitoExtension.class)
public class DefaultTbAssetProfileCacheTest {
@Mock
private AssetProfileService assetProfileService;
@Mock
private AssetService assetService;
private DefaultTbAssetProfileCache cache;
@BeforeEach
public void setUp() {
cache = new DefaultTbAssetProfileCache(assetProfileService, assetService);
}
@Test
public void onComponentLifecycleEvent_tenantDeleted_evictsAssetProfilesForThatTenant() {
TenantId tenant1 = new TenantId(UUID.randomUUID());
TenantId tenant2 = new TenantId(UUID.randomUUID());
AssetProfileId profileId1 = new AssetProfileId(UUID.randomUUID());
AssetProfileId profileId2 = new AssetProfileId(UUID.randomUUID());
loadProfileIntoCache(tenant1, profileId1);
loadProfileIntoCache(tenant2, profileId2);
cache.onComponentLifecycleEvent(new ComponentLifecycleMsg(tenant1, tenant1, ComponentLifecycleEvent.DELETED));
// 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();
verify(assetProfileService, times(1)).findAssetProfileById(tenant2, profileId2);
}
@Test
public void onComponentLifecycleEvent_tenantDeleted_evictsAssetMappingsForThatTenant() {
TenantId tenant = new TenantId(UUID.randomUUID());
AssetProfileId profileId = new AssetProfileId(UUID.randomUUID());
AssetId assetId = new AssetId(UUID.randomUUID());
loadProfileIntoCache(tenant, profileId);
loadAssetMappingIntoCache(tenant, assetId, profileId);
cache.onComponentLifecycleEvent(new ComponentLifecycleMsg(tenant, tenant, ComponentLifecycleEvent.DELETED));
// After tenant deletion, asset-to-profile mapping should be gone; get() should try to reload
when(assetService.findAssetById(any(), any())).thenReturn(null);
assertThat(cache.get(tenant, assetId)).isNull();
verify(assetService, times(2)).findAssetById(tenant, assetId); // once on load, once after eviction
}
@Test
public void onComponentLifecycleEvent_tenantDeleted_removesListenersForThatTenant() {
TenantId tenant = new TenantId(UUID.randomUUID());
EntityId listenerId = new AssetId(UUID.randomUUID());
AtomicInteger callCount = new AtomicInteger();
cache.addListener(tenant, listenerId, profile -> callCount.incrementAndGet(), null);
cache.onComponentLifecycleEvent(new ComponentLifecycleMsg(tenant, tenant, ComponentLifecycleEvent.DELETED));
// Evicting a profile after tenant deletion should not trigger the removed listener
AssetProfileId profileId = new AssetProfileId(UUID.randomUUID());
loadProfileIntoCache(tenant, profileId);
cache.evict(tenant, profileId);
assertThat(callCount.get()).isZero();
}
@Test
public void onComponentLifecycleEvent_tenantUpdated_doesNotEvictProfiles() {
TenantId tenant = new TenantId(UUID.randomUUID());
AssetProfileId profileId = new AssetProfileId(UUID.randomUUID());
loadProfileIntoCache(tenant, profileId);
cache.onComponentLifecycleEvent(new ComponentLifecycleMsg(tenant, tenant, ComponentLifecycleEvent.UPDATED));
// Profile should still be served from cache without hitting the service again
cache.get(tenant, profileId);
verify(assetProfileService, times(1)).findAssetProfileById(tenant, profileId);
}
@Test
public void onComponentLifecycleEvent_differentTenantDeleted_keepsOtherTenantsProfiles() {
TenantId tenant1 = new TenantId(UUID.randomUUID());
TenantId tenant2 = new TenantId(UUID.randomUUID());
AssetProfileId profileId1 = new AssetProfileId(UUID.randomUUID());
AssetProfileId profileId2 = new AssetProfileId(UUID.randomUUID());
AssetProfile profile1 = loadProfileIntoCache(tenant1, profileId1);
loadProfileIntoCache(tenant2, profileId2);
cache.onComponentLifecycleEvent(new ComponentLifecycleMsg(tenant2, tenant2, ComponentLifecycleEvent.DELETED));
assertThat(cache.get(tenant1, profileId1)).isEqualTo(profile1);
verify(assetProfileService, times(1)).findAssetProfileById(tenant1, profileId1);
}
// --- Helpers ---
private AssetProfile loadProfileIntoCache(TenantId tenantId, AssetProfileId profileId) {
AssetProfile profile = new AssetProfile();
profile.setId(profileId);
profile.setTenantId(tenantId);
when(assetProfileService.findAssetProfileById(tenantId, profileId)).thenReturn(profile);
cache.get(tenantId, profileId);
return profile;
}
private void loadAssetMappingIntoCache(TenantId tenantId, AssetId assetId, AssetProfileId profileId) {
Asset asset = new Asset();
asset.setId(assetId);
asset.setAssetProfileId(profileId);
when(assetService.findAssetById(tenantId, assetId)).thenReturn(asset);
cache.get(tenantId, assetId);
}
}

160
application/src/test/java/org/thingsboard/server/service/profile/DefaultTbDeviceProfileCacheTest.java

@ -0,0 +1,160 @@
/**
* Copyright © 2016-2026 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.profile;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.Mock;
import org.mockito.junit.jupiter.MockitoExtension;
import org.thingsboard.server.common.data.Device;
import org.thingsboard.server.common.data.DeviceProfile;
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.plugin.ComponentLifecycleEvent;
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.UUID;
import java.util.concurrent.atomic.AtomicInteger;
import static org.assertj.core.api.Assertions.assertThat;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.Mockito.times;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
@ExtendWith(MockitoExtension.class)
public class DefaultTbDeviceProfileCacheTest {
@Mock
private DeviceProfileService deviceProfileService;
@Mock
private DeviceService deviceService;
private DefaultTbDeviceProfileCache cache;
@BeforeEach
public void setUp() {
cache = new DefaultTbDeviceProfileCache(deviceProfileService, deviceService);
}
@Test
public void onComponentLifecycleEvent_tenantDeleted_evictsDeviceProfilesForThatTenant() {
TenantId tenant1 = new TenantId(UUID.randomUUID());
TenantId tenant2 = new TenantId(UUID.randomUUID());
DeviceProfileId profileId1 = new DeviceProfileId(UUID.randomUUID());
DeviceProfileId profileId2 = new DeviceProfileId(UUID.randomUUID());
loadProfileIntoCache(tenant1, profileId1);
loadProfileIntoCache(tenant2, profileId2);
cache.onComponentLifecycleEvent(new ComponentLifecycleMsg(tenant1, tenant1, ComponentLifecycleEvent.DELETED));
// After deletion tenant1 profile should be reloaded from service on next get
when(deviceProfileService.findDeviceProfileById(any(), any())).thenReturn(null);
assertThat(cache.get(tenant1, profileId1)).isNull();
// tenant2 profile should still be served from cache (no extra service call)
verify(deviceProfileService, times(1)).findDeviceProfileById(tenant2, profileId2);
}
@Test
public void onComponentLifecycleEvent_tenantDeleted_evictsDeviceMappingsForThatTenant() {
TenantId tenant = new TenantId(UUID.randomUUID());
DeviceProfileId profileId = new DeviceProfileId(UUID.randomUUID());
DeviceId deviceId = new DeviceId(UUID.randomUUID());
loadProfileIntoCache(tenant, profileId);
loadDeviceMappingIntoCache(tenant, deviceId, profileId);
cache.onComponentLifecycleEvent(new ComponentLifecycleMsg(tenant, tenant, ComponentLifecycleEvent.DELETED));
// After tenant deletion, device-to-profile mapping should be gone; get() should try to reload
when(deviceService.findDeviceById(any(), any())).thenReturn(null);
assertThat(cache.get(tenant, deviceId)).isNull();
verify(deviceService, times(2)).findDeviceById(tenant, deviceId); // once on load, once after eviction
}
@Test
public void onComponentLifecycleEvent_tenantDeleted_removesListenersForThatTenant() {
TenantId tenant = new TenantId(UUID.randomUUID());
EntityId listenerId = new DeviceId(UUID.randomUUID());
AtomicInteger callCount = new AtomicInteger();
cache.addListener(tenant, listenerId, profile -> callCount.incrementAndGet(), null);
cache.onComponentLifecycleEvent(new ComponentLifecycleMsg(tenant, tenant, ComponentLifecycleEvent.DELETED));
// Evicting a profile after tenant deletion should not trigger the removed listener
DeviceProfileId profileId = new DeviceProfileId(UUID.randomUUID());
loadProfileIntoCache(tenant, profileId);
cache.evict(tenant, profileId);
assertThat(callCount.get()).isZero();
}
@Test
public void onComponentLifecycleEvent_tenantUpdated_doesNotEvictProfiles() {
TenantId tenant = new TenantId(UUID.randomUUID());
DeviceProfileId profileId = new DeviceProfileId(UUID.randomUUID());
loadProfileIntoCache(tenant, profileId);
cache.onComponentLifecycleEvent(new ComponentLifecycleMsg(tenant, tenant, ComponentLifecycleEvent.UPDATED));
// Profile should still be served from cache without hitting the service again
cache.get(tenant, profileId);
verify(deviceProfileService, times(1)).findDeviceProfileById(tenant, profileId);
}
@Test
public void onComponentLifecycleEvent_differentTenantDeleted_keepsOtherTenantsProfiles() {
TenantId tenant1 = new TenantId(UUID.randomUUID());
TenantId tenant2 = new TenantId(UUID.randomUUID());
DeviceProfileId profileId1 = new DeviceProfileId(UUID.randomUUID());
DeviceProfileId profileId2 = new DeviceProfileId(UUID.randomUUID());
DeviceProfile profile1 = loadProfileIntoCache(tenant1, profileId1);
loadProfileIntoCache(tenant2, profileId2);
cache.onComponentLifecycleEvent(new ComponentLifecycleMsg(tenant2, tenant2, ComponentLifecycleEvent.DELETED));
assertThat(cache.get(tenant1, profileId1)).isEqualTo(profile1);
verify(deviceProfileService, times(1)).findDeviceProfileById(tenant1, profileId1);
}
// --- Helpers ---
private DeviceProfile loadProfileIntoCache(TenantId tenantId, DeviceProfileId profileId) {
DeviceProfile profile = new DeviceProfile();
profile.setId(profileId);
profile.setTenantId(tenantId);
when(deviceProfileService.findDeviceProfileById(tenantId, profileId)).thenReturn(profile);
cache.get(tenantId, profileId);
return profile;
}
private void loadDeviceMappingIntoCache(TenantId tenantId, DeviceId deviceId, DeviceProfileId profileId) {
Device device = new Device();
device.setId(deviceId);
device.setDeviceProfileId(profileId);
when(deviceService.findDeviceById(tenantId, deviceId)).thenReturn(device);
cache.get(tenantId, deviceId);
}
}
Loading…
Cancel
Save