Commit 235df090 by olly Committed by Oliver Woodman

Support multiple non-overlapping write locks in SimpleCache

Issue: #5978
PiperOrigin-RevId: 313802629
parent 52e39cd7
......@@ -134,6 +134,8 @@
* Downloads and caching:
* Merge downloads in `SegmentDownloader` to improve overall download speed
([#5978](https://github.com/google/ExoPlayer/issues/5978)).
* Support multiple non-overlapping write locks for the same key in
`SimpleCache`.
* Replace `CacheDataSinkFactory` and `CacheDataSourceFactory` with
`CacheDataSink.Factory` and `CacheDataSource.Factory` respectively.
* Remove `DownloadConstructorHelper` and use `CacheDataSource.Factory`
......
......@@ -31,8 +31,10 @@ import com.google.android.exoplayer2.upstream.cache.CacheKeyFactory;
import com.google.android.exoplayer2.upstream.cache.CacheWriter;
import com.google.android.exoplayer2.upstream.cache.ContentMetadata;
import com.google.android.exoplayer2.util.Assertions;
import com.google.android.exoplayer2.util.Log;
import com.google.android.exoplayer2.util.PriorityTaskManager;
import com.google.android.exoplayer2.util.PriorityTaskManager.PriorityTooLowException;
import com.google.android.exoplayer2.util.SystemClock;
import com.google.android.exoplayer2.util.Util;
import java.io.IOException;
import java.util.ArrayList;
......@@ -218,17 +220,23 @@ public abstract class SegmentDownloader<M extends FilterableManifest<M>> impleme
}
}
long timer = 0;
@Override
public final void remove() {
Cache cache = Assertions.checkNotNull(cacheDataSourceFactory.getCache());
CacheKeyFactory cacheKeyFactory = cacheDataSourceFactory.getCacheKeyFactory();
CacheDataSource dataSource = cacheDataSourceFactory.createDataSourceForRemovingDownload();
try {
timer = SystemClock.DEFAULT.elapsedRealtime();
M manifest = getManifest(dataSource, manifestDataSpec);
Log.e("XXX", "E1\t" + (SystemClock.DEFAULT.elapsedRealtime() - timer));
timer = SystemClock.DEFAULT.elapsedRealtime();
List<Segment> segments = getSegments(dataSource, manifest, true);
for (int i = 0; i < segments.size(); i++) {
cache.removeResource(cacheKeyFactory.buildCacheKey(segments.get(i).dataSpec));
}
Log.e("XXX", "E2\t" + (SystemClock.DEFAULT.elapsedRealtime() - timer));
} catch (IOException e) {
// Ignore exceptions when removing.
} finally {
......
......@@ -165,7 +165,7 @@ public interface Cache {
* defines the file in which the data is stored. {@link CacheSpan#isCached} is true. The caller
* may read from the cache file, but does not acquire any locks.
*
* <p>If there is no cache entry overlapping {@code offset}, then the returned {@link CacheSpan}
* <p>If there is no cache entry overlapping {@code position}, then the returned {@link CacheSpan}
* defines a hole in the cache starting at {@code position} into which the caller may write as it
* obtains the data from some other source. The returned {@link CacheSpan} serves as a lock.
* Whilst the caller holds the lock it may write data into the hole. It may split data into
......@@ -177,31 +177,40 @@ public interface Cache {
*
* @param key The cache key of the resource.
* @param position The starting position in the resource from which data is required.
* @param length The length of the data being requested, or {@link C#LENGTH_UNSET} if unbounded.
* The length is ignored in the case of a cache hit. In the case of a cache miss, it defines
* the maximum length of the hole {@link CacheSpan} that's returned. Cache implementations may
* support parallel writes into non-overlapping holes, and so passing the actual required
* length should be preferred to passing {@link C#LENGTH_UNSET} when possible.
* @return The {@link CacheSpan}.
* @throws InterruptedException If the thread was interrupted.
* @throws CacheException If an error is encountered.
*/
@WorkerThread
CacheSpan startReadWrite(String key, long position) throws InterruptedException, CacheException;
CacheSpan startReadWrite(String key, long position, long length)
throws InterruptedException, CacheException;
/**
* Same as {@link #startReadWrite(String, long)}. However, if the cache entry is locked, then
* instead of blocking, this method will return null as the {@link CacheSpan}.
* Same as {@link #startReadWrite(String, long, long)}. However, if the cache entry is locked,
* then instead of blocking, this method will return null as the {@link CacheSpan}.
*
* <p>This method may be slow and shouldn't normally be called on the main thread.
*
* @param key The cache key of the resource.
* @param position The starting position in the resource from which data is required.
* @param length The length of the data being requested, or {@link C#LENGTH_UNSET} if unbounded.
* The length is ignored in the case of a cache hit. In the case of a cache miss, it defines
* the range of data locked by the returned {@link CacheSpan}.
* @return The {@link CacheSpan}. Or null if the cache entry is locked.
* @throws CacheException If an error is encountered.
*/
@WorkerThread
@Nullable
CacheSpan startReadWriteNonBlocking(String key, long position) throws CacheException;
CacheSpan startReadWriteNonBlocking(String key, long position, long length) throws CacheException;
/**
* Obtains a cache file into which data can be written. Must only be called when holding a
* corresponding hole {@link CacheSpan} obtained from {@link #startReadWrite(String, long)}.
* corresponding hole {@link CacheSpan} obtained from {@link #startReadWrite(String, long, long)}.
*
* <p>This method may be slow and shouldn't normally be called on the main thread.
*
......@@ -217,7 +226,7 @@ public interface Cache {
/**
* Commits a file into the cache. Must only be called when holding a corresponding hole {@link
* CacheSpan} obtained from {@link #startReadWrite(String, long)}.
* CacheSpan} obtained from {@link #startReadWrite(String, long, long)}.
*
* <p>This method may be slow and shouldn't normally be called on the main thread.
*
......@@ -229,7 +238,7 @@ public interface Cache {
void commitFile(File file, long length) throws CacheException;
/**
* Releases a {@link CacheSpan} obtained from {@link #startReadWrite(String, long)} which
* Releases a {@link CacheSpan} obtained from {@link #startReadWrite(String, long, long)} which
* corresponded to a hole in the cache.
*
* @param holeSpan The {@link CacheSpan} being released.
......
......@@ -691,13 +691,13 @@ public final class CacheDataSource implements DataSource {
nextSpan = null;
} else if (blockOnCache) {
try {
nextSpan = cache.startReadWrite(key, readPosition);
nextSpan = cache.startReadWrite(key, readPosition, bytesRemaining);
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
throw new InterruptedIOException();
}
} else {
nextSpan = cache.startReadWriteNonBlocking(key, readPosition);
nextSpan = cache.startReadWriteNonBlocking(key, readPosition, bytesRemaining);
}
DataSpec nextDataSpec;
......
......@@ -98,4 +98,8 @@ public class CacheSpan implements Comparable<CacheSpan> {
return startOffsetDiff == 0 ? 0 : ((startOffsetDiff < 0) ? -1 : 1);
}
@Override
public String toString() {
return "[" + position + ", " + length + "]";
}
}
......@@ -19,8 +19,10 @@ import static com.google.android.exoplayer2.util.Assertions.checkArgument;
import static com.google.android.exoplayer2.util.Assertions.checkState;
import androidx.annotation.Nullable;
import com.google.android.exoplayer2.C;
import com.google.android.exoplayer2.util.Log;
import java.io.File;
import java.util.ArrayList;
import java.util.TreeSet;
/** Defines the cached content for a single resource. */
......@@ -34,10 +36,11 @@ import java.util.TreeSet;
public final String key;
/** The cached spans of this content. */
private final TreeSet<SimpleCacheSpan> cachedSpans;
/** Currently locked ranges. */
private final ArrayList<Range> lockedRanges;
/** Metadata values. */
private DefaultContentMetadata metadata;
/** Whether the content is locked. */
private boolean locked;
/**
* Creates a CachedContent.
......@@ -53,7 +56,8 @@ import java.util.TreeSet;
this.id = id;
this.key = key;
this.metadata = metadata;
this.cachedSpans = new TreeSet<>();
cachedSpans = new TreeSet<>();
lockedRanges = new ArrayList<>();
}
/** Returns the metadata. */
......@@ -72,14 +76,58 @@ import java.util.TreeSet;
return !metadata.equals(oldMetadata);
}
/** Returns whether the content is locked. */
public boolean isLocked() {
return locked;
/** Returns whether the entire resource is fully unlocked. */
public boolean isFullyUnlocked() {
return lockedRanges.isEmpty();
}
/**
* Returns whether the specified range of the resource is fully locked by a single lock.
*
* @param position The position of the range.
* @param length The length of the range, or {@link C#LENGTH_UNSET} if unbounded.
* @return Whether the range is fully locked by a single lock.
*/
public boolean isFullyLocked(long position, long length) {
for (int i = 0; i < lockedRanges.size(); i++) {
if (lockedRanges.get(i).contains(position, length)) {
return true;
}
}
return false;
}
/**
* Attempts to lock the specified range of the resource.
*
* @param position The position of the range.
* @param length The length of the range, or {@link C#LENGTH_UNSET} if unbounded.
* @return Whether the range was successfully locked.
*/
public boolean lockRange(long position, long length) {
for (int i = 0; i < lockedRanges.size(); i++) {
if (lockedRanges.get(i).intersects(position, length)) {
return false;
}
}
lockedRanges.add(new Range(position, length));
return true;
}
/** Sets the locked state of the content. */
public void setLocked(boolean locked) {
this.locked = locked;
/**
* Unlocks the currently locked range starting at the specified position.
*
* @param position The starting position of the locked range.
* @throws IllegalStateException If there was no locked range starting at the specified position.
*/
public void unlockRange(long position) {
for (int i = 0; i < lockedRanges.size(); i++) {
if (lockedRanges.get(i).position == position) {
lockedRanges.remove(i);
return;
}
}
throw new IllegalStateException();
}
/** Adds the given {@link SimpleCacheSpan} which contains a part of the content. */
......@@ -93,18 +141,25 @@ import java.util.TreeSet;
}
/**
* Returns the span containing the position. If there isn't one, it returns a hole span
* which defines the maximum extents of the hole in the cache.
* Returns the cache span corresponding to the provided range. See {@link
* Cache#startReadWrite(String, long, long)} for detailed descriptions of the returned spans.
*
* @param position The position of the span being requested.
* @param length The length of the span, or {@link C#LENGTH_UNSET} if unbounded.
* @return The corresponding cache {@link SimpleCacheSpan}.
*/
public SimpleCacheSpan getSpan(long position) {
public SimpleCacheSpan getSpan(long position, long length) {
SimpleCacheSpan lookupSpan = SimpleCacheSpan.createLookup(key, position);
SimpleCacheSpan floorSpan = cachedSpans.floor(lookupSpan);
if (floorSpan != null && floorSpan.position + floorSpan.length > position) {
return floorSpan;
}
SimpleCacheSpan ceilSpan = cachedSpans.ceiling(lookupSpan);
return ceilSpan == null ? SimpleCacheSpan.createOpenHole(key, position)
: SimpleCacheSpan.createClosedHole(key, position, ceilSpan.position - position);
if (ceilSpan != null) {
long holeLength = ceilSpan.position - position;
length = length == C.LENGTH_UNSET ? holeLength : Math.min(holeLength, length);
}
return SimpleCacheSpan.createHole(key, position, length);
}
/**
......@@ -121,7 +176,7 @@ import java.util.TreeSet;
public long getCachedBytesLength(long position, long length) {
checkArgument(position >= 0);
checkArgument(length >= 0);
SimpleCacheSpan span = getSpan(position);
SimpleCacheSpan span = getSpan(position, length);
if (span.isHoleSpan()) {
// We don't have a span covering the start of the queried region.
return -Math.min(span.isOpenEnded() ? Long.MAX_VALUE : span.length, length);
......@@ -215,4 +270,51 @@ import java.util.TreeSet;
&& cachedSpans.equals(that.cachedSpans)
&& metadata.equals(that.metadata);
}
private static final class Range {
/** The starting position of the range. */
public final long position;
/** The length of the range, or {@link C#LENGTH_UNSET} if unbounded. */
public final long length;
public Range(long position, long length) {
this.position = position;
this.length = length;
}
/**
* Returns whether this range fully contains the range specified by {@code otherPosition} and
* {@code otherLength}.
*
* @param otherPosition The position of the range to check.
* @param otherLength The length of the range to check, or {@link C#LENGTH_UNSET} if unbounded.
* @return Whether this range fully contains the specified range.
*/
public boolean contains(long otherPosition, long otherLength) {
if (length == C.LENGTH_UNSET) {
return otherPosition >= position;
} else if (otherLength == C.LENGTH_UNSET) {
return false;
} else {
return position <= otherPosition && (otherPosition + otherLength) <= (position + length);
}
}
/**
* Returns whether this range intersects with the range specified by {@code otherPosition} and
* {@code otherLength}.
*
* @param otherPosition The position of the range to check.
* @param otherLength The length of the range to check, or {@link C#LENGTH_UNSET} if unbounded.
* @return Whether this range intersects with the specified range.
*/
public boolean intersects(long otherPosition, long otherLength) {
if (position <= otherPosition) {
return length == C.LENGTH_UNSET || position + length > otherPosition;
} else {
return otherLength == C.LENGTH_UNSET || otherPosition + otherLength > position;
}
}
}
}
......@@ -273,7 +273,7 @@ import org.checkerframework.checker.nullness.compatqual.NullableType;
*/
public void maybeRemove(String key) {
@Nullable CachedContent cachedContent = keyToContent.get(key);
if (cachedContent != null && cachedContent.isEmpty() && !cachedContent.isLocked()) {
if (cachedContent != null && cachedContent.isEmpty() && cachedContent.isFullyUnlocked()) {
keyToContent.remove(key);
int id = cachedContent.id;
boolean neverStored = newIds.get(id);
......
......@@ -353,13 +353,13 @@ public final class SimpleCache implements Cache {
}
@Override
public synchronized CacheSpan startReadWrite(String key, long position)
public synchronized CacheSpan startReadWrite(String key, long position, long length)
throws InterruptedException, CacheException {
Assertions.checkState(!released);
checkInitialization();
while (true) {
CacheSpan span = startReadWriteNonBlocking(key, position);
CacheSpan span = startReadWriteNonBlocking(key, position, length);
if (span != null) {
return span;
} else {
......@@ -375,12 +375,12 @@ public final class SimpleCache implements Cache {
@Override
@Nullable
public synchronized CacheSpan startReadWriteNonBlocking(String key, long position)
public synchronized CacheSpan startReadWriteNonBlocking(String key, long position, long length)
throws CacheException {
Assertions.checkState(!released);
checkInitialization();
SimpleCacheSpan span = getSpan(key, position);
SimpleCacheSpan span = getSpan(key, position, length);
if (span.isCached) {
// Read case.
......@@ -388,9 +388,8 @@ public final class SimpleCache implements Cache {
}
CachedContent cachedContent = contentIndex.getOrAdd(key);
if (!cachedContent.isLocked()) {
if (cachedContent.lockRange(position, span.length)) {
// Write case.
cachedContent.setLocked(true);
return span;
}
......@@ -405,7 +404,7 @@ public final class SimpleCache implements Cache {
CachedContent cachedContent = contentIndex.get(key);
Assertions.checkNotNull(cachedContent);
Assertions.checkState(cachedContent.isLocked());
Assertions.checkState(cachedContent.isFullyLocked(position, length));
if (!cacheDir.exists()) {
// For some reason the cache directory doesn't exist. Make a best effort to create it.
cacheDir.mkdirs();
......@@ -435,7 +434,7 @@ public final class SimpleCache implements Cache {
SimpleCacheSpan span =
Assertions.checkNotNull(SimpleCacheSpan.createCacheEntry(file, length, contentIndex));
CachedContent cachedContent = Assertions.checkNotNull(contentIndex.get(span.key));
Assertions.checkState(cachedContent.isLocked());
Assertions.checkState(cachedContent.isFullyLocked(span.position, span.length));
// Check if the span conflicts with the set content length
long contentLength = ContentMetadata.getContentLength(cachedContent.getMetadata());
......@@ -464,8 +463,7 @@ public final class SimpleCache implements Cache {
public synchronized void releaseHoleSpan(CacheSpan holeSpan) {
Assertions.checkState(!released);
CachedContent cachedContent = Assertions.checkNotNull(contentIndex.get(holeSpan.key));
Assertions.checkState(cachedContent.isLocked());
cachedContent.setLocked(false);
cachedContent.unlockRange(holeSpan.position);
contentIndex.maybeRemove(cachedContent.key);
notifyAll();
}
......@@ -688,23 +686,21 @@ public final class SimpleCache implements Cache {
}
/**
* Returns the cache span corresponding to the provided lookup span.
*
* <p>If the lookup position is contained by an existing entry in the cache, then the returned
* span defines the file in which the data is stored. If the lookup position is not contained by
* an existing entry, then the returned span defines the maximum extents of the hole in the cache.
* Returns the cache span corresponding to the provided key and range. See {@link
* Cache#startReadWrite(String, long, long)} for detailed descriptions of the returned spans.
*
* @param key The key of the span being requested.
* @param position The position of the span being requested.
* @param length The length of the span, or {@link C#LENGTH_UNSET} if unbounded.
* @return The corresponding cache {@link SimpleCacheSpan}.
*/
private SimpleCacheSpan getSpan(String key, long position) {
private SimpleCacheSpan getSpan(String key, long position, long length) {
@Nullable CachedContent cachedContent = contentIndex.get(key);
if (cachedContent == null) {
return SimpleCacheSpan.createOpenHole(key, position);
return SimpleCacheSpan.createHole(key, position, length);
}
while (true) {
SimpleCacheSpan span = cachedContent.getSpan(position);
SimpleCacheSpan span = cachedContent.getSpan(position, length);
if (span.isCached && span.file.length() != span.length) {
// The file has been modified or deleted underneath us. It's likely that other files will
// have been modified too, so scan the whole in-memory representation.
......
......@@ -54,7 +54,7 @@ import java.util.regex.Pattern;
* Creates a lookup span.
*
* @param key The cache key of the resource.
* @param position The position of the {@link CacheSpan} in the resource.
* @param position The position of the span in the resource.
* @return The span.
*/
public static SimpleCacheSpan createLookup(String key, long position) {
......@@ -62,25 +62,14 @@ import java.util.regex.Pattern;
}
/**
* Creates an open hole span.
* Creates a hole span.
*
* @param key The cache key of the resource.
* @param position The position of the {@link CacheSpan} in the resource.
* @return The span.
*/
public static SimpleCacheSpan createOpenHole(String key, long position) {
return new SimpleCacheSpan(key, position, C.LENGTH_UNSET, C.TIME_UNSET, null);
}
/**
* Creates a closed hole span.
*
* @param key The cache key of the resource.
* @param position The position of the {@link CacheSpan} in the resource.
* @param length The length of the {@link CacheSpan}.
* @return The span.
* @param position The position of the span in the resource.
* @param length The length of the span, or {@link C#LENGTH_UNSET} if unbounded.
* @return The hole span.
*/
public static SimpleCacheSpan createClosedHole(String key, long position, long length) {
public static SimpleCacheSpan createHole(String key, long position, long length) {
return new SimpleCacheSpan(key, position, length, C.TIME_UNSET, null);
}
......@@ -191,12 +180,11 @@ import java.util.regex.Pattern;
/**
* @param key The cache key of the resource.
* @param position The position of the {@link CacheSpan} in the resource.
* @param length The length of the {@link CacheSpan}, or {@link C#LENGTH_UNSET} if this is an
* open-ended hole.
* @param position The position of the span in the resource.
* @param length The length of the span, or {@link C#LENGTH_UNSET} if this is an open-ended hole.
* @param lastTouchTimestamp The last touch timestamp, or {@link C#TIME_UNSET} if {@link
* #isCached} is false.
* @param file The file corresponding to this {@link CacheSpan}, or null if it's a hole.
* @param file The file corresponding to this span, or null if it's a hole.
*/
private SimpleCacheSpan(
String key, long position, long length, long lastTouchTimestamp, @Nullable File file) {
......
......@@ -384,7 +384,7 @@ public final class CacheDataSourceTest {
.appendReadData(1);
// Lock the content on the cache.
CacheSpan cacheSpan = cache.startReadWriteNonBlocking(defaultCacheKey, 0);
CacheSpan cacheSpan = cache.startReadWriteNonBlocking(defaultCacheKey, 0, C.LENGTH_UNSET);
assertThat(cacheSpan).isNotNull();
assertThat(cacheSpan.isHoleSpan()).isTrue();
......
......@@ -301,7 +301,7 @@ public class CachedContentIndexTest {
public void cantRemoveLockedCachedContent() {
CachedContentIndex index = newInstance();
CachedContent cachedContent = index.getOrAdd("key1");
cachedContent.setLocked(true);
cachedContent.lockRange(0, 1);
index.maybeRemove(cachedContent.key);
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or sign in to comment