0.2.78
This commit is contained in:
@@ -17,6 +17,7 @@ import (
|
||||
"time"
|
||||
|
||||
"github.com/ponzischeme89/memby/server/internal/adminevents"
|
||||
"github.com/ponzischeme89/memby/server/internal/runtimestats"
|
||||
"github.com/ponzischeme89/memby/server/internal/store"
|
||||
)
|
||||
|
||||
@@ -31,6 +32,22 @@ const tick = 30 * time.Second
|
||||
// minutes announcing itself into the notification bell every ten minutes.
|
||||
type TaskFunc func(ctx context.Context) (detail string, err error)
|
||||
|
||||
// Outcome is the richer form: the sentence, plus what the run counted.
|
||||
//
|
||||
// It exists because "412 movies checked, 7 updated, 2 skipped" is the question an operator
|
||||
// has about an integration and a sentence is a poor place to keep numbers — they cannot be
|
||||
// compared between runs, sorted, or drawn as anything but prose. Most tasks count nothing
|
||||
// and keep the plain TaskFunc; only work that processes a batch has anything to say here.
|
||||
type Outcome struct {
|
||||
// Detail is the one line the console prints beside the run, and is empty when nothing
|
||||
// happened — the same rule TaskFunc's return follows, and for the same reason.
|
||||
Detail string
|
||||
store.RunCounts
|
||||
}
|
||||
|
||||
// WorkFunc is a task that counts what it did. A task declares Run or Work, never both.
|
||||
type WorkFunc func(ctx context.Context) (Outcome, error)
|
||||
|
||||
// Task is a declaration. Everything about it except the operator's overrides is code, so
|
||||
// the registry is readable as a list of what the gateway does in the background.
|
||||
type Task struct {
|
||||
@@ -45,7 +62,14 @@ type Task struct {
|
||||
// RunOnStart runs the task once shortly after boot regardless of when it last ran.
|
||||
// For jobs whose cost is trivial and whose value is highest immediately.
|
||||
RunOnStart bool
|
||||
Run TaskFunc
|
||||
// Integration names the external service this job belongs to, and is empty for the
|
||||
// gateway's own work. It is what lets the integrations area read its operational
|
||||
// history out of this registry rather than keeping a second one: a run is filed
|
||||
// against the service as well as against the job, and nothing else is needed.
|
||||
Integration string
|
||||
Run TaskFunc
|
||||
// Work is Run's counting form. Exactly one of the two must be set.
|
||||
Work WorkFunc
|
||||
}
|
||||
|
||||
// Status is one task as the console reads it: the declaration, the operator's overrides,
|
||||
@@ -55,6 +79,9 @@ type Status struct {
|
||||
Name string `json:"name"`
|
||||
Description string `json:"description"`
|
||||
Group string `json:"group"`
|
||||
// Integration is the external service this job belongs to, empty for the gateway's
|
||||
// own work. The integrations area lists a service's jobs by matching on it.
|
||||
Integration string `json:"integration,omitempty"`
|
||||
Interval int64 `json:"intervalSeconds"`
|
||||
// DefaultInterval is the cadence declared in code, which Interval hides whenever an
|
||||
// operator has overridden it. Both are sent because the console cannot otherwise tell
|
||||
@@ -132,8 +159,8 @@ func (s *Scheduler) Register(task Task) {
|
||||
if s == nil {
|
||||
return
|
||||
}
|
||||
if task.ID == "" || task.Run == nil {
|
||||
panic("scheduler: a task needs an id and a function")
|
||||
if task.ID == "" || (task.Run == nil) == (task.Work == nil) {
|
||||
panic("scheduler: a task needs an id and exactly one of Run or Work")
|
||||
}
|
||||
s.mu.Lock()
|
||||
defer s.mu.Unlock()
|
||||
@@ -166,7 +193,9 @@ func (s *Scheduler) Start(ctx context.Context) {
|
||||
s.mu.Unlock()
|
||||
|
||||
s.restore(ctx)
|
||||
go s.loop(ctx)
|
||||
// Named rather than launched bare, so the console can say this worker is running
|
||||
// without having to infer it from a stack. See internal/runtimestats.
|
||||
runtimestats.Go("Task scheduler", "Scheduled jobs", func() { s.loop(ctx) })
|
||||
}
|
||||
|
||||
func (s *Scheduler) restore(ctx context.Context) {
|
||||
@@ -320,7 +349,7 @@ func (s *Scheduler) execute(ctx context.Context, entry *registered, trigger stri
|
||||
|
||||
var runID int64
|
||||
if s.store != nil {
|
||||
id, err := s.store.BeginTaskRun(ctx, task.ID, trigger)
|
||||
id, err := s.store.BeginTaskRun(ctx, task.ID, task.Integration, trigger)
|
||||
if err != nil {
|
||||
s.log.Warn("could not open task run", "task", task.ID, "error", err)
|
||||
} else {
|
||||
@@ -329,9 +358,10 @@ func (s *Scheduler) execute(ctx context.Context, entry *registered, trigger stri
|
||||
}
|
||||
|
||||
runCtx, cancel := context.WithTimeout(ctx, task.Timeout)
|
||||
detail, err := s.safeRun(runCtx, task)
|
||||
outcome, err := s.safeRun(runCtx, task)
|
||||
cancel()
|
||||
elapsed := time.Since(started)
|
||||
detail := outcome.Detail
|
||||
|
||||
status := store.TaskSuccess
|
||||
failure := ""
|
||||
@@ -340,7 +370,7 @@ func (s *Scheduler) execute(ctx context.Context, entry *registered, trigger stri
|
||||
}
|
||||
if s.store != nil && runID != 0 {
|
||||
if closeErr := s.store.FinishTaskRun(
|
||||
context.WithoutCancel(ctx), runID, status, detail, failure,
|
||||
context.WithoutCancel(ctx), runID, status, detail, failure, outcome.RunCounts,
|
||||
); closeErr != nil {
|
||||
s.log.Warn("could not close task run", "task", task.ID, "error", closeErr)
|
||||
}
|
||||
@@ -349,9 +379,11 @@ func (s *Scheduler) execute(ctx context.Context, entry *registered, trigger stri
|
||||
finished := time.Now()
|
||||
entry.mu.Lock()
|
||||
entry.lastRun = &store.TaskRun{
|
||||
ID: runID, TaskID: task.ID, Trigger: trigger, Status: status,
|
||||
ID: runID, TaskID: task.ID, IntegrationID: task.Integration,
|
||||
Trigger: trigger, Status: status,
|
||||
StartedAt: started, FinishedAt: &finished,
|
||||
DurationMS: elapsed.Milliseconds(), Detail: detail, Error: failure,
|
||||
Counts: outcome.RunCounts,
|
||||
}
|
||||
entry.mu.Unlock()
|
||||
|
||||
@@ -362,13 +394,17 @@ func (s *Scheduler) execute(ctx context.Context, entry *registered, trigger stri
|
||||
// panic takes the whole process down for a reason nobody is watching for, and one
|
||||
// housekeeping job with a nil map must not be able to stop the gateway serving
|
||||
// television.
|
||||
func (s *Scheduler) safeRun(ctx context.Context, task Task) (detail string, err error) {
|
||||
func (s *Scheduler) safeRun(ctx context.Context, task Task) (outcome Outcome, err error) {
|
||||
defer func() {
|
||||
if recovered := recover(); recovered != nil {
|
||||
err = fmt.Errorf("task panicked: %v", recovered)
|
||||
}
|
||||
}()
|
||||
return task.Run(ctx)
|
||||
if task.Work != nil {
|
||||
return task.Work(ctx)
|
||||
}
|
||||
detail, err := task.Run(ctx)
|
||||
return Outcome{Detail: detail}, err
|
||||
}
|
||||
|
||||
// announce writes the log line and, when it is worth an operator's attention, publishes
|
||||
@@ -489,6 +525,7 @@ func (s *Scheduler) Snapshot() []Status {
|
||||
status := Status{
|
||||
ID: entry.task.ID, Name: entry.task.Name,
|
||||
Description: entry.task.Description, Group: entry.task.Group,
|
||||
Integration: entry.task.Integration,
|
||||
Interval: int64(entry.effectiveInterval() / time.Second),
|
||||
DefaultInterval: int64(entry.task.Interval / time.Second),
|
||||
Enabled: entry.enabled, Running: entry.running,
|
||||
|
||||
Reference in New Issue
Block a user