/* This code is part of Freenet. It is distributed under the GNU General * Public License, version 2 (or at your option any later version). See * http://www.gnu.org/ for further details of the GPL. */ package freenet.client.async; import static java.lang.String.format; import java.security.MessageDigest; import java.util.ArrayList; import java.util.List; import freenet.crypt.RandomSource; import freenet.crypt.SHA256; import freenet.keys.Key; import freenet.keys.KeyBlock; import freenet.keys.NodeSSK; import freenet.node.SendableGet; import freenet.node.SendableRequest; import freenet.support.LogThresholdCallback; import freenet.support.Logger; import freenet.support.Logger.LogLevel; /** * <p>Tracks exactly which keys we are listening for. This is * decoupled from actually requesting them because we want to pick up the data even if we didn't * request it - some nearby node requested it, it got inserted through this node, it was offered * via ULPRs some time after we requested it etc.</p> * * <p>The queue of requests to run, and the algorithm to choose which to start, is in * @see ClientRequestSchedulerSelector .</p> * * PERSISTENCE: This class is NOT serialized, it is recreated on every startup, and downloads are * re-registered with this class (for KeyListeners) and downloads and uploads are re-registered * with the ClientRequestSelector. * @author toad */ class KeyListenerTracker implements KeySalter { private static volatile boolean logMINOR; static { Logger.registerLogThresholdCallback(new LogThresholdCallback() { @Override public void shouldUpdate() { logMINOR = Logger.shouldLog(LogLevel.MINOR, this); } }); } /** Minimum number of retries at which we start to hold it against a request. * See the comments on fixRetryCount; we don't want many untried requests to prevent * us from trying requests which have only been tried once (e.g. USK checkers), from * other clients (and we DO want retries to take precedence over client round robin IF * the request has been tried many times already). */ private static final int MIN_RETRY_COUNT = 3; final boolean isInsertScheduler; final boolean isSSKScheduler; final boolean isRTScheduler; protected final ClientRequestScheduler sched; /** Transient even for persistent scheduler. There is one for each of transient, persistent. */ private final ArrayList<KeyListener> keyListeners; final boolean persistent; public boolean persistent() { return persistent; } protected KeyListenerTracker(boolean forInserts, boolean forSSKs, boolean forRT, RandomSource random, ClientRequestScheduler sched, byte[] globalSalt, boolean persistent) { this.isInsertScheduler = forInserts; this.isSSKScheduler = forSSKs; this.isRTScheduler = forRT; this.sched = sched; keyListeners = new ArrayList<KeyListener>(); if(globalSalt == null) { globalSalt = new byte[32]; random.nextBytes(globalSalt); } this.globalSalt = globalSalt; this.persistent = persistent; } /** * Mangle the retry count. * Below a certain number of attempts, we don't prefer one request to another just because * it's been tried more times. The reason for this is to prevent floods of low-retry-count * requests from starving other clients' requests which need to be retried. The other * solution would be to sort by client before retry count, but that would be excessive * IMHO; we DO want to avoid rerequesting keys we've tried many times before. */ protected static int fixRetryCount(int retryCount) { return Math.max(0, retryCount-MIN_RETRY_COUNT); } public void addPendingKeys(KeyListener listener) { if(listener == null) throw new NullPointerException(); synchronized (this) { // We have to register before checking the disk, so it may well get registered twice. if(keyListeners.contains(listener)) return; keyListeners.add(listener); } if (logMINOR) Logger.minor(this, "Added pending keys to "+this+" : size now "+keyListeners.size()+" : "+listener); } public boolean removePendingKeys(KeyListener listener) { boolean ret; synchronized (this) { ret = keyListeners.remove(listener); } listener.onRemove(); if (logMINOR) Logger.minor(this, "Removed pending keys from "+this+" : size now "+keyListeners.size()+" : "+listener, new Exception("debug")); return ret; } public boolean removePendingKeys(HasKeyListener hasListener) { ArrayList<KeyListener> matches = new ArrayList<KeyListener>(); synchronized (this) { for (KeyListener listener : keyListeners) { HasKeyListener hkl; try { hkl = listener.getHasKeyListener(); } catch (Throwable t) { Logger.error(this, format("Error in getHasKeyListener callback for %s", listener), t); continue; } if (hkl == hasListener) { matches.add(listener); } } } if (matches.isEmpty()) { return false; } for (KeyListener listener : matches) { try { removePendingKeys(listener); } catch (Throwable t) { Logger.error(this, format("Error while removing %s", listener), t); } } return true; } public short getKeyPrio(Key key, short priority, ClientContext context) { assert(key instanceof NodeSSK == isSSKScheduler); byte[] saltedKey = saltKey(key); List<KeyListener> matches = probablyWantKey(key, saltedKey); if (matches.isEmpty()) { return priority; } for (KeyListener listener : matches) { short prio; try { prio = listener.definitelyWantKey(key, saltedKey, sched.clientContext); } catch (Throwable t) { Logger.error(this, format("Error in definitelyWantKey callback for %s", listener), t); continue; } if(prio == -1) continue; if(prio < priority) priority = prio; } return priority; } public synchronized long countWaitingKeys() { long count = 0; for (KeyListener listener : keyListeners) { try { count += listener.countKeys(); } catch (Throwable t) { Logger.error(this, format("Error in countKeys callback for %s", listener), t); } } return count; } public boolean anyWantKey(Key key, ClientContext context) { assert(key instanceof NodeSSK == isSSKScheduler); byte[] saltedKey = saltKey(key); List<KeyListener> matches = probablyWantKey(key, saltedKey); if (!matches.isEmpty()) { for (KeyListener listener : matches) { try { if (listener.definitelyWantKey(key, saltedKey, sched.clientContext) >= 0) { return true; } } catch (Throwable t) { Logger.error(this, format("Error in definitelyWantKey callback for %s", listener), t); } } } return false; } public synchronized boolean anyProbablyWantKey(Key key, ClientContext context) { assert(key instanceof NodeSSK == isSSKScheduler); byte[] saltedKey = saltKey(key); for (KeyListener listener : keyListeners) { try { if (listener.probablyWantKey(key, saltedKey)) { return true; } } catch (Throwable t) { Logger.error(this, format("Error in probablyWantKey callback for %s", listener), t); } } return false; } public boolean tripPendingKey(Key key, KeyBlock block, ClientContext context) { if((key instanceof NodeSSK) != isSSKScheduler) { Logger.error(this, "Key "+key+" on scheduler ssk="+isSSKScheduler, new Exception("debug")); return false; } assert(key instanceof NodeSSK == isSSKScheduler); byte[] saltedKey = saltKey(key); List<KeyListener> matches = probablyWantKey(key, saltedKey); boolean ret = false; for (KeyListener listener : matches) { try { if (listener.handleBlock(key, saltedKey, block, context)) { ret = true; } } catch (Throwable t) { Logger.error(this, format("Error in handleBlock callback for %s", listener), t); } if (listener.isEmpty()) { try { removePendingKeys(listener); } catch (Throwable t) { Logger.error(this, format("Error while removing %s", listener), t); } } } return ret; } public SendableGet[] requestsForKey(Key key, ClientContext context) { ArrayList<SendableGet> list = new ArrayList<SendableGet>(); assert(key instanceof NodeSSK == isSSKScheduler); byte[] saltedKey = saltKey(key); List<KeyListener> matches = probablyWantKey(key, saltedKey); for (KeyListener listener : matches) { SendableGet[] reqs; try { reqs = listener.getRequestsForKey(key, saltedKey, context); } catch (Throwable t) { Logger.error(this, format("Error in getRequestsForKey callback for %s", listener), t); continue; } if (reqs == null) { continue; } for (SendableGet req : reqs) { list.add(req); } } if (list.isEmpty()) { return null; } return list.toArray(new SendableGet[list.size()]); } @Override public String toString() { StringBuffer sb = new StringBuffer(); sb.append(super.toString()); sb.append(':'); if(isInsertScheduler) sb.append("insert:"); if(isSSKScheduler) sb.append("SSK"); else sb.append("CHK"); return sb.toString(); } public byte[] globalSalt; public byte[] saltKey(Key key) { MessageDigest md = SHA256.getMessageDigest(); md.update(key.getRoutingKey()); md.update(globalSalt); byte[] ret = md.digest(); SHA256.returnMessageDigest(md); return ret; } protected void hintGlobalSalt(byte[] globalSalt2) { if(globalSalt == null) globalSalt = globalSalt2; } /** * Returns all KeyListeners that return true on probablyWantKey(key, saltedKey) */ private List<KeyListener> probablyWantKey(Key key, byte[] saltedKey) { ArrayList<KeyListener> matches = new ArrayList<KeyListener>(); synchronized (this) { for (KeyListener listener : keyListeners) { try { if (!listener.probablyWantKey(key, saltedKey)) { continue; } } catch (Throwable t) { Logger.error(this, format("Error in probablyWantKey callback for %s", listener), t); continue; } matches.add(listener); } } return matches; } }