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
@@ -9,6 +9,7 @@ module MessageDriver
9
9
 
10
10
  module Adapters
11
11
  class BunnyAdapter < Base
12
+ NETWORK_ERRORS = [Bunny::TCPConnectionFailed, Bunny::ConnectionClosedError, Bunny::ConnectionLevelException, Bunny::NetworkErrorWrapper, Bunny::NetworkFailure, IOError].freeze
12
13
 
13
14
  class Message < MessageDriver::Message::Base
14
15
  attr_reader :delivery_info
@@ -17,13 +18,21 @@ module MessageDriver
17
18
  super(payload, properties[:headers]||{}, properties)
18
19
  @delivery_info = delivery_info
19
20
  end
21
+
22
+ def delivery_tag
23
+ delivery_info.delivery_tag
24
+ end
25
+
26
+ def redelivered?
27
+ delivery_info.redelivered?
28
+ end
20
29
  end
21
30
 
22
31
  class Destination < MessageDriver::Destination::Base
23
- def publish(body, headers={}, properties={})
32
+ def publish_params(headers, properties)
24
33
  props = @message_props.merge(properties)
25
34
  props[:headers] = headers if headers
26
- @adapter.publish(body, exchange_name, routing_key(properties), props)
35
+ [exchange_name, routing_key(properties), props]
27
36
  end
28
37
 
29
38
  def exchange_name
@@ -36,24 +45,31 @@ module MessageDriver
36
45
  end
37
46
 
38
47
  class QueueDestination < Destination
39
- def after_initialize
48
+ def after_initialize(adapter_context)
40
49
  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
+ adapter_context.with_channel(false) do |ch|
51
+ bunny_queue(ch, true)
50
52
  end
51
53
  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
+ raise MessageDriver::Error, "server-named queues must be declared, but you provided :no_declare => true" if @name.empty?
55
+ raise MessageDriver::Error, "queues with bindings must be declared, but you provided :no_declare => true" if @dest_options[:bindings]
54
56
  end
55
57
  end
56
58
 
59
+ def bunny_queue(channel, initialize=false)
60
+ queue = channel.queue(@name, @dest_options)
61
+ if initialize
62
+ @name = queue.name
63
+ if bindings = @dest_options[:bindings]
64
+ bindings.each do |bnd|
65
+ raise MessageDriver::Error, "binding #{bnd.inspect} must provide a source!" unless bnd[:source]
66
+ queue.bind(bnd[:source], bnd[:args]||{})
67
+ end
68
+ end
69
+ end
70
+ queue
71
+ end
72
+
57
73
  def exchange_name
58
74
  ""
59
75
  end
@@ -63,29 +79,31 @@ module MessageDriver
63
79
  end
64
80
 
65
81
  def message_count
66
- @adapter.current_context.with_channel(false) do |ch|
82
+ Client.current_adapter_context.with_channel(false) do |ch|
67
83
  ch.queue(@name, @dest_options.merge(passive: true)).message_count
68
84
  end
69
85
  end
70
- end
71
86
 
72
- class ExchangeDestination < Destination
73
- def pop_message(destination, options={})
74
- raise MessageDriver::Exception, "You can't pop a message off an exchange"
87
+ def purge
88
+ Client.current_adapter_context.with_channel(false) do |ch|
89
+ bunny_queue(ch).purge
90
+ end
75
91
  end
92
+ end
76
93
 
77
- def after_initialize
94
+ class ExchangeDestination < Destination
95
+ def after_initialize(adapter_context)
78
96
  if declare = @dest_options[:declare]
79
- @adapter.current_context.with_channel(false) do |ch|
97
+ adapter_context.with_channel(false) do |ch|
80
98
  type = declare.delete(:type)
81
- raise MessageDriver::Exception, "you must provide a valid exchange type" unless type
99
+ raise MessageDriver::Error, "you must provide a valid exchange type" unless type
82
100
  ch.exchange_declare(@name, type, declare)
83
101
  end
84
102
  end
85
103
  if bindings = @dest_options[:bindings]
86
- @adapter.current_context.with_channel(false) do |ch|
104
+ adapter_context.with_channel(false) do |ch|
87
105
  bindings.each do |bnd|
88
- raise MessageDriver::Exception, "binding #{bnd.inspect} must provide a source!" unless bnd[:source]
106
+ raise MessageDriver::Error, "binding #{bnd.inspect} must provide a source!" unless bnd[:source]
89
107
  ch.exchange_bind(bnd[:source], @name, bnd[:args]||{})
90
108
  end
91
109
  end
@@ -93,139 +111,261 @@ module MessageDriver
93
111
  end
94
112
  end
95
113
 
114
+ class Subscription < Subscription::Base
115
+ def start
116
+ raise MessageDriver::Error, "subscriptions are only supported with QueueDestinations" unless destination.is_a? QueueDestination
117
+ @sub_ctx = adapter.new_subscription_context(self)
118
+ @error_handler = options[:error_handler]
119
+ @sub_ctx.with_channel do |ch|
120
+ queue = destination.bunny_queue(@sub_ctx.channel)
121
+ ack_mode = case options[:ack]
122
+ when :auto, nil
123
+ :auto
124
+ when :manual
125
+ :manual
126
+ when :transactional
127
+ :transactional
128
+ else
129
+ raise MessageDriver::Error, "unrecognized :ack option #{options[:ack]}"
130
+ end
131
+ @bunny_consumer = queue.subscribe(options.merge(manual_ack: true)) do |delivery_info, properties, payload|
132
+ message = @sub_ctx.args_to_message(delivery_info, properties, payload)
133
+ Client.with_adapter_context(@sub_ctx) do
134
+ begin
135
+ case ack_mode
136
+ when :auto
137
+ consumer.call(message)
138
+ @sub_ctx.ack_message(message)
139
+ when :manual
140
+ consumer.call(message)
141
+ when :transactional
142
+ Client.with_message_transaction do
143
+ consumer.call(message)
144
+ @sub_ctx.ack_message(message)
145
+ end
146
+ end
147
+ rescue => e
148
+ if e.is_a?(DontRequeue) || (options[:retry_redelivered] == false && message.redelivered?)
149
+ if [:auto, :transactional].include? ack_mode
150
+ @sub_ctx.nack_message(message, requeue: false)
151
+ end
152
+ else
153
+ if @sub_ctx.valid? && ack_mode == :auto
154
+ begin
155
+ @sub_ctx.nack_message(message, requeue: true)
156
+ rescue => e
157
+ logger.error exception_to_str(e)
158
+ end
159
+ end
160
+ @error_handler.call(e, message) unless @error_handler.nil?
161
+ end
162
+ end
163
+ end
164
+ end
165
+ end
166
+ end
167
+
168
+ def unsubscribe
169
+ unless @bunny_consumer.nil?
170
+ @bunny_consumer.cancel
171
+ @bunny_consumer = nil
172
+ end
173
+ unless @sub_ctx.nil?
174
+ @sub_ctx.invalidate(true)
175
+ @sub_ctx = nil
176
+ end
177
+ end
178
+ end
179
+
96
180
  def initialize(config)
97
181
  validate_bunny_version
98
-
99
- @connection = Bunny.new(config.merge(threaded: false))
182
+ @config = config
100
183
  end
101
184
 
102
185
  def connection(ensure_started=true)
186
+ @connection ||= Bunny.new(@config)
103
187
  if ensure_started && !@connection.open?
104
- @connection.start
188
+ begin
189
+ @connection.start
190
+ rescue *NETWORK_ERRORS => e
191
+ raise MessageDriver::ConnectionError.new(e.to_s, e)
192
+ end
105
193
  end
106
194
  @connection
107
195
  end
108
196
 
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)
197
+ def stop
198
+ begin
199
+ super
200
+ @connection.close if !@connection.nil? && @connection.open?
201
+ rescue *NETWORK_ERRORS => e
202
+ logger.error "error while attempting connection close\n#{exception_to_str(e)}"
112
203
  end
113
204
  end
114
205
 
115
- def pop_message(destination, options={})
116
- current_context.with_channel do |ch|
117
- queue = ch.queue(destination, passive: true)
206
+ def build_context
207
+ BunnyContext.new(self)
208
+ end
209
+
210
+ def new_subscription_context(subscription)
211
+ ctx = new_context
212
+ ctx.channel = connection.create_channel
213
+ ctx.subscription = subscription
214
+ ctx
215
+ end
216
+
217
+ class BunnyContext < ContextBase
218
+ attr_accessor :channel, :subscription
118
219
 
119
- message = queue.pop
120
- if message.nil? || message[0].nil?
121
- nil
220
+ def initialize(adapter)
221
+ super(adapter)
222
+ @is_transactional = false
223
+ @rollback_only = false
224
+ @need_channel_reset = false
225
+ @in_transaction = false
226
+ end
227
+
228
+ def create_destination(name, dest_options={}, message_props={})
229
+ dest = case type = dest_options.delete(:type)
230
+ when :exchange
231
+ ExchangeDestination.new(self.adapter, name, dest_options, message_props)
232
+ when :queue, nil
233
+ QueueDestination.new(self.adapter, name, dest_options, message_props)
122
234
  else
123
- Message.new(*message)
235
+ raise MessageDriver::Error, "invalid destination type #{type}"
124
236
  end
237
+ dest.after_initialize(self)
238
+ dest
125
239
  end
126
- end
127
240
 
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}"
241
+ def supports_transactions?
242
+ true
136
243
  end
137
- end
138
244
 
139
- def with_transaction(options={}, &block)
140
- current_context.with_transaction(&block)
141
- end
245
+ def begin_transaction(options={})
246
+ raise MessageDriver::TransactionError, "you can't begin another transaction, you are already in one!" if in_transaction?
247
+ unless is_transactional?
248
+ with_channel(false) do |ch|
249
+ ch.tx_select
250
+ end
251
+ @is_transactional = true
252
+ end
253
+ @in_transaction = true
254
+ end
142
255
 
143
- def stop
144
- @connection.close if @connection.open?
145
- @context = nil
146
- end
256
+ def commit_transaction(channel_commit=false)
257
+ raise MessageDriver::TransactionError, "you can't finish the transaction unless you already in one!" if !in_transaction? && !channel_commit
258
+ begin
259
+ if is_transactional? && valid? && !@need_channel_reset
260
+ if @rollback_only
261
+ @channel.tx_rollback
262
+ else
263
+ @channel.tx_commit
264
+ end
265
+ end
266
+ ensure
267
+ @rollback_only = false
268
+ @in_transaction = false
269
+ end
270
+ end
147
271
 
148
- def current_context
149
- if !@context.nil? && @context.need_new_context?
150
- @context = nil
272
+ def rollback_transaction
273
+ @rollback_only = true
274
+ commit_transaction
151
275
  end
152
- @context ||= ChannelContext.new(connection)
153
- end
154
276
 
155
- private
277
+ def is_transactional?
278
+ @is_transactional
279
+ end
156
280
 
157
- class ChannelContext
158
- attr_reader :connection, :transaction_depth
281
+ def in_transaction?
282
+ @in_transaction
283
+ end
159
284
 
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
285
+ def publish(destination, body, headers={}, properties={})
286
+ exchange, routing_key, props = *destination.publish_params(headers, properties)
287
+ with_channel(true) do |ch|
288
+ ch.basic_publish(body, exchange, routing_key, props)
289
+ end
168
290
  end
169
291
 
170
- def is_transactional?
171
- @is_transactional
292
+ def pop_message(destination, options={})
293
+ raise MessageDriver::Error, "You can't pop a message off an exchange" if destination.is_a? ExchangeDestination
294
+
295
+ with_channel(false) do |ch|
296
+ queue = ch.queue(destination.name, passive: true)
297
+
298
+ message = queue.pop(ack: !!options[:client_ack])
299
+ if message.nil? || message[0].nil?
300
+ nil
301
+ else
302
+ args_to_message(*message)
303
+ end
304
+ end
172
305
  end
173
306
 
174
- def connection_failed?
175
- @connection_failed
307
+ def supports_client_acks?
308
+ true
176
309
  end
177
310
 
178
- def with_transaction(&block)
179
- if !is_transactional?
180
- @channel.tx_select
181
- @is_transactional = true
311
+ def ack_message(message, options={})
312
+ with_channel(true) do |ch|
313
+ ch.ack(message.delivery_tag)
182
314
  end
315
+ end
183
316
 
184
- begin
185
- @transaction_depth += 1
186
- yield
187
- commit_transaction
188
- rescue
189
- rollback_transaction
190
- raise
191
- ensure
192
- @transaction_depth -= 1
317
+ def nack_message(message, options={})
318
+ requeue = options[:requeue].kind_of?(FalseClass) ? false : true
319
+ with_channel(true) do |ch|
320
+ ch.reject(message.delivery_tag, requeue)
321
+ end
322
+ end
323
+
324
+ def supports_subscriptions?
325
+ true
326
+ end
327
+
328
+ def subscribe(destination, options={}, &consumer)
329
+ sub = Subscription.new(adapter, destination, consumer, options)
330
+ sub.start
331
+ sub
332
+ end
333
+
334
+ def invalidate(in_unsubscribe=false)
335
+ super()
336
+ unless @subscription.nil? || in_unsubscribe
337
+ @subscription.unsubscribe
338
+ end
339
+ unless @channel.nil?
340
+ @channel.close if @channel.open?
193
341
  end
194
342
  end
195
343
 
196
344
  def with_channel(require_commit=true)
197
345
  raise MessageDriver::TransactionRollbackOnly if @rollback_only
198
- raise MessageDriver::Exception, "oh shit!" if @connection_failed
346
+ raise MessageDriver::Error, "oh nos!" if !valid?
347
+ @channel = adapter.connection.create_channel if @channel.nil?
199
348
  reset_channel if @need_channel_reset
200
349
  begin
201
350
  result = yield @channel
202
- commit_transaction(true) if require_commit
351
+ commit_transaction(true) if require_commit && is_transactional? && !in_transaction?
203
352
  result
204
353
  rescue Bunny::ChannelLevelException => e
205
354
  @need_channel_reset = true
206
- @rollback_only = true if is_transactional?
355
+ @rollback_only = true if in_transaction?
207
356
  if e.kind_of? Bunny::NotFound
208
- raise MessageDriver::QueueNotFound.new(e)
357
+ raise MessageDriver::QueueNotFound.new(e.to_s, e)
209
358
  else
210
- raise MessageDriver::WrappedException.new(e)
359
+ raise MessageDriver::WrappedError.new(e.to_s, e)
211
360
  end
212
- rescue Bunny::NetworkErrorWrapper, IOError => e
213
- @connection_failed = true
214
- @rollback_only = true if is_transactional?
215
- raise MessageDriver::ConnectionException.new(e)
361
+ rescue *NETWORK_ERRORS => e
362
+ @rollback_only = true if in_transaction?
363
+ raise MessageDriver::ConnectionError.new(e.to_s, e)
216
364
  end
217
365
  end
218
366
 
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
367
+ def args_to_message(delivery_info, properties, payload)
368
+ Message.new(delivery_info, properties, payload)
229
369
  end
230
370
 
231
371
  private
@@ -234,35 +374,19 @@ module MessageDriver
234
374
  unless @channel.open?
235
375
  @channel.open
236
376
  @is_transactional = false
377
+ @rollback_only = true if in_transaction?
237
378
  end
238
379
  @need_channel_reset = false
239
380
  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
381
  end
260
382
 
383
+ private
384
+
261
385
  def validate_bunny_version
262
- required = Gem::Requirement.create('~> 0.9.0.pre7')
386
+ required = Gem::Requirement.create('>= 0.9.3')
263
387
  current = Gem::Version.create(Bunny::VERSION)
264
388
  unless required.satisfied_by? current
265
- raise MessageDriver::Exception, "bunny 0.9.0.pre7 or later is required for the bunny adapter"
389
+ raise MessageDriver::Error, "bunny 0.9.3 or later is required for the bunny adapter"
266
390
  end
267
391
  end
268
392
  end
@@ -1,3 +1,5 @@
1
+ require 'forwardable'
2
+
1
3
  module MessageDriver
2
4
  class Broker
3
5
  def in_memory_adapter
@@ -9,49 +11,126 @@ module MessageDriver
9
11
  class InMemoryAdapter < Base
10
12
 
11
13
  class Message < MessageDriver::Message::Base
14
+ end
12
15
 
16
+ class Subscription < MessageDriver::Subscription::Base
17
+ def unsubscribe
18
+ adapter.remove_subscription_for(destination.name)
19
+ end
20
+
21
+ def deliver_message(message)
22
+ begin
23
+ consumer.call(message)
24
+ rescue => e
25
+ unless options[:error_handler].nil?
26
+ options[:error_handler].call(e, message)
27
+ end
28
+ end
29
+ end
13
30
  end
14
31
 
15
32
  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
33
+
34
+ def subscription
35
+ adapter.subscription_for(name)
19
36
  end
37
+
20
38
  def message_count
21
- @message_store[@name].size
39
+ message_queue.size
40
+ end
41
+
42
+ def pop_message(options={})
43
+ message_queue.shift
44
+ end
45
+
46
+ def subscribe(options={}, &consumer)
47
+ subscription = Subscription.new(adapter, self, consumer, options)
48
+ adapter.set_subscription_for(name, subscription)
49
+ until (msg = pop_message).nil?
50
+ subscription.deliver_message(msg)
51
+ end
52
+ subscription
53
+ end
54
+
55
+ def publish(body, headers={}, properties={})
56
+ msg = Message.new(body, headers, properties)
57
+ sub = subscription
58
+ if sub.nil?
59
+ message_queue << msg
60
+ else
61
+ sub.deliver_message(msg)
62
+ end
63
+ end
64
+
65
+ private
66
+ def message_queue
67
+ adapter.message_queue_for(name)
22
68
  end
23
69
  end
24
70
 
25
71
  def initialize(config={})
26
- #does nothing
72
+ @destinations = {}
73
+ @message_store = Hash.new { |h,k| h[k] = [] }
74
+ @subscriptions = Hash.new
27
75
  end
28
76
 
29
- def publish(destination, body, headers={}, properties={})
30
- message_store[destination] << Message.new(body, headers, properties)
77
+ def build_context
78
+ InMemoryContext.new(self)
31
79
  end
32
80
 
33
- def pop_message(destination, options={})
34
- message_store[destination].shift
81
+ def create_destination(name, dest_options={}, message_props={})
82
+ destination = Destination.new(self, name, dest_options, message_props)
83
+ @destinations[name] = destination
35
84
  end
36
85
 
37
- def stop
38
- message_store.clear
86
+ class InMemoryContext < ContextBase
87
+ extend Forwardable
88
+
89
+ def_delegators :adapter, :create_destination
90
+
91
+ def publish(destination, body, headers={}, properties={})
92
+ destination.publish(body, headers, properties)
93
+ end
94
+
95
+ def pop_message(destination, options={})
96
+ destination.pop_message(options)
97
+ end
98
+
99
+ def subscribe(destination, options={}, &consumer)
100
+ destination.subscribe(options, &consumer)
101
+ end
102
+
103
+ def supports_subscriptions?
104
+ true
105
+ end
39
106
  end
40
107
 
41
- def create_destination(name, dest_options={}, message_props={})
42
- Destination.new(self, name, dest_options, message_props, message_store)
108
+ def stop
109
+ super
110
+ reset_after_tests
43
111
  end
44
112
 
45
113
  def reset_after_tests
46
- message_store.each do |destination, message_array|
47
- message_array.replace([])
114
+ @message_store.keys.each do |k|
115
+ @message_store[k] = []
48
116
  end
117
+ @subscriptions.clear
118
+ end
119
+
120
+ def message_queue_for(name)
121
+ @message_store[name]
122
+ end
123
+
124
+ def subscription_for(name)
125
+ @subscriptions[name]
49
126
  end
50
127
 
51
- private
128
+ def set_subscription_for(name, subscription)
129
+ @subscriptions[name] = subscription
130
+ end
52
131
 
53
- def message_store
54
- @message_store ||= Hash.new { |h,k| h[k] = [] }
132
+ def remove_subscription_for(name)
133
+ @subscriptions.delete(name)
55
134
  end
56
135
  end
57
136
  end