-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathrunner.go
More file actions
102 lines (94 loc) · 3.24 KB
/
Copy pathrunner.go
File metadata and controls
102 lines (94 loc) · 3.24 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
package kata
import (
"context"
)
// Runner orchestrates a sequence of steps with automatic compensation on failure.
// It is safe to reuse a Runner across multiple Run calls (e.g. per-request).
type Runner[T any] struct {
steps []stepper[T]
config runnerConfig
}
// New creates a reusable Runner from a sequence of steps and parallel groups.
//
// Steps execute in order. On failure, completed steps are compensated in reverse.
//
// runner := kata.New(
// kata.Step("charge", chargeCard).Compensate(refundCard).Retry(3, kata.Exponential(100*time.Millisecond)),
// kata.Step("reserve", reserveStock).Compensate(releaseStock),
// kata.Parallel("notify",
// kata.Step("email", sendEmail),
// kata.Step("sms", sendSMS),
// ),
// )
func New[T any](steps ...stepper[T]) *Runner[T] {
return &Runner[T]{steps: steps}
}
// WithOptions returns a new Runner with the given options applied.
// Useful when you want to add hooks without changing the step definitions.
//
// runner := kata.New(step1, step2).WithOptions(kata.WithHooks(myHooks))
func (r *Runner[T]) WithOptions(opts ...RunnerOption) *Runner[T] {
cfg := r.config
for _, o := range opts {
o(&cfg)
}
return &Runner[T]{steps: r.steps, config: cfg}
}
// Run executes all steps in order against the given state.
//
// If the context is cancelled between steps, the runner stops and compensates
// all completed steps. Compensation always runs with context.Background() to
// guarantee rollback completes even after cancellation (e.g. SIGTERM).
//
// Returns:
// - nil on success
// - *StepError if a step failed and all compensations ran successfully
// - *CompensationError if a step failed AND some compensations also failed
func (r *Runner[T]) Run(ctx context.Context, state T) error {
h := r.config.hooks
completed := make([]stepper[T], 0, len(r.steps))
for _, step := range r.steps {
// Check context between steps so we don't start new work after
// cancellation (e.g. SIGTERM, request timeout).
if ctx.Err() != nil {
compFailures := r.compensate(context.Background(), completed, state, h)
if len(compFailures) > 0 {
return &CompensationError{
StepName: step.stepName(),
StepCause: ctx.Err(),
Failed: compFailures,
}
}
return &StepError{
StepName: step.stepName(),
Cause: ctx.Err(),
}
}
if err := step.execute(ctx, state, h); err != nil {
// Use context.Background() for compensation so that a cancelled/deadline-exceeded
// ctx (e.g. from SIGTERM) does not prevent rollback from running to completion.
compFailures := r.compensate(context.Background(), completed, state, h)
if len(compFailures) > 0 {
return &CompensationError{
StepName: step.stepName(),
StepCause: err,
Failed: compFailures,
}
}
return &StepError{
StepName: step.stepName(),
Cause: err,
}
}
completed = append(completed, step)
}
return nil
}
// compensate runs rollback for all completed steps in reverse order.
func (r *Runner[T]) compensate(ctx context.Context, completed []stepper[T], state T, h Hooks) []CompensationFailure {
var failures []CompensationFailure
for i := len(completed) - 1; i >= 0; i-- {
failures = append(failures, completed[i].rollback(ctx, state, h)...)
}
return failures
}