geneva_drive 0.7.0 → 0.8.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 (33) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +7 -0
  3. data/MANUAL.md +84 -1
  4. data/Rakefile +14 -1
  5. data/bin/matrix +76 -0
  6. data/bin/md2html +114 -0
  7. data/lib/generators/geneva_drive/install/templates/add_resumable_step_support.rb +5 -9
  8. data/lib/generators/geneva_drive/install/templates/add_signals_support.rb +5 -10
  9. data/lib/geneva_drive/executor.rb +18 -0
  10. data/lib/geneva_drive/json_column.rb +72 -0
  11. data/lib/geneva_drive/migration_helpers.rb +27 -0
  12. data/lib/geneva_drive/removed_step_definition.rb +34 -0
  13. data/lib/geneva_drive/signal.rb +4 -2
  14. data/lib/geneva_drive/step_collection.rb +31 -7
  15. data/lib/geneva_drive/step_definition.rb +9 -0
  16. data/lib/geneva_drive/step_execution.rb +3 -2
  17. data/lib/geneva_drive/version.rb +1 -1
  18. data/lib/geneva_drive/workflow.rb +170 -2
  19. data/lib/geneva_drive.rb +2 -0
  20. data/test/dummy/config/database.yml +40 -5
  21. data/test/dummy/config/initializers/geneva_drive.rb +46 -0
  22. data/test/dummy_install/db/migrate/20260217112949_create_geneva_drive_workflows.rb +76 -0
  23. data/test/dummy_install/db/migrate/20260217112950_create_geneva_drive_step_executions.rb +99 -0
  24. data/test/dummy_install/db/migrate/20260217112951_add_finished_at_to_geneva_drive_step_executions.rb +27 -0
  25. data/test/dummy_install/db/migrate/20260217112952_add_error_class_name_to_geneva_drive_step_executions.rb +9 -0
  26. data/test/dummy_install/db/migrate/20260217112953_add_resumable_step_support_to_geneva_drive_step_executions.rb +128 -0
  27. data/test/dummy_install/db/migrate/20260217112954_add_chained_step_executions_to_geneva_drive_step_executions.rb +136 -0
  28. data/test/dummy_install/db/schema.rb +75 -0
  29. data/test/dummy_install/log/test.log +1852 -0
  30. data/test/test_helper.rb +51 -13
  31. data/test/workflow/removed_step_test.rb +294 -0
  32. data/test/workflow/resume_at_test.rb +170 -0
  33. metadata +16 -2
@@ -0,0 +1,128 @@
1
+ # frozen_string_literal: true
2
+
3
+ # license: LGPLv3.0
4
+
5
+ class AddResumableStepSupportToGenevaDriveStepExecutions < ActiveRecord::Migration[7.2]
6
+ def change
7
+ # Use database-native JSON type:
8
+ # - PostgreSQL: jsonb (indexed, efficient, supports containment queries)
9
+ # - MySQL 5.7+: json (native validation and storage)
10
+ # - SQLite: json (Rails handles as TEXT with serialization)
11
+ adapter = connection.adapter_name.downcase
12
+ if adapter.include?("postgresql")
13
+ add_column :geneva_drive_step_executions, :cursor, :jsonb
14
+ else
15
+ add_column :geneva_drive_step_executions, :cursor, :json
16
+ end
17
+
18
+ add_column :geneva_drive_step_executions, :completed_iterations, :integer, default: 0, null: false
19
+
20
+ reversible do |direction|
21
+ direction.up { update_unique_index_for_suspended_state }
22
+ direction.down { restore_original_unique_index }
23
+ end
24
+ end
25
+
26
+ private
27
+
28
+ def update_unique_index_for_suspended_state
29
+ # Update unique constraint to include 'suspended' as an active state.
30
+ # The constraint ensures only one active step execution per workflow.
31
+ adapter = connection.adapter_name.downcase
32
+
33
+ if adapter.include?("postgresql")
34
+ execute <<-SQL
35
+ DROP INDEX IF EXISTS index_geneva_drive_step_executions_one_active;
36
+ SQL
37
+ execute <<-SQL
38
+ CREATE UNIQUE INDEX index_geneva_drive_step_executions_one_active
39
+ ON geneva_drive_step_executions (workflow_id)
40
+ WHERE state IN ('scheduled', 'in_progress', 'suspended');
41
+ SQL
42
+ elsif adapter.include?("mysql")
43
+ # Drop the old generated column and index
44
+ execute <<-SQL
45
+ DROP INDEX index_geneva_drive_step_executions_one_active
46
+ ON geneva_drive_step_executions;
47
+ SQL
48
+ execute <<-SQL
49
+ ALTER TABLE geneva_drive_step_executions
50
+ DROP COLUMN active_unique_key;
51
+ SQL
52
+ # Create new generated column including suspended state
53
+ execute <<-SQL
54
+ ALTER TABLE geneva_drive_step_executions
55
+ ADD COLUMN active_unique_key VARCHAR(767)
56
+ AS (
57
+ CASE
58
+ WHEN state IN ('scheduled', 'in_progress', 'suspended')
59
+ THEN CAST(workflow_id AS CHAR)
60
+ ELSE NULL
61
+ END
62
+ ) STORED;
63
+ SQL
64
+ execute <<-SQL
65
+ CREATE UNIQUE INDEX index_geneva_drive_step_executions_one_active
66
+ ON geneva_drive_step_executions (active_unique_key);
67
+ SQL
68
+ elsif adapter.include?("sqlite")
69
+ execute <<-SQL
70
+ DROP INDEX IF EXISTS index_geneva_drive_step_executions_one_active;
71
+ SQL
72
+ execute <<-SQL
73
+ CREATE UNIQUE INDEX index_geneva_drive_step_executions_one_active
74
+ ON geneva_drive_step_executions (workflow_id)
75
+ WHERE state IN ('scheduled', 'in_progress', 'suspended');
76
+ SQL
77
+ end
78
+ end
79
+
80
+ def restore_original_unique_index
81
+ # Restore original unique constraint without 'suspended' state
82
+ adapter = connection.adapter_name.downcase
83
+
84
+ if adapter.include?("postgresql")
85
+ execute <<-SQL
86
+ DROP INDEX IF EXISTS index_geneva_drive_step_executions_one_active;
87
+ SQL
88
+ execute <<-SQL
89
+ CREATE UNIQUE INDEX index_geneva_drive_step_executions_one_active
90
+ ON geneva_drive_step_executions (workflow_id)
91
+ WHERE state IN ('scheduled', 'in_progress');
92
+ SQL
93
+ elsif adapter.include?("mysql")
94
+ execute <<-SQL
95
+ DROP INDEX index_geneva_drive_step_executions_one_active
96
+ ON geneva_drive_step_executions;
97
+ SQL
98
+ execute <<-SQL
99
+ ALTER TABLE geneva_drive_step_executions
100
+ DROP COLUMN active_unique_key;
101
+ SQL
102
+ execute <<-SQL
103
+ ALTER TABLE geneva_drive_step_executions
104
+ ADD COLUMN active_unique_key VARCHAR(767)
105
+ AS (
106
+ CASE
107
+ WHEN state IN ('scheduled', 'in_progress')
108
+ THEN CAST(workflow_id AS CHAR)
109
+ ELSE NULL
110
+ END
111
+ ) STORED;
112
+ SQL
113
+ execute <<-SQL
114
+ CREATE UNIQUE INDEX index_geneva_drive_step_executions_one_active
115
+ ON geneva_drive_step_executions (active_unique_key);
116
+ SQL
117
+ elsif adapter.include?("sqlite")
118
+ execute <<-SQL
119
+ DROP INDEX IF EXISTS index_geneva_drive_step_executions_one_active;
120
+ SQL
121
+ execute <<-SQL
122
+ CREATE UNIQUE INDEX index_geneva_drive_step_executions_one_active
123
+ ON geneva_drive_step_executions (workflow_id)
124
+ WHERE state IN ('scheduled', 'in_progress');
125
+ SQL
126
+ end
127
+ end
128
+ end
@@ -0,0 +1,136 @@
1
+ # frozen_string_literal: true
2
+
3
+ # license: LGPLv3.0
4
+
5
+ class AddChainedStepExecutionsToGenevaDriveStepExecutions < ActiveRecord::Migration[7.2]
6
+ include GenevaDrive::MigrationHelpers
7
+
8
+ def change
9
+ # Add continues_from_id to link chained step executions.
10
+ # Match the primary key type (bigint or uuid) of the step_executions table.
11
+ # No foreign key constraint — SQLite rewrites the table on add_foreign_key,
12
+ # which can destroy data.
13
+ add_column :geneva_drive_step_executions, :continues_from_id, geneva_drive_key_type
14
+ add_index :geneva_drive_step_executions, :continues_from_id
15
+
16
+ # Remove completed_iterations - it's unnecessary bookkeeping
17
+ remove_column :geneva_drive_step_executions, :completed_iterations, :integer
18
+
19
+ # Remove 'suspended' state - executions now complete and spawn successors
20
+ reversible do |direction|
21
+ direction.up do
22
+ # Convert any suspended executions to completed
23
+ execute <<-SQL
24
+ UPDATE geneva_drive_step_executions
25
+ SET state = 'completed'
26
+ WHERE state = 'suspended';
27
+ SQL
28
+
29
+ # Update unique constraint: only scheduled and in_progress are active
30
+ update_unique_index_without_suspended
31
+ end
32
+
33
+ direction.down do
34
+ restore_suspended_unique_index
35
+ end
36
+ end
37
+ end
38
+
39
+ private
40
+
41
+ def update_unique_index_without_suspended
42
+ adapter = connection.adapter_name.downcase
43
+
44
+ if adapter.include?("postgresql")
45
+ execute <<-SQL
46
+ DROP INDEX IF EXISTS index_geneva_drive_step_executions_one_active;
47
+ SQL
48
+ execute <<-SQL
49
+ CREATE UNIQUE INDEX index_geneva_drive_step_executions_one_active
50
+ ON geneva_drive_step_executions (workflow_id)
51
+ WHERE state IN ('scheduled', 'in_progress');
52
+ SQL
53
+ elsif adapter.include?("mysql")
54
+ execute <<-SQL
55
+ DROP INDEX index_geneva_drive_step_executions_one_active
56
+ ON geneva_drive_step_executions;
57
+ SQL
58
+ execute <<-SQL
59
+ ALTER TABLE geneva_drive_step_executions
60
+ DROP COLUMN active_unique_key;
61
+ SQL
62
+ execute <<-SQL
63
+ ALTER TABLE geneva_drive_step_executions
64
+ ADD COLUMN active_unique_key VARCHAR(767)
65
+ AS (
66
+ CASE
67
+ WHEN state IN ('scheduled', 'in_progress')
68
+ THEN CAST(workflow_id AS CHAR)
69
+ ELSE NULL
70
+ END
71
+ ) STORED;
72
+ SQL
73
+ execute <<-SQL
74
+ CREATE UNIQUE INDEX index_geneva_drive_step_executions_one_active
75
+ ON geneva_drive_step_executions (active_unique_key);
76
+ SQL
77
+ elsif adapter.include?("sqlite")
78
+ execute <<-SQL
79
+ DROP INDEX IF EXISTS index_geneva_drive_step_executions_one_active;
80
+ SQL
81
+ execute <<-SQL
82
+ CREATE UNIQUE INDEX index_geneva_drive_step_executions_one_active
83
+ ON geneva_drive_step_executions (workflow_id)
84
+ WHERE state IN ('scheduled', 'in_progress');
85
+ SQL
86
+ end
87
+ end
88
+
89
+ def restore_suspended_unique_index
90
+ adapter = connection.adapter_name.downcase
91
+
92
+ if adapter.include?("postgresql")
93
+ execute <<-SQL
94
+ DROP INDEX IF EXISTS index_geneva_drive_step_executions_one_active;
95
+ SQL
96
+ execute <<-SQL
97
+ CREATE UNIQUE INDEX index_geneva_drive_step_executions_one_active
98
+ ON geneva_drive_step_executions (workflow_id)
99
+ WHERE state IN ('scheduled', 'in_progress', 'suspended');
100
+ SQL
101
+ elsif adapter.include?("mysql")
102
+ execute <<-SQL
103
+ DROP INDEX index_geneva_drive_step_executions_one_active
104
+ ON geneva_drive_step_executions;
105
+ SQL
106
+ execute <<-SQL
107
+ ALTER TABLE geneva_drive_step_executions
108
+ DROP COLUMN active_unique_key;
109
+ SQL
110
+ execute <<-SQL
111
+ ALTER TABLE geneva_drive_step_executions
112
+ ADD COLUMN active_unique_key VARCHAR(767)
113
+ AS (
114
+ CASE
115
+ WHEN state IN ('scheduled', 'in_progress', 'suspended')
116
+ THEN CAST(workflow_id AS CHAR)
117
+ ELSE NULL
118
+ END
119
+ ) STORED;
120
+ SQL
121
+ execute <<-SQL
122
+ CREATE UNIQUE INDEX index_geneva_drive_step_executions_one_active
123
+ ON geneva_drive_step_executions (active_unique_key);
124
+ SQL
125
+ elsif adapter.include?("sqlite")
126
+ execute <<-SQL
127
+ DROP INDEX IF EXISTS index_geneva_drive_step_executions_one_active;
128
+ SQL
129
+ execute <<-SQL
130
+ CREATE UNIQUE INDEX index_geneva_drive_step_executions_one_active
131
+ ON geneva_drive_step_executions (workflow_id)
132
+ WHERE state IN ('scheduled', 'in_progress', 'suspended');
133
+ SQL
134
+ end
135
+ end
136
+ end
@@ -0,0 +1,75 @@
1
+ # This file is auto-generated from the current state of the database. Instead
2
+ # of editing this file, please use the migrations feature of Active Record to
3
+ # incrementally modify your database, and then regenerate this schema definition.
4
+ #
5
+ # This file is the source Rails uses to define your schema when running `bin/rails
6
+ # db:schema:load`. When creating a new database, `bin/rails db:schema:load` tends to
7
+ # be faster and is potentially less error prone than running all of your
8
+ # migrations from scratch. Old migrations may fail to apply correctly if those
9
+ # migrations use external dependencies or application code.
10
+ #
11
+ # It's strongly recommended that you check this file into your version control system.
12
+
13
+ ActiveRecord::Schema[8.1].define(version: 2026_02_17_112954) do
14
+ # These are extensions that must be enabled in order to support this database
15
+ enable_extension "pg_catalog.plpgsql"
16
+
17
+ create_table "geneva_drive_step_executions", force: :cascade do |t|
18
+ t.datetime "canceled_at"
19
+ t.datetime "completed_at"
20
+ t.bigint "continues_from_id"
21
+ t.datetime "created_at", null: false
22
+ t.jsonb "cursor"
23
+ t.text "error_backtrace"
24
+ t.string "error_class_name"
25
+ t.text "error_message"
26
+ t.datetime "failed_at"
27
+ t.datetime "finished_at"
28
+ t.string "job_id"
29
+ t.string "outcome"
30
+ t.datetime "scheduled_for", null: false
31
+ t.datetime "skipped_at"
32
+ t.datetime "started_at"
33
+ t.string "state", default: "scheduled", null: false
34
+ t.string "step_name", null: false
35
+ t.datetime "updated_at", null: false
36
+ t.bigint "workflow_id", null: false
37
+ t.index ["continues_from_id"], name: "index_geneva_drive_step_executions_on_continues_from_id"
38
+ t.index ["finished_at"], name: "index_geneva_drive_step_executions_on_finished_at"
39
+ t.index ["scheduled_for"], name: "index_geneva_drive_step_executions_on_scheduled_for"
40
+ t.index ["state", "scheduled_for"], name: "index_geneva_drive_step_executions_scheduled"
41
+ t.index ["state"], name: "index_geneva_drive_step_executions_on_state"
42
+ t.index ["workflow_id", "created_at"], name: "idx_on_workflow_id_created_at_af16a14fb2"
43
+ t.index ["workflow_id", "state"], name: "index_geneva_drive_step_executions_on_workflow_id_and_state"
44
+ t.index ["workflow_id"], name: "index_geneva_drive_step_executions_on_workflow_id"
45
+ t.index ["workflow_id"], name: "index_geneva_drive_step_executions_one_active", unique: true, where: "((state)::text = ANY ((ARRAY['scheduled'::character varying, 'in_progress'::character varying])::text[]))"
46
+ end
47
+
48
+ create_table "geneva_drive_workflows", force: :cascade do |t|
49
+ t.boolean "allow_multiple", default: false, null: false
50
+ t.datetime "created_at", null: false
51
+ t.string "current_step_name"
52
+ t.bigint "hero_id", null: false
53
+ t.string "hero_type", null: false
54
+ t.string "next_step_name"
55
+ t.datetime "started_at"
56
+ t.string "state", default: "ready", null: false
57
+ t.datetime "transitioned_at"
58
+ t.string "type", null: false
59
+ t.datetime "updated_at", null: false
60
+ t.index ["hero_type", "hero_id"], name: "index_geneva_drive_workflows_on_hero_type_and_hero_id"
61
+ t.index ["state"], name: "index_geneva_drive_workflows_on_state"
62
+ t.index ["type", "hero_type", "hero_id"], name: "index_geneva_drive_workflows_unique_ongoing", unique: true, where: "(((state)::text <> ALL ((ARRAY['finished'::character varying, 'canceled'::character varying])::text[])) AND (allow_multiple = false))"
63
+ t.index ["type"], name: "index_geneva_drive_workflows_on_type"
64
+ end
65
+
66
+ create_table "users", force: :cascade do |t|
67
+ t.boolean "active", default: true
68
+ t.datetime "created_at", null: false
69
+ t.string "email", null: false
70
+ t.string "name"
71
+ t.datetime "updated_at", null: false
72
+ end
73
+
74
+ add_foreign_key "geneva_drive_step_executions", "geneva_drive_workflows", column: "workflow_id", on_delete: :cascade
75
+ end