Database Sharding & the Entity Pool
Drumee does not keep all tenants in one big schema. It shards by entity: every
workspace (hub) and every user (drumate) gets its own MariaDB database plus its
own MFS storage root on disk. A single installation runs
hundreds or thousands of these databases side by side, with the central Yellow Pages
(yp) database acting as the registry that maps requests to the right shard.
This page explains the sharding model and the offline factory — the daemon that makes sharding practical by pre-building databases in advance.
The sharding model
| Entity type | Becomes | Has its own |
|---|---|---|
hub | A collaborative workspace | MariaDB database, MFS storage root, members/roles/permissions, theme |
drumate | A single user | MariaDB database, MFS storage root, personal contacts/tags/activity |
- Strict isolation. A tenant's data lives only in its own database; there is no shared table holding everyone's rows. A bug or a permission error in one shard cannot leak another tenant's data.
- Shared definitions, separate data. Every hub and drumate database is built from the
same schema — the
commonclass (MFS procedures, trash routines, shared tables) is deployed identically into each. - Routing. When a request arrives, the session layer identifies the target entity
(via the
Hostheader / session) and the ACL layer resolves itsdb_name. All subsequent queries are scoped to that shard. See Request Pipeline.
Every shard is an independent database with its own MFS root — there is no cross-tenant
table. The yp registry is the only shared database and the single point that maps an
entity to its shard.
Database naming
Every entity database is named by the make_db_name() SQL function as <x>_<id> — a
16-character hex id preceded by a single hex character and an underscore:
-- yellow_page/procedures/functions.sql
SELECT concat(substring(uniqueId(),-1,1), '_', uniqueId()) INTO _res;
The leading <x>_ is a meaningless bucketing prefix — it does not encode the entity
type (9_ does not mean "user"). It exists to spread databases across the namespace so the
database engine can optimise (distributing on-disk schema directories rather than clustering
thousands of similarly-named databases). The only fixed name is yp. See
Database Naming Conventions.
Why a pre-provisioning factory
Provisioning a new shard is expensive: CREATE DATABASE, load the full schema (routines,
tables, triggers), create the on-disk MFS storage directory, chown it, and create the VFS
root node. Doing all of that synchronously while a user waits for "create workspace" or
"sign up" would be slow.
Instead, Drumee keeps a warm pool of ready-made, empty entities. Each pooled entity is a
fully built hub or drumate database marked area='pool'. When a real workspace or user
is needed, the application simply claims a clean pool entity and flips it into active use
— there is no schema build on the hot path.
The factory daemon
The factory (offline/factory/) is a long-running background daemon that tops the pool up.
It is not started by npm run dev; its entry point is offline/drumate.js, and it runs
as its own PM2 process (factory).
offline/drumate.js # entry point: new Factory({type:'drumate', schemas}).start()
offline/factory/index.js # __drumee_factory (extends Offline) — the daemon loop
offline/factory/schema.js # __schema — builds one entity at a time
Watermark (target pool size)
The factory keeps at least watermark clean entities of each type in the pool. The
default is 210 for both hub and drumate, overridable via config (watermark.hub /
watermark.drumate):
this.watermark = {
hub: watermark.hub || 210,
drumate: watermark.drumate || 210,
};
Current depth comes from the pool_free(type) SQL function, which counts rows in
yp.entity where area='pool' and type=_type.
Startup
- Open a
ypconnection and resolve the per-type watermarks. checkSanity()— refuse to run in unsafe conditions (see below).- Build the SQL templates for both types (unless
--rebuild=noand cached templates already exist). - Enter the infinite
run()loop.
A template is a schema-only mysqldump (--routines --quick --no-data --single-transaction --skip-comments) of the first active entity of that type in domain
1, written to /tmp/drumee-template-<type>.sql with a .ok sentinel marking completion.
It is the blueprint copied into every new entity of that type.
The run loop
An infinite while (1) await run(). Each pass, for each type:
check_pool(type)→ returns the current count only if it is>= watermark, else0.- If the template
.oksentinel is gone → abort ("Exit due to doubious template!"). - If the pool is below watermark → set the loop timer to 15 s and build one more
entity via
make_schema(type). - If the pool is full → ramp the timer up by 1 s each idle pass (capped at 60 s) to back off polling.
So the daemon polls aggressively while it has work to do, and idles ever more lazily once the pool is satisfied.
Building one entity
make_schema(type) runs create_entity() on a __schema instance:
entity_create(type)(yellow_page/procedures/entity/create.sql) — inserts anentityrow witharea='pool',status='frozen', and runsCREATE DATABASE.load_sql()— pipes the cached/tmptemplate dump into the new database.create_vfs_root()—mkdir -p <home_dir>/__storage__,chownit to the system user, thenmfs_create_nodefor the root node and setsentity.home_id.- Marks the entity reusable:
settings.pool_state = "clean".
On any failure, delete_entity() rolls everything back (entity_delete + rm -rf of the
validated home dir) and the error propagates so the loop retries next pass.
How pooled entities are consumed
The factory only produces. Consumption happens elsewhere via pickupEntity(type)
(yellow_page/procedures/utils/pickupEntity.sql), which claims a random pool entity whose
settings.$.pool_state = 'clean', moving it out of area='pool'. That drops pool_free,
which eventually triggers the factory to build a replacement.
⚠️ Restart the factory after schema patches
When you patch hub-, drumate-, or common-class schemas, you must restart the factory.
The template is dumped once at startup and cached in /tmp/drumee-template-<type>.sql.
The run loop reuses that cached dump for every entity it provisions and never re-reads the
schema while running. So any routine, table, or trigger change patched into the live
databases will not reach newly pooled entities until the factory restarts and re-dumps the
template. Until then the pool keeps handing out entities built from the old schema.
After applying such patches:
- Patch the reference/active schemas as usual (
bin/patch-from-manifest, etc.). - Restart the factory so it re-dumps the current schema (not with
--rebuild=no, which keeps the stale cached dump). - Optionally drain the pool entities built before the patch — they will not contain the new definitions.
Operational notes
- Primary only. The factory creates databases;
checkSanity()aborts ifSHOW SLAVE STATUSreports a running replica. - Privileged. Must run as
rootor the configuredsystem_user(needed tochownMFS directories). --rebuild=no. Skips rebuilding templates at startup if cached/tmpdumps and their.oksentinels already exist.- Tune pool size with
watermark.hub/watermark.drumate(default 210 each). - Inspect the pool:
SELECT type, COUNT(*) FROM entity WHERE area='pool' GROUP BY type;and theadmin.show_watermarkservice.
Key SQL touchpoints
| Routine | Where | Role |
|---|---|---|
make_db_name() | yellow_page/procedures/functions.sql | Generate the <x>_<id> shard database name |
pool_free(type) | yellow_page/procedures/admin/pool_free.sql | Count clean pool entities of a type |
entity_create(type) | yellow_page/procedures/entity/create.sql | Insert entity row + CREATE DATABASE |
entity_delete(id) | yellow_page/procedures/entity/delete.sql | Roll back a half-built entity |
pickupEntity(type) | yellow_page/procedures/utils/pickupEntity.sql | Claim a clean pool entity |
mfs_create_node | common/procedures/mfs/ | Create the entity's MFS root node |