package freenet.client.async; import java.io.BufferedOutputStream; import java.io.ByteArrayInputStream; import java.io.ByteArrayOutputStream; import java.io.DataInput; import java.io.DataInputStream; import java.io.DataOutput; import java.io.DataOutputStream; import java.io.File; import java.io.IOException; import java.io.InputStream; import java.io.ObjectInputStream; import java.io.ObjectOutputStream; import java.io.OutputStream; import java.util.HashMap; import java.util.Map; import java.util.Random; import freenet.clients.fcp.ClientRequest; import freenet.clients.fcp.RequestIdentifier; import freenet.crypt.CRCChecksumChecker; import freenet.crypt.ChecksumChecker; import freenet.crypt.ChecksumFailedException; import freenet.node.DatabaseKey; import freenet.node.MasterKeysWrongPasswordException; import freenet.node.Node; import freenet.node.NodeClientCore; import freenet.node.NodeInitException; import freenet.node.RequestStarterGroup; import freenet.support.Executor; import freenet.support.Logger; import freenet.support.Ticker; import freenet.support.api.Bucket; import freenet.support.io.DelayedFree; import freenet.support.io.FileBucket; import freenet.support.io.FileUtil; import freenet.support.io.PersistentTempBucketFactory; import freenet.support.io.PrependLengthOutputStream; import freenet.support.io.StorageFormatException; import freenet.support.io.TempBucketFactory; import static java.util.concurrent.TimeUnit.MINUTES; /** Top level of persistence mechanism for ClientRequest's (persistent downloads and uploads). * Note that we use three different persistence mechanisms here: * 1) Splitfile persistence. The downloaded data and all the status for a splitfile is kept in a * single random access file (technically a LockableRandomAccessBuffer). * 2) Java persistence. The overall list of ClientRequest's is stored to client.dat using * serialization, by this class. * 3) A simple binary fallback. For complicated requests this will just record enough information * to restart the request, but for simple splitfile downloads, we can resume from (1). * * The reason for this seemingly unnecessary complexity is: * 1) Robustness, even against (reasonable) data corruption (e.g. on writing frequently written * parts of files), and against problems in serialization. * 2) Minimising disk I/O (particularly disk seeks), especially in splitfile fetches. Decoding a * segment takes effectively a single read and a couple of writes, for example. * 3) Allowing us to store all the important information about a download, including the downloaded * data and the status, in a temporary file close to where the final file will be saved. Then we * can (if there is no compression or filtering) simply truncate the file to complete. * * Also, most of the important global structures are kept in RAM and recreated after downloads are * read in. Notably ClientRequestScheduler/Selector, which keep a tree of requests to choose from, * and a set of Bloom filters to identify which blocks belong to which request (we won't always get * a block as the direct result of doing our own requests). * * Please don't shove it all into a database without serious consideration of the performance and * reliability implications! One query per block is not feasible on typical end user hardware, and * disks aren't reliable. Losing all downloads when there is a one byte data corruption is * unacceptable. And blocks should be kept close to where they are supposed to end up... * * Note that inserts may be somewhat less robust than requests. This is intentional as inserts * should be relatively short-lived or they won't be much use to anyone as the data will have * fallen out. * * SCHEMA MIGRATION: Note that changing classes that are Serializable can result in restarting * downloads or losing uploads. * @author toad */ public class ClientLayerPersister extends PersistentJobRunnerImpl { static final long INTERVAL = MINUTES.toMillis(10); private final Node node; // Needed for bandwidth stats putter private final NodeClientCore clientCore; private final PersistentTempBucketFactory persistentTempFactory; /** Needed for temporary storage when writing objects. Some of them might be big, e.g. site * inserts. */ private final TempBucketFactory tempBucketFactory; private final PersistentStatsPutter bandwidthStatsPutter; private byte[] salt; private boolean newSalt; private final ChecksumChecker checker; // Can be set later ... private Bucket writeToBucket; private File writeToFilename; private File writeToBackupFilename; private File deleteAfterSuccessfulWrite; private File otherDeleteAfterSuccessfulWrite; private File dir; private String baseName; private static final long MAGIC = 0xd332925f3caf4aedL; private static final int VERSION = 1; private static volatile boolean logMINOR; static { Logger.registerClass(ClientLayerPersister.class); } /** Load everything. * @param persistentTempFactory Only passed in so that we can call its pre- and post- commit * hooks. We don't explicitly save it; it must be populated lazily in onResume() like * everything else. */ public ClientLayerPersister(Executor executor, Ticker ticker, Node node, NodeClientCore core, PersistentTempBucketFactory persistentTempFactory, TempBucketFactory tempBucketFactory, PersistentStatsPutter stats) { super(executor, ticker, INTERVAL); this.node = node; this.clientCore = core; this.persistentTempFactory = persistentTempFactory; this.tempBucketFactory = tempBucketFactory; this.checker = new CRCChecksumChecker(); this.bandwidthStatsPutter = stats; } /** Set the files to write to and set up encryption * @param noWrite If true, don't write the data to disk at all, and delete existing * client.dat*. * @throws MasterKeysWrongPasswordException If we need the encryption key but it has not been * supplied. */ public void setFilesAndLoad(File dir, String baseName, boolean writeEncrypted, boolean noWrite, DatabaseKey encryptionKey, ClientContext context, RequestStarterGroup requestStarters, Random random) throws MasterKeysWrongPasswordException { if(noWrite) super.disableWrite(); synchronized(serializeCheckpoints) { this.dir = dir; this.baseName = baseName; if(noWrite) { writeToBucket = null; writeToFilename = null; writeToBackupFilename = null; deleteFile(dir, baseName, false, false); deleteFile(dir, baseName, false, true); deleteFile(dir, baseName, true, false); deleteFile(dir, baseName, true, true); onStarted(true); if(salt == null) { salt = new byte[32]; random.nextBytes(salt); requestStarters.setGlobalSalt(salt); } } else if(!hasLoaded()) { // Some serialization failures cause us to fail only at the point of scheduling the request. // So if that happens we need to retry with serialization turned off. // The requests that loaded fine already will not be affected as we check for duplicates. if(innerSetFilesAndLoad(false, dir, baseName, writeEncrypted, encryptionKey, context, requestStarters, random)) { Logger.error(this, "Some requests failed to restart after serializing. Trying to recover/restart ..."); System.err.println("Some requests failed to restart after serializing. Trying to recover/restart ..."); innerSetFilesAndLoad(true, dir, baseName, writeEncrypted, encryptionKey, context, requestStarters, random); } onStarted(noWrite); } else { innerSetFilesOnly(dir, baseName, writeEncrypted, encryptionKey); onStarted(false); } } } private void deleteFile(File dir, String baseName, boolean backup, boolean encrypted) { File f = makeFilename(dir, baseName, backup, encrypted); try { FileUtil.secureDelete(f); } catch (IOException e) { f.delete(); if(f.exists()) { System.err.println("Failed to delete "+f+" when setting maximum security level."); System.err.println("There may be traces on disk of your previous download queue."); // FIXME useralert??? } } } private void innerSetFilesOnly(File dir, String baseName, boolean writeEncrypted, DatabaseKey encryptionKey) throws MasterKeysWrongPasswordException { if(writeEncrypted && encryptionKey == null) throw new MasterKeysWrongPasswordException(); File oldWriteToFilename = writeToFilename; writeToBucket = makeBucket(dir, baseName, false, writeEncrypted ? encryptionKey : null); writeToFilename = makeFilename(dir, baseName, false, writeEncrypted); writeToBackupFilename = makeFilename(dir, baseName, true, writeEncrypted); if(writeToFilename.equals(oldWriteToFilename)) return; System.out.println("Will save downloads to "+writeToFilename); deleteAfterSuccessfulWrite = makeFilename(dir, baseName, false, !writeEncrypted); otherDeleteAfterSuccessfulWrite = makeFilename(dir, baseName, true, !writeEncrypted); queueNormalOrDrop(new PersistentJob() { @Override public boolean run(ClientContext context) { return true; // Force a checkpoint ASAP. // This also avoids any possible locking issues. } }); } private boolean innerSetFilesAndLoad(boolean noSerialize, File dir, String baseName, boolean writeEncrypted, DatabaseKey encryptionKey, ClientContext context, RequestStarterGroup requestStarters, Random random) throws MasterKeysWrongPasswordException { if(writeEncrypted && encryptionKey == null) throw new MasterKeysWrongPasswordException(); File clientDat = new File(dir, baseName); File clientDatCrypt = new File(dir, baseName+".crypt"); File clientDatBak = new File(dir, baseName+".bak"); File clientDatBakCrypt = new File(dir, baseName+".bak.crypt"); boolean clientDatExists = clientDat.exists(); boolean clientDatCryptExists = clientDatCrypt.exists(); boolean clientDatBakExists = clientDatBak.exists(); boolean clientDatBakCryptExists = clientDatBakCrypt.exists(); if(encryptionKey == null) { if(clientDatCryptExists || clientDatBakCryptExists) throw new MasterKeysWrongPasswordException(); } boolean failedSerialize = false; PartialLoad loaded = new PartialLoad(); if(clientDatExists) { innerLoad(loaded, makeBucket(dir, baseName, false, null), noSerialize, context, requestStarters, random); } if(clientDatCryptExists && loaded.needsMore()) { innerLoad(loaded, makeBucket(dir, baseName, false, encryptionKey), noSerialize, context, requestStarters, random); } if(clientDatBakExists) { innerLoad(loaded, makeBucket(dir, baseName, true, null), noSerialize, context, requestStarters, random); } if(clientDatBakCryptExists && loaded.needsMore()) { innerLoad(loaded, makeBucket(dir, baseName, true, encryptionKey), noSerialize, context, requestStarters, random); } deleteAfterSuccessfulWrite = writeEncrypted ? clientDat : clientDatCrypt; otherDeleteAfterSuccessfulWrite = writeEncrypted ? clientDatBak : clientDatBakCrypt; writeToBucket = makeBucket(dir, baseName, false, writeEncrypted ? encryptionKey : null); writeToFilename = makeFilename(dir, baseName, false, writeEncrypted); writeToBackupFilename = makeFilename(dir, baseName, true, writeEncrypted); if(loaded.doneSomething()) { if(!noSerialize) { onLoading(); if(loaded.getSalt() == null) { salt = new byte[32]; random.nextBytes(salt); Logger.error(this, "Checksum failed for salt value"); System.err.println("Salt value corrupted, downloads will need to regenerate Bloom filters, this may cause some delay and disk/CPU usage..."); newSalt = true; } else { salt = loaded.salt; } } int success = 0; int restoredRestarted = 0; int restoredFully = 0; int failed = 0; // Resume the requests. for(PartiallyLoadedRequest partial : loaded.partiallyLoadedRequests.values()) { ClientRequest req = partial.request; if(req == null) continue; try { req.onResume(context); if(partial.status == RequestLoadStatus.RESTORED_FULLY || partial.status == RequestLoadStatus.RESTORED_RESTARTED) { req.start(context); } switch(partial.status) { case LOADED: success++; break; case RESTORED_FULLY: restoredFully++; break; case RESTORED_RESTARTED: restoredRestarted++; break; case FAILED: failed++; break; } } catch (Throwable t) { if(partial.status == RequestLoadStatus.LOADED) failedSerialize = true; failed++; System.err.println("Unable to resume request "+req+" after loading it."); Logger.error(this, "Unable to resume request "+req+" after loading it: "+t, t); try { req.cancel(context); } catch (Throwable t1) { Logger.error(this, "Unable to terminate "+req+" after failure: "+t1, t1); } } } if(success > 0) System.out.println("Resumed "+success+" requests ..."); if(restoredFully > 0) System.out.println("Restored "+restoredFully+" requests (in spite of data corruption)"); if(restoredRestarted > 0) System.out.println("Restarted "+restoredRestarted+" requests (due to data corruption)"); if(failed > 0) System.err.println("Failed to restore "+failed+" requests due to data corruption"); return failedSerialize; } else { // FIXME backups etc! System.err.println("Starting request persistence layer without resuming ..."); salt = new byte[32]; random.nextBytes(salt); requestStarters.setGlobalSalt(salt); onStarted(false); return false; } } /** Create a Bucket for client.dat[.bak][.crypt]. * @param dir The parent directory. * @param baseName The base name, usually "client.dat". * @param backup True if we want the .bak file. * @param encryptionKey Non-null if we want an encrypted file. */ private Bucket makeBucket(File dir, String baseName, boolean backup, DatabaseKey encryptionKey) { File filename = makeFilename(dir, baseName, backup, encryptionKey != null); Bucket bucket = new FileBucket(filename, false, false, false, false); if(encryptionKey != null) bucket = encryptionKey.createEncryptedBucketForClientLayer(bucket); return bucket; } private File makeFilename(File parent, String baseName, boolean backup, boolean encrypted) { return new File(parent, baseName + (backup ? ".bak" : "") + (encrypted ? ".crypt" : "")); } private enum RequestLoadStatus { // In order of preference, best first. LOADED, RESTORED_FULLY, RESTORED_RESTARTED, FAILED } private class PartiallyLoadedRequest { final ClientRequest request; final RequestLoadStatus status; PartiallyLoadedRequest(ClientRequest request, RequestLoadStatus status) { this.request = request; this.status = status; } } private class PartialLoad { private final Map<RequestIdentifier, PartiallyLoadedRequest> partiallyLoadedRequests = new HashMap<RequestIdentifier, PartiallyLoadedRequest>(); private byte[] salt; private boolean somethingFailed; private boolean doneSomething; /** Add a partially loaded request. * @param reqID The request identifier. Must be non-null; caller should regenerate it if * necessary. */ void addPartiallyLoadedRequest(RequestIdentifier reqID, ClientRequest request, RequestLoadStatus status) { if(reqID == null) { if(request == null) { somethingFailed = true; return; } else { reqID = request.getRequestIdentifier(); } } PartiallyLoadedRequest old = partiallyLoadedRequests.get(reqID); if(old == null || old.status.ordinal() > status.ordinal()) { partiallyLoadedRequests.put(reqID, new PartiallyLoadedRequest(request, status)); if(!(status == RequestLoadStatus.LOADED || status == RequestLoadStatus.RESTORED_FULLY)) somethingFailed = true; doneSomething = true; } } public boolean needsMore() { return somethingFailed || !doneSomething; } public void setSomethingFailed() { somethingFailed = true; } public void setSalt(byte[] loadedSalt) { if(salt == null) salt = loadedSalt; doneSomething = true; } public byte[] getSalt() { return salt; } public boolean doneSomething() { return doneSomething; } } private void innerLoad(PartialLoad loaded, Bucket bucket, boolean noSerialize, ClientContext context, RequestStarterGroup requestStarters, Random random) { long length = bucket.size(); InputStream fis = null; try { fis = bucket.getInputStream(); innerLoad(loaded, fis, length, !noSerialize && !loaded.doneSomething(), context, requestStarters, random, noSerialize); } catch (IOException e) { // FIXME tell user more obviously. Logger.error(this, "Failed to load persistent requests from "+bucket+" : "+e, e); System.err.println("Failed to load persistent requests from "+bucket+" : "+e); e.printStackTrace(); loaded.setSomethingFailed(); } catch (Throwable t) { Logger.error(this, "Failed to load persistent requests from "+bucket+" : "+t, t); System.err.println("Failed to load persistent requests from "+bucket+" : "+t); t.printStackTrace(); loaded.setSomethingFailed(); } finally { try { if(fis != null) fis.close(); } catch (IOException e) { System.err.println("Failed to load persistent requests: "+e); e.printStackTrace(); } } } private void innerLoad(PartialLoad loaded, InputStream fis, long length, boolean latest, ClientContext context, RequestStarterGroup requestStarters, Random random, boolean noSerialize) throws NodeInitException, IOException { ObjectInputStream ois = new ObjectInputStream(fis); long magic = ois.readLong(); if(magic != MAGIC) throw new IOException("Bad magic"); int version = ois.readInt(); if(version != VERSION) throw new IOException("Bad version"); byte[] salt = new byte[32]; try { checker.readAndChecksum(ois, salt, 0, salt.length); loaded.setSalt(salt); } catch (ChecksumFailedException e1) { Logger.error(this, "Unable to read global salt (checksum failed)"); } requestStarters.setGlobalSalt(salt); int requestCount = ois.readInt(); for(int i=0;i<requestCount;i++) { ClientRequest request = null; RequestIdentifier reqID = readRequestIdentifier(ois); if(reqID != null && context.persistentRoot.hasRequest(reqID)) { Logger.warning(this, "Not reading request because already have it"); skipChecksummedObject(ois, length); // Request itself skipChecksummedObject(ois, length); // Recovery data continue; } try { if(!noSerialize) { request = (ClientRequest) readChecksummedObject(ois, length); if(request != null) { if(reqID != null) { if(!reqID.sameIdentifier(request.getRequestIdentifier())) { Logger.error(this, "Request does not match request identifier, discarding"); request = null; } else { loaded.addPartiallyLoadedRequest(reqID, request, RequestLoadStatus.LOADED); } } } } else skipChecksummedObject(ois, length); } catch (ChecksumFailedException e) { Logger.error(this, "Failed to load request (checksum failed)"); System.err.println("Failed to load a request (checksum failed)"); } catch (Throwable t) { // Some more serious problem. Try to load the rest anyway. Logger.error(this, "Failed to load request: "+t, t); System.err.println("Failed to load a request: "+t); t.printStackTrace(); } if(request == null || logMINOR) { try { ClientRequest restored = readRequestFromRecoveryData(ois, length, reqID); if(request == null && restored != null) { request = restored; boolean loadedFully = restored.fullyResumed(); loaded.addPartiallyLoadedRequest(reqID, request, loadedFully ? RequestLoadStatus.RESTORED_FULLY : RequestLoadStatus.RESTORED_RESTARTED); } } catch (ChecksumFailedException e) { if(request == null) { Logger.error(this, "Failed to recover a request (checksum failed)"); System.err.println("Failed to recover a request (checksum failed)"); } else { Logger.error(this, "Test recovery failed: Checksum failed for "+reqID); } if(request == null) loaded.addPartiallyLoadedRequest(reqID, null, RequestLoadStatus.FAILED); } catch (StorageFormatException e) { if(request == null) { Logger.error(this, "Failed to recovery a request (storage format): "+e, e); System.err.println("Failed to recovery a request (storage format): "+e); e.printStackTrace(); } else { Logger.error(this, "Test recovery failed for "+reqID+" : "+e, e); } if(request == null) loaded.addPartiallyLoadedRequest(reqID, null, RequestLoadStatus.FAILED); } } else { skipChecksummedObject(ois, length); } } if(latest) { try { // Don't bother with the buckets to free or the stats unless reading from the latest version (client.dat not client.dat.bak). readStatsAndBuckets(ois, length, context); } catch (Throwable t) { Logger.error(this, "Failed to restore stats and delete old temp files: "+t, t); } } ois.close(); fis = null; } private void readStatsAndBuckets(ObjectInputStream ois, long length, ClientContext context) throws IOException, ClassNotFoundException { PersistentStatsPutter storedStatsPutter = (PersistentStatsPutter) ois.readObject(); this.bandwidthStatsPutter.addFrom(storedStatsPutter); int count = ois.readInt(); DelayedFree[] buckets = new DelayedFree[count]; for(int i=0;i<count;i++) { try { buckets[i] = (DelayedFree) readChecksummedObject(ois, length); } catch (ChecksumFailedException e) { Logger.warning(this, "Failed to load a bucket to free"); } } persistentTempFactory.finishDelayedFree(buckets); } @Override protected void innerCheckpoint(boolean shutdown) { save(shutdown); } protected void save(boolean shutdown) { if(writeToFilename == null) return; if(writeToFilename.exists()) { FileUtil.renameTo(writeToFilename, writeToBackupFilename); } if(innerSave(shutdown)) { if(deleteAfterSuccessfulWrite != null) { deleteAfterSuccessfulWrite.delete(); deleteAfterSuccessfulWrite = null; } if(otherDeleteAfterSuccessfulWrite != null) { otherDeleteAfterSuccessfulWrite.delete(); otherDeleteAfterSuccessfulWrite = null; } } } private boolean innerSave(boolean shutdown) { DelayedFree[] buckets = persistentTempFactory.grabBucketsToFree(); OutputStream fos = null; try { fos = writeToBucket.getOutputStream(); BufferedOutputStream bos = new BufferedOutputStream(fos); ObjectOutputStream oos = new ObjectOutputStream(bos); oos.writeLong(MAGIC); oos.writeInt(VERSION); checker.writeAndChecksum(oos, salt); ClientRequest[] requests = getRequests(); if(shutdown) { for(ClientRequest req : requests) { if(req == null) continue; try { req.onShutdown(getClientContext()); } catch (Throwable t) { Logger.error(this, "Caught while calling shutdown callback on "+req+": "+t, t); } } } oos.writeInt(requests.length); for(ClientRequest req : requests) { // Write the request identifier so we can skip reading the request if we already have it. writeRequestIdentifier(oos, req.getRequestIdentifier()); // Write the actual request. writeChecksummedObject(oos, req, req.toString()); // Write recovery data. This is just enough to restart the request from scratch, // but may support continuing the request in simple cases e.g. if a fetch is now // just a single splitfile. writeRecoveryData(oos, req); } bandwidthStatsPutter.updateData(node); oos.writeObject(bandwidthStatsPutter); if(buckets == null) { oos.writeInt(0); } else { oos.writeInt(buckets.length); for(DelayedFree bucket : buckets) writeChecksummedObject(oos, bucket, null); } oos.close(); fos = null; Logger.normal(this, "Saved "+requests.length+" requests to "+writeToFilename); persistentTempFactory.finishDelayedFree(buckets); return true; } catch (IOException e) { System.err.println("Failed to write persistent requests: "+e); e.printStackTrace(); return false; } finally { try { if(fos != null) fos.close(); } catch (IOException e) { System.err.println("Failed to write persistent requests: "+e); e.printStackTrace(); } } } private void writeRecoveryData(ObjectOutputStream os, ClientRequest req) throws IOException { PrependLengthOutputStream oos = checker.checksumWriterWithLength(os, tempBucketFactory); DataOutputStream dos = new DataOutputStream(oos); try { req.getClientDetail(dos, checker); dos.close(); oos = null; } catch (Throwable e) { Logger.error(this, "Unable to write recovery data for "+req+" : "+e, e); System.err.println("Unable to write recovery data for "+req+" : "+e); e.printStackTrace(); oos.abort(); } finally { if(oos != null) oos.close(); } } private ClientRequest readRequestFromRecoveryData(ObjectInputStream is, long totalLength, RequestIdentifier reqID) throws IOException, ChecksumFailedException, StorageFormatException { InputStream tmp = checker.checksumReaderWithLength(is, this.tempBucketFactory, totalLength); try { DataInputStream dis = new DataInputStream(tmp); ClientRequest request = ClientRequest.restartFrom(dis, reqID, getClientContext(), checker); dis.close(); dis = null; tmp = null; return request; } catch (Throwable t) { Logger.error(this, "Serialization failed: "+t, t); return null; } finally { if(tmp != null) tmp.close(); } } private void writeChecksummedObject(ObjectOutputStream os, Object req, String name) throws IOException { PrependLengthOutputStream oos = checker.checksumWriterWithLength(os, tempBucketFactory); try { ObjectOutputStream innerOOS = new ObjectOutputStream(oos); innerOOS.writeObject(req); innerOOS.close(); oos = null; } catch (Throwable e) { Logger.error(this, "Unable to write recovery data for "+name+" : "+e, e); oos.abort(); } finally { if(oos != null) oos.close(); } } private Object readChecksummedObject(ObjectInputStream is, long totalLength) throws IOException, ChecksumFailedException, ClassNotFoundException { InputStream ois = checker.checksumReaderWithLength(is, this.tempBucketFactory, totalLength); try { ObjectInputStream oo = new ObjectInputStream(ois); Object ret = oo.readObject(); oo.close(); oo = null; ois = null; return ret; } catch (Throwable t) { Logger.error(this, "Serialization failed: "+t, t); return null; } finally { if(ois != null) ois.close(); } } private void skipChecksummedObject(ObjectInputStream is, long totalLength) throws IOException { long length = is.readLong(); if(length > totalLength) throw new IOException("Too long: "+length+" > "+totalLength); FileUtil.skipFully(is, length + checker.checksumLength()); } private ClientRequest[] getRequests() { return clientCore.getPersistentRequests(); } public boolean newSalt() { return newSalt; } private RequestIdentifier readRequestIdentifier(DataInput is) throws IOException { short length = is.readShort(); if(length <= 0) return null; byte[] buf = new byte[length]; try { checker.readAndChecksum(is, buf, 0, length); } catch (ChecksumFailedException e) { Logger.error(this, "Checksum failed reading RequestIdentifier. This is not serious but means we will have to read the next request even if we don't need it."); return null; } DataInputStream dis = new DataInputStream(new ByteArrayInputStream(buf)); try { return new RequestIdentifier(dis); } catch (IOException e) { Logger.error(this, "Failed to parse RequestIdentifier in spite of valid checksum (probably a bug): "+e, e); return null; } } private void writeRequestIdentifier(DataOutput os, RequestIdentifier req) throws IOException { ByteArrayOutputStream baos = new ByteArrayOutputStream(); OutputStream oos = checker.checksumWriter(baos); DataOutputStream dos = new DataOutputStream(oos); req.writeTo(dos); dos.close(); byte[] buf = baos.toByteArray(); os.writeShort(buf.length - checker.checksumLength()); os.write(buf); } public synchronized File getWriteFilename() { return writeToFilename; } public void panic() { killAndWaitForNotWriting(); deleteAllFiles(); } public void deleteAllFiles() { synchronized(serializeCheckpoints) { deleteFile(dir, baseName, false, false); deleteFile(dir, baseName, false, true); deleteFile(dir, baseName, true, false); deleteFile(dir, baseName, true, true); } } public void disableWrite() { synchronized(serializeCheckpoints) { writeToFilename = null; writeToBackupFilename = null; writeToBucket = null; } super.disableWrite(); } }