0
Fork 0
mirror of https://github.com/project-zot/zot.git synced 2025-01-06 22:40:28 -05:00
zot/pkg/scheduler/scheduler_test.go
peusebiu 7642e5af98
fix(scheduler): fix data race (#2085)
* fix(scheduler): data race when pushing new tasks

the problem here is that scheduler can be closed in two ways:
- canceling the context given as argument to scheduler.RunScheduler()
- running scheduler.Shutdown()

because of this shutdown can trigger a data race between calling scheduler.inShutdown()
and actually pushing tasks into the pool workers

solved that by keeping a quit channel and listening on both quit channel and ctx.Done()
and closing the worker chan and scheduler afterwards.

Signed-off-by: Petu Eusebiu <peusebiu@cisco.com>

* refactor(scheduler): refactor into a single shutdown

before this we could stop scheduler either by closing the context
provided to RunScheduler(ctx) or by running Shutdown().

simplify things by getting rid of the external context in RunScheduler().
keep an internal context in the scheduler itself and pass it down to all tasks.

Signed-off-by: Petu Eusebiu <peusebiu@cisco.com>

---------

Signed-off-by: Petu Eusebiu <peusebiu@cisco.com>
2023-12-11 10:00:34 -08:00

333 lines
8.9 KiB
Go

package scheduler_test
import (
"context"
"errors"
"fmt"
"os"
"runtime"
"testing"
"time"
. "github.com/smartystreets/goconvey/convey"
"zotregistry.io/zot/pkg/api/config"
"zotregistry.io/zot/pkg/extensions/monitoring"
"zotregistry.io/zot/pkg/log"
"zotregistry.io/zot/pkg/scheduler"
)
type task struct {
log log.Logger
msg string
err bool
}
var errInternal = errors.New("task: internal error")
func (t *task) DoWork(ctx context.Context) error {
if t.err {
return errInternal
}
for idx := 0; idx < 5; idx++ {
if ctx.Err() != nil {
return ctx.Err()
}
time.Sleep(100 * time.Millisecond)
}
t.log.Info().Msg(t.msg)
return nil
}
func (t *task) String() string {
return t.Name()
}
func (t *task) Name() string {
return "TestTask"
}
type generator struct {
log log.Logger
priority string
done bool
index int
step int
}
func (g *generator) Next() (scheduler.Task, error) {
if g.step > 100 {
g.done = true
}
g.step++
g.index++
if g.step%11 == 0 {
return nil, nil
}
if g.step%13 == 0 {
return nil, errInternal
}
return &task{log: g.log, msg: fmt.Sprintf("executing %s task; index: %d", g.priority, g.index), err: false}, nil
}
func (g *generator) IsDone() bool {
return g.done
}
func (g *generator) IsReady() bool {
return true
}
func (g *generator) Reset() {
g.done = false
g.step = 0
}
type shortGenerator struct {
log log.Logger
priority string
done bool
index int
step int
}
func (g *shortGenerator) Next() (scheduler.Task, error) {
g.done = true
return &task{log: g.log, msg: fmt.Sprintf("executing %s task; index: %d", g.priority, g.index), err: false}, nil
}
func (g *shortGenerator) IsDone() bool {
return g.done
}
func (g *shortGenerator) IsReady() bool {
return true
}
func (g *shortGenerator) Reset() {
g.done = true
g.step = 0
}
func TestScheduler(t *testing.T) {
Convey("Test active to waiting periodic generator", t, func() {
logFile, err := os.CreateTemp("", "zot-log*.txt")
So(err, ShouldBeNil)
defer os.Remove(logFile.Name()) // clean up
logger := log.NewLogger("debug", logFile.Name())
metrics := monitoring.NewMetricsServer(true, logger)
sch := scheduler.NewScheduler(config.New(), metrics, logger)
genH := &shortGenerator{log: logger, priority: "high priority"}
// interval has to be higher than throttle value to simulate
sch.SubmitGenerator(genH, 6*time.Second, scheduler.HighPriority)
sch.RunScheduler()
time.Sleep(7 * time.Second)
sch.Shutdown()
data, err := os.ReadFile(logFile.Name())
So(err, ShouldBeNil)
So(string(data), ShouldContainSubstring, "waiting generator is ready, pushing to ready generators")
})
Convey("Test order of generators in queue", t, func() {
logFile, err := os.CreateTemp("", "zot-log*.txt")
So(err, ShouldBeNil)
defer os.Remove(logFile.Name()) // clean up
logger := log.NewLogger("debug", logFile.Name())
cfg := config.New()
cfg.Scheduler = &config.SchedulerConfig{NumWorkers: 3}
metrics := monitoring.NewMetricsServer(true, logger)
sch := scheduler.NewScheduler(cfg, metrics, logger)
genL := &generator{log: logger, priority: "low priority"}
sch.SubmitGenerator(genL, time.Duration(0), scheduler.LowPriority)
genM := &generator{log: logger, priority: "medium priority"}
sch.SubmitGenerator(genM, time.Duration(0), scheduler.MediumPriority)
genH := &generator{log: logger, priority: "high priority"}
sch.SubmitGenerator(genH, time.Duration(0), scheduler.HighPriority)
sch.RunScheduler()
time.Sleep(4 * time.Second)
sch.Shutdown()
data, err := os.ReadFile(logFile.Name())
So(err, ShouldBeNil)
So(string(data), ShouldContainSubstring, "executing high priority task; index: 1")
So(string(data), ShouldContainSubstring, "executing high priority task; index: 2")
So(string(data), ShouldNotContainSubstring, "executing medium priority task; index: 1")
So(string(data), ShouldNotContainSubstring, "failed to execute task")
})
Convey("Test task returning an error", t, func() {
logFile, err := os.CreateTemp("", "zot-log*.txt")
So(err, ShouldBeNil)
defer os.Remove(logFile.Name()) // clean up
logger := log.NewLogger("debug", logFile.Name())
metrics := monitoring.NewMetricsServer(true, logger)
sch := scheduler.NewScheduler(config.New(), metrics, logger)
t := &task{log: logger, msg: "", err: true}
sch.SubmitTask(t, scheduler.MediumPriority)
sch.RunScheduler()
time.Sleep(500 * time.Millisecond)
sch.Shutdown()
data, err := os.ReadFile(logFile.Name())
So(err, ShouldBeNil)
So(string(data), ShouldContainSubstring, "adding a new task")
So(string(data), ShouldContainSubstring, "failed to execute task")
})
Convey("Test resubmit generator", t, func() {
logFile, err := os.CreateTemp("", "zot-log*.txt")
So(err, ShouldBeNil)
defer os.Remove(logFile.Name()) // clean up
logger := log.NewLogger("debug", logFile.Name())
metrics := monitoring.NewMetricsServer(true, logger)
sch := scheduler.NewScheduler(config.New(), metrics, logger)
genL := &generator{log: logger, priority: "low priority"}
sch.SubmitGenerator(genL, 20*time.Millisecond, scheduler.LowPriority)
sch.RunScheduler()
time.Sleep(4 * time.Second)
sch.Shutdown()
data, err := os.ReadFile(logFile.Name())
So(err, ShouldBeNil)
So(string(data), ShouldContainSubstring, "executing low priority task; index: 1")
So(string(data), ShouldContainSubstring, "executing low priority task; index: 2")
})
Convey("Try to add a task with wrong priority", t, func() {
logFile, err := os.CreateTemp("", "zot-log*.txt")
So(err, ShouldBeNil)
defer os.Remove(logFile.Name()) // clean up
logger := log.NewLogger("debug", logFile.Name())
metrics := monitoring.NewMetricsServer(true, logger)
sch := scheduler.NewScheduler(config.New(), metrics, logger)
t := &task{log: logger, msg: "", err: false}
sch.SubmitTask(t, -1)
data, err := os.ReadFile(logFile.Name())
So(err, ShouldBeNil)
So(string(data), ShouldNotContainSubstring, "adding a new task")
})
Convey("Test adding a new task when context is done", t, func() {
logFile, err := os.CreateTemp("", "zot-log*.txt")
So(err, ShouldBeNil)
defer os.Remove(logFile.Name()) // clean up
logger := log.NewLogger("debug", logFile.Name())
metrics := monitoring.NewMetricsServer(true, logger)
sch := scheduler.NewScheduler(config.New(), metrics, logger)
sch.RunScheduler()
sch.Shutdown()
time.Sleep(500 * time.Millisecond)
t := &task{log: logger, msg: "", err: false}
sch.SubmitTask(t, scheduler.LowPriority)
data, err := os.ReadFile(logFile.Name())
So(err, ShouldBeNil)
So(string(data), ShouldNotContainSubstring, "adding a new task")
})
Convey("Test stopping scheduler by calling Shutdown()", t, func() {
logFile, err := os.CreateTemp("", "zot-log*.txt")
So(err, ShouldBeNil)
defer os.Remove(logFile.Name()) // clean up
logger := log.NewLogger("debug", logFile.Name())
metrics := monitoring.NewMetricsServer(true, logger)
sch := scheduler.NewScheduler(config.New(), metrics, logger)
genL := &generator{log: logger, priority: "medium priority"}
sch.SubmitGenerator(genL, 20*time.Millisecond, scheduler.MediumPriority)
sch.RunScheduler()
time.Sleep(4 * time.Second)
sch.Shutdown()
data, err := os.ReadFile(logFile.Name())
So(err, ShouldBeNil)
So(string(data), ShouldContainSubstring, "executing medium priority task; index: 1")
So(string(data), ShouldContainSubstring, "executing medium priority task; index: 2")
So(string(data), ShouldContainSubstring, "received stop signal, gracefully shutting down...")
})
Convey("Test scheduler Priority.String() method", t, func() {
var p scheduler.Priority //nolint: varnamelen
// test invalid priority
p = 6238734
So(p.String(), ShouldEqual, "invalid")
p = scheduler.LowPriority
So(p.String(), ShouldEqual, "low")
p = scheduler.MediumPriority
So(p.String(), ShouldEqual, "medium")
p = scheduler.HighPriority
So(p.String(), ShouldEqual, "high")
})
Convey("Test scheduler State.String() method", t, func() {
var s scheduler.State //nolint: varnamelen
// test invalid state
s = -67
So(s.String(), ShouldEqual, "invalid")
s = scheduler.Ready
So(s.String(), ShouldEqual, "ready")
s = scheduler.Waiting
So(s.String(), ShouldEqual, "waiting")
s = scheduler.Done
So(s.String(), ShouldEqual, "done")
})
}
func TestGetNumWorkers(t *testing.T) {
Convey("Test setting the number of workers - default value", t, func() {
logger := log.NewLogger("debug", "logFile")
metrics := monitoring.NewMetricsServer(true, logger)
sch := scheduler.NewScheduler(config.New(), metrics, logger)
defer os.Remove("logFile")
So(sch.NumWorkers, ShouldEqual, runtime.NumCPU()*4)
})
Convey("Test setting the number of workers - getting the value from config", t, func() {
cfg := config.New()
cfg.Scheduler = &config.SchedulerConfig{NumWorkers: 3}
logger := log.NewLogger("debug", "logFile")
metrics := monitoring.NewMetricsServer(true, logger)
sch := scheduler.NewScheduler(cfg, metrics, logger)
defer os.Remove("logFile")
So(sch.NumWorkers, ShouldEqual, 3)
})
}