Closed paaulhier closed 3 months ago
There were a few small discussions in the Discord server about introducing something like this to Lamp. In my previous command framework, cub, there was a @RunAsync
annotation which did exactly that. However, it brought many subtle issues with it and introduced a lot of design problems. To name a few:
It is also, admittedly, easy to misuse, which violates one of Lamp's core principles, to be misuse-resistant. For example:
@Async
is better, and give in to the magic of asynchronous stuffBesides that, Lamp does indeed support many asynchronous features that avoid most of these pitfalls:
T
, you can make it return CompletionStage<T>
or CompletableFuture<T>
and Lamp will automatically deal with it appropriately.CommandHandler.supportSuspendFunctions()
extension function)For example, compare these two examples:
@Command("save-entities-in-world")
public CompletableFuture<Component> saveEntitiesInWorld(CommandSender sender, @Default("self") World world) {
// These functions must be called on the main thread
sender.sendMessage("Saving...");
List<Entity> entities = world.getEntities();
return CompletableFuture.supplyAsync(() -> {
File file = new File("entities-" + world.getName() + ".json");
// save entities here...
return Component.text("Entities saved successfully");
});
}
In this one, the asynchronous part is explicit and clear, and it's hard to accidentally call non-async-safe functions asynchronously. This is fully supported and it is the recommended way for doing asynchronous things.
@Async
@Command("save-entities-in-world")
public Component saveEntitiesInWorld(CommandSender sender, @Default("self") World world) {
// Calling functions that are not async-safe may cause undefined behavior
sender.sendMessage("Saving...");
List<Entity> entities = world.getEntities();
File file = new File("entities-" + world.getName() + ".json");
// save entities here
return Component.text("Entities saved successfully");
}
This one, while it looks nicer, is very bug-prone and it's easy to mistakenly call APIs that are not supported asynchronously. I find myself always overlooking the @Async
annotation, and the code can be unpredictable at times. By the time the developer finds out about the bug, they will have to revert to using something similar to the first example. So it's time and effort wasted.
Such problems are also easy to avoid in Kotlin coroutines:
@Command("save-entities-in-world")
suspend fun saveEntitiesInWorld(sender: CommandSender, @Default("self") world: World) {
// calls in the main thread
val entities = world.getEntities()
// calls on the IO dispatcher
withContext(Dispatchers.IO) {
val file = File(("entities-" + world.getName()).toString() + ".json")
// save entities here
}
}
Here, it's also clear and explicit, and the developer has complete control over which parts are executed asynchronously and which ones are not.
While I'm not entirely against introducing such an annotation, it may require more work and more explicitness than merely a marker on a function.
Create an @Async annotation to make commands be executed asynchronously