pg_eventstore 0.2.6 → 0.4.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (46) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +8 -0
  3. data/README.md +4 -2
  4. data/db/migrations/10_create_subscription_commands.sql +15 -0
  5. data/db/migrations/11_create_subscriptions_set_commands.sql +15 -0
  6. data/db/migrations/12_improve_events_indexes.sql +1 -0
  7. data/db/migrations/9_create_subscriptions.sql +46 -0
  8. data/docs/configuration.md +42 -21
  9. data/docs/subscriptions.md +170 -0
  10. data/lib/pg_eventstore/callbacks.rb +122 -0
  11. data/lib/pg_eventstore/client.rb +2 -2
  12. data/lib/pg_eventstore/config.rb +35 -3
  13. data/lib/pg_eventstore/connection.rb +2 -2
  14. data/lib/pg_eventstore/errors.rb +63 -0
  15. data/lib/pg_eventstore/{pg_result_deserializer.rb → event_deserializer.rb} +11 -14
  16. data/lib/pg_eventstore/extensions/callbacks_extension.rb +95 -0
  17. data/lib/pg_eventstore/extensions/options_extension.rb +25 -23
  18. data/lib/pg_eventstore/extensions/using_connection_extension.rb +35 -0
  19. data/lib/pg_eventstore/pg_connection.rb +29 -0
  20. data/lib/pg_eventstore/queries/event_queries.rb +5 -26
  21. data/lib/pg_eventstore/queries/event_type_queries.rb +13 -0
  22. data/lib/pg_eventstore/queries/subscription_command_queries.rb +81 -0
  23. data/lib/pg_eventstore/queries/subscription_queries.rb +160 -0
  24. data/lib/pg_eventstore/queries/subscriptions_set_command_queries.rb +76 -0
  25. data/lib/pg_eventstore/queries/subscriptions_set_queries.rb +89 -0
  26. data/lib/pg_eventstore/queries.rb +6 -0
  27. data/lib/pg_eventstore/query_builders/events_filtering_query.rb +14 -2
  28. data/lib/pg_eventstore/sql_builder.rb +54 -10
  29. data/lib/pg_eventstore/subscriptions/basic_runner.rb +220 -0
  30. data/lib/pg_eventstore/subscriptions/command_handlers/subscription_feeder_commands.rb +52 -0
  31. data/lib/pg_eventstore/subscriptions/command_handlers/subscription_runners_commands.rb +68 -0
  32. data/lib/pg_eventstore/subscriptions/commands_handler.rb +62 -0
  33. data/lib/pg_eventstore/subscriptions/events_processor.rb +72 -0
  34. data/lib/pg_eventstore/subscriptions/runner_state.rb +45 -0
  35. data/lib/pg_eventstore/subscriptions/subscription.rb +141 -0
  36. data/lib/pg_eventstore/subscriptions/subscription_feeder.rb +171 -0
  37. data/lib/pg_eventstore/subscriptions/subscription_handler_performance.rb +39 -0
  38. data/lib/pg_eventstore/subscriptions/subscription_runner.rb +125 -0
  39. data/lib/pg_eventstore/subscriptions/subscription_runners_feeder.rb +38 -0
  40. data/lib/pg_eventstore/subscriptions/subscriptions_manager.rb +105 -0
  41. data/lib/pg_eventstore/subscriptions/subscriptions_set.rb +97 -0
  42. data/lib/pg_eventstore/tasks/setup.rake +1 -1
  43. data/lib/pg_eventstore/utils.rb +66 -0
  44. data/lib/pg_eventstore/version.rb +1 -1
  45. data/lib/pg_eventstore.rb +19 -1
  46. metadata +31 -4
@@ -0,0 +1,76 @@
1
+ # frozen_string_literal: true
2
+
3
+ module PgEventstore
4
+ # @!visibility private
5
+ class SubscriptionsSetCommandQueries
6
+ attr_reader :connection
7
+ private :connection
8
+
9
+ # @param connection [PgEventstore::Connection]
10
+ def initialize(connection)
11
+ @connection = connection
12
+ end
13
+
14
+ # @param subscriptions_set_id [Integer]
15
+ # @param command_name [String]
16
+ # @return [Hash, nil]
17
+ def find_by(subscriptions_set_id:, command_name:)
18
+ sql_builder =
19
+ SQLBuilder.new.
20
+ select('*').
21
+ from('subscriptions_set_commands').
22
+ where('subscriptions_set_id = ? AND name = ?', subscriptions_set_id, command_name)
23
+ pg_result = connection.with do |conn|
24
+ conn.exec_params(*sql_builder.to_exec_params)
25
+ end
26
+ return if pg_result.ntuples.zero?
27
+
28
+ deserialize(pg_result.to_a.first)
29
+ end
30
+
31
+ # @param subscriptions_set_id [Integer]
32
+ # @param command_name [String]
33
+ # @return [Hash]
34
+ def create_by(subscriptions_set_id:, command_name:)
35
+ sql = <<~SQL
36
+ INSERT INTO subscriptions_set_commands (name, subscriptions_set_id)
37
+ VALUES ($1, $2)
38
+ RETURNING *
39
+ SQL
40
+ pg_result = connection.with do |conn|
41
+ conn.exec_params(sql, [command_name, subscriptions_set_id])
42
+ end
43
+ deserialize(pg_result.to_a.first)
44
+ end
45
+
46
+ # @param subscriptions_set_id [Integer]
47
+ # @return [Array<Hash>]
48
+ def find_commands(subscriptions_set_id)
49
+ sql_builder =
50
+ SQLBuilder.new.select('*').
51
+ from('subscriptions_set_commands').
52
+ where("subscriptions_set_id = ?", subscriptions_set_id).
53
+ order('id ASC')
54
+ pg_result = connection.with do |conn|
55
+ conn.exec_params(*sql_builder.to_exec_params)
56
+ end
57
+ pg_result.to_a.map(&method(:deserialize))
58
+ end
59
+
60
+ # @param id [Integer]
61
+ # @return [void]
62
+ def delete(id)
63
+ connection.with do |conn|
64
+ conn.exec_params('DELETE FROM subscriptions_set_commands WHERE id = $1', [id])
65
+ end
66
+ end
67
+
68
+ private
69
+
70
+ # @param hash [Hash]
71
+ # @return [Hash]
72
+ def deserialize(hash)
73
+ hash.transform_keys(&:to_sym)
74
+ end
75
+ end
76
+ end
@@ -0,0 +1,89 @@
1
+ # frozen_string_literal: true
2
+
3
+ module PgEventstore
4
+ # @!visibility private
5
+ class SubscriptionsSetQueries
6
+ attr_reader :connection
7
+ private :connection
8
+
9
+ # @param connection [PgEventstore::Connection]
10
+ def initialize(connection)
11
+ @connection = connection
12
+ end
13
+
14
+ # @param attrs [Hash]
15
+ # @return [Array<Hash>]
16
+ def find_all(attrs)
17
+ builder = SQLBuilder.new.select('*').from('subscriptions_set')
18
+ attrs.each do |attr, val|
19
+ builder.where("#{attr} = ?", val)
20
+ end
21
+
22
+ pg_result = connection.with do |conn|
23
+ conn.exec_params(*builder.to_exec_params)
24
+ end
25
+ pg_result.to_a.map(&method(:deserialize))
26
+ end
27
+
28
+ # The same as #find_all, but returns first result
29
+ # @return [Hash, nil]
30
+ def find_by(...)
31
+ find_all(...).first
32
+ end
33
+
34
+ # @param id [String] UUIDv4
35
+ # @return [Hash]
36
+ # @raise [PgEventstore::RecordNotFound]
37
+ def find!(id)
38
+ find_by(id: id) || raise(RecordNotFound.new("subscriptions_set", id))
39
+ end
40
+
41
+ # @param attrs [Hash]
42
+ # @return [Hash]
43
+ def create(attrs)
44
+ sql = <<~SQL
45
+ INSERT INTO subscriptions_set (#{attrs.keys.join(', ')})
46
+ VALUES (#{Utils.positional_vars(attrs.values)})
47
+ RETURNING *
48
+ SQL
49
+ pg_result = connection.with do |conn|
50
+ conn.exec_params(sql, attrs.values)
51
+ end
52
+ deserialize(pg_result.to_a.first)
53
+ end
54
+
55
+ # @param id [String] UUIDv4
56
+ # @param attrs [Hash]
57
+ def update(id, attrs)
58
+ attrs = { updated_at: Time.now.utc }.merge(attrs)
59
+ attrs_sql = attrs.keys.map.with_index(1) do |attr, index|
60
+ "#{attr} = $#{index}"
61
+ end.join(', ')
62
+ sql = <<~SQL
63
+ UPDATE subscriptions_set SET #{attrs_sql} WHERE id = $#{attrs.keys.size + 1} RETURNING *
64
+ SQL
65
+ pg_result = connection.with do |conn|
66
+ conn.exec_params(sql, [*attrs.values, id])
67
+ end
68
+ raise(RecordNotFound.new("subscriptions_set", id)) if pg_result.ntuples.zero?
69
+
70
+ deserialize(pg_result.to_a.first)
71
+ end
72
+
73
+ # @return id [Integer]
74
+ # @return [void]
75
+ def delete(id)
76
+ connection.with do |conn|
77
+ conn.exec_params('DELETE FROM subscriptions_set WHERE id = $1', [id])
78
+ end
79
+ end
80
+
81
+ private
82
+
83
+ # @param hash [Hash]
84
+ # @return [Hash]
85
+ def deserialize(hash)
86
+ hash.transform_keys(&:to_sym)
87
+ end
88
+ end
89
+ end
@@ -1,9 +1,15 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require_relative 'sql_builder'
4
+ require_relative 'query_builders/events_filtering_query'
3
5
  require_relative 'queries/transaction_queries'
4
6
  require_relative 'queries/event_queries'
5
7
  require_relative 'queries/stream_queries'
6
8
  require_relative 'queries/event_type_queries'
9
+ require_relative 'queries/subscription_queries'
10
+ require_relative 'queries/subscriptions_set_queries'
11
+ require_relative 'queries/subscription_command_queries'
12
+ require_relative 'queries/subscriptions_set_command_queries'
7
13
 
8
14
  module PgEventstore
9
15
  # @!visibility private
@@ -5,7 +5,7 @@ module PgEventstore
5
5
  # @!visibility private
6
6
  class EventsFiltering
7
7
  DEFAULT_OFFSET = 0
8
- DEFAULT_LIMIT = 1000
8
+ DEFAULT_LIMIT = 1_000
9
9
  SQL_DIRECTIONS = {
10
10
  'asc' => 'ASC',
11
11
  'desc' => 'DESC',
@@ -16,8 +16,15 @@ module PgEventstore
16
16
  }.tap do |directions|
17
17
  directions.default = 'ASC'
18
18
  end.freeze
19
+ SUBSCRIPTIONS_OPTIONS = %i[from_position resolve_link_tos filter max_count].freeze
19
20
 
20
21
  class << self
22
+ # @param options [Hash]
23
+ # @return [PgEventstore::QueryBuilders::EventsFiltering]
24
+ def subscriptions_events_filtering(options)
25
+ all_stream_filtering(options.slice(*SUBSCRIPTIONS_OPTIONS))
26
+ end
27
+
21
28
  # @param options [Hash]
22
29
  # @param offset [Integer]
23
30
  # @return [PgEventstore::QueryBuilders::EventsFiltering]
@@ -96,7 +103,7 @@ module PgEventstore
96
103
  sql = event_type_ids.size.times.map do
97
104
  "?"
98
105
  end.join(", ")
99
- @sql_builder.where("event_types.id IN (#{sql})", *event_type_ids)
106
+ @sql_builder.where("events.event_type_id IN (#{sql})", *event_type_ids)
100
107
  end
101
108
 
102
109
  # @param revision [Integer, nil]
@@ -157,6 +164,11 @@ module PgEventstore
157
164
  join("LEFT JOIN events original_events ON original_events.id = events.link_id")
158
165
  end
159
166
 
167
+ # @return [PgEventstore::SQLBuilder]
168
+ def to_sql_builder
169
+ @sql_builder
170
+ end
171
+
160
172
  # @return [Array]
161
173
  def to_exec_params
162
174
  @sql_builder.to_exec_params
@@ -13,6 +13,8 @@ module PgEventstore
13
13
  @limit_value = nil
14
14
  @offset_value = nil
15
15
  @positional_values = []
16
+ @positional_values_size = 0
17
+ @union_values = []
16
18
  end
17
19
 
18
20
  # @param sql [String]
@@ -32,8 +34,7 @@ module PgEventstore
32
34
  # @param arguments [Array] positional values
33
35
  # @return self
34
36
  def where(sql, *arguments)
35
- sql = extract_positional_args(sql, *arguments)
36
- @where_values['AND'].push("(#{sql})")
37
+ @where_values['AND'].push([sql, arguments])
37
38
  self
38
39
  end
39
40
 
@@ -41,8 +42,7 @@ module PgEventstore
41
42
  # @param arguments [Object] positional values
42
43
  # @return self
43
44
  def where_or(sql, *arguments)
44
- sql = extract_positional_args(sql, *arguments)
45
- @where_values['OR'].push("(#{sql})")
45
+ @where_values['OR'].push([sql, arguments])
46
46
  self
47
47
  end
48
48
 
@@ -57,7 +57,7 @@ module PgEventstore
57
57
  # @param arguments [Object]
58
58
  # @return self
59
59
  def join(sql, *arguments)
60
- @join_values.push(extract_positional_args(sql, *arguments))
60
+ @join_values.push([sql, arguments])
61
61
  self
62
62
  end
63
63
 
@@ -82,7 +82,34 @@ module PgEventstore
82
82
  self
83
83
  end
84
84
 
85
+ # @param another_builder [PgEventstore::SQLBuilder]
86
+ # @return [self]
87
+ def union(another_builder)
88
+ @union_values.push(another_builder)
89
+ self
90
+ end
91
+
85
92
  def to_exec_params
93
+ return [single_query_sql, @positional_values] if @union_values.empty?
94
+
95
+ [union_query_sql, @positional_values]
96
+ end
97
+
98
+ protected
99
+
100
+ # @return [Array<Object>] sql positional values
101
+ def positional_values
102
+ @positional_values
103
+ end
104
+
105
+ def positional_values_size=(val)
106
+ @positional_values_size = val
107
+ end
108
+
109
+ private
110
+
111
+ # @return [String]
112
+ def single_query_sql
86
113
  where_sql = [where_sql('OR'), where_sql('AND')].reject(&:empty?).map { |sql| "(#{sql})" }.join(' AND ')
87
114
  sql = "SELECT #{select_sql} FROM #{@from_value}"
88
115
  sql += " #{join_sql}" unless @join_values.empty?
@@ -90,10 +117,22 @@ module PgEventstore
90
117
  sql += " ORDER BY #{order_sql}" unless @order_values.empty?
91
118
  sql += " LIMIT #{@limit_value}" if @limit_value
92
119
  sql += " OFFSET #{@offset_value}" if @offset_value
93
- [sql, @positional_values]
120
+ sql
94
121
  end
95
122
 
96
- private
123
+ # @return [String]
124
+ def union_query_sql
125
+ sql = single_query_sql
126
+ union_parts = ["(#{sql})"]
127
+ union_parts += @union_values.map do |builder|
128
+ builder.positional_values_size = @positional_values_size
129
+ builder_sql, values = builder.to_exec_params
130
+ @positional_values.push(*values)
131
+ @positional_values_size += values.size
132
+ "(#{builder_sql})"
133
+ end
134
+ union_parts.join(' UNION ALL ')
135
+ end
97
136
 
98
137
  # @return [String]
99
138
  def select_sql
@@ -103,12 +142,14 @@ module PgEventstore
103
142
  # @param join_pattern [String] "OR"/"AND"
104
143
  # @return [String]
105
144
  def where_sql(join_pattern)
106
- @where_values[join_pattern].join(" #{join_pattern} ")
145
+ @where_values[join_pattern].map do |sql, args|
146
+ "(#{extract_positional_args(sql, *args)})"
147
+ end.join(" #{join_pattern} ")
107
148
  end
108
149
 
109
150
  # @return [String]
110
151
  def join_sql
111
- @join_values.join(" ")
152
+ @join_values.map { |sql, args| extract_positional_args(sql, *args) }.join(" ")
112
153
  end
113
154
 
114
155
  # @return [String]
@@ -116,10 +157,13 @@ module PgEventstore
116
157
  @order_values.join(', ')
117
158
  end
118
159
 
160
+ # Replaces "?" signs in the given string with positional variables and memorize positional values they refer to.
161
+ # @return [String]
119
162
  def extract_positional_args(sql, *arguments)
120
163
  sql.gsub("?").each_with_index do |_, index|
121
164
  @positional_values.push(arguments[index])
122
- "$#{@positional_values.size}"
165
+ @positional_values_size += 1
166
+ "$#{@positional_values_size}"
123
167
  end
124
168
  end
125
169
  end
@@ -0,0 +1,220 @@
1
+ # frozen_string_literal: true
2
+
3
+ module PgEventstore
4
+ # Implements simple background jobs runner. The job execution is done via declaring a callback on the specific
5
+ # action. The implementation also allows you to hook into different places of life cycle of the runner by defining
6
+ # callbacks on various actions. Here is the list of available actions:
7
+ # - :before_runner_started. Happens before the runner's state switches from "initial"/"stopped" to "running" and
8
+ # runner's thread is started. It is also fired when the runner is restoring - right after :before_runner_restored
9
+ # action.
10
+ # - :after_runner_stopped. Happens after runner's state got switched from "running"/"dead" to "stopped" and runner's
11
+ # thread is terminated.
12
+ # - :before_runner_restored. Happens before runner's state gets switched from "dead" to "running" and runner's
13
+ # thread is started.
14
+ # - :process_async. Happens each @run_interval seconds within runner's thread.
15
+ # - :after_runner_died. Happens when runner's state switches to "dead" because of exception inside runner's thread.
16
+ # Callback function must be able to accept one argument - the exception which caused the runner to die will be
17
+ # passed.
18
+ # - :change_state. It happens each time the runner changes the state. Callback function must be able to accept one
19
+ # argument - current state will be passed.
20
+ #
21
+ # Example of BasicRunner usage:
22
+ # class MyAwesomeRunner
23
+ # extend Forwardable
24
+ #
25
+ # def_delegators :@basic_runner, :start, :stop, :wait_for_finish, :stop_async, :restore
26
+ #
27
+ # def initialize
28
+ # @basic_runner = PgEventstore::BasicRunner.new(1, 2)
29
+ # @jobs_performed = 0
30
+ # attach_runner_callbacks
31
+ # end
32
+ #
33
+ # private
34
+ #
35
+ # def attach_runner_callbacks
36
+ # @basic_runner.define_callback(:change_state, :after, method(:state_changed))
37
+ # @basic_runner.define_callback(:process_async, :before, method(:process_action))
38
+ # @basic_runner.define_callback(:process_async, :after, method(:count_jobs))
39
+ # @basic_runner.define_callback(:before_runner_started, :before, method(:before_runner_started))
40
+ # @basic_runner.define_callback(:after_runner_stopped, :before, method(:after_runner_stopped))
41
+ # @basic_runner.define_callback(:after_runner_died, :before, method(:after_runner_died))
42
+ # end
43
+ #
44
+ # def process_action
45
+ # raise "What's the point? I can not handle this any more!" if @jobs_performed >= 3
46
+ # puts "Doing some heavy lifting job"
47
+ # sleep 2 # Simulate long running job
48
+ # end
49
+ #
50
+ # def count_jobs
51
+ # @jobs_performed += 1
52
+ # end
53
+ #
54
+ # # @param state [String]
55
+ # def state_changed(state)
56
+ # puts "New state is #{state.inspect}"
57
+ # end
58
+ #
59
+ # def before_runner_started
60
+ # puts "Doing some preparations..."
61
+ # end
62
+ #
63
+ # def after_runner_stopped
64
+ # puts "You job is not processing any more. Total jobs performed: #{@jobs_performed}. Bye-bye!"
65
+ # end
66
+ #
67
+ # def after_runner_died(error)
68
+ # puts "Error occurred: #{error.inspect}"
69
+ # end
70
+ # end
71
+ #
72
+ # runner = MyAwesomeRunner.new
73
+ # runner.start # to start your background runner to process the job, defined by #process_action method
74
+ # runner.stop # to stop the runner
75
+ #
76
+ # See {PgEventstore::RunnerState} for the list of available states
77
+ # See {PgEventstore::CallbacksExtension} and {PgEventstore::Callbacks} for more info about how to use callbacks
78
+ class BasicRunner
79
+ extend Forwardable
80
+ include Extensions::CallbacksExtension
81
+
82
+ def_delegators :@state, :initial?, :running?, :halting?, :stopped?, :dead?
83
+
84
+ # @param run_interval [Integer, Float] seconds. Determines how often to run async task. Async task is determined by
85
+ # :after_runner_stopped callback
86
+ # @param async_shutdown_time [Integer, Float] seconds. Determines how long to wait for the async shutdown to wait
87
+ # for the runner to finish.
88
+ def initialize(run_interval, async_shutdown_time)
89
+ @run_interval = run_interval
90
+ @async_shutdown_time = async_shutdown_time
91
+ @state = RunnerState.new
92
+ @mutex = Thread::Mutex.new
93
+ delegate_change_state_cbx
94
+ end
95
+
96
+ # Start asynchronous runner. If the runner is dead - please use #restore to restart it.
97
+ # @return [self]
98
+ def start
99
+ synchronize do
100
+ return self unless @state.initial? || @state.stopped?
101
+
102
+ callbacks.run_callbacks(:before_runner_started)
103
+ _start
104
+ end
105
+ self
106
+ end
107
+
108
+ # Stop asynchronous runner. This operation is immediate and it won't be waiting for current job to finish - it will
109
+ # instantly halt it. If you care about the result of your async job - use #stop_async instead.
110
+ # @return [self]
111
+ def stop
112
+ synchronize do
113
+ return self unless @state.running? || @state.dead?
114
+
115
+ @runner&.exit
116
+ @runner = nil
117
+ @state.stopped!
118
+ callbacks.run_callbacks(:after_runner_stopped)
119
+ end
120
+ self
121
+ end
122
+
123
+ # Asynchronously stop asynchronous runner. This operation spawns another thread to gracefully stop the runner. It
124
+ # will wait up to @async_shutdown_time seconds before force-stopping the runner.
125
+ # @return [self]
126
+ def stop_async
127
+ synchronize do
128
+ return self unless @state.running? || @state.dead?
129
+
130
+ @state.halting!
131
+ Thread.new do
132
+ stopping_at = Time.now.utc
133
+ halt = false
134
+ loop do
135
+ synchronize do
136
+ # Give the runner up to @async_shutdown_time seconds for graceful shutdown
137
+ @runner&.exit if Time.now.utc - stopping_at > @async_shutdown_time
138
+
139
+ unless @runner&.alive?
140
+ @state.stopped!
141
+ @runner = nil
142
+ callbacks.run_callbacks(:after_runner_stopped)
143
+ halt = true
144
+ end
145
+ end
146
+ break if halt
147
+ sleep 0.1
148
+ end
149
+ end
150
+ self
151
+ end
152
+ end
153
+
154
+ # Restores the runner after its death.
155
+ # @return [self]
156
+ def restore
157
+ synchronize do
158
+ return self unless @state.dead?
159
+
160
+ callbacks.run_callbacks(:before_runner_restored)
161
+ _start
162
+ end
163
+ self
164
+ end
165
+
166
+ # Wait until the runner switches the state to either "stopped" or "dead". This operation is synchronous.
167
+ # @return [self]
168
+ def wait_for_finish
169
+ loop do
170
+ continue = synchronize do
171
+ @state.halting? || @state.running?
172
+ end
173
+ break unless continue
174
+
175
+ sleep 0.1
176
+ end
177
+ self
178
+ end
179
+
180
+ # @return [String]
181
+ def state
182
+ @state.to_s
183
+ end
184
+
185
+ private
186
+
187
+ def synchronize
188
+ @mutex.synchronize { yield }
189
+ end
190
+
191
+ # @return [void]
192
+ def _start
193
+ @state.running!
194
+ @runner = Thread.new do
195
+ loop do
196
+ Thread.current.exit unless @state.running?
197
+ sleep @run_interval
198
+
199
+ callbacks.run_callbacks(:process_async)
200
+ end
201
+ rescue => error
202
+ synchronize do
203
+ raise unless @state.halting? || @state.running?
204
+
205
+ @state.dead!
206
+ callbacks.run_callbacks(:after_runner_died, error)
207
+ end
208
+ end
209
+ end
210
+
211
+ # Delegates :change_state action to the runner
212
+ def delegate_change_state_cbx
213
+ @state.define_callback(:change_state, :before, method(:change_state))
214
+ end
215
+
216
+ def change_state(...)
217
+ callbacks.run_callbacks(:change_state, ...)
218
+ end
219
+ end
220
+ end
@@ -0,0 +1,52 @@
1
+ # frozen_string_literal: true
2
+
3
+ module PgEventstore
4
+ module CommandHandlers
5
+ class SubscriptionFeederCommands
6
+ AVAILABLE_COMMANDS = %w[StopAll StartAll].freeze
7
+
8
+ # @param config_name [Symbol]
9
+ # @param subscription_feeder [PgEventstore::SubscriptionFeeder]
10
+ def initialize(config_name, subscription_feeder)
11
+ @config_name = config_name
12
+ @subscription_feeder = subscription_feeder
13
+ end
14
+
15
+ # Look up commands for the given SubscriptionFeeder and execute them
16
+ # @return [void]
17
+ def process
18
+ queries.find_commands(@subscription_feeder.id).each do |command|
19
+ unless AVAILABLE_COMMANDS.include?(command[:name])
20
+ PgEventstore.logger&.warn(
21
+ "#{self.class.name}: Don't know how to handle #{command[:name].inspect}. Details: #{command.inspect}."
22
+ )
23
+ next
24
+ end
25
+ send(Utils.underscore_str(command[:name]))
26
+ ensure
27
+ queries.delete(command[:id])
28
+ end
29
+ end
30
+
31
+ private
32
+
33
+ # @return [PgEventstore::SubscriptionsSetCommandQueries]
34
+ def queries
35
+ SubscriptionsSetCommandQueries.new(connection)
36
+ end
37
+
38
+ # @return [PgEventstore::Connection]
39
+ def connection
40
+ PgEventstore.connection(@config_name)
41
+ end
42
+
43
+ def stop_all
44
+ @subscription_feeder.stop_all
45
+ end
46
+
47
+ def start_all
48
+ @subscription_feeder.start_all
49
+ end
50
+ end
51
+ end
52
+ end
@@ -0,0 +1,68 @@
1
+ # frozen_string_literal: true
2
+
3
+ module PgEventstore
4
+ module CommandHandlers
5
+ class SubscriptionRunnersCommands
6
+ AVAILABLE_COMMANDS = %w[StopRunner RestoreRunner StartRunner].freeze
7
+
8
+ # @param config_name [Symbol]
9
+ # @param runners [Array<PgEventstore::SubscriptionRunner>]
10
+ def initialize(config_name, runners)
11
+ @config_name = config_name
12
+ @runners = runners
13
+ end
14
+
15
+ # Look up commands for all given SubscriptionRunner-s and execute them
16
+ # @return [void]
17
+ def process
18
+ queries.find_commands(@runners.map(&:id)).each do |command|
19
+ unless AVAILABLE_COMMANDS.include?(command[:name])
20
+ PgEventstore.logger&.warn(
21
+ "#{self.class.name}: Don't know how to handle #{command[:name].inspect}. Details: #{command.inspect}."
22
+ )
23
+ next
24
+ end
25
+ send(Utils.underscore_str(command[:name]), command[:subscription_id])
26
+ ensure
27
+ queries.delete(command[:id])
28
+ end
29
+ end
30
+
31
+ private
32
+
33
+ # @return [PgEventstore::SubscriptionCommandQueries]
34
+ def queries
35
+ SubscriptionCommandQueries.new(connection)
36
+ end
37
+
38
+ # @return [PgEventstore::Connection]
39
+ def connection
40
+ PgEventstore.connection(@config_name)
41
+ end
42
+
43
+ # @param subscription_id [Integer]
44
+ # @return [PgEventstore::SubscriptionRunner, nil]
45
+ def find_subscription_runner(subscription_id)
46
+ @runners.find { |runner| runner.id == subscription_id }
47
+ end
48
+
49
+ # @param subscription_id [Integer]
50
+ # @return [void]
51
+ def start_runner(subscription_id)
52
+ find_subscription_runner(subscription_id)&.start
53
+ end
54
+
55
+ # @param subscription_id [Integer]
56
+ # @return [void]
57
+ def restore_runner(subscription_id)
58
+ find_subscription_runner(subscription_id)&.restore
59
+ end
60
+
61
+ # @param subscription_id [Integer]
62
+ # @return [void]
63
+ def stop_runner(subscription_id)
64
+ find_subscription_runner(subscription_id)&.stop_async
65
+ end
66
+ end
67
+ end
68
+ end