/*
* 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.resource.internal;
import io.atomix.catalyst.concurrent.Futures;
import io.atomix.catalyst.concurrent.Listener;
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.copycat.Command;
import io.atomix.copycat.Query;
import io.atomix.copycat.client.CopycatClient;
import io.atomix.copycat.session.Session;
import io.atomix.manager.internal.CloseResource;
import io.atomix.manager.internal.DeleteResource;
import io.atomix.manager.internal.GetResource;
import io.atomix.resource.Resource;
import io.atomix.resource.internal.ResourceCommand;
import java.util.Collection;
import java.util.HashSet;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.CopyOnWriteArraySet;
import java.util.function.Consumer;
/**
* Special {@link CopycatClient} implementation for operating on server-side replicated state
* on behalf of a client-side {@link Resource} instance.
* <p>
* The instance client handles submission of {@link Command commands} and {@link Query queries}
* for a {@link Resource} instance. It handles wrapping client operations in {@link InstanceCommand}
* and {@link InstanceQuery} to route resource operations to the appropriate replicated state machine.
*
* @author <a href="http://github.com/kuujo">Jordan Halterman</a>
*/
public final class InstanceClient implements CopycatClient {
private volatile long resource;
private final ResourceInstance instance;
private final CopycatClient client;
private volatile Session clientSession;
private volatile InstanceSession session;
private volatile State state;
private final Listener<State> changeListener;
private final Map<String, Set<EventListener>> eventListeners = new ConcurrentHashMap<>();
private final Map<String, Listener<InstanceEvent<?>>> listeners = new ConcurrentHashMap<>();
private final Set<StateChangeListener> changeListeners = new CopyOnWriteArraySet<>();
private volatile CompletableFuture<CopycatClient> openFuture;
private volatile CompletableFuture<CopycatClient> recoverFuture;
private volatile CompletableFuture<Void> closeFuture;
public InstanceClient(ResourceInstance instance, CopycatClient client) {
this.instance = Assert.notNull(instance, "instance");
this.client = Assert.notNull(client, "client");
this.state = State.CLOSED;
this.changeListener = client.onStateChange(this::onStateChange);
}
@Override
public State state() {
return state;
}
/**
* Called when the parent client's state changes.
*/
private void onStateChange(State state) {
// Don't allow the underlying client to transition the instance's state if the state is CLOSED.
if (this.state != State.CLOSED && this.state != state) {
// If the underlying client registered a new session, set the instance's state to SUSPENDED
// and recover the instance using the new session.
if (!client.session().equals(clientSession)) {
clientSession = client.session();
if (this.state != State.SUSPENDED) {
this.state = State.SUSPENDED;
changeListeners.forEach(l -> l.accept(state));
}
recover();
}
// If the underlying client's state has changed, update the instance's state and invoke listeners.
// This ensures that the instance's state changes to SUSPENDED when the client's state changes
// to SUSPENDED, and the instance's state only changes back to CONNECTED if the underlying client's
// state changed back to CONNECTED *and* its session was maintained.
else {
this.state = state;
changeListeners.forEach(l -> l.accept(state));
}
}
}
@Override
public Listener<State> onStateChange(Consumer<State> callback) {
return client.onStateChange(callback);
}
@Override
public ThreadContext context() {
return client.context();
}
@Override
public Transport transport() {
return client.transport();
}
@Override
public Session session() {
return session;
}
@Override
public Serializer serializer() {
return client.serializer();
}
@Override
public <T> CompletableFuture<T> submit(Command<T> command) {
if (command instanceof ResourceCommand.Delete) {
return client.submit(new InstanceCommand<>(resource, command))
.thenCompose(v -> client.submit(new DeleteResource(resource)))
.thenApply(result -> null);
}
return client.submit(new InstanceCommand<>(resource, command));
}
@Override
public <T> CompletableFuture<T> submit(Query<T> query) {
return client.submit(new InstanceQuery<>(resource, query));
}
@Override
public Listener<Void> onEvent(String event, Runnable callback) {
return onEvent(event, v -> callback.run());
}
@Override
@SuppressWarnings("unchecked")
public synchronized <T> Listener<T> onEvent(String event, Consumer<T> listener) {
Assert.notNull(event, "event");
Assert.notNull(listener, "listener");
Set<EventListener> listeners = eventListeners.get(event);
if (listeners == null) {
listeners = new HashSet<>();
eventListeners.put(event, listeners);
this.listeners.put(event, client.onEvent(event, message -> handleEvent(event, message)));
}
EventListener context = new EventListener(event, listener);
listeners.add(context);
return context;
}
/**
* Handles receiving a resource message.
*/
@SuppressWarnings("unchecked")
private void handleEvent(String event, InstanceEvent<?> message) {
if (message.resource() == resource) {
Set<EventListener> listeners = eventListeners.get(event);
if (listeners != null) {
for (EventListener listener : listeners) {
listener.accept(message.message());
}
}
}
}
@Override
public synchronized CompletableFuture<CopycatClient> connect() {
if (state != State.CLOSED)
return Futures.exceptionalFuture(new IllegalStateException("client already open"));
if (openFuture == null) {
openFuture = client.submit(new GetResource(instance.key(), instance.type(), instance.config())).thenApply(this::completeOpen);
}
return openFuture;
}
@Override
public CompletableFuture<CopycatClient> connect(Collection<Address> members) {
if (members == null) {
return connect();
} else {
throw new UnsupportedOperationException();
}
}
/**
* Completes the registration of a new session.
*/
private synchronized CopycatClient completeOpen(long resourceId) {
this.resource = resourceId;
this.clientSession = client.session();
this.session = new InstanceSession(resourceId, clientSession, client.context());
this.state = State.CONNECTED;
changeListeners.forEach(l -> l.accept(State.CONNECTED));
openFuture = null;
recoverFuture = null;
return this;
}
@Override
public synchronized CompletableFuture<CopycatClient> recover() {
if (state != State.SUSPENDED)
return Futures.exceptionalFuture(new IllegalStateException("client not suspended"));
if (recoverFuture == null) {
recoverFuture = client.submit(new GetResource(instance.key(), instance.type(), instance.config())).thenApply(this::completeOpen);
}
return recoverFuture;
}
@Override
public synchronized CompletableFuture<Void> close() {
if (state == State.CLOSED)
return Futures.exceptionalFuture(new IllegalStateException("client already closed"));
if (closeFuture == null) {
closeFuture = client.submit(new CloseResource(resource))
.whenComplete((result, error) -> {
synchronized (this) {
instance.close();
changeListener.close();
for (Map.Entry<String, Listener<InstanceEvent<?>>> entry : listeners.entrySet()) {
entry.getValue().close();
}
listeners.clear();
this.state = State.CLOSED;
changeListeners.forEach(l -> l.accept(State.CLOSED));
closeFuture = null;
}
});
}
return closeFuture;
}
@Override
public String toString() {
return String.format("%s[resource=%d]", getClass().getSimpleName(), resource);
}
/**
* Receive listener context.
*/
private class EventListener<T> implements Listener<T> {
private final String event;
private final Consumer<T> listener;
private EventListener(String event, Consumer<T> listener) {
this.event = event;
this.listener = listener;
}
@Override
public void accept(T event) {
listener.accept(event);
}
@Override
public void close() {
synchronized (InstanceClient.this) {
Set<EventListener> listeners = eventListeners.get(event);
if (listeners != null) {
listeners.remove(this);
if (listeners.isEmpty()) {
eventListeners.remove(event);
Listener listener = InstanceClient.this.listeners.remove(event);
if (listener != null) {
listener.close();
}
}
}
}
}
}
/**
* Resource state change listener.
*/
private class StateChangeListener implements Listener<State> {
private final Consumer<State> callback;
private StateChangeListener(Consumer<State> callback) {
this.callback = callback;
changeListeners.add(this);
}
@Override
public void accept(State state) {
callback.accept(state);
}
@Override
public void close() {
changeListeners.remove(this);
}
}
}