Skip to content
Open
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
5 changes: 5 additions & 0 deletions .github/workflows/verify-split-files.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down Expand Up @@ -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"
Expand Down Expand Up @@ -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

Expand Down
26 changes: 25 additions & 1 deletion docs/lib.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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 <unit>...` gives each systemd unit a drop-in,
`/etc/systemd/system/<unit>.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=<kB>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`,
Expand Down
1 change: 1 addition & 0 deletions lib/API.txt
Original file line number Diff line number Diff line change
Expand Up @@ -140,6 +140,7 @@ setup_gs
setup_hwaccel
setup_imagemagick
setup_java
setup_java_memory
setup_local_ip_helper
setup_mariadb
setup_mariadb_db
Expand Down
18 changes: 18 additions & 0 deletions lib/runtime.func
Original file line number Diff line number Diff line change
Expand Up @@ -1347,6 +1347,24 @@ setup_java() {
msg_ok "Setup Temurin JDK $JAVA_VERSION"
}

setup_java_memory() {
[[ -r /sys/fs/cgroup/memory.max && "$(</sys/fs/cgroup/memory.max)" == max ]] || return 0
grep -qs ' /proc/meminfo fuse\.lxcfs ' /proc/mounts || return 0
[[ -d /run/systemd/system ]] || return 0

local unit
for unit in "$@"; do
mkdir -p "/etc/systemd/system/${unit}.service.d"
cat <<'EOF' >"/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.
#
Expand Down
77 changes: 77 additions & 0 deletions tests/java-memory.sh
Original file line number Diff line number Diff line change
@@ -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))
Loading