amqp 0.7.0.pre → 0.7.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 (71) hide show
  1. data/.gitignore +4 -0
  2. data/.rspec +2 -0
  3. data/CHANGELOG +8 -2
  4. data/CONTRIBUTORS +22 -0
  5. data/Gemfile +3 -3
  6. data/README.md +20 -11
  7. data/Rakefile +30 -6
  8. data/amqp.gemspec +1 -1
  9. data/bin/cleanify.rb +50 -0
  10. data/examples/amqp/simple.rb +6 -4
  11. data/examples/mq/ack.rb +8 -6
  12. data/examples/mq/automatic_binding_for_default_direct_exchange.rb +65 -0
  13. data/examples/mq/callbacks.rb +9 -1
  14. data/examples/mq/clock.rb +17 -17
  15. data/examples/mq/hashtable.rb +19 -10
  16. data/examples/mq/internal.rb +13 -11
  17. data/examples/mq/logger.rb +38 -36
  18. data/examples/mq/multiclock.rb +16 -7
  19. data/examples/mq/pingpong.rb +16 -7
  20. data/examples/mq/pop.rb +8 -6
  21. data/examples/mq/primes-simple.rb +2 -0
  22. data/examples/mq/primes.rb +7 -5
  23. data/examples/mq/stocks.rb +14 -5
  24. data/lib/amqp.rb +12 -8
  25. data/lib/amqp/buffer.rb +35 -158
  26. data/lib/amqp/client.rb +34 -22
  27. data/lib/amqp/frame.rb +8 -64
  28. data/lib/amqp/protocol.rb +21 -70
  29. data/lib/amqp/server.rb +11 -9
  30. data/lib/amqp/spec.rb +8 -6
  31. data/lib/amqp/version.rb +2 -0
  32. data/lib/ext/blankslate.rb +3 -1
  33. data/lib/ext/em.rb +2 -0
  34. data/lib/ext/emfork.rb +13 -11
  35. data/lib/mq.rb +253 -156
  36. data/lib/mq/collection.rb +6 -88
  37. data/lib/mq/exchange.rb +70 -13
  38. data/lib/mq/header.rb +12 -6
  39. data/lib/mq/logger.rb +9 -7
  40. data/lib/mq/queue.rb +42 -30
  41. data/lib/mq/rpc.rb +6 -4
  42. data/protocol/codegen.rb +20 -18
  43. data/research/api.rb +10 -46
  44. data/research/primes-forked.rb +9 -7
  45. data/research/primes-processes.rb +74 -72
  46. data/research/primes-threaded.rb +9 -7
  47. data/spec/integration/automatic_binding_for_default_direct_exchange_spec.rb +61 -0
  48. data/spec/mq_helper.rb +70 -0
  49. data/spec/spec_helper.rb +84 -29
  50. data/spec/unit/amqp/buffer_spec.rb +178 -0
  51. data/spec/unit/amqp/client_spec.rb +472 -0
  52. data/spec/unit/amqp/frame_spec.rb +60 -0
  53. data/spec/unit/amqp/misc_spec.rb +123 -0
  54. data/spec/unit/amqp/protocol_spec.rb +53 -0
  55. data/spec/unit/mq/channel_close_spec.rb +15 -0
  56. data/spec/unit/mq/collection_spec.rb +129 -0
  57. data/spec/unit/mq/exchange_declaration_spec.rb +524 -0
  58. data/spec/unit/mq/misc_spec.rb +228 -0
  59. data/spec/unit/mq/mq_basic_spec.rb +39 -0
  60. data/spec/unit/mq/queue_declaration_spec.rb +97 -0
  61. data/spec/unit/mq/queue_spec.rb +71 -0
  62. metadata +33 -21
  63. data/Gemfile.lock +0 -16
  64. data/old/README +0 -30
  65. data/old/Rakefile +0 -12
  66. data/old/amqp-0.8.json +0 -606
  67. data/old/amqp_spec.rb +0 -796
  68. data/old/amqpc.rb +0 -695
  69. data/old/codegen.rb +0 -148
  70. data/spec/channel_close_spec.rb +0 -13
  71. data/spec/sync_async_spec.rb +0 -52
data/lib/mq.rb CHANGED
@@ -1,3 +1,5 @@
1
+ # encoding: utf-8
2
+
1
3
  #:main: README
2
4
  #
3
5
 
@@ -17,6 +19,18 @@ class MQ
17
19
 
18
20
  # Raised whenever an illegal operation is attempted.
19
21
  class Error < StandardError; end
22
+
23
+ class IncompatibleOptionsError < Error
24
+ def initialize(name, opts_1, opts_2)
25
+ super("There is already an instance called #{name} with options #{opts_1.inspect}, you can't define the same instance with different options (#{opts_2.inspect})!")
26
+ end
27
+ end
28
+
29
+ class ChannelClosedError < Error
30
+ def initialize(instance)
31
+ super("The channel #{instance.channel} was closed, you can't use it anymore!")
32
+ end
33
+ end
20
34
  end
21
35
 
22
36
  # The top-level class for building AMQP clients. This class contains several
@@ -116,9 +130,20 @@ end
116
130
  # ["every 2 seconds", :received, Tue Jan 06 22:46:20 -0600 2009]
117
131
  #
118
132
  class MQ
133
+
134
+ #
135
+ # Behaviors
136
+ #
137
+
119
138
  include AMQP
120
139
  include EM::Deferrable
121
140
 
141
+
142
+
143
+ #
144
+ # API
145
+ #
146
+
122
147
  # Returns a new channel. A channel is a bidirectional virtual
123
148
  # connection between the client and the AMQP server. Elsewhere in the
124
149
  # library the channel is referred to in parameter lists as +mq+.
@@ -136,148 +161,28 @@ class MQ
136
161
  # channel = MQ.new AMQP::connect
137
162
  # end
138
163
  #
139
- def initialize connection = nil
140
- raise 'MQ can only be used from within EM.run{}' unless EM.reactor_running?
164
+ def initialize(connection = nil)
165
+ raise 'MQ can only be used from within EM.run {}' unless EM.reactor_running?
166
+
167
+ @_send_mutex = Mutex.new
168
+ @get_queue_mutex = Mutex.new
141
169
 
142
170
  @connection = connection || AMQP.start
143
171
 
144
- conn.callback{ |c|
172
+ conn.callback { |c|
145
173
  @channel = c.add_channel(self)
146
174
  send Protocol::Channel::Open.new
147
175
  }
148
176
  end
149
- attr_reader :channel, :connection
150
-
151
- def check_content_completion
152
- if @body.length >= @header.size
153
- @header.properties.update(@method.arguments)
154
- @consumer.receive @header, @body if @consumer
155
- @body = @header = @consumer = @method = nil
156
- end
157
- end
158
-
159
- # May raise a MQ::Error exception when the frame payload contains a
160
- # Protocol::Channel::Close object.
161
- #
162
- # This usually occurs when a client attempts to perform an illegal
163
- # operation. A short, and incomplete, list of potential illegal operations
164
- # follows:
165
- # * publish a message to a deleted exchange (NOT_FOUND)
166
- # * declare an exchange using the reserved 'amq.' naming structure (ACCESS_REFUSED)
167
- #
168
- def process_frame frame
169
- log :received, frame
170
-
171
- case frame
172
- when Frame::Header
173
- @header = frame.payload
174
- @body = ''
175
- check_content_completion
176
-
177
- when Frame::Body
178
- @body << frame.payload
179
- check_content_completion
180
-
181
- when Frame::Method
182
- case method = frame.payload
183
- when Protocol::Channel::OpenOk
184
- send Protocol::Access::Request.new(:realm => '/data',
185
- :read => true,
186
- :write => true,
187
- :active => true,
188
- :passive => true)
189
-
190
- when Protocol::Access::RequestOk
191
- @ticket = method.ticket
192
- callback{
193
- send Protocol::Channel::Close.new(:reply_code => 200,
194
- :reply_text => 'bye',
195
- :method_id => 0,
196
- :class_id => 0)
197
- } if @closing
198
- succeed
199
-
200
- when Protocol::Basic::CancelOk
201
- if @consumer = consumers[ method.consumer_tag ]
202
- @consumer.cancelled
203
- else
204
- MQ.error "Basic.CancelOk for invalid consumer tag: #{method.consumer_tag}"
205
- end
206
177
 
207
- when Protocol::Exchange::DeclareOk
208
- # We can't use exchanges[method.exchange] because if the name would
209
- # be an empty string, then AMQP broker generated a random one.
210
- exchanges = self.exchanges.select { |exchange| exchange.opts[:nowait].eql?(false) }
211
- exchange = exchanges.reverse.find { |exchange| exchange.status.eql?(:unfinished) }
212
- exchange.receive_response method
213
-
214
- when Protocol::Queue::DeclareOk
215
- # We can't use queues[method.queue] because if the name would
216
- # be an empty string, then AMQP broker generated a random one.
217
- queues = self.queues.select { |queue| queue.opts[:nowait].eql?(false) }
218
- queue = queues.reverse.find { |queue| queue.status.eql?(:unfinished) }
219
- queue.receive_status method
220
-
221
- when Protocol::Queue::BindOk
222
- # We can't use queues[method.queue] because if the name would
223
- # be an empty string, then AMQP broker generated a random one.
224
- queues = self.queues.select { |queue| queue.opts[:nowait].eql?(false) }
225
- queue = queues.reverse.find { |queue| queue.status.eql?(:unbound) }
226
- queue.after_bind method
227
-
228
- when Protocol::Basic::Deliver, Protocol::Basic::GetOk
229
- @method = method
230
- @header = nil
231
- @body = ''
232
-
233
- if method.is_a? Protocol::Basic::GetOk
234
- @consumer = get_queue{|q| q.shift }
235
- MQ.error "No pending Basic.GetOk requests" unless @consumer
236
- else
237
- @consumer = consumers[ method.consumer_tag ]
238
- MQ.error "Basic.Deliver for invalid consumer tag: #{method.consumer_tag}" unless @consumer
239
- end
240
-
241
- when Protocol::Basic::GetEmpty
242
- if @consumer = get_queue{|q| q.shift }
243
- @consumer.receive nil, nil
244
- else
245
- MQ.error "Basic.GetEmpty for invalid consumer"
246
- end
247
-
248
- when Protocol::Channel::Close
249
- raise Error, "#{method.reply_text} in #{Protocol.classes[method.class_id].methods[method.method_id]} on #{@channel}"
250
-
251
- when Protocol::Channel::CloseOk
252
- @on_close && @on_close.call(self)
253
-
254
- @closing = false
255
- conn.callback{ |c|
256
- c.channels.delete @channel
257
- c.close if c.channels.empty?
258
- }
178
+ attr_reader :channel, :connection, :status
179
+ alias :conn :connection
259
180
 
260
- when Protocol::Basic::ConsumeOk
261
- if @consumer = consumers[ method.consumer_tag ]
262
- @consumer.confirm_subscribe
263
- else
264
- MQ.error "Basic.ConsumeOk for invalid consumer tag: #{method.consumer_tag}"
265
- end
266
- end
267
- end
181
+ def closed?
182
+ @status.eql?(:closed)
268
183
  end
269
184
 
270
- def send *args
271
- conn.callback{ |c|
272
- (@_send_mutex ||= Mutex.new).synchronize do
273
- args.each do |data|
274
- data.ticket = @ticket if @ticket and data.respond_to? :ticket=
275
- log :sending, data
276
- c.send data, :channel => @channel
277
- end
278
- end
279
- }
280
- end
185
+
281
186
 
282
187
  # Defines, intializes and returns an Exchange to act as an ingress
283
188
  # point for all published messages.
@@ -352,8 +257,16 @@ class MQ
352
257
  # * redeclare an already-declared exchange to a different type
353
258
  # * :passive => true and the exchange does not exist (NOT_FOUND)
354
259
  #
355
- def direct name = 'amq.direct', opts = {}, &block
356
- self.exchanges << Exchange.new(self, :direct, name, opts, &block)
260
+ def direct(name = 'amq.direct', opts = {}, &block)
261
+ if exchange = self.exchanges.find { |exchange| exchange.name == name }
262
+ extended_opts = Exchange.add_default_options(:direct, name, opts, block)
263
+
264
+ validate_parameters_match!(exchange, extended_opts)
265
+
266
+ exchange
267
+ else
268
+ self.exchanges << Exchange.new(self, :direct, name, opts, &block)
269
+ end
357
270
  end
358
271
 
359
272
  # Defines, intializes and returns an Exchange to act as an ingress
@@ -438,8 +351,16 @@ class MQ
438
351
  # * redeclare an already-declared exchange to a different type
439
352
  # * :passive => true and the exchange does not exist (NOT_FOUND)
440
353
  #
441
- def fanout name = 'amq.fanout', opts = {}, &block
442
- self.exchanges << Exchange.new(self, :fanout, name, opts, &block)
354
+ def fanout(name = 'amq.fanout', opts = {}, &block)
355
+ if exchange = self.exchanges.find { |exchange| exchange.name == name }
356
+ extended_opts = Exchange.add_default_options(:fanout, name, opts, block)
357
+
358
+ validate_parameters_match!(exchange, extended_opts)
359
+
360
+ exchange
361
+ else
362
+ self.exchanges << Exchange.new(self, :fanout, name, opts, &block)
363
+ end
443
364
  end
444
365
 
445
366
  # Defines, intializes and returns an Exchange to act as an ingress
@@ -550,8 +471,16 @@ class MQ
550
471
  # * redeclare an already-declared exchange to a different type
551
472
  # * :passive => true and the exchange does not exist (NOT_FOUND)
552
473
  #
553
- def topic name = 'amq.topic', opts = {}, &block
554
- self.exchanges << Exchange.new(self, :topic, name, opts, &block)
474
+ def topic(name = 'amq.topic', opts = {}, &block)
475
+ if exchange = self.exchanges.find { |exchange| exchange.name == name }
476
+ extended_opts = Exchange.add_default_options(:topic, name, opts, block)
477
+
478
+ validate_parameters_match!(exchange, extended_opts)
479
+
480
+ exchange
481
+ else
482
+ self.exchanges << Exchange.new(self, :topic, name, opts, &block)
483
+ end
555
484
  end
556
485
 
557
486
  # Defines, intializes and returns an Exchange to act as an ingress
@@ -630,8 +559,16 @@ class MQ
630
559
  # * redeclare an already-declared exchange to a different type
631
560
  # * :passive => true and the exchange does not exist (NOT_FOUND)
632
561
  # * using a value other than "any" or "all" for "x-match"
633
- def headers name = 'amq.match', opts = {}, &block
634
- self.exchanges << Exchange.new(self, :headers, name, opts, &block)
562
+ def headers(name = 'amq.match', opts = {}, &block)
563
+ if exchange = self.exchanges.find { |exchange| exchange.name == name }
564
+ extended_opts = Exchange.add_default_options(:headers, name, opts, block)
565
+
566
+ validate_parameters_match!(exchange, extended_opts)
567
+
568
+ exchange
569
+ else
570
+ self.exchanges << Exchange.new(self, :headers, name, opts, &block)
571
+ end
635
572
  end
636
573
 
637
574
  # Queues store and forward messages. Queues can be configured in the server
@@ -647,8 +584,8 @@ class MQ
647
584
  #
648
585
  # == Options
649
586
  # * :passive => true | false (default false)
650
- # If set, the server will not create the exchange if it does not
651
- # already exist. The client can use this to check whether an exchange
587
+ # If set, the server will not create the queue if it does not
588
+ # already exist. The client can use this to check whether the queue
652
589
  # exists without modifying the server state.
653
590
  #
654
591
  # * :durable => true | false (default false)
@@ -687,7 +624,7 @@ class MQ
687
624
  #
688
625
  # The server waits for a short period of time before
689
626
  # determining the queue is unused to give time to the client code
690
- # to bind an exchange to it.
627
+ # to bind a queue to it.
691
628
  #
692
629
  # If the queue has been previously declared, this option is ignored
693
630
  # on subsequent declarations.
@@ -700,11 +637,19 @@ class MQ
700
637
  # not wait for a reply method. If the server could not complete the
701
638
  # method it will raise a channel or connection exception.
702
639
  #
703
- def queue name, opts = {}, &block
704
- self.queues << Queue.new(self, name, opts, &block)
640
+ def queue(name, opts = {}, &block)
641
+ if queue = self.queues.find { |queue| queue.name == name }
642
+ extended_opts = Queue.add_default_options(name, opts, block)
643
+
644
+ validate_parameters_match!(queue, extended_opts)
645
+
646
+ queue
647
+ else
648
+ self.queues << Queue.new(self, name, opts, &block)
649
+ end
705
650
  end
706
651
 
707
- def queue! name, opts = {}, &block
652
+ def queue!(name, opts = {}, &block)
708
653
  self.queues.add! Queue.new(self, name, opts, &block)
709
654
  end
710
655
 
@@ -744,7 +689,7 @@ class MQ
744
689
  # end
745
690
  # end
746
691
  #
747
- def rpc name, obj = nil
692
+ def rpc(name, obj = nil)
748
693
  rpcs[name] ||= RPC.new(self, name, obj)
749
694
  end
750
695
 
@@ -772,7 +717,9 @@ class MQ
772
717
 
773
718
  def prefetch(size)
774
719
  @prefetch_size = size
720
+
775
721
  send Protocol::Basic::Qos.new(:prefetch_size => 0, :prefetch_count => size, :global => false)
722
+
776
723
  self
777
724
  end
778
725
 
@@ -784,7 +731,7 @@ class MQ
784
731
  # If this flag is true, the server will attempt to requeue the message, potentially then
785
732
  # delivering it to an alternative subscriber.
786
733
  #
787
- def recover requeue = false
734
+ def recover(requeue = false)
788
735
  send Protocol::Basic::Recover.new(:requeue => requeue)
789
736
  self
790
737
  end
@@ -805,7 +752,7 @@ class MQ
805
752
 
806
753
  def get_queue
807
754
  if block_given?
808
- (@get_queue_mutex ||= Mutex.new).synchronize{
755
+ @get_queue_mutex.synchronize {
809
756
  yield( @get_queue ||= [] )
810
757
  }
811
758
  end
@@ -833,26 +780,176 @@ class MQ
833
780
  @consumers = {}
834
781
 
835
782
  exs = @exchanges
836
- @exchanges = {}
837
- exs.each{ |_,e| e.reset } if exs
783
+ @exchanges = MQ::Collection.new
784
+ exs.each { |e| e.reset } if exs
838
785
 
839
786
  qus = @queues
840
- @queues = {}
841
- qus.each{ |_,q| q.reset } if qus
787
+ @queues = MQ::Collection.new
788
+ qus.each { |q| q.reset } if qus
842
789
 
843
790
  prefetch(@prefetch_size) if @prefetch_size
844
791
  end
845
792
 
793
+
794
+ #
795
+ # Implementation
796
+ #
797
+
798
+ # May raise a MQ::Error exception when the frame payload contains a
799
+ # Protocol::Channel::Close object.
800
+ #
801
+ # This usually occurs when a client attempts to perform an illegal
802
+ # operation. A short, and incomplete, list of potential illegal operations
803
+ # follows:
804
+ # * publish a message to a deleted exchange (NOT_FOUND)
805
+ # * declare an exchange using the reserved 'amq.' naming structure (ACCESS_REFUSED)
806
+ #
807
+ def process_frame(frame)
808
+ log :received, frame
809
+
810
+ case frame
811
+ when Frame::Header
812
+ @header = frame.payload
813
+ @body = ''
814
+ check_content_completion
815
+
816
+ when Frame::Body
817
+ @body << frame.payload
818
+ check_content_completion
819
+
820
+ when Frame::Method
821
+ case method = frame.payload
822
+ when Protocol::Channel::OpenOk
823
+ send Protocol::Access::Request.new(:realm => '/data',
824
+ :read => true,
825
+ :write => true,
826
+ :active => true,
827
+ :passive => true)
828
+
829
+ when Protocol::Access::RequestOk
830
+ @ticket = method.ticket
831
+ callback {
832
+ send Protocol::Channel::Close.new(:reply_code => 200,
833
+ :reply_text => 'bye',
834
+ :method_id => 0,
835
+ :class_id => 0)
836
+ } if @closing
837
+ succeed
838
+
839
+ when Protocol::Basic::CancelOk
840
+ if @consumer = consumers[ method.consumer_tag ]
841
+ @consumer.cancelled
842
+ else
843
+ MQ.error "Basic.CancelOk for invalid consumer tag: #{method.consumer_tag}"
844
+ end
845
+
846
+ when Protocol::Exchange::DeclareOk
847
+ # We can't use exchanges[method.exchange] because if the name would
848
+ # be an empty string, then AMQP broker generated a random one.
849
+ exchanges = self.exchanges.select { |exchange| exchange.opts[:nowait].eql?(false) }
850
+ exchange = exchanges.reverse.find { |exchange| exchange.status.eql?(:unfinished) }
851
+ exchange.receive_response method
852
+
853
+ when Protocol::Queue::DeclareOk
854
+ # We can't use queues[method.queue] because if the name would
855
+ # be an empty string, then AMQP broker generated a random one.
856
+ queues = self.queues.select { |queue| queue.opts[:nowait].eql?(false) }
857
+ queue = queues.reverse.find { |queue| queue.status.eql?(:unfinished) }
858
+ queue.receive_status method
859
+
860
+ when Protocol::Queue::BindOk
861
+ # We can't use queues[method.queue] because if the name would
862
+ # be an empty string, then AMQP broker generated a random one.
863
+ queues = self.queues.select { |queue| queue.sync_bind }
864
+ queue = queues.reverse.find { |queue| queue.status.eql?(:unbound) }
865
+ queue.after_bind method
866
+
867
+ when Protocol::Basic::Deliver, Protocol::Basic::GetOk
868
+ @method = method
869
+ @header = nil
870
+ @body = ''
871
+
872
+ if method.is_a? Protocol::Basic::GetOk
873
+ @consumer = get_queue { |q| q.shift }
874
+ MQ.error "No pending Basic.GetOk requests" unless @consumer
875
+ else
876
+ @consumer = consumers[ method.consumer_tag ]
877
+ MQ.error "Basic.Deliver for invalid consumer tag: #{method.consumer_tag}" unless @consumer
878
+ end
879
+
880
+ when Protocol::Basic::GetEmpty
881
+ if @consumer = get_queue { |q| q.shift }
882
+ @consumer.receive nil, nil
883
+ else
884
+ MQ.error "Basic.GetEmpty for invalid consumer"
885
+ end
886
+
887
+ when Protocol::Channel::Close
888
+ @status = :closed
889
+ MQ.error "#{method.reply_text} in #{Protocol.classes[method.class_id].methods[method.method_id]} on #{@channel}"
890
+
891
+ when Protocol::Channel::CloseOk
892
+ @status = :closed
893
+ @on_close && @on_close.call(self)
894
+
895
+ @closing = false
896
+ conn.callback { |c|
897
+ c.channels.delete @channel
898
+ c.close if c.channels.empty?
899
+ }
900
+
901
+ when Protocol::Basic::ConsumeOk
902
+ if @consumer = consumers[ method.consumer_tag ]
903
+ @consumer.confirm_subscribe
904
+ else
905
+ MQ.error "Basic.ConsumeOk for invalid consumer tag: #{method.consumer_tag}"
906
+ end
907
+ end
908
+ end
909
+ end # process_frame
910
+
911
+
912
+ def send(*args)
913
+ conn.callback { |c|
914
+ @_send_mutex.synchronize do
915
+ args.each do |data|
916
+ unless self.closed?
917
+ data.ticket = @ticket if @ticket and data.respond_to? :ticket=
918
+ log :sending, data
919
+ c.send data, :channel => @channel
920
+ else
921
+ unless data.class == AMQP::Protocol::Channel::CloseOk
922
+ raise ChannelClosedError.new(self)
923
+ end
924
+ end
925
+ end
926
+ end
927
+ }
928
+ end # send
929
+
930
+
931
+ def check_content_completion
932
+ if @body.length >= @header.size
933
+ @header.properties.update(@method.arguments)
934
+ @consumer.receive @header, @body if @consumer
935
+ @body = @header = @consumer = @method = nil
936
+ end
937
+ end # check_content_completion
938
+
939
+
846
940
  private
847
941
 
848
- def log *args
942
+ def log(*args)
849
943
  return unless MQ.logging
850
944
  pp args
851
945
  puts
852
- end
946
+ end # log
853
947
 
854
- attr_reader :connection
855
- alias :conn :connection
948
+ def validate_parameters_match!(entity, parameters)
949
+ unless entity.opts == parameters || parameters[:passive]
950
+ raise IncompatibleOptionsError.new(entity.name, entity.opts, parameters)
951
+ end
952
+ end # validate_parameters_match!(entity, parameters)
856
953
  end
857
954
 
858
955
  #-- convenience wrapper (read: HACK) for thread-local MQ object