thrifter 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,10 @@
1
+ machine:
2
+ services:
3
+ - docker
4
+ dependencies:
5
+ override:
6
+ - echo 'Nothing to see here'
7
+ test:
8
+ override:
9
+ - make test-ci
10
+ - make clean
@@ -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,10 @@
1
+ module Thrifter
2
+ module Ping
3
+ def up?
4
+ ping
5
+ true
6
+ rescue
7
+ false
8
+ end
9
+ end
10
+ 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,3 @@
1
+ module Thrifter
2
+ VERSION = "0.1.0"
3
+ 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
@@ -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