mirror of https://github.com/go-co-op/gocron.git
elector & locker were failing to send out when not leader (#688)
* elector & locker were failing to send out when not leader * update test to confirm non-active elector/locker are checked * clean up data race * try to make test more reliable
This commit is contained in:
parent
c2f95759d2
commit
ebec5e9f91
|
|
@ -66,6 +66,11 @@ func main() {
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
|
## Examples
|
||||||
|
|
||||||
|
- [Go doc examples](https://pkg.go.dev/github.com/go-co-op/gocron/v2#pkg-examples)
|
||||||
|
- [Examples directory](examples)
|
||||||
|
|
||||||
## Concepts
|
## Concepts
|
||||||
|
|
||||||
- **Job**: The job encapsulates a "task", which is made up of a go function and any function parameters. The Job then
|
- **Job**: The job encapsulates a "task", which is made up of a go function and any function parameters. The Job then
|
||||||
|
|
|
||||||
|
|
@ -516,7 +516,7 @@ func ExampleWithClock() {
|
||||||
}
|
}
|
||||||
|
|
||||||
func ExampleWithDistributedElector() {
|
func ExampleWithDistributedElector() {
|
||||||
//var _ Elector = (*myElector)(nil)
|
//var _ gocron.Elector = (*myElector)(nil)
|
||||||
//
|
//
|
||||||
//type myElector struct{}
|
//type myElector struct{}
|
||||||
//
|
//
|
||||||
|
|
@ -524,15 +524,15 @@ func ExampleWithDistributedElector() {
|
||||||
// return nil
|
// return nil
|
||||||
//}
|
//}
|
||||||
//
|
//
|
||||||
//elector := myElector{}
|
//elector := &myElector{}
|
||||||
//
|
//
|
||||||
//_, _ = NewScheduler(
|
//_, _ = gocron.NewScheduler(
|
||||||
// WithDistributedElector(elector),
|
// gocron.WithDistributedElector(elector),
|
||||||
//)
|
//)
|
||||||
}
|
}
|
||||||
|
|
||||||
func ExampleWithDistributedLocker() {
|
func ExampleWithDistributedLocker() {
|
||||||
//var _ Locker = (*myLocker)(nil)
|
//var _ gocron.Locker = (*myLocker)(nil)
|
||||||
//
|
//
|
||||||
//type myLocker struct{}
|
//type myLocker struct{}
|
||||||
//
|
//
|
||||||
|
|
@ -549,10 +549,10 @@ func ExampleWithDistributedLocker() {
|
||||||
// return nil
|
// return nil
|
||||||
//}
|
//}
|
||||||
//
|
//
|
||||||
//locker := myLocker{}
|
//locker := &myLocker{}
|
||||||
//
|
//
|
||||||
//_, _ = NewScheduler(
|
//_, _ = gocron.NewScheduler(
|
||||||
// WithDistributedLocker(locker),
|
// gocron.WithDistributedLocker(locker),
|
||||||
//)
|
//)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,73 @@
|
||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"fmt"
|
||||||
|
"log"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/go-co-op/gocron/v2"
|
||||||
|
)
|
||||||
|
|
||||||
|
var _ gocron.Elector = (*myElector)(nil)
|
||||||
|
|
||||||
|
type myElector struct {
|
||||||
|
num int
|
||||||
|
leader bool
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m myElector) IsLeader(_ context.Context) error {
|
||||||
|
if m.leader {
|
||||||
|
log.Printf("node %d is leader", m.num)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
log.Printf("node %d is not leader", m.num)
|
||||||
|
return fmt.Errorf("not leader")
|
||||||
|
}
|
||||||
|
|
||||||
|
func main() {
|
||||||
|
log.SetFlags(log.LstdFlags | log.Lmicroseconds)
|
||||||
|
|
||||||
|
for i := 0; i < 3; i++ {
|
||||||
|
go func(i int) {
|
||||||
|
elector := &myElector{
|
||||||
|
num: i,
|
||||||
|
}
|
||||||
|
if i == 0 {
|
||||||
|
elector.leader = true
|
||||||
|
}
|
||||||
|
|
||||||
|
scheduler, err := gocron.NewScheduler(
|
||||||
|
gocron.WithDistributedElector(elector),
|
||||||
|
)
|
||||||
|
if err != nil {
|
||||||
|
log.Println(err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
_, err = scheduler.NewJob(
|
||||||
|
gocron.DurationJob(time.Second),
|
||||||
|
gocron.NewTask(func() {
|
||||||
|
log.Println("run job")
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
log.Println(err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
scheduler.Start()
|
||||||
|
|
||||||
|
if i == 0 {
|
||||||
|
time.Sleep(5 * time.Second)
|
||||||
|
elector.leader = false
|
||||||
|
}
|
||||||
|
if i == 1 {
|
||||||
|
time.Sleep(5 * time.Second)
|
||||||
|
elector.leader = true
|
||||||
|
}
|
||||||
|
}(i)
|
||||||
|
}
|
||||||
|
|
||||||
|
select {} // wait forever
|
||||||
|
}
|
||||||
20
executor.go
20
executor.go
|
|
@ -193,7 +193,7 @@ func (e *executor) start() {
|
||||||
// complete.
|
// complete.
|
||||||
standardJobsWg.Add(1)
|
standardJobsWg.Add(1)
|
||||||
go func(j internalJob) {
|
go func(j internalJob) {
|
||||||
e.runJob(j, jIn.shouldSendOut)
|
e.runJob(j, jIn)
|
||||||
standardJobsWg.Done()
|
standardJobsWg.Done()
|
||||||
}(*j)
|
}(*j)
|
||||||
}
|
}
|
||||||
|
|
@ -264,7 +264,7 @@ func (e *executor) limitModeRunner(name string, in chan jobIn, wg *waitGroupWith
|
||||||
e.limitMode.singletonJobs[jIn.id] = struct{}{}
|
e.limitMode.singletonJobs[jIn.id] = struct{}{}
|
||||||
e.limitMode.singletonJobsMu.Unlock()
|
e.limitMode.singletonJobsMu.Unlock()
|
||||||
}
|
}
|
||||||
e.runJob(*j, jIn.shouldSendOut)
|
e.runJob(*j, jIn)
|
||||||
|
|
||||||
if j.singletonMode {
|
if j.singletonMode {
|
||||||
e.limitMode.singletonJobsMu.Lock()
|
e.limitMode.singletonJobsMu.Lock()
|
||||||
|
|
@ -302,7 +302,7 @@ func (e *executor) singletonModeRunner(name string, in chan jobIn, wg *waitGroup
|
||||||
j := requestJobCtx(ctx, jIn.id, e.jobOutRequest)
|
j := requestJobCtx(ctx, jIn.id, e.jobOutRequest)
|
||||||
cancel()
|
cancel()
|
||||||
if j != nil {
|
if j != nil {
|
||||||
e.runJob(*j, jIn.shouldSendOut)
|
e.runJob(*j, jIn)
|
||||||
}
|
}
|
||||||
|
|
||||||
// remove the limiter block to allow another job to be scheduled
|
// remove the limiter block to allow another job to be scheduled
|
||||||
|
|
@ -317,7 +317,7 @@ func (e *executor) singletonModeRunner(name string, in chan jobIn, wg *waitGroup
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (e *executor) runJob(j internalJob, shouldSendOut bool) {
|
func (e *executor) runJob(j internalJob, jIn jobIn) {
|
||||||
if j.ctx == nil {
|
if j.ctx == nil {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
@ -331,26 +331,20 @@ func (e *executor) runJob(j internalJob, shouldSendOut bool) {
|
||||||
|
|
||||||
if e.elector != nil {
|
if e.elector != nil {
|
||||||
if err := e.elector.IsLeader(j.ctx); err != nil {
|
if err := e.elector.IsLeader(j.ctx); err != nil {
|
||||||
|
e.sendOutToScheduler(&jIn)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
} else if e.locker != nil {
|
} else if e.locker != nil {
|
||||||
lock, err := e.locker.Lock(j.ctx, j.name)
|
lock, err := e.locker.Lock(j.ctx, j.name)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
e.sendOutToScheduler(&jIn)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
defer func() { _ = lock.Unlock(j.ctx) }()
|
defer func() { _ = lock.Unlock(j.ctx) }()
|
||||||
}
|
}
|
||||||
_ = callJobFuncWithParams(j.beforeJobRuns, j.id, j.name)
|
_ = callJobFuncWithParams(j.beforeJobRuns, j.id, j.name)
|
||||||
|
|
||||||
if shouldSendOut {
|
e.sendOutToScheduler(&jIn)
|
||||||
select {
|
|
||||||
case <-e.ctx.Done():
|
|
||||||
return
|
|
||||||
case <-j.ctx.Done():
|
|
||||||
return
|
|
||||||
case e.jobIDsOut <- j.id:
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
startTime := time.Now()
|
startTime := time.Now()
|
||||||
err := callJobFuncWithParams(j.function, j.parameters...)
|
err := callJobFuncWithParams(j.function, j.parameters...)
|
||||||
|
|
|
||||||
|
|
@ -1140,6 +1140,7 @@ var _ Elector = (*testElector)(nil)
|
||||||
type testElector struct {
|
type testElector struct {
|
||||||
mu sync.Mutex
|
mu sync.Mutex
|
||||||
leaderElected bool
|
leaderElected bool
|
||||||
|
notLeader chan struct{}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (t *testElector) IsLeader(ctx context.Context) error {
|
func (t *testElector) IsLeader(ctx context.Context) error {
|
||||||
|
|
@ -1152,6 +1153,7 @@ func (t *testElector) IsLeader(ctx context.Context) error {
|
||||||
t.mu.Lock()
|
t.mu.Lock()
|
||||||
defer t.mu.Unlock()
|
defer t.mu.Unlock()
|
||||||
if t.leaderElected {
|
if t.leaderElected {
|
||||||
|
t.notLeader <- struct{}{}
|
||||||
return fmt.Errorf("already elected leader")
|
return fmt.Errorf("already elected leader")
|
||||||
}
|
}
|
||||||
t.leaderElected = true
|
t.leaderElected = true
|
||||||
|
|
@ -1163,12 +1165,14 @@ var _ Locker = (*testLocker)(nil)
|
||||||
type testLocker struct {
|
type testLocker struct {
|
||||||
mu sync.Mutex
|
mu sync.Mutex
|
||||||
jobLocked bool
|
jobLocked bool
|
||||||
|
notLocked chan struct{}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (t *testLocker) Lock(_ context.Context, _ string) (Lock, error) {
|
func (t *testLocker) Lock(_ context.Context, _ string) (Lock, error) {
|
||||||
t.mu.Lock()
|
t.mu.Lock()
|
||||||
defer t.mu.Unlock()
|
defer t.mu.Unlock()
|
||||||
if t.jobLocked {
|
if t.jobLocked {
|
||||||
|
t.notLocked <- struct{}{}
|
||||||
return nil, fmt.Errorf("job already locked")
|
return nil, fmt.Errorf("job already locked")
|
||||||
}
|
}
|
||||||
t.jobLocked = true
|
t.jobLocked = true
|
||||||
|
|
@ -1184,21 +1188,58 @@ func (t testLock) Unlock(_ context.Context) error {
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestScheduler_WithDistributed(t *testing.T) {
|
func TestScheduler_WithDistributed(t *testing.T) {
|
||||||
|
notLocked := make(chan struct{}, 10)
|
||||||
|
notLeader := make(chan struct{}, 10)
|
||||||
|
|
||||||
goleak.VerifyNone(t)
|
goleak.VerifyNone(t)
|
||||||
tests := []struct {
|
tests := []struct {
|
||||||
name string
|
name string
|
||||||
count int
|
count int
|
||||||
opt SchedulerOption
|
opt SchedulerOption
|
||||||
|
assertions func(*testing.T)
|
||||||
}{
|
}{
|
||||||
{
|
{
|
||||||
"3 schedulers with elector",
|
"3 schedulers with elector",
|
||||||
3,
|
3,
|
||||||
WithDistributedElector(&testElector{}),
|
WithDistributedElector(&testElector{
|
||||||
|
notLeader: notLeader,
|
||||||
|
}),
|
||||||
|
func(t *testing.T) {
|
||||||
|
timeout := time.Now().Add(1 * time.Second)
|
||||||
|
var notLeaderCount int
|
||||||
|
for {
|
||||||
|
if time.Now().After(timeout) {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
select {
|
||||||
|
case <-notLeader:
|
||||||
|
notLeaderCount++
|
||||||
|
default:
|
||||||
|
}
|
||||||
|
}
|
||||||
|
assert.Equal(t, 2, notLeaderCount)
|
||||||
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"3 schedulers with locker",
|
"3 schedulers with locker",
|
||||||
3,
|
3,
|
||||||
WithDistributedLocker(&testLocker{}),
|
WithDistributedLocker(&testLocker{
|
||||||
|
notLocked: notLocked,
|
||||||
|
}),
|
||||||
|
func(t *testing.T) {
|
||||||
|
timeout := time.Now().Add(1 * time.Second)
|
||||||
|
var notLockedCount int
|
||||||
|
for {
|
||||||
|
if time.Now().After(timeout) {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
select {
|
||||||
|
case <-notLocked:
|
||||||
|
notLockedCount++
|
||||||
|
default:
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -1222,6 +1263,7 @@ func TestScheduler_WithDistributed(t *testing.T) {
|
||||||
),
|
),
|
||||||
NewTask(
|
NewTask(
|
||||||
func() {
|
func() {
|
||||||
|
time.Sleep(100 * time.Millisecond)
|
||||||
jobsRan <- struct{}{}
|
jobsRan <- struct{}{}
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
|
|
@ -1263,6 +1305,8 @@ func TestScheduler_WithDistributed(t *testing.T) {
|
||||||
}
|
}
|
||||||
|
|
||||||
assert.Equal(t, 1, runCount)
|
assert.Equal(t, 1, runCount)
|
||||||
|
time.Sleep(time.Second)
|
||||||
|
tt.assertions(t)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue