diff --git a/README.md b/README.md index 68c6ad0..90bf18f 100644 --- a/README.md +++ b/README.md @@ -27,6 +27,10 @@ Running tests works as follows: bundle exec rspec ``` +Leader/follower tournament sync docs: + +- [doc/leader_follower.md](doc/leader_follower.md) + ## Docker [Registry](https://gitlab.com/turniere/turniere-backend/container_registry) diff --git a/doc/leader_follower.md b/doc/leader_follower.md new file mode 100644 index 0000000..9319640 --- /dev/null +++ b/doc/leader_follower.md @@ -0,0 +1,157 @@ +# Leader/Follower Tournament Sync + +Backend supports one-way tournament replication: + +- leader tournament stays writable +- follower tournament stays read only +- leader pushes full snapshots asynchronously +- follower never pulls +- follower never needs to reach leader + +This fits live topology: + +- leader = local laptop / Raspberry Pi / LAN host without public IP +- follower = internet VM with public HTTPS endpoint +- only leader needs outbound access to follower + +## Live Setup + +### 1. Create follower tournament on remote backend + +Create empty tournament with: + +- `read_only_mode: true` +- `sync_auth_token: ` + +Do not send `teams` payload for follower. Backend allows empty read only follower tournament creation. + +### 2. Create normal leader tournament on local backend + +Create tournament as usual. This remains normal writable tournament. + +### 3. Configure leader to push to follower + +Update leader tournament with: + +- `sync_target_url: https://remote.example/tournaments/:id/sync_state` +- `sync_auth_token: ` + +Leader becomes push source. Follower accepts only authenticated snapshot pushes. + +### 4. Run tournament normally on leader + +These leader-side actions enqueue async snapshot push: + +- tournament updates +- timer updates +- team renames +- match state changes +- match score changes +- stage finish / playoff generation + +Queue retries with backoff until follower accepts snapshot. + +## Takeover + +If remote must become writable after sync phase: + +1. disable `read_only_mode` on follower +2. backend clears follower token when no longer needed +3. follower becomes normal standalone tournament again + +After takeover, leader should no longer push to old follower URL. + +## API Contract + +### Leader-side writable config + +Tournament push mode active when: + +- `sync_target_url` present +- `sync_auth_token` present +- `read_only_mode` false + +### Follower-side push receiver + +Tournament accepts push when: + +- `read_only_mode` true +- `sync_auth_token` present + +Push endpoint: + +- `PATCH /tournaments/:id/sync_state` + +Auth: + +- `Authorization: Bearer ` + +Payload: + +- `{ snapshot: ... }` + +Snapshot import replaces tournament graph using `sync_source_id` mapping, not local DB ids. + +## Frontend Integration Notes + +Frontend does not need separate "follower UI mode" for normal viewers. Remote tournament should look same for non-owner users. + +Owner-facing frontend can expose: + +- `read_only_mode` toggle +- `sync_target_url` input +- `sync_auth_token` input +- sync health display: + - `sync_last_pushed_at` + - `sync_last_push_error` + +Recommended owner UX: + +1. create remote follower first +2. copy remote `sync_auth_token` +3. paste remote `sync_state` URL into leader +4. save leader sync config +5. show last push time / error state +6. allow explicit follower takeover by disabling `read_only_mode` + +Frontend should block owner edits on follower when `read_only_mode` true. Backend already enforces this with `423 Locked`. + +## Future Feature Rule + +Any new persisted tournament state must be handled in three places: + +1. leader write path +2. snapshot export/import path +3. follower/read-only behavior + +Guardrail now exists: + +- `TournamentSyncSchema` lists every synced and ignored model column +- spec fails when new DB column appears without explicit sync decision + +This does not auto-implement sync for new features. It makes missing sync work fail loudly in tests instead of silently drifting. + +For every new tournament feature, add: + +1. schema decision in `TournamentSyncSchema` +2. snapshot builder/importer support if state must replicate +3. one follower lifecycle test or roundtrip spec that proves behavior survives sync + +## Coverage Today + +Current backend coverage includes: + +- async push queue and retry behavior +- follower auth and read-only lock +- full follower lifecycle HTTP E2E with unrelated tournaments and mismatched ids +- team renames +- multiple decider matches +- playoff generation blocked before deciders finish +- playoff generation after deciders finish +- follower takeover +- large roundtrip graph coverage for 32 groups, 4 teams each, 64-team playoffs + +Still not magic: + +- if new feature adds new state and nobody updates sync contract, tests will fail +- if new feature changes behavior but not schema, add behavior test too