/* * 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.manager; import io.atomix.catalyst.concurrent.Futures; import io.atomix.catalyst.concurrent.ThreadContext; 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.ConfigurationException; import io.atomix.copycat.client.*; import io.atomix.manager.internal.GetResourceKeys; import io.atomix.manager.internal.ResourceExists; import io.atomix.manager.options.ClientOptions; import io.atomix.manager.resource.internal.InstanceClient; import io.atomix.manager.resource.internal.ResourceInstance; import io.atomix.manager.util.ResourceManagerTypeResolver; import io.atomix.resource.Resource; import io.atomix.resource.ResourceRegistry; import io.atomix.resource.ResourceType; import java.time.Duration; import java.util.*; import java.util.concurrent.CompletableFuture; import java.util.concurrent.ConcurrentHashMap; 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) * ); * ResourceManager manager = ResourceClient.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. * <p> * <b>Client lifecycle</b> * <p> * When a client is {@link #connect(Collection) connected}, 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 ResourceClient implements ResourceManager<ResourceClient> { /** * 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. * @throws NullPointerException if {@code members} is null */ public static Builder builder() { return new Builder(); } /** * Returns a new ResourceClient 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 new Builder() .withTransport(clientProperties.transport()) .withSerializer(clientProperties.serializer()); } private final CopycatClient client; private final Map<Class<? extends Resource<?>>, ResourceType> types = new ConcurrentHashMap<>(); private final Map<String, Resource<?>> instances = new ConcurrentHashMap<>(); private final Map<String, CompletableFuture> futures = new ConcurrentHashMap<>(); /** * @throws NullPointerException if {@code client} or {@code registry} are null */ public ResourceClient(CopycatClient client) { this.client = Assert.notNull(client, "client"); } /** * Returns the underlying Copycat client. * * @return The underlying Copycat client. */ public CopycatClient client() { return client; } @Override public ThreadContext context() { return client.context(); } @Override public Serializer serializer() { return client.serializer(); } @Override public final ResourceType type(Class<? extends Resource<?>> type) { return types.computeIfAbsent(type, ResourceType::new); } @Override public CompletableFuture<Boolean> exists(String key) { return client.submit(new ResourceExists(key)); } @Override public CompletableFuture<Set<String>> keys() { return client.submit(new GetResourceKeys()); } @Override @SuppressWarnings("unchecked") public <T extends Resource> CompletableFuture<Set<String>> keys(Class<? super T> type) { return keys(type((Class<? extends Resource<?>>) type)); } @Override public CompletableFuture<Set<String>> keys(ResourceType type) { return client.submit(new GetResourceKeys(Assert.notNull(type, "type").id())); } @Override @SuppressWarnings("unchecked") public <T extends Resource> CompletableFuture<T> getResource(String key, Class<? super T> type) { return getResource(key, type((Class<? extends Resource<?>>) type), new Resource.Config(), new Resource.Options()); } @Override @SuppressWarnings("unchecked") public <T extends Resource> CompletableFuture<T> getResource(String key, Class<? super T> type, Resource.Config config) { return this.<T>getResource(key, type((Class<? extends Resource<?>>) type), config, new Resource.Options()); } @Override @SuppressWarnings("unchecked") public <T extends Resource> CompletableFuture<T> getResource(String key, Class<? super T> type, Resource.Options options) { return this.<T>getResource(key, type((Class<? extends Resource<?>>) type), new Resource.Config(), options); } @Override @SuppressWarnings("unchecked") public <T extends Resource> CompletableFuture<T> getResource(String key, Class<? super T> type, Resource.Config config, Resource.Options options) { return this.<T>getResource(key, type((Class<? extends Resource<?>>) type), config, options); } @Override @SuppressWarnings("unchecked") public <T extends Resource> CompletableFuture<T> getResource(String key, ResourceType type) { return getResource(key, type, new Resource.Config(), new Resource.Options()); } @Override @SuppressWarnings("unchecked") public <T extends Resource> CompletableFuture<T> getResource(String key, ResourceType type, Resource.Config config) { return this.<T>getResource(key, type, config, new Resource.Options()); } @Override @SuppressWarnings("unchecked") public <T extends Resource> CompletableFuture<T> getResource(String key, ResourceType type, Resource.Options options) { return this.<T>getResource(key, type, new Resource.Config(), options); } @Override @SuppressWarnings("unchecked") public synchronized <T extends Resource> CompletableFuture<T> getResource(String key, ResourceType type, Resource.Config config, Resource.Options options) { Assert.notNull(key, "key"); Assert.notNull(type, "type"); Assert.notNull(config, "config"); Assert.notNull(options, "options"); T resource; // Determine whether a singleton instance of the given resource key already exists. Resource<?> check = instances.get(key); if (check == null) { ResourceInstance instance = new ResourceInstance(key, type, config, this::close); InstanceClient client = new InstanceClient(instance, this.client); try { check = type.factory().newInstance().createInstance(client, options); instances.put(key, check); } catch (InstantiationException | IllegalAccessException e) { return Futures.exceptionalFuture(e); } } // Ensure the existing singleton instance type matches the requested instance type. If the instance // was created new, this condition will always pass. If there was another instance created of a // different type, an exception will be returned without having to make a request to the cluster. if (check.type().id() != type.id()) { return Futures.exceptionalFuture(new IllegalArgumentException("inconsistent resource type: " + type)); } resource = (T) check; // Ensure if a singleton instance is already being created, the existing open future is returned. return futures.computeIfAbsent(key, k -> resource.open()); } /** * Closes the given resource instance. * * @param instance The instance to close. */ private synchronized void close(ResourceInstance instance) { instances.remove(instance.key()); futures.remove(instance.key()); } /** * Returns the resource client state. * * @return The resource client state. */ public CopycatClient.State state() { return client.state(); } /** * Connects the client to the cluster. * * @param cluster The cluster configuration to which to connect the client. * @return A completable future to be completed once the client is connected. */ public CompletableFuture<ResourceClient> connect(Address... cluster) { return connect(Arrays.asList(cluster)); } /** * Connects the client to the cluster. * * @param cluster The cluster configuration to which to connect the client. * @return A completable future to be completed once the client is connected. */ public CompletableFuture<ResourceClient> connect(Collection<Address> cluster) { return client.connect(cluster).thenApply(v -> this); } /** * Closes the client. * * @return A completable future to be completed once the client has been closed. */ public synchronized CompletableFuture<Void> close() { CompletableFuture<?>[] futures = new CompletableFuture[instances.size()]; int i = 0; for (Resource<?> instance : instances.values()) { futures[i++] = instance.close(); } return CompletableFuture.allOf(futures).thenCompose(v -> client.close()); } @Override public String toString() { return String.format("%s[session=%s]", getClass().getSimpleName(), client.session()); } /** * Builds a {@link ResourceClient}. * <p> * The client builder configures a {@link ResourceClient} to connect to a cluster of {@link ResourceServer}s. * To create a client builder, use the {@link #builder()} method. * <pre> * {@code * ResourceClient client = ResourceClient.builder(servers) * .withTransport(new NettyTransport()) * .build(); * } * </pre> */ public static class Builder implements io.atomix.catalyst.util.Builder<ResourceClient> { private final ResourceRegistry registry = new ResourceRegistry(); private CopycatClient.Builder clientBuilder; private Transport transport; protected Builder() { clientBuilder = CopycatClient.builder() .withServerSelectionStrategy(ServerSelectionStrategies.ANY) .withConnectionStrategy(ConnectionStrategies.FIBONACCI_BACKOFF) .withRecoveryStrategy(RecoveryStrategies.RECOVER); } /** * 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) { clientBuilder.withTransport(transport); this.transport = transport; return this; } /** * Sets the Atomix connection strategy. * * @param connectionStrategy The client connection strategy. * @return The Atomix builder. * @throws NullPointerException If the {@code connection strategy} is {@code null} */ public Builder withConnectionStrategy(ConnectionStrategy connectionStrategy) { clientBuilder.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) { clientBuilder.withSerializer(serializer); return this; } /** * Sets the client session timeout. * * @param sessionTimeout The client session timeout. * @return The client builder. */ public Builder withSessionTimeout(Duration sessionTimeout) { clientBuilder.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) { return withResourceTypes(Arrays.asList(types).stream().map(ResourceType::new).collect(Collectors.toList())); } /** * Sets the available resource types. * * @param types The available resource types. * @return The client builder. */ public Builder withResourceTypes(ResourceType... types) { return withResourceTypes(Arrays.asList(types)); } /** * Sets the available resource types. * * @param types The available resource types. * @return The client builder. */ public Builder withResourceTypes(Collection<ResourceType> types) { types.forEach(registry::register); return this; } /** * Adds a resource type to the server. * * @param type The resource type. * @return The server builder. */ public Builder addResourceType(Class<? extends Resource<?>> type) { return addResourceType(new ResourceType(type)); } /** * Adds a resource type to the server. * * @param type The resource type. * @return The server builder. */ public Builder addResourceType(ResourceType type) { registry.register(type); return this; } @Override public ResourceClient build() { if (transport == null) { try { transport = (Transport) Class.forName("io.atomix.catalyst.transport.netty.NettyTransport").newInstance(); } catch (ClassNotFoundException | InstantiationException | IllegalAccessException e) { throw new ConfigurationException("transport not configured"); } } CopycatClient client = clientBuilder.build(); client.serializer().resolve(new ResourceManagerTypeResolver()); for (ResourceType type : registry.types()) { try { type.factory().newInstance().createSerializableTypeResolver().resolve(client.serializer().registry()); } catch (InstantiationException | IllegalAccessException e) { throw new ResourceManagerException(e); } } return new ResourceClient(client); } } }