durable_flow 0.1.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.
@@ -0,0 +1,146 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "rails"
4
+ require "active_job"
5
+ require "active_job/continuation"
6
+ require "active_record"
7
+ require "active_support/core_ext/numeric/time"
8
+ require "active_support/core_ext/object/blank"
9
+ require "active_support/core_ext/module/attribute_accessors"
10
+ require "securerandom"
11
+
12
+ require "durable_flow/version"
13
+ require "durable_flow/errors"
14
+ require "durable_flow/serializer"
15
+ require "durable_flow/schema"
16
+ require "durable_flow/live"
17
+ require "durable_flow/workflow_logger"
18
+ require "durable_flow/workflow_timeline"
19
+ require "durable_flow/models/application_record"
20
+ require "durable_flow/models/workflow_run"
21
+ require "durable_flow/models/workflow_step"
22
+ require "durable_flow/models/workflow_event"
23
+ require "durable_flow/models/workflow_wait"
24
+ require "durable_flow/models/workflow_log"
25
+ require "durable_flow/dispatcher"
26
+ require "durable_flow/event_subscriber"
27
+ require "durable_flow/step_proxy"
28
+ require "durable_flow/workflow"
29
+ require "durable_flow/engine"
30
+ require "durable_flow/railtie"
31
+
32
+ module DurableFlow
33
+ NOOP_LIVE_BROADCASTER = ->(_change) {}
34
+
35
+ mattr_accessor :event_subscriber, default: nil
36
+ mattr_accessor :execution_lock_ttl, default: 10.minutes
37
+ mattr_accessor :live_broadcaster, default: NOOP_LIVE_BROADCASTER
38
+ mattr_accessor :live_subscribers, default: []
39
+
40
+ WORKFLOW_COMPLETED_EVENT = "durable_flow.workflow.completed"
41
+ WORKFLOW_FAILED_EVENT = "durable_flow.workflow.failed"
42
+ IGNORED_EVENT_NAMESPACES = %w[
43
+ action_controller
44
+ action_mailbox
45
+ action_mailer
46
+ action_text
47
+ action_view
48
+ active_job
49
+ active_record
50
+ active_storage
51
+ active_support
52
+ rails
53
+ ].freeze
54
+
55
+ class << self
56
+ def subscribe_to_rails_events!
57
+ return event_subscriber if event_subscriber
58
+ return unless defined?(Rails) && Rails.respond_to?(:event)
59
+
60
+ self.event_subscriber = EventSubscriber.new
61
+ Rails.event.subscribe(event_subscriber) { |event| record_event?(event[:name]) }
62
+ event_subscriber
63
+ end
64
+
65
+ def unsubscribe_from_rails_events!
66
+ return unless event_subscriber
67
+ return unless defined?(Rails) && Rails.respond_to?(:event)
68
+
69
+ Rails.event.unsubscribe(event_subscriber)
70
+ self.event_subscriber = nil
71
+ end
72
+
73
+ def notify(name, payload = nil, **kwargs)
74
+ if defined?(Rails) && Rails.respond_to?(:event)
75
+ payload ? Rails.event.notify(name, payload, **kwargs) : Rails.event.notify(name, **kwargs)
76
+ else
77
+ event = {
78
+ name: name.to_s,
79
+ payload: payload || kwargs,
80
+ tags: {},
81
+ context: {},
82
+ timestamp: Process.clock_gettime(Process::CLOCK_REALTIME, :nanosecond),
83
+ }
84
+ EventSubscriber.new.emit(event)
85
+ end
86
+ end
87
+
88
+ def broadcast_change(change)
89
+ broadcast_live_change(live_broadcaster, change)
90
+ live_subscribers.each { |subscriber| broadcast_live_change(subscriber, change) }
91
+ change
92
+ end
93
+
94
+ def on_change(&block)
95
+ raise ArgumentError, "Provide a block" unless block
96
+
97
+ self.live_subscribers += [ block ]
98
+ block
99
+ end
100
+
101
+ def unsubscribe_from_changes(subscriber)
102
+ self.live_subscribers -= [ subscriber ]
103
+ end
104
+
105
+ def reset_live_broadcasters!
106
+ self.live_broadcaster = NOOP_LIVE_BROADCASTER
107
+ self.live_subscribers = []
108
+ end
109
+
110
+ def database_ready?
111
+ return false unless defined?(ActiveRecord::Base)
112
+ return false unless ActiveRecord::Base.connected?
113
+
114
+ connection = ActiveRecord::Base.connection
115
+ %w[
116
+ durable_flow_workflow_runs
117
+ durable_flow_workflow_steps
118
+ durable_flow_workflow_events
119
+ durable_flow_workflow_waits
120
+ durable_flow_workflow_logs
121
+ ].all? { |table| connection.data_source_exists?(table) }
122
+ rescue ActiveRecord::ActiveRecordError
123
+ false
124
+ end
125
+
126
+ def record_event?(name)
127
+ return false if Fiber[:durable_flow_recording_event]
128
+
129
+ name = name.to_s
130
+ return true if name.start_with?("durable_flow.")
131
+
132
+ IGNORED_EVENT_NAMESPACES.none? do |namespace|
133
+ name == namespace || name.start_with?("#{namespace}.") || name.end_with?(".#{namespace}")
134
+ end
135
+ end
136
+
137
+ private
138
+ def broadcast_live_change(callable, change)
139
+ callable.call(change)
140
+ rescue StandardError => error
141
+ if defined?(Rails) && Rails.respond_to?(:error)
142
+ Rails.error.report(error, handled: true)
143
+ end
144
+ end
145
+ end
146
+ end
@@ -0,0 +1,22 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "rails/generators"
4
+ require "rails/generators/active_record"
5
+
6
+ module DurableFlow
7
+ module Generators
8
+ class InstallGenerator < Rails::Generators::Base
9
+ include ActiveRecord::Generators::Migration
10
+
11
+ source_root File.expand_path("templates", __dir__)
12
+
13
+ def copy_migration
14
+ migration_template "create_durable_flow_tables.rb", "db/migrate/create_durable_flow_tables.rb"
15
+ end
16
+
17
+ def self.next_migration_number(dirname)
18
+ ActiveRecord::Generators::Base.next_migration_number(dirname)
19
+ end
20
+ end
21
+ end
22
+ end
@@ -0,0 +1,93 @@
1
+ # frozen_string_literal: true
2
+
3
+ class CreateDurableFlowTables < ActiveRecord::Migration[8.0]
4
+ def change
5
+ create_table :durable_flow_workflow_runs do |t|
6
+ t.string :run_id, null: false
7
+ t.string :job_id, null: false
8
+ t.string :workflow_class, null: false
9
+ t.string :status, null: false, default: "enqueued"
10
+ t.string :queue_name
11
+ t.integer :priority
12
+ t.json :arguments
13
+ t.json :serialized_job
14
+ t.json :last_error
15
+ t.datetime :started_at
16
+ t.datetime :interrupted_at
17
+ t.datetime :completed_at
18
+ t.datetime :failed_at
19
+ t.string :execution_locked_by
20
+ t.datetime :execution_locked_at
21
+ t.datetime :execution_lock_expires_at
22
+ t.timestamps
23
+ end
24
+
25
+ add_index :durable_flow_workflow_runs, :run_id, unique: true
26
+ add_index :durable_flow_workflow_runs, [ :workflow_class, :status ], name: "idx_durable_flow_runs_on_class_status"
27
+ add_index :durable_flow_workflow_runs, :execution_lock_expires_at, name: "idx_durable_flow_runs_on_lock_expiry"
28
+
29
+ create_table :durable_flow_workflow_steps do |t|
30
+ t.references :workflow_run, null: false, index: false
31
+ t.string :name, null: false
32
+ t.string :status, null: false, default: "pending"
33
+ t.integer :attempts, null: false, default: 0
34
+ t.json :result
35
+ t.json :metadata
36
+ t.datetime :started_at
37
+ t.datetime :completed_at
38
+ t.timestamps
39
+ end
40
+
41
+ add_index :durable_flow_workflow_steps,
42
+ [ :workflow_run_id, :name ],
43
+ unique: true,
44
+ name: "idx_durable_flow_steps_on_run_and_name"
45
+
46
+ create_table :durable_flow_workflow_events do |t|
47
+ t.string :name, null: false
48
+ t.json :payload
49
+ t.json :tags
50
+ t.json :context
51
+ t.json :source_location
52
+ t.datetime :occurred_at, null: false
53
+ t.timestamps
54
+ end
55
+
56
+ add_index :durable_flow_workflow_events, [ :name, :occurred_at ], name: "idx_durable_flow_events_on_name_time"
57
+
58
+ create_table :durable_flow_workflow_waits do |t|
59
+ t.references :workflow_run, null: false, index: false
60
+ t.references :workflow_step, null: false, index: false
61
+ t.references :workflow_event, index: false
62
+ t.string :event_name, null: false
63
+ t.string :status, null: false, default: "pending"
64
+ t.json :match
65
+ t.datetime :timeout_at
66
+ t.timestamps
67
+ end
68
+
69
+ add_index :durable_flow_workflow_waits,
70
+ [ :workflow_run_id, :workflow_step_id ],
71
+ unique: true,
72
+ name: "idx_durable_flow_waits_on_run_and_step"
73
+ add_index :durable_flow_workflow_waits,
74
+ [ :event_name, :status ],
75
+ name: "idx_durable_flow_waits_on_event_status"
76
+
77
+ create_table :durable_flow_workflow_logs do |t|
78
+ t.references :workflow_run, null: false, index: false
79
+ t.references :workflow_step, index: false
80
+ t.string :level, null: false
81
+ t.string :message, null: false
82
+ t.json :data
83
+ t.timestamps
84
+ end
85
+
86
+ add_index :durable_flow_workflow_logs,
87
+ [ :workflow_run_id, :created_at ],
88
+ name: "idx_durable_flow_logs_on_run_time"
89
+ add_index :durable_flow_workflow_logs,
90
+ [ :workflow_step_id, :created_at ],
91
+ name: "idx_durable_flow_logs_on_step_time"
92
+ end
93
+ end
metadata ADDED
@@ -0,0 +1,216 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: durable_flow
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.1.0
5
+ platform: ruby
6
+ authors:
7
+ - DurableFlow contributors
8
+ bindir: bin
9
+ cert_chain: []
10
+ date: 1980-01-02 00:00:00.000000000 Z
11
+ dependencies:
12
+ - !ruby/object:Gem::Dependency
13
+ name: actionpack
14
+ requirement: !ruby/object:Gem::Requirement
15
+ requirements:
16
+ - - ">="
17
+ - !ruby/object:Gem::Version
18
+ version: 8.1.0
19
+ - - "<"
20
+ - !ruby/object:Gem::Version
21
+ version: '9.0'
22
+ type: :runtime
23
+ prerelease: false
24
+ version_requirements: !ruby/object:Gem::Requirement
25
+ requirements:
26
+ - - ">="
27
+ - !ruby/object:Gem::Version
28
+ version: 8.1.0
29
+ - - "<"
30
+ - !ruby/object:Gem::Version
31
+ version: '9.0'
32
+ - !ruby/object:Gem::Dependency
33
+ name: activejob
34
+ requirement: !ruby/object:Gem::Requirement
35
+ requirements:
36
+ - - ">="
37
+ - !ruby/object:Gem::Version
38
+ version: 8.1.0
39
+ - - "<"
40
+ - !ruby/object:Gem::Version
41
+ version: '9.0'
42
+ type: :runtime
43
+ prerelease: false
44
+ version_requirements: !ruby/object:Gem::Requirement
45
+ requirements:
46
+ - - ">="
47
+ - !ruby/object:Gem::Version
48
+ version: 8.1.0
49
+ - - "<"
50
+ - !ruby/object:Gem::Version
51
+ version: '9.0'
52
+ - !ruby/object:Gem::Dependency
53
+ name: activerecord
54
+ requirement: !ruby/object:Gem::Requirement
55
+ requirements:
56
+ - - ">="
57
+ - !ruby/object:Gem::Version
58
+ version: 8.1.0
59
+ - - "<"
60
+ - !ruby/object:Gem::Version
61
+ version: '9.0'
62
+ type: :runtime
63
+ prerelease: false
64
+ version_requirements: !ruby/object:Gem::Requirement
65
+ requirements:
66
+ - - ">="
67
+ - !ruby/object:Gem::Version
68
+ version: 8.1.0
69
+ - - "<"
70
+ - !ruby/object:Gem::Version
71
+ version: '9.0'
72
+ - !ruby/object:Gem::Dependency
73
+ name: activesupport
74
+ requirement: !ruby/object:Gem::Requirement
75
+ requirements:
76
+ - - ">="
77
+ - !ruby/object:Gem::Version
78
+ version: 8.1.0
79
+ - - "<"
80
+ - !ruby/object:Gem::Version
81
+ version: '9.0'
82
+ type: :runtime
83
+ prerelease: false
84
+ version_requirements: !ruby/object:Gem::Requirement
85
+ requirements:
86
+ - - ">="
87
+ - !ruby/object:Gem::Version
88
+ version: 8.1.0
89
+ - - "<"
90
+ - !ruby/object:Gem::Version
91
+ version: '9.0'
92
+ - !ruby/object:Gem::Dependency
93
+ name: railties
94
+ requirement: !ruby/object:Gem::Requirement
95
+ requirements:
96
+ - - ">="
97
+ - !ruby/object:Gem::Version
98
+ version: 8.1.0
99
+ - - "<"
100
+ - !ruby/object:Gem::Version
101
+ version: '9.0'
102
+ type: :runtime
103
+ prerelease: false
104
+ version_requirements: !ruby/object:Gem::Requirement
105
+ requirements:
106
+ - - ">="
107
+ - !ruby/object:Gem::Version
108
+ version: 8.1.0
109
+ - - "<"
110
+ - !ruby/object:Gem::Version
111
+ version: '9.0'
112
+ - !ruby/object:Gem::Dependency
113
+ name: minitest
114
+ requirement: !ruby/object:Gem::Requirement
115
+ requirements:
116
+ - - "~>"
117
+ - !ruby/object:Gem::Version
118
+ version: '5.20'
119
+ type: :development
120
+ prerelease: false
121
+ version_requirements: !ruby/object:Gem::Requirement
122
+ requirements:
123
+ - - "~>"
124
+ - !ruby/object:Gem::Version
125
+ version: '5.20'
126
+ - !ruby/object:Gem::Dependency
127
+ name: solid_queue
128
+ requirement: !ruby/object:Gem::Requirement
129
+ requirements:
130
+ - - '='
131
+ - !ruby/object:Gem::Version
132
+ version: 1.1.2
133
+ type: :development
134
+ prerelease: false
135
+ version_requirements: !ruby/object:Gem::Requirement
136
+ requirements:
137
+ - - '='
138
+ - !ruby/object:Gem::Version
139
+ version: 1.1.2
140
+ - !ruby/object:Gem::Dependency
141
+ name: sqlite3
142
+ requirement: !ruby/object:Gem::Requirement
143
+ requirements:
144
+ - - "~>"
145
+ - !ruby/object:Gem::Version
146
+ version: '2.0'
147
+ type: :development
148
+ prerelease: false
149
+ version_requirements: !ruby/object:Gem::Requirement
150
+ requirements:
151
+ - - "~>"
152
+ - !ruby/object:Gem::Version
153
+ version: '2.0'
154
+ description: An Inngest-style durable workflow runtime built on ActiveJob::Continuable,
155
+ Active Record, and Rails.event.
156
+ email:
157
+ - dev@example.com
158
+ executables: []
159
+ extensions: []
160
+ extra_rdoc_files: []
161
+ files:
162
+ - MIT-LICENSE
163
+ - README.md
164
+ - app/controllers/durable_flow/workflow_runs_controller.rb
165
+ - app/views/durable_flow/workflow_runs/index.html.erb
166
+ - app/views/durable_flow/workflow_runs/show.html.erb
167
+ - app/views/layouts/durable_flow/application.html.erb
168
+ - config/routes.rb
169
+ - lib/durable_flow.rb
170
+ - lib/durable_flow/dispatcher.rb
171
+ - lib/durable_flow/engine.rb
172
+ - lib/durable_flow/errors.rb
173
+ - lib/durable_flow/event_subscriber.rb
174
+ - lib/durable_flow/live.rb
175
+ - lib/durable_flow/models/application_record.rb
176
+ - lib/durable_flow/models/workflow_event.rb
177
+ - lib/durable_flow/models/workflow_log.rb
178
+ - lib/durable_flow/models/workflow_run.rb
179
+ - lib/durable_flow/models/workflow_step.rb
180
+ - lib/durable_flow/models/workflow_wait.rb
181
+ - lib/durable_flow/railtie.rb
182
+ - lib/durable_flow/schema.rb
183
+ - lib/durable_flow/serializer.rb
184
+ - lib/durable_flow/step_proxy.rb
185
+ - lib/durable_flow/test_helper.rb
186
+ - lib/durable_flow/version.rb
187
+ - lib/durable_flow/workflow.rb
188
+ - lib/durable_flow/workflow_logger.rb
189
+ - lib/durable_flow/workflow_timeline.rb
190
+ - lib/generators/durable_flow/install_generator.rb
191
+ - lib/generators/durable_flow/templates/create_durable_flow_tables.rb
192
+ homepage: https://github.com/skorfmann/durableflow
193
+ licenses:
194
+ - MIT
195
+ metadata:
196
+ homepage_uri: https://github.com/skorfmann/durableflow
197
+ source_code_uri: https://github.com/skorfmann/durableflow
198
+ rubygems_mfa_required: 'true'
199
+ rdoc_options: []
200
+ require_paths:
201
+ - lib
202
+ required_ruby_version: !ruby/object:Gem::Requirement
203
+ requirements:
204
+ - - ">="
205
+ - !ruby/object:Gem::Version
206
+ version: 3.2.0
207
+ required_rubygems_version: !ruby/object:Gem::Requirement
208
+ requirements:
209
+ - - ">="
210
+ - !ruby/object:Gem::Version
211
+ version: '0'
212
+ requirements: []
213
+ rubygems_version: 3.6.9
214
+ specification_version: 4
215
+ summary: Durable workflows on Rails Active Job continuations.
216
+ test_files: []