message_bus 0.0.2 → 0.9.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.

Files changed (43) hide show
  1. checksums.yaml +4 -4
  2. data/.gitignore +18 -0
  3. data/.travis.yml +6 -0
  4. data/CHANGELOG +9 -0
  5. data/Gemfile +15 -0
  6. data/Guardfile +7 -0
  7. data/README.md +8 -0
  8. data/Rakefile +14 -0
  9. data/assets/application.handlebars +7 -0
  10. data/assets/application.js +79 -0
  11. data/assets/ember.js +26839 -0
  12. data/assets/handlebars.js +2201 -0
  13. data/assets/index.handlebars +25 -0
  14. data/assets/jquery-1.8.2.js +9440 -0
  15. data/assets/message-bus.js +247 -0
  16. data/examples/bench/ab.sample +1 -0
  17. data/examples/bench/config.ru +24 -0
  18. data/examples/bench/payload.post +1 -0
  19. data/examples/bench/unicorn.conf.rb +4 -0
  20. data/examples/chat/chat.rb +74 -0
  21. data/examples/chat/config.ru +2 -0
  22. data/lib/message_bus.rb +60 -5
  23. data/lib/message_bus/client.rb +45 -7
  24. data/lib/message_bus/connection_manager.rb +35 -7
  25. data/lib/message_bus/em_ext.rb +5 -0
  26. data/lib/message_bus/rack/middleware.rb +60 -89
  27. data/lib/message_bus/rack/thin_ext.rb +71 -0
  28. data/lib/message_bus/rails/railtie.rb +4 -1
  29. data/lib/message_bus/reliable_pub_sub.rb +22 -4
  30. data/lib/message_bus/version.rb +1 -1
  31. data/message_bus.gemspec +20 -0
  32. data/spec/lib/client_spec.rb +50 -0
  33. data/spec/lib/connection_manager_spec.rb +83 -0
  34. data/spec/lib/fake_async_middleware.rb +134 -0
  35. data/spec/lib/handlers/demo_message_handler.rb +5 -0
  36. data/spec/lib/message_bus_spec.rb +112 -0
  37. data/spec/lib/message_handler_spec.rb +39 -0
  38. data/spec/lib/middleware_spec.rb +306 -0
  39. data/spec/lib/multi_process_spec.rb +60 -0
  40. data/spec/lib/reliable_pub_sub_spec.rb +167 -0
  41. data/spec/spec_helper.rb +19 -0
  42. data/vendor/assets/javascripts/message-bus.js +247 -0
  43. metadata +55 -26
@@ -0,0 +1,247 @@
1
+ /*jshint bitwise: false*/
2
+
3
+ /**
4
+ Message Bus functionality.
5
+
6
+ @class MessageBus
7
+ @namespace Discourse
8
+ @module Discourse
9
+ **/
10
+ window.MessageBus = (function() {
11
+ // http://stackoverflow.com/questions/105034/how-to-create-a-guid-uuid-in-javascript
12
+ var callbacks, clientId, failCount, interval, shouldLongPoll, queue, responseCallbacks, uniqueId, baseUrl;
13
+ var me, started, stopped, longPoller, pollTimeout;
14
+
15
+ uniqueId = function() {
16
+ return 'xxxxxxxxxxxx4xxxyxxxxxxxxxxxxxxx'.replace(/[xy]/g, function(c) {
17
+ var r, v;
18
+ r = Math.random() * 16 | 0;
19
+ v = c === 'x' ? r : (r & 0x3 | 0x8);
20
+ return v.toString(16);
21
+ });
22
+ };
23
+
24
+ clientId = uniqueId();
25
+ responseCallbacks = {};
26
+ callbacks = [];
27
+ queue = [];
28
+ interval = null;
29
+ failCount = 0;
30
+ baseUrl = "/";
31
+
32
+ /* TODO: The plan is to force a long poll as soon as page becomes visible
33
+ // MIT based off https://github.com/mathiasbynens/jquery-visibility/blob/master/jquery-visibility.js
34
+ initVisibilityTracking = function(window, document, $, undefined) {
35
+ var prefix;
36
+ var property;
37
+ // In Opera, `'onfocusin' in document == true`, hence the extra `hasFocus` check to detect IE-like behavior
38
+ var eventName = 'onfocusin' in document && 'hasFocus' in document ? 'focusin focusout' : 'focus blur';
39
+ var prefixes = ['webkit', 'o', 'ms', 'moz', ''];
40
+ var $event = $.event;
41
+
42
+ while ((prefix = prefixes.pop()) !== undefined) {
43
+ property = (prefix ? prefix + 'H': 'h') + 'idden';
44
+ var supportsVisibility = typeof document[property] === 'boolean';
45
+ if (supportsVisibility) {
46
+ eventName = prefix + 'visibilitychange';
47
+ break;
48
+ }
49
+ }
50
+
51
+ $(/blur$/.test(eventName) ? window : document).on(eventName, function(event) {
52
+ var type = event.type;
53
+ var originalEvent = event.originalEvent;
54
+
55
+ // Avoid errors from triggered native events for which `originalEvent` is
56
+ // not available.
57
+ if (!originalEvent) {
58
+ return;
59
+ }
60
+
61
+ var toElement = originalEvent.toElement;
62
+
63
+ // If it’s a `{focusin,focusout}` event (IE), `fromElement` and `toElement`
64
+ // should both be `null` or `undefined`; else, the page visibility hasn’t
65
+ // changed, but the user just clicked somewhere in the doc. In IE9, we need
66
+ // to check the `relatedTarget` property instead.
67
+ if (
68
+ !/^focus./.test(type) || (
69
+ toElement === undefined &&
70
+ originalEvent.fromElement === undefined &&
71
+ originalEvent.relatedTarget === undefined
72
+ )
73
+ ) {
74
+ visibilityChanged(property && document[property] || /^(?:blur|focusout)$/.test(type) ? 'hide' : 'show');
75
+ }
76
+ });
77
+
78
+ };
79
+ */
80
+
81
+ var hiddenProperty;
82
+
83
+ $.each(["","webkit","ms","moz","ms"], function(index, prefix){
84
+ var check = prefix + (prefix === "" ? "hidden" : "Hidden");
85
+ if(document[check] !== undefined ){
86
+ hiddenProperty = check;
87
+ }
88
+ });
89
+
90
+ var isHidden = function() {
91
+ if (hiddenProperty !== undefined){
92
+ return document[hiddenProperty];
93
+ } else {
94
+ return !document.hasFocus;
95
+ }
96
+ };
97
+
98
+ shouldLongPoll = function() {
99
+ return me.alwaysLongPoll || !isHidden();
100
+ };
101
+
102
+ longPoller = function(poll,data){
103
+ var gotData = false;
104
+ var lastAjax = new Date();
105
+
106
+ return $.ajax(baseUrl + "message-bus/" + clientId + "/poll?" + (!shouldLongPoll() || !me.enableLongPolling ? "dlp=t" : ""), {
107
+ data: data,
108
+ cache: false,
109
+ dataType: 'json',
110
+ type: 'POST',
111
+ headers: {
112
+ 'X-SILENCE-LOGGER': 'true'
113
+ },
114
+ success: function(messages) {
115
+ failCount = 0;
116
+ $.each(messages,function(_,message) {
117
+ gotData = true;
118
+ $.each(callbacks, function(_,callback) {
119
+ if (callback.channel === message.channel) {
120
+ callback.last_id = message.message_id;
121
+ callback.func(message.data);
122
+ }
123
+ if (message.channel === "/__status") {
124
+ if (message.data[callback.channel] !== undefined) {
125
+ callback.last_id = message.data[callback.channel];
126
+ }
127
+ }
128
+ });
129
+ });
130
+ },
131
+ error: failCount += 1,
132
+ complete: function() {
133
+ if (gotData) {
134
+ pollTimeout = setTimeout(poll, 100);
135
+ } else {
136
+ interval = me.callbackInterval;
137
+ if (failCount > 2) {
138
+ interval = interval * failCount;
139
+ } else if (!shouldLongPoll()) {
140
+ // slowning down stuff a lot when hidden
141
+ // we will need to add a lot of fine tuning here
142
+ interval = interval * 4;
143
+ }
144
+ if (interval > me.maxPollInterval) {
145
+ interval = me.maxPollInterval;
146
+ }
147
+
148
+ interval -= (new Date() - lastAjax);
149
+ if (interval < 100) {
150
+ interval = 100;
151
+ }
152
+
153
+ pollTimeout = setTimeout(poll, interval);
154
+ }
155
+ me.longPoll = null;
156
+ }
157
+ });
158
+ };
159
+
160
+ me = {
161
+ enableLongPolling: true,
162
+ callbackInterval: 15000,
163
+ maxPollInterval: 3 * 60 * 1000,
164
+ callbacks: callbacks,
165
+ clientId: clientId,
166
+ alwaysLongPoll: false,
167
+ baseUrl: baseUrl,
168
+
169
+ stop: function() {
170
+ stopped = true;
171
+ started = false;
172
+ },
173
+
174
+ // Start polling
175
+ start: function(opts) {
176
+ var poll;
177
+
178
+ if (started) return;
179
+ started = true;
180
+ stopped = false;
181
+
182
+ if (!opts) opts = {};
183
+
184
+ poll = function() {
185
+ var data;
186
+
187
+ if(stopped) {
188
+ return;
189
+ }
190
+
191
+ if (callbacks.length === 0) {
192
+ setTimeout(poll, 500);
193
+ return;
194
+ }
195
+
196
+ data = {};
197
+ $.each(callbacks, function(_,callback) {
198
+ data[callback.channel] = callback.last_id;
199
+ });
200
+ me.longPoll = longPoller(poll,data);
201
+ };
202
+ poll();
203
+ },
204
+
205
+ // Subscribe to a channel
206
+ subscribe: function(channel, func, lastId) {
207
+
208
+ if(!started && !stopped){
209
+ me.start();
210
+ }
211
+
212
+ if (typeof(lastId) !== "number" || lastId < -1){
213
+ lastId = -1;
214
+ }
215
+ callbacks.push({
216
+ channel: channel,
217
+ func: func,
218
+ last_id: lastId
219
+ });
220
+ if (me.longPoll) {
221
+ return me.longPoll.abort();
222
+ }
223
+ },
224
+
225
+ // Unsubscribe from a channel
226
+ unsubscribe: function(channel) {
227
+ // TODO proper globbing
228
+ var glob;
229
+ if (channel.indexOf("*", channel.length - 1) !== -1) {
230
+ channel = channel.substr(0, channel.length - 1);
231
+ glob = true;
232
+ }
233
+ callbacks = $.grep(callbacks,function(callback) {
234
+ if (glob) {
235
+ return callback.channel.substr(0, channel.length) !== channel;
236
+ } else {
237
+ return callback.channel !== channel;
238
+ }
239
+ });
240
+ if (me.longPoll) {
241
+ return me.longPoll.abort();
242
+ }
243
+ }
244
+ };
245
+
246
+ return me;
247
+ })();
@@ -0,0 +1 @@
1
+ ab -n 1000 -c 25 -p payload.post -T "application/x-www-form-urlencoded" "http://localhost:3000/message-bus/poll"
@@ -0,0 +1,24 @@
1
+ require 'message_bus'
2
+
3
+ if defined?(PhusionPassenger)
4
+ PhusionPassenger.on_event(:starting_worker_process) do |forked|
5
+ if forked
6
+ # We're in smart spawning mode.
7
+ MessageBus.after_fork
8
+ else
9
+ # We're in conservative spawning mode. We don't need to do anything.
10
+ end
11
+ end
12
+ end
13
+
14
+
15
+ # require 'rack-mini-profiler'
16
+
17
+ # Rack::MiniProfiler.config.storage = Rack::MiniProfiler::MemoryStore
18
+
19
+ # use Rack::MiniProfiler
20
+ MessageBus.long_polling_interval = 1000 * 2
21
+ MessageBus.rack_hijack_enabled = true
22
+ MessageBus.max_active_clients = 3
23
+ use MessageBus::Rack::Middleware
24
+ run lambda { |env| [200, {"Content-Type" => "text/html"}, ["Howdy"]] }
@@ -0,0 +1 @@
1
+ foo=222&bob=1
@@ -0,0 +1,4 @@
1
+ require 'message_bus'
2
+ after_fork do |server, worker|
3
+ MessageBus.after_fork
4
+ end
@@ -0,0 +1,74 @@
1
+ $LOAD_PATH.unshift File.expand_path('../../../lib', __FILE__)
2
+ require 'message_bus'
3
+ require 'sinatra'
4
+ require 'sinatra/base'
5
+
6
+
7
+ class Chat < Sinatra::Base
8
+
9
+ set :public_folder, File.expand_path('../../../assets',__FILE__)
10
+
11
+ use MessageBus::Rack::Middleware
12
+
13
+ post '/message' do
14
+ MessageBus.publish '/message', params
15
+
16
+ "OK"
17
+ end
18
+
19
+ get '/' do
20
+
21
+ <<HTML
22
+
23
+ <html>
24
+ <head>
25
+ <script src="/jquery-1.8.2.js"></script>
26
+ <script src="/message-bus.js"></script>
27
+ </head>
28
+ <body>
29
+ <p>Chat Demo</p>
30
+ <div id='messages'></div>
31
+ <div id='panel'>
32
+ <form>
33
+ <textarea cols=80 rows=2></textarea>
34
+ </form>
35
+ </div>
36
+ <div id='your-name'>Enter your name: <input type='text'/>
37
+
38
+ <script>
39
+ $(function() {
40
+ var name;
41
+
42
+ $('#messages, #panel').hide();
43
+
44
+ $('#your-name input').keyup(function(e){
45
+ if(e.keyCode == 13) {
46
+ name = $(this).val();
47
+ $('#your-name').hide();
48
+ $('#messages, #panel').show();
49
+ }
50
+ });
51
+
52
+
53
+ MessageBus.subscribe("/message", function(msg){
54
+ $('#messages').append("<p>"+ msg.name + " said: " + msg.data + "</p>");
55
+ }, 0); // last id is zero, so getting backlog
56
+
57
+
58
+ $('textarea').keyup(function(e){
59
+ if(e.keyCode == 13) {
60
+ $.post("/message", { data: $('form textarea').val(), name: name} );
61
+ $('textarea').val("");
62
+ }
63
+ });
64
+
65
+ });
66
+ </script>
67
+ </body>
68
+ </html>
69
+
70
+ HTML
71
+ end
72
+
73
+ run! if app_file == $0
74
+ end
@@ -0,0 +1,2 @@
1
+ require './chat'
2
+ run Chat
data/lib/message_bus.rb CHANGED
@@ -51,6 +51,38 @@ module MessageBus::Implementation
51
51
  @long_polling_enabled = val
52
52
  end
53
53
 
54
+ # The number of simultanuous clients we can service
55
+ # will revert to polling if we are out of slots
56
+ def max_active_clients=(val)
57
+ @max_active_clients = val
58
+ end
59
+
60
+ def max_active_clients
61
+ @max_active_clients || 1000
62
+ end
63
+
64
+ def rack_hijack_enabled?
65
+ if @rack_hijack_enabled.nil?
66
+ @rack_hijack_enabled = true
67
+
68
+ # without this switch passenger will explode
69
+ # it will run out of connections after about 10
70
+ if defined? PhusionPassenger
71
+ @rack_hijack_enabled = false
72
+ if PhusionPassenger.respond_to? :advertised_concurrency_level
73
+ PhusionPassenger.advertised_concurrency_level = 0
74
+ @rack_hijack_enabled = true
75
+ end
76
+ end
77
+ end
78
+
79
+ @rack_hijack_enabled
80
+ end
81
+
82
+ def rack_hijack_enabled=(val)
83
+ @rack_hijack_enabled = val
84
+ end
85
+
54
86
  def long_polling_interval=(millisecs)
55
87
  @long_polling_interval = millisecs
56
88
  end
@@ -96,6 +128,18 @@ module MessageBus::Implementation
96
128
  @is_admin_lookup
97
129
  end
98
130
 
131
+ def client_filter(channel, &blk)
132
+ @client_filters ||= {}
133
+ @client_filters[channel] = blk if blk
134
+ @client_filters[channel]
135
+ end
136
+
137
+ def around_client_batch(channel, &blk)
138
+ @around_client_batches ||= {}
139
+ @around_client_batches[channel] = blk if blk
140
+ @around_client_batches[channel]
141
+ end
142
+
99
143
  def on_connect(&blk)
100
144
  @on_connect = blk if blk
101
145
  @on_connect
@@ -158,9 +202,9 @@ module MessageBus::Implementation
158
202
 
159
203
  # encode channel name to include site
160
204
  def encode_channel_name(channel)
161
- if MessageBus.site_id_lookup
205
+ if site_id_lookup
162
206
  raise ArgumentError.new channel if channel.include? ENCODE_SITE_TOKEN
163
- "#{channel}#{ENCODE_SITE_TOKEN}#{MessageBus.site_id_lookup.call}"
207
+ "#{channel}#{ENCODE_SITE_TOKEN}#{site_id_lookup.call}"
164
208
  else
165
209
  channel
166
210
  end
@@ -179,13 +223,13 @@ module MessageBus::Implementation
179
223
  end
180
224
 
181
225
  def local_unsubscribe(channel=nil, &blk)
182
- site_id = MessageBus.site_id_lookup.call if MessageBus.site_id_lookup
226
+ site_id = site_id_lookup.call if site_id_lookup
183
227
  unsubscribe_impl(channel, site_id, &blk)
184
228
  end
185
229
 
186
230
  # subscribe only on current site
187
231
  def local_subscribe(channel=nil, &blk)
188
- site_id = MessageBus.site_id_lookup.call if MessageBus.site_id_lookup
232
+ site_id = site_id_lookup.call if site_id_lookup
189
233
  subscribe_impl(channel, site_id, &blk)
190
234
  end
191
235
 
@@ -208,6 +252,16 @@ module MessageBus::Implementation
208
252
  reliable_pub_sub.last_id(encode_channel_name(channel))
209
253
  end
210
254
 
255
+
256
+ def destroy
257
+ reliable_pub_sub.global_unsubscribe
258
+ end
259
+
260
+ def after_fork
261
+ reliable_pub_sub.after_fork
262
+ ensure_subscriber_thread
263
+ end
264
+
211
265
  protected
212
266
 
213
267
  def decode_message!(msg)
@@ -239,10 +293,11 @@ module MessageBus::Implementation
239
293
  end
240
294
  end
241
295
 
296
+
242
297
  def ensure_subscriber_thread
243
298
  @mutex ||= Mutex.new
244
299
  @mutex.synchronize do
245
- return if @subscriber_thread
300
+ return if @subscriber_thread && @subscriber_thread.alive?
246
301
  @subscriber_thread = Thread.new do
247
302
  reliable_pub_sub.global_subscribe do |msg|
248
303
  begin