message-driver 0.1.0 → 0.2.0.rc1

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 +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