pg_versions 1.0 → 2.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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 272ebc261a3bbb1c96eb4545b2a215a55366b950ead6dd4b4547dbcf1ee6ea6a
4
- data.tar.gz: 5ba78eabb8f3fca3b7d1302b3e032660c625c294ae6cef31eed252e801558acb
3
+ metadata.gz: f6eab4d5b85196e9a17fe15efdb09cb6ef83d6821a060c40a5c68e3fae7c9fed
4
+ data.tar.gz: ff1c5c4157163144f7bb1808115c52e50438d8119415b7ea19df18e82e9d5bcd
5
5
  SHA512:
6
- metadata.gz: 2fb87931f3595b7938328461397901244ae2d357f80b93e5698bc813ba25e4ee542228afd68e17e2e3fc96e523ac0f86a22a6fc24e1bad81eb5557020b330716
7
- data.tar.gz: 7ecac0582c70f42295e870c4dcb8df35f23b94e9f5e5461717f50e4a8d8b561cc60513bf67eb02f9918f2b5887b2d70b4f9a59226cdf6a99cb08ebb9831d5234
6
+ metadata.gz: 12c82c86f3138b9b27eaade5a3eb97779b246de26a680de3e23e6d99daa576cc56a1b04ce2c8b82b81f72f8402a73f5eb2e5e7d4babb57651d458317f40eeea5
7
+ data.tar.gz: 70c41180db0eff19a524f1b65165800c08a50a22ddf25f07daaacb58a0c0b901e0bbf22b620f61499f52788ec4d9120de373b2cdbb3a1aaae3d02481dc211bd3
@@ -1,5 +1,5 @@
1
1
  # Operations on versions:
2
- # 1. bump - increase or assert version. ensures new version is unique and higher than any previous version, even if table entry existed before and got removed
2
+ # 1. bump - increase or assert version. ensures new version is unique and higher than any previous version, even if table entry existed before and got removed
3
3
  # 2. read - always returns a version, even if table entry is missing
4
4
  # 3. remove (clean) - for periodic pruning of old entries
5
5
  #
@@ -32,22 +32,28 @@ require 'set'
32
32
  module PgVersions
33
33
 
34
34
 
35
- class ConnectionAcquisitionFailedError < StandardError; end
35
+ class InvalidParameters < StandardError; end
36
36
 
37
37
  def self.timestamp_to_integers(input)
38
38
  "to_char(%s, 'YYYYMMDD')::integer || ',' || to_char(%s, 'HH24MISS')::integer || ',' || to_char(%s, 'US')::integer"%[input, input, input]
39
39
  end
40
40
 
41
41
 
42
- def self.with_connection(pg_connection)
43
- if pg_connection
44
- yield pg_connection
45
- elsif defined? ActiveRecord
42
+ def self.with_connection(pg_connection, reset: false, &block)
43
+ if pg_connection.kind_of? PG::Connection
44
+ if reset
45
+ pg_connection.sync_reset
46
+ pg_connection.exec("select;")
47
+ end
48
+ 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
46
52
  ActiveRecord::Base.connection_pool.with_connection { |ar_connection|
47
- yield ar_connection.instance_variable_get(:@connection)
53
+ block.call(ar_connection.instance_variable_get(:@connection))
48
54
  }
49
55
  else
50
- raise ConnectionAcquisitionFailedError, "Missing connection. Either pass pg connection object or import ActiveRecord."
56
+ raise InvalidParameters, "Missing connection. Either pass pg connection object or import ActiveRecord."
51
57
  end
52
58
  end
53
59
 
@@ -75,6 +81,7 @@ module PgVersions
75
81
 
76
82
  #TODO: ensure this is called only once per transaction, or that all bumps occur in the same order in all transactions, to avoid deadlocks
77
83
  def self.bump(*channels, connection: nil)
84
+ #TODO: pg_connection.exec returned nil once during testing.
78
85
  PgVersions.with_connection(connection) { |pg_connection|
79
86
  channels = [channels].flatten.sort
80
87
  return {} if channels.size == 0
@@ -101,7 +108,9 @@ module PgVersions
101
108
  FROM
102
109
  to_bump
103
110
  JOIN updated ON to_bump.channel = updated.channel;
104
- ").map { |row| [channels[Integer(row["i"])], string_to_version(row["version"])] }.to_h
111
+ ") { |result|
112
+ result.map { |row| [channels[Integer(row["i"])], string_to_version(row["version"])] }.to_h
113
+ }
105
114
  }
106
115
  end
107
116
 
@@ -124,8 +133,10 @@ module PgVersions
124
133
  JOIN pg_versions ON pg_versions.channel = channels.channel
125
134
  ORDER BY
126
135
  i DESC;
127
- ").each { |row|
128
- versions[channels.delete_at(Integer(row["i"]))] = string_to_version(row["version"])
136
+ ") { |result|
137
+ result.each { |row|
138
+ versions[channels.delete_at(Integer(row["i"]))] = string_to_version(row["version"])
139
+ }
129
140
  }
130
141
  #TODO: bump in the same query instead of calling bump
131
142
  versions.merge!(self.bump(channels, connection: pg_connection)) if channels.size > 0
@@ -135,89 +146,193 @@ module PgVersions
135
146
 
136
147
 
137
148
  class Notification
138
- attr_reader :channel, :version
139
- def initialize(channel,version)
140
- @channel, @version = channel, version
149
+ attr_reader :changed_versions, :changed, :all_versions, :versions
150
+ def initialize(changed_versions, all_versions)
151
+ @changed_versions, @all_versions = changed_versions, all_versions
152
+ @changed, @versions = changed_versions, all_versions
141
153
  end
142
154
  end
143
155
 
144
156
 
145
- class Connection
146
-
147
- def initialize(connection=nil)
148
- @actor_commands = Queue.new
149
- actor_notify_r, @actor_notify_w = IO.pipe
150
157
 
151
- connection_error_queue = Queue.new
152
- @actor = Thread.new {
158
+ class ConnectionThread
159
+
160
+ class Closing < StandardError
161
+ attr_reader :retpipe
162
+ def initialize(retpipe)
163
+ @retpipe = retpipe
164
+ end
165
+ end
166
+
167
+
168
+ attr_reader :status
169
+
170
+ def initialize(connection)
171
+ retry_on_exceptions = [ PG::ConnectionBad, PG::UnableToSend ]
172
+ retry_on_exceptions << ActiveRecord::ConnectionNotEstablished if defined? ActiveRecord
173
+
174
+ @subscribers = Hash.new { |h,k| h[k] = Set.new }
175
+ @status = :disconnected
176
+
177
+ @thread_requests_mutex = Mutex.new
178
+ @thread_requests = []
179
+ thread_requests_notify_r, @thread_requests_notify_w = IO.pipe
180
+
181
+ @thread = Thread.new {
182
+ reset_connection = false
183
+ retry_delay = 0
153
184
  begin
154
- PgVersions.with_connection(connection) { |pg_connection|
155
- connection_error_queue << false
156
- subscribers = Hash.new { |h,k| h[k] = Set.new }
185
+ PgVersions.with_connection(connection, reset: reset_connection) do |pg_connection|
186
+
187
+ @status = :connected
188
+ retry_delay = 0
189
+
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
+
157
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
+
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
+
158
215
  #TODO: handle errors
159
- reads,_writes,_errors = IO::select([pg_connection.socket_io, actor_notify_r])
216
+ reads,_writes,_errors = IO::select([pg_connection.socket_io, thread_requests_notify_r])
160
217
 
161
218
  if reads.include?(pg_connection.socket_io)
162
219
  pg_connection.consume_input
163
220
  end
164
221
 
165
- if reads.include?(actor_notify_r)
166
- @actor_commands.shift.call(pg_connection, subscribers)
167
- actor_notify_r.read(1)
168
- end
169
-
170
- while notification = pg_connection.notifies
171
- channel, payload = notification[:relname], notification[:extra]
172
- subscribers[channel].each { |subscriber|
173
- subscriber.notify(channel, PgVersions.string_to_version(payload))
174
- }
222
+ if reads.include?(thread_requests_notify_r)
223
+ thread_requests_notify_r.read(1)
175
224
  end
176
225
  }
177
- }
178
- rescue ConnectionAcquisitionFailedError => e
179
- connection_error_queue << e
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
239
+ end
240
+
241
+ private def listen(pg_connection, channel)
242
+ pg_connection.exec("LISTEN #{PG::Connection.quote_ident(channel)}")
243
+ end
244
+
245
+ private def unlisten(pg_connection, channel)
246
+ pg_connection.exec("UNLISTEN #{PG::Connection.quote_ident(channel)}")
247
+ end
248
+
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
253
+ }
254
+ subscriber.notify(PgVersions.read(channels, connection: pg_connection))
255
+ true
256
+ 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)
180
264
  end
181
265
  }
182
- (connection_error = connection_error_queue.shift) and raise connection_error
266
+ true
183
267
  end
184
268
 
269
+ private def read(pg_connection, channels)
270
+ PgVersions.read(channels, connection: pg_connection)
271
+ end
185
272
 
186
- def actor_call(&block)
187
- done = Queue.new
188
- @actor_commands << proc { |pg_connection, subscribers|
189
- done << block.call(pg_connection, subscribers)
190
- }
191
- @actor_notify_w.write('!')
192
- done.shift
273
+ private def bump(pg_connection, channels)
274
+ PgVersions.bump(channels, connection: pg_connection)
193
275
  end
194
276
 
277
+ def request(function, *params)
278
+ request_nonblock(function, *params).pop
279
+ end
195
280
 
196
- def bump(*channels)
197
- actor_call { |pg_connection, _subscribers|
198
- PgVersions.bump(channels, connection: pg_connection)
281
+ def request_nonblock(function, *params)
282
+ retpipe = Queue.new
283
+ @thread_requests_mutex.synchronize {
284
+ @thread_requests.push [function, retpipe, params]
199
285
  }
286
+ @thread_requests_notify_w.write('!')
287
+ retpipe
288
+ end
289
+
290
+ end
291
+
292
+ class Connection
293
+
294
+ def initialize(connection=nil)
295
+ @connection_thread = ConnectionThread.new(connection)
296
+ end
297
+
298
+ def close()
299
+ @connection_thread.request(:stop)
300
+ end
301
+
302
+ def bump(*channels)
303
+ @connection_thread.request(:bump, channels)
200
304
  end
201
305
 
202
306
 
203
307
  def read(*channels)
204
- actor_call { |pg_connection, _subscribers|
205
- PgVersions.read(channels, connection: pg_connection)
206
- }
308
+ @connection_thread.request(:read, channels)
207
309
  end
208
310
 
209
311
 
210
- def subscribe(*channels, known: {})
211
- subscription = Subscription.new(self)
312
+ def subscribe(*channels, known: {}, batch_delay: 0.01)
313
+ subscription = Subscription.new(@connection_thread, batch_delay)
212
314
  subscription.subscribe([channels].flatten, known: known)
213
- subscription
315
+ if block_given?
316
+ Thread.handle_interrupt(Object => :never) {
317
+ begin
318
+ Thread.handle_interrupt(Object => :immediate) {
319
+ yield subscription
320
+ }
321
+ ensure
322
+ subscription.drop
323
+ end
324
+ }
325
+ else
326
+ subscription
327
+ end
214
328
  end
215
329
 
216
330
 
217
331
  class Subscription
218
332
 
219
- def initialize(connection)
220
- @connection = connection
333
+ def initialize(connection_thread, batch_delay)
334
+ @connection_thread = connection_thread
335
+ @batch_delay = batch_delay
221
336
  @notifications = Queue.new
222
337
  @already_known_versions = Hash.new { |h,k| h[k] = [] }
223
338
  @channels = Hash.new(0)
@@ -231,15 +346,7 @@ module PgVersions
231
346
  (@channels[channel] += 1) == 1
232
347
  }
233
348
  if channels.size > 0
234
- @connection.actor_call { |pg_connection, subscribers|
235
- channels.each { |channel|
236
- subscribers[channel] << self
237
- pg_connection.exec("LISTEN #{PG::Connection.quote_ident(channel)}") if subscribers[channel].size == 1
238
- }
239
- PgVersions.read(channels, connection: pg_connection).each_pair { |channel, version|
240
- notify(channel, version)
241
- }
242
- }
349
+ @connection_thread.request(:subscribe, self, channels)
243
350
  end
244
351
  end
245
352
 
@@ -252,59 +359,68 @@ module PgVersions
252
359
  @channels.delete(channel) if @channels[channel] == 0
253
360
  not @channels.has_key?(channel)
254
361
  }
255
- @connection.actor_call { |pg_connection, subscribers|
256
- channels.each { |channel|
257
- subscribers[channel].delete(self)
258
- if subscribers[channel].size == 0
259
- pg_connection.exec("UNLISTEN #{PG::Connection.quote_ident(channel)}")
260
- subscribers.delete(channel)
261
- end
262
- }
263
- }
362
+ @connection_thread.request(:unsubscribe, self, channels)
264
363
  end
265
364
 
266
365
 
267
- def read(*channels, notify: true)
366
+ def read(*channels, notify: false)
268
367
  channels = @channels.keys if channels.size == 0
269
- versions = @connection.actor_call { |pg_connection, subscribers|
270
- PgVersions.read(channels, connection: pg_connection)
271
- }
368
+ versions = @connection_thread.request(:read, channels)
272
369
  update_already_known_versions(versions) if not notify
273
370
  versions
274
371
  end
275
372
 
276
373
 
277
- def bump(*channels, notify: true)
374
+ def bump(*channels, notify: false)
278
375
  channels = @channels.keys if channels.size == 0
279
- versions = @connection.actor_call { |pg_connection, subscribers|
280
- PgVersions.bump(channels, connection: pg_connection)
281
- }
376
+ versions = @connection_thread.request(:bump, channels)
282
377
  update_already_known_versions(versions) if not notify
283
378
  versions
284
379
  end
285
380
 
286
381
 
287
- def wait(new_already_known_versions = {})
382
+ #TODO: make this resume-able after forced exception
383
+ def wait(new_already_known_versions = {}, batch_delay: nil)
384
+ batch_delay = @batch_delay if batch_delay.nil?
288
385
  update_already_known_versions(new_already_known_versions)
289
386
  loop {
290
- channel, version = @notifications.shift
291
- return nil if not channel #termination
292
- if (@already_known_versions[channel] <=> version) == -1
293
- @already_known_versions[channel] = version
294
- return Notification.new(channel, version)
387
+ events = [@notifications.shift]
388
+ sleep batch_delay if batch_delay
389
+ events << @notifications.shift while not @notifications.empty?
390
+ changed_versions = {}
391
+ events.each { |versions|
392
+ return nil if not versions #termination
393
+ versions.each { |channel, version|
394
+ if (@already_known_versions[channel] <=> version) == -1
395
+ @already_known_versions[channel] = version
396
+ changed_versions[channel] = version
397
+ end
398
+ }
399
+ }
400
+ if changed_versions.size > 0
401
+ return Notification.new(changed_versions, @already_known_versions.dup)
295
402
  end
296
403
  }
297
404
  end
298
405
 
299
406
 
300
- def notify(channel, payload)
301
- @notifications << [channel, payload]
407
+ def each(new_already_known_versions = {}, batch_delay: nil)
408
+ update_already_known_versions(new_already_known_versions)
409
+ while notification = wait(batch_delay: batch_delay)
410
+ yield notification
411
+ end
412
+ end
413
+
414
+
415
+ def notify(versions)
416
+ @notifications << versions
302
417
  end
303
418
 
304
419
 
305
420
  def drop
306
- @notifications << [nil, nil]
307
- unsubscribe(@channels.keys)
421
+ @notifications << nil
422
+ @connection_thread.request_nonblock(:unsubscribe, self, @channels.keys)
423
+ #TODO: what to do if this object gets used after drop?
308
424
  end
309
425
 
310
426
 
@@ -1,3 +1,3 @@
1
1
  module PgVersions
2
- VERSION = "1.0"
2
+ VERSION = "2.1"
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: '1.0'
4
+ version: '2.1'
5
5
  platform: ruby
6
6
  authors:
7
7
  - yunta
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2022-01-16 00:00:00.000000000 Z
11
+ date: 2023-01-14 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.2.22
103
+ rubygems_version: 3.3.26
104
104
  signing_key:
105
105
  specification_version: 4
106
106
  summary: Persistent timestamped postgres notification library