message_bus 0.0.1

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.

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
@@ -0,0 +1,3 @@
1
+ module MessageBus
2
+ VERSION = "0.0.1"
3
+ end
@@ -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: []