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
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Gritz
|
|
4
|
+
module Metrics
|
|
5
|
+
# Master-owned totals survive worker retirement; only replay state is forgotten.
|
|
6
|
+
# @api private
|
|
7
|
+
class Aggregator
|
|
8
|
+
def initialize
|
|
9
|
+
@lock = Mutex.new
|
|
10
|
+
@rpc = {}
|
|
11
|
+
@sequences = {}
|
|
12
|
+
@rejected = 0
|
|
13
|
+
@restarts = Hash.new(0)
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
def apply(worker, envelope)
|
|
17
|
+
unless envelope.is_a?(Hash) && envelope[:seq].is_a?(Integer) && envelope[:seq].positive?
|
|
18
|
+
raise ConfigurationError, "Invalid metric sequence"
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
@lock.synchronize do
|
|
22
|
+
sequence = envelope[:seq]
|
|
23
|
+
previous = @sequences.fetch(worker, 0)
|
|
24
|
+
return false if sequence <= previous
|
|
25
|
+
raise ConfigurationError, "Metric sequence gap: expected #{previous + 1}, got #{sequence}" unless sequence == previous + 1
|
|
26
|
+
|
|
27
|
+
delta = envelope[:delta]
|
|
28
|
+
validate_delta!(delta)
|
|
29
|
+
delta[:rpc].each do |row|
|
|
30
|
+
labels = row.slice(:service, :method, :code)
|
|
31
|
+
aggregate = @rpc[labels.values]
|
|
32
|
+
unless aggregate
|
|
33
|
+
aggregate = Recorder.empty_row(**labels)
|
|
34
|
+
@rpc[aggregate.values_at(:service, :method, :code).freeze] = aggregate
|
|
35
|
+
end
|
|
36
|
+
Recorder::SCALARS.each { |field| aggregate[field] += row[field] }
|
|
37
|
+
Recorder::BUCKET_FIELDS.each do |field|
|
|
38
|
+
row[field].each_with_index { |count, index| aggregate[field][index] += count }
|
|
39
|
+
end
|
|
40
|
+
end
|
|
41
|
+
@rejected += delta[:rejected]
|
|
42
|
+
@sequences[worker] = sequence
|
|
43
|
+
true
|
|
44
|
+
end
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
def forget(worker) = @lock.synchronize { @sequences.delete(worker) }
|
|
48
|
+
|
|
49
|
+
def record_restart(reason:)
|
|
50
|
+
@lock.synchronize { @restarts[reason.to_s] += 1 }
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
def render(workers: [])
|
|
54
|
+
rows = workers.map { |worker| worker.is_a?(Hash) ? worker : worker.stats.merge(pid: worker.pid, index: worker.index, state: worker.state) }
|
|
55
|
+
@lock.synchronize do
|
|
56
|
+
output = +""
|
|
57
|
+
histogram(output, "rpc_server_duration_seconds", Recorder::DURATION_BUCKETS, :duration_buckets, :duration_sum)
|
|
58
|
+
histogram(output, "rpc_server_requests_per_rpc", Recorder::MESSAGE_BUCKETS, :request_buckets, :request_sum)
|
|
59
|
+
histogram(output, "rpc_server_responses_per_rpc", Recorder::MESSAGE_BUCKETS, :response_buckets, :response_sum)
|
|
60
|
+
output << "# TYPE gritz_rejected_total counter\ngritz_rejected_total #{@rejected}\n"
|
|
61
|
+
output << "# TYPE gritz_worker_restarts_total counter\n"
|
|
62
|
+
@restarts.sort.each { |reason, count| output << "gritz_worker_restarts_total{reason=\"#{escape(reason)}\"} #{count}\n" }
|
|
63
|
+
output << "# TYPE gritz_workers gauge\n"
|
|
64
|
+
Supervisor::WorkerHandle::STATES.each { |state| output << "gritz_workers{state=\"#{state}\"} #{rows.count { |row| row[:state] == state }}\n" }
|
|
65
|
+
serving = rows.select { |row| %w[ready draining].include?(row[:state]) }
|
|
66
|
+
output << "# TYPE gritz_threadpool_busy gauge\ngritz_threadpool_busy #{serving.sum { |row| row[:busy_threads] || row[:busy] || 0 }}\n"
|
|
67
|
+
output << "# TYPE gritz_threadpool_capacity gauge\ngritz_threadpool_capacity #{serving.sum { |row| row[:capacity] || 0 }}\n"
|
|
68
|
+
output << "# TYPE gritz_worker_pss_bytes gauge\n"
|
|
69
|
+
rows.each do |row|
|
|
70
|
+
next unless row[:pss_bytes]
|
|
71
|
+
|
|
72
|
+
output << "gritz_worker_pss_bytes{worker=\"#{row[:index]}\",pid=\"#{row[:pid]}\"} #{row[:pss_bytes]}\n"
|
|
73
|
+
end
|
|
74
|
+
output
|
|
75
|
+
end
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
private
|
|
79
|
+
|
|
80
|
+
def validate_delta!(delta)
|
|
81
|
+
valid = delta.is_a?(Hash) && delta[:rpc].is_a?(Array) && nonnegative_integer?(delta[:rejected])
|
|
82
|
+
raise ConfigurationError, "Invalid metric delta" unless valid
|
|
83
|
+
|
|
84
|
+
delta[:rpc].each do |row|
|
|
85
|
+
valid = row.is_a?(Hash) && %i[service method].all? { |key| row[key].is_a?(String) && !row[key].empty? } &&
|
|
86
|
+
row[:code].is_a?(Integer) && row[:code].between?(0, 16) &&
|
|
87
|
+
%i[count request_sum response_sum].all? { |key| nonnegative_integer?(row[key]) } &&
|
|
88
|
+
row[:duration_sum].is_a?(Numeric) && row[:duration_sum].real? && row[:duration_sum].to_f.finite? && row[:duration_sum] >= 0
|
|
89
|
+
raise ConfigurationError, "Invalid RPC metric row" unless valid
|
|
90
|
+
|
|
91
|
+
Recorder::BUCKET_FIELDS.each do |field|
|
|
92
|
+
size = field == :duration_buckets ? Recorder::DURATION_BUCKETS.size : Recorder::MESSAGE_BUCKETS.size
|
|
93
|
+
buckets = row[field]
|
|
94
|
+
unless buckets.is_a?(Array) && buckets.size == size && buckets.all? { |count| nonnegative_integer?(count) } && buckets.sum == row[:count]
|
|
95
|
+
raise ConfigurationError, "Invalid RPC metric histogram"
|
|
96
|
+
end
|
|
97
|
+
end
|
|
98
|
+
end
|
|
99
|
+
end
|
|
100
|
+
|
|
101
|
+
def nonnegative_integer?(value) = value.is_a?(Integer) && value >= 0
|
|
102
|
+
|
|
103
|
+
def histogram(output, name, bounds, bucket_field, sum_field)
|
|
104
|
+
output << "# TYPE #{name} histogram\n"
|
|
105
|
+
@rpc.sort_by { |key, _row| key }.each do |_key, row|
|
|
106
|
+
labels = "rpc_service=\"#{escape(row[:service])}\",rpc_method=\"#{escape(row[:method])}\",rpc_grpc_status_code=\"#{row[:code]}\""
|
|
107
|
+
total = 0
|
|
108
|
+
bounds.each_with_index do |upper, index|
|
|
109
|
+
total += row[bucket_field][index]
|
|
110
|
+
output << "#{name}_bucket{#{labels},le=\"#{upper.infinite? ? '+Inf' : upper}\"} #{total}\n"
|
|
111
|
+
end
|
|
112
|
+
output << "#{name}_sum{#{labels}} #{row[sum_field]}\n#{name}_count{#{labels}} #{row[:count]}\n"
|
|
113
|
+
end
|
|
114
|
+
end
|
|
115
|
+
|
|
116
|
+
def escape(value) = value.gsub(/[\\"\n]/) { |character| { "\\" => "\\\\", '"' => '\\"', "\n" => "\\n" }.fetch(character) }
|
|
117
|
+
end
|
|
118
|
+
end
|
|
119
|
+
end
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "json"
|
|
4
|
+
|
|
5
|
+
module Gritz
|
|
6
|
+
module Metrics
|
|
7
|
+
# Per-worker RPC deltas; only one bounded detached batch is needed by the sender.
|
|
8
|
+
# @api private
|
|
9
|
+
class Recorder
|
|
10
|
+
DURATION_BUCKETS = [0.005, 0.01, 0.025, 0.05, 0.1, 0.25, 0.5, 1, 2.5, 5, 10, Float::INFINITY].freeze
|
|
11
|
+
MESSAGE_BUCKETS = [0, 1, 2, 4, 8, 16, 32, 64, 128, 256, 1024, Float::INFINITY].freeze
|
|
12
|
+
SCALARS = %i[count duration_sum request_sum response_sum].freeze
|
|
13
|
+
BUCKET_FIELDS = %i[duration_buckets request_buckets response_buckets].freeze
|
|
14
|
+
|
|
15
|
+
def initialize
|
|
16
|
+
@lock = Mutex.new
|
|
17
|
+
@rpc = {}
|
|
18
|
+
@last_rejected = @rejected_delta = 0
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
def self.empty_row(service:, method:, code:)
|
|
22
|
+
{ service: service.dup.freeze, method: method.dup.freeze, code: code, count: 0, duration_sum: 0.0, request_sum: 0, response_sum: 0,
|
|
23
|
+
duration_buckets: Array.new(DURATION_BUCKETS.size, 0), request_buckets: Array.new(MESSAGE_BUCKETS.size, 0),
|
|
24
|
+
response_buckets: Array.new(MESSAGE_BUCKETS.size, 0) }
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
def record_rpc(service:, method:, code:, duration:, requests:, responses:)
|
|
28
|
+
unless [service, method].all? { |label| label.is_a?(String) && !label.empty? } && code.is_a?(Integer) && code.between?(0, 16) &&
|
|
29
|
+
duration.is_a?(Numeric) && duration.real? && duration.to_f.finite? && duration >= 0 &&
|
|
30
|
+
[requests, responses].all? { |value| value.is_a?(Integer) && value >= 0 }
|
|
31
|
+
raise ArgumentError, "Invalid RPC metric observation"
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
@lock.synchronize do
|
|
35
|
+
row = @rpc[[service, method, code]]
|
|
36
|
+
unless row
|
|
37
|
+
row = self.class.empty_row(service: service, method: method, code: code)
|
|
38
|
+
@rpc[[row[:service], row[:method], code].freeze] = row
|
|
39
|
+
end
|
|
40
|
+
row[:count] += 1
|
|
41
|
+
row[:duration_sum] += duration.to_f
|
|
42
|
+
row[:request_sum] += requests
|
|
43
|
+
row[:response_sum] += responses
|
|
44
|
+
row[:duration_buckets][DURATION_BUCKETS.bsearch_index { |upper| duration <= upper }] += 1
|
|
45
|
+
row[:request_buckets][MESSAGE_BUCKETS.bsearch_index { |upper| requests <= upper }] += 1
|
|
46
|
+
row[:response_buckets][MESSAGE_BUCKETS.bsearch_index { |upper| responses <= upper }] += 1
|
|
47
|
+
end
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
def observe_rejected(total)
|
|
51
|
+
@lock.synchronize do
|
|
52
|
+
raise ArgumentError, "Rejected counter must be a nondecreasing integer" unless total.is_a?(Integer) && total >= @last_rejected
|
|
53
|
+
|
|
54
|
+
@rejected_delta += total - @last_rejected
|
|
55
|
+
@last_rejected = total
|
|
56
|
+
end
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
# Optional process observations and shutdown for installed worker backends.
|
|
60
|
+
def observe_worker(_status); end
|
|
61
|
+
def close(timeout: nil); end
|
|
62
|
+
|
|
63
|
+
# The caller retains this detached delta until its status channel accepts the row.
|
|
64
|
+
def take_delta(max_bytes: Supervisor::StatusChannel::MAX_LINE_BYTES - 4096)
|
|
65
|
+
raise ArgumentError, "Metric packet budget must be positive" unless max_bytes.is_a?(Integer) && max_bytes.positive?
|
|
66
|
+
|
|
67
|
+
@lock.synchronize do
|
|
68
|
+
return nil if @rpc.empty? && @rejected_delta.zero?
|
|
69
|
+
|
|
70
|
+
delta = { rpc: [], rejected: @rejected_delta }
|
|
71
|
+
used_bytes = JSON.generate(delta).bytesize
|
|
72
|
+
raise ArgumentError, "Metric packet budget is too small" if used_bytes > max_bytes
|
|
73
|
+
|
|
74
|
+
selected = []
|
|
75
|
+
@rpc.each do |key, row|
|
|
76
|
+
row_bytes = JSON.generate(row).bytesize + (selected.empty? ? 0 : 1)
|
|
77
|
+
if used_bytes + row_bytes > max_bytes
|
|
78
|
+
raise ArgumentError, "Metric row exceeds the packet budget" if selected.empty?
|
|
79
|
+
|
|
80
|
+
break
|
|
81
|
+
end
|
|
82
|
+
delta[:rpc] << row
|
|
83
|
+
selected << key
|
|
84
|
+
used_bytes += row_bytes
|
|
85
|
+
end
|
|
86
|
+
selected.each { |key| @rpc.delete(key) }
|
|
87
|
+
@rejected_delta = 0
|
|
88
|
+
delta
|
|
89
|
+
end
|
|
90
|
+
end
|
|
91
|
+
end
|
|
92
|
+
end
|
|
93
|
+
end
|
|
@@ -1,23 +1,25 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
require "securerandom"
|
|
4
|
-
require "json"
|
|
5
4
|
|
|
6
5
|
module Gritz
|
|
7
6
|
module Middleware
|
|
8
7
|
# Converts application exceptions to safe RPC errors.
|
|
9
8
|
# @api public
|
|
10
9
|
class ExceptionMapper
|
|
11
|
-
def initialize(app, mappings: {}, expose_errors: false)
|
|
10
|
+
def initialize(app, mappings: {}, expose_errors: false, passthrough_remote_errors: false)
|
|
12
11
|
@app = app
|
|
13
12
|
@mappings = mappings
|
|
14
13
|
@expose_errors = expose_errors
|
|
14
|
+
@passthrough_remote_errors = passthrough_remote_errors
|
|
15
15
|
end
|
|
16
16
|
|
|
17
17
|
def call(context)
|
|
18
18
|
@app.call(context)
|
|
19
|
-
rescue Gritz::Error
|
|
20
|
-
raise
|
|
19
|
+
rescue Gritz::Error => e
|
|
20
|
+
raise unless e.remote? && !@passthrough_remote_errors
|
|
21
|
+
|
|
22
|
+
raise_internal(context, e)
|
|
21
23
|
rescue StandardError => e
|
|
22
24
|
mapping = @mappings.find { |klass, _| e.is_a?(klass) }&.last
|
|
23
25
|
if mapping
|
|
@@ -31,10 +33,15 @@ module Gritz
|
|
|
31
33
|
raise Errors::InvalidArgument, "invalid record"
|
|
32
34
|
end
|
|
33
35
|
|
|
36
|
+
raise_internal(context, e)
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
private
|
|
40
|
+
|
|
41
|
+
def raise_internal(context, error)
|
|
34
42
|
error_id = SecureRandom.uuid
|
|
35
|
-
context.
|
|
36
|
-
|
|
37
|
-
raise Errors::Internal.new(@expose_errors ? e.message : "internal error (#{error_id})", metadata: { "error-id" => error_id })
|
|
43
|
+
context.store[:gritz_error] = { error_id: error_id, error: error.class.name, message: error.message, backtrace: error.backtrace }
|
|
44
|
+
raise Errors::Internal.new(@expose_errors ? error.message : "internal error (#{error_id})", metadata: { "error-id" => error_id })
|
|
38
45
|
end
|
|
39
46
|
end
|
|
40
47
|
end
|
|
@@ -23,9 +23,33 @@ module Gritz
|
|
|
23
23
|
raise
|
|
24
24
|
ensure
|
|
25
25
|
duration = (Process.clock_gettime(Process::CLOCK_MONOTONIC) - started) * 1000
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
26
|
+
fields = { request_id: context.request_id, service: context.method.service,
|
|
27
|
+
method: context.method.name, code: code, duration_ms: duration.round(3),
|
|
28
|
+
peer: context.peer, worker: context.worker, pid: Process.pid, bytes_in: context.bytes_in, bytes_out: context.bytes_out }
|
|
29
|
+
fields.merge!(context.store.fetch(:gritz_error, {}))
|
|
30
|
+
fields = prepare(fields, context.log_redact)
|
|
31
|
+
message = if context.log_format == :logfmt
|
|
32
|
+
fields.map do |key, value|
|
|
33
|
+
value = value.to_s if value.is_a?(Symbol)
|
|
34
|
+
value = JSON.generate(value) if value.is_a?(Hash) || value.is_a?(Array)
|
|
35
|
+
"#{key}=#{JSON.generate(value)}"
|
|
36
|
+
end.join(" ")
|
|
37
|
+
else
|
|
38
|
+
JSON.generate(fields)
|
|
39
|
+
end
|
|
40
|
+
context.logger.info(message)
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
private
|
|
44
|
+
|
|
45
|
+
def prepare(value, redacted)
|
|
46
|
+
case value
|
|
47
|
+
when Hash
|
|
48
|
+
value.to_h { |key, item| [key, redacted.include?(key.to_s) ? "[FILTERED]" : prepare(item, redacted)] }
|
|
49
|
+
when Array then value.map { |item| prepare(item, redacted) }
|
|
50
|
+
when String then value.encode(Encoding::UTF_8, invalid: :replace, undef: :replace)
|
|
51
|
+
else value
|
|
52
|
+
end
|
|
29
53
|
end
|
|
30
54
|
end
|
|
31
55
|
end
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Gritz
|
|
4
|
+
module Middleware
|
|
5
|
+
# One completion observation wraps the complete unary or streaming action.
|
|
6
|
+
# @api public
|
|
7
|
+
class Metrics
|
|
8
|
+
def initialize(app)
|
|
9
|
+
@app = app
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
def call(context)
|
|
13
|
+
started = Process.clock_gettime(Process::CLOCK_MONOTONIC)
|
|
14
|
+
code = 0
|
|
15
|
+
@app.call(context)
|
|
16
|
+
rescue Gritz::Error => e
|
|
17
|
+
code = e.grpc_code
|
|
18
|
+
raise
|
|
19
|
+
rescue StandardError
|
|
20
|
+
code = 13
|
|
21
|
+
raise
|
|
22
|
+
ensure
|
|
23
|
+
context.metrics&.record_rpc(service: context.method.service, method: context.method.name, code: code,
|
|
24
|
+
duration: Process.clock_gettime(Process::CLOCK_MONOTONIC) - started,
|
|
25
|
+
requests: context.requests_count, responses: context.responses_count)
|
|
26
|
+
end
|
|
27
|
+
end
|
|
28
|
+
end
|
|
29
|
+
end
|
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "socket"
|
|
4
|
+
require "json"
|
|
5
|
+
|
|
6
|
+
module Gritz
|
|
7
|
+
module Supervisor
|
|
8
|
+
# Small, bounded HTTP probe listener polled by its owning process.
|
|
9
|
+
# @api private
|
|
10
|
+
class AdminServer
|
|
11
|
+
MAX_CLIENTS = 32
|
|
12
|
+
MAX_HEADER_BYTES = 8192
|
|
13
|
+
CLIENT_TIMEOUT = 2.0
|
|
14
|
+
attr_reader :io, :address
|
|
15
|
+
|
|
16
|
+
def initialize(bind:, status:, ready:, metrics:, logger:)
|
|
17
|
+
host, port = bind.match(/\A(\[[^\]]+\]|[^:]+):(\d+)\z/).captures
|
|
18
|
+
host = host.delete_prefix("[").delete_suffix("]")
|
|
19
|
+
@io = TCPServer.new(host, Integer(port))
|
|
20
|
+
@address = "#{host.include?(':') ? "[#{host}]" : host}:#{@io.addr[1]}"
|
|
21
|
+
@status = status
|
|
22
|
+
@ready = ready
|
|
23
|
+
@metrics = metrics
|
|
24
|
+
@logger = logger
|
|
25
|
+
@clients = {}
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
def ios = [@io, *@clients.keys]
|
|
29
|
+
|
|
30
|
+
def poll
|
|
31
|
+
4.times do
|
|
32
|
+
socket = @io.accept_nonblock(exception: false)
|
|
33
|
+
break if socket == :wait_readable
|
|
34
|
+
|
|
35
|
+
if @clients.size >= MAX_CLIENTS
|
|
36
|
+
socket.close
|
|
37
|
+
else
|
|
38
|
+
@clients[socket] = { input: +"".b, output: nil, deadline: monotonic + CLIENT_TIMEOUT }
|
|
39
|
+
end
|
|
40
|
+
rescue Errno::ECONNABORTED
|
|
41
|
+
next
|
|
42
|
+
end
|
|
43
|
+
@clients.each do |socket, client|
|
|
44
|
+
if monotonic >= client[:deadline]
|
|
45
|
+
drop(socket)
|
|
46
|
+
next
|
|
47
|
+
end
|
|
48
|
+
read_request(socket, client) unless client[:output]
|
|
49
|
+
write_response(socket, client) if client[:output]
|
|
50
|
+
rescue IOError, SystemCallError
|
|
51
|
+
drop(socket)
|
|
52
|
+
end
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
def close
|
|
56
|
+
@clients.each_key(&:close)
|
|
57
|
+
@clients.clear
|
|
58
|
+
@io.close unless @io.closed?
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
private
|
|
62
|
+
|
|
63
|
+
def monotonic = Process.clock_gettime(Process::CLOCK_MONOTONIC)
|
|
64
|
+
|
|
65
|
+
def drop(socket)
|
|
66
|
+
@clients.delete(socket)
|
|
67
|
+
socket.close unless socket.closed?
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
def read_request(socket, client)
|
|
71
|
+
chunk = socket.read_nonblock(4096, exception: false)
|
|
72
|
+
return if chunk == :wait_readable
|
|
73
|
+
return drop(socket) if chunk.nil?
|
|
74
|
+
|
|
75
|
+
client[:input] << chunk
|
|
76
|
+
if client[:input].bytesize > MAX_HEADER_BYTES
|
|
77
|
+
respond(client, 431, "Request headers too large\n")
|
|
78
|
+
elsif client[:input].include?("\r\n\r\n")
|
|
79
|
+
method, path, version = client[:input].lines.first.split
|
|
80
|
+
if !%w[HTTP/1.0 HTTP/1.1].include?(version)
|
|
81
|
+
respond(client, 400, "Invalid request\n")
|
|
82
|
+
elsif method != "GET"
|
|
83
|
+
respond(client, 405, "GET required\n")
|
|
84
|
+
else
|
|
85
|
+
route(client, path)
|
|
86
|
+
end
|
|
87
|
+
end
|
|
88
|
+
end
|
|
89
|
+
|
|
90
|
+
def route(client, path)
|
|
91
|
+
case path
|
|
92
|
+
when "/livez" then respond(client, 200, "ok\n")
|
|
93
|
+
when "/readyz"
|
|
94
|
+
ready = @ready.call
|
|
95
|
+
respond(client, ready ? 200 : 503, ready ? "ready\n" : "not ready\n")
|
|
96
|
+
when "/status" then respond(client, 200, "#{JSON.generate(@status.call)}\n", "application/json")
|
|
97
|
+
when "/metrics" then respond(client, 200, @metrics.call, "text/plain; version=0.0.4")
|
|
98
|
+
else respond(client, 404, "Not found\n")
|
|
99
|
+
end
|
|
100
|
+
rescue StandardError => e
|
|
101
|
+
@logger.error("Admin #{path}: #{e.message}")
|
|
102
|
+
respond(client, 500, "Internal error\n")
|
|
103
|
+
end
|
|
104
|
+
|
|
105
|
+
def respond(client, code, body, content_type = "text/plain")
|
|
106
|
+
reasons = { 200 => "OK", 400 => "Bad Request", 404 => "Not Found", 405 => "Method Not Allowed",
|
|
107
|
+
431 => "Request Header Fields Too Large", 500 => "Internal Server Error", 503 => "Service Unavailable" }
|
|
108
|
+
client[:output] =
|
|
109
|
+
"HTTP/1.1 #{code} #{reasons.fetch(code)}\r\nContent-Type: #{content_type}\r\nContent-Length: #{body.bytesize}\r\nConnection: close\r\n\r\n#{body}"
|
|
110
|
+
end
|
|
111
|
+
|
|
112
|
+
def write_response(socket, client)
|
|
113
|
+
written = socket.write_nonblock(client[:output], exception: false)
|
|
114
|
+
return if written == :wait_writable
|
|
115
|
+
|
|
116
|
+
client[:output] = client[:output].byteslice(written..)
|
|
117
|
+
drop(socket) if client[:output].empty?
|
|
118
|
+
end
|
|
119
|
+
end
|
|
120
|
+
end
|
|
121
|
+
end
|