axn 0.1.0.pre.alpha.4.1 → 0.1.0.pre.alpha.4.2
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 +19 -0
- data/Rakefile +3 -2
- data/docs/.vitepress/config.mjs +0 -1
- data/docs/advanced/rough.md +16 -4
- data/docs/reference/class.md +10 -0
- data/docs/reference/configuration.md +93 -48
- data/docs/strategies/form.md +11 -2
- data/docs/strategies/transaction.md +4 -3
- data/lib/axn/async/adapters/sidekiq/auto_configure.rb +15 -0
- data/lib/axn/async/adapters/sidekiq.rb +6 -2
- data/lib/axn/async/enqueue_all_orchestrator.rb +6 -6
- data/lib/axn/async/exception_reporting.rb +3 -3
- data/lib/axn/async.rb +2 -2
- data/lib/axn/configuration.rb +25 -2
- data/lib/axn/core/automatic_logging.rb +0 -62
- data/lib/axn/core/context/facade.rb +1 -1
- data/lib/axn/core/contract.rb +76 -55
- data/lib/axn/core/contract_for_subfields.rb +9 -6
- data/lib/axn/core/default_call.rb +1 -14
- data/lib/axn/core/field_resolvers/model.rb +1 -1
- data/lib/axn/core/flow/handlers/invoker.rb +16 -5
- data/lib/axn/core/flow/handlers/matcher.rb +4 -7
- data/lib/axn/core/flow.rb +0 -2
- data/lib/axn/core/hooks.rb +0 -66
- data/lib/axn/core/memoization.rb +1 -1
- data/lib/axn/core/nesting_tracking.rb +8 -7
- data/lib/axn/core/validation/validators/validate_validator.rb +1 -1
- data/lib/axn/core.rb +7 -28
- data/lib/axn/executor.rb +454 -0
- data/lib/axn/extension_config.rb +13 -0
- data/lib/axn/extras/strategies/client.rb +46 -0
- data/lib/axn/{util/logging.rb → internal/call_logger.rb} +12 -7
- data/lib/axn/{util → internal}/callable.rb +1 -1
- data/lib/axn/{util → internal}/contract_error_handling.rb +1 -1
- data/lib/axn/internal/exception_context.rb +148 -0
- data/lib/axn/internal/field_config.rb +25 -0
- data/lib/axn/{util → internal}/global_id_serialization.rb +1 -1
- data/lib/axn/{util → internal}/memoization.rb +1 -1
- data/lib/axn/internal/{logging.rb → piping_error.rb} +5 -2
- data/lib/axn/internal/subfield_path.rb +33 -0
- data/lib/axn/{core → internal}/timing.rb +1 -19
- data/lib/axn/internal/tracing.rb +39 -0
- data/lib/axn/strategies/form.rb +16 -3
- data/lib/axn/version.rb +1 -1
- data/lib/axn.rb +21 -7
- metadata +15 -14
- data/docs/recipes/formatting-context-for-error-tracking.md +0 -186
- data/lib/axn/core/contract_validation.rb +0 -77
- data/lib/axn/core/contract_validation_for_subfields.rb +0 -165
- data/lib/axn/core/flow/exception_execution.rb +0 -73
- data/lib/axn/core/tracing.rb +0 -110
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: bbed8a136915346d270fb287bd513684ddfd9b70692fa8a06384924a34198d6b
|
|
4
|
+
data.tar.gz: a9f8bb70324642e6da0ecba6522eaa8587f53259ad3488ddc72ed20927a73ad3
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 260b77024b257ad90ac69ae67deb6468a6c30d4bd47d877eefb709879e8e1a16a6f0cf519607951f3de3d9fe2663bb84cab37a58383846f495ddc17eed053614
|
|
7
|
+
data.tar.gz: 583c4c638a5bdc2c9d5686551e305972756d7737450c3ec8a02f3a5b236582a59e41e2290532ed8d69b9fc4c145364db0ac8f0a4a8cbda83736bf10cd7a5093f
|
data/CHANGELOG.md
CHANGED
|
@@ -3,6 +3,25 @@
|
|
|
3
3
|
## Unreleased
|
|
4
4
|
* N/A
|
|
5
5
|
|
|
6
|
+
## 0.1.0-alpha.4.2
|
|
7
|
+
* [FEAT] Add extensible field metadata support for `expects`/`exposes`:
|
|
8
|
+
* **New** `Axn.extension_config` registry for library-facing configuration (separate from app-facing `Axn.config`)
|
|
9
|
+
* **New** `description:` metadata option for field declarations (e.g., `expects :name, description: "The user's name"`)
|
|
10
|
+
* **New** `FieldConfig#metadata` and `SubfieldConfig#metadata` attributes with `#description` accessor
|
|
11
|
+
* **New** `Axn.extension_config.register_field_metadata_key(:key)` for wrapper gems to register custom metadata keys
|
|
12
|
+
* **New** Unknown validation keys now raise `ArgumentError` (catches typos like `nummericality:`)
|
|
13
|
+
* **New** Metadata can only be provided when declaring a single field (multi-field + metadata raises `ArgumentError`)
|
|
14
|
+
* [FEAT] Add `readers:` option validation: `readers: false` now raises `ArgumentError` when used without `on:` (only valid for subfields)
|
|
15
|
+
* [BUGFIX] `set_default_async(:sidekiq)` now properly triggers `AutoConfigure.register!`
|
|
16
|
+
* [BREAKING] Refactored context API for exception reporting and handlers:
|
|
17
|
+
* **Removed** `context_for_logging(direction)` instance method
|
|
18
|
+
* **Added** public `execution_context` method returning structured hash: `{ inputs: {...}, outputs: {...}, **extra_keys }`
|
|
19
|
+
* **Added** private `inputs_for_logging` / `outputs_for_logging` methods for automatic pre/post logging (do NOT include extra context)
|
|
20
|
+
* **Renamed** `set_logging_context` → `set_execution_context`, `clear_logging_context` → `clear_execution_context`, hook `additional_logging_context` → `additional_execution_context`
|
|
21
|
+
* **Reserved keys:** `:inputs` and `:outputs` cannot be set via `set_execution_context` or the hook—they always come from the action's contract
|
|
22
|
+
* **Internal:** Class method `context_for_logging(data:, direction:)` renamed to `_context_slice(data:, direction:)`
|
|
23
|
+
* Exception context now includes both `inputs` and `outputs` with additional context merged at the top level (not nested inside `inputs`)
|
|
24
|
+
|
|
6
25
|
## 0.1.0-alpha.4.1
|
|
7
26
|
* [BREAKING][BUGFIX] `fail!` in async jobs no longer triggers retries - business logic failures complete without retry (Sidekiq and ActiveJob adapters)
|
|
8
27
|
* [FEAT] Add `async_exception_reporting` config to control when `on_exception` triggers in async context (`:every_attempt`, `:first_and_exhausted`, `:only_exhausted`)
|
data/Rakefile
CHANGED
|
@@ -139,8 +139,9 @@ namespace :benchmark do
|
|
|
139
139
|
end
|
|
140
140
|
end
|
|
141
141
|
|
|
142
|
-
# Require verify to pass before release
|
|
143
|
-
|
|
142
|
+
# Require verify to pass before release. This relies on the default gem release task
|
|
143
|
+
# (from bundler/gem_tasks) depending on "build"; verify runs before build, so before push.
|
|
144
|
+
Rake::Task["build"].enhance([:verify])
|
|
144
145
|
|
|
145
146
|
# Automatically run benchmark:release after rake release
|
|
146
147
|
Rake::Task["release"].enhance do
|
data/docs/.vitepress/config.mjs
CHANGED
|
@@ -58,7 +58,6 @@ export default defineConfig({
|
|
|
58
58
|
{ text: 'Validating User Input', link: '/recipes/validating-user-input' },
|
|
59
59
|
{ text: 'Testing Actions', link: '/recipes/testing' },
|
|
60
60
|
{ text: 'RuboCop Integration', link: '/recipes/rubocop-integration' },
|
|
61
|
-
{ text: 'Formatting Context for Error Tracking', link: '/recipes/formatting-context-for-error-tracking' },
|
|
62
61
|
]
|
|
63
62
|
},
|
|
64
63
|
{
|
data/docs/advanced/rough.md
CHANGED
|
@@ -17,14 +17,26 @@ For information about logging configuration, see the [Configuration reference](/
|
|
|
17
17
|
- **Log levels**: [log_level](/reference/configuration#log-level)
|
|
18
18
|
- **Automatic logging**: [Automatic Logging](/reference/configuration#automatic-logging)
|
|
19
19
|
|
|
20
|
-
### `
|
|
20
|
+
### `execution_context`
|
|
21
21
|
|
|
22
|
-
The `
|
|
23
|
-
|
|
24
|
-
|
|
22
|
+
The `execution_context` method returns a structured hash for exception reporting and handlers:
|
|
23
|
+
|
|
24
|
+
```ruby
|
|
25
|
+
{
|
|
26
|
+
inputs: { ... }, # Filtered inbound fields (sensitive values removed)
|
|
27
|
+
outputs: { ... }, # Filtered outbound fields (sensitive values removed)
|
|
28
|
+
# ... any extra keys from set_execution_context or additional_execution_context hook
|
|
29
|
+
}
|
|
30
|
+
```
|
|
25
31
|
|
|
26
32
|
This is automatically passed to the `on_exception` hook. See [Adding Additional Context to Exception Logging](/reference/configuration#adding-additional-context-to-exception-logging) for customizing the context.
|
|
27
33
|
|
|
34
|
+
**Private methods for automatic logging:**
|
|
35
|
+
- `inputs_for_logging` - Returns only filtered inbound fields (used by pre-execution logs)
|
|
36
|
+
- `outputs_for_logging` - Returns only filtered outbound fields (used by post-execution logs)
|
|
37
|
+
|
|
38
|
+
These private methods do NOT include additional context from `set_execution_context` or the hook—they are specifically for automatic logging which only needs to show what the action was called with and what it produced.
|
|
39
|
+
|
|
28
40
|
### `#inspect` Support
|
|
29
41
|
|
|
30
42
|
Action instances provide a readable `#inspect` output that shows:
|
data/docs/reference/class.md
CHANGED
|
@@ -101,6 +101,16 @@ end
|
|
|
101
101
|
Defaults work the same way for subfields as they do for top-level fields - they are applied when the subfield is missing or explicitly `nil`, but not for blank values.
|
|
102
102
|
:::
|
|
103
103
|
|
|
104
|
+
#### Disabling subfield readers
|
|
105
|
+
|
|
106
|
+
By default, subfields create top-level reader methods (e.g., `random` in the example above). You can disable this with `readers: false`:
|
|
107
|
+
|
|
108
|
+
```ruby
|
|
109
|
+
expects :data, type: Hash, on: :event, readers: false
|
|
110
|
+
```
|
|
111
|
+
|
|
112
|
+
This is useful when you have duplicate sub-keys across different parent fields, or when you want to access subfields only through the parent. Note that `readers: false` is only valid for subfields (i.e., when using `on:`) — using it on top-level fields will raise an `ArgumentError`.
|
|
113
|
+
|
|
104
114
|
#### `preprocess`
|
|
105
115
|
`expects` also supports a `preprocess` option that, if set to a callable, will be executed _before_ applying any defaults or validations. This can be useful for type coercion, e.g.:
|
|
106
116
|
|
|
@@ -5,12 +5,13 @@ Somewhere at boot (e.g. `config/initializers/actions.rb` in Rails), you can call
|
|
|
5
5
|
```ruby
|
|
6
6
|
Axn.configure do |c|
|
|
7
7
|
c.log_level = :info
|
|
8
|
-
c.logger =
|
|
8
|
+
c.logger = Rails.logger
|
|
9
|
+
|
|
9
10
|
c.on_exception = proc do |e, action:, context:|
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
11
|
+
Honeybadger.notify(
|
|
12
|
+
"[#{action.class.name}] #{e.class.name}: #{e.message}",
|
|
13
|
+
context: context
|
|
14
|
+
)
|
|
14
15
|
end
|
|
15
16
|
end
|
|
16
17
|
```
|
|
@@ -22,44 +23,82 @@ By default any swallowed errors are noted in the logs, but it's _highly recommen
|
|
|
22
23
|
For example, if you're using Honeybadger this could look something like:
|
|
23
24
|
|
|
24
25
|
```ruby
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
end
|
|
26
|
+
Axn.configure do |c|
|
|
27
|
+
c.on_exception = proc do |e, action:, context:|
|
|
28
|
+
Honeybadger.notify(
|
|
29
|
+
"[#{action.class.name}] #{e.class.name}: #{e.message}",
|
|
30
|
+
context: context
|
|
31
|
+
)
|
|
32
32
|
end
|
|
33
|
+
end
|
|
33
34
|
```
|
|
34
35
|
|
|
35
36
|
**Note:** The `action:` and `context:` keyword arguments are *optional*—your proc can accept any combination of `e`, `action:`, and `context:`. Only the keyword arguments you explicitly declare will be passed to your handler. All of the following are valid:
|
|
36
37
|
|
|
37
38
|
```ruby
|
|
38
|
-
|
|
39
|
-
|
|
39
|
+
# Only exception object
|
|
40
|
+
c.on_exception = proc { |e| ... }
|
|
40
41
|
|
|
41
|
-
|
|
42
|
-
|
|
42
|
+
# Exception and action
|
|
43
|
+
c.on_exception = proc { |e, action:| ... }
|
|
43
44
|
|
|
44
|
-
|
|
45
|
-
|
|
45
|
+
# Exception and context
|
|
46
|
+
c.on_exception = proc { |e, context:| ... }
|
|
46
47
|
|
|
47
|
-
|
|
48
|
-
|
|
48
|
+
# Exception, action, and context
|
|
49
|
+
c.on_exception = proc { |e, action:, context:| ... }
|
|
49
50
|
```
|
|
50
51
|
|
|
51
|
-
|
|
52
|
+
### Context Structure
|
|
53
|
+
|
|
54
|
+
The `context` hash is automatically formatted and contains:
|
|
55
|
+
|
|
56
|
+
```ruby
|
|
57
|
+
{
|
|
58
|
+
inputs: { ... }, # Action inputs (declared expects fields only), formatted recursively
|
|
59
|
+
outputs: { ... }, # Action outputs (declared exposes fields only), formatted recursively
|
|
60
|
+
# ... any extra keys from set_execution_context or additional_execution_context hook
|
|
61
|
+
# e.g. client_strategy__last_request: { url: ..., method: ..., status: ... }
|
|
62
|
+
current_attributes: { ... }, # Current.attributes (auto-included if defined and present)
|
|
63
|
+
async: { ... } # Async retry info (only present in async context)
|
|
64
|
+
}
|
|
65
|
+
```
|
|
52
66
|
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
67
|
+
Additional context (like `client_strategy__last_request` from the `:client` strategy) appears at the top level alongside `inputs` and `outputs`, not nested inside them. Formatting is applied recursively to nested hashes and arrays.
|
|
68
|
+
|
|
69
|
+
**What gets formatted automatically:**
|
|
70
|
+
- **ActiveRecord objects** → GlobalID strings (e.g., `"gid://app/User/123"`)
|
|
71
|
+
- **ActionController::Parameters** → Plain hashes
|
|
72
|
+
- **Axn::FormObject instances** → Hash representation
|
|
73
|
+
|
|
74
|
+
**Example with all context fields:**
|
|
75
|
+
|
|
76
|
+
```ruby
|
|
77
|
+
Axn.configure do |c|
|
|
78
|
+
c.on_exception = proc do |e, action:, context:|
|
|
79
|
+
# context[:inputs] - Your action's inputs (formatted)
|
|
80
|
+
# context[:outputs] - Your action's outputs (formatted)
|
|
81
|
+
# context[:client_strategy__last_request] - Example extra key from :client strategy
|
|
82
|
+
# context[:current_attributes] - Rails Current.attributes (if present)
|
|
83
|
+
# context[:async] - Retry info (if in async context)
|
|
84
|
+
|
|
85
|
+
Honeybadger.notify(e, context: context)
|
|
86
|
+
end
|
|
87
|
+
end
|
|
88
|
+
```
|
|
89
|
+
|
|
90
|
+
### Additional Notes
|
|
91
|
+
|
|
92
|
+
- Sensitive fields (marked with `expects :foo, sensitive: true`) are automatically filtered to `"[FILTERED]"`
|
|
93
|
+
- If your handler raises an exception, the failure will be swallowed and logged
|
|
94
|
+
- This handler is global across _all_ actions. You can also specify per-action handlers via [the class-level declaration](/reference/class#on-exception)
|
|
95
|
+
- Complex objects are automatically formatted for error tracking systems
|
|
57
96
|
|
|
58
97
|
### Adding Additional Context to Exception Logging
|
|
59
98
|
|
|
60
99
|
When processing records in a loop or performing batch operations, you may want to include additional context (like which record is being processed) in exception logs. You can do this in two ways:
|
|
61
100
|
|
|
62
|
-
**Option 1: Explicit setter** - Call `
|
|
101
|
+
**Option 1: Explicit setter** - Call `set_execution_context` during execution:
|
|
63
102
|
|
|
64
103
|
```ruby
|
|
65
104
|
class ProcessPendingRecords
|
|
@@ -67,14 +106,14 @@ class ProcessPendingRecords
|
|
|
67
106
|
|
|
68
107
|
def call
|
|
69
108
|
pending_records.each do |record|
|
|
70
|
-
|
|
109
|
+
set_execution_context(current_record_id: record.id, batch_index: @index)
|
|
71
110
|
# ... process record ...
|
|
72
111
|
end
|
|
73
112
|
end
|
|
74
113
|
end
|
|
75
114
|
```
|
|
76
115
|
|
|
77
|
-
**Option 2: Hook method** - Define a private `
|
|
116
|
+
**Option 2: Hook method** - Define a private `additional_execution_context` method that returns a hash:
|
|
78
117
|
|
|
79
118
|
```ruby
|
|
80
119
|
class ProcessPendingRecords
|
|
@@ -89,7 +128,7 @@ class ProcessPendingRecords
|
|
|
89
128
|
|
|
90
129
|
private
|
|
91
130
|
|
|
92
|
-
def
|
|
131
|
+
def additional_execution_context
|
|
93
132
|
return {} unless @current_record
|
|
94
133
|
|
|
95
134
|
{
|
|
@@ -100,16 +139,19 @@ class ProcessPendingRecords
|
|
|
100
139
|
end
|
|
101
140
|
```
|
|
102
141
|
|
|
103
|
-
Both approaches can be used together - they will be merged. The additional context is **only** included in exception
|
|
142
|
+
Both approaches can be used together - they will be merged at the top level of the context hash. The additional context is **only** included in `execution_context` (used for exception reporting and handlers), not in normal pre/post execution logs, and is evaluated lazily (the hook method is only called when needed).
|
|
143
|
+
|
|
144
|
+
**Reserved keys:** The keys `:inputs` and `:outputs` are reserved. If you try to set them via `set_execution_context` or the hook, they will be ignored—the actual inputs and outputs always come from the action's contract.
|
|
104
145
|
|
|
105
|
-
Action-specific `on_exception` handlers can
|
|
146
|
+
Action-specific `on_exception` handlers can access the full context by calling `execution_context`:
|
|
106
147
|
|
|
107
148
|
```ruby
|
|
108
149
|
class ProcessPendingRecords
|
|
109
150
|
include Axn
|
|
110
151
|
|
|
111
152
|
on_exception do |exception:|
|
|
112
|
-
|
|
153
|
+
ctx = execution_context
|
|
154
|
+
log "Failed processing. Inputs: #{ctx[:inputs]}, Extra: #{ctx[:current_record_id]}"
|
|
113
155
|
# ... handle exception with context ...
|
|
114
156
|
end
|
|
115
157
|
end
|
|
@@ -342,22 +384,24 @@ When `on_exception` is triggered in an async context, the `context` hash include
|
|
|
342
384
|
```ruby
|
|
343
385
|
Axn.configure do |c|
|
|
344
386
|
c.on_exception = proc do |e, action:, context:|
|
|
387
|
+
# context[:async] is automatically included when in async context
|
|
388
|
+
# Available fields:
|
|
389
|
+
# context[:async][:adapter] # :sidekiq or :active_job
|
|
390
|
+
# context[:async][:attempt] # Current attempt (1-indexed)
|
|
391
|
+
# context[:async][:max_retries] # Max retry attempts
|
|
392
|
+
# context[:async][:job_id] # Job ID (if available)
|
|
393
|
+
# context[:async][:first_attempt] # true if first attempt
|
|
394
|
+
# context[:async][:retries_exhausted] # true if all retries exhausted
|
|
395
|
+
|
|
345
396
|
if context[:async]
|
|
346
|
-
#
|
|
347
|
-
|
|
348
|
-
# context[:async][:attempt] # Current attempt (1-indexed)
|
|
349
|
-
# context[:async][:max_retries] # Max retry attempts
|
|
350
|
-
# context[:async][:job_id] # Job ID (if available)
|
|
351
|
-
# context[:async][:first_attempt] # true if first attempt
|
|
352
|
-
# context[:async][:retries_exhausted] # true if all retries exhausted
|
|
353
|
-
|
|
354
|
-
Honeybadger.notify(e, context: {
|
|
355
|
-
axn_context: context,
|
|
397
|
+
# Add custom retry info to context
|
|
398
|
+
enhanced_context = context.merge(
|
|
356
399
|
retry_info: "Attempt #{context[:async][:attempt]} of #{context[:async][:max_retries]}"
|
|
357
|
-
|
|
400
|
+
)
|
|
401
|
+
Honeybadger.notify(e, context: enhanced_context)
|
|
358
402
|
else
|
|
359
|
-
# Foreground execution -
|
|
360
|
-
Honeybadger.notify(e, context:
|
|
403
|
+
# Foreground execution - context still includes inputs and current_attributes
|
|
404
|
+
Honeybadger.notify(e, context: context)
|
|
361
405
|
end
|
|
362
406
|
end
|
|
363
407
|
end
|
|
@@ -549,9 +593,10 @@ Axn.configure do |c|
|
|
|
549
593
|
|
|
550
594
|
# Exception handling
|
|
551
595
|
c.on_exception = proc do |e, action:, context:|
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
596
|
+
Honeybadger.notify(
|
|
597
|
+
"[#{action.class.name}] #{e.class.name}: #{e.message}",
|
|
598
|
+
context: context
|
|
599
|
+
)
|
|
555
600
|
end
|
|
556
601
|
|
|
557
602
|
# Observability
|
data/docs/strategies/form.md
CHANGED
|
@@ -62,7 +62,9 @@ end
|
|
|
62
62
|
|
|
63
63
|
### Inline Form Definition
|
|
64
64
|
|
|
65
|
-
You can
|
|
65
|
+
You can pass the form in directly as a block instead of a separate class.
|
|
66
|
+
|
|
67
|
+
**Block only** — the form class is not assigned to a constant, but it is given a `name` (default: the action’s name + `Form`, e.g. `"CreateUser::Form"`) so it shows up clearly in logging and exception reporting:
|
|
66
68
|
|
|
67
69
|
```ruby
|
|
68
70
|
class CreateUser
|
|
@@ -79,7 +81,14 @@ class CreateUser
|
|
|
79
81
|
end
|
|
80
82
|
```
|
|
81
83
|
|
|
82
|
-
|
|
84
|
+
**Block + type string** — the form class is named using the given string (e.g. `"CreateUser::Form"`). If that constant doesn't exist yet, the block defines the class and we assign it to that name:
|
|
85
|
+
|
|
86
|
+
```ruby
|
|
87
|
+
use :form, type: "CreateUser::Form" do
|
|
88
|
+
validates :email, presence: true
|
|
89
|
+
validates :name, presence: true
|
|
90
|
+
end
|
|
91
|
+
```
|
|
83
92
|
|
|
84
93
|
### Custom Field Names
|
|
85
94
|
|
|
@@ -18,12 +18,13 @@ class TransferFunds
|
|
|
18
18
|
end
|
|
19
19
|
```
|
|
20
20
|
|
|
21
|
-
**Important**: The transaction wraps
|
|
21
|
+
**Important**: The transaction wraps:
|
|
22
22
|
- `before` hooks
|
|
23
23
|
- The main `call` method
|
|
24
24
|
- `after` hooks
|
|
25
|
-
- Success/failure callbacks (`on_success`, `on_failure`, etc.)
|
|
26
25
|
|
|
27
|
-
|
|
26
|
+
So the transaction is still open until all of the above complete. If any of them raise or call `fail!`, the transaction is rolled back.
|
|
27
|
+
|
|
28
|
+
**`on_success` runs after the transaction commits.** It is invoked only after the transaction block has finished successfully. Use `on_success` (not `after` hooks) for work that should run outside the transaction—for example, calling an external HTTP service—so the DB transaction can commit and release the connection before that work runs. Putting slow or unreliable external calls inside `call` or `after` keeps the transaction open until they complete and can block the connection.
|
|
28
29
|
|
|
29
30
|
**Requirements**: Requires ActiveRecord to be available in your application.
|
|
@@ -42,6 +42,21 @@ module Axn
|
|
|
42
42
|
false
|
|
43
43
|
end
|
|
44
44
|
|
|
45
|
+
# Ensures middleware and death handler are registered for the current
|
|
46
|
+
# Axn.config.async_exception_reporting mode. Call when the Sidekiq adapter
|
|
47
|
+
# is included (e.g. via async :sidekiq or default async) so that default
|
|
48
|
+
# mode works without the app setting async_exception_reporting explicitly.
|
|
49
|
+
# Safe to call multiple times - register! is idempotent.
|
|
50
|
+
# No-ops when Sidekiq is a minimal stub (e.g. in unit tests without full Sidekiq).
|
|
51
|
+
def ensure_registered_for_current_config!
|
|
52
|
+
return unless defined?(::Sidekiq) && ::Sidekiq.respond_to?(:configure_server)
|
|
53
|
+
|
|
54
|
+
mode = Axn.config.async_exception_reporting
|
|
55
|
+
return if mode == :every_attempt
|
|
56
|
+
|
|
57
|
+
register!
|
|
58
|
+
end
|
|
59
|
+
|
|
45
60
|
# Registers both middleware and death handler.
|
|
46
61
|
# Safe to call multiple times - will not duplicate registrations.
|
|
47
62
|
def register!
|
|
@@ -18,6 +18,10 @@ module Axn
|
|
|
18
18
|
# Use Sidekiq::Job if available (Sidekiq 7+), otherwise error
|
|
19
19
|
raise LoadError, "Sidekiq::Job is not available. Please check your Sidekiq version." unless defined?(::Sidekiq::Job)
|
|
20
20
|
|
|
21
|
+
# Ensure middleware and death handler are registered for current async_exception_reporting
|
|
22
|
+
# (e.g. when async :sidekiq is used without ever setting async_exception_reporting).
|
|
23
|
+
AutoConfigure.ensure_registered_for_current_config!
|
|
24
|
+
|
|
21
25
|
include ::Sidekiq::Job
|
|
22
26
|
|
|
23
27
|
# Sidekiq's processor calls .new on the worker class from outside the class hierarchy
|
|
@@ -42,7 +46,7 @@ module Axn
|
|
|
42
46
|
normalized_options = _extract_and_normalize_async_options(kwargs)
|
|
43
47
|
|
|
44
48
|
# Convert kwargs to string keys and handle GlobalID conversion
|
|
45
|
-
job_kwargs = Axn::
|
|
49
|
+
job_kwargs = Axn::Internal::GlobalIdSerialization.serialize(kwargs)
|
|
46
50
|
|
|
47
51
|
# Process normalized async options if present
|
|
48
52
|
if normalized_options
|
|
@@ -58,7 +62,7 @@ module Axn
|
|
|
58
62
|
end
|
|
59
63
|
|
|
60
64
|
def perform(*args)
|
|
61
|
-
context = Axn::
|
|
65
|
+
context = Axn::Internal::GlobalIdSerialization.deserialize(args.first)
|
|
62
66
|
|
|
63
67
|
# Validate Sidekiq configuration once on first job execution in a real server context.
|
|
64
68
|
# Skip validation when:
|
|
@@ -28,18 +28,18 @@ module Axn
|
|
|
28
28
|
target = target_class_name.constantize
|
|
29
29
|
|
|
30
30
|
# Deserialize static_args (convert GlobalID strings back to objects)
|
|
31
|
-
deserialized_static_args = Axn::
|
|
31
|
+
deserialized_static_args = Axn::Internal::GlobalIdSerialization.deserialize(static_args)
|
|
32
32
|
|
|
33
33
|
count = self.class.execute_iteration(
|
|
34
34
|
target,
|
|
35
35
|
**deserialized_static_args,
|
|
36
|
-
on_progress: method(:
|
|
36
|
+
on_progress: method(:set_execution_context),
|
|
37
37
|
)
|
|
38
38
|
|
|
39
39
|
message_parts = ["Batch enqueued #{count} jobs for #{target.name}"]
|
|
40
40
|
message_parts << "with explicit args: #{static_args.inspect}" if static_args.any?
|
|
41
41
|
|
|
42
|
-
Axn::
|
|
42
|
+
Axn::Internal::CallLogger.log_at_level(
|
|
43
43
|
self.class,
|
|
44
44
|
level: :info,
|
|
45
45
|
message_parts:,
|
|
@@ -77,7 +77,7 @@ module Axn
|
|
|
77
77
|
execute_iteration_without_logging(target, **static_args)
|
|
78
78
|
else
|
|
79
79
|
# Serialize static_args for Sidekiq (convert GlobalID objects, stringify keys)
|
|
80
|
-
serialized_static_args = Axn::
|
|
80
|
+
serialized_static_args = Axn::Internal::GlobalIdSerialization.serialize(resolved_static)
|
|
81
81
|
|
|
82
82
|
# Execute iteration in background via EnqueueAllOrchestrator
|
|
83
83
|
call_async(target_class_name: target.name, static_args: serialized_static_args)
|
|
@@ -280,7 +280,7 @@ module Axn
|
|
|
280
280
|
filter_result = begin
|
|
281
281
|
config.filter_block.call(item)
|
|
282
282
|
rescue StandardError => e
|
|
283
|
-
Axn::Internal::
|
|
283
|
+
Axn::Internal::PipingError.swallow(
|
|
284
284
|
"filter block for :#{config.field}",
|
|
285
285
|
exception: e,
|
|
286
286
|
)
|
|
@@ -294,7 +294,7 @@ module Axn
|
|
|
294
294
|
begin
|
|
295
295
|
item.public_send(config.via)
|
|
296
296
|
rescue StandardError => e
|
|
297
|
-
Axn::Internal::
|
|
297
|
+
Axn::Internal::PipingError.swallow(
|
|
298
298
|
"via extraction (:#{config.via}) for :#{config.field}",
|
|
299
299
|
exception: e,
|
|
300
300
|
)
|
|
@@ -16,8 +16,8 @@ module Axn
|
|
|
16
16
|
# @param extra_context [Hash] additional context to merge (e.g., discarded: true, _job_metadata)
|
|
17
17
|
# @param log_prefix [String] prefix for error logging (e.g., "Sidekiq death handler")
|
|
18
18
|
def trigger_on_exception(exception:, action_class:, retry_context:, job_args:, extra_context: {}, log_prefix: "async")
|
|
19
|
-
# Filter sensitive values using the action class's
|
|
20
|
-
filtered_context = action_class.
|
|
19
|
+
# Filter sensitive values using the action class's internal _context_slice
|
|
20
|
+
filtered_context = action_class._context_slice(data: job_args, direction: :inbound)
|
|
21
21
|
|
|
22
22
|
# Build final context with async info (avoid mutating extra_context)
|
|
23
23
|
async_extra = extra_context[:async] || {}
|
|
@@ -31,7 +31,7 @@ module Axn
|
|
|
31
31
|
# Trigger on_exception
|
|
32
32
|
Axn.config.on_exception(exception, action: proxy_action, context:)
|
|
33
33
|
rescue StandardError => e
|
|
34
|
-
Axn::Internal::
|
|
34
|
+
Axn::Internal::PipingError.swallow("in #{log_prefix}", exception: e)
|
|
35
35
|
end
|
|
36
36
|
end
|
|
37
37
|
|
data/lib/axn/async.rb
CHANGED
|
@@ -104,11 +104,11 @@ module Axn
|
|
|
104
104
|
ActiveSupport::Notifications.instrument("axn.call_async", payload)
|
|
105
105
|
rescue StandardError => e
|
|
106
106
|
# Don't raise in notification emission to avoid interfering with async enqueueing
|
|
107
|
-
Axn::Internal::
|
|
107
|
+
Axn::Internal::PipingError.swallow("emitting notification for axn.call_async", action_class: self, exception: e)
|
|
108
108
|
end
|
|
109
109
|
|
|
110
110
|
def _log_async_invocation(kwargs, adapter_name:)
|
|
111
|
-
Axn::
|
|
111
|
+
Axn::Internal::CallLogger.log_at_level(
|
|
112
112
|
self,
|
|
113
113
|
level: log_calls_level,
|
|
114
114
|
message_parts: ["Enqueueing async execution via #{adapter_name}"],
|
data/lib/axn/configuration.rb
CHANGED
|
@@ -7,7 +7,7 @@ module Axn
|
|
|
7
7
|
|
|
8
8
|
class Configuration
|
|
9
9
|
attr_accessor :emit_metrics, :raise_piping_errors_in_dev
|
|
10
|
-
attr_writer :logger, :env, :on_exception, :additional_includes, :log_level, :rails
|
|
10
|
+
attr_writer :logger, :env, :on_exception, :additional_includes, :log_level, :rails, :_include_retry_command_in_exceptions
|
|
11
11
|
|
|
12
12
|
# Controls when on_exception is triggered in async context (Sidekiq/ActiveJob).
|
|
13
13
|
# Options:
|
|
@@ -40,6 +40,13 @@ module Axn
|
|
|
40
40
|
|
|
41
41
|
def additional_includes = @additional_includes ||= []
|
|
42
42
|
|
|
43
|
+
# EXPERIMENTAL: When true, automatically generates a retry command in exception context.
|
|
44
|
+
# This is marked experimental because the retry command generation may not work well
|
|
45
|
+
# for all action types (e.g., actions with complex object dependencies).
|
|
46
|
+
def _include_retry_command_in_exceptions
|
|
47
|
+
@_include_retry_command_in_exceptions.nil? ? false : @_include_retry_command_in_exceptions
|
|
48
|
+
end
|
|
49
|
+
|
|
43
50
|
def _default_async_adapter = @default_async_adapter ||= false
|
|
44
51
|
def _default_async_config = @default_async_config ||= {}
|
|
45
52
|
def _default_async_config_block = @default_async_config_block
|
|
@@ -51,6 +58,7 @@ module Axn
|
|
|
51
58
|
@default_async_config = config.any? ? config : {}
|
|
52
59
|
@default_async_config_block = block_given? ? block : nil
|
|
53
60
|
|
|
61
|
+
_ensure_async_exception_reporting_registered_for_adapter(adapter)
|
|
54
62
|
_apply_async_to_enqueue_all_orchestrator
|
|
55
63
|
end
|
|
56
64
|
|
|
@@ -65,6 +73,7 @@ module Axn
|
|
|
65
73
|
@enqueue_all_async_config = config.any? ? config : {}
|
|
66
74
|
@enqueue_all_async_config_block = block_given? ? block : nil
|
|
67
75
|
|
|
76
|
+
_ensure_async_exception_reporting_registered_for_adapter(adapter)
|
|
68
77
|
_apply_async_to_enqueue_all_orchestrator
|
|
69
78
|
end
|
|
70
79
|
|
|
@@ -87,7 +96,7 @@ module Axn
|
|
|
87
96
|
return unless @on_exception
|
|
88
97
|
|
|
89
98
|
# Only pass the args and kwargs that the given block expects
|
|
90
|
-
Axn::
|
|
99
|
+
Axn::Internal::Callable.call_with_desired_shape(@on_exception, args: [e], kwargs: { action:, context: })
|
|
91
100
|
end
|
|
92
101
|
|
|
93
102
|
def logger
|
|
@@ -128,6 +137,20 @@ module Axn
|
|
|
128
137
|
)
|
|
129
138
|
end
|
|
130
139
|
|
|
140
|
+
# Ensures the given async adapter has exception-reporting components registered
|
|
141
|
+
# for the current async_exception_reporting mode (e.g. Sidekiq middleware/death handler).
|
|
142
|
+
# Called when setting default or enqueue_all async adapter so the default mode works
|
|
143
|
+
# without the app having to set async_exception_reporting explicitly.
|
|
144
|
+
def _ensure_async_exception_reporting_registered_for_adapter(adapter)
|
|
145
|
+
return if adapter.nil? || adapter == false
|
|
146
|
+
|
|
147
|
+
case adapter
|
|
148
|
+
when :sidekiq
|
|
149
|
+
_auto_configure_sidekiq_for_async_exception_reporting(async_exception_reporting)
|
|
150
|
+
end
|
|
151
|
+
# Active Job has no global registration (per-class proxy with after_discard).
|
|
152
|
+
end
|
|
153
|
+
|
|
131
154
|
# Auto-configures Sidekiq middleware and death handler when async_exception_reporting
|
|
132
155
|
# is set to a mode that requires them.
|
|
133
156
|
#
|