Skip to content

Fix player fact loss on server switch, add MongoDB auth, make reload safe - #1

Open
Chikage04 wants to merge 12 commits into
mainfrom
fix/fact-persistence-and-mongo-auth
Open

Fix player fact loss on server switch, add MongoDB auth, make reload safe#1
Chikage04 wants to merge 12 commits into
mainfrom
fix/fact-persistence-and-mongo-auth

Conversation

@Chikage04

Copy link
Copy Markdown

Summary

Three fixes around the MongoDB-backed fact persistence layer on a network.

1. Stop player fact loss when switching servers (5857a00)

  • reconcileGroup keyed its delete-missing on the persistable subset of the cache, so any fact whose entry isn't defined/loaded on the current server was wiped from the shared DB ("Loaded 10 → Synced 2 → lost 8"). Deletion is now keyed on cache presence — only facts the player genuinely no longer has are removed, never ones this server merely can't persist.
  • The periodic async flush could run after a player was evicted on quit, observe an empty cache and deleteMany() every fact. Flushes are now gated on per-player ownership and serialised with a striped lock, so an evicted/never-loaded player is never reconciled.

2. Support separate MongoDB credentials (0a608af)

username / password / auth_source config keys were silently ignored (only uri/name were read) → a secured MongoDB rejected everything with "Command find requires authentication" (error 13). The client is now built from MongoClientSettings with an optional MongoCredential (auth_source defaults to the database name, so the password no longer has to be URL-encoded into the URI; an explicit credential overrides one embedded in the URI). The previously-unused timeout_ms is applied as the server-selection timeout, and the default config.yml documents the new keys.

3. Keep the Mongo connection open across runtime reloads (08dadad)

Typewriter resolves Inkwell's FactStorage once via by lazy at its own startup and caches it (FactDatabase.storage), so closing the MongoClient on a PlugMan//reload left that binding on a dead connection → "Mongo storeFacts failed: state should be: open". onDisable no longer closes the client or unloads the Koin module on a runtime disable — it still flushes online players' facts first, so nothing is lost. Because the binding is cached, a full server restart is needed to re-point Typewriter's hook; the README's reload note documents this.

Testing

  • ./gradlew shadowJar builds cleanly.
  • New regression tests: a Docker-free MockK test for the eviction race, plus Testcontainers tests for presence-based reconciliation. The Testcontainers suite requires Docker (not run in this environment).

🤖 Generated with Claude Code

Chikage04 and others added 12 commits June 26, 2026 02:03
Two defects in the per-player fact sync could delete facts from the
shared MongoDB on a server switch:

1. reconcileGroup() keyed its delete-missing on the *persistable* subset
   of the cache. Any fact whose entry isn't defined/loaded on the current
   server failed isPersistable() and was wiped from the shared store
   (symptom: "Loaded 10 -> Synced 2 -> lost 8"). Deletion is now keyed on
   cache *presence*: reconcileGroup(groupId, upserts, present) only removes
   facts the player genuinely no longer has, never ones this server merely
   can't persist.

2. The periodic async flush could run after a player was evicted on quit,
   observing an empty cache and issuing deleteMany() that wiped every fact.
   FactSessionSync now gates flushes on per-player ownership and serialises
   load/flush/evict with a striped lock, so an evicted or never-loaded
   player is never reconciled.

Adds an injectable cache provider to FactSessionSync for testing, a MockK
unit test for the eviction race, and Testcontainers tests covering
presence-based reconciliation.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
DatabaseConfig only read `uri` and `name`, so the `username`, `password`
and `auth_source` config keys were silently ignored — a secured MongoDB
rejected every operation with "Command find requires authentication"
(error 13). The Mongo client is now built from MongoClientSettings:

- optional username/password/auth_source are applied as a MongoCredential
  (auth_source defaults to the database name), so the password no longer
  has to be URL-encoded into the URI; an explicit credential overrides one
  embedded in the URI;
- `timeout_ms` (previously parsed but unused) is applied as the server
  selection timeout.

The default config.yml documents the new auth keys.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Typewriter resolves Inkwell's FactStorage once via `by lazy` at its own
startup and caches it (FactDatabase.storage). Closing the MongoClient on a
runtime disable (PlugMan / /reload) therefore left that cached binding
pointing at a dead connection, so Typewriter's periodic storeFacts failed
with "state should be: open". onDisable no longer closes the client or
unloads the Koin module on a reload — it still flushes online players'
facts first, so nothing is lost. Because the binding is cached, a full
server restart is needed to re-point Typewriter's hook; the README's
reload note documents this.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Specs the fact_catalog (name↔id, upsert-only) + PlaceholderAPI
%inkwell_fact_<name>% reading the value from Typewriter's already-loaded
fact cache, so facts display on servers without the defining page — no
page merging.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01DSbnaoxxpzqfrNexWsaVj5
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01DSbnaoxxpzqfrNexWsaVj5
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01DSbnaoxxpzqfrNexWsaVj5
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01DSbnaoxxpzqfrNexWsaVj5
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01DSbnaoxxpzqfrNexWsaVj5
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01DSbnaoxxpzqfrNexWsaVj5
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01DSbnaoxxpzqfrNexWsaVj5
…he plugin

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01DSbnaoxxpzqfrNexWsaVj5
The placeholder now takes the entry id directly and reads the value from the
already-loaded fact cache, so the name->id catalog is unnecessary. Removes
MongoFactCatalog/FactCatalogPublisher and their tests/config; the fact_catalog
toggle becomes fact_placeholder.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01DSbnaoxxpzqfrNexWsaVj5
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