active_durable 0.5.0 → 0.6.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (41) hide show
  1. checksums.yaml +4 -4
  2. data/.yardopts +8 -0
  3. data/CHANGELOG.md +52 -2
  4. data/README.md +97 -17
  5. data/app/controllers/active_durable/executions_controller.rb +2 -1
  6. data/app/helpers/active_durable/dashboard_helper.rb +1 -1
  7. data/app/views/active_durable/executions/index.html.erb +3 -2
  8. data/app/views/active_durable/executions/show.html.erb +10 -0
  9. data/app/views/layouts/active_durable/application.html.erb +4 -2
  10. data/lib/active_durable/configuration.rb +4 -0
  11. data/lib/active_durable/engine.rb +2 -4
  12. data/lib/active_durable/errors.rb +30 -0
  13. data/lib/active_durable/execution.rb +9 -2
  14. data/lib/active_durable/flow.rb +126 -15
  15. data/lib/active_durable/flow_parallel.rb +17 -0
  16. data/lib/active_durable/lease.rb +2 -0
  17. data/lib/active_durable/notebook.rb +4 -2
  18. data/lib/active_durable/open_telemetry.rb +14 -2
  19. data/lib/active_durable/operations.rb +5 -2
  20. data/lib/active_durable/parallel.rb +15 -0
  21. data/lib/active_durable/prune_job.rb +16 -0
  22. data/lib/active_durable/pruner.rb +34 -0
  23. data/lib/active_durable/record.rb +2 -0
  24. data/lib/active_durable/registry.rb +4 -0
  25. data/lib/active_durable/retry_policy.rb +2 -0
  26. data/lib/active_durable/run_job.rb +3 -0
  27. data/lib/active_durable/runner.rb +34 -2
  28. data/lib/active_durable/serializer.rb +2 -0
  29. data/lib/active_durable/signal_record.rb +2 -0
  30. data/lib/active_durable/step.rb +10 -0
  31. data/lib/active_durable/sweep_job.rb +3 -0
  32. data/lib/active_durable/sweeper.rb +26 -6
  33. data/lib/active_durable/testing.rb +8 -0
  34. data/lib/active_durable/version.rb +2 -1
  35. data/lib/active_durable.rb +115 -11
  36. data/lib/generators/active_durable/install/install_generator.rb +2 -0
  37. data/lib/generators/active_durable/install/templates/create_active_durable_tables.rb.tt +1 -0
  38. data/lib/generators/active_durable/upgrade/templates/add_active_durable_prune_index.rb.tt +14 -0
  39. data/lib/generators/active_durable/upgrade/upgrade_generator.rb +39 -0
  40. data/lib/tasks/active_durable.rake +13 -2
  41. metadata +30 -11
@@ -4,19 +4,39 @@ module ActiveDurable
4
4
  # The safety net. Finds executions that should be running but have no job: the process died between
5
5
  # the commit and the enqueue, a timer job was lost, or a worker crashed and its lease expired.
6
6
  # Enqueuing twice is harmless: only one worker can claim the lease.
7
+ #
8
+ # @api private
7
9
  module Sweeper
8
10
  module_function
9
11
 
10
12
  def call(now: ActiveDurable.now)
13
+ ids = due(now: now)
14
+ ids.each { |id| ActiveDurable.enqueue(id) }
15
+ ids
16
+ end
17
+
18
+ # The rake task. With the :async adapter (the Rails default in development) a job lives in the memory of the
19
+ # process that enqueued it, and this process ends as soon as the task does: run the executions here instead.
20
+ def call_from_task(now: ActiveDurable.now)
21
+ return [call(now: now), :enqueued] unless in_process_adapter?
22
+
23
+ ids = due(now: now)
24
+ ids.each { |id| RunJob.perform_now(id) }
25
+ [ids, :ran]
26
+ end
27
+
28
+ def in_process_adapter?
29
+ RunJob.queue_adapter.is_a?(ActiveJob::QueueAdapters::AsyncAdapter)
30
+ end
31
+
32
+ def due(now: ActiveDurable.now)
11
33
  quiet = Execution.active
12
34
  .where("locked_until IS NULL OR locked_until < ?", now)
13
35
  .where("updated_at < ?", now - ActiveDurable.config.sweep_grace.to_f)
14
- due = quiet.where(status: %w[pending running])
15
- .or(quiet.where(wake_at: ..now))
16
- .or(quiet.where(status: "waiting", id: SignalRecord.pending.select(:execution_id)))
17
- ids = due.pluck(:id)
18
- ids.each { |id| ActiveDurable.enqueue(id) }
19
- ids
36
+ ready = quiet.where(status: %w[pending running])
37
+ .or(quiet.where(wake_at: ..now))
38
+ .or(quiet.where(status: "waiting", id: SignalRecord.pending.select(:execution_id)))
39
+ ready.pluck(:id)
20
40
  end
21
41
  end
22
42
  end
@@ -57,6 +57,7 @@ module ActiveDurable
57
57
  end.unshift(discovery)
58
58
  end
59
59
 
60
+ # @api private
60
61
  def crash_at(recipe, input, hit_number, signals)
61
62
  id = start_quietly(recipe, input)
62
63
  hits = 0
@@ -72,6 +73,9 @@ module ActiveDurable
72
73
  drain(id, signals: signals)
73
74
  end
74
75
 
76
+ # Starts an execution without enqueuing its job, so a test drives it with {drain}.
77
+ #
78
+ # @return [String] the execution id
75
79
  def start_quietly(recipe, input)
76
80
  previous = ActiveDurable.enqueue_disabled
77
81
  ActiveDurable.enqueue_disabled = true
@@ -80,6 +84,7 @@ module ActiveDurable
80
84
  ActiveDurable.enqueue_disabled = previous
81
85
  end
82
86
 
87
+ # @api private
83
88
  def with_crash_hook(hook)
84
89
  previous = ActiveDurable.crash_hook
85
90
  ActiveDurable.crash_hook = hook
@@ -89,6 +94,8 @@ module ActiveDurable
89
94
  end
90
95
 
91
96
  # Decides what has to happen before the next run. Returns false when nothing can move the execution.
97
+ #
98
+ # @api private
92
99
  def ready_for_next_run?(execution, signals)
93
100
  now = ActiveDurable.now
94
101
  if execution.locked_until && execution.locked_until > now
@@ -101,6 +108,7 @@ module ActiveDurable
101
108
  true
102
109
  end
103
110
 
111
+ # @api private
104
112
  def waiting_can_move?(execution, signals, now)
105
113
  return true if SignalRecord.pending.exists?(execution_id: execution.id)
106
114
 
@@ -1,5 +1,6 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module ActiveDurable
4
- VERSION = "0.5.0"
4
+ # The gem's version.
5
+ VERSION = "0.6.0"
5
6
  end
@@ -22,9 +22,27 @@ require_relative "active_durable/parallel"
22
22
  require_relative "active_durable/flow_parallel"
23
23
  require_relative "active_durable/runner"
24
24
  require_relative "active_durable/sweeper"
25
+ require_relative "active_durable/pruner"
25
26
  require_relative "active_durable/operations"
26
27
 
27
- # Durable sagas for Rails. See README.md.
28
+ # Durable sagas for Rails: finish the work or undo it in order, even if the server dies.
29
+ #
30
+ # Define a recipe with {define}, start it with {start}, and operate it with {retry}, {compensate}, {rerun} and
31
+ # {prune}. Inside a recipe, every call goes through a {Flow}. `Durable` is a short alias of this module.
32
+ #
33
+ # @example
34
+ # CheckoutSaga = Durable.define(:checkout) do |flow, order_id:|
35
+ # order = Order.find(order_id)
36
+ # flow.transaction(:reserve_stock, undo: -> { order.release_stock! }) { order.reserve_stock! }
37
+ # flow.step(:charge, undo: ->(charge, ticket) { Payments.refund(charge, ticket) }) do |ticket|
38
+ # Payments.charge(order, ticket)
39
+ # end
40
+ # end
41
+ #
42
+ # Order.transaction do
43
+ # order = Order.create!(order_params)
44
+ # Durable.start(:checkout, id: "checkout-#{order.id}", order_id: order.id)
45
+ # end
28
46
  module ActiveDurable
29
47
  # Loaded on first use: defining them at require time would load ActiveRecord::Base and
30
48
  # ActiveJob::Base before a Rails app has applied its configuration.
@@ -34,41 +52,77 @@ module ActiveDurable
34
52
  autoload :SignalRecord, File.expand_path("active_durable/signal_record", __dir__)
35
53
  autoload :RunJob, File.expand_path("active_durable/run_job", __dir__)
36
54
  autoload :SweepJob, File.expand_path("active_durable/sweep_job", __dir__)
55
+ autoload :PruneJob, File.expand_path("active_durable/prune_job", __dir__)
37
56
 
38
57
  class << self
58
+ # @api private
39
59
  attr_writer :time_offset
60
+ # @api private
40
61
  attr_accessor :crash_hook, :enqueue_disabled
41
62
 
63
+ # The settings of the whole gem.
64
+ #
65
+ # @return [Configuration]
42
66
  def config
43
67
  @config ||= Configuration.new
44
68
  end
45
69
 
70
+ # Changes the settings, usually in config/initializers/active_durable.rb.
71
+ #
72
+ # @yieldparam config [Configuration]
73
+ # @return [void]
74
+ # @example
75
+ # ActiveDurable.configure do |config|
76
+ # config.queue_name = :sagas
77
+ # config.lease_duration = 10.minutes
78
+ # end
46
79
  def configure
47
80
  yield config
48
81
  end
49
82
 
83
+ # @api private
50
84
  def registry
51
85
  @registry ||= Registry.new
52
86
  end
53
87
 
54
- # Defines a recipe. Assign the result to a constant (CheckoutSaga = Durable.define(:checkout) { ... })
55
- # so Rails can autoload it in any process.
88
+ # Defines a recipe: the steps of a saga. Assign the result to a constant (CheckoutSaga = Durable.define(:checkout)
89
+ # { ... }) in app/sagas/checkout_saga.rb, so Rails can autoload it in any process.
56
90
  #
57
- # To change a recipe while executions are in flight, keep the old block and add a new version:
58
- # new executions use the highest version, and every execution keeps running the version it started with.
91
+ # To change a recipe while executions are in flight, keep the old block and add a new version: new executions
92
+ # use the highest version, and every execution keeps running the version it started with.
93
+ #
94
+ # @param name [Symbol, String] the recipe name, used by {start}
95
+ # @param version [Integer] the recipe version
96
+ # @yieldparam flow [Flow] the object every step goes through
97
+ # @yieldparam input [Hash] the keyword arguments given to {start}, with string keys turned into keywords
98
+ # @return [Recipe] assign it to a constant
99
+ # @raise [ArgumentError] without a block or a name
59
100
  def define(name, version: 1, &)
60
101
  registry.define(name, version: version, &)
61
102
  end
62
103
 
63
- # { ["checkout", 1] => 12, ["checkout", 2] => 340 }: executions that are not finished yet (active or
64
- # blocked) per recipe version. A version can be deleted once it no longer appears here.
104
+ # Executions that are not finished yet (active or blocked) per recipe version. A version can be deleted once it
105
+ # no longer appears here. `bin/rails active_durable:versions` prints it.
106
+ #
107
+ # @return [Hash{Array(String, Integer) => Integer}] for example { ["checkout", 1] => 12, ["checkout", 2] => 340 }
65
108
  def versions_in_use
66
109
  Execution.where(status: Execution::ACTIVE + %w[blocked]).group(:recipe, :recipe_version).count
67
110
  .transform_keys { |(recipe, version)| [recipe, version.to_i] }
68
111
  end
69
112
 
70
- # Starts a saga. Call it inside the transaction that creates your record: the saga row is committed
71
- # with it, and the job is enqueued only after the commit. Pass id: to make the call idempotent.
113
+ # Starts a saga. Call it inside the transaction that creates your record: the saga row is committed with it,
114
+ # and its job is enqueued only after the commit, so a crash in between cannot lose it.
115
+ #
116
+ # @param name [Symbol, String] a recipe given to {define}
117
+ # @param id [String, nil] the execution id. With one, the call is idempotent: starting the same id twice returns
118
+ # the first execution. Without one, a random id is used.
119
+ # @param input [Hash] keyword arguments for the recipe; stored as JSON
120
+ # @return [Execution]
121
+ # @raise [UnknownRecipe] if no recipe has that name
122
+ # @raise [ArgumentError] if the id already belongs to an execution of another recipe
123
+ # @raise [NotSerializable] if the input cannot be stored as JSON
124
+ # @example
125
+ # Durable.start(:checkout, id: "checkout-#{order.id}", order_id: order.id)
72
126
  def start(name, id: nil, **input)
73
127
  recipe = registry.latest(name)
74
128
  attributes = { recipe: recipe.name, recipe_version: recipe.version, status: "pending",
@@ -81,7 +135,17 @@ module ActiveDurable
81
135
  raise ArgumentError, "execution #{id} already exists for recipe :#{execution.recipe}"
82
136
  end
83
137
 
84
- # Delivers a signal to a saga waiting (now or later) in flow.wait_for(name).
138
+ # Delivers a signal to a saga waiting, now or later, in flow.wait_for(name). A signal that arrives before the
139
+ # saga waits is kept until it does.
140
+ #
141
+ # @param execution_id [String]
142
+ # @param name [Symbol, String] the name given to {Flow#wait_for}
143
+ # @param payload [Object] what wait_for returns; stored as JSON
144
+ # @return [void]
145
+ # @raise [ActiveRecord::RecordNotFound] if there is no such execution
146
+ # @raise [Error] if the execution already finished
147
+ # @example In a webhook controller
148
+ # Durable.signal("loan-42", :kyc_done, verified: true)
85
149
  def signal(execution_id, name, payload = nil)
86
150
  execution = Execution.find(execution_id)
87
151
  raise Error, "execution #{execution_id} already finished (#{execution.status})" if execution.terminal?
@@ -90,37 +154,74 @@ module ActiveDurable
90
154
  payload: Serializer.normalize(payload, "signal payload"))
91
155
  end
92
156
 
157
+ # @param execution_id [String]
158
+ # @return [Execution]
159
+ # @raise [ActiveRecord::RecordNotFound]
93
160
  def find(execution_id)
94
161
  Execution.find(execution_id)
95
162
  end
96
163
 
97
- # See ActiveDurable::Operations.
164
+ # Resumes a blocked execution where it stopped: failed steps (or failed undos, if it was undoing) get a fresh
165
+ # set of attempts. Use it after fixing what blocked it, such as a bug in the recipe or a failing hook.
166
+ #
167
+ # @param execution_id [String]
168
+ # @return [Execution]
169
+ # @raise [Error] if the execution is not blocked, or a worker is running it right now
98
170
  def retry(execution_id)
99
171
  Operations.retry(execution_id)
100
172
  end
101
173
 
174
+ # Undoes every finished step of an execution that has not passed its point of no return, last one first, and
175
+ # then runs its flow.on(:compensated) hook.
176
+ #
177
+ # @param execution_id [String]
178
+ # @param reason [String] recorded as the execution's error
179
+ # @return [Execution]
180
+ # @raise [Error] if it already passed flow.pivot, is already undoing, or a worker is running it right now
102
181
  def compensate(execution_id, reason: "compensated by an operator")
103
182
  Operations.compensate(execution_id, reason: reason)
104
183
  end
105
184
 
185
+ # Starts a new execution that reuses the original's steps before `from` and runs `from` and the following ones
186
+ # again, with new tickets (so they have effects again). A blocked original becomes superseded.
187
+ #
188
+ # @param execution_id [String]
189
+ # @param from [Symbol, String] a step in the original's notebook
190
+ # @return [Execution] the new execution
191
+ # @raise [Error] if the original has no such step, is active, or a worker is running it right now
106
192
  def rerun(execution_id, from:)
107
193
  Operations.rerun(execution_id, from: from)
108
194
  end
109
195
 
196
+ # Deletes finished executions (completed, compensated, superseded) older than `older_than`, with their notebook
197
+ # and signals. Active and blocked executions are never deleted. {PruneJob} calls it with the default.
198
+ #
199
+ # @param older_than [ActiveSupport::Duration, Numeric] seconds; config.keep_finished_for by default
200
+ # @return [Integer] how many executions were deleted
201
+ # @raise [ArgumentError] if older_than is nil
202
+ def prune(older_than: config.keep_finished_for)
203
+ Pruner.call(older_than: older_than)
204
+ end
205
+
206
+ # @api private
110
207
  def now
111
208
  config.clock.call + time_offset
112
209
  end
113
210
 
211
+ # @api private
114
212
  def time_offset
115
213
  @time_offset ||= 0.0
116
214
  end
117
215
 
118
216
  # Objects with #capture (called in the worker thread) and #wrap(captured) { } (called around each
119
217
  # flow.parallel branch thread). Used to carry context such as OpenTelemetry spans into the branches.
218
+ #
219
+ # @api private
120
220
  def branch_wrappers
121
221
  @branch_wrappers ||= []
122
222
  end
123
223
 
224
+ # @api private
124
225
  def enqueue(execution_id, wait_until: nil)
125
226
  return if enqueue_disabled
126
227
 
@@ -128,11 +229,14 @@ module ActiveDurable
128
229
  job.perform_later(execution_id)
129
230
  end
130
231
 
232
+ # @api private
131
233
  def instrument(event, payload = {}, &)
132
234
  ActiveSupport::Notifications.instrument("#{event}.active_durable", payload, &)
133
235
  end
134
236
 
135
237
  # Hook used by ActiveDurable::Testing to simulate crashes at precise points.
238
+ #
239
+ # @api private
136
240
  def crash_point(kind, name)
137
241
  crash_hook&.call(kind, name)
138
242
  end
@@ -4,6 +4,7 @@ require "rails/generators"
4
4
  require "rails/generators/active_record"
5
5
 
6
6
  module ActiveDurable
7
+ # Rails generators: `active_durable:install` for new apps, `active_durable:upgrade` after updating the gem.
7
8
  module Generators
8
9
  # rails generate active_durable:install
9
10
  class InstallGenerator < Rails::Generators::Base
@@ -12,6 +13,7 @@ module ActiveDurable
12
13
  source_root File.expand_path("templates", __dir__)
13
14
  desc "Creates the migration for ActiveDurable's tables (sagas, notebook and signals)."
14
15
 
16
+ # @api private
15
17
  def create_migration_file
16
18
  migration_template "create_active_durable_tables.rb.tt", "db/migrate/create_active_durable_tables.rb",
17
19
  migration_version: migration_version
@@ -19,6 +19,7 @@ class CreateActiveDurableTables < ActiveRecord::Migration<%= migration_version %
19
19
  t.timestamps precision: 6
20
20
  end
21
21
  add_index :durable_executions, %i[status wake_at]
22
+ add_index :durable_executions, %i[status updated_at]
22
23
  add_index :durable_executions, %i[recipe status]
23
24
  add_index :durable_executions, :forked_from
24
25
 
@@ -0,0 +1,14 @@
1
+ # frozen_string_literal: true
2
+
3
+ # ActiveDurable 0.6: finds finished executions to prune, and quiet ones to sweep, without scanning the table.
4
+ class AddActiveDurablePruneIndex < ActiveRecord::Migration<%= migration_version %>
5
+ def up
6
+ add_index :durable_executions, %i[status updated_at] unless index_exists?(:durable_executions, %i[status updated_at])
7
+ end
8
+
9
+ def down
10
+ return unless index_exists?(:durable_executions, %i[status updated_at])
11
+
12
+ remove_index :durable_executions, column: %i[status updated_at]
13
+ end
14
+ end
@@ -0,0 +1,39 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "rails/generators"
4
+ require "rails/generators/active_record"
5
+
6
+ module ActiveDurable
7
+ module Generators
8
+ # rails generate active_durable:upgrade
9
+ #
10
+ # Adds the migrations a newer ActiveDurable needs to an app that installed an older one. It skips the ones the
11
+ # app already has, and each migration checks the database first, so running it twice changes nothing.
12
+ class UpgradeGenerator < Rails::Generators::Base
13
+ include ActiveRecord::Generators::Migration
14
+
15
+ source_root File.expand_path("templates", __dir__)
16
+ desc "Adds the migrations that newer ActiveDurable versions need. Run it after updating the gem."
17
+
18
+ # In the order they were released. Never edit one that shipped: add a new one.
19
+ MIGRATIONS = %w[add_active_durable_prune_index].freeze
20
+
21
+ # @api private
22
+ def create_migration_files
23
+ MIGRATIONS.each do |name|
24
+ if self.class.migration_exists?(File.join(destination_root, "db/migrate"), name)
25
+ say_status :skip, "db/migrate/*_#{name}.rb (already there)", :yellow
26
+ else
27
+ migration_template "#{name}.rb.tt", "db/migrate/#{name}.rb", migration_version: migration_version
28
+ end
29
+ end
30
+ end
31
+
32
+ private
33
+
34
+ def migration_version
35
+ "[#{ActiveRecord::Migration.current_version}]"
36
+ end
37
+ end
38
+ end
39
+ end
@@ -16,9 +16,20 @@ namespace :active_durable do
16
16
  end
17
17
  end
18
18
 
19
+ desc "Delete finished executions older than config.keep_finished_for, with their notebook (run it daily)"
20
+ task prune: :environment do
21
+ days = (ActiveDurable.config.keep_finished_for.to_f / 86_400).round(2)
22
+ days = days.to_i if days == days.to_i
23
+ puts "ActiveDurable: deleted #{ActiveDurable.prune} finished execution(s) older than #{days} days"
24
+ end
25
+
19
26
  desc "Enqueue executions that should be running but have no job (run it every minute)"
20
27
  task sweep: :environment do
21
- ids = ActiveDurable::Sweeper.call
22
- puts "ActiveDurable: enqueued #{ids.size} execution(s)"
28
+ ids, how = ActiveDurable::Sweeper.call_from_task
29
+ if how == :ran
30
+ puts "ActiveDurable: ran #{ids.size} execution(s) in this process (the :async adapter would lose their jobs)"
31
+ else
32
+ puts "ActiveDurable: enqueued #{ids.size} execution(s)"
33
+ end
23
34
  end
24
35
  end
metadata CHANGED
@@ -1,14 +1,13 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: active_durable
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.5.0
4
+ version: 0.6.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - William Romero
8
- autorequire:
9
8
  bindir: exe
10
9
  cert_chain: []
11
- date: 2026-10-06 00:00:00.000000000 Z
10
+ date: 1980-01-02 00:00:00.000000000 Z
12
11
  dependencies:
13
12
  - !ruby/object:Gem::Dependency
14
13
  name: activejob
@@ -17,6 +16,9 @@ dependencies:
17
16
  - - ">="
18
17
  - !ruby/object:Gem::Version
19
18
  version: '6.1'
19
+ - - "<"
20
+ - !ruby/object:Gem::Version
21
+ version: '9'
20
22
  type: :runtime
21
23
  prerelease: false
22
24
  version_requirements: !ruby/object:Gem::Requirement
@@ -24,6 +26,9 @@ dependencies:
24
26
  - - ">="
25
27
  - !ruby/object:Gem::Version
26
28
  version: '6.1'
29
+ - - "<"
30
+ - !ruby/object:Gem::Version
31
+ version: '9'
27
32
  - !ruby/object:Gem::Dependency
28
33
  name: activerecord
29
34
  requirement: !ruby/object:Gem::Requirement
@@ -31,6 +36,9 @@ dependencies:
31
36
  - - ">="
32
37
  - !ruby/object:Gem::Version
33
38
  version: '6.1'
39
+ - - "<"
40
+ - !ruby/object:Gem::Version
41
+ version: '9'
34
42
  type: :runtime
35
43
  prerelease: false
36
44
  version_requirements: !ruby/object:Gem::Requirement
@@ -38,6 +46,9 @@ dependencies:
38
46
  - - ">="
39
47
  - !ruby/object:Gem::Version
40
48
  version: '6.1'
49
+ - - "<"
50
+ - !ruby/object:Gem::Version
51
+ version: '9'
41
52
  - !ruby/object:Gem::Dependency
42
53
  name: activesupport
43
54
  requirement: !ruby/object:Gem::Requirement
@@ -45,6 +56,9 @@ dependencies:
45
56
  - - ">="
46
57
  - !ruby/object:Gem::Version
47
58
  version: '6.1'
59
+ - - "<"
60
+ - !ruby/object:Gem::Version
61
+ version: '9'
48
62
  type: :runtime
49
63
  prerelease: false
50
64
  version_requirements: !ruby/object:Gem::Requirement
@@ -52,6 +66,9 @@ dependencies:
52
66
  - - ">="
53
67
  - !ruby/object:Gem::Version
54
68
  version: '6.1'
69
+ - - "<"
70
+ - !ruby/object:Gem::Version
71
+ version: '9'
55
72
  description: |
56
73
  ActiveDurable runs multi-step business flows (checkouts, onboarding, payouts) as durable sagas
57
74
  stored in your own database. Completed steps are checkpointed and never repeated after a crash,
@@ -63,6 +80,7 @@ executables: []
63
80
  extensions: []
64
81
  extra_rdoc_files: []
65
82
  files:
83
+ - ".yardopts"
66
84
  - CHANGELOG.md
67
85
  - LICENSE.txt
68
86
  - README.md
@@ -86,6 +104,8 @@ files:
86
104
  - lib/active_durable/open_telemetry.rb
87
105
  - lib/active_durable/operations.rb
88
106
  - lib/active_durable/parallel.rb
107
+ - lib/active_durable/prune_job.rb
108
+ - lib/active_durable/pruner.rb
89
109
  - lib/active_durable/record.rb
90
110
  - lib/active_durable/registry.rb
91
111
  - lib/active_durable/retry_policy.rb
@@ -100,18 +120,18 @@ files:
100
120
  - lib/active_durable/version.rb
101
121
  - lib/generators/active_durable/install/install_generator.rb
102
122
  - lib/generators/active_durable/install/templates/create_active_durable_tables.rb.tt
123
+ - lib/generators/active_durable/upgrade/templates/add_active_durable_prune_index.rb.tt
124
+ - lib/generators/active_durable/upgrade/upgrade_generator.rb
103
125
  - lib/tasks/active_durable.rake
104
126
  - sig/active_durable.rbs
105
- homepage: https://github.com/williamromero/active_durable
127
+ homepage: https://webresstudio.github.io/active_durable/
106
128
  licenses:
107
129
  - MIT
108
130
  metadata:
109
- homepage_uri: https://github.com/williamromero/active_durable
110
- source_code_uri: https://github.com/williamromero/active_durable
111
- changelog_uri: https://github.com/williamromero/active_durable/blob/main/CHANGELOG.md
112
- bug_tracker_uri: https://github.com/williamromero/active_durable/issues
131
+ source_code_uri: https://github.com/webresstudio/active_durable
132
+ changelog_uri: https://github.com/webresstudio/active_durable/blob/main/CHANGELOG.md
133
+ bug_tracker_uri: https://github.com/webresstudio/active_durable/issues
113
134
  rubygems_mfa_required: 'true'
114
- post_install_message:
115
135
  rdoc_options: []
116
136
  require_paths:
117
137
  - lib
@@ -126,8 +146,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
126
146
  - !ruby/object:Gem::Version
127
147
  version: '0'
128
148
  requirements: []
129
- rubygems_version: 3.5.7
130
- signing_key:
149
+ rubygems_version: 4.0.20
131
150
  specification_version: 4
132
151
  summary: 'Durable sagas for Rails: finish the work or undo it in order, even if the
133
152
  server dies.'