Skip to content

Commit fae6a0b

Browse files
authored
Add ActionScopeListener (#3668)
`ActionScopeListener` allows developers to hook in to the `close()` method of `ActionScope`, and perform actions immediately before the scope is closed. For example, you could buffer logs while the scope is open, and then dedupe and flush them before it's closed. Currently `ActionScopeListener` only supports `onClose()`, but in the future could support `onEnter()` as well.
1 parent f53befc commit fae6a0b

7 files changed

Lines changed: 78 additions & 5 deletions

File tree

misk-action-scopes/api/misk-action-scopes.api

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,10 @@ public final class misk/scope/ActionScope$Instance : java/lang/AutoCloseable {
4242
public final fun inScope (Lkotlin/jvm/functions/Function0;)Ljava/lang/Object;
4343
}
4444

45+
public abstract interface class misk/scope/ActionScopeListener {
46+
public abstract fun onClose ()V
47+
}
48+
4549
public abstract class misk/scope/ActionScopedProviderModule : misk/inject/KAbstractModule {
4650
public static final field Companion Lmisk/scope/ActionScopedProviderModule$Companion;
4751
public fun <init> ()V
@@ -51,6 +55,7 @@ public abstract class misk/scope/ActionScopedProviderModule : misk/inject/KAbstr
5155
public final fun bindConstant (Lkotlin/reflect/KClass;Ljava/lang/Object;Ljava/lang/annotation/Annotation;)V
5256
public static synthetic fun bindConstant$default (Lmisk/scope/ActionScopedProviderModule;Lcom/google/inject/TypeLiteral;Ljava/lang/Object;Ljava/lang/annotation/Annotation;ILjava/lang/Object;)V
5357
public static synthetic fun bindConstant$default (Lmisk/scope/ActionScopedProviderModule;Lkotlin/reflect/KClass;Ljava/lang/Object;Ljava/lang/annotation/Annotation;ILjava/lang/Object;)V
58+
public final fun bindListener (Lcom/google/inject/TypeLiteral;)V
5459
public final fun bindProvider (Lcom/google/inject/TypeLiteral;Lkotlin/reflect/KClass;)V
5560
public final fun bindProvider (Lcom/google/inject/TypeLiteral;Lkotlin/reflect/KClass;Ljava/lang/Class;)V
5661
public final fun bindProvider (Lcom/google/inject/TypeLiteral;Lkotlin/reflect/KClass;Ljava/lang/annotation/Annotation;)V

misk-action-scopes/src/main/kotlin/misk/scope/ActionScope.kt

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,8 @@ internal constructor(
2323
// on ActionScopedProviders, which might depend on other ActionScopeds. We break
2424
// this circular dependency by injecting a map of Provider<ActionScopedProvider>
2525
// rather than the map of ActionScopedProvider directly
26-
private val providers: @JvmSuppressWildcards Map<Key<*>, Provider<ActionScopedProvider<*>>>
26+
private val providers: @JvmSuppressWildcards Map<Key<*>, Provider<ActionScopedProvider<*>>>,
27+
private val listeners: @JvmSuppressWildcards Provider<Set<ActionScopeListener>>,
2728
) : AutoCloseable {
2829
companion object {
2930
private val threadLocalInstance = ThreadLocal<Instance>()
@@ -123,7 +124,11 @@ internal constructor(
123124
}
124125

125126
override fun close() {
126-
threadLocalInstance.remove()
127+
try {
128+
listeners.get().forEach { it.onClose() }
129+
} finally {
130+
threadLocalInstance.remove()
131+
}
127132

128133
// Explicitly NOT removing threadLocalUUID because we want to retain the thread's UUID if
129134
// the action scope is re-entered on the same thread.
Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
package misk.scope
2+
3+
/**
4+
* A listener that can be called at various points within an [ActionScope]'s lifecycle. See the comment on each method
5+
* to understand when in the lifecycle it is called.
6+
*/
7+
interface ActionScopeListener {
8+
/**
9+
* Called during [ActionScope.close], immediately before the [ThreadLocal] that holds the [ActionScope.Instance] is
10+
* removed.
11+
*
12+
* The [ActionScope] is not closed until all the listeners are executed. That means that:
13+
* - Immediately before [onClose] is called, [ActionScope.inScope] returns true
14+
* - During [onClose], [ActionScope.inScope] returns true
15+
* - Immediately after all listeners have called [onClose], [ActionScope.inScope] returns false
16+
*
17+
* The [ActionScope] being closed or not is independent of the lifecycle of any action scoped values. A reference to
18+
* an object provided by an [ActionScopedProvider] can be held and used even if [ActionScope.inScope] returns false.
19+
* For example: an action-scoped HTTP Request Body may still be transmitting to the client despite the scope being
20+
* closed.
21+
*/
22+
fun onClose()
23+
}

misk-action-scopes/src/main/kotlin/misk/scope/ActionScopedProviderModule.kt

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ abstract class ActionScopedProviderModule : KAbstractModule() {
2121
override fun configure() {
2222
MapBinder.newMapBinder(binder(), KEY_TYPE, ACTION_SCOPED_PROVIDER_TYPE)
2323
Multibinder.newSetBinder(binder(), KEY_TYPE)
24+
Multibinder.newSetBinder(binder(), ACTION_SCOPE_LISTENER_TYPE)
2425
configureProviders()
2526
}
2627

@@ -146,6 +147,14 @@ abstract class ActionScopedProviderModule : KAbstractModule() {
146147
bindProvider(typeKey, actionScopedKey, binder().getProvider(providerType.java))
147148
}
148149

150+
inline fun <reified T : ActionScopeListener> bindListener() {
151+
bindListener(object : TypeLiteral<T>() {})
152+
}
153+
154+
fun bindListener(typeLiteral: TypeLiteral<out ActionScopeListener>) {
155+
Multibinder.newSetBinder(binder(), ACTION_SCOPE_LISTENER_TYPE).addBinding().to(typeLiteral)
156+
}
157+
149158
private fun <T> bindProvider(
150159
key: Key<T>,
151160
actionScopedKey: Key<ActionScoped<T>>,
@@ -190,6 +199,7 @@ abstract class ActionScopedProviderModule : KAbstractModule() {
190199

191200
private val KEY_TYPE = object : TypeLiteral<Key<*>>() {}
192201
private val ACTION_SCOPED_PROVIDER_TYPE = object : TypeLiteral<ActionScopedProvider<*>>() {}
202+
private val ACTION_SCOPE_LISTENER_TYPE = object : TypeLiteral<ActionScopeListener>() {}
193203

194204
private class SeedDataActionScopedProvider<out T>(private val key: Key<T>) : ActionScopedProvider<T> {
195205
override fun get(): T {

misk-action-scopes/src/test/kotlin/misk/scope/ActionScopedTest.kt

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ import kotlinx.coroutines.runBlocking
1313
import misk.inject.keyOf
1414
import misk.inject.toKey
1515
import misk.inject.uninject
16+
import misk.scope.TestActionScopedProviderModule.TestListener
1617
import org.assertj.core.api.Assertions.assertThat
1718
import org.junit.jupiter.api.BeforeEach
1819
import org.junit.jupiter.api.Test
@@ -36,6 +37,8 @@ internal class ActionScopedTest {
3637

3738
@Inject private lateinit var scope: ActionScope
3839

40+
@Inject private lateinit var testListener: TestListener
41+
3942
@BeforeEach
4043
fun clearInjections() {
4144
uninject(this)
@@ -277,4 +280,19 @@ internal class ActionScopedTest {
277280
assertThat(countingString.get()).isEqualTo("Called CountingProvider 1 time(s)")
278281
}
279282
}
283+
284+
@Test
285+
fun `listeners are called before the scope closes`() {
286+
val injector = Guice.createInjector(TestActionScopedProviderModule())
287+
injector.injectMembers(this)
288+
289+
assertThat(testListener.result).isNull()
290+
291+
scope.create(mapOf()).inScope {
292+
// We don't have to do anything in the scope, just call inScope() so that close() is called. The listener is
293+
// then triggered, setting the result field to an action scoped value, to show we're still in an action scope.
294+
}
295+
296+
assertThat(testListener.result).isEqualTo("constant-value")
297+
}
280298
}

misk-action-scopes/src/test/kotlin/misk/scope/TestActionScopedProviderModule.kt

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import com.google.inject.TypeLiteral
44
import com.google.inject.name.Named
55
import com.google.inject.name.Names
66
import jakarta.inject.Inject
7+
import jakarta.inject.Singleton
78
import java.util.Optional
89

910
internal class TestActionScopedProviderModule : ActionScopedProviderModule() {
@@ -19,6 +20,7 @@ internal class TestActionScopedProviderModule : ActionScopedProviderModule() {
1920
bindProvider(nullableStringTypeLiteral, NullableFooProvider::class, Names.named("nullable-foo"))
2021
bindProvider(nullableStringTypeLiteral, NullableBasedOnFooProvider::class, Names.named("nullable-based-on-foo"))
2122
bindProvider(String::class, CountingProvider::class, Names.named(("counting")))
23+
bindListener<TestListener>()
2224
}
2325

2426
class BarProvider @Inject internal constructor(@Named("from-seed") private val seedData: ActionScoped<String>) :
@@ -75,6 +77,16 @@ internal class TestActionScopedProviderModule : ActionScopedProviderModule() {
7577
override fun get(): String = "Called CountingProvider ${++callCount} time(s)"
7678
}
7779

80+
@Singleton
81+
class TestListener @Inject constructor(@Named("constant") private val constant: ActionScoped<String>) :
82+
ActionScopeListener {
83+
var result: String? = null
84+
85+
override fun onClose() {
86+
result = constant.get()
87+
}
88+
}
89+
7890
companion object {
7991
val nullableStringTypeLiteral = object : TypeLiteral<String?>() {}
8092
}

misk-inject/src/main/kotlin/misk/inject/Guice.kt

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -110,9 +110,9 @@ inline fun <reified T : Any> keyOf(a: KClass<out Annotation>?): Key<T> = keyOf(a
110110
*/
111111
inline fun <reified T : Any> keyOf(qualifier: BindingQualifier? = null): Key<T> =
112112
when (qualifier) {
113-
is BindingQualifier.InstanceQualifier -> Key.get(T::class.java, qualifier.annotation)
114-
is BindingQualifier.TypeClassifier -> Key.get(T::class.java, qualifier.type.java)
115-
null -> Key.get(T::class.java)
113+
is BindingQualifier.InstanceQualifier -> Key.get(object : TypeLiteral<T>() {}, qualifier.annotation)
114+
is BindingQualifier.TypeClassifier -> Key.get(object : TypeLiteral<T>() {}, qualifier.type.java)
115+
null -> Key.get(object : TypeLiteral<T>() {})
116116
}
117117

118118
/**

0 commit comments

Comments
 (0)