RaymondChou-thrift_client 0.9.3

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA1:
3
+ metadata.gz: 9e16ff9dcf19cf0dff6b0fc0c760fdfaeee7560b
4
+ data.tar.gz: b79f3d1cae0c4fb18727988b6e42f82fdf39e260
5
+ SHA512:
6
+ metadata.gz: 5a2895690e6da4f99c1f18670fcda05b55c243a357848a99cb2d629bfeb8c5352332ecdbea2a9933224ef4e6d6d468f7cb08a530c842d8e3f63d73346ce6a084
7
+ data.tar.gz: 82e6c2a65ab738c07b0e2c2e9982fb9852ba8c872460a7446c634113f627f932d4346d59e3beeb505f3d6570bc2189bbee1dd6383e126b36ac57ce405b5d12ef
@@ -0,0 +1,35 @@
1
+ require 'thrift'
2
+ require 'thrift_client/thrift'
3
+ require 'thrift_client/server'
4
+ require 'thrift_client/abstract_thrift_client'
5
+
6
+ class ThriftClient < AbstractThriftClient
7
+ class NoServersAvailable < StandardError; end
8
+
9
+ =begin rdoc
10
+ Create a new ThriftClient instance. Accepts an internal Thrift client class (such as CassandraRb::Client), a list of servers with ports, and optional parameters.
11
+
12
+ Valid optional parameters are:
13
+
14
+ <tt>:protocol</tt>:: Which Thrift protocol to use. Defaults to <tt>Thrift::BinaryProtocol</tt>.
15
+ <tt>:protocol_extra_params</tt>:: An array of additional parameters to pass to the protocol initialization call. Defaults to <tt>[]</tt>.
16
+ <tt>:transport</tt>:: Which Thrift transport to use. Defaults to <tt>Thrift::Socket</tt>.
17
+ <tt>:transport_wrapper</tt>:: Which Thrift transport wrapper to use. Defaults to <tt>Thrift::FramedTransport</tt>.
18
+ <tt>:exception_classes</tt>:: Which exceptions to catch and retry when sending a request. Defaults to <tt>[IOError, Thrift::Exception, Thrift::ApplicationException, Thrift::TransportException, NoServersAvailable]</tt>
19
+ <tt>:exception_class_overrides</tt>:: For specifying children of classes in exception_classes for which you don't want to retry or reconnect.
20
+ <tt>:raise</tt>:: Whether to reraise errors if no responsive servers are found. Defaults to <tt>true</tt>.
21
+ <tt>:retries</tt>:: How many times to retry a request. Defaults to 0.
22
+ <tt>:server_retry_period</tt>:: How many seconds to wait before trying to reconnect to a dead server. Defaults to <tt>1</tt>. Set to <tt>nil</tt> to disable.
23
+ <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).
24
+ <tt>:timeout</tt>:: Specify the default timeout in seconds. Defaults to <tt>1</tt>.
25
+ <tt>:connect_timeout</tt>:: Specify the connection timeout in seconds. Defaults to <tt>0.1</tt>.
26
+ <tt>:timeout_overrides</tt>:: Specify additional timeouts on a per-method basis, in seconds. Only works with <tt>Thrift::BufferedTransport</tt>.
27
+ <tt>:cached_connections</tt>:: Cache connections between requests. Trades connect() costs for open sockets. Defaults to <tt>false</tt>.
28
+ <tt>:defaults</tt>:: Specify default values to return on a per-method basis, if <tt>:raise</tt> is set to false.
29
+ <tt>:multiplexed_protocol</tt>:: Thrift::MultiplexedProtocol.new(protocol, 'WechatService')
30
+
31
+ =end rdoc
32
+ def initialize(client_class, servers, options = {})
33
+ super
34
+ end
35
+ end
@@ -0,0 +1,226 @@
1
+ class AbstractThriftClient
2
+ include ThriftHelpers
3
+
4
+ DISCONNECT_ERRORS = [
5
+ IOError,
6
+ Thrift::Exception,
7
+ Thrift::ApplicationException,
8
+ Thrift::TransportException
9
+ ]
10
+
11
+ DEFAULT_WRAPPED_ERRORS = [
12
+ Thrift::ApplicationException,
13
+ Thrift::TransportException,
14
+ ]
15
+
16
+ DEFAULTS = {
17
+ :protocol => Thrift::BinaryProtocol,
18
+ :protocol_extra_params => [],
19
+ :transport => Thrift::Socket,
20
+ :transport_wrapper => Thrift::FramedTransport,
21
+ :raise => true,
22
+ :defaults => {},
23
+ :exception_classes => DISCONNECT_ERRORS,
24
+ :exception_class_overrides => [],
25
+ :retries => 0,
26
+ :server_retry_period => 1,
27
+ :server_max_requests => nil,
28
+ :retry_overrides => {},
29
+ :wrapped_exception_classes => DEFAULT_WRAPPED_ERRORS,
30
+ :connect_timeout => 0.1,
31
+ :timeout => 1,
32
+ :timeout_overrides => {},
33
+ :cached_connections => false
34
+ }
35
+
36
+ attr_reader :last_client, :client, :client_class, :current_server, :server_list, :options, :client_methods
37
+
38
+ def self.create_wrapped_exception_classes(client_class, wrapped_exception_classes = DEFAULT_WRAPPED_ERRORS)
39
+ wrapped_exception_classes.map do |exception_klass|
40
+ name = exception_klass.to_s.split('::').last
41
+ begin
42
+ client_class.const_get(name)
43
+ rescue NameError
44
+ client_class.const_set(name, Class.new(exception_klass))
45
+ end
46
+ end
47
+ end
48
+
49
+ def initialize(client_class, servers, options = {})
50
+ @options = DEFAULTS.merge(options)
51
+ @options[:server_retry_period] ||= 0
52
+
53
+ @client_class = client_class
54
+ @server_list = Array(servers).collect do |s|
55
+ Server.new(s, @client_class, @options)
56
+ end.sort_by { rand }
57
+
58
+ @current_server = @server_list.first
59
+
60
+ @callbacks = {}
61
+ @client_methods = []
62
+ @client_class.instance_methods.each do |method_name|
63
+ if method_name != 'send_message' && method_name =~ /^send_(.*)$/
64
+ instance_eval("def #{$1}(*args); handled_proxy(:'#{$1}', *args); end", __FILE__, __LINE__)
65
+ @client_methods << $1
66
+ end
67
+ end
68
+ @request_count = 0
69
+ self.class.create_wrapped_exception_classes(@client_class, @options[:wrapped_exception_classes])
70
+ end
71
+
72
+ # Adds a callback that will be invoked at a certain time. The valid callback types are:
73
+ # :post_connect - should accept a single AbstractThriftClient argument, which is the client object to
74
+ # which the callback was added. Called after a connection to the remote thrift server
75
+ # is established.
76
+ # :before_method - should accept a single method name argument. Called before a method is invoked on the
77
+ # thrift server.
78
+ # :on_exception - should accept 2 args: an Exception instance and a method name. Called right before the
79
+ # exception is raised.
80
+ def add_callback(callback_type, &block)
81
+ case callback_type
82
+ when :post_connect, :before_method, :on_exception
83
+ @callbacks[callback_type] ||= []
84
+ @callbacks[callback_type].push(block)
85
+ # Allow chaining
86
+ return self
87
+ else
88
+ return nil
89
+ end
90
+ end
91
+
92
+ def inspect
93
+ "<#{self.class}(#{client_class}) @current_server=#{@current_server}>"
94
+ end
95
+
96
+ # Force the client to connect to the server. Not necessary to be
97
+ # called as the connection will be made on the first RPC method
98
+ # call.
99
+ def connect!(method = nil)
100
+ start_time ||= Time.now
101
+ @current_server = next_live_server
102
+ @client = @current_server.client
103
+ @last_client = @client
104
+ do_callbacks(:post_connect, self)
105
+ rescue IOError, Thrift::TransportException
106
+ disconnect!(true)
107
+ timeout = timeout(method)
108
+ if timeout && Time.now - start_time > timeout
109
+ no_servers_available!
110
+ else
111
+ retry
112
+ end
113
+ end
114
+
115
+ def disconnect!(error = false)
116
+ if @current_server
117
+ @current_server.mark_down!(@options[:server_retry_period]) if error
118
+ @current_server.close
119
+ end
120
+
121
+ @client = nil
122
+ @current_server = nil
123
+ @request_count = 0
124
+ end
125
+
126
+ private
127
+
128
+ # Calls all callbacks of the specified type with the given args
129
+ def do_callbacks(callback_type_sym, *args)
130
+ return unless @callbacks[callback_type_sym]
131
+ @callbacks[callback_type_sym].each do |callback|
132
+ callback.call(*args)
133
+ end
134
+ end
135
+
136
+ def next_live_server
137
+ @server_index ||= 0
138
+ @server_list.length.times do |i|
139
+ cur = (1 + @server_index + i) % @server_list.length
140
+ if @server_list[cur].up?
141
+ @server_index = cur
142
+ return @server_list[cur]
143
+ end
144
+ end
145
+ no_servers_available!
146
+ end
147
+
148
+ def ensure_socket_alignment
149
+ incomplete = true
150
+ result = yield
151
+ incomplete = false
152
+ result
153
+ # Thrift exceptions get read off the wire. We can consider them complete requests
154
+ rescue Thrift::Exception => e
155
+ incomplete = false
156
+ raise e
157
+ ensure
158
+ disconnect! if incomplete
159
+ end
160
+
161
+ def handled_proxy(method_name, *args)
162
+ begin
163
+ connect!(method_name.to_sym) unless @client
164
+ if has_timeouts?
165
+ @client.timeout = timeout(method_name.to_sym)
166
+ end
167
+ @request_count += 1
168
+ do_callbacks(:before_method, method_name)
169
+ ensure_socket_alignment { @client.send(method_name, *args) }
170
+ rescue *@options[:exception_class_overrides] => e
171
+ raise_or_default(e, method_name)
172
+ rescue *@options[:exception_classes] => e
173
+ disconnect!(true)
174
+ tries ||= (@options[:retry_overrides][method_name.to_sym] || @options[:retries]) + 1
175
+ tries -= 1
176
+ if tries > 0
177
+ retry
178
+ else
179
+ raise_or_default(e, method_name)
180
+ end
181
+ rescue Exception => e
182
+ raise_or_default(e, method_name)
183
+ ensure
184
+ disconnect! if @options[:server_max_requests] && @request_count >= @options[:server_max_requests]
185
+ end
186
+ end
187
+
188
+ def raise_or_default(e, method_name)
189
+ if @options[:raise]
190
+ raise_wrapped_error(e, method_name)
191
+ else
192
+ @options[:defaults][method_name.to_sym]
193
+ end
194
+ end
195
+
196
+ def raise_wrapped_error(e, method_name)
197
+ do_callbacks(:on_exception, e, method_name)
198
+ if @options[:wrapped_exception_classes].include?(e.class)
199
+ raise @client_class.const_get(e.class.to_s.split('::').last), e.message, e.backtrace
200
+ else
201
+ raise e
202
+ end
203
+ end
204
+
205
+ def has_timeouts?
206
+ @has_timeouts ||= @options[:timeout_overrides].any? && transport_can_timeout?
207
+ end
208
+
209
+ def timeout(method = nil)
210
+ @options[:timeout_overrides][method] || @options[:timeout]
211
+ end
212
+
213
+ def transport_can_timeout?
214
+ if (@options[:transport_wrapper] || @options[:transport]).method_defined?(:timeout=)
215
+ true
216
+ else
217
+ warn "ThriftClient: Timeout overrides have no effect with with transport type #{(@options[:transport_wrapper] || @options[:transport])}"
218
+ false
219
+ end
220
+ end
221
+
222
+ def no_servers_available!
223
+ servers = @server_list.map { |s| s.to_s }.join(',')
224
+ raise ThriftClient::NoServersAvailable, "No live servers in [#{servers}]."
225
+ end
226
+ end
@@ -0,0 +1,4 @@
1
+ require "thrift_client/connection/base"
2
+ require "thrift_client/connection/socket"
3
+ require "thrift_client/connection/http"
4
+ require "thrift_client/connection/factory"
@@ -0,0 +1,25 @@
1
+ module ThriftHelpers
2
+ module Connection
3
+ class Base
4
+ attr_accessor :transport, :server
5
+
6
+ def initialize(transport, transport_wrapper, server, timeout)
7
+ @transport = transport
8
+ @transport_wrapper = transport_wrapper
9
+ @server = server
10
+ @timeout = timeout
11
+ end
12
+
13
+ def connect!
14
+ raise NotImplementedError
15
+ end
16
+
17
+ def open?
18
+ @transport.open?
19
+ end
20
+
21
+ def close
22
+ end
23
+ end
24
+ end
25
+ end
@@ -0,0 +1,13 @@
1
+ module ThriftHelpers
2
+ module Connection
3
+ class Factory
4
+ def self.create(transport, transport_wrapper, server, timeout)
5
+ if transport == Thrift::HTTPClientTransport
6
+ Connection::HTTP.new(transport, transport_wrapper, server, timeout)
7
+ else
8
+ Connection::Socket.new(transport, transport_wrapper, server, timeout)
9
+ end
10
+ end
11
+ end
12
+ end
13
+ end
@@ -0,0 +1,29 @@
1
+ module ThriftHelpers
2
+ module Connection
3
+ class HTTP < Base
4
+ def initialize(*args)
5
+ super *args
6
+
7
+ uri = parse_server(@server)
8
+ @transport = Thrift::HTTPClientTransport.new(@server)
9
+ end
10
+
11
+ def connect!
12
+ http = Net::HTTP.new(uri.host, uri.port)
13
+ http.use_ssl = uri.scheme == "https"
14
+ http.get(uri.path)
15
+ end
16
+
17
+ def open?
18
+ true
19
+ end
20
+
21
+ private
22
+ def parse_server(server)
23
+ uri = URI.parse(server)
24
+ raise ArgumentError, 'Servers must start with http' unless uri.scheme =~ /^http/
25
+ uri
26
+ end
27
+ end
28
+ end
29
+ end
@@ -0,0 +1,29 @@
1
+ module ThriftHelpers
2
+ module Connection
3
+ class Socket < Base
4
+ def initialize(*args)
5
+ super *args
6
+
7
+ host, port = parse_server(@server)
8
+ @transport = @transport.new(host, port.to_i, @timeout)
9
+ @transport = @transport_wrapper.new(@transport) if @transport_wrapper
10
+ end
11
+
12
+ def close
13
+ @transport.close
14
+ end
15
+
16
+ def connect!
17
+ @transport.open
18
+ end
19
+
20
+ private
21
+
22
+ def parse_server(server)
23
+ host, port = server.to_s.split(":")
24
+ raise ArgumentError, 'Servers must be in the form "host:port"' unless host and port
25
+ [host, port]
26
+ end
27
+ end
28
+ end
29
+ end
@@ -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
+
30
+ raise Thrift::TransportException, "Unable to connect to #{@host}:#{@port}" unless @connection.connected?
31
+ @connection
32
+ end
33
+
34
+ def close
35
+ @connection.close
36
+ end
37
+
38
+ def read(sz)
39
+ @connection.blocking_read(sz)
40
+ end
41
+
42
+ def write(buf)
43
+ @connection.send_data(buf)
44
+ end
45
+ end
46
+
47
+ module EventMachineConnection
48
+ GARBAGE_BUFFER_SIZE = 4096 # 4kB
49
+
50
+ include EM::Deferrable
51
+
52
+ def self.connect(host='localhost', port=9090, timeout=5, &block)
53
+ EM.connect(host, port, self, host, port) do |conn|
54
+ conn.pending_connect_timeout = timeout
55
+ end
56
+ end
57
+
58
+ def trap
59
+ begin
60
+ yield
61
+ rescue Exception => ex
62
+ puts ex.message
63
+ puts ex.backtrace.join("\n")
64
+ end
65
+ end
66
+
67
+ def initialize(host, port=9090)
68
+ @host, @port = host, port
69
+ @index = 0
70
+ @disconnected = 'not connected'
71
+ @buf = ''
72
+ end
73
+
74
+ def close
75
+ trap do
76
+ @disconnected = 'closed'
77
+ close_connection(true)
78
+ end
79
+ end
80
+
81
+ def blocking_read(size)
82
+ raise IOError, "lost connection to #{@host}:#{@port}: #{@disconnected}" if @disconnected
83
+ if can_read?(size)
84
+ yank(size)
85
+ else
86
+ raise ArgumentError, "Unexpected state" if @size or @callback
87
+
88
+ fiber = Fiber.current
89
+ @size = size
90
+ @callback = proc { |data|
91
+ fiber.resume(data)
92
+ }
93
+ Fiber.yield
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
+ !@disconnected
112
+ end
113
+
114
+ def connection_completed
115
+ @disconnected = nil
116
+ succeed
117
+ end
118
+
119
+ def unbind
120
+ if !@disconnected
121
+ @disconnected = 'unbound'
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