Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add removal listener + build on local cache module #442

Open
wants to merge 3 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions cache/api/cache.api
Original file line number Diff line number Diff line change
Expand Up @@ -187,6 +187,7 @@ public abstract interface class com/dropbox/android/external/cache3/RemovalListe
public final class com/dropbox/android/external/cache3/RemovalNotification : java/util/Map$Entry {
public static fun create (Ljava/lang/Object;Ljava/lang/Object;Lcom/dropbox/android/external/cache3/RemovalCause;)Lcom/dropbox/android/external/cache3/RemovalNotification;
public fun equals (Ljava/lang/Object;)Z
public fun getCause ()Lcom/dropbox/android/external/cache3/RemovalCause;
public fun getKey ()Ljava/lang/Object;
public fun getValue ()Ljava/lang/Object;
public fun hashCode ()I
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ public final class RemovalNotification<K, V> implements Map.Entry<K, V> {

private final K key;
private final V value;
@Nullable
@Nonnull
private final RemovalCause cause;

/**
Expand Down Expand Up @@ -54,6 +54,10 @@ private RemovalNotification( K key, V value, @Nonnull RemovalCause cause) {
// }
// --Commented out by Inspection STOP (11/29/16, 5:04 PM)

@Nonnull public RemovalCause getCause() {
return cause;
}

@Override public K getKey() {
return key;
}
Expand Down
4 changes: 3 additions & 1 deletion store/api/store.api
Original file line number Diff line number Diff line change
Expand Up @@ -63,7 +63,7 @@ public final class com/dropbox/android/external/store4/FetcherResult$Error$Messa
public final class com/dropbox/android/external/store4/MemoryPolicy {
public static final field Companion Lcom/dropbox/android/external/store4/MemoryPolicy$Companion;
public static final field DEFAULT_SIZE_POLICY J
public synthetic fun <init> (JJJJLcom/dropbox/android/external/store4/Weigher;Lkotlin/jvm/internal/DefaultConstructorMarker;)V
public synthetic fun <init> (JJJJLcom/dropbox/android/external/store4/Weigher;Lkotlin/jvm/functions/Function3;Lkotlin/jvm/internal/DefaultConstructorMarker;)V
public final fun getExpireAfterAccess-UwyO8pc ()J
public final fun getExpireAfterWrite-UwyO8pc ()J
public final fun getHasAccessPolicy ()Z
Expand All @@ -72,6 +72,7 @@ public final class com/dropbox/android/external/store4/MemoryPolicy {
public final fun getHasWritePolicy ()Z
public final fun getMaxSize ()J
public final fun getMaxWeight ()J
public final fun getRemovalListener ()Lkotlin/jvm/functions/Function3;
public final fun getWeigher ()Lcom/dropbox/android/external/store4/Weigher;
public final fun isDefaultWritePolicy ()Z
}
Expand All @@ -87,6 +88,7 @@ public final class com/dropbox/android/external/store4/MemoryPolicy$MemoryPolicy
public final fun setExpireAfterAccess-LRDsOJo (J)Lcom/dropbox/android/external/store4/MemoryPolicy$MemoryPolicyBuilder;
public final fun setExpireAfterWrite-LRDsOJo (J)Lcom/dropbox/android/external/store4/MemoryPolicy$MemoryPolicyBuilder;
public final fun setMaxSize (J)Lcom/dropbox/android/external/store4/MemoryPolicy$MemoryPolicyBuilder;
public final fun setRemovalListener (Lkotlin/jvm/functions/Function3;)Lcom/dropbox/android/external/store4/MemoryPolicy$MemoryPolicyBuilder;
public final fun setWeigherAndMaxWeight (Lcom/dropbox/android/external/store4/Weigher;J)Lcom/dropbox/android/external/store4/MemoryPolicy$MemoryPolicyBuilder;
}

Expand Down
2 changes: 1 addition & 1 deletion store/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ plugins {
}

dependencies {
implementation libraries.cache
implementation project(path: ':cache')
implementation project(path: ':multicast')
implementation libraries.coroutinesCore

Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
package com.dropbox.android.external.store4

import com.dropbox.android.external.cache3.RemovalCause
import kotlin.time.Duration
import kotlin.time.ExperimentalTime

Expand All @@ -17,6 +18,8 @@ internal object OneWeigher : Weigher<Any, Any> {
override fun weigh(key: Any, value: Any): Int = 1
}

typealias RemovalFunction<Key, Value> = (Key, Value, RemovalCause) -> Unit

/**
* MemoryPolicy holds all required info to create MemoryCache
*
Expand All @@ -34,7 +37,8 @@ class MemoryPolicy<in Key : Any, in Value : Any> internal constructor(
val expireAfterAccess: Duration,
val maxSize: Long,
val maxWeight: Long,
val weigher: Weigher<Key, Value>
val weigher: Weigher<Key, Value>,
val removalListener: ((Key, Value, RemovalCause) -> Unit)?
) {

val isDefaultWritePolicy: Boolean = expireAfterWrite == DEFAULT_DURATION_POLICY
Expand All @@ -53,6 +57,7 @@ class MemoryPolicy<in Key : Any, in Value : Any> internal constructor(
private var maxSize: Long = DEFAULT_SIZE_POLICY
private var maxWeight: Long = DEFAULT_SIZE_POLICY
private var weigher: Weigher<Key, Value> = OneWeigher
private var removalListener: ((Key, Value, RemovalCause) -> Unit)? = null

fun setExpireAfterWrite(expireAfterWrite: Duration): MemoryPolicyBuilder<Key, Value> =
apply {
Expand Down Expand Up @@ -97,12 +102,25 @@ class MemoryPolicy<in Key : Any, in Value : Any> internal constructor(
this.maxWeight = maxWeight
}

/**
* Specifies a listener instance that caches should notify each time an entry is removed for
* any [com.nytimes.android.external.cache3.RemovalCause]. Each cache created by this builder
* will invoke this listener as part of the routine maintenance described in the
* class documentation above.
*/
fun setRemovalListener(
removalListener: (Key, Value, RemovalCause) -> Unit
): MemoryPolicyBuilder<Key, Value> = apply {
this.removalListener = removalListener
}

fun build() = MemoryPolicy<Key, Value>(
expireAfterWrite = expireAfterWrite,
expireAfterAccess = expireAfterAccess,
maxSize = maxSize,
maxWeight = maxWeight,
weigher = weigher
weigher = weigher,
removalListener = removalListener,
)
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,26 +15,16 @@
*/
package com.dropbox.android.external.store4.impl

import com.dropbox.android.external.store4.CacheType
import com.dropbox.android.external.store4.ExperimentalStoreApi
import com.dropbox.android.external.store4.Fetcher
import com.dropbox.android.external.store4.MemoryPolicy
import com.dropbox.android.external.store4.ResponseOrigin
import com.dropbox.android.external.store4.SourceOfTruth
import com.dropbox.android.external.store4.Store
import com.dropbox.android.external.store4.StoreRequest
import com.dropbox.android.external.store4.StoreResponse
import com.dropbox.android.external.cache3.CacheBuilder
import com.dropbox.android.external.cache3.RemovalListener
import com.dropbox.android.external.store4.*
import com.dropbox.android.external.store4.impl.operators.Either
import com.dropbox.android.external.store4.impl.operators.merge
import com.nytimes.android.external.cache3.CacheBuilder
import com.dropbox.android.external.store4.utils.listenerScoped
import kotlinx.coroutines.CompletableDeferred
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.emitAll
import kotlinx.coroutines.flow.flow
import kotlinx.coroutines.flow.onEach
import kotlinx.coroutines.flow.onStart
import kotlinx.coroutines.flow.transform
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.flow.*
import java.util.concurrent.TimeUnit
import kotlin.time.ExperimentalTime

Expand All @@ -57,6 +47,7 @@ internal class RealStore<Key : Any, Input : Any, Output : Any>(
SourceOfTruthWithBarrier(it)
}

@OptIn(ExperimentalCoroutinesApi::class)
private val memCache = memoryPolicy?.let {
CacheBuilder.newBuilder().apply {
if (memoryPolicy.hasAccessPolicy) {
Expand All @@ -79,6 +70,13 @@ internal class RealStore<Key : Any, Input : Any, Output : Any>(
maximumWeight(memoryPolicy.maxWeight)
weigher { k: Key, v: Output -> memoryPolicy.weigher.weigh(k, v) }
}
memoryPolicy.removalListener?.let { listener ->
listener.listenerScoped(scope) { callback ->
removalListener(RemovalListener<Key, Output> {
callback?.invoke(it.key, it.value, it.cause)
})
}
}
}.build<Key, Output>()
}

Expand Down Expand Up @@ -276,4 +274,4 @@ internal class RealStore<Key : Any, Input : Any, Output : Any>(
}
}
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
package com.dropbox.android.external.store4.utils

import kotlinx.coroutines.CancellationException
import kotlinx.coroutines.CoroutineExceptionHandler
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.launch

inline fun <T> T.listenerScoped(
coroutineScope: CoroutineScope,
crossinline register: (T?) -> Unit,
) {
var weakListener: T? = this
coroutineScope.launch(
CoroutineExceptionHandler { _, throwable ->
if (throwable is CancellationException) {
weakListener = null
}
}
) {
register.invoke(weakListener)
}
}
Original file line number Diff line number Diff line change
@@ -1,18 +1,21 @@
package com.dropbox.android.external.store4.impl

import com.dropbox.android.external.cache3.RemovalCause
import com.dropbox.android.external.store4.Fetcher
import com.dropbox.android.external.store4.MemoryPolicy
import com.dropbox.android.external.store4.StoreBuilder
import com.dropbox.android.external.store4.get
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.FlowPreview
import kotlinx.coroutines.runBlocking
import com.nhaarman.mockitokotlin2.mock
import com.nhaarman.mockitokotlin2.never
import com.nhaarman.mockitokotlin2.verify
import kotlinx.coroutines.*
import org.junit.Test
import org.junit.runner.RunWith
import org.junit.runners.JUnit4
import kotlin.time.ExperimentalTime
import kotlin.time.minutes


@FlowPreview
@ExperimentalTime
@ExperimentalCoroutinesApi
Expand All @@ -39,4 +42,61 @@ class StoreWithInMemoryCacheTest {
store.get(2)
}
}
}

@Test
fun `store requests with removal listener when its in-memory cache is at the maximum size`() {
val listenerMock = mock<(Int, String, RemovalCause) -> Unit>()
val scope = CoroutineScope(Job())

val store = StoreBuilder
.from(Fetcher.of { key: Int -> "result_$key" })
.scope(scope)
.cachePolicy(
MemoryPolicy
.builder<Int, String>()
.setMaxSize(1)
.setRemovalListener(listenerMock)
.build()
)
.build()

runBlocking {
store.get(0)
store.get(1)
store.get(2)

verify(listenerMock).invoke(0, "result_0", RemovalCause.SIZE)
verify(listenerMock).invoke(1, "result_1", RemovalCause.SIZE)
}
}

@Test
fun `store requests with removal listener when scope is cancel`() {
val listenerMock = mock<(Int, String, RemovalCause) -> Unit>()
val scope = CoroutineScope(Job())

val store = StoreBuilder
.from(Fetcher.of { key: Int -> "result_$key" })
.scope(scope)
.cachePolicy(
MemoryPolicy
.builder<Int, String>()
.setMaxSize(1)
.setRemovalListener(listenerMock)
.build()
)
.build()

runBlocking {
store.get(0)
store.get(1)

scope.cancel()
// TODO We must call store.get(2) to verify that the listener is not call
// but with the cancelled scope the methode get throw the JobCancellationException

verify(listenerMock).invoke(0, "result_0", RemovalCause.SIZE)
verify(listenerMock, never()).invoke(1, "result_1", RemovalCause.SIZE)
}
}
}