Class CachedStorage

java.lang.Object
com.enhancedechest.storage.CachedStorage
All Implemented Interfaces:
EnderChestStorage

public final class CachedStorage extends Object implements EnderChestStorage
Lazy-loading write-back cache over the SQL backend, and the EnderChestStorage the rest of the plugin sees. A player's rows are read from SQL once, on first touch (normally the join prefetch; otherwise on demand inside whatever storage call missed — an admin command on an offline player, an expiry sweep, a migration), and every operation after that is served from memory with identical semantics to the old SQL (index allocation, primary fallback, transfer collision rules, targeted settings upserts).

This class is a thin façade holding only the ender-chest domain logic; the machinery is split into two collaborators in this package:

  • ChestCacheState — the pure in-memory row model (materialized rows + dirty tracking + the low-level map operations these methods build on).
  • OwnerResidencyCache — the residency/flush/eviction coherence engine (the single lock, load-on-miss protocol, write-back, and idle eviction). Every per-owner method below runs its body through OwnerResidencyCache.withOwner(java.util.UUID, java.util.function.Supplier<T>) so the owner is guaranteed resident for the duration; see that class for the load-bearing residency invariant and lock ordering.

Cross-server sharing: by default (the CrossServerCoordinator.NOOP coordinator) this cache is authoritative while an owner is resident and the quit write-back is delayed, so two servers pointed at the same database would overwrite each other on fast server switches — running multiple servers against one database is unsupported unless cross-server.enabled is on. In that mode a Redis-backed coordinator adds a distributed leg to the residency invariant (resident ⇒ this server holds the owner's lock; released only after the owner flushed clean), which is what makes the shared database safe. See OwnerResidencyCache and CrossServerCoordinator.

  • Nested Class Summary

  • Constructor Summary

    Constructors
    Constructor
    Description
    CachedStorage(StorageBackend backend, org.slf4j.Logger logger, Telemetry telemetry)
     
    CachedStorage(StorageBackend backend, org.slf4j.Logger logger, Telemetry telemetry, CrossServerCoordinator coordinator)
     
  • Method Summary

    Modifier and Type
    Method
    Description
    void
    backup(Path target)
    Writes a consistent snapshot of the entire database to target (which must not already exist).
    void
    clearChestContents(UUID owner, int index)
    Empties a chest's contents (sets the stored bytes to NULL), keeping its size, name, icon and kind.
    void
    Clears the primary flag from all of the player's chests, leaving them with no main chest.
    void
    Flushes everything still dirty, then closes the SQL backend.
    void
    completeMigration(UUID owner, byte[] containerData, @org.jetbrains.annotations.Nullable byte[] overflow, int tempSize, long tempExpiresAt)
    Commits a vanilla migration in one transaction: writes chest #1's contents and sets its migrated flag (one single-row UPDATE — chest #1's size is never changed), plus, when overflow is non-null, inserts a temp chest at the next free index carrying the vanilla items that did not fit.
    long
    Total number of rows in the enderchests table across every player and chest kind.
    int
    createChest(UUID owner, int size, @Nullable Long expiresAt)
    Creates a new NORMAL chest at the lowest free index.
    int
    createPermChest(UUID owner, int size)
    Creates a permission-granted chest (kind=PERM) at the lowest free index.
    void
    deleteChest(UUID owner, int index)
    Deletes a chest.
    void
    ensureChest(UUID owner, int index, int size)
    Creates a chest at a specific index if it does not already exist (used by migration).
    int
    Evicts every owner that is offline and fully flushed.
    findExpired(long now)
    Returns every chest whose expiry is set and at or before now.
    @Nullable UUID
    Resolves a stored in-game name to its UUID, case-insensitively, or null if no player with that name has been recorded.
    int
    Writes every dirty row back to SQL.
    void
    Writes one quitter's dirty rows back, then evicts them if clean and still offline.
    int
    Reads the persisted applied_default_size baseline — the base-chest size currently dictated by the player's enhancedechest.default_size.<size> permission, or 0 when the base chest is not permission-managed.
    int
    Returns the index of the chest /ec opens: the primary if one is flagged, otherwise the lowest-indexed chest.
    int[]
    Bulk-inserts raw players and enderchests rows verbatim into this (fresh, active) backend, in a single transaction.
    void
    Initializes the SQL backend (schema + migrations).
    boolean
    Returns true if the player's chest #1 has its migrated flag set.
    boolean
    isPinned(UUID owner)
    Whether an owner is currently pinned (online here).
    Returns the player's chests ordered by index.
    Returns every recorded player with a non-null username, with the epoch-ms they were last seen (0 when never recorded).
    @Nullable EnderChestData
    loadChest(UUID owner, int index)
    Loads a single chest, or null if no such (owner, index) row exists.
    Loads the player's settings, or PlayerSettings.defaults() if they have no row yet.
    void
    pin(UUID owner)
    Marks an owner online: their rows survive every eviction until unpin(java.util.UUID).
    boolean
    reclaimTemp(UUID owner, int tempIndex, int targetIndex)
    Moves a temp chest's contents into an empty chest and deletes the temp row, in one transaction — the inverse of a spill, used when a player is granted a new chest that can hold a temp chest whole (see ChestSpillService#reclaimTempInto).
    void
    recordPlayerSeen(UUID owner, String name, long lastOnline)
    Records that a player was seen: their current in-game name against their UUID (so name→UUID resolution works for offline players from the plugin's own data, with no server usercache or Mojang lookup), and lastOnline as epoch-ms.
    void
    renameChest(UUID owner, int index, @Nullable String name)
    Sets or clears a chest's custom display name (null resets to the default numbered title).
    void
    resizeChest(UUID owner, int index, int size)
    Changes a chest's slot count.
    void
    saveChest(UUID owner, int index, byte[] containerData)
    Updates the container bytes of an existing chest.
    void
    saveSettings(UUID owner, PlayerSettings settings)
    Upserts the player's editMode/appliedDefaultSize (whole-object save): updates the existing row, or inserts one if none exists.
    void
    setAppliedDefaultSize(UUID owner, int size)
    Targeted single-field upsert of just the applied_default_size baseline (no preceding read), leaving every other setting untouched.
    void
    setEditMode(UUID owner, boolean editMode)
    Targeted single-field upsert of just the edit-mode preference.
    void
    setIcon(UUID owner, int index, @Nullable String icon)
    Sets or clears a chest's icon (material key, e.g.
    void
    setMigrated(UUID owner, boolean migrated)
    Updates the migrated flag on the player's chest #1.
    void
    setPrimary(UUID owner, int index)
    Makes the given chest the player's primary, clearing the flag from all others.
    void
    spillRemove(UUID owner, int index, @org.jetbrains.annotations.Nullable byte[] items, int tempSize, long tempExpiresAt)
    Deletes a chest, optionally spilling its items into a new temp chest, in one transaction.
    void
    spillShrink(UUID owner, int index, int newSize, byte[] visible, @org.jetbrains.annotations.Nullable byte[] overflow, int tempSize, long tempExpiresAt)
    Shrinks a chest and spills any cut-off items into a new temp chest, in one transaction.
    boolean
    True if this backend can produce a file snapshot via EnderChestStorage.backup(Path).
    int
    transferChests(UUID from, UUID to, @Nullable Integer onlyIndex, Set<Integer> preserveDestIndices, long tempExpiresAt)
    Moves a player's NORMAL chests onto another player in a single transaction (the account-switch primitive behind /ee transfer).
    void
    unpin(UUID owner)
    Unmarks an owner as online.

    Methods inherited from class java.lang.Object

    clone, equals, finalize, getClass, hashCode, notify, notifyAll, toString, wait, wait, wait

    Methods inherited from interface com.enhancedechest.storage.EnderChestStorage

    createChest
  • Constructor Details

  • Method Details

    • init

      public void init()
      Initializes the SQL backend (schema + migrations). Player data is loaded lazily, per owner.
      Specified by:
      init in interface EnderChestStorage
    • close

      public void close()
      Flushes everything still dirty, then closes the SQL backend.
      Specified by:
      close in interface EnderChestStorage
    • pin

      public void pin(UUID owner)
      Marks an owner online: their rows survive every eviction until unpin(java.util.UUID).
    • unpin

      public void unpin(UUID owner)
      Unmarks an owner as online. Their rows stay resident until flushed clean and evicted.
    • isPinned

      public boolean isPinned(UUID owner)
      Whether an owner is currently pinned (online here). Used by the cross-server handover check.
    • flush

      public int flush()
      Writes every dirty row back to SQL. @return rows written/deleted (0 when nothing was dirty).
    • flushOwner

      public void flushOwner(UUID owner)
      Writes one quitter's dirty rows back, then evicts them if clean and still offline.
    • evictIdle

      public int evictIdle()
      Evicts every owner that is offline and fully flushed. @return the number evicted.
    • supportsBackup

      public boolean supportsBackup()
      Description copied from interface: EnderChestStorage
      True if this backend can produce a file snapshot via EnderChestStorage.backup(Path). Only the file-based SQLite backend supports it today; remote backends (MySQL/MariaDB/Postgres) return false and must be backed up with the database server's own tooling.
      Specified by:
      supportsBackup in interface EnderChestStorage
    • backup

      public void backup(Path target) throws Exception
      Description copied from interface: EnderChestStorage
      Writes a consistent snapshot of the entire database to target (which must not already exist). The snapshot is safe to take while players are saving — it does not interrupt writes. Only valid when EnderChestStorage.supportsBackup() is true.
      Specified by:
      backup in interface EnderChestStorage
      Throws:
      Exception - if the snapshot fails; the caller logs it and leaves the live DB untouched.
    • importRows

      public int[] importRows(List<EnderChestStorage.RawPlayerRow> players, List<EnderChestStorage.RawChestRow> chests)
      Description copied from interface: EnderChestStorage
      Bulk-inserts raw players and enderchests rows verbatim into this (fresh, active) backend, in a single transaction. Every column is copied as-is — including the container_data bytes — so no item (de)serialization happens; this is the DB→DB conversion primitive behind /ee import.

      Inserts are batched per table (one reused PreparedStatement each, flushed in chunks) with autocommit off, so the whole copy commits once. A duplicate primary key (or any other failure) rolls the whole transaction back and throws — the caller guards that the destination is empty first, so a duplicate signals a real problem rather than silently merging.

      Specified by:
      importRows in interface EnderChestStorage
      Returns:
      [playersInserted, chestsInserted]
    • countChests

      public long countChests()
      Description copied from interface: EnderChestStorage
      Total number of rows in the enderchests table across every player and chest kind. Used by /ee import to refuse converting into a non-empty destination (import only into a fresh DB).
      Specified by:
      countChests in interface EnderChestStorage
    • listChests

      public List<ChestSummary> listChests(UUID owner)
      Description copied from interface: EnderChestStorage
      Returns the player's chests ordered by index. Empty list if the player owns none.
      Specified by:
      listChests in interface EnderChestStorage
    • getPrimaryIndex

      public int getPrimaryIndex(UUID owner)
      Description copied from interface: EnderChestStorage
      Returns the index of the chest /ec opens: the primary if one is flagged, otherwise the lowest-indexed chest. Returns -1 if the player owns no chests.
      Specified by:
      getPrimaryIndex in interface EnderChestStorage
    • loadChest

      @Nullable public @Nullable EnderChestData loadChest(UUID owner, int index)
      Description copied from interface: EnderChestStorage
      Loads a single chest, or null if no such (owner, index) row exists.
      Specified by:
      loadChest in interface EnderChestStorage
    • findExpired

      public List<EnderChestStorage.ExpiredRef> findExpired(long now)
      Description copied from interface: EnderChestStorage
      Returns every chest whose expiry is set and at or before now.
      Specified by:
      findExpired in interface EnderChestStorage
    • saveChest

      public void saveChest(UUID owner, int index, byte[] containerData)
      Description copied from interface: EnderChestStorage
      Updates the container bytes of an existing chest. No-op if the row was deleted while open. Size, name and primary flag are never touched here.
      Specified by:
      saveChest in interface EnderChestStorage
    • createChest

      public int createChest(UUID owner, int size, @Nullable @Nullable Long expiresAt)
      Description copied from interface: EnderChestStorage
      Creates a new NORMAL chest at the lowest free index. If expiresAt is non-null the chest expires at that epoch-millis instant (an expirable granted chest); null = never expires. No chest is ever auto-flagged primary; the main chest is set only via EnderChestStorage.setPrimary(java.util.UUID, int).
      Specified by:
      createChest in interface EnderChestStorage
      Returns:
      the index assigned to the new chest
    • createPermChest

      public int createPermChest(UUID owner, int size)
      Description copied from interface: EnderChestStorage
      Creates a permission-granted chest (kind=PERM) at the lowest free index. PERM chests carry no expiry and are never auto-flagged primary; they are granted/removed by the reconcile of com.enhancedechest.service.PermissionChestService and are invisible to admin commands.
      Specified by:
      createPermChest in interface EnderChestStorage
      Returns:
      the index assigned to the new chest
    • ensureChest

      public void ensureChest(UUID owner, int index, int size)
      Description copied from interface: EnderChestStorage
      Creates a chest at a specific index if it does not already exist (used by migration). No-op if the row already exists. Never auto-flags primary.
      Specified by:
      ensureChest in interface EnderChestStorage
    • resizeChest

      public void resizeChest(UUID owner, int index, int size)
      Description copied from interface: EnderChestStorage
      Changes a chest's slot count. Caller validates size (multiple of 9, 9..54).
      Specified by:
      resizeChest in interface EnderChestStorage
    • deleteChest

      public void deleteChest(UUID owner, int index)
      Description copied from interface: EnderChestStorage
      Deletes a chest. No survivor is promoted: if the deleted chest was the main, the player simply has no main until they choose one again.
      Specified by:
      deleteChest in interface EnderChestStorage
    • clearChestContents

      public void clearChestContents(UUID owner, int index)
      Description copied from interface: EnderChestStorage
      Empties a chest's contents (sets the stored bytes to NULL), keeping its size, name, icon and kind.
      Specified by:
      clearChestContents in interface EnderChestStorage
    • spillShrink

      public void spillShrink(UUID owner, int index, int newSize, byte[] visible, @Nullable @org.jetbrains.annotations.Nullable byte[] overflow, int tempSize, long tempExpiresAt)
      Description copied from interface: EnderChestStorage
      Shrinks a chest and spills any cut-off items into a new temp chest, in one transaction. The original row is updated to newSize with visible as its new contents; if overflow is non-null a temp chest (kind=TEMP, appended after the highest index so it sorts last, expiring at tempExpiresAt) is inserted holding it. Never leaves items in two rows at once.
      Specified by:
      spillShrink in interface EnderChestStorage
      Parameters:
      tempSize - slot count of the temp chest created for the overflow (ignored if no overflow)
    • spillRemove

      public void spillRemove(UUID owner, int index, @Nullable @org.jetbrains.annotations.Nullable byte[] items, int tempSize, long tempExpiresAt)
      Description copied from interface: EnderChestStorage
      Deletes a chest, optionally spilling its items into a new temp chest, in one transaction. If items is non-null a temp chest holding them is inserted before the original row is deleted. No survivor is promoted to primary (the main is an explicit player choice).
      Specified by:
      spillRemove in interface EnderChestStorage
      Parameters:
      tempSize - slot count of the temp chest created for the items (ignored if items is null)
    • reclaimTemp

      public boolean reclaimTemp(UUID owner, int tempIndex, int targetIndex)
      Description copied from interface: EnderChestStorage
      Moves a temp chest's contents into an empty chest and deletes the temp row, in one transaction — the inverse of a spill, used when a player is granted a new chest that can hold a temp chest whole (see ChestSpillService#reclaimTempInto).

      The stored bytes are copied verbatim: nothing is decoded, merged or re-packed, so every item keeps the exact slot it occupied in the temp chest. That is only sound while the target is at least as large as the temp chest and is itself empty, so both are re-checked here under the lock along with the two kinds — the caller picked the pair from a list it fetched earlier, and an expiry sweep or a concurrent op may have invalidated it since. Any failed check is a no-op returning false, never a partial move.

      Specified by:
      reclaimTemp in interface EnderChestStorage
      Returns:
      true if the contents were moved and the temp row deleted, false if a check failed
    • transferChests

      public int transferChests(UUID from, UUID to, @Nullable @Nullable Integer onlyIndex, Set<Integer> preserveDestIndices, long tempExpiresAt)
      Description copied from interface: EnderChestStorage
      Moves a player's NORMAL chests onto another player in a single transaction (the account-switch primitive behind /ee transfer). For each source NORMAL chest (all of them, or only onlyIndex when non-null) a copy is written to to at the same index, carrying its size, custom name, icon and contents (and, on a full transfer, the primary flag); the source row is then deleted, so the items live on exactly one account.

      The destination's pre-existing NORMAL chests in scope are removed first so the destination ends up with exactly the source's chest count and nothing stacked on top. Any destination index listed in preserveDestIndices is re-inserted as a TEMP chest (carrying its items and expiring at tempExpiresAt) before removal, so those items remain recoverable; indices not listed are discarded. Destination PERM/TEMP chests sitting on an index the source needs are relocated to a free index rather than dropped. The whole thing is one transaction, so a failure leaves both players untouched.

      Specified by:
      transferChests in interface EnderChestStorage
      Parameters:
      onlyIndex - a single source index to transfer, or null for every NORMAL chest
      preserveDestIndices - destination indices whose items must be kept (spilled to a temp chest); empty to discard every replaced destination chest
      tempExpiresAt - epoch-millis expiry stamped on any temp chest created for preserved items
      Returns:
      the number of chests transferred
    • renameChest

      public void renameChest(UUID owner, int index, @Nullable @Nullable String name)
      Description copied from interface: EnderChestStorage
      Sets or clears a chest's custom display name (null resets to the default numbered title).
      Specified by:
      renameChest in interface EnderChestStorage
    • setIcon

      public void setIcon(UUID owner, int index, @Nullable @Nullable String icon)
      Description copied from interface: EnderChestStorage
      Sets or clears a chest's icon (material key, e.g. minecraft:diamond; null resets to default).
      Specified by:
      setIcon in interface EnderChestStorage
    • setPrimary

      public void setPrimary(UUID owner, int index)
      Description copied from interface: EnderChestStorage
      Makes the given chest the player's primary, clearing the flag from all others.
      Specified by:
      setPrimary in interface EnderChestStorage
    • clearPrimary

      public void clearPrimary(UUID owner)
      Description copied from interface: EnderChestStorage
      Clears the primary flag from all of the player's chests, leaving them with no main chest.
      Specified by:
      clearPrimary in interface EnderChestStorage
    • isMigrated

      public boolean isMigrated(UUID owner)
      Description copied from interface: EnderChestStorage
      Returns true if the player's chest #1 has its migrated flag set.
      Specified by:
      isMigrated in interface EnderChestStorage
    • setMigrated

      public void setMigrated(UUID owner, boolean migrated)
      Description copied from interface: EnderChestStorage
      Updates the migrated flag on the player's chest #1. No-op if chest #1 does not exist.
      Specified by:
      setMigrated in interface EnderChestStorage
    • completeMigration

      public void completeMigration(UUID owner, byte[] containerData, @Nullable @org.jetbrains.annotations.Nullable byte[] overflow, int tempSize, long tempExpiresAt)
      Description copied from interface: EnderChestStorage
      Commits a vanilla migration in one transaction: writes chest #1's contents and sets its migrated flag (one single-row UPDATE — chest #1's size is never changed), plus, when overflow is non-null, inserts a temp chest at the next free index carrying the vanilla items that did not fit. Used by the vanilla migration so no crash or concurrent runner can ever observe "contents written but not yet flagged" — the migration merge is not idempotent, so a re-run against an unflagged row would duplicate items. No-op on chest #1 if it does not exist.
      Specified by:
      completeMigration in interface EnderChestStorage
      Parameters:
      overflow - encoded bytes of the vanilla items that did not fit, or null when all fit
      tempSize - slot count of the temp chest (ignored when overflow is null)
      tempExpiresAt - epoch-ms expiry of the temp chest (ignored when overflow is null)
    • loadSettings

      public PlayerSettings loadSettings(UUID owner)
      Description copied from interface: EnderChestStorage
      Loads the player's settings, or PlayerSettings.defaults() if they have no row yet. Never returns null — an absent row is indistinguishable from an all-defaults one.
      Specified by:
      loadSettings in interface EnderChestStorage
    • saveSettings

      public void saveSettings(UUID owner, PlayerSettings settings)
      Description copied from interface: EnderChestStorage
      Upserts the player's editMode/appliedDefaultSize (whole-object save): updates the existing row, or inserts one if none exists. Does not touch username — that field is written only by EnderChestStorage.recordPlayerSeen(java.util.UUID, java.lang.String, long), so a save built from a stale in-memory copy never clobbers a name recorded since it was loaded.
      Specified by:
      saveSettings in interface EnderChestStorage
    • setEditMode

      public void setEditMode(UUID owner, boolean editMode)
      Description copied from interface: EnderChestStorage
      Targeted single-field upsert of just the edit-mode preference. Cheaper than EnderChestStorage.loadSettings(java.util.UUID) + EnderChestStorage.saveSettings(java.util.UUID, com.enhancedechest.model.PlayerSettings) for a one-field toggle (no preceding read) and never clobbers other settings. Use EnderChestStorage.saveSettings(java.util.UUID, com.enhancedechest.model.PlayerSettings) when persisting the whole object.
      Specified by:
      setEditMode in interface EnderChestStorage
    • getAppliedDefaultSize

      public int getAppliedDefaultSize(UUID owner)
      Description copied from interface: EnderChestStorage
      Reads the persisted applied_default_size baseline — the base-chest size currently dictated by the player's enhancedechest.default_size.<size> permission, or 0 when the base chest is not permission-managed. Returns 0 for a player with no settings row. Used by /ee resize to decide (even for an offline owner) whether the base chest is off-limits.
      Specified by:
      getAppliedDefaultSize in interface EnderChestStorage
    • setAppliedDefaultSize

      public void setAppliedDefaultSize(UUID owner, int size)
      Description copied from interface: EnderChestStorage
      Targeted single-field upsert of just the applied_default_size baseline (no preceding read), leaving every other setting untouched. Written by the default-size reconcile whenever the player's permission-derived base size changes. 0 records "not permission-managed".
      Specified by:
      setAppliedDefaultSize in interface EnderChestStorage
    • recordPlayerSeen

      public void recordPlayerSeen(UUID owner, String name, long lastOnline)
      Description copied from interface: EnderChestStorage
      Records that a player was seen: their current in-game name against their UUID (so name→UUID resolution works for offline players from the plugin's own data, with no server usercache or Mojang lookup), and lastOnline as epoch-ms. Called on join and quit, and from ChestOpener's open prelude when the name it already loaded differs.

      Cheap to call often: this only mutates the in-memory row and marks it dirty, so the SQL write rides the next batched flush rather than costing a statement per call.

      Specified by:
      recordPlayerSeen in interface EnderChestStorage
    • findUuidByName

      @Nullable public @Nullable UUID findUuidByName(String name)
      Description copied from interface: EnderChestStorage
      Resolves a stored in-game name to its UUID, case-insensitively, or null if no player with that name has been recorded. Never blocks on the network — this reads only the plugin's own table.
      Specified by:
      findUuidByName in interface EnderChestStorage
    • loadAllPlayerNames

      public List<EnderChestStorage.PlayerNameRecord> loadAllPlayerNames()
      Description copied from interface: EnderChestStorage
      Returns every recorded player with a non-null username, with the epoch-ms they were last seen (0 when never recorded). Used once at startup to populate the in-memory PlayerNameIndex so offline-player tab-completion and resolution can answer from memory instead of hitting the DB on every keystroke.
      Specified by:
      loadAllPlayerNames in interface EnderChestStorage