Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

WithSingletonMode(gocron.LimitModeWait) not queuing #676

Closed
Shuri2060 opened this issue Feb 22, 2024 · 2 comments · Fixed by #686
Closed

WithSingletonMode(gocron.LimitModeWait) not queuing #676

Shuri2060 opened this issue Feb 22, 2024 · 2 comments · Fixed by #686
Labels
bug Something isn't working

Comments

@Shuri2060
Copy link

Describe the bug

WithSingletonMode(gocron.LimitModeWait) not queuing overlapping tasks, skipping instead

To Reproduce

Ran in VSCode

package math_test

import (
	"log"
	"testing"

	"github.com/go-co-op/gocron/v2"
)

func TestScheduler(t *testing.T) {
	log.Println(1)

	scheduler, _ := gocron.NewScheduler(
		gocron.WithLimitConcurrentJobs(1, gocron.LimitModeWait),
		gocron.WithGlobalJobOptions(
			gocron.WithSingletonMode(gocron.LimitModeWait),
		),
	)
	scheduler.NewJob(
		gocron.CronJob("1,2,3,4,5,7 * * * * *", true),
		gocron.NewTask(Callback),
	)

	scheduler.Start()
	select {} // wait forever
}

func Callback() {
	log.Println("A")
	x := 1
	for {
		x++
		if x > 3381203829 {
			break
		}
	}
	log.Println("B")
}

Output only 3 runs occurred:

2024/02/22 16:26:58 1
2024/02/22 16:27:01 A
2024/02/22 16:27:03 B
2024/02/22 16:27:04 A
2024/02/22 16:27:06 B
2024/02/22 16:27:07 A
2024/02/22 16:27:09 B

Version

v2
387cbe4

Expected behavior

A total of 6 runs of Callback to occur, although delayed from their start time due to the length of execution

@Shuri2060 Shuri2060 added the bug Something isn't working label Feb 22, 2024
@kj87au
Copy link

kj87au commented Mar 4, 2024

I think this would be expected behaviour given your setup.

`
func TestScheduler(t *testing.T) {
log.Println(1)

scheduler, _ := gocron.NewScheduler(
	gocron.WithLimitConcurrentJobs(1, gocron.LimitModeWait),         // Limit total jobs running to one
	gocron.WithGlobalJobOptions(
		gocron.WithSingletonMode(gocron.LimitModeWait),              // Limit total amount of individual jobs to one
	),
)
scheduler.NewJob(
	gocron.CronJob("1,2,3,4,5,7 * * * * *", true),                                    // Only run jobs on sec 1, 2, 3, 4, 5 & 7
	gocron.NewTask(Callback),
)

scheduler.Start()
select {} // wait forever

}`
Ignoring the double usage of LimitConcurrent with Singleton (as we only really have one job).

And looking at LimitModeWait
// LimitModeWait causes jobs reaching the limit set in
// WithLimitConcurrentJobs or WithSingletonMode to wait
// in a queue until a slot becomes available to run.
//
// Note: this mode can produce unpredictable results as
// job execution order isn't guaranteed. For example, a job that
// executes frequently may pile up in the wait queue and be executed
// many times back to back when the queue opens.
//
// Warning: do not use this mode if your jobs will continue to stack
// up beyond the ability of the limit workers to keep up. An example of
// what NOT to do:

The cron string only lets us run jobs during seconds 1,2,3,4,5 & 7 therefore these are our 6 "slots", If we have one running we will queue it.

Lets name the seconds Job1-Job7

  1. Q checked, No Jobs; Job1 starts Q=[]
  2. Job1 running; Job2 goes to start; gets deferred to Q. Q=[Job2]
  3. Job1 running; Job3 goes to start; gets deferred to Q; Job1 finishes. Q=[Job2, Job3]
  4. Next job is popped from queue, Job2 starts; Job4 goes to start, gets deferred to Q Q=[Job3, Job4]
  5. Job2 running; Job5 goes to start; gets deferred to Q. Q=[Job3, Job4, Job5]
  6. Nothing get started as cron string has no 6, Job2 finishes. Q=[Job3, Job4, Job5]
  7. Next job is popped from queue, Job3 starts; Job7 goes to start, gets differed to Q Q=[Job4, Job5, Job6]
  8. nothing happens as jobs don't run on second 8
  9. Job3 finishes; nothing happens as jobs don't run on second 9
    ... No other jobs can run until the next minute as per cron string. we will start pulling jobs from the Q once triggered.

I could be wrong though

@JohnRoesler
Copy link
Contributor

Would you be willing to validate the release candidate solves the issue? v2.2.5-rc1

Testing it locally, it does now run your job 6 times, and they will just be spread out depending on how much time you sleep. For example with a 2s sleep per run:

package main

import (
	"fmt"
	"log"
	"time"

	"github.com/go-co-op/gocron/v2"
)

func main() {
	scheduler, err := gocron.NewScheduler(
		gocron.WithGlobalJobOptions(
			gocron.WithSingletonMode(gocron.LimitModeWait),
		))
	if err != nil {
		fmt.Println("Failed to create new scheduler")
		fmt.Print(err)
		return
	}

	scheduler.NewJob(
		gocron.CronJob("1,2,3,4,5,7 * * * * *", true),
		gocron.NewTask(Callback),
	)

	scheduler.Start()
	select {
	// do something here to handle stopping your app
	}
}

func Callback() {
	log.Println("A")
	time.Sleep(2 * time.Second)
	log.Println("B")
}

Output:

2024/03/05 10:30:01 A
2024/03/05 10:30:03 B

2024/03/05 10:30:03 A
2024/03/05 10:30:05 B

2024/03/05 10:30:05 A
2024/03/05 10:30:07 B

2024/03/05 10:30:07 A
2024/03/05 10:30:09 B

2024/03/05 10:30:09 A
2024/03/05 10:30:11 B

2024/03/05 10:30:11 A
2024/03/05 10:30:13 B

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
bug Something isn't working
Projects
None yet
Development

Successfully merging a pull request may close this issue.

3 participants