pg_eventstore 0.3.0 → 0.4.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 (44) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +4 -0
  3. data/README.md +1 -0
  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/errors.rb +63 -0
  14. data/lib/pg_eventstore/{pg_result_deserializer.rb → event_deserializer.rb} +11 -14
  15. data/lib/pg_eventstore/extensions/callbacks_extension.rb +95 -0
  16. data/lib/pg_eventstore/extensions/options_extension.rb +25 -23
  17. data/lib/pg_eventstore/extensions/using_connection_extension.rb +35 -0
  18. data/lib/pg_eventstore/queries/event_queries.rb +5 -26
  19. data/lib/pg_eventstore/queries/event_type_queries.rb +13 -0
  20. data/lib/pg_eventstore/queries/subscription_command_queries.rb +81 -0
  21. data/lib/pg_eventstore/queries/subscription_queries.rb +160 -0
  22. data/lib/pg_eventstore/queries/subscriptions_set_command_queries.rb +76 -0
  23. data/lib/pg_eventstore/queries/subscriptions_set_queries.rb +89 -0
  24. data/lib/pg_eventstore/queries.rb +6 -0
  25. data/lib/pg_eventstore/query_builders/events_filtering_query.rb +14 -2
  26. data/lib/pg_eventstore/sql_builder.rb +54 -10
  27. data/lib/pg_eventstore/subscriptions/basic_runner.rb +220 -0
  28. data/lib/pg_eventstore/subscriptions/command_handlers/subscription_feeder_commands.rb +52 -0
  29. data/lib/pg_eventstore/subscriptions/command_handlers/subscription_runners_commands.rb +68 -0
  30. data/lib/pg_eventstore/subscriptions/commands_handler.rb +62 -0
  31. data/lib/pg_eventstore/subscriptions/events_processor.rb +72 -0
  32. data/lib/pg_eventstore/subscriptions/runner_state.rb +45 -0
  33. data/lib/pg_eventstore/subscriptions/subscription.rb +141 -0
  34. data/lib/pg_eventstore/subscriptions/subscription_feeder.rb +171 -0
  35. data/lib/pg_eventstore/subscriptions/subscription_handler_performance.rb +39 -0
  36. data/lib/pg_eventstore/subscriptions/subscription_runner.rb +125 -0
  37. data/lib/pg_eventstore/subscriptions/subscription_runners_feeder.rb +38 -0
  38. data/lib/pg_eventstore/subscriptions/subscriptions_manager.rb +105 -0
  39. data/lib/pg_eventstore/subscriptions/subscriptions_set.rb +97 -0
  40. data/lib/pg_eventstore/tasks/setup.rake +1 -1
  41. data/lib/pg_eventstore/utils.rb +66 -0
  42. data/lib/pg_eventstore/version.rb +1 -1
  43. data/lib/pg_eventstore.rb +19 -1
  44. metadata +30 -4
@@ -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
@@ -0,0 +1,62 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'command_handlers/subscription_feeder_commands'
4
+ require_relative 'command_handlers/subscription_runners_commands'
5
+
6
+ module PgEventstore
7
+ # This class implements the runner which processes remote commands in the background. This allows you to remotely
8
+ # control such actions as stop, start and restart of your Subscriptions.
9
+ # @!visibility private
10
+ class CommandsHandler
11
+ extend Forwardable
12
+
13
+ RESTART_DELAY = 5 # seconds
14
+
15
+ def_delegators :@basic_runner, :start, :stop, :state, :stop_async, :wait_for_finish
16
+
17
+ # @param config_name [Symbol]
18
+ # @param subscription_feeder [PgEventstore::SUbscriptionFeeder]
19
+ # @param runners [Array<PgEventstore::SubscriptionRunner>]
20
+ def initialize(config_name, subscription_feeder, runners)
21
+ @config_name = config_name
22
+ @subscription_feeder = subscription_feeder
23
+ @runners = runners
24
+ @basic_runner = BasicRunner.new(1, 0)
25
+ attach_runner_callbacks
26
+ end
27
+
28
+ private
29
+
30
+ def attach_runner_callbacks
31
+ @basic_runner.define_callback(:process_async, :before, method(:process_async))
32
+ @basic_runner.define_callback(:after_runner_died, :before, method(:after_runner_died))
33
+ end
34
+
35
+ def process_async
36
+ subscription_feeder_commands.process
37
+ subscription_runners_commands.process
38
+ end
39
+
40
+ # @param error [StandardError]
41
+ # @return [void]
42
+ def after_runner_died(error)
43
+ PgEventstore.logger&.error "#{self.class.name}: Error occurred: #{error.message}"
44
+ PgEventstore.logger&.error "#{self.class.name}: Backtrace: #{error.backtrace&.join("\n")}"
45
+ PgEventstore.logger&.error "#{self.class.name}: Trying to auto-repair in #{RESTART_DELAY} seconds..."
46
+ Thread.new do
47
+ sleep RESTART_DELAY
48
+ @basic_runner.restore
49
+ end
50
+ end
51
+
52
+ # @return [PgEventstore::CommandHandlers::SubscriptionFeederCommands]
53
+ def subscription_feeder_commands
54
+ CommandHandlers::SubscriptionFeederCommands.new(@config_name, @subscription_feeder)
55
+ end
56
+
57
+ # @return [PgEventstore::CommandHandlers::SubscriptionRunnersCommands]
58
+ def subscription_runners_commands
59
+ CommandHandlers::SubscriptionRunnersCommands.new(@config_name, @runners)
60
+ end
61
+ end
62
+ end