okhttp3.internal.cache2.Relay Maven / Gradle / Ivy
/*
* Copyright (C) 2016 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 okhttp3.internal.cache2;
import java.io.File;
import java.io.IOException;
import java.io.RandomAccessFile;
import okio.Buffer;
import okio.ByteString;
import okio.Source;
import okio.Timeout;
import static okhttp3.internal.Util.closeQuietly;
/**
* Replicates a single upstream source into multiple downstream sources. Each downstream source
* returns the same bytes as the upstream source. Downstream sources may read data either as it
* is returned by upstream, or after the upstream source has been exhausted.
*
* As bytes are returned from upstream they are written to a local file. Downstream sources read
* from this file as necessary.
*
*
This class also keeps a small buffer of bytes recently read from upstream. This is intended to
* save a small amount of file I/O and data copying.
*/
// TODO(jwilson): what to do about timeouts? They could be different and unfortunately when any
// timeout is hit we like to tear down the whole stream.
final class Relay {
private static final int SOURCE_UPSTREAM = 1;
private static final int SOURCE_FILE = 2;
static final ByteString PREFIX_CLEAN = ByteString.encodeUtf8("OkHttp cache v1\n");
static final ByteString PREFIX_DIRTY = ByteString.encodeUtf8("OkHttp DIRTY :(\n");
private static final long FILE_HEADER_SIZE = 32L;
/**
* Read/write persistence of the upstream source and its metadata. Its layout is as follows:
*
*
* - 16 bytes: either {@code OkHttp cache v1\n} if the persisted file is complete. This is
* another sequence of bytes if the file is incomplete and should not be used.
*
- 8 bytes: n: upstream data size
*
- 8 bytes: m: metadata size
*
- n bytes: upstream data
*
- m bytes: metadata
*
*
* This is closed and assigned to null when the last source is closed and no further sources
* are permitted.
*/
RandomAccessFile file;
/** The thread that currently has access to upstream. Possibly null. Guarded by this. */
Thread upstreamReader;
/**
* Null once the file has a complete copy of the upstream bytes. Only the {@code upstreamReader}
* thread may access this source.
*/
Source upstream;
/**
* A buffer for {@code upstreamReader} to use when pulling bytes from upstream. Only the {@code
* upstreamReader} thread may access this buffer.
*/
final Buffer upstreamBuffer = new Buffer();
/** The number of bytes consumed from {@link #upstream}. Guarded by this. */
long upstreamPos;
/** True if there are no further bytes to read from {@code upstream}. Guarded by this. */
boolean complete;
/** User-supplied additional data persisted with the source data. */
private final ByteString metadata;
/**
* The most recently read bytes from {@link #upstream}. This is a suffix of {@link #file}. Guarded
* by this.
*/
final Buffer buffer = new Buffer();
/** The maximum size of {@code buffer}. */
final long bufferMaxSize;
/**
* Reference count of the number of active sources reading this stream. When decremented to 0
* resources are released and all following calls to {@link #newSource} return null. Guarded by
* this.
*/
int sourceCount;
private Relay(RandomAccessFile file, Source upstream, long upstreamPos, ByteString metadata,
long bufferMaxSize) {
this.file = file;
this.upstream = upstream;
this.complete = upstream == null;
this.upstreamPos = upstreamPos;
this.metadata = metadata;
this.bufferMaxSize = bufferMaxSize;
}
/**
* Creates a new relay that reads a live stream from {@code upstream}, using {@code file} to share
* that data with other sources.
*
*
Warning: callers to this method must immediately call {@link #newSource} to
* create a source and close that when they're done. Otherwise a handle to {@code file} will be
* leaked.
*/
public static Relay edit(
File file, Source upstream, ByteString metadata, long bufferMaxSize) throws IOException {
RandomAccessFile randomAccessFile = new RandomAccessFile(file, "rw");
Relay result = new Relay(randomAccessFile, upstream, 0L, metadata, bufferMaxSize);
// Write a dirty header. That way if we crash we won't attempt to recover this.
randomAccessFile.setLength(0L);
result.writeHeader(PREFIX_DIRTY, -1L, -1L);
return result;
}
/**
* Creates a relay that reads a recorded stream from {@code file}.
*
*
Warning: callers to this method must immediately call {@link #newSource} to
* create a source and close that when they're done. Otherwise a handle to {@code file} will be
* leaked.
*/
public static Relay read(File file) throws IOException {
RandomAccessFile randomAccessFile = new RandomAccessFile(file, "rw");
FileOperator fileOperator = new FileOperator(randomAccessFile.getChannel());
// Read the header.
Buffer header = new Buffer();
fileOperator.read(0, header, FILE_HEADER_SIZE);
ByteString prefix = header.readByteString(PREFIX_CLEAN.size());
if (!prefix.equals(PREFIX_CLEAN)) throw new IOException("unreadable cache file");
long upstreamSize = header.readLong();
long metadataSize = header.readLong();
// Read the metadata.
Buffer metadataBuffer = new Buffer();
fileOperator.read(FILE_HEADER_SIZE + upstreamSize, metadataBuffer, metadataSize);
ByteString metadata = metadataBuffer.readByteString();
// Return the result.
return new Relay(randomAccessFile, null, upstreamSize, metadata, 0L);
}
private void writeHeader(
ByteString prefix, long upstreamSize, long metadataSize) throws IOException {
Buffer header = new Buffer();
header.write(prefix);
header.writeLong(upstreamSize);
header.writeLong(metadataSize);
if (header.size() != FILE_HEADER_SIZE) throw new IllegalArgumentException();
FileOperator fileOperator = new FileOperator(file.getChannel());
fileOperator.write(0, header, FILE_HEADER_SIZE);
}
private void writeMetadata(long upstreamSize) throws IOException {
Buffer metadataBuffer = new Buffer();
metadataBuffer.write(metadata);
FileOperator fileOperator = new FileOperator(file.getChannel());
fileOperator.write(FILE_HEADER_SIZE + upstreamSize, metadataBuffer, metadata.size());
}
void commit(long upstreamSize) throws IOException {
// Write metadata to the end of the file.
writeMetadata(upstreamSize);
file.getChannel().force(false);
// Once everything else is in place we can swap the dirty header for a clean one.
writeHeader(PREFIX_CLEAN, upstreamSize, metadata.size());
file.getChannel().force(false);
// This file is complete.
synchronized (Relay.this) {
complete = true;
}
closeQuietly(upstream);
upstream = null;
}
boolean isClosed() {
return file == null;
}
public ByteString metadata() {
return metadata;
}
/**
* Returns a new source that returns the same bytes as upstream. Returns null if this relay has
* been closed and no further sources are possible. In that case callers should retry after
* building a new relay with {@link #read}.
*/
public Source newSource() {
synchronized (Relay.this) {
if (file == null) return null;
sourceCount++;
}
return new RelaySource();
}
class RelaySource implements Source {
private final Timeout timeout = new Timeout();
/** The operator to read and write the shared file. Null if this source is closed. */
private FileOperator fileOperator = new FileOperator(file.getChannel());
/** The next byte to read. This is always less than or equal to {@code upstreamPos}. */
private long sourcePos;
/**
* Selects where to find the bytes for a read and read them. This is one of three sources.
*
*
Upstream:
* In this case the current thread is assigned as the upstream reader. We read bytes from
* upstream and copy them to both the file and to the buffer. Finally we release the upstream
* reader lock and return the new bytes.
*
* The file
* In this case we copy bytes from the file to the {@code sink}.
*
* The buffer
* In this case the bytes are immediately copied into {@code sink} and the number of bytes
* copied is returned.
*
* If upstream would be selected but another thread is already reading upstream this will
* block until that read completes. It is possible to time out while waiting for that.
*/
@Override public long read(Buffer sink, long byteCount) throws IOException {
if (fileOperator == null) throw new IllegalStateException("closed");
long upstreamPos;
int source;
selectSource:
synchronized (Relay.this) {
// We need new data from upstream.
while (sourcePos == (upstreamPos = Relay.this.upstreamPos)) {
// No more data upstream. We're done.
if (complete) return -1L;
// Another thread is already reading. Wait for that.
if (upstreamReader != null) {
timeout.waitUntilNotified(Relay.this);
continue;
}
// We will do the read.
upstreamReader = Thread.currentThread();
source = SOURCE_UPSTREAM;
break selectSource;
}
long bufferPos = upstreamPos - buffer.size();
// Bytes of the read precede the buffer. Read from the file.
if (sourcePos < bufferPos) {
source = SOURCE_FILE;
break selectSource;
}
// The buffer has the data we need. Read from there and return immediately.
long bytesToRead = Math.min(byteCount, upstreamPos - sourcePos);
buffer.copyTo(sink, sourcePos - bufferPos, bytesToRead);
sourcePos += bytesToRead;
return bytesToRead;
}
// Read from the file.
if (source == SOURCE_FILE) {
long bytesToRead = Math.min(byteCount, upstreamPos - sourcePos);
fileOperator.read(FILE_HEADER_SIZE + sourcePos, sink, bytesToRead);
sourcePos += bytesToRead;
return bytesToRead;
}
// Read from upstream. This always reads a full buffer: that might be more than what the
// current call to Source.read() has requested.
try {
long upstreamBytesRead = upstream.read(upstreamBuffer, bufferMaxSize);
// If we've exhausted upstream, we're done.
if (upstreamBytesRead == -1L) {
commit(upstreamPos);
return -1L;
}
// Update this source and prepare this call's result.
long bytesRead = Math.min(upstreamBytesRead, byteCount);
upstreamBuffer.copyTo(sink, 0, bytesRead);
sourcePos += bytesRead;
// Append the upstream bytes to the file.
fileOperator.write(
FILE_HEADER_SIZE + upstreamPos, upstreamBuffer.clone(), upstreamBytesRead);
synchronized (Relay.this) {
// Append new upstream bytes into the buffer. Trim it to its max size.
buffer.write(upstreamBuffer, upstreamBytesRead);
if (buffer.size() > bufferMaxSize) {
buffer.skip(buffer.size() - bufferMaxSize);
}
// Now that the file and buffer have bytes, adjust upstreamPos.
Relay.this.upstreamPos += upstreamBytesRead;
}
return bytesRead;
} finally {
synchronized (Relay.this) {
upstreamReader = null;
Relay.this.notifyAll();
}
}
}
@Override public Timeout timeout() {
return timeout;
}
@Override public void close() throws IOException {
if (fileOperator == null) return; // Already closed.
fileOperator = null;
RandomAccessFile fileToClose = null;
synchronized (Relay.this) {
sourceCount--;
if (sourceCount == 0) {
fileToClose = file;
file = null;
}
}
if (fileToClose != null) {
closeQuietly(fileToClose);
}
}
}
}