bunny 0.8.0 → 0.9.0.pre1

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 (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