Merge pull request #307 from solonovamax/fix/commands-completing-before-execution

Fix cloud swallowing exceptions in `suspend` methods.
This commit is contained in:
solo 2021-10-04 08:14:58 -04:00 committed by Jason
parent ad80933a20
commit 8711d19703
3 changed files with 55 additions and 37 deletions

View file

@ -89,6 +89,8 @@ public final class AsynchronousCommandExecutionCoordinator<C> extends CommandExe
resultFuture.completeExceptionally(new CommandExecutionException(throwable, commandContext)); resultFuture.completeExceptionally(new CommandExecutionException(throwable, commandContext));
} }
} }
// Only complete when the execution is actually finished. See #306 for more info.
resultFuture.complete(new CommandResult<>(commandContext));
}); });
} }
}; };
@ -97,30 +99,25 @@ public final class AsynchronousCommandExecutionCoordinator<C> extends CommandExe
final @NonNull Pair<@Nullable Command<C>, @Nullable Exception> pair = final @NonNull Pair<@Nullable Command<C>, @Nullable Exception> pair =
this.getCommandTree().parse(commandContext, input); this.getCommandTree().parse(commandContext, input);
if (pair.getSecond() != null) { if (pair.getSecond() != null) {
final CompletableFuture<CommandResult<C>> future = new CompletableFuture<>(); resultFuture.completeExceptionally(pair.getSecond());
future.completeExceptionally(pair.getSecond()); } else {
return future; this.executor.execute(() -> commandConsumer.accept(pair.getFirst()));
} }
return CompletableFuture.supplyAsync(() -> { } else {
commandConsumer.accept(pair.getFirst()); this.executor.execute(() -> {
return new CommandResult<>(commandContext); try {
}, this.executor); final @NonNull Pair<@Nullable Command<C>, @Nullable Exception> pair =
} this.getCommandTree().parse(commandContext, input);
if (pair.getSecond() != null) {
this.executor.execute(() -> { resultFuture.completeExceptionally(pair.getSecond());
try { } else {
final @NonNull Pair<@Nullable Command<C>, @Nullable Exception> pair = commandConsumer.accept(pair.getFirst());
this.getCommandTree().parse(commandContext, input); }
if (pair.getSecond() != null) { } catch (final Exception e) {
resultFuture.completeExceptionally(pair.getSecond()); resultFuture.completeExceptionally(e);
} else {
commandConsumer.accept(pair.getFirst());
resultFuture.complete(new CommandResult<>(commandContext));
} }
} catch (final Exception e) { });
resultFuture.completeExceptionally(e); }
}
});
return resultFuture; return resultFuture;
} }

View file

@ -29,8 +29,8 @@ import cloud.commandframework.context.CommandContext
import cloud.commandframework.execution.CommandExecutionCoordinator import cloud.commandframework.execution.CommandExecutionCoordinator
import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.GlobalScope import kotlinx.coroutines.GlobalScope
import kotlinx.coroutines.async import kotlinx.coroutines.future.future
import kotlinx.coroutines.future.asCompletableFuture import java.lang.reflect.InvocationTargetException
import java.lang.reflect.Method import java.lang.reflect.Method
import java.util.concurrent.CompletableFuture import java.util.concurrent.CompletableFuture
import java.util.function.Predicate import java.util.function.Predicate
@ -71,12 +71,15 @@ private class KotlinMethodCommandExecutionHandler<C>(
override fun executeFuture(commandContext: CommandContext<C>): CompletableFuture<Void?> { override fun executeFuture(commandContext: CommandContext<C>): CompletableFuture<Void?> {
val instance = context().instance() val instance = context().instance()
val params = createParameterValues(commandContext, commandContext.flags(), false) val params = createParameterValues(commandContext, commandContext.flags(), false)
// We need to propagate exceptions to the caller. // We need to propagate exceptions to the caller.
return coroutineScope return coroutineScope.future(this@KotlinMethodCommandExecutionHandler.coroutineContext) {
.async<Void?>(this@KotlinMethodCommandExecutionHandler.coroutineContext) { try {
context().method().kotlinFunction?.callSuspend(instance, *params.toTypedArray()) context().method().kotlinFunction!!.callSuspend(instance, *params.toTypedArray())
null } catch (e: InvocationTargetException) { // unwrap invocation exception
e.cause?.let { throw it } ?: throw e // if cause exists, throw, else rethrow invocation exception
} }
.asCompletableFuture() null
}
} }
} }

View file

@ -26,6 +26,7 @@ package cloud.commandframework.kotlin.coroutines
import cloud.commandframework.CommandManager import cloud.commandframework.CommandManager
import cloud.commandframework.annotations.AnnotationParser import cloud.commandframework.annotations.AnnotationParser
import cloud.commandframework.annotations.CommandMethod import cloud.commandframework.annotations.CommandMethod
import cloud.commandframework.exceptions.CommandExecutionException
import cloud.commandframework.execution.AsynchronousCommandExecutionCoordinator import cloud.commandframework.execution.AsynchronousCommandExecutionCoordinator
import cloud.commandframework.internal.CommandRegistrationHandler import cloud.commandframework.internal.CommandRegistrationHandler
import cloud.commandframework.meta.CommandMeta import cloud.commandframework.meta.CommandMeta
@ -36,13 +37,15 @@ import kotlinx.coroutines.runBlocking
import kotlinx.coroutines.withContext import kotlinx.coroutines.withContext
import org.junit.jupiter.api.BeforeEach import org.junit.jupiter.api.BeforeEach
import org.junit.jupiter.api.Test import org.junit.jupiter.api.Test
import org.junit.jupiter.api.assertThrows
import java.util.concurrent.ExecutorService
import java.util.concurrent.Executors import java.util.concurrent.Executors
import java.util.concurrent.TimeUnit import java.util.concurrent.TimeUnit
class KotlinAnnotatedMethodsTest { class KotlinAnnotatedMethodsTest {
companion object { companion object {
val executorService = Executors.newSingleThreadExecutor() val executorService: ExecutorService = Executors.newSingleThreadExecutor()
} }
private lateinit var commandManager: CommandManager<TestCommandSender> private lateinit var commandManager: CommandManager<TestCommandSender>
@ -68,15 +71,27 @@ class KotlinAnnotatedMethodsTest {
commandManager.executeCommand(TestCommandSender(), "test").await() commandManager.executeCommand(TestCommandSender(), "test").await()
} }
@Test
fun `test suspending command methods with exception`(): Unit = runBlocking {
AnnotationParser(commandManager, TestCommandSender::class.java) {
SimpleCommandMeta.empty()
}
.also { it.installCoroutineSupport() }
.parse(CommandMethods())
assertThrows<CommandExecutionException> {
commandManager.executeCommand(TestCommandSender(), "test-exception").await()
}
}
private class TestCommandSender private class TestCommandSender
private class TestCommandManager : private class TestCommandManager : CommandManager<TestCommandSender>(
CommandManager<TestCommandSender>( AsynchronousCommandExecutionCoordinator.newBuilder<TestCommandSender>()
AsynchronousCommandExecutionCoordinator.newBuilder<TestCommandSender>() .withExecutor(executorService)
.withExecutor(executorService) .build(),
.build(), CommandRegistrationHandler.nullCommandRegistrationHandler()
CommandRegistrationHandler.nullCommandRegistrationHandler() ) {
) {
override fun hasPermission(sender: TestCommandSender, permission: String): Boolean = true override fun hasPermission(sender: TestCommandSender, permission: String): Boolean = true
@ -90,5 +105,8 @@ class KotlinAnnotatedMethodsTest {
withContext(Dispatchers.Default) { withContext(Dispatchers.Default) {
println("called from thread: ${Thread.currentThread().name}") println("called from thread: ${Thread.currentThread().name}")
} }
@CommandMethod("test-exception")
public suspend fun suspendingCommandWithException(): Unit = throw IllegalStateException()
} }
} }