Sunday, July 15, 2018

Add Custom Scope to a Grails 2 Application . . .

Grails services are Spring managed singleton beans by default. Singleton is one of the five different scopes (singleton, prototype, request, session, and globalSession) that Spring framework offers for managed beans. Prior to Spring 2.5, there were only 2 standard scopes: singleton and prototype. Spring 2.5 added 3 additional scopes: requestsession and globalSession for use in web-based applications. Grails adds two more scopes to the mix: flow and flash.

Sometimes, you might run into a situation that none of these scopes meet your requirements. For instance, when you have a multi-tenant or multi-client application, what you may need is a client scope, a separate bean instance for each client. Recently, I ran into this situation. Addressing an existing performance issue drove me into this situation, making an use case for a custom scope.

We have a Grails 2.5.4 multi-client application with clientId included in URL mappings for end-points like: /clients/$clientId/resource. There has been a performance issue with one of the end-points backed by a service. The service has a heavy-weight method which builds and caches client data once for each client with an expiration time set to expire cache few hours once built. It takes few minutes to build this data due to it's nature and some data rule complexities. Once built and cached, it rebuilds data from the cache really fast. One of the performance improvements identified upfront was to limit concurrent calls to that method. Obviously, there is no point in allowing concurrent builds for the same client. The solution to put in place was to allow one-and-only-one concurrent call for any given client, but allow concurrent calls for different clients one per each client. Making the method synchronized is an easy way to limit concurrent calls, but it only solves half of the problem. As service is a singleton bean and synchronized method uses this as the object lock. With that, concurrent calls get executed serially, across all clients. But, we need to allow concurrent executions for different clients, but not for the same client. It is still possible to achieve this with synchronized method, but only when there is one service instance per client. This opened the need for a custom scope. Since Spring 2.0, the concept of scoping beans is made extensible and is the way to add custom scope.

Spring maintains a cache of all scoped beans in it's container. Every scope except prototype has it's own in-memory cache of bean instances which is initialized & populated when the application gets started and maintained by spring container. All spring managed beans either get instantiated or get proxies created during the application startup. Obviously, prototype scoped beans don't need any kind of cache as they get created and injected into all beans that are auto-wired with Singleton beans. A Grails service being singleton bean is also stateless and hence it's methods can be executed by multiple concurrent threads.

Spring has a good documentation of all these details and the API is well-documented as well. For creating a custom scope, all we need to do is: 1) Create a custom Scope object by implementing org.springframework.beans.factory.config.Scope interface and 2) Register custom scope with Spring container. 3) Scope required beans at this custom scope. Sounds simple, in a Grails application it should even be simpler. Lets get through step-by-step implementation details of adding a new custom scope: Client scope to a Grails 2 application.

Environment: Grails 2.5.4, Spring 4.1.8, Java 8 on MacOS High Sierra 10.13.5

Step-1 Implement custom Scope

This is straight forward. Just implement the interface and provide implementation for all essential methods. Remember, the implementation should also maintain it's own cache for this custom scoped beans. Also, make sure that any needed scoped context (in this case, it is clientId) is available and accessible in this implementation when a reference to the bean scoped at this custom scope is needed.

src/groovy/com/giri/grails/scope/ClientScope.groovy
package com.giri.grails.scope import grails.plugin.springsecurity.SpringSecurityService import grails.util.Holders import groovy.util.logging.Log4j import org.springframework.beans.factory.ObjectFactory import org.springframework.beans.factory.config.Scope /** * Custom scope bean for client-scoped services registered in resources.groovy. * * All services(Spring beans) that need client-scope should define static property of scope like: * static scope = ClientScope.SCOPE_NAME * * @see resources.groovy * * @author gpottepalem * Created on July 15, 2018 */ @Log4j class ClientScope implements Scope { static final String SCOPE_NAME = 'clientScope' /** * Client scoped bean store. * A synchronized multi-thread-safe map of various beans scoped with {@link ClientScope#SCOPE_NAME} * Spring framework depends on this store for maintaining beans defined with this custom client-scope. * e.g. Two clients with one common service and two different services * [ client-1 : [ * 'ClientService' : clientServiceObjectRef, * 'OtherService' : otherServiceObjectRef * ], * client-2 : [ * 'ClientService' : clientServiceObjectRef, * 'SomeOtherService' : someOtherServiceObjectRef * ] * ] */ private Map<Integer, Map<String, Object<?> clientScopedBeansMap = [:].asSynchronized() /** * Helper method, returns client-scoped beans for a given client. * @param clientId the client id * @return A map of client-scoped beans for the given client. */ private Map<String Object> getClientScopedBeans(Integer clientId) { if(!clientScopedBeansMap[clientId]) { clientScopedBeansMap[clientId] = [:].asSynchronized() log.debug "No client scoped bean found for client:$clientId, just created new map" } return clientScopedBeansMap[clientId] } /** * Helper method, returns clientId taking it from authenticated user. * @return clientId of the current user logged in */ private Integer getClientId() { (Holders.grailsApplication.mainContext.getBean('springSecurityService') as SpringSecurityService).authentication?.clientId } @Override Object get(String name, ObjectFactory<?> objectFactory) { synchronized (this) { Integer clientId = getClientId() Map<String Object> clientScopedBeans = getClientScopedBeans(clientId) if (!clientScopedBeans[name]) { clientScopedBeans[name] = objectFactory.object log.debug "Added new instance: ${clientScopedBeans[name]} for bean: $name for client:$clientId to the bean store" } return clientScopedBeans[name] } } @Override Object remove(String name) { Map<String Object> scopedBeanMap = getClientScopedBeans(getClientId()) return scopedBeanMap.remove(name) } @Override void registerDestructionCallback(String s, Runnable runnable) { // nothing to register } @Override Object resolveContextualObject(String s) { return null } @Override String getConversationId() { return SCOPE_NAME } }

Step-2 Register custom scope

Register custom scope in:
grails-app/conf/spring/resources.groovy
import com.giri.grails.scope.ClientScope import org.springframework.beans.factory.config.CustomScopeConfigurer beans = { ... // Custom scope: per-client clientScope(ClientScope) // register all custom scopes customScopeConfigurer(CustomScopeConfigurer) { scopes = [(ClientScope.SCOPE_NAME) : ref('clientScope')].asImmutable() } ... }

Step-3 Scope a client-specific service with custom scope

Say, we have a service ClientService that we need to scope at clientScope. Just define a static scope property set with this custom scope like:
grails-app/services/com/giri/ClientService.groovy
package com.giri import com.giri.grails.scope.ClientScope class ClientService { static scope = ClientScope.SCOPE_NAME def clientDataBuilderService //DI //delegates to clientDataBuilderService synchronized Map buildClientData(String clientId) { clientDataBuilderService.buildData(clientId) } ... }

Step-4 Custom-scoped service - Dependency Injection

Grails supports Spring Dependency Injection by convention. A property name that matches the class name of a Spring managed bean gets injected automatically. Unlike Spring applications, you don't need @Autowired annotation on a property or a setter method. But for custom scoped beans, the actual custom scoped bean might get instantiated lazily when the client context (in this case, clientId) is available in the application (either taken from the request, session, security authentication etc.). This context is not known at the start of the application. So, without creating proxies, dependency injection may not be possible. This requires the scoped bean to be programmatically resolved, unlike auto-wired by Grails convention. The following is a way to get a handle to the scoped bean instance. The assumption here is, that the context needed for creating a scoped bean for specific client (clientId) is available in the security context and all end-points are secured.

grails-app/controllers/com/giri/ClientController.groovy
package com.giri import grails.converters.JSON import grails.util.Holders class ClientController { ... def index(String clientId) { ClientService clientService = Holders.grailsApplication.mainContext.getBean('clientService', ClientService.class) clientService.buildClientData(clientId) as JSON } ... }

TIP: Unit Testing

Without scoped bean dependency injected by following Grails naming convention for DI and by referring it using Grails ApplicationContext puts us into a limitation in unit tests. The actual scoped bean instance is needed which can otherwise be mocked if it was injected. Typically, Grails doesn't load bean definitions as the complete ApplicationContext is not needed in unit-tests. The following are two options that Grails offers to get away with this and have bean definitions loaded and ApplicationContext available for unit-tests:

Option-1
test/unit/com/giri/ClientControllerSpec.groovy
package com.giri import grails.test.mixin.TestFor import spock.lang.Specification @TestFor(ClientController) class ClientControllerSpec extends Specification { static loadExternalBeans = true //loads beans defined in resources.groovy and beans are available in applicationConext ClientService clientService def setup() { //controller.clientService = Mock(ClientService) //doesn't work clientService = applicationContext.getBean('clientService') clientService.clientDataBuilderService = Mock(ClientDataBuilderService) } void "test index"() { when: controller.index('client-1') then: (1.._) * clientService.clientDataBuilderService.buildData('client-1') >> ['abcd' : 1234] and: response.json == [ "abcd": 1234 ] } }

Option-2
package com.giri import grails.test.mixin.TestFor import spock.lang.Specification @TestFor(ClientController) class ClientControllerSpec extends Specification { ClientService clientService def setup() { //controller.clientService = Mock(ClientService) //doesn't work //define bean to get into applicationContext as it is not injected to mock it out defineBeans { clientService(ClientService) } clientService = applicationContext.getBean('clientService') clientService.clientDataBuilderService = Mock(ClientDataBuilderService) } void "test index"() { when: controller.index('client-1') then: (1.._) * clientService.clientDataBuilderService.buildData('client-1') >> ['abcd' : 1234] and: response.json == [ "abcd": 1234 ] } }

Summary

To add custom scope to a Grails Application, all you need to know is some Spring Framework details and Grails integration with Spring. There is still one improvement that can be made to this solution, getting scoped beans injected by following Grails convention. This particular use-case requires client context (clientId) to be available for scoped bean cache maintenance. This makes the case for a proxy to be generated for this custom-scoped beans. A proxy bean needs to be generated and injected for scoped bean at the start of application. The proxy bean should be able to retrieve the actual target bean from the scoped cache and delegate method calls to that target object. This might simply need some additional Spring configurations, I guess. I left it out for now, to explore later.

References

No comments:

Post a Comment