msgr 0.0.0 → 0.1.0

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.
Files changed (85) hide show
  1. checksums.yaml +4 -4
  2. data/.gitignore +7 -1
  3. data/.travis.yml +5 -3
  4. data/Gemfile +27 -2
  5. data/Guardfile +14 -0
  6. data/README.md +56 -5
  7. data/Rakefile +30 -4
  8. data/lib/msgr/binding.rb +34 -7
  9. data/lib/msgr/client.rb +54 -24
  10. data/lib/msgr/connection.rb +64 -17
  11. data/lib/msgr/consumer.rb +28 -0
  12. data/lib/msgr/dispatcher.rb +12 -5
  13. data/lib/msgr/logging.rb +1 -1
  14. data/lib/msgr/message/acknowledge.rb +30 -0
  15. data/lib/msgr/message.rb +10 -3
  16. data/lib/msgr/pool.rb +31 -31
  17. data/lib/msgr/railtie.rb +51 -0
  18. data/lib/msgr/route.rb +19 -6
  19. data/lib/msgr/routes.rb +34 -2
  20. data/lib/msgr/version.rb +1 -1
  21. data/lib/msgr.rb +15 -7
  22. data/msgr.gemspec +2 -5
  23. data/scripts/simple_test.rb +18 -94
  24. data/spec/fixtures/msgr-routes-test-1.rb +2 -0
  25. data/spec/integration/dummy/README.rdoc +28 -0
  26. data/spec/integration/dummy/Rakefile +6 -0
  27. data/spec/integration/dummy/app/assets/images/.keep +0 -0
  28. data/spec/integration/dummy/app/assets/javascripts/application.js +13 -0
  29. data/spec/integration/dummy/app/assets/stylesheets/application.css +13 -0
  30. data/spec/integration/dummy/app/consumers/application_consumer.rb +3 -0
  31. data/spec/integration/dummy/app/consumers/test_consumer.rb +12 -0
  32. data/spec/integration/dummy/app/controllers/application_controller.rb +5 -0
  33. data/spec/integration/dummy/app/controllers/concerns/.keep +0 -0
  34. data/spec/integration/dummy/app/controllers/test_controller.rb +10 -0
  35. data/spec/integration/dummy/app/helpers/application_helper.rb +2 -0
  36. data/spec/integration/dummy/app/mailers/.keep +0 -0
  37. data/spec/integration/dummy/app/models/.keep +0 -0
  38. data/spec/integration/dummy/app/models/concerns/.keep +0 -0
  39. data/spec/integration/dummy/app/views/layouts/application.html.erb +14 -0
  40. data/spec/integration/dummy/bin/bundle +3 -0
  41. data/spec/integration/dummy/bin/rails +4 -0
  42. data/spec/integration/dummy/bin/rake +4 -0
  43. data/spec/integration/dummy/config/application.rb +24 -0
  44. data/spec/integration/dummy/config/boot.rb +5 -0
  45. data/spec/integration/dummy/config/database.yml +25 -0
  46. data/spec/integration/dummy/config/environment.rb +5 -0
  47. data/spec/integration/dummy/config/environments/development.rb +29 -0
  48. data/spec/integration/dummy/config/environments/production.rb +80 -0
  49. data/spec/integration/dummy/config/environments/test.rb +36 -0
  50. data/spec/integration/dummy/config/initializers/backtrace_silencers.rb +7 -0
  51. data/spec/integration/dummy/config/initializers/filter_parameter_logging.rb +4 -0
  52. data/spec/integration/dummy/config/initializers/inflections.rb +16 -0
  53. data/spec/integration/dummy/config/initializers/mime_types.rb +5 -0
  54. data/spec/integration/dummy/config/initializers/secret_token.rb +12 -0
  55. data/spec/integration/dummy/config/initializers/session_store.rb +3 -0
  56. data/spec/integration/dummy/config/initializers/wrap_parameters.rb +14 -0
  57. data/spec/integration/dummy/config/locales/en.yml +23 -0
  58. data/spec/integration/dummy/config/msgr.rb +3 -0
  59. data/spec/integration/dummy/config/rabbitmq.yml +11 -0
  60. data/spec/integration/dummy/config/routes.rb +58 -0
  61. data/spec/integration/dummy/config.ru +4 -0
  62. data/spec/integration/dummy/db/test.sqlite3 +0 -0
  63. data/spec/integration/dummy/lib/assets/.keep +0 -0
  64. data/spec/integration/dummy/log/.keep +0 -0
  65. data/spec/integration/dummy/public/404.html +58 -0
  66. data/spec/integration/dummy/public/422.html +58 -0
  67. data/spec/integration/dummy/public/500.html +57 -0
  68. data/spec/integration/dummy/public/favicon.ico +0 -0
  69. data/spec/integration/msgr/railtie_spec.rb +12 -0
  70. data/spec/integration/msgr_spec.rb +12 -0
  71. data/spec/integration/spec_helper.rb +48 -0
  72. data/spec/msgr/msgr/client_spec.rb +50 -0
  73. data/spec/msgr/msgr/connection_spec.rb +51 -0
  74. data/spec/msgr/{consumer_spec.rb → msgr/consumer_spec.rb} +0 -0
  75. data/spec/msgr/msgr/pool_spec.rb +68 -0
  76. data/spec/msgr/{route_spec.rb → msgr/route_spec.rb} +0 -0
  77. data/spec/msgr/msgr/routes_spec.rb +113 -0
  78. data/spec/msgr/msgr_spec.rb +36 -0
  79. data/spec/{spec_helper.rb → msgr/spec_helper.rb} +15 -0
  80. data/spec/msgr/support/.keep +0 -0
  81. data/spec/support/setup.rb +27 -0
  82. metadata +126 -59
  83. data/spec/msgr/client_spec.rb +0 -5
  84. data/spec/msgr/routes_spec.rb +0 -59
  85. data/spec/msgr_spec.rb +0 -5
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: 5a4c0e2fbb4ebe739cd8441da5136c32974e33e3
4
- data.tar.gz: 8cd8950010d87d4c3a4c90fa26a2f4544bfd1908
3
+ metadata.gz: e8d6c4cc8a9208899a92286b5ee2be798aceb0bd
4
+ data.tar.gz: 5c3c2a20221a8da75f8aa4ab81e0e792daf15ab3
5
5
  SHA512:
6
- metadata.gz: 6b8268da9f156fa7be5b4da0768e766d90242d60186aa7a79d9d229270ac5957bf0e498c5edbb963bc8d0abb759e91545536e23444ff4ce6ab25bc8931518c96
7
- data.tar.gz: 5af9d36176f5b99c4249c89a2819d4ab3682c90320d44a0c219b077ed2d3d4f7d9f6dd6e0899452d7d1c3b390ea89a2fdf53f9f2e41c91cb9cd58b55c24598db
6
+ metadata.gz: 48370c17631a999f1bf475ec869944d95868486d68d7e28cc3020947fa064592c8d0f98c5072acc285af13cad4624524ca66616af5ecadf964ce7bca7da73eac
7
+ data.tar.gz: 35bf1311b013e5a5a15ec733a0daa343b83bca70b2fd7b49700b4988afc94c233cec0fd25a7afbf324dc633ed300e4580cf8b3494a622c38fff98aa8856e449a
data/.gitignore CHANGED
@@ -20,4 +20,10 @@ pmip
20
20
  *.iml
21
21
  .rbx
22
22
  .rspec
23
- log/*.log
23
+ *.log
24
+ spec/integration/dummy/db/*.sqlite3
25
+ spec/integration/dummy/db/*.sqlite3-journal
26
+ spec/integration/dummy/tmp
27
+ spec/integration/dummy/.sass-cache
28
+ spec/integration/dummy/.generators
29
+ spec/integration/dummy/.rakeTasks
data/.travis.yml CHANGED
@@ -1,14 +1,16 @@
1
1
  language: ruby
2
2
  bundler_args: --without development
3
+ services:
4
+ - rabbitmq
3
5
  rvm:
4
6
  - 2.0.0
5
7
  - 1.9.3
6
8
  - jruby
7
9
  - rbx-19mode
8
-
9
10
  gemfile:
10
11
  - gemfiles/Gemfile.rails-3-2
11
12
  - gemfiles/Gemfile.rails-4-0
12
13
 
13
- services:
14
- - rabbitmq
14
+ script:
15
+ - bundle exec rake spec:msgr
16
+ - bundle exec rake spec:integration
data/Gemfile CHANGED
@@ -1,4 +1,29 @@
1
1
  source 'https://rubygems.org'
2
2
 
3
- # Specify your gem's dependencies in msgr.gemspec
4
- gemspec
3
+ # Development gems
4
+ #
5
+ gem 'rake'
6
+ gem 'rspec'
7
+ gem 'rspec-message-within', '>= 1.1.1'
8
+ gem 'fuubar'
9
+ gem 'coveralls'
10
+
11
+ # Doc
12
+ group :development do
13
+ gem 'yard', '~> 0.8.6'
14
+ gem 'listen'
15
+ gem 'guard-yard'
16
+ gem 'guard-rspec'
17
+ gem 'redcarpet', platform: :ruby
18
+ end
19
+
20
+ group :rails do
21
+ gem 'rails'
22
+ gem 'rspec-rails'
23
+ gem 'sqlite3', platform: :ruby
24
+ gem 'activerecord-jdbcsqlite3-adapter', platform: :jruby
25
+ end
26
+
27
+ # Specify your gem's dependencies in acfs.gemspec
28
+ gemroot = File.dirname File.absolute_path __FILE__
29
+ gemspec path: gemroot
data/Guardfile ADDED
@@ -0,0 +1,14 @@
1
+ # A sample Guardfile
2
+ # More info at https://github.com/guard/guard#readme
3
+
4
+ guard 'rspec' do
5
+ watch(%r{^spec/.+_spec\.rb$})
6
+ watch(%r{^lib/(.+)\.rb$}) { |m| "spec/#{m[1]}_spec.rb" }
7
+
8
+ watch('spec/spec_helper.rb') { 'spec' }
9
+ watch(%r{^spec/support/(.+)\.rb$}) { 'spec' }
10
+ end
11
+
12
+ guard 'yard' do
13
+ watch(%r{lib/.+\.rb})
14
+ end
data/README.md CHANGED
@@ -1,14 +1,14 @@
1
- # Msgr - *A Rails-like Messaging Framework*
1
+ # Msgr: *Rails-like Messaging Framework*
2
2
 
3
3
  [![Gem Version](https://badge.fury.io/rb/msgr.png)](http://badge.fury.io/rb/msgr)
4
4
  [![Build Status](https://travis-ci.org/jgraichen/msgr.png?branch=master)](https://travis-ci.org/jgraichen/msgr)
5
5
  [![Coverage Status](https://coveralls.io/repos/jgraichen/msgr/badge.png?branch=master)](https://coveralls.io/r/jgraichen/msgr)
6
6
  [![Code Climate](https://codeclimate.com/github/jgraichen/msgr.png)](https://codeclimate.com/github/jgraichen/msgr)
7
7
  [![Dependency Status](https://gemnasium.com/jgraichen/msgr.png)](https://gemnasium.com/jgraichen/msgr)
8
- [![RubyDoc Documentation](https://raw.github.com/jgraichen/msgr/master/rubydoc.png)](http://rubydoc.info/github/jgraichen/msgr/master/frames)
8
+ [![RubyDoc Documentation](https://raw.github.com/jgraichen/shields/master/rubydoc.png)](http://rubydoc.info/github/jgraichen/msgr/master/frames)
9
9
 
10
- You know it and ou like it. Using Rails you can just declare your routes and
11
- create a controller. Now it just works.
10
+ You know it and you like it. Using Rails you can just declare your routes and
11
+ create a controller. That's all you need to process requests.
12
12
 
13
13
  With *Msgr* you can do the same for asynchronous AMQP messaging. Just define
14
14
  your routes, create your consumer and watch you app processing messages.
@@ -31,7 +31,58 @@ Or install it yourself as:
31
31
 
32
32
  ## Usage
33
33
 
34
- TODO: Write usage instructions here
34
+ After adding 'msgr' to your gemfile create a `config/rabbitmq.yml` like this:
35
+
36
+ ```yaml
37
+ common: &common
38
+ uri: amqp://localhost/
39
+
40
+ test:
41
+ <<: *common
42
+
43
+ development:
44
+ <<: *common
45
+
46
+ production:
47
+ <<: *common
48
+ ```
49
+
50
+ Specify your messaging routes in `config/msgr.rb`:
51
+
52
+ ```ruby
53
+ route 'local.test.index', to: 'test#index'
54
+ route 'local.test.another_action', to: 'test#another_action'
55
+ ```
56
+
57
+ Create your consumer in `app/consumers`:
58
+
59
+ ```ruby
60
+ class TestConsumer < ApplicationConsumer
61
+ def index
62
+ data = { fuubar: 'abc' }
63
+
64
+ publish data, to: 'local.test.another_action'
65
+ end
66
+
67
+ def another_action
68
+ puts "#{payload.inspect}"
69
+ end
70
+ end
71
+ ```
72
+
73
+ Use `Msgr.publish` in to publish a message:
74
+
75
+ ```ruby
76
+ class TestController < ApplicationController
77
+ def index
78
+ @data = { abc: 'abc' }
79
+
80
+ Msgr.publish @data, to: 'local.test.index'
81
+
82
+ render nothing: true
83
+ end
84
+ end
85
+ ```
35
86
 
36
87
  ## Contributing
37
88
 
data/Rakefile CHANGED
@@ -8,10 +8,36 @@ require 'rake'
8
8
  require 'bundler/gem_tasks'
9
9
  require 'rspec/core/rake_task'
10
10
 
11
- task 'default' => 'ci'
12
- task 'ci' => 'spec'
11
+ # Delegate spec task task to spec:all to run all specs.
12
+ task :spec => 'spec:all'
13
13
 
14
14
  desc 'Run all specs'
15
- RSpec::Core::RakeTask.new('spec') do |t|
16
- t.pattern = 'spec/msgr/**/*_spec.rb'
15
+ namespace :spec do
16
+
17
+ desc 'Run all msgr specs and all integration specs.'
18
+ task :all => [ :msgr, :integration ]
19
+
20
+ desc 'Run all unit specs.'
21
+ RSpec::Core::RakeTask.new(:msgr) do |t|
22
+ t.ruby_opts="#{ENV['CI'] ? '' : '-w -W2'} -Ispec/support -rsetup -Ispec/msgr"
23
+ t.pattern = 'spec/msgr/**/*_spec.rb'
24
+ end
25
+
26
+ desc 'Run all integration specs.'
27
+ RSpec::Core::RakeTask.new(:integration) do |t|
28
+ t.ruby_opts="#{ENV['CI'] ? '' : '-w -W2'} -Ispec/support -rsetup -Ispec/integration"
29
+ t.pattern = 'spec/integration/**/*_spec.rb'
30
+ end
31
+ end
32
+
33
+ begin
34
+ require 'yard'
35
+ require 'yard/rake/yardoc_task'
36
+
37
+ YARD::Rake::YardocTask.new do |t|
38
+ t.files = %w(lib/**/*.rb)
39
+ t.options = %w(--output-dir doc/)
40
+ end
41
+ rescue LoadError
42
+ nil
17
43
  end
data/lib/msgr/binding.rb CHANGED
@@ -1,28 +1,33 @@
1
1
  module Msgr
2
2
  # A single binding
3
3
  class Binding
4
- attr_reader :connection, :route, :subscription, :dispatcher
4
+ include Logging
5
+ attr_reader :connection, :route, :subscription, :dispatcher, :queue
5
6
 
6
7
  def initialize(connection, route, dispatcher)
7
8
  @connection = connection
8
9
  @route = route
9
10
  @dispatcher = dispatcher
10
11
 
11
- queue = connection.queue route.name
12
+ exchange = connection.exchange
13
+ @queue = connection.queue route.name
12
14
 
13
- queue.bind connection.exchange, routing_key: route.key
15
+ route.keys.each do |key|
16
+ log(:debug) { "Bind #{key} to #{@queue.name}." }
14
17
 
15
- @subscription = queue.subscribe ack: true, &method(:call)
18
+ queue.bind exchange, routing_key: key
19
+ end
20
+
21
+ @subscription = queue.subscribe(ack: true) { |*args| call *args }
16
22
  end
17
23
 
18
24
  # Called from Bunny Thread Pool. Will create message object from
19
25
  # provided bunny data and dispatch message to connection.
20
26
  #
21
27
  def call(info, metadata, payload)
22
- message = Message.new(connection, info, metadata, payload, route)
23
- dispatcher.dispatch :call, message
28
+ dispatcher.dispatch Message.new(connection, info, metadata, payload, route)
24
29
  rescue => error
25
- Msgr.logger.warn(self) { "Error received within bunny subscribe handler: #{error.inspect}." }
30
+ log(:error) { "Error received within subscribe handler: #{error.inspect}\n#{error.backtrace.join("\n ")}" }
26
31
  end
27
32
 
28
33
  # Cancel subscription to not receive any more messages.
@@ -30,5 +35,27 @@ module Msgr
30
35
  def release
31
36
  subscription.cancel if subscription
32
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
33
60
  end
34
61
  end
data/lib/msgr/client.rb CHANGED
@@ -6,22 +6,22 @@ module Msgr
6
6
  include Celluloid
7
7
  include Logging
8
8
 
9
- attr_reader :pool, :uri
9
+ attr_reader :uri
10
10
 
11
- def initialize(config)
11
+ def initialize(config = {})
12
12
  @uri = URI.parse config[:uri] ? config.delete(:uri) : 'amqp://localhost/'
13
- @uri.protocol = 'amqps' if config[:secure]
14
- @uri.user = config.delete :user if config[:user]
15
- @uri.password = config.delete :password if config[:password]
16
- @uri.host = config.delete :host if config[:host]
17
- @uri.port = config.delete(:port).to_i if config[:port]
18
- @uri.path = "/#{config.delete :vhost}".gsub /\/+/, '/' if config[:vhost]
13
+ config[:pass] ||= @uri.password
19
14
 
20
- @config = config
21
- @bunny = Bunny.new @uri.to_s
22
- @pool = Pool.new Dispatcher, autostart: false
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
19
+ @uri.path = config[:vhost] ||= @uri.path.present? ? @uri.path : '/'
20
+ config.reject! { |_,v| v.nil? }
23
21
 
24
- @uri.password = nil
22
+ @config = config
23
+ @bunny = Bunny.new config
24
+ @pool = Pool.new Dispatcher, size: config[:size]
25
25
  end
26
26
 
27
27
  def running?; @running end
@@ -31,6 +31,10 @@ module Msgr
31
31
  @routes ||= Routes.new
32
32
  end
33
33
 
34
+ def new_connection
35
+ @connection = Connection.new @bunny, routes, @pool, prefix: @config[:prefix]
36
+ end
37
+
34
38
  def reload
35
39
  raise StandardError.new 'Client not running.' unless running?
36
40
  log(:info) { 'Reload client.' }
@@ -39,8 +43,7 @@ module Msgr
39
43
  @connection.terminate
40
44
 
41
45
  log(:debug) { 'Create new connection.' }
42
-
43
- @connection = Connection.new @bunny, routes, pool
46
+ new_connection
44
47
 
45
48
  log(:info) { 'Client reloaded.' }
46
49
  end
@@ -50,26 +53,28 @@ module Msgr
50
53
 
51
54
  @bunny.start
52
55
  @pool.start
56
+ @running = true
57
+ new_connection
53
58
 
54
- @running = true
55
- @connection = Connection.new @bunny, routes, pool
56
-
57
- log(:info) { "Client started. pool: #{pool.size}" }
59
+ log(:info) { 'Client started.' }
58
60
  end
59
61
 
60
- def stop
62
+ def stop(opts = {})
61
63
  return unless running?
64
+ opts.reverse_merge! timeout: 10, delete: false
62
65
 
63
- @running = false
64
- log(:info) { 'Graceful shutdown client...' }
66
+ stop_connection opts
65
67
 
66
- @connection.release
67
- @pool.stop
68
+ @running = false
69
+ wait_for_graceful_shutdown opts if opts[:timeout]
68
70
 
69
71
  log(:debug) { 'Terminating...' }
70
72
 
73
+ if opts[:delete]
74
+ log(:debug) { 'Delete connection.' }
75
+ @connection.delete
76
+ end
71
77
  @connection.terminate
72
- @pool.terminate
73
78
  @bunny.stop
74
79
 
75
80
  log(:info) { 'Terminated.' }
@@ -78,5 +83,30 @@ module Msgr
78
83
  def publish(routing_key, payload)
79
84
  @connection.publish payload, routing_key: routing_key
80
85
  end
86
+
87
+ private
88
+ def stop_connection(opts)
89
+ timeout = [opts[:wait_empty].to_i, 1].max
90
+
91
+ begin
92
+ if opts[:wait_empty]
93
+ log(:info) { "Shutdown requested: Wait until all queues are empty. (TIMEOUT: #{timeout}s)" }
94
+ @connection.future(:release, true).value timeout
95
+ else
96
+ @connection.future(:release).value timeout
97
+ end
98
+ rescue TimeoutError
99
+ log(:warn) { "Could release connection within #{timeout} seconds." }
100
+ end
101
+ end
102
+
103
+ def wait_for_graceful_shutdown(opts)
104
+ timeout = [opts[:timeout].to_i, 0].max
105
+ log(:info) { "Attempting graceful shutdown within #{timeout} seconds..." }
106
+
107
+ @pool.future(:stop).value timeout
108
+ rescue TimeoutError
109
+ log(:warn) { "Could not shutdown pool within #{timeout} seconds." }
110
+ end
81
111
  end
82
112
  end
@@ -4,32 +4,37 @@ module Msgr
4
4
  include Celluloid
5
5
  include Logging
6
6
 
7
- attr_reader :conn, :pool, :routes
7
+ attr_reader :conn, :dispatcher, :routes, :opts
8
8
  finalizer :close
9
9
 
10
- def initialize(conn, routes, pool)
11
- @conn = conn
12
- @pool = pool
13
- @routes = routes
10
+ def initialize(conn, routes, dispatcher, opts = {})
11
+ @conn = conn
12
+ @dispatcher = dispatcher
13
+ @routes = routes
14
+ @opts = opts
14
15
 
15
16
  @channel = conn.create_channel
16
17
  @channel.prefetch(10)
17
18
 
18
- rebind
19
+ bind
19
20
  end
20
21
 
21
- def rebind(routes = nil)
22
- routes = self.routes unless routes
23
-
24
- # First release old bindings
22
+ def rebind
25
23
  release
24
+ bind
25
+ end
26
26
 
27
+ def bind
27
28
  # Create new bindings
28
- routes.each { |route| bindings << Binding.new(Actor.current, route, pool) }
29
+ routes.each { |route| bindings << Binding.new(Actor.current, route, dispatcher) }
29
30
 
30
31
  log(:debug) { 'New routes bound.' }
31
32
  end
32
33
 
34
+ def prefix(name = '')
35
+ opts[:prefix] ? "#{opts[:prefix]}-#{name}" : name
36
+ end
37
+
33
38
  # Used to store al bindings. Allows use to
34
39
  # release bindings when receiver should not longer
35
40
  # receive messages but channel need to be open
@@ -40,36 +45,78 @@ module Msgr
40
45
  end
41
46
 
42
47
  def queue(name)
43
- @channel.queue name, durable: true
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
51
  end
45
52
 
46
53
  def exchange
47
- @exchange ||= @channel.topic 'msgr', durable: true
54
+ unless @exchange
55
+ @exchange = @channel.topic prefix('msgr'), durable: true
56
+
57
+ log(:debug) { "Created exchange #{@exchange.name} (type: #{@exchange.type}, durable: #{@exchange.durable?}, auto_delete: #{@exchange.auto_delete?})" }
58
+ end
59
+
60
+ @exchange
48
61
  end
49
62
 
50
63
  # Release all bindings but do not close channel. Will not
51
64
  # longer receive any message but channel can be used to
52
65
  # acknowledge currently processing messages.
53
66
  #
54
- def release
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
77
+ end
78
+ else
79
+ bindings.each &:release
80
+ end
81
+
82
+ log(:debug) { 'All bindings released.' }
83
+ end
84
+
85
+ def delete
55
86
  return unless bindings.any?
56
87
 
57
- log(:debug) { 'Release all bindings.' }
88
+ log(:debug) { 'Delete all bindings and exchange.' }
58
89
 
59
- bindings.each { |binding| binding.release }
90
+ bindings.each { |binding| binding.delete }
60
91
  bindings.clear
92
+
93
+ @exchange.delete if @exchange
61
94
  end
62
95
 
63
96
  def publish(payload, opts = {})
97
+ opts[:routing_key] ||= opts[:to]
98
+ raise ArgumentError, 'Missing routing key.' unless opts[:routing_key]
99
+
64
100
  log(:debug) { "Publish message to #{opts[:routing_key]}" }
65
101
 
66
- exchange.publish payload, opts.merge(persistent: true)
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')
107
+ end
67
108
  end
68
109
 
69
110
  def ack(delivery_tag)
111
+ log(:debug) { "Ack message: #{delivery_tag}" }
70
112
  @channel.ack delivery_tag
71
113
  end
72
114
 
115
+ def reject(delivery_tag, requeue = true)
116
+ log(:debug) { "Reject message: #{delivery_tag}" }
117
+ @channel.reject delivery_tag, requeue
118
+ end
119
+
73
120
  def close
74
121
  @channel.close if @channel.open?
75
122
  log(:debug) { 'Connection closed.' }
@@ -0,0 +1,28 @@
1
+ module Msgr
2
+
3
+ class Consumer
4
+ include Logging
5
+
6
+ attr_reader :message
7
+ delegate :payload, to: :@message
8
+ delegate :action, to: :'@message.route'
9
+ delegate :consumer, to: :'@message.consumer'
10
+
11
+ def dispatch(message)
12
+ @message = message
13
+
14
+ action = message.route.action.to_sym
15
+ raise Msgr::NoAction.new "No action `#{action}` for `#{self.class.name}`." unless respond_to? action
16
+
17
+ log(:debug) { "Invoke action #{action.inspect}." }
18
+
19
+ send action
20
+
21
+ log(:debug) { "Action #{action.inspect} done." }
22
+ end
23
+
24
+ def publish(data, opts = {})
25
+ Msgr.client.publish(data, opts)
26
+ end
27
+ end
28
+ end
@@ -7,16 +7,23 @@ module Msgr
7
7
  class Dispatcher
8
8
  include Logging
9
9
 
10
- def initialize
10
+ def call(message)
11
+ dispatch message
12
+
13
+ # Acknowledge message unless it is already acknowledged.
14
+ message.ack unless message.acked?
11
15
 
16
+ log(:warn) { "Message not acked!" } unless message.acked?
12
17
  end
13
18
 
14
- def call(message)
15
- log(:debug) { "Receive dispatched message: #{message.payload}" }
19
+ def dispatch(message)
20
+ consumer_class = Object.const_get message.route.consumer
16
21
 
17
- message.ack
22
+ log(:debug) { "Dispatch message to #{consumer_class.name}" }
18
23
 
19
- log(:debug) { 'Dispatched message acknowledged.' }
24
+ consumer_class.new.dispatch message
25
+ rescue => error
26
+ log(:error) { error }
20
27
  end
21
28
 
22
29
  def to_s
data/lib/msgr/logging.rb CHANGED
@@ -2,7 +2,7 @@ module Msgr
2
2
 
3
3
  module Logging
4
4
  def log(level)
5
- Msgr.logger.send(level, self.log_name) { yield }
5
+ Msgr.logger.send(level, self.log_name) { yield } if Msgr.logger
6
6
  end
7
7
 
8
8
  def log_name
@@ -0,0 +1,30 @@
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/message.rb CHANGED
@@ -1,7 +1,10 @@
1
1
  module Msgr
2
2
 
3
+ require 'msgr/message/acknowledge'
4
+
3
5
  class Message
4
- attr_reader :delivery_info, :metadata, :payload
6
+ include Acknowledge
7
+ attr_reader :delivery_info, :metadata, :payload, :route
5
8
 
6
9
  def initialize(connection, delivery_info, metadata, payload, route)
7
10
  @connection = connection
@@ -9,10 +12,14 @@ module Msgr
9
12
  @metadata = metadata
10
13
  @payload = payload
11
14
  @route = route
15
+
16
+ if content_type == 'application/json'
17
+ @payload = JSON[payload].symbolize_keys
18
+ end
12
19
  end
13
20
 
14
- def ack
15
- @connection.ack delivery_info.delivery_tag
21
+ def content_type
22
+ @metadata.content_type
16
23
  end
17
24
  end
18
25
  end