garaio_bunny 2.19.1

Sign up to get free protection for your applications and to get access to all the features.
Files changed (40) hide show
  1. checksums.yaml +7 -0
  2. data/README.md +231 -0
  3. data/lib/amq/protocol/extensions.rb +16 -0
  4. data/lib/bunny/authentication/credentials_encoder.rb +55 -0
  5. data/lib/bunny/authentication/external_mechanism_encoder.rb +27 -0
  6. data/lib/bunny/authentication/plain_mechanism_encoder.rb +19 -0
  7. data/lib/bunny/channel.rb +2055 -0
  8. data/lib/bunny/channel_id_allocator.rb +82 -0
  9. data/lib/bunny/concurrent/atomic_fixnum.rb +75 -0
  10. data/lib/bunny/concurrent/condition.rb +66 -0
  11. data/lib/bunny/concurrent/continuation_queue.rb +62 -0
  12. data/lib/bunny/concurrent/linked_continuation_queue.rb +61 -0
  13. data/lib/bunny/concurrent/synchronized_sorted_set.rb +56 -0
  14. data/lib/bunny/consumer.rb +128 -0
  15. data/lib/bunny/consumer_tag_generator.rb +23 -0
  16. data/lib/bunny/consumer_work_pool.rb +122 -0
  17. data/lib/bunny/cruby/socket.rb +110 -0
  18. data/lib/bunny/cruby/ssl_socket.rb +118 -0
  19. data/lib/bunny/delivery_info.rb +93 -0
  20. data/lib/bunny/exceptions.rb +269 -0
  21. data/lib/bunny/exchange.rb +275 -0
  22. data/lib/bunny/framing.rb +56 -0
  23. data/lib/bunny/get_response.rb +83 -0
  24. data/lib/bunny/heartbeat_sender.rb +71 -0
  25. data/lib/bunny/jruby/socket.rb +57 -0
  26. data/lib/bunny/jruby/ssl_socket.rb +58 -0
  27. data/lib/bunny/message_properties.rb +119 -0
  28. data/lib/bunny/queue.rb +393 -0
  29. data/lib/bunny/reader_loop.rb +158 -0
  30. data/lib/bunny/return_info.rb +74 -0
  31. data/lib/bunny/session.rb +1483 -0
  32. data/lib/bunny/socket.rb +14 -0
  33. data/lib/bunny/ssl_socket.rb +14 -0
  34. data/lib/bunny/test_kit.rb +41 -0
  35. data/lib/bunny/timeout.rb +7 -0
  36. data/lib/bunny/transport.rb +526 -0
  37. data/lib/bunny/version.rb +6 -0
  38. data/lib/bunny/versioned_delivery_tag.rb +28 -0
  39. data/lib/bunny.rb +92 -0
  40. metadata +127 -0
@@ -0,0 +1,275 @@
1
+ require 'amq/protocol'
2
+
3
+ module Bunny
4
+ # Represents AMQP 0.9.1 exchanges.
5
+ #
6
+ # @see http://rubybunny.info/articles/exchanges.html Exchanges and Publishing guide
7
+ # @see http://rubybunny.info/articles/extensions.html RabbitMQ Extensions guide
8
+ class Exchange
9
+
10
+ #
11
+ # API
12
+ #
13
+
14
+ # @return [Bunny::Channel]
15
+ attr_reader :channel
16
+
17
+ # @return [String]
18
+ attr_reader :name
19
+
20
+ # Type of this exchange (one of: :direct, :fanout, :topic, :headers).
21
+ # @return [Symbol]
22
+ attr_reader :type
23
+
24
+ # @return [Symbol]
25
+ # @api plugin
26
+ attr_reader :status
27
+
28
+ # Options hash this exchange instance was instantiated with
29
+ # @return [Hash]
30
+ attr_accessor :opts
31
+
32
+
33
+ # The default exchange. This exchange is a direct exchange that is predefined by the broker
34
+ # and that cannot be removed. Every queue is bound to this exchange by default with
35
+ # the following routing semantics: messages will be routed to the queue with the same
36
+ # name as the message's routing key. In other words, if a message is published with
37
+ # a routing key of "weather.usa.ca.sandiego" and there is a queue with this name,
38
+ # the message will be routed to the queue.
39
+ #
40
+ # @param [Bunny::Channel] channel_or_connection Channel to use. {Bunny::Session} instances
41
+ # are only supported for backwards compatibility.
42
+ #
43
+ # @example Publishing a messages to the tasks queue
44
+ # channel = Bunny::Channel.new(connection)
45
+ # tasks_queue = channel.queue("tasks")
46
+ # Bunny::Exchange.default(channel).publish("make clean", :routing_key => "tasks")
47
+ #
48
+ # @see http://rubybunny.info/articles/exchanges.html Exchanges and Publishing guide
49
+ # @see http://www.rabbitmq.com/resources/specs/amqp0-9-1.pdf AMQP 0.9.1 specification (Section 2.1.2.4)
50
+ # @note Do not confuse the default exchange with amq.direct: amq.direct is a pre-defined direct
51
+ # exchange that doesn't have any special routing semantics.
52
+ # @return [Exchange] An instance that corresponds to the default exchange (of type direct).
53
+ # @api public
54
+ def self.default(channel_or_connection)
55
+ self.new(channel_or_connection, :direct, AMQ::Protocol::EMPTY_STRING, :no_declare => true)
56
+ end
57
+
58
+ # @param [Bunny::Channel] channel Channel this exchange will use.
59
+ # @param [Symbol,String] type Exchange type
60
+ # @param [String] name Exchange name
61
+ # @param [Hash] opts Exchange properties
62
+ #
63
+ # @option opts [Boolean] :durable (false) Should this exchange be durable?
64
+ # @option opts [Boolean] :auto_delete (false) Should this exchange be automatically deleted when it is no longer used?
65
+ # @option opts [Boolean] :arguments ({}) Additional optional arguments (typically used by RabbitMQ extensions and plugins)
66
+ #
67
+ # @see Bunny::Channel#topic
68
+ # @see Bunny::Channel#fanout
69
+ # @see Bunny::Channel#direct
70
+ # @see Bunny::Channel#headers
71
+ # @see http://rubybunny.info/articles/exchanges.html Exchanges and Publishing guide
72
+ # @see http://rubybunny.info/articles/extensions.html RabbitMQ Extensions guide
73
+ # @api public
74
+ def initialize(channel, type, name, opts = {})
75
+ @channel = channel
76
+ @name = name
77
+ @type = type
78
+ @options = self.class.add_default_options(name, opts)
79
+
80
+ @durable = @options[:durable]
81
+ @auto_delete = @options[:auto_delete]
82
+ @internal = @options[:internal]
83
+ @arguments = @options[:arguments]
84
+
85
+ @bindings = Set.new
86
+
87
+ declare! unless opts[:no_declare] || predeclared? || (@name == AMQ::Protocol::EMPTY_STRING)
88
+
89
+ @channel.register_exchange(self)
90
+ end
91
+
92
+ # @return [Boolean] true if this exchange was declared as durable (will survive broker restart).
93
+ # @api public
94
+ def durable?
95
+ @durable
96
+ end # durable?
97
+
98
+ # @return [Boolean] true if this exchange was declared as automatically deleted (deleted as soon as last consumer unbinds).
99
+ # @api public
100
+ def auto_delete?
101
+ @auto_delete
102
+ end # auto_delete?
103
+
104
+ # @return [Boolean] true if this exchange is internal (used solely for exchange-to-exchange
105
+ # bindings and cannot be published to by clients)
106
+ def internal?
107
+ @internal
108
+ end
109
+
110
+ # @return [Hash] Additional optional arguments (typically used by RabbitMQ extensions and plugins)
111
+ # @api public
112
+ def arguments
113
+ @arguments
114
+ end
115
+
116
+
117
+ # Publishes a message
118
+ #
119
+ # @param [String] payload Message payload. It will never be modified by Bunny or RabbitMQ in any way.
120
+ # @param [Hash] opts Message properties (metadata) and delivery settings
121
+ #
122
+ # @option opts [String] :routing_key Routing key
123
+ # @option opts [Boolean] :persistent Should the message be persisted to disk?
124
+ # @option opts [Boolean] :mandatory Should the message be returned if it cannot be routed to any queue?
125
+ # @option opts [Integer] :timestamp A timestamp associated with this message
126
+ # @option opts [Integer] :expiration Expiration time after which the message will be deleted
127
+ # @option opts [String] :type Message type, e.g. what type of event or command this message represents. Can be any string
128
+ # @option opts [String] :reply_to Queue name other apps should send the response to
129
+ # @option opts [String] :content_type Message content type (e.g. application/json)
130
+ # @option opts [String] :content_encoding Message content encoding (e.g. gzip)
131
+ # @option opts [String] :correlation_id Message correlated to this one, e.g. what request this message is a reply for
132
+ # @option opts [Integer] :priority Message priority, 0 to 9. Not used by RabbitMQ, only applications
133
+ # @option opts [String] :message_id Any message identifier
134
+ # @option opts [String] :user_id Optional user ID. Verified by RabbitMQ against the actual connection username
135
+ # @option opts [String] :app_id Optional application ID
136
+ #
137
+ # @return [Bunny::Exchange] Self
138
+ # @see http://rubybunny.info/articles/exchanges.html Exchanges and Publishing guide
139
+ # @api public
140
+ def publish(payload, opts = {})
141
+ @channel.basic_publish(payload, self.name, (opts.delete(:routing_key) || opts.delete(:key)), opts)
142
+
143
+ self
144
+ end
145
+
146
+
147
+ # Deletes the exchange unless it is predeclared
148
+ #
149
+ # @param [Hash] opts Options
150
+ #
151
+ # @option opts [Boolean] if_unused (false) Should this exchange be deleted only if it is no longer used
152
+ #
153
+ # @see http://rubybunny.info/articles/exchanges.html Exchanges and Publishing guide
154
+ # @api public
155
+ def delete(opts = {})
156
+ @channel.deregister_exchange(self)
157
+ @channel.exchange_delete(@name, opts) unless predeclared?
158
+ end
159
+
160
+ # Binds an exchange to another (source) exchange using exchange.bind AMQP 0.9.1 extension
161
+ # that RabbitMQ provides.
162
+ #
163
+ # @param [String] source Source exchange name
164
+ # @param [Hash] opts Options
165
+ #
166
+ # @option opts [String] routing_key (nil) Routing key used for binding
167
+ # @option opts [Hash] arguments ({}) Optional arguments
168
+ #
169
+ # @return [Bunny::Exchange] Self
170
+ # @see http://rubybunny.info/articles/exchanges.html Exchanges and Publishing guide
171
+ # @see http://rubybunny.info/articles/bindings.html Bindings guide
172
+ # @see http://rubybunny.info/articles/extensions.html RabbitMQ Extensions guide
173
+ # @api public
174
+ def bind(source, opts = {})
175
+ @channel.exchange_bind(source, self, opts)
176
+ @bindings.add(source: source, opts: opts)
177
+
178
+ self
179
+ end
180
+
181
+ # Unbinds an exchange from another (source) exchange using exchange.unbind AMQP 0.9.1 extension
182
+ # that RabbitMQ provides.
183
+ #
184
+ # @param [String] source Source exchange name
185
+ # @param [Hash] opts Options
186
+ #
187
+ # @option opts [String] routing_key (nil) Routing key used for binding
188
+ # @option opts [Hash] arguments ({}) Optional arguments
189
+ #
190
+ # @return [Bunny::Exchange] Self
191
+ # @see http://rubybunny.info/articles/exchanges.html Exchanges and Publishing guide
192
+ # @see http://rubybunny.info/articles/bindings.html Bindings guide
193
+ # @see http://rubybunny.info/articles/extensions.html RabbitMQ Extensions guide
194
+ # @api public
195
+ def unbind(source, opts = {})
196
+ @channel.exchange_unbind(source, self, opts)
197
+ @bindings.delete(source: source, opts: opts)
198
+
199
+ self
200
+ end
201
+
202
+ # Defines a block that will handle returned messages
203
+ # @see http://rubybunny.info/articles/exchanges.html
204
+ # @api public
205
+ def on_return(&block)
206
+ @on_return = block
207
+
208
+ self
209
+ end
210
+
211
+ # Waits until all outstanding publisher confirms on the channel
212
+ # arrive.
213
+ #
214
+ # This is a convenience method that delegates to {Bunny::Channel#wait_for_confirms}
215
+ #
216
+ # @api public
217
+ def wait_for_confirms
218
+ @channel.wait_for_confirms
219
+ end
220
+
221
+ # @private
222
+ def recover_from_network_failure
223
+ declare! unless @options[:no_declare] ||predefined?
224
+
225
+ @bindings.each do |b|
226
+ bind(b[:source], b[:opts])
227
+ end
228
+ end
229
+
230
+
231
+ #
232
+ # Implementation
233
+ #
234
+
235
+ # @private
236
+ def handle_return(basic_return, properties, content)
237
+ if @on_return
238
+ @on_return.call(basic_return, properties, content)
239
+ else
240
+ # TODO: log a warning
241
+ end
242
+ end
243
+
244
+ # @return [Boolean] true if this exchange is a pre-defined one (amq.direct, amq.fanout, amq.match and so on)
245
+ def predefined?
246
+ (@name == AMQ::Protocol::EMPTY_STRING) || !!(@name =~ /^amq\.(direct|fanout|topic|headers|match)/i)
247
+ end # predefined?
248
+ alias predeclared? predefined?
249
+
250
+ protected
251
+
252
+ # @private
253
+ def declare!
254
+ @channel.exchange_declare(@name, @type, @options)
255
+ end
256
+
257
+ # @private
258
+ def self.add_default_options(name, opts)
259
+ # :nowait is always false for Bunny
260
+ h = { :queue => name, :nowait => false }.merge(opts)
261
+
262
+ if name.empty?
263
+ {
264
+ :passive => false,
265
+ :durable => false,
266
+ :auto_delete => false,
267
+ :internal => false,
268
+ :arguments => nil
269
+ }.merge(h)
270
+ else
271
+ h
272
+ end
273
+ end
274
+ end
275
+ end
@@ -0,0 +1,56 @@
1
+ module Bunny
2
+ # @private
3
+ module Framing
4
+ ENCODINGS_SUPPORTED = defined? Encoding
5
+ HEADER_SLICE = (0..6).freeze
6
+ DATA_SLICE = (7..-1).freeze
7
+ PAYLOAD_SLICE = (0..-2).freeze
8
+
9
+ # @private
10
+ module String
11
+ class Frame < AMQ::Protocol::Frame
12
+ def self.decode(string)
13
+ header = string[HEADER_SLICE]
14
+ type, channel, size = self.decode_header(header)
15
+ data = string[DATA_SLICE]
16
+ payload = data[PAYLOAD_SLICE]
17
+ frame_end = data[-1, 1]
18
+
19
+ frame_end.force_encoding(AMQ::Protocol::Frame::FINAL_OCTET.encoding) if ENCODINGS_SUPPORTED
20
+
21
+ # 1) the size is miscalculated
22
+ if payload.bytesize != size
23
+ raise BadLengthError.new(size, payload.bytesize)
24
+ end
25
+
26
+ # 2) the size is OK, but the string doesn't end with FINAL_OCTET
27
+ raise NoFinalOctetError.new if frame_end != AMQ::Protocol::Frame::FINAL_OCTET
28
+
29
+ self.new(type, payload, channel)
30
+ end
31
+ end
32
+ end # String
33
+
34
+
35
+ # @private
36
+ module IO
37
+ class Frame < AMQ::Protocol::Frame
38
+ def self.decode(io)
39
+ header = io.read(7)
40
+ type, channel, size = self.decode_header(header)
41
+ data = io.read_fully(size + 1)
42
+ payload, frame_end = data[PAYLOAD_SLICE], data[-1, 1]
43
+
44
+ # 1) the size is miscalculated
45
+ if payload.bytesize != size
46
+ raise BadLengthError.new(size, payload.bytesize)
47
+ end
48
+
49
+ # 2) the size is OK, but the string doesn't end with FINAL_OCTET
50
+ raise NoFinalOctetError.new if frame_end != AMQ::Protocol::Frame::FINAL_OCTET
51
+ self.new(type, payload, channel)
52
+ end # self.from
53
+ end # Frame
54
+ end # IO
55
+ end # Framing
56
+ end # Bunny
@@ -0,0 +1,83 @@
1
+ require "bunny/versioned_delivery_tag"
2
+
3
+ module Bunny
4
+ # Wraps {AMQ::Protocol::Basic::GetOk} to
5
+ # provide access to the delivery properties as immutable hash as
6
+ # well as methods.
7
+ class GetResponse
8
+
9
+ #
10
+ # Behaviors
11
+ #
12
+
13
+ include Enumerable
14
+
15
+ #
16
+ # API
17
+ #
18
+
19
+ # @return [Bunny::Channel] Channel this basic.get-ok response is on
20
+ attr_reader :channel
21
+
22
+ # @private
23
+ def initialize(get_ok, channel)
24
+ @get_ok = get_ok
25
+ @hash = {
26
+ :delivery_tag => @get_ok.delivery_tag,
27
+ :redelivered => @get_ok.redelivered,
28
+ :exchange => @get_ok.exchange,
29
+ :routing_key => @get_ok.routing_key,
30
+ :channel => channel
31
+ }
32
+ @channel = channel
33
+ end
34
+
35
+ # Iterates over the delivery properties
36
+ # @see Enumerable#each
37
+ def each(*args, &block)
38
+ @hash.each(*args, &block)
39
+ end
40
+
41
+ # Accesses delivery properties by key
42
+ # @see Hash#[]
43
+ def [](k)
44
+ @hash[k]
45
+ end
46
+
47
+ # @return [Hash] Hash representation of this delivery info
48
+ def to_hash
49
+ @hash
50
+ end
51
+
52
+ # @private
53
+ def to_s
54
+ to_hash.to_s
55
+ end
56
+
57
+ # @private
58
+ def inspect
59
+ to_hash.inspect
60
+ end
61
+
62
+ # @return [String] Delivery identifier that is used to acknowledge, reject and nack deliveries
63
+ def delivery_tag
64
+ @get_ok.delivery_tag
65
+ end
66
+
67
+ # @return [Boolean] true if this delivery is a redelivery (the message was requeued at least once)
68
+ def redelivered
69
+ @get_ok.redelivered
70
+ end
71
+ alias redelivered? redelivered
72
+
73
+ # @return [String] Name of the exchange this message was published to
74
+ def exchange
75
+ @get_ok.exchange
76
+ end
77
+
78
+ # @return [String] Routing key this message was published with
79
+ def routing_key
80
+ @get_ok.routing_key
81
+ end
82
+ end
83
+ end
@@ -0,0 +1,71 @@
1
+ require "thread"
2
+ require "amq/protocol/client"
3
+ require "amq/protocol/frame"
4
+
5
+ module Bunny
6
+ # Periodically sends heartbeats, keeping track of the last publishing activity.
7
+ #
8
+ # @private
9
+ class HeartbeatSender
10
+
11
+ #
12
+ # API
13
+ #
14
+
15
+ def initialize(transport, logger)
16
+ @transport = transport
17
+ @logger = logger
18
+ @mutex = Monitor.new
19
+
20
+ @last_activity_time = Time.now
21
+ end
22
+
23
+ def start(period = 30)
24
+ @mutex.synchronize do
25
+ # calculate interval as half the given period plus
26
+ # some compensation for Ruby's implementation inaccuracy
27
+ # (we cannot get at the nanos level the Java client uses, and
28
+ # our approach is simplistic). MK.
29
+ @interval = [(period / 2) - 1, 0.4].max
30
+
31
+ @thread = Thread.new(&method(:run))
32
+ @thread.report_on_exception = false if @thread.respond_to?(:report_on_exception)
33
+ end
34
+ end
35
+
36
+ def stop
37
+ @mutex.synchronize { @thread.exit }
38
+ end
39
+
40
+ def signal_activity!
41
+ @last_activity_time = Time.now
42
+ end
43
+
44
+ protected
45
+
46
+ def run
47
+ begin
48
+ loop do
49
+ self.beat
50
+
51
+ sleep @interval
52
+ end
53
+ rescue IOError => ioe
54
+ @logger.error "I/O error in the hearbeat sender: #{ioe.message}"
55
+ stop
56
+ rescue Exception => e
57
+ @logger.error "Error in the hearbeat sender: #{e.message}"
58
+ stop
59
+ end
60
+ end
61
+
62
+ def beat
63
+ now = Time.now
64
+
65
+ if now > (@last_activity_time + @interval)
66
+ @logger.debug { "Sending a heartbeat, last activity time: #{@last_activity_time}, interval (s): #{@interval}" }
67
+ @transport.write_without_timeout(AMQ::Protocol::HeartbeatFrame.encode, true)
68
+ end
69
+ end
70
+ end
71
+ end
@@ -0,0 +1,57 @@
1
+ require "bunny/cruby/socket"
2
+
3
+ module Bunny
4
+ module JRuby
5
+ # TCP socket extension that uses Socket#readpartial to avoid excessive CPU
6
+ # burn after some time. See issue #165.
7
+ # @private
8
+ module Socket
9
+ include Bunny::Socket
10
+
11
+ def self.open(host, port, options = {})
12
+ socket = ::Socket.tcp(host, port, nil, nil,
13
+ connect_timeout: options[:connect_timeout])
14
+ if ::Socket.constants.include?('TCP_NODELAY') || ::Socket.constants.include?(:TCP_NODELAY)
15
+ socket.setsockopt(::Socket::IPPROTO_TCP, ::Socket::TCP_NODELAY, true)
16
+ end
17
+ socket.setsockopt(::Socket::SOL_SOCKET, ::Socket::SO_KEEPALIVE, true) if options.fetch(:keepalive, true)
18
+ socket.extend self
19
+ socket.options = { :host => host, :port => port }.merge(options)
20
+ socket
21
+ rescue Errno::ETIMEDOUT
22
+ raise ClientTimeout
23
+ end
24
+
25
+ # Reads given number of bytes with an optional timeout
26
+ #
27
+ # @param [Integer] count How many bytes to read
28
+ # @param [Integer] timeout Timeout
29
+ #
30
+ # @return [String] Data read from the socket
31
+ # @api public
32
+ def read_fully(count, timeout = nil)
33
+ value = ''
34
+
35
+ begin
36
+ loop do
37
+ value << read_nonblock(count - value.bytesize)
38
+ break if value.bytesize >= count
39
+ end
40
+ rescue EOFError
41
+ # JRuby specific fix via https://github.com/jruby/jruby/issues/1694#issuecomment-54873532
42
+ IO.select([self], nil, nil, timeout)
43
+ retry
44
+ rescue *READ_RETRY_EXCEPTION_CLASSES
45
+ if IO.select([self], nil, nil, timeout)
46
+ retry
47
+ else
48
+ raise Timeout::Error, "IO timeout when reading #{count} bytes"
49
+ end
50
+ end
51
+
52
+ value
53
+ end # read_fully
54
+
55
+ end
56
+ end
57
+ end
@@ -0,0 +1,58 @@
1
+ module Bunny
2
+ module JRuby
3
+ begin
4
+ require "bunny/cruby/ssl_socket"
5
+ require "openssl"
6
+
7
+ # TLS-enabled TCP socket that implements convenience
8
+ # methods found in Bunny::Socket.
9
+ class SSLSocket < Bunny::SSLSocket
10
+
11
+ def initialize(*args)
12
+ super
13
+ @__bunny_socket_eof_flag__ = false
14
+ end
15
+
16
+ # Reads given number of bytes with an optional timeout
17
+ #
18
+ # @param [Integer] count How many bytes to read
19
+ # @param [Integer] timeout Timeout
20
+ #
21
+ # @return [String] Data read from the socket
22
+ # @api public
23
+ def read_fully(count, timeout = nil)
24
+ return nil if @__bunny_socket_eof_flag__
25
+
26
+ value = ''
27
+ begin
28
+ loop do
29
+ value << read_nonblock(count - value.bytesize)
30
+ break if value.bytesize >= count
31
+ end
32
+ rescue EOFError => e
33
+ @__bunny_socket_eof_flag__ = true
34
+ rescue OpenSSL::SSL::SSLError => e
35
+ if e.message == "read would block"
36
+ if IO.select([self], nil, nil, timeout)
37
+ retry
38
+ else
39
+ raise Timeout::Error, "IO timeout when reading #{count} bytes"
40
+ end
41
+ else
42
+ raise e
43
+ end
44
+ rescue *READ_RETRY_EXCEPTION_CLASSES => e
45
+ if IO.select([self], nil, nil, timeout)
46
+ retry
47
+ else
48
+ raise Timeout::Error, "IO timeout when reading #{count} bytes"
49
+ end
50
+ end
51
+ value
52
+ end
53
+ end
54
+ rescue LoadError => le
55
+ puts "Could not load OpenSSL"
56
+ end
57
+ end
58
+ end