Feature/clear all cache (#117)

* public api to clear memory and disk caches
This commit is contained in:
Mike Nakhimovich 2017-02-08 15:44:12 -05:00 committed by GitHub
parent 4afb8ceb0c
commit f4fdd1e9d9
11 changed files with 275 additions and 32 deletions

View file

@ -90,7 +90,7 @@ public class StoreRefreshWhenStaleTest {
verify(fetcher, times(0)).fetch(barCode);
verify(persister, times(1)).getRecordState(barCode);
store.clearMemory(barCode);
store.clear(barCode);
result = store.get(barCode).test().awaitTerminalEvent().getOnNextEvents().get(0);
assertThat(result).isEqualTo(disk2);
verify(fetcher, times(0)).fetch(barCode);

View file

@ -0,0 +1,10 @@
package com.nytimes.android.external.store.base;
/**
* Persisters should implement Clearable if they want store.clear(key) to also clear the persister
* @param <T> Type of key/request param in store
*/
public interface Clearable<T> {
void clear(T key);
}

View file

@ -43,7 +43,9 @@ public interface Store<T, V> {
Observable<T> fetch(@Nonnull V key);
/**
* @return an Observable that emits new items when they arrive.
* @return an Observable that emits "fresh" new response from the store that hit the fetcher
* WARNING: stream is an endless observable, be careful when combining
* with operators that expect an OnComplete event
*/
@Nonnull
Observable<T> stream();
@ -63,12 +65,26 @@ public interface Store<T, V> {
/**
* Clear the memory cache of all entries
*/
@Deprecated
void clearMemory();
/**
* Purge a particular entry from memory cache.
*/
@Deprecated
void clearMemory(@Nonnull V key);
/**
* purges all entries from memory and disk cache
* Persister will only be cleared if they implements Clearable
*/
void clear();
/**
* Purge a particular entry from memory and disk cache.
* Persister will only be cleared if they implements Clearable
*/
void clear(@Nonnull V key);
}

View file

@ -109,6 +109,16 @@ public class ProxyStore<Parsed> implements Store<Parsed> {
internalStore.clearMemory(barCode);
}
@Override
public void clear() {
internalStore.clear();
}
@Override
public void clear(@Nonnull BarCode key) {
internalStore.clear(key);
}
protected Observable<Parsed> memory(@Nonnull BarCode id) {
return internalStore.memory(id);
}

View file

@ -2,11 +2,11 @@ package com.nytimes.android.external.store.base.impl;
import com.nytimes.android.external.cache.Cache;
import com.nytimes.android.external.cache.CacheBuilder;
import com.nytimes.android.external.store.base.Clearable;
import com.nytimes.android.external.store.base.Fetcher;
import com.nytimes.android.external.store.base.InternalStore;
import com.nytimes.android.external.store.base.Parser;
import com.nytimes.android.external.store.base.Persister;
import com.nytimes.android.external.store.util.NoopPersister;
import com.nytimes.android.external.store.util.OnErrorResumeWithEmpty;
import java.util.concurrent.Callable;
@ -309,22 +309,25 @@ final class RealInternalStore<Raw, Parsed, Key> implements InternalStore<Parsed,
}
@Override
@Deprecated
public void clearMemory() {
inFlightRequests.invalidateAll();
clearDiskIfNoOp();
for (Key cachedKey : memCache.asMap().keySet()) {
memCache.invalidate(cachedKey);
clearDiskIfNoOp(cachedKey);
refreshSubject.onNext(cachedKey);
}
}
private void clearDiskIfNoOp() {
if (persister() instanceof NoopPersister) {
persister = new NoopPersister<>();
private boolean clearDiskIfNoOp(Key barCode) {
if (persister() instanceof Clearable) {
((Clearable<Key>) persister).clear(barCode);
return true;
}
return false;
}
/**
@ -333,11 +336,33 @@ final class RealInternalStore<Raw, Parsed, Key> implements InternalStore<Parsed,
* @param barCode of data to clear
*/
@Override
@Deprecated
public void clearMemory(@Nonnull final Key barCode) {
inFlightRequests.invalidate(barCode);
clearDiskIfNoOp();
memCache.invalidate(barCode);
refreshSubject.onNext(barCode);
if (persister instanceof Clearable) {
((Clearable<Key>) persister).clear(barCode);
}
}
@Override
public void clear(){
for (Key cachedKey : memCache.asMap().keySet()) {
clear(cachedKey);
}
}
@Override
public void clear(@Nonnull Key key) {
inFlightRequests.invalidate(key);
memCache.invalidate(key);
if (persister instanceof Clearable) {
((Clearable<Key>) persister).clear(key);
}
refreshSubject.onNext(key);
}

View file

@ -107,6 +107,17 @@ public class RealStore<Parsed, Key> implements Store<Parsed, Key> {
internalStore.clearMemory(barCode);
}
@Override
public void clear() {
internalStore.clear();
}
@Override
public void clear(@Nonnull Key key) {
internalStore.clear();
}
protected Observable<Parsed> memory(@Nonnull Key id) {
return internalStore.memory(id);
}

View file

@ -1,6 +1,7 @@
package com.nytimes.android.external.store.util;
import com.nytimes.android.external.store.base.Clearable;
import com.nytimes.android.external.store.base.Persister;
import java.util.concurrent.ConcurrentHashMap;
@ -13,8 +14,8 @@ import rx.Observable;
/**
* Pass-through diskdao for stores that don't want to use persister
*/
public class NoopPersister<Raw, Key> implements Persister<Raw, Key> {
private final ConcurrentMap<Key, Raw> networkResponses = new ConcurrentHashMap<>();
public class NoopPersister<Raw, Key> implements Persister<Raw, Key>, Clearable<Key> {
protected final ConcurrentMap<Key, Raw> networkResponses = new ConcurrentHashMap<>();
@Nonnull
@Override
@ -29,4 +30,9 @@ public class NoopPersister<Raw, Key> implements Persister<Raw, Key> {
networkResponses.put(barCode, raw);
return Observable.just(true);
}
@Override
public void clear(Key key) {
networkResponses.remove(key);
}
}

View file

@ -42,7 +42,7 @@ public class ClearStoreMemoryTest {
@Test
public void testClearSingleBarCode() {
// one request should produce one call
//one request should produce one call
BarCode barcode = new BarCode("type", "key");
store.get(barcode).test().awaitTerminalEvent();
assertThat(networkCalls).isEqualTo(1);
@ -58,14 +58,14 @@ public class ClearStoreMemoryTest {
BarCode b1 = new BarCode("type1", "key1");
BarCode b2 = new BarCode("type2", "key2");
// each request should produce one call
//each request should produce one call
store.get(b1).test().awaitTerminalEvent();
store.get(b2).test().awaitTerminalEvent();
assertThat(networkCalls).isEqualTo(2);
store.clearMemory();
// after everything is cleared each request should produce another 2 calls
//after everything is cleared each request should produce another 2 calls
store.get(b1).test().awaitTerminalEvent();
store.get(b2).test().awaitTerminalEvent();
assertThat(networkCalls).isEqualTo(4);

View file

@ -0,0 +1,110 @@
package com.nytimes.android.external.store;
import com.nytimes.android.external.store.base.Fetcher;
import com.nytimes.android.external.store.base.Store;
import com.nytimes.android.external.store.base.impl.BarCode;
import com.nytimes.android.external.store.base.impl.StoreBuilder;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.mockito.Mock;
import org.mockito.runners.MockitoJUnitRunner;
import java.util.concurrent.Callable;
import java.util.concurrent.atomic.AtomicInteger;
import javax.annotation.Nonnull;
import rx.Observable;
import static com.nytimes.android.external.store.GetRefreshingTest.ClearingPersister;
import static org.assertj.core.api.Assertions.assertThat;
import static org.mockito.Mockito.when;
@RunWith(MockitoJUnitRunner.class)
public class ClearStoreTest {
@Mock
ClearingPersister persister;
private AtomicInteger networkCalls;
private Store<Integer> store;
@Before
public void setUp() {
networkCalls = new AtomicInteger(0);
store = StoreBuilder.<Integer>builder()
.fetcher(new Fetcher<Integer, BarCode>() {
@Nonnull
@Override
public Observable<Integer> fetch(BarCode barCode) {
return Observable.fromCallable(new Callable<Integer>() {
@Override
public Integer call() {
return networkCalls.incrementAndGet();
}
});
}
})
.persister(persister)
.open();
}
@Test
public void testClearSingleBarCode() {
// one request should produce one call
BarCode barcode = new BarCode("type", "key");
when(persister.read(barcode))
.thenReturn(Observable.<Integer>empty()) //read from disk on get
.thenReturn(Observable.just(1)) //read from disk after fetching from network
.thenReturn(Observable.<Integer>empty()) //read from disk after clearing
.thenReturn(Observable.just(1)); //read from disk after making additional network call
when(persister.write(barcode, 1)).thenReturn(Observable.just(true));
when(persister.write(barcode, 2)).thenReturn(Observable.just(true));
store.get(barcode).test().awaitTerminalEvent();
assertThat(networkCalls.intValue()).isEqualTo(1);
// after clearing the memory another call should be made
store.clear(barcode);
store.get(barcode).test().awaitTerminalEvent();
assertThat(networkCalls.intValue()).isEqualTo(2);
}
@Test
public void testClearAllBarCodes() {
BarCode barcode1 = new BarCode("type1", "key1");
BarCode barcode2 = new BarCode("type2", "key2");
when(persister.read(barcode1))
.thenReturn(Observable.<Integer>empty()) //read from disk
.thenReturn(Observable.just(1)) //read from disk after fetching from network
.thenReturn(Observable.<Integer>empty()) //read from disk after clearing disk cache
.thenReturn(Observable.just(1)); //read from disk after making additional network call
when(persister.write(barcode1, 1)).thenReturn(Observable.just(true));
when(persister.write(barcode1, 2)).thenReturn(Observable.just(true));
when(persister.read(barcode2))
.thenReturn(Observable.<Integer>empty()) //read from disk
.thenReturn(Observable.just(1)) //read from disk after fetching from network
.thenReturn(Observable.<Integer>empty()) //read from disk after clearing disk cache
.thenReturn(Observable.just(1)); //read from disk after making additional network call
when(persister.write(barcode2, 1)).thenReturn(Observable.just(true));
when(persister.write(barcode2, 2)).thenReturn(Observable.just(true));
// each request should produce one call
store.get(barcode1).test().awaitTerminalEvent();
store.get(barcode2).test().awaitTerminalEvent();
assertThat(networkCalls.intValue()).isEqualTo(2);
store.clear();
// after everything is cleared each request should produce another 2 calls
store.get(barcode1).test().awaitTerminalEvent();
store.get(barcode2).test().awaitTerminalEvent();
assertThat(networkCalls.intValue()).isEqualTo(4);
}
}

View file

@ -1,14 +1,20 @@
package com.nytimes.android.external.store;
import com.nytimes.android.external.store.base.Clearable;
import com.nytimes.android.external.store.base.Fetcher;
import com.nytimes.android.external.store.base.Store;
import com.nytimes.android.external.store.base.Persister;
import com.nytimes.android.external.store.base.beta.Store;
import com.nytimes.android.external.store.base.impl.BarCode;
import com.nytimes.android.external.store.base.impl.StoreBuilder;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.mockito.Mock;
import org.mockito.runners.MockitoJUnitRunner;
import java.util.concurrent.Callable;
import java.util.concurrent.atomic.AtomicInteger;
import javax.annotation.Nonnull;
@ -16,16 +22,19 @@ import rx.Observable;
import rx.observers.AssertableSubscriber;
import static org.assertj.core.api.Assertions.assertThat;
import static org.mockito.Mockito.when;
@RunWith(MockitoJUnitRunner.class)
public class GetRefreshingTest {
private int networkCalls = 0;
private Store<Integer> store;
@Mock
ClearingPersister persister;
private AtomicInteger networkCalls;
private Store<Integer, BarCode> store;
@Before
public void setUp() {
networkCalls = 0;
store = StoreBuilder.<Integer>builder()
networkCalls = new AtomicInteger(0);
store = StoreBuilder.<Integer>barcode()
.fetcher(new Fetcher<Integer, BarCode>() {
@Nonnull
@Override
@ -33,28 +42,38 @@ public class GetRefreshingTest {
return Observable.fromCallable(new Callable<Integer>() {
@Override
public Integer call() {
return networkCalls++;
return networkCalls.incrementAndGet();
}
});
}
})
.persister(persister)
.open();
}
@Test
public void testRefreshOnClear() {
BarCode barcode = new BarCode("type", "key");
AssertableSubscriber<Integer> testStore = store.getRefreshing(barcode).test();
assertThat(testStore.getValueCount()).isEqualTo(1);
assertThat(networkCalls).isEqualTo(1);
when(persister.read(barcode))
.thenReturn(Observable.<Integer>empty()) //read from disk
.thenReturn(Observable.just(1)) //read from disk after fetching from network
.thenReturn(Observable.<Integer>empty()) //read from disk after clearing disk cache
.thenReturn(Observable.just(1)); //read from disk after making additional network call
when(persister.write(barcode, 1)).thenReturn(Observable.just(true));
when(persister.write(barcode, 2)).thenReturn(Observable.just(true));
AssertableSubscriber<Integer> refreshingObservable = store.getRefreshing(barcode).test();
assertThat(refreshingObservable.getValueCount()).isEqualTo(1);
assertThat(networkCalls.intValue()).isEqualTo(1);
//clearing the store should produce another network call
store.clearMemory(barcode);
assertThat(testStore.getValueCount()).isEqualTo(2);
assertThat(networkCalls).isEqualTo(2);
store.clear(barcode);
assertThat(refreshingObservable.getValueCount()).isEqualTo(2);
assertThat(networkCalls.intValue()).isEqualTo(2);
store.get(barcode).test().awaitTerminalEvent();
assertThat(testStore.getValueCount()).isEqualTo(2);
assertThat(networkCalls).isEqualTo(2);
assertThat(refreshingObservable.getValueCount()).isEqualTo(2);
assertThat(networkCalls.intValue()).isEqualTo(2);
}
@Test
@ -62,17 +81,53 @@ public class GetRefreshingTest {
BarCode barcode1 = new BarCode("type", "key");
BarCode barcode2 = new BarCode("type", "key2");
when(persister.read(barcode1))
.thenReturn(Observable.<Integer>empty()) //read from disk
.thenReturn(Observable.just(1)) //read from disk after fetching from network
.thenReturn(Observable.<Integer>empty()) //read from disk after clearing disk cache
.thenReturn(Observable.just(1)); //read from disk after making additional network call
when(persister.write(barcode1, 1)).thenReturn(Observable.just(true));
when(persister.write(barcode1, 2)).thenReturn(Observable.just(true));
when(persister.read(barcode2))
.thenReturn(Observable.<Integer>empty()) //read from disk
.thenReturn(Observable.just(1)) //read from disk after fetching from network
.thenReturn(Observable.<Integer>empty()) //read from disk after clearing disk cache
.thenReturn(Observable.just(1)); //read from disk after making additional network call
when(persister.write(barcode2, 1)).thenReturn(Observable.just(true));
when(persister.write(barcode2, 2)).thenReturn(Observable.just(true));
AssertableSubscriber<Integer> testObservable1 = store.getRefreshing(barcode1).test();
AssertableSubscriber<Integer> testObservable2 = store.getRefreshing(barcode2).test();
assertThat(testObservable1.getValueCount()).isEqualTo(1);
assertThat(testObservable2.getValueCount()).isEqualTo(1);
assertThat(networkCalls).isEqualTo(2);
assertThat(networkCalls.intValue()).isEqualTo(2);
store.clearMemory();
assertThat(networkCalls).isEqualTo(4);
store.clear();
assertThat(networkCalls.intValue()).isEqualTo(4);
}
//everything will be mocked
static class ClearingPersister implements Persister<Integer, BarCode>, Clearable<BarCode> {
@Override
public void clear(BarCode key) {
throw new RuntimeException();
}
@Nonnull
@Override
public Observable<Integer> read(BarCode barCode) {
throw new RuntimeException();
}
@Nonnull
@Override
public Observable<Boolean> write(BarCode barCode, Integer integer) {
throw new RuntimeException();
}
}
}

View file

@ -123,7 +123,7 @@ public class StoreTest {
public void testSubclass() {
ProxyStore<String> simpleStore = new SampleStore(fetcher, persister);
simpleStore.clearMemory();
simpleStore.clear();
when(fetcher.fetch(barCode))
.thenReturn(Observable.just(NETWORK));