/** Resets the members to which to connect. */
 private ClientSession resetMembers() {
   if (connectMembers.isEmpty() || connectMembers.size() < members.size() - 1) {
     connectMembers = new ArrayList<>(members);
   }
   return this;
 }
  /**
   * Sends a session request.
   *
   * @param request The request to send.
   * @param future The future to complete once the response is received.
   * @param checkOpen Whether to check if the session is open.
   * @param <T> The request type.
   * @param <U> The response type.
   * @return The provided future to be completed once the response is received.
   */
  private <T extends Request<T>, U extends Response<U>> CompletableFuture<U> request(
      T request, CompletableFuture<U> future, boolean checkOpen, boolean recordFailures) {
    context.checkThread();

    // If the session already expired, immediately fail the future.
    if (checkOpen && !isOpen()) {
      future.completeExceptionally(new IllegalStateException("session expired"));
      return future;
    }

    // If we're already connected to a server, use the existing connection. The connection will be
    // reset in the event
    // of an error on any connection, or callers can reset connections as well.
    if (connection != null) {
      return request(request, connection, future, checkOpen, true);
    }

    // If we've run out of servers to which to attempt to connect, determine whether we should
    // expire the
    // session based on the responses from servers with which we did successfully communicate and
    // the
    // time we were last able to successfully communicate with a correct server process. The
    // failureTime
    // indicates the first time we received a NO_LEADER_ERROR from a server.
    if (connectMembers.isEmpty()) {
      // If open checks are not being performed, don't retry connecting to the servers. Simply fail.
      if (!checkOpen) {
        LOGGER.warn("Failed to connect to cluster");
        future.completeExceptionally(new IllegalStateException("session not open"));
      }
      // If retries have already been scheduled, queue a callback to be called to retry the request.
      else if (retryFuture != null) {
        retries.add(
            () -> {
              LOGGER.debug("Retrying: {}", request);
              request(request, future, true, true);
            });
      }
      // If all servers indicated that no leader could be found and a session timeout has elapsed,
      // time out the session.
      else if (failureTime > 0 && failureTime + timeout < System.currentTimeMillis()) {
        LOGGER.warn("Lost session");
        resetConnection().onExpire();
        future.completeExceptionally(new IllegalStateException("session expired"));
      }
      // If not all servers responded with a NO_LEADER_EXCEPTION or less than a session timeout has
      // expired,
      // schedule a retry to attempt to connect to servers again.
      else {
        LOGGER.warn("Failed to communicate with cluster. Retrying");
        retryFuture = context.schedule(Duration.ofMillis(200), this::retryRequests);
        retries.add(() -> resetMembers().request(request, future, true, true));
      }
      return future;
    }

    // Remove the next random member from the members list.
    Address member = connectMembers.remove(random.nextInt(connectMembers.size()));

    // Connect to the server. If the connection fails, recursively attempt to connect to the next
    // server,
    // otherwise setup the connection and send the request.
    if (connectFuture == null) {
      // If there's no existing connect future, create a new one.
      LOGGER.info("Connecting: {}", member.socketAddress());
      connectFuture =
          client
              .connect(member)
              .thenCompose(this::setupConnection)
              .whenComplete(
                  (connection, error) -> {
                    connectFuture = null;
                    if (!checkOpen || isOpen()) {
                      if (error == null) {
                        request(request, connection, future, checkOpen, recordFailures);
                      } else {
                        LOGGER.info("Failed to connect: {}", member.socketAddress());
                        resetConnection().request(request, future, checkOpen, recordFailures);
                      }
                    } else {
                      future.completeExceptionally(new IllegalStateException("session not open"));
                    }
                  });
    } else {
      // We don't want concurrent requests to attempt to connect to the same server at the same
      // time, so
      // if the connection is already being attempted, piggyback on the existing connect future.
      connectFuture.whenComplete(
          (connection, error) -> {
            if (!checkOpen || isOpen()) {
              if (error == null) {
                request(request, connection, future, checkOpen, recordFailures);
              } else {
                request(request, future, checkOpen, recordFailures);
              }
            } else {
              future.completeExceptionally(new IllegalStateException("session not open"));
            }
          });
    }
    return future;
  }