Skip to content

Classes for building Guice Scopes easily transferable between Threads.

License

Notifications You must be signed in to change notification settings

morgwai/guice-context-scopes

Repository files navigation

Guice Context Scopes

Classes for building Guice Scopes easily transferable when dispatching work to other Threads.
Copyright 2021 Piotr Morgwai Kotarbinski, Licensed under the Apache License, Version 2.0

latest release: 11.1 (javadoc)

OVERVIEW

Asynchronous servers (such as gRPC or asynchronous Servlets) often need to switch between various Threads. This requires extra care to not lose a given current Guice Scope: it needs to be preserved as long as we are in the context of a given event/request/call/session, regardless of Thread switching.

To ease this up, this lib formally introduces a notion of an InjectionContext that stores scoped Objects and can be tracked using ContextTrackers when switching between Threads. Trackers are in turn used by ContextScopes to obtain the Context that is current at a given moment and from which scoped Objects will be obtained.

DEVELOPING SCOPES

Creation of a (set of) custom Scope(s) boils down to the below things:

  1. Defining at least one concrete subclass of TrackableContext (subclass of InjectionContext). For example ServletRequestContext in case of Java Servlet containers.
  2. Defining a concrete subclass of ScopeModule with public final ContextScope fields corresponding to the above TrackableContext subclasses, initialized with newContextScope(name, trackableCtxClass) calls.
  3. Hooking creation of the above TrackableContext instances into a given existing framework: for example in case of Java Servlet containers, a Filter may be created that for each new incoming ServletRequest will execute chain.doFilter(request, response) within a newly created ServletRequestContext instance.
  4. Optionally defining subclasses of InjectionContext for Context types that are induced by some TrackableContext subclass. For example entering into a ServletRequestContext may induce entering into the corresponding HttpSessionContext.
  5. Defining public final InducedContextScope fields in the ScopeModule subclass from the point 2, corresponding to the above induced Context types (if any) and initialized with newInducedContextScope(...) calls.
  6. For app-level code development convenience, defining a public final ContextBinder field in the ScopeModule subclass from the point 2, initialized with a newContextBinder() call. This may be useful for app developers when creating their global ContextTrackingExecutor instances bound for injection with toInstance(myGlobalCtxTrackingExecutor) calls in their Modules: see USAGE section.

App developers should then create a global instance of this ScopeModule subclass defined in the point 2, pass its Scopes to their other Modules (as needed for scoping of their app components, see PORTABLE MODULES section) and finally pass this ScopeModule instance to their Guice.createInjector(...) call(s) along with their other Moudles.

USAGE

When switching Threads in a low level library code, static helper methods getActiveContexts(List<ContextTracker<?>>) and executeWithinAll(List<TrackableContext>, Runnable) can be used to manually transfer all active Contexts:

class MyComponent {

    @Inject List<ContextTracker<?>> allTrackers;

    void methodThatCallsSomeAsyncMethod(/* ... */) {
        // other code here...
        final var activeCtxs = ContextTracker.getActiveContexts(allTrackers);
        someAsyncMethod(
            arg1,
            /* ... */
            argN,
            (callbackParamIfNeeded) -> TrackableContext.executeWithinAll(
                activeCtxs,
                () -> {
                    // callback code here will run within the same Contexts
                    // as methodThatDispatchesToExecutor(...)
                }
            )
        );
    }
}

For higher level abstraction and app-level code, ContextBinder class was introduced that allows to bind closures defined as common functional interfaces (Runnable, Callable, Consumer, BiConsumer, Function, BiFunction) to Contexts that were active at the time of a given binding:

class MyComponent {  // compare with the "low-level" version above

    @Inject ContextBinder ctxBinder;

    void methodThatCallsSomeAsyncMethod(/* ... */) {
        // other code here...
        someAsyncMethod(
            arg1,
            /* ... */
            argN,
            ctxBinder.bindToContext((callbackParamIfNeeded) -> {
                // callback code here will run within the same Contexts
                // as methodThatDispatchesToExecutor(...)
            })
        );
    }
}

For app development convenience, ContextTrackingExecutor interface and decorator was provided that uses ContextBinderto automatically transfer active Contexts when executing tasks.

class MyOtherComponent {

    ContextTrackingExecutor executor;

    @Inject void setContextBinder(ContextBinder ctxBinder) {
        executor = ContextTrackingExecutor.of(Executors.newFixedThreadPool(5), ctxBinder);
    }

    void methodThatDispatchesToExecutor(/* ... */) {
        // other code here...
        executor.execute(() -> {
            // task code here will run within the same Contexts
            // as methodThatDispatchesToExecutor(...)
        });
    }
}

DERIVED LIBS

gRPC Guice Scopes
Servlet and Websocket Guice Scopes

DEVELOPING PORTABLE MODULES

As the official Guice Servlet Scopes lib stores its Scope instances as static vars (ServletScopes.REQUEST and ServletScopes.SESSION), developers tended to scope their components using these static references directly in their Modules or even worse using @RequestScoped and @SessionScoped annotations. This makes such Modules (or even whole components in case of annotations) tightly tied to Java Servlet framework and if there's a need to use them with gRPC or websockets, they must be rewritten.

To avoid this problem, first, scoping annotations should never be used in components that are meant to be portable, so that they are not tied to any given framework. Instead they should be explicitly bound in appropriate Scopes in their corresponding Modules.
Second, Modules should not use static references to Scopes, but instead accept Scopes as their constructor params. In case of most technologies, usually 2 types of Scopes make sense: a short-term one for storing stuff like EntityManagers, pooled JDBC Connections or enclosing transactions and a long-term one for storing stuff like auth-tokens, credentials, client conversation state (like the immortal shopping cart) etc.

Therefore most Modules should have a constructor that accepts such 2 Scope references (public MyModule(Scope shortTermScope, Scope longTermScope) {...}) and then use these to bind components. This allows to reuse such Modules in several environments:

  • When developing a Servlet app using the official Guice Servlet Scopes lib, MyModule may be created with new MyModule(ServletScopes.REQUEST, ServletScopes.SESSION).
  • In case of a websocket client app or a standalone websocket server, it may be created with new MyModule(websocketModule.containerCallScope, websocketModule.websocketConnectionScope).
  • For a websocket server app embedded in a Servlet Container it may be either new MyModule(websocketModule.containerCallScope, websocketModule.websocketConnectionScope) or new MyModule(servletModule.containerCallScope, servletModule.httpSessionScope) depending whether it is desired to share state between websocket Endpoints and Servlets and whether enforcing of HttpSession creation for websocket connections is acceptable.
  • For a gRPC app, it may be new MyModule(grpcModule.listenerEventScope, grpcModule.rpcScope).