Coroutines in onDisable
(This site is only relevant for Spigot, CraftBukkit and Paper)
After moving most of your code to suspend functions, you may want to launch a coroutine in the onDisable
or
any other function, which gets called, after the plugin has already been disabled.
Default Behaviour (ShutdownStrategy=Scheduler)
The default behaviour of MCCoroutine is to stop all coroutines immediately, once the BukkitScheduler
has been
shutdown. This happens automatically and before your onDisable
function of your JavaPlugin
class
gets called.
If you try the following, you run into the following exception.
override fun onDisable() {
println("[onDisable] Is starting on Thread:${Thread.currentThread().name}/${Thread.currentThread().id}/primaryThread=${Bukkit.isPrimaryThread()}")
val plugin = this
plugin.launch {
println("[onDisable] Simulating data save on Thread:${Thread.currentThread().name}/${Thread.currentThread().id}/primaryThread=${Bukkit.isPrimaryThread()}")
Thread.sleep(500)
}
println("[onDisable] Is ending on Thread:${Thread.currentThread().name}/${Thread.currentThread().id}/primaryThread=${Bukkit.isPrimaryThread()}")
}
java.lang.RuntimeException: Plugin MCCoroutine-Sample attempted to start a new coroutine session while being disabled.
This behaviour makes sense, because the BukkitScheduler works in the same way. MCCoroutine is just a smart wrapper for it.
Calling a suspend function
However, you may have to call a suspend function anyway. This one of the few exceptions were using runBlocking
makes
sense:
override fun onDisable() {
println("[onDisable] Is starting on Thread:${Thread.currentThread().name}/${Thread.currentThread().id}/primaryThread=${Bukkit.isPrimaryThread()}")
val plugin = this
runBlocking {
foo()
}
println("[onDisable] Is ending on Thread:${Thread.currentThread().name}/${Thread.currentThread().id}/primaryThread=${Bukkit.isPrimaryThread()}")
}
suspend fun foo() {
println("[onDisable] Simulating data save on Thread:${Thread.currentThread().name}/${Thread.currentThread().id}/primaryThread=${Bukkit.isPrimaryThread()}")
Thread.sleep(500)
}
Manual Behaviour (ShutdownStrategy=Manual)
The default strategy is the recommend one and you should design your plugin according that.
However, there may be edge cases, where
you need full control over handling remaining coroutine jobs and use minecraftDispatcher
or asyncDispatcher
after the plugin has been disabled.
Change the shutdownStrategy in onEnable
override fun onEnable() {
val plugin = this
plugin.mcCoroutineConfiguration.shutdownStrategy = ShutdownStrategy.MANUAL
// Your code ...
}
Call disposePluginSession
after you are finished.
override fun onDisable() {
// Your code ...
val plugin = this
plugin.mcCoroutineConfiguration.disposePluginSession()
}
Plugin.launch is back
This allows to use plugin.launch
in your onDisable
function.
override fun onDisable() {
val plugin = this
println("[MCCoroutineSamplePlugin/onDisableAsync] Is starting on Thread:${Thread.currentThread().name}/${Thread.currentThread().id}/primaryThread=${Bukkit.isPrimaryThread()}")
plugin.launch {
println("[MCCoroutineSamplePlugin/onDisableAsync] Number 1:${Thread.currentThread().name}/${Thread.currentThread().id}/primaryThread=${Bukkit.isPrimaryThread()}")
delay(500)
println("[MCCoroutineSamplePlugin/onDisableAsync] Number 2:${Thread.currentThread().name}/${Thread.currentThread().id}/primaryThread=${Bukkit.isPrimaryThread()}")
}
plugin.mcCoroutineConfiguration.disposePluginSession()
println("[MCCoroutineSamplePlugin/onDisableAsync] Is ending on Thread:${Thread.currentThread().name}/${Thread.currentThread().id}/primaryThread=${Bukkit.isPrimaryThread()}")
}
[Server thread/INFO]: [MCCoroutine-Sample] Disabling MCCoroutine-Sample
[Server thread/INFO]: [MCCoroutineSamplePlugin/onDisableAsync] Is starting on Thread:Server thread/55/primaryThread=true
[Server thread/INFO]: [MCCoroutineSamplePlugin/onDisableAsync] Number 1:Server thread/55/primaryThread=true
[Server thread/INFO]: [MCCoroutineSamplePlugin/onDisableAsync] Is ending on Thread:Server thread/55/primaryThread=true
However, the message [MCCoroutineSamplePlugin/onDisableAsync] Number 2
will not printed,
because plugin.mcCoroutineConfiguration.disposePluginSession()
is called first (context switch of delay).
This means, we need to use runBlocking
anyway:
override fun onDisable() {
val plugin = this
println("[MCCoroutineSamplePlugin/onDisableAsync] Is starting on Thread:${Thread.currentThread().name}/${Thread.currentThread().id}/primaryThread=${Bukkit.isPrimaryThread()}")
runBlocking {
plugin.launch {
println("[MCCoroutineSamplePlugin/onDisableAsync] Number 1:${Thread.currentThread().name}/${Thread.currentThread().id}/primaryThread=${Bukkit.isPrimaryThread()}")
delay(500)
println("[MCCoroutineSamplePlugin/onDisableAsync] Number 2:${Thread.currentThread().name}/${Thread.currentThread().id}/primaryThread=${Bukkit.isPrimaryThread()}")
}.join()
}
plugin.mcCoroutineConfiguration.disposePluginSession()
println("[MCCoroutineSamplePlugin/onDisableAsync] Is ending on Thread:${Thread.currentThread().name}/${Thread.currentThread().id}/primaryThread=${Bukkit.isPrimaryThread()}")
}
[Server thread/INFO]: [MCCoroutine-Sample] Disabling MCCoroutine-Sample
[Server thread/INFO]: [MCCoroutineSamplePlugin/onDisableAsync] Is starting on Thread:Server thread/55/primaryThread=true
[Server thread/INFO]: [MCCoroutineSamplePlugin/onDisableAsync] Number 1:Server thread/55/primaryThread=true
[kotlinx.coroutines.DefaultExecutor/INFO]: [MCCoroutineSamplePlugin/onDisableAsync] Number 2:kotlinx.coroutines.DefaultExecutor/133/primaryThread=false
[Server thread/INFO]: [MCCoroutineSamplePlugin/onDisableAsync] Is ending on Thread:Server thread/55/primaryThread=true
This helps, however it is important to notice that the thread
executing MCCoroutineSamplePlugin/onDisableAsync] Number 2
is no longer the primary thread even though
we are using the plugin.launch
scope, which should guarantee this.
After the BukkitScheduler
has been shutdown, MCCoroutine is no longer able to guarantee any context switches.
Depending on your use case, you may or may not care about that.
Therefore, think twice if you really want to have so much control. You are on your own, if you set the shutdownStrategy to manual.
Waiting for jobs to complete
One useful case, where you want to set the shutdownStrategy to manual is to be able to wait for long running jobs to complete before you disable the plugin.
private var longRunningJob: Job? = null
override fun onEnable() {
val plugin = this
plugin.mcCoroutineConfiguration.shutdownStrategy = ShutdownStrategy.MANUAL
longRunningJob = plugin.launch {
delay(10000)
println("Over")
}
}
override fun onDisable() {
runBlocking {
longRunningJob!!.join()
}
val plugin = this
plugin.mcCoroutineConfiguration.disposePluginSession()
}
[Server thread/INFO]: [MCCoroutine-Sample] Disabling MCCoroutine-Sample
[kotlinx.coroutines.DefaultExecutor/INFO]: Over
Waiting for all jobs to complete
You can also wait for all of your spawned open jobs to complete.
override fun onEnable() {
val plugin = this
plugin.mcCoroutineConfiguration.shutdownStrategy = ShutdownStrategy.MANUAL
plugin.launch {
delay(10000)
println("Over")
}
}
override fun onDisable() {
val plugin = this
runBlocking {
plugin.scope.coroutineContext[Job]!!.children.forEach { childJob ->
childJob.join()
}
}
plugin.mcCoroutineConfiguration.disposePluginSession()
}
[Server thread/INFO]: [MCCoroutine-Sample] Disabling MCCoroutine-Sample
[kotlinx.coroutines.DefaultExecutor/INFO]: Over