1.15.2 help w/ CompletableFuture...

Discussion in 'Spigot Plugin Development' started by chmod_777, Mar 26, 2020.

  1. Re-uploading because I got no responses D:

    Im trying to implement async data handling in my code and I cannot get it to quite work. All of my data handling (mysql, sqlite, yaml for now) works flawlessly, at least to my knowledge, but I recently decided to allow them to run asynchronously.

    I started with flat file storage since its the easiest and most straight forward (Is there even any benefit to offloading flat file data off the main thread?), but for whatever reason I cannot get the FileConfiguration that is returned in the supplyAsync() to be assigned to an instance variable named 'data'. Because of this I'm thrown null pointers for obvious reasons.

    Here's the method:
    Code (Java):
    protected void returnFlatFile(String fileName) {
            CompletableFuture.supplyAsync(() -> {
                file = new File(plugin.getDataFolder().getPath(), fileName);
                if (!file.exists()) {
                    logger.log(Level.INFO, NEW_FILE);
                    try {
                        file.createNewFile();
                        logger.log(Level.INFO, "Done.");
                    } catch (IOException e) {
                        e.printStackTrace();
                        logger.log(Level.SEVERE, IO_ERROR);
                    }
                }
                return YamlConfiguration.loadConfiguration(file);
            }, plugin.getThreadPool()).thenAcceptAsync(config -> data = config, plugin.getThreadPool());
        }

    I know I can:
    Code (Java):

    protected FileConfiguration returnFlatFile(String fileName) {
          CompletableFuture<FileConfiguration> complete = CompletableFuture.supplyAsync(() -> { ... });

          return complete.get() //Try catch(s) go here but I was too lazy to write it :P
    }
     
    but I would rather avoid handling exceptions. That being said I still consider myself a newbie, so your judgement may be better than mine.

    Here is the stacktrace:
    Code (Text):
    java.lang.NullPointerException: null
            at net.il0c4l.seasons.storage.FlatDataHandler.<init>(FlatDataHandler.java:27) ~[?:?]
            at net.il0c4l.seasons.Main.setStorageType(Main.java:52) ~[?:?]
            at net.il0c4l.seasons.Main.onEnable(Main.java:29) ~[?:?]
            at org.bukkit.plugin.java.JavaPlugin.setEnabled(JavaPlugin.java:263) ~[spigot-1.15.2.jar:git-Spigot-a03b1fd-fc318cc]
            at org.bukkit.plugin.java.JavaPluginLoader.enablePlugin(JavaPluginLoader.java:351) [spigot-1.15.2.jar:git-Spigot-a03b1fd-fc318cc]
            at org.bukkit.plugin.SimplePluginManager.enablePlugin(SimplePluginManager.java:480) [spigot-1.15.2.jar:git-Spigot-a03b1fd-fc318cc]
            at org.bukkit.craftbukkit.v1_15_R1.CraftServer.enablePlugin(CraftServer.java:464) [spigot-1.15.2.jar:git-Spigot-a03b1fd-fc318cc]
            at org.bukkit.craftbukkit.v1_15_R1.CraftServer.enablePlugins(CraftServer.java:378) [spigot-1.15.2.jar:git-Spigot-a03b1fd-fc318cc]
            at net.minecraft.server.v1_15_R1.MinecraftServer.a(MinecraftServer.java:457) [spigot-1.15.2.jar:git-Spigot-a03b1fd-fc318cc]
            at net.minecraft.server.v1_15_R1.DedicatedServer.init(DedicatedServer.java:274) [spigot-1.15.2.jar:git-Spigot-a03b1fd-fc318cc]
            at net.minecraft.server.v1_15_R1.MinecraftServer.run(MinecraftServer.java:784) [spigot-1.15.2.jar:git-Spigot-a03b1fd-fc318cc]
            at java.lang.Thread.run(Thread.java:832) [?:?]
     

    Like I said, Im almost positive I am getting thrown a null pointer because I run the 'returnFlatFile(...)' method in the constructor, which [should] instantiate a FileConfiguration object which I call in the next line of the constructor, so I can only suspect that the 'returnFlatFile(...)' method is not assigning the FileConfiguration to the instance variable I want.

    Also, if there is a better way to handle async tasks, please let me know, but I've heard CompletableFuture is the way to go.

    Here's the entire class:
    Code (Java):
    package net.il0c4l.seasons.storage;

    import net.il0c4l.seasons.Main;
    import org.bukkit.configuration.ConfigurationSection;
    import org.bukkit.configuration.file.FileConfiguration;
    import org.bukkit.configuration.file.YamlConfiguration;
    import java.io.File;
    import java.io.IOException;
    import java.util.ArrayList;
    import java.util.UUID;
    import java.util.concurrent.CompletableFuture;
    import java.util.logging.Level;
    import java.util.logging.Logger;

    public class FlatDataHandler extends DataHandler {

        private FileConfiguration data;
        private Main plugin;
        private File file;
        private final Logger logger;

        public FlatDataHandler(final Main plugin, String fileName) {
            super(plugin);
            this.plugin = plugin;
            logger = plugin.getLogger();
            returnFlatFile(fileName);
            if (!data.contains("UUID")) {                   /*This is where the null pointer is thrown which makes
                data.createSection("UUID");                sense because it is the first time I call 'data' */

            }
            saveEntries();
        }

        protected void returnFlatFile(String fileName) {
            CompletableFuture.supplyAsync(() -> {
                file = new File(plugin.getDataFolder().getPath(), fileName);
                if (!file.exists()) {
                    logger.log(Level.INFO, NEW_FILE);
                    try {
                        file.createNewFile();
                        logger.log(Level.INFO, "Done.");
                    } catch (IOException e) {
                        e.printStackTrace();
                        logger.log(Level.SEVERE, IO_ERROR);
                    }
                }
                return YamlConfiguration.loadConfiguration(file);
            }, plugin.getThreadPool()).thenAcceptAsync(config -> data = config, plugin.getThreadPool());
        }

        protected FileConfiguration returnFlatFile_test(String fileName){
            file = new File(plugin.getDataFolder().getPath(), fileName);
            if (!file.exists()) {
                logger.log(Level.INFO, NEW_FILE);
                try {
                    file.createNewFile();
                    logger.log(Level.INFO, "Done.");
                } catch (IOException e) {
                    e.printStackTrace();
                    logger.log(Level.SEVERE, IO_ERROR);
                }
            }
            return YamlConfiguration.loadConfiguration(file);
        }

        protected void saveEntries() {
            CompletableFuture.runAsync(() -> {
                try {
                    data.save(file);
                } catch (IOException e) {
                    logger.log(Level.SEVERE, IO_ERROR);
                }
            }, plugin.getThreadPool());
        }

        @Override
        protected void getEntriesFromStorage() {
            CompletableFuture.supplyAsync(() -> {
                ArrayList<Entry> entries = new ArrayList<>();
                for (String sec : data.getConfigurationSection("UUID").getKeys(false)) {
                    String command = data.getString("UUID." + sec + ".command");
                    int progress = data.getInt("UUID." + sec + ".progress");

                    Entry entry = new Entry(UUID.fromString(sec), command, progress);
                    entries.add(entry);
                }
                return entries;
            }, plugin.getThreadPool()).thenApply(lst -> super.entries = lst);
        }

        @Override
        public void syncEntries() {
            CompletableFuture.runAsync(() -> {
                for (Entry entry : super.entries) {
                    ConfigurationSection sec = data.getConfigurationSection("UUID." + entry.getUUID().toString());
                    sec.set("command", entry.getCommand());
                    sec.set("progress", entry.getProgress());
                }
                saveEntries();
            }, plugin.getThreadPool());
        }



        private static final String NEW_FILE = "No flat storage file found! I'm going to try to create one for you...";
        private static final String IO_ERROR = "Something has gone wrong while handling a storage file! Check write permissions maybe?";
    }

    Super class:
    Code (Java):
    package net.il0c4l.seasons.storage;

    import net.il0c4l.seasons.Main;
    import org.bukkit.configuration.file.FileConfiguration;

    import java.util.ArrayList;
    import java.util.UUID;


    public abstract class DataHandler {

        private Main plugin;
        private static final String[] DATA_TYPES = {"mysql", "yaml", "sqlite"};

        protected ArrayList<Entry> entries;

        public DataHandler(final Main plugin){
            entries = new ArrayList<>();
            this.plugin = plugin;

        }

        public static String getStorageType(FileConfiguration config){
            for(String str : config.getConfigurationSection("storage.type").getKeys(false)){
                if(config.getBoolean("storage.type." + str)){
                    for(int i=0; i<DATA_TYPES.length; i++){
                        if(str.equalsIgnoreCase(DATA_TYPES[i])){
                            return str;
                        }
                    }
                }
            }
            return "default";
        }

        public boolean entryExists(Entry entry) {
            return entries.contains(entry);
        }

        public boolean entryExists(UUID uuid){
            for(Entry entry : entries){
                if(entry.getUUID().equals(uuid)){
                    return true;
                }
            }
            return false;
        }

        public void addEntry(Entry entry){
            entries.add(entry);
        }

        public Entry getEntry(UUID uuid){
            if(indexOfEntry(uuid) == -1){
                return null;
            }
            return entries.get(indexOfEntry(uuid));
        }

        protected int indexOfEntry(UUID uuid){
            int found = -1;
            for(int i=0; i<entries.size(); i++){
                if(entries.get(i).getUUID().equals(uuid)){
                    found = i;
                }
            }
            return found;
        }

        protected int indexOfEntry(Entry entry){
            int found = -1;
            for(int i=0; i<entries.size(); i++){
                if(entries.get(i).equals(entry)){
                    found = i;
                }
            }
            return found;
        }

        public void updateEntry(Entry entry){
            entries.forEach(iter -> {
                if(iter.equals(entry)){
                    iter = entry;
                }
            });
        }

        protected abstract void getEntriesFromStorage();
        public abstract void syncEntries();
    }

    Main class:
    Code (Java):
    package net.il0c4l.seasons;

    import net.il0c4l.seasons.listener.EntityDeathListener;
    import net.il0c4l.seasons.listener.PlayerLoginListener;
    import net.il0c4l.seasons.storage.DataHandler;
    import net.il0c4l.seasons.storage.FlatDataHandler;
    import net.il0c4l.seasons.storage.MySQLDataHandler;
    import net.il0c4l.seasons.storage.SQLiteDataHandler;
    import org.bukkit.plugin.java.JavaPlugin;
    import java.util.Map;
    import java.util.concurrent.*;
    import java.util.logging.Level;

    public class Main extends JavaPlugin {

        private static int T_NUM;
        private Executor executor;
        private ChallengeHandler challengeHandler;
        private DataHandler storage;

        @Override
        public void onEnable(){
            saveDefaultConfig();
            executor = Executors.newFixedThreadPool(T_NUM=5);
            challengeHandler = new ChallengeHandler(this);
            setStorageType();
            new EntityDeathListener(this);
            new PlayerLoginListener(this);
            aEntrySync();
        }

        @Override
        public void onDisable(){

        }

        public void setStorageType(){
            String type = DataHandler.getStorageType(getConfig()).toLowerCase();
            switch(type){
                case "mysql":
                    Map<String, Object> creds = getConfig().getConfigurationSection("storage.mysql").getValues(false);
                    storage = new MySQLDataHandler(this, (String) creds.get("username"), (String) creds.get("password"), (String) creds.get("host"), (String) creds.get("port"), (String) creds.get("dbname"), (String) creds.get("useSSL"));
                    getLogger().log(Level.INFO, "CONNECTED");
                    break;
                case "sqlite":
                    storage = new SQLiteDataHandler(this);
                    break;
                case "yaml":
                    storage = new FlatDataHandler(this, "data.yml");
                    break;
                case "default":
                    getLogger().log(Level.WARNING, "No storage type chosen. Defaulting to flat file storage!");
                    getConfig().set("storage.yaml.type", true);
                    storage = new FlatDataHandler(this, "data.yml");
                    type = "yaml";
                    break;
            }
            getLogger().log(Level.INFO, "Set storage type to " + type);
        }

        public Executor getThreadPool(){ return executor; }

        public ChallengeHandler getChallengeHandler(){
            return challengeHandler;
        }

        public DataHandler getDataHandler(){
            return storage;
        }

        public int getNumThreads(){ return T_NUM; }

        private void aEntrySync(){
            Runnable run = new Runnable() {
                @Override
                public void run(){
                    try{
                        Thread.sleep(15000L);
                    } catch(InterruptedException e){
                        e.printStackTrace();
                    } finally{
                        storage.syncEntries();
                        getLogger().log(Level.INFO, "SYNCED");
                    }
                }
            };

            ExecutorService executorService = Executors.newSingleThreadExecutor();
            executorService.submit(() -> {
                while(isEnabled()) { run.run(); }
            });

        }
    }

    Probably did not have to show all these classes, but I thought I would post all (my) classes included in the stacktrace and the DataHandler abstract class.

    Thanks in advance!
     
  2. SteelPhoenix

    Moderator

    Is there any particular reason you're working with futures rather than Bukkit's provided scheduler? Also, you're trying to use the data field while the future isn't even processed yet.
     
  3. So the future hasn't completed before I call the object it instantiates?

    No particular reason, it was the first option I have explored. Is it better to use BukkitScheduler?