Refactor caches (#2775) (#2777)

* TenantAwareCacheManager define CacheEvictEvent which could be used to evict entities in general way
* JpaTenantConfigurationManagement start using genera cache approach

Signed-off-by: Avgustin Marinov <Avgustin.Marinov@bosch.com>
This commit is contained in:
Avgustin Marinov
2025-10-28 14:13:53 +02:00
committed by GitHub
parent 2d562f64cb
commit d488ad6b5f
16 changed files with 462 additions and 377 deletions

View File

@@ -13,18 +13,21 @@ import java.util.Collection;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.concurrent.Callable;
import java.util.concurrent.ConcurrentHashMap;
import com.github.benmanes.caffeine.cache.Caffeine;
import lombok.AccessLevel;
import lombok.NoArgsConstructor;
import lombok.NonNull;
import lombok.Value;
import lombok.extern.slf4j.Slf4j;
import org.eclipse.hawkbit.tenancy.TenantAware.TenantResolver;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.cache.Cache;
import org.springframework.cache.CacheManager;
import org.springframework.cache.caffeine.CaffeineCache;
import org.springframework.context.event.EventListener;
import org.springframework.core.env.Environment;
import org.springframework.lang.Nullable;
@@ -44,7 +47,6 @@ public class TenantAwareCacheManager implements CacheManager {
private static final String CONFIG_SPEC = "spec";
private static final TenantAwareCacheManager SINGLETON = new TenantAwareCacheManager();
private CacheManager globalCacheManager;
private final Map<String, CacheManager> tenant2CacheManager = new ConcurrentHashMap<>();
@@ -66,13 +68,14 @@ public class TenantAwareCacheManager implements CacheManager {
globalCacheManager = new TenantCacheManager(null);
}
@Nullable
@NonNull
@Override
public Cache getCache(@NonNull final String name) {
return Optional.ofNullable(resolver.resolveTenant())
final Cache cache = Optional.ofNullable(resolver.resolveTenant())
.map(currentTenant -> tenant2CacheManager.computeIfAbsent(currentTenant, TenantCacheManager::new))
.orElse(globalCacheManager)
.getCache(name);
return cache == null ? new Nop(name) : cache;
}
@NonNull
@@ -87,11 +90,54 @@ public class TenantAwareCacheManager implements CacheManager {
}
}
public void evictTenant(final String tenant) {
if (tenant == null) {
globalCacheManager.getCacheNames().forEach(name -> Optional.ofNullable(globalCacheManager.getCache(name)).ifPresent(Cache::clear));
} else {
tenant2CacheManager.remove(tenant);
/**
* Ensures that cache eviction takes place in microservice mode in case of deletions.
*
* @param event The event indicating that a configuration value has been deleted.
*/
@EventListener
@SuppressWarnings("java:S3776") // not too complex and this way is more readable
public void onCacheEvictEvent(final CacheEvictEvent event) {
final CacheManager cacheManager = event.getTenant() == null ? globalCacheManager : tenant2CacheManager.get(event.getTenant());
if (cacheManager != null) {
if (event.getCacheName() == null) { // evict all caches
if (event.getTenant() == null) { // global cache
cacheManager.getCacheNames().forEach(name -> {
final Cache cache = cacheManager.getCache(name);
if (cache != null) {
cache.clear();
}
});
} else { // tenant specific cache
tenant2CacheManager.remove(event.getTenant());
}
} else {
final Cache cache = cacheManager.getCache(event.getCacheName());
if (cache != null) {
if (event.getCacheKey() == null) { // evict all keys
cache.clear();
} else { // evict specific key
cache.evict(event.getCacheKey());
}
}
}
}
}
public interface CacheEvictEvent {
String getTenant();
String getCacheName(); // null means - all caches shall be evicted
Object getCacheKey(); // null means - all keys shall be evicted
@Value
class Default implements CacheEvictEvent {
String tenant;
String cacheName;
Object cacheKey;
}
}
@@ -118,7 +164,17 @@ public class TenantAwareCacheManager implements CacheManager {
}
}
try {
return new CaffeineCache(n, Caffeine.from(spec).build(), false);
return new CaffeineCache(n, Caffeine.from(spec).build(), false) {
@Nullable
@Override
@SuppressWarnings("java:S2638") // used internally in hawkbit and want to return null instead of error
protected Object toStoreValue(@Nullable Object userValue) {
// we want to return pure null to caffeine when null, in order to do not cache
// we want to allow null results but not to be cached!
return userValue;
}
};
} catch (final IllegalArgumentException e) {
log.error("Invalid cache spec: {}", spec, e);
throw new IllegalStateException("Invalid cache spec: " + spec, e);
@@ -132,4 +188,52 @@ public class TenantAwareCacheManager implements CacheManager {
return caches.keySet();
}
}
@Value
private static class Nop implements Cache {
String name;
@NonNull
@Override
public String getName() {
return name;
}
@NonNull
@Override
public Object getNativeCache() {
return this;
}
@Override
public ValueWrapper get(@NonNull final Object key) {
return null;
}
@Override
public <T> T get(@NonNull final Object key, final Class<T> type) {
return null;
}
@Override
public <T> T get(@NonNull final Object key, @NonNull final Callable<T> valueLoader) {
return null;
}
@Override
public void put(@NonNull final Object key, final Object value) {
// nop
}
@Override
public void evict(@NonNull final Object key) {
// nop
}
@Override
public void clear() {
// nop
}
}
}