verstak/internal/core/plugins/scheduler.go

129 lines
2.5 KiB
Go

package plugins
import (
"fmt"
"log"
"sync"
"time"
)
// Task represents a single background task instance.
type Task struct {
ID string
Interval time.Duration
Script string // relative path to .lua file (or "hook:name" for a Lua function)
IsHook bool // if true, Script is a function name to call via CallHook
stopCh chan struct{}
stopped bool
}
// Scheduler manages background tasks for a plugin.
type Scheduler struct {
plugin *Plugin
vm *LuaVM
tasks []*Task
mu sync.Mutex
wg sync.WaitGroup
}
// NewScheduler creates a scheduler for a plugin.
func NewScheduler(p *Plugin, vm *LuaVM) *Scheduler {
return &Scheduler{
plugin: p,
vm: vm,
}
}
// AddTask adds a task from a BackgroundTask definition.
func (s *Scheduler) AddTask(bg BackgroundTask) error {
dur, err := parseDuration(bg.Interval)
if err != nil {
return fmt.Errorf("task %s: %w", bg.ID, err)
}
s.mu.Lock()
defer s.mu.Unlock()
task := &Task{
ID: bg.ID,
Interval: dur,
Script: bg.Script,
stopCh: make(chan struct{}),
}
s.tasks = append(s.tasks, task)
return nil
}
// Start begins all registered tasks.
func (s *Scheduler) Start() {
s.mu.Lock()
defer s.mu.Unlock()
for _, t := range s.tasks {
if t.stopped {
continue
}
s.wg.Add(1)
go s.runTask(t)
}
}
// Stop cancels all running tasks and waits for them to finish.
func (s *Scheduler) Stop() {
s.mu.Lock()
for _, t := range s.tasks {
if !t.stopped {
close(t.stopCh)
t.stopped = true
}
}
tasks := s.tasks
s.tasks = nil
s.mu.Unlock()
s.wg.Wait()
_ = tasks // keep reference until wg done
}
func (s *Scheduler) runTask(t *Task) {
defer s.wg.Done()
ticker := time.NewTicker(t.Interval)
defer ticker.Stop()
for {
select {
case <-ticker.C:
s.executeTask(t)
case <-t.stopCh:
return
}
}
}
func (s *Scheduler) executeTask(t *Task) {
if s.vm == nil {
return
}
if t.IsHook {
if err := s.vm.CallHook(t.Script); err != nil {
log.Printf("[plugins] task %s/%s hook error: %v", s.plugin.Meta.Name, t.ID, err)
}
return
}
if err := s.vm.LoadScript(t.Script); err != nil {
log.Printf("[plugins] task %s/%s script error: %v", s.plugin.Meta.Name, t.ID, err)
}
}
// parseDuration parses a human-readable interval like "5m", "1h", "30s".
func parseDuration(s string) (time.Duration, error) {
d, err := time.ParseDuration(s)
if err == nil {
return d, nil
}
// Try cron-like or other formats later
return 0, fmt.Errorf("invalid interval %q: use Go duration format (e.g. 5m, 1h, 30s)", s)
}