Marius Metzger
05/19/2021, 3:13 PM@InternalCoroutinesApi
override fun isDispatchNeeded(context: CoroutineContext): Boolean {
if (Bukkit.isPrimaryThread()) return false
// Check if the primary server thread is blocked during the execution of this coroutine.
// If it is, using the Bukkit scheduler would cause a deadlock, as scheduled tasks
// are executed at the end of the tick, which requires the server thread to be running.
// To detect whether the server thread is blocked, we traverse the hierarchy of Jobs upwards
// to check if there's a coroutine running in a [runBlocking] context.
// If so, we don't dispatch, causing the action to be executed on the current thread.
// As the primary thread is locked, this has no thread-safety implications with regards to
// the Bukkit API.
// However, Bukkit methods that check whether they're performed on the server thread,
// such as spawning an entity, will fail exceptionally - if this becomes an issue,
// we can move the [isDispatchNeeded] logic into [dispatch], toggling a global flag while
// directly executing the runnable, and patch the server software to respect this flag
// during those checks.
var job: Job? = context[Job]
while (job != null) {
if (job.javaClass == blockingCoroutineClass) {
// we found a coroutine spawned via runBlocking - check whether it blocks the
// primary server thread using the same logic as [CraftServer.isPrimaryThread]
val blockedThread = Reflect.on(job).field("blockedThread").get<Thread>()
if (blockedThread == MinecraftServer.getServer().serverThread ||
blockedThread == MinecraftServer.getServer().shutdownThread ||
blockedThread is TickThread) {
return false
}
}
if (AbstractCoroutine::class.java.isAssignableFrom(job.javaClass)) {
// traverse context hierarchy upwards
job = Reflect.on(job).field("parentContext").get<CoroutineContext>()[Job]
} else {
throw UnsupportedOperationException("Can't handle jobs of type ${job.javaClass}")
}
}
return true
}
it’s not beautiful, but it worked - until I just updated to kotlinx.coroutines 1.5.0, which removes the field AbstractCoroutine.parentContext
in this commit.
Now my question is: is there still a way to hackily derive the parent Job
from a coroutine?
Or even better - is there a proper solution to this issue that doesn’t require messing with coroutine internals at all?
any help is appreciated 😊Zach Klippenstein (he/him) [MOD]
05/19/2021, 3:51 PMRyan Rolnicki
05/19/2021, 3:55 PMval result = async(<http://Dispatchers.IO|Dispatchers.IO>) { /* some I/O operations */ }
and then synchronously wait for the result to be set, which would mean you don't need the continuation to be scheduled. Which is fine, since you've already established you want to block the one and only server thread in this function.
Which feels hacky to me, so I'm looking forward to people that know more chiming in 🙂araqnid
05/19/2021, 4:40 PMfuture(<http://Dispatchers.IO|Dispatchers.IO>) { …}.join()
to do that via CompletableFutureMarius Metzger
05/19/2021, 5:17 PMjoin()
on that thread, therefore deadlocking when switching to the server thread inside the async
call?Marius Metzger
05/19/2021, 5:21 PMsuspend fun reloadConfigs() = withContext(<http://Dispatchers.IO|Dispatchers.IO>) {
// fetch the config files on the IO thread
// ...
// we're done - let's emit an event on the server thread
withContext(Dispatchers.Minecraft) {
emit(ConfigReloadedEvent)
}
}
when reloading configs while the server is running, we want to load the files in the background, hence reloadConfigs
being a suspend fun. however during startup, we need to halt the server thread until configs are initially loaded to ensure everything can get initialized.
but wouldn’t above code still break this:
fun onEnable() {
// we're on the server thread here and want to wait until configs are initially loaded
async(<http://Dispatchers.IO|Dispatchers.IO>) {
reloadConfigs()
}.join() // <- deadlock due to withContext(Dispatchers.Minecraft) call in reloadConfigs?
}
araqnid
05/19/2021, 5:58 PMaraqnid
05/19/2021, 5:59 PM