diff --git a/.idea/modules/packages/google-agones-sdk/bindings/java/shulker.packages.google-agones-sdk-bindings-java.main.iml b/.idea/modules/packages/google-agones-sdk/bindings/java/shulker.packages.google-agones-sdk-bindings-java.main.iml index e02e12bd..7cad772b 100644 --- a/.idea/modules/packages/google-agones-sdk/bindings/java/shulker.packages.google-agones-sdk-bindings-java.main.iml +++ b/.idea/modules/packages/google-agones-sdk/bindings/java/shulker.packages.google-agones-sdk-bindings-java.main.iml @@ -39,12 +39,6 @@ - - - - - - @@ -54,15 +48,6 @@ - - - - - - - - - diff --git a/.idea/modules/packages/google-agones-sdk/bindings/java/shulker.packages.google-agones-sdk-bindings-java.test.iml b/.idea/modules/packages/google-agones-sdk/bindings/java/shulker.packages.google-agones-sdk-bindings-java.test.iml index 3d57d996..02d297bb 100644 --- a/.idea/modules/packages/google-agones-sdk/bindings/java/shulker.packages.google-agones-sdk-bindings-java.test.iml +++ b/.idea/modules/packages/google-agones-sdk/bindings/java/shulker.packages.google-agones-sdk-bindings-java.test.iml @@ -38,25 +38,10 @@ - - - - - - - - - - - - - - - diff --git a/.idea/modules/packages/shulker-proxy-agent/shulker.packages.shulker-proxy-agent.bungeecord.iml b/.idea/modules/packages/shulker-proxy-agent/shulker.packages.shulker-proxy-agent.bungeecord.iml index 17c99862..7aae7798 100644 --- a/.idea/modules/packages/shulker-proxy-agent/shulker.packages.shulker-proxy-agent.bungeecord.iml +++ b/.idea/modules/packages/shulker-proxy-agent/shulker.packages.shulker-proxy-agent.bungeecord.iml @@ -82,6 +82,7 @@ + @@ -129,12 +130,14 @@ + + + - @@ -165,7 +168,6 @@ - diff --git a/.idea/modules/packages/shulker-proxy-agent/shulker.packages.shulker-proxy-agent.common.iml b/.idea/modules/packages/shulker-proxy-agent/shulker.packages.shulker-proxy-agent.common.iml index de646be6..fadadf42 100644 --- a/.idea/modules/packages/shulker-proxy-agent/shulker.packages.shulker-proxy-agent.common.iml +++ b/.idea/modules/packages/shulker-proxy-agent/shulker.packages.shulker-proxy-agent.common.iml @@ -76,6 +76,7 @@ + @@ -112,6 +113,9 @@ + + + @@ -130,7 +134,6 @@ - diff --git a/.idea/modules/packages/shulker-proxy-agent/shulker.packages.shulker-proxy-agent.velocity.iml b/.idea/modules/packages/shulker-proxy-agent/shulker.packages.shulker-proxy-agent.velocity.iml index 0dd29b61..22586fd8 100644 --- a/.idea/modules/packages/shulker-proxy-agent/shulker.packages.shulker-proxy-agent.velocity.iml +++ b/.idea/modules/packages/shulker-proxy-agent/shulker.packages.shulker-proxy-agent.velocity.iml @@ -83,6 +83,7 @@ + @@ -91,7 +92,7 @@ - + @@ -131,6 +132,8 @@ + + @@ -159,7 +162,6 @@ - diff --git a/.idea/modules/packages/shulker-proxy-api/shulker.packages.shulker-proxy-api.main.iml b/.idea/modules/packages/shulker-proxy-api/shulker.packages.shulker-proxy-api.main.iml index 69a1a5b3..5197100f 100644 --- a/.idea/modules/packages/shulker-proxy-api/shulker.packages.shulker-proxy-api.main.iml +++ b/.idea/modules/packages/shulker-proxy-api/shulker.packages.shulker-proxy-api.main.iml @@ -39,26 +39,11 @@ - - - - - - - - - - - - - - - diff --git a/.idea/modules/packages/shulker-proxy-api/shulker.packages.shulker-proxy-api.test.iml b/.idea/modules/packages/shulker-proxy-api/shulker.packages.shulker-proxy-api.test.iml index e6c98412..419aabfe 100644 --- a/.idea/modules/packages/shulker-proxy-api/shulker.packages.shulker-proxy-api.test.iml +++ b/.idea/modules/packages/shulker-proxy-api/shulker.packages.shulker-proxy-api.test.iml @@ -38,25 +38,10 @@ - - - - - - - - - - - - - - - diff --git a/.idea/modules/packages/shulker-sdk/bindings/java/shulker.packages.shulker-sdk-bindings-java.main.iml b/.idea/modules/packages/shulker-sdk/bindings/java/shulker.packages.shulker-sdk-bindings-java.main.iml index 68375a29..8f2bdc60 100644 --- a/.idea/modules/packages/shulker-sdk/bindings/java/shulker.packages.shulker-sdk-bindings-java.main.iml +++ b/.idea/modules/packages/shulker-sdk/bindings/java/shulker.packages.shulker-sdk-bindings-java.main.iml @@ -39,12 +39,6 @@ - - - - - - @@ -54,15 +48,6 @@ - - - - - - - - - diff --git a/.idea/modules/packages/shulker-sdk/bindings/java/shulker.packages.shulker-sdk-bindings-java.test.iml b/.idea/modules/packages/shulker-sdk/bindings/java/shulker.packages.shulker-sdk-bindings-java.test.iml index 40331a09..cb8a4234 100644 --- a/.idea/modules/packages/shulker-sdk/bindings/java/shulker.packages.shulker-sdk-bindings-java.test.iml +++ b/.idea/modules/packages/shulker-sdk/bindings/java/shulker.packages.shulker-sdk-bindings-java.test.iml @@ -38,25 +38,10 @@ - - - - - - - - - - - - - - - diff --git a/.idea/modules/packages/shulker-server-agent/shulker.packages.shulker-server-agent.common.iml b/.idea/modules/packages/shulker-server-agent/shulker.packages.shulker-server-agent.common.iml index dd07f0b0..39042ba9 100644 --- a/.idea/modules/packages/shulker-server-agent/shulker.packages.shulker-server-agent.common.iml +++ b/.idea/modules/packages/shulker-server-agent/shulker.packages.shulker-server-agent.common.iml @@ -44,27 +44,12 @@ - - - - - - - - - - - - - - - diff --git a/.idea/modules/packages/shulker-server-agent/shulker.packages.shulker-server-agent.main.iml b/.idea/modules/packages/shulker-server-agent/shulker.packages.shulker-server-agent.main.iml index ae7c08c0..ec3616a7 100644 --- a/.idea/modules/packages/shulker-server-agent/shulker.packages.shulker-server-agent.main.iml +++ b/.idea/modules/packages/shulker-server-agent/shulker.packages.shulker-server-agent.main.iml @@ -39,24 +39,9 @@ - - - - - - - - - - - - - - - diff --git a/.idea/modules/packages/shulker-server-agent/shulker.packages.shulker-server-agent.paper.iml b/.idea/modules/packages/shulker-server-agent/shulker.packages.shulker-server-agent.paper.iml index 028d91ec..a0efffea 100644 --- a/.idea/modules/packages/shulker-server-agent/shulker.packages.shulker-server-agent.paper.iml +++ b/.idea/modules/packages/shulker-server-agent/shulker.packages.shulker-server-agent.paper.iml @@ -45,12 +45,6 @@ - - - - - - @@ -58,15 +52,6 @@ - - - - - - - - - diff --git a/.idea/modules/packages/shulker-server-agent/shulker.packages.shulker-server-agent.test.iml b/.idea/modules/packages/shulker-server-agent/shulker.packages.shulker-server-agent.test.iml index d8335c92..0068abf4 100644 --- a/.idea/modules/packages/shulker-server-agent/shulker.packages.shulker-server-agent.test.iml +++ b/.idea/modules/packages/shulker-server-agent/shulker.packages.shulker-server-agent.test.iml @@ -38,25 +38,10 @@ - - - - - - - - - - - - - - - diff --git a/.idea/modules/packages/shulker-server-api/shulker.packages.shulker-server-api.main.iml b/.idea/modules/packages/shulker-server-api/shulker.packages.shulker-server-api.main.iml index ea4ba241..e0a7ce29 100644 --- a/.idea/modules/packages/shulker-server-api/shulker.packages.shulker-server-api.main.iml +++ b/.idea/modules/packages/shulker-server-api/shulker.packages.shulker-server-api.main.iml @@ -39,26 +39,11 @@ - - - - - - - - - - - - - - - diff --git a/.idea/modules/packages/shulker-server-api/shulker.packages.shulker-server-api.test.iml b/.idea/modules/packages/shulker-server-api/shulker.packages.shulker-server-api.test.iml index 2a4afd10..38e91bee 100644 --- a/.idea/modules/packages/shulker-server-api/shulker.packages.shulker-server-api.test.iml +++ b/.idea/modules/packages/shulker-server-api/shulker.packages.shulker-server-api.test.iml @@ -38,25 +38,10 @@ - - - - - - - - - - - - - - - diff --git a/.idea/modules/packages/shulker.packages.main.iml b/.idea/modules/packages/shulker.packages.main.iml index 4d21f4eb..86f3e160 100644 --- a/.idea/modules/packages/shulker.packages.main.iml +++ b/.idea/modules/packages/shulker.packages.main.iml @@ -39,24 +39,9 @@ - - - - - - - - - - - - - - - diff --git a/.idea/modules/packages/shulker.packages.test.iml b/.idea/modules/packages/shulker.packages.test.iml index 5cf223db..357218eb 100644 --- a/.idea/modules/packages/shulker.packages.test.iml +++ b/.idea/modules/packages/shulker.packages.test.iml @@ -38,25 +38,10 @@ - - - - - - - - - - - - - - - diff --git a/.idea/modules/shulker.main.iml b/.idea/modules/shulker.main.iml index 30bbe565..3a6e57fa 100644 --- a/.idea/modules/shulker.main.iml +++ b/.idea/modules/shulker.main.iml @@ -37,9 +37,24 @@ + + + + + + + + + + + + + + + diff --git a/.idea/modules/shulker.test.iml b/.idea/modules/shulker.test.iml index 0bf1abb2..eabeca5a 100644 --- a/.idea/modules/shulker.test.iml +++ b/.idea/modules/shulker.test.iml @@ -38,9 +38,24 @@ + + + + + + + + + + + + + + + diff --git a/packages/shulker-proxy-agent/build.gradle.kts b/packages/shulker-proxy-agent/build.gradle.kts index 78f8d5aa..6f0eb587 100644 --- a/packages/shulker-proxy-agent/build.gradle.kts +++ b/packages/shulker-proxy-agent/build.gradle.kts @@ -15,6 +15,9 @@ dependencies { // Agones "commonImplementation"(project(":packages:google-agones-sdk-bindings-java")) "commonRuntimeOnly"("io.grpc:grpc-netty-shaded:1.58.0") + + // Sync + "commonImplementation"("redis.clients:jedis:5.0.2") } setOf("bungeecordJar", "velocityJar").forEach { taskName -> diff --git a/packages/shulker-proxy-agent/src/bungeecord/kotlin/io/shulkermc/proxyagent/bungeecord/ProxyInterfaceBungeeCord.kt b/packages/shulker-proxy-agent/src/bungeecord/kotlin/io/shulkermc/proxyagent/bungeecord/ProxyInterfaceBungeeCord.kt index d381e912..08ec9355 100644 --- a/packages/shulker-proxy-agent/src/bungeecord/kotlin/io/shulkermc/proxyagent/bungeecord/ProxyInterfaceBungeeCord.kt +++ b/packages/shulker-proxy-agent/src/bungeecord/kotlin/io/shulkermc/proxyagent/bungeecord/ProxyInterfaceBungeeCord.kt @@ -2,20 +2,25 @@ package io.shulkermc.proxyagent.bungeecord import io.shulkermc.proxyagent.ProxyInterface import io.shulkermc.proxyagent.platform.Player +import io.shulkermc.proxyagent.platform.PlayerDisconnectHook import io.shulkermc.proxyagent.platform.PlayerPreLoginHook +import io.shulkermc.proxyagent.platform.ServerPostConnectHook 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.connection.ProxiedPlayer +import net.md_5.bungee.api.event.PlayerDisconnectEvent import net.md_5.bungee.api.event.PreLoginEvent import net.md_5.bungee.api.event.ServerConnectEvent +import net.md_5.bungee.api.event.ServerConnectedEvent import net.md_5.bungee.api.plugin.Listener import net.md_5.bungee.api.plugin.Plugin import net.md_5.bungee.api.scheduler.ScheduledTask import net.md_5.bungee.event.EventHandler import net.md_5.bungee.event.EventPriority import java.net.InetSocketAddress +import java.util.UUID import java.util.concurrent.TimeUnit class ProxyInterfaceBungeeCord( @@ -34,6 +39,34 @@ class ProxyInterfaceBungeeCord( return this.proxy.servers.containsKey(name) } + override fun addPlayerPreLoginHook(hook: PlayerPreLoginHook) { + this.proxy.pluginManager.registerListener( + this.plugin, + object : Listener { + @EventHandler(priority = EventPriority.HIGHEST) + private fun onPreLogin(event: PreLoginEvent) { + if (event.isCancelled) return + val result = hook() + + if (!result.allowed) + event.setCancelReason(*BungeeComponentSerializer.get().serialize(result.rejectComponent!!)) + } + } + ) + } + + override fun addPlayerDisconnectHook(hook: PlayerDisconnectHook) { + this.proxy.pluginManager.registerListener( + this.plugin, + object : Listener { + @EventHandler(priority = EventPriority.LOWEST) + private fun onPlayerDisconnect(event: PlayerDisconnectEvent) { + hook(wrapPlayer(event.player)) + } + } + ) + } + override fun addServerPreConnectHook(hook: ServerPreConnectHook) { this.proxy.pluginManager.registerListener( this.plugin, @@ -50,17 +83,13 @@ class ProxyInterfaceBungeeCord( ) } - override fun addPlayerPreLoginHook(hook: PlayerPreLoginHook) { + override fun addServerPostConnectHook(hook: ServerPostConnectHook) { this.proxy.pluginManager.registerListener( this.plugin, object : Listener { - @EventHandler(priority = EventPriority.HIGHEST) - private fun onPreLogin(event: PreLoginEvent) { - if (event.isCancelled) return - val result = hook() - - if (!result.allowed) - event.setCancelReason(*BungeeComponentSerializer.get().serialize(result.rejectComponent!!)) + @EventHandler(priority = EventPriority.LOWEST) + private fun onServerConnected(event: ServerConnectedEvent) { + hook(wrapPlayer(event.player), event.server.info.name) } } ) @@ -80,6 +109,9 @@ class ProxyInterfaceBungeeCord( private fun wrapPlayer(bungeePlayer: ProxiedPlayer): Player { return object : Player { + override val uniqueId: UUID + get() = bungeePlayer.uniqueId + override fun disconnect(component: Component) { bungeePlayer.disconnect(*BungeeComponentSerializer.get().serialize(component)) } diff --git a/packages/shulker-proxy-agent/src/common/kotlin/io/shulkermc/proxyagent/Configuration.kt b/packages/shulker-proxy-agent/src/common/kotlin/io/shulkermc/proxyagent/Configuration.kt index b91dcc70..5bd19ed4 100644 --- a/packages/shulker-proxy-agent/src/common/kotlin/io/shulkermc/proxyagent/Configuration.kt +++ b/packages/shulker-proxy-agent/src/common/kotlin/io/shulkermc/proxyagent/Configuration.kt @@ -1,7 +1,17 @@ package io.shulkermc.proxyagent object Configuration { - val PROXY_NAMESPACE = requireNotNull(System.getenv("SHULKER_PROXY_NAMESPACE")) { "Missing SHULKER_PROXY_NAMESPACE" } - val PROXY_NAME = requireNotNull(System.getenv("SHULKER_PROXY_NAME")) { "Missing SHULKER_PROXY_NAME" } - val PROXY_TTL_SECONDS = requireNotNull(System.getenv("SHULKER_PROXY_TTL_SECONDS")) { "Missing SHULKER_PROXY_TTL_SECONDS" }.toLong() + val PROXY_NAMESPACE = getStringEnv("SHULKER_PROXY_NAMESPACE") + val PROXY_NAME = getStringEnv("SHULKER_PROXY_NAME") + val PROXY_TTL_SECONDS = getLongEnv("SHULKER_PROXY_TTL_SECONDS") + + val REDIS_HOST = getStringEnv("SHULKER_PROXY_REDIS_HOST") + val REDIS_PORT = getIntEnv("SHULKER_PROXY_REDIS_PORT") + val REDIS_USERNAME = getOptionalStringEnv("SHULKER_PROXY_REDIS_USERNAME") + val REDIS_PASSWORD = getOptionalStringEnv("SHULKER_PROXY_REDIS_PASSWORD") + + private fun getStringEnv(name: String): String = requireNotNull(System.getenv(name)) { "Missing $name" } + private fun getOptionalStringEnv(name: String): String? = System.getenv(name) + private fun getIntEnv(name: String): Int = getStringEnv(name).toInt() + private fun getLongEnv(name: String): Long = getStringEnv(name).toLong() } diff --git a/packages/shulker-proxy-agent/src/common/kotlin/io/shulkermc/proxyagent/ProxyInterface.kt b/packages/shulker-proxy-agent/src/common/kotlin/io/shulkermc/proxyagent/ProxyInterface.kt index b99d632a..b6d6da0f 100644 --- a/packages/shulker-proxy-agent/src/common/kotlin/io/shulkermc/proxyagent/ProxyInterface.kt +++ b/packages/shulker-proxy-agent/src/common/kotlin/io/shulkermc/proxyagent/ProxyInterface.kt @@ -1,6 +1,8 @@ package io.shulkermc.proxyagent +import io.shulkermc.proxyagent.platform.PlayerDisconnectHook import io.shulkermc.proxyagent.platform.PlayerPreLoginHook +import io.shulkermc.proxyagent.platform.ServerPostConnectHook import io.shulkermc.proxyagent.platform.ServerPreConnectHook import java.net.InetSocketAddress import java.util.concurrent.TimeUnit @@ -10,8 +12,10 @@ interface ProxyInterface { fun unregisterServer(name: String): Boolean fun hasServer(name: String): Boolean - fun addServerPreConnectHook(hook: ServerPreConnectHook) fun addPlayerPreLoginHook(hook: PlayerPreLoginHook) + fun addPlayerDisconnectHook(hook: PlayerDisconnectHook) + fun addServerPreConnectHook(hook: ServerPreConnectHook) + fun addServerPostConnectHook(hook: ServerPostConnectHook) fun getPlayerCount(): Int diff --git a/packages/shulker-proxy-agent/src/common/kotlin/io/shulkermc/proxyagent/ShulkerProxyAgentCommon.kt b/packages/shulker-proxy-agent/src/common/kotlin/io/shulkermc/proxyagent/ShulkerProxyAgentCommon.kt index a90b5e44..f99622da 100644 --- a/packages/shulker-proxy-agent/src/common/kotlin/io/shulkermc/proxyagent/ShulkerProxyAgentCommon.kt +++ b/packages/shulker-proxy-agent/src/common/kotlin/io/shulkermc/proxyagent/ShulkerProxyAgentCommon.kt @@ -2,30 +2,40 @@ package io.shulkermc.proxyagent import dev.agones.AgonesSDK import dev.agones.AgonesSDKImpl +import io.shulkermc.proxyagent.adapters.cache.CacheAdapter +import io.shulkermc.proxyagent.adapters.cache.RedisCacheAdapter import io.shulkermc.proxyagent.adapters.filesystem.FileSystemAdapter -import io.shulkermc.proxyagent.adapters.filesystem.FileSystemAdapterImpl +import io.shulkermc.proxyagent.adapters.filesystem.LocalFileSystemAdapter import io.shulkermc.proxyagent.adapters.kubernetes.KubernetesGatewayAdapter -import io.shulkermc.proxyagent.adapters.kubernetes.KubernetesGatewayAdapterImpl +import io.shulkermc.proxyagent.adapters.kubernetes.ImplKubernetesGatewayAdapter import io.shulkermc.proxyagent.api.ShulkerProxyAPI import io.shulkermc.proxyagent.api.ShulkerProxyAPIImpl import io.shulkermc.proxyagent.services.PlayerMovementService import io.shulkermc.proxyagent.services.ProxyLifecycleService import io.shulkermc.proxyagent.services.ServerDirectoryService +import io.shulkermc.proxyagent.tasks.HealthcheckTask +import io.shulkermc.proxyagent.tasks.LostProxyPurgeTask +import redis.clients.jedis.JedisPool import java.lang.Exception -import java.util.concurrent.TimeUnit import java.util.logging.Logger import kotlin.system.exitProcess class ShulkerProxyAgentCommon(val proxyInterface: ProxyInterface, val logger: Logger) { - private lateinit var agonesGateway: AgonesSDK + lateinit var agonesGateway: AgonesSDK + private lateinit var jedisPool: JedisPool + + // Adapters lateinit var kubernetesGateway: KubernetesGatewayAdapter lateinit var fileSystem: FileSystemAdapter + lateinit var cache: CacheAdapter + // Services lateinit var serverDirectoryService: ServerDirectoryService lateinit var playerMovementService: PlayerMovementService private lateinit var proxyLifecycleService: ProxyLifecycleService private lateinit var healthcheckTask: ProxyInterface.ScheduledTask + private lateinit var lostProxyPurgeTask: ProxyInterface.ScheduledTask fun onProxyInitialization() { try { @@ -35,17 +45,21 @@ class ShulkerProxyAgentCommon(val proxyInterface: ProxyInterface, val logger: Lo ShulkerProxyAPI.INSTANCE = ShulkerProxyAPIImpl(this) - this.fileSystem = FileSystemAdapterImpl() - this.kubernetesGateway = KubernetesGatewayAdapterImpl(Configuration.PROXY_NAMESPACE, Configuration.PROXY_NAME) + this.jedisPool = this.createJedisPool() + this.jedisPool.resource.use { jedis -> jedis.ping() } + + this.fileSystem = LocalFileSystemAdapter() + this.kubernetesGateway = ImplKubernetesGatewayAdapter(Configuration.PROXY_NAMESPACE, Configuration.PROXY_NAME) + this.cache = RedisCacheAdapter(this.jedisPool) this.serverDirectoryService = ServerDirectoryService(this) this.playerMovementService = PlayerMovementService(this) this.proxyLifecycleService = ProxyLifecycleService(this) - this.healthcheckTask = this.proxyInterface.scheduleRepeatingTask(0L, 5L, TimeUnit.SECONDS) { - this.agonesGateway.sendHealthcheck() - } + this.healthcheckTask = HealthcheckTask(this).schedule() + this.lostProxyPurgeTask = LostProxyPurgeTask(this).schedule() + this.cache.registerProxy(Configuration.PROXY_NAME) this.agonesGateway.setAllocated() } catch (e: Exception) { this.logger.severe("Failed to parse configuration") @@ -55,19 +69,29 @@ class ShulkerProxyAgentCommon(val proxyInterface: ProxyInterface, val logger: Lo } fun onProxyShutdown() { + this.cache.unregisterProxy(Configuration.PROXY_NAME) + this.healthcheckTask.cancel() this.proxyLifecycleService.destroy() this.kubernetesGateway.destroy() + this.jedisPool.destroy() this.agonesGateway.askShutdown() this.agonesGateway.destroy() } fun shutdown() { try { + this.cache.unregisterProxy(Configuration.PROXY_NAME) this.agonesGateway.askShutdown() } catch (ex: Exception) { this.logger.severe("Failed to ask Agones sidecar to shutdown properly, stopping process manually") exitProcess(0) } } + + private fun createJedisPool(): JedisPool { + if (Configuration.REDIS_USERNAME != null && Configuration.REDIS_PASSWORD != null) + return JedisPool(Configuration.REDIS_HOST, Configuration.REDIS_PORT) + return JedisPool(Configuration.REDIS_HOST, Configuration.REDIS_PORT, Configuration.REDIS_USERNAME, Configuration.REDIS_PASSWORD) + } } diff --git a/packages/shulker-proxy-agent/src/common/kotlin/io/shulkermc/proxyagent/adapters/cache/CacheAdapter.kt b/packages/shulker-proxy-agent/src/common/kotlin/io/shulkermc/proxyagent/adapters/cache/CacheAdapter.kt new file mode 100644 index 00000000..ef6aa3d0 --- /dev/null +++ b/packages/shulker-proxy-agent/src/common/kotlin/io/shulkermc/proxyagent/adapters/cache/CacheAdapter.kt @@ -0,0 +1,25 @@ +package io.shulkermc.proxyagent.adapters.cache + +import io.shulkermc.proxyagent.api.ShulkerProxyAPI.PlayerPosition +import java.util.Optional +import java.util.UUID + +interface CacheAdapter { + fun registerProxy(name: String) + fun unregisterProxy(name: String) + fun updateProxyLastSeen(name: String) + fun listRegisteredProxies(): List + + fun setPlayerPosition(playerId: UUID, proxyName: String, serverName: String) + fun unsetPlayerPosition(playerId: UUID) + fun getPlayerPosition(playerId: UUID): Optional + fun isPlayerConnected(playerId: UUID): Boolean + + fun tryLockLostProxiesPurgeTask(ownerProxyName: String, ttlSeconds: Long): Optional + + data class RegisteredProxy(val proxyName: String, val lastSeenMillis: Long) + + interface Lock { + fun release() + } +} diff --git a/packages/shulker-proxy-agent/src/common/kotlin/io/shulkermc/proxyagent/adapters/cache/RedisCacheAdapter.kt b/packages/shulker-proxy-agent/src/common/kotlin/io/shulkermc/proxyagent/adapters/cache/RedisCacheAdapter.kt new file mode 100644 index 00000000..71c9049f --- /dev/null +++ b/packages/shulker-proxy-agent/src/common/kotlin/io/shulkermc/proxyagent/adapters/cache/RedisCacheAdapter.kt @@ -0,0 +1,137 @@ +package io.shulkermc.proxyagent.adapters.cache + +import io.shulkermc.proxyagent.api.ShulkerProxyAPI.PlayerPosition +import redis.clients.jedis.JedisPool +import redis.clients.jedis.params.SetParams +import java.util.Optional +import java.util.UUID + +class RedisCacheAdapter(private val jedisPool: JedisPool) : CacheAdapter { + override fun registerProxy(name: String) { + this.jedisPool.resource.use { jedis -> + val pipeline = jedis.pipelined() + pipeline.sadd("shulker:proxies", name) + pipeline.hset("shulker:proxies:last-seen", name, System.currentTimeMillis().toString()) + pipeline.sync() + } + } + + override fun unregisterProxy(name: String) { + this.jedisPool.resource.use { jedis -> + val pipeline = jedis.pipelined() + pipeline.srem("shulker:proxies", name) + pipeline.hdel("shulker:proxies:last-seen", name) + val playerIdsResponse = pipeline.smembers("shulker:proxies:$name:players") + pipeline.del("shulker:proxies:$name:players") + pipeline.sync() + + val playerIds = playerIdsResponse.get() + 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.sync() + } + } + + override fun updateProxyLastSeen(name: String) { + this.jedisPool.resource.use { jedis -> + jedis.hset("shulker:proxies:last-seen", name, System.currentTimeMillis().toString()) + } + } + + override fun listRegisteredProxies(): List { + this.jedisPool.resource.use { jedis -> + val registeredProxies = jedis.smembers("shulker:proxies") + val lastSeenMillis = jedis.hgetAll("shulker:proxies:last-seen") + + return registeredProxies.map { proxyName -> + val lastSeen = lastSeenMillis[proxyName]?.toLong() ?: 0L + CacheAdapter.RegisteredProxy(proxyName, lastSeen) + } + } + } + + override fun setPlayerPosition(playerId: UUID, proxyName: String, serverName: String) { + this.jedisPool.resource.use { jedis -> + val playerIdString = playerId.toString() + val currentProxy = jedis.hget("shulker:players:current-proxy", 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) + + if (currentProxy != null) + pipeline.srem("shulker:proxies:$currentProxy:players", playerIdString) + pipeline.sadd("shulker:proxies:$proxyName:players", playerIdString) + + pipeline.sync() + } + } + + override fun unsetPlayerPosition(playerId: UUID) { + this.jedisPool.resource.use { jedis -> + val playerIdString = playerId.toString() + val currentProxy = jedis.hget("shulker:players:current-proxy", 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:$currentProxy:players", playerIdString) + + pipeline.sync() + } + } + + override fun getPlayerPosition(playerId: UUID): Optional { + this.jedisPool.resource.use { jedis -> + 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) + pipeline.sync() + + if (proxyNameResponse != null && serverNameResponse != null) { + val proxyName = proxyNameResponse.get() + val serverName = serverNameResponse.get() + return Optional.of(PlayerPosition(proxyName, serverName)) + } else { + this.unsetPlayerPosition(playerId) + } + + return Optional.empty() + } + } + + override fun isPlayerConnected(playerId: UUID): Boolean { + this.jedisPool.resource.use { jedis -> + return jedis.sismember("shulker:players:online", playerId.toString()) + } + } + + override fun tryLockLostProxiesPurgeTask(ownerProxyName: String, ttlSeconds: Long): Optional = + this.tryLock(ownerProxyName, "shulker:lock:lost-proxies-purge", ttlSeconds) + + private fun tryLock(ownerProxyName: String, key: String, ttlSeconds: Long): Optional { + this.jedisPool.resource.use { jedis -> + val success = jedis.set(key, ownerProxyName, SetParams().nx().ex(ttlSeconds)) != null + + if (success) { + return Optional.of(object : CacheAdapter.Lock { + override fun release() { + jedisPool.resource.use { jedis -> jedis.del(key) } + } + }) + } + + return Optional.empty() + } + } +} diff --git a/packages/shulker-proxy-agent/src/common/kotlin/io/shulkermc/proxyagent/adapters/filesystem/FileSystemAdapterImpl.kt b/packages/shulker-proxy-agent/src/common/kotlin/io/shulkermc/proxyagent/adapters/filesystem/LocalFileSystemAdapter.kt similarity index 85% rename from packages/shulker-proxy-agent/src/common/kotlin/io/shulkermc/proxyagent/adapters/filesystem/FileSystemAdapterImpl.kt rename to packages/shulker-proxy-agent/src/common/kotlin/io/shulkermc/proxyagent/adapters/filesystem/LocalFileSystemAdapter.kt index 9f93b192..14d55926 100644 --- a/packages/shulker-proxy-agent/src/common/kotlin/io/shulkermc/proxyagent/adapters/filesystem/FileSystemAdapterImpl.kt +++ b/packages/shulker-proxy-agent/src/common/kotlin/io/shulkermc/proxyagent/adapters/filesystem/LocalFileSystemAdapter.kt @@ -5,7 +5,7 @@ import java.nio.file.Path private var DRAIN_LOCK_PATH = Path.of("/tmp/drain-lock") -class FileSystemAdapterImpl : FileSystemAdapter { +class LocalFileSystemAdapter : FileSystemAdapter { override fun createDrainFile() { if (!Files.exists(DRAIN_LOCK_PATH)) Files.createFile(DRAIN_LOCK_PATH) diff --git a/packages/shulker-proxy-agent/src/common/kotlin/io/shulkermc/proxyagent/adapters/kubernetes/KubernetesGatewayAdapterImpl.kt b/packages/shulker-proxy-agent/src/common/kotlin/io/shulkermc/proxyagent/adapters/kubernetes/ImplKubernetesGatewayAdapter.kt similarity index 98% rename from packages/shulker-proxy-agent/src/common/kotlin/io/shulkermc/proxyagent/adapters/kubernetes/KubernetesGatewayAdapterImpl.kt rename to packages/shulker-proxy-agent/src/common/kotlin/io/shulkermc/proxyagent/adapters/kubernetes/ImplKubernetesGatewayAdapter.kt index 99517e3d..8db5eca5 100644 --- a/packages/shulker-proxy-agent/src/common/kotlin/io/shulkermc/proxyagent/adapters/kubernetes/KubernetesGatewayAdapterImpl.kt +++ b/packages/shulker-proxy-agent/src/common/kotlin/io/shulkermc/proxyagent/adapters/kubernetes/ImplKubernetesGatewayAdapter.kt @@ -9,7 +9,7 @@ import io.fabric8.kubernetes.client.okhttp.OkHttpClientFactory import io.shulkermc.proxyagent.adapters.kubernetes.models.AgonesV1GameServer import java.util.concurrent.CompletionStage -class KubernetesGatewayAdapterImpl(proxyNamespace: String, proxyName: String) : KubernetesGatewayAdapter { +class ImplKubernetesGatewayAdapter(proxyNamespace: String, proxyName: String) : KubernetesGatewayAdapter { private val kubernetesClient: KubernetesClient = KubernetesClientBuilder() .withHttpClientFactory(OkHttpClientFactory()) .build() diff --git a/packages/shulker-proxy-agent/src/common/kotlin/io/shulkermc/proxyagent/api/ShulkerProxyAPIImpl.kt b/packages/shulker-proxy-agent/src/common/kotlin/io/shulkermc/proxyagent/api/ShulkerProxyAPIImpl.kt index 3599845f..53b95563 100644 --- a/packages/shulker-proxy-agent/src/common/kotlin/io/shulkermc/proxyagent/api/ShulkerProxyAPIImpl.kt +++ b/packages/shulker-proxy-agent/src/common/kotlin/io/shulkermc/proxyagent/api/ShulkerProxyAPIImpl.kt @@ -1,7 +1,12 @@ package io.shulkermc.proxyagent.api import io.shulkermc.proxyagent.ShulkerProxyAgentCommon +import java.util.Optional +import java.util.UUID class ShulkerProxyAPIImpl(private val agent: ShulkerProxyAgentCommon) : ShulkerProxyAPI() { override fun getServersByTag(tag: String): Set = this.agent.serverDirectoryService.getServersByTag(tag) + + override fun getPlayerPosition(playerId: UUID): Optional = this.agent.cache.getPlayerPosition(playerId) + override fun isPlayerConnected(playerId: UUID): Boolean = this.agent.cache.isPlayerConnected(playerId) } diff --git a/packages/shulker-proxy-agent/src/common/kotlin/io/shulkermc/proxyagent/platform/Player.kt b/packages/shulker-proxy-agent/src/common/kotlin/io/shulkermc/proxyagent/platform/Player.kt index 360cd42b..b13e348d 100644 --- a/packages/shulker-proxy-agent/src/common/kotlin/io/shulkermc/proxyagent/platform/Player.kt +++ b/packages/shulker-proxy-agent/src/common/kotlin/io/shulkermc/proxyagent/platform/Player.kt @@ -1,7 +1,9 @@ package io.shulkermc.proxyagent.platform import net.kyori.adventure.text.Component +import java.util.UUID interface Player { + val uniqueId: UUID fun disconnect(component: Component) } diff --git a/packages/shulker-proxy-agent/src/common/kotlin/io/shulkermc/proxyagent/platform/PlayerDisconnectHook.kt b/packages/shulker-proxy-agent/src/common/kotlin/io/shulkermc/proxyagent/platform/PlayerDisconnectHook.kt new file mode 100644 index 00000000..12afbd72 --- /dev/null +++ b/packages/shulker-proxy-agent/src/common/kotlin/io/shulkermc/proxyagent/platform/PlayerDisconnectHook.kt @@ -0,0 +1,3 @@ +package io.shulkermc.proxyagent.platform + +typealias PlayerDisconnectHook = (player: Player) -> Unit diff --git a/packages/shulker-proxy-agent/src/common/kotlin/io/shulkermc/proxyagent/platform/ServerPostConnectHook.kt b/packages/shulker-proxy-agent/src/common/kotlin/io/shulkermc/proxyagent/platform/ServerPostConnectHook.kt new file mode 100644 index 00000000..63244919 --- /dev/null +++ b/packages/shulker-proxy-agent/src/common/kotlin/io/shulkermc/proxyagent/platform/ServerPostConnectHook.kt @@ -0,0 +1,3 @@ +package io.shulkermc.proxyagent.platform + +typealias ServerPostConnectHook = (player: Player, serverName: String) -> Unit diff --git a/packages/shulker-proxy-agent/src/common/kotlin/io/shulkermc/proxyagent/services/PlayerMovementService.kt b/packages/shulker-proxy-agent/src/common/kotlin/io/shulkermc/proxyagent/services/PlayerMovementService.kt index 216fee33..5ef76373 100644 --- a/packages/shulker-proxy-agent/src/common/kotlin/io/shulkermc/proxyagent/services/PlayerMovementService.kt +++ b/packages/shulker-proxy-agent/src/common/kotlin/io/shulkermc/proxyagent/services/PlayerMovementService.kt @@ -1,5 +1,6 @@ package io.shulkermc.proxyagent.services +import io.shulkermc.proxyagent.Configuration import io.shulkermc.proxyagent.ShulkerProxyAgentCommon import io.shulkermc.proxyagent.platform.Player import io.shulkermc.proxyagent.platform.PlayerPreLoginHookResult @@ -26,10 +27,10 @@ class PlayerMovementService(private val agent: ShulkerProxyAgentCommon) { private var acceptingPlayers = true init { - this.agent.proxyInterface.addPlayerPreLoginHook { this.onPlayerPreLogin() } - this.agent.proxyInterface.addServerPreConnectHook { player, originalServerName -> - this.onServerPreConnect(player, originalServerName) - } + this.agent.proxyInterface.addPlayerPreLoginHook(this::onPlayerPreLogin) + this.agent.proxyInterface.addPlayerDisconnectHook(this::onPlayerDisconnect) + this.agent.proxyInterface.addServerPreConnectHook(this::onServerPreConnect) + this.agent.proxyInterface.addServerPostConnectHook(this::onServerPostConnect) } fun setAcceptingPlayers(acceptingPlayers: Boolean) { @@ -48,12 +49,20 @@ class PlayerMovementService(private val agent: ShulkerProxyAgentCommon) { PlayerPreLoginHookResult.allow() } + private fun onPlayerDisconnect(player: Player) { + this.agent.cache.unsetPlayerPosition(player.uniqueId) + } + private fun onServerPreConnect(player: Player, originalServerName: String): ServerPreConnectHookResult { if (originalServerName == LIMBO_TAG) return this.tryConnectToLimboOrDisconnect(player) return ServerPreConnectHookResult(Optional.empty()) } + private fun onServerPostConnect(player: Player, serverName: String) { + this.agent.cache.setPlayerPosition(player.uniqueId, Configuration.PROXY_NAME, serverName) + } + private fun tryConnectToLimboOrDisconnect(player: Player): ServerPreConnectHookResult { val firstLimboServer = this.agent.serverDirectoryService.getServersByTag(LIMBO_TAG).firstOrNull() if (firstLimboServer != null) diff --git a/packages/shulker-proxy-agent/src/common/kotlin/io/shulkermc/proxyagent/tasks/HealthcheckTask.kt b/packages/shulker-proxy-agent/src/common/kotlin/io/shulkermc/proxyagent/tasks/HealthcheckTask.kt new file mode 100644 index 00000000..3c1db971 --- /dev/null +++ b/packages/shulker-proxy-agent/src/common/kotlin/io/shulkermc/proxyagent/tasks/HealthcheckTask.kt @@ -0,0 +1,17 @@ +package io.shulkermc.proxyagent.tasks + +import io.shulkermc.proxyagent.Configuration +import io.shulkermc.proxyagent.ProxyInterface +import io.shulkermc.proxyagent.ShulkerProxyAgentCommon +import java.util.concurrent.TimeUnit + +class HealthcheckTask(private val agent: ShulkerProxyAgentCommon) : Runnable { + fun schedule(): ProxyInterface.ScheduledTask { + return this.agent.proxyInterface.scheduleRepeatingTask(0L, 5L, TimeUnit.SECONDS, this) + } + + override fun run() { + this.agent.agonesGateway.sendHealthcheck() + this.agent.cache.updateProxyLastSeen(Configuration.PROXY_NAME) + } +} diff --git a/packages/shulker-proxy-agent/src/common/kotlin/io/shulkermc/proxyagent/tasks/LostProxyPurgeTask.kt b/packages/shulker-proxy-agent/src/common/kotlin/io/shulkermc/proxyagent/tasks/LostProxyPurgeTask.kt new file mode 100644 index 00000000..1295fe49 --- /dev/null +++ b/packages/shulker-proxy-agent/src/common/kotlin/io/shulkermc/proxyagent/tasks/LostProxyPurgeTask.kt @@ -0,0 +1,34 @@ +package io.shulkermc.proxyagent.tasks + +import io.shulkermc.proxyagent.Configuration +import io.shulkermc.proxyagent.ProxyInterface +import io.shulkermc.proxyagent.ShulkerProxyAgentCommon +import java.util.concurrent.TimeUnit + +class LostProxyPurgeTask(private val agent: ShulkerProxyAgentCommon) : Runnable { + companion object { + private const val PROXY_LOST_MILLIS_THRESHOLD = 1L + } + + fun schedule(): ProxyInterface.ScheduledTask { + return this.agent.proxyInterface.scheduleRepeatingTask(1L, 1L, TimeUnit.MINUTES, this) + } + + override fun run() { + val maybeLock = this.agent.cache.tryLockLostProxiesPurgeTask(Configuration.PROXY_NAME, 15L) + + if (maybeLock.isPresent) { + val lock = maybeLock.get() + this.agent.logger.info("Purging lost proxies") + + this.agent.cache.listRegisteredProxies() + .filter { System.currentTimeMillis() - it.lastSeenMillis > PROXY_LOST_MILLIS_THRESHOLD } + .forEach { + this.agent.cache.unregisterProxy(it.proxyName) + this.agent.logger.info("Unregistered lost proxy ${it.proxyName}") + } + + lock.release() + } + } +} diff --git a/packages/shulker-proxy-agent/src/velocity/kotlin/io/shulkermc/proxyagent/velocity/ProxyInterfaceVelocity.kt b/packages/shulker-proxy-agent/src/velocity/kotlin/io/shulkermc/proxyagent/velocity/ProxyInterfaceVelocity.kt index 10790e57..dd22bc35 100644 --- a/packages/shulker-proxy-agent/src/velocity/kotlin/io/shulkermc/proxyagent/velocity/ProxyInterfaceVelocity.kt +++ b/packages/shulker-proxy-agent/src/velocity/kotlin/io/shulkermc/proxyagent/velocity/ProxyInterfaceVelocity.kt @@ -1,17 +1,22 @@ package io.shulkermc.proxyagent.velocity import com.velocitypowered.api.event.PostOrder +import com.velocitypowered.api.event.connection.DisconnectEvent import com.velocitypowered.api.event.connection.PreLoginEvent +import com.velocitypowered.api.event.player.ServerPostConnectEvent import com.velocitypowered.api.event.player.ServerPreConnectEvent import com.velocitypowered.api.proxy.Player import com.velocitypowered.api.proxy.ProxyServer import com.velocitypowered.api.proxy.server.ServerInfo import com.velocitypowered.api.scheduler.ScheduledTask import io.shulkermc.proxyagent.ProxyInterface +import io.shulkermc.proxyagent.platform.PlayerDisconnectHook import io.shulkermc.proxyagent.platform.PlayerPreLoginHook +import io.shulkermc.proxyagent.platform.ServerPostConnectHook import io.shulkermc.proxyagent.platform.ServerPreConnectHook import net.kyori.adventure.text.Component import java.net.InetSocketAddress +import java.util.UUID import java.util.concurrent.TimeUnit import kotlin.jvm.optionals.getOrElse @@ -31,6 +36,22 @@ class ProxyInterfaceVelocity(private val plugin: ShulkerProxyAgentVelocity, priv return this.proxy.getServer(name).isPresent } + override fun addPlayerPreLoginHook(hook: PlayerPreLoginHook) { + this.proxy.eventManager.register(this.plugin, PreLoginEvent::class.java, PostOrder.FIRST) { event -> + if (!event.result.isAllowed) return@register + val result = hook() + + if (!result.allowed) + event.result = PreLoginEvent.PreLoginComponentResult.denied(result.rejectComponent) + } + } + + override fun addPlayerDisconnectHook(hook: PlayerDisconnectHook) { + this.proxy.eventManager.register(this.plugin, DisconnectEvent::class.java, PostOrder.LAST) { event -> + hook(this.wrapPlayer(event.player)) + } + } + override fun addServerPreConnectHook(hook: ServerPreConnectHook) { this.proxy.eventManager.register(this.plugin, ServerPreConnectEvent::class.java, PostOrder.LAST) { event -> if (!event.result.isAllowed) return@register @@ -41,13 +62,10 @@ class ProxyInterfaceVelocity(private val plugin: ShulkerProxyAgentVelocity, priv } } - override fun addPlayerPreLoginHook(hook: PlayerPreLoginHook) { - this.proxy.eventManager.register(this.plugin, PreLoginEvent::class.java, PostOrder.FIRST) { event -> - if (!event.result.isAllowed) return@register - val result = hook() - - if (!result.allowed) - event.result = PreLoginEvent.PreLoginComponentResult.denied(result.rejectComponent) + @Suppress("UnstableApiUsage") + override fun addServerPostConnectHook(hook: ServerPostConnectHook) { + this.proxy.eventManager.register(this.plugin, ServerPostConnectEvent::class.java, PostOrder.LAST) { event -> + hook(this.wrapPlayer(event.player), event.player.currentServer.get().serverInfo.name) } } @@ -76,6 +94,9 @@ class ProxyInterfaceVelocity(private val plugin: ShulkerProxyAgentVelocity, priv private fun wrapPlayer(velocityPlayer: Player): io.shulkermc.proxyagent.platform.Player { return object : io.shulkermc.proxyagent.platform.Player { + override val uniqueId: UUID + get() = velocityPlayer.uniqueId + override fun disconnect(component: Component) { velocityPlayer.disconnect(component) } diff --git a/packages/shulker-proxy-api/src/main/java/io/shulkermc/proxyagent/api/ShulkerProxyAPI.java b/packages/shulker-proxy-api/src/main/java/io/shulkermc/proxyagent/api/ShulkerProxyAPI.java index faf1113e..2a642362 100644 --- a/packages/shulker-proxy-api/src/main/java/io/shulkermc/proxyagent/api/ShulkerProxyAPI.java +++ b/packages/shulker-proxy-api/src/main/java/io/shulkermc/proxyagent/api/ShulkerProxyAPI.java @@ -1,9 +1,18 @@ package io.shulkermc.proxyagent.api; +import org.jetbrains.annotations.NotNull; + +import java.util.Optional; import java.util.Set; +import java.util.UUID; public abstract class ShulkerProxyAPI { public static ShulkerProxyAPI INSTANCE; - abstract public Set getServersByTag(String tag); + abstract public Set getServersByTag(@NotNull String tag); + + abstract public Optional getPlayerPosition(@NotNull UUID playerId); + abstract public boolean isPlayerConnected(@NotNull UUID playerId); + + public record PlayerPosition(@NotNull String proxyName, @NotNull String serverName) {} }