Repository navigation
Persistence
How persistence works as built, and why: one database with a schema per owner, and a queue that carries every owner's work to it so no owner ever waits. The facts to look up are on Persistence schemas and Persistence write-behind; what's still to build is on Persistence design.
flowchart TD
O["An owner"] -->|"work naming its keys"| Q["Write-behind queue"]
Q -->|"heads every key's line"| W["A save worker"]
W -->|"one transaction"| DB[("PostgreSQL 18")]
W -->|"committed, refused or retried"| O
One database, a schema per owner. The account owner's data lives in account, the realm owner's in realm, and the command layer's audit in ops, each with its own context and its own migrations (Persistence schemas).
No owner touches the database. An owner hands the write-behind queue a piece of work naming the keys it touches, and hears back by message on its own thread. The queue keeps one line per key, so work on one account or character runs in order and work on different ones runs side by side (Persistence write-behind).
An answer waits for its commit. An owner tells the client a change happened only once the change is committed, and a write a unique rule refuses comes back as an answer, not a failure.
Sign-on is the one direct read. The gateway reads an account's credentials itself, through the account directory, because it holds no game state an owner would have to wait for.
An owner that waits on the database stalls everything it holds for as long as the database takes. Owners ask, workers do, and the order of saves and loads per key is kept in one place, so a load can't overtake the save it depends on.
PostgreSQL 18 is the minimum because names and emails use a case-insensitive ICU collation, and LIKE works on such collations from 18.
| Idea | What killed it |
|---|---|
citext for names and emails |
An ICU collation does the same job, and PostgreSQL 18 lets LIKE work on it |
| Setting | Default | Sets |
|---|---|---|
Persistence:ConnectionString |
none, required | The database |
Persistence:SaveWorkers |
4 | Workers applying work, at least one |
Persistence:FirstRetryDelay |
100 ms | The first wait before a retry |
Persistence:MaxRetryDelay |
30 s | The longest wait between retries |
Persistence:FirstIdBlockTimeout |
30 s | How long start-up waits for the first block of character ids |
| Term | Means here |
|---|---|
| Work | One piece of database work an owner hands the queue, with the keys it touches |
| Key | What orders work: an account, a character, an email or a named line |
| Refused | Completed without writing, because a unique rule said no |
- Work that fails for a reason other than the database retries forever, blocking its keys; Persistence write-behind has the detail.
- The
contentschema and the load workers come later (Persistence design).
| Piece | Path |
|---|---|
| Contexts, conventions and migrations | Source/NexusTomorrow.Persistence/ |
| Account work and the account directory | Source/NexusTomorrow.Persistence/Account/ |
| Character work and id blocks | Source/NexusTomorrow.Persistence/Realm/ |
| The audit | Source/NexusTomorrow.Persistence/Ops/ |
| The write-behind queue | Source/NexusTomorrow.Persistence/WriteBehind/ |
| Tests | Tests/NexusTomorrow.Persistence.Tests/ |
Start here
How the server works
- Server core design
- Host
- Gateway
- Owners
- Persistence
- Command layer
- Harness
- Map scheduler design
- Timer wheel design
The protocol
Working on it