Память 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>
This commit is contained in:
+102
-17
@@ -23,10 +23,13 @@ import (
|
||||
type State string
|
||||
|
||||
const (
|
||||
StateStopped State = "stopped"
|
||||
StateStarting State = "starting"
|
||||
StateRunning State = "running"
|
||||
StateStopping State = "stopping"
|
||||
StateStopped State = "stopped"
|
||||
// StateRestarting — сервер упал и ждёт автоподъёма. Отдельное состояние
|
||||
// нужно, чтобы это ожидание можно было отменить кнопкой.
|
||||
StateRestarting State = "restarting"
|
||||
StateStarting State = "starting"
|
||||
StateRunning State = "running"
|
||||
StateStopping State = "stopping"
|
||||
)
|
||||
|
||||
// Ошибки, на которые HTTP-слой отвечает осмысленным кодом.
|
||||
@@ -42,20 +45,30 @@ const playersPollInterval = 30 * time.Second
|
||||
// restartDelay — пауза перед автоподъёмом упавшего сервера.
|
||||
const restartDelay = 10 * time.Second
|
||||
|
||||
// Сервер, падающий сразу после старта, автоперезапуском не лечится: обычно
|
||||
// это нехватка памяти или испорченный конфиг. После нескольких таких падений
|
||||
// подряд перестаём поднимать его сам, чтобы не крутить бесконечный цикл.
|
||||
const (
|
||||
quickCrashWindow = time.Minute
|
||||
maxQuickCrashes = 3
|
||||
)
|
||||
|
||||
// Status — снимок состояния для веб-панели.
|
||||
type Status struct {
|
||||
State State `json:"state"`
|
||||
// Profile и ProfileTitle описывают профиль, который сейчас работает
|
||||
// (а если сервер остановлен — который будет запущен).
|
||||
Profile string `json:"profile"`
|
||||
ProfileTitle string `json:"profile_title"`
|
||||
Installed bool `json:"installed"`
|
||||
PID int `json:"pid"`
|
||||
UptimeSec int64 `json:"uptime_sec"`
|
||||
Players []string `json:"players"`
|
||||
PlayersAt int64 `json:"players_at"`
|
||||
LastExit string `json:"last_exit"`
|
||||
AutoRestart bool `json:"auto_restart"`
|
||||
Profile string `json:"profile"`
|
||||
ProfileTitle string `json:"profile_title"`
|
||||
Installed bool `json:"installed"`
|
||||
PID int `json:"pid"`
|
||||
UptimeSec int64 `json:"uptime_sec"`
|
||||
Players []string `json:"players"`
|
||||
PlayersAt int64 `json:"players_at"`
|
||||
LastExit string `json:"last_exit"`
|
||||
AutoRestart bool `json:"auto_restart"`
|
||||
// RestartInSec — сколько секунд осталось до автоподъёма (0, если не ждём).
|
||||
RestartInSec int64 `json:"restart_in_sec"`
|
||||
Proc sysinfo.ProcStats `json:"proc"`
|
||||
Host sysinfo.HostStats `json:"host"`
|
||||
}
|
||||
@@ -89,6 +102,13 @@ type Supervisor struct {
|
||||
captured []string
|
||||
autoQuery bool
|
||||
|
||||
// restartCancel закрывается, чтобы отменить ожидание автоперезапуска;
|
||||
// restartAt — когда оно закончится.
|
||||
restartCancel chan struct{}
|
||||
restartAt time.Time
|
||||
// crashStreak — сколько раз подряд сервер упал сразу после запуска.
|
||||
crashStreak int
|
||||
|
||||
sampler *sysinfo.Sampler
|
||||
stopOnce sync.Once
|
||||
quit chan struct{}
|
||||
@@ -165,9 +185,14 @@ func (s *Supervisor) Status() Status {
|
||||
if s.cmd != nil && s.cmd.Process != nil {
|
||||
st.PID = s.cmd.Process.Pid
|
||||
}
|
||||
if !s.startedAt.IsZero() && s.state != StateStopped {
|
||||
if !s.startedAt.IsZero() && s.state != StateStopped && s.state != StateRestarting {
|
||||
st.UptimeSec = int64(time.Since(s.startedAt).Seconds())
|
||||
}
|
||||
if s.state == StateRestarting && !s.restartAt.IsZero() {
|
||||
if left := time.Until(s.restartAt).Seconds(); left > 0 {
|
||||
st.RestartInSec = int64(left + 0.5)
|
||||
}
|
||||
}
|
||||
s.mu.Unlock()
|
||||
|
||||
st.Installed = s.Installed()
|
||||
@@ -185,9 +210,28 @@ func (s *Supervisor) Status() Status {
|
||||
func (s *Supervisor) Start() error {
|
||||
s.mu.Lock()
|
||||
defer s.mu.Unlock()
|
||||
|
||||
// Запуск руками — знак, что причину падений устранили.
|
||||
s.crashStreak = 0
|
||||
// Если ждали автоподъёма, поднимаем немедленно.
|
||||
s.cancelRestartLocked()
|
||||
return s.startLocked()
|
||||
}
|
||||
|
||||
// cancelRestartLocked снимает запланированный автоперезапуск. Вызывается под
|
||||
// удержанным s.mu.
|
||||
func (s *Supervisor) cancelRestartLocked() {
|
||||
if s.restartCancel == nil {
|
||||
return
|
||||
}
|
||||
close(s.restartCancel)
|
||||
s.restartCancel = nil
|
||||
s.restartAt = time.Time{}
|
||||
if s.state == StateRestarting {
|
||||
s.state = StateStopped
|
||||
}
|
||||
}
|
||||
|
||||
func (s *Supervisor) startLocked() error {
|
||||
if s.state != StateStopped {
|
||||
return ErrAlreadyRunning
|
||||
@@ -204,8 +248,8 @@ func (s *Supervisor) startLocked() error {
|
||||
profile := s.cfg.ActiveProfile()
|
||||
// Лимит памяти живёт в файле запуска, общем для всех профилей, поэтому
|
||||
// выставляем его на каждом старте — иначе профиль унаследует чужой -Xmx.
|
||||
if err := pzconfig.SetJavaMemory(s.cfg.ServerDir, profile.JavaMemory); err != nil {
|
||||
s.AppendLog("manager", "Не удалось выставить лимит памяти JVM: "+err.Error())
|
||||
if err := pzconfig.SetJavaMemory(s.cfg.ServerDir, profile.JavaMemory, profile.JavaMemoryMin); err != nil {
|
||||
s.AppendLog("manager", "Не удалось выставить границы памяти JVM: "+err.Error())
|
||||
}
|
||||
|
||||
args := []string{"-servername", profile.ID}
|
||||
@@ -322,6 +366,8 @@ func (s *Supervisor) wait(cmd *exec.Cmd, exited chan struct{}) {
|
||||
err := cmd.Wait()
|
||||
close(exited)
|
||||
|
||||
lifetime := time.Since(s.startedAt)
|
||||
|
||||
s.mu.Lock()
|
||||
wasIntentional := s.intentionalStop
|
||||
s.state = StateStopped
|
||||
@@ -336,6 +382,15 @@ func (s *Supervisor) wait(cmd *exec.Cmd, exited chan struct{}) {
|
||||
s.lastExit = "штатное завершение"
|
||||
}
|
||||
reason := s.lastExit
|
||||
|
||||
// Падение сразу после запуска — признак того, что перезапуск не поможет:
|
||||
// не хватило памяти, испорчен конфиг, нет файлов сервера.
|
||||
if !wasIntentional && lifetime < quickCrashWindow {
|
||||
s.crashStreak++
|
||||
} else {
|
||||
s.crashStreak = 0
|
||||
}
|
||||
streak := s.crashStreak
|
||||
autoRestart := s.cfg.AutoRestart && !wasIntentional
|
||||
s.mu.Unlock()
|
||||
|
||||
@@ -343,14 +398,38 @@ func (s *Supervisor) wait(cmd *exec.Cmd, exited chan struct{}) {
|
||||
if !autoRestart {
|
||||
return
|
||||
}
|
||||
if streak >= maxQuickCrashes {
|
||||
s.AppendLog("manager", fmt.Sprintf(
|
||||
"Сервер падает сразу после запуска %d раза подряд — автоперезапуск остановлен. "+
|
||||
"Посмотрите ошибку выше и запустите сервер вручную.", streak))
|
||||
return
|
||||
}
|
||||
|
||||
s.mu.Lock()
|
||||
cancel := make(chan struct{})
|
||||
s.restartCancel = cancel
|
||||
s.restartAt = time.Now().Add(restartDelay)
|
||||
s.state = StateRestarting
|
||||
s.mu.Unlock()
|
||||
|
||||
s.AppendLog("manager", fmt.Sprintf("Автоперезапуск через %s (можно отменить кнопкой «Остановить»)", restartDelay))
|
||||
|
||||
s.AppendLog("manager", fmt.Sprintf("Автоперезапуск через %s", restartDelay))
|
||||
select {
|
||||
case <-time.After(restartDelay):
|
||||
case <-cancel:
|
||||
// Состояние уже переставил тот, кто отменил ожидание.
|
||||
s.AppendLog("manager", "Автоперезапуск отменён")
|
||||
return
|
||||
case <-s.quit:
|
||||
return
|
||||
}
|
||||
|
||||
s.mu.Lock()
|
||||
s.restartCancel = nil
|
||||
s.restartAt = time.Time{}
|
||||
if s.state == StateRestarting {
|
||||
s.state = StateStopped
|
||||
}
|
||||
err = s.startLocked()
|
||||
s.mu.Unlock()
|
||||
if err != nil && !errors.Is(err, ErrAlreadyRunning) {
|
||||
@@ -394,6 +473,12 @@ func (s *Supervisor) sendQuiet(command string) error {
|
||||
// мир), затем SIGTERM группе процессов и в крайнем случае SIGKILL.
|
||||
func (s *Supervisor) Stop() error {
|
||||
s.mu.Lock()
|
||||
// Ждём автоподъёма — «остановить» здесь значит «не поднимать».
|
||||
if s.state == StateRestarting {
|
||||
s.cancelRestartLocked()
|
||||
s.mu.Unlock()
|
||||
return nil
|
||||
}
|
||||
if s.state == StateStopped || s.cmd == nil {
|
||||
s.mu.Unlock()
|
||||
return ErrNotRunning
|
||||
|
||||
Reference in New Issue
Block a user