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 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: []