Passing parameters and returning data to/from Runnable

Discussion in 'Spigot Plugin Development' started by zeronerve, Dec 6, 2018 at 4:07 PM.

  1. Hi I have a MySQL database from which I'm pulling player data for leaderboards. Since the leaderboards are built each time a Lobby is created, this might happen during gameplay, so I need a way to run the getLeadersByPoints(int max, other params) method asynchronously, which should return an ArrayList<PlayerDAO>.

    My question is twofold: How can I pass parameters into an async thread or runnable? What approach can I take to "return" the ArrayList so that it can be used to populate leaderboards? Futures? Callbacks? I have no experience with either. All of this works wonderfully on the main thread, except for the potential lag.

    Code (Text):
    public class GetLeaders {

    public static ArrayList<LeaderDAO> getLeadersByPoints(int max) {

            ArrayList<LeaderDAO> leaderList = new ArrayList<>();

            //get data from MySQL

            return leaderList;
    }


     
    ...is being called from the Lobby class ...
    Code (Text):

    private void createLeaderboard(Location leaderboardLoc) {

            ...
         
            leaders = GetLeaders.getLeadersByPoints(20);

             ...
     
  2. I handle my async msql queries with a ExecutorService and a Consumer Callback:
    you create a Thread Pool with the Executor and in a query method (with your statement and a consumer as parameter) you call the execute function which basically calls the accept function of the consumer.
    Code (Java):
    // your mysql class which handles the connection to the database
    // ....
    ExecutorService executor = Executors.newCachedThreadPool();
    // ....
    public void query(String statement, Consumer<ResultSet> consumer)
    {
          executor.execute(() ->
          {
                ResultSet result = connectionVar.prepareStatement(statement).executeQuery();
                Bukkit.getScheduler().runTask(pluginVar, () -> consumer.accept(result));
          });
    }
    // ....
    So inside your getLeadersByPoints function you would do something like this:
    Code (Java):
    // ....
    mysqlVar.query("*YOUR QUERY HERE*", new Consumer<ResultSet>()
    {
          @Override
          public void accept(ResultSet set)
          {
                // use the ResultSet to fill your ArrayList
           }
    });
    // ....

    I hope this helps. If not feel free to ask.
     
    • Like Like x 1
  3. Thank you! Let me think on your response. I'm using hikariCP as a connection pool. Not sure how the connection pool would affect this, if at all. I really appreciate it.
     
  4. I'm not really familiar to this kind of stuff but you can probably use CompletableFuture
    Code (Java):
    public CompletableFuture<List<LeaderDAO>> getLeadersByPoints(int points) {
        return CompletableFuture.supplyAsync(() -> {
            List<LeaderDAO> leaders = new ArrayList<>();
            // your stuff from getting data from db
            return leaders;
        });
    }
    and you can use CompletableFuture#whenComplete
    Code (Java):
    getLeadersByPoints(10).whenComplete((leaders, throwable) -> this.leaders = leaders);
     
    • Like Like x 1
  5. I never worked with hikariCP so there might be some handy functions to solve your problem.
    Anyway I don't think using hikariCP causes problems with the mentioned methods.

    Yeah, this would work fine too.
     
  6. Just tested the CompleteableFuture. Works great. I had to remember to put all the tasks to run after whenComplete was fired. So I expended the lambda and add the code to build the leaderboard. I kept wondering why leaders was null right after the getLeaders call, and then I remembered it's another thread that returns when the job was done. Thanks so much!
     
  7. It looks like the ExecutorService might be a good fit for when I need to handle multiple operations in a cached pool.
     
  8. I just encountered a bug where the leaderboard only builds correctly on the first pass of my Lobby init(). When the Lobby reset() runs the same getLeaders() method, it returns the full recordset, but only the first is added to the Hologram.


    Code (Text):
        private void createLeaderboardHolo(Location leaderboardLoc) {
            this.leaderboardHolo = HologramsAPI.createHologram(CaptureTheFlag.plugin, leaderboardLoc);
            TextLine textLine = leaderboardHolo.appendTextLine("All Time Leaders");
            TextLine textLine2 = leaderboardHolo.appendTextLine("------------------------");

            Leaders.getLeadersByPoints(15).whenCompleteAsync((leadersIn, throwable) -> {
                ListIterator leadersIterator = leadersIn.listIterator();
                for (Leader leader : leadersIn) { CaptureTheFlag.plugin.log("leadersIn : " + leader.getUsername());}
                int i = 1;
                while (leadersIterator.hasNext()) {
                    Leader l = (Leader) leadersIterator.next();
                    CaptureTheFlag.plugin.log("Leader instance in Lobby: " + l.toString());
                    TextLine textLine3 = this.leaderboardHolo.appendTextLine(ChatColor.YELLOW + "#" + i + " " + ChatColor.GOLD + l.getUsername() + " : " +
                            ChatColor.WHITE + l.getPoints() + " " +      ChatColor.LIGHT_PURPLE + sp(l.getPoints(),      "Point",       "Points") + " " +
                            ChatColor.WHITE + l.getCaptures() + " " +    ChatColor.LIGHT_PURPLE + sp(l.getCaptures(),    "Cap",         "Caps") + " " +
                            ChatColor.WHITE + l.getWins() + " " +        ChatColor.LIGHT_PURPLE + sp(l.getWins(),        "Win",         "Wins") + " " +
                            ChatColor.WHITE + l.getKills() + " " +       ChatColor.LIGHT_PURPLE + sp(l.getKills(),       "Kill",        "Kills") + " " +
                            ChatColor.WHITE + l.getDeaths() + " " +      ChatColor.LIGHT_PURPLE + sp(l.getDeaths(),      "Death",       "Deaths") + " " +
                            ChatColor.WHITE + l.getRecoveries() + " " +  ChatColor.LIGHT_PURPLE + sp(l.getRecoveries(),  "Recovery",    "Recoveries") + " " +
                            ChatColor.WHITE + l.getGamesPlayed() + " " + ChatColor.LIGHT_PURPLE + sp(l.getGamesPlayed(), "Game",        "Games")
                    );
                    //leadersIterator.remove();
                    i++;
                }
            });
        }
    So the log line "Leader instance in Lobby" shows that all the leaders have been returned, but the .appendTextLine method only runs once. So I have only one leader on the list. Any ideas? Would this have anything to do with the CompletableFuture approach?
     
  9. I ended up using an ExecutorService which fixed the problem.

    Code (Text):
     private void createLeaderboardHolo(Location leaderboardLoc) {
            ExecutorService service = Executors.newSingleThreadExecutor();
            Future<ArrayList<Leader>> leaderList = service.submit(new Leaders(20));

            try {
                this.leaders = leaderList.get();
            } catch (InterruptedException e) {
                e.printStackTrace();
            } catch (ExecutionException e) {
                e.printStackTrace();
            }
            service.shutdown();

            //build leaderboard hologram
    It submits a new Leaders class which returns the <ArrayList<Leader>> leaderList.

    Code (Text):
    public class Leaders implements Callable<ArrayList<Leader>> {

        private int max;

        public Leaders(int max) {
            this.max = max;
    }


        @Override
        public ArrayList<Leader> call() {

         //get connection and ResultSet
         //build leaderList from ResultSet

         return leaderList;

     
     

Share This Page