This commit is contained in:
Yvan
2026-08-18 15:04:07 +08:00
parent 10ccc005be
commit 1ef8ecb7b2
42 changed files with 3027 additions and 296 deletions
+13 -7
View File
@@ -103,12 +103,12 @@ func truncateTaskText(value string) string {
return value[:limit] + "...(截断)"
}
func (e *TaskExecutor) runMethod(task *biz.TimedTask) error {
func (e *TaskExecutor) runMethod(ctx context.Context, task *biz.TimedTask) error {
method, ok := biz.TaskMethodByName(task.MethodName)
if !ok {
return fmt.Errorf("方法 %s 未注册(需在 internal/worker/task_registry.go 经 biz.RegisterTaskMethod 注册)", task.MethodName)
}
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Minute)
runCtx, cancel := context.WithTimeout(ctx, 5*time.Minute)
defer cancel()
done := make(chan error, 1)
go func() {
@@ -117,7 +117,7 @@ func (e *TaskExecutor) runMethod(task *biz.TimedTask) error {
done <- fmt.Errorf("panic: %v", recovered)
}
}()
done <- method(ctx, json.RawMessage(task.Params))
done <- method(runCtx, json.RawMessage(task.Params))
}()
select {
case err := <-done:
@@ -125,12 +125,18 @@ func (e *TaskExecutor) runMethod(task *biz.TimedTask) error {
return errTaskTimeout
}
return err
case <-ctx.Done():
return errTaskTimeout
case <-runCtx.Done():
if errors.Is(runCtx.Err(), context.DeadlineExceeded) {
return errTaskTimeout
}
return runCtx.Err()
}
}
func (e *TaskExecutor) Run(ctx context.Context, task *biz.TimedTask, trigger string) (log *biz.TimedTaskLog) {
if ctx == nil {
ctx = context.Background()
}
started := time.Now()
log = &biz.TimedTaskLog{TaskID: task.ID, TaskName: task.Name, TriggerType: trigger, StartedAt: started, Status: "success"}
defer func() {
@@ -148,9 +154,9 @@ func (e *TaskExecutor) Run(ctx context.Context, task *biz.TimedTask, trigger str
var err error
switch task.ExecutorType {
case biz.TaskExecutorMethod:
err = e.runMethod(task)
err = e.runMethod(ctx, task)
case biz.TaskExecutorHTTP:
runCtx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
runCtx, cancel := context.WithTimeout(ctx, 30*time.Second)
log.Output, err = e.runHTTP(runCtx, task)
cancel()
default:
+80
View File
@@ -1,8 +1,15 @@
package worker
import (
"context"
"encoding/json"
"errors"
"net"
"strings"
"testing"
"time"
"kra/internal/biz"
)
func TestPrivateIP(t *testing.T) {
@@ -15,3 +22,76 @@ func TestPrivateIP(t *testing.T) {
t.Fatal("public IP was classified as private")
}
}
func TestRunMethodReportsParentDeadlineAsTimeout(t *testing.T) {
const methodName = "worker-test-parent-deadline"
biz.RegisterTaskMethod(methodName, "test", func(ctx context.Context, _ json.RawMessage) error {
<-ctx.Done()
return ctx.Err()
})
executor := &TaskExecutor{}
ctx, cancel := context.WithTimeout(context.Background(), 20*time.Millisecond)
defer cancel()
if err := executor.runMethod(ctx, &biz.TimedTask{MethodName: methodName}); !errors.Is(err, errTaskTimeout) {
t.Fatalf("runMethod() error = %v, want errTaskTimeout", err)
}
}
func TestRunMethodConvertsPanicToError(t *testing.T) {
const methodName = "worker-test-panic"
biz.RegisterTaskMethod(methodName, "test", func(context.Context, json.RawMessage) error {
panic("boom")
})
executor := &TaskExecutor{}
err := executor.runMethod(context.Background(), &biz.TimedTask{MethodName: methodName})
if err == nil || !strings.Contains(err.Error(), "panic: boom") {
t.Fatalf("runMethod() error = %v, want recovered panic", err)
}
}
func TestRunMethodUsesParentContext(t *testing.T) {
const methodName = "worker-test-parent-context"
started := make(chan struct{})
biz.RegisterTaskMethod(methodName, "test", func(ctx context.Context, _ json.RawMessage) error {
close(started)
<-ctx.Done()
return ctx.Err()
})
executor := &TaskExecutor{}
ctx, cancel := context.WithCancel(context.Background())
done := make(chan error, 1)
go func() {
done <- executor.runMethod(ctx, &biz.TimedTask{MethodName: methodName})
}()
select {
case <-started:
case <-time.After(time.Second):
t.Fatal("registered method did not start")
}
cancel()
select {
case err := <-done:
if !errors.Is(err, context.Canceled) {
t.Fatalf("runMethod() error = %v, want context.Canceled", err)
}
case <-time.After(time.Second):
t.Fatal("runMethod() did not stop after parent cancellation")
}
}
func TestRunMethodHonorsDeadlineWhenMethodIgnoresContext(t *testing.T) {
const methodName = "worker-test-ignores-context"
release := make(chan struct{})
biz.RegisterTaskMethod(methodName, "test", func(context.Context, json.RawMessage) error {
<-release
return nil
})
executor := &TaskExecutor{}
ctx, cancel := context.WithTimeout(context.Background(), 20*time.Millisecond)
defer cancel()
if err := executor.runMethod(ctx, &biz.TimedTask{MethodName: methodName}); !errors.Is(err, errTaskTimeout) {
close(release)
t.Fatalf("runMethod() error = %v, want errTaskTimeout", err)
}
close(release)
}
+274
View File
@@ -0,0 +1,274 @@
package worker
import (
"context"
"encoding/json"
"errors"
"io"
"log/slog"
"sync"
"sync/atomic"
"testing"
"time"
"kra/internal/biz"
)
type workerTaskRepo struct {
mu sync.Mutex
items []*biz.TimedTask
listErr error
logs chan *biz.TimedTaskLog
}
func (r *workerTaskRepo) CreateTask(context.Context, *biz.TimedTask) error { return nil }
func (r *workerTaskRepo) UpdateTask(context.Context, *biz.TimedTask) error { return nil }
func (r *workerTaskRepo) DeleteTask(context.Context, uint) error { return nil }
func (r *workerTaskRepo) ToggleTask(context.Context, uint, bool) error { return nil }
func (r *workerTaskRepo) CleanupLogs(context.Context) error { return nil }
func (r *workerTaskRepo) TaskNameExists(context.Context, string, uint) (bool, error) {
return false, nil
}
func (r *workerTaskRepo) FindTask(_ context.Context, id uint) (*biz.TimedTask, error) {
r.mu.Lock()
defer r.mu.Unlock()
for _, item := range r.items {
if item.ID == id {
return cloneTimedTask(item), nil
}
}
return nil, errors.New("task not found")
}
func (r *workerTaskRepo) ListTasks(context.Context, int, int, *biz.TimedTask) ([]*biz.TimedTask, int64, error) {
r.mu.Lock()
defer r.mu.Unlock()
if r.listErr != nil {
return nil, 0, r.listErr
}
items := make([]*biz.TimedTask, 0, len(r.items))
for _, item := range r.items {
items = append(items, cloneTimedTask(item))
}
return items, int64(len(items)), nil
}
func (r *workerTaskRepo) RecordTaskLog(_ context.Context, value *biz.TimedTaskLog) error {
if r.logs != nil {
copy := *value
r.logs <- &copy
}
return nil
}
func (r *workerTaskRepo) ListTaskLogs(context.Context, int, int, uint, string) ([]*biz.TimedTaskLog, int64, error) {
return nil, 0, nil
}
func newTestTaskScheduler(repo *workerTaskRepo) *TaskScheduler {
tasks := biz.NewTaskUsecase(repo)
executor := &TaskExecutor{tasks: tasks}
logger := slog.New(slog.NewTextHandler(io.Discard, nil))
return NewTaskScheduler(tasks, nil, executor, logger)
}
func TestReloadPreservesExistingScheduleOnPreparationFailure(t *testing.T) {
repo := &workerTaskRepo{}
scheduler := newTestTaskScheduler(repo)
old := &biz.TimedTask{ID: 1, Name: "old", Spec: "0 0 * * *", Enabled: true}
if err := scheduler.Schedule(old); err != nil {
t.Fatalf("schedule old task: %v", err)
}
t.Run("query failure", func(t *testing.T) {
repo.mu.Lock()
repo.listErr = errors.New("database unavailable")
repo.mu.Unlock()
if err := scheduler.Reload(context.Background()); err == nil {
t.Fatal("Reload() error = nil, want query error")
}
assertOnlyScheduledTask(t, scheduler, old.ID)
})
t.Run("invalid cron", func(t *testing.T) {
repo.mu.Lock()
repo.listErr = nil
repo.items = []*biz.TimedTask{{ID: 2, Name: "invalid", Spec: "bad cron", Enabled: true}}
repo.mu.Unlock()
if err := scheduler.Reload(context.Background()); err == nil {
t.Fatal("Reload() error = nil, want cron error")
}
assertOnlyScheduledTask(t, scheduler, old.ID)
})
}
func assertOnlyScheduledTask(t *testing.T, scheduler *TaskScheduler, id uint) {
t.Helper()
scheduler.mu.Lock()
defer scheduler.mu.Unlock()
if len(scheduler.entries) != 1 {
t.Fatalf("scheduled task count = %d, want 1", len(scheduler.entries))
}
if _, ok := scheduler.entries[id]; !ok {
t.Fatalf("scheduled task %d was replaced after failed reload", id)
}
if got := len(scheduler.standard.Entries()) + len(scheduler.seconds.Entries()); got != 1 {
t.Fatalf("cron entry count = %d, want 1", got)
}
}
func TestConcurrentScheduleKeepsSingleEntry(t *testing.T) {
scheduler := newTestTaskScheduler(&workerTaskRepo{})
const workers = 32
var wg sync.WaitGroup
for i := 0; i < workers; i++ {
wg.Add(1)
go func() {
defer wg.Done()
if err := scheduler.Schedule(&biz.TimedTask{ID: 7, Name: "same", Spec: "0 0 * * *", Enabled: true}); err != nil {
t.Errorf("Schedule() error = %v", err)
}
}()
}
wg.Wait()
assertOnlyScheduledTask(t, scheduler, 7)
}
func TestManualTriggerSkipsOverlappingExecution(t *testing.T) {
const methodName = "worker-test-no-overlap"
started := make(chan struct{}, 2)
release := make(chan struct{})
var calls atomic.Int32
biz.RegisterTaskMethod(methodName, "test", func(context.Context, json.RawMessage) error {
calls.Add(1)
started <- struct{}{}
<-release
return nil
})
task := &biz.TimedTask{ID: 9, Name: "single", ExecutorType: biz.TaskExecutorMethod, MethodName: methodName}
repo := &workerTaskRepo{items: []*biz.TimedTask{task}, logs: make(chan *biz.TimedTaskLog, 2)}
scheduler := newTestTaskScheduler(repo)
if err := scheduler.TriggerID(context.Background(), task.ID); err != nil {
t.Fatalf("first manual trigger failed: %v", err)
}
select {
case <-started:
case <-time.After(time.Second):
t.Fatal("first task execution did not start")
}
if err := scheduler.TriggerID(context.Background(), task.ID); err == nil {
t.Fatal("overlapping manual trigger returned success")
}
if got := calls.Load(); got != 1 {
t.Fatalf("execution count while first run is active = %d, want 1", got)
}
close(release)
select {
case <-repo.logs:
case <-time.After(time.Second):
t.Fatal("first task execution did not finish")
}
waitForSchedulerIdle(t, scheduler)
if err := scheduler.TriggerID(context.Background(), task.ID); err != nil {
t.Fatalf("manual trigger failed after the previous run finished: %v", err)
}
select {
case <-started:
case <-time.After(time.Second):
t.Fatal("task could not run again after the first execution finished")
}
select {
case <-repo.logs:
case <-time.After(time.Second):
t.Fatal("second task execution did not finish")
}
waitForSchedulerIdle(t, scheduler)
if got := calls.Load(); got != 2 {
t.Fatalf("total execution count = %d, want 2", got)
}
}
func TestAutomaticAndManualTriggersShareOverlapGate(t *testing.T) {
const methodName = "worker-test-auto-manual-overlap"
started := make(chan struct{}, 1)
release := make(chan struct{})
var calls atomic.Int32
biz.RegisterTaskMethod(methodName, "test", func(context.Context, json.RawMessage) error {
calls.Add(1)
started <- struct{}{}
<-release
return nil
})
task := &biz.TimedTask{ID: 10, Name: "shared-gate", ExecutorType: biz.TaskExecutorMethod, MethodName: methodName}
repo := &workerTaskRepo{logs: make(chan *biz.TimedTaskLog, 1)}
scheduler := newTestTaskScheduler(repo)
if !scheduler.dispatch(task, "auto", true) {
t.Fatal("automatic dispatch was rejected while idle")
}
select {
case <-started:
case <-time.After(time.Second):
t.Fatal("automatic task execution did not start")
}
if scheduler.Trigger(task) {
t.Fatal("manual trigger bypassed an active automatic execution")
}
if got := calls.Load(); got != 1 {
t.Fatalf("execution count while automatic run is active = %d, want 1", got)
}
close(release)
select {
case <-repo.logs:
case <-time.After(time.Second):
t.Fatal("automatic task execution did not finish")
}
waitForSchedulerIdle(t, scheduler)
}
func waitForSchedulerIdle(t *testing.T, scheduler *TaskScheduler) {
t.Helper()
scheduler.runMu.Lock()
idle := scheduler.idle
scheduler.runMu.Unlock()
select {
case <-idle:
case <-time.After(time.Second):
t.Fatal("scheduler did not become idle")
}
}
func TestStopCancelsAndWaitsForActiveRuns(t *testing.T) {
scheduler := newTestTaskScheduler(&workerTaskRepo{})
runCtx, cancel := context.WithCancel(context.Background())
scheduler.ctxMu.Lock()
scheduler.runContext = runCtx
scheduler.cancel = cancel
scheduler.ctxMu.Unlock()
taskCtx, ok := scheduler.beginRun(12)
if !ok {
t.Fatal("beginRun() rejected an idle scheduler")
}
finished := make(chan struct{})
go func() {
<-taskCtx.Done()
time.Sleep(20 * time.Millisecond)
close(finished)
scheduler.finishRun(12)
}()
stopCtx, stopCancel := context.WithTimeout(context.Background(), time.Second)
defer stopCancel()
if err := scheduler.Stop(stopCtx); err != nil {
t.Fatalf("Stop() error = %v", err)
}
select {
case <-finished:
default:
t.Fatal("Stop() returned before the active run finished")
}
if _, ok := scheduler.beginRun(13); ok {
t.Fatal("scheduler accepted a new run after Stop()")
}
}
+172 -64
View File
@@ -3,6 +3,7 @@ package worker
import (
"context"
"encoding/json"
"errors"
"fmt"
"log/slog"
"sync"
@@ -24,6 +25,10 @@ type TaskScheduler struct {
ctxMu sync.RWMutex
runContext context.Context
cancel context.CancelFunc
runMu sync.Mutex
running map[uint]struct{}
idle chan struct{}
stopping bool
subMu sync.RWMutex
subscribers map[uint]map[chan []byte]struct{}
}
@@ -34,7 +39,9 @@ type scheduledEntry struct {
}
func NewTaskScheduler(tasks *biz.TaskUsecase, authorities *biz.AuthorityUsecase, executor *TaskExecutor, logger *slog.Logger) *TaskScheduler {
return &TaskScheduler{tasks: tasks, authorities: authorities, executor: executor, logger: logger.With("mod", "timedTask"), standard: cron.New(), seconds: cron.New(cron.WithSeconds()), entries: map[uint]scheduledEntry{}, subscribers: map[uint]map[chan []byte]struct{}{}}
idle := make(chan struct{})
close(idle)
return &TaskScheduler{tasks: tasks, authorities: authorities, executor: executor, logger: logger.With("mod", "timedTask"), standard: cron.New(), seconds: cron.New(cron.WithSeconds()), entries: map[uint]scheduledEntry{}, running: map[uint]struct{}{}, idle: idle, subscribers: map[uint]map[chan []byte]struct{}{}}
}
func NewTaskRuntime(scheduler *TaskScheduler) biz.TaskRuntime { return scheduler }
@@ -44,21 +51,12 @@ func (s *TaskScheduler) Start(ctx context.Context) error {
s.ctxMu.Lock()
s.runContext, s.cancel = runContext, cancel
s.ctxMu.Unlock()
s.runMu.Lock()
s.stopping = false
s.runMu.Unlock()
s.standard.Start()
s.seconds.Start()
items, _, err := s.tasks.ListTasks(ctx, 0, 0, nil)
if err == nil {
for _, task := range items {
if task.Enabled {
if scheduleErr := s.Schedule(task); scheduleErr != nil {
s.logger.ErrorContext(ctx, "restore timed task failed", "id", task.ID, "error", scheduleErr)
}
}
}
// Report the number of rows read from the database, not only
// the enabled rows that were scheduled successfully.
s.logger.InfoContext(ctx, "定时任务加载完成", "task_count", len(items))
} else {
if err := s.Reload(ctx); err != nil {
s.logger.WarnContext(ctx, "timed task table is not ready", "error", err)
}
<-ctx.Done()
@@ -67,28 +65,38 @@ func (s *TaskScheduler) Start(ctx context.Context) error {
func (s *TaskScheduler) Stop(ctx context.Context) error {
s.closeSubscribers()
s.runMu.Lock()
s.stopping = true
idle := s.idle
if idle == nil {
idle = make(chan struct{})
close(idle)
}
s.runMu.Unlock()
s.ctxMu.Lock()
if s.cancel != nil {
s.cancel()
}
cancel := s.cancel
s.ctxMu.Unlock()
if cancel != nil {
cancel()
}
standardDone, secondsDone := s.standard.Stop().Done(), s.seconds.Stop().Done()
select {
case <-standardDone:
case <-ctx.Done():
return ctx.Err()
}
select {
case <-secondsDone:
return nil
case <-ctx.Done():
return ctx.Err()
for _, done := range []<-chan struct{}{standardDone, secondsDone, idle} {
select {
case <-done:
case <-ctx.Done():
return ctx.Err()
}
}
return nil
}
func (s *TaskScheduler) Remove(id uint) {
s.mu.Lock()
defer s.mu.Unlock()
s.removeLocked(id)
}
func (s *TaskScheduler) removeLocked(id uint) {
if old, ok := s.entries[id]; ok {
if old.seconds {
s.seconds.Remove(old.entry)
@@ -101,26 +109,33 @@ func (s *TaskScheduler) Remove(id uint) {
func (s *TaskScheduler) Reload(ctx context.Context) error {
s.mu.Lock()
for id, old := range s.entries {
if old.seconds {
s.seconds.Remove(old.entry)
} else {
s.standard.Remove(old.entry)
}
delete(s.entries, id)
}
s.mu.Unlock()
defer s.mu.Unlock()
items, _, err := s.tasks.ListTasks(ctx, 0, 0, nil)
if err != nil {
return err
}
type preparedTask struct {
task *biz.TimedTask
schedule cron.Schedule
}
prepared := make([]preparedTask, 0, len(items))
for _, task := range items {
if task.Enabled {
if err = s.Schedule(task); err != nil {
return err
schedule, parseErr := parseTaskSchedule(task)
if parseErr != nil {
return fmt.Errorf("定时任务 %d 的 cron 表达式非法: %w", task.ID, parseErr)
}
prepared = append(prepared, preparedTask{task: cloneTimedTask(task), schedule: schedule})
}
}
for id := range s.entries {
s.removeLocked(id)
}
for _, item := range prepared {
s.scheduleLocked(item.task, item.schedule)
}
s.logger.InfoContext(ctx, "定时任务重载完成", "task_count", len(items))
return nil
}
@@ -134,8 +149,58 @@ func (s *TaskScheduler) executionContext() context.Context {
return context.Background()
}
func (s *TaskScheduler) run(task *biz.TimedTask, trigger string) {
log := s.executor.Run(s.executionContext(), task, trigger)
func (s *TaskScheduler) beginRun(id uint) (context.Context, bool) {
ctx := s.executionContext()
s.runMu.Lock()
defer s.runMu.Unlock()
if s.stopping || ctx.Err() != nil {
return nil, false
}
if s.running == nil {
s.running = map[uint]struct{}{}
}
if _, exists := s.running[id]; exists {
return nil, false
}
if len(s.running) == 0 {
s.idle = make(chan struct{})
}
s.running[id] = struct{}{}
return ctx, true
}
func (s *TaskScheduler) finishRun(id uint) {
s.runMu.Lock()
defer s.runMu.Unlock()
if _, exists := s.running[id]; !exists {
return
}
delete(s.running, id)
if len(s.running) == 0 && s.idle != nil {
close(s.idle)
}
}
func (s *TaskScheduler) dispatch(task *biz.TimedTask, trigger string, async bool) bool {
ctx, ok := s.beginRun(task.ID)
if !ok {
s.logger.Warn("timed task skipped because it is already running or the scheduler is stopping", "task_id", task.ID, "task_name", task.Name, "trigger_type", trigger)
return false
}
run := func() {
defer s.finishRun(task.ID)
s.run(ctx, task, trigger)
}
if async {
go run()
return true
}
run()
return true
}
func (s *TaskScheduler) run(ctx context.Context, task *biz.TimedTask, trigger string) {
log := s.executor.Run(ctx, task, trigger)
attributes := []any{"task_id", log.TaskID, "task_name", log.TaskName, "trigger_type", log.TriggerType, "status", log.Status, "duration_ms", log.DurationMS, "started_at", log.StartedAt, "finished_at", log.FinishedAt}
if log.ErrorMsg != "" {
attributes = append(attributes, "error", log.ErrorMsg)
@@ -146,7 +211,9 @@ func (s *TaskScheduler) run(task *biz.TimedTask, trigger string) {
s.logger.Error("timed task finished", attributes...)
}
if log.Status != "success" {
ids, err := s.authorities.AuthorityUserIDs(context.Background(), 888)
alertCtx, cancel := context.WithTimeout(context.WithoutCancel(ctx), 3*time.Second)
defer cancel()
ids, err := s.authorities.AuthorityUserIDs(alertCtx, 888)
if err != nil {
s.logger.Error("query timed task alert recipients failed", "error", err)
return
@@ -156,36 +223,73 @@ func (s *TaskScheduler) run(task *biz.TimedTask, trigger string) {
}
func (s *TaskScheduler) Schedule(task *biz.TimedTask) error {
s.Remove(task.ID)
if task == nil {
return errors.New("定时任务不能为空")
}
var schedule cron.Schedule
var err error
if task.Enabled {
schedule, err = parseTaskSchedule(task)
if err != nil {
return err
}
}
s.mu.Lock()
defer s.mu.Unlock()
s.removeLocked(task.ID)
if !task.Enabled {
return nil
}
copy := *task
run := func() {
s.run(&copy, "auto")
}
var id cron.EntryID
var err error
if task.WithSeconds {
id, err = s.seconds.AddFunc(task.Spec, run)
} else {
id, err = s.standard.AddFunc(task.Spec, run)
}
if err != nil {
return err
}
s.mu.Lock()
s.entries[task.ID] = scheduledEntry{seconds: task.WithSeconds, entry: id}
s.mu.Unlock()
s.scheduleLocked(cloneTimedTask(task), schedule)
return nil
}
func parseTaskSchedule(task *biz.TimedTask) (cron.Schedule, error) {
if task.WithSeconds {
return cron.NewParser(cron.Second | cron.Minute | cron.Hour | cron.Dom | cron.Month | cron.Dow | cron.Descriptor).Parse(task.Spec)
}
return cron.ParseStandard(task.Spec)
}
func cloneTimedTask(task *biz.TimedTask) *biz.TimedTask {
copy := *task
copy.Params = append([]byte(nil), task.Params...)
copy.HTTPHeader = append([]byte(nil), task.HTTPHeader...)
return &copy
}
func (s *TaskScheduler) scheduleLocked(task *biz.TimedTask, schedule cron.Schedule) {
run := func() {
s.dispatch(task, "auto", false)
}
if task.WithSeconds {
id := s.seconds.Schedule(schedule, cron.FuncJob(run))
s.entries[task.ID] = scheduledEntry{seconds: true, entry: id}
} else {
id := s.standard.Schedule(schedule, cron.FuncJob(run))
s.entries[task.ID] = scheduledEntry{entry: id}
}
}
func (s *TaskScheduler) ScheduleID(ctx context.Context, id uint) error {
s.mu.Lock()
defer s.mu.Unlock()
task, err := s.tasks.FindTask(ctx, id)
if err != nil {
return err
}
return s.Schedule(task)
var schedule cron.Schedule
if task.Enabled {
schedule, err = parseTaskSchedule(task)
if err != nil {
return err
}
}
s.removeLocked(task.ID)
if task.Enabled {
s.scheduleLocked(cloneTimedTask(task), schedule)
}
return nil
}
func (s *TaskScheduler) TriggerID(ctx context.Context, id uint) error {
@@ -193,7 +297,9 @@ func (s *TaskScheduler) TriggerID(ctx context.Context, id uint) error {
if err != nil {
return err
}
s.Trigger(task)
if !s.Trigger(task) {
return errors.New("任务正在执行或调度器正在停止")
}
return nil
}
@@ -211,9 +317,11 @@ func (s *TaskScheduler) NextRuns() map[uint]time.Time {
return out
}
func (s *TaskScheduler) Trigger(task *biz.TimedTask) {
copy := *task
go s.run(&copy, "manual")
func (s *TaskScheduler) Trigger(task *biz.TimedTask) bool {
if task == nil {
return false
}
return s.dispatch(cloneTimedTask(task), "manual", true)
}
func (s *TaskScheduler) Subscribe(userID uint) chan []byte {