Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -74,7 +74,7 @@ type TaskInfo struct {
Steps []Step // steps the task implements (nil = all)
Options []TaskInstanceOption // task options set by the task itself
InitError error // initialization error
NotRun func(ctx context.Context, cause error) // called if Run returns without running any step
Skipped func(ctx context.Context, cause error) // called if Run returns without running any step
}
```

Expand Down
22 changes: 11 additions & 11 deletions build.go
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ type TaskBuildFunc func(ctx context.Context) error
// BuildTask creates a task from callback functions.
//
// It is also the way to decorate an existing task: use [WithParent] to forward all steps not set here to it,
// and [WithName], [WithTaskOptions] and [WithNotRun] to add to its [TaskInfo]. To customize how the steps are
// and [WithName], [WithTaskOptions] and [WithSkipped] to add to its [TaskInfo]. To customize how the steps are
// called, use [WithHandler] in [Manager.AddTask].
func BuildTask(options ...TaskBuildOption) TaskBuild {
return newTaskBuild(options...)
Expand Down Expand Up @@ -68,7 +68,7 @@ func WithTeardown(f TaskBuildFunc) TaskBuildOption {

// WithParent sets a parent task. Any step not set in the built task will be forwarded to it.
// Its [TaskInfo] is merged into the built task one: its name is used if one is not set, its options are applied
// before the built task ones, its initialization error is returned, and its [TaskInfo.NotRun] is called.
// before the built task ones, its initialization error is returned, and its [TaskInfo.Skipped] is called.
func WithParent(parent Task) TaskBuildOption {
return func(build *taskBuild) {
if parent == nil {
Expand All @@ -86,11 +86,11 @@ func WithTaskOptions(options ...TaskInstanceOption) TaskBuildOption {
}
}

// WithNotRun adds a callback for [TaskInfo.NotRun]. All callbacks are called in order, before the parent one.
func WithNotRun(f func(ctx context.Context, cause error)) TaskBuildOption {
// WithSkipped adds a callback for [TaskInfo.Skipped]. All callbacks are called in order, before the parent one.
func WithSkipped(f func(ctx context.Context, cause error)) TaskBuildOption {
return func(build *taskBuild) {
if f != nil {
build.notRun = append(build.notRun, f)
build.skipped = append(build.skipped, f)
}
}
}
Expand All @@ -102,7 +102,7 @@ type taskBuild struct {
parent atomic.Pointer[Task]
state atomic.Pointer[taskBuildState]
options []TaskInstanceOption
notRun []func(ctx context.Context, cause error)
skipped []func(ctx context.Context, cause error)
name string
}

Expand Down Expand Up @@ -137,13 +137,13 @@ func (t *taskBuild) TaskInfo() TaskInfo {
Options: slices.Concat(parentInfo.Options, t.options),
InitError: state.initError,
}
if len(t.notRun) > 0 || parentInfo.NotRun != nil {
ret.NotRun = func(ctx context.Context, cause error) {
for _, f := range t.notRun {
if len(t.skipped) > 0 || parentInfo.Skipped != nil {
ret.Skipped = func(ctx context.Context, cause error) {
for _, f := range t.skipped {
f(ctx, cause)
}
if parentInfo.NotRun != nil {
parentInfo.NotRun(ctx, cause)
if parentInfo.Skipped != nil {
parentInfo.Skipped(ctx, cause)
}
}
}
Expand Down
16 changes: 8 additions & 8 deletions build_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -132,28 +132,28 @@ func TestBuildTaskParentInitError(t *testing.T) {
assert.ErrorIs(t, err, ErrNilTask)
}

func TestBuildTaskNotRun(t *testing.T) {
func TestBuildTaskSkipped(t *testing.T) {
var calls []string
errCause := errors.New("cause")

task := BuildTask(
WithParent(BuildTask(
WithStart(func(ctx context.Context) error { return nil }),
WithNotRun(func(ctx context.Context, cause error) {
WithSkipped(func(ctx context.Context, cause error) {
assert.Check(t, errors.Is(cause, errCause))
calls = append(calls, "parent")
}),
)),
WithNotRun(func(ctx context.Context, cause error) { calls = append(calls, "task1") }),
WithNotRun(func(ctx context.Context, cause error) { calls = append(calls, "task2") }),
WithSkipped(func(ctx context.Context, cause error) { calls = append(calls, "task1") }),
WithSkipped(func(ctx context.Context, cause error) { calls = append(calls, "task2") }),
)

notRun := task.TaskInfo().NotRun
assert.Assert(t, notRun != nil)
notRun(t.Context(), errCause)
skipped := task.TaskInfo().Skipped
assert.Assert(t, skipped != nil)
skipped(t.Context(), errCause)
assert.DeepEqual(t, []string{"task1", "task2", "parent"}, calls)

assert.Assert(t, BuildTask(WithStart(func(ctx context.Context) error { return nil })).TaskInfo().NotRun == nil)
assert.Assert(t, BuildTask(WithStart(func(ctx context.Context) error { return nil })).TaskInfo().Skipped == nil)
}

type testTaskInfo struct {
Expand Down
2 changes: 1 addition & 1 deletion defs.go
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ var (
ErrAlreadyInitialized = errors.New("already initialized")
ErrNotInitialized = errors.New("not initialized")
ErrDuplicateStep = errors.New("duplicate step")
ErrTaskNotRun = errors.New("task not run")
ErrTaskSkipped = errors.New("task skipped")
)

const (
Expand Down
8 changes: 4 additions & 4 deletions futuretask/task.go
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ import (
// setupFunc must not be nil: unlike [instancetask.Build], there would be no data to resolve the future
// with.
// If the task never runs, for example because a previous stage failed, the future is resolved with
// [svcinit.ErrTaskNotRun], so waiters don't block forever.
// [svcinit.ErrTaskSkipped], so waiters don't block forever.
func New[T any](setupFunc instancetask.BuildSetupFunc[T], options ...instancetask.BuildOption[T]) *Task[T] {
future := svcinit.NewFuture[T]()
if setupFunc == nil {
Expand All @@ -32,16 +32,16 @@ func New[T any](setupFunc instancetask.BuildSetupFunc[T], options ...instancetas
}
future.Resolve(data)
return data, nil
}, append(slices.Clip(options), instancetask.WithNotRun[T](func(_ context.Context, cause error) {
}, append(slices.Clip(options), instancetask.WithSkipped[T](func(_ context.Context, cause error) {
select {
case <-future.Done():
return // resolving twice panics.
default:
}
if cause != nil {
future.ResolveError(fmt.Errorf("%w: %w", svcinit.ErrTaskNotRun, cause))
future.ResolveError(fmt.Errorf("%w: %w", svcinit.ErrTaskSkipped, cause))
} else {
future.ResolveError(svcinit.ErrTaskNotRun)
future.ResolveError(svcinit.ErrTaskSkipped)
}
}))...),
future: future,
Expand Down
16 changes: 8 additions & 8 deletions futuretask/task_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -105,7 +105,7 @@ func TestInitError(t *testing.T) {
assert.ErrorIs(t, err, svcinit.ErrNilTask)
}

func TestNotRunAfterSetupError(t *testing.T) {
func TestSkippedAfterSetupError(t *testing.T) {
synctest.Test(t, func(t *testing.T) {
errSetup := errors.New("setup error")

Expand Down Expand Up @@ -136,12 +136,12 @@ func TestNotRunAfterSetupError(t *testing.T) {

err = sinit.Run(t.Context())
assert.ErrorIs(t, err, errSetup)
assert.ErrorIs(t, valueErr, svcinit.ErrTaskNotRun)
assert.ErrorIs(t, valueErr, svcinit.ErrTaskSkipped)
assert.ErrorIs(t, valueErr, errSetup)
})
}

func TestNotRunInitError(t *testing.T) {
func TestSkippedInitError(t *testing.T) {
sinit, err := svcinit.New()
assert.NilError(t, err)

Expand All @@ -156,12 +156,12 @@ func TestNotRunInitError(t *testing.T) {
assert.ErrorIs(t, err, svcinit.ErrNilTask)

_, err = future.Value(svcinit.WithoutFutureWait())
assert.ErrorIs(t, err, svcinit.ErrTaskNotRun)
assert.ErrorIs(t, err, svcinit.ErrTaskSkipped)
}

// TestNotRunDecorated is a regression test: decorating a future task with svcinit.BuildTask used to drop its
// "not run" notification, so the future was never resolved and its waiters blocked forever.
func TestNotRunDecorated(t *testing.T) {
// TestSkippedDecorated is a regression test: decorating a future task with svcinit.BuildTask used to drop its
// "skipped" notification, so the future was never resolved and its waiters blocked forever.
func TestSkippedDecorated(t *testing.T) {
for _, tc := range []struct {
name string
decorate func(task svcinit.Task) svcinit.Task
Expand Down Expand Up @@ -203,7 +203,7 @@ func TestNotRunDecorated(t *testing.T) {
assert.ErrorIs(t, err, errSetup)

_, err = future.Value(svcinit.WithoutFutureWait())
assert.ErrorIs(t, err, svcinit.ErrTaskNotRun)
assert.ErrorIs(t, err, svcinit.ErrTaskSkipped)
assert.ErrorIs(t, err, errSetup)
})
})
Expand Down
6 changes: 3 additions & 3 deletions instancetask/build.go
Original file line number Diff line number Diff line change
Expand Up @@ -141,9 +141,9 @@ func WithTaskOptions[T any](options ...svcinit.TaskInstanceOption) BuildOption[T
return withBuildOption[T](svcinit.WithTaskOptions(options...))
}

// WithNotRun adds a callback for [svcinit.TaskInfo.NotRun]. See [svcinit.WithNotRun].
func WithNotRun[T any](f func(ctx context.Context, cause error)) BuildOption[T] {
return withBuildOption[T](svcinit.WithNotRun(f))
// WithSkipped adds a callback for [svcinit.TaskInfo.Skipped]. See [svcinit.WithSkipped].
func WithSkipped[T any](f func(ctx context.Context, cause error)) BuildOption[T] {
return withBuildOption[T](svcinit.WithSkipped(f))
}

// internal
Expand Down
8 changes: 4 additions & 4 deletions instancetask/build_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -94,12 +94,12 @@ func TestBuildTaskInfoFromParent(t *testing.T) {
svcinit.WithName("parent"),
svcinit.WithStart(func(ctx context.Context) error { return nil }),
svcinit.WithTaskOptions(svcinit.WithCancelContext(true)),
svcinit.WithNotRun(func(ctx context.Context, cause error) { calls = append(calls, "parent") }),
svcinit.WithSkipped(func(ctx context.Context, cause error) { calls = append(calls, "parent") }),
)

task := Build[int](func(ctx context.Context) (int, error) { return 1, nil },
WithParent[int](parent),
WithNotRun[int](func(ctx context.Context, cause error) { calls = append(calls, "task") }),
WithSkipped[int](func(ctx context.Context, cause error) { calls = append(calls, "task") }),
)

info := svcinit.GetTaskInfo(task)
Expand All @@ -108,8 +108,8 @@ func TestBuildTaskInfoFromParent(t *testing.T) {
assert.DeepEqual(t, []svcinit.Step{svcinit.StepSetup, svcinit.StepStart}, info.Steps,
cmpopts.SortSlices(cmp.Less[svcinit.Step]))
assert.Equal(t, 1, len(info.Options))
assert.Assert(t, info.NotRun != nil)
info.NotRun(t.Context(), nil)
assert.Assert(t, info.Skipped != nil)
info.Skipped(t.Context(), nil)
assert.DeepEqual(t, []string{"task", "parent"}, calls)
}

Expand Down
20 changes: 10 additions & 10 deletions run.go
Original file line number Diff line number Diff line change
Expand Up @@ -37,10 +37,10 @@ func (m *Manager) runWithStopErrors(ctx context.Context, options ...RunOption) (
}

// if returning before any stage runs, notify all tasks that they will never run.
notRunNotified := false
skippedNotified := false
defer func() {
if !notRunNotified {
m.notifyTasksNotRun(ctx, m.stages, cause)
if !skippedNotified {
m.notifyTasksSkipped(ctx, m.stages, cause)
}
}()

Expand Down Expand Up @@ -106,8 +106,8 @@ func (m *Manager) runWithStopErrors(ctx context.Context, options ...RunOption) (

// stages after a setup failure will never run any step, notify their tasks now so anything waiting on
// them (like a Future) is released before shutdown starts waiting for tasks to finish.
notRunNotified = true
m.notifyTasksNotRun(ctx, m.stages[stagesRun:], setupErr)
skippedNotified = true
m.notifyTasksSkipped(ctx, m.stages[stagesRun:], setupErr)

if setupErr == nil {
m.logger.InfoContext(ctx, "waiting for first task to return")
Expand Down Expand Up @@ -515,17 +515,17 @@ func (m *Manager) runStageStep(ctx, taskDoneCtx context.Context, loggerStage *sl
return int(taskCount.Load())
}

// notifyTasksNotRun calls [TaskInfo.NotRun] for all tasks of the passed stages, which must be stages
// notifyTasksSkipped calls [TaskInfo.Skipped] for all tasks of the passed stages, which must be stages
// that will never run any step.
func (m *Manager) notifyTasksNotRun(ctx context.Context, stages []string, cause error) {
func (m *Manager) notifyTasksSkipped(ctx context.Context, stages []string, cause error) {
ctx = context.WithoutCancel(ctx)
for _, stage := range stages {
for tw := range m.tasks.stageTasks(stage) {
if notRun := GetTaskInfo(tw.task).NotRun; notRun != nil {
m.logger.Log(ctx, slog2.LevelTrace, "notifying task not run",
if skipped := GetTaskInfo(tw.task).Skipped; skipped != nil {
m.logger.Log(ctx, slog2.LevelTrace, "notifying task skipped",
"stage", stage,
"task", GetTaskDescription(tw.task))
notRun(ctx, cause)
skipped(ctx, cause)
}
}
}
Expand Down
4 changes: 2 additions & 2 deletions task.go
Original file line number Diff line number Diff line change
Expand Up @@ -46,10 +46,10 @@ type TaskInfo struct {
// InitError is a task initialization error. If not nil, [Manager.AddTask] won't add the task, and
// [Manager.Run] will return the error.
InitError error
// NotRun is called when [Manager.Run] returns without having run any of the task steps, for example because
// Skipped is called when [Manager.Run] returns without having run any of the task steps, for example because
// a setup step of a previous stage failed. cause is the error returned from Run, and may be nil.
// It can be used to release anything waiting on the task, like an unresolved [Future].
NotRun func(ctx context.Context, cause error)
Skipped func(ctx context.Context, cause error)
}

// TaskWithInfo allows a task to describe its optional metadata and behavior.
Expand Down
Loading