Truly reloading a plugin

Discussion in 'Spigot Plugin Development' started by titivermeesch, Oct 7, 2019.

  1. Hi there,

    So I've been working on a plugin for a long time but how do I completely reload my plugin? Not only the config.
    This is because I have Bukkit scheduler running and just canceling them don't work. The only thing that seems to fix it is to do /reload and then it will cancel all the current tasks.
    So how do I disable and enable it again?
     
  2. you can use reflections and access the insides of javapluginloader and do it from there
     
  3. You need to be able to have a manager or some sort of class that's able to reference these tasks. You will need to cancel them and restart them. All reloading has to be done by you so whatever you want to reload, you have to design your classes with that functionality in mind
     
  4. I used the cancelTasks() function inside Bukkit but that don't work, it stops them from doing what they do, but once I run the function again that started those tasks, the same task starts again with the same information I gave in the past.
     
  5. How would that work then? Didn't do a lot of stuff with reflections so yeah...
     
  6. Gadse

    Gadse Previously GummiBoat

    I don't think you should fully reload the entire plugin with reflection, but rather properly clear your tasks and variables that might store any information you want to be reset.

    You can cancel all tasks from a plugin with BukkitScheduler#cancelTasks. Your issue then should only be to reset any variables you might have. The optimal way to achieve this, would be to have an object which represents your variables and re-initializing it. Of course, just having final default variables which you set them back to would work too.
     
    • Agree Agree x 1
  7. But cancelTasks should cancel everything right? I don't store anything in variables. I just start a tasks and then cancel everything, but they just get paused.

    I use
    Bukkit.getScheduler().scheduleSyncRepeatingTask
    and that's all
     
  8. Gadse

    Gadse Previously GummiBoat

    It cancels all tasks your plugin started, yes. If you don't have any external data that provides the task with information, it will not retain information from old tasks (since the old one doesn't get resumed, but a new one gets created). I don't know which API version you're working with, but in newer versions, you should use BukkitRunnable#runTaskTimer.
     
  9. That's what I did. I'm stopping all tasks. Reloading the config, and run those tasks again with the proper config and he just restarts the old timers, or maybe my config isn't reloading correctly then?
     
  10. Gadse

    Gadse Previously GummiBoat

    The only way to find out is debugging. Log things every time you reload. If you're stuck somewhere, show a code snippet and we may be able to help further.
     
  11. If you're looking only to reload your own plugin, you should use BukkitRunnables as this:
    instance = new BukkitRunnable() {
    public void run() {
    //your code
    }
    }.runTaskTimer(plugin, 1, 1);

    and then you can onDisable use instance.cancel();
     
  12. You should not use reflection for this. Reflection is an extremely powerful concept that has a great many valid, useful applications, but this isn't one of them. Using reflection here is a recipe for disaster and not in any way justified.

    To truly "reload" something, you have to know what that something does, and how to undo it. A good rule of thumb when dealing with any sort of resource is to always keep in mind how you would go about deallocating it once you have allocated it. A classic example in this community is the idea of cooldowns, where you keep a reference to something, e.g. a player, in a map along with the time that they "did the thing" (alternatively, when they are allowed to do that thing again). The suggestions will have you call put() on the map, but a lot of them omit the remove() counterpart, which leads to memory leaks - a player who visits your server, triggering the cooldown, and then leaving, will leave a trace in your map that will only be removed if you actively remove it or "reload" your plugin, leaving the removal of the references to the garbage collector.

    Your problem is a generalization of the cooldown problem. When you start a task, you need to keep track of it if you ever want to remove it again. The concept of a "manager", as @DiamondDagger590 mentions, establishes the sort of "bookkeeping" you need. But that manager then also need to be kept track of. The whole thing boils down to a hierarchy of management, where the act of "reloading" needs to trigger a cascading cleanup throughout your code base. But you can start off with something a little more flat.

    ---

    Let's assume you have a class, HydrationReminder, which is responsible for reminding people to stay hydrated (you should write this plugin now and release it when summer rolls back around). It might look like this:
    Code (Java):
    class HydrationReminder {
      private static final long INTERVAL = 5 * 60 * 20; // 5 minutes
      private final Plugin plugin;

      public HydrationReminder(Plugin plugin) {
        this.plugin = plugin;
      }

      public void start() {
        plugin.getServer().getScheduler().scheduleSyncRepeatingTask(plugin, () -> {
          // Broadcast to all online players here
        }, INTERVAL, INTERVAL);
      }
    }
    Then in your main plugin class, you instantiate it and call its start() method:
    Code (Java):
    class SummerTime extends JavaPlugin {
      @Override
      public void onEnable() {
        new HydrationReminder(this).start();
      }
    }
    Right, so when the plugin is enabled, the hydration reminder is started, and all is well. Now you want to reload the whole thing. In this case, a clean reload would have you cancel the task, but you don't have any reference to it. You can use the cancelTasks(Plugin) method on the BukkitScheduler, but you don't always have that kind of luxury built for you, so let's assume it doesn't exist. What does exist is a return value on the scheduling method. So if we modify the HydrationReminder and add a stop() method, we have the ability to actually clean up after ourselves:
    Code (Java):
    class HydrationReminder {
      private static final long INTERVAL = 5 * 60 * 20; // 5 minutes
      private final Plugin plugin;
      private int taskId;

      public HydrationReminder(Plugin plugin) {
        this.plugin = plugin;
      }

      public void start() {
        taskId = plugin.getServer().getScheduler().scheduleSyncRepeatingTask(plugin, () -> {
          // Broadcast to all online players here
        }, INTERVAL, INTERVAL);
      }

      public void stop() {
        plugin.getServer().getScheduler().cancelTask(taskId);
      }
    }
    Let's assume the cascading reload is invoked on the main plugin class, which means it needs a reload() method. It also needs to maintain a reference to the hydration reminder to be able to call the stop method on it:
    Code (Java):
    class SummerTime extends JavaPlugin {
      private HydrationReminder reminder;

      @Override
      public void onEnable() {
        this.reminder = new HydrationReminder(this);
        this.reminder.start();
      }

      public void reload() {
        this.reminder.stop();
      }
    }
    Now, this obviously won't actually reload the plugin correctly, because all it does is stop the reminder. It doesn't restart it as it should. If we modify the structure a little bit by introducing the idea of "starting" and "stopping" to the main plugin class, while maintaining the idea of "setting up" in the onEnable method, we can realize the "reload" concept by simply stopping and then starting the reminder:
    Code (Java):
    class SummerTime extends JavaPlugin {
      private HydrationReminder reminder;

      @Override
      public void onEnable() {
        this.reminder = new HydrationReminder(this);
        start();
      }

      public void reload() {
        stop();
        start();
      }

      private void start() {
        reminder.start();
      }

      private void stop() {
        reminder.stop();
      }
    }
    You will want to scatter a few sanity checks around the code, just to be sure you're not triggering some nasty exceptions (check if reminder is not null, check if the taskId is positive before cancelling it, etc.), but hopefully this goes to show how you can approach this sort of thing.

    As for "lingering state", you can get around that by properly "cleaning up" any references to other objects and values. In some cases, it's a lot easier to leave whatever cleanup you can to the garbage collector and just instantiate new objects (here it would be a new instance of HydrationReminder), but it's important that you clean up whatever the garbage collector won't clean up for you, such as event listener registrations and running tasks. By taking the more manual approach, however, you retain full control over what happens when you reload. Keep track of your state :)
     
    • Informative Informative x 1
  13. Thanks a lot for this information, I'll try to apply this and see how it goes