stepper_motor 0.1.6 → 0.1.8
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/.github/dependabot.yml +12 -0
- data/.github/workflows/ci.yml +51 -0
- data/CHANGELOG.md +77 -2
- data/Gemfile +11 -0
- data/README.md +13 -374
- data/Rakefile +21 -3
- data/bin/test +5 -0
- data/lib/generators/install_generator.rb +6 -1
- data/lib/generators/stepper_motor_migration_003.rb.erb +6 -0
- data/lib/generators/stepper_motor_migration_004.rb.erb +26 -0
- data/lib/stepper_motor/forward_scheduler.rb +8 -4
- data/lib/stepper_motor/journey/flow_control.rb +58 -0
- data/lib/stepper_motor/journey/recovery.rb +34 -0
- data/lib/stepper_motor/journey.rb +85 -84
- data/lib/stepper_motor/perform_step_job_v2.rb +2 -2
- data/lib/stepper_motor/railtie.rb +1 -1
- data/lib/stepper_motor/recover_stuck_journeys_job_v1.rb +3 -1
- data/lib/stepper_motor/step.rb +70 -5
- data/lib/stepper_motor/version.rb +1 -1
- data/lib/stepper_motor.rb +1 -2
- data/lib/tasks/stepper_motor_tasks.rake +8 -0
- data/manual/MANUAL.md +538 -0
- data/rbi/stepper_motor.rbi +459 -0
- data/sig/stepper_motor.rbs +406 -3
- data/stepper_motor.gemspec +49 -0
- data/test/dummy/Rakefile +8 -0
- data/test/dummy/app/assets/stylesheets/application.css +1 -0
- data/test/dummy/app/controllers/application_controller.rb +6 -0
- data/test/dummy/app/helpers/application_helper.rb +4 -0
- data/test/dummy/app/jobs/application_job.rb +9 -0
- data/test/dummy/app/mailers/application_mailer.rb +6 -0
- data/test/dummy/app/models/application_record.rb +5 -0
- data/test/dummy/app/views/layouts/application.html.erb +27 -0
- data/test/dummy/app/views/layouts/mailer.html.erb +13 -0
- data/test/dummy/app/views/layouts/mailer.text.erb +1 -0
- data/test/dummy/app/views/pwa/manifest.json.erb +22 -0
- data/test/dummy/app/views/pwa/service-worker.js +26 -0
- data/test/dummy/bin/dev +2 -0
- data/test/dummy/bin/rails +4 -0
- data/test/dummy/bin/rake +4 -0
- data/test/dummy/bin/setup +34 -0
- data/test/dummy/config/application.rb +28 -0
- data/test/dummy/config/boot.rb +7 -0
- data/test/dummy/config/cable.yml +10 -0
- data/test/dummy/config/database.yml +32 -0
- data/test/dummy/config/environment.rb +7 -0
- data/test/dummy/config/environments/development.rb +71 -0
- data/test/dummy/config/environments/production.rb +91 -0
- data/test/dummy/config/environments/test.rb +55 -0
- data/test/dummy/config/initializers/content_security_policy.rb +27 -0
- data/test/dummy/config/initializers/filter_parameter_logging.rb +10 -0
- data/test/dummy/config/initializers/inflections.rb +18 -0
- data/test/dummy/config/initializers/stepper_motor.rb +3 -0
- data/test/dummy/config/locales/en.yml +31 -0
- data/test/dummy/config/puma.rb +40 -0
- data/test/dummy/config/routes.rb +16 -0
- data/test/dummy/config/storage.yml +34 -0
- data/test/dummy/config.ru +8 -0
- data/test/dummy/db/migrate/20250520094921_stepper_motor_migration_001.rb +38 -0
- data/test/dummy/db/migrate/20250520094922_stepper_motor_migration_002.rb +8 -0
- data/test/dummy/db/migrate/20250522212312_stepper_motor_migration_003.rb +7 -0
- data/test/dummy/db/migrate/20250525110812_stepper_motor_migration_004.rb +28 -0
- data/test/dummy/db/schema.rb +37 -0
- data/test/dummy/public/400.html +114 -0
- data/test/dummy/public/404.html +114 -0
- data/test/dummy/public/406-unsupported-browser.html +114 -0
- data/test/dummy/public/422.html +114 -0
- data/test/dummy/public/500.html +114 -0
- data/test/dummy/public/icon.png +0 -0
- data/test/dummy/public/icon.svg +3 -0
- data/test/side_effects_helper.rb +67 -0
- data/test/stepper_motor/cyclic_scheduler_test.rb +77 -0
- data/{spec/stepper_motor/forward_scheduler_spec.rb → test/stepper_motor/forward_scheduler_test.rb} +9 -10
- data/test/stepper_motor/journey/exception_handling_test.rb +89 -0
- data/test/stepper_motor/journey/flow_control_test.rb +78 -0
- data/test/stepper_motor/journey/idempotency_test.rb +65 -0
- data/test/stepper_motor/journey/step_definition_test.rb +187 -0
- data/test/stepper_motor/journey/uniqueness_test.rb +48 -0
- data/test/stepper_motor/journey_test.rb +352 -0
- data/{spec/stepper_motor/recover_stuck_journeys_job_spec.rb → test/stepper_motor/recover_stuck_journeys_job_test.rb} +14 -14
- data/{spec/stepper_motor/recovery_spec.rb → test/stepper_motor/recovery_test.rb} +27 -27
- data/test/stepper_motor/test_helper_test.rb +44 -0
- data/test/stepper_motor_test.rb +9 -0
- data/test/test_helper.rb +46 -0
- metadata +120 -24
- data/.rspec +0 -3
- data/.ruby-version +0 -1
- data/.standard.yml +0 -4
- data/.yardopts +0 -1
- data/spec/helpers/side_effects.rb +0 -85
- data/spec/spec_helper.rb +0 -90
- data/spec/stepper_motor/cyclic_scheduler_spec.rb +0 -68
- data/spec/stepper_motor/generator_spec.rb +0 -16
- data/spec/stepper_motor/journey_spec.rb +0 -401
- data/spec/stepper_motor/test_helper_spec.rb +0 -48
- data/spec/stepper_motor_spec.rb +0 -7
@@ -0,0 +1,459 @@
|
|
1
|
+
# typed: strong
|
2
|
+
# StepperMotor is a module for building multi-step flows where steps are sequential and only
|
3
|
+
# ever progress forward. The building block of StepperMotor is StepperMotor::Journey
|
4
|
+
module StepperMotor
|
5
|
+
VERSION = T.let("0.1.8", T.untyped)
|
6
|
+
|
7
|
+
class Error < StandardError
|
8
|
+
end
|
9
|
+
|
10
|
+
class JourneyNotPersisted < StepperMotor::Error
|
11
|
+
end
|
12
|
+
|
13
|
+
class StepConfigurationError < ArgumentError
|
14
|
+
end
|
15
|
+
|
16
|
+
# Describes a step in a journey. These objects get stored inside the `step_definitions`
|
17
|
+
# array of the Journey subclass. When the step gets performed, the block passed to the
|
18
|
+
# constructor will be instance_exec'd with the Journey model being the context
|
19
|
+
class Step
|
20
|
+
# sord omit - no YARD type given for "seq:", using untyped
|
21
|
+
# sord warn - ActiveSupport::Duration wasn't able to be resolved to a constant in this project
|
22
|
+
# Creates a new step definition
|
23
|
+
#
|
24
|
+
# _@param_ `name` — the name of the Step
|
25
|
+
#
|
26
|
+
# _@param_ `wait` — the amount of time to wait before entering the step
|
27
|
+
#
|
28
|
+
# _@param_ `on_exception` — the action to take if an exception occurs when performing the step. The possible values are: * `:cancel!` - cancels the Journey and re-raises the exception. The Journey will be persisted before re-raising. * `:reattempt!` - reattempts the Journey and re-raises the exception. The Journey will be persisted before re-raising.
|
29
|
+
sig do
|
30
|
+
params(
|
31
|
+
name: T.any(String, Symbol),
|
32
|
+
seq: T.untyped,
|
33
|
+
on_exception: Symbol,
|
34
|
+
wait: T.any(Numeric, ActiveSupport::Duration),
|
35
|
+
step_block: T.untyped
|
36
|
+
).void
|
37
|
+
end
|
38
|
+
def initialize(name:, seq:, on_exception:, wait: 0, &step_block); end
|
39
|
+
|
40
|
+
# Performs the step on the passed Journey, wrapping the step with the required context.
|
41
|
+
#
|
42
|
+
# _@param_ `journey` — the journey to perform the step in. If a `step_block` is passed in, it is going to be executed in the context of the journey using `instance_exec`. If only the name of the step has been provided, an accordingly named public method on the journey will be called
|
43
|
+
#
|
44
|
+
# _@return_ — void
|
45
|
+
sig { params(journey: StepperMotor::Journey).returns(T.untyped) }
|
46
|
+
def perform_in_context_of(journey); end
|
47
|
+
|
48
|
+
# _@return_ — the name of the step or method to call on the Journey
|
49
|
+
sig { returns(String) }
|
50
|
+
attr_reader :name
|
51
|
+
|
52
|
+
# sord warn - ActiveSupport::Duration wasn't able to be resolved to a constant in this project
|
53
|
+
# _@return_ — how long to wait before performing the step
|
54
|
+
sig { returns(T.any(Numeric, ActiveSupport::Duration)) }
|
55
|
+
attr_reader :wait
|
56
|
+
|
57
|
+
# sord omit - no YARD type given for :seq, using untyped
|
58
|
+
sig { returns(T.untyped) }
|
59
|
+
attr_reader :seq
|
60
|
+
|
61
|
+
class MissingDefinition < NoMethodError
|
62
|
+
end
|
63
|
+
end
|
64
|
+
|
65
|
+
# A Journey is the main building block of StepperMotor. You create a journey to guide a particular model
|
66
|
+
# ("hero") through a sequence of steps. Any of your model can be the hero and have multiple Journeys. To create
|
67
|
+
# your own Journey, subclass the `StepperMotor::Journey` class and define your steps. For example, a drip mail
|
68
|
+
# campaign can look like this:
|
69
|
+
#
|
70
|
+
#
|
71
|
+
# class ResubscribeCampaign < StepperMotor::Journey
|
72
|
+
# step do
|
73
|
+
# ReinviteMailer.with(recipient: hero).deliver_later
|
74
|
+
# end
|
75
|
+
#
|
76
|
+
# step wait: 3.days do
|
77
|
+
# cancel! if hero.active?
|
78
|
+
# ReinviteMailer.with(recipient: hero).deliver_later
|
79
|
+
# end
|
80
|
+
#
|
81
|
+
# step wait: 3.days do
|
82
|
+
# cancel! if hero.active?
|
83
|
+
# ReinviteMailer.with(recipient: hero).deliver_later
|
84
|
+
# end
|
85
|
+
#
|
86
|
+
# step wait: 3.days do
|
87
|
+
# cancel! if hero.active?
|
88
|
+
# hero.close_account!
|
89
|
+
# end
|
90
|
+
# end
|
91
|
+
#
|
92
|
+
# Creating a record for the Journey (just using `create!`) will instantly send your hero on their way:
|
93
|
+
#
|
94
|
+
# ResubscribeCampaign.create!(hero: current_account)
|
95
|
+
#
|
96
|
+
# To stop the journey forcibly, delete it from your database - or call `cancel!` within any of the steps.
|
97
|
+
class Journey < ActiveRecord::Base
|
98
|
+
include StepperMotor::Journey::FlowControl
|
99
|
+
include StepperMotor::Journey::Recovery
|
100
|
+
STATES = T.let(%w[ready paused performing canceled finished], T.untyped)
|
101
|
+
|
102
|
+
# sord omit - no YARD return type given, using untyped
|
103
|
+
# Alias for the class attribute, for brevity
|
104
|
+
#
|
105
|
+
# _@see_ `Journey.step_definitions`
|
106
|
+
sig { returns(T.untyped) }
|
107
|
+
def step_definitions; end
|
108
|
+
|
109
|
+
# sord duck - #to_f looks like a duck type, replacing with untyped
|
110
|
+
# sord warn - ActiveSupport::Duration wasn't able to be resolved to a constant in this project
|
111
|
+
# sord duck - #to_f looks like a duck type, replacing with untyped
|
112
|
+
# sord warn - ActiveSupport::Duration wasn't able to be resolved to a constant in this project
|
113
|
+
# Defines a step in the journey.
|
114
|
+
# Steps are stacked top to bottom and get performed in sequence.
|
115
|
+
#
|
116
|
+
# _@param_ `name` — the name of the step. If none is provided, a name will be automatically generated based on the position of the step in the list of `step_definitions`. The name can also be used to call a method on the `Journey` instead of calling the provided block.
|
117
|
+
#
|
118
|
+
# _@param_ `wait` — the amount of time this step should wait before getting performed. When the journey gets scheduled, the triggering job is going to be delayed by this amount of time, and the `next_step_to_be_performed_at` attribute will be set to the current time plus the wait duration. Mutually exclusive with `after:`
|
119
|
+
#
|
120
|
+
# _@param_ `after` — the amount of time this step should wait before getting performed including all the previous waits. This allows you to set the wait time based on the time after the journey started, as opposed to when the previous step has completed. When the journey gets scheduled, the triggering job is going to be delayed by this amount of time _minus the `wait` values of the preceding steps, and the `next_step_to_be_performed_at` attribute will be set to the current time. The `after` value gets converted into the `wait` value and passed to the step definition. Mutually exclusive with `wait:`
|
121
|
+
#
|
122
|
+
# _@param_ `on_exception` — See {StepperMotor::Step#on_exception}
|
123
|
+
#
|
124
|
+
# _@param_ `additional_step_definition_options` — Any remaining options get passed to `StepperMotor::Step.new` as keyword arguments.
|
125
|
+
#
|
126
|
+
# _@return_ — the step definition that has been created
|
127
|
+
sig do
|
128
|
+
params(
|
129
|
+
name: T.nilable(String),
|
130
|
+
wait: T.nilable(T.any(Float, T.untyped, ActiveSupport::Duration)),
|
131
|
+
after: T.nilable(T.any(Float, T.untyped, ActiveSupport::Duration)),
|
132
|
+
on_exception: Symbol,
|
133
|
+
additional_step_definition_options: T.untyped,
|
134
|
+
blk: T.untyped
|
135
|
+
).returns(StepperMotor::Step)
|
136
|
+
end
|
137
|
+
def self.step(name = nil, wait: nil, after: nil, on_exception: :pause!, **additional_step_definition_options, &blk); end
|
138
|
+
|
139
|
+
# sord warn - "StepperMotor::Step?" does not appear to be a type
|
140
|
+
# Returns the `Step` object for a named step. This is used when performing a step, but can also
|
141
|
+
# be useful in other contexts.
|
142
|
+
#
|
143
|
+
# _@param_ `by_step_name` — the name of the step to find
|
144
|
+
sig { params(by_step_name: T.any(Symbol, String)).returns(SORD_ERROR_StepperMotorStep) }
|
145
|
+
def self.lookup_step_definition(by_step_name); end
|
146
|
+
|
147
|
+
# sord omit - no YARD type given for "by_step_name", using untyped
|
148
|
+
# sord omit - no YARD return type given, using untyped
|
149
|
+
# Alias for the class method, for brevity
|
150
|
+
#
|
151
|
+
# _@see_ `Journey.lookup_step_definition`
|
152
|
+
sig { params(by_step_name: T.untyped).returns(T.untyped) }
|
153
|
+
def lookup_step_definition(by_step_name); end
|
154
|
+
|
155
|
+
# Performs the next step in the journey. Will check whether any other process has performed the step already
|
156
|
+
# and whether the record is unchanged, and will then lock it and set the state to 'performimg'.
|
157
|
+
#
|
158
|
+
# After setting the state, it will determine the next step to perform, and perform it. Depending on the outcome of
|
159
|
+
# the step another `PerformStepJob` may get enqueued. If the journey ends here, the journey record will set its state
|
160
|
+
# to 'finished'.
|
161
|
+
#
|
162
|
+
# _@param_ `idempotency_key` — If provided, the step will only be performed if the idempotency key matches the current idempotency key. This ensures that the only the triggering job that was scheduled for this step can trigger the step and not any other.
|
163
|
+
sig { params(idempotency_key: T.nilable(String)).void }
|
164
|
+
def perform_next_step!(idempotency_key: nil); end
|
165
|
+
|
166
|
+
# sord warn - ActiveSupport::Duration wasn't able to be resolved to a constant in this project
|
167
|
+
sig { returns(ActiveSupport::Duration) }
|
168
|
+
def time_remaining_until_final_step; end
|
169
|
+
|
170
|
+
# sord omit - no YARD type given for "next_step_definition", using untyped
|
171
|
+
# sord omit - no YARD type given for "wait:", using untyped
|
172
|
+
# sord omit - no YARD return type given, using untyped
|
173
|
+
sig { params(next_step_definition: T.untyped, wait: T.untyped).returns(T.untyped) }
|
174
|
+
def set_next_step_and_enqueue(next_step_definition, wait: nil); end
|
175
|
+
|
176
|
+
# sord omit - no YARD return type given, using untyped
|
177
|
+
sig { returns(T.untyped) }
|
178
|
+
def logger; end
|
179
|
+
|
180
|
+
# sord omit - no YARD type given for "step_name", using untyped
|
181
|
+
# sord omit - no YARD return type given, using untyped
|
182
|
+
sig { params(step_name: T.untyped).returns(T.untyped) }
|
183
|
+
def after_locking_for_step(step_name); end
|
184
|
+
|
185
|
+
# sord omit - no YARD type given for "step_name", using untyped
|
186
|
+
# sord omit - no YARD type given for "exception", using untyped
|
187
|
+
# sord omit - no YARD return type given, using untyped
|
188
|
+
sig { params(step_name: T.untyped, exception: T.untyped).returns(T.untyped) }
|
189
|
+
def after_performing_step_with_exception(step_name, exception); end
|
190
|
+
|
191
|
+
# sord omit - no YARD type given for "step_name", using untyped
|
192
|
+
# sord omit - no YARD return type given, using untyped
|
193
|
+
sig { params(step_name: T.untyped).returns(T.untyped) }
|
194
|
+
def before_step_starts(step_name); end
|
195
|
+
|
196
|
+
# sord omit - no YARD type given for "step_name", using untyped
|
197
|
+
# sord omit - no YARD return type given, using untyped
|
198
|
+
sig { params(step_name: T.untyped).returns(T.untyped) }
|
199
|
+
def after_performing_step_without_exception(step_name); end
|
200
|
+
|
201
|
+
# sord omit - no YARD return type given, using untyped
|
202
|
+
sig { returns(T.untyped) }
|
203
|
+
def schedule!; end
|
204
|
+
|
205
|
+
# sord omit - no YARD return type given, using untyped
|
206
|
+
sig { returns(T.untyped) }
|
207
|
+
def recover!; end
|
208
|
+
|
209
|
+
# Is a convenient way to end a hero's journey. Imagine you enter a step where you are inviting a user
|
210
|
+
# to rejoin the platform, and are just about to send them an email - but they have already joined. You
|
211
|
+
# can therefore cancel their journey. Canceling bails you out of the `step`-defined block and sets the journey record to the `canceled` state.
|
212
|
+
#
|
213
|
+
# Calling `cancel!` within a step will abort the execution of the current step.
|
214
|
+
#
|
215
|
+
# _@return_ — void
|
216
|
+
sig { returns(T.untyped) }
|
217
|
+
def cancel!; end
|
218
|
+
|
219
|
+
# sord omit - no YARD type given for "wait:", using untyped
|
220
|
+
# Inside a step it is possible to ask StepperMotor to retry to start the step at a later point in time. Maybe now is an inconvenient moment
|
221
|
+
# (are you about to send a push notification at 3AM perhaps?). The `wait:` parameter specifies how long to defer reattempting the step for.
|
222
|
+
# Reattempting will resume the step from the beginning, so the step should be idempotent.
|
223
|
+
#
|
224
|
+
# `reattempt!` may only be called within a step.
|
225
|
+
#
|
226
|
+
# _@return_ — void
|
227
|
+
sig { params(wait: T.untyped).returns(T.untyped) }
|
228
|
+
def reattempt!(wait: nil); end
|
229
|
+
|
230
|
+
# Is used to pause a Journey at any point. The "paused" state is similar to the "ready" state, except that "perform_next_step!" on the
|
231
|
+
# journey will do nothing - even if it is scheduled to be performed. Pausing a Journey can be useful in the following situations:
|
232
|
+
#
|
233
|
+
# * The hero of the journey is in a compliance procedure, and their Journeys should not continue
|
234
|
+
# * The external resource a Journey will be calling is not available
|
235
|
+
# * There is a bug in the Journey implementation and you need some time to get it fixed without canceling or recreating existing Journeys
|
236
|
+
#
|
237
|
+
# Calling `pause!` within a step will abort the execution of the current step.
|
238
|
+
#
|
239
|
+
# _@return_ — void
|
240
|
+
sig { returns(T.untyped) }
|
241
|
+
def pause!; end
|
242
|
+
|
243
|
+
# Is used to resume a paused Journey. It places the Journey into the `ready` state and schedules the job to perform that step.
|
244
|
+
#
|
245
|
+
# Calling `resume!` is only permitted outside of a step
|
246
|
+
#
|
247
|
+
# _@return_ — void
|
248
|
+
sig { returns(T.untyped) }
|
249
|
+
def resume!; end
|
250
|
+
|
251
|
+
module Recovery
|
252
|
+
extend ActiveSupport::Concern
|
253
|
+
|
254
|
+
# sord omit - no YARD return type given, using untyped
|
255
|
+
sig { returns(T.untyped) }
|
256
|
+
def recover!; end
|
257
|
+
end
|
258
|
+
|
259
|
+
module FlowControl
|
260
|
+
# Is a convenient way to end a hero's journey. Imagine you enter a step where you are inviting a user
|
261
|
+
# to rejoin the platform, and are just about to send them an email - but they have already joined. You
|
262
|
+
# can therefore cancel their journey. Canceling bails you out of the `step`-defined block and sets the journey record to the `canceled` state.
|
263
|
+
#
|
264
|
+
# Calling `cancel!` within a step will abort the execution of the current step.
|
265
|
+
#
|
266
|
+
# _@return_ — void
|
267
|
+
sig { returns(T.untyped) }
|
268
|
+
def cancel!; end
|
269
|
+
|
270
|
+
# sord omit - no YARD type given for "wait:", using untyped
|
271
|
+
# Inside a step it is possible to ask StepperMotor to retry to start the step at a later point in time. Maybe now is an inconvenient moment
|
272
|
+
# (are you about to send a push notification at 3AM perhaps?). The `wait:` parameter specifies how long to defer reattempting the step for.
|
273
|
+
# Reattempting will resume the step from the beginning, so the step should be idempotent.
|
274
|
+
#
|
275
|
+
# `reattempt!` may only be called within a step.
|
276
|
+
#
|
277
|
+
# _@return_ — void
|
278
|
+
sig { params(wait: T.untyped).returns(T.untyped) }
|
279
|
+
def reattempt!(wait: nil); end
|
280
|
+
|
281
|
+
# Is used to pause a Journey at any point. The "paused" state is similar to the "ready" state, except that "perform_next_step!" on the
|
282
|
+
# journey will do nothing - even if it is scheduled to be performed. Pausing a Journey can be useful in the following situations:
|
283
|
+
#
|
284
|
+
# * The hero of the journey is in a compliance procedure, and their Journeys should not continue
|
285
|
+
# * The external resource a Journey will be calling is not available
|
286
|
+
# * There is a bug in the Journey implementation and you need some time to get it fixed without canceling or recreating existing Journeys
|
287
|
+
#
|
288
|
+
# Calling `pause!` within a step will abort the execution of the current step.
|
289
|
+
#
|
290
|
+
# _@return_ — void
|
291
|
+
sig { returns(T.untyped) }
|
292
|
+
def pause!; end
|
293
|
+
|
294
|
+
# Is used to resume a paused Journey. It places the Journey into the `ready` state and schedules the job to perform that step.
|
295
|
+
#
|
296
|
+
# Calling `resume!` is only permitted outside of a step
|
297
|
+
#
|
298
|
+
# _@return_ — void
|
299
|
+
sig { returns(T.untyped) }
|
300
|
+
def resume!; end
|
301
|
+
end
|
302
|
+
end
|
303
|
+
|
304
|
+
class Railtie < Rails::Railtie
|
305
|
+
end
|
306
|
+
|
307
|
+
module TestHelper
|
308
|
+
# Allows running a given Journey to completion, skipping across the waiting periods.
|
309
|
+
# This is useful to evaluate all side effects of a Journey. The helper will ensure
|
310
|
+
# that the number of steps performed is equal to the number of steps defined - this way
|
311
|
+
# it will not enter into an endless loop. If, after completing all the steps, the journey
|
312
|
+
# has neither canceled nor finished, an exception will be raised.
|
313
|
+
#
|
314
|
+
# _@param_ `journey` — the journey to speedrun
|
315
|
+
#
|
316
|
+
# _@return_ — void
|
317
|
+
sig { params(journey: StepperMotor::Journey).returns(T.untyped) }
|
318
|
+
def speedrun_journey(journey); end
|
319
|
+
|
320
|
+
# Performs the named step of the journey without waiting for the time to perform the step.
|
321
|
+
#
|
322
|
+
# _@param_ `journey` — the journey to speedrun
|
323
|
+
#
|
324
|
+
# _@param_ `step_name` — the name of the step to run
|
325
|
+
#
|
326
|
+
# _@return_ — void
|
327
|
+
sig { params(journey: StepperMotor::Journey, step_name: Symbol).returns(T.untyped) }
|
328
|
+
def immediately_perform_single_step(journey, step_name); end
|
329
|
+
end
|
330
|
+
|
331
|
+
# The generator is used to install StepperMotor. It adds an example Journey, a configing
|
332
|
+
# initializer and the migration that creates tables.
|
333
|
+
# Run it with `bin/rails g stepper_motor:install` in your console.
|
334
|
+
class InstallGenerator < Rails::Generators::Base
|
335
|
+
include ActiveRecord::Generators::Migration
|
336
|
+
UUID_MESSAGE = T.let(<<~MSG, T.untyped)
|
337
|
+
If set, uuid type will be used for hero_id. Use this
|
338
|
+
if most of your models use UUD as primary key"
|
339
|
+
MSG
|
340
|
+
|
341
|
+
# sord omit - no YARD return type given, using untyped
|
342
|
+
# Generates monolithic migration file that contains all database changes.
|
343
|
+
sig { returns(T.untyped) }
|
344
|
+
def create_migration_file; end
|
345
|
+
|
346
|
+
# sord omit - no YARD return type given, using untyped
|
347
|
+
sig { returns(T.untyped) }
|
348
|
+
def create_initializer; end
|
349
|
+
|
350
|
+
sig { returns(T::Boolean) }
|
351
|
+
def uuid_fk?; end
|
352
|
+
|
353
|
+
# sord omit - no YARD return type given, using untyped
|
354
|
+
sig { returns(T.untyped) }
|
355
|
+
def migration_version; end
|
356
|
+
end
|
357
|
+
|
358
|
+
# The cyclic scheduler is designed to be run regularly via a cron job. On every
|
359
|
+
# cycle, it is going to look for Journeys which are going to come up for step execution
|
360
|
+
# before the next cycle is supposed to run. Then it is going to enqueue jobs to perform
|
361
|
+
# steps on those journeys. Since the scheduler gets run at a discrete interval, but we
|
362
|
+
# still them to be processed on time, if we only picked up the journeys which have the
|
363
|
+
# step execution time set to now or earlier, we will always have delays. This is why
|
364
|
+
# this scheduler enqueues jobs for journeys whose time to run is between now and the
|
365
|
+
# next cycle.
|
366
|
+
#
|
367
|
+
# Once the job gets created, it then gets enqueued and gets picked up by the ActiveJob
|
368
|
+
# worker normally. If you are using SQS, which has a limit of 900 seconds for the `wait:`
|
369
|
+
# value, you need to run the scheduler at least (!) every 900 seconds, and preferably
|
370
|
+
# more frequently (for example, once every 5 minutes). This scheduler is also going to be
|
371
|
+
# more gentle with ActiveJob adapters that may get slower with large queue depths, such as
|
372
|
+
# good_job. This scheduler is a good fit if you are using an ActiveJob adapter which:
|
373
|
+
#
|
374
|
+
# * Does not allow easy introspection of jobs in the future (like Redis-based queues)
|
375
|
+
# * Limits the value of the `wait:` parameter
|
376
|
+
#
|
377
|
+
# The scheduler needs to be configured in your cron table.
|
378
|
+
class CyclicScheduler < StepperMotor::ForwardScheduler
|
379
|
+
# sord warn - ActiveSupport::Duration wasn't able to be resolved to a constant in this project
|
380
|
+
# Creates a new scheduler. The scheduler needs to know how frequently it is going to be running -
|
381
|
+
# you define that frequency when you configure your cron job that calls `run_scheduling_cycle`. Journeys which
|
382
|
+
# have to perform their steps between the runs of the cycles will generate jobs. The more frequent the scheduling
|
383
|
+
# cycle, the fewer jobs are going to be created per cycle.
|
384
|
+
#
|
385
|
+
# _@param_ `cycle_duration` — how frequently the scheduler runs
|
386
|
+
sig { params(cycle_duration: ActiveSupport::Duration).void }
|
387
|
+
def initialize(cycle_duration:); end
|
388
|
+
|
389
|
+
# Run a scheduling cycle. This should be called from your ActiveJob that runs on a regular Cron cadence. Ideally you
|
390
|
+
# would call the instance of the scheduler configured for the whole StepperMotor (so that the `cycle_duration` gets
|
391
|
+
# correctly applied, as it is necessary to pick the journeys to step). Normally, you would do this:
|
392
|
+
sig { void }
|
393
|
+
def run_scheduling_cycle; end
|
394
|
+
|
395
|
+
# sord omit - no YARD type given for "journey", using untyped
|
396
|
+
# sord omit - no YARD return type given, using untyped
|
397
|
+
sig { params(journey: T.untyped).returns(T.untyped) }
|
398
|
+
def schedule(journey); end
|
399
|
+
|
400
|
+
class RunSchedulingCycleJob < ActiveJob::Base
|
401
|
+
# sord omit - no YARD return type given, using untyped
|
402
|
+
sig { returns(T.untyped) }
|
403
|
+
def perform; end
|
404
|
+
end
|
405
|
+
end
|
406
|
+
|
407
|
+
class PerformStepJob < ActiveJob::Base
|
408
|
+
# sord omit - no YARD type given for "journey_gid", using untyped
|
409
|
+
# sord omit - no YARD return type given, using untyped
|
410
|
+
sig { params(journey_gid: T.untyped).returns(T.untyped) }
|
411
|
+
def perform(journey_gid); end
|
412
|
+
end
|
413
|
+
|
414
|
+
# The forward scheduler enqueues a job for every Journey that
|
415
|
+
# gets sent to the `#schedule`. The job is then stored in the queue
|
416
|
+
# and gets picked up by the ActiveJob worker normally. This is the simplest
|
417
|
+
# option if your ActiveJob adapter supports far-ahead scheduling. Some adapters,
|
418
|
+
# such as SQS, have limitations regarding the maximum delay after which a message
|
419
|
+
# will become visible. For SQS, the limit is 900 seconds. If the job is further in the future,
|
420
|
+
# it is likely going to fail to get enqueued. If you are working with a queue adapter that:
|
421
|
+
#
|
422
|
+
# * Does not allow easy introspection of jobs in the future (like Redis-based queues)
|
423
|
+
# * Limits the value of the `wait:` parameter
|
424
|
+
#
|
425
|
+
# this scheduler may not be a good fit for you, and you will need to use the {CyclicScheduler} instead.
|
426
|
+
# Note that this scheduler is also likely to populate your queue with a high number of "far out"
|
427
|
+
# jobs to be performed in the future. Different ActiveJob adapters are known to have varying
|
428
|
+
# performance depending on the number of jobs in the queue. For example, good_job is known to
|
429
|
+
# struggle a bit if the queue contains a large number of jobs (even if those jobs are not yet
|
430
|
+
# scheduled to be performed). For good_job the {CyclicScheduler} is also likely to be a better option.
|
431
|
+
class ForwardScheduler
|
432
|
+
# sord omit - no YARD type given for "journey", using untyped
|
433
|
+
# sord omit - no YARD return type given, using untyped
|
434
|
+
sig { params(journey: T.untyped).returns(T.untyped) }
|
435
|
+
def schedule(journey); end
|
436
|
+
end
|
437
|
+
|
438
|
+
class PerformStepJobV2 < ActiveJob::Base
|
439
|
+
# sord omit - no YARD type given for "journey_id:", using untyped
|
440
|
+
# sord omit - no YARD type given for "journey_class_name:", using untyped
|
441
|
+
# sord omit - no YARD type given for "idempotency_key:", using untyped
|
442
|
+
# sord omit - no YARD return type given, using untyped
|
443
|
+
sig { params(journey_id: T.untyped, journey_class_name: T.untyped, idempotency_key: T.untyped).returns(T.untyped) }
|
444
|
+
def perform(journey_id:, journey_class_name:, idempotency_key: nil); end
|
445
|
+
end
|
446
|
+
|
447
|
+
# The purpose of this job is to find journeys which have, for whatever reason, remained in the
|
448
|
+
# `performing` state for far longer than the journey is supposed to. At the moment it assumes
|
449
|
+
# any journey that stayed in `performing` for longer than 1 hour has hung. Add this job to your
|
450
|
+
# cron table and perform it regularly.
|
451
|
+
class RecoverStuckJourneysJobV1 < ActiveJob::Base
|
452
|
+
DEFAULT_STUCK_FOR = T.let(2.days, T.untyped)
|
453
|
+
|
454
|
+
# sord omit - no YARD type given for "stuck_for:", using untyped
|
455
|
+
# sord omit - no YARD return type given, using untyped
|
456
|
+
sig { params(stuck_for: T.untyped).returns(T.untyped) }
|
457
|
+
def perform(stuck_for: DEFAULT_STUCK_FOR); end
|
458
|
+
end
|
459
|
+
end
|