Skip to content

Fix SurfPlayerSyncTask for multiproxy: scope stale eviction to current proxy only and sync current server#35

Open
Copilot wants to merge 9 commits intoversion/1.21.11from
copilot/sync-players-across-servers
Open

Fix SurfPlayerSyncTask for multiproxy: scope stale eviction to current proxy only and sync current server#35
Copilot wants to merge 9 commits intoversion/1.21.11from
copilot/sync-players-across-servers

Conversation

Copy link

Copilot AI commented Mar 4, 2026

clearPlayers() wiped the entire shared Redis SyncMap, destroying cached player entries belonging to sibling proxy instances in a multiproxy setup.

Changes

SurfPlayerSyncTask

  • Replaced global clearPlayers() with targeted per-proxy invalidation
  • Uses SurfProxyServer.current().getPlayers() to scope lookups to only this proxy's cached entries
  • Stale entries (cached under this proxy, absent from Velocity's allPlayers) → invalidatePlayer()
  • Live playerscurrentServer is updated from the Velocity player's actual connected server via velocityPlayer.currentServer.getOrNull()?.serverInfo?.name, then cachePlayer() persists the updated entry (upsert/override, no data loss)
  • Uncached online players → if a Velocity-connected player is not found in the cache (findPlayerByUuid returns null), the sync falls back to getOrLoadOrCreatePlayerByUuid() (same as the join handler) and sets currentProxy = SurfProxyServer.current() on the loaded/created player before caching
  • Other proxies' entries in the shared Redis map are left untouched
  • onlineUuids uses toHashSet() for O(1) membership checks during stale-eviction
private fun syncPlayers() {
    plugin.pluginContainer.launch {
        val onlinePlayers = proxy.allPlayers.map { velocityPlayer ->
            val surfPlayer = surfPlayerService.findPlayerByUuid(velocityPlayer.uniqueId)
                ?: surfPlayerService.getOrLoadOrCreatePlayerByUuid(velocityPlayer.uniqueId).also {
                    it.currentProxy = SurfProxyServer.current()
                }
            val currentServerName = velocityPlayer.currentServer.getOrNull()?.serverInfo?.name
            val currentServer = currentServerName?.let { SurfServer[it] }
            surfPlayer.copy(currentServer = currentServer)
        }

        val onlineUuids = onlinePlayers.map { it.uuid }.toHashSet()

        // Only remove stale entries that belong to this proxy
        SurfProxyServer.current().getPlayers()
            .filter { it.uuid !in onlineUuids }
            .forEach { surfPlayerService.invalidatePlayer(it.uuid) }

        onlinePlayers.forEach { surfPlayerService.cachePlayer(it) }
    }
}

💬 We'd love your input! Share your thoughts on Copilot coding agent in our 2 minute survey.

Co-authored-by: TheBjoRedCraft <143264463+TheBjoRedCraft@users.noreply.github.com>
@TheBjoRedCraft
Copy link
Member

@copilot this wont work. There is multiproxy support.

Copilot AI changed the title [WIP] Implement player sync task with Redis Add periodic player cache sync task via Redis on Velocity proxy Mar 4, 2026
…for current proxy

Co-authored-by: TheBjoRedCraft <143264463+TheBjoRedCraft@users.noreply.github.com>
Copy link
Author

Copilot AI commented Mar 4, 2026

@copilot this wont work. There is multiproxy support.

Fixed in 1fdde55. Instead of clearPlayers() (which wipes the entire shared Redis map), the sync now uses SurfProxyServer.current().getPlayers() to scope invalidation to only stale entries belonging to this proxy — players cached with currentProxy == current who are no longer actually connected. Other proxies' player entries in the shared Redis SyncMap are left untouched.

Copilot AI changed the title Add periodic player cache sync task via Redis on Velocity proxy Fix SurfPlayerSyncTask for multiproxy: scope stale eviction to current proxy only Mar 4, 2026
@TheBjoRedCraft
Copy link
Member

@copilot update the players current server to the server from the velocity player

Co-authored-by: TheBjoRedCraft <143264463+TheBjoRedCraft@users.noreply.github.com>
Copy link
Author

Copilot AI commented Mar 4, 2026

@copilot update the players current server to the server from the velocity player

Done in 5068f84. During the sync tick, each Velocity Player's current server is now read via velocityPlayer.currentServer.getOrNull()?.serverInfo?.name, resolved via CommonSurfServer[name], and applied to the SurfPlayer with copy(currentServer = ...) before cachePlayer() persists it — same pattern as handleSwitch in ConnectionListener.

Copilot AI changed the title Fix SurfPlayerSyncTask for multiproxy: scope stale eviction to current proxy only Fix SurfPlayerSyncTask for multiproxy: scope stale eviction to current proxy only and sync current server Mar 4, 2026
@TheBjoRedCraft TheBjoRedCraft self-requested a review March 4, 2026 19:37
@TheBjoRedCraft TheBjoRedCraft marked this pull request as ready for review March 4, 2026 19:37
Copilot AI review requested due to automatic review settings March 4, 2026 19:37
Copy link
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Adds a periodic Velocity-side player cache synchronization intended for multiproxy setups, avoiding global Redis player-cache wipes while keeping per-proxy entries fresh and updating players’ currentServer.

Changes:

  • Introduces SurfPlayerSyncTask that invalidates stale cached players only for SurfProxyServer.current() and upserts live players with an updated currentServer.
  • Starts/stops the sync task during Velocity proxy initialize/shutdown.
  • Adjusts server lookup operators: CommonSurfServer[name] now uses getCommonServerByName, and SurfServer[name] is added for backend-only lookups.

Reviewed changes

Copilot reviewed 6 out of 6 changed files in this pull request and generated 2 comments.

Show a summary per file
File Description
surf-core-velocity/src/main/kotlin/dev/slne/surf/core/velocity/task/SurfPlayerSyncTask.kt New scheduled task to invalidate stale per-proxy player cache entries and update currentServer.
surf-core-velocity/src/main/kotlin/dev/slne/surf/core/velocity/VelocityMain.kt Wires the new sync task into proxy lifecycle.
surf-core-api/surf-core-api-common/src/main/kotlin/dev/slne/surf/core/api/common/server/SurfServer.kt Adds operator fun get(name) for backend server lookup.
surf-core-api/surf-core-api-common/src/main/kotlin/dev/slne/surf/core/api/common/server/CommonSurfServer.kt Changes operator fun get(name) to return proxy-or-backend via getCommonServerByName.
gradle.properties Bumps snapshot version.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.


companion object {
operator fun get(name: String) = surfCoreApi.getServerByName(name)
operator fun get(name: String) = surfCoreApi.getCommonServerByName(name)
Copy link

Copilot AI Mar 4, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Changing CommonSurfServer[name] to call getCommonServerByName widens the operator’s return type from SurfServer? to CommonSurfServer?. There are existing call sites that relied on the old type inference (e.g. surf-core-velocity/.../ConnectionListener.kt uses CommonSurfServer[toServer] and passes it into SurfPlayer.copy(currentServer = ...), where currentServer is a SurfServer?). After this change that code will no longer type-check. Consider updating those call sites to use SurfServer[name] when a backend server is required, or otherwise explicitly narrow/cast the result before assigning to currentServer.

Suggested change
operator fun get(name: String) = surfCoreApi.getCommonServerByName(name)
operator fun get(name: String): SurfServer? =
surfCoreApi.getCommonServerByName(name) as? SurfServer

Copilot uses AI. Check for mistakes.
Comment on lines +39 to +43
val onlineUuids = onlinePlayers.map { it.uuid }

SurfProxyServer.current().getPlayers()
.filter { it.uuid !in onlineUuids }
.forEach { surfPlayerService.invalidatePlayer(it.uuid) }
Copy link

Copilot AI Mar 4, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

onlineUuids is built as a List and then used for membership checks inside a filter (uuid !in onlineUuids). This makes the stale-eviction step O(n*m) due to linear contains checks. Convert onlineUuids to a HashSet (e.g. via toHashSet()) before filtering to keep lookups O(1).

Copilot uses AI. Check for mistakes.
@TheBjoRedCraft
Copy link
Member

@copilot wenn der spieler nicht in der online list ist, aber auf dem proxy online wird nix gemacht, zuerst soll geguckt werden ob er irgendwo anders on ist, wenn ja soll der genommen werden, wenn nicht soll der asus der db geladen oder erstellt werden wie beim join

Co-authored-by: TheBjoRedCraft <143264463+TheBjoRedCraft@users.noreply.github.com>
Copy link
Author

Copilot AI commented Mar 4, 2026

@copilot wenn der spieler nicht in der online list ist, aber auf dem proxy online wird nix gemacht, zuerst soll geguckt werden ob er irgendwo anders on ist, wenn ja soll der genommen werden, wenn nich...

Done in edd6f3f. When a Velocity player is online but not found in the cache (findPlayerByUuid returns null), the sync now falls back to getOrLoadOrCreatePlayerByUuid() — same as handleConnect on join — and sets currentProxy = SurfProxyServer.current() on the loaded/created player. currentServer is then updated from Velocity's reported server, and the result is persisted via cachePlayer().

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.

3 participants