/*
* 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.resource;
import io.atomix.catalyst.concurrent.Listener;
import io.atomix.catalyst.concurrent.ThreadContext;
import io.atomix.catalyst.serializer.Serializer;
import io.atomix.catalyst.util.Assert;
import io.atomix.copycat.client.CopycatClient;
import io.atomix.resource.internal.ResourceCommand;
import io.atomix.resource.internal.ResourceCopycatClient;
import io.atomix.resource.internal.ResourceEvent;
import io.atomix.resource.internal.ResourceQuery;
import java.util.Map;
import java.util.Properties;
import java.util.Set;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.CopyOnWriteArraySet;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.function.Consumer;
/**
* Abstract resource.
*
* @author <a href="http://github.com/kuujo>Jordan Halterman</a>
*/
public abstract class AbstractResource<T extends Resource<T>> implements Resource<T> {
private final ResourceType type;
protected final CopycatClient client;
protected volatile Config config;
protected final Options options;
private volatile State state;
private final Set<StateChangeListener> changeListeners = new CopyOnWriteArraySet<>();
private final Set<RecoveryListener> recoveryListeners = new CopyOnWriteArraySet<>();
private final Map<Integer, Set<Consumer>> eventListeners = new ConcurrentHashMap<>();
private final AtomicInteger recoveryAttempt = new AtomicInteger(0);
protected AbstractResource(CopycatClient client, Properties options) {
this(client, null, options);
}
protected AbstractResource(CopycatClient client, ResourceType type, Properties options) {
this.client = new ResourceCopycatClient(Assert.notNull(client, "client"));
if (type == null)
type = new ResourceType(getClass());
this.type = type;
client.serializer().register(ResourceCommand.class, -50);
client.serializer().register(ResourceQuery.class, -51);
client.serializer().register(ResourceQuery.Config.class, -52);
client.serializer().register(ResourceCommand.Delete.class, -53);
client.serializer().register(ResourceType.class, -54);
client.serializer().register(ResourceEvent.class, -49);
this.config = new Config();
this.options = new Options(Assert.notNull(options, "options"));
client.onStateChange(this::onStateChange);
}
/**
* Registers a new event listener for the given event type.
* <p>
* Resource implementations should use this method to register event listeners for non-lifecycle
* resource events. Calling this method will cause the local session to be automatically registered
* with the state machine to listen for new events published in the state machine via the
* {@code notify} method.
*
* @param type The event type for which to register the event listener.
* @param callback The event listener callback.
* @param <T> The event type.
* @return A completable future to be completed once the event listener has been registered.
*/
protected synchronized <T extends Event> CompletableFuture<Listener<T>> onEvent(EventType type, Consumer<T> callback) {
Set<Consumer> listeners = eventListeners.computeIfAbsent(type.id(), id -> new CopyOnWriteArraySet<>());
listeners.add(callback);
return client.submit(new ResourceCommand.Register(type.id())).whenComplete((result, error) -> {
if (error != null) {
synchronized (this) {
listeners.remove(callback);
if (listeners.isEmpty()) {
eventListeners.remove(type.id());
client.submit(new ResourceCommand.Unregister(type.id()));
}
}
}
}).<Listener<T>>thenApply(v -> new Listener<T>() {
@Override
public void accept(T event) {
callback.accept(event);
}
@Override
public void close() {
synchronized (this) {
listeners.remove(callback);
if (listeners.isEmpty()) {
eventListeners.remove(type.id());
client.submit(new ResourceCommand.Unregister(type.id()));
}
}
}
});
}
/**
* Handles an event from the cluster.
*/
@SuppressWarnings("unchecked")
private void onEvent(ResourceEvent event) {
Set<Consumer> listeners = eventListeners.get(event.id());
if (listeners != null) {
for (Consumer listener : listeners) {
listener.accept(event.event());
}
}
}
/**
* Called when a client state change occurs.
*/
private void onStateChange(CopycatClient.State state) {
final State newState = State.valueOf(state.name());
if (this.state == State.SUSPENDED && newState == State.CONNECTED) {
final int attempt = recoveryAttempt.incrementAndGet();
recover(attempt).whenComplete((v, e) -> {
// @todo what should on error? close client?
recoveryListeners.forEach(l -> l.accept(attempt));
});
}
this.state = newState;
changeListeners.forEach(l -> l.accept(this.state));
}
@Override
public ResourceType type() {
return type;
}
@Override
public Serializer serializer() {
return client.serializer();
}
@Override
public Config config() {
return config;
}
@Override
public Options options() {
return options;
}
@Override
public State state() {
return state;
}
@Override
public Listener<State> onStateChange(Consumer<State> callback) {
return new StateChangeListener(Assert.notNull(callback, "callback"));
}
@Override
public Listener<Integer> onRecovery(Consumer<Integer> callback) {
return new RecoveryListener(Assert.notNull(callback, "callback"));
}
@Override
public ThreadContext context() {
return client.context();
}
@Override
@SuppressWarnings("unchecked")
public CompletableFuture<T> open() {
return client.connect()
.thenCompose(v -> client.submit(new ResourceQuery.Config()))
.thenApply(config -> {
this.config = new Config(config);
client.<ResourceEvent>onEvent("event", this::onEvent);
return (T) this;
});
}
@Override
public boolean isOpen() {
return state != State.CLOSED;
}
@Override
public CompletableFuture<Void> close() {
return client.close();
}
@Override
public boolean isClosed() {
return state == State.CLOSED;
}
@Override
public CompletableFuture<Void> delete() {
return client.submit(new ResourceCommand.Delete());
}
@Override
public int hashCode() {
return 37 * 23 + client.hashCode();
}
@Override
public boolean equals(Object object) {
return object instanceof AbstractResource && ((AbstractResource) object).client.session().id() == client.session().id();
}
@Override
public String toString() {
return String.format("%s[id=%s]", getClass().getSimpleName(), client.session().id());
}
protected CompletableFuture<Void> recover(Integer attempt) {
return CompletableFuture.completedFuture(null);
}
/**
* 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);
}
}
/**
* Resource state change listener.
*/
private class RecoveryListener implements Listener<Integer> {
private final Consumer<Integer> callback;
private RecoveryListener(Consumer<Integer> callback) {
this.callback = callback;
recoveryListeners.add(this);
}
@Override
public void accept(Integer attempt) {
callback.accept(attempt);
}
@Override
public void close() {
recoveryListeners.remove(this);
}
}
}