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.
- checksums.yaml +4 -4
- data/CHANGELOG.md +7 -3
- data/lib/pg_eventstore/event.rb +2 -0
- data/lib/pg_eventstore/queries/event_queries.rb +4 -3
- data/lib/pg_eventstore/queries/partition_queries.rb +71 -6
- data/lib/pg_eventstore/query_builders/events_filtering.rb +1 -5
- data/lib/pg_eventstore/query_builders/partitions_filtering.rb +26 -16
- data/lib/pg_eventstore/sql_builder.rb +30 -12
- data/lib/pg_eventstore/subscriptions/callback_handlers/subscription_runner_handlers.rb +9 -0
- data/lib/pg_eventstore/subscriptions/queries/service_queries.rb +73 -0
- data/lib/pg_eventstore/subscriptions/queries/subscription_queries.rb +1 -0
- data/lib/pg_eventstore/subscriptions/subscription.rb +11 -1
- data/lib/pg_eventstore/subscriptions/subscription_feeder.rb +7 -1
- data/lib/pg_eventstore/subscriptions/subscription_position_evaluation.rb +195 -0
- data/lib/pg_eventstore/subscriptions/subscription_runner.rb +18 -2
- data/lib/pg_eventstore/subscriptions/subscription_runners_feeder.rb +1 -1
- data/lib/pg_eventstore/subscriptions/subscriptions_manager.rb +12 -1
- data/lib/pg_eventstore/version.rb +1 -1
- data/lib/pg_eventstore/web/paginator/events_collection.rb +1 -1
- data/lib/pg_eventstore/web/paginator/stream_ids_collection.rb +2 -2
- data/sig/pg_eventstore/event.rbs +3 -1
- data/sig/pg_eventstore/queries/partition_queries.rbs +5 -1
- data/sig/pg_eventstore/query_builders/partitions_filtering.rbs +9 -5
- data/sig/pg_eventstore/sql_builder.rbs +8 -2
- data/sig/pg_eventstore/subscriptions/callback_handlers/subscription_runner_handlers.rbs +3 -0
- data/sig/pg_eventstore/subscriptions/queries/service_queries.rbs +15 -0
- data/sig/pg_eventstore/subscriptions/subscription.rbs +2 -0
- data/sig/pg_eventstore/subscriptions/subscription_feeder.rbs +2 -0
- data/sig/pg_eventstore/subscriptions/subscription_position_evaluation.rbs +53 -0
- data/sig/pg_eventstore/subscriptions/subscription_runner.rbs +3 -2
- data/sig/pg_eventstore/subscriptions/subscriptions_manager.rbs +2 -0
- metadata +5 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 94443dc3aa36701d5e126573a5babdd4585932d803b766b06b94c861f6b4ef5a
|
|
4
|
+
data.tar.gz: 86fcca21a3a2d3da35f3c760542b23cc97ae93ce97bae7b9baf011a957a9ffea
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
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
|
|
data/lib/pg_eventstore/event.rb
CHANGED
|
@@ -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(
|
|
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(
|
|
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(
|
|
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
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
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(*
|
|
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
|
-
|
|
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 [
|
|
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 [
|
|
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 [
|
|
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
|
-
|
|
78
|
+
def with_stream_names
|
|
79
|
+
@sql_builder.where('stream_name IS NOT NULL')
|
|
80
|
+
end
|
|
66
81
|
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
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
|
-
|
|
74
|
-
|
|
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 [
|
|
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 [
|
|
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 #{
|
|
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
|
|
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
|
-
|
|
209
|
-
|
|
210
|
-
|
|
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:
|
|
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
|
-
|
|
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(
|
|
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
|
|
@@ -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})
|
|
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(
|
|
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(
|
|
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])
|
data/sig/pg_eventstore/event.rbs
CHANGED
|
@@ -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
|
|
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
|
|
10
|
+
def self.correct_stream_filter?: (::Hash[untyped, untyped] stream_attrs) -> bool
|
|
11
11
|
|
|
12
|
-
def
|
|
12
|
+
def add_event_types: (::Array[String] event_types) -> SQLBuilder
|
|
13
13
|
|
|
14
|
-
def
|
|
14
|
+
def add_stream_attrs: (?context: String?, ?stream_name: String?) -> SQLBuilder
|
|
15
15
|
|
|
16
|
-
|
|
16
|
+
def with_event_types: -> SQLBuilder
|
|
17
17
|
|
|
18
|
-
def
|
|
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: () ->
|
|
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
|
|
@@ -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
|
-
|
|
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
|
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.
|
|
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
|