thrift_server 0.1.1 → 1.0.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.
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