tonic-rails 0.2.0 → 0.4.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 +4 -4
- data/CHANGELOG.md +44 -1
- data/Cargo.lock +35 -1
- data/README.md +145 -2
- data/ext/tonic_rails_native/Cargo.toml +3 -1
- data/ext/tonic_rails_native/src/dispatch.rs +23 -1
- data/ext/tonic_rails_native/src/gvl.rs +22 -0
- data/ext/tonic_rails_native/src/lib.rs +2 -0
- data/ext/tonic_rails_native/src/server.rs +121 -1
- data/lib/generators/tonic_rails/install/install_generator.rb +45 -0
- data/lib/generators/tonic_rails/install/templates/tonic_rails.rb.tt +21 -0
- data/lib/generators/tonic_rails/service/service_generator.rb +110 -0
- data/lib/generators/tonic_rails/service/templates/handler.rb.tt +38 -0
- data/lib/generators/tonic_rails/service/templates/service.proto.tt +16 -0
- data/lib/generators/tonic_rails/service/templates/service_spec.rb.tt +17 -0
- data/lib/generators/tonic_rails/service/templates/service_test.rb.tt +14 -0
- data/lib/tonic/rails/base.rb +104 -5
- data/lib/tonic/rails/configuration.rb +58 -3
- data/lib/tonic/rails/health.rb +105 -0
- data/lib/tonic/rails/peer.rb +60 -0
- data/lib/tonic/rails/router.rb +1 -0
- data/lib/tonic/rails/server.rb +50 -5
- data/lib/tonic/rails/tasks/tonic-rails.rake +13 -1
- data/lib/tonic/rails/testing/errors.rb +47 -0
- data/lib/tonic/rails/testing/minitest.rb +44 -0
- data/lib/tonic/rails/testing/rspec.rb +53 -0
- data/lib/tonic/rails/testing.rb +5 -3
- data/lib/tonic/rails/version.rb +1 -1
- data/lib/tonic-rails.rb +2 -0
- metadata +13 -1
data/lib/tonic/rails/server.rb
CHANGED
|
@@ -22,6 +22,7 @@ module Tonic
|
|
|
22
22
|
@state = :stopped
|
|
23
23
|
@stop_signal = nil
|
|
24
24
|
@stop_requested = false
|
|
25
|
+
@health = Health.new(configuration, @router)
|
|
25
26
|
@lock = Mutex.new
|
|
26
27
|
end
|
|
27
28
|
|
|
@@ -58,6 +59,7 @@ module Tonic
|
|
|
58
59
|
@native = build_native_server
|
|
59
60
|
@native.start_server
|
|
60
61
|
log_pump = spawn_log_pump
|
|
62
|
+
@health.attach(@native)
|
|
61
63
|
@state = :running
|
|
62
64
|
|
|
63
65
|
request_log = subscribe_request_logger
|
|
@@ -67,10 +69,12 @@ module Tonic
|
|
|
67
69
|
yield self if block_given?
|
|
68
70
|
|
|
69
71
|
wait_for_stop(workers)
|
|
72
|
+
@health.detach
|
|
70
73
|
shutdown(workers)
|
|
71
74
|
ensure
|
|
72
75
|
ActiveSupport::Notifications.unsubscribe(request_log) if request_log
|
|
73
76
|
previous_handlers&.each { |signal, handler| Signal.trap(signal, handler || "DEFAULT") }
|
|
77
|
+
@health.detach
|
|
74
78
|
@native&.stop_server
|
|
75
79
|
stop_log_pump(log_pump)
|
|
76
80
|
@native = nil
|
|
@@ -91,6 +95,25 @@ module Tonic
|
|
|
91
95
|
@state == :running
|
|
92
96
|
end
|
|
93
97
|
|
|
98
|
+
# Mark the whole server, or the given services (fully-qualified
|
|
99
|
+
# names), SERVING in grpc.health.v1. Takes effect immediately when
|
|
100
|
+
# running, otherwise when the server starts.
|
|
101
|
+
#
|
|
102
|
+
# With `config.health_check`, a service reports SERVING only while the
|
|
103
|
+
# check passes *and* it's marked serving here.
|
|
104
|
+
def serving!(*services)
|
|
105
|
+
@health.mark(services, true)
|
|
106
|
+
self
|
|
107
|
+
end
|
|
108
|
+
|
|
109
|
+
# Mark the whole server, or the given services, NOT_SERVING (e.g. for
|
|
110
|
+
# maintenance), so load balancers and readiness probes route around
|
|
111
|
+
# this instance. Calls are still served.
|
|
112
|
+
def not_serving!(*services)
|
|
113
|
+
@health.mark(services, false)
|
|
114
|
+
self
|
|
115
|
+
end
|
|
116
|
+
|
|
94
117
|
# @return [Hash{Symbol=>Integer}] request and connection counters
|
|
95
118
|
def stats
|
|
96
119
|
native = @native
|
|
@@ -107,19 +130,30 @@ module Tonic
|
|
|
107
130
|
native = TonicRails::Native::Server.new(config.bind_address, config.max_message_size)
|
|
108
131
|
native.on_request(build_callback)
|
|
109
132
|
native.set_routes(@router.paths)
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
133
|
+
apply_connection_settings(native, config)
|
|
134
|
+
apply_features(native, config)
|
|
135
|
+
native
|
|
136
|
+
end
|
|
137
|
+
|
|
138
|
+
def apply_connection_settings(native, config)
|
|
114
139
|
native.set_max_connections(config.max_connections)
|
|
115
140
|
native.set_concurrency_limit_per_connection(config.concurrency_limit_per_connection)
|
|
116
141
|
native.set_worker_threads(config.worker_threads || 0)
|
|
117
142
|
native.set_keepalive(config.keepalive_interval.to_f, config.keepalive_timeout.to_f)
|
|
118
143
|
native.set_dispatch_queue_size(config.dispatch_queue_size)
|
|
144
|
+
end
|
|
145
|
+
|
|
146
|
+
def apply_features(native, config)
|
|
147
|
+
if config.tls?
|
|
148
|
+
native.set_tls(config.tls_cert_path, config.tls_key_path)
|
|
149
|
+
native.set_tls_client_ca(config.tls_client_ca_path) if config.tls_client_ca_path
|
|
150
|
+
end
|
|
119
151
|
native.set_compression(config.compression ? true : false)
|
|
120
152
|
native.set_reflection(config.reflection ? true : false)
|
|
121
153
|
native.set_reflection_descriptors(Reflection.file_descriptors(@router.service_names)) if config.reflection
|
|
122
|
-
|
|
154
|
+
return unless config.grpc_web
|
|
155
|
+
|
|
156
|
+
native.set_grpc_web(config.grpc_web_origins, config.grpc_web_allowed_headers, config.grpc_web_exposed_headers)
|
|
123
157
|
end
|
|
124
158
|
|
|
125
159
|
# The proc the transport calls for every RPC:
|
|
@@ -250,6 +284,16 @@ module Tonic
|
|
|
250
284
|
logger&.info("tonic-rails: stopped")
|
|
251
285
|
end
|
|
252
286
|
|
|
287
|
+
def log_grpc_web(logger, config)
|
|
288
|
+
return unless config.grpc_web
|
|
289
|
+
|
|
290
|
+
origins = config.grpc_web_origins
|
|
291
|
+
logger.info("tonic-rails: gRPC-Web enabled (#{origins.empty? ? 'same-origin only' : "origins: #{origins.join(', ')}"})")
|
|
292
|
+
return unless origins.include?("*")
|
|
293
|
+
|
|
294
|
+
logger.warn("tonic-rails: gRPC-Web allows any origin; any website can call these services from a visitor's browser")
|
|
295
|
+
end
|
|
296
|
+
|
|
253
297
|
def log_startup
|
|
254
298
|
config = @configuration
|
|
255
299
|
logger = config.logger
|
|
@@ -261,6 +305,7 @@ module Tonic
|
|
|
261
305
|
logger.warn("tonic-rails: TLS is not configured; gRPC traffic is plaintext")
|
|
262
306
|
end
|
|
263
307
|
logger.info("tonic-rails: reflection enabled") if config.reflection
|
|
308
|
+
log_grpc_web(logger, config)
|
|
264
309
|
logger.info(
|
|
265
310
|
"tonic-rails: serving #{@router.service_names.join(', ')} on #{config.bind_address} " \
|
|
266
311
|
"(#{config.dispatch_threads} dispatch threads)",
|
|
@@ -27,7 +27,19 @@ namespace :tonic_rails do
|
|
|
27
27
|
ruby_out = ENV.fetch("RUBY_OUT", "lib/grpc")
|
|
28
28
|
protos = Dir.glob(File.join(proto_dir, "**", "*.proto"))
|
|
29
29
|
abort "No .proto files found in #{proto_dir}/" if protos.empty?
|
|
30
|
-
|
|
30
|
+
version = begin
|
|
31
|
+
IO.popen(%w[protoc --version], err: File::NULL, &:read)[/\d+(\.\d+)+/]
|
|
32
|
+
rescue SystemCallError
|
|
33
|
+
nil
|
|
34
|
+
end
|
|
35
|
+
abort "protoc not found on PATH. Install the Protocol Buffers compiler (22.0 or newer)." unless version
|
|
36
|
+
# protoc before 22 emits the Ruby DSL that google-protobuf 4 removed
|
|
37
|
+
# (`undefined method 'build' for DescriptorPool` at load time).
|
|
38
|
+
major = version.split(".").then { |parts| parts.first == "3" ? parts[1].to_i : parts.first.to_i }
|
|
39
|
+
if major < 22
|
|
40
|
+
abort "protoc #{version} is too old for google-protobuf 4: install protoc 22.0 or newer " \
|
|
41
|
+
"(distribution packages are often older; see https://github.com/protocolbuffers/protobuf/releases)."
|
|
42
|
+
end
|
|
31
43
|
|
|
32
44
|
FileUtils.mkdir_p(ruby_out)
|
|
33
45
|
sh "protoc", "--ruby_out=#{ruby_out}", "-I#{proto_dir}", *protos
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Tonic
|
|
4
|
+
module Rails
|
|
5
|
+
module Testing
|
|
6
|
+
# Shared helpers for the RSpec matcher and Minitest assertions.
|
|
7
|
+
# @api private
|
|
8
|
+
module Errors
|
|
9
|
+
module_function
|
|
10
|
+
|
|
11
|
+
# Exceptions that carry a gRPC status: tonic-rails' own, and the
|
|
12
|
+
# official grpc gem client's, when it's loaded.
|
|
13
|
+
def classes
|
|
14
|
+
[Tonic::Rails::RpcError, (GRPC::BadStatus if defined?(GRPC::BadStatus))].compact
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
def status?(error)
|
|
18
|
+
classes.any? { |klass| error.is_a?(klass) }
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
# @return [Integer]
|
|
22
|
+
def code(error)
|
|
23
|
+
error.code
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
# @return [String] the status message the client sees. The grpc
|
|
27
|
+
# gem keeps it in #details (its #message adds the code).
|
|
28
|
+
def message(error)
|
|
29
|
+
text = defined?(GRPC::BadStatus) && error.is_a?(GRPC::BadStatus) ? error.details.to_s : error.message
|
|
30
|
+
text.dup.force_encoding(Encoding::UTF_8)
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
def code_name(code)
|
|
34
|
+
Tonic::Rails::RpcError::CODE_NAMES.fetch(code, code)
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
def message_matches?(expected, actual)
|
|
38
|
+
expected.is_a?(Regexp) ? expected.match?(actual) : expected.to_s == actual
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
def describe(error)
|
|
42
|
+
"#{code_name(code(error)).to_s.upcase} (#{message(error).inspect})"
|
|
43
|
+
end
|
|
44
|
+
end
|
|
45
|
+
end
|
|
46
|
+
end
|
|
47
|
+
end
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "tonic/rails/testing"
|
|
4
|
+
require "tonic/rails/testing/errors"
|
|
5
|
+
|
|
6
|
+
module Tonic
|
|
7
|
+
module Rails
|
|
8
|
+
module Testing
|
|
9
|
+
# Minitest assertions for gRPC statuses:
|
|
10
|
+
#
|
|
11
|
+
# class ItemServiceTest < ActiveSupport::TestCase
|
|
12
|
+
# include Tonic::Rails::Testing::Assertions
|
|
13
|
+
#
|
|
14
|
+
# test "missing items" do
|
|
15
|
+
# error = assert_grpc_error(:not_found) { Tonic::Rails::Testing.call(ItemService, :GetItem, { id: 0 }) }
|
|
16
|
+
# assert_match(/0/, error.message)
|
|
17
|
+
# end
|
|
18
|
+
# end
|
|
19
|
+
module Assertions
|
|
20
|
+
# Asserts the block raises a gRPC status (Tonic::Rails::RpcError or
|
|
21
|
+
# GRPC::BadStatus) with the given code and, optionally, message
|
|
22
|
+
# (String or Regexp). Returns the error.
|
|
23
|
+
def assert_grpc_error(code = nil, message = nil, &)
|
|
24
|
+
errors = Testing::Errors
|
|
25
|
+
error = assert_raises(*errors.classes, &)
|
|
26
|
+
if code
|
|
27
|
+
expected = Tonic::Rails::RpcError.normalize_code(code)
|
|
28
|
+
assert_equal(
|
|
29
|
+
errors.code_name(expected), errors.code_name(errors.code(error)),
|
|
30
|
+
"Expected gRPC status #{errors.code_name(expected).to_s.upcase}, got #{errors.describe(error)}"
|
|
31
|
+
)
|
|
32
|
+
end
|
|
33
|
+
if message
|
|
34
|
+
assert(
|
|
35
|
+
errors.message_matches?(message, errors.message(error)),
|
|
36
|
+
"Expected gRPC message #{message.inspect}, got #{errors.message(error).inspect}",
|
|
37
|
+
)
|
|
38
|
+
end
|
|
39
|
+
error
|
|
40
|
+
end
|
|
41
|
+
end
|
|
42
|
+
end
|
|
43
|
+
end
|
|
44
|
+
end
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "rspec/expectations"
|
|
4
|
+
require "tonic/rails/testing"
|
|
5
|
+
require "tonic/rails/testing/errors"
|
|
6
|
+
|
|
7
|
+
# Matches a block that ends with a gRPC status, raised as a
|
|
8
|
+
# Tonic::Rails::RpcError (Testing.call) or a GRPC::BadStatus (the official
|
|
9
|
+
# client):
|
|
10
|
+
#
|
|
11
|
+
# expect { Tonic::Rails::Testing.call(ItemService, :GetItem, { id: 0 }) }
|
|
12
|
+
# .to raise_grpc_error(:not_found)
|
|
13
|
+
# expect { stub.get_item(request) }.to raise_grpc_error(:invalid_argument).with_message(/name/)
|
|
14
|
+
# expect { ... }.to raise_grpc_error # any non-OK status
|
|
15
|
+
RSpec::Matchers.define :raise_grpc_error do |expected_code = nil|
|
|
16
|
+
errors = Tonic::Rails::Testing::Errors
|
|
17
|
+
|
|
18
|
+
supports_block_expectations
|
|
19
|
+
|
|
20
|
+
chain(:with_message) { |message| @expected_message = message }
|
|
21
|
+
|
|
22
|
+
match do |block|
|
|
23
|
+
@expected = expected_code && Tonic::Rails::RpcError.normalize_code(expected_code)
|
|
24
|
+
begin
|
|
25
|
+
block.call
|
|
26
|
+
@error = nil
|
|
27
|
+
rescue Exception => e # rubocop:disable Lint/RescueException
|
|
28
|
+
raise unless errors.status?(e)
|
|
29
|
+
|
|
30
|
+
@error = e
|
|
31
|
+
end
|
|
32
|
+
return false unless @error
|
|
33
|
+
|
|
34
|
+
(@expected.nil? || errors.code(@error) == @expected) &&
|
|
35
|
+
(!defined?(@expected_message) || errors.message_matches?(@expected_message, errors.message(@error)))
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
description do
|
|
39
|
+
text = +"raise a gRPC error"
|
|
40
|
+
text << " #{errors.code_name(@expected).to_s.upcase}" if @expected
|
|
41
|
+
text << " with message #{@expected_message.inspect}" if defined?(@expected_message)
|
|
42
|
+
text
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
failure_message do
|
|
46
|
+
actual = @error ? "got #{errors.describe(@error)}" : "nothing was raised"
|
|
47
|
+
"expected block to #{description}, but #{actual}"
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
failure_message_when_negated do
|
|
51
|
+
"expected block not to #{description}, but got #{errors.describe(@error)}"
|
|
52
|
+
end
|
|
53
|
+
end
|
data/lib/tonic/rails/testing.rb
CHANGED
|
@@ -65,11 +65,12 @@ module Tonic
|
|
|
65
65
|
# request; an Array of them for client- and bidi-streaming RPCs
|
|
66
66
|
# @param metadata [Hash{String=>String}]
|
|
67
67
|
# @param timeout_ms [Integer, nil] simulated client deadline
|
|
68
|
+
# @param peer [Tonic::Rails::Peer, nil] simulated client (address, certificates)
|
|
68
69
|
# @return [Google::Protobuf::AbstractMessage, Array] the response, or
|
|
69
70
|
# an Array of responses for server- and bidi-streaming RPCs
|
|
70
71
|
# @raise [RpcError] when the handler ends the call with a status
|
|
71
|
-
def call(service_class, rpc_name, request, metadata: {}, timeout_ms: nil)
|
|
72
|
-
invoke(service_class, rpc_name, request, metadata: metadata, timeout_ms: timeout_ms).response
|
|
72
|
+
def call(service_class, rpc_name, request, metadata: {}, timeout_ms: nil, peer: nil)
|
|
73
|
+
invoke(service_class, rpc_name, request, metadata: metadata, timeout_ms: timeout_ms, peer: peer).response
|
|
73
74
|
end
|
|
74
75
|
|
|
75
76
|
# Like #call, but returns a Result with the response metadata too:
|
|
@@ -80,7 +81,7 @@ module Tonic
|
|
|
80
81
|
# result.trailers["x-cost-ms"] # => "3"
|
|
81
82
|
#
|
|
82
83
|
# @return [Result]
|
|
83
|
-
def invoke(service_class, rpc_name, request, metadata: {}, timeout_ms: nil)
|
|
84
|
+
def invoke(service_class, rpc_name, request, metadata: {}, timeout_ms: nil, peer: nil)
|
|
84
85
|
spec = service_class.grpc_rpcs.fetch(rpc_name.to_s) do
|
|
85
86
|
raise ArgumentError, "#{service_class} has no RPC #{rpc_name}"
|
|
86
87
|
end
|
|
@@ -95,6 +96,7 @@ module Tonic
|
|
|
95
96
|
reader: reader,
|
|
96
97
|
sender: sender,
|
|
97
98
|
timeout_ms: timeout_ms,
|
|
99
|
+
peer: peer,
|
|
98
100
|
)
|
|
99
101
|
|
|
100
102
|
response = if spec[:response_stream]
|
data/lib/tonic/rails/version.rb
CHANGED
data/lib/tonic-rails.rb
CHANGED
|
@@ -11,11 +11,13 @@ end
|
|
|
11
11
|
|
|
12
12
|
require "tonic/rails/version"
|
|
13
13
|
require "tonic/rails/metadata"
|
|
14
|
+
require "tonic/rails/peer"
|
|
14
15
|
require "tonic/rails/rpc_error"
|
|
15
16
|
require "tonic/rails/configuration"
|
|
16
17
|
require "tonic/rails/router"
|
|
17
18
|
require "tonic/rails/instrumentation"
|
|
18
19
|
require "tonic/rails/base"
|
|
20
|
+
require "tonic/rails/health"
|
|
19
21
|
require "tonic/rails/reflection"
|
|
20
22
|
require "tonic/rails/server"
|
|
21
23
|
|
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: tonic-rails
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.
|
|
4
|
+
version: 0.4.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Matt Tanous
|
|
@@ -80,12 +80,21 @@ files:
|
|
|
80
80
|
- ext/tonic_rails_native/src/log.rs
|
|
81
81
|
- ext/tonic_rails_native/src/net.rs
|
|
82
82
|
- ext/tonic_rails_native/src/server.rs
|
|
83
|
+
- lib/generators/tonic_rails/install/install_generator.rb
|
|
84
|
+
- lib/generators/tonic_rails/install/templates/tonic_rails.rb.tt
|
|
85
|
+
- lib/generators/tonic_rails/service/service_generator.rb
|
|
86
|
+
- lib/generators/tonic_rails/service/templates/handler.rb.tt
|
|
87
|
+
- lib/generators/tonic_rails/service/templates/service.proto.tt
|
|
88
|
+
- lib/generators/tonic_rails/service/templates/service_spec.rb.tt
|
|
89
|
+
- lib/generators/tonic_rails/service/templates/service_test.rb.tt
|
|
83
90
|
- lib/puma/plugin/tonic_rails.rb
|
|
84
91
|
- lib/tonic-rails.rb
|
|
85
92
|
- lib/tonic/rails/base.rb
|
|
86
93
|
- lib/tonic/rails/configuration.rb
|
|
94
|
+
- lib/tonic/rails/health.rb
|
|
87
95
|
- lib/tonic/rails/instrumentation.rb
|
|
88
96
|
- lib/tonic/rails/metadata.rb
|
|
97
|
+
- lib/tonic/rails/peer.rb
|
|
89
98
|
- lib/tonic/rails/railtie.rb
|
|
90
99
|
- lib/tonic/rails/reflection.rb
|
|
91
100
|
- lib/tonic/rails/router.rb
|
|
@@ -93,6 +102,9 @@ files:
|
|
|
93
102
|
- lib/tonic/rails/server.rb
|
|
94
103
|
- lib/tonic/rails/tasks/tonic-rails.rake
|
|
95
104
|
- lib/tonic/rails/testing.rb
|
|
105
|
+
- lib/tonic/rails/testing/errors.rb
|
|
106
|
+
- lib/tonic/rails/testing/minitest.rb
|
|
107
|
+
- lib/tonic/rails/testing/rspec.rb
|
|
96
108
|
- lib/tonic/rails/version.rb
|
|
97
109
|
homepage: https://github.com/CodingAnarchy/tonic-rails
|
|
98
110
|
licenses:
|