/* Copyright (c) 2011 Danish Maritime Authority.
*
* 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 net.maritimecloud.mms.server.connection.client;
import static java.util.Objects.requireNonNull;
import java.util.Queue;
import java.util.concurrent.BlockingQueue;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.Executor;
import java.util.concurrent.LinkedBlockingQueue;
import java.util.concurrent.locks.ReentrantLock;
import net.maritimecloud.internal.mms.messages.spi.MmsMessage;
import net.maritimecloud.message.Message;
import net.maritimecloud.mms.server.connection.transport.ServerTransport;
import net.maritimecloud.util.Binary;
import org.cakeframework.container.concurrent.ThreadManager;
/**
*
* @author Kasper Nielsen
*/
public class Session {
/** The client that this session is attached to. */
private final Client client;
/** A context map used for attaching state to the session. */
private final ConcurrentHashMap<String, Object> contextMap = new ConcurrentHashMap<>();
private long latestMessageIdAckedByRemote /* = 0 */;
long latestMessageIdReceivedByRemote;
private long nextMessageIdToSend = 1;
/** A queue of messages that have not yet been acked. */
private final Queue<SessionMessageFuture> unAckedMessages = new LinkedBlockingQueue<>();
/**
* A executor that is used to asynchronous write messages. The reason is websocket.asyncwrite will sometime call
* into @onClose on the transport. onClose will try to acquire a write lock, to properly lock it. However, a receive
* function (also holding a read lock) might be try to do the same thing. So instead of running into problems. We
* let another thread write the message to the websocket. Avoid calling recursively into close.writeLock()
*/
private final Executor sendExecutor;
/** The unique session id. */
private final Binary sessionId = Binary.random(32);
/** A listener of incoming messages */
private final Session.Listener sessionMessageListener;
/** The system time of the last received message. */
private volatile long timeOfLastReceivedMessage = System.nanoTime();
/** The transport to send messages on. Might be null, for example, if the remote client is disconnected. */
private Writer writer;
Session(Client client) {
this.client = requireNonNull(client);
this.sessionMessageListener = requireNonNull(client.clientManager.mmsServer.getService(Session.Listener.class));
ThreadManager tm = client.clientManager.mmsServer.getService(ThreadManager.class);
this.sendExecutor = tm.getExecutor("mms");
}
/** Invoked whenever the session is killed permanently. Makes sure all outstanding writes are marked as failed. */
void disconnectedWithWriteLock(boolean destroy) {
this.writer = null;
// loeb igennem alle, marker dem som doede
// og toem alle koere
}
// called while readlocked on the client.
// might be called concurrently, so we lock it for now, but might find another solution in the future
SessionMessageFuture enqueueMessageWithReadLock(Message msg) {
// We need to have another thread write the message. The problem is that even though
MmsMessage m = new MmsMessage(msg);
synchronized (unAckedMessages) {
SessionMessageFuture smf = new SessionMessageFuture(m, nextMessageIdToSend++);
m.setMessageId(smf.messageId);
m.setLatestReceivedId(latestMessageIdReceivedByRemote);
unAckedMessages.add(smf);
// only write if connected, otherwise leave in notAcked queue
if (writer != null) {
writer.send(smf, sendExecutor);
}
return smf;
}
}
/**
* @return the client
*/
public Client getClient() {
return client;
}
public Object getContext(String key) {
return contextMap.get(key);
}
/**
* Returns the session id of this session.
*
* @return the session id of this session
*/
public Binary getSessionId() {
return sessionId;
}
/**
* @return the timeOfLastReceivedMessage
*/
public long getTimeOfLastReceivedMessage() {
return timeOfLastReceivedMessage;
}
/**
* Invoked whenever the underlying transport has been successfully connected. Takes care of resending all messages.
*
* @param transport
*/
void onConnectWithWriteLock(ServerTransport transport, long msgId) {
// Start by removing messages that already been acked according to msgId
removeAckedExclusively(msgId);
writer = new Writer(transport);
// Technically it is okay to send messages directly but we should probably send them async as well
for (SessionMessageFuture f : unAckedMessages) {
transport.sendMessage(f.message);
nextMessageIdToSend = f.messageId + 1;
}
}
/**
* Receives a message while connected. This is always invoked one at a time.
*
* @param message
* the message that was received
*/
void onMessageWithReadLock(MmsMessage message) {
timeOfLastReceivedMessage = System.nanoTime();
latestMessageIdReceivedByRemote = message.getMessageId();
latestMessageIdAckedByRemote = message.getLatestReceivedId();
// So hmm, the two above why are they above this line.
// Mainly because the listener will most likely send a reply message
// And it will strange that latestReceivedMessageId has not been updated
// to include the latest received message (the initiating message of the reply message)
sessionMessageListener.onMessage(this, message.getM());
removeAckedExclusively(latestMessageIdAckedByRemote);
}
private void removeAckedExclusively(long id) {
for (;;) {
SessionMessageFuture f = unAckedMessages.peek();
if (f == null || f.messageId > id) {
return;
}
f.protocolAcked().complete(null);
unAckedMessages.poll();// remove peeked element
}
}
public SessionMessageFuture send(Message message) {
// delegate to client manager to make sure we have the latest and greatest
return client.sendMessage(this, message);
}
public interface Listener {
/**
*
* <p>
* The reason it is marked acked and we do not supply some kind og completion token is that messages cannot be
* acked in a random order. Before message x can be acked, message x-1 must already have been acked.
*
* @param session
* @param message
*/
// is under readlock, is invoked in order one at a time
void onMessage(Session session, Message message);
}
static class Writer implements Runnable {
private final ReentrantLock executorLock = new ReentrantLock();
private final BlockingQueue<SessionMessageFuture> q = new LinkedBlockingQueue<>();
final ServerTransport transport;
Writer(ServerTransport transport) {
this.transport = requireNonNull(transport);
}
/** {@inheritDoc} */
public void run() {
// We need retry check for extra elements one more time, if we have successfully polled elements
// This is because of a rare race condition where
// T1[Executor Thread] : q.poll() returns null
// T2 : submits an message to be send and queues this runnable, T3 picks it up.
// T3[Executor Thread] can not obtain the lock because T1 has not yet released it and returns emptyhanded
boolean sholdRetry;
do {
sholdRetry = false;
if (executorLock.tryLock()) {
try {
SessionMessageFuture s = q.poll();
while (s != null) {
sholdRetry = true;
try {
transport.sendMessage(s.message);
} catch (Exception e) {
e.printStackTrace();
}
s = q.poll();
}
} finally {
executorLock.unlock();
}
}
} while (sholdRetry);
}
void send(SessionMessageFuture f, Executor e) {
q.add(f);
e.execute(this);
}
}
}