cmdx 1.12.0 → 1.14.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/CHANGELOG.md +88 -71
- data/LICENSE.txt +3 -20
- data/README.md +8 -7
- data/lib/cmdx/attribute.rb +21 -5
- data/lib/cmdx/chain.rb +18 -4
- data/lib/cmdx/context.rb +18 -0
- data/lib/cmdx/executor.rb +35 -30
- data/lib/cmdx/result.rb +45 -2
- data/lib/cmdx/task.rb +22 -1
- data/lib/cmdx/version.rb +1 -1
- data/mkdocs.yml +67 -37
- metadata +3 -57
- data/.cursor/prompts/docs.md +0 -12
- data/.cursor/prompts/llms.md +0 -8
- data/.cursor/prompts/rspec.md +0 -24
- data/.cursor/prompts/yardoc.md +0 -15
- data/.cursor/rules/cursor-instructions.mdc +0 -68
- data/.irbrc +0 -18
- data/.rspec +0 -4
- data/.rubocop.yml +0 -95
- data/.ruby-version +0 -1
- data/.yard-lint.yml +0 -174
- data/.yardopts +0 -7
- data/docs/.DS_Store +0 -0
- data/docs/assets/favicon.ico +0 -0
- data/docs/assets/favicon.svg +0 -1
- data/docs/attributes/coercions.md +0 -155
- data/docs/attributes/defaults.md +0 -77
- data/docs/attributes/definitions.md +0 -283
- data/docs/attributes/naming.md +0 -68
- data/docs/attributes/transformations.md +0 -63
- data/docs/attributes/validations.md +0 -336
- data/docs/basics/chain.md +0 -108
- data/docs/basics/context.md +0 -121
- data/docs/basics/execution.md +0 -96
- data/docs/basics/setup.md +0 -84
- data/docs/callbacks.md +0 -157
- data/docs/configuration.md +0 -314
- data/docs/deprecation.md +0 -145
- data/docs/getting_started.md +0 -126
- data/docs/index.md +0 -134
- data/docs/internationalization.md +0 -126
- data/docs/interruptions/exceptions.md +0 -52
- data/docs/interruptions/faults.md +0 -169
- data/docs/interruptions/halt.md +0 -216
- data/docs/logging.md +0 -94
- data/docs/middlewares.md +0 -191
- data/docs/outcomes/result.md +0 -194
- data/docs/outcomes/states.md +0 -66
- data/docs/outcomes/statuses.md +0 -65
- data/docs/retries.md +0 -121
- data/docs/stylesheets/extra.css +0 -42
- data/docs/tips_and_tricks.md +0 -157
- data/docs/workflows.md +0 -226
- data/examples/active_record_database_transaction.md +0 -27
- data/examples/active_record_query_tagging.md +0 -46
- data/examples/flipper_feature_flags.md +0 -50
- data/examples/paper_trail_whatdunnit.md +0 -39
- data/examples/redis_idempotency.md +0 -71
- data/examples/sentry_error_tracking.md +0 -46
- data/examples/sidekiq_async_execution.md +0 -29
- data/examples/stoplight_circuit_breaker.md +0 -36
- data/src/cmdx-dark-logo.png +0 -0
- data/src/cmdx-favicon.svg +0 -1
- data/src/cmdx-light-logo.png +0 -0
- data/src/cmdx-logo.svg +0 -1
data/docs/workflows.md
DELETED
|
@@ -1,226 +0,0 @@
|
|
|
1
|
-
# Workflows
|
|
2
|
-
|
|
3
|
-
Compose multiple tasks into powerful, sequential pipelines. Workflows provide a declarative way to build complex business processes with conditional execution, shared context, and flexible error handling.
|
|
4
|
-
|
|
5
|
-
## Declarations
|
|
6
|
-
|
|
7
|
-
Tasks run in declaration order (FIFO), sharing a common context across the pipeline.
|
|
8
|
-
|
|
9
|
-
!!! warning
|
|
10
|
-
|
|
11
|
-
Don't define a `work` method in workflows—the module handles execution automatically.
|
|
12
|
-
|
|
13
|
-
### Task
|
|
14
|
-
|
|
15
|
-
```ruby
|
|
16
|
-
class OnboardingWorkflow < CMDx::Task
|
|
17
|
-
include CMDx::Workflow
|
|
18
|
-
|
|
19
|
-
task CreateUserProfile
|
|
20
|
-
task SetupAccountPreferences
|
|
21
|
-
|
|
22
|
-
tasks SendWelcomeEmail, SendWelcomeSms, CreateDashboard
|
|
23
|
-
end
|
|
24
|
-
```
|
|
25
|
-
|
|
26
|
-
!!! tip
|
|
27
|
-
|
|
28
|
-
Execute tasks in parallel via the [cmdx-parallel](https://github.com/drexed/cmdx-parallel) gem.
|
|
29
|
-
|
|
30
|
-
### Group
|
|
31
|
-
|
|
32
|
-
Group related tasks to share configuration:
|
|
33
|
-
|
|
34
|
-
!!! warning "Important"
|
|
35
|
-
|
|
36
|
-
Settings and conditionals apply to all tasks in the group.
|
|
37
|
-
|
|
38
|
-
```ruby
|
|
39
|
-
class ContentModerationWorkflow < CMDx::Task
|
|
40
|
-
include CMDx::Workflow
|
|
41
|
-
|
|
42
|
-
# Screening phase
|
|
43
|
-
tasks ScanForProfanity, CheckForSpam, ValidateImages, breakpoints: ["skipped"]
|
|
44
|
-
|
|
45
|
-
# Review phase
|
|
46
|
-
tasks ApplyFilters, ScoreContent, FlagSuspicious
|
|
47
|
-
|
|
48
|
-
# Decision phase
|
|
49
|
-
tasks PublishContent, QueueForReview, NotifyModerators
|
|
50
|
-
end
|
|
51
|
-
```
|
|
52
|
-
|
|
53
|
-
### Conditionals
|
|
54
|
-
|
|
55
|
-
Conditionals support multiple syntaxes for flexible execution control:
|
|
56
|
-
|
|
57
|
-
```ruby
|
|
58
|
-
class ContentAccessCheck
|
|
59
|
-
def call(task)
|
|
60
|
-
task.context.user.can?(:publish_content)
|
|
61
|
-
end
|
|
62
|
-
end
|
|
63
|
-
|
|
64
|
-
class OnboardingWorkflow < CMDx::Task
|
|
65
|
-
include CMDx::Workflow
|
|
66
|
-
|
|
67
|
-
# If and/or Unless
|
|
68
|
-
task SendWelcomeEmail, if: :email_configured?, unless: :email_disabled?
|
|
69
|
-
|
|
70
|
-
# Proc
|
|
71
|
-
task SendWelcomeEmail, if: -> { Rails.env.production? && self.class.name.include?("Premium") }
|
|
72
|
-
|
|
73
|
-
# Lambda
|
|
74
|
-
task SendWelcomeEmail, if: proc { context.features_enabled? }
|
|
75
|
-
|
|
76
|
-
# Class or Module
|
|
77
|
-
task SendWelcomeEmail, unless: ContentAccessCheck
|
|
78
|
-
|
|
79
|
-
# Instance
|
|
80
|
-
task SendWelcomeEmail, if: ContentAccessCheck.new
|
|
81
|
-
|
|
82
|
-
# Conditional applies to all tasks of this declaration group
|
|
83
|
-
tasks SendWelcomeEmail, CreateDashboard, SetupTutorial, if: :email_configured?
|
|
84
|
-
|
|
85
|
-
private
|
|
86
|
-
|
|
87
|
-
def email_configured?
|
|
88
|
-
context.user.email_address == true
|
|
89
|
-
end
|
|
90
|
-
|
|
91
|
-
def email_disabled?
|
|
92
|
-
context.user.communication_preference == :disabled
|
|
93
|
-
end
|
|
94
|
-
end
|
|
95
|
-
```
|
|
96
|
-
|
|
97
|
-
## Halt Behavior
|
|
98
|
-
|
|
99
|
-
By default, skipped tasks don't stop the workflow—they're treated as no-ops. Configure breakpoints globally or per-task to customize this behavior.
|
|
100
|
-
|
|
101
|
-
```ruby
|
|
102
|
-
class AnalyticsWorkflow < CMDx::Task
|
|
103
|
-
include CMDx::Workflow
|
|
104
|
-
|
|
105
|
-
task CollectMetrics # If fails → workflow stops
|
|
106
|
-
task FilterOutliers # If skipped → workflow continues
|
|
107
|
-
task GenerateDashboard # Only runs if no failures occurred
|
|
108
|
-
end
|
|
109
|
-
```
|
|
110
|
-
|
|
111
|
-
### Task Configuration
|
|
112
|
-
|
|
113
|
-
Configure halt behavior for the entire workflow:
|
|
114
|
-
|
|
115
|
-
```ruby
|
|
116
|
-
class SecurityWorkflow < CMDx::Task
|
|
117
|
-
include CMDx::Workflow
|
|
118
|
-
|
|
119
|
-
# Halt on both failed and skipped results
|
|
120
|
-
settings(workflow_breakpoints: ["skipped", "failed"])
|
|
121
|
-
|
|
122
|
-
task PerformSecurityScan
|
|
123
|
-
task ValidateSecurityRules
|
|
124
|
-
end
|
|
125
|
-
|
|
126
|
-
class OptionalTasksWorkflow < CMDx::Task
|
|
127
|
-
include CMDx::Workflow
|
|
128
|
-
|
|
129
|
-
# Never halt, always continue
|
|
130
|
-
settings(breakpoints: [])
|
|
131
|
-
|
|
132
|
-
task TryBackupData
|
|
133
|
-
task TryCleanupLogs
|
|
134
|
-
task TryOptimizeCache
|
|
135
|
-
end
|
|
136
|
-
```
|
|
137
|
-
|
|
138
|
-
### Group Configuration
|
|
139
|
-
|
|
140
|
-
Different task groups can have different halt behavior:
|
|
141
|
-
|
|
142
|
-
```ruby
|
|
143
|
-
class SubscriptionWorkflow < CMDx::Task
|
|
144
|
-
include CMDx::Workflow
|
|
145
|
-
|
|
146
|
-
task CreateSubscription, ValidatePayment, workflow_breakpoints: ["skipped", "failed"]
|
|
147
|
-
|
|
148
|
-
# Never halt, always continue
|
|
149
|
-
task SendConfirmationEmail, UpdateBilling, breakpoints: []
|
|
150
|
-
end
|
|
151
|
-
```
|
|
152
|
-
|
|
153
|
-
## Nested Workflows
|
|
154
|
-
|
|
155
|
-
Build hierarchical workflows by composing workflows within workflows:
|
|
156
|
-
|
|
157
|
-
```ruby
|
|
158
|
-
class EmailPreparationWorkflow < CMDx::Task
|
|
159
|
-
include CMDx::Workflow
|
|
160
|
-
|
|
161
|
-
task ValidateRecipients
|
|
162
|
-
task CompileTemplate
|
|
163
|
-
end
|
|
164
|
-
|
|
165
|
-
class EmailDeliveryWorkflow < CMDx::Task
|
|
166
|
-
include CMDx::Workflow
|
|
167
|
-
|
|
168
|
-
tasks SendEmails, TrackDeliveries
|
|
169
|
-
end
|
|
170
|
-
|
|
171
|
-
class CompleteEmailWorkflow < CMDx::Task
|
|
172
|
-
include CMDx::Workflow
|
|
173
|
-
|
|
174
|
-
task EmailPreparationWorkflow
|
|
175
|
-
task EmailDeliveryWorkflow, if: proc { context.preparation_successful? }
|
|
176
|
-
task GenerateDeliveryReport
|
|
177
|
-
end
|
|
178
|
-
```
|
|
179
|
-
|
|
180
|
-
## Parallel Execution
|
|
181
|
-
|
|
182
|
-
Run tasks concurrently using the [Parallel](https://github.com/grosser/parallel) gem. It automatically uses all available processors for maximum throughput.
|
|
183
|
-
|
|
184
|
-
!!! warning
|
|
185
|
-
|
|
186
|
-
Context is read-only during parallel execution. Load all required data beforehand.
|
|
187
|
-
|
|
188
|
-
```ruby
|
|
189
|
-
class SendWelcomeNotifications < CMDx::Task
|
|
190
|
-
include CMDx::Workflow
|
|
191
|
-
|
|
192
|
-
# Default options (dynamically calculated to available processors)
|
|
193
|
-
tasks SendWelcomeEmail, SendWelcomeSms, SendWelcomePush, strategy: :parallel
|
|
194
|
-
|
|
195
|
-
# Fix number of threads
|
|
196
|
-
tasks SendWelcomeEmail, SendWelcomeSms, SendWelcomePush, strategy: :parallel, in_threads: 2
|
|
197
|
-
|
|
198
|
-
# Fix number of forked processes
|
|
199
|
-
tasks SendWelcomeEmail, SendWelcomeSms, SendWelcomePush, strategy: :parallel, in_processes: 2
|
|
200
|
-
|
|
201
|
-
# NOTE: Reactors are not supported
|
|
202
|
-
end
|
|
203
|
-
```
|
|
204
|
-
|
|
205
|
-
## Task Generator
|
|
206
|
-
|
|
207
|
-
Generate new CMDx workflow tasks quickly using the built-in generator:
|
|
208
|
-
|
|
209
|
-
```bash
|
|
210
|
-
rails generate cmdx:workflow SendNotifications
|
|
211
|
-
```
|
|
212
|
-
|
|
213
|
-
This creates a new workflow task file with the basic structure:
|
|
214
|
-
|
|
215
|
-
```ruby
|
|
216
|
-
# app/tasks/send_notifications.rb
|
|
217
|
-
class SendNotifications < CMDx::Task
|
|
218
|
-
include CMDx::Workflow
|
|
219
|
-
|
|
220
|
-
tasks Task1, Task2
|
|
221
|
-
end
|
|
222
|
-
```
|
|
223
|
-
|
|
224
|
-
!!! tip
|
|
225
|
-
|
|
226
|
-
Use **present tense verbs + pluralized noun** for workflow task names, eg: `SendNotifications`, `DownloadFiles`, `ValidateDocuments`
|
|
@@ -1,27 +0,0 @@
|
|
|
1
|
-
# Active Record Query Tagging
|
|
2
|
-
|
|
3
|
-
Wrap task or workflow execution in a database transaction. This is essential for data integrity when multiple steps modify the database.
|
|
4
|
-
|
|
5
|
-
### Setup
|
|
6
|
-
|
|
7
|
-
```ruby
|
|
8
|
-
# lib/cmdx_database_transaction_middleware.rb
|
|
9
|
-
class CmdxDatabaseTransactionMiddleware
|
|
10
|
-
def self.call(task, **options, &)
|
|
11
|
-
ActiveRecord::Base.transaction(requires_new: true, &)
|
|
12
|
-
end
|
|
13
|
-
end
|
|
14
|
-
```
|
|
15
|
-
|
|
16
|
-
### Usage
|
|
17
|
-
|
|
18
|
-
```ruby
|
|
19
|
-
class MyTask < CMDx::Task
|
|
20
|
-
register :middleware, CmdxDatabaseTransactionMiddleware
|
|
21
|
-
|
|
22
|
-
def work
|
|
23
|
-
# Do work...
|
|
24
|
-
end
|
|
25
|
-
|
|
26
|
-
end
|
|
27
|
-
```
|
|
@@ -1,46 +0,0 @@
|
|
|
1
|
-
# Active Record Query Tagging
|
|
2
|
-
|
|
3
|
-
Add a comment to every query indicating some context to help you track down where that query came from, eg:
|
|
4
|
-
|
|
5
|
-
```sh
|
|
6
|
-
/*cmdx_task_class:ExportReportTask,cmdx_chain_id:018c2b95-b764-7615*/ SELECT * FROM reports WHERE id = 1
|
|
7
|
-
```
|
|
8
|
-
|
|
9
|
-
### Setup
|
|
10
|
-
|
|
11
|
-
```ruby
|
|
12
|
-
# config/application.rb
|
|
13
|
-
config.active_record.query_log_tags_enabled = true
|
|
14
|
-
config.active_record.query_log_tags += [
|
|
15
|
-
:cmdx_correlation_id,
|
|
16
|
-
:cmdx_chain_id,
|
|
17
|
-
:cmdx_task_class,
|
|
18
|
-
:cmdx_task_id
|
|
19
|
-
]
|
|
20
|
-
|
|
21
|
-
# lib/cmdx_query_tagging_middleware.rb
|
|
22
|
-
class CmdxQueryTaggingMiddleware
|
|
23
|
-
def self.call(task, **options, &)
|
|
24
|
-
ActiveSupport::ExecutionContext.set(
|
|
25
|
-
cmdx_correlation_id: task.result.metadata[:correlation_id],
|
|
26
|
-
cmdx_chain_id: task.chain.id,
|
|
27
|
-
cmdx_task_class: task.class.name,
|
|
28
|
-
cmdx_task_id: task.id,
|
|
29
|
-
&
|
|
30
|
-
)
|
|
31
|
-
end
|
|
32
|
-
end
|
|
33
|
-
```
|
|
34
|
-
|
|
35
|
-
### Usage
|
|
36
|
-
|
|
37
|
-
```ruby
|
|
38
|
-
class MyTask < CMDx::Task
|
|
39
|
-
register :middleware, CmdxQueryTaggingMiddleware
|
|
40
|
-
|
|
41
|
-
def work
|
|
42
|
-
# Do work...
|
|
43
|
-
end
|
|
44
|
-
|
|
45
|
-
end
|
|
46
|
-
```
|
|
@@ -1,50 +0,0 @@
|
|
|
1
|
-
# Flipper Feature Flags
|
|
2
|
-
|
|
3
|
-
Control task execution based on Flipper feature flags.
|
|
4
|
-
|
|
5
|
-
<https://github.com/flippercloud/flipper>
|
|
6
|
-
|
|
7
|
-
### Setup
|
|
8
|
-
|
|
9
|
-
```ruby
|
|
10
|
-
# lib/cmdx_flipper_middleware.rb
|
|
11
|
-
class CmdxFlipperMiddleware
|
|
12
|
-
def self.call(task, **options, &)
|
|
13
|
-
feature_name = options.fetch(:feature)
|
|
14
|
-
actor = options.fetch(:actor, -> { task.context[:user] })
|
|
15
|
-
|
|
16
|
-
# Resolve actor if it's a proc
|
|
17
|
-
actor = actor.call if actor.respond_to?(:call)
|
|
18
|
-
|
|
19
|
-
if Flipper.enabled?(feature_name, actor)
|
|
20
|
-
yield
|
|
21
|
-
else
|
|
22
|
-
# Option 1: Skip the task
|
|
23
|
-
task.skip!("Feature #{feature_name} is disabled")
|
|
24
|
-
|
|
25
|
-
# Option 2: Fail the task
|
|
26
|
-
# task.fail!("Feature #{feature_name} is disabled")
|
|
27
|
-
end
|
|
28
|
-
end
|
|
29
|
-
end
|
|
30
|
-
```
|
|
31
|
-
|
|
32
|
-
### Usage
|
|
33
|
-
|
|
34
|
-
```ruby
|
|
35
|
-
class NewFeatureTask < CMDx::Task
|
|
36
|
-
# Execute only if :new_feature is enabled for the user in context
|
|
37
|
-
register :middleware, CmdxFlipperMiddleware,
|
|
38
|
-
feature: :new_feature
|
|
39
|
-
|
|
40
|
-
# Customize the actor resolution
|
|
41
|
-
register :middleware, CmdxFlipperMiddleware,
|
|
42
|
-
feature: :beta_access,
|
|
43
|
-
actor: -> { task.context[:company] }
|
|
44
|
-
|
|
45
|
-
def work
|
|
46
|
-
# ...
|
|
47
|
-
end
|
|
48
|
-
end
|
|
49
|
-
```
|
|
50
|
-
|
|
@@ -1,39 +0,0 @@
|
|
|
1
|
-
# Paper Trail Whatdunnit
|
|
2
|
-
|
|
3
|
-
Tag paper trail version records with which service made a change with a custom `whatdunnit` attribute.
|
|
4
|
-
|
|
5
|
-
<https://github.com/paper-trail-gem/paper_trail?tab=readme-ov-file#4c-storing-metadata>
|
|
6
|
-
|
|
7
|
-
### Setup
|
|
8
|
-
|
|
9
|
-
```ruby
|
|
10
|
-
# lib/cmdx_paper_trail_middleware.rb
|
|
11
|
-
class CmdxPaperTrailMiddleware
|
|
12
|
-
def self.call(task, **options, &)
|
|
13
|
-
# This makes sure to reset the whatdunnit value to the previous
|
|
14
|
-
# value for nested task calls
|
|
15
|
-
|
|
16
|
-
begin
|
|
17
|
-
PaperTrail.request.controller_info ||= {}
|
|
18
|
-
old_whatdunnit = PaperTrail.request.controller_info[:whatdunnit]
|
|
19
|
-
PaperTrail.request.controller_info[:whatdunnit] = task.class.name
|
|
20
|
-
yield
|
|
21
|
-
ensure
|
|
22
|
-
PaperTrail.request.controller_info[:whatdunnit] = old_whatdunnit
|
|
23
|
-
end
|
|
24
|
-
end
|
|
25
|
-
end
|
|
26
|
-
```
|
|
27
|
-
|
|
28
|
-
### Usage
|
|
29
|
-
|
|
30
|
-
```ruby
|
|
31
|
-
class MyTask < CMDx::Task
|
|
32
|
-
register :middleware, CmdxPaperTrailMiddleware
|
|
33
|
-
|
|
34
|
-
def work
|
|
35
|
-
# Do work...
|
|
36
|
-
end
|
|
37
|
-
|
|
38
|
-
end
|
|
39
|
-
```
|
|
@@ -1,71 +0,0 @@
|
|
|
1
|
-
# Redis Idempotency
|
|
2
|
-
|
|
3
|
-
Ensure tasks are executed exactly once using Redis to store execution state. This is critical for non-idempotent operations like charging a credit card or sending an email.
|
|
4
|
-
|
|
5
|
-
### Setup
|
|
6
|
-
|
|
7
|
-
```ruby
|
|
8
|
-
# lib/cmdx_redis_idempotency_middleware.rb
|
|
9
|
-
class CmdxRedisIdempotencyMiddleware
|
|
10
|
-
def self.call(task, **options, &block)
|
|
11
|
-
key = generate_key(task, options[:key])
|
|
12
|
-
ttl = options[:ttl] || 5.minutes.to_i
|
|
13
|
-
|
|
14
|
-
# Attempt to lock the key
|
|
15
|
-
if Redis.current.set(key, "processing", nx: true, ex: ttl)
|
|
16
|
-
begin
|
|
17
|
-
block.call.tap |result|
|
|
18
|
-
Redis.current.set(key, result.status, xx: true, ex: ttl)
|
|
19
|
-
end
|
|
20
|
-
rescue => e
|
|
21
|
-
Redis.current.del(key)
|
|
22
|
-
raise(e)
|
|
23
|
-
end
|
|
24
|
-
else
|
|
25
|
-
# Key exists, handle duplicate
|
|
26
|
-
status = Redis.current.get(key)
|
|
27
|
-
|
|
28
|
-
if status == "processing"
|
|
29
|
-
task.result.tap { |r| r.skip!("Duplicate request: currently processing", halt: true) }
|
|
30
|
-
else
|
|
31
|
-
task.result.tap { |r| r.skip!("Duplicate request: already processed (#{status})", halt: true) }
|
|
32
|
-
end
|
|
33
|
-
end
|
|
34
|
-
end
|
|
35
|
-
|
|
36
|
-
def self.generate_key(task, key_gen)
|
|
37
|
-
id = if key_gen.respond_to?(:call)
|
|
38
|
-
key_gen.call(task)
|
|
39
|
-
elsif key_gen.is_a?(Symbol)
|
|
40
|
-
task.send(key_gen)
|
|
41
|
-
else
|
|
42
|
-
task.context[:idempotency_key]
|
|
43
|
-
end
|
|
44
|
-
|
|
45
|
-
"cmdx:idempotency:#{task.class.name}:#{id}"
|
|
46
|
-
end
|
|
47
|
-
end
|
|
48
|
-
```
|
|
49
|
-
|
|
50
|
-
### Usage
|
|
51
|
-
|
|
52
|
-
```ruby
|
|
53
|
-
class ChargeCustomer < CMDx::Task
|
|
54
|
-
# Use context[:payment_id] as the unique key
|
|
55
|
-
register :middleware, CmdxIdempotencyMiddleware,
|
|
56
|
-
key: ->(t) { t.context[:payment_id] }
|
|
57
|
-
|
|
58
|
-
def work
|
|
59
|
-
# Charge logic...
|
|
60
|
-
end
|
|
61
|
-
end
|
|
62
|
-
|
|
63
|
-
# First run: Executes
|
|
64
|
-
ChargeCustomer.call(payment_id: "123")
|
|
65
|
-
# => Success
|
|
66
|
-
|
|
67
|
-
# Second run: Skips
|
|
68
|
-
ChargeCustomer.call(payment_id: "123")
|
|
69
|
-
# => Skipped (reason: "Duplicate request: already processed (success)")
|
|
70
|
-
```
|
|
71
|
-
|
|
@@ -1,46 +0,0 @@
|
|
|
1
|
-
# Sentry Error Tracking
|
|
2
|
-
|
|
3
|
-
Report unhandled exceptions and unexpected task failures to Sentry with detailed context.
|
|
4
|
-
|
|
5
|
-
<https://github.com/getsentry/sentry-ruby>
|
|
6
|
-
|
|
7
|
-
### Setup
|
|
8
|
-
|
|
9
|
-
```ruby
|
|
10
|
-
# lib/cmdx_sentry_middleware.rb
|
|
11
|
-
class CmdxSentryMiddleware
|
|
12
|
-
def self.call(task, **options, &)
|
|
13
|
-
Sentry.with_scope do |scope|
|
|
14
|
-
scope.set_tags(task: task.class.name)
|
|
15
|
-
scope.set_context(:user, Current.user.sentry_attributes)
|
|
16
|
-
|
|
17
|
-
yield.tap do |result|
|
|
18
|
-
# Optional: Report logical failures if needed
|
|
19
|
-
if Array(options[:report_on]).include?(result.status)
|
|
20
|
-
Sentry.capture_message("Task #{result.status}: #{result.reason}", level: :warning)
|
|
21
|
-
end
|
|
22
|
-
end
|
|
23
|
-
end
|
|
24
|
-
rescue => e
|
|
25
|
-
Sentry.capture_exception(e)
|
|
26
|
-
raise(e) # Re-raise to let the task handle the error or bubble up
|
|
27
|
-
end
|
|
28
|
-
end
|
|
29
|
-
```
|
|
30
|
-
|
|
31
|
-
### Usage
|
|
32
|
-
|
|
33
|
-
```ruby
|
|
34
|
-
class ProcessPayment < CMDx::Task
|
|
35
|
-
# Report exceptions only
|
|
36
|
-
register :middleware, CmdxSentryMiddleware
|
|
37
|
-
|
|
38
|
-
# Report exceptions AND logical failures (result.failure?)
|
|
39
|
-
register :middleware, CmdxSentryMiddleware, report_on: %w[failed skipped]
|
|
40
|
-
|
|
41
|
-
def work
|
|
42
|
-
# ...
|
|
43
|
-
end
|
|
44
|
-
end
|
|
45
|
-
```
|
|
46
|
-
|
|
@@ -1,29 +0,0 @@
|
|
|
1
|
-
# Sidekiq Async Execute
|
|
2
|
-
|
|
3
|
-
Execute tasks asynchronously using Sidekiq without creating separate job classes.
|
|
4
|
-
|
|
5
|
-
<https://github.com/sidekiq/sidekiq>
|
|
6
|
-
|
|
7
|
-
### Setup
|
|
8
|
-
|
|
9
|
-
```ruby
|
|
10
|
-
class MyTask < CMDx::Task
|
|
11
|
-
include Sidekiq::Job
|
|
12
|
-
|
|
13
|
-
def work
|
|
14
|
-
# Do work...
|
|
15
|
-
end
|
|
16
|
-
|
|
17
|
-
# Use execute! to trigger Sidekiq's retry logic on failures/exceptions.
|
|
18
|
-
def perform
|
|
19
|
-
self.class.execute!
|
|
20
|
-
end
|
|
21
|
-
|
|
22
|
-
end
|
|
23
|
-
```
|
|
24
|
-
|
|
25
|
-
### Usage
|
|
26
|
-
|
|
27
|
-
```ruby
|
|
28
|
-
MyTask.perform_async
|
|
29
|
-
```
|
|
@@ -1,36 +0,0 @@
|
|
|
1
|
-
# Stoplight Circuit Breaker
|
|
2
|
-
|
|
3
|
-
Integrate circuit breakers to protect external service calls and prevent cascading failures when dependencies are unavailable.
|
|
4
|
-
|
|
5
|
-
<https://github.com/bolshakov/stoplight>
|
|
6
|
-
|
|
7
|
-
### Setup
|
|
8
|
-
|
|
9
|
-
```ruby
|
|
10
|
-
# lib/cmdx_stoplight_middleware.rb
|
|
11
|
-
class CmdxStoplightMiddleware
|
|
12
|
-
def self.call(task, **options, &)
|
|
13
|
-
light = Stoplight(options[:name] || task.class.name, **options)
|
|
14
|
-
light.run(&)
|
|
15
|
-
rescue Stoplight::Error::RedLight => e
|
|
16
|
-
task.result.tap { |r| r.fail!("[#{e.class}] #{e.message}", cause: e) }
|
|
17
|
-
end
|
|
18
|
-
end
|
|
19
|
-
```
|
|
20
|
-
|
|
21
|
-
### Usage
|
|
22
|
-
|
|
23
|
-
```ruby
|
|
24
|
-
class MyTask < CMDx::Task
|
|
25
|
-
# With default options
|
|
26
|
-
register :middleware, CmdxStoplightMiddleware
|
|
27
|
-
|
|
28
|
-
# With stoplight options
|
|
29
|
-
register :middleware, CmdxStoplightMiddleware, cool_off_time: 10
|
|
30
|
-
|
|
31
|
-
def work
|
|
32
|
-
# Do work...
|
|
33
|
-
end
|
|
34
|
-
|
|
35
|
-
end
|
|
36
|
-
```
|
data/src/cmdx-dark-logo.png
DELETED
|
Binary file
|
data/src/cmdx-favicon.svg
DELETED
|
@@ -1 +0,0 @@
|
|
|
1
|
-
<svg width="104.098" height="112.266" viewBox="0 0 60 64.708" xmlns="http://www.w3.org/2000/svg"><path d="M29.907 17.723 26.4 23.17 13.384 3.323h3.507l9.508 14.77 1.938-3.139L18.737 0H7.291L26.4 29.262 45.045 0h-3.97L30 17.54zM9.23 61.293H6.091l18.646-29.447L4.43.093H.46l20.308 31.846L0 64.708l10.985-.092 39.138-61.2h3.323L31.57 37.754l13.662 21.415h3.97L35.445 37.754 59.537.093H48.37zm29.63-23.262 15.047 23.262H43.384l-13.662-20.77-15.508 24.093h3.97l11.63-18 11.723 18H60L41.17 35.354l-.278-.461z" fill="#000"/></svg>
|
data/src/cmdx-light-logo.png
DELETED
|
Binary file
|
data/src/cmdx-logo.svg
DELETED
|
@@ -1 +0,0 @@
|
|
|
1
|
-
<svg width="352.2" height="112.266" viewBox="0 0 203 64.708" xmlns="http://www.w3.org/2000/svg" xmlSpace="preserve"><path d="M172.908 17.723 169.4 23.17 156.385 3.323h3.507l9.508 14.77 1.938-3.139L161.738 0h-11.446L169.4 29.262 188.046 0h-3.97L173 17.54zm-20.677 43.57h-3.139l18.646-29.447L147.431.093h-3.97l20.308 31.846L143 64.708l10.985-.092 39.138-61.2h3.323L174.57 37.754l13.662 21.415h3.969l-13.754-21.415L202.538.093H191.37zm29.63-23.262 15.047 23.262h-10.523l-13.662-20.77-15.508 24.093h3.97l11.63-18 11.723 18H203l-18.83-29.262-.278-.461z" fill="#fe1817"/><path d="M41.667 14v12.8h-23.42c-3.214.272-5.665 3.05-5.665 6.318s2.45 5.937 5.664 6.318H33.17v4.248H18.246a10.65 10.65 0 0 1-9.858-10.62c0-5.447 4.194-10.077 9.64-10.512h19.39v-4.303H18.246v.054A14.823 14.823 0 0 0 4.248 33.118c0 7.898 6.21 14.38 13.998 14.815h19.172v-8.497h4.249v12.745h-23.42A19.063 19.063 0 0 1 0 33.118a19.033 19.033 0 0 1 18.246-19.063zM75 35.623 87.2 14h13.508v38.181H87.963v-14.27l-8.116 14.27h-9.749L57.734 30.504v-8.007l14.87 25.436h4.792l14.815-25.436v25.436h4.249V18.249H89.65l-14.76 25.49-14.542-25.49H53.54v29.684h4.194v-8.007l4.249 7.299v4.956H49.292v-38.18H62.8zM108.333 14h23.42C141.94 14.436 150 22.824 150 33.064c0 10.294-8.061 18.681-18.246 19.117h-23.42V22.497h23.42a10.65 10.65 0 0 1 9.858 10.621c0 5.447-4.194 10.13-9.64 10.566H116.83V30.286h4.248v9.15l10.676-.054c3.213-.273 5.664-3.05 5.664-6.264 0-2.778-1.743-5.065-4.194-5.991-.926-.382-1.47-.382-2.94-.382h-17.702v21.188h19.172a14.84 14.84 0 0 0 13.998-14.87c0-7.897-6.21-14.379-13.998-14.814h-23.42z"/></svg>
|