fl-thrift_client 0.4.2

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