thrift_server 0.1.1 → 1.0.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (42) hide show
  1. checksums.yaml +4 -4
  2. data/Dockerfile +2 -0
  3. data/Makefile +27 -14
  4. data/README.md +142 -43
  5. data/circle.yml +14 -0
  6. data/echo_client.rb +22 -3
  7. data/echo_server.rb +23 -16
  8. data/echo_service.thrift +11 -0
  9. data/lib/thrift_server.rb +112 -36
  10. data/lib/thrift_server/instrumentation_middleware.rb +33 -0
  11. data/lib/thrift_server/log_subscriber.rb +60 -0
  12. data/lib/thrift_server/publisher.rb +25 -0
  13. data/lib/thrift_server/rpc_metrics_subscriber.rb +25 -0
  14. data/lib/thrift_server/server_metrics_subscriber.rb +32 -0
  15. data/lib/thrift_server/thread_pool_server.rb +115 -0
  16. data/lib/thrift_server/threaded_server.rb +87 -0
  17. data/lib/thrift_server/validation_middleware.rb +14 -0
  18. data/lib/thrift_server/version.rb +2 -2
  19. data/script/circleci/cache-image +10 -0
  20. data/test/log_subscriber_test.rb +86 -0
  21. data/test/processor_test.rb +189 -0
  22. data/test/rpc_metrics_subscriber_test.rb +44 -0
  23. data/test/server_metrics_subscriber_test.rb +51 -0
  24. data/test/support/log_yielder.rb +32 -0
  25. data/test/support/server_tests.rb +87 -0
  26. data/test/test_helper.rb +32 -23
  27. data/test/thread_pool_server_test.rb +101 -0
  28. data/test/threaded_server_test.rb +56 -0
  29. data/test/validation_middleware_test.rb +71 -0
  30. data/thrift_server.gemspec +2 -2
  31. metadata +34 -22
  32. data/lib/thrift_server/error_tracking_middleware.rb +0 -12
  33. data/lib/thrift_server/honeybadger_error_tracker.rb +0 -11
  34. data/lib/thrift_server/logging_middleware.rb +0 -13
  35. data/lib/thrift_server/metrics_middleware.rb +0 -16
  36. data/script/buildbox/ci +0 -5
  37. data/script/buildbox/step_failed +0 -5
  38. data/test/acceptance_test.rb +0 -225
  39. data/test/error_tracking_middleware_test.rb +0 -25
  40. data/test/honeybadger_error_tracking_test.rb +0 -12
  41. data/test/logging_middleware_test.rb +0 -42
  42. data/test/metrics_middleware_test.rb +0 -70
@@ -0,0 +1,33 @@
1
+ module ThriftServer
2
+ class InstrumentationMiddleware
3
+ extend Forwardable
4
+
5
+ def_delegators :publisher, :publish
6
+
7
+ include Concord.new(:app, :publisher)
8
+
9
+ def call(rpc)
10
+ start_time = Time.now
11
+
12
+ publish :rpc_incoming, rpc
13
+
14
+ app.call(rpc).tap do |response|
15
+ latency = (Time.now - start_time) * 1000
16
+
17
+ publish :rpc_ok, rpc, response, {
18
+ latency: latency
19
+ }
20
+ end
21
+ rescue => ex
22
+ latency = (Time.now - start_time) * 1000
23
+
24
+ if rpc.protocol_exception? ex
25
+ publish :rpc_exception, rpc, ex, latency: latency
26
+ else
27
+ publish :rpc_error, rpc, ex, latency: latency
28
+ end
29
+
30
+ raise ex
31
+ end
32
+ end
33
+ end
@@ -0,0 +1,60 @@
1
+ module ThriftServer
2
+ class LogSubscriber
3
+ include Concord.new(:logger)
4
+
5
+ def thread_pool_server_pool_change(meta)
6
+ logger.debug :server do
7
+ "Thread pool change: %+d" % [ meta.fetch(:delta) ]
8
+ end
9
+ end
10
+
11
+ def server_connection_opened(addr)
12
+ logger.debug :server do
13
+ "%s:%d connected" % [
14
+ addr.ip_address,
15
+ addr.ip_port
16
+ ]
17
+ end
18
+ end
19
+
20
+ def server_connection_closed(addr)
21
+ logger.debug :server do
22
+ "%s:%d disconnected" % [
23
+ addr.ip_address,
24
+ addr.ip_port
25
+ ]
26
+ end
27
+ end
28
+
29
+ def rpc_ok(rpc, response, meta)
30
+ logger.info :processor do
31
+ "%s => OK (%.2fms)" % [
32
+ rpc.name,
33
+ meta.fetch(:latency)
34
+ ]
35
+ end
36
+ end
37
+
38
+ def rpc_error(rpc, ex, meta)
39
+ logger.info :processor do
40
+ "%s => Error! %s (%.2fms)" % [
41
+ rpc.name,
42
+ ex.class.name,
43
+ meta.fetch(:latency)
44
+ ]
45
+ end
46
+
47
+ logger.error ex
48
+ end
49
+
50
+ def rpc_exception(rpc, ex, meta)
51
+ logger.info :processor do
52
+ "%s => %s (%.2fms)" % [
53
+ rpc.name,
54
+ rpc.exception_name(ex),
55
+ meta.fetch(:latency)
56
+ ]
57
+ end
58
+ end
59
+ end
60
+ end
@@ -0,0 +1,25 @@
1
+ module ThriftServer
2
+ class Publisher
3
+ include Enumerable
4
+
5
+ extend Forwardable
6
+
7
+ def_delegators :listeners, :each
8
+
9
+ attr_reader :listeners
10
+
11
+ def initialize
12
+ @listeners = [ ]
13
+ end
14
+
15
+ def subscribe(object)
16
+ listeners << object
17
+ end
18
+
19
+ def publish(event, *args)
20
+ listeners.each do |listener|
21
+ listener.send(event, *args) if listener.respond_to? event
22
+ end
23
+ end
24
+ end
25
+ end
@@ -0,0 +1,25 @@
1
+ module ThriftServer
2
+ class RpcMetricsSubscriber
3
+ include Concord.new(:statsd)
4
+
5
+ def rpc_incoming(rpc)
6
+ statsd.increment "rpc.#{rpc}.incoming"
7
+ end
8
+
9
+ def rpc_ok(rpc, response, meta)
10
+ statsd.increment "rpc.#{rpc}.success"
11
+ statsd.timing "rpc.#{rpc}.latency", meta.fetch(:latency)
12
+ end
13
+
14
+ def rpc_exception(rpc, ex, meta)
15
+ statsd.increment "rpc.#{rpc}.exception"
16
+ statsd.increment "rpc.#{rpc}.exception.#{rpc.exception_name(ex)}"
17
+ statsd.timing "rpc.#{rpc}.latency", meta.fetch(:latency)
18
+ end
19
+
20
+ def rpc_error(rpc, ex, meta)
21
+ statsd.increment "rpc.#{rpc}.error"
22
+ statsd.timing "rpc.#{rpc}.latency", meta.fetch(:latency)
23
+ end
24
+ end
25
+ end
@@ -0,0 +1,32 @@
1
+ module ThriftServer
2
+ class ServerMetricsSubscriber
3
+ include Concord.new(:statsd)
4
+
5
+ def server_connection_opened(*)
6
+ statsd.gauge 'server.connection.active', '+1'
7
+ end
8
+
9
+ def server_connection_closed(*)
10
+ statsd.gauge 'server.connection.active', '-1'
11
+ end
12
+
13
+ def rpc_incoming(rpc)
14
+ statsd.increment 'rpc.incoming'
15
+ end
16
+
17
+ def rpc_ok(rpc, response, meta)
18
+ statsd.increment 'rpc.success'
19
+ statsd.timing 'rpc.latency', meta.fetch(:latency)
20
+ end
21
+
22
+ def rpc_exception(rpc, ex, meta)
23
+ statsd.increment 'rpc.exception'
24
+ statsd.timing 'rpc.latency', meta.fetch(:latency)
25
+ end
26
+
27
+ def rpc_error(rpc, ex, meta)
28
+ statsd.increment 'rpc.error'
29
+ statsd.timing 'rpc.latency', meta.fetch(:latency)
30
+ end
31
+ end
32
+ end
@@ -0,0 +1,115 @@
1
+ module ThriftServer
2
+ class ThreadPoolServer < Thrift::ThreadPoolServer
3
+ class LogSubscriber
4
+ include Concord.new(:logger)
5
+
6
+ def server_start(server)
7
+ logger.info :server do
8
+ "Started on port %d" % [ server.port ]
9
+ end
10
+
11
+ logger.info :server do
12
+ "-> Threads: %d" % [ server.threads ]
13
+ end
14
+
15
+ logger.info :server do
16
+ "-> Transport: %s" % [ server.transport ]
17
+ end
18
+
19
+ logger.info :server do
20
+ "-> Protocol: %s" % [ server.protocol ]
21
+ end
22
+ end
23
+ end
24
+
25
+ class MetricsSubscriber
26
+ include Concord.new(:statsd)
27
+
28
+ def thread_pool_server_pool_change(meta)
29
+ statsd.gauge('server.pool.size', '%+d' % [ meta.fetch(:delta) ])
30
+ end
31
+ end
32
+
33
+ extend Forwardable
34
+
35
+ def_delegators :@processor, :use
36
+ def_delegators :@processor, :publisher, :publish, :subscribe
37
+
38
+ attr_accessor :port
39
+
40
+ def log(logger)
41
+ subscribe LogSubscriber.new(logger)
42
+ subscribe ThriftServer::LogSubscriber.new(logger)
43
+ end
44
+
45
+ def metrics(statsd)
46
+ subscribe MetricsSubscriber.new(statsd)
47
+ subscribe ServerMetricsSubscriber.new(statsd)
48
+ subscribe RpcMetricsSubscriber.new(statsd)
49
+ end
50
+
51
+ def threads
52
+ @thread_q.max
53
+ end
54
+
55
+ def protocol
56
+ @protocol_factory
57
+ end
58
+
59
+ def transport
60
+ @transport_factory
61
+ end
62
+
63
+ def server_transport
64
+ @server_transport
65
+ end
66
+
67
+ def start(dry_run: false)
68
+ publish :server_start, self
69
+
70
+ serve unless dry_run
71
+ end
72
+
73
+ # NOTE: this a direct copy of the upstream code with instrumentation added.
74
+ def serve
75
+ @server_transport.listen
76
+
77
+ begin
78
+ loop do
79
+ @thread_q.push(:token)
80
+ publish :thread_pool_server_pool_change, delta: 1
81
+
82
+ Thread.new do
83
+ begin
84
+ loop do
85
+ client = @server_transport.accept
86
+ remote_address = client.handle.remote_address
87
+
88
+ publish :server_connection_opened, remote_address
89
+
90
+ trans = @transport_factory.get_transport(client)
91
+ prot = @protocol_factory.get_protocol(trans)
92
+ begin
93
+ loop do
94
+ @processor.process(prot, prot)
95
+ end
96
+ rescue Thrift::TransportException, Thrift::ProtocolException => e
97
+ publish :server_connection_closed, remote_address
98
+ ensure
99
+ trans.close
100
+ end
101
+ end
102
+ rescue => e
103
+ @exception_q.push(e)
104
+ ensure
105
+ publish :thread_pool_server_pool_change, delta: -1
106
+ @thread_q.pop # thread died!
107
+ end
108
+ end
109
+ end
110
+ ensure
111
+ @server_transport.close
112
+ end
113
+ end
114
+ end
115
+ end
@@ -0,0 +1,87 @@
1
+ module ThriftServer
2
+ class ThreadedServer < Thrift::ThreadedServer
3
+ class LogSubscriber
4
+ include Concord.new(:logger)
5
+
6
+ def server_start(server)
7
+ logger.info :server do
8
+ "Started on port %d" % [ server.port ]
9
+ end
10
+
11
+ logger.info :server do
12
+ "-> Transport: %s" % [ server.transport ]
13
+ end
14
+
15
+ logger.info :server do
16
+ "-> Protocol: %s" % [ server.protocol ]
17
+ end
18
+ end
19
+ end
20
+
21
+ extend Forwardable
22
+
23
+ def_delegators :@processor, :use
24
+ def_delegators :@processor, :publisher, :publish, :subscribe
25
+
26
+ attr_accessor :port
27
+
28
+ def log(logger)
29
+ subscribe LogSubscriber.new(logger)
30
+ subscribe ThriftServer::LogSubscriber.new(logger)
31
+ end
32
+
33
+ def metrics(statsd)
34
+ subscribe ServerMetricsSubscriber.new(statsd)
35
+ subscribe RpcMetricsSubscriber.new(statsd)
36
+ end
37
+
38
+ def protocol
39
+ @protocol_factory
40
+ end
41
+
42
+ def transport
43
+ @transport_factory
44
+ end
45
+
46
+ def server_transport
47
+ @server_transport
48
+ end
49
+
50
+ def start(dry_run: false)
51
+ publish :server_start, self
52
+
53
+ serve unless dry_run
54
+ end
55
+
56
+ # NOTE: this is a copy of the upstream code with instrumentation added.
57
+ def serve
58
+ begin
59
+ @server_transport.listen
60
+ loop do
61
+ client = @server_transport.accept
62
+
63
+ remote_address = client.handle.remote_address
64
+ publish :server_connection_opened, remote_address
65
+
66
+ trans = @transport_factory.get_transport(client)
67
+ prot = @protocol_factory.get_protocol(trans)
68
+
69
+ Thread.new(prot, trans) do |p, t|
70
+ begin
71
+ loop do
72
+ @processor.process(p, p)
73
+ end
74
+ rescue Thrift::TransportException, Thrift::ProtocolException
75
+ ensure
76
+ publish :server_connection_closed, remote_address
77
+
78
+ t.close
79
+ end
80
+ end
81
+ end
82
+ ensure
83
+ @server_transport.close
84
+ end
85
+ end
86
+ end
87
+ end
@@ -0,0 +1,14 @@
1
+ module ThriftServer
2
+ class ValidationMiddleware
3
+ include Concord.new(:app)
4
+
5
+ def call(rpc)
6
+ app.call(rpc).tap do |response|
7
+ case response
8
+ when Thrift::Struct, Thrift::Struct_Union
9
+ Thrift::Validator.new.validate response
10
+ end
11
+ end
12
+ end
13
+ end
14
+ end
@@ -1,3 +1,3 @@
1
- class ThriftServer
2
- VERSION = "0.1.1"
1
+ module ThriftServer
2
+ VERSION = "1.0.0"
3
3
  end
@@ -0,0 +1,10 @@
1
+ #!/usr/bin/env bash
2
+
3
+ set -eou pipefail
4
+
5
+ if [ ! -f "${1}.tar.gz" ]; then
6
+ docker pull "$2"
7
+ docker save "$2" | gzip --fast > "${1}.tar.gz"
8
+ else
9
+ gunzip -c "${1}.tar.gz" | docker load
10
+ fi
@@ -0,0 +1,86 @@
1
+ require_relative 'test_helper'
2
+
3
+ class LogSubscriberTest < MiniTest::Unit::TestCase
4
+ attr_reader :logger, :subscriber, :rpc
5
+
6
+ def setup
7
+ @logger = mock
8
+ @subscriber = ThriftServer::LogSubscriber.new LogYielder.new(logger)
9
+
10
+ @rpc = ThriftServer::RPC.new :foo, :bar
11
+ end
12
+
13
+ def test_thread_pool_server_pool_change_with_positive_delta
14
+ logger.expects(:debug).with do |line|
15
+ line =~ /\+1/
16
+ end
17
+
18
+ subscriber.thread_pool_server_pool_change delta: 1
19
+ end
20
+
21
+ def test_thread_pool_server_pool_change_with_negative_delta
22
+ logger.expects(:debug).with do |line|
23
+ line =~ /-1/
24
+ end
25
+
26
+ subscriber.thread_pool_server_pool_change delta: -1
27
+ end
28
+
29
+ def test_server_connection_opened
30
+ addr = stub ip_address: 'stub_ip', ip_port: 823
31
+
32
+ logger.expects(:debug).with do |line|
33
+ line =~ /stub_ip/ && line =~ /823/
34
+ end
35
+
36
+ subscriber.server_connection_opened addr
37
+ end
38
+
39
+ def test_server_connection_closed
40
+ addr = stub ip_address: 'stub_ip', ip_port: 823
41
+
42
+ logger.expects(:debug).with do |line|
43
+ line =~ /stub_ip/ && line =~ /823/
44
+ end
45
+
46
+ subscriber.server_connection_closed addr
47
+ end
48
+
49
+ def test_rpc_ok_logs_result_to_info
50
+ logger.expects(:info).with do |line|
51
+ assert_match /foo/, line, 'RPC name not printed'
52
+ assert_match /OK/, line
53
+ assert_match /5.*ms/, line, 'Timing not printed'
54
+ end
55
+
56
+ subscriber.rpc_ok rpc, :response, latency: 5
57
+ end
58
+
59
+ def test_rpc_error_prints_result_and_trace
60
+ error = StandardError.new
61
+
62
+ logger.expects(:info).with do |line|
63
+ assert_match /foo/, line, 'RPC name not printed'
64
+ refute_match /OK/, line
65
+ assert_match /StandardError/, line
66
+ assert_match /5.*ms/, line, 'Timing not printed'
67
+ end
68
+
69
+ logger.expects(:error).with(error)
70
+
71
+ subscriber.rpc_error rpc, error, latency: 5
72
+ end
73
+
74
+ def test_rpc_exception_prints_protocol_name
75
+ rpc.exceptions = { exName: TestException }
76
+
77
+ logger.expects(:info).with do |line|
78
+ assert_match /foo/, line, 'RPC name not printed'
79
+ refute_match /OK/, line
80
+ assert_match /exName/, line
81
+ assert_match /5.*ms/, line, 'Timing not printed'
82
+ end
83
+
84
+ subscriber.rpc_exception rpc, TestException.new(:message), latency: 5
85
+ end
86
+ end