geneva_drive 0.5.0 → 0.7.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 (60) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +14 -1
  3. data/MANUAL.md +449 -11
  4. data/README.md +5 -1
  5. data/lib/generators/geneva_drive/install/install_generator.rb +20 -0
  6. data/lib/generators/geneva_drive/install/templates/add_metadata_to_workflows.rb +17 -0
  7. data/lib/generators/geneva_drive/install/templates/add_resumable_step_support.rb +29 -0
  8. data/lib/generators/geneva_drive/install/templates/add_signals_support.rb +94 -0
  9. data/lib/generators/geneva_drive/install/templates/add_started_at_index_to_step_executions.rb +48 -0
  10. data/lib/generators/geneva_drive/install/templates/create_workflows_migration.rb +11 -0
  11. data/lib/generators/geneva_drive/install/templates/initializer.rb.tt +8 -0
  12. data/lib/geneva_drive/combined_exception_policy.rb +1 -1
  13. data/lib/geneva_drive/exception_policy.rb +36 -10
  14. data/lib/geneva_drive/executor.rb +352 -26
  15. data/lib/geneva_drive/flow_control.rb +87 -7
  16. data/lib/geneva_drive/iterable_step.rb +199 -0
  17. data/lib/geneva_drive/job_options.rb +70 -0
  18. data/lib/geneva_drive/jobs/housekeeping_job.rb +120 -7
  19. data/lib/geneva_drive/resumable_step_definition.rb +97 -0
  20. data/lib/geneva_drive/signal.rb +243 -0
  21. data/lib/geneva_drive/signal_matcher.rb +60 -0
  22. data/lib/geneva_drive/step_definition.rb +96 -2
  23. data/lib/geneva_drive/step_execution.rb +141 -2
  24. data/lib/geneva_drive/test_helpers.rb +212 -6
  25. data/lib/geneva_drive/version.rb +1 -1
  26. data/lib/geneva_drive/workflow/metadata_accessor.rb +85 -0
  27. data/lib/geneva_drive/workflow.rb +559 -43
  28. data/lib/geneva_drive.rb +37 -0
  29. data/test/dsl/signal_step_definition_test.rb +132 -0
  30. data/test/dsl/step_definition_test.rb +70 -0
  31. data/test/jobs/housekeeping_job_test.rb +119 -0
  32. data/test/jobs/housekeeping_signals_test.rb +198 -0
  33. data/test/jobs/perform_step_job_test.rb +25 -2
  34. data/test/migration_helpers_test.rb +1 -1
  35. data/test/test_helper.rb +4 -0
  36. data/test/test_helper_test.rb +281 -0
  37. data/test/workflow/cursor_size_limit_test.rb +75 -0
  38. data/test/workflow/instrumentation_test.rb +28 -0
  39. data/test/workflow/resumable_step_integration_test.rb +341 -0
  40. data/test/workflow/resumable_step_test.rb +615 -0
  41. data/test/workflow/resumable_without_migration_test.rb +103 -0
  42. data/test/workflow/signal_consumption_test.rb +397 -0
  43. data/test/workflow/signal_delivery_test.rb +255 -0
  44. data/test/workflow/signal_durability_test.rb +164 -0
  45. data/test/workflow/signal_rendezvous_test.rb +581 -0
  46. data/test/workflow/signals_without_migration_test.rb +97 -0
  47. data/test/workflow/workflow_test.rb +72 -1
  48. metadata +25 -15
  49. data/test/dummy/config/initializers/geneva_drive.rb +0 -44
  50. data/test/dummy/db/migrate/20260219212321_create_geneva_drive_workflows.rb +0 -74
  51. data/test/dummy/db/migrate/20260219212322_create_geneva_drive_step_executions.rb +0 -108
  52. data/test/dummy/db/migrate/20260219212323_add_finished_at_to_geneva_drive_step_executions.rb +0 -25
  53. data/test/dummy/db/migrate/20260219212324_add_error_class_name_to_geneva_drive_step_executions.rb +0 -7
  54. data/test/dummy/db/migrate/20260316100007_add_metadata_to_geneva_drive_step_executions.rb +0 -17
  55. data/test/dummy/db/migrate/20260327000000_allow_null_hero_on_geneva_drive_workflows.rb +0 -66
  56. data/test/dummy/db/schema.rb +0 -73
  57. data/test/dummy/log/development.log +0 -202
  58. data/test/dummy/log/test.log +0 -79471
  59. data/test/dummy/log/test.log.0 +0 -4
  60. data/test/dummy/tmp/local_secret.txt +0 -1
data/test/test_helper.rb CHANGED
@@ -54,6 +54,10 @@ class ActiveSupport::TestCase
54
54
  # same parallel process.
55
55
  setup do
56
56
  GenevaDrive::StepExecution.reset_metadata_column_cache!
57
+ GenevaDrive::StepExecution.reset_resumable_columns_cache!
58
+ GenevaDrive::StepExecution.reset_signal_columns_cache!
59
+ GenevaDrive::Signal.reset_table_available_cache!
60
+ GenevaDrive::Workflow.reset_metadata_column_cache!
57
61
  end
58
62
 
59
63
  # Helper to create a test user
@@ -0,0 +1,281 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "test_helper"
4
+
5
+ class TestHelperTest < ActiveSupport::TestCase
6
+ include GenevaDrive::TestHelpers
7
+
8
+ class MultiStepWorkflow < GenevaDrive::Workflow
9
+ step :first_step do
10
+ Thread.current[:first_step_ran] = true
11
+ end
12
+
13
+ step :second_step do
14
+ Thread.current[:second_step_ran] = true
15
+ end
16
+
17
+ step :third_step do
18
+ Thread.current[:third_step_ran] = true
19
+ end
20
+ end
21
+
22
+ class ResumableMultiStepWorkflow < GenevaDrive::Workflow
23
+ resumable_step :resumable_first do |iter|
24
+ items = %w[a b c]
25
+ iter.iterate_over(items) do |item|
26
+ Thread.current[:resumable_items] ||= []
27
+ Thread.current[:resumable_items] << item
28
+ end
29
+ end
30
+
31
+ step :after_resumable do
32
+ Thread.current[:after_resumable_ran] = true
33
+ end
34
+ end
35
+
36
+ setup do
37
+ @user = create_user
38
+ reset_thread_tracking!
39
+ end
40
+
41
+ teardown do
42
+ reset_thread_tracking!
43
+ end
44
+
45
+ def reset_thread_tracking!
46
+ Thread.current[:first_step_ran] = nil
47
+ Thread.current[:second_step_ran] = nil
48
+ Thread.current[:third_step_ran] = nil
49
+ Thread.current[:resumable_items] = nil
50
+ Thread.current[:after_resumable_ran] = nil
51
+ end
52
+
53
+ # speedrun_current_step tests
54
+
55
+ test "speedrun_current_step only runs the current step, not subsequent steps" do
56
+ workflow = MultiStepWorkflow.create!(hero: @user)
57
+
58
+ speedrun_current_step(workflow)
59
+
60
+ assert Thread.current[:first_step_ran], "first step should have run"
61
+ assert_nil Thread.current[:second_step_ran], "second step should NOT have run"
62
+ assert_nil Thread.current[:third_step_ran], "third step should NOT have run"
63
+
64
+ # Workflow should still be ready with next step scheduled
65
+ workflow.reload
66
+ assert_equal "ready", workflow.state
67
+ assert_equal "second_step", workflow.next_step_name
68
+ end
69
+
70
+ test "speedrun_current_step runs resumable step to completion without continuing" do
71
+ workflow = ResumableMultiStepWorkflow.create!(hero: @user)
72
+
73
+ speedrun_current_step(workflow)
74
+
75
+ assert_equal %w[a b c], Thread.current[:resumable_items], "resumable step should complete all items"
76
+ assert_nil Thread.current[:after_resumable_ran], "step after resumable should NOT have run"
77
+
78
+ workflow.reload
79
+ assert_equal "ready", workflow.state
80
+ assert_equal "after_resumable", workflow.next_step_name
81
+ end
82
+
83
+ test "speedrun_current_step returns the step execution" do
84
+ workflow = MultiStepWorkflow.create!(hero: @user)
85
+
86
+ result = speedrun_current_step(workflow)
87
+
88
+ assert_instance_of GenevaDrive::StepExecution, result
89
+ assert_equal "first_step", result.step_name
90
+ assert_equal "completed", result.state
91
+ end
92
+
93
+ test "speedrun_current_step returns nil if no current execution" do
94
+ workflow = MultiStepWorkflow.create!(hero: @user)
95
+ speedrun_workflow(workflow) # Complete the whole workflow
96
+
97
+ result = speedrun_current_step(workflow)
98
+
99
+ assert_nil result
100
+ end
101
+
102
+ # speedrun_workflow tests
103
+
104
+ test "speedrun_workflow runs all steps to completion" do
105
+ workflow = MultiStepWorkflow.create!(hero: @user)
106
+
107
+ speedrun_workflow(workflow)
108
+
109
+ assert Thread.current[:first_step_ran]
110
+ assert Thread.current[:second_step_ran]
111
+ assert Thread.current[:third_step_ran]
112
+ assert_equal "finished", workflow.state
113
+ end
114
+
115
+ test "speedrun_workflow handles resumable steps" do
116
+ workflow = ResumableMultiStepWorkflow.create!(hero: @user)
117
+
118
+ speedrun_workflow(workflow)
119
+
120
+ assert_equal %w[a b c], Thread.current[:resumable_items]
121
+ assert Thread.current[:after_resumable_ran]
122
+ assert_equal "finished", workflow.state
123
+ end
124
+
125
+ # perform_next_step tests
126
+
127
+ test "perform_next_step executes one step and stops" do
128
+ workflow = MultiStepWorkflow.create!(hero: @user)
129
+
130
+ perform_next_step(workflow)
131
+
132
+ assert Thread.current[:first_step_ran]
133
+ assert_nil Thread.current[:second_step_ran]
134
+ end
135
+
136
+ test "perform_next_step returns the executed step execution" do
137
+ workflow = MultiStepWorkflow.create!(hero: @user)
138
+
139
+ result = perform_next_step(workflow)
140
+
141
+ assert_instance_of GenevaDrive::StepExecution, result
142
+ assert_equal "first_step", result.step_name
143
+ end
144
+
145
+ test "perform_next_step returns nil when no current execution" do
146
+ workflow = MultiStepWorkflow.create!(hero: @user)
147
+ speedrun_workflow(workflow)
148
+
149
+ result = perform_next_step(workflow)
150
+
151
+ assert_nil result
152
+ end
153
+
154
+ # perform_step_inline tests
155
+
156
+ test "perform_step_inline executes a specific step by name" do
157
+ workflow = MultiStepWorkflow.create!(hero: @user)
158
+
159
+ perform_step_inline(workflow, :second_step)
160
+
161
+ assert_nil Thread.current[:first_step_ran], "first step should not run"
162
+ assert Thread.current[:second_step_ran], "second step should run"
163
+ end
164
+
165
+ test "perform_step_inline executes current step when no name given" do
166
+ workflow = MultiStepWorkflow.create!(hero: @user)
167
+
168
+ perform_step_inline(workflow)
169
+
170
+ assert Thread.current[:first_step_ran]
171
+ end
172
+
173
+ test "perform_step_inline raises ArgumentError for non-existent step" do
174
+ workflow = MultiStepWorkflow.create!(hero: @user)
175
+
176
+ error = assert_raises(ArgumentError) do
177
+ perform_step_inline(workflow, :non_existent_step)
178
+ end
179
+
180
+ assert_match(/non_existent_step/, error.message)
181
+ assert_match(/not defined/, error.message)
182
+ end
183
+
184
+ test "perform_step_inline returns the step execution" do
185
+ workflow = MultiStepWorkflow.create!(hero: @user)
186
+
187
+ result = perform_step_inline(workflow, :second_step)
188
+
189
+ assert_instance_of GenevaDrive::StepExecution, result
190
+ assert_equal "second_step", result.step_name
191
+ end
192
+
193
+ # assert_step_executed tests
194
+
195
+ test "assert_step_executed passes when step is in expected state" do
196
+ workflow = MultiStepWorkflow.create!(hero: @user)
197
+ perform_next_step(workflow)
198
+
199
+ # Should not raise
200
+ assert_step_executed(workflow, :first_step)
201
+ assert_step_executed(workflow, :first_step, state: "completed")
202
+ end
203
+
204
+ test "assert_step_executed fails when step not found" do
205
+ workflow = MultiStepWorkflow.create!(hero: @user)
206
+
207
+ error = assert_raises(Minitest::Assertion) do
208
+ assert_step_executed(workflow, :first_step)
209
+ end
210
+
211
+ assert_match(/first_step/, error.message)
212
+ end
213
+
214
+ test "assert_step_executed fails when step in different state" do
215
+ workflow = MultiStepWorkflow.create!(hero: @user)
216
+ perform_next_step(workflow)
217
+
218
+ error = assert_raises(Minitest::Assertion) do
219
+ assert_step_executed(workflow, :first_step, state: "failed")
220
+ end
221
+
222
+ assert_match(/first_step/, error.message)
223
+ end
224
+
225
+ # assert_workflow_state tests
226
+
227
+ test "assert_workflow_state passes when state matches" do
228
+ workflow = MultiStepWorkflow.create!(hero: @user)
229
+
230
+ # Should not raise
231
+ assert_workflow_state(workflow, :ready)
232
+ assert_workflow_state(workflow, "ready")
233
+ end
234
+
235
+ test "assert_workflow_state fails when state differs" do
236
+ workflow = MultiStepWorkflow.create!(hero: @user)
237
+
238
+ error = assert_raises(Minitest::Assertion) do
239
+ assert_workflow_state(workflow, :finished)
240
+ end
241
+
242
+ assert_match(/finished/, error.message)
243
+ assert_match(/ready/, error.message)
244
+ end
245
+
246
+ test "assert_workflow_state works after workflow completes" do
247
+ workflow = MultiStepWorkflow.create!(hero: @user)
248
+ speedrun_workflow(workflow)
249
+
250
+ assert_workflow_state(workflow, :finished)
251
+ end
252
+
253
+ # assert_cursor tests
254
+
255
+ test "assert_cursor passes when cursor matches" do
256
+ workflow = ResumableMultiStepWorkflow.create!(hero: @user)
257
+
258
+ # Execute the step - it will complete
259
+ speedrun_current_step(workflow)
260
+
261
+ # After completion, we can check the cursor on the last execution
262
+ # Note: For a fully completed step, cursor may be nil or final value
263
+ workflow.reload
264
+ execution = workflow.step_executions.find_by(step_name: "resumable_first")
265
+ assert_not_nil execution
266
+ end
267
+
268
+ # assert_step_has_successor tests
269
+
270
+ test "assert_step_has_successor passes when successor exists" do
271
+ workflow = ResumableMultiStepWorkflow.create!(hero: @user)
272
+
273
+ # Execute the resumable step - it will complete without successor in this simple case
274
+ speedrun_current_step(workflow)
275
+
276
+ # Just verify the helper doesn't crash when there's no successor
277
+ workflow.reload
278
+ completed = workflow.step_executions.find_by(step_name: "resumable_first", state: "completed")
279
+ assert completed, "Expected completed execution"
280
+ end
281
+ end
@@ -0,0 +1,75 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "test_helper"
4
+
5
+ class CursorSizeLimitTest < ActiveSupport::TestCase
6
+ include GenevaDrive::TestHelpers
7
+
8
+ class PayloadCursorWorkflow < GenevaDrive::Workflow
9
+ resumable_step :process do |iter|
10
+ iter.set!(Thread.current[:cursor_payload])
11
+ end
12
+ end
13
+
14
+ setup do
15
+ @user = create_user
16
+ @original_limit = GenevaDrive.max_cursor_size
17
+ end
18
+
19
+ teardown do
20
+ GenevaDrive.max_cursor_size = @original_limit
21
+ Thread.current[:cursor_payload] = nil
22
+ end
23
+
24
+ test "defaults to 128 KB" do
25
+ assert_equal 128 * 1024, GenevaDrive.max_cursor_size
26
+ end
27
+
28
+ test "an oversized cursor raises CursorTooLargeError and pauses the workflow" do
29
+ GenevaDrive.max_cursor_size = 1024
30
+ Thread.current[:cursor_payload] = "x" * 2048
31
+
32
+ workflow = PayloadCursorWorkflow.create!(hero: @user)
33
+
34
+ error = assert_raises(GenevaDrive::CursorTooLargeError) { perform_next_step(workflow) }
35
+ assert_match(/max_cursor_size/, error.message)
36
+
37
+ workflow.reload
38
+ assert_equal "paused", workflow.state
39
+ end
40
+
41
+ test "a cursor under the limit passes" do
42
+ GenevaDrive.max_cursor_size = 1024
43
+ Thread.current[:cursor_payload] = "x" * 100
44
+
45
+ workflow = PayloadCursorWorkflow.create!(hero: @user)
46
+ speedrun_workflow(workflow)
47
+
48
+ assert_equal "finished", workflow.state
49
+ end
50
+
51
+ test "a nil limit disables the check" do
52
+ GenevaDrive.max_cursor_size = nil
53
+ Thread.current[:cursor_payload] = "x" * (256 * 1024)
54
+
55
+ workflow = PayloadCursorWorkflow.create!(hero: @user)
56
+ speedrun_workflow(workflow)
57
+
58
+ assert_equal "finished", workflow.state
59
+ end
60
+
61
+ test "cursor_value= enforces the limit too" do
62
+ GenevaDrive.max_cursor_size = 1024
63
+
64
+ workflow = PayloadCursorWorkflow.create!(hero: @user)
65
+ execution = workflow.current_execution
66
+
67
+ assert_raises(GenevaDrive::CursorTooLargeError) do
68
+ execution.cursor_value = "x" * 2048
69
+ end
70
+
71
+ execution.cursor_value = "x" * 100
72
+ execution.save!
73
+ assert_equal "x" * 100, execution.reload.cursor_value
74
+ end
75
+ end
@@ -60,6 +60,14 @@ class InstrumentationTest < ActiveSupport::TestCase
60
60
  end
61
61
  end
62
62
 
63
+ class UnreportedExceptionWorkflow < GenevaDrive::Workflow
64
+ on_exception :pause!, report: :never
65
+
66
+ step :failing_step do
67
+ raise StandardError, "test error"
68
+ end
69
+ end
70
+
63
71
  setup do
64
72
  @user = create_user
65
73
  @events = []
@@ -181,6 +189,26 @@ class InstrumentationTest < ActiveSupport::TestCase
181
189
  assert_equal "test error", event[:payload][:exception].message
182
190
  end
183
191
 
192
+ test "adds GenevaDrive context to Rails error reports during step execution" do
193
+ workflow = ExceptionWorkflow.create!(hero: @user)
194
+ step_execution = workflow.step_executions.first
195
+
196
+ report = assert_error_reported(StandardError) do
197
+ assert_raises(StandardError) do
198
+ GenevaDrive::Executor.execute!(step_execution)
199
+ end
200
+ end
201
+
202
+ assert_equal({
203
+ workflow_id: workflow.id,
204
+ workflow_class: "InstrumentationTest::ExceptionWorkflow",
205
+ step_execution_id: step_execution.id,
206
+ step_name: "failing_step",
207
+ hero_type: "User",
208
+ hero_id: @user.id
209
+ }, report.context[:geneva_drive])
210
+ end
211
+
184
212
  test "emits finalize.geneva_drive event after step completion" do
185
213
  InstrumentedWorkflow.reset_tracking!
186
214
  subscribe_to("finalize.geneva_drive")