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,136 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Lepus
|
4
|
+
class Process
|
5
|
+
class NotFoundError < RuntimeError
|
6
|
+
def initialize(id)
|
7
|
+
super("Process with id #{id} not found")
|
8
|
+
end
|
9
|
+
end
|
10
|
+
|
11
|
+
ATTRIBUTES = %i[id name pid hostname kind last_heartbeat_at supervisor_id].freeze
|
12
|
+
MEMORY_GRABBER = case RUBY_PLATFORM
|
13
|
+
when /linux/
|
14
|
+
->(pid) {
|
15
|
+
IO.readlines("/proc/#{$$}/status").each do |line|
|
16
|
+
next unless line.start_with?("VmRSS:")
|
17
|
+
break line.split[1].to_i
|
18
|
+
end
|
19
|
+
}
|
20
|
+
when /darwin|bsd/
|
21
|
+
->(pid) {
|
22
|
+
`ps -o pid,rss -p #{pid}`.lines.last.split.last.to_i
|
23
|
+
}
|
24
|
+
else
|
25
|
+
->(pid) { 0 }
|
26
|
+
end
|
27
|
+
|
28
|
+
class << self
|
29
|
+
def register(**attributes)
|
30
|
+
attributes[:id] ||= SecureRandom.uuid
|
31
|
+
Lepus.instrument :register_process, **attributes do |payload|
|
32
|
+
new(**attributes).tap do |process|
|
33
|
+
ProcessRegistry.instance.add(process)
|
34
|
+
payload[:process_id] = process.id
|
35
|
+
end
|
36
|
+
rescue Exception => error # rubocop:disable Lint/RescueException
|
37
|
+
payload[:error] = error
|
38
|
+
raise
|
39
|
+
end
|
40
|
+
end
|
41
|
+
|
42
|
+
def prune(excluding: nil)
|
43
|
+
Lepus.instrument :prune_processes, size: 0 do |payload|
|
44
|
+
arr = prunable
|
45
|
+
arr.delete(excluding) if excluding
|
46
|
+
payload[:size] = arr.size
|
47
|
+
|
48
|
+
arr.each(&:prune)
|
49
|
+
end
|
50
|
+
end
|
51
|
+
|
52
|
+
def prunable
|
53
|
+
ProcessRegistry.instance.all.select do |process|
|
54
|
+
process.last_heartbeat_at && process.last_heartbeat_at < Time.now - Lepus.config.process_alive_threshold
|
55
|
+
end
|
56
|
+
end
|
57
|
+
end
|
58
|
+
|
59
|
+
attr_reader :attributes
|
60
|
+
|
61
|
+
def initialize(**attributes)
|
62
|
+
@attributes = attributes
|
63
|
+
@attributes[:id] ||= SecureRandom.uuid
|
64
|
+
end
|
65
|
+
|
66
|
+
ATTRIBUTES.each do |attribute|
|
67
|
+
define_method(attribute) { attributes[attribute] }
|
68
|
+
end
|
69
|
+
|
70
|
+
def last_heartbeat_at
|
71
|
+
attributes[:last_heartbeat_at]
|
72
|
+
end
|
73
|
+
|
74
|
+
def rss_memory
|
75
|
+
MEMORY_GRABBER.call(pid)
|
76
|
+
end
|
77
|
+
|
78
|
+
def heartbeat
|
79
|
+
now = Time.now
|
80
|
+
Lepus.instrument :heartbeat_process, process: self, rss_memory: 0, last_heartbeat_at: now do |payload|
|
81
|
+
ProcessRegistry.instance.find(id) # ensure process is still registered
|
82
|
+
|
83
|
+
update_attributes(last_heartbeat_at: now)
|
84
|
+
payload[:rss_memory] = rss_memory
|
85
|
+
rescue Exception => error # rubocop:disable Lint/RescueException
|
86
|
+
payload[:error] = error
|
87
|
+
raise
|
88
|
+
end
|
89
|
+
end
|
90
|
+
|
91
|
+
def update_attributes(new_attributes)
|
92
|
+
@attributes = @attributes.merge(new_attributes)
|
93
|
+
end
|
94
|
+
|
95
|
+
def destroy!
|
96
|
+
Lepus.instrument :destroy_process, process: self do |payload|
|
97
|
+
ProcessRegistry.instance.delete(self)
|
98
|
+
rescue Exception => error # rubocop:disable Lint/RescueException
|
99
|
+
payload[:error] = error
|
100
|
+
raise
|
101
|
+
end
|
102
|
+
end
|
103
|
+
|
104
|
+
def deregister(pruned: false)
|
105
|
+
Lepus.instrument :deregister_process, process: self, pruned: pruned do |payload|
|
106
|
+
destroy!
|
107
|
+
|
108
|
+
unless supervised? || pruned
|
109
|
+
supervisees.each(&:deregister)
|
110
|
+
end
|
111
|
+
rescue Exception => error # rubocop:disable Lint/RescueException
|
112
|
+
payload[:error] = error
|
113
|
+
raise
|
114
|
+
end
|
115
|
+
end
|
116
|
+
|
117
|
+
def prune
|
118
|
+
deregister(pruned: true)
|
119
|
+
end
|
120
|
+
|
121
|
+
def supervised?
|
122
|
+
!attributes[:supervisor_id].nil?
|
123
|
+
end
|
124
|
+
|
125
|
+
def eql?(other)
|
126
|
+
other.is_a?(self.class) && other.id == id && other.pid == pid
|
127
|
+
end
|
128
|
+
alias_method :==, :eql?
|
129
|
+
|
130
|
+
private
|
131
|
+
|
132
|
+
def supervisees
|
133
|
+
ProcessRegistry.instance.all.select { |process| process.supervisor_id == id }
|
134
|
+
end
|
135
|
+
end
|
136
|
+
end
|
@@ -0,0 +1,37 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "singleton"
|
4
|
+
|
5
|
+
module Lepus
|
6
|
+
class ProcessRegistry
|
7
|
+
include Singleton
|
8
|
+
|
9
|
+
def initialize
|
10
|
+
@processes = ::Concurrent::Hash.new
|
11
|
+
end
|
12
|
+
|
13
|
+
def add(process)
|
14
|
+
@processes[process.id] = process
|
15
|
+
end
|
16
|
+
|
17
|
+
def delete(process)
|
18
|
+
@processes.delete(process.id)
|
19
|
+
end
|
20
|
+
|
21
|
+
def find(id)
|
22
|
+
@processes[id] || raise(Lepus::Process::NotFoundError.new(id))
|
23
|
+
end
|
24
|
+
|
25
|
+
def exists?(id)
|
26
|
+
@processes.key?(id)
|
27
|
+
end
|
28
|
+
|
29
|
+
def all
|
30
|
+
@processes.values
|
31
|
+
end
|
32
|
+
|
33
|
+
def clear
|
34
|
+
@processes.clear
|
35
|
+
end
|
36
|
+
end
|
37
|
+
end
|
@@ -0,0 +1,50 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Lepus
|
4
|
+
module Processes
|
5
|
+
class Base
|
6
|
+
include Callbacks
|
7
|
+
include AppExecutor
|
8
|
+
include Registrable
|
9
|
+
include Interruptible
|
10
|
+
include Procline
|
11
|
+
|
12
|
+
attr_reader :name
|
13
|
+
|
14
|
+
def initialize(*)
|
15
|
+
@name = generate_name
|
16
|
+
@stopped = false
|
17
|
+
end
|
18
|
+
|
19
|
+
def kind
|
20
|
+
self.class.name.split("::").last
|
21
|
+
end
|
22
|
+
|
23
|
+
def hostname
|
24
|
+
@hostname ||= Socket.gethostname.force_encoding(Encoding::UTF_8)
|
25
|
+
end
|
26
|
+
|
27
|
+
def pid
|
28
|
+
@pid ||= ::Process.pid
|
29
|
+
end
|
30
|
+
|
31
|
+
def metadata
|
32
|
+
{}
|
33
|
+
end
|
34
|
+
|
35
|
+
def stop
|
36
|
+
@stopped = true
|
37
|
+
end
|
38
|
+
|
39
|
+
private
|
40
|
+
|
41
|
+
def generate_name
|
42
|
+
[kind.downcase, SecureRandom.hex(10)].join("-")
|
43
|
+
end
|
44
|
+
|
45
|
+
def stopped?
|
46
|
+
@stopped
|
47
|
+
end
|
48
|
+
end
|
49
|
+
end
|
50
|
+
end
|
@@ -0,0 +1,72 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Lepus::Processes
|
4
|
+
module Callbacks
|
5
|
+
def self.included(base)
|
6
|
+
base.extend(ClassMethods)
|
7
|
+
base.send :include, InstanceMethods
|
8
|
+
end
|
9
|
+
|
10
|
+
module InstanceMethods
|
11
|
+
def run_process_callbacks(name)
|
12
|
+
self.class.send(:"before_#{name}_callbacks").each do |method|
|
13
|
+
send(method)
|
14
|
+
end
|
15
|
+
|
16
|
+
result = yield if block_given?
|
17
|
+
|
18
|
+
self.class.send(:"after_#{name}_callbacks").each do |method|
|
19
|
+
send(method)
|
20
|
+
end
|
21
|
+
|
22
|
+
result
|
23
|
+
end
|
24
|
+
end
|
25
|
+
|
26
|
+
module ClassMethods
|
27
|
+
def inherited(base)
|
28
|
+
base.instance_variable_set(:@before_boot_callbacks, before_boot_callbacks.dup)
|
29
|
+
base.instance_variable_set(:@after_boot_callbacks, after_boot_callbacks.dup)
|
30
|
+
base.instance_variable_set(:@before_shutdown_callbacks, before_shutdown_callbacks.dup)
|
31
|
+
base.instance_variable_set(:@after_shutdown_callbacks, after_shutdown_callbacks.dup)
|
32
|
+
super
|
33
|
+
end
|
34
|
+
|
35
|
+
def before_boot(*methods)
|
36
|
+
@before_boot_callbacks ||= []
|
37
|
+
@before_boot_callbacks.concat methods
|
38
|
+
end
|
39
|
+
|
40
|
+
def after_boot(*methods)
|
41
|
+
@after_boot_callbacks ||= []
|
42
|
+
@after_boot_callbacks.concat methods
|
43
|
+
end
|
44
|
+
|
45
|
+
def before_shutdown(*methods)
|
46
|
+
@before_shutdown_callbacks ||= []
|
47
|
+
@before_shutdown_callbacks.concat methods
|
48
|
+
end
|
49
|
+
|
50
|
+
def after_shutdown(*methods)
|
51
|
+
@after_shutdown_callbacks ||= []
|
52
|
+
@after_shutdown_callbacks.concat methods
|
53
|
+
end
|
54
|
+
|
55
|
+
def before_boot_callbacks
|
56
|
+
@before_boot_callbacks || []
|
57
|
+
end
|
58
|
+
|
59
|
+
def after_boot_callbacks
|
60
|
+
@after_boot_callbacks || []
|
61
|
+
end
|
62
|
+
|
63
|
+
def before_shutdown_callbacks
|
64
|
+
@before_shutdown_callbacks || []
|
65
|
+
end
|
66
|
+
|
67
|
+
def after_shutdown_callbacks
|
68
|
+
@after_shutdown_callbacks || []
|
69
|
+
end
|
70
|
+
end
|
71
|
+
end
|
72
|
+
end
|
@@ -0,0 +1,113 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Lepus::Processes
|
4
|
+
class Consumer < Base
|
5
|
+
include Runnable
|
6
|
+
|
7
|
+
attr_reader :consumer_class
|
8
|
+
|
9
|
+
def initialize(class_name:, **options)
|
10
|
+
@consumer_class = class_name
|
11
|
+
@consumer_class = Lepus::Primitive::String.new(@consumer_class).constantize if @consumer_class.is_a?(String)
|
12
|
+
|
13
|
+
super(**options)
|
14
|
+
end
|
15
|
+
|
16
|
+
def metadata
|
17
|
+
super.merge(consumer_class: consumer_class.to_s)
|
18
|
+
end
|
19
|
+
|
20
|
+
def before_fork
|
21
|
+
return unless @consumer_class.respond_to?(:before_fork, true)
|
22
|
+
|
23
|
+
@consumer_class.send(:before_fork)
|
24
|
+
end
|
25
|
+
|
26
|
+
def after_fork
|
27
|
+
return unless @consumer_class.respond_to?(:after_fork, true)
|
28
|
+
|
29
|
+
@consumer_class.send(:after_fork)
|
30
|
+
end
|
31
|
+
|
32
|
+
private
|
33
|
+
|
34
|
+
SLEEP_INTERVAL = 5
|
35
|
+
|
36
|
+
def run
|
37
|
+
wrap_in_app_executor do
|
38
|
+
setup_consumer! # initialize bunny consumer within the #run method to ensure the process is running in the correct thread
|
39
|
+
end
|
40
|
+
|
41
|
+
loop do
|
42
|
+
break if shutting_down?
|
43
|
+
|
44
|
+
wrap_in_app_executor do
|
45
|
+
interruptible_sleep(SLEEP_INTERVAL)
|
46
|
+
end
|
47
|
+
end
|
48
|
+
ensure
|
49
|
+
Lepus.instrument(:shutdown_process, process: self) do
|
50
|
+
run_process_callbacks(:shutdown) { shutdown }
|
51
|
+
end
|
52
|
+
end
|
53
|
+
|
54
|
+
def shutdown
|
55
|
+
@subscriptions.to_a.each(&:cancel)
|
56
|
+
@channel&.close
|
57
|
+
@bunny&.close
|
58
|
+
|
59
|
+
super
|
60
|
+
end
|
61
|
+
|
62
|
+
def set_procline
|
63
|
+
procline consumer_class.name
|
64
|
+
end
|
65
|
+
|
66
|
+
def setup_consumer!
|
67
|
+
if consumer_class.config.nil?
|
68
|
+
raise Lepus::InvalidConsumerConfigError, "Consumer #{consumer_class.name} has no configuration"
|
69
|
+
end
|
70
|
+
|
71
|
+
@bunny = Thread.current[:lepus_bunny] || Lepus.config.create_connection
|
72
|
+
@channel = Thread.current[:lepus_channel] || begin
|
73
|
+
@bunny.create_channel(nil, 1, true).tap do |channel|
|
74
|
+
channel.prefetch(1) # @TODO make this configurable
|
75
|
+
channel.on_uncaught_exception { |error|
|
76
|
+
handle_thread_error(error)
|
77
|
+
}
|
78
|
+
end
|
79
|
+
end
|
80
|
+
|
81
|
+
@exchange = @channel.exchange(*consumer_class.config.exchange_args)
|
82
|
+
if (args = consumer_class.config.retry_queue_args)
|
83
|
+
@retry_queue = @channel.queue(*args)
|
84
|
+
end
|
85
|
+
if (args = consumer_class.config.error_queue_args)
|
86
|
+
@error_queue = @channel.queue(*args)
|
87
|
+
end
|
88
|
+
|
89
|
+
@subscriptions = Array.new((_threads = 1)) do |n| # may add multiple consumers in the future
|
90
|
+
main_queue = @channel.queue(*consumer_class.config.consumer_queue_args)
|
91
|
+
consumer_class.config.binds_args.each do |opts|
|
92
|
+
main_queue.bind(@exchange, **opts)
|
93
|
+
end
|
94
|
+
|
95
|
+
consumer_instance = consumer_class.new
|
96
|
+
consumer_wrapper = Lepus::ConsumerWrapper.new(
|
97
|
+
consumer_instance,
|
98
|
+
main_queue.channel,
|
99
|
+
main_queue,
|
100
|
+
"#{consumer_class.name}-#{n + 1}"
|
101
|
+
)
|
102
|
+
consumer_wrapper.on_delivery do |delivery_info, metadata, payload|
|
103
|
+
consumer_wrapper.process_delivery(delivery_info, metadata, payload)
|
104
|
+
end
|
105
|
+
main_queue.subscribe_with(consumer_wrapper)
|
106
|
+
end
|
107
|
+
rescue Bunny::TCPConnectionFailed, Bunny::PossibleAuthenticationFailureError
|
108
|
+
raise Lepus::ShutdownError
|
109
|
+
rescue Lepus::InvalidConsumerConfigError
|
110
|
+
raise Lepus::ShutdownError
|
111
|
+
end
|
112
|
+
end
|
113
|
+
end
|
@@ -0,0 +1,38 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Lepus::Processes
|
4
|
+
module Interruptible
|
5
|
+
def wake_up
|
6
|
+
interrupt
|
7
|
+
end
|
8
|
+
|
9
|
+
private
|
10
|
+
|
11
|
+
SELF_PIPE_BLOCK_SIZE = 11
|
12
|
+
|
13
|
+
def interrupt
|
14
|
+
self_pipe[:writer].write_nonblock(".")
|
15
|
+
rescue Errno::EAGAIN, Errno::EINTR
|
16
|
+
# Ignore writes that would block and retry
|
17
|
+
# if another signal arrived while writing
|
18
|
+
retry
|
19
|
+
end
|
20
|
+
|
21
|
+
def interruptible_sleep(time)
|
22
|
+
if time > 0 && self_pipe[:reader].wait_readable(time)
|
23
|
+
loop { self_pipe[:reader].read_nonblock(SELF_PIPE_BLOCK_SIZE) }
|
24
|
+
end
|
25
|
+
rescue Errno::EAGAIN, Errno::EINTR
|
26
|
+
end
|
27
|
+
|
28
|
+
# Self-pipe for signal-handling (http://cr.yp.to/docs/selfpipe.html)
|
29
|
+
def self_pipe
|
30
|
+
@self_pipe ||= create_self_pipe
|
31
|
+
end
|
32
|
+
|
33
|
+
def create_self_pipe
|
34
|
+
reader, writer = IO.pipe
|
35
|
+
{reader: reader, writer: writer}
|
36
|
+
end
|
37
|
+
end
|
38
|
+
end
|
@@ -0,0 +1,67 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Lepus::Processes
|
4
|
+
module Registrable
|
5
|
+
def self.included(base)
|
6
|
+
base.send :include, InstanceMethods
|
7
|
+
base.class_eval do
|
8
|
+
after_boot :register
|
9
|
+
after_boot :launch_heartbeat
|
10
|
+
|
11
|
+
before_shutdown :stop_heartbeat
|
12
|
+
after_shutdown :deregister
|
13
|
+
end
|
14
|
+
end
|
15
|
+
|
16
|
+
module InstanceMethods
|
17
|
+
def process_id
|
18
|
+
process&.id
|
19
|
+
end
|
20
|
+
|
21
|
+
private
|
22
|
+
|
23
|
+
attr_accessor :process
|
24
|
+
|
25
|
+
def register
|
26
|
+
@process = Lepus::Process.register(
|
27
|
+
kind: kind,
|
28
|
+
name: name,
|
29
|
+
pid: pid,
|
30
|
+
hostname: hostname,
|
31
|
+
supervisor_id: respond_to?(:supervisor) ? supervisor&.id : nil
|
32
|
+
)
|
33
|
+
end
|
34
|
+
|
35
|
+
def deregister
|
36
|
+
process&.deregister
|
37
|
+
end
|
38
|
+
|
39
|
+
def registered?
|
40
|
+
!!process
|
41
|
+
end
|
42
|
+
|
43
|
+
def launch_heartbeat
|
44
|
+
@heartbeat_task = ::Concurrent::TimerTask.new(execution_interval: Lepus.config.process_heartbeat_interval) do
|
45
|
+
wrap_in_app_executor { heartbeat }
|
46
|
+
end
|
47
|
+
|
48
|
+
@heartbeat_task.add_observer do |_time, _result, error|
|
49
|
+
handle_thread_error(error) if error
|
50
|
+
end
|
51
|
+
|
52
|
+
@heartbeat_task.execute
|
53
|
+
end
|
54
|
+
|
55
|
+
def stop_heartbeat
|
56
|
+
@heartbeat_task&.shutdown
|
57
|
+
end
|
58
|
+
|
59
|
+
def heartbeat
|
60
|
+
process.heartbeat
|
61
|
+
rescue Process::NotFoundError
|
62
|
+
self.process = nil
|
63
|
+
wake_up
|
64
|
+
end
|
65
|
+
end
|
66
|
+
end
|
67
|
+
end
|
@@ -0,0 +1,102 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Lepus::Processes
|
4
|
+
module Runnable
|
5
|
+
include Supervised
|
6
|
+
|
7
|
+
class InquiryMode
|
8
|
+
def initialize(mode)
|
9
|
+
@mode = mode.to_sym
|
10
|
+
end
|
11
|
+
|
12
|
+
%i[inline async fork].each do |value|
|
13
|
+
define_method(:"#{value}?") { @mode == value }
|
14
|
+
end
|
15
|
+
end
|
16
|
+
|
17
|
+
def start
|
18
|
+
boot
|
19
|
+
|
20
|
+
if running_async?
|
21
|
+
@thread = create_thread { run }
|
22
|
+
else
|
23
|
+
run
|
24
|
+
end
|
25
|
+
end
|
26
|
+
|
27
|
+
def stop
|
28
|
+
super
|
29
|
+
|
30
|
+
wake_up
|
31
|
+
@thread&.join
|
32
|
+
end
|
33
|
+
|
34
|
+
def mode=(mode)
|
35
|
+
@mode = InquiryMode.new(mode)
|
36
|
+
end
|
37
|
+
|
38
|
+
private
|
39
|
+
|
40
|
+
DEFAULT_MODE = :async
|
41
|
+
|
42
|
+
def mode
|
43
|
+
@mode ||= InquiryMode.new(DEFAULT_MODE)
|
44
|
+
end
|
45
|
+
|
46
|
+
def boot
|
47
|
+
Lepus.instrument(:start_process, process: self) do
|
48
|
+
run_process_callbacks(:boot) do
|
49
|
+
if running_as_fork?
|
50
|
+
register_signal_handlers
|
51
|
+
set_procline
|
52
|
+
end
|
53
|
+
end
|
54
|
+
end
|
55
|
+
end
|
56
|
+
|
57
|
+
def shutting_down?
|
58
|
+
stopped? || (running_as_fork? && supervisor_went_away?) || !registered? # || finished?
|
59
|
+
end
|
60
|
+
|
61
|
+
def run
|
62
|
+
raise NotImplementedError
|
63
|
+
end
|
64
|
+
|
65
|
+
# @TODO Add it to the inline mode
|
66
|
+
# def finished?
|
67
|
+
# running_inline? && all_work_completed?
|
68
|
+
# end
|
69
|
+
|
70
|
+
# def all_work_completed?
|
71
|
+
# false
|
72
|
+
# end
|
73
|
+
|
74
|
+
def shutdown
|
75
|
+
end
|
76
|
+
|
77
|
+
def set_procline
|
78
|
+
end
|
79
|
+
|
80
|
+
# def running_inline?
|
81
|
+
# mode.inline?
|
82
|
+
# end
|
83
|
+
|
84
|
+
def running_async?
|
85
|
+
mode.async?
|
86
|
+
end
|
87
|
+
|
88
|
+
def running_as_fork?
|
89
|
+
mode.fork?
|
90
|
+
end
|
91
|
+
|
92
|
+
def create_thread(&block)
|
93
|
+
Thread.new do
|
94
|
+
Thread.current.name = name
|
95
|
+
yield
|
96
|
+
rescue Exception => exception # rubocop:disable Lint/RescueException
|
97
|
+
handle_thread_error(exception)
|
98
|
+
raise
|
99
|
+
end
|
100
|
+
end
|
101
|
+
end
|
102
|
+
end
|
@@ -0,0 +1,44 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Lepus::Processes
|
4
|
+
module Supervised
|
5
|
+
def self.included(base)
|
6
|
+
base.send :include, InstanceMethods
|
7
|
+
base.class_eval do
|
8
|
+
attr_reader :supervisor
|
9
|
+
end
|
10
|
+
end
|
11
|
+
|
12
|
+
module InstanceMethods
|
13
|
+
def supervised_by(process)
|
14
|
+
@supervisor = process
|
15
|
+
end
|
16
|
+
|
17
|
+
private
|
18
|
+
|
19
|
+
def set_procline
|
20
|
+
procline "waiting"
|
21
|
+
end
|
22
|
+
|
23
|
+
def supervisor_went_away?
|
24
|
+
supervised? && supervisor.pid != ::Process.ppid
|
25
|
+
end
|
26
|
+
|
27
|
+
def supervised?
|
28
|
+
!!supervisor
|
29
|
+
end
|
30
|
+
|
31
|
+
def register_signal_handlers
|
32
|
+
%w[INT TERM].each do |signal|
|
33
|
+
trap(signal) do
|
34
|
+
stop
|
35
|
+
end
|
36
|
+
end
|
37
|
+
|
38
|
+
trap(:QUIT) do
|
39
|
+
exit!
|
40
|
+
end
|
41
|
+
end
|
42
|
+
end
|
43
|
+
end
|
44
|
+
end
|