All Downloads are FREE. Search and download functionalities are using the official Maven repository.

com.squareup.tape2.QueueFile Maven / Gradle / Ivy

There is a newer version: 2.0.0-SNAPSHOT
Show newest version
/*
 * Copyright (C) 2010 Square, Inc.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
package com.squareup.tape2;

import java.io.Closeable;
import java.io.File;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.io.RandomAccessFile;
import java.nio.channels.FileChannel;
import java.util.ConcurrentModificationException;
import java.util.Iterator;
import java.util.NoSuchElementException;
import javax.annotation.Nullable;

import static java.lang.Math.min;

/**
 * A reliable, efficient, file-based, FIFO queue. Additions and removals are O(1). All operations
 * are atomic. Writes are synchronous; data will be written to disk before an operation returns.
 * The underlying file is structured to survive process and even system crashes. If an I/O
 * exception is thrown during a mutating change, the change is aborted. It is safe to continue to
 * use a {@code QueueFile} instance after an exception.
 *
 * 

Note that this implementation is not synchronized. * *

In a traditional queue, the remove operation returns an element. In this queue, * {@link #peek} and {@link #remove} are used in conjunction. Use * {@code peek} to retrieve the first element, and then {@code remove} to remove it after * successful processing. If the system crashes after {@code peek} and during processing, the * element will remain in the queue, to be processed when the system restarts. * *

NOTE: The current implementation is built for file systems that support * atomic segment writes (like YAFFS). Most conventional file systems don't support this; if the * power goes out while writing a segment, the segment will contain garbage and the file will be * corrupt. We'll add journaling support so this class can be used with more file systems later. * * Construct instances with {@link Builder}. * * @author Bob Lee ([email protected]) */ public final class QueueFile implements Closeable, Iterable { /** Leading bit set to 1 indicating a versioned header and the version of 1. */ private static final int VERSIONED_HEADER = 0x80000001; /** Initial file size in bytes. */ static final int INITIAL_LENGTH = 4096; // one file system block /** A block of nothing to write over old data. */ private static final byte[] ZEROES = new byte[INITIAL_LENGTH]; /** * The underlying file. Uses a ring buffer to store entries. Designed so that a modification * isn't committed or visible until we write the header. The header is much smaller than a * segment. So long as the underlying file system supports atomic segment writes, changes to the * queue are atomic. Storing the file length ensures we can recover from a failed expansion * (i.e. if setting the file length succeeds but the process dies before the data can be copied). *

* This implementation supports two versions of the on-disk format. *

   * Format:
   *   16-32 bytes      Header
   *   ...              Data
   *
   * Header (32 bytes):
   *   1 bit            Versioned indicator [0 = legacy (see "Legacy Header"), 1 = versioned]
   *   31 bits          Version, always 1
   *   8 bytes          File length
   *   4 bytes          Element count
   *   8 bytes          Head element position
   *   8 bytes          Tail element position
   *
   * Legacy Header (16 bytes):
   *   1 bit            Legacy indicator, always 0 (see "Header")
   *   31 bits          File length
   *   4 bytes          Element count
   *   4 bytes          Head element position
   *   4 bytes          Tail element position
   *
   * Element:
   *   4 bytes          Data length
   *   ...              Data
   * 
*/ final RandomAccessFile raf; /** Keep file around for error reporting. */ final File file; /** True when using the versioned header format. Otherwise use the legacy format. */ final boolean versioned; /** The header length in bytes: 16 or 32. */ final int headerLength; /** Cached file length. Always a power of 2. */ long fileLength; /** Number of elements. */ @Private int elementCount; /** Pointer to first (or eldest) element. */ @Private Element first; /** Pointer to last (or newest) element. */ private Element last; /** In-memory buffer. Big enough to hold the header. */ private final byte[] buffer = new byte[32]; /** * The number of times this file has been structurally modified — it is incremented during * {@link #remove(int)} and {@link #add(byte[], int, int)}. Used by {@link ElementIterator} * to guard against concurrent modification. */ @Private int modCount = 0; /** When true, removing an element will also overwrite data with zero bytes. */ private final boolean zero; @Private boolean closed; @Private static RandomAccessFile initializeFromFile(File file, boolean forceLegacy) throws IOException { if (!file.exists()) { // Use a temp file so we don't leave a partially-initialized file. File tempFile = new File(file.getPath() + ".tmp"); RandomAccessFile raf = open(tempFile); try { raf.setLength(INITIAL_LENGTH); raf.seek(0); if (forceLegacy) { raf.writeInt(INITIAL_LENGTH); } else { raf.writeInt(VERSIONED_HEADER); raf.writeLong(INITIAL_LENGTH); } } finally { raf.close(); } // A rename is atomic. if (!tempFile.renameTo(file)) { throw new IOException("Rename failed!"); } } return open(file); } /** Opens a random access file that writes synchronously. */ private static RandomAccessFile open(File file) throws FileNotFoundException { return new RandomAccessFile(file, "rwd"); } QueueFile(File file, RandomAccessFile raf, boolean zero, boolean forceLegacy) throws IOException { this.file = file; this.raf = raf; this.zero = zero; raf.seek(0); raf.readFully(buffer); versioned = !forceLegacy && (buffer[0] & 0x80) != 0; long firstOffset; long lastOffset; if (versioned) { headerLength = 32; int version = readInt(buffer, 0) & 0x7FFFFFFF; if (version != 1) { throw new IOException( "Unable to read version " + version + " format. Supported versions are 1 and legacy."); } fileLength = readLong(buffer, 4); elementCount = readInt(buffer, 12); firstOffset = readLong(buffer, 16); lastOffset = readLong(buffer, 24); } else { headerLength = 16; fileLength = readInt(buffer, 0); elementCount = readInt(buffer, 4); firstOffset = readInt(buffer, 8); lastOffset = readInt(buffer, 12); } if (fileLength > raf.length()) { throw new IOException( "File is truncated. Expected length: " + fileLength + ", Actual length: " + raf.length()); } else if (fileLength <= headerLength) { throw new IOException( "File is corrupt; length stored in header (" + fileLength + ") is invalid."); } first = readElement(firstOffset); last = readElement(lastOffset); } /** * Stores an {@code int} in the {@code byte[]}. The behavior is equivalent to calling * {@link RandomAccessFile#writeInt}. */ private static void writeInt(byte[] buffer, int offset, int value) { buffer[offset ] = (byte) (value >> 24); buffer[offset + 1] = (byte) (value >> 16); buffer[offset + 2] = (byte) (value >> 8); buffer[offset + 3] = (byte) value; } /** Reads an {@code int} from the {@code byte[]}. */ private static int readInt(byte[] buffer, int offset) { return ((buffer[offset ] & 0xff) << 24) + ((buffer[offset + 1] & 0xff) << 16) + ((buffer[offset + 2] & 0xff) << 8) + (buffer[offset + 3] & 0xff); } /** * Stores an {@code long} in the {@code byte[]}. The behavior is equivalent to calling * {@link RandomAccessFile#writeLong}. */ private static void writeLong(byte[] buffer, int offset, long value) { buffer[offset ] = (byte) (value >> 56); buffer[offset + 1] = (byte) (value >> 48); buffer[offset + 2] = (byte) (value >> 40); buffer[offset + 3] = (byte) (value >> 32); buffer[offset + 4] = (byte) (value >> 24); buffer[offset + 5] = (byte) (value >> 16); buffer[offset + 6] = (byte) (value >> 8); buffer[offset + 7] = (byte) value; } /** Reads an {@code long} from the {@code byte[]}. */ private static long readLong(byte[] buffer, int offset) { return ((buffer[offset ] & 0xffL) << 56) + ((buffer[offset + 1] & 0xffL) << 48) + ((buffer[offset + 2] & 0xffL) << 40) + ((buffer[offset + 3] & 0xffL) << 32) + ((buffer[offset + 4] & 0xffL) << 24) + ((buffer[offset + 5] & 0xffL) << 16) + ((buffer[offset + 6] & 0xffL) << 8) + (buffer[offset + 7] & 0xffL); } /** * Writes header atomically. The arguments contain the updated values. The class member fields * should not have changed yet. This only updates the state in the file. It's up to the caller to * update the class member variables *after* this call succeeds. Assumes segment writes are * atomic in the underlying file system. */ private void writeHeader(long fileLength, int elementCount, long firstPosition, long lastPosition) throws IOException { raf.seek(0); if (versioned) { writeInt(buffer, 0, VERSIONED_HEADER); writeLong(buffer, 4, fileLength); writeInt(buffer, 12, elementCount); writeLong(buffer, 16, firstPosition); writeLong(buffer, 24, lastPosition); raf.write(buffer, 0, 32); return; } // Legacy queue header. writeInt(buffer, 0, (int) fileLength); // Signed, so leading bit is always 0 aka legacy. writeInt(buffer, 4, elementCount); writeInt(buffer, 8, (int) firstPosition); writeInt(buffer, 12, (int) lastPosition); raf.write(buffer, 0, 16); } @Private Element readElement(long position) throws IOException { if (position == 0) return Element.NULL; ringRead(position, buffer, 0, Element.HEADER_LENGTH); int length = readInt(buffer, 0); return new Element(position, length); } /** Wraps the position if it exceeds the end of the file. */ @Private long wrapPosition(long position) { return position < fileLength ? position : headerLength + position - fileLength; } /** * Writes count bytes from buffer to position in file. Automatically wraps write if position is * past the end of the file or if buffer overlaps it. * * @param position in file to write to * @param buffer to write from * @param count # of bytes to write */ private void ringWrite(long position, byte[] buffer, int offset, int count) throws IOException { position = wrapPosition(position); if (position + count <= fileLength) { raf.seek(position); raf.write(buffer, offset, count); } else { // The write overlaps the EOF. // # of bytes to write before the EOF. Guaranteed to be less than Integer.MAX_VALUE. int beforeEof = (int) (fileLength - position); raf.seek(position); raf.write(buffer, offset, beforeEof); raf.seek(headerLength); raf.write(buffer, offset + beforeEof, count - beforeEof); } } private void ringErase(long position, long length) throws IOException { while (length > 0) { int chunk = (int) min(length, ZEROES.length); ringWrite(position, ZEROES, 0, chunk); length -= chunk; position += chunk; } } /** * Reads count bytes into buffer from file. Wraps if necessary. * * @param position in file to read from * @param buffer to read into * @param count # of bytes to read */ @Private void ringRead(long position, byte[] buffer, int offset, int count) throws IOException { position = wrapPosition(position); if (position + count <= fileLength) { raf.seek(position); raf.readFully(buffer, offset, count); } else { // The read overlaps the EOF. // # of bytes to read before the EOF. Guaranteed to be less than Integer.MAX_VALUE. int beforeEof = (int) (fileLength - position); raf.seek(position); raf.readFully(buffer, offset, beforeEof); raf.seek(headerLength); raf.readFully(buffer, offset + beforeEof, count - beforeEof); } } /** * Adds an element to the end of the queue. * * @param data to copy bytes from */ public void add(byte[] data) throws IOException { add(data, 0, data.length); } /** * Adds an element to the end of the queue. * * @param data to copy bytes from * @param offset to start from in buffer * @param count number of bytes to copy * @throws IndexOutOfBoundsException if {@code offset < 0} or {@code count < 0}, or if {@code * offset + count} is bigger than the length of {@code buffer}. */ public void add(byte[] data, int offset, int count) throws IOException { if (data == null) { throw new NullPointerException("data == null"); } if ((offset | count) < 0 || count > data.length - offset) { throw new IndexOutOfBoundsException(); } if (closed) throw new IllegalStateException("closed"); expandIfNecessary(count); // Insert a new element after the current last element. boolean wasEmpty = isEmpty(); long position = wasEmpty ? headerLength : wrapPosition(last.position + Element.HEADER_LENGTH + last.length); Element newLast = new Element(position, count); // Write length. writeInt(buffer, 0, count); ringWrite(newLast.position, buffer, 0, Element.HEADER_LENGTH); // Write data. ringWrite(newLast.position + Element.HEADER_LENGTH, data, offset, count); // Commit the addition. If wasEmpty, first == last. long firstPosition = wasEmpty ? newLast.position : first.position; writeHeader(fileLength, elementCount + 1, firstPosition, newLast.position); last = newLast; elementCount++; modCount++; if (wasEmpty) first = last; // first element } private long usedBytes() { if (elementCount == 0) return headerLength; if (last.position >= first.position) { // Contiguous queue. return (last.position - first.position) // all but last entry + Element.HEADER_LENGTH + last.length // last entry + headerLength; } else { // tail < head. The queue wraps. return last.position // buffer front + header + Element.HEADER_LENGTH + last.length // last entry + fileLength - first.position; // buffer end } } private long remainingBytes() { return fileLength - usedBytes(); } /** Returns true if this queue contains no entries. */ public boolean isEmpty() { return elementCount == 0; } /** * If necessary, expands the file to accommodate an additional element of the given length. * * @param dataLength length of data being added */ private void expandIfNecessary(long dataLength) throws IOException { long elementLength = Element.HEADER_LENGTH + dataLength; long remainingBytes = remainingBytes(); if (remainingBytes >= elementLength) return; // Expand. long previousLength = fileLength; long newLength; // Double the length until we can fit the new data. do { remainingBytes += previousLength; newLength = previousLength << 1; previousLength = newLength; } while (remainingBytes < elementLength); setLength(newLength); // Calculate the position of the tail end of the data in the ring buffer long endOfLastElement = wrapPosition(last.position + Element.HEADER_LENGTH + last.length); long count = 0; // If the buffer is split, we need to make it contiguous if (endOfLastElement <= first.position) { FileChannel channel = raf.getChannel(); channel.position(fileLength); // destination position count = endOfLastElement - headerLength; if (channel.transferTo(headerLength, count, channel) != count) { throw new AssertionError("Copied insufficient number of bytes!"); } } // Commit the expansion. if (last.position < first.position) { long newLastPosition = fileLength + last.position - headerLength; writeHeader(newLength, elementCount, first.position, newLastPosition); last = new Element(newLastPosition, last.length); } else { writeHeader(newLength, elementCount, first.position, last.position); } fileLength = newLength; if (zero) { ringErase(headerLength, count); } } /** Sets the length of the file. */ private void setLength(long newLength) throws IOException { // Set new file length (considered metadata) and sync it to storage. raf.setLength(newLength); raf.getChannel().force(true); } /** Reads the eldest element. Returns null if the queue is empty. */ public @Nullable byte[] peek() throws IOException { if (closed) throw new IllegalStateException("closed"); if (isEmpty()) return null; int length = first.length; byte[] data = new byte[length]; ringRead(first.position + Element.HEADER_LENGTH, data, 0, length); return data; } /** * Returns an iterator over elements in this QueueFile. * *

The iterator disallows modifications to be made to the QueueFile during iteration. Removing * elements from the head of the QueueFile is permitted during iteration using * {@link Iterator#remove()}. * *

The iterator may throw an unchecked {@link IOException} during {@link Iterator#next()} * or {@link Iterator#remove()}. */ @Override public Iterator iterator() { return new ElementIterator(); } private final class ElementIterator implements Iterator { /** Index of element to be returned by subsequent call to next. */ int nextElementIndex = 0; /** Position of element to be returned by subsequent call to next. */ private long nextElementPosition = first.position; /** * The {@link #modCount} value that the iterator believes that the backing QueueFile should * have. If this expectation is violated, the iterator has detected concurrent modification. */ int expectedModCount = modCount; @Private ElementIterator() { } private void checkForComodification() { if (modCount != expectedModCount) throw new ConcurrentModificationException(); } @Override public boolean hasNext() { if (closed) throw new IllegalStateException("closed"); checkForComodification(); return nextElementIndex != elementCount; } @Override public byte[] next() { if (closed) throw new IllegalStateException("closed"); checkForComodification(); if (isEmpty()) throw new NoSuchElementException(); if (nextElementIndex >= elementCount) throw new NoSuchElementException(); try { // Read the current element. Element current = readElement(nextElementPosition); byte[] buffer = new byte[current.length]; nextElementPosition = wrapPosition(current.position + Element.HEADER_LENGTH); ringRead(nextElementPosition, buffer, 0, current.length); // Update the pointer to the next element. nextElementPosition = wrapPosition(current.position + Element.HEADER_LENGTH + current.length); nextElementIndex++; // Return the read element. return buffer; } catch (IOException e) { throw QueueFile.getSneakyThrowable(e); } } @Override public void remove() { checkForComodification(); if (isEmpty()) throw new NoSuchElementException(); if (nextElementIndex != 1) { throw new UnsupportedOperationException("Removal is only permitted from the head."); } try { QueueFile.this.remove(); } catch (IOException e) { throw QueueFile.getSneakyThrowable(e); } expectedModCount = modCount; nextElementIndex--; } } /** Returns the number of elements in this queue. */ public int size() { return elementCount; } /** * Removes the eldest element. * * @throws NoSuchElementException if the queue is empty */ public void remove() throws IOException { remove(1); } /** * Removes the eldest {@code n} elements. * * @throws NoSuchElementException if the queue is empty */ public void remove(int n) throws IOException { if (n < 0) { throw new IllegalArgumentException("Cannot remove negative (" + n + ") number of elements."); } if (n == 0) { return; } if (n == elementCount) { clear(); return; } if (isEmpty()) { throw new NoSuchElementException(); } if (n > elementCount) { throw new IllegalArgumentException( "Cannot remove more elements (" + n + ") than present in queue (" + elementCount + ")."); } long eraseStartPosition = first.position; long eraseTotalLength = 0; // Read the position and length of the new first element. long newFirstPosition = first.position; int newFirstLength = first.length; for (int i = 0; i < n; i++) { eraseTotalLength += Element.HEADER_LENGTH + newFirstLength; newFirstPosition = wrapPosition(newFirstPosition + Element.HEADER_LENGTH + newFirstLength); ringRead(newFirstPosition, buffer, 0, Element.HEADER_LENGTH); newFirstLength = readInt(buffer, 0); } // Commit the header. writeHeader(fileLength, elementCount - n, newFirstPosition, last.position); elementCount -= n; modCount++; first = new Element(newFirstPosition, newFirstLength); if (zero) { ringErase(eraseStartPosition, eraseTotalLength); } } /** Clears this queue. Truncates the file to the initial size. */ public void clear() throws IOException { if (closed) throw new IllegalStateException("closed"); // Commit the header. writeHeader(INITIAL_LENGTH, 0, 0, 0); if (zero) { // Zero out data. raf.seek(headerLength); raf.write(ZEROES, 0, INITIAL_LENGTH - headerLength); } elementCount = 0; first = Element.NULL; last = Element.NULL; if (fileLength > INITIAL_LENGTH) setLength(INITIAL_LENGTH); fileLength = INITIAL_LENGTH; modCount++; } /** The underlying {@link File} backing this queue. */ public File file() { return file; } @Override public void close() throws IOException { closed = true; raf.close(); } @Override public String toString() { return "QueueFile{" + "file=" + file + ", zero=" + zero + ", versioned=" + versioned + ", length=" + fileLength + ", size=" + elementCount + ", first=" + first + ", last=" + last + '}'; } /** A pointer to an element. */ static class Element { static final Element NULL = new Element(0, 0); /** Length of element header in bytes. */ static final int HEADER_LENGTH = 4; /** Position in file. */ final long position; /** The length of the data. */ final int length; /** * Constructs a new element. * * @param position within file * @param length of data */ Element(long position, int length) { this.position = position; this.length = length; } @Override public String toString() { return getClass().getSimpleName() + "[position=" + position + ", length=" + length + "]"; } } /** Fluent API for creating {@link QueueFile} instances. */ public static final class Builder { final File file; boolean zero = true; boolean forceLegacy = false; /** Start constructing a new queue backed by the given file. */ public Builder(File file) { if (file == null) { throw new NullPointerException("file == null"); } this.file = file; } /** When true, removing an element will also overwrite data with zero bytes. */ public Builder zero(boolean zero) { this.zero = zero; return this; } /** When true, only the legacy (Tape 1.x) format will be used. */ public Builder forceLegacy(boolean forceLegacy) { this.forceLegacy = forceLegacy; return this; } /** * Constructs a new queue backed by the given builder. Only one instance should access a given * file at a time. */ public QueueFile build() throws IOException { RandomAccessFile raf = initializeFromFile(file, forceLegacy); QueueFile qf = null; try { qf = new QueueFile(file, raf, zero, forceLegacy); return qf; } finally { if (qf == null) { raf.close(); } } } } /** * Use this to throw checked exceptions from iterator methods that do not declare that they throw * checked exceptions. */ @SuppressWarnings({"unchecked", "TypeParameterUnusedInFormals"}) static T getSneakyThrowable(Throwable t) throws T { throw (T) t; } }





© 2015 - 2025 Weber Informatics LLC | Privacy Policy