/* 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.io.ByteArrayOutputStream;
import java.io.DataOutputStream;
import java.io.IOException;
import java.io.Serializable;
import java.util.Date;
import java.util.WeakHashMap;
import freenet.crypt.ChecksumChecker;
import freenet.keys.FreenetURI;
import freenet.node.RequestClient;
import freenet.node.SendableRequest;
import freenet.node.useralerts.SimpleUserAlert;
import freenet.node.useralerts.UserAlert;
import freenet.support.CurrentTimeUTC;
import freenet.support.Logger;
import freenet.support.Logger.LogLevel;
import freenet.support.io.ResumeFailedException;
/** A high level request or insert. This may create any number of low-level requests of inserts,
* for example a request may follow redirects, download splitfiles and unpack containers, while an
* insert (for a file or a freesite) may also have to insert many blocks. A high-level request is
* created by a client, has a FetchContext or InsertContext for configuration. Compare to
* @see SendableRequest for a low-level request (which may still be multiple actual requests or
* inserts).
* WARNING: Changing non-transient members on classes that are Serializable can result in
* restarting downloads or losing uploads.
*/
public abstract class ClientRequester implements Serializable, ClientRequestSchedulerGroup {
private static final long serialVersionUID = 1L;
private static volatile boolean logMINOR;
static {
Logger.registerClass(ClientRequester.class);
}
public abstract void onTransition(ClientGetState oldState, ClientGetState newState, ClientContext context);
// FIXME move the priority classes from RequestStarter here
/** Priority class of the request or insert. */
protected short priorityClass;
/** Whether this is a real-time request */
protected final boolean realTimeFlag;
/** Has the request or insert been cancelled? */
protected boolean cancelled;
/** The RequestClient, used to determine whether this request is
* persistent, and also we round-robin between different RequestClient's
* in scheduling within a given priority class and retry count. */
protected transient RequestClient client;
/** What is our priority class? */
public short getPriorityClass() {
return priorityClass;
}
/**
* zero arg c'tor for db4o on jamvm / for serialization.
*/
protected ClientRequester() {
realTimeFlag = false;
creationTime = 0;
hashCode = 0;
}
protected ClientRequester(short priorityClass, ClientBaseCallback cb) {
this.priorityClass = priorityClass;
this.client = cb.getRequestClient();
this.realTimeFlag = client.realTimeFlag();
if(client == null)
throw new NullPointerException();
hashCode = super.hashCode(); // the old object id will do fine, as long as we ensure it doesn't change!
synchronized(allRequesters) {
if(!persistent())
allRequesters.put(this, dumbValue);
}
creationTime = System.currentTimeMillis();
}
/** Cancel the request. Inner method, subclasses should actually tell
* the ClientGetState or whatever to cancel itself: this does not do
* anything apart from set a flag!
* @return Whether we were already cancelled.
*/
protected synchronized boolean cancel() {
boolean ret = cancelled;
cancelled = true;
return ret;
}
/** Cancel the request. Subclasses must implement to actually tell the
* ClientGetState's or ClientPutState's to cancel.
* @param context The ClientContext object including essential but
* non-persistent objects such as the schedulers.
*/
public abstract void cancel(ClientContext context);
/** Is the request or insert cancelled? */
public boolean isCancelled() {
return cancelled;
}
/** Get the URI for the request or insert. For a request this is set at
* creation, but for an insert, it is set when we know what the final
* URI will be. */
public abstract FreenetURI getURI();
/** Is the request or insert completed (succeeded, failed, or
* cancelled, which is a kind of failure)? */
public abstract boolean isFinished();
private final int hashCode;
/**
* We need a hash code that persists across restarts.
*/
@Override
public int hashCode() {
return hashCode;
}
/** Total number of blocks this request has tried to fetch/put. */
protected int totalBlocks;
/** Number of blocks we have successfully completed a fetch/put for. */
protected int successfulBlocks;
/**
* ATTENTION: This may be null for very old databases.
* @see #getLatestSuccess() Explanation of the content and especially the default value. */
protected Date latestSuccess = CurrentTimeUTC.get();
/** Number of blocks which have failed. */
protected int failedBlocks;
/** Number of blocks which have failed fatally. */
protected int fatallyFailedBlocks;
/** @see #getLatestFailure() */
protected Date latestFailure = null;
/** Minimum number of blocks required to succeed for success. */
protected int minSuccessBlocks;
/** Has totalBlocks stopped growing? */
protected boolean blockSetFinalized;
/** Has at least one block been scheduled to be sent to the network?
* Requests can be satisfied entirely from the datastore sometimes. */
protected boolean sentToNetwork;
public int getTotalBlocks() {
return totalBlocks;
}
/**
* UTC Date of latest increase of {@link #successfulBlocks}.<br>
* Initialized to current time for usability purposes: This allows the user to sort downloads by
* last success in the user interface to determine which ones are stalling - those will be the
* ones with the oldest last success date. If we initialized it to "null" only, that would not
* be possible: The user couldn't distinguish very old stalling downloads from downloads which
* merely had no success yet because they were added a short time ago.<br> */
public Date getLatestSuccess() {
// clone() because Date is mutable.
// Null-check for backwards compatibility: Old serialized versions of objects of this
// class might not have this field yet.
return latestSuccess != null ? (Date)latestSuccess.clone() : new Date(0);
}
/**
* UTC Date of latest increase of {@link #failedBlocks} or {@link #fatallyFailedBlocks}.<br>
* Null if there was no failure yet. */
public Date getLatestFailure() {
// clone() because Date is mutable.
// Null-check for backwards compatibility: Old serialized versions of objects of this
// class might not have this field yet.
return latestFailure != null ? (Date)latestFailure.clone() : null;
}
protected synchronized void resetBlocks() {
totalBlocks = 0;
successfulBlocks = 0;
// See ClientRequester.getLatestSuccess() for why this defaults to current time.
latestSuccess = CurrentTimeUTC.get();
failedBlocks = 0;
fatallyFailedBlocks = 0;
latestFailure = null;
minSuccessBlocks = 0;
blockSetFinalized = false;
sentToNetwork = false;
}
/** The set of blocks has been finalised, total will not change any
* more. Notify clients.
* @param context The ClientContext object including essential but
* non-persistent objects such as the schedulers.
*/
public void blockSetFinalized(ClientContext context) {
synchronized(this) {
if(blockSetFinalized) return;
blockSetFinalized = true;
}
if(logMINOR)
Logger.minor(this, "Finalized set of blocks for "+this, new Exception("debug"));
notifyClients(context);
}
/** Add a block to our estimate of the total. Don't notify clients. */
public void addBlock() {
boolean wasFinalized;
synchronized (this) {
totalBlocks++;
wasFinalized = blockSetFinalized;
}
if (wasFinalized) {
if (LogLevel.MINOR.matchesThreshold(Logger.globalGetThresholdNew()))
Logger.error(this, "addBlock() but set finalized! on " + this, new Exception("error"));
else
Logger.error(this, "addBlock() but set finalized! on " + this);
}
if(logMINOR) Logger.minor(this, "addBlock(): total="+totalBlocks+" successful="+successfulBlocks+" failed="+failedBlocks+" required="+minSuccessBlocks);
}
/** Add several blocks to our estimate of the total. Don't notify clients. */
public void addBlocks(int num) {
boolean wasFinalized;
synchronized (this) {
totalBlocks += num;
wasFinalized = blockSetFinalized;
}
if (wasFinalized) {
if (LogLevel.MINOR.matchesThreshold(Logger.globalGetThresholdNew()))
Logger.error(this, "addBlocks() but set finalized! on "+this, new Exception("error"));
else
Logger.error(this, "addBlocks() but set finalized! on "+this);
}
if(logMINOR) Logger.minor(this, "addBlocks("+num+"): total="+totalBlocks+" successful="+successfulBlocks+" failed="+failedBlocks+" required="+minSuccessBlocks);
}
/** We completed a block. Count it and notify clients unless dontNotify. */
public void completedBlock(boolean dontNotify, ClientContext context) {
if(logMINOR)
Logger.minor(this, "Completed block ("+dontNotify+ "): total="+totalBlocks+" success="+successfulBlocks+" failed="+failedBlocks+" fatally="+fatallyFailedBlocks+" finalised="+blockSetFinalized+" required="+minSuccessBlocks+" on "+this);
synchronized(this) {
if(cancelled) return;
successfulBlocks++;
latestSuccess = CurrentTimeUTC.get();
}
if(dontNotify) return;
notifyClients(context);
}
transient static final UserAlert brokenClientAlert = new SimpleUserAlert(true, "Some broken downloads/uploads were cancelled. Please restart them.", "Some downloads/uploads were broken due to a bug (some time before 1287) causing unrecoverable database corruption. They have been cancelled. Please restart them from the Downloads or Uploads page.", "Some downloads/uploads were broken due to a pre-1287 bug, please restart them.", UserAlert.ERROR);
/** A block failed. Count it and notify our clients. */
public void failedBlock(boolean dontNotify, ClientContext context) {
synchronized(this) {
failedBlocks++;
latestFailure = CurrentTimeUTC.get();
}
if(!dontNotify)
notifyClients(context);
}
/** A block failed. Count it and notify our clients. */
public void failedBlock(ClientContext context) {
failedBlock(false, context);
}
/** A block failed fatally. Count it and notify our clients. */
public void fatallyFailedBlock(ClientContext context) {
synchronized(this) {
fatallyFailedBlocks++;
latestFailure = CurrentTimeUTC.get();
}
notifyClients(context);
}
/** Add one or more blocks to the number of requires blocks, and don't notify the clients. */
public synchronized void addMustSucceedBlocks(int blocks) {
totalBlocks += blocks;
minSuccessBlocks += blocks;
if(logMINOR) Logger.minor(this, "addMustSucceedBlocks("+blocks+"): total="+totalBlocks+" successful="+successfulBlocks+" failed="+failedBlocks+" required="+minSuccessBlocks);
}
/** Insertors should override this. The method is duplicated rather than calling addMustSucceedBlocks to avoid confusing consequences when addMustSucceedBlocks does other things. */
public synchronized void addRedundantBlocksInsert(int blocks) {
totalBlocks += blocks;
minSuccessBlocks += blocks;
if(logMINOR) Logger.minor(this, "addMustSucceedBlocks("+blocks+"): total="+totalBlocks+" successful="+successfulBlocks+" failed="+failedBlocks+" required="+minSuccessBlocks);
}
/** Notify clients by calling innerNotifyClients off-thread. */
public final void notifyClients(ClientContext context) {
context.getJobRunner(persistent()).queueNormalOrDrop(new PersistentJob() {
@Override
public boolean run(ClientContext context) {
innerNotifyClients(context);
return false;
}
});
}
/** Notify clients, usually via a SplitfileProgressEvent, of the current progress. Called
* off-thread. Please do not change SimpleEventProducer to always produce events off-thread, it
* is better to deal with that here, because events could be re-ordered, which matters for some
* events notably SimpleProgressEvent. */
protected abstract void innerNotifyClients(ClientContext context);
/** Called when we first send a request to the network. Ensures that it really is the first time and
* passes on to innerToNetwork().
*/
public void toNetwork(ClientContext context) {
synchronized(this) {
if(sentToNetwork) return;
sentToNetwork = true;
}
innerToNetwork(context);
}
/** Notify clients that a request has gone to the network, for the first time, i.e. we have finished
* checking the datastore for at least one part of the request. */
protected abstract void innerToNetwork(ClientContext context);
protected void clearCountersOnRestart() {
this.blockSetFinalized = false;
this.cancelled = false;
this.failedBlocks = 0;
this.fatallyFailedBlocks = 0;
this.latestFailure = null;
this.minSuccessBlocks = 0;
this.sentToNetwork = false;
this.successfulBlocks = 0;
// See ClientRequester.getLatestSuccess() for why this defaults to current time.
this.latestSuccess = CurrentTimeUTC.get();
this.totalBlocks = 0;
}
/** Get client context object */
public RequestClient getClient() {
return client;
}
/** Change the priority class of the request (request includes inserts here).
* @param newPriorityClass The new priority class for the request or insert.
* @param ctx The ClientContext, contains essential transient objects such as the schedulers.
*/
public void setPriorityClass(short newPriorityClass, ClientContext ctx) {
short oldPrio;
synchronized(this) {
oldPrio = priorityClass;
this.priorityClass = newPriorityClass;
}
if(logMINOR) Logger.minor(this, "Changing priority class of "+this+" from "+oldPrio+" to "+newPriorityClass);
ctx.getChkFetchScheduler(realTimeFlag).reregisterAll(this, oldPrio);
ctx.getChkInsertScheduler(realTimeFlag).reregisterAll(this, oldPrio);
ctx.getSskFetchScheduler(realTimeFlag).reregisterAll(this, oldPrio);
ctx.getSskInsertScheduler(realTimeFlag).reregisterAll(this, oldPrio);
}
public boolean realTimeFlag() {
return realTimeFlag;
}
/** Is this request persistent? */
public boolean persistent() {
return client.persistent();
}
private static WeakHashMap<ClientRequester,Object> allRequesters = new WeakHashMap<ClientRequester,Object>();
private static Object dumbValue = new Object();
public final long creationTime;
public static ClientRequester[] getAll() {
synchronized(allRequesters) {
return allRequesters.keySet().toArray(new ClientRequester[0]);
}
}
/** @return A byte[] representing the original client, to be written to the file storing a
* persistent download. E.g. for FCP, this will include the Identifier, whether it is on the
* global queue and the client name.
* @param checker Used to checksum and isolate large components where we can recover if they
* fail.
* @throws IOException */
public byte[] getClientDetail(ChecksumChecker checker) throws IOException {
return new byte[0];
}
protected static byte[] getClientDetail(PersistentClientCallback callback, ChecksumChecker checker) throws IOException {
ByteArrayOutputStream baos = new ByteArrayOutputStream();
DataOutputStream dos = new DataOutputStream(baos);
callback.getClientDetail(dos, checker);
return baos.toByteArray();
}
private transient boolean resumed = false;
/** Called for a persistent request after startup. Should call notifyClients() at the end,
* after the callback has been registered etc.
* @throws ResumeFailedException */
public final void onResume(ClientContext context) throws ResumeFailedException {
synchronized(this) {
if(resumed) return;
resumed = true;
}
innerOnResume(context);
}
/** Called by onResume() once and only once after restarting. Must be overridden, and must call
* super.innerOnResume().
* @throws ResumeFailedException */
protected void innerOnResume(ClientContext context) throws ResumeFailedException {
ClientBaseCallback cb = getCallback();
client = cb.getRequestClient();
assert(client.persistent());
if(sentToNetwork)
innerToNetwork(context);
}
protected abstract ClientBaseCallback getCallback();
/** Called just before the final write when shutting down the node. */
public void onShutdown(ClientContext context) {
// Do nothing.
}
public boolean isCurrentState(ClientGetState state) {
return false;
}
/**
* Get the group the request belongs to. For single requests (the default) this is the request
* itself; for those in a group, such as a site insert, it is a common value between them.
*/
public ClientRequestSchedulerGroup getSchedulerGroup() {
return this;
}
}