thrift_server 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA1:
3
+ metadata.gz: 9558c4785c5f60c3178add28862c2074d0a45ed9
4
+ data.tar.gz: af9ce55e5055cca900ff4d32f48558bb21d5a158
5
+ SHA512:
6
+ metadata.gz: 3ec3fd8f56101e485a8f03654cbb921f6d1f5f047b1f408e20da258d11231d5b0e77a0124bd614b2af13f224baeb9776abbc4ea18b93d3c8db3f9813a986d05f
7
+ data.tar.gz: 8696c6d72a71ed0c6307dd88bd722d40109abbcc0761da9033294808f3a0d0c8e9cfc607c818ef29abed3a7c6eb381ea7acaa0b486e2a889dfd27015fcfce38f
data/.dockerignore ADDED
@@ -0,0 +1,3 @@
1
+ .git
2
+ .vagrant
3
+ tmp
data/.gitignore ADDED
@@ -0,0 +1,24 @@
1
+ *.gem
2
+ *.rbc
3
+ .bundle
4
+ .config
5
+ .yardoc
6
+ Gemfile.lock
7
+ InstalledFiles
8
+ _yardoc
9
+ coverage
10
+ doc/
11
+ lib/bundler/man
12
+ pkg
13
+ rdoc
14
+ spec/reports
15
+ test/tmp
16
+ test/version_tmp
17
+ tmp
18
+ *.bundle
19
+ *.so
20
+ *.o
21
+ *.a
22
+ mkmf.log
23
+ vendor/cache
24
+ gen-rb/
data/Dockerfile ADDED
@@ -0,0 +1,12 @@
1
+ FROM ruby:2.1
2
+
3
+ RUN mkdir -p /usr/src/app
4
+ WORKDIR /usr/src/app
5
+
6
+ ADD Gemfile /usr/src/app/Gemfile
7
+ ADD lib/thrift_server/version.rb /usr/src/app/lib/thrift_server/version.rb
8
+ ADD thrift_server.gemspec /usr/src/app/thrift_server.gemspec
9
+
10
+ RUN bundle install
11
+
12
+ CMD [ "bundle", "console" ]
data/Gemfile ADDED
@@ -0,0 +1,4 @@
1
+ source 'https://rubygems.org'
2
+
3
+ # Specify your gem's dependencies in thrift_server.gemspec
4
+ gemspec
data/LICENSE.txt ADDED
@@ -0,0 +1,22 @@
1
+ Copyright (c) 2014 ahawkins
2
+
3
+ MIT License
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining
6
+ a copy of this software and associated documentation files (the
7
+ "Software"), to deal in the Software without restriction, including
8
+ without limitation the rights to use, copy, modify, merge, publish,
9
+ distribute, sublicense, and/or sell copies of the Software, and to
10
+ permit persons to whom the Software is furnished to do so, subject to
11
+ the following conditions:
12
+
13
+ The above copyright notice and this permission notice shall be
14
+ included in all copies or substantial portions of the Software.
15
+
16
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
17
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
18
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
19
+ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
20
+ LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
21
+ OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
22
+ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
data/Makefile ADDED
@@ -0,0 +1,43 @@
1
+ APP:=thrift_server
2
+
3
+ .DEFAULT_GOAL:=build
4
+
5
+ APP_RUN:=docker run --rm -t -v $(CURDIR):/usr/src/app $(APP)
6
+
7
+ gen-rb/echo_service.rb: echo_service.thrift
8
+ docker run --rm -t -v $(CURDIR):/data thrift:0.9.2 \
9
+ thrift -o /data --gen rb /data/$<
10
+
11
+ .PHONY: thrift
12
+ thrift: gen-rb/echo_service.rb
13
+
14
+ tmp/image: Dockerfile Gemfile lib/thrift_server/version.rb thrift_server.gemspec
15
+ docker build -t $(APP) .
16
+ mkdir -p $(@D)
17
+ docker inspect -f '{{.Id}}' $(APP) >> $@
18
+
19
+ .PHONY: build
20
+ build: tmp/image
21
+
22
+ .PHONY: test-unit
23
+ test-unit: tmp/image
24
+ $(APP_RUN) bundle exec rake
25
+
26
+ .PHONY: test-network
27
+ test-network: tmp/image gen-rb/echo_service.rb
28
+ -@docker stop server > /dev/null 2>&1
29
+ -@docker rm -v server > /dev/null 2>&1
30
+ docker run -d --name server -v $(CURDIR):/usr/src/app $(APP) ruby echo_server.rb
31
+ docker run --rm -t -v $(CURDIR):/usr/src/app --link server:server $(APP) ruby echo_client.rb server 9090
32
+
33
+ .PHONY: test-ci
34
+ test-ci: test-unit test-network
35
+
36
+ .PHONY: clean
37
+ clean:
38
+ -docker stop server > /dev/null 2>&1
39
+ -docker rm -v server > /dev/null 2>&1
40
+ @mkdir -p tmp
41
+ @touch tmp/image
42
+ cat tmp/image | xargs --no-run-if-empty docker rmi
43
+ rm -rf tmp/image
data/README.md ADDED
@@ -0,0 +1,108 @@
1
+ # ThriftServer
2
+
3
+ Encapsulate bolierplate code and functionality for running Thrift
4
+ servers in Ruby. Bundled functionality:
5
+
6
+ * Error tracking
7
+ * Metrics on each RPC
8
+ * Logging on each RPC
9
+ * Middleware based approaching making it easy to extend
10
+ * Binary protocol
11
+ * Framed transport
12
+ * Thread pool sever
13
+
14
+ ## Installation
15
+
16
+ Add this line to your application's Gemfile:
17
+
18
+ gem 'thrift_server'
19
+
20
+ And then execute:
21
+
22
+ $ bundle
23
+
24
+ Or install it yourself as:
25
+
26
+ $ gem install thrift_server
27
+
28
+ ## Usage
29
+
30
+ The libary is defined to wrapping Thrift's processor classes in useful
31
+ functionality for production servers. You must provide objects
32
+ required to implement the behavior. Here's an example from
33
+ [echo\_server.rb](echo_server.rb).
34
+
35
+ server = ThriftServer.build(EchoService::Processor, Handler.new, {
36
+ logger: Logger.new($stdout),
37
+ statsd: Statsd.new,
38
+ error_tracker: ErrorTracker.new
39
+ })
40
+
41
+ # Now call serve to start it
42
+ server.serve
43
+
44
+ The first arument is a generated `Thrift::Processor` subclass. The second
45
+ is your implementation of the define protocol. The options hash
46
+ defines all the misc objecs and settings. The following options are
47
+ available:
48
+
49
+ * `logger:` - a `Logger` instance
50
+ * `statsd:` - a `Statsd` instance from the [statsd-ruby gem](https://github.com/reinh/statsd)
51
+ * `error_tracker:` object that implement `#track(rpc, error). Use
52
+ `ThriftServer::HoneybaderErrorTracker` if unsure
53
+ * `threads:` - number of threads to run. Defaults to `4`
54
+ * `port:` - port to run server on. Defaults to `9090`
55
+
56
+ ## Middleware
57
+
58
+ The library uses a middleware approach to implement preprocessing of
59
+ each RPC before handing it off the original handler. You can
60
+ implement your own middleware easily. The middleware must respond to
61
+ `call` and accept a `ThriftServer::RPC` object.
62
+ `ThriftServer::RPC` is a simple struct with two members: `name` and
63
+ `args`. Here's an example that dumps the `args` to stdout.
64
+
65
+ class ExampleMiddleware
66
+ include Concord.new(:app)
67
+
68
+ def call(rpc)
69
+ puts rpc.args.inspect
70
+ app.call rpc
71
+ end
72
+ end
73
+
74
+ New middleware can be added at build time or afterwards.
75
+
76
+ ThriftServer::build processor, handler, options do |stack|
77
+ stack.use ExampleMiddlware
78
+ end
79
+
80
+ Middleware can also be added after the server is built
81
+
82
+ server = ThriftServer::build processor, handler, options
83
+ server.use ExampleMiddleware
84
+ serve.serve # start it!
85
+
86
+ ## Implementation
87
+
88
+ `ThriftServer` used metaprogramming and delegating to implement the
89
+ funcationality. `build` used the processor class to determine which
90
+ methods are required by the protocol. A middleware stack is created
91
+ using the object passed in the `options` hash. A delegate class is
92
+ created to wrap each method call to pass through the stack before
93
+ calling the defined handler. A subclass of the processor is defined to
94
+ wrap the handler provided in `initialize` in the delegate class.
95
+
96
+ ## Development
97
+
98
+ $ vagrant up
99
+ $ vagrant ssh
100
+ $ cd /vagrant
101
+ $ make test-ci
102
+
103
+ ## Contributing
104
+
105
+ 1. Create your feature branch (`git checkout -b my-new-feature`)
106
+ 2. Commit your changes (`git commit -am 'Add some feature'`)
107
+ 3. Push to the branch (`git push origin my-new-feature`)
108
+ 4. Create a new Pull Request
data/Rakefile ADDED
@@ -0,0 +1,12 @@
1
+ require "bundler/gem_tasks"
2
+ require "rake/testtask"
3
+
4
+ Rake::TestTask.new do |f|
5
+ f.test_files = Rake::FileList['test/**/*_test.rb']
6
+ end
7
+
8
+ task :benchmark do
9
+ ruby "benchmark/server.rb"
10
+ end
11
+
12
+ task default: :test
data/Vagrantfile ADDED
@@ -0,0 +1,14 @@
1
+ # -*- mode: ruby -*-
2
+ # vi: set ft=ruby :
3
+
4
+ # Vagrantfile API/syntax version. Don't touch unless you know what you're doing!
5
+ VAGRANTFILE_API_VERSION = "2"
6
+
7
+ Vagrant.configure(VAGRANTFILE_API_VERSION) do |config|
8
+ config.vm.box = "ubuntu/trusty64"
9
+
10
+ config.vm.provision "docker" do |docker|
11
+ docker.pull_images "ruby:2.1"
12
+ docker.pull_images "thrift:0.9.2"
13
+ end
14
+ end
@@ -0,0 +1,44 @@
1
+ require 'bundler/setup'
2
+ require 'thrift_server'
3
+
4
+ require 'benchmark/ips'
5
+ require 'logger-better'
6
+ require 'middleware'
7
+
8
+ class NullStatsd
9
+ def time(*) ; yield ; end
10
+ def increment(*) ; end
11
+ end
12
+
13
+ class NullErrorTracker
14
+ def track(*) ; end
15
+ end
16
+
17
+ class FakeProcessor
18
+ include Concord.new(:handler)
19
+
20
+ def process_someRPC
21
+ handler.someRPC
22
+ end
23
+ end
24
+
25
+ class FakeHandler
26
+ def someRPC
27
+ :response
28
+ end
29
+ end
30
+
31
+ Benchmark.ips do |x|
32
+ x.time = 20
33
+ x.warmup = 5
34
+
35
+ processor = ThriftServer.wrap(FakeProcessor, {
36
+ logger: NullLogger.new,
37
+ statsd: NullStatsd.new,
38
+ error_tracker: NullErrorTracker.new
39
+ })
40
+
41
+ server = processor.new FakeHandler.new
42
+
43
+ x.report("server") { server.process_someRPC }
44
+ end
data/echo_client.rb ADDED
@@ -0,0 +1,35 @@
1
+ $stdout.sync = true
2
+ $stderr.sync = true
3
+
4
+ require 'bundler/setup'
5
+ require 'thrift_server'
6
+
7
+ $LOAD_PATH << "#{__dir__}/gen-rb"
8
+
9
+ require 'echo_service'
10
+
11
+ host, port = ARGV[0], ARGV[1]
12
+
13
+ if !host || !port
14
+ $stdout.puts "usage: echo_client HOST PORT"
15
+ abort
16
+ end
17
+
18
+ logger = Logger.new $stdout
19
+
20
+ transport = Thrift::FramedTransport.new(Thrift::Socket.new(host, port.to_i))
21
+ transport.open
22
+
23
+ protocol = Thrift::BinaryProtocol.new transport
24
+ client = EchoService::Client.new protocol
25
+
26
+ result = client.echo "testing"
27
+
28
+ transport.close
29
+
30
+ if result == 'testing'
31
+ $stdout.puts 'OK'
32
+ else
33
+ $stderr.puts 'Message not echoed'
34
+ abort
35
+ end
data/echo_server.rb ADDED
@@ -0,0 +1,33 @@
1
+ $stdout.sync = true
2
+ $stderr.sync = true
3
+
4
+ require 'bundler/setup'
5
+ require 'thrift_server'
6
+
7
+ $LOAD_PATH << "#{__dir__}/gen-rb"
8
+
9
+ require 'echo_service'
10
+
11
+ class Handler
12
+ def echo(msg)
13
+ msg
14
+ end
15
+ end
16
+
17
+ class ErrorTracker
18
+ def track(*)
19
+
20
+ end
21
+ end
22
+
23
+ logger = Logger.new $stdout
24
+
25
+ server = ThriftServer.build(EchoService::Processor, Handler.new, {
26
+ logger: logger,
27
+ statsd: Statsd.new,
28
+ error_tracker: ErrorTracker.new
29
+ })
30
+
31
+ logger.info 'Starting server'
32
+
33
+ server.serve
@@ -0,0 +1,3 @@
1
+ service EchoService {
2
+ string echo(1: string message)
3
+ }
@@ -0,0 +1,110 @@
1
+ require "thrift_server/version"
2
+
3
+ require 'thrift'
4
+ require 'middleware'
5
+ require 'concord'
6
+ require 'forwardable'
7
+ require 'honeybadger'
8
+ require 'statsd-ruby'
9
+
10
+ require_relative 'thrift_server/logging_middleware'
11
+ require_relative 'thrift_server/metrics_middleware'
12
+ require_relative 'thrift_server/error_tracking_middleware'
13
+ require_relative 'thrift_server/honeybadger_error_tracker'
14
+
15
+ class ThriftServer
16
+ RPC = Struct.new(:name, :args)
17
+
18
+ class MiddlewareStack < Middleware::Builder
19
+ def finalize!
20
+ stack.freeze
21
+ to_app
22
+ end
23
+ end
24
+
25
+ class StackDelegate
26
+ class Dispatcher
27
+ include Concord.new(:app, :handler)
28
+
29
+ def call(rpc)
30
+ handler.send rpc.name, *rpc.args
31
+ end
32
+ end
33
+
34
+ extend Forwardable
35
+
36
+ include Concord.new(:stack, :handler)
37
+
38
+ def_delegator :stack, :use
39
+
40
+ def call(rpc)
41
+ app.call rpc
42
+ end
43
+
44
+ private
45
+ def app
46
+ @app ||= finalize_stack!
47
+ end
48
+
49
+ def finalize_stack!
50
+ stack.use Dispatcher, handler
51
+ stack.finalize!
52
+ end
53
+ end
54
+
55
+ class << self
56
+ def build(processor, handler, options = { }, &block)
57
+ stack = wrap(processor, options, &block).new handler
58
+ transport = Thrift::ServerSocket.new options.fetch(:port, 9090)
59
+ transport_factory = Thrift::FramedTransportFactory.new
60
+
61
+ Thrift::ThreadPoolServer.new stack, transport, transport_factory, nil, options.fetch(:threads, 4)
62
+ end
63
+
64
+ def wrap(processor, options = { })
65
+ rpcs = processor.instance_methods.select { |m| m =~ /^process_(.+)$/ }
66
+
67
+ logger = options.fetch :logger do
68
+ fail ArgumentError, ':logger required'
69
+ end
70
+
71
+ statsd = options.fetch :statsd do
72
+ fail ArgumentError, ':statsd required'
73
+ end
74
+
75
+ error_tracker = options.fetch :error_tracker do
76
+ fail ArgumentError, ':error_tracker required'
77
+ end
78
+
79
+ stack = MiddlewareStack.new
80
+ stack.use ErrorTrackingMiddleware, error_tracker
81
+ stack.use MetricsMiddleware, statsd
82
+ stack.use LoggingMiddleware, logger
83
+
84
+ yield stack if block_given?
85
+
86
+ wrapped = Class.new processor do
87
+ extend Forwardable
88
+
89
+ def_delegator :@handler, :use
90
+
91
+ define_method :initialize do |handler|
92
+ stack_delegator = Class.new StackDelegate
93
+ stack_delegator.module_eval do
94
+ rpcs.each do |method|
95
+ rpc_name = method.to_s.match(/^process_(.+)$/)[1]
96
+
97
+ define_method rpc_name.to_sym do |*args|
98
+ call RPC.new(rpc_name, args)
99
+ end
100
+ end
101
+ end
102
+
103
+ super stack_delegator.new(stack, handler)
104
+ end
105
+ end
106
+
107
+ wrapped
108
+ end
109
+ end
110
+ end
@@ -0,0 +1,12 @@
1
+ class ThriftServer
2
+ class ErrorTrackingMiddleware
3
+ include Concord.new(:app, :logger)
4
+
5
+ def call(rpc)
6
+ app.call rpc
7
+ rescue => ex
8
+ logger.track rpc, ex
9
+ raise ex
10
+ end
11
+ end
12
+ end
@@ -0,0 +1,11 @@
1
+ class ThriftServer
2
+ class HoneybadgerErrorTracker
3
+ def initialize(client = Honeybadger)
4
+ @client = client
5
+ end
6
+
7
+ def track(rpc, error)
8
+ @client.notify_or_ignore error
9
+ end
10
+ end
11
+ end
@@ -0,0 +1,13 @@
1
+ class ThriftServer
2
+ class LoggingMiddleware
3
+ include Concord.new(:app, :logger)
4
+
5
+ def call(rpc)
6
+ logger.info "Incoming RPC: #{rpc.name}"
7
+ app.call rpc
8
+ rescue => ex
9
+ logger.error ex
10
+ raise ex
11
+ end
12
+ end
13
+ end
@@ -0,0 +1,14 @@
1
+ class ThriftServer
2
+ class MetricsMiddleware
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 => ex
10
+ statsd.increment 'errors'
11
+ raise ex
12
+ end
13
+ end
14
+ end
@@ -0,0 +1,3 @@
1
+ class ThriftServer
2
+ VERSION = "0.1.0"
3
+ end
@@ -0,0 +1,5 @@
1
+ #!/usr/bin/env bash
2
+
3
+ set -eou pipefail
4
+
5
+ make test-ci clean
@@ -0,0 +1,5 @@
1
+ #!/usr/bin/env bash
2
+
3
+ set -eou pipefail
4
+
5
+ make clean
@@ -0,0 +1,225 @@
1
+ require_relative 'test_helper'
2
+
3
+ class AcceptanceTest < MiniTest::Unit::TestCase
4
+ attr_reader :processor
5
+
6
+ def setup
7
+ @processor = Processor(:getItems)
8
+ end
9
+
10
+ def wrap(klass, &block)
11
+ ThriftServer.wrap(processor, {
12
+ logger: NullLogger.new,
13
+ statsd: FakeStatsd.new,
14
+ error_tracker: NullErrorTracker.new,
15
+ }, &block)
16
+ end
17
+
18
+ def test_wrap_fails_if_no_logger
19
+ ex = assert_raises ArgumentError do
20
+ ThriftServer.wrap(processor, {
21
+ statsd: FakeStatsd.new,
22
+ error_tracker: NullErrorTracker.new,
23
+ })
24
+ end
25
+
26
+ assert_match /logger/, ex.to_s
27
+ end
28
+
29
+ def test_wrap_fails_if_no_stats
30
+ ex = assert_raises ArgumentError do
31
+ ThriftServer.wrap(processor, {
32
+ logger: NullLogger.new,
33
+ error_tracker: NullErrorTracker.new,
34
+ })
35
+ end
36
+
37
+ assert_match /statsd/, ex.to_s
38
+ end
39
+
40
+ def test_wrap_fails_if_no_error_tracker
41
+ ex = assert_raises ArgumentError do
42
+ ThriftServer.wrap(processor, {
43
+ logger: NullLogger.new,
44
+ statsd: FakeStatsd.new
45
+ })
46
+ end
47
+
48
+ assert_match /error_tracker/, ex.to_s
49
+ end
50
+
51
+ def test_wraps_methods_defined_by_the_protocol
52
+ handler = stub
53
+ handler.expects(:getItems).with(:request).returns(:response)
54
+
55
+ stack = wrap(processor).new(handler)
56
+
57
+ assert_equal :response, stack.process_getItems(:request)
58
+ end
59
+
60
+ def test_can_add_new_middleware_after_wrapping
61
+ handler = mock
62
+ handler.expects(:getItems).with(:modified_args)
63
+
64
+ stack = wrap(processor).new(handler)
65
+
66
+ test_middleware = Class.new do
67
+ def initialize(app)
68
+ @app = app
69
+ end
70
+
71
+ def call(env)
72
+ env.args = :modified_args
73
+ @app.call env
74
+ end
75
+ end
76
+
77
+ stack.use test_middleware
78
+
79
+ stack.process_getItems :request
80
+ end
81
+
82
+ def test_cannot_add_middleware_to_stack_after_first_rpc
83
+ handler = stub getItems: :response
84
+
85
+ stack = wrap(processor).new(handler)
86
+
87
+ stack.process_getItems :request
88
+
89
+ ex = assert_raises RuntimeError do
90
+ stack.use ->(rpc) { rpc }
91
+ end
92
+
93
+ assert_match /frozen/, ex.to_s
94
+ end
95
+
96
+ def test_wrap_yields_the_middleware_stack
97
+ handler = mock
98
+ handler.expects(:getItems).with(:from_block)
99
+
100
+ test_middleware = Class.new do
101
+ def initialize(app)
102
+ @app = app
103
+ end
104
+
105
+ def call(env)
106
+ env.args = :from_block
107
+ @app.call env
108
+ end
109
+ end
110
+
111
+ stack_processor = wrap processor do |stack|
112
+ stack.use test_middleware
113
+ end
114
+ stack = stack_processor.new(handler)
115
+
116
+ stack.process_getItems :request
117
+ end
118
+
119
+ def test_build_returns_thread_pool_server
120
+ handler = stub getItems: :response
121
+ server = ThriftServer.build(processor, handler, {
122
+ logger: NullLogger.new,
123
+ statsd: FakeStatsd.new,
124
+ error_tracker: NullErrorTracker.new
125
+ })
126
+
127
+ assert_instance_of Thrift::ThreadPoolServer, server
128
+ end
129
+
130
+ def test_build_defaults_to_port_9090
131
+ handler = stub getItems: :response
132
+ server = ThriftServer.build(processor, handler, {
133
+ logger: NullLogger.new,
134
+ statsd: FakeStatsd.new,
135
+ error_tracker: NullErrorTracker.new
136
+ })
137
+
138
+ assert_equal 9090, server.port
139
+ end
140
+
141
+ def test_build_accepts_port_options
142
+ handler = stub getItems: :response
143
+ server = ThriftServer.build(processor, handler, {
144
+ logger: NullLogger.new,
145
+ statsd: FakeStatsd.new,
146
+ error_tracker: NullErrorTracker.new,
147
+ port: 5000
148
+ })
149
+
150
+ assert_equal 5000, server.port
151
+ end
152
+
153
+ def test_build_defaults_to_4_threads
154
+ handler = stub getItems: :response
155
+ server = ThriftServer.build(processor, handler, {
156
+ logger: NullLogger.new,
157
+ statsd: FakeStatsd.new,
158
+ error_tracker: NullErrorTracker.new
159
+ })
160
+
161
+ assert_equal 4, server.threads
162
+ end
163
+
164
+ def test_build_accepts_threads_option
165
+ handler = stub getItems: :response
166
+ server = ThriftServer.build(processor, handler, {
167
+ logger: NullLogger.new,
168
+ statsd: FakeStatsd.new,
169
+ error_tracker: NullErrorTracker.new,
170
+ threads: 8
171
+ })
172
+
173
+ assert_equal 8, server.threads
174
+ end
175
+
176
+ def test_builds_creates_server_with_framed_transport
177
+ handler = stub getItems: :response
178
+ server = ThriftServer.build(processor, handler, {
179
+ logger: NullLogger.new,
180
+ statsd: FakeStatsd.new,
181
+ error_tracker: NullErrorTracker.new
182
+ })
183
+
184
+ assert_instance_of Thrift::FramedTransportFactory, server.transport_factory
185
+ end
186
+
187
+ def test_build_uses_server_socket_transport
188
+ handler = stub getItems: :response
189
+ server = ThriftServer.build(processor, handler, {
190
+ logger: NullLogger.new,
191
+ statsd: FakeStatsd.new,
192
+ error_tracker: NullErrorTracker.new
193
+ })
194
+
195
+ assert_instance_of Thrift::ServerSocket, server.server_transport
196
+ end
197
+
198
+ def test_build_creates_server_with_binary_protocol
199
+ handler = stub getItems: :response
200
+ server = ThriftServer.build(processor, handler, {
201
+ logger: NullLogger.new,
202
+ statsd: FakeStatsd.new,
203
+ error_tracker: NullErrorTracker.new
204
+ })
205
+
206
+ assert_instance_of Thrift::BinaryProtocolFactory, server.protocol_factory
207
+ end
208
+
209
+ def test_buil_accepts_a_block_to_customize_the_middleware_stack
210
+ handler = stub getItems: :response
211
+ block_yielded = false
212
+
213
+ server = ThriftServer.build(processor, handler, {
214
+ logger: NullLogger.new,
215
+ statsd: FakeStatsd.new,
216
+ error_tracker: NullErrorTracker.new
217
+ }) do |stack|
218
+ block_yielded = true
219
+
220
+ assert_instance_of ThriftServer::MiddlewareStack, stack
221
+ end
222
+
223
+ assert block_yielded, 'Block not used'
224
+ end
225
+ end
@@ -0,0 +1,25 @@
1
+ require_relative 'test_helper'
2
+
3
+ class ErrorTrackingMiddlewareTest < MiniTest::Unit::TestCase
4
+ attr_reader :rpc
5
+
6
+ def setup
7
+ @rpc = ThriftServer::RPC.new :foo, :bar
8
+ end
9
+
10
+ def test_logs_errors
11
+ error = TestError.new
12
+
13
+ logger = mock
14
+ logger.expects(:track).with(rpc, error)
15
+
16
+ app = stub
17
+ app.stubs(:call).raises(error)
18
+
19
+ middleware = ThriftServer::ErrorTrackingMiddleware.new app, logger
20
+
21
+ assert_raises TestError do
22
+ middleware.call rpc
23
+ end
24
+ end
25
+ end
@@ -0,0 +1,12 @@
1
+ require_relative 'test_helper'
2
+
3
+ class HoneybadgerErrorTrackerTest < MiniTest::Unit::TestCase
4
+ def test_logs_according_to_honeybadger_api
5
+ client = mock
6
+ client.expects(:notify_or_ignore).with(:error)
7
+
8
+ tracker = ThriftServer::HoneybadgerErrorTracker.new client
9
+
10
+ tracker.track :rpc, :error
11
+ end
12
+ end
@@ -0,0 +1,42 @@
1
+ require_relative 'test_helper'
2
+
3
+ class LoggingMiddlewareTest < MiniTest::Unit::TestCase
4
+ attr_reader :output, :logger, :rpc
5
+
6
+ def setup
7
+ @output = StringIO.new
8
+ @logger = Logger.new output
9
+ @rpc = ThriftServer::RPC.new :foo, :bar
10
+ end
11
+
12
+ def test_logs_incoming_rpcs_names
13
+ app = stub call: :result
14
+
15
+ middleware = ThriftServer::LoggingMiddleware.new(app, logger)
16
+
17
+ middleware.call rpc
18
+
19
+ assert_logged output, 'INFO'
20
+ assert_logged output, rpc.name
21
+ end
22
+
23
+ def test_logs_errors
24
+ app = stub
25
+ app.stubs(:call).raises(TestError)
26
+
27
+ middleware = ThriftServer::LoggingMiddleware.new(app, logger)
28
+
29
+ assert_raises TestError do
30
+ middleware.call rpc
31
+ end
32
+
33
+ assert_logged output, 'ERROR'
34
+ assert_logged output, 'TestError'
35
+ end
36
+
37
+ private
38
+ def assert_logged(log, text)
39
+ log.rewind
40
+ assert log.read.include?(text.to_s), "#{text} not logged"
41
+ end
42
+ end
@@ -0,0 +1,51 @@
1
+ require_relative 'test_helper'
2
+
3
+ class MetricsMiddlewareTest < MiniTest::Unit::TestCase
4
+ attr_reader :rpc
5
+
6
+ def setup
7
+ @rpc = ThriftServer::RPC.new :foo, :bar
8
+ end
9
+
10
+ def test_times_the_rpc
11
+ app = stub call: :result
12
+
13
+ statsd = Class.new FakeStatsd do
14
+ attr_reader :timer
15
+
16
+ def time(*args)
17
+ @timer = args.first
18
+ yield
19
+ end
20
+ end.new
21
+
22
+ middleware = ThriftServer::MetricsMiddleware.new(app, statsd)
23
+
24
+ middleware.call rpc
25
+
26
+ assert statsd.timer, 'Timer not recorded'
27
+ assert_equal rpc.name, statsd.timer
28
+ end
29
+
30
+ def test_increments_a_counter_on_failed_rpcs
31
+ app = stub
32
+ app.stubs(:call).raises(TestError)
33
+
34
+ statsd = Class.new FakeStatsd do
35
+ attr_reader :counter
36
+
37
+ def increment(*args)
38
+ @counter = args.first
39
+ end
40
+ end.new
41
+
42
+ middleware = ThriftServer::MetricsMiddleware.new(app, statsd)
43
+
44
+ assert_raises TestError do
45
+ middleware.call rpc
46
+ end
47
+
48
+ assert statsd.counter, 'Counter not updated'
49
+ assert_equal 'errors', statsd.counter
50
+ end
51
+ end
@@ -0,0 +1,60 @@
1
+ require 'bundler/setup'
2
+ require 'thrift_server'
3
+
4
+ require 'stringio'
5
+ require 'logger-better'
6
+
7
+ require 'minitest/autorun'
8
+ require 'mocha/mini_test'
9
+
10
+ TestError = Class.new StandardError
11
+
12
+ class FakeStatsd
13
+ def time(*)
14
+ yield
15
+ end
16
+
17
+ def increment(*)
18
+
19
+ end
20
+ end
21
+
22
+ class NullErrorTracker
23
+ def track(*)
24
+
25
+ end
26
+ end
27
+
28
+ def Processor(*rpcs)
29
+ Class.new do
30
+ def initialize(handler)
31
+ @handler = handler
32
+ end
33
+
34
+ rpcs.each do |method_name|
35
+ define_method "process_#{method_name}" do |*args|
36
+ # nothing to do, this is just a stub to create objects inline
37
+ # with the generate thrift processors
38
+ @handler.send method_name, *args
39
+ end
40
+ end
41
+ end
42
+ end
43
+
44
+ # Monkey patch attr readers to test various server settings
45
+ module Thrift
46
+ class ServerSocket
47
+ attr_reader :port
48
+ end
49
+
50
+ class ThreadPoolServer
51
+ attr_reader :server_transport, :transport_factory, :protocol_factory
52
+ def threads
53
+ @thread_q.max
54
+ end
55
+
56
+ def port
57
+ server_transport.port
58
+ end
59
+ end
60
+ end
@@ -0,0 +1,32 @@
1
+ # coding: utf-8
2
+ lib = File.expand_path('../lib', __FILE__)
3
+ $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
4
+ require 'thrift_server/version'
5
+
6
+ Gem::Specification.new do |spec|
7
+ spec.name = "thrift_server"
8
+ spec.version = ThriftServer::VERSION
9
+ spec.authors = ["ahawkins"]
10
+ spec.email = ["adam@saltside.se"]
11
+ spec.summary = %q{Encapsulate error handling, logging, and metrics for thrift servers}
12
+ spec.description = %q{}
13
+ spec.homepage = "https://github.com/saltside/thrift_server-ruby"
14
+ spec.license = "MIT"
15
+
16
+ spec.files = `git ls-files -z`.split("\x0")
17
+ spec.executables = spec.files.grep(%r{^bin/}) { |f| File.basename(f) }
18
+ spec.test_files = spec.files.grep(%r{^(test|spec|features)/})
19
+ spec.require_paths = ["lib"]
20
+
21
+ spec.add_dependency "statsd-ruby"
22
+ spec.add_dependency "middleware"
23
+ spec.add_dependency "concord"
24
+ spec.add_dependency "honeybadger"
25
+ spec.add_dependency "thrift"
26
+
27
+ spec.add_development_dependency "bundler", "~> 1.6"
28
+ spec.add_development_dependency "rake"
29
+ spec.add_development_dependency "mocha"
30
+ spec.add_development_dependency "logger-better"
31
+ spec.add_development_dependency "benchmark-ips"
32
+ end
metadata ADDED
@@ -0,0 +1,218 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: thrift_server
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.1.0
5
+ platform: ruby
6
+ authors:
7
+ - ahawkins
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+ date: 2014-12-13 00:00:00.000000000 Z
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: statsd-ruby
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - ">="
18
+ - !ruby/object:Gem::Version
19
+ version: '0'
20
+ type: :runtime
21
+ prerelease: false
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - ">="
25
+ - !ruby/object:Gem::Version
26
+ version: '0'
27
+ - !ruby/object:Gem::Dependency
28
+ name: middleware
29
+ requirement: !ruby/object:Gem::Requirement
30
+ requirements:
31
+ - - ">="
32
+ - !ruby/object:Gem::Version
33
+ version: '0'
34
+ type: :runtime
35
+ prerelease: false
36
+ version_requirements: !ruby/object:Gem::Requirement
37
+ requirements:
38
+ - - ">="
39
+ - !ruby/object:Gem::Version
40
+ version: '0'
41
+ - !ruby/object:Gem::Dependency
42
+ name: concord
43
+ requirement: !ruby/object:Gem::Requirement
44
+ requirements:
45
+ - - ">="
46
+ - !ruby/object:Gem::Version
47
+ version: '0'
48
+ type: :runtime
49
+ prerelease: false
50
+ version_requirements: !ruby/object:Gem::Requirement
51
+ requirements:
52
+ - - ">="
53
+ - !ruby/object:Gem::Version
54
+ version: '0'
55
+ - !ruby/object:Gem::Dependency
56
+ name: honeybadger
57
+ requirement: !ruby/object:Gem::Requirement
58
+ requirements:
59
+ - - ">="
60
+ - !ruby/object:Gem::Version
61
+ version: '0'
62
+ type: :runtime
63
+ prerelease: false
64
+ version_requirements: !ruby/object:Gem::Requirement
65
+ requirements:
66
+ - - ">="
67
+ - !ruby/object:Gem::Version
68
+ version: '0'
69
+ - !ruby/object:Gem::Dependency
70
+ name: thrift
71
+ requirement: !ruby/object:Gem::Requirement
72
+ requirements:
73
+ - - ">="
74
+ - !ruby/object:Gem::Version
75
+ version: '0'
76
+ type: :runtime
77
+ prerelease: false
78
+ version_requirements: !ruby/object:Gem::Requirement
79
+ requirements:
80
+ - - ">="
81
+ - !ruby/object:Gem::Version
82
+ version: '0'
83
+ - !ruby/object:Gem::Dependency
84
+ name: bundler
85
+ requirement: !ruby/object:Gem::Requirement
86
+ requirements:
87
+ - - "~>"
88
+ - !ruby/object:Gem::Version
89
+ version: '1.6'
90
+ type: :development
91
+ prerelease: false
92
+ version_requirements: !ruby/object:Gem::Requirement
93
+ requirements:
94
+ - - "~>"
95
+ - !ruby/object:Gem::Version
96
+ version: '1.6'
97
+ - !ruby/object:Gem::Dependency
98
+ name: rake
99
+ requirement: !ruby/object:Gem::Requirement
100
+ requirements:
101
+ - - ">="
102
+ - !ruby/object:Gem::Version
103
+ version: '0'
104
+ type: :development
105
+ prerelease: false
106
+ version_requirements: !ruby/object:Gem::Requirement
107
+ requirements:
108
+ - - ">="
109
+ - !ruby/object:Gem::Version
110
+ version: '0'
111
+ - !ruby/object:Gem::Dependency
112
+ name: mocha
113
+ requirement: !ruby/object:Gem::Requirement
114
+ requirements:
115
+ - - ">="
116
+ - !ruby/object:Gem::Version
117
+ version: '0'
118
+ type: :development
119
+ prerelease: false
120
+ version_requirements: !ruby/object:Gem::Requirement
121
+ requirements:
122
+ - - ">="
123
+ - !ruby/object:Gem::Version
124
+ version: '0'
125
+ - !ruby/object:Gem::Dependency
126
+ name: logger-better
127
+ requirement: !ruby/object:Gem::Requirement
128
+ requirements:
129
+ - - ">="
130
+ - !ruby/object:Gem::Version
131
+ version: '0'
132
+ type: :development
133
+ prerelease: false
134
+ version_requirements: !ruby/object:Gem::Requirement
135
+ requirements:
136
+ - - ">="
137
+ - !ruby/object:Gem::Version
138
+ version: '0'
139
+ - !ruby/object:Gem::Dependency
140
+ name: benchmark-ips
141
+ requirement: !ruby/object:Gem::Requirement
142
+ requirements:
143
+ - - ">="
144
+ - !ruby/object:Gem::Version
145
+ version: '0'
146
+ type: :development
147
+ prerelease: false
148
+ version_requirements: !ruby/object:Gem::Requirement
149
+ requirements:
150
+ - - ">="
151
+ - !ruby/object:Gem::Version
152
+ version: '0'
153
+ description: ''
154
+ email:
155
+ - adam@saltside.se
156
+ executables: []
157
+ extensions: []
158
+ extra_rdoc_files: []
159
+ files:
160
+ - ".dockerignore"
161
+ - ".gitignore"
162
+ - Dockerfile
163
+ - Gemfile
164
+ - LICENSE.txt
165
+ - Makefile
166
+ - README.md
167
+ - Rakefile
168
+ - Vagrantfile
169
+ - benchmark/server.rb
170
+ - echo_client.rb
171
+ - echo_server.rb
172
+ - echo_service.thrift
173
+ - lib/thrift_server.rb
174
+ - lib/thrift_server/error_tracking_middleware.rb
175
+ - lib/thrift_server/honeybadger_error_tracker.rb
176
+ - lib/thrift_server/logging_middleware.rb
177
+ - lib/thrift_server/metrics_middleware.rb
178
+ - lib/thrift_server/version.rb
179
+ - script/buildbox/ci
180
+ - script/buildbox/step_failed
181
+ - test/acceptance_test.rb
182
+ - test/error_tracking_middleware_test.rb
183
+ - test/honeybadger_error_tracking_test.rb
184
+ - test/logging_middleware_test.rb
185
+ - test/metrics_middleware_test.rb
186
+ - test/test_helper.rb
187
+ - thrift_server.gemspec
188
+ homepage: https://github.com/saltside/thrift_server-ruby
189
+ licenses:
190
+ - MIT
191
+ metadata: {}
192
+ post_install_message:
193
+ rdoc_options: []
194
+ require_paths:
195
+ - lib
196
+ required_ruby_version: !ruby/object:Gem::Requirement
197
+ requirements:
198
+ - - ">="
199
+ - !ruby/object:Gem::Version
200
+ version: '0'
201
+ required_rubygems_version: !ruby/object:Gem::Requirement
202
+ requirements:
203
+ - - ">="
204
+ - !ruby/object:Gem::Version
205
+ version: '0'
206
+ requirements: []
207
+ rubyforge_project:
208
+ rubygems_version: 2.2.2
209
+ signing_key:
210
+ specification_version: 4
211
+ summary: Encapsulate error handling, logging, and metrics for thrift servers
212
+ test_files:
213
+ - test/acceptance_test.rb
214
+ - test/error_tracking_middleware_test.rb
215
+ - test/honeybadger_error_tracking_test.rb
216
+ - test/logging_middleware_test.rb
217
+ - test/metrics_middleware_test.rb
218
+ - test/test_helper.rb