com.zipwhip.api.signals.sockets.SocketSignalProvider Maven / Gradle / Ivy
package com.zipwhip.api.signals.sockets;
import com.zipwhip.api.NestedObservableFuture;
import com.zipwhip.api.signals.PingEvent;
import com.zipwhip.api.signals.SignalConnection;
import com.zipwhip.api.signals.SignalProvider;
import com.zipwhip.api.signals.VersionMapEntry;
import com.zipwhip.api.signals.commands.*;
import com.zipwhip.api.signals.sockets.netty.NettySignalConnection;
import com.zipwhip.concurrent.*;
import com.zipwhip.events.Observer;
import com.zipwhip.executors.NamedThreadFactory;
import com.zipwhip.important.ImportantTaskExecutor;
import com.zipwhip.important.Scheduler;
import com.zipwhip.important.schedulers.ZipwhipTimerScheduler;
import com.zipwhip.lifecycle.DestroyableBase;
import com.zipwhip.signals.address.ClientAddress;
import com.zipwhip.signals.presence.Presence;
import com.zipwhip.signals.presence.PresenceCategory;
import com.zipwhip.timers.HashedWheelTimer;
import com.zipwhip.timers.Timer;
import com.zipwhip.util.Asserts;
import com.zipwhip.util.CollectionUtil;
import com.zipwhip.util.FutureDateUtil;
import com.zipwhip.util.StringUtil;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.net.InetSocketAddress;
import java.util.*;
import java.util.concurrent.Executor;
import java.util.concurrent.TimeUnit;
import static com.zipwhip.concurrent.ThreadUtil.ensureLock;
/**
* Created by IntelliJ IDEA. User: Michael Date: 8/1/11 Time: 4:30 PM
*
* The SocketSignalProvider will connect to the Zipwhip SignalServer via TCP.
*
* This interface is intended to be used by 1 and only 1 ZipwhipClient object.
* This is a very high level interaction where you connect for 1 user.
*/
public class SocketSignalProvider extends SignalProviderBase implements SignalProvider {
private static final Logger LOGGER = LoggerFactory.getLogger(SocketSignalProvider.class);
private final Map> slidingWindows = new HashMap>();
protected final ImportantTaskExecutor importantTaskExecutor;
protected final Scheduler scheduler;
protected final Timer timer;
// this is how we interact with the underlying signal server.
protected final SignalConnection signalConnection;
public SocketSignalProvider() {
this(new NettySignalConnection());
}
public SocketSignalProvider(SignalConnection connection) {
this(connection, null);
}
public SocketSignalProvider(SignalConnection connection, Executor executor) {
this(connection, executor, null);
}
public SocketSignalProvider(SignalConnection connection, Executor executor, Timer timer) {
super(executor);
if (timer == null) {
this.timer = new HashedWheelTimer(new NamedThreadFactory("SocketSignalProvider-"), 1, TimeUnit.SECONDS);
this.link(new DestroyableBase() {
@Override
protected void onDestroy() {
SocketSignalProvider.this.timer.stop();
}
});
} else {
this.timer = timer;
}
this.scheduler = new ZipwhipTimerScheduler(this.timer);
this.importantTaskExecutor = new ImportantTaskExecutor(this.scheduler);
this.link(importantTaskExecutor);
// TODO: we need to double check that the connection state hasn't changed while waiting.
this.scheduler.onScheduleComplete(this.onScheduleComplete);
if (connection == null) {
this.signalConnection = new NettySignalConnection();
this.link(signalConnection);
} else {
this.signalConnection = connection;
}
this.initEvents2();
}
private void initEvents2() {
resetConnectFutureIfMatchingObserver = new DifferentExecutorObserverAdapter>(executor,
new ThreadSafeObserverAdapter>(
resetConnectFutureIfMatchingObserver));
notifyConnectedOnFinishObserver = new DifferentExecutorObserverAdapter>(executor,
new ThreadSafeObserverAdapter>(
notifyConnectedOnFinishObserver));
// TODO: do we really have to do this? Isn't the "importantTaskExecutor" responsible for this?
this.signalConnection.getConnectEvent().addObserver(
new DifferentExecutorObserverAdapter(executor,
new ThreadSafeObserverAdapter(sendConnectCommandIfConnectedObserver)));
/**
* Forward disconnect events up to clients
*/
this.signalConnection.getDisconnectEvent().addObserver(
new DifferentExecutorObserverAdapter(executor,
new ThreadSafeObserverAdapter(
executeDisconnectStateObserver)));
this.signalConnection.getCommandReceivedEvent().addObserver(
new DifferentExecutorObserverAdapter(executor,
new ThreadSafeObserverAdapter(
new ActiveConnectionObserverAdapter(onMessageReceived))));
this.signalConnection.getPingEventReceivedEvent().addObserver(
new DifferentExecutorObserverAdapter(executor,
new ThreadSafeObserverAdapter(
new ActiveConnectionObserverAdapter(pingReceivedEvent))));
// the ActiveConnectionObserverAdapter will filter out old noise and adapt over the "sender" to the currentConnection if/only if they are active.
this.signalConnection.getExceptionEvent().addObserver(
new DifferentExecutorObserverAdapter(executor,
new ThreadSafeObserverAdapter(
new ActiveConnectionObserverAdapter(exceptionEvent))));
/**
* Observe our own version changed events so we can stay in sync internally
*/
getVersionChangedEvent().addObserver(updateVersionsOnVersionChanged);
getNewClientIdReceivedEvent().addObserver(updateStateOnNewClientIdReceived);
}
@Override
public synchronized ObservableFuture connect(String clientId, final Map versions, Presence presence) {
synchronized (signalConnection) {
final ObservableFuture connectFuture = getUnchangingConnectFuture();
if (connectFuture != null) {
return connectFuture;
}
// Make sure ConnectionHandler doesn't change while you work on it.
synchronized (PROVIDER_CONNECTION_HANDLE_LOCK) {
final ConnectionHandle connectionHandle = getUnchangingConnectionHandle();
if (connectionHandle != null) {
LOGGER.warn(String.format("We are already connected (%s). Returning fake future.", connectionHandle));
return new FakeObservableFuture(connectionHandle, connectionHandle);
}
// keep track of the original one, so we can detect change
final String finalClientId = clientId;
if (StringUtil.exists(clientId)) {
originalClientId = clientId;
}
// Hold onto these objects for internal reconnect attempts
if (presence != null) {
this.presence = presence;
}
if (CollectionUtil.exists(versions)) {
this.versions = versions;
}
final SignalProviderConnectionHandle finalSignalProviderConnectionHandle = createAndSetActiveSignalProviderConnection();
final NestedObservableFuture finalConnectFuture = createSelfHealingConnectFuture(finalSignalProviderConnectionHandle);
synchronized (finalConnectFuture) {
setConnectFuture(finalConnectFuture);
try {
// This future already has an underlying timeout.
// this signalConnection.connect() is
ObservableFuture requestFuture = signalConnection.connect();
requestFuture.addObserver(
new DifferentExecutorObserverAdapter>(executor,
new ThreadSafeObserverAdapter>(
new UpdateStateOnConnectCompleteObserver(finalConnectFuture, finalSignalProviderConnectionHandle, finalClientId))));
} catch (Exception e) {
clearConnectionHandle(finalSignalProviderConnectionHandle);
clearConnectFuture(finalConnectFuture);
throw new RuntimeException(e);
}
}
return finalConnectFuture;
}
}
}
@Override
public synchronized ObservableFuture disconnect() {
return disconnect(false);
}
@Override
public synchronized ObservableFuture disconnect(boolean causedByNetwork) {
synchronized (signalConnection) {
synchronized (PROVIDER_CONNECTION_HANDLE_LOCK) {
accessConnectionState();
accessConnectionHandle();
ConnectionState state = getConnectionState();
switch (state) {
case CONNECTING:
// we'll be interrupting the signalConnection, but that's ok.
case CONNECTED:
final ObservableFuture connectFuture = getUnchangingConnectFuture();
if (connectFuture != null){
synchronized (connectFuture) {
// we're interrupting our tasks
cancelConnectFuture();
}
}
break;
case AUTHENTICATED:
// we are allowed to disconnect in this case.
break;
case DISCONNECTING:
return getUnchangingConnectionHandle().getDisconnectFuture();
case DISCONNECTED:
return new FakeFailingObservableFuture(this, new IllegalStateException("Not currently connected"));
}
final SignalProviderConnectionHandle finalConnectionHandle = getUnchangingConnectionHandle();
Asserts.assertTrue(finalConnectionHandle != null, "The current connectionHandle must never disagree with the stateManager!");
if (finalConnectionHandle == null) return null; // not reachable. just here for compiler warnings.
synchronized (finalConnectionHandle) {
ObservableFuture disconnectFuture = signalConnection.disconnect(causedByNetwork);
Asserts.assertTrue(disconnectFuture != null, "DisconnectFuture null?");
if (disconnectFuture == null) return null; // not reachable. just here for compiler warnings.
if (finalConnectionHandle.connectionHandle != null) {
// it might be null if the connection was never fully baked.
Asserts.assertTrue(disconnectFuture == finalConnectionHandle.connectionHandle.getDisconnectFuture(), "The different handles didnt agree?");
} else {
disconnectFuture.addObserver(new DebugObserver());
disconnectFuture.addObserver(
new DifferentExecutorObserverAdapter>(executor,
new ThreadSafeObserverAdapter>(
new Observer>() {
@Override
public void notify(Object sender, ObservableFuture item) {
synchronized (PROVIDER_CONNECTION_HANDLE_LOCK) {
final ConnectionHandle c = getUnchangingConnectionHandle();
if (c == finalConnectionHandle) {
synchronized (c) {
clearConnectionHandle(c);
}
}
}
NestedObservableFuture.syncState(item, finalConnectionHandle.getDisconnectFuture(), finalConnectionHandle);
}
})));
}
return finalConnectionHandle.getDisconnectFuture();
}
}
}
}
private final Observer onMessageReceived = new Observer() {
/**
* The NettySignalConnection will call this method when there's an
* event from the remote SignalServer.
*
* @param sender The sender might not be the same object every time.
* @param command Rich object representing the command received from the SignalServer.
*/
@Override
public void notify(Object sender, Command command) {
SignalProviderConnectionHandle connection = (SignalProviderConnectionHandle) sender;
Asserts.assertTrue(!connection.isDestroyed(), "The connection wasn't active?!?");
// Check if this command has a valid version number associated with it...
if (command.getVersion() != null && command.getVersion().getValue() > 0) {
String versionKey = command.getVersion().getKey();
synchronized (slidingWindows) {
if (!slidingWindows.containsKey(versionKey)) {
LOGGER.warn("Creating sliding window for key " + versionKey);
SlidingWindow newWindow = new SlidingWindow(timer, versionKey);
newWindow.onHoleTimeout(signalHoleObserver);
newWindow.onPacketsReleased(packetReleasedObserver);
if (versions != null && versions.get(versionKey) != null) {
LOGGER.debug("Initializing sliding window index sequence to " + versions.get(versionKey));
newWindow.setIndexSequence(versions.get(versionKey));
}
slidingWindows.put(versionKey, newWindow);
}
}
// This list will be populated with the sequential packets that should be released
List commandResults = new ArrayList();
LOGGER.debug("Signal version " + command.getVersion().getValue());
SlidingWindow.ReceiveResult result = slidingWindows.get(versionKey).receive(command.getVersion().getValue(), command, commandResults);
switch (result) {
case EXPECTED_SEQUENCE:
LOGGER.debug("EXPECTED_SEQUENCE: " + commandResults);
handleCommands(connection, commandResults);
break;
case HOLE_FILLED:
LOGGER.debug("HOLE_FILLED: " + commandResults);
handleCommands(connection, commandResults);
break;
case DUPLICATE_SEQUENCE:
LOGGER.warn("DUPLICATE_SEQUENCE: " + commandResults);
break;
case POSITIVE_HOLE:
LOGGER.warn("POSITIVE_HOLE: " + commandResults);
break;
case NEGATIVE_HOLE:
LOGGER.debug("NEGATIVE_HOLE: " + commandResults);
handleCommands(connection, commandResults);
break;
default:
LOGGER.warn("UNKNOWN_RESULT: " + commandResults);
}
} else {
// Non versioned command, not windowed
handleCommand(connection, command);
}
}
@Override
public String toString() {
return "onMessageReceived";
}
};
private final Observer sendConnectCommandIfConnectedObserver = new Observer() {
/**
* The NettySignalConnection will call this method when a TCP socket connection is attempted.
*/
@Override
public void notify(Object sender, final ConnectionHandle socketConnectionHandle) {
ObservableFuture connectFuture = getUnchangingConnectFuture();
if (connectFuture != null) {
LOGGER.warn(String.format("We were currently 'connecting' and got a %s hit. Just quitting.", this));
return;
} else if (socketConnectionHandle == null) {
throw new NullPointerException("The socketConnectionHandle can never be null!");
}
synchronized (PROVIDER_CONNECTION_HANDLE_LOCK) {
SignalProviderConnectionHandle connectionHandle = getUnchangingConnectionHandle();
if (connectionHandle == null) {
// this might be an unsolicited reconnect due to the ReconnectStrategy of the SignalConnection.
// In that scenario we won't see this connection coming. We need to just accept this case and self heal.
ensureState(ConnectionState.DISCONNECTED);
// we just transitioned to a connected state!
connectionHandle = createAndSetActiveSignalProviderConnection();
connectionHandle.setConnectionHandle(socketConnectionHandle);
LOGGER.debug(String.format("Created our own in-line connectionHandle %s", connectionHandle));
}
final SignalProviderConnectionHandle finalSignalProviderConnectionHandle = connectionHandle;
Asserts.assertTrue(finalSignalProviderConnectionHandle != null, "getCurrentConnectionHandle() was null!");
if (!finalSignalProviderConnectionHandle.isFor(socketConnectionHandle)) {
// that's ok.
LOGGER.error(String.format("Got a stale request? Connection %s was not for %s", finalSignalProviderConnectionHandle, socketConnectionHandle));
return;
}
Asserts.assertTrue(finalSignalProviderConnectionHandle.isFor(socketConnectionHandle), String.format("getCurrentConnectionHandle() said this was for a different connection (%s/%s)!", socketConnectionHandle, finalSignalProviderConnectionHandle.connectionHandle));
synchronized (socketConnectionHandle) {
boolean connected = !socketConnectionHandle.isDestroyed();
if (!connected) {
LOGGER.warn(String.format("Got a %s hit but the handle was destroyed: %s", this, socketConnectionHandle));
return;
}
// The socketConnectionHandle is not allowed to destroy during this block.
// The socketConnectionHandle is not allowed to destroy during this block.
// The socketConnectionHandle is not allowed to destroy during this block.
/**
* If we have a successful TCP connection then check if we need to send the connect command.
*/
ObservableFuture connectCommandFuture = writeConnectCommandAsyncWithTimeoutBakedIn(socketConnectionHandle);
connectCommandFuture.addObserver(
new DifferentExecutorObserverAdapter>(executor,
new ThreadSafeObserverAdapter>(
new HandleConnectCommandIfDoneObserver(finalSignalProviderConnectionHandle))));
connectCommandFuture.addObserver(
new DifferentExecutorObserverAdapter>(executor,
new ThreadSafeObserverAdapter>(
new Observer>() {
@Override
public void notify(Object sender, final ObservableFuture future) {
if (future.isSuccess()) {
finalSignalProviderConnectionHandle.finishedActionConnect = true;
if (!socketConnectionHandle.isDestroyed()) {
// great, we're still active and we got a success from the future
// transition our state.
notifyConnected(finalSignalProviderConnectionHandle, true);
} else {
// the socketConnection isn't active. Someone else torn it down or we're late to the game.
// just quit.
LOGGER.error("The socketConnectionHandle was destroyed!");
}
} else {
// we might be in a timeout scenario?
// who's job is it to kill the connection?
// we'll kill it. This shoud cause a reconnect because we passed in true
// the reconnect strategy should kick in?
LOGGER.error("Issuing disconnect request since future was not successful.");
socketConnectionHandle.disconnect(true);
}
}
})));
}
}
}
@Override
public String toString() {
return "sendConnectCommandIfConnectedObserver";
}
};
private final Observer executeDisconnectStateObserver = new Observer() {
@Override
public void notify(Object sender, ConnectionHandle socketConnectionHandle) {
ObservableFuture connectionFuture = getUnchangingConnectFuture();
if (connectionFuture != null && !connectionFuture.isDone()) {
LOGGER.debug("executeDisconnectStateObserver: Currently in the connecting phase, ignoring notify()");
return;
}
synchronized (PROVIDER_CONNECTION_HANDLE_LOCK) {
accessConnectionHandle();
SignalProviderConnectionHandle myConnectionHandle = getUnchangingConnectionHandle();
if (myConnectionHandle == null || myConnectionHandle.isDestroyed()) {
LOGGER.error(String.format("myConnectionHandle was null (or destroyed)! Quitting."));
return;
} else if (!myConnectionHandle.isFor(socketConnectionHandle)) {
LOGGER.error(String.format("executeDisconnectStateObserver got %s was expecting %s.for(). Quitting.", socketConnectionHandle, myConnectionHandle));
return;
}
executeDisconnect(myConnectionHandle);
}
}
@Override
public String toString() {
return "executeDisconnectStateObserver";
}
};
private void executeDisconnect(ConnectionHandle connectionHandle) {
LOGGER.debug("SignalConnection said disconnected, we're going to update our local state.");
final SignalProviderConnectionHandle finalSignalProviderConnectionHandle = this.getUnchangingConnectionHandle();
if (finalSignalProviderConnectionHandle == null) {
LOGGER.error("Does not seem to be an active ConnectionHandle. Quitting executeDisconnect()");
return;
} else if (finalSignalProviderConnectionHandle != connectionHandle) {
LOGGER.error("The connectionHandles didnt agree, so not executing the disconnect events");
return;
}
synchronized (finalSignalProviderConnectionHandle) {
// We have the SignalConnection lock so we're allowed to set this.
finalSignalProviderConnectionHandle.destroy();
clearConnectionHandle(finalSignalProviderConnectionHandle);
}
LOGGER.debug("Announcing connection changed (successing the 'disconnectFuture')");
// TODO: do we execute these observers while holding locks?
connectionHandle.getDisconnectFuture().setSuccess(connectionHandle);
connectionChangedEvent.notifyObservers(connectionHandle, Boolean.FALSE);
}
private final Observer updateStateOnNewClientIdReceived = new Observer() {
@Override
public void notify(Object sender, String newClientId) {
SignalProviderConnectionHandle connection = (SignalProviderConnectionHandle) sender;
Asserts.assertTrue(!connection.isDestroyed(), "bad state?");
clientId = newClientId;
originalClientId = newClientId;
if (presence != null) {
presence.setAddress(new ClientAddress(newClientId));
}
}
@Override
public String toString() {
return "updateStateOnNewClientIdReceived";
}
};
private void handleCommands(SignalProviderConnectionHandle connection, List commands) {
for (Command command : commands) {
handleCommand(connection, command);
}
}
private final Observer updateVersionsOnVersionChanged = new Observer() {
@Override
public void notify(Object sender, VersionMapEntry version) {
SignalProviderConnectionHandle connection = (SignalProviderConnectionHandle) sender;
Asserts.assertTrue(!connection.isDestroyed(), "The connection is not active?!?");
versions.put(version.getKey(), version.getValue());
}
@Override
public String toString() {
return "onVersionChanged";
}
};
private void handleCommand(SignalProviderConnectionHandle connection, Command command) {
commandReceivedEvent.notifyObservers(connection, command);
if (command.getVersion() != null && command.getVersion().getValue() > 0) {
newVersionEvent.notifyObservers(connection, command.getVersion());
}
if (command instanceof ConnectCommand) {
handleConnectCommand(connection, (ConnectCommand) command);
} else if (command instanceof DisconnectCommand) {
handleDisconnectCommand(connection, (DisconnectCommand) command);
} else if (command instanceof SubscriptionCompleteCommand) {
handleSubscriptionCompleteCommand(connection, (SubscriptionCompleteCommand) command);
} else if (command instanceof SignalCommand) {
handleSignalCommand(connection, (SignalCommand) command);
} else if (command instanceof PresenceCommand) {
handlePresenceCommand(connection, (PresenceCommand) command);
} else if (command instanceof SignalVerificationCommand) {
handleSignalVerificationCommand(connection, (SignalVerificationCommand) command);
} else if (command instanceof NoopCommand) {
LOGGER.debug("Received NoopCommand");
} else {
LOGGER.warn("Unrecognized command: " + command.getClass().getSimpleName());
}
}
/*
* This method allows us to decouple connection.connect() from provider.connect() for
* cases when we have been notified by the connection that it has a successful connection.
*/
private ObservableFuture writeConnectCommandAsyncWithTimeoutBakedIn(ConnectionHandle connectionHandle) {
return writeConnectCommandAsyncWithTimeoutBakedIn(connectionHandle, clientId, versions);
}
/**
* This future will self cancel if the timeout elapses.
*/
private ObservableFuture writeConnectCommandAsyncWithTimeoutBakedIn(ConnectionHandle connectionHandle, String clientId, Map versions) {
return importantTaskExecutor.enqueue(null,
new ConnectCommandTask(SocketSignalProvider.this.signalConnection, connectionHandle, clientId, versions, presence),
FutureDateUtil.inFuture(SocketSignalProvider.this.signalConnection.getConnectTimeoutSeconds(), TimeUnit.SECONDS));
}
private void notifyConnected(ConnectionHandle connectionHandle, boolean connected) {
synchronized (PROVIDER_CONNECTION_HANDLE_LOCK) {
ensureCorrectConnectionHandle(connectionHandle);
// If the state has changed then notify
connectionChangedEvent.notifyObservers(connectionHandle, connected);
}
}
public synchronized ConnectionState getConnectionState() {
synchronized (signalConnection) {
ConnectionState signalConnectionState = signalConnection.getConnectionState();
synchronized (PROVIDER_CONNECTION_HANDLE_LOCK) {
final SignalProviderConnectionHandle connectionHandle = getUnchangingConnectionHandle();
if (connectionHandle == null) {
return ConnectionState.DISCONNECTED;
}
switch (signalConnectionState) {
case CONNECTING:
return ConnectionState.CONNECTING;
case CONNECTED:
if (connectionHandle.finishedActionConnect) {
return ConnectionState.AUTHENTICATED;
}
return ConnectionState.CONNECTED;
case DISCONNECTING:
return ConnectionState.DISCONNECTING;
case DISCONNECTED:
return ConnectionState.DISCONNECTED;
default:
throw new IllegalStateException("Odd unexpected state");
}
}
}
}
public boolean isConnected() {
ConnectionState state = getConnectionState();
return state == ConnectionState.CONNECTED || state == ConnectionState.AUTHENTICATED;
}
private void tearDownConnection(SignalProviderConnectionHandle connectionHandle, final ObservableFuture connectFuture) {
connectionHandle.destroy();
final ConnectionHandle finalConnectionHandle = getUnchangingConnectionHandle();
if (connectionHandle != finalConnectionHandle) {
// not the same, just shrug it off.
LOGGER.error("The connectionHandle %s did not match %s so not doing a clear.");
return;
} else {
synchronized (connectionHandle) {
LOGGER.debug(String.format("2 Set connectionHandle to null! %s", Thread.currentThread()));
clearConnectionHandle(connectionHandle);
}
}
if (connectFuture != null) {
synchronized (connectFuture) {
clearConnectFuture(connectFuture);
}
}
// announce we destroyed it.
connectionHandle.getDisconnectFuture().setSuccess(connectionHandle);
}
private void sendConnectCommand(final ObservableFuture finalConnectFuture, final SignalProviderConnectionHandle connectionHandle, final String clientId, Map versions) {
// send in the connect command (will queue up and execute in our signalProvider.executor
// so we must be sure not to block (it's the current thread we're on right now!)).
ObservableFuture sendConnectCommandFuture
= writeConnectCommandAsyncWithTimeoutBakedIn(connectionHandle, clientId, versions);
sendConnectCommandFuture.addObserver(new HandleConnectCommandIfDoneObserver(connectionHandle));
sendConnectCommandFuture.addObserver(new CascadeSuccessToFuture(connectionHandle, finalConnectFuture));
// /**
// * Because the sendConnectCommandFuture will self-timeout, we don't have to do a block/timeout
// * of our own.
// *
// * Regardless of success/failure we will be in the "signalProvider" thread.
// */
// sendConnectCommandFuture.addObserver(new Observer>() {
//
// // WE ARE IN THE "SignalProvider.executor" THREAD if that executor is "simple" then
// // we're in the connection thread or the scheduler thread.
// @Override
// public void notify(Object sender, ObservableFuture future) {
// if (finalConnectFuture.isCancelled()) {
// LOGGER.warn("Our connectFuture was cancelled!");
// return;
// }
//
// // the "sender" is the "task"
// // we are in the "signalProvider.executor" thread.
// synchronized (SocketSignalProvider.this) {
// if (future.isSuccess()) {
// try {
// final ConnectCommand connectCommand = future.getResult();
//
// // THE handleConnectCommand METHOD WILL THROW THE APPROPRIATE EVENTS.
// handleConnectCommand(connectionHandle, connectCommand);
//
// LOGGER.debug("Success finalConnectFuture!");
// connectionHandle.finishedActionConnect = true;
// } finally {
//
// }
// } else {
// // NOTE: what thread are we in? is this a deadlock?
// // who's job is it to tear down the connection?
// NestedObservableFuture.syncState(future, finalConnectFuture, connectionHandle);
//
// // TODO: what happens if the ConnectCommand times out? Who's job is it to tear down?
// disconnect(true);
// }
// }
// }
//
// @Override
// public String toString() {
// return "sendConnectCommandFuture";
// }
// });
}
private SignalProviderConnectionHandle createAndSetActiveSignalProviderConnection() {
SignalProviderConnectionHandle connection = newConnectionHandle();
synchronized (connection) {
this.setConnectionHandle(connection);
// /**
// * We need this in case it does not connect successfully.
// */
// connection.getDisconnectFuture().addObserver(
// new DifferentExecutorObserverAdapter>(executor,
// new ThreadSafeObserverAdapter>(
// new ActiveConnectionObserverAdapter>(
// new Observer>() {
// @Override
// public void notify(Object sender, ObservableFuture item) {
// ConnectionHandle connectionHandle = (ConnectionHandle) sender;
//
// synchronized (SocketSignalProvider.this) {
// final ConnectionHandle finalConnectionHandle = getUnchangingConnectionHandle();
//
// synchronized (finalConnectionHandle) {
// if (finalConnectionHandle == connectionHandle) {
// executeDisconnect(finalConnectionHandle);
// }
// }
// }
// }
//
// @Override
// public String toString() {
// return "selfHealOnConnectionDisconnect";
// }
// }))));
}
return connection;
}
private NestedObservableFuture createSelfHealingConnectFuture(ConnectionHandle connectionHandle) {
NestedObservableFuture future = new NestedObservableFuture(connectionHandle) {
@Override
public String toString() {
return "connectFuture";
}
};
future.addObserver(new DebugObserver());
future.addObserver(resetConnectFutureIfMatchingObserver);
future.addObserver(notifyConnectedOnFinishObserver);
return future;
}
private Observer> resetConnectFutureIfMatchingObserver = new Observer>() {
@Override
public void notify(Object sender, ObservableFuture future) {
final ObservableFuture connectFuture = getUnchangingConnectFuture();
if (connectFuture == null) {
return;
}
synchronized (connectFuture) {
if (future == connectFuture) {
clearConnectFuture(future);
}
}
}
};
private Observer> notifyConnectedOnFinishObserver = new Observer>() {
@Override
public void notify(Object sender, ObservableFuture future) {
ConnectionHandle connectionHandle = (SignalProviderConnectionHandle) sender;
if (future.isSuccess()) {
Asserts.assertTrue(sender == future.getResult(), "The connections should agree.");
boolean connected = !connectionHandle.isDestroyed();
notifyConnected(connectionHandle, connected);
} else {
// Do we need to throw the not connected event? It didn't change?
// notifyConnected(connectionHandle, false);
}
}
};
// TODO: who calls this because it could be a deadlock
// TODO: THIS METHOD HAS SERIOUS PROBLEMS AND NEEDS TESTING
public ObservableFuture resetDisconnectAndConnect() {
synchronized (signalConnection) {
synchronized (PROVIDER_CONNECTION_HANDLE_LOCK) {
final ConnectionHandle connectionHandle = getUnchangingConnectionHandle();
if (connectionHandle != null) {
synchronized (connectionHandle) {
return executeResetDisconnectAndConnect();
}
} else {
return executeResetDisconnectAndConnect();
}
}
}
}
private ObservableFuture executeResetDisconnectAndConnect() {
final String c = clientId = originalClientId = StringUtil.EMPTY_STRING;
// TODO: I think this is a bug. The local hashmap being cleared doesnt really do anything on disk.
versions.clear();
synchronized (slidingWindows) {
for (String key : slidingWindows.keySet()) {
slidingWindows.get(key).reset();
}
}
// todo: is this the right event?
newClientIdReceivedEvent.notifyObservers(getUnchangingConnectionHandle(), c);
return signalConnection.reconnect();
}
@Override
public ObservableFuture ping() {
if (signalConnection.getConnectionState() == ConnectionState.CONNECTED) {
return signalConnection.ping();
} else {
return new FakeFailingObservableFuture(this, new Exception("Not connected"));
}
}
public SignalConnection getSignalConnection() {
return signalConnection;
}
private void handleConnectCommand(SignalProviderConnectionHandle connectionHandle, ConnectCommand command) {
// we are in the "Channel" thread.
// We already have the SignalConnection and CONNECTION_BEING_CHANGED locks right now.
// it's illegal order-of-operations to synchronize on "this"
synchronized (connectionHandle) {
if (LOGGER.isDebugEnabled())
LOGGER.debug(String.format("handleConnectCommand(%s, %s)", connectionHandle, command));
boolean newClientId = false;
if (command.isSuccessful()) {
// copy it over for stale checking
originalClientId = clientId;
clientId = command.getClientId();
if (!StringUtil.equals(clientId, originalClientId)) {
if (LOGGER.isDebugEnabled()) {
LOGGER.debug("Received a new client id: " + clientId);
}
newClientId = true;
}
if (newClientId) {
// not the same, lets announce
// announce on a separate thread
newClientIdReceivedEvent.notifyObservers(connectionHandle, clientId);
}
requestBacklog(connectionHandle);
}
}
}
private void requestBacklog(SignalProviderConnectionHandle connectionHandle) {
// we are on the SignalProvider thread, since the events all bootstrap the notify
// our "executor." We can't trust that the connection is still connected.
if (versions != null) {
// kind of cheating i guess.
for (String key : versions.keySet()) {
connectionHandle.write(new BackfillCommand(Collections.singletonList(versions.get(key)), key))
.addObserver((Observer) logIfWriteFailedObserver);
}
}
}
private void handleDisconnectCommand(SignalProviderConnectionHandle connection, DisconnectCommand command) {
synchronized (connection) {
LOGGER.debug("Handling DisconnectCommand");
try {
LOGGER.debug("Disconnecting (with network=false). There should not be any auto reconnect activity now.");
disconnect();
} catch (Exception e) {
LOGGER.error("Error disconnecting", e);
}
if (command.isBan()) {
LOGGER.warn("BANNED by SignalServer! Those jerks!");
}
// If the command has not said 'ban' and 'stop'
if (!command.isStop() && !command.isBan()) {
String host;
int port;
try {
InetSocketAddress address = (InetSocketAddress) this.signalConnection.getAddress();
host = address.getHostName();
port = address.getPort();
} catch (Exception e) {
LOGGER.error("Could not determine host/port: ", e);
return;
}
boolean hostChanged = false;
if (!StringUtil.EMPTY_STRING.equals(command.getHost())) {
host = command.getHost();
hostChanged = true;
}
boolean portChanged = false;
if (command.getPort() > 0) {
port = command.getPort();
portChanged = true;
}
if (hostChanged || portChanged) {
this.signalConnection.setAddress(new InetSocketAddress(host, port));
LOGGER.warn(String.format("We are going to connect again %d seconds from now to host: %s and port: %s", command.getReconnectDelay(), host, port));
} else {
LOGGER.debug(String.format("We are going to connect again %d seconds from now", command.getReconnectDelay()));
}
scheduler.schedule(clientId, FutureDateUtil.inFuture(command.getReconnectDelay(), TimeUnit.SECONDS));
}
}
}
private final Observer onScheduleComplete = new Observer() {
@Override
public void notify(Object sender, String clientId) {
if (!StringUtil.equals(SocketSignalProvider.this.clientId, clientId)) {
// must have been for a different request.
return;
} else if (getConnectionState() == ConnectionState.CONNECTED) {
LOGGER.debug("It seems that the connectionState is connected already. Aborting this reconnect attempt.");
return;
}
LOGGER.debug("Executing the connect that was requested by the server.");
try {
connect(clientId, versions, presence).addObserver(retryOnFailureObserver);
} catch (Exception e) {
LOGGER.error("Crash on connect. We hope that the reconnectStrategy will do us good.", e);
}
}
};
private Observer> retryOnFailureObserver = new Observer>() {
@Override
public void notify(Object sender, ObservableFuture item) {
if (item.isSuccess() && item.getResult() != null && !item.getResult().getDisconnectFuture().isDone()) {
LOGGER.warn("The future was successful! We reconnected just fine.");
return;
} else if (item.isCancelled()) {
LOGGER.warn("The future was cancelled, so this must mean it was forcibly disconnected! Not retrying.");
return;
}
// TODO: how do we handle this case? It's considered an 'initial connect' so it can't be retried.
connect(clientId, versions, presence).addObserver(this);
}
};
private void handlePresenceCommand(SignalProviderConnectionHandle connection, PresenceCommand command) {
if (LOGGER.isDebugEnabled()) {
LOGGER.debug("Handling PresenceCommand " + command.getPresence());
}
boolean selfPresenceExists = false;
List presenceList = command.getPresence();
if (presenceList == null) {
if (LOGGER.isDebugEnabled()) {
LOGGER.debug("Nothing is known about us or our peers");
}
selfPresenceExists = false;
} else {
for (Presence presence : command.getPresence()) {
if (clientId.equals(presence.getAddress().getClientId())) {
selfPresenceExists = true;
}
if (presence.getCategory().equals(PresenceCategory.Phone)) {
presenceReceivedEvent.notifyObservers(connection, presence.getConnected());
}
}
}
if (!selfPresenceExists) {
if (presence != null) {
if (LOGGER.isDebugEnabled()) {
LOGGER.debug("Reidentifying our presence object");
}
connection.write(new PresenceCommand(Collections.singletonList(presence)));
} else {
LOGGER.debug("Our presence object was empty, so we didn't share it");
}
}
}
private void handleSignalCommand(SignalProviderConnectionHandle connection, SignalCommand command) {
LOGGER.debug("Handling SignalCommand");
// Distribute the command and the raw signal to give client's flexibility regarding what data they need
signalCommandReceivedEvent.notifyObservers(connection, Collections.singletonList(command));
signalReceivedEvent.notifyObservers(connection, Collections.singletonList(command.getSignal()));
}
private void handleSubscriptionCompleteCommand(SignalProviderConnectionHandle connection, SubscriptionCompleteCommand command) {
if (LOGGER.isDebugEnabled()) {
LOGGER.debug("Handling SubscriptionCompleteCommand " + command.toString());
}
if (!sendPresence(presence)) {
LOGGER.warn("Tried and failed to send presence");
}
// Asserts.assertTrue(authenticationKeyChain.isAuthenticated(clientId, command.getSubscriptionId()), "This subscriptionId was already authenticated!");
// // WARNING: We don't know which clientId this really came in for..
// authenticationKeyChain.add(clientId, command.getSubscriptionId());
subscriptionCompleteReceivedEvent.notifyObservers(this, command);
}
private boolean sendPresence(Presence presence) {
if (presence != null) {
// Set our clientId in case its not already there
presence.setAddress(new ClientAddress(clientId));
// TODO handle send future
signalConnection.send(new PresenceCommand(Collections.singletonList(presence)));
return true;
} else {
return false;
}
}
private void handleSignalVerificationCommand(SignalProviderConnectionHandle connection, SignalVerificationCommand command) {
LOGGER.debug("Processing SignalVerificationCommand " + command.toString());
signalVerificationReceivedEvent.notifyObservers(this, null);
}
private final Observer signalHoleObserver = new Observer() {
@Override
public void notify(Object sender, SlidingWindow.HoleRange hole) {
LOGGER.debug("Signal hole detected, requesting backfill for " + hole.toString());
signalConnection.send(new BackfillCommand(hole.getRange(), hole.getKey()));
}
};
private final Observer> packetReleasedObserver = new Observer>() {
@Override
public void notify(Object sender, List commands) {
LOGGER.warn(commands.size() + " packets released due to timeout, leaving a hole.");
// TODO: how do we know this is the right connection???
synchronized (SocketSignalProvider.this) {
synchronized (signalConnection) {
synchronized (PROVIDER_CONNECTION_HANDLE_LOCK) {
final SignalProviderConnectionHandle finalConnectionHandle = getUnchangingConnectionHandle();
handleCommands(finalConnectionHandle, commands);
}
}
}
}
};
public ImportantTaskExecutor getImportantTaskExecutor() {
return importantTaskExecutor;
}
// private static class StateBundle {
// private long connectionId;
// private String clientId;
// }
private final Observer logIfWriteFailedObserver = new Observer() {
@Override
public void notify(Object sender, ObservableFuture item) {
if (!item.isSuccess()) {
LOGGER.error("FAILED TO WRITE TO CHANNEL! " + item);
}
}
};
@Override
protected synchronized ObservableFuture disconnect(ConnectionHandle connectionHandle, boolean causedByNetwork) {
synchronized (signalConnection) {
return super.disconnect(connectionHandle, causedByNetwork);
}
}
@Override
protected void accessConnectFuture() {
ensureLock(signalConnection);
super.accessConnectFuture();
}
@Override
protected void accessConnectionHandle() {
ensureLock(signalConnection);
super.accessConnectionHandle();
}
@Override
protected void clearConnectionHandle(ConnectionHandle finalConnectionHandle) {
ensureLock(signalConnection);
super.clearConnectionHandle(finalConnectionHandle);
}
private void ensureState(ConnectionState state) {
accessConnectionState();
final ConnectionState existingState = getConnectionState();
Asserts.assertTrue(existingState == state, String.format("The state was supposed to be %s but was %s", state, existingState));
}
private void accessConnectionState() {
ensureLock(SocketSignalProvider.this);
ensureLock(signalConnection);
}
@Override
protected void onDestroy() {
}
/**
* This class is attached to the signalConnection.connect() future. It will execute in our executor in a
* thread-safe way.
*/
private class UpdateStateOnConnectCompleteObserver implements Observer> {
final ObservableFuture finalConnectFuture;
final SignalProviderConnectionHandle finalSignalProviderConnectionHandle;
final String finalClientId;
private UpdateStateOnConnectCompleteObserver(ObservableFuture finalConnectFuture, SignalProviderConnectionHandle finalSignalProviderConnectionHandle, String finalClientId) {
this.finalConnectFuture = finalConnectFuture;
this.finalSignalProviderConnectionHandle = finalSignalProviderConnectionHandle;
this.finalClientId = finalClientId;
}
@Override
public void notify(Object sender, ObservableFuture signalConnectionFuture) {
synchronized (PROVIDER_CONNECTION_HANDLE_LOCK) {
synchronized (finalConnectFuture) {
if (finalConnectFuture.isCancelled()) {
synchronized (finalSignalProviderConnectionHandle) {
LOGGER.warn("Our connection was cancelled. " + finalConnectFuture);
tearDownConnection(finalSignalProviderConnectionHandle, finalConnectFuture);
// oh shit the returned future was cancelled!
Asserts.assertTrue(getConnectFuture() != finalConnectFuture, "The futures shouldn't be the same!");
return;
}
}
final ConnectionHandle c = getUnchangingConnectionHandle();
synchronized (finalSignalProviderConnectionHandle) {
synchronized (c) {
if (signalConnectionFuture.isCancelled()) {
LOGGER.warn("Our connection was cancelled. " + signalConnectionFuture);
tearDownConnection(finalSignalProviderConnectionHandle, finalConnectFuture);
finalConnectFuture.setFailure(new Exception(String.format("The connectionHandles didn't match up. Was %s expected %s", c, finalSignalProviderConnectionHandle)));
return;
} else if (signalConnectionFuture.isFailed()) {
LOGGER.warn("Our connection was failed. " + signalConnectionFuture);
tearDownConnection(finalSignalProviderConnectionHandle, finalConnectFuture);
finalConnectFuture.setFailure(signalConnectionFuture.getCause());
return;
} else if (finalSignalProviderConnectionHandle != c) {
tearDownConnection(finalSignalProviderConnectionHandle, finalConnectFuture);
LOGGER.error(String.format("The connectionHandles didn't match up. Was %s expected %s", c, finalSignalProviderConnectionHandle));
finalConnectFuture.setFailure(new Exception(String.format("The connectionHandles didn't match up. Was %s expected %s", c, finalSignalProviderConnectionHandle)));
return;
}
}
final ConnectionHandle connectionHandle = signalConnectionFuture.getResult();
// set the currently active connection's internal connection.
finalSignalProviderConnectionHandle.setConnectionHandle(connectionHandle);
sendConnectCommand(finalConnectFuture, finalSignalProviderConnectionHandle, finalClientId, versions);
}
}
}
}
}
private static class CascadeSuccessToFuture implements Observer> {
final ConnectionHandle connectionHandle;
final ObservableFuture future;
private CascadeSuccessToFuture(ConnectionHandle connectionHandle, ObservableFuture future) {
this.connectionHandle = connectionHandle;
this.future = future;
}
@Override
public void notify(Object sender, ObservableFuture otherFuture) {
// synchronized (signalConnection) {
if (otherFuture.isSuccess()) {
this.future.setSuccess(connectionHandle);
} else {
NestedObservableFuture.syncState(otherFuture, this.future, connectionHandle);
}
// }
}
}
private class HandleConnectCommandIfDoneObserver implements Observer> {
private final SignalProviderConnectionHandle finalConnectionHandle;
private HandleConnectCommandIfDoneObserver(SignalProviderConnectionHandle finalConnectionHandle) {
this.finalConnectionHandle = finalConnectionHandle;
}
@Override
public void notify(Object sender, ObservableFuture future) {
if (!future.isSuccess()) {
// shit!
return;
}
ConnectCommand connectCommand = future.getResult();
finalConnectionHandle.finishedActionConnect = true;
handleConnectCommand(finalConnectionHandle, connectCommand);
}
}
private class ThreadSafeObserverAdapter implements Observer {
final Observer observer;
private ThreadSafeObserverAdapter(Observer observer) {
this.observer = observer;
}
@Override
public void notify(Object sender, T item) {
synchronized (SocketSignalProvider.this) {
synchronized (signalConnection) {
observer.notify(sender, item);
}
}
}
@Override
public String toString() {
return String.format("[t: %s]", observer.toString());
}
}
private class ActiveConnectionObserverAdapter implements Observer {
private final Observer observer;
private ActiveConnectionObserverAdapter(Observer observer) {
this.observer = observer;
}
@Override
public void notify(Object sender, T data) {
ConnectionHandle connectionHandle = (ConnectionHandle) sender;
synchronized (PROVIDER_CONNECTION_HANDLE_LOCK) {
final SignalProviderConnectionHandle signalProviderConnectionHandle = getUnchangingConnectionHandle();
if (signalProviderConnectionHandle == null) {
LOGGER.error(String.format("%s: The signalProviderConnection is null, so it must be inactive. Quitting.", this));
return;
}
synchronized (signalProviderConnectionHandle) {
if (signalProviderConnectionHandle.isDestroyed()) {
LOGGER.error(String.format("%s: The signalProviderConnection is not active. Quitting.", this));
return;
} else if (!signalProviderConnectionHandle.isFor(connectionHandle)) {
LOGGER.error(String.format("%s: The signalProviderConnection is not for the current connection. Quitting", this));
return;
}
// during this observer.notify call you are guaranteed that the currentConnectionHandle cannot change
// underneath you.
observer.notify(signalProviderConnectionHandle, data);
}
}
}
@Override
public String toString() {
return String.format("[a: %s]", observer);
}
}
}
© 2015 - 2025 Weber Informatics LLC | Privacy Policy