Skip to content

Commit

Permalink
feat(shulker-proxy-agent): use proxy capacities as max slots in ping …
Browse files Browse the repository at this point in the history
…requests
  • Loading branch information
jeremylvln committed Feb 6, 2024
1 parent 4d86d34 commit 31cd569
Show file tree
Hide file tree
Showing 8 changed files with 113 additions and 51 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ import io.shulkermc.proxyagent.platform.ServerPreConnectHook
import net.kyori.adventure.text.Component
import net.kyori.adventure.text.serializer.bungeecord.BungeeComponentSerializer
import net.md_5.bungee.api.ProxyServer
import net.md_5.bungee.api.chat.TextComponent
import net.md_5.bungee.api.connection.ProxiedPlayer
import net.md_5.bungee.api.event.PermissionCheckEvent
import net.md_5.bungee.api.event.PlayerDisconnectEvent
Expand Down Expand Up @@ -56,7 +57,8 @@ class ProxyInterfaceBungeeCord(
@EventHandler(priority = EventPriority.LOWEST)
fun onPreLogin(event: ProxyPingEvent) {
val result = hook()
event.response.players.online = result.playerCount
event.response.players.online = result.onlinePlayerCount
event.response.players.max = result.maxPlayerCount
}
}
)
Expand All @@ -73,8 +75,9 @@ class ProxyInterfaceBungeeCord(
val result = hook()

if (!result.allowed) {
@Suppress("UnsafeCallOnNullableType")
event.setCancelReason(*BungeeComponentSerializer.get().serialize(result.rejectComponent!!))
event.reason = TextComponent.fromArray(
*BungeeComponentSerializer.get().serialize(result.rejectComponent!!)

Check warning

Code scanning / detekt

Unsafe calls on nullable types detected. These calls will throw a NullPointerException in case the nullable value is null. Warning

Calling !! on a nullable type will throw a NullPointerException at runtime in case the value is null. It should be avoided.
)
}
}
}
Expand Down Expand Up @@ -166,6 +169,10 @@ class ProxyInterfaceBungeeCord(
return this.proxy.players.size
}

override fun getPlayerCapacity(): Int {
return this.proxy.config.playerLimit
}

override fun scheduleDelayedTask(
delay: Long,
timeUnit: TimeUnit,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ interface ProxyInterface {
fun prepareNetworkAdminsPermissions(playerIds: List<UUID>)
fun teleportPlayerOnServer(playerName: String, serverName: String)
fun getPlayerCount(): Int
fun getPlayerCapacity(): Int

fun scheduleDelayedTask(delay: Long, timeUnit: TimeUnit, runnable: Runnable): ScheduledTask
fun scheduleRepeatingTask(delay: Long, interval: Long, timeUnit: TimeUnit, runnable: Runnable): ScheduledTask
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -86,7 +86,7 @@ class ShulkerProxyAgentCommon(val proxyInterface: ProxyInterface, val logger: Lo
)
}

this.cache.registerProxy(Configuration.PROXY_NAME)
this.cache.registerProxy(Configuration.PROXY_NAME, this.proxyInterface.getPlayerCapacity())
this.agonesGateway.setAllocated()
} catch (@Suppress("TooGenericExceptionCaught") e: Exception) {
this.logger.log(Level.SEVERE, "Shulker Agent crashed, stopping proxy", e)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import java.util.Optional
import java.util.UUID

interface CacheAdapter {
fun registerProxy(proxyName: String)
fun registerProxy(proxyName: String, proxyCapacity: Int)
fun unregisterProxy(proxyName: String)
fun updateProxyLastSeen(proxyName: String)
fun listRegisteredProxies(): List<RegisteredProxy>
Expand All @@ -25,8 +25,9 @@ interface CacheAdapter {
fun getPlayerNamesFromIds(playerIds: List<UUID>): Map<UUID, String>

fun countOnlinePlayers(): Int
fun countPlayerCapacity(): Int

data class RegisteredProxy(val proxyName: String, val lastSeenMillis: Long)
data class RegisteredProxy(val proxyName: String, val proxyCapacity: Int, val lastSeenMillis: Long)

interface Lock : AutoCloseable
}
Original file line number Diff line number Diff line change
Expand Up @@ -10,91 +10,124 @@ class RedisCacheAdapter(private val jedisPool: JedisPool) : CacheAdapter {
companion object {
private const val PROXY_LOST_PURGE_LOCK_SECONDS = 15L
private const val PLAYER_ID_CACHE_TTL_SECONDS = 60L * 60 * 24 * 14

private const val KEY_PREFIX = "shulker"

// Proxies keys
private const val PROXIES_KEY_PREFIX = "$KEY_PREFIX:proxies"
private const val PROXIES_SET_KEY = PROXIES_KEY_PREFIX
private const val PROXIES_CAPACITY_HASH_KEY = "$PROXIES_KEY_PREFIX:capacity"
private const val PROXIES_LAST_SEEN_HASH_KEY = "$PROXIES_KEY_PREFIX:last-seen"
private val PROXIES_PLAYERS_SET_KEY = { proxyName: String -> "$PROXIES_KEY_PREFIX:$proxyName:players" }

// Servers keys
private const val SERVERS_KEY_PREFIX = "$KEY_PREFIX:servers"
private val SERVERS_PLAYERS_SET_KEY = { serverName: String -> "$SERVERS_KEY_PREFIX:$serverName:players" }

// Players keys
private const val PLAYERS_KEY_PREFIX = "$KEY_PREFIX:players"
private const val PLAYERS_ONLINE_SET_KEY = "$PLAYERS_KEY_PREFIX:online"
private const val PLAYERS_CURRENT_PROXY_HASH_KEY = "$PLAYERS_KEY_PREFIX:current-proxy"
private const val PLAYERS_CURRENT_SERVER_HASH_KEY = "$PLAYERS_KEY_PREFIX:current-server"

// UUID cache keys
private const val UUID_CACHE_KEY_PREFIX = "$KEY_PREFIX:uuid-cache"
private val UUID_CACHE_NAME_TO_ID_KEY = { name: String -> "$UUID_CACHE_KEY_PREFIX:name-to-id:$name" }
private val UUID_CACHE_ID_TO_NAME_KEY = { id: String -> "$UUID_CACHE_KEY_PREFIX:id-to-name:$id" }

// Locks keys
private const val LOCKS_KEY_PREFIX = "$KEY_PREFIX:locks"
private const val LOCKS_LOST_PROXIES_PURGE_KEY = "$LOCKS_KEY_PREFIX:lost-proxies-purge"
}

override fun registerProxy(proxyName: String) {
override fun registerProxy(proxyName: String, proxyCapacity: Int) {
this.jedisPool.resource.use { jedis ->
val pipeline = jedis.pipelined()
pipeline.sadd("shulker:proxies", proxyName)
pipeline.hset("shulker:proxies:last-seen", proxyName, System.currentTimeMillis().toString())
pipeline.sadd(PROXIES_SET_KEY, proxyName)
pipeline.hset(PROXIES_CAPACITY_HASH_KEY, proxyName, proxyCapacity.toString())
pipeline.hset(PROXIES_LAST_SEEN_HASH_KEY, proxyName, System.currentTimeMillis().toString())
pipeline.sync()
}
}

override fun unregisterProxy(proxyName: String) {
this.jedisPool.resource.use { jedis ->
val playerIds = jedis.smembers("shulker:proxies:$proxyName:players")
val playersRedisKey = PROXIES_PLAYERS_SET_KEY(proxyName)
val playerIds = jedis.smembers(playersRedisKey)

val pipeline = jedis.pipelined()
pipeline.srem("shulker:proxies", proxyName)
pipeline.hdel("shulker:proxies:last-seen", proxyName)
pipeline.del("shulker:proxies:$proxyName:players")
pipeline.srem(PROXIES_SET_KEY, proxyName)
pipeline.hdel(PROXIES_CAPACITY_HASH_KEY, proxyName)
pipeline.hdel(PROXIES_LAST_SEEN_HASH_KEY, proxyName)
pipeline.del(playersRedisKey)
pipeline.sync()

val playerPipeline = jedis.pipelined()
playerIds.forEach { playerId ->
playerPipeline.srem("shulker:players:online", playerId)
playerPipeline.hdel("shulker:players:current-proxy", playerId)
playerPipeline.hdel("shulker:players:current-server", playerId)
playerPipeline.srem(PLAYERS_ONLINE_SET_KEY, playerId)
playerPipeline.hdel(PLAYERS_CURRENT_PROXY_HASH_KEY, playerId)
playerPipeline.hdel(PLAYERS_CURRENT_SERVER_HASH_KEY, playerId)
}
playerPipeline.sync()
}
}

override fun updateProxyLastSeen(proxyName: String) {
this.jedisPool.resource.use { jedis ->
jedis.hset("shulker:proxies:last-seen", proxyName, System.currentTimeMillis().toString())
jedis.hset(PROXIES_LAST_SEEN_HASH_KEY, proxyName, System.currentTimeMillis().toString())
}
}

override fun listRegisteredProxies(): List<CacheAdapter.RegisteredProxy> {
this.jedisPool.resource.use { jedis ->
val registeredProxies = jedis.smembers("shulker:proxies")
val lastSeenMillis = jedis.hgetAll("shulker:proxies:last-seen")
val registeredProxies = jedis.smembers(PROXIES_SET_KEY)
val capacities = jedis.hgetAll(PROXIES_CAPACITY_HASH_KEY)
val lastSeenMillis = jedis.hgetAll(PROXIES_LAST_SEEN_HASH_KEY)

return registeredProxies.map { proxyName ->
val capacity = capacities[proxyName]?.toInt() ?: 0
val lastSeen = lastSeenMillis[proxyName]?.toLong() ?: 0L
CacheAdapter.RegisteredProxy(proxyName, lastSeen)
CacheAdapter.RegisteredProxy(proxyName, capacity, lastSeen)
}
}
}

override fun tryLockLostProxiesPurgeTask(ownerProxyName: String): Optional<CacheAdapter.Lock> =
this.tryLock(ownerProxyName, "shulker:lock:lost-proxies-purge", PROXY_LOST_PURGE_LOCK_SECONDS)
this.tryLock(ownerProxyName, LOCKS_LOST_PROXIES_PURGE_KEY, PROXY_LOST_PURGE_LOCK_SECONDS)

override fun unregisterServer(serverName: String) {
this.jedisPool.resource.use { jedis ->
jedis.del("shulker:servers:$serverName:players")
jedis.del(SERVERS_PLAYERS_SET_KEY(serverName))
}
}

override fun listPlayersInServer(serverName: String): List<UUID> {
this.jedisPool.resource.use { jedis ->
val playerIds = jedis.smembers("shulker:servers:$serverName:players")
val playerIds = jedis.smembers(SERVERS_PLAYERS_SET_KEY(serverName))
return playerIds.map(UUID::fromString)
}
}

override fun setPlayerPosition(playerId: UUID, proxyName: String, serverName: String) {
this.jedisPool.resource.use { jedis ->
val playerIdString = playerId.toString()
val oldProxyName = jedis.hget("shulker:players:current-proxy", playerIdString)
val oldServerName = jedis.hget("shulker:players:current-server", playerIdString)
val oldProxyName = jedis.hget(PLAYERS_CURRENT_PROXY_HASH_KEY, playerIdString)
val oldServerName = jedis.hget(PLAYERS_CURRENT_SERVER_HASH_KEY, playerIdString)

val pipeline = jedis.pipelined()
pipeline.sadd("shulker:players:online", playerIdString)
pipeline.hset("shulker:players:current-proxy", playerIdString, proxyName)
pipeline.hset("shulker:players:current-server", playerIdString, serverName)
pipeline.sadd(PLAYERS_ONLINE_SET_KEY, playerIdString)
pipeline.hset(PLAYERS_CURRENT_PROXY_HASH_KEY, playerIdString, proxyName)
pipeline.hset(PLAYERS_CURRENT_SERVER_HASH_KEY, playerIdString, serverName)

if (oldProxyName != null) {
pipeline.srem("shulker:proxies:$oldProxyName:players", playerIdString)
pipeline.srem(PROXIES_PLAYERS_SET_KEY(oldProxyName), playerIdString)
}
pipeline.sadd("shulker:proxies:$proxyName:players", playerIdString)
pipeline.sadd(PROXIES_PLAYERS_SET_KEY(proxyName), playerIdString)

if (oldServerName != null) {
pipeline.srem("shulker:servers:$oldServerName:players", playerIdString)
pipeline.srem(SERVERS_PLAYERS_SET_KEY(oldServerName), playerIdString)
}
pipeline.sadd("shulker:servers:$serverName:players", playerIdString)
pipeline.sadd(SERVERS_PLAYERS_SET_KEY(serverName), playerIdString)

pipeline.sync()
}
Expand All @@ -103,15 +136,15 @@ class RedisCacheAdapter(private val jedisPool: JedisPool) : CacheAdapter {
override fun unsetPlayerPosition(playerId: UUID) {
this.jedisPool.resource.use { jedis ->
val playerIdString = playerId.toString()
val currentProxyName = jedis.hget("shulker:players:current-proxy", playerIdString)
val currentServerName = jedis.hget("shulker:players:current-server", playerIdString)
val currentProxyName = jedis.hget(PLAYERS_CURRENT_PROXY_HASH_KEY, playerIdString)
val currentServerName = jedis.hget(PLAYERS_CURRENT_SERVER_HASH_KEY, playerIdString)

val pipeline = jedis.pipelined()
pipeline.srem("shulker:players:online", playerIdString)
pipeline.hdel("shulker:players:current-proxy", playerIdString)
pipeline.hdel("shulker:players:current-server", playerIdString)
pipeline.srem("shulker:proxies:$currentProxyName:players", playerIdString)
pipeline.srem("shulker:servers:$currentServerName:players", playerIdString)
pipeline.srem(PLAYERS_ONLINE_SET_KEY, playerIdString)
pipeline.hdel(PLAYERS_CURRENT_PROXY_HASH_KEY, playerIdString)
pipeline.hdel(PLAYERS_CURRENT_SERVER_HASH_KEY, playerIdString)
pipeline.srem(PROXIES_PLAYERS_SET_KEY(currentProxyName), playerIdString)
pipeline.srem(SERVERS_PLAYERS_SET_KEY(currentServerName), playerIdString)
pipeline.sync()
}
}
Expand All @@ -121,8 +154,8 @@ class RedisCacheAdapter(private val jedisPool: JedisPool) : CacheAdapter {
val playerIdString = playerId.toString()

val pipeline = jedis.pipelined()
val proxyNameResponse = pipeline.hget("shulker:players:current-proxy", playerIdString)
val serverNameResponse = pipeline.hget("shulker:players:current-server", playerIdString)
val proxyNameResponse = pipeline.hget(PLAYERS_CURRENT_PROXY_HASH_KEY, playerIdString)
val serverNameResponse = pipeline.hget(PLAYERS_CURRENT_SERVER_HASH_KEY, playerIdString)
pipeline.sync()

if (proxyNameResponse != null && serverNameResponse != null) {
Expand All @@ -139,7 +172,7 @@ class RedisCacheAdapter(private val jedisPool: JedisPool) : CacheAdapter {

override fun isPlayerConnected(playerId: UUID): Boolean {
this.jedisPool.resource.use { jedis ->
return jedis.sismember("shulker:players:online", playerId.toString())
return jedis.sismember(PLAYERS_ONLINE_SET_KEY, playerId.toString())
}
}

Expand All @@ -149,28 +182,28 @@ class RedisCacheAdapter(private val jedisPool: JedisPool) : CacheAdapter {
val params = SetParams().ex(PLAYER_ID_CACHE_TTL_SECONDS)

val pipeline = jedis.pipelined()
pipeline.set("shulker:uuid-cache:id-to-name:$playerIdString", playerName, params)
pipeline.set("shulker:uuid-cache:name-to-id:$playerName", playerIdString, params)
pipeline.set(UUID_CACHE_ID_TO_NAME_KEY(playerIdString), playerName, params)
pipeline.set(UUID_CACHE_NAME_TO_ID_KEY(playerName), playerIdString, params)
pipeline.sync()
}
}

override fun getPlayerNameFromId(playerId: UUID): Optional<String> {
this.jedisPool.resource.use { jedis ->
return Optional.ofNullable(jedis.get("shulker:uuid-cache:id-to-name:$playerId"))
return Optional.ofNullable(jedis.get(UUID_CACHE_ID_TO_NAME_KEY(playerId.toString())))
}
}

override fun getPlayerIdFromName(playerName: String): Optional<UUID> {
this.jedisPool.resource.use { jedis ->
return Optional.ofNullable(jedis.get("shulker:uuid-cache:name-to-id:$playerName")).map(UUID::fromString)
return Optional.ofNullable(jedis.get(UUID_CACHE_NAME_TO_ID_KEY(playerName))).map(UUID::fromString)
}
}

override fun getPlayerNamesFromIds(playerIds: List<UUID>): Map<UUID, String> {
this.jedisPool.resource.use { jedis ->
val pipeline = jedis.pipelined()
val responses = playerIds.associateWith { uuid -> pipeline.get("shulker:uuid-cache:id-to-name:$uuid") }
val responses = playerIds.associateWith { uuid -> pipeline.get(UUID_CACHE_ID_TO_NAME_KEY(uuid.toString())) }
pipeline.sync()

return responses.mapValues { (_, response) -> response.get() }
Expand All @@ -179,7 +212,13 @@ class RedisCacheAdapter(private val jedisPool: JedisPool) : CacheAdapter {

override fun countOnlinePlayers(): Int {
this.jedisPool.resource.use { jedis ->
return jedis.scard("shulker:players:online").toInt()
return jedis.scard(PLAYERS_ONLINE_SET_KEY).toInt()
}
}

override fun countPlayerCapacity(): Int {
this.jedisPool.resource.use { jedis ->
return jedis.hgetAll(PROXIES_CAPACITY_HASH_KEY).values.sumOf { it.toInt() }
}
}

Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
package io.shulkermc.proxyagent.platform

data class ProxyPingHookResult(val playerCount: Int)
data class ProxyPingHookResult(val onlinePlayerCount: Int, val maxPlayerCount: Int)

typealias ProxyPingHook = () -> ProxyPingHookResult
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ class PlayerMovementService(private val agent: ShulkerProxyAgentCommon) {
private const val LIMBO_TAG = "limbo"

private const val ONLINE_PLAYERS_COUNT_MEMOIZE_SECONDS = 10L
private const val PLAYER_CAPACITY_COUNT_MEMOIZE_SECONDS = 60L

private val MSG_NOT_ACCEPTING_PLAYERS = createDisconnectMessage(
"Proxy is not accepting players, try reconnect.",
Expand All @@ -35,6 +36,12 @@ class PlayerMovementService(private val agent: ShulkerProxyAgentCommon) {
ONLINE_PLAYERS_COUNT_MEMOIZE_SECONDS,
java.util.concurrent.TimeUnit.SECONDS
)
private val playerCapacityCountSupplier = Suppliers.memoizeWithExpiration(
{ this.agent.cache.countPlayerCapacity() },
PLAYER_CAPACITY_COUNT_MEMOIZE_SECONDS,
java.util.concurrent.TimeUnit.SECONDS
)

private var acceptingPlayers = true

init {
Expand All @@ -57,7 +64,7 @@ class PlayerMovementService(private val agent: ShulkerProxyAgentCommon) {
}

private fun onProxyPing(): ProxyPingHookResult {
return ProxyPingHookResult(this.onlinePlayerCountSupplier.get())
return ProxyPingHookResult(this.onlinePlayerCountSupplier.get(), this.playerCapacityCountSupplier.get())
}

private fun onPlayerPreLogin(): PlayerPreLoginHookResult {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -53,7 +53,10 @@ class ProxyInterfaceVelocity(
this.mapPostOrder(postOrder)
) { event ->
val result = hook()
event.ping = event.ping.asBuilder().onlinePlayers(result.playerCount).build()
event.ping = event.ping.asBuilder()
.onlinePlayers(result.onlinePlayerCount)
.maximumPlayers(result.maxPlayerCount)
.build()
}
}

Expand Down Expand Up @@ -137,6 +140,10 @@ class ProxyInterfaceVelocity(
return this.proxy.playerCount
}

override fun getPlayerCapacity(): Int {
return this.proxy.configuration.showMaxPlayers
}

override fun scheduleDelayedTask(
delay: Long,
timeUnit: TimeUnit,
Expand Down

0 comments on commit 31cd569

Please sign in to comment.