message_bus 2.0.0.beta.2 → 2.0.0.beta.3

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.

Potentially problematic release.


This version of message_bus might be problematic. Click here for more details.

@@ -0,0 +1,396 @@
1
+ require 'pg'
2
+
3
+ module MessageBus::Postgres; end
4
+
5
+ class MessageBus::Postgres::Client
6
+ INHERITED_CONNECTIONS = []
7
+
8
+ class Listener
9
+ attr_reader :do_sub, :do_unsub, :do_message
10
+
11
+ def subscribe(&block)
12
+ @do_sub = block
13
+ end
14
+
15
+ def unsubscribe(&block)
16
+ @do_unsub = block
17
+ end
18
+
19
+ def message(&block)
20
+ @do_message = block
21
+ end
22
+ end
23
+
24
+ def initialize(config)
25
+ @config = config
26
+ @listening_on = {}
27
+ @available = []
28
+ @allocated = {}
29
+ @mutex = Mutex.new
30
+ @pid = Process.pid
31
+ end
32
+
33
+ def add(channel, value)
34
+ hold{|conn| exec_prepared(conn, 'insert_message', [channel, value]){|r| r.getvalue(0,0).to_i}}
35
+ end
36
+
37
+ def clear_global_backlog(backlog_id, num_to_keep)
38
+ if backlog_id > num_to_keep
39
+ hold{|conn| exec_prepared(conn, 'clear_global_backlog', [backlog_id - num_to_keep])}
40
+ nil
41
+ end
42
+ end
43
+
44
+ def clear_channel_backlog(channel, backlog_id, num_to_keep)
45
+ hold{|conn| exec_prepared(conn, 'clear_channel_backlog', [channel, backlog_id, num_to_keep])}
46
+ nil
47
+ end
48
+
49
+ def expire(max_backlog_age)
50
+ hold{|conn| exec_prepared(conn, 'expire', [max_backlog_age])}
51
+ nil
52
+ end
53
+
54
+ def backlog(channel, backlog_id)
55
+ hold{|conn| exec_prepared(conn, 'channel_backlog', [channel, backlog_id]){|r| r.values.each{|a| a[0] = a[0].to_i}}}
56
+ end
57
+
58
+ def global_backlog(backlog_id)
59
+ hold{|conn| exec_prepared(conn, 'global_backlog', [backlog_id]){|r| r.values.each{|a| a[0] = a[0].to_i}}}
60
+ end
61
+
62
+ def get_value(channel, id)
63
+ hold{|conn| exec_prepared(conn, 'get_message', [channel, id]){|r| r.getvalue(0,0)}}
64
+ end
65
+
66
+ def reconnect
67
+ sync do
68
+ @listening_on.clear
69
+ @available.clear
70
+ end
71
+ end
72
+
73
+ # Dangerous, drops the message_bus table containing the backlog if it exists.
74
+ def reset!
75
+ hold do |conn|
76
+ conn.exec 'DROP TABLE IF EXISTS message_bus'
77
+ create_table(conn)
78
+ end
79
+ end
80
+
81
+ def max_id(channel=nil)
82
+ block = proc do |r|
83
+ if r.ntuples > 0
84
+ r.getvalue(0,0).to_i
85
+ else
86
+ 0
87
+ end
88
+ end
89
+
90
+ if channel
91
+ hold{|conn| exec_prepared(conn, 'max_channel_id', [channel], &block)}
92
+ else
93
+ hold{|conn| exec_prepared(conn, 'max_id', &block)}
94
+ end
95
+ end
96
+
97
+ def publish(channel, data)
98
+ hold{|conn| exec_prepared(conn, 'publish', [channel, data])}
99
+ end
100
+
101
+ def subscribe(channel)
102
+ obj = Object.new
103
+ sync{@listening_on[channel] = obj}
104
+ listener = Listener.new
105
+ yield listener
106
+ pid = Process.pid
107
+
108
+ conn = raw_pg_connection
109
+ conn.exec "LISTEN #{channel}"
110
+ listener.do_sub.call
111
+ while listening_on?(channel, obj)
112
+ conn.wait_for_notify(10) do |_,_,payload|
113
+ listener.do_message.call(nil, payload)
114
+ end
115
+ break if pid != Process.pid
116
+ end
117
+ listener.do_unsub.call
118
+
119
+ if pid != Process.pid
120
+ sync{INHERITED_CONNECTIONS << conn}
121
+ else
122
+ conn.exec "UNLISTEN #{channel}"
123
+ end
124
+
125
+ nil
126
+ end
127
+
128
+ def unsubscribe
129
+ sync{@listening_on.clear}
130
+ end
131
+
132
+ private
133
+
134
+ def exec_prepared(conn, *a)
135
+ r = conn.exec_prepared(*a)
136
+ yield r if block_given?
137
+ ensure
138
+ r.clear if r.respond_to?(:clear)
139
+ end
140
+
141
+ def create_table(conn)
142
+ conn.exec 'CREATE TABLE message_bus (id bigserial PRIMARY KEY, channel text NOT NULL, value text NOT NULL, added_at timestamp DEFAULT CURRENT_TIMESTAMP NOT NULL)'
143
+ conn.exec 'CREATE INDEX table_channel_id_index ON message_bus (channel, id)'
144
+ conn.exec 'CREATE INDEX table_added_at_index ON message_bus (added_at)'
145
+ nil
146
+ end
147
+
148
+ def hold
149
+ current_pid = Process.pid
150
+ if current_pid != @pid
151
+ @pid = current_pid
152
+ sync do
153
+ @listening_on.clear
154
+ INHERITED_CONNECTIONS.concat(@available)
155
+ @available.clear
156
+ end
157
+ end
158
+
159
+ if conn = sync{@allocated[Thread.current]}
160
+ return yield(conn)
161
+ end
162
+
163
+ begin
164
+ conn = sync{@available.shift} || new_pg_connection
165
+ sync{@allocated[Thread.current] = conn}
166
+ yield conn
167
+ rescue PG::ConnectionBad => e
168
+ # don't add this connection back to the pool
169
+ ensure
170
+ sync{@allocated.delete(Thread.current)}
171
+ if Process.pid != current_pid
172
+ sync{INHERITED_CONNECTIONS << conn}
173
+ elsif conn && !e
174
+ sync{@available << conn}
175
+ end
176
+ end
177
+ end
178
+
179
+ def raw_pg_connection
180
+ PG::Connection.connect(@config[:backend_options] || {})
181
+ end
182
+
183
+ def new_pg_connection
184
+ conn = raw_pg_connection
185
+
186
+ begin
187
+ conn.exec("SELECT 'message_bus'::regclass")
188
+ rescue PG::UndefinedTable
189
+ create_table(conn)
190
+ end
191
+
192
+ conn.exec 'PREPARE insert_message AS INSERT INTO message_bus (channel, value) VALUES ($1, $2) RETURNING id'
193
+ conn.exec 'PREPARE clear_global_backlog AS DELETE FROM message_bus WHERE (id <= $1)'
194
+ conn.exec 'PREPARE clear_channel_backlog AS DELETE FROM message_bus WHERE ((channel = $1) AND (id <= (SELECT id FROM message_bus WHERE ((channel = $1) AND (id <= $2)) ORDER BY id DESC OFFSET $3)))'
195
+ conn.exec 'PREPARE channel_backlog AS SELECT id, value FROM message_bus WHERE ((channel = $1) AND (id > $2)) ORDER BY id'
196
+ conn.exec 'PREPARE global_backlog AS SELECT id, channel, value FROM message_bus WHERE (id > $1) ORDER BY id'
197
+ conn.exec "PREPARE expire AS DELETE FROM message_bus WHERE added_at < CURRENT_TIMESTAMP - ($1::text || ' seconds')::interval"
198
+ conn.exec 'PREPARE get_message AS SELECT value FROM message_bus WHERE ((channel = $1) AND (id = $2))'
199
+ conn.exec 'PREPARE max_channel_id AS SELECT max(id) FROM message_bus WHERE (channel = $1)'
200
+ conn.exec 'PREPARE max_id AS SELECT max(id) FROM message_bus'
201
+ conn.exec 'PREPARE publish AS SELECT pg_notify($1, $2)'
202
+
203
+ conn
204
+ end
205
+
206
+ def listening_on?(channel, obj)
207
+ sync{@listening_on[channel]} == obj
208
+ end
209
+
210
+ def sync
211
+ @mutex.synchronize{yield}
212
+ end
213
+ end
214
+
215
+ class MessageBus::Postgres::ReliablePubSub
216
+ attr_reader :subscribed
217
+ attr_accessor :max_publish_retries, :max_publish_wait, :max_backlog_size,
218
+ :max_global_backlog_size, :max_in_memory_publish_backlog,
219
+ :max_backlog_age
220
+
221
+ UNSUB_MESSAGE = "$$UNSUBSCRIBE"
222
+
223
+ def self.reset!(config)
224
+ MessageBus::Postgres::Client.new(config).reset!
225
+ end
226
+
227
+ # max_backlog_size is per multiplexed channel
228
+ def initialize(config = {}, max_backlog_size = 1000)
229
+ @config = config
230
+ @max_backlog_size = max_backlog_size
231
+ @max_global_backlog_size = 2000
232
+ @max_publish_retries = 10
233
+ @max_publish_wait = 500 #ms
234
+ @max_in_memory_publish_backlog = 1000
235
+ @in_memory_backlog = []
236
+ @lock = Mutex.new
237
+ @flush_backlog_thread = nil
238
+ # after 7 days inactive backlogs will be removed
239
+ @max_backlog_age = 604800
240
+ @h = {}
241
+ end
242
+
243
+ def new_connection
244
+ MessageBus::Postgres::Client.new(@config)
245
+ end
246
+
247
+ def backend
248
+ :postgres
249
+ end
250
+
251
+ def after_fork
252
+ client.reconnect
253
+ end
254
+
255
+ def postgresql_channel_name
256
+ db = @config[:db] || 0
257
+ "_message_bus_#{db}"
258
+ end
259
+
260
+ def client
261
+ @client ||= new_connection
262
+ end
263
+
264
+ # use with extreme care, will nuke all of the data
265
+ def reset!
266
+ client.reset!
267
+ end
268
+
269
+ def publish(channel, data, queue_in_memory=true)
270
+ client = self.client
271
+ backlog_id = client.add(channel, data)
272
+ msg = MessageBus::Message.new backlog_id, backlog_id, channel, data
273
+ payload = msg.encode
274
+ client.publish postgresql_channel_name, payload
275
+ client.clear_global_backlog(backlog_id, @max_global_backlog_size)
276
+ client.expire(@max_backlog_age)
277
+ client.clear_channel_backlog(channel, backlog_id, @max_backlog_size)
278
+
279
+ backlog_id
280
+ end
281
+
282
+ def last_id(channel)
283
+ client.max_id(channel)
284
+ end
285
+
286
+ def backlog(channel, last_id = nil)
287
+ items = client.backlog channel, last_id.to_i
288
+
289
+ items.map! do |id, data|
290
+ MessageBus::Message.new id, id, channel, data
291
+ end
292
+ end
293
+
294
+ def global_backlog(last_id = nil)
295
+ last_id = last_id.to_i
296
+
297
+ items = client.global_backlog last_id.to_i
298
+
299
+ items.map! do |id, channel, data|
300
+ MessageBus::Message.new id, id, channel, data
301
+ end
302
+ end
303
+
304
+ def get_message(channel, message_id)
305
+ if data = client.get_value(channel, message_id)
306
+ MessageBus::Message.new message_id, message_id, channel, data
307
+ else
308
+ nil
309
+ end
310
+ end
311
+
312
+ def subscribe(channel, last_id = nil)
313
+ # trivial implementation for now,
314
+ # can cut down on connections if we only have one global subscriber
315
+ raise ArgumentError unless block_given?
316
+
317
+ global_subscribe(last_id) do |m|
318
+ yield m if m.channel == channel
319
+ end
320
+ end
321
+
322
+ def process_global_backlog(highest_id)
323
+ if highest_id > client.max_id
324
+ highest_id = 0
325
+ end
326
+
327
+ global_backlog(highest_id).each do |old|
328
+ yield old
329
+ highest_id = old.global_id
330
+ end
331
+
332
+ highest_id
333
+ end
334
+
335
+ def global_unsubscribe
336
+ client.publish(postgresql_channel_name, UNSUB_MESSAGE)
337
+ @subscribed = false
338
+ end
339
+
340
+ def global_subscribe(last_id=nil, &blk)
341
+ raise ArgumentError unless block_given?
342
+ highest_id = last_id
343
+
344
+ begin
345
+ client.subscribe(postgresql_channel_name) do |on|
346
+ h = {}
347
+
348
+ on.subscribe do
349
+ if highest_id
350
+ process_global_backlog(highest_id) do |m|
351
+ h[m.global_id] = true
352
+ yield m
353
+ end
354
+ end
355
+ h = nil if h.empty?
356
+ @subscribed = true
357
+ end
358
+
359
+ on.unsubscribe do
360
+ @subscribed = false
361
+ end
362
+
363
+ on.message do |c,m|
364
+ if m == UNSUB_MESSAGE
365
+ @subscribed = false
366
+ return
367
+ end
368
+ m = MessageBus::Message.decode m
369
+
370
+ # we have 3 options
371
+ #
372
+ # 1. message came in the correct order GREAT, just deal with it
373
+ # 2. message came in the incorrect order COMPLICATED, wait a tiny bit and clear backlog
374
+ # 3. message came in the incorrect order and is lowest than current highest id, reset
375
+
376
+ if h
377
+ # If already yielded during the clear backlog when subscribing,
378
+ # don't yield a duplicate copy.
379
+ unless h.delete(m.global_id)
380
+ h = nil if h.empty?
381
+ yield m
382
+ end
383
+ else
384
+ yield m
385
+ end
386
+ end
387
+ end
388
+ rescue => error
389
+ MessageBus.logger.warn "#{error} subscribe failed, reconnecting in 1 second. Call stack\n#{error.backtrace.join("\n")}"
390
+ sleep 1
391
+ retry
392
+ end
393
+ end
394
+
395
+ MessageBus::BACKENDS[:postgres] = self
396
+ end
@@ -339,4 +339,5 @@ class MessageBus::Redis::ReliablePubSub
339
339
  end
340
340
  end
341
341
 
342
+ MessageBus::BACKENDS[:redis] = self
342
343
  end
@@ -84,13 +84,22 @@ class MessageBus::Rack::Middleware
84
84
  client = MessageBus::Client.new(message_bus: @bus, client_id: client_id,
85
85
  user_id: user_id, site_id: site_id, group_ids: group_ids)
86
86
 
87
- request = Rack::Request.new(env)
88
- request.POST.each do |k,v|
89
- if k == "__seq".freeze
90
- client.seq = v.to_i
91
- else
87
+ if channels = env['message_bus.channels']
88
+ if seq = env['message_bus.seq']
89
+ client.seq = seq.to_i
90
+ end
91
+ channels.each do |k, v|
92
92
  client.subscribe(k, v)
93
93
  end
94
+ else
95
+ request = Rack::Request.new(env)
96
+ request.POST.each do |k,v|
97
+ if k == "__seq".freeze
98
+ client.seq = v.to_i
99
+ else
100
+ client.subscribe(k, v)
101
+ end
102
+ end
94
103
  end
95
104
 
96
105
  headers = {}
@@ -1,3 +1,3 @@
1
1
  module MessageBus
2
- VERSION = "2.0.0.beta.2"
2
+ VERSION = "2.0.0.beta.3"
3
3
  end
data/message_bus.gemspec CHANGED
@@ -15,6 +15,8 @@ Gem::Specification.new do |gem|
15
15
  gem.name = "message_bus"
16
16
  gem.require_paths = ["lib"]
17
17
  gem.version = MessageBus::VERSION
18
+ gem.required_ruby_version = ">= 1.9.3"
18
19
  gem.add_runtime_dependency 'rack', '>= 1.1.3'
19
- gem.add_runtime_dependency 'redis'
20
+ gem.add_development_dependency 'redis'
21
+ gem.add_development_dependency 'pg'
20
22
  end
@@ -1,10 +1,11 @@
1
+ require_relative '../../../spec_helper'
1
2
  asset_directory = File.expand_path('../../../../../assets', __FILE__)
2
3
  asset_file_paths = Dir.glob(File.join(asset_directory, 'message-bus.js'))
3
4
  asset_file_names = asset_file_paths.map{|e| File.basename(e) }
4
5
 
5
6
  describe asset_file_names do
6
7
  it 'should contain .js files' do
7
- expect(asset_file_names).to include('message-bus.js')
8
+ asset_file_names.must_include('message-bus.js')
8
9
  end
9
10
  end
10
11
 
@@ -12,8 +13,7 @@ asset_file_paths.each do | path |
12
13
  describe "Asset file #{File.basename(path).inspect}" do
13
14
  it 'should be encodable as UTF8' do
14
15
  binary_data = File.open(path, 'rb'){|f| f.read }
15
- encode_block = -> { binary_data.encode(Encoding::UTF_8) }
16
- expect(encode_block).not_to raise_error
16
+ binary_data.encode(Encoding::UTF_8)
17
17
  end
18
18
  end
19
- end
19
+ end