Class DispatchingOnReadyHandler<ResponseT>

  • All Implemented Interfaces:
    Runnable

    public class DispatchingOnReadyHandler<ResponseT>
    extends Object
    implements Runnable
    Handles streaming of messages to a CallStreamObserver from multiple threads with respect to flow-control to ensure that no excessive buffering occurs.

    Setting an instance using setOnReadyHandler(dispatchingOnReadyHandler) will eventually have similar effects as the below pseudo-code:

    for (int i = 0; i < numberOfTasks; i++) taskExecutor.execute(() -> {
        try {
            while ( ! completionIndicator.apply(i))
                streamObserver.onNext(messageProducer.apply(i));
            if (allTasksCompleted()) streamObserver.onCompleted();
        } catch (Throwable t) {
            var toReport = handleException(taskNumber, throwable);
            if (toReport != null) streamObserver.onError(toReport);
        } finally {
            cleanupHandler.accept(i);
        }
    });
     
    However, calls to streamObserver are properly synchronized and the work is automatically suspended/resumed whenever streamObserver becomes unready/ready and executor's threads are released during time when observer is unready.

    Typical usage:

    public void myServerStreamingMethod(
            RequestMessage request, StreamObserver<ResponseMessage> basicResponseObserver) {
        var state = new MyCallState(request, NUMBER_OF_TASKS);
        var responseObserver =
                (ServerCallStreamObserver<ResponseMessage>) basicResponseObserver;
        responseObserver.setOnCancelHandler(() -> log.fine("client cancelled"));
        final var handler = new DispatchingServerStreamingCallHandler<>(
            responseObserver,
            taskExecutor,
            NUMBER_OF_TASKS,
            (i) -> state.isCompleted(i),
            (i) -> state.produceNextResponseMessage(i),
            (i, error) -> {
                state.fail(error);  // interrupt other tasks
                if (error instanceof StatusRuntimeException) return null;
                return Status.INTERNAL.asException();
            },
            (i) -> state.cleanup(i)
        );
        responseObserver.setOnReadyHandler(handler);
    }
     

    NOTE: this class is not suitable for cases where executor's thread should not be released, such as JDBC/JPA processing where executor threads correspond to pooled connections that must be retained in order not to lose given DB transaction/cursor. In such cases processing should be implemented similar as the below code:

    public void myServerStreamingMethod(
            RequestMessage request, StreamObserver<ResponseMessage> basicResponseObserver) {
        responseObserver.setOnReadyHandler(() -> {
            synchronized (responseObserver) {
                responseObserver.notify();
            }
        });
        jdbcExecutor.execute(() -> {
            try {
                var state = new MyCallState(request);
                while ( ! state.isCompleted()) {
                    synchronized (responseObserver) {
                        while ( ! responseObserver.isReady()) responseObserver.wait();
                    }
                    responseObserver.onNext(state.produceNextResponseMessage());
                }
                responseObserver.onCompleted();
            } catch (Throwable t) {
                if ( ! (t instanceof StatusRuntimeException)) responseObserver.onError(t);
            } finally {
                state.cleanup();
            }
        });
    }