Bug 14: stream journal reads instead of buffering them; soft memory limit for the GC
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Fable 5.1
parent
03be65ee99
commit
c1805f6ad5
@@ -13,6 +13,7 @@ import (
|
|||||||
"os/user"
|
"os/user"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
"runtime"
|
"runtime"
|
||||||
|
"runtime/debug"
|
||||||
"strings"
|
"strings"
|
||||||
"syscall"
|
"syscall"
|
||||||
"time"
|
"time"
|
||||||
@@ -171,6 +172,10 @@ func serve(cfg *config.Config) error {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Keep the heap tidy: the daemon's live data is tens of MB; without a limit the GC
|
||||||
|
// happily lets RSS coast at twice that. (Soft limit: exceeding it only means more GC.)
|
||||||
|
debug.SetMemoryLimit(160 << 20)
|
||||||
|
|
||||||
col := metrics.NewCollector()
|
col := metrics.NewCollector()
|
||||||
go col.Run(context.Background())
|
go col.Run(context.Background())
|
||||||
|
|
||||||
|
|||||||
+46
-19
@@ -116,6 +116,35 @@ func run(ctx context.Context, stdin string, args ...string) (string, error) {
|
|||||||
return out.String(), nil
|
return out.String(), nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// runLines streams a command's stdout to fn line by line, so multi-megabyte journal
|
||||||
|
// dumps never sit in memory whole.
|
||||||
|
func runLines(ctx context.Context, fn func(line []byte), args ...string) error {
|
||||||
|
ctx, cancel := context.WithTimeout(ctx, 30*time.Second)
|
||||||
|
defer cancel()
|
||||||
|
cmd := exec.CommandContext(ctx, args[0], args[1:]...)
|
||||||
|
var errb bytes.Buffer
|
||||||
|
cmd.Stderr = &errb
|
||||||
|
stdout, err := cmd.StdoutPipe()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if err := cmd.Start(); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
sc := bufio.NewScanner(stdout)
|
||||||
|
sc.Buffer(make([]byte, 0, 64<<10), 4<<20)
|
||||||
|
for sc.Scan() {
|
||||||
|
fn(sc.Bytes())
|
||||||
|
}
|
||||||
|
if err := cmd.Wait(); err != nil {
|
||||||
|
if msg := strings.TrimSpace(errb.String()); msg != "" {
|
||||||
|
return errors.New(msg)
|
||||||
|
}
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
func (m *Manager) sudoArgs(args ...string) []string {
|
func (m *Manager) sudoArgs(args ...string) []string {
|
||||||
if m.sudo {
|
if m.sudo {
|
||||||
return append([]string{"sudo", "-n"}, args...)
|
return append([]string{"sudo", "-n"}, args...)
|
||||||
@@ -414,18 +443,15 @@ func (m *Manager) lastRunsFromJournal(ctx context.Context) map[string]int64 {
|
|||||||
if m.journalSudo {
|
if m.journalSudo {
|
||||||
args = append([]string{"sudo", "-n"}, args...)
|
args = append([]string{"sudo", "-n"}, args...)
|
||||||
}
|
}
|
||||||
out, _ := run(ctx, "", args...)
|
|
||||||
seen := m.logging
|
seen := m.logging
|
||||||
sc := bufio.NewScanner(strings.NewReader(out))
|
_ = runLines(ctx, func(line []byte) {
|
||||||
sc.Buffer(make([]byte, 0, 64<<10), 4<<20)
|
|
||||||
for sc.Scan() {
|
|
||||||
var raw struct {
|
var raw struct {
|
||||||
Msg any `json:"MESSAGE"`
|
Msg any `json:"MESSAGE"`
|
||||||
TS string `json:"__REALTIME_TIMESTAMP"`
|
TS string `json:"__REALTIME_TIMESTAMP"`
|
||||||
Cursor string `json:"__CURSOR"`
|
Cursor string `json:"__CURSOR"`
|
||||||
}
|
}
|
||||||
if json.Unmarshal(sc.Bytes(), &raw) != nil {
|
if json.Unmarshal(line, &raw) != nil {
|
||||||
continue
|
return
|
||||||
}
|
}
|
||||||
seen = true
|
seen = true
|
||||||
if raw.Cursor != "" {
|
if raw.Cursor != "" {
|
||||||
@@ -434,14 +460,14 @@ func (m *Manager) lastRunsFromJournal(ctx context.Context) map[string]int64 {
|
|||||||
msg, _ := raw.Msg.(string)
|
msg, _ := raw.Msg.(string)
|
||||||
mm := cronLogRe.FindStringSubmatch(msg)
|
mm := cronLogRe.FindStringSubmatch(msg)
|
||||||
if mm == nil {
|
if mm == nil {
|
||||||
continue
|
return
|
||||||
}
|
}
|
||||||
us, _ := strconv.ParseInt(raw.TS, 10, 64)
|
us, _ := strconv.ParseInt(raw.TS, 10, 64)
|
||||||
k := mm[1] + "\x00" + mm[2]
|
k := mm[1] + "\x00" + mm[2]
|
||||||
if us/1000 > res[k] {
|
if us/1000 > res[k] {
|
||||||
res[k] = us / 1000
|
res[k] = us / 1000
|
||||||
}
|
}
|
||||||
}
|
}, args...)
|
||||||
m.mu.Lock()
|
m.mu.Lock()
|
||||||
m.lastRuns, m.lastAt, m.lastCursor, m.logging = res, time.Now(), cursor, seen
|
m.lastRuns, m.lastAt, m.lastCursor, m.logging = res, time.Now(), cursor, seen
|
||||||
m.mu.Unlock()
|
m.mu.Unlock()
|
||||||
@@ -454,28 +480,29 @@ func (m *Manager) JobLogs(ctx context.Context, j Job, n int) ([]map[string]any,
|
|||||||
if m.journalSudo {
|
if m.journalSudo {
|
||||||
args = append([]string{"sudo", "-n"}, args...)
|
args = append([]string{"sudo", "-n"}, args...)
|
||||||
}
|
}
|
||||||
out, err := run(ctx, "", args...)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
var res []map[string]any
|
var res []map[string]any
|
||||||
sc := bufio.NewScanner(strings.NewReader(out))
|
needle := "(" + j.User + ") CMD (" + j.Command + ")"
|
||||||
sc.Buffer(make([]byte, 0, 64<<10), 4<<20)
|
err := runLines(ctx, func(line []byte) {
|
||||||
for sc.Scan() {
|
if !bytes.Contains(line, []byte(j.Command[:min(len(j.Command), 40)])) { // cheap pre-filter before JSON
|
||||||
|
return
|
||||||
|
}
|
||||||
var raw struct {
|
var raw struct {
|
||||||
Msg any `json:"MESSAGE"`
|
Msg any `json:"MESSAGE"`
|
||||||
TS string `json:"__REALTIME_TIMESTAMP"`
|
TS string `json:"__REALTIME_TIMESTAMP"`
|
||||||
PID string `json:"_PID"`
|
PID string `json:"_PID"`
|
||||||
}
|
}
|
||||||
if json.Unmarshal(sc.Bytes(), &raw) != nil {
|
if json.Unmarshal(line, &raw) != nil {
|
||||||
continue
|
return
|
||||||
}
|
}
|
||||||
msg, _ := raw.Msg.(string)
|
msg, _ := raw.Msg.(string)
|
||||||
if !strings.Contains(msg, "("+j.User+") CMD ("+j.Command+")") {
|
if !strings.Contains(msg, needle) {
|
||||||
continue
|
return
|
||||||
}
|
}
|
||||||
us, _ := strconv.ParseInt(raw.TS, 10, 64)
|
us, _ := strconv.ParseInt(raw.TS, 10, 64)
|
||||||
res = append(res, map[string]any{"t": us / 1000, "p": 6, "ident": "CRON", "pid": raw.PID, "msg": msg})
|
res = append(res, map[string]any{"t": us / 1000, "p": 6, "ident": "CRON", "pid": raw.PID, "msg": msg})
|
||||||
|
}, args...)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
}
|
}
|
||||||
if len(res) > n {
|
if len(res) > n {
|
||||||
res = res[len(res)-n:]
|
res = res[len(res)-n:]
|
||||||
|
|||||||
Reference in New Issue
Block a user