-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathdoc.go
More file actions
345 lines (248 loc) · 10.3 KB
/
Copy pathdoc.go
File metadata and controls
345 lines (248 loc) · 10.3 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
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
/*
Package durex provides a durable background job queue and workflow engine for Go.
Durex is a lightweight, embeddable task queue with persistence, automatic retries,
workflow sequences, and saga pattern support. It's an alternative to Asynq, River,
and Temporal for teams who want workflow capabilities without infrastructure complexity.
Use SQLite for development, PostgreSQL for production. No Redis or Kafka required.
# Quick Start
store := storage.NewMemory()
exec := durex.New(store, durex.WithParallelism(4))
exec.HandleFunc("greet", func(ctx context.Context, cmd *durex.Instance) (durex.Result, error) {
fmt.Println("Hello,", cmd.GetString("name"))
return durex.Empty(), nil
})
exec.Start(context.Background())
defer exec.Stop()
exec.Add(ctx, durex.Spec{
Name: "greet",
Data: durex.M{"name": "World"},
})
# Key Features
- Persistent Commands: Commands survive process restarts
- Automatic Retries: Configurable retry logic with backoff strategies
- Deadlines: Time-bound execution with expiration handling
- Command Chaining: Build workflows with sequences
- Recovery: Custom error handling and compensation (saga pattern)
- Middleware: Extensible execution pipeline
- Multiple Storage Backends: PostgreSQL, SQLite, Memory
- Rate Limiting: Control concurrent execution per command type
- Deduplication: Prevent duplicate commands with unique keys
- Context Propagation: Trace and correlation IDs across command chains
- Execution History: Full audit trail of command lifecycle events
- Prometheus Metrics: Built-in metrics for monitoring
- Web Dashboard: Real-time monitoring UI with retry/cancel actions
- Dead Letter Queue: Preserve failed commands for inspection and replay
- Panic Recovery: Workers survive panics and continue processing
- Stuck Command Recovery: Automatic detection and recovery of stuck commands
- Health Endpoint: /api/health for load balancer health checks
# Basic Usage
Define a command by implementing the Command interface:
type SendEmailCommand struct {
durex.BaseCommand
mailer *MailService
}
func (c *SendEmailCommand) Name() string {
return "sendEmail"
}
func (c *SendEmailCommand) Execute(ctx context.Context, cmd *durex.Instance) (durex.Result, error) {
to := cmd.GetString("to")
subject := cmd.GetString("subject")
body := cmd.GetString("body")
if err := c.mailer.Send(to, subject, body); err != nil {
return durex.Empty(), err // Will retry if retries > 0
}
return durex.Empty(), nil
}
Create an executor and register commands:
storage := storage.NewMemory()
executor := durex.New(storage,
durex.WithParallelism(4),
durex.WithDefaultRetries(3),
)
executor.Register(&SendEmailCommand{mailer: mailerService})
executor.Start(ctx)
defer executor.Stop()
Add commands for execution:
executor.Add(ctx, durex.Spec{
Name: "sendEmail",
Data: durex.M{
"to": "user@example.com",
"subject": "Welcome!",
"body": "Thanks for signing up.",
},
Retries: 3,
})
# Command Results
Commands return a Result that tells the executor what to do next:
- durex.Empty(): Command completed, no follow-up actions
- durex.Repeat(): Reschedule this command to run again after its Period
- durex.Retry(): Retry immediately (uses retry counter, doesn't trigger Recover)
- durex.Next(spec): Spawn a single follow-up command
- durex.Spawn(specs...): Spawn multiple follow-up commands
# Command Chaining
Build workflows by chaining commands:
executor.Add(ctx, durex.Spec{
Name: "validateOrder",
Sequence: []string{"processPayment", "shipOrder", "sendConfirmation"},
Data: durex.M{"orderId": "12345"},
})
Each command can continue the sequence:
func (c *ValidateOrderCommand) Execute(ctx context.Context, cmd *durex.Instance) (durex.Result, error) {
// Validate order...
cmd.Set("validated", true)
return cmd.ContinueSequence(nil), nil
}
# Error Handling
Commands can implement the Recoverable interface for custom error handling:
func (c *SendEmailCommand) Recover(ctx context.Context, cmd *durex.Instance, err error) (durex.Result, error) {
// Log the failure, notify ops, spawn compensation commands
return durex.Next(durex.Spec{
Name: "notifyFailure",
Data: durex.M{"error": err.Error()},
}), nil
}
# Deadlines
Set execution deadlines:
executor.Add(ctx, durex.Spec{
Name: "processOrder",
Deadline: 5 * time.Minute,
})
Commands can implement Expirable to handle deadline expiration:
func (c *ProcessOrderCommand) Expired(ctx context.Context, cmd *durex.Instance) (durex.Result, error) {
// Handle timeout - refund, notify, etc.
return durex.Empty(), nil
}
# Middleware
Add cross-cutting concerns:
executor := durex.New(storage,
durex.WithMiddleware(
func(ctx durex.MiddlewareContext, next func() (durex.Result, error)) (durex.Result, error) {
start := time.Now()
result, err := next()
log.Printf("Command %s took %v", ctx.Command.Name, time.Since(start))
return result, err
},
),
)
# Storage Backends
Durex supports multiple storage backends:
// In-memory (for testing)
storage := storage.NewMemory()
// SQLite (for single-instance deployments)
storage, _ := storage.OpenSQLite("commands.db")
storage.Migrate(ctx)
// PostgreSQL (for production)
db, _ := sql.Open("postgres", "postgres://...")
storage := storage.NewPostgres(db)
storage.Migrate(ctx)
# Backoff Strategies
Configure retry backoff behavior:
executor := durex.New(storage,
durex.WithBackoff(durex.DefaultExponentialBackoff()),
)
Available strategies:
- durex.NoBackoff(): Immediate retry (default)
- durex.ConstantBackoff{Delay: 5 * time.Second}: Fixed delay
- durex.LinearBackoff{InitialDelay: time.Second, MaxDelay: time.Minute}
- durex.ExponentialBackoff{InitialDelay: time.Second, MaxDelay: 5 * time.Minute, Multiplier: 2.0}
- durex.JitteredBackoff{Strategy: ..., JitterRate: 0.1}: Add randomness to prevent thundering herd
# Deduplication
Prevent duplicate commands with unique keys:
executor.Add(ctx, durex.Spec{
Name: "sendEmail",
UniqueKey: "email:user123:welcome", // Only one active command with this key
})
If a non-terminal command with the same UniqueKey exists, Add() returns ErrDuplicateCommand.
# Rate Limiting
Control concurrent command execution:
executor := durex.New(storage,
durex.WithRateLimit("sendEmail", 10), // Max 10 concurrent emails
durex.WithRateLimit("apiCall", 5), // Max 5 concurrent API calls
durex.WithGlobalRateLimit(100), // Max 100 total concurrent commands
)
# Tracing and Correlation
Commands automatically propagate trace and correlation IDs to child commands:
executor.Add(ctx, durex.Spec{
Name: "workflow",
TraceID: "trace-123", // Propagated to all children
CorrelationID: "correlation-456", // Links related commands
})
Access these in your command:
func (c *MyCommand) Execute(ctx context.Context, cmd *durex.Instance) (durex.Result, error) {
log.Printf("TraceID: %s, CorrelationID: %s", cmd.TraceID, cmd.CorrelationID)
// ...
}
# Web Dashboard
Enable the built-in monitoring dashboard:
// Simple standalone server
go executor.ServeDashboard(":8080")
// Or integrate with existing HTTP server
http.Handle("/durex/", http.StripPrefix("/durex", executor.DashboardHandler()))
// Or enable via option (auto-starts with executor)
executor := durex.New(store, durex.WithDashboard(":8080"))
The dashboard provides:
- Live command counts (pending, completed, failed, repeating)
- Recent commands with status, attempts, and timing
- Retry and cancel actions for individual commands
- Health endpoint at /api/health
# Execution History
Every command tracks its execution history for debugging and auditing:
history, _ := executor.History(ctx, "cmd_abc123")
for _, event := range history {
fmt.Printf("%s: %s (attempt %d)\n", event.Timestamp, event.Type, event.Attempt)
}
Event types: created, started, completed, failed, retrying, expired, cancelled, repeating, recovered.
History is also available via the dashboard API: GET /api/commands/history?id=<command_id>
# Prometheus Metrics
Enable Prometheus metrics for monitoring:
metrics := durex.NewPrometheusMetrics(prometheus.DefaultRegisterer)
executor := durex.New(store, durex.WithMetrics(metrics))
Exported metrics:
- durex_commands_started_total: Counter per command name
- durex_commands_completed_total: Counter per command name
- durex_commands_failed_total: Counter per command name
- durex_commands_retried_total: Counter per command name
- durex_command_duration_seconds: Histogram per command name
- durex_queue_size: Gauge for current queue size
# Dead Letter Queue
Enable DLQ to preserve failed commands for inspection and replay:
executor := durex.New(store, durex.WithDeadLetterQueue())
// Inspect failed commands
deadLettered, _ := executor.FindDeadLettered(ctx)
// Replay a command
executor.ReplayFromDLQ(ctx, "cmd_abc123")
// Purge old entries
executor.PurgeDLQ(ctx, 7*24*time.Hour)
# Reliability Features
Durex includes several reliability features:
Panic Recovery: Workers automatically recover from panics in command handlers.
The command is marked as failed, and workers continue processing other commands.
Stuck Command Recovery: Commands stuck in STARTED status (e.g., after a crash)
are automatically detected and reset to PENDING:
executor := durex.New(store,
durex.WithStuckCommandRecovery(
time.Minute, // Check every minute
5*time.Minute, // Reset commands stuck >5 min
),
)
# Multi-Instance Deployment
For horizontal scaling with PostgreSQL, Durex automatically uses row-level locking:
db, _ := sql.Open("postgres", "postgres://...")
store := storage.NewPostgres(db)
store.Migrate(ctx)
executor := durex.New(store,
durex.WithPollInterval(500*time.Millisecond),
durex.WithClaimBatchSize(20),
)
Multiple executor instances can safely run concurrently - each will claim different commands.
# Production Considerations
For production deployments:
- Use PostgreSQL for durability and multi-instance support
- Configure appropriate parallelism based on workload
- Set up monitoring using the MetricsCollector interface
- Implement proper error handling and alerting
- Use deadlines to prevent runaway commands
- Consider idempotency in command implementations
See the examples directory for complete working examples.
*/
package durex