thrifter 0.1.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/.dockerignore +3 -0
- data/.gitignore +14 -0
- data/Dockerfile +15 -0
- data/Gemfile +4 -0
- data/LICENSE.txt +22 -0
- data/Makefile +57 -0
- data/README.md +283 -0
- data/Rakefile +8 -0
- data/Vagrantfile +126 -0
- data/circle.yml +10 -0
- data/lib/thrifter.rb +166 -0
- data/lib/thrifter/error_wrapping_middleware.rb +49 -0
- data/lib/thrifter/ping.rb +10 -0
- data/lib/thrifter/queueing.rb +38 -0
- data/lib/thrifter/retry.rb +57 -0
- data/lib/thrifter/statsd_middleware.rb +26 -0
- data/lib/thrifter/version.rb +3 -0
- data/script/monkey-client +61 -0
- data/script/server +32 -0
- data/test.thrift +8 -0
- data/test/acceptance_test.rb +221 -0
- data/test/error_wrapping_middleware_test.rb +73 -0
- data/test/ping_test.rb +31 -0
- data/test/queuing_test.rb +73 -0
- data/test/retry_test.rb +116 -0
- data/test/statsd_middleware_test.rb +99 -0
- data/test/test_helper.rb +45 -0
- data/thrifter.gemspec +35 -0
- data/vendor/gen-rb/test_constants.rb +9 -0
- data/vendor/gen-rb/test_service.rb +122 -0
- data/vendor/gen-rb/test_types.rb +25 -0
- metadata +264 -0
data/circle.yml
ADDED
data/lib/thrifter.rb
ADDED
@@ -0,0 +1,166 @@
|
|
1
|
+
require 'thrifter/version'
|
2
|
+
|
3
|
+
require 'forwardable'
|
4
|
+
require 'uri'
|
5
|
+
require 'tnt'
|
6
|
+
require 'concord'
|
7
|
+
require 'thrift'
|
8
|
+
require 'thrift-base64'
|
9
|
+
require 'middleware'
|
10
|
+
require 'connection_pool'
|
11
|
+
|
12
|
+
module Thrifter
|
13
|
+
RPC = Struct.new(:name, :args)
|
14
|
+
|
15
|
+
class MiddlewareStack < Middleware::Builder
|
16
|
+
def finalize!
|
17
|
+
stack.freeze
|
18
|
+
to_app
|
19
|
+
end
|
20
|
+
end
|
21
|
+
|
22
|
+
class NullStatsd
|
23
|
+
def time(*)
|
24
|
+
yield
|
25
|
+
end
|
26
|
+
|
27
|
+
def increment(*)
|
28
|
+
|
29
|
+
end
|
30
|
+
end
|
31
|
+
|
32
|
+
RESERVED_METHODS = [
|
33
|
+
:send_message,
|
34
|
+
:send_oneway_message,
|
35
|
+
:send_message_args
|
36
|
+
]
|
37
|
+
|
38
|
+
Configuration = Struct.new :transport, :protocol,
|
39
|
+
:pool_size, :pool_timeout,
|
40
|
+
:uri, :rpc_timeout,
|
41
|
+
:stack, :statsd
|
42
|
+
|
43
|
+
class << self
|
44
|
+
def build(client_class, &block)
|
45
|
+
rpcs = client_class.instance_methods.each_with_object([ ]) do |method_name, rpcs|
|
46
|
+
next if RESERVED_METHODS.include? method_name
|
47
|
+
next unless method_name =~ /^send_(?<rpc>.+)$/
|
48
|
+
rpcs << Regexp.last_match[:rpc].to_sym
|
49
|
+
end
|
50
|
+
|
51
|
+
rpcs.freeze
|
52
|
+
|
53
|
+
Class.new Client do
|
54
|
+
rpcs.each do |rpc_name|
|
55
|
+
define_method rpc_name do |*args|
|
56
|
+
invoke RPC.new(rpc_name, args)
|
57
|
+
end
|
58
|
+
end
|
59
|
+
|
60
|
+
class_eval(&block) if block
|
61
|
+
|
62
|
+
private
|
63
|
+
|
64
|
+
define_method :rpcs do
|
65
|
+
rpcs
|
66
|
+
end
|
67
|
+
|
68
|
+
define_method :client_class do
|
69
|
+
client_class
|
70
|
+
end
|
71
|
+
end
|
72
|
+
end
|
73
|
+
end
|
74
|
+
|
75
|
+
class Client
|
76
|
+
class Dispatcher
|
77
|
+
include Concord.new(:app, :transport, :client)
|
78
|
+
|
79
|
+
def call(rpc)
|
80
|
+
transport.open
|
81
|
+
client.send rpc.name, *rpc.args
|
82
|
+
ensure
|
83
|
+
transport.close
|
84
|
+
end
|
85
|
+
end
|
86
|
+
|
87
|
+
class << self
|
88
|
+
extend Forwardable
|
89
|
+
|
90
|
+
attr_accessor :config
|
91
|
+
|
92
|
+
def_delegators :config, :stack
|
93
|
+
def_delegators :stack, :use
|
94
|
+
|
95
|
+
def configure
|
96
|
+
yield config
|
97
|
+
end
|
98
|
+
|
99
|
+
# NOTE: the inherited hook is better than doing singleton
|
100
|
+
# methods for config. This works when Thrifter is used like a
|
101
|
+
# struct MyClient = Thrifter.build(MyService) or like delegate
|
102
|
+
# class MyClient < Thrifter.build(MyService). The end result is
|
103
|
+
# each class has it's own configuration instance.
|
104
|
+
def inherited(base)
|
105
|
+
base.config = Configuration.new
|
106
|
+
base.configure do |config|
|
107
|
+
config.transport = Thrift::FramedTransport
|
108
|
+
config.protocol = Thrift::BinaryProtocol
|
109
|
+
config.pool_size = 12
|
110
|
+
config.pool_timeout = 0.1
|
111
|
+
config.rpc_timeout = 0.3
|
112
|
+
config.statsd = NullStatsd.new
|
113
|
+
config.stack = MiddlewareStack.new
|
114
|
+
end
|
115
|
+
end
|
116
|
+
end
|
117
|
+
|
118
|
+
def initialize
|
119
|
+
fail ArgumentError, 'config.uri not set!' unless config.uri
|
120
|
+
|
121
|
+
uri = URI(config.uri)
|
122
|
+
|
123
|
+
fail ArgumentError, 'URI did not contain port' unless uri.port
|
124
|
+
|
125
|
+
@pool = ConnectionPool.new size: config.pool_size.to_i, timeout: config.pool_timeout.to_f do
|
126
|
+
stack = MiddlewareStack.new
|
127
|
+
|
128
|
+
stack.use config.stack
|
129
|
+
|
130
|
+
# Insert metrics here so metrics are as close to the network
|
131
|
+
# as possible. This excludes time in any middleware an
|
132
|
+
# application may have configured.
|
133
|
+
stack.use StatsdMiddleware, config.statsd
|
134
|
+
|
135
|
+
socket = Thrift::Socket.new uri.host, uri.port, config.rpc_timeout.to_f
|
136
|
+
transport = config.transport.new socket
|
137
|
+
protocol = config.protocol.new transport
|
138
|
+
|
139
|
+
stack.use Dispatcher, transport, client_class.new(protocol)
|
140
|
+
|
141
|
+
stack.finalize!
|
142
|
+
end
|
143
|
+
end
|
144
|
+
|
145
|
+
private
|
146
|
+
|
147
|
+
def pool
|
148
|
+
@pool
|
149
|
+
end
|
150
|
+
|
151
|
+
def config
|
152
|
+
self.class.config
|
153
|
+
end
|
154
|
+
|
155
|
+
def invoke(rpc)
|
156
|
+
pool.with do |stack|
|
157
|
+
stack.call rpc
|
158
|
+
end
|
159
|
+
end
|
160
|
+
end
|
161
|
+
end
|
162
|
+
|
163
|
+
require_relative 'thrifter/statsd_middleware'
|
164
|
+
require_relative 'thrifter/ping'
|
165
|
+
require_relative 'thrifter/error_wrapping_middleware'
|
166
|
+
require_relative 'thrifter/retry'
|
@@ -0,0 +1,49 @@
|
|
1
|
+
module Thrifter
|
2
|
+
ClientError = Tnt.boom do |ex|
|
3
|
+
"#{ex.class}: #{ex.message}"
|
4
|
+
end
|
5
|
+
|
6
|
+
class ErrorWrappingMiddleware
|
7
|
+
WRAP = [
|
8
|
+
Thrift::TransportException,
|
9
|
+
Thrift::ProtocolException,
|
10
|
+
Thrift::ApplicationException,
|
11
|
+
Timeout::Error,
|
12
|
+
|
13
|
+
# This exception is a superclass for all Errno things coming
|
14
|
+
# from the operating system network stack. See the documentation
|
15
|
+
# on Error no for more information.
|
16
|
+
SystemCallError
|
17
|
+
]
|
18
|
+
|
19
|
+
class << self
|
20
|
+
def wrapped
|
21
|
+
WRAP
|
22
|
+
end
|
23
|
+
end
|
24
|
+
|
25
|
+
def initialize(app, extras = [ ])
|
26
|
+
@app, @extras = app, extras
|
27
|
+
end
|
28
|
+
|
29
|
+
def call(rpc)
|
30
|
+
app.call rpc
|
31
|
+
rescue *wrapped => ex
|
32
|
+
raise ClientError, ex
|
33
|
+
end
|
34
|
+
|
35
|
+
private
|
36
|
+
|
37
|
+
def app
|
38
|
+
@app
|
39
|
+
end
|
40
|
+
|
41
|
+
def extras
|
42
|
+
@extras
|
43
|
+
end
|
44
|
+
|
45
|
+
def wrapped
|
46
|
+
self.class.wrapped + extras
|
47
|
+
end
|
48
|
+
end
|
49
|
+
end
|
@@ -0,0 +1,38 @@
|
|
1
|
+
require 'sidekiq'
|
2
|
+
require 'sidekiq-thrift_arguments'
|
3
|
+
|
4
|
+
module Thrifter
|
5
|
+
module Queueing
|
6
|
+
class Job
|
7
|
+
include Sidekiq::Worker
|
8
|
+
include Sidekiq::ThriftArguments
|
9
|
+
|
10
|
+
# NOTE: sidekiq-thrift_arguments does not recurse into
|
11
|
+
# arguments. The thrift objects must not be inside an array or
|
12
|
+
# other structure. This is why the method has so many arguments.
|
13
|
+
# Sidekik-thrift_arguments will correctly pick up any complex
|
14
|
+
# type in the splat and handle serialization/deserialization
|
15
|
+
def perform(klass, rpc_name, *rpc_args)
|
16
|
+
client = klass.constantize.new
|
17
|
+
client.send rpc_name, *rpc_args
|
18
|
+
end
|
19
|
+
end
|
20
|
+
|
21
|
+
class Proxy
|
22
|
+
def initialize(klass, rpcs)
|
23
|
+
rpcs.each do |name|
|
24
|
+
define_singleton_method name do |*args|
|
25
|
+
job_args = [ klass.to_s, name ].concat(args)
|
26
|
+
Job.perform_async(*job_args)
|
27
|
+
end
|
28
|
+
end
|
29
|
+
end
|
30
|
+
end
|
31
|
+
|
32
|
+
def queued
|
33
|
+
Proxy.new(self.class, rpcs).tap do |proxy|
|
34
|
+
yield proxy if block_given?
|
35
|
+
end
|
36
|
+
end
|
37
|
+
end
|
38
|
+
end
|
@@ -0,0 +1,57 @@
|
|
1
|
+
module Thrifter
|
2
|
+
RetryError = Tnt.boom do |count, rpc|
|
3
|
+
"#{rpc} RPC unsuccessful after #{count} times"
|
4
|
+
end
|
5
|
+
|
6
|
+
module Retry
|
7
|
+
RETRIABLE_ERRORS = [
|
8
|
+
Thrift::TransportException,
|
9
|
+
Thrift::ProtocolException,
|
10
|
+
Thrift::ApplicationException,
|
11
|
+
Timeout::Error,
|
12
|
+
Errno::ECONNREFUSED,
|
13
|
+
Errno::EADDRNOTAVAIL,
|
14
|
+
Errno::EHOSTUNREACH,
|
15
|
+
Errno::EHOSTDOWN,
|
16
|
+
Errno::ETIMEDOUT
|
17
|
+
]
|
18
|
+
|
19
|
+
class Proxy
|
20
|
+
attr_reader :tries, :interval, :client
|
21
|
+
|
22
|
+
def initialize(client, tries, interval, rpcs)
|
23
|
+
@client, @tries, @interval = client, tries, interval
|
24
|
+
|
25
|
+
rpcs.each do |name|
|
26
|
+
define_singleton_method name do |*args|
|
27
|
+
invoke_with_retry(name, *args)
|
28
|
+
end
|
29
|
+
end
|
30
|
+
end
|
31
|
+
|
32
|
+
private
|
33
|
+
|
34
|
+
def invoke_with_retry(name, *args)
|
35
|
+
counter = 0
|
36
|
+
|
37
|
+
begin
|
38
|
+
counter = counter + 1
|
39
|
+
client.send name, *args
|
40
|
+
rescue *RETRIABLE_ERRORS => ex
|
41
|
+
if counter < tries
|
42
|
+
sleep interval
|
43
|
+
retry
|
44
|
+
else
|
45
|
+
raise RetryError.new(tries, name)
|
46
|
+
end
|
47
|
+
end
|
48
|
+
end
|
49
|
+
end
|
50
|
+
|
51
|
+
def with_retry(tries: 5, interval: 0.01)
|
52
|
+
Proxy.new(self, tries, interval, rpcs).tap do |proxy|
|
53
|
+
yield proxy if block_given?
|
54
|
+
end
|
55
|
+
end
|
56
|
+
end
|
57
|
+
end
|
@@ -0,0 +1,26 @@
|
|
1
|
+
module Thrifter
|
2
|
+
class StatsdMiddleware
|
3
|
+
include Concord.new(:app, :statsd)
|
4
|
+
|
5
|
+
def call(rpc)
|
6
|
+
statsd.time rpc.name do
|
7
|
+
app.call rpc
|
8
|
+
end
|
9
|
+
rescue Thrift::TransportException => ex
|
10
|
+
statsd.increment 'errors.transport'
|
11
|
+
raise ex
|
12
|
+
rescue Thrift::ProtocolException => ex
|
13
|
+
statsd.increment 'errors.protocol'
|
14
|
+
raise ex
|
15
|
+
rescue Thrift::ApplicationException => ex
|
16
|
+
statsd.increment 'errors.application'
|
17
|
+
raise ex
|
18
|
+
rescue Timeout::Error => ex
|
19
|
+
statsd.increment 'errors.timeout'
|
20
|
+
raise ex
|
21
|
+
rescue => ex
|
22
|
+
statsd.increment 'errors.other'
|
23
|
+
raise ex
|
24
|
+
end
|
25
|
+
end
|
26
|
+
end
|
@@ -0,0 +1,61 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
|
3
|
+
$stdout.sync = true
|
4
|
+
$stderr.sync = true
|
5
|
+
|
6
|
+
uri = ARGV[0]
|
7
|
+
|
8
|
+
if uri.nil?
|
9
|
+
abort 'USAGE: monkey-client HOST:PORT'
|
10
|
+
end
|
11
|
+
|
12
|
+
require 'bundler/setup'
|
13
|
+
require 'thrifter'
|
14
|
+
require 'thrifter/queueing'
|
15
|
+
require 'securerandom'
|
16
|
+
|
17
|
+
require 'eventmachine'
|
18
|
+
|
19
|
+
root = File.expand_path '../..', __FILE__
|
20
|
+
$LOAD_PATH << "#{root}/vendor/gen-rb"
|
21
|
+
|
22
|
+
require 'test_service'
|
23
|
+
|
24
|
+
class MonkeyClient < Thrifter.build(TestService::Client)
|
25
|
+
include Thrifter::Retry
|
26
|
+
|
27
|
+
config.uri = "tcp://#{ARGV[0]}"
|
28
|
+
end
|
29
|
+
|
30
|
+
client = MonkeyClient.new
|
31
|
+
|
32
|
+
EM.run do
|
33
|
+
rand(1..5).times do
|
34
|
+
EM.add_periodic_timer rand(0.1..2.0) do
|
35
|
+
client.echo(TestMessage.new(message: "Echo #{SecureRandom.hex(16)}"))
|
36
|
+
end
|
37
|
+
end
|
38
|
+
|
39
|
+
rand(1..5).times do
|
40
|
+
EM.add_periodic_timer rand(0.1..2.0) do
|
41
|
+
client.onewayEcho(TestMessage.new(message: "Oneway: #{SecureRandom.hex(16)}"))
|
42
|
+
end
|
43
|
+
end
|
44
|
+
|
45
|
+
rand(1..5).times do
|
46
|
+
EM.add_periodic_timer rand(0.1..2.0) do
|
47
|
+
client.with_retry.echo(TestMessage.new(message: 'retried echo'))
|
48
|
+
end
|
49
|
+
end
|
50
|
+
|
51
|
+
rand(1..5).times do
|
52
|
+
EM.add_periodic_timer rand(0.1..2.0) do
|
53
|
+
client.with_retry.onewayEcho(TestMessage.new(message: 'retried oneway echo'))
|
54
|
+
end
|
55
|
+
end
|
56
|
+
|
57
|
+
EM.add_timer 30 do
|
58
|
+
puts "Good monkey...here's your banana."
|
59
|
+
EM.stop
|
60
|
+
end
|
61
|
+
end
|
data/script/server
ADDED
@@ -0,0 +1,32 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
|
3
|
+
$stdout.sync = true
|
4
|
+
$stderr.sync = true
|
5
|
+
|
6
|
+
require 'bundler/setup'
|
7
|
+
require 'thrift'
|
8
|
+
|
9
|
+
root = File.expand_path '../..', __FILE__
|
10
|
+
$LOAD_PATH << "#{root}/vendor/gen-rb"
|
11
|
+
|
12
|
+
require 'test_service'
|
13
|
+
|
14
|
+
puts 'Starting on port 9090.....'
|
15
|
+
|
16
|
+
class Handler
|
17
|
+
def echo(message)
|
18
|
+
puts "echo --#{message.inspect}"
|
19
|
+
message
|
20
|
+
end
|
21
|
+
|
22
|
+
def onewayEcho(message)
|
23
|
+
puts "onewayEcho -- #{message.inspect}"
|
24
|
+
# nada
|
25
|
+
end
|
26
|
+
end
|
27
|
+
|
28
|
+
processor = TestService::Processor.new Handler.new
|
29
|
+
transport = Thrift::ServerSocket.new 9090
|
30
|
+
transport_factory = Thrift::FramedTransportFactory.new
|
31
|
+
server = Thrift::ThreadPoolServer.new processor, transport, transport_factory, nil, threads = 10
|
32
|
+
server.serve
|