Implementing AroundInvoke Interceptors for suspend functions

I’m working on a Quarkus Kotlin project where we are using coroutines almost exclusively, but one challenge I’ve run into is working out how to correctly implement AOP interceptors when the methods we are intercepting are suspend functions, and the activity we want to do “around” the call is also suspending, but our access to the invocation context is at the Java reflection level. While Quarkus has some Kotlin/coroutine support in filters, there is currently no direct support for interceptors.

I have an approach that appears to work but I’m not confident that this is the correct approach and worry it’s going to break in some subtle way. So I’d welcome some critique of this code and suggestions on how to do this better. In particular the direct use of COROUTINE_SUSPENDED worries me here.

@InterceptorBinding
@Retention(AnnotationRetention.RUNTIME)
@Target(AnnotationTarget.CLASS, AnnotationTarget.FUNCTION, AnnotationTarget.CONSTRUCTOR)
annotation class MyInterceptor

@MyInterceptor
@Interceptor
@Priority(1)
class CoroutineInterceptor {
    @AroundInvoke
    fun intercept(context: InvocationContext): Any? {
        val continuation = context.parameters.find { it is Continuation<*> } as Continuation<*>?

        return if (continuation != null) {
            // launch a job to do our "around" work and then complete the invocation on the callee
            // we use the context from the continuation so *should* share the same job/scheduling?
            CoroutineScope(continuation.context).launch {
                somethingAsync()
                context.proceed()
            }
            // return this special value to the caller to say we are suspending
            kotlin.coroutines.intrinsics.COROUTINE_SUSPENDED
        } else {
            // no-op if not a suspend function
            context.proceed()
        }
    }

    suspend fun somethingAsync(): Unit {
      TODO()
    }
}
1 Like

For anyone curious about whether the above really works - no it does not, not really. It will leak if there is any cleanup needed after the method invocation because we don’t hook into the caller’s continuation.

I have actually got something that works but it’s quite messy and I want to clean it up a bit and check my assumptions before putting any more wrongness on the internet.

I now have a more thoroughly worked out Interceptor example that deals with actions before resuming continuations at https://github.com/allertonm/quarkus-coroutine-interceptor-example/blob/main/src/main/kotlin/com/example/Interceptors.kt

(I also discovered a Quarkus bug in the course of writing this AroundInvoke Interceptors can only modify parameters if first in chain · Issue #34001 · quarkusio/quarkus · GitHub)

I’m facing a lot of issues using CDI Interceptors with Kotlin Coroutines on suspend functions and interceptor chain. For example when the ic.proceed() runs a suspend function in chain.

Would be nice if the CDI spec consider new approaches or even the Quarkus Arc implementation supported the Coroutine Passing Style in a proper way.