Asynchronously working with a database

Jun 12, 2016
Asynchronously working with a database
  • Asynchronously working with a database

    Using the Bukkit scheduler for async tasks safely



    Note: Before editing this article, please ask in the discussion section for a second opinion – personal opinions and views should be kept out of wiki articles. Thank you!


    Before you start(top)


    For this example we will be using MongoDB, but this tutorial should be followable without having a lot of knowledge about MongoDB. If you do wish to read about MongoDB you can read the "Using MongoDB" wiki page. We will also be creating a command, of which there is an explanation here.

    What is asynchronous(top)


    Every program has a starting point, from which the execution of the program happens basically line-by-line, which means every task will run after the previous task is finished. Because this is in a lot of cases not the most optimal situation there is a way of starting a new task, that starts doing it thing, while the main program continues as well.

    Those tasks are called Threads by the system and most programming languages that allow you to make use of them. Making use of multiple threads is especially useful if for instance one task is designed to wait for input, and you don't want the entire program to stop until a player sends a chat message. Threads are also smartly distributed by the operating system to the cores of the processor, which means that if you have multiple cores you can even get a performance increase from utilizing threads, as minecraft's main loop runs on a single core.

    Asynchronous is the opposite of synchronous, and all there is to it is that when you execute a task in a new or seperate thread from where you are starting the task from, you're calling it asynchronously, while if you're starting a task and waiting for it to be finished in the same thread as where you started it from, you have called it synchronously. Neither is bad or good on it's own, the usages of both depend on the situation.

    Creating a /playerinfo command(top)


    For this tutorial we will assume that a valid connection is made to the database, and that a collection exists with some player data in it. The command will take one argument which will be the player name or uuid. An example of a player document in the database:
    Code (Text):
    {
        _id: ObjectId("564bff868ab04da7798b4569"),
        username: "Wouto1997",
        lookupUsername: "wouto1997",
        uuid: "a44c33ce480e486f9f782d1f52db037b",
        money: 17500,
        flying: true,
        friends: [],
        rank: "DEVELOPER",
        lastSeen: ISODate("2015-11-18T11:33:10.852Z"),
        registered: ISODate("2014-08-14T21:15:10.152Z")
    }
    So we will immediately start off with a basic /playerinfo command that is running completely synchronously.

    Code (Java):
    public class CommandPlayerInfo implements CommandExecutor {

        @Override
        public boolean onCommand(CommandSender cs, Command cmnd, String label, String[] args) {
            if (args.length < 1) {
                cs.sendMessage("Usage:");
                cs.sendMessage("  /playerinfo <name>");
                cs.sendMessage("  /playerinfo <uuid>");
                return true;
            }
            String param = args[0];
            String key = null;
            if (param.length() <= 16 && param.length() >= 3) {
                key = "lookupUsername";
            } else {
                param = param.replaceAll("-", "");
                if (param.length() != 32) {
                    cs.sendMessage("Invalid username or uuid");
                    return true;
                }
                key = "uuid";
            }
            param = param.toLowerCase();
            DBCollection playerCollection = DatabaseHelper.getPlayerDatabase();
            DBObject result = playerCollection.findOne(new BasicDBObject(key, param));
            if (result == null) {
                cs.sendMessage("The specified player could not be found");
                return true;
            }
            cs.sendMessage( "Information about " + ((String) result.get("username")) );
            cs.sendMessage( "UUID: " + ((String) result.get("uuid")) );
            cs.sendMessage( "money: " + ((Integer) result.get("money")) );
            cs.sendMessage( "Fly: " + ((Boolean) result.get("fly")).toString() );
            cs.sendMessage( "Friends: " + ((BasicDBList) result.get("friends")).size() );
            cs.sendMessage( "rank: " + ((String) result.get("rank")) );
            cs.sendMessage( "Last online: " + ((Date) result.get("lastSeen")).toLocaleString() );
            cs.sendMessage( "Registered: " + ((Date) result.get("registered")).toLocaleString() );
            return true;
        }

    }

    Using bukkit's asynchronous functions(top)


    The first thing you'll need to learn about is the Bukkit Scheduler, which you can access with Bukkit.getScheduler() from anywhere. There are 3 different types of tasks you can add to the scheduler, and each of those types has a synchronous and an asynchronous variant.

    Important to note is that a synchronous task in this context is ran on a minecraft tick, and not in the thread (usually) where you're calling it from. It is advised to not access most of the bukkit API while not inside the minecraft-tick thread, but on the other hand it's also smart to execute database and other heavy tasks outside of the main thread.

    We will be using runTask and runTaskAsynchronously only for this example, as we do not wish to execute a command later or repeatedly. After having parsed all the arguments the first thing we want to do is "escape" the command thread and start a new thread seperated from minecraft's threads.

    Code (Java):
            Bukkit.getScheduler().runTaskAsynchronously(plugin, new Runnable() {
                @Override
                public void run() {
             
                }
            });
    Now that we have an asynchronous job we want to be able to pass data to it. Since this run method is outside of the rest of the command we need to make all variables that this task accesses final. Making a variable final removes the ability to re-assign it, however functions on the variable still work as expected. The code we end up with now looks like this:

    Code (Java):
        @Override
        public boolean onCommand(final CommandSender cs, Command cmnd, String label, String[] args) {
            if (args.length < 1) {
                cs.sendMessage("Usage:");
                cs.sendMessage("  /playerinfo <name>");
                cs.sendMessage("  /playerinfo <uuid>");
                return true;
            }
            String param = args[0];
            String key = null;
            if (param.length() <= 16 && param.length() >= 3) {
                key = "lookupUsername";
            } else {
                param = param.replaceAll("-", "");
                if (param.length() != 32) {
                    cs.sendMessage("Invalid username or uuid");
                    return true;
                }
                key = "uuid";
            }
            final String fparam = param.toLowerCase();
            final String fkey = key;
     
            Bukkit.getScheduler().runTaskAsynchronously(plugin, new Runnable() {
                @Override
                public void run() {
                    DBCollection playerCollection = DatabaseHelper.getPlayerDatabase();
                    DBObject result = playerCollection.findOne(new BasicDBObject(fkey, fparam));
                    if (result == null) {
                        fcs.sendMessage("The specified player could not be found");
                        return;
                    }
                    fcs.sendMessage("Information about " + ((String) result.get("username")));
                    fcs.sendMessage("UUID: " + ((String) result.get("uuid")));
                    fcs.sendMessage("money: " + ((Integer) result.get("money")));
                    fcs.sendMessage("Fly: " + ((Boolean) result.get("fly")).toString());
                    fcs.sendMessage("Friends: " + ((BasicDBList) result.get("friends")).size());
                    fcs.sendMessage("rank: " + ((String) result.get("rank")));
                    fcs.sendMessage("Last online: " + ((Date) result.get("lastSeen")).toLocaleString());
                    fcs.sendMessage("Registered: " + ((Date) result.get("registered")).toLocaleString());
                }
            });

            return true;
        }
    Note how I had to make the param, key, and cs variable final, simply by creating a new variable that has the final keyword, and assigning the original value to the new variable. The CommandSender was made final by adding the final keyword simple in the parameter part of the method. You may also notice now that the "return true" at the bottom is going to be called before the command is done, and this is perfectly fine. The only thing wrong now is that we're accessing bukkit functions inside an asynchronous task, the sendMessage functions. And although I am quite sure this will work just fine, I would also advise you to go back to the main thread, especially if you're doing more than sending messages to the player.

    However now that we've already got one task in a command it may get messy to create another one inside of that, so that brings me to the next step, which is a very nice way to make this process way more beautiful.

    Callbacks(top)


    Creating callbacks is a very elegant way of hiding one or more tasks in the background and returning to where you were in the code you were writing only when the other tasks are done. The Runnable class used by the scheduler in the earlier snippets are basically callbacks aswell, they just call back very quickly as all they have to wait for is the minecraft tick to occur.

    To create our own callback all we have to do is create an interface. I named mine FindOneCallback, and I gave it one function that can be called when the query is done. The callback class looks like this:

    Code (Java):
    public interface FindOneCallback {

        public void onQueryDone(DBObject result);

    }
    Now we want to create a function that has all the functionality of the database handling stuff. It may look a bit messy but it's basically 2 times the scheduler thing we did before, only the first one is to get out of the main loop, and the second to get back in.

    Code (Java):
        public static void findPlayerAsync(final DBObject query, final FindOneCallback callback) {
            // Run outside of the tick loop
            Bukkit.getScheduler().runTaskAsynchronously(plugin, new Runnable() {
                @Override
                public void run() {
                    DBCollection playerCollection = DatabaseHelper.getPlayerDatabase();
                    final DBObject result = playerCollection.findOne(query);
                    // go back to the tick loop
                    Bukkit.getScheduler().runTask(plugin, new Runnable() {
                        @Override
                        public void run() {
                            // call the callback with the result
                            callback.onQueryDone(result);
                        }
                    });
                }
            });
        }

    Reorganising the command(top)

    Now all we have to do is patch our command, and you'll notice how much cleaner the command can become by utilizing this, as well as improving performance quite a bit.

    Code (Java):
        @Override
        public boolean onCommand(final CommandSender cs, Command cmnd, String label, String[] args) {
            if (args.length < 1) {
                cs.sendMessage("Usage:");
                cs.sendMessage("  /playerinfo <name>");
                cs.sendMessage("  /playerinfo <uuid>");
                return true;
            }
            String param = args[0];
            String key = null;
            if (param.length() <= 16 && param.length() >= 3) {
                key = "lookupUsername";
            } else {
                param = param.replaceAll("-", "");
                if (param.length() != 32) {
                    cs.sendMessage("Invalid username or uuid");
                    return true;
                }
                key = "uuid";
            }
     
            BasicDBObject query = new BasicDBObject(key, param);
     
            DatabaseHelper.findPlayerAsync(query, new FindOneCallback() {
                @Override
                public void onQueryDone(DBObject result) {
                    if (result == null) {
                        cs.sendMessage("The specified player could not be found");
                        return;
                    }
                    cs.sendMessage("Information about " + ((String) result.get("username")));
                    cs.sendMessage("UUID: " + ((String) result.get("uuid")));
                    cs.sendMessage("money: " + ((Integer) result.get("money")));
                    cs.sendMessage("Fly: " + ((Boolean) result.get("fly")).toString());
                    cs.sendMessage("Friends: " + ((BasicDBList) result.get("friends")).size());
                    cs.sendMessage("rank: " + ((String) result.get("rank")));
                    cs.sendMessage("Last online: " + ((Date) result.get("lastSeen")).toLocaleString());
                    cs.sendMessage("Registered: " + ((Date) result.get("registered")).toLocaleString());
                }
            });

            return true;
        }

    Conclusion(top)

    The usage of asynchronous programming in this example may have been overkill, however for tasks like playerdata loading when a user joins the server, or updating values in the database whenever certain events occur, this is a very advised method of handling it. Asynchronous programming is not only useful for MongoDB, but can as easily be used for SQL, file reading/writing, transfering or receiving data from a website, and many other things.
  • Loading...
  • Loading...