geneva_drive 0.7.0 → 0.9.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/CHANGELOG.md +21 -0
- data/MANUAL.md +299 -5
- data/README.md +14 -1
- data/Rakefile +28 -1
- data/bin/matrix +76 -0
- data/bin/md2html +114 -0
- data/lib/generators/geneva_drive/install/templates/add_resumable_step_support.rb +5 -9
- data/lib/generators/geneva_drive/install/templates/add_signals_support.rb +5 -10
- data/lib/generators/geneva_drive/install/templates/create_workflows_migration.rb +1 -1
- data/lib/generators/geneva_drive/install/templates/initializer.rb.tt +33 -2
- data/lib/geneva_drive/dispatchers/active_job_dispatcher.rb +33 -0
- data/lib/geneva_drive/dispatchers/sidekiq_dispatcher.rb +112 -0
- data/lib/geneva_drive/engine.rb +13 -0
- data/lib/geneva_drive/executor.rb +23 -3
- data/lib/geneva_drive/job_options.rb +7 -3
- data/lib/geneva_drive/jobs/perform_step_job.rb +2 -35
- data/lib/geneva_drive/jobs/sidekiq_housekeeping_job.rb +32 -0
- data/lib/geneva_drive/jobs/sidekiq_perform_step_job.rb +35 -0
- data/lib/geneva_drive/jobs/step_execution_lookup.rb +42 -0
- data/lib/geneva_drive/json_column.rb +72 -0
- data/lib/geneva_drive/migration_helpers.rb +60 -3
- data/lib/geneva_drive/removed_step_definition.rb +34 -0
- data/lib/geneva_drive/signal.rb +4 -2
- data/lib/geneva_drive/step_collection.rb +31 -7
- data/lib/geneva_drive/step_definition.rb +9 -0
- data/lib/geneva_drive/step_execution.rb +3 -2
- data/lib/geneva_drive/version.rb +1 -1
- data/lib/geneva_drive/workflow.rb +200 -39
- data/lib/geneva_drive.rb +65 -0
- data/test/dummy/config/database.yml +40 -5
- data/test/dummy/config/initializers/geneva_drive.rb +78 -0
- data/test/dummy/db/mysql/schema-8.1.rb +95 -0
- data/test/dummy/db/postgres/schema-8.1.rb +99 -0
- data/test/dummy/db/sqlite/schema-7.2.rb +96 -0
- data/test/dummy/db/sqlite/schema-8.0.rb +96 -0
- data/test/dummy/db/sqlite/schema-8.1.rb +96 -0
- data/test/dummy/db/sqlite/test.sqlite3 +0 -0
- data/test/dummy/db/sqlite/test.sqlite3-0 +0 -0
- data/test/dummy/db/sqlite/test.sqlite3-1 +0 -0
- data/test/dummy/db/sqlite/test.sqlite3-2 +0 -0
- data/test/dummy/db/sqlite/test.sqlite3-3 +0 -0
- data/test/dummy/db/sqlite/test.sqlite3-4 +0 -0
- data/test/dummy/db/sqlite/test.sqlite3-5 +0 -0
- data/test/dummy/db/sqlite/test.sqlite3-6 +0 -0
- data/test/dummy/db/sqlite/test.sqlite3-7 +0 -0
- data/test/dummy/db/sqlite/test.sqlite3-8 +0 -0
- data/test/dummy/db/sqlite/test.sqlite3-9 +0 -0
- data/test/dummy/db/sqlite/test.sqlite3_0 +0 -0
- data/test/dummy/db/sqlite/test.sqlite3_1 +0 -0
- data/test/dummy/db/sqlite/test.sqlite3_2 +0 -0
- data/test/dummy/db/sqlite/test.sqlite3_3 +0 -0
- data/test/dummy/db/sqlite/test.sqlite3_4 +0 -0
- data/test/dummy/db/sqlite/test.sqlite3_5 +0 -0
- data/test/dummy/db/sqlite/test.sqlite3_6 +0 -0
- data/test/dummy/db/sqlite/test.sqlite3_7 +0 -0
- data/test/dummy/db/sqlite/test.sqlite3_8 +0 -0
- data/test/dummy/db/sqlite/test.sqlite3_9 +0 -0
- data/test/dummy/db/sqlite3/schema-7.2.rb +96 -0
- data/test/dummy/db/sqlite3/schema-8.1.rb +96 -0
- data/test/dummy/db/sqlite3/test.sqlite3 +0 -0
- data/test/dummy/db/sqlite3/test.sqlite3_0 +0 -0
- data/test/dummy/db/sqlite3/test.sqlite3_1 +0 -0
- data/test/dummy/db/sqlite3/test.sqlite3_2 +0 -0
- data/test/dummy/db/sqlite3/test.sqlite3_3 +0 -0
- data/test/dummy/db/sqlite3/test.sqlite3_4 +0 -0
- data/test/dummy/db/sqlite3/test.sqlite3_5 +0 -0
- data/test/dummy/db/sqlite3/test.sqlite3_6 +0 -0
- data/test/dummy/db/sqlite3/test.sqlite3_7 +0 -0
- data/test/dummy/db/sqlite3/test.sqlite3_8 +0 -0
- data/test/dummy/db/sqlite3/test.sqlite3_9 +0 -0
- data/test/dummy_install/db/migrate/20260217112949_create_geneva_drive_workflows.rb +76 -0
- data/test/dummy_install/db/migrate/20260217112950_create_geneva_drive_step_executions.rb +99 -0
- data/test/dummy_install/db/migrate/20260217112951_add_finished_at_to_geneva_drive_step_executions.rb +27 -0
- data/test/dummy_install/db/migrate/20260217112952_add_error_class_name_to_geneva_drive_step_executions.rb +9 -0
- data/test/dummy_install/db/migrate/20260217112953_add_resumable_step_support_to_geneva_drive_step_executions.rb +128 -0
- data/test/dummy_install/db/migrate/20260217112954_add_chained_step_executions_to_geneva_drive_step_executions.rb +136 -0
- data/test/dummy_install/db/schema.rb +75 -0
- data/test/dummy_install/log/test.log +1852 -0
- data/test/generators/install_generator_test.rb +56 -0
- data/test/geneva_drive_test.rb +10 -0
- data/test/migration_helpers_test.rb +62 -0
- data/test/sidekiq/sidekiq_dispatcher_test.rb +322 -0
- data/test/sidekiq/sidekiq_redis_test.rb +115 -0
- data/test/test_helper.rb +51 -13
- data/test/workflow/pause_resume_test.rb +15 -0
- data/test/workflow/removed_step_test.rb +294 -0
- data/test/workflow/resume_at_test.rb +170 -0
- metadata +62 -1
|
@@ -0,0 +1,294 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "test_helper"
|
|
4
|
+
|
|
5
|
+
class RemovedStepTest < ActiveSupport::TestCase
|
|
6
|
+
include GenevaDrive::TestHelpers
|
|
7
|
+
|
|
8
|
+
# The shape a workflow has after `capture_payment` was taken out of the
|
|
9
|
+
# middle of it and a gravestone left behind.
|
|
10
|
+
class PaymentWorkflow < GenevaDrive::Workflow
|
|
11
|
+
step :authorize do
|
|
12
|
+
Thread.current[:ran] << :authorize
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
removed_step :capture_payment
|
|
16
|
+
|
|
17
|
+
step :send_receipt do
|
|
18
|
+
Thread.current[:ran] << :send_receipt
|
|
19
|
+
end
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
# A gravestone in first position, so nothing runnable precedes it.
|
|
23
|
+
class LeadingRemovalWorkflow < GenevaDrive::Workflow
|
|
24
|
+
removed_step :old_preflight
|
|
25
|
+
|
|
26
|
+
step :only_step do
|
|
27
|
+
Thread.current[:ran] << :only_step
|
|
28
|
+
end
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
# A gravestone in last position, so nothing runnable follows it.
|
|
32
|
+
class TrailingRemovalWorkflow < GenevaDrive::Workflow
|
|
33
|
+
step :only_step do
|
|
34
|
+
Thread.current[:ran] << :only_step
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
removed_step :old_teardown
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
# Every step removed - there is nothing left to run at all.
|
|
41
|
+
class FullyRemovedWorkflow < GenevaDrive::Workflow
|
|
42
|
+
removed_step :gone_one
|
|
43
|
+
removed_step :gone_two
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
# A step deleted outright, with no gravestone. This is the situation
|
|
47
|
+
# removed_step exists to prevent, kept here to prove the default is
|
|
48
|
+
# unchanged: the workflow still pauses rather than guessing.
|
|
49
|
+
class UndeclaredRemovalWorkflow < GenevaDrive::Workflow
|
|
50
|
+
step :step_one do
|
|
51
|
+
Thread.current[:ran] << :step_one
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
step :step_two do
|
|
55
|
+
Thread.current[:ran] << :step_two
|
|
56
|
+
end
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
setup do
|
|
60
|
+
@user = create_user
|
|
61
|
+
Thread.current[:ran] = []
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
teardown do
|
|
65
|
+
Thread.current[:ran] = nil
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
# ===========================================
|
|
69
|
+
# The declaration itself
|
|
70
|
+
# ===========================================
|
|
71
|
+
|
|
72
|
+
test "removed_step registers a gravestone that reports itself as removed" do
|
|
73
|
+
step_def = PaymentWorkflow.steps.named("capture_payment")
|
|
74
|
+
|
|
75
|
+
assert_not_nil step_def
|
|
76
|
+
assert_predicate step_def, :removed?
|
|
77
|
+
assert_equal "capture_payment", step_def.name
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
test "an ordinary step is not removed" do
|
|
81
|
+
assert_not_predicate PaymentWorkflow.steps.named("authorize"), :removed?
|
|
82
|
+
end
|
|
83
|
+
|
|
84
|
+
test "a removed step keeps its name resolvable" do
|
|
85
|
+
assert PaymentWorkflow.steps.key?("capture_payment")
|
|
86
|
+
end
|
|
87
|
+
|
|
88
|
+
test "a removed step holds its position in the declared order" do
|
|
89
|
+
assert_equal %w[authorize capture_payment send_receipt], PaymentWorkflow.steps.map(&:name)
|
|
90
|
+
end
|
|
91
|
+
|
|
92
|
+
test "runnable omits removed steps and removed lists them" do
|
|
93
|
+
assert_equal %w[authorize send_receipt], PaymentWorkflow.steps.runnable.map(&:name)
|
|
94
|
+
assert_equal %w[capture_payment], PaymentWorkflow.steps.removed.map(&:name)
|
|
95
|
+
end
|
|
96
|
+
|
|
97
|
+
test "removed_step refuses a name already taken by a step" do
|
|
98
|
+
error = assert_raises(GenevaDrive::StepConfigurationError) do
|
|
99
|
+
Class.new(GenevaDrive::Workflow) do
|
|
100
|
+
def self.name = "DoubleDeclarationWorkflow"
|
|
101
|
+
|
|
102
|
+
step :thing do
|
|
103
|
+
# no-op
|
|
104
|
+
end
|
|
105
|
+
|
|
106
|
+
removed_step :thing
|
|
107
|
+
end
|
|
108
|
+
end
|
|
109
|
+
|
|
110
|
+
assert_match(/already defined/, error.message)
|
|
111
|
+
end
|
|
112
|
+
|
|
113
|
+
test "removed_step requires a name" do
|
|
114
|
+
assert_raises(ArgumentError) do
|
|
115
|
+
Class.new(GenevaDrive::Workflow) do
|
|
116
|
+
removed_step nil
|
|
117
|
+
end
|
|
118
|
+
end
|
|
119
|
+
end
|
|
120
|
+
|
|
121
|
+
test "before_step can still reference a removed step" do
|
|
122
|
+
klass = Class.new(GenevaDrive::Workflow) do
|
|
123
|
+
def self.name = "PositionedAroundRemovalWorkflow"
|
|
124
|
+
|
|
125
|
+
removed_step :gone
|
|
126
|
+
|
|
127
|
+
step :inserted, before_step: :gone do
|
|
128
|
+
# no-op
|
|
129
|
+
end
|
|
130
|
+
end
|
|
131
|
+
|
|
132
|
+
assert_equal %w[inserted gone], klass.steps.map(&:name)
|
|
133
|
+
end
|
|
134
|
+
|
|
135
|
+
# ===========================================
|
|
136
|
+
# Navigation never lands on a gravestone
|
|
137
|
+
# ===========================================
|
|
138
|
+
|
|
139
|
+
test "next_after steps over a removed step" do
|
|
140
|
+
assert_equal "send_receipt", PaymentWorkflow.steps.next_after("authorize").name
|
|
141
|
+
end
|
|
142
|
+
|
|
143
|
+
test "next_after from a removed step returns the next runnable one" do
|
|
144
|
+
assert_equal "send_receipt", PaymentWorkflow.steps.next_after("capture_payment").name
|
|
145
|
+
end
|
|
146
|
+
|
|
147
|
+
test "next_after returns nil when only removed steps remain" do
|
|
148
|
+
assert_nil TrailingRemovalWorkflow.steps.next_after("only_step")
|
|
149
|
+
end
|
|
150
|
+
|
|
151
|
+
test "next_after from the beginning skips a leading removed step" do
|
|
152
|
+
assert_equal "only_step", LeadingRemovalWorkflow.steps.next_after(nil).name
|
|
153
|
+
end
|
|
154
|
+
|
|
155
|
+
test "next_after still returns nil for a name the collection never had" do
|
|
156
|
+
assert_nil PaymentWorkflow.steps.next_after("never_existed")
|
|
157
|
+
end
|
|
158
|
+
|
|
159
|
+
test "previous_before steps over a removed step" do
|
|
160
|
+
assert_equal "authorize", PaymentWorkflow.steps.previous_before("send_receipt").name
|
|
161
|
+
end
|
|
162
|
+
|
|
163
|
+
test "previous_before returns nil when only removed steps precede" do
|
|
164
|
+
assert_nil LeadingRemovalWorkflow.steps.previous_before("only_step")
|
|
165
|
+
end
|
|
166
|
+
|
|
167
|
+
# ===========================================
|
|
168
|
+
# Scheduling never spools a removed step
|
|
169
|
+
# ===========================================
|
|
170
|
+
|
|
171
|
+
test "a new workflow runs straight past a removed middle step" do
|
|
172
|
+
workflow = PaymentWorkflow.create!(hero: @user)
|
|
173
|
+
speedrun_workflow(workflow)
|
|
174
|
+
|
|
175
|
+
assert_equal %i[authorize send_receipt], Thread.current[:ran]
|
|
176
|
+
assert_workflow_state(workflow, :finished)
|
|
177
|
+
assert_empty workflow.step_executions.where(step_name: "capture_payment")
|
|
178
|
+
end
|
|
179
|
+
|
|
180
|
+
test "a new workflow starts at the first runnable step, not a leading gravestone" do
|
|
181
|
+
workflow = LeadingRemovalWorkflow.create!(hero: @user)
|
|
182
|
+
|
|
183
|
+
assert_equal "only_step", workflow.next_step_name
|
|
184
|
+
|
|
185
|
+
speedrun_workflow(workflow)
|
|
186
|
+
|
|
187
|
+
assert_equal %i[only_step], Thread.current[:ran]
|
|
188
|
+
assert_workflow_state(workflow, :finished)
|
|
189
|
+
end
|
|
190
|
+
|
|
191
|
+
test "a trailing gravestone finishes the workflow instead of being scheduled" do
|
|
192
|
+
workflow = TrailingRemovalWorkflow.create!(hero: @user)
|
|
193
|
+
speedrun_workflow(workflow)
|
|
194
|
+
|
|
195
|
+
assert_workflow_state(workflow, :finished)
|
|
196
|
+
assert_empty workflow.step_executions.where(step_name: "old_teardown")
|
|
197
|
+
end
|
|
198
|
+
|
|
199
|
+
test "a workflow whose every step was removed finishes immediately" do
|
|
200
|
+
workflow = FullyRemovedWorkflow.create!(hero: @user)
|
|
201
|
+
|
|
202
|
+
assert_workflow_state(workflow, :finished)
|
|
203
|
+
assert_empty workflow.step_executions
|
|
204
|
+
end
|
|
205
|
+
|
|
206
|
+
# ===========================================
|
|
207
|
+
# Executions scheduled before the removal shipped
|
|
208
|
+
# ===========================================
|
|
209
|
+
|
|
210
|
+
test "an execution scheduled before the removal is skipped and the workflow continues" do
|
|
211
|
+
workflow = PaymentWorkflow.create!(hero: @user)
|
|
212
|
+
|
|
213
|
+
# Stand in for a rolling deploy: this row was written by the old code,
|
|
214
|
+
# which still had capture_payment as a real step.
|
|
215
|
+
execution = reschedule_at(workflow, "capture_payment")
|
|
216
|
+
|
|
217
|
+
execution.execute!
|
|
218
|
+
|
|
219
|
+
assert_equal "skipped", execution.reload.state
|
|
220
|
+
assert_equal "skipped", execution.outcome
|
|
221
|
+
|
|
222
|
+
speedrun_workflow(workflow)
|
|
223
|
+
|
|
224
|
+
assert_equal %i[send_receipt], Thread.current[:ran]
|
|
225
|
+
assert_workflow_state(workflow, :finished)
|
|
226
|
+
end
|
|
227
|
+
|
|
228
|
+
test "an execution for a trailing removed step finishes the workflow" do
|
|
229
|
+
workflow = TrailingRemovalWorkflow.create!(hero: @user)
|
|
230
|
+
execution = reschedule_at(workflow, "old_teardown")
|
|
231
|
+
|
|
232
|
+
execution.execute!
|
|
233
|
+
|
|
234
|
+
assert_equal "skipped", execution.reload.state
|
|
235
|
+
assert_workflow_state(workflow, :finished)
|
|
236
|
+
end
|
|
237
|
+
|
|
238
|
+
test "a step deleted without a gravestone still pauses the workflow" do
|
|
239
|
+
workflow = UndeclaredRemovalWorkflow.create!(hero: @user)
|
|
240
|
+
execution = reschedule_at(workflow, "deleted_without_a_trace")
|
|
241
|
+
|
|
242
|
+
assert_raises(GenevaDrive::StepNotDefinedError) { execution.execute! }
|
|
243
|
+
|
|
244
|
+
assert_workflow_state(workflow, :paused)
|
|
245
|
+
end
|
|
246
|
+
|
|
247
|
+
# ===========================================
|
|
248
|
+
# Knowing when the gravestone can go
|
|
249
|
+
# ===========================================
|
|
250
|
+
|
|
251
|
+
test "removed_steps_in_flight counts rows still pointing at a removed step" do
|
|
252
|
+
workflow = PaymentWorkflow.create!(hero: @user)
|
|
253
|
+
reschedule_at(workflow, "capture_payment")
|
|
254
|
+
|
|
255
|
+
assert_equal({"capture_payment" => 1}, PaymentWorkflow.removed_steps_in_flight)
|
|
256
|
+
end
|
|
257
|
+
|
|
258
|
+
test "removed_steps_in_flight ignores workflows that will never run again" do
|
|
259
|
+
workflow = PaymentWorkflow.create!(hero: @user)
|
|
260
|
+
reschedule_at(workflow, "capture_payment")
|
|
261
|
+
workflow.update!(state: "canceled")
|
|
262
|
+
|
|
263
|
+
assert_empty PaymentWorkflow.removed_steps_in_flight
|
|
264
|
+
end
|
|
265
|
+
|
|
266
|
+
test "removed_steps_in_flight is empty once nothing references the name" do
|
|
267
|
+
PaymentWorkflow.create!(hero: @user)
|
|
268
|
+
|
|
269
|
+
assert_empty PaymentWorkflow.removed_steps_in_flight
|
|
270
|
+
end
|
|
271
|
+
|
|
272
|
+
test "removed_steps_in_flight is empty for a workflow with no removals" do
|
|
273
|
+
assert_empty UndeclaredRemovalWorkflow.removed_steps_in_flight
|
|
274
|
+
end
|
|
275
|
+
|
|
276
|
+
private
|
|
277
|
+
|
|
278
|
+
# Replaces whatever is scheduled with an execution for the given step name,
|
|
279
|
+
# the way a row written by a previous deploy would look.
|
|
280
|
+
def reschedule_at(workflow, step_name)
|
|
281
|
+
workflow.step_executions.where(state: %w[scheduled waiting]).update_all(
|
|
282
|
+
state: "canceled",
|
|
283
|
+
outcome: "canceled",
|
|
284
|
+
canceled_at: Time.current
|
|
285
|
+
)
|
|
286
|
+
execution = workflow.step_executions.create!(
|
|
287
|
+
step_name: step_name,
|
|
288
|
+
state: "scheduled",
|
|
289
|
+
scheduled_for: Time.current
|
|
290
|
+
)
|
|
291
|
+
workflow.update!(next_step_name: step_name, current_step_name: nil)
|
|
292
|
+
execution
|
|
293
|
+
end
|
|
294
|
+
end
|
|
@@ -0,0 +1,170 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "test_helper"
|
|
4
|
+
|
|
5
|
+
class ResumeAtTest < ActiveSupport::TestCase
|
|
6
|
+
include GenevaDrive::TestHelpers
|
|
7
|
+
|
|
8
|
+
class ThreeStepWorkflow < GenevaDrive::Workflow
|
|
9
|
+
step :step_one do
|
|
10
|
+
Thread.current[:ran] << :step_one
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
step :step_two do
|
|
14
|
+
Thread.current[:ran] << :step_two
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
step :step_three do
|
|
18
|
+
Thread.current[:ran] << :step_three
|
|
19
|
+
end
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
class WorkflowWithRemoval < GenevaDrive::Workflow
|
|
23
|
+
step :step_one do
|
|
24
|
+
Thread.current[:ran] << :step_one
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
removed_step :parked_forever
|
|
28
|
+
|
|
29
|
+
step :step_three do
|
|
30
|
+
Thread.current[:ran] << :step_three
|
|
31
|
+
end
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
setup do
|
|
35
|
+
@user = create_user
|
|
36
|
+
Thread.current[:ran] = []
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
teardown do
|
|
40
|
+
Thread.current[:ran] = nil
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
# ===========================================
|
|
44
|
+
# resume_at!
|
|
45
|
+
# ===========================================
|
|
46
|
+
|
|
47
|
+
test "resume_at! continues from the named step" do
|
|
48
|
+
workflow = paused_at(ThreeStepWorkflow.create!(hero: @user), "step_two")
|
|
49
|
+
|
|
50
|
+
execution = workflow.resume_at!(:step_three)
|
|
51
|
+
|
|
52
|
+
assert_equal "step_three", execution.step_name
|
|
53
|
+
assert_equal "step_three", workflow.reload.next_step_name
|
|
54
|
+
assert_equal "ready", workflow.state
|
|
55
|
+
|
|
56
|
+
speedrun_workflow(workflow)
|
|
57
|
+
|
|
58
|
+
assert_equal %i[step_three], Thread.current[:ran]
|
|
59
|
+
assert_workflow_state(workflow, :finished)
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
test "resume_at! cancels the execution the workflow was pointed at" do
|
|
63
|
+
workflow = ThreeStepWorkflow.create!(hero: @user)
|
|
64
|
+
stale = workflow.current_execution
|
|
65
|
+
workflow.pause!
|
|
66
|
+
|
|
67
|
+
workflow.resume_at!(:step_three)
|
|
68
|
+
|
|
69
|
+
assert_equal "canceled", stale.reload.state
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
test "resume_at! accepts a string as readily as a symbol" do
|
|
73
|
+
workflow = paused_at(ThreeStepWorkflow.create!(hero: @user), "step_two")
|
|
74
|
+
|
|
75
|
+
assert_equal "step_three", workflow.resume_at!("step_three").step_name
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
test "resume_at! rejects a step that is not defined" do
|
|
79
|
+
workflow = paused_at(ThreeStepWorkflow.create!(hero: @user), "step_two")
|
|
80
|
+
|
|
81
|
+
error = assert_raises(GenevaDrive::StepNotDefinedError) { workflow.resume_at!(:nowhere) }
|
|
82
|
+
|
|
83
|
+
assert_match(/not defined/, error.message)
|
|
84
|
+
assert_match(/step_one, step_two, step_three/, error.message)
|
|
85
|
+
assert_equal "paused", workflow.reload.state
|
|
86
|
+
end
|
|
87
|
+
|
|
88
|
+
test "resume_at! refuses to land on a removed step" do
|
|
89
|
+
workflow = paused_at(WorkflowWithRemoval.create!(hero: @user), "step_one")
|
|
90
|
+
|
|
91
|
+
error = assert_raises(GenevaDrive::StepNotDefinedError) { workflow.resume_at!(:parked_forever) }
|
|
92
|
+
|
|
93
|
+
assert_match(/removed_step/, error.message)
|
|
94
|
+
assert_equal "paused", workflow.reload.state
|
|
95
|
+
end
|
|
96
|
+
|
|
97
|
+
test "resume_at! only applies to paused workflows" do
|
|
98
|
+
workflow = ThreeStepWorkflow.create!(hero: @user)
|
|
99
|
+
|
|
100
|
+
assert_raises(GenevaDrive::InvalidStateError) { workflow.resume_at!(:step_three) }
|
|
101
|
+
end
|
|
102
|
+
|
|
103
|
+
# ===========================================
|
|
104
|
+
# resume! on a workflow pointed at a vanished step
|
|
105
|
+
# ===========================================
|
|
106
|
+
|
|
107
|
+
test "resume! explains itself when the step it points at is gone" do
|
|
108
|
+
workflow = paused_at(ThreeStepWorkflow.create!(hero: @user), "deleted_without_a_trace")
|
|
109
|
+
|
|
110
|
+
error = assert_raises(GenevaDrive::StepNotDefinedError) { workflow.resume! }
|
|
111
|
+
|
|
112
|
+
assert_match(/no longer defined/, error.message)
|
|
113
|
+
assert_match(/resume_at!/, error.message)
|
|
114
|
+
assert_match(/removed_step :deleted_without_a_trace/, error.message)
|
|
115
|
+
assert_match(/step_one, step_two, step_three/, error.message)
|
|
116
|
+
end
|
|
117
|
+
|
|
118
|
+
test "resume_at! rescues a workflow stranded by a vanished step" do
|
|
119
|
+
workflow = paused_at(ThreeStepWorkflow.create!(hero: @user), "deleted_without_a_trace")
|
|
120
|
+
|
|
121
|
+
workflow.resume_at!(:step_three)
|
|
122
|
+
speedrun_workflow(workflow)
|
|
123
|
+
|
|
124
|
+
assert_equal %i[step_three], Thread.current[:ran]
|
|
125
|
+
assert_workflow_state(workflow, :finished)
|
|
126
|
+
end
|
|
127
|
+
|
|
128
|
+
# ===========================================
|
|
129
|
+
# A step removed while an execution was parked
|
|
130
|
+
# ===========================================
|
|
131
|
+
|
|
132
|
+
test "resume! releases an execution parked on a since-removed step" do
|
|
133
|
+
workflow = WorkflowWithRemoval.create!(hero: @user)
|
|
134
|
+
|
|
135
|
+
# The old code parked here waiting for a signal; the new code has only a
|
|
136
|
+
# gravestone, so nothing can ever match.
|
|
137
|
+
workflow.step_executions.where(state: %w[scheduled waiting]).update_all(
|
|
138
|
+
state: "canceled", outcome: "canceled", canceled_at: Time.current
|
|
139
|
+
)
|
|
140
|
+
parked = workflow.step_executions.create!(
|
|
141
|
+
step_name: "parked_forever",
|
|
142
|
+
state: "waiting",
|
|
143
|
+
scheduled_for: Time.current,
|
|
144
|
+
waiting_since: Time.current
|
|
145
|
+
)
|
|
146
|
+
workflow.update!(next_step_name: "parked_forever", current_step_name: nil)
|
|
147
|
+
workflow.pause!
|
|
148
|
+
|
|
149
|
+
workflow.resume!
|
|
150
|
+
|
|
151
|
+
assert_equal "skipped", parked.reload.state
|
|
152
|
+
|
|
153
|
+
speedrun_workflow(workflow)
|
|
154
|
+
|
|
155
|
+
assert_equal %i[step_three], Thread.current[:ran]
|
|
156
|
+
assert_workflow_state(workflow, :finished)
|
|
157
|
+
end
|
|
158
|
+
|
|
159
|
+
private
|
|
160
|
+
|
|
161
|
+
# Puts the workflow in the state it would be in after pausing while pointed
|
|
162
|
+
# at the given step name, with no live execution left behind.
|
|
163
|
+
def paused_at(workflow, step_name)
|
|
164
|
+
workflow.step_executions.where(state: %w[scheduled waiting]).update_all(
|
|
165
|
+
state: "canceled", outcome: "canceled", canceled_at: Time.current
|
|
166
|
+
)
|
|
167
|
+
workflow.update!(state: "paused", next_step_name: step_name, current_step_name: nil)
|
|
168
|
+
workflow
|
|
169
|
+
end
|
|
170
|
+
end
|
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: geneva_drive
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.
|
|
4
|
+
version: 0.9.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Julik Tarkhanov
|
|
@@ -94,6 +94,7 @@ files:
|
|
|
94
94
|
- MANUAL.md
|
|
95
95
|
- README.md
|
|
96
96
|
- Rakefile
|
|
97
|
+
- bin/matrix
|
|
97
98
|
- bin/md2html
|
|
98
99
|
- bin/rails
|
|
99
100
|
- bin/rubocop
|
|
@@ -113,6 +114,8 @@ files:
|
|
|
113
114
|
- lib/generators/geneva_drive/install/templates/initializer.rb.tt
|
|
114
115
|
- lib/geneva_drive.rb
|
|
115
116
|
- lib/geneva_drive/combined_exception_policy.rb
|
|
117
|
+
- lib/geneva_drive/dispatchers/active_job_dispatcher.rb
|
|
118
|
+
- lib/geneva_drive/dispatchers/sidekiq_dispatcher.rb
|
|
116
119
|
- lib/geneva_drive/engine.rb
|
|
117
120
|
- lib/geneva_drive/exception_policy.rb
|
|
118
121
|
- lib/geneva_drive/executor.rb
|
|
@@ -121,7 +124,12 @@ files:
|
|
|
121
124
|
- lib/geneva_drive/job_options.rb
|
|
122
125
|
- lib/geneva_drive/jobs/housekeeping_job.rb
|
|
123
126
|
- lib/geneva_drive/jobs/perform_step_job.rb
|
|
127
|
+
- lib/geneva_drive/jobs/sidekiq_housekeeping_job.rb
|
|
128
|
+
- lib/geneva_drive/jobs/sidekiq_perform_step_job.rb
|
|
129
|
+
- lib/geneva_drive/jobs/step_execution_lookup.rb
|
|
130
|
+
- lib/geneva_drive/json_column.rb
|
|
124
131
|
- lib/geneva_drive/migration_helpers.rb
|
|
132
|
+
- lib/geneva_drive/removed_step_definition.rb
|
|
125
133
|
- lib/geneva_drive/resumable_step_definition.rb
|
|
126
134
|
- lib/geneva_drive/signal.rb
|
|
127
135
|
- lib/geneva_drive/signal_matcher.rb
|
|
@@ -168,12 +176,52 @@ files:
|
|
|
168
176
|
- test/dummy/config/initializers/assets.rb
|
|
169
177
|
- test/dummy/config/initializers/content_security_policy.rb
|
|
170
178
|
- test/dummy/config/initializers/filter_parameter_logging.rb
|
|
179
|
+
- test/dummy/config/initializers/geneva_drive.rb
|
|
171
180
|
- test/dummy/config/initializers/inflections.rb
|
|
172
181
|
- test/dummy/config/locales/en.yml
|
|
173
182
|
- test/dummy/config/puma.rb
|
|
174
183
|
- test/dummy/config/routes.rb
|
|
175
184
|
- test/dummy/config/storage.yml
|
|
176
185
|
- test/dummy/db/migrate/20241217000000_create_users.rb
|
|
186
|
+
- test/dummy/db/mysql/schema-8.1.rb
|
|
187
|
+
- test/dummy/db/postgres/schema-8.1.rb
|
|
188
|
+
- test/dummy/db/sqlite/schema-7.2.rb
|
|
189
|
+
- test/dummy/db/sqlite/schema-8.0.rb
|
|
190
|
+
- test/dummy/db/sqlite/schema-8.1.rb
|
|
191
|
+
- test/dummy/db/sqlite/test.sqlite3
|
|
192
|
+
- test/dummy/db/sqlite/test.sqlite3-0
|
|
193
|
+
- test/dummy/db/sqlite/test.sqlite3-1
|
|
194
|
+
- test/dummy/db/sqlite/test.sqlite3-2
|
|
195
|
+
- test/dummy/db/sqlite/test.sqlite3-3
|
|
196
|
+
- test/dummy/db/sqlite/test.sqlite3-4
|
|
197
|
+
- test/dummy/db/sqlite/test.sqlite3-5
|
|
198
|
+
- test/dummy/db/sqlite/test.sqlite3-6
|
|
199
|
+
- test/dummy/db/sqlite/test.sqlite3-7
|
|
200
|
+
- test/dummy/db/sqlite/test.sqlite3-8
|
|
201
|
+
- test/dummy/db/sqlite/test.sqlite3-9
|
|
202
|
+
- test/dummy/db/sqlite/test.sqlite3_0
|
|
203
|
+
- test/dummy/db/sqlite/test.sqlite3_1
|
|
204
|
+
- test/dummy/db/sqlite/test.sqlite3_2
|
|
205
|
+
- test/dummy/db/sqlite/test.sqlite3_3
|
|
206
|
+
- test/dummy/db/sqlite/test.sqlite3_4
|
|
207
|
+
- test/dummy/db/sqlite/test.sqlite3_5
|
|
208
|
+
- test/dummy/db/sqlite/test.sqlite3_6
|
|
209
|
+
- test/dummy/db/sqlite/test.sqlite3_7
|
|
210
|
+
- test/dummy/db/sqlite/test.sqlite3_8
|
|
211
|
+
- test/dummy/db/sqlite/test.sqlite3_9
|
|
212
|
+
- test/dummy/db/sqlite3/schema-7.2.rb
|
|
213
|
+
- test/dummy/db/sqlite3/schema-8.1.rb
|
|
214
|
+
- test/dummy/db/sqlite3/test.sqlite3
|
|
215
|
+
- test/dummy/db/sqlite3/test.sqlite3_0
|
|
216
|
+
- test/dummy/db/sqlite3/test.sqlite3_1
|
|
217
|
+
- test/dummy/db/sqlite3/test.sqlite3_2
|
|
218
|
+
- test/dummy/db/sqlite3/test.sqlite3_3
|
|
219
|
+
- test/dummy/db/sqlite3/test.sqlite3_4
|
|
220
|
+
- test/dummy/db/sqlite3/test.sqlite3_5
|
|
221
|
+
- test/dummy/db/sqlite3/test.sqlite3_6
|
|
222
|
+
- test/dummy/db/sqlite3/test.sqlite3_7
|
|
223
|
+
- test/dummy/db/sqlite3/test.sqlite3_8
|
|
224
|
+
- test/dummy/db/sqlite3/test.sqlite3_9
|
|
177
225
|
- test/dummy/public/400.html
|
|
178
226
|
- test/dummy/public/404.html
|
|
179
227
|
- test/dummy/public/406-unsupported-browser.html
|
|
@@ -182,12 +230,23 @@ files:
|
|
|
182
230
|
- test/dummy/public/icon.png
|
|
183
231
|
- test/dummy/public/icon.svg
|
|
184
232
|
- test/dummy_install/config/storage.yml
|
|
233
|
+
- test/dummy_install/db/migrate/20260217112949_create_geneva_drive_workflows.rb
|
|
234
|
+
- test/dummy_install/db/migrate/20260217112950_create_geneva_drive_step_executions.rb
|
|
235
|
+
- test/dummy_install/db/migrate/20260217112951_add_finished_at_to_geneva_drive_step_executions.rb
|
|
236
|
+
- test/dummy_install/db/migrate/20260217112952_add_error_class_name_to_geneva_drive_step_executions.rb
|
|
237
|
+
- test/dummy_install/db/migrate/20260217112953_add_resumable_step_support_to_geneva_drive_step_executions.rb
|
|
238
|
+
- test/dummy_install/db/migrate/20260217112954_add_chained_step_executions_to_geneva_drive_step_executions.rb
|
|
239
|
+
- test/dummy_install/db/schema.rb
|
|
240
|
+
- test/dummy_install/log/test.log
|
|
241
|
+
- test/generators/install_generator_test.rb
|
|
185
242
|
- test/geneva_drive_test.rb
|
|
186
243
|
- test/jobs/housekeeping_job_test.rb
|
|
187
244
|
- test/jobs/housekeeping_signals_test.rb
|
|
188
245
|
- test/jobs/perform_step_job_double_deferral_test.rb
|
|
189
246
|
- test/jobs/perform_step_job_test.rb
|
|
190
247
|
- test/migration_helpers_test.rb
|
|
248
|
+
- test/sidekiq/sidekiq_dispatcher_test.rb
|
|
249
|
+
- test/sidekiq/sidekiq_redis_test.rb
|
|
191
250
|
- test/step_execution/metadata_accessor_test.rb
|
|
192
251
|
- test/test_helper.rb
|
|
193
252
|
- test/test_helper_test.rb
|
|
@@ -205,10 +264,12 @@ files:
|
|
|
205
264
|
- test/workflow/pause_resume_test.rb
|
|
206
265
|
- test/workflow/precondition_exception_test.rb
|
|
207
266
|
- test/workflow/reattempt_on_exception_integration_test.rb
|
|
267
|
+
- test/workflow/removed_step_test.rb
|
|
208
268
|
- test/workflow/resumable_step_integration_test.rb
|
|
209
269
|
- test/workflow/resumable_step_test.rb
|
|
210
270
|
- test/workflow/resumable_without_migration_test.rb
|
|
211
271
|
- test/workflow/resume_and_skip_test.rb
|
|
272
|
+
- test/workflow/resume_at_test.rb
|
|
212
273
|
- test/workflow/signal_consumption_test.rb
|
|
213
274
|
- test/workflow/signal_delivery_test.rb
|
|
214
275
|
- test/workflow/signal_durability_test.rb
|