active_durable 0.6.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 (32) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +99 -1
  3. data/README.md +238 -12
  4. data/app/controllers/active_durable/executions_controller.rb +2 -1
  5. data/app/helpers/active_durable/dashboard_helper.rb +12 -4
  6. data/app/views/active_durable/executions/index.html.erb +1 -1
  7. data/app/views/active_durable/executions/show.html.erb +9 -6
  8. data/app/views/layouts/active_durable/application.html.erb +3 -3
  9. data/lib/active_durable/configuration.rb +30 -0
  10. data/lib/active_durable/errors.rb +53 -8
  11. data/lib/active_durable/execution.rb +22 -2
  12. data/lib/active_durable/flow.rb +147 -31
  13. data/lib/active_durable/flow_parallel.rb +38 -12
  14. data/lib/active_durable/notebook.rb +17 -4
  15. data/lib/active_durable/operations.rb +55 -17
  16. data/lib/active_durable/pruner.rb +19 -4
  17. data/lib/active_durable/record.rb +9 -1
  18. data/lib/active_durable/runner.rb +45 -10
  19. data/lib/active_durable/serializer.rb +42 -5
  20. data/lib/active_durable/signal_record.rb +18 -2
  21. data/lib/active_durable/step.rb +20 -0
  22. data/lib/active_durable/sweeper.rb +1 -1
  23. data/lib/active_durable/testing.rb +18 -13
  24. data/lib/active_durable/version.rb +1 -1
  25. data/lib/active_durable.rb +79 -11
  26. data/lib/generators/active_durable/install/templates/create_active_durable_tables.rb.tt +15 -6
  27. data/lib/generators/active_durable/upgrade/templates/add_active_durable_context.rb.tt +19 -0
  28. data/lib/generators/active_durable/upgrade/templates/add_active_durable_continued_as.rb.tt +21 -0
  29. data/lib/generators/active_durable/upgrade/templates/add_active_durable_signal_name.rb.tt +22 -0
  30. data/lib/generators/active_durable/upgrade/templates/make_active_durable_ids_case_sensitive.rb.tt +72 -0
  31. data/lib/generators/active_durable/upgrade/upgrade_generator.rb +4 -1
  32. metadata +5 -1
@@ -4,33 +4,37 @@ class CreateActiveDurableTables < ActiveRecord::Migration<%= migration_version %
4
4
  def change
5
5
  # One row per saga run. It is also the buzón: create it in the same transaction as your record.
6
6
  create_table :durable_executions, id: false do |t|
7
- t.string :id, null: false, primary_key: true
7
+ t.string :id, null: false, primary_key: true, **exact
8
8
  t.string :recipe, null: false
9
9
  t.integer :recipe_version, null: false, default: 1
10
10
  t.string :status, null: false
11
11
  t.boolean :compensating, null: false, default: false
12
12
  t.column :input, json_type
13
+ t.column :context, json_type
13
14
  t.column :output, json_type
14
15
  t.column :error, json_type
15
16
  t.datetime :wake_at, precision: 6
16
17
  t.datetime :locked_until, precision: 6
17
18
  t.string :lease_token
18
- t.string :forked_from
19
+ t.string :forked_from, **exact
20
+ t.string :continued_as, **exact
19
21
  t.timestamps precision: 6
20
22
  end
21
23
  add_index :durable_executions, %i[status wake_at]
22
24
  add_index :durable_executions, %i[status updated_at]
23
25
  add_index :durable_executions, %i[recipe status]
24
26
  add_index :durable_executions, :forked_from
27
+ add_index :durable_executions, :continued_as
25
28
 
26
29
  # The notebook: one row per step (and per undo) of an execution.
27
30
  create_table :durable_steps do |t|
28
- t.string :execution_id, null: false
29
- t.string :name, null: false
31
+ t.string :execution_id, null: false, **exact
32
+ t.string :name, null: false, **exact
30
33
  t.string :kind, null: false
31
34
  t.integer :position
32
35
  t.string :status, null: false
33
36
  t.integer :attempts, null: false, default: 0
37
+ t.string :signal_name, **exact
34
38
  t.column :result, json_type
35
39
  t.column :error, json_type
36
40
  t.datetime :wake_at, precision: 6
@@ -42,8 +46,8 @@ class CreateActiveDurableTables < ActiveRecord::Migration<%= migration_version %
42
46
 
43
47
  # Signals for flow.wait_for. They can arrive before the saga starts waiting.
44
48
  create_table :durable_signals do |t|
45
- t.string :execution_id, null: false
46
- t.string :name, null: false
49
+ t.string :execution_id, null: false, **exact
50
+ t.string :name, null: false, **exact
47
51
  t.column :payload, json_type
48
52
  t.datetime :consumed_at, precision: 6
49
53
  t.datetime :created_at, null: false, precision: 6
@@ -58,4 +62,9 @@ class CreateActiveDurableTables < ActiveRecord::Migration<%= migration_version %
58
62
  def json_type
59
63
  connection.adapter_name.downcase.include?("postg") ? :jsonb : :json
60
64
  end
65
+
66
+ # MySQL compares strings ignoring case and accents by default; ids and step names must match exactly.
67
+ def exact
68
+ connection.adapter_name.match?(/mysql|trilogy/i) ? { collation: "utf8mb4_bin" } : {}
69
+ end
61
70
  end
@@ -0,0 +1,19 @@
1
+ # frozen_string_literal: true
2
+
3
+ # ActiveDurable 0.8: the context an execution started with (config.context), given back to it on every run.
4
+ class AddActiveDurableContext < ActiveRecord::Migration<%= migration_version %>
5
+ def up
6
+ add_column :durable_executions, :context, json_type unless column_exists?(:durable_executions, :context)
7
+ end
8
+
9
+ def down
10
+ remove_column :durable_executions, :context if column_exists?(:durable_executions, :context)
11
+ end
12
+
13
+ private
14
+
15
+ # jsonb on PostgreSQL, json elsewhere, like the other JSON columns.
16
+ def json_type
17
+ connection.adapter_name.downcase.include?("postg") ? :jsonb : :json
18
+ end
19
+ end
@@ -0,0 +1,21 @@
1
+ # frozen_string_literal: true
2
+
3
+ # ActiveDurable 0.8: flow.continue_as_new closes an execution and points it to the one that carries on.
4
+ class AddActiveDurableContinuedAs < ActiveRecord::Migration<%= migration_version %>
5
+ def up
6
+ add_column :durable_executions, :continued_as, :string, **exact unless column_exists?(:durable_executions, :continued_as)
7
+ add_index :durable_executions, :continued_as unless index_exists?(:durable_executions, :continued_as)
8
+ end
9
+
10
+ def down
11
+ remove_index :durable_executions, :continued_as if index_exists?(:durable_executions, :continued_as)
12
+ remove_column :durable_executions, :continued_as if column_exists?(:durable_executions, :continued_as)
13
+ end
14
+
15
+ private
16
+
17
+ # MySQL compares strings ignoring case and accents by default; ids must match exactly.
18
+ def exact
19
+ connection.adapter_name.match?(/mysql|trilogy/i) ? { collation: "utf8mb4_bin" } : {}
20
+ end
21
+ end
@@ -0,0 +1,22 @@
1
+ # frozen_string_literal: true
2
+
3
+ # ActiveDurable 0.8: flow.wait_for(name, signal: ...) writes down which signal a step waits for, when it is not the
4
+ # step's own name.
5
+ class AddActiveDurableSignalName < ActiveRecord::Migration<%= migration_version %>
6
+ def up
7
+ return if column_exists?(:durable_steps, :signal_name)
8
+
9
+ add_column :durable_steps, :signal_name, :string, **exact
10
+ end
11
+
12
+ def down
13
+ remove_column :durable_steps, :signal_name if column_exists?(:durable_steps, :signal_name)
14
+ end
15
+
16
+ private
17
+
18
+ # MySQL compares strings ignoring case and accents by default; signal names must match exactly.
19
+ def exact
20
+ connection.adapter_name.match?(/mysql|trilogy/i) ? { collation: "utf8mb4_bin" } : {}
21
+ end
22
+ end
@@ -0,0 +1,72 @@
1
+ # frozen_string_literal: true
2
+
3
+ # ActiveDurable 0.7: MySQL compared ids and step names ignoring case and accents, so "order-abc" and "order-ÁBC"
4
+ # shared one saga. These columns now compare byte by byte, as on PostgreSQL and SQLite. Elsewhere it does nothing.
5
+ #
6
+ # On MySQL it rebuilds the three tables and blocks writes to them while it runs: run it at a quiet time, or prune
7
+ # first. MySQL cannot roll DDL back, so if it stops halfway, run it again: it finishes the columns and puts the
8
+ # foreign keys back.
9
+ class MakeActiveDurableIdsCaseSensitive < ActiveRecord::Migration<%= migration_version %>
10
+ COLUMNS = {
11
+ durable_executions: %w[id forked_from],
12
+ durable_steps: %w[execution_id name],
13
+ durable_signals: %w[execution_id name]
14
+ }.freeze
15
+
16
+ def up
17
+ change_collation { |_table| "utf8mb4_bin" }
18
+ end
19
+
20
+ def down
21
+ change_collation { |table| table_collation(table) }
22
+ end
23
+
24
+ private
25
+
26
+ def change_collation
27
+ return unless connection.adapter_name.match?(/mysql|trilogy/i)
28
+
29
+ changes = COLUMNS.flat_map do |table, names|
30
+ wanted = yield(table)
31
+ connection.columns(table).select { |column| names.include?(column.name) && column.collation != wanted }
32
+ .map { |column| [table, column, wanted] }
33
+ end
34
+
35
+ # Both ends of a foreign key must share a collation, so the keys come off while the columns change.
36
+ on_delete = remove_foreign_keys if changes.any?
37
+ changes.each do |table, column, collation|
38
+ change_column table, column.name, :string, limit: column.limit, null: column.null, collation: collation
39
+ end
40
+ restore_foreign_keys(on_delete || {})
41
+ end
42
+
43
+ FOREIGN_KEY_TABLES = %i[durable_steps durable_signals].freeze
44
+
45
+ def foreign_key(table)
46
+ connection.foreign_keys(table).find { |key| key.column == "execution_id" }
47
+ end
48
+
49
+ def remove_foreign_keys
50
+ FOREIGN_KEY_TABLES.each_with_object({}) do |table, on_delete|
51
+ key = foreign_key(table)
52
+ next unless key
53
+
54
+ on_delete[table] = key.options[:on_delete]
55
+ remove_foreign_key table, column: :execution_id
56
+ end
57
+ end
58
+
59
+ # Also puts back keys that an earlier run, stopped halfway, left off.
60
+ def restore_foreign_keys(on_delete)
61
+ FOREIGN_KEY_TABLES.each do |table|
62
+ next if foreign_key(table)
63
+
64
+ add_foreign_key table, :durable_executions, column: :execution_id, on_delete: on_delete.fetch(table, :cascade)
65
+ end
66
+ end
67
+
68
+ def table_collation(table)
69
+ select_value("SELECT TABLE_COLLATION FROM information_schema.TABLES " \
70
+ "WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = #{connection.quote(table.to_s)}")
71
+ end
72
+ end
@@ -16,7 +16,10 @@ module ActiveDurable
16
16
  desc "Adds the migrations that newer ActiveDurable versions need. Run it after updating the gem."
17
17
 
18
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
19
+ MIGRATIONS = %w[
20
+ add_active_durable_prune_index make_active_durable_ids_case_sensitive add_active_durable_context
21
+ add_active_durable_signal_name add_active_durable_continued_as
22
+ ].freeze
20
23
 
21
24
  # @api private
22
25
  def create_migration_files
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: active_durable
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.6.0
4
+ version: 0.8.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - William Romero
@@ -120,7 +120,11 @@ files:
120
120
  - lib/active_durable/version.rb
121
121
  - lib/generators/active_durable/install/install_generator.rb
122
122
  - lib/generators/active_durable/install/templates/create_active_durable_tables.rb.tt
123
+ - lib/generators/active_durable/upgrade/templates/add_active_durable_context.rb.tt
124
+ - lib/generators/active_durable/upgrade/templates/add_active_durable_continued_as.rb.tt
123
125
  - lib/generators/active_durable/upgrade/templates/add_active_durable_prune_index.rb.tt
126
+ - lib/generators/active_durable/upgrade/templates/add_active_durable_signal_name.rb.tt
127
+ - lib/generators/active_durable/upgrade/templates/make_active_durable_ids_case_sensitive.rb.tt
124
128
  - lib/generators/active_durable/upgrade/upgrade_generator.rb
125
129
  - lib/tasks/active_durable.rake
126
130
  - sig/active_durable.rbs