gritz-core 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 +13 -0
- data/README.md +4 -0
- data/lib/gritz/channel_registry.rb +42 -0
- data/lib/gritz/cli.rb +31 -5
- data/lib/gritz/client/call_context.rb +60 -0
- data/lib/gritz/client/invocation.rb +84 -0
- data/lib/gritz/client.rb +109 -0
- data/lib/gritz/configuration.rb +23 -9
- data/lib/gritz/context.rb +31 -2
- data/lib/gritz/controller.rb +14 -0
- data/lib/gritz/core/version.rb +1 -1
- data/lib/gritz/dispatcher.rb +9 -3
- data/lib/gritz/errors.rb +3 -1
- data/lib/gritz/metrics/aggregator.rb +119 -0
- data/lib/gritz/metrics/recorder.rb +93 -0
- data/lib/gritz/middleware/exception_mapper.rb +14 -7
- data/lib/gritz/middleware/logging.rb +27 -3
- data/lib/gritz/middleware/metrics.rb +29 -0
- data/lib/gritz/middleware/stack.rb +1 -1
- data/lib/gritz/supervisor/admin_server.rb +121 -0
- data/lib/gritz/supervisor/launcher.rb +381 -0
- data/lib/gritz/supervisor/master.rb +220 -29
- data/lib/gritz/supervisor/status_channel.rb +15 -3
- data/lib/gritz/supervisor/worker_handle.rb +4 -3
- data/lib/gritz/testing/cluster.rb +43 -7
- data/lib/gritz/worker/runner.rb +111 -16
- metadata +30 -1
data/lib/gritz/worker/runner.rb
CHANGED
|
@@ -7,27 +7,38 @@ module Gritz
|
|
|
7
7
|
# Owns transport resources and heartbeats in one serving process.
|
|
8
8
|
# @api private
|
|
9
9
|
class Runner
|
|
10
|
-
def initialize(index:, config:, logger:, status_io: nil)
|
|
10
|
+
def initialize(index:, config:, logger:, status_io: nil, owner_channel: nil)
|
|
11
11
|
@index = index
|
|
12
12
|
@config = config
|
|
13
13
|
@logger = logger
|
|
14
|
-
@
|
|
14
|
+
@owner_channel = owner_channel
|
|
15
|
+
@status = owner_channel || (Supervisor::StatusChannel.new(status_io) if status_io)
|
|
16
|
+
@recorder = Metrics::Recorder.new
|
|
17
|
+
@metrics = Metrics::Aggregator.new
|
|
18
|
+
@metric_seq = 0
|
|
19
|
+
@born_at = monotonic
|
|
15
20
|
end
|
|
16
21
|
|
|
17
22
|
def run
|
|
18
23
|
@exit_status = 0
|
|
19
24
|
begin
|
|
20
|
-
@signals = Supervisor::SignalQueue.new(signals: %w[TERM INT QUIT HUP])
|
|
25
|
+
@signals = Supervisor::SignalQueue.new(signals: %w[TERM INT QUIT HUP USR1 USR2])
|
|
21
26
|
report("booting")
|
|
22
27
|
require "gritz/native"
|
|
23
28
|
@boot_started = true
|
|
24
29
|
@config.preload! unless @config.preload_app?
|
|
25
30
|
@config.run_hooks(:on_worker_boot, @index)
|
|
31
|
+
@recorder = @config.metrics_recorder_factory.call(worker: @index) if @config.metrics_recorder_factory
|
|
26
32
|
router = Router.new(controllers: @config.controllers, strict: @config.strict_routes, logger: @logger)
|
|
27
|
-
dispatcher = Dispatcher.new(router:, middleware: @config.middleware, logger: @logger
|
|
33
|
+
dispatcher = Dispatcher.new(router:, middleware: @config.middleware, logger: @logger, metrics: @recorder,
|
|
34
|
+
worker: @index, log_format: @config.log_format, log_redact: @config.log_redact)
|
|
28
35
|
@adapter = Transport::Native.new(config: @config, dispatcher:, logger: @logger)
|
|
29
36
|
@port = @adapter.bind(@config.bind)
|
|
30
37
|
@adapter.start
|
|
38
|
+
unless @status
|
|
39
|
+
@admin = Supervisor::AdminServer.new(bind: @config.admin_bind, status: -> { snapshot }, ready: -> { ready? },
|
|
40
|
+
metrics: -> { @metrics.render(workers: [@last_status].compact) }, logger: @logger)
|
|
41
|
+
end
|
|
31
42
|
@logger.info("Gritz #{Core::VERSION} listening on #{@config.bind.sub(/:\d+\z/, ":#{@port}")}")
|
|
32
43
|
report("ready")
|
|
33
44
|
serve
|
|
@@ -50,35 +61,100 @@ module Gritz
|
|
|
50
61
|
@signals.drain.each do |name|
|
|
51
62
|
case name
|
|
52
63
|
when "HUP" then @logger.reopen
|
|
64
|
+
when "USR1"
|
|
65
|
+
if @config.workers.zero?
|
|
66
|
+
@logger.warn("USR1 requires workers > 0; use USR2 to reload a single-process server")
|
|
67
|
+
else
|
|
68
|
+
drain
|
|
69
|
+
end
|
|
70
|
+
when "USR2"
|
|
71
|
+
if @owner_channel
|
|
72
|
+
@pending_reexec = true
|
|
73
|
+
else
|
|
74
|
+
@logger.warn("USR2 requires the gritz launcher")
|
|
75
|
+
end
|
|
53
76
|
when "QUIT"
|
|
54
|
-
|
|
77
|
+
drain
|
|
55
78
|
@adapter.kill
|
|
56
79
|
@transport_stopped = true
|
|
57
80
|
return 0
|
|
58
81
|
when "TERM", "INT"
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
@
|
|
62
|
-
return 0
|
|
82
|
+
drain
|
|
83
|
+
# Supervised children have already waited in the master's drain phase.
|
|
84
|
+
@drain_until ||= monotonic + (@config.workers.zero? ? @config.drain_delay : 0)
|
|
63
85
|
end
|
|
64
86
|
end
|
|
65
87
|
now = monotonic
|
|
88
|
+
if @drain_until && now >= @drain_until
|
|
89
|
+
@stop_deadline = now + @config.shutdown_timeout
|
|
90
|
+
@adapter.stop(deadline: Time.now + @config.shutdown_timeout)
|
|
91
|
+
@transport_stopped = true
|
|
92
|
+
return 0
|
|
93
|
+
end
|
|
94
|
+
@status&.flush
|
|
95
|
+
if @pending_reexec && @owner_channel.write(type: "reexec", pid: Process.pid)
|
|
96
|
+
@pending_reexec = false
|
|
97
|
+
end
|
|
98
|
+
publish_metrics
|
|
99
|
+
@admin&.poll
|
|
66
100
|
if now >= next_status
|
|
67
|
-
report("ready")
|
|
101
|
+
report(@draining ? "draining" : "ready")
|
|
68
102
|
next_status = now + @config.status_interval
|
|
69
103
|
end
|
|
70
|
-
|
|
104
|
+
next_event = [next_status, @drain_until].compact.min
|
|
105
|
+
IO.select([@signals.io, *@admin&.ios.to_a], nil, nil, (next_event - monotonic).clamp(0, 0.05))
|
|
71
106
|
end
|
|
72
107
|
end
|
|
73
108
|
|
|
74
109
|
def report(state, include_stats: true, **extra)
|
|
75
|
-
return unless @status
|
|
76
|
-
|
|
77
110
|
stats = include_stats && @adapter ? @adapter.stats : {}
|
|
111
|
+
@recorder.observe_rejected(stats.fetch(:rejected_total, 0)) if include_stats && @adapter
|
|
112
|
+
evaluate_health if state == "ready"
|
|
78
113
|
row = { inflight: 0, capacity: @config.threads, requests_total: 0, oldest_inflight_age: 0 }.merge(stats)
|
|
79
114
|
row.merge!(pid: Process.pid, index: @index, state:, ts: monotonic, port: @port,
|
|
80
|
-
busy_threads: stats.fetch(:busy_threads, stats.fetch(:busy, 0))
|
|
81
|
-
|
|
115
|
+
busy_threads: stats.fetch(:busy_threads, stats.fetch(:busy, 0)), healthy: state == "ready" && @healthy,
|
|
116
|
+
checks: @checks || {}, worker_started_at: @born_at)
|
|
117
|
+
@last_status = row.merge(extra)
|
|
118
|
+
@recorder.observe_worker(@last_status)
|
|
119
|
+
@status&.write(@owner_channel ? snapshot : @last_status)
|
|
120
|
+
end
|
|
121
|
+
|
|
122
|
+
def snapshot
|
|
123
|
+
{ type: "status", pid: Process.pid, state: @draining ? "draining" : "running", desired: 1, workers: [@last_status].compact }
|
|
124
|
+
end
|
|
125
|
+
|
|
126
|
+
def ready? = !@draining && @last_status && @last_status[:state] == "ready" && @last_status[:healthy]
|
|
127
|
+
|
|
128
|
+
def evaluate_health
|
|
129
|
+
@checks = @config.health_checks.to_h do |name, callback|
|
|
130
|
+
[name, callback.call ? true : false]
|
|
131
|
+
rescue StandardError => e
|
|
132
|
+
@logger.warn("Health check #{name}: #{e.message}")
|
|
133
|
+
[name, false]
|
|
134
|
+
end
|
|
135
|
+
@healthy = @adapter.update_health(ready: true, checks: @checks)
|
|
136
|
+
end
|
|
137
|
+
|
|
138
|
+
def drain
|
|
139
|
+
return if @draining
|
|
140
|
+
|
|
141
|
+
@draining = true
|
|
142
|
+
@adapter.drain!
|
|
143
|
+
report("draining")
|
|
144
|
+
end
|
|
145
|
+
|
|
146
|
+
def publish_metrics
|
|
147
|
+
@pending_delta ||= @recorder.take_delta
|
|
148
|
+
return true unless @pending_delta
|
|
149
|
+
|
|
150
|
+
packet = { type: "metrics", pid: Process.pid, worker_pid: Process.pid, worker_started_at: @born_at,
|
|
151
|
+
seq: @metric_seq + 1, delta: @pending_delta }
|
|
152
|
+
accepted = @status ? @status.write(packet) : @metrics.apply(self, packet)
|
|
153
|
+
if accepted
|
|
154
|
+
@metric_seq += 1
|
|
155
|
+
@pending_delta = nil
|
|
156
|
+
end
|
|
157
|
+
accepted
|
|
82
158
|
end
|
|
83
159
|
|
|
84
160
|
def fail_worker(error)
|
|
@@ -90,10 +166,29 @@ module Gritz
|
|
|
90
166
|
def finish
|
|
91
167
|
cleanup { @adapter&.kill unless @transport_stopped }
|
|
92
168
|
cleanup { @config.run_hooks(:on_worker_shutdown, @index) if @boot_started }
|
|
93
|
-
|
|
169
|
+
cleanup { @recorder.observe_rejected(@adapter.stats.fetch(:rejected_total, 0)) if @adapter }
|
|
170
|
+
deadline = @stop_deadline || (monotonic + @config.shutdown_timeout)
|
|
171
|
+
loop do
|
|
172
|
+
@status&.flush
|
|
173
|
+
accepted = publish_metrics
|
|
174
|
+
@pending_delta ||= @recorder.take_delta if accepted
|
|
175
|
+
break if accepted && !@pending_delta && (@status.nil? || @status.flush)
|
|
176
|
+
break if monotonic >= deadline || @status&.closed?
|
|
177
|
+
|
|
178
|
+
@status&.io&.wait_writable((deadline - monotonic).clamp(0, 0.01))
|
|
179
|
+
end
|
|
180
|
+
until report("stopped", include_stats: false, exit_status: @exit_status) != false
|
|
181
|
+
break if monotonic >= deadline || @status&.closed?
|
|
182
|
+
|
|
183
|
+
@status.flush
|
|
184
|
+
@status.io.wait_writable((deadline - monotonic).clamp(0, 0.01)) unless @status.closed?
|
|
185
|
+
end
|
|
186
|
+
@status.io.wait_writable((deadline - monotonic).clamp(0, 0.01)) until !@status || @status.flush || @status.closed? || monotonic >= deadline
|
|
94
187
|
ensure
|
|
95
188
|
begin
|
|
189
|
+
cleanup { @recorder.close(timeout: [(deadline || monotonic) - monotonic, 0].max) }
|
|
96
190
|
@signals&.close
|
|
191
|
+
@admin&.close
|
|
97
192
|
ensure
|
|
98
193
|
@status&.close
|
|
99
194
|
end
|
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: gritz-core
|
|
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
|
|
@@ -9,6 +9,26 @@ bindir: bin
|
|
|
9
9
|
cert_chain: []
|
|
10
10
|
date: 1980-01-02 00:00:00.000000000 Z
|
|
11
11
|
dependencies:
|
|
12
|
+
- !ruby/object:Gem::Dependency
|
|
13
|
+
name: fiddle
|
|
14
|
+
requirement: !ruby/object:Gem::Requirement
|
|
15
|
+
requirements:
|
|
16
|
+
- - ">="
|
|
17
|
+
- !ruby/object:Gem::Version
|
|
18
|
+
version: '1.1'
|
|
19
|
+
- - "<"
|
|
20
|
+
- !ruby/object:Gem::Version
|
|
21
|
+
version: '3'
|
|
22
|
+
type: :runtime
|
|
23
|
+
prerelease: false
|
|
24
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
25
|
+
requirements:
|
|
26
|
+
- - ">="
|
|
27
|
+
- !ruby/object:Gem::Version
|
|
28
|
+
version: '1.1'
|
|
29
|
+
- - "<"
|
|
30
|
+
- !ruby/object:Gem::Version
|
|
31
|
+
version: '3'
|
|
12
32
|
- !ruby/object:Gem::Dependency
|
|
13
33
|
name: google-protobuf
|
|
14
34
|
requirement: !ruby/object:Gem::Requirement
|
|
@@ -101,7 +121,11 @@ files:
|
|
|
101
121
|
- LICENSE.txt
|
|
102
122
|
- README.md
|
|
103
123
|
- lib/gritz/call.rb
|
|
124
|
+
- lib/gritz/channel_registry.rb
|
|
104
125
|
- lib/gritz/cli.rb
|
|
126
|
+
- lib/gritz/client.rb
|
|
127
|
+
- lib/gritz/client/call_context.rb
|
|
128
|
+
- lib/gritz/client/invocation.rb
|
|
105
129
|
- lib/gritz/configuration.rb
|
|
106
130
|
- lib/gritz/configuration_error.rb
|
|
107
131
|
- lib/gritz/context.rb
|
|
@@ -113,12 +137,17 @@ files:
|
|
|
113
137
|
- lib/gritz/errors.rb
|
|
114
138
|
- lib/gritz/fork_guard.rb
|
|
115
139
|
- lib/gritz/method_descriptor.rb
|
|
140
|
+
- lib/gritz/metrics/aggregator.rb
|
|
141
|
+
- lib/gritz/metrics/recorder.rb
|
|
116
142
|
- lib/gritz/middleware/context.rb
|
|
117
143
|
- lib/gritz/middleware/exception_mapper.rb
|
|
118
144
|
- lib/gritz/middleware/logging.rb
|
|
145
|
+
- lib/gritz/middleware/metrics.rb
|
|
119
146
|
- lib/gritz/middleware/request_id.rb
|
|
120
147
|
- lib/gritz/middleware/stack.rb
|
|
121
148
|
- lib/gritz/router.rb
|
|
149
|
+
- lib/gritz/supervisor/admin_server.rb
|
|
150
|
+
- lib/gritz/supervisor/launcher.rb
|
|
122
151
|
- lib/gritz/supervisor/master.rb
|
|
123
152
|
- lib/gritz/supervisor/signal_queue.rb
|
|
124
153
|
- lib/gritz/supervisor/status_channel.rb
|