Class CachedStorage
- All Implemented Interfaces:
EnderChestStorage
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 throughOwnerResidencyCache.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
Nested classes/interfaces inherited from interface com.enhancedechest.storage.EnderChestStorage
EnderChestStorage.ExpiredRef, EnderChestStorage.PlayerNameRecord, EnderChestStorage.RawChestRow, EnderChestStorage.RawPlayerRow -
Constructor Summary
ConstructorsConstructorDescriptionCachedStorage(StorageBackend backend, org.slf4j.Logger logger, Telemetry telemetry) CachedStorage(StorageBackend backend, org.slf4j.Logger logger, Telemetry telemetry, CrossServerCoordinator coordinator) -
Method Summary
Modifier and TypeMethodDescriptionvoidWrites a consistent snapshot of the entire database totarget(which must not already exist).voidclearChestContents(UUID owner, int index) Empties a chest's contents (sets the stored bytes to NULL), keeping its size, name, icon and kind.voidclearPrimary(UUID owner) Clears the primary flag from all of the player's chests, leaving them with no main chest.voidclose()Flushes everything still dirty, then closes the SQL backend.voidcompleteMigration(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, whenoverflowis non-null, inserts a temp chest at the next free index carrying the vanilla items that did not fit.longTotal number of rows in theendercheststable across every player and chest kind.intcreateChest(UUID owner, int size, @Nullable Long expiresAt) Creates a new NORMAL chest at the lowest free index.intcreatePermChest(UUID owner, int size) Creates a permission-granted chest (kind=PERM) at the lowest free index.voiddeleteChest(UUID owner, int index) Deletes a chest.voidensureChest(UUID owner, int index, int size) Creates a chest at a specific index if it does not already exist (used by migration).intEvicts every owner that is offline and fully flushed.findExpired(long now) Returns every chest whose expiry is set and at or beforenow.@Nullable UUIDfindUuidByName(String name) Resolves a stored in-game name to its UUID, case-insensitively, ornullif no player with that name has been recorded.intflush()Writes every dirty row back to SQL.voidflushOwner(UUID owner) Writes one quitter's dirty rows back, then evicts them if clean and still offline.intgetAppliedDefaultSize(UUID owner) Reads the persistedapplied_default_sizebaseline — the base-chest size currently dictated by the player'senhancedechest.default_size.<size>permission, or0when the base chest is not permission-managed.intgetPrimaryIndex(UUID owner) Returns the index of the chest /ec opens: the primary if one is flagged, otherwise the lowest-indexed chest.int[]importRows(List<EnderChestStorage.RawPlayerRow> players, List<EnderChestStorage.RawChestRow> chests) Bulk-inserts rawplayersandenderchestsrows verbatim into this (fresh, active) backend, in a single transaction.voidinit()Initializes the SQL backend (schema + migrations).booleanisMigrated(UUID owner) Returns true if the player's chest #1 has its migrated flag set.booleanWhether an owner is currently pinned (online here).listChests(UUID owner) 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 (0when never recorded).@Nullable EnderChestDataLoads a single chest, or null if no such (owner, index) row exists.loadSettings(UUID owner) Loads the player's settings, orPlayerSettings.defaults()if they have no row yet.voidMarks an owner online: their rows survive every eviction untilunpin(java.util.UUID).booleanreclaimTemp(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 (seeChestSpillService#reclaimTempInto).voidrecordPlayerSeen(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), andlastOnlineas epoch-ms.voidrenameChest(UUID owner, int index, @Nullable String name) Sets or clears a chest's custom display name (null resets to the default numbered title).voidresizeChest(UUID owner, int index, int size) Changes a chest's slot count.voidUpdates the container bytes of an existing chest.voidsaveSettings(UUID owner, PlayerSettings settings) Upserts the player'seditMode/appliedDefaultSize(whole-object save): updates the existing row, or inserts one if none exists.voidsetAppliedDefaultSize(UUID owner, int size) Targeted single-field upsert of just theapplied_default_sizebaseline (no preceding read), leaving every other setting untouched.voidsetEditMode(UUID owner, boolean editMode) Targeted single-field upsert of just the edit-mode preference.voidSets or clears a chest's icon (material key, e.g.voidsetMigrated(UUID owner, boolean migrated) Updates the migrated flag on the player's chest #1.voidsetPrimary(UUID owner, int index) Makes the given chest the player's primary, clearing the flag from all others.voidspillRemove(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.voidspillShrink(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.booleanTrue if this backend can produce a file snapshot viaEnderChestStorage.backup(Path).inttransferChests(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).voidUnmarks an owner as online.Methods inherited from class java.lang.Object
clone, equals, finalize, getClass, hashCode, notify, notifyAll, toString, wait, wait, waitMethods inherited from interface com.enhancedechest.storage.EnderChestStorage
createChest
-
Constructor Details
-
CachedStorage
-
CachedStorage
public CachedStorage(StorageBackend backend, org.slf4j.Logger logger, Telemetry telemetry, CrossServerCoordinator coordinator)
-
-
Method Details
-
init
public void init()Initializes the SQL backend (schema + migrations). Player data is loaded lazily, per owner.- Specified by:
initin interfaceEnderChestStorage
-
close
public void close()Flushes everything still dirty, then closes the SQL backend.- Specified by:
closein interfaceEnderChestStorage
-
pin
Marks an owner online: their rows survive every eviction untilunpin(java.util.UUID). -
unpin
Unmarks an owner as online. Their rows stay resident until flushed clean and evicted. -
isPinned
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
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:EnderChestStorageTrue if this backend can produce a file snapshot viaEnderChestStorage.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:
supportsBackupin interfaceEnderChestStorage
-
backup
Description copied from interface:EnderChestStorageWrites a consistent snapshot of the entire database totarget(which must not already exist). The snapshot is safe to take while players are saving — it does not interrupt writes. Only valid whenEnderChestStorage.supportsBackup()is true.- Specified by:
backupin interfaceEnderChestStorage- 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:EnderChestStorageBulk-inserts rawplayersandenderchestsrows verbatim into this (fresh, active) backend, in a single transaction. Every column is copied as-is — including thecontainer_databytes — so no item (de)serialization happens; this is the DB→DB conversion primitive behind/ee import.Inserts are batched per table (one reused
PreparedStatementeach, 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:
importRowsin interfaceEnderChestStorage- Returns:
[playersInserted, chestsInserted]
-
countChests
public long countChests()Description copied from interface:EnderChestStorageTotal number of rows in theendercheststable across every player and chest kind. Used by/ee importto refuse converting into a non-empty destination (import only into a fresh DB).- Specified by:
countChestsin interfaceEnderChestStorage
-
listChests
Description copied from interface:EnderChestStorageReturns the player's chests ordered by index. Empty list if the player owns none.- Specified by:
listChestsin interfaceEnderChestStorage
-
getPrimaryIndex
Description copied from interface:EnderChestStorageReturns 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:
getPrimaryIndexin interfaceEnderChestStorage
-
loadChest
Description copied from interface:EnderChestStorageLoads a single chest, or null if no such (owner, index) row exists.- Specified by:
loadChestin interfaceEnderChestStorage
-
findExpired
Description copied from interface:EnderChestStorageReturns every chest whose expiry is set and at or beforenow.- Specified by:
findExpiredin interfaceEnderChestStorage
-
saveChest
Description copied from interface:EnderChestStorageUpdates 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:
saveChestin interfaceEnderChestStorage
-
createChest
Description copied from interface:EnderChestStorageCreates a new NORMAL chest at the lowest free index. IfexpiresAtis 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 viaEnderChestStorage.setPrimary(java.util.UUID, int).- Specified by:
createChestin interfaceEnderChestStorage- Returns:
- the index assigned to the new chest
-
createPermChest
Description copied from interface:EnderChestStorageCreates 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 ofcom.enhancedechest.service.PermissionChestServiceand are invisible to admin commands.- Specified by:
createPermChestin interfaceEnderChestStorage- Returns:
- the index assigned to the new chest
-
ensureChest
Description copied from interface:EnderChestStorageCreates 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:
ensureChestin interfaceEnderChestStorage
-
resizeChest
Description copied from interface:EnderChestStorageChanges a chest's slot count. Caller validates size (multiple of 9, 9..54).- Specified by:
resizeChestin interfaceEnderChestStorage
-
deleteChest
Description copied from interface:EnderChestStorageDeletes 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:
deleteChestin interfaceEnderChestStorage
-
clearChestContents
Description copied from interface:EnderChestStorageEmpties a chest's contents (sets the stored bytes to NULL), keeping its size, name, icon and kind.- Specified by:
clearChestContentsin interfaceEnderChestStorage
-
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:EnderChestStorageShrinks a chest and spills any cut-off items into a new temp chest, in one transaction. The original row is updated tonewSizewithvisibleas its new contents; ifoverflowis non-null a temp chest (kind=TEMP, appended after the highest index so it sorts last, expiring attempExpiresAt) is inserted holding it. Never leaves items in two rows at once.- Specified by:
spillShrinkin interfaceEnderChestStorage- 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:EnderChestStorageDeletes a chest, optionally spilling its items into a new temp chest, in one transaction. Ifitemsis 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:
spillRemovein interfaceEnderChestStorage- Parameters:
tempSize- slot count of the temp chest created for the items (ignored ifitemsis null)
-
reclaimTemp
Description copied from interface:EnderChestStorageMoves 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 (seeChestSpillService#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:
reclaimTempin interfaceEnderChestStorage- 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:EnderChestStorageMoves 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 onlyonlyIndexwhen non-null) a copy is written totoat 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
preserveDestIndicesis re-inserted as a TEMP chest (carrying its items and expiring attempExpiresAt) 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:
transferChestsin interfaceEnderChestStorage- Parameters:
onlyIndex- a single source index to transfer, or null for every NORMAL chestpreserveDestIndices- destination indices whose items must be kept (spilled to a temp chest); empty to discard every replaced destination chesttempExpiresAt- epoch-millis expiry stamped on any temp chest created for preserved items- Returns:
- the number of chests transferred
-
renameChest
Description copied from interface:EnderChestStorageSets or clears a chest's custom display name (null resets to the default numbered title).- Specified by:
renameChestin interfaceEnderChestStorage
-
setIcon
Description copied from interface:EnderChestStorageSets or clears a chest's icon (material key, e.g.minecraft:diamond; null resets to default).- Specified by:
setIconin interfaceEnderChestStorage
-
setPrimary
Description copied from interface:EnderChestStorageMakes the given chest the player's primary, clearing the flag from all others.- Specified by:
setPrimaryin interfaceEnderChestStorage
-
clearPrimary
Description copied from interface:EnderChestStorageClears the primary flag from all of the player's chests, leaving them with no main chest.- Specified by:
clearPrimaryin interfaceEnderChestStorage
-
isMigrated
Description copied from interface:EnderChestStorageReturns true if the player's chest #1 has its migrated flag set.- Specified by:
isMigratedin interfaceEnderChestStorage
-
setMigrated
Description copied from interface:EnderChestStorageUpdates the migrated flag on the player's chest #1. No-op if chest #1 does not exist.- Specified by:
setMigratedin interfaceEnderChestStorage
-
completeMigration
public void completeMigration(UUID owner, byte[] containerData, @Nullable @org.jetbrains.annotations.Nullable byte[] overflow, int tempSize, long tempExpiresAt) Description copied from interface:EnderChestStorageCommits 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, whenoverflowis 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:
completeMigrationin interfaceEnderChestStorage- Parameters:
overflow- encoded bytes of the vanilla items that did not fit, or null when all fittempSize- slot count of the temp chest (ignored whenoverflowis null)tempExpiresAt- epoch-ms expiry of the temp chest (ignored whenoverflowis null)
-
loadSettings
Description copied from interface:EnderChestStorageLoads the player's settings, orPlayerSettings.defaults()if they have no row yet. Never returns null — an absent row is indistinguishable from an all-defaults one.- Specified by:
loadSettingsin interfaceEnderChestStorage
-
saveSettings
Description copied from interface:EnderChestStorageUpserts the player'seditMode/appliedDefaultSize(whole-object save): updates the existing row, or inserts one if none exists. Does not touchusername— that field is written only byEnderChestStorage.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:
saveSettingsin interfaceEnderChestStorage
-
setEditMode
Description copied from interface:EnderChestStorageTargeted single-field upsert of just the edit-mode preference. Cheaper thanEnderChestStorage.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. UseEnderChestStorage.saveSettings(java.util.UUID, com.enhancedechest.model.PlayerSettings)when persisting the whole object.- Specified by:
setEditModein interfaceEnderChestStorage
-
getAppliedDefaultSize
Description copied from interface:EnderChestStorageReads the persistedapplied_default_sizebaseline — the base-chest size currently dictated by the player'senhancedechest.default_size.<size>permission, or0when the base chest is not permission-managed. Returns0for a player with no settings row. Used by/ee resizeto decide (even for an offline owner) whether the base chest is off-limits.- Specified by:
getAppliedDefaultSizein interfaceEnderChestStorage
-
setAppliedDefaultSize
Description copied from interface:EnderChestStorageTargeted single-field upsert of just theapplied_default_sizebaseline (no preceding read), leaving every other setting untouched. Written by the default-size reconcile whenever the player's permission-derived base size changes.0records "not permission-managed".- Specified by:
setAppliedDefaultSizein interfaceEnderChestStorage
-
recordPlayerSeen
Description copied from interface:EnderChestStorageRecords 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), andlastOnlineas epoch-ms. Called on join and quit, and fromChestOpener'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:
recordPlayerSeenin interfaceEnderChestStorage
-
findUuidByName
Description copied from interface:EnderChestStorageResolves a stored in-game name to its UUID, case-insensitively, ornullif no player with that name has been recorded. Never blocks on the network — this reads only the plugin's own table.- Specified by:
findUuidByNamein interfaceEnderChestStorage
-
loadAllPlayerNames
Description copied from interface:EnderChestStorageReturns every recorded player with a non-null username, with the epoch-ms they were last seen (0when never recorded). Used once at startup to populate the in-memoryPlayerNameIndexso offline-player tab-completion and resolution can answer from memory instead of hitting the DB on every keystroke.- Specified by:
loadAllPlayerNamesin interfaceEnderChestStorage
-