bunny 0.8.0 → 0.9.0.pre1

Sign up to get free protection for your applications and to get access to all the features.
Files changed (91) hide show
  1. data/.gitignore +7 -1
  2. data/.travis.yml +14 -4
  3. data/ChangeLog.md +72 -0
  4. data/Gemfile +17 -11
  5. data/README.md +82 -0
  6. data/bunny.gemspec +6 -13
  7. data/examples/connection/heartbeat.rb +17 -0
  8. data/lib/bunny.rb +40 -56
  9. data/lib/bunny/channel.rb +615 -19
  10. data/lib/bunny/channel_id_allocator.rb +59 -0
  11. data/lib/bunny/compatibility.rb +24 -0
  12. data/lib/bunny/concurrent/condition.rb +63 -0
  13. data/lib/bunny/consumer.rb +42 -26
  14. data/lib/bunny/consumer_tag_generator.rb +22 -0
  15. data/lib/bunny/consumer_work_pool.rb +67 -0
  16. data/lib/bunny/exceptions.rb +128 -0
  17. data/lib/bunny/exchange.rb +131 -136
  18. data/lib/bunny/framing.rb +53 -0
  19. data/lib/bunny/heartbeat_sender.rb +59 -0
  20. data/lib/bunny/main_loop.rb +70 -0
  21. data/lib/bunny/message_metadata.rb +126 -0
  22. data/lib/bunny/queue.rb +102 -275
  23. data/lib/bunny/session.rb +478 -0
  24. data/lib/bunny/socket.rb +44 -0
  25. data/lib/bunny/system_timer.rb +9 -9
  26. data/lib/bunny/transport.rb +179 -0
  27. data/lib/bunny/version.rb +1 -1
  28. data/spec/compatibility/queue_declare_spec.rb +40 -0
  29. data/spec/higher_level_api/integration/basic_ack_spec.rb +54 -0
  30. data/spec/higher_level_api/integration/basic_consume_spec.rb +51 -0
  31. data/spec/higher_level_api/integration/basic_get_spec.rb +47 -0
  32. data/spec/higher_level_api/integration/basic_nack_spec.rb +39 -0
  33. data/spec/higher_level_api/integration/basic_publish_spec.rb +105 -0
  34. data/spec/higher_level_api/integration/basic_qos_spec.rb +32 -0
  35. data/spec/higher_level_api/integration/basic_recover_spec.rb +18 -0
  36. data/spec/higher_level_api/integration/basic_reject_spec.rb +53 -0
  37. data/spec/higher_level_api/integration/basic_return_spec.rb +33 -0
  38. data/spec/higher_level_api/integration/channel_close_spec.rb +29 -0
  39. data/spec/higher_level_api/integration/channel_flow_spec.rb +24 -0
  40. data/spec/higher_level_api/integration/channel_open_spec.rb +57 -0
  41. data/spec/higher_level_api/integration/channel_open_stress_spec.rb +22 -0
  42. data/spec/higher_level_api/integration/confirm_select_spec.rb +19 -0
  43. data/spec/higher_level_api/integration/connection_spec.rb +340 -0
  44. data/spec/higher_level_api/integration/exchange_bind_spec.rb +31 -0
  45. data/spec/higher_level_api/integration/exchange_declare_spec.rb +183 -0
  46. data/spec/higher_level_api/integration/exchange_delete_spec.rb +37 -0
  47. data/spec/higher_level_api/integration/exchange_unbind_spec.rb +40 -0
  48. data/spec/higher_level_api/integration/queue_bind_spec.rb +109 -0
  49. data/spec/higher_level_api/integration/queue_declare_spec.rb +129 -0
  50. data/spec/higher_level_api/integration/queue_delete_spec.rb +38 -0
  51. data/spec/higher_level_api/integration/queue_purge_spec.rb +30 -0
  52. data/spec/higher_level_api/integration/queue_unbind_spec.rb +33 -0
  53. data/spec/higher_level_api/integration/tx_commit_spec.rb +21 -0
  54. data/spec/higher_level_api/integration/tx_rollback_spec.rb +21 -0
  55. data/spec/lower_level_api/integration/basic_cancel_spec.rb +57 -0
  56. data/spec/lower_level_api/integration/basic_consume_spec.rb +100 -0
  57. data/spec/spec_helper.rb +64 -0
  58. data/spec/unit/bunny_spec.rb +15 -0
  59. data/spec/unit/concurrent/condition_spec.rb +66 -0
  60. metadata +135 -93
  61. data/CHANGELOG +0 -21
  62. data/README.textile +0 -76
  63. data/Rakefile +0 -14
  64. data/examples/simple.rb +0 -32
  65. data/examples/simple_ack.rb +0 -35
  66. data/examples/simple_consumer.rb +0 -55
  67. data/examples/simple_fanout.rb +0 -41
  68. data/examples/simple_headers.rb +0 -42
  69. data/examples/simple_publisher.rb +0 -29
  70. data/examples/simple_topic.rb +0 -61
  71. data/ext/amqp-0.9.1.json +0 -389
  72. data/ext/config.yml +0 -4
  73. data/ext/qparser.rb +0 -426
  74. data/lib/bunny/client.rb +0 -370
  75. data/lib/bunny/subscription.rb +0 -92
  76. data/lib/qrack/amq-client-url.rb +0 -165
  77. data/lib/qrack/channel.rb +0 -20
  78. data/lib/qrack/client.rb +0 -247
  79. data/lib/qrack/errors.rb +0 -5
  80. data/lib/qrack/protocol/protocol.rb +0 -135
  81. data/lib/qrack/protocol/spec.rb +0 -525
  82. data/lib/qrack/qrack.rb +0 -20
  83. data/lib/qrack/queue.rb +0 -40
  84. data/lib/qrack/subscription.rb +0 -152
  85. data/lib/qrack/transport/buffer.rb +0 -305
  86. data/lib/qrack/transport/frame.rb +0 -102
  87. data/spec/spec_09/amqp_url_spec.rb +0 -19
  88. data/spec/spec_09/bunny_spec.rb +0 -76
  89. data/spec/spec_09/connection_spec.rb +0 -34
  90. data/spec/spec_09/exchange_spec.rb +0 -173
  91. data/spec/spec_09/queue_spec.rb +0 -240
@@ -1,166 +1,161 @@
1
- # encoding: utf-8
1
+ require "bunny/compatibility"
2
2
 
3
3
  module Bunny
4
-
5
- # *Exchanges* are the routing and distribution hub of AMQP. All messages that Bunny sends
6
- # to an AMQP broker/server @have_to pass through an exchange in order to be routed to a
7
- # destination queue. The AMQP specification defines the types of exchange that you can create.
8
- #
9
- # At the time of writing there are four (4) types of exchange defined:
10
- #
11
- # * @:direct@
12
- # * @:fanout@
13
- # * @:topic@
14
- # * @:headers@
15
- #
16
- # AMQP-compliant brokers/servers are required to provide default exchanges for the @direct@ and
17
- # @fanout@ exchange types. All default exchanges are prefixed with @'amq.'@, for example:
18
- #
19
- # * @amq.direct@
20
- # * @amq.fanout@
21
- # * @amq.topic@
22
- # * @amq.match@ or @amq.headers@
23
- #
24
- # If you want more information about exchanges, please consult the documentation for your
25
- # target broker/server or visit the "AMQP website":http://www.amqp.org to find the version of the
26
- # specification that applies to your target broker/server.
27
4
  class Exchange
28
5
 
29
- attr_reader :client, :type, :name, :opts, :key
30
-
31
- def initialize(client, name, opts = {})
32
- # check connection to server
33
- raise Bunny::ConnectionError, 'Not connected to server' if client.status == :not_connected
6
+ include Bunny::Compatibility
34
7
 
35
- @client, @name, @opts = client, name, opts
36
8
 
37
- # set up the exchange type catering for default names
38
- if name =~ /^amq\.(.+)$/
39
- predeclared = true
40
- new_type = $1
41
- # handle 'amq.match' default
42
- new_type = 'headers' if new_type == 'match'
43
- @type = new_type.to_sym
44
- else
45
- @type = opts[:type] || :direct
46
- end
9
+ #
10
+ # API
11
+ #
47
12
 
48
- @key = opts[:key]
49
- @client.exchanges[@name] ||= self
13
+ # @return [Bunny::Channel]
14
+ attr_reader :channel
50
15
 
51
- # ignore the :nowait option if passed, otherwise program will hang waiting for a
52
- # response that will not be sent by the server
53
- opts.delete(:nowait)
16
+ # @return [String]
17
+ attr_reader :name
54
18
 
55
- unless predeclared or name == ''
56
- opts = {
57
- :exchange => name, :type => type, :nowait => false,
58
- :deprecated_ticket => 0, :deprecated_auto_delete => false, :deprecated_internal => false
59
- }.merge(opts)
19
+ # Type of this exchange (one of: :direct, :fanout, :topic, :headers).
20
+ # @return [Symbol]
21
+ attr_reader :type
60
22
 
61
- client.send_frame(Qrack::Protocol::Exchange::Declare.new(opts))
23
+ # @return [Symbol]
24
+ # @api plugin
25
+ attr_reader :status
62
26
 
63
- method = client.next_method
27
+ # Options hash this exchange instance was instantiated with
28
+ # @return [Hash]
29
+ attr_accessor :opts
64
30
 
65
- client.check_response(method, Qrack::Protocol::Exchange::DeclareOk, "Error declaring exchange #{name}: type = #{type}")
66
- end
67
- end
68
31
 
69
- # Requests that an exchange is deleted from broker/server. Removes reference from exchanges
70
- # if successful. If an error occurs raises {Bunny::ProtocolError}.
32
+ # The default exchange. Default exchange is a direct exchange that is predefined.
33
+ # It cannot be removed. Every queue is bind to this (direct) exchange by default with
34
+ # the following routing semantics: messages will be routed to the queue withe same
35
+ # same name as message's routing key. In other words, if a message is published with
36
+ # a routing key of "weather.usa.ca.sandiego" and there is a queue Q with this name,
37
+ # that message will be routed to Q.
71
38
  #
72
- # @option opts [Boolean] :if_unused (false)
73
- # If set to @true@, the server will only delete the exchange if it has no queue bindings. If the exchange has queue bindings the server does not delete it but raises a channel exception instead.
39
+ # @param [Bunny::Channel] channel Channel to use.
74
40
  #
75
- # @option opts [Boolean] :nowait (false)
76
- # Ignored by Bunny, always @false@.
41
+ # @example Publishing a messages to the tasks queue
42
+ # channel = Bunny::Channel.new(connection)
43
+ # tasks_queue = channel.queue("tasks")
44
+ # Bunny::Exchange.default(channel).publish("make clean", routing_key => "tasks")
77
45
  #
78
- # @return [Symbol] @:delete_ok@ if successful.
79
- def delete(opts = {})
80
- # ignore the :nowait option if passed, otherwise program will hang waiting for a
81
- # response that will not be sent by the server
82
- opts.delete(:nowait)
46
+ # @see Exchange
47
+ # @see http://files.travis-ci.org/docs/amqp/0.9.1/AMQP091Specification.pdf AMQP 0.9.1 specification (Section 2.1.2.4)
48
+ # @note Do not confuse default exchange with amq.direct: amq.direct is a pre-defined direct
49
+ # exchange that doesn't have any special routing semantics.
50
+ # @return [Exchange] An instance that corresponds to the default exchange (of type direct).
51
+ # @api public
52
+ def self.default(channel_or_connection)
53
+ self.new(channel_from(channel_or_connection), :direct, AMQ::Protocol::EMPTY_STRING, :no_declare => true)
54
+ end
55
+
56
+
57
+ def initialize(channel_or_connection, type, name, opts = {})
58
+ # old Bunny versions pass a connection here. In that case,
59
+ # we just use default channel from it. MK.
60
+ @channel = channel_from(channel_or_connection)
61
+ @name = name
62
+ @type = type
63
+ @options = self.class.add_default_options(name, opts)
83
64
 
84
- opts = { :exchange => name, :nowait => false, :deprecated_ticket => 0 }.merge(opts)
65
+ @durable = @options[:durable]
66
+ @auto_delete = @options[:auto_delete]
67
+ @arguments = @options[:arguments]
85
68
 
86
- client.send_frame(Qrack::Protocol::Exchange::Delete.new(opts))
69
+ declare! unless opts[:no_declare] || (@name =~ /^amq\..+/) || (@name == AMQ::Protocol::EMPTY_STRING)
87
70
 
88
- method = client.next_method
71
+ @channel.register_exchange(self)
72
+ end
89
73
 
90
- client.check_response(method, Qrack::Protocol::Exchange::DeleteOk, "Error deleting exchange #{name}")
74
+ # @return [Boolean] true if this exchange was declared as durable (will survive broker restart).
75
+ # @api public
76
+ def durable?
77
+ @durable
78
+ end # durable?
91
79
 
92
- client.exchanges.delete(name)
80
+ # @return [Boolean] true if this exchange was declared as automatically deleted (deleted as soon as last consumer unbinds).
81
+ # @api public
82
+ def auto_delete?
83
+ @auto_delete
84
+ end # auto_delete?
93
85
 
94
- # return confirmation
95
- :delete_ok
86
+ def arguments
87
+ @arguments
96
88
  end
97
89
 
98
- # Publishes a message to a specific exchange. The message will be routed to queues as defined
99
- # by the exchange configuration and distributed to any active consumers when the transaction,
100
- # if any, is committed.
101
- #
102
- # @option opts [String] :key
103
- # Specifies the routing key for the message. The routing key is
104
- # used for routing messages depending on the exchange configuration.
105
- #
106
- # @option opts [String] :content_type
107
- # Specifies the content type for the message.
108
- #
109
- # @option opts [Boolean] :mandatory (false)
110
- # Tells the server how to react if the message cannot be routed to a queue.
111
- # If set to @true@, the server will return an unroutable message
112
- # with a Return method. If this flag is zero, the server silently drops the message.
113
- #
114
- # @option opts [Boolean] :immediate (false)
115
- # Tells the server how to react if the message cannot be routed to a queue consumer
116
- # immediately. If set to @true@, the server will return an undeliverable message with
117
- # a Return method. If set to @false@, the server will queue the message, but with no
118
- # guarantee that it will ever be consumed.
90
+
91
+
92
+ def publish(payload, opts = {})
93
+ @channel.basic_publish(payload, self.name, opts.delete(:routing_key), opts)
94
+
95
+ self
96
+ end
97
+
98
+
99
+ # Deletes the exchange
100
+ # @api public
101
+ def delete(opts = {})
102
+ @channel.exchange_delete(@name, opts)
103
+ end
104
+
105
+
106
+ def bind(source, opts = {})
107
+ @channel.exchange_bind(source, self, opts)
108
+ end
109
+
110
+ def unbind(source, opts = {})
111
+ @channel.exchange_unbind(source, self, opts)
112
+ end
113
+
114
+
115
+ def on_return(&block)
116
+ @on_return = block
117
+ end
118
+
119
+
119
120
  #
120
- # @option opts [Boolean] :persistent (false)
121
- # Tells the server whether to persist the message. If set to @true@, the message will
122
- # be persisted to disk and not lost if the server restarts. If set to @false@, the message
123
- # will not be persisted across server restart. Setting to @true@ incurs a performance penalty
124
- # as there is an extra cost associated with disk access.
121
+ # Implementation
125
122
  #
126
- # @return [NilClass] nil
127
- def publish(data, opts = {})
128
- opts = opts.dup
129
- out = []
130
-
131
-
132
- # Set up options
133
- routing_key = opts.delete(:key) || key
134
- mandatory = opts.delete(:mandatory)
135
- immediate = opts.delete(:immediate)
136
- delivery_mode = opts.delete(:persistent) ? 2 : 1
137
- content_type = opts.delete(:content_type) || 'application/octet-stream'
138
- reply_to = opts.delete(:reply_to)
139
- correlation_id = opts.delete(:correlation_id)
140
- user_id = opts.delete(:user_id)
141
-
142
- out << Qrack::Protocol::Basic::Publish.new({ :exchange => name,
143
- :routing_key => routing_key,
144
- :mandatory => mandatory,
145
- :immediate => immediate,
146
- :deprecated_ticket => 0 })
147
- data = data.to_s
148
- out << Qrack::Protocol::Header.new(
149
- Qrack::Protocol::Basic,
150
- data.bytesize, {
151
- :content_type => content_type,
152
- :delivery_mode => delivery_mode,
153
- :reply_to => reply_to,
154
- :correlation_id => correlation_id,
155
- :user_id => user_id,
156
- :priority => 0
157
- }.merge(opts)
158
- )
159
- out << Qrack::Transport::Body.new(data)
160
-
161
- client.send_frame(*out)
123
+
124
+ def handle_return(basic_return, properties, content)
125
+ if @on_return
126
+ @on_return.call(basic_return, properties, content)
127
+ else
128
+ # TODO: log a warning
129
+ end
162
130
  end
163
131
 
164
- end
132
+ protected
165
133
 
134
+ # @private
135
+ def declare!
136
+ @channel.exchange_declare(@name, @type, @options)
137
+ end
138
+
139
+ # @private
140
+ def self.add_default_options(name, opts, block)
141
+ { :exchange => name, :nowait => (block.nil? && !name.empty?) }.merge(opts)
142
+ end
143
+
144
+ # @private
145
+ def self.add_default_options(name, opts)
146
+ # :nowait is always false for Bunny
147
+ h = { :queue => name, :nowait => false }.merge(opts)
148
+
149
+ if name.empty?
150
+ {
151
+ :passive => false,
152
+ :durable => false,
153
+ :auto_delete => false,
154
+ :arguments => nil
155
+ }.merge(h)
156
+ else
157
+ h
158
+ end
159
+ end
160
+ end
166
161
  end
@@ -0,0 +1,53 @@
1
+ module Bunny
2
+ module Framing
3
+ ENCODINGS_SUPPORTED = defined? Encoding
4
+ HEADER_SLICE = (0..6).freeze
5
+ DATA_SLICE = (7..-1).freeze
6
+ PAYLOAD_SLICE = (0..-2).freeze
7
+
8
+ module String
9
+ class Frame < AMQ::Protocol::Frame
10
+ def self.decode(string)
11
+ header = string[HEADER_SLICE]
12
+ type, channel, size = self.decode_header(header)
13
+ data = string[DATA_SLICE]
14
+ payload = data[PAYLOAD_SLICE]
15
+ frame_end = data[-1, 1]
16
+
17
+ frame_end.force_encoding(AMQ::Protocol::Frame::FINAL_OCTET.encoding) if ENCODINGS_SUPPORTED
18
+
19
+ # 1) the size is miscalculated
20
+ if payload.bytesize != size
21
+ raise BadLengthError.new(size, payload.bytesize)
22
+ end
23
+
24
+ # 2) the size is OK, but the string doesn't end with FINAL_OCTET
25
+ raise NoFinalOctetError.new if frame_end != AMQ::Protocol::Frame::FINAL_OCTET
26
+
27
+ self.new(type, payload, channel)
28
+ end
29
+ end
30
+ end # String
31
+
32
+
33
+ module IO
34
+ class Frame < AMQ::Protocol::Frame
35
+ def self.decode(io)
36
+ header = io.read(7)
37
+ type, channel, size = self.decode_header(header)
38
+ data = io.read_fully(size + 1)
39
+ payload, frame_end = data[PAYLOAD_SLICE], data[-1, 1]
40
+
41
+ # 1) the size is miscalculated
42
+ if payload.bytesize != size
43
+ raise BadLengthError.new(size, payload.bytesize)
44
+ end
45
+
46
+ # 2) the size is OK, but the string doesn't end with FINAL_OCTET
47
+ raise NoFinalOctetError.new if frame_end != AMQ::Protocol::Frame::FINAL_OCTET
48
+ self.new(type, payload, channel)
49
+ end # self.from
50
+ end # Frame
51
+ end # IO
52
+ end # Framing
53
+ end # Bunny
@@ -0,0 +1,59 @@
1
+ require "thread"
2
+ require "amq/protocol/client"
3
+ require "amq/protocol/frame"
4
+
5
+ module Bunny
6
+ class HeartbeatSender
7
+
8
+ #
9
+ # API
10
+ #
11
+
12
+ def initialize(transport)
13
+ @transport = transport
14
+ @mutex = Mutex.new
15
+
16
+ @last_activity_time = Time.now
17
+ end
18
+
19
+ def start(period = 30)
20
+ @mutex.synchronize do
21
+ @period = period
22
+
23
+ @thread = Thread.new(&method(:run))
24
+ end
25
+ end
26
+
27
+ def stop
28
+ @mutex.synchronize { @thread.exit }
29
+ end
30
+
31
+ def signal_activity!
32
+ @last_activity_time = Time.now
33
+ end
34
+
35
+ protected
36
+
37
+ def run
38
+ begin
39
+ loop do
40
+ self.beat
41
+
42
+ sleep (@period / 2)
43
+ end
44
+ rescue IOError => ioe
45
+ # ignored
46
+ rescue Exception => e
47
+ puts e.message
48
+ end
49
+ end
50
+
51
+ def beat
52
+ now = Time.now
53
+
54
+ if now > (@last_activity_time + @period)
55
+ @transport.send_raw(AMQ::Protocol::HeartbeatFrame.encode)
56
+ end
57
+ end
58
+ end
59
+ end
@@ -0,0 +1,70 @@
1
+ require "thread"
2
+
3
+ module Bunny
4
+ # Network activity loop that reads and passes incoming AMQP 0.9.1 methods for
5
+ # processing. They are dispatched further down the line in Bunny::Session and Bunny::Channel.
6
+ # This loop uses a separate thread internally.
7
+ #
8
+ # This mimics the way RabbitMQ Java is designed quite closely.
9
+ class MainLoop
10
+
11
+ def initialize(transport, session)
12
+ @transport = transport
13
+ @session = session
14
+ end
15
+
16
+
17
+ def start
18
+ @thread = Thread.new(&method(:run_loop))
19
+ end
20
+
21
+ def run_loop
22
+ loop do
23
+ begin
24
+ break if @stopping
25
+
26
+ frame = @transport.read_next_frame
27
+ @session.signal_activity!
28
+
29
+ next if frame.is_a?(AMQ::Protocol::HeartbeatFrame)
30
+
31
+ if !frame.final? || frame.method_class.has_content?
32
+ header = @transport.read_next_frame
33
+ content = ''
34
+
35
+ if header.body_size > 0
36
+ loop do
37
+ body_frame = @transport.read_next_frame
38
+ content << body_frame.decode_payload
39
+
40
+ break if content.bytesize >= header.body_size
41
+ end
42
+ end
43
+
44
+ @session.handle_frameset(frame.channel, [frame.decode_payload, header.decode_payload, content])
45
+ else
46
+ @session.handle_frame(frame.channel, frame.decode_payload)
47
+ end
48
+ rescue Timeout::Error => te
49
+ # given that the server may be pushing data to us, timeout detection/handling
50
+ # should happen per operation and not in this loop
51
+ rescue Errno::EBADF => ebadf
52
+ # ignored, happens when we loop after the transport has already been closed
53
+ rescue Exception => e
54
+ puts e.class.name
55
+ puts e.message
56
+ puts e.backtrace
57
+ end
58
+ end
59
+ end
60
+
61
+ def stop
62
+ @stopping = true
63
+ end
64
+
65
+ def kill
66
+ @thread.kill
67
+ @thread.join
68
+ end
69
+ end
70
+ end