pg_versions 2.1 → 3.1
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 +331 -168
- data/lib/pg_versions/version.rb +1 -1
- metadata +3 -6
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: c5607f7d063544acad22c803124f555de0e1929d51877a7d116a4564724265fc
|
4
|
+
data.tar.gz: 1b430f7a3e100e1c5e25dd0deff40e52fab5bca8724ef05bcdeabf69ca85cd68
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 85a44f72043f552245c243305a2373719eb0c439c97b7e89ed3f08fcab4cb5ac59644b880233abda2d2edef6c9981c8c8e225fb7b9114a3e11eb04392ae8b146
|
7
|
+
data.tar.gz: abab7289eafaf1b6886401eca4bc2c65de173804e424c6c527ddbb9ea3dd254dad430f9fff0fd6a008de6e83bd9f17560b7532346401f0f1f92d5eb2331e14b6
|
@@ -26,98 +26,140 @@
|
|
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."
|
75
|
+
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)
|
57
86
|
end
|
58
|
-
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
|
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
|
-
PgVersions.with_connection(connection) { |pg_connection|
|
86
|
-
|
87
|
-
return {} if
|
88
|
-
|
89
|
-
# table-wide share lock is there to mutually exclude table cleaner
|
90
|
-
# clock_timestamp() - this has to be a timestamp after table lock got acquired
|
91
|
-
pg_connection.exec("
|
92
|
-
LOCK TABLE pg_versions IN ACCESS SHARE MODE;
|
93
|
-
WITH
|
94
|
-
to_bump(i, channel) AS (VALUES #{quoted_channels})
|
95
|
-
, current_instant(ts) AS (VALUES (clock_timestamp()))
|
96
|
-
, updated AS (
|
97
|
-
INSERT INTO pg_versions(channel, instant, counter)
|
98
|
-
SELECT to_bump.channel, (SELECT ts FROM current_instant), 0 FROM to_bump
|
99
|
-
ON CONFLICT (channel) DO UPDATE SET
|
100
|
-
instant = GREATEST(pg_versions.instant, EXCLUDED.instant),
|
101
|
-
counter = CASE WHEN pg_versions.instant < EXCLUDED.instant THEN 0 ELSE pg_versions.counter + 1 END
|
102
|
-
RETURNING channel, instant, pg_versions.counter
|
103
|
-
)
|
104
|
-
SELECT DISTINCT
|
105
|
-
i
|
106
|
-
, #{timestamp_to_integers('updated.instant')} || ',' || updated.counter::text AS version
|
107
|
-
, pg_notify(updated.channel::text, #{timestamp_to_integers('updated.instant')} || ',' || updated.counter::text)::text
|
108
|
-
FROM
|
109
|
-
to_bump
|
110
|
-
JOIN updated ON to_bump.channel = updated.channel;
|
111
|
-
") { |result|
|
145
|
+
def self.bump(connection, *channels)
|
146
|
+
channels = [channels].flatten.sort.uniq
|
147
|
+
PgVersions.with_connection(connection, false) { |pg_connection|
|
148
|
+
sql = self.bump_sql(*channels)
|
149
|
+
return {} if sql == ""
|
150
|
+
pg_connection.exec(sql) { |result|
|
112
151
|
result.map { |row| [channels[Integer(row["i"])], string_to_version(row["version"])] }.to_h
|
113
152
|
}
|
114
153
|
}
|
115
154
|
end
|
116
155
|
|
117
156
|
|
118
|
-
|
119
|
-
|
120
|
-
|
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|
|
162
|
+
channels = [channels].flatten.sort.uniq
|
121
163
|
return {} if channels.size == 0
|
122
164
|
versions = {}
|
123
165
|
quoted_channels = channels.map.with_index { |channel, i| "(#{i},'#{pg_connection.escape_string(channel)}')" }.join(", ")
|
@@ -138,8 +180,7 @@ module PgVersions
|
|
138
180
|
versions[channels.delete_at(Integer(row["i"]))] = string_to_version(row["version"])
|
139
181
|
}
|
140
182
|
}
|
141
|
-
|
142
|
-
versions.merge!(self.bump(channels, connection: pg_connection)) if channels.size > 0
|
183
|
+
versions.merge!(self.bump(pg_connection, channels)) if channels.size > 0
|
143
184
|
versions
|
144
185
|
}
|
145
186
|
end
|
@@ -154,163 +195,279 @@ module PgVersions
|
|
154
195
|
end
|
155
196
|
|
156
197
|
|
198
|
+
class ConnectionInner
|
157
199
|
|
158
|
-
|
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
|
159
209
|
|
160
|
-
|
161
|
-
|
162
|
-
|
163
|
-
|
210
|
+
|
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
|
164
247
|
end
|
165
248
|
end
|
166
249
|
|
167
250
|
|
168
|
-
|
251
|
+
def wake_processor
|
252
|
+
@command_notify_w&.write('!')
|
253
|
+
@command_notify_w&.flush
|
254
|
+
end
|
169
255
|
|
170
|
-
def initialize(connection)
|
171
|
-
retry_on_exceptions = [ PG::ConnectionBad, PG::UnableToSend ]
|
172
|
-
retry_on_exceptions << ActiveRecord::ConnectionNotEstablished if defined? ActiveRecord
|
173
256
|
|
174
|
-
|
175
|
-
@
|
257
|
+
def get_channels
|
258
|
+
@mutex.synchronize {
|
259
|
+
return @subscriptions.keys
|
260
|
+
}
|
261
|
+
end
|
176
262
|
|
177
|
-
@thread_requests_mutex = Mutex.new
|
178
|
-
@thread_requests = []
|
179
|
-
thread_requests_notify_r, @thread_requests_notify_w = IO.pipe
|
180
263
|
|
181
|
-
|
182
|
-
|
183
|
-
|
184
|
-
|
185
|
-
|
186
|
-
|
187
|
-
|
188
|
-
retry_delay = 0
|
264
|
+
def notify(channel, version)
|
265
|
+
@mutex.synchronize {
|
266
|
+
(@subscriptions[channel] or []).each { |subscriber|
|
267
|
+
subscriber.notify({ channel => version })
|
268
|
+
}
|
269
|
+
}
|
270
|
+
end
|
189
271
|
|
190
|
-
@subscribers.each_key { |channel|
|
191
|
-
listen(pg_connection, channel)
|
192
|
-
}
|
193
|
-
read(pg_connection, @subscribers.keys).each_pair { |channel, version|
|
194
|
-
@subscribers[channel].each { |subscriber|
|
195
|
-
subscriber.notify({ channel => version })
|
196
|
-
}
|
197
|
-
}
|
198
272
|
|
199
|
-
|
200
|
-
|
201
|
-
|
202
|
-
|
203
|
-
|
204
|
-
|
205
|
-
@thread_requests.clear
|
206
|
-
}
|
273
|
+
def taking_bumps
|
274
|
+
@mutex.synchronize {
|
275
|
+
yield @bumps
|
276
|
+
@bumps = []
|
277
|
+
}
|
278
|
+
end
|
207
279
|
|
208
|
-
while notification = pg_connection.notifies
|
209
|
-
channel, payload = notification[:relname], notification[:extra]
|
210
|
-
@subscribers[channel].each { |subscriber|
|
211
|
-
subscriber.notify({ channel => PgVersions.string_to_version(payload) })
|
212
|
-
}
|
213
|
-
end
|
214
280
|
|
215
|
-
|
216
|
-
|
217
|
-
|
218
|
-
|
219
|
-
|
220
|
-
|
281
|
+
def taking_reads
|
282
|
+
@mutex.synchronize {
|
283
|
+
yield @reads
|
284
|
+
@reads = []
|
285
|
+
}
|
286
|
+
end
|
221
287
|
|
222
|
-
|
223
|
-
|
224
|
-
|
225
|
-
|
226
|
-
|
227
|
-
|
228
|
-
|
229
|
-
|
230
|
-
|
231
|
-
@status = :disconnected
|
232
|
-
$stderr.puts "Pg connection failed. Retrying in #{retry_delay/1000.0}s."
|
233
|
-
sleep retry_delay/1000.0
|
234
|
-
retry_delay = { 0=>100, 100=>1000, 1000=>2000, 2000=>2000 }[retry_delay]
|
235
|
-
retry
|
236
|
-
end
|
237
|
-
}
|
238
|
-
@thread.abort_on_exception = true
|
288
|
+
|
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
|
239
297
|
end
|
240
298
|
|
241
|
-
|
242
|
-
|
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
|
243
307
|
end
|
244
308
|
|
245
|
-
|
246
|
-
|
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
|
247
318
|
end
|
248
319
|
|
249
|
-
|
250
|
-
|
251
|
-
|
252
|
-
|
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
|
+
}
|
253
328
|
}
|
254
|
-
subscriber.notify(
|
329
|
+
subscriber.notify(read(channels)) # this runs wake_processor, so not doing it explicitly
|
255
330
|
true
|
256
331
|
end
|
257
|
-
|
258
|
-
|
259
|
-
|
260
|
-
|
261
|
-
if @
|
262
|
-
|
263
|
-
@
|
264
|
-
|
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
|
+
}
|
265
341
|
}
|
342
|
+
wake_processor
|
266
343
|
true
|
267
344
|
end
|
268
345
|
|
269
|
-
private def read(pg_connection, channels)
|
270
|
-
PgVersions.read(channels, connection: pg_connection)
|
271
|
-
end
|
272
346
|
|
273
|
-
|
274
|
-
|
347
|
+
def is_closing
|
348
|
+
@mutex.synchronize {
|
349
|
+
return @state == :closing
|
350
|
+
}
|
275
351
|
end
|
276
352
|
|
277
|
-
def request(function, *params)
|
278
|
-
request_nonblock(function, *params).pop
|
279
|
-
end
|
280
353
|
|
281
|
-
def
|
282
|
-
|
283
|
-
@
|
284
|
-
@
|
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
|
285
370
|
}
|
286
|
-
|
287
|
-
retpipe
|
371
|
+
result.pop
|
288
372
|
end
|
289
|
-
|
373
|
+
|
290
374
|
end
|
291
375
|
|
376
|
+
|
292
377
|
class Connection
|
293
|
-
|
294
|
-
def initialize(
|
295
|
-
@
|
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
|
296
451
|
end
|
297
452
|
|
298
|
-
|
299
|
-
|
453
|
+
|
454
|
+
def close
|
455
|
+
@inner.close
|
300
456
|
end
|
301
457
|
|
458
|
+
|
302
459
|
def bump(*channels)
|
303
|
-
@
|
460
|
+
@inner.bump(channels)
|
304
461
|
end
|
305
462
|
|
306
463
|
|
307
464
|
def read(*channels)
|
308
|
-
@
|
465
|
+
@inner.read(channels)
|
309
466
|
end
|
310
467
|
|
311
468
|
|
312
469
|
def subscribe(*channels, known: {}, batch_delay: 0.01)
|
313
|
-
subscription = Subscription.new(@
|
470
|
+
subscription = Subscription.new(@inner, batch_delay)
|
314
471
|
subscription.subscribe([channels].flatten, known: known)
|
315
472
|
if block_given?
|
316
473
|
Thread.handle_interrupt(Object => :never) {
|
@@ -328,16 +485,23 @@ module PgVersions
|
|
328
485
|
end
|
329
486
|
|
330
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
|
+
|
331
495
|
class Subscription
|
332
496
|
|
333
|
-
def initialize(
|
334
|
-
@
|
497
|
+
def initialize(inner, batch_delay)
|
498
|
+
@inner = inner
|
335
499
|
@batch_delay = batch_delay
|
336
500
|
@notifications = Queue.new
|
337
501
|
@already_known_versions = Hash.new { |h,k| h[k] = [] }
|
338
502
|
@channels = Hash.new(0)
|
339
503
|
end
|
340
|
-
|
504
|
+
|
341
505
|
|
342
506
|
def subscribe(channels, known: {})
|
343
507
|
update_already_known_versions(known)
|
@@ -346,10 +510,10 @@ module PgVersions
|
|
346
510
|
(@channels[channel] += 1) == 1
|
347
511
|
}
|
348
512
|
if channels.size > 0
|
349
|
-
@
|
513
|
+
@inner.subscribe(self, channels)
|
350
514
|
end
|
351
515
|
end
|
352
|
-
|
516
|
+
|
353
517
|
|
354
518
|
def unsubscribe(*channels)
|
355
519
|
channels = [channels].flatten
|
@@ -359,13 +523,13 @@ module PgVersions
|
|
359
523
|
@channels.delete(channel) if @channels[channel] == 0
|
360
524
|
not @channels.has_key?(channel)
|
361
525
|
}
|
362
|
-
@
|
526
|
+
@inner.unsubscribe(self, channels)
|
363
527
|
end
|
364
528
|
|
365
529
|
|
366
530
|
def read(*channels, notify: false)
|
367
531
|
channels = @channels.keys if channels.size == 0
|
368
|
-
versions = @
|
532
|
+
versions = @inner.read(channels)
|
369
533
|
update_already_known_versions(versions) if not notify
|
370
534
|
versions
|
371
535
|
end
|
@@ -373,7 +537,7 @@ module PgVersions
|
|
373
537
|
|
374
538
|
def bump(*channels, notify: false)
|
375
539
|
channels = @channels.keys if channels.size == 0
|
376
|
-
versions = @
|
540
|
+
versions = @inner.bump(channels)
|
377
541
|
update_already_known_versions(versions) if not notify
|
378
542
|
versions
|
379
543
|
end
|
@@ -403,8 +567,7 @@ module PgVersions
|
|
403
567
|
}
|
404
568
|
end
|
405
569
|
|
406
|
-
|
407
|
-
def each(new_already_known_versions = {}, batch_delay: nil)
|
570
|
+
alias_method :each, def for_each_notification(new_already_known_versions = {}, batch_delay: nil)
|
408
571
|
update_already_known_versions(new_already_known_versions)
|
409
572
|
while notification = wait(batch_delay: batch_delay)
|
410
573
|
yield notification
|
@@ -419,7 +582,7 @@ module PgVersions
|
|
419
582
|
|
420
583
|
def drop
|
421
584
|
@notifications << nil
|
422
|
-
@
|
585
|
+
@inner.unsubscribe(self, @channels.keys) if @channels.keys.size > 0
|
423
586
|
#TODO: what to do if this object gets used after drop?
|
424
587
|
end
|
425
588
|
|
data/lib/pg_versions/version.rb
CHANGED
metadata
CHANGED
@@ -1,14 +1,13 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: pg_versions
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: '
|
4
|
+
version: '3.1'
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- yunta
|
8
|
-
autorequire:
|
9
8
|
bindir: bin
|
10
9
|
cert_chain: []
|
11
|
-
date:
|
10
|
+
date: 1980-01-02 00:00:00.000000000 Z
|
12
11
|
dependencies:
|
13
12
|
- !ruby/object:Gem::Dependency
|
14
13
|
name: rspec
|
@@ -85,7 +84,6 @@ homepage: https://gitlab.com/yunta/pg-versions
|
|
85
84
|
licenses:
|
86
85
|
- MIT
|
87
86
|
metadata: {}
|
88
|
-
post_install_message:
|
89
87
|
rdoc_options: []
|
90
88
|
require_paths:
|
91
89
|
- lib
|
@@ -100,8 +98,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
100
98
|
- !ruby/object:Gem::Version
|
101
99
|
version: '0'
|
102
100
|
requirements: []
|
103
|
-
rubygems_version: 3.
|
104
|
-
signing_key:
|
101
|
+
rubygems_version: 3.6.9
|
105
102
|
specification_version: 4
|
106
103
|
summary: Persistent timestamped postgres notification library
|
107
104
|
test_files: []
|