[
https://issues.apache.org/jira/browse/FINERACT-1493?page=com.atlassian.jira.plugin.system.issuetabpanels:all-tabpanel
]
Aleksandar Vidakovic updated FINERACT-1493:
-------------------------------------------
Fix Version/s: 1.16.0
> Configurable single and multi tenancy
> -------------------------------------
>
> Key: FINERACT-1493
> URL: https://issues.apache.org/jira/browse/FINERACT-1493
> Project: Apache Fineract
> Issue Type: New Feature
> Reporter: Aleksandar Vidakovic
> Assignee: Aleksandar Vidakovic
> Priority: Major
> Fix For: 1.16.0
>
>
> Make multi-tenancy optional. The way it is implemented and the fact that the
> "TenancyUtil" is used in pretty much all cache annotations slows down
> processing in general quite considerably. This could be achieved by
> introducing properties file based configuration of tenants. Each tenant would
> have his own application-xxx.properties file ("xxx" indicating the tenant
> ID). This would allow dev-ops and admins to de-/activate tenants based on
> file configurations and doesn't require to setup a separate database for the
> tenant configuration beforehand.
> In case we have only 1 tenant (which is probably happening in 90% of the
> production deployments) then we can even replace the routing datasource (see
> [https://github.com/apache/fineract/blame/develop/fineract-core/src/main/java/org/apache/fineract/infrastructure/core/service/database/RoutingDataSource.java]).
> The one tenant scenario would allow us to configure a "normal" data source
> that doesn't need to lookup the tenant information on each request that hits
> the system. Other potential issues related to this are memory leaks (if the
> tenant information is not properly removed) and preventing us to take full
> advantage of virtual threads (thread local variables are still inherited from
> their native thread parents... scoped values in JDK 25 are a better solution,
> needs a separate Jira ticket).
> The current tenant system has also implications on the cache configuration.
> Example from the current codebase:
> {code:java}
> @Cacheable(key =
> "T(org.apache.fineract.infrastructure.core.service.ThreadLocalContextUtil).getTenant().getTenantIdentifier().concat('calId_'
> + #calendarId + '{_}entityId{_}' + #entityId + '{_}entityTypeId{_}' +
> #entityTypeId)")
> {code}
> Pretty much every single cache annotation we have contains SpEL expressions
> like these. In turn that means that every single cache lookup (or storage) is
> using thread local variables to lookup the current tenant details (here only
> tenant ID is needed to ensure that each tenant has an isolated cache and
> values don't leak between tenants). The first thing we should do here - no
> matter if we change the tenant mechanics or not - is to configure the
> separate tenant caches in a central Spring Java configuration instead of
> dragging this concern into each annotation in the code base; this makes life
> (read: maintenance) already a lot easier. If we had to change then something
> then we could do this in one central place and don't have to touch pretty
> much the whole code base. Once that is done then we can introduce conditional
> cache configurations for multi tenant (use of thread local variables as
> before) and single tenant (no use of thread local variables) systems.
> Such a configuration could look like:
> {code:java}
> public class TenantAwareCache implements Cache {
> private final CacheManager delegate;
> private final String name;
> public TenantAwareCache(CacheManager delegate, String name) {
> this.delegate = delegate;
> this.name = name;
> }
> /** Resolve the underlying cache for the current tenant. */
> private Cache current() {
> String tenant =
> ThreadLocalContextUtil.getTenant().getTenantIdentifier();
> // Each tenant gets its own isolated cache region.
> return delegate.getCache(name + "::" + tenant);
> }
> @Override public String getName() { return name; }
> @Override public Object getNativeCache() { return
> current().getNativeCache(); }
> @Override public ValueWrapper get(Object key) { return
> current().get(key); }
> @Override public <T> T get(Object key, Class<T> type) { return
> current().get(key, type); }
> @Override public <T> T get(Object key, Callable<T> loader) { return
> current().get(key, loader); }
> @Override public void put(Object key, Object value) {
> current().put(key, value); }
> @Override public ValueWrapper putIfAbsent(Object key, Object v){ return
> current().putIfAbsent(key, v); }
> @Override public void evict(Object key) {
> current().evict(key); }
> @Override public boolean evictIfPresent(Object key) { return
> current().evictIfPresent(key); }
> @Override public void clear() {
> current().clear(); } // per-tenant!
> @Override public boolean invalidate() { return
> current().invalidate(); }
> }
> {code}
> {code:java}
> public class TenantAwareCacheManager extends AbstractCacheManager {
> private final CacheManager delegate;
> public TenantAwareCacheManager(CacheManager delegate) {
> this.delegate = delegate;
> }
> @Override
> protected Collection<? extends Cache> loadCaches() {
> List<Cache> caches = new ArrayList<>();
> for (String name : delegate.getCacheNames()) {
> caches.add(new TenantAwareCache(delegate, name));
> }
> return caches;
> }
> @Override
> protected Cache getMissingCache(String name) {
> return new TenantAwareCache(delegate, name);
> }
> }
> {code}
> {code:java}
> @Configuration
> @EnableCaching
> public class CacheConfiguration {
> @Bean
> public CacheManager cacheManager() {
> // Any delegate works: Caffeine, ConcurrentMapCacheManager,
> RedisCacheManager, ...
> CaffeineCacheManager delegate = new CaffeineCacheManager();
> delegate.setCaffeine(
> Caffeine.newBuilder()
> .expireAfterWrite(10, TimeUnit.MINUTES)
> .maximumSize(10_000)
> );
> // Allow dynamic per-tenant cache creation (default is true).
> delegate.setAsyncCacheMode(false);
> return new TenantAwareCacheManager(delegate);
> }
> }
> {code}
> This would allow us to simplify the cache annoations to (using previous -
> already complex - example):
> {code:java}
> @Cacheable(key = "'calId_' + #calendarId + '{_}entityId{_}' + #entityId +
> '{_}entityTypeId{_}' + #entityTypeId")
> {code}
--
This message was sent by Atlassian Jira
(v8.20.10#820010)