/*
* Dijjer - A Peer to Peer HTTP Cache
* Copyright (C) 2004,2005 Change.Tv, Inc
*
* This program is free software; you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation; either version 2 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program; if not, write to the Free Software
* Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
*/
package freenet.io.comm;
import static java.util.concurrent.TimeUnit.MILLISECONDS;
import static java.util.concurrent.TimeUnit.MINUTES;
import static java.util.concurrent.TimeUnit.SECONDS;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.HashSet;
import java.util.LinkedList;
import java.util.ListIterator;
import java.util.Map;
import freenet.io.comm.MessageFilter.MATCHED;
import freenet.node.PeerNode;
import freenet.support.Executor;
import freenet.support.LogThresholdCallback;
import freenet.support.Logger;
import freenet.support.Ticker;
import freenet.support.TimeUtil;
import freenet.support.Logger.LogLevel;
public class MessageCore {
public static final String VERSION = "$Id: MessageCore.java,v 1.22 2005/08/25 17:28:19 amphibian Exp $";
private static volatile boolean logMINOR;
private static volatile boolean logDEBUG;
static {
Logger.registerLogThresholdCallback(new LogThresholdCallback(){
@Override
public void shouldUpdate(){
logMINOR = Logger.shouldLog(LogLevel.MINOR, this);
logDEBUG = Logger.shouldLog(LogLevel.DEBUG, this);
}
});
}
private Dispatcher _dispatcher;
private Executor _executor;
/** _filters serves as lock for both */
private final LinkedList<MessageFilter> _filters = new LinkedList<MessageFilter>();
private final LinkedList<Message> _unclaimed = new LinkedList<Message>();
private static final int MAX_UNMATCHED_FIFO_SIZE = 50000;
private static final long MAX_UNCLAIMED_FIFO_ITEM_LIFETIME = MINUTES.toMillis(10); // maybe this should be per message type??
// FIXME do we need MIN_FILTER_REMOVE_TIME? Can we make this more efficient?
// FIXME may not work well for newly added filters with timeouts close to the minimum, or filters with timeouts close to the minimum in general.
private static final long MAX_FILTER_REMOVE_TIME = SECONDS.toMillis(1);
private static final long MIN_FILTER_REMOVE_TIME = MILLISECONDS.toMillis(100);
private long startedTime;
public synchronized long getStartedTime() {
return startedTime;
}
public MessageCore(Executor executor) {
_executor = executor;
}
/**
* Decode a packet from data and a peer.
* Can be called by IncomingPacketFilter's.
* @param data
* @param offset
* @param length
* @param peer
*/
public Message decodeSingleMessage(byte[] data, int offset, int length, PeerContext peer, int overhead) {
try {
return Message.decodeMessageFromPacket(data, offset, length, peer, overhead);
} catch (Throwable t) {
Logger.error(this, "Could not decode packet: "+t, t);
return null;
}
}
public void start(final Ticker ticker) {
synchronized(this) {
startedTime = System.currentTimeMillis();
}
ticker.queueTimedJob(new Runnable() {
@Override
public void run() {
long now = System.currentTimeMillis();
long nextRun = now + MAX_FILTER_REMOVE_TIME;
try {
nextRun = removeTimedOutFilters(nextRun);
} catch (Throwable t) {
Logger.error(this, "Failed to remove timed out filters: "+t, t);
} finally {
ticker.queueTimedJob(this, Math.max(MIN_FILTER_REMOVE_TIME, System.currentTimeMillis() - nextRun));
}
}
}, MIN_FILTER_REMOVE_TIME);
}
/**
* Remove timed out filters.
*/
long removeTimedOutFilters(long nextTimeout) {
long tStart = System.currentTimeMillis() + 1;
// Extra millisecond to give waitFor() a chance to remove the filter.
// Avoids exhaustive and unsuccessful search in waitFor() removal of a timed out filter.
if(logMINOR)
Logger.minor(this, "Removing timed out filters");
HashSet<MessageFilter> timedOutFilters = null;
synchronized (_filters) {
for (ListIterator<MessageFilter> i = _filters.listIterator(); i.hasNext();) {
MessageFilter f = i.next();
if (f.timedOut(tStart)) {
if(logMINOR)
Logger.minor(this, "Removing "+f);
i.remove();
if(timedOutFilters == null)
timedOutFilters = new HashSet<MessageFilter>();
if(!timedOutFilters.add(f))
Logger.error(this, "Filter "+f+" is in filter list twice!");
if(logMINOR) {
for (ListIterator<Message> it = _unclaimed.listIterator(); it.hasNext();) {
Message m = it.next();
MATCHED status = f.match(m, true, tStart);
if (status == MATCHED.MATCHED) {
// Don't match it, we timed out; two-level timeouts etc may want it for the next filter.
Logger.error(this, "Timed out but should have matched in _unclaimed: "+m+" for "+f);
break;
}
}
}
} else {
if(f.hasCallback() && nextTimeout > f.getTimeout())
nextTimeout = f.getTimeout();
}
// Do not break after finding a non-timed-out filter because some filters may
// be timed out because their client callbacks say they should be.
// Also simplifies the logic significantly, we've had some major bugs here.
// See also the end of waitFor() for another weird case.
}
}
if(timedOutFilters != null) {
for(MessageFilter f : timedOutFilters) {
f.setMessage(null);
f.onTimedOut(_executor);
}
}
long tEnd = System.currentTimeMillis();
if(tEnd - tStart > 50) {
if(tEnd - tStart > 3000)
Logger.error(this, "removeTimedOutFilters took "+(tEnd-tStart)+"ms");
else
if(logMINOR) Logger.minor(this, "removeTimedOutFilters took "+(tEnd-tStart)+"ms");
}
return nextTimeout;
}
/**
* Dispatch a message to a waiting filter, or feed it to the
* Dispatcher if none are found.
* @param m The Message to dispatch.
*/
public void checkFilters(Message m, PacketSocketHandler from) {
final boolean logMINOR = MessageCore.logMINOR;
final boolean logDEBUG = MessageCore.logDEBUG;
long tStart = System.currentTimeMillis();
if(logMINOR) Logger.minor(this, "checkFilters: "+m+" from "+m.getSource());
if ((m.getSource()) instanceof PeerNode)
{
((PeerNode)m.getSource()).addToLocalNodeReceivedMessagesFromStatistic(m);
}
boolean matched = false;
if (logMINOR && !(m.getSpec().equals(DMT.packetTransmit))) {
Logger.minor(this, "" + (System.currentTimeMillis() % 60000) + ' ' + from + " <- "
+ m.getSource() + " : " + m);
}
MessageFilter match = null;
ArrayList<MessageFilter> timedOut = null;
synchronized (_filters) {
for (ListIterator<MessageFilter> i = _filters.listIterator(); i.hasNext();) {
MessageFilter f = i.next();
if (f.matched()) {
Logger.error(this, "removed pre-matched message filter found in _filters: "+f);
i.remove();
continue;
}
MATCHED status = f.match(m, tStart);
if(status == MATCHED.TIMED_OUT || status == MATCHED.TIMED_OUT_AND_MATCHED) {
if(timedOut == null)
timedOut = new ArrayList<MessageFilter>();
timedOut.add(f);
i.remove();
continue;
} else if(status == MATCHED.MATCHED) {
matched = true;
i.remove();
match = f;
// We must setMessage() inside the lock to ensure that waitFor() sees it even if it times out.
f.setMessage(m);
if(logMINOR) Logger.minor(this, "Matched (1): "+f);
break; // Only one match permitted per message
} else if(logDEBUG) Logger.minor(this, "Did not match "+f);
}
}
if(timedOut != null) {
for(MessageFilter f : timedOut) {
if(logMINOR) Logger.minor(this, "Timed out "+f);
f.setMessage(null);
f.onTimedOut(_executor);
}
}
if(match != null) {
match.onMatched(_executor);
}
// Feed unmatched messages to the dispatcher
if ((!matched) && (_dispatcher != null)) {
try {
if(logMINOR) Logger.minor(this, "Feeding to dispatcher: "+m);
matched = _dispatcher.handleMessage(m);
} catch (Throwable t) {
Logger.error(this, "Dispatcher threw "+t, t);
}
}
if(timedOut != null) timedOut.clear();
// Keep the last few _unclaimed messages around in case the intended receiver isn't receiving yet
if (!matched) {
if(logMINOR) Logger.minor(this, "Unclaimed: "+m);
/** Check filters and then add to _unmatched is ATOMIC
* It has to be atomic, because otherwise we can get a
* race condition that results in timeouts on MFs.
*
* Specifically:
* - Thread A receives packet
* - Thread A checks filters. It doesn't match any.
* - Thread A feeds to Dispatcher.
* - Thread B creates filter.
* - Thread B checks _unmatched.
* - Thread B adds filter.
* - Thread B sleeps.
* - Thread A returns from Dispatcher. Which didn't match.
* - Thread A adds to _unmatched.
*
* OOPS!
* The only way to fix this is to have checking the
* filters and unmatched be a single atomic operation.
* Another race is possible if we merely recheck the
* filters after we return from dispatcher, for example.
*/
synchronized (_filters) {
if(logMINOR) Logger.minor(this, "Rechecking filters and adding message");
for (ListIterator<MessageFilter> i = _filters.listIterator(); i.hasNext();) {
MessageFilter f = i.next();
MATCHED status = f.match(m, tStart);
if(status == MATCHED.MATCHED) {
matched = true;
match = f;
i.remove();
if(logMINOR) Logger.minor(this, "Matched (2): "+f);
match.setMessage(m);
break; // Only one match permitted per message
} else if(status == MATCHED.TIMED_OUT || status == MATCHED.TIMED_OUT_AND_MATCHED) {
if(timedOut == null)
timedOut = new ArrayList<MessageFilter>();
timedOut.add(f);
i.remove();
continue;
}
}
if(!matched) {
while (_unclaimed.size() > MAX_UNMATCHED_FIFO_SIZE) {
Message removed = _unclaimed.removeFirst();
long messageLifeTime = System.currentTimeMillis() - removed.localInstantiationTime;
if ((removed.getSource()) instanceof PeerNode) {
Logger.normal(this, "Dropping unclaimed from "+removed.getSource().getPeer()+", lived "+TimeUtil.formatTime(messageLifeTime, 2, true)+" (quantity)"+": "+removed);
} else {
Logger.normal(this, "Dropping unclaimed, lived "+TimeUtil.formatTime(messageLifeTime, 2, true)+" (quantity)"+": "+removed);
}
}
_unclaimed.addLast(m);
if(logMINOR) Logger.minor(this, "Done");
}
}
if(match != null) {
match.onMatched(_executor);
}
if(timedOut != null) {
for(MessageFilter f : timedOut) {
f.setMessage(null);
f.onTimedOut(_executor);
}
}
}
long tEnd = System.currentTimeMillis();
long dT = tEnd - tStart;
if(dT > 50) {
if(dT > 3000)
Logger.error(this, "checkFilters took "+(dT)+"ms with unclaimedFIFOSize of "+_unclaimed.size()+" for matched: "+matched);
else
if(logMINOR) Logger.minor(this, "checkFilters took "+(dT)+"ms with unclaimedFIFOSize of "+_unclaimed.size()+" for matched: "+matched);
}
}
/** IncomingPacketFilter should call this when a node is disconnected. */
public void onDisconnect(PeerContext ctx) {
ArrayList<MessageFilter> droppedFilters = null; // rare operation, we can waste objects for better locking
synchronized(_filters) {
ListIterator<MessageFilter> i = _filters.listIterator();
while (i.hasNext()) {
MessageFilter f = i.next();
if(f.matchesDroppedConnection(ctx)) {
if(droppedFilters == null)
droppedFilters = new ArrayList<MessageFilter>();
droppedFilters.add(f);
i.remove();
}
}
}
if(droppedFilters != null) {
for(MessageFilter mf : droppedFilters) {
mf.onDroppedConnection(ctx, _executor);
}
}
}
/** IncomingPacketFilter should call this when a node connects with a new boot ID */
public void onRestart(PeerContext ctx) {
ArrayList<MessageFilter> droppedFilters = null; // rare operation, we can waste objects for better locking
synchronized(_filters) {
ListIterator<MessageFilter> i = _filters.listIterator();
while (i.hasNext()) {
MessageFilter f = i.next();
if(f.matchesRestartedConnection(ctx)) {
if(droppedFilters == null)
droppedFilters = new ArrayList<MessageFilter>();
droppedFilters.add(f);
i.remove();
}
}
}
if(droppedFilters != null) {
for(MessageFilter mf : droppedFilters) {
mf.onRestartedConnection(ctx, _executor);
}
}
}
public void addAsyncFilter(MessageFilter filter, AsyncMessageFilterCallback callback, ByteCounter ctr) throws DisconnectedException {
filter.setAsyncCallback(callback, ctr);
if(filter.matched()) {
Logger.error(this, "addAsyncFilter() on a filter which is already matched: "+filter, new Exception("error"));
filter.clearMatched();
}
filter.onStartWaiting(false);
if(logMINOR) Logger.minor(this, "Adding async filter "+filter+" for "+callback);
Message ret = null;
if(filter.anyConnectionsDropped()) {
throw new DisconnectedException();
//or... filter.onDroppedConnection(filter.droppedConnection());
}
// Check to see whether the filter matches any of the recently _unclaimed messages
// Drop any _unclaimed messages that the filter doesn't match that are also older than MAX_UNCLAIMED_FIFO_ITEM_LIFETIME
long now = System.currentTimeMillis();
long messageDropTime = now - MAX_UNCLAIMED_FIFO_ITEM_LIFETIME;
long messageLifeTime = 0;
long timeout = filter.getTimeout();
synchronized (_filters) {
//Once in the list, it is up to the callback system to trigger the disconnection, however, we may
//have disconnected between check above and locking, so we *must* check again.
if(filter.anyConnectionsDropped()) {
throw new DisconnectedException();
//or... filter.onDroppedConnection(filter.droppedConnection());
//but we are holding the _filters lock!
}
if(logMINOR) Logger.minor(this, "Checking _unclaimed");
for (ListIterator<Message> i = _unclaimed.listIterator(); i.hasNext();) {
Message m = i.next();
// These messages have already arrived, so we can match against them even if we are timed out.
MATCHED status = filter.match(m, true, now);
if (status == MATCHED.MATCHED) {
i.remove();
ret = m;
if(logMINOR) Logger.minor(this, "Matching from _unclaimed");
break;
} else if (m.localInstantiationTime < messageDropTime) {
i.remove();
messageLifeTime = now - m.localInstantiationTime;
if ((m.getSource()) instanceof PeerNode) {
Logger.normal(this, "Dropping unclaimed from "+m.getSource().getPeer()+", lived "+TimeUtil.formatTime(messageLifeTime, 2, true)+" (age)"+": "+m);
} else {
Logger.normal(this, "Dropping unclaimed, lived "+TimeUtil.formatTime(messageLifeTime, 2, true)+" (age)"+": "+m);
}
}
}
if (ret == null && timeout >= System.currentTimeMillis()) {
if(logMINOR) Logger.minor(this, "Not in _unclaimed");
// Insert filter into filter list in order of timeout
ListIterator<MessageFilter> i = _filters.listIterator();
while (true) {
if (!i.hasNext()) {
i.add(filter);
if(logMINOR) Logger.minor(this, "Added at end");
return;
}
MessageFilter mf = i.next();
if (mf.getTimeout() > timeout) {
i.previous();
i.add(filter);
if(logMINOR) Logger.minor(this, "Added in middle - mf timeout="+mf.getTimeout()+" - my timeout="+filter.getTimeout());
return;
}
}
}
}
if(ret != null) {
filter.setMessage(ret);
filter.onMatched(_executor);
filter.clearMatched();
} else {
filter.onTimedOut(_executor);
}
}
/**
* Wait for a filter to trigger, or timeout. Blocks until either the trigger is activated, or it times
* out, or the peer is disconnected.
* @param filter The filter to wait for. This filter must not have a callback.
* @param ctr Byte counter to add bytes from the message to.
*
* @return Either a message, or null if the filter timed out.
*
* @throws DisconnectedException If the single peer being waited for disconnects.
* @throws IllegalArgumentException If {@code filter} has a callback
*/
public Message waitFor(MessageFilter filter, ByteCounter ctr) throws DisconnectedException {
if(logDEBUG) Logger.debug(this, "Waiting for "+filter);
if(filter.hasCallback()) {
throw new IllegalArgumentException("waitFor called with a filter that has a callback");
}
long startTime = System.currentTimeMillis();
if(filter.matched()) {
Logger.error(this, "waitFor() on a filter which is already matched: "+filter, new Exception("error"));
filter.clearMatched();
}
filter.onStartWaiting(true);
Message ret = null;
if(filter.anyConnectionsDropped()) {
filter.onDroppedConnection(filter.droppedConnection(), _executor);
throw new DisconnectedException();
}
// Check to see whether the filter matches any of the recently _unclaimed messages
// Drop any _unclaimed messages that the filter doesn't match that are also older than MAX_UNCLAIMED_FIFO_ITEM_LIFETIME
long now = System.currentTimeMillis();
long messageDropTime = now - MAX_UNCLAIMED_FIFO_ITEM_LIFETIME;
long messageLifeTime = 0;
synchronized (_filters) {
if(logMINOR) Logger.minor(this, "Checking _unclaimed");
for (ListIterator<Message> i = _unclaimed.listIterator(); i.hasNext();) {
Message m = i.next();
MATCHED status = filter.match(m, true, startTime);
if(status == MATCHED.MATCHED) {
i.remove();
ret = m;
if(logMINOR) Logger.minor(this, "Matching from _unclaimed");
break;
} else if (m.localInstantiationTime < messageDropTime) {
i.remove();
messageLifeTime = now - m.localInstantiationTime;
if ((m.getSource()) instanceof PeerNode) {
Logger.normal(this, "Dropping unclaimed from "+m.getSource().getPeer()+", lived "+TimeUtil.formatTime(messageLifeTime, 2, true)+" (age)"+": "+m);
} else {
Logger.normal(this, "Dropping unclaimed, lived "+TimeUtil.formatTime(messageLifeTime, 2, true)+" (age)"+": "+m);
}
}
}
if (ret == null) {
if(logMINOR) Logger.minor(this, "Not in _unclaimed");
// Insert filter into filter list in order of timeout
ListIterator<MessageFilter> i = _filters.listIterator();
while (true) {
if (!i.hasNext()) {
i.add(filter);
if(logMINOR) Logger.minor(this, "Added at end "+filter);
break;
}
MessageFilter mf = i.next();
if (mf.getTimeout() > filter.getTimeout()) {
i.previous();
i.add(filter);
if(logMINOR) Logger.minor(this, "Added in middle - mf timeout="+mf.getTimeout()+" - my timeout="+filter.getTimeout()+" filter "+filter);
break;
}
}
}
}
long tEnd = System.currentTimeMillis();
if(tEnd - now > 50) {
if(tEnd - now > 3000)
Logger.error(this, "waitFor _unclaimed iteration took "+(tEnd-now)+"ms with unclaimedFIFOSize of "+_unclaimed.size()+" for ret of "+ret);
else
if(logMINOR) Logger.minor(this, "waitFor _unclaimed iteration took "+(tEnd-now)+"ms with unclaimedFIFOSize of "+_unclaimed.size()+" for ret of "+ret);
}
// Unlock to wait on filter
// Waiting on the filter won't release the outer lock
// So we have to release it here
if(ret == null) {
if(logMINOR) Logger.minor(this, "Waiting...");
synchronized (filter) {
try {
// Precaution against filter getting matched between being added to _filters and
// here - bug discovered by Mason
// Check reallyTimedOut() too a) for paranoia, b) for filters with a callback (we could conceivably waitFor() them).
while(!(filter.matched() || (filter.droppedConnection() != null) || (filter.reallyTimedOut(now = System.currentTimeMillis())))) {
long wait = filter.getTimeout()-now;
if(wait <= 0)
break;
filter.wait(wait);
}
if(filter.droppedConnection() != null)
throw new DisconnectedException();
} catch (InterruptedException e) {
}
ret = filter.getMessage();
}
if(logMINOR) Logger.minor(this, "Returning "+ret+" from "+filter);
}
// More tricky locking ...
synchronized(_filters) {
// Some nasty race conditions can happen here.
// E.g. the filter can be matched and yet we timeout at the same time.
// Hence we need to be absolutely sure that when we remove it it hasn't been matched.
// Note also that the locking does work here - the filter lock is taken last, and
// _filters protects both the unwanted messages (above), the filter list, and
// is taken when a match is found too.
if(ret == null) {
// Check again.
if(filter.matched()) {
ret = filter.getMessage();
}
}
filter.clearMatched();
// We must remove it from _filters before we return, or when it is re-added,
// it will be in the list twice, and potentially many more times than twice!
// Fortunately, it will be close to the beginning of the filters list, having
// just timed out. That is assuming it hasn't already been removed; in that
// case, this will be slower.
_filters.remove(filter);
// A filter being waitFor()'ed cannot have any callbacks, so we don't need to call onMatched().
}
// Probably get rid...
// if (Dijjer.getDijjer().getDumpMessageWaitTimes() != null) {
// Dijjer.getDijjer().getDumpMessageWaitTimes().println(filter.toString() + "\t" + filter.getInitialTimeout() + "\t"
// + (System.currentTimeMillis() - startTime));
// Dijjer.getDijjer().getDumpMessageWaitTimes().flush();
// }
long endTime = System.currentTimeMillis();
if(logDEBUG) Logger.debug(this, "Returning in "+(endTime-startTime)+"ms");
if((ctr != null) && (ret != null))
ctr.receivedBytes(ret._receivedByteCount);
return ret;
}
/**
* Send a Message to a PeerContext.
* @throws NotConnectedException If we are not currently connected to the node.
*/
public void send(PeerContext destination, Message m, ByteCounter ctr) throws NotConnectedException {
if(m.getSpec().isInternalOnly()) {
Logger.error(this, "Trying to send internal-only message "+m+" of spec "+m.getSpec(), new Exception("debug"));
return;
}
destination.sendAsync(m, null, ctr);
}
public void setDispatcher(Dispatcher d) {
_dispatcher = d;
}
/**
* @return the number of received messages that are currently unclaimed
*/
public int getUnclaimedFIFOSize() {
synchronized (_filters){
return _unclaimed.size();
}
}
public Map<String, Integer> getUnclaimedFIFOMessageCounts() {
Map<String, Integer> messageCounts = new HashMap<String, Integer>();
synchronized(_filters) {
for (ListIterator<Message> i = _unclaimed.listIterator(); i.hasNext();) {
Message m = i.next();
String messageName = m.getSpec().getName();
Integer messageCount = messageCounts.get(messageName);
if (messageCount == null) {
messageCounts.put(messageName, Integer.valueOf(1) );
} else {
messageCount = Integer.valueOf(messageCount.intValue() + 1);
messageCounts.put(messageName, messageCount );
}
}
}
return messageCounts;
}
public Executor getExecutor() {
return _executor;
}
}