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