/* 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 java.util.ArrayList; import java.util.HashSet; import freenet.crypt.RandomSource; import freenet.keys.Key; import freenet.node.BaseSendableGet; import freenet.node.KeysFetchingLocally; import freenet.node.LowLevelGetException; import freenet.node.NodeClientCore; import freenet.node.RequestClient; import freenet.node.RequestCompletionListener; import freenet.node.RequestScheduler; import freenet.node.SendableRequestItem; import freenet.node.SendableRequestItemKey; import freenet.node.SendableRequestSender; import freenet.support.ListUtils; import freenet.support.LogThresholdCallback; import freenet.support.Logger; import freenet.support.Logger.LogLevel; /** * All the keys at a given priority which we have received key offers from other nodes for. * * This list needs to be kept up to date when: * - A request is removed. * - A request's priority changes. * - A key is found. * - A node disconnects or restarts (through the BlockOffer objects on the FailureTable). * * And of course, when an offer is received, we need to add an element. * * @author toad * */ @SuppressWarnings("serial") // We don't serialize this. public class OfferedKeysList extends BaseSendableGet implements RequestClient { private final HashSet<Key> keys; private final ArrayList<Key> keysList; // O(1) remove random element the way we use it, see chooseKey(). private static volatile boolean logMINOR; private static volatile boolean logDEBUG; static { Logger.registerLogThresholdCallback(new LogThresholdCallback(){ @Override public void shouldUpdate(){ logMINOR = Logger.shouldLog(LogLevel.MINOR, this); logDEBUG = Logger.shouldLog(LogLevel.DEBUG, this); } }); } private final RandomSource random; private final short priorityClass; private final boolean isSSK; OfferedKeysList(NodeClientCore core, RandomSource random, short priorityClass, boolean isSSK, boolean realTimeFlag) { super(false, realTimeFlag); this.keys = new HashSet<Key>(); this.keysList = new ArrayList<Key>(); this.random = random; this.priorityClass = priorityClass; this.isSSK = isSSK; } /** Called when a key is found, when it no longer belongs to this list etc. */ public synchronized void remove(Key key) { assert(keysList.size() == keys.size()); if(keys.remove(key)) { ListUtils.removeBySwapLast(keysList, key); if(logMINOR) Logger.minor(this, "Found "+key+" , removing it "+" for "+this+" size now "+keysList.size()); } assert(keysList.size() == keys.size()); } public synchronized boolean isEmpty() { return keys.isEmpty(); } @Override public long countAllKeys(ClientContext context) { // Not supported. throw new UnsupportedOperationException(); } @Override public long countSendableKeys(ClientContext context) { // Not supported. throw new UnsupportedOperationException(); } private static class MySendableRequestItem implements SendableRequestItem, SendableRequestItemKey { final Key key; MySendableRequestItem(Key key) { this.key = key; } @Override public void dump() { // Ignore, we will be GC'ed } @Override public SendableRequestItemKey getKey() { return this; } } @Override public synchronized SendableRequestItem chooseKey(KeysFetchingLocally fetching, ClientContext context) { assert(keysList.size() == keys.size()); if(keys.size() == 1) { // Shortcut the common case Key k = keysList.get(0); if(fetching.hasKey(k, null)) return null; // Ignore RecentlyFailed because an offered key overrides it. keys.remove(k); keysList.remove(0); keysList.trimToSize(); return new MySendableRequestItem(k); } for(int i=0;i<10;i++) { // Pick a random key if(keysList.isEmpty()) return null; int ptr = random.nextInt(keysList.size()); // Avoid shuffling penalty by swapping the chosen element with the end. Key k = keysList.get(ptr); if(fetching.hasKey(k, null)) continue; // Ignore RecentlyFailed because an offered key overrides it. ListUtils.removeBySwapLast(keysList, ptr); keys.remove(k); assert(keysList.size() == keys.size()); return new MySendableRequestItem(k); } return null; } @Override public RequestClient getClient() { return this; } @Override public ClientRequester getClientRequest() { // FIXME is this safe? return null; } @Override public short getPriorityClass() { return priorityClass; } @Override public void internalError(Throwable t, RequestScheduler sched, ClientContext context, boolean persistent) { Logger.error(this, "Internal error: "+t, t); } @Override public SendableRequestSender getSender(ClientContext context) { return new SendableRequestSender() { @Override public boolean send(NodeClientCore core, final RequestScheduler sched, ClientContext context, ChosenBlock req) { final Key key = ((MySendableRequestItem) req.token).key; // Have to cache it in order to propagate it; FIXME // Don't let a node force us to start a real request for a specific key. // We check the datastore, take up offers if any (on a short timeout), and then quit if we still haven't fetched the data. // Obviously this may have a marginal impact on load but it should only be marginal. core.asyncGet(key, true, new RequestCompletionListener() { @Override public void onSucceeded() { // We don't use ChosenBlockImpl so have to remove the keys from the fetching set ourselves. sched.removeFetchingKey(key); sched.wakeStarter(); } @Override public void onFailed(LowLevelGetException e) { // We don't use ChosenBlockImpl so have to remove the keys from the fetching set ourselves. sched.removeFetchingKey(key); // Something might be waiting for a request to complete (e.g. if we have two requests for the same key), // so wake the starter thread. sched.wakeStarter(); } }, true, false, realTimeFlag, false, false); // FIXME reconsider canWriteClientCache=false parameter. return true; } @Override public boolean sendIsBlocking() { return false; } }; } @Override public boolean isCancelled() { return false; } public synchronized void queueKey(Key key) { assert(keysList.size() == keys.size()); if(keys.add(key)) { keysList.add(key); if(logMINOR) Logger.minor(this, "Queued key "+key+" on "+this); } assert(keysList.size() == keys.size()); } @Override public Key getNodeKey(SendableRequestItem token) { return ((MySendableRequestItem) token).key; } @Override public boolean isSSK() { return isSSK; } @Override public boolean isInsert() { return false; } @Override public ClientRequestScheduler getScheduler(ClientContext context) { if(isSSK) return context.getSskFetchScheduler(realTimeFlag); else return context.getChkFetchScheduler(realTimeFlag); } @Override public boolean preRegister(ClientContext context, boolean toNetwork) { // Ignore return false; } @Override public long getWakeupTime(ClientContext context, long now) { if(isEmpty()) { return Long.MAX_VALUE; } return 0; } }