gritz-native 0.1.0 → 0.3.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 +12 -0
- data/README.md +1 -1
- data/lib/gritz/native/fork_guard.rb +10 -0
- data/lib/gritz/native/version.rb +1 -1
- data/lib/gritz/native.rb +2 -0
- data/lib/gritz/testing/server.rb +10 -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 +52 -5
- metadata +6 -4
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 11284795837121fb007d3158c2fa704281946741d262e2101cf0ab3b134183e5
|
|
4
|
+
data.tar.gz: b1c1a7c414f1b3f435af88183f8da23dd7aaabcff4591355dbfdd138deeffcb0
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 832837820eb0ecb86bef7fe3cf84514f52a22b92933fb08bf9fd8d3cb48fee690585ba42c853bee984bdd801fb93c7fdaa96e5a873ffee93b976b417f99f08b0
|
|
7
|
+
data.tar.gz: f7b4a6ba6ba8ad367db6d6d486fc041471f73a7fa72663ed90ee71ed1445122d287c92c2ceaef986b9b7572445f3c03960e4a65f01af4588be904b2c67d251a8
|
data/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,17 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 0.3.0
|
|
4
|
+
|
|
5
|
+
- Serve TLS and mTLS connections and expose authenticated client certificates to handlers.
|
|
6
|
+
- Provide standard gRPC Health Check and Watch methods linked to application health and draining.
|
|
7
|
+
- Finish accepted RPCs during graceful shutdown and phased worker replacement.
|
|
8
|
+
- Report actual native thread-pool usage, including active Health watches.
|
|
9
|
+
|
|
10
|
+
## 0.2.0
|
|
11
|
+
|
|
12
|
+
- Detect native gRPC constructors in the master before fork.
|
|
13
|
+
- Support experimental Linux grpc fork callbacks for parent-side clients.
|
|
14
|
+
|
|
3
15
|
## 0.1.0
|
|
4
16
|
|
|
5
17
|
Initial release.
|
data/README.md
CHANGED
|
@@ -17,7 +17,7 @@ Gritz::Testing::Server.start(controllers: [GreeterController]) do |server|
|
|
|
17
17
|
end
|
|
18
18
|
```
|
|
19
19
|
|
|
20
|
-
The helper opens an ephemeral port and stops the server when the block exits. The
|
|
20
|
+
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
21
|
|
|
22
22
|
## Development
|
|
23
23
|
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
# Constructor instrumentation installed without activating a master guard.
|
|
4
|
+
# @api private
|
|
5
|
+
module Gritz::Native::ForkGuard
|
|
6
|
+
[GRPC::Core::Channel, GRPC::Core::Server, GRPC::Core::ChannelCredentials,
|
|
7
|
+
GRPC::Core::ServerCredentials, GRPC::Core::CallCredentials, GRPC::ClientStub, GRPC::RpcServer].each do |klass|
|
|
8
|
+
Gritz::ForkGuard.install(klass)
|
|
9
|
+
end
|
|
10
|
+
end
|
data/lib/gritz/native/version.rb
CHANGED
data/lib/gritz/native.rb
CHANGED
|
@@ -6,8 +6,10 @@ require "google/rpc/status_pb"
|
|
|
6
6
|
require "google/protobuf/any_pb"
|
|
7
7
|
require "google/protobuf/well_known_types"
|
|
8
8
|
require_relative "native/version"
|
|
9
|
+
require_relative "native/fork_guard"
|
|
9
10
|
require_relative "transport/native"
|
|
10
11
|
require_relative "transport/native/call"
|
|
11
12
|
require_relative "transport/native/bridge"
|
|
12
13
|
require_relative "transport/native/server"
|
|
14
|
+
require_relative "transport/native/health"
|
|
13
15
|
require_relative "testing/server"
|
data/lib/gritz/testing/server.rb
CHANGED
|
@@ -46,6 +46,12 @@ module Gritz
|
|
|
46
46
|
@port = transport.bind
|
|
47
47
|
@address = @config.bind.sub(/:\d+\z/, ":#{port}")
|
|
48
48
|
transport.start
|
|
49
|
+
unless @config.health_checks.empty?
|
|
50
|
+
@health_stop = Queue.new
|
|
51
|
+
@health_thread = Thread.new do
|
|
52
|
+
transport.refresh_health until @health_stop.pop(timeout: @config.status_interval)
|
|
53
|
+
end
|
|
54
|
+
end
|
|
49
55
|
self
|
|
50
56
|
rescue StandardError
|
|
51
57
|
stop
|
|
@@ -58,6 +64,10 @@ module Gritz
|
|
|
58
64
|
|
|
59
65
|
@stopped = true
|
|
60
66
|
begin
|
|
67
|
+
@health_stop&.push(true)
|
|
68
|
+
if @health_thread && !@health_thread.join(@config.shutdown_timeout)
|
|
69
|
+
@health_thread.kill.join
|
|
70
|
+
end
|
|
61
71
|
transport&.stop(deadline: Time.now + @config.shutdown_timeout)
|
|
62
72
|
ensure
|
|
63
73
|
@config.run_hooks(:on_worker_shutdown, 0) if @lifecycle_started
|
|
@@ -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,16 @@ 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
|
+
|
|
13
|
+
def self.prefork
|
|
14
|
+
GRPC.prefork
|
|
15
|
+
rescue RuntimeError => e
|
|
16
|
+
raise "#{e.message}. fork_mode :grpc_fork_support requires GRPC_ENABLE_FORK_SUPPORT=1 before requiring grpc."
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
def self.postfork_parent = GRPC.postfork_parent
|
|
20
|
+
def self.postfork_child = GRPC.postfork_child
|
|
12
21
|
|
|
13
22
|
def initialize(config:, dispatcher:, logger:)
|
|
14
23
|
@config = config
|
|
@@ -23,9 +32,9 @@ module Gritz
|
|
|
23
32
|
# @return [Integer] the port selected by C-core
|
|
24
33
|
def bind(listener_spec = @config.bind)
|
|
25
34
|
raise ArgumentError, "transport is already bound" if @server
|
|
26
|
-
raise ArgumentError, "TLS is not available in this version" unless @config.tls.empty?
|
|
27
35
|
raise ArgumentError, "at least one controller must be registered" if @dispatcher.router.routes.empty?
|
|
28
36
|
|
|
37
|
+
credentials = server_credentials
|
|
29
38
|
created_server = @server = Server.new(
|
|
30
39
|
pool_size: @config.threads,
|
|
31
40
|
# grpc 1.83 accepts but ignores this value: busy pools reject immediately.
|
|
@@ -38,7 +47,9 @@ module Gritz
|
|
|
38
47
|
@dispatcher.router.routes.values.group_by(&:service_class).each do |service_class, descriptors|
|
|
39
48
|
@server.handle(Bridge.build(service_class, descriptors, self))
|
|
40
49
|
end
|
|
41
|
-
@
|
|
50
|
+
@health = Health.new(@dispatcher.router.routes.values.map(&:service).uniq)
|
|
51
|
+
@server.handle(@health)
|
|
52
|
+
@port = @server.add_http2_port(listener_spec, credentials)
|
|
42
53
|
raise ArgumentError, "could not bind #{listener_spec}" unless @port.positive?
|
|
43
54
|
|
|
44
55
|
@port
|
|
@@ -53,7 +64,10 @@ module Gritz
|
|
|
53
64
|
raise ArgumentError, "transport is already started" if @thread
|
|
54
65
|
|
|
55
66
|
@thread = Thread.new { @server.run }
|
|
56
|
-
|
|
67
|
+
if @server.wait_till_running(5)
|
|
68
|
+
refresh_health
|
|
69
|
+
return self
|
|
70
|
+
end
|
|
57
71
|
|
|
58
72
|
@thread.value unless @thread.alive?
|
|
59
73
|
kill
|
|
@@ -67,6 +81,7 @@ module Gritz
|
|
|
67
81
|
def running? = @thread&.alive? && @server.running?
|
|
68
82
|
|
|
69
83
|
def stop(deadline:)
|
|
84
|
+
drain!
|
|
70
85
|
unless @thread
|
|
71
86
|
@server&.close_unstarted
|
|
72
87
|
@server = nil
|
|
@@ -79,11 +94,35 @@ module Gritz
|
|
|
79
94
|
|
|
80
95
|
def kill = stop(deadline: Time.now)
|
|
81
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
|
+
|
|
82
120
|
def stats
|
|
121
|
+
busy = @server&.busy_threads || 0
|
|
83
122
|
@lock.synchronize do
|
|
84
123
|
oldest = @inflight.values.min
|
|
85
124
|
{
|
|
86
|
-
inflight: @inflight.size, busy:
|
|
125
|
+
inflight: @inflight.size, busy: busy, capacity: @config.threads,
|
|
87
126
|
rejected_total: @rejected_total, requests_total: @requests_total,
|
|
88
127
|
oldest_inflight_age: oldest ? Process.clock_gettime(Process::CLOCK_MONOTONIC) - oldest : 0
|
|
89
128
|
}
|
|
@@ -105,6 +144,14 @@ module Gritz
|
|
|
105
144
|
|
|
106
145
|
private
|
|
107
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
|
+
|
|
108
155
|
def status_exception(call, error)
|
|
109
156
|
metadata = call.trailing_metadata.merge(error.metadata)
|
|
110
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.3.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.3.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.3.0
|
|
46
46
|
- !ruby/object:Gem::Dependency
|
|
47
47
|
name: grpc
|
|
48
48
|
requirement: !ruby/object:Gem::Requirement
|
|
@@ -75,11 +75,13 @@ files:
|
|
|
75
75
|
- LICENSE.txt
|
|
76
76
|
- README.md
|
|
77
77
|
- lib/gritz/native.rb
|
|
78
|
+
- lib/gritz/native/fork_guard.rb
|
|
78
79
|
- lib/gritz/native/version.rb
|
|
79
80
|
- lib/gritz/testing/server.rb
|
|
80
81
|
- lib/gritz/transport/native.rb
|
|
81
82
|
- lib/gritz/transport/native/bridge.rb
|
|
82
83
|
- lib/gritz/transport/native/call.rb
|
|
84
|
+
- lib/gritz/transport/native/health.rb
|
|
83
85
|
- lib/gritz/transport/native/server.rb
|
|
84
86
|
homepage: https://github.com/gritzrpc/gritz-native
|
|
85
87
|
licenses:
|
|
@@ -103,7 +105,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
|
103
105
|
- !ruby/object:Gem::Version
|
|
104
106
|
version: '0'
|
|
105
107
|
requirements: []
|
|
106
|
-
rubygems_version:
|
|
108
|
+
rubygems_version: 3.6.9
|
|
107
109
|
specification_version: 4
|
|
108
110
|
summary: gRPC C-core transport for Gritz
|
|
109
111
|
test_files: []
|