Skip to content

Multi-server data lifecycle, cache coherency, distributed locking and indexed leaderboards#1

Merged
Swofty-Developments merged 8 commits into
mainfrom
feat/multi-server-lifecycle
Jul 23, 2026
Merged

Multi-server data lifecycle, cache coherency, distributed locking and indexed leaderboards#1
Swofty-Developments merged 8 commits into
mainfrom
feat/multi-server-lifecycle

Conversation

@Swofty-Developments

Copy link
Copy Markdown
Owner

No description provided.

Swofty-Developments added 8 commits July 23, 2026 23:26
Previously a container serialized only the fields touched this session and saved
that as a full overwrite, so the first write from a node that had not read every
field silently dropped the unread fields from storage. When a player moved between
server instances this lost data.

The container now retains the full backing document it last saw in storage and, on
serialize, merges the touched fields over it, so untouched fields are preserved. A
null-set records a tombstone that suppresses the field from the merged output, so
clearing a value still deletes it instead of resurrecting the stored one. DataFormat
gains readRaw/writeRaw for whole-document access, implemented for the JSON format,
and the managers warm the backing document once per entity rather than reloading
storage on every field access.
Adds an explicit lifecycle to the data API so a node can warm an entity's whole
document into its cache in a single storage read, flush pending changes, and evict
the entity when finished. load/loadAsync/flush/unload/isLoaded/loadedPlayers cover
players and loadLink/flushLink/unloadLink/isLinkLoaded cover shared entities such as
an island or coop.

Eviction is what keeps a multi-server deployment correct: without it a container
lingered in a node's cache forever, so a player who changed data on another server
and later returned would be served the stale cached value and, on the next write,
overwrite the fresh data. unload now flushes and drops the container so a later visit
reloads from storage. load is also the primitive a proxy needs to prepare a player's
data on the target server before moving them there.

When a distributed event bus is present the API now keeps loaded containers coherent
with peer writes: a remote change to a currently-loaded entity updates the local view
in place, preserving the prior dirty state so a read-only node does not re-persist
stale sibling fields. Coherency applies to fields that have a subscription registered
on the receiving node. An optional deferred-persistence mode buffers writes until
flush or unload so a play session can be written back once instead of per field;
shutdown flushes everything so no buffered write is lost.
Transactions previously guarded only a JVM-local monitor, so two servers mutating the
same shared entity (e.g. a coop or island) could interleave and lose updates despite the
API presenting transactions as atomic. A DistributedLock can now be supplied; when present,
transactions take a cross-node lock keyed by the entity in addition to the local monitor,
and a public lock(key, timeout) helper exposes the same primitive for app-level critical
sections that span more than one field or entity. When absent, behaviour is unchanged.

Ships a Redis implementation using SET NX PX with a compare-and-delete release so a lock is
only released by its owner and a lease bounds a crashed holder, plus a reentrant in-memory
implementation for single-node use and tests.
getTop, getTopPaged and the query helpers previously loaded and deserialized every stored
player on each call, an O(N) scan that does not hold up for a live leaderboard. Storage
backends may now implement the optional LeaderboardIndex capability (Redis sorted sets, or an
in-memory index for single-node and tests). A field is opted in with trackLeaderboard and a
scorer; each persisted write to it updates the index, so getTop and getTopPaged read the
ranked slice directly and then load only that page's values, turning the query into
O(log N + page). rebuildLeaderboard backfills the index from data written before the field
was tracked. Backends without the capability, or untracked fields, fall through to the
existing scan, so the change is purely additive.
The indexed leaderboard previously kept a scan-based path and chose it whenever the field was
not tracked or the storage lacked an index, so forgetting to register a leaderboard silently
restored the O(N) scan the index was meant to replace. Leaderboards are now index-only: getTop
and getTopPaged require a registered field and read the sorted index directly, and both
trackLeaderboard (on a storage without an index) and ranking an unregistered field fail fast
with a clear error. getTop(field, limit, comparator) remains as the explicit scan-based
escape hatch for ad-hoc custom orderings.
Ranking a field no longer requires a trackLeaderboard call. The first getTop/getTopPaged for a
field builds its index from existing data in one scan, and from then on the index exists in shared
storage so every node maintains it on write via a new updateScoreIfPresent (an atomic
exists-then-add). Numeric values are scored automatically, so the common case needs zero setup; a
field that is never ranked never gets an index and its writes cost nothing. trackLeaderboard now
only registers a score function for ranking a non-numeric field, and rebuildLeaderboard(field)
forces a rebuild. Storage without an index still throws on ranking rather than silently scanning.
The net.swofty.api package held its plumbing (PlayerDataManager, LinkedDataManager,
DataContainer, BulkOperationExecutor, TransactionManager, ExpirationManager, LinkRegistryImpl,
Validation) as public classes, so with no module descriptor they were importable from the
published jar and invited callers to bypass DataAPI and reach into internals. Nothing outside the
package references them — only DataAPIImpl is used externally — so they are now package-private.
DataAPIImpl and the DataAPI interface are unchanged; this only removes accidental public surface
and drops the internals from the generated Javadoc.
@Swofty-Developments
Swofty-Developments marked this pull request as ready for review July 23, 2026 14:11
@Swofty-Developments
Swofty-Developments merged commit 8f670a1 into main Jul 23, 2026
1 check passed
@Swofty-Developments
Swofty-Developments deleted the feat/multi-server-lifecycle branch July 23, 2026 14:11
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant