Files
pzmanager/internal/pzconfig/java_test.go
T
mrleo1nidandClaude Opus 5 e78a72d516 Память JVM задаётся раздельно, автоперезапуск можно отменить
Панель принудительно ставила -Xms равным -Xmx: куча не растёт на ходу, но
JVM обязана занять весь объём при старте. На машине с 5 ГБ ОЗУ сервер с
4g так вообще не поднимался — «Failed to allocate initial Java heap».

Теперь -Xmx задаётся как раньше, а -Xms стал отдельным полем профиля и по
умолчанию пуст: JVM берёт память по мере надобности. Пара значений
проверяется при сохранении профиля, а не при запуске сервера, — про
опечатку лучше узнать из формы, чем из лога упавшего сервера.

Там же в супервизоре: ожидание автоподъёма стало отдельным состоянием.
Раньше упавший сервер уходил в цикл «падение — пауза 10 секунд — падение»,
причём во время паузы он считался остановленным, и прервать это было
нечем. Теперь паузу видно в статусе с обратным отсчётом и её можно
отменить, а после трёх падений сразу после запуска подряд панель
перестаёт поднимать сервер сама: так проявляется нехватка памяти или
битый конфиг, и перезапуск тут не помогает.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-05 02:25:16 +03:00

168 lines
5.1 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
package pzconfig
import (
"encoding/json"
"os"
"path/filepath"
"testing"
)
// TestSetJavaMemoryKeepsOnlyXmx: -Xms равным -Xmx заставляет JVM занять весь
// объём при старте, и на машине с небольшой ОЗУ сервер падает с «Failed to
// allocate initial Java heap». Ставим только верхнюю границу.
func TestSetJavaMemoryKeepsOnlyXmx(t *testing.T) {
dir := t.TempDir()
writeLauncher(t, dir, map[string]any{
"mainClass": "zombie/network/GameServer",
"vmArgs": []any{
"-Djava.awt.headless=true",
"-XX:+UseZGC",
// Значения от прошлой установки должны замениться.
"-Xms8g",
"-Xmx8g",
},
})
if err := SetJavaMemory(dir, "3g", ""); err != nil {
t.Fatal(err)
}
args := launcherArgs(t, dir)
var xms, xmx int
for _, a := range args {
switch {
case len(a) > 4 && a[:4] == "-Xms":
xms++
t.Errorf("в аргументах остался %s — JVM займёт всю память при старте", a)
case a == "-Xmx3g":
xmx++
}
}
if xms != 0 {
t.Errorf("-Xms не должен добавляться, найдено вхождений: %d", xms)
}
if xmx != 1 {
t.Errorf("-Xmx3g встречается %d раз, ожидалось 1: %v", xmx, args)
}
// Чужие аргументы должны остаться нетронутыми.
if !contains(args, "-XX:+UseZGC") || !contains(args, "-Djava.awt.headless=true") {
t.Errorf("потеряны исходные аргументы: %v", args)
}
}
func TestSetJavaMemoryValidates(t *testing.T) {
dir := t.TempDir()
writeLauncher(t, dir, map[string]any{"vmArgs": []any{}})
for _, bad := range []string{"4", "4gb", "много", "-4g", "4g; rm -rf /"} {
if err := SetJavaMemory(dir, bad, ""); err == nil {
t.Errorf("значение %q принято, ожидалась ошибка", bad)
}
}
// Пустые значения — «не трогать», это не ошибка.
if err := SetJavaMemory(dir, "", ""); err != nil {
t.Errorf("пустое значение вернуло ошибку: %v", err)
}
for _, good := range []string{"512m", "4g", "8G", "16384M"} {
if err := SetJavaMemory(dir, good, ""); err != nil {
t.Errorf("значение %q отклонено: %v", good, err)
}
}
// JVM с -Xms больше -Xmx не стартует, поэтому ловим это заранее.
if err := SetJavaMemory(dir, "2g", "4g"); err == nil {
t.Error("начальный размер больше максимального принят, ожидалась ошибка")
}
if err := SetJavaMemory(dir, "4g", "2g"); err != nil {
t.Errorf("корректная пара 2g/4g отклонена: %v", err)
}
}
func writeLauncher(t *testing.T, dir string, doc map[string]any) {
t.Helper()
raw, err := json.MarshalIndent(doc, "", " ")
if err != nil {
t.Fatal(err)
}
if err := os.WriteFile(filepath.Join(dir, javaLauncherFile), raw, 0o644); err != nil {
t.Fatal(err)
}
}
func launcherArgs(t *testing.T, dir string) []string {
t.Helper()
raw, err := os.ReadFile(filepath.Join(dir, javaLauncherFile))
if err != nil {
t.Fatal(err)
}
var doc struct {
VMArgs []string `json:"vmArgs"`
}
if err := json.Unmarshal(raw, &doc); err != nil {
t.Fatal(err)
}
return doc.VMArgs
}
func contains(list []string, want string) bool {
for _, item := range list {
if item == want {
return true
}
}
return false
}
// TestSetJavaMemoryWritesXmsWhenAsked: минимальный размер кучи задаётся
// отдельно и попадает в файл, только если его действительно указали.
func TestSetJavaMemoryWritesXmsWhenAsked(t *testing.T) {
dir := t.TempDir()
writeLauncher(t, dir, map[string]any{"vmArgs": []any{"-XX:+UseZGC"}})
if err := SetJavaMemory(dir, "6g", "2g"); err != nil {
t.Fatal(err)
}
args := launcherArgs(t, dir)
if !contains(args, "-Xms2g") || !contains(args, "-Xmx6g") {
t.Fatalf("границы кучи не выставлены: %v", args)
}
// Убрали минимум — из файла он тоже должен уйти.
if err := SetJavaMemory(dir, "6g", ""); err != nil {
t.Fatal(err)
}
args = launcherArgs(t, dir)
for _, a := range args {
if len(a) > 4 && a[:4] == "-Xms" {
t.Errorf("после очистки минимума остался %s: %v", a, args)
}
}
if !contains(args, "-Xmx6g") {
t.Errorf("максимум потерян: %v", args)
}
}
func TestParseMemory(t *testing.T) {
cases := map[string]int64{
"": 0,
"512m": 512 * 1024 * 1024,
"4g": 4 * 1024 * 1024 * 1024,
"8G": 8 * 1024 * 1024 * 1024,
"1024k": 1024 * 1024,
}
for input, want := range cases {
got, err := ParseMemory(input)
if err != nil {
t.Errorf("ParseMemory(%q): %v", input, err)
continue
}
if got != want {
t.Errorf("ParseMemory(%q) = %d, ожидалось %d", input, got, want)
}
}
if _, err := ParseMemory("4гб"); err == nil {
t.Error("значение с кириллицей принято")
}
}