diff --git a/application/src/main/resources/thingsboard.yml b/application/src/main/resources/thingsboard.yml index 5104f2e264..54c1159003 100644 --- a/application/src/main/resources/thingsboard.yml +++ b/application/src/main/resources/thingsboard.yml @@ -647,6 +647,9 @@ cache: trendzSettings: timeToLiveInMinutes: "${CACHE_SPECS_TRENDZ_SETTINGS_TTL:1440}" # Trendz settings cache TTL maxSize: "${CACHE_SPECS_TRENDZ_SETTINGS_MAX_SIZE:10000}" # 0 means the cache is disabled + aiSettings: + timeToLiveInMinutes: "${CACHE_SPECS_AI_SETTINGS_TTL:1440}" # AI settings cache TTL + maxSize: "${CACHE_SPECS_AI_SETTINGS_MAX_SIZE:10000}" # 0 means the cache is disabled # Deliberately placed outside the 'specs' group above notificationRules: diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/CacheConstants.java b/common/data/src/main/java/org/thingsboard/server/common/data/CacheConstants.java index 5b167c88a2..4b103d769a 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/CacheConstants.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/CacheConstants.java @@ -15,7 +15,8 @@ */ package org.thingsboard.server.common.data; -public class CacheConstants { +public final class CacheConstants { + public static final String DEVICE_CREDENTIALS_CACHE = "deviceCredentials"; public static final String RELATIONS_CACHE = "relations"; public static final String DEVICE_CACHE = "devices"; @@ -36,6 +37,7 @@ public class CacheConstants { public static final String NOTIFICATION_SETTINGS_CACHE = "notificationSettings"; public static final String SENT_NOTIFICATIONS_CACHE = "sentNotifications"; public static final String TRENDZ_SETTINGS_CACHE = "trendzSettings"; + public static final String AI_SETTINGS_CACHE = "aiSettings"; public static final String ASSET_PROFILE_CACHE = "assetProfiles"; public static final String ATTRIBUTES_CACHE = "attributes"; @@ -54,4 +56,5 @@ public class CacheConstants { public static final String ALARM_TYPES_CACHE = "alarmTypes"; public static final String QR_CODE_SETTINGS_CACHE = "qrCodeSettings"; public static final String MOBILE_SECRET_KEY_CACHE = "mobileSecretKey"; + } diff --git a/dao/src/main/java/org/thingsboard/server/dao/ai/AiSettingsCacheEvictEvent.java b/dao/src/main/java/org/thingsboard/server/dao/ai/AiSettingsCacheEvictEvent.java new file mode 100644 index 0000000000..d6cad8bf16 --- /dev/null +++ b/dao/src/main/java/org/thingsboard/server/dao/ai/AiSettingsCacheEvictEvent.java @@ -0,0 +1,29 @@ +/** + * Copyright © 2016-2025 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.ai; + +import org.thingsboard.server.common.data.id.AiSettingsId; +import org.thingsboard.server.common.data.id.TenantId; + +import java.util.Set; + +record AiSettingsCacheEvictEvent(Set keys) { + + static AiSettingsCacheEvictEvent of(TenantId tenantId, AiSettingsId aiSettingsId) { + return new AiSettingsCacheEvictEvent(Set.of(AiSettingsCacheKey.of(tenantId, aiSettingsId))); + } + +} diff --git a/dao/src/main/java/org/thingsboard/server/dao/ai/AiSettingsCacheKey.java b/dao/src/main/java/org/thingsboard/server/dao/ai/AiSettingsCacheKey.java new file mode 100644 index 0000000000..fcf737d3a5 --- /dev/null +++ b/dao/src/main/java/org/thingsboard/server/dao/ai/AiSettingsCacheKey.java @@ -0,0 +1,47 @@ +/** + * Copyright © 2016-2025 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.ai; + +import org.thingsboard.server.cache.VersionedCacheKey; +import org.thingsboard.server.common.data.id.AiSettingsId; +import org.thingsboard.server.common.data.id.TenantId; + +import java.util.UUID; + +import static java.util.Objects.requireNonNull; + +record AiSettingsCacheKey(UUID tenantId, UUID aiSettingsId) implements VersionedCacheKey { + + AiSettingsCacheKey { + requireNonNull(tenantId); + requireNonNull(aiSettingsId); + } + + static AiSettingsCacheKey of(TenantId tenantId, AiSettingsId aiSettingsId) { + return new AiSettingsCacheKey(tenantId.getId(), aiSettingsId.getId()); + } + + @Override + public boolean isVersioned() { + return true; + } + + @Override + public String toString() { + return tenantId + "_" + aiSettingsId; + } + +} diff --git a/dao/src/main/java/org/thingsboard/server/dao/ai/AiSettingsCaffeineCache.java b/dao/src/main/java/org/thingsboard/server/dao/ai/AiSettingsCaffeineCache.java new file mode 100644 index 0000000000..f006c0a757 --- /dev/null +++ b/dao/src/main/java/org/thingsboard/server/dao/ai/AiSettingsCaffeineCache.java @@ -0,0 +1,33 @@ +/** + * Copyright © 2016-2025 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.ai; + +import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; +import org.springframework.cache.CacheManager; +import org.springframework.stereotype.Component; +import org.thingsboard.server.cache.VersionedCaffeineTbCache; +import org.thingsboard.server.common.data.CacheConstants; +import org.thingsboard.server.common.data.ai.AiSettings; + +@Component("AiSettingsCache") +@ConditionalOnProperty(prefix = "cache", value = "type", havingValue = "caffeine", matchIfMissing = true) +class AiSettingsCaffeineCache extends VersionedCaffeineTbCache { + + AiSettingsCaffeineCache(CacheManager cacheManager) { + super(cacheManager, CacheConstants.AI_SETTINGS_CACHE); + } + +} diff --git a/dao/src/main/java/org/thingsboard/server/dao/ai/AiSettingsRedisCache.java b/dao/src/main/java/org/thingsboard/server/dao/ai/AiSettingsRedisCache.java new file mode 100644 index 0000000000..0c1da26769 --- /dev/null +++ b/dao/src/main/java/org/thingsboard/server/dao/ai/AiSettingsRedisCache.java @@ -0,0 +1,36 @@ +/** + * Copyright © 2016-2025 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.ai; + +import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; +import org.springframework.data.redis.connection.RedisConnectionFactory; +import org.springframework.stereotype.Component; +import org.thingsboard.server.cache.CacheSpecsMap; +import org.thingsboard.server.cache.TBRedisCacheConfiguration; +import org.thingsboard.server.cache.TbJsonRedisSerializer; +import org.thingsboard.server.cache.VersionedRedisTbCache; +import org.thingsboard.server.common.data.CacheConstants; +import org.thingsboard.server.common.data.ai.AiSettings; + +@Component("AiSettingsCache") +@ConditionalOnProperty(prefix = "cache", value = "type", havingValue = "redis") +class AiSettingsRedisCache extends VersionedRedisTbCache { + + AiSettingsRedisCache(TBRedisCacheConfiguration configuration, CacheSpecsMap cacheSpecsMap, RedisConnectionFactory connectionFactory) { + super(CacheConstants.AI_SETTINGS_CACHE, cacheSpecsMap, connectionFactory, configuration, new TbJsonRedisSerializer<>(AiSettings.class)); + } + +} diff --git a/dao/src/main/java/org/thingsboard/server/dao/ai/AiSettingsServiceImpl.java b/dao/src/main/java/org/thingsboard/server/dao/ai/AiSettingsServiceImpl.java index 1bfa316c73..517d23c496 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/ai/AiSettingsServiceImpl.java +++ b/dao/src/main/java/org/thingsboard/server/dao/ai/AiSettingsServiceImpl.java @@ -15,10 +15,11 @@ */ package org.thingsboard.server.dao.ai; +import com.google.common.collect.Sets; import lombok.RequiredArgsConstructor; -import org.springframework.context.ApplicationEventPublisher; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; +import org.springframework.transaction.event.TransactionalEventListener; import org.thingsboard.server.common.data.EntityType; import org.thingsboard.server.common.data.ai.AiSettings; import org.thingsboard.server.common.data.id.AiSettingsId; @@ -27,26 +28,33 @@ import org.thingsboard.server.common.data.id.HasId; import org.thingsboard.server.common.data.id.TenantId; import org.thingsboard.server.common.data.page.PageData; import org.thingsboard.server.common.data.page.PageLink; +import org.thingsboard.server.dao.entity.CachedVersionedEntityService; import org.thingsboard.server.dao.eventsourcing.DeleteEntityEvent; import org.thingsboard.server.dao.eventsourcing.SaveEntityEvent; import org.thingsboard.server.dao.service.DataValidator; import java.util.List; import java.util.Optional; +import java.util.Set; -import static org.thingsboard.server.dao.entity.AbstractEntityService.checkConstraintViolation; import static org.thingsboard.server.dao.service.Validator.validatePageLink; @Service @RequiredArgsConstructor -class AiSettingsServiceImpl implements AiSettingsService { +class AiSettingsServiceImpl extends CachedVersionedEntityService implements AiSettingsService { - private final ApplicationEventPublisher eventPublisher; private final DataValidator aiSettingsValidator; private final AiSettingsDao aiSettingsDao; @Override + @TransactionalEventListener + public void handleEvictEvent(AiSettingsCacheEvictEvent event) { + cache.evict(event.keys()); + } + + @Override + @Transactional public AiSettings save(AiSettings aiSettings) { AiSettings oldSettings = aiSettingsValidator.validate(aiSettings, AiSettings::getTenantId); @@ -58,15 +66,22 @@ class AiSettingsServiceImpl implements AiSettingsService { throw e; } + boolean created = oldSettings == null; + boolean updated = oldSettings != null; + eventPublisher.publishEvent(SaveEntityEvent.builder() .tenantId(savedSettings.getTenantId()) .entity(savedSettings) .oldEntity(oldSettings) .entityId(savedSettings.getId()) - .created(oldSettings == null) + .created(created) .broadcastEvent(true) .build()); + if (updated) { + publishEvictEvent(AiSettingsCacheEvictEvent.of(savedSettings.getTenantId(), savedSettings.getId())); + } + return savedSettings; } @@ -83,25 +98,20 @@ class AiSettingsServiceImpl implements AiSettingsService { @Override public Optional findAiSettingsByTenantIdAndId(TenantId tenantId, AiSettingsId aiSettingsId) { - return aiSettingsDao.findByTenantIdAndId(tenantId, aiSettingsId); + var cacheKey = AiSettingsCacheKey.of(tenantId, aiSettingsId); + return Optional.ofNullable(cache.get(cacheKey, () -> aiSettingsDao.findByTenantIdAndId(tenantId, aiSettingsId).orElse(null))); } @Override + @Transactional public boolean deleteByTenantIdAndId(TenantId tenantId, AiSettingsId aiSettingsId) { - Optional aiSettingsOpt = aiSettingsDao.findByTenantIdAndId(tenantId, aiSettingsId); - if (aiSettingsOpt.isEmpty()) { - return false; - } - boolean deleted = aiSettingsDao.deleteByTenantIdAndId(tenantId, aiSettingsId); - if (deleted) { - publishDeleteEvent(aiSettingsOpt.get()); - } - return deleted; + return deleteByTenantIdAndIdInternal(tenantId, aiSettingsId); } @Override public Optional> findEntity(TenantId tenantId, EntityId entityId) { - return Optional.ofNullable(aiSettingsDao.findById(tenantId, entityId.getId())); + return findAiSettingsByTenantIdAndId(tenantId, (AiSettingsId) entityId) + .map(aiSettings -> aiSettings); // necessary to cast to HasId } @Override @@ -110,8 +120,22 @@ class AiSettingsServiceImpl implements AiSettingsService { } @Override + @Transactional public void deleteEntity(TenantId tenantId, EntityId id, boolean force) { - deleteByTenantIdAndId(tenantId, new AiSettingsId(id.getId())); + deleteByTenantIdAndIdInternal(tenantId, new AiSettingsId(id.getId())); + } + + private boolean deleteByTenantIdAndIdInternal(TenantId tenantId, AiSettingsId aiSettingsId) { + Optional aiSettingsOpt = aiSettingsDao.findByTenantIdAndId(tenantId, aiSettingsId); + if (aiSettingsOpt.isEmpty()) { + return false; + } + boolean deleted = aiSettingsDao.deleteByTenantIdAndId(tenantId, aiSettingsId); + if (deleted) { + publishDeleteEvent(aiSettingsOpt.get()); + publishEvictEvent(AiSettingsCacheEvictEvent.of(tenantId, aiSettingsId)); + } + return deleted; } @Override @@ -121,8 +145,16 @@ class AiSettingsServiceImpl implements AiSettingsService { if (deletedSettings.isEmpty()) { return; } + aiSettingsDao.deleteByTenantId(tenantId); - deletedSettings.forEach(this::publishDeleteEvent); + + Set cacheKeys = Sets.newHashSetWithExpectedSize(deletedSettings.size()); + deletedSettings.forEach(settings -> { + publishDeleteEvent(settings); + cacheKeys.add(AiSettingsCacheKey.of(settings.getTenantId(), settings.getId())); + }); + + publishEvictEvent(new AiSettingsCacheEvictEvent(cacheKeys)); } private void publishDeleteEvent(AiSettings settings) {