message-driver 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 (58) hide show
  1. data/.gitignore +20 -0
  2. data/.rbenv-version +1 -0
  3. data/.relish +2 -0
  4. data/.rspec +2 -0
  5. data/.travis.yml +23 -0
  6. data/CHANGELOG.md +17 -0
  7. data/Gemfile +20 -0
  8. data/Guardfile +39 -0
  9. data/LICENSE.txt +22 -0
  10. data/README.md +36 -0
  11. data/Rakefile +23 -0
  12. data/features/.nav +12 -0
  13. data/features/CHANGELOG.md +17 -0
  14. data/features/README.md +1 -0
  15. data/features/Rails.md +1 -0
  16. data/features/amqp_specific_features/README.md +3 -0
  17. data/features/amqp_specific_features/binding_amqp_destinations.feature +50 -0
  18. data/features/amqp_specific_features/declaring_amqp_exchanges.feature +22 -0
  19. data/features/amqp_specific_features/server_named_destinations.feature +35 -0
  20. data/features/destination_metadata.feature +33 -0
  21. data/features/dynamic_destinations.feature +41 -0
  22. data/features/error_handling.feature +47 -0
  23. data/features/getting_started.md +1 -0
  24. data/features/publishing_a_message.feature +19 -0
  25. data/features/publishing_with_transactions.feature +36 -0
  26. data/features/step_definitions/dynamic_destinations_steps.rb +12 -0
  27. data/features/step_definitions/error_handling_steps.rb +11 -0
  28. data/features/step_definitions/steps.rb +41 -0
  29. data/features/support/env.rb +7 -0
  30. data/features/support/firewall_helper.rb +59 -0
  31. data/features/support/message_table_matcher.rb +11 -0
  32. data/features/support/no_error_matcher.rb +13 -0
  33. data/features/support/test_runner.rb +50 -0
  34. data/features/support/transforms.rb +17 -0
  35. data/lib/message-driver.rb +1 -0
  36. data/lib/message_driver/adapters/base.rb +29 -0
  37. data/lib/message_driver/adapters/bunny_adapter.rb +270 -0
  38. data/lib/message_driver/adapters/in_memory_adapter.rb +58 -0
  39. data/lib/message_driver/broker.rb +95 -0
  40. data/lib/message_driver/destination.rb +31 -0
  41. data/lib/message_driver/exceptions.rb +18 -0
  42. data/lib/message_driver/message.rb +13 -0
  43. data/lib/message_driver/message_publisher.rb +15 -0
  44. data/lib/message_driver/version.rb +5 -0
  45. data/lib/message_driver.rb +18 -0
  46. data/message-driver.gemspec +27 -0
  47. data/spec/integration/amqp_integration_spec.rb +146 -0
  48. data/spec/integration/message_driver/adapters/bunny_adapter_spec.rb +301 -0
  49. data/spec/spec_helper.rb +20 -0
  50. data/spec/support/shared/destination_examples.rb +41 -0
  51. data/spec/units/message_driver/adapters/base_spec.rb +44 -0
  52. data/spec/units/message_driver/adapters/in_memory_adapter_spec.rb +43 -0
  53. data/spec/units/message_driver/broker_spec.rb +98 -0
  54. data/spec/units/message_driver/destination_spec.rb +11 -0
  55. data/spec/units/message_driver/message_publisher_spec.rb +65 -0
  56. data/spec/units/message_driver/message_spec.rb +19 -0
  57. data/test_lib/broker_config.rb +25 -0
  58. metadata +203 -0
@@ -0,0 +1,59 @@
1
+ module FirewallHelper
2
+
3
+ COMMANDS = {
4
+ darwin: {
5
+ setup: [
6
+ "sudo ipfw add 02070 deny tcp from any to any 5672"
7
+ ],
8
+ teardown: [
9
+ "sudo ipfw delete 02070"
10
+ ]
11
+ },
12
+ linux: {
13
+ setup: [
14
+ "sudo iptables -N block-rabbit",
15
+ "sudo iptables -A block-rabbit -p tcp --dport 5672 -j DROP",
16
+ "sudo iptables -A block-rabbit -p tcp --sport 5672 -j DROP",
17
+ "sudo iptables -I INPUT -j block-rabbit",
18
+ "sudo iptables -I OUTPUT -j block-rabbit"
19
+ ],
20
+ teardown: [
21
+ "sudo iptables -D INPUT -j block-rabbit",
22
+ "sudo iptables -D OUTPUT -j block-rabbit",
23
+ "sudo iptables -F block-rabbit",
24
+ "sudo iptables -X block-rabbit"
25
+ ]
26
+ }
27
+ }
28
+
29
+ def block_broker_port
30
+ run_commands(:setup)
31
+ @firewall_rule_set = true
32
+ end
33
+
34
+ def unblock_broker_port
35
+ run_commands(:teardown) if @firewall_rule_set
36
+ @firewall_rule_set = false
37
+ end
38
+
39
+ def run_commands(step)
40
+ COMMANDS[os][step].each do |cmd|
41
+ result = system(cmd)
42
+ raise "command `#{cmd}` failed!" unless result
43
+ end
44
+ end
45
+
46
+ def os
47
+ if darwin?
48
+ :darwin
49
+ else
50
+ :linux
51
+ end
52
+ end
53
+
54
+ def darwin?
55
+ system("uname | grep Darwin")
56
+ end
57
+ end
58
+
59
+ World(FirewallHelper)
@@ -0,0 +1,11 @@
1
+ RSpec::Matchers.define :match_message_table do |expected|
2
+ match do |messages|
3
+ actual = messages.collect do |msg|
4
+ expected.headers.inject({}) do |memo, obj|
5
+ memo[obj] = msg.send(obj)
6
+ memo
7
+ end
8
+ end
9
+ actual == expected.hashes
10
+ end
11
+ end
@@ -0,0 +1,13 @@
1
+ RSpec::Matchers.define :have_no_errors do
2
+ match do |test_runner|
3
+ test_runner.raised_error == nil
4
+ end
5
+
6
+ failure_message_for_should do |test_runner|
7
+ err = test_runner.raised_error
8
+ filtered = (err.backtrace || []).reject do |line|
9
+ Cucumber::Ast::StepInvocation::BACKTRACE_FILTER_PATTERNS.detect { |p| line =~ p }
10
+ end
11
+ (["#{err.class}: #{err.to_s}"]+filtered).join("\n ")
12
+ end
13
+ end
@@ -0,0 +1,50 @@
1
+ require 'message_driver'
2
+
3
+ class TestRunner
4
+ include MessageDriver::MessagePublisher
5
+ include RSpec::Matchers
6
+
7
+ attr_accessor :raised_error
8
+
9
+ def config_broker(src)
10
+ instance_eval(src)
11
+ end
12
+
13
+ def run_test_code(src)
14
+ begin
15
+ instance_eval(src)
16
+ rescue Exception => e
17
+ @raised_error = e
18
+ end
19
+ end
20
+
21
+ def fetch_messages(destination)
22
+ case destination
23
+ when String, Symbol
24
+ fetch_messages(MessageDriver::Broker.find_destination(destination))
25
+ when MessageDriver::Destination::Base
26
+ result = []
27
+ begin
28
+ msg = destination.pop_message
29
+ result << msg unless msg.nil?
30
+ end until msg.nil?
31
+ result
32
+ else
33
+ raise "didn't understand destination #{destination}"
34
+ end
35
+ end
36
+
37
+ def publish_table_to_destination(destination, table)
38
+ table.hashes.each do |msg|
39
+ destination.publish(msg[:body], msg[:headers]||{}, msg[:properties]||{})
40
+ end
41
+ end
42
+ end
43
+
44
+ module KnowsMyTestRunner
45
+ def test_runner
46
+ @test_runner ||= TestRunner.new
47
+ end
48
+ end
49
+
50
+ World(KnowsMyTestRunner)
@@ -0,0 +1,17 @@
1
+ NUMBER = Transform(/^\d+|no$/) do |num|
2
+ case num
3
+ when "no"
4
+ 0
5
+ else
6
+ num.to_i
7
+ end
8
+ end
9
+
10
+ STRING_OR_SYM = Transform(/^:?\w+$/) do |str|
11
+ case str
12
+ when /^:/
13
+ str.slice(1, str.length-1).to_sym
14
+ else
15
+ str
16
+ end
17
+ end
@@ -0,0 +1 @@
1
+ require "message_driver"
@@ -0,0 +1,29 @@
1
+ module MessageDriver
2
+ module Adapters
3
+ class Base
4
+ def initialize(configuration)
5
+ raise "Must be implemented in subclass"
6
+ end
7
+
8
+ def publish(destination, body, headers={}, properties={})
9
+ raise "Must be implemented in subclass"
10
+ end
11
+
12
+ def pop_message(destination, options={})
13
+ raise "Must be implemented in subclass"
14
+ end
15
+
16
+ def stop
17
+ raise "Must be implemented in subclass"
18
+ end
19
+
20
+ def create_destination(name, dest_options={}, message_props={})
21
+ raise "Must be implemented in subclass"
22
+ end
23
+
24
+ def with_transaction(options={}, &block)
25
+ yield
26
+ end
27
+ end
28
+ end
29
+ end
@@ -0,0 +1,270 @@
1
+ require 'bunny'
2
+
3
+ module MessageDriver
4
+ class Broker
5
+ def bunny_adapter
6
+ MessageDriver::Adapters::BunnyAdapter
7
+ end
8
+ end
9
+
10
+ module Adapters
11
+ class BunnyAdapter < Base
12
+
13
+ class Message < MessageDriver::Message::Base
14
+ attr_reader :delivery_info
15
+
16
+ def initialize(delivery_info, properties, payload)
17
+ super(payload, properties[:headers]||{}, properties)
18
+ @delivery_info = delivery_info
19
+ end
20
+ end
21
+
22
+ class Destination < MessageDriver::Destination::Base
23
+ def publish(body, headers={}, properties={})
24
+ props = @message_props.merge(properties)
25
+ props[:headers] = headers if headers
26
+ @adapter.publish(body, exchange_name, routing_key(properties), props)
27
+ end
28
+
29
+ def exchange_name
30
+ @name
31
+ end
32
+
33
+ def routing_key(properties)
34
+ properties[:routing_key]
35
+ end
36
+ end
37
+
38
+ class QueueDestination < Destination
39
+ def after_initialize
40
+ unless @dest_options[:no_declare]
41
+ @adapter.current_context.with_channel(false) do |ch|
42
+ queue = ch.queue(@name, @dest_options)
43
+ @name = queue.name
44
+ if bindings = @dest_options[:bindings]
45
+ bindings.each do |bnd|
46
+ raise MessageDriver::Exception, "binding #{bnd.inspect} must provide a source!" unless bnd[:source]
47
+ queue.bind(bnd[:source], bnd[:args]||{})
48
+ end
49
+ end
50
+ end
51
+ else
52
+ raise MessageDriver::Exception, "server-named queues must be declared, but you provided :no_declare => true" if @name.empty?
53
+ raise MessageDriver::Exception, "queues with bindings must be declared, but you provided :no_declare => true" if @dest_options[:bindings]
54
+ end
55
+ end
56
+
57
+ def exchange_name
58
+ ""
59
+ end
60
+
61
+ def routing_key(properties)
62
+ @name
63
+ end
64
+
65
+ def message_count
66
+ @adapter.current_context.with_channel(false) do |ch|
67
+ ch.queue(@name, @dest_options.merge(passive: true)).message_count
68
+ end
69
+ end
70
+ end
71
+
72
+ class ExchangeDestination < Destination
73
+ def pop_message(destination, options={})
74
+ raise MessageDriver::Exception, "You can't pop a message off an exchange"
75
+ end
76
+
77
+ def after_initialize
78
+ if declare = @dest_options[:declare]
79
+ @adapter.current_context.with_channel(false) do |ch|
80
+ type = declare.delete(:type)
81
+ raise MessageDriver::Exception, "you must provide a valid exchange type" unless type
82
+ ch.exchange_declare(@name, type, declare)
83
+ end
84
+ end
85
+ if bindings = @dest_options[:bindings]
86
+ @adapter.current_context.with_channel(false) do |ch|
87
+ bindings.each do |bnd|
88
+ raise MessageDriver::Exception, "binding #{bnd.inspect} must provide a source!" unless bnd[:source]
89
+ ch.exchange_bind(bnd[:source], @name, bnd[:args]||{})
90
+ end
91
+ end
92
+ end
93
+ end
94
+ end
95
+
96
+ def initialize(config)
97
+ validate_bunny_version
98
+
99
+ @connection = Bunny.new(config.merge(threaded: false))
100
+ end
101
+
102
+ def connection(ensure_started=true)
103
+ if ensure_started && !@connection.open?
104
+ @connection.start
105
+ end
106
+ @connection
107
+ end
108
+
109
+ def publish(body, exchange, routing_key, properties)
110
+ current_context.with_channel(true) do |ch|
111
+ ch.basic_publish(body, exchange, routing_key, properties)
112
+ end
113
+ end
114
+
115
+ def pop_message(destination, options={})
116
+ current_context.with_channel do |ch|
117
+ queue = ch.queue(destination, passive: true)
118
+
119
+ message = queue.pop
120
+ if message.nil? || message[0].nil?
121
+ nil
122
+ else
123
+ Message.new(*message)
124
+ end
125
+ end
126
+ end
127
+
128
+ def create_destination(name, dest_options={}, message_props={})
129
+ case type = dest_options.delete(:type)
130
+ when :exchange
131
+ ExchangeDestination.new(self, name, dest_options, message_props)
132
+ when :queue, nil
133
+ QueueDestination.new(self, name, dest_options, message_props)
134
+ else
135
+ raise MessageDriver::Exception, "invalid destination type #{type}"
136
+ end
137
+ end
138
+
139
+ def with_transaction(options={}, &block)
140
+ current_context.with_transaction(&block)
141
+ end
142
+
143
+ def stop
144
+ @connection.close if @connection.open?
145
+ @context = nil
146
+ end
147
+
148
+ def current_context
149
+ if !@context.nil? && @context.need_new_context?
150
+ @context = nil
151
+ end
152
+ @context ||= ChannelContext.new(connection)
153
+ end
154
+
155
+ private
156
+
157
+ class ChannelContext
158
+ attr_reader :connection, :transaction_depth
159
+
160
+ def initialize(connection)
161
+ @connection = connection
162
+ @channel = connection.create_channel
163
+ @transaction_depth = 0
164
+ @is_transactional = false
165
+ @rollback_only = false
166
+ @need_channel_reset = false
167
+ @connection_failed = false
168
+ end
169
+
170
+ def is_transactional?
171
+ @is_transactional
172
+ end
173
+
174
+ def connection_failed?
175
+ @connection_failed
176
+ end
177
+
178
+ def with_transaction(&block)
179
+ if !is_transactional?
180
+ @channel.tx_select
181
+ @is_transactional = true
182
+ end
183
+
184
+ begin
185
+ @transaction_depth += 1
186
+ yield
187
+ commit_transaction
188
+ rescue
189
+ rollback_transaction
190
+ raise
191
+ ensure
192
+ @transaction_depth -= 1
193
+ end
194
+ end
195
+
196
+ def with_channel(require_commit=true)
197
+ raise MessageDriver::TransactionRollbackOnly if @rollback_only
198
+ raise MessageDriver::Exception, "oh shit!" if @connection_failed
199
+ reset_channel if @need_channel_reset
200
+ begin
201
+ result = yield @channel
202
+ commit_transaction(true) if require_commit
203
+ result
204
+ rescue Bunny::ChannelLevelException => e
205
+ @need_channel_reset = true
206
+ @rollback_only = true if is_transactional?
207
+ if e.kind_of? Bunny::NotFound
208
+ raise MessageDriver::QueueNotFound.new(e)
209
+ else
210
+ raise MessageDriver::WrappedException.new(e)
211
+ end
212
+ rescue Bunny::NetworkErrorWrapper, IOError => e
213
+ @connection_failed = true
214
+ @rollback_only = true if is_transactional?
215
+ raise MessageDriver::ConnectionException.new(e)
216
+ end
217
+ end
218
+
219
+ def within_transaction?
220
+ @transaction_depth > 0
221
+ end
222
+
223
+ def need_new_context?
224
+ if is_transactional?
225
+ !within_transaction? && connection_failed?
226
+ else
227
+ connection_failed?
228
+ end
229
+ end
230
+
231
+ private
232
+
233
+ def reset_channel
234
+ unless @channel.open?
235
+ @channel.open
236
+ @is_transactional = false
237
+ end
238
+ @need_channel_reset = false
239
+ end
240
+
241
+ def commit_transaction(from_channel=false)
242
+ threshold = from_channel ? 0 : 1
243
+ if is_transactional? && @transaction_depth <= threshold && !connection_failed?
244
+ unless @need_channel_reset
245
+ unless @rollback_only
246
+ @channel.tx_commit
247
+ else
248
+ @channel.tx_rollback
249
+ end
250
+ end
251
+ @rollback_only = false
252
+ end
253
+ end
254
+
255
+ def rollback_transaction
256
+ @rollback_only = true
257
+ commit_transaction
258
+ end
259
+ end
260
+
261
+ def validate_bunny_version
262
+ required = Gem::Requirement.create('~> 0.9.0.pre7')
263
+ current = Gem::Version.create(Bunny::VERSION)
264
+ unless required.satisfied_by? current
265
+ raise MessageDriver::Exception, "bunny 0.9.0.pre7 or later is required for the bunny adapter"
266
+ end
267
+ end
268
+ end
269
+ end
270
+ end
@@ -0,0 +1,58 @@
1
+ module MessageDriver
2
+ class Broker
3
+ def in_memory_adapter
4
+ MessageDriver::Adapters::InMemoryAdapter
5
+ end
6
+ end
7
+
8
+ module Adapters
9
+ class InMemoryAdapter < Base
10
+
11
+ class Message < MessageDriver::Message::Base
12
+
13
+ end
14
+
15
+ class Destination < MessageDriver::Destination::Base
16
+ def initialize(adapter, name, dest_options, message_props, message_store)
17
+ super(adapter, name, dest_options, message_props)
18
+ @message_store = message_store
19
+ end
20
+ def message_count
21
+ @message_store[@name].size
22
+ end
23
+ end
24
+
25
+ def initialize(config={})
26
+ #does nothing
27
+ end
28
+
29
+ def publish(destination, body, headers={}, properties={})
30
+ message_store[destination] << Message.new(body, headers, properties)
31
+ end
32
+
33
+ def pop_message(destination, options={})
34
+ message_store[destination].shift
35
+ end
36
+
37
+ def stop
38
+ message_store.clear
39
+ end
40
+
41
+ def create_destination(name, dest_options={}, message_props={})
42
+ Destination.new(self, name, dest_options, message_props, message_store)
43
+ end
44
+
45
+ def reset_after_tests
46
+ message_store.each do |destination, message_array|
47
+ message_array.replace([])
48
+ end
49
+ end
50
+
51
+ private
52
+
53
+ def message_store
54
+ @message_store ||= Hash.new { |h,k| h[k] = [] }
55
+ end
56
+ end
57
+ end
58
+ end
@@ -0,0 +1,95 @@
1
+ require 'forwardable'
2
+
3
+ module MessageDriver
4
+ class Broker
5
+ extend Forwardable
6
+
7
+ attr_reader :adapter, :configuration, :destinations
8
+
9
+ def_delegators :@adapter, :stop
10
+
11
+ class << self
12
+ def configure(options)
13
+ @instance = new(options)
14
+ end
15
+
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)
22
+ end
23
+
24
+ def instance
25
+ @instance
26
+ end
27
+
28
+ def define
29
+ yield @instance
30
+ end
31
+ end
32
+
33
+ def initialize(options)
34
+ @adapter = resolve_adapter(options[:adapter], options)
35
+ @configuration = options
36
+ @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)
47
+ end
48
+
49
+ def dynamic_destination(dest_name, dest_options={}, message_props={})
50
+ adapter.create_destination(dest_name, dest_options, message_props)
51
+ end
52
+
53
+ def destination(key, dest_name, dest_options={}, message_props={})
54
+ dest = dynamic_destination(dest_name, dest_options, message_props)
55
+ @destinations[key] = dest
56
+ end
57
+
58
+ def with_transaction(options={}, &block)
59
+ adapter.with_transaction(options, &block)
60
+ end
61
+
62
+ private
63
+
64
+ def find_destination(destination)
65
+ @destinations[destination]
66
+ end
67
+
68
+ def resolve_adapter(adapter, options)
69
+ case adapter
70
+ when nil
71
+ raise "you must specify an adapter"
72
+ when Symbol, String
73
+ resolve_adapter(find_adapter_class(adapter), options)
74
+ when Class
75
+ resolve_adapter(adapter.new(options), options)
76
+ when MessageDriver::Adapters::Base
77
+ adapter
78
+ else
79
+ raise "adapter must be a MessageDriver::Adapters::Base, but this object is a #{adapter.class}"
80
+ end
81
+ end
82
+
83
+ def find_adapter_class(adapter_name)
84
+ require "message_driver/adapters/#{adapter_name}_adapter"
85
+
86
+ adapter_method = "#{adapter_name}_adapter"
87
+
88
+ unless respond_to?(adapter_method)
89
+ raise "the adapter #{adapter_name} must provide MessageDriver::Broker.#{adapter_method} that returns the adapter class"
90
+ end
91
+
92
+ send(adapter_method)
93
+ end
94
+ end
95
+ end
@@ -0,0 +1,31 @@
1
+ module MessageDriver
2
+ module Destination
3
+ class Base
4
+ attr_reader :adapter, :name, :dest_options, :message_props
5
+
6
+ def initialize(adapter, name, dest_options, message_props)
7
+ @adapter = adapter
8
+ @name = name
9
+ @dest_options = dest_options
10
+ @message_props = message_props
11
+ after_initialize
12
+ end
13
+
14
+ def publish(body, headers={}, properties={})
15
+ @adapter.publish(@name, body, headers, @message_props.merge(properties))
16
+ end
17
+
18
+ def pop_message(options={})
19
+ @adapter.pop_message(@name, options)
20
+ end
21
+
22
+ def after_initialize
23
+ #does nothing, feel free to override as needed
24
+ end
25
+
26
+ def message_count
27
+ raise "#message_count is not supported by #{self.class}"
28
+ end
29
+ end
30
+ end
31
+ end
@@ -0,0 +1,18 @@
1
+ module MessageDriver
2
+ class Exception < ::Exception; end
3
+
4
+ class WrappedException < Exception
5
+ attr_reader :other
6
+
7
+ def initialize(other, msg=nil)
8
+ super(msg || other.to_s)
9
+ @other = other
10
+ end
11
+ end
12
+
13
+ class QueueNotFound < WrappedException; end
14
+
15
+ class ConnectionException < WrappedException; end
16
+
17
+ class TransactionRollbackOnly < Exception; end
18
+ end
@@ -0,0 +1,13 @@
1
+ module MessageDriver
2
+ module Message
3
+ class Base
4
+ attr_reader :body, :headers, :properties
5
+
6
+ def initialize(body, headers, properties)
7
+ @body = body
8
+ @headers = headers
9
+ @properties = properties
10
+ end
11
+ end
12
+ end
13
+ end
@@ -0,0 +1,15 @@
1
+ module MessageDriver
2
+ module MessagePublisher
3
+ def publish(destination, body, headers={}, properties={})
4
+ Broker.publish(destination, body, headers, properties)
5
+ end
6
+
7
+ def pop_message(destination, options={})
8
+ Broker.pop_message(destination, options)
9
+ end
10
+
11
+ def with_message_transaction(options={}, &block)
12
+ Broker.with_transaction(options, &block)
13
+ end
14
+ end
15
+ end
@@ -0,0 +1,5 @@
1
+ module Message
2
+ module Driver
3
+ VERSION = "0.1.0"
4
+ end
5
+ end