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.
- checksums.yaml +4 -4
- data/.yardopts +8 -0
- data/CHANGELOG.md +52 -2
- data/README.md +97 -17
- data/app/controllers/active_durable/executions_controller.rb +2 -1
- data/app/helpers/active_durable/dashboard_helper.rb +1 -1
- data/app/views/active_durable/executions/index.html.erb +3 -2
- data/app/views/active_durable/executions/show.html.erb +10 -0
- data/app/views/layouts/active_durable/application.html.erb +4 -2
- data/lib/active_durable/configuration.rb +4 -0
- data/lib/active_durable/engine.rb +2 -4
- data/lib/active_durable/errors.rb +30 -0
- data/lib/active_durable/execution.rb +9 -2
- data/lib/active_durable/flow.rb +126 -15
- data/lib/active_durable/flow_parallel.rb +17 -0
- data/lib/active_durable/lease.rb +2 -0
- data/lib/active_durable/notebook.rb +4 -2
- data/lib/active_durable/open_telemetry.rb +14 -2
- data/lib/active_durable/operations.rb +5 -2
- data/lib/active_durable/parallel.rb +15 -0
- data/lib/active_durable/prune_job.rb +16 -0
- data/lib/active_durable/pruner.rb +34 -0
- data/lib/active_durable/record.rb +2 -0
- data/lib/active_durable/registry.rb +4 -0
- data/lib/active_durable/retry_policy.rb +2 -0
- data/lib/active_durable/run_job.rb +3 -0
- data/lib/active_durable/runner.rb +34 -2
- data/lib/active_durable/serializer.rb +2 -0
- data/lib/active_durable/signal_record.rb +2 -0
- data/lib/active_durable/step.rb +10 -0
- data/lib/active_durable/sweep_job.rb +3 -0
- data/lib/active_durable/sweeper.rb +26 -6
- data/lib/active_durable/testing.rb +8 -0
- data/lib/active_durable/version.rb +2 -1
- data/lib/active_durable.rb +115 -11
- data/lib/generators/active_durable/install/install_generator.rb +2 -0
- data/lib/generators/active_durable/install/templates/create_active_durable_tables.rb.tt +1 -0
- data/lib/generators/active_durable/upgrade/templates/add_active_durable_prune_index.rb.tt +14 -0
- data/lib/generators/active_durable/upgrade/upgrade_generator.rb +39 -0
- data/lib/tasks/active_durable.rake +13 -2
- 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
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
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
|
|
data/lib/active_durable.rb
CHANGED
|
@@ -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
|
|
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
|
-
#
|
|
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
|
-
#
|
|
64
|
-
#
|
|
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
|
-
#
|
|
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
|
|
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
|
-
#
|
|
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.
|
|
22
|
-
|
|
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.
|
|
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:
|
|
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.
|
|
127
|
+
homepage: https://webresstudio.github.io/active_durable/
|
|
106
128
|
licenses:
|
|
107
129
|
- MIT
|
|
108
130
|
metadata:
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
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:
|
|
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.'
|