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

Sign up to get free protection for your applications and to get access to all the features.

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