mirror of
https://github.com/J3vb/OwnCord.git
synced 2026-09-03 03:50:00 +03:00
* feat(server): supervisor detection and server.restart_mode config key RunningUnderSupervisor detects systemd (INVOCATION_ID) and, best-effort, NSSM (NSSM_SERVICE_NAME — 2.24 does not set it, so NSSM deployments set the mode explicitly). server.restart_mode (auto|spawn|supervised, default auto, env OWNCORD_SERVER_RESTART_MODE) selects how a self-restart hands off after the server drains: exit for the supervisor to relaunch, or spawn the replacement directly. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Ngzj2Rx9UGC35uLHAfErMp * fix(server): make the self-restart handoff drain fully before starting the successor The update/restore/wizard restart previously spawned the replacement while the old server was still serving, then SIGTERMed itself and hard-exited after 10s. That design failed in every documented deployment mode: under the shipped systemd unit the spawned child (same cgroup) was killed when the old main process exited and Restart=on-failure never relaunched a clean exit; on Windows the self-SIGTERM is unsupported and silently dropped, so graceful shutdown never ran — hub.GracefulStop (the only caller of LiveKitProcess.Stop) was skipped, orphaning livekit-server on TCP 7880/UDP 50000-60000 and dropping queued event/audit batches; and NSSM's relaunch raced the self-spawned replacement for the database lock. Admin handlers now perform only the on-disk swap and request a restart through an injected hook (admin.SetRestartHandoff). The main package's restart coordinator cancels the parent of run()'s signal.NotifyContext — the exact drain a SIGTERM triggers, on every platform — and after run() has fully torn down (listeners closed, hub and LiveKit stopped, queues flushed, DB closed and its lock released) main() performs the handoff: spawn the replacement in spawn mode, or exit 0 for the supervisor in supervised mode. A 90s backstop force-exits a wedged teardown; the DB-lock and bind retries demote to safety nets. A three-state guard (idle/busy/restart-pending) serializes update apply, backup restore, and setup-wizard restarts against each other: concurrent applies no longer race the same staged .new file or broadcast a spurious update_aborted, and conflicting requests get 409 UPDATE_IN_PROGRESS / RESTART_PENDING. The swap being free of process side effects also makes the apply success path unit-testable for the first time. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Ngzj2Rx9UGC35uLHAfErMp * fix(server): errno-based bind-conflict detection, ACME bind retry, LiveKit Pdeathsig isAddrInUse now unwraps to the platform errno (EADDRINUSE; WSAEADDRINUSE 10048 on Windows) with the English strings kept only as fallback — the string-only match never fired on localized Windows, silently disabling the bind retry. The retry loop is extracted into serveWithBindRetry and now also covers the ACME :80 challenge server, which previously gave up on first conflict and stayed dead (breaking HTTP-01 renewals) until the next restart. The .old-binary boot cleanup retries briefly for the window where a spawn-mode predecessor has not fully exited. The companion livekit-server gets Pdeathsig SIGKILL on Linux so a parent killed without teardown (kill -9, OOM, backstop exit) cannot orphan it with the voice ports held. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Ngzj2Rx9UGC35uLHAfErMp * docs(deploy): Restart=always unit and per-supervisor restart-mode guidance Restart=always is what lets the deliberate clean exit after a self-update/restore relaunch under systemd (systemctl stop is never auto-restarted; failure exits behave as before). Deployment docs gain the required NSSM AppEnvironmentExtra line, the Task Scheduler and Docker restart-policy notes, and the new drain-then-handoff update flow. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Ngzj2Rx9UGC35uLHAfErMp --------- Co-authored-by: Claude <noreply@anthropic.com>
52 lines
1.8 KiB
Go
52 lines
1.8 KiB
Go
package db
|
|
|
|
import (
|
|
"errors"
|
|
"log/slog"
|
|
"time"
|
|
)
|
|
|
|
// errAlreadyLocked reports that another live process holds the database's
|
|
// single-process lock. The locks used here (flock on Unix, an exclusive file
|
|
// handle on Windows) are released by the OS when their holder exits, so a
|
|
// held lock always means a running process, never a stale file.
|
|
var errAlreadyLocked = errors.New("database lock held by another process")
|
|
|
|
// lockFilePath is the sidecar lock file next to the SQLite database.
|
|
func lockFilePath(dbPath string) string { return dbPath + ".lock" }
|
|
|
|
// acquireProcessLock takes the single-process lock for dbPath, retrying for
|
|
// a bounded window before giving up with errAlreadyLocked.
|
|
//
|
|
// The restart handoff no longer overlaps by design — the old process closes
|
|
// the database (releasing this lock) and exits before its replacement is
|
|
// started, in both spawn and supervised restart modes (Server/restart.go).
|
|
// The retry survives as a safety net for the cases that can still race: a
|
|
// supervisor relaunching the service while a wedged predecessor is being
|
|
// backstop-killed, and the final old-style update from a release that still
|
|
// spawned mid-drain. A genuinely concurrent long-lived second process still
|
|
// fails, just after the wait.
|
|
func acquireProcessLock(dbPath string) (release func(), err error) {
|
|
const (
|
|
retryFor = 30 * time.Second
|
|
retryEvery = 500 * time.Millisecond
|
|
)
|
|
deadline := time.Now().Add(retryFor)
|
|
logged := false
|
|
for {
|
|
release, err = tryLockFile(lockFilePath(dbPath))
|
|
if err == nil || !errors.Is(err, errAlreadyLocked) {
|
|
return release, err
|
|
}
|
|
if time.Now().After(deadline) {
|
|
return nil, err
|
|
}
|
|
if !logged {
|
|
slog.Info("db: database is locked by another process; waiting for it to exit (restart handoff)",
|
|
"path", dbPath, "wait_up_to", retryFor.String())
|
|
logged = true
|
|
}
|
|
time.Sleep(retryEvery)
|
|
}
|
|
}
|