ing
This commit is contained in:
parent
595f56aa7c
commit
6b4beb77fb
217
time/scheduler/scheduler.go
Normal file
217
time/scheduler/scheduler.go
Normal file
|
@ -0,0 +1,217 @@
|
|||
package scheduler
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"log"
|
||||
"os"
|
||||
"os/signal"
|
||||
"sync"
|
||||
"syscall"
|
||||
"time"
|
||||
|
||||
"git.loafle.net/overflow/probe/service/scheduler/storage"
|
||||
"git.loafle.net/overflow/probe/service/scheduler/task"
|
||||
)
|
||||
|
||||
type Scheduler struct {
|
||||
taskTargetRegistry *task.TaskTargetRegistry
|
||||
|
||||
tasks sync.Map
|
||||
adapter *storageAdapter
|
||||
|
||||
stopChan chan bool
|
||||
}
|
||||
|
||||
// New will return a new instance of the Scheduler struct.
|
||||
func New(taskStorage storage.TaskStorage) *Scheduler {
|
||||
taskTargetRegistry := &task.TaskTargetRegistry{}
|
||||
|
||||
return &Scheduler{
|
||||
taskTargetRegistry: taskTargetRegistry,
|
||||
stopChan: make(chan bool),
|
||||
adapter: &storageAdapter{
|
||||
taskStorage: taskStorage,
|
||||
taskTargetRegistry: taskTargetRegistry,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
// RunAt will schedule function to be executed once at the given time.
|
||||
func (s *Scheduler) RunAt(time time.Time, targetFunc interface{}, params ...interface{}) (string, error) {
|
||||
taskTarget, err := s.taskTargetRegistry.Add(targetFunc)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
task := task.New(taskTarget, params)
|
||||
task.NextRun = time
|
||||
s.registerTask(task)
|
||||
|
||||
return task.Hash(), nil
|
||||
}
|
||||
|
||||
// RunAfter executes function once after a specific duration has elapsed.
|
||||
func (s *Scheduler) RunAfter(duration time.Duration, targetFunc interface{}, params ...interface{}) (string, error) {
|
||||
return s.RunAt(time.Now().Add(duration), targetFunc, params...)
|
||||
}
|
||||
|
||||
// RunEvery will schedule function to be executed every time the duration has elapsed.
|
||||
func (s *Scheduler) RunEvery(duration time.Duration, targetFunc interface{}, params ...interface{}) (string, error) {
|
||||
taskTarget, err := s.taskTargetRegistry.Add(targetFunc)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
task := task.New(taskTarget, params)
|
||||
task.IsRecurring = true
|
||||
task.Duration = duration
|
||||
task.NextRun = time.Now().Add(duration)
|
||||
s.registerTask(task)
|
||||
|
||||
return task.Hash(), nil
|
||||
}
|
||||
|
||||
// Start will run the scheduler's timer and will trigger the execution
|
||||
// of tasks depending on their schedule.
|
||||
func (s *Scheduler) Start() error {
|
||||
sigChan := make(chan os.Signal, 1)
|
||||
signal.Notify(sigChan, syscall.SIGINT, syscall.SIGTERM)
|
||||
|
||||
// Populate tasks from storage
|
||||
if err := s.populateTasks(); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := s.persistRegisteredTasks(); err != nil {
|
||||
return err
|
||||
}
|
||||
s.runPending()
|
||||
|
||||
go func() {
|
||||
ticker := time.NewTicker(1 * time.Second)
|
||||
for {
|
||||
select {
|
||||
case <-ticker.C:
|
||||
s.runPending()
|
||||
case <-sigChan:
|
||||
s.stopChan <- true
|
||||
case <-s.stopChan:
|
||||
close(s.stopChan)
|
||||
}
|
||||
}
|
||||
}()
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// Stop will put the scheduler to halt
|
||||
func (s *Scheduler) Stop() {
|
||||
s.stopChan <- true
|
||||
}
|
||||
|
||||
// Wait is a convenience function for blocking until the scheduler is stopped.
|
||||
func (s *Scheduler) Wait() {
|
||||
<-s.stopChan
|
||||
}
|
||||
|
||||
// Cancel is used to cancel the planned execution of a specific task using it's ID.
|
||||
// The ID is returned when the task was scheduled using RunAt, RunAfter or RunEvery
|
||||
func (s *Scheduler) Cancel(taskID string) error {
|
||||
t, ok := s.tasks.Load(taskID)
|
||||
if !ok {
|
||||
return fmt.Errorf("Task not found")
|
||||
}
|
||||
|
||||
s.adapter.Remove(t.(*task.Task))
|
||||
s.tasks.Delete(taskID)
|
||||
return nil
|
||||
}
|
||||
|
||||
// Clear will cancel the execution and clear all registered tasks.
|
||||
func (s *Scheduler) Clear() {
|
||||
s.tasks.Range(func(_taskID, _task interface{}) bool {
|
||||
s.adapter.Remove(_task.(*task.Task))
|
||||
s.tasks.Delete(_taskID.(string))
|
||||
return true
|
||||
})
|
||||
|
||||
s.taskTargetRegistry = &task.TaskTargetRegistry{}
|
||||
}
|
||||
|
||||
func (s *Scheduler) populateTasks() error {
|
||||
tasks, err := s.adapter.Fetch()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
for _, _task := range tasks {
|
||||
// If we can't find the function, it's been changed/removed by user
|
||||
exists := s.taskTargetRegistry.Exists(_task.TaskTarget.Name)
|
||||
if !exists {
|
||||
log.Printf("%s was not found, it will be removed\n", _task.TaskTarget.Name)
|
||||
_ = s.adapter.Remove(_task)
|
||||
continue
|
||||
}
|
||||
|
||||
// If the task instance is still registered with the same computed hash then move on.
|
||||
// Otherwise, one of the attributes changed and therefore, the task instance should
|
||||
// be added to the list of tasks to be executed with the stored params
|
||||
_eTask, ok := s.tasks.Load(_task.Hash())
|
||||
if !ok {
|
||||
log.Printf("Detected a change in attributes of one of the instances of task %s, \n",
|
||||
_task.TaskTarget.Name)
|
||||
_task.TaskTarget, _ = s.taskTargetRegistry.Get(_task.TaskTarget.Name)
|
||||
_eTask = _task
|
||||
s.tasks.Store(_task.Hash(), _eTask)
|
||||
}
|
||||
eTask := _eTask.(*task.Task)
|
||||
|
||||
// Skip task which is not a recurring one and the NextRun has already passed
|
||||
if !_task.IsRecurring && _task.NextRun.Before(time.Now()) {
|
||||
// We might have a task instance which was executed already.
|
||||
// In this case, delete it.
|
||||
_ = s.adapter.Remove(_task)
|
||||
s.tasks.Delete(_task.Hash())
|
||||
continue
|
||||
}
|
||||
|
||||
// Duration may have changed for recurring tasks
|
||||
if _task.IsRecurring && eTask.Duration != _task.Duration {
|
||||
// Reschedule NextRun based on _task.LastRun + registeredTask.Duration
|
||||
eTask.NextRun = _task.LastRun.Add(eTask.Duration)
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *Scheduler) persistRegisteredTasks() error {
|
||||
var err error
|
||||
s.tasks.Range(func(_taskID, _task interface{}) bool {
|
||||
err = s.adapter.Add(_task.(*task.Task))
|
||||
if nil != err {
|
||||
return false
|
||||
}
|
||||
return true
|
||||
})
|
||||
|
||||
return err
|
||||
}
|
||||
|
||||
func (s *Scheduler) runPending() {
|
||||
s.tasks.Range(func(_taskID, _task interface{}) bool {
|
||||
t := _task.(*task.Task)
|
||||
if t.IsDue() {
|
||||
go t.Run()
|
||||
if !t.IsRecurring {
|
||||
s.adapter.Remove(t)
|
||||
s.tasks.Delete(t.Hash())
|
||||
}
|
||||
}
|
||||
return true
|
||||
})
|
||||
}
|
||||
|
||||
func (s *Scheduler) registerTask(task *task.Task) {
|
||||
s.taskTargetRegistry.Add(task.TaskTarget)
|
||||
|
||||
s.tasks.Store(task.Hash(), task)
|
||||
}
|
41
time/scheduler/scheduler_test.go
Normal file
41
time/scheduler/scheduler_test.go
Normal file
|
@ -0,0 +1,41 @@
|
|||
package scheduler_test
|
||||
|
||||
import (
|
||||
"log"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"git.loafle.net/overflow/probe/service/scheduler"
|
||||
"git.loafle.net/overflow/probe/service/scheduler/storage"
|
||||
)
|
||||
|
||||
func TestDiscoverPort(t *testing.T) {
|
||||
_storage := storage.NewMemoryStorage()
|
||||
s := scheduler.New(_storage)
|
||||
|
||||
// Start a task with arguments
|
||||
tID1, err := s.RunEvery(1*time.Second, TaskWithArgs, "Hello from recurring task 1")
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
|
||||
// Start the same task as above with a different argument
|
||||
_, err = s.RunEvery(4*time.Second, TaskWithArgs, "Hello from recurring task 2")
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
s.Start()
|
||||
|
||||
time.Sleep(4 * time.Second)
|
||||
s.Cancel(tID1)
|
||||
|
||||
s.Wait()
|
||||
}
|
||||
|
||||
func TaskWithoutArgs() {
|
||||
log.Println("TaskWithoutArgs is executed")
|
||||
}
|
||||
|
||||
func TaskWithArgs(message string) {
|
||||
log.Println("TaskWithArgs is executed. message:", message)
|
||||
}
|
141
time/scheduler/storage.go
Normal file
141
time/scheduler/storage.go
Normal file
|
@ -0,0 +1,141 @@
|
|||
package scheduler
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"reflect"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"git.loafle.net/overflow/probe/service/scheduler/storage"
|
||||
"git.loafle.net/overflow/probe/service/scheduler/task"
|
||||
)
|
||||
|
||||
type storageAdapter struct {
|
||||
taskStorage storage.TaskStorage
|
||||
taskTargetRegistry *task.TaskTargetRegistry
|
||||
}
|
||||
|
||||
func (a *storageAdapter) Add(task *task.Task) error {
|
||||
ta, err := a.getTaskAttribute(task)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return a.taskStorage.Add(ta)
|
||||
}
|
||||
|
||||
func (a *storageAdapter) Fetch() ([]*task.Task, error) {
|
||||
tas, err := a.taskStorage.Fetch()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
var tasks []*task.Task
|
||||
for _, ta := range tas {
|
||||
lastRun, err := time.Parse(time.RFC3339, ta.LastRun)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
nextRun, err := time.Parse(time.RFC3339, ta.NextRun)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
duration, err := time.ParseDuration(ta.Duration)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
isRecurring, err := strconv.Atoi(ta.IsRecurring)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
taskTarget, err := a.taskTargetRegistry.Get(ta.Name)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
params, err := paramsFromString(taskTarget, ta.Params)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
t := task.NewWithSchedule(taskTarget, params, &task.TaskSchedule{
|
||||
IsRecurring: isRecurring == 1,
|
||||
Duration: time.Duration(duration),
|
||||
LastRun: lastRun,
|
||||
NextRun: nextRun,
|
||||
})
|
||||
tasks = append(tasks, t)
|
||||
}
|
||||
return tasks, nil
|
||||
}
|
||||
|
||||
func (a *storageAdapter) Remove(task *task.Task) error {
|
||||
ta, err := a.getTaskAttribute(task)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return a.taskStorage.Remove(ta)
|
||||
}
|
||||
|
||||
func (a *storageAdapter) getTaskAttribute(task *task.Task) (*storage.TaskAttribute, error) {
|
||||
params, err := paramsToString(task.Params)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
isRecurring := 0
|
||||
if task.IsRecurring {
|
||||
isRecurring = 1
|
||||
}
|
||||
|
||||
return &storage.TaskAttribute{
|
||||
Hash: string(task.Hash()),
|
||||
Name: task.TaskTarget.Name,
|
||||
LastRun: task.LastRun.Format(time.RFC3339),
|
||||
NextRun: task.NextRun.Format(time.RFC3339),
|
||||
Duration: task.Duration.String(),
|
||||
IsRecurring: strconv.Itoa(isRecurring),
|
||||
Params: params,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func paramsToString(params []interface{}) (string, error) {
|
||||
var paramsList []string
|
||||
for _, param := range params {
|
||||
paramStr, err := json.Marshal(param)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
paramsList = append(paramsList, string(paramStr))
|
||||
}
|
||||
data, err := json.Marshal(paramsList)
|
||||
return string(data), err
|
||||
}
|
||||
|
||||
func paramsFromString(taskTarget *task.TaskTarget, payload string) ([]interface{}, error) {
|
||||
var params []interface{}
|
||||
if strings.TrimSpace(payload) == "" {
|
||||
return params, nil
|
||||
}
|
||||
paramTypes := taskTarget.Params()
|
||||
var paramsStrings []string
|
||||
err := json.Unmarshal([]byte(payload), ¶msStrings)
|
||||
if err != nil {
|
||||
return params, err
|
||||
}
|
||||
for i, paramStr := range paramsStrings {
|
||||
paramType := paramTypes[i]
|
||||
target := reflect.New(paramType)
|
||||
err := json.Unmarshal([]byte(paramStr), target.Interface())
|
||||
if err != nil {
|
||||
return params, err
|
||||
}
|
||||
param := reflect.Indirect(target).Interface()
|
||||
params = append(params, param)
|
||||
}
|
||||
|
||||
return params, nil
|
||||
}
|
35
time/scheduler/storage/memory.go
Normal file
35
time/scheduler/storage/memory.go
Normal file
|
@ -0,0 +1,35 @@
|
|||
package storage
|
||||
|
||||
// MemoryStorage is a memory task store
|
||||
type MemoryStorage struct {
|
||||
taskAttributes []*TaskAttribute
|
||||
}
|
||||
|
||||
// NewMemoryStorage returns an instance of MemoryStorage.
|
||||
func NewMemoryStorage() *MemoryStorage {
|
||||
return &MemoryStorage{}
|
||||
}
|
||||
|
||||
// Add adds a task to the memory store.
|
||||
func (s *MemoryStorage) Add(taskAttribute *TaskAttribute) error {
|
||||
s.taskAttributes = append(s.taskAttributes, taskAttribute)
|
||||
return nil
|
||||
}
|
||||
|
||||
// Fetch will return all tasks stored.
|
||||
func (s *MemoryStorage) Fetch() ([]*TaskAttribute, error) {
|
||||
return s.taskAttributes, nil
|
||||
}
|
||||
|
||||
// Remove will remove task from store
|
||||
func (s *MemoryStorage) Remove(taskAttribute *TaskAttribute) error {
|
||||
var newTaskAttributes []*TaskAttribute
|
||||
for _, eTaskAttribute := range s.taskAttributes {
|
||||
if taskAttribute.Hash == eTaskAttribute.Hash {
|
||||
continue
|
||||
}
|
||||
newTaskAttributes = append(newTaskAttributes, eTaskAttribute)
|
||||
}
|
||||
s.taskAttributes = newTaskAttributes
|
||||
return nil
|
||||
}
|
18
time/scheduler/storage/storage.go
Normal file
18
time/scheduler/storage/storage.go
Normal file
|
@ -0,0 +1,18 @@
|
|||
package storage
|
||||
|
||||
type TaskAttribute struct {
|
||||
Hash string
|
||||
Name string
|
||||
LastRun string
|
||||
NextRun string
|
||||
Duration string
|
||||
IsRecurring string
|
||||
Params string
|
||||
}
|
||||
|
||||
// TaskStorage is the interface to implement when adding custom task storage.
|
||||
type TaskStorage interface {
|
||||
Add(*TaskAttribute) error
|
||||
Fetch() ([]*TaskAttribute, error)
|
||||
Remove(*TaskAttribute) error
|
||||
}
|
81
time/scheduler/task/registry.go
Normal file
81
time/scheduler/task/registry.go
Normal file
|
@ -0,0 +1,81 @@
|
|||
package task
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"reflect"
|
||||
"runtime"
|
||||
"sync"
|
||||
)
|
||||
|
||||
type TaskTarget struct {
|
||||
Name string
|
||||
targetFunc interface{}
|
||||
paramTypes map[string]reflect.Type
|
||||
}
|
||||
|
||||
// Params returns the list of parameter types
|
||||
func (t *TaskTarget) Params() []reflect.Type {
|
||||
ft := reflect.TypeOf(t.targetFunc)
|
||||
paramTypes := make([]reflect.Type, ft.NumIn())
|
||||
for idx := 0; idx < ft.NumIn(); idx++ {
|
||||
in := ft.In(idx)
|
||||
paramTypes[idx] = in
|
||||
}
|
||||
return paramTypes
|
||||
}
|
||||
|
||||
type TaskTargetRegistry struct {
|
||||
targetFuncs sync.Map
|
||||
}
|
||||
|
||||
// Add appends the function to the registry after resolving specific information about this function.
|
||||
func (r *TaskTargetRegistry) Add(targetFunc interface{}) (*TaskTarget, error) {
|
||||
fv := reflect.ValueOf(targetFunc)
|
||||
if fv.Kind() != reflect.Func {
|
||||
return nil, fmt.Errorf("Provided function value is not an actual function")
|
||||
}
|
||||
|
||||
name := runtime.FuncForPC(fv.Pointer()).Name()
|
||||
funcInstance, err := r.Get(name)
|
||||
if err == nil {
|
||||
return funcInstance, nil
|
||||
}
|
||||
|
||||
taskTarget := &TaskTarget{
|
||||
Name: name,
|
||||
targetFunc: targetFunc,
|
||||
paramTypes: r.resolveParamTypes(targetFunc),
|
||||
}
|
||||
|
||||
r.targetFuncs.Store(name, taskTarget)
|
||||
|
||||
return taskTarget, nil
|
||||
}
|
||||
|
||||
// Get returns the TaskTarget instance which holds all information about any single registered task function.
|
||||
func (r *TaskTargetRegistry) Get(name string) (*TaskTarget, error) {
|
||||
taskTarget, ok := r.targetFuncs.Load(name)
|
||||
if ok {
|
||||
return taskTarget.(*TaskTarget), nil
|
||||
}
|
||||
return nil, fmt.Errorf("Function %s not found", name)
|
||||
}
|
||||
|
||||
// Exists checks if a function with provided name exists.
|
||||
func (r *TaskTargetRegistry) Exists(name string) bool {
|
||||
_, ok := r.targetFuncs.Load(name)
|
||||
if ok {
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func (r *TaskTargetRegistry) resolveParamTypes(targetFunc interface{}) map[string]reflect.Type {
|
||||
paramTypes := make(map[string]reflect.Type)
|
||||
funcType := reflect.TypeOf(targetFunc)
|
||||
for idx := 0; idx < funcType.NumIn(); idx++ {
|
||||
in := funcType.In(idx)
|
||||
paramTypes[in.Name()] = in
|
||||
}
|
||||
return paramTypes
|
||||
}
|
79
time/scheduler/task/task.go
Normal file
79
time/scheduler/task/task.go
Normal file
|
@ -0,0 +1,79 @@
|
|||
package task
|
||||
|
||||
import (
|
||||
"crypto/sha1"
|
||||
"fmt"
|
||||
"io"
|
||||
"reflect"
|
||||
"time"
|
||||
)
|
||||
|
||||
type TaskSchedule struct {
|
||||
IsRecurring bool
|
||||
LastRun time.Time
|
||||
NextRun time.Time
|
||||
Duration time.Duration
|
||||
}
|
||||
|
||||
type Task struct {
|
||||
*TaskSchedule
|
||||
TaskTarget *TaskTarget
|
||||
Params []interface{}
|
||||
}
|
||||
|
||||
// New returns an instance of task
|
||||
func New(taskTarget *TaskTarget, params []interface{}) *Task {
|
||||
return &Task{
|
||||
TaskSchedule: &TaskSchedule{},
|
||||
TaskTarget: taskTarget,
|
||||
Params: params,
|
||||
}
|
||||
}
|
||||
|
||||
// NewWithSchedule creates an instance of task with the provided schedule information
|
||||
func NewWithSchedule(taskTarget *TaskTarget, params []interface{}, taskSchedule *TaskSchedule) *Task {
|
||||
return &Task{
|
||||
TaskSchedule: taskSchedule,
|
||||
TaskTarget: taskTarget,
|
||||
Params: params,
|
||||
}
|
||||
}
|
||||
|
||||
// IsDue returns a boolean indicating whether the task should execute or not
|
||||
func (t *Task) IsDue() bool {
|
||||
timeNow := time.Now()
|
||||
return timeNow == t.NextRun || timeNow.After(t.NextRun)
|
||||
}
|
||||
|
||||
// Run will execute the task and schedule it's next run.
|
||||
func (t *Task) Run() {
|
||||
// Reschedule task first to prevent running the task
|
||||
// again in case the execution time takes more than the
|
||||
// task's duration value.
|
||||
t.scheduleNextRun()
|
||||
|
||||
fv := reflect.ValueOf(t.TaskTarget.targetFunc)
|
||||
params := make([]reflect.Value, len(t.Params))
|
||||
for i, param := range t.Params {
|
||||
params[i] = reflect.ValueOf(param)
|
||||
}
|
||||
fv.Call(params)
|
||||
}
|
||||
|
||||
// Hash will return the SHA1 representation of the task's data.
|
||||
func (t *Task) Hash() string {
|
||||
hash := sha1.New()
|
||||
_, _ = io.WriteString(hash, t.TaskTarget.Name)
|
||||
_, _ = io.WriteString(hash, fmt.Sprintf("%+v", t.Params))
|
||||
_, _ = io.WriteString(hash, fmt.Sprintf("%s", t.TaskSchedule.Duration))
|
||||
_, _ = io.WriteString(hash, fmt.Sprintf("%t", t.TaskSchedule.IsRecurring))
|
||||
return fmt.Sprintf("%x", hash.Sum(nil))
|
||||
}
|
||||
|
||||
func (t *Task) scheduleNextRun() {
|
||||
if !t.IsRecurring {
|
||||
return
|
||||
}
|
||||
t.LastRun = t.NextRun
|
||||
t.NextRun = t.NextRun.Add(t.Duration)
|
||||
}
|
Loading…
Reference in New Issue
Block a user