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