gdgvda / cron

a cron library for go

Geek Repo:Geek Repo

Github PK Tool:Github PK Tool

remove Start() method

janrnc opened this issue · comments

Cron provides two similar methods: Run() and Start().

cron/cron.go

Lines 214 to 235 in 5eb0ef7

// Start the cron scheduler in its own goroutine, or no-op if already started.
func (c *Cron) Start() {
c.runningMu.Lock()
defer c.runningMu.Unlock()
if c.running {
return
}
c.running = true
go c.run()
}
// Run the cron scheduler, or no-op if already running.
func (c *Cron) Run() {
c.runningMu.Lock()
if c.running {
c.runningMu.Unlock()
return
}
c.running = true
c.runningMu.Unlock()
c.run()
}

I think that calling go cron.Run() the behavior would be the same as cron.Start(). IMO we could remove Start() and keep just the Run() method, letting the user decide the execution goroutine on its own.

I think they look similar, but they are not same as Start facilitates running cron scheduler in its own goroutine while Run()-> runs the cron scheduler in current goroutine.

Hi @rameshgkwd05,
I agree. What I'm proposing is a matter of API design. Assuming that calling go cron.Run() the behavior would be the same as using cron.Start(), IMHO exposing a single blocking method reduces the API surface and makes the usage more clear from a Go perspective. In order to run the cron scheduler in its own goroutine you would have to add the go keyword in front, as you would do for any other function. What do you think?

Hi @janfranchini
By looking at history at original cron repo, I saw that Run() was added later thats why it resonates partially with your point of confusion in API design. Originally, Start, Stop, Addfunc,.. interfaces were created and they go well with the cron scheduler running in go routine.

About keeping Start or Run, I would go with Start as we usually run cron job in background, so does the Start method.
Also it frees API consumers of worries of explicitly calling go Start() to start a cron job. It also help to write an easy go tests for the caller functions.

There is for sure a reasonable motivation behind the current API. We are starting from scratch in terms of stable releases and the intent, for now, is to review the current state of the code.

About keeping Start or Run, I would go with Start as we usually run cron job in background

This is a good point. I think too that the background usage would cover most (almost every IMO) of the cases. But as you said, cron.Run() was added later and this makes me think that this blocking feature was somehow wanted and appreciated. Unfortunately I didn't find any discussion motivating this change.

Now, the point is that of finding a trade-off between having a single exposed method and both use cases being simple to deal with.

  • Keeping only cron.Start(): in order to behave as blocking you would need to stop the execution after the scheduler started. See robfig/cron#143
  • Keeping only cron.Run(): in order to behave as non-blocking you would have to use go cron.Run().

This all said, both ways sound fine to me, I definitely appreciate the idea of providing what's going to be used most of the time. It's more a style decision. @Zavy86, @inalto, what are your preferences?

Once decided, @rameshgkwd05 would you like to open a PR for this?

A use case I didn't consider is that of stopping the scheduler when using the first option I listed with blocking behavior. select {} blocks forever, instead I would expect the execution to resume in case the scheduler is stopped. For making this work with the first option we would need some communication mechanism so that externally it's possible to wait this event instead of waiting indefinitely.

Using the second solution this behavior would be for free. This point makes me lean towards the second option.

While trying to remove the Start() method I realized a side effect of using go cron.Run().

cron/cron_test.go

Lines 93 to 112 in abf1acd

// Start, stop, then add an entry. Verify entry doesn't run.
func TestStopCausesJobsToNotRun(t *testing.T) {
wg := &sync.WaitGroup{}
wg.Add(1)
cron := newWithSeconds()
cron.Start()
cron.Stop()
_, err := cron.Add("* * * * * ?", func() { wg.Done() })
if err != nil {
t.Error("non-nil error")
}
select {
case <-time.After(OneSecond):
// No job ran!
case <-wait(wg):
t.Fatal("expected stopped cron does not run any job")
}
}

Consider the change when starting the scheduler in the above test: without goroutine synchronization, the scheduler could start after the Stop() and/or Add() calls, resulting in unpredictable behavior. I don't think adding extra complexity to this makes sense, so I'm closing for now.