/* 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 freenet.client.FetchException;
import freenet.crypt.RandomSource;
import freenet.keys.Key;
import freenet.keys.KeyBlock;
import freenet.node.BaseSendableGet;
import freenet.node.KeysFetchingLocally;
import freenet.node.LowLevelGetException;
import freenet.node.LowLevelPutException;
import freenet.node.Node;
import freenet.node.NodeClientCore;
import freenet.node.PrioRunnable;
import freenet.node.RequestScheduler;
import freenet.node.RequestStarter;
import freenet.node.SendableGet;
import freenet.node.SendableInsert;
import freenet.node.SendableRequest;
import freenet.node.SendableRequestItemKey;
import freenet.support.Fields;
import freenet.support.IdentityHashSet;
import freenet.support.Logger;
import freenet.support.io.NativeThread;
/**
* Every X seconds, the RequestSender calls the ClientRequestScheduler to
* ask for a request to start. A request is then started, in its own
* thread. It is removed at that point.
*/
public class ClientRequestScheduler implements RequestScheduler {
private KeyListenerTracker schedCore;
final KeyListenerTracker schedTransient;
final transient ClientRequestSelector selector;
private static volatile boolean logMINOR;
private static volatile boolean logDEBUG;
static {
Logger.registerClass(ClientRequestScheduler.class);
}
/** Offered keys list. Only one, not split by priority, to prevent various attacks relating
* to offering specific keys and timing how long it takes for the node to request the key.
* Non-persistent. */
private final OfferedKeysList offeredKeys;
// we have one for inserts and one for requests
final boolean isInsertScheduler;
final boolean isSSKScheduler;
final boolean isRTScheduler;
final RandomSource random;
private final RequestStarter starter;
private final Node node;
public final String name;
final DatastoreChecker datastoreChecker;
public final ClientContext clientContext;
final PersistentJobRunner jobRunner;
public static final String PRIORITY_NONE = "NONE";
public static final String PRIORITY_SOFT = "SOFT";
public static final String PRIORITY_HARD = "HARD";
private String choosenPriorityScheduler;
public ClientRequestScheduler(boolean forInserts, boolean forSSKs, boolean forRT, RandomSource random, RequestStarter starter, Node node, NodeClientCore core, String name, ClientContext context) {
this.isInsertScheduler = forInserts;
this.isSSKScheduler = forSSKs;
this.isRTScheduler = forRT;
schedTransient = new KeyListenerTracker(forInserts, forSSKs, forRT, random, this, null, false);
this.datastoreChecker = core.storeChecker;
this.starter = starter;
this.random = random;
this.node = node;
this.clientContext = context;
selector = new ClientRequestSelector(forInserts, forSSKs, forRT, this);
this.name = name;
this.choosenPriorityScheduler = PRIORITY_HARD; // Will be reset later.
if(!forInserts) {
offeredKeys = new OfferedKeysList(core, random, (short)0, forSSKs, forRT);
} else {
offeredKeys = null;
}
jobRunner = clientContext.jobRunner;
}
public void startCore(byte[] globalSaltPersistent) {
schedCore = new KeyListenerTracker(isInsertScheduler, isSSKScheduler, isRTScheduler, random, this, globalSaltPersistent, true);
}
/** Called by the config. Callback
*
* @param val
*/
public synchronized void setPriorityScheduler(String val){
choosenPriorityScheduler = val;
}
static final int QUEUE_THRESHOLD = 100;
public void registerInsert(final SendableRequest req, boolean persistent) {
if(!isInsertScheduler)
throw new IllegalArgumentException("Adding a SendableInsert to a request scheduler!!");
selector.innerRegister(req, clientContext, null);
starter.wakeUp();
}
/**
* Register a group of requests (not inserts): a GotKeyListener and/or one
* or more SendableGet's.
* @param listener Listens for specific keys. Can be null if the listener
* is already registered i.e. on retrying.
* @param getters The actual requests to register to the request sender queue.
* @param persistent True if the request is persistent.
* @param onDatabaseThread True if we are running on the database thread.
* NOTE: delayedStoreCheck/probablyNotInStore is unnecessary because we only
* register the listener once.
* @throws FetchException
*/
public void register(final HasKeyListener hasListener, final SendableGet[] getters, final boolean persistent, final BlockSet blocks, final boolean noCheckStore) throws KeyListenerConstructionException {
if(logMINOR)
Logger.minor(this, "register("+persistent+","+hasListener+","+Fields.commaList(getters));
if(isInsertScheduler) {
IllegalStateException e = new IllegalStateException("finishRegister on an insert scheduler");
throw e;
}
final KeyListener listener;
if(hasListener != null) {
listener = hasListener.makeKeyListener(clientContext, false);
if(listener != null)
(persistent ? schedCore : schedTransient).addPendingKeys(listener);
else
Logger.normal(this, "No KeyListener for "+hasListener);
} else
listener = null;
if(getters != null && !noCheckStore) {
for(SendableGet getter : getters)
datastoreChecker.queueRequest(getter, blocks);
} else {
boolean anyValid = false;
for(SendableGet getter : getters) {
if(!(getter.isCancelled() || getter.getWakeupTime(clientContext, System.currentTimeMillis()) != 0))
anyValid = true;
}
finishRegister(getters, false, anyValid);
}
}
void finishRegister(final SendableGet[] getters, boolean persistent, final boolean anyValid) {
if(logMINOR) Logger.minor(this, "finishRegister for "+Fields.commaList(getters)+" anyValid="+anyValid+" persistent="+persistent);
if(isInsertScheduler) {
IllegalStateException e = new IllegalStateException("finishRegister on an insert scheduler");
for(SendableGet getter : getters) {
getter.internalError(e, this, clientContext, persistent);
}
throw e;
}
if(persistent) {
// Add to the persistent registration queue
if(logMINOR)
Logger.minor(this, "finishRegister() for "+Fields.commaList(getters));
if(anyValid) {
boolean wereAnyValid = false;
for(SendableGet getter : getters) {
// Just check isCancelled, we have already checked the cooldown.
if(!(getter.isCancelled())) {
wereAnyValid = true;
if(!getter.preRegister(clientContext, true)) {
selector.innerRegister(getter, clientContext, getters);
}
} else
getter.preRegister(clientContext, false);
}
if(!wereAnyValid) {
Logger.normal(this, "No requests valid");
}
} else {
Logger.normal(this, "No valid requests passed in");
}
} else {
// Register immediately.
for(SendableGet getter : getters) {
if((!anyValid) || getter.isCancelled()) {
getter.preRegister(clientContext, false);
continue;
} else {
if(getter.preRegister(clientContext, true)) continue;
}
if(!getter.isCancelled())
selector.innerRegister(getter, clientContext, getters);
}
starter.wakeUp();
}
}
/**
* All the persistent SendableRequest's currently running (either actually in flight, just chosen,
* awaiting the callbacks being executed etc). We MUST compare by pointer, as this is accessed on
* threads other than the database thread, so we don't know whether they are active (and in fact
* that may change under us!). So it can't be a HashSet.
*/
private final transient IdentityHashSet<SendableRequest> runningPersistentRequests = new IdentityHashSet<SendableRequest> ();
@Override
public void removeRunningRequest(SendableRequest request) {
synchronized(runningPersistentRequests) {
if(runningPersistentRequests.remove(request)) {
if(logMINOR)
Logger.minor(this, "Removed running request "+request+" size now "+runningPersistentRequests.size());
}
}
// We *DO* need to call clearCooldown here because it only becomes runnable for persistent requests after it has been removed from starterQueue.
request.clearWakeupTime(clientContext);
}
@Override
public boolean isRunningOrQueuedPersistentRequest(SendableRequest request) {
synchronized(runningPersistentRequests) {
if(runningPersistentRequests.contains(request)) return true;
}
return false;
}
/**
* Called by RequestStarter to find a request to run.
*/
@Override
public ChosenBlock grabRequest() {
short fuzz = -1;
if(PRIORITY_SOFT.equals(choosenPriorityScheduler))
fuzz = -1;
else if(PRIORITY_HARD.equals(choosenPriorityScheduler))
fuzz = 0;
return selector.chooseRequest(fuzz, random, offeredKeys, starter, isRTScheduler, clientContext);
}
/**
* Remove a KeyListener from the list of KeyListeners.
* @param getter
* @param complain
*/
public void removePendingKeys(KeyListener getter, boolean complain) {
boolean found = schedTransient.removePendingKeys(getter);
if(schedCore != null)
found |= schedCore.removePendingKeys(getter);
if(complain && !found)
Logger.error(this, "Listener not found when removing: "+getter);
}
/**
* Remove a KeyListener from the list of KeyListeners.
* @param getter
* @param complain
*/
public void removePendingKeys(HasKeyListener getter, boolean complain) {
boolean found = schedTransient.removePendingKeys(getter);
if(schedCore != null)
found |= schedCore.removePendingKeys(getter);
if(complain && !found)
Logger.error(this, "Listener not found when removing: "+getter);
}
public void reregisterAll(final ClientRequester request, short oldPrio) {
selector.reregisterAll(request, this, clientContext, oldPrio);
starter.wakeUp();
}
public String getChoosenPriorityScheduler() {
return choosenPriorityScheduler;
}
static final int TRIP_PENDING_PRIORITY = NativeThread.HIGH_PRIORITY-1;
@Override
public synchronized void succeeded(final BaseSendableGet succeeded, boolean persistent) {
selector.succeeded(succeeded);
}
public void tripPendingKey(final KeyBlock block) {
if(logMINOR) Logger.minor(this, "tripPendingKey("+block.getKey()+")");
if(offeredKeys != null) {
offeredKeys.remove(block.getKey());
}
final Key key = block.getKey();
if(schedTransient.anyProbablyWantKey(key, clientContext)) {
this.clientContext.mainExecutor.execute(new PrioRunnable() {
@Override
public void run() {
schedTransient.tripPendingKey(key, block, clientContext);
}
@Override
public int getPriority() {
return TRIP_PENDING_PRIORITY;
}
}, "Trip pending key (transient)");
}
if(schedCore == null) return;
if(schedCore.anyProbablyWantKey(key, clientContext)) {
try {
// This is definitely NOT an internal job.
// It can wait until after the next checkpoint if necessary. So use queue().
jobRunner.queue(new PersistentJob() {
@Override
public boolean run(ClientContext context) {
if(logMINOR) Logger.minor(this, "tripPendingKey for "+key);
schedCore.tripPendingKey(key, block, clientContext);
return false;
}
@Override
public String toString() {
return "tripPendingKey";
}
}, TRIP_PENDING_PRIORITY);
} catch (PersistenceDisabledException e) {
// Nothing to do
}
}
}
/* FIXME SECURITY When/if introduce tunneling or similar mechanism for starting requests
* at a distance this will need to be reconsidered. See the comments on the caller in
* RequestHandler (onAbort() handler). */
@Override
public boolean wantKey(Key key) {
if(schedTransient.anyProbablyWantKey(key, clientContext)) return true;
if(schedCore != null && schedCore.anyProbablyWantKey(key, clientContext)) return true;
return false;
}
/** Queue the offered key */
public void queueOfferedKey(final Key key, boolean realTime) {
if(logMINOR)
Logger.minor(this, "queueOfferedKey("+key);
offeredKeys.queueKey(key);
starter.wakeUp();
}
public void dequeueOfferedKey(Key key) {
offeredKeys.remove(key);
}
@Override
public long countQueuedRequests() {
return selector.countQueuedRequests(clientContext);
}
@Override
public KeysFetchingLocally fetchingKeys() {
return selector;
}
@Override
public void removeFetchingKey(Key key) {
// Don't need to call clearCooldown(), because selector will do it for each request blocked on the key.
selector.removeFetchingKey(key);
}
@Override
public void removeRunningInsert(SendableInsert insert, SendableRequestItemKey token) {
selector.removeRunningInsert(token);
// Must remove here, because blocks selection and therefore creates cooldown cache entries.
insert.clearWakeupTime(clientContext);
}
@Override
public void callFailure(final SendableGet get, final LowLevelGetException e, int prio, boolean persistent) {
if(!persistent) {
get.onFailure(e, null, clientContext);
} else {
try {
jobRunner.queue(new PersistentJob() {
@Override
public boolean run(ClientContext context) {
get.onFailure(e, null, clientContext);
return false;
}
@Override
public String toString() {
return "SendableGet onFailure";
}
}, prio);
} catch (PersistenceDisabledException e1) {
Logger.error(this, "callFailure() on a persistent request but database disabled", new Exception("error"));
}
}
}
@Override
public void callFailure(final SendableInsert insert, final LowLevelPutException e, int prio, boolean persistent) {
if(!persistent) {
insert.onFailure(e, null, clientContext);
} else {
try {
jobRunner.queue(new PersistentJob() {
@Override
public boolean run(ClientContext context) {
insert.onFailure(e, null, context);
return false;
}
@Override
public String toString() {
return "SendableInsert onFailure";
}
}, prio);
} catch (PersistenceDisabledException e1) {
Logger.error(this, "callFailure() on a persistent request but database disabled", new Exception("error"));
}
}
}
@Override
public ClientContext getContext() {
return clientContext;
}
/**
* @return True unless the key was already present.
*/
@Override
public boolean addToFetching(Key key) {
return selector.addToFetching(key);
}
@Override
public boolean addRunningInsert(SendableInsert insert, SendableRequestItemKey token) {
return selector.addRunningInsert(token);
}
@Override
public boolean hasFetchingKey(Key key, BaseSendableGet getterWaiting, boolean persistent) {
return selector.hasKey(key, null);
}
public long countPersistentWaitingKeys() {
if(schedCore == null) return 0;
return schedCore.countWaitingKeys();
}
public boolean isInsertScheduler() {
return isInsertScheduler;
}
@Override
public void wakeStarter() {
starter.wakeUp();
}
public byte[] saltKey(boolean persistent, Key key) {
return persistent ? schedCore.saltKey(key) : schedTransient.saltKey(key);
}
/** Only used in rare special cases e.g. ClientRequestSelector.
* FIXME add some interfaces to get rid of this gross layer violation. */
Node getNode() {
return node;
}
public KeySalter getGlobalKeySalter(boolean persistent) {
return persistent ? schedCore : schedTransient;
}
@Override
public ClientRequestSelector getSelector() {
return selector;
}
}