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

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