message_bus 2.1.6 → 2.2.0.pre

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.

Potentially problematic release.


This version of message_bus might be problematic. Click here for more details.

Files changed (49) hide show
  1. checksums.yaml +4 -4
  2. data/.rubocop.yml +13 -92
  3. data/.rubocop_todo.yml +659 -0
  4. data/.travis.yml +1 -1
  5. data/CHANGELOG +61 -0
  6. data/Dockerfile +18 -0
  7. data/Gemfile +3 -1
  8. data/Guardfile +0 -1
  9. data/README.md +188 -101
  10. data/Rakefile +12 -1
  11. data/assets/message-bus.js +1 -1
  12. data/docker-compose.yml +46 -0
  13. data/examples/bench/config.ru +8 -9
  14. data/examples/bench/unicorn.conf.rb +1 -1
  15. data/examples/chat/chat.rb +150 -153
  16. data/examples/minimal/config.ru +2 -3
  17. data/lib/message_bus.rb +224 -36
  18. data/lib/message_bus/backends.rb +7 -0
  19. data/lib/message_bus/backends/base.rb +184 -0
  20. data/lib/message_bus/backends/memory.rb +304 -226
  21. data/lib/message_bus/backends/postgres.rb +359 -318
  22. data/lib/message_bus/backends/redis.rb +380 -337
  23. data/lib/message_bus/client.rb +99 -41
  24. data/lib/message_bus/connection_manager.rb +29 -21
  25. data/lib/message_bus/diagnostics.rb +50 -41
  26. data/lib/message_bus/distributed_cache.rb +5 -7
  27. data/lib/message_bus/message.rb +2 -2
  28. data/lib/message_bus/rack/diagnostics.rb +65 -55
  29. data/lib/message_bus/rack/middleware.rb +64 -44
  30. data/lib/message_bus/rack/thin_ext.rb +13 -9
  31. data/lib/message_bus/rails/railtie.rb +2 -0
  32. data/lib/message_bus/timer_thread.rb +2 -2
  33. data/lib/message_bus/version.rb +2 -1
  34. data/message_bus.gemspec +3 -2
  35. data/spec/assets/support/jasmine_helper.rb +1 -1
  36. data/spec/lib/fake_async_middleware.rb +1 -6
  37. data/spec/lib/message_bus/assets/asset_encoding_spec.rb +3 -3
  38. data/spec/lib/message_bus/backend_spec.rb +409 -0
  39. data/spec/lib/message_bus/client_spec.rb +8 -11
  40. data/spec/lib/message_bus/connection_manager_spec.rb +8 -14
  41. data/spec/lib/message_bus/distributed_cache_spec.rb +0 -4
  42. data/spec/lib/message_bus/multi_process_spec.rb +6 -7
  43. data/spec/lib/message_bus/rack/middleware_spec.rb +47 -43
  44. data/spec/lib/message_bus/timer_thread_spec.rb +0 -2
  45. data/spec/lib/message_bus_spec.rb +59 -43
  46. data/spec/spec_helper.rb +16 -4
  47. metadata +12 -9
  48. data/spec/lib/message_bus/backends/postgres_spec.rb +0 -221
  49. data/spec/lib/message_bus/backends/redis_spec.rb +0 -271
@@ -1,7 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- class MessageBus::Message < Struct.new(:global_id, :message_id, :channel , :data)
4
-
3
+ # Represents a published message and its encoding for persistence.
4
+ class MessageBus::Message < Struct.new(:global_id, :message_id, :channel, :data)
5
5
  attr_accessor :site_id, :user_ids, :group_ids, :client_ids
6
6
 
7
7
  def self.decode(encoded)
@@ -1,68 +1,21 @@
1
1
  # frozen_string_literal: true
2
+
2
3
  module MessageBus::Rack; end
3
4
 
5
+ # Accepts requests from clients interested in using diagnostics functionality
6
+ # @see MessageBus::Diagnostics
4
7
  class MessageBus::Rack::Diagnostics
8
+ # @param [Proc] app the rack app
9
+ # @param [Hash] config
10
+ # @option config [MessageBus::Instance] :message_bus (`MessageBus`) a specific instance of message_bus
5
11
  def initialize(app, config = {})
6
12
  @app = app
7
13
  @bus = config[:message_bus] || MessageBus
8
14
  end
9
15
 
10
- def js_asset(name)
11
- return generate_script_tag(name) unless @bus.cache_assets
12
- @@asset_cache ||= {}
13
- @@asset_cache[name] ||= generate_script_tag(name)
14
- @@asset_cache[name]
15
- end
16
-
17
- def generate_script_tag(name)
18
- "<script src='/message-bus/_diagnostics/assets/#{name}?#{file_hash(name)}' type='text/javascript'></script>"
19
- end
20
-
21
- def file_hash(asset)
22
- require 'digest/sha1'
23
- Digest::SHA1.hexdigest(asset_contents(asset))
24
- end
25
-
26
- def asset_contents(asset)
27
- File.open(asset_path(asset)).read
28
- end
29
-
30
- def asset_path(asset)
31
- File.expand_path("../../../../assets/#{asset}", __FILE__)
32
- end
33
-
34
- def index
35
- html = <<HTML
36
- <!DOCTYPE html>
37
- <html>
38
- <head>
39
- </head>
40
- <body>
41
- <div id="app"></div>
42
- #{js_asset "jquery-1.8.2.js"}
43
- #{js_asset "handlebars.js"}
44
- #{js_asset "ember.js"}
45
- #{js_asset "message-bus.js"}
46
- #{js_asset "application.handlebars"}
47
- #{js_asset "index.handlebars"}
48
- #{js_asset "application.js"}
49
- </body>
50
- </html>
51
- HTML
52
- return [200, { "content-type" => "text/html;" }, [html]]
53
- end
54
-
55
- def translate_handlebars(name, content)
56
- "Ember.TEMPLATES['#{name}'] = Ember.Handlebars.compile(#{indent(content).inspect});"
57
- end
58
-
59
- # from ember-rails
60
- def indent(string)
61
- string.gsub(/$(.)/m, "\\1 ").strip
62
- end
63
-
16
+ # Process an HTTP request from a subscriber client
17
+ # @param [Rack::Request::Env] env the request environment
64
18
  def call(env)
65
-
66
19
  return @app.call(env) unless env['PATH_INFO'].start_with? '/message-bus/_diagnostics'
67
20
 
68
21
  route = env['PATH_INFO'].split('/message-bus/_diagnostics')[1]
@@ -97,4 +50,61 @@ HTML
97
50
 
98
51
  return [404, {}, ['not found']]
99
52
  end
53
+
54
+ private
55
+
56
+ def js_asset(name)
57
+ return generate_script_tag(name) unless @bus.cache_assets
58
+
59
+ @@asset_cache ||= {}
60
+ @@asset_cache[name] ||= generate_script_tag(name)
61
+ @@asset_cache[name]
62
+ end
63
+
64
+ def generate_script_tag(name)
65
+ "<script src='/message-bus/_diagnostics/assets/#{name}?#{file_hash(name)}' type='text/javascript'></script>"
66
+ end
67
+
68
+ def file_hash(asset)
69
+ require 'digest/sha1'
70
+ Digest::SHA1.hexdigest(asset_contents(asset))
71
+ end
72
+
73
+ def asset_contents(asset)
74
+ File.open(asset_path(asset)).read
75
+ end
76
+
77
+ def asset_path(asset)
78
+ File.expand_path("../../../../assets/#{asset}", __FILE__)
79
+ end
80
+
81
+ def index
82
+ html = <<~HTML
83
+ <!DOCTYPE html>
84
+ <html>
85
+ <head>
86
+ </head>
87
+ <body>
88
+ <div id="app"></div>
89
+ #{js_asset "jquery-1.8.2.js"}
90
+ #{js_asset "handlebars.js"}
91
+ #{js_asset "ember.js"}
92
+ #{js_asset "message-bus.js"}
93
+ #{js_asset "application.handlebars"}
94
+ #{js_asset "index.handlebars"}
95
+ #{js_asset "application.js"}
96
+ </body>
97
+ </html>
98
+ HTML
99
+ return [200, { "content-type" => "text/html;" }, [html]]
100
+ end
101
+
102
+ def translate_handlebars(name, content)
103
+ "Ember.TEMPLATES['#{name}'] = Ember.Handlebars.compile(#{indent(content).inspect});"
104
+ end
105
+
106
+ # from ember-rails
107
+ def indent(string)
108
+ string.gsub(/$(.)/m, "\\1 ").strip
109
+ end
100
110
  end
@@ -1,44 +1,49 @@
1
1
  # frozen_string_literal: true
2
+
2
3
  require 'json'
3
4
 
4
5
  # our little message bus, accepts long polling and polling
5
6
  module MessageBus::Rack; end
6
7
 
8
+ # Accepts requests from subscribers, validates and authenticates them,
9
+ # delivers existing messages from the backlog and informs a
10
+ # `MessageBus::ConnectionManager` of a connection which is remaining open.
7
11
  class MessageBus::Rack::Middleware
8
-
9
- def start_listener
10
- unless @started_listener
11
-
12
- thin = defined?(Thin::Server) && ObjectSpace.each_object(Thin::Server).to_a.first
13
- thin_running = thin && thin.running?
14
-
15
- @subscription = @bus.subscribe do |msg|
16
- run = proc do
17
- begin
18
- @connection_manager.notify_clients(msg) if @connection_manager
19
- rescue
20
- @bus.logger.warn "Failed to notify clients: #{$!} #{$!.backtrace}"
21
- end
22
- end
23
-
24
- if thin_running
25
- EM.next_tick(&run)
26
- else
27
- @bus.timer.queue(&run)
28
- end
29
-
30
- @started_listener = true
31
- end
32
- end
12
+ # @param [Array<MessageBus::Message>] backlog a list of messages for delivery
13
+ # @return [JSON] a JSON representation of the backlog, compliant with the
14
+ # subscriber API specification
15
+ def self.backlog_to_json(backlog)
16
+ m = backlog.map do |msg|
17
+ {
18
+ global_id: msg.global_id,
19
+ message_id: msg.message_id,
20
+ channel: msg.channel,
21
+ data: msg.data
22
+ }
23
+ end.to_a
24
+ JSON.dump(m)
33
25
  end
34
26
 
27
+ # @return [Boolean] whether the message listener (subscriber) is started or not)
28
+ attr_reader :started_listener
29
+
30
+ # Sets up the middleware to receive subscriber client requests and begins
31
+ # listening for messages published on the bus for re-distribution (unless
32
+ # the bus is disabled).
33
+ #
34
+ # @param [Proc] app the rack app
35
+ # @param [Hash] config
36
+ # @option config [MessageBus::Instance] :message_bus (`MessageBus`) a specific instance of message_bus
35
37
  def initialize(app, config = {})
36
38
  @app = app
37
39
  @bus = config[:message_bus] || MessageBus
38
40
  @connection_manager = MessageBus::ConnectionManager.new(@bus)
39
- self.start_listener
41
+ @started_listener = false
42
+ start_listener unless @bus.off?
40
43
  end
41
44
 
45
+ # Stops listening for messages on the bus
46
+ # @return [void]
42
47
  def stop_listener
43
48
  if @subscription
44
49
  @bus.unsubscribe(&@subscription)
@@ -46,20 +51,9 @@ class MessageBus::Rack::Middleware
46
51
  end
47
52
  end
48
53
 
49
- def self.backlog_to_json(backlog)
50
- m = backlog.map do |msg|
51
- {
52
- global_id: msg.global_id,
53
- message_id: msg.message_id,
54
- channel: msg.channel,
55
- data: msg.data
56
- }
57
- end.to_a
58
- JSON.dump(m)
59
- end
60
-
54
+ # Process an HTTP request from a subscriber client
55
+ # @param [Rack::Request::Env] env the request environment
61
56
  def call(env)
62
-
63
57
  return @app.call(env) unless env['PATH_INFO'] =~ /^\/message-bus\//
64
58
 
65
59
  # special debug/test route
@@ -136,9 +130,9 @@ class MessageBus::Rack::Middleware
136
130
  backlog = client.backlog
137
131
 
138
132
  if backlog.length > 0 && !allow_chunked
139
- client.cancel
133
+ client.close
140
134
  @bus.logger.debug "Delivering backlog #{backlog} to client #{client_id} for user #{user_id}"
141
- [200, headers, [self.class.backlog_to_json(backlog)] ]
135
+ [200, headers, [self.class.backlog_to_json(backlog)]]
142
136
  elsif long_polling && env['rack.hijack'] && @bus.rack_hijack_enabled?
143
137
  io = env['rack.hijack'].call
144
138
  # TODO disable client till deliver backlog is called
@@ -182,7 +176,6 @@ class MessageBus::Rack::Middleware
182
176
  else
183
177
  [200, headers, [self.class.backlog_to_json(backlog)]]
184
178
  end
185
-
186
179
  rescue => e
187
180
  if @bus.on_middleware_error && result = @bus.on_middleware_error.call(env, e)
188
181
  result
@@ -191,6 +184,8 @@ class MessageBus::Rack::Middleware
191
184
  end
192
185
  end
193
186
 
187
+ private
188
+
194
189
  def close_db_connection!
195
190
  # IMPORTANT
196
191
  # ConnectionManagement in Rails puts a BodyProxy around stuff
@@ -206,12 +201,37 @@ class MessageBus::Rack::Middleware
206
201
 
207
202
  client.cleanup_timer = @bus.timer.queue(@bus.long_polling_interval.to_f / 1000) {
208
203
  begin
209
- client.cleanup_timer = nil
210
- client.ensure_closed!
204
+ client.close
211
205
  @connection_manager.remove_client(client)
212
206
  rescue
213
207
  @bus.logger.warn "Failed to clean up client properly: #{$!} #{$!.backtrace}"
214
208
  end
215
209
  }
216
210
  end
211
+
212
+ def start_listener
213
+ unless @started_listener
214
+
215
+ thin = defined?(Thin::Server) && ObjectSpace.each_object(Thin::Server).to_a.first
216
+ thin_running = thin && thin.running?
217
+
218
+ @subscription = @bus.subscribe do |msg|
219
+ run = proc do
220
+ begin
221
+ @connection_manager.notify_clients(msg) if @connection_manager
222
+ rescue
223
+ @bus.logger.warn "Failed to notify clients: #{$!} #{$!.backtrace}"
224
+ end
225
+ end
226
+
227
+ if thin_running
228
+ EM.next_tick(&run)
229
+ else
230
+ @bus.timer.queue(&run)
231
+ end
232
+
233
+ @started_listener = true
234
+ end
235
+ end
236
+ end
217
237
  end
@@ -1,4 +1,5 @@
1
1
  # frozen_string_literal: true
2
+
2
3
  # there is also another in cramp this is from https://github.com/macournoyer/thin_async/blob/master/lib/thin/async.rb
3
4
  module Thin
4
5
  unless defined?(DeferrableBody)
@@ -21,16 +22,19 @@ module Thin
21
22
  end
22
23
 
23
24
  private
24
- def schedule_dequeue
25
- return unless @body_callback
26
- ::EM.next_tick do
27
- next unless body = @queue.shift
28
- body.each do |chunk|
29
- @body_callback.call(chunk)
30
- end
31
- schedule_dequeue unless @queue.empty?
25
+
26
+ def schedule_dequeue
27
+ return unless @body_callback
28
+
29
+ ::EM.next_tick do
30
+ next unless body = @queue.shift
31
+
32
+ body.each do |chunk|
33
+ @body_callback.call(chunk)
32
34
  end
35
+ schedule_dequeue unless @queue.empty?
33
36
  end
37
+ end
34
38
  end
35
39
  end
36
40
 
@@ -51,6 +55,7 @@ module Thin
51
55
 
52
56
  def send_headers
53
57
  return if @headers_sent
58
+
54
59
  @callback.call [@status, @headers, @body]
55
60
  @headers_sent = true
56
61
  end
@@ -67,6 +72,5 @@ module Thin
67
72
  send_headers
68
73
  ::EM.next_tick { @body.succeed }
69
74
  end
70
-
71
75
  end
72
76
  end
@@ -1,4 +1,5 @@
1
1
  # frozen_string_literal: true
2
+
2
3
  module MessageBus; module Rails; end; end
3
4
 
4
5
  # rails engine for asset pipeline
@@ -25,6 +26,7 @@ class MessageBus::Rails::Railtie < ::Rails::Railtie
25
26
 
26
27
  def api_only?(config)
27
28
  return false unless config.respond_to?(:api_only)
29
+
28
30
  config.api_only
29
31
  end
30
32
 
@@ -1,6 +1,6 @@
1
1
  # frozen_string_literal: true
2
- class MessageBus::TimerThread
3
2
 
3
+ class MessageBus::TimerThread
4
4
  attr_reader :jobs
5
5
 
6
6
  class Cancelable
@@ -16,6 +16,7 @@ class MessageBus::TimerThread
16
16
  def initialize(job)
17
17
  @job = job
18
18
  end
19
+
19
20
  def cancel
20
21
  @job[1] = NOOP
21
22
  end
@@ -123,5 +124,4 @@ class MessageBus::TimerThread
123
124
  end
124
125
  end
125
126
  end
126
-
127
127
  end
@@ -1,4 +1,5 @@
1
1
  # frozen_string_literal: true
2
+
2
3
  module MessageBus
3
- VERSION = "2.1.6"
4
+ VERSION = "2.2.0.pre"
4
5
  end
@@ -1,5 +1,6 @@
1
1
  # frozen_string_literal: true
2
2
  # -*- encoding: utf-8 -*-
3
+
3
4
  require File.expand_path('../lib/message_bus/version', __FILE__)
4
5
 
5
6
  Gem::Specification.new do |gem|
@@ -10,13 +11,13 @@ Gem::Specification.new do |gem|
10
11
  gem.homepage = "https://github.com/SamSaffron/message_bus"
11
12
  gem.license = "MIT"
12
13
  gem.files = `git ls-files`.split($\) +
13
- ["vendor/assets/javascripts/message-bus.js", "vendor/assets/javascripts/message-bus-ajax.js"]
14
+ ["vendor/assets/javascripts/message-bus.js", "vendor/assets/javascripts/message-bus-ajax.js"]
14
15
  gem.executables = gem.files.grep(%r{^bin/}).map { |f| File.basename(f) }
15
16
  gem.test_files = gem.files.grep(%r{^(test|spec|features)/})
16
17
  gem.name = "message_bus"
17
18
  gem.require_paths = ["lib"]
18
19
  gem.version = MessageBus::VERSION
19
- gem.required_ruby_version = ">= 2.2.0"
20
+ gem.required_ruby_version = ">= 2.3.0"
20
21
  gem.add_runtime_dependency 'rack', '>= 1.1.3'
21
22
  gem.add_development_dependency 'redis'
22
23
  gem.add_development_dependency 'pg'
@@ -1,4 +1,4 @@
1
- Jasmine.configure do |config|
1
+ Jasmine.configure do |_config|
2
2
  # patch for travis
3
3
  if ENV['TRAVIS']
4
4
  module ::Phantomjs
@@ -1,7 +1,6 @@
1
1
  require 'http/parser'
2
2
  class FakeAsyncMiddleware
3
-
4
- def initialize(app,config={})
3
+ def initialize(app, config = {})
5
4
  @app = app
6
5
  @bus = config[:message_bus] || MessageBus
7
6
  @simulate_thin_async = false
@@ -31,7 +30,6 @@ class FakeAsyncMiddleware
31
30
  @in_async
32
31
  end
33
32
 
34
-
35
33
  def simulate_thin_async?
36
34
  @simulate_thin_async && @bus.long_polling_enabled?
37
35
  end
@@ -64,7 +62,6 @@ class FakeAsyncMiddleware
64
62
  [parser.status_code, parser.headers, [body]]
65
63
  end
66
64
 
67
-
68
65
  def call_rack_hijack(env)
69
66
  # this is not to spec, the spec actually return, but here we will simply simulate and block
70
67
  result = nil
@@ -106,7 +103,6 @@ class FakeAsyncMiddleware
106
103
 
107
104
  @in_async = false
108
105
  result || [500, {}, ['timeout']]
109
-
110
106
  end
111
107
 
112
108
  def call_thin_async(env)
@@ -147,4 +143,3 @@ class FakeAsyncMiddleware
147
143
  result || [500, {}, ['timeout']]
148
144
  end
149
145
  end
150
-