gritz-native 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 +14 -0
- data/README.md +5 -1
- data/lib/gritz/native/version.rb +1 -1
- data/lib/gritz/native.rb +4 -0
- data/lib/gritz/testing/server.rb +29 -4
- data/lib/gritz/transport/native/client.rb +31 -0
- data/lib/gritz/transport/native/client_invocation.rb +80 -0
- data/lib/gritz/transport/native/health.rb +118 -0
- data/lib/gritz/transport/native/server.rb +39 -0
- data/lib/gritz/transport/native.rb +43 -5
- metadata +6 -3
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: c84c8135c9f1374962f4dd07181d16d2568498ceb55c4eb03c7d0dce59d475d8
|
|
4
|
+
data.tar.gz: c4dbff62a8188ea82f4c49eb4a7b470a4fee5cbf9199489f88bbab74b8ee08fb
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: c61a25e22d150d219828f0a07f9dc39069fb792b3c8b5fefd82b6af6637071d7849df836736bcfe600da23eff7d8f39d604f8b5602966768e92dcc3915f71fb1
|
|
7
|
+
data.tar.gz: 1d4279464949f4bfacaf45405bc9c670de00b25673d3a5dc6ec941de86c24bd9cb475b6db5790000425fb6c74883042515f65b645b5fefb875e33ebfb739723b
|
data/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,19 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 0.4.0
|
|
4
|
+
|
|
5
|
+
- Connect `Gritz::Client` to all four native RPC forms with shared worker-local channels and deferred credentials.
|
|
6
|
+
- Cancel unfinished response streams and decode downstream status, trailers and protobuf rich error details into typed Gritz errors.
|
|
7
|
+
- Pass retry and load-balancing service configuration to the native channel.
|
|
8
|
+
- Run installed worker telemetry with real-server test helpers and close it after the final RPC observations.
|
|
9
|
+
|
|
10
|
+
## 0.3.0
|
|
11
|
+
|
|
12
|
+
- Serve TLS and mTLS connections and expose authenticated client certificates to handlers.
|
|
13
|
+
- Provide standard gRPC Health Check and Watch methods linked to application health and draining.
|
|
14
|
+
- Finish accepted RPCs during graceful shutdown and phased worker replacement.
|
|
15
|
+
- Report actual native thread-pool usage, including active Health watches.
|
|
16
|
+
|
|
3
17
|
## 0.2.0
|
|
4
18
|
|
|
5
19
|
- Detect native gRPC constructors in the master before fork.
|
data/README.md
CHANGED
|
@@ -10,6 +10,10 @@ require "gritz/native"
|
|
|
10
10
|
|
|
11
11
|
The entry point loads `gritz-core` and registers `Gritz::Transport::Native`. The configuration value is `transport :native`. Use [gritz](https://github.com/gritzrpc/gritz) for the default combination and executable. The Fiber adapter `gritz-async` is planned separately.
|
|
12
12
|
|
|
13
|
+
The adapter also connects `Gritz::Client.define` to all four RPC forms, sharing channels within each worker and creating fresh connections after fork. It cancels unfinished streams, decodes rich downstream errors, and passes retry/load-balancing configuration to C-core. See the [client guide](https://github.com/gritzrpc/gritz-core/blob/main/docs/guides/clients.md).
|
|
14
|
+
|
|
15
|
+
The [three-service integration report](https://github.com/gritzrpc/gritz-otel/blob/main/docs/reports/T4-08-client-chain.md) records worker-local channels, propagated deadlines, connected server/client spans and real OTLP/HTTP metrics.
|
|
16
|
+
|
|
13
17
|
```ruby
|
|
14
18
|
Gritz::Testing::Server.start(controllers: [GreeterController]) do |server|
|
|
15
19
|
stub = Helloworld::Greeter::Stub.new(server.address, :this_channel_is_insecure)
|
|
@@ -17,7 +21,7 @@ Gritz::Testing::Server.start(controllers: [GreeterController]) do |server|
|
|
|
17
21
|
end
|
|
18
22
|
```
|
|
19
23
|
|
|
20
|
-
The helper opens an ephemeral port and stops the server when the block exits. The helper runs a single process. Supervised native servers
|
|
24
|
+
The helper opens an ephemeral port and stops the server when the block exits. The helper runs a single process. Supervised native servers support forked workers on Linux. TLS and required client certificate verification are configured with `tls cert:, key:, client_ca:`. Standard gRPC Health Check/Watch tracks application checks and draining. See the [Kubernetes guide](docs/guides/kubernetes.md) and [framework configuration](https://github.com/gritzrpc/gritz-core/blob/main/docs/guides/configuration.md).
|
|
21
25
|
|
|
22
26
|
## Development
|
|
23
27
|
|
data/lib/gritz/native/version.rb
CHANGED
data/lib/gritz/native.rb
CHANGED
|
@@ -11,4 +11,8 @@ require_relative "transport/native"
|
|
|
11
11
|
require_relative "transport/native/call"
|
|
12
12
|
require_relative "transport/native/bridge"
|
|
13
13
|
require_relative "transport/native/server"
|
|
14
|
+
require_relative "transport/native/health"
|
|
15
|
+
require_relative "transport/native/client"
|
|
14
16
|
require_relative "testing/server"
|
|
17
|
+
|
|
18
|
+
Gritz::Client.adapter = Gritz::Transport::Native::Client
|
data/lib/gritz/testing/server.rb
CHANGED
|
@@ -39,13 +39,20 @@ module Gritz
|
|
|
39
39
|
begin
|
|
40
40
|
@config.preload! if @config.preload_app?
|
|
41
41
|
router = Router.new(controllers: @config.controllers, strict: @config.strict_routes, logger: @logger)
|
|
42
|
-
dispatcher = Dispatcher.new(router:, middleware: @config.middleware, logger: @logger)
|
|
43
|
-
@transport = Transport::Native.new(config: @config, dispatcher:, logger: @logger)
|
|
44
42
|
@lifecycle_started = true
|
|
45
43
|
@config.run_hooks(:on_worker_boot, 0)
|
|
44
|
+
@recorder = @config.metrics_recorder_factory&.call(worker: 0)
|
|
45
|
+
dispatcher = Dispatcher.new(router:, middleware: @config.middleware, logger: @logger, metrics: @recorder)
|
|
46
|
+
@transport = Transport::Native.new(config: @config, dispatcher:, logger: @logger)
|
|
46
47
|
@port = transport.bind
|
|
47
48
|
@address = @config.bind.sub(/:\d+\z/, ":#{port}")
|
|
48
49
|
transport.start
|
|
50
|
+
unless @config.health_checks.empty?
|
|
51
|
+
@health_stop = Queue.new
|
|
52
|
+
@health_thread = Thread.new do
|
|
53
|
+
transport.refresh_health until @health_stop.pop(timeout: @config.status_interval)
|
|
54
|
+
end
|
|
55
|
+
end
|
|
49
56
|
self
|
|
50
57
|
rescue StandardError
|
|
51
58
|
stop
|
|
@@ -57,12 +64,30 @@ module Gritz
|
|
|
57
64
|
return if @stopped
|
|
58
65
|
|
|
59
66
|
@stopped = true
|
|
67
|
+
deadline = monotonic + @config.shutdown_timeout
|
|
60
68
|
begin
|
|
61
|
-
|
|
69
|
+
@health_stop&.push(true)
|
|
70
|
+
if @health_thread && !@health_thread.join([deadline - monotonic, 0].max)
|
|
71
|
+
@health_thread.kill.join
|
|
72
|
+
end
|
|
73
|
+
transport&.stop(deadline: Time.now + [deadline - monotonic, 0].max)
|
|
62
74
|
ensure
|
|
63
|
-
|
|
75
|
+
begin
|
|
76
|
+
@config.run_hooks(:on_worker_shutdown, 0) if @lifecycle_started
|
|
77
|
+
ensure
|
|
78
|
+
begin
|
|
79
|
+
@recorder&.observe_rejected(transport.stats.fetch(:rejected_total, 0)) if transport
|
|
80
|
+
@recorder&.observe_worker({ pid: Process.pid, index: 0, state: "stopped" })
|
|
81
|
+
ensure
|
|
82
|
+
@recorder&.close(timeout: [deadline - monotonic, 0].max)
|
|
83
|
+
end
|
|
84
|
+
end
|
|
64
85
|
end
|
|
65
86
|
end
|
|
87
|
+
|
|
88
|
+
private
|
|
89
|
+
|
|
90
|
+
def monotonic = Process.clock_gettime(Process::CLOCK_MONOTONIC)
|
|
66
91
|
end
|
|
67
92
|
end
|
|
68
93
|
end
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "client_invocation"
|
|
4
|
+
|
|
5
|
+
module Gritz
|
|
6
|
+
module Transport
|
|
7
|
+
class Native
|
|
8
|
+
# Builds official gRPC connections only on the first process-local client call.
|
|
9
|
+
# @api private
|
|
10
|
+
class Client
|
|
11
|
+
Connection = Struct.new(:target, :credentials, :args, :channel, keyword_init: true)
|
|
12
|
+
|
|
13
|
+
def self.connect(target:, credentials:, args:)
|
|
14
|
+
resolved = credentials.respond_to?(:call) ? credentials.call : credentials
|
|
15
|
+
resolved = :this_channel_is_insecure if resolved == :insecure
|
|
16
|
+
channel_credentials = resolved.is_a?(GRPC::Core::CallCredentials) ? GRPC::Core::ChannelCredentials.new : resolved
|
|
17
|
+
channel = GRPC::ClientStub.setup_channel(nil, target, channel_credentials, args.dup)
|
|
18
|
+
Connection.new(target:, credentials: resolved, args:, channel:)
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
def self.invoke(stub_class:, connection:, method:, request:, options:, around:, &)
|
|
22
|
+
stub = stub_class.new(connection.target, connection.credentials,
|
|
23
|
+
channel_override: connection.channel, channel_args: connection.args.dup)
|
|
24
|
+
stub.extend(ClientInvocation)
|
|
25
|
+
stub.instance_variable_set(:@gritz_around, around)
|
|
26
|
+
stub.public_send(method, request, options, &)
|
|
27
|
+
end
|
|
28
|
+
end
|
|
29
|
+
end
|
|
30
|
+
end
|
|
31
|
+
end
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Gritz
|
|
4
|
+
module Transport
|
|
5
|
+
class Native
|
|
6
|
+
# Captures the generated stub's route and keeps middleware around native completion.
|
|
7
|
+
# @api private
|
|
8
|
+
module ClientInvocation
|
|
9
|
+
{ request_response: :unary, client_streamer: :client_streaming,
|
|
10
|
+
server_streamer: :server_streaming, bidi_streamer: :bidi }.each do |native_method, kind|
|
|
11
|
+
define_method(native_method) do |method, request, marshal, unmarshal, **options, &block|
|
|
12
|
+
terminal = lambda do |context, &_reply|
|
|
13
|
+
execute_gritz_client(context) do |response|
|
|
14
|
+
input = context.method.client_streaming? ? context.each_request : context.request
|
|
15
|
+
super(method, input, marshal, unmarshal, **context.options.merge(return_op: true), &response)
|
|
16
|
+
end
|
|
17
|
+
end
|
|
18
|
+
@gritz_around.call(terminal, method:, kind:, request:, options:, &block)
|
|
19
|
+
end
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
private
|
|
23
|
+
|
|
24
|
+
def execute_gritz_client(context)
|
|
25
|
+
context.check_deadline!
|
|
26
|
+
context.check_cancelled!
|
|
27
|
+
response = if context.method.server_streaming?
|
|
28
|
+
lambda do |message|
|
|
29
|
+
context.check_deadline!
|
|
30
|
+
context.check_cancelled!
|
|
31
|
+
context.response_block.call(message)
|
|
32
|
+
end
|
|
33
|
+
end
|
|
34
|
+
operation = yield(response)
|
|
35
|
+
context.operation = operation
|
|
36
|
+
result = operation.execute
|
|
37
|
+
completed = true
|
|
38
|
+
result
|
|
39
|
+
rescue GRPC::BadStatus => e
|
|
40
|
+
raise decode_gritz_error(e)
|
|
41
|
+
ensure
|
|
42
|
+
if operation && !completed
|
|
43
|
+
context.store[:gritz_cancelled] = true
|
|
44
|
+
cancel_gritz_operation(operation)
|
|
45
|
+
end
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
def cancel_gritz_operation(operation)
|
|
49
|
+
operation.cancel
|
|
50
|
+
rescue GRPC::Core::CallError
|
|
51
|
+
# A received non-OK status may already have closed the native call.
|
|
52
|
+
nil
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
def decode_gritz_error(error)
|
|
56
|
+
status = begin
|
|
57
|
+
error.to_rpc_status
|
|
58
|
+
rescue Google::Protobuf::ParseError, TypeError, ArgumentError
|
|
59
|
+
nil
|
|
60
|
+
end
|
|
61
|
+
details = if status && status.code == error.code
|
|
62
|
+
status.details.map { |any| decode_gritz_detail(any) }
|
|
63
|
+
else
|
|
64
|
+
[]
|
|
65
|
+
end
|
|
66
|
+
code = error.code.is_a?(Integer) && (1...Errors::CODES.length).cover?(error.code) ? error.code : 2
|
|
67
|
+
klass = Errors.for_code(code)
|
|
68
|
+
klass.new(error.details, details:, metadata: error.metadata.dup, remote: true)
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
def decode_gritz_detail(any)
|
|
72
|
+
descriptor = Google::Protobuf::DescriptorPool.generated_pool.lookup(any.type_name)
|
|
73
|
+
descriptor.respond_to?(:msgclass) ? any.unpack(descriptor.msgclass) : any
|
|
74
|
+
rescue Google::Protobuf::ParseError, TypeError, ArgumentError
|
|
75
|
+
any
|
|
76
|
+
end
|
|
77
|
+
end
|
|
78
|
+
end
|
|
79
|
+
end
|
|
80
|
+
end
|
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "grpc/health/v1/health_services_pb"
|
|
4
|
+
|
|
5
|
+
module Gritz
|
|
6
|
+
module Transport
|
|
7
|
+
class Native
|
|
8
|
+
# Standard health RPCs share the server pool; each open Watch uses one pool thread.
|
|
9
|
+
# @api private
|
|
10
|
+
class Health < Grpc::Health::V1::Health::Service
|
|
11
|
+
# Ruby's server call view cannot observe cancellation until the call ends.
|
|
12
|
+
# Watch needs a dedicated close observer, with at most one per occupied pool thread.
|
|
13
|
+
class WatchDescriptor < GRPC::RpcDesc
|
|
14
|
+
def handle_server_streamer(active_call, method, interception)
|
|
15
|
+
request = active_call.read_unary_request
|
|
16
|
+
call = active_call.single_req_view
|
|
17
|
+
raw = active_call.instance_variable_get(:@call)
|
|
18
|
+
closed = false
|
|
19
|
+
observer = Thread.new do
|
|
20
|
+
raw.run_batch(GRPC::Core::CallOps::RECV_CLOSE_ON_SERVER => nil)
|
|
21
|
+
rescue GRPC::Core::CallError
|
|
22
|
+
nil
|
|
23
|
+
ensure
|
|
24
|
+
closed = true
|
|
25
|
+
method.receiver.wake
|
|
26
|
+
end
|
|
27
|
+
call.define_singleton_method(:cancelled?) { closed }
|
|
28
|
+
status = begin
|
|
29
|
+
interception.intercept!(:server_streamer, method:, call:, request:) do
|
|
30
|
+
method.call(request, call).each { |reply| active_call.remote_send(reply) }
|
|
31
|
+
end
|
|
32
|
+
::Struct::Status.new(GRPC::Core::StatusCodes::OK, "OK", active_call.output_metadata)
|
|
33
|
+
rescue GRPC::BadStatus => e
|
|
34
|
+
::Struct::Status.new(e.code, e.details, e.metadata)
|
|
35
|
+
rescue GRPC::Core::OutOfTime
|
|
36
|
+
::Struct::Status.new(GRPC::Core::StatusCodes::DEADLINE_EXCEEDED, "late", {})
|
|
37
|
+
rescue StandardError, NotImplementedError
|
|
38
|
+
::Struct::Status.new(GRPC::Core::StatusCodes::UNKNOWN, "Health handler failed", {})
|
|
39
|
+
end
|
|
40
|
+
active_call.send_initial_metadata
|
|
41
|
+
raw.run_batch(GRPC::Core::CallOps::SEND_STATUS_FROM_SERVER => status)
|
|
42
|
+
observer.join
|
|
43
|
+
rescue GRPC::Core::CallError
|
|
44
|
+
nil
|
|
45
|
+
ensure
|
|
46
|
+
if observer
|
|
47
|
+
raw.cancel if observer.alive?
|
|
48
|
+
observer.join
|
|
49
|
+
# Finish only after the native close operation has released its call resources.
|
|
50
|
+
active_call.__send__(:set_output_stream_done)
|
|
51
|
+
end
|
|
52
|
+
end
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
rpc_descs[:Watch] = WatchDescriptor.new(*rpc_descs.fetch(:Watch).to_a)
|
|
56
|
+
|
|
57
|
+
def initialize(services)
|
|
58
|
+
super()
|
|
59
|
+
@services = ["", *services].to_set
|
|
60
|
+
@mutex = Mutex.new
|
|
61
|
+
@changed = ConditionVariable.new
|
|
62
|
+
@serving = @closed = false
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
def update(ready)
|
|
66
|
+
@mutex.synchronize do
|
|
67
|
+
serving = ready && !@closed
|
|
68
|
+
@changed.broadcast if serving != @serving
|
|
69
|
+
@serving = serving
|
|
70
|
+
end
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
def drain!
|
|
74
|
+
@mutex.synchronize do
|
|
75
|
+
@serving = false
|
|
76
|
+
@closed = true
|
|
77
|
+
@changed.broadcast
|
|
78
|
+
end
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
def wake = @mutex.synchronize { @changed.broadcast }
|
|
82
|
+
|
|
83
|
+
def check(request, _call)
|
|
84
|
+
raise GRPC::NotFound, "unknown service" unless @services.include?(request.service)
|
|
85
|
+
|
|
86
|
+
@mutex.synchronize { Grpc::Health::V1::HealthCheckResponse.new(status: status_for(request.service)) }
|
|
87
|
+
end
|
|
88
|
+
|
|
89
|
+
def watch(request, call)
|
|
90
|
+
Enumerator.new do |replies|
|
|
91
|
+
previous = nil
|
|
92
|
+
loop do
|
|
93
|
+
status, closed = @mutex.synchronize do
|
|
94
|
+
@changed.wait(@mutex, 0.05) while !@closed && !call.cancelled? && status_for(request.service) == previous
|
|
95
|
+
[status_for(request.service), @closed]
|
|
96
|
+
end
|
|
97
|
+
break if call.cancelled?
|
|
98
|
+
|
|
99
|
+
if status != previous
|
|
100
|
+
replies << Grpc::Health::V1::HealthCheckResponse.new(status:)
|
|
101
|
+
previous = status
|
|
102
|
+
end
|
|
103
|
+
break if closed
|
|
104
|
+
end
|
|
105
|
+
end
|
|
106
|
+
end
|
|
107
|
+
|
|
108
|
+
private
|
|
109
|
+
|
|
110
|
+
def status_for(service)
|
|
111
|
+
return :SERVICE_UNKNOWN unless @services.include?(service)
|
|
112
|
+
|
|
113
|
+
@serving ? :SERVING : :NOT_SERVING
|
|
114
|
+
end
|
|
115
|
+
end
|
|
116
|
+
end
|
|
117
|
+
end
|
|
118
|
+
end
|
|
@@ -17,6 +17,14 @@ module Gritz
|
|
|
17
17
|
result
|
|
18
18
|
end
|
|
19
19
|
|
|
20
|
+
def busy_threads
|
|
21
|
+
# grpc 1.83 has no public busy count; its idle queues cover decode and
|
|
22
|
+
# response writes as well as handlers. Live threads also handle pre-start/stop.
|
|
23
|
+
workers = @pool.instance_variable_get(:@workers)
|
|
24
|
+
ready = @pool.instance_variable_get(:@ready_workers)
|
|
25
|
+
[workers.count(&:alive?) - ready.size, 0].max
|
|
26
|
+
end
|
|
27
|
+
|
|
20
28
|
def stop(deadline:)
|
|
21
29
|
# grpc 1.83's stop takes no deadline and reads @poll_period instead.
|
|
22
30
|
# Set its relative grace from the caller's absolute shutdown deadline.
|
|
@@ -24,6 +32,37 @@ module Gritz
|
|
|
24
32
|
super()
|
|
25
33
|
end
|
|
26
34
|
|
|
35
|
+
# RpcServer's loop checks running_state before every RequestCall. Its stop
|
|
36
|
+
# holds that mutex during shutdown_and_notify, so an accepted RPC can leave
|
|
37
|
+
# the loop blocked while C-core still has unmatched calls. Keep consuming
|
|
38
|
+
# until C-core closes RequestCall instead of stopping on the Ruby state.
|
|
39
|
+
def loop_handle_server_calls
|
|
40
|
+
raise "not started" if running_state == :not_started
|
|
41
|
+
|
|
42
|
+
loop do
|
|
43
|
+
rpc = @server.request_call
|
|
44
|
+
break if rpc && rpc.call.nil?
|
|
45
|
+
|
|
46
|
+
active_call = new_active_server_call(rpc)
|
|
47
|
+
next unless active_call
|
|
48
|
+
|
|
49
|
+
@pool.schedule(active_call) do |pair|
|
|
50
|
+
call, method = pair
|
|
51
|
+
rpc_descs[method].run_server_method(call, rpc_handlers[method], @interceptors.build_context)
|
|
52
|
+
rescue StandardError
|
|
53
|
+
call.send_status(GRPC::Core::StatusCodes::INTERNAL, "Server handler failed")
|
|
54
|
+
end
|
|
55
|
+
rescue GRPC::Core::CallError, RuntimeError => e
|
|
56
|
+
break unless running_state == :running
|
|
57
|
+
|
|
58
|
+
GRPC.logger.warn("server call failed: #{e}")
|
|
59
|
+
end
|
|
60
|
+
@run_mutex.synchronize do
|
|
61
|
+
transition_running_state(:stopped)
|
|
62
|
+
@server.close
|
|
63
|
+
end
|
|
64
|
+
end
|
|
65
|
+
|
|
27
66
|
def close_unstarted
|
|
28
67
|
return unless running_state == :not_started
|
|
29
68
|
|
|
@@ -8,7 +8,7 @@ module Gritz
|
|
|
8
8
|
# Runs generated services through Gritz's transport-independent dispatcher.
|
|
9
9
|
# @api public
|
|
10
10
|
class Native
|
|
11
|
-
def self.capabilities = Set[:unary, :client_streaming, :server_streaming, :bidi, :reuseport].freeze
|
|
11
|
+
def self.capabilities = Set[:unary, :client_streaming, :server_streaming, :bidi, :reuseport, :health, :tls, :mtls].freeze
|
|
12
12
|
|
|
13
13
|
def self.prefork
|
|
14
14
|
GRPC.prefork
|
|
@@ -32,9 +32,9 @@ module Gritz
|
|
|
32
32
|
# @return [Integer] the port selected by C-core
|
|
33
33
|
def bind(listener_spec = @config.bind)
|
|
34
34
|
raise ArgumentError, "transport is already bound" if @server
|
|
35
|
-
raise ArgumentError, "TLS is not available in this version" unless @config.tls.empty?
|
|
36
35
|
raise ArgumentError, "at least one controller must be registered" if @dispatcher.router.routes.empty?
|
|
37
36
|
|
|
37
|
+
credentials = server_credentials
|
|
38
38
|
created_server = @server = Server.new(
|
|
39
39
|
pool_size: @config.threads,
|
|
40
40
|
# grpc 1.83 accepts but ignores this value: busy pools reject immediately.
|
|
@@ -47,7 +47,9 @@ module Gritz
|
|
|
47
47
|
@dispatcher.router.routes.values.group_by(&:service_class).each do |service_class, descriptors|
|
|
48
48
|
@server.handle(Bridge.build(service_class, descriptors, self))
|
|
49
49
|
end
|
|
50
|
-
@
|
|
50
|
+
@health = Health.new(@dispatcher.router.routes.values.map(&:service).uniq)
|
|
51
|
+
@server.handle(@health)
|
|
52
|
+
@port = @server.add_http2_port(listener_spec, credentials)
|
|
51
53
|
raise ArgumentError, "could not bind #{listener_spec}" unless @port.positive?
|
|
52
54
|
|
|
53
55
|
@port
|
|
@@ -62,7 +64,10 @@ module Gritz
|
|
|
62
64
|
raise ArgumentError, "transport is already started" if @thread
|
|
63
65
|
|
|
64
66
|
@thread = Thread.new { @server.run }
|
|
65
|
-
|
|
67
|
+
if @server.wait_till_running(5)
|
|
68
|
+
refresh_health
|
|
69
|
+
return self
|
|
70
|
+
end
|
|
66
71
|
|
|
67
72
|
@thread.value unless @thread.alive?
|
|
68
73
|
kill
|
|
@@ -76,6 +81,7 @@ module Gritz
|
|
|
76
81
|
def running? = @thread&.alive? && @server.running?
|
|
77
82
|
|
|
78
83
|
def stop(deadline:)
|
|
84
|
+
drain!
|
|
79
85
|
unless @thread
|
|
80
86
|
@server&.close_unstarted
|
|
81
87
|
@server = nil
|
|
@@ -88,11 +94,35 @@ module Gritz
|
|
|
88
94
|
|
|
89
95
|
def kill = stop(deadline: Time.now)
|
|
90
96
|
|
|
97
|
+
def update_health(ready:, checks: {})
|
|
98
|
+
healthy = ready && checks.values.all? && !@draining
|
|
99
|
+
@health&.update(healthy)
|
|
100
|
+
healthy
|
|
101
|
+
end
|
|
102
|
+
|
|
103
|
+
def drain!
|
|
104
|
+
@draining = true
|
|
105
|
+
@health&.drain!
|
|
106
|
+
self
|
|
107
|
+
end
|
|
108
|
+
|
|
109
|
+
# @api private
|
|
110
|
+
def refresh_health
|
|
111
|
+
checks = @config.health_checks.transform_values do |check|
|
|
112
|
+
check.call ? true : false
|
|
113
|
+
rescue StandardError => e
|
|
114
|
+
@logger.warn("Health check failed (#{e.class})")
|
|
115
|
+
false
|
|
116
|
+
end
|
|
117
|
+
update_health(ready: running?, checks:)
|
|
118
|
+
end
|
|
119
|
+
|
|
91
120
|
def stats
|
|
121
|
+
busy = @server&.busy_threads || 0
|
|
92
122
|
@lock.synchronize do
|
|
93
123
|
oldest = @inflight.values.min
|
|
94
124
|
{
|
|
95
|
-
inflight: @inflight.size, busy:
|
|
125
|
+
inflight: @inflight.size, busy: busy, capacity: @config.threads,
|
|
96
126
|
rejected_total: @rejected_total, requests_total: @requests_total,
|
|
97
127
|
oldest_inflight_age: oldest ? Process.clock_gettime(Process::CLOCK_MONOTONIC) - oldest : 0
|
|
98
128
|
}
|
|
@@ -114,6 +144,14 @@ module Gritz
|
|
|
114
144
|
|
|
115
145
|
private
|
|
116
146
|
|
|
147
|
+
def server_credentials
|
|
148
|
+
return :this_port_is_insecure if @config.tls.empty?
|
|
149
|
+
|
|
150
|
+
ca = @config.tls[:client_ca] && File.read(@config.tls[:client_ca])
|
|
151
|
+
GRPC::Core::ServerCredentials.new(ca, [{ private_key: File.read(@config.tls.fetch(:key)),
|
|
152
|
+
cert_chain: File.read(@config.tls.fetch(:cert)) }], !ca.nil?)
|
|
153
|
+
end
|
|
154
|
+
|
|
117
155
|
def status_exception(call, error)
|
|
118
156
|
metadata = call.trailing_metadata.merge(error.metadata)
|
|
119
157
|
unless error.details.empty?
|
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: gritz-native
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.
|
|
4
|
+
version: 0.4.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Yudai Takada
|
|
@@ -35,14 +35,14 @@ dependencies:
|
|
|
35
35
|
requirements:
|
|
36
36
|
- - '='
|
|
37
37
|
- !ruby/object:Gem::Version
|
|
38
|
-
version: 0.
|
|
38
|
+
version: 0.4.0
|
|
39
39
|
type: :runtime
|
|
40
40
|
prerelease: false
|
|
41
41
|
version_requirements: !ruby/object:Gem::Requirement
|
|
42
42
|
requirements:
|
|
43
43
|
- - '='
|
|
44
44
|
- !ruby/object:Gem::Version
|
|
45
|
-
version: 0.
|
|
45
|
+
version: 0.4.0
|
|
46
46
|
- !ruby/object:Gem::Dependency
|
|
47
47
|
name: grpc
|
|
48
48
|
requirement: !ruby/object:Gem::Requirement
|
|
@@ -81,6 +81,9 @@ files:
|
|
|
81
81
|
- lib/gritz/transport/native.rb
|
|
82
82
|
- lib/gritz/transport/native/bridge.rb
|
|
83
83
|
- lib/gritz/transport/native/call.rb
|
|
84
|
+
- lib/gritz/transport/native/client.rb
|
|
85
|
+
- lib/gritz/transport/native/client_invocation.rb
|
|
86
|
+
- lib/gritz/transport/native/health.rb
|
|
84
87
|
- lib/gritz/transport/native/server.rb
|
|
85
88
|
homepage: https://github.com/gritzrpc/gritz-native
|
|
86
89
|
licenses:
|