stepper_motor 0.1.12 → 0.1.15

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.
data/manual/MANUAL.md CHANGED
@@ -230,6 +230,322 @@ end
230
230
 
231
231
  Here, we first initiate a payment using an idempotency key, and then poll for its completion or failure repeatedly. When a payment fails or succeeds, we notify the sender and finish the `Journey`. Note that this `Journey` is of a _payment,_ not of the user. A user may have multiple Payments in flight, each with their own `Journey` being tracket transactionally and correctly.
232
232
 
233
+ ### Polling with anonymous steps
234
+
235
+ Here's an example of a Journey that polls an external API for a status update, using anonymous steps with increasing intervals:
236
+
237
+ ```ruby
238
+ class OrderStatusPollingJourney < StepperMotor::Journey
239
+ alias_method :order, :hero
240
+
241
+ # Initial check
242
+ step do
243
+ check_status!
244
+ end
245
+
246
+ # Check after 30 seconds
247
+ step(wait: 30.seconds) do
248
+ check_status!
249
+ end
250
+
251
+ # Check every minute for 5 minutes
252
+ 5.times do
253
+ step(wait: 1.minute) do
254
+ check_status!
255
+ end
256
+ end
257
+
258
+ # Check every 5 minutes for 30 minutes
259
+ 6.times do
260
+ step(wait: 5.minutes) do
261
+ check_status!
262
+ end
263
+ end
264
+
265
+ # Final check after 1 hour
266
+ step(wait: 1.hour) do
267
+ check_status!
268
+ end
269
+
270
+ private
271
+
272
+ def check_status!
273
+ status = ExternalOrderAPI.fetch_status(order.external_id)
274
+
275
+ case status
276
+ when :completed
277
+ order.complete!
278
+ finished!
279
+ when :failed
280
+ order.fail!
281
+ cancel!
282
+ when :processing
283
+ # Do nothing, will continue to next step
284
+ else
285
+ # Unexpected status, pause for manual investigation
286
+ pause!
287
+ end
288
+ end
289
+ end
290
+ ```
291
+
292
+ ## Defining steps
293
+
294
+ Steps are key building blocks in stepper_motor and can be defined in a number of ways. Rules of thumb:
295
+
296
+ * When steps are recalled to be performed, they get recalled _by name._
297
+ * When preparing for the next step or skipping to the next step, _the next step from the current in order of definition_ is going to be used.
298
+ * Step names within a Journey subclass must be unique.
299
+
300
+ stepper_motor will help you follow these rules, so don't worry too much about them just yet.
301
+
302
+ The step definition is some form of callable block, which is then going to run in the context of this Journey instance (or the Journey subclass' instance)
303
+
304
+ > [!TIP]
305
+ > Regardless whether the step definition is a method or a block - the step will be `instance_exec`-ed in the context of the Journey instance.
306
+ > Inside the step code you have access to all the methods of the Journey, including instance variables.
307
+
308
+ ### Anonymous steps
309
+
310
+ The simplest is using anonymous steps and blocks (stepper_motor will generate you step names):
311
+
312
+ ```ruby
313
+ class EraseAccountJourney < StepperMotor::Journey
314
+ step { hero.datapoints.in_batches.each(&:delete_all) }
315
+ step { hero.update!(email: "#{Digest::SHA1.hexdigest(hero.email)}@example.com") }
316
+ end
317
+ ```
318
+
319
+ Anonymous steps have a very good use for polling, when you want to define the repeats of the step programmatically. For example:
320
+
321
+ ```ruby
322
+ class PollStatusJourney < StepperMotor::Journey
323
+ alias_method :payment, :hero
324
+
325
+ step { verify_status! }
326
+
327
+ # Then after 5 minutes
328
+ step(wait: 5.minutes) { verify_status! }
329
+
330
+ # Check every 2 hours after
331
+ 12.times do
332
+ step(wait: 2.hours) { verify_status! }
333
+ end
334
+
335
+ # Check once a day after that
336
+ 7.times do
337
+ step(wait: 1.day) { verify_status! }
338
+ end
339
+
340
+ step :terminate do
341
+ payment.failed!
342
+ end
343
+
344
+ def verify_status!
345
+ status = payment.current_status
346
+ finished! if status.complete?
347
+ end
348
+ end
349
+ ```
350
+
351
+ ### Named steps
352
+
353
+ You can give the steps names. Step names must be unique and stepper_motor will raise an exception if you reuse a step name:
354
+
355
+ ```ruby
356
+ class EraseAccountJourney < StepperMotor::Journey
357
+ step :delete_datapoints { hero.datapoints.in_batches.each(&:delete_all) }
358
+ step :pseudonymize_email { hero.update!(email: "#{Digest::SHA1.hexdigest(hero.email)}@example.com") }
359
+ end
360
+ ```
361
+
362
+ The name of the step is what stepper_motor is going to persist to resume the Journey later. Naming your steps is useful because you then can then update your code and insert steps between the existing ones, and have your `Journey` correctly identify the right step. Steps are performed in the order they are defined. Imagine you start with this step sequence:
363
+
364
+ ```ruby
365
+ step :one do
366
+ # perform some action
367
+ end
368
+
369
+ step :two do
370
+ # perform some other action
371
+ end
372
+ ```
373
+
374
+ You have a `Journey` which is about to start step `one`. When the step gets performed, stepper_motor will do a lookup to find _the next step in order of definition._ In this case the step will be step `two`, so the name of that step will be saved with the `Journey`. Imagine you then edit the code to add an extra step between those:
375
+
376
+ ```ruby
377
+ step :one do
378
+ # perform some action
379
+ end
380
+
381
+ step :one_bis do
382
+ # some compliance action
383
+ end
384
+
385
+ step :two do
386
+ # perform some other action
387
+ end
388
+ ```
389
+
390
+ Your existing `Journey` is already primed to perform step `two`. However, a `Journey` which is about to perform step `one` will now set `one_bis` as the next step to perform. This allows limited reordering and editing of `Journey` definitions after they have already begun.
391
+
392
+ ### Using instance methods as steps
393
+
394
+ You can use instance methods of your Journey as steps by passing their name as a symbol to the `step` method:
395
+
396
+ ```ruby
397
+ class Erasure < StepperMotor::Journey
398
+ step :erase_attachments
399
+ step :erase_emails
400
+
401
+ def erase_attachments
402
+ hero.uploaded_attachments.find_each(&:destroy)
403
+ end
404
+
405
+ def erase_emails
406
+ while hero.emails.count > 0
407
+ hero.emails.limit(5000).delete_all
408
+ end
409
+ end
410
+ end
411
+ ```
412
+
413
+ Since a method definition in Ruby returns a Symbol, you can use the return value of the `def` expression
414
+ to define a `step` immediately:
415
+
416
+ ```ruby
417
+ class Erasure < StepperMotor::Journey
418
+ step def erase_attachments
419
+ hero.uploaded_attachments.find_each(&:destroy)
420
+ end
421
+
422
+ step def erase_emails
423
+ while hero.emails.count > 0
424
+ hero.emails.limit(5000).delete_all
425
+ end
426
+ end
427
+ end
428
+ ```
429
+
430
+ ### Conditional steps with `if:`
431
+
432
+ You can make steps conditional by using the `if:` parameter. This allows you to skip steps based on runtime conditions. The `if:` parameter accepts:
433
+
434
+ * A symbol (method name) that returns a boolean.
435
+ * A callable (lambda or proc) that returns a boolean. It will be `instance_exec`d in the context of the Journey.
436
+ * A literal boolean value (`true` or `false`) or a `nil` (treated as `false`)
437
+
438
+ When a step's condition evaluates to `false`, the step is skipped and the journey continues to the next step. If there are no more steps, the journey finishes.
439
+
440
+ #### Using method names as conditions
441
+
442
+ ```ruby
443
+ class UserOnboardingJourney < StepperMotor::Journey
444
+ step :send_welcome_email, if: :should_send_welcome? do
445
+ WelcomeMailer.welcome(hero).deliver_later
446
+ end
447
+
448
+ step :send_premium_offer, if: :is_premium_user? do
449
+ PremiumOfferMailer.exclusive_offer(hero).deliver_later
450
+ end
451
+
452
+ step :complete_onboarding do
453
+ hero.update!(onboarding_completed_at: Time.current)
454
+ end
455
+
456
+ private
457
+
458
+ def should_send_welcome?
459
+ hero.email.present? && !hero.welcome_email_sent?
460
+ end
461
+
462
+ def is_premium_user?
463
+ hero.subscription&.premium?
464
+ end
465
+ end
466
+ ```
467
+
468
+ #### Using callable conditions
469
+
470
+ You can use lambdas or procs for more dynamic conditions. They will be `instance_exec`d in the context of the Journey.:
471
+
472
+ ```ruby
473
+ class OrderProcessingJourney < StepperMotor::Journey
474
+ step :send_confirmation, if: -> { hero.email.present? } do
475
+ OrderConfirmationMailer.confirm(hero).deliver_later
476
+ end
477
+
478
+ step :process_payment
479
+ PaymentProcessor.charge(hero)
480
+ end
481
+ end
482
+ ```
483
+
484
+ #### Skipping steps with literal conditions
485
+
486
+ You can use literal boolean values to conditionally include or exclude steps:
487
+
488
+ ```ruby
489
+ class FeatureFlagJourney < StepperMotor::Journey
490
+ step :new_feature_step, if: Rails.application.config.new_feature_enabled do
491
+ NewFeatureService.process(hero)
492
+ end
493
+
494
+ step :legacy_step, if: ENV["PERFORM_LEGACY_STEP"] do # This step will never execute
495
+ LegacyService.process(hero)
496
+ end
497
+
498
+ step :always_execute do
499
+ # This step always runs
500
+ end
501
+ end
502
+ ```
503
+
504
+ When a step is skipped due to a false condition, the journey seamlessly continues to the next step without any interruption - or finishes if that step was the last one.
505
+
506
+ #### Accessing Journey state in conditions
507
+
508
+ It is possible to store instance variables on the `Journey` instance, but they do not persist between steps. This is very important to remember:
509
+
510
+ > [!WARNING]
511
+ > Because conditions are `instance_exec`d they can access instance variables of the `Journey`. It will also break majestically, because
512
+ > the `Journey` is getting persisted and then loaded from the database on a different matchine, and it is always consumed fresh.
513
+ > This means that the volatile state such as instance variables is not going to be available between steps. Always assume that
514
+ > the `Journey` you are inside of does not have any instance variables set by previous steps and has just been freshly loaded from the database.
515
+
516
+
517
+ ### Waiting for the start of the step
518
+
519
+ You configure how long a step should wait before starting using the `wait:` parameter. The `wait:` can be arbirarily long - but must be finite:
520
+
521
+ ```ruby
522
+ class DraconianPasswordResetJourney < StepperMotor::Journey
523
+ step :ask_for_password_reset, wait: 6.months do
524
+ PasswordExpiredMailer.expired(hero).deliver_later
525
+ reattempt!
526
+ emd
527
+ end
528
+ ```
529
+
530
+ The `wait:` parameter defines the amount of time computed **from the moment the Journey gets created or the previous step is completed.**
531
+
532
+ ## Journey states
533
+
534
+ The Journeys are managed using a state machine, which stepper_motor completely coordinates for you. The states are as follows:
535
+
536
+ ```mermaid
537
+ stateDiagram-v2
538
+ [*] --> ready: create
539
+ ready --> performing: perform_next_step!
540
+ performing --> ready: reattempt!
541
+ performing --> finished: step completes
542
+ performing --> canceled: cancel!
543
+ performing --> paused: pause! or exception
544
+ paused --> ready: resume!
545
+ finished --> [*]
546
+ canceled --> [*]
547
+ ```
548
+
233
549
  ## Flow control within steps
234
550
 
235
551
  Inside a step, you currently can use the following flow control methods:
@@ -250,7 +566,6 @@ Inside a step, you currently can use the following flow control methods:
250
566
 
251
567
  You can't call those methods outside of the context of a performing step, and an exception is going to be raised if you do.
252
568
 
253
-
254
569
  ## Transactional semantics within steps
255
570
 
256
571
  Getting the transactional semantics _right_ with a system like stepper_motor is crucial. We strike a decent balance between reliability/durability and performance, namely:
@@ -273,16 +588,12 @@ We recommend using a "co-committing" ActiveJob adapter with stepper_motor (an ad
273
588
  * [good_job](https://github.com/bensheldon/good_job)
274
589
  * [solid_queue](https://github.com/rails/solid_queue) - with the same database used for the queue as the one used for Journeys
275
590
 
276
- While Rails core admittedly [insists on the stylistic choice of denying the users the option of co-committing their jobs](https://github.com/rails/rails/pull/53375#issuecomment-2555694252) we find this a highly inconsiderate choice, which has highly negative consequences for a system such as stepper_motor - where durability is paramount. Having good defaults is appropriate, but not removing a crucial affordance that a DB-based job queue provides is downright user-hostile.
591
+ While Rails core admittedly [insists on the stylistic choice of denying the users the option of co-committing their jobs](https://github.com/rails/rails/pull/53375#issuecomment-2555694252) we find this a highly inconsiderate choice. It has negative consequences for a system such as stepper_motor - where durability is paramount. Having good defaults is great, but removing a crucial affordance that a DB-based job queue provides is user-hostile.
277
592
 
278
593
  In the future, stepper_motor _may_ move to a transactional outbox pattern whereby we emit events into a separate table and whichever queue adapter you have installed will be picking those messages up.
594
+ Note that at the moment - if your ActiveJob adapter enqueues after commit - it is possible for your app to crash _between_ the Journey committing and your job enqueueing.
279
595
 
280
- For its own "trigger" job (the `PerformStepJob` and its versions) stepper_motor is configured to commit it with the Journey state changes, within the same transaction.
281
-
282
- This is done for the following reasons:
283
-
284
- * Not having the Journey with up-to-date state in teh DB when the job performs will lead to the job silently skipping, which is undesirable
285
- * But having the app crash between the Journey state committing and the trigger job committing is even less desirable. This would lead to jobs hanging in the `ready` state indefinitly, seemingly at random.
596
+ We may address this in the future using a transactional outbox.
286
597
 
287
598
  ## Saving side-effects of steps
288
599
 
@@ -386,102 +697,40 @@ end
386
697
 
387
698
  If you use in-time scheduling you will need to add the `StepperMotor::ScheduleLoopJob` to your cron jobs, and perform it frequently enough. Note that having just the granularity of your cron jobs (minutes) may not be enough as reattempts of the steps may get scheduled with a smaller delay - of a few seconds, for instance.
388
699
 
389
- ## Naming steps
390
-
391
- stepper_motor will name steps for you. However, using named steps is useful because you then can insert steps between existing ones, and have your `Journey` correctly identify the right step. Steps are performed in the order they are defined. Imagine you start with this step sequence:
392
-
393
- ```ruby
394
- step :one do
395
- # perform some action
396
- end
397
-
398
- step :two do
399
- # perform some other action
400
- end
401
- ```
402
-
403
- You have a `Journey` which is about to start step `one`. When the step gets performed, stepper_motor will do a lookup to find _the next step in order of definition._ In this case the step will be step `two`, so the name of that step will be saved with the `Journey`. Imagine you then edit the code to add an extra step between those:
404
-
405
- ```ruby
406
- step :one do
407
- # perform some action
408
- end
409
-
410
- step :one_bis do
411
- # some compliance action
412
- end
413
-
414
- step :two do
415
- # perform some other action
416
- end
417
- ```
700
+ ## Exception handling inside steps
418
701
 
419
- Your existing `Journey` is already primed to perform step `two`. However, a `Journey` which is about to perform step `one` will now set `one_bis` as the next step to perform. This allows limited reordering and editing of `Journey` definitions after they have already begun.
702
+ > [!IMPORTANT]
703
+ > Exception handling in steps is in flux, expect API changes.
420
704
 
421
- So, rules of thumb:
705
+ By default, an exception raised inside a step of a Journey will _pause_ that Journey at that step. This is done for a number of reasons:
422
706
 
423
- * When steps are recalled to be performed, they get recalled _by name._
424
- * When preparing for the next step, _the next step from the current in order of definition_ is going to be used.
707
+ * An endlessly reattempting step can cause load on your infrastructure and will never stop retrying
708
+ * Since at the moment there is no configuration for backoff, such a step is likely to hit rate limits on the external resource it hits
709
+ * It is likely a condition that was not anticipated when the Journey was written, thus a blind reattempt is unwise.
425
710
 
426
- ## Using instance methods as steps
711
+ While we may change this in future versions of `stepper_motor`, the current default is thus to `pause!` the Journey if an unhandled exception occurs.
427
712
 
428
- You can use instance methods as steps by passing their name as a symbol to the `step` method:
713
+ Should that happen, you can query for the paused Journeys in your Rails console:
429
714
 
430
715
  ```ruby
431
- class Erasure < StepperMotor::Journey
432
- step :erase_attachments
433
- step :erase_emails
434
-
435
- def erase_attachments
436
- hero.uploaded_attachments.find_each(&:destroy)
437
- end
438
-
439
- def erase_emails
440
- while hero.emails.count > 0
441
- hero.emails.limit(5000).delete_all
442
- end
443
- end
444
- end
716
+ my-app(dev)> StepperMotor::Journey.paused
445
717
  ```
446
718
 
447
- Since a method definition in Ruby returns a Symbol, you can use the return value of the `def` expression
448
- to define a `step` immediately:
719
+ If a journey has been paused, you can resume it manually from the Rails console once you have investigated and rectified the error. You do so by calling `resume!` on it:
449
720
 
450
721
  ```ruby
451
- class Erasure < StepperMotor::Journey
452
- step def erase_attachments
453
- hero.uploaded_attachments.find_each(&:destroy)
454
- end
455
-
456
- step def erase_emails
457
- while hero.emails.count > 0
458
- hero.emails.limit(5000).delete_all
459
- end
460
- end
461
- end
722
+ my-app(dev)> journey = StepperMotor::Journey.paused.first!
723
+ my-app(dev)> paused_journey.resume!
462
724
  ```
463
- ## Exception handling inside steps
464
-
465
- > [!IMPORTANT]
466
- > Exception handling in steps is in flux, expect API changes.
467
-
468
- When performing the step, any exceptions raised from within the step will be stored in a local
469
- variable to allow the Journey to be released as either `ready`, `finished` or `canceled`. The exception
470
- will be raised from within an `ensure` block after the persistence of the Journey has been taken care of.
471
-
472
- By default, an exception raised inside a step of a Journey will _pause_ that Journey. This is done for a number of reasons:
473
725
 
474
- * An endlessly reattempting step can cause load on your infrastructure and will never stop retrying
475
- * Since at the moment there is no configuration for backoff, such a step is likely to hit rate limits on the external resource it hits
476
- * It is likely a condition that was not anticipated when the Journey was written, thus a blind reattempt is unwise.
726
+ This action is deliberately manual. You can also set up a job which automatically resumes paused Journeys, but we do not recommend that as things usually fail for a reason.
477
727
 
478
- While we may change this in future versions of `stepper_motor`, the current default is thus to `pause!` the Journey if an unhandled
479
- exception occurs. You can, however, switch it to `reattempt!` or `cancel!` a Journey should a particular step raise. This is configured per step:
728
+ If you can make a decision to cancel or reattempt the journey - that's possible as well. This is configured per step:
480
729
 
481
730
  ```ruby
482
731
  class Erasure < StepperMotor::Journey
483
732
  step :initiate_deletion, on_exception: :reattempt! do
484
- # ..Do the requisite work
733
+ # ..Do the requisite work with aggressive retries
485
734
  end
486
735
  end
487
736
  ```
@@ -524,4 +773,6 @@ class Payment < StepperMotor::Journey
524
773
  end
525
774
  ```
526
775
 
776
+ It can be helpful to understand how exception handling is done by stepper_motor internally: when performing the step, any exceptions raised from within the step will be stored in a local variable to allow the Journey to be released as `ready`, `finished`, `canceled` or `paused`.
527
777
 
778
+ That exception will is then going to be raised from within an `ensure` block, but only after the persistence of the Journey has been taken care of. This way the Journey has way more chance to reach a stable persisted state where it can be recovered (provided the database accepts the write, of course).
@@ -2,7 +2,7 @@
2
2
  # StepperMotor is a module for building multi-step flows where steps are sequential and only
3
3
  # ever progress forward. The building block of StepperMotor is StepperMotor::Journey
4
4
  module StepperMotor
5
- VERSION = T.let("0.1.12", T.untyped)
5
+ VERSION = T.let("0.1.15", T.untyped)
6
6
  PerformStepJobV2 = T.let(StepperMotor::PerformStepJob, T.untyped)
7
7
  RecoverStuckJourneysJobV1 = T.let(StepperMotor::RecoverStuckJourneysJob, T.untyped)
8
8
 
@@ -33,16 +33,27 @@ module StepperMotor
33
33
  # _@param_ `wait` — the amount of time to wait before entering the step
34
34
  #
35
35
  # _@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.
36
+ #
37
+ # _@param_ `if` — condition to check before performing the step. If a boolean is provided, it will be used directly. If nil is provided, it will be treated as false. If a symbol is provided, it will call the method on the Journey. If a block is provided, it will be executed with the Journey as context. The step will only be performed if the condition returns a truthy value.
36
38
  sig do
37
39
  params(
38
40
  name: T.any(String, Symbol),
39
41
  seq: T.untyped,
40
42
  on_exception: Symbol,
41
43
  wait: T.any(Numeric, ActiveSupport::Duration),
44
+ if: T.any(TrueClass, FalseClass, NilClass, Symbol, Proc),
42
45
  step_block: T.untyped
43
46
  ).void
44
47
  end
45
- def initialize(name:, seq:, on_exception:, wait: 0, &step_block); end
48
+ def initialize(name:, seq:, on_exception:, wait: 0, if: true, &step_block); end
49
+
50
+ # Checks if the step should be performed based on the if condition
51
+ #
52
+ # _@param_ `journey` — the journey to check the condition for
53
+ #
54
+ # _@return_ — true if the step should be performed, false otherwise
55
+ sig { params(journey: StepperMotor::Journey).returns(T::Boolean) }
56
+ def should_perform?(journey); end
46
57
 
47
58
  # Performs the step on the passed Journey, wrapping the step with the required context.
48
59
  #
@@ -128,6 +139,8 @@ module StepperMotor
128
139
  #
129
140
  # _@param_ `on_exception` — See {StepperMotor::Step#on_exception}
130
141
  #
142
+ # _@param_ `if` — condition to check before performing the step. If a symbol is provided, it will call the method on the Journey. If a block is provided, it will be executed with the Journey as context. The step will only be performed if the condition returns a truthy value.
143
+ #
131
144
  # _@param_ `additional_step_definition_options` — Any remaining options get passed to `StepperMotor::Step.new` as keyword arguments.
132
145
  #
133
146
  # _@return_ — the step definition that has been created
@@ -137,11 +150,12 @@ module StepperMotor
137
150
  wait: T.nilable(T.any(Float, T.untyped, ActiveSupport::Duration)),
138
151
  after: T.nilable(T.any(Float, T.untyped, ActiveSupport::Duration)),
139
152
  on_exception: Symbol,
140
- additional_step_definition_options: T.untyped,
153
+ if: T.any(TrueClass, FalseClass, Symbol, Proc),
154
+ additional_step_definition_options: T::Hash[T.untyped, T.untyped],
141
155
  blk: T.untyped
142
156
  ).returns(StepperMotor::Step)
143
157
  end
144
- def self.step(name = nil, wait: nil, after: nil, on_exception: :pause!, **additional_step_definition_options, &blk); end
158
+ def self.step(name = nil, wait: nil, after: nil, on_exception: :pause!, if: true, **additional_step_definition_options, &blk); end
145
159
 
146
160
  # sord warn - "StepperMotor::Step?" does not appear to be a type
147
161
  # Returns the `Step` object for a named step. This is used when performing a step, but can also
@@ -31,13 +31,23 @@ module StepperMotor
31
31
  # _@param_ `wait` — the amount of time to wait before entering the step
32
32
  #
33
33
  # _@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.
34
+ #
35
+ # _@param_ `if` — condition to check before performing the step. If a boolean is provided, it will be used directly. If nil is provided, it will be treated as false. If a symbol is provided, it will call the method on the Journey. If a block is provided, it will be executed with the Journey as context. The step will only be performed if the condition returns a truthy value.
34
36
  def initialize: (
35
37
  name: (String | Symbol),
36
38
  seq: untyped,
37
39
  on_exception: Symbol,
38
- ?wait: (Numeric | ActiveSupport::Duration)
40
+ ?wait: (Numeric | ActiveSupport::Duration),
41
+ ?if: (TrueClass | FalseClass | NilClass | Symbol | Proc)
39
42
  ) -> void
40
43
 
44
+ # Checks if the step should be performed based on the if condition
45
+ #
46
+ # _@param_ `journey` — the journey to check the condition for
47
+ #
48
+ # _@return_ — true if the step should be performed, false otherwise
49
+ def should_perform?: (StepperMotor::Journey journey) -> bool
50
+
41
51
  # Performs the step on the passed Journey, wrapping the step with the required context.
42
52
  #
43
53
  # _@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
@@ -117,6 +127,8 @@ module StepperMotor
117
127
  #
118
128
  # _@param_ `on_exception` — See {StepperMotor::Step#on_exception}
119
129
  #
130
+ # _@param_ `if` — condition to check before performing the step. If a symbol is provided, it will call the method on the Journey. If a block is provided, it will be executed with the Journey as context. The step will only be performed if the condition returns a truthy value.
131
+ #
120
132
  # _@param_ `additional_step_definition_options` — Any remaining options get passed to `StepperMotor::Step.new` as keyword arguments.
121
133
  #
122
134
  # _@return_ — the step definition that has been created
@@ -125,7 +137,8 @@ module StepperMotor
125
137
  ?wait: (Float | untyped | ActiveSupport::Duration)?,
126
138
  ?after: (Float | untyped | ActiveSupport::Duration)?,
127
139
  ?on_exception: Symbol,
128
- **untyped additional_step_definition_options
140
+ ?if: (TrueClass | FalseClass | Symbol | Proc),
141
+ **::Hash[untyped, untyped] additional_step_definition_options
129
142
  ) -> StepperMotor::Step
130
143
 
131
144
  # sord warn - "StepperMotor::Step?" does not appear to be a type
@@ -37,6 +37,8 @@ Gem::Specification.new do |spec|
37
37
  spec.add_development_dependency "minitest"
38
38
  spec.add_development_dependency "rails", "~> 7.0"
39
39
  spec.add_development_dependency "sqlite3"
40
+ spec.add_development_dependency "mysql2"
41
+ spec.add_development_dependency "pg"
40
42
  spec.add_development_dependency "rake", "~> 13.0"
41
43
  spec.add_development_dependency "standard", "~> 1.50.0", "< 2.0"
42
44
  spec.add_development_dependency "magic_frozen_string_literal"
@@ -0,0 +1,14 @@
1
+ default: &default
2
+ adapter: mysql2
3
+ database: stepper_motor_dummy_<%= Rails.env %>
4
+ pool: <%= ENV.fetch("RAILS_MAX_THREADS") { 5 } %>
5
+ username: root
6
+ password: constabulary
7
+ timeout: 5000
8
+
9
+ development:
10
+ <<: *default
11
+ test:
12
+ <<: *default
13
+ production:
14
+ <<: *default
@@ -0,0 +1,14 @@
1
+ default: &default
2
+ adapter: postgres
3
+ database: stepper_motor_dummy_<%= Rails.env %>
4
+ pool: <%= ENV.fetch("RAILS_MAX_THREADS") { 5 } %>
5
+ username: root
6
+ password: constabulary
7
+ timeout: 5000
8
+
9
+ development:
10
+ <<: *default
11
+ test:
12
+ <<: *default
13
+ production:
14
+ <<: *default
@@ -0,0 +1,32 @@
1
+ # SQLite. Versions 3.8.0 and up are supported.
2
+ # gem install sqlite3
3
+ #
4
+ # Ensure the SQLite 3 gem is defined in your Gemfile
5
+ # gem "sqlite3"
6
+ #
7
+ default: &default
8
+ adapter: sqlite3
9
+ pool: <%= ENV.fetch("RAILS_MAX_THREADS") { 5 } %>
10
+ timeout: 5000
11
+
12
+ development:
13
+ <<: *default
14
+ database: storage/development.sqlite3
15
+
16
+ # Warning: The database defined as "test" will be erased and
17
+ # re-generated from your development database when you run "rake".
18
+ # Do not set this db to the same as development or production.
19
+ test:
20
+ <<: *default
21
+ database: storage/test.sqlite3
22
+
23
+
24
+ # SQLite3 write its data on the local filesystem, as such it requires
25
+ # persistent disks. If you are deploying to a managed service, you should
26
+ # make sure it provides disk persistence, as many don't.
27
+ #
28
+ # Similarly, if you deploy your application as a Docker container, you must
29
+ # ensure the database is located in a persisted volume.
30
+ production:
31
+ <<: *default
32
+ # database: path/to/persistent/storage/production.sqlite3
@@ -9,7 +9,7 @@
9
9
  #
10
10
  # and add its cycle job into your recurring jobs table. For example, for solid_queue:
11
11
  #
12
- # stepper_motor_houseleeping:
12
+ # run_stepper_motor_scheduling_cycle:
13
13
  # schedule: "*/30 * * * *" # Every 30 minutes
14
14
  # class: "StepperMotor::CyclicScheduler::RunSchedulingCycleJob"
15
15
  #