Search code examples
grailsgrails-orm

How to resolve a database tenant in a multi tenant grails app when the code is run from a scheduled job?


I am using grails 4.0.3 and the app is multi tenant in database level. I am resolving tenant based on subdomain of the request. The multi tenancy is working well for all the cases except for the scheduled jobs. My understanding is since the scheduled job do not have a request scope the tenant resolver is never called because there is no request to trigger the tenant resolver. I have a database transactions going on inside scheduled jobs. And these transactions always point to default database.

How can i resolve a tenant from such scenarios where there is no request scope.

My application.yml configuration for tenant resolver is as follows:

grails:
    profile: web
    codegen:
        defaultPackage: com.pomco.middleware
    gorm:
        reactor:
            # Whether to translate GORM events into Reactor events
            # Disabled by default for performance reasons
            events: false
        multiTenancy:
            mode: DATABASE
            tenantResolverClass: com.pomco.middleware.multitenant.CustomSubDomainTenantResolver

And the CustomSubDomainTenantResolver looks like this

class CustomSubDomainTenantResolver implements TenantResolver{

    @Override
    Serializable resolveTenantIdentifier() {

        RequestAttributes requestAttributes = RequestContextHolder.getRequestAttributes()
        if(requestAttributes instanceof ServletWebRequest) {
            HttpServletRequest httpServletRequest = ((ServletWebRequest) requestAttributes).getRequest()
            def env = DatasourceEnvironment.lookupEnvironmentByHostname(httpServletRequest.getServerName())
            String subDomainId = env?.tenantId
            if( subDomainId) {
                if(!DatabaseProvisioningService.getSourceByTenantId(subDomainId) && subDomainId != 'DEFAULT'){
                    def tenantByUrl = DatabaseProvisioningService.tenantSources.find {it.value == DatabaseProvisioningService.getDbUrlByEnv(env)}
                    if (tenantByUrl){
                        return tenantByUrl.key
                    }
                }
                return subDomainId
            }
            else {
                return ConnectionSource.DEFAULT
            }
        }
        else if(!requestAttributes)
            return ConnectionSource.DEFAULT
        throw new TenantNotFoundException("Tenant could not be resolved outside a web request")
    }
}

Solution

  • The solution for this problem is using tenant manually by using withId closure inside scheduled job.

    For example i was not able to get tenant in following Scheduled job.

    @CurrentTenant
    def ExampleService{
    
    // this method runs with required tenant as this is a service method annotated with @CurrentTenant
    def methodThatHasTenantScope(){
        ScheduledFuture<?> helperFuture = null
        def result = null
    
        ScheduledFuture<?> future = ses.scheduleWithFixedDelay(new Runnable() {
            // BUT here there is no tenant scope as this is a threaded task which doesn't have a request scope
            // so this job runs with default tenant
            private long count = 0
            @Override
            void run() {
                // job task here runs with default tenant
            }
        }, DELAY, FREQUENCY_MILLIS, TimeUnit.MILLISECONDS)
        return result
    }
    }
    

    Now we need to get the tenant id manually in the method methodThatHasTenantScope and pass it to the scheduled job method as follows:

    @CurrentTenant
    def ExampleService{
    
            @Autowired
            HibernateDatastore hibernateDatastore
    
            def methodThatHasTenantScope(){
                Serializable tenantId = Tenants.currentId(HibernateDatastore)
                log.trace("Current tenant id: $tenantId")
                ScheduledFuture<?> helperFuture = null
                def result = null
    
                ScheduledFuture<?> future = ses.scheduleWithFixedDelay(new Runnable() {
                    private long count = 0
                    @Override
                    void run() {
                     //here we have tenantId from the scope of the method
                        withId(tenantId){
                            // job task runs with required tenant
                        }
                    }
                }, DELAY, FREQUENCY_MILLIS, TimeUnit.MILLISECONDS)
                return result
            }
     }