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