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.
- checksums.yaml +4 -4
- data/CHANGELOG.md +7 -0
- data/MANUAL.md +84 -1
- data/Rakefile +14 -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/geneva_drive/executor.rb +18 -0
- data/lib/geneva_drive/json_column.rb +72 -0
- data/lib/geneva_drive/migration_helpers.rb +27 -0
- 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 +170 -2
- data/lib/geneva_drive.rb +2 -0
- data/test/dummy/config/database.yml +40 -5
- data/test/dummy/config/initializers/geneva_drive.rb +46 -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/test_helper.rb +51 -13
- data/test/workflow/removed_step_test.rb +294 -0
- data/test/workflow/resume_at_test.rb +170 -0
- 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
|