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.
@@ -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
- if config.tls?
111
- native.set_tls(config.tls_cert_path, config.tls_key_path)
112
- native.set_tls_client_ca(config.tls_client_ca_path) if config.tls_client_ca_path
113
- end
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
- native
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
- abort "protoc not found on PATH. Install the Protocol Buffers compiler." unless system("protoc", "--version", out: File::NULL)
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
@@ -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]
@@ -2,6 +2,6 @@
2
2
 
3
3
  module Tonic
4
4
  module Rails
5
- VERSION = "0.2.0"
5
+ VERSION = "0.4.0"
6
6
  end
7
7
  end
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.2.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: