
io.atomix.copycat.client.CopycatClient Maven / Gradle / Ivy
/*
* 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;
import io.atomix.catalyst.serializer.Serializer;
import io.atomix.catalyst.serializer.ServiceLoaderTypeResolver;
import io.atomix.catalyst.transport.Address;
import io.atomix.catalyst.transport.Transport;
import io.atomix.catalyst.util.Assert;
import io.atomix.catalyst.util.ConfigurationException;
import io.atomix.catalyst.util.concurrent.Futures;
import io.atomix.catalyst.util.concurrent.ThreadContext;
import io.atomix.copycat.client.session.ClientSession;
import io.atomix.copycat.client.session.Session;
import java.util.*;
import java.util.concurrent.CompletableFuture;
/**
* Provides a feature complex {@link RaftClient}.
*
* Copycat clients can be constructed using the {@link CopycatClient.Builder}. To create a new client builder, use the
* static {@link #builder(Address...)} method, passing one or more server {@link Address}:
*
* {@code
* CopycatClient client = CopycatClient.builder(new Address("123.456.789.0", 5000), new Address("123.456.789.1", 5000).build();
* }
*
* By default, the client will attempt to use the {@code NettyTransport} to communicate with the cluster. See the {@link CopycatClient.Builder}
* documentation for client configuration options.
*
* Copycat clients interact with one or more nodes in a cluster through a session. When the client is {@link #open() opened},
* the client will attempt to one of the known member {@link Address} provided to the builder. As long as the client can
* communicate with at least one correct member of the cluster, it can open a session. Once the client is able to register a
* {@link Session}, it will receive an updated list of members for the entire cluster and thereafter be allowed to communicate
* with all servers.
*
* Sessions are created by registering the client through the cluster leader. Clients always connect to a single node in the
* cluster, and in the event of a node failure or partition, the client will detect the failure and reconnect to a correct server.
*
* Clients periodically send keep-alive requests to the server to which they're connected. The keep-alive request
* interval is determined by the cluster's session timeout, and the session timeout is determined by the leader's configuration
* at the time that the session is registered. This ensures that clients cannot be misconfigured with a keep-alive interval
* greater than the cluster's session timeout.
*
* Clients communicate with the distributed state machine by submitting {@link Command commands} and {@link Query queries} to
* the cluster through the {@link #submit(Command)} and {@link #submit(Query)} methods respectively:
*
* {@code
* client.submit(new PutCommand("foo", "Hello world!")).thenAccept(result -> {
* System.out.println("Result is " + result);
* });
* }
*
* All client methods are fully asynchronous and return {@link CompletableFuture}. To block until a method is complete, use
* the {@link CompletableFuture#get()} or {@link CompletableFuture#join()} methods.
*
* Sessions work to provide linearizable semantics for client {@link Command commands}. When a command is submitted to the cluster,
* the command will be forwarded to the leader where it will be logged and replicated. Once the command is stored on a majority
* of servers, the leader will apply it to its state machine and respond according to the command's {@link Command#consistency()}.
* See the {@link Command.ConsistencyLevel} documentation for more info.
*
* Sessions also allow {@link Query queries} (read-only requests) submitted by the client to optionally be executed on follower
* nodes. When a query is submitted to the cluster, the query's {@link Query#consistency()} will be used to determine how the
* query is handled. For queries with stronger consistency levels, they will be forwarded to the cluster's leader. For weaker
* consistency queries, they may be executed on follower nodes according to the consistency level constraints. See the
* {@link Query.ConsistencyLevel} documentation for more info.
*
* @author Jordan Halterman
*/
public class CopycatClient implements RaftClient {
/**
* Returns a new Raft client builder.
*
* The provided set of members will be used to connect to the Raft cluster. The members list does not have to represent
* the complete list of servers in the cluster, but it must have at least one reachable member that can communicate with
* the cluster's leader.
*
* @param members The cluster members to which to connect.
* @return The client builder.
*/
public static Builder builder(Address... members) {
return builder(Arrays.asList(Assert.notNull(members, "members")));
}
/**
* Returns a new Raft client builder.
*
* The provided set of members will be used to connect to the Raft cluster. The members list does not have to represent
* the complete list of servers in the cluster, but it must have at least one reachable member that can communicate with
* the cluster's leader.
*
* @param members The cluster members to which to connect.
* @return The client builder.
*/
public static Builder builder(Collection
members) {
return new Builder(members);
}
private final UUID id = UUID.randomUUID();
private final Transport transport;
private final Collection members;
private final Serializer serializer;
private final ConnectionStrategy connectionStrategy;
private final RecoveryStrategy recoveryStrategy;
private ClientSession session;
private CompletableFuture openFuture;
private CompletableFuture closeFuture;
protected CopycatClient(Transport transport, Collection members, Serializer serializer, ConnectionStrategy connectionStrategy, RecoveryStrategy recoveryStrategy) {
serializer.resolve(new ServiceLoaderTypeResolver());
this.transport = Assert.notNull(transport, "transport");
this.members = Assert.notNull(members, "members");
this.serializer = Assert.notNull(serializer, "serializer");
this.connectionStrategy = Assert.notNull(connectionStrategy, "connectionStrategy");
this.recoveryStrategy = Assert.notNull(recoveryStrategy, "recoveryStrategy");
}
@Override
public ThreadContext context() {
return session != null ? session.context() : null;
}
@Override
public Transport transport() {
return transport;
}
@Override
public Serializer serializer() {
return serializer;
}
@Override
public Session session() {
return session;
}
@Override
public CompletableFuture submit(Command command) {
Assert.notNull(command, "command");
if (session == null)
return Futures.exceptionalFuture(new IllegalStateException("client not open"));
return session.submit(command);
}
@Override
public CompletableFuture submit(Query query) {
Assert.notNull(query, "query");
if (session == null)
return Futures.exceptionalFuture(new IllegalStateException("client not open"));
return session.submit(query);
}
@Override
public CompletableFuture open() {
if (session != null && session.isOpen())
return CompletableFuture.completedFuture(this);
if (openFuture == null) {
synchronized (this) {
if (openFuture == null) {
ClientSession session = new ClientSession(id, transport, members, serializer, connectionStrategy);
if (closeFuture == null) {
openFuture = session.open().thenApply(s -> {
synchronized (this) {
openFuture = null;
this.session = session;
registerStrategies(session);
return this;
}
});
} else {
openFuture = closeFuture.thenCompose(v -> session.open().thenApply(s -> {
synchronized (this) {
openFuture = null;
this.session = session;
registerStrategies(session);
return this;
}
}));
}
}
}
}
return openFuture;
}
@Override
public boolean isOpen() {
return session != null && session.isOpen();
}
/**
* Registers strategies on the client's session.
*/
private void registerStrategies(Session session) {
session.onClose(s -> {
this.session = null;
if (s.isExpired()) {
recoveryStrategy.recover(this);
}
});
}
@Override
public CompletableFuture close() {
if (session == null || !session.isOpen())
return CompletableFuture.completedFuture(null);
if (closeFuture == null) {
synchronized (this) {
if (session == null) {
return CompletableFuture.completedFuture(null);
}
if (closeFuture == null) {
if (openFuture == null) {
closeFuture = session.close().whenComplete((result, error) -> {
synchronized (this) {
session = null;
closeFuture = null;
}
});
} else {
closeFuture = openFuture.thenCompose(v -> session.close().whenComplete((result, error) -> {
synchronized (this) {
session = null;
closeFuture = null;
}
}));
}
}
}
}
return closeFuture;
}
@Override
public boolean isClosed() {
return session == null || session.isClosed();
}
/**
* Builds a new Copycat client.
*
* New client builders should be constructed using the static {@link #builder(Address...)} factory method.
*
* {@code
* CopycatClient client = CopycatClient.builder(new Address("123.456.789.0", 5000), new Address("123.456.789.1", 5000)
* .withTransport(new NettyTransport())
* .build();
* }
*
*/
public static class Builder extends io.atomix.catalyst.util.Builder {
private Transport transport;
private Serializer serializer;
private Set members;
private ConnectionStrategy connectionStrategy = ConnectionStrategies.FOLLOWERS;
private RecoveryStrategy recoveryStrategy = RecoveryStrategies.CLOSE;
private Builder(Collection members) {
this.members = new HashSet<>(Assert.notNull(members, "members"));
}
/**
* Sets the client transport.
*
* By default, the client will use the {@code NettyTransport} with an event loop pool equal to
* {@link Runtime#availableProcessors()}.
*
* @param transport The client transport.
* @return The client builder.
* @throws NullPointerException if {@code transport} is null
*/
public Builder withTransport(Transport transport) {
this.transport = Assert.notNull(transport, "transport");
return this;
}
/**
* Sets the client serializer.
*
* By default, the client will use a {@link Serializer} configured with the {@link ServiceLoaderTypeResolver}.
*
* @param serializer The client serializer.
* @return The client builder.
* @throws NullPointerException if {@code serializer} is null
*/
public Builder withSerializer(Serializer serializer) {
this.serializer = Assert.notNull(serializer, "serializer");
return this;
}
/**
* Sets the client connection strategy.
*
* @param connectionStrategy The client connection strategy.
* @return The client builder.
*/
public Builder withConnectionStrategy(ConnectionStrategy connectionStrategy) {
this.connectionStrategy = Assert.notNull(connectionStrategy, "connectionStrategy");
return this;
}
/**
* Sets the client recovery strategy.
*
* @param recoveryStrategy The client recovery strategy.
* @return The client builder.
*/
public Builder withRecoveryStrategy(RecoveryStrategy recoveryStrategy) {
this.recoveryStrategy = Assert.notNull(recoveryStrategy, "recoveryStrategy");
return this;
}
/**
* @throws ConfigurationException if transport is not configured and {@code io.atomix.catalyst.transport.NettyTransport}
* is not found on the classpath
*/
@Override
public CopycatClient build() {
// If the transport is not configured, attempt to use the default Netty transport.
if (transport == null) {
try {
transport = (Transport) Class.forName("io.atomix.catalyst.transport.NettyTransport").newInstance();
} catch (ClassNotFoundException | InstantiationException | IllegalAccessException e) {
throw new ConfigurationException("transport not configured");
}
}
// If no serializer instance was provided, create one.
if (serializer == null) {
serializer = new Serializer();
}
return new CopycatClient(transport, members, serializer, connectionStrategy, recoveryStrategy);
}
}
}