msgr 0.4.1 → 0.5.0

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: 2c7b66e79b0bc86ea60a569f091bb1552a82ed4f
4
- data.tar.gz: 3eccf709cf43f74f17898de8340a10456f76f4a9
3
+ metadata.gz: ddae04ef1c28f7eb451ee8988da5949ab0c97d6e
4
+ data.tar.gz: 12490c056d64088711f79d9ddc2d8c227015efc0
5
5
  SHA512:
6
- metadata.gz: 409a4007b8329b9d4952ee58cbc80f637669af1701bcd664401b365b4bf0c34a95a052b89da567487e7051b2ff075a67ea6b92016bff023c8fbd65912a4fce69
7
- data.tar.gz: 4cae8599276ef6e5fc7732fa216a63872cb87ed682dfcb497d4c0a47910c94a3e66469eed1c2fa162eca42684f4676cacedb70907357eea3aeee463d92c5533c
6
+ metadata.gz: 984436fe6ac5ce0783c6a8fabd2039c1d119dcc7c62cb749493818c44c3f6c6c2c6e350efd0555933012b74637e61b140d3dfbf8cf5c7981d2912248eb580e62
7
+ data.tar.gz: 1fc475e7d83893f7129e4fd6b42736d647cac8b74cd03e93fc1167a331398d44c07ed3964e139e708ff66a19a22e89b17271c066c025b937a3eff07ce1bdd747
data/lib/msgr.rb CHANGED
@@ -1,22 +1,18 @@
1
1
  require 'msgr/version'
2
- require 'celluloid'
3
2
  require 'active_support'
4
3
  require 'active_support/core_ext/object/blank'
5
4
  require 'active_support/core_ext/module/delegation'
6
5
  require 'active_support/core_ext/string/inflections'
7
6
  require 'active_support/core_ext/hash/reverse_merge'
8
7
  require 'active_support/core_ext/hash/keys'
9
- require 'json'
10
8
 
11
9
  require 'msgr/logging'
12
- require 'msgr/binding'
13
10
  require 'msgr/client'
14
11
  require 'msgr/connection'
15
12
  require 'msgr/consumer'
16
13
  require 'msgr/dispatcher'
17
14
  require 'msgr/errors'
18
15
  require 'msgr/message'
19
- require 'msgr/pool'
20
16
  require 'msgr/route'
21
17
  require 'msgr/routes'
22
18
 
@@ -49,16 +45,8 @@ module Msgr
49
45
  @logger = logger
50
46
  end
51
47
 
52
- def after_load(&block)
53
- @after_load_callbacks ||= []
54
- @after_load_callbacks << block
55
- end
56
-
57
48
  def start
58
49
  client.start
59
- (@after_load_callbacks || []).each do |callback|
60
- callback.call client
61
- end
62
50
  client
63
51
  end
64
52
  end
data/lib/msgr/client.rb CHANGED
@@ -1,126 +1,119 @@
1
- require 'bunny'
2
-
3
1
  module Msgr
4
2
 
5
3
  class Client
6
- include Celluloid
7
4
  include Logging
8
-
9
- attr_reader :uri
5
+ attr_reader :uri, :config
10
6
 
11
7
  def initialize(config = {})
12
- @uri = URI.parse config[:uri] ? config.delete(:uri) : 'amqp://localhost/'
8
+ @uri = ::URI.parse config[:uri] ? config.delete(:uri) : 'amqp://localhost/'
13
9
  config[:pass] ||= @uri.password
14
10
 
15
- @uri.user = config[:user] ||= @uri.user || 'guest'
16
- @uri.scheme = (config[:ssl] ||= @uri.scheme.to_s.downcase == 'amqps') ? 'amqps' : 'amqp'
17
- @uri.host = config[:host] ||= @uri.host || '127.0.0.1'
18
- @uri.port = config[:port] ||= @uri.port
11
+ @uri.user = config[:user] ||= @uri.user || 'guest'
12
+ @uri.scheme = (config[:ssl] ||= @uri.scheme.to_s.downcase == 'amqps') ? 'amqps' : 'amqp'
13
+ @uri.host = config[:host] ||= @uri.host || '127.0.0.1'
14
+ @uri.port = config[:port] ||= @uri.port
19
15
  @uri.path = config[:vhost] ||= @uri.path.present? ? @uri.path : '/'
20
- config.reject! { |_,v| v.nil? }
16
+ config.reject! { |_, v| v.nil? }
21
17
 
22
18
  @config = config
23
- end
19
+ @config[:max] ||= 2
24
20
 
25
- def running?; @running end
26
- def log_name; self.class.name end
21
+ @mutex = ::Mutex.new
22
+ @routes = Routes.new
23
+ @pid ||= ::Process.pid
27
24
 
28
- def routes
29
- @routes ||= Routes.new
25
+ log(:info) { "Created new client on process ##{@pid}..." }
30
26
  end
31
27
 
32
- def new_connection
33
- @connection = Connection.new @bunny, routes, @pool, prefix: @config[:prefix]
34
- end
35
-
36
- def reload
37
- raise StandardError.new 'Client not running.' unless running?
38
- log(:info) { 'Reload client.' }
39
-
40
- @connection.release
41
- @connection.terminate
42
-
43
- log(:debug) { 'Create new connection.' }
44
- new_connection
45
-
46
- log(:info) { 'Client reloaded.' }
28
+ def running?
29
+ mutex.synchronize do
30
+ check_process!
31
+ connection.running?
32
+ end
47
33
  end
48
34
 
49
35
  def start
50
- log(:info) { "Start client to #{uri}" }
36
+ mutex.synchronize do
37
+ check_process!
38
+ return if connection.running?
51
39
 
52
- init
53
- launch
40
+ log(:info) { "Start on #{uri}..." }
54
41
 
55
- log(:info) { 'Client started.' }
42
+ @routes << config[:routing_file] if config[:routing_file].present?
43
+ @routes.reload
44
+ connection.bind(@routes)
45
+ end
56
46
  end
57
47
 
58
- def stop(opts = {})
59
- return unless running?
60
- opts.reverse_merge! timeout: 10, delete: false
48
+ def stop
49
+ mutex.synchronize do
50
+ check_process!
61
51
 
62
- stop_connection opts
52
+ log(:info) { "Stop on #{uri}..." }
63
53
 
64
- @running = false
65
- wait_for_graceful_shutdown opts if opts[:timeout]
54
+ return unless connection.running?
66
55
 
67
- log(:debug) { 'Terminating...' }
56
+ connection.release
57
+ connection.close
58
+ dispatcher.shutdown
68
59
 
69
- if opts[:delete]
70
- log(:debug) { 'Delete connection.' }
71
- @connection.delete
60
+ reset
72
61
  end
73
- @connection.terminate
74
- @bunny.stop
75
-
76
- log(:info) { 'Terminated.' }
77
62
  end
78
63
 
79
64
  def publish(payload, opts = {})
80
- opts[:routing_key] = opts.delete(:to) if opts[:to]
81
- @connection.publish payload, opts
65
+ mutex.synchronize do
66
+ check_process!
67
+ connection.publish payload, opts
68
+ end
82
69
  end
83
70
 
84
- private
85
- def stop_connection(opts)
86
- timeout = [opts[:wait_empty].to_i, 1].max
87
-
88
- begin
89
- if opts[:wait_empty]
90
- log(:info) { "Shutdown requested: Wait until all queues are empty. (TIMEOUT: #{timeout}s)" }
91
- @connection.future(:release, true).value timeout
92
- else
93
- @connection.future(:release).value timeout
94
- end
95
- rescue TimeoutError
96
- log(:warn) { "Could release connection within #{timeout} seconds." }
71
+ def routes
72
+ mutex.synchronize do
73
+ @routes
74
+ end
75
+ end
76
+
77
+ def release
78
+ mutex.synchronize do
79
+ check_process!
80
+ return unless running?
81
+
82
+ connection.release
97
83
  end
98
84
  end
99
85
 
100
- def wait_for_graceful_shutdown(opts)
101
- timeout = [opts[:timeout].to_i, 0].max
102
- log(:info) { "Attempting graceful shutdown within #{timeout} seconds..." }
86
+ private
87
+ def mutex
88
+ @mutex
89
+ end
90
+
91
+ def check_process!
92
+ unless ::Process.pid == @pid
93
+ log(:warn) { 'Fork detected. Reset internal state...' }
103
94
 
104
- @pool.future(:stop).value timeout
105
- rescue TimeoutError
106
- log(:warn) { "Could not shutdown pool within #{timeout} seconds." }
95
+ reset
96
+ @pid = ::Process.pid
97
+ end
107
98
  end
108
99
 
109
- def init
110
- @bunny = Bunny.new @config
111
- @pool = Pool.new Dispatcher, size: @config[:size]
112
- if @config[:routing_file]
113
- routes.files << @config[:routing_file]
100
+ def connection
101
+ @connection ||= Connection.new(uri, config, dispatcher).tap do
102
+ log(:debug) { 'Created new connection..' }
114
103
  end
104
+ end
115
105
 
116
- routes.reload
106
+ def dispatcher
107
+ @dispatcher ||= Dispatcher.new(config).tap do
108
+ log(:debug) { 'Created new dispatcher..' }
109
+ end
117
110
  end
118
111
 
119
- def launch
120
- @bunny.start
121
- @pool.start
122
- @running = true
123
- new_connection
112
+ def reset
113
+ @connection = nil
114
+ @pool = nil
115
+ @channel = nil
116
+ @subscriptions = nil
124
117
  end
125
118
  end
126
119
  end
@@ -1,125 +1,125 @@
1
+ require 'bunny'
2
+ require 'multi_json'
3
+
1
4
  module Msgr
2
5
 
3
6
  class Connection
4
- include Celluloid
5
7
  include Logging
6
8
 
7
- attr_reader :conn, :dispatcher, :routes, :opts
8
- finalizer :close
9
-
10
- def initialize(conn, routes, dispatcher, opts = {})
11
- @conn = conn
12
- @dispatcher = dispatcher
13
- @routes = routes
14
- @opts = opts
9
+ EXCHANGE_NAME = 'msgr'
15
10
 
16
- @channel = conn.create_channel
17
- @channel.prefetch(10)
11
+ attr_reader :uri, :config
18
12
 
19
- bind
13
+ def initialize(uri, config, dispatcher)
14
+ @uri = uri
15
+ @config = config
16
+ @dispatcher = dispatcher
20
17
  end
21
18
 
22
- def rebind
23
- release
24
- bind
19
+ def running?
20
+ subscriptions.any?
25
21
  end
26
22
 
27
- def bind
28
- # Create new bindings
29
- routes.each { |route| bindings << Binding.new(Actor.current, route, dispatcher) }
23
+ def publish(payload, opts = {})
24
+ opts[:routing_key] = opts.delete(:to) if opts[:to]
25
+
26
+ begin
27
+ payload = MultiJson.dump(payload)
28
+ exchange.publish payload, opts.merge(persistent: true, content_type: 'application/json')
29
+ rescue => error
30
+ exchange.publish payload.to_s, opts.merge(persistent: true, content_type: 'application/text')
31
+ end
30
32
 
31
- log(:debug) { 'New routes bound.' }
33
+ log(:debug) { "Published message to #{opts[:routing_key]}" }
32
34
  end
33
35
 
34
- def prefix(name = '')
35
- opts[:prefix] ? "#{opts[:prefix]}-#{name}" : name
36
+ def connection
37
+ @connection ||= ::Bunny.new(config).tap { |b| b.start }
36
38
  end
37
39
 
38
- # Used to store al bindings. Allows use to
39
- # release bindings when receiver should not longer
40
- # receive messages but channel need to be open
41
- # to allow further acknowledgments.
42
- #
43
- def bindings
44
- @bindings ||= []
40
+ def channel
41
+ @channel ||= connection.create_channel
45
42
  end
46
43
 
47
- def queue(name)
48
- @channel.queue(prefix(name), durable: true).tap do |queue|
49
- log(:debug) { "Create queue #{queue.name} (durable: #{queue.durable?}, auto_delete: #{queue.auto_delete?})" }
50
- end
44
+ def release
45
+ subscriptions.each { |subscription| subscription.cancel }
51
46
  end
52
47
 
53
- def exchange
54
- unless @exchange
55
- @exchange = @channel.topic prefix('msgr'), durable: true
48
+ def subscriptions
49
+ @subscription ||= []
50
+ end
56
51
 
57
- log(:debug) { "Created exchange #{@exchange.name} (type: #{@exchange.type}, durable: #{@exchange.durable?}, auto_delete: #{@exchange.auto_delete?})" }
52
+ def prefix(name)
53
+ if config[:prefix].present?
54
+ "#{config[:prefix]}-#{name}"
55
+ else
56
+ name
58
57
  end
59
-
60
- @exchange
61
58
  end
62
59
 
63
- # Release all bindings but do not close channel. Will not
64
- # longer receive any message but channel can be used to
65
- # acknowledge currently processing messages.
66
- #
67
- def release(wait = false)
68
- return unless bindings.any?
69
-
70
- log(:debug) { "Release all bindings#{wait ? ' after queues are empty': ''}..." }
71
-
72
- if wait
73
- binds = bindings.dup
74
- while binds.any?
75
- binds.reject! { |b| b.release_if_empty }
76
- sleep 1
60
+ def exchange
61
+ @exchange ||= channel.topic(prefix(EXCHANGE_NAME), durable: true).tap do |ex|
62
+ log(:debug) do
63
+ "Created exchange #{ex.name} (type: #{ex.type}, " \
64
+ "durable: #{ex.durable?}, auto_delete: #{ex.auto_delete?})"
77
65
  end
78
- else
79
- bindings.each &:release
80
66
  end
81
-
82
- log(:debug) { 'All bindings released.' }
83
67
  end
84
68
 
85
- def delete
86
- return unless bindings.any?
87
-
88
- log(:debug) { 'Delete all bindings and exchange.' }
89
-
90
- bindings.each { |binding| binding.delete }
91
- bindings.clear
92
-
93
- @exchange.delete if @exchange
69
+ def queue(name)
70
+ channel.queue(prefix(name), durable: true).tap do |queue|
71
+ log(:debug) do
72
+ "Create queue #{queue.name} (durable: #{queue.durable?}, " \
73
+ "auto_delete: #{queue.auto_delete?})"
74
+ end
75
+ end
94
76
  end
95
77
 
96
- def publish(payload, opts = {})
97
- opts[:routing_key] ||= opts[:to]
98
- raise ArgumentError, 'Missing routing key.' unless opts[:routing_key]
99
-
100
- log(:debug) { "Publish message to #{opts[:routing_key]}" }
101
-
102
- begin
103
- payload = JSON.generate(payload)
104
- exchange.publish payload, opts.merge(persistent: true, content_type: 'application/json')
105
- rescue => error
106
- exchange.publish payload.to_s, opts.merge(persistent: true, content_type: 'application/text')
78
+ def bind(routes)
79
+ if routes.empty?
80
+ log(:warn) { "No routes to bound to. Bind will have no effect. (#{routes.inspect})" }
81
+ else
82
+ bind_all(routes)
107
83
  end
108
84
  end
109
85
 
110
86
  def ack(delivery_tag)
111
- log(:debug) { "Ack message: #{delivery_tag}" }
112
- @channel.ack delivery_tag
87
+ channel.ack delivery_tag
88
+ log(:debug) { "Acked message: #{delivery_tag}" }
113
89
  end
114
90
 
115
91
  def reject(delivery_tag, requeue = true)
116
- log(:debug) { "Reject message: #{delivery_tag}" }
117
- @channel.reject delivery_tag, requeue
92
+ channel.reject delivery_tag, requeue
93
+ log(:debug) { "Rejected message: #{delivery_tag}" }
118
94
  end
119
95
 
120
96
  def close
121
- @channel.close if @channel.open?
122
- log(:debug) { 'Connection closed.' }
97
+ channel.close if @channel && @channel.open?
98
+ connection.close if @connection
99
+ log(:debug) { 'Closed.' }
100
+ end
101
+
102
+ private
103
+ def bind_all(routes)
104
+ routes.each do |route|
105
+ queue = queue(route.name)
106
+
107
+ route.keys.each do |key|
108
+ log(:debug) { "Bind #{key} to #{queue.name}." }
109
+
110
+ queue.bind exchange, routing_key: key
111
+ end
112
+
113
+ subscriptions << queue.subscribe(ack: true) do |*args|
114
+ begin
115
+ @dispatcher.call Message.new(self, *args, route)
116
+ rescue => err
117
+ log(:error) do
118
+ "Rescued error from subscribe: #{err.class.name}: #{err}\n#{err.backtrace.join("\n")}"
119
+ end
120
+ end
121
+ end
122
+ end
123
123
  end
124
124
  end
125
125
  end
@@ -1,3 +1,5 @@
1
+ require 'concurrent/cached_thread_pool'
2
+
1
3
  module Msgr
2
4
 
3
5
  # The Dispatcher receives incoming messages,
@@ -7,13 +9,18 @@ module Msgr
7
9
  class Dispatcher
8
10
  include Logging
9
11
 
10
- def call(message)
11
- dispatch message
12
+ attr_reader :pool
12
13
 
13
- # Acknowledge message unless it is already acknowledged.
14
- message.ack unless message.acked?
14
+ def initialize(config)
15
+ log(:info) { "Initialize new dispatcher (#{Rails.env})..." }
15
16
 
16
- log(:warn) { "Message not acked!" } unless message.acked?
17
+ @pool = ::Concurrent::CachedThreadPool.new(max: config[:max])
18
+ end
19
+
20
+ def call(message)
21
+ pool.post(message) do |message|
22
+ dispatch message
23
+ end
17
24
  end
18
25
 
19
26
  def dispatch(message)
@@ -22,8 +29,18 @@ module Msgr
22
29
  log(:debug) { "Dispatch message to #{consumer_class.name}" }
23
30
 
24
31
  consumer_class.new.dispatch message
32
+
33
+ # Acknowledge message unless it is already acknowledged.
34
+ message.ack unless message.acked?
25
35
  rescue => error
26
- log(:error) { error }
36
+ log(:error) do
37
+ "Dispatcher error: #{error.class.name}: #{error}\n" +
38
+ error.backtrace.join("\n")
39
+ end
40
+ end
41
+
42
+ def shutdown
43
+
27
44
  end
28
45
 
29
46
  def to_s
data/lib/msgr/logging.rb CHANGED
@@ -2,11 +2,11 @@ module Msgr
2
2
 
3
3
  module Logging
4
4
  def log(level)
5
- Msgr.logger.send(level, self.log_name) { yield } if Msgr.logger
5
+ Msgr.logger.send(level) { "#{self.log_name} #{yield}" } if Msgr.logger
6
6
  end
7
7
 
8
8
  def log_name
9
- "[#{Thread.current.object_id}] #{self.to_s}"
9
+ "[#{Thread.current.object_id.to_s(16)}] <#{self.class.name}[#{object_id.to_s(16)}]>"
10
10
  end
11
11
  end
12
12
  end
data/lib/msgr/message.rb CHANGED
@@ -1,9 +1,6 @@
1
1
  module Msgr
2
2
 
3
- require 'msgr/message/acknowledge'
4
-
5
3
  class Message
6
- include Acknowledge
7
4
  attr_reader :delivery_info, :metadata, :payload, :route
8
5
 
9
6
  def initialize(connection, delivery_info, metadata, payload, route)
@@ -14,12 +11,34 @@ module Msgr
14
11
  @route = route
15
12
 
16
13
  if content_type == 'application/json'
17
- @payload = JSON[payload].symbolize_keys
14
+ @payload = MultiJson.load(payload)
15
+ @payload.symbolize_keys! if @payload.respond_to? :symbolize_keys!
18
16
  end
19
17
  end
20
18
 
21
19
  def content_type
22
20
  @metadata.content_type
23
21
  end
22
+
23
+ # Check if message is already acknowledged.
24
+ #
25
+ # @return [Boolean] True if message is acknowledged, false otherwise.
26
+ # @api public
27
+ #
28
+ def acked?
29
+ @acked ? true : false
30
+ end
31
+
32
+ # Send message acknowledge to broker unless message is
33
+ # already acknowledged.
34
+ #
35
+ # @api public
36
+ #
37
+ def ack
38
+ unless acked?
39
+ @acked = true
40
+ @connection.ack(delivery_info.delivery_tag)
41
+ end
42
+ end
24
43
  end
25
44
  end
data/lib/msgr/railtie.rb CHANGED
@@ -16,7 +16,6 @@ module Msgr
16
16
  initializer 'msgr.start' do
17
17
  config.after_initialize do |app|
18
18
  Msgr.logger = app.config.msgr.logger
19
- Celluloid.logger = app.config.msgr.logger
20
19
 
21
20
  self.class.load app.config.msgr
22
21
  end
@@ -28,26 +27,6 @@ module Msgr
28
27
  return unless cfg # no config given -> does not load Msgr
29
28
 
30
29
  Msgr.config = cfg
31
-
32
- # later loading for e.g. unicorn
33
- if Rails.env.development?
34
- Msgr.after_load do |client|
35
- setup_autoreload client
36
- end
37
- end
38
-
39
- Msgr.start if cfg[:autostart]
40
- end
41
-
42
- def setup_autoreload(client)
43
- reloader = ActiveSupport::FileUpdateChecker.new client.routes.files do
44
- client.routes.reload
45
- client.reload
46
- end
47
-
48
- ActionDispatch::Reloader.to_prepare do
49
- reloader.execute_if_updated
50
- end
51
30
  end
52
31
 
53
32
  def parse_config(cfg)
data/lib/msgr/routes.rb CHANGED
@@ -1,14 +1,16 @@
1
1
  module Msgr
2
2
 
3
3
  class Routes
4
+ include Logging
4
5
  attr_reader :routes
5
- delegate :each, to: :@routes
6
+ delegate :each, :empty?, :size, :any?, to: :@routes
6
7
 
7
8
  def initialize
8
9
  @routes = []
9
10
  end
10
11
 
11
12
  def configure(&block)
13
+ blocks << block
12
14
  instance_eval &block
13
15
  end
14
16
 
@@ -16,17 +18,27 @@ module Msgr
16
18
  @files ||= []
17
19
  end
18
20
 
21
+ def blocks
22
+ @blocks ||= []
23
+ end
24
+
19
25
  def files=(files)
20
26
  @files = Array files
21
27
  end
22
28
 
29
+ def <<(file)
30
+ files << file
31
+ end
32
+
23
33
  def reload
24
34
  routes.clear
35
+ blocks.each { |block| instance_eval(&block) }
36
+ files.uniq!
25
37
  files.each do |file|
26
38
  if File.exists? file
27
39
  load file
28
40
  else
29
- Msgr.logger.warn "Routes file `#{file}` does not exists (anymore)."
41
+ log(:warn) { "Routes file `#{file}` does not exists (anymore)." }
30
42
  end
31
43
  end
32
44
  end
data/lib/msgr/version.rb CHANGED
@@ -1,8 +1,8 @@
1
1
  module Msgr
2
2
  module VERSION
3
3
  MAJOR = 0
4
- MINOR = 4
5
- PATCH = 1
4
+ MINOR = 5
5
+ PATCH = 0
6
6
  STAGE = nil
7
7
  STRING = [MAJOR, MINOR, PATCH, STAGE].reject(&:nil?).join('.')
8
8
 
data/msgr.gemspec CHANGED
@@ -20,7 +20,8 @@ Gem::Specification.new do |spec|
20
20
 
21
21
  spec.add_dependency 'activesupport'
22
22
  spec.add_dependency 'bunny', '~> 1.0'
23
- spec.add_dependency 'celluloid'
23
+ spec.add_dependency 'concurrent-ruby'
24
+ spec.add_dependency 'multi_json'
24
25
 
25
26
  spec.add_development_dependency 'bundler', '~> 1.3'
26
27
  end
@@ -8,7 +8,7 @@ end
8
8
 
9
9
  describe Msgr do
10
10
  before do
11
- Msgr.logger = nil;
11
+ Msgr.logger = nil
12
12
  Msgr.logger.level = Logger::Severity::DEBUG if Msgr.logger
13
13
  end
14
14
 
@@ -23,22 +23,14 @@ describe Msgr do
23
23
  end
24
24
 
25
25
  after do
26
- client.stop timeout: 10, delete: true, wait_empty: 10
26
+ client.stop
27
27
  end
28
28
 
29
29
  it 'should dispatch published methods to consumer' do
30
- expect_any_instance_of(TestConsumer).to receive(:index).within(10).seconds.and_call_original
30
+ expect_any_instance_of(TestConsumer).to receive(:index).seconds.and_call_original
31
31
 
32
32
  client.publish 'Payload', to: 'routing.key'
33
33
 
34
- sleep 10
35
- end
36
-
37
- describe '.after_load' do
38
- before { allow_any_instance_of(Msgr::Client).to receive(:launch) }
39
-
40
- it 'should yield the given block when Msgr.start is called' do
41
- expect { |cb| Msgr.after_load &cb; Msgr.start }.to yield_with_args
42
- end
34
+ sleep 4
43
35
  end
44
36
  end
@@ -22,12 +22,6 @@ RSpec.configure do |config|
22
22
  end
23
23
 
24
24
  config.before do
25
- Celluloid.logger = nil
26
-
27
- Celluloid.shutdown
28
- Celluloid.boot
29
-
30
- Celluloid.logger = nil
31
25
  Msgr.logger = false
32
26
  end
33
27
  end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: msgr
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.4.1
4
+ version: 0.5.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Jan Graichen
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2014-03-18 00:00:00.000000000 Z
11
+ date: 2014-03-20 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: activesupport
@@ -39,7 +39,21 @@ dependencies:
39
39
  - !ruby/object:Gem::Version
40
40
  version: '1.0'
41
41
  - !ruby/object:Gem::Dependency
42
- name: celluloid
42
+ name: concurrent-ruby
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: multi_json
43
57
  requirement: !ruby/object:Gem::Requirement
44
58
  requirements:
45
59
  - - ">="
@@ -84,7 +98,6 @@ files:
84
98
  - gemfiles/Gemfile.rails-3-2
85
99
  - gemfiles/Gemfile.rails-4-0
86
100
  - lib/msgr.rb
87
- - lib/msgr/binding.rb
88
101
  - lib/msgr/client.rb
89
102
  - lib/msgr/connection.rb
90
103
  - lib/msgr/consumer.rb
@@ -92,8 +105,6 @@ files:
92
105
  - lib/msgr/errors.rb
93
106
  - lib/msgr/logging.rb
94
107
  - lib/msgr/message.rb
95
- - lib/msgr/message/acknowledge.rb
96
- - lib/msgr/pool.rb
97
108
  - lib/msgr/railtie.rb
98
109
  - lib/msgr/route.rb
99
110
  - lib/msgr/routes.rb
@@ -148,10 +159,8 @@ files:
148
159
  - spec/integration/msgr/railtie_spec.rb
149
160
  - spec/integration/msgr_spec.rb
150
161
  - spec/integration/spec_helper.rb
151
- - spec/msgr/msgr/client_spec.rb
152
162
  - spec/msgr/msgr/connection_spec.rb
153
163
  - spec/msgr/msgr/consumer_spec.rb
154
- - spec/msgr/msgr/pool_spec.rb
155
164
  - spec/msgr/msgr/route_spec.rb
156
165
  - spec/msgr/msgr/routes_spec.rb
157
166
  - spec/msgr/msgr_spec.rb
@@ -231,10 +240,8 @@ test_files:
231
240
  - spec/integration/msgr/railtie_spec.rb
232
241
  - spec/integration/msgr_spec.rb
233
242
  - spec/integration/spec_helper.rb
234
- - spec/msgr/msgr/client_spec.rb
235
243
  - spec/msgr/msgr/connection_spec.rb
236
244
  - spec/msgr/msgr/consumer_spec.rb
237
- - spec/msgr/msgr/pool_spec.rb
238
245
  - spec/msgr/msgr/route_spec.rb
239
246
  - spec/msgr/msgr/routes_spec.rb
240
247
  - spec/msgr/msgr_spec.rb
data/lib/msgr/binding.rb DELETED
@@ -1,61 +0,0 @@
1
- module Msgr
2
- # A single binding
3
- class Binding
4
- include Logging
5
- attr_reader :connection, :route, :subscription, :dispatcher, :queue
6
-
7
- def initialize(connection, route, dispatcher)
8
- @connection = connection
9
- @route = route
10
- @dispatcher = dispatcher
11
-
12
- exchange = connection.exchange
13
- @queue = connection.queue route.name
14
-
15
- route.keys.each do |key|
16
- log(:debug) { "Bind #{key} to #{@queue.name}." }
17
-
18
- queue.bind exchange, routing_key: key
19
- end
20
-
21
- @subscription = queue.subscribe(ack: true) { |*args| call *args }
22
- end
23
-
24
- # Called from Bunny Thread Pool. Will create message object from
25
- # provided bunny data and dispatch message to connection.
26
- #
27
- def call(info, metadata, payload)
28
- dispatcher.dispatch Message.new(connection, info, metadata, payload, route)
29
- rescue => error
30
- log(:error) { "Error received within subscribe handler: #{error.inspect}\n#{error.backtrace.join("\n ")}" }
31
- end
32
-
33
- # Cancel subscription to not receive any more messages.
34
- #
35
- def release
36
- subscription.cancel if subscription
37
- end
38
-
39
- def release_if_empty
40
- if queue_empty?
41
- release
42
- true
43
- else
44
- false
45
- end
46
- end
47
-
48
- def queue_empty?
49
- @queue.message_count == 0
50
- end
51
-
52
- def delete
53
- release
54
- queue.delete
55
- end
56
-
57
- def purge
58
- queue.purge
59
- end
60
- end
61
- end
@@ -1,30 +0,0 @@
1
- module Msgr
2
- class Message
3
-
4
- #
5
- #
6
- module Acknowledge
7
-
8
- # Check if message is already acknowledged.
9
- #
10
- # @return [Boolean] True if message is acknowledged, false otherwise.
11
- # @api public
12
- #
13
- def acked?
14
- @acked ? true : false
15
- end
16
-
17
- # Send message acknowledge to broker unless message is
18
- # already acknowledged.
19
- #
20
- # @api public
21
- #
22
- def ack
23
- unless acked?
24
- @acked = true
25
- @connection.future(:ack, delivery_info.delivery_tag).value
26
- end
27
- end
28
- end
29
- end
30
- end
data/lib/msgr/pool.rb DELETED
@@ -1,165 +0,0 @@
1
- module Msgr
2
-
3
- class Pool
4
- include Celluloid
5
- include Logging
6
- attr_reader :size
7
-
8
- def initialize(runner_klass, opts = {})
9
- @runner_klass = runner_klass
10
- @runner_args = opts[:args] ? Array(opts[:args]) : []
11
- @size = (opts[:size] || Celluloid.cores).to_i
12
- @running = false
13
-
14
- raise ArgumentError.new 'Pool size must be greater zero.' if @size <= 0
15
-
16
- log(:debug) { "Inialize size => #{@size}" }
17
-
18
- every([opts.fetch(:stats_interval, 30).to_i, 1].max) { log_status } if opts[:nostats].nil? || opts[:nostats]
19
- end
20
-
21
- def running?
22
- @running
23
- end
24
-
25
- def idle; @idle ||= [] end
26
- def busy; @busy ||= [] end
27
-
28
- def start
29
- return if running?
30
-
31
- log(:debug) { 'Spin up worker pool' }
32
- @running = true
33
-
34
- idle.clear
35
- busy.clear
36
-
37
- @size.times.map do |index|
38
- idle << Worker.new_link(Actor.current, index, @runner_klass, @runner_args)
39
- end
40
-
41
- log(:debug) { 'Pool ready.' }
42
- end
43
-
44
- def log_status
45
- log(:info) { "[STATUS] Idle: #{idle.size} Busy: #{busy.size}" }
46
- end
47
-
48
- # Request a graceful shutdown of all pool workers.
49
- #
50
- def stop
51
- log(:debug) { 'Graceful shutdown requested.' }
52
-
53
- @running = false
54
- idle.each { |worker| worker.terminate }
55
- idle.clear
56
-
57
- if busy.any?
58
- log(:debug) { "Wait for #{busy.size} workers to terminate." }
59
-
60
- wait :shutdown
61
- end
62
-
63
- log(:debug) { 'Graceful shutdown done.' }
64
- end
65
- alias_method :shutdown, :stop
66
-
67
- # Check if a worker is available.
68
- #
69
- # @return [Boolean] True if at least on idle worker is available, false otherwise.
70
- #
71
- def available?
72
- idle.any?
73
- end
74
-
75
- def messages
76
- @message ||= []
77
- end
78
-
79
- # Dispatch given message to a worker.
80
- #
81
- def dispatch(*args)
82
- log(:debug) { 'Dispatch message to worker.' }
83
-
84
- fetch_idle_worker.future :dispatch, args
85
- end
86
-
87
- # Return an idle worker.
88
- #
89
- def fetch_idle_worker
90
- if (worker = idle.shift)
91
- busy << worker
92
- worker
93
- else
94
- wait :worker_done
95
- fetch_idle_worker
96
- end
97
- end
98
-
99
- # Called by worker to indicated it has finished processing.
100
- #
101
- # @param [Pool::Worker] worker Worker that finished processing.
102
- #
103
- def executed(worker)
104
- busy.delete worker
105
-
106
- if running?
107
- idle << worker
108
- after(0) { signal :worker_done }
109
- else
110
- log(:debug) { "Terminate worker. Still #{busy.size} to go..." }
111
-
112
- worker.terminate if worker.alive?
113
- if busy.empty?
114
- log(:debug) { 'All worker down. Signal :shutdown.' }
115
- after(0) { signal :shutdown }
116
- end
117
- end
118
- end
119
-
120
- def to_s
121
- "#{self.class.name}[#{@runner_klass}]<#{object_id}>"
122
- end
123
-
124
- # Worker actor capsuling worker logic and dispatching
125
- # tasks to custom runner object.
126
- #
127
- class Worker
128
- include Celluloid
129
- include Logging
130
- attr_reader :pool, :index, :runner
131
-
132
- def initialize(pool, index, runner_klass, runner_args)
133
- @pool = pool
134
- @poolname = pool.to_s
135
- @index = index
136
- @runner = runner_klass.new *runner_args
137
-
138
- log(:debug) { 'Worker ready.' }
139
- end
140
-
141
- # Dispatch given method and argument to custom runner.
142
- # Arguments are used to call `#send` on runner instance.
143
- #
144
- def dispatch(args)
145
- log(:debug) { "Dispatch to runner: #{runner.class.name}" }
146
-
147
- # Send method to custom runner.
148
- runner.send :call, *args
149
-
150
- rescue => error
151
- log(:error) { "Received error from runner: #{error.message}\n#{error.backtrace.join(" \n")}" }
152
- ensure
153
- if pool.alive?
154
- pool.executed Actor.current
155
- else
156
- terminate
157
- end
158
- end
159
-
160
- def to_s
161
- "#{@poolname}[##{index}]"
162
- end
163
- end
164
- end
165
- end
@@ -1,58 +0,0 @@
1
- require 'spec_helper'
2
-
3
- describe Msgr::Client do
4
-
5
- describe '#start' do
6
- let(:params) { [] }
7
- let(:client) { Msgr::Client.new *params }
8
- before { allow_any_instance_of(Msgr::Client).to receive(:launch) }
9
-
10
- context 'with URI' do
11
- it 'should pass URI options to bunny (I)' do
12
- expect(Bunny).to receive(:new)
13
- .with(pass: 'guest', user: 'guest', ssl: false, host: 'localhost', vhost: '/')
14
-
15
- Msgr::Client.new(uri: 'amqp://guest:guest@localhost/').start
16
- end
17
-
18
- it 'should pass URI options to bunny (II)' do
19
- expect(Bunny).to receive(:new)
20
- .with(pass: 'msgr', user: 'abc', ssl: true, host: 'bogus.example.org', vhost: '/rabbit')
21
-
22
- Msgr::Client.new(uri: 'amqps://abc:msgr@bogus.example.org/rabbit').start
23
- end
24
- end
25
-
26
- context 'with options' do
27
- it 'should pass options to bunny' do
28
- expect(Bunny).to receive(:new)
29
- .with(pass: 'guest', user: 'guest', ssl: false, host: 'localhost', vhost: '/')
30
-
31
- Msgr::Client.new(pass: 'guest', user: 'guest', ssl: false, host: 'localhost', vhost: '/').start
32
- end
33
- end
34
-
35
- context 'with URI and options' do
36
- it 'should pass merged options to bunny' do
37
- expect(Bunny).to receive(:new)
38
- .with(pass: 'msgr', user: 'abc', ssl: false, host: 'localhost', vhost: '/joghurt')
39
-
40
- Msgr::Client.new(uri: 'ampq://abc@localhost', pass: 'msgr', vhost: '/joghurt').start
41
- end
42
-
43
- it 'should pass prefer hash option' do
44
- expect(Bunny).to receive(:new)
45
- .with(ssl: true, host: 'a.example.org', vhost: '/', user: 'guest')
46
-
47
- Msgr::Client.new(uri: 'ampq://localhost', ssl: true, host: 'a.example.org').start
48
- end
49
- end
50
-
51
- context 'routes' do
52
- let(:params) { [ routing_file: 'config/msgr.rb']}
53
- before { client.start }
54
- subject { client.routes }
55
- its(:files) { should eq ['config/msgr.rb'] }
56
- end
57
- end
58
- end
@@ -1,68 +0,0 @@
1
- require 'spec_helper'
2
-
3
- $shutdown_test_graceful_down = false
4
-
5
- class Runner
6
- def call(*_) end
7
-
8
- def shutdown_test
9
- sleep 2
10
- $shutdown_test_graceful_down = true
11
- end
12
- end
13
-
14
- describe Msgr::Pool do
15
- let(:pool) { Msgr::Pool.new Runner }
16
-
17
- describe '#initialize' do
18
- let(:opts) { {} }
19
- let(:pool) { Msgr::Pool.new Runner, opts }
20
-
21
- context 'pool size' do
22
- let(:opts) { {size: 4} }
23
- before { pool }
24
-
25
- it 'should define number of worker actors' do
26
- expect(pool.size).to eq 4
27
- end
28
- end
29
- end
30
-
31
- describe '#start' do
32
- let!(:pool) { Msgr::Pool.new Runner, size: 4 }
33
-
34
- it 'should start worker actors' do
35
- expect { pool.start }.to change { Celluloid::Actor.all.size }.by(4)
36
- end
37
- end
38
-
39
- describe '#size' do
40
- it 'should default to number of available cores' do
41
- expect(pool.size).to eq Celluloid.cores
42
- end
43
- end
44
-
45
- describe '#dispatch' do
46
- let!(:pool) { Msgr::Pool.new Runner, size: 4 }
47
- before { pool.start }
48
-
49
- it 'should dispatch message to runner' do
50
- expect_any_instance_of(Runner).to receive(:call).within(10).seconds.once
51
- pool.dispatch 5, 3.2, 'hello'
52
- end
53
- end
54
-
55
- describe '#shutdown' do
56
- let!(:pool) { Msgr::Pool.new Runner, size: 1 }
57
- before do
58
- pool.start
59
- $shutdown_test_graceful_down = false
60
- end
61
-
62
- it 'should do a graceful shutdown of all worker' do
63
- pool.dispatch :shutdown_test
64
- pool.shutdown
65
- expect($shutdown_test_graceful_down).to be_false
66
- end
67
- end
68
- end