fix(sentry): skip "daemon not running" stop/reload errors
This commit is contained in:
parent
fceadd2009
commit
4d7444ef5b
6 changed files with 90 additions and 12 deletions
|
|
@ -2,6 +2,8 @@ package agent
|
|||
|
||||
import (
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"time"
|
||||
|
|
@ -9,6 +11,11 @@ import (
|
|||
"github.com/torrentclaw/unarr/internal/config"
|
||||
)
|
||||
|
||||
// ErrDaemonNotRunning is returned by callers that need a running daemon but
|
||||
// find no state file on disk. Sentinel so user-facing commands (stop/reload)
|
||||
// can wrap it and Sentry can filter it out as a non-bug.
|
||||
var ErrDaemonNotRunning = errors.New("daemon does not appear to be running (state file not found)")
|
||||
|
||||
// DaemonState is written to disk every heartbeat for external tools to read.
|
||||
type DaemonState struct {
|
||||
AgentID string `json:"agentId"`
|
||||
|
|
@ -69,17 +76,31 @@ func WriteState(state *DaemonState) {
|
|||
os.Rename(tmp, path)
|
||||
}
|
||||
|
||||
// ReadState reads the daemon state from disk. Returns nil if not found.
|
||||
// ReadState reads the daemon state from disk. Returns nil if not found or
|
||||
// unreadable. Use LoadState when callers need to distinguish "not running"
|
||||
// from "state file corrupted".
|
||||
func ReadState() *DaemonState {
|
||||
state, _ := LoadState()
|
||||
return state
|
||||
}
|
||||
|
||||
// LoadState reads the daemon state and returns explicit errors:
|
||||
// - ErrDaemonNotRunning when the state file does not exist
|
||||
// - a wrapped json error when the file exists but cannot be decoded
|
||||
// (a real bug worth reporting to Sentry)
|
||||
func LoadState() (*DaemonState, error) {
|
||||
data, err := os.ReadFile(StateFilePath())
|
||||
if err != nil {
|
||||
return nil
|
||||
if errors.Is(err, os.ErrNotExist) {
|
||||
return nil, ErrDaemonNotRunning
|
||||
}
|
||||
return nil, err
|
||||
}
|
||||
var state DaemonState
|
||||
if json.Unmarshal(data, &state) != nil {
|
||||
return nil
|
||||
if err := json.Unmarshal(data, &state); err != nil {
|
||||
return nil, fmt.Errorf("decode daemon state %s: %w", StateFilePath(), err)
|
||||
}
|
||||
return &state
|
||||
return &state, nil
|
||||
}
|
||||
|
||||
// RemoveState deletes the state file (called on clean shutdown).
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
package agent
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"testing"
|
||||
|
|
@ -104,3 +105,39 @@ func TestReadStateCorruptedJSON(t *testing.T) {
|
|||
t.Errorf("ReadState() should return nil for corrupted JSON, got %+v", state)
|
||||
}
|
||||
}
|
||||
|
||||
func TestLoadStateNotFound(t *testing.T) {
|
||||
tmpDir := t.TempDir()
|
||||
origFn := stateFilePathFn
|
||||
stateFilePathFn = func() string { return filepath.Join(tmpDir, "nonexistent.json") }
|
||||
defer func() { stateFilePathFn = origFn }()
|
||||
|
||||
state, err := LoadState()
|
||||
if state != nil {
|
||||
t.Errorf("LoadState() state = %+v, want nil", state)
|
||||
}
|
||||
if !errors.Is(err, ErrDaemonNotRunning) {
|
||||
t.Errorf("LoadState() err = %v, want ErrDaemonNotRunning", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestLoadStateCorruptedJSON(t *testing.T) {
|
||||
tmpDir := t.TempDir()
|
||||
origFn := stateFilePathFn
|
||||
path := filepath.Join(tmpDir, "daemon.state.json")
|
||||
stateFilePathFn = func() string { return path }
|
||||
defer func() { stateFilePathFn = origFn }()
|
||||
|
||||
os.WriteFile(path, []byte("not valid json{{{"), 0o644)
|
||||
|
||||
state, err := LoadState()
|
||||
if state != nil {
|
||||
t.Errorf("LoadState() state = %+v, want nil", state)
|
||||
}
|
||||
if err == nil {
|
||||
t.Fatal("LoadState() err = nil, want decode error")
|
||||
}
|
||||
if errors.Is(err, ErrDaemonNotRunning) {
|
||||
t.Error("corrupt state must not be reported as ErrDaemonNotRunning — it would be filtered from Sentry")
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue