https://kotlinlang.org logo
#coroutines
Title
# coroutines
m

Marius Metzger

05/19/2021, 3:13 PM
Copy code
@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 😊
z

Zach Klippenstein (he/him) [MOD]

05/19/2021, 3:51 PM
I think the proper solution is “don’t use runBlocking”. This sounds like one of the issues discussed in this blog.
r

Ryan Rolnicki

05/19/2021, 3:55 PM
Is it necessarily the case you can't schedule actions meant to be run on the server thread from another thread? Conceptually, runBlocking is meant to block the thread in question, so the result seems not unexpected. Keeping the code close to as it is, you could do something like
val 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 🙂
a

araqnid

05/19/2021, 4:40 PM
yes, sth like
future(<http://Dispatchers.IO|Dispatchers.IO>) { …}.join()
to do that via CompletableFuture
m

Marius Metzger

05/19/2021, 5:17 PM
I’m not sure I understand - wouldn’t your example still block the main server thread when calling
join()
on that thread, therefore deadlocking when switching to the server thread inside the
async
call?
let me give my actual real-world use case - our configuration system has the following function to (re-)load config files:
Copy code
suspend 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:
Copy code
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?
}
a

araqnid

05/19/2021, 5:58 PM
ah, I see what you mean. Yes, you need a way for bits of main-thread work to be dispatched while waiting for it to happen. I think that using runBlocking, which effectively creates its own event loop for the interim, is hard going. Can you change the approach for startup so that it isn’t based on blocking functions? I know we have apps that have lifecycle hooks so that they can perform startup actions and eventually announce that startup is complete- concepts like “the current configuration” could be made available through some sort of lazy-load?
I’m surprised you can’t just get the parent job of a job rather than going through the parent context
4 Views