diff --git a/RELEASENOTES.md b/RELEASENOTES.md index 98e5847ba5f..c1b45b4b19b 100644 --- a/RELEASENOTES.md +++ b/RELEASENOTES.md @@ -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` diff --git a/library/core/src/main/java/com/google/android/exoplayer2/offline/SegmentDownloader.java b/library/core/src/main/java/com/google/android/exoplayer2/offline/SegmentDownloader.java index 28ed994168d..a67123ca05e 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/offline/SegmentDownloader.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/offline/SegmentDownloader.java @@ -31,8 +31,10 @@ 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 void cancel() { } } + 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 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 { diff --git a/library/core/src/main/java/com/google/android/exoplayer2/upstream/cache/Cache.java b/library/core/src/main/java/com/google/android/exoplayer2/upstream/cache/Cache.java index 33f0dc35f26..c917929111b 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/upstream/cache/Cache.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/upstream/cache/Cache.java @@ -165,7 +165,7 @@ public CacheException(String message, Throwable cause) { * 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. * - *

If there is no cache entry overlapping {@code offset}, then the returned {@link CacheSpan} + *

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 CacheException(String message, Throwable cause) { * * @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}. * *

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)}. * *

This method may be slow and shouldn't normally be called on the main thread. * @@ -217,7 +226,7 @@ public CacheException(String message, Throwable cause) { /** * 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)}. * *

This method may be slow and shouldn't normally be called on the main thread. * @@ -229,7 +238,7 @@ public CacheException(String message, Throwable cause) { 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. diff --git a/library/core/src/main/java/com/google/android/exoplayer2/upstream/cache/CacheDataSource.java b/library/core/src/main/java/com/google/android/exoplayer2/upstream/cache/CacheDataSource.java index d20aaa0b638..e1e2e5194b5 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/upstream/cache/CacheDataSource.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/upstream/cache/CacheDataSource.java @@ -691,13 +691,13 @@ private void openNextSource(boolean checkCache) throws IOException { 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; diff --git a/library/core/src/main/java/com/google/android/exoplayer2/upstream/cache/CacheSpan.java b/library/core/src/main/java/com/google/android/exoplayer2/upstream/cache/CacheSpan.java index a4dacbe95c0..978b5bacfa5 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/upstream/cache/CacheSpan.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/upstream/cache/CacheSpan.java @@ -98,4 +98,8 @@ public int compareTo(CacheSpan another) { return startOffsetDiff == 0 ? 0 : ((startOffsetDiff < 0) ? -1 : 1); } + @Override + public String toString() { + return "[" + position + ", " + length + "]"; + } } diff --git a/library/core/src/main/java/com/google/android/exoplayer2/upstream/cache/CachedContent.java b/library/core/src/main/java/com/google/android/exoplayer2/upstream/cache/CachedContent.java index 82dfd3fa992..352483fe8fd 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/upstream/cache/CachedContent.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/upstream/cache/CachedContent.java @@ -19,8 +19,10 @@ 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 @@ public final String key; /** The cached spans of this content. */ private final TreeSet cachedSpans; + /** Currently locked ranges. */ + private final ArrayList lockedRanges; + /** Metadata values. */ private DefaultContentMetadata metadata; - /** Whether the content is locked. */ - private boolean locked; /** * Creates a CachedContent. @@ -53,7 +56,8 @@ public CachedContent(int id, String key, DefaultContentMetadata metadata) { 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 @@ public boolean applyMetadataMutations(ContentMetadataMutations mutations) { 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 @@ public TreeSet getSpans() { } /** - * 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 @@ public SimpleCacheSpan getSpan(long position) { 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 @@ public boolean equals(@Nullable Object o) { && 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; + } + } + } } diff --git a/library/core/src/main/java/com/google/android/exoplayer2/upstream/cache/CachedContentIndex.java b/library/core/src/main/java/com/google/android/exoplayer2/upstream/cache/CachedContentIndex.java index 62c831ca117..8bf7b9d2d69 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/upstream/cache/CachedContentIndex.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/upstream/cache/CachedContentIndex.java @@ -273,7 +273,7 @@ public String getKeyForId(int id) { */ 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); diff --git a/library/core/src/main/java/com/google/android/exoplayer2/upstream/cache/SimpleCache.java b/library/core/src/main/java/com/google/android/exoplayer2/upstream/cache/SimpleCache.java index 1cb6d13fc08..d7038c3a3d3 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/upstream/cache/SimpleCache.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/upstream/cache/SimpleCache.java @@ -353,13 +353,13 @@ public synchronized long getCacheSpace() { } @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 synchronized CacheSpan startReadWrite(String key, long position) @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 synchronized CacheSpan startReadWriteNonBlocking(String key, long positio } 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 synchronized File startFile(String key, long position, long length) throw 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 synchronized void commitFile(File file, long length) throws CacheExceptio 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 synchronized void commitFile(File file, long length) throws CacheExceptio 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 @@ private SimpleCacheSpan touchSpan(String key, SimpleCacheSpan span) { } /** - * Returns the cache span corresponding to the provided lookup span. - * - *

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. diff --git a/library/core/src/main/java/com/google/android/exoplayer2/upstream/cache/SimpleCacheSpan.java b/library/core/src/main/java/com/google/android/exoplayer2/upstream/cache/SimpleCacheSpan.java index 3a5279c949b..d02f7c0988b 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/upstream/cache/SimpleCacheSpan.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/upstream/cache/SimpleCacheSpan.java @@ -54,7 +54,7 @@ public static File getCacheFile(File cacheDir, int id, long position, long times * 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 @@ public static SimpleCacheSpan createLookup(String key, long position) { } /** - * 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 @@ private static File upgradeFile(File file, CachedContentIndex index) { /** * @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) { diff --git a/library/core/src/test/java/com/google/android/exoplayer2/upstream/cache/CacheDataSourceTest.java b/library/core/src/test/java/com/google/android/exoplayer2/upstream/cache/CacheDataSourceTest.java index d8a7a034066..328d80bf484 100644 --- a/library/core/src/test/java/com/google/android/exoplayer2/upstream/cache/CacheDataSourceTest.java +++ b/library/core/src/test/java/com/google/android/exoplayer2/upstream/cache/CacheDataSourceTest.java @@ -384,7 +384,7 @@ public void switchToCacheSourceWithNonBlockingCacheDataSource() throws Exception .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(); diff --git a/library/core/src/test/java/com/google/android/exoplayer2/upstream/cache/CachedContentIndexTest.java b/library/core/src/test/java/com/google/android/exoplayer2/upstream/cache/CachedContentIndexTest.java index bbb372b5e2e..1237d3a3127 100644 --- a/library/core/src/test/java/com/google/android/exoplayer2/upstream/cache/CachedContentIndexTest.java +++ b/library/core/src/test/java/com/google/android/exoplayer2/upstream/cache/CachedContentIndexTest.java @@ -301,7 +301,7 @@ public void cantRemoveNotEmptyCachedContent() throws Exception { public void cantRemoveLockedCachedContent() { CachedContentIndex index = newInstance(); CachedContent cachedContent = index.getOrAdd("key1"); - cachedContent.setLocked(true); + cachedContent.lockRange(0, 1); index.maybeRemove(cachedContent.key); diff --git a/library/core/src/test/java/com/google/android/exoplayer2/upstream/cache/SimpleCacheTest.java b/library/core/src/test/java/com/google/android/exoplayer2/upstream/cache/SimpleCacheTest.java index 670eceff578..fce14794ebe 100644 --- a/library/core/src/test/java/com/google/android/exoplayer2/upstream/cache/SimpleCacheTest.java +++ b/library/core/src/test/java/com/google/android/exoplayer2/upstream/cache/SimpleCacheTest.java @@ -100,7 +100,7 @@ public void newInstance_withExistingCacheDirectory_loadsCachedData() throws Exce SimpleCache simpleCache = getSimpleCache(); // Write some data and metadata to the cache. - CacheSpan holeSpan = simpleCache.startReadWrite(KEY_1, 0); + CacheSpan holeSpan = simpleCache.startReadWrite(KEY_1, 0, LENGTH_UNSET); addCache(simpleCache, KEY_1, 0, 15); simpleCache.releaseHoleSpan(holeSpan); ContentMetadataMutations mutations = new ContentMetadataMutations(); @@ -112,7 +112,7 @@ public void newInstance_withExistingCacheDirectory_loadsCachedData() throws Exce simpleCache = getSimpleCache(); // Read the cached data and metadata back. - CacheSpan fileSpan = simpleCache.startReadWrite(KEY_1, 0); + CacheSpan fileSpan = simpleCache.startReadWrite(KEY_1, 0, LENGTH_UNSET); assertCachedDataReadCorrect(fileSpan); assertThat(ContentMetadata.getRedirectedUri(simpleCache.getContentMetadata(KEY_1))) .isEqualTo(Uri.parse("https://redirect.google.com")); @@ -130,7 +130,7 @@ public void newInstance_withExistingCacheInstance_fails() { public void newInstance_withExistingCacheDirectory_resolvesInconsistentState() throws Exception { SimpleCache simpleCache = getSimpleCache(); - CacheSpan holeSpan = simpleCache.startReadWrite(KEY_1, 0); + CacheSpan holeSpan = simpleCache.startReadWrite(KEY_1, 0, LENGTH_UNSET); addCache(simpleCache, KEY_1, 0, 15); simpleCache.releaseHoleSpan(holeSpan); simpleCache.removeSpan(simpleCache.getCachedSpans(KEY_1).first()); @@ -151,7 +151,7 @@ public void newInstance_withExistingCacheDirectory_resolvesInconsistentState() t @Test public void newInstance_withEncryptedIndex() throws Exception { SimpleCache simpleCache = getEncryptedSimpleCache(ENCRYPTED_INDEX_KEY); - CacheSpan holeSpan = simpleCache.startReadWrite(KEY_1, 0); + CacheSpan holeSpan = simpleCache.startReadWrite(KEY_1, 0, LENGTH_UNSET); addCache(simpleCache, KEY_1, 0, 15); simpleCache.releaseHoleSpan(holeSpan); simpleCache.release(); @@ -160,7 +160,7 @@ public void newInstance_withEncryptedIndex() throws Exception { simpleCache = getEncryptedSimpleCache(ENCRYPTED_INDEX_KEY); // Read the cached data back. - CacheSpan fileSpan = simpleCache.startReadWrite(KEY_1, 0); + CacheSpan fileSpan = simpleCache.startReadWrite(KEY_1, 0, LENGTH_UNSET); assertCachedDataReadCorrect(fileSpan); } @@ -169,7 +169,7 @@ public void newInstance_withEncryptedIndexAndWrongKey_clearsCache() throws Excep SimpleCache simpleCache = getEncryptedSimpleCache(ENCRYPTED_INDEX_KEY); // Write data. - CacheSpan holeSpan = simpleCache.startReadWrite(KEY_1, 0); + CacheSpan holeSpan = simpleCache.startReadWrite(KEY_1, 0, LENGTH_UNSET); addCache(simpleCache, KEY_1, 0, 15); simpleCache.releaseHoleSpan(holeSpan); simpleCache.release(); @@ -187,7 +187,7 @@ public void newInstance_withEncryptedIndexAndNoKey_clearsCache() throws Exceptio SimpleCache simpleCache = getEncryptedSimpleCache(ENCRYPTED_INDEX_KEY); // Write data. - CacheSpan holeSpan = simpleCache.startReadWrite(KEY_1, 0); + CacheSpan holeSpan = simpleCache.startReadWrite(KEY_1, 0, LENGTH_UNSET); addCache(simpleCache, KEY_1, 0, 15); simpleCache.releaseHoleSpan(holeSpan); simpleCache.release(); @@ -204,59 +204,179 @@ public void newInstance_withEncryptedIndexAndNoKey_clearsCache() throws Exceptio public void write_oneLock_oneFile_thenRead() throws Exception { SimpleCache simpleCache = getSimpleCache(); - CacheSpan holeSpan1 = simpleCache.startReadWrite(KEY_1, 0); - assertThat(holeSpan1.isCached).isFalse(); - assertThat(holeSpan1.isOpenEnded()).isTrue(); + CacheSpan holeSpan = simpleCache.startReadWrite(KEY_1, 0, LENGTH_UNSET); + assertThat(holeSpan.isCached).isFalse(); + assertThat(holeSpan.isOpenEnded()).isTrue(); addCache(simpleCache, KEY_1, 0, 15); - CacheSpan readSpan = simpleCache.startReadWrite(KEY_1, 0); + CacheSpan readSpan = simpleCache.startReadWrite(KEY_1, 0, LENGTH_UNSET); assertThat(readSpan.position).isEqualTo(0); assertThat(readSpan.length).isEqualTo(15); assertCachedDataReadCorrect(readSpan); assertThat(simpleCache.getCacheSpace()).isEqualTo(15); + + simpleCache.releaseHoleSpan(holeSpan); } @Test public void write_oneLock_twoFiles_thenRead() throws Exception { SimpleCache simpleCache = getSimpleCache(); - CacheSpan holeSpan = simpleCache.startReadWrite(KEY_1, 0); + CacheSpan holeSpan = simpleCache.startReadWrite(KEY_1, 0, LENGTH_UNSET); + addCache(simpleCache, KEY_1, 0, 7); + addCache(simpleCache, KEY_1, 7, 8); + + CacheSpan readSpan1 = simpleCache.startReadWrite(KEY_1, 0, LENGTH_UNSET); + assertThat(readSpan1.position).isEqualTo(0); + assertThat(readSpan1.length).isEqualTo(7); + assertCachedDataReadCorrect(readSpan1); + CacheSpan readSpan2 = simpleCache.startReadWrite(KEY_1, 7, LENGTH_UNSET); + assertThat(readSpan2.position).isEqualTo(7); + assertThat(readSpan2.length).isEqualTo(8); + assertCachedDataReadCorrect(readSpan2); + assertThat(simpleCache.getCacheSpace()).isEqualTo(15); + + simpleCache.releaseHoleSpan(holeSpan); + } + + @Test + public void write_twoLocks_twoFiles_thenRead() throws Exception { + SimpleCache simpleCache = getSimpleCache(); + + CacheSpan holeSpan1 = simpleCache.startReadWrite(KEY_1, 0, 7); + CacheSpan holeSpan2 = simpleCache.startReadWrite(KEY_1, 7, 8); + addCache(simpleCache, KEY_1, 0, 7); addCache(simpleCache, KEY_1, 7, 8); - CacheSpan readSpan1 = simpleCache.startReadWrite(KEY_1, 0); + CacheSpan readSpan1 = simpleCache.startReadWrite(KEY_1, 0, LENGTH_UNSET); assertThat(readSpan1.position).isEqualTo(0); assertThat(readSpan1.length).isEqualTo(7); assertCachedDataReadCorrect(readSpan1); - CacheSpan readSpan2 = simpleCache.startReadWrite(KEY_1, 7); + CacheSpan readSpan2 = simpleCache.startReadWrite(KEY_1, 7, LENGTH_UNSET); assertThat(readSpan2.position).isEqualTo(7); assertThat(readSpan2.length).isEqualTo(8); assertCachedDataReadCorrect(readSpan2); assertThat(simpleCache.getCacheSpace()).isEqualTo(15); + + simpleCache.releaseHoleSpan(holeSpan1); + simpleCache.releaseHoleSpan(holeSpan2); } @Test public void write_differentKeyLocked_thenRead() throws Exception { SimpleCache simpleCache = getSimpleCache(); - CacheSpan holeSpan1 = simpleCache.startReadWrite(KEY_1, 50); + CacheSpan holeSpan1 = simpleCache.startReadWrite(KEY_1, 0, LENGTH_UNSET); - CacheSpan holeSpan2 = simpleCache.startReadWrite(KEY_2, 50); - assertThat(holeSpan1.isCached).isFalse(); - assertThat(holeSpan1.isOpenEnded()).isTrue(); + CacheSpan holeSpan2 = simpleCache.startReadWrite(KEY_2, 0, LENGTH_UNSET); + assertThat(holeSpan2.isCached).isFalse(); + assertThat(holeSpan2.isOpenEnded()).isTrue(); addCache(simpleCache, KEY_2, 0, 15); - CacheSpan readSpan = simpleCache.startReadWrite(KEY_2, 0); + CacheSpan readSpan = simpleCache.startReadWrite(KEY_2, 0, LENGTH_UNSET); assertThat(readSpan.length).isEqualTo(15); assertCachedDataReadCorrect(readSpan); assertThat(simpleCache.getCacheSpace()).isEqualTo(15); + + simpleCache.releaseHoleSpan(holeSpan1); + simpleCache.releaseHoleSpan(holeSpan2); + } + + @Test + public void write_oneLock_fileExceedsLock_fails() throws Exception { + SimpleCache simpleCache = getSimpleCache(); + + CacheSpan holeSpan = simpleCache.startReadWrite(KEY_1, 0, 10); + + assertThrows(IllegalStateException.class, () -> addCache(simpleCache, KEY_1, 0, 11)); + + simpleCache.releaseHoleSpan(holeSpan); } @Test - public void write_sameKeyLocked_fails() throws Exception { + public void write_twoLocks_oneFileSpanningBothLocks_fails() throws Exception { SimpleCache simpleCache = getSimpleCache(); - CacheSpan cacheSpan1 = simpleCache.startReadWrite(KEY_1, 50); - assertThat(simpleCache.startReadWriteNonBlocking(KEY_1, 25)).isNull(); + CacheSpan holeSpan1 = simpleCache.startReadWrite(KEY_1, 0, 7); + CacheSpan holeSpan2 = simpleCache.startReadWrite(KEY_1, 7, 8); + + assertThrows(IllegalStateException.class, () -> addCache(simpleCache, KEY_1, 0, 15)); + + simpleCache.releaseHoleSpan(holeSpan1); + simpleCache.releaseHoleSpan(holeSpan2); + } + + @Test + public void write_unboundedRangeLocked_lockingOverlappingRange_fails() throws Exception { + SimpleCache simpleCache = getSimpleCache(); + CacheSpan holeSpan = simpleCache.startReadWrite(KEY_1, 50, LENGTH_UNSET); + + // Overlapping cannot be locked. + assertThat(simpleCache.startReadWriteNonBlocking(KEY_1, 49, 2)).isNull(); + assertThat(simpleCache.startReadWriteNonBlocking(KEY_1, 99, 2)).isNull(); + assertThat(simpleCache.startReadWriteNonBlocking(KEY_1, 0, LENGTH_UNSET)).isNull(); + assertThat(simpleCache.startReadWriteNonBlocking(KEY_1, 9, LENGTH_UNSET)).isNull(); + + simpleCache.releaseHoleSpan(holeSpan); + } + + @Test + public void write_unboundedRangeLocked_lockingNonOverlappingRange_succeeds() throws Exception { + SimpleCache simpleCache = getSimpleCache(); + CacheSpan holeSpan1 = simpleCache.startReadWrite(KEY_1, 50, LENGTH_UNSET); + + // Non-overlapping range can be locked. + CacheSpan holeSpan2 = simpleCache.startReadWrite(KEY_1, 0, 50); + assertThat(holeSpan2.isCached).isFalse(); + assertThat(holeSpan2.position).isEqualTo(0); + assertThat(holeSpan2.length).isEqualTo(50); + + simpleCache.releaseHoleSpan(holeSpan1); + simpleCache.releaseHoleSpan(holeSpan2); + } + + @Test + public void write_boundedRangeLocked_lockingOverlappingRange_fails() throws Exception { + SimpleCache simpleCache = getSimpleCache(); + CacheSpan holeSpan = simpleCache.startReadWrite(KEY_1, 50, 50); + + // Overlapping cannot be locked. + assertThat(simpleCache.startReadWriteNonBlocking(KEY_1, 49, 2)).isNull(); + assertThat(simpleCache.startReadWriteNonBlocking(KEY_1, 99, 2)).isNull(); + assertThat(simpleCache.startReadWriteNonBlocking(KEY_1, 0, LENGTH_UNSET)).isNull(); + assertThat(simpleCache.startReadWriteNonBlocking(KEY_1, 99, LENGTH_UNSET)).isNull(); + + simpleCache.releaseHoleSpan(holeSpan); + } + + @Test + public void write_boundedRangeLocked_lockingNonOverlappingRange_succeeds() throws Exception { + SimpleCache simpleCache = getSimpleCache(); + + CacheSpan holeSpan1 = simpleCache.startReadWrite(KEY_1, 50, 50); + assertThat(holeSpan1.isCached).isFalse(); + assertThat(holeSpan1.length).isEqualTo(50); + + // Non-overlapping range can be locked. + CacheSpan holeSpan2 = simpleCache.startReadWriteNonBlocking(KEY_1, 49, 1); + assertThat(holeSpan2.isCached).isFalse(); + assertThat(holeSpan2.position).isEqualTo(49); + assertThat(holeSpan2.length).isEqualTo(1); + simpleCache.releaseHoleSpan(holeSpan2); + + CacheSpan holeSpan3 = simpleCache.startReadWriteNonBlocking(KEY_1, 100, 1); + assertThat(holeSpan3.isCached).isFalse(); + assertThat(holeSpan3.position).isEqualTo(100); + assertThat(holeSpan3.length).isEqualTo(1); + simpleCache.releaseHoleSpan(holeSpan3); + + CacheSpan holeSpan4 = simpleCache.startReadWriteNonBlocking(KEY_1, 100, LENGTH_UNSET); + assertThat(holeSpan4.isCached).isFalse(); + assertThat(holeSpan4.position).isEqualTo(100); + assertThat(holeSpan4.isOpenEnded()).isTrue(); + simpleCache.releaseHoleSpan(holeSpan4); + + simpleCache.releaseHoleSpan(holeSpan1); } @Test @@ -275,11 +395,11 @@ public void applyContentMetadataMutations_setsContentLength() throws Exception { @Test public void removeSpans_removesSpansWithSameKey() throws Exception { SimpleCache simpleCache = getSimpleCache(); - CacheSpan holeSpan = simpleCache.startReadWrite(KEY_1, 0); + CacheSpan holeSpan = simpleCache.startReadWrite(KEY_1, 0, LENGTH_UNSET); addCache(simpleCache, KEY_1, 0, 10); addCache(simpleCache, KEY_1, 20, 10); simpleCache.releaseHoleSpan(holeSpan); - holeSpan = simpleCache.startReadWrite(KEY_2, 20); + holeSpan = simpleCache.startReadWrite(KEY_2, 20, LENGTH_UNSET); addCache(simpleCache, KEY_2, 20, 10); simpleCache.releaseHoleSpan(holeSpan); @@ -309,7 +429,7 @@ public void getCachedLength_noCachedContent_returnsNegativeMaxHoleLength() { @Test public void getCachedLength_returnsNegativeHoleLength() throws Exception { SimpleCache simpleCache = getSimpleCache(); - CacheSpan holeSpan = simpleCache.startReadWrite(KEY_1, /* position= */ 0); + CacheSpan holeSpan = simpleCache.startReadWrite(KEY_1, /* position= */ 0, LENGTH_UNSET); addCache(simpleCache, KEY_1, /* position= */ 50, /* length= */ 50); simpleCache.releaseHoleSpan(holeSpan); @@ -330,7 +450,7 @@ public void getCachedLength_returnsNegativeHoleLength() throws Exception { @Test public void getCachedLength_returnsCachedLength() throws Exception { SimpleCache simpleCache = getSimpleCache(); - CacheSpan holeSpan = simpleCache.startReadWrite(KEY_1, /* position= */ 0); + CacheSpan holeSpan = simpleCache.startReadWrite(KEY_1, /* position= */ 0, LENGTH_UNSET); addCache(simpleCache, KEY_1, /* position= */ 0, /* length= */ 50); simpleCache.releaseHoleSpan(holeSpan); @@ -353,7 +473,7 @@ public void getCachedLength_returnsCachedLength() throws Exception { @Test public void getCachedLength_withMultipleAdjacentSpans_returnsCachedLength() throws Exception { SimpleCache simpleCache = getSimpleCache(); - CacheSpan holeSpan = simpleCache.startReadWrite(KEY_1, /* position= */ 0); + CacheSpan holeSpan = simpleCache.startReadWrite(KEY_1, /* position= */ 0, LENGTH_UNSET); addCache(simpleCache, KEY_1, /* position= */ 0, /* length= */ 25); addCache(simpleCache, KEY_1, /* position= */ 25, /* length= */ 25); simpleCache.releaseHoleSpan(holeSpan); @@ -377,7 +497,7 @@ public void getCachedLength_withMultipleAdjacentSpans_returnsCachedLength() thro @Test public void getCachedLength_withMultipleNonAdjacentSpans_returnsCachedLength() throws Exception { SimpleCache simpleCache = getSimpleCache(); - CacheSpan holeSpan = simpleCache.startReadWrite(KEY_1, /* position= */ 0); + CacheSpan holeSpan = simpleCache.startReadWrite(KEY_1, /* position= */ 0, LENGTH_UNSET); addCache(simpleCache, KEY_1, /* position= */ 0, /* length= */ 10); addCache(simpleCache, KEY_1, /* position= */ 15, /* length= */ 35); simpleCache.releaseHoleSpan(holeSpan); @@ -419,7 +539,7 @@ public void getCachedBytes_noCachedContent_returnsZero() { @Test public void getCachedBytes_withMultipleAdjacentSpans_returnsCachedBytes() throws Exception { SimpleCache simpleCache = getSimpleCache(); - CacheSpan holeSpan = simpleCache.startReadWrite(KEY_1, /* position= */ 0); + CacheSpan holeSpan = simpleCache.startReadWrite(KEY_1, /* position= */ 0, LENGTH_UNSET); addCache(simpleCache, KEY_1, /* position= */ 0, /* length= */ 25); addCache(simpleCache, KEY_1, /* position= */ 25, /* length= */ 25); simpleCache.releaseHoleSpan(holeSpan); @@ -443,7 +563,7 @@ public void getCachedBytes_withMultipleAdjacentSpans_returnsCachedBytes() throws @Test public void getCachedBytes_withMultipleNonAdjacentSpans_returnsCachedBytes() throws Exception { SimpleCache simpleCache = getSimpleCache(); - CacheSpan holeSpan = simpleCache.startReadWrite(KEY_1, /* position= */ 0); + CacheSpan holeSpan = simpleCache.startReadWrite(KEY_1, /* position= */ 0, LENGTH_UNSET); addCache(simpleCache, KEY_1, /* position= */ 0, /* length= */ 10); addCache(simpleCache, KEY_1, /* position= */ 15, /* length= */ 35); simpleCache.releaseHoleSpan(holeSpan); @@ -474,7 +594,7 @@ public void exceptionDuringIndexStore_doesNotPreventEviction() throws Exception cacheDir, new LeastRecentlyUsedCacheEvictor(20), contentIndex, /* fileIndex= */ null); // Add some content. - CacheSpan holeSpan = simpleCache.startReadWrite(KEY_1, 0); + CacheSpan holeSpan = simpleCache.startReadWrite(KEY_1, 0, LENGTH_UNSET); addCache(simpleCache, KEY_1, 0, 15); // Make index.store() throw exception from now on. @@ -502,7 +622,8 @@ public void usingReleasedCache_throwsException() { SimpleCache simpleCache = new SimpleCache(cacheDir, new NoOpCacheEvictor()); simpleCache.release(); assertThrows( - IllegalStateException.class, () -> simpleCache.startReadWriteNonBlocking(KEY_1, 0)); + IllegalStateException.class, + () -> simpleCache.startReadWriteNonBlocking(KEY_1, 0, LENGTH_UNSET)); } private SimpleCache getSimpleCache() {