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.
- checksums.yaml +4 -4
- data/.gitignore +18 -0
- data/.travis.yml +6 -0
- data/CHANGELOG +9 -0
- data/Gemfile +15 -0
- data/Guardfile +7 -0
- data/README.md +8 -0
- data/Rakefile +14 -0
- data/assets/application.handlebars +7 -0
- data/assets/application.js +79 -0
- data/assets/ember.js +26839 -0
- data/assets/handlebars.js +2201 -0
- data/assets/index.handlebars +25 -0
- data/assets/jquery-1.8.2.js +9440 -0
- data/assets/message-bus.js +247 -0
- data/examples/bench/ab.sample +1 -0
- data/examples/bench/config.ru +24 -0
- data/examples/bench/payload.post +1 -0
- data/examples/bench/unicorn.conf.rb +4 -0
- data/examples/chat/chat.rb +74 -0
- data/examples/chat/config.ru +2 -0
- data/lib/message_bus.rb +60 -5
- data/lib/message_bus/client.rb +45 -7
- data/lib/message_bus/connection_manager.rb +35 -7
- data/lib/message_bus/em_ext.rb +5 -0
- data/lib/message_bus/rack/middleware.rb +60 -89
- data/lib/message_bus/rack/thin_ext.rb +71 -0
- data/lib/message_bus/rails/railtie.rb +4 -1
- data/lib/message_bus/reliable_pub_sub.rb +22 -4
- data/lib/message_bus/version.rb +1 -1
- data/message_bus.gemspec +20 -0
- data/spec/lib/client_spec.rb +50 -0
- data/spec/lib/connection_manager_spec.rb +83 -0
- data/spec/lib/fake_async_middleware.rb +134 -0
- data/spec/lib/handlers/demo_message_handler.rb +5 -0
- data/spec/lib/message_bus_spec.rb +112 -0
- data/spec/lib/message_handler_spec.rb +39 -0
- data/spec/lib/middleware_spec.rb +306 -0
- data/spec/lib/multi_process_spec.rb +60 -0
- data/spec/lib/reliable_pub_sub_spec.rb +167 -0
- data/spec/spec_helper.rb +19 -0
- data/vendor/assets/javascripts/message-bus.js +247 -0
- 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,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
|
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
|
205
|
+
if site_id_lookup
|
162
206
|
raise ArgumentError.new channel if channel.include? ENCODE_SITE_TOKEN
|
163
|
-
"#{channel}#{ENCODE_SITE_TOKEN}#{
|
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 =
|
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 =
|
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
|