Class TaskManager

java.lang.Object
com.ultikits.ultitools.manager.TaskManager

@Internal public class TaskManager extends Object
Manages scheduled tasks for plugin modules.

Scans beans for Scheduled annotated methods and registers them as Bukkit tasks. Automatically cancels all tasks when a plugin is unloaded.

Thread-confinement assumption (IN-02, gate-1 review, 16-REVIEW-residue.md): pluginTasks/externalTasks/coreTasks are plain, non-concurrent collections (HashMap/ArrayList). scanAndSchedule(UltiToolsPlugin, Object, Consumer) mutates them synchronously, once per successfully-scheduled task, inside its own scan loop (#410) - every current call site (PluginManager.onPluginRegistered, registerScheduledMethodsCore(Object), registerScheduledMethodsExternal(String, Object)) runs during plugin loading on the main thread, so this is safe today, but nothing enforces it. A future caller invoking any registerScheduledMethods* entry point from an async context would race these collections with no compile-time or runtime signal - the same main-thread-only contract GuiScheduler's own class javadoc documents explicitly for its collections.

Config-bound tasks (#531): a Scheduled method whose period or delay is bound to a module config key is scheduled from the key's value (seconds, times 20) and kept as a TaskManager.BoundTask handle in boundTasks, so rescheduleBound(UltiToolsPlugin) can find that one method's task again after /ul reload. A method with no binding takes exactly the pre-#531 path -- same scheduler call, same bucket, same log line.

Since:
6.2.0
  • Field Details

    • pluginTasks

      private final Map<UltiToolsPlugin,List<org.bukkit.scheduler.BukkitTask>> pluginTasks
    • externalTasks

      private final Map<String,List<org.bukkit.scheduler.BukkitTask>> externalTasks
    • coreTasks

      private final List<org.bukkit.scheduler.BukkitTask> coreTasks
      Tasks owned by the framework itself rather than by a plugin module or an external plugin.

      A framework-owned object belongs to no SimpleContainer, so it is reached by neither registerScheduledMethods(UltiToolsPlugin, Object) nor registerScheduledMethodsExternal(String, Object) -- both of which iterate a container's beans. Before 6.3.0 there was no third bucket, which is why PlayerCacheManager.sweepExpiredEntries() carried a @Scheduled annotation that was never registered and never ran (#384).

      Since:
      6.3.0
    • boundTasks

      private final Map<UltiToolsPlugin,List<TaskManager.BoundTask>> boundTasks
      Config-bound tasks, per module, as handles that remember their method, their binding, their current timing and their last run -- the per-method record the three buckets above do not keep. Only modules can bind, so there is no external or core counterpart.
      Since:
      6.3.0
    • hostPlugin

      private final org.bukkit.plugin.java.JavaPlugin hostPlugin
  • Constructor Details

    • TaskManager

      public TaskManager(org.bukkit.plugin.java.JavaPlugin hostPlugin)
  • Method Details

    • registerScheduledMethods

      public void registerScheduledMethods(UltiToolsPlugin plugin, Object bean)
      Scan a bean for Scheduled methods and register them as Bukkit tasks.
      Parameters:
      plugin - the owning plugin module
      bean - the bean instance to scan
    • registerScheduledMethodsCore

      public void registerScheduledMethodsCore(Object bean)
      Scan a framework-owned object for Scheduled methods and register them.

      The other two entry points key their tasks on a plugin module or an external plugin, and both are reached by iterating a SimpleContainer's beans. An object the framework constructs directly -- PlayerCacheManager, for instance -- belongs to no container and is therefore reached by neither, which is why its @Scheduled method silently never ran before 6.3.0 (#384). This is the third bucket, for objects the framework owns.

      Callers must not invoke this ad hoc. The set of framework-owned types whose scheduled methods are registered is declared by PluginManager.FRAMEWORK_SCHEDULED_OWNER_TYPES and enforced by FrameworkScheduledWiringTest; adding an object here without adding its type there would reintroduce exactly the drift that guard exists to prevent.

      Parameters:
      bean - the framework-owned instance to scan
      Since:
      6.3.0
    • cancelAllCore

      public void cancelAllCore()
      Cancel every framework-owned scheduled task.

      Called from PluginManager.close(), which UltiTools.onDisable() invokes. A repeating task that is not cancelled here survives a /reload and a second copy is scheduled on the next enable.

      Since:
      6.3.0
    • scanAndSchedule

      private void scanAndSchedule(UltiToolsPlugin module, Object bean, Consumer<org.bukkit.scheduler.BukkitTask> recorder)
      Scan one bean for Scheduled methods and schedule each valid one, recording each task in its owning bucket as soon as it is created rather than batching them into a list returned at the end.

      The single implementation behind all three registration entry points, which differ only in which bucket (recorder) they file the resulting tasks under. Three copies of this body would be three places to keep a signature check, a scheduling rule or a log line in step.

      #410: before this, tasks were accumulated in a local list and handed to the owning bucket only via created.addAll(...) in the CALLER, after this method returned. If scheduling a later Scheduled method threw (e.g. runTaskTimer throwing because the host plugin became disabled mid-registration), every task already scheduled for earlier methods in this same scan was already a live Bukkit task, but the exception meant this method never returned, so created never reached any bucket -- cancelAll/cancelAllExternal/cancelAllCore had no record of them and could never cancel them at teardown. Calling recorder inside the loop, immediately after each task is scheduled, means an exception from a later method can no longer un-record an earlier one: whatever the loop reached before throwing is already exactly where it needs to be for teardown to find it. This also restores the original, pre-three-bucket-split behaviour of recording each task as it was scheduled, one at a time. #531: a config-bound method is handed to scheduleBound(com.ultikits.ultitools.abstracts.UltiToolsPlugin, java.lang.Object, java.lang.Class<?>, java.lang.reflect.Method, com.ultikits.ultitools.annotations.Scheduled) instead and recorded in boundTasks as soon as it is scheduled, by the same #410 rule. Only a module has a config registry to bind against, so a bound method reached with no module (an external plugin's or a framework-owned bean) is refused.

      Parameters:
      module - the owning module, or null for an external plugin or the framework
      bean - the instance to scan
      recorder - invoked once per successfully scheduled task, in declaration order, with that task -- files it into this bean's owning bucket immediately
      Throws:
      PluginModuleException - for a config-bound method with no module, or for a binding that cannot be resolved
      Since:
      6.3.0
    • cancelAll

      public void cancelAll(UltiToolsPlugin plugin)
      Cancel all scheduled tasks for a plugin.
      Parameters:
      plugin - the plugin to cancel tasks for
    • scheduleBound

      private void scheduleBound(UltiToolsPlugin module, Object bean, Class<?> targetClass, Method method, Scheduled scheduled)
      Schedules one config-bound Scheduled method from its resolved binding and records its handle in boundTasks immediately (#410).
      Throws:
      PluginModuleException - if the binding cannot be resolved
      ConfigurationException - if a bound value is out of range
    • arm

      private org.bukkit.scheduler.BukkitTask arm(TaskManager.BoundTask handle, long firstDelay)
      Schedules handle's method with its current timing, first running after firstDelay ticks. Each call builds a new runnable -- a BukkitRunnable can be scheduled only once. Always on the main thread: a bound task cannot be async (ConfigBindings.checkShape(String, Scheduled)), so its run is observed exactly where the reload step also runs.
    • rescheduleBound

      public void rescheduleBound(UltiToolsPlugin plugin)
      Applies plugin's reloaded configuration to its config-bound Scheduled tasks. Called by UltiToolsPlugin.reloadSelf(), through PluginManager.applyReloadedConfigBindings, only after the configuration reload succeeded.

      For each bound task whose bound value changed:

      • it has run before: the next run is last run + new period, or the next tick if that moment has already passed;
      • it has not run yet: the first run is arm tick + new delay, or the next tick if that moment has already passed.
      So a reload never runs a task early and never postpones it by restarting its clock -- a 30-minute interest payment is neither paid on reload nor pushed back by repeated reloads. A task whose bound values did not change is left alone. A bound value that is now out of range (< 1 second, null, or too large) is not applied: the running value is kept and one WARNING names the module, the key, the rejected value and the value kept.

      Where "the last run" comes from. A bound task is always sync, so it runs on the main thread, and its last run is observed at the moment it starts -- the same thread this step runs on, so the two cannot race. An async binding is refused at load instead of predicting when the server dispatches async work: that prediction was wrong for tasks armed at boot (#531 gate-1 round 2, WR-01).

      Must run on the main thread, where TaskManager's collections live; from any other thread it throws before reading them (PluginManager checks first and logs instead).

      Parameters:
      plugin - the module whose configuration was just reloaded
      Since:
      6.3.0
    • rescheduleOne

      private void rescheduleOne(UltiToolsPlugin plugin, TaskManager.BoundTask handle)
    • reloadedTicks

      private static long reloadedTicks(UltiToolsPlugin plugin, TaskManager.BoundTask handle, ConfigBindings.Source source, long currentTicks, Set<String> warnedKeys)
      Returns:
      the reloaded value of source in ticks, or currentTicks if it is unbound or its new value is out of range (then warned once per key)
    • validateConfigBindings

      static void validateConfigBindings(UltiToolsPlugin module, Object bean)
      Load-time check of every config-bound Scheduled method on bean, over exactly the methods scanAndSchedule(com.ultikits.ultitools.abstracts.UltiToolsPlugin, java.lang.Object, java.util.function.Consumer<org.bukkit.scheduler.BukkitTask>) would schedule. Used by PluginManager before any Bukkit side effect, so an invalid binding refuses the one module (#531).
      Parameters:
      module - the module being assembled
      bean - one of its container singletons
      Throws:
      PluginModuleException - if a binding cannot be resolved
      ConfigurationException - if a bound value is out of range
      Since:
      6.3.0
    • firstBoundMethod

      static String firstBoundMethod(Object bean)
      Parameters:
      bean - a container singleton
      Returns:
      Class.method of the first config-bound Scheduled method that scanAndSchedule(com.ultikits.ultitools.abstracts.UltiToolsPlugin, java.lang.Object, java.util.function.Consumer<org.bukkit.scheduler.BukkitTask>) would schedule, or null
      Since:
      6.3.0
    • refuseConfigBindings

      static void refuseConfigBindings(Object bean)
      Refuses any config-bound Scheduled method on bean -- for a container with no module config registry (an external plugin's).
      Parameters:
      bean - one of the container's singletons
      Throws:
      PluginModuleException - naming the first bound method found
      Since:
      6.3.0
    • checkSignatureOrWarn

      private static boolean checkSignatureOrWarn(Class<?> targetClass, Method method)
      Returns:
      true if method takes no parameters and returns void
    • isSchedulableSignature

      private static boolean isSchedulableSignature(Method method)
    • bindingOutsideModule

      private static PluginModuleException bindingOutsideModule(Class<?> targetClass, Method method)
    • getTargetClass

      private Class<?> getTargetClass(Class<?> clazz)
      Get the original class, unwrapping proxies generated by ProxyFactory.

      Delegates entirely to ProxyFactory.unwrap(Class) - proxy identity is owned by that class, not derived here. A proxy of a proxy already carries a ProxyOf marker naming the original target, so this needs no hierarchy walk and no Object.class fallback: unwrap returns a non-proxy argument unchanged, which is the same answer the removed fallback was approximating for the common case.

    • registerScheduledMethodsExternal

      public void registerScheduledMethodsExternal(String pluginName, Object bean)
      Scan a bean for Scheduled methods and register them for an external plugin.
      Parameters:
      pluginName - the external plugin name (used as key)
      bean - the bean instance to scan
      Since:
      6.2.2
    • cancelAllExternal

      public void cancelAllExternal(String pluginName)
      Cancel all scheduled tasks for an external plugin.
      Parameters:
      pluginName - the external plugin name
      Since:
      6.2.2
    • getTaskCount

      int getTaskCount(UltiToolsPlugin plugin)
      Get the number of registered tasks for a plugin (for testing).
    • getCoreTaskCount

      int getCoreTaskCount()
      Get the number of registered framework-owned tasks (for testing).
      Since:
      6.3.0
    • getExternalTaskCount

      int getExternalTaskCount(String pluginName)
      Get the number of registered tasks for an external plugin (for testing).