fl-thrift_client 0.4.2

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,145 @@
1
+ raise RuntimeError, "The eventmachine transport requires Ruby 1.9.x" if RUBY_VERSION < '1.9.0'
2
+
3
+ require 'eventmachine'
4
+ require 'fiber'
5
+
6
+ # EventMachine-ready Thrift connection
7
+ # Should not be used with a transport wrapper since it already performs buffering in Ruby.
8
+ module Thrift
9
+ class EventMachineTransport < BaseTransport
10
+ def initialize(host, port=9090, timeout=5)
11
+ @host, @port, @timeout = host, port, timeout
12
+ @connection = nil
13
+ end
14
+
15
+ def open?
16
+ @connection && @connection.connected?
17
+ end
18
+
19
+ def open
20
+ fiber = Fiber.current
21
+ @connection = EventMachineConnection.connect(@host, @port, @timeout)
22
+ @connection.callback do
23
+ fiber.resume
24
+ end
25
+ @connection.errback do
26
+ fiber.resume
27
+ end
28
+ Fiber.yield
29
+ @connection
30
+ end
31
+
32
+ def close
33
+ @connection.close
34
+ end
35
+
36
+ def read(sz)
37
+ @connection.blocking_read(sz)
38
+ end
39
+
40
+ def write(buf)
41
+ @connection.send_data(buf)
42
+ end
43
+ end
44
+
45
+ module EventMachineConnection
46
+ GARBAGE_BUFFER_SIZE = 4096 # 4kB
47
+
48
+ include EM::Deferrable
49
+
50
+ def self.connect(host='localhost', port=9090, timeout=5, &block)
51
+ EM.connect(host, port, self, host, port) do |conn|
52
+ conn.pending_connect_timeout = timeout
53
+ end
54
+ end
55
+
56
+ def trap
57
+ begin
58
+ yield
59
+ rescue Exception => ex
60
+ puts ex.message
61
+ puts ex.backtrace.join("\n")
62
+ end
63
+ end
64
+
65
+ def initialize(host, port=9090)
66
+ @host, @port = host, port
67
+ @index = 0
68
+ @connected = false
69
+ @buf = ''
70
+ end
71
+
72
+ def close
73
+ trap do
74
+ @connected = false
75
+ close_connection(true)
76
+ end
77
+ end
78
+
79
+ def blocking_read(size)
80
+ raise IOError, "lost connection to #{@host}:#{@port}" unless @connected
81
+ trap do
82
+ if can_read?(size)
83
+ yank(size)
84
+ else
85
+ raise ArgumentError, "Unexpected state" if @size or @callback
86
+
87
+ fiber = Fiber.current
88
+ @size = size
89
+ @callback = proc { |data|
90
+ fiber.resume(data)
91
+ }
92
+ Fiber.yield
93
+ end
94
+ end
95
+ end
96
+
97
+ def receive_data(data)
98
+ trap do
99
+ (@buf) << data
100
+
101
+ if @callback and can_read?(@size)
102
+ callback = @callback
103
+ data = yank(@size)
104
+ @callback = @size = nil
105
+ callback.call(data)
106
+ end
107
+ end
108
+ end
109
+
110
+ def connected?
111
+ @connected
112
+ end
113
+
114
+ def connection_completed
115
+ @connected = true
116
+ succeed
117
+ end
118
+
119
+ def unbind
120
+ if @connected
121
+ @connected = false
122
+ else
123
+ fail
124
+ end
125
+ end
126
+
127
+ def can_read?(size)
128
+ @buf.size >= @index + size
129
+ end
130
+
131
+ private
132
+
133
+ def yank(len)
134
+ data = @buf.slice(@index, len)
135
+ @index += len
136
+ @index = @buf.size if @index > @buf.size
137
+ if @index >= GARBAGE_BUFFER_SIZE
138
+ @buf = @buf.slice(@index..-1)
139
+ @index = 0
140
+ end
141
+ data
142
+ end
143
+
144
+ end
145
+ end
@@ -0,0 +1,263 @@
1
+ require 'socket'
2
+ require 'getoptlong'
3
+
4
+ class ThriftClient
5
+
6
+ # This is a simplified form of thrift, useful for clients only, and not
7
+ # making any attempt to have good performance. It's intended to be used by
8
+ # small command-line tools that don't want to install a dozen ruby files.
9
+ module Simple
10
+ VERSION_1 = 0x8001
11
+
12
+ # message types
13
+ CALL, REPLY, EXCEPTION = (1..3).to_a
14
+
15
+ # value types
16
+ STOP, VOID, BOOL, BYTE, DOUBLE, _, I16, _, I32, _, I64, STRING, STRUCT, MAP, SET, LIST = (0..15).to_a
17
+
18
+ FORMATS = {
19
+ BYTE => "c",
20
+ DOUBLE => "G",
21
+ I16 => "n",
22
+ I32 => "N",
23
+ }
24
+
25
+ SIZES = {
26
+ BYTE => 1,
27
+ DOUBLE => 8,
28
+ I16 => 2,
29
+ I32 => 4,
30
+ }
31
+
32
+ module ComplexType
33
+ module Extends
34
+ def type_id=(n)
35
+ @type_id = n
36
+ end
37
+
38
+ def type_id
39
+ @type_id
40
+ end
41
+ end
42
+
43
+ module Includes
44
+ def to_i
45
+ self.class.type_id
46
+ end
47
+
48
+ def to_s
49
+ args = self.values.map { |v| self.class.type_id == STRUCT ? v.name : v.to_s }.join(", ")
50
+ "#{self.class.name}.new(#{args})"
51
+ end
52
+ end
53
+ end
54
+
55
+ def self.make_type(type_id, name, *args)
56
+ klass = Struct.new("STT_#{name}", *args)
57
+ klass.send(:extend, ComplexType::Extends)
58
+ klass.send(:include, ComplexType::Includes)
59
+ klass.type_id = type_id
60
+ klass
61
+ end
62
+
63
+ ListType = make_type(LIST, "ListType", :element_type)
64
+ MapType = make_type(MAP, "MapType", :key_type, :value_type)
65
+ SetType = make_type(SET, "SetType", :element_type)
66
+ StructType = make_type(STRUCT, "StructType", :struct_class)
67
+
68
+ class << self
69
+ def pack_value(type, value)
70
+ case type
71
+ when BOOL
72
+ [ value ? 1 : 0 ].pack("c")
73
+ when STRING
74
+ [ value.size, value ].pack("Na*")
75
+ when I64
76
+ [ value >> 32, value & 0xffffffff ].pack("NN")
77
+ when ListType
78
+ [ type.element_type.to_i, value.size ].pack("cN") + value.map { |item| pack_value(type.element_type, item) }.join("")
79
+ when MapType
80
+ [ type.key_type.to_i, type.value_type.to_i, value.size ].pack("ccN") + value.map { |k, v| pack_value(type.key_type, k) + pack_value(type.value_type, v) }.join("")
81
+ when SetType
82
+ [ type.element_type.to_i, value.size ].pack("cN") + value.map { |item| pack_value(type.element_type, item) }.join("")
83
+ when StructType
84
+ value._pack
85
+ else
86
+ [ value ].pack(FORMATS[type])
87
+ end
88
+ end
89
+
90
+ def pack_request(method_name, arg_struct, request_id=0)
91
+ [ VERSION_1, CALL, method_name.to_s.size, method_name.to_s, request_id, arg_struct._pack ].pack("nnNa*Na*")
92
+ end
93
+
94
+ def read_value(s, type)
95
+ case type
96
+ when BOOL
97
+ s.read(1).unpack("c").first != 0
98
+ when STRING
99
+ len = s.read(4).unpack("N").first
100
+ s.read(len)
101
+ when I64
102
+ hi, lo = s.read(8).unpack("NN")
103
+ (hi << 32) | lo
104
+ when LIST
105
+ read_list(s)
106
+ when MAP
107
+ read_map(s)
108
+ when STRUCT
109
+ read_struct(s)
110
+ when ListType
111
+ read_list(s, type.element_type)
112
+ when MapType
113
+ read_map(s, type.key_type, type.value_type)
114
+ when StructType
115
+ read_struct(s, type.struct_class)
116
+ else
117
+ s.read(SIZES[type]).unpack(FORMATS[type]).first
118
+ end
119
+ end
120
+
121
+ def read_list(s, element_type=nil)
122
+ etype, len = s.read(5).unpack("cN")
123
+ expected_type = (element_type and element_type.to_i == etype.to_i) ? element_type : etype
124
+ rv = []
125
+ len.times do
126
+ rv << read_value(s, expected_type)
127
+ end
128
+ rv
129
+ end
130
+
131
+ def read_map(s, key_type=nil, value_type=nil)
132
+ ktype, vtype, len = s.read(6).unpack("ccN")
133
+ rv = {}
134
+ expected_key_type, expected_value_type = if key_type and value_type and key_type.to_i == ktype and value_type.to_i == vtype
135
+ [ key_type, value_type ]
136
+ else
137
+ [ ktype, vtype ]
138
+ end
139
+ len.times do
140
+ key = read_value(s, expected_key_type)
141
+ value = read_value(s, expected_value_type)
142
+ rv[key] = value
143
+ end
144
+ rv
145
+ end
146
+
147
+ def read_struct(s, struct_class=nil)
148
+ rv = struct_class.new()
149
+ while true
150
+ type = s.read(1).unpack("c").first
151
+ return rv if type == STOP
152
+ fid = s.read(2).unpack("n").first
153
+ field = struct_class ? struct_class._fields.find { |f| (f.fid == fid) and (f.type.to_i == type) } : nil
154
+ value = read_value(s, field ? field.type : type)
155
+ rv[field.name] = value if field
156
+ end
157
+ end
158
+
159
+ def read_response(s, rv_class)
160
+ version, message_type, method_name_len = s.read(8).unpack("nnN")
161
+ method_name = s.read(method_name_len)
162
+ seq_id = s.read(4).unpack("N").first
163
+ [ method_name, seq_id, read_struct(s, rv_class).rv ]
164
+ end
165
+ end
166
+
167
+ ## ----------------------------------------
168
+
169
+ class Field
170
+ attr_accessor :name, :type, :fid
171
+
172
+ def initialize(name, type, fid)
173
+ @name = name
174
+ @type = type
175
+ @fid = fid
176
+ end
177
+
178
+ def pack(value)
179
+ value.nil? ? "" : [ type.to_i, fid, ThriftClient::Simple.pack_value(type, value) ].pack("cna*")
180
+ end
181
+ end
182
+
183
+ class ThriftException < RuntimeError
184
+ def initialize(reason)
185
+ @reason = reason
186
+ end
187
+
188
+ def to_s
189
+ "ThriftException(#{@reason.inspect})"
190
+ end
191
+ end
192
+
193
+ module ThriftStruct
194
+ module Include
195
+ def _pack
196
+ self.class._fields.map { |f| f.pack(self[f.name]) }.join + [ STOP ].pack("c")
197
+ end
198
+ end
199
+
200
+ module Extend
201
+ def _fields
202
+ @fields
203
+ end
204
+
205
+ def _fields=(f)
206
+ @fields = f
207
+ end
208
+ end
209
+ end
210
+
211
+ def self.make_struct(name, *fields)
212
+ st_name = "ST_#{name}"
213
+ if Struct.constants.include?(st_name)
214
+ warn "#{caller[0]}: Struct::#{st_name} is already defined; returning original class."
215
+ Struct.const_get(st_name)
216
+ else
217
+ names = fields.map { |f| f.name.to_sym }
218
+ klass = Struct.new(st_name, *names)
219
+ klass.send(:include, ThriftStruct::Include)
220
+ klass.send(:extend, ThriftStruct::Extend)
221
+ klass._fields = fields
222
+ klass
223
+ end
224
+ end
225
+
226
+ class ThriftService
227
+ def initialize(sock)
228
+ @sock = sock
229
+ end
230
+
231
+ def self._arg_structs
232
+ @_arg_structs = {} if @_arg_structs.nil?
233
+ @_arg_structs
234
+ end
235
+
236
+ def self.thrift_method(name, rtype, *args)
237
+ arg_struct = ThriftClient::Simple.make_struct("Args__#{self.name}__#{name}", *args)
238
+ rv_struct = ThriftClient::Simple.make_struct("Retval__#{self.name}__#{name}", ThriftClient::Simple::Field.new(:rv, rtype, 0))
239
+ _arg_structs[name.to_sym] = [ arg_struct, rv_struct ]
240
+
241
+ arg_names = args.map { |a| a.name.to_s }.join(", ")
242
+ class_eval "def #{name}(#{arg_names}); _proxy(:#{name}#{args.size > 0 ? ', ' : ''}#{arg_names}); end"
243
+ end
244
+
245
+ def _proxy(method_name, *args)
246
+ cls = self.class.ancestors.find { |cls| cls.respond_to?(:_arg_structs) and cls._arg_structs[method_name.to_sym] }
247
+ arg_class, rv_class = cls._arg_structs[method_name.to_sym]
248
+ arg_struct = arg_class.new(*args)
249
+ @sock.write(ThriftClient::Simple.pack_request(method_name, arg_struct))
250
+ rv = ThriftClient::Simple.read_response(@sock, rv_class)
251
+ rv[2]
252
+ end
253
+
254
+ # convenience. robey is lazy.
255
+ [[ :field, "Field.new" ], [ :struct, "StructType.new" ],
256
+ [ :list, "ListType.new" ], [ :map, "MapType.new" ]].each do |new_name, old_name|
257
+ class_eval "def self.#{new_name}(*args); ThriftClient::Simple::#{old_name}(*args); end"
258
+ end
259
+
260
+ [ :void, :bool, :byte, :double, :i16, :i32, :i64, :string ].each { |sym| class_eval "def self.#{sym}; ThriftClient::Simple::#{sym.to_s.upcase}; end" }
261
+ end
262
+ end
263
+ end
@@ -0,0 +1,21 @@
1
+ module Thrift
2
+ class BufferedTransport
3
+ def timeout=(timeout)
4
+ @transport.timeout = timeout
5
+ end
6
+
7
+ def timeout
8
+ @transport.timeout
9
+ end
10
+ end
11
+
12
+ module Client
13
+ def timeout=(timeout)
14
+ @iprot.trans.timeout = timeout
15
+ end
16
+
17
+ def timeout
18
+ @iprot.trans.timeout
19
+ end
20
+ end
21
+ end
@@ -0,0 +1,45 @@
1
+ if ENV["ANCIENT_THRIFT"]
2
+ $LOAD_PATH.unshift("/Users/eweaver/p/twitter/rails/vendor/gems/thrift-751142/lib")
3
+ $LOAD_PATH.unshift("/Users/eweaver/p/twitter/rails/vendor/gems/thrift-751142/ext")
4
+ require 'thrift'
5
+ else
6
+ require 'rubygems'
7
+ require 'thrift'
8
+ end
9
+
10
+ require 'rubygems'
11
+ require 'thrift_client/thrift'
12
+ require 'thrift_client/connection'
13
+ require 'thrift_client/abstract_thrift_client'
14
+
15
+ class ThriftClient < AbstractThriftClient
16
+ # This error is for backwards compatibility only. If defined in
17
+ # RetryingThriftClient instead, causes the test suite will break.
18
+ class NoServersAvailable < StandardError; end
19
+ include RetryingThriftClient
20
+ include TimingOutThriftClient
21
+
22
+ =begin rdoc
23
+ Create a new ThriftClient instance. Accepts an internal Thrift client class (such as CassandraRb::Client), a list of servers with ports, and optional parameters.
24
+
25
+ Valid optional parameters are:
26
+
27
+ <tt>:protocol</tt>:: Which Thrift protocol to use. Defaults to <tt>Thrift::BinaryProtocol</tt>.
28
+ <tt>:protocol_extra_params</tt>:: An array of additional parameters to pass to the protocol initialization call. Defaults to <tt>[]</tt>.
29
+ <tt>:transport</tt>:: Which Thrift transport to use. Defaults to <tt>Thrift::FramedTransport</tt>.
30
+ <tt>:randomize_server_list</tt>:: Whether to connect to the servers randomly, instead of in order. Defaults to <tt>true</tt>.
31
+ <tt>:exception_classes</tt>:: Which exceptions to catch and retry when sending a request. Defaults to <tt>[IOError, Thrift::Exception, Thrift::ProtocolException, Thrift::ApplicationException, Thrift::TransportException, NoServersAvailable]</tt>
32
+ <tt>:raise</tt>:: Whether to reraise errors if no responsive servers are found. Defaults to <tt>true</tt>.
33
+ <tt>:retries</tt>:: How many times to retry a request. Defaults to the number of servers defined.
34
+ <tt>:server_retry_period</tt>:: How many seconds to wait before trying to reconnect after marking all servers as down. Defaults to <tt>1</tt>. Set to <tt>nil</tt> to retry endlessly.
35
+ <tt>:server_max_requests</tt>:: How many requests to perform before moving on to the next server in the pool, regardless of error status. Defaults to <tt>nil</tt> (no limit).
36
+ <tt>:timeout</tt>:: Specify the default timeout in seconds. Defaults to <tt>1</tt>.
37
+ <tt>:timeout_overrides</tt>:: Specify additional timeouts on a per-method basis, in seconds. Only works with <tt>Thrift::BufferedTransport</tt>.
38
+ <tt>:defaults</tt>:: Specify default values to return on a per-method basis, if <tt>:raise</tt> is set to false.
39
+
40
+ =end rdoc
41
+
42
+ def initialize(client_class, servers, options = {})
43
+ super
44
+ end
45
+ end
@@ -0,0 +1,77 @@
1
+ #
2
+ # Autogenerated by Thrift
3
+ #
4
+ # DO NOT EDIT UNLESS YOU ARE SURE THAT YOU KNOW WHAT YOU ARE DOING
5
+ #
6
+
7
+ require 'thrift'
8
+
9
+ module Greeter
10
+ class Client
11
+ include ::Thrift::Client
12
+
13
+ def greeting(name)
14
+ send_greeting(name)
15
+ return recv_greeting()
16
+ end
17
+
18
+ def send_greeting(name)
19
+ send_message('greeting', Greeting_args, :name => name)
20
+ end
21
+
22
+ def recv_greeting()
23
+ result = receive_message(Greeting_result)
24
+ return result.success unless result.success.nil?
25
+ raise ::Thrift::ApplicationException.new(::Thrift::ApplicationException::MISSING_RESULT, 'greeting failed: unknown result')
26
+ end
27
+
28
+ end
29
+
30
+ class Processor
31
+ include ::Thrift::Processor
32
+
33
+ def process_greeting(seqid, iprot, oprot)
34
+ args = read_args(iprot, Greeting_args)
35
+ result = Greeting_result.new()
36
+ result.success = @handler.greeting(args.name)
37
+ write_result(result, oprot, 'greeting', seqid)
38
+ end
39
+
40
+ end
41
+
42
+ # HELPER FUNCTIONS AND STRUCTURES
43
+
44
+ class Greeting_args
45
+ include ::Thrift::Struct
46
+ NAME = 1
47
+
48
+ ::Thrift::Struct.field_accessor self, :name
49
+ FIELDS = {
50
+ NAME => {:type => ::Thrift::Types::STRING, :name => 'name'}
51
+ }
52
+
53
+ def struct_fields; FIELDS; end
54
+
55
+ def validate
56
+ end
57
+
58
+ end
59
+
60
+ class Greeting_result
61
+ include ::Thrift::Struct
62
+ SUCCESS = 0
63
+
64
+ ::Thrift::Struct.field_accessor self, :success
65
+ FIELDS = {
66
+ SUCCESS => {:type => ::Thrift::Types::STRING, :name => 'success'}
67
+ }
68
+
69
+ def struct_fields; FIELDS; end
70
+
71
+ def validate
72
+ end
73
+
74
+ end
75
+
76
+ end
77
+
@@ -0,0 +1,3 @@
1
+ service Greeter {
2
+ string greeting(1:string name)
3
+ }
@@ -0,0 +1,40 @@
1
+ module Greeter
2
+ class Handler
3
+ def greeting(name)
4
+ "hello there #{name}!"
5
+ end
6
+ end
7
+
8
+ class Server
9
+ def initialize(port)
10
+ @port = port
11
+ handler = Greeter::Handler.new
12
+ processor = Greeter::Processor.new(handler)
13
+ transport = Thrift::ServerSocket.new("127.0.0.1", port)
14
+ transportFactory = Thrift::FramedTransportFactory.new()
15
+ @server = Thrift::SimpleServer.new(processor, transport, transportFactory)
16
+ end
17
+
18
+ def serve
19
+ @server.serve()
20
+ end
21
+ end
22
+
23
+ # client:
24
+ # trans = Thrift::HTTPClientTransport.new("http://127.0.0.1:9292/greeter")
25
+ # prot = Thrift::BinaryProtocol.new(trans)
26
+ # c = Greeter::Client.new(prot)
27
+ class HTTPServer
28
+ def initialize(uri)
29
+ uri = URI.parse(uri)
30
+ handler = Greeter::Handler.new
31
+ processor = Greeter::Processor.new(handler)
32
+ path = uri.path[1..-1]
33
+ @server = Thrift::MongrelHTTPServer.new(processor, :port => uri.port, :ip => uri.host, :path => path)
34
+ end
35
+
36
+ def serve
37
+ @server.serve()
38
+ end
39
+ end
40
+ end
@@ -0,0 +1,112 @@
1
+ require "#{File.dirname(__FILE__)}/test_helper"
2
+
3
+ class MultipleWorkingServersTest < Test::Unit::TestCase
4
+ def setup
5
+ @servers = ["127.0.0.1:1461", "127.0.0.1:1462", "127.0.0.1:1463"]
6
+ @socket = 1461
7
+ @timeout = 0.2
8
+ @options = {:protocol_extra_params => [false]}
9
+ @pids = []
10
+ @servers.each do |s|
11
+ @pids << Process.fork do
12
+ Signal.trap("INT") { exit }
13
+ Greeter::Server.new(s.split(':').last).serve
14
+ end
15
+ end
16
+ # Need to give the child process a moment to open the listening socket or
17
+ # we get occasional "could not connect" errors in tests.
18
+ sleep 0.05
19
+ end
20
+
21
+ def teardown
22
+ @pids.each do |pid|
23
+ Process.kill("INT", pid)
24
+ Process.wait(pid)
25
+ end
26
+ end
27
+
28
+ def test_server_creates_new_client_that_can_talk_to_all_servers_after_disconnect
29
+ client = ThriftClient.new(Greeter::Client, @servers, @options)
30
+ client.greeting("someone")
31
+ internal_client = client.client
32
+ client.greeting("someone")
33
+ assert_equal internal_client, client.client # Sanity check
34
+
35
+ client.disconnect!
36
+ client.greeting("someone")
37
+ internal_client = client.client
38
+ client.greeting("someone")
39
+ assert_equal internal_client, client.client
40
+ internal_client = client.client
41
+ client.greeting("someone")
42
+ assert_equal internal_client, client.client
43
+
44
+ # Moves on to the second server
45
+ assert_nothing_raised {
46
+ client.greeting("someone")
47
+ client.greeting("someone")
48
+ }
49
+ end
50
+
51
+ def test_server_doesnt_max_out_after_explicit_disconnect
52
+ client = ThriftClient.new(Greeter::Client, @servers, @options.merge(:server_max_requests => 2))
53
+ client.greeting("someone")
54
+ internal_client = client.client
55
+ client.greeting("someone")
56
+ assert_equal internal_client, client.client # Sanity check
57
+
58
+ client.disconnect!
59
+
60
+ client.greeting("someone")
61
+ internal_client = client.client
62
+ client.greeting("someone")
63
+ assert_equal internal_client, client.client, "ThriftClient should not have reset the internal client if the counter was reset on disconnect"
64
+ end
65
+
66
+ def test_server_disconnect_doesnt_drop_servers_with_retry_period
67
+ client = ThriftClient.new(Greeter::Client, @servers, @options.merge(:server_max_requests => 2, :retry_period => 1))
68
+ 3.times {
69
+ client.greeting("someone")
70
+ internal_client = client.client
71
+ client.greeting("someone")
72
+ assert_equal internal_client, client.client # Sanity check
73
+
74
+ client.disconnect!
75
+
76
+ client.greeting("someone")
77
+ internal_client = client.client
78
+ client.greeting("someone")
79
+ assert_equal internal_client, client.client, "ThriftClient should not have reset the internal client if the counter was reset on disconnect"
80
+ }
81
+ end
82
+
83
+
84
+ def test_server_max_requests
85
+ client = ThriftClient.new(Greeter::Client, @servers, @options.merge(:server_max_requests => 2))
86
+
87
+ client.greeting("someone")
88
+ internal_client = client.client
89
+
90
+ client.greeting("someone")
91
+ assert_equal internal_client, client.client
92
+
93
+ # This next call maxes out the requests for that "client" object
94
+ # and moves on to the next.
95
+ client.greeting("someone")
96
+ assert_not_equal internal_client, new_client = client.client
97
+
98
+ # And here we should still have the same client as the last one...
99
+ client.greeting("someone")
100
+ assert_equal new_client, client.client
101
+
102
+ # Until we max it out, too.
103
+ client.greeting("someone")
104
+ assert_not_equal new_client, client.client
105
+ assert_not_nil client.client
106
+
107
+ new_new_client = client.client
108
+ # And we should still have one server left
109
+ client.greeting("someone")
110
+ assert_equal new_new_client, client.client
111
+ end
112
+ end