ws.wamp.jawampa.connection.QueueingConnectionController Maven / Gradle / Ivy
/*
* Copyright 2015 Matthias Einwag
*
* The jawampa authors license this file to you 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 ws.wamp.jawampa.connection;
import java.util.ArrayDeque;
import java.util.Deque;
import java.util.concurrent.RejectedExecutionException;
import java.util.concurrent.ScheduledExecutorService;
import ws.wamp.jawampa.ApplicationError;
import ws.wamp.jawampa.WampMessages.WampMessage;
import ws.wamp.jawampa.WampSerialization;
public class QueueingConnectionController implements IConnectionController {
static class QueuedMessage {
public final WampMessage message;
public final IWampConnectionPromise promise;
public QueuedMessage(WampMessage message, IWampConnectionPromise promise) {
this.message = message;
this.promise = promise;
}
}
/** Possible states while closing the connection */
enum CloseStatus {
/** Close was not issued */
None,
/** Connection should be closed at the next possible point of time */
CloseNow,
/** Connection should be closed after all already queued messages have been sent */
CloseAfterRemaining,
/** Close was issued but not yet acknowledged */
CloseSent,
/** Close is acknowledged */
Closed
}
final ICompletionCallback messageSentHandler = new ICompletionCallback () {
@Override
public void onCompletion(final IWampConnectionFuture future) {
tryScheduleAction(new Runnable() {
@Override
public void run() {
// Dequeue the first element of the queue.
// Queue might be empty if closed in between
QueuedMessage first = queuedMessages.poll();
if (future.isSuccess())
first.promise.fulfill(null);
else {
first.promise.reject(future.error());
}
/** Whether to close after this call */
boolean sendClose =
(closeStatus == CloseStatus.CloseNow) ||
(closeStatus == CloseStatus.CloseAfterRemaining && queuedMessages.size() == 0);
if (sendClose) {
// Close the connection now
closeStatus = CloseStatus.CloseSent;
connection.close(true, connectionClosedPromise);
} else if (queuedMessages.size() >= 1) {
// There's more to send
WampMessage nextMessage = queuedMessages.peek().message;
messageSentPromise.reset(messageSentHandler, null);
connection.sendMessage(nextMessage, messageSentPromise);
}
}
});
}
};
final ICompletionCallback connectionClosedHandler = new ICompletionCallback () {
@Override
public void onCompletion(final IWampConnectionFuture future) {
tryScheduleAction(new Runnable() {
@Override
public void run() {
assert (closeStatus == CloseStatus.CloseSent);
// The connection is now finally closed
closeStatus = CloseStatus.Closed;
// Complete all pending sends
while (queuedMessages.size() > 0) {
QueuedMessage nextMessage = queuedMessages.remove();
nextMessage.promise.reject(
new ApplicationError(ApplicationError.TRANSPORT_CLOSED));
// This could theoretically cause side effects.
// However it is not valid to call anything on the controller after
// close() anyway, so it isn't valid.
}
// Forward the result
if (future.isSuccess()) queuedClose.fulfill(null);
else queuedClose.reject(future.error());
queuedClose = null;
}
});
}
};
/**
* Promise that will be fulfilled when the underlying connection
* has sent a single message. The promise will be reused for
* all messages that will be sent through this controller.
*/
final WampConnectionPromise messageSentPromise =
new WampConnectionPromise(messageSentHandler, null);
/**
* Promise that will be fulfilled when the connection was closed
* and the close was acknowledged by the underlying connection.
*/
final WampConnectionPromise connectionClosedPromise =
new WampConnectionPromise(connectionClosedHandler, null);
/** The scheduler on which all state transitions will run */
final ScheduledExecutorService scheduler;
/** The wrapped connection object. Must be injected later due to Router design */
IWampConnection connection;
/** The wrapped listener object */
final IWampConnectionListener connectionListener;
/** Queued messages */
Deque queuedMessages = new ArrayDeque();
/** Holds the promise that will be fulfilled when the connection was closed */
IWampConnectionPromise queuedClose = null;
/** Whether to forward incoming messages or not */
boolean forwardIncoming = true;
CloseStatus closeStatus = CloseStatus.None;
public QueueingConnectionController(ScheduledExecutorService scheduler,
IWampConnectionListener connectionListener) {
this.scheduler = scheduler;
this.connectionListener = connectionListener;
}
@Override
public IWampConnectionListener connectionListener() {
return connectionListener;
}
@Override
public IWampConnection connection() {
return connection;
}
@Override
public void setConnection(IWampConnection connection) {
this.connection = connection;
}
/**
* Tries to schedule a runnable on the underlying executor.
* Rejected executions will be suppressed.
* This is useful for cases when the clients EventLoop is shut down before
* the EventLoop of the underlying connection.
*
* @param action The action to schedule.
*/
private void tryScheduleAction(Runnable action) {
try {
scheduler.submit(action);
} catch (RejectedExecutionException e) {}
}
// IWampConnection members
@Override
public WampSerialization serialization() {
return connection.serialization();
}
@Override
public boolean isSingleWriteOnly() {
return false;
}
@Override
public void sendMessage(WampMessage message, IWampConnectionPromise promise) {
if (closeStatus != CloseStatus.None)
throw new IllegalStateException("close() was already called");
queuedMessages.add(new QueuedMessage(message, promise));
// Check if there is already a send in progress
if (queuedMessages.size() == 1) {
// We are first in queue. Send immediately
messageSentPromise.reset(messageSentHandler, null);
connection.sendMessage(message, messageSentPromise);
}
}
@Override
public void close(boolean sendRemaining, IWampConnectionPromise promise) {
if (closeStatus != CloseStatus.None)
throw new IllegalStateException("close() was already called");
// Mark as closed. No other actions allowed after that
if (sendRemaining) closeStatus = CloseStatus.CloseAfterRemaining;
else closeStatus = CloseStatus.CloseNow;
queuedClose = promise;
// Avoid forwarding of new incoming messages
forwardIncoming = false;
if (queuedMessages.size() == 0) {
// Can immediately start to close
closeStatus = CloseStatus.CloseSent;
connection.close(true, connectionClosedPromise);
}
}
// IWampConnectionListener methods
@Override
public void transportClosed() {
tryScheduleAction(new Runnable() {
@Override
public void run() {
// Avoid forwarding more than once
if (!forwardIncoming) return;
forwardIncoming = false;
connectionListener.transportClosed();
}
});
}
@Override
public void transportError(final Throwable cause) {
tryScheduleAction(new Runnable() {
@Override
public void run() {
// Avoid forwarding more than once
if (!forwardIncoming) return;
forwardIncoming = false;
connectionListener.transportError(cause);
}
});
}
@Override
public void messageReceived(final WampMessage message) {
tryScheduleAction(new Runnable() {
@Override
public void run() {
// Drop messages that arrive after close
if (!forwardIncoming) return;
connectionListener.messageReceived(message);
}
});
}
}
© 2015 - 2025 Weber Informatics LLC | Privacy Policy