Shynixn / MCCoroutine

MCCoroutine is a library, which adds extensive support for Kotlin Coroutines for Minecraft Server environments.

Geek Repo:Geek Repo

Github PK Tool:Github PK Tool

[Question] Changing an event status after calling a suspending function

HaKIMus opened this issue · comments

commented

Hi,

To describe my issue, let's take a look on the following event handler:

    @EventHandler(ignoreCancelled = false)
    suspend fun onBlockBreak(e: BlockBreakEvent) {
        val blockMaterial = e.block.type // Assigning blockMaterial here, as it gets erased after calling a suspending function

        withContext(Dispatchers.IO) {
            delay(10)
        }

        if (blockMaterial == Material.STONE) {
            e.isDropItems = false // Does not stop items drop
            e.isCancelled = true // Does not cancel the event
        }
    }

After calling a suspending function - withContext(Dispatchers.IO delay(10) in this case - the event gets "erased/reset".

My question is: How can I keep the access to the event after calling a suspending function?

It'd be nice if someone could explain me why the event gets erased, too.

commented

My take on why I can't cancel the event after running a suspending function - it might take an unspecified amount of time to resume the event after hitting a suspension point. Because of that, a "default behaviour" must be executed so the state of the event will not be frozen(?) without a state of completion.

If my assumption is correct, then I'd like to see reasoning behind erasing event data like e.block.

I'd like to know where the logic responsible for executing the default behaviour is stored, so I could examine it myself.

Hello,

the definition of suspending a thread compared to blocking a thread is that a thread can continue to perform other work when it is suspended. It cannot do other work while it is blocked.

However, a suspended thread cannot magically reverse or change what has already happend. The first time you suspend the thread, the event results are returned to the caller of the event. All the other code will be executed later once the minecraft server thread has got time again.

Basically, Kotlin Coroutines together with MCCoroutine allows you to asynchronous code in sequential manner without having to deal with callbacks.

I'll give you some examples how the compiler transforms your events once you use withContext or async in a suspend function:

    val coroutinesIoThreadPool : Executor // Abstraction of KotlinCoroutines Dispatcher.IO
    val minecraftThread : Executor // Abstraction of the minecraft server scheduler.

    @EventHandler(ignoreCancelled = false)
    fun onBlockBreak(e: BlockBreakEvent) {
        val blockMaterial = e.block.type // Assigning blockMaterial here, as it gets erased after calling a suspending function

        coroutinesIoThreadPool.execute(Runnable { 
            Thread.sleep(10)
            
            minecraftThread.execute(Runnable {

                if (blockMaterial == Material.STONE) {
                    e.isDropItems = false // Does not stop items drop
                    e.isCancelled = true // Does not cancel the event
                }
            })
        })
    }

Of course, Kotlin Coroutines does it a way smarter but I think this makes it clear why you cannot change the event result after the first suspension.

Disclaimer: This is not how the byte code eventually looks like. delay(10) is not compiled to Thread.sleep(10). This is just an example to show some of the aspects of the state machine being generated by the compiler.

commented

Thank you for the explanation, now it's clear to me what is happening inside the event handler and why the event gets "erased".

I'd like to ask you one more question. What would you suggest to do in case where you need for a heavy computation to determine how to react on a given event?

What I did in my case is the following:

  1. Introduced a cache to minimise the amount of computations,
  2. Limited occurrences of starting the computation - in case of BlockBreakEvent, I'm applying on players semi-adventure mode by which they can break blocks (trigger BlockBreakEvent) that I specify only,
  3. If neccessary, I try to reflect the behaviour of (not-)/cancelling an event that is using a suspending function.

Do I miss any critical solution/optimisation to this problem?

I don't think there exists a solution which fits all cases. It depends.
When implementing MCCoroutine for the bukkit this exact question was also one of the questions haunting me. 😄

Let's list up some facts:

  • A heavy computation must not be executed on the main thread.
  • MCCoroutine does not protect you regarding spam. For example, a player could break a lot of blocks in a very short time frame but each time the block break event is triggerd, a new task is scheduled. One player could theoretically create tons of tasks if each of the scheduled tasks takes quite a while. Therefore background tasks should be reasonably long. What's reasonably depends on your usecase (server hardware and amount of players) but for the block break event, it would be fine to execute one database operation in my opinion. The PlayerMoveEvent should not perform any context switches for example.
  • I think you also noticed it too: The threading problems do not magically go away when using Coroutines. It just easier to use and maintain it.

Regarding your 3 points. All are valid approaches to your problem. There isn't a "silver bullet" solution.

commented

Got it!

Thank you for helping me, I appreciate it.

The issue can be closed.