pg_versions 2.0 → 3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/db/migrate/1_create_pg_versions_table.rb +2 -2
- data/lib/pg_versions/pg_versions.rb +355 -181
- data/lib/pg_versions/version.rb +1 -1
- metadata +3 -3
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: c7b6354edf3a40a5c96c953a579ce6a8e581933c271722abb90bfee0ce056e9c
|
4
|
+
data.tar.gz: 90e4f121ce0888b6cf2a57f7fe92370be6e7933ef6d4ea5288620f4869bc6e36
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: a0054bcc270e214fa919c6d9003f99ae85cf0f95c821a0a0267e2256d1627a0a4310a3b4ab90cf1eb349c2700d7b90b38dd86b8de77c48bb241a42c770ec5980
|
7
|
+
data.tar.gz: 0fa50a3c96302bd9e14ea3ba943ab63c70ecf005741cf33fb6181f56c4f22c1e21e4140c325e9a53902ea3b40c22e99a66c59f92fd9eab96bc7655d116a09a95
|
@@ -26,94 +26,139 @@
|
|
26
26
|
|
27
27
|
|
28
28
|
require 'set'
|
29
|
+
require 'pg'
|
29
30
|
|
30
31
|
#TODO: prepared statements?
|
32
|
+
#TODO: use ractor instead of thread for event listening
|
31
33
|
|
32
34
|
module PgVersions
|
33
35
|
|
34
36
|
|
35
37
|
class InvalidParameters < StandardError; end
|
38
|
+
class ConnectionClosed < StandardError; end
|
36
39
|
|
37
40
|
def self.timestamp_to_integers(input)
|
38
41
|
"to_char(%s, 'YYYYMMDD')::integer || ',' || to_char(%s, 'HH24MISS')::integer || ',' || to_char(%s, 'US')::integer"%[input, input, input]
|
39
42
|
end
|
40
43
|
|
41
44
|
|
42
|
-
def self.with_connection(
|
43
|
-
if
|
45
|
+
def self.with_connection(pg_connection_param, reset, &block)
|
46
|
+
if pg_connection_param.kind_of? ::PG::Connection
|
47
|
+
if reset
|
48
|
+
pg_connection_param.sync_reset
|
49
|
+
pg_connection_param.exec("select;")
|
50
|
+
end
|
51
|
+
block.call(pg_connection_param)
|
52
|
+
elsif pg_connection_param.respond_to? :call
|
53
|
+
pg_connection_param.call(reset, &block)
|
54
|
+
elsif pg_connection_param.kind_of?(String) or pg_connection_param.kind_of?(Hash)
|
55
|
+
Thread.handle_interrupt(Object => :never) {
|
56
|
+
begin
|
57
|
+
pg_connection = nil
|
58
|
+
Thread.handle_interrupt(Object => :immediate) {
|
59
|
+
pg_connection = ::PG.connect(pg_connection_param)
|
60
|
+
block.call(pg_connection)
|
61
|
+
}
|
62
|
+
ensure
|
63
|
+
pg_connection&.close
|
64
|
+
end
|
65
|
+
}
|
66
|
+
elsif defined?(ActiveRecord) and pg_connection_param.kind_of?(Class) and pg_connection_param <= ActiveRecord::Base
|
67
|
+
pg_connection = pg_connection_param.connection.raw_connection
|
44
68
|
if reset
|
45
69
|
pg_connection.sync_reset
|
46
70
|
pg_connection.exec("select;")
|
47
71
|
end
|
48
72
|
block.call(pg_connection)
|
49
|
-
elsif pg_connection.respond_to? :call
|
50
|
-
pg_connection.call(reset, &block)
|
51
|
-
elsif pg_connection.nil? and defined? ActiveRecord
|
52
|
-
ActiveRecord::Base.connection_pool.with_connection { |ar_connection|
|
53
|
-
block.call(ar_connection.instance_variable_get(:@connection))
|
54
|
-
}
|
55
73
|
else
|
56
|
-
raise InvalidParameters, "
|
74
|
+
raise InvalidParameters, "Invalid connection parameter (#{pg_connection_param.inspect}). Either pass PG::Connection object, url string, hash, ActiveRecord::Base class (or subclass) or call one of the ActiveRecord methods that come with PgVersions refinement."
|
57
75
|
end
|
58
|
-
end
|
76
|
+
end
|
77
|
+
|
78
|
+
|
79
|
+
refine ActiveRecord::Base.singleton_class do
|
80
|
+
def bump(*channels)
|
81
|
+
PgVersions::bump(self, *channels)
|
82
|
+
end
|
83
|
+
|
84
|
+
def read(*channels)
|
85
|
+
PgVersions::read(self, *channels)
|
86
|
+
end
|
87
|
+
end
|
59
88
|
|
60
89
|
|
61
|
-
def self.string_to_version(version_str)
|
90
|
+
def self.string_to_version(version_str)
|
62
91
|
version_str.split(",").map { |str| Integer(str) }
|
63
92
|
end
|
64
93
|
|
65
94
|
|
66
|
-
def self.create_table(connection
|
67
|
-
PgVersions.with_connection(connection) { |pg_connection|
|
95
|
+
def self.create_table(connection)
|
96
|
+
PgVersions.with_connection(connection, false) { |pg_connection|
|
68
97
|
open(File.dirname(__FILE__)+"/../../create-table.sql") { |sql_file|
|
69
98
|
pg_connection.exec sql_file.read
|
70
99
|
}
|
71
100
|
}
|
72
101
|
end
|
73
102
|
|
74
|
-
|
75
|
-
|
103
|
+
|
104
|
+
def self.drop_table(connection)
|
105
|
+
PgVersions.with_connection(connection, false) { |pg_connection|
|
76
106
|
open(File.dirname(__FILE__)+"/../../drop-table.sql") { |sql_file|
|
77
107
|
pg_connection.exec sql_file.read
|
78
108
|
}
|
79
109
|
}
|
80
110
|
end
|
81
|
-
|
111
|
+
|
112
|
+
|
113
|
+
def self.bump_sql(*channels)
|
114
|
+
channels = [channels].flatten.sort
|
115
|
+
return "" if channels.size == 0
|
116
|
+
encoder = PG::TextEncoder::QuotedLiteral.new(elements_type: PG::TextEncoder::String.new)
|
117
|
+
quoted_channels = channels.map.with_index { |channel, i| "(#{i},#{encoder.encode(channel)})" }.join(", ")
|
118
|
+
# table-wide share lock is there to mutually exclude table cleaner
|
119
|
+
# clock_timestamp() - this has to be a timestamp after table lock got acquired
|
120
|
+
"
|
121
|
+
LOCK TABLE pg_versions IN ACCESS SHARE MODE;
|
122
|
+
WITH
|
123
|
+
to_bump(i, channel) AS (VALUES #{quoted_channels})
|
124
|
+
, current_instant(ts) AS (VALUES (clock_timestamp()))
|
125
|
+
, updated AS (
|
126
|
+
INSERT INTO pg_versions(channel, instant, counter)
|
127
|
+
SELECT to_bump.channel, (SELECT ts FROM current_instant), 0 FROM to_bump
|
128
|
+
ON CONFLICT (channel) DO UPDATE SET
|
129
|
+
instant = GREATEST(pg_versions.instant, EXCLUDED.instant),
|
130
|
+
counter = CASE WHEN pg_versions.instant < EXCLUDED.instant THEN 0 ELSE pg_versions.counter + 1 END
|
131
|
+
RETURNING channel, instant, pg_versions.counter
|
132
|
+
)
|
133
|
+
SELECT DISTINCT
|
134
|
+
i
|
135
|
+
, #{timestamp_to_integers('updated.instant')} || ',' || updated.counter::text AS version
|
136
|
+
, pg_notify(updated.channel::text, #{timestamp_to_integers('updated.instant')} || ',' || updated.counter::text)::text
|
137
|
+
FROM
|
138
|
+
to_bump
|
139
|
+
JOIN updated ON to_bump.channel = updated.channel;
|
140
|
+
"
|
141
|
+
end
|
142
|
+
|
143
|
+
|
82
144
|
#TODO: ensure this is called only once per transaction, or that all bumps occur in the same order in all transactions, to avoid deadlocks
|
83
|
-
def self.bump(*channels
|
84
|
-
|
85
|
-
|
86
|
-
|
87
|
-
|
88
|
-
|
89
|
-
|
90
|
-
|
91
|
-
LOCK TABLE pg_versions IN ACCESS SHARE MODE;
|
92
|
-
WITH
|
93
|
-
to_bump(i, channel) AS (VALUES #{quoted_channels})
|
94
|
-
, current_instant(ts) AS (VALUES (clock_timestamp()))
|
95
|
-
, updated AS (
|
96
|
-
INSERT INTO pg_versions(channel, instant, counter)
|
97
|
-
SELECT to_bump.channel, (SELECT ts FROM current_instant), 0 FROM to_bump
|
98
|
-
ON CONFLICT (channel) DO UPDATE SET
|
99
|
-
instant = GREATEST(pg_versions.instant, EXCLUDED.instant),
|
100
|
-
counter = CASE WHEN pg_versions.instant < EXCLUDED.instant THEN 0 ELSE pg_versions.counter + 1 END
|
101
|
-
RETURNING channel, instant, pg_versions.counter
|
102
|
-
)
|
103
|
-
SELECT DISTINCT
|
104
|
-
i
|
105
|
-
, #{timestamp_to_integers('updated.instant')} || ',' || updated.counter::text AS version
|
106
|
-
, pg_notify(updated.channel::text, #{timestamp_to_integers('updated.instant')} || ',' || updated.counter::text)::text
|
107
|
-
FROM
|
108
|
-
to_bump
|
109
|
-
JOIN updated ON to_bump.channel = updated.channel;
|
110
|
-
").map { |row| [channels[Integer(row["i"])], string_to_version(row["version"])] }.to_h
|
145
|
+
def self.bump(connection, *channels)
|
146
|
+
channels = [channels].flatten.sort
|
147
|
+
PgVersions.with_connection(connection, false) { |pg_connection|
|
148
|
+
sql = self.bump_sql(*channels)
|
149
|
+
return {} if sql == ""
|
150
|
+
pg_connection.exec(sql) { |result|
|
151
|
+
result.map { |row| [channels[Integer(row["i"])], string_to_version(row["version"])] }.to_h
|
152
|
+
}
|
111
153
|
}
|
112
154
|
end
|
113
155
|
|
114
|
-
|
115
|
-
|
116
|
-
|
156
|
+
|
157
|
+
#TODO: bump in the same query instead of calling bump
|
158
|
+
#TODO: do we really need to bump though?
|
159
|
+
#TODO: and then, implement read_sql
|
160
|
+
def self.read(connection, *channels)
|
161
|
+
PgVersions.with_connection(connection, false) { |pg_connection|
|
117
162
|
channels = [channels].flatten.sort
|
118
163
|
return {} if channels.size == 0
|
119
164
|
versions = {}
|
@@ -130,11 +175,12 @@ module PgVersions
|
|
130
175
|
JOIN pg_versions ON pg_versions.channel = channels.channel
|
131
176
|
ORDER BY
|
132
177
|
i DESC;
|
133
|
-
")
|
134
|
-
|
178
|
+
") { |result|
|
179
|
+
result.each { |row|
|
180
|
+
versions[channels.delete_at(Integer(row["i"]))] = string_to_version(row["version"])
|
181
|
+
}
|
135
182
|
}
|
136
|
-
|
137
|
-
versions.merge!(self.bump(channels, connection: pg_connection)) if channels.size > 0
|
183
|
+
versions.merge!(self.bump(pg_connection, channels)) if channels.size > 0
|
138
184
|
versions
|
139
185
|
}
|
140
186
|
end
|
@@ -149,164 +195,279 @@ module PgVersions
|
|
149
195
|
end
|
150
196
|
|
151
197
|
|
198
|
+
class ConnectionInner
|
199
|
+
|
200
|
+
def initialize()
|
201
|
+
@mutex = Mutex.new
|
202
|
+
@command_notify_w = nil
|
203
|
+
@subscriptions = {}
|
204
|
+
@bumps = []
|
205
|
+
@reads = []
|
206
|
+
@closers = []
|
207
|
+
@state = :idle # idle, processing, closing, closed
|
208
|
+
end
|
152
209
|
|
153
|
-
class ConnectionThread
|
154
210
|
|
155
|
-
|
156
|
-
|
157
|
-
|
158
|
-
@
|
211
|
+
def process
|
212
|
+
Thread.handle_interrupt(Object => :never) do
|
213
|
+
command_notify_r = nil
|
214
|
+
@mutex.synchronize {
|
215
|
+
case @state
|
216
|
+
when :idle
|
217
|
+
@state = :processing
|
218
|
+
when :processing
|
219
|
+
raise "Attempt to run processing on a connection that is already being processed"
|
220
|
+
when :closing, :closed
|
221
|
+
return
|
222
|
+
end
|
223
|
+
}
|
224
|
+
begin
|
225
|
+
command_notify_r, @command_notify_w = IO.pipe
|
226
|
+
Thread.handle_interrupt(Object => :immediate) {
|
227
|
+
yield command_notify_r
|
228
|
+
}
|
229
|
+
ensure
|
230
|
+
@mutex.synchronize {
|
231
|
+
command_notify_r&.close
|
232
|
+
@command_notify_w&.close
|
233
|
+
@command_notify_w = nil
|
234
|
+
case @state
|
235
|
+
when :idle, :closed
|
236
|
+
raise "'processor exit in #{@state} state. Please inform the developer of this gem."
|
237
|
+
when :processing
|
238
|
+
@state = :idle
|
239
|
+
when :closing
|
240
|
+
@state = :closed
|
241
|
+
@closers.each { |closer|
|
242
|
+
closer.push true
|
243
|
+
}
|
244
|
+
end
|
245
|
+
}
|
246
|
+
end
|
159
247
|
end
|
160
248
|
end
|
161
249
|
|
250
|
+
|
251
|
+
def wake_processor
|
252
|
+
@command_notify_w&.write('!')
|
253
|
+
@command_notify_w&.flush
|
254
|
+
end
|
162
255
|
|
163
|
-
attr_reader :status
|
164
256
|
|
165
|
-
def
|
166
|
-
|
167
|
-
|
257
|
+
def get_channels
|
258
|
+
@mutex.synchronize {
|
259
|
+
return @subscriptions.keys
|
260
|
+
}
|
261
|
+
end
|
168
262
|
|
169
|
-
@subscribers = Hash.new { |h,k| h[k] = Set.new }
|
170
|
-
@status = :disconnected
|
171
263
|
|
172
|
-
|
173
|
-
@
|
174
|
-
|
264
|
+
def notify(channel, version)
|
265
|
+
@mutex.synchronize {
|
266
|
+
(@subscriptions[channel] or []).each { |subscriber|
|
267
|
+
subscriber.notify({ channel => version })
|
268
|
+
}
|
269
|
+
}
|
270
|
+
end
|
175
271
|
|
176
|
-
@thread = Thread.new {
|
177
|
-
reset_connection = false
|
178
|
-
retry_delay = 0
|
179
|
-
begin
|
180
|
-
PgVersions.with_connection(connection, reset: reset_connection) do |pg_connection|
|
181
|
-
|
182
|
-
@status = :connected
|
183
|
-
retry_delay = 0
|
184
272
|
|
185
|
-
|
186
|
-
|
187
|
-
|
188
|
-
|
189
|
-
|
190
|
-
|
191
|
-
subscriber.notify({ channel => version })
|
192
|
-
}
|
193
|
-
}
|
273
|
+
def taking_bumps
|
274
|
+
@mutex.synchronize {
|
275
|
+
yield @bumps
|
276
|
+
@bumps = []
|
277
|
+
}
|
278
|
+
end
|
194
279
|
|
195
|
-
loop {
|
196
|
-
@thread_requests_mutex.synchronize {
|
197
|
-
@thread_requests.each { |function, retpipe, params|
|
198
|
-
raise Closing, retpipe if function == :stop
|
199
|
-
retpipe << send(function, pg_connection, *params)
|
200
|
-
}
|
201
|
-
@thread_requests.clear
|
202
|
-
}
|
203
280
|
|
204
|
-
|
205
|
-
|
206
|
-
|
207
|
-
|
208
|
-
|
209
|
-
|
281
|
+
def taking_reads
|
282
|
+
@mutex.synchronize {
|
283
|
+
yield @reads
|
284
|
+
@reads = []
|
285
|
+
}
|
286
|
+
end
|
210
287
|
|
211
|
-
#TODO: handle errors
|
212
|
-
reads,_writes,_errors = IO::select([pg_connection.socket_io, thread_requests_notify_r])
|
213
|
-
|
214
|
-
if reads.include?(pg_connection.socket_io)
|
215
|
-
pg_connection.consume_input
|
216
|
-
end
|
217
288
|
|
218
|
-
|
219
|
-
|
220
|
-
|
221
|
-
|
222
|
-
|
223
|
-
|
224
|
-
|
225
|
-
|
226
|
-
reset_connection = true
|
227
|
-
@status = :disconnected
|
228
|
-
$stderr.puts "Pg connection failed. Retrying in #{retry_delay/1000.0}s."
|
229
|
-
sleep retry_delay/1000.0
|
230
|
-
retry_delay = { 0=>100, 100=>1000, 1000=>2000, 2000=>2000 }[retry_delay]
|
231
|
-
retry
|
232
|
-
end
|
233
|
-
}
|
234
|
-
@thread.abort_on_exception = true
|
289
|
+
def bump(channels)
|
290
|
+
result = Queue.new
|
291
|
+
@mutex.synchronize {
|
292
|
+
raise ConnectionClosed if @state == :closing || @state == :closed
|
293
|
+
@bumps << [result, channels]
|
294
|
+
}
|
295
|
+
wake_processor
|
296
|
+
result.pop
|
235
297
|
end
|
236
298
|
|
237
|
-
|
238
|
-
|
299
|
+
|
300
|
+
def bump_nonblock(channels)
|
301
|
+
@mutex.synchronize {
|
302
|
+
raise ConnectionClosed if @state == :closing || @state == :closed
|
303
|
+
@bumps << [nil, channels]
|
304
|
+
}
|
305
|
+
wake_processor
|
306
|
+
nil
|
239
307
|
end
|
240
308
|
|
241
|
-
|
242
|
-
|
309
|
+
|
310
|
+
def read(channels)
|
311
|
+
result = Queue.new
|
312
|
+
@mutex.synchronize {
|
313
|
+
raise ConnectionClosed if @state == :closing || @state == :closed
|
314
|
+
@reads << [result, channels]
|
315
|
+
}
|
316
|
+
wake_processor
|
317
|
+
result.pop
|
243
318
|
end
|
244
319
|
|
245
|
-
|
246
|
-
|
247
|
-
|
248
|
-
|
320
|
+
|
321
|
+
def subscribe(subscriber, channels)
|
322
|
+
@mutex.synchronize {
|
323
|
+
raise ConnectionClosed if @state == :closing || @state == :closed
|
324
|
+
channels.each { |channel|
|
325
|
+
@subscriptions[channel] = [] if @subscriptions[channel].nil?
|
326
|
+
@subscriptions[channel].push(subscriber)
|
327
|
+
}
|
249
328
|
}
|
250
|
-
subscriber.notify(
|
329
|
+
subscriber.notify(read(channels)) # this runs wake_processor, so not doing it explicitly
|
251
330
|
true
|
252
331
|
end
|
253
|
-
|
254
|
-
|
255
|
-
|
256
|
-
|
257
|
-
if @
|
258
|
-
|
259
|
-
@
|
260
|
-
|
332
|
+
|
333
|
+
|
334
|
+
def unsubscribe(subscriber, channels)
|
335
|
+
@mutex.synchronize {
|
336
|
+
raise ConnectionClosed if @state == :closing || @state == :closed
|
337
|
+
channels.each { |channel|
|
338
|
+
@subscriptions[channel].delete(subscriber)
|
339
|
+
@subscriptions.delete(channel) if @subscriptions[channel].size == 0
|
340
|
+
}
|
261
341
|
}
|
342
|
+
wake_processor
|
262
343
|
true
|
263
344
|
end
|
264
345
|
|
265
|
-
private def read(pg_connection, channels)
|
266
|
-
PgVersions.read(channels, connection: pg_connection)
|
267
|
-
end
|
268
346
|
|
269
|
-
|
270
|
-
|
347
|
+
def is_closing
|
348
|
+
@mutex.synchronize {
|
349
|
+
return @state == :closing
|
350
|
+
}
|
271
351
|
end
|
272
352
|
|
273
|
-
def request(function, *params)
|
274
|
-
request_nonblock(function, *params).pop
|
275
|
-
end
|
276
353
|
|
277
|
-
def
|
278
|
-
|
279
|
-
@
|
280
|
-
@
|
354
|
+
def close
|
355
|
+
result = Queue.new
|
356
|
+
@mutex.synchronize {
|
357
|
+
case @state
|
358
|
+
when :idle
|
359
|
+
@state = :closed
|
360
|
+
return
|
361
|
+
when :processing
|
362
|
+
@state = :closing
|
363
|
+
@closers << result
|
364
|
+
wake_processor
|
365
|
+
when :closing
|
366
|
+
@closers << result
|
367
|
+
when :closed
|
368
|
+
return
|
369
|
+
end
|
281
370
|
}
|
282
|
-
|
283
|
-
retpipe
|
371
|
+
result.pop
|
284
372
|
end
|
285
|
-
|
373
|
+
|
286
374
|
end
|
287
375
|
|
376
|
+
|
288
377
|
class Connection
|
289
|
-
|
290
|
-
def initialize(
|
291
|
-
@
|
378
|
+
|
379
|
+
def initialize()
|
380
|
+
@inner = ConnectionInner.new
|
381
|
+
end
|
382
|
+
|
383
|
+
|
384
|
+
def process(connection_param=nil, autoreconnect=true, &block)
|
385
|
+
raise "Both 'connection_param' and a block were given. Don't know which to use." if !connection_param.nil? and !block.nil?
|
386
|
+
connection_param ||= block
|
387
|
+
|
388
|
+
retry_on_exceptions = [ ::PG::ConnectionBad, ::PG::UnableToSend ]
|
389
|
+
retry_delay = 0
|
390
|
+
|
391
|
+
@inner.process do |notification_r|
|
392
|
+
raise if not notification_r
|
393
|
+
PgVersions.with_connection(connection_param, true) do |pg_connection|
|
394
|
+
|
395
|
+
listening_to_channels = @inner.get_channels
|
396
|
+
listening_to_channels.each { |channel|
|
397
|
+
pg_connection.exec("LISTEN #{::PG::Connection.quote_ident(channel)}")
|
398
|
+
}
|
399
|
+
PgVersions.read(pg_connection, listening_to_channels).each { |channel, version|
|
400
|
+
@inner.notify(channel, version)
|
401
|
+
}
|
402
|
+
|
403
|
+
loop {
|
404
|
+
channels_to_listen_to = @inner.get_channels
|
405
|
+
(listening_to_channels - channels_to_listen_to).each { |removed_channel|
|
406
|
+
pg_connection.exec("UNLISTEN #{::PG::Connection.quote_ident(removed_channel)}")
|
407
|
+
}
|
408
|
+
(channels_to_listen_to - listening_to_channels).each { |added_channel|
|
409
|
+
pg_connection.exec("LISTEN #{::PG::Connection.quote_ident(added_channel)}")
|
410
|
+
}
|
411
|
+
listening_to_channels = channels_to_listen_to
|
412
|
+
|
413
|
+
@inner.taking_bumps { |bumps|
|
414
|
+
channels_to_bump = bumps.map(&:last).flatten.uniq
|
415
|
+
bumped_versions = PgVersions.bump(pg_connection, channels_to_bump)
|
416
|
+
bumps.each { |bumper, channels|
|
417
|
+
bumper.push bumped_versions.slice(*channels) if not bumper.nil?
|
418
|
+
}
|
419
|
+
}
|
420
|
+
|
421
|
+
@inner.taking_reads { |reads|
|
422
|
+
channels_to_read = reads.map(&:last).uniq
|
423
|
+
read_versions = PgVersions.read(pg_connection, channels_to_read)
|
424
|
+
reads.each { |reader, channels|
|
425
|
+
reader.push read_versions.slice(*channels)
|
426
|
+
}
|
427
|
+
}
|
428
|
+
|
429
|
+
break if @inner.is_closing
|
430
|
+
|
431
|
+
while notification = pg_connection.notifies
|
432
|
+
channel, payload = notification[:relname], notification[:extra]
|
433
|
+
@inner.notify(channel, PgVersions.string_to_version(payload))
|
434
|
+
end
|
435
|
+
|
436
|
+
#TODO: handle errors
|
437
|
+
reads,_writes,_errors = IO::select([pg_connection.socket_io, notification_r])
|
438
|
+
pg_connection.consume_input if reads.include?(pg_connection.socket_io)
|
439
|
+
notification_r.read(1) if reads.include?(notification_r) #TODO: read everything that can be read here
|
440
|
+
|
441
|
+
}
|
442
|
+
end
|
443
|
+
rescue *retry_on_exceptions => error
|
444
|
+
raise if connection_param.kind_of?(::PG::Connection) or !autoreconnect
|
445
|
+
return if @inner.is_closing
|
446
|
+
$stderr.puts "Pg connection failed (retrying in #{retry_delay/1000.0}s):\n\t#{error.message}"
|
447
|
+
sleep retry_delay/1000.0
|
448
|
+
retry_delay = { 0=>100, 100=>1000, 1000=>2000, 2000=>2000 }[retry_delay]
|
449
|
+
retry
|
450
|
+
end
|
292
451
|
end
|
293
452
|
|
294
|
-
|
295
|
-
|
453
|
+
|
454
|
+
def close
|
455
|
+
@inner.close
|
296
456
|
end
|
297
457
|
|
458
|
+
|
298
459
|
def bump(*channels)
|
299
|
-
@
|
460
|
+
@inner.bump(channels)
|
300
461
|
end
|
301
462
|
|
302
463
|
|
303
464
|
def read(*channels)
|
304
|
-
@
|
465
|
+
@inner.read(channels)
|
305
466
|
end
|
306
467
|
|
307
468
|
|
308
|
-
def subscribe(*channels, known: {})
|
309
|
-
subscription = Subscription.new(@
|
469
|
+
def subscribe(*channels, known: {}, batch_delay: 0.01)
|
470
|
+
subscription = Subscription.new(@inner, batch_delay)
|
310
471
|
subscription.subscribe([channels].flatten, known: known)
|
311
472
|
if block_given?
|
312
473
|
Thread.handle_interrupt(Object => :never) {
|
@@ -324,15 +485,23 @@ module PgVersions
|
|
324
485
|
end
|
325
486
|
|
326
487
|
|
488
|
+
alias_method :each, def for_each_notification(*channels, known: {}, batch_delay: 0.01, &block)
|
489
|
+
subscribe(*channels, known: known, batch_delay: batch_delay) { |subscription|
|
490
|
+
subscription.for_each_notification(&block)
|
491
|
+
}
|
492
|
+
end
|
493
|
+
|
494
|
+
|
327
495
|
class Subscription
|
328
496
|
|
329
|
-
def initialize(
|
330
|
-
@
|
497
|
+
def initialize(inner, batch_delay)
|
498
|
+
@inner = inner
|
499
|
+
@batch_delay = batch_delay
|
331
500
|
@notifications = Queue.new
|
332
501
|
@already_known_versions = Hash.new { |h,k| h[k] = [] }
|
333
502
|
@channels = Hash.new(0)
|
334
503
|
end
|
335
|
-
|
504
|
+
|
336
505
|
|
337
506
|
def subscribe(channels, known: {})
|
338
507
|
update_already_known_versions(known)
|
@@ -341,10 +510,10 @@ module PgVersions
|
|
341
510
|
(@channels[channel] += 1) == 1
|
342
511
|
}
|
343
512
|
if channels.size > 0
|
344
|
-
@
|
513
|
+
@inner.subscribe(self, channels)
|
345
514
|
end
|
346
515
|
end
|
347
|
-
|
516
|
+
|
348
517
|
|
349
518
|
def unsubscribe(*channels)
|
350
519
|
channels = [channels].flatten
|
@@ -354,13 +523,13 @@ module PgVersions
|
|
354
523
|
@channels.delete(channel) if @channels[channel] == 0
|
355
524
|
not @channels.has_key?(channel)
|
356
525
|
}
|
357
|
-
@
|
526
|
+
@inner.unsubscribe(self, channels)
|
358
527
|
end
|
359
528
|
|
360
529
|
|
361
530
|
def read(*channels, notify: false)
|
362
531
|
channels = @channels.keys if channels.size == 0
|
363
|
-
versions = @
|
532
|
+
versions = @inner.read(channels)
|
364
533
|
update_already_known_versions(versions) if not notify
|
365
534
|
versions
|
366
535
|
end
|
@@ -368,34 +537,39 @@ module PgVersions
|
|
368
537
|
|
369
538
|
def bump(*channels, notify: false)
|
370
539
|
channels = @channels.keys if channels.size == 0
|
371
|
-
versions = @
|
540
|
+
versions = @inner.bump(channels)
|
372
541
|
update_already_known_versions(versions) if not notify
|
373
542
|
versions
|
374
543
|
end
|
375
544
|
|
376
545
|
|
377
546
|
#TODO: make this resume-able after forced exception
|
378
|
-
def wait(new_already_known_versions = {})
|
547
|
+
def wait(new_already_known_versions = {}, batch_delay: nil)
|
548
|
+
batch_delay = @batch_delay if batch_delay.nil?
|
379
549
|
update_already_known_versions(new_already_known_versions)
|
380
550
|
loop {
|
381
|
-
|
382
|
-
|
383
|
-
|
384
|
-
|
385
|
-
|
386
|
-
|
387
|
-
|
388
|
-
|
551
|
+
events = [@notifications.shift]
|
552
|
+
sleep batch_delay if batch_delay
|
553
|
+
events << @notifications.shift while not @notifications.empty?
|
554
|
+
changed_versions = {}
|
555
|
+
events.each { |versions|
|
556
|
+
return nil if not versions #termination
|
557
|
+
versions.each { |channel, version|
|
558
|
+
if (@already_known_versions[channel] <=> version) == -1
|
559
|
+
@already_known_versions[channel] = version
|
560
|
+
changed_versions[channel] = version
|
561
|
+
end
|
562
|
+
}
|
563
|
+
}
|
389
564
|
if changed_versions.size > 0
|
390
565
|
return Notification.new(changed_versions, @already_known_versions.dup)
|
391
566
|
end
|
392
567
|
}
|
393
568
|
end
|
394
569
|
|
395
|
-
|
396
|
-
def each(new_already_known_versions = {})
|
570
|
+
alias_method :each, def for_each_notification(new_already_known_versions = {}, batch_delay: nil)
|
397
571
|
update_already_known_versions(new_already_known_versions)
|
398
|
-
while notification = wait()
|
572
|
+
while notification = wait(batch_delay: batch_delay)
|
399
573
|
yield notification
|
400
574
|
end
|
401
575
|
end
|
@@ -408,7 +582,7 @@ module PgVersions
|
|
408
582
|
|
409
583
|
def drop
|
410
584
|
@notifications << nil
|
411
|
-
@
|
585
|
+
@inner.unsubscribe(self, @channels.keys) if @channels.keys.size > 0
|
412
586
|
#TODO: what to do if this object gets used after drop?
|
413
587
|
end
|
414
588
|
|
data/lib/pg_versions/version.rb
CHANGED
metadata
CHANGED
@@ -1,14 +1,14 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: pg_versions
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: '
|
4
|
+
version: '3.0'
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- yunta
|
8
8
|
autorequire:
|
9
9
|
bindir: bin
|
10
10
|
cert_chain: []
|
11
|
-
date:
|
11
|
+
date: 2025-03-02 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: rspec
|
@@ -100,7 +100,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
100
100
|
- !ruby/object:Gem::Version
|
101
101
|
version: '0'
|
102
102
|
requirements: []
|
103
|
-
rubygems_version: 3.
|
103
|
+
rubygems_version: 3.5.22
|
104
104
|
signing_key:
|
105
105
|
specification_version: 4
|
106
106
|
summary: Persistent timestamped postgres notification library
|