pg_eventstore 1.13.3 → 1.13.4

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 (32) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +7 -3
  3. data/lib/pg_eventstore/event.rb +2 -0
  4. data/lib/pg_eventstore/queries/event_queries.rb +4 -3
  5. data/lib/pg_eventstore/queries/partition_queries.rb +71 -6
  6. data/lib/pg_eventstore/query_builders/events_filtering.rb +1 -5
  7. data/lib/pg_eventstore/query_builders/partitions_filtering.rb +26 -16
  8. data/lib/pg_eventstore/sql_builder.rb +30 -12
  9. data/lib/pg_eventstore/subscriptions/callback_handlers/subscription_runner_handlers.rb +9 -0
  10. data/lib/pg_eventstore/subscriptions/queries/service_queries.rb +73 -0
  11. data/lib/pg_eventstore/subscriptions/queries/subscription_queries.rb +1 -0
  12. data/lib/pg_eventstore/subscriptions/subscription.rb +11 -1
  13. data/lib/pg_eventstore/subscriptions/subscription_feeder.rb +7 -1
  14. data/lib/pg_eventstore/subscriptions/subscription_position_evaluation.rb +195 -0
  15. data/lib/pg_eventstore/subscriptions/subscription_runner.rb +18 -2
  16. data/lib/pg_eventstore/subscriptions/subscription_runners_feeder.rb +1 -1
  17. data/lib/pg_eventstore/subscriptions/subscriptions_manager.rb +12 -1
  18. data/lib/pg_eventstore/version.rb +1 -1
  19. data/lib/pg_eventstore/web/paginator/events_collection.rb +1 -1
  20. data/lib/pg_eventstore/web/paginator/stream_ids_collection.rb +2 -2
  21. data/sig/pg_eventstore/event.rbs +3 -1
  22. data/sig/pg_eventstore/queries/partition_queries.rbs +5 -1
  23. data/sig/pg_eventstore/query_builders/partitions_filtering.rbs +9 -5
  24. data/sig/pg_eventstore/sql_builder.rbs +8 -2
  25. data/sig/pg_eventstore/subscriptions/callback_handlers/subscription_runner_handlers.rbs +3 -0
  26. data/sig/pg_eventstore/subscriptions/queries/service_queries.rbs +15 -0
  27. data/sig/pg_eventstore/subscriptions/subscription.rbs +2 -0
  28. data/sig/pg_eventstore/subscriptions/subscription_feeder.rbs +2 -0
  29. data/sig/pg_eventstore/subscriptions/subscription_position_evaluation.rbs +53 -0
  30. data/sig/pg_eventstore/subscriptions/subscription_runner.rbs +3 -2
  31. data/sig/pg_eventstore/subscriptions/subscriptions_manager.rbs +2 -0
  32. metadata +5 -1
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 12b4fa45d100b1c19c06d3ec5066b1ba3016059d6465517f814f435427b68581
4
- data.tar.gz: 86b78cbbe11da08a8d4fd1abeca95af36089ae0837abda4fd830d34dbf97badc
3
+ metadata.gz: 94443dc3aa36701d5e126573a5babdd4585932d803b766b06b94c861f6b4ef5a
4
+ data.tar.gz: 86fcca21a3a2d3da35f3c760542b23cc97ae93ce97bae7b9baf011a957a9ffea
5
5
  SHA512:
6
- metadata.gz: 80f670af45edd8684746bb055c18c9174ed85e11ccfc4dede1cb129e83e1e8059be7ad9a8cd5880323c33595e8ad40dd8f81c7e4cce4c320bd60df29011349a7
7
- data.tar.gz: 504bf0e5dcc20e134c7c2d3ef7ddfcdcf1af4b8c07567f251f00a0326f7441e0f4bab52b8a82d9abe90eadd27fc8e07b47a9b7f5eba53cc092bc2d32bef54caf
6
+ metadata.gz: 4fb5ed37edce4c557082ba60cfa18f7209f795fa29b98dff9f848bb83b0b1cec889f4a7ca7f77bef5c0d9c6b4065d53765dcfcdf50dffe2628c3e3ffb57d26ad
7
+ data.tar.gz: 0bc33722f13c57c11888ac205873ac93a3610ef5218e8dccf7c3e64d671852757e9d755ffe17ea8176b238dbe1722df47db522b5252ddceed52e14534c164c45
data/CHANGELOG.md CHANGED
@@ -1,5 +1,9 @@
1
1
  ## [Unreleased]
2
2
 
3
+ ## [1.13.4]
4
+
5
+ - Fix subscriptions potentially skipping events when multiple events are appended in concurrent transactions
6
+
3
7
  ## [1.13.3]
4
8
 
5
9
  - Reduce subscription delays for newly published events
@@ -118,7 +122,7 @@ subscriptions_manager.subscribe(
118
122
  ```
119
123
 
120
124
  ## [1.1.5]
121
- - Review the way to handle SubscriptionAlreadyLockedError error. This removes noise when attempting to lock an already locked subscription.
125
+ - Review the way to handle SubscriptionAlreadyLockedError error. This removes noise when attempting to lock an already locked subscription.
122
126
 
123
127
  ## [1.1.4]
124
128
  - Add rbs signatures
@@ -167,7 +171,7 @@ subscriptions_manager.subscribe(
167
171
 
168
172
  ## [0.10.2] - 2024-03-13
169
173
 
170
- - Review the approach to resolve link events
174
+ - Review the approach to resolve link events
171
175
  - Fix subscriptions restart interval option not being processed correctly
172
176
 
173
177
  ## [0.10.1] - 2024-03-12
@@ -230,7 +234,7 @@ subscriptions_manager.subscribe(
230
234
 
231
235
  ## [0.3.0] - 2024-01-24
232
236
 
233
- - Log SQL queries when `PgEvenstore.logger` is set and it is in `:debug` mode
237
+ - Log SQL queries when `PgEvenstore.logger` is set and it is in `:debug` mode
234
238
 
235
239
  ## [0.2.6] - 2023-12-20
236
240
 
@@ -6,6 +6,8 @@ module PgEventstore
6
6
 
7
7
  # @return [String] a type of link event
8
8
  LINK_TYPE = '$>'
9
+ # @return [String]
10
+ PRIMARY_TABLE_NAME = 'events'
9
11
 
10
12
  # @!attribute id
11
13
  # @return [String] UUIDv4 string
@@ -28,7 +28,8 @@ module PgEventstore
28
28
  def event_exists?(event)
29
29
  return false if event.id.nil? || event.stream.nil?
30
30
 
31
- sql_builder = SQLBuilder.new.select('1 as exists').from('events').where('id = ?', event.id).limit(1)
31
+ sql_builder = SQLBuilder.new.select('1 as exists').from(Event::PRIMARY_TABLE_NAME).where('id = ?', event.id)
32
+ sql_builder.limit(1)
32
33
  sql_builder.where(
33
34
  'context = ? and stream_name = ? and type = ?', event.stream.context, event.stream.stream_name, event.type
34
35
  )
@@ -42,7 +43,7 @@ module PgEventstore
42
43
  # @param events [Array<PgEventstore::Event>]
43
44
  # @return [Array<String>]
44
45
  def ids_from_db(events)
45
- sql_builder = SQLBuilder.new.from('events').select('id')
46
+ sql_builder = SQLBuilder.new.from(Event::PRIMARY_TABLE_NAME).select('id')
46
47
  partition_attrs = events.map { |event| [event.stream&.context, event.stream&.stream_name, event.type] }.uniq
47
48
  partition_attrs.each do |context, stream_name, event_type|
48
49
  sql_builder.where_or('context = ? and stream_name = ? and type = ?', context, stream_name, event_type)
@@ -57,7 +58,7 @@ module PgEventstore
57
58
  # @param stream [PgEventstore::Stream]
58
59
  # @return [Integer, nil]
59
60
  def stream_revision(stream)
60
- sql_builder = SQLBuilder.new.from('events').select('stream_revision')
61
+ sql_builder = SQLBuilder.new.from(Event::PRIMARY_TABLE_NAME).select('stream_revision')
61
62
  sql_builder.where('context = ? and stream_name = ? and stream_id = ?', *stream.to_a)
62
63
  sql_builder.order('stream_revision DESC').limit(1)
63
64
  connection.with do |conn|
@@ -178,14 +178,42 @@ module PgEventstore
178
178
 
179
179
  # @param stream_filters [Array<Hash[Symbol, String]>]
180
180
  # @param event_filters [Array<String>]
181
+ # @param scope [Symbol] what kind of partition we want to receive. Available options are :event_type, :context,
182
+ # :stream_name and :auto. In :auto mode the scope will be calculated based on stream_filters and event_filters.
181
183
  # @return [Array<PgEventstore::Partition>]
182
- def partitions(stream_filters, event_filters)
183
- partitions_filter = QueryBuilders::PartitionsFiltering.new
184
- stream_filters.each { |attrs| partitions_filter.add_stream_attrs(**attrs) }
185
- partitions_filter.add_event_types(event_filters)
186
- partitions_filter.with_event_types
184
+ def partitions(stream_filters, event_filters, scope: :event_type)
185
+ stream_filters = stream_filters.select { QueryBuilders::PartitionsFiltering.correct_stream_filter?(_1) }
186
+ sql_builder =
187
+ if event_filters.any?
188
+ # When event type filters are present - they apply constraints to any stream filter. Thus, we can't look up
189
+ # partitions by stream attributes separately.
190
+ filter = QueryBuilders::PartitionsFiltering.new
191
+ stream_filters.each { |attrs| filter.add_stream_attrs(**attrs) }
192
+ filter.add_event_types(event_filters)
193
+ set_partitions_scope(filter, stream_filters, event_filters, scope)
194
+ else
195
+ # When event type filters are absent - we can look up partitions by context and context/stream_name
196
+ # separately, thus potentially producing one-to-one mapping of filter-to-partition with :auto scope. For
197
+ # example, let's say we have stream attributes filter like
198
+ # [{ context: 'FooCtx', stream_name: 'Bar'}, { context: 'BarCtx' }], then we would be able to look up
199
+ # partitions by the exact match, returning only two of them according to the provided filters - stream
200
+ # partition for first filter and context partition for second filter.
201
+ builders = stream_filters.map do |attrs|
202
+ filter = QueryBuilders::PartitionsFiltering.new
203
+ filter.add_stream_attrs(**attrs)
204
+ set_partitions_scope(filter, [attrs], event_filters, scope)
205
+ end
206
+
207
+ sql_builder = SQLBuilder.union_builders(builders) if builders.any?
208
+ sql_builder ||
209
+ begin
210
+ builder = QueryBuilders::PartitionsFiltering.new
211
+ set_partitions_scope(builder, stream_filters, event_filters, scope)
212
+ end
213
+ end
214
+
187
215
  connection.with do |conn|
188
- conn.exec_params(*partitions_filter.to_exec_params)
216
+ conn.exec_params(*sql_builder.to_exec_params)
189
217
  end.map(&method(:deserialize))
190
218
  end
191
219
 
@@ -210,6 +238,43 @@ module PgEventstore
210
238
 
211
239
  private
212
240
 
241
+ # @param partitions_filter [PgEventstore::QueryBuilders::PartitionsFiltering]
242
+ # @param stream_filters [Array<Hash[Symbol, String]>]
243
+ # @param event_filters [Array<String>]
244
+ # @param scope [Symbol]
245
+ # @return [PgEventstore::SQLBuilder]
246
+ def set_partitions_scope(partitions_filter, stream_filters, event_filters, scope)
247
+ case scope
248
+ when :event_type
249
+ partitions_filter.with_event_types
250
+ when :stream_name
251
+ filter = QueryBuilders::PartitionsFiltering.new
252
+ filter.without_event_types
253
+ filter.with_stream_names
254
+ builder = filter.to_sql_builder
255
+ builder.where(
256
+ '(context, stream_name) in ?',
257
+ partitions_filter.to_sql_builder.unselect.select('context, stream_name').group('context, stream_name')
258
+ )
259
+ when :context
260
+ filter = QueryBuilders::PartitionsFiltering.new
261
+ filter.without_event_types
262
+ filter.without_stream_names
263
+ builder = filter.to_sql_builder
264
+ builder.where('context in ?', partitions_filter.to_sql_builder.unselect.select('context').group('context'))
265
+ when :auto
266
+ if event_filters.any?
267
+ set_partitions_scope(partitions_filter, stream_filters, event_filters, :event_type)
268
+ elsif stream_filters.any? { _1[:stream_name] }
269
+ set_partitions_scope(partitions_filter, stream_filters, event_filters, :stream_name)
270
+ else
271
+ set_partitions_scope(partitions_filter, stream_filters, event_filters, :context)
272
+ end
273
+ else
274
+ raise NotImplementedError, "Don't know how to handle #{scope.inspect} scope!"
275
+ end
276
+ end
277
+
213
278
  # @param attrs [Hash]
214
279
  # @return [PgEventstore::Partition]
215
280
  def deserialize(attrs)
@@ -4,10 +4,6 @@ module PgEventstore
4
4
  module QueryBuilders
5
5
  # @!visibility private
6
6
  class EventsFiltering < BasicFiltering
7
- # @return [String]
8
- TABLE_NAME = 'events'
9
- private_constant :TABLE_NAME
10
-
11
7
  # @return [Integer]
12
8
  DEFAULT_LIMIT = 1_000
13
9
  # @return [Hash<String => String, Symbol => String>]
@@ -107,7 +103,7 @@ module PgEventstore
107
103
 
108
104
  # @return [String]
109
105
  def to_table_name
110
- TABLE_NAME
106
+ Event::PRIMARY_TABLE_NAME
111
107
  end
112
108
 
113
109
  # @param context [String, nil]
@@ -28,6 +28,19 @@ module PgEventstore
28
28
  end
29
29
  streams || []
30
30
  end
31
+
32
+ # @param stream_attrs [Hash]
33
+ # @return [Boolean]
34
+ def correct_stream_filter?(stream_attrs)
35
+ result = (stream_attrs in { context: String, stream_name: String } | { context: String })
36
+ return true if result
37
+
38
+ PgEventstore.logger&.debug(<<~TEXT)
39
+ Ignoring unsupported stream filter format for grouped read #{stream_attrs.compact.inspect}. \
40
+ See docs/reading_events.md docs for supported formats.
41
+ TEXT
42
+ false
43
+ end
31
44
  end
32
45
 
33
46
  # @return [String]
@@ -37,10 +50,10 @@ module PgEventstore
37
50
 
38
51
  # @param context [String, nil]
39
52
  # @param stream_name [String, nil]
40
- # @return [void]
53
+ # @return [PgEventstore::SQLBuilder]
41
54
  def add_stream_attrs(context: nil, stream_name: nil)
42
55
  stream_attrs = { context: context, stream_name: stream_name }
43
- return unless correct_stream_filter?(stream_attrs)
56
+ return @sql_builder unless self.class.correct_stream_filter?(stream_attrs)
44
57
 
45
58
  stream_attrs.compact!
46
59
  sql = stream_attrs.map do |attr, _|
@@ -50,31 +63,28 @@ module PgEventstore
50
63
  end
51
64
 
52
65
  # @param event_types [Array<String>]
53
- # @return [void]
66
+ # @return [PgEventstore::SQLBuilder]
54
67
  def add_event_types(event_types)
55
- return if event_types.empty?
68
+ return @sql_builder if event_types.empty?
56
69
 
57
70
  @sql_builder.where("#{to_table_name}.event_type = ANY(?::varchar[])", event_types)
58
71
  end
59
72
 
60
- # @return [void]
73
+ # @return [PgEventstore::SQLBuilder]
61
74
  def with_event_types
62
75
  @sql_builder.where('event_type IS NOT NULL')
63
76
  end
64
77
 
65
- private
78
+ def with_stream_names
79
+ @sql_builder.where('stream_name IS NOT NULL')
80
+ end
66
81
 
67
- # @param stream_attrs [Hash]
68
- # @return [Boolean]
69
- def correct_stream_filter?(stream_attrs)
70
- result = (stream_attrs in { context: String, stream_name: String } | { context: String, stream_name: nil })
71
- return true if result
82
+ def without_event_types
83
+ @sql_builder.where('event_type IS NULL')
84
+ end
72
85
 
73
- PgEventstore.logger&.debug(<<~TEXT)
74
- Ignoring unsupported stream filter format for grouped read #{stream_attrs.compact.inspect}. \
75
- See docs/reading_events.md docs for supported formats.
76
- TEXT
77
- false
86
+ def without_stream_names
87
+ @sql_builder.where('stream_name IS NULL')
78
88
  end
79
89
  end
80
90
  end
@@ -64,7 +64,7 @@ module PgEventstore
64
64
  self
65
65
  end
66
66
 
67
- # @param table_name [String]
67
+ # @param table_name [String | SQLBuilder]
68
68
  # @return [self]
69
69
  def from(table_name)
70
70
  @from_value = table_name
@@ -132,7 +132,7 @@ module PgEventstore
132
132
  self
133
133
  end
134
134
 
135
- # @return [Array<String, Array<Object>>]
135
+ # @return [[String, Array<_>]]
136
136
  def to_exec_params
137
137
  @positional_values.clear
138
138
  @positional_values_size = 0
@@ -141,19 +141,27 @@ module PgEventstore
141
141
 
142
142
  protected
143
143
 
144
- # @return [Array<String, Array<Object>>]
144
+ # @return [[String, Array<_>]]
145
145
  def _to_exec_params
146
146
  return [single_query_sql, @positional_values] if @union_values.empty?
147
147
 
148
148
  [union_query_sql, @positional_values]
149
149
  end
150
150
 
151
+ # @return [String]
152
+ def from_sql
153
+ return @from_value if @from_value.is_a?(String)
154
+
155
+ sql = merge(@from_value)
156
+ "(#{sql}) #{@from_value.from_sql}"
157
+ end
158
+
151
159
  private
152
160
 
153
161
  # @return [String]
154
162
  def single_query_sql
155
163
  where_sql = [where_sql('OR'), where_sql('AND')].reject(&:empty?).map { |sql| "(#{sql})" }.join(' AND ')
156
- sql = "SELECT #{select_sql} FROM #{@from_value}"
164
+ sql = "SELECT #{select_sql} FROM #{from_sql}"
157
165
  sql += " #{join_sql}" unless @join_values.empty?
158
166
  sql += " WHERE #{where_sql}" unless where_sql.empty?
159
167
  sql += " GROUP BY #{@group_values.join(', ')}" unless @group_values.empty?
@@ -168,11 +176,7 @@ module PgEventstore
168
176
  sql = single_query_sql
169
177
  union_parts = ["(#{sql})"]
170
178
  union_parts += @union_values.map do |builder|
171
- builder.positional_values_size = @positional_values_size
172
- builder_sql, values = builder._to_exec_params
173
- @positional_values.push(*values)
174
- @positional_values_size += values.size
175
- "(#{builder_sql})"
179
+ "(#{merge(builder)})"
176
180
  end
177
181
  union_parts.join(' UNION ALL ')
178
182
  end
@@ -200,14 +204,28 @@ module PgEventstore
200
204
  @order_values.join(', ')
201
205
  end
202
206
 
207
+ # @param builder [PgEventstore::SQLBuilder]
208
+ # @return [String]
209
+ def merge(builder)
210
+ builder.positional_values_size = @positional_values_size
211
+ sql_query, positional_values = builder._to_exec_params
212
+ @positional_values.push(*positional_values)
213
+ @positional_values_size += positional_values.size
214
+ sql_query
215
+ end
216
+
203
217
  # Replaces "?" signs in the given string with positional variables and memorize positional values they refer to.
204
218
  # @param sql [String]
205
219
  # @return [String]
206
220
  def extract_positional_args(sql, *arguments)
207
221
  sql.gsub('?').each_with_index do |_, index|
208
- @positional_values.push(arguments[index])
209
- @positional_values_size += 1
210
- "$#{@positional_values_size}"
222
+ if arguments[index].is_a?(SQLBuilder)
223
+ "(#{merge(arguments[index])})"
224
+ else
225
+ @positional_values.push(arguments[index])
226
+ @positional_values_size += 1
227
+ "$#{@positional_values_size}"
228
+ end
211
229
  end
212
230
  end
213
231
  end
@@ -52,6 +52,15 @@ module PgEventstore
52
52
  def update_subscription_state(subscription, state)
53
53
  subscription.update(state: state)
54
54
  end
55
+
56
+ # @param subscription_position_evaluation [PgEventstore::SubscriptionPositionEvaluation]
57
+ # @param state [String]
58
+ # @return [void]
59
+ def stop_position_evaluation(subscription_position_evaluation, state)
60
+ return if state == 'running'
61
+
62
+ subscription_position_evaluation.stop_evaluation
63
+ end
55
64
  end
56
65
  end
57
66
  end
@@ -0,0 +1,73 @@
1
+ # frozen_string_literal: true
2
+
3
+ module PgEventstore
4
+ # @!visibility private
5
+ class ServiceQueries
6
+ # @param connection [PgEventstore::Connection]
7
+ def initialize(connection)
8
+ @connection = connection
9
+ end
10
+
11
+ # @param relation_oids [Array<Integer>]
12
+ # @return [Array<String>]
13
+ def relation_transaction_ids(relation_oids)
14
+ result = @connection.with do |conn|
15
+ # Look up transactions that change table's content
16
+ conn.exec_params(
17
+ <<~SQL,
18
+ SELECT virtualtransaction AS trx_id FROM pg_locks
19
+ WHERE relation = ANY($1::oid[]) AND mode = 'RowExclusiveLock'
20
+ SQL
21
+ [relation_oids]
22
+ )
23
+ end
24
+ result.map { _1['trx_id'] }
25
+ end
26
+
27
+ # @param relation_ids [Array<Integer>]
28
+ # @param transaction_ids [Array<String>]
29
+ # @return [Boolean]
30
+ def transactions_in_progress?(relation_ids:, transaction_ids:)
31
+ result = @connection.with do |conn|
32
+ conn.exec_params(
33
+ <<~SQL,
34
+ SELECT 1 as one FROM pg_locks
35
+ WHERE virtualtransaction = ANY($1) AND relation = ANY($2::oid[]) AND mode = 'RowExclusiveLock'
36
+ LIMIT 1
37
+ SQL
38
+ [transaction_ids, relation_ids]
39
+ )
40
+ end
41
+ result.any?
42
+ end
43
+
44
+ # @param table_names [Array<String>] existing table names
45
+ # @return [Integer]
46
+ def max_global_position(table_names)
47
+ return 0 if table_names.empty?
48
+
49
+ partition_builds = table_names.map do |table_name|
50
+ SQLBuilder.new.select("MAX(#{table_name}.global_position) AS global_position").from(table_name)
51
+ end
52
+ sql, positional_values = SQLBuilder.union_builders(partition_builds).to_exec_params
53
+ result = @connection.with do |conn|
54
+ conn.exec_params("SELECT MAX(global_position) AS max_pos FROM (#{sql}) pos", positional_values)
55
+ end
56
+ result.first['max_pos'] || 0
57
+ end
58
+
59
+ # @param table_names [Array<String>]
60
+ # @return [Hash<String, Integer>]
61
+ def relation_ids_by_names(table_names)
62
+ result = @connection.with do |conn|
63
+ conn.exec_params(
64
+ <<~SQL,
65
+ SELECT relname, oid FROM pg_class WHERE relname = ANY($1::varchar[])
66
+ SQL
67
+ [table_names]
68
+ )
69
+ end
70
+ result.each_with_object({}) { |attrs, res| res[attrs['relname']] = attrs['oid'] }
71
+ end
72
+ end
73
+ end
@@ -177,6 +177,7 @@ module PgEventstore
177
177
  # @return [PgEventstore::SQLBuilder]
178
178
  def query_builder(id, options)
179
179
  builder = PgEventstore::QueryBuilders::EventsFiltering.subscriptions_events_filtering(options).to_sql_builder
180
+ builder.where('global_position <= ?', options[:to_position]) if options[:to_position]
180
181
  builder.select("#{id} as runner_id")
181
182
  end
182
183
 
@@ -6,6 +6,16 @@ module PgEventstore
6
6
  include Extensions::UsingConnectionExtension
7
7
  include Extensions::OptionsExtension
8
8
 
9
+ # Determines the minimal allowed value of events pull frequency of the particular subscription. You can find similar
10
+ # constant - SubscriptionFeeder::EVENTS_PULL_INTERVAL. Unlike it - this one is responsible to detect whether the
11
+ # subscription should be included in the subscriptions list to query next chunk of events. Thus, this setting only
12
+ # determines whether it is time to make a request, but how frequent would be the actual request - determines
13
+ # SubscriptionFeeder::EVENTS_PULL_INTERVAL.
14
+ # @see PgEventstore::SubscriptionFeeder::EVENTS_PULL_INTERVAL
15
+ # @return [Float]
16
+ MIN_EVENTS_PULL_INTERVAL = 0.2
17
+ private_constant :MIN_EVENTS_PULL_INTERVAL
18
+
9
19
  # @return [Time]
10
20
  DEFAULT_TIMESTAMP = Time.at(0).utc.freeze
11
21
 
@@ -167,7 +177,7 @@ module PgEventstore
167
177
  restart_count: 0,
168
178
  last_restarted_at: nil,
169
179
  max_restarts_number: max_restarts_number,
170
- chunk_query_interval: chunk_query_interval,
180
+ chunk_query_interval: [chunk_query_interval, MIN_EVENTS_PULL_INTERVAL].max,
171
181
  last_chunk_fed_at: DEFAULT_TIMESTAMP,
172
182
  last_chunk_greatest_position: nil,
173
183
  last_error: nil,
@@ -7,6 +7,12 @@ module PgEventstore
7
7
  class SubscriptionFeeder
8
8
  extend Forwardable
9
9
 
10
+ # Determines how often to fetch events from the event store.
11
+ # @see PgEventstore::Subscription::MIN_EVENTS_PULL_INTERVAL
12
+ # @return [Float]
13
+ EVENTS_PULL_INTERVAL = 0.1 # seconds
14
+ private_constant :EVENTS_PULL_INTERVAL
15
+
10
16
  attr_reader :config_name
11
17
 
12
18
  def_delegators :@basic_runner, :start, :stop, :restore, :state, :wait_for_finish, :stop_async, :running?
@@ -17,7 +23,7 @@ module PgEventstore
17
23
  def initialize(config_name:, subscriptions_set_lifecycle:, subscriptions_lifecycle:)
18
24
  @config_name = config_name
19
25
  @basic_runner = BasicRunner.new(
20
- run_interval: 0.2,
26
+ run_interval: EVENTS_PULL_INTERVAL,
21
27
  async_shutdown_time: 0,
22
28
  recovery_strategies: recovery_strategies(config_name, subscriptions_set_lifecycle)
23
29
  )
@@ -0,0 +1,195 @@
1
+ # frozen_string_literal: true
2
+
3
+ module PgEventstore
4
+ # When subscription pulls events at the edge of the events list and several events arrives concurrently - there is a
5
+ # chance some events will never be picked. Example:
6
+ # - event1 has been assigned global_position#9
7
+ # - event2 has been assigned global_position#10
8
+ # - event1 and event2 are currently in concurrent transactions, but those transactions does not block each other
9
+ # - transaction holding event2 commits first
10
+ # - a subscription picks event2 and sets next position to 11
11
+ # - transaction holding event1 commits
12
+ # Illustration:
13
+ # Time → → → → → → → → → → → → → → → → → → → → → → → → → → → → → → → → → → → → → → → → → → → → → → → → → → → → → → →
14
+ #
15
+ # T1: BEGIN
16
+ # INSERT INTO events(...);
17
+ # --------------------global_position#9------------------->
18
+ # COMMIT
19
+ # T2: BEGIN
20
+ # INSERT INTO events(...);
21
+ # --------------global_position#10-------->
22
+ # COMMIT
23
+ # Query events: SELECT * FROM event WHERE global_position >= 8
24
+ # --------------Picks #8, #10, but never #9-------->
25
+ #
26
+ # To solve this problem we can:
27
+ # 1. pause events fetching for the subscription
28
+ # 2. fetch latest global position that matches subscription's filter
29
+ # 3. wait for all currently running transactions that affects on the subscription to finish
30
+ # 4. unpause events fetching and use this position as a right hand limiter, because we can now confidently say there
31
+ # are no uncommited events that would be lost otherwise
32
+ # This class is responsible for the step 2 and step 3, and persists the last safe position to be used in step 4.
33
+ # @!visibility private
34
+ class SubscriptionPositionEvaluation
35
+ # Determines how often to check the list of currently running transactions
36
+ # @return [Float]
37
+ TRANSACTIONS_STATUS_REFRESH_INTERVAL = 0.05 # seconds
38
+ private_constant :TRANSACTIONS_STATUS_REFRESH_INTERVAL
39
+
40
+ # @param config_name [Symbol]
41
+ # @param filter_options [Hash]
42
+ def initialize(config_name:, filter_options:)
43
+ @config_name = config_name
44
+ @stream_filters = QueryBuilders::PartitionsFiltering.extract_streams_filter(filter: filter_options)
45
+ @event_type_filters = QueryBuilders::PartitionsFiltering.extract_event_types_filter(filter: filter_options)
46
+ @position_to_evaluate = nil
47
+ @position_is_safe = nil
48
+ @last_safe_position = nil
49
+ @runner = nil
50
+ @relation_ids_cache = {}
51
+ @mutex = Mutex.new
52
+ end
53
+
54
+ # @param position_to_evaluate [Integer]
55
+ # @return [self]
56
+ def evaluate(position_to_evaluate)
57
+ unless self.position_to_evaluate == position_to_evaluate
58
+ stop_evaluation
59
+ self.position_to_evaluate = position_to_evaluate
60
+ calculate_safe_position
61
+ end
62
+ self
63
+ end
64
+
65
+ # @return [Boolean]
66
+ def safe?
67
+ @mutex.synchronize { @position_is_safe } || false
68
+ end
69
+
70
+ # @return [Integer, nil]
71
+ def last_safe_position
72
+ @mutex.synchronize { @last_safe_position }
73
+ end
74
+
75
+ # @return [Thread, nil] a runner who is being stopped if any
76
+ def stop_evaluation
77
+ _stop_evaluation(@runner)
78
+ end
79
+
80
+ private
81
+
82
+ # @param runner [Thread, nil]
83
+ # @return [Thread, nil]
84
+ def _stop_evaluation(runner)
85
+ runner&.exit
86
+ self.position_is_safe = nil
87
+ self.last_safe_position = nil
88
+ self.position_to_evaluate = nil
89
+ end
90
+
91
+ # @return [Integer, nil]
92
+ def position_to_evaluate
93
+ @mutex.synchronize { @position_to_evaluate }
94
+ end
95
+
96
+ # @return [Boolean, nil]
97
+ def position_is_safe
98
+ @mutex.synchronize { @position_is_safe }
99
+ end
100
+
101
+ # @param val [Integer, nil]
102
+ # @return [Integer, nil]
103
+ def last_safe_position=(val)
104
+ @mutex.synchronize { @last_safe_position = val }
105
+ end
106
+
107
+ # @param val [Integer, nil]
108
+ # @return [Integer, nil]
109
+ def position_to_evaluate=(val)
110
+ @mutex.synchronize { @position_to_evaluate = val }
111
+ end
112
+
113
+ # @param val [Boolean, nil]
114
+ # @return [Boolean, nil]
115
+ def position_is_safe=(val)
116
+ @mutex.synchronize { @position_is_safe = val }
117
+ end
118
+
119
+ # @return [Array<String>]
120
+ def affected_tables
121
+ if @stream_filters.empty? && @event_type_filters.empty?
122
+ [Event::PRIMARY_TABLE_NAME]
123
+ else
124
+ partition_queries.partitions(@stream_filters, @event_type_filters, scope: :auto).map(&:table_name)
125
+ end
126
+ end
127
+
128
+ # @return [void]
129
+ def calculate_safe_position
130
+ @runner = Thread.new do
131
+ tables_to_track = affected_tables
132
+ update_relation_ids_cache(tables_to_track)
133
+ safe_position = service_queries.max_global_position(tables_to_track)
134
+ trx_ids = transaction_queries.transaction do
135
+ service_queries.relation_transaction_ids(@relation_ids_cache.values)
136
+ end
137
+
138
+ loop do
139
+ transactions_in_progress = service_queries.transactions_in_progress?(
140
+ relation_ids: @relation_ids_cache.values, transaction_ids: trx_ids
141
+ )
142
+ break unless transactions_in_progress
143
+
144
+ sleep TRANSACTIONS_STATUS_REFRESH_INTERVAL
145
+ end
146
+ @mutex.synchronize do
147
+ if safe_position >= @position_to_evaluate
148
+ # We progressed forward. New position can be persisted
149
+ @last_safe_position = safe_position
150
+ @position_is_safe = true
151
+ else
152
+ # safe_position is less than the current position to fetch the events from. This means that no new events
153
+ # are present at this point. We will need to re-evaluate the safe position during next attempts. Until that
154
+ # we can't progress.
155
+ @position_to_evaluate = nil
156
+ end
157
+ end
158
+ rescue
159
+ # Clean up the state immediately in case of error
160
+ _stop_evaluation(nil)
161
+ end
162
+ end
163
+
164
+ # @param tables_to_track [Array<String>]
165
+ # @return [void]
166
+ def update_relation_ids_cache(tables_to_track)
167
+ missing_relation_ids = tables_to_track - @relation_ids_cache.keys
168
+ deleted_relations = @relation_ids_cache.keys - tables_to_track
169
+ @relation_ids_cache = @relation_ids_cache.except(*deleted_relations)
170
+ return if missing_relation_ids.empty?
171
+
172
+ @relation_ids_cache.merge!(service_queries.relation_ids_by_names(missing_relation_ids))
173
+ end
174
+
175
+ # @return [PgEventstore::PartitionQueries]
176
+ def partition_queries
177
+ PartitionQueries.new(connection)
178
+ end
179
+
180
+ # @return [PgEventstore::TransactionQueries]
181
+ def transaction_queries
182
+ TransactionQueries.new(connection)
183
+ end
184
+
185
+ # @return [PgEventstore::ServiceQueries]
186
+ def service_queries
187
+ ServiceQueries.new(connection)
188
+ end
189
+
190
+ # @return [PgEventstore::Connection]
191
+ def connection
192
+ PgEventstore.connection(@config_name)
193
+ end
194
+ end
195
+ end
@@ -28,17 +28,23 @@ module PgEventstore
28
28
  # @param stats [PgEventstore::SubscriptionHandlerPerformance]
29
29
  # @param events_processor [PgEventstore::EventsProcessor]
30
30
  # @param subscription [PgEventstore::Subscription]
31
- def initialize(stats:, events_processor:, subscription:)
31
+ # @param position_evaluation [PgEventstore::SubscriptionPositionEvaluation]
32
+ def initialize(stats:, events_processor:, subscription:, position_evaluation:)
32
33
  @stats = stats
33
34
  @events_processor = events_processor
34
35
  @subscription = subscription
36
+ @position_evaluation = position_evaluation
35
37
 
36
38
  attach_callbacks
37
39
  end
38
40
 
39
41
  # @return [Hash]
40
42
  def next_chunk_query_opts
41
- @subscription.options.merge(from_position: next_chunk_global_position, max_count: estimate_events_number)
43
+ @subscription.options.merge(
44
+ from_position: next_chunk_global_position,
45
+ max_count: estimate_events_number,
46
+ to_position: @position_evaluation.last_safe_position
47
+ )
42
48
  end
43
49
 
44
50
  # @return [Boolean]
@@ -46,6 +52,11 @@ module PgEventstore
46
52
  @subscription.last_chunk_fed_at + @subscription.chunk_query_interval <= Time.now.utc
47
53
  end
48
54
 
55
+ # @return [Boolean]
56
+ def next_chunk_safe?
57
+ estimate_events_number == 0 ? false : @position_evaluation.evaluate(next_chunk_global_position).safe?
58
+ end
59
+
49
60
  private
50
61
 
51
62
  # @return [Integer]
@@ -97,6 +108,11 @@ module PgEventstore
97
108
  :change_state, :after,
98
109
  SubscriptionRunnerHandlers.setup_handler(:update_subscription_state, @subscription)
99
110
  )
111
+ # Prevent dangling position evaluation runner when subscription changes the state to something except 'running'
112
+ @events_processor.define_callback(
113
+ :change_state, :after,
114
+ SubscriptionRunnerHandlers.setup_handler(:stop_position_evaluation, @position_evaluation)
115
+ )
100
116
  end
101
117
  end
102
118
  end
@@ -12,7 +12,7 @@ module PgEventstore
12
12
  # @param runners [Array<PgEventstore::SubscriptionRunner>]
13
13
  # @return [void]
14
14
  def feed(runners)
15
- runners = runners.select(&:running?).select(&:time_to_feed?)
15
+ runners = runners.select(&:running?).select(&:time_to_feed?).select(&:next_chunk_safe?)
16
16
  return if runners.empty?
17
17
 
18
18
  runners_query_options = runners.to_h { |runner| [runner.id, runner.next_chunk_query_opts] }
@@ -10,6 +10,7 @@ require_relative 'synchronized_array'
10
10
  require_relative 'events_processor'
11
11
  require_relative 'subscription_handler_performance'
12
12
  require_relative 'subscription_runner'
13
+ require_relative 'subscription_position_evaluation'
13
14
  require_relative 'subscriptions_set'
14
15
  require_relative 'subscription_runners_feeder'
15
16
  require_relative 'subscriptions_set_lifecycle'
@@ -27,6 +28,7 @@ require_relative 'queries/subscription_command_queries'
27
28
  require_relative 'queries/subscription_queries'
28
29
  require_relative 'queries/subscriptions_set_command_queries'
29
30
  require_relative 'queries/subscriptions_set_queries'
31
+ require_relative 'queries/service_queries'
30
32
  require_relative 'commands_handler'
31
33
 
32
34
  module PgEventstore
@@ -109,7 +111,11 @@ module PgEventstore
109
111
  graceful_shutdown_timeout: graceful_shutdown_timeout,
110
112
  recovery_strategies: recovery_strategies(subscription, restart_terminator, failed_subscription_notifier)
111
113
  ),
112
- subscription: subscription
114
+ subscription: subscription,
115
+ position_evaluation: SubscriptionPositionEvaluation.new(
116
+ config_name: config.name,
117
+ filter_options: options[:filter] || {}
118
+ )
113
119
  )
114
120
 
115
121
  @subscriptions_lifecycle.runners.push(runner)
@@ -186,5 +192,10 @@ module PgEventstore
186
192
  ),
187
193
  ]
188
194
  end
195
+
196
+ # @return [PgEventstore::Connection]
197
+ def connection
198
+ PgEventstore.connection(config_name)
199
+ end
189
200
  end
190
201
  end
@@ -2,5 +2,5 @@
2
2
 
3
3
  module PgEventstore
4
4
  # @return [String]
5
- VERSION = '1.13.3'
5
+ VERSION = '1.13.4'
6
6
  end
@@ -58,7 +58,7 @@ module PgEventstore
58
58
  options.merge(from_position: from_position, max_count: per_page, direction: order == :asc ? :desc : :asc)
59
59
  ).to_sql_builder.unselect.select('global_position').offset(1)
60
60
  sql, params = sql_builder.to_exec_params
61
- sql = "SELECT * FROM (#{sql}) events ORDER BY global_position #{order} LIMIT 1"
61
+ sql = "SELECT * FROM (#{sql}) #{Event::PRIMARY_TABLE_NAME} ORDER BY global_position #{order} LIMIT 1"
62
62
  connection.with do |conn|
63
63
  conn.exec_params(sql, params)
64
64
  end.to_a.dig(0, 'global_position')
@@ -11,7 +11,7 @@ module PgEventstore
11
11
  def collection
12
12
  @collection ||=
13
13
  begin
14
- sql_builder = SQLBuilder.new.select('stream_id').from('events')
14
+ sql_builder = SQLBuilder.new.select('stream_id').from(Event::PRIMARY_TABLE_NAME)
15
15
  sql_builder.where('context = ? and stream_name = ?', options[:context], options[:stream_name])
16
16
  sql_builder.where('stream_id like ?', "#{options[:query]}%")
17
17
  sql_builder.where("stream_id #{direction_operator} ?", starting_id) if starting_id
@@ -27,7 +27,7 @@ module PgEventstore
27
27
  return unless collection.size == per_page
28
28
 
29
29
  starting_id = collection.first['stream_id']
30
- sql_builder = SQLBuilder.new.select('stream_id').from('events')
30
+ sql_builder = SQLBuilder.new.select('stream_id').from(Event::PRIMARY_TABLE_NAME)
31
31
  sql_builder.where("stream_id #{direction_operator} ?", starting_id)
32
32
  sql_builder.where('stream_id like ?', "#{options[:query]}%")
33
33
  sql_builder.where('context = ? and stream_name = ?', options[:context], options[:stream_name])
@@ -1,7 +1,9 @@
1
1
  module PgEventstore
2
2
  class Event
3
- include PgEventstore::Extensions::OptionsExtension
3
+ include Extensions::OptionsExtension
4
+
4
5
  LINK_TYPE: String
6
+ PRIMARY_TABLE_NAME: String
5
7
 
6
8
  def ==: ((Object | PgEventstore::Event) other) -> bool
7
9
 
@@ -39,7 +39,8 @@ module PgEventstore
39
39
  # _@return_ — partition attributes
40
40
  def context_partition: (PgEventstore::Stream stream) -> ::Hash[untyped, untyped]?
41
41
 
42
- def partitions: (Array[Hash[Symbol, String | nil]] stream_filters, Array[String] event_filters)-> Array[Partition]
42
+ def partitions: (Array[Hash[Symbol, String | nil]] stream_filters, Array[String] event_filters,
43
+ ?scope: Symbol) -> Array[Partition]
43
44
 
44
45
  # _@param_ `stream`
45
46
  #
@@ -76,5 +77,8 @@ module PgEventstore
76
77
  private
77
78
 
78
79
  def deserialize: (Hash[untyped, untyped] attrs)-> Partition
80
+
81
+ def set_partitions_scope: (QueryBuilders::PartitionsFiltering partitions_filter, Array[untyped] stream_filters,
82
+ Array[untyped] event_filters, Symbol scope) -> SQLBuilder
79
83
  end
80
84
  end
@@ -7,15 +7,19 @@ module PgEventstore
7
7
 
8
8
  def self.extract_streams_filter: (Hash[untyped, untyped] options) -> Array[Hash[untyped, untyped]]
9
9
 
10
- def add_event_types: (::Array[String] event_types) -> void
10
+ def self.correct_stream_filter?: (::Hash[untyped, untyped] stream_attrs) -> bool
11
11
 
12
- def add_stream_attrs: (?context: String?, ?stream_name: String?) -> void
12
+ def add_event_types: (::Array[String] event_types) -> SQLBuilder
13
13
 
14
- def with_event_types: -> void
14
+ def add_stream_attrs: (?context: String?, ?stream_name: String?) -> SQLBuilder
15
15
 
16
- private
16
+ def with_event_types: -> SQLBuilder
17
17
 
18
- def correct_stream_filter?: (::Hash[untyped, untyped] stream_attrs) -> bool
18
+ def with_stream_names: -> SQLBuilder
19
+
20
+ def without_event_types: -> SQLBuilder
21
+
22
+ def without_stream_names: -> SQLBuilder
19
23
  end
20
24
  end
21
25
  end
@@ -2,6 +2,8 @@ module PgEventstore
2
2
  class SQLBuilder
3
3
  def initialize: () -> void
4
4
 
5
+ def from_sql: -> String
6
+
5
7
  # _@param_ `sql`
6
8
  def select: (String sql) -> self
7
9
 
@@ -18,7 +20,7 @@ module PgEventstore
18
20
  def where_or: (String sql, *Object arguments) -> self
19
21
 
20
22
  # _@param_ `table_name`
21
- def from: (String table_name) -> self
23
+ def from: (String | SQLBuilder table_name) -> self
22
24
 
23
25
  # _@param_ `sql`
24
26
  #
@@ -53,7 +55,7 @@ module PgEventstore
53
55
  # _@param_ `val`
54
56
  def positional_values_size=: (Integer val) -> Integer
55
57
 
56
- def _to_exec_params: () -> ::Array[(String | ::Array[untyped])]
58
+ def _to_exec_params: () -> [String, ::Array[untyped]]
57
59
 
58
60
  def single_query_sql: () -> String
59
61
 
@@ -70,5 +72,9 @@ module PgEventstore
70
72
 
71
73
  # _@param_ `sql`
72
74
  def extract_positional_args: (String sql, *untyped arguments) -> String
75
+
76
+ private
77
+
78
+ def merge: (SQLBuilder builder)-> String
73
79
  end
74
80
  end
@@ -1,5 +1,8 @@
1
1
  module PgEventstore
2
2
  class SubscriptionRunnerHandlers
3
+ def self.stop_position_evaluation: (SubscriptionPositionEvaluation subscription_position_evaluation,
4
+ String state) -> void
5
+
3
6
  def self.track_exec_time: (PgEventstore::SubscriptionHandlerPerformance stats, ^() -> void, Integer _current_position) -> void
4
7
 
5
8
  def self.update_subscription_stats: (PgEventstore::Subscription subscription, PgEventstore::SubscriptionHandlerPerformance stats, Integer current_position) -> void
@@ -0,0 +1,15 @@
1
+ module PgEventstore
2
+ class ServiceQueries
3
+ @connection: Connection
4
+
5
+ def initialize: (Connection connection) -> void
6
+
7
+ def max_global_position: (Array[String] table_names) -> Integer
8
+
9
+ def relation_ids_by_names: (Array[String] table_names) -> Hash[String, Integer]
10
+
11
+ def relation_transaction_ids: (Array[Integer] relation_oids) -> Array[String]
12
+
13
+ def transactions_in_progress?: (relation_ids: Array[Integer], transaction_ids: Array[String]) -> bool
14
+ end
15
+ end
@@ -2,6 +2,8 @@ module PgEventstore
2
2
  class Subscription
3
3
  DEFAULT_TIMESTAMP: Time
4
4
 
5
+ MIN_EVENTS_PULL_INTERVAL: Float
6
+
5
7
  include PgEventstore::Extensions::UsingConnectionExtension
6
8
  include PgEventstore::Extensions::OptionsExtension
7
9
 
@@ -1,5 +1,7 @@
1
1
  module PgEventstore
2
2
  class SubscriptionFeeder
3
+ EVENTS_PULL_INTERVAL: Float
4
+
3
5
  extend Forwardable
4
6
 
5
7
  @basic_runner: PgEventstore::BasicRunner
@@ -0,0 +1,53 @@
1
+ module PgEventstore
2
+ class SubscriptionPositionEvaluation
3
+ TRANSACTIONS_STATUS_REFRESH_INTERVAL: Float
4
+
5
+ @config_name: Symbol
6
+ @event_type_filters: Array[String]
7
+ @last_safe_position: Integer?
8
+ @mutex: Mutex
9
+ @position_is_safe: bool?
10
+ @position_to_evaluate: Integer?
11
+ @relation_ids_cache: Hash[String, Integer]
12
+ @runner: Thread?
13
+ @stream_filters: Array[Hash[untyped, untyped]]
14
+
15
+ def initialize: (config_name: Symbol, filter_options: Hash[untyped, untyped]) -> void
16
+
17
+ def evaluate: (Integer position_to_evaluate) -> self
18
+
19
+ def last_safe_position: -> Integer?
20
+
21
+ def safe?: -> bool
22
+
23
+ def stop_evaluation: -> void
24
+
25
+ private
26
+
27
+ def _stop_evaluation: (Thread? runner)-> void
28
+
29
+ def affected_tables: -> Array[String]
30
+
31
+ def calculate_safe_position: -> void
32
+
33
+ def connection: -> Connection
34
+
35
+ def last_safe_position=: (Integer? val) -> Integer?
36
+
37
+ def partition_queries: -> PartitionQueries
38
+
39
+ def position_is_safe: -> bool?
40
+
41
+ def position_is_safe=: (bool? val) -> bool?
42
+
43
+ def position_to_evaluate: -> Integer?
44
+
45
+ def position_to_evaluate=: (Integer? val) -> Integer?
46
+
47
+ def service_queries: -> ServiceQueries
48
+
49
+ def transaction_queries: -> TransactionQueries
50
+
51
+ def update_relation_ids_cache: (Array[String] tables_to_track) -> void
52
+ end
53
+ end
@@ -12,12 +12,13 @@ module PgEventstore
12
12
  stats: SubscriptionHandlerPerformance,
13
13
  events_processor: EventsProcessor,
14
14
  subscription: Subscription,
15
- ?restart_terminator: _RestartTerminator?,
16
- ?failed_subscription_notifier: _FailedSubscriptionNotifier?
15
+ position_evaluation: SubscriptionPositionEvaluation,
17
16
  ) -> void
18
17
 
19
18
  def next_chunk_query_opts: () -> ::Hash[untyped, untyped]
20
19
 
20
+ def next_chunk_safe?: -> bool
21
+
21
22
  def time_to_feed?: () -> bool
22
23
 
23
24
  def next_chunk_global_position: () -> Integer
@@ -77,6 +77,8 @@ module PgEventstore
77
77
 
78
78
  private
79
79
 
80
+ def connection: -> Connection
81
+
80
82
  def recovery_strategies: (
81
83
  Subscription subscription,
82
84
  _RestartTerminator? restart_terminator,
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: pg_eventstore
3
3
  version: !ruby/object:Gem::Version
4
- version: 1.13.3
4
+ version: 1.13.4
5
5
  platform: ruby
6
6
  authors:
7
7
  - Ivan Dzyzenko
@@ -167,6 +167,7 @@ files:
167
167
  - lib/pg_eventstore/subscriptions/events_processor.rb
168
168
  - lib/pg_eventstore/subscriptions/extensions/base_command_extension.rb
169
169
  - lib/pg_eventstore/subscriptions/extensions/command_class_lookup_extension.rb
170
+ - lib/pg_eventstore/subscriptions/queries/service_queries.rb
170
171
  - lib/pg_eventstore/subscriptions/queries/subscription_command_queries.rb
171
172
  - lib/pg_eventstore/subscriptions/queries/subscription_queries.rb
172
173
  - lib/pg_eventstore/subscriptions/queries/subscriptions_set_command_queries.rb
@@ -187,6 +188,7 @@ files:
187
188
  - lib/pg_eventstore/subscriptions/subscription_feeder_commands/stop.rb
188
189
  - lib/pg_eventstore/subscriptions/subscription_feeder_commands/stop_all.rb
189
190
  - lib/pg_eventstore/subscriptions/subscription_handler_performance.rb
191
+ - lib/pg_eventstore/subscriptions/subscription_position_evaluation.rb
190
192
  - lib/pg_eventstore/subscriptions/subscription_runner.rb
191
193
  - lib/pg_eventstore/subscriptions/subscription_runner_commands.rb
192
194
  - lib/pg_eventstore/subscriptions/subscription_runner_commands/base.rb
@@ -332,6 +334,7 @@ files:
332
334
  - sig/pg_eventstore/subscriptions/events_processor.rbs
333
335
  - sig/pg_eventstore/subscriptions/extensions/base_command_extension.rbs
334
336
  - sig/pg_eventstore/subscriptions/extensions/command_class_lookup_extension.rbs
337
+ - sig/pg_eventstore/subscriptions/queries/service_queries.rbs
335
338
  - sig/pg_eventstore/subscriptions/queries/subscription_command_queries.rbs
336
339
  - sig/pg_eventstore/subscriptions/queries/subscription_queries.rbs
337
340
  - sig/pg_eventstore/subscriptions/queries/subscriptions_set_command_queries.rbs
@@ -351,6 +354,7 @@ files:
351
354
  - sig/pg_eventstore/subscriptions/subscription_feeder_commands/stop.rbs
352
355
  - sig/pg_eventstore/subscriptions/subscription_feeder_commands/stop_all.rbs
353
356
  - sig/pg_eventstore/subscriptions/subscription_handler_performance.rbs
357
+ - sig/pg_eventstore/subscriptions/subscription_position_evaluation.rbs
354
358
  - sig/pg_eventstore/subscriptions/subscription_runner.rbs
355
359
  - sig/pg_eventstore/subscriptions/subscription_runner_commands.rbs
356
360
  - sig/pg_eventstore/subscriptions/subscription_runner_commands/base.rbs