io.soliton.protobuf.EnvelopeServerHandler.java Source code

Java tutorial

Introduction

Here is the source code for io.soliton.protobuf.EnvelopeServerHandler.java

Source

/**
 * Copyright 2013 Julien Silland
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package io.soliton.protobuf;

import com.google.common.annotations.VisibleForTesting;
import com.google.common.base.Preconditions;
import com.google.common.base.Throwables;
import com.google.common.collect.MapMaker;
import com.google.common.util.concurrent.FutureCallback;
import com.google.common.util.concurrent.Futures;
import com.google.common.util.concurrent.ListenableFuture;
import com.google.protobuf.ByteString;
import com.google.protobuf.InvalidProtocolBufferException;
import com.google.protobuf.Message;
import io.netty.channel.Channel;
import io.netty.channel.ChannelFuture;
import io.netty.channel.ChannelHandlerContext;
import io.netty.channel.SimpleChannelInboundHandler;
import io.netty.util.concurrent.GenericFutureListener;

import java.util.Map;
import java.util.concurrent.ConcurrentMap;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.logging.Level;
import java.util.logging.Logger;

/**
 * Shared server-side handler implementation for servers whose method calls
 * are encoded using an {@link Envelope} message.
 *
 * @param <I> The incoming request type in which the RPC envelope is encoded
 * @param <O> The outgoing response type in which the RPC envelope will be encoded
 * @author Julien Silland (julien@soliton.io)
 */
public abstract class EnvelopeServerHandler<I, O> extends SimpleChannelInboundHandler<I> {

    public static final Logger logger = Logger.getLogger(EnvelopeServerHandler.class.getCanonicalName());

    private final ConcurrentMap<Long, ListenableFuture<?>> pendingRequests = new MapMaker().makeMap();
    private final ExecutorService responseCallbackExecutor = Executors.newCachedThreadPool();

    private final ServiceGroup services;
    private final ServerLogger serverLogger;

    public EnvelopeServerHandler(ServiceGroup services, ServerLogger serverLogger) {
        this.services = Preconditions.checkNotNull(services);
        this.serverLogger = Preconditions.checkNotNull(serverLogger);
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public boolean isSharable() {
        return true;
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public void channelRead0(ChannelHandlerContext context, I request) throws Exception {
        if (!accept(request)) {
            context.fireChannelRead(request);
            return;
        }

        Envelope envelope = null;
        try {
            envelope = convertRequest(request);
        } catch (RequestConversionException rce) {
            serverLogger.logClientError(rce);
            throw rce;
        }

        if (envelope.hasControl() && envelope.getControl().getCancel()) {
            ListenableFuture<?> pending = pendingRequests.remove(envelope.getRequestId());
            if (pending != null) {
                boolean cancelled = pending.cancel(true);
                context.channel().writeAndFlush(Envelope.newBuilder().setRequestId(envelope.getRequestId())
                        .setControl(Control.newBuilder().setCancel(cancelled)).build());
            }
            return;
        }

        Service service = services.lookupByName(envelope.getService());
        if (service == null) {
            serverLogger.logUnknownService(service);
            logger.warning(String.format("Received request for unknown service %s", envelope.getService()));
            context.channel()
                    .writeAndFlush(
                            Envelope.newBuilder().setRequestId(envelope.getRequestId())
                                    .setControl(Control.newBuilder()
                                            .setError(String.format("Unknown service %s", envelope.getService())))
                                    .build());
            return;
        }

        ServerMethod<? extends Message, ? extends Message> method = service.lookup(envelope.getMethod());
        if (method == null) {
            serverLogger.logUnknownMethod(service, envelope.getMethod());
            logger.warning(String.format("Received request for unknown method %s/%s", envelope.getService(),
                    envelope.getMethod()));
            context.channel().writeAndFlush(Envelope.newBuilder().setRequestId(envelope.getRequestId())
                    .setControl(Control.newBuilder().setError(
                            String.format("Unknown method %s/%s", envelope.getService(), envelope.getMethod())))
                    .build());
            return;
        }
        serverLogger.logMethodCall(service, method);
        invoke(method, envelope.getPayload(), envelope.getRequestId(), context.channel());
    }

    /**
     * Should be overridden by subclasses to refuse the processing of a given
     * request.
     *
     * This implementation return {@code true} in all cases.
     *
     * @param request the received request
     * @return {@code true} if the request should be processed, {@code false}
     * otherwise.
     */
    protected boolean accept(I request) {
        return true;
    }

    /**
     * Performs a single method invocation.
     *
     * @param method the method to invoke
     * @param payload the serialized parameter received from the client
     * @param requestId the unique identifier of the request
     * @param channel the channel to use for responding to the client
     * @param <I> the type of the method's parameter
     * @param <O> the return type of the method
     */
    private <I extends Message, O extends Message> void invoke(ServerMethod<I, O> method, ByteString payload,
            long requestId, Channel channel) {
        FutureCallback<O> callback = new ServerMethodCallback<>(method, requestId, channel);
        try {
            I request = method.inputParser().parseFrom(payload);
            ListenableFuture<O> result = method.invoke(request);
            pendingRequests.put(requestId, result);
            Futures.addCallback(result, callback, responseCallbackExecutor);
        } catch (InvalidProtocolBufferException ipbe) {
            callback.onFailure(ipbe);
        }
    }

    @VisibleForTesting
    public Map<Long, ListenableFuture<?>> pendingRequests() {
        return pendingRequests;
    }

    /**
     * Implemented by subclasses to convert the incoming request into an
     * {@link Envelope}
     *
     * @param request the incoming request
     */
    protected abstract Envelope convertRequest(I request) throws RequestConversionException;

    /**
     * Implemented by subclasses to convert an outgoing response into their
     * specific output type
     *
     * @param response the response to be converted into the output type
     */
    protected abstract O convertResponse(Envelope response);

    /**
     * Encapsulates the logic to execute when the invocation of a service
     * method is done.
     *
     * @param <M> the method's return type
     */
    private class ServerMethodCallback<M extends Message> implements FutureCallback<M> {

        private final ServerMethod<?, M> serverMethod;
        private final long requestId;
        private final Channel channel;

        private ServerMethodCallback(ServerMethod<?, M> serverMethod, long requestId, Channel channel) {
            this.serverMethod = serverMethod;
            this.requestId = requestId;
            this.channel = channel;
        }

        /**
         * {@inheritDoc}
         */
        @Override
        public void onSuccess(M result) {
            serverLogger.logServerSuccess(serverMethod);
            pendingRequests.remove(requestId);
            Envelope response = Envelope.newBuilder().setPayload(result.toByteString()).setRequestId(requestId)
                    .build();

            channel.writeAndFlush(convertResponse(response))
                    .addListener(new GenericFutureListener<ChannelFuture>() {

                        public void operationComplete(ChannelFuture future) {
                            if (!future.isSuccess()) {
                                serverLogger.logLinkFailure(serverMethod, future.cause());
                                logger.log(Level.WARNING, String.format("Failed to respond to client on %s ",
                                        channel.remoteAddress().toString()), future.cause());
                            }
                        }

                    });
        }

        /**
         * {@inheritDoc}
         */
        @Override
        public void onFailure(Throwable throwable) {
            logger.info("Responding to client with failure");
            serverLogger.logServerFailure(serverMethod, throwable);
            pendingRequests.remove(requestId);
            Control control = Control.newBuilder().setError(Throwables.getStackTraceAsString(throwable)).build();
            Envelope response = Envelope.newBuilder().setControl(control).build();
            channel.writeAndFlush(convertResponse(response))
                    .addListener(new GenericFutureListener<ChannelFuture>() {

                        public void operationComplete(ChannelFuture future) {
                            if (!future.isSuccess()) {
                                serverLogger.logLinkFailure(serverMethod, future.cause());
                                logger.warning(String.format("Failed to respond to client on %s ",
                                        channel.remoteAddress().toString()));
                            }
                        }

                    });
        }

    }

    /**
     * Occurs when an incoming request couldn't be converted into an envelope.
     */
    protected static class RequestConversionException extends Exception {
        private final Object request;

        /**
         * Exhaustive constructor.
         *
         * @param request the request that couldn't be converted
         */
        public RequestConversionException(Object request) {
            this.request = request;
        }

        /**
         * Exhaustive constructor.
         *
         * @param request the request that couldn't be converted
         * @param exception the underlying exception
         */
        public RequestConversionException(Object request, Throwable exception) {
            super(exception);
            this.request = request;
        }

        @Override
        public String getMessage() {
            return String.format("Could not convert incoming request: %s", request);
        }
    }
}