feat: add cron
This commit is contained in:
		
							parent
							
								
									4b4d3b743d
								
							
						
					
					
						commit
						5e31d27c54
					
				
							
								
								
									
										160
									
								
								pkg/cron/cron.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										160
									
								
								pkg/cron/cron.go
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,160 @@
 | 
			
		||||
package cron
 | 
			
		||||
 | 
			
		||||
import (
 | 
			
		||||
	"context"
 | 
			
		||||
	"fmt"
 | 
			
		||||
	"math/rand"
 | 
			
		||||
	"strconv"
 | 
			
		||||
	"strings"
 | 
			
		||||
	"time"
 | 
			
		||||
 | 
			
		||||
	"github.com/sour-is/ev/internal/lg"
 | 
			
		||||
	"github.com/sour-is/ev/pkg/locker"
 | 
			
		||||
	"github.com/sour-is/ev/pkg/set"
 | 
			
		||||
	"go.opentelemetry.io/otel/attribute"
 | 
			
		||||
	"golang.org/x/sync/errgroup"
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
type task func(context.Context, time.Time) error
 | 
			
		||||
type job struct {
 | 
			
		||||
	Month, Weekday, Day,
 | 
			
		||||
	Hour, Minute, Second *set.BoundSet[int8]
 | 
			
		||||
	Task task
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
var DefaultGranularity = time.Minute
 | 
			
		||||
 | 
			
		||||
type state struct {
 | 
			
		||||
	queue []task
 | 
			
		||||
}
 | 
			
		||||
type cron struct {
 | 
			
		||||
	jobs        []job
 | 
			
		||||
	state       *locker.Locked[state]
 | 
			
		||||
	granularity time.Duration
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func New(granularity time.Duration) *cron {
 | 
			
		||||
	return &cron{granularity: granularity, state: locker.New(&state{})}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func parseInto(c string, s *set.BoundSet[int8]) *set.BoundSet[int8] {
 | 
			
		||||
	if c == "*" || c == "" {
 | 
			
		||||
		s.AddRange(0, 100)
 | 
			
		||||
	}
 | 
			
		||||
	for _, split := range strings.Split(c, ",") {
 | 
			
		||||
		minmax := strings.SplitN(split, "-", 2)
 | 
			
		||||
		switch len(minmax) {
 | 
			
		||||
		case 2:
 | 
			
		||||
			min, _ := strconv.ParseInt(minmax[0], 10, 8)
 | 
			
		||||
			max, _ := strconv.ParseInt(minmax[1], 10, 8)
 | 
			
		||||
			s.AddRange(int8(min), int8(max))
 | 
			
		||||
		default:
 | 
			
		||||
			min, _ := strconv.ParseInt(minmax[0], 10, 8)
 | 
			
		||||
			s.Add(int8(min))
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
	return s
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// This function creates a new job that occurs at the given day and the given
 | 
			
		||||
// 24hour time. Any of the values may be -1 as an "any" match, so passing in
 | 
			
		||||
// a day of -1, the event occurs every day; passing in a second value of -1, the
 | 
			
		||||
// event will fire every second that the other parameters match.
 | 
			
		||||
func (c *cron) NewJob(expr string, task task) {
 | 
			
		||||
	sp := append(strings.Fields(expr), make([]string, 5)...)[:5]
 | 
			
		||||
 | 
			
		||||
	job := job{
 | 
			
		||||
		Month:   parseInto(sp[4], set.NewBoundSet[int8](1, 12)),
 | 
			
		||||
		Weekday: parseInto(sp[3], set.NewBoundSet[int8](0, 6)),
 | 
			
		||||
		Day:     parseInto(sp[2], set.NewBoundSet[int8](1, 31)),
 | 
			
		||||
		Hour:    parseInto(sp[1], set.NewBoundSet[int8](0, 23)),
 | 
			
		||||
		Minute:  parseInto(sp[0], set.NewBoundSet[int8](0, 59)),
 | 
			
		||||
		Task:    task,
 | 
			
		||||
	}
 | 
			
		||||
	c.jobs = append(c.jobs, job)
 | 
			
		||||
}
 | 
			
		||||
func (c *cron) Once(ctx context.Context, once task) {
 | 
			
		||||
	c.state.Modify(ctx, func(ctx context.Context, state *state) error {
 | 
			
		||||
		state.queue = append(state.queue, once)
 | 
			
		||||
		return nil
 | 
			
		||||
	})
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (cj job) Matches(t time.Time) (ok bool) {
 | 
			
		||||
	return cj.Month.Has(int8(t.Month())) &&
 | 
			
		||||
		cj.Day.Has(int8(t.Day())) &&
 | 
			
		||||
		cj.Weekday.Has(int8(t.Weekday()%7)) &&
 | 
			
		||||
		cj.Hour.Has(int8(t.Hour())) &&
 | 
			
		||||
		cj.Minute.Has(int8(t.Minute()))
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (cj job) String() string {
 | 
			
		||||
	return fmt.Sprintf("job[\n m:%s\n h:%s\n d:%s\n w:%s\n M:%s\n]", cj.Minute, cj.Hour, cj.Day, cj.Weekday, cj.Month)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (c *cron) Run(ctx context.Context) error {
 | 
			
		||||
	tick := time.NewTicker(c.granularity)
 | 
			
		||||
	defer tick.Stop()
 | 
			
		||||
 | 
			
		||||
	go c.run(ctx, time.Now())
 | 
			
		||||
 | 
			
		||||
	for {
 | 
			
		||||
		select {
 | 
			
		||||
		case <-ctx.Done():
 | 
			
		||||
			return nil
 | 
			
		||||
		case now := <-tick.C:
 | 
			
		||||
			// fmt.Println(now.Second(), now.Hour(), now.Day(), int8(now.Weekday()), uint8(now.Month()))
 | 
			
		||||
			go c.run(ctx, now)
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (c *cron) run(ctx context.Context, now time.Time) {
 | 
			
		||||
	var run []task
 | 
			
		||||
	ctx, span := lg.Span(ctx)
 | 
			
		||||
	defer span.End()
 | 
			
		||||
 | 
			
		||||
	// Add Jitter
 | 
			
		||||
	timer := time.NewTimer(time.Duration(rand.Intn(300)) * time.Millisecond)
 | 
			
		||||
	select {
 | 
			
		||||
	case <-ctx.Done():
 | 
			
		||||
		timer.Stop()
 | 
			
		||||
		return
 | 
			
		||||
	case <-timer.C:
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	span.AddEvent("Cron Run: " + now.Format(time.RFC822))
 | 
			
		||||
	// fmt.Println("Cron Run: ", now.Format(time.RFC822))
 | 
			
		||||
 | 
			
		||||
	c.state.Modify(ctx, func(ctx context.Context, state *state) error {
 | 
			
		||||
		run = append(run, state.queue...)
 | 
			
		||||
		state.queue = state.queue[:0]
 | 
			
		||||
 | 
			
		||||
		return nil
 | 
			
		||||
	})
 | 
			
		||||
 | 
			
		||||
	for _, j := range c.jobs {
 | 
			
		||||
		if j.Matches(now) {
 | 
			
		||||
			span.AddEvent(j.String())
 | 
			
		||||
			run = append(run, j.Task)
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	if len(run) == 0 {
 | 
			
		||||
		return
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	wg, _ := errgroup.WithContext(ctx)
 | 
			
		||||
 | 
			
		||||
	for i := range run {
 | 
			
		||||
		fn := run[i]
 | 
			
		||||
		wg.Go(func() error { return fn(ctx, now) })
 | 
			
		||||
	}
 | 
			
		||||
	span.SetAttributes(
 | 
			
		||||
		attribute.String("tick", now.String()),
 | 
			
		||||
		attribute.Int("count", len(run)),
 | 
			
		||||
	)
 | 
			
		||||
 | 
			
		||||
	err := wg.Wait()
 | 
			
		||||
	span.RecordError(err)
 | 
			
		||||
}
 | 
			
		||||
		Loading…
	
	
			
			x
			
			
		
	
		Reference in New Issue
	
	Block a user