Why TaskChain

Lax edited this page Apr 20, 2018 · 5 revisions

Game API's and Thread Safety

Many games, especially in the Minecraft Eco System, have the concept of a "Main" thread. This is a thread that all of the games central logic runs on and where the world data is manipulated.

For API's, unless they are specifically made thread safe, they need to be ran on this main thread only. If an API is not thread safe, and you try to say, interact with the world, you are not guaranteed behavior. That world data may change at the exact same time you are accessing it, and give you inconsistent results.

A player may log out while you are trying to give them items, resulting in the items being given AFTER the data was saved.

It's essentially unsafe to do things off of the main thread unless the API itself adds all the protections for the corner cases such as described for you.

Mixing Game API usage and heavy blocking operations

Say you need to retrieve some data from a database, or call some Web REST API, or load a giant file off the hard drive.

Then you need to act on that data, and use it with your game API's. For example, "Retrieve the Item that was mailed to the player from the database and give it to them"

What if your database is under heavy load, or worse: not in same data center. That request may take over 300 milliseconds. In Game time, that is an eternity. In Minecraft for example, that is 6 ticks out of 20 lost!

The main recommendation to do this would be to perform your blocking operations such as the ones described asynchronous to the main thread.

But now you need to act on that response. As just explained, your API may not be thread safe! So you're off on this thread, and you need to now call your game API. You need to "Get back on" the main thread before calling that API.

Typical solutions to this

Ok so you started a thread, did some work, have your response and need to use the API. You would likely use your games scheduler to then get back on the main thread, and call your API. after your API, you then need to record a success state for the operation, which also needs to be done asynchronously.

Your code now likely looks like this.

public class UglyExample {

    ExecutorService threadPool;

    public void someBigOperation(Object player, Object identifier) {
        threadPool.submit(() -> {
            Object result = someLongBlockingRequest(player, identifier);
            if (result == null) {
                sendPlayerMessage(player, "Sorry, something failed!");
                return;
            }
            GameScheduler.runTaskOnMain( () -> {
                Object opResult = GameAPI.doSomethingToPlayerWithResult(player, result);
                if (opResult == null) {
                    sendPlayerMessage(player, "Sorry, something failed!");
                    return;
                }
                threadPool.submit(() -> {
                    markOperationSuccessful(player, identifier, opResult);
                });
            });
        });
    }

}

Works, but super ugly! And lets be fair, this is the cleaner version of what this could look like. In Minecraft/Bukkit development, you're likely going to see 3-4 more levels deep, using the Bukkit Scheduler for async tasks.

Here is it in TaskChain form!

public class GoodExample {

    TaskChainFactory taskChain;

    public void someBigOperation(Object player, Object identifier) {
        taskChain.newChain()
                .asyncFirst(() -> someLongBlockingRequest(player, identifier))
                .abortIfNull(ActionHandlers.MESSAGE, player, "Sorry, something failed!")
                .sync((result) -> GameAPI.doSomethingToPlayerWithResult(player, result))
                .abortIfNull(ActionHandlers.MESSAGE, player, "Sorry, something failed!")
                .async((opResult) -> markOperationSuccessful(player, identifier, opResult))
                .execute();
    }

}

TaskChain cleans up your code and makes it much clearer what is going out without deeply nesting callbacks.

It helps ensure your code runs on the proper thread, and switches when it needs to. It also helps you build more functional code, which will encourage code re-use with task creators.

All the thread switching boilerplate is removed, and you can switch back and forth between threads as much as you want without more nested callbacks.

You can’t perform that action at this time.
You signed in with another tab or window. Reload to refresh your session. You signed out in another tab or window. Reload to refresh your session.
Press h to open a hovercard with more details.