message-driver 0.1.0

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