/**
   * Sends an RPC request to the given {@link InetSocketAddress}.
   *
   * @param remoteSocketAddress the remote address to send the request to
   * @param request the RPC request to send
   * @return the return value of the RPC call, possibly <code>null</code>
   * @throws Throwable any exception that is thrown by the remote receiver of the RPC call
   */
  Object sendRPCRequest(final InetSocketAddress remoteSocketAddress, final RPCRequest request)
      throws Throwable {

    if (this.shutdownRequested.get())
      throw new IOException("Shutdown of RPC service has already been requested");

    final long start = System.currentTimeMillis();
    final DatagramPacket[] packets = this.messageToPackets(remoteSocketAddress, request);
    final Integer messageID = Integer.valueOf(request.getMessageID());

    final RPCRequestMonitor requestMonitor = new RPCRequestMonitor();

    this.pendingRequests.put(messageID, requestMonitor);

    RPCResponse rpcResponse = null;
    int numberOfRetries;
    try {

      numberOfRetries = this.networkThread.send(packets);

      // Wait for the response
      synchronized (requestMonitor) {
        while (true) {

          if (requestMonitor.rpcResponse != null) {
            rpcResponse = requestMonitor.rpcResponse;
            break;
          }

          final long sleepTime = RPC_TIMEOUT - (System.currentTimeMillis() - start);
          if (sleepTime > 0L) requestMonitor.wait(sleepTime);
          else break;
        }
      }

    } finally {
      // Request is no longer pending
      this.pendingRequests.remove(messageID);
    }

    if (rpcResponse == null)
      throw new IOException(
          "Unable to complete RPC of method "
              + request.getMethodName()
              + " on "
              + remoteSocketAddress);

    // Report the successful call to the statistics module
    final String methodName = request.getMethodName();
    this.statistics.reportSuccessfulTransmission(methodName, packets.length, numberOfRetries);
    this.statistics.reportRTT(methodName, (int) (System.currentTimeMillis() - start));

    // TODO: Send clean up message

    if (rpcResponse instanceof RPCReturnValue) return ((RPCReturnValue) rpcResponse).getRetVal();
    throw ((RPCThrowable) rpcResponse).getThrowable();
  }
  private void processIncomingRPCRequest(
      final InetSocketAddress remoteSocketAddress, final RPCRequest rpcRequest) {

    final Integer messageID = Integer.valueOf(rpcRequest.getMessageID());

    if (this.requestsBeingProcessed.putIfAbsent(messageID, rpcRequest) != null) {
      Log.debug(
          "Request " + rpcRequest.getMessageID() + " is already being processed at the moment");
      return;
    }

    final CachedResponse cachedResponse = this.cachedResponses.get(messageID);
    if (cachedResponse != null) {
      try {
        final int numberOfRetries = this.networkThread.send(cachedResponse.packets);
        this.statistics.reportSuccessfulTransmission(
            rpcRequest.getMethodName() + " (Response)",
            cachedResponse.packets.length,
            numberOfRetries);
      } catch (final Exception e) {
        Log.error("Caught exception while trying to send RPC response: ", e);
      } finally {
        this.requestsBeingProcessed.remove(messageID);
      }
      return;
    }

    final RPCProtocol callbackHandler = this.callbackHandlers.get(rpcRequest.getInterfaceName());
    if (callbackHandler == null) {
      Log.error("Cannot find callback handler for protocol " + rpcRequest.getInterfaceName());
      this.requestsBeingProcessed.remove(messageID);
      return;
    }

    try {
      final Method method =
          callbackHandler
              .getClass()
              .getMethod(rpcRequest.getMethodName(), rpcRequest.getParameterTypes());

      RPCResponse rpcResponse = null;
      try {
        final Object retVal = method.invoke(callbackHandler, rpcRequest.getArgs());
        rpcResponse = new RPCReturnValue(rpcRequest.getMessageID(), retVal);
      } catch (final InvocationTargetException ite) {

        Throwable targetException = ite.getTargetException();

        // Make sure the stack trace is correctly filled
        targetException.getStackTrace();

        if (!this.isThrowableRegistered(targetException.getClass()))
          targetException = wrapInIOException(rpcRequest, targetException);

        rpcResponse = new RPCThrowable(rpcRequest.getMessageID(), targetException);
      }
      final DatagramPacket[] packets = this.messageToPackets(remoteSocketAddress, rpcResponse);
      this.cachedResponses.put(messageID, new CachedResponse(System.currentTimeMillis(), packets));

      final int numberOfRetries = this.networkThread.send(packets);
      this.statistics.reportSuccessfulTransmission(
          rpcRequest.getMethodName() + " (Response)", packets.length, numberOfRetries);

    } catch (final Exception e) {
      Log.error("Caught processing RPC request: ", e);
    } finally {
      this.requestsBeingProcessed.remove(messageID);
    }
  }