service_skeleton 0.0.0.49.g47046b9
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 +7 -0
- data/.editorconfig +7 -0
- data/.gitignore +9 -0
- data/.rubocop.yml +1 -0
- data/.travis.yml +11 -0
- data/.yardopts +1 -0
- data/CODE_OF_CONDUCT.md +49 -0
- data/CONTRIBUTING.md +13 -0
- data/LICENCE +674 -0
- data/README.md +767 -0
- data/lib/service_skeleton.rb +41 -0
- data/lib/service_skeleton/config.rb +133 -0
- data/lib/service_skeleton/config_class.rb +16 -0
- data/lib/service_skeleton/config_variable.rb +44 -0
- data/lib/service_skeleton/config_variable/boolean.rb +21 -0
- data/lib/service_skeleton/config_variable/enum.rb +27 -0
- data/lib/service_skeleton/config_variable/float.rb +25 -0
- data/lib/service_skeleton/config_variable/integer.rb +25 -0
- data/lib/service_skeleton/config_variable/kv_list.rb +26 -0
- data/lib/service_skeleton/config_variable/path_list.rb +13 -0
- data/lib/service_skeleton/config_variable/string.rb +18 -0
- data/lib/service_skeleton/config_variable/url.rb +36 -0
- data/lib/service_skeleton/config_variable/yaml_file.rb +42 -0
- data/lib/service_skeleton/config_variables.rb +79 -0
- data/lib/service_skeleton/error.rb +10 -0
- data/lib/service_skeleton/filtering_logger.rb +38 -0
- data/lib/service_skeleton/generator.rb +165 -0
- data/lib/service_skeleton/logging_helpers.rb +28 -0
- data/lib/service_skeleton/metric_method_name.rb +9 -0
- data/lib/service_skeleton/metrics_methods.rb +37 -0
- data/lib/service_skeleton/runner.rb +46 -0
- data/lib/service_skeleton/service_name.rb +20 -0
- data/lib/service_skeleton/signal_manager.rb +202 -0
- data/lib/service_skeleton/signals_methods.rb +15 -0
- data/lib/service_skeleton/ultravisor_children.rb +17 -0
- data/lib/service_skeleton/ultravisor_loggerstash.rb +11 -0
- data/service_skeleton.gemspec +55 -0
- metadata +356 -0
@@ -0,0 +1,79 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require_relative "./error"
|
4
|
+
|
5
|
+
require "service_skeleton/config_variable"
|
6
|
+
|
7
|
+
module ServiceSkeleton
|
8
|
+
module ConfigVariables
|
9
|
+
UNDEFINED = Module.new
|
10
|
+
private_constant :UNDEFINED
|
11
|
+
|
12
|
+
def registered_variables
|
13
|
+
@registered_variables ||= []
|
14
|
+
end
|
15
|
+
|
16
|
+
def register_variable(name, klass, **opts)
|
17
|
+
if opts[:default] == UNDEFINED
|
18
|
+
opts.delete(:default)
|
19
|
+
end
|
20
|
+
|
21
|
+
registered_variables << {
|
22
|
+
name: name,
|
23
|
+
class: klass,
|
24
|
+
opts: opts,
|
25
|
+
}
|
26
|
+
end
|
27
|
+
|
28
|
+
def boolean(var_name, default: UNDEFINED, sensitive: false)
|
29
|
+
register_variable(var_name, ConfigVariable::Boolean, default: default, sensitive: sensitive)
|
30
|
+
end
|
31
|
+
|
32
|
+
def enum(var_name, values:, default: UNDEFINED, sensitive: false)
|
33
|
+
unless values.is_a?(Hash) || values.is_a?(Array)
|
34
|
+
raise ArgumentError,
|
35
|
+
"values option to enum must be a hash or array"
|
36
|
+
end
|
37
|
+
|
38
|
+
register_variable(var_name, ConfigVariable::Enum, default: default, sensitive: sensitive, values: values)
|
39
|
+
end
|
40
|
+
|
41
|
+
def float(var_name, default: UNDEFINED, sensitive: false, range: -Float::INFINITY..Float::INFINITY)
|
42
|
+
register_variable(var_name, ConfigVariable::Float, default: default, sensitive: sensitive, range: range)
|
43
|
+
end
|
44
|
+
|
45
|
+
def integer(var_name, default: UNDEFINED, sensitive: false, range: -Float::INFINITY..Float::INFINITY)
|
46
|
+
register_variable(var_name, ConfigVariable::Integer, default: default, sensitive: sensitive, range: range)
|
47
|
+
end
|
48
|
+
|
49
|
+
def kv_list(var_name, default: UNDEFINED, sensitive: false, key_pattern: /\A#{var_name}_(.*)\z/)
|
50
|
+
register_variable(var_name, ConfigVariable::KVList, default: default, sensitive: sensitive, key_pattern: key_pattern)
|
51
|
+
end
|
52
|
+
|
53
|
+
def path_list(var_name, default: UNDEFINED, sensitive: false)
|
54
|
+
register_variable(var_name, ConfigVariable::PathList, default: default, sensitive: sensitive)
|
55
|
+
end
|
56
|
+
|
57
|
+
def string(var_name, default: UNDEFINED, sensitive: false, match: nil)
|
58
|
+
register_variable(var_name, ConfigVariable::String, default: default, sensitive: sensitive, match: match)
|
59
|
+
end
|
60
|
+
|
61
|
+
def url(var_name, default: UNDEFINED, sensitive: false)
|
62
|
+
register_variable(var_name, ConfigVariable::URL, default: default, sensitive: sensitive)
|
63
|
+
end
|
64
|
+
|
65
|
+
def yaml_file(var_name, default: UNDEFINED, sensitive: false, klass: nil)
|
66
|
+
register_variable(var_name, ConfigVariable::YamlFile, default: default, sensitive: sensitive, klass: klass)
|
67
|
+
end
|
68
|
+
end
|
69
|
+
end
|
70
|
+
|
71
|
+
require_relative "config_variable/boolean"
|
72
|
+
require_relative "config_variable/enum"
|
73
|
+
require_relative "config_variable/float"
|
74
|
+
require_relative "config_variable/integer"
|
75
|
+
require_relative "config_variable/kv_list"
|
76
|
+
require_relative "config_variable/path_list"
|
77
|
+
require_relative "config_variable/string"
|
78
|
+
require_relative "config_variable/url"
|
79
|
+
require_relative "config_variable/yaml_file"
|
@@ -0,0 +1,10 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module ServiceSkeleton
|
4
|
+
class Error < StandardError
|
5
|
+
class CannotSanitizeEnvironmentError < Error; end
|
6
|
+
class InvalidEnvironmentError < Error; end
|
7
|
+
class InvalidMetricNameError < Error; end
|
8
|
+
class InvalidServiceClassError < Error; end
|
9
|
+
end
|
10
|
+
end
|
@@ -0,0 +1,38 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'logger'
|
4
|
+
|
5
|
+
module FilteringLogger
|
6
|
+
attr_reader :filters
|
7
|
+
|
8
|
+
def filters=(f)
|
9
|
+
raise ArgumentError, "Must provide an array" unless f.is_a?(Array)
|
10
|
+
|
11
|
+
@filters = f
|
12
|
+
end
|
13
|
+
|
14
|
+
def add(s, m = nil, p = nil, &blk)
|
15
|
+
p ||= @progname
|
16
|
+
|
17
|
+
if @filters && p
|
18
|
+
@filters.each do |re, sev|
|
19
|
+
if re === p
|
20
|
+
if s < sev
|
21
|
+
return true
|
22
|
+
else
|
23
|
+
# We force the severity to nil for this call to override
|
24
|
+
# the logger's default severity filtering logic, because
|
25
|
+
# messages without a severity are always logged
|
26
|
+
return super(nil, m, p, &blk)
|
27
|
+
end
|
28
|
+
end
|
29
|
+
end
|
30
|
+
end
|
31
|
+
|
32
|
+
super
|
33
|
+
end
|
34
|
+
|
35
|
+
alias log add
|
36
|
+
end
|
37
|
+
|
38
|
+
Logger.prepend(FilteringLogger)
|
@@ -0,0 +1,165 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require_relative "config"
|
4
|
+
require_relative "signal_manager"
|
5
|
+
require_relative "ultravisor_loggerstash"
|
6
|
+
|
7
|
+
require "frankenstein/ruby_gc_metrics"
|
8
|
+
require "frankenstein/ruby_vm_metrics"
|
9
|
+
require "frankenstein/process_metrics"
|
10
|
+
require "frankenstein/server"
|
11
|
+
require "prometheus/client/registry"
|
12
|
+
require "sigdump"
|
13
|
+
require "ultravisor"
|
14
|
+
|
15
|
+
module ServiceSkeleton
|
16
|
+
module Generator
|
17
|
+
def generate(config:, metrics_registry:, service_metrics:, service_signal_handlers:)
|
18
|
+
Ultravisor.new(logger: config.logger).tap do |ultravisor|
|
19
|
+
initialize_metrics(ultravisor, config, metrics_registry, service_metrics)
|
20
|
+
initialize_loggerstash(ultravisor, config, metrics_registry)
|
21
|
+
initialize_signals(ultravisor, config, service_signal_handlers, metrics_registry)
|
22
|
+
end
|
23
|
+
end
|
24
|
+
|
25
|
+
private
|
26
|
+
|
27
|
+
def initialize_metrics(ultravisor, config, registry, metrics)
|
28
|
+
Frankenstein::RubyGCMetrics.register(registry)
|
29
|
+
Frankenstein::RubyVMMetrics.register(registry)
|
30
|
+
Frankenstein::ProcessMetrics.register(registry)
|
31
|
+
|
32
|
+
metrics.each do |m|
|
33
|
+
registry.register(m)
|
34
|
+
|
35
|
+
method_name = m.method_name(config.service_name)
|
36
|
+
|
37
|
+
if registry.singleton_class.method_defined?(method_name)
|
38
|
+
raise ServiceSkeleton::Error::InvalidMetricNameError,
|
39
|
+
"Metric method #{method_name} is already defined"
|
40
|
+
end
|
41
|
+
|
42
|
+
registry.define_singleton_method(method_name) do
|
43
|
+
m
|
44
|
+
end
|
45
|
+
end
|
46
|
+
|
47
|
+
if config.metrics_port
|
48
|
+
config.logger.info(config.service_name) { "Starting metrics server on port #{config.metrics_port}" }
|
49
|
+
ultravisor.add_child(
|
50
|
+
id: :metrics_server,
|
51
|
+
klass: Frankenstein::Server,
|
52
|
+
method: :run,
|
53
|
+
args: [
|
54
|
+
port: config.metrics_port,
|
55
|
+
logger: config.logger,
|
56
|
+
metrics_prefix: :"#{config.service_name}_metrics_server",
|
57
|
+
registry: registry,
|
58
|
+
]
|
59
|
+
)
|
60
|
+
end
|
61
|
+
end
|
62
|
+
|
63
|
+
def initialize_loggerstash(ultravisor, config, registry)
|
64
|
+
if config.logstash_server && !config.logstash_server.empty?
|
65
|
+
config.logger.info(config.service_name) { "Configuring loggerstash to send to #{config.logstash_server}" }
|
66
|
+
|
67
|
+
ultravisor.add_child(
|
68
|
+
id: :logstash_writer,
|
69
|
+
klass: LogstashWriter,
|
70
|
+
method: :run,
|
71
|
+
args: [
|
72
|
+
server_name: config.logstash_server,
|
73
|
+
metrics_registry: registry,
|
74
|
+
logger: config.logger,
|
75
|
+
],
|
76
|
+
access: :unsafe
|
77
|
+
)
|
78
|
+
|
79
|
+
config.logger.singleton_class.prepend(Loggerstash::Mixin)
|
80
|
+
|
81
|
+
config.logger.instance_variable_set(:@ultravisor, ultravisor)
|
82
|
+
config.logger.singleton_class.prepend(ServiceSkeleton::UltravisorLoggerstash)
|
83
|
+
end
|
84
|
+
end
|
85
|
+
|
86
|
+
def initialize_signals(ultravisor, config, service_signals, metrics_registry)
|
87
|
+
counter = metrics_registry.counter(:"#{config.service_name}_signals_handled_total", docstring: "How many of each signal have been handled", labels: %i{signal})
|
88
|
+
|
89
|
+
ultravisor.add_child(
|
90
|
+
id: :signal_manager,
|
91
|
+
klass: ServiceSkeleton::SignalManager,
|
92
|
+
method: :run,
|
93
|
+
args: [
|
94
|
+
logger: config.logger,
|
95
|
+
counter: counter,
|
96
|
+
signals: global_signals(ultravisor, config.logger) + wrap_service_signals(service_signals, ultravisor),
|
97
|
+
],
|
98
|
+
shutdown: {
|
99
|
+
method: :shutdown,
|
100
|
+
timeout: 1,
|
101
|
+
}
|
102
|
+
)
|
103
|
+
end
|
104
|
+
|
105
|
+
def global_signals(ultravisor, logger)
|
106
|
+
# For mysterious reasons of mystery, simplecov doesn't recognise these
|
107
|
+
# procs as being called, even though there are definitely tests for
|
108
|
+
# them. So...
|
109
|
+
#:nocov:
|
110
|
+
[
|
111
|
+
[
|
112
|
+
"USR1",
|
113
|
+
->() {
|
114
|
+
logger.level -= 1 unless logger.level == Logger::DEBUG
|
115
|
+
logger.info($0) { "Received SIGUSR1; log level is now #{Logger::SEV_LABEL[logger.level]}." }
|
116
|
+
}
|
117
|
+
],
|
118
|
+
[
|
119
|
+
"USR2",
|
120
|
+
->() {
|
121
|
+
logger.level += 1 unless logger.level == Logger::ERROR
|
122
|
+
logger.info($0) { "Received SIGUSR2; log level is now #{Logger::SEV_LABEL[logger.level]}." }
|
123
|
+
}
|
124
|
+
],
|
125
|
+
[
|
126
|
+
"HUP",
|
127
|
+
->() {
|
128
|
+
logger.reopen
|
129
|
+
logger.info($0) { "Received SIGHUP; log file handle reopened" }
|
130
|
+
}
|
131
|
+
],
|
132
|
+
[
|
133
|
+
"QUIT",
|
134
|
+
->() { Sigdump.dump("+") }
|
135
|
+
],
|
136
|
+
[
|
137
|
+
"INT",
|
138
|
+
->() {
|
139
|
+
ultravisor.shutdown(wait: false, force: !!@shutting_down)
|
140
|
+
@shutting_down = true
|
141
|
+
}
|
142
|
+
],
|
143
|
+
[
|
144
|
+
"TERM",
|
145
|
+
->() {
|
146
|
+
ultravisor.shutdown(wait: false, force: !!@shutting_down)
|
147
|
+
@shutting_down = true
|
148
|
+
}
|
149
|
+
]
|
150
|
+
]
|
151
|
+
#:nocov:
|
152
|
+
end
|
153
|
+
|
154
|
+
def wrap_service_signals(signals, ultravisor)
|
155
|
+
[].tap do |signal_list|
|
156
|
+
signals.each do |service_name, sigs|
|
157
|
+
sigs.each do |sig, proc|
|
158
|
+
wrapped_proc = ->() { ultravisor[service_name.to_sym].unsafe_instance.instance_eval(&proc) }
|
159
|
+
signal_list << [sig, wrapped_proc]
|
160
|
+
end
|
161
|
+
end
|
162
|
+
end
|
163
|
+
end
|
164
|
+
end
|
165
|
+
end
|
@@ -0,0 +1,28 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module ServiceSkeleton
|
4
|
+
module LoggingHelpers
|
5
|
+
private
|
6
|
+
|
7
|
+
def log_exception(ex, progname = nil)
|
8
|
+
#:nocov:
|
9
|
+
progname ||= "#{self.class.to_s}##{caller_locations(2, 1).first.label}"
|
10
|
+
|
11
|
+
logger.error(progname) do
|
12
|
+
explanation = if block_given?
|
13
|
+
yield
|
14
|
+
else
|
15
|
+
nil
|
16
|
+
end
|
17
|
+
|
18
|
+
(["#{explanation}#{explanation ? ": " : ""}#{ex.message} (#{ex.class})"] + ex.backtrace).join("\n ")
|
19
|
+
end
|
20
|
+
#:nocov:
|
21
|
+
end
|
22
|
+
|
23
|
+
def logloc
|
24
|
+
loc = caller_locations.first
|
25
|
+
"#{self.class}##{loc.label}"
|
26
|
+
end
|
27
|
+
end
|
28
|
+
end
|
@@ -0,0 +1,37 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "prometheus/client"
|
4
|
+
|
5
|
+
require_relative "metric_method_name"
|
6
|
+
|
7
|
+
Prometheus::Client::Metric.include(ServiceSkeleton::MetricMethodName)
|
8
|
+
|
9
|
+
module ServiceSkeleton
|
10
|
+
module MetricsMethods
|
11
|
+
def registered_metrics
|
12
|
+
@registered_metrics || []
|
13
|
+
end
|
14
|
+
|
15
|
+
def metric(metric)
|
16
|
+
@registered_metrics ||= []
|
17
|
+
|
18
|
+
@registered_metrics << metric
|
19
|
+
end
|
20
|
+
|
21
|
+
def counter(name, docstring:, labels: [], preset_labels: {})
|
22
|
+
metric(Prometheus::Client::Counter.new(name, docstring: docstring, labels: labels, preset_labels: preset_labels))
|
23
|
+
end
|
24
|
+
|
25
|
+
def gauge(name, docstring:, labels: [], preset_labels: {})
|
26
|
+
metric(Prometheus::Client::Gauge.new(name, docstring: docstring, labels: labels, preset_labels: preset_labels))
|
27
|
+
end
|
28
|
+
|
29
|
+
def summary(name, docstring:, labels: [], preset_labels: {})
|
30
|
+
metric(Prometheus::Client::Summary.new(name, docstring: docstring, labels: labels, preset_labels: preset_labels))
|
31
|
+
end
|
32
|
+
|
33
|
+
def histogram(name, docstring:, labels: [], preset_labels: {}, buckets: Prometheus::Client::Histogram::DEFAULT_BUCKETS)
|
34
|
+
metric(Prometheus::Client::Histogram.new(name, docstring: docstring, labels: labels, preset_labels: preset_labels, buckets: buckets))
|
35
|
+
end
|
36
|
+
end
|
37
|
+
end
|
@@ -0,0 +1,46 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require_relative "config"
|
4
|
+
require_relative "logging_helpers"
|
5
|
+
require_relative "signal_manager"
|
6
|
+
|
7
|
+
require "frankenstein/ruby_gc_metrics"
|
8
|
+
require "frankenstein/ruby_vm_metrics"
|
9
|
+
require "frankenstein/process_metrics"
|
10
|
+
require "frankenstein/server"
|
11
|
+
require "prometheus/client/registry"
|
12
|
+
require "sigdump"
|
13
|
+
require "ultravisor"
|
14
|
+
|
15
|
+
module ServiceSkeleton
|
16
|
+
class Runner
|
17
|
+
include ServiceSkeleton::LoggingHelpers
|
18
|
+
|
19
|
+
def initialize(klass, env)
|
20
|
+
@config = (klass.config_class || ServiceSkeleton::Config).new(env, klass.service_name, klass.registered_variables)
|
21
|
+
@logger = @config.logger
|
22
|
+
|
23
|
+
@metrics_registry = Prometheus::Client::Registry.new
|
24
|
+
|
25
|
+
@ultravisor = ServiceSkeleton.generate(
|
26
|
+
config: @config,
|
27
|
+
metrics_registry: @metrics_registry,
|
28
|
+
service_metrics: klass.registered_metrics,
|
29
|
+
service_signal_handlers: { klass.service_name.to_sym => klass.registered_signal_handlers }
|
30
|
+
)
|
31
|
+
|
32
|
+
klass.register_ultravisor_children(@ultravisor, config: @config, metrics_registry: @metrics_registry)
|
33
|
+
end
|
34
|
+
|
35
|
+
def run
|
36
|
+
logger.info(logloc) { "Starting service #{@config.service_name}" }
|
37
|
+
logger.info(logloc) { (["Environment:"] + @config.env.map { |k, v| "#{k}=#{v.inspect}" }).join("\n ") }
|
38
|
+
|
39
|
+
@ultravisor.run
|
40
|
+
end
|
41
|
+
|
42
|
+
private
|
43
|
+
|
44
|
+
attr_reader :logger
|
45
|
+
end
|
46
|
+
end
|
@@ -0,0 +1,20 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module ServiceSkeleton
|
4
|
+
module ServiceName
|
5
|
+
def service_name
|
6
|
+
service_name_from_class(self)
|
7
|
+
end
|
8
|
+
|
9
|
+
private
|
10
|
+
|
11
|
+
def service_name_from_class(klass)
|
12
|
+
klass.to_s
|
13
|
+
.gsub("::", "_")
|
14
|
+
.gsub(/([A-Z]+)([A-Z][a-z])/, '\1_\2')
|
15
|
+
.gsub(/([a-z\d])([A-Z])/, '\1_\2')
|
16
|
+
.downcase
|
17
|
+
.gsub(/[^a-zA-Z0-9_]/, "_")
|
18
|
+
end
|
19
|
+
end
|
20
|
+
end
|
@@ -0,0 +1,202 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require_relative "./logging_helpers"
|
4
|
+
|
5
|
+
module ServiceSkeleton
|
6
|
+
# Manage signals in a sane and safe manner.
|
7
|
+
#
|
8
|
+
# Signal handling is a shit of a thing. The code that runs when a signal is
|
9
|
+
# triggered can't use mutexes (which are used in all sorts of places you
|
10
|
+
# might not expect, like Logger!) or anything else that might block. This
|
11
|
+
# greatly constrains what you can do inside a signal handler, so the standard
|
12
|
+
# approach is to stuff a character down a pipe, and then have the *real*
|
13
|
+
# signal handling run later.
|
14
|
+
#
|
15
|
+
# Also, there's always the (slim) possibility that something else might have
|
16
|
+
# hooked into a signal we want to receive. Because only a single signal
|
17
|
+
# handler can be active for a given signal at a time, we need to "chain" the
|
18
|
+
# existing handler, by calling the previous signal handler from our signal
|
19
|
+
# handler after we've done what we need to do. This class takes care of
|
20
|
+
# that, too, because it's a legend.
|
21
|
+
#
|
22
|
+
# So that's what this class does: it allows you to specify signals and
|
23
|
+
# associated blocks of code to run, it sets up signal handlers which send
|
24
|
+
# notifications to a background thread and chain correctly, and it manages
|
25
|
+
# the background thread to receive the notifications and execute the
|
26
|
+
# associated blocks of code outside of the context of the signal handler.
|
27
|
+
#
|
28
|
+
class SignalManager
|
29
|
+
include ServiceSkeleton::LoggingHelpers
|
30
|
+
|
31
|
+
# Setup a signal handler instance.
|
32
|
+
#
|
33
|
+
# @param logger [Logger] the logger to use for all the interesting information
|
34
|
+
# about what we're up to.
|
35
|
+
#
|
36
|
+
def initialize(logger:, counter:, signals:)
|
37
|
+
@logger, @signal_counter, @signal_list = logger, counter, signals
|
38
|
+
|
39
|
+
@registry = Hash.new { |h, k| h[k] = SignalHandler.new(k) }
|
40
|
+
|
41
|
+
@signal_list.each do |sig, proc|
|
42
|
+
@registry[signum(sig)] << proc
|
43
|
+
end
|
44
|
+
end
|
45
|
+
|
46
|
+
def run
|
47
|
+
logger.info(logloc) { "Starting signal manager for #{@signal_list.length} signals" }
|
48
|
+
|
49
|
+
@r, @w = IO.pipe
|
50
|
+
|
51
|
+
install_signal_handlers
|
52
|
+
|
53
|
+
signals_loop
|
54
|
+
ensure
|
55
|
+
remove_signal_handlers
|
56
|
+
end
|
57
|
+
|
58
|
+
def shutdown
|
59
|
+
@r.close
|
60
|
+
end
|
61
|
+
|
62
|
+
private
|
63
|
+
|
64
|
+
attr_reader :logger
|
65
|
+
|
66
|
+
def signals_loop
|
67
|
+
#:nocov:
|
68
|
+
loop do
|
69
|
+
begin
|
70
|
+
if ios = IO.select([@r])
|
71
|
+
if ios.first.include?(@r)
|
72
|
+
if ios.first.first.eof?
|
73
|
+
logger.info(logloc) { "Signal pipe closed; shutting down" }
|
74
|
+
break
|
75
|
+
else
|
76
|
+
c = ios.first.first.read_nonblock(1)
|
77
|
+
logger.debug(logloc) { "Received character #{c.inspect} from signal pipe" }
|
78
|
+
handle_signal(c)
|
79
|
+
end
|
80
|
+
else
|
81
|
+
logger.error(logloc) { "Mysterious return from select: #{ios.inspect}" }
|
82
|
+
end
|
83
|
+
end
|
84
|
+
rescue IOError
|
85
|
+
# Something has gone terribly wrong here... bail
|
86
|
+
break
|
87
|
+
rescue StandardError => ex
|
88
|
+
log_exception(ex) { "Exception in select loop" }
|
89
|
+
end
|
90
|
+
end
|
91
|
+
#:nocov:
|
92
|
+
end
|
93
|
+
|
94
|
+
# Given a character (presumably) received via the signal pipe, execute the
|
95
|
+
# associated handler.
|
96
|
+
#
|
97
|
+
# @param char [String] a single character, corresponding to an entry in the
|
98
|
+
# signal registry.
|
99
|
+
#
|
100
|
+
# @return [void]
|
101
|
+
#
|
102
|
+
def handle_signal(char)
|
103
|
+
if @registry.has_key?(char.ord)
|
104
|
+
handler = @registry[char.ord]
|
105
|
+
logger.debug(logloc) { "#{handler.signame} received" }
|
106
|
+
@signal_counter.increment(labels: { signal: handler.signame.to_s })
|
107
|
+
|
108
|
+
begin
|
109
|
+
handler.call
|
110
|
+
rescue StandardError => ex
|
111
|
+
log_exception(ex) { "Exception while calling signal handler" }
|
112
|
+
end
|
113
|
+
else
|
114
|
+
logger.error(logloc) { "Unrecognised signal character: #{char.inspect}" }
|
115
|
+
end
|
116
|
+
end
|
117
|
+
|
118
|
+
def install_signal_handlers
|
119
|
+
@registry.values.each do |h|
|
120
|
+
h.write_pipe = @w
|
121
|
+
h.hook
|
122
|
+
end
|
123
|
+
end
|
124
|
+
|
125
|
+
def signum(spec)
|
126
|
+
if spec.is_a?(Integer)
|
127
|
+
return spec
|
128
|
+
end
|
129
|
+
|
130
|
+
if spec.is_a?(Symbol)
|
131
|
+
str = spec.to_s
|
132
|
+
elsif spec.is_a?(String)
|
133
|
+
str = spec.dup
|
134
|
+
else
|
135
|
+
raise ArgumentError,
|
136
|
+
"Unsupported class (#{spec.class}) of signal specifier #{spec.inspect}"
|
137
|
+
end
|
138
|
+
|
139
|
+
str.sub!(/\ASIG/i, '')
|
140
|
+
|
141
|
+
if Signal.list[str.upcase]
|
142
|
+
Signal.list[str.upcase]
|
143
|
+
else
|
144
|
+
raise ArgumentError,
|
145
|
+
"Unrecognised signal specifier #{spec.inspect}"
|
146
|
+
end
|
147
|
+
end
|
148
|
+
|
149
|
+
def remove_signal_handlers
|
150
|
+
@registry.values.each { |h| h.unhook }
|
151
|
+
end
|
152
|
+
|
153
|
+
class SignalHandler
|
154
|
+
attr_reader :signame
|
155
|
+
attr_writer :write_pipe
|
156
|
+
|
157
|
+
def initialize(signum)
|
158
|
+
@signum = signum
|
159
|
+
@callbacks = []
|
160
|
+
|
161
|
+
@signame = Signal.list.invert[@signum]
|
162
|
+
end
|
163
|
+
|
164
|
+
def <<(proc)
|
165
|
+
@callbacks << proc
|
166
|
+
end
|
167
|
+
|
168
|
+
def call
|
169
|
+
@callbacks.each { |cb| cb.call }
|
170
|
+
end
|
171
|
+
|
172
|
+
def hook
|
173
|
+
@handler = ->(_) do
|
174
|
+
#:nocov:
|
175
|
+
@write_pipe.write_nonblock(@signum.chr) rescue nil
|
176
|
+
@chain.call if @chain.respond_to?(:call)
|
177
|
+
#:nocov:
|
178
|
+
end
|
179
|
+
|
180
|
+
@chain = Signal.trap(@signum, &@handler)
|
181
|
+
end
|
182
|
+
|
183
|
+
def unhook
|
184
|
+
#:nocov:
|
185
|
+
tmp_handler = Signal.trap(@signum, "IGNORE")
|
186
|
+
if tmp_handler == @handler
|
187
|
+
# The current handler is ours, so we can replace it
|
188
|
+
# with the chained handler
|
189
|
+
Signal.trap(@signum, @chain)
|
190
|
+
else
|
191
|
+
# The current handler *isn't* ours, so we better
|
192
|
+
# put it back, because whoever owns it might get
|
193
|
+
# angry.
|
194
|
+
Signal.trap(@signum, tmp_handler)
|
195
|
+
end
|
196
|
+
#:nocov:
|
197
|
+
end
|
198
|
+
end
|
199
|
+
|
200
|
+
private_constant :SignalHandler
|
201
|
+
end
|
202
|
+
end
|