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

fix fallback for suspended methods #8825

Merged
Merged
Show file tree
Hide file tree
Changes from 1 commit
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
Original file line number Diff line number Diff line change
Expand Up @@ -78,15 +78,25 @@ public Object intercept(MethodInvocationContext<Object, Object> context) {
}
InterceptedMethod interceptedMethod = InterceptedMethod.of(context);
try {
if (context.isSuspend()) {

}

graemerocher marked this conversation as resolved.
Show resolved Hide resolved
switch (interceptedMethod.resultType()) {
case PUBLISHER:
return interceptedMethod.handleResult(
fallbackForReactiveType(context, interceptedMethod.interceptResultAsPublisher())
);
case COMPLETION_STAGE:
return interceptedMethod.handleResult(
if (context.isSuspend()) {
return interceptedMethod.handleResult(
fallbackForSuspend(context, interceptedMethod.interceptResultAsCompletionStage())
);
} else {
return interceptedMethod.handleResult(
fallbackForFuture(context, interceptedMethod.interceptResultAsCompletionStage())
);
);
}
case SYNCHRONOUS:
try {
return context.proceed();
Expand Down Expand Up @@ -193,6 +203,32 @@ private CompletionStage<?> fallbackForFuture(MethodInvocationContext<Object, Obj
return newFuture;
}

private CompletionStage<?> fallbackForSuspend(MethodInvocationContext<Object, Object> context, CompletionStage<?> result) {
CompletableFuture<Object> newFuture = new CompletableFuture<>();
result.whenComplete((o, throwable) -> {
if (throwable == null) {
newFuture.complete(o);
} else {
Optional<? extends MethodExecutionHandle<?, Object>> fallbackMethod = findFallbackMethod(context);
if (fallbackMethod.isPresent()) {
MethodExecutionHandle<?, Object> fallbackHandle = fallbackMethod.get();
if (LOG.isDebugEnabled()) {
LOG.debug("Type [{}] resolved fallback: {}", context.getTarget().getClass(), fallbackHandle);
}
try {
newFuture.complete(fallbackHandle.invoke(context.getParameterValues()));
} catch (Throwable t) {
newFuture.completeExceptionally(t);
}
} else {
newFuture.completeExceptionally(throwable);
}
}
});

return newFuture;
}

/**
* Resolves a fallback for the given execution context and exception.
*
Expand Down
183 changes: 183 additions & 0 deletions test-suite-kotlin/src/test/kotlin/io/micronaut/retry/FallbackSpec.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,183 @@
package io.micronaut.retry

import io.micronaut.context.ApplicationContext
import io.micronaut.context.annotation.Requires
import io.micronaut.http.HttpResponse
import io.micronaut.http.HttpStatus
import io.micronaut.http.annotation.Controller
import io.micronaut.http.annotation.Post
import io.micronaut.http.client.annotation.Client
import io.micronaut.retry.annotation.Fallback
import io.micronaut.retry.annotation.Recoverable
import io.micronaut.runtime.server.EmbeddedServer
import kotlinx.coroutines.runBlocking
import org.junit.jupiter.api.AfterEach
import org.junit.jupiter.api.BeforeEach
import org.junit.jupiter.api.Test
import org.junit.jupiter.api.assertThrows
import kotlin.test.assertEquals
import kotlin.test.assertNull

class FallbackSpec {

lateinit var server: EmbeddedServer
lateinit var fallbackClient: FallbackClient

@BeforeEach
fun setUp() {
server = ApplicationContext.run(EmbeddedServer::class.java, mapOf("spec.name" to "FallbackClientSpec"))
val context = server.applicationContext
fallbackClient = context.getBean(FallbackClient::class.java)
}

@AfterEach
fun tearDown() {
server.close()
}

@Test
fun `server ok with string output`() {
runBlocking {
val response = fallbackClient.stringOutput(false, false)
assertEquals("server ok", response)
}
}

@Test
fun `server ok with HttpResponse output`() {
runBlocking {
val response = fallbackClient.httpResponseOutput(false, false)
assertEquals(HttpStatus.OK, response.status)
assertEquals("server ok", response.body())
}
}

@Test
fun `server ok with null`() {
runBlocking {
val response = fallbackClient.nullOutput(false, false)
assertNull(response)
}
}

@Test
fun `server fail with string output`() {
runBlocking {
val response = fallbackClient.stringOutput(true, false)
assertEquals("fallback ok", response)
}
}

@Test
fun `server fail with HttpResponse output`() {
runBlocking {
val response = fallbackClient.httpResponseOutput(true, false)
assertEquals(HttpStatus.OK, response.status)
assertEquals("fallback ok", response.body())
}
}

@Test
fun `server fail with null`() {
runBlocking {
val response = fallbackClient.nullOutput(true, false)
assertNull(response)
}
}

@Test
fun `faillback fail with string output`() {
runBlocking {
val exception = assertThrows<RuntimeException> { fallbackClient.stringOutput(true, true) }
assertEquals("fallback fail", exception.message)
}
}

@Test
fun `faillback fail with HttpResponse output`() {
runBlocking {
val exception = assertThrows<RuntimeException> { fallbackClient.httpResponseOutput(true, true) }
assertEquals("fallback fail", exception.message)
}
}

@Test
fun `faillback fail with null`() {
runBlocking {
val exception = assertThrows<RuntimeException> { fallbackClient.nullOutput(true, true) }
assertEquals("fallback fail", exception.message)
}
}


}


@Requires(property = "spec.name", value = "FallbackClientSpec")
@Controller("/fallback")
class FallbackClientController {

@Post("stringOutput")
fun stringOutput(serverFail: Boolean, fallbackFail: Boolean): HttpResponse<String> {
return httpResponseOutput(serverFail, fallbackFail)
}

@Post("httpResponseOutput")
fun httpResponseOutput(serverFail: Boolean, fallbackFail: Boolean): HttpResponse<String> {
return if (serverFail) {
HttpResponse.serverError("server fail")
} else {
HttpResponse.ok("server ok")
}
}

@Post("nullOutput")
fun nullOutput(serverFail: Boolean, fallbackFail: Boolean): HttpResponse<String> {
return if (serverFail) {
HttpResponse.serverError()
} else {
HttpResponse.ok()
}
}
}

@Client("/fallback")
@Recoverable(api = FallbackClientFallback::class)
interface FallbackClient {

@Post("stringOutput")
suspend fun stringOutput(serverFail: Boolean, fallbackFail: Boolean): String

@Post("httpResponseOutput")
suspend fun httpResponseOutput(serverFail: Boolean, fallbackFail: Boolean): HttpResponse<String>

@Post("nullOutput")
suspend fun nullOutput(serverFail: Boolean, fallbackFail: Boolean): String?
}

@Fallback
open class FallbackClientFallback : FallbackClient {
override suspend fun stringOutput(serverFail: Boolean, fallbackFail: Boolean): String {
return if (fallbackFail) {
throw RuntimeException("fallback fail")
} else {
"fallback ok"
}
}

override suspend fun httpResponseOutput(serverFail: Boolean, fallbackFail: Boolean): HttpResponse<String> {
return if (fallbackFail) {
throw RuntimeException("fallback fail")
} else {
HttpResponse.ok("fallback ok")
}
}

override suspend fun nullOutput(serverFail: Boolean, fallbackFail: Boolean): String? {
return if (fallbackFail) {
throw RuntimeException("fallback fail")
} else {
null
}
}
}