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
@@ -0,0 +1,478 @@
1
+ require "socket"
2
+ require "thread"
3
+
4
+ require "bunny/transport"
5
+ require "bunny/channel_id_allocator"
6
+ require "bunny/heartbeat_sender"
7
+ require "bunny/main_loop"
8
+
9
+ require "bunny/concurrent/condition"
10
+
11
+ require "amq/protocol/client"
12
+ require "amq/settings"
13
+
14
+ module Bunny
15
+ class Session
16
+
17
+ DEFAULT_HOST = "127.0.0.1"
18
+ DEFAULT_VHOST = "/"
19
+ DEFAULT_USER = "guest"
20
+ DEFAULT_PASSWORD = "guest"
21
+ # 0 means "no heartbeat". This is the same default RabbitMQ Java client and amqp gem
22
+ # use.
23
+ DEFAULT_HEARTBEAT = 0
24
+ # 128K
25
+ DEFAULT_FRAME_MAX = 131072
26
+
27
+ # backwards compatibility
28
+ CONNECT_TIMEOUT = Transport::DEFAULT_CONNECTION_TIMEOUT
29
+
30
+
31
+ DEFAULT_CLIENT_PROPERTIES = {
32
+ # once we support AMQP 0.9.1 extensions, this needs to be updated. MK.
33
+ :capabilities => {},
34
+ :product => "Bunny",
35
+ :platform => ::RUBY_DESCRIPTION,
36
+ :version => Bunny::VERSION,
37
+ :information => "http://github.com/ruby-amqp/bunny"
38
+ }
39
+
40
+ DEFAULT_LOCALE = "en_GB"
41
+
42
+
43
+ #
44
+ # API
45
+ #
46
+
47
+ attr_reader :status, :host, :port, :heartbeat, :user, :pass, :vhost, :frame_max, :default_channel
48
+ attr_reader :server_capabilities, :server_properties, :server_authentication_mechanisms, :server_locales
49
+ attr_reader :default_channel
50
+ attr_reader :channel_id_allocator
51
+
52
+ def initialize(connection_string_or_opts = Hash.new, optz = Hash.new)
53
+ opts = case connection_string_or_opts
54
+ when String then
55
+ AMQ::Settings.parse_amqp_url(connection_string_or_opts)
56
+ when Hash then
57
+ connection_string_or_opts
58
+ end.merge(optz)
59
+
60
+ @opts = opts
61
+ @host = self.hostname_from(opts)
62
+ @port = self.port_from(opts)
63
+ @user = self.username_from(opts)
64
+ @pass = self.password_from(opts)
65
+ @vhost = self.vhost_from(opts)
66
+ @logfile = opts[:logfile]
67
+ @logging = opts[:logging] || false
68
+
69
+ @status = :not_connected
70
+ @frame_max = opts[:frame_max] || DEFAULT_FRAME_MAX
71
+ # currently ignored
72
+ @channel_max = opts[:channel_max] || 0
73
+ @heartbeat = self.heartbeat_from(opts)
74
+
75
+ @client_properties = opts[:properties] || DEFAULT_CLIENT_PROPERTIES
76
+ @mechanism = "PLAIN"
77
+ @locale = @opts.fetch(:locale, DEFAULT_LOCALE)
78
+
79
+ @channel_id_allocator = ChannelIdAllocator.new
80
+ @channel_mutex = Mutex.new
81
+ @channels = Hash.new
82
+
83
+ # Create channel 0
84
+ @channel0 = Bunny::Channel.new(self, 0)
85
+
86
+ @continuations = ::Queue.new
87
+ end
88
+
89
+ def hostname; self.host; end
90
+ def username; self.user; end
91
+ def password; self.pass; end
92
+ def virtual_host; self.vhost; end
93
+
94
+ def uses_tls?
95
+ @transport.uses_tls?
96
+ end
97
+ alias tls? uses_tls?
98
+
99
+ def uses_ssl?
100
+ @transport.uses_ssl?
101
+ end
102
+ alias ssl? uses_ssl?
103
+
104
+ def channel0
105
+ @channel0
106
+ end
107
+
108
+ def channel
109
+ @default_channel
110
+ end
111
+
112
+
113
+ def start
114
+ @status = :connecting
115
+
116
+ self.initialize_transport
117
+
118
+ self.init_connection
119
+ self.open_connection
120
+
121
+ self.start_main_loop
122
+
123
+ @default_channel = self.create_channel
124
+ end
125
+
126
+
127
+ def create_channel(n = nil)
128
+ if n && (ch = @channels[n])
129
+ ch
130
+ else
131
+ ch = Bunny::Channel.new(self, n)
132
+ ch.open
133
+ ch
134
+ end
135
+ end
136
+
137
+ def close
138
+ if @transport.open?
139
+ close_all_channels
140
+
141
+ Bunny::Timer.timeout(@disconnect_timeout, ClientTimeout) do
142
+ self.close_connection(false)
143
+ end
144
+ end
145
+ end
146
+ alias stop close
147
+
148
+ def with_channel(n = nil)
149
+ ch = create_channel(n)
150
+ yield ch
151
+ ch.close
152
+
153
+ self
154
+ end
155
+
156
+
157
+ def connecting?
158
+ status == :connecting
159
+ end
160
+
161
+ def closed?
162
+ status == :closed
163
+ end
164
+
165
+ def open?
166
+ status == :open || status == :connected
167
+ end
168
+ alias connected? open?
169
+
170
+ def prefetch(prefetch_count)
171
+ self.basic_qos(prefetch_count, true)
172
+ end
173
+
174
+ #
175
+ # Implementation
176
+ #
177
+
178
+
179
+ def open_channel(ch)
180
+ n = ch.number
181
+ self.register_channel(ch)
182
+
183
+ @transport.send_frame(AMQ::Protocol::Channel::Open.encode(n, AMQ::Protocol::EMPTY_STRING))
184
+ @last_channel_open_ok = @continuations.pop
185
+ raise_if_continuation_resulted_in_a_connection_error!
186
+
187
+ @last_channel_open_ok
188
+ end
189
+
190
+ def close_channel(ch)
191
+ n = ch.number
192
+
193
+ @transport.send_frame(AMQ::Protocol::Channel::Close.encode(n, 200, "Goodbye", 0, 0))
194
+ @last_channel_close_ok = @continuations.pop
195
+ raise_if_continuation_resulted_in_a_connection_error!
196
+
197
+ self.unregister_channel(ch)
198
+ @last_channel_close_ok
199
+ end
200
+
201
+ def close_all_channels
202
+ @channels.reject {|n, ch| n == 0 || !ch.open? }.each do |_, ch|
203
+ Bunny::Timer.timeout(@disconnect_timeout, ClientTimeout) { ch.close }
204
+ end
205
+ end
206
+
207
+ def close_connection(sync = true)
208
+ @transport.send_frame(AMQ::Protocol::Connection::Close.encode(200, "Goodbye", 0, 0))
209
+
210
+ if @heartbeat_sender
211
+ @heartbeat_sender.stop
212
+ end
213
+ @status = :not_connected
214
+
215
+ if sync
216
+ @last_connection_close_ok = @continuations.pop
217
+ end
218
+ end
219
+
220
+ def handle_frame(ch_number, method)
221
+ # puts "Session#handle_frame on #{ch_number}: #{method.inspect}"
222
+ case method
223
+ when AMQ::Protocol::Channel::OpenOk then
224
+ @continuations.push(method)
225
+ when AMQ::Protocol::Channel::CloseOk then
226
+ @continuations.push(method)
227
+ when AMQ::Protocol::Connection::Close then
228
+ @last_connection_error = instantiate_connection_level_exception(method)
229
+ @contunuations.push(method)
230
+ when AMQ::Protocol::Connection::CloseOk then
231
+ @last_connection_close_ok = method
232
+ begin
233
+ @continuations.clear
234
+
235
+ @event_loop.stop
236
+ @event_loop = nil
237
+
238
+ @transport.close
239
+ rescue Exception => e
240
+ puts e.class.name
241
+ puts e.message
242
+ puts e.backtrace
243
+ ensure
244
+ @active_continuation.notify_all if @active_continuation
245
+ @active_continuation = false
246
+ end
247
+ when AMQ::Protocol::Channel::Close then
248
+ begin
249
+ ch = @channels[ch_number]
250
+ ch.handle_method(method)
251
+ ensure
252
+ self.unregister_channel(ch)
253
+ end
254
+ when AMQ::Protocol::Basic::GetEmpty then
255
+ @channels[ch_number].handle_basic_get_empty(method)
256
+ else
257
+ @channels[ch_number].handle_method(method)
258
+ end
259
+ end
260
+
261
+ def raise_if_continuation_resulted_in_a_connection_error!
262
+ raise @last_connection_error if @last_connection_error
263
+ end
264
+
265
+ def handle_frameset(ch_number, frames)
266
+ method = frames.first
267
+
268
+ case method
269
+ when AMQ::Protocol::Basic::GetOk then
270
+ @channels[ch_number].handle_basic_get_ok(*frames)
271
+ when AMQ::Protocol::Basic::GetEmpty then
272
+ @channels[ch_number].handle_basic_get_empty(*frames)
273
+ when AMQ::Protocol::Basic::Return then
274
+ @channels[ch_number].handle_basic_return(*frames)
275
+ else
276
+ @channels[ch_number].handle_frameset(*frames)
277
+ end
278
+ end
279
+
280
+ def send_raw(*args)
281
+ @transport.write(*args)
282
+ end
283
+
284
+ def instantiate_connection_level_exception(frame)
285
+ case frame
286
+ when AMQ::Protocol::Connection::Close then
287
+ klass = case frame.reply_code
288
+ when 504 then
289
+ ChannelError
290
+ end
291
+
292
+ klass.new("Connection-level error: #{frame.reply_text}", self, frame)
293
+ end
294
+ end
295
+
296
+ def hostname_from(options)
297
+ options[:host] || options[:hostname] || DEFAULT_HOST
298
+ end
299
+
300
+ def port_from(options)
301
+ fallback = if options[:tls] || options[:ssl]
302
+ AMQ::Protocol::TLS_PORT
303
+ else
304
+ AMQ::Protocol::DEFAULT_PORT
305
+ end
306
+
307
+ options.fetch(:port, fallback)
308
+ end
309
+
310
+ def vhost_from(options)
311
+ options[:virtual_host] || options[:vhost] || DEFAULT_VHOST
312
+ end
313
+
314
+ def username_from(options)
315
+ options[:username] || options[:user] || DEFAULT_USER
316
+ end
317
+
318
+ def password_from(options)
319
+ options[:password] || options[:pass] || options [:pwd] || DEFAULT_PASSWORD
320
+ end
321
+
322
+ def heartbeat_from(options)
323
+ options[:heartbeat] || options[:heartbeat_interval] || options[:requested_heartbeat] || DEFAULT_HEARTBEAT
324
+ end
325
+
326
+ def next_channel_id
327
+ @channel_id_allocator.next_channel_id
328
+ end
329
+
330
+ def release_channel_id(i)
331
+ @channel_id_allocator.release_channel_id(i)
332
+ end
333
+
334
+ def register_channel(ch)
335
+ @channel_mutex.synchronize do
336
+ @channels[ch.number] = ch
337
+ end
338
+ end
339
+
340
+ def unregister_channel(ch)
341
+ @channel_mutex.synchronize do
342
+ n = ch.number
343
+
344
+ self.release_channel_id(n)
345
+ @channels.delete(ch.number)
346
+ end
347
+ end
348
+
349
+ def start_main_loop
350
+ @event_loop = MainLoop.new(@transport, self)
351
+ @event_loop.start
352
+ end
353
+
354
+ def signal_activity!
355
+ @heartbeat_sender.signal_activity! if @heartbeat_sender
356
+ end
357
+
358
+
359
+ # Sends frame to the peer, checking that connection is open.
360
+ # Exposed primarily for Bunny::Channel
361
+ #
362
+ # @raise [ConnectionClosedError]
363
+ # @private
364
+ def send_frame(frame)
365
+ if closed?
366
+ raise ConnectionClosedError.new(frame)
367
+ else
368
+ @transport.send_raw(frame.encode)
369
+ end
370
+ end
371
+
372
+ # Sends multiple frames, one by one. For thread safety this method takes a channel
373
+ # object and synchronizes on it.
374
+ #
375
+ # @api public
376
+ def send_frameset(frames, channel)
377
+ # some developers end up sharing channels between threads and when multiple
378
+ # threads publish on the same channel aggressively, at some point frames will be
379
+ # delivered out of order and broker will raise 505 UNEXPECTED_FRAME exception.
380
+ # If we synchronize on the channel, however, this is both thread safe and pretty fine-grained
381
+ # locking. Note that "single frame" methods do not need this kind of synchronization. MK.
382
+ channel.synchronize do
383
+ frames.each { |frame| @transport.send_frame(frame) }
384
+ @transport.flush
385
+ end
386
+ end # send_frameset(frames)
387
+
388
+ protected
389
+
390
+ def socket_open?
391
+ !@socket.nil? && !@socket.closed?
392
+ end
393
+
394
+ def init_connection
395
+ self.send_preamble
396
+
397
+ connection_start = @transport.read_next_frame.decode_payload
398
+
399
+ @server_properties = connection_start.server_properties
400
+ @server_capabilities = @server_properties["capabilities"]
401
+
402
+ @server_authentication_mechanisms = (connection_start.mechanisms || "").split(" ")
403
+ @server_locales = Array(connection_start.locales)
404
+
405
+ @status = :connected
406
+ end
407
+
408
+ def open_connection
409
+ @transport.send_frame(AMQ::Protocol::Connection::StartOk.encode(@client_properties, @mechanism, self.encode_credentials(username, password), @locale))
410
+
411
+ frame = begin
412
+ @transport.read_next_frame
413
+ # frame timeout means the broker has closed the TCP connection, which it
414
+ # does per 0.9.1 spec.
415
+ rescue Errno::ECONNRESET, ClientTimeout, AMQ::Protocol::EmptyResponseError, EOFError => e
416
+ nil
417
+ end
418
+ if frame.nil?
419
+ @state = :closed
420
+ raise Bunny::PossibleAuthenticationFailureError.new(self.user, self.vhost, self.password.size)
421
+ end
422
+
423
+ connection_tune = frame.decode_payload
424
+ @frame_max = connection_tune.frame_max.freeze
425
+ @heartbeat ||= connection_tune.heartbeat
426
+
427
+ @transport.send_frame(AMQ::Protocol::Connection::TuneOk.encode(@channel_max, @frame_max, @heartbeat))
428
+ @transport.send_frame(AMQ::Protocol::Connection::Open.encode(self.vhost))
429
+
430
+ frame2 = begin
431
+ @transport.read_next_frame
432
+ # frame timeout means the broker has closed the TCP connection, which it
433
+ # does per 0.9.1 spec.
434
+ rescue Errno::ECONNRESET, ClientTimeout, AMQ::Protocol::EmptyResponseError, EOFError => e
435
+ nil
436
+ end
437
+ if frame2.nil?
438
+ @state = :closed
439
+ raise Bunny::PossibleAuthenticationFailureError.new(self.user, self.vhost, self.password.size)
440
+ end
441
+ connection_open_ok = frame2.decode_payload
442
+
443
+ @status = :open
444
+ if @heartbeat && @heartbeat > 0
445
+ initialize_heartbeat_sender
446
+ end
447
+
448
+ raise "could not open connection: server did not respond with connection.open-ok" unless connection_open_ok.is_a?(AMQ::Protocol::Connection::OpenOk)
449
+ end
450
+
451
+ def initialize_heartbeat_sender
452
+ @heartbeat_sender = HeartbeatSender.new(@transport)
453
+ @heartbeat_sender.start(@heartbeat)
454
+ end
455
+
456
+
457
+ def initialize_transport
458
+ @transport = Transport.new(@host, @port, @opts)
459
+ end
460
+
461
+ # Sends AMQ protocol header (also known as preamble).
462
+ def send_preamble
463
+ @transport.send_raw(AMQ::Protocol::PREAMBLE)
464
+ end
465
+
466
+
467
+
468
+
469
+ # @api plugin
470
+ # @see http://tools.ietf.org/rfc/rfc2595.txt RFC 2595
471
+ def encode_credentials(username, password)
472
+ "\0#{username}\0#{password}"
473
+ end # encode_credentials(username, password)
474
+ end # Session
475
+
476
+ # backwards compatibility
477
+ Client = Session
478
+ end