Class PlayerCacheManager
Three field shapes are recognised: a Map keyed by UUID (the entire entry --
including a nested value, e.g. Map<UUID, Map<String, Long>> -- is removed on quit), a
Set<UUID> (the UUID itself is removed), and a Map whose VALUE is a
UUID (every entry whose value equals the quitting player's UUID is removed, other
players' entries are left intact). A field whose generic shape cannot be resolved to one of
these three is refused at registration time -- an annotation that cannot take effect fails
loudly rather than being silently skipped.
A non-bean instance -- one that is never resolved from any SimpleContainer, e.g. a
command validator new-ed directly in a constructor -- can register itself via tryRegister(Object) even before the core plugin is enabled; see that method's javadoc.
- Since:
- 6.2.0
-
Nested Class Summary
Nested ClassesModifier and TypeClassDescriptionstatic interfaceOpt-in contract for a tracked instance that also carries time-based (as opposed to player-quit-based) state to prune.private static enumThe three field shapesregisterBean(Object)can sweep.private static classprivate static class -
Field Summary
FieldsModifier and TypeFieldDescription(package private) static final longPeriod, in ticks, between two consecutive expiry sweeps (sweepExpiredEntries()).private static final Loggerprivate final List<PlayerCacheManager.TrackedBean> -
Constructor Summary
Constructors -
Method Summary
Modifier and TypeMethodDescriptionprivate static PlayerCacheManager.FieldShapeclassifyField(Field field) Classifies aPlayerCache-annotated field's generic shape.intReturns the number of beans being tracked.private booleanprivate static booleanprivate static voidlogQuitFailure(String action, PlayerCacheManager.TrackedBean tracked, PlayerCacheManager.TrackedField tf, UUID playerId, Throwable failure) voidonPlayerQuit(UUID playerId) Called when a player quits.voidregisterBean(Object bean) Scans a bean for @PlayerCache fields and tracks them.private static PlayerCacheManagervoidPeriodic, time-based expiry sweep -- driven by the framework's ownScheduled(i.e.private static voidsweepField(Object value, PlayerCacheManager.FieldShape shape, UUID playerId) static voidtryRegister(Object instance) Attempts lazy first-use registration of a non-bean instance with the livePlayerCacheManagersingleton.static voidtryUnregister(Object instance) The static counterpart totryRegister(Object)-- unregisters a previously registered non-bean instance from the livePlayerCacheManagersingleton.voidunregisterBean(Object bean) Removes a bean from tracking.
-
Field Details
-
LOGGER
-
EXPIRY_SWEEP_PERIOD_TICKS
static final long EXPIRY_SWEEP_PERIOD_TICKSPeriod, in ticks, between two consecutive expiry sweeps (sweepExpiredEntries()).Chosen conservatively: this task runs for the entire lifetime of a long-running server, and the per-player state it exists to bound (command cooldowns, usage locks) is expressed in seconds-to-minutes, not milliseconds -- there is no correctness requirement for the sweep to run more often than once every few minutes. 6000 ticks is 5 minutes at the server's nominal 20 ticks/second, which keeps the sweep's own overhead negligible (an empty or small registry costs microseconds per pass) while bounding the worst-case staleness of any expired entry to 5 minutes.
- See Also:
-
trackedBeans
-
-
Constructor Details
-
PlayerCacheManager
public PlayerCacheManager()
-
-
Method Details
-
tryRegister
Attempts lazy first-use registration of a non-bean instance with the livePlayerCacheManagersingleton.Accepts any object -- it does not need to be a container-managed bean; this is the entry point objects that are
new-ed directly (e.g. a command validator constructed insideBaseCommandExecutor's constructor) use to reach the sweep mechanism, sincePluginManager's bean-iteration registration loops only ever see objects resolved from a plugin'sSimpleContainer.Safe to call before the core plugin is enabled. If
UltiTools.getInstance()(or itsPluginManager/PlayerCacheManager) is not yet available -- for example, a barenew MyCommand()constructed in a unit test with no running server -- this method returns without throwing and without registering anything. It is intended to be retried on a later first-use once the core plugin is available; degrading gracefully to "never registered, never swept" in a pure-unit-test context is deliberate (D-03).Idempotent per instance. Calling this method more than once with the same instance (by reference identity, not
equals) results in exactly one tracked entry and one sweep per player quit -- seeregisterBean(Object).The caller owns unregistration. This method has no matching automatic teardown; a caller that registers itself is responsible for calling
tryUnregister(Object)on its own teardown, so the manager does not keep tracking an instance whose owner has gone away.- Parameters:
instance- the object to register; scanned forPlayerCache-annotated fields- Since:
- 6.3.0
-
tryUnregister
The static counterpart totryRegister(Object)-- unregisters a previously registered non-bean instance from the livePlayerCacheManagersingleton.Safe to call before the core plugin is enabled or after it has been disabled: if the live manager cannot be resolved, this is a no-op. Also safe to call on an instance that was never registered (e.g. because an earlier
tryRegister(Object)attempt found the core plugin unavailable) --unregisterBean(Object)is itself a no-op in that case.- Parameters:
instance- the object to stop tracking- Since:
- 6.3.0
-
resolveLive
-
registerBean
Scans a bean for @PlayerCache fields and tracks them.Idempotent by reference identity: registering the same instance more than once is a no-op on the second and subsequent calls, so an instance never accumulates duplicate tracked entries or is swept more than once per player quit.
- Throws:
PluginModuleException- if aPlayerCache-annotated field's shape is none of the three this manager can sweep (aMapkeyed byUUID, aSet<UUID>, or aMapwhose value is aUUID) -- an annotation that cannot take effect must fail loudly rather than be silently skipped.
-
isTracked
-
classifyField
Classifies aPlayerCache-annotated field's generic shape.Java 8 erasure means the key-side and value-side
Mapcases cannot be told apart fromField.getType()alone -- this readsField.getGenericType()and its actual type arguments instead. Where the generic signature is absent or unresolvable (a raw type, or neither side isUUID), this returnsnullrather than guessing, and the caller refuses the field.- Parameters:
field- the field to classify- Returns:
- the recognised shape, or
nullif the field's shape is unsupported
-
isUuid
-
unregisterBean
Removes a bean from tracking. -
onPlayerQuit
Called when a player quits. Cleans up all tracked fields across all three supported shapes.For each tracked field, in turn: if the field is
@PlayerCache(saveBeforeRemove = true)and its bean implementsPlayerCacheSaver,PlayerCacheSaver.savePlayerData(UUID)runs first; then the quitting player's entry is removed from the field.Each field is isolated (as of 6.3.0, #643). A save or a removal that throws -- any
Exception, such as a module's database error or a checked exception the saver throws without declaring it (Lombok@SneakyThrows), or anErrorsuch as aNoClassDefFoundErrorfrom a module whose classes are gone, contained here as the module unload steps contain it -- is logged as one WARNING naming the bean class, the field and the player, and the cleanup continues with every other field of every tracked bean. The beans are visited from a snapshot, assweepExpiredEntries()does, so a saver that registers or unregisters a tracked object during the quit does not end the cleanup.A failed save does not keep the entry: the entry is removed anyway (maintainer decision, 2026-10-08), because
saveBeforeRemoveorders the save before the removal and does not make the removal conditional on it. The framework does not retry the save; a module that must not lose the data retries or persists it itself, insidesavePlayerData. Nothing is thrown to the caller (the framework's quit listener). Before 6.3.0 the first such failure ended the cleanup: that field's entry and the entries of every later field were left in memory, and the exception escaped into Bukkit's event dispatch.- Parameters:
playerId- the UUID of the player who quit
-
logQuitFailure
private static void logQuitFailure(String action, PlayerCacheManager.TrackedBean tracked, PlayerCacheManager.TrackedField tf, UUID playerId, Throwable failure) -
sweepField
-
getTrackedBeanCount
public int getTrackedBeanCount()Returns the number of beans being tracked. -
sweepExpiredEntries
public void sweepExpiredEntries()Periodic, time-based expiry sweep -- driven by the framework's ownScheduled(i.e.@Scheduled) mechanism rather than a hand-rolledBukkitRunnable, because this state is stale on a clock, not on a player-quit event: it must be pruned whether or not any player is online (D-03 -- mirrorsCooldownValidator.cleanupExpired's shape).Every tracked instance that implements
PlayerCacheManager.ExpiringPlayerCachehasPlayerCacheManager.ExpiringPlayerCache.sweepExpired()invoked once per pass. One participant's hook throwing -- an exception, or (as of 6.3.0, #643) anErrorsuch as aNoClassDefFoundError-- is caught, logged, and does not prevent the remaining participants' hooks from running in the same pass. An instance that does not implementPlayerCacheManager.ExpiringPlayerCacheis not visited at all -- opt-in by type, not by reflection guessing at method names. Tolerates an empty registry and being invoked mid-shutdown: it touches only this manager's own tracked list, never the Bukkit API directly.Sweep period: see
EXPIRY_SWEEP_PERIOD_TICKS(5 minutes -- see that constant's javadoc for the rationale).- Since:
- 6.3.0
-