-
Notifications
You must be signed in to change notification settings - Fork 1.1k
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
Appropriate way to implement an error handling interceptor on suspend functions #8891
Comments
I have tried many different solutions to no avail including the following:
try {
val result = interceptedMethod.handleResult(interceptedMethod.interceptResultAsCompletionStage())
return result
catch (ex: Throwable) {
print("caught exception")
} still Of course, I would much prefer not to avoid the Do I need to restructure my error handling to support these errors being produced in |
Does anyone have any idea on how to proceed for this? |
Kotlin coroutine needs to be intercepted as completion stage, see https://github.com/micronaut-projects/micronaut-core/blob/4.0.x/context/src/main/java/io/micronaut/scheduling/async/AsyncInterceptor.java |
I have attempted to implement the interceptor in this style multiple times but still get the same result. I've updated the linked example repo with a boiled down reproduction of this issue and copied the interceptor code here for convenience: package live.bunch.interceptor
import io.micronaut.aop.InterceptedMethod
import io.micronaut.aop.InterceptorBean
import io.micronaut.aop.MethodInterceptor
import io.micronaut.aop.MethodInvocationContext
import jakarta.inject.Singleton
import java.util.concurrent.CompletableFuture
import java.util.concurrent.Executors
@Singleton
@InterceptorBean(FaultyInterceptorAnnotation::class)
class FaultyInterceptor() : MethodInterceptor<Any?, Any?> {
override fun intercept(context: MethodInvocationContext<Any?, Any?>): Any? {
val executorService = Executors.newSingleThreadExecutor()
if (context.isSuspend) {
val interceptedMethod = InterceptedMethod.of(context)
println("Attempting to intercept method ${context.methodName}")
try {
val res = interceptedMethod.handleResult(
CompletableFuture.supplyAsync(
{ interceptedMethod.interceptResultAsCompletionStage() },
executorService
)
)
println("Proceeded with context and got result $res")
return res
} catch (ex: Throwable) {
println("EXPECTED PRINT - Encountered exception attempting on context.proceed()")
ex.printStackTrace()
throw ex
}
} else {
println("Method ${context.methodName} is not a suspend function")
return context.proceed()
}
}
} When using this interceptor, no exception is caught (i.e. |
you might want to look at micronaut-core/retry/src/main/java/io/micronaut/retry/intercept/RecoveryInterceptor.java Line 86 in 5113c33
|
The res should be completable future and it will have the exception propagated in it |
I'm still taking a look at using the But yes, the The problem is that since this isn't a |
Updating the try catch logic within that conditional like so to hopefully give some insight into what is happening with the completable: val interceptedMethod = InterceptedMethod.of(context)
println("Attempting to intercept method ${context.methodName}")
try {
val res = interceptedMethod.handleResult(
CompletableFuture.supplyAsync(
{
interceptedMethod.interceptResultAsCompletionStage()
.exceptionally { ex ->
println("intercepted error: $ex")
return@exceptionally null
}
},
executorService
)
)
println("Proceeded with context and got result $res")
return res
} catch (ex: Throwable) {
println("EXPECTED PRINT - Encountered exception attempting on context.proceed()")
ex.printStackTrace()
throw ex
} This is what's printed:
I think I am expecting |
Sorry, I have misled you. |
If I understand correctly, that is the intention behind @Override
public Object handleResult(Object result) {
CompletionStage completionStageResult;
if (result instanceof CompletionStage) {
completionStageResult = (CompletionStage<?>) result;
} else {
throw new IllegalStateException("Cannot convert " + result + " to 'java.util.concurrent.CompletionStage'");
}
return KotlinInterceptedMethodHelper.handleResult(completionStageResult, isUnitValueType, continuation);
} that helper method eventually calls While it is suspended, a value is being returned. Are you suggesting I wait on the future returned from |
Waiting on the future explicitly actually causes execution to hang: try {
return interceptedMethod.interceptResultAsCompletionStage()
.whenComplete { result, ex ->
if (result != null) {
println("intercepted result: $result")
}
if (ex != null) {
println("intercepted error: $ex")
}
println("intercepted result: $result, ex: $ex")
}
.toCompletableFuture()
.get()
} catch (ex: Throwable) {
println("EXPECTED PRINT - Encountered exception attempting on context.proceed()")
ex.printStackTrace()
throw ex
} Only |
Are there any other suggestions w.r.t. this issue? None of these strategies seem to work. There is an additional piece of context which can help understand why this does not work: We are using [grpc-kotlin] and we are facing this issue when implementing a The problem seems to be that even when including the exception handling and mapping and other tasks in the |
Don't block the future, pass it to
|
Awesome, thank you very much for this. Solved with the following usage: val interceptedMethod = InterceptedMethod.of(context)
if (context.isSuspend) {
val resultFuture = CompletableFuture<Any?>()
// ...
try {
interceptedMethod.interceptResultAsCompletionStage()
.whenComplete { res, ex ->
if (ex == null) {
resultFuture.complete(res)
} else {
// Handle errors that occur when the intercepted method suspends ...
resultFuture.completeExceptionally(ex)
}
}
} catch (ex: Throwable) {
// Handle errors that occur when the intercepted method never suspends ...
resultFuture.completeExceptionally(ex)
}
// ...
return interceptedMethod.handleResult(resultFuture)
} |
Expected Behavior
I am trying to implement an AOP interceptor that intercepts kotlin suspend functions and appropriately handles / transforms errors for other interceptors and logic (e.g. gRPC).
With the below interceptor and example implementation using that interceptor, I am unable to get
context.proceed()
to bubble up the error that is thrown inside the suspend function implementation. Instead an internalCOROUTINE_SUSPENDED
value is returned and from what I can tell, the exception is thrown later when kotlin decides to unsuspend the coroutine however it is still never caught by any try catch or other error handlers in the interceptor.Given the interceptor + implementation below - I expect the print statement in the
catch {}
block to be printed along with others in the following sequence:Actual Behaviour
The order that actually gets printed is:
Note that the expected
print
statement in the interceptorcatch {}
block is never executed.Below are brief interceptor and example implementations - I have also provided an example repository. I got this output from running the one test there.
Interceptor:
Example implementation
Steps To Reproduce
testItWorks
Environment Information
Example Application
https://github.com/SolomanDavis/micronaut-interceptor-suspend
Version
3.6.2 -> 3.8.6
The text was updated successfully, but these errors were encountered: