Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: exclude proxy from load balancer when full #396

Merged
merged 5 commits into from
Feb 6, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -283,6 +283,13 @@ spec:
description: Name of an optional ConfigMap already containing the server configuration
nullable: true
type: string
lifecycleStrategy:
default: AllocateWhenNotEmpty
description: Strategy to apply concerning Agones `GameServer` lifecycle management
enum:
- AllocateWhenNotEmpty
- Manual
type: string
maxPlayers:
default: 20
description: Number of maximum players that can connect to the MinecraftServer Deployment
Expand Down
7 changes: 7 additions & 0 deletions kube/helm/templates/crds/shulkermc.io_minecraftservers.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,13 @@ spec:
description: Name of an optional ConfigMap already containing the server configuration
nullable: true
type: string
lifecycleStrategy:
default: AllocateWhenNotEmpty
description: Strategy to apply concerning Agones `GameServer` lifecycle management
enum:
- AllocateWhenNotEmpty
- Manual
type: string
maxPlayers:
default: 20
description: Number of maximum players that can connect to the MinecraftServer Deployment
Expand Down
6 changes: 6 additions & 0 deletions kube/helm/templates/crds/shulkermc.io_proxyfleets.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -337,6 +337,12 @@ spec:
type: object
nullable: true
type: array
playersDeltaBeforeExclusion:
default: 15
description: Number of player slots to reserve when exclusing a proxy from the load balancer. This will allow load balancer implementations to update itself while still being able to accept some players
format: uint32
minimum: 0.0
type: integer
plugins:
description: List of references to plugins to download
items:
Expand Down
13 changes: 13 additions & 0 deletions packages/shulker-crds/src/v1alpha1/proxy_fleet.rs
Original file line number Diff line number Diff line change
Expand Up @@ -130,6 +130,15 @@ pub struct ProxyFleetTemplateConfigurationSpec {
/// drained automatically
#[schemars(default = "ProxyFleetTemplateConfigurationSpec::default_ttl_seconds")]
pub ttl_seconds: u32,

/// Number of player slots to reserve when exclusing a proxy
/// from the load balancer. This will allow load balancer
/// implementations to update itself while still being able
/// to accept some players
#[schemars(
default = "ProxyFleetTemplateConfigurationSpec::default_players_delta_before_exclusion"
)]
pub players_delta_before_exclusion: u32,
}

#[cfg(not(tarpaulin_include))]
Expand All @@ -153,6 +162,10 @@ impl ProxyFleetTemplateConfigurationSpec {
fn default_ttl_seconds() -> u32 {
86400
}

fn default_players_delta_before_exclusion() -> u32 {
15
}
}

#[derive(Deserialize, Serialize, Clone, Debug, Default, JsonSchema)]
Expand Down
2 changes: 2 additions & 0 deletions packages/shulker-operator/assets/proxy-probe-readiness.sh
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@ set -o xtrace

if [ -f "/tmp/drain-lock" ]; then
echo "Drain lock found" && exit 1
elif [ -f "/tmp/readiness-lock" ]; then
echo "Readiness lock found" && exit 1
fi

bash /usr/bin/health.sh
Original file line number Diff line number Diff line change
Expand Up @@ -348,6 +348,7 @@ mod bungeecord {
server_icon: "A Server Icon".to_string(),
proxy_protocol: true,
ttl_seconds: 300,
players_delta_before_exclusion: 15,
};
let service_spec = Some(ProxyFleetServiceSpec {
type_: ProxyFleetServiceType::LoadBalancer,
Expand Down Expand Up @@ -552,6 +553,7 @@ mod velocity {
server_icon: "A Server Icon".to_string(),
proxy_protocol: true,
ttl_seconds: 300,
players_delta_before_exclusion: 15,
};
let service_spec = Some(ProxyFleetServiceSpec {
type_: ProxyFleetServiceType::LoadBalancer,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,7 @@ lazy_static! {
server_icon: "abc==".to_string(),
proxy_protocol: true,
ttl_seconds: 3600,
players_delta_before_exclusion: 15,
},
pod_overrides: Some(ProxyFleetTemplatePodOverridesSpec {
image: None,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -447,6 +447,11 @@ impl<'a> FleetBuilder {
value: Some(spec.config.ttl_seconds.to_string()),
..EnvVar::default()
},
EnvVar {
name: "SHULKER_PROXY_PLAYER_DELTA_BEFORE_EXCLUSION".to_string(),
value: Some(spec.config.players_delta_before_exclusion.to_string()),
..EnvVar::default()
},
EnvVar {
name: "SHULKER_NETWORK_ADMINS".to_string(),
value: Some(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ apiVersion: v1
kind: ConfigMap
data:
init-fs.sh: "#!/bin/sh\nset -euo pipefail\nset -o xtrace\n\ncp \"${SHULKER_CONFIG_DIR}/probe-readiness.sh\" \"${SHULKER_PROXY_DATA_DIR}/probe-readiness.sh\"\ncat \"${SHULKER_CONFIG_DIR}/server-icon.png\" | base64 -d > \"${SHULKER_PROXY_DATA_DIR}/server-icon.png\"\n\nif [ \"${SHULKER_VERSION_CHANNEL}\" == \"Velocity\" ]; then\n cp \"${SHULKER_CONFIG_DIR}/velocity-config.toml\" \"${SHULKER_PROXY_DATA_DIR}/velocity.toml\"\n echo \"dummy\" > \"${SHULKER_PROXY_DATA_DIR}/forwarding.secret\"\nelse\n cp \"${SHULKER_CONFIG_DIR}/bungeecord-config.yml\" \"${SHULKER_PROXY_DATA_DIR}/config.yml\"\nfi\n\nif [ ! -z \"${SHULKER_PROXY_PLUGIN_URLS+x}\" ]; then\n mkdir -p \"${SHULKER_PROXY_DATA_DIR}/plugins\"\n for plugin_url in ${SHULKER_PROXY_PLUGIN_URLS//;/ }; do\n (cd \"${SHULKER_PROXY_DATA_DIR}/plugins\" && wget \"${plugin_url}\")\n done\nfi\n\nif [ ! -z \"${SHULKER_PROXY_PATCH_URLS+x}\" ]; then\n for patch_url in ${SHULKER_PROXY_PATCH_URLS//;/ }; do\n (cd \"${SHULKER_PROXY_DATA_DIR}\" && wget \"${patch_url}\" -O - | tar -xzv)\n done\nfi\n"
probe-readiness.sh: "#!/bin/sh\nset -euo pipefail\nset -o xtrace\n\nif [ -f \"/tmp/drain-lock\" ]; then\n echo \"Drain lock found\" && exit 1\nfi\n\nbash /usr/bin/health.sh\n"
probe-readiness.sh: "#!/bin/sh\nset -euo pipefail\nset -o xtrace\n\nif [ -f \"/tmp/drain-lock\" ]; then\n echo \"Drain lock found\" && exit 1\nelif [ -f \"/tmp/readiness-lock\" ]; then\n echo \"Readiness lock found\" && exit 1\nfi\n\nbash /usr/bin/health.sh\n"
server-icon.png: abc==
velocity-config.toml: "config-version = \"2.6\"\nbind = \"0.0.0.0:25577\"\nmotd = \"A Motd\"\nshow-max-players = 1000\nonline-mode = true\nforce-key-authentication = true\nprevent-client-proxy-connections = true\nforwarding-secret-file = \"/mnt/shulker/forwarding-secret/key\"\nplayer-info-forwarding-mode = \"modern\"\n\n[servers]\nlobby = \"localhost:30000\"\nlimbo = \"localhost:30001\"\ntry = [\"lobby\", \"limbo\"]\n\n[forced-hosts]\n\n[advanced]\nhaproxy-protocol = true\ntcp-fast-open = true\n\n"
metadata:
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -73,6 +73,8 @@ spec:
fieldPath: metadata.namespace
- name: SHULKER_PROXY_TTL_SECONDS
value: "3600"
- name: SHULKER_PROXY_PLAYER_DELTA_BEFORE_EXCLUSION
value: "15"
- name: SHULKER_NETWORK_ADMINS
value: ""
- name: SHULKER_PROXY_REDIS_HOST
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,13 +2,17 @@ package io.shulkermc.proxyagent

import java.util.Optional
import java.util.UUID
import kotlin.jvm.optionals.getOrDefault

@SuppressWarnings("detekt:MagicNumber")
object Configuration {
val CLUSTER_NAME = getStringEnv("SHULKER_CLUSTER_NAME")

val PROXY_NAMESPACE = getStringEnv("SHULKER_PROXY_NAMESPACE")
val PROXY_NAME = getStringEnv("SHULKER_PROXY_NAME")
val PROXY_TTL_SECONDS = getLongEnv("SHULKER_PROXY_TTL_SECONDS")
val PROXY_PLAYER_DELTA_BEFORE_EXCLUSION = getOptionalIntEnv("SHULKER_PROXY_PLAYER_DELTA_BEFORE_EXCLUSION")
.getOrDefault(15)

val NETWORK_ADMINS: List<UUID> = getOptionalStringEnv("SHULKER_NETWORK_ADMINS")
.map {
Expand All @@ -26,5 +30,7 @@ object Configuration {
private fun getStringEnv(name: String): String = requireNotNull(System.getenv(name)) { "Missing $name" }
private fun getOptionalStringEnv(name: String): Optional<String> = Optional.ofNullable(System.getenv(name))
private fun getIntEnv(name: String): Int = getStringEnv(name).toInt()
private fun getOptionalIntEnv(name: String): Optional<Int> = Optional.ofNullable(System.getenv(name))
.map { it.toInt() }
private fun getLongEnv(name: String): Long = getStringEnv(name).toLong()
}
Original file line number Diff line number Diff line change
Expand Up @@ -87,7 +87,7 @@ class ShulkerProxyAgentCommon(val proxyInterface: ProxyInterface, val logger: Lo
}

this.cache.registerProxy(Configuration.PROXY_NAME, this.proxyInterface.getPlayerCapacity())
this.agonesGateway.setAllocated()
this.agonesGateway.setReady()
} catch (@Suppress("TooGenericExceptionCaught") e: Exception) {
this.logger.log(Level.SEVERE, "Shulker Agent crashed, stopping proxy", e)
this.shutdown()
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,8 @@
package io.shulkermc.proxyagent.adapters.filesystem

interface FileSystemAdapter {
fun createDrainFile()
fun createDrainLock()

fun createReadinessLock()
fun deleteReadinessLock()
}
Original file line number Diff line number Diff line change
Expand Up @@ -4,11 +4,22 @@ import java.nio.file.Files
import java.nio.file.Path

private val DRAIN_LOCK_PATH = Path.of("/tmp/drain-lock")
private val READINESS_LOCK_PATH = Path.of("/tmp/readiness-lock")

class LocalFileSystemAdapter : FileSystemAdapter {
override fun createDrainFile() {
override fun createDrainLock() {
if (!Files.exists(DRAIN_LOCK_PATH)) {
Files.createFile(DRAIN_LOCK_PATH)
}
}

override fun createReadinessLock() {
if (!Files.exists(READINESS_LOCK_PATH)) {
Files.createFile(READINESS_LOCK_PATH)
}
}

override fun deleteReadinessLock() {
Files.deleteIfExists(READINESS_LOCK_PATH)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,9 @@ class PlayerMovementService(private val agent: ShulkerProxyAgentCommon) {
)
}

private val maxPlayersWithExclusionDelta =
this.agent.proxyInterface.getPlayerCapacity() - Configuration.PROXY_PLAYER_DELTA_BEFORE_EXCLUSION

private val onlinePlayerCountSupplier = Suppliers.memoizeWithExpiration(
{ this.agent.cache.countOnlinePlayers() },
ONLINE_PLAYERS_COUNT_MEMOIZE_SECONDS,
Expand All @@ -42,6 +45,7 @@ class PlayerMovementService(private val agent: ShulkerProxyAgentCommon) {
java.util.concurrent.TimeUnit.SECONDS
)

private var isAllocatedByAgones = false
private var acceptingPlayers = true

init {
Expand All @@ -57,8 +61,10 @@ class PlayerMovementService(private val agent: ShulkerProxyAgentCommon) {
this.acceptingPlayers = acceptingPlayers

if (acceptingPlayers) {
this.agent.fileSystem.deleteReadinessLock()
this.agent.logger.info("Proxy is now accepting players")
} else {
this.agent.fileSystem.createReadinessLock()
this.agent.logger.info("Proxy is no longer accepting players")
}
}
Expand All @@ -71,15 +77,34 @@ class PlayerMovementService(private val agent: ShulkerProxyAgentCommon) {
if (!this.acceptingPlayers) {
return PlayerPreLoginHookResult.disallow(MSG_NOT_ACCEPTING_PLAYERS)
}

return PlayerPreLoginHookResult.allow()
}

private fun onPlayerLogin(player: Player) {
this.agent.cache.updateCachedPlayerName(player.uniqueId, player.name)

if (!this.isAllocatedByAgones) {
this.isAllocatedByAgones = true
this.agent.agonesGateway.setAllocated()
}

if (this.isProxyConsideredFull()) {
this.setAcceptingPlayers(false)
}
}

private fun onPlayerDisconnect(player: Player) {
this.agent.cache.unsetPlayerPosition(player.uniqueId)

if (this.isAllocatedByAgones && this.agent.proxyInterface.getPlayerCount() == 0) {
this.isAllocatedByAgones = false
this.agent.agonesGateway.setReady()
}

if (!this.isProxyConsideredFull()) {
this.setAcceptingPlayers(true)
}
}

private fun onServerPreConnect(player: Player, originalServerName: String): ServerPreConnectHookResult {
Expand All @@ -105,4 +130,8 @@ class PlayerMovementService(private val agent: ShulkerProxyAgentCommon) {
private fun onServerPostConnect(player: Player, serverName: String) {
this.agent.cache.setPlayerPosition(player.uniqueId, Configuration.PROXY_NAME, serverName)
}

private fun isProxyConsideredFull(): Boolean {
return this.agent.proxyInterface.getPlayerCount() >= this.maxPlayersWithExclusionDelta
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,7 @@ class ProxyLifecycleService(private val agent: ShulkerProxyAgentCommon) {
}
this.drained = true

this.agent.fileSystem.createDrainFile()
this.agent.fileSystem.createDrainLock()
this.agent.playerMovementService.setAcceptingPlayers(false)

this.agent.proxyInterface.scheduleRepeatingTask(
Expand Down
Loading