pg_versions 2.1 → 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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: f6eab4d5b85196e9a17fe15efdb09cb6ef83d6821a060c40a5c68e3fae7c9fed
4
- data.tar.gz: ff1c5c4157163144f7bb1808115c52e50438d8119415b7ea19df18e82e9d5bcd
3
+ metadata.gz: c7b6354edf3a40a5c96c953a579ce6a8e581933c271722abb90bfee0ce056e9c
4
+ data.tar.gz: 90e4f121ce0888b6cf2a57f7fe92370be6e7933ef6d4ea5288620f4869bc6e36
5
5
  SHA512:
6
- metadata.gz: 12c82c86f3138b9b27eaade5a3eb97779b246de26a680de3e23e6d99daa576cc56a1b04ce2c8b82b81f72f8402a73f5eb2e5e7d4babb57651d458317f40eeea5
7
- data.tar.gz: 70c41180db0eff19a524f1b65165800c08a50a22ddf25f07daaacb58a0c0b901e0bbf22b620f61499f52788ec4d9120de373b2cdbb3a1aaae3d02481dc211bd3
6
+ metadata.gz: a0054bcc270e214fa919c6d9003f99ae85cf0f95c821a0a0267e2256d1627a0a4310a3b4ab90cf1eb349c2700d7b90b38dd86b8de77c48bb241a42c770ec5980
7
+ data.tar.gz: 0fa50a3c96302bd9e14ea3ba943ab63c70ecf005741cf33fb6181f56c4f22c1e21e4140c325e9a53902ea3b40c22e99a66c59f92fd9eab96bc7655d116a09a95
@@ -1,11 +1,11 @@
1
1
  class CreatePgVersionsTable < ActiveRecord::Migration[7.0]
2
2
 
3
3
  def up
4
- PgVersions.create_table
4
+ PgVersions.create_table(ActiveRecord::Base)
5
5
  end
6
6
 
7
7
  def down
8
- PgVersions.drop_table
8
+ PgVersions.drop_table(ActiveRecord::Base)
9
9
  end
10
10
 
11
11
  end
@@ -26,97 +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(pg_connection, reset: false, &block)
43
- if pg_connection.kind_of? PG::Connection
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, "Missing connection. Either pass pg connection object or import ActiveRecord."
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=nil)
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
- def self.drop_table(connection=nil)
75
- PgVersions.with_connection(connection) { |pg_connection|
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, connection: nil)
84
- #TODO: pg_connection.exec returned nil once during testing.
85
- PgVersions.with_connection(connection) { |pg_connection|
86
- channels = [channels].flatten.sort
87
- return {} if channels.size == 0
88
- quoted_channels = channels.map.with_index { |channel, i| "(#{i},'#{pg_connection.escape_string(channel)}')" }.join(", ")
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
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
-
118
- def self.read(*channels, connection: nil)
119
- PgVersions.with_connection(connection) { |pg_connection|
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|
120
162
  channels = [channels].flatten.sort
121
163
  return {} if channels.size == 0
122
164
  versions = {}
@@ -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
- #TODO: bump in the same query instead of calling bump
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
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
157
209
 
158
- class ConnectionThread
159
210
 
160
- class Closing < StandardError
161
- attr_reader :retpipe
162
- def initialize(retpipe)
163
- @retpipe = retpipe
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
 
250
+
251
+ def wake_processor
252
+ @command_notify_w&.write('!')
253
+ @command_notify_w&.flush
254
+ end
167
255
 
168
- attr_reader :status
169
256
 
170
- def initialize(connection)
171
- retry_on_exceptions = [ PG::ConnectionBad, PG::UnableToSend ]
172
- retry_on_exceptions << ActiveRecord::ConnectionNotEstablished if defined? ActiveRecord
257
+ def get_channels
258
+ @mutex.synchronize {
259
+ return @subscriptions.keys
260
+ }
261
+ end
173
262
 
174
- @subscribers = Hash.new { |h,k| h[k] = Set.new }
175
- @status = :disconnected
176
263
 
177
- @thread_requests_mutex = Mutex.new
178
- @thread_requests = []
179
- thread_requests_notify_r, @thread_requests_notify_w = IO.pipe
264
+ def notify(channel, version)
265
+ @mutex.synchronize {
266
+ (@subscriptions[channel] or []).each { |subscriber|
267
+ subscriber.notify({ channel => version })
268
+ }
269
+ }
270
+ end
180
271
 
181
- @thread = Thread.new {
182
- reset_connection = false
183
- retry_delay = 0
184
- begin
185
- PgVersions.with_connection(connection, reset: reset_connection) do |pg_connection|
186
-
187
- @status = :connected
188
- retry_delay = 0
189
272
 
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
- }
273
+ def taking_bumps
274
+ @mutex.synchronize {
275
+ yield @bumps
276
+ @bumps = []
277
+ }
278
+ end
198
279
 
199
- loop {
200
- @thread_requests_mutex.synchronize {
201
- @thread_requests.each { |function, retpipe, params|
202
- raise Closing, retpipe if function == :stop
203
- retpipe << send(function, pg_connection, *params)
204
- }
205
- @thread_requests.clear
206
- }
207
280
 
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
281
+ def taking_reads
282
+ @mutex.synchronize {
283
+ yield @reads
284
+ @reads = []
285
+ }
286
+ end
214
287
 
215
- #TODO: handle errors
216
- reads,_writes,_errors = IO::select([pg_connection.socket_io, thread_requests_notify_r])
217
-
218
- if reads.include?(pg_connection.socket_io)
219
- pg_connection.consume_input
220
- end
221
288
 
222
- if reads.include?(thread_requests_notify_r)
223
- thread_requests_notify_r.read(1)
224
- end
225
- }
226
- rescue Closing => e
227
- e.retpipe << true
228
- end
229
- rescue *retry_on_exceptions => e
230
- reset_connection = true
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
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
- private def listen(pg_connection, channel)
242
- pg_connection.exec("LISTEN #{PG::Connection.quote_ident(channel)}")
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
- private def unlisten(pg_connection, channel)
246
- pg_connection.exec("UNLISTEN #{PG::Connection.quote_ident(channel)}")
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
- private def subscribe(pg_connection, subscriber, channels)
250
- channels.each { |channel|
251
- @subscribers[channel] << subscriber
252
- listen(pg_connection, channel) if @subscribers[channel].size == 1
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(PgVersions.read(channels, connection: pg_connection))
329
+ subscriber.notify(read(channels)) # this runs wake_processor, so not doing it explicitly
255
330
  true
256
331
  end
257
-
258
- private def unsubscribe(pg_connection, subscriber, channels)
259
- channels.each { |channel|
260
- @subscribers[channel].delete(subscriber)
261
- if @subscribers[channel].size == 0
262
- unlisten(pg_connection, channel)
263
- @subscribers.delete(channel)
264
- end
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
- private def bump(pg_connection, channels)
274
- PgVersions.bump(channels, connection: pg_connection)
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 request_nonblock(function, *params)
282
- retpipe = Queue.new
283
- @thread_requests_mutex.synchronize {
284
- @thread_requests.push [function, retpipe, params]
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
- @thread_requests_notify_w.write('!')
287
- retpipe
371
+ result.pop
288
372
  end
289
-
373
+
290
374
  end
291
375
 
376
+
292
377
  class Connection
293
-
294
- def initialize(connection=nil)
295
- @connection_thread = ConnectionThread.new(connection)
378
+
379
+ def initialize()
380
+ @inner = ConnectionInner.new
296
381
  end
297
382
 
298
- def close()
299
- @connection_thread.request(:stop)
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
451
+ end
452
+
453
+
454
+ def close
455
+ @inner.close
300
456
  end
301
457
 
458
+
302
459
  def bump(*channels)
303
- @connection_thread.request(:bump, channels)
460
+ @inner.bump(channels)
304
461
  end
305
462
 
306
463
 
307
464
  def read(*channels)
308
- @connection_thread.request(:read, channels)
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(@connection_thread, batch_delay)
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(connection_thread, batch_delay)
334
- @connection_thread = connection_thread
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
- @connection_thread.request(:subscribe, self, channels)
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
- @connection_thread.request(:unsubscribe, self, channels)
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 = @connection_thread.request(:read, channels)
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 = @connection_thread.request(:bump, channels)
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
- @connection_thread.request_nonblock(:unsubscribe, self, @channels.keys)
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
 
@@ -1,3 +1,3 @@
1
1
  module PgVersions
2
- VERSION = "2.1"
2
+ VERSION = "3.0"
3
3
  end
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: '2.1'
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: 2023-01-14 00:00:00.000000000 Z
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.3.26
103
+ rubygems_version: 3.5.22
104
104
  signing_key:
105
105
  specification_version: 4
106
106
  summary: Persistent timestamped postgres notification library