msgr 0.0.0 → 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
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