-
Notifications
You must be signed in to change notification settings - Fork 7
Storage Backends
Where PlayerData, PDSections and account data physically live is an admin decision, made in
plugins/EverNifeCore/storage.yml. Plugins advise a default; the admin has the final say. The storage
engine is EveryDatabase - EverNifeCore configures
and drives it.
The file is generated with a full set of commented, disabled example backends on first boot, so most
admins only flip enabled and fill in credentials.
storage-backends:
playerdata: # the entry key is a FREE unique id (name it anything)
enabled: true
type: groupedfile
path: plugins/EverNifeCore/StorageData/PlayerData
format: yaml # yaml | json (file backends only)
networkdata:
enabled: true
type: groupedfile
path: plugins/EverNifeCore/StorageData/NetworkData
format: yaml
mysql:
enabled: false
type: sql
url: "jdbc:mysql://localhost:3306/minecraft"
user: root
pass: ""
pool:
minIdle: 2
maxSize: 10
connectTimeoutSeconds: 5
idleTimeoutSeconds: 30
default-backend: playerdata # used when nothing more specific is configuredThe entry key is a free unique id, not the type. Declaring several backends of the same type is how
you point different data at different servers - e.g. a mysql_economy and a mysql_points, both
type: sql, on two databases.
Two backends come enabled, split by role rather than by type:
| Id | Holds | Requirement |
|---|---|---|
playerdata |
evernifecore_playerdata, every pd_<plugin>_<id>
|
what belongs to ONE player on THIS server |
networkdata |
ec_accounts, every acs_<plugin>_<id>, ec_server_cooldowns
|
what the whole network must agree on |
The id names the role and type names the technology, which is why switching networkdata to
type: sql does not turn its id into a lie. Both start as local groupedfile folders: a single server
is a network of one, and that is the correct answer until a second server joins - at which point you
point networkdata at a database both servers reach and nothing else changes. The disabled localfile
example deliberately sits in a folder called SomeRandomFolder, so enabling it without editing the
path reads as unfinished rather than as a deliberate neighbour of the other two.
Every backend's runtime dependency (JDBC drivers, the Mongo driver) is downloaded up front at boot,
so switching type here never hits a missing dependency.
type: maps to one of these. Full capability matrix and per-backend setup live in the EveryDatabase
wiki - the short version:
type: |
Engine | Good for | EveryDatabase page |
|---|---|---|---|
groupedfile |
key-major files (one file per key, all its collections) | the per-player model; the factory default | Grouped Files |
localfile |
one file per entity | tiny deploys, human-readable | Local Files |
sql |
MySQL / MariaDB (HikariCP pool, JSON column) | production, shared DB | MySQL & MariaDB |
postgresql |
PostgreSQL (HikariCP pool, JSON column) | production, shared DB | PostgreSQL |
h2 |
H2 embedded / file / tcp | embedded / dev | H2 |
mongo |
MongoDB (native BSON documents) | document workloads | MongoDB |
memory |
in-memory only, ephemeral | tests / throwaway servers | In-Memory |
format: yaml | json applies to the file backends (groupedfile / localfile) only; yaml is the
default and json is always written pretty/indented. For choosing among them, see
Choosing a Backend.
⚠️ memorydata is lost on shutdown - the parser warns when you enable it. Use it only for tests.
Once backends are declared, four blocks route the actual data. Each may name a storage-backend-id;
naming a missing or disabled backend is a fail-fast error at boot with a message that points at the
offending key.
playerdata:
storage-backend-id: playerdata # must be an enabled backend id
collection: evernifecore_playerdata
load-mode: ALL # ALL (default) | RECENT
recent-days: 60 # RECENT: how far back to eager-load
login-timeout-seconds: 15 # how long a login may wait on storage before being denied
slow-login-report-seconds: 3 # print a per-section breakdown of a login slower than this
default-idle-grace-seconds: 3600 # how long a section lingers after its player goes offline
orphan-reaper:
enabled: false # opt-in; sweeps section rows whose base is gone
interval-minutes: 360load-mode: ALL loads every player at startup; RECENT eager-loads only players seen in the last
recent-days and lazy-loads the rest on demand.
default-idle-grace-seconds (default 3600) is how long a player's section stays in memory after that
player goes offline - the server-wide value for every section whose developer did not ask for a
specific one. Lower it to reclaim memory sooner (0 releases as soon as the player leaves), raise it
so a reconnect does not have to read from the database again.
login-timeout-seconds (default 15) bounds the whole login resolution - the player row plus every
section that loads at login - not one read. When it is exceeded the login is denied rather than served
with data missing.
slow-login-report-seconds (default 3, 0 disables) is when the framework stops being silent about a
slow login and prints what it was waiting on: per section, the time, the plugin that declared it, that
plugin's author and the backend it lives on. Reach for it before blaming EverNifeCore for a slow join -
the breakdown usually names a specific plugin or a specific database. It prints on a timeout too, with
the sections still loading marked as pending.
pdsections:
myplugin: # your plugin's name, lowercase
jobs: # the section id declared at registration
storage-backend-id: mysql
# collection: pd_myplugin_jobs
# lifecycle: LAZY # LAZY | ONLINE | RESIDENT | PRELOADED
# idle-grace-seconds: 600 # overrides the developer AND the global default
# cache: { policy: TTL, ttlSeconds: 300 } # ALWAYS | TTLGenerated automatically the first time each PDSection registers, keyed by the section's id (not
its class name), so renaming the class never orphans the entry. The admin then edits
storage-backend-id, collection, lifecycle, idle-grace-seconds and cache freely - keys are
matched case-insensitively.
lifecycle overrides the plugin's declaration. It exists for one situation: the slow-login report
named a section you would rather not pay for at every join, and LAZY takes it off the login path -
the first read then happens whenever something actually asks for it. An unknown value fails the boot
with a message listing the valid ones, rather than silently falling back.
An admin cache: sets freshness only; when a cell enters and leaves memory is the developer's
lifecycle. NOCACHE is refused for a PDSection - the cached cell is the
instance the flush pipeline persists, so bypassing the cache would lose every write.
An entry no registered section claims is reported with a WARNING shortly after boot: either the plugin that owned it is gone, or its section id changed and the rows of the old collection are no longer reachable. Nothing is moved or deleted for you.
accountsections:
myplugin:
achievements:
collection: acs_myplugin_achievements
# cache: { policy: TTL, ttlSeconds: 300 }The same idea for account-wide sections: generated on first registration, keyed by the section id, matched case-insensitively, and an unclaimed entry gets the same orphan WARNING.
Two knobs only, and the difference from pdsections is deliberate. There is no per-section
backend: the whole account family lives on the backend set under network, which is what
lets a link absorb an account's rows without coordinating writes across backends. There is no
per-section lifecycle or idle-grace-seconds either - the family has one cycle, driven by whether
any member of the account is online.
cache.policy: TTL is the one worth knowing about. An account row is written by every instance of
the network, so on a backend with no change feed the login refresh would otherwise be the only moment
another server's write becomes visible here. A TTL bounds that staleness without needing Redis.
NOCACHE is refused, for the same reason as a PDSection.
network:
storage-backend-id: networkdata # REQUIRED - an enabled backend id, named explicitly
# idle-grace-seconds: 3600 # absent = follow playerdata.default-idle-grace-seconds
server-cooldowns:
collection: ec_server_cooldowns
# cache: { policy: TTL, ttlSeconds: 300 }The one backend every server of your network must agree on. It holds the account registry
(ec_accounts), every account-wide section, the network-wide server cooldowns, and whatever a plugin
puts there through ECNetworkStorage.
Two different questions meet here, and only the first one is configured:
- "do all my servers see the same row?" - answered by the backend below. Point every server at one shared database and they share data with no linking involved, because Minecraft servers already agree on a player's UUID.
-
"are these two identities the same person?" - answered by
/ecaccount link, per person, by an admin. It is needed when the UUID itself differs (a Hytale server, a Discord identity). Nothing instorage.ymlenables or disables it; a link exists only once someone runs the command. See Accounts.
storage-backend-id is required and explicit. Absent, empty, naming a backend that is not declared,
or naming a disabled one - each is a critical error that cancels the boot. There is deliberately no
"empty inherits default-backend": that implicit fallback is what turns an unrelated edit into a
silent migration of the whole network family.
⚠️ Astorage.ymlstill carrying the oldmulti-platform-accountsblock is refused, with a message mapping each key onto its replacement. Ignoring it would let an admin read their own file back and believe those keys still do something.enabledhas no replacement: linking identities was never a switch, and without a link every identity is its own account exactly as before.
idle-grace-seconds is how long an account row stays in memory once no member of that account is
online here - after the last one quits, and equally for a row that was read for an account nobody was
playing on (an offline lookup, an aggregate). One value covers the whole family; what a single section
can override lives in accountsections.
server-cooldowns is the entry for the collection behind Cooldown.network(id) (see
Cooldowns). It is generated when absent and takes two keys: collection, which is how you
get out of a name collision with another plugin, and cache, where policy: TTL bounds how stale
another server's write may look here on a backend with no change feed. NOCACHE is refused - the flush
iterates the cached values, so a route without a cache would lose every write. There is no
storage-backend-id here: these rows belong to the network family, and the family moves as one.
Two enabled file backends (groupedfile / localfile) that resolve to the same directory - or one
whose directory sits inside the other's - cancel the boot, at the same severity as a database that
does not answer. The check runs on the config alone, before anything connects, and compares absolute
normalized paths (never toRealPath, which would throw on a first boot where the folder does not exist
yet).
It is fatal rather than a warning because the damage is invisible:
- a
groupedfile's per-key lock lives in a store per Storage instance. Two backends over one directory are two independent lock maps over the same files: mutual exclusion stops existing and two concurrent writes to one key overwrite each other with no error; - listing reads every file in the directory carrying the resolved extension, so each backend starts trying to decode the other's files as its own;
- the schema/migration bookkeeping of one becomes a stray file in the other's directory.
A warning in a boot log nobody reads does not protect against that, and by the time anyone notices the
data is already corrupt. The fix costs one edited path.
Two things this rule deliberately does not cover:
- a disabled backend, which opens no file at all;
- an
h2database file sitting inside such a directory. Its file does not match the extension a file backend lists, so it is neither read nor overwritten - visual clutter, not corruption.
When several instances share one database, each instance caches rows independently - a write on
instance A must invalidate the same entry on instance B. That is what multi-server-cache-sync does.
multi-server-cache-sync:
enabled: true # default; HARMLESS no-op on a single server
transport: auto # auto | redis | native
redis:
enabled: false
host: localhost
port: 6379Transport resolution:
-
auto(default) - Redis if theredisblock is enabled, else the backends' native change feed when every manager's backend has one and none of them is a file backend, else a silent no-op. -
redis- force the Redis pub/sub transport. -
native- use only the backends' native feed; this is the only way to reach a file backend's.
Which backends have a native feed: MongoDB, PostgreSQL and the file backends
(groupedfile / localfile). sql (MySQL/MariaDB) has none - there, cross-instance coherence exists
only through Redis.
📌 Why
autoskips a file backend's feed. That feed watches this machine's filesystem, so it cannot carry a write made by another server, and an event has no origin - every local write comes back and re-invalidates the cell it just updated. On a stock install that would cost a watcher thread per storage and a wasted reload per write to learn what this server already knew. Asking for it by name (transport: native) is where it earns its keep: an admin editing the data files by hand gets the server to notice.
📌 Current state of the Redis transport. The Redis transport lives in the optional
everydatabase-manager-jedismodule, which is not bundled with EverNifeCore on this version. If you configure aredisblock without that runtime on the classpath, EverNifeCore logs a warning at boot and cache-sync via Redis is a no-op. The native-feed path (Mongo/PostgreSQL) is wired and works today. On a single server the whole block is a harmless no-op regardless.
For the deeper model, see Cross-Process Cache Sync.
logging:
level: warn # warn (default) | info | debug | traceControls the EveryDatabase storage log verbosity (bind reports, conflict/flush lines).
A backend declared enabled: true that does not answer at boot stops the server. This is on by
default, and it is deliberate: booting without the database that holds player data would let every
player join with empty data, and the first save would overwrite the real rows with those empty ones.
Every unreachable backend is reported at once, not one per restart. The report names each one with
its type, the target it was pointed at (with the password redacted), which storage.yml keys
route data to it, and the root cause of the failure; the full stack traces are printed above the
banner.
# plugins/EverNifeCore/config.yml
Settings:
Storage:
STOP_SERVER_IF_STORAGE_IS_UNREACHABLE: true # the defaultTwo things worth knowing before you flip it:
- It only ever stops a BOOT. A failed reload never stops anything - the previously loaded storage is still live and serving, so the report says so and nothing is lost. Fix the config or the database and reload again.
-
falsedoes not make the server work without a database. EverNifeCore stays disabled either way, every plugin that depends on it fails, and whatever data does get written diverges from what the database holds. The only real ways out of a boot failure are starting the database, fixing the url/user/pass, or settingenabled: falseon a backend you no longer use.
| Subcommand | Permission node | What it does |
|---|---|---|
status |
evernifecore.command.storage.status |
Prints the routing (which backend/collection each entity persists on), the claimed collections per backend with their owner, plus health counters - quit-flush retry backlog, adopted conflicts, last failed write. |
transfer section <plugin:section> <backend> |
evernifecore.command.storage.transfer |
Migrates one PDSection's collection to another backend at runtime (see below). |
transfer network <backend> |
evernifecore.command.storage.transfer |
Moves the whole network family to another backend (see below). |
transfer is a branch, not a command: typing it alone lists the two migrations. The permission sits on
the branch, so granting evernifecore.command.storage.transfer grants both.
The claim listing in status is how a collection a plugin put on the network backend becomes visible
at all: it gets no storage.yml entry, so the claim is its only registration.
You are not locked in. A single PDSection's collection can be migrated to another backend at runtime
without editing files by hand - see Legacy Data Migration for
/ecstorage transfer section. The underlying builder is EveryDatabase's
Moving Data Between Backends.
The day a second server joins, the network family has to leave that local folder. transfer network
does it in one command - the day-one local groupedfile is a decision you can undo.
The unit is the whole network family, indivisibly. A link absorbs an account's rows in one place, so a family split across backends would need a write coordinated across two databases - the very thing giving it one backend refused. It is also the command an admin actually wants: they think "I want my network on MySQL", not "I want collection X moved".
What travels is what is claimed on the source backend - ec_accounts, every acs_*,
ec_server_cooldowns and every collection a plugin claimed through ECNetworkStorage. Nobody
maintains a list, so a plugin's collection goes along with the framework's.
The order of events:
- The preview prints first, naming every collection that will move and its owner. A claim recorded without a descriptor cannot be copied by anything, so it is named as left behind rather than skipped in silence - reading "transferred everything" over a collection that stayed put is the failure worth preventing here.
- Everything is flushed, because the copy reads the backend: anything still dirty in memory would be left behind.
- Each collection is claimed on the target and copied there.
- Cutover writes
network.storage-backend-id: <target>intostorage.ymland re-runs the core's storage reload, so every family lands on exactly what was copied and plugin storage-reload callbacks fire on the way through.
If any step fails, the claims this transfer created on the target are released, the old binding stays exactly as it was, and the reason is reported. The source collections are never deleted (same discipline as the PDSection transfer), so undoing is a matter of pointing the config back. Only one network transfer runs at a time, process-wide. A maintenance window is recommended.
- PlayerData & PDSections - what gets stored and how cache policies map on.
- Inline Backends for Plugins - a plugin routing its OWN data elsewhere, and reaching the shared network backend.
- Accounts - the shared account backend.
- Legacy Data Migration - first-boot import and runtime transfer.
-
Command Framework - how
/ecstorageand other builtin commands are declared.
EverNifeCore · Home · made by Petrus Pradella
Getting Started
Commands & Text
Player Data & Storage
- PlayerData & PDSections
- Accounts
- Storage Backends
- Inline Backends for Plugins
- Legacy Data Migration
- Cooldowns
Config & Minecraft Systems
- Configuration
- Scheduler & Threading
- Items & NBT
- GUI Framework
- Integrations
- Economy
- Version Compatibility
Architecture & Reference