Refactor caches (#2775)
* added static usage of cache in order access it easier * added mandatory (in hawkbit-core) registration - always tenant aware caches shall be used - hawkbit depends on it * added per cache and tenant name configuration * (not really realted to caches) but in order to be easier evicted entities after commit handlers are now statically accessed Signed-off-by: Avgustin Marinov <Avgustin.Marinov@bosch.com>
This commit is contained in:
@@ -0,0 +1,128 @@
|
||||
/**
|
||||
* Copyright (c) 2025 Contributors to the Eclipse Foundation
|
||||
*
|
||||
* This program and the accompanying materials are made
|
||||
* available under the terms of the Eclipse Public License 2.0
|
||||
* which is available at https://www.eclipse.org/legal/epl-2.0/
|
||||
*
|
||||
* SPDX-License-Identifier: EPL-2.0
|
||||
*/
|
||||
package org.eclipse.hawkbit.tenancy;
|
||||
|
||||
import java.util.Collection;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Optional;
|
||||
import java.util.concurrent.ConcurrentHashMap;
|
||||
|
||||
import com.github.benmanes.caffeine.cache.Caffeine;
|
||||
import lombok.AccessLevel;
|
||||
import lombok.NoArgsConstructor;
|
||||
import lombok.NonNull;
|
||||
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.core.env.Environment;
|
||||
import org.springframework.lang.Nullable;
|
||||
|
||||
/**
|
||||
* A spring Cache Manager that handles the multi tenancy.
|
||||
* <ul>
|
||||
* <li>If a tenant is resolved by the {@link TenantResolver}, a dedicated cache manager for that tenant is used/created.</li>
|
||||
* <li>If no tenant is resolved, a global cache manager is used.</li>
|
||||
* </ul>
|
||||
*/
|
||||
@NoArgsConstructor(access = AccessLevel.PRIVATE)
|
||||
@SuppressWarnings("java:S6548") // singleton holder ensures static access to spring resources in some places
|
||||
public class TenantAwareCacheManager implements CacheManager {
|
||||
|
||||
private static final String CONFIG_PREFIX = "hawkbit.cache.";
|
||||
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<>();
|
||||
|
||||
private TenantResolver resolver;
|
||||
private Environment env;
|
||||
|
||||
// default caffeine cache spec - see com.github.benmanes.caffeine.cache.CaffeineSpec javadoc for format details
|
||||
private String defaultSpec;
|
||||
|
||||
public static TenantAwareCacheManager getInstance() {
|
||||
return SINGLETON;
|
||||
}
|
||||
|
||||
@Autowired
|
||||
public void init(final TenantResolver resolver, final Environment env) {
|
||||
this.resolver = resolver;
|
||||
this.env = env;
|
||||
defaultSpec = env.resolvePlaceholders("${" + CONFIG_PREFIX + CONFIG_SPEC + ":expireAfterWrite=${hawkbit.cache.ttl:10s}}");
|
||||
globalCacheManager = new TenantCacheManager(null);
|
||||
}
|
||||
|
||||
@Nullable
|
||||
@Override
|
||||
public Cache getCache(@NonNull final String name) {
|
||||
return Optional.ofNullable(resolver.resolveTenant())
|
||||
.map(currentTenant -> tenant2CacheManager.computeIfAbsent(currentTenant, TenantCacheManager::new))
|
||||
.orElse(globalCacheManager)
|
||||
.getCache(name);
|
||||
}
|
||||
|
||||
@NonNull
|
||||
@Override
|
||||
public Collection<String> getCacheNames() {
|
||||
final String currentTenant = resolver.resolveTenant();
|
||||
if (currentTenant == null) {
|
||||
return globalCacheManager.getCacheNames();
|
||||
} else {
|
||||
final CacheManager cacheManager = tenant2CacheManager.get(currentTenant);
|
||||
return cacheManager == null ? List.of() : cacheManager.getCacheNames();
|
||||
}
|
||||
}
|
||||
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
||||
private class TenantCacheManager implements CacheManager {
|
||||
|
||||
private final String tenant;
|
||||
private final Map<String, Cache> caches = new ConcurrentHashMap<>();
|
||||
|
||||
private TenantCacheManager(final String tenant) {
|
||||
this.tenant = tenant;
|
||||
}
|
||||
|
||||
@Nullable
|
||||
@Override
|
||||
public Cache getCache(@NonNull final String name) {
|
||||
return caches.computeIfAbsent(name, n -> {
|
||||
// try tenant and cache specific config first
|
||||
String spec = env.getProperty(CONFIG_PREFIX + (tenant == null ? "" : tenant + ".") + name + "." + CONFIG_SPEC);
|
||||
if (spec == null) {
|
||||
// try cache specific config next
|
||||
spec = env.getProperty(CONFIG_PREFIX + name + "." + CONFIG_SPEC);
|
||||
if (spec == null) {
|
||||
spec = defaultSpec;
|
||||
}
|
||||
}
|
||||
return new CaffeineCache(n, Caffeine.from(spec).build(), false);
|
||||
});
|
||||
}
|
||||
|
||||
@NonNull
|
||||
@Override
|
||||
public Collection<String> getCacheNames() {
|
||||
return caches.keySet();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,24 @@
|
||||
/**
|
||||
* Copyright (c) 2025 Contributors to the Eclipse Foundation
|
||||
*
|
||||
* This program and the accompanying materials are made
|
||||
* available under the terms of the Eclipse Public License 2.0
|
||||
* which is available at https://www.eclipse.org/legal/epl-2.0/
|
||||
*
|
||||
* SPDX-License-Identifier: EPL-2.0
|
||||
*/
|
||||
package org.eclipse.hawkbit.tenancy;
|
||||
|
||||
import lombok.AccessLevel;
|
||||
import lombok.NoArgsConstructor;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
|
||||
@Configuration
|
||||
public class TenantCacheConfiguration {
|
||||
|
||||
@Bean
|
||||
TenantAwareCacheManager cacheManager() {
|
||||
return TenantAwareCacheManager.getInstance();
|
||||
}
|
||||
}
|
||||
@@ -1,103 +0,0 @@
|
||||
/**
|
||||
* Copyright (c) 2015 Bosch Software Innovations GmbH and others
|
||||
*
|
||||
* This program and the accompanying materials are made
|
||||
* available under the terms of the Eclipse Public License 2.0
|
||||
* which is available at https://www.eclipse.org/legal/epl-2.0/
|
||||
*
|
||||
* SPDX-License-Identifier: EPL-2.0
|
||||
*/
|
||||
package org.eclipse.hawkbit.tenancy.cache;
|
||||
|
||||
import java.util.Collection;
|
||||
import java.util.Collections;
|
||||
import java.util.Optional;
|
||||
|
||||
import lombok.NonNull;
|
||||
import org.eclipse.hawkbit.tenancy.TenantAware;
|
||||
import org.springframework.cache.Cache;
|
||||
import org.springframework.cache.CacheManager;
|
||||
import org.springframework.lang.Nullable;
|
||||
|
||||
/**
|
||||
* A {@link CacheManager} delegator which wraps the {@link CacheManager#getCache(String)} and {@link CacheManager#getCacheNames()}
|
||||
* to include the {@link TenantAware#getCurrentTenant()} when accessing a cache, so caches are seperated.
|
||||
* <p/>
|
||||
* Additionally, it also provides functionality to retrieve all caches overall tenants at once, for monitoring and system access.
|
||||
*/
|
||||
public class TenantAwareCacheManager implements TenantCacheManager {
|
||||
|
||||
private static final String TENANT_CACHE_DELIMITER = "|";
|
||||
|
||||
private final CacheManager delegate;
|
||||
private final TenantAware tenantAware;
|
||||
|
||||
/**
|
||||
* Constructor.
|
||||
*
|
||||
* @param delegate the {@link CacheManager} to delegate to.
|
||||
* @param tenantAware the tenant aware to retrieve the current tenant
|
||||
*/
|
||||
public TenantAwareCacheManager(final CacheManager delegate, final TenantAware tenantAware) {
|
||||
this.delegate = delegate;
|
||||
this.tenantAware = tenantAware;
|
||||
}
|
||||
|
||||
@Nullable
|
||||
@Override
|
||||
public Cache getCache(@NonNull final String name) {
|
||||
final String currentTenant = tenantAware.getCurrentTenant();
|
||||
if (isTenantInvalid(currentTenant)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return delegate.getCache(buildKey(currentTenant.toUpperCase(), name));
|
||||
}
|
||||
|
||||
@NonNull
|
||||
@Override
|
||||
public Collection<String> getCacheNames() {
|
||||
final String currentTenant = tenantAware.getCurrentTenant();
|
||||
if (isTenantInvalid(currentTenant)) {
|
||||
return Collections.emptyList();
|
||||
}
|
||||
|
||||
return getCacheNames(currentTenant.toUpperCase());
|
||||
}
|
||||
|
||||
@Override
|
||||
public Cache getDirectCache(final String name) {
|
||||
return delegate.getCache(name);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void evictCaches(final String tenant) {
|
||||
getCacheNames(tenant).forEach(
|
||||
cacheName -> Optional.ofNullable(delegate.getCache(buildKey(tenant, cacheName))).ifPresent(Cache::clear));
|
||||
}
|
||||
|
||||
/**
|
||||
* A direct-access for retrieving all cache names overall tenants.
|
||||
*
|
||||
* @return all cache names without tenant check
|
||||
*/
|
||||
public Collection<String> getDirectCacheNames() {
|
||||
return delegate.getCacheNames();
|
||||
}
|
||||
|
||||
private static boolean isTenantInvalid(final String tenant) {
|
||||
return tenant == null || tenant.contains(TENANT_CACHE_DELIMITER);
|
||||
}
|
||||
|
||||
private static String buildKey(final String tenant, final String cacheName) {
|
||||
return tenant + TENANT_CACHE_DELIMITER + cacheName;
|
||||
}
|
||||
|
||||
private Collection<String> getCacheNames(final String tenant) {
|
||||
final String tenantWithDelimiter = tenant + TENANT_CACHE_DELIMITER;
|
||||
return delegate.getCacheNames().parallelStream()
|
||||
.filter(cacheName -> cacheName.startsWith(tenantWithDelimiter))
|
||||
.map(cacheName -> cacheName.substring(tenantWithDelimiter.length()))
|
||||
.toList();
|
||||
}
|
||||
}
|
||||
@@ -1,35 +0,0 @@
|
||||
/**
|
||||
* Copyright (c) 2015 Bosch Software Innovations GmbH and others
|
||||
*
|
||||
* This program and the accompanying materials are made
|
||||
* available under the terms of the Eclipse Public License 2.0
|
||||
* which is available at https://www.eclipse.org/legal/epl-2.0/
|
||||
*
|
||||
* SPDX-License-Identifier: EPL-2.0
|
||||
*/
|
||||
package org.eclipse.hawkbit.tenancy.cache;
|
||||
|
||||
import org.springframework.cache.Cache;
|
||||
import org.springframework.cache.CacheManager;
|
||||
|
||||
/**
|
||||
* A cache interface which handles multi tenancy.
|
||||
*/
|
||||
public interface TenantCacheManager extends CacheManager {
|
||||
|
||||
/**
|
||||
* A direct-access for retrieving the cache without including the current tenant key. This is necessary e.g. for retrieving caches not for
|
||||
* the current tenant.
|
||||
*
|
||||
* @param name the name of the cache to retrieve directly
|
||||
* @return the cache associated with the name without tenancy separation
|
||||
*/
|
||||
Cache getDirectCache(String name);
|
||||
|
||||
/**
|
||||
* Evicts all caches for a given tenant. All caches under a certain tenant gets evicted.
|
||||
*
|
||||
* @param tenant the tenant to evict caches
|
||||
*/
|
||||
void evictCaches(String tenant);
|
||||
}
|
||||
@@ -1,2 +1,3 @@
|
||||
org.eclipse.hawkbit.tenancy.TenantCacheConfiguration
|
||||
org.eclipse.hawkbit.tenancy.TenantMetricsConfiguration.WebConfig
|
||||
org.eclipse.hawkbit.tenancy.TenantMetricsConfiguration.RepositoryConfig
|
||||
|
||||
Reference in New Issue
Block a user