gritz-core 0.2.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.
@@ -7,27 +7,37 @@ 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)
26
31
  router = Router.new(controllers: @config.controllers, strict: @config.strict_routes, logger: @logger)
27
- dispatcher = Dispatcher.new(router:, middleware: @config.middleware, logger: @logger)
32
+ dispatcher = Dispatcher.new(router:, middleware: @config.middleware, logger: @logger, metrics: @recorder,
33
+ worker: @index, log_format: @config.log_format, log_redact: @config.log_redact)
28
34
  @adapter = Transport::Native.new(config: @config, dispatcher:, logger: @logger)
29
35
  @port = @adapter.bind(@config.bind)
30
36
  @adapter.start
37
+ unless @status
38
+ @admin = Supervisor::AdminServer.new(bind: @config.admin_bind, status: -> { snapshot }, ready: -> { ready? },
39
+ metrics: -> { @metrics.render(workers: [@last_status].compact) }, logger: @logger)
40
+ end
31
41
  @logger.info("Gritz #{Core::VERSION} listening on #{@config.bind.sub(/:\d+\z/, ":#{@port}")}")
32
42
  report("ready")
33
43
  serve
@@ -50,35 +60,99 @@ module Gritz
50
60
  @signals.drain.each do |name|
51
61
  case name
52
62
  when "HUP" then @logger.reopen
63
+ when "USR1"
64
+ if @config.workers.zero?
65
+ @logger.warn("USR1 requires workers > 0; use USR2 to reload a single-process server")
66
+ else
67
+ drain
68
+ end
69
+ when "USR2"
70
+ if @owner_channel
71
+ @pending_reexec = true
72
+ else
73
+ @logger.warn("USR2 requires the gritz launcher")
74
+ end
53
75
  when "QUIT"
54
- report("draining")
76
+ drain
55
77
  @adapter.kill
56
78
  @transport_stopped = true
57
79
  return 0
58
80
  when "TERM", "INT"
59
- report("draining")
60
- @adapter.stop(deadline: Time.now + @config.shutdown_timeout)
61
- @transport_stopped = true
62
- return 0
81
+ drain
82
+ # Supervised children have already waited in the master's drain phase.
83
+ @drain_until ||= monotonic + (@config.workers.zero? ? @config.drain_delay : 0)
63
84
  end
64
85
  end
65
86
  now = monotonic
87
+ if @drain_until && now >= @drain_until
88
+ @stop_deadline = now + @config.shutdown_timeout
89
+ @adapter.stop(deadline: Time.now + @config.shutdown_timeout)
90
+ @transport_stopped = true
91
+ return 0
92
+ end
93
+ @status&.flush
94
+ if @pending_reexec && @owner_channel.write(type: "reexec", pid: Process.pid)
95
+ @pending_reexec = false
96
+ end
97
+ publish_metrics
98
+ @admin&.poll
66
99
  if now >= next_status
67
- report("ready")
100
+ report(@draining ? "draining" : "ready")
68
101
  next_status = now + @config.status_interval
69
102
  end
70
- @signals.io.wait_readable([next_status - monotonic, 0].max)
103
+ next_event = [next_status, @drain_until].compact.min
104
+ IO.select([@signals.io, *@admin&.ios.to_a], nil, nil, (next_event - monotonic).clamp(0, 0.05))
71
105
  end
72
106
  end
73
107
 
74
108
  def report(state, include_stats: true, **extra)
75
- return unless @status
76
-
77
109
  stats = include_stats && @adapter ? @adapter.stats : {}
110
+ @recorder.observe_rejected(stats.fetch(:rejected_total, 0)) if include_stats && @adapter
111
+ evaluate_health if state == "ready"
78
112
  row = { inflight: 0, capacity: @config.threads, requests_total: 0, oldest_inflight_age: 0 }.merge(stats)
79
113
  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))
114
+ busy_threads: stats.fetch(:busy_threads, stats.fetch(:busy, 0)), healthy: state == "ready" && @healthy,
115
+ checks: @checks || {}, worker_started_at: @born_at)
116
+ @last_status = row.merge(extra)
117
+ @status&.write(@owner_channel ? snapshot : @last_status)
118
+ end
119
+
120
+ def snapshot
121
+ { type: "status", pid: Process.pid, state: @draining ? "draining" : "running", desired: 1, workers: [@last_status].compact }
122
+ end
123
+
124
+ def ready? = !@draining && @last_status && @last_status[:state] == "ready" && @last_status[:healthy]
125
+
126
+ def evaluate_health
127
+ @checks = @config.health_checks.to_h do |name, callback|
128
+ [name, callback.call ? true : false]
129
+ rescue StandardError => e
130
+ @logger.warn("Health check #{name}: #{e.message}")
131
+ [name, false]
132
+ end
133
+ @healthy = @adapter.update_health(ready: true, checks: @checks)
134
+ end
135
+
136
+ def drain
137
+ return if @draining
138
+
139
+ @draining = true
140
+ @adapter.drain!
141
+ report("draining")
142
+ end
143
+
144
+ def publish_metrics
145
+ @pending_delta ||= @recorder.take_delta
146
+ return true unless @pending_delta
147
+
148
+ packet = { type: "metrics", pid: Process.pid, worker_pid: Process.pid, worker_started_at: @born_at,
149
+ seq: @metric_seq + 1, delta: @pending_delta }
150
+ accepted = @status ? @status.write(packet) : @metrics.apply(self, packet)
151
+ if accepted
152
+ @metric_seq += 1
153
+ @pending_delta = nil
154
+ end
155
+ accepted
82
156
  end
83
157
 
84
158
  def fail_worker(error)
@@ -90,10 +164,28 @@ module Gritz
90
164
  def finish
91
165
  cleanup { @adapter&.kill unless @transport_stopped }
92
166
  cleanup { @config.run_hooks(:on_worker_shutdown, @index) if @boot_started }
93
- report("stopped", include_stats: false, exit_status: @exit_status)
167
+ cleanup { @recorder.observe_rejected(@adapter.stats.fetch(:rejected_total, 0)) if @adapter }
168
+ deadline = @stop_deadline || (monotonic + @config.shutdown_timeout)
169
+ loop do
170
+ @status&.flush
171
+ accepted = publish_metrics
172
+ @pending_delta ||= @recorder.take_delta if accepted
173
+ break if accepted && !@pending_delta && (@status.nil? || @status.flush)
174
+ break if monotonic >= deadline || @status&.closed?
175
+
176
+ @status&.io&.wait_writable((deadline - monotonic).clamp(0, 0.01))
177
+ end
178
+ until report("stopped", include_stats: false, exit_status: @exit_status) != false
179
+ break if monotonic >= deadline || @status&.closed?
180
+
181
+ @status.flush
182
+ @status.io.wait_writable((deadline - monotonic).clamp(0, 0.01)) unless @status.closed?
183
+ end
184
+ @status.io.wait_writable((deadline - monotonic).clamp(0, 0.01)) until !@status || @status.flush || @status.closed? || monotonic >= deadline
94
185
  ensure
95
186
  begin
96
187
  @signals&.close
188
+ @admin&.close
97
189
  ensure
98
190
  @status&.close
99
191
  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.3.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
@@ -113,12 +133,17 @@ files:
113
133
  - lib/gritz/errors.rb
114
134
  - lib/gritz/fork_guard.rb
115
135
  - lib/gritz/method_descriptor.rb
136
+ - lib/gritz/metrics/aggregator.rb
137
+ - lib/gritz/metrics/recorder.rb
116
138
  - lib/gritz/middleware/context.rb
117
139
  - lib/gritz/middleware/exception_mapper.rb
118
140
  - lib/gritz/middleware/logging.rb
141
+ - lib/gritz/middleware/metrics.rb
119
142
  - lib/gritz/middleware/request_id.rb
120
143
  - lib/gritz/middleware/stack.rb
121
144
  - lib/gritz/router.rb
145
+ - lib/gritz/supervisor/admin_server.rb
146
+ - lib/gritz/supervisor/launcher.rb
122
147
  - lib/gritz/supervisor/master.rb
123
148
  - lib/gritz/supervisor/signal_queue.rb
124
149
  - lib/gritz/supervisor/status_channel.rb