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.Address;
import io.atomix.catalyst.transport.Client;
import io.atomix.catalyst.transport.Connection;
import io.atomix.catalyst.transport.TransportException;
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.BiFunction;
import java.util.function.Consumer;
import java.util.function.Function;

/**
 * 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 public CompletableFuture send(Object request) { CompletableFuture future = new CompletableFuture<>(); sendRequest((Request) request, (r, c) -> c.send(r), future); return future; } @Override public CompletableFuture sendAndReceive(T request) { CompletableFuture future = new CompletableFuture<>(); sendRequest((Request) request, (r, c) -> c.sendAndReceive(r), future); return future; } /** * Sends the given request attempt to the cluster. */ private void sendRequest(T request, BiFunction> sender, CompletableFuture future) { if (open) { connect().whenComplete((c, e) -> sendRequest(request, sender, c, e, future)); } } /** * Sends the given request attempt to the cluster via the given connection if connected. */ private void sendRequest(T request, BiFunction> sender, Connection connection, Throwable error, CompletableFuture future) { if (open) { if (error == null) { if (connection != null) { LOGGER.trace("{} - Sending {}", id, request); sender.apply(request, connection).whenComplete((r, e) -> { if (e != null || r != null) { handleResponse(request, sender, connection, (Response) r, e, future); } else { future.complete(null); } }); } else { future.completeExceptionally(new ConnectException("Failed to connect to the cluster")); } } else { LOGGER.trace("{} - Resending {}: {}", id, request, error); resendRequest(error, request, sender, connection, future); } } } /** * Resends a request due to a request failure, resetting the connection if necessary. */ @SuppressWarnings("unchecked") private void resendRequest(Throwable cause, T request, BiFunction sender, Connection connection, CompletableFuture future) { // If the connection has not changed, reset it and connect to the next server. if (this.connection == connection) { LOGGER.trace("{} - Resetting connection. Reason: {}", id, cause); this.connection = null; connection.close(); } // Create a new connection and resend the request. This will force retries to piggyback on any existing // connect attempts. connect().whenComplete((c, e) -> sendRequest(request, sender, c, e, future)); } /** * Handles a response from the cluster. */ @SuppressWarnings("unchecked") private void handleResponse(T request, BiFunction sender, Connection connection, Response 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 || response.error() == CopycatError.Type.INTERNAL_ERROR) { LOGGER.trace("{} - Received {}", id, response); future.complete(response); } else { resendRequest(response.error().createException(), request, sender, connection, future); } } else if (error instanceof ConnectException || error instanceof TimeoutException || error instanceof TransportException || error instanceof ClosedChannelException) { resendRequest(error, request, sender, connection, future); } else { LOGGER.debug("{} - {} failed! Reason: {}", id, request, error); future.completeExceptionally(error); } } } /** * Connects to the cluster. */ private CompletableFuture connect() { // If the address selector has been reset then reset the connection. if (selector.state() == AddressSelector.State.RESET && connection != null) { if (connectFuture != null) { return connectFuture; } CompletableFuture future = new OrderedCompletableFuture<>(); future.whenComplete((r, e) -> this.connectFuture = null); this.connectFuture = future; Connection oldConnection = this.connection; this.connection = null; oldConnection.close(); connect(future); return future; } // 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. CompletableFuture future = new OrderedCompletableFuture<>(); future.whenComplete((r, e) -> this.connectFuture = null); this.connectFuture = future; reset().connect(future); return future; } /** * Attempts to connect to the cluster. */ private void connect(CompletableFuture future) { if (!selector.hasNext()) { LOGGER.debug("{} - Failed to connect to the cluster", id); future.complete(null); } else { Address address = selector.next(); LOGGER.debug("{} - Connecting to {}", id, 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 { LOGGER.debug("{} - Failed to connect! Reason: {}", id, error); connect(future); } } } /** * Sets up the given connection. */ @SuppressWarnings("unchecked") private void setupConnection(Address address, Connection connection, CompletableFuture future) { LOGGER.debug("{} - Setting up connection to {}", id, address); this.connection = connection; connection.onClose(c -> { if (c.equals(this.connection)) { LOGGER.debug("{} - Connection closed", id); this.connection = null; } }); connection.onException(c -> { if (c.equals(this.connection)) { LOGGER.debug("{} - Connection lost", id); this.connection = null; } }); for (Map.Entry, Function> entry : handlers.entrySet()) { connection.handler(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.trace("{} - Sending {}", id, request); connection.sendAndReceive(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.trace("{} - Received {}", id, 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 { LOGGER.debug("{} - Failed to connect! Reason: {}", id, error); connect(future); } } } @Override public Connection handler(Class type, Consumer handler) { return handler(type, r -> { handler.accept(r); return null; }); } @Override public Connection handler(Class type, Function> 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 onException(Consumer listener) { throw new UnsupportedOperationException(); } @Override public Listener onClose(Consumer listener) { throw new UnsupportedOperationException(); } @Override public CompletableFuture close() { open = false; return CompletableFuture.completedFuture(null); } }