gritz-core 0.4.0 → 0.5.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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 600c0b33c1d7f7008866ee20d789504559715612886cd3f8dc33413e5192cb36
4
- data.tar.gz: c3797690d572a4d29f5275946b16f84b5717fb19e4f31fd6a20217ece840eb92
3
+ metadata.gz: ed1b7320b17241d7bf44746aff7108965c5bd4ef5c5acfeb26bade59786c5775
4
+ data.tar.gz: 062fa21a3446621c20990013a0bb7a6dd23c38803d5e58597902f83079dba21c
5
5
  SHA512:
6
- metadata.gz: f2858b3d8e39c28d309a3acce2255fcc42ec4aba071b2b79735025a840973ffe0016183204eb7e87c67b1a14f11fef48ffa9e6560234cfd5da0866ccd195f68f
7
- data.tar.gz: 149a97e113197dc7691152d5aa0f7e246adb55cc0bb90a699b122078ea3272cd32787461808b78d7379804c83a487866f503d8eea7f135a5c356c011e40a2209
6
+ metadata.gz: 0f4702d111e9e72546b8a0a219703e355fba71a7ecbcd064d7c2fca60461789ee8c5c03dc98d050d843302f71506e9a9169bc88c9c8d73671231ff8ca8ca6035
7
+ data.tar.gz: 3fc17735fed2dc9068e54aae5853d6595d344458cb535eaa3be2ec8a63d2cd1c5d1f546175183639b3fa6f3309506bfa8d3041b91211456982023b187b6cb1a7
data/CHANGELOG.md CHANGED
@@ -1,5 +1,15 @@
1
1
  # Changelog
2
2
 
3
+ ## 0.5.0
4
+
5
+ - Migrate Gruf controllers and server interceptors through `Gritz::Compat::Gruf`, with a configuration conversion tool.
6
+ - Configure optional gRPC Reflection through the `reflection` setting.
7
+ - Avoid unnecessary supervisor snapshot allocation when status consumers are paused.
8
+ - Publish worker metrics at `status_interval` to reduce supervisor overhead; retain retries and final shutdown totals.
9
+ - Eagerly load the transport-independent framework before application warmup so forked workers can share its memory.
10
+ - Send supervisor snapshots at `status_interval`, with immediate lifecycle and health changes, to reduce parent-process allocation.
11
+ - Reuse signal, status and admin read buffers to avoid allocating memory on every idle poll.
12
+
3
13
  ## 0.4.0
4
14
 
5
15
  - Define lazy, fork-safe clients with process-local shared connections, per-call middleware, deadline propagation and selected request headers.
data/README.md CHANGED
@@ -14,6 +14,8 @@ Supervised servers support phased replacement, fresh-interpreter hot reexec, req
14
14
 
15
15
  `Gritz::Client.define` creates lazy, fork-safe client constants with deadline propagation, selected request headers and middleware around complete streams. See the [client guide](docs/guides/clients.md). Optional [gritz-otel](https://github.com/gritzrpc/gritz-otel) adds server/client spans and worker OTLP metrics after fork.
16
16
 
17
+ The optional `Gritz::Compat::Gruf` module adapts Gruf controllers, returned response streams and server interceptors. `gritz-migrate-gruf` converts supported static configuration without executing the source file. See the [Gruf migration guide](docs/guides/migrating-from-gruf.md). Add [gritz-rails](https://github.com/gritzrpc/gritz-rails) for Rails execution, generators and development reloading.
18
+
17
19
  ## Development
18
20
 
19
21
  ```sh
@@ -0,0 +1,25 @@
1
+ #!/usr/bin/env ruby
2
+ # frozen_string_literal: true
3
+
4
+ require "gritz/core"
5
+ require "optparse"
6
+
7
+ begin
8
+ destination = nil
9
+ parser = OptionParser.new do |options|
10
+ options.banner = "Usage: gritz-migrate-gruf SOURCE [--output config/gritz.rb]"
11
+ options.on("--output PATH", "Write a new file; existing files are never overwritten") { |path| destination = path }
12
+ end
13
+ parser.parse!
14
+ raise OptionParser::MissingArgument, "SOURCE" unless ARGV.size == 1
15
+
16
+ output = Gritz::Compat::Gruf::ConfigConverter.new.convert(File.read(ARGV.first))
17
+ if destination
18
+ File.open(destination, File::WRONLY | File::CREAT | File::EXCL) { |file| file.write(output) }
19
+ else
20
+ $stdout.write(output)
21
+ end
22
+ rescue Gritz::ConfigurationError, OptionParser::ParseError, SystemCallError => e
23
+ warn "gritz-migrate-gruf: #{e.message}"
24
+ exit 1
25
+ end
@@ -0,0 +1,159 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "ripper"
4
+
5
+ module Gritz
6
+ module Compat
7
+ module Gruf
8
+ # Converts a deliberately small, literal-only subset of Gruf.configure.
9
+ # @api public
10
+ class ConfigConverter
11
+ ARGUMENTS = {
12
+ "grpc.max_receive_message_length" => [:max_receive_message_size, 1],
13
+ "grpc.max_send_message_length" => [:max_send_message_size, 1],
14
+ "grpc.max_metadata_size" => [:max_metadata_size, 1],
15
+ "grpc.max_connection_age_ms" => [:max_connection_age, 1000.0],
16
+ "grpc.max_connection_age_grace_ms" => [:max_connection_age_grace, 1000.0],
17
+ "grpc.keepalive_time_ms" => [:keepalive_time, 1000.0]
18
+ }.freeze
19
+
20
+ # @param source [String] Ruby source containing one static Gruf.configure block
21
+ # @return [String] Gritz configuration requiring manual review before use
22
+ # @raise [Gritz::ConfigurationError] when a setting or expression cannot be converted
23
+ # @api public
24
+ def convert(source)
25
+ settings = assignments(source)
26
+ output = { workers: 0 }
27
+ settings.each do |name, value|
28
+ case name
29
+ when "server_binding_url" then output[:bind] = value
30
+ when "rpc_server_options" then server_options(value, output)
31
+ when "use_ssl", "ssl_crt_file", "ssl_key_file" then next
32
+ when "default_client_host", "default_channel_credentials"
33
+ unsupported("#{name}: configure Gritz::Client.define manually")
34
+ else unsupported("#{name}: manual migration required; see migrating-from-gruf.md")
35
+ end
36
+ end
37
+ config = Gritz::Configuration.new
38
+ output.each { |name, value| config.public_send("#{name}=", value) }
39
+ config.validate!
40
+ tls = tls_options(settings)
41
+ output[:tls] = tls unless tls.empty?
42
+ ["# Review controller registration, interceptors and fork safety before starting.",
43
+ 'require "gritz/compat/gruf"', "", *output.map { |name, value| "#{name}(#{value.inspect})" }, "",
44
+ "# register_controller YourMigratedController", ""].join("\n")
45
+ end
46
+
47
+ private
48
+
49
+ def assignments(source)
50
+ tree = Ripper.sexp(source)
51
+ unsupported("invalid Ruby syntax; manual correction required") unless tree
52
+ statements = tree[1].reject { |node| node[0] == :void_stmt }
53
+ unsupported("expected one Gruf.configure block; other executable code needs manual migration") unless statements.size == 1
54
+ node = statements.first
55
+ call = node[1]
56
+ block = node[2]
57
+ unless node[0] == :method_add_block && call&.dig(0) == :call && call.dig(3, 1) == "configure" &&
58
+ %i[var_ref top_const_ref].include?(call.dig(1, 0)) && call.dig(1, 1, 1) == "Gruf"
59
+ unsupported("expected Gruf.configure; manual migration required")
60
+ end
61
+ params = block&.dig(1, 1)
62
+ unless %i[do_block brace_block].include?(block&.dig(0)) && params&.dig(0) == :params &&
63
+ params[1]&.size == 1 && params[2..].all?(&:nil?) && block.dig(1, 2) == false
64
+ unsupported("configure needs one block variable; manual migration required")
65
+ end
66
+ variable = params[1][0][1]
67
+ body = block[2]
68
+ if block[0] == :do_block
69
+ unsupported("rescue/ensure needs manual migration") unless body[2..].all?(&:nil?)
70
+ body = body[1]
71
+ end
72
+ body.each_with_object({}) do |statement, values|
73
+ next if statement[0] == :void_stmt
74
+
75
+ field = statement[1]
76
+ unless statement[0] == :assign && field&.dig(0) == :field && field.dig(1, 0) == :var_ref && field.dig(1, 1, 1) == variable
77
+ unsupported("only literal assignments are supported; interceptors and other code require manual migration")
78
+ end
79
+ name = field[3][1]
80
+ unsupported("duplicate setting #{name}; manual migration required") if values.key?(name)
81
+ values[name] = literal(statement[2])
82
+ end
83
+ end
84
+
85
+ def literal(node)
86
+ case node[0]
87
+ when :@int then Integer(node[1].delete("_"), 0)
88
+ when :@float then Float(node[1].delete("_"))
89
+ when :string_literal
90
+ parts = node[1][1..]
91
+ unsupported("strings must be literals without interpolation or escapes") unless parts.all? { |part|
92
+ part[0] == :@tstring_content && !part[1].include?("\\")
93
+ }
94
+ parts.map { |part| part[1] }.join
95
+ when :symbol_literal
96
+ token = node.dig(1, 1)
97
+ unsupported("symbol must be a static literal") unless %i[@ident @op @const].include?(token&.dig(0))
98
+ token[1].to_sym
99
+ when :@label then node[1].delete_suffix(":").to_sym
100
+ when :var_ref
101
+ return { "true" => true, "false" => false, "nil" => nil }.fetch(node[1][1]) if node[1][0] == :@kw
102
+
103
+ unsupported("dynamic values need manual migration; use static literals")
104
+ when :unary
105
+ unsupported("only negative numeric literals are supported") unless node[1] == :-@ && %i[@int @float].include?(node.dig(2, 0))
106
+ -literal(node[2])
107
+ when :hash
108
+ pairs = node.dig(1, 1) || []
109
+ pairs.each_with_object({}) do |pair, values|
110
+ unsupported("hash splats need manual migration; use literal entries") unless pair[0] == :assoc_new
111
+ key = literal(pair[1])
112
+ unsupported("duplicate hash key #{key}; manual migration required") if values.key?(key)
113
+ values[key] = literal(pair[2])
114
+ end
115
+ else unsupported("dynamic values need manual migration; only static literals are supported")
116
+ end
117
+ rescue ArgumentError, KeyError
118
+ unsupported("invalid literal; manual migration required")
119
+ end
120
+
121
+ def server_options(options, output)
122
+ unsupported("rpc_server_options must be a literal Hash") unless options.is_a?(Hash)
123
+ options.each do |name, value|
124
+ case name
125
+ when :pool_size then output[:threads] = value
126
+ when :max_waiting_requests then output[:max_waiting_requests] = value
127
+ when :server_args
128
+ unsupported("server_args must be a literal Hash") unless value.is_a?(Hash)
129
+ value.each do |key, argument|
130
+ conversion = ARGUMENTS[key]
131
+ unsupported("#{key}: manual C-core argument migration required") unless conversion
132
+ unsupported("#{key} must be a numeric literal") unless argument.is_a?(Numeric)
133
+ setting, divisor = conversion
134
+ output[setting] = argument / divisor
135
+ end
136
+ else unsupported("#{name}: manual rpc_server_options migration required")
137
+ end
138
+ end
139
+ end
140
+
141
+ def tls_options(settings)
142
+ enabled = settings.fetch("use_ssl", false)
143
+ unsupported("use_ssl must be a boolean literal") unless [true, false].include?(enabled)
144
+ paths = %w[ssl_crt_file ssl_key_file]
145
+ unless enabled
146
+ paths.each { |key| unsupported("#{key} requires use_ssl=true; manual review required") if settings.key?(key) }
147
+ return {}
148
+ end
149
+ unless paths.all? { |key| settings[key].is_a?(String) && !settings[key].empty? }
150
+ unsupported("use_ssl=true requires ssl_crt_file and ssl_key_file; manual review required")
151
+ end
152
+ { cert: settings.fetch("ssl_crt_file"), key: settings.fetch("ssl_key_file") }
153
+ end
154
+
155
+ def unsupported(message) = raise Gritz::ConfigurationError, message
156
+ end
157
+ end
158
+ end
159
+ end
@@ -0,0 +1,201 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "gritz/core"
4
+ require "json"
5
+
6
+ module Gritz
7
+ module Compat
8
+ # Selected Gruf 2.22 controller/interceptor APIs, without loading Gruf or grpc.
9
+ # @api public
10
+ module Gruf
11
+ REQUEST_KEY = Object.new.freeze
12
+
13
+ # @api private
14
+ def self.request_for(context)
15
+ context.store[REQUEST_KEY] ||= Request.new(context)
16
+ end
17
+
18
+ # Wraps a Gruf-style interceptor as a Gritz server middleware.
19
+ # @param klass [Class] interceptor implementing #call and yielding to the action
20
+ # @return [Class] middleware accepted by Middleware::Stack#use
21
+ # @api public
22
+ def self.interceptor(klass)
23
+ Class.new(InterceptorAdapter) do
24
+ define_method(:initialize) { |app, **options| super(app, klass, options) }
25
+ end
26
+ end
27
+
28
+ class RequestContext < Hash
29
+ def [](key) = super(convert_key(key))
30
+
31
+ def []=(key, value)
32
+ super(convert_key(key), value)
33
+ end
34
+
35
+ def fetch(key, ...) = super(convert_key(key), ...)
36
+ def key?(key) = super(convert_key(key))
37
+ def merge!(other, &) = super(other.transform_keys { |key| convert_key(key) }, &)
38
+ alias update merge!
39
+
40
+ private
41
+
42
+ def convert_key(key) = key.is_a?(Symbol) ? key.to_s : key
43
+ end
44
+
45
+ class ActiveCall
46
+ def initialize(context) = @context = context
47
+ def metadata = @context.metadata
48
+ def deadline = @context.deadline
49
+ def peer = @context.peer
50
+ def cancelled? = @context.cancelled?
51
+ def output_metadata = @context.call.trailing_metadata
52
+ end
53
+
54
+ class Request
55
+ attr_reader :context, :active_call, :error
56
+
57
+ def initialize(context)
58
+ @rpc_context = context
59
+ @original = Gritz::Controller::Request.for(context)
60
+ @context = RequestContext.new
61
+ @active_call = ActiveCall.new(context)
62
+ @error = Error.new
63
+ end
64
+
65
+ def service = @rpc_context.method.service_class
66
+ def method_key = @rpc_context.method.action
67
+ def request_class = @rpc_context.method.input_type
68
+ def response_class = @rpc_context.method.output_type
69
+ def metadata = @rpc_context.metadata
70
+ def type = self
71
+ def client_streamer? = @rpc_context.method.kind == :client_streaming
72
+ def server_streamer? = @rpc_context.method.kind == :server_streaming
73
+ def bidi_streamer? = @rpc_context.method.kind == :bidi
74
+ def request_response? = @rpc_context.method.kind == :unary
75
+
76
+ def service_key
77
+ name = service.name || @rpc_context.method.service
78
+ name.gsub(/([A-Z]+)([A-Z][a-z])/, '\1_\2').gsub(/([a-z\d])([A-Z])/, '\1_\2')
79
+ .gsub("::", ".").downcase.delete_suffix(".service")
80
+ end
81
+
82
+ def method_name = "#{service_key}.#{method_key}"
83
+
84
+ def message
85
+ return @original.message unless @rpc_context.method.client_streaming?
86
+
87
+ @message ||= client_streamer? ? proc { |&block| @original.each_message(&block) } : @original.each_message
88
+ end
89
+
90
+ def messages(&block)
91
+ return [message] unless @rpc_context.method.client_streaming?
92
+
93
+ block ? @original.each_message(&block) : @original.each_message
94
+ end
95
+ end
96
+
97
+ class Error
98
+ Field = Struct.new(:field_name, :error_code, :message)
99
+ DebugInfo = Struct.new(:detail, :stack_trace)
100
+ attr_accessor :code, :app_code, :message
101
+ attr_reader :field_errors, :debug_info, :metadata
102
+
103
+ def initialize
104
+ @field_errors = []
105
+ @metadata = {}
106
+ end
107
+
108
+ def add_field_error(field, code, message = "") = @field_errors << Field.new(field, code, message)
109
+ def has_field_errors? = !@field_errors.empty? # rubocop:disable Naming/PredicatePrefix -- Preserve the upstream Gruf method name.
110
+
111
+ def set_debug_info(detail, stack_trace = [])
112
+ @debug_info = DebugInfo.new(detail, stack_trace.is_a?(String) ? stack_trace.split("\n") : Array(stack_trace))
113
+ end
114
+
115
+ def metadata=(value)
116
+ @metadata = value.transform_values(&:to_s)
117
+ end
118
+
119
+ def to_h
120
+ { code:, app_code:, message:, field_errors: field_errors.map(&:to_h), debug_info: debug_info.to_h }
121
+ end
122
+
123
+ def fail!(_active_call = nil)
124
+ canonical = { bad_request: :invalid_argument, unauthorized: :permission_denied }.fetch(code, code)
125
+ trailers = metadata.merge("error-internal-bin" => JSON.generate(to_h))
126
+ raise Gritz::Errors.for_code(canonical).new(message, metadata: trailers)
127
+ end
128
+ end
129
+
130
+ module ErrorHelpers
131
+ def add_field_error(...) = error.add_field_error(...)
132
+ def has_field_errors? = error.has_field_errors? # rubocop:disable Naming/PredicatePrefix -- Preserve the upstream Gruf method name.
133
+ def set_debug_info(...) = error.set_debug_info(...)
134
+
135
+ def fail!(code, app_code = nil, message = "", metadata = {})
136
+ error.code = code.to_sym
137
+ error.app_code = app_code ? app_code.to_sym : error.code
138
+ error.message = message.to_s
139
+ error.metadata = metadata
140
+ error.fail!(request.active_call)
141
+ end
142
+ end
143
+
144
+ # Controller base retaining selected Gruf request, error and streaming APIs.
145
+ # @api public
146
+ class Controller < Gritz::Controller
147
+ include ErrorHelpers
148
+
149
+ def self.bound_service = service_class
150
+
151
+ def self.action_defined?(action)
152
+ super && ![Controller, ErrorHelpers].include?(instance_method(action).owner)
153
+ end
154
+
155
+ def initialize(context:)
156
+ super
157
+ @request = Gruf.request_for(context)
158
+ end
159
+
160
+ def error = request.error
161
+
162
+ def process(action)
163
+ result = super
164
+ return result unless context.method.server_streaming?
165
+
166
+ result.each { |message| stream.write(message) } if result.respond_to?(:each)
167
+ nil
168
+ end
169
+ end
170
+
171
+ Base = Controller
172
+
173
+ # Base for migrated interceptors whose #call yields to the next handler.
174
+ # @api public
175
+ class ServerInterceptor
176
+ include ErrorHelpers
177
+
178
+ attr_reader :request, :error, :options
179
+
180
+ def initialize(request, error, options = {})
181
+ @request = request
182
+ @error = error
183
+ @options = options
184
+ end
185
+ end
186
+
187
+ class InterceptorAdapter
188
+ def initialize(app, klass, options)
189
+ @app = app
190
+ @klass = klass
191
+ @options = options
192
+ end
193
+
194
+ def call(context)
195
+ request = Gruf.request_for(context)
196
+ @klass.new(request, request.error, @options.dup).call { @app.call(context) }
197
+ end
198
+ end
199
+ end
200
+ end
201
+ end
@@ -10,7 +10,7 @@ module Gritz
10
10
  workers: 0, threads: 16, max_waiting_requests: 64,
11
11
  transport: :native, listener_strategy: :reuseport,
12
12
  bind: "0.0.0.0:50051", admin_bind: "127.0.0.1:9090",
13
- fork_mode: :clean, fork_guard: :raise, strict_routes: false,
13
+ fork_mode: :clean, fork_guard: :raise, strict_routes: false, reflection: false,
14
14
  drain_delay: 5.0, shutdown_timeout: 25.0, worker_boot_timeout: 60.0,
15
15
  worker_timeout: 30.0, status_interval: 1.0, min_ready_workers: 1,
16
16
  phased_restart_surge: 1, max_connection_age: 300.0,
@@ -148,6 +148,7 @@ module Gritz
148
148
 
149
149
  def preload!
150
150
  @preloaders.each(&:call)
151
+ Core::LOADER.eager_load
151
152
  end
152
153
 
153
154
  def add_hook(name, &block)
@@ -42,7 +42,7 @@ module Gritz
42
42
 
43
43
  def initialize(context:)
44
44
  @context = context
45
- @request = Request.new(context.call, context)
45
+ @request = Request.for(context)
46
46
  @stream = Stream.new(context.call, context)
47
47
  end
48
48
 
@@ -93,6 +93,13 @@ module Gritz
93
93
 
94
94
  # @api public
95
95
  class Request
96
+ STORE_KEY = Object.new.freeze
97
+
98
+ # @api private
99
+ def self.for(context)
100
+ context.store[STORE_KEY] ||= new(context.call, context)
101
+ end
102
+
96
103
  def initialize(call, context)
97
104
  @call = call
98
105
  @context = context
@@ -2,6 +2,6 @@
2
2
 
3
3
  module Gritz
4
4
  module Core
5
- VERSION = "0.4.0"
5
+ VERSION = "0.5.0"
6
6
  end
7
7
  end
@@ -23,6 +23,7 @@ module Gritz
23
23
  @metrics = metrics
24
24
  @logger = logger
25
25
  @clients = {}
26
+ @read_buffer = +""
26
27
  end
27
28
 
28
29
  def ios = [@io, *@clients.keys]
@@ -68,7 +69,7 @@ module Gritz
68
69
  end
69
70
 
70
71
  def read_request(socket, client)
71
- chunk = socket.read_nonblock(4096, exception: false)
72
+ chunk = socket.read_nonblock(4096, @read_buffer, exception: false)
72
73
  return if chunk == :wait_readable
73
74
  return drop(socket) if chunk.nil?
74
75
 
@@ -49,7 +49,7 @@ module Gritz
49
49
  end
50
50
  update_lifecycle
51
51
  @admin&.poll
52
- @observer&.write(status)
52
+ @observer&.write { status }
53
53
  @observer&.flush
54
54
  break if @stopping && @generations.empty?
55
55
 
@@ -50,7 +50,10 @@ module Gritz
50
50
  advance_replacement
51
51
  maintain_worker_count unless @shutdown_at
52
52
  flush_forwarded
53
- @reports&.write(@owner_channel ? status.merge(type: "status") : status) if @forwarded.empty?
53
+ if @reports && @forwarded.empty? && (!@next_report_at || now >= @next_report_at || @shutdown_at) &&
54
+ @reports.write { @owner_channel ? status.merge(type: "status") : status }
55
+ @next_report_at = now + @config.status_interval
56
+ end
54
57
  @admin&.poll
55
58
  if @owner_channel&.closed?
56
59
  @exit_status = 1
@@ -132,6 +135,7 @@ module Gritz
132
135
  handle = WorkerHandle.new(pid: pid, index: index, status_io: reader)
133
136
  handle.recycle_factor = 1.0 + (rand * @config.worker_recycle.fetch(:jitter, 0.0))
134
137
  @workers[pid] = handle
138
+ @next_report_at = nil
135
139
  @logger.info("Worker #{index} spawned pid=#{pid}")
136
140
  handle
137
141
  rescue StandardError
@@ -159,7 +163,9 @@ module Gritz
159
163
  end
160
164
  next
161
165
  end
166
+ changed = handle.state != message[:state] || (message.key?(:healthy) && handle.stats[:healthy] != message[:healthy])
162
167
  handle.update(message, now: now)
168
+ @next_report_at = nil if changed
163
169
  if message[:state] == "failed" && !@ever_ready
164
170
  @exit_status = 1
165
171
  begin_shutdown
@@ -207,6 +213,7 @@ module Gritz
207
213
  break if handle.channel.closed?
208
214
  end
209
215
  @workers.delete(pid)
216
+ @next_report_at = nil
210
217
  @metrics.forget(handle)
211
218
  handle.close
212
219
  child_status = result.last
@@ -223,10 +230,12 @@ module Gritz
223
230
  break unless @forwarded.empty?
224
231
  rescue Errno::ECHILD
225
232
  @workers.delete(pid)&.close
233
+ @next_report_at = nil
226
234
  end
227
235
  end
228
236
 
229
237
  def handle_signal(signal)
238
+ @next_report_at = nil
230
239
  case signal
231
240
  when "TERM", "INT" then begin_shutdown
232
241
  when "QUIT" then begin_shutdown(immediate: true)
@@ -301,6 +310,7 @@ module Gritz
301
310
  kill(fresh) if fresh
302
311
  @logger.warn("Replacement failed; keeping previous workers")
303
312
  @replacement = nil
313
+ @next_report_at = nil
304
314
  @replacement_queue.clear
305
315
  @recycle_retry_at = now + @config.worker_boot_timeout
306
316
  return true
@@ -314,6 +324,7 @@ module Gritz
314
324
  return if old || !@replacement[:retired]
315
325
 
316
326
  @replacement = nil
327
+ @next_report_at = nil
317
328
  end
318
329
  while (entry = @replacement_queue.shift)
319
330
  old_pid, reason = entry
@@ -10,6 +10,7 @@ module Gritz
10
10
  def initialize(signals:)
11
11
  @io, @writer = IO.pipe
12
12
  @pending = []
13
+ @read_buffer = +""
13
14
  @previous = {}
14
15
  signals.each do |name|
15
16
  @previous[name] = Signal.trap(name) do
@@ -26,7 +27,7 @@ module Gritz
26
27
 
27
28
  def drain
28
29
  loop do
29
- break unless @io.read_nonblock(4096, exception: false).is_a?(String)
30
+ break unless @io.read_nonblock(4096, @read_buffer, exception: false).is_a?(String)
30
31
  end
31
32
  # Swap instead of clearing: a signal arriving here belongs to the next drain.
32
33
  pending = @pending
@@ -16,14 +16,16 @@ module Gritz
16
16
  def initialize(io)
17
17
  @io = io
18
18
  @buffer = +""
19
+ @read_buffer = +""
19
20
  @pending = +""
20
21
  @discarding = false
21
22
  @closed = io.closed?
22
23
  end
23
24
 
24
- def write(status)
25
+ def write(status = nil)
25
26
  return false if closed? || flush_pending.positive?
26
27
 
28
+ status = yield if block_given?
27
29
  line = "#{JSON.generate(status)}\n"
28
30
  return false if line.bytesize > MAX_LINE_BYTES
29
31
 
@@ -54,7 +56,7 @@ module Gritz
54
56
 
55
57
  remaining = max_bytes
56
58
  while remaining.positive?
57
- chunk = @io.read_nonblock([4096, remaining].min, exception: false)
59
+ chunk = @io.read_nonblock([4096, remaining].min, @read_buffer, exception: false)
58
60
  break if chunk == :wait_readable
59
61
 
60
62
  if chunk.nil?
@@ -95,7 +95,7 @@ module Gritz
95
95
  if @pending_reexec && @owner_channel.write(type: "reexec", pid: Process.pid)
96
96
  @pending_reexec = false
97
97
  end
98
- publish_metrics
98
+ publish_metrics if @pending_delta || now >= next_status
99
99
  @admin&.poll
100
100
  if now >= next_status
101
101
  report(@draining ? "draining" : "ready")
metadata CHANGED
@@ -1,11 +1,11 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: gritz-core
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.4.0
4
+ version: 0.5.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Yudai Takada
8
- bindir: bin
8
+ bindir: exe
9
9
  cert_chain: []
10
10
  date: 1980-01-02 00:00:00.000000000 Z
11
11
  dependencies:
@@ -113,19 +113,23 @@ description: Transport-independent controllers, routing, middleware, configurati
113
113
  and testing for Gritz.
114
114
  email:
115
115
  - t.yudai92@gmail.com
116
- executables: []
116
+ executables:
117
+ - gritz-migrate-gruf
117
118
  extensions: []
118
119
  extra_rdoc_files: []
119
120
  files:
120
121
  - CHANGELOG.md
121
122
  - LICENSE.txt
122
123
  - README.md
124
+ - exe/gritz-migrate-gruf
123
125
  - lib/gritz/call.rb
124
126
  - lib/gritz/channel_registry.rb
125
127
  - lib/gritz/cli.rb
126
128
  - lib/gritz/client.rb
127
129
  - lib/gritz/client/call_context.rb
128
130
  - lib/gritz/client/invocation.rb
131
+ - lib/gritz/compat/gruf.rb
132
+ - lib/gritz/compat/gruf/config_converter.rb
129
133
  - lib/gritz/configuration.rb
130
134
  - lib/gritz/configuration_error.rb
131
135
  - lib/gritz/context.rb