Conversation
There was a problem hiding this comment.
Pull request overview
This PR refactors SurfStats into a client + microservice architecture using RabbitMQ packets for persistence, moving DB writes out of the Paper plugin and into a standalone microservice.
Changes:
- Split
surf-stats-coreintosurf-stats-core-common(packets) andsurf-stats-core-client(file parsing, diff tracking, Rabbit client). - Add
surf-stats-microserviceto receive RabbitMQ requests and persist servers/players/stats via Exposed R2DBC tables. - Update Paper plugin wiring and dependencies to use the new client services and required RabbitMQ module.
Reviewed changes
Copilot reviewed 51 out of 64 changed files in this pull request and generated 4 comments.
Show a summary per file
| File | Description |
|---|---|
| surf-stats-paper/src/main/kotlin/dev/slne/surf/stats/paper/listener/PlayerStatsListener.kt | Switches listener integration to the new client API and updated quit signature. |
| surf-stats-paper/src/main/kotlin/dev/slne/surf/stats/paper/SurfStatsPlugin.kt | Updates lifecycle to use statsInstance + periodic diff saving with UUID-only processing. |
| surf-stats-paper/src/main/kotlin/dev/slne/surf/stats/paper/PaperStatsInstance.kt | Rebinds Paper service implementation to new core-client StatsInstance. |
| surf-stats-paper/build.gradle.kts | Updates dependencies to surf-stats-core-client and adds required rabbit/core wiring. |
| surf-stats-microservice/src/main/kotlin/dev/slne/surf/stats/microservice/handler/StatsPacketHandler.kt | Adds Rabbit handler to save current/diff stats batches and respond with failed UUIDs. |
| surf-stats-microservice/src/main/kotlin/dev/slne/surf/stats/microservice/handler/ServerPacketHandler.kt | Adds Rabbit handler to ensure server row exists. |
| surf-stats-microservice/src/main/kotlin/dev/slne/surf/stats/microservice/handler/PlayerPacketHandler.kt | Adds Rabbit handler to ensure player row exists. |
| surf-stats-microservice/src/main/kotlin/dev/slne/surf/stats/microservice/db/tables/StatKeysTable.kt | Introduces microservice-local stat key dimension table (Key transform). |
| surf-stats-microservice/src/main/kotlin/dev/slne/surf/stats/microservice/db/tables/StatCategoriesTable.kt | Introduces microservice-local stat category dimension table (Key transform). |
| surf-stats-microservice/src/main/kotlin/dev/slne/surf/stats/microservice/db/tables/ServersTable.kt | Moves/renames servers table into microservice package. |
| surf-stats-microservice/src/main/kotlin/dev/slne/surf/stats/microservice/db/tables/PlayersTable.kt | Moves/renames players table into microservice package. |
| surf-stats-microservice/src/main/kotlin/dev/slne/surf/stats/microservice/db/tables/PlayerStatsTable.kt | Updates current stats table columns to use Key transforms for FK references. |
| surf-stats-microservice/src/main/kotlin/dev/slne/surf/stats/microservice/db/tables/PlayerStatsHistoryTable.kt | Updates history table to use Key transforms and indexes for diff storage. |
| surf-stats-microservice/src/main/kotlin/dev/slne/surf/stats/microservice/db/StatsDatabaseService.kt | Implements persistence operations (ensure server/player, save current/diff batches). |
| surf-stats-microservice/src/main/kotlin/dev/slne/surf/stats/microservice/StatsMicroservice.kt | Adds the standalone microservice bootstrap wiring for DB + RabbitMQ. |
| surf-stats-microservice/build.gradle.kts | Adds standalone + microservice gradle config and core-common dependency. |
| surf-stats-core/surf-stats-core-common/src/main/kotlin/dev/slne/surf/stats/core/common/packets/StatsUuidResponsePacket.kt | Adds response packet for returning failed UUIDs. |
| surf-stats-core/surf-stats-core-common/src/main/kotlin/dev/slne/surf/stats/core/common/packets/SaveStatsRequestPacket.kt | Adds request packet for saving current/diff batches. |
| surf-stats-core/surf-stats-core-common/src/main/kotlin/dev/slne/surf/stats/core/common/packets/SaveServerRequestPacket.kt | Adds request packet to ensure server metadata exists. |
| surf-stats-core/surf-stats-core-common/src/main/kotlin/dev/slne/surf/stats/core/common/packets/SavePlayerRequestPacket.kt | Adds request packet to ensure player metadata exists. |
| surf-stats-core/surf-stats-core-common/build.gradle.kts | Enables microservice rabbit common module support. |
| surf-stats-core/surf-stats-core-client/src/main/kotlin/dev/slne/surf/stats/core/client/service/StatisticsManagerService.kt | Introduces client-side snapshot/diff service API. |
| surf-stats-core/surf-stats-core-client/src/main/kotlin/dev/slne/surf/stats/core/client/service/StatisticsManagerServiceImpl.kt | Implements snapshot caching + diff computation with Caffeine. |
| surf-stats-core/surf-stats-core-client/src/main/kotlin/dev/slne/surf/stats/core/client/repository/PlayerStatsRepository.kt | Updates repository contract (UUID-only) for new model types. |
| surf-stats-core/surf-stats-core-client/src/main/kotlin/dev/slne/surf/stats/core/client/repository/PlayerStatsRepositoryImpl.kt | Reworks file-backed loading and error fallback to empty stats. |
| surf-stats-core/surf-stats-core-client/src/main/kotlin/dev/slne/surf/stats/core/client/json/StatsJsonModel.kt | Updates JSON model to produce typed keys for stats entries. |
| surf-stats-core/surf-stats-core-client/src/main/kotlin/dev/slne/surf/stats/core/client/json/StatsFileService.kt | Updates file service contract (removes name-based overloads). |
| surf-stats-core/surf-stats-core-client/src/main/kotlin/dev/slne/surf/stats/core/client/json/StatsFileServiceImpl.kt | Reworks file parsing to new PlayerStats model and typed keys. |
| surf-stats-core/surf-stats-core-client/src/main/kotlin/dev/slne/surf/stats/core/client/SurfStatsApiImpl.kt | New client API implementation: compute diffs and send RabbitMQ save requests. |
| surf-stats-core/surf-stats-core-client/src/main/kotlin/dev/slne/surf/stats/core/client/StatsInstance.kt | New client instance providing RabbitMQ connectivity + server registration. |
| surf-stats-core/surf-stats-core-client/build.gradle.kts | Adds client module build config with rabbit client API. |
| surf-stats-api/src/main/kotlin/dev/slne/surf/stats/api/model/StatEntry.kt | Changes stat keys/categories to serializable Key types and adds serialization. |
| surf-stats-api/src/main/kotlin/dev/slne/surf/stats/api/model/PlayerStats.kt | Replaces prior player-identifying fields with playerUuid/serverName + Collection impl. |
| surf-stats-api/src/main/kotlin/dev/slne/surf/stats/api/model/PlayerStatsBatch.kt | Redefines batch payload to be serialization/Rabbit-friendly. |
| surf-stats-api/src/main/kotlin/dev/slne/surf/stats/api/SurfStatsApi.kt | Simplifies SurfStats API surface to UUID-based processing and save/get operations. |
| surf-stats-api/build.gradle.kts | Switches stats API module to the core gradle plugin. |
| surf-stats-core/src/test/resources/stats/9e94922b-c7b0-43f4-9fab-7c5ff9b07061.json | Removes old test resource JSON (tests removed as part of refactor). |
| surf-stats-core/src/test/resources/stats/47c09dc5-1e14-409f-80c0-c031df3acb13.json | Removes old test resource JSON (tests removed as part of refactor). |
| surf-stats-core/src/test/kotlin/dev/slne/surf/stats/core/service/StatsFileServiceImplTest.kt | Removes old file service tests. |
| surf-stats-core/src/test/kotlin/dev/slne/surf/stats/core/service/StatisticsManagerServiceImplTest.kt | Removes old diff/snapshot tests. |
| surf-stats-core/src/test/kotlin/dev/slne/surf/stats/core/repository/PlayerStatsRepositoryImplTest.kt | Removes old repository tests. |
| surf-stats-core/src/test/kotlin/dev/slne/surf/stats/core/json/StatsJsonModelTest.kt | Removes old JSON model tests. |
| surf-stats-core/src/main/kotlin/dev/slne/surf/stats/core/service/StatisticsManagerServiceImpl.kt | Removes old in-core implementation in favor of new client module. |
| surf-stats-core/src/main/kotlin/dev/slne/surf/stats/core/database/StatsDatabaseService.kt | Removes old in-core DB persistence in favor of microservice. |
| surf-stats-core/src/main/kotlin/dev/slne/surf/stats/core/api/SurfStatsApiImpl.kt | Removes old in-core SurfStatsApi implementation in favor of client module. |
| surf-stats-core/src/main/kotlin/dev/slne/surf/stats/core/AbstractStatsInstance.kt | Removes old instance abstraction (DB-init responsibilities moved). |
| surf-stats-core/src/main/kotlin/dev/slne/surf/stats/core/database/table/StatKeysTable.kt | Removes old core DB table definition (moved to microservice). |
| surf-stats-core/src/main/kotlin/dev/slne/surf/stats/core/database/table/StatCategoriesTable.kt | Removes old core DB table definition (moved to microservice). |
| surf-stats-api/src/main/kotlin/dev/slne/surf/stats/api/service/StatisticsManagerService.kt | Removes old API-level statistics manager service (replaced by client module). |
| surf-stats-api/src/main/kotlin/dev/slne/surf/stats/api/StatsInstance.kt | Removes old API-level instance interface (replaced by client StatsInstance). |
| settings.gradle.kts | Updates module includes for core split + adds microservice module. |
| gradle/libs.versions.toml | Removes old surf-database version catalog entry (moved to plugin wiring). |
| gradle.properties | Bumps project version to 2.0.1-SNAPSHOT. |
| build.gradle.kts | Adds microservice gradle plugin to buildscript classpath. |
Comments suppressed due to low confidence (1)
surf-stats-core/surf-stats-core-client/src/main/kotlin/dev/slne/surf/stats/core/client/json/StatsJsonModel.kt:41
- In
statsMapconstruction, the inner destructured variable is namedkey, which shadows the importedkey(...)function. As written,key(key)will try to invoke the localStringvariable and won’t compile. Rename the destructured variable (e.g.,statKey) or qualify the function call so the correctkey(...)function is used.
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| private suspend fun ensureDimensions( | ||
| stats: PlayerStats, | ||
| ) = suspendTransaction { | ||
| val categories = stats.map { it.category }.toSet() | ||
| val keys = stats.map { it.key }.toSet() | ||
|
|
||
| StatCategoriesTable.batchInsert(categories, ignore = true) { category -> | ||
| this[StatCategoriesTable.name] = category | ||
| } | ||
|
|
||
| StatKeysTable.batchInsert(keys, ignore = true) { key -> | ||
| this[StatKeysTable.name] = key | ||
| } | ||
| } |
There was a problem hiding this comment.
ensureDimensions wraps its work in suspendTransaction, but it’s called from inside other suspendTransaction blocks (saveBatch / saveDiffBatch). This creates nested transactions, which can break Exposed R2DBC transaction handling. Make ensureDimensions run within the caller’s transaction (no suspendTransaction inside), or have callers invoke it outside and reuse a single transaction.
| suspend fun ensurePlayer(playerUuid: UUID, playerName: String) = suspendTransaction { | ||
| PlayersTable.upsert { | ||
| it[uuid] = playerUuid | ||
| it[name] = playerName | ||
| it[dataVersion] = 0 | ||
| it[updatedAt] = CurrentTimestamp | ||
| } |
There was a problem hiding this comment.
ensurePlayer always writes dataVersion = 0, and the dataVersion carried on PlayerStatsBatch is never persisted. If the DB schema still expects the client’s DataVersion, this will silently lose that information. Consider either persisting the actual data version from the request (and/or JSON parsing) or removing the column/field if it’s no longer needed.
| // Only update snapshots for players whose diffs were persisted successfully | ||
| for (uuid in uuids) { | ||
| if (uuid !in failedUuids && diffBatches.any { it.playerUuid == uuid }) { | ||
| StatisticsManagerService.updateSnapshot(uuid) | ||
| } |
There was a problem hiding this comment.
Updating snapshots does an any { ... } scan over diffBatches for every uuid in uuids, which is O(n²) on shutdown/periodic saves. Precompute a Set of UUIDs that actually had diffs (e.g., diffBatches.map { it.playerUuid }.toSet()) and check membership instead.
| override fun isTracking(uuid: UUID): Boolean { | ||
| return snapshotMap.any { it.playerUuid == uuid } | ||
| } |
There was a problem hiding this comment.
isTracking currently builds a new snapshotMap list and linearly scans it (any { ... }). This is unnecessarily expensive and allocates on every call. Prefer checking the underlying Caffeine cache directly (e.g., getIfPresent(uuid) != null or asMap().containsKey(uuid)).
No description provided.