eda1112f3b
llama-server aborts inside its own static teardown on SIGTERM — the handler calls exit(), stream_session_manager's destructor throws, and the process dies "signal: aborted (core dumped)". mavgpud sends that signal on every eviction, so a routine yield wrote a multi-gigabyte core into systemd-coredump and logged the same line a real crash would. LimitCORE=0 in the unit stops the disk cost. A yielding flag, set by stop and cleared by start, makes the log distinguish the two: only an exit we did not ask for is still reported as an exit. Not filed upstream. Searched ggml-org/llama.cpp for "ggml_uncaught_exception" with SIGTERM and for stream_session_manager and found nothing matching, so the issue still wants writing — by someone with an account on that tracker, which is why it is not in this commit.
60 lines
1.7 KiB
Go
60 lines
1.7 KiB
Go
package main
|
|
|
|
import (
|
|
"os"
|
|
"path/filepath"
|
|
"testing"
|
|
"time"
|
|
)
|
|
|
|
// fakeServer writes an executable standing in for llama-server: it ignores
|
|
// SIGTERM the way the real one effectively does — by dying messily rather than
|
|
// cleanly — and reports a non-zero status.
|
|
func fakeServer(t *testing.T, body string) string {
|
|
t.Helper()
|
|
path := filepath.Join(t.TempDir(), "fake-llama-server")
|
|
if err := os.WriteFile(path, []byte("#!/bin/sh\n"+body+"\n"), 0o755); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
return path
|
|
}
|
|
|
|
// A deliberate stop is a yield, and the log has to say so.
|
|
//
|
|
// llama-server aborts inside its own static teardown on SIGTERM, so the exit
|
|
// status of a routine yield is identical to that of a real crash. Reading the
|
|
// mavgpud log, the two were indistinguishable (Vikunja #491).
|
|
func TestStopMarksTheExitAsAYield(t *testing.T) {
|
|
r := newRunner(fakeServer(t, "while : ; do sleep 1 ; done"), nil, "")
|
|
if err := r.start(); err != nil {
|
|
t.Fatalf("start: %v", err)
|
|
}
|
|
r.mu.Lock()
|
|
if r.yielding {
|
|
t.Error("a freshly started server is already marked as yielding")
|
|
}
|
|
r.mu.Unlock()
|
|
|
|
r.stop(2 * time.Second)
|
|
deadline := time.Now().Add(2 * time.Second)
|
|
for time.Now().Before(deadline) {
|
|
if !r.running() {
|
|
return
|
|
}
|
|
time.Sleep(10 * time.Millisecond)
|
|
}
|
|
t.Fatal("the child outlived stop")
|
|
}
|
|
|
|
// Stopping when nothing is running must not arm the flag for the next child.
|
|
// The next exit after that would be a real crash logged as a yield.
|
|
func TestStopWithNoChildDoesNotArmTheFlag(t *testing.T) {
|
|
r := newRunner("/nonexistent", nil, "")
|
|
r.stop(10 * time.Millisecond)
|
|
r.mu.Lock()
|
|
defer r.mu.Unlock()
|
|
if r.yielding {
|
|
t.Error("stop armed the yield flag with no child running")
|
|
}
|
|
}
|