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

Aleksandar Vidakovic updated FINERACT-1493:
-------------------------------------------
    Description: 
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}


  was: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.


> 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: Minor
>
> 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