message_bus 0.0.1
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 +7 -0
- data/LICENSE +22 -0
- data/README.md +64 -0
- data/lib/message_bus/client.rb +81 -0
- data/lib/message_bus/connection_manager.rb +66 -0
- data/lib/message_bus/diagnostics.rb +53 -0
- data/lib/message_bus/message.rb +17 -0
- data/lib/message_bus/message_handler.rb +26 -0
- data/lib/message_bus/rack/diagnostics.rb +98 -0
- data/lib/message_bus/rack/middleware.rb +171 -0
- data/lib/message_bus/rails/railtie.rb +9 -0
- data/lib/message_bus/reliable_pub_sub.rb +262 -0
- data/lib/message_bus/version.rb +3 -0
- data/lib/message_bus.rb +272 -0
- metadata +112 -0
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA1:
|
3
|
+
metadata.gz: 0b1d52c269db2dcec90068d0ceb828b969666a64
|
4
|
+
data.tar.gz: e2e46e33612cde225c94c17367599b63f3c824f4
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: 523d379b630f2c3d23db73835bde99b5fda6003f448ce7d9f3d66c169bbd18775d251aad51dc269bfe987e62279d49475712f5bab32db1baaf2c404fa6110a5c
|
7
|
+
data.tar.gz: 42aa9744353b2a70fb7dfe6422a30eaaa721deb161695f0ea084cec809acde201af451cc253f801d3623257ad7f700fb8921d482d2d61fe21909542ef8e4074a
|
data/LICENSE
ADDED
@@ -0,0 +1,22 @@
|
|
1
|
+
Copyright (c) 2012 Sam Saffron
|
2
|
+
|
3
|
+
MIT License
|
4
|
+
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining
|
6
|
+
a copy of this software and associated documentation files (the
|
7
|
+
"Software"), to deal in the Software without restriction, including
|
8
|
+
without limitation the rights to use, copy, modify, merge, publish,
|
9
|
+
distribute, sublicense, and/or sell copies of the Software, and to
|
10
|
+
permit persons to whom the Software is furnished to do so, subject to
|
11
|
+
the following conditions:
|
12
|
+
|
13
|
+
The above copyright notice and this permission notice shall be
|
14
|
+
included in all copies or substantial portions of the Software.
|
15
|
+
|
16
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
17
|
+
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
18
|
+
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
19
|
+
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
|
20
|
+
LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
|
21
|
+
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
22
|
+
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
data/README.md
ADDED
@@ -0,0 +1,64 @@
|
|
1
|
+
# MessageBus
|
2
|
+
|
3
|
+
A reliable, robust messaging bus for Ruby processes and web clients built on Redis.
|
4
|
+
|
5
|
+
|
6
|
+
## Installation
|
7
|
+
|
8
|
+
Add this line to your application's Gemfile:
|
9
|
+
|
10
|
+
gem 'message_bus'
|
11
|
+
|
12
|
+
And then execute:
|
13
|
+
|
14
|
+
$ bundle
|
15
|
+
|
16
|
+
Or install it yourself as:
|
17
|
+
|
18
|
+
$ gem install message_bus
|
19
|
+
|
20
|
+
## Usage
|
21
|
+
|
22
|
+
Server to Server messaging
|
23
|
+
|
24
|
+
```ruby
|
25
|
+
message_id = MessageBus.publish "/channel", "message"
|
26
|
+
|
27
|
+
# in another process / spot
|
28
|
+
|
29
|
+
MessageBus.subscribe "/channel" do |msg|
|
30
|
+
# block called in a backgroud thread when message is recieved
|
31
|
+
end
|
32
|
+
|
33
|
+
MessageBus.backlog "/channel", id
|
34
|
+
# returns all messages after the id
|
35
|
+
|
36
|
+
|
37
|
+
# messages can be targetted at particular users or groups
|
38
|
+
MessageBus.publish "/channel", user_ids: [1,2,3], group_ids: [4,5,6]
|
39
|
+
|
40
|
+
# message bus determines the user ids and groups based on env
|
41
|
+
|
42
|
+
MessageBus.user_id_lookup do |env|
|
43
|
+
# return the user id here
|
44
|
+
end
|
45
|
+
|
46
|
+
MessageBus.group_ids_lookup do |env|
|
47
|
+
# return the group ids the user belongs to
|
48
|
+
# can be nil or []
|
49
|
+
end
|
50
|
+
|
51
|
+
```
|
52
|
+
|
53
|
+
|
54
|
+
## Similar projects
|
55
|
+
|
56
|
+
Faye - http://faye.jcoglan.com/
|
57
|
+
|
58
|
+
## Contributing
|
59
|
+
|
60
|
+
1. Fork it
|
61
|
+
2. Create your feature branch (`git checkout -b my-new-feature`)
|
62
|
+
3. Commit your changes (`git commit -am 'Added some feature'`)
|
63
|
+
4. Push to the branch (`git push origin my-new-feature`)
|
64
|
+
5. Create new Pull Request
|
@@ -0,0 +1,81 @@
|
|
1
|
+
class MessageBus::Client
|
2
|
+
attr_accessor :client_id, :user_id, :group_ids, :connect_time, :subscribed_sets, :site_id, :cleanup_timer, :async_response
|
3
|
+
def initialize(opts)
|
4
|
+
self.client_id = opts[:client_id]
|
5
|
+
self.user_id = opts[:user_id]
|
6
|
+
self.group_ids = opts[:group_ids] || []
|
7
|
+
self.site_id = opts[:site_id]
|
8
|
+
self.connect_time = Time.now
|
9
|
+
@subscriptions = {}
|
10
|
+
end
|
11
|
+
|
12
|
+
def close
|
13
|
+
return unless @async_response
|
14
|
+
write_and_close "[]"
|
15
|
+
end
|
16
|
+
|
17
|
+
def closed
|
18
|
+
!@async_response
|
19
|
+
end
|
20
|
+
|
21
|
+
def subscribe(channel, last_seen_id)
|
22
|
+
last_seen_id ||= MessageBus.last_id(channel)
|
23
|
+
@subscriptions[channel] = last_seen_id
|
24
|
+
end
|
25
|
+
|
26
|
+
def subscriptions
|
27
|
+
@subscriptions
|
28
|
+
end
|
29
|
+
|
30
|
+
def <<(msg)
|
31
|
+
write_and_close messages_to_json([msg])
|
32
|
+
end
|
33
|
+
|
34
|
+
def subscriptions
|
35
|
+
@subscriptions
|
36
|
+
end
|
37
|
+
|
38
|
+
def allowed?(msg)
|
39
|
+
allowed = !msg.user_ids || msg.user_ids.include?(self.user_id)
|
40
|
+
allowed && (
|
41
|
+
msg.group_ids.nil? ||
|
42
|
+
msg.group_ids.length == 0 ||
|
43
|
+
(
|
44
|
+
msg.group_ids - self.group_ids
|
45
|
+
).length < msg.group_ids.length
|
46
|
+
)
|
47
|
+
end
|
48
|
+
|
49
|
+
def backlog
|
50
|
+
r = []
|
51
|
+
@subscriptions.each do |k,v|
|
52
|
+
next if v.to_i < 0
|
53
|
+
messages = MessageBus.backlog(k,v)
|
54
|
+
messages.each do |msg|
|
55
|
+
r << msg if allowed?(msg)
|
56
|
+
end
|
57
|
+
end
|
58
|
+
# stats message for all newly subscribed
|
59
|
+
status_message = nil
|
60
|
+
@subscriptions.each do |k,v|
|
61
|
+
if v.to_i == -1
|
62
|
+
status_message ||= {}
|
63
|
+
status_message[k] = MessageBus.last_id(k)
|
64
|
+
end
|
65
|
+
end
|
66
|
+
r << MessageBus::Message.new(-1, -1, '/__status', status_message) if status_message
|
67
|
+
r
|
68
|
+
end
|
69
|
+
|
70
|
+
protected
|
71
|
+
|
72
|
+
def write_and_close(data)
|
73
|
+
@async_response << data
|
74
|
+
@async_response.done
|
75
|
+
@async_response = nil
|
76
|
+
end
|
77
|
+
|
78
|
+
def messages_to_json(msgs)
|
79
|
+
MessageBus::Rack::Middleware.backlog_to_json(msgs)
|
80
|
+
end
|
81
|
+
end
|
@@ -0,0 +1,66 @@
|
|
1
|
+
require 'json' unless defined? ::JSON
|
2
|
+
|
3
|
+
class MessageBus::ConnectionManager
|
4
|
+
|
5
|
+
def initialize
|
6
|
+
@clients = {}
|
7
|
+
@subscriptions = {}
|
8
|
+
end
|
9
|
+
|
10
|
+
def notify_clients(msg)
|
11
|
+
begin
|
12
|
+
site_subs = @subscriptions[msg.site_id]
|
13
|
+
subscription = site_subs[msg.channel] if site_subs
|
14
|
+
|
15
|
+
return unless subscription
|
16
|
+
|
17
|
+
subscription.each do |client_id|
|
18
|
+
client = @clients[client_id]
|
19
|
+
if client && client.allowed?(msg)
|
20
|
+
client << msg
|
21
|
+
# turns out you can delete from a set while itereating
|
22
|
+
remove_client(client)
|
23
|
+
end
|
24
|
+
end
|
25
|
+
rescue => e
|
26
|
+
MessageBus.logger.error "notify clients crash #{e} : #{e.backtrace}"
|
27
|
+
end
|
28
|
+
end
|
29
|
+
|
30
|
+
def add_client(client)
|
31
|
+
@clients[client.client_id] = client
|
32
|
+
@subscriptions[client.site_id] ||= {}
|
33
|
+
client.subscriptions.each do |k,v|
|
34
|
+
subscribe_client(client, k)
|
35
|
+
end
|
36
|
+
end
|
37
|
+
|
38
|
+
def remove_client(c)
|
39
|
+
@clients.delete c.client_id
|
40
|
+
@subscriptions[c.site_id].each do |k, set|
|
41
|
+
set.delete c.client_id
|
42
|
+
end
|
43
|
+
c.cleanup_timer.cancel
|
44
|
+
end
|
45
|
+
|
46
|
+
def lookup_client(client_id)
|
47
|
+
@clients[client_id]
|
48
|
+
end
|
49
|
+
|
50
|
+
def subscribe_client(client,channel)
|
51
|
+
set = @subscriptions[client.site_id][channel]
|
52
|
+
unless set
|
53
|
+
set = Set.new
|
54
|
+
@subscriptions[client.site_id][channel] = set
|
55
|
+
end
|
56
|
+
set << client.client_id
|
57
|
+
end
|
58
|
+
|
59
|
+
def stats
|
60
|
+
{
|
61
|
+
client_count: @clients.length,
|
62
|
+
subscriptions: @subscriptions
|
63
|
+
}
|
64
|
+
end
|
65
|
+
|
66
|
+
end
|
@@ -0,0 +1,53 @@
|
|
1
|
+
class MessageBus::Diagnostics
|
2
|
+
def self.full_process_path
|
3
|
+
begin
|
4
|
+
system = `uname`.strip
|
5
|
+
if system == "Darwin"
|
6
|
+
`ps -o "comm=" -p #{Process.pid}`
|
7
|
+
elsif system == "FreeBSD"
|
8
|
+
`ps -o command -p #{Process.pid}`.split("\n",2)[1].strip()
|
9
|
+
else
|
10
|
+
info = `ps -eo "%p|$|%a" | grep '^\\s*#{Process.pid}'`
|
11
|
+
info.strip.split('|$|')[1]
|
12
|
+
end
|
13
|
+
rescue
|
14
|
+
# skip it ... not linux or something weird
|
15
|
+
end
|
16
|
+
end
|
17
|
+
|
18
|
+
def self.hostname
|
19
|
+
begin
|
20
|
+
`hostname`.strip
|
21
|
+
rescue
|
22
|
+
# skip it
|
23
|
+
end
|
24
|
+
end
|
25
|
+
|
26
|
+
def self.enable
|
27
|
+
full_path = full_process_path
|
28
|
+
start_time = Time.now.to_f
|
29
|
+
hostname = self.hostname
|
30
|
+
|
31
|
+
# it may make sense to add a channel per machine/host to streamline
|
32
|
+
# process to process comms
|
33
|
+
MessageBus.subscribe('/_diagnostics/hup') do |msg|
|
34
|
+
if Process.pid == msg.data["pid"] && hostname == msg.data["hostname"]
|
35
|
+
$shutdown = true
|
36
|
+
sleep 4
|
37
|
+
Process.kill("HUP", $$)
|
38
|
+
end
|
39
|
+
end
|
40
|
+
|
41
|
+
MessageBus.subscribe('/_diagnostics/discover') do |msg|
|
42
|
+
MessageBus.on_connect.call msg.site_id if MessageBus.on_connect
|
43
|
+
MessageBus.publish '/_diagnostics/process-discovery', {
|
44
|
+
pid: Process.pid,
|
45
|
+
process_name: $0,
|
46
|
+
full_path: full_path,
|
47
|
+
uptime: (Time.now.to_f - start_time).to_i,
|
48
|
+
hostname: hostname
|
49
|
+
}, user_ids: [msg.data["user_id"]]
|
50
|
+
MessageBus.on_disconnect.call msg.site_id if MessageBus.on_disconnect
|
51
|
+
end
|
52
|
+
end
|
53
|
+
end
|
@@ -0,0 +1,17 @@
|
|
1
|
+
class MessageBus::Message < Struct.new(:global_id, :message_id, :channel , :data)
|
2
|
+
|
3
|
+
attr_accessor :site_id, :user_ids, :group_ids
|
4
|
+
|
5
|
+
def self.decode(encoded)
|
6
|
+
s1 = encoded.index("|")
|
7
|
+
s2 = encoded.index("|", s1+1)
|
8
|
+
s3 = encoded.index("|", s2+1)
|
9
|
+
|
10
|
+
MessageBus::Message.new encoded[0..s1].to_i, encoded[s1+1..s2].to_i, encoded[s2+1..s3-1].gsub("$$123$$", "|"), encoded[s3+1..-1]
|
11
|
+
end
|
12
|
+
|
13
|
+
# only tricky thing to encode is pipes in a channel name ... do a straight replace
|
14
|
+
def encode
|
15
|
+
global_id.to_s << "|" << message_id.to_s << "|" << channel.gsub("|","$$123$$") << "|" << data
|
16
|
+
end
|
17
|
+
end
|
@@ -0,0 +1,26 @@
|
|
1
|
+
class MessageBus::MessageHandler
|
2
|
+
def self.load_handlers(path)
|
3
|
+
Dir.glob("#{path}/*.rb").each do |f|
|
4
|
+
load "#{f}"
|
5
|
+
end
|
6
|
+
end
|
7
|
+
|
8
|
+
def self.handle(name,&blk)
|
9
|
+
raise ArgumentError.new("expecting block") unless block_given?
|
10
|
+
raise ArgumentError.new("name") unless name
|
11
|
+
|
12
|
+
@@handlers ||= {}
|
13
|
+
@@handlers[name] = blk
|
14
|
+
end
|
15
|
+
|
16
|
+
def self.call(site_id, name, data, current_user_id)
|
17
|
+
begin
|
18
|
+
MessageBus.on_connect.call(site_id) if MessageBus.on_connect
|
19
|
+
@@handlers[name].call(data,current_user_id)
|
20
|
+
ensure
|
21
|
+
MessageBus.on_disconnect.call(site_id) if MessageBus.on_disconnect
|
22
|
+
end
|
23
|
+
end
|
24
|
+
|
25
|
+
|
26
|
+
end
|
@@ -0,0 +1,98 @@
|
|
1
|
+
module MessageBus::Rack; end
|
2
|
+
|
3
|
+
class MessageBus::Rack::Diagnostics
|
4
|
+
def initialize(app, config = {})
|
5
|
+
@app = app
|
6
|
+
end
|
7
|
+
|
8
|
+
def js_asset(name)
|
9
|
+
return generate_script_tag(name) unless MessageBus.cache_assets
|
10
|
+
@@asset_cache ||= {}
|
11
|
+
@@asset_cache[name] ||= generate_script_tag(name)
|
12
|
+
@@asset_cache[name]
|
13
|
+
end
|
14
|
+
|
15
|
+
def generate_script_tag(name)
|
16
|
+
"<script src='/message-bus/_diagnostics/assets/#{name}?#{file_hash(name)}' type='text/javascript'></script>"
|
17
|
+
end
|
18
|
+
|
19
|
+
def file_hash(asset)
|
20
|
+
require 'digest/sha1'
|
21
|
+
Digest::SHA1.hexdigest(asset_contents(asset))
|
22
|
+
end
|
23
|
+
|
24
|
+
def asset_contents(asset)
|
25
|
+
File.open(asset_path(asset)).read
|
26
|
+
end
|
27
|
+
|
28
|
+
def asset_path(asset)
|
29
|
+
File.expand_path("../../../../assets/#{asset}", __FILE__)
|
30
|
+
end
|
31
|
+
|
32
|
+
def index
|
33
|
+
html = <<HTML
|
34
|
+
<!DOCTYPE html>
|
35
|
+
<html>
|
36
|
+
<head>
|
37
|
+
</head>
|
38
|
+
<body>
|
39
|
+
<div id="app"></div>
|
40
|
+
#{js_asset "jquery-1.8.2.js"}
|
41
|
+
#{js_asset "handlebars.js"}
|
42
|
+
#{js_asset "ember.js"}
|
43
|
+
#{js_asset "message-bus.js"}
|
44
|
+
#{js_asset "application.handlebars"}
|
45
|
+
#{js_asset "index.handlebars"}
|
46
|
+
#{js_asset "application.js"}
|
47
|
+
</body>
|
48
|
+
</html>
|
49
|
+
HTML
|
50
|
+
return [200, {"content-type" => "text/html;"}, [html]]
|
51
|
+
end
|
52
|
+
|
53
|
+
def translate_handlebars(name, content)
|
54
|
+
"Ember.TEMPLATES['#{name}'] = Ember.Handlebars.compile(#{indent(content).inspect});"
|
55
|
+
end
|
56
|
+
|
57
|
+
# from ember-rails
|
58
|
+
def indent(string)
|
59
|
+
string.gsub(/$(.)/m, "\\1 ").strip
|
60
|
+
end
|
61
|
+
|
62
|
+
def call(env)
|
63
|
+
|
64
|
+
return @app.call(env) unless env['PATH_INFO'].start_with? '/message-bus/_diagnostics'
|
65
|
+
|
66
|
+
route = env['PATH_INFO'].split('/message-bus/_diagnostics')[1]
|
67
|
+
|
68
|
+
if MessageBus.is_admin_lookup.nil? || !MessageBus.is_admin_lookup.call(env)
|
69
|
+
return [403, {}, ['not allowed']]
|
70
|
+
end
|
71
|
+
|
72
|
+
return index unless route
|
73
|
+
|
74
|
+
if route == '/discover'
|
75
|
+
user_id = MessageBus.user_id_lookup.call(env)
|
76
|
+
MessageBus.publish('/_diagnostics/discover', user_id: user_id)
|
77
|
+
return [200, {}, ['ok']]
|
78
|
+
end
|
79
|
+
|
80
|
+
if route =~ /^\/hup\//
|
81
|
+
hostname, pid = route.split('/hup/')[1].split('/')
|
82
|
+
MessageBus.publish('/_diagnostics/hup', {hostname: hostname, pid: pid.to_i})
|
83
|
+
return [200, {}, ['ok']]
|
84
|
+
end
|
85
|
+
|
86
|
+
asset = route.split('/assets/')[1]
|
87
|
+
if asset && !asset !~ /\//
|
88
|
+
content = asset_contents(asset)
|
89
|
+
split = asset.split('.')
|
90
|
+
if split[1] == 'handlebars'
|
91
|
+
content = translate_handlebars(split[0],content)
|
92
|
+
end
|
93
|
+
return [200, {'content-type' => 'text/javascript;'}, [content]]
|
94
|
+
end
|
95
|
+
|
96
|
+
return [404, {}, ['not found']]
|
97
|
+
end
|
98
|
+
end
|
@@ -0,0 +1,171 @@
|
|
1
|
+
# our little message bus, accepts long polling and web sockets
|
2
|
+
require 'thin'
|
3
|
+
require 'eventmachine'
|
4
|
+
|
5
|
+
module MessageBus::Rack; end
|
6
|
+
|
7
|
+
class MessageBus::Rack::Middleware
|
8
|
+
|
9
|
+
def self.start_listener
|
10
|
+
unless @started_listener
|
11
|
+
MessageBus.subscribe do |msg|
|
12
|
+
if EM.reactor_running?
|
13
|
+
EM.next_tick do
|
14
|
+
@@connection_manager.notify_clients(msg) if @@connection_manager
|
15
|
+
end
|
16
|
+
end
|
17
|
+
end
|
18
|
+
@started_listener = true
|
19
|
+
end
|
20
|
+
end
|
21
|
+
|
22
|
+
def initialize(app, config = {})
|
23
|
+
@app = app
|
24
|
+
@@connection_manager = MessageBus::ConnectionManager.new
|
25
|
+
self.class.start_listener
|
26
|
+
end
|
27
|
+
|
28
|
+
def self.backlog_to_json(backlog)
|
29
|
+
m = backlog.map do |msg|
|
30
|
+
{
|
31
|
+
:global_id => msg.global_id,
|
32
|
+
:message_id => msg.message_id,
|
33
|
+
:channel => msg.channel,
|
34
|
+
:data => msg.data
|
35
|
+
}
|
36
|
+
end.to_a
|
37
|
+
JSON.dump(m)
|
38
|
+
end
|
39
|
+
|
40
|
+
def call(env)
|
41
|
+
|
42
|
+
return @app.call(env) unless env['PATH_INFO'] =~ /^\/message-bus/
|
43
|
+
|
44
|
+
# special debug/test route
|
45
|
+
if ::MessageBus.allow_broadcast? && env['PATH_INFO'] == '/message-bus/broadcast'
|
46
|
+
parsed = Rack::Request.new(env)
|
47
|
+
::MessageBus.publish parsed["channel"], parsed["data"]
|
48
|
+
return [200,{"Content-Type" => "text/html"},["sent"]]
|
49
|
+
end
|
50
|
+
|
51
|
+
if env['PATH_INFO'].start_with? '/message-bus/_diagnostics'
|
52
|
+
diags = MessageBus::Rack::Diagnostics.new(@app)
|
53
|
+
return diags.call(env)
|
54
|
+
end
|
55
|
+
|
56
|
+
client_id = env['PATH_INFO'].split("/")[2]
|
57
|
+
return [404, {}, ["not found"]] unless client_id
|
58
|
+
|
59
|
+
user_id = MessageBus.user_id_lookup.call(env) if MessageBus.user_id_lookup
|
60
|
+
group_ids = MessageBus.group_ids_lookup.call(env) if MessageBus.group_ids_lookup
|
61
|
+
site_id = MessageBus.site_id_lookup.call(env) if MessageBus.site_id_lookup
|
62
|
+
|
63
|
+
client = MessageBus::Client.new(client_id: client_id, user_id: user_id, site_id: site_id, group_ids: group_ids)
|
64
|
+
|
65
|
+
connection = env['em.connection']
|
66
|
+
|
67
|
+
request = Rack::Request.new(env)
|
68
|
+
request.POST.each do |k,v|
|
69
|
+
client.subscribe(k, v)
|
70
|
+
end
|
71
|
+
|
72
|
+
backlog = client.backlog
|
73
|
+
headers = {}
|
74
|
+
headers["Cache-Control"] = "must-revalidate, private, max-age=0"
|
75
|
+
headers["Content-Type"] ="application/json; charset=utf-8"
|
76
|
+
|
77
|
+
if backlog.length > 0
|
78
|
+
[200, headers, [self.class.backlog_to_json(backlog)] ]
|
79
|
+
elsif MessageBus.long_polling_enabled? && env['QUERY_STRING'] !~ /dlp=t/ && EM.reactor_running?
|
80
|
+
response = Thin::AsyncResponse.new(env)
|
81
|
+
response.headers["Cache-Control"] = "must-revalidate, private, max-age=0"
|
82
|
+
response.headers["Content-Type"] ="application/json; charset=utf-8"
|
83
|
+
response.status = 200
|
84
|
+
client.async_response = response
|
85
|
+
|
86
|
+
@@connection_manager.add_client(client)
|
87
|
+
|
88
|
+
client.cleanup_timer = ::EM::Timer.new(MessageBus.long_polling_interval.to_f / 1000) {
|
89
|
+
client.close
|
90
|
+
@@connection_manager.remove_client(client)
|
91
|
+
}
|
92
|
+
|
93
|
+
throw :async
|
94
|
+
else
|
95
|
+
[200, headers, ["[]"]]
|
96
|
+
end
|
97
|
+
|
98
|
+
end
|
99
|
+
end
|
100
|
+
|
101
|
+
# there is also another in cramp this is from https://github.com/macournoyer/thin_async/blob/master/lib/thin/async.rb
|
102
|
+
module Thin
|
103
|
+
unless defined?(DeferrableBody)
|
104
|
+
# Based on version from James Tucker <raggi@rubyforge.org>
|
105
|
+
class DeferrableBody
|
106
|
+
include ::EM::Deferrable
|
107
|
+
|
108
|
+
def initialize
|
109
|
+
@queue = []
|
110
|
+
end
|
111
|
+
|
112
|
+
def call(body)
|
113
|
+
@queue << body
|
114
|
+
schedule_dequeue
|
115
|
+
end
|
116
|
+
|
117
|
+
def each(&blk)
|
118
|
+
@body_callback = blk
|
119
|
+
schedule_dequeue
|
120
|
+
end
|
121
|
+
|
122
|
+
private
|
123
|
+
def schedule_dequeue
|
124
|
+
return unless @body_callback
|
125
|
+
::EM.next_tick do
|
126
|
+
next unless body = @queue.shift
|
127
|
+
body.each do |chunk|
|
128
|
+
@body_callback.call(chunk)
|
129
|
+
end
|
130
|
+
schedule_dequeue unless @queue.empty?
|
131
|
+
end
|
132
|
+
end
|
133
|
+
end
|
134
|
+
end
|
135
|
+
|
136
|
+
# Response whos body is sent asynchronously.
|
137
|
+
class AsyncResponse
|
138
|
+
include Rack::Response::Helpers
|
139
|
+
|
140
|
+
attr_reader :headers, :callback, :closed
|
141
|
+
attr_accessor :status
|
142
|
+
|
143
|
+
def initialize(env, status=200, headers={})
|
144
|
+
@callback = env['async.callback']
|
145
|
+
@body = DeferrableBody.new
|
146
|
+
@status = status
|
147
|
+
@headers = headers
|
148
|
+
@headers_sent = false
|
149
|
+
end
|
150
|
+
|
151
|
+
def send_headers
|
152
|
+
return if @headers_sent
|
153
|
+
@callback.call [@status, @headers, @body]
|
154
|
+
@headers_sent = true
|
155
|
+
end
|
156
|
+
|
157
|
+
def write(body)
|
158
|
+
send_headers
|
159
|
+
@body.call(body.respond_to?(:each) ? body : [body])
|
160
|
+
end
|
161
|
+
alias :<< :write
|
162
|
+
|
163
|
+
# Tell Thin the response is complete and the connection can be closed.
|
164
|
+
def done
|
165
|
+
@closed = true
|
166
|
+
send_headers
|
167
|
+
::EM.next_tick { @body.succeed }
|
168
|
+
end
|
169
|
+
|
170
|
+
end
|
171
|
+
end
|
@@ -0,0 +1,9 @@
|
|
1
|
+
module MessageBus; module Rails; end; end
|
2
|
+
|
3
|
+
class MessageBus::Rails::Railtie < ::Rails::Railtie
|
4
|
+
initializer "message_bus.configure_init" do |app|
|
5
|
+
MessageBus::MessageHandler.load_handlers("#{Rails.root}/app/message_handlers")
|
6
|
+
app.middleware.use MessageBus::Rack::Middleware
|
7
|
+
MessageBus.logger = Rails.logger
|
8
|
+
end
|
9
|
+
end
|
@@ -0,0 +1,262 @@
|
|
1
|
+
require 'redis'
|
2
|
+
# the heart of the message bus, it acts as 2 things
|
3
|
+
#
|
4
|
+
# 1. A channel multiplexer
|
5
|
+
# 2. Backlog storage per-multiplexed channel.
|
6
|
+
#
|
7
|
+
# ids are all sequencially increasing numbers starting at 0
|
8
|
+
#
|
9
|
+
|
10
|
+
|
11
|
+
class MessageBus::ReliablePubSub
|
12
|
+
|
13
|
+
class NoMoreRetries < StandardError; end
|
14
|
+
class BackLogOutOfOrder < StandardError
|
15
|
+
attr_accessor :highest_id
|
16
|
+
|
17
|
+
def initialize(highest_id)
|
18
|
+
@highest_id = highest_id
|
19
|
+
end
|
20
|
+
end
|
21
|
+
|
22
|
+
def max_publish_retries=(val)
|
23
|
+
@max_publish_retries = val
|
24
|
+
end
|
25
|
+
|
26
|
+
def max_publish_retries
|
27
|
+
@max_publish_retries ||= 10
|
28
|
+
end
|
29
|
+
|
30
|
+
def max_publish_wait=(ms)
|
31
|
+
@max_publish_wait = ms
|
32
|
+
end
|
33
|
+
|
34
|
+
def max_publish_wait
|
35
|
+
@max_publish_wait ||= 500
|
36
|
+
end
|
37
|
+
|
38
|
+
# max_backlog_size is per multiplexed channel
|
39
|
+
def initialize(redis_config = {}, max_backlog_size = 1000)
|
40
|
+
@redis_config = redis_config
|
41
|
+
@max_backlog_size = 1000
|
42
|
+
# we can store a ton here ...
|
43
|
+
@max_global_backlog_size = 100000
|
44
|
+
end
|
45
|
+
|
46
|
+
# amount of global backlog we can spin through
|
47
|
+
def max_global_backlog_size=(val)
|
48
|
+
@max_global_backlog_size = val
|
49
|
+
end
|
50
|
+
|
51
|
+
# per channel backlog size
|
52
|
+
def max_backlog_size=(val)
|
53
|
+
@max_backlog_size = val
|
54
|
+
end
|
55
|
+
|
56
|
+
def new_redis_connection
|
57
|
+
::Redis.new(@redis_config)
|
58
|
+
end
|
59
|
+
|
60
|
+
def redis_channel_name
|
61
|
+
db = @redis_config[:db] || 0
|
62
|
+
"discourse_#{db}"
|
63
|
+
end
|
64
|
+
|
65
|
+
# redis connection used for publishing messages
|
66
|
+
def pub_redis
|
67
|
+
@pub_redis ||= new_redis_connection
|
68
|
+
end
|
69
|
+
|
70
|
+
def backlog_key(channel)
|
71
|
+
"__mb_backlog_n_#{channel}"
|
72
|
+
end
|
73
|
+
|
74
|
+
def backlog_id_key(channel)
|
75
|
+
"__mb_backlog_id_n_#{channel}"
|
76
|
+
end
|
77
|
+
|
78
|
+
def global_id_key
|
79
|
+
"__mb_global_id_n"
|
80
|
+
end
|
81
|
+
|
82
|
+
def global_backlog_key
|
83
|
+
"__mb_global_backlog_n"
|
84
|
+
end
|
85
|
+
|
86
|
+
# use with extreme care, will nuke all of the data
|
87
|
+
def reset!
|
88
|
+
pub_redis.keys("__mb_*").each do |k|
|
89
|
+
pub_redis.del k
|
90
|
+
end
|
91
|
+
end
|
92
|
+
|
93
|
+
def publish(channel, data)
|
94
|
+
redis = pub_redis
|
95
|
+
backlog_id_key = backlog_id_key(channel)
|
96
|
+
backlog_key = backlog_key(channel)
|
97
|
+
|
98
|
+
global_id = nil
|
99
|
+
backlog_id = nil
|
100
|
+
|
101
|
+
redis.multi do |m|
|
102
|
+
global_id = m.incr(global_id_key)
|
103
|
+
backlog_id = m.incr(backlog_id_key)
|
104
|
+
end
|
105
|
+
|
106
|
+
global_id = global_id.value
|
107
|
+
backlog_id = backlog_id.value
|
108
|
+
|
109
|
+
msg = MessageBus::Message.new global_id, backlog_id, channel, data
|
110
|
+
payload = msg.encode
|
111
|
+
|
112
|
+
redis.zadd backlog_key, backlog_id, payload
|
113
|
+
redis.zadd global_backlog_key, global_id, backlog_id.to_s << "|" << channel
|
114
|
+
|
115
|
+
redis.publish redis_channel_name, payload
|
116
|
+
|
117
|
+
if backlog_id > @max_backlog_size
|
118
|
+
redis.zremrangebyscore backlog_key, 1, backlog_id - @max_backlog_size
|
119
|
+
end
|
120
|
+
|
121
|
+
if global_id > @max_global_backlog_size
|
122
|
+
redis.zremrangebyscore global_backlog_key, 1, backlog_id - @max_backlog_size
|
123
|
+
end
|
124
|
+
|
125
|
+
backlog_id
|
126
|
+
end
|
127
|
+
|
128
|
+
def last_id(channel)
|
129
|
+
redis = pub_redis
|
130
|
+
backlog_id_key = backlog_id_key(channel)
|
131
|
+
redis.get(backlog_id_key).to_i
|
132
|
+
end
|
133
|
+
|
134
|
+
def backlog(channel, last_id = nil)
|
135
|
+
redis = pub_redis
|
136
|
+
backlog_key = backlog_key(channel)
|
137
|
+
items = redis.zrangebyscore backlog_key, last_id.to_i + 1, "+inf"
|
138
|
+
|
139
|
+
items.map do |i|
|
140
|
+
MessageBus::Message.decode(i)
|
141
|
+
end
|
142
|
+
end
|
143
|
+
|
144
|
+
def global_backlog(last_id = nil)
|
145
|
+
last_id = last_id.to_i
|
146
|
+
redis = pub_redis
|
147
|
+
|
148
|
+
items = redis.zrangebyscore global_backlog_key, last_id.to_i + 1, "+inf"
|
149
|
+
|
150
|
+
items.map! do |i|
|
151
|
+
pipe = i.index "|"
|
152
|
+
message_id = i[0..pipe].to_i
|
153
|
+
channel = i[pipe+1..-1]
|
154
|
+
m = get_message(channel, message_id)
|
155
|
+
m
|
156
|
+
end
|
157
|
+
|
158
|
+
items.compact!
|
159
|
+
items
|
160
|
+
end
|
161
|
+
|
162
|
+
def get_message(channel, message_id)
|
163
|
+
redis = pub_redis
|
164
|
+
backlog_key = backlog_key(channel)
|
165
|
+
|
166
|
+
items = redis.zrangebyscore backlog_key, message_id, message_id
|
167
|
+
if items && items[0]
|
168
|
+
MessageBus::Message.decode(items[0])
|
169
|
+
else
|
170
|
+
nil
|
171
|
+
end
|
172
|
+
end
|
173
|
+
|
174
|
+
def subscribe(channel, last_id = nil)
|
175
|
+
# trivial implementation for now,
|
176
|
+
# can cut down on connections if we only have one global subscriber
|
177
|
+
raise ArgumentError unless block_given?
|
178
|
+
|
179
|
+
if last_id
|
180
|
+
# we need to translate this to a global id, at least give it a shot
|
181
|
+
# we are subscribing on global and global is always going to be bigger than local
|
182
|
+
# so worst case is a replay of a few messages
|
183
|
+
message = get_message(channel, last_id)
|
184
|
+
if message
|
185
|
+
last_id = message.global_id
|
186
|
+
end
|
187
|
+
end
|
188
|
+
global_subscribe(last_id) do |m|
|
189
|
+
yield m if m.channel == channel
|
190
|
+
end
|
191
|
+
end
|
192
|
+
|
193
|
+
def process_global_backlog(highest_id, raise_error, &blk)
|
194
|
+
global_backlog(highest_id).each do |old|
|
195
|
+
if highest_id + 1 == old.global_id
|
196
|
+
yield old
|
197
|
+
highest_id = old.global_id
|
198
|
+
else
|
199
|
+
raise BackLogOutOfOrder.new(highest_id) if raise_error
|
200
|
+
if old.global_id > highest_id
|
201
|
+
yield old
|
202
|
+
highest_id = old.global_id
|
203
|
+
end
|
204
|
+
end
|
205
|
+
end
|
206
|
+
highest_id
|
207
|
+
end
|
208
|
+
|
209
|
+
def global_subscribe(last_id=nil, &blk)
|
210
|
+
raise ArgumentError unless block_given?
|
211
|
+
highest_id = last_id
|
212
|
+
|
213
|
+
clear_backlog = lambda do
|
214
|
+
retries = 4
|
215
|
+
begin
|
216
|
+
highest_id = process_global_backlog(highest_id, retries > 0, &blk)
|
217
|
+
rescue BackLogOutOfOrder => e
|
218
|
+
highest_id = e.highest_id
|
219
|
+
retries -= 1
|
220
|
+
sleep(rand(50) / 1000.0)
|
221
|
+
retry
|
222
|
+
end
|
223
|
+
end
|
224
|
+
|
225
|
+
|
226
|
+
begin
|
227
|
+
redis = new_redis_connection
|
228
|
+
|
229
|
+
if highest_id
|
230
|
+
clear_backlog.call(&blk)
|
231
|
+
end
|
232
|
+
|
233
|
+
redis.subscribe(redis_channel_name) do |on|
|
234
|
+
on.subscribe do
|
235
|
+
if highest_id
|
236
|
+
clear_backlog.call(&blk)
|
237
|
+
end
|
238
|
+
end
|
239
|
+
on.message do |c,m|
|
240
|
+
m = MessageBus::Message.decode m
|
241
|
+
|
242
|
+
# we have 2 options
|
243
|
+
#
|
244
|
+
# 1. message came in the correct order GREAT, just deal with it
|
245
|
+
# 2. message came in the incorrect order COMPLICATED, wait a tiny bit and clear backlog
|
246
|
+
|
247
|
+
if highest_id.nil? || m.global_id == highest_id + 1
|
248
|
+
highest_id = m.global_id
|
249
|
+
yield m
|
250
|
+
else
|
251
|
+
clear_backlog.call(&blk)
|
252
|
+
end
|
253
|
+
end
|
254
|
+
end
|
255
|
+
rescue => error
|
256
|
+
MessageBus.logger.warn "#{error} subscribe failed, reconnecting in 1 second. Call stack #{error.backtrace}"
|
257
|
+
sleep 1
|
258
|
+
retry
|
259
|
+
end
|
260
|
+
end
|
261
|
+
|
262
|
+
end
|
data/lib/message_bus.rb
ADDED
@@ -0,0 +1,272 @@
|
|
1
|
+
# require 'thin'
|
2
|
+
# require 'eventmachine'
|
3
|
+
# require 'rack'
|
4
|
+
# require 'redis'
|
5
|
+
|
6
|
+
require "message_bus/version"
|
7
|
+
require "message_bus/message"
|
8
|
+
require "message_bus/reliable_pub_sub"
|
9
|
+
require "message_bus/client"
|
10
|
+
require "message_bus/connection_manager"
|
11
|
+
require "message_bus/message_handler"
|
12
|
+
require "message_bus/diagnostics"
|
13
|
+
require "message_bus/rack/middleware"
|
14
|
+
require "message_bus/rack/diagnostics"
|
15
|
+
|
16
|
+
# we still need to take care of the logger
|
17
|
+
if defined?(::Rails)
|
18
|
+
require 'message_bus/rails/railtie'
|
19
|
+
end
|
20
|
+
|
21
|
+
module MessageBus; end
|
22
|
+
module MessageBus::Implementation
|
23
|
+
|
24
|
+
def cache_assets=(val)
|
25
|
+
@cache_assets = val
|
26
|
+
end
|
27
|
+
|
28
|
+
def cache_assets
|
29
|
+
if defined? @cache_assets
|
30
|
+
@cache_assets
|
31
|
+
else
|
32
|
+
true
|
33
|
+
end
|
34
|
+
end
|
35
|
+
|
36
|
+
def logger=(logger)
|
37
|
+
@logger = logger
|
38
|
+
end
|
39
|
+
|
40
|
+
def logger
|
41
|
+
return @logger if @logger
|
42
|
+
require 'logger'
|
43
|
+
@logger = Logger.new(STDOUT)
|
44
|
+
end
|
45
|
+
|
46
|
+
def long_polling_enabled?
|
47
|
+
@long_polling_enabled == false ? false : true
|
48
|
+
end
|
49
|
+
|
50
|
+
def long_polling_enabled=(val)
|
51
|
+
@long_polling_enabled = val
|
52
|
+
end
|
53
|
+
|
54
|
+
def long_polling_interval=(millisecs)
|
55
|
+
@long_polling_interval = millisecs
|
56
|
+
end
|
57
|
+
|
58
|
+
def long_polling_interval
|
59
|
+
@long_polling_interval || 30 * 1000
|
60
|
+
end
|
61
|
+
|
62
|
+
def off
|
63
|
+
@off = true
|
64
|
+
end
|
65
|
+
|
66
|
+
def on
|
67
|
+
@off = false
|
68
|
+
end
|
69
|
+
|
70
|
+
# Allow us to inject a redis db
|
71
|
+
def redis_config=(config)
|
72
|
+
@redis_config = config
|
73
|
+
end
|
74
|
+
|
75
|
+
def redis_config
|
76
|
+
@redis_config ||= {}
|
77
|
+
end
|
78
|
+
|
79
|
+
def site_id_lookup(&blk)
|
80
|
+
@site_id_lookup = blk if blk
|
81
|
+
@site_id_lookup
|
82
|
+
end
|
83
|
+
|
84
|
+
def user_id_lookup(&blk)
|
85
|
+
@user_id_lookup = blk if blk
|
86
|
+
@user_id_lookup
|
87
|
+
end
|
88
|
+
|
89
|
+
def group_ids_lookup(&blk)
|
90
|
+
@group_ids_lookup = blk if blk
|
91
|
+
@group_ids_lookup
|
92
|
+
end
|
93
|
+
|
94
|
+
def is_admin_lookup(&blk)
|
95
|
+
@is_admin_lookup = blk if blk
|
96
|
+
@is_admin_lookup
|
97
|
+
end
|
98
|
+
|
99
|
+
def on_connect(&blk)
|
100
|
+
@on_connect = blk if blk
|
101
|
+
@on_connect
|
102
|
+
end
|
103
|
+
|
104
|
+
def on_disconnect(&blk)
|
105
|
+
@on_disconnect = blk if blk
|
106
|
+
@on_disconnect
|
107
|
+
end
|
108
|
+
|
109
|
+
def allow_broadcast=(val)
|
110
|
+
@allow_broadcast = val
|
111
|
+
end
|
112
|
+
|
113
|
+
def allow_broadcast?
|
114
|
+
@allow_broadcast ||=
|
115
|
+
if defined? ::Rails
|
116
|
+
::Rails.env.test? || ::Rails.env.development?
|
117
|
+
else
|
118
|
+
false
|
119
|
+
end
|
120
|
+
end
|
121
|
+
|
122
|
+
def reliable_pub_sub
|
123
|
+
@reliable_pub_sub ||= MessageBus::ReliablePubSub.new redis_config
|
124
|
+
end
|
125
|
+
|
126
|
+
def enable_diagnostics
|
127
|
+
MessageBus::Diagnostics.enable
|
128
|
+
end
|
129
|
+
|
130
|
+
def publish(channel, data, opts = nil)
|
131
|
+
return if @off
|
132
|
+
|
133
|
+
user_ids = nil
|
134
|
+
group_ids = nil
|
135
|
+
if opts
|
136
|
+
user_ids = opts[:user_ids]
|
137
|
+
group_ids = opts[:group_ids]
|
138
|
+
end
|
139
|
+
|
140
|
+
encoded_data = JSON.dump({
|
141
|
+
data: data,
|
142
|
+
user_ids: user_ids,
|
143
|
+
group_ids: group_ids
|
144
|
+
})
|
145
|
+
|
146
|
+
reliable_pub_sub.publish(encode_channel_name(channel), encoded_data)
|
147
|
+
end
|
148
|
+
|
149
|
+
def blocking_subscribe(channel=nil, &blk)
|
150
|
+
if channel
|
151
|
+
reliable_pub_sub.subscribe(encode_channel_name(channel), &blk)
|
152
|
+
else
|
153
|
+
reliable_pub_sub.global_subscribe(&blk)
|
154
|
+
end
|
155
|
+
end
|
156
|
+
|
157
|
+
ENCODE_SITE_TOKEN = "$|$"
|
158
|
+
|
159
|
+
# encode channel name to include site
|
160
|
+
def encode_channel_name(channel)
|
161
|
+
if MessageBus.site_id_lookup
|
162
|
+
raise ArgumentError.new channel if channel.include? ENCODE_SITE_TOKEN
|
163
|
+
"#{channel}#{ENCODE_SITE_TOKEN}#{MessageBus.site_id_lookup.call}"
|
164
|
+
else
|
165
|
+
channel
|
166
|
+
end
|
167
|
+
end
|
168
|
+
|
169
|
+
def decode_channel_name(channel)
|
170
|
+
channel.split(ENCODE_SITE_TOKEN)
|
171
|
+
end
|
172
|
+
|
173
|
+
def subscribe(channel=nil, &blk)
|
174
|
+
subscribe_impl(channel, nil, &blk)
|
175
|
+
end
|
176
|
+
|
177
|
+
# subscribe only on current site
|
178
|
+
def local_subscribe(channel=nil, &blk)
|
179
|
+
site_id = MessageBus.site_id_lookup.call if MessageBus.site_id_lookup
|
180
|
+
subscribe_impl(channel, site_id, &blk)
|
181
|
+
end
|
182
|
+
|
183
|
+
def backlog(channel=nil, last_id)
|
184
|
+
old =
|
185
|
+
if channel
|
186
|
+
reliable_pub_sub.backlog(encode_channel_name(channel), last_id)
|
187
|
+
else
|
188
|
+
reliable_pub_sub.global_backlog(encode_channel_name(channel), last_id)
|
189
|
+
end
|
190
|
+
|
191
|
+
old.each{ |m|
|
192
|
+
decode_message!(m)
|
193
|
+
}
|
194
|
+
old
|
195
|
+
end
|
196
|
+
|
197
|
+
|
198
|
+
def last_id(channel)
|
199
|
+
reliable_pub_sub.last_id(encode_channel_name(channel))
|
200
|
+
end
|
201
|
+
|
202
|
+
protected
|
203
|
+
|
204
|
+
def decode_message!(msg)
|
205
|
+
channel, site_id = decode_channel_name(msg.channel)
|
206
|
+
msg.channel = channel
|
207
|
+
msg.site_id = site_id
|
208
|
+
parsed = JSON.parse(msg.data)
|
209
|
+
msg.data = parsed["data"]
|
210
|
+
msg.user_ids = parsed["user_ids"]
|
211
|
+
msg.group_ids = parsed["group_ids"]
|
212
|
+
end
|
213
|
+
|
214
|
+
def subscribe_impl(channel, site_id, &blk)
|
215
|
+
@subscriptions ||= {}
|
216
|
+
@subscriptions[site_id] ||= {}
|
217
|
+
@subscriptions[site_id][channel] ||= []
|
218
|
+
@subscriptions[site_id][channel] << blk
|
219
|
+
ensure_subscriber_thread
|
220
|
+
end
|
221
|
+
|
222
|
+
def ensure_subscriber_thread
|
223
|
+
@mutex ||= Mutex.new
|
224
|
+
@mutex.synchronize do
|
225
|
+
return if @subscriber_thread
|
226
|
+
@subscriber_thread = Thread.new do
|
227
|
+
reliable_pub_sub.global_subscribe do |msg|
|
228
|
+
begin
|
229
|
+
decode_message!(msg)
|
230
|
+
|
231
|
+
globals = @subscriptions[nil]
|
232
|
+
locals = @subscriptions[msg.site_id] if msg.site_id
|
233
|
+
|
234
|
+
global_globals = globals[nil] if globals
|
235
|
+
local_globals = locals[nil] if locals
|
236
|
+
|
237
|
+
globals = globals[msg.channel] if globals
|
238
|
+
locals = locals[msg.channel] if locals
|
239
|
+
|
240
|
+
multi_each(globals,locals, global_globals, local_globals) do |c|
|
241
|
+
begin
|
242
|
+
c.call msg
|
243
|
+
rescue => e
|
244
|
+
MessageBus.logger.warn "failed to deliver message, skipping #{msg.inspect}\n ex: #{e} backtrace: #{e.backtrace}"
|
245
|
+
end
|
246
|
+
end
|
247
|
+
|
248
|
+
rescue => e
|
249
|
+
MessageBus.logger.warn "failed to process message #{msg.inspect}\n ex: #{e} backtrace: #{e.backtrace}"
|
250
|
+
end
|
251
|
+
|
252
|
+
end
|
253
|
+
end
|
254
|
+
end
|
255
|
+
end
|
256
|
+
|
257
|
+
def multi_each(*args,&block)
|
258
|
+
args.each do |a|
|
259
|
+
a.each(&block) if a
|
260
|
+
end
|
261
|
+
end
|
262
|
+
|
263
|
+
end
|
264
|
+
|
265
|
+
module MessageBus
|
266
|
+
extend MessageBus::Implementation
|
267
|
+
end
|
268
|
+
|
269
|
+
# allows for multiple buses per app
|
270
|
+
class MessageBus::Instance
|
271
|
+
include MessageBus::Implementation
|
272
|
+
end
|
metadata
ADDED
@@ -0,0 +1,112 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: message_bus
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 0.0.1
|
5
|
+
platform: ruby
|
6
|
+
authors:
|
7
|
+
- Sam Saffron
|
8
|
+
autorequire:
|
9
|
+
bindir: bin
|
10
|
+
cert_chain: []
|
11
|
+
date: 2013-05-15 00:00:00.000000000 Z
|
12
|
+
dependencies:
|
13
|
+
- !ruby/object:Gem::Dependency
|
14
|
+
name: rack
|
15
|
+
requirement: !ruby/object:Gem::Requirement
|
16
|
+
requirements:
|
17
|
+
- - '>='
|
18
|
+
- !ruby/object:Gem::Version
|
19
|
+
version: 1.1.3
|
20
|
+
type: :runtime
|
21
|
+
prerelease: false
|
22
|
+
version_requirements: !ruby/object:Gem::Requirement
|
23
|
+
requirements:
|
24
|
+
- - '>='
|
25
|
+
- !ruby/object:Gem::Version
|
26
|
+
version: 1.1.3
|
27
|
+
- !ruby/object:Gem::Dependency
|
28
|
+
name: thin
|
29
|
+
requirement: !ruby/object:Gem::Requirement
|
30
|
+
requirements:
|
31
|
+
- - '>='
|
32
|
+
- !ruby/object:Gem::Version
|
33
|
+
version: '0'
|
34
|
+
type: :runtime
|
35
|
+
prerelease: false
|
36
|
+
version_requirements: !ruby/object:Gem::Requirement
|
37
|
+
requirements:
|
38
|
+
- - '>='
|
39
|
+
- !ruby/object:Gem::Version
|
40
|
+
version: '0'
|
41
|
+
- !ruby/object:Gem::Dependency
|
42
|
+
name: eventmachine
|
43
|
+
requirement: !ruby/object:Gem::Requirement
|
44
|
+
requirements:
|
45
|
+
- - '>='
|
46
|
+
- !ruby/object:Gem::Version
|
47
|
+
version: '0'
|
48
|
+
type: :runtime
|
49
|
+
prerelease: false
|
50
|
+
version_requirements: !ruby/object:Gem::Requirement
|
51
|
+
requirements:
|
52
|
+
- - '>='
|
53
|
+
- !ruby/object:Gem::Version
|
54
|
+
version: '0'
|
55
|
+
- !ruby/object:Gem::Dependency
|
56
|
+
name: redis
|
57
|
+
requirement: !ruby/object:Gem::Requirement
|
58
|
+
requirements:
|
59
|
+
- - '>='
|
60
|
+
- !ruby/object:Gem::Version
|
61
|
+
version: '0'
|
62
|
+
type: :runtime
|
63
|
+
prerelease: false
|
64
|
+
version_requirements: !ruby/object:Gem::Requirement
|
65
|
+
requirements:
|
66
|
+
- - '>='
|
67
|
+
- !ruby/object:Gem::Version
|
68
|
+
version: '0'
|
69
|
+
description: A message bus built on websockets
|
70
|
+
email:
|
71
|
+
- sam.saffron@gmail.com
|
72
|
+
executables: []
|
73
|
+
extensions: []
|
74
|
+
extra_rdoc_files: []
|
75
|
+
files:
|
76
|
+
- README.md
|
77
|
+
- LICENSE
|
78
|
+
- lib/message_bus.rb
|
79
|
+
- lib/message_bus/rack/diagnostics.rb
|
80
|
+
- lib/message_bus/rack/middleware.rb
|
81
|
+
- lib/message_bus/message.rb
|
82
|
+
- lib/message_bus/connection_manager.rb
|
83
|
+
- lib/message_bus/message_handler.rb
|
84
|
+
- lib/message_bus/rails/railtie.rb
|
85
|
+
- lib/message_bus/version.rb
|
86
|
+
- lib/message_bus/diagnostics.rb
|
87
|
+
- lib/message_bus/client.rb
|
88
|
+
- lib/message_bus/reliable_pub_sub.rb
|
89
|
+
homepage: ''
|
90
|
+
licenses: []
|
91
|
+
metadata: {}
|
92
|
+
post_install_message:
|
93
|
+
rdoc_options: []
|
94
|
+
require_paths:
|
95
|
+
- lib
|
96
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
97
|
+
requirements:
|
98
|
+
- - '>='
|
99
|
+
- !ruby/object:Gem::Version
|
100
|
+
version: '0'
|
101
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
102
|
+
requirements:
|
103
|
+
- - '>='
|
104
|
+
- !ruby/object:Gem::Version
|
105
|
+
version: '0'
|
106
|
+
requirements: []
|
107
|
+
rubyforge_project:
|
108
|
+
rubygems_version: 2.0.2
|
109
|
+
signing_key:
|
110
|
+
specification_version: 4
|
111
|
+
summary: ''
|
112
|
+
test_files: []
|