package freenet.node;
import static java.util.concurrent.TimeUnit.SECONDS;
import java.lang.ref.WeakReference;
import java.util.HashSet;
import freenet.support.Logger;
import freenet.support.LogThresholdCallback;
import freenet.support.Logger.LogLevel;
/**
* Base class for tags representing a running request. These store enough information
* to detect whether they are finished; if they are still in the list, this normally
* represents a bug.
* @author Matthew Toseland <toad@amphibian.dyndns.org> (0xE43DA450)
*/
public abstract class UIDTag {
private static volatile boolean logMINOR;
static {
Logger.registerLogThresholdCallback(new LogThresholdCallback(){
@Override
public void shouldUpdate(){
logMINOR = Logger.shouldLog(LogLevel.MINOR, this);
}
});
}
final long createdTime;
final boolean wasLocal;
private final WeakReference<PeerNode> sourceRef;
final boolean realTimeFlag;
protected final RequestTracker tracker;
protected boolean accepted;
protected boolean sourceRestarted;
private boolean slowDown;
/** Nodes we have routed to at some point */
private HashSet<PeerNode> routedTo = null;
/** Nodes we are currently talking to i.e. which have not yet removed our UID from the
* list of active requests. */
private HashSet<PeerNode> currentlyRoutingTo = null;
/** Node we are currently doing an offered-key-fetch from */
private HashSet<PeerNode> fetchingOfferedKeyFrom = null;
/** We are waiting for two stage timeouts from these nodes. If the handler is unlocked
* and there are still nodes in the above two, we will log an error; but if those nodes
* are here too, we will reassignTagToSelf and not log an error. */
private HashSet<PeerNode> handlingTimeouts = null;
protected boolean notRoutedOnwards;
final long uid;
protected boolean unlockedHandler;
protected boolean noRecordUnlock;
private boolean hasUnlocked;
private boolean waitingForSlot;
UIDTag(PeerNode source, boolean realTimeFlag, long uid, Node node) {
createdTime = System.currentTimeMillis();
this.sourceRef = source == null ? null : source.myRef;
wasLocal = source == null;
this.realTimeFlag = realTimeFlag;
this.tracker = node.tracker;
this.uid = uid;
if(logMINOR)
Logger.minor(this, "Created "+this);
if(wasLocal) accepted = true; // FIXME remove, but it's always true at the moment.
}
public abstract void logStillPresent(Long uid);
long age() {
return System.currentTimeMillis() - createdTime;
}
/** Notify that we are routing to, or fetching an offered key from, a
* specific node. This should be called before we send the actual request
* message, to avoid us thinking we have more outgoing capacity than we
* actually have on a specific peer.
* @param peer The peer we are routing to.
* @param offeredKey If true, we are fetching an offered key, if false we
* are routing a normal request. Fetching an offered key is quite distinct,
* notably it has much shorter timeouts.
* @return True if we were already routing to (or fetching an offered key
* from, depending on offeredKey) the peer.
*/
public synchronized boolean addRoutedTo(PeerNode peer, boolean offeredKey) {
if(logMINOR)
Logger.minor(this, "Routing to "+peer+" on "+this+(offeredKey ? " (offered)" : ""), new Exception("debug"));
if(routedTo == null) routedTo = new HashSet<PeerNode>();
routedTo.add(peer);
if(offeredKey) {
if(fetchingOfferedKeyFrom == null) fetchingOfferedKeyFrom = new HashSet<PeerNode>();
return fetchingOfferedKeyFrom.add(peer);
} else {
if(currentlyRoutingTo == null) currentlyRoutingTo = new HashSet<PeerNode>();
return currentlyRoutingTo.add(peer);
}
}
public synchronized boolean hasRoutedTo(PeerNode peer) {
if(routedTo == null) return false;
return routedTo.contains(peer);
}
public synchronized boolean currentlyRoutingTo(PeerNode peer) {
if(currentlyRoutingTo == null) return false;
return currentlyRoutingTo.contains(peer);
}
// Note that these don't actually get removed until the request is finished, unless there is a disconnection or similar. But that
// is generally not a problem as they complete quickly and successfully mostly.
// The alternative would be to remove when the transfer is finished, but that does not
// guarantee the UID has been freed; we can only safely (for load management purposes)
// remove once we have an acknowledgement which is sent after the UID is removed.
public synchronized boolean currentlyFetchingOfferedKeyFrom(PeerNode peer) {
if(fetchingOfferedKeyFrom == null) return false;
return fetchingOfferedKeyFrom.contains(peer);
}
/** Notify that we are no longer fetching an offered key from a specific
* node. Must be called only when we are sure the next node doesn't think
* we are routing to it any more. See removeRoutingTo() explanation for more
* detail. When we are not routing to any nodes, and not fetching from, and
* the handler has also been unlocked, the UID is unlocked.
* @param next The node we are no longer fetching an offered key from.
*/
public void removeFetchingOfferedKeyFrom(PeerNode next) {
boolean noRecordUnlock;
synchronized(this) {
if(fetchingOfferedKeyFrom == null) return;
fetchingOfferedKeyFrom.remove(next);
if(handlingTimeouts != null) {
handlingTimeouts.remove(next);
}
if(!mustUnlock()) return;
noRecordUnlock = this.noRecordUnlock;
}
if(logMINOR) Logger.minor(this, "Unlocking "+this);
innerUnlock(noRecordUnlock);
}
/** Notify that we are no longer routing to a specific node. When we are
* not routing to (or fetching offered keys from) any nodes, and the handler
* side has also been unlocked, the whole tag is unlocked (note that this
* is only relevant to incoming requests; outgoing requests only care about
* what we are routing to). We should not call this method until we are
* reasonably sure that the node in question no longer thinks we are
* routing to it. Whereas we unlock the handler as soon as possible,
* without waiting for acknowledgement of our completion notice. Be
* cautious (late) in what you send, and generous (early) in what
* you accept! This avoids problems with the previous node thinking we've
* finished when we haven't, or us thinking the next node has finished when
* it hasn't.
* @param next The node we are no longer routing to.
*/
public void removeRoutingTo(PeerNode next) {
if(logMINOR)
Logger.minor(this, "No longer routing to "+next+" on "+this, new Exception("debug"));
boolean noRecordUnlock;
synchronized(this) {
if(currentlyRoutingTo == null) return;
if(!currentlyRoutingTo.remove(next)) {
Logger.warning(this, "Removing wrong node or removing twice? on "+this+" : "+next, new Exception("debug"));
}
if(handlingTimeouts != null) {
handlingTimeouts.remove(next);
}
if(!mustUnlock()) return;
noRecordUnlock = this.noRecordUnlock;
}
if(logMINOR) Logger.minor(this, "Unlocking "+this);
innerUnlock(noRecordUnlock);
}
protected void innerUnlock(boolean noRecordUnlock) {
tracker.unlockUID(this, false, noRecordUnlock);
}
public void postUnlock() {
PeerNode[] peers;
synchronized(this) {
if(routedTo != null)
peers = routedTo.toArray(new PeerNode[routedTo.size()]);
else
peers = null;
}
if(peers != null)
for(PeerNode p : peers)
p.postUnlock(this);
}
/** Add up the expected transfers in.
* @param ignoreLocalVsRemote If true, pretend that the request is remote even if it's local.
* @param outwardTransfersPerInsert Expected number of outward transfers for an insert.
* @param forAccept If true, we are deciding whether to accept a request.
* If false, we are deciding whether to SEND a request. We need to be more
* careful for the latter than the former, to avoid unnecessary rejections
* and mandatory backoffs.
*/
public abstract int expectedTransfersIn(boolean ignoreLocalVsRemote, int outwardTransfersPerInsert, boolean forAccept);
/** Add up the expected transfers out.
* @param ignoreLocalVsRemote If true, pretend that the request is remote even if it's local.
* @param outwardTransfersPerInsert Expected number of outward transfers for an insert.
* @param forAccept If true, we are deciding whether to accept a request.
* If false, we are deciding whether to SEND a request. We need to be more
* careful for the latter than the former, to avoid unnecessary rejections
* and mandatory backoffs.
*/
public abstract int expectedTransfersOut(boolean ignoreLocalVsRemote, int outwardTransfersPerInsert, boolean forAccept);
public synchronized void setNotRoutedOnwards() {
this.notRoutedOnwards = true;
}
private boolean reassigned;
/** Get the effective source node (e.g. for load management). This is null if the tag
* was reassigned to us. */
public synchronized PeerNode getSource() {
if(reassigned) return null;
if(wasLocal) return null;
return sourceRef.get();
}
/** Reassign the tag to us rather than its original sender. */
public synchronized void reassignToSelf() {
if(wasLocal) return;
reassigned = true;
}
/** Was the request originated locally? This returns the original answer: It is not
* affected by reassigning to self. */
public boolean wasLocal() {
return wasLocal;
}
/** Is the request local now? I.e. was it either originated locally or reassigned to
* self? */
public boolean isLocal() {
if(wasLocal) return true;
synchronized(this) {
return reassigned;
}
}
public abstract boolean isSSK();
public abstract boolean isInsert();
public abstract boolean isOfferReply();
/** Caller must call innerUnlock(noRecordUnlock) immediately if this returns true.
* Hence derived versions should call mustUnlock() only after they have checked their
* own unlock blockers. */
protected synchronized boolean mustUnlock() {
if(hasUnlocked) return false;
if(!unlockedHandler) return false;
if(currentlyRoutingTo != null && !currentlyRoutingTo.isEmpty()) {
if(!(reassigned || wasLocal || sourceRestarted || timedOutButContinued)) {
boolean expected = false;
if(handlingTimeouts != null) {
expected = true;
for(PeerNode pn : currentlyRoutingTo) {
if(handlingTimeouts.contains(pn)) {
if(logMINOR) Logger.debug(this, "Still waiting for "+pn.shortToString()+" but expected because handling timeout in unlockHandler - will reassign to self to resolve timeouts");
break;
}
expected = false;
}
}
if(!expected) {
if(handlingTimeouts != null)
Logger.normal(this, "Unlocked handler but still routing to "+currentlyRoutingTo+" - expected because have timed out so a fork might have succeeded and we might be waiting for the original");
else
Logger.error(this, "Unlocked handler but still routing to "+currentlyRoutingTo+" yet not reassigned on "+this, new Exception("debug"));
} else
reassignToSelf();
}
return false;
}
if(fetchingOfferedKeyFrom != null && !fetchingOfferedKeyFrom.isEmpty()) {
if(!(reassigned || wasLocal)) {
boolean expected = false;
if(handlingTimeouts != null) {
expected = true;
for(PeerNode pn : fetchingOfferedKeyFrom) {
if(handlingTimeouts.contains(pn)) {
if(logMINOR) Logger.debug(this, "Still waiting for "+pn.shortToString()+" but expected because handling timeout in unlockHandler - will reassign to self to resolve timeouts");
break;
}
expected = false;
}
}
if(!expected)
// Fork succeeds can't happen for fetch-offered-keys.
Logger.error(this, "Unlocked handler but still fetching offered keys from "+fetchingOfferedKeyFrom+" yet not reassigned on "+this, new Exception("debug"));
else
reassignToSelf();
}
return false;
}
Logger.normal(this, "Unlocking "+this, new Exception("debug"));
hasUnlocked = true;
return true;
}
/** Unlock the handler. That is, the incoming request has finished. This
* method should be called before the acknowledgement that the request has
* finished is sent downstream. Therefore, we will never be waiting for an
* acknowledgement from downstream in order to release the slot it is using,
* during which time it might think we are rejecting wrongly.
*
* Once both the incoming and outgoing requests are unlocked, the whole tag
* is unlocked.
*/
public void unlockHandler(boolean noRecord) {
boolean canUnlock;
synchronized(this) {
if(unlockedHandler) return;
noRecordUnlock = noRecord;
unlockedHandler = true;
canUnlock = mustUnlock();
}
if(canUnlock)
innerUnlock(noRecordUnlock);
else {
Logger.normal(this, "Cannot unlock yet in unlockHandler, still sending requests");
}
}
public void unlockHandler() {
unlockHandler(false);
}
// LOCKING: Synchronized because of access to currentlyRoutingTo i.e. to avoid ConcurrentModificationException.
// UIDTag lock is always taken last anyway so this is safe.
// Also it is only used in logging anyway.
@Override
public synchronized String toString() {
StringBuffer sb = new StringBuffer();
sb.append(super.toString());
sb.append(":");
sb.append(uid);
if(unlockedHandler)
sb.append(" (unlocked handler)");
if(hasUnlocked)
sb.append(" (unlocked)");
if(noRecordUnlock)
sb.append(" (don't record unlock)");
if(currentlyRoutingTo != null && !currentlyRoutingTo.isEmpty()) {
sb.append(" (routing to ");
for(PeerNode pn : currentlyRoutingTo) {
sb.append(pn.shortToString());
sb.append(",");
}
sb.setLength(sb.length()-1);
sb.append(")");
}
if(fetchingOfferedKeyFrom != null)
sb.append(" (fetch offered keys from ").append(fetchingOfferedKeyFrom.size()).append(")");
if(sourceRestarted)
sb.append(" (source restarted)");
if(timedOutButContinued)
sb.append(" (timed out but continued)");
return sb.toString();
}
/** Mark a peer as handling a timeout. Hence if when the handler is unlocked, this
* peer is still marked as routing to (or fetching offered keys from), rather than
* logging an error, we will reassign this tag to self, to wait for the fatal timeout.
* @param next
*/
public synchronized void handlingTimeout(PeerNode next) {
if(handlingTimeouts == null)
handlingTimeouts = new HashSet<PeerNode>();
handlingTimeouts.add(next);
}
private long loggedStillPresent;
private static final long LOGGED_STILL_PRESENT_INTERVAL = SECONDS.toMillis(60);
public void maybeLogStillPresent(long now, Long uid) {
if(now - createdTime > RequestTracker.TIMEOUT) {
synchronized(this) {
if(now - loggedStillPresent < LOGGED_STILL_PRESENT_INTERVAL) return;
loggedStillPresent = now;
}
logStillPresent(uid);
}
}
public synchronized void setAccepted() {
accepted = true;
}
private boolean timedOutButContinued;
/** Set when we are going to tell downstream that the request has timed out,
* but can't terminate it yet. We will terminate the request if we have to
* reroute it, and we count it towards the peer's limit, but we don't stop
* messages to the request source. */
public synchronized void timedOutToHandlerButContinued() {
timedOutButContinued = true;
}
/** The handler disconnected or restarted. */
public synchronized void onRestartOrDisconnectSource() {
sourceRestarted = true;
}
// The third option is reassignToSelf(). We only use that when we actually
// want the data, and mean to continue. In that case, none of the next three
// are appropriate.
/** Should we deduct this request from the source's limit, instead of
* counting it towards it? A normal request is counted towards it. A hidden
* request is deducted from it. This is used when the source has restarted
* but also in some other cases. */
public synchronized boolean countAsSourceRestarted() {
return sourceRestarted || timedOutButContinued;
}
/** Should we send messages to the source? */
public synchronized boolean hasSourceReallyRestarted() {
return sourceRestarted;
}
/** Should we stop the request as soon as is convenient? Normally this
* happens when the source is restarted or disconnected. */
public synchronized boolean shouldStop() {
return sourceRestarted || timedOutButContinued;
}
public synchronized boolean isSource(PeerNode pn) {
if(reassigned) return false;
if(wasLocal) return false;
if(sourceRef == null) return false;
return sourceRef == pn.myRef;
}
public synchronized void setWaitingForSlot() {
// FIXME use a counter on Node.
// We'd need to ensure it ALWAYS gets unset when some wierd
// error happens.
if(waitingForSlot) return;
waitingForSlot = true;
}
public synchronized void clearWaitingForSlot() {
// FIXME use a counter on Node.
// We'd need to ensure it ALWAYS gets unset when some wierd
// error happens.
// Probably we can do this just by calling clearWaitingForSlot() when unlocking???
if(!waitingForSlot) return;
waitingForSlot = false;
}
public synchronized boolean isWaitingForSlot() {
return waitingForSlot;
}
/** Set a flag indicating the originator should slow down. Only used at the shouldRejectRequest stage. */
synchronized void slowDown() {
slowDown = true;
}
/** Query the slow-down flag. Should be checked after shouldRejectRequest. */
synchronized boolean shouldSlowDown() {
return slowDown;
}
}