amqp 0.7.0.pre → 0.7.0

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