/* 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.internal.mms.client.connection.session; import static java.util.Objects.requireNonNull; import java.util.AbstractMap; import java.util.LinkedList; import java.util.Map; import java.util.Map.Entry; import java.util.concurrent.BlockingQueue; import java.util.concurrent.ConcurrentSkipListMap; import java.util.concurrent.Executor; import java.util.concurrent.Executors; import java.util.concurrent.LinkedBlockingQueue; import java.util.concurrent.ThreadFactory; import java.util.concurrent.locks.Condition; import java.util.concurrent.locks.ReentrantLock; import net.maritimecloud.internal.mms.client.connection.transport.ClientTransport; import net.maritimecloud.internal.mms.messages.spi.MmsMessage; import net.maritimecloud.internal.util.concurrent.CompletableFuture; import net.maritimecloud.message.Message; /** * * @author Kasper Nielsen */ class SessionSender extends Thread { final ReentrantLock lock = new ReentrantLock(); final Session session; volatile long nextMsgId = 1L; final ConcurrentSkipListMap<Long, UnAcked> futures = new ConcurrentSkipListMap<>(); final LinkedList<Msg> messages = new LinkedList<>(); /** Signaled when the state of the connection manager changes. */ final Condition stateChange = lock.newCondition(); final Writer writer = new Writer(); static final Executor e; static { e = Executors.newSingleThreadExecutor(new ThreadFactory() { @Override public Thread newThread(Runnable r) { Thread t = Executors.defaultThreadFactory().newThread(r); t.setDaemon(true); return t; } }); } SessionSender(Session session) { this.session = requireNonNull(session); setDaemon(true); setName("MMSClient-SessionSender"); } void completeAll() { for (UnAcked m : futures.values()) { m.msg.onAck.complete(null); } } void onAck(long id) { lock.lock(); try { Entry<Long, SessionSender.UnAcked> e = futures.firstEntry(); while (e != null && e.getKey() <= id) { e = futures.pollFirstEntry(); e.getValue().msg.onAck.complete(null); e = futures.firstEntry(); } } finally { lock.unlock(); } } void reconnectUnderLock(long lastReceivedId) { onAck(lastReceivedId); nextMsgId = lastReceivedId + 1; for (UnAcked u : futures.descendingMap().values()) { messages.addFirst(u.msg); } futures.clear(); stateChange.signalAll(); } public void run() { while (!session.isClosed) { lock.lock(); try { SessionState s = session.state; if (s instanceof SessionStateConnected) { SessionStateConnected ssc = (SessionStateConnected) s; Msg poll = messages.poll(); if (poll == Msg.SUP) { poll = null; } if (poll != null) { MmsMessage mms = new MmsMessage(poll.message); long id = nextMsgId; mms.setMessageId(id); futures.put(id, new UnAcked(poll, mms)); nextMsgId++; mms.setLatestReceivedId(session.latestReceivedId); writer.send(ssc.transport, mms, e); } } if (!session.isClosed && messages.isEmpty()) { try { stateChange.await(); } catch (InterruptedException probablyShutdown) {} } } finally { lock.unlock(); } } } void send(Message message, CompletableFuture<Void> onAck) { lock.lock(); try { messages.add(new Msg(message, onAck)); stateChange.signalAll(); } finally { lock.unlock(); } } void sup() { lock.lock(); try { messages.add(null); stateChange.signalAll(); } finally { lock.unlock(); } } static class UnAcked { final Msg msg; final MmsMessage mm; UnAcked(Msg msg, MmsMessage mm) { this.msg = msg; this.mm = mm; } } static class Msg { static final Msg SUP = new Msg(); final Message message; final CompletableFuture<Void> onAck; Msg() { this.message = null; this.onAck = null; } Msg(Message message, CompletableFuture<Void> onAck) { this.message = requireNonNull(message); this.onAck = requireNonNull(onAck); } } static class Writer implements Runnable { private final ReentrantLock executorLock = new ReentrantLock(); private final BlockingQueue<Map.Entry<ClientTransport, MmsMessage>> q = new LinkedBlockingQueue<>(); /** {@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 { Entry<ClientTransport, MmsMessage> s = q.poll(); while (s != null) { sholdRetry = true; try { s.getKey().sendMessage(s.getValue()); } catch (Exception e) { e.printStackTrace(); } s = q.poll(); } } finally { executorLock.unlock(); } } } while (sholdRetry); } void send(ClientTransport transport, MmsMessage message, Executor e) { q.add(new AbstractMap.SimpleImmutableEntry<>(transport, message)); e.execute(this); } } }