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.
@@ -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
- @status = Supervisor::StatusChannel.new(status_io) if status_io
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
- report("draining")
77
+ drain
55
78
  @adapter.kill
56
79
  @transport_stopped = true
57
80
  return 0
58
81
  when "TERM", "INT"
59
- report("draining")
60
- @adapter.stop(deadline: Time.now + @config.shutdown_timeout)
61
- @transport_stopped = true
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
- @signals.io.wait_readable([next_status - monotonic, 0].max)
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
- @status.write(row.merge(extra))
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
- report("stopped", include_stats: false, exit_status: @exit_status)
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.2.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