diff --git a/go.mod b/go.mod index 18de474..466e7d8 100644 --- a/go.mod +++ b/go.mod @@ -1,3 +1,3 @@ module github.com/sunshineplan/utils -go 1.24 +go 1.25 diff --git a/scheduler/clock.go b/scheduler/clock.go index 1b27c20..2e969c5 100644 --- a/scheduler/clock.go +++ b/scheduler/clock.go @@ -12,11 +12,16 @@ var ( _ Schedule = clockSched{} ) +// Clock represents a specific time of day (hour, minute, second) that can be used +// as a schedule condition. Each field (hour, min, sec) can be optional, allowing +// partial matching (e.g., “every minute at second 0” or “every day at 12:00:*”). type Clock struct { clock.Clock hour, min, sec bool } +// atClock creates a new clock instance from the given hour, minute, and second values. +// A value of -1 is treated as a wildcard and replaced by 0 internally. func atClock(hour, min, sec int) clock.Clock { if hour == -1 { hour = 0 @@ -30,6 +35,8 @@ func atClock(hour, min, sec int) clock.Clock { return clock.New(hour, min, sec) } +// AtClock creates a new Clock schedule at the specified hour, minute, and second. +// Use -1 for a wildcard (any value). Panics on invalid input. func AtClock(hour, min, sec int) *Clock { if hour > 23 || hour < -1 || min > 59 || min < -1 || @@ -50,20 +57,26 @@ func AtClock(hour, min, sec int) *Clock { return &c } +// FullClock returns a Clock with all fields as wildcards (matches any time). func FullClock() *Clock { return new(Clock) } +// AtHour returns a Clock that triggers at the specified hour (minute and second = 0). func AtHour(hour int) *Clock { return AtClock(hour, 0, 0) } +// AtMinute returns a Clock that triggers at the specified minute of any hour. func AtMinute(min int) *Clock { return AtClock(-1, min, 0) } +// AtSecond returns a Clock that triggers at the specified second of any minute. func AtSecond(sec int) *Clock { return AtClock(-1, -1, sec) } +// ClockFromString parses a clock string (e.g. "12:30:00") into a Clock schedule. +// Panics if the string cannot be parsed. func ClockFromString(str string) *Clock { c, err := clock.Parse(str) if err != nil { @@ -72,6 +85,7 @@ func ClockFromString(str string) *Clock { return &Clock{c, true, true, true} } +// HourSchedule creates a multi-schedule that triggers on any of the specified hours. func HourSchedule(hour ...int) Schedule { var s multiSched for _, hour := range hour { @@ -80,6 +94,7 @@ func HourSchedule(hour ...int) Schedule { return s } +// MinuteSchedule creates a multi-schedule that triggers on any of the specified minutes. func MinuteSchedule(min ...int) Schedule { var s multiSched for _, min := range min { @@ -88,6 +103,7 @@ func MinuteSchedule(min ...int) Schedule { return s } +// SecondSchedule creates a multi-schedule that triggers on any of the specified seconds. func SecondSchedule(sec ...int) Schedule { var s multiSched for _, sec := range sec { @@ -96,6 +112,7 @@ func SecondSchedule(sec ...int) Schedule { return s } +// Hour sets the hour field of the Clock (or disables it if -1). func (c *Clock) Hour(hour int) *Clock { if hour > 23 || hour < -1 { panic(fmt.Sprint("invalid hour ", hour)) @@ -109,6 +126,7 @@ func (c *Clock) Hour(hour int) *Clock { return c } +// Minute sets the minute field of the Clock (or disables it if -1). func (c *Clock) Minute(min int) *Clock { if min > 59 || min < -1 { panic(fmt.Sprint("invalid minute ", min)) @@ -122,6 +140,7 @@ func (c *Clock) Minute(min int) *Clock { return c } +// Second sets the second field of the Clock (or disables it if -1). func (c *Clock) Second(sec int) *Clock { if sec > 59 || sec < -1 { panic(fmt.Sprint("invalid second ", sec)) @@ -135,6 +154,8 @@ func (c *Clock) Second(sec int) *Clock { return c } +// IsMatched reports whether the given time matches this Clock. +// Wildcard fields (hour/min/sec=false) are ignored during comparison. func (c Clock) IsMatched(t time.Time) bool { hour, min, sec := t.Clock() return (!c.hour || c.Clock.Hour() == hour) && @@ -142,6 +163,8 @@ func (c Clock) IsMatched(t time.Time) bool { (!c.sec || c.Clock.Second() == sec) } +// Next returns the next time that matches this Clock configuration +// after the given reference time. Handles wildcards intelligently. func (c Clock) Next(t time.Time) (next time.Time) { t = t.Truncate(time.Second) if c.IsMatched(t) { @@ -165,7 +188,7 @@ func (c Clock) Next(t time.Time) (next time.Time) { hour = t.Hour() } switch next = time.Date(year, month, day, hour, min, sec, 0, t.Location()); t.Compare(next) { - case 1: + case 1: // next < t if !c.sec { next = next.Add(-time.Duration(sec) * time.Second) } @@ -181,7 +204,7 @@ func (c Clock) Next(t time.Time) (next time.Time) { return next.Add(time.Hour) } return next.AddDate(0, 0, 1) - case -1: + case -1: // next > t if !c.sec { next = next.Add(-time.Duration(sec) * time.Second) } @@ -189,11 +212,12 @@ func (c Clock) Next(t time.Time) (next time.Time) { next = next.Add(-time.Duration(min) * time.Minute) } return - default: + default: // equal return t } } +// String returns a human-readable representation such as "12:--:--" or "14:30:00". func (c Clock) String() string { var hour, min, sec string if !c.hour { @@ -214,11 +238,16 @@ func (c Clock) String() string { return fmt.Sprintf("%s:%s:%s", hour, min, sec) } +// clockSched defines a schedule for a time interval within a day, +// repeatedly triggering at a fixed duration between start and end. type clockSched struct { start, end *Clock d time.Duration } +// ClockSchedule creates a new schedule that triggers every d duration +// between start and end (inclusive). The duration must be at least one second +// and an integer multiple of a second. func ClockSchedule(start, end *Clock, d time.Duration) Schedule { if d < time.Second || d%time.Second != 0 { panic("the minimum duration is one second and must be a multiple of seconds") @@ -226,11 +255,15 @@ func ClockSchedule(start, end *Clock, d time.Duration) Schedule { return clockSched{start, end, d} } +// IsMatched reports whether the given time falls within [start, end] +// and aligns with the configured duration d. func (s clockSched) IsMatched(t time.Time) bool { start, end, tc := s.start, s.end, AtClock(t.Clock()).Clock return (start.Equal(tc) || start.Before(tc) && end.After(tc) || end.Equal(tc)) && tc.Since(start.Clock)%s.d == 0 } +// Next returns the next matching time within the configured range. +// If none is found, it wraps to the next occurrence of start. func (s clockSched) Next(t time.Time) time.Time { if s.IsMatched(t) { t = t.Add(time.Second) @@ -244,15 +277,7 @@ func (s clockSched) Next(t time.Time) time.Time { return s.start.Next(t) } -func (s clockSched) TickerDuration() time.Duration { - if s.start.Clock.Second() != 0 { - return time.Second - } else if s.start.Clock.Minute() != 0 && s.d%time.Minute == 0 { - return time.Minute - } - return s.d -} - +// String returns a readable representation like "08:00:00-18:00:00(every 30m0s)". func (s clockSched) String() string { return fmt.Sprintf("%q-%q(every %s)", s.start, s.end, s.d) } diff --git a/scheduler/complex.go b/scheduler/complex.go index 1c8dfc4..daf8bb8 100644 --- a/scheduler/complex.go +++ b/scheduler/complex.go @@ -13,19 +13,29 @@ var ( _ complexSched = condSched{} ) +// complexSched defines an internal interface for composite schedules. +// It extends Schedule with initialization and introspection capabilities. type complexSched interface { + // IsMatched reports whether the given time matches the composite condition. IsMatched(time.Time) bool + // Next returns the next time that satisfies the composite condition. Next(time.Time) time.Time + // String returns a human-readable representation. String() string + // init initializes internal states or nested schedules using the given start time. init(t time.Time) + // len returns the number of sub-schedules contained in this composite schedule. len() int } +// complex is a type constraint used for generic initialization of schedule slices. type complex interface { ~[]Schedule } +// initComplexSched initializes all sub-schedules that implement complexSched or tickerSched. +// It ensures that nested or periodic schedules have their starting point properly set. func initComplexSched[sche complex](s sche, t time.Time) { for _, s := range s { if i, ok := s.(complexSched); ok { @@ -36,20 +46,27 @@ func initComplexSched[sche complex](s sche, t time.Time) { } } +// multiSched represents a composite schedule that matches if *any* +// of its sub-schedules match — i.e., a logical OR operation. type multiSched []Schedule +// MultiSchedule creates a new schedule that triggers when any of the provided +// schedules match. Equivalent to a logical OR of all schedules. func MultiSchedule(schedules ...Schedule) Schedule { return multiSched(schedules) } +// init initializes nested schedules recursively. func (s multiSched) init(t time.Time) { initComplexSched(s, t) } +// len returns the number of contained sub-schedules. func (s multiSched) len() int { return len(s) } +// IsMatched returns true if any sub-schedule matches the given time. func (s multiSched) IsMatched(t time.Time) bool { for _, i := range s { if i.IsMatched(t) { @@ -59,6 +76,8 @@ func (s multiSched) IsMatched(t time.Time) bool { return false } +// Next returns the earliest next time among all sub-schedules. +// If no valid next time exists, it returns a zero time. func (s multiSched) Next(t time.Time) (next time.Time) { for _, i := range s { if t := i.Next(t); next.IsZero() || !t.IsZero() && t.Before(next) { @@ -68,6 +87,7 @@ func (s multiSched) Next(t time.Time) (next time.Time) { return } +// String returns a readable representation of the multi-schedule. func (s multiSched) String() string { switch len(s) { case 0: @@ -87,20 +107,27 @@ func (s multiSched) String() string { } } +// condSched represents a composite schedule that matches only if *all* +// of its sub-schedules match — i.e., a logical AND operation. type condSched []Schedule +// ConditionSchedule creates a new schedule that triggers only when all +// of the provided schedules match simultaneously. Equivalent to a logical AND. func ConditionSchedule(schedules ...Schedule) Schedule { return condSched(schedules) } +// init initializes nested schedules recursively. func (s condSched) init(t time.Time) { initComplexSched(s, t) } +// len returns the number of contained sub-schedules. func (s condSched) len() int { return len(s) } +// IsMatched returns true only if all sub-schedules match the given time. func (s condSched) IsMatched(t time.Time) bool { if s.len() == 0 { return false @@ -113,15 +140,21 @@ func (s condSched) IsMatched(t time.Time) bool { return true } +// Next returns the next time that satisfies all sub-schedules simultaneously. +// If there are no schedules, it returns zero time. +// If the current time already matches, it advances by one second to find the next occurrence. func (s condSched) Next(t time.Time) (next time.Time) { if l := len(s); l == 0 { return time.Time{} } else if l == 1 { return s[0].Next(t) } + // Avoid returning the same time repeatedly if it already matches. if s.IsMatched(t) { t = t.Add(time.Second) } + // Increment one second at a time until all conditions match, + // but limit the search to one year to avoid infinite loops. for next = t.Truncate(time.Second); !s.IsMatched(next); next = next.Add(time.Second) { if next.Sub(t) >= time.Hour*24*366 { return time.Time{} @@ -130,6 +163,7 @@ func (s condSched) Next(t time.Time) (next time.Time) { return } +// String returns a readable representation of the condition schedule. func (s condSched) String() string { switch len(s) { case 0: diff --git a/scheduler/init.go b/scheduler/init.go index afc11ff..481807a 100644 --- a/scheduler/init.go +++ b/scheduler/init.go @@ -6,14 +6,25 @@ import ( "github.com/sunshineplan/utils/container" ) +// subscriber is a global registry mapping event channels to their target times. +// It is used internally by all running schedulers to receive tick events. var subscriber = container.NewMap[chan Event, time.Time]() +// Event represents a time event emitted by the scheduler engine. +// It carries both the actual trigger time (Time) and the intended schedule time (Goal). +// If Missed is true, the event was triggered after its scheduled Goal. type Event struct { Time time.Time Goal time.Time Missed bool } +// init launches a global background goroutine that ticks every second. +// For each tick, it compares the current time with all subscribed times, +// and sends corresponding Event values to their channels. +// +// This mechanism allows multiple Scheduler instances to share the same +// centralized time source and operate concurrently. func init() { go func() { for t := range time.NewTicker(time.Second).C { @@ -31,10 +42,12 @@ func init() { }() } +// subscribe registers a channel to receive an Event when the given time arrives. func subscribe(t time.Time, c chan Event) { subscriber.Swap(c, t.Truncate(time.Second)) } +// unsubscribe removes a previously registered channel from the subscriber map. func unsubscribe(c chan Event) { subscriber.Delete(c) } diff --git a/scheduler/schedule.go b/scheduler/schedule.go index afed064..9dba746 100644 --- a/scheduler/schedule.go +++ b/scheduler/schedule.go @@ -8,9 +8,15 @@ import ( "github.com/sunshineplan/utils/clock" ) +// Schedule defines a time-based trigger that determines +// whether a given time matches a schedule and can compute the next trigger time. type Schedule interface { + // IsMatched reports whether the given time matches the schedule. IsMatched(time.Time) bool + // Next returns the next time that satisfies the schedule after the given time. + // If there is no valid next time, a zero time is returned. Next(time.Time) time.Time + // String returns a human-readable description of the schedule. String() string } @@ -20,12 +26,14 @@ var ( _ Schedule = tickerSched{} ) +// datetimeLayout defines supported layouts for parsing date/time strings. var datetimeLayout = []string{ "2006-01-02", "2006-01-02 15:04", "2006-01-02 15:04:05", } +// parseTime attempts to parse a string using a list of layouts. func parseTime(value string, layout []string) (t time.Time, err error) { for _, layout := range layout { t, err = time.Parse(layout, value) @@ -36,6 +44,9 @@ func parseTime(value string, layout []string) (t time.Time, err error) { return } +// ScheduleFromString constructs a Schedule from one or more string expressions. +// It supports both clock expressions (parsed by [clock.Parse]) +// and absolute timestamps (e.g., "2006-01-02 15:04:05"). func ScheduleFromString(str ...string) Schedule { var s multiSched for _, str := range str { @@ -53,6 +64,8 @@ func ScheduleFromString(str ...string) Schedule { return s } +// sched represents a calendar-based schedule with optional year, month, day and time components. +// A zero value (0) for year/month/day acts as a wildcard that matches any value. type sched struct { year int month time.Month @@ -60,6 +73,8 @@ type sched struct { clock *Clock } +// NewSchedule creates a new date-based Schedule. +// Zero values for year/month/day represent wildcards that match any year, month, or day. func NewSchedule(year int, month time.Month, day int, clock *Clock) Schedule { if clock == nil { clock = new(Clock) @@ -67,6 +82,7 @@ func NewSchedule(year int, month time.Month, day int, clock *Clock) Schedule { return sched{year, month, day, clock} } +// TimeSchedule creates a schedule based on one or more absolute times. func TimeSchedule(t ...time.Time) Schedule { var s multiSched for _, t := range t { @@ -76,6 +92,7 @@ func TimeSchedule(t ...time.Time) Schedule { return s } +// IsMatched reports whether the given time matches the date and clock configuration. func (s sched) IsMatched(t time.Time) bool { year, month, day := t.Date() if (s.year == 0 || s.year == year) && @@ -86,11 +103,14 @@ func (s sched) IsMatched(t time.Time) bool { return false } +// Next returns the next time that matches this schedule after t. +// If no valid next time exists, it returns a zero time. func (s sched) Next(t time.Time) (next time.Time) { t = t.Truncate(time.Second) next = s.clock.Next(t) t = t.Add(time.Second) year, month, day := next.Date() + // Apply wildcard substitutions if s.year != 0 { year = s.year } @@ -102,7 +122,8 @@ func (s sched) Next(t time.Time) (next time.Time) { } hour, min, sec := next.Clock() switch next = time.Date(year, month, day, hour, min, sec, 0, t.Location()); t.Compare(next) { - case 1: + case 1: // t > next + // Reset lower time fields if they are not specified if !s.clock.sec { sec = 0 } @@ -113,6 +134,7 @@ func (s sched) Next(t time.Time) (next time.Time) { hour = 0 } next = time.Date(year, month, day, hour, min, sec, 0, t.Location()) + // Handle wildcard advancement for day/month/year if s.day == 0 { if next = next.AddDate(0, 0, 1); (next.Month() != month && s.month != 0) || (next.Year() != year && s.year != 0) || @@ -132,7 +154,7 @@ func (s sched) Next(t time.Time) (next time.Time) { if s.year == 0 { next = next.AddDate(1, 0, 0) } - case -1: + case -1: // t < next if !s.clock.sec { sec = 0 } @@ -161,6 +183,7 @@ func (s sched) Next(t time.Time) (next time.Time) { return } +// String returns the formatted representation of the schedule. func (s sched) String() string { var year, month, day string if s.year == 0 { @@ -181,12 +204,14 @@ func (s sched) String() string { return fmt.Sprintf("%s/%s/%s %s", year, month, day, s.clock) } +// weekSched represents a schedule based on ISO week and weekday. type weekSched struct { year, week int weekday *time.Weekday clock *Clock } +// ISOWeekSchedule creates a schedule based on ISO week number and weekday. func ISOWeekSchedule(year int, week int, weekday *time.Weekday, clock *Clock) Schedule { if clock == nil { clock = new(Clock) @@ -194,6 +219,7 @@ func ISOWeekSchedule(year int, week int, weekday *time.Weekday, clock *Clock) Sc return weekSched{year, week, weekday, clock} } +// Weekday creates a schedule that matches the specified weekdays at any time of day. func Weekday(weekday ...time.Weekday) Schedule { var s multiSched for _, weekday := range weekday { @@ -202,11 +228,13 @@ func Weekday(weekday ...time.Weekday) Schedule { return s } +// Predefined weekday groups. var ( Weekdays = Weekday(time.Monday, time.Tuesday, time.Wednesday, time.Thursday, time.Friday) Weekends = Weekday(time.Saturday, time.Sunday) ) +// IsMatched reports whether the given time matches the week and weekday pattern. func (s weekSched) IsMatched(t time.Time) bool { year, week := t.ISOWeek() weekday := t.Weekday() @@ -224,6 +252,7 @@ func (s weekSched) IsMatched(t time.Time) bool { return false } +// newWeekdayTime returns the time corresponding to a given ISO week, weekday, and clock. func newWeekdayTime(year int, week int, weekday time.Weekday, hour, min, sec int, loc *time.Location) time.Time { t := time.Date(year, 1, 1, hour, min, sec, 0, loc) if wd := t.Weekday(); wd != weekday { @@ -247,6 +276,7 @@ func newWeekdayTime(year int, week int, weekday time.Weekday, hour, min, sec int return t } +// Next returns the next time that matches this ISO week schedule after t. func (s weekSched) Next(t time.Time) (next time.Time) { if s.week < 0 || s.week > 53 { return time.Time{} @@ -267,7 +297,7 @@ func (s weekSched) Next(t time.Time) (next time.Time) { } hour, min, sec := next.Clock() switch next = newWeekdayTime(year, week, weekday, hour, min, sec, t.Location()); t.Compare(next) { - case 1: + case 1: // t > next if !s.clock.sec { sec = 0 } @@ -305,7 +335,7 @@ func (s weekSched) Next(t time.Time) (next time.Time) { next = next.AddDate(0, 0, 7) } } - case -1: + case -1: // t < next if !s.clock.sec { sec = 0 } @@ -334,6 +364,7 @@ func (s weekSched) Next(t time.Time) (next time.Time) { return } +// String returns the formatted representation of the ISO week schedule. func (s weekSched) String() string { var year, week, weekday string if s.year == 0 { @@ -354,11 +385,14 @@ func (s weekSched) String() string { return fmt.Sprintf("%s/ISOWeek:%s/Weekday:%s %s", year, week, weekday, s.clock) } +// tickerSched represents a recurring schedule that triggers every fixed duration. type tickerSched struct { d time.Duration start time.Time } +// Every returns a schedule that triggers repeatedly at the given durations. +// The minimum granularity is one second, and durations must be multiples of one second. func Every(d ...time.Duration) Schedule { var s multiSched for _, d := range d { @@ -370,10 +404,12 @@ func Every(d ...time.Duration) Schedule { return s } +// init records the start time as the reference point for periodic scheduling. func (s *tickerSched) init(t time.Time) { s.start = t.Truncate(time.Second) } +// IsMatched reports whether the given time aligns exactly with the ticker interval. func (s tickerSched) IsMatched(t time.Time) bool { if s.d == 0 { return false @@ -381,6 +417,7 @@ func (s tickerSched) IsMatched(t time.Time) bool { return t.Truncate(time.Second).Sub(s.start)%s.d == 0 } +// Next returns the next tick time after t based on the duration interval. func (s tickerSched) Next(t time.Time) time.Time { t = t.Truncate(time.Second) if d := t.Sub(s.start); d > 0 { @@ -393,6 +430,7 @@ func (s tickerSched) Next(t time.Time) time.Time { return s.start.Add(s.d) } +// String returns a human-readable representation of the ticker schedule. func (s tickerSched) String() string { return fmt.Sprint("Every ", s.d) } diff --git a/scheduler/scheduler.go b/scheduler/scheduler.go index 2b56b31..2a9d5d6 100644 --- a/scheduler/scheduler.go +++ b/scheduler/scheduler.go @@ -5,22 +5,29 @@ import ( "errors" "log/slog" "sync" + "sync/atomic" "time" ) var ( - ErrNoFunction = errors.New("scheduler function is not set") - ErrNoSchedule = errors.New("no schedule has been added") + // ErrNoFunction indicates that no function has been registered to run. + ErrNoFunction = errors.New("scheduler function is not set") + // ErrNoSchedule indicates that no schedule has been configured. + ErrNoSchedule = errors.New("no schedule has been added") + // ErrAlreadyRunning indicates that the scheduler is already active. ErrAlreadyRunning = errors.New("scheduler is already running") ) +// Scheduler defines a flexible time-based job runner. +// It supports multiple schedules, condition combinations, missed-event handling, +// and optional structured debug logging. type Scheduler struct { mu sync.Mutex - tc chan Event - fn []func(Event) + tc chan Event // event channel from global ticker + fn []func(Event) // functions to execute on trigger - ignoreMissed bool + ignoreMissed atomic.Bool // whether to skip missed executions sched complexSched @@ -30,28 +37,33 @@ type Scheduler struct { debugLogger *slog.Logger } +// NewScheduler creates a new, uninitialized Scheduler instance. func NewScheduler() *Scheduler { return &Scheduler{tc: make(chan Event, 1)} } +// WithDebug attaches a slog.Logger for debug output. func (sched *Scheduler) WithDebug(logger *slog.Logger) *Scheduler { sched.debugLogger = logger return sched } +// SetIgnoreMissed sets whether missed schedule times should be ignored. +// If true, the scheduler will skip backlogged runs caused by delays. func (sched *Scheduler) SetIgnoreMissed(ignore bool) *Scheduler { - sched.mu.Lock() - defer sched.mu.Unlock() - sched.ignoreMissed = ignore + sched.ignoreMissed.Store(ignore) return sched } +// debug logs a debug message if a logger is configured. func (sched *Scheduler) debug(msg string, args ...any) { if sched.debugLogger != nil { sched.debugLogger.Debug(msg, args...) } } +// At sets the scheduler to trigger when *any* of the provided schedules match. +// This is a logical OR of all schedules. func (sched *Scheduler) At(schedules ...Schedule) *Scheduler { if len(schedules) == 0 { panic("no schedules") @@ -63,6 +75,8 @@ func (sched *Scheduler) At(schedules ...Schedule) *Scheduler { return sched } +// AtCondition sets the scheduler to trigger only when *all* schedules match. +// This is a logical AND of all schedules. func (sched *Scheduler) AtCondition(schedules ...Schedule) *Scheduler { if len(schedules) == 0 { panic("no schedules") @@ -74,6 +88,7 @@ func (sched *Scheduler) AtCondition(schedules ...Schedule) *Scheduler { return sched } +// String returns a human-readable representation of the configured schedule. func (sched *Scheduler) String() string { sched.mu.Lock() defer sched.mu.Unlock() @@ -83,6 +98,7 @@ func (sched *Scheduler) String() string { return sched.sched.String() } +// Clear removes all schedules and stops any running context. func (sched *Scheduler) Clear() { sched.mu.Lock() defer sched.mu.Unlock() @@ -92,6 +108,8 @@ func (sched *Scheduler) Clear() { } } +// Run registers one or more functions to be executed when the schedule triggers. +// Functions are executed in separate goroutines. func (sched *Scheduler) Run(fn ...func(Event)) *Scheduler { sched.mu.Lock() defer sched.mu.Unlock() @@ -103,6 +121,8 @@ func (sched *Scheduler) Run(fn ...func(Event)) *Scheduler { return sched } +// init prepares the scheduler for execution. +// It validates configuration, initializes contexts, and subscribes for the next event. func (sched *Scheduler) init() error { sched.mu.Lock() defer sched.mu.Unlock() @@ -123,6 +143,8 @@ func (sched *Scheduler) init() error { return ErrAlreadyRunning } +// start launches the main loop that listens for Event notifications +// and executes registered functions upon each trigger. func (sched *Scheduler) start(once bool) error { if err := sched.init(); err != nil { return err @@ -135,7 +157,7 @@ func (sched *Scheduler) start(once bool) error { sched.debug("Scheduler Missed Run Time", "Name", sched.sched, "Time", e.Time, "Goal", e.Goal) } sched.mu.Lock() - if once || !e.Missed || !sched.ignoreMissed { + if once || !e.Missed || !sched.ignoreMissed.Load() { for _, fn := range sched.fn { go fn(e) } @@ -162,10 +184,13 @@ func (sched *Scheduler) start(once bool) error { return nil } +// Start begins running the scheduler continuously until Stop is called. func (sched *Scheduler) Start() error { return sched.start(false) } +// Once starts the scheduler for a single execution and returns an error channel +// that reports initialization status. func (sched *Scheduler) Once() <-chan error { c := make(chan error, 1) go func() { @@ -174,6 +199,8 @@ func (sched *Scheduler) Once() <-chan error { return c } +// Do runs the given function according to the current schedule configuration. +// If the scheduler is already running, the error is suppressed. func (sched *Scheduler) Do(fn func(Event)) error { sched.Run(fn) err := sched.Start() @@ -183,10 +210,13 @@ func (sched *Scheduler) Do(fn func(Event)) error { return err } +// Stop stops the scheduler and cancels its running context. func (sched *Scheduler) Stop() { sched.cancel() } +// immediately triggers all registered functions immediately with the given time, +// returning a channel that closes when all functions complete. func (sched *Scheduler) immediately(t time.Time) <-chan struct{} { sched.mu.Lock() defer sched.mu.Unlock() @@ -194,10 +224,7 @@ func (sched *Scheduler) immediately(t time.Time) <-chan struct{} { var wg sync.WaitGroup wg.Add(len(sched.fn)) for _, fn := range sched.fn { - go func(f func(Event)) { - defer wg.Done() - f(Event{Time: t, Goal: t}) - }(fn) + wg.Go(func() { fn(Event{Time: t, Goal: t}) }) } done := make(chan struct{}) go func() { @@ -207,10 +234,12 @@ func (sched *Scheduler) immediately(t time.Time) <-chan struct{} { return done } +// Immediately triggers all registered functions right now (non-scheduled). func (sched *Scheduler) Immediately() <-chan struct{} { return sched.immediately(time.Now()) } +// Forever blocks indefinitely, keeping the current goroutine alive. func Forever() { select {} }