/*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses 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 com.sun.jini.jeri.internal.mux;
import com.sun.jini.thread.Executor;
import com.sun.jini.thread.GetThreadPoolAction;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.nio.ByteBuffer;
import java.security.AccessController;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Iterator;
import java.util.LinkedList;
import java.util.logging.Level;
import java.util.logging.Logger;
import net.jini.core.constraint.InvocationConstraints;
import net.jini.io.UnsupportedConstraintException;
import net.jini.io.context.AcknowledgmentSource;
import net.jini.jeri.InboundRequest;
import net.jini.jeri.OutboundRequest;
/**
* A Session represents a single session of a multiplexed connection,
* for either client-side and server-side perspective. The particular
* role (CLIENT or SERVER) is indicated at construction time.
*
* @author Sun Microsystems, Inc.
**/
final class Session {
static final int CLIENT = 0;
static final int SERVER = 1;
private static final int IDLE = 0;
private static final int OPEN = 1;
private static final int FINISHED = 2;
private static final int TERMINATED = 3;
private static final String[] stateNames = {
"idle", "open", "finished", "terminated"
};
/**
* pool of threads for executing tasks in system thread group: used for
* I/O (reader and writer) threads and other asynchronous tasks
**/
private static final Executor systemThreadPool =
(Executor) AccessController.doPrivileged(
new GetThreadPoolAction(false));
/** mux logger */
private static final Logger logger =
Logger.getLogger("net.jini.jeri.connection.mux");
private final Mux mux;
private final int sessionID;
private final int role;
private final OutputStream out;
private final InputStream in;
/** lock guarding all mutable instance state (below) */
private final Object sessionLock = new Object();
private boolean sessionDown = false;
private String sessionDownMessage;
private Throwable sessionDownCause;
private int outState;
private int outRation;
private final boolean outRationInfinite;
private boolean partialDeliveryStatus = false;
private int inState;
private int inRation;
private final boolean inRationInfinite;
private int inBufRemaining = 0;
private final LinkedList inBufQueue = new LinkedList();
private int inBufPos = 0;
private boolean inEOF = false;
private boolean inClosed = false;
private boolean fakeOKtoWrite = false; // REMIND
private boolean removeLater = false; // REMIND
private boolean receivedAckRequired = false;
private boolean sentAcknowledgment = false;
private Collection ackListeners = null;
private boolean sentAckRequired = false;
private boolean receivedAcknowledgment = false;
/**
*
*/
Session(Mux mux, int sessionID, int role) {
this.mux = mux;
this.sessionID = sessionID;
this.role = role;
out = new MuxOutputStream();
in = new MuxInputStream();
outState = (role == CLIENT ? IDLE : OPEN);
outRation = mux.initialOutboundRation;
outRationInfinite = (outRation == 0);
inState = (role == CLIENT ? IDLE : OPEN);
inRation = mux.initialInboundRation;
inRationInfinite = (inRation == 0);
}
/**
*
*/
OutboundRequest getOutboundRequest() {
assert role == CLIENT;
return new OutboundRequest() {
public void populateContext(Collection context) {
((MuxClient) mux).populateContext(context);
}
public InvocationConstraints getUnfulfilledConstraints() {
/*
* NYI: We currently have no request-specific hook
* back to the transport implementation, so we must
* depend on OutboundRequest wrapping for this method.
*/
throw new AssertionError();
}
public OutputStream getRequestOutputStream() { return out; }
public InputStream getResponseInputStream() { return in; }
public boolean getDeliveryStatus() {
synchronized (sessionLock) {
return partialDeliveryStatus;
}
}
public void abort() { Session.this.abort(); }
};
}
/**
*
*/
InboundRequest getInboundRequest() {
assert role == SERVER;
return new InboundRequest() {
public void checkPermissions() {
((MuxServer) mux).checkPermissions();
}
public InvocationConstraints
checkConstraints(InvocationConstraints constraints)
throws UnsupportedConstraintException
{
return ((MuxServer) mux).checkConstraints(constraints);
}
public void populateContext(Collection context) {
context.add(new AcknowledgmentSource() {
public boolean addAcknowledgmentListener(
AcknowledgmentSource.Listener listener)
{
if (listener == null) {
throw new NullPointerException();
}
synchronized (sessionLock) {
if (outState < FINISHED) {
if (ackListeners == null) {
ackListeners = new ArrayList(3);
}
ackListeners.add(listener);
return true;
} else {
return false;
}
}
}
});
((MuxServer) mux).populateContext(context);
}
public InputStream getRequestInputStream() { return in; }
public OutputStream getResponseOutputStream() { return out; }
public void abort() { Session.this.abort(); }
};
}
/**
*
*/
void abort() {
synchronized (sessionLock) {
if (!sessionDown) {
if (logger.isLoggable(Level.FINEST)) {
logger.log(Level.FINEST,
"outState=" + stateNames[outState] +
",inState=" + stateNames[inState] +
",role=" + (role == CLIENT ? "CLIENT" : "SERVER"));
}
if (outState == IDLE) {
mux.removeSession(sessionID);
} else if (outState < TERMINATED) {
if (role == SERVER && outState == FINISHED) {
/*
* In this case, send Close rather than Abort, so that
* a client that still hasn't finished writing will not
* get an unnecessary failure and will be able to read
* the complete response as intended (still permitting
* server-side defensive abort() invocation).
*/
mux.asyncSendClose(sessionID);
} else {
mux.asyncSendAbort(Mux.Abort | (role == SERVER ?
Mux.Abort_partial : 0),
sessionID, null);
}
setOutState(TERMINATED);
}
setDown("request aborted", null);
}
/*
* After the application has invoked abort() on the request, we
* must no longer try to "fake" an OK session.
*/
fakeOKtoWrite = false;
/*
* If removing this session from the connection's table
* was delayed in order to be able to send an
* Acknowledgment message, then we remove it on local
* abort in order to clean up resources. Also make sure
* that our state is considered terminated so that no
* future Acknowledgment message will be sent.
*/
if (removeLater) {
if (outState < TERMINATED) {
setOutState(TERMINATED);
}
mux.removeSession(sessionID);
removeLater = false;
}
}
}
/**
*
*/
void setDown(String message, Throwable cause) {
synchronized (sessionLock) {
if (!sessionDown) {
sessionDown = true;
sessionDownMessage = message;
sessionDownCause = cause;
sessionLock.notifyAll();
}
}
}
/**
*
*/
void handleIncrementRation(int increment) throws ProtocolException {
synchronized (sessionLock) {
if (inState == IDLE || inState == TERMINATED) {
throw new ProtocolException("IncrementRation on " +
stateNames[inState] + " session: " + sessionID);
}
if (!outRationInfinite) {
if (outRation + increment < outRation) {
throw new ProtocolException("ration overflow");
}
if (outState == OPEN) {
if (increment > 0) {
if (outRation == 0) {
sessionLock.notifyAll();
}
outRation += increment;
}
}
} // ignore message if outbound ration is infinite
}
}
/**
*
*/
void handleAbort(boolean partial) throws ProtocolException {
synchronized (sessionLock) {
if (inState == IDLE || inState == TERMINATED) {
throw new ProtocolException("Abort on " +
stateNames[inState] + " session: " + sessionID);
}
setInState(TERMINATED);
partialDeliveryStatus = partial;
/*
* Respond with an abort of this side of the session, if it's
* still open.
*/
/*
* REMIND: Technically, the client should not have to send
* an Abort message here if it is already in the finished
* state, although the spec would seem to suggest that it
* should do so regardless. A particular reason that we
* send it here in that case, though, is that it should be
* a cheap way to avoid 4827402 for that case-- to ensure
* that no late Acknowledgment message gets sent after the
* session has been removed.
*/
if (outState < TERMINATED) {
mux.asyncSendAbort(Mux.Abort | (role == SERVER ?
Mux.Abort_partial : 0),
sessionID, null);
setOutState(TERMINATED);
}
setDown("request aborted by remote endpoint", null);
if (sentAckRequired && !receivedAcknowledgment) {
notifyAcknowledgmentListeners(false);
} // REMIND: what about other dangling acknowledgments?
mux.removeSession(sessionID);
}
}
/**
*
*/
void handleClose() throws ProtocolException {
if (role != CLIENT) {
throw new ProtocolException("Close sent by client");
}
synchronized (sessionLock) {
if (inState != FINISHED) {
throw new ProtocolException("Close on " +
stateNames[inState] + " session: " + sessionID);
}
if (outState < FINISHED) {
/*
* From a protocol perspective, we need to terminate the
* session at this point (because we're not finished, but
* we don't want to hold on to it unnecessarily). But we
* also don't want the session to appear failed while the
* client is still writing-- instead, we want the client
* to be able to successfully read the complete response
* that was received-- so this flag is set to
* (temporarily) fake that the session is still in OK
* shape (but not send any more data for it).
*/
fakeOKtoWrite = true;
mux.asyncSendAbort(Mux.Abort, sessionID, null);
setOutState(TERMINATED);
/*
* REMIND: This approach causes a premature negative
* acknowledgment to the server. It seems that
* ideally, if receivedAckRequired is true, we should
* delay sending the Abort message until the response
* input stream is closed and an Acknowledgment has
* been sent-- although that would be somewhat at odds
* with the "timely fashion" prescription of the Close
* message specification.
*/
}
setInState(TERMINATED);
setDown("request closed by server", null);
/*
* If we still (might) need to send an Acknowledgment,
* then we must delay removing this session from the
* connection's table now, to prevent the sessionID being
* reused before the Acknowledgment message is sent.
*/
if (outState == TERMINATED ||
!receivedAckRequired || sentAcknowledgment)
{
mux.removeSession(sessionID);
} else {
removeLater = true;
}
}
}
/**
*
*/
void handleAcknowledgment() throws ProtocolException {
if (role != SERVER) {
throw new ProtocolException("Acknowledgment sent by server");
}
synchronized (sessionLock) {
if (inState == IDLE || inState == TERMINATED) {
throw new ProtocolException("Acknowledgment on " +
stateNames[inState] + " session: " + sessionID);
}
if (outState < FINISHED) {
throw new ProtocolException(
"acknowledgment received before EOF sent");
}
if (!sentAckRequired) {
throw new ProtocolException("acknowledgment not requested");
}
if (receivedAcknowledgment) {
throw new ProtocolException("duplicate acknowledgment");
}
receivedAcknowledgment = true;
notifyAcknowledgmentListeners(true);
}
}
/**
*
*/
void handleData(ByteBuffer data,
boolean eof, boolean close, boolean ackRequired)
throws ProtocolException
{
assert eof || (!close && !ackRequired);
if (ackRequired && role != CLIENT) {
throw new ProtocolException("Data/ackRequired sent by client");
}
synchronized (sessionLock) {
boolean notified = close; // close always causes notification
if (inState != OPEN) {
throw new ProtocolException("Data on " +
stateNames[inState] + " session: " + sessionID);
}
int length = data.remaining();
if (!inRationInfinite && length > inRation) {
throw new ProtocolException("input ration exceeded");
}
if (!inClosed && outState < TERMINATED) {
if (length > 0) {
if (inBufRemaining == 0) {
sessionLock.notifyAll();
notified = true;
}
inBufQueue.addLast(data);
inBufRemaining += length;
if (!inRationInfinite) {
inRation -= length;
}
}
}
if (eof) {
inEOF = true;
setInState(FINISHED);
if (!notified) {
sessionLock.notifyAll();
}
if (ackRequired) {
receivedAckRequired = true;
// send acknowledgment if input stream already closed?
}
if (close) {
handleClose();
}
// REMIND: send Close if appropriate?
}
}
}
/**
*
*/
void handleOpen() throws ProtocolException {
assert role == SERVER;
synchronized (sessionLock) {
if (inState < FINISHED || outState < TERMINATED) {
throw new ProtocolException(
inState < FINISHED ?
("Data/open on " +
stateNames[inState] + " session: " + sessionID) :
("Data/open before previous session terminated"));
}
setInState(TERMINATED);
// REMIND: process dangling acknowledgments here?
setDown("old request", null); // extraneous?
sessionLock.notifyAll();
mux.removeSession(sessionID);
}
}
/**
*
*/
private void setOutState(int newState) {
assert newState > outState;
outState = newState;
}
/**
*
*/
private void setInState(int newState) {
assert newState > inState;
inState = newState;
}
private void notifyAcknowledgmentListeners(final boolean received) {
if (ackListeners != null) {
systemThreadPool.execute(new Runnable() {
public void run() {
Iterator iter = ackListeners.iterator();
while (iter.hasNext()) {
AcknowledgmentSource.Listener listener =
(AcknowledgmentSource.Listener) iter.next();
listener.acknowledgmentReceived(received);
}
}
}, "Mux ack notifier");
}
}
/**
* Output stream returned by OutboundRequests and InboundRequests for
* a session of a multiplexed connection.
*/
private class MuxOutputStream extends OutputStream {
private ByteBuffer buffer = mux.directBuffersUseful() ?
ByteBuffer.allocateDirect(mux.maxFragmentSize) :
ByteBuffer.allocate(mux.maxFragmentSize);
MuxOutputStream() { }
public synchronized void write(int b) throws IOException {
if (!buffer.hasRemaining()) {
writeBuffer(false);
} else {
synchronized (sessionLock) { // REMIND: necessary?
ensureOpen();
}
}
buffer.put((byte) b);
}
public synchronized void write(byte[] b, int off, int len)
throws IOException
{
if (b == null) {
throw new NullPointerException();
} else if ((off < 0) || (off > b.length) || (len < 0) ||
((off + len) > b.length) || ((off + len) < 0))
{
throw new IndexOutOfBoundsException();
} else if (len == 0) {
synchronized (sessionLock) {
ensureOpen();
}
return;
}
while (len > 0) {
int avail = buffer.remaining();
if (len <= avail) {
synchronized (sessionLock) {
ensureOpen();
}
buffer.put(b, off, len);
return;
}
buffer.put(b, off, avail);
off += avail;
len -= avail;
writeBuffer(false);
}
}
public synchronized void flush() throws IOException {
// synchronized (sessionLock) {
// ensureOpen();
// }
//
// while (buffer.hasRemaining()) {
// writeBuffer(false);
// }
}
public synchronized void close() throws IOException {
if (logger.isLoggable(Level.FINEST)) {
logger.log(Level.FINEST,
"STACK TRACE", new Throwable("STACK TRACE"));
}
synchronized (sessionLock) {
ensureOpen();
}
while (!writeBuffer(true)) { }
}
/**
*
* This method must ONLY be invoked while synchronized on
* this session's lock.
*/
private void ensureOpen() throws IOException {
assert Thread.holdsLock(sessionLock);
/*
* While we're faking that the session is still OK when it really
* isn't (see above comments), return silently from here.
*/
if (fakeOKtoWrite) {
return;
}
if (outState > OPEN) {
if (outState == FINISHED) {
throw new IOException("stream closed");
} else {
throw new IOException("session terminated");
}
} else if (sessionDown) {
IOException ioe = new IOException(sessionDownMessage);
if (sessionDownCause != null) {
ioe.initCause(sessionDownCause);
}
throw ioe;
}
}
/**
* Writes as much of the contents of this stream's output buffer
* as is allowed by the current output ration. Upon normal return,
* at least one byte will have been transferred from the buffer to
* the multiplexed connection output queue, and the buffer will have
* been compacted, ready to be filled at the current position.
*
* Returns true if closeIfComplete and session was marked EOF (with
* complete buffer written); if true, stream's output buffer should
* no longer be accessed (because this method will not wait for
* actual writing of the message).
*/
private boolean writeBuffer(boolean closeIfComplete)
throws IOException
{
buffer.flip();
int origLimit = buffer.limit();
int toSend;
IOFuture future = null;
boolean eofSent = false;
synchronized (sessionLock) {
while (buffer.remaining() > 0 &&
!outRationInfinite && outRation < 1 &&
!sessionDown && outState == OPEN)
{
try {
sessionLock.wait(); // REMIND: timeout?
} catch (InterruptedException e) {
String message = "request I/O interrupted";
setDown(message, e);
IOException ioe = new IOException(message);
ioe.initCause(e);
throw ioe;
}
}
ensureOpen();
assert buffer.remaining() == 0 || outRationInfinite ||
outRation > 0 || fakeOKtoWrite;
/*
* If we're just faking that the session is OK when it really
* isn't, then we need to stop the writing from proceeding
* past this barrier-- and if a close was requested, then
* satisfy it right away.
*/
if (fakeOKtoWrite) {
assert role == CLIENT && inState == TERMINATED;
if (closeIfComplete) {
fakeOKtoWrite = false;
}
buffer.position(origLimit);
buffer.compact();
return closeIfComplete;
}
boolean complete;
if (outRationInfinite || buffer.remaining() <= outRation) {
toSend = buffer.remaining();
complete = true;
} else {
toSend = outRation;
buffer.limit(toSend);
complete = false;
}
if (!outRationInfinite) {
outRation -= toSend;
}
partialDeliveryStatus = true;
boolean open = outState == IDLE;
boolean eof = closeIfComplete && complete;
boolean close = role == SERVER && eof && inState > OPEN;
boolean ackRequired = role == SERVER && eof &&
(ackListeners != null && !ackListeners.isEmpty());
int op = Mux.Data |
(open ? Mux.Data_open : 0) |
(eof ? Mux.Data_eof : 0) |
(close ? Mux.Data_close : 0) |
(ackRequired ? Mux.Data_ackRequired : 0);
/*
* If we are the server-side, send even the final Data message
* for this session synchronously with this method, so that the
* VM will not exit before it gets delivered. Otherwise, let
* final Data messages (those with eof true) be sent after this
* method completes.
*/
if (!eof || role == SERVER) {
future = mux.futureSendData(op, sessionID, buffer);
} else {
mux.asyncSendData(op, sessionID, buffer);
}
if (outState == IDLE) {
setOutState(OPEN);
setInState(OPEN);
}
if (eof) {
eofSent = true;
setOutState(close ? TERMINATED : FINISHED);
if (ackRequired) {
sentAckRequired = true;
}
sessionLock.notifyAll();
}
}
if (future != null) {
waitForIO(future);
buffer.limit(origLimit); // REMIND: finally?
buffer.compact();
}
return eofSent;
}
/**
*
* This method must NOT be invoked while synchronized on
* this session's lock.
*/
private void waitForIO(IOFuture future) throws IOException {
assert !Thread.holdsLock(sessionLock);
try {
future.waitUntilDone();
} catch (InterruptedException e) {
String message = "request I/O interrupted";
setDown(message, e);
IOException ioe = new IOException(message);
ioe.initCause(e);
throw ioe;
} catch (IOException e) {
setDown(e.getMessage(), e.getCause());
throw e;
}
}
}
/**
* Output stream returned by OutboundRequests and InboundRequests for
* a session of a multiplexed connection.
*/
private class MuxInputStream extends InputStream {
MuxInputStream() { }
public int read() throws IOException {
synchronized (sessionLock) {
if (inClosed) {
throw new IOException("stream closed");
}
while (inBufRemaining == 0 &&
!sessionDown && inState <= OPEN && !inClosed)
{
if (inState == IDLE) {
assert outState == IDLE;
mux.asyncSendData(Mux.Data | Mux.Data_open,
sessionID, null);
setOutState(OPEN);
setInState(OPEN);
}
if (!inRationInfinite && inRation == 0) {
int inc = mux.initialInboundRation;
mux.asyncSendIncrementRation(sessionID, inc);
inRation += inc;
}
try {
sessionLock.wait(); // REMIND: timeout?
} catch (InterruptedException e) {
String message = "request I/O interrupted";
setDown(message, e);
IOException ioe = new IOException(message);
ioe.initCause(e);
throw ioe;
}
}
if (inClosed) {
throw new IOException("stream closed");
}
if (inBufRemaining == 0) {
if (inEOF) {
return -1;
} else {
if (inState == TERMINATED) {
throw new IOException(
"request aborted by remote endpoint");
}
assert sessionDown;
IOException ioe = new IOException(sessionDownMessage);
if (sessionDownCause != null) {
ioe.initCause(sessionDownCause);
}
throw ioe;
}
}
assert inBufQueue.size() > 0;
int result = -1;
while (result == -1) {
ByteBuffer buf = (ByteBuffer) inBufQueue.getFirst();
if (inBufPos < buf.limit()) {
result = (buf.get() & 0xFF);
inBufPos++;
inBufRemaining--;
}
if (inBufPos == buf.limit()) {
inBufQueue.removeFirst();
inBufPos = 0;
}
}
if (!inRationInfinite) {
checkInboundRation();
}
return result;
}
}
public int read(byte b[], int off, int len) throws IOException {
if (b == null) {
throw new NullPointerException();
} else if ((off < 0) || (off > b.length) || (len < 0) ||
((off + len) > b.length) || ((off + len) < 0))
{
throw new IndexOutOfBoundsException();
}
synchronized (sessionLock) {
if (inClosed) {
throw new IOException("stream closed");
} else if (len == 0) {
/*
* REMIND: What if
* - stream is at EOF?
* - session was aborted?
*/
return 0;
}
while (inBufRemaining == 0 &&
!sessionDown && inState <= OPEN && !inClosed)
{
if (inState == IDLE) {
assert outState == IDLE;
mux.asyncSendData(Mux.Data | Mux.Data_open,
sessionID, null);
setOutState(OPEN);
setInState(OPEN);
}
if (!inRationInfinite && inRation == 0) {
int inc = mux.initialInboundRation;
mux.asyncSendIncrementRation(sessionID, inc);
inRation += inc;
}
try {
sessionLock.wait(); // REMIND: timeout?
} catch (InterruptedException e) {
String message = "request I/O interrupted";
setDown(message, e);
IOException ioe = new IOException(message);
ioe.initCause(e);
throw ioe;
}
}
if (inClosed) {
throw new IOException("stream closed");
}
if (inBufRemaining == 0) {
if (inEOF) {
return -1;
} else {
if (inState == TERMINATED) {
throw new IOException(
"request aborted by remote endpoint");
}
assert sessionDown;
IOException ioe = new IOException(sessionDownMessage);
if (sessionDownCause != null) {
ioe.initCause(sessionDownCause);
}
throw ioe;
}
}
assert inBufQueue.size() > 0;
int remaining = len;
while (remaining > 0 && inBufRemaining > 0) {
ByteBuffer buf = (ByteBuffer) inBufQueue.getFirst();
if (inBufPos < buf.limit()) {
int toCopy = Math.min(buf.limit() - inBufPos,
remaining);
buf.get(b, off, toCopy);
inBufPos += toCopy;
inBufRemaining -= toCopy;
off += toCopy;
remaining -= toCopy;
}
if (inBufPos == buf.limit()) {
inBufQueue.removeFirst();
inBufPos = 0;
}
}
if (!inRationInfinite) {
checkInboundRation();
}
return len - remaining;
}
}
/**
* Sends ration increment, if read drained buffers below
* a certain mark.
*
* This method must NOT be invoked if the inbound ration in
* infinite, and it must ONLY be invoked while synchronized on
* this session's lock.
*
* REMIND: The implementation of this action will be a
* significant area for performance tuning.
*/
private void checkInboundRation() {
assert Thread.holdsLock(sessionLock);
assert !inRationInfinite;
if (inState >= FINISHED) {
return;
}
int mark = mux.initialInboundRation / 2;
int used = inBufRemaining + inRation;
if (used <= mark) {
int inc = mux.initialInboundRation - used;
mux.asyncSendIncrementRation(sessionID, inc);
inRation += inc;
}
}
public int available() throws IOException {
synchronized (sessionLock) {
if (inClosed) {
throw new IOException("stream closed");
}
/*
* REMIND: What if
* - stream is at EOF?
* - session was aborted?
*/
return inBufRemaining;
}
}
public void close() {
synchronized (sessionLock) {
if (inClosed) {
return;
}
inClosed = true;
inBufQueue.clear(); // allow GC of unread data
if (role == CLIENT && !sentAcknowledgment &&
receivedAckRequired && outState < TERMINATED)
{
mux.asyncSendAcknowledgment(sessionID);
sentAcknowledgment = true;
/*
* If removing this session from the connection's
* table was delayed in order to be able to send
* an Acknowledgment message, then take care of
* removing it now.
*/
if (removeLater) {
setOutState(TERMINATED);
mux.removeSession(sessionID);
removeLater = false;
}
}
sessionLock.notifyAll();
}
}
}
}