message_bus 1.0.12 → 1.0.13

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.

checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: 9edd8ca36b455984824a1b9841e626352f04751e
4
- data.tar.gz: 4f9069b34ea6823a316d20079ec698444f0c6e43
3
+ metadata.gz: 65597f9dd4a3b22117b594ccaff431aa8ed63198
4
+ data.tar.gz: 66c1a2a4079eb8d685e6029f1c0a6da4ecb7b7d3
5
5
  SHA512:
6
- metadata.gz: 6fcc39d57dc21800df3e6fee29ec9bcf95285a44588e6b05e4746693cd070c7f6492e86fdc852351bfde59b209f7d21780912927d0bf20a290f6d4c4ebfb2ebd
7
- data.tar.gz: 408932aae79880e94ac15d65c2a5d6fa75eb46eacc2a6fda8eb45f704daac3e7cc1322e4ea5dbe5653b548cfa6b8299a15e157dce46d7bc00d288e45a4d2ef3a
6
+ metadata.gz: 6b5e51a5a51aec25039cef045b5c75cd4b27e610c7f96c7fa694aee4c7cc0a65b43be5224b8daed76f3a20908a8382baee9c20b15f281c5d76573c135599ea5e
7
+ data.tar.gz: 121e47af10c76c4ad0ffc7cd70f679632c9be4808adae2314ef977bafd82a341456feb5d18b2ac14827ee2abb4bf7723aad759df87ddab0330c054fd1787bfc2
data/CHANGELOG CHANGED
@@ -1,3 +1,8 @@
1
+ 08-06-2015
2
+ - Version 1.0.13
3
+ - Fix: on global subscribe reconnect replay missed messages
4
+ - Feature: keepalive tests for global subscribe, catches hung redis connections
5
+
1
6
  28-05-2015
2
7
  - Version 1.0.12
3
8
  - Feature: Support client_id targetted message
data/Gemfile CHANGED
@@ -7,6 +7,7 @@ group :test do
7
7
  gem 'rspec'
8
8
  gem 'redis'
9
9
  gem 'rake'
10
+ gem 'rbtrace'
10
11
  gem 'guard-rspec'
11
12
  gem 'rb-inotify', require: RUBY_PLATFORM =~ /linux/i ? 'rb-inotify' : false
12
13
  gem 'rack'
@@ -2,7 +2,48 @@ $LOAD_PATH.unshift File.expand_path('../../../lib', __FILE__)
2
2
  require 'message_bus'
3
3
  require 'sinatra'
4
4
  require 'sinatra/base'
5
+ require 'set'
6
+ require 'json'
5
7
 
8
+ $online = Hash.new
9
+
10
+ MessageBus.subscribe "/presence" do |msg|
11
+ if user = msg.data["enter"]
12
+ $online[user] = Time.now
13
+ end
14
+ if user = msg.data["leave"]
15
+ $online.delete user
16
+ end
17
+ end
18
+
19
+ MessageBus.user_id_lookup do |env|
20
+ name = env["HTTP_X_NAME"]
21
+ if name
22
+ unless $online[name]
23
+ MessageBus.publish "/presence", {enter: name}
24
+ end
25
+ $online[name] = Time.now
26
+ end
27
+ name
28
+ end
29
+
30
+ def expire_old_sessions
31
+ $online.each do |name,time|
32
+ if (Time.now - (5*60)) > time
33
+ puts "forcing leave for #{name} session timed out"
34
+ MessageBus.publish "/presence", {leave: name}
35
+ end
36
+ end
37
+ rescue => e
38
+ # need to make $online thread safe
39
+ p e
40
+ end
41
+ Thread.new do
42
+ while true
43
+ expire_old_sessions
44
+ sleep 1
45
+ end
46
+ end
6
47
 
7
48
  class Chat < Sinatra::Base
8
49
 
@@ -10,8 +51,25 @@ class Chat < Sinatra::Base
10
51
 
11
52
  use MessageBus::Rack::Middleware
12
53
 
54
+ post '/enter' do
55
+ name = params["name"]
56
+ i = 1
57
+ while $online.include? name
58
+ name = "#{params["name"]}#{i}"
59
+ i += 1
60
+ end
61
+ MessageBus.publish '/presence', {enter: name}
62
+ {users: $online.keys, name: name}.to_json
63
+ end
64
+
65
+ post '/leave' do
66
+ #puts "Got leave for #{params["name"]}"
67
+ MessageBus.publish '/presence', {leave: params["name"]}
68
+ end
69
+
13
70
  post '/message' do
14
- MessageBus.publish '/message', params
71
+ msg = {data: params["data"][0..500], name: params["name"][0..100]}
72
+ MessageBus.publish '/message', msg
15
73
 
16
74
  "OK"
17
75
  end
@@ -26,13 +84,29 @@ class Chat < Sinatra::Base
26
84
  <script src="/message-bus.js"></script>
27
85
  <style>
28
86
  #panel { position: fixed; bottom: 0; background-color: #FFFFFF; }
87
+ #panel form { margin-bottom: 4px; }
88
+ #panel button, #panel textarea { height: 40px }
89
+ #panel button { width: 100px; vertical-align: top; }
29
90
  #messages { padding-bottom: 40px; }
30
91
  .hidden { display: none; }
92
+ #users {
93
+ position: fixed; top: 0; right: 0; width: 160px; background-color: #fafafa; height: 100%;
94
+ border-left: 1px solid #dfdfdf;
95
+ padding: 5px;
96
+ }
97
+ #users ul li { margin: 0; padding: 0; }
98
+ #users ul li.me { font-weight: bold; }
99
+ #users ul { list-style: none; margin 0; padding: 0; }
100
+ body { padding-right: 160px; }
31
101
  </style>
32
102
  </head>
33
103
  <body>
34
104
  <p>This is a trivial chat demo... It is implemented as a <a href="https://github.com/SamSaffron/message_bus/blob/master/examples/chat/chat.rb">Sinatra app</a>. The <a href="https://github.com/SamSaffron/message_bus">message_bus</a> can easily be added to any Rails/Rack app. <small>This app can be deployed with <a href="https://github.com/discourse/discourse_docker">Discourse Docker</a> using <a href="https://github.com/SamSaffron/message_bus/blob/master/examples/chat/docker_container/chat.yml">this template</a>.</small></p>
35
105
  <div id='messages' class="hidden"></div>
106
+ <div id='users' class="hidden">
107
+ <h3>Online</h3>
108
+ <ul></ul>
109
+ </div>
36
110
  <div id='panel' class="hidden">
37
111
  <form>
38
112
  <textarea cols=80 rows=2></textarea>
@@ -45,14 +119,67 @@ class Chat < Sinatra::Base
45
119
  $(function() {
46
120
  var name;
47
121
 
122
+ MessageBus.ajax = function(args){
123
+ args["headers"]["X-NAME"] = name;
124
+ return $.ajax(args);
125
+ };
126
+
127
+ var enter = function(name, opts) {
128
+ if (opts && opts.check) {
129
+ var found = false;
130
+ $('#users ul li').each(function(){
131
+ found = found || ($(this).text().trim() === name.trim());
132
+ if (found && opts.me) {
133
+ $(this).remove();
134
+ found = false;
135
+ }
136
+ return !found;
137
+ });
138
+ if (found) { return; }
139
+ }
140
+
141
+ var li = $('<li></li>');
142
+ li.text(name);
143
+ if (opts && opts.me) {
144
+ li.addClass("me");
145
+ $('#users ul').prepend(li);
146
+ } else {
147
+ $('#users ul').append(li);
148
+ }
149
+ };
150
+
48
151
  $('#messages, #panel').addClass('hidden');
49
152
 
50
153
  $('#your-name input').keyup(function(e){
51
154
  if(e.keyCode == 13) {
52
155
  name = $(this).val();
156
+ $.post("/enter", { name: name}, null, "json" ).success(function(data){
157
+ $.each(data.users,function(idx, name){
158
+ enter(name);
159
+ });
160
+ name = data.name;
161
+ enter(name, {check: true, me: true});
162
+ });
53
163
  $('#your-name').addClass('hidden');
54
- $('#messages, #panel').removeClass('hidden');
55
- $('#send').text("send (" + name + ")");
164
+ $('#messages, #panel, #users').removeClass('hidden');
165
+ $(document.body).scrollTop(document.body.scrollHeight);
166
+
167
+ window.onbeforeunload = function(){
168
+ $.post("/leave", { name: name });
169
+ };
170
+
171
+ MessageBus.subscribe("/presence", function(msg){
172
+ if (msg.enter) {
173
+ enter(msg.enter, {check: true});
174
+ }
175
+ if (msg.leave) {
176
+ $('#users ul li').each(function(){
177
+ if ($(this).text() === msg.leave) {
178
+ $(this).remove()
179
+ }
180
+ });
181
+ }
182
+ });
56
183
  }
57
184
  });
58
185
 
@@ -70,7 +197,13 @@ class Chat < Sinatra::Base
70
197
  if (val.length === 0) {
71
198
  return;
72
199
  }
73
- $.post("/message", { data: val, name: name} );
200
+
201
+ if (val.length > 500) {
202
+ alert("message too long");
203
+ return false;
204
+ } else {
205
+ $.post("/message", { data: val, name: name} );
206
+ }
74
207
  $('textarea').val("");
75
208
  };
76
209
 
@@ -31,6 +31,11 @@ hooks:
31
31
  - gem install unicorn
32
32
  - gem install redis
33
33
  - gem install sinatra
34
+ - gem install rbtrace
35
+ - replace:
36
+ - filename: $home/examples/chat/chat.rb
37
+ - from: "require 'json'"
38
+ - to: "require 'json'\nrequire 'rbtrace'"
34
39
  - file:
35
40
  path: /etc/service/unicorn/run
36
41
  chmod: "+x"
data/lib/message_bus.rb CHANGED
@@ -9,6 +9,7 @@ require "message_bus/diagnostics"
9
9
  require "message_bus/rack/middleware"
10
10
  require "message_bus/rack/diagnostics"
11
11
  require "message_bus/redis/reliable_pub_sub"
12
+ require "message_bus/timer_thread"
12
13
 
13
14
  # we still need to take care of the logger
14
15
  if defined?(::Rails)
@@ -297,11 +298,14 @@ module MessageBus::Implementation
297
298
  @destroyed = true
298
299
  end
299
300
  @subscriber_thread.join if @subscriber_thread
301
+ timer.stop
300
302
  end
301
303
 
302
304
  def after_fork
303
305
  reliable_pub_sub.after_fork
304
306
  ensure_subscriber_thread
307
+ # will ensure timer is running
308
+ timer.queue{}
305
309
  end
306
310
 
307
311
  def listening?
@@ -313,6 +317,17 @@ module MessageBus::Implementation
313
317
  reliable_pub_sub.reset!
314
318
  end
315
319
 
320
+ def timer
321
+ return @timer_thread if @timer_thread
322
+ @timer_thread ||= begin
323
+ t = MessageBus::TimerThread.new
324
+ t.on_error do |e|
325
+ logger.warn "Failed to process job: #{e} #{e.backtrace}"
326
+ end
327
+ t
328
+ end
329
+ end
330
+
316
331
  protected
317
332
 
318
333
  def global?(channel)
@@ -370,24 +385,51 @@ module MessageBus::Implementation
370
385
  end
371
386
  end
372
387
 
388
+ KEEPALIVE_INTERVAL = 60
389
+
373
390
  def new_subscriber_thread
374
- Thread.new do
391
+ thread = Thread.new do
375
392
  begin
376
393
  global_subscribe_thread unless @destroyed
377
394
  rescue => e
378
395
  MessageBus.logger.warn "Unexpected error in subscriber thread #{e}"
379
396
  end
380
397
  end
398
+ blk = proc do
399
+ if !@destroyed && thread.alive?
400
+ publish("/__mb_keepalive__/", Process.pid, user_ids: [-1])
401
+ if Time.now - (@last_message || Time.now) > KEEPALIVE_INTERVAL*2
402
+ MessageBus.logger.warn "Global messages on #{Process.pid} timed out, restarting thread"
403
+ # No other clean way to remove this thread, its listening on a socket
404
+ # no data is arriving
405
+ #
406
+ # In production we see this kind of situation ... sometimes ... when there is
407
+ # a VRRP failover, or weird networking condition
408
+ thread.kill
409
+ sleep 1
410
+ ensure_subscriber_thread
411
+ else
412
+ timer.queue(KEEPALIVE_INTERVAL, &blk)
413
+ end
414
+ end
415
+ end
416
+ timer.queue(KEEPALIVE_INTERVAL, &blk)
417
+
418
+ thread
381
419
  end
382
420
 
383
421
  def global_subscribe_thread
384
- reliable_pub_sub.global_subscribe do |msg|
422
+ # pretend we just got a message
423
+ @last_message = Time.now
424
+ reliable_pub_sub.global_subscribe(@global_id) do |msg|
385
425
  begin
426
+ @last_message = Time.now
386
427
  decode_message!(msg)
387
428
  globals, locals, local_globals, global_globals = nil
388
429
 
389
430
  @mutex.synchronize do
390
431
  raise MessageBus::BusDestroyed if @destroyed
432
+ @last_message_time = Time.now
391
433
  globals = @subscriptions[nil]
392
434
  locals = @subscriptions[msg.site_id] if msg.site_id
393
435
 
@@ -408,6 +450,7 @@ module MessageBus::Implementation
408
450
  rescue => e
409
451
  MessageBus.logger.warn "failed to process message #{msg.inspect}\n ex: #{e} backtrace: #{e.backtrace}"
410
452
  end
453
+ @global_id = msg.global_id
411
454
  end
412
455
  end
413
456
 
@@ -5,12 +5,6 @@ class MessageBus::Rack::Middleware
5
5
 
6
6
  def start_listener
7
7
  unless @started_listener
8
- require 'message_bus/timer_thread'
9
-
10
- @timer_thread = MessageBus::TimerThread.new
11
- @timer_thread.on_error do |e|
12
- @bus.logger.warn "Failed to process job: #{e} #{e.backtrace}"
13
- end
14
8
 
15
9
  thin = defined?(Thin::Server) && ObjectSpace.each_object(Thin::Server).to_a.first
16
10
  thin_running = thin && thin.running?
@@ -27,7 +21,7 @@ class MessageBus::Rack::Middleware
27
21
  if thin_running
28
22
  EM.next_tick(&run)
29
23
  else
30
- @timer_thread.queue(&run)
24
+ MessageBus.timer.queue(&run)
31
25
  end
32
26
 
33
27
  @started_listener = true
@@ -45,7 +39,6 @@ class MessageBus::Rack::Middleware
45
39
  def stop_listener
46
40
  if @subscription
47
41
  @bus.unsubscribe(&@subscription)
48
- @timer_thread.stop if @timer_thread
49
42
  @started_listener = false
50
43
  end
51
44
  end
@@ -165,7 +158,7 @@ class MessageBus::Rack::Middleware
165
158
  def add_client_with_timeout(client)
166
159
  @connection_manager.add_client(client)
167
160
 
168
- client.cleanup_timer = @timer_thread.queue( @bus.long_polling_interval.to_f / 1000) {
161
+ client.cleanup_timer = MessageBus.timer.queue( @bus.long_polling_interval.to_f / 1000) {
169
162
  begin
170
163
  client.cleanup_timer = nil
171
164
  client.ensure_closed!
@@ -13,6 +13,14 @@ class MessageBus::TimerThread
13
13
  end
14
14
  end
15
15
 
16
+ class CancelableEvery
17
+ attr_accessor :cancelled, :current
18
+ def cancel
19
+ current.cancel if current
20
+ @cancelled = true
21
+ end
22
+ end
23
+
16
24
  def initialize
17
25
  @stopped = false
18
26
  @jobs = []
@@ -24,6 +32,27 @@ class MessageBus::TimerThread
24
32
 
25
33
  def stop
26
34
  @stopped = true
35
+ running = true
36
+ while running
37
+ @mutex.synchronize do
38
+ running = @thread && @thread.alive?
39
+ @thread.wakeup if running
40
+ end
41
+ sleep 0
42
+ end
43
+ end
44
+
45
+ def every(delay, &block)
46
+ result = CancelableEvery.new
47
+ do_work = proc do
48
+ begin
49
+ block.call
50
+ ensure
51
+ result.current = queue(delay, &do_work)
52
+ end
53
+ end
54
+ result.current = queue(delay,&do_work)
55
+ result
27
56
  end
28
57
 
29
58
  # queue a block to run after a certain delay (in seconds)
@@ -1,3 +1,3 @@
1
1
  module MessageBus
2
- VERSION = "1.0.12"
2
+ VERSION = "1.0.13"
3
3
  end
@@ -52,6 +52,7 @@ describe MessageBus::Redis::ReliablePubSub do
52
52
  Process.wait(pid)
53
53
  end
54
54
  end
55
+ bus.global_unsubscribe
55
56
  end
56
57
  end
57
58
  end
@@ -10,6 +10,21 @@ describe MessageBus::TimerThread do
10
10
  @timer.stop
11
11
  end
12
12
 
13
+ it "allows you to queue every jobs" do
14
+ i = 0
15
+ m = Mutex.new
16
+ every = @timer.every(0.001){m.synchronize{i += 1}}
17
+ # allow lots of time, cause in test mode stuff can be slow
18
+ wait_for(1000) do
19
+ m.synchronize do
20
+ every.cancel if i == 3
21
+ i == 3
22
+ end
23
+ end
24
+ sleep 0.002
25
+ i.should == 3
26
+ end
27
+
13
28
  it "allows you to cancel timers" do
14
29
  success = true
15
30
  @timer.queue(0.005){success=false}.cancel
@@ -21,18 +36,27 @@ describe MessageBus::TimerThread do
21
36
  counter = 0
22
37
  failed = nil
23
38
 
24
- items = (0..5).to_a.shuffle
39
+ ready = 0
40
+
41
+ items = (0...4).to_a.shuffle
25
42
  items.map do |i|
26
43
  # threading introduces a delay meaning we need to wait a long time
27
44
  Thread.new do
28
- @timer.queue(i/500.0) do
45
+ ready += 1
46
+ while ready < 4
47
+ sleep 0
48
+ end
49
+ # on my mbp I measure at least 200ms of schedule jitter for Thread
50
+ @timer.queue(i/3.0) do
51
+ #puts "counter #{counter} i #{i}"
29
52
  failed = true if counter != i
30
53
  counter += 1
31
54
  end
55
+ #puts "\nqueued #{i/200.0} #{i} #{Time.now.to_f}\n"
32
56
  end
33
57
  end
34
58
 
35
- wait_for(1500) {
59
+ wait_for(3000) {
36
60
  counter == items.length
37
61
  }
38
62
 
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: message_bus
3
3
  version: !ruby/object:Gem::Version
4
- version: 1.0.12
4
+ version: 1.0.13
5
5
  platform: ruby
6
6
  authors:
7
7
  - Sam Saffron
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2015-05-28 00:00:00.000000000 Z
11
+ date: 2015-06-08 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: rack