chaotic_job 0.4.0 → 0.6.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 +8 -0
- data/README.md +37 -16
- data/lib/chaotic_job/glitch.rb +27 -12
- data/lib/chaotic_job/scenario.rb +5 -19
- data/lib/chaotic_job/simulation.rb +11 -18
- data/lib/chaotic_job/tracer.rb +50 -0
- data/lib/chaotic_job/version.rb +1 -1
- data/lib/chaotic_job.rb +14 -5
- metadata +3 -2
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 10db3cce1a0eedffe044477a348122d781e19cfbb6f561c9e389926d7de21153
|
4
|
+
data.tar.gz: a48f0d63526613593a25cc92d1c3eca794adb99466ef697a3c932e581a08434a
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: a1ea02d93adf652616f25478c8d7f628f30d56137485b10988bd2f08b9a62153327cd60c787555d7ba4286a7c26f94616ee45c92a58778e0689011461a39bb3b
|
7
|
+
data.tar.gz: 6d94662caef5559bca3de08c69674ad10dd4fa349fbd8434a42dc65c8baa8ba6da13f6cc8d13b9af815f4610148a874ba3f9fa0b9e5c8aa97e7b131afc5c3689
|
data/CHANGELOG.md
CHANGED
@@ -1,5 +1,13 @@
|
|
1
1
|
## [Unreleased]
|
2
2
|
|
3
|
+
## [0.6.0] - 2025-06-08
|
4
|
+
|
5
|
+
- `run_scenario` requires a Glitch instance [#5](https://github.com/fractaledmind/chaotic_job/pull/5)
|
6
|
+
|
7
|
+
## [0.5.0] - 2025-06-04
|
8
|
+
|
9
|
+
- Add a Tracer class [#3](https://github.com/fractaledmind/chaotic_job/pull/3)
|
10
|
+
|
3
11
|
## [0.4.0] - 2025-05-27
|
4
12
|
|
5
13
|
- Allow a Glitch to be defined for a method call or method return [#4](https://github.com/fractaledmind/chaotic_job/pull/4)
|
data/README.md
CHANGED
@@ -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: ChaoticJob::Glitch.before_call("Job#step_3"))
|
111
111
|
|
112
112
|
assert_equal 5, ChaoticJob::Journal.total
|
113
113
|
end
|
@@ -124,14 +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.
|
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.
|
128
|
+
|
129
|
+
A "glitch" is describes precisely where you would like the failure to occur. The description is composed first of 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
130
|
|kind|key format|key example|
|
129
131
|
|---|---|---|
|
130
|
-
|
131
|
-
|
132
|
-
|
132
|
+
|`before_line`|`"#{file_path}:#{line_number}"`|`"/Users/you/path/to/file.rb:123"`|
|
133
|
+
|`before_call`|`"#{YourClass.name}(.|#)#{method_name}"`|`"YourClass.some_class_method"`|
|
134
|
+
|`before_return`|`"#{YourClass.name}(.|#)#{method_name}"`|`"YourClass#some_instance_method"`|
|
133
135
|
|
134
|
-
As you can see, the
|
136
|
+
As you can see, the `before_call` and `before_return` keys are formatted the same, and can identify any instance (`#`) or class (`.`) method.
|
135
137
|
|
136
138
|
What the example scenario above does is inject a glitch before the `step_3` method is called, here:
|
137
139
|
|
@@ -144,10 +146,10 @@ def perform
|
|
144
146
|
end
|
145
147
|
```
|
146
148
|
|
147
|
-
If we wanted to inject a glitch right before the `step_3` method finishes, we could define the glitch as a
|
149
|
+
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
150
|
|
149
151
|
```ruby
|
150
|
-
run_scenario(Job.new, glitch:
|
152
|
+
run_scenario(Job.new, glitch: ChaoticJob::Glitch.before_return("Job#step_3"))
|
151
153
|
```
|
152
154
|
|
153
155
|
and it would inject the transient error right here:
|
@@ -159,19 +161,21 @@ def step_3
|
|
159
161
|
end
|
160
162
|
```
|
161
163
|
|
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
|
164
|
+
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
165
|
|
164
166
|
```ruby
|
165
|
-
run_scenario(Job.new, glitch:
|
167
|
+
run_scenario(Job.new, glitch: ChaoticJob::Glitch.before_line("#{__FILE__}:6"))
|
166
168
|
```
|
167
169
|
|
168
|
-
If you want to simulate multiple glitches affecting a job run, you
|
170
|
+
If you want to simulate multiple glitches affecting a job run, you simply define additional failure points using the fluid interface of the `ChaoticJob::Glitch` class:
|
169
171
|
|
170
172
|
```ruby
|
171
|
-
run_scenario(
|
172
|
-
|
173
|
-
|
174
|
-
|
173
|
+
run_scenario(
|
174
|
+
Job.new,
|
175
|
+
glitch: ChaoticJob::Glitch
|
176
|
+
.before_call("Job#step_1")
|
177
|
+
.before_return("Job#step_3")
|
178
|
+
)
|
175
179
|
```
|
176
180
|
|
177
181
|
Scenario testing is useful to test the behavior of a job under a specific set of conditions. But, if you want to test the behavior of a job under a variety of conditions, you can use the `run_simulation` method. Instead of running a single scenario, a simulation will run the full set of possible error scenarios for your job.
|
@@ -217,7 +221,24 @@ More specifically, it will create a scenario injecting a glitch before every lin
|
|
217
221
|
|
218
222
|
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.
|
219
223
|
|
220
|
-
|
224
|
+
If you want to have the simulation run against a larger collection of scenarios, you can capture a custom callstack using the `ChaoticJob::Tracer` class and pass it to the `run_simulation` method as the `callstack` parameter. A `Tracer` is initialized with a block that determines which `TracePoint` events to collect. You then call `capture` with a block that defines the code to be traced. The default `Simulation` tracer collects all events for the passed job and then traces the job execution, essentially like this:
|
225
|
+
|
226
|
+
```ruby
|
227
|
+
job_file_path = YourJob.instance_method(:perform).source_location&.first
|
228
|
+
tracer = Tracer.new { |tp| tp.path == job_file_path || tp.defined_class == YourJob }
|
229
|
+
tracer.capture { YourJob.perform_now }
|
230
|
+
```
|
231
|
+
|
232
|
+
To capture, for example, a custom callstack that includes all events within your application, you can use the `ChaoticJob::Tracer` class as follows:
|
233
|
+
|
234
|
+
```ruby
|
235
|
+
tracer = ChaoticJob::Tracer.new { |tp| tp.path.start_with?(Rails.root.to_s) }
|
236
|
+
tracer.capture { YourJob.perform_now }
|
237
|
+
```
|
238
|
+
|
239
|
+
If you passed this callstack to your simulation, it would test what happens to your job whenever a transient glitch is injected anywhere in your application code called as a part of executing the job under test.
|
240
|
+
|
241
|
+
Remember, 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.
|
221
242
|
|
222
243
|
## Development
|
223
244
|
|
data/lib/chaotic_job/glitch.rb
CHANGED
@@ -1,12 +1,24 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
|
-
# Glitch.
|
4
|
-
# Glitch.
|
5
|
-
# Glitch.
|
6
|
-
# Glitch.
|
3
|
+
# Glitch.before_line("job_crucible.rb:10") { do_anything }
|
4
|
+
# Glitch.before_call("Model#method", String, name: "Joel") { do_anything }
|
5
|
+
# Glitch.before_return("Model#method", String, name: "Joel") { do_anything }
|
6
|
+
# Glitch.inject! { execute code to glitch }
|
7
7
|
|
8
8
|
module ChaoticJob
|
9
9
|
class Glitch
|
10
|
+
def self.before_line(key, &block)
|
11
|
+
new.before_line(key, &block)
|
12
|
+
end
|
13
|
+
|
14
|
+
def self.before_call(key, ...)
|
15
|
+
new.before_call(key, ...)
|
16
|
+
end
|
17
|
+
|
18
|
+
def self.before_return(key, return_type = nil, &block)
|
19
|
+
new.before_return(key, return_type, &block)
|
20
|
+
end
|
21
|
+
|
10
22
|
def initialize
|
11
23
|
@breakpoints = {}
|
12
24
|
end
|
@@ -26,6 +38,15 @@ module ChaoticJob
|
|
26
38
|
self
|
27
39
|
end
|
28
40
|
|
41
|
+
def set_action(force: false, &block)
|
42
|
+
@breakpoints.each do |_key, handlers|
|
43
|
+
handlers.each do |_event, handler|
|
44
|
+
handler[:block] = block if handler[:block].nil? || force
|
45
|
+
end
|
46
|
+
end
|
47
|
+
self
|
48
|
+
end
|
49
|
+
|
29
50
|
def inject!(&block)
|
30
51
|
breakpoints = @breakpoints
|
31
52
|
|
@@ -45,17 +66,11 @@ module ChaoticJob
|
|
45
66
|
end
|
46
67
|
|
47
68
|
def all_executed?
|
48
|
-
@breakpoints.all? do |
|
49
|
-
handlers.all? { |
|
69
|
+
@breakpoints.all? do |_key, handlers|
|
70
|
+
handlers.all? { |_event, handler| handler[:executed] }
|
50
71
|
end
|
51
72
|
end
|
52
73
|
|
53
|
-
# def inspect
|
54
|
-
# @breakpoints.flat_map do |location, configs|
|
55
|
-
# configs.keys.map { |position| "#{position}-#{location}" }
|
56
|
-
# end.join("|>")
|
57
|
-
# end
|
58
|
-
|
59
74
|
private
|
60
75
|
|
61
76
|
def set_breakpoint(key, event, *args, retval: nil, **kwargs, &block)
|
data/lib/chaotic_job/scenario.rb
CHANGED
@@ -7,20 +7,20 @@ module ChaoticJob
|
|
7
7
|
class Scenario
|
8
8
|
attr_reader :events
|
9
9
|
|
10
|
-
def initialize(job,
|
10
|
+
def initialize(job, glitch:, raise: RetryableError, capture: /active_job/)
|
11
11
|
@job = job
|
12
|
-
@
|
12
|
+
@glitch = (Glitch === glitch) ? glitch : (raise Error.new("glitch: must be a Glitch instance, but got #{glitch.inspect}"))
|
13
13
|
@raise = binding.local_variable_get(:raise)
|
14
14
|
@capture = capture
|
15
|
-
@glitch = nil
|
16
15
|
@events = []
|
17
16
|
end
|
18
17
|
|
19
18
|
def run(&block)
|
20
19
|
@job.class.retry_on RetryableError, attempts: 10, wait: 1, jitter: 0
|
20
|
+
@glitch.set_action { raise @raise }
|
21
21
|
|
22
22
|
ActiveSupport::Notifications.subscribed(->(event) { @events << event.dup }, @capture) do
|
23
|
-
glitch.inject! do
|
23
|
+
@glitch.inject! do
|
24
24
|
@job.enqueue
|
25
25
|
if block
|
26
26
|
block.call
|
@@ -30,25 +30,11 @@ module ChaoticJob
|
|
30
30
|
end
|
31
31
|
end
|
32
32
|
|
33
|
-
# TODO: assert that all
|
34
|
-
end
|
35
|
-
|
36
|
-
def to_s
|
37
|
-
@glitches.map { |position, location| "#{position}-#{location}" }.join("|>")
|
33
|
+
# TODO: assert that all glitch ran
|
38
34
|
end
|
39
35
|
|
40
36
|
def all_glitched?
|
41
37
|
@glitch.all_executed?
|
42
38
|
end
|
43
|
-
|
44
|
-
private
|
45
|
-
|
46
|
-
def glitch
|
47
|
-
@glitch ||= Glitch.new.tap do |glitch|
|
48
|
-
@glitches.each do |kind, location, _description|
|
49
|
-
glitch.public_send(kind, location) { raise @raise }
|
50
|
-
end
|
51
|
-
end
|
52
|
-
end
|
53
39
|
end
|
54
40
|
end
|
@@ -6,13 +6,16 @@
|
|
6
6
|
# Simulation.new(job).scenarios
|
7
7
|
module ChaoticJob
|
8
8
|
class Simulation
|
9
|
-
def initialize(job, depth: 1, variations: 100, test: nil, seed: nil)
|
9
|
+
def initialize(job, callstack: nil, depth: 1, variations: 100, test: nil, seed: nil)
|
10
10
|
@template = job
|
11
|
+
@callstack = callstack || capture_callstack
|
11
12
|
@depth = depth
|
12
13
|
@variations = variations
|
13
14
|
@test = test
|
14
15
|
@seed = seed || Random.new_seed
|
15
16
|
@random = Random.new(@seed)
|
17
|
+
|
18
|
+
raise Error.new("callstack must be a generated via the ChaoticJob::Tracer") unless @callstack.is_a?(Stack)
|
16
19
|
end
|
17
20
|
|
18
21
|
def run(&callback)
|
@@ -27,9 +30,8 @@ module ChaoticJob
|
|
27
30
|
end
|
28
31
|
|
29
32
|
def permutations
|
30
|
-
|
31
|
-
|
32
|
-
[:before_line, "#{path}:#{lineno}"]
|
33
|
+
error_locations = @callstack.map do |event, key|
|
34
|
+
["before_#{event}", key]
|
33
35
|
end
|
34
36
|
error_locations.permutation(@depth)
|
35
37
|
end
|
@@ -43,7 +45,8 @@ module ChaoticJob
|
|
43
45
|
def scenarios
|
44
46
|
variants.map do |glitches|
|
45
47
|
job = clone_job_template
|
46
|
-
|
48
|
+
glitch = Glitch.new.tap { |g| glitches.each { |event, key| g.public_send(event, key) } }
|
49
|
+
scenario = Scenario.new(job, glitch: glitch)
|
47
50
|
job.job_id = scenario.to_s
|
48
51
|
scenario
|
49
52
|
end
|
@@ -52,23 +55,13 @@ module ChaoticJob
|
|
52
55
|
private
|
53
56
|
|
54
57
|
def capture_callstack
|
55
|
-
return @callstack if defined?(@callstack)
|
56
|
-
|
57
|
-
@callstack = Set.new
|
58
58
|
job_class = @template.class
|
59
59
|
job_file_path = job_class.instance_method(:perform).source_location&.first
|
60
|
+
tracer = Tracer.new { |tp| tp.path == job_file_path || tp.defined_class == job_class }
|
61
|
+
callstack = tracer.capture { @template.dup.perform_now }
|
60
62
|
|
61
|
-
trace = TracePoint.new(:line) do |tp|
|
62
|
-
next if tp.defined_class == self.class
|
63
|
-
next unless tp.path == job_file_path ||
|
64
|
-
tp.defined_class == job_class
|
65
|
-
|
66
|
-
@callstack << [tp.path, tp.lineno]
|
67
|
-
end
|
68
|
-
|
69
|
-
trace.enable { @template.dup.perform_now }
|
70
63
|
@template.class.queue_adapter.enqueued_jobs = []
|
71
|
-
|
64
|
+
callstack
|
72
65
|
end
|
73
66
|
|
74
67
|
def run_scenario(scenario, &callback)
|
@@ -0,0 +1,50 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
# Tracer.new { |tp| tp.path.start_with? "foo" }
|
4
|
+
# Tracer.new.capture { code_execution_to_trace_callstack }
|
5
|
+
|
6
|
+
module ChaoticJob
|
7
|
+
class Tracer
|
8
|
+
def initialize(&constraint)
|
9
|
+
@constraint = constraint
|
10
|
+
@callstack = Stack.new
|
11
|
+
end
|
12
|
+
|
13
|
+
def capture(&block)
|
14
|
+
trace = TracePoint.new(:line, :call, :return) do |tp|
|
15
|
+
# :nocov: SimpleCov cannot track code executed _within_ a TracePoint
|
16
|
+
next if tp.defined_class == self.class
|
17
|
+
next unless @constraint.call(tp)
|
18
|
+
|
19
|
+
case tp.event
|
20
|
+
when :line
|
21
|
+
key = line_key(tp)
|
22
|
+
when :call, :return
|
23
|
+
key = call_key(tp)
|
24
|
+
end
|
25
|
+
|
26
|
+
@callstack << [tp.event, key]
|
27
|
+
# :nocov:
|
28
|
+
end
|
29
|
+
|
30
|
+
trace.enable(&block)
|
31
|
+
@callstack
|
32
|
+
end
|
33
|
+
|
34
|
+
private
|
35
|
+
|
36
|
+
# :nocov: SimpleCov cannot track code executed _within_ a TracePoint
|
37
|
+
def line_key(event)
|
38
|
+
"#{event.path}:#{event.lineno}"
|
39
|
+
end
|
40
|
+
|
41
|
+
def call_key(event)
|
42
|
+
if Module === event.self
|
43
|
+
"#{event.self}.#{event.method_id}"
|
44
|
+
else
|
45
|
+
"#{event.defined_class}##{event.method_id}"
|
46
|
+
end
|
47
|
+
end
|
48
|
+
# :nocov:
|
49
|
+
end
|
50
|
+
end
|
data/lib/chaotic_job/version.rb
CHANGED
data/lib/chaotic_job.rb
CHANGED
@@ -3,13 +3,16 @@
|
|
3
3
|
require_relative "chaotic_job/version"
|
4
4
|
require_relative "chaotic_job/journal"
|
5
5
|
require_relative "chaotic_job/performer"
|
6
|
+
require_relative "chaotic_job/tracer"
|
6
7
|
require_relative "chaotic_job/glitch"
|
7
8
|
require_relative "chaotic_job/scenario"
|
8
9
|
require_relative "chaotic_job/simulation"
|
10
|
+
require "set"
|
9
11
|
|
10
12
|
module ChaoticJob
|
11
|
-
|
12
|
-
|
13
|
+
Error = Class.new(StandardError)
|
14
|
+
RetryableError = Class.new(Error)
|
15
|
+
Stack = Set
|
13
16
|
|
14
17
|
def self.log_to_journal!(item = nil, scope: nil)
|
15
18
|
if item && scope
|
@@ -63,19 +66,23 @@ module ChaoticJob
|
|
63
66
|
Performer.perform_all_after(time)
|
64
67
|
end
|
65
68
|
|
66
|
-
def run_simulation(job, depth: nil, variations: nil, &block)
|
69
|
+
def run_simulation(job, depth: nil, variations: nil, callstack: nil, &block)
|
67
70
|
seed = defined?(RSpec) ? RSpec.configuration.seed : Minitest.seed
|
68
71
|
kwargs = {test: self, seed: seed}
|
69
72
|
kwargs[:depth] = depth if depth
|
70
73
|
kwargs[:variations] = variations if variations
|
74
|
+
kwargs[:callstack] = callstack if callstack
|
71
75
|
self.simulation_scenario = nil
|
72
76
|
Simulation.new(job, **kwargs).run(&block)
|
73
77
|
end
|
74
78
|
|
75
|
-
def run_scenario(job, glitch
|
76
|
-
kwargs = {
|
79
|
+
def run_scenario(job, glitch:, raise: nil, capture: nil, &block)
|
80
|
+
kwargs = {}
|
81
|
+
|
82
|
+
kwargs[:glitch] = glitch
|
77
83
|
kwargs[:raise] = binding.local_variable_get(:raise) if binding.local_variable_get(:raise)
|
78
84
|
kwargs[:capture] = capture if capture
|
85
|
+
|
79
86
|
if block
|
80
87
|
Scenario.new(job, **kwargs).run(&block)
|
81
88
|
else
|
@@ -83,6 +90,8 @@ module ChaoticJob
|
|
83
90
|
end
|
84
91
|
end
|
85
92
|
|
93
|
+
private
|
94
|
+
|
86
95
|
def assert(test, msg = nil)
|
87
96
|
return super unless @simulation_scenario
|
88
97
|
|
metadata
CHANGED
@@ -1,13 +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.6.0
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Stephen Margheim
|
8
8
|
bindir: exe
|
9
9
|
cert_chain: []
|
10
|
-
date: 2025-
|
10
|
+
date: 2025-06-08 00:00:00.000000000 Z
|
11
11
|
dependencies:
|
12
12
|
- !ruby/object:Gem::Dependency
|
13
13
|
name: activejob
|
@@ -41,6 +41,7 @@ files:
|
|
41
41
|
- lib/chaotic_job/performer.rb
|
42
42
|
- lib/chaotic_job/scenario.rb
|
43
43
|
- lib/chaotic_job/simulation.rb
|
44
|
+
- lib/chaotic_job/tracer.rb
|
44
45
|
- lib/chaotic_job/version.rb
|
45
46
|
- sig/chaotic_job.rbs
|
46
47
|
homepage: https://github.com/fractaledmind/chaotic_job
|