stepper_motor 0.1.9 → 0.1.11
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 +19 -6
- data/README.md +2 -4
- data/lib/generators/install_generator.rb +5 -5
- data/lib/generators/stepper_motor_migration_004.rb.erb +2 -2
- data/lib/stepper_motor/base_job.rb +5 -0
- data/lib/stepper_motor/cyclic_scheduler.rb +1 -1
- data/lib/stepper_motor/delete_completed_journeys_job.rb +16 -0
- data/lib/stepper_motor/forward_scheduler.rb +1 -1
- data/lib/stepper_motor/housekeeping_job.rb +8 -0
- data/lib/stepper_motor/perform_step_job.rb +25 -2
- data/lib/stepper_motor/{recover_stuck_journeys_job_v1.rb → recover_stuck_journeys_job.rb} +5 -4
- data/lib/stepper_motor/version.rb +1 -1
- data/lib/stepper_motor.rb +14 -2
- data/lib/tasks/stepper_motor_tasks.rake +1 -1
- data/manual/MANUAL.md +0 -11
- data/rbi/stepper_motor.rbi +48 -16
- data/sig/stepper_motor.rbs +40 -12
- data/stepper_motor.gemspec +1 -1
- data/test/dummy/config/initializers/stepper_motor.rb +39 -0
- data/test/dummy/db/migrate/{20250525132526_stepper_motor_migration_004.rb → 20250528141038_stepper_motor_migration_004.rb} +2 -2
- data/test/dummy/db/schema.rb +1 -1
- data/test/stepper_motor/completed_journeys_cleanup_test.rb +96 -0
- data/test/stepper_motor/configuration_test.rb +8 -0
- data/test/stepper_motor/cyclic_scheduler_test.rb +2 -2
- data/test/stepper_motor/forward_scheduler_test.rb +1 -1
- data/test/stepper_motor/housekeeping_job_test.rb +13 -0
- data/test/stepper_motor/perform_step_job_test.rb +60 -0
- data/test/stepper_motor/recover_stuck_journeys_job_test.rb +8 -3
- metadata +25 -10
- data/lib/stepper_motor/perform_step_job_v2.rb +0 -12
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 619a3e8a9435e06f7fcf5b61ddc41d7ebb62450cb9fafccf89f8399818264ef0
|
4
|
+
data.tar.gz: 3c3009f6b5c48a8dcdcd92187dbadc21d92c37b86746b8426dba8b6b60cb007f
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 1911106735b7ef6d54e7b3149cfbf26a13a6a0ec8e69a45429e244216c32f46b63b114b8101b369a36c22cdae10682a8c0adf599f18efe9bcfd2a9ded05be3ab
|
7
|
+
data.tar.gz: e9996b145db211444300230ae742e8da61f8db21f3841a21c97a37779203767f4fc46535656c698c494001bbe024c6cd681599595df5a73fdaae101417ebde4c
|
data/CHANGELOG.md
CHANGED
@@ -1,16 +1,30 @@
|
|
1
1
|
# Changelog
|
2
2
|
|
3
|
+
## [0.1.11] - 2025-06-08
|
4
|
+
|
5
|
+
- Add automatic cleanup of completed journeys after a configurable time period
|
6
|
+
- Add `HousekeepingJob` to run cleanup and recovery tasks
|
7
|
+
- Add ability to extend all StepperMotor jobs with custom configuration
|
8
|
+
- Merge V2/V1 job variants into single classes with backward compatibility
|
9
|
+
- Pin standardrb version to avoid Rubocop errors
|
10
|
+
- Improve documentation and test coverage
|
11
|
+
|
12
|
+
## [0.1.10] - 2025-05-28
|
13
|
+
|
14
|
+
- Remove `algorithm: :concurrently` from migrations. If a user needs to conform with strong_migrations
|
15
|
+
they can always edit the migration themselves.
|
16
|
+
|
3
17
|
## [0.1.9] - 2025-05-25
|
4
18
|
|
5
19
|
- Repair bodged migration from the previous release
|
6
20
|
|
7
21
|
## [0.1.8] - 2025-05-25
|
8
22
|
|
9
|
-
- Add ability to pause and resume journeys (
|
10
|
-
- Add basic exception rescue during steps (
|
11
|
-
- Add idempotency keys when performing steps (
|
12
|
-
- Add support for blockless step definitions (
|
13
|
-
- Migrate from RSpec to Minitest (
|
23
|
+
- Add ability to pause and resume journeys (https://github.com/stepper-motor/stepper_motor/pull/24)
|
24
|
+
- Add basic exception rescue during steps (https://github.com/stepper-motor/stepper_motor/pull/23)
|
25
|
+
- Add idempotency keys when performing steps (https://github.com/stepper-motor/stepper_motor/pull/21)
|
26
|
+
- Add support for blockless step definitions (https://github.com/stepper-motor/stepper_motor/pull/22)
|
27
|
+
- Migrate from RSpec to Minitest (https://github.com/stepper-motor/stepper_motor/pull/19)
|
14
28
|
- Add Rake task for recovery
|
15
29
|
- Add proper Rails engine tests
|
16
30
|
- Improve test organization and coverage
|
@@ -41,7 +55,6 @@
|
|
41
55
|
- Refactor PerformStepJob to use Journey class in job arguments
|
42
56
|
- Remove GlobalID dependency
|
43
57
|
- Add ability to resolve Journey from base class using `find()`
|
44
|
-
- Prepare groundwork for future improvements (#12)
|
45
58
|
|
46
59
|
## [0.1.4] - 2025-03-11
|
47
60
|
|
data/README.md
CHANGED
@@ -24,7 +24,7 @@ end
|
|
24
24
|
SignupJourney.create!(hero: current_user)
|
25
25
|
```
|
26
26
|
|
27
|
-
Want to know more? Dive into the [manual](
|
27
|
+
Want to know more? Dive into the [manual](/manual/MANUAL.md) we provide.
|
28
28
|
|
29
29
|
## Installation
|
30
30
|
|
@@ -47,9 +47,7 @@ Starting with versions 0.2.x and up, stepper_motor will make the best possible e
|
|
47
47
|
|
48
48
|
## Development
|
49
49
|
|
50
|
-
After checking out the repo, run `
|
51
|
-
|
52
|
-
To install this gem onto your local machine, run `bundle exec rake install`. To release a new version, update the version number in `version.rb`, and then run `bundle exec rake release`, which will create a git tag for the version, push git commits and the created tag, and push the `.gem` file to [rubygems.org](https://rubygems.org).
|
50
|
+
After checking out the repo, run `bundle` to install dependencies. The development process from there on is like any other gem.
|
53
51
|
|
54
52
|
## Is it any good?
|
55
53
|
|
@@ -9,8 +9,8 @@ module StepperMotor
|
|
9
9
|
# Run it with `bin/rails g stepper_motor:install` in your console.
|
10
10
|
class InstallGenerator < Rails::Generators::Base
|
11
11
|
UUID_MESSAGE = <<~MSG
|
12
|
-
If set, uuid type will be used for hero_id
|
13
|
-
if most of your models use UUD as primary key"
|
12
|
+
If set, uuid type will be used for hero_id of the Journeys, as well as for the Journey IDs.
|
13
|
+
Use this if most of your models use UUD as primary key"
|
14
14
|
MSG
|
15
15
|
|
16
16
|
include ActiveRecord::Generators::Migration
|
@@ -31,9 +31,9 @@ module StepperMotor
|
|
31
31
|
end
|
32
32
|
|
33
33
|
def create_initializer
|
34
|
-
|
35
|
-
|
36
|
-
|
34
|
+
# Take the initializer code from the test dummy app, it is the best place really.
|
35
|
+
initializer_code_from_dummy_app = File.read(__dir__ + "/../../test/dummy/config/initializers/stepper_motor.rb")
|
36
|
+
create_file "config/initializers/stepper_motor.rb", initializer_code_from_dummy_app
|
37
37
|
end
|
38
38
|
|
39
39
|
private
|
@@ -7,7 +7,7 @@ class StepperMotorMigration004 < ActiveRecord::Migration[<%= migration_version %
|
|
7
7
|
name: :idx_journeys_one_per_hero_with_paused
|
8
8
|
|
9
9
|
# Remove old indexes that only include 'ready' state
|
10
|
-
remove_index :stepper_motor_journeys, [:type, :hero_id, :hero_type], name: :one_per_hero_index, where: "allow_multiple = '0' AND state IN ('ready', 'performing')"
|
10
|
+
remove_index :stepper_motor_journeys, [:type, :hero_id, :hero_type], name: :one_per_hero_index, where: "allow_multiple = '0' AND state IN ('ready', 'performing')"
|
11
11
|
end
|
12
12
|
|
13
13
|
def down
|
@@ -19,6 +19,6 @@ class StepperMotorMigration004 < ActiveRecord::Migration[<%= migration_version %
|
|
19
19
|
name: :one_per_hero_index
|
20
20
|
|
21
21
|
# Remove new indexes
|
22
|
-
remove_index :stepper_motor_journeys, name: :idx_journeys_one_per_hero_with_paused
|
22
|
+
remove_index :stepper_motor_journeys, name: :idx_journeys_one_per_hero_with_paused
|
23
23
|
end
|
24
24
|
end
|
@@ -21,7 +21,7 @@
|
|
21
21
|
#
|
22
22
|
# The scheduler needs to be configured in your cron table.
|
23
23
|
class StepperMotor::CyclicScheduler < StepperMotor::ForwardScheduler
|
24
|
-
class RunSchedulingCycleJob <
|
24
|
+
class RunSchedulingCycleJob < StepperMotor::BaseJob
|
25
25
|
def perform
|
26
26
|
scheduler = StepperMotor.scheduler
|
27
27
|
return unless scheduler.is_a?(StepperMotor::CyclicScheduler)
|
@@ -0,0 +1,16 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
# The purpose of this job is to find journeys which have completed (finished or canceled) some
|
4
|
+
# time ago and to delete them. The time is configured in the initializer.
|
5
|
+
class StepperMotor::DeleteCompletedJourneysJob < StepperMotor::BaseJob
|
6
|
+
def perform(completed_for: StepperMotor.delete_completed_journeys_after, **)
|
7
|
+
return unless completed_for.present?
|
8
|
+
|
9
|
+
scope = StepperMotor::Journey.where(state: ["finished", "canceled"], updated_at: ..completed_for.ago)
|
10
|
+
scope.in_batches.each do |rel|
|
11
|
+
rel.delete_all
|
12
|
+
rescue => e
|
13
|
+
Rails.try(:error).try(:report, e)
|
14
|
+
end
|
15
|
+
end
|
16
|
+
end
|
@@ -19,7 +19,7 @@
|
|
19
19
|
# scheduled to be performed). For good_job the {CyclicScheduler} is also likely to be a better option.
|
20
20
|
class StepperMotor::ForwardScheduler
|
21
21
|
def schedule(journey)
|
22
|
-
StepperMotor::
|
22
|
+
StepperMotor::PerformStepJob
|
23
23
|
.set(wait_until: journey.next_step_to_be_performed_at)
|
24
24
|
.perform_later(journey_id: journey.id, journey_class_name: journey.class.to_s, idempotency_key: journey.idempotency_key)
|
25
25
|
end
|
@@ -2,8 +2,18 @@
|
|
2
2
|
|
3
3
|
require "active_job"
|
4
4
|
|
5
|
-
class StepperMotor::PerformStepJob <
|
6
|
-
def perform(
|
5
|
+
class StepperMotor::PerformStepJob < StepperMotor::BaseJob
|
6
|
+
def perform(*posargs, **kwargs)
|
7
|
+
if posargs.length == 1 && kwargs.empty?
|
8
|
+
perform_via_journey_gid(*posargs)
|
9
|
+
else
|
10
|
+
perform_via_kwargs(**kwargs)
|
11
|
+
end
|
12
|
+
end
|
13
|
+
|
14
|
+
private
|
15
|
+
|
16
|
+
def perform_via_journey_gid(journey_gid)
|
7
17
|
# Pass the GlobalID instead of the record itself, so that we can rescue the non-existing record
|
8
18
|
# exception here as opposed to the job deserialization
|
9
19
|
journey = begin
|
@@ -13,4 +23,17 @@ class StepperMotor::PerformStepJob < ActiveJob::Base
|
|
13
23
|
end
|
14
24
|
journey.perform_next_step!
|
15
25
|
end
|
26
|
+
|
27
|
+
def perform_via_kwargs(journey_id:, journey_class_name:, idempotency_key: nil, **)
|
28
|
+
journey = begin
|
29
|
+
StepperMotor::Journey.find(journey_id)
|
30
|
+
rescue ActiveRecord::RecordNotFound
|
31
|
+
# The journey has been canceled and destroyed previously or elsewhere
|
32
|
+
return
|
33
|
+
end
|
34
|
+
journey.perform_next_step!(idempotency_key: idempotency_key)
|
35
|
+
end
|
16
36
|
end
|
37
|
+
|
38
|
+
# Alias for the previous job name
|
39
|
+
StepperMotor::PerformStepJobV2 = StepperMotor::PerformStepJob
|
@@ -1,19 +1,20 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
|
-
require "active_job"
|
4
|
-
|
5
3
|
# The purpose of this job is to find journeys which have, for whatever reason, remained in the
|
6
4
|
# `performing` state for far longer than the journey is supposed to. At the moment it assumes
|
7
5
|
# any journey that stayed in `performing` for longer than 1 hour has hung. Add this job to your
|
8
6
|
# cron table and perform it regularly.
|
9
|
-
class StepperMotor::
|
7
|
+
class StepperMotor::RecoverStuckJourneysJob < StepperMotor::BaseJob
|
10
8
|
DEFAULT_STUCK_FOR = 2.days
|
11
9
|
|
12
10
|
def perform(stuck_for: DEFAULT_STUCK_FOR)
|
13
11
|
StepperMotor::Journey.stuck(stuck_for.ago).find_each do |journey|
|
14
12
|
journey.recover!
|
15
13
|
rescue => e
|
16
|
-
Rails
|
14
|
+
Rails.try(:error).try(:report, e)
|
17
15
|
end
|
18
16
|
end
|
19
17
|
end
|
18
|
+
|
19
|
+
# Alias for the previous job name
|
20
|
+
StepperMotor::RecoverStuckJourneysJobV1 = StepperMotor::RecoverStuckJourneysJob
|
data/lib/stepper_motor.rb
CHANGED
@@ -13,13 +13,25 @@ module StepperMotor
|
|
13
13
|
|
14
14
|
autoload :Journey, File.dirname(__FILE__) + "/stepper_motor/journey.rb"
|
15
15
|
autoload :Step, File.dirname(__FILE__) + "/stepper_motor/step.rb"
|
16
|
+
|
17
|
+
autoload :BaseJob, File.dirname(__FILE__) + "/stepper_motor/base_job.rb"
|
16
18
|
autoload :PerformStepJob, File.dirname(__FILE__) + "/stepper_motor/perform_step_job.rb"
|
17
|
-
autoload :PerformStepJobV2, File.dirname(__FILE__) + "/stepper_motor/
|
18
|
-
autoload :
|
19
|
+
autoload :PerformStepJobV2, File.dirname(__FILE__) + "/stepper_motor/perform_step_job.rb"
|
20
|
+
autoload :HousekeepingJob, File.dirname(__FILE__) + "/stepper_motor/housekeeping_job.rb"
|
21
|
+
autoload :DeleteCompletedJourneysJob, File.dirname(__FILE__) + "/stepper_motor/delete_completed_journeys_job.rb"
|
22
|
+
autoload :RecoverStuckJourneysJob, File.dirname(__FILE__) + "/stepper_motor/recover_stuck_journeys_job.rb"
|
23
|
+
autoload :RecoverStuckJourneysJobV1, File.dirname(__FILE__) + "/stepper_motor/recover_stuck_journeys_job.rb"
|
24
|
+
|
19
25
|
autoload :InstallGenerator, File.dirname(__FILE__) + "/generators/install_generator.rb"
|
20
26
|
autoload :ForwardScheduler, File.dirname(__FILE__) + "/stepper_motor/forward_scheduler.rb"
|
21
27
|
autoload :CyclicScheduler, File.dirname(__FILE__) + "/stepper_motor/cyclic_scheduler.rb"
|
22
28
|
autoload :TestHelper, File.dirname(__FILE__) + "/stepper_motor/test_helper.rb"
|
23
29
|
|
24
30
|
mattr_accessor :scheduler, default: ForwardScheduler.new
|
31
|
+
mattr_accessor :delete_completed_journeys_after, default: 30.days
|
32
|
+
|
33
|
+
# Extends the BaseJob of the library with any additional options
|
34
|
+
def self.extend_base_job(&blk)
|
35
|
+
BaseJob.class_eval(&blk)
|
36
|
+
end
|
25
37
|
end
|
data/manual/MANUAL.md
CHANGED
@@ -1,14 +1,3 @@
|
|
1
|
-
|
2
|
-
## Installation
|
3
|
-
|
4
|
-
Add the gem to the application's Gemfile, and then generate and run the migration
|
5
|
-
|
6
|
-
$ bundle add stepper_motor
|
7
|
-
$ bundle install
|
8
|
-
$ bin/rails g stepper_motor:install --uuid # Pass "uuid" if you are using UUID for your primary and foreign keys
|
9
|
-
$ bin/rails db:migrate
|
10
|
-
|
11
|
-
|
12
1
|
## Intro
|
13
2
|
|
14
3
|
`stepper_motor` solves a real, tangible problem in Rails apps - tracking activities over long periods of time. It does so in a durable, reentrant and consistent manner, utilizing the guarantees provided by your relational database you already have.
|
data/rbi/stepper_motor.rbi
CHANGED
@@ -2,7 +2,14 @@
|
|
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.
|
5
|
+
VERSION = T.let("0.1.11", T.untyped)
|
6
|
+
PerformStepJobV2 = T.let(StepperMotor::PerformStepJob, T.untyped)
|
7
|
+
RecoverStuckJourneysJobV1 = T.let(StepperMotor::RecoverStuckJourneysJob, T.untyped)
|
8
|
+
|
9
|
+
# sord omit - no YARD return type given, using untyped
|
10
|
+
# Extends the BaseJob of the library with any additional options
|
11
|
+
sig { params(blk: T.untyped).returns(T.untyped) }
|
12
|
+
def self.extend_base_job(&blk); end
|
6
13
|
|
7
14
|
class Error < StandardError
|
8
15
|
end
|
@@ -304,6 +311,12 @@ module StepperMotor
|
|
304
311
|
class Railtie < Rails::Railtie
|
305
312
|
end
|
306
313
|
|
314
|
+
# All StepperMotor job classes inherit from this one. It is available for
|
315
|
+
# extension from StepperMotor.extend_base_job_class so that you can set
|
316
|
+
# priority, include and prepend modules and so forth.
|
317
|
+
class BaseJob < ActiveJob::Base
|
318
|
+
end
|
319
|
+
|
307
320
|
module TestHelper
|
308
321
|
# Allows running a given Journey to completion, skipping across the waiting periods.
|
309
322
|
# This is useful to evaluate all side effects of a Journey. The helper will ensure
|
@@ -334,8 +347,8 @@ module StepperMotor
|
|
334
347
|
class InstallGenerator < Rails::Generators::Base
|
335
348
|
include ActiveRecord::Generators::Migration
|
336
349
|
UUID_MESSAGE = T.let(<<~MSG, T.untyped)
|
337
|
-
If set, uuid type will be used for hero_id
|
338
|
-
if most of your models use UUD as primary key"
|
350
|
+
If set, uuid type will be used for hero_id of the Journeys, as well as for the Journey IDs.
|
351
|
+
Use this if most of your models use UUD as primary key"
|
339
352
|
MSG
|
340
353
|
|
341
354
|
# sord omit - no YARD return type given, using untyped
|
@@ -397,18 +410,37 @@ MSG
|
|
397
410
|
sig { params(journey: T.untyped).returns(T.untyped) }
|
398
411
|
def schedule(journey); end
|
399
412
|
|
400
|
-
class RunSchedulingCycleJob <
|
413
|
+
class RunSchedulingCycleJob < StepperMotor::BaseJob
|
401
414
|
# sord omit - no YARD return type given, using untyped
|
402
415
|
sig { returns(T.untyped) }
|
403
416
|
def perform; end
|
404
417
|
end
|
405
418
|
end
|
406
419
|
|
407
|
-
class
|
420
|
+
class HousekeepingJob < StepperMotor::BaseJob
|
421
|
+
# sord omit - no YARD return type given, using untyped
|
422
|
+
sig { returns(T.untyped) }
|
423
|
+
def perform; end
|
424
|
+
end
|
425
|
+
|
426
|
+
class PerformStepJob < StepperMotor::BaseJob
|
427
|
+
# sord omit - no YARD type given for "*posargs", using untyped
|
428
|
+
# sord omit - no YARD type given for "**kwargs", using untyped
|
429
|
+
# sord omit - no YARD return type given, using untyped
|
430
|
+
sig { params(posargs: T.untyped, kwargs: T.untyped).returns(T.untyped) }
|
431
|
+
def perform(*posargs, **kwargs); end
|
432
|
+
|
408
433
|
# sord omit - no YARD type given for "journey_gid", using untyped
|
409
434
|
# sord omit - no YARD return type given, using untyped
|
410
435
|
sig { params(journey_gid: T.untyped).returns(T.untyped) }
|
411
|
-
def
|
436
|
+
def perform_via_journey_gid(journey_gid); end
|
437
|
+
|
438
|
+
# sord omit - no YARD type given for "journey_id:", using untyped
|
439
|
+
# sord omit - no YARD type given for "journey_class_name:", using untyped
|
440
|
+
# sord omit - no YARD type given for "idempotency_key:", using untyped
|
441
|
+
# sord omit - no YARD return type given, using untyped
|
442
|
+
sig { params(journey_id: T.untyped, journey_class_name: T.untyped, idempotency_key: T.untyped).returns(T.untyped) }
|
443
|
+
def perform_via_kwargs(journey_id:, journey_class_name:, idempotency_key: nil); end
|
412
444
|
end
|
413
445
|
|
414
446
|
# The forward scheduler enqueues a job for every Journey that
|
@@ -435,20 +467,11 @@ MSG
|
|
435
467
|
def schedule(journey); end
|
436
468
|
end
|
437
469
|
|
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
470
|
# The purpose of this job is to find journeys which have, for whatever reason, remained in the
|
448
471
|
# `performing` state for far longer than the journey is supposed to. At the moment it assumes
|
449
472
|
# any journey that stayed in `performing` for longer than 1 hour has hung. Add this job to your
|
450
473
|
# cron table and perform it regularly.
|
451
|
-
class
|
474
|
+
class RecoverStuckJourneysJob < StepperMotor::BaseJob
|
452
475
|
DEFAULT_STUCK_FOR = T.let(2.days, T.untyped)
|
453
476
|
|
454
477
|
# sord omit - no YARD type given for "stuck_for:", using untyped
|
@@ -456,4 +479,13 @@ MSG
|
|
456
479
|
sig { params(stuck_for: T.untyped).returns(T.untyped) }
|
457
480
|
def perform(stuck_for: DEFAULT_STUCK_FOR); end
|
458
481
|
end
|
482
|
+
|
483
|
+
# The purpose of this job is to find journeys which have completed (finished or canceled) some
|
484
|
+
# time ago and to delete them. The time is configured in the initializer.
|
485
|
+
class DeleteCompletedJourneysJob < StepperMotor::BaseJob
|
486
|
+
# sord omit - no YARD type given for "completed_for:", using untyped
|
487
|
+
# sord omit - no YARD return type given, using untyped
|
488
|
+
sig { params(completed_for: T.untyped).returns(T.untyped) }
|
489
|
+
def perform(completed_for: StepperMotor.delete_completed_journeys_after); end
|
490
|
+
end
|
459
491
|
end
|
data/sig/stepper_motor.rbs
CHANGED
@@ -2,6 +2,12 @@
|
|
2
2
|
# ever progress forward. The building block of StepperMotor is StepperMotor::Journey
|
3
3
|
module StepperMotor
|
4
4
|
VERSION: untyped
|
5
|
+
PerformStepJobV2: untyped
|
6
|
+
RecoverStuckJourneysJobV1: untyped
|
7
|
+
|
8
|
+
# sord omit - no YARD return type given, using untyped
|
9
|
+
# Extends the BaseJob of the library with any additional options
|
10
|
+
def self.extend_base_job: () -> untyped
|
5
11
|
|
6
12
|
class Error < StandardError
|
7
13
|
end
|
@@ -269,6 +275,12 @@ module StepperMotor
|
|
269
275
|
class Railtie < Rails::Railtie
|
270
276
|
end
|
271
277
|
|
278
|
+
# All StepperMotor job classes inherit from this one. It is available for
|
279
|
+
# extension from StepperMotor.extend_base_job_class so that you can set
|
280
|
+
# priority, include and prepend modules and so forth.
|
281
|
+
class BaseJob < ActiveJob::Base
|
282
|
+
end
|
283
|
+
|
272
284
|
module TestHelper
|
273
285
|
# Allows running a given Journey to completion, skipping across the waiting periods.
|
274
286
|
# This is useful to evaluate all side effects of a Journey. The helper will ensure
|
@@ -350,16 +362,32 @@ module StepperMotor
|
|
350
362
|
# sord omit - no YARD return type given, using untyped
|
351
363
|
def schedule: (untyped journey) -> untyped
|
352
364
|
|
353
|
-
class RunSchedulingCycleJob <
|
365
|
+
class RunSchedulingCycleJob < StepperMotor::BaseJob
|
354
366
|
# sord omit - no YARD return type given, using untyped
|
355
367
|
def perform: () -> untyped
|
356
368
|
end
|
357
369
|
end
|
358
370
|
|
359
|
-
class
|
371
|
+
class HousekeepingJob < StepperMotor::BaseJob
|
372
|
+
# sord omit - no YARD return type given, using untyped
|
373
|
+
def perform: () -> untyped
|
374
|
+
end
|
375
|
+
|
376
|
+
class PerformStepJob < StepperMotor::BaseJob
|
377
|
+
# sord omit - no YARD type given for "*posargs", using untyped
|
378
|
+
# sord omit - no YARD type given for "**kwargs", using untyped
|
379
|
+
# sord omit - no YARD return type given, using untyped
|
380
|
+
def perform: (*untyped posargs, **untyped kwargs) -> untyped
|
381
|
+
|
360
382
|
# sord omit - no YARD type given for "journey_gid", using untyped
|
361
383
|
# sord omit - no YARD return type given, using untyped
|
362
|
-
def
|
384
|
+
def perform_via_journey_gid: (untyped journey_gid) -> untyped
|
385
|
+
|
386
|
+
# sord omit - no YARD type given for "journey_id:", using untyped
|
387
|
+
# sord omit - no YARD type given for "journey_class_name:", using untyped
|
388
|
+
# sord omit - no YARD type given for "idempotency_key:", using untyped
|
389
|
+
# sord omit - no YARD return type given, using untyped
|
390
|
+
def perform_via_kwargs: (journey_id: untyped, journey_class_name: untyped, ?idempotency_key: untyped) -> untyped
|
363
391
|
end
|
364
392
|
|
365
393
|
# The forward scheduler enqueues a job for every Journey that
|
@@ -385,23 +413,23 @@ module StepperMotor
|
|
385
413
|
def schedule: (untyped journey) -> untyped
|
386
414
|
end
|
387
415
|
|
388
|
-
class PerformStepJobV2 < ActiveJob::Base
|
389
|
-
# sord omit - no YARD type given for "journey_id:", using untyped
|
390
|
-
# sord omit - no YARD type given for "journey_class_name:", using untyped
|
391
|
-
# sord omit - no YARD type given for "idempotency_key:", using untyped
|
392
|
-
# sord omit - no YARD return type given, using untyped
|
393
|
-
def perform: (journey_id: untyped, journey_class_name: untyped, ?idempotency_key: untyped) -> untyped
|
394
|
-
end
|
395
|
-
|
396
416
|
# The purpose of this job is to find journeys which have, for whatever reason, remained in the
|
397
417
|
# `performing` state for far longer than the journey is supposed to. At the moment it assumes
|
398
418
|
# any journey that stayed in `performing` for longer than 1 hour has hung. Add this job to your
|
399
419
|
# cron table and perform it regularly.
|
400
|
-
class
|
420
|
+
class RecoverStuckJourneysJob < StepperMotor::BaseJob
|
401
421
|
DEFAULT_STUCK_FOR: untyped
|
402
422
|
|
403
423
|
# sord omit - no YARD type given for "stuck_for:", using untyped
|
404
424
|
# sord omit - no YARD return type given, using untyped
|
405
425
|
def perform: (?stuck_for: untyped) -> untyped
|
406
426
|
end
|
427
|
+
|
428
|
+
# The purpose of this job is to find journeys which have completed (finished or canceled) some
|
429
|
+
# time ago and to delete them. The time is configured in the initializer.
|
430
|
+
class DeleteCompletedJourneysJob < StepperMotor::BaseJob
|
431
|
+
# sord omit - no YARD type given for "completed_for:", using untyped
|
432
|
+
# sord omit - no YARD return type given, using untyped
|
433
|
+
def perform: (?completed_for: untyped) -> untyped
|
434
|
+
end
|
407
435
|
end
|
data/stepper_motor.gemspec
CHANGED
@@ -38,7 +38,7 @@ Gem::Specification.new do |spec|
|
|
38
38
|
spec.add_development_dependency "rails", "~> 7.0"
|
39
39
|
spec.add_development_dependency "sqlite3"
|
40
40
|
spec.add_development_dependency "rake", "~> 13.0"
|
41
|
-
spec.add_development_dependency "standard"
|
41
|
+
spec.add_development_dependency "standard", "~> 1.50.0", "< 2.0"
|
42
42
|
spec.add_development_dependency "magic_frozen_string_literal"
|
43
43
|
spec.add_development_dependency "yard"
|
44
44
|
spec.add_development_dependency "redcarpet" # needed for the yard gem to enable Github Flavored Markdown
|
@@ -1 +1,40 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
# Sets the scheduler. The ForwardScheduler will enqueue jobs for performing steps
|
4
|
+
# regardless of how far in the future a step needs to be taken. The CyclicScheduler
|
5
|
+
# will only enqueue jobs for steps which are to be performed soon. If you want to use
|
6
|
+
# the CyclicScheduler, you will need to configure it for the proper interval duration:
|
7
|
+
#
|
8
|
+
# StepperMotor.scheduler = StepperMotor::CyclicScheduler.new(cycle_duration: 30.minutes)
|
9
|
+
#
|
10
|
+
# and add its cycle job into your recurring jobs table. For example, for solid_queue:
|
11
|
+
#
|
12
|
+
# stepper_motor_houseleeping:
|
13
|
+
# schedule: "*/30 * * * *" # Every 30 minutes
|
14
|
+
# class: "StepperMotor::CyclicScheduler::RunSchedulingCycleJob"
|
15
|
+
#
|
16
|
+
# The cadence of the cyclic scheduler and the cadence of your cron job should be equal.
|
17
|
+
#
|
18
|
+
# If your queue is not susceptible to performance degradation with large numbers of
|
19
|
+
# "far future" jobs and allows scheduling "far ahead" - you can use the `ForwardScheduler`
|
20
|
+
# which is the default.
|
1
21
|
StepperMotor.scheduler = StepperMotor::ForwardScheduler.new
|
22
|
+
|
23
|
+
# Sets the amount of time after which completed (finished and canceled)
|
24
|
+
# Journeys are going to be deleted from the database. If you want to keep
|
25
|
+
# them in the database indefinitely, set this parameter to `nil`.
|
26
|
+
# To perform the actual cleanups, add the `StepperMotor::HousekeepingJob` to your
|
27
|
+
# recurring jobs table. For example, for solid_queue:
|
28
|
+
#
|
29
|
+
# stepper_motor_housekeeping:
|
30
|
+
# schedule: "*/30 * * * *" # Every 30 minutes
|
31
|
+
# class: "StepperMotor::HousekeepingJob"
|
32
|
+
StepperMotor.delete_completed_journeys_after = 30.days
|
33
|
+
|
34
|
+
# Extends the base StepperMotor ActiveJob with any calls you would use to customise a
|
35
|
+
# job in your codebase. At the minimum, we recommend setting all StepperMotor job priorities
|
36
|
+
# to "high" - according to the priority denomination you are using.
|
37
|
+
# StepperMotor.extend_base_job do
|
38
|
+
# priority :high
|
39
|
+
# discard_on ActiveRecord::NotFound
|
40
|
+
# end
|
@@ -7,7 +7,7 @@ class StepperMotorMigration004 < ActiveRecord::Migration[7.2]
|
|
7
7
|
name: :idx_journeys_one_per_hero_with_paused
|
8
8
|
|
9
9
|
# Remove old indexes that only include 'ready' state
|
10
|
-
remove_index :stepper_motor_journeys, [:type, :hero_id, :hero_type], name: :one_per_hero_index, where: "allow_multiple = '0' AND state IN ('ready', 'performing')"
|
10
|
+
remove_index :stepper_motor_journeys, [:type, :hero_id, :hero_type], name: :one_per_hero_index, where: "allow_multiple = '0' AND state IN ('ready', 'performing')"
|
11
11
|
end
|
12
12
|
|
13
13
|
def down
|
@@ -19,6 +19,6 @@ class StepperMotorMigration004 < ActiveRecord::Migration[7.2]
|
|
19
19
|
name: :one_per_hero_index
|
20
20
|
|
21
21
|
# Remove new indexes
|
22
|
-
remove_index :stepper_motor_journeys, name: :idx_journeys_one_per_hero_with_paused
|
22
|
+
remove_index :stepper_motor_journeys, name: :idx_journeys_one_per_hero_with_paused
|
23
23
|
end
|
24
24
|
end
|
data/test/dummy/db/schema.rb
CHANGED
@@ -10,7 +10,7 @@
|
|
10
10
|
#
|
11
11
|
# It's strongly recommended that you check this file into your version control system.
|
12
12
|
|
13
|
-
ActiveRecord::Schema[7.2].define(version:
|
13
|
+
ActiveRecord::Schema[7.2].define(version: 2025_05_28_141038) do
|
14
14
|
create_table "stepper_motor_journeys", force: :cascade do |t|
|
15
15
|
t.string "type", null: false
|
16
16
|
t.string "state", default: "ready"
|
@@ -0,0 +1,96 @@
|
|
1
|
+
require "test_helper"
|
2
|
+
|
3
|
+
class CompletedJourneysCleanupTest < ActiveSupport::TestCase
|
4
|
+
include SideEffects::TestHelper
|
5
|
+
|
6
|
+
test "defines a variable on StepperMotor and sets it to a default value" do
|
7
|
+
assert StepperMotor.delete_completed_journeys_after
|
8
|
+
assert_equal 30.days, StepperMotor.delete_completed_journeys_after
|
9
|
+
end
|
10
|
+
|
11
|
+
test "cleans up finished journeys" do
|
12
|
+
previous_setting = StepperMotor.delete_completed_journeys_after
|
13
|
+
|
14
|
+
journey_class = create_journey_subclass do
|
15
|
+
step wait: 20.minutes do
|
16
|
+
SideEffects.touch! :a_step
|
17
|
+
end
|
18
|
+
end
|
19
|
+
journey = journey_class.create!
|
20
|
+
another_journey = journey_class.create!
|
21
|
+
|
22
|
+
assert_no_changes "StepperMotor::Journey.count" do
|
23
|
+
StepperMotor::DeleteCompletedJourneysJob.new.perform
|
24
|
+
end
|
25
|
+
|
26
|
+
travel_to Time.current + 20.minutes + 1.second
|
27
|
+
journey.perform_next_step!
|
28
|
+
|
29
|
+
assert SideEffects.produced?(:a_step)
|
30
|
+
assert journey.finished?
|
31
|
+
|
32
|
+
assert_no_changes "StepperMotor::Journey.count" do
|
33
|
+
StepperMotor::DeleteCompletedJourneysJob.new.perform
|
34
|
+
end
|
35
|
+
|
36
|
+
StepperMotor.delete_completed_journeys_after = 7.minutes
|
37
|
+
travel_to Time.current + 7.minutes + 1.second
|
38
|
+
|
39
|
+
assert_changes "StepperMotor::Journey.count", -1 do
|
40
|
+
StepperMotor::DeleteCompletedJourneysJob.new.perform
|
41
|
+
end
|
42
|
+
|
43
|
+
assert_raises(ActiveRecord::RecordNotFound) { journey.reload }
|
44
|
+
assert_nothing_raised { another_journey.reload }
|
45
|
+
ensure
|
46
|
+
StepperMotor.delete_completed_journeys_after = previous_setting
|
47
|
+
end
|
48
|
+
|
49
|
+
test "cleans up canceled journeys" do
|
50
|
+
previous_setting = StepperMotor.delete_completed_journeys_after
|
51
|
+
|
52
|
+
journey_class = create_journey_subclass do
|
53
|
+
step wait: 20.minutes do
|
54
|
+
# noop
|
55
|
+
end
|
56
|
+
end
|
57
|
+
journey = journey_class.create!
|
58
|
+
journey.cancel!
|
59
|
+
|
60
|
+
assert_no_changes "StepperMotor::Journey.count" do
|
61
|
+
StepperMotor::DeleteCompletedJourneysJob.new.perform
|
62
|
+
end
|
63
|
+
|
64
|
+
StepperMotor.delete_completed_journeys_after = 7.minutes
|
65
|
+
travel_to Time.current + 7.minutes + 1.second
|
66
|
+
|
67
|
+
assert_changes "StepperMotor::Journey.count", -1 do
|
68
|
+
StepperMotor::DeleteCompletedJourneysJob.new.perform
|
69
|
+
end
|
70
|
+
|
71
|
+
assert_raises(ActiveRecord::RecordNotFound) { journey.reload }
|
72
|
+
ensure
|
73
|
+
StepperMotor.delete_completed_journeys_after = previous_setting
|
74
|
+
end
|
75
|
+
|
76
|
+
test "does not delete any journeys if the setting is set to nil" do
|
77
|
+
previous_setting = StepperMotor.delete_completed_journeys_after
|
78
|
+
StepperMotor.delete_completed_journeys_after = nil
|
79
|
+
|
80
|
+
journey_class = create_journey_subclass do
|
81
|
+
step wait: 20.minutes do
|
82
|
+
# noop
|
83
|
+
end
|
84
|
+
end
|
85
|
+
journey = journey_class.create!
|
86
|
+
journey.cancel!
|
87
|
+
|
88
|
+
travel_to Time.current + 365.days
|
89
|
+
assert_no_changes "StepperMotor::Journey.count" do
|
90
|
+
StepperMotor::DeleteCompletedJourneysJob.new.perform
|
91
|
+
end
|
92
|
+
assert_nothing_raised { journey.reload }
|
93
|
+
ensure
|
94
|
+
StepperMotor.delete_completed_journeys_after = previous_setting
|
95
|
+
end
|
96
|
+
end
|
@@ -39,7 +39,7 @@ class CyclicSchedulerTest < ActiveSupport::TestCase
|
|
39
39
|
scheduler = StepperMotor::CyclicScheduler.new(cycle_duration: 40.minutes)
|
40
40
|
StepperMotor.scheduler = scheduler
|
41
41
|
|
42
|
-
assert_enqueued_jobs 1, only: StepperMotor::
|
42
|
+
assert_enqueued_jobs 1, only: StepperMotor::PerformStepJob do
|
43
43
|
far_future_journey_class.create!
|
44
44
|
end
|
45
45
|
end
|
@@ -54,7 +54,7 @@ class CyclicSchedulerTest < ActiveSupport::TestCase
|
|
54
54
|
end
|
55
55
|
journey.update!(next_step_to_be_performed_at: 10.minutes.ago)
|
56
56
|
|
57
|
-
assert_enqueued_jobs 1, only: StepperMotor::
|
57
|
+
assert_enqueued_jobs 1, only: StepperMotor::PerformStepJob do
|
58
58
|
scheduler.run_scheduling_cycle
|
59
59
|
end
|
60
60
|
end
|
@@ -31,7 +31,7 @@ class ForwardSchedulerTest < ActiveSupport::TestCase
|
|
31
31
|
assert_equal 1, enqueued_jobs.size
|
32
32
|
job = enqueued_jobs.first
|
33
33
|
|
34
|
-
assert_equal "StepperMotor::
|
34
|
+
assert_equal "StepperMotor::PerformStepJob", job["job_class"]
|
35
35
|
assert_not_nil job["scheduled_at"]
|
36
36
|
|
37
37
|
scheduled_at = Time.parse(job["scheduled_at"])
|
@@ -0,0 +1,13 @@
|
|
1
|
+
require "test_helper"
|
2
|
+
|
3
|
+
class HousekeepingJobTest < ActiveSupport::TestCase
|
4
|
+
include ActiveJob::TestHelper
|
5
|
+
|
6
|
+
test "runs without exceptions and enqueues the two actual jobs" do
|
7
|
+
assert_nothing_raised do
|
8
|
+
StepperMotor::HousekeepingJob.perform_now
|
9
|
+
end
|
10
|
+
|
11
|
+
assert_enqueued_jobs 2
|
12
|
+
end
|
13
|
+
end
|
@@ -0,0 +1,60 @@
|
|
1
|
+
require "test_helper"
|
2
|
+
|
3
|
+
class PerformStepJobTest < ActiveSupport::TestCase
|
4
|
+
include SideEffects::TestHelper
|
5
|
+
|
6
|
+
test "exposes the V2 variant in a constant to allow old jobs to be unserialized" do
|
7
|
+
assert defined?(StepperMotor::PerformStepJobV2)
|
8
|
+
assert_equal StepperMotor::PerformStepJob, StepperMotor::PerformStepJobV2
|
9
|
+
end
|
10
|
+
|
11
|
+
test "allows perform() with a GlobalID as argument" do
|
12
|
+
journey = create_journey_subclass do
|
13
|
+
step do
|
14
|
+
# noop
|
15
|
+
end
|
16
|
+
end.create!
|
17
|
+
|
18
|
+
assert_nothing_raised { StepperMotor::PerformStepJob.new.perform(journey.to_global_id) }
|
19
|
+
assert journey.reload.finished?
|
20
|
+
end
|
21
|
+
|
22
|
+
test "allows perform() with the journey class name and ID" do
|
23
|
+
journey = create_journey_subclass do
|
24
|
+
step do
|
25
|
+
# noop
|
26
|
+
end
|
27
|
+
end.create!
|
28
|
+
|
29
|
+
assert_nothing_raised do
|
30
|
+
StepperMotor::PerformStepJob.new.perform(journey_id: journey.id, journey_class_name: journey.class.name)
|
31
|
+
end
|
32
|
+
assert journey.reload.finished?
|
33
|
+
end
|
34
|
+
|
35
|
+
test "allows perform() with the journey class name, ID and idempotency key" do
|
36
|
+
journey = create_journey_subclass do
|
37
|
+
step do
|
38
|
+
# noop
|
39
|
+
end
|
40
|
+
end.create!
|
41
|
+
|
42
|
+
assert_nothing_raised do
|
43
|
+
StepperMotor::PerformStepJob.new.perform(journey_id: journey.id, journey_class_name: journey.class.name, idempotency_key: journey.idempotency_key)
|
44
|
+
end
|
45
|
+
assert journey.reload.finished?
|
46
|
+
end
|
47
|
+
|
48
|
+
test "skips without exceptions if the idempotency key is incorrect" do
|
49
|
+
journey = create_journey_subclass do
|
50
|
+
step do
|
51
|
+
# noop
|
52
|
+
end
|
53
|
+
end.create!
|
54
|
+
|
55
|
+
assert_nothing_raised do
|
56
|
+
StepperMotor::PerformStepJob.new.perform(journey_id: journey.id, journey_class_name: journey.class.name, idempotency_key: "wrong")
|
57
|
+
end
|
58
|
+
assert journey.reload.ready?
|
59
|
+
end
|
60
|
+
end
|
@@ -7,6 +7,11 @@ class RecoverStuckJourneysJobTest < ActiveSupport::TestCase
|
|
7
7
|
StepperMotor::Journey.delete_all
|
8
8
|
end
|
9
9
|
|
10
|
+
test "still has the previous job class name available to allow older jobs to be unserialized" do
|
11
|
+
assert defined?(StepperMotor::RecoverStuckJourneysJobV1)
|
12
|
+
assert_equal StepperMotor::RecoverStuckJourneysJob, StepperMotor::RecoverStuckJourneysJobV1
|
13
|
+
end
|
14
|
+
|
10
15
|
test "handles recovery from a background job" do
|
11
16
|
stuck_journey_class1 = create_journey_subclass do
|
12
17
|
self.when_stuck = :cancel
|
@@ -52,12 +57,12 @@ class RecoverStuckJourneysJobTest < ActiveSupport::TestCase
|
|
52
57
|
assert journey_to_cancel.reload.performing?
|
53
58
|
assert journey_to_reattempt.reload.performing?
|
54
59
|
|
55
|
-
StepperMotor::
|
60
|
+
StepperMotor::RecoverStuckJourneysJob.perform_now(stuck_for: 2.days)
|
56
61
|
assert journey_to_cancel.reload.performing?
|
57
62
|
assert journey_to_reattempt.reload.performing?
|
58
63
|
|
59
64
|
travel_to Time.now + 2.days + 1.second
|
60
|
-
StepperMotor::
|
65
|
+
StepperMotor::RecoverStuckJourneysJob.perform_now(stuck_for: 2.days)
|
61
66
|
|
62
67
|
assert journey_to_cancel.reload.canceled?
|
63
68
|
assert journey_to_reattempt.reload.ready?
|
@@ -83,7 +88,7 @@ class RecoverStuckJourneysJobTest < ActiveSupport::TestCase
|
|
83
88
|
journey_to_cancel.class.update_all(type: "UnknownJourneySubclass")
|
84
89
|
|
85
90
|
assert_nothing_raised do
|
86
|
-
StepperMotor::
|
91
|
+
StepperMotor::RecoverStuckJourneysJob.perform_now(stuck_for: 2.days)
|
87
92
|
end
|
88
93
|
end
|
89
94
|
end
|
metadata
CHANGED
@@ -1,13 +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.11
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Julik Tarkhanov
|
8
|
+
autorequire:
|
8
9
|
bindir: exe
|
9
10
|
cert_chain: []
|
10
|
-
date: 2025-
|
11
|
+
date: 2025-06-08 00:00:00.000000000 Z
|
11
12
|
dependencies:
|
12
13
|
- !ruby/object:Gem::Dependency
|
13
14
|
name: activerecord
|
@@ -111,16 +112,22 @@ dependencies:
|
|
111
112
|
name: standard
|
112
113
|
requirement: !ruby/object:Gem::Requirement
|
113
114
|
requirements:
|
114
|
-
- - "
|
115
|
+
- - "~>"
|
115
116
|
- !ruby/object:Gem::Version
|
116
|
-
version:
|
117
|
+
version: 1.50.0
|
118
|
+
- - "<"
|
119
|
+
- !ruby/object:Gem::Version
|
120
|
+
version: '2.0'
|
117
121
|
type: :development
|
118
122
|
prerelease: false
|
119
123
|
version_requirements: !ruby/object:Gem::Requirement
|
120
124
|
requirements:
|
121
|
-
- - "
|
125
|
+
- - "~>"
|
122
126
|
- !ruby/object:Gem::Version
|
123
|
-
version:
|
127
|
+
version: 1.50.0
|
128
|
+
- - "<"
|
129
|
+
- !ruby/object:Gem::Version
|
130
|
+
version: '2.0'
|
124
131
|
- !ruby/object:Gem::Dependency
|
125
132
|
name: magic_frozen_string_literal
|
126
133
|
requirement: !ruby/object:Gem::Requirement
|
@@ -200,15 +207,17 @@ files:
|
|
200
207
|
- lib/generators/stepper_motor_migration_003.rb.erb
|
201
208
|
- lib/generators/stepper_motor_migration_004.rb.erb
|
202
209
|
- lib/stepper_motor.rb
|
210
|
+
- lib/stepper_motor/base_job.rb
|
203
211
|
- lib/stepper_motor/cyclic_scheduler.rb
|
212
|
+
- lib/stepper_motor/delete_completed_journeys_job.rb
|
204
213
|
- lib/stepper_motor/forward_scheduler.rb
|
214
|
+
- lib/stepper_motor/housekeeping_job.rb
|
205
215
|
- lib/stepper_motor/journey.rb
|
206
216
|
- lib/stepper_motor/journey/flow_control.rb
|
207
217
|
- lib/stepper_motor/journey/recovery.rb
|
208
218
|
- lib/stepper_motor/perform_step_job.rb
|
209
|
-
- lib/stepper_motor/perform_step_job_v2.rb
|
210
219
|
- lib/stepper_motor/railtie.rb
|
211
|
-
- lib/stepper_motor/
|
220
|
+
- lib/stepper_motor/recover_stuck_journeys_job.rb
|
212
221
|
- lib/stepper_motor/step.rb
|
213
222
|
- lib/stepper_motor/test_helper.rb
|
214
223
|
- lib/stepper_motor/version.rb
|
@@ -253,7 +262,7 @@ files:
|
|
253
262
|
- test/dummy/db/migrate/20250525132523_stepper_motor_migration_001.rb
|
254
263
|
- test/dummy/db/migrate/20250525132524_stepper_motor_migration_002.rb
|
255
264
|
- test/dummy/db/migrate/20250525132525_stepper_motor_migration_003.rb
|
256
|
-
- test/dummy/db/migrate/
|
265
|
+
- test/dummy/db/migrate/20250528141038_stepper_motor_migration_004.rb
|
257
266
|
- test/dummy/db/schema.rb
|
258
267
|
- test/dummy/public/400.html
|
259
268
|
- test/dummy/public/404.html
|
@@ -263,14 +272,18 @@ files:
|
|
263
272
|
- test/dummy/public/icon.png
|
264
273
|
- test/dummy/public/icon.svg
|
265
274
|
- test/side_effects_helper.rb
|
275
|
+
- test/stepper_motor/completed_journeys_cleanup_test.rb
|
276
|
+
- test/stepper_motor/configuration_test.rb
|
266
277
|
- test/stepper_motor/cyclic_scheduler_test.rb
|
267
278
|
- test/stepper_motor/forward_scheduler_test.rb
|
279
|
+
- test/stepper_motor/housekeeping_job_test.rb
|
268
280
|
- test/stepper_motor/journey/exception_handling_test.rb
|
269
281
|
- test/stepper_motor/journey/flow_control_test.rb
|
270
282
|
- test/stepper_motor/journey/idempotency_test.rb
|
271
283
|
- test/stepper_motor/journey/step_definition_test.rb
|
272
284
|
- test/stepper_motor/journey/uniqueness_test.rb
|
273
285
|
- test/stepper_motor/journey_test.rb
|
286
|
+
- test/stepper_motor/perform_step_job_test.rb
|
274
287
|
- test/stepper_motor/recover_stuck_journeys_job_test.rb
|
275
288
|
- test/stepper_motor/recovery_test.rb
|
276
289
|
- test/stepper_motor/test_helper_test.rb
|
@@ -284,6 +297,7 @@ metadata:
|
|
284
297
|
homepage_uri: https://steppermotor.dev
|
285
298
|
source_code_uri: https://github.com/stepper-motor/stepper_motor
|
286
299
|
changelog_uri: https://github.com/stepper-motor/stepper_motor/blob/main/CHANGELOG.md
|
300
|
+
post_install_message:
|
287
301
|
rdoc_options: []
|
288
302
|
require_paths:
|
289
303
|
- lib
|
@@ -298,7 +312,8 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
298
312
|
- !ruby/object:Gem::Version
|
299
313
|
version: '0'
|
300
314
|
requirements: []
|
301
|
-
rubygems_version: 3.
|
315
|
+
rubygems_version: 3.4.10
|
316
|
+
signing_key:
|
302
317
|
specification_version: 4
|
303
318
|
summary: Effortless step workflows that embed nicely inside Rails
|
304
319
|
test_files: []
|
@@ -1,12 +0,0 @@
|
|
1
|
-
# frozen_string_literal: true
|
2
|
-
|
3
|
-
require "active_job"
|
4
|
-
|
5
|
-
class StepperMotor::PerformStepJobV2 < ActiveJob::Base
|
6
|
-
def perform(journey_id:, journey_class_name:, idempotency_key: nil, **)
|
7
|
-
journey = StepperMotor::Journey.find(journey_id)
|
8
|
-
journey.perform_next_step!(idempotency_key: idempotency_key)
|
9
|
-
rescue ActiveRecord::RecordNotFound
|
10
|
-
# The journey has been canceled and destroyed previously or elsewhere
|
11
|
-
end
|
12
|
-
end
|