message-driver 0.1.0 → 0.2.0.rc1

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 +7 -0
  2. data/.gitignore +1 -0
  3. data/.travis.yml +18 -7
  4. data/CHANGELOG.md +12 -2
  5. data/Gemfile +17 -0
  6. data/Guardfile +8 -4
  7. data/README.md +14 -5
  8. data/Rakefile +44 -11
  9. data/examples/basic_producer_and_consumer/Gemfile +5 -0
  10. data/examples/basic_producer_and_consumer/common.rb +17 -0
  11. data/examples/basic_producer_and_consumer/consumer.rb +24 -0
  12. data/examples/basic_producer_and_consumer/producer.rb +33 -0
  13. data/features/.nav +8 -0
  14. data/features/CHANGELOG.md +12 -2
  15. data/features/amqp_specific_features/binding_amqp_destinations.feature +7 -7
  16. data/features/amqp_specific_features/declaring_amqp_exchanges.feature +3 -3
  17. data/features/amqp_specific_features/nack_redelivered_messages.feature +92 -0
  18. data/features/amqp_specific_features/requeueing_on_nack.feature +44 -0
  19. data/features/amqp_specific_features/server_named_destinations.feature +5 -5
  20. data/features/client_acks.feature +92 -0
  21. data/features/destination_metadata.feature +9 -11
  22. data/features/dynamic_destinations.feature +7 -7
  23. data/features/error_handling.feature +11 -9
  24. data/features/logging.feature +14 -0
  25. data/features/message_consumers/auto_ack_consumers.feature +79 -0
  26. data/features/message_consumers/manual_ack_consumers.feature +95 -0
  27. data/features/message_consumers/transactional_ack_consumers.feature +77 -0
  28. data/features/message_consumers.feature +54 -0
  29. data/features/publishing_a_message.feature +6 -10
  30. data/features/publishing_with_transactions.feature +10 -14
  31. data/features/rabbitmq_specific_features/dead_letter_queueing.feature +116 -0
  32. data/features/step_definitions/dynamic_destinations_steps.rb +3 -3
  33. data/features/step_definitions/error_handling_steps.rb +4 -2
  34. data/features/step_definitions/logging_steps.rb +28 -0
  35. data/features/step_definitions/message_consumers_steps.rb +29 -0
  36. data/features/step_definitions/steps.rb +60 -9
  37. data/features/support/broker_config_helper.rb +19 -0
  38. data/features/support/env.rb +1 -0
  39. data/features/support/firewall_helper.rb +8 -11
  40. data/features/support/message_table_matcher.rb +21 -5
  41. data/features/support/test_runner.rb +39 -16
  42. data/lib/message_driver/adapters/base.rb +51 -4
  43. data/lib/message_driver/adapters/bunny_adapter.rb +251 -127
  44. data/lib/message_driver/adapters/in_memory_adapter.rb +97 -18
  45. data/lib/message_driver/adapters/stomp_adapter.rb +127 -0
  46. data/lib/message_driver/broker.rb +23 -24
  47. data/lib/message_driver/client.rb +157 -0
  48. data/lib/message_driver/destination.rb +7 -4
  49. data/lib/message_driver/errors.rb +27 -0
  50. data/lib/message_driver/logging.rb +11 -0
  51. data/lib/message_driver/message.rb +8 -0
  52. data/lib/message_driver/subscription.rb +18 -0
  53. data/lib/message_driver/vendor/.document +0 -0
  54. data/lib/message_driver/vendor/nesty/nested_error.rb +26 -0
  55. data/lib/message_driver/vendor/nesty.rb +1 -0
  56. data/lib/message_driver/version.rb +1 -1
  57. data/lib/message_driver.rb +4 -2
  58. data/message-driver.gemspec +4 -4
  59. data/spec/integration/{amqp_integration_spec.rb → bunny/amqp_integration_spec.rb} +29 -28
  60. data/spec/integration/bunny/bunny_adapter_spec.rb +339 -0
  61. data/spec/integration/in_memory/in_memory_adapter_spec.rb +126 -0
  62. data/spec/integration/stomp/stomp_adapter_spec.rb +142 -0
  63. data/spec/spec_helper.rb +5 -2
  64. data/spec/support/shared/adapter_examples.rb +17 -0
  65. data/spec/support/shared/client_ack_examples.rb +18 -0
  66. data/spec/support/shared/context_examples.rb +14 -0
  67. data/spec/support/shared/destination_examples.rb +4 -5
  68. data/spec/support/shared/subscription_examples.rb +146 -0
  69. data/spec/support/shared/transaction_examples.rb +43 -0
  70. data/spec/support/utils.rb +14 -0
  71. data/spec/units/message_driver/adapters/base_spec.rb +38 -19
  72. data/spec/units/message_driver/broker_spec.rb +71 -18
  73. data/spec/units/message_driver/client_spec.rb +375 -0
  74. data/spec/units/message_driver/destination_spec.rb +9 -0
  75. data/spec/units/message_driver/logging_spec.rb +18 -0
  76. data/spec/units/message_driver/message_spec.rb +36 -0
  77. data/spec/units/message_driver/subscription_spec.rb +24 -0
  78. data/test_lib/broker_config.rb +50 -20
  79. metadata +83 -45
  80. data/.rbenv-version +0 -1
  81. data/lib/message_driver/exceptions.rb +0 -18
  82. data/lib/message_driver/message_publisher.rb +0 -15
  83. data/spec/integration/message_driver/adapters/bunny_adapter_spec.rb +0 -301
  84. data/spec/units/message_driver/adapters/in_memory_adapter_spec.rb +0 -43
  85. data/spec/units/message_driver/message_publisher_spec.rb +0 -65
@@ -0,0 +1,127 @@
1
+ require 'stomp'
2
+ require 'forwardable'
3
+
4
+ module MessageDriver
5
+ class Broker
6
+ def stomp_adapter
7
+ MessageDriver::Adapters::StompAdapter
8
+ end
9
+ end
10
+
11
+ module Adapters
12
+ class StompAdapter < Base
13
+
14
+ class Message < MessageDriver::Message::Base
15
+ attr_reader :stomp_message
16
+ def initialize(stomp_message)
17
+ @stomp_message = stomp_message
18
+ super(stomp_message.body, stomp_message.headers, {})
19
+ end
20
+ end
21
+
22
+ class Destination < MessageDriver::Destination::Base
23
+
24
+ end
25
+
26
+ attr_reader :config, :poll_timeout
27
+
28
+ def initialize(config)
29
+ validate_stomp_version
30
+
31
+ @config = config.symbolize_keys
32
+ connect_headers = @config[:connect_headers] ||= {}
33
+ connect_headers.symbolize_keys
34
+ connect_headers[:"accept-version"] = "1.1,1.2"
35
+
36
+ vhost = @config.delete(:vhost)
37
+ connect_headers[:host] = vhost if vhost
38
+
39
+ @poll_timeout = 1
40
+ end
41
+
42
+ class StompContext < ContextBase
43
+ extend Forwardable
44
+
45
+ def_delegators :adapter, :with_connection, :poll_timeout
46
+
47
+ #def subscribe(destination, consumer)
48
+ #destination.subscribe(&consumer)
49
+ #end
50
+
51
+ def create_destination(name, dest_options={}, message_props={})
52
+ unless name.start_with?("/")
53
+ name = "/queue/#{name}"
54
+ end
55
+ Destination.new(self, name, dest_options, message_props)
56
+ end
57
+
58
+ def publish(destination, body, headers={}, properties={})
59
+ with_connection do |connection|
60
+ connection.publish(destination.name, body, headers)
61
+ end
62
+ end
63
+
64
+ def pop_message(destination, options={})
65
+ with_connection do |connection|
66
+ sub_id = connection.uuid
67
+ msg = nil
68
+ count = 0
69
+ options[:id] = sub_id #this is a workaround for https://github.com/stompgem/stomp/issues/56
70
+ connection.subscribe(destination.name, options, sub_id)
71
+ while msg.nil? && count < max_poll_count
72
+ msg = connection.poll
73
+ if msg.nil?
74
+ count += 1
75
+ sleep 0.1
76
+ end
77
+ end
78
+ connection.unsubscribe(destination.name, options, sub_id)
79
+ Message.new(msg) if msg
80
+ end
81
+ end
82
+
83
+ private
84
+
85
+ def max_poll_count
86
+ (poll_timeout / 0.1).to_i
87
+ end
88
+ end
89
+
90
+ def build_context
91
+ StompContext.new(self)
92
+ end
93
+
94
+ def with_connection
95
+ begin
96
+ @connection ||= open_connection
97
+ yield @connection
98
+ rescue SystemCallError, IOError => e
99
+ raise MessageDriver::ConnectionError.new(e)
100
+ end
101
+ end
102
+
103
+ def stop
104
+ super
105
+ if @connection
106
+ @connection.disconnect
107
+ end
108
+ end
109
+
110
+ private
111
+
112
+ def open_connection
113
+ conn = Stomp::Connection.new(@config)
114
+ raise MessageDriver::ConnectionError, conn.connection_frame.to_s unless conn.open?
115
+ conn
116
+ end
117
+
118
+ def validate_stomp_version
119
+ required = Gem::Requirement.create('~> 1.2.10')
120
+ current = Gem::Version.create(Stomp::Version::STRING)
121
+ unless required.satisfied_by? current
122
+ raise MessageDriver::Error, "stomp 1.2.10 or a later version of the 1.2.x series is required for the stomp adapter"
123
+ end
124
+ end
125
+ end
126
+ end
127
+ end
@@ -1,10 +1,11 @@
1
1
  require 'forwardable'
2
+ require 'logger'
2
3
 
3
4
  module MessageDriver
4
5
  class Broker
5
6
  extend Forwardable
6
7
 
7
- attr_reader :adapter, :configuration, :destinations
8
+ attr_reader :adapter, :configuration, :destinations, :consumers, :logger
8
9
 
9
10
  def_delegators :@adapter, :stop
10
11
 
@@ -13,12 +14,8 @@ module MessageDriver
13
14
  @instance = new(options)
14
15
  end
15
16
 
16
- def method_missing(m, *args)
17
- @instance.send(m, *args)
18
- end
19
-
20
- def with_transaction(options={}, &block)
21
- @instance.with_transaction(options, &block)
17
+ def method_missing(m, *args, &block)
18
+ @instance.send(m, *args, &block)
22
19
  end
23
20
 
24
21
  def instance
@@ -34,37 +31,39 @@ module MessageDriver
34
31
  @adapter = resolve_adapter(options[:adapter], options)
35
32
  @configuration = options
36
33
  @destinations = {}
37
- end
38
-
39
- def publish(destination, body, headers={}, properties={})
40
- dest = find_destination(destination)
41
- dest.publish(body, headers, properties)
42
- end
43
-
44
- def pop_message(destination, options={})
45
- dest = find_destination(destination)
46
- dest.pop_message(options)
34
+ @consumers = {}
35
+ @logger = options[:logger] || Logger.new(STDOUT).tap{|l| l.level = Logger::INFO}
36
+ logger.debug "MessageDriver configured successfully!"
47
37
  end
48
38
 
49
39
  def dynamic_destination(dest_name, dest_options={}, message_props={})
50
- adapter.create_destination(dest_name, dest_options, message_props)
40
+ Client.dynamic_destination(dest_name, dest_options, message_props)
51
41
  end
52
42
 
53
43
  def destination(key, dest_name, dest_options={}, message_props={})
54
- dest = dynamic_destination(dest_name, dest_options, message_props)
44
+ dest = Client.dynamic_destination(dest_name, dest_options, message_props)
55
45
  @destinations[key] = dest
56
46
  end
57
47
 
58
- def with_transaction(options={}, &block)
59
- adapter.with_transaction(options, &block)
48
+ def consumer(key, &block)
49
+ raise MessageDriver::Error, "you must provide a block" unless block_given?
50
+ @consumers[key] = block
60
51
  end
61
52
 
62
- private
53
+ def find_destination(destination_name)
54
+ destination = @destinations[destination_name]
55
+ raise MessageDriver::NoSuchDestinationError, "no destination #{destination_name} has been configured" if destination.nil?
56
+ destination
57
+ end
63
58
 
64
- def find_destination(destination)
65
- @destinations[destination]
59
+ def find_consumer(consumer_name)
60
+ consumer = @consumers[consumer_name]
61
+ raise MessageDriver::NoSuchConsumerError, "no consumer #{consumer_name} has been configured" if consumer.nil?
62
+ consumer
66
63
  end
67
64
 
65
+ private
66
+
68
67
  def resolve_adapter(adapter, options)
69
68
  case adapter
70
69
  when nil
@@ -0,0 +1,157 @@
1
+ require 'forwardable'
2
+
3
+ module MessageDriver
4
+ module Client
5
+ include Logging
6
+ extend self
7
+
8
+ def publish(destination, body, headers={}, properties={})
9
+ dest = find_destination(destination)
10
+ current_adapter_context.publish(dest, body, headers, properties)
11
+ end
12
+
13
+ def pop_message(destination, options={})
14
+ dest = find_destination(destination)
15
+ current_adapter_context.pop_message(dest, options)
16
+ end
17
+
18
+ def subscribe(destination_name, consumer_name, options={})
19
+ destination = find_destination(destination_name)
20
+ consumer = find_consumer(consumer_name)
21
+ current_adapter_context.subscribe(destination, options, &consumer)
22
+ end
23
+
24
+ def dynamic_destination(dest_name, dest_options={}, message_props={})
25
+ current_adapter_context.create_destination(dest_name, dest_options, message_props)
26
+ end
27
+
28
+ def ack_message(message, options={})
29
+ ctx = current_adapter_context
30
+ if ctx.supports_client_acks?
31
+ ctx.ack_message(message, options)
32
+ else
33
+ logger.debug("this adapter does not support client acks")
34
+ end
35
+ end
36
+
37
+ def nack_message(message, options={})
38
+ ctx = current_adapter_context
39
+ if ctx.supports_client_acks?
40
+ ctx.nack_message(message, options)
41
+ else
42
+ logger.debug("this adapter does not support client acks")
43
+ end
44
+ end
45
+
46
+ def with_message_transaction(options={}, &block)
47
+ wrapper = fetch_context_wrapper
48
+ wrapper.increment_transaction_depth
49
+ begin
50
+ if wrapper.transaction_depth == 1 && wrapper.ctx.supports_transactions?
51
+ wrapper.ctx.begin_transaction(options)
52
+ begin
53
+ yield
54
+ wrapper.ctx.commit_transaction
55
+ rescue
56
+ begin
57
+ wrapper.ctx.rollback_transaction
58
+ rescue => e
59
+ logger.error exception_to_str(e)
60
+ end
61
+ raise
62
+ end
63
+ else
64
+ logger.debug("this adapter does not support transactions")
65
+ yield
66
+ end
67
+ ensure
68
+ wrapper.decrement_transaction_depth
69
+ end
70
+ end
71
+
72
+ def current_adapter_context(initialize=true)
73
+ ctx = fetch_context_wrapper(initialize)
74
+ ctx.nil? ? nil : ctx.ctx
75
+ end
76
+
77
+ def with_adapter_context(adapter_context, &block)
78
+ old_ctx, Thread.current[:adapter_context] = fetch_context_wrapper(false), build_context_wrapper(adapter_context)
79
+ begin
80
+ yield
81
+ ensure
82
+ set_context_wrapper(old_ctx)
83
+ end
84
+ end
85
+
86
+ def clear_context
87
+ wrapper = fetch_context_wrapper(false)
88
+ unless wrapper.nil?
89
+ wrapper.invalidate
90
+ set_context_wrapper(nil)
91
+ end
92
+ end
93
+
94
+ private
95
+
96
+ def fetch_context_wrapper(initialize=true)
97
+ wrapper = Thread.current[:adapter_context]
98
+ if wrapper.nil? || !wrapper.valid?
99
+ if initialize
100
+ wrapper = build_context_wrapper
101
+ else
102
+ wrapper = nil
103
+ end
104
+ Thread.current[:adapter_context] = wrapper
105
+ end
106
+ wrapper
107
+ end
108
+
109
+ def set_context_wrapper(wrapper)
110
+ Thread.current[:adapter_context] = wrapper
111
+ end
112
+
113
+ def build_context_wrapper(ctx=Broker.adapter.new_context)
114
+ ContextWrapper.new(ctx)
115
+ end
116
+
117
+ def find_destination(destination)
118
+ case destination
119
+ when Destination::Base
120
+ destination
121
+ else
122
+ Broker.find_destination(destination)
123
+ end
124
+ end
125
+
126
+ def find_consumer(consumer)
127
+ Broker.find_consumer(consumer)
128
+ end
129
+
130
+ def adapter
131
+ Broker.adapter
132
+ end
133
+
134
+ class ContextWrapper
135
+ extend Forwardable
136
+
137
+ def_delegators :@ctx, :valid?, :invalidate
138
+
139
+ attr_reader :ctx
140
+ attr_reader :transaction_depth
141
+
142
+ def initialize(ctx)
143
+ @ctx = ctx
144
+ @transaction_depth = 0
145
+ end
146
+
147
+ def increment_transaction_depth
148
+ @transaction_depth += 1
149
+ end
150
+
151
+ def decrement_transaction_depth
152
+ @transaction_depth -= 1
153
+ end
154
+ end
155
+ end
156
+
157
+ end
@@ -8,24 +8,27 @@ module MessageDriver
8
8
  @name = name
9
9
  @dest_options = dest_options
10
10
  @message_props = message_props
11
- after_initialize
12
11
  end
13
12
 
14
13
  def publish(body, headers={}, properties={})
15
- @adapter.publish(@name, body, headers, @message_props.merge(properties))
14
+ Client.publish(self, body, headers, properties)
16
15
  end
17
16
 
18
17
  def pop_message(options={})
19
- @adapter.pop_message(@name, options)
18
+ Client.pop_message(self, options)
20
19
  end
21
20
 
22
- def after_initialize
21
+ def after_initialize(adapter_context)
23
22
  #does nothing, feel free to override as needed
24
23
  end
25
24
 
26
25
  def message_count
27
26
  raise "#message_count is not supported by #{self.class}"
28
27
  end
28
+
29
+ def subscribe(&consumer)
30
+ raise "#subscribe is not supported by #{self.class}"
31
+ end
29
32
  end
30
33
  end
31
34
  end
@@ -0,0 +1,27 @@
1
+ vendor = File.expand_path('../vendor', __FILE__)
2
+ $:.unshift(vendor) unless $:.include?(vendor)
3
+
4
+ require 'nesty'
5
+
6
+ module MessageDriver
7
+ class Error < StandardError; end
8
+ class TransactionError < Error; end
9
+ class TransactionRollbackOnly < TransactionError; end
10
+ class NoSuchDestinationError < Error; end
11
+ class NoSuchConsumerError < Error; end
12
+
13
+ class WrappedError < Error
14
+ include Nesty::NestedError
15
+ end
16
+ class QueueNotFound < WrappedError; end
17
+ class ConnectionError < WrappedError; end
18
+
19
+ module DontRequeue; end
20
+ class DontRequeueError < Error
21
+ include DontRequeue
22
+ end
23
+
24
+ class WrappedDontRequeueError < WrappedError
25
+ include DontRequeue
26
+ end
27
+ end
@@ -0,0 +1,11 @@
1
+ module MessageDriver
2
+ module Logging
3
+ def logger
4
+ MessageDriver::Broker.logger
5
+ end
6
+
7
+ def exception_to_str(e)
8
+ ([e.to_s] + e.backtrace).join(" \n")
9
+ end
10
+ end
11
+ end
@@ -8,6 +8,14 @@ module MessageDriver
8
8
  @headers = headers
9
9
  @properties = properties
10
10
  end
11
+
12
+ def ack(options={})
13
+ Client.ack_message(self, options)
14
+ end
15
+
16
+ def nack(options={})
17
+ Client.nack_message(self, options)
18
+ end
11
19
  end
12
20
  end
13
21
  end
@@ -0,0 +1,18 @@
1
+ module MessageDriver
2
+ module Subscription
3
+ class Base
4
+ attr_reader :adapter, :destination, :consumer, :options
5
+
6
+ def initialize(adapter, destination, consumer, options={})
7
+ @adapter = adapter
8
+ @destination = destination
9
+ @consumer = consumer
10
+ @options = options
11
+ end
12
+
13
+ def unsubscribe
14
+ raise "must be implemented in subclass"
15
+ end
16
+ end
17
+ end
18
+ end
File without changes
@@ -0,0 +1,26 @@
1
+ module Nesty
2
+ module NestedError
3
+ attr_reader :nested, :raw_backtrace
4
+
5
+ def initialize(msg = nil, nested)
6
+ super(msg)
7
+ @nested = nested
8
+ end
9
+
10
+ def set_backtrace(backtrace)
11
+ @raw_backtrace = backtrace
12
+ if nested
13
+ backtrace = backtrace - nested_raw_backtrace
14
+ backtrace += ["#{nested.backtrace.first}: #{nested.message} (#{nested.class.name})"]
15
+ backtrace += nested.backtrace[1..-1] || []
16
+ end
17
+ super(backtrace)
18
+ end
19
+
20
+ private
21
+
22
+ def nested_raw_backtrace
23
+ nested.respond_to?(:raw_backtrace) ? nested.raw_backtrace : nested.backtrace
24
+ end
25
+ end
26
+ end
@@ -0,0 +1 @@
1
+ require "nesty/nested_error"
@@ -1,5 +1,5 @@
1
1
  module Message
2
2
  module Driver
3
- VERSION = "0.1.0"
3
+ VERSION = "0.2.0.rc1"
4
4
  end
5
5
  end
@@ -1,11 +1,13 @@
1
1
  require 'message_driver/version'
2
2
 
3
- require 'message_driver/exceptions'
3
+ require 'message_driver/errors'
4
4
  require 'message_driver/broker'
5
+ require 'message_driver/logging'
5
6
  require 'message_driver/message'
6
7
  require 'message_driver/destination'
8
+ require 'message_driver/subscription'
7
9
  require 'message_driver/adapters/base'
8
- require 'message_driver/message_publisher'
10
+ require 'message_driver/client'
9
11
 
10
12
  module MessageDriver
11
13
  def self.configure(options={})
@@ -7,10 +7,10 @@ Gem::Specification.new do |gem|
7
7
  gem.name = "message-driver"
8
8
  gem.version = Message::Driver::VERSION
9
9
  gem.authors = ["Matt Campbell"]
10
- gem.email = ["matt@soupmatt.com"]
10
+ gem.email = ["message-driver@soupmatt.com"]
11
11
  gem.description = %q{Easy message queues for ruby using AMQ, STOMP and others}
12
12
  gem.summary = %q{Easy message queues for ruby}
13
- gem.homepage = "https://github.com/soupmatt/message_driver"
13
+ gem.homepage = "https://github.com/message-driver/message-driver"
14
14
  gem.license = "MIT"
15
15
 
16
16
  gem.files = `git ls-files`.split($/)
@@ -21,7 +21,7 @@ Gem::Specification.new do |gem|
21
21
  gem.required_ruby_version = '>= 1.9.2'
22
22
 
23
23
  gem.add_development_dependency "rake"
24
- gem.add_development_dependency "rspec", "~> 2.13.0"
24
+ gem.add_development_dependency "rspec", "~> 2.14.0"
25
25
  gem.add_development_dependency "cucumber"
26
- gem.add_development_dependency "bunny", "~> 0.9.0.pre7"
26
+ gem.add_development_dependency "aruba"
27
27
  end