Phase 6: docs (CHANGELOG, ERROR_LOG, TEST_PROCEDURE)
Adds three documentation files covering the Phase 0-5 hardening work:
CHANGELOG.md
- Bilingual EN/FR, strict template (English first, then ---, then French).
- Version section 2.1.5 dated 2026-04-22 (NO version bump per
CLAUDE.md version-lock rule).
- Sections: Fixed / Added / Changed / Correctifs / Ajouts / Modifications.
ERROR_LOG.md
- Journal of 8 bugs discovered and fixed during the hardening sweep.
- Each entry: Context / Error / Root cause / Fix / Prevention rule.
- Cross-references commits bea5f80 / c84f920 / 746cb56 / c70ca9f / bd0482c.
TEST_PROCEDURE_v2.1.5.html
- Self-contained HTML (no external deps), bilingual EN/FR.
- 10 test scenarios tagged CRITICAL / HIGH / MEDIUM with Setup, Steps,
Expected Results, and a regression-check block.
- Covers: drop+deco+reco, backpack dup, SS shulker dup, kill -9 recovery,
zombie-peer short-circuit, periodic save, pool stats, heartbeat,
curios cap unavailable, cross-server claim.
This commit is contained in:
parent
bd0482cb76
commit
a83543853c
61
CHANGELOG.md
Normal file
61
CHANGELOG.md
Normal file
|
|
@ -0,0 +1,61 @@
|
|||
# Changelog
|
||||
|
||||
All notable changes to **PlayerSync** are documented here.
|
||||
|
||||
---
|
||||
|
||||
## [2.1.5] - 2026-04-22
|
||||
|
||||
### Fixed (English first)
|
||||
|
||||
- **Critical item duplication on drop + quick disconnect + reconnect** — Race condition between the auto-save background task and the logout background task could commit a stale snapshot AFTER the logout save, resurrecting dropped items. Triple guard now applied: `pendingLogoutSaves` check (early + under lock) and `SELECT online FROM player_data` skip if logout already committed. Logout BG now acquires `bgLock` with blocking `.lock()` for proper serialization.
|
||||
- **Backpack / Sophisticated Storage merge-on-restore duplication** — `setBackpackContents` / `setStorageContents` upstream are shallow merges, not replaces. Restore now calls `removeBackpackContents` / `removeStorageContents` (with reflection fallback if absent) AND passes a defensive NBT copy. Fixes mass-duplication of items in backpacks/shulkers on every cross-server transfer.
|
||||
- **Cross-server save overwrite** — When `writeSnapshotToDB`'s `last_server` guard blocked the core player_data UPDATE, the downstream backpack/SS/RS2 saves still executed and overwrote the claiming server's data. The function now returns a boolean; all 5 callers short-circuit downstream writes on guard block.
|
||||
- **30-second join delay on zombie peer servers** — `doPlayerJoin` poll waited the full 60 attempts (30s) for server_ids that no longer existed (legacy `server_id=0` rows, or peers that crashed without clearing `online=0`). New `isPeerServerStale` check (peer_id=0 OR heartbeat >60s) takes over immediately and force-clears the orphaned flag. Poll max raised from 60 to 120 attempts (60s) for legitimate slow shutdowns.
|
||||
- **Curios wipe on dead player** — Legacy `StoreCurios` wrote an empty flatMap when the Curios capability was unavailable, wiping DB data. Now early-returns with a WARN log.
|
||||
|
||||
### Added
|
||||
|
||||
- **JVM shutdown hook (kill -9 / OOM / SIGTERM recovery)** — New `CrashRecovery.installShutdownHook` registers a non-daemon hook that calls `VanillaSync.emergencyFlushAll` synchronously to snapshot and write every online player before process exit. Marks `server_info.enable=0` so peers detect the shutdown.
|
||||
- **Startup orphan-flag recovery** — `CrashRecovery.clearOrphanedOnlineFlags` runs at `onServerStarting` to clear any `player_data.online=1` rows left by a previous ungraceful exit. Logs the count via `SyncLogger`.
|
||||
- **Zombie-peer reporter** — `CrashRecovery.reportZombiePeers` logs peer `server_id`s whose heartbeat is stale or missing at boot time.
|
||||
- **Server heartbeat service** — `HeartbeatService` pings `server_info.last_update` every 10 seconds so peer servers can distinguish live from dead via the new `isPeerServerStale` check.
|
||||
- **Periodic full-save scheduler** — `PeriodicSaveService` triggers a complete save (player data + backpacks + SS + RS2) for every online synced player every `auto_save_interval_minutes` (new config, default 10, range 0-1440). Independent of NeoForge's vanilla `PlayerEvent.SaveToFile` cadence.
|
||||
- **Dimension-change save trigger** — New `onPlayerChangeDimension` handler, gated by `save_on_dimension_change` config (default false). Protects against mid-teleport crashes.
|
||||
- **Executor + HikariCP pool stats reporter** — `PoolStatsReporter` logs `[POOL] executor active/queue/idle, hikari active/idle` every 5 minutes. WARN thresholds trigger when queue >400/512 or Hikari active >=14/15.
|
||||
- **Structured logging events** — `SyncLogger` gained `containerForceClosed`, `modCompatSkip`, `modCompatSaved`, `modCompatRestored`, `storageSave`, `poolStats`, `warnPlayer`, `nbtAnomaly` for finer-grained diagnostics.
|
||||
|
||||
### Changed
|
||||
|
||||
- **`writeSnapshotToDB` signature** — Now returns `boolean` instead of `void`. `true` means the core UPDATE persisted, `false` means the `last_server` guard blocked. All callers MUST check the return before firing downstream backpack/SS/RS2 writes.
|
||||
- **Default `auto_save_interval_minutes`** — 10 min (new config key). Trades data-loss window on crash for DB load. Set to 0 to disable.
|
||||
- **Backpack / SS restore** — Now uses two-step clear (public API + reflection fallback) and defensive NBT copy before upstream setter. Full log line per restore with `cleared_via=api|reflection` and `nbt_keys=N`.
|
||||
|
||||
---
|
||||
|
||||
### Correctifs (French mirror)
|
||||
|
||||
- **Duplication d'items critique lors d'un drop + déconnexion rapide + reconnexion** — Race condition entre la task auto-save background et la task logout background pouvait commiter un snapshot périmé APRÈS le save logout, ressuscitant les items drop. Triple garde maintenant appliquée : check `pendingLogoutSaves` (early + sous lock) et skip via `SELECT online FROM player_data` si le logout a déjà commité. La task logout BG acquiert maintenant `bgLock` en blocking `.lock()` pour sérialiser proprement.
|
||||
- **Duplication Backpack / Sophisticated Storage par merge au restore** — `setBackpackContents` / `setStorageContents` en amont sont des merges shallow, pas des replaces. Le restore appelle maintenant `removeBackpackContents` / `removeStorageContents` (avec fallback reflection si absent) ET passe une copie défensive du NBT. Corrige la duplication massive d'items dans les backpacks/shulkers à chaque transfert cross-server.
|
||||
- **Écrasement cross-server des saves** — Quand le guard `last_server` de `writeSnapshotToDB` bloquait l'UPDATE core player_data, les saves downstream backpack/SS/RS2 s'exécutaient quand même et écrasaient les données du serveur ayant claim. La fonction retourne maintenant un boolean ; les 5 callers court-circuitent les writes downstream en cas de guard block.
|
||||
- **Délai de 30 secondes à la connexion sur serveurs zombies** — Le poll `doPlayerJoin` attendait les 60 tentatives (30s) pour des `server_id` n'existant plus (lignes legacy `server_id=0`, ou peers ayant crashé sans clear `online=0`). Nouveau check `isPeerServerStale` (peer_id=0 OU heartbeat >60s) prend la main immédiatement et force-clear le flag orphelin. Poll max passé de 60 à 120 tentatives (60s) pour couvrir les shutdowns lents légitimes.
|
||||
- **Wipe Curios sur joueur mort** — La méthode legacy `StoreCurios` écrivait un flatMap vide quand la capability Curios était absente, wipant les données DB. Elle early-return maintenant avec un log WARN.
|
||||
|
||||
### Ajouts (French mirror)
|
||||
|
||||
- **Hook JVM shutdown (kill -9 / OOM / SIGTERM recovery)** — Nouveau `CrashRecovery.installShutdownHook` enregistre un hook non-daemon qui appelle `VanillaSync.emergencyFlushAll` synchronement pour snapshot et écrire chaque joueur online avant la fin du process. Marque `server_info.enable=0` pour que les peers détectent le shutdown.
|
||||
- **Recovery des flags orphelins au boot** — `CrashRecovery.clearOrphanedOnlineFlags` tourne au `onServerStarting` pour clear les rows `player_data.online=1` laissées par une sortie ungracieuse précédente. Log le compte via `SyncLogger`.
|
||||
- **Reporter de peers zombies** — `CrashRecovery.reportZombiePeers` log les `server_id` peers dont le heartbeat est stale ou absent au boot.
|
||||
- **Service heartbeat** — `HeartbeatService` ping `server_info.last_update` toutes les 10 secondes pour que les peers distinguent live vs dead via le nouveau check `isPeerServerStale`.
|
||||
- **Scheduler de sauvegarde périodique** — `PeriodicSaveService` déclenche une save complète (player data + backpacks + SS + RS2) pour chaque joueur online synced toutes les `auto_save_interval_minutes` (nouvelle config, défaut 10, plage 0-1440). Indépendant de la cadence vanilla `PlayerEvent.SaveToFile` de NeoForge.
|
||||
- **Trigger save sur changement de dimension** — Nouveau handler `onPlayerChangeDimension`, gated par la config `save_on_dimension_change` (défaut false). Protège contre les crashes en plein téléport.
|
||||
- **Reporter stats executor + HikariCP** — `PoolStatsReporter` log `[POOL] executor active/queue/idle, hikari active/idle` toutes les 5 min. Seuils WARN quand queue >400/512 ou Hikari active >=14/15.
|
||||
- **Événements structurés** — `SyncLogger` a gagné `containerForceClosed`, `modCompatSkip`, `modCompatSaved`, `modCompatRestored`, `storageSave`, `poolStats`, `warnPlayer`, `nbtAnomaly` pour un diagnostic plus fin.
|
||||
|
||||
### Modifications
|
||||
|
||||
- **Signature `writeSnapshotToDB`** — Retourne maintenant `boolean` au lieu de `void`. `true` = l'UPDATE core a persisté, `false` = le guard `last_server` a bloqué. Tous les callers DOIVENT vérifier le retour avant de déclencher les writes downstream backpack/SS/RS2.
|
||||
- **Défaut `auto_save_interval_minutes`** — 10 min (nouvelle clé config). Trade-off entre fenêtre de perte de données sur crash et charge DB. 0 pour désactiver.
|
||||
- **Restore Backpack / SS** — Utilise maintenant un clear en deux étapes (API publique + fallback reflection) et une copie défensive NBT avant le setter upstream. Log complet par restore avec `cleared_via=api|reflection` et `nbt_keys=N`.
|
||||
|
||||
---
|
||||
141
ERROR_LOG.md
Normal file
141
ERROR_LOG.md
Normal file
|
|
@ -0,0 +1,141 @@
|
|||
# PlayerSync — Error Log
|
||||
|
||||
Journal des erreurs rencontrées et corrigées. Chaque entrée documente un bug, sa cause racine, son correctif et la règle de prévention à appliquer systématiquement.
|
||||
|
||||
---
|
||||
|
||||
## [2026-04-22 02:54] — Item duplication on drop + quick disconnect + reconnect
|
||||
|
||||
**Context** : Un joueur drop un item au sol, se déconnecte très rapidement, puis se reconnecte → l'item est présent deux fois (en inventory restauré + encore au sol).
|
||||
|
||||
**Error** : Duplication systématique reproductible en production.
|
||||
|
||||
**Root cause** : Race condition entre `onPlayerSaveToFile` background task (auto-save périodique) et `onPlayerLogout` background task.
|
||||
1. `SaveToFile` capture un snapshot sur main thread AVANT le drop (item encore en inventory) → task async soumise.
|
||||
2. Le joueur drop l'item → inventory vide, ItemEntity dans le monde.
|
||||
3. Le joueur disconnect → logout capture un snapshot FRESH (sans item), soumet le write.
|
||||
4. Les deux BG tasks s'exécutent en parallèle. Si la task auto-save (qui portait une snapshot STALE avec l'item) commit APRÈS la task logout (qui portait FRESH sans l'item), la DB finit en STALE.
|
||||
5. Reconnexion → inventory restauré avec l'item + ItemEntity toujours au sol → 2 copies.
|
||||
|
||||
**Fix** (commit `bea5f80`) : Triple guard dans l'auto-save BG task :
|
||||
- Early skip si `pendingLogoutSaves.containsKey(uuid)` avant tryLock.
|
||||
- Re-check sous lock après tryLock (race window fermée).
|
||||
- `SELECT online FROM player_data WHERE uuid=?` — skip si online=0 (logout a committé).
|
||||
|
||||
Logout BG task acquiert maintenant `bgLock.lock()` (blocking) pour sérialiser proprement avec les auto-save BG qui utilisent `tryLock`. `removePlayerLock` réordonné avant `bgLock.unlock()` pour que les auto-save BG qui wake après unlock voient `containsKey=false` et skip.
|
||||
|
||||
**Prevention** : **JAMAIS de BG task qui modifie la DB sans un guard `online=0` + `pendingLogoutSaves` check**. Si deux paths peuvent écrire le même row, ils DOIVENT partager un lock blocking OU le path "fresh" doit être détectable via DB state (online flag, version column).
|
||||
|
||||
---
|
||||
|
||||
## [2026-04-22 03:15] — Backpack duplication on cross-server transfer
|
||||
|
||||
**Context** : Un joueur utilise un backpack Sophisticated Backpacks sur Server A, change de serveur, et constate que le contenu du backpack est dupliqué.
|
||||
|
||||
**Error** : Duplication systématique d'items dans les backpacks et shulkers Sophisticated Storage lors de transferts cross-server ou reconnexions.
|
||||
|
||||
**Root cause** : `BackpackStorage.setBackpackContents()` et `ItemContentsStorage.setStorageContents()` en amont sont des **merges shallow**, pas des replaces. Quand le restore applique le snapshot sauvegardé, il MERGE avec les contents existants en mémoire (SavedData persistée sur disk localement ou vue ouverte par un autre joueur). Les sous-tags "items" survivent → duplication.
|
||||
|
||||
**Fix** (commit `c84f920`) :
|
||||
- Backpack : appel `store.removeBackpackContents(uuid)` EXPLICITE avant `setBackpackContents`. Si l'API throw (absent dans certaines versions), fallback reflection qui parcourt les champs `Map` de `BackpackStorage` et remove l'entrée directement.
|
||||
- SS : nouveau helper `clearSSStorageContents` qui tente `removeStorageContents(UUID)` via reflection, puis fallback reflection sur champs Map. `setDirty()` appelé pour forcer le flush.
|
||||
- Les deux paths passent maintenant une **copie défensive** (`nbt.copy()`) à l'upstream setter, jamais la référence partagée.
|
||||
|
||||
**Prevention** :
|
||||
- **Toujours clear avant restore pour toute structure qui merge au lieu de replace** (backpack, SS, RS2 disks).
|
||||
- **Toujours passer une copie défensive** d'un CompoundTag à un setter qui peut la stocker en interne.
|
||||
- **Logger `clear_via=api/reflection`** pour diagnostiquer les régressions upstream.
|
||||
|
||||
---
|
||||
|
||||
## [2026-04-22 03:20] — Cross-server saves can overwrite claimed data
|
||||
|
||||
**Context** : Deux serveurs sauvent un même joueur simultanément (edge case lors de changements de serveurs rapides).
|
||||
|
||||
**Error** : Les données de l'un écrasent silencieusement les données de l'autre. Backpack/SS/RS2 perdus.
|
||||
|
||||
**Root cause** : `writeSnapshotToDB` retournait `void`. Même si son guard `last_server=?` bloquait le write du core player_data (rows affected = 0), les appels downstream `saveBackpackSnapshots` / `saveSSSnapshots` / `saveRS2DisksByLevel` s'exécutaient INCONDITIONNELLEMENT et écrasaient `backpack_data` (qui n'a pas de guard propre — keyé par storage UUID, pas player UUID).
|
||||
|
||||
**Fix** (commit `c84f920`) : `writeSnapshotToDB` retourne maintenant `boolean`. Les 5 callers (logout, shutdown, auto-save SaveToFile, staggered auto-save, death-save) vérifient le retour et **short-circuitent** les writes downstream si le core a été blocké.
|
||||
|
||||
**Prevention** : **Une fonction qui a un guard silencieux DOIT signaler son résultat au caller**. Ne jamais supposer que les writes downstream sont implicitement protégés par un guard en amont — vérifier explicitement.
|
||||
|
||||
---
|
||||
|
||||
## [2026-04-22 03:25] — 30s delay on player join (RACE timeout 60/60)
|
||||
|
||||
**Context** : À chaque connexion, log flood `Waiting for server X to finish saving (attempt 60/60)` et le joueur attend 30s avant de récupérer ses données.
|
||||
|
||||
**Error** : Poll timeout systématique sur des server_ids qui n'existent plus ou sur un server_id=0.
|
||||
|
||||
**Root cause** :
|
||||
- Le poll `doPlayerJoin` attend que l'autre serveur clear `online=0`. Si l'autre serveur a crashé sans le faire (pas de shutdown hook), le poll attend jusqu'à épuisement des 60 tentatives.
|
||||
- `server_id=0` est une ligne orpheline héritée d'une écriture legacy (avant que le default `Random().nextInt(1, MAX-1)` soit appliqué).
|
||||
|
||||
**Fix** (commit `c84f920`) :
|
||||
- Nouvelle méthode `isPeerServerStale(peerId, staleMs)` qui check `server_info.last_update`. Si l'heartbeat est vieux de >60s OU si `peerId == 0`, le poll considère le serveur comme zombie et force-clear `online=0`.
|
||||
- Poll max passé de 60 à 120 tentatives (60s total) pour couvrir les shutdowns lents.
|
||||
- Phase 3 : `HeartbeatService` tick toutes les 10s → permet aux peers de détecter les zombies.
|
||||
- Phase 3 : `CrashRecovery.clearOrphanedOnlineFlags()` au boot → nettoie les rows stuck à online=1 après un crash ungracieux.
|
||||
|
||||
**Prevention** : **Tout état "en cours" en DB doit avoir un heartbeat OU un timeout**. Un flag `online=1` sans heartbeat est un bug en attendant de se produire (le process qui l'a set peut crasher).
|
||||
|
||||
---
|
||||
|
||||
## [2026-04-22 03:30] — StoreCurios NPE / data wipe on dead player
|
||||
|
||||
**Context** : Un joueur meurt puis se déconnecte rapidement. Son curios sont vidés de la DB.
|
||||
|
||||
**Error** : Méthode legacy `StoreCurios` écrivait un flatMap vide quand `CuriosApi.getCuriosInventory(player)` retournait un `Optional.empty()` (capability détachée après death).
|
||||
|
||||
**Root cause** : La méthode utilisait `handlerOpt.ifPresent(...)` mais fallait au `REPLACE INTO` même si le flatMap était vide → wipe DB data pour un joueur mort.
|
||||
|
||||
**Fix** (commit `c84f920`) : Early return avec log `WARN [store-curios] handler unavailable for UUID — skipping write to avoid wiping DB data` si `handlerOpt.isEmpty()`.
|
||||
|
||||
**Prevention** : **Ne JAMAIS écrire un état "vide" dans la DB si la source est incertaine**. Une capability absente ≠ joueur sans curios — c'est un état indéterminé. Skip write + log.
|
||||
|
||||
---
|
||||
|
||||
## [2026-04-22 03:40] — Player data loss on kill -9 / OOM
|
||||
|
||||
**Context** : Process serveur tué via `kill -9` ou OOM — au redémarrage, les joueurs qui étaient online ne récupèrent pas leurs données des dernières minutes.
|
||||
|
||||
**Error** : `ServerStoppingEvent` n'est pas déclenché lors d'un kill ungracieux, donc aucune save n'est exécutée. Les rows `player_data` restent aussi à `online=1` → le poll de doPlayerJoin sur un autre serveur attend 30s pour rien.
|
||||
|
||||
**Fix** (commit `746cb56`, Phase 3) :
|
||||
- `CrashRecovery.installShutdownHook(() -> emergencyFlushAll())` — JVM hook non-daemon enregistré au boot. Appelle une méthode synchrone qui snapshot et write tous les joueurs online sans passer par l'executor (qui peut être déjà mort).
|
||||
- Marque `server_info.enable=0` à la sortie pour notifier les peers.
|
||||
- `CrashRecovery.clearOrphanedOnlineFlags()` au boot suivant — clear les rows stuck et log le nombre via SyncLogger.
|
||||
- `HeartbeatService` tick toutes les 10s pendant le run — permet aux peers de détecter la mort.
|
||||
|
||||
**Prevention** :
|
||||
- **Tout process long-running doit avoir un JVM shutdown hook** pour couvrir SIGTERM / kill doux / OOM soft.
|
||||
- **Tout flag "en cours" persistant doit avoir un recovery path au boot suivant**.
|
||||
- **Un heartbeat périodique est obligatoire** si d'autres processus dépendent de savoir si on est alive.
|
||||
|
||||
---
|
||||
|
||||
## [2026-04-22 03:50] — Inventory loss window of 30 min between auto-saves
|
||||
|
||||
**Context** : Les auto-saves ne se déclenchaient que lors des PlayerEvent.SaveToFile natifs (cadence vanilla = autosave world, typiquement 6000 ticks). Si un crash survenait entre deux saves, jusqu'à 15+ minutes de jeu étaient perdus.
|
||||
|
||||
**Fix** (commit `c70ca9f`, Phase 4) :
|
||||
- `PeriodicSaveService` — scheduler indépendant qui déclenche un full-flush toutes les `auto_save_interval_minutes` (défaut 10). Hops au main thread pour snapshotter, puis soumet les writes async via `snapshotAndQueueSave`.
|
||||
- `onPlayerChangeDimension` — trigger additionnel gated par `save_on_dimension_change` (défaut false). Sauve avant teleport cross-dimension.
|
||||
|
||||
**Prevention** : **Ne jamais dépendre uniquement des events du framework** pour déclencher une sauvegarde critique. Doubler avec un scheduler indépendant et rendre l'intervalle configurable.
|
||||
|
||||
---
|
||||
|
||||
## [2026-04-22 04:00] — Executor queue saturation invisible
|
||||
|
||||
**Context** : Sous charge (35+ joueurs), l'executor `PlayerSync` peut saturer (queue >400) et déclencher `CallerRunsPolicy` qui bloque le main thread. Aucune alerte dans les logs.
|
||||
|
||||
**Fix** (commit `bd0482c`, Phase 5) :
|
||||
- `PoolStatsReporter` — scheduler dédié 5-min qui log `[POOL] executor active/queue/idle, hikari active/idle`.
|
||||
- WARN log si queue > 400/512 ou hikari active >= 14/15.
|
||||
- Accesseur `JDBCsetUp.getPoolMXBean()` pour exposer Hikari en read-only.
|
||||
|
||||
**Prevention** : **Tout pool/queue critique doit être monitoré périodiquement** avec des seuils d'alerte sous la capacité max. Invisible ≠ sain.
|
||||
|
||||
---
|
||||
523
TEST_PROCEDURE_v2.1.5.html
Normal file
523
TEST_PROCEDURE_v2.1.5.html
Normal file
|
|
@ -0,0 +1,523 @@
|
|||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<title>Test Procedure — PlayerSync v2.1.5</title>
|
||||
<style>
|
||||
body { font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif; max-width: 1100px; margin: 2em auto; padding: 0 1em; color: #222; line-height: 1.55; }
|
||||
h1 { border-bottom: 3px solid #007acc; padding-bottom: 0.3em; }
|
||||
h2 { color: #007acc; margin-top: 1.8em; border-bottom: 1px solid #ddd; padding-bottom: 0.2em; }
|
||||
h3 { color: #333; margin-top: 1.3em; }
|
||||
.scenario { background: #f6f8fa; border-left: 4px solid #007acc; padding: 1em 1.2em; margin: 1em 0; border-radius: 4px; }
|
||||
.scenario.critical { border-left-color: #d73a49; }
|
||||
.scenario.high { border-left-color: #f0983a; }
|
||||
.meta { font-size: 0.9em; color: #666; margin-bottom: 0.5em; }
|
||||
.steps, .expected { margin: 0.5em 0; }
|
||||
.steps li, .expected li { margin: 0.25em 0; }
|
||||
code { background: #eef1f4; padding: 0.1em 0.4em; border-radius: 3px; font-size: 0.92em; }
|
||||
.lang-sep { margin: 4em 0 2em; border-top: 2px dashed #999; padding-top: 2em; }
|
||||
.checkbox { margin-right: 0.5em; }
|
||||
.tag { display: inline-block; padding: 0.1em 0.5em; border-radius: 3px; font-size: 0.8em; font-weight: bold; color: white; }
|
||||
.tag-critical { background: #d73a49; }
|
||||
.tag-high { background: #f0983a; }
|
||||
.tag-medium { background: #6f42c1; }
|
||||
.regression { background: #fff5f5; border: 1px solid #feb2b2; padding: 1em; border-radius: 4px; margin-top: 2em; }
|
||||
table { border-collapse: collapse; width: 100%; margin: 1em 0; }
|
||||
th, td { border: 1px solid #ddd; padding: 0.5em 0.8em; text-align: left; }
|
||||
th { background: #007acc; color: white; }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
|
||||
<h1>Test Procedure — PlayerSync v2.1.5</h1>
|
||||
<div class="meta">Date : <strong>2026-04-22</strong> | Branch: <code>1.21.1-dev</code> | Minecraft 1.21.1 / NeoForge 21.1.137 / Java 21</div>
|
||||
|
||||
<h2>Setup</h2>
|
||||
|
||||
<ol>
|
||||
<li>Démarrer MariaDB dev : <code>docker compose up -d</code></li>
|
||||
<li>Build : <code>./gradlew build</code> — le JAR apparaît dans <code>build/libs/playersync-1.21.1-2.1.5.jar</code></li>
|
||||
<li>Deux instances serveur nécessaires : <code>./gradlew runServer</code> (Server A) + copie avec <code>Server_id</code> différent dans <code>run-2/config/playersync-common.toml</code> (Server B)</li>
|
||||
<li>Adminer : <code>http://localhost:8080</code> (login <code>playersync</code>/<code>playersync</code>)</li>
|
||||
<li>Monitorer en continu : <code>tail -f run/logs/playersync/sync.log</code></li>
|
||||
</ol>
|
||||
|
||||
<h2>Scenarios to test</h2>
|
||||
|
||||
<div class="scenario critical">
|
||||
<span class="tag tag-critical">CRITICAL</span>
|
||||
<h3>1. Drop + deco rapide + reco (regression Phase 0)</h3>
|
||||
<div class="steps">
|
||||
<strong>Steps:</strong>
|
||||
<ol>
|
||||
<li><input type="checkbox" class="checkbox"> Join Server A, fill inventory with a diamond sword</li>
|
||||
<li><input type="checkbox" class="checkbox"> Drop the sword with Q</li>
|
||||
<li><input type="checkbox" class="checkbox"> Immediately disconnect (within 1 second)</li>
|
||||
<li><input type="checkbox" class="checkbox"> Rejoin Server A</li>
|
||||
</ol>
|
||||
</div>
|
||||
<div class="expected">
|
||||
<strong>Expected:</strong>
|
||||
<ul>
|
||||
<li>Inventory does NOT contain the sword</li>
|
||||
<li>The ItemEntity is still on the ground where dropped</li>
|
||||
<li>Player has exactly 1 copy of the sword total</li>
|
||||
<li>Log shows <code>[SAVE] LOGOUT completed</code> then either no SaveToFile BG write or a <code>[GUARD] SaveToFile BG skipped — player already offline in DB</code></li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="scenario critical">
|
||||
<span class="tag tag-critical">CRITICAL</span>
|
||||
<h3>2. Backpack duplication (Sophisticated Backpacks)</h3>
|
||||
<div class="steps">
|
||||
<strong>Steps:</strong>
|
||||
<ol>
|
||||
<li><input type="checkbox" class="checkbox"> Join Server A, craft a SophisticatedBackpack, fill with 10 diamond blocks</li>
|
||||
<li><input type="checkbox" class="checkbox"> Disconnect</li>
|
||||
<li><input type="checkbox" class="checkbox"> Join Server B (configure different Server_id)</li>
|
||||
<li><input type="checkbox" class="checkbox"> Open backpack, count diamond blocks</li>
|
||||
</ol>
|
||||
</div>
|
||||
<div class="expected">
|
||||
<strong>Expected:</strong>
|
||||
<ul>
|
||||
<li>Exactly 10 diamond blocks (no duplication)</li>
|
||||
<li>Log shows <code>[restore-backpack] uuid=... nbt_keys=... cleared_via=api</code> (or <code>reflection</code> as fallback)</li>
|
||||
<li>No WARN about failed removeBackpackContents</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="scenario critical">
|
||||
<span class="tag tag-critical">CRITICAL</span>
|
||||
<h3>3. Sophisticated Storage shulker duplication</h3>
|
||||
<div class="steps">
|
||||
<strong>Steps:</strong>
|
||||
<ol>
|
||||
<li><input type="checkbox" class="checkbox"> Join Server A, pack a diamond-filled shulker into your inventory</li>
|
||||
<li><input type="checkbox" class="checkbox"> Have Player B (on same server) open your inventory via admin / trade / viewer</li>
|
||||
<li><input type="checkbox" class="checkbox"> Disconnect Player A</li>
|
||||
<li><input type="checkbox" class="checkbox"> Player A reconnects to Server B</li>
|
||||
<li><input type="checkbox" class="checkbox"> Unpack shulker, count diamonds</li>
|
||||
</ol>
|
||||
</div>
|
||||
<div class="expected">
|
||||
<strong>Expected:</strong>
|
||||
<ul>
|
||||
<li>Exactly original diamond count</li>
|
||||
<li>Log shows <code>[CONTAINER_CLOSE]</code> for Player B (viewer forced closed)</li>
|
||||
<li>Log shows <code>[restore-ss] uuid=... nbt_keys=...</code></li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="scenario critical">
|
||||
<span class="tag tag-critical">CRITICAL</span>
|
||||
<h3>4. Kill -9 / OOM recovery</h3>
|
||||
<div class="steps">
|
||||
<strong>Steps:</strong>
|
||||
<ol>
|
||||
<li><input type="checkbox" class="checkbox"> Join Server A, set inventory to known state (put a named diamond)</li>
|
||||
<li><input type="checkbox" class="checkbox"> Find server java PID : <code>jps | grep Forge</code></li>
|
||||
<li><input type="checkbox" class="checkbox"> Kill brutally : <code>kill -9 <pid></code> (or Task Manager → End Task on Windows)</li>
|
||||
<li><input type="checkbox" class="checkbox"> Restart server A</li>
|
||||
<li><input type="checkbox" class="checkbox"> Join Server A, check inventory</li>
|
||||
</ol>
|
||||
</div>
|
||||
<div class="expected">
|
||||
<strong>Expected:</strong>
|
||||
<ul>
|
||||
<li>On startup log: <code>[crash-recovery] cleared N orphan online=1 rows</code></li>
|
||||
<li>On startup log: <code>[crash-recovery] JVM shutdown hook installed</code> AND ideally (if hook ran): <code>[emergency-flush] flushed N players</code></li>
|
||||
<li>Inventory matches last state before kill (within ~10 min auto-save window)</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="scenario high">
|
||||
<span class="tag tag-high">HIGH</span>
|
||||
<h3>5. Zombie peer server join (no 30s wait)</h3>
|
||||
<div class="steps">
|
||||
<strong>Steps:</strong>
|
||||
<ol>
|
||||
<li><input type="checkbox" class="checkbox"> In Adminer, manually set <code>player_data.last_server=99999</code> and <code>online=1</code> for a test UUID</li>
|
||||
<li><input type="checkbox" class="checkbox"> Join any running server with that UUID</li>
|
||||
</ol>
|
||||
</div>
|
||||
<div class="expected">
|
||||
<strong>Expected:</strong>
|
||||
<ul>
|
||||
<li>Join happens within a few seconds (not 30s)</li>
|
||||
<li>Log shows <code>[RACE] Peer server 99999 is dead/zombie — taking over</code></li>
|
||||
<li>DB now shows <code>last_server=<thisServer></code>, <code>online=1</code></li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="scenario high">
|
||||
<span class="tag tag-high">HIGH</span>
|
||||
<h3>6. Periodic auto-save (10 min)</h3>
|
||||
<div class="steps">
|
||||
<strong>Steps:</strong>
|
||||
<ol>
|
||||
<li><input type="checkbox" class="checkbox"> Set <code>auto_save_interval_minutes=1</code> in config for quick test</li>
|
||||
<li><input type="checkbox" class="checkbox"> Join server, add items to inventory</li>
|
||||
<li><input type="checkbox" class="checkbox"> Wait 1 minute (watch sync.log)</li>
|
||||
<li><input type="checkbox" class="checkbox"> Kill -9 server</li>
|
||||
<li><input type="checkbox" class="checkbox"> Restart, rejoin, check inventory</li>
|
||||
</ol>
|
||||
</div>
|
||||
<div class="expected">
|
||||
<strong>Expected:</strong>
|
||||
<ul>
|
||||
<li>Log shows <code>[periodic-save] queued snapshots for N player(s)</code> after 1 min</li>
|
||||
<li>Post-crash inventory reflects the state AT the last periodic tick</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="scenario high">
|
||||
<span class="tag tag-high">HIGH</span>
|
||||
<h3>7. Pool saturation WARN log</h3>
|
||||
<div class="steps">
|
||||
<strong>Steps:</strong>
|
||||
<ol>
|
||||
<li><input type="checkbox" class="checkbox"> Wait 5 minutes after server start (for first PoolStatsReporter tick)</li>
|
||||
<li><input type="checkbox" class="checkbox"> Grep sync.log for <code>[POOL]</code></li>
|
||||
</ol>
|
||||
</div>
|
||||
<div class="expected">
|
||||
<strong>Expected:</strong>
|
||||
<ul>
|
||||
<li>At least one line like <code>[POOL] executor active=0 queue=0 pool_idle=4 | hikari active=0 idle=4</code></li>
|
||||
<li>No WARN unless under load</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="scenario high">
|
||||
<span class="tag tag-high">HIGH</span>
|
||||
<h3>8. Heartbeat updates server_info</h3>
|
||||
<div class="steps">
|
||||
<strong>Steps:</strong>
|
||||
<ol>
|
||||
<li><input type="checkbox" class="checkbox"> In Adminer, watch <code>server_info.last_update</code> for this server's id</li>
|
||||
<li><input type="checkbox" class="checkbox"> Refresh every 20s for 1 minute</li>
|
||||
</ol>
|
||||
</div>
|
||||
<div class="expected">
|
||||
<strong>Expected:</strong>
|
||||
<ul>
|
||||
<li><code>last_update</code> advances by ~10000 ms at every refresh</li>
|
||||
<li>Log shows <code>[heartbeat] started</code> on boot</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="scenario">
|
||||
<span class="tag tag-medium">MEDIUM</span>
|
||||
<h3>9. Curios capability unavailable — no wipe</h3>
|
||||
<div class="steps">
|
||||
<strong>Steps:</strong>
|
||||
<ol>
|
||||
<li><input type="checkbox" class="checkbox"> Equip curios items, die in lava</li>
|
||||
<li><input type="checkbox" class="checkbox"> Force-disconnect during death animation</li>
|
||||
<li><input type="checkbox" class="checkbox"> Reconnect</li>
|
||||
</ol>
|
||||
</div>
|
||||
<div class="expected">
|
||||
<strong>Expected:</strong>
|
||||
<ul>
|
||||
<li>If cap was unavailable: log shows <code>[store-curios] handler unavailable for ... skipping write</code></li>
|
||||
<li>Curios row in DB NOT wiped</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="scenario">
|
||||
<span class="tag tag-medium">MEDIUM</span>
|
||||
<h3>10. Cross-server claim + downstream short-circuit</h3>
|
||||
<div class="steps">
|
||||
<strong>Steps:</strong>
|
||||
<ol>
|
||||
<li><input type="checkbox" class="checkbox"> Player connected on Server A</li>
|
||||
<li><input type="checkbox" class="checkbox"> Disconnect then immediately join Server B (within 200ms)</li>
|
||||
<li><input type="checkbox" class="checkbox"> Check sync.log on Server A</li>
|
||||
</ol>
|
||||
</div>
|
||||
<div class="expected">
|
||||
<strong>Expected:</strong>
|
||||
<ul>
|
||||
<li>Server A may log <code>[GUARD]</code> (last_server guard blocked) if B claimed during A's save</li>
|
||||
<li>If blocked: <code>[SAVE_SKIP] LOGOUT skipped: core guard blocked</code></li>
|
||||
<li>Player inventory on B = inventory as it was on A (no merge, no overwrite)</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<h2>Regression checks</h2>
|
||||
|
||||
<div class="regression">
|
||||
<strong>Watch for these regressions after Phase 0-5 deployment:</strong>
|
||||
<ul>
|
||||
<li>TPS drop during auto-save ticks (periodic save at 10 min should be invisible to gameplay)</li>
|
||||
<li>HikariCP leak warnings — <code>leakDetectionThreshold=25000</code>, warnings mean a connection held >25s</li>
|
||||
<li>CallerRunsPolicy triggering (queue full) — look for WARN <code>[pool-stats] executor queue high</code></li>
|
||||
<li>Deadlock on logout → join (bgLock serialization) — log should show <code>[SAVE] LOGOUT completed</code> within ~500ms</li>
|
||||
<li>Reflection fallback firing repeatedly — means upstream removeBackpackContents / removeStorageContents API broke</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<div class="lang-sep"></div>
|
||||
|
||||
<!-- ================================================================ -->
|
||||
<!-- FRENCH VERSION -->
|
||||
<!-- ================================================================ -->
|
||||
|
||||
<h1>Procédure de Test — PlayerSync v2.1.5 (Version Française)</h1>
|
||||
<div class="meta">Date : <strong>2026-04-22</strong> | Branche : <code>1.21.1-dev</code> | Minecraft 1.21.1 / NeoForge 21.1.137 / Java 21</div>
|
||||
|
||||
<h2>Mise en place</h2>
|
||||
|
||||
<ol>
|
||||
<li>Démarrer MariaDB dev : <code>docker compose up -d</code></li>
|
||||
<li>Build : <code>./gradlew build</code> — le JAR sort dans <code>build/libs/playersync-1.21.1-2.1.5.jar</code></li>
|
||||
<li>Deux instances serveur nécessaires : <code>./gradlew runServer</code> (Serveur A) + copie avec <code>Server_id</code> différent dans <code>run-2/config/playersync-common.toml</code> (Serveur B)</li>
|
||||
<li>Adminer : <code>http://localhost:8080</code> (login <code>playersync</code>/<code>playersync</code>)</li>
|
||||
<li>Monitorer en continu : <code>tail -f run/logs/playersync/sync.log</code></li>
|
||||
</ol>
|
||||
|
||||
<h2>Scénarios à tester</h2>
|
||||
|
||||
<div class="scenario critical">
|
||||
<span class="tag tag-critical">CRITIQUE</span>
|
||||
<h3>1. Drop + déco rapide + reco (régression Phase 0)</h3>
|
||||
<div class="steps">
|
||||
<strong>Étapes :</strong>
|
||||
<ol>
|
||||
<li><input type="checkbox" class="checkbox"> Join Serveur A, remplir l'inventaire avec une épée de diamant</li>
|
||||
<li><input type="checkbox" class="checkbox"> Drop l'épée avec Q</li>
|
||||
<li><input type="checkbox" class="checkbox"> Déconnecter immédiatement (moins d'une seconde)</li>
|
||||
<li><input type="checkbox" class="checkbox"> Rejoin Serveur A</li>
|
||||
</ol>
|
||||
</div>
|
||||
<div class="expected">
|
||||
<strong>Résultat attendu :</strong>
|
||||
<ul>
|
||||
<li>L'inventaire ne contient PAS l'épée</li>
|
||||
<li>L'ItemEntity est toujours au sol où elle a été drop</li>
|
||||
<li>Le joueur a exactement 1 copie de l'épée au total</li>
|
||||
<li>Logs : <code>[SAVE] LOGOUT completed</code> puis soit aucun write SaveToFile BG, soit <code>[GUARD] SaveToFile BG skipped — player already offline in DB</code></li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="scenario critical">
|
||||
<span class="tag tag-critical">CRITIQUE</span>
|
||||
<h3>2. Duplication Backpack (Sophisticated Backpacks)</h3>
|
||||
<div class="steps">
|
||||
<strong>Étapes :</strong>
|
||||
<ol>
|
||||
<li><input type="checkbox" class="checkbox"> Join Serveur A, craft un SophisticatedBackpack, remplir avec 10 blocs de diamant</li>
|
||||
<li><input type="checkbox" class="checkbox"> Déconnecter</li>
|
||||
<li><input type="checkbox" class="checkbox"> Join Serveur B (configurer un Server_id différent)</li>
|
||||
<li><input type="checkbox" class="checkbox"> Ouvrir le backpack, compter les blocs de diamant</li>
|
||||
</ol>
|
||||
</div>
|
||||
<div class="expected">
|
||||
<strong>Résultat attendu :</strong>
|
||||
<ul>
|
||||
<li>Exactement 10 blocs de diamant (pas de duplication)</li>
|
||||
<li>Logs : <code>[restore-backpack] uuid=... nbt_keys=... cleared_via=api</code> (ou <code>reflection</code> en fallback)</li>
|
||||
<li>Aucun WARN sur un removeBackpackContents raté</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="scenario critical">
|
||||
<span class="tag tag-critical">CRITIQUE</span>
|
||||
<h3>3. Duplication shulker Sophisticated Storage</h3>
|
||||
<div class="steps">
|
||||
<strong>Étapes :</strong>
|
||||
<ol>
|
||||
<li><input type="checkbox" class="checkbox"> Join Serveur A, packer un shulker plein de diamants dans l'inventaire</li>
|
||||
<li><input type="checkbox" class="checkbox"> Faire ouvrir l'inventaire par un autre Joueur B (via admin / échange / viewer)</li>
|
||||
<li><input type="checkbox" class="checkbox"> Déconnecter le Joueur A</li>
|
||||
<li><input type="checkbox" class="checkbox"> Joueur A se reconnecte sur Serveur B</li>
|
||||
<li><input type="checkbox" class="checkbox"> Déballer le shulker, compter les diamants</li>
|
||||
</ol>
|
||||
</div>
|
||||
<div class="expected">
|
||||
<strong>Résultat attendu :</strong>
|
||||
<ul>
|
||||
<li>Compte de diamants identique à l'original</li>
|
||||
<li>Logs : <code>[CONTAINER_CLOSE]</code> pour le Joueur B (viewer force-fermé)</li>
|
||||
<li>Logs : <code>[restore-ss] uuid=... nbt_keys=...</code></li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="scenario critical">
|
||||
<span class="tag tag-critical">CRITIQUE</span>
|
||||
<h3>4. Recovery kill -9 / OOM</h3>
|
||||
<div class="steps">
|
||||
<strong>Étapes :</strong>
|
||||
<ol>
|
||||
<li><input type="checkbox" class="checkbox"> Join Serveur A, mettre l'inventaire dans un état connu (poser un diamant nommé)</li>
|
||||
<li><input type="checkbox" class="checkbox"> Trouver le PID java du serveur : <code>jps | grep Forge</code></li>
|
||||
<li><input type="checkbox" class="checkbox"> Kill brutal : <code>kill -9 <pid></code> (ou Task Manager → End Task sur Windows)</li>
|
||||
<li><input type="checkbox" class="checkbox"> Redémarrer le serveur A</li>
|
||||
<li><input type="checkbox" class="checkbox"> Join Serveur A, vérifier l'inventaire</li>
|
||||
</ol>
|
||||
</div>
|
||||
<div class="expected">
|
||||
<strong>Résultat attendu :</strong>
|
||||
<ul>
|
||||
<li>Au boot : log <code>[crash-recovery] cleared N orphan online=1 rows</code></li>
|
||||
<li>Au boot : <code>[crash-recovery] JVM shutdown hook installed</code> ET idéalement (si le hook a tourné) : <code>[emergency-flush] flushed N players</code></li>
|
||||
<li>L'inventaire correspond au dernier état avant kill (dans la fenêtre auto-save ~10 min)</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="scenario high">
|
||||
<span class="tag tag-high">HIGH</span>
|
||||
<h3>5. Join sur serveur peer zombie (pas d'attente 30s)</h3>
|
||||
<div class="steps">
|
||||
<strong>Étapes :</strong>
|
||||
<ol>
|
||||
<li><input type="checkbox" class="checkbox"> Dans Adminer, setter manuellement <code>player_data.last_server=99999</code> et <code>online=1</code> pour un UUID test</li>
|
||||
<li><input type="checkbox" class="checkbox"> Joindre n'importe quel serveur en cours avec cet UUID</li>
|
||||
</ol>
|
||||
</div>
|
||||
<div class="expected">
|
||||
<strong>Résultat attendu :</strong>
|
||||
<ul>
|
||||
<li>La connexion se fait en quelques secondes (pas 30s)</li>
|
||||
<li>Logs : <code>[RACE] Peer server 99999 is dead/zombie — taking over</code></li>
|
||||
<li>La DB affiche maintenant <code>last_server=<thisServer></code>, <code>online=1</code></li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="scenario high">
|
||||
<span class="tag tag-high">HIGH</span>
|
||||
<h3>6. Auto-save périodique (10 min)</h3>
|
||||
<div class="steps">
|
||||
<strong>Étapes :</strong>
|
||||
<ol>
|
||||
<li><input type="checkbox" class="checkbox"> Setter <code>auto_save_interval_minutes=1</code> en config pour un test rapide</li>
|
||||
<li><input type="checkbox" class="checkbox"> Join le serveur, ajouter des items à l'inventaire</li>
|
||||
<li><input type="checkbox" class="checkbox"> Attendre 1 minute (surveiller sync.log)</li>
|
||||
<li><input type="checkbox" class="checkbox"> Kill -9 du serveur</li>
|
||||
<li><input type="checkbox" class="checkbox"> Redémarrer, rejoin, vérifier l'inventaire</li>
|
||||
</ol>
|
||||
</div>
|
||||
<div class="expected">
|
||||
<strong>Résultat attendu :</strong>
|
||||
<ul>
|
||||
<li>Log : <code>[periodic-save] queued snapshots for N player(s)</code> après 1 min</li>
|
||||
<li>L'inventaire post-crash reflète l'état AU dernier tick périodique</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="scenario high">
|
||||
<span class="tag tag-high">HIGH</span>
|
||||
<h3>7. Log WARN sur saturation pool</h3>
|
||||
<div class="steps">
|
||||
<strong>Étapes :</strong>
|
||||
<ol>
|
||||
<li><input type="checkbox" class="checkbox"> Attendre 5 minutes après le boot du serveur (premier tick PoolStatsReporter)</li>
|
||||
<li><input type="checkbox" class="checkbox"> Grep sync.log pour <code>[POOL]</code></li>
|
||||
</ol>
|
||||
</div>
|
||||
<div class="expected">
|
||||
<strong>Résultat attendu :</strong>
|
||||
<ul>
|
||||
<li>Au moins une ligne comme <code>[POOL] executor active=0 queue=0 pool_idle=4 | hikari active=0 idle=4</code></li>
|
||||
<li>Aucun WARN sauf sous charge</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="scenario high">
|
||||
<span class="tag tag-high">HIGH</span>
|
||||
<h3>8. Heartbeat update server_info</h3>
|
||||
<div class="steps">
|
||||
<strong>Étapes :</strong>
|
||||
<ol>
|
||||
<li><input type="checkbox" class="checkbox"> Dans Adminer, surveiller <code>server_info.last_update</code> pour l'id de ce serveur</li>
|
||||
<li><input type="checkbox" class="checkbox"> Refresh toutes les 20s pendant 1 minute</li>
|
||||
</ol>
|
||||
</div>
|
||||
<div class="expected">
|
||||
<strong>Résultat attendu :</strong>
|
||||
<ul>
|
||||
<li><code>last_update</code> avance de ~10000 ms à chaque refresh</li>
|
||||
<li>Log : <code>[heartbeat] started</code> au boot</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="scenario">
|
||||
<span class="tag tag-medium">MEDIUM</span>
|
||||
<h3>9. Capability Curios absente — pas de wipe</h3>
|
||||
<div class="steps">
|
||||
<strong>Étapes :</strong>
|
||||
<ol>
|
||||
<li><input type="checkbox" class="checkbox"> Équiper des items curios, mourir dans la lave</li>
|
||||
<li><input type="checkbox" class="checkbox"> Force-déconnecter pendant l'animation de mort</li>
|
||||
<li><input type="checkbox" class="checkbox"> Reconnexion</li>
|
||||
</ol>
|
||||
</div>
|
||||
<div class="expected">
|
||||
<strong>Résultat attendu :</strong>
|
||||
<ul>
|
||||
<li>Si cap absente : log <code>[store-curios] handler unavailable for ... skipping write</code></li>
|
||||
<li>Row curios en DB NON wipée</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="scenario">
|
||||
<span class="tag tag-medium">MEDIUM</span>
|
||||
<h3>10. Claim cross-server + court-circuit downstream</h3>
|
||||
<div class="steps">
|
||||
<strong>Étapes :</strong>
|
||||
<ol>
|
||||
<li><input type="checkbox" class="checkbox"> Joueur connecté sur Serveur A</li>
|
||||
<li><input type="checkbox" class="checkbox"> Déco puis immédiatement join Serveur B (<200ms)</li>
|
||||
<li><input type="checkbox" class="checkbox"> Vérifier sync.log sur Serveur A</li>
|
||||
</ol>
|
||||
</div>
|
||||
<div class="expected">
|
||||
<strong>Résultat attendu :</strong>
|
||||
<ul>
|
||||
<li>Serveur A peut logger <code>[GUARD]</code> (last_server guard a bloqué) si B a claim pendant la save de A</li>
|
||||
<li>Si blocké : <code>[SAVE_SKIP] LOGOUT skipped: core guard blocked</code></li>
|
||||
<li>Inventaire joueur sur B = inventaire tel qu'il était sur A (pas de merge, pas d'overwrite)</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<h2>Vérifications régressions</h2>
|
||||
|
||||
<div class="regression">
|
||||
<strong>Surveiller ces régressions après le déploiement Phases 0-5 :</strong>
|
||||
<ul>
|
||||
<li>TPS drop pendant les ticks auto-save (la save périodique à 10 min doit être invisible gameplay)</li>
|
||||
<li>Warnings HikariCP leak — <code>leakDetectionThreshold=25000</code>, warnings = connexion tenue >25s</li>
|
||||
<li>Déclenchement CallerRunsPolicy (queue pleine) — WARN <code>[pool-stats] executor queue high</code></li>
|
||||
<li>Deadlock sur logout → join (sérialisation bgLock) — le log doit montrer <code>[SAVE] LOGOUT completed</code> en ~500ms</li>
|
||||
<li>Fallback reflection qui tourne répétitivement — signifie que l'API upstream removeBackpackContents / removeStorageContents a été cassée</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<p style="text-align: center; color: #999; margin-top: 3em; font-size: 0.9em;">
|
||||
Author: vyrriox | PlayerSync v2.1.5 | 2026-04-22
|
||||
</p>
|
||||
|
||||
</body>
|
||||
</html>
|
||||
Loading…
Reference in New Issue
Block a user