message_bus 1.0.12 → 1.0.13

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.

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