/* 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;
}
}