Skip to content

morgwai/grpc-scopes

Repository files navigation

gRPC Guice Scopes

RPC Scope and Listener-event Scope (a single message or a handler call) for gRPC client and server apps.
Copyright 2021 Piotr Morgwai Kotarbinski, Licensed under the Apache License, Version 2.0

latest release: 15.0 (javadoc)

See CHANGES for the summary of changes between releases. If the major version of a subsequent release remains unchanged, it is supposed to be backwards compatible in terms of API and behaviour with previous ones with the same major version (meaning that it should be safe to just blindly update in dependent projects and things should not break under normal circumstances).

OVERVIEW

Provides rpcScope and listenerEventScope Guice Scopes for both client and server apps.
Oversimplifying, in case of streaming inbound (streaming requests to servers and streaming responses to clients), listenerEventScope spans over the processing of a single message from the stream or over a single call to any registered handler (eg with setOnReadyHandler(...), setOnCancelHandler(...) etc), while rpcScope spans over a whole given RPC.
Oversimplifying again, in case of unary inbound, these 2 Scopes have roughly similar span (although if any handlers are registered, they will have a separate listenerEventScope).
See this DZone article for an extended high-level explanation and the javadocs of ListenerEventContext, ServerRpcContext, ClientRpcContext for technical details.

MAIN USER CLASSES

Contains the above Scopes and gRPC Interceptors that start the above Contexts.

An interface and a decorator for Executors that automatically transfer active Contexts when executing tasks.

Binds tasks and callbacks (Runnables, Callables, Consumers etc) to Contexts that were active at the time of a given binding. This can be used to transfer Contexts semi-automatically when switching Threads, for example when passing callbacks to async functions.

USAGE

  1. Create a GrpcModule instance and pass its listenerEventScope and rpcScope to other Modules as shortTermScope and longTermScope respectively (see DEVELOPING PORTABLE MODULES).
  2. Other Modules may use the passed Scopes in their bindings: bind(MyComponent.class).to(MyComponentImpl.class).in(longTermScope);
  3. All gRPC Services added to Servers must be intercepted with GrpcModule.serverInterceptor similarly to the following: .addService(ServerInterceptors.intercept(myService, grpcModule.serverInterceptor /* more interceptors here... */))
  4. All client Channels must be intercepted with GrpcModule.clientInterceptor or GrpcModule.nestingClientInterceptor similarly to the following: ClientInterceptors.intercept(channel, grpcModule.clientInterceptor)

Server sample

public class MyServer {

    final Server grpcServer;

    public MyServer(int port /* more params here... */) throws Exception {
        final var grpcModule = new GrpcModule();
        final var injector = Guice.createInjector(
            grpcModule,
            new ThirdPartyModule(grpcModule.listenerEventScope, grpcModule.rpcScope),
            (binder) -> {
                binder.bind(MyComponent.class)
                    .to(MyComponentImpl.class)
                    .in(grpcModule.rpcScope);
                // more bindings here
            }
            /* more modules here... */
        );

        final var myService = injector.getInstance(MyService.class);
                // myService will get Provider<MyComponent> injected
        // more services here...

        grpcServer = ServerBuilder
            .forPort(port)
            .addService(ServerInterceptors.intercept(
                    myService, grpcModule.serverInterceptor /* more interceptors here... */))
            // more services here...
            .build();

        Runtime.getRuntime().addShutdownHook(new Thread(() -> {/* shutdown code here... */}));
        grpcServer.start();
    }

    public static void main(String[] args) throws Exception {
        new MyServer(6666 /* more params here... */).grpcServer.awaitTermination();
    }

    // more code here...
}

Client sample

public class MyClient {

    public static void main(String[] args) throws Exception {
        final var managedChannel = ManagedChannelBuilder
            .forTarget(args[1])
            .usePlaintext()
            .build();
        EntityManagerFactory factoryToClose = null;
        try {
            final var entityManagerFactory = createEntityManagerFactory(args[0]);
            factoryToClose = entityManagerFactory;
            final var grpcModule = new GrpcModule();
            final var channel = ClientInterceptors.intercept(
                    managedChannel, grpcModule.nestingClientInterceptor);
            final var stub = MyServiceGrpc.newStub(channel);
    
            final Module myModule = (binder) -> {
                binder.bind(EntityManagerFactory.class)
                    .toInstance(entityManagerFactory);
                binder.bind(EntityManager.class)
                    .toProvider(entityManagerFactory::createEntityManager)
                    .in(grpcModule.listenerEventScope);
                binder.bind(MyDao.class)
                    .to(MyJpaDao.class)
                    .in(Scopes.SINGLETON);
                // more bindings here
            };
            // more modules here

            final var injector = Guice.createInjector(
                    grpcModule, myModule /* more modules here... */);
            final var myResponseObserver = injector.getInstance(MyResponseObserver.class);
                    // myResponseObserver will get MyDao injected

            stub.myUnaryRequestStreamingResponseProcedure(args[2], myResponseObserver);
            myResponseObserver.awaitCompletion(5, MINUTES);
        } finally {
            managedChannel.shutdown().awaitTermination(5, SECONDS);
            if ( !managedChannel.isTerminated()) {
                System.err.println("channel has NOT shutdown cleanly");
                managedChannel.shutdownNow();
            }
            if (factoryToClose != null) factoryToClose.close();
        }
    }

    // more code here...
}

Transferring Contexts to callbacks with ContextBinder

class MyComponent {

    @Inject ContextBinder ctxBinder;

    void methodThatCallsSomeAsyncMethod(/* ... */) {
        // other code here...
        someAsyncMethod(arg1, /* ... */ argN, ctxBinder.bindToContext((callbackParam) -> {
            // callback code here...
        }));
    }
}

Dependency management

Dependencies of this jar on guice and grpc are declared as optional, so that apps can use any versions of these deps with a compatible API.

EXAMPLES

See sample app