lepus 0.0.1.beta2
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/.github/workflows/specs.yml +44 -0
- data/.gitignore +12 -0
- data/.rspec +1 -0
- data/.rubocop.yml +35 -0
- data/.tool-versions +1 -0
- data/CHANGELOG.md +10 -0
- data/Gemfile +6 -0
- data/Gemfile.lock +120 -0
- data/LICENSE.txt +21 -0
- data/README.md +213 -0
- data/Rakefile +4 -0
- data/bin/console +9 -0
- data/bin/setup +7 -0
- data/docker-compose.yml +8 -0
- data/exec/lepus +9 -0
- data/gemfiles/rails52.gemfile +5 -0
- data/gemfiles/rails52.gemfile.lock +242 -0
- data/gemfiles/rails61.gemfile +5 -0
- data/gemfiles/rails61.gemfile.lock +260 -0
- data/lepus.gemspec +53 -0
- data/lib/lepus/app_executor.rb +19 -0
- data/lib/lepus/cli.rb +27 -0
- data/lib/lepus/configuration.rb +90 -0
- data/lib/lepus/consumer.rb +177 -0
- data/lib/lepus/consumer_config.rb +149 -0
- data/lib/lepus/consumer_wrapper.rb +46 -0
- data/lib/lepus/lifecycle_hooks.rb +49 -0
- data/lib/lepus/message.rb +37 -0
- data/lib/lepus/middleware.rb +18 -0
- data/lib/lepus/middlewares/honeybadger.rb +23 -0
- data/lib/lepus/middlewares/json.rb +35 -0
- data/lib/lepus/middlewares/max_retry.rb +57 -0
- data/lib/lepus/primitive/string.rb +55 -0
- data/lib/lepus/process.rb +136 -0
- data/lib/lepus/process_registry.rb +37 -0
- data/lib/lepus/processes/base.rb +50 -0
- data/lib/lepus/processes/callbacks.rb +72 -0
- data/lib/lepus/processes/consumer.rb +113 -0
- data/lib/lepus/processes/interruptible.rb +38 -0
- data/lib/lepus/processes/procline.rb +11 -0
- data/lib/lepus/processes/registrable.rb +67 -0
- data/lib/lepus/processes/runnable.rb +102 -0
- data/lib/lepus/processes/supervised.rb +44 -0
- data/lib/lepus/processes.rb +6 -0
- data/lib/lepus/producer.rb +42 -0
- data/lib/lepus/rails/log_subscriber.rb +120 -0
- data/lib/lepus/rails/railtie.rb +31 -0
- data/lib/lepus/rails.rb +7 -0
- data/lib/lepus/supervisor/config.rb +45 -0
- data/lib/lepus/supervisor/maintenance.rb +35 -0
- data/lib/lepus/supervisor/pidfile.rb +61 -0
- data/lib/lepus/supervisor/pidfiled.rb +29 -0
- data/lib/lepus/supervisor/signals.rb +71 -0
- data/lib/lepus/supervisor.rb +204 -0
- data/lib/lepus/timer.rb +29 -0
- data/lib/lepus/version.rb +5 -0
- data/lib/lepus.rb +95 -0
- data/lib/puma/plugin/lepus.rb +74 -0
- metadata +290 -0
@@ -0,0 +1,42 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Lepus
|
4
|
+
class Producer
|
5
|
+
DEFAULT_EXCHANGE_OPTIONS = {
|
6
|
+
type: :topic,
|
7
|
+
durable: true,
|
8
|
+
auto_delete: false
|
9
|
+
}.freeze
|
10
|
+
|
11
|
+
DEFAULT_PUBLISH_OPTIONS = {
|
12
|
+
expiration: 7 * (60 * 60 * 24)
|
13
|
+
}.freeze
|
14
|
+
|
15
|
+
def initialize(exchange_name, **options)
|
16
|
+
@exchange_name = exchange_name
|
17
|
+
@exchange_options = DEFAULT_EXCHANGE_OPTIONS.merge(options)
|
18
|
+
end
|
19
|
+
|
20
|
+
def publish(message, **options)
|
21
|
+
payload = if message.is_a?(String)
|
22
|
+
options[:content_type] ||= "text/plain"
|
23
|
+
message
|
24
|
+
else
|
25
|
+
options[:content_type] ||= "application/json"
|
26
|
+
MultiJson.dump(message)
|
27
|
+
end
|
28
|
+
|
29
|
+
bunny.with_channel do |channel|
|
30
|
+
exchange = channel.exchange(@exchange_name, @exchange_options)
|
31
|
+
exchange.publish(
|
32
|
+
payload,
|
33
|
+
DEFAULT_PUBLISH_OPTIONS.merge(options)
|
34
|
+
)
|
35
|
+
end
|
36
|
+
end
|
37
|
+
|
38
|
+
def bunny
|
39
|
+
Thread.current[:lepus_bunny] ||= Lepus.config.create_connection(suffix: "producer")
|
40
|
+
end
|
41
|
+
end
|
42
|
+
end
|
@@ -0,0 +1,120 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
class Lepus::LogSubscriber < ActiveSupport::LogSubscriber
|
4
|
+
def start_process(event)
|
5
|
+
process = event.payload[:process]
|
6
|
+
|
7
|
+
attributes = {
|
8
|
+
pid: process.pid,
|
9
|
+
hostname: process.hostname,
|
10
|
+
process_id: process.process_id,
|
11
|
+
name: process.name
|
12
|
+
}.merge(process.metadata)
|
13
|
+
|
14
|
+
info formatted_event(event, action: "Started #{process.kind}", **attributes)
|
15
|
+
end
|
16
|
+
|
17
|
+
def shutdown_process(event)
|
18
|
+
process = event.payload[:process]
|
19
|
+
|
20
|
+
attributes = {
|
21
|
+
pid: process.pid,
|
22
|
+
hostname: process.hostname,
|
23
|
+
process_id: process.process_id,
|
24
|
+
name: process.name
|
25
|
+
}.merge(process.metadata)
|
26
|
+
|
27
|
+
info formatted_event(event, action: "Shutdown #{process.kind}", **attributes)
|
28
|
+
end
|
29
|
+
|
30
|
+
def register_process(event)
|
31
|
+
process_kind = event.payload[:kind]
|
32
|
+
attributes = event.payload.slice(:pid, :hostname, :process_id, :name)
|
33
|
+
|
34
|
+
if (error = event.payload[:error])
|
35
|
+
warn formatted_event(event, action: "Error registering #{process_kind}", **attributes.merge(error: formatted_error(error)))
|
36
|
+
else
|
37
|
+
debug formatted_event(event, action: "Register #{process_kind}", **attributes)
|
38
|
+
end
|
39
|
+
end
|
40
|
+
|
41
|
+
def deregister_process(event)
|
42
|
+
process = event.payload[:process]
|
43
|
+
|
44
|
+
attributes = {
|
45
|
+
process_id: process.id,
|
46
|
+
pid: process.pid,
|
47
|
+
hostname: process.hostname,
|
48
|
+
name: process.name,
|
49
|
+
last_heartbeat_at: process.last_heartbeat_at&.strftime("%Y-%m-%d %H:%M:%S"),
|
50
|
+
claimed_size: event.payload[:claimed_size],
|
51
|
+
pruned: event.payload[:pruned]
|
52
|
+
}
|
53
|
+
|
54
|
+
if (error = event.payload[:error])
|
55
|
+
warn formatted_event(event, action: "Error deregistering #{process.kind}", **attributes.merge(error: formatted_error(error)))
|
56
|
+
else
|
57
|
+
debug formatted_event(event, action: "Deregister #{process.kind}", **attributes)
|
58
|
+
end
|
59
|
+
end
|
60
|
+
|
61
|
+
def prune_processes(event)
|
62
|
+
debug formatted_event(event, action: "Prune dead processes", **event.payload.slice(:size))
|
63
|
+
end
|
64
|
+
|
65
|
+
def thread_error(event)
|
66
|
+
error formatted_event(event, action: "Error in thread", error: formatted_error(event.payload[:error]))
|
67
|
+
end
|
68
|
+
|
69
|
+
def graceful_termination(event)
|
70
|
+
attributes = event.payload.slice(:process_id, :supervisor_pid, :supervised_processes)
|
71
|
+
|
72
|
+
if event.payload[:shutdown_timeout_exceeded]
|
73
|
+
warn formatted_event(event, action: "Supervisor wasn't terminated gracefully - shutdown timeout exceeded", **attributes)
|
74
|
+
else
|
75
|
+
info formatted_event(event, action: "Supervisor terminated gracefully", **attributes)
|
76
|
+
end
|
77
|
+
end
|
78
|
+
|
79
|
+
def immediate_termination(event)
|
80
|
+
info formatted_event(event, action: "Supervisor terminated immediately", **event.payload.slice(:process_id, :supervisor_pid, :supervised_processes))
|
81
|
+
end
|
82
|
+
|
83
|
+
def unhandled_signal_error(event)
|
84
|
+
error formatted_event(event, action: "Received unhandled signal", **event.payload.slice(:signal))
|
85
|
+
end
|
86
|
+
|
87
|
+
def replace_fork(event)
|
88
|
+
status = event.payload[:status]
|
89
|
+
attributes = event.payload.slice(:pid).merge \
|
90
|
+
status: status.exitstatus || "no exit status set",
|
91
|
+
pid_from_status: status.pid,
|
92
|
+
signaled: status.signaled?,
|
93
|
+
stopsig: status.stopsig,
|
94
|
+
termsig: status.termsig
|
95
|
+
|
96
|
+
if (replaced_fork = event.payload[:fork])
|
97
|
+
info formatted_event(event, action: "Replaced terminated #{replaced_fork.kind}", **attributes.merge(hostname: replaced_fork.hostname, name: replaced_fork.name))
|
98
|
+
else
|
99
|
+
warn formatted_event(event, action: "Tried to replace forked process but it had already died", **attributes)
|
100
|
+
end
|
101
|
+
end
|
102
|
+
|
103
|
+
private
|
104
|
+
|
105
|
+
def formatted_event(event, action:, **attributes)
|
106
|
+
"Lepus-#{Lepus::VERSION} #{action} (#{event.duration.round(1)}ms) #{formatted_attributes(**attributes)}"
|
107
|
+
end
|
108
|
+
|
109
|
+
def formatted_attributes(**attributes)
|
110
|
+
attributes.map { |attr, value| "#{attr}: #{value.inspect}" }.join(", ")
|
111
|
+
end
|
112
|
+
|
113
|
+
def formatted_error(error)
|
114
|
+
[error.class, error.message].compact.join(" ")
|
115
|
+
end
|
116
|
+
|
117
|
+
def logger
|
118
|
+
Lepus.logger
|
119
|
+
end
|
120
|
+
end
|
@@ -0,0 +1,31 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Lepus
|
4
|
+
class Railtie < ::Rails::Railtie
|
5
|
+
config.lepus = ActiveSupport::OrderedOptions.new
|
6
|
+
config.lepus.app_executor = nil
|
7
|
+
config.lepus.on_thread_error = nil
|
8
|
+
|
9
|
+
initializer "lepus.app_executor", before: :run_prepare_callbacks do |app|
|
10
|
+
config.lepus.app_executor ||= app.executor
|
11
|
+
if ::Rails.respond_to?(:error) && config.lepus.on_thread_error.nil?
|
12
|
+
config.lepus.on_thread_error = ->(exception) { ::Rails.error.report(exception, handled: false) }
|
13
|
+
elsif config.lepus.on_thread_error.nil?
|
14
|
+
config.lepus.on_thread_error = ->(exception) { Lepus.logger.error(exception) }
|
15
|
+
end
|
16
|
+
|
17
|
+
Lepus.config.app_executor = config.lepus.app_executor
|
18
|
+
Lepus.config.on_thread_error = config.lepus.on_thread_error
|
19
|
+
end
|
20
|
+
|
21
|
+
initializer "lepus.logger" do
|
22
|
+
ActiveSupport.on_load(:lepus) do
|
23
|
+
self.logger = ::Rails.logger if logger == Lepus::DEFAULT_LOGGER
|
24
|
+
end
|
25
|
+
|
26
|
+
Lepus::LogSubscriber.attach_to :lepus
|
27
|
+
end
|
28
|
+
end
|
29
|
+
|
30
|
+
ActiveSupport.run_load_hooks(:lepus, self)
|
31
|
+
end
|
data/lib/lepus/rails.rb
ADDED
@@ -0,0 +1,45 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "yaml"
|
4
|
+
require "logger"
|
5
|
+
|
6
|
+
module Lepus
|
7
|
+
class Supervisor < Processes::Base
|
8
|
+
class Config
|
9
|
+
class ProcessStruct < Struct.new(:process_class, :attributes)
|
10
|
+
def instantiate
|
11
|
+
process_class.new(**attributes)
|
12
|
+
end
|
13
|
+
end
|
14
|
+
|
15
|
+
attr_accessor :pidfile, :require_file
|
16
|
+
|
17
|
+
def initialize(require_file: nil, pidfile: "tmp/pids/lepus.pid", **kwargs)
|
18
|
+
@pidfile = pidfile
|
19
|
+
@require_file = require_file
|
20
|
+
self.consumers = kwargs[:consumers] if kwargs.key?(:consumers)
|
21
|
+
end
|
22
|
+
|
23
|
+
def configured_processes
|
24
|
+
consumer_processes
|
25
|
+
end
|
26
|
+
|
27
|
+
def consumers=(vals)
|
28
|
+
@consumer_processes = nil
|
29
|
+
@consumers = Array(vals).map(&:to_s)
|
30
|
+
end
|
31
|
+
|
32
|
+
def consumers
|
33
|
+
@consumers ||= Lepus::Consumer.descendants.reject(&:abstract_class?).map(&:name).compact
|
34
|
+
end
|
35
|
+
|
36
|
+
protected
|
37
|
+
|
38
|
+
def consumer_processes
|
39
|
+
@consumer_processes ||= consumers.map do |class_name|
|
40
|
+
ProcessStruct.new(Lepus::Processes::Consumer, {class_name: class_name})
|
41
|
+
end
|
42
|
+
end
|
43
|
+
end
|
44
|
+
end
|
45
|
+
end
|
@@ -0,0 +1,35 @@
|
|
1
|
+
module Lepus
|
2
|
+
class Supervisor < Processes::Base
|
3
|
+
module Maintenance
|
4
|
+
def self.included(base)
|
5
|
+
base.send :include, InstanceMethods
|
6
|
+
end
|
7
|
+
|
8
|
+
module InstanceMethods
|
9
|
+
private
|
10
|
+
|
11
|
+
def launch_maintenance_task
|
12
|
+
@maintenance_task = ::Concurrent::TimerTask.new(run_now: true, execution_interval: Lepus.config.process_alive_threshold) do
|
13
|
+
prune_dead_processes
|
14
|
+
end
|
15
|
+
|
16
|
+
@maintenance_task.add_observer do |_, _, error|
|
17
|
+
handle_thread_error(error) if error
|
18
|
+
end
|
19
|
+
|
20
|
+
@maintenance_task.execute
|
21
|
+
end
|
22
|
+
|
23
|
+
def stop_maintenance_task
|
24
|
+
@maintenance_task&.shutdown
|
25
|
+
end
|
26
|
+
|
27
|
+
def prune_dead_processes
|
28
|
+
wrap_in_app_executor do
|
29
|
+
Lepus::Process.prune(excluding: process)
|
30
|
+
end
|
31
|
+
end
|
32
|
+
end
|
33
|
+
end
|
34
|
+
end
|
35
|
+
end
|
@@ -0,0 +1,61 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Lepus
|
4
|
+
class Supervisor < Processes::Base
|
5
|
+
class Pidfile
|
6
|
+
def initialize(path)
|
7
|
+
@path = path
|
8
|
+
@pid = ::Process.pid
|
9
|
+
end
|
10
|
+
|
11
|
+
def setup
|
12
|
+
check_status
|
13
|
+
write_file
|
14
|
+
set_at_exit_hook
|
15
|
+
end
|
16
|
+
|
17
|
+
def delete
|
18
|
+
delete_file
|
19
|
+
end
|
20
|
+
|
21
|
+
private
|
22
|
+
|
23
|
+
attr_reader :path, :pid
|
24
|
+
|
25
|
+
def check_status
|
26
|
+
if ::File.exist?(path)
|
27
|
+
existing_pid = ::File.read(path).strip.to_i
|
28
|
+
existing_pid > 0 && ::Process.kill(0, existing_pid)
|
29
|
+
|
30
|
+
already_running!
|
31
|
+
else
|
32
|
+
FileUtils.mkdir_p File.dirname(path)
|
33
|
+
end
|
34
|
+
rescue Errno::ESRCH
|
35
|
+
# Process is dead, ignore, just delete the file
|
36
|
+
delete
|
37
|
+
rescue Errno::EPERM
|
38
|
+
already_running!
|
39
|
+
end
|
40
|
+
|
41
|
+
def write_file
|
42
|
+
::File.open(path, ::File::CREAT | ::File::EXCL | ::File::WRONLY) { |file| file.write(pid.to_s) }
|
43
|
+
rescue Errno::EEXIST
|
44
|
+
check_status
|
45
|
+
retry
|
46
|
+
end
|
47
|
+
|
48
|
+
def set_at_exit_hook
|
49
|
+
at_exit { delete if ::Process.pid == pid }
|
50
|
+
end
|
51
|
+
|
52
|
+
def delete_file
|
53
|
+
::File.delete(path) if ::File.exist?(path)
|
54
|
+
end
|
55
|
+
|
56
|
+
def already_running!
|
57
|
+
abort "A supervisor is already running. Check #{path}"
|
58
|
+
end
|
59
|
+
end
|
60
|
+
end
|
61
|
+
end
|
@@ -0,0 +1,29 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Lepus
|
4
|
+
class Supervisor < Processes::Base
|
5
|
+
module Pidfiled
|
6
|
+
def self.included(base)
|
7
|
+
base.send :include, InstanceMethods
|
8
|
+
base.class_eval do
|
9
|
+
before_boot :setup_pidfile
|
10
|
+
after_shutdown :delete_pidfile
|
11
|
+
end
|
12
|
+
end
|
13
|
+
|
14
|
+
module InstanceMethods
|
15
|
+
private
|
16
|
+
|
17
|
+
def setup_pidfile
|
18
|
+
if (path = configuration.pidfile)
|
19
|
+
@pidfile = Pidfile.new(path).tap(&:setup)
|
20
|
+
end
|
21
|
+
end
|
22
|
+
|
23
|
+
def delete_pidfile
|
24
|
+
@pidfile&.delete
|
25
|
+
end
|
26
|
+
end
|
27
|
+
end
|
28
|
+
end
|
29
|
+
end
|
@@ -0,0 +1,71 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Lepus
|
4
|
+
class Supervisor < Processes::Base
|
5
|
+
module Signals
|
6
|
+
def self.included(base)
|
7
|
+
base.send :include, InstanceMethods
|
8
|
+
base.class_eval do
|
9
|
+
before_boot :register_signal_handlers
|
10
|
+
after_shutdown :restore_default_signal_handlers
|
11
|
+
end
|
12
|
+
end
|
13
|
+
|
14
|
+
module InstanceMethods
|
15
|
+
private
|
16
|
+
|
17
|
+
SIGNALS = %i[QUIT INT TERM]
|
18
|
+
|
19
|
+
def register_signal_handlers
|
20
|
+
SIGNALS.each do |signal|
|
21
|
+
trap(signal) do
|
22
|
+
signal_queue << signal
|
23
|
+
interrupt
|
24
|
+
end
|
25
|
+
end
|
26
|
+
end
|
27
|
+
|
28
|
+
def restore_default_signal_handlers
|
29
|
+
SIGNALS.each do |signal|
|
30
|
+
trap(signal, :DEFAULT)
|
31
|
+
end
|
32
|
+
end
|
33
|
+
|
34
|
+
def process_signal_queue
|
35
|
+
while (signal = signal_queue.shift)
|
36
|
+
handle_signal(signal)
|
37
|
+
end
|
38
|
+
end
|
39
|
+
|
40
|
+
def handle_signal(signal)
|
41
|
+
case signal
|
42
|
+
when :TERM, :INT
|
43
|
+
stop
|
44
|
+
terminate_gracefully
|
45
|
+
when :QUIT
|
46
|
+
stop
|
47
|
+
terminate_immediately
|
48
|
+
else
|
49
|
+
Lepus.instrument :unhandled_signal_error, signal: signal
|
50
|
+
end
|
51
|
+
end
|
52
|
+
|
53
|
+
def signal_processes(pids, signal)
|
54
|
+
pids.each do |pid|
|
55
|
+
signal_process pid, signal
|
56
|
+
end
|
57
|
+
end
|
58
|
+
|
59
|
+
def signal_process(pid, signal)
|
60
|
+
::Process.kill signal, pid
|
61
|
+
rescue Errno::ESRCH
|
62
|
+
# Ignore, process died before
|
63
|
+
end
|
64
|
+
|
65
|
+
def signal_queue
|
66
|
+
@signal_queue ||= []
|
67
|
+
end
|
68
|
+
end
|
69
|
+
end
|
70
|
+
end
|
71
|
+
end
|
@@ -0,0 +1,204 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Lepus
|
4
|
+
class Supervisor < Processes::Base
|
5
|
+
include LifecycleHooks
|
6
|
+
include Maintenance
|
7
|
+
include Signals
|
8
|
+
include Pidfiled
|
9
|
+
|
10
|
+
class << self
|
11
|
+
def start(**options)
|
12
|
+
# Lepus.config.supervisor = true
|
13
|
+
config = Config.new(**options)
|
14
|
+
new(config).tap(&:start)
|
15
|
+
end
|
16
|
+
end
|
17
|
+
|
18
|
+
def initialize(configuration)
|
19
|
+
@configuration = configuration
|
20
|
+
@forks = {}
|
21
|
+
@configured_processes = {}
|
22
|
+
ProcessRegistry.instance # Ensure the registry is initialized
|
23
|
+
end
|
24
|
+
|
25
|
+
def start
|
26
|
+
boot
|
27
|
+
|
28
|
+
run_start_hooks
|
29
|
+
|
30
|
+
start_processes
|
31
|
+
launch_maintenance_task
|
32
|
+
|
33
|
+
supervise
|
34
|
+
end
|
35
|
+
|
36
|
+
def stop
|
37
|
+
super
|
38
|
+
run_stop_hooks
|
39
|
+
end
|
40
|
+
|
41
|
+
private
|
42
|
+
|
43
|
+
attr_reader :configuration, :forks, :configured_processes
|
44
|
+
|
45
|
+
def boot
|
46
|
+
Lepus.instrument(:start_process, process: self) do
|
47
|
+
if configuration.require_file
|
48
|
+
Kernel.require configuration.require_file
|
49
|
+
else
|
50
|
+
begin
|
51
|
+
require "rails"
|
52
|
+
require_relative "rails"
|
53
|
+
require File.expand_path("config/environment", Dir.pwd)
|
54
|
+
rescue LoadError
|
55
|
+
# Rails not found
|
56
|
+
end
|
57
|
+
end
|
58
|
+
|
59
|
+
setup_consumers
|
60
|
+
check_bunny_connection
|
61
|
+
|
62
|
+
run_process_callbacks(:boot) do
|
63
|
+
sync_std_streams
|
64
|
+
end
|
65
|
+
end
|
66
|
+
end
|
67
|
+
|
68
|
+
def setup_consumers
|
69
|
+
Lepus.eager_load_consumers!
|
70
|
+
|
71
|
+
if configuration.consumers.empty?
|
72
|
+
abort "No consumers found. Exiting..."
|
73
|
+
end
|
74
|
+
end
|
75
|
+
|
76
|
+
def check_bunny_connection
|
77
|
+
temp_bunny = Lepus.config.create_connection(suffix: "(boot-check)")
|
78
|
+
temp_bunny.close
|
79
|
+
end
|
80
|
+
|
81
|
+
def start_processes
|
82
|
+
configuration.configured_processes.each do |configured_process|
|
83
|
+
start_process(configured_process)
|
84
|
+
end
|
85
|
+
end
|
86
|
+
|
87
|
+
def supervise
|
88
|
+
loop do
|
89
|
+
break if stopped?
|
90
|
+
|
91
|
+
set_procline
|
92
|
+
process_signal_queue
|
93
|
+
|
94
|
+
unless stopped?
|
95
|
+
reap_and_replace_terminated_forks
|
96
|
+
interruptible_sleep(1)
|
97
|
+
end
|
98
|
+
end
|
99
|
+
ensure
|
100
|
+
shutdown
|
101
|
+
end
|
102
|
+
|
103
|
+
def start_process(configured_process)
|
104
|
+
process_instance = configured_process.instantiate.tap do |instance|
|
105
|
+
instance.supervised_by process
|
106
|
+
instance.mode = :fork
|
107
|
+
end
|
108
|
+
|
109
|
+
process_instance.before_fork
|
110
|
+
pid = fork do
|
111
|
+
process_instance.after_fork
|
112
|
+
process_instance.start
|
113
|
+
end
|
114
|
+
|
115
|
+
configured_processes[pid] = configured_process
|
116
|
+
forks[pid] = process_instance
|
117
|
+
end
|
118
|
+
|
119
|
+
def set_procline
|
120
|
+
procline "supervising #{supervised_processes.join(", ")}"
|
121
|
+
end
|
122
|
+
|
123
|
+
def terminate_gracefully
|
124
|
+
Lepus.instrument(:graceful_termination, process_id: process_id, supervisor_pid: ::Process.pid, supervised_processes: supervised_processes) do |payload|
|
125
|
+
term_forks
|
126
|
+
|
127
|
+
shutdown_timeout = 5
|
128
|
+
puts "\nWaiting up to #{shutdown_timeout} seconds for processes to terminate gracefully..."
|
129
|
+
Timer.wait_until(shutdown_timeout, -> { all_forks_terminated? }) do
|
130
|
+
reap_terminated_forks
|
131
|
+
end
|
132
|
+
|
133
|
+
unless all_forks_terminated?
|
134
|
+
payload[:shutdown_timeout_exceeded] = true
|
135
|
+
terminate_immediately
|
136
|
+
end
|
137
|
+
end
|
138
|
+
end
|
139
|
+
|
140
|
+
def terminate_immediately
|
141
|
+
Lepus.instrument(:immediate_termination, process_id: process_id, supervisor_pid: ::Process.pid, supervised_processes: supervised_processes) do
|
142
|
+
quit_forks
|
143
|
+
end
|
144
|
+
end
|
145
|
+
|
146
|
+
def shutdown
|
147
|
+
Lepus.instrument(:shutdown_process, process: self) do
|
148
|
+
run_process_callbacks(:shutdown) do
|
149
|
+
stop_maintenance_task
|
150
|
+
end
|
151
|
+
end
|
152
|
+
end
|
153
|
+
|
154
|
+
def sync_std_streams
|
155
|
+
$stdout.sync = $stderr.sync = true
|
156
|
+
end
|
157
|
+
|
158
|
+
def supervised_processes
|
159
|
+
forks.keys
|
160
|
+
end
|
161
|
+
|
162
|
+
def term_forks
|
163
|
+
signal_processes(forks.keys, :TERM)
|
164
|
+
end
|
165
|
+
|
166
|
+
def quit_forks
|
167
|
+
signal_processes(forks.keys, :QUIT)
|
168
|
+
end
|
169
|
+
|
170
|
+
def reap_and_replace_terminated_forks
|
171
|
+
loop do
|
172
|
+
pid, status = ::Process.waitpid2(-1, ::Process::WNOHANG)
|
173
|
+
break unless pid
|
174
|
+
|
175
|
+
replace_fork(pid, status)
|
176
|
+
end
|
177
|
+
end
|
178
|
+
|
179
|
+
def reap_terminated_forks
|
180
|
+
loop do
|
181
|
+
pid, _ = ::Process.waitpid2(-1, ::Process::WNOHANG)
|
182
|
+
break unless pid
|
183
|
+
|
184
|
+
configured_processes.delete(pid)
|
185
|
+
end
|
186
|
+
rescue SystemCallError
|
187
|
+
# All children already reaped
|
188
|
+
end
|
189
|
+
|
190
|
+
def replace_fork(pid, status)
|
191
|
+
Lepus.instrument(:replace_fork, supervisor_pid: ::Process.pid, pid: pid, status: status) do |payload|
|
192
|
+
if (terminated_fork = forks.delete(pid))
|
193
|
+
payload[:fork] = terminated_fork
|
194
|
+
|
195
|
+
start_process(configured_processes.delete(pid))
|
196
|
+
end
|
197
|
+
end
|
198
|
+
end
|
199
|
+
|
200
|
+
def all_forks_terminated?
|
201
|
+
forks.empty?
|
202
|
+
end
|
203
|
+
end
|
204
|
+
end
|
data/lib/lepus/timer.rb
ADDED
@@ -0,0 +1,29 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Lepus
|
4
|
+
module Timer
|
5
|
+
extend self
|
6
|
+
|
7
|
+
def wait_until(timeout, condition, &block)
|
8
|
+
if timeout > 0
|
9
|
+
deadline = monotonic_time_now + timeout
|
10
|
+
|
11
|
+
while monotonic_time_now < deadline && !condition.call
|
12
|
+
sleep 0.1
|
13
|
+
yield
|
14
|
+
end
|
15
|
+
else
|
16
|
+
until condition.call
|
17
|
+
sleep 0.5
|
18
|
+
yield
|
19
|
+
end
|
20
|
+
end
|
21
|
+
end
|
22
|
+
|
23
|
+
private
|
24
|
+
|
25
|
+
def monotonic_time_now
|
26
|
+
::Process.clock_gettime(::Process::CLOCK_MONOTONIC)
|
27
|
+
end
|
28
|
+
end
|
29
|
+
end
|