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