/* 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.FetchContext; import freenet.keys.ClientKey; import freenet.keys.ClientKeyBlock; import freenet.keys.ClientSSK; import freenet.keys.Key; import freenet.keys.KeyBlock; import freenet.keys.KeyVerifyException; import freenet.node.KeysFetchingLocally; import freenet.node.LowLevelGetException; import freenet.node.NullSendableRequestItem; import freenet.node.RequestClient; import freenet.node.RequestScheduler; import freenet.node.SendableGet; import freenet.node.SendableRequestItem; import freenet.support.Logger; import freenet.support.TimeUtil; /** * Base class implements most of what is needed for fetching a single block. * * WARNING: Changing non-transient members on classes that are Serializable can result in * restarting downloads or losing uploads. * @author toad */ public abstract class BaseSingleFileFetcher extends SendableGet implements HasKeyListener { private static final long serialVersionUID = 1L; protected final ClientKey key; protected boolean cancelled; protected boolean finished; final int maxRetries; private int retryCount; protected final FetchContext ctx; protected boolean deleteFetchContext; static final SendableRequestItem[] keys = new SendableRequestItem[] { NullSendableRequestItem.nullItem }; private int cachedCooldownTries; private long cachedCooldownTime; public transient long cooldownWakeupTime; private static volatile boolean logMINOR; static { Logger.registerClass(BaseSingleFileFetcher.class); } protected BaseSingleFileFetcher(ClientKey key, int maxRetries, FetchContext ctx, ClientRequester parent, boolean deleteFetchContext, boolean realTimeFlag) { super(parent, realTimeFlag); this.deleteFetchContext = deleteFetchContext; if(logMINOR) Logger.minor(this, "Creating BaseSingleFileFetcher for "+key); retryCount = 0; this.maxRetries = maxRetries; this.key = key; this.ctx = ctx; if(ctx == null) throw new NullPointerException(); } @Override public long countAllKeys(ClientContext context) { return 1; } @Override public long countSendableKeys(ClientContext context) { return 1; } @Override public SendableRequestItem chooseKey(KeysFetchingLocally fetching, ClientContext context) { Key k = key.getNodeKey(false); if(fetching.hasKey(k, this)) return null; long l = fetching.checkRecentlyFailed(k, realTimeFlag); long now = System.currentTimeMillis(); if(l > 0 && l > now) { if(maxRetries == -1 || (maxRetries >= RequestScheduler.COOLDOWN_RETRIES)) { // FIXME synchronization!!! if(logMINOR) Logger.minor(this, "RecentlyFailed -> cooldown until "+TimeUtil.formatTime(l-now)+" on "+this); cooldownWakeupTime = Math.max(cooldownWakeupTime, l); return null; } else { this.onFailure(new LowLevelGetException(LowLevelGetException.RECENTLY_FAILED), null, context); return null; } } return keys[0]; } @Override public ClientKey getKey(SendableRequestItem token) { return key; } @Override public FetchContext getContext() { return ctx; } @Override public boolean isSSK() { return key instanceof ClientSSK; } /** Try again - returns true if we can retry */ protected boolean retry(ClientContext context) { if(isEmpty()) { if(logMINOR) Logger.minor(this, "Not retrying because empty"); return false; // Cannot retry e.g. because we got the block and it failed to decode - that's a fatal error. } // We want 0, 1, ... maxRetries i.e. maxRetries+1 attempts (maxRetries=0 => try once, no retries, maxRetries=1 = original try + 1 retry) int r; r = ++retryCount; if(logMINOR) Logger.minor(this, "Attempting to retry... (max "+maxRetries+", current "+r+") on "+this+" finished="+finished+" cancelled="+cancelled); if((r <= maxRetries) || (maxRetries == -1)) { checkCachedCooldownData(); if(cachedCooldownTries == 0 || r % cachedCooldownTries == 0) { // Add to cooldown queue. Don't reschedule yet. long now = System.currentTimeMillis(); if(cooldownWakeupTime > now) { Logger.error(this, "Already on the cooldown queue for "+this+" until "+freenet.support.TimeUtil.formatTime(cooldownWakeupTime - now), new Exception("error")); } else { if(logMINOR) Logger.minor(this, "Adding to cooldown queue "+this); cooldownWakeupTime = now + cachedCooldownTime; reduceWakeupTime(cooldownWakeupTime, context); if(logMINOR) Logger.minor(this, "Added single file fetcher into cooldown until "+TimeUtil.formatTime(cooldownWakeupTime - now)); } onEnterFiniteCooldown(context); } else { // Wake the CRS after clearing cache. clearWakeupTime(context); } return true; // We will retry in any case, maybe not just not yet. } unregister(context, getPriorityClass()); return false; } private void checkCachedCooldownData() { // 0/0 is illegal, and it's also the default, so use it to indicate we haven't fetched them. if(!(cachedCooldownTime == 0 && cachedCooldownTries == 0)) { // Okay, we have already got them. return; } innerCheckCachedCooldownData(); } private void innerCheckCachedCooldownData() { cachedCooldownTries = ctx.getCooldownRetries(); cachedCooldownTime = ctx.getCooldownTime(); } protected void onEnterFiniteCooldown(ClientContext context) { // Do nothing. } @Override public ClientRequester getClientRequest() { return parent; } @Override public short getPriorityClass() { return parent.getPriorityClass(); } public void cancel(ClientContext context) { synchronized(this) { cancelled = true; } unregisterAll(context); } /** * Remove the pendingKeys item and then remove from the queue as well. * Call unregister(container) if you only want to remove from the queue. */ public void unregisterAll(ClientContext context) { getScheduler(context).removePendingKeys(this, false); unregister(context, (short)-1); } @Override public void unregister(ClientContext context, short oldPrio) { super.unregister(context, oldPrio); } @Override public synchronized boolean isCancelled() { return cancelled; } public synchronized boolean isEmpty() { return cancelled || finished; } @Override public RequestClient getClient() { return parent.getClient(); } public void onGotKey(Key key, KeyBlock block, ClientContext context) { synchronized(this) { if(finished) { if(logMINOR) Logger.minor(this, "onGotKey() called twice on "+this, new Exception("debug")); return; } finished = true; if(isCancelled()) return; if(key == null) throw new NullPointerException(); if(this.key == null) throw new NullPointerException("Key is null on "+this); if(!key.equals(this.key.getNodeKey(false))) { Logger.normal(this, "Got sent key "+key+" but want "+this.key+" for "+this); return; } } unregister(context, getPriorityClass()); // Key has already been removed from pendingKeys onSuccess(block, false, null, context); } public void onSuccess(KeyBlock lowLevelBlock, boolean fromStore, SendableRequestItem token, ClientContext context) { ClientKeyBlock block; try { block = Key.createKeyBlock(this.key, lowLevelBlock); onSuccess(block, fromStore, token, context); } catch (KeyVerifyException e) { onBlockDecodeError(token, context); } } protected abstract void onBlockDecodeError(SendableRequestItem token, ClientContext context); /** Called when/if the low-level request succeeds. */ public abstract void onSuccess(ClientKeyBlock block, boolean fromStore, Object token, ClientContext context); @Override public long getCooldownWakeup(SendableRequestItem token, ClientContext context) { return cooldownWakeupTime; } public void schedule(ClientContext context) { if(key == null) throw new NullPointerException(); getScheduler(context).register(this, new SendableGet[] { this }, persistent, ctx.blocks, false); } public void reschedule(ClientContext context) { getScheduler(context).register(null, new SendableGet[] { this }, persistent, ctx.blocks, true); } public SendableGet getRequest(Key key) { return this; } @Override public Key[] listKeys() { synchronized(this) { if(cancelled || finished) return new Key[0]; } return new Key[] { key.getNodeKey(true) }; } @Override public KeyListener makeKeyListener(ClientContext context, boolean onStartup) { synchronized(this) { if(finished) return null; if(cancelled) return null; } if(key == null) { Logger.error(this, "Key is null - left over BSSF? on "+this+" in makeKeyListener()", new Exception("error")); return null; } Key newKey = key.getNodeKey(true); if(parent == null) { Logger.error(this, "Parent is null on "+this+" persistent="+persistent+" key="+key+" ctx="+ctx); return null; } short prio = parent.getPriorityClass(); KeyListener ret = new SingleKeyListener(newKey, this, prio, persistent); return ret; } protected abstract void notFoundInStore(ClientContext context); @Override public boolean preRegister(ClientContext context, boolean toNetwork) { if(!toNetwork) return false; boolean localOnly = ctx.localRequestOnly; if(localOnly) { notFoundInStore(context); return true; } parent.toNetwork(context); return false; } @Override public synchronized long getWakeupTime(ClientContext context, long now) { if(cancelled || finished) return -1; long wakeTime = cooldownWakeupTime; if(wakeTime <= now) cooldownWakeupTime = wakeTime = 0; KeysFetchingLocally fetching = getScheduler(context).fetchingKeys(); if(wakeTime <= 0 && fetching.hasKey(getNodeKey(null), this)) { wakeTime = Long.MAX_VALUE; // tracker.cooldownWakeupTime is only set for a real cooldown period, NOT when we go into hierarchical cooldown because the request is already running. } if(wakeTime == 0) return 0; return wakeTime; } /** Reread the cached cooldown values (and anything else) from the FetchContext * after it changes. FIXME: Ideally this should be a generic mechanism, but * that looks too complex without significant changes to data structures. * For now it's just a hack to make changing the polling interval in USKs work. * @see <a href="https://bugs.freenetproject.org/view.php?id=4984">Bug</a> * @param context The context object. */ public void onChangedFetchContext(ClientContext context) { synchronized(this) { if(cancelled || finished) return; } innerCheckCachedCooldownData(); } public void onResume(ClientContext context) { schedule(context); } }