-
Notifications
You must be signed in to change notification settings - Fork 5
Expand file tree
/
Copy patherrors.go
More file actions
238 lines (219 loc) · 9.85 KB
/
errors.go
File metadata and controls
238 lines (219 loc) · 9.85 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
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
package workflow
import (
"context"
"errors"
"fmt"
)
// ErrNoCheckpoint is returned when Resume or RunOrResume cannot find a
// checkpoint for the given execution ID. Use errors.Is to check for it.
var ErrNoCheckpoint = errors.New("workflow: no checkpoint found")
// ErrAlreadyStarted is returned when Run/Execute is called on an Execution
// that has already been started.
var ErrAlreadyStarted = errors.New("workflow: execution already started")
// ErrNilExecution is returned when Runner.Run receives a nil *Execution.
var ErrNilExecution = errors.New("workflow: execution must not be nil")
// ErrInvalidHeartbeatInterval is returned when a HeartbeatConfig has a
// non-positive Interval.
var ErrInvalidHeartbeatInterval = errors.New("workflow: heartbeat interval must be positive")
// ErrNilHeartbeatFunc is returned when a HeartbeatConfig has a nil Func.
var ErrNilHeartbeatFunc = errors.New("workflow: heartbeat func must not be nil")
// Structural validation sentinels. All are reported as ValidationProblem
// fields on *ValidationError when workflow.New runs.
var (
// ErrDuplicateStepName is reported when two steps share a name.
ErrDuplicateStepName = errors.New("workflow: duplicate step name")
// ErrEmptyStepName is reported when a step has no name.
ErrEmptyStepName = errors.New("workflow: empty step name")
// ErrUnknownStartStep is reported when Options.StartAt names a step
// that does not exist in the workflow.
ErrUnknownStartStep = errors.New("workflow: start step not found")
// ErrUnknownEdgeTarget is reported when an edge points at a step
// that does not exist in the workflow.
ErrUnknownEdgeTarget = errors.New("workflow: edge destination not found")
// ErrUnknownCatchTarget is reported when a catch handler points at
// a step that does not exist in the workflow.
ErrUnknownCatchTarget = errors.New("workflow: catch destination not found")
// ErrUnknownJoinBranch is reported when JoinConfig.Branches names
// a branch that no upstream edge declares.
ErrUnknownJoinBranch = errors.New("workflow: join branch not found")
// ErrInvalidStepKind is reported when a step mixes multiple step
// kinds (activity/join/wait_signal/sleep/pause).
ErrInvalidStepKind = errors.New("workflow: conflicting step kinds")
// ErrInvalidModifier is reported when a modifier field (Retry,
// Catch) is attached to a step kind that cannot use it.
ErrInvalidModifier = errors.New("workflow: modifier not allowed on step kind")
// ErrInvalidRetryConfig is reported when a RetryConfig has
// nonsensical bounds (negative retries, MaxDelay < BaseDelay, etc.).
ErrInvalidRetryConfig = errors.New("workflow: invalid retry config")
// ErrInvalidSleepConfig is reported when a SleepConfig has a
// non-positive Duration.
ErrInvalidSleepConfig = errors.New("workflow: invalid sleep config")
// ErrInvalidWaitConfig is reported when a WaitSignalConfig has a
// missing topic, non-positive timeout, or dangling OnTimeout.
ErrInvalidWaitConfig = errors.New("workflow: invalid wait_signal config")
// ErrReservedBranchName is reported when a named branch uses the
// reserved name "main".
ErrReservedBranchName = errors.New("workflow: branch name 'main' is reserved")
// ErrDuplicateBranchName is reported when two edges declare the
// same branch name.
ErrDuplicateBranchName = errors.New("workflow: duplicate branch name")
// ErrUnknownActivity is reported when a step references an activity
// name that is not registered on the ActivityRegistry passed to
// NewExecution. Surfaced as a ValidationProblem on *ValidationError.
ErrUnknownActivity = errors.New("workflow: activity not registered")
// ErrInvalidTemplate is reported when a parameter template or
// WaitSignalConfig.Topic template fails to parse or compile.
ErrInvalidTemplate = errors.New("workflow: invalid template")
// ErrInvalidExpression is reported when an edge condition or
// parameter script expression fails to compile.
ErrInvalidExpression = errors.New("workflow: invalid expression")
// ErrInvalidStorePath is reported when a Store field
// (Step.Store, WaitSignalConfig.Store, CatchConfig.Store,
// Output.Variable) is given with a leading "state." prefix. Store
// fields must be bare variable names.
ErrInvalidStorePath = errors.New("workflow: store field must be a bare variable name")
)
// Error type constants for classification and matching
const (
// ErrorTypeAll acts as a wildcard that matches any error except
// fatal errors. A retry/catch pattern of ErrorTypeAll will NOT
// match an error classified as ErrorTypeFatal — fatal errors are
// matchable only by an explicit ErrorTypeFatal pattern. This is
// the documented escape valve for "this error must not be
// retried, even by callers using the default catch-all pattern."
ErrorTypeAll = "all"
// ErrorTypeActivityFailed matches any error except timeouts and fatal errors
ErrorTypeActivityFailed = "activity_failed"
// ErrorTypeTimeout matches an error that wraps
// context.DeadlineExceeded or workflow.ErrWaitTimeout. Substring
// matching of the literal string "timeout" is intentionally NOT
// done — too many error messages contain the word incidentally.
// Surface a real timeout via context.DeadlineExceeded or
// ErrWaitTimeout for it to be classified here.
ErrorTypeTimeout = "timeout"
// ErrorTypeFatal indicates an execution failed due to a fatal error.
// The approach we're taking is that by default, unknown errors are
// classified as activity failed errors. This is because we want to
// allow retries on unknown errors by default. If we know a specific
// error should NOT be retried, it should have type=ErrorTypeFatal set.
ErrorTypeFatal = "fatal_error"
)
// WorkflowError represents a structured error with classification.
// It supports Go's error wrapping patterns via Unwrap.
//
// # Details
//
// Details is intentionally typed as any so consumers can attach
// arbitrary structured context. It is NOT guaranteed to round-trip
// through Checkpoint persistence: Checkpoint.Error is a flat string,
// so on resume the Details field will be lost. If a consumer needs
// structured details to survive a checkpoint/resume cycle, wrap a
// custom error type and surface the structure from the wrapped error
// instead of relying on Details.
type WorkflowError struct {
Type string `json:"type"`
Cause string `json:"cause"`
Details interface{} `json:"details,omitempty"`
Wrapped error `json:"-"` // Original error being wrapped
}
// Error implements the error interface
func (e *WorkflowError) Error() string {
return fmt.Sprintf("workflow: %s: %s", e.Type, e.Cause)
}
// Unwrap implements the error unwrapping interface for Go's errors.Is and errors.As
func (e *WorkflowError) Unwrap() error {
return e.Wrapped
}
// ErrorOutput represents the structured error information passed to catch handlers
type ErrorOutput struct {
Error string `json:"Error"`
Cause string `json:"Cause"`
Details interface{} `json:"Details,omitempty"`
}
// NewWorkflowError creates a new WorkflowError with the specified type and cause.
// The type can be any user-defined string e.g. "network-error". The important
// thing is that it may be used to match against the type used in a retry config.
func NewWorkflowError(errorType, cause string) *WorkflowError {
return &WorkflowError{
Type: errorType,
Cause: cause,
}
}
// ClassifyError attempts to classify a regular error into a WorkflowError
func ClassifyError(err error) *WorkflowError {
// If the error is already a WorkflowError, return it
var workflowError *WorkflowError
if errors.As(err, &workflowError) {
return workflowError
}
// ErrWaitTimeout is a first-class timeout sentinel emitted by
// workflow.Wait and the declarative WaitSignal step. Reuse the
// existing "timeout" classification so catch handlers with
// ErrorEquals=["timeout"] route these the same as any other
// timeout. This keeps consumer error handling uniform.
if errors.Is(err, ErrWaitTimeout) {
return &WorkflowError{
Type: ErrorTypeTimeout,
Cause: err.Error(),
Wrapped: err,
}
}
// Real timeouts only: a wrapped context.DeadlineExceeded.
// context.Canceled is intentionally NOT classified as a timeout —
// it represents caller-initiated cancellation, not a deadline
// expiry, and routing it through the timeout catch leads to
// confusing behavior. Substring matching of "timeout" in the
// error message is also intentionally not done.
if errors.Is(err, context.DeadlineExceeded) {
return &WorkflowError{
Type: ErrorTypeTimeout,
Cause: err.Error(),
Wrapped: err,
}
}
// Default to an activity failed error
return &WorkflowError{
Type: ErrorTypeActivityFailed,
Cause: err.Error(),
Wrapped: err,
}
}
// MatchesErrorType checks if an error matches a specified error type pattern
func MatchesErrorType(err error, errorType string) bool {
// Fence violations are never retryable or catchable
if errors.Is(err, ErrFenceViolation) {
return false
}
// Wait-unwinds are not failures — they are suspensions — and must
// never match a retry or catch pattern. The engine also has an
// explicit isWaitUnwind guard in executeStep/executeStepWithRetry,
// but this keeps MatchesErrorType consistent so callers that use
// it for custom error handling also see the bypass.
if isWaitUnwind(err) {
return false
}
wErr := ClassifyError(err)
// Fatal errors are only matched by the ErrorTypeFatal pattern
if wErr.Type == ErrorTypeFatal {
return errorType == ErrorTypeFatal
}
// Otherwise...
switch errorType {
case ErrorTypeAll:
return true
case ErrorTypeActivityFailed:
return wErr.Type != ErrorTypeTimeout
default:
// Note the intent here is to handle arbitrary error type strings, not
// just a fixed set of types.
return wErr.Type == errorType
}
}
// ToErrorOutput converts a WorkflowError to ErrorOutput for catch handlers
func (e *WorkflowError) ToErrorOutput() ErrorOutput {
return ErrorOutput{
Error: e.Type,
Cause: e.Cause,
Details: e.Details,
}
}