[ 
https://issues.apache.org/jira/browse/FINERACT-1493?page=com.atlassian.jira.plugin.system.issuetabpanels:all-tabpanel
 ]

Aleksandar Vidakovic updated FINERACT-1493:
-------------------------------------------
    Priority: Major  (was: Minor)

> 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
>
> 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)

Reply via email to