All Downloads are FREE. Search and download functionalities are using the official Maven repository.

io.atomix.copycat.client.util.ClientConnection Maven / Gradle / Ivy

There is a newer version: 1.2.8
Show newest version
/*
 * Copyright 2015 the original author or authors.
 *
 * 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.atomix.copycat.client.util;

import io.atomix.catalyst.concurrent.Listener;
import io.atomix.catalyst.transport.*;
import io.atomix.catalyst.util.Assert;
import io.atomix.copycat.error.CopycatError;
import io.atomix.copycat.protocol.ConnectRequest;
import io.atomix.copycat.protocol.ConnectResponse;
import io.atomix.copycat.protocol.Request;
import io.atomix.copycat.protocol.Response;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import java.net.ConnectException;
import java.nio.channels.ClosedChannelException;
import java.util.Collection;
import java.util.Map;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.TimeoutException;
import java.util.function.Consumer;

/**
 * Client connection that recursively connects to servers in the cluster and attempts to submit requests.
 *
 * @author  servers() {
    return selector.servers();
  }

  /**
   * Resets the client connection.
   *
   * @return The client connection.
   */
  public ClientConnection reset() {
    selector.reset();
    return this;
  }

  /**
   * Resets the client connection.
   *
   * @param leader The current cluster leader.
   * @param servers The current servers.
   * @return The client connection.
   */
  public ClientConnection reset(Address leader, Collection
servers) { selector.reset(leader, servers); return this; } @Override @SuppressWarnings("unchecked") public CompletableFuture send(T request) { CompletableFuture future = new CompletableFuture<>(); sendRequest((Request) request, (CompletableFuture) future); return future; } /** * Sends the given request attempt to the cluster. */ private void sendRequest(T request, CompletableFuture future) { if (open) { connect().whenComplete((c, e) -> sendRequest(request, c, e, future)); } } /** * Sends the given request attempt to the cluster via the given connection if connected. */ private void sendRequest(T request, Connection connection, Throwable error, CompletableFuture future) { if (open) { if (error == null) { if (connection != null) { connection.send(request).whenComplete((r, e) -> handleResponse(request, r, e, future)); } else { future.completeExceptionally(new ConnectException("failed to connect")); } } else { this.connection = null; next().whenComplete((c, e) -> sendRequest(request, c, e, future)); } } } /** * Handles a response from the cluster. */ private void handleResponse(T request, U response, Throwable error, CompletableFuture future) { if (open) { if (error == null) { if (response.status() == Response.Status.OK || response.error() == CopycatError.Type.COMMAND_ERROR || response.error() == CopycatError.Type.QUERY_ERROR || response.error() == CopycatError.Type.APPLICATION_ERROR || response.error() == CopycatError.Type.UNKNOWN_SESSION_ERROR) { future.complete(response); } else { next().whenComplete((c, e) -> sendRequest(request, c, e, future)); } } else if (error instanceof ConnectException || error instanceof TimeoutException || error instanceof TransportException || error instanceof ClosedChannelException) { next().whenComplete((c, e) -> sendRequest(request, c, e, future)); } else { future.completeExceptionally(error); } } } /** * Connects to the cluster. */ private CompletableFuture connect() { // If the address selector has been then reset the connection. if (selector.state() == AddressSelector.State.RESET && connection != null) { if (connectFuture != null) return connectFuture; CompletableFuture future = new CompletableFuture<>(); connectFuture = future; connection.close().whenComplete((result, error) -> connect(future)); return connectFuture.whenComplete((result, error) -> connectFuture = null); } // If a connection was already established then use that connection. if (connection != null) return CompletableFuture.completedFuture(connection); // If a connection is currently being established then piggyback on the connect future. if (connectFuture != null) return connectFuture; // Create a new connect future and connect to the first server in the cluster. connectFuture = new CompletableFuture<>(); connect(connectFuture); // Reset the connect future field once the connection is complete. return connectFuture.whenComplete((result, error) -> connectFuture = null); } /** * Connects to the cluster using the next connection. */ private CompletableFuture next() { if (connection != null) return connection.close().thenRun(() -> connection = null).thenCompose(v -> connect()); return connect(); } /** * Attempts to connect to the cluster. */ private void connect(CompletableFuture future) { if (!selector.hasNext()) { LOGGER.debug("Failed to connect to the cluster"); future.complete(null); } else { Address address = selector.next(); LOGGER.debug("Connecting to {}", address); client.connect(address).whenComplete((c, e) -> handleConnection(address, c, e, future)); } } /** * Handles a connection to a server. */ private void handleConnection(Address address, Connection connection, Throwable error, CompletableFuture future) { if (open) { if (error == null) { setupConnection(address, connection, future); } else { connect(future); } } } /** * Sets up the given connection. */ @SuppressWarnings("unchecked") private void setupConnection(Address address, Connection connection, CompletableFuture future) { LOGGER.debug("Setting up connection to {}", address); this.connection = connection; connection.closeListener(c -> { if (c.equals(this.connection)) { LOGGER.debug("Connection closed"); this.connection = null; } }); connection.exceptionListener(c -> { if (c.equals(this.connection)) { LOGGER.debug("Connection lost"); this.connection = null; } }); for (Map.Entry, MessageHandler> entry : handlers.entrySet()) { connection.handler((Class) entry.getKey(), entry.getValue()); } // When we first connect to a new server, first send a ConnectRequest to the server to establish // the connection with the server-side state machine. ConnectRequest request = ConnectRequest.builder() .withClientId(id) .build(); LOGGER.debug("Sending {}", request); connection.send(request).whenComplete((r, e) -> handleConnectResponse(r, e, future)); } /** * Handles a connect response. */ private void handleConnectResponse(ConnectResponse response, Throwable error, CompletableFuture future) { if (open) { if (error == null) { LOGGER.debug("Received {}", response); // If the connection was successfully created, immediately send a keep-alive request // to the server to ensure we maintain our session and get an updated list of server addresses. if (response.status() == Response.Status.OK) { selector.reset(response.leader(), response.members()); future.complete(connection); } else { connect(future); } } else { connect(future); } } } @Override public Connection handler(Class type, MessageHandler handler) { Assert.notNull(type, "type"); Assert.notNull(handler, "handler"); handlers.put(type, handler); if (connection != null) connection.handler(type, handler); return this; } @Override public Listener exceptionListener(Consumer listener) { throw new UnsupportedOperationException(); } @Override public Listener closeListener(Consumer listener) { throw new UnsupportedOperationException(); } @Override public CompletableFuture close() { open = false; return CompletableFuture.completedFuture(null); } }