From e6914939697c81cb3b6e427e464a9af1686f029a Mon Sep 17 00:00:00 2001 From: MickLesk <47820557+MickLesk@users.noreply.github.com> Date: Thu, 29 Oct 2026 18:20:52 +0100 Subject: [PATCH] Add setup_java_memory: size Java services from the CT's RAM on Proxmox Proxmox puts a CT's memory limit above the CT's cgroup root, so the JVM sees memory.max=max and sizes its default heap from the host's RAM (12 GB in a 2 GB CT on a 48 GB host). setup_java_memory gives a systemd unit a drop-in that reads MemTotal from lxcfs on every start and passes -XX:MaxRAM via JAVA_TOOL_OPTIONS. It is a no-op where the limit is visible (Incus), without lxcfs, or without systemd. --- .github/workflows/verify-split-files.yml | 5 ++ docs/lib.md | 26 +++++++- lib/API.txt | 1 + lib/runtime.func | 18 ++++++ tests/java-memory.sh | 77 ++++++++++++++++++++++++ 5 files changed, 126 insertions(+), 1 deletion(-) create mode 100644 tests/java-memory.sh diff --git a/.github/workflows/verify-split-files.yml b/.github/workflows/verify-split-files.yml index e37088a..097edb2 100644 --- a/.github/workflows/verify-split-files.yml +++ b/.github/workflows/verify-split-files.yml @@ -28,6 +28,7 @@ on: - "tests/mongodb-upgrade.sh" - "tests/uv-required-version.sh" - "tests/docker-lxcfs.sh" + - "tests/java-memory.sh" - "tests/external-installer.sh" - "tests/forge-version-key.sh" - "tests/nodejs-key-refresh.sh" @@ -65,6 +66,7 @@ on: - "tests/mongodb-upgrade.sh" - "tests/uv-required-version.sh" - "tests/docker-lxcfs.sh" + - "tests/java-memory.sh" - "tests/external-installer.sh" - "tests/forge-version-key.sh" - "tests/nodejs-key-refresh.sh" @@ -122,6 +124,9 @@ jobs: - name: Docker lxcfs visibility regression tests run: bash tests/docker-lxcfs.sh + - name: Java memory drop-in tests + run: bash tests/java-memory.sh + - name: External installer confirmation tests run: bash tests/external-installer.sh diff --git a/docs/lib.md b/docs/lib.md index ab74c3a..2c9e133 100644 --- a/docs/lib.md +++ b/docs/lib.md @@ -58,7 +58,7 @@ migration depends on the old version. Language runtimes and the application-level installers: `setup_nodejs`, `setup_uv`, `setup_php`, `setup_go`, `setup_ruby`, `setup_rust`, -`setup_java`, `setup_dotnet`, `setup_composer`, `setup_ffmpeg`, +`setup_java`, `setup_java_memory`, `setup_dotnet`, `setup_composer`, `setup_ffmpeg`, `setup_imagemagick`, `setup_yq`, `setup_adminer`. ### Docker inside LXC @@ -102,6 +102,30 @@ inherited, and `/sys/devices/system/cpu/online` is bound for glibc's dashboard, Arcane's Docker info panel) are unaffected, and `/proc/stat` (CPU) is not cgroup-scoped. +### Java inside LXC + +Proxmox sets a CT's memory limit one cgroup above the CT's cgroup root, so +inside the CT `memory.max` reads `max`. The JVM then finds no container limit +and sizes its heap from the host's RAM: a 2 GB CT on a 48 GB host gets a 12 GB +default heap, and `-XX:MaxRAMPercentage=75` gives 36 GB. Incus exposes the limit +at the CT's cgroup root, where the JVM finds it on its own. + +`setup_java_memory ...` gives each systemd unit a drop-in, +`/etc/systemd/system/.service.d/java-memory.conf`. On every start it reads +`MemTotal` from `/proc/meminfo`, which lxcfs reports as the CT's limit, and +passes it as `JAVA_TOOL_OPTIONS=-XX:MaxRAM=k`. Call it after creating the +unit in the install script and again in `update_script` so existing installs +get it. + +- `MaxRAM` only replaces the RAM figure the JVM's defaults are computed from. + `-XX:MaxRAMPercentage`, `-Xmx` and an explicit `-XX:MaxRAM` on the command + line still apply and win over it. +- After `pct set --memory`, restarting the service is enough. +- It is a no-op unless `/sys/fs/cgroup/memory.max` reads `max`, lxcfs serves + `/proc/meminfo` and systemd runs, so it leaves Incus, VMs and Alpine alone. +- It owns `JAVA_TOOL_OPTIONS` for the unit: a value set with `Environment=` in + the unit is replaced. + ## [`db.func`](../lib/db.func) `setup_postgresql`, `setup_mysql`, `setup_mariadb`, `setup_mongodb`, diff --git a/lib/API.txt b/lib/API.txt index c297bdd..4f469a5 100644 --- a/lib/API.txt +++ b/lib/API.txt @@ -140,6 +140,7 @@ setup_gs setup_hwaccel setup_imagemagick setup_java +setup_java_memory setup_local_ip_helper setup_mariadb setup_mariadb_db diff --git a/lib/runtime.func b/lib/runtime.func index 2b0cf76..36a807e 100644 --- a/lib/runtime.func +++ b/lib/runtime.func @@ -1347,6 +1347,24 @@ setup_java() { msg_ok "Setup Temurin JDK $JAVA_VERSION" } +setup_java_memory() { + [[ -r /sys/fs/cgroup/memory.max && "$("/etc/systemd/system/${unit}.service.d/java-memory.conf" +[Service] +ExecStartPre=-+/bin/sh -c 'm=$$(sed -n "s/^MemTotal: *\([0-9]*\) kB/\1/p" /proc/meminfo); if [ -n "$$m" ]; then echo "JAVA_TOOL_OPTIONS=-XX:MaxRAM=$${m}k" >/run/%N-java-memory.env; else rm -f /run/%N-java-memory.env; fi' +EnvironmentFile=-/run/%N-java-memory.env +EOF + done + systemctl daemon-reload + msg_ok "Sized Java heap from container memory" +} + # ------------------------------------------------------------------------------ # Installs a local IP updater script using networkd-dispatcher. # diff --git a/tests/java-memory.sh b/tests/java-memory.sh new file mode 100644 index 0000000..45eee3c --- /dev/null +++ b/tests/java-memory.sh @@ -0,0 +1,77 @@ +#!/usr/bin/env bash +set -euo pipefail + +CORE_DIR="$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")/.." && pwd)" +TEST_DIR="$(mktemp -d)" +trap 'rm -rf -- "$TEST_DIR"' EXIT +passed=0 failed=0 + +eval "$(awk '/^setup_java_memory\(\) \{/,/^}/' "$CORE_DIR/lib/runtime.func" | + sed -e "s#/sys/fs/cgroup/memory.max#$TEST_DIR/memory.max#g" \ + -e "s#/proc/mounts#$TEST_DIR/mounts#g" \ + -e "s#/run/systemd/system#$TEST_DIR/systemd#g" \ + -e "s#/etc/systemd/system#$TEST_DIR/units#g")" + +msg_ok() { :; } +systemctl() { echo "$*" >>"$TEST_DIR/systemctl.log"; } + +check() { + if [[ "$3" == "$2" ]]; then + echo "PASS $1" + passed=$((passed + 1)) + else + echo "FAIL $1: expected <$2>, got <$3>" >&2 + failed=$((failed + 1)) + fi +} +run() { + rm -rf "$TEST_DIR/units" "$TEST_DIR/systemd" "$TEST_DIR/memory.max" "$TEST_DIR/systemctl.log" + [[ "$1" == - ]] || echo "$1" >"$TEST_DIR/memory.max" + echo "$2" >"$TEST_DIR/mounts" + [[ "$3" == no ]] || mkdir -p "$TEST_DIR/systemd" + shift 3 + setup_java_memory "$@" +} +dropin() { echo "$TEST_DIR/units/$1.service.d/java-memory.conf"; } +has_dropin() { [[ -f "$(dropin "$1")" ]] && echo yes || echo no; } +start_pre() { + local cmd + printf '%b' "$2" >"$TEST_DIR/meminfo" + cmd="$(sed -n 's/^ExecStartPre=-+//p' "$(dropin "$1")" | + sed -e 's/\$\$/$/g' -e "s#%N#$1#g" -e "s#/proc/meminfo#$TEST_DIR/meminfo#g" -e "s#/run/#$TEST_DIR/run/#g")" + mkdir -p "$TEST_DIR/run" + eval "$cmd" +} +env_file() { cat "$TEST_DIR/run/$1-java-memory.env" 2>/dev/null || echo none; } + +lxcfs='lxcfs /proc/meminfo fuse.lxcfs rw,nosuid,nodev,relatime,user_id=0,group_id=0,allow_other 0 0' + +run max "$lxcfs" yes stirlingpdf unoserver-java +check "pve: drop-in for first unit" "yes" "$(has_dropin stirlingpdf)" +check "pve: drop-in for second unit" "yes" "$(has_dropin unoserver-java)" +check "pve: one daemon-reload" "daemon-reload" "$(cat "$TEST_DIR/systemctl.log")" +check "pve: env file is optional" "EnvironmentFile=-/run/%N-java-memory.env" "$(grep '^EnvironmentFile=' "$(dropin stirlingpdf)")" +check "pve: start step runs as root, failure ignored" "yes" "$(grep -q '^ExecStartPre=-+/bin/sh -c ' "$(dropin stirlingpdf)" && echo yes)" +first="$(cat "$(dropin stirlingpdf)")" +setup_java_memory stirlingpdf +check "second run leaves the drop-in identical" "$first" "$(cat "$(dropin stirlingpdf)")" + +start_pre stirlingpdf 'MemTotal: 2097152 kB\nMemFree: 1048576 kB\n' +check "start: MaxRAM from MemTotal" "JAVA_TOOL_OPTIONS=-XX:MaxRAM=2097152k" "$(env_file stirlingpdf)" +start_pre stirlingpdf 'MemTotal: 3145728 kB\n' +check "start: follows a resized CT" "JAVA_TOOL_OPTIONS=-XX:MaxRAM=3145728k" "$(env_file stirlingpdf)" +start_pre stirlingpdf 'MemFree: 1048576 kB\n' +check "start: no MemTotal drops the stale env file" "none" "$(env_file stirlingpdf)" + +run 2147483648 "$lxcfs" yes stirlingpdf +check "incus: limit visible, untouched" "no" "$(has_dropin stirlingpdf)" +run - "$lxcfs" yes stirlingpdf +check "no cgroup v2 memory.max: untouched" "no" "$(has_dropin stirlingpdf)" +run max 'proc /proc proc rw 0 0' yes stirlingpdf +check "no lxcfs: untouched" "no" "$(has_dropin stirlingpdf)" +run max "$lxcfs" no stirlingpdf +check "no systemd: untouched" "no" "$(has_dropin stirlingpdf)" +check "no systemd: no daemon-reload" "no" "$([[ -s "$TEST_DIR/systemctl.log" ]] && echo yes || echo no)" + +printf '%s passed, %s failed\n' "$passed" "$failed" +((failed == 0))