dubbo source code analysis 1: rpc asynchronous to synchronous

Posted by barrowvian on Tue, 25 Jan 2022 06:33:15 +0100

summary

I haven't seen you for a long time. How are you recently? The project that has been in a hurry for a month has finally been completed. You can learn something in your spare time. Today, let's take a look at dubbo's asynchronous to synchronous.

The reason for this is that there is a need for asynchronous to synchronous conversion in recent projects, so we learn from the implementation of dubbo. Let's take a look at how dubbo does it today.

Source code analysis

There are many kinds of dubbo remote rpc protocols and network frameworks. We take the default dubbo protocol and network framework netty as the starting point for analysis. The package structure is shown in the following figure:

The DubboInvoker class is very important because the client does not have a specific implementation, but the calling logic is implemented through the agent, and this class is the final worker. Its internal core methods are as follows:

    protected Result doInvoke(final Invocation invocation) throws Throwable {        RpcInvocation inv = (RpcInvocation) invocation;        final String methodName = RpcUtils.getMethodName(invocation);        inv.setAttachment(PATH_KEY, getUrl().getPath());        inv.setAttachment(VERSION_KEY, version);
        ExchangeClient currentClient;        if (clients.length == 1) {            currentClient = clients[0];        } else {            currentClient = clients[index.getAndIncrement() % clients.length];        }        try {            boolean isOneway = RpcUtils.isOneway(getUrl(), invocation);            int timeout = calculateTimeout(invocation, methodName);            invocation.put(TIMEOUT_KEY, timeout);            if (isOneway) {                boolean isSent = getUrl().getMethodParameter(methodName, Constants.SENT_KEY, false);                currentClient.send(inv, isSent);                return AsyncRpcResult.newDefaultAsyncResult(invocation);            } else {                ExecutorService executor = getCallbackExecutor(getUrl(), inv);                CompletableFuture<AppResponse> appResponseFuture =                        //todo , this is the place where the request is really initiated. You can interrupt the point by yourself. The remote rpc call finally comes to the currentclient request(inv, timeout, executor). thenApply(obj -> (AppResponse) obj);                //  save for 2.6. x compatibility, for example, TraceFilter in Zipkin uses com. alibaba. xxx. FutureAdapter                FutureContext. getContext(). setCompatibleFuture(appResponseFuture);                 AsyncRpcResult result = new AsyncRpcResult(appResponseFuture, inv);                 result. setExecutor(executor);                 return result;            }        }  catch (TimeoutException e) {            throw new RpcException(RpcException.TIMEOUT_EXCEPTION, "Invoke remote method timeout. method: " + invocation.getMethodName() + ", provider: " + getUrl() + ", cause: " + e.getMessage(), e);        }  catch (RemotingException e) {            throw new RpcException(RpcException.NETWORK_EXCEPTION, "Failed to invoke remote method: " + invocation.getMethodName() + ", provider: " + getUrl() + ", cause: " + e.getMessage(), e);        }    }

Through the breakpoint, we can find the following information that is finally by the ExchangeChannel interface:

    CompletableFuture<Object> request(Object request, int timeout, ExecutorService executor) throws RemotingException;

rpc request initiated by api

This interface returns completable Future, which is java 1 A class introduced in 8 provides a very powerful extension function of Future, which can help us simplify the complexity of asynchronous programming. It is found that it is finally made by the implementation class HeaderExchangeChannel. The source code is as follows:

    @Override    public CompletableFuture<Object> request(Object request, int timeout, ExecutorService executor) throws RemotingException {        if (closed) {            throw new RemotingException(this.getLocalAddress(), null, "Failed to send request " + request + ", cause: The channel " + this + " is closed!");        }        Request req = new Request();        req.setVersion(Version.getProtocolVersion());        req.setTwoWay(true);        req.setData(request);        //That's the point. Create a Future through defaultfuture and return it. DefaultFuture future = DefaultFuture.newFuture(channel, req, timeout, executor);        try {            channel.send(req);        } catch (RemotingException e) {            future.cancel();            throw e;        }        return future;    }

This is divided into 2 steps: first, create a DefaultFuture through DefaultFuture, then call the send method to send the message, and then return DefaultFuture.

Let's take a look at the send method, which will eventually call the NettyChannel class, which is relatively simple. The message sent by the native netty is written as follows:

    public void send(Object message, boolean sent) throws RemotingException {        // whether the channel is closed        super.send(message, sent);
        boolean success = true;        int timeout = 0;        try {            ChannelFuture future = channel.writeAndFlush(message);            if (sent) {                // wait timeout ms                timeout = getUrl().getPositiveParameter(TIMEOUT_KEY, DEFAULT_TIMEOUT);                success = future.await(timeout);            }            Throwable cause = future.cause();            if (cause != null) {                throw cause;            }        } catch (Throwable e) {            removeChannelIfDisconnected(channel);            throw new RemotingException(this, "Failed to send message " + PayloadDropper.getRequestWithoutData(message) + " to " + getRemoteAddress() + ", cause: " + e.getMessage(), e);        }        if (!success) {            throw new RemotingException(this, "Failed to send message " + PayloadDropper.getRequestWithoutData(message) + " to " + getRemoteAddress()                    + "in timeout(" + timeout + "ms) limit");        }    }

OK, let's take a look at the operations of the DefaultFuture class.

    private static final Map<Long, Channel> CHANNELS = new ConcurrentHashMap<>();
    private static final Map<Long, DefaultFuture> FUTURES = new ConcurrentHashMap<>();        //The key point is to realize the asynchronous to synchronous operation through the secondary method. Private void doreceived (response RES) {if (RES = = null) {throw new IllegalStateException ("response cannot be null");} if (res.getStatus() == Response. OK) {            this.complete(res.getResult());        }  else if (res.getStatus() == Response. CLIENT_ TIMEOUT || res.getStatus() == Response. SERVER_ TIMEOUT) {            this.completeExceptionally(new TimeoutException(res.getStatus() == Response.SERVER_TIMEOUT, channel, res.getErrorMessage()));        }  else {            this.completeExceptionally(new RemotingException(channel, res.getErrorMessage()));        }
        // the result is returning, but the caller thread may still waiting        // to avoid endless waiting for whatever reason, notify caller thread to return.        if (executor != null && executor instanceof ThreadlessExecutor) {            ThreadlessExecutor threadlessExecutor = (ThreadlessExecutor) executor;            if (threadlessExecutor.isWaiting()) {                threadlessExecutor.notifyReturn(new IllegalStateException("The result has returned, but the biz thread is still waiting" +                        " which is not an expected state, interrupt the thread manually by returning an exception."));            }        }    }        private static class TimeoutCheckTask implements TimerTask {
        private final Long requestID;
        TimeoutCheckTask(Long requestID) {            this.requestID = requestID;        }
        @Override        public void run(Timeout timeout) {            DefaultFuture future = DefaultFuture.getFuture(requestID);            if (future == null || future.isDone()) {                return;            }
            if (future.getExecutor() != null) {                future.getExecutor().execute(() -> notifyTimeout(future));            } else {                notifyTimeout(future);            }        }
        private void notifyTimeout(DefaultFuture future) {            // create exception response.            Response timeoutResponse = new Response(future.getId());            // set timeout status.            timeoutResponse.setStatus(future.isSent() ? Response.SERVER_TIMEOUT : Response.CLIENT_TIMEOUT);            timeoutResponse.setErrorMessage(future.getTimeoutMessage(true));            // handle response.            DefaultFuture.received(future.getChannel(), timeoutResponse, true);        }    }

The source code here only shows the key code. The two variables CHANNELS and futures are used to save the current link and calling thread.

The doReceived method is to receive the return from the server and set the return information to the CompletableFuture result of the caller. The calling chain is as follows:

NettyClientHandler---->

                channelRead-->

HeaderExchangeHandler--->

                 received---->

                 handleResponse----->

DefaultFuture---->

                 received---->

                 doReceived

The source code involved is as follows:

    NettyClientHandler      @Override      public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {          NettyChannel channel = NettyChannel.getOrAddChannel(ctx.channel(), url, handler);          handler.received(channel, msg);      }   HeaderExchangeHandler          @Override          public void received(Channel channel, Object message) throws RemotingException {              final ExchangeChannel exchangeChannel = HeaderExchangeChannel.getOrAddChannel(channel);              if (message instanceof Request) {                  // handle request.                  Request request = (Request) message;                  if (request.isEvent()) {                      handlerEvent(channel, request);                  } else {                      if (request.isTwoWay()) {                          handleRequest(exchangeChannel, request);                      } else {                          handler.received(exchangeChannel, request.getData());                      }                  }              } else if (message instanceof Response) {                  handleResponse(channel, (Response) message);              } else if (message instanceof String) {                  if (isClientSide(channel)) {                      Exception e = new Exception("Dubbo client can not supported string message: " + message + " in channel: " + channel + ", url: " + channel.getUrl());                      logger.error(e.getMessage(), e);                  } else {                      String echo = handler.telnet(channel, (String) message);                      if (echo != null && echo.length() > 0) {                          channel.send(echo);                      }                  }              } else {                  handler.received(exchangeChannel, message);              }          }       static void handleResponse(Channel channel, Response response) throws RemotingException {            if (response != null && !response.isHeartbeat()) {                DefaultFuture.received(channel, response);            }        } DefaultFuture        public static void received(Channel channel, Response response) {            received(channel, response, false);        }            public static void received(Channel channel, Response response, boolean timeout) {            try {                DefaultFuture future = FUTURES.remove(response.getId());                if (future != null) {                    Timeout t = future.timeoutCheckTask;                    if (!timeout) {                        // decrease Time                        t.cancel();                    }                    future.doReceived(response);                } else {                    logger.warn("The timeout response finally returned at "                            + (new SimpleDateFormat("yyyy-MM-dd HH:mm:ss.SSS").format(new Date()))                            + ", response status is " + response.getStatus()                            + (channel == null ? "" : ", channel: " + channel.getLocalAddress()                            + " -> " + channel.getRemoteAddress()) + ", please check provider side for detailed result.");                }            } finally {                CHANNELS.remove(response.getId());            }        }

We can also see that DefaultFuture does timeout processing. If there is no response within a certain time, set the timeout and return the implementation method. The internal class of DefaultFuture:

                                        TimeoutCheckTask----->notifyTimeout

summary

So far, the core logic of dubbo asynchronous to synchronization has been sorted out. Its core class is DefaultFutrue. Concurrent HashMap is used to record id and corresponding defaultFuture object, and completable future is used to achieve the effect of synchronous call.

We use dubbo's idea for reference, but our interaction adopts a short connection, that is, the link is re created each time (the short connection is set because the server transmits unstable signals through 4G IOT network card and the power is limited), so the link information cannot be saved locally.

Therefore, we use redis to save the current transaction pipeline. Once the server returns, it will save the processing results of this pipeline to redis. The current process takes turns to train redis to obtain the results and start the asynchronous thread. If it exceeds a fixed time, it will return after timeout.

By reading the source code and solving their problems in the actual development, this mood is very good. At this point, it is more convinced that the action of reading the source code is correct. dubbo is the essence everywhere. There are many places worth our reference. This year, we set up a flag complete reading dubbo source code for ourselves and insist on at least 2 monthly dubbo source analysis.

Well, that's all for today. I'll see you next time!

MYSQL Series classic articles

 

Topics: Java Zookeeper rpc