thrift_server 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 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