package freenet.clients.fcp; import java.io.DataInputStream; import java.io.DataOutputStream; import java.io.IOException; import java.io.Serializable; import freenet.client.async.ClientContext; import freenet.client.async.ClientRequester; import freenet.client.async.PersistenceDisabledException; import freenet.client.async.PersistentJob; import freenet.crypt.ChecksumChecker; import freenet.keys.FreenetURI; import freenet.node.PrioRunnable; import freenet.node.RequestClient; import freenet.node.RequestClientBuilder; import freenet.node.RequestStarter; import freenet.support.LogThresholdCallback; import freenet.support.Logger; import freenet.support.Logger.LogLevel; import freenet.support.io.NativeThread; import freenet.support.io.ResumeFailedException; import freenet.support.io.StorageFormatException; /** * A request process carried out by the node for an FCP client. * Examples: ClientGet, ClientPut, MultiGet. */ public abstract class ClientRequest implements Serializable { /** * ATTENTION: When incrementing this, please skip version 2. Version 2 had already temporarily * been used by a development branch. */ private static final long serialVersionUID = 1L; /** URI to fetch, or target URI to insert to */ protected FreenetURI uri; /** Unique request identifier */ protected final String identifier; /** Verbosity level. Relevant to all ClientRequests, although they interpret it * differently. */ protected final int verbosity; /** Original FCPConnectionHandler. Null if persistence != connection */ protected transient final FCPConnectionHandler origHandler; /** Is the request on the global queue? */ protected final boolean global; /** If the request isn't on the global queue, what is the client's name? */ protected final String clientName; /** Client */ protected transient PersistentRequestClient client; /** Priority class */ protected short priorityClass; /** Is the request scheduled as "real-time" (as opposed to bulk)? */ protected final boolean realTime; /** Persistence type */ protected final Persistence persistence; /** Has the request finished? */ protected boolean finished; /** Client token (string to feed back to the client on a Persistent* when he does a * ListPersistentRequests). */ protected String clientToken; /** Timestamp : startup time */ protected final long startupTime; /** Timestamp : completion time */ protected long completionTime; protected transient RequestClient lowLevelClient; private final int hashCode; // for debugging it is good to have a persistent id @Override public int hashCode() { return hashCode; } private static volatile boolean logMINOR; static { Logger.registerLogThresholdCallback(new LogThresholdCallback() { @Override public void shouldUpdate() { logMINOR = Logger.shouldLog(LogLevel.MINOR, this); } }); } public ClientRequest(FreenetURI uri2, String identifier2, int verbosity2, String charset, FCPConnectionHandler handler, PersistentRequestClient client, short priorityClass2, Persistence persistenceType2, boolean realTime, String clientToken2, boolean global) { int hash = super.hashCode(); if(hash == 0) hash = 1; hashCode = hash; this.uri = uri2; this.identifier = identifier2; if(global) { this.verbosity = Integer.MAX_VALUE; this.clientName = null; } else { this.verbosity = verbosity2; this.clientName = client.name; } this.finished = false; this.priorityClass = priorityClass2; this.persistence = persistenceType2; this.clientToken = clientToken2; this.global = global; if(persistence == Persistence.CONNECTION) { this.origHandler = handler; lowLevelClient = origHandler.connectionRequestClient(realTime); this.client = null; } else { origHandler = null; this.client = client; assert client != null; assert(client.persistence == persistence); lowLevelClient = client.lowLevelClient(realTime); } assert lowLevelClient != null; this.startupTime = System.currentTimeMillis(); this.realTime = realTime; } public ClientRequest(FreenetURI uri2, String identifier2, int verbosity2, String charset, FCPConnectionHandler handler, short priorityClass2, Persistence persistenceType2, final boolean realTime, String clientToken2, boolean global) { int hash = super.hashCode(); if(hash == 0) hash = 1; hashCode = hash; this.uri = uri2; this.identifier = identifier2; this.finished = false; this.priorityClass = priorityClass2; this.persistence = persistenceType2; this.clientToken = clientToken2; this.global = global; if(persistence == Persistence.CONNECTION) { this.origHandler = handler; client = null; lowLevelClient = new RequestClientBuilder().realTime(realTime).build(); this.clientName = null; this.verbosity = verbosity2; } else { origHandler = null; if(global) { client = persistence == Persistence.FOREVER ? handler.server.globalForeverClient : handler.server.globalRebootClient; this.verbosity = Integer.MAX_VALUE; clientName = null; } else { client = persistence == Persistence.FOREVER ? handler.getForeverClient() : handler.getRebootClient(); this.verbosity = verbosity2; this.clientName = client.name; } lowLevelClient = client.lowLevelClient(realTime); if(lowLevelClient == null) throw new NullPointerException("No lowLevelClient from client: "+client+" global = "+global+" persistence = "+persistence); } if(lowLevelClient.persistent() != (persistence == Persistence.FOREVER)) throw new IllegalStateException("Low level client.persistent="+lowLevelClient.persistent()+" but persistence type = "+persistence); if(client != null) assert(client.persistence == persistence); this.startupTime = System.currentTimeMillis(); this.realTime = realTime; } protected ClientRequest() { // For serialization. identifier = null; verbosity = 0; origHandler = null; global = false; clientName = null; realTime = false; persistence = null; startupTime = 0; hashCode = 0; } /** Lost connection */ public abstract void onLostConnection(ClientContext context); /** Send any pending messages for a persistent request e.g. after reconnecting */ public abstract void sendPendingMessages(FCPConnectionOutputHandler handler, String listRequestIdentifier, boolean includePersistentRequest, boolean includeData, boolean onlyData); // Persistence public enum Persistence { /** Default: persists until connection loss. */ CONNECTION, /** Reports to client by name; persists over connection loss. * Not saved to disk, so dies on reboot. */ REBOOT, /** Same as reboot but saved to disk, persists forever. */ FOREVER; public static Persistence parseOrThrow(String persistenceString, String identifier, boolean global) throws MessageInvalidException { try { if(persistenceString == null) return Persistence.CONNECTION; else return Persistence.valueOf(persistenceString.toUpperCase()); } catch (IllegalArgumentException e) { throw new MessageInvalidException(ProtocolErrorMessage.ERROR_PARSING_NUMBER, "Error parsing Persistence field: "+persistenceString, identifier, global); } } @Deprecated // Only for migration public static Persistence getByCode(short persistenceType) { if(persistenceType < 0 || persistenceType > values().length) throw new IllegalArgumentException(); return values()[persistenceType]; } } abstract void register(boolean noTags) throws IdentifierCollisionException; public void cancel(ClientContext context) { ClientRequester cr = getClientRequest(); // It might have been finished on startup. if(logMINOR) Logger.minor(this, "Cancelling "+cr+" for "+this+" persistence = "+persistence); if(cr != null) cr.cancel(context); freeData(); } public boolean isPersistentForever() { return persistence == Persistence.FOREVER; } /** Is the request persistent? False = we can drop the request if we lose the connection */ public boolean isPersistent() { return persistence != Persistence.CONNECTION; } public boolean hasFinished() { return finished; } /** Get identifier string for request */ public String getIdentifier() { return identifier; } protected abstract ClientRequester getClientRequest(); /** Completed request dropped off the end without being acknowledged */ public void dropped(ClientContext context) { cancel(context); freeData(); } /** Return the priority class */ public short getPriority(){ return priorityClass; } /** Free cached data bucket(s) */ protected abstract void freeData(); /** Request completed. But we may have to stick around until we are acked. */ protected void finish() { if(persistence == Persistence.CONNECTION) origHandler.finishedClientRequest(this); else client.finishedClientRequest(this); } public abstract double getSuccessFraction(); public abstract double getTotalBlocks(); public abstract double getMinBlocks(); public abstract double getFetchedBlocks(); public abstract double getFailedBlocks(); public abstract double getFatalyFailedBlocks(); public abstract String getFailureReason(boolean longDescription); /** * Has the total number of blocks to insert been determined yet? */ public abstract boolean isTotalFinalized(); /** Start the request, if it has not already been started. */ public abstract void start(ClientContext context); protected boolean started; public boolean isStarted() { return started; } public abstract boolean hasSucceeded(); /** * Returns the time of the request’s last activity, or {@code 0} if there is * no known last activity. * * @return The time of the request’s last activity, or {@code 0} * @deprecated * Use {@link ClientRequester#getLatestSuccess()} instead. You can use * {@link #getClientRequest()} to obtain the ClientRequester. */ @Deprecated public long getLastActivity() { ClientRequester cr = getClientRequest(); // I have not actually read the code to find out whether this if() is needed. // But this function is deprecated at the time of its writing and thus will be removed soon, // so there is no reason to invest the time to read the code to see whether it will happen. if (cr == null) { return 0; } return cr.getLatestSuccess().getTime(); } public abstract boolean canRestart(); public abstract boolean restart(ClientContext context, boolean disableFilterData) throws PersistenceDisabledException; /** * Called after a ModifyPersistentRequest. * Sends a PersistentRequestModified message to clients if any value changed. */ public void modifyRequest(String newClientToken, short newPriorityClass, FCPServer server) { boolean clientTokenChanged = false; boolean priorityClassChanged = false; if(newClientToken != null) { if( clientToken != null ) { if( !newClientToken.equals(clientToken) ) { this.clientToken = newClientToken; // token changed clientTokenChanged = true; } } else { this.clientToken = newClientToken; // first time the token is set clientTokenChanged = true; } } if(newPriorityClass >= 0 && newPriorityClass != priorityClass) { this.priorityClass = newPriorityClass; ClientRequester r = getClientRequest(); r.setPriorityClass(priorityClass, server.core.clientContext); priorityClassChanged = true; if(client != null) { RequestStatusCache cache = client.getRequestStatusCache(); if(cache != null) { cache.setPriority(identifier, newPriorityClass); } } } if(! ( clientTokenChanged || priorityClassChanged ) ) { return; // quick return, nothing was changed } server.core.clientContext.jobRunner.setCheckpointASAP(); // this could become too complex with more parameters, but for now its ok final PersistentRequestModifiedMessage modifiedMsg; if( clientTokenChanged && priorityClassChanged ) { modifiedMsg = new PersistentRequestModifiedMessage(identifier, global, priorityClass, clientToken); } else if( priorityClassChanged ) { modifiedMsg = new PersistentRequestModifiedMessage(identifier, global, priorityClass); } else if( clientTokenChanged ) { modifiedMsg = new PersistentRequestModifiedMessage(identifier, global, clientToken); } else { return; // paranoia, we should not be here if nothing was changed! } client.queueClientRequestMessage(modifiedMsg, 0); } public void restartAsync(final FCPServer server, final boolean disableFilterData) throws PersistenceDisabledException { synchronized(this) { this.started = false; } if(client != null) { RequestStatusCache cache = client.getRequestStatusCache(); if(cache != null) { cache.updateStarted(identifier, false); } } if(persistence == Persistence.FOREVER) { server.core.clientContext.jobRunner.queue(new PersistentJob() { @Override public boolean run(ClientContext context) { try { restart(context, disableFilterData); } catch (PersistenceDisabledException e) { // Impossible } return true; } }, NativeThread.HIGH_PRIORITY); } else { server.core.getExecutor().execute(new PrioRunnable() { @Override public int getPriority() { return NativeThread.NORM_PRIORITY; } @Override public void run() { try { restart(server.core.clientContext, disableFilterData); } catch (PersistenceDisabledException e) { // Impossible } } }, "Restart request"); } } /** * Called after a RemovePersistentRequest. Send a PersistentRequestRemoved to the clients. * If the request is in the database, delete it. */ public void requestWasRemoved(ClientContext context) { if(persistence != Persistence.FOREVER) return; } protected boolean isGlobalQueue() { if(client == null) return false; return client.isGlobalQueue; } public PersistentRequestClient getClient(){ return client; } abstract RequestStatus getStatus(); private static final long CLIENT_DETAIL_MAGIC = 0xebf0b4f4fa9f6721L; private static final int CLIENT_DETAIL_VERSION = 1; public void getClientDetail(DataOutputStream dos, ChecksumChecker checker) throws IOException { if(persistence != Persistence.FOREVER) return; dos.writeLong(CLIENT_DETAIL_MAGIC); dos.writeInt(CLIENT_DETAIL_VERSION); // Identify the request first. RequestIdentifier req = getRequestIdentifier(); req.writeTo(dos); // Basic details needed for scheduling, reporting and completion. dos.writeBoolean(realTime); dos.writeInt(verbosity); dos.writeLong(startupTime); // persistence is assumed to be PERSIST_FOREVER. // uri will be handled by subclasses. // This can change. dos.writeShort(priorityClass); // This can change and is variable size. if(clientToken == null) dos.writeBoolean(false); else { dos.writeBoolean(true); dos.writeUTF(clientToken); } // Stuff that changes on completion dos.writeBoolean(finished); } protected ClientRequest(DataInputStream dis, RequestIdentifier reqID, ClientContext context) throws IOException, StorageFormatException { long magic = dis.readLong(); if(magic != CLIENT_DETAIL_MAGIC) throw new StorageFormatException("Bad magic"); int version = dis.readInt(); if(version != CLIENT_DETAIL_VERSION) throw new StorageFormatException("Bad version"); RequestIdentifier copyReq = new RequestIdentifier(dis); if(!copyReq.equals(reqID)) throw new StorageFormatException("Request identifier has changed"); realTime = dis.readBoolean(); verbosity = dis.readInt(); startupTime = dis.readLong(); priorityClass = dis.readShort(); if(priorityClass < RequestStarter.MAXIMUM_PRIORITY_CLASS || priorityClass > RequestStarter.PAUSED_PRIORITY_CLASS) throw new StorageFormatException("Bogus priority"); if(dis.readBoolean()) clientToken = dis.readUTF(); else clientToken = null; finished = dis.readBoolean(); persistence = Persistence.FOREVER; origHandler = null; identifier = reqID.identifier; global = reqID.globalQueue; clientName = reqID.clientName; hashCode = super.hashCode(); // We can't wait until onResume() to get the client, because it may be used in the // constructors. this.client = context.persistentRoot.makeClient(global, clientName); this.lowLevelClient = client.lowLevelClient(realTime); } /** Called just after serializing in the request. Called by the ClientRequester, i.e. the tree * starts there, and we MUST NOT call back to it or we get an infinite recursion. The main * purpose of this method is to give us an opportunity to connect to the various (transient) * system utilities we get from ClientContext, e.g. bucket factories, the FCP persistent root * etc. The base class implementation in ClientRequest will register the request with an * PersistentRequestClient via the new PersistentRequestRoot. * @param context Contains all the important system utilities. * @throws ResumeFailedException */ public final void onResume(ClientContext context) throws ResumeFailedException { client = context.persistentRoot.makeClient(global, clientName); lowLevelClient = client.lowLevelClient(realTime); innerResume(context); ClientRequester req = getClientRequest(); if(req != null) req.onResume(context); // Can legally be null. context.persistentRoot.resume(this, global, clientName); } protected abstract void innerResume(ClientContext context) throws ResumeFailedException; public RequestClient getRequestClient() { return lowLevelClient; } /** Get the RequestIdentifier. This just includes the queue and the identifier. */ public RequestIdentifier getRequestIdentifier() { if(persistence == Persistence.CONNECTION) throw new IllegalStateException(); // Not associated with any client. return new RequestIdentifier(global, clientName, identifier, getType()); } abstract RequestIdentifier.RequestType getType(); public static ClientRequest restartFrom(DataInputStream dis, RequestIdentifier reqID, ClientContext context, ChecksumChecker checker) throws StorageFormatException, IOException, ResumeFailedException { switch(reqID.type) { case GET: return ClientGet.restartFrom(dis, reqID, context, checker); default: return null; } } /** Return true if we resumed the original fetch from stored data (usually a file for a * splitfile download), rather than having to restart it (which happens in most other cases * when we resume). */ public abstract boolean fullyResumed(); /** Called just before the final write when the node is shutting down. Should write any dirty * data to disk etc. */ public void onShutdown(ClientContext context) { ClientRequester request = getClientRequest(); if(request != null) request.onShutdown(context); } }