//Prevayler(TM) - The Free-Software Prevalence Layer. //Copyright (C) 2001-2005 Klaus Wuestefeld //This library is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. //Contributions: Justin Sampson, Tobias Hill. package org.prevayler.foundation; import java.io.ByteArrayOutputStream; import java.io.File; import java.io.FileDescriptor; import java.io.FileOutputStream; import java.io.IOException; public class DurableOutputStream { /** * These two locks allow the two main activities of this class, * serializing transactions to a buffer on the one hand and flushing * the buffer and syncing to disk on the other hand, to proceed * concurrently. Note that where both locks are required, we always * acquire the _syncLock before acquiring the _writeLock to avoid * deadlock. */ private final Object _writeLock = new Object(); private final Object _syncLock = new Object(); /** The File object is only stashed for the sake of the file() getter. */ private final File _file; /** All access guarded by _syncLock. */ private final FileOutputStream _fileOutputStream; /** All access guarded by _syncLock. */ private final FileDescriptor _fileDescriptor; /** All access guarded by _writeLock. */ private ByteArrayOutputStream _active = new ByteArrayOutputStream(); /** All access guarded by _syncLock. */ private ByteArrayOutputStream _inactive = new ByteArrayOutputStream(); /** All access guarded by _writeLock. */ private boolean _closed = false; /** All access guarded by _writeLock. */ private int _objectsWritten = 0; /** All access guarded by _syncLock. */ private int _objectsSynced = 0; /** All access guarded by _syncLock. */ private int _fileSyncCount = 0; public DurableOutputStream(File file) throws IOException { _file = file; _fileOutputStream = new FileOutputStream(file); _fileDescriptor = _fileOutputStream.getFD(); } public void sync(Guided guide) throws IOException { int thisWrite; // When a thread arrives here, all we care about at first is that it // gets properly sequenced according to its turn. guide.startTurn(); try { thisWrite = writeObject(guide); } finally { guide.endTurn(); } // Now, having ended the turn, the next thread is allowed to come in // and try to write its object before we get to the sync. waitUntilSynced(thisWrite); } private int writeObject(Guided guide) throws IOException { synchronized (_writeLock) { if (_closed) { throw new IOException("already closed"); } try { guide.writeTo(_active); } catch (IOException exception) { internalClose(); throw exception; } _objectsWritten++; return _objectsWritten; } } private void waitUntilSynced(int thisWrite) throws IOException { // Here's the real magic. If this thread is the first to have written // an object after a period of inactivity, and there are no other // threads coming in, then thisWrite is trivially one greater than // _objectsSynced, so this thread goes right ahead to sync its own // object alone. But then, if another thread comes along and writes // another object while this thread is syncing, it will write to the // _active buffer and then promply block on the _syncLock until this // thread finishes the sync. If threads continue to come in at just // about the rate that syncs can happen, each thread will wait for the // previous sync to complete and then initiate its own sync. The // latency for the first thread is exactly the time for one sync, which // is the minimum possible latency; the latency for any later thread is // somewhere between that minimum and a maximum of two syncs, with the // average being closer to the minimum end. // // Now, consider the steady state under heavy load. Some thread will // always be syncing the _inactive buffer to disk, so every thread that // arrives will write its object to the _active buffer and then wait // here on the _syncLock. If 10 threads arrive during a given sync // operation, then _active will hold 10 objects when that sync // completes. As soon as that earlier thread releases _syncLock, one of // those 10 new threads will acquire the lock and notice that its // object has not yet been synced; it will then swap the buffers and // flush and sync all 10 objects at once. Each of the 10 threads will // acquire _syncLock in turn and now see that their object has already // been synced and do nothing. synchronized (_syncLock) { if (_objectsSynced < thisWrite) { int objectsWritten; synchronized (_writeLock) { if (_closed) { throw new IOException("already closed"); } ByteArrayOutputStream swap = _active; _active = _inactive; _inactive = swap; objectsWritten = _objectsWritten; } try { // Resetting the buffer clears its contents but keeps the // allocated space. Therefore the buffers should quickly // reach a steady state of an appropriate size and then not // need to grow any more. _inactive.writeTo(_fileOutputStream); _inactive.reset(); _fileOutputStream.flush(); _fileDescriptor.sync(); } catch (IOException exception) { internalClose(); throw exception; } _objectsSynced = objectsWritten; _fileSyncCount++; } } } public void close() throws IOException { synchronized (_syncLock) { synchronized (_writeLock) { if (_closed) { return; } internalClose(); _fileOutputStream.close(); } } } private void internalClose() { synchronized (_writeLock) { _closed = true; _active = null; _inactive = null; } } public File file() { return _file; } public synchronized int fileSyncCount() { synchronized (_syncLock) { return _fileSyncCount; } } public boolean reallyClosed() { synchronized (_writeLock) { return _closed; } } }