/**
 * Licensed to the Apache Software Foundation (ASF) under one
 * or more contributor license agreements.  See the NOTICE file
 * distributed with this work for additional information
 * regarding copyright ownership.  The ASF licenses this file
 * to you 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 org.apache.hadoop.hbase.ipc;

import io.netty.bootstrap.Bootstrap;
import io.netty.buffer.ByteBuf;
import io.netty.buffer.ByteBufOutputStream;
import io.netty.channel.Channel;
import io.netty.channel.ChannelFuture;
import io.netty.channel.ChannelFutureListener;
import io.netty.handler.codec.LengthFieldBasedFrameDecoder;
import io.netty.util.Timeout;
import io.netty.util.TimerTask;
import io.netty.util.concurrent.GenericFutureListener;
import io.netty.util.concurrent.Promise;

import java.io.IOException;
import java.net.ConnectException;
import java.net.InetSocketAddress;
import java.net.SocketException;
import java.nio.ByteBuffer;
import java.security.PrivilegedExceptionAction;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Random;
import java.util.concurrent.TimeUnit;

import javax.security.sasl.SaslException;

import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.apache.hadoop.hbase.HConstants;
import org.apache.hadoop.hbase.classification.InterfaceAudience;
import org.apache.hadoop.hbase.exceptions.ConnectionClosingException;
import org.apache.hadoop.hbase.protobuf.ProtobufUtil;
import org.apache.hadoop.hbase.protobuf.generated.AuthenticationProtos;
import org.apache.hadoop.hbase.protobuf.generated.RPCProtos;
import org.apache.hadoop.hbase.protobuf.generated.TracingProtos;
import org.apache.hadoop.hbase.security.AuthMethod;
import org.apache.hadoop.hbase.security.SaslClientHandler;
import org.apache.hadoop.hbase.security.SaslUtil;
import org.apache.hadoop.hbase.security.SecurityInfo;
import org.apache.hadoop.hbase.security.User;
import org.apache.hadoop.hbase.security.token.AuthenticationTokenSelector;
import org.apache.hadoop.hbase.util.EnvironmentEdgeManager;
import org.apache.hadoop.io.Text;
import org.apache.hadoop.ipc.RemoteException;
import org.apache.hadoop.security.SecurityUtil;
import org.apache.hadoop.security.UserGroupInformation;
import org.apache.hadoop.security.token.Token;
import org.apache.hadoop.security.token.TokenIdentifier;
import org.apache.hadoop.security.token.TokenSelector;
import org.apache.htrace.Span;
import org.apache.htrace.Trace;

import com.google.protobuf.Descriptors;
import com.google.protobuf.Message;
import com.google.protobuf.RpcCallback;

/**
 * Netty RPC channel
 */
@InterfaceAudience.Private
public class AsyncRpcChannel {
  public static final Log LOG = LogFactory.getLog(AsyncRpcChannel.class.getName());

  private static final int MAX_SASL_RETRIES = 5;

  protected final static Map<AuthenticationProtos.TokenIdentifier.Kind, TokenSelector<? extends
      TokenIdentifier>> tokenHandlers = new HashMap<>();

  static {
    tokenHandlers.put(AuthenticationProtos.TokenIdentifier.Kind.HBASE_AUTH_TOKEN,
        new AuthenticationTokenSelector());
  }

  final AsyncRpcClient client;

  // Contains the channel to work with.
  // Only exists when connected
  private Channel channel;

  String name;
  final User ticket;
  final String serviceName;
  final InetSocketAddress address;

  private int ioFailureCounter = 0;
  private int connectFailureCounter = 0;

  boolean useSasl;
  AuthMethod authMethod;
  private int reloginMaxBackoff;
  private Token<? extends TokenIdentifier> token;
  private String serverPrincipal;


  // NOTE: closed and connected flags below are only changed when a lock on pendingCalls
  private final Map<Integer, AsyncCall> pendingCalls = new HashMap<Integer, AsyncCall>();
  private boolean connected = false;
  private boolean closed = false;

  private Timeout cleanupTimer;

  private final TimerTask timeoutTask = new TimerTask() {
    @Override
    public void run(Timeout timeout) throws Exception {
      cleanupCalls();
    }
  };

  /**
   * Constructor for netty RPC channel
   *
   * @param bootstrap to construct channel on
   * @param client    to connect with
   * @param ticket of user which uses connection
   *               @param serviceName name of service to connect to
   * @param address to connect to
   */
  public AsyncRpcChannel(Bootstrap bootstrap, final AsyncRpcClient client, User ticket, String
      serviceName, InetSocketAddress address) {
    this.client = client;

    this.ticket = ticket;
    this.serviceName = serviceName;
    this.address = address;

    this.channel = connect(bootstrap).channel();

    name = ("IPC Client (" + channel.hashCode() + ") connection to " +
        address.toString() +
        ((ticket == null) ?
            " from an unknown user" :
            (" from " + ticket.getName())));
  }

  /**
   * Connect to channel
   *
   * @param bootstrap to connect to
   * @return future of connection
   */
  private ChannelFuture connect(final Bootstrap bootstrap) {
    return bootstrap.remoteAddress(address).connect()
        .addListener(new GenericFutureListener<ChannelFuture>() {
          @Override
          public void operationComplete(final ChannelFuture f) throws Exception {
            if (!f.isSuccess()) {
              if (f.cause() instanceof SocketException) {
                retryOrClose(bootstrap, connectFailureCounter++, f.cause());
              } else {
                retryOrClose(bootstrap, ioFailureCounter++, f.cause());
              }
              return;
            }
            channel = f.channel();

            setupAuthorization();

            ByteBuf b = channel.alloc().directBuffer(6);
            createPreamble(b, authMethod);
            channel.writeAndFlush(b).addListener(ChannelFutureListener.CLOSE_ON_FAILURE);
            if (useSasl) {
              UserGroupInformation ticket = AsyncRpcChannel.this.ticket.getUGI();
              if (authMethod == AuthMethod.KERBEROS) {
                if (ticket != null && ticket.getRealUser() != null) {
                  ticket = ticket.getRealUser();
                }
              }
              SaslClientHandler saslHandler;
              if (ticket == null) {
                throw new FatalConnectionException("ticket/user is null");
              }
              final UserGroupInformation realTicket = ticket;
              saslHandler = ticket.doAs(new PrivilegedExceptionAction<SaslClientHandler>() {
                @Override
                public SaslClientHandler run() throws IOException {
                  return getSaslHandler(realTicket, bootstrap);
                }
              });
              if (saslHandler != null) {
                // Sasl connect is successful. Let's set up Sasl channel handler
                channel.pipeline().addFirst(saslHandler);
              } else {
                // fall back to simple auth because server told us so.
                authMethod = AuthMethod.SIMPLE;
                useSasl = false;
              }
            } else {
              startHBaseConnection(f.channel());
            }
          }
        });
  }

  /**
   * Start HBase connection
   *
   * @param ch channel to start connection on
   */
  private void startHBaseConnection(Channel ch) {
    ch.pipeline()
        .addLast("frameDecoder", new LengthFieldBasedFrameDecoder(Integer.MAX_VALUE, 0, 4, 0, 4));
    ch.pipeline().addLast(new AsyncServerResponseHandler(this));
    try {
      writeChannelHeader(ch).addListener(new GenericFutureListener<ChannelFuture>() {
        @Override
        public void operationComplete(ChannelFuture future) throws Exception {
          if (!future.isSuccess()) {
            close(future.cause());
            return;
          }
          List<AsyncCall> callsToWrite;
          synchronized (pendingCalls) {
            connected = true;
            callsToWrite = new ArrayList<AsyncCall>(pendingCalls.values());
          }
          for (AsyncCall call : callsToWrite) {
            writeRequest(call);
          }
        }
      });
    } catch (IOException e) {
      close(e);
    }
  }

  /**
   * Get SASL handler
   * @param bootstrap to reconnect to
   * @return new SASL handler
   * @throws java.io.IOException if handler failed to create
   */
  private SaslClientHandler getSaslHandler(final UserGroupInformation realTicket,
      final Bootstrap bootstrap) throws IOException {
    return new SaslClientHandler(realTicket, authMethod, token, serverPrincipal,
        client.fallbackAllowed, client.conf.get("hbase.rpc.protection",
          SaslUtil.QualityOfProtection.AUTHENTICATION.name().toLowerCase()),
        new SaslClientHandler.SaslExceptionHandler() {
          @Override
          public void handle(int retryCount, Random random, Throwable cause) {
            try {
              // Handle Sasl failure. Try to potentially get new credentials
              handleSaslConnectionFailure(retryCount, cause, realTicket);

              // Try to reconnect
              client.newTimeout(new TimerTask() {
                @Override
                public void run(Timeout timeout) throws Exception {
                  connect(bootstrap);
                }
              }, random.nextInt(reloginMaxBackoff) + 1, TimeUnit.MILLISECONDS);
            } catch (IOException | InterruptedException e) {
              close(e);
            }
          }
        }, new SaslClientHandler.SaslSuccessfulConnectHandler() {
          @Override
          public void onSuccess(Channel channel) {
            startHBaseConnection(channel);
          }
        });
  }

  /**
   * Retry to connect or close
   *
   * @param bootstrap      to connect with
   * @param connectCounter amount of tries
   * @param e              exception of fail
   */
  private void retryOrClose(final Bootstrap bootstrap, int connectCounter, Throwable e) {
    if (connectCounter < client.maxRetries) {
      client.newTimeout(new TimerTask() {
        @Override public void run(Timeout timeout) throws Exception {
          connect(bootstrap);
        }
      }, client.failureSleep, TimeUnit.MILLISECONDS);
    } else {
      client.failedServers.addToFailedServers(address);
      close(e);
    }
  }

  /**
   * Calls method on channel
   * @param method to call
   * @param controller to run call with
   * @param request to send
   * @param responsePrototype to construct response with
   */
  public Promise<Message> callMethod(final Descriptors.MethodDescriptor method,
      final PayloadCarryingRpcController controller, final Message request,
      final Message responsePrototype) {
    final AsyncCall call =
        new AsyncCall(channel.eventLoop(), client.callIdCnt.getAndIncrement(), method, request,
            controller, responsePrototype);
    controller.notifyOnCancel(new RpcCallback<Object>() {
      @Override
      public void run(Object parameter) {
        // TODO: do not need to call AsyncCall.setFailed?
        synchronized (pendingCalls) {
          pendingCalls.remove(call.id);
        }
      }
    });
    // TODO: this should be handled by PayloadCarryingRpcController.
    if (controller.isCanceled()) {
      // To finish if the call was cancelled before we set the notification (race condition)
      call.cancel(true);
      return call;
    }

    synchronized (pendingCalls) {
      if (closed) {
        Promise<Message> promise = channel.eventLoop().newPromise();
        promise.setFailure(new ConnectException());
        return promise;
      }
      pendingCalls.put(call.id, call);
      // Add timeout for cleanup if none is present
      if (cleanupTimer == null && call.getRpcTimeout() > 0) {
        cleanupTimer =
            client.newTimeout(timeoutTask, call.getRpcTimeout(),
              TimeUnit.MILLISECONDS);
      }
      if (!connected) {
        return call;
      }
    }
    writeRequest(call);
    return call;
  }

  AsyncCall removePendingCall(int id) {
    synchronized (pendingCalls) {
      return pendingCalls.remove(id);
    }
  }

  /**
   * Write the channel header
   *
   * @param channel to write to
   * @return future of write
   * @throws java.io.IOException on failure to write
   */
  private ChannelFuture writeChannelHeader(Channel channel) throws IOException {
    RPCProtos.ConnectionHeader.Builder headerBuilder =
        RPCProtos.ConnectionHeader.newBuilder().setServiceName(serviceName);

    RPCProtos.UserInformation userInfoPB = buildUserInfo(ticket.getUGI(), authMethod);
    if (userInfoPB != null) {
      headerBuilder.setUserInfo(userInfoPB);
    }

    if (client.codec != null) {
      headerBuilder.setCellBlockCodecClass(client.codec.getClass().getCanonicalName());
    }
    if (client.compressor != null) {
      headerBuilder.setCellBlockCompressorClass(client.compressor.getClass().getCanonicalName());
    }

    headerBuilder.setVersionInfo(ProtobufUtil.getVersionInfo());
    RPCProtos.ConnectionHeader header = headerBuilder.build();


    int totalSize = IPCUtil.getTotalSizeWhenWrittenDelimited(header);

    ByteBuf b = channel.alloc().directBuffer(totalSize);

    b.writeInt(header.getSerializedSize());
    b.writeBytes(header.toByteArray());

    return channel.writeAndFlush(b);
  }

  /**
   * Write request to channel
   *
   * @param call    to write
   */
  private void writeRequest(final AsyncCall call) {
    try {
      final RPCProtos.RequestHeader.Builder requestHeaderBuilder = RPCProtos.RequestHeader
          .newBuilder();
      requestHeaderBuilder.setCallId(call.id)
              .setMethodName(call.method.getName()).setRequestParam(call.param != null);

      if (Trace.isTracing()) {
        Span s = Trace.currentSpan();
        requestHeaderBuilder.setTraceInfo(TracingProtos.RPCTInfo.newBuilder().
            setParentId(s.getSpanId()).setTraceId(s.getTraceId()));
      }

      ByteBuffer cellBlock = client.buildCellBlock(call.controller.cellScanner());
      if (cellBlock != null) {
        final RPCProtos.CellBlockMeta.Builder cellBlockBuilder = RPCProtos.CellBlockMeta
            .newBuilder();
        cellBlockBuilder.setLength(cellBlock.limit());
        requestHeaderBuilder.setCellBlockMeta(cellBlockBuilder.build());
      }
      // Only pass priority if there one.  Let zero be same as no priority.
      if (call.controller.getPriority() != 0) {
        requestHeaderBuilder.setPriority(call.controller.getPriority());
      }

      RPCProtos.RequestHeader rh = requestHeaderBuilder.build();

      int totalSize = IPCUtil.getTotalSizeWhenWrittenDelimited(rh, call.param);
      if (cellBlock != null) {
        totalSize += cellBlock.remaining();
      }

      ByteBuf b = channel.alloc().directBuffer(4 + totalSize);
      try(ByteBufOutputStream out = new ByteBufOutputStream(b)) {
        IPCUtil.write(out, rh, call.param, cellBlock);
      }

      channel.writeAndFlush(b).addListener(new CallWriteListener(this, call.id));
    } catch (IOException e) {
      close(e);
    }
  }

  /**
   * Set up server authorization
   *
   * @throws java.io.IOException if auth setup failed
   */
  private void setupAuthorization() throws IOException {
    SecurityInfo securityInfo = SecurityInfo.getInfo(serviceName);
    this.useSasl = client.userProvider.isHBaseSecurityEnabled();

    this.token = null;
    if (useSasl && securityInfo != null) {
      AuthenticationProtos.TokenIdentifier.Kind tokenKind = securityInfo.getTokenKind();
      if (tokenKind != null) {
        TokenSelector<? extends TokenIdentifier> tokenSelector = tokenHandlers.get(tokenKind);
        if (tokenSelector != null) {
          token = tokenSelector
              .selectToken(new Text(client.clusterId), ticket.getUGI().getTokens());
        } else if (LOG.isDebugEnabled()) {
          LOG.debug("No token selector found for type " + tokenKind);
        }
      }
      String serverKey = securityInfo.getServerPrincipal();
      if (serverKey == null) {
        throw new IOException("Can't obtain server Kerberos config key from SecurityInfo");
      }
      this.serverPrincipal = SecurityUtil.getServerPrincipal(client.conf.get(serverKey),
          address.getAddress().getCanonicalHostName().toLowerCase());
      if (LOG.isDebugEnabled()) {
        LOG.debug("RPC Server Kerberos principal name for service=" + serviceName + " is "
            + serverPrincipal);
      }
    }

    if (!useSasl) {
      authMethod = AuthMethod.SIMPLE;
    } else if (token != null) {
      authMethod = AuthMethod.DIGEST;
    } else {
      authMethod = AuthMethod.KERBEROS;
    }

    if (LOG.isDebugEnabled()) {
      LOG.debug("Use " + authMethod + " authentication for service " + serviceName +
          ", sasl=" + useSasl);
    }
    reloginMaxBackoff = client.conf.getInt("hbase.security.relogin.maxbackoff", 5000);
  }

  /**
   * Build the user information
   *
   * @param ugi        User Group Information
   * @param authMethod Authorization method
   * @return UserInformation protobuf
   */
  private RPCProtos.UserInformation buildUserInfo(UserGroupInformation ugi, AuthMethod authMethod) {
    if (ugi == null || authMethod == AuthMethod.DIGEST) {
      // Don't send user for token auth
      return null;
    }
    RPCProtos.UserInformation.Builder userInfoPB = RPCProtos.UserInformation.newBuilder();
    if (authMethod == AuthMethod.KERBEROS) {
      // Send effective user for Kerberos auth
      userInfoPB.setEffectiveUser(ugi.getUserName());
    } else if (authMethod == AuthMethod.SIMPLE) {
      //Send both effective user and real user for simple auth
      userInfoPB.setEffectiveUser(ugi.getUserName());
      if (ugi.getRealUser() != null) {
        userInfoPB.setRealUser(ugi.getRealUser().getUserName());
      }
    }
    return userInfoPB.build();
  }

  /**
   * Create connection preamble
   *
   * @param byteBuf    to write to
   * @param authMethod to write
   */
  private void createPreamble(ByteBuf byteBuf, AuthMethod authMethod) {
    byteBuf.writeBytes(HConstants.RPC_HEADER);
    byteBuf.writeByte(HConstants.RPC_CURRENT_VERSION);
    byteBuf.writeByte(authMethod.code);
  }

  /**
   * Close connection
   *
   * @param e exception on close
   */
  public void close(final Throwable e) {
    client.removeConnection(this);

    // Move closing from the requesting thread to the channel thread
    channel.eventLoop().execute(new Runnable() {
      @Override
      public void run() {
        List<AsyncCall> toCleanup;
        synchronized (pendingCalls) {
          if (closed) {
            return;
          }
          closed = true;
          toCleanup = new ArrayList<AsyncCall>(pendingCalls.values());
          pendingCalls.clear();
        }
        IOException closeException = null;
        if (e != null) {
          if (e instanceof IOException) {
            closeException = (IOException) e;
          } else {
            closeException = new IOException(e);
          }
        }
        // log the info
        if (LOG.isDebugEnabled() && closeException != null) {
          LOG.debug(name + ": closing ipc connection to " + address, closeException);
        }
        if (cleanupTimer != null) {
          cleanupTimer.cancel();
          cleanupTimer = null;
        }
        for (AsyncCall call : toCleanup) {
          call.setFailed(closeException != null ? closeException : new ConnectionClosingException(
              "Call id=" + call.id + " on server " + address + " aborted: connection is closing"));
        }
        channel.disconnect().addListener(ChannelFutureListener.CLOSE);
        if (LOG.isDebugEnabled()) {
          LOG.debug(name + ": closed");
        }
      }
    });
  }

  /**
   * Clean up calls.
   *
   * @param cleanAll true if all calls should be cleaned, false for only the timed out calls
   */
  private void cleanupCalls() {
    List<AsyncCall> toCleanup = new ArrayList<AsyncCall>();
    long currentTime = EnvironmentEdgeManager.currentTime();
    long nextCleanupTaskDelay = -1L;
    synchronized (pendingCalls) {
      for (Iterator<AsyncCall> iter = pendingCalls.values().iterator(); iter.hasNext();) {
        AsyncCall call = iter.next();
        long timeout = call.getRpcTimeout();
        if (timeout > 0) {
          if (currentTime - call.getStartTime() >= timeout) {
            iter.remove();
            toCleanup.add(call);
          } else {
            if (nextCleanupTaskDelay < 0 || timeout < nextCleanupTaskDelay) {
              nextCleanupTaskDelay = timeout;
            }
          }
        }
      }
      if (nextCleanupTaskDelay > 0) {
        cleanupTimer =
            client.newTimeout(timeoutTask, nextCleanupTaskDelay,
              TimeUnit.MILLISECONDS);
      } else {
        cleanupTimer = null;
      }
    }
    for (AsyncCall call : toCleanup) {
      call.setFailed(new CallTimeoutException("Call id=" + call.id + ", waitTime="
          + (currentTime - call.getStartTime()) + ", rpcTimeout=" + call.getRpcTimeout()));
    }
  }

  /**
   * Check if the connection is alive
   *
   * @return true if alive
   */
  public boolean isAlive() {
    return channel.isOpen();
  }

  /**
   * Check if user should authenticate over Kerberos
   *
   * @return true if should be authenticated over Kerberos
   * @throws java.io.IOException on failure of check
   */
  private synchronized boolean shouldAuthenticateOverKrb() throws IOException {
    UserGroupInformation loginUser = UserGroupInformation.getLoginUser();
    UserGroupInformation currentUser = UserGroupInformation.getCurrentUser();
    UserGroupInformation realUser = currentUser.getRealUser();
    return authMethod == AuthMethod.KERBEROS &&
        loginUser != null &&
        //Make sure user logged in using Kerberos either keytab or TGT
        loginUser.hasKerberosCredentials() &&
        // relogin only in case it is the login user (e.g. JT)
        // or superuser (like oozie).
        (loginUser.equals(currentUser) || loginUser.equals(realUser));
  }

  /**
   * If multiple clients with the same principal try to connect
   * to the same server at the same time, the server assumes a
   * replay attack is in progress. This is a feature of kerberos.
   * In order to work around this, what is done is that the client
   * backs off randomly and tries to initiate the connection
   * again.
   * The other problem is to do with ticket expiry. To handle that,
   * a relogin is attempted.
   * <p>
   * The retry logic is governed by the {@link #shouldAuthenticateOverKrb}
   * method. In case when the user doesn't have valid credentials, we don't
   * need to retry (from cache or ticket). In such cases, it is prudent to
   * throw a runtime exception when we receive a SaslException from the
   * underlying authentication implementation, so there is no retry from
   * other high level (for eg, HCM or HBaseAdmin).
   * </p>
   *
   * @param currRetries retry count
   * @param ex          exception describing fail
   * @param user        which is trying to connect
   * @throws java.io.IOException  if IO fail
   * @throws InterruptedException if thread is interrupted
   */
  private void handleSaslConnectionFailure(final int currRetries, final Throwable ex,
      final UserGroupInformation user) throws IOException, InterruptedException {
    user.doAs(new PrivilegedExceptionAction<Void>() {
      public Void run() throws IOException, InterruptedException {
        if (shouldAuthenticateOverKrb()) {
          if (currRetries < MAX_SASL_RETRIES) {
            LOG.debug("Exception encountered while connecting to the server : " + ex);
            //try re-login
            if (UserGroupInformation.isLoginKeytabBased()) {
              UserGroupInformation.getLoginUser().reloginFromKeytab();
            } else {
              UserGroupInformation.getLoginUser().reloginFromTicketCache();
            }

            // Should reconnect
            return null;
          } else {
            String msg = "Couldn't setup connection for " +
                UserGroupInformation.getLoginUser().getUserName() +
                " to " + serverPrincipal;
            LOG.warn(msg);
            throw (IOException) new IOException(msg).initCause(ex);
          }
        } else {
          LOG.warn("Exception encountered while connecting to " +
              "the server : " + ex);
        }
        if (ex instanceof RemoteException) {
          throw (RemoteException) ex;
        }
        if (ex instanceof SaslException) {
          String msg = "SASL authentication failed." +
              " The most likely cause is missing or invalid credentials." +
              " Consider 'kinit'.";
          LOG.fatal(msg, ex);
          throw new RuntimeException(msg, ex);
        }
        throw new IOException(ex);
      }
    });
  }

  public int getConnectionHashCode() {
    return ConnectionId.hashCode(ticket, serviceName, address);
  }

  @Override
  public String toString() {
    return this.address.toString() + "/" + this.serviceName + "/" + this.ticket;
  }

  /**
   * Listens to call writes and fails if write failed
   */
  private static final class CallWriteListener implements ChannelFutureListener {
    private final AsyncRpcChannel rpcChannel;
    private final int id;

    public CallWriteListener(AsyncRpcChannel asyncRpcChannel, int id) {
      this.rpcChannel = asyncRpcChannel;
      this.id = id;
    }

    @Override
    public void operationComplete(ChannelFuture future) throws Exception {
      if (!future.isSuccess()) {
        AsyncCall call = rpcChannel.removePendingCall(id);
        if (call != null) {
          if (future.cause() instanceof IOException) {
            call.setFailed((IOException) future.cause());
          } else {
            call.setFailed(new IOException(future.cause()));
          }
        }
      }
    }
  }
}
