axn 0.1.0.pre.alpha.2.8.1 → 0.1.0.pre.alpha.4
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/.cursor/commands/pr.md +36 -0
- data/.cursor/rules/axn-framework-patterns.mdc +43 -0
- data/.cursor/rules/general-coding-standards.mdc +27 -0
- data/.cursor/rules/spec/testing-patterns.mdc +40 -0
- data/CHANGELOG.md +57 -0
- data/Rakefile +114 -4
- data/docs/.vitepress/config.mjs +19 -10
- data/docs/advanced/conventions.md +3 -3
- data/docs/advanced/mountable.md +476 -0
- data/docs/advanced/profiling.md +351 -0
- data/docs/advanced/rough.md +27 -8
- data/docs/index.md +5 -3
- data/docs/intro/about.md +1 -1
- data/docs/intro/overview.md +6 -6
- data/docs/recipes/formatting-context-for-error-tracking.md +186 -0
- data/docs/recipes/memoization.md +103 -18
- data/docs/recipes/rubocop-integration.md +38 -284
- data/docs/recipes/testing.md +14 -14
- data/docs/recipes/validating-user-input.md +1 -1
- data/docs/reference/async.md +429 -0
- data/docs/reference/axn-result.md +107 -0
- data/docs/reference/class.md +225 -64
- data/docs/reference/configuration.md +366 -34
- data/docs/reference/form-object.md +252 -0
- data/docs/reference/instance.md +14 -29
- data/docs/strategies/client.md +212 -0
- data/docs/strategies/form.md +235 -0
- data/docs/strategies/index.md +21 -21
- data/docs/strategies/transaction.md +1 -1
- data/docs/usage/setup.md +16 -2
- data/docs/usage/steps.md +7 -7
- data/docs/usage/using.md +23 -12
- data/docs/usage/writing.md +191 -12
- data/lib/axn/async/adapters/active_job.rb +74 -0
- data/lib/axn/async/adapters/disabled.rb +41 -0
- data/lib/axn/async/adapters/sidekiq.rb +67 -0
- data/lib/axn/async/adapters.rb +26 -0
- data/lib/axn/async/batch_enqueue/config.rb +38 -0
- data/lib/axn/async/batch_enqueue.rb +99 -0
- data/lib/axn/async/enqueue_all_orchestrator.rb +363 -0
- data/lib/axn/async.rb +178 -0
- data/lib/axn/configuration.rb +113 -0
- data/lib/{action → axn}/context.rb +22 -4
- data/lib/axn/core/automatic_logging.rb +89 -0
- data/lib/axn/core/context/facade.rb +69 -0
- data/lib/{action → axn}/core/context/facade_inspector.rb +32 -5
- data/lib/{action → axn}/core/context/internal.rb +5 -5
- data/lib/{action → axn}/core/contract.rb +111 -73
- data/lib/{action → axn}/core/contract_for_subfields.rb +30 -35
- data/lib/{action → axn}/core/contract_validation.rb +27 -12
- data/lib/axn/core/contract_validation_for_subfields.rb +165 -0
- data/lib/axn/core/default_call.rb +63 -0
- data/lib/axn/core/field_resolvers/extract.rb +32 -0
- data/lib/axn/core/field_resolvers/model.rb +63 -0
- data/lib/axn/core/field_resolvers.rb +24 -0
- data/lib/{action → axn}/core/flow/callbacks.rb +7 -7
- data/lib/{action → axn}/core/flow/exception_execution.rb +9 -13
- data/lib/{action → axn}/core/flow/handlers/base_descriptor.rb +3 -2
- data/lib/{action → axn}/core/flow/handlers/descriptors/callback_descriptor.rb +2 -2
- data/lib/{action → axn}/core/flow/handlers/descriptors/message_descriptor.rb +23 -11
- data/lib/axn/core/flow/handlers/invoker.rb +47 -0
- data/lib/{action → axn}/core/flow/handlers/matcher.rb +9 -19
- data/lib/{action → axn}/core/flow/handlers/registry.rb +3 -1
- data/lib/{action → axn}/core/flow/handlers/resolvers/base_resolver.rb +1 -1
- data/lib/{action → axn}/core/flow/handlers/resolvers/callback_resolver.rb +2 -2
- data/lib/{action → axn}/core/flow/handlers/resolvers/message_resolver.rb +12 -3
- data/lib/axn/core/flow/handlers.rb +20 -0
- data/lib/{action → axn}/core/flow/messages.rb +8 -8
- data/lib/{action → axn}/core/flow.rb +4 -4
- data/lib/{action → axn}/core/hooks.rb +17 -5
- data/lib/axn/core/logging.rb +48 -0
- data/lib/axn/core/memoization.rb +53 -0
- data/lib/{action → axn}/core/nesting_tracking.rb +1 -1
- data/lib/{action → axn}/core/timing.rb +1 -1
- data/lib/axn/core/tracing.rb +90 -0
- data/lib/axn/core/use_strategy.rb +29 -0
- data/lib/{action → axn}/core/validation/fields.rb +26 -2
- data/lib/{action → axn}/core/validation/subfields.rb +14 -12
- data/lib/axn/core/validation/validators/model_validator.rb +36 -0
- data/lib/axn/core/validation/validators/type_validator.rb +80 -0
- data/lib/{action → axn}/core/validation/validators/validate_validator.rb +12 -2
- data/lib/{action → axn}/core.rb +55 -55
- data/lib/{action → axn}/exceptions.rb +12 -2
- data/lib/axn/extras/strategies/client.rb +150 -0
- data/lib/axn/extras/strategies/vernier.rb +121 -0
- data/lib/axn/extras.rb +4 -0
- data/lib/axn/factory.rb +122 -34
- data/lib/axn/form_object.rb +90 -0
- data/lib/axn/internal/logging.rb +30 -0
- data/lib/axn/internal/registry.rb +87 -0
- data/lib/axn/mountable/descriptor.rb +76 -0
- data/lib/axn/mountable/helpers/class_builder.rb +193 -0
- data/lib/axn/mountable/helpers/mounter.rb +33 -0
- data/lib/axn/mountable/helpers/namespace_manager.rb +38 -0
- data/lib/axn/mountable/helpers/validator.rb +112 -0
- data/lib/axn/mountable/inherit_profiles.rb +72 -0
- data/lib/axn/mountable/mounting_strategies/_base.rb +87 -0
- data/lib/axn/mountable/mounting_strategies/axn.rb +48 -0
- data/lib/axn/mountable/mounting_strategies/method.rb +95 -0
- data/lib/axn/mountable/mounting_strategies/step.rb +69 -0
- data/lib/axn/mountable/mounting_strategies.rb +32 -0
- data/lib/axn/mountable.rb +119 -0
- data/lib/axn/rails/engine.rb +51 -0
- data/lib/axn/rails/generators/axn_generator.rb +86 -0
- data/lib/axn/rails/generators/templates/action.rb.erb +17 -0
- data/lib/axn/rails/generators/templates/action_spec.rb.erb +25 -0
- data/lib/{action → axn}/result.rb +32 -13
- data/lib/axn/strategies/form.rb +98 -0
- data/lib/axn/strategies/transaction.rb +26 -0
- data/lib/axn/strategies.rb +20 -0
- data/lib/axn/testing/spec_helpers.rb +6 -8
- data/lib/axn/util/callable.rb +120 -0
- data/lib/axn/util/contract_error_handling.rb +32 -0
- data/lib/axn/util/execution_context.rb +34 -0
- data/lib/axn/util/global_id_serialization.rb +52 -0
- data/lib/axn/util/logging.rb +87 -0
- data/lib/axn/util/memoization.rb +20 -0
- data/lib/axn/version.rb +1 -1
- data/lib/axn.rb +26 -16
- data/lib/rubocop/cop/axn/README.md +23 -23
- data/lib/rubocop/cop/axn/unchecked_result.rb +138 -17
- metadata +106 -64
- data/.rspec +0 -3
- data/.rubocop.yml +0 -76
- data/.tool-versions +0 -1
- data/docs/reference/action-result.md +0 -37
- data/lib/action/attachable/base.rb +0 -43
- data/lib/action/attachable/steps.rb +0 -63
- data/lib/action/attachable/subactions.rb +0 -70
- data/lib/action/attachable.rb +0 -17
- data/lib/action/configuration.rb +0 -55
- data/lib/action/core/automatic_logging.rb +0 -93
- data/lib/action/core/context/facade.rb +0 -48
- data/lib/action/core/flow/handlers/invoker.rb +0 -73
- data/lib/action/core/flow/handlers.rb +0 -20
- data/lib/action/core/logging.rb +0 -37
- data/lib/action/core/tracing.rb +0 -17
- data/lib/action/core/use_strategy.rb +0 -30
- data/lib/action/core/validation/validators/model_validator.rb +0 -34
- data/lib/action/core/validation/validators/type_validator.rb +0 -30
- data/lib/action/enqueueable/via_sidekiq.rb +0 -76
- data/lib/action/enqueueable.rb +0 -13
- data/lib/action/strategies/transaction.rb +0 -19
- data/lib/action/strategies.rb +0 -48
- data/lib/axn/util.rb +0 -24
- data/package.json +0 -10
- data/yarn.lock +0 -1166
|
@@ -0,0 +1,429 @@
|
|
|
1
|
+
# Async Execution
|
|
2
|
+
|
|
3
|
+
Axn provides built-in support for asynchronous execution through background job processing libraries. This allows you to execute actions in the background without blocking the main thread.
|
|
4
|
+
|
|
5
|
+
## Overview
|
|
6
|
+
|
|
7
|
+
Async execution in Axn is designed to be simple and consistent across different background job libraries. You can configure async behavior globally or per-action, and all async adapters support the same interface.
|
|
8
|
+
|
|
9
|
+
## Basic Usage
|
|
10
|
+
|
|
11
|
+
### Configuring Async Adapters
|
|
12
|
+
|
|
13
|
+
```ruby
|
|
14
|
+
class EmailAction
|
|
15
|
+
include Axn
|
|
16
|
+
|
|
17
|
+
# Configure async adapter
|
|
18
|
+
async :sidekiq
|
|
19
|
+
|
|
20
|
+
expects :user, :message
|
|
21
|
+
|
|
22
|
+
def call
|
|
23
|
+
# Send email logic
|
|
24
|
+
end
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
# Execute immediately (synchronous)
|
|
28
|
+
result = EmailAction.call(user: user, message: "Welcome!")
|
|
29
|
+
|
|
30
|
+
# Execute asynchronously (background)
|
|
31
|
+
EmailAction.call_async(user: user, message: "Welcome!")
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
### Available Async Adapters
|
|
35
|
+
|
|
36
|
+
#### Sidekiq
|
|
37
|
+
|
|
38
|
+
The Sidekiq adapter provides integration with the Sidekiq background job processing library.
|
|
39
|
+
|
|
40
|
+
```ruby
|
|
41
|
+
# In your action class
|
|
42
|
+
async :sidekiq do
|
|
43
|
+
sidekiq_options queue: "high_priority", retry: 5, priority: 10
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
# Or with keyword arguments (shorthand)
|
|
47
|
+
async :sidekiq, queue: "high_priority", retry: 5
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
**Configuration options:**
|
|
51
|
+
- `queue`: The Sidekiq queue name (default: "default")
|
|
52
|
+
- `retry`: Number of retry attempts (default: 25)
|
|
53
|
+
- `priority`: Job priority (default: 0)
|
|
54
|
+
- Any other Sidekiq options supported by `sidekiq_options`
|
|
55
|
+
|
|
56
|
+
#### ActiveJob
|
|
57
|
+
|
|
58
|
+
The ActiveJob adapter provides integration with Rails' ActiveJob framework.
|
|
59
|
+
|
|
60
|
+
```ruby
|
|
61
|
+
# In your action class
|
|
62
|
+
async :active_job do
|
|
63
|
+
queue_as "high_priority"
|
|
64
|
+
self.priority = 10
|
|
65
|
+
self.wait = 5.minutes
|
|
66
|
+
end
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
**Configuration options:**
|
|
70
|
+
- `queue_as`: The ActiveJob queue name
|
|
71
|
+
- `priority`: Job priority
|
|
72
|
+
- `wait`: Delay before execution
|
|
73
|
+
- Any other ActiveJob options
|
|
74
|
+
|
|
75
|
+
#### Disabled
|
|
76
|
+
|
|
77
|
+
Disables async execution entirely. The action will raise a `NotImplementedError` when `call_async` is called.
|
|
78
|
+
|
|
79
|
+
```ruby
|
|
80
|
+
# In your action class
|
|
81
|
+
async false
|
|
82
|
+
```
|
|
83
|
+
|
|
84
|
+
## Delayed Execution
|
|
85
|
+
|
|
86
|
+
All async adapters support delayed execution using the `_async` parameter in `call_async`. This allows you to schedule actions to run at specific future times without changing the interface.
|
|
87
|
+
|
|
88
|
+
```ruby
|
|
89
|
+
class EmailAction
|
|
90
|
+
include Axn
|
|
91
|
+
async :sidekiq
|
|
92
|
+
|
|
93
|
+
expects :user, :message
|
|
94
|
+
|
|
95
|
+
def call
|
|
96
|
+
# Send email logic
|
|
97
|
+
end
|
|
98
|
+
end
|
|
99
|
+
|
|
100
|
+
# Immediate execution
|
|
101
|
+
EmailAction.call_async(user: user, message: "Welcome!")
|
|
102
|
+
|
|
103
|
+
# Delayed execution - wait 1 hour
|
|
104
|
+
EmailAction.call_async(user: user, message: "Follow up", _async: { wait: 1.hour })
|
|
105
|
+
|
|
106
|
+
# Scheduled execution - run at specific time
|
|
107
|
+
EmailAction.call_async(user: user, message: "Reminder", _async: { wait_until: 1.week.from_now })
|
|
108
|
+
```
|
|
109
|
+
|
|
110
|
+
### Supported Scheduling Options
|
|
111
|
+
|
|
112
|
+
- `wait`: Execute after a specific time interval (e.g., `1.hour`, `30.minutes`)
|
|
113
|
+
- `wait_until`: Execute at a specific future time (e.g., `1.hour.from_now`, `Time.parse("2024-01-01 12:00:00")`)
|
|
114
|
+
|
|
115
|
+
### Adapter-Specific Behavior
|
|
116
|
+
|
|
117
|
+
- **Sidekiq**: Uses `perform_in` for `wait` and `perform_at` for `wait_until`
|
|
118
|
+
- **ActiveJob**: Uses `set(wait:)` for `wait` and `set(wait_until:)` for `wait_until`
|
|
119
|
+
- **Disabled**: Ignores scheduling options and raises `NotImplementedError`
|
|
120
|
+
|
|
121
|
+
### Parameter Name Safety
|
|
122
|
+
|
|
123
|
+
The `_async` parameter is reserved for scheduling options.
|
|
124
|
+
|
|
125
|
+
## Global Configuration
|
|
126
|
+
|
|
127
|
+
You can set default async configuration that will be applied to all actions that don't explicitly configure their own async behavior:
|
|
128
|
+
|
|
129
|
+
```ruby
|
|
130
|
+
Axn.configure do |c|
|
|
131
|
+
# Set a default async configuration
|
|
132
|
+
c.set_default_async(:sidekiq, queue: "default") do
|
|
133
|
+
sidekiq_options retry: 3
|
|
134
|
+
end
|
|
135
|
+
end
|
|
136
|
+
|
|
137
|
+
# Now all actions will use Sidekiq by default
|
|
138
|
+
class MyAction
|
|
139
|
+
include Axn
|
|
140
|
+
# No async configuration needed - uses default
|
|
141
|
+
end
|
|
142
|
+
```
|
|
143
|
+
|
|
144
|
+
## Error Handling
|
|
145
|
+
|
|
146
|
+
Async actions trigger via `call!` internally, so they raise on failure, which means the background job system can seamlessly handle retries.
|
|
147
|
+
|
|
148
|
+
```ruby
|
|
149
|
+
class FailingAction
|
|
150
|
+
include Axn
|
|
151
|
+
async :sidekiq, retry: 3
|
|
152
|
+
|
|
153
|
+
def call
|
|
154
|
+
fail! "Something went wrong"
|
|
155
|
+
end
|
|
156
|
+
end
|
|
157
|
+
|
|
158
|
+
# The job will be retried up to 3 times before giving up
|
|
159
|
+
FailingAction.call_async(data: "test")
|
|
160
|
+
```
|
|
161
|
+
|
|
162
|
+
## Batch Enqueueing with `enqueues_each`
|
|
163
|
+
|
|
164
|
+
The `enqueues_each` method provides a declarative way to set up batch enqueueing. It automatically iterates over collections and enqueues each item as a separate background job.
|
|
165
|
+
|
|
166
|
+
### Basic Usage
|
|
167
|
+
|
|
168
|
+
```ruby
|
|
169
|
+
class SyncForCompany
|
|
170
|
+
include Axn
|
|
171
|
+
async :sidekiq
|
|
172
|
+
|
|
173
|
+
expects :company, model: Company
|
|
174
|
+
|
|
175
|
+
def call
|
|
176
|
+
puts "Syncing data for company: #{company.name}"
|
|
177
|
+
# Sync individual company data
|
|
178
|
+
end
|
|
179
|
+
|
|
180
|
+
# No enqueues_each needed! Source is auto-inferred from model: Company
|
|
181
|
+
end
|
|
182
|
+
|
|
183
|
+
# Usage
|
|
184
|
+
SyncForCompany.enqueue_all # Automatically iterates Company.all and enqueues each company
|
|
185
|
+
```
|
|
186
|
+
|
|
187
|
+
**How it works:**
|
|
188
|
+
1. `enqueue_all` validates configuration upfront (async configured, static args present)
|
|
189
|
+
2. If all arguments are serializable, enqueues an `EnqueueAllOrchestrator` job in the background
|
|
190
|
+
3. If any argument is unserializable (e.g., an AR relation override), executes in the foreground instead
|
|
191
|
+
4. During execution, iterates over the source collection and enqueues individual jobs
|
|
192
|
+
5. Model-based iterations (using `find_each`) are processed first for memory efficiency
|
|
193
|
+
|
|
194
|
+
### Auto-Inference from `model:` Declarations
|
|
195
|
+
|
|
196
|
+
If a field has a `model:` declaration and the model class responds to `find_each`, you **don't need to explicitly declare `enqueues_each`**. The source collection is automatically inferred:
|
|
197
|
+
|
|
198
|
+
```ruby
|
|
199
|
+
class SyncForCompany
|
|
200
|
+
include Axn
|
|
201
|
+
async :sidekiq
|
|
202
|
+
|
|
203
|
+
expects :company, model: Company # Auto-inferred: Company.all
|
|
204
|
+
|
|
205
|
+
def call
|
|
206
|
+
# ... sync logic
|
|
207
|
+
end
|
|
208
|
+
|
|
209
|
+
# No enqueues_each needed - automatically iterates Company.all
|
|
210
|
+
end
|
|
211
|
+
|
|
212
|
+
SyncForCompany.enqueue_all # Works without explicit enqueues_each!
|
|
213
|
+
```
|
|
214
|
+
|
|
215
|
+
### Explicit Configuration with `enqueues_each`
|
|
216
|
+
|
|
217
|
+
Use `enqueues_each` when you need to:
|
|
218
|
+
- Override the default source (e.g., `Company.active` instead of `Company.all`)
|
|
219
|
+
- Add filtering logic
|
|
220
|
+
- Extract specific attributes
|
|
221
|
+
- Iterate over fields without `model:` declarations
|
|
222
|
+
|
|
223
|
+
```ruby
|
|
224
|
+
class SyncForCompany
|
|
225
|
+
include Axn
|
|
226
|
+
async :sidekiq
|
|
227
|
+
|
|
228
|
+
expects :company, model: Company
|
|
229
|
+
|
|
230
|
+
def call
|
|
231
|
+
# ... sync logic
|
|
232
|
+
end
|
|
233
|
+
|
|
234
|
+
# Override default source
|
|
235
|
+
enqueues_each :company, from: -> { Company.active }
|
|
236
|
+
|
|
237
|
+
# With extraction (passes company_id instead of company object)
|
|
238
|
+
enqueues_each :company_id, from: -> { Company.active }, via: :id
|
|
239
|
+
|
|
240
|
+
# With filter block
|
|
241
|
+
enqueues_each :company do |company|
|
|
242
|
+
company.active? && !company.in_exit?
|
|
243
|
+
end
|
|
244
|
+
|
|
245
|
+
# Method name as source
|
|
246
|
+
enqueues_each :company, from: :active_companies
|
|
247
|
+
end
|
|
248
|
+
```
|
|
249
|
+
|
|
250
|
+
### Overriding on `enqueue_all` Call
|
|
251
|
+
|
|
252
|
+
You can override iteration sources or make fields static when calling `enqueue_all`:
|
|
253
|
+
|
|
254
|
+
```ruby
|
|
255
|
+
class SyncForCompany
|
|
256
|
+
include Axn
|
|
257
|
+
async :sidekiq
|
|
258
|
+
|
|
259
|
+
expects :company, model: Company
|
|
260
|
+
expects :user, model: User
|
|
261
|
+
|
|
262
|
+
def call
|
|
263
|
+
# ... sync logic
|
|
264
|
+
end
|
|
265
|
+
|
|
266
|
+
# Default: iterates Company.all
|
|
267
|
+
enqueues_each :company
|
|
268
|
+
end
|
|
269
|
+
|
|
270
|
+
# Override with a subset (enumerable kwarg replaces source)
|
|
271
|
+
SyncForCompany.enqueue_all(company: Company.active.limit(10))
|
|
272
|
+
|
|
273
|
+
# Override with a single value (scalar kwarg makes it static, no iteration)
|
|
274
|
+
SyncForCompany.enqueue_all(company: Company.find(123))
|
|
275
|
+
|
|
276
|
+
# Mix static and iterated fields
|
|
277
|
+
SyncForCompany.enqueue_all(
|
|
278
|
+
company: Company.active, # Iterates over active companies
|
|
279
|
+
user: User.find(1) # Static: same user for all jobs
|
|
280
|
+
)
|
|
281
|
+
```
|
|
282
|
+
|
|
283
|
+
### Dynamic Iteration via Kwargs
|
|
284
|
+
|
|
285
|
+
You can iterate over fields without any `enqueues_each` declaration by passing enumerables directly:
|
|
286
|
+
|
|
287
|
+
```ruby
|
|
288
|
+
class ProcessFormats
|
|
289
|
+
include Axn
|
|
290
|
+
async :sidekiq
|
|
291
|
+
|
|
292
|
+
expects :format
|
|
293
|
+
expects :mode
|
|
294
|
+
|
|
295
|
+
def call
|
|
296
|
+
# ... process logic
|
|
297
|
+
end
|
|
298
|
+
end
|
|
299
|
+
|
|
300
|
+
# Pass enumerables to create cross-product iteration
|
|
301
|
+
ProcessFormats.enqueue_all(
|
|
302
|
+
format: [:csv, :json, :xml], # Iterates: 3 jobs
|
|
303
|
+
mode: :full # Static: same mode for all
|
|
304
|
+
)
|
|
305
|
+
|
|
306
|
+
# Multiple enumerables create cross-product
|
|
307
|
+
ProcessFormats.enqueue_all(
|
|
308
|
+
format: [:csv, :json], # 2 formats
|
|
309
|
+
mode: [:full, :incremental] # 2 modes
|
|
310
|
+
)
|
|
311
|
+
# Result: 2 × 2 = 4 jobs total
|
|
312
|
+
```
|
|
313
|
+
|
|
314
|
+
**Note:** Arrays and Sets are treated as static values (not iterated) when the field expects an enumerable type:
|
|
315
|
+
|
|
316
|
+
```ruby
|
|
317
|
+
expects :tags, type: Array
|
|
318
|
+
|
|
319
|
+
# This passes the entire array as a static value
|
|
320
|
+
ProcessTags.enqueue_all(tags: ["ruby", "rails", "testing"])
|
|
321
|
+
```
|
|
322
|
+
|
|
323
|
+
### Multi-Field Cross-Product Iteration
|
|
324
|
+
|
|
325
|
+
Multiple `enqueues_each` declarations create a cross-product of all combinations:
|
|
326
|
+
|
|
327
|
+
```ruby
|
|
328
|
+
class SyncForUserAndCompany
|
|
329
|
+
include Axn
|
|
330
|
+
async :sidekiq
|
|
331
|
+
|
|
332
|
+
expects :user, model: User
|
|
333
|
+
expects :company, model: Company
|
|
334
|
+
|
|
335
|
+
def call
|
|
336
|
+
# ... sync logic for user + company combination
|
|
337
|
+
end
|
|
338
|
+
|
|
339
|
+
enqueues_each :user, from: -> { User.active }
|
|
340
|
+
enqueues_each :company, from: -> { Company.active }
|
|
341
|
+
end
|
|
342
|
+
|
|
343
|
+
# Creates user_count × company_count jobs
|
|
344
|
+
# Each combination of (user, company) gets its own job
|
|
345
|
+
SyncForUserAndCompany.enqueue_all
|
|
346
|
+
```
|
|
347
|
+
|
|
348
|
+
### Static Fields
|
|
349
|
+
|
|
350
|
+
Fields declared with `expects` but not covered by `enqueues_each` (or auto-inference) become static fields that must be passed to `enqueue_all`:
|
|
351
|
+
|
|
352
|
+
```ruby
|
|
353
|
+
class SyncWithMode
|
|
354
|
+
include Axn
|
|
355
|
+
async :sidekiq
|
|
356
|
+
|
|
357
|
+
expects :company, model: Company # Auto-inferred, will iterate
|
|
358
|
+
expects :sync_mode # Static, must be provided
|
|
359
|
+
|
|
360
|
+
def call
|
|
361
|
+
# Uses both company (iterated) and sync_mode (static)
|
|
362
|
+
end
|
|
363
|
+
end
|
|
364
|
+
|
|
365
|
+
# sync_mode must be provided - it's passed to every enqueued job
|
|
366
|
+
SyncWithMode.enqueue_all(sync_mode: :full)
|
|
367
|
+
```
|
|
368
|
+
|
|
369
|
+
### Memory Efficiency
|
|
370
|
+
|
|
371
|
+
For optimal memory usage, model-based configs (using `find_each`) are automatically processed first in nested iterations. This ensures ActiveRecord-style batch processing happens before loading potentially large enumerables into memory.
|
|
372
|
+
|
|
373
|
+
```ruby
|
|
374
|
+
# Model-based iteration uses find_each (memory efficient)
|
|
375
|
+
expects :company, model: Company # Processed first
|
|
376
|
+
|
|
377
|
+
# Array-based iteration uses each (loads all into memory)
|
|
378
|
+
enqueues_each :format, from: -> { [:csv, :json, :xml] } # Processed second
|
|
379
|
+
```
|
|
380
|
+
|
|
381
|
+
### Iteration Method Selection
|
|
382
|
+
|
|
383
|
+
- **`find_each`**: Used when the source responds to `find_each` (ActiveRecord collections) - processes in batches for memory efficiency
|
|
384
|
+
- **`each`**: Used for plain arrays and other enumerables - loads all items into memory
|
|
385
|
+
|
|
386
|
+
### Background vs Foreground Execution
|
|
387
|
+
|
|
388
|
+
By default, `enqueue_all` enqueues an `EnqueueAllOrchestrator` job to perform the iteration and individual job enqueueing in the background. This makes it safe to call directly from clock processes (e.g., Heroku scheduler) without risking memory bloat from loading large collections.
|
|
389
|
+
|
|
390
|
+
However, if you pass an **enumerable override** (like an ActiveRecord relation or array), `enqueue_all` automatically falls back to foreground execution—iterating and enqueueing immediately in the current process. This is because the iteration source (a lambda wrapping the enumerable) cannot be serialized for background execution.
|
|
391
|
+
|
|
392
|
+
```ruby
|
|
393
|
+
class SyncForCompany
|
|
394
|
+
include Axn
|
|
395
|
+
async :sidekiq
|
|
396
|
+
|
|
397
|
+
expects :company, model: Company
|
|
398
|
+
|
|
399
|
+
def call
|
|
400
|
+
# ... sync logic
|
|
401
|
+
end
|
|
402
|
+
|
|
403
|
+
enqueues_each :company, from: -> { Company.active }
|
|
404
|
+
end
|
|
405
|
+
|
|
406
|
+
# Default: enqueues orchestrator job in background (safe for clock processes)
|
|
407
|
+
SyncForCompany.enqueue_all
|
|
408
|
+
|
|
409
|
+
# Override with a relation: executes in foreground (relation can't be serialized)
|
|
410
|
+
SyncForCompany.enqueue_all(company: Company.where(plan: "enterprise"))
|
|
411
|
+
|
|
412
|
+
# Override with a single value: runs in background (GlobalID-serializable scalar)
|
|
413
|
+
SyncForCompany.enqueue_all(company: Company.find(123))
|
|
414
|
+
```
|
|
415
|
+
|
|
416
|
+
**Why this matters:**
|
|
417
|
+
- Configure your default iteration source via `enqueues_each` for scheduled/recurring jobs
|
|
418
|
+
- By default, `enqueue_all` runs safely in the background without loading your entire dataset into memory
|
|
419
|
+
- For one-off manual calls with a filtered subset, pass an enumerable override—foreground execution handles it automatically
|
|
420
|
+
- Scalar overrides (single objects) are serialized via GlobalID and still run in the background
|
|
421
|
+
|
|
422
|
+
This design lets you use the same action class for both scheduled batch processing and ad-hoc targeted runs.
|
|
423
|
+
|
|
424
|
+
### Edge Cases and Limitations
|
|
425
|
+
|
|
426
|
+
1. **Fields expecting enumerable types**: If a field expects `Array` or `Set`, arrays/sets passed to `enqueue_all` are treated as static values (not iterated)
|
|
427
|
+
2. **Strings and Hashes**: Always treated as static values, even though they respond to `:each`
|
|
428
|
+
3. **No model or source**: If a field has no `model:` declaration and no `enqueues_each` with `from:`, you must pass it as a kwarg to `enqueue_all` or it will raise an error
|
|
429
|
+
4. **Required static fields**: Fields without defaults that aren't covered by iteration must be provided to `enqueue_all`
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
# `Axn::Result`
|
|
2
|
+
|
|
3
|
+
Every `call` invocation on an Axn will return an `Axn::Result` instance, which provides a consistent interface:
|
|
4
|
+
|
|
5
|
+
| Method | Description |
|
|
6
|
+
| -- | -- |
|
|
7
|
+
| `ok?` | `true` if the call succeeded, `false` if not.
|
|
8
|
+
| `error` | User-facing error message (string), if not `ok?` (else nil)
|
|
9
|
+
| `success` | User-facing success message (string), if `ok?` (else nil)
|
|
10
|
+
| `message` | User-facing message (string), always defined (`ok? ? success : error`)
|
|
11
|
+
| `exception` | If not `ok?` because an exception was swallowed, will be set to the swallowed exception (note: rarely used outside development; prefer to let the library automatically handle exception handling for you)
|
|
12
|
+
| `outcome` | The execution outcome as a string inquirer (`success?`, `failure?`, `exception?`)
|
|
13
|
+
| `elapsed_time` | Execution time in milliseconds (Float)
|
|
14
|
+
| `finalized?` | `true` if the result has completed execution (either successfully or with an exception), `false` if still in progress
|
|
15
|
+
| any `expose`d values | guaranteed to be set if `ok?` (since they have outgoing presence validations by default; any missing would have failed the action)
|
|
16
|
+
|
|
17
|
+
NOTE: `success` and `error` (and so implicitly `message`) can be configured per-action via [the `success` and `error` declarations](/reference/class#success-and-error).
|
|
18
|
+
|
|
19
|
+
### Clarification of exposed values
|
|
20
|
+
|
|
21
|
+
In addition to the core interface, your Action's Result class will have methods defined to read the values of any attributes that were explicitly exposed. For example, given this action and result:
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
```ruby
|
|
25
|
+
class Foo
|
|
26
|
+
include Axn
|
|
27
|
+
|
|
28
|
+
exposes :bar, :baz # [!code focus]
|
|
29
|
+
|
|
30
|
+
def call
|
|
31
|
+
expose bar: 1, baz: 2
|
|
32
|
+
end
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
result = Foo.call # [!code focus]
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
`result` will have both `bar` and `baz` reader methods (which will return 1 and 2, respectively).
|
|
39
|
+
|
|
40
|
+
## Pattern Matching Support
|
|
41
|
+
|
|
42
|
+
`Axn::Result` supports Ruby 3's pattern matching feature, allowing you to destructure results in a more expressive way:
|
|
43
|
+
|
|
44
|
+
```ruby
|
|
45
|
+
case SomeAction.call
|
|
46
|
+
in ok: true, success: String => message, user:, order:
|
|
47
|
+
process_success(user, order, message)
|
|
48
|
+
in ok: false, error: String => message
|
|
49
|
+
handle_error(message)
|
|
50
|
+
end
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
### Available Pattern Matching Keys
|
|
54
|
+
|
|
55
|
+
When pattern matching, the following keys are available:
|
|
56
|
+
|
|
57
|
+
- `ok` - Boolean success state (`true` for success, `false` for failure)
|
|
58
|
+
- `success` - Success message string (only present when `ok` is `true`)
|
|
59
|
+
- `error` - Error message string (only present when `ok` is `false`)
|
|
60
|
+
- `message` - Always present message string (success or error)
|
|
61
|
+
- `outcome` - Symbol indicating the execution outcome (`:success`, `:failure`, or `:exception`)
|
|
62
|
+
- `finalized` - Boolean indicating if execution completed
|
|
63
|
+
- Any exposed values from the action
|
|
64
|
+
|
|
65
|
+
### Pattern Matching Examples
|
|
66
|
+
|
|
67
|
+
**Basic Success/Failure Matching:**
|
|
68
|
+
```ruby
|
|
69
|
+
case result
|
|
70
|
+
in ok: true, user: User => user
|
|
71
|
+
puts "User created: #{user.name}"
|
|
72
|
+
in ok: false, error: String => message
|
|
73
|
+
puts "Error: #{message}"
|
|
74
|
+
end
|
|
75
|
+
```
|
|
76
|
+
|
|
77
|
+
**Outcome-Based Matching:**
|
|
78
|
+
```ruby
|
|
79
|
+
case result
|
|
80
|
+
in ok: true, outcome: :success, data: { id: Integer => id }
|
|
81
|
+
puts "Success with ID: #{id}"
|
|
82
|
+
in ok: false, outcome: :failure, error: String => message
|
|
83
|
+
puts "Business logic failure: #{message}"
|
|
84
|
+
in ok: false, outcome: :exception, error: String => message
|
|
85
|
+
puts "System error: #{message}"
|
|
86
|
+
end
|
|
87
|
+
```
|
|
88
|
+
|
|
89
|
+
**Complex Nested Data Matching:**
|
|
90
|
+
```ruby
|
|
91
|
+
case result
|
|
92
|
+
in ok: true, order: { id: Integer => order_id, items: [{ name: String => item_name }] }
|
|
93
|
+
puts "Order #{order_id} created with #{item_name}"
|
|
94
|
+
in ok: false, error: String => message, field: String => field
|
|
95
|
+
puts "Validation failed on #{field}: #{message}"
|
|
96
|
+
end
|
|
97
|
+
```
|
|
98
|
+
|
|
99
|
+
**Type Guards and Variable Binding:**
|
|
100
|
+
```ruby
|
|
101
|
+
case result
|
|
102
|
+
in ok: true, success: String => message, user: { email: String => email }
|
|
103
|
+
send_notification(email, message)
|
|
104
|
+
in ok: false, error: String => message, code: String => code
|
|
105
|
+
log_error(code: code, message: message)
|
|
106
|
+
end
|
|
107
|
+
```
|