pg_eventstore 1.11.0 → 1.13.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 (82) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +9 -0
  3. data/docs/reading_events.md +98 -0
  4. data/lib/pg_eventstore/cli/commands/base_command.rb +2 -0
  5. data/lib/pg_eventstore/cli/commands/callback_handlers/start_cmd_handlers.rb +1 -0
  6. data/lib/pg_eventstore/cli/commands/help_command.rb +1 -0
  7. data/lib/pg_eventstore/cli/commands/start_subscriptions_command.rb +3 -1
  8. data/lib/pg_eventstore/cli/commands/stop_subscriptions_command.rb +1 -0
  9. data/lib/pg_eventstore/cli/exit_codes.rb +1 -0
  10. data/lib/pg_eventstore/cli/parser_options/base_options.rb +1 -0
  11. data/lib/pg_eventstore/cli/parser_options/default_options.rb +1 -0
  12. data/lib/pg_eventstore/cli/parser_options/metadata.rb +1 -0
  13. data/lib/pg_eventstore/cli/parser_options/subscription_options.rb +1 -0
  14. data/lib/pg_eventstore/cli/try_to_delete_subscriptions_set.rb +2 -1
  15. data/lib/pg_eventstore/cli/try_unlock_subscriptions_set.rb +1 -0
  16. data/lib/pg_eventstore/cli/wait_for_subscriptions_set_shutdown.rb +1 -0
  17. data/lib/pg_eventstore/client.rb +22 -4
  18. data/lib/pg_eventstore/commands/all_stream_read_grouped.rb +69 -0
  19. data/lib/pg_eventstore/commands/regular_stream_read_grouped.rb +31 -0
  20. data/lib/pg_eventstore/commands.rb +2 -0
  21. data/lib/pg_eventstore/connection.rb +8 -3
  22. data/lib/pg_eventstore/extensions/callback_handlers_extension.rb +1 -0
  23. data/lib/pg_eventstore/partition.rb +23 -0
  24. data/lib/pg_eventstore/pg_connection.rb +1 -0
  25. data/lib/pg_eventstore/queries/event_queries.rb +18 -0
  26. data/lib/pg_eventstore/queries/partition_queries.rb +21 -0
  27. data/lib/pg_eventstore/queries.rb +2 -0
  28. data/lib/pg_eventstore/query_builders/basic_filtering.rb +27 -0
  29. data/lib/pg_eventstore/query_builders/events_filtering.rb +47 -31
  30. data/lib/pg_eventstore/query_builders/partitions_filtering.rb +83 -0
  31. data/lib/pg_eventstore/sql_builder.rb +10 -0
  32. data/lib/pg_eventstore/subscriptions/basic_runner.rb +122 -35
  33. data/lib/pg_eventstore/subscriptions/callback_handlers/commands_handler_handlers.rb +1 -14
  34. data/lib/pg_eventstore/subscriptions/callback_handlers/events_processor_handlers.rb +4 -3
  35. data/lib/pg_eventstore/subscriptions/callback_handlers/subscription_feeder_handlers.rb +1 -14
  36. data/lib/pg_eventstore/subscriptions/callback_handlers/subscription_runner_handlers.rb +1 -19
  37. data/lib/pg_eventstore/subscriptions/command_handlers/subscription_feeder_commands.rb +1 -0
  38. data/lib/pg_eventstore/subscriptions/command_handlers/subscription_runners_commands.rb +1 -0
  39. data/lib/pg_eventstore/subscriptions/commands_handler.rb +5 -8
  40. data/lib/pg_eventstore/subscriptions/events_processor.rb +7 -2
  41. data/lib/pg_eventstore/subscriptions/extensions/base_command_extension.rb +1 -0
  42. data/lib/pg_eventstore/subscriptions/extensions/command_class_lookup_extension.rb +1 -0
  43. data/lib/pg_eventstore/subscriptions/queries/subscription_queries.rb +1 -9
  44. data/lib/pg_eventstore/subscriptions/runner_recovery_strategies/restore_connection.rb +44 -0
  45. data/lib/pg_eventstore/subscriptions/runner_recovery_strategies/restore_subscription_feeder.rb +27 -0
  46. data/lib/pg_eventstore/subscriptions/runner_recovery_strategies/restore_subscription_runner.rb +34 -0
  47. data/lib/pg_eventstore/subscriptions/runner_recovery_strategies.rb +5 -0
  48. data/lib/pg_eventstore/subscriptions/runner_recovery_strategy.rb +21 -0
  49. data/lib/pg_eventstore/subscriptions/subscription_feeder.rb +18 -5
  50. data/lib/pg_eventstore/subscriptions/subscription_runner.rb +1 -13
  51. data/lib/pg_eventstore/subscriptions/subscriptions_lifecycle.rb +1 -0
  52. data/lib/pg_eventstore/subscriptions/subscriptions_manager.rb +20 -3
  53. data/lib/pg_eventstore/subscriptions/subscriptions_set_lifecycle.rb +1 -0
  54. data/lib/pg_eventstore/utils.rb +5 -4
  55. data/lib/pg_eventstore/version.rb +1 -1
  56. data/lib/pg_eventstore/web/application.rb +5 -3
  57. data/lib/pg_eventstore.rb +3 -1
  58. data/sig/pg_eventstore/client.rbs +2 -0
  59. data/sig/pg_eventstore/commands/all_stream_read_grouped.rbs +16 -0
  60. data/sig/pg_eventstore/commands/regular_stream_read_grouped.rbs +8 -0
  61. data/sig/pg_eventstore/connection.rbs +2 -0
  62. data/sig/pg_eventstore/partition.rbs +15 -0
  63. data/sig/pg_eventstore/queries/event_queries.rbs +2 -0
  64. data/sig/pg_eventstore/queries/partition_queries.rbs +6 -0
  65. data/sig/pg_eventstore/query_builders/basic_filtering.rbs +15 -0
  66. data/sig/pg_eventstore/query_builders/events_filtering_query.rbs +17 -17
  67. data/sig/pg_eventstore/query_builders/partitions_filtering.rbs +21 -0
  68. data/sig/pg_eventstore/sql_builder.rbs +1 -1
  69. data/sig/pg_eventstore/subscriptions/basic_runner.rbs +26 -10
  70. data/sig/pg_eventstore/subscriptions/callback_handlers/commands_handler_handlers.rbs +5 -5
  71. data/sig/pg_eventstore/subscriptions/callback_handlers/subscription_feeder_handlers.rbs +1 -3
  72. data/sig/pg_eventstore/subscriptions/callback_handlers/subscription_runner_handlers.rbs +0 -4
  73. data/sig/pg_eventstore/subscriptions/commands_handler.rbs +8 -11
  74. data/sig/pg_eventstore/subscriptions/events_processor.rbs +5 -17
  75. data/sig/pg_eventstore/subscriptions/runner_recovery_strategies/restore_connection.rbs +18 -0
  76. data/sig/pg_eventstore/subscriptions/runner_recovery_strategies/restore_subscription_feeder.rbs +11 -0
  77. data/sig/pg_eventstore/subscriptions/runner_recovery_strategies/restore_subscription_runner.rbs +17 -0
  78. data/sig/pg_eventstore/subscriptions/runner_recovery_strategy.rbs +7 -0
  79. data/sig/pg_eventstore/subscriptions/subscription_feeder.rbs +6 -8
  80. data/sig/pg_eventstore/subscriptions/subscription_runner.rbs +6 -35
  81. data/sig/pg_eventstore/subscriptions/subscriptions_manager.rbs +8 -0
  82. metadata +22 -6
@@ -3,7 +3,11 @@
3
3
  module PgEventstore
4
4
  module QueryBuilders
5
5
  # @!visibility private
6
- class EventsFiltering
6
+ class EventsFiltering < BasicFiltering
7
+ # @return [String]
8
+ TABLE_NAME = 'events'
9
+ private_constant :TABLE_NAME
10
+
7
11
  # @return [Integer]
8
12
  DEFAULT_LIMIT = 1_000
9
13
  # @return [Hash<String => String, Symbol => String>]
@@ -26,6 +30,7 @@ module PgEventstore
26
30
  # @return [PgEventstore::EventsFiltering]
27
31
  def events_filtering(stream, options)
28
32
  return all_stream_filtering(options) if stream.all_stream?
33
+
29
34
  if stream.system? && Stream::KNOWN_SYSTEM_STREAMS.include?(stream.context)
30
35
  return system_stream_filtering(stream, options)
31
36
  end
@@ -43,11 +48,9 @@ module PgEventstore
43
48
  # @return [PgEventstore::QueryBuilders::EventsFiltering]
44
49
  def all_stream_filtering(options)
45
50
  event_filter = new
46
- options in { filter: { event_types: Array => event_types } }
47
- event_filter.add_event_types(event_types)
51
+ event_filter.add_event_types(extract_event_types_filter(options))
48
52
  event_filter.add_limit(options[:max_count])
49
- options in { filter: { streams: Array => streams } }
50
- streams&.each { |attrs| event_filter.add_stream_attrs(**attrs) }
53
+ extract_streams_filter(options).each { |attrs| event_filter.add_stream_attrs(**attrs) }
51
54
  event_filter.add_global_position(options[:from_position], options[:direction])
52
55
  event_filter.add_all_stream_direction(options[:direction])
53
56
  event_filter
@@ -58,8 +61,7 @@ module PgEventstore
58
61
  # @return [PgEventstore::QueryBuilders::EventsFiltering]
59
62
  def specific_stream_filtering(stream, options)
60
63
  event_filter = new
61
- options in { filter: { event_types: Array => event_types } }
62
- event_filter.add_event_types(event_types)
64
+ event_filter.add_event_types(extract_event_types_filter(options))
63
65
  event_filter.add_limit(options[:max_count])
64
66
  event_filter.add_stream_attrs(**stream.to_hash)
65
67
  event_filter.add_revision(options[:from_revision], options[:direction])
@@ -75,14 +77,39 @@ module PgEventstore
75
77
  event_filter.set_source(stream.context)
76
78
  end
77
79
  end
80
+
81
+ # @param options [Hash]
82
+ # @return [Array<String>]
83
+ def extract_event_types_filter(options)
84
+ options in { filter: { event_types: Array => event_types } }
85
+ event_types&.select! do
86
+ _1.is_a?(String)
87
+ end
88
+ event_types || []
89
+ end
90
+
91
+ # @param options [Hash]
92
+ # @return [Array<Hash[Symbol, String]>]
93
+ def extract_streams_filter(options)
94
+ options in { filter: { streams: Array => streams } }
95
+ streams = streams&.map do
96
+ _1 in { context: String | NilClass => context }
97
+ _1 in { stream_name: String | NilClass => stream_name }
98
+ _1 in { stream_id: String | NilClass => stream_id }
99
+ { context: context, stream_name: stream_name, stream_id: stream_id }
100
+ end
101
+ streams || []
102
+ end
78
103
  end
79
104
 
80
105
  def initialize
81
- @sql_builder =
82
- SQLBuilder.new.
83
- select('events.*').
84
- from('events').
85
- limit(DEFAULT_LIMIT)
106
+ super
107
+ @sql_builder.limit(DEFAULT_LIMIT)
108
+ end
109
+
110
+ # @return [String]
111
+ def to_table_name
112
+ TABLE_NAME
86
113
  end
87
114
 
88
115
  # @param context [String, nil]
@@ -95,21 +122,20 @@ module PgEventstore
95
122
 
96
123
  stream_attrs.compact!
97
124
  sql = stream_attrs.map do |attr, _|
98
- "events.#{attr} = ?"
125
+ "#{to_table_name}.#{attr} = ?"
99
126
  end.join(" AND ")
100
127
  @sql_builder.where_or(sql, *stream_attrs.values)
101
128
  end
102
129
 
103
- # @param event_types [Array<String>, nil]
130
+ # @param event_types [Array<String>]
104
131
  # @return [void]
105
132
  def add_event_types(event_types)
106
- return if event_types.nil?
107
133
  return if event_types.empty?
108
134
 
109
135
  sql = event_types.size.times.map do
110
136
  "?"
111
137
  end.join(", ")
112
- @sql_builder.where("events.type IN (#{sql})", *event_types)
138
+ @sql_builder.where("#{to_table_name}.type IN (#{sql})", *event_types)
113
139
  end
114
140
 
115
141
  # @param revision [Integer, nil]
@@ -118,7 +144,7 @@ module PgEventstore
118
144
  def add_revision(revision, direction)
119
145
  return unless revision
120
146
 
121
- @sql_builder.where("events.stream_revision #{direction_operator(direction)} ?", revision)
147
+ @sql_builder.where("#{to_table_name}.stream_revision #{direction_operator(direction)} ?", revision)
122
148
  end
123
149
 
124
150
  # @param position [Integer, nil]
@@ -127,19 +153,19 @@ module PgEventstore
127
153
  def add_global_position(position, direction)
128
154
  return unless position
129
155
 
130
- @sql_builder.where("events.global_position #{direction_operator(direction)} ?", position)
156
+ @sql_builder.where("#{to_table_name}.global_position #{direction_operator(direction)} ?", position)
131
157
  end
132
158
 
133
159
  # @param direction [String, Symbol, nil]
134
160
  # @return [void]
135
161
  def add_stream_direction(direction)
136
- @sql_builder.order("events.stream_revision #{SQL_DIRECTIONS[direction]}")
162
+ @sql_builder.order("#{to_table_name}.stream_revision #{SQL_DIRECTIONS[direction]}")
137
163
  end
138
164
 
139
165
  # @param direction [String, Symbol, nil]
140
166
  # @return [void]
141
167
  def add_all_stream_direction(direction)
142
- @sql_builder.order("events.global_position #{SQL_DIRECTIONS[direction]}")
168
+ @sql_builder.order("#{to_table_name}.global_position #{SQL_DIRECTIONS[direction]}")
143
169
  end
144
170
 
145
171
  # @param limit [Integer, nil]
@@ -150,20 +176,10 @@ module PgEventstore
150
176
  @sql_builder.limit(limit)
151
177
  end
152
178
 
153
- # @return [PgEventstore::SQLBuilder]
154
- def to_sql_builder
155
- @sql_builder
156
- end
157
-
158
- # @return [Array]
159
- def to_exec_params
160
- @sql_builder.to_exec_params
161
- end
162
-
163
179
  # @param table_name [String] system stream view name
164
180
  # @return [void]
165
181
  def set_source(table_name)
166
- @sql_builder.from(%{ "#{PG::Connection.escape(table_name)}" events })
182
+ @sql_builder.from(%{ "#{PG::Connection.escape(table_name)}" #{to_table_name} })
167
183
  end
168
184
 
169
185
  private
@@ -0,0 +1,83 @@
1
+ # frozen_string_literal: true
2
+
3
+ module PgEventstore
4
+ module QueryBuilders
5
+ # @!visibility private
6
+ class PartitionsFiltering < BasicFiltering
7
+ # @return [String]
8
+ TABLE_NAME = 'partitions'
9
+ private_constant :TABLE_NAME
10
+
11
+ class << self
12
+ # @param options [Hash]
13
+ # @return [Array<String>]
14
+ def extract_event_types_filter(options)
15
+ options in { filter: { event_types: Array => event_types } }
16
+ event_types&.select! do
17
+ _1.is_a?(String)
18
+ end
19
+ event_types || []
20
+ end
21
+
22
+ # @param options [Hash]
23
+ # @return [Array<Hash[Symbol, String]>]
24
+ def extract_streams_filter(options)
25
+ options in { filter: { streams: Array => streams } }
26
+ streams = streams&.map do
27
+ _1 in { context: String | NilClass => context }
28
+ _1 in { stream_name: String | NilClass => stream_name }
29
+ { context: context, stream_name: stream_name }
30
+ end
31
+ streams || []
32
+ end
33
+ end
34
+
35
+ # @return [String]
36
+ def to_table_name
37
+ TABLE_NAME
38
+ end
39
+
40
+ # @param context [String, nil]
41
+ # @param stream_name [String, nil]
42
+ # @return [void]
43
+ def add_stream_attrs(context: nil, stream_name: nil)
44
+ stream_attrs = { context: context, stream_name: stream_name }
45
+ return unless correct_stream_filter?(stream_attrs)
46
+
47
+ stream_attrs.compact!
48
+ sql = stream_attrs.map do |attr, _|
49
+ "#{to_table_name}.#{attr} = ?"
50
+ end.join(" AND ")
51
+ @sql_builder.where_or(sql, *stream_attrs.values)
52
+ end
53
+
54
+ # @param event_types [Array<String>]
55
+ # @return [void]
56
+ def add_event_types(event_types)
57
+ return if event_types.empty?
58
+
59
+ @sql_builder.where("#{to_table_name}.event_type = ANY(?::varchar[])", event_types)
60
+ end
61
+
62
+ # @return [void]
63
+ def with_event_types
64
+ @sql_builder.where('event_type IS NOT NULL')
65
+ end
66
+
67
+ private
68
+
69
+ # @param stream_attrs [Hash]
70
+ # @return [Boolean]
71
+ def correct_stream_filter?(stream_attrs)
72
+ result = (stream_attrs in { context: String, stream_name: String } | { context: String, stream_name: nil })
73
+ return true if result
74
+
75
+ PgEventstore&.logger&.debug(<<~TEXT)
76
+ Ignoring unsupported stream filter format for grouped read #{stream_attrs.compact.inspect}. \
77
+ See docs/reading_events.md docs for supported formats.
78
+ TEXT
79
+ false
80
+ end
81
+ end
82
+ end
83
+ end
@@ -4,6 +4,16 @@ module PgEventstore
4
4
  # Deadly simple SQL builder
5
5
  # @!visibility private
6
6
  class SQLBuilder
7
+ class << self
8
+ # @param builders [Array<PgEventstore::SQLBuilder>]
9
+ # @return [PgEventstore::SQLBuilder]
10
+ def union_builders(builders)
11
+ builders[1..].each_with_object(builders[0]) do |builder, first_builder|
12
+ first_builder.union(builder)
13
+ end
14
+ end
15
+ end
16
+
7
17
  def initialize
8
18
  @select_values = []
9
19
  @from_value = nil
@@ -1,9 +1,9 @@
1
1
  # frozen_string_literal: true
2
2
 
3
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:
4
+ # Implements simple background job runner. A job execution is done via declaring a callback on a specific action. The
5
+ # implementation also allows you to hook into different places of life cycle of the runner by defining callbacks on
6
+ # various actions. Here is the list of available actions:
7
7
  # - :before_runner_started. Happens before the runner's state switches from "initial"/"stopped" to "running" and
8
8
  # runner's thread is started. It is also fired when the runner is restoring - right after :before_runner_restored
9
9
  # action.
@@ -24,12 +24,41 @@ module PgEventstore
24
24
  #
25
25
  # def_delegators :@basic_runner, :start, :stop, :wait_for_finish, :stop_async, :restore
26
26
  #
27
+ # class SimpleRecoveryStrategy
28
+ # include PgEventstore::RunnerRecoveryStrategy
29
+ #
30
+ # def initialize(restore_func)
31
+ # @attempts_count = 0
32
+ # @restore_func = restore_func
33
+ # end
34
+ #
35
+ # def recovers?(error)
36
+ # error.message.include?("I can not handle this any more!")
37
+ # end
38
+ #
39
+ # def recover(error)
40
+ # (@attempts_count < 3).tap do |res|
41
+ # @attempts_count += 1
42
+ # @restore_func.call if res
43
+ # end
44
+ # end
45
+ # end
46
+ #
27
47
  # def initialize
28
- # @basic_runner = PgEventstore::BasicRunner.new(1, 2)
48
+ # @basic_runner = PgEventstore::BasicRunner.new(
49
+ # run_interval: 1, async_shutdown_time: 2, recovery_strategies: recovery_strategies
50
+ # )
29
51
  # @jobs_performed = 0
52
+ # @jobs_limit = 3
30
53
  # attach_runner_callbacks
31
54
  # end
32
55
  #
56
+ # protected
57
+ #
58
+ # def work_harder
59
+ # @jobs_limit += 3
60
+ # end
61
+ #
33
62
  # private
34
63
  #
35
64
  # def attach_runner_callbacks
@@ -42,7 +71,7 @@ module PgEventstore
42
71
  # end
43
72
  #
44
73
  # def process_action
45
- # raise "What's the point? I can not handle this any more!" if @jobs_performed >= 3
74
+ # raise "What's the point? I can not handle this any more!" if @jobs_performed >= @jobs_limit
46
75
  # puts "Doing some heavy lifting job"
47
76
  # sleep 2 # Simulate long running job
48
77
  # end
@@ -67,6 +96,10 @@ module PgEventstore
67
96
  # def after_runner_died(error)
68
97
  # puts "Error occurred: #{error.inspect}"
69
98
  # end
99
+ #
100
+ # def recovery_strategies
101
+ # [SimpleRecoveryStrategy.new(method(:work_harder))]
102
+ # end
70
103
  # end
71
104
  #
72
105
  # runner = MyAwesomeRunner.new
@@ -85,11 +118,13 @@ module PgEventstore
85
118
  # :after_runner_stopped callback
86
119
  # @param async_shutdown_time [Integer, Float] seconds. Determines how long to wait before force-shutdown the runner.
87
120
  # It is only meaningful for the #stop_async
88
- def initialize(run_interval, async_shutdown_time)
121
+ def initialize(run_interval:, async_shutdown_time:, recovery_strategies: [])
89
122
  @run_interval = run_interval
90
123
  @async_shutdown_time = async_shutdown_time
124
+ @recovery_strategies = recovery_strategies
91
125
  @state = RunnerState.new
92
126
  @mutex = Thread::Mutex.new
127
+ @runner = nil
93
128
  delegate_change_state_cbx
94
129
  end
95
130
 
@@ -127,36 +162,40 @@ module PgEventstore
127
162
  synchronize do
128
163
  return self unless @state.running? || @state.dead?
129
164
 
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
165
+ begin
166
+ @state.halting!
167
+ ensure
168
+ Thread.new do
169
+ stopping_at = Time.now.utc
170
+ halt = false
171
+ loop do
172
+ synchronize do
173
+ # Give the runner up to @async_shutdown_time seconds for graceful shutdown
174
+ @runner&.exit if Time.now.utc - stopping_at > @async_shutdown_time
175
+
176
+ unless @runner&.alive?
177
+ @state.stopped!
178
+ callbacks.run_callbacks(:after_runner_stopped)
179
+ end
180
+ ensure
181
+ next if @runner&.alive?
138
182
 
139
- unless @runner&.alive?
140
- @state.stopped!
141
183
  @runner = nil
142
- callbacks.run_callbacks(:after_runner_stopped)
143
184
  halt = true
144
185
  end
186
+ break if halt
187
+ sleep 0.1
145
188
  end
146
- break if halt
147
- sleep 0.1
148
189
  end
149
190
  end
150
- self
151
191
  end
192
+ self
152
193
  end
153
194
 
154
195
  # Restores the runner after its death.
155
196
  # @return [self]
156
197
  def restore
157
- synchronize do
158
- return self unless @state.dead?
159
-
198
+ within_state(:dead) do
160
199
  callbacks.run_callbacks(:before_runner_restored)
161
200
  _start
162
201
  end
@@ -183,7 +222,7 @@ module PgEventstore
183
222
  end
184
223
 
185
224
  # @param state [Symbol]
186
- # @return [Object] a result of evaluating of passed block
225
+ # @return [Object, nil] a result of evaluating of passed block
187
226
  def within_state(state, &blk)
188
227
  synchronize do
189
228
  return unless @state.public_send("#{RunnerState::STATES.fetch(state)}?")
@@ -192,6 +231,20 @@ module PgEventstore
192
231
  end
193
232
  end
194
233
 
234
+ protected
235
+
236
+ # @param error [StandardError]
237
+ # @param strategy [PgEventstore::RunnerRecoveryStrategy]
238
+ # @param current_runner_id [Integer]
239
+ # @return [Thread]
240
+ def async_recover(error, strategy, current_runner_id)
241
+ Thread.new do
242
+ init_recovery_ripper(current_runner_id)
243
+ Thread.current.exit unless strategy.recover(error)
244
+ recoverable { restore }
245
+ end
246
+ end
247
+
195
248
  private
196
249
 
197
250
  def synchronize
@@ -201,19 +254,15 @@ module PgEventstore
201
254
  # @return [void]
202
255
  def _start
203
256
  @state.running!
257
+ ensure
204
258
  @runner = Thread.new do
205
- loop do
206
- Thread.current.exit unless @state.running?
207
- sleep @run_interval
208
-
209
- callbacks.run_callbacks(:process_async)
210
- end
211
- rescue => error
212
- synchronize do
213
- raise unless @state.halting? || @state.running?
259
+ recoverable do
260
+ loop do
261
+ Thread.current.exit unless @state.running?
262
+ sleep @run_interval
214
263
 
215
- @state.dead!
216
- callbacks.run_callbacks(:after_runner_died, error)
264
+ callbacks.run_callbacks(:process_async)
265
+ end
217
266
  end
218
267
  end
219
268
  end
@@ -228,5 +277,43 @@ module PgEventstore
228
277
  def change_state(...)
229
278
  callbacks.run_callbacks(:change_state, ...)
230
279
  end
280
+
281
+ # @param error [StandardError]
282
+ # @return [PgEventstore::RunnerRecoveryStrategy, nil]
283
+ def suitable_strategy(error)
284
+ @recovery_strategies.find { _1.recovers?(error) }
285
+ end
286
+
287
+ # @return [void]
288
+ def recoverable
289
+ yield
290
+ rescue => error
291
+ synchronize do
292
+ raise unless @state.halting? || @state.running?
293
+
294
+ recovery_strategy = suitable_strategy(error)
295
+ @state.dead!
296
+ callbacks.run_callbacks(:after_runner_died, error)
297
+ ensure
298
+ async_recover(error, recovery_strategy, @runner.__id__) if recovery_strategy
299
+ end
300
+ end
301
+
302
+ # @param current_runner_id [Integer]
303
+ # @return [Thread]
304
+ def init_recovery_ripper(current_runner_id)
305
+ recovery_job = Thread.current
306
+ Thread.new do
307
+ loop do
308
+ synchronize do
309
+ recovery_job.exit unless @state.dead?
310
+ recovery_job.exit unless current_runner_id == @runner.__id__
311
+ end
312
+ break unless recovery_job.alive?
313
+
314
+ sleep 1
315
+ end
316
+ end
317
+ end
231
318
  end
232
319
  end
@@ -1,6 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module PgEventstore
4
+ # @!visibility private
4
5
  class CommandsHandlerHandlers
5
6
  include Extensions::CallbackHandlersExtension
6
7
 
@@ -19,20 +20,6 @@ module PgEventstore
19
20
  def process_runners_commands(config_name, runners, subscription_feeder)
20
21
  CommandHandlers::SubscriptionRunnersCommands.new(config_name, runners, subscription_feeder.id).process
21
22
  end
22
-
23
- # @param basic_runner [PgEventstore::BasicRunner]
24
- # @param restart_delay [Integer]
25
- # @param error [StandardError]
26
- # @return [void]
27
- def restore_runner(basic_runner, restart_delay, error)
28
- PgEventstore.logger&.error "PgEventstore::CommandsHandler: Error occurred: #{error.message}"
29
- PgEventstore.logger&.error "PgEventstore::CommandsHandler: Backtrace: #{error.backtrace&.join("\n")}"
30
- PgEventstore.logger&.error "PgEventstore::CommandsHandler: Trying to auto-repair in #{restart_delay} seconds..."
31
- Thread.new do
32
- sleep restart_delay
33
- basic_runner.restore
34
- end
35
- end
36
23
  end
37
24
  end
38
25
  end
@@ -1,6 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module PgEventstore
4
+ # @!visibility private
4
5
  class EventsProcessorHandlers
5
6
  include Extensions::CallbackHandlersExtension
6
7
 
@@ -15,10 +16,10 @@ module PgEventstore
15
16
 
16
17
  callbacks.run_callbacks(:process, Utils.original_global_position(raw_event)) do
17
18
  handler.call(raw_event)
19
+ rescue => exception
20
+ raw_events.unshift(raw_event)
21
+ raise Utils.wrap_exception(exception, global_position: Utils.original_global_position(raw_event))
18
22
  end
19
- rescue => exception
20
- raw_events.unshift(raw_event)
21
- raise Utils.wrap_exception(exception, global_position: Utils.original_global_position(raw_event))
22
23
  end
23
24
 
24
25
  # @param callbacks [PgEventstore::Callbacks]
@@ -1,6 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module PgEventstore
4
+ # @!visibility private
4
5
  class SubscriptionFeederHandlers
5
6
  include Extensions::CallbackHandlersExtension
6
7
 
@@ -39,20 +40,6 @@ module PgEventstore
39
40
  )
40
41
  end
41
42
 
42
- # @param subscriptions_set_lifecycle [PgEventstore::SubscriptionsSetLifecycle]
43
- # @param basic_runner [PgEventstore::BasicRunner]
44
- # @param _error [StandardError]
45
- # @return [void]
46
- def restart_runner(subscriptions_set_lifecycle, basic_runner, _error)
47
- subscriptions_set = subscriptions_set_lifecycle.persisted_subscriptions_set
48
- return if subscriptions_set.restart_count >= subscriptions_set.max_restarts_number
49
-
50
- Thread.new do
51
- sleep subscriptions_set.time_between_restarts
52
- basic_runner.restore
53
- end
54
- end
55
-
56
43
  # @param subscriptions_set_lifecycle [PgEventstore::SubscriptionsSetLifecycle]
57
44
  # @return [void]
58
45
  def ping_subscriptions_set(subscriptions_set_lifecycle)
@@ -1,6 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module PgEventstore
4
+ # @!visibility private
4
5
  class SubscriptionRunnerHandlers
5
6
  include Extensions::CallbackHandlersExtension
6
7
 
@@ -32,25 +33,6 @@ module PgEventstore
32
33
  subscription.update(last_error: Utils.error_info(error), last_error_occurred_at: Time.now.utc)
33
34
  end
34
35
 
35
- # @param subscription [PgEventstore::Subscription]
36
- # @param restart_terminator [#call, nil]
37
- # @param failed_subscription_notifier [#call, nil]
38
- # @param events_processor [PgEventstore::EventsProcessor]
39
- # @param error [PgEventstore::WrappedException]
40
- # @return [void]
41
- def restart_events_processor(subscription, restart_terminator, failed_subscription_notifier, events_processor,
42
- error)
43
- return if restart_terminator&.call(subscription.dup)
44
- if subscription.restart_count >= subscription.max_restarts_number
45
- return failed_subscription_notifier&.call(subscription.dup, Utils.unwrap_exception(error))
46
- end
47
-
48
- Thread.new do
49
- sleep subscription.time_between_restarts
50
- events_processor.restore
51
- end
52
- end
53
-
54
36
  # @param subscription [PgEventstore::Subscription]
55
37
  # @param global_position [Integer]
56
38
  # @return [void]
@@ -2,6 +2,7 @@
2
2
 
3
3
  module PgEventstore
4
4
  module CommandHandlers
5
+ # @!visibility private
5
6
  class SubscriptionFeederCommands
6
7
  # @param config_name [Symbol]
7
8
  # @param subscription_feeder [PgEventstore::SubscriptionFeeder]
@@ -2,6 +2,7 @@
2
2
 
3
3
  module PgEventstore
4
4
  module CommandHandlers
5
+ # @!visibility private
5
6
  class SubscriptionRunnersCommands
6
7
  # @param config_name [Symbol]
7
8
  # @param runners [Array<PgEventstore::SubscriptionRunner>]
@@ -10,8 +10,6 @@ module PgEventstore
10
10
  class CommandsHandler
11
11
  extend Forwardable
12
12
 
13
- # @return [Integer] the delay in seconds between runner restarts
14
- RESTART_DELAY = 5
15
13
  # @return [Integer] seconds, how often to check for new commands
16
14
  PULL_INTERVAL = 1
17
15
 
@@ -24,7 +22,11 @@ module PgEventstore
24
22
  @config_name = config_name
25
23
  @subscription_feeder = subscription_feeder
26
24
  @runners = runners
27
- @basic_runner = BasicRunner.new(PULL_INTERVAL, 0)
25
+ @basic_runner = BasicRunner.new(
26
+ run_interval: PULL_INTERVAL,
27
+ async_shutdown_time: 0,
28
+ recovery_strategies: [RunnerRecoveryStrategies::RestoreConnection.new(config_name)]
29
+ )
28
30
  attach_runner_callbacks
29
31
  end
30
32
 
@@ -39,11 +41,6 @@ module PgEventstore
39
41
  :process_async, :before,
40
42
  CommandsHandlerHandlers.setup_handler(:process_runners_commands, @config_name, @runners, @subscription_feeder)
41
43
  )
42
-
43
- @basic_runner.define_callback(
44
- :after_runner_died, :before,
45
- CommandsHandlerHandlers.setup_handler(:restore_runner, @basic_runner, RESTART_DELAY)
46
- )
47
44
  end
48
45
  end
49
46
  end