amq-client 0.7.0.alpha3 → 0.7.0.alpha4

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