Create a scheduler as a first-class core module feature, following the same pattern used for the Asynq queue system. The new core/scheduler/ subpackage contains QueueItem and ProcessedItem GORM models (tables queue_items and processed_items), a Store for database operations (enqueue, claim batch with SELECT FOR UPDATE SKIP LOCKED, result recording, stuck recovery), a Semaphore kill switch, and the polling Scheduler loop with configurable interval and rate limit. The core/scheduler.go wrapper exposes a Schedulermux struct (analogous to Queuemux) with SchedulerInit(), AddWork(), SetStore(), and RunScheduler() methods, plus global accessors ResolveSchedulerStore() and SchedulerSemaphoreSetGreen/Red/IsGreen() so controllers can enqueue work and control the semaphore without importing the subpackage directly.
This commit is contained in:
parent
b757aef6b1
commit
3710fbe343
3 changed files with 547 additions and 0 deletions
147
scheduler.go
Normal file
147
scheduler.go
Normal file
|
|
@ -0,0 +1,147 @@
|
|||
// Copyright (c) 2026 Jose Cely <me@jacs.guru>
|
||||
// Use of this source code is governed by MIT-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
package core
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"log"
|
||||
"time"
|
||||
|
||||
"git.smarteching.com/goffee/core/logger"
|
||||
"git.smarteching.com/goffee/core/scheduler"
|
||||
)
|
||||
|
||||
// Default values for SchedulerConfig
|
||||
var DefaultSchedulerInterval = 10 * time.Second
|
||||
var DefaultSchedulerRateLimit = 10
|
||||
|
||||
// globalSchedulerMux holds the active Schedulermux instance so controllers
|
||||
// can access it without requiring a direct import chain.
|
||||
var globalSchedulerMux *Schedulermux
|
||||
|
||||
// Schedulermux wraps the scheduler Registry and manages the lifecycle
|
||||
// of a simple database-backed task scheduler.
|
||||
type Schedulermux struct {
|
||||
registry *scheduler.Registry
|
||||
store *scheduler.Store
|
||||
semaphore *scheduler.Semaphore
|
||||
sched *scheduler.Scheduler
|
||||
}
|
||||
|
||||
// SchedulerInit initializes the scheduler internals (registry and semaphore).
|
||||
// The store must be set separately via SetStore before starting.
|
||||
func (s *Schedulermux) SchedulerInit() {
|
||||
s.registry = scheduler.NewRegistry()
|
||||
s.semaphore = scheduler.NewSemaphore(true)
|
||||
globalSchedulerMux = s
|
||||
}
|
||||
|
||||
// SetStore sets the store (backed by *gorm.DB) on the scheduler.
|
||||
// This also runs AutoMigrate for the queue_items and processed_items tables.
|
||||
func (s *Schedulermux) SetStore(st *scheduler.Store) {
|
||||
s.store = st
|
||||
}
|
||||
|
||||
// AddWork registers a task handler function for a given task type.
|
||||
func (s *Schedulermux) AddWork(taskType string, handler scheduler.HandlerFunc) {
|
||||
s.registry.Register(taskType, handler)
|
||||
}
|
||||
|
||||
// GetStore returns the scheduler store, or nil if not initialized.
|
||||
func (s *Schedulermux) GetStore() *scheduler.Store {
|
||||
return s.store
|
||||
}
|
||||
|
||||
// RunScheduler starts the scheduler loop with the given configuration.
|
||||
// It creates the internal Scheduler instance and launches it in a goroutine.
|
||||
func (s *Schedulermux) RunScheduler(config SchedulerConfig) {
|
||||
if s.store == nil {
|
||||
log.Fatal("scheduler: store is not set — call SetStore before RunScheduler")
|
||||
}
|
||||
|
||||
interval := config.SchedulerInterval
|
||||
if interval <= 0 {
|
||||
interval = DefaultSchedulerInterval
|
||||
}
|
||||
|
||||
rateLimit := config.SchedulerRateLimit
|
||||
if rateLimit < 0 {
|
||||
rateLimit = DefaultSchedulerRateLimit
|
||||
}
|
||||
|
||||
s.sched = scheduler.NewScheduler(
|
||||
scheduler.SchedulerConfig{
|
||||
Interval: interval,
|
||||
RateLimit: rateLimit,
|
||||
},
|
||||
s.semaphore,
|
||||
s.store,
|
||||
s.registry,
|
||||
)
|
||||
|
||||
// Recover stuck items from a previous crash before starting.
|
||||
ctx := context.Background()
|
||||
recovered, err := s.store.RecoverStuck(ctx)
|
||||
if err != nil {
|
||||
logger.ResolveLogger().Error(fmt.Sprintf("[scheduler] recover stuck error: %v", err))
|
||||
} else if recovered > 0 {
|
||||
logger.ResolveLogger().Info(fmt.Sprintf("[scheduler] recovered %d stuck task(s) back to pending", recovered))
|
||||
}
|
||||
|
||||
s.sched.Start(ctx)
|
||||
}
|
||||
|
||||
// StopScheduler gracefully stops the scheduler loop.
|
||||
func (s *Schedulermux) StopScheduler() {
|
||||
if s.sched != nil {
|
||||
s.sched.Stop()
|
||||
}
|
||||
}
|
||||
|
||||
// SemaphoreSetGreen allows task execution to proceed.
|
||||
func (s *Schedulermux) SemaphoreSetGreen() {
|
||||
if s.semaphore != nil {
|
||||
s.semaphore.SetGreen()
|
||||
}
|
||||
}
|
||||
|
||||
// SemaphoreSetRed blocks task execution.
|
||||
func (s *Schedulermux) SemaphoreSetRed() {
|
||||
if s.semaphore != nil {
|
||||
s.semaphore.SetRed()
|
||||
}
|
||||
}
|
||||
|
||||
// ResolveSchedulerStore returns the store from the global scheduler mux instance.
|
||||
// Returns nil if the scheduler was never initialized.
|
||||
func ResolveSchedulerStore() *scheduler.Store {
|
||||
if globalSchedulerMux == nil {
|
||||
return nil
|
||||
}
|
||||
return globalSchedulerMux.GetStore()
|
||||
}
|
||||
|
||||
// SchedulerSemaphoreSetGreen enables task execution by setting the semaphore to green.
|
||||
func SchedulerSemaphoreSetGreen() {
|
||||
if globalSchedulerMux != nil {
|
||||
globalSchedulerMux.SemaphoreSetGreen()
|
||||
}
|
||||
}
|
||||
|
||||
// SchedulerSemaphoreSetRed blocks task execution by setting the semaphore to red.
|
||||
func SchedulerSemaphoreSetRed() {
|
||||
if globalSchedulerMux != nil {
|
||||
globalSchedulerMux.SemaphoreSetRed()
|
||||
}
|
||||
}
|
||||
|
||||
// SchedulerSemaphoreIsGreen returns whether the scheduler semaphore is currently green.
|
||||
func SchedulerSemaphoreIsGreen() bool {
|
||||
if globalSchedulerMux == nil || globalSchedulerMux.semaphore == nil {
|
||||
return false
|
||||
}
|
||||
return globalSchedulerMux.semaphore.IsGreen()
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue