io.atomix.copycat.client.DefaultCopycatClient 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.concurrent.BlockingFuture;
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.session.ClientSession;
import io.atomix.copycat.client.util.AddressSelector;
import io.atomix.copycat.session.ClosedSessionException;
import io.atomix.copycat.session.Session;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.time.Duration;
import java.util.Collection;
import java.util.Collections;
import java.util.Set;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.CopyOnWriteArraySet;
import java.util.function.Consumer;
/**
* Default Copycat client implementation.
*
* @author cluster;
private final Transport transport;
private final ThreadContext ioContext;
private final ThreadContext eventContext;
private final AddressSelector selector;
private final Duration sessionTimeout;
private final ConnectionStrategy connectionStrategy;
private final RecoveryStrategy recoveryStrategy;
private ClientSession session;
private volatile State state = State.CLOSED;
private volatile CompletableFuture openFuture;
private volatile CompletableFuture recoverFuture;
private volatile CompletableFuture closeFuture;
private final Set changeListeners = new CopyOnWriteArraySet<>();
private final Set> eventListeners = new CopyOnWriteArraySet<>();
private Listener changeListener;
DefaultCopycatClient(String clientId, Collection cluster, Transport transport, ThreadContext ioContext, ThreadContext eventContext, ServerSelectionStrategy selectionStrategy, ConnectionStrategy connectionStrategy, RecoveryStrategy recoveryStrategy, Duration sessionTimeout) {
this.clientId = Assert.notNull(clientId, "clientId");
this.cluster = Assert.notNull(cluster, "cluster");
this.transport = Assert.notNull(transport, "transport");
this.ioContext = Assert.notNull(ioContext, "ioContext");
this.eventContext = Assert.notNull(eventContext, "eventContext");
this.selector = new AddressSelector(selectionStrategy);
this.connectionStrategy = Assert.notNull(connectionStrategy, "connectionStrategy");
this.recoveryStrategy = Assert.notNull(recoveryStrategy, "recoveryStrategy");
this.sessionTimeout = Assert.notNull(sessionTimeout, "sessionTimeout");
}
@Override
public State state() {
return state;
}
/**
* Updates the client state.
*/
private void setState(State state) {
if (this.state != state) {
this.state = state;
LOGGER.debug("State changed: {}", state);
changeListeners.forEach(l -> l.accept(state));
}
}
@Override
public Listener onStateChange(Consumer callback) {
return new StateChangeListener(callback);
}
@Override
public Transport transport() {
return transport;
}
@Override
public Serializer serializer() {
ThreadContext context = ThreadContext.currentContext();
return context != null ? context.serializer() : this.eventContext.serializer();
}
@Override
public Session session() {
return session;
}
@Override
public ThreadContext context() {
return eventContext;
}
/**
* Creates a new child session.
*/
private ClientSession newSession() {
ClientSession session = new ClientSession(clientId, transport.client(), selector, ioContext, connectionStrategy, sessionTimeout);
// Update the session change listener.
if (changeListener != null)
changeListener.close();
changeListener = session.onStateChange(this::onStateChange);
// Register all event listeners.
eventListeners.forEach(l -> l.register(session));
return session;
}
/**
* Handles a session state change.
*/
private void onStateChange(Session.State state) {
switch (state) {
// When the session is opened, transition the state to CONNECTED.
case OPEN:
setState(State.CONNECTED);
break;
// When the session becomes unstable, transition the state to SUSPENDED.
case UNSTABLE:
setState(State.SUSPENDED);
break;
// When the session is expired, transition the state to SUSPENDED if necessary. The recovery strategy
// must determine whether to attempt to recover the client.
case EXPIRED:
setState(State.SUSPENDED);
recoveryStrategy.recover(this);
break;
case CLOSED:
setState(State.CLOSED);
break;
default:
break;
}
}
@Override
public synchronized CompletableFuture connect(Collection cluster) {
if (state != State.CLOSED)
return CompletableFuture.completedFuture(this);
if (openFuture == null) {
openFuture = new CompletableFuture<>();
// If the provided cluster list is null or empty, use the default list.
if (cluster == null || cluster.isEmpty()) {
cluster = this.cluster;
}
// If the default list is null or empty, use the default host:port.
if (cluster == null || cluster.isEmpty()) {
cluster = Collections.singletonList(new Address(DEFAULT_HOST, DEFAULT_PORT));
}
// Reset the connection list to allow the selection strategy to prioritize connections.
selector.reset(null, cluster);
// Create and register a new session.
session = newSession();
session.register().whenCompleteAsync((result, error) -> {
if (error == null) {
openFuture.complete(this);
} else {
openFuture.completeExceptionally(error);
}
}, eventContext.executor());
}
return openFuture;
}
@Override
public CompletableFuture submit(Command command) {
ClientSession session = this.session;
if (session == null)
return Futures.exceptionalFuture(new ClosedSessionException("session closed"));
BlockingFuture future = new BlockingFuture<>();
session.submit(command).whenComplete((result, error) -> {
if (eventContext.isBlocked()) {
future.accept(result, error);
} else {
eventContext.executor().execute(() -> future.accept(result, error));
}
});
return future;
}
@Override
public CompletableFuture submit(Query query) {
ClientSession session = this.session;
if (session == null)
return Futures.exceptionalFuture(new ClosedSessionException("session closed"));
BlockingFuture future = new BlockingFuture<>();
session.submit(query).whenComplete((result, error) -> {
if (eventContext.isBlocked()) {
future.accept(result, error);
} else {
eventContext.executor().execute(() -> future.accept(result, error));
}
});
return future;
}
@Override
public Listener onEvent(String event, Runnable callback) {
return onEvent(event, v -> callback.run());
}
@Override
public Listener onEvent(String event, Consumer callback) {
EventListener listener = new EventListener<>(event, callback);
listener.register(session);
return listener;
}
@Override
public synchronized CompletableFuture recover() {
if (recoverFuture == null) {
LOGGER.debug("Recovering session {}", this.session.id());
recoverFuture = new CompletableFuture<>();
session.close().whenCompleteAsync((closeResult, closeError) -> {
session = newSession();
session.register().whenCompleteAsync((registerResult, registerError) -> {
CompletableFuture recoverFuture = this.recoverFuture;
if (registerError == null) {
recoverFuture.complete(this);
} else {
recoverFuture.completeExceptionally(registerError);
}
this.recoverFuture = null;
}, eventContext.executor());
}, eventContext.executor());
}
return recoverFuture;
}
@Override
public synchronized CompletableFuture close() {
if (state == State.CLOSED)
return CompletableFuture.completedFuture(null);
if (closeFuture == null) {
// Close the child session and call close listeners once complete.
closeFuture = new CompletableFuture<>();
session.close().whenCompleteAsync((result, error) -> {
setState(State.CLOSED);
CompletableFuture.runAsync(() -> {
ioContext.close();
eventContext.close();
transport.close();
if (error == null) {
closeFuture.complete(null);
} else {
closeFuture.completeExceptionally(error);
}
});
}, eventContext.executor());
}
return closeFuture;
}
/**
* Kills the client.
*
* @return A completable future to be completed once the client's session has been killed.
*/
public synchronized CompletableFuture kill() {
if (state == State.CLOSED)
return CompletableFuture.completedFuture(null);
if (closeFuture == null) {
closeFuture = session.kill()
.whenComplete((result, error) -> {
setState(State.CLOSED);
CompletableFuture.runAsync(() -> {
ioContext.close();
eventContext.close();
transport.close();
});
});
}
return closeFuture;
}
@Override
public int hashCode() {
return 23 + 37 * (session != null ? session.hashCode() : 0);
}
@Override
public boolean equals(Object object) {
return object instanceof DefaultCopycatClient && ((DefaultCopycatClient) object).session() == session;
}
@Override
public String toString() {
return String.format("%s[session=%s]", getClass().getSimpleName(), session);
}
/**
* State change listener.
*/
private final class StateChangeListener implements Listener {
private final Consumer callback;
protected StateChangeListener(Consumer callback) {
this.callback = callback;
changeListeners.add(this);
}
@Override
public void accept(State state) {
eventContext.executor().execute(() -> callback.accept(state));
}
@Override
public void close() {
changeListeners.remove(this);
}
}
/**
* Event listener wrapper.
*/
private final class EventListener implements Listener {
private final String event;
private final Consumer callback;
private Listener parent;
private EventListener(String event, Consumer callback) {
this.event = event;
this.callback = callback;
eventListeners.add(this);
}
/**
* Registers the session event listener.
*/
public void register(ClientSession session) {
parent = session.onEvent(event, this);
}
@Override
public void accept(T message) {
if (eventContext.isBlocked()) {
callback.accept(message);
} else {
eventContext.executor().execute(() -> callback.accept(message));
}
}
@Override
public void close() {
parent.close();
eventListeners.remove(this);
}
}
}
© 2015 - 2024 Weber Informatics LLC | Privacy Policy