Files
mq/scheduler.go
2024-12-26 13:44:06 +05:45

446 lines
11 KiB
Go

package mq
import (
"context"
"fmt"
"strconv"
"strings"
"sync"
"time"
)
type ScheduleOptions struct {
Handler Handler
Callback Callback
Interval time.Duration
Overlap bool
Recurring bool
}
type SchedulerOption func(*ScheduleOptions)
// Helper functions to create SchedulerOptions
func WithSchedulerHandler(handler Handler) SchedulerOption {
return func(opts *ScheduleOptions) {
opts.Handler = handler
}
}
func WithSchedulerCallback(callback Callback) SchedulerOption {
return func(opts *ScheduleOptions) {
opts.Callback = callback
}
}
func WithOverlap() SchedulerOption {
return func(opts *ScheduleOptions) {
opts.Overlap = true
}
}
func WithInterval(interval time.Duration) SchedulerOption {
return func(opts *ScheduleOptions) {
opts.Interval = interval
}
}
func WithRecurring() SchedulerOption {
return func(opts *ScheduleOptions) {
opts.Recurring = true
}
}
// defaultOptions returns the default scheduling options
func defaultSchedulerOptions() *ScheduleOptions {
return &ScheduleOptions{
Interval: time.Minute,
Recurring: true,
}
}
type SchedulerConfig struct {
Callback Callback
Overlap bool
}
type ScheduledTask struct {
ctx context.Context
handler Handler
payload *Task
config SchedulerConfig
schedule *Schedule
stop chan struct{}
executionHistory []ExecutionHistory
}
type Schedule struct {
TimeOfDay time.Time
CronSpec string
DayOfWeek []time.Weekday
DayOfMonth []int
Interval time.Duration
Recurring bool
}
func (s *Schedule) ToHumanReadable() string {
var sb strings.Builder
if s.CronSpec != "" {
cronDescription, err := parseCronSpec(s.CronSpec)
if err != nil {
sb.WriteString(fmt.Sprintf("Invalid CRON spec: %s\n", err.Error()))
} else {
sb.WriteString(fmt.Sprintf("CRON-based schedule: %s\n", cronDescription))
}
}
if s.Interval > 0 {
sb.WriteString(fmt.Sprintf("Recurring every %s\n", s.Interval))
}
if len(s.DayOfMonth) > 0 {
sb.WriteString("Occurs on the following days of the month: ")
for i, day := range s.DayOfMonth {
if i > 0 {
sb.WriteString(", ")
}
sb.WriteString(fmt.Sprintf("%d", day))
}
sb.WriteString("\n")
}
if len(s.DayOfWeek) > 0 {
sb.WriteString("Occurs on the following days of the week: ")
for i, day := range s.DayOfWeek {
if i > 0 {
sb.WriteString(", ")
}
sb.WriteString(day.String())
}
sb.WriteString("\n")
}
if !s.TimeOfDay.IsZero() {
sb.WriteString(fmt.Sprintf("Time of day: %s\n", s.TimeOfDay.Format("15:04")))
}
if s.Recurring {
sb.WriteString("This schedule is recurring.\n")
} else {
sb.WriteString("This schedule is one-time.\n")
}
if sb.Len() == 0 {
sb.WriteString("No schedule defined.")
}
return sb.String()
}
type CronSchedule struct {
Minute string
Hour string
DayOfMonth string
Month string
DayOfWeek string
}
func (c CronSchedule) String() string {
return fmt.Sprintf("At %s minute(s) past %s, on %s, during %s, every %s", c.Minute, c.Hour, c.DayOfWeek, c.Month, c.DayOfMonth)
}
func parseCronSpec(cronSpec string) (CronSchedule, error) {
parts := strings.Fields(cronSpec)
if len(parts) != 5 {
return CronSchedule{}, fmt.Errorf("invalid CRON spec: expected 5 fields, got %d", len(parts))
}
minute, err := cronFieldToString(parts[0], "minute")
if err != nil {
return CronSchedule{}, err
}
hour, err := cronFieldToString(parts[1], "hour")
if err != nil {
return CronSchedule{}, err
}
dayOfMonth, err := cronFieldToString(parts[2], "day of the month")
if err != nil {
return CronSchedule{}, err
}
month, err := cronFieldToString(parts[3], "month")
if err != nil {
return CronSchedule{}, err
}
dayOfWeek, err := cronFieldToString(parts[4], "day of the week")
if err != nil {
return CronSchedule{}, err
}
return CronSchedule{
Minute: minute,
Hour: hour,
DayOfMonth: dayOfMonth,
Month: month,
DayOfWeek: dayOfWeek,
}, nil
}
func cronFieldToString(field string, fieldName string) (string, error) {
switch field {
case "*":
return fmt.Sprintf("every %s", fieldName), nil
default:
values, err := parseCronValue(field)
if err != nil {
return "", fmt.Errorf("invalid %s field: %s", fieldName, err.Error())
}
return fmt.Sprintf("%s %s", strings.Join(values, ", "), fieldName), nil
}
}
func parseCronValue(field string) ([]string, error) {
var values []string
ranges := strings.Split(field, ",")
for _, r := range ranges {
if strings.Contains(r, "-") {
bounds := strings.Split(r, "-")
if len(bounds) != 2 {
return nil, fmt.Errorf("invalid range: %s", r)
}
start, err := strconv.Atoi(bounds[0])
if err != nil {
return nil, err
}
end, err := strconv.Atoi(bounds[1])
if err != nil {
return nil, err
}
for i := start; i <= end; i++ {
values = append(values, strconv.Itoa(i))
}
} else {
values = append(values, r)
}
}
return values, nil
}
type Scheduler struct {
pool *Pool
tasks []ScheduledTask
mu sync.Mutex
}
func (s *Scheduler) Start() {
for _, task := range s.tasks {
go s.schedule(task)
}
}
func (s *Scheduler) schedule(task ScheduledTask) {
if task.schedule.Interval > 0 {
ticker := time.NewTicker(task.schedule.Interval)
defer ticker.Stop()
if task.schedule.Recurring {
for {
select {
case <-ticker.C:
s.executeTask(task)
case <-task.stop:
return
}
}
} else {
// Execute once and return
select {
case <-ticker.C:
s.executeTask(task)
case <-task.stop:
return
}
}
} else if task.schedule.Recurring {
for {
now := time.Now()
nextRun := task.getNextRunTime(now)
select {
case <-time.After(nextRun.Sub(now)):
s.executeTask(task)
case <-task.stop:
return
}
}
}
}
func NewScheduler(pool *Pool) *Scheduler {
return &Scheduler{pool: pool}
}
func (task ScheduledTask) getNextRunTime(now time.Time) time.Time {
if task.schedule.CronSpec != "" {
return task.getNextCronRunTime(now)
}
if len(task.schedule.DayOfMonth) > 0 {
for _, day := range task.schedule.DayOfMonth {
nextRun := time.Date(now.Year(), now.Month(), day, task.schedule.TimeOfDay.Hour(), task.schedule.TimeOfDay.Minute(), 0, 0, now.Location())
if nextRun.After(now) {
return nextRun
}
}
nextMonth := now.AddDate(0, 1, 0)
return time.Date(nextMonth.Year(), nextMonth.Month(), task.schedule.DayOfMonth[0], task.schedule.TimeOfDay.Hour(), task.schedule.TimeOfDay.Minute(), 0, 0, now.Location())
}
if len(task.schedule.DayOfWeek) > 0 {
for _, weekday := range task.schedule.DayOfWeek {
nextRun := nextWeekday(now, weekday).Truncate(time.Minute).Add(task.schedule.TimeOfDay.Sub(time.Time{}))
if nextRun.After(now) {
return nextRun
}
}
}
return now
}
func (task ScheduledTask) getNextCronRunTime(now time.Time) time.Time {
cronSpecs, err := parseCronSpec(task.schedule.CronSpec)
if err != nil {
fmt.Println(fmt.Sprintf("Invalid CRON spec format: %s", err.Error()))
return now
}
nextRun := now
nextRun = task.applyCronField(nextRun, cronSpecs.Minute, "minute")
nextRun = task.applyCronField(nextRun, cronSpecs.Hour, "hour")
nextRun = task.applyCronField(nextRun, cronSpecs.DayOfMonth, "day")
nextRun = task.applyCronField(nextRun, cronSpecs.Month, "month")
nextRun = task.applyCronField(nextRun, cronSpecs.DayOfWeek, "weekday")
return nextRun
}
func (task ScheduledTask) applyCronField(t time.Time, fieldSpec string, unit string) time.Time {
switch fieldSpec {
case "*":
return t
default:
value, _ := strconv.Atoi(fieldSpec)
switch unit {
case "minute":
if t.Minute() > value {
t = t.Add(time.Hour)
}
t = time.Date(t.Year(), t.Month(), t.Day(), t.Hour(), value, 0, 0, t.Location())
case "hour":
if t.Hour() > value {
t = t.AddDate(0, 0, 1)
}
t = time.Date(t.Year(), t.Month(), t.Day(), value, t.Minute(), 0, 0, t.Location())
case "day":
if t.Day() > value {
t = t.AddDate(0, 1, 0)
}
t = time.Date(t.Year(), t.Month(), value, t.Hour(), t.Minute(), 0, 0, t.Location())
case "month":
if int(t.Month()) > value {
t = t.AddDate(1, 0, 0)
}
t = time.Date(t.Year(), time.Month(value), t.Day(), t.Hour(), t.Minute(), 0, 0, t.Location())
case "weekday":
weekday := time.Weekday(value)
for t.Weekday() != weekday {
t = t.AddDate(0, 0, 1)
}
}
return t
}
}
func nextWeekday(t time.Time, weekday time.Weekday) time.Time {
daysUntil := (int(weekday) - int(t.Weekday()) + 7) % 7
if daysUntil == 0 {
daysUntil = 7
}
return t.AddDate(0, 0, daysUntil)
}
func (s *Scheduler) executeTask(task ScheduledTask) {
if task.config.Overlap || len(task.schedule.DayOfWeek) == 0 {
go func() {
result := task.handler(task.ctx, task.payload)
task.executionHistory = append(task.executionHistory, ExecutionHistory{Timestamp: time.Now(), Result: result})
if task.config.Callback != nil {
_ = task.config.Callback(task.ctx, result)
}
fmt.Printf("Executed scheduled task: %s\n", task.payload.ID)
}()
}
}
func (s *Scheduler) AddTask(ctx context.Context, payload *Task, opts ...SchedulerOption) {
s.mu.Lock()
defer s.mu.Unlock()
// Create a default options instance
options := defaultSchedulerOptions()
for _, opt := range opts {
opt(options)
}
if options.Handler == nil {
options.Handler = s.pool.handler
}
if options.Callback == nil {
options.Callback = s.pool.callback
}
stop := make(chan struct{})
// Create a new ScheduledTask using the provided options
s.tasks = append(s.tasks, ScheduledTask{
ctx: ctx,
handler: options.Handler,
payload: payload,
stop: stop,
config: SchedulerConfig{
Callback: options.Callback,
Overlap: options.Overlap,
},
schedule: &Schedule{
Interval: options.Interval,
Recurring: options.Recurring,
},
})
// Start scheduling the task
go s.schedule(s.tasks[len(s.tasks)-1])
}
func (s *Scheduler) RemoveTask(payloadID string) {
s.mu.Lock()
defer s.mu.Unlock()
for i, task := range s.tasks {
if task.payload.ID == payloadID {
close(task.stop)
s.tasks = append(s.tasks[:i], s.tasks[i+1:]...)
break
}
}
}
type ExecutionHistory struct {
Timestamp time.Time
Result Result
}
func (s *Scheduler) PrintAllTasks() {
s.mu.Lock()
defer s.mu.Unlock()
fmt.Println("Scheduled Tasks:")
for _, task := range s.tasks {
fmt.Printf("Task ID: %s, Next Execution: %s\n", task.payload.ID, task.getNextRunTime(time.Now()))
}
}
func (s *Scheduler) PrintExecutionHistory(taskID string) {
s.mu.Lock()
defer s.mu.Unlock()
for _, task := range s.tasks {
if task.payload.ID == taskID {
fmt.Printf("Execution History for Task ID: %s\n", taskID)
for _, history := range task.executionHistory {
fmt.Printf("Timestamp: %s, Result: %v\n", history.Timestamp, history.Result.Error)
}
return
}
}
fmt.Printf("No task found with ID: %s\n", taskID)
}