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