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.
- checksums.yaml +4 -4
- data/CHANGELOG.md +4 -0
- data/README.md +1 -0
- data/db/migrations/10_create_subscription_commands.sql +15 -0
- data/db/migrations/11_create_subscriptions_set_commands.sql +15 -0
- data/db/migrations/12_improve_events_indexes.sql +1 -0
- data/db/migrations/9_create_subscriptions.sql +46 -0
- data/docs/configuration.md +42 -21
- data/docs/subscriptions.md +170 -0
- data/lib/pg_eventstore/callbacks.rb +122 -0
- data/lib/pg_eventstore/client.rb +2 -2
- data/lib/pg_eventstore/config.rb +35 -3
- data/lib/pg_eventstore/errors.rb +63 -0
- data/lib/pg_eventstore/{pg_result_deserializer.rb → event_deserializer.rb} +11 -14
- data/lib/pg_eventstore/extensions/callbacks_extension.rb +95 -0
- data/lib/pg_eventstore/extensions/options_extension.rb +25 -23
- data/lib/pg_eventstore/extensions/using_connection_extension.rb +35 -0
- data/lib/pg_eventstore/queries/event_queries.rb +5 -26
- data/lib/pg_eventstore/queries/event_type_queries.rb +13 -0
- data/lib/pg_eventstore/queries/subscription_command_queries.rb +81 -0
- data/lib/pg_eventstore/queries/subscription_queries.rb +160 -0
- data/lib/pg_eventstore/queries/subscriptions_set_command_queries.rb +76 -0
- data/lib/pg_eventstore/queries/subscriptions_set_queries.rb +89 -0
- data/lib/pg_eventstore/queries.rb +6 -0
- data/lib/pg_eventstore/query_builders/events_filtering_query.rb +14 -2
- data/lib/pg_eventstore/sql_builder.rb +54 -10
- data/lib/pg_eventstore/subscriptions/basic_runner.rb +220 -0
- data/lib/pg_eventstore/subscriptions/command_handlers/subscription_feeder_commands.rb +52 -0
- data/lib/pg_eventstore/subscriptions/command_handlers/subscription_runners_commands.rb +68 -0
- data/lib/pg_eventstore/subscriptions/commands_handler.rb +62 -0
- data/lib/pg_eventstore/subscriptions/events_processor.rb +72 -0
- data/lib/pg_eventstore/subscriptions/runner_state.rb +45 -0
- data/lib/pg_eventstore/subscriptions/subscription.rb +141 -0
- data/lib/pg_eventstore/subscriptions/subscription_feeder.rb +171 -0
- data/lib/pg_eventstore/subscriptions/subscription_handler_performance.rb +39 -0
- data/lib/pg_eventstore/subscriptions/subscription_runner.rb +125 -0
- data/lib/pg_eventstore/subscriptions/subscription_runners_feeder.rb +38 -0
- data/lib/pg_eventstore/subscriptions/subscriptions_manager.rb +105 -0
- data/lib/pg_eventstore/subscriptions/subscriptions_set.rb +97 -0
- data/lib/pg_eventstore/tasks/setup.rake +1 -1
- data/lib/pg_eventstore/utils.rb +66 -0
- data/lib/pg_eventstore/version.rb +1 -1
- data/lib/pg_eventstore.rb +19 -1
- 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 =
|
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("
|
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
|
-
|
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
|
-
|
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(
|
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
|
-
|
120
|
+
sql
|
94
121
|
end
|
95
122
|
|
96
|
-
|
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].
|
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
|
-
|
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
|