amq-client 0.7.0.alpha3 → 0.7.0.alpha4

Sign up to get free protection for your applications and to get access to all the features.
data/.gitignore CHANGED
@@ -6,3 +6,4 @@ doc/*
6
6
  .rvmrc
7
7
  # see http://bit.ly/h2WJPm for reasoning
8
8
  Gemfile.lock
9
+ vendor
data/Gemfile CHANGED
@@ -3,16 +3,17 @@
3
3
  source :rubygems
4
4
 
5
5
  # Use local clones if possible.
6
+ # If you want to use your local copy, just symlink it to vendor.
6
7
  def custom_gem(name, options = Hash.new)
7
- local_path = File.expand_path("../../#{name}", __FILE__)
8
- if ENV["USE_AMQP_CUSTOM_GEMS"] && File.directory?(local_path)
8
+ local_path = File.expand_path("../vendor/#{name}", __FILE__)
9
+ if File.exist?(local_path)
9
10
  gem name, options.merge(:path => local_path).delete_if { |key, _| [:git, :branch].include?(key) }
10
11
  else
11
12
  gem name, options
12
13
  end
13
14
  end
14
15
 
15
- gem "eventmachine", "0.12.10" #, "1.0.0.beta.3"
16
+ gem "eventmachine"
16
17
  # cool.io uses iobuffer that won't compile on JRuby
17
18
  # (and, probably, Windows)
18
19
  gem "cool.io", :platform => :ruby
data/amq-client.gemspec CHANGED
@@ -21,7 +21,7 @@ Gem::Specification.new do |s|
21
21
  s.extra_rdoc_files = ["README.textile"] + Dir.glob("doc/*")
22
22
 
23
23
  # Dependencies
24
- s.add_dependency "eventmachine", "~> 0.12.10"
24
+ s.add_dependency "eventmachine"
25
25
  s.add_dependency "amq-protocol"
26
26
 
27
27
 
@@ -17,7 +17,7 @@ end
17
17
 
18
18
 
19
19
  def amq_client_example(description = "", &block)
20
- AMQ::Client::Coolio.connect(:port => 5672, :vhost => "/amq_client_testbed") do |client|
20
+ AMQ::Client::CoolioClient.connect(:port => 5672, :vhost => "/amq_client_testbed") do |client|
21
21
  begin
22
22
  puts
23
23
  puts
@@ -0,0 +1,43 @@
1
+ #!/usr/bin/env ruby
2
+ # encoding: utf-8
3
+
4
+ __dir = File.join(File.dirname(File.expand_path(__FILE__)))
5
+ require File.join(__dir, "example_helper")
6
+
7
+
8
+ EM.run do
9
+ reconnector = Proc.new { |client, settings|
10
+ puts "Asked to reconnect to #{settings[:host]}:#{settings[:port]}"
11
+
12
+
13
+ }
14
+
15
+
16
+ AMQ::Client::EventMachineClient.connect(:port => 5672,
17
+ :vhost => "/amq_client_testbed",
18
+ :user => "amq_client_gem",
19
+ :password => "amq_client_gem_password",
20
+ :connection_timeout => 0.3,
21
+ :on_tcp_connection_failure => Proc.new { |settings| puts "Failed to connect, this was NOT expected"; EM.stop }) do |client|
22
+
23
+ client.on_tcp_connection_loss do |cl, settings|
24
+ puts "tcp_connection_loss handler kicks in"
25
+ cl.reconnect(1)
26
+ end
27
+
28
+ show_stopper = Proc.new {
29
+ client.disconnect {
30
+ puts "Disconnected. Exiting…"
31
+ EM.stop
32
+ }
33
+ }
34
+
35
+ Signal.trap "INT", show_stopper
36
+ Signal.trap "TERM", show_stopper
37
+
38
+ EM.add_timer(30, show_stopper)
39
+
40
+
41
+ puts "Connected, authenticated. To really exercise this example, shut AMQP broker down for a few seconds. If you don't it will exit gracefully in 30 seconds."
42
+ end
43
+ end
data/lib/amq/client.rb CHANGED
@@ -3,6 +3,9 @@
3
3
  require "amq/client/version"
4
4
  require "amq/client/exceptions"
5
5
  require "amq/client/adapter"
6
+ require "amq/client/channel"
7
+ require "amq/client/exchange"
8
+ require "amq/client/queue"
6
9
 
7
10
  begin
8
11
  require "amq/protocol/client"
@@ -13,3 +16,55 @@ rescue LoadError => exception
13
16
  raise exception
14
17
  end
15
18
  end
19
+
20
+ module AMQ
21
+ module Client
22
+ # List all the available as a hash of {adapter_name: metadata},
23
+ # where metadata are hash with :path and :const_name keys.
24
+ #
25
+ # @example
26
+ # AMQ::Client.adapters[:socket]
27
+ # # => {path: full_path, const_name: "SocketClient"}}
28
+ # @return [Hash]
29
+ # @api public
30
+ def self.adapters
31
+ @adapters ||= begin
32
+ root = File.expand_path("../client/adapters", __FILE__)
33
+ Dir.glob("#{root}/*.rb").inject(Hash.new) do |buffer, path|
34
+ name = path.match(/([^\/]+)\.rb$/)[1]
35
+ const_base = name.to_s.gsub(/(^|_)(.)/) { $2.upcase! }
36
+ meta = {:path => path, :const_name => "#{const_base}Client"}
37
+ buffer.merge!(name.to_sym => meta)
38
+ end
39
+ end
40
+ end
41
+
42
+ # Establishes connection to AMQ broker using given adapter
43
+ # (defaults to the socket adapter) and returns it. The new
44
+ # connection object is yielded to the block if it is given.
45
+ #
46
+ # @example
47
+ # AMQ::Client.connect(adapter: "socket") do |client|
48
+ # # Use the client.
49
+ # end
50
+ # @param [Hash] Connection parameters, including :adapter to use.
51
+ # @api public
52
+ def self.connect(settings = nil, &block)
53
+ adapter = (settings && settings.delete(:adapter)) || :socket
54
+ adapter = load_adapter(adapter)
55
+ adapter.connect(settings, &block)
56
+ end
57
+
58
+ # Loads adapter from amq/client/adapters.
59
+ #
60
+ # @raise [InvalidAdapterNameError] When loading attempt failed (LoadError was raised).
61
+ def self.load_adapter(adapter)
62
+ meta = self.adapters[adapter.to_sym]
63
+
64
+ require meta[:path]
65
+ const_get(meta[:const_name])
66
+ rescue LoadError
67
+ raise InvalidAdapterNameError.new(adapter)
68
+ end
69
+ end
70
+ end
@@ -4,6 +4,7 @@ require "amq/client/logging"
4
4
  require "amq/client/settings"
5
5
  require "amq/client/entity"
6
6
  require "amq/client/connection"
7
+ require "amq/client/channel"
7
8
 
8
9
  module AMQ
9
10
  # For overview of AMQP client adapters API, see {AMQ::Client::Adapter}
@@ -104,55 +105,25 @@ module AMQ
104
105
  end
105
106
 
106
107
 
107
- # @example Registering Channel implementation
108
- # Adapter.register_entity(:channel, Channel)
109
- # # ... so then I can do:
110
- # channel = client.channel(1)
111
- # # instead of:
112
- # channel = Channel.new(client, 1)
113
- def register_entity(name, klass)
114
- define_method(name) do |*args, &block|
115
- klass.new(self, *args, &block)
116
- end
117
- end
118
-
119
108
  # Establishes connection to AMQ broker and returns it. New connection object is yielded to
120
109
  # the block if it is given.
121
110
  #
111
+ # @example Specifying adapter via the :adapter option
112
+ # AMQ::Client::Adapter.connect(adapter: "socket")
113
+ # @example Specifying using custom adapter class
114
+ # AMQ::Client::SocketClient.connect
122
115
  # @param [Hash] Connection parameters, including :adapter to use.
123
116
  # @api public
124
117
  def connect(settings = nil, &block)
125
- if settings && settings[:adapter]
126
- adapter = load_adapter(settings[:adapter])
127
- else
128
- adapter = self
129
- end
118
+ # TODO: this doesn't look very nice, do we need it?
119
+ # Let's make it an instance thing by instance = self.new(settings)
120
+ @settings = settings = Settings.configure(settings)
130
121
 
131
- @settings = AMQ::Client::Settings.configure(settings)
132
- instance = adapter.new
133
- instance.establish_connection(@settings)
134
- # We don't need anything more, once the server receives the preable, he sends Connection.Start, we just have to reply.
122
+ instance = self.new
123
+ instance.establish_connection(settings)
124
+ instance.register_connection_callback(&block)
135
125
 
136
- if block
137
- block.call(instance)
138
-
139
- instance.disconnect
140
- else
141
- instance
142
- end
143
- end
144
-
145
-
146
- # Loads adapter from amq/client/adapters.
147
- #
148
- # @raise [InvalidAdapterNameError] When loading attempt failed (LoadError was raised).
149
- def load_adapter(adapter)
150
- require "amq/client/adapters/#{adapter}"
151
-
152
- const_name = adapter.to_s.gsub(/(^|_)(.)/) { $2.upcase! }
153
- const_get(const_name)
154
- rescue LoadError
155
- raise InvalidAdapterNameError.new(adapter)
126
+ instance
156
127
  end
157
128
 
158
129
  # @see AMQ::Client::Adapter
@@ -184,17 +155,14 @@ module AMQ
184
155
 
185
156
  include AMQ::Client::StatusMixin
186
157
 
158
+ extend RegisterEntityMixin
159
+
160
+ register_entity :channel, AMQ::Client::Channel
187
161
 
188
162
  #
189
163
  # API
190
164
  #
191
165
 
192
- def self.load_adapter(adapter)
193
- ClassMethods.load_adapter(adapter)
194
- end
195
-
196
-
197
-
198
166
  def initialize(*args)
199
167
  super(*args)
200
168
 
@@ -229,6 +197,8 @@ module AMQ
229
197
  # @todo This method should await broker's response with Close-Ok. {http://github.com/michaelklishin MK}.
230
198
  # @see #close_connection
231
199
  def disconnect(reply_code = 200, reply_text = "Goodbye", &block)
200
+ @intentionally_closing_connection = true
201
+
232
202
  self.on_disconnection(&block)
233
203
  closing!
234
204
  self.connection.close(reply_code, reply_text)
@@ -352,5 +322,3 @@ module AMQ
352
322
  end # Adapter
353
323
  end # Client
354
324
  end # AMQ
355
-
356
- require "amq/client/channel"
@@ -4,12 +4,11 @@
4
4
 
5
5
  require "cool.io"
6
6
  require "amq/client"
7
-
8
7
  require "amq/client/framing/string/frame"
9
8
 
10
9
  module AMQ
11
10
  module Client
12
- class Coolio
11
+ class CoolioClient
13
12
  class Socket < ::Coolio::TCPSocket
14
13
  attr_accessor :adapter
15
14
 
@@ -48,27 +47,31 @@ module AMQ
48
47
  end
49
48
  end
50
49
 
50
+ #
51
51
  # Behaviors
52
+ #
53
+
52
54
  include AMQ::Client::Adapter
55
+ include AMQ::Client::CallbacksMixin
53
56
 
54
57
  self.sync = false
55
58
 
59
+ #
56
60
  # API
61
+ #
62
+
57
63
  attr_accessor :socket
58
64
  attr_accessor :callbacks
59
65
  attr_accessor :connections
60
66
 
61
- class << self
62
- def connect(settings, &block)
63
- settings = self.settings.merge(settings)
64
- host, port = settings[:host], settings[:port]
65
- instance = new
66
- socket = Socket.connect(instance, settings[:host], settings[:port])
67
- socket.attach Cool.io::Loop.default
68
- instance.socket = socket
69
- instance.on_connection(&block)
70
- instance
71
- end
67
+ def establish_connection(settings)
68
+ socket = Socket.connect(self, settings[:host], settings[:port])
69
+ socket.attach(Cool.io::Loop.default)
70
+ self.socket = socket
71
+ end
72
+
73
+ def register_connection_callback(&block)
74
+ self.on_connection(&block)
72
75
  end
73
76
 
74
77
  def initialize
@@ -138,59 +141,6 @@ module AMQ
138
141
  end
139
142
 
140
143
 
141
-
142
- #
143
- # Callbacks
144
- #
145
-
146
- def redefine_callback(event, callable = nil, &block)
147
- f = (callable || block)
148
- # yes, re-assign!
149
- @callbacks[event] = [f]
150
-
151
- self
152
- end
153
-
154
- def define_callback(event, callable = nil, &block)
155
- f = (callable || block)
156
- @callbacks[event] ||= []
157
-
158
- @callbacks[event] << f if f
159
-
160
- self
161
- end # define_callback(event, &block)
162
- alias append_callback define_callback
163
-
164
- def prepend_callback(event, &block)
165
- @callbacks[event] ||= []
166
- @callbacks[event].unshift(block)
167
-
168
- self
169
- end # prepend_callback(event, &block)
170
-
171
-
172
- def exec_callback(name, *args, &block)
173
- callbacks = Array(self.callbacks[name])
174
- callbacks.map { |c| c.call(*args, &block) } if callbacks.any?
175
- end
176
-
177
- def exec_callback_once(name, *args, &block)
178
- callbacks = Array(self.callbacks.delete(name))
179
- callbacks.map { |c| c.call(*args, &block) } if callbacks.any?
180
- end
181
-
182
- def exec_callback_yielding_self(name, *args, &block)
183
- callbacks = Array(self.callbacks[name])
184
- callbacks.map { |c| c.call(self, *args, &block) } if callbacks.any?
185
- end
186
-
187
- def exec_callback_once_yielding_self(name, *args, &block)
188
- callbacks = Array(self.callbacks.delete(name))
189
- callbacks.map { |c| c.call(self, *args, &block) } if callbacks.any?
190
- end
191
-
192
-
193
-
194
144
  protected
195
145
 
196
146
  def post_init
@@ -1,12 +1,9 @@
1
1
  # encoding: utf-8
2
2
 
3
+ require "eventmachine"
3
4
  require "amq/client"
4
- require "amq/client/channel"
5
- require "amq/client/exchange"
6
5
  require "amq/client/framing/string/frame"
7
6
 
8
- require "eventmachine"
9
-
10
7
  module AMQ
11
8
  module Client
12
9
  class EventMachineClient < EM::Connection
@@ -23,27 +20,52 @@ module AMQ
23
20
 
24
21
  self.sync = false
25
22
 
26
- register_entity :channel, AMQ::Client::Channel
27
- register_entity :exchange, AMQ::Client::Exchange
28
-
29
23
  #
30
24
  # API
31
25
  #
32
26
 
33
27
  def self.connect(settings = nil, &block)
34
- settings = AMQ::Client::Settings.configure(settings)
28
+ @settings = settings = Settings.configure(settings)
29
+
35
30
  instance = EventMachine.connect(settings[:host], settings[:port], self, settings)
31
+ instance.register_connection_callback(&block)
32
+
33
+ instance
34
+ end
35
+
36
+
37
+ def reconnect(period = 5, force = false)
38
+ if @reconnecting and not force
39
+ EventMachine::Timer.new(period) {
40
+ reconnect(period, true)
41
+ }
42
+ return
43
+ end
44
+
45
+ if !@reconnecting
46
+ @reconnecting = true
47
+ @connections.each { |c| c.handle_connection_interruption }
48
+ self.reset
49
+ end
50
+
51
+ self.reconnect(@settings[:host], @settings[:port])
52
+ end
53
+
54
+
55
+ def establish_connection(settings)
56
+ # Unfortunately there doesn't seem to be any sane way
57
+ # how to get EventMachine connect to the instance level.
58
+ end
36
59
 
60
+ def register_connection_callback(&block)
37
61
  unless block.nil?
38
62
  # delay calling block we were given till after we receive
39
63
  # connection.open-ok. Connection will notify us when
40
64
  # that happens.
41
- instance.on_connection do
42
- block.call(instance)
65
+ self.on_connection do
66
+ block.call(self)
43
67
  end
44
68
  end
45
-
46
- instance
47
69
  end
48
70
 
49
71
 
@@ -53,24 +75,21 @@ module AMQ
53
75
  def initialize(*args)
54
76
  super(*args)
55
77
 
78
+ @connections = Array.new
79
+ # track TCP connection state, used to detect initial TCP connection failures.
80
+ @tcp_connection_established = false
81
+ @tcp_connection_failed = false
82
+ @intentionally_closing_connection = false
83
+
56
84
  # EventMachine::Connection's and Adapter's constructors arity
57
85
  # make it easier to use *args. MK.
58
- @settings = args.first
59
- @connections = Array.new
86
+ @settings = args.first
60
87
  @on_possible_authentication_failure = @settings[:on_possible_authentication_failure]
61
- @on_tcp_connection_failure = @settings[:on_tcp_connection_failure] || Proc.new { |settings| raise AMQ::Client::TCPConnectionFailed.new(settings) }
88
+ @on_tcp_connection_failure = @settings[:on_tcp_connection_failure] || Proc.new { |settings|
89
+ raise AMQ::Client::TCPConnectionFailed.new(settings)
90
+ }
62
91
 
63
- @chunk_buffer = ""
64
- @connection_deferrable = Deferrable.new
65
- @disconnection_deferrable = Deferrable.new
66
-
67
- @authenticating = false
68
-
69
- # succeeds when connection is open, that is, vhost is selected
70
- # and client is given green light to proceed.
71
- @connection_opened_deferrable = Deferrable.new
72
-
73
- @tcp_connection_established = false
92
+ self.reset
74
93
 
75
94
  if self.heartbeat_interval > 0
76
95
  @last_server_heartbeat = Time.now
@@ -79,10 +98,6 @@ module AMQ
79
98
  end # initialize(*args)
80
99
 
81
100
 
82
- def establish_connection(settings)
83
- # an intentional no-op
84
- end
85
-
86
101
  alias send_raw send_data
87
102
 
88
103
 
@@ -111,8 +126,6 @@ module AMQ
111
126
  # to take some time and to not be worth in as long as #post_init
112
127
  # works fine. MK.
113
128
  upgrade_to_tls_if_necessary
114
-
115
- self.handshake
116
129
  rescue Exception => error
117
130
  raise error
118
131
  end # post_init
@@ -121,8 +134,22 @@ module AMQ
121
134
  def connection_completed
122
135
  # we only can safely set this value here because EventMachine is a lovely piece of
123
136
  # software that calls #post_init before #unbind even when TCP connection
124
- # fails. Yes, it makes as much sense to me MK.
125
- @tcp_connection_established = true
137
+ # fails. MK.
138
+ @tcp_connection_established = true
139
+ # again, this is because #unbind is called in different situations
140
+ # and there is no easy way to tell initial connection failure
141
+ # from connection loss. Not in EventMachine 0.12.x, anyway. MK.
142
+ @had_successfull_connected_before = true
143
+
144
+ @reconnecting = false
145
+
146
+ self.handshake
147
+ end
148
+
149
+ def close_connection(*args)
150
+ @intentionally_closing_connection = true
151
+
152
+ super(*args)
126
153
  end
127
154
 
128
155
  # Called by EventMachine reactor when
@@ -132,19 +159,22 @@ module AMQ
132
159
  # * There is a network connection issue
133
160
  # * Initial TCP connection fails
134
161
  def unbind
135
- if !@tcp_connection_established
162
+ if !@tcp_connection_established && !@had_successfull_connected_before
163
+ @tcp_connection_failed = true
136
164
  self.tcp_connection_failed
137
165
  end
138
166
 
139
167
  closing!
140
-
141
168
  @tcp_connection_established = false
142
169
 
143
- @connections.each { |c| c.on_connection_interruption }
170
+ @connections.each { |c| c.handle_connection_interruption }
144
171
  @disconnection_deferrable.succeed
145
172
 
146
173
  closed!
147
174
 
175
+
176
+ self.tcp_connection_lost if !@intentionally_closing_connection && @had_successfull_connected_before
177
+
148
178
  # since AMQP spec dictates that authentication failure is a protocol exception
149
179
  # and protocol exceptions result in connection closure, check whether we are
150
180
  # in the authentication stage. If so, it is likely to signal an authentication
@@ -170,23 +200,35 @@ module AMQ
170
200
  end
171
201
  end
172
202
 
173
-
174
-
203
+ # Defines a callback that will be executed when AMQP connection is considered open,
204
+ # after client and broker has agreed on max channel identifier and maximum allowed frame
205
+ # size. You can define more than one callback.
206
+ #
207
+ # @see #on_open
208
+ # @api public
175
209
  def on_connection(&block)
176
210
  @connection_deferrable.callback(&block)
177
211
  end # on_connection(&block)
178
212
 
179
- # called by AMQ::Client::Connection after we receive connection.open-ok.
213
+ # Called by AMQ::Client::Connection after we receive connection.open-ok.
214
+ # @api public
180
215
  def connection_successful
181
216
  @connection_deferrable.succeed
182
217
  end # connection_successful
183
218
 
184
219
 
185
-
220
+ # Defines a callback that will be executed when AMQP connection is considered open,
221
+ # before client and broker has agreed on max channel identifier and maximum allowed frame
222
+ # size. You can define more than one callback.
223
+ #
224
+ # @see #on_connection
225
+ # @api public
186
226
  def on_open(&block)
187
227
  @connection_opened_deferrable.callback(&block)
188
228
  end # on_open(&block)
189
229
 
230
+ # Called by AMQ::Client::Connection after we receive connection.tune.
231
+ # @api public
190
232
  def open_successful
191
233
  @authenticating = false
192
234
  @connection_opened_deferrable.succeed
@@ -195,31 +237,61 @@ module AMQ
195
237
  end # open_successful
196
238
 
197
239
 
198
-
240
+ # Defines a callback that will be run when broker confirms connection termination
241
+ # (client receives connection.close-ok). You can define more than one callback.
242
+ #
243
+ # @api public
199
244
  def on_disconnection(&block)
200
245
  @disconnection_deferrable.callback(&block)
201
246
  end # on_disconnection(&block)
202
247
 
203
- # called by AMQ::Client::Connection after we receive connection.close-ok.
248
+ # Called by AMQ::Client::Connection after we receive connection.close-ok.
249
+ #
250
+ # @api public
204
251
  def disconnection_successful
205
252
  @disconnection_deferrable.succeed
206
253
 
207
254
  self.close_connection
255
+ self.reset
208
256
  closed!
209
257
  end # disconnection_successful
210
258
 
211
259
 
212
-
260
+ # Defines a callback that will be run when initial TCP connection fails.
261
+ # You can define only one callback.
262
+ #
263
+ # @api public
213
264
  def on_tcp_connection_failure(&block)
214
265
  @on_tcp_connection_failure = block
215
266
  end
216
267
 
268
+ # Called when initial TCP connection fails.
269
+ # @api public
217
270
  def tcp_connection_failed
218
271
  @on_tcp_connection_failure.call(@settings) if @on_tcp_connection_failure
219
272
  end
220
273
 
221
274
 
275
+ # Defines a callback that will be run when initial TCP connection fails.
276
+ # You can define only one callback.
277
+ #
278
+ # @api public
279
+ def on_tcp_connection_loss(&block)
280
+ @on_tcp_connection_loss = block
281
+ end
282
+
283
+ # Called when initial TCP connection fails.
284
+ # @api public
285
+ def tcp_connection_lost
286
+ @on_tcp_connection_loss.call(self, @settings) if @on_tcp_connection_loss
287
+ end
288
+
289
+
222
290
 
291
+ # Defines a callback that will be run when TCP connection is closed before authentication
292
+ # finishes. Usually this means authentication failure. You can define only one callback.
293
+ #
294
+ # @api public
223
295
  def on_possible_authentication_failure(&block)
224
296
  @on_possible_authentication_failure = block
225
297
  end
@@ -243,6 +315,19 @@ module AMQ
243
315
  @size = 0
244
316
  @payload = ""
245
317
  @frames = Array.new
318
+
319
+ @chunk_buffer = ""
320
+ @connection_deferrable = Deferrable.new
321
+ @disconnection_deferrable = Deferrable.new
322
+ # succeeds when connection is open, that is, vhost is selected
323
+ # and client is given green light to proceed.
324
+ @connection_opened_deferrable = Deferrable.new
325
+
326
+ # used to track down whether authentication succeeded. AMQP 0.9.1 dictates
327
+ # that on authentication failure broker must close TCP connection without sending
328
+ # any more data. This is why we need to explicitly track whether we are past
329
+ # authentication stage to signal possible authentication failures.
330
+ @authenticating = false
246
331
  end
247
332
 
248
333
  # @see http://tools.ietf.org/rfc/rfc2595.txt RFC 2595
@@ -272,7 +357,7 @@ module AMQ
272
357
  start_tls(tls_options)
273
358
  elsif tls_options
274
359
  start_tls
275
- end
360
+ end
276
361
  end
277
362
  end # EventMachineClient
278
363
  end # Client