0

I want to create a scheduler so that it executes a task every second for example, but also would like to have and http interface to stop/start the scheduler and get more stats/info, after reading more about timers & tickers, channels and gorutines I came out with this:

https://gist.github.com/nbari/483c5b382c795bf290b5

package main

import (
    "fmt"
    "log"
    "net/http"
    "time"
)

var timer *time.Ticker

func scheduler(seconds time.Duration) *time.Ticker {
    ticker := time.NewTicker(seconds * time.Second)
    go func() {
        for t := range ticker.C {
            // do stuff
            fmt.Println(t)
        }
    }()
    return ticker
}

func Start(timer *time.Ticker) http.Handler {
    return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
        timer = scheduler(1)
        w.Write([]byte("Starting scheduler"))
    })
}

func Stop(timer *time.Ticker) http.Handler {
    return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
        timer.Stop()
        w.Write([]byte("Stoping scheduler"))
    })
}

func main() {
    timer = scheduler(1)
    http.Handle("/start", Start(timer))
    http.Handle("/stop", Stop(timer))
    log.Fatal(http.ListenAndServe(":8080", nil))
}

The above code is working but I have a global "timer" variable, I would like to know if there is a better way to implement this and also a way for handle more than 1 scheduler, currently thinking on probably implementing kind of a container for all the scheduler but would like to have some feedbacks that could help me find clever solutions.

nbari
  • 25,603
  • 10
  • 76
  • 131

1 Answers1

0

Yes, there is a better way:

You should use channels here, and, as you suggested, some data structure to hold more schedulers.

I came up with this, it's a most basic working example of what I'd do:

package main

import (
    "errors"
    "fmt"
    "sync"
    "time"
)

// a scheduler runs f on every receive on t and exits when receiving from quit is non-blocking
type scheduler struct {
    t    <-chan time.Time
    quit chan struct{}
    f    func()
}

// a schedulerPool holds multiple schedulers
type schedulerPool struct {
    schedulers map[int]scheduler //I used a map here so you can use more clever keys
    counter    int
    mut        sync.Mutex
}

func newPool() *schedulerPool {
    return &schedulerPool{
        schedulers: make(map[int]scheduler),
    }
}

// start adds and starts a new scheduler that will execute f every interval.
// It returns the generated ID for the scheduler, or an error.
func (p *schedulerPool) start(interval time.Duration, f func()) (int, error) {
    p.mut.Lock()
    defer p.mut.Unlock()
    index := p.counter
    p.counter++

    if _, ok := p.schedulers[index]; ok {
        return 0, errors.New("key already in use")
    }

    sched := scheduler{
        t:    time.NewTicker(interval).C,
        quit: make(chan struct{}),
        f:    f,
    }

    p.schedulers[index] = sched
    go func() {
        for {
            select {
            case <-sched.t:
                sched.f()
            case <-sched.quit:
                return
            }
        }
    }()
    return index, nil
}

// stop stops the scheduler with the given ID.
func (p *schedulerPool) stop(index int) error {
    p.mut.Lock()
    defer p.mut.Unlock()

    sched, ok := p.schedulers[index]
    if !ok {
        return errors.New("does not exist")
    }

    close(sched.quit)
    return nil
}

func main() {
    // get a new pool
    pool := newPool()

    // start a scheduler
    idx1, err := pool.start(time.Second, func() { fmt.Println("hello 1") })
    if err != nil {
        panic(err)
    }

    // start another scheduler
    idx2, err := pool.start(time.Second, func() { fmt.Println("hello 2") })
    if err != nil {
        panic(err)
    }

    // wait some time
    time.Sleep(3 * time.Second)

    // stop the second scheduler
    err = pool.stop(idx2)
    if err != nil {
        panic(err)
    }

    // wait some more
    time.Sleep(3 * time.Second)

    // stop the first scheduler
    err = pool.stop(idx1)
    if err != nil {
        panic(err)
    }
}

Check it out on the playground.

Note that there is no guarantee that the first scheduler will run six times in total and the second one three times.

Also note a few other things I've done:

  • Map access has to be synchronized
  • Generating IDs must be synchronized
  • You should check if the key is actually in the map
  • Use channels to start, stop and control schedulers (for example, add a channel beep to have a scheduler dump something to the console (like "beep"))
  • Define a type for the exact signature of function you want to execute, for example type schedFunc func(int, int)

Obviously, you need to wrap that stuff inside of http handlers, probably take the ID or parameters from the querystring, or some other fancy stuff. This is just for demonstration.

mrd0ll4r
  • 866
  • 5
  • 12
  • Many thanks for your answer, but if I am right time.NewTicker returns a channel and I think I am currently using them here: https://gist.github.com/nbari/483c5b382c795bf290b5#file-scheduler-go-L15, also why not use timer.Stop? what is the difference of creating another channel (quit) to stop the timer ? – nbari Oct 29 '15 at 13:04
  • 1
    @nbari: from the docs for Ticker and Timer: "Stop does not close the channel, to prevent a read from the channel succeeding incorrectly". Your for range loop will never exit. – JimB Oct 29 '15 at 13:06
  • I am a little confuse, since in the tick_test.go they use ``Stop()`` and seems to be working, please check https://golang.org/src/time/tick_test.go lines 28-35, basically I don't understand why I need a control channel (quit) just for stoping a previous opened channel, could you please elaborate more on this, thanks in advance. – nbari Oct 30 '15 at 10:21
  • Hi, I after reading this answer http://stackoverflow.com/questions/17797754/ticker-stop-behaviour-in-golang I got it better, if changing the int on the map to a string could help to avoid the "sync" or why indeed the for synchronising using ``mut.Lock/Unlock`` ? – nbari Oct 30 '15 at 11:10
  • No. Map access has to be synchronized, maps are not thread-safe. – mrd0ll4r Oct 30 '15 at 23:31