pg_versions 1.0 → 2.1

Sign up to get free protection for your applications and to get access to all the features.
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