/*
* Copyright 2016 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;
import io.atomix.catalyst.serializer.Serializer;
import io.atomix.catalyst.transport.Address;
import io.atomix.catalyst.transport.Transport;
import io.atomix.catalyst.util.Assert;
import io.atomix.catalyst.util.PropertiesReader;
import io.atomix.copycat.client.ConnectionStrategy;
import io.atomix.manager.ResourceClient;
import io.atomix.manager.ResourceServer;
import io.atomix.manager.options.ClientOptions;
import io.atomix.resource.Resource;
import io.atomix.resource.ResourceType;
import java.time.Duration;
import java.util.Arrays;
import java.util.Collection;
import java.util.Properties;
import java.util.concurrent.CompletableFuture;
import java.util.stream.Collectors;
/**
* Provides an interface for creating and operating on {@link io.atomix.resource.Resource}s remotely.
* <p>
* This {@link ResourceClient} implementation facilitates working with {@link io.atomix.resource.Resource}s remotely as
* a client of the Atomix cluster. To create a client, construct a client builder via {@link #builder()}.
* The builder requires a list of {@link Address}es to which to connect.
* <pre>
* {@code
* List<Address> servers = Arrays.asList(
* new Address("123.456.789.0", 5000),
* new Address("123.456.789.1", 5000)
* );
* Atomix atomix = AtomixClient.builder(servers)
* .withTransport(new NettyTransport())
* .build();
* }
* </pre>
* The {@link Address} list does not have to include all servers in the cluster, but must include at least one live
* server in order for the client to connect. Once the client connects to the cluster and opens a session, the client
* will receive an updated list of servers to which to connect.
* <p>
* Clients communicate with the cluster via a {@link Transport}. By default, the {@code NettyTransport} is used if
* no transport is explicitly configured. Thus, if no transport is configured then the Netty transport is expected
* to be available on the classpath.
* <h2>Client lifecycle</h2>
* When a client is {@link #connect(Address...) started}, the client will attempt to contact random servers in the provided
* {@link Address} list to open a new session. Opening a client session requires only that the client be able to
* communicate with at least one server which can communicate with the leader. Once a session has been opened,
* the client will periodically send keep-alive requests to the cluster to maintain its session. In the event
* that the client crashes or otherwise becomes disconnected from the cluster, the client's session will expire
* after a configured session timeout and the client will have to open a new session to reconnect.
* <p>
* Clients may connect to and communicate with any server in the cluster. Typically, once a client connects to a
* server, the client will attempt to remain connected to that server until some exceptional case occurs. Exceptional
* cases may include a failure of the server, a network partition, or the server falling too far out of sync with
* the rest of the cluster. When a failure in the cluster occurs and the client becomes disconnected from the cluster,
* it will transparently reconnect to random servers until it finds a reachable server.
* <p>
* During certain cluster events such as leadership changes, the client may not be able to communicate with the
* cluster for some arbitrary (but typically short) period of time. During that time, Atomix guarantees that the
* client's session will not expire even if its timeout elapses. Once a new leader is elected, the client's session
* timeout is reset.
*
* @author <a href="http://github.com/kuujo">Jordan Halterman</a>
*/
public class AtomixClient extends Atomix {
/**
* Returns a new Atomix replica builder from the given configuration file.
*
* @param properties The properties file from which to load the replica builder.
* @return The replica builder.
*/
public static Builder builder(String properties) {
return builder(PropertiesReader.load(properties).properties());
}
/**
* Returns a new Atomix replica builder from the given properties.
*
* @param properties The properties from which to load the replica builder.
* @return The replica builder.
*/
public static Builder builder(Properties properties) {
ClientOptions clientProperties = new ClientOptions(properties);
return builder()
.withTransport(clientProperties.transport())
.withSerializer(clientProperties.serializer());
}
/**
* Returns a new Atomix client builder.
* <p>
* 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.
*
* @return The client builder.
*/
public static Builder builder() {
return new Builder(ResourceClient.builder());
}
/**
* Builds the underlying resource client from the given properties.
*/
private static ResourceClient buildClient(Properties properties) {
ClientOptions clientProperties = new ClientOptions(properties);
return ResourceClient.builder()
.withTransport(clientProperties.transport())
.build();
}
/**
* Constructs a client from the given properties.
*
* @param properties The properties from which to construct the client.
*/
public AtomixClient(Properties properties) {
this(buildClient(properties));
}
/**
* Constructs a client for the given resource client.
*
* @param client The resource client.
*/
public AtomixClient(ResourceClient client) {
super(client);
}
/**
* Connects the client to the cluster.
*
* @return A completable future to be completed once the client has been connected.
*/
public CompletableFuture<Atomix> connect(Address... cluster) {
return connect(Arrays.asList(Assert.notNull(cluster, "cluster")));
}
/**
* Connects the client to the cluster.
*
* @return A completable future to be completed once the client has been connected.
*/
public CompletableFuture<Atomix> connect(Collection<Address> cluster) {
return client.connect(cluster).thenApply(v -> this);
}
/**
* Closes the client.
*
* @return A completable future to be completed once the client is closed.
*/
public CompletableFuture<Void> close() {
return client.close();
}
/**
* Builder for programmatically constructing an {@link AtomixClient}.
* <p>
* The client builder configures an {@link AtomixClient} to connect to a cluster of {@link ResourceServer}s.
* To create a client builder, use the {@link #builder()} method.
* <pre>
* {@code
* Atomix client = AtomixClient.builder(servers)
* .withTransport(new NettyTransport())
* .build();
* }
* </pre>
*/
public static class Builder implements io.atomix.catalyst.util.Builder<AtomixClient> {
private final ResourceClient.Builder builder;
private Builder(ResourceClient.Builder builder) {
this.builder = Assert.notNull(builder, "builder").withResourceTypes(RESOURCES);
}
/**
* Sets the Atomix transport.
* <p>
* The configured transport should be the same transport as all other nodes in the cluster.
* If no transport is explicitly provided, the instance will default to the {@code NettyTransport}
* if available on the classpath.
*
* @param transport The Atomix transport.
* @return The Atomix builder.
* @throws NullPointerException if {@code transport} is {@code null}
*/
public Builder withTransport(Transport transport) {
builder.withTransport(transport);
return this;
}
/**
* Sets the Atomix connection strategy.
*
* @param connectionStrategy The Atomix connection strategy.
* @return The Atomix builder.
* @throws NullPointerException If the {@code connection strategy} is {@code null}
*/
public Builder withConnectionStrategy(ConnectionStrategy connectionStrategy) {
builder.withConnectionStrategy(connectionStrategy);
return this;
}
/**
* Sets the Atomix serializer.
* <p>
* The serializer will be used to serialize and deserialize operations that are sent over the wire.
*
* @param serializer The Atomix serializer.
* @return The Atomix builder.
*/
public Builder withSerializer(Serializer serializer) {
builder.withSerializer(serializer);
return this;
}
/**
* Sets the client session timeout.
*
* @param sessionTimeout The client session timeout.
* @return The client builder.
*/
public Builder withSessionTimeout(Duration sessionTimeout) {
builder.withSessionTimeout(sessionTimeout);
return this;
}
/**
* Sets the available resource types.
*
* @param types The available resource types.
* @return The client builder.
*/
public Builder withResourceTypes(Class<? extends Resource<?>>... types) {
if (types != null) {
return withResourceTypes(Arrays.asList(types).stream().map(ResourceType::new).collect(Collectors.toList()));
}
return this;
}
/**
* Sets the available resource types.
*
* @param types The available resource types.
* @return The client builder.
*/
public Builder withResourceTypes(ResourceType... types) {
if (types != null) {
return withResourceTypes(Arrays.asList(types));
}
return this;
}
/**
* Sets the available resource types.
*
* @param types The available resource types.
* @return The client builder.
*/
public Builder withResourceTypes(Collection<ResourceType> types) {
builder.withResourceTypes(types);
return this;
}
/**
* Adds a resource type to the client.
*
* @param type The resource type.
* @return The client builder.
*/
public Builder addResourceType(Class<? extends Resource<?>> type) {
return addResourceType(new ResourceType(type));
}
/**
* Adds a resource type to the client.
*
* @param type The resource type.
* @return The client builder.
*/
public Builder addResourceType(ResourceType type) {
builder.addResourceType(type);
return this;
}
@Override
public AtomixClient build() {
return new AtomixClient(builder.build());
}
}
}