Class: Gritz::Configuration

Inherits:
Object
  • Object
show all
Defined in:
/home/runner/work/gritz/gritz/sources/gritz-core/lib/gritz/configuration.rb,
tmp/docs-input/dynamic_api.rb

Overview

Validated startup settings for single-process and supervised servers.

Constant Summary collapse

DEFAULTS =
{
  workers: 0, threads: 16, max_waiting_requests: 64,
  transport: :native, listener_strategy: :reuseport,
  bind: "0.0.0.0:50051", admin_bind: "127.0.0.1:9090",
  fork_mode: :clean, fork_guard: :raise, strict_routes: false, reflection: false,
  drain_delay: 5.0, shutdown_timeout: 25.0, worker_boot_timeout: 60.0,
  worker_timeout: 30.0, status_interval: 1.0, min_ready_workers: 1,
  phased_restart_surge: 1, max_connection_age: 300.0,
  max_connection_age_grace: 30.0, keepalive_time: 60.0,
  keepalive_permit_without_calls: true,
  max_receive_message_size: 4 * 1024 * 1024,
  max_send_message_size: 4 * 1024 * 1024, max_metadata_size: 8192,
  metrics_backend: :pipe, log_format: :json, log_redact: [], worker_recycle: {}, tls: {},
  pid_file: "", reexec_timeout: 60.0
}.freeze
ENUMS =
{
  transport: %i[native async], listener_strategy: %i[reuseport inherited_fd port_per_worker],
  fork_mode: %i[clean grpc_fork_support], fork_guard: %i[raise warn off],
  metrics_backend: %i[pipe otlp mmap], log_format: %i[json logfmt]
}.freeze
HOOKS =
%i[before_fork on_worker_boot on_worker_shutdown].freeze

Instance Attribute Summary collapse

Class Method Summary collapse

Instance Method Summary collapse

Constructor Details

#initialize ⇒ Configuration

Returns a new instance of Configuration.



34
35
36
37
38
39
40
41
42
# File '/home/runner/work/gritz/gritz/sources/gritz-core/lib/gritz/configuration.rb', line 34

def initialize
  DEFAULTS.each { |name, value| public_send("#{name}=", value.dup) }
  @controllers = []
  @middleware = Middleware::Stack.default
  @hooks = HOOKS.to_h { |name| [name, []] }
  @health_checks = {}
  @preload_app = false
  @preloaders = []
end

Instance Attribute Details

#admin_bind ⇒ Object



16
17
18
# File 'tmp/docs-input/dynamic_api.rb', line 16

def admin_bind
  @admin_bind
end

#bind ⇒ Object



14
15
16
# File 'tmp/docs-input/dynamic_api.rb', line 14

def bind
  @bind
end

#controllers ⇒ Object



68
69
70
# File 'tmp/docs-input/dynamic_api.rb', line 68

def controllers
  @controllers
end

#drain_delay ⇒ Object



26
27
28
# File 'tmp/docs-input/dynamic_api.rb', line 26

def drain_delay
  @drain_delay
end

#fork_guard ⇒ Object



20
21
22
# File 'tmp/docs-input/dynamic_api.rb', line 20

def fork_guard
  @fork_guard
end

#fork_mode ⇒ Object



18
19
20
# File 'tmp/docs-input/dynamic_api.rb', line 18

def fork_mode
  @fork_mode
end

#health_checks ⇒ Object (readonly)



32
33
34
# File '/home/runner/work/gritz/gritz/sources/gritz-core/lib/gritz/configuration.rb', line 32

def health_checks
  @health_checks
end

#keepalive_permit_without_calls ⇒ Object



46
47
48
# File 'tmp/docs-input/dynamic_api.rb', line 46

def keepalive_permit_without_calls
  @keepalive_permit_without_calls
end

#keepalive_time ⇒ Object



44
45
46
# File 'tmp/docs-input/dynamic_api.rb', line 44

def keepalive_time
  @keepalive_time
end

#listener_strategy ⇒ Object



12
13
14
# File 'tmp/docs-input/dynamic_api.rb', line 12

def listener_strategy
  @listener_strategy
end

#log_format ⇒ Object



56
57
58
# File 'tmp/docs-input/dynamic_api.rb', line 56

def log_format
  @log_format
end

#log_redact ⇒ Object



58
59
60
# File 'tmp/docs-input/dynamic_api.rb', line 58

def log_redact
  @log_redact
end

#max_connection_age ⇒ Object



40
41
42
# File 'tmp/docs-input/dynamic_api.rb', line 40

def max_connection_age
  @max_connection_age
end

#max_connection_age_grace ⇒ Object



42
43
44
# File 'tmp/docs-input/dynamic_api.rb', line 42

def max_connection_age_grace
  @max_connection_age_grace
end

#max_metadata_size ⇒ Object



52
53
54
# File 'tmp/docs-input/dynamic_api.rb', line 52

def 
  @max_metadata_size
end

#max_receive_message_size ⇒ Object



48
49
50
# File 'tmp/docs-input/dynamic_api.rb', line 48

def max_receive_message_size
  @max_receive_message_size
end

#max_send_message_size ⇒ Object



50
51
52
# File 'tmp/docs-input/dynamic_api.rb', line 50

def max_send_message_size
  @max_send_message_size
end

#max_waiting_requests ⇒ Object



8
9
10
# File 'tmp/docs-input/dynamic_api.rb', line 8

def max_waiting_requests
  @max_waiting_requests
end

#metrics_backend ⇒ Object



54
55
56
# File 'tmp/docs-input/dynamic_api.rb', line 54

def metrics_backend
  @metrics_backend
end

#metrics_recorder_factory ⇒ Object



74
75
76
# File 'tmp/docs-input/dynamic_api.rb', line 74

def metrics_recorder_factory
  @metrics_recorder_factory
end

#middleware ⇒ Object



70
71
72
# File 'tmp/docs-input/dynamic_api.rb', line 70

def middleware
  @middleware
end

#min_ready_workers ⇒ Object



36
37
38
# File 'tmp/docs-input/dynamic_api.rb', line 36

def min_ready_workers
  @min_ready_workers
end

#phased_restart_surge ⇒ Object



38
39
40
# File 'tmp/docs-input/dynamic_api.rb', line 38

def phased_restart_surge
  @phased_restart_surge
end

#pid_file ⇒ Object



64
65
66
# File 'tmp/docs-input/dynamic_api.rb', line 64

def pid_file
  @pid_file
end

#preload_app ⇒ Object



72
73
74
# File 'tmp/docs-input/dynamic_api.rb', line 72

def preload_app
  @preload_app
end

#reexec_timeout ⇒ Object



66
67
68
# File 'tmp/docs-input/dynamic_api.rb', line 66

def reexec_timeout
  @reexec_timeout
end

#reflection ⇒ Object



24
25
26
# File 'tmp/docs-input/dynamic_api.rb', line 24

def reflection
  @reflection
end

#shutdown_timeout ⇒ Object



28
29
30
# File 'tmp/docs-input/dynamic_api.rb', line 28

def shutdown_timeout
  @shutdown_timeout
end

#status_interval ⇒ Object



34
35
36
# File 'tmp/docs-input/dynamic_api.rb', line 34

def status_interval
  @status_interval
end

#strict_routes ⇒ Object



22
23
24
# File 'tmp/docs-input/dynamic_api.rb', line 22

def strict_routes
  @strict_routes
end

#threads ⇒ Object



6
7
8
# File 'tmp/docs-input/dynamic_api.rb', line 6

def threads
  @threads
end

#tls ⇒ Object



62
63
64
# File 'tmp/docs-input/dynamic_api.rb', line 62

def tls
  @tls
end

#transport ⇒ Object



10
11
12
# File 'tmp/docs-input/dynamic_api.rb', line 10

def transport
  @transport
end

#worker_boot_timeout ⇒ Object



30
31
32
# File 'tmp/docs-input/dynamic_api.rb', line 30

def worker_boot_timeout
  @worker_boot_timeout
end

#worker_recycle ⇒ Object



60
61
62
# File 'tmp/docs-input/dynamic_api.rb', line 60

def worker_recycle
  @worker_recycle
end

#worker_timeout ⇒ Object



32
33
34
# File 'tmp/docs-input/dynamic_api.rb', line 32

def worker_timeout
  @worker_timeout
end

#workers ⇒ Object



4
5
6
# File 'tmp/docs-input/dynamic_api.rb', line 4

def workers
  @workers
end

Class Method Details

.load(path: nil, env: ENV, overrides: {}) ⇒ Object

CLI overrides > environment > configuration file > defaults.



45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
# File '/home/runner/work/gritz/gritz/sources/gritz-core/lib/gritz/configuration.rb', line 45

def self.load(path: nil, env: ENV, overrides: {})
  config = new
  DSL.new(config).evaluate(path) if path
  env.each do |key, value|
    next unless key.start_with?("GRITZ_")
    next if key == "GRITZ_RELEASE" # Dependency selection in the release workflow, not a runtime setting.

    name = key.delete_prefix("GRITZ_").downcase.to_sym
    raise ConfigurationError, "Unknown environment setting #{key}" unless DEFAULTS.key?(name)
    next if overrides.key?(name)

    config.public_send("#{name}=", parse_environment(name, value))
  end
  overrides.each { |name, value| config.public_send("#{name}=", value) }
  config.validate!
end

.parse_environment(name, value) ⇒ Object



62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
# File '/home/runner/work/gritz/gritz/sources/gritz-core/lib/gritz/configuration.rb', line 62

def self.parse_environment(name, value)
  case DEFAULTS.fetch(name)
  when Integer then Integer(value, 10)
  when Float then Float(value)
  when Symbol then value.to_sym
  when Hash, Array then JSON.parse(value, symbolize_names: true)
  when true, false
    return true if %w[true 1].include?(value)
    return false if %w[false 0].include?(value)

    raise ArgumentError, "expected true or false"
  else value
  end
rescue ArgumentError, TypeError, JSON::ParserError => e
  raise ConfigurationError, "Invalid GRITZ_#{name.to_s.upcase}: #{e.message}"
end

Instance Method Details

#add_hook(name, &block) ⇒ Object

Raises:



159
160
161
162
163
# File '/home/runner/work/gritz/gritz/sources/gritz-core/lib/gritz/configuration.rb', line 159

def add_hook(name, &block)
  raise ConfigurationError, "#{name} requires a block" unless block

  @hooks.fetch(name) << block
end

#add_preloader(&block) ⇒ Object



149
150
151
152
# File '/home/runner/work/gritz/gritz/sources/gritz-core/lib/gritz/configuration.rb', line 149

def add_preloader(&block)
  @preload_app = true
  @preloaders << block if block
end

#preload! ⇒ Object



154
155
156
157
# File '/home/runner/work/gritz/gritz/sources/gritz-core/lib/gritz/configuration.rb', line 154

def preload!
  @preloaders.each(&:call)
  Core::LOADER.eager_load
end

#preload_app? ⇒ Boolean

Returns:

  • (Boolean)


115
# File '/home/runner/work/gritz/gritz/sources/gritz-core/lib/gritz/configuration.rb', line 115

def preload_app? = preload_app

#run_hooks(name, *args) ⇒ Object



165
166
167
# File '/home/runner/work/gritz/gritz/sources/gritz-core/lib/gritz/configuration.rb', line 165

def run_hooks(name, *args)
  @hooks.fetch(name).each { |hook| hook.call(*args) }
end

#server_args ⇒ Object



169
170
171
172
173
174
175
176
177
178
179
180
# File '/home/runner/work/gritz/gritz/sources/gritz-core/lib/gritz/configuration.rb', line 169

def server_args
  {
    "grpc.so_reuseport" => listener_strategy == :reuseport ? 1 : 0,
    "grpc.max_connection_age_ms" => (max_connection_age * 1000).to_i,
    "grpc.max_connection_age_grace_ms" => (max_connection_age_grace * 1000).to_i,
    "grpc.keepalive_time_ms" => (keepalive_time * 1000).to_i,
    "grpc.keepalive_permit_without_calls" => keepalive_permit_without_calls ? 1 : 0,
    "grpc.max_receive_message_length" => max_receive_message_size,
    "grpc.max_send_message_length" => max_send_message_size,
    "grpc.max_metadata_size" => 
  }
end

#validate! ⇒ Object

Raises:



79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
# File '/home/runner/work/gritz/gritz/sources/gritz-core/lib/gritz/configuration.rb', line 79

def validate!
  DEFAULTS.each do |name, default|
    value = public_send(name)
    valid = case default
            when Integer then value.is_a?(Integer) && value.between?(name == :workers ? 0 : 1, 2_147_483_647)
            when Float then value.is_a?(Numeric) && value.real? && value.finite? && value.between?(0, 2_147_483.647)
            when true, false then [true, false].include?(value)
            else value.is_a?(default.class)
            end
    raise ConfigurationError, "Invalid #{name}: #{value.inspect}" unless valid
  end
  %i[shutdown_timeout worker_boot_timeout worker_timeout status_interval reexec_timeout].each do |name|
    raise ConfigurationError, "#{name} must be positive" unless public_send(name).positive?
  end
  ENUMS.each do |name, allowed|
    raise ConfigurationError, "#{name} must be one of #{allowed.join(', ')}" unless allowed.include?(public_send(name))
  end
  validate_addresses!
  validate_recycle!
  validate_tls!
  raise ConfigurationError, "controllers must be an Array of classes" unless controllers.is_a?(Array) && controllers.all?(Class)
  raise ConfigurationError, "middleware must be a Stack" unless middleware.is_a?(Middleware::Stack)
  if metrics_recorder_factory && !metrics_recorder_factory.respond_to?(:call)
    raise ConfigurationError, "metrics recorder factory must be callable"
  end
  raise ConfigurationError, "preload_app must be boolean" unless [true, false].include?(preload_app)
  unless log_redact.all? { |name| name.is_a?(String) || name.is_a?(Symbol) }
    raise ConfigurationError, "log_redact must contain metadata names"
  end
  unless health_checks.all? { |name, check| (name.is_a?(String) || name.is_a?(Symbol)) && check.respond_to?(:call) }
    raise ConfigurationError, "health_checks must contain named callbacks"
  end

  self
end

#validate_runtime! ⇒ Object

Fail before allocating transport resources for unsupported release features.

Raises:



118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
# File '/home/runner/work/gritz/gritz/sources/gritz-core/lib/gritz/configuration.rb', line 118

def validate_runtime!
  validate!
  supported = transport == :native ? %i[reuseport] : %i[reuseport inherited_fd]
  raise ConfigurationError, "#{transport} does not support listener_strategy #{listener_strategy}" unless supported.include?(listener_strategy)
  if transport != :native && fork_mode == :grpc_fork_support
    raise ConfigurationError, "grpc_fork_support requires transport :native"
  end
  unless metrics_backend == :pipe || (metrics_backend == :otlp && metrics_recorder_factory)
    raise ConfigurationError, "metrics_backend requires an installed recorder (:pipe is built in)"
  end
  raise ConfigurationError, "This release supports phased_restart_surge 1" unless phased_restart_surge == 1

  unless worker_recycle.empty?
    raise ConfigurationError, "worker_recycle requires workers > 0" unless workers.positive?
    if bind.end_with?(":0") && listener_strategy != :inherited_fd
      raise ConfigurationError, "worker_recycle requires a fixed bind port"
    end
  end
  raise ConfigurationError, "grpc_fork_support requires workers > 0" if workers.zero? && fork_mode != :clean
  raise ConfigurationError, "at least one controller must be registered" if controllers.empty?

  self
end

#validate_single_process! ⇒ Object

Raises:



142
143
144
145
146
147
# File '/home/runner/work/gritz/gritz/sources/gritz-core/lib/gritz/configuration.rb', line 142

def validate_single_process!
  validate_runtime!
  raise ConfigurationError, "Testing::Server requires workers 0; use Testing::Cluster for supervised servers" unless workers.zero?

  self
end