chaotic_job 0.3.0 → 0.4.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 +4 -0
- data/README.md +49 -21
- data/lib/chaotic_job/glitch.rb +99 -30
- data/lib/chaotic_job/journal.rb +2 -1
- data/lib/chaotic_job/scenario.rb +3 -3
- data/lib/chaotic_job/simulation.rb +3 -13
- data/lib/chaotic_job/version.rb +1 -1
- data/lib/chaotic_job.rb +2 -2
- metadata +3 -4
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 98b01482666de5069e40dbe4eeb828cb527e4dd31004b3900a134ceaec5cb42a
|
4
|
+
data.tar.gz: f3df1be0fa3192aa842b74fa56754e56ed071f48d3c2fd027ca88d05da3f5e90
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: aff436f38402fddae8e62a7dbb441dc8949169abd1aa992fdf5e59f33e007f8dc3aba29787a3cea4bf3d421698ed1ac76c171388c26a3d2ab884e7880296ba84
|
7
|
+
data.tar.gz: 72986fa7274aade0e1fe7f9a5b71e4911467f18bf4929b90960ba62b03721611e46da8958c0536a92f92e98f4b4a2c7315e674db73bbcc6866cc177070a6f92f
|
data/CHANGELOG.md
CHANGED
@@ -1,5 +1,9 @@
|
|
1
1
|
## [Unreleased]
|
2
2
|
|
3
|
+
## [0.4.0] - 2025-05-27
|
4
|
+
|
5
|
+
- Allow a Glitch to be defined for a method call or method return [#4](https://github.com/fractaledmind/chaotic_job/pull/4)
|
6
|
+
|
3
7
|
## [0.3.0] - 2024-12-17
|
4
8
|
|
5
9
|
- Ensure that assertion failure messages raised within a simulation contain the scenario description
|
data/README.md
CHANGED
@@ -44,7 +44,7 @@ The `ChaoticJob::Helpers` module provides 6 methods, 4 of which simply allow you
|
|
44
44
|
|
45
45
|
### Glitches
|
46
46
|
|
47
|
-
A central concept in `ChaoticJob` is the _glitch_. A glitch is an error injected into the job execution flow via a [`TracePoint`](https://docs.ruby-lang.org/en/master/TracePoint.html). Glitches are transient errors, which means they occur
|
47
|
+
A central concept in `ChaoticJob` is the _glitch_. A glitch is an error injected into the job execution flow via a [`TracePoint`](https://docs.ruby-lang.org/en/master/TracePoint.html). Glitches are transient errors, which means they occur _once_ and **only once**, making them perfect for testing a job's resilience to unpredictable failures that can occur while running jobs, like network issues, upstream API outages, rate limits, or infrastructure failure. By default, `ChaoticJob` raises a custom error defined by the gem (`ChaoticJob::RetryableError`), which the internals of the gem ensure that the job under test is configured to retry on; you can, however, raise specific errors as needed when setting up your [scenarios](#simulating-failures). By forcing a retry via the error handling mechanisms of Active Job, glitches are a simple but effective way to test that your job is resilient to any kind of transient error that the job is configured to retry on.
|
48
48
|
|
49
49
|
### Performing Jobs
|
50
50
|
|
@@ -107,7 +107,7 @@ test "scenario of a simple job" do
|
|
107
107
|
def step_3; ChaoticJob::Journal.log; end
|
108
108
|
end
|
109
109
|
|
110
|
-
run_scenario(Job.new, glitch: [
|
110
|
+
run_scenario(Job.new, glitch: [:before_call, "Job#step_3"])
|
111
111
|
|
112
112
|
assert_equal 5, ChaoticJob::Journal.total
|
113
113
|
end
|
@@ -124,7 +124,16 @@ end
|
|
124
124
|
> | `Journal.entries` | get all of the logged values under the default scope |
|
125
125
|
> | `Journal.entries(scope: :special)` | get all of the logged values under a particular scope |
|
126
126
|
|
127
|
-
In this example, the job being tested is defined within the test case. You can, of course, also test jobs defined in your application. The key detail is the `glitch` keyword argument. A "glitch" is simply a tuple that describes precisely where you would like the failure to occur. The first element of the tuple is the
|
127
|
+
In this example, the job being tested is defined within the test case. You can, of course, also test jobs defined in your application. The key detail is the `glitch` keyword argument. A "glitch" is simply a tuple that describes precisely where you would like the failure to occur. The first element of the tuple is the _kind_ of glitch, which can be either `:before_line`, `:before_call`, or `:before_return`. These refer to the three kinds of `TracePoint` events that the gem hooks into. The second element is the _key_ for the code that will be affected by the glitch. This _key_ is a specially formatted string that defines the specific bit of code that the glitch should be inserted before. The different kinds of glitches are identified by different kinds of keys:
|
128
|
+
|kind|key format|key example|
|
129
|
+
|---|---|---|
|
130
|
+
|`:before_line`|`"#{file_path}:#{line_number}"`|`"/Users/you/path/to/file.rb:123"`|
|
131
|
+
|`:before_call`|`"#{YourClass.name}(.|#)#{method_name}"`|`"YourClass.some_class_method"`|
|
132
|
+
|`:before_return`|`"#{YourClass.name}(.|#)#{method_name}"`|`"YourClass#some_instance_method"`|
|
133
|
+
|
134
|
+
As you can see, the `:before_call` and `:before_return` keys are formatted the same, and can identify any instance (`#`) or class (`.`) method.
|
135
|
+
|
136
|
+
What the example scenario above does is inject a glitch before the `step_3` method is called, here:
|
128
137
|
|
129
138
|
```ruby
|
130
139
|
def perform
|
@@ -135,12 +144,33 @@ def perform
|
|
135
144
|
end
|
136
145
|
```
|
137
146
|
|
147
|
+
If we wanted to inject a glitch right before the `step_3` method finishes, we could define the glitch as a `:before_return`, like this:
|
148
|
+
|
149
|
+
```ruby
|
150
|
+
run_scenario(Job.new, glitch: [:before_return, "Job#step_3"])
|
151
|
+
```
|
152
|
+
|
153
|
+
and it would inject the transient error right here:
|
154
|
+
|
155
|
+
```ruby
|
156
|
+
def step_3
|
157
|
+
ChaoticJob::Journal.log
|
158
|
+
# <-- HERE
|
159
|
+
end
|
160
|
+
```
|
161
|
+
|
162
|
+
Finally, if you need to inject a glitch right before a particular line of code is executed that is neither a method call nor a method return, you can use the `:before_line` key, like this:
|
163
|
+
|
164
|
+
```ruby
|
165
|
+
run_scenario(Job.new, glitch: [:before_line, "#{__FILE__}:6"])
|
166
|
+
```
|
167
|
+
|
138
168
|
If you want to simulate multiple glitches affecting a job run, you can use the plural `glitches` keyword argument instead and pass an array of tuples:
|
139
169
|
|
140
170
|
```ruby
|
141
171
|
run_scenario(Job.new, glitches: [
|
142
|
-
[
|
143
|
-
[
|
172
|
+
[:before_call, "Job#step_1"],
|
173
|
+
[:before_return, "Job#step_1"]
|
144
174
|
])
|
145
175
|
```
|
146
176
|
|
@@ -166,28 +196,26 @@ test "simulation of a simple job" do
|
|
166
196
|
end
|
167
197
|
```
|
168
198
|
|
169
|
-
|
199
|
+
More specifically, it will create a scenario injecting a glitch before every line of code executed in your job. So, in this example, the simulation will run 12 scenarios:
|
170
200
|
|
171
201
|
```ruby
|
172
202
|
[
|
173
|
-
[[:
|
174
|
-
[[:
|
175
|
-
[[:
|
176
|
-
[[:
|
177
|
-
[[:
|
178
|
-
[[:
|
179
|
-
[[:
|
180
|
-
[[:
|
181
|
-
[[:
|
182
|
-
[[:
|
183
|
-
[[:
|
184
|
-
[[:
|
203
|
+
[[:before_line, "test_chaotic_job.rb:69"]],
|
204
|
+
[[:before_line, "test_chaotic_job.rb:75"]],
|
205
|
+
[[:before_line, "test_chaotic_job.rb:74"]],
|
206
|
+
[[:before_line, "test_chaotic_job.rb:74"]],
|
207
|
+
[[:before_line, "test_chaotic_job.rb:68"]],
|
208
|
+
[[:before_line, "test_chaotic_job.rb:70"]],
|
209
|
+
[[:before_line, "test_chaotic_job.rb:68"]],
|
210
|
+
[[:before_line, "test_chaotic_job.rb:73"]],
|
211
|
+
[[:before_line, "test_chaotic_job.rb:75"]],
|
212
|
+
[[:before_line, "test_chaotic_job.rb:69"]],
|
213
|
+
[[:before_line, "test_chaotic_job.rb:70"]],
|
214
|
+
[[:before_line, "test_chaotic_job.rb:73"]]
|
185
215
|
]
|
186
216
|
```
|
187
217
|
|
188
|
-
It generates all possible glitch scenarios by performing your job once with a [`TracePoint`](https://docs.ruby-lang.org/en/master/TracePoint.html) that captures each line executed in your job. It then computes all possible glitch locations to produce a set of scenarios that will be run.
|
189
|
-
|
190
|
-
[^1]: The logic to determine all possible glitch locations essentially produces two locations, before and after, for each executed line. It then dedupes the functionally equivalent locations of `[:after, "file:1"]` and `[:before, "file:2"]`.
|
218
|
+
It generates all possible glitch scenarios by performing your job once with a [`TracePoint`](https://docs.ruby-lang.org/en/master/TracePoint.html) that captures each line executed in your job. It then computes all possible glitch locations to produce a set of scenarios that will be run. The block that you pass to `run_simulation` will be called for each scenario, allowing you to make assertions about the behavior of your job under all scenarios.
|
191
219
|
|
192
220
|
In your application tests, you will want to make assertions about the side-effects that your job performs, asserting that they are correctly idempotent (only occur once) and result in the correct state.
|
193
221
|
|
data/lib/chaotic_job/glitch.rb
CHANGED
@@ -1,46 +1,47 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
|
-
# Glitch.new.
|
4
|
-
# Glitch.new.
|
3
|
+
# Glitch.new.before_line("job_crucible.rb:10") { do_anything }
|
4
|
+
# Glitch.new.before_call("Model#method", String, name: "Joel") { do_anything }
|
5
|
+
# Glitch.new.before_return("Model#method", String, name: "Joel") { do_anything }
|
5
6
|
# Glitch.new.inject! { execute code to glitch }
|
6
7
|
|
7
8
|
module ChaoticJob
|
8
9
|
class Glitch
|
9
10
|
def initialize
|
10
11
|
@breakpoints = {}
|
11
|
-
@file_contents = {}
|
12
12
|
end
|
13
13
|
|
14
|
-
def
|
15
|
-
set_breakpoint(
|
14
|
+
def before_line(key, &block)
|
15
|
+
set_breakpoint(key, :line, &block)
|
16
|
+
self
|
16
17
|
end
|
17
18
|
|
18
|
-
def
|
19
|
-
set_breakpoint(
|
19
|
+
def before_call(key, ...)
|
20
|
+
set_breakpoint(key, :call, ...)
|
21
|
+
self
|
20
22
|
end
|
21
23
|
|
22
|
-
def
|
23
|
-
|
24
|
-
|
25
|
-
|
26
|
-
# content = @file_contents[tp.path]
|
27
|
-
# line = content[tp.lineno - 1]
|
28
|
-
# next unless line.match? key
|
24
|
+
def before_return(key, return_type = nil, &block)
|
25
|
+
set_breakpoint(key, :return, retval: return_type, &block)
|
26
|
+
self
|
27
|
+
end
|
29
28
|
|
30
|
-
|
31
|
-
|
29
|
+
def inject!(&block)
|
30
|
+
breakpoints = @breakpoints
|
32
31
|
|
33
|
-
|
34
|
-
|
35
|
-
|
36
|
-
|
32
|
+
trace = TracePoint.new(:line, :call, :return) do |tp|
|
33
|
+
# :nocov: SimpleCov cannot track code executed _within_ a TracePoint
|
34
|
+
key = derive_key(tp)
|
35
|
+
matchers = derive_matchers(tp)
|
36
|
+
|
37
|
+
next unless (defn = breakpoints.dig(key, tp.event))
|
38
|
+
next unless matches?(defn, matchers)
|
39
|
+
|
40
|
+
execute_block(defn)
|
41
|
+
# :nocov:
|
37
42
|
end
|
38
43
|
|
39
|
-
trace.enable
|
40
|
-
yield if block_given?
|
41
|
-
ensure
|
42
|
-
trace.disable
|
43
|
-
execute_block(@breakpoints[prev_key][:after]) if prev_key && @breakpoints.key?(prev_key)
|
44
|
+
trace.enable(&block)
|
44
45
|
end
|
45
46
|
|
46
47
|
def all_executed?
|
@@ -57,11 +58,51 @@ module ChaoticJob
|
|
57
58
|
|
58
59
|
private
|
59
60
|
|
60
|
-
def set_breakpoint(
|
61
|
-
@breakpoints[
|
62
|
-
|
63
|
-
|
64
|
-
|
61
|
+
def set_breakpoint(key, event, *args, retval: nil, **kwargs, &block)
|
62
|
+
@breakpoints[key] ||= {}
|
63
|
+
@breakpoints[key][event] = {args: args, kwargs: kwargs, retval: retval, block: block, executed: false}
|
64
|
+
end
|
65
|
+
|
66
|
+
# :nocov: SimpleCov cannot track code executed _within_ a TracePoint
|
67
|
+
def matches?(defn, matchers)
|
68
|
+
return true if defn.nil?
|
69
|
+
return true if matchers.nil?
|
70
|
+
return true if defn[:args].empty? && defn[:kwargs].empty? && defn[:retval].nil?
|
71
|
+
|
72
|
+
args = []
|
73
|
+
kwargs = {}
|
74
|
+
retval = nil
|
75
|
+
|
76
|
+
matchers.each do |kind, name, value|
|
77
|
+
case kind
|
78
|
+
when :req
|
79
|
+
args << value
|
80
|
+
when :opt
|
81
|
+
args << value if value
|
82
|
+
when :rest
|
83
|
+
args.concat(value) if value
|
84
|
+
when :keyreq
|
85
|
+
kwargs[name] = value
|
86
|
+
when :key
|
87
|
+
kwargs[name] = value
|
88
|
+
when :keyrest
|
89
|
+
kwargs.merge!(value) if value
|
90
|
+
when :retval
|
91
|
+
retval = value
|
92
|
+
end
|
93
|
+
end
|
94
|
+
|
95
|
+
defn[:args].each_with_index do |type, index|
|
96
|
+
return false unless type === args[index]
|
97
|
+
end
|
98
|
+
|
99
|
+
defn[:kwargs].each do |key, type|
|
100
|
+
return false unless type === kwargs[key]
|
101
|
+
end
|
102
|
+
|
103
|
+
return false unless defn[:retval] === retval
|
104
|
+
|
105
|
+
true
|
65
106
|
end
|
66
107
|
|
67
108
|
def execute_block(handler)
|
@@ -71,5 +112,33 @@ module ChaoticJob
|
|
71
112
|
handler[:executed] = true
|
72
113
|
handler[:block].call
|
73
114
|
end
|
115
|
+
|
116
|
+
def derive_key(trace)
|
117
|
+
case trace.event
|
118
|
+
when :line
|
119
|
+
"#{trace.path}:#{trace.lineno}"
|
120
|
+
when :call, :return
|
121
|
+
if Module === trace.self
|
122
|
+
"#{trace.self}.#{trace.method_id}"
|
123
|
+
else
|
124
|
+
"#{trace.defined_class}##{trace.method_id}"
|
125
|
+
end
|
126
|
+
end
|
127
|
+
end
|
128
|
+
|
129
|
+
def derive_matchers(trace)
|
130
|
+
case trace.event
|
131
|
+
when :line
|
132
|
+
nil
|
133
|
+
when :call
|
134
|
+
trace.parameters.map do |type, name|
|
135
|
+
value = trace.binding.local_variable_get(name) rescue nil # standard:disable Style/RescueModifier
|
136
|
+
[type, name, value]
|
137
|
+
end
|
138
|
+
when :return
|
139
|
+
[[:retval, nil, trace.return_value]]
|
140
|
+
end
|
141
|
+
end
|
142
|
+
# :nocov:
|
74
143
|
end
|
75
144
|
end
|
data/lib/chaotic_job/journal.rb
CHANGED
@@ -18,6 +18,7 @@ module ChaoticJob
|
|
18
18
|
@logs ||= {}
|
19
19
|
@logs[scope] ||= []
|
20
20
|
@logs[scope] << item
|
21
|
+
item
|
21
22
|
end
|
22
23
|
|
23
24
|
def size(scope: :default)
|
@@ -29,7 +30,7 @@ module ChaoticJob
|
|
29
30
|
end
|
30
31
|
|
31
32
|
def top(scope: :default)
|
32
|
-
entries&.first
|
33
|
+
entries(scope: scope)&.first
|
33
34
|
end
|
34
35
|
end
|
35
36
|
end
|
data/lib/chaotic_job/scenario.rb
CHANGED
@@ -10,7 +10,7 @@ module ChaoticJob
|
|
10
10
|
def initialize(job, glitches:, raise: RetryableError, capture: /active_job/)
|
11
11
|
@job = job
|
12
12
|
@glitches = glitches
|
13
|
-
@raise = raise
|
13
|
+
@raise = binding.local_variable_get(:raise)
|
14
14
|
@capture = capture
|
15
15
|
@glitch = nil
|
16
16
|
@events = []
|
@@ -45,8 +45,8 @@ module ChaoticJob
|
|
45
45
|
|
46
46
|
def glitch
|
47
47
|
@glitch ||= Glitch.new.tap do |glitch|
|
48
|
-
@glitches.each do |
|
49
|
-
glitch.public_send(
|
48
|
+
@glitches.each do |kind, location, _description|
|
49
|
+
glitch.public_send(kind, location) { raise @raise }
|
50
50
|
end
|
51
51
|
end
|
52
52
|
end
|
@@ -27,20 +27,10 @@ module ChaoticJob
|
|
27
27
|
end
|
28
28
|
|
29
29
|
def permutations
|
30
|
-
callstack = capture_callstack.
|
31
|
-
error_locations = callstack.
|
32
|
-
|
33
|
-
rpath, rno = right
|
34
|
-
key = "#{lpath}:#{lno}"
|
35
|
-
# inject an error before and after each non-adjacent line
|
36
|
-
if lpath == rpath && rno == lno + 1
|
37
|
-
[[:before, key]]
|
38
|
-
else
|
39
|
-
[[:before, key], [:after, key]]
|
40
|
-
end
|
30
|
+
callstack = capture_callstack.map { |path, line| "#{path}:#{line}" }
|
31
|
+
error_locations = callstack.map do |path, lineno|
|
32
|
+
[:before_line, "#{path}:#{lineno}"]
|
41
33
|
end
|
42
|
-
final_key = callstack.last.join(":")
|
43
|
-
error_locations.push [:before, final_key], [:after, final_key]
|
44
34
|
error_locations.permutation(@depth)
|
45
35
|
end
|
46
36
|
|
data/lib/chaotic_job/version.rb
CHANGED
data/lib/chaotic_job.rb
CHANGED
@@ -74,7 +74,7 @@ module ChaoticJob
|
|
74
74
|
|
75
75
|
def run_scenario(job, glitch: nil, glitches: nil, raise: nil, capture: nil, &block)
|
76
76
|
kwargs = {glitches: glitches || [glitch]}
|
77
|
-
kwargs[:raise] = raise if raise
|
77
|
+
kwargs[:raise] = binding.local_variable_get(:raise) if binding.local_variable_get(:raise)
|
78
78
|
kwargs[:capture] = capture if capture
|
79
79
|
if block
|
80
80
|
Scenario.new(job, **kwargs).run(&block)
|
@@ -89,7 +89,7 @@ module ChaoticJob
|
|
89
89
|
contextual_msg = lambda do
|
90
90
|
# copied from the original `assert` method in Minitest::Assertions
|
91
91
|
default_msg = "Expected #{mu_pp test} to be truthy."
|
92
|
-
custom_msg =
|
92
|
+
custom_msg = msg.is_a?(Proc) ? msg.call : msg
|
93
93
|
full_msg = custom_msg || default_msg
|
94
94
|
" #{@simulation_scenario}\n#{full_msg}"
|
95
95
|
end
|
metadata
CHANGED
@@ -1,14 +1,13 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: chaotic_job
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.
|
4
|
+
version: 0.4.0
|
5
5
|
platform: ruby
|
6
|
-
original_platform: ''
|
7
6
|
authors:
|
8
7
|
- Stephen Margheim
|
9
8
|
bindir: exe
|
10
9
|
cert_chain: []
|
11
|
-
date:
|
10
|
+
date: 2025-05-27 00:00:00.000000000 Z
|
12
11
|
dependencies:
|
13
12
|
- !ruby/object:Gem::Dependency
|
14
13
|
name: activejob
|
@@ -65,7 +64,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
65
64
|
- !ruby/object:Gem::Version
|
66
65
|
version: '0'
|
67
66
|
requirements: []
|
68
|
-
rubygems_version: 3.6.
|
67
|
+
rubygems_version: 3.6.3
|
69
68
|
specification_version: 4
|
70
69
|
summary: Test ActiveJobs for reliability and resilience.
|
71
70
|
test_files: []
|