diff --git a/.github/workflows/validate.yml b/.github/workflows/validate.yml index 64df75b..88511cc 100644 --- a/.github/workflows/validate.yml +++ b/.github/workflows/validate.yml @@ -55,6 +55,9 @@ jobs: - name: Check Palworld frame rate against an engine response run: python3 test-palworld-performance.py + - name: Check Minecraft JVM settings in the pinned image + run: python3 test-minecraft-jvm.py + - name: Check ARK world migration in the pinned vendor image run: python3 test-ark-volume-migration.py diff --git a/.schema/gametemplate.schema.json b/.schema/gametemplate.schema.json index f286e39..1350c3f 100644 --- a/.schema/gametemplate.schema.json +++ b/.schema/gametemplate.schema.json @@ -943,6 +943,10 @@ "items": { "description": "ConfigField is a single user-tunable setting surfaced in the wizard.", "properties": { + "applyOnRestart": { + "description": "ApplyOnRestart stages this environment value for the next container start.\nEdits and clears update the config Secret without rolling the game Pod.\nConfig files that reference the field are refreshed by their init container\non the next Pod start. Other fields keep their usual rollout behavior.", + "type": "boolean" + }, "autoFromMemoryLimit": { "description": "AutoFromMemoryLimit, when set, computes this field's value from the\ngame container's effective memory limit whenever neither\nGameServer.spec.config nor Default provides one: floor(limit \u00d7\npercent / 100), rendered as whole mebibytes with an \"M\" suffix\n(e.g. \"6144M\"). Lets templates size game memory settings (like a\nJVM heap) to whatever resources the server was given instead of a\nstatic default \u2014 an explicit user value always wins, and a template\nwithout a memory limit leaves the field unset.", "properties": { @@ -1033,7 +1037,13 @@ "name", "type" ], - "type": "object" + "type": "object", + "x-kubernetes-validations": [ + { + "message": "applyOnRestart requires an environment field", + "rule": "!has(self.applyOnRestart) || !self.applyOnRestart || !has(self.target) || self.target == 'env'" + } + ] }, "type": "array" }, diff --git a/minecraft-java/README.md b/minecraft-java/README.md index f58f9af..20d2a65 100644 --- a/minecraft-java/README.md +++ b/minecraft-java/README.md @@ -36,23 +36,25 @@ one-click install a mod or plugin by name without hunting for a URL. The chosen file downloads through the same allowlist (Modrinth's CDN is listed). **From URL** install remains available for anything not on Modrinth. -## Memory / JVM heap - -Left empty (the default), the wizard's **JVM max heap** / **JVM initial -heap** fields (configSchema `MAX_MEMORY` / `INIT_MEMORY`) are sized -automatically to **75% of the container memory limit** — pick 8Gi of -memory and the heap becomes 6144M, and it re-tracks the limit whenever -you resize the server. The remaining 25% is headroom for non-heap JVM -memory: the limit only caps the cgroup — it does not size the heap — so -a heap at or above the limit gets the server OOM-killed, and a limit far -above the heap wastes RAM the JVM will never touch. Automatic sizing -needs gameplane ≥ 0.2.0-beta.6 (`gameplaneMinVersion` gates this bundle -accordingly). - -Setting either field explicitly (e.g. `2G`, `3072M`) turns the automatic -sizing off for that field and feeds the value straight to the itzg -image's `-Xmx`/`-Xms` plumbing; keep an explicit max heap roughly 25% -below the container limit for the same reason. +## JVM startup settings (2.12.0) + +Install the Gameplane CRDs and operator with support for `applyOnRestart` +before updating the catalog module to 2.12.0 (minimum Gameplane 0.4.1). +Adopting this template on a running server can restart it once. A server +using the OnRequest template policy can schedule that adoption. Once adopted, +saving or clearing heap sizes, extra JVM options or Aikar's flags leaves the +running game in place. Restart it when ready to use the new values. + +Each empty heap field independently defaults to 75% of the container memory +limit, rounded down to whole MiB. A 4Gi limit gives 3072M for each automatic +field. To set the maximum to 2G, also set an initial heap of 2G or less. +Leave space for the JVM's non-heap allocations. + +Fixed sizes such as `512M` and `2G` become `-Xms` and `-Xmx`. A value such +as `37.5%` selects the JVM's InitialRAMPercentage or MaxRAMPercentage mode. +`JVM_OPTS` and `JVM_XX_OPTS` pass space-separated arguments through to the +image. Set heap sizes in their dedicated fields so a later argument does +not override the preview. `USE_AIKAR_FLAGS` enables the image's own preset. ## Logs (including install) diff --git a/minecraft-java/locales/.review.json b/minecraft-java/locales/.review.json index bc91a35..9394e5c 100644 --- a/minecraft-java/locales/.review.json +++ b/minecraft-java/locales/.review.json @@ -64,11 +64,15 @@ "config.DIFFICULTY.options.normal": "317b32c14369", "config.DIFFICULTY.options.peaceful": "2a4baa647f52", "config.HARDCORE.displayName": "38956488bd78", - "config.INIT_MEMORY.description": "b4a5e6eacd0a", + "config.INIT_MEMORY.description": "ff410f20c96d", "config.INIT_MEMORY.displayName": "cfd0b63a804d", + "config.JVM_OPTS.description": "0bb24aebd8fb", + "config.JVM_OPTS.displayName": "64dc680da4d7", + "config.JVM_XX_OPTS.description": "29fc716839c6", + "config.JVM_XX_OPTS.displayName": "df73f9d53612", "config.LEVEL_SEED.description": "cb50f3d0826b", "config.LEVEL_SEED.displayName": "2af8986090a5", - "config.MAX_MEMORY.description": "f1fbbbcce1d6", + "config.MAX_MEMORY.description": "59c3fc2af7ce", "config.MAX_MEMORY.displayName": "5a53a9f3d0ad", "config.MAX_PLAYERS.displayName": "5a6ba90743a6", "config.MODE.displayName": "ede76ceab6f1", @@ -81,6 +85,8 @@ "config.ONLINE_MODE.description": "e0d4a3c8f231", "config.ONLINE_MODE.displayName": "6443cff7f789", "config.PVP.displayName": "82f91ef7cf31", + "config.USE_AIKAR_FLAGS.description": "6a12bb75a5f6", + "config.USE_AIKAR_FLAGS.displayName": "d4a0c9b99aab", "config.VIEW_DISTANCE.displayName": "e308828f4f7c", "config.WHITELIST_ENABLED.displayName": "adda297584e7", "metrics.difficulty.displayName": "be44133ed57f", diff --git a/minecraft-java/locales/en.json b/minecraft-java/locales/en.json index b7b2446..6a9f63a 100644 --- a/minecraft-java/locales/en.json +++ b/minecraft-java/locales/en.json @@ -256,13 +256,29 @@ "description": "Label of configuration field HARDCORE (template.yaml spec.configSchema)" }, "config.INIT_MEMORY.description": { - "defaultMessage": "Starting heap size (-Xms), at most JVM max heap. Leave empty to match the automatic max heap (a fixed heap avoids GC resizes).", + "defaultMessage": "Starting heap, for example 512M or 37.5%. Leave empty for 75% of the container memory limit. Keep the initial heap at or below the maximum.", "description": "Help text of configuration field INIT_MEMORY (template.yaml spec.configSchema)" }, "config.INIT_MEMORY.displayName": { "defaultMessage": "JVM initial heap", "description": "Label of configuration field INIT_MEMORY (template.yaml spec.configSchema)" }, + "config.JVM_OPTS.description": { + "defaultMessage": "Space-separated Java options, for example -Dfile.encoding=UTF-8.", + "description": "Help text of configuration field JVM_OPTS (template.yaml spec.configSchema)" + }, + "config.JVM_OPTS.displayName": { + "defaultMessage": "Additional JVM options", + "description": "Label of configuration field JVM_OPTS (template.yaml spec.configSchema)" + }, + "config.JVM_XX_OPTS.description": { + "defaultMessage": "Space-separated JVM tuning options, for example -XX:+PrintCommandLineFlags.", + "description": "Help text of configuration field JVM_XX_OPTS (template.yaml spec.configSchema)" + }, + "config.JVM_XX_OPTS.displayName": { + "defaultMessage": "Additional JVM tuning options", + "description": "Label of configuration field JVM_XX_OPTS (template.yaml spec.configSchema)" + }, "config.LEVEL_SEED.description": { "defaultMessage": "Leave blank for a random seed.", "description": "Help text of configuration field LEVEL_SEED (template.yaml spec.configSchema)" @@ -272,7 +288,7 @@ "description": "Label of configuration field LEVEL_SEED (template.yaml spec.configSchema)" }, "config.MAX_MEMORY.description": { - "defaultMessage": "Java heap ceiling (-Xmx, e.g. 2G, 3072M). Leave empty to size it automatically to 75% of the container memory limit — it then tracks the limit whenever you resize the server. If you set it yourself, keep roughly 25% below the limit: the JVM needs headroom for non-heap memory, and exceeding the limit gets the server OOM-killed.", + "defaultMessage": "Heap ceiling, for example 2G or 65%. Leave empty for 75% of the container memory limit. Allow room for memory outside the Java heap.", "description": "Help text of configuration field MAX_MEMORY (template.yaml spec.configSchema)" }, "config.MAX_MEMORY.displayName": { @@ -323,6 +339,14 @@ "defaultMessage": "PvP enabled", "description": "Label of configuration field PVP (template.yaml spec.configSchema)" }, + "config.USE_AIKAR_FLAGS.description": { + "defaultMessage": "Let the image select its Aikar garbage collection flags for this JVM and heap.", + "description": "Help text of configuration field USE_AIKAR_FLAGS (template.yaml spec.configSchema)" + }, + "config.USE_AIKAR_FLAGS.displayName": { + "defaultMessage": "Use Aikar''s flags", + "description": "Label of configuration field USE_AIKAR_FLAGS (template.yaml spec.configSchema)" + }, "config.VIEW_DISTANCE.displayName": { "defaultMessage": "View distance (chunks)", "description": "Label of configuration field VIEW_DISTANCE (template.yaml spec.configSchema)" diff --git a/minecraft-java/locales/fr.json b/minecraft-java/locales/fr.json index b384b82..c5ef1ae 100644 --- a/minecraft-java/locales/fr.json +++ b/minecraft-java/locales/fr.json @@ -256,13 +256,29 @@ "description": "Label of configuration field HARDCORE (template.yaml spec.configSchema)" }, "config.INIT_MEMORY.description": { - "defaultMessage": "Taille initiale du tas (-Xms), au maximum égale au maximum de la JVM. Laisser vide pour correspondre au tas maximum automatique (un tas fixe évite les redimensionnements du GC).", + "defaultMessage": "Taille initiale du tas, par exemple 512M ou 37.5%. Laisser vide pour utiliser 75 % de la limite mémoire du conteneur. La taille initiale doit être inférieure ou égale au maximum.", "description": "Help text of configuration field INIT_MEMORY (template.yaml spec.configSchema)" }, "config.INIT_MEMORY.displayName": { "defaultMessage": "Tas initial de la JVM", "description": "Label of configuration field INIT_MEMORY (template.yaml spec.configSchema)" }, + "config.JVM_OPTS.description": { + "defaultMessage": "Options Java séparées par des espaces, par exemple -Dfile.encoding=UTF-8.", + "description": "Help text of configuration field JVM_OPTS (template.yaml spec.configSchema)" + }, + "config.JVM_OPTS.displayName": { + "defaultMessage": "Options JVM supplémentaires", + "description": "Label of configuration field JVM_OPTS (template.yaml spec.configSchema)" + }, + "config.JVM_XX_OPTS.description": { + "defaultMessage": "Options de réglage de la JVM séparées par des espaces, par exemple -XX:+PrintCommandLineFlags.", + "description": "Help text of configuration field JVM_XX_OPTS (template.yaml spec.configSchema)" + }, + "config.JVM_XX_OPTS.displayName": { + "defaultMessage": "Options de réglage supplémentaires de la JVM", + "description": "Label of configuration field JVM_XX_OPTS (template.yaml spec.configSchema)" + }, "config.LEVEL_SEED.description": { "defaultMessage": "Laisser vide pour une graine aléatoire.", "description": "Help text of configuration field LEVEL_SEED (template.yaml spec.configSchema)" @@ -272,7 +288,7 @@ "description": "Label of configuration field LEVEL_SEED (template.yaml spec.configSchema)" }, "config.MAX_MEMORY.description": { - "defaultMessage": "Plafond du tas Java (-Xmx, ex. 2G, 3072M). Laisser vide pour le dimensionner automatiquement à 75 % de la limite de mémoire du conteneur : il suit alors la limite à chaque redimensionnement du serveur. Si vous le définissez vous-même, gardez environ 25 % de marge sous la limite : la JVM a besoin de place pour la mémoire hors tas, et dépasser la limite entraîne l’arrêt du serveur par OOM-kill.", + "defaultMessage": "Plafond du tas, par exemple 2G ou 65%. Laisser vide pour utiliser 75 % de la limite mémoire du conteneur. Prévoir de la mémoire en dehors du tas Java.", "description": "Help text of configuration field MAX_MEMORY (template.yaml spec.configSchema)" }, "config.MAX_MEMORY.displayName": { @@ -323,6 +339,14 @@ "defaultMessage": "PvP activé", "description": "Label of configuration field PVP (template.yaml spec.configSchema)" }, + "config.USE_AIKAR_FLAGS.description": { + "defaultMessage": "Laisser l’image choisir les options de ramasse-miettes d’Aikar adaptées à la JVM et au tas.", + "description": "Help text of configuration field USE_AIKAR_FLAGS (template.yaml spec.configSchema)" + }, + "config.USE_AIKAR_FLAGS.displayName": { + "defaultMessage": "Utiliser les options d’Aikar", + "description": "Label of configuration field USE_AIKAR_FLAGS (template.yaml spec.configSchema)" + }, "config.VIEW_DISTANCE.displayName": { "defaultMessage": "Distance d’affichage (chunks)", "description": "Label of configuration field VIEW_DISTANCE (template.yaml spec.configSchema)" diff --git a/minecraft-java/module.yaml b/minecraft-java/module.yaml index 8ed001c..966abb7 100644 --- a/minecraft-java/module.yaml +++ b/minecraft-java/module.yaml @@ -2,10 +2,10 @@ apiVersion: gameplane.local/module/v1 name: minecraft-java displayName: Minecraft (Java Edition) -version: 2.11.0 +version: 2.12.0 game: minecraft-java categories: [Sandbox, Survival, Building, Modded, Creative] summary: Vanilla / Paper / Spigot / Bukkit / Purpur / Forge / NeoForge / Fabric / Quilt servers (itzg/minecraft-server) with a version+loader picker spanning 1.16.5–latest, per-loader mod volumes, and in-app Modrinth browse. homepage: https://www.minecraft.net/ license: MIT -gameplaneMinVersion: 0.2.0-beta.7 +gameplaneMinVersion: 0.4.1 diff --git a/minecraft-java/template.yaml b/minecraft-java/template.yaml index a7f2b1d..438a15e 100644 --- a/minecraft-java/template.yaml +++ b/minecraft-java/template.yaml @@ -23,7 +23,7 @@ spec: displayName: Minecraft (Java Edition) game: minecraft-java # Kept in lockstep with module.yaml's bundle version from 2.5.0 onward. - version: 2.11.0 + version: 2.12.0 categories: [Sandbox, Survival, Building, Modded, Creative] # Brand color the dashboard tints this game's icon/accents with. accentColor: "#5b9a3e" @@ -828,31 +828,41 @@ spec: type: int default: "10" - # The itzg image passes these to the JVM as -Xms/-Xmx. Left empty, - # the operator sizes both to 75% of the container memory limit - # (autoFromMemoryLimit — needs gameplane ≥ 0.2.0-beta.6; the - # remaining 25% is headroom for non-heap JVM memory, which is what - # keeps the cgroup from OOM-killing the server). Equal -Xms/-Xmx - # gives a fixed heap, which game servers prefer over GC-driven - # resizes. - name: MAX_MEMORY displayName: JVM max heap description: >- - Java heap ceiling (-Xmx, e.g. 2G, 3072M). Leave empty to size it - automatically to 75% of the container memory limit — it then - tracks the limit whenever you resize the server. If you set it - yourself, keep roughly 25% below the limit: the JVM needs - headroom for non-heap memory, and exceeding the limit gets the - server OOM-killed. + Heap ceiling, for example 2G or 65%. Leave empty for 75% of the + container memory limit. Allow room for memory outside the Java heap. type: string + applyOnRestart: true autoFromMemoryLimit: percent: 75 - name: INIT_MEMORY displayName: JVM initial heap description: >- - Starting heap size (-Xms), at most JVM max heap. Leave empty to - match the automatic max heap (a fixed heap avoids GC resizes). + Starting heap, for example 512M or 37.5%. Leave empty for 75% of the + container memory limit. Keep the initial heap at or below the maximum. type: string + applyOnRestart: true autoFromMemoryLimit: percent: 75 + + - name: JVM_OPTS + displayName: Additional JVM options + description: Space-separated Java options, for example -Dfile.encoding=UTF-8. + type: string + applyOnRestart: true + + - name: JVM_XX_OPTS + displayName: Additional JVM tuning options + description: Space-separated JVM tuning options, for example -XX:+PrintCommandLineFlags. + type: string + applyOnRestart: true + + - name: USE_AIKAR_FLAGS + displayName: Use Aikar's flags + description: Let the image select its Aikar garbage collection flags for this JVM and heap. + type: bool + default: "false" + applyOnRestart: true diff --git a/test-minecraft-jvm.py b/test-minecraft-jvm.py new file mode 100644 index 0000000..371fa11 --- /dev/null +++ b/test-minecraft-jvm.py @@ -0,0 +1,54 @@ +"""Verify the Minecraft Java startup settings with their pinned image launcher.""" + +from pathlib import Path +import shlex +import subprocess +import unittest + +import yaml + + +class MinecraftStartup(unittest.TestCase): + @classmethod + def setUpClass(cls): + path = Path(__file__).parent / "minecraft-java/template.yaml" + template = yaml.safe_load(path.read_text())["spec"] + cls.image = template["image"] + cls.fields = {f["name"]: f for f in template["configSchema"]} + + def launch(self, **env): + args = ["docker", "run", "--rm", "--platform", "linux/amd64", "--network", "none", + "--tmpfs", "/data", "--entrypoint", "/image/scripts/start-finalExec"] + for name, value in {**env, "SETUP_ONLY": "true", "VERSION": "1.21.4", + "TYPE": "VANILLA", "SERVER": "server.jar"}.items(): + args.extend(("-e", f"{name}={value}")) + output = subprocess.check_output([*args, self.image], text=True, encoding="utf-8") + command = next(line for line in output.splitlines() if line.startswith("SETUP_ONLY: java ")) + return shlex.split(command) + + def test_automatic_and_explicit_settings(self): + for name in ["INIT_MEMORY", "MAX_MEMORY", "JVM_OPTS", "JVM_XX_OPTS", "USE_AIKAR_FLAGS"]: + self.assertTrue(self.fields[name]["applyOnRestart"]) + self.assertEqual(self.fields["USE_AIKAR_FLAGS"]["default"], "false") + # The operator sends whole MiB for each autoFromMemoryLimit field. + env = {} + for name in ["INIT_MEMORY", "MAX_MEMORY"]: + percent = self.fields[name]["autoFromMemoryLimit"]["percent"] + self.assertEqual(percent, 75) + env[name] = f"{4096 * percent // 100}M" + args = self.launch(**env) + self.assertIn("-Xms3072M", args) + self.assertIn("-Xmx3072M", args) + self.assertNotIn("-Daikars.new.flags=true", args) + args = self.launch(INIT_MEMORY="37.5%", MAX_MEMORY="2G", JVM_OPTS="-Dexample=true", + JVM_XX_OPTS="-XX:+PrintCommandLineFlags", USE_AIKAR_FLAGS="true") + for flag in ["-XX:InitialRAMPercentage=37.5", "-Xmx2G", "-Dexample=true", + "-XX:+PrintCommandLineFlags", "-Daikars.new.flags=true"]: + self.assertIn(flag, args) + args = self.launch(INIT_MEMORY="512M", MAX_MEMORY="75%") + self.assertIn("-Xms512M", args) + self.assertIn("-XX:MaxRAMPercentage=75", args) + + +if __name__ == "__main__": + unittest.main()