stepper_motor 0.1.5 → 0.1.7
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/lib/generators/stepper_motor_migration_002.rb.erb +6 -0
- data/lib/stepper_motor/journey.rb +27 -0
- data/lib/stepper_motor/railtie.rb +1 -1
- data/lib/stepper_motor/{reap_hung_journeys_job.rb → recover_stuck_journeys_job_v1.rb} +8 -5
- data/lib/stepper_motor/version.rb +1 -1
- data/lib/stepper_motor.rb +2 -1
- data/spec/spec_helper.rb +1 -1
- data/spec/stepper_motor/recover_stuck_journeys_job_spec.rb +89 -0
- data/spec/stepper_motor/recovery_spec.rb +105 -0
- metadata +6 -3
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: dfb7061ca792fe914c363ae586ba322df95147dc832c39852af36809f0013e73
|
4
|
+
data.tar.gz: cbc8583f27710cc4d800dd7d380b527c1382c021cfed1430c2eb10afa2c4b591
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 99e9981596319a6c18856536b0f9889b2eb5cf2c69b1306bb762a7ea9af3d95b746882c4af7f20f39c45068c81fe1134f157b773121edef016163e2831bec754
|
7
|
+
data.tar.gz: 54c83aa39398e84bf34640301f1258aef437f4e1dfd6a3e2b08b0f70ab8e76bc59b9aa8f032681185c8635528d4be9f67c247be468f98c718bcadd2524c6c3e3
|
@@ -52,6 +52,33 @@ module StepperMotor
|
|
52
52
|
where(hero: hero)
|
53
53
|
}
|
54
54
|
|
55
|
+
# Allows querying for Journeys which are stuck in "performing" state since a certain
|
56
|
+
# timestamp. These Journeys have likely been stuck because the worker that was performing
|
57
|
+
# the step has crashed or was forcibly restarted.
|
58
|
+
scope :stuck, ->(since) {
|
59
|
+
where(updated_at: ..since).performing
|
60
|
+
}
|
61
|
+
|
62
|
+
# Sets the behavior when a Journey gets stuck in "performing" state. The default us "reattempt" -
|
63
|
+
# it is going to try to restart the step the Journey got stuck on
|
64
|
+
class_attribute :when_stuck, default: :reattempt, instance_accessor: false, instance_reader: true
|
65
|
+
|
66
|
+
def recover!
|
67
|
+
case when_stuck
|
68
|
+
when :reattempt
|
69
|
+
with_lock do
|
70
|
+
return unless performing?
|
71
|
+
ready!
|
72
|
+
schedule!
|
73
|
+
end
|
74
|
+
else
|
75
|
+
with_lock do
|
76
|
+
return unless performing?
|
77
|
+
canceled!
|
78
|
+
end
|
79
|
+
end
|
80
|
+
end
|
81
|
+
|
55
82
|
after_create do |journey|
|
56
83
|
journey.step_definitions.any? ? journey.set_next_step_and_enqueue(journey.step_definitions.first) : journey.finished!
|
57
84
|
end
|
@@ -1,14 +1,17 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
|
+
require "active_job"
|
4
|
+
|
3
5
|
# The purpose of this job is to find journeys which have, for whatever reason, remained in the
|
4
6
|
# `performing` state for far longer than the journey is supposed to. At the moment it assumes
|
5
7
|
# any journey that stayed in `performing` for longer than 1 hour has hung. Add this job to your
|
6
8
|
# cron table and perform it regularly.
|
7
|
-
class StepperMotor::
|
8
|
-
def perform
|
9
|
-
StepperMotor::Journey.
|
10
|
-
|
11
|
-
|
9
|
+
class StepperMotor::RecoverStuckJourneysJobV1 < ActiveJob::Base
|
10
|
+
def perform(stuck_for: 2.days)
|
11
|
+
StepperMotor::Journey.stuck(stuck_for.ago).find_each do |journey|
|
12
|
+
journey.recover!
|
13
|
+
rescue => e
|
14
|
+
Rails&.error&.report(e)
|
12
15
|
end
|
13
16
|
end
|
14
17
|
end
|
data/lib/stepper_motor.rb
CHANGED
@@ -1,6 +1,7 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
3
|
require_relative "stepper_motor/version"
|
4
|
+
require_relative "stepper_motor/railtie" if defined?(Rails::Railtie)
|
4
5
|
require "active_support"
|
5
6
|
|
6
7
|
module StepperMotor
|
@@ -14,12 +15,12 @@ module StepperMotor
|
|
14
15
|
autoload :Step, File.dirname(__FILE__) + "/stepper_motor/step.rb"
|
15
16
|
autoload :PerformStepJob, File.dirname(__FILE__) + "/stepper_motor/perform_step_job.rb"
|
16
17
|
autoload :PerformStepJobV2, File.dirname(__FILE__) + "/stepper_motor/perform_step_job_v2.rb"
|
18
|
+
autoload :RecoverStuckJourneysJobV1, File.dirname(__FILE__) + "/stepper_motor/recover_stuck_journeys_job_v1.rb"
|
17
19
|
autoload :InstallGenerator, File.dirname(__FILE__) + "/generators/install_generator.rb"
|
18
20
|
autoload :ForwardScheduler, File.dirname(__FILE__) + "/stepper_motor/forward_scheduler.rb"
|
19
21
|
autoload :CyclicScheduler, File.dirname(__FILE__) + "/stepper_motor/cyclic_scheduler.rb"
|
20
22
|
autoload :TestHelper, File.dirname(__FILE__) + "/stepper_motor/test_helper.rb"
|
21
23
|
|
22
|
-
require_relative "stepper_motor/railtie" if defined?(Rails::Railtie)
|
23
24
|
|
24
25
|
mattr_accessor :scheduler, default: ForwardScheduler.new
|
25
26
|
end
|
data/spec/spec_helper.rb
CHANGED
@@ -56,7 +56,7 @@ end
|
|
56
56
|
module JourneyDefinitionHelper
|
57
57
|
def create_journey_subclass(&blk)
|
58
58
|
# https://stackoverflow.com/questions/4113479/dynamic-class-definition-with-a-class-name
|
59
|
-
random_component = Random.hex(
|
59
|
+
random_component = Random.hex(2)
|
60
60
|
random_name = "JourneySubclass#{random_component}"
|
61
61
|
klass = Class.new(StepperMotor::Journey, &blk)
|
62
62
|
Object.const_set(random_name, klass)
|
@@ -0,0 +1,89 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require_relative "../spec_helper"
|
4
|
+
|
5
|
+
RSpec.describe "RecoveryStuckJourneysJobV1" do
|
6
|
+
before do
|
7
|
+
StepperMotor::Journey.delete_all
|
8
|
+
end
|
9
|
+
|
10
|
+
it "handles recovery from a background job" do
|
11
|
+
stuck_journey_class1 = create_journey_subclass do
|
12
|
+
self.when_stuck = :cancel
|
13
|
+
|
14
|
+
step :first do
|
15
|
+
end
|
16
|
+
|
17
|
+
step :second, wait: 4.days do
|
18
|
+
Fiber.yield # Simulate the journey hanging
|
19
|
+
end
|
20
|
+
end
|
21
|
+
|
22
|
+
stuck_journey_class2 = create_journey_subclass do
|
23
|
+
self.when_stuck = :reattempt
|
24
|
+
|
25
|
+
step :first do
|
26
|
+
end
|
27
|
+
|
28
|
+
step :second, wait: 4.days do
|
29
|
+
Fiber.yield # Simulate the journey hanging
|
30
|
+
end
|
31
|
+
end
|
32
|
+
|
33
|
+
freeze_time
|
34
|
+
|
35
|
+
journey_to_cancel = stuck_journey_class1.create!
|
36
|
+
journey_to_reattempt = stuck_journey_class2.create!
|
37
|
+
|
38
|
+
journey_to_cancel.perform_next_step!
|
39
|
+
journey_to_reattempt.perform_next_step!
|
40
|
+
|
41
|
+
travel_to Time.now + 5.days
|
42
|
+
|
43
|
+
# Get both stuck
|
44
|
+
Fiber.new do
|
45
|
+
journey_to_cancel.perform_next_step!
|
46
|
+
end.resume
|
47
|
+
|
48
|
+
Fiber.new do
|
49
|
+
journey_to_reattempt.perform_next_step!
|
50
|
+
end.resume
|
51
|
+
|
52
|
+
expect(journey_to_cancel.reload).to be_performing
|
53
|
+
expect(journey_to_reattempt.reload).to be_performing
|
54
|
+
|
55
|
+
StepperMotor::RecoverStuckJourneysJobV1.perform_now(stuck_for: 2.days)
|
56
|
+
expect(journey_to_cancel.reload).to be_performing
|
57
|
+
expect(journey_to_reattempt.reload).to be_performing
|
58
|
+
|
59
|
+
travel_to Time.now + 2.days + 1.second
|
60
|
+
StepperMotor::RecoverStuckJourneysJobV1.perform_now(stuck_for: 2.days)
|
61
|
+
|
62
|
+
expect(journey_to_cancel.reload).to be_canceled
|
63
|
+
expect(journey_to_reattempt.reload).to be_ready
|
64
|
+
end
|
65
|
+
|
66
|
+
it "does not raise when the class of the journey is no longer present" do
|
67
|
+
stuck_journey_class1 = create_journey_subclass do
|
68
|
+
self.when_stuck = :cancel
|
69
|
+
|
70
|
+
step :first do
|
71
|
+
Fiber.yield # Simulate the journey hanging
|
72
|
+
end
|
73
|
+
end
|
74
|
+
|
75
|
+
freeze_time
|
76
|
+
|
77
|
+
journey_to_cancel = stuck_journey_class1.create!
|
78
|
+
Fiber.new do
|
79
|
+
journey_to_cancel.perform_next_step!
|
80
|
+
end.resume
|
81
|
+
expect(journey_to_cancel.reload).to be_performing
|
82
|
+
|
83
|
+
journey_to_cancel.class.update_all(type: "UnknownJourneySubclass")
|
84
|
+
|
85
|
+
expect {
|
86
|
+
StepperMotor::RecoverStuckJourneysJobV1.perform_now(stuck_for: 2.days)
|
87
|
+
}.not_to raise_error
|
88
|
+
end
|
89
|
+
end
|
@@ -0,0 +1,105 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require_relative "../spec_helper"
|
4
|
+
|
5
|
+
RSpec.describe "Recovery of stuck journeys" do
|
6
|
+
before do
|
7
|
+
StepperMotor::Journey.delete_all
|
8
|
+
end
|
9
|
+
|
10
|
+
it "recovers a journey by reattempting it" do
|
11
|
+
stuck_journey_class = create_journey_subclass do
|
12
|
+
step :first do
|
13
|
+
end
|
14
|
+
|
15
|
+
step :second, wait: 4.days do
|
16
|
+
Fiber.yield # Simulate the journey hanging
|
17
|
+
end
|
18
|
+
|
19
|
+
step :third, wait: 4.days do
|
20
|
+
end
|
21
|
+
end
|
22
|
+
|
23
|
+
freeze_time
|
24
|
+
journey = stuck_journey_class.create!
|
25
|
+
|
26
|
+
journey.perform_next_step!
|
27
|
+
expect(journey.next_step_name).to eq("second")
|
28
|
+
|
29
|
+
travel_to Time.now + 5.days
|
30
|
+
|
31
|
+
expect(stuck_journey_class.when_stuck).to eq(:reattempt)
|
32
|
+
expect(journey.when_stuck).to eq(:reattempt)
|
33
|
+
|
34
|
+
# Hang the journey in "performing"
|
35
|
+
stuck_fiber = Fiber.new do
|
36
|
+
journey.perform_next_step!
|
37
|
+
end
|
38
|
+
stuck_fiber.resume
|
39
|
+
|
40
|
+
expect(journey).to be_persisted
|
41
|
+
expect(journey).to be_performing
|
42
|
+
expect(journey.updated_at).to eq(Time.now)
|
43
|
+
|
44
|
+
expect(StepperMotor::Journey.stuck(1.days.ago)).not_to include(journey)
|
45
|
+
|
46
|
+
travel_to Time.now + 2.days
|
47
|
+
expect(StepperMotor::Journey.stuck(2.days.ago)).to include(journey)
|
48
|
+
|
49
|
+
perform_at_before_recovery = journey.next_step_to_be_performed_at
|
50
|
+
expect {
|
51
|
+
journey.reload.recover!
|
52
|
+
}.not_to raise_error
|
53
|
+
|
54
|
+
journey.reload
|
55
|
+
expect(journey.next_step_to_be_performed_at).to eq(perform_at_before_recovery)
|
56
|
+
expect(journey.next_step_name).to eq("second")
|
57
|
+
end
|
58
|
+
|
59
|
+
it "recovers a journey by canceling it" do
|
60
|
+
stuck_journey_class = create_journey_subclass do
|
61
|
+
self.when_stuck = :cancel
|
62
|
+
|
63
|
+
step :first do
|
64
|
+
end
|
65
|
+
|
66
|
+
step :second, wait: 4.days do
|
67
|
+
Fiber.yield # Simulate the journey hanging
|
68
|
+
end
|
69
|
+
|
70
|
+
step :third, wait: 4.days do
|
71
|
+
end
|
72
|
+
end
|
73
|
+
|
74
|
+
freeze_time
|
75
|
+
journey = stuck_journey_class.create!
|
76
|
+
|
77
|
+
journey.perform_next_step!
|
78
|
+
expect(journey.next_step_name).to eq("second")
|
79
|
+
|
80
|
+
travel_to Time.now + 5.days
|
81
|
+
|
82
|
+
expect(stuck_journey_class.when_stuck).to eq(:cancel)
|
83
|
+
expect(journey.when_stuck).to eq(:cancel)
|
84
|
+
|
85
|
+
# Hang the journey in "performing"
|
86
|
+
stuck_fiber = Fiber.new do
|
87
|
+
journey.perform_next_step!
|
88
|
+
end
|
89
|
+
stuck_fiber.resume
|
90
|
+
|
91
|
+
expect(journey).to be_persisted
|
92
|
+
expect(journey).to be_performing
|
93
|
+
expect(journey.updated_at).to eq(Time.now)
|
94
|
+
|
95
|
+
travel_to Time.now + 2.days
|
96
|
+
expect(StepperMotor::Journey.stuck(2.days.ago)).to include(journey)
|
97
|
+
|
98
|
+
expect {
|
99
|
+
journey.reload.recover!
|
100
|
+
}.not_to raise_error
|
101
|
+
|
102
|
+
journey.reload
|
103
|
+
expect(journey).to be_canceled
|
104
|
+
end
|
105
|
+
end
|
metadata
CHANGED
@@ -1,14 +1,14 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: stepper_motor
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.1.
|
4
|
+
version: 0.1.7
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Julik Tarkhanov
|
8
8
|
autorequire:
|
9
9
|
bindir: exe
|
10
10
|
cert_chain: []
|
11
|
-
date: 2025-
|
11
|
+
date: 2025-05-19 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: activerecord
|
@@ -197,6 +197,7 @@ files:
|
|
197
197
|
- bin/setup
|
198
198
|
- lib/generators/install_generator.rb
|
199
199
|
- lib/generators/stepper_motor_migration_001.rb.erb
|
200
|
+
- lib/generators/stepper_motor_migration_002.rb.erb
|
200
201
|
- lib/stepper_motor.rb
|
201
202
|
- lib/stepper_motor/cyclic_scheduler.rb
|
202
203
|
- lib/stepper_motor/forward_scheduler.rb
|
@@ -204,7 +205,7 @@ files:
|
|
204
205
|
- lib/stepper_motor/perform_step_job.rb
|
205
206
|
- lib/stepper_motor/perform_step_job_v2.rb
|
206
207
|
- lib/stepper_motor/railtie.rb
|
207
|
-
- lib/stepper_motor/
|
208
|
+
- lib/stepper_motor/recover_stuck_journeys_job_v1.rb
|
208
209
|
- lib/stepper_motor/step.rb
|
209
210
|
- lib/stepper_motor/test_helper.rb
|
210
211
|
- lib/stepper_motor/version.rb
|
@@ -215,6 +216,8 @@ files:
|
|
215
216
|
- spec/stepper_motor/forward_scheduler_spec.rb
|
216
217
|
- spec/stepper_motor/generator_spec.rb
|
217
218
|
- spec/stepper_motor/journey_spec.rb
|
219
|
+
- spec/stepper_motor/recover_stuck_journeys_job_spec.rb
|
220
|
+
- spec/stepper_motor/recovery_spec.rb
|
218
221
|
- spec/stepper_motor/test_helper_spec.rb
|
219
222
|
- spec/stepper_motor_spec.rb
|
220
223
|
homepage: https://steppermotor.dev
|