Skip to content

Commit bd7be1b

Browse files
erdemguvenojw28
authored andcommitted
Cache support unbounded requests.
------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=131696858
1 parent dfad745 commit bd7be1b

File tree

11 files changed

+579
-85
lines changed

11 files changed

+579
-85
lines changed
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,190 @@
1+
/*
2+
* Copyright (C) 2016 The Android Open Source Project
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
package com.google.android.exoplayer2.upstream.cache;
17+
18+
import android.net.Uri;
19+
import android.test.InstrumentationTestCase;
20+
import android.test.MoreAsserts;
21+
import com.google.android.exoplayer2.C;
22+
import com.google.android.exoplayer2.testutil.FakeDataSource;
23+
import com.google.android.exoplayer2.testutil.FakeDataSource.Builder;
24+
import com.google.android.exoplayer2.testutil.TestUtil;
25+
import com.google.android.exoplayer2.upstream.DataSpec;
26+
import com.google.android.exoplayer2.upstream.HttpDataSource.InvalidResponseCodeException;
27+
import java.io.File;
28+
import java.io.IOException;
29+
import java.util.Arrays;
30+
31+
/** Unit tests for {@link CacheDataSource}. */
32+
public class CacheDataSourceTest extends InstrumentationTestCase {
33+
34+
private static final byte[] TEST_DATA = new byte[] {0, 1, 2, 3, 4, 5, 6, 7, 8, 9};
35+
private static final int MAX_CACHE_FILE_SIZE = 3;
36+
private static final String KEY_1 = "key 1";
37+
private static final String KEY_2 = "key 2";
38+
39+
private File cacheDir;
40+
private SimpleCache simpleCache;
41+
42+
@Override
43+
protected void setUp() throws Exception {
44+
// Create a temporary folder
45+
cacheDir = File.createTempFile("CacheDataSourceTest", null);
46+
assertTrue(cacheDir.delete());
47+
assertTrue(cacheDir.mkdir());
48+
49+
simpleCache = new SimpleCache(cacheDir, new NoOpCacheEvictor());
50+
}
51+
52+
@Override
53+
protected void tearDown() throws Exception {
54+
TestUtil.recursiveDelete(cacheDir);
55+
}
56+
57+
public void testMaxCacheFileSize() throws Exception {
58+
CacheDataSource cacheDataSource = createCacheDataSource(false, false, false);
59+
assertReadDataContentLength(cacheDataSource, false, false);
60+
assertEquals((int) Math.ceil((double) TEST_DATA.length / MAX_CACHE_FILE_SIZE),
61+
cacheDir.listFiles().length);
62+
}
63+
64+
public void testCacheAndRead() throws Exception {
65+
assertCacheAndRead(false, false);
66+
}
67+
68+
public void testCacheAndReadUnboundedRequest() throws Exception {
69+
assertCacheAndRead(true, false);
70+
}
71+
72+
public void testCacheAndReadUnknownLength() throws Exception {
73+
assertCacheAndRead(false, true);
74+
}
75+
76+
// Disabled test as we don't support caching of definitely unknown length content
77+
public void disabledTestCacheAndReadUnboundedRequestUnknownLength() throws Exception {
78+
assertCacheAndRead(true, true);
79+
}
80+
81+
public void testUnsatisfiableRange() throws Exception {
82+
// Bounded request but the content length is unknown. This forces all data to be cached but not
83+
// the length
84+
assertCacheAndRead(false, true);
85+
86+
// Now do an unbounded request. This will read all of the data from cache and then try to read
87+
// more from upstream which will cause to a 416 so CDS will store the length.
88+
CacheDataSource cacheDataSource = createCacheDataSource(true, true, true);
89+
assertReadDataContentLength(cacheDataSource, true, true);
90+
91+
// If the user try to access off range then it should throw an IOException
92+
try {
93+
cacheDataSource = createCacheDataSource(false, false, false);
94+
cacheDataSource.open(new DataSpec(Uri.EMPTY, TEST_DATA.length, 5, KEY_1));
95+
fail();
96+
} catch (TestIOException e) {
97+
// success
98+
}
99+
}
100+
101+
public void testContentLengthEdgeCases() throws Exception {
102+
// Read partial at EOS but don't cross it so length is unknown
103+
CacheDataSource cacheDataSource = createCacheDataSource(false, false, true);
104+
assertReadData(cacheDataSource, true, TEST_DATA.length - 2, 2);
105+
assertEquals(C.LENGTH_UNSET, simpleCache.getContentLength(KEY_1));
106+
107+
// Now do an unbounded request for whole data. This will cause a bounded request from upstream.
108+
// End of data from upstream shouldn't be mixed up with EOS and cause length set wrong.
109+
cacheDataSource = createCacheDataSource(true, false, true);
110+
assertReadDataContentLength(cacheDataSource, true, true);
111+
112+
// Now the length set correctly do an unbounded request with offset
113+
assertEquals(2, cacheDataSource.open(new DataSpec(Uri.EMPTY, TEST_DATA.length - 2,
114+
C.LENGTH_UNSET, KEY_1)));
115+
116+
// An unbounded request with offset for not cached content
117+
assertEquals(C.LENGTH_UNSET, cacheDataSource.open(new DataSpec(Uri.EMPTY, TEST_DATA.length - 2,
118+
C.LENGTH_UNSET, KEY_2)));
119+
}
120+
121+
private void assertCacheAndRead(boolean unboundedRequest, boolean simulateUnknownLength)
122+
throws IOException {
123+
// Read all data from upstream and cache
124+
CacheDataSource cacheDataSource = createCacheDataSource(false, false, simulateUnknownLength);
125+
assertReadDataContentLength(cacheDataSource, unboundedRequest, simulateUnknownLength);
126+
127+
// Just read from cache
128+
cacheDataSource = createCacheDataSource(false, true, simulateUnknownLength);
129+
assertReadDataContentLength(cacheDataSource, unboundedRequest,
130+
false /*length is already cached*/);
131+
}
132+
133+
/**
134+
* Reads data until EOI and compares it to {@link #TEST_DATA}. Also checks content length returned
135+
* from open() call and the cached content length.
136+
*/
137+
private void assertReadDataContentLength(CacheDataSource cacheDataSource,
138+
boolean unboundedRequest, boolean unknownLength) throws IOException {
139+
int length = unboundedRequest ? C.LENGTH_UNSET : TEST_DATA.length;
140+
assertReadData(cacheDataSource, unknownLength, 0, length);
141+
assertEquals("When the range specified, CacheDataSource doesn't reach EOS so shouldn't cache "
142+
+ "content length", !unboundedRequest ? C.LENGTH_UNSET : TEST_DATA.length,
143+
simpleCache.getContentLength(KEY_1));
144+
}
145+
146+
private void assertReadData(CacheDataSource cacheDataSource, boolean unknownLength, int position,
147+
int length) throws IOException {
148+
int actualLength = TEST_DATA.length - position;
149+
if (length != C.LENGTH_UNSET) {
150+
actualLength = Math.min(actualLength, length);
151+
}
152+
assertEquals(unknownLength ? length : actualLength,
153+
cacheDataSource.open(new DataSpec(Uri.EMPTY, position, length, KEY_1)));
154+
155+
byte[] buffer = new byte[100];
156+
int index = 0;
157+
while (true) {
158+
int read = cacheDataSource.read(buffer, index, buffer.length - index);
159+
if (read == C.RESULT_END_OF_INPUT) {
160+
break;
161+
}
162+
index += read;
163+
}
164+
assertEquals(actualLength, index);
165+
MoreAsserts.assertEquals(Arrays.copyOfRange(TEST_DATA, position, position + actualLength),
166+
Arrays.copyOf(buffer, index));
167+
168+
cacheDataSource.close();
169+
}
170+
171+
private CacheDataSource createCacheDataSource(boolean set416exception, boolean setReadException,
172+
boolean simulateUnknownLength) {
173+
Builder builder = new Builder();
174+
if (setReadException) {
175+
builder.appendReadError(new IOException("Shouldn't read from upstream"));
176+
}
177+
builder.setSimulateUnknownLength(simulateUnknownLength);
178+
builder.appendReadData(TEST_DATA);
179+
FakeDataSource upstream = builder.build();
180+
upstream.setUnsatisfiableRangeException(set416exception
181+
? new InvalidResponseCodeException(416, null, null)
182+
: new TestIOException());
183+
return new CacheDataSource(simpleCache, upstream,
184+
CacheDataSource.FLAG_BLOCK_ON_CACHE | CacheDataSource.FLAG_CACHE_UNBOUNDED_REQUESTS,
185+
MAX_CACHE_FILE_SIZE);
186+
}
187+
188+
private static class TestIOException extends IOException {}
189+
190+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,123 @@
1+
/*
2+
* Copyright (C) 2016 The Android Open Source Project
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
package com.google.android.exoplayer2.upstream.cache;
17+
18+
import android.test.InstrumentationTestCase;
19+
20+
import com.google.android.exoplayer2.C;
21+
import com.google.android.exoplayer2.testutil.TestUtil;
22+
import java.io.File;
23+
import java.io.FileOutputStream;
24+
import java.io.IOException;
25+
import java.util.NavigableSet;
26+
import java.util.Set;
27+
28+
/**
29+
* Unit tests for {@link SimpleCache}.
30+
*/
31+
public class SimpleCacheTest extends InstrumentationTestCase {
32+
33+
private static final String KEY_1 = "key1";
34+
35+
private File cacheDir;
36+
37+
@Override
38+
protected void setUp() throws Exception {
39+
// Create a temporary folder
40+
cacheDir = File.createTempFile("SimpleCacheTest", null);
41+
assertTrue(cacheDir.delete());
42+
assertTrue(cacheDir.mkdir());
43+
}
44+
45+
@Override
46+
protected void tearDown() throws Exception {
47+
TestUtil.recursiveDelete(cacheDir);
48+
}
49+
50+
public void testCommittingOneFile() throws Exception {
51+
SimpleCache simpleCache = new SimpleCache(cacheDir, new NoOpCacheEvictor());
52+
53+
CacheSpan cacheSpan = simpleCache.startReadWrite(KEY_1, 0);
54+
assertFalse(cacheSpan.isCached);
55+
assertTrue(cacheSpan.isOpenEnded());
56+
57+
assertNull(simpleCache.startReadWriteNonBlocking(KEY_1, 0));
58+
59+
assertEquals(0, simpleCache.getKeys().size());
60+
NavigableSet<CacheSpan> cachedSpans = simpleCache.getCachedSpans(KEY_1);
61+
assertTrue(cachedSpans == null || cachedSpans.size() == 0);
62+
assertEquals(0, simpleCache.getCacheSpace());
63+
assertEquals(0, cacheDir.listFiles().length);
64+
65+
addCache(simpleCache, 0, 15);
66+
67+
Set<String> cachedKeys = simpleCache.getKeys();
68+
assertEquals(1, cachedKeys.size());
69+
assertTrue(cachedKeys.contains(KEY_1));
70+
cachedSpans = simpleCache.getCachedSpans(KEY_1);
71+
assertEquals(1, cachedSpans.size());
72+
assertTrue(cachedSpans.contains(cacheSpan));
73+
assertEquals(15, simpleCache.getCacheSpace());
74+
75+
cacheSpan = simpleCache.startReadWrite(KEY_1, 0);
76+
assertTrue(cacheSpan.isCached);
77+
assertFalse(cacheSpan.isOpenEnded());
78+
assertEquals(15, cacheSpan.length);
79+
}
80+
81+
public void testSetGetLength() throws Exception {
82+
SimpleCache simpleCache = new SimpleCache(cacheDir, new NoOpCacheEvictor());
83+
84+
assertEquals(C.LENGTH_UNSET, simpleCache.getContentLength(KEY_1));
85+
assertTrue(simpleCache.setContentLength(KEY_1, 15));
86+
assertEquals(15, simpleCache.getContentLength(KEY_1));
87+
88+
simpleCache.startReadWrite(KEY_1, 0);
89+
90+
addCache(simpleCache, 0, 15);
91+
92+
assertTrue(simpleCache.setContentLength(KEY_1, 150));
93+
assertEquals(150, simpleCache.getContentLength(KEY_1));
94+
95+
addCache(simpleCache, 140, 10);
96+
97+
// Try to set length shorter then the content
98+
assertFalse(simpleCache.setContentLength(KEY_1, 15));
99+
assertEquals("Content length should be unchanged.",
100+
150, simpleCache.getContentLength(KEY_1));
101+
102+
/* TODO Enable when the length persistance is fixed
103+
// Check if values are kept after cache is reloaded.
104+
simpleCache = new SimpleCache(cacheDir, new NoOpCacheEvictor());
105+
assertEquals(150, simpleCache.getContentLength(KEY_1));
106+
CacheSpan lastSpan = simpleCache.startReadWrite(KEY_1, 145);
107+
108+
// Removing the last span shouldn't cause the length be change next time cache loaded
109+
simpleCache.removeSpan(lastSpan);
110+
simpleCache = new SimpleCache(cacheDir, new NoOpCacheEvictor());
111+
assertEquals(150, simpleCache.getContentLength(KEY_1));
112+
*/
113+
}
114+
115+
private void addCache(SimpleCache simpleCache, int position, int length) throws IOException {
116+
File file = simpleCache.startFile(KEY_1, position, length);
117+
FileOutputStream fos = new FileOutputStream(file);
118+
fos.write(new byte[length]);
119+
fos.close();
120+
simpleCache.commitFile(file);
121+
}
122+
123+
}

library/src/main/java/com/google/android/exoplayer2/upstream/cache/Cache.java

+21-3
Original file line numberDiff line numberDiff line change
@@ -143,11 +143,11 @@ interface Listener {
143143
*
144144
* @param key The cache key for the data.
145145
* @param position The starting position of the data.
146-
* @param length The length of the data to be written. Used only to ensure that there is enough
147-
* space in the cache.
146+
* @param maxLength The maximum length of the data to be written. Used only to ensure that there
147+
* is enough space in the cache.
148148
* @return The file into which data should be written.
149149
*/
150-
File startFile(String key, long position, long length);
150+
File startFile(String key, long position, long maxLength);
151151

152152
/**
153153
* Commits a file into the cache. Must only be called when holding a corresponding hole
@@ -182,4 +182,22 @@ interface Listener {
182182
*/
183183
boolean isCached(String key, long position, long length);
184184

185+
/**
186+
* Sets the content length for the given key.
187+
*
188+
* @param key The cache key for the data.
189+
* @param length The length of the data.
190+
* @return Whether the length was set successfully. Returns false if the length conflicts with the
191+
* existing contents of the cache.
192+
*/
193+
boolean setContentLength(String key, long length);
194+
195+
/**
196+
* Returns the content length for the given key if one set, or {@link
197+
* com.google.android.exoplayer2.C#LENGTH_UNSET} otherwise.
198+
*
199+
* @param key The cache key for the data.
200+
*/
201+
long getContentLength(String key);
202+
185203
}

library/src/main/java/com/google/android/exoplayer2/upstream/cache/CacheDataSink.java

+11-5
Original file line numberDiff line numberDiff line change
@@ -64,12 +64,12 @@ public CacheDataSink(Cache cache, long maxCacheFileSize) {
6464

6565
@Override
6666
public void open(DataSpec dataSpec) throws CacheDataSinkException {
67-
// TODO: Support caching for unbounded requests. See TODO in {@link CacheDataSource} for
68-
// more details.
69-
Assertions.checkState(dataSpec.length != C.LENGTH_UNSET);
67+
this.dataSpec = dataSpec;
68+
if (dataSpec.length == C.LENGTH_UNSET) {
69+
return;
70+
}
71+
dataSpecBytesWritten = 0;
7072
try {
71-
this.dataSpec = dataSpec;
72-
dataSpecBytesWritten = 0;
7373
openNextOutputStream();
7474
} catch (FileNotFoundException e) {
7575
throw new CacheDataSinkException(e);
@@ -78,6 +78,9 @@ public void open(DataSpec dataSpec) throws CacheDataSinkException {
7878

7979
@Override
8080
public void write(byte[] buffer, int offset, int length) throws CacheDataSinkException {
81+
if (dataSpec.length == C.LENGTH_UNSET) {
82+
return;
83+
}
8184
try {
8285
int bytesWritten = 0;
8386
while (bytesWritten < length) {
@@ -99,6 +102,9 @@ public void write(byte[] buffer, int offset, int length) throws CacheDataSinkExc
99102

100103
@Override
101104
public void close() throws CacheDataSinkException {
105+
if (dataSpec == null || dataSpec.length == C.LENGTH_UNSET) {
106+
return;
107+
}
102108
try {
103109
closeCurrentOutputStream();
104110
} catch (IOException e) {

0 commit comments

Comments
 (0)