-
-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy patherrors.go
More file actions
203 lines (177 loc) · 5.85 KB
/
errors.go
File metadata and controls
203 lines (177 loc) · 5.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
package ojs
import (
"errors"
"fmt"
"time"
)
// Standard OJS error codes as defined in the OJS HTTP binding specification.
const (
ErrCodeHandlerError = "handler_error"
ErrCodeTimeout = "timeout"
ErrCodeCancelled = "cancelled"
ErrCodeInvalidPayload = "invalid_payload"
ErrCodeInvalidRequest = "invalid_request"
ErrCodeNotFound = "not_found"
ErrCodeBackendError = "backend_error"
ErrCodeRateLimited = "rate_limited"
ErrCodeDuplicate = "duplicate"
ErrCodeQueuePaused = "queue_paused"
ErrCodeSchemaValidation = "schema_validation"
ErrCodeUnsupported = "unsupported"
)
// Sentinel errors for use with errors.Is.
var (
ErrNotFound = errors.New("ojs: resource not found")
ErrDuplicate = errors.New("ojs: duplicate job")
ErrQueuePaused = errors.New("ojs: queue is paused")
ErrRateLimited = errors.New("ojs: rate limit exceeded")
ErrConflict = errors.New("ojs: conflict")
ErrBackend = errors.New("ojs: backend error")
ErrTimeout = errors.New("ojs: timeout")
)
// RateLimitInfo contains rate limit metadata extracted from response headers.
type RateLimitInfo struct {
// Limit is the maximum number of requests allowed per window.
Limit int64
// Remaining is the number of requests remaining in the current window.
Remaining int64
// Reset is the Unix timestamp (seconds) when the current window resets.
Reset int64
// RetryAfter is the duration to wait before retrying, from the Retry-After header.
RetryAfter time.Duration
}
// BatchPartialError is returned when EnqueueBatch receives fewer jobs
// back than were submitted, indicating some jobs in the batch failed.
type BatchPartialError struct {
Submitted int // Number of jobs submitted
Succeeded int // Number of jobs successfully enqueued
Jobs []Job // The successfully enqueued jobs
}
func (e *BatchPartialError) Error() string {
return fmt.Sprintf("ojs: batch partial failure: %d/%d jobs enqueued", e.Succeeded, e.Submitted)
}
// Error represents a structured OJS API error.
// It supports errors.Is and errors.As for idiomatic Go error handling.
type Error struct {
// Code is the machine-readable error code from the OJS standard vocabulary.
Code string `json:"code"`
// Message is a human-readable description of the error.
Message string `json:"message"`
// Retryable indicates whether the client should retry the request.
Retryable bool `json:"retryable"`
// Details contains additional structured error context.
Details map[string]any `json:"details,omitempty"`
// RequestID is the unique request identifier for correlation.
RequestID string `json:"request_id,omitempty"`
// HTTPStatus is the HTTP status code from the response.
HTTPStatus int `json:"-"`
// RetryAfter is the duration to wait before retrying, extracted from the
// Retry-After response header. Zero means the header was absent or invalid.
RetryAfter time.Duration `json:"-"`
// RateLimit contains rate limit metadata from response headers, if present.
RateLimit *RateLimitInfo `json:"-"`
}
// Error implements the error interface.
func (e *Error) Error() string {
if e.RequestID != "" {
return fmt.Sprintf("ojs: %s: %s (request_id=%s)", e.Code, e.Message, e.RequestID)
}
return fmt.Sprintf("ojs: %s: %s", e.Code, e.Message)
}
// Is enables errors.Is matching against sentinel errors.
func (e *Error) Is(target error) bool {
switch e.Code {
case ErrCodeNotFound:
return target == ErrNotFound
case ErrCodeDuplicate:
return target == ErrDuplicate
case ErrCodeQueuePaused:
return target == ErrQueuePaused
case ErrCodeRateLimited:
return target == ErrRateLimited
case ErrCodeBackendError:
return target == ErrBackend
case ErrCodeTimeout:
return target == ErrTimeout
}
if e.HTTPStatus == 409 {
return target == ErrConflict
}
return false
}
// Unwrap returns the sentinel error corresponding to the error code.
func (e *Error) Unwrap() error {
switch e.Code {
case ErrCodeNotFound:
return ErrNotFound
case ErrCodeDuplicate:
return ErrDuplicate
case ErrCodeQueuePaused:
return ErrQueuePaused
case ErrCodeRateLimited:
return ErrRateLimited
case ErrCodeBackendError:
return ErrBackend
case ErrCodeTimeout:
return ErrTimeout
}
if e.HTTPStatus == 409 {
return ErrConflict
}
return nil
}
// IsRetryable returns true if the error indicates the operation can be retried.
// OJS API errors use their Retryable field. Errors wrapped with [NonRetryable]
// are never retryable. All other errors return false.
func IsRetryable(err error) bool {
var nr *nonRetryableError
if errors.As(err, &nr) {
return false
}
var ojsErr *Error
if errors.As(err, &ojsErr) {
return ojsErr.Retryable
}
return false
}
// isHandlerRetryable determines retryability for handler errors in the worker.
// Handler errors default to retryable unless explicitly wrapped with NonRetryable.
func isHandlerRetryable(err error) bool {
var nr *nonRetryableError
if errors.As(err, &nr) {
return false
}
return true
}
// nonRetryableError wraps an error to mark it as non-retryable.
type nonRetryableError struct {
err error
}
func (e *nonRetryableError) Error() string { return e.err.Error() }
func (e *nonRetryableError) Unwrap() error { return e.err }
// NonRetryable wraps an error to indicate that the job should not be retried.
// The worker will NACK the job with retryable=false, causing it to be
// discarded rather than re-queued.
//
// Example:
//
// worker.Register("email.send", func(ctx ojs.JobContext) error {
// if !isValidEmail(ctx.Job.Args["to"]) {
// return ojs.NonRetryable(fmt.Errorf("invalid email address"))
// }
// return sendEmail(ctx)
// })
func NonRetryable(err error) error {
if err == nil {
return nil
}
return &nonRetryableError{err: err}
}
// ErrorCode extracts the OJS error code from an error, if available.
func ErrorCode(err error) string {
var ojsErr *Error
if errors.As(err, &ojsErr) {
return ojsErr.Code
}
return ""
}