main_loop 0.1.3.16874 → 0.1.4.367214
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/README.md +419 -21
- data/lib/main_loop/bus.rb +81 -5
- data/lib/main_loop/dispatcher.rb +102 -5
- data/lib/main_loop/handler.rb +102 -7
- data/lib/main_loop/loop.rb +132 -7
- data/lib/main_loop/process_handler.rb +91 -13
- data/lib/main_loop/thread_handler.rb +80 -12
- data/lib/main_loop/version.rb +1 -1
- data/lib/main_loop.rb +41 -2
- metadata +34 -6
data/lib/main_loop/dispatcher.rb
CHANGED
|
@@ -1,13 +1,50 @@
|
|
|
1
1
|
require 'monitor'
|
|
2
2
|
require 'logger'
|
|
3
3
|
|
|
4
|
+
# = MainLoop::Dispatcher
|
|
5
|
+
#
|
|
6
|
+
# Координирует обработчиков, управляет жизненным циклом, обрабатывает сигнал терминации с таймаутом.
|
|
7
|
+
#
|
|
8
|
+
# Использует {MonitorMixin} для потоко-безопасности. Управляет списком обработчиков ({handlers})
|
|
9
|
+
# и обеспечивает корректное завершение всех обработчиков при получении сигнала терминации.
|
|
10
|
+
#
|
|
11
|
+
# == Жизненный цикл обработчиков
|
|
12
|
+
#
|
|
13
|
+
# 1. Регистрация через {add_handler}
|
|
14
|
+
# 2. Обработка сигнала терминации через {term}
|
|
15
|
+
# 3. Если не завершились за timeout → {kill}
|
|
16
|
+
# 4. {try_exit!} когда все завершены
|
|
17
|
+
#
|
|
18
|
+
# == Пример использования
|
|
19
|
+
#
|
|
20
|
+
# bus = MainLoop::Bus.new
|
|
21
|
+
# dispatcher = MainLoop::Dispatcher.new(bus, timeout: 10, logger: logger)
|
|
22
|
+
#
|
|
23
|
+
# MainLoop::ProcessHandler.new dispatcher, 'worker' do
|
|
24
|
+
# # код процесса
|
|
25
|
+
# end
|
|
26
|
+
#
|
|
27
|
+
# dispatcher.term # инициировать терминацию
|
|
28
|
+
#
|
|
29
|
+
# == См. также
|
|
30
|
+
# - {MainLoop::Bus} — канал событий
|
|
31
|
+
# - {MainLoop::Handler} — базовый класс обработчиков
|
|
32
|
+
# - {MainLoop::Loop} — координирует Dispatcher
|
|
33
|
+
|
|
4
34
|
module MainLoop
|
|
5
35
|
class Dispatcher
|
|
6
|
-
|
|
7
36
|
include MonitorMixin
|
|
8
37
|
|
|
9
38
|
attr_reader :bus, :handlers, :logger
|
|
10
39
|
|
|
40
|
+
# == Инициализация
|
|
41
|
+
#
|
|
42
|
+
# @param bus [Bus] канал событий
|
|
43
|
+
# @param timeout [Integer] таймаут для принудительного завершения в секундах (по умолчанию 5)
|
|
44
|
+
# @param logger [Logger] логгер (по умолчанию Logger.new(nil))
|
|
45
|
+
# @option bus [Bus]
|
|
46
|
+
# @option timeout [Integer] 5
|
|
47
|
+
# @option logger [Logger] nil
|
|
11
48
|
def initialize(bus, timeout: 5, logger: nil)
|
|
12
49
|
super()
|
|
13
50
|
@bus = bus
|
|
@@ -17,23 +54,42 @@ module MainLoop
|
|
|
17
54
|
@exit_code = 0
|
|
18
55
|
end
|
|
19
56
|
|
|
57
|
+
# == Обработка завершения процессов
|
|
58
|
+
#
|
|
59
|
+
# Обрабатывает массив завершенных процессов.
|
|
60
|
+
#
|
|
61
|
+
# @param statuses [Array<Array>] массив пар (pid, status)
|
|
62
|
+
# @example
|
|
63
|
+
# dispatcher.reap([[123, status], [456, nil]])
|
|
20
64
|
def reap(statuses)
|
|
21
65
|
statuses.each do |(pid, status)|
|
|
22
66
|
reap_by_id(pid, status)
|
|
23
67
|
end
|
|
24
68
|
end
|
|
25
69
|
|
|
70
|
+
# == Обработка завершения процесса по ID
|
|
71
|
+
#
|
|
72
|
+
# Находит обработчика по ID и вызывает его {Handler#reap}.
|
|
73
|
+
#
|
|
74
|
+
# @param id [String] идентификатор обработчика (pid для процессов, object_id для потоков)
|
|
75
|
+
# @param status [Process::Status|nil] статус завершения или nil если неизвестен
|
|
26
76
|
def reap_by_id(id, status)
|
|
27
77
|
synchronize do
|
|
28
78
|
if (handler = handlers.find {|h| h.id == id })
|
|
29
|
-
logger.info("Reap handler #{handler.name.inspect}. Status: #{status
|
|
79
|
+
logger.info("Reap handler #{handler.name.inspect}. Status: #{status&.inspect}")
|
|
30
80
|
handler.reap(status)
|
|
31
81
|
else
|
|
32
|
-
logger.debug("Reap unknown handler. Status: #{status
|
|
82
|
+
logger.debug("Reap unknown handler. Status: #{status&.inspect}. Skipped")
|
|
33
83
|
end
|
|
34
84
|
end
|
|
35
85
|
end
|
|
36
86
|
|
|
87
|
+
# == Регистрация обработчика
|
|
88
|
+
#
|
|
89
|
+
# Добавляет обработчик в список. Если уже происходит терминация,
|
|
90
|
+
# сразу посылает `term` новому обработчику.
|
|
91
|
+
#
|
|
92
|
+
# @param handler [Handler] обработчик для регистрации
|
|
37
93
|
def add_handler(handler)
|
|
38
94
|
synchronize do
|
|
39
95
|
handler.term if terminating?
|
|
@@ -41,10 +97,24 @@ module MainLoop
|
|
|
41
97
|
end
|
|
42
98
|
end
|
|
43
99
|
|
|
100
|
+
# == Проверка терминации
|
|
101
|
+
#
|
|
102
|
+
# @return [Time|nil] момент начала терминации или nil если не терминация
|
|
44
103
|
def terminating?
|
|
45
104
|
@terminating_at
|
|
46
105
|
end
|
|
47
106
|
|
|
107
|
+
# == Инициировать терминацию
|
|
108
|
+
#
|
|
109
|
+
# Отправляет сигнал терминации всем обработчикам.
|
|
110
|
+
# Если уже в процессе терминации — принудительное завершение (kill).
|
|
111
|
+
#
|
|
112
|
+
# Если это первый вызов:
|
|
113
|
+
# - Устанавливает @terminating_at = Time.now
|
|
114
|
+
# - Отправляет term каждому обработчику
|
|
115
|
+
#
|
|
116
|
+
# Если уже терминация:
|
|
117
|
+
# - Отправляет kill каждому обработчику
|
|
48
118
|
def term
|
|
49
119
|
synchronize do
|
|
50
120
|
if terminating?
|
|
@@ -58,11 +128,19 @@ module MainLoop
|
|
|
58
128
|
end
|
|
59
129
|
end
|
|
60
130
|
|
|
131
|
+
# == Отправить сигнал аварийного завершения
|
|
132
|
+
#
|
|
133
|
+
# Устанавливает 코드 выхода 3 и инициирует терминацию.
|
|
134
|
+
# Если уже терминация — ничего не делает.
|
|
61
135
|
def crash
|
|
62
136
|
@exit_code = 3
|
|
63
137
|
term unless terminating?
|
|
64
138
|
end
|
|
65
139
|
|
|
140
|
+
# == Тик цикла диспетчера
|
|
141
|
+
#
|
|
142
|
+
# Вызывается в каждом цикле {MainLoop#start_loop_forever}.
|
|
143
|
+
# Проверяет необходимость принудительного завершения по timeout.
|
|
66
144
|
def tick
|
|
67
145
|
log_status if logger.debug?
|
|
68
146
|
return unless terminating?
|
|
@@ -76,14 +154,30 @@ module MainLoop
|
|
|
76
154
|
handlers.each(&:kill)
|
|
77
155
|
end
|
|
78
156
|
|
|
157
|
+
# == Проверка необходимости принудительного завершения
|
|
158
|
+
#
|
|
159
|
+
# Проверяет, превышен ли timeout с момента начала терминации.
|
|
160
|
+
#
|
|
161
|
+
# @return [Boolean] true если timeout превышен
|
|
79
162
|
def need_force_kill?
|
|
80
163
|
@terminating_at && (Time.now - @terminating_at) >= @timeout
|
|
81
164
|
end
|
|
82
165
|
|
|
166
|
+
# == Получить список PID процессов
|
|
167
|
+
#
|
|
168
|
+
# @return [Array<Integer>] массив PID всех процессовых обработчиков
|
|
83
169
|
def pids
|
|
84
170
|
handlers.map{|h| h.pid rescue nil }.compact
|
|
85
171
|
end
|
|
86
172
|
|
|
173
|
+
# == Завершить программу
|
|
174
|
+
#
|
|
175
|
+
# Если все обработчики завершены, вызывает exit с соответствующим кодом.
|
|
176
|
+
#
|
|
177
|
+
# Код выхода:
|
|
178
|
+
# - @exit_code если все обработчики завершились успешно
|
|
179
|
+
# - 1 если любой обработчик завершился с ошибкой
|
|
180
|
+
#
|
|
87
181
|
# :nocov:
|
|
88
182
|
def try_exit!
|
|
89
183
|
synchronize do
|
|
@@ -97,6 +191,11 @@ module MainLoop
|
|
|
97
191
|
end
|
|
98
192
|
# :nocov:
|
|
99
193
|
|
|
194
|
+
# == Логировать статус
|
|
195
|
+
#
|
|
196
|
+
# Логирует текущее состояние обработчиков (DEBUG уровень).
|
|
197
|
+
# Формат: "Total:N Running:M Finihsed:K. TERM"
|
|
198
|
+
#
|
|
100
199
|
# :nocov:
|
|
101
200
|
def log_status
|
|
102
201
|
total = handlers.size
|
|
@@ -106,7 +205,5 @@ module MainLoop
|
|
|
106
205
|
logger.debug("Total:#{total} Running:#{running} Finihsed:#{finihsed}. #{term_text}".strip)
|
|
107
206
|
end
|
|
108
207
|
# :nocov:
|
|
109
|
-
|
|
110
208
|
end
|
|
111
209
|
end
|
|
112
|
-
|
data/lib/main_loop/handler.rb
CHANGED
|
@@ -1,10 +1,61 @@
|
|
|
1
1
|
require 'logger'
|
|
2
2
|
|
|
3
|
+
# = MainLoop::Handler
|
|
4
|
+
#
|
|
5
|
+
# Абстрактный базовый класс для обработчиков процессов и потоков.
|
|
6
|
+
#
|
|
7
|
+
# Определяет интерфейс и общую логику:
|
|
8
|
+
# - управление retry_count (количество повторов)
|
|
9
|
+
# - логика handle_retry
|
|
10
|
+
# - предикаты (finished?, success?, running?, terminating?)
|
|
11
|
+
# - обратный вызов on_term
|
|
12
|
+
#
|
|
13
|
+
# == Абстрактные методы (для реализации в подклассах)
|
|
14
|
+
#
|
|
15
|
+
# - {#id} — идентификатор обработчика (pid для процессов, object_id для потоков)
|
|
16
|
+
# - {#term} — отправка сигнала терминации
|
|
17
|
+
# - {#run} — запуск обработчика
|
|
18
|
+
# - {#kill} — принудительное завершение
|
|
19
|
+
# - {#reap(status)} — обработка завершения процесса/потока
|
|
20
|
+
#
|
|
21
|
+
# == Пример использования
|
|
22
|
+
#
|
|
23
|
+
# class MyHandler < MainLoop::Handler
|
|
24
|
+
# def id
|
|
25
|
+
# @process_id
|
|
26
|
+
# end
|
|
27
|
+
#
|
|
28
|
+
# def term
|
|
29
|
+
# Process.kill('TERM', @process_id)
|
|
30
|
+
# end
|
|
31
|
+
#
|
|
32
|
+
# def run
|
|
33
|
+
# @process_id = Process.fork { yield }
|
|
34
|
+
# end
|
|
35
|
+
#
|
|
36
|
+
# def kill
|
|
37
|
+
# Process.kill('KILL', @process_id)
|
|
38
|
+
# end
|
|
39
|
+
#
|
|
40
|
+
# def reap(status)
|
|
41
|
+
# # обработка завершения
|
|
42
|
+
# end
|
|
43
|
+
# end
|
|
44
|
+
#
|
|
45
|
+
# == См. также
|
|
46
|
+
# - {MainLoop::ProcessHandler} — реализация для процессов
|
|
47
|
+
# - {MainLoop::ThreadHandler} — реализация для потоков
|
|
48
|
+
|
|
3
49
|
module MainLoop
|
|
4
50
|
class Handler
|
|
5
|
-
|
|
6
51
|
attr_reader :dispatcher, :name, :logger
|
|
7
52
|
|
|
53
|
+
# == Инициализация
|
|
54
|
+
#
|
|
55
|
+
# @param dispatcher [Dispatcher] ссылка на диспетчер
|
|
56
|
+
# @param name [String] имя обработчика
|
|
57
|
+
# @param retry_count [Integer, :unlimited] количество повторов после завершения
|
|
58
|
+
# @param logger [Logger] логгер (по умолчанию Logger.new(nil))
|
|
8
59
|
def initialize(dispatcher, name, *_args, retry_count: 0, logger: nil, **_kwargs)
|
|
9
60
|
@dispatcher = dispatcher
|
|
10
61
|
@name = name
|
|
@@ -14,82 +65,126 @@ module MainLoop
|
|
|
14
65
|
@handler_type = 'Unknown'
|
|
15
66
|
end
|
|
16
67
|
|
|
68
|
+
# == Идентификатор (абстрактный)
|
|
69
|
+
#
|
|
70
|
+
# @return [String] идентификатор обработчика
|
|
71
|
+
# @raise [RuntimeError] если не реализован в подклассе
|
|
17
72
|
# :nocov:
|
|
18
73
|
def id(*_args)
|
|
19
74
|
raise 'not implemented!'
|
|
20
75
|
end
|
|
21
76
|
# :nocov:
|
|
22
77
|
|
|
78
|
+
# == Терминация (абстрактный)
|
|
79
|
+
#
|
|
80
|
+
# Отправляет сигнал терминации обработчику.
|
|
81
|
+
# @raise [RuntimeError] если не реализован в подклассе
|
|
23
82
|
# :nocov:
|
|
24
83
|
def term(*_args)
|
|
25
84
|
raise 'not implemented!'
|
|
26
85
|
end
|
|
27
86
|
# :nocov:
|
|
28
87
|
|
|
88
|
+
# == Запуск (абстрактный)
|
|
89
|
+
#
|
|
90
|
+
# Запускает обработчик.
|
|
91
|
+
# @raise [RuntimeError] если не реализован в подклассе
|
|
29
92
|
# :nocov:
|
|
30
93
|
def run(*_args)
|
|
31
94
|
raise 'not implemented!'
|
|
32
95
|
end
|
|
33
96
|
# :nocov:
|
|
34
97
|
|
|
98
|
+
# == Принудительное завершение (абстрактный)
|
|
99
|
+
#
|
|
100
|
+
# Принудительно завершает обработчик.
|
|
101
|
+
# @raise [RuntimeError] если не реализован в подклассе
|
|
35
102
|
# :nocov:
|
|
36
103
|
def kill(*_args)
|
|
37
104
|
raise 'not implemented!'
|
|
38
105
|
end
|
|
39
106
|
# :nocov:
|
|
40
107
|
|
|
108
|
+
# == Обработка завершения (абстрактный)
|
|
109
|
+
#
|
|
110
|
+
# @param status [Process::Status|nil] статус завершения
|
|
111
|
+
# @raise [RuntimeError] если не реализован в подклассе
|
|
41
112
|
# :nocov:
|
|
42
113
|
def reap(*_args)
|
|
43
114
|
raise 'not implemented!'
|
|
44
115
|
end
|
|
45
116
|
# :nocov:
|
|
46
117
|
|
|
47
|
-
#
|
|
118
|
+
# == Публикация события
|
|
119
|
+
#
|
|
120
|
+
# Отправляет событие в канал событий диспетчера.
|
|
121
|
+
#
|
|
122
|
+
# @param event [String, Symbol] событие для отправки
|
|
48
123
|
def publish(event)
|
|
49
124
|
dispatcher.bus.puts(event)
|
|
50
125
|
end
|
|
51
|
-
# :nocov:
|
|
52
126
|
|
|
127
|
+
# == Установить обратный вызов терминации
|
|
128
|
+
#
|
|
129
|
+
# @param block [Proc] блок кода, который будет вызван при терминации
|
|
53
130
|
def on_term &block
|
|
54
131
|
@on_term = block
|
|
55
132
|
end
|
|
56
133
|
|
|
134
|
+
# == Проверка завершения
|
|
135
|
+
#
|
|
136
|
+
# @return [Boolean] true если обработчик завершен
|
|
57
137
|
# :nocov:
|
|
58
138
|
def finished?
|
|
59
139
|
@finished
|
|
60
140
|
end
|
|
61
141
|
# :nocov:
|
|
62
142
|
|
|
143
|
+
# == Проверка успешного завершения
|
|
144
|
+
#
|
|
145
|
+
# @return [Boolean] true если завершен и успешно
|
|
63
146
|
# :nocov:
|
|
64
147
|
def success?
|
|
65
148
|
finished? && @success
|
|
66
149
|
end
|
|
67
150
|
# :nocov:
|
|
68
151
|
|
|
152
|
+
# == Проверка запущенности
|
|
153
|
+
#
|
|
154
|
+
# @return [Boolean] true если обработчик работает
|
|
69
155
|
# :nocov:
|
|
70
156
|
def running?
|
|
71
157
|
!finished?
|
|
72
158
|
end
|
|
73
159
|
# :nocov:
|
|
74
160
|
|
|
161
|
+
# == Проверка терминации
|
|
162
|
+
#
|
|
163
|
+
# @return [Time|nil] момент начала терминации или nil
|
|
75
164
|
# :nocov:
|
|
76
165
|
def terminating?
|
|
77
166
|
@terminating_at
|
|
78
167
|
end
|
|
79
168
|
# :nocov:
|
|
80
169
|
|
|
170
|
+
# == Логика повторов
|
|
171
|
+
#
|
|
172
|
+
# Управляет повторами после завершения:
|
|
173
|
+
# - :unlimited — бесконечные повторы
|
|
174
|
+
# - Integer >= 0 — декремент и повтор
|
|
175
|
+
# - иначе — отправляет term через bus
|
|
176
|
+
#
|
|
177
|
+
# @return void
|
|
81
178
|
def handle_retry
|
|
82
179
|
if @retry_count == :unlimited
|
|
83
180
|
logger.info "#{@handler_type}[#{name}] retry...."
|
|
84
|
-
self.run
|
|
181
|
+
self.run
|
|
85
182
|
elsif @retry_count && (@retry_count -= 1) >= 0
|
|
86
183
|
logger.info "#{@handler_type}[#{name}] retry...."
|
|
87
|
-
self.run
|
|
184
|
+
self.run
|
|
88
185
|
else
|
|
89
186
|
publish(:term)
|
|
90
187
|
end
|
|
91
188
|
end
|
|
92
|
-
|
|
93
189
|
end
|
|
94
190
|
end
|
|
95
|
-
|
data/lib/main_loop/loop.rb
CHANGED
|
@@ -2,13 +2,49 @@ require 'logger'
|
|
|
2
2
|
require 'timeouter'
|
|
3
3
|
|
|
4
4
|
module MainLoop
|
|
5
|
-
|
|
5
|
+
# Сигналы для терминации
|
|
6
|
+
# @return [Array<String>]
|
|
6
7
|
TERM_SIGNALS = %w[INT TERM].freeze
|
|
7
8
|
|
|
8
|
-
|
|
9
|
+
# = MainLoop::Loop
|
|
10
|
+
#
|
|
11
|
+
# Главный цикл управления, запускает обработку сигналов, обрабатывает события из Bus.
|
|
12
|
+
#
|
|
13
|
+
# == Жизненный цикл
|
|
14
|
+
#
|
|
15
|
+
# 1. {#run} устанавливает {#install_signal_handlers}
|
|
16
|
+
# 2. {#start_loop_forever} запускает цикл обработки событий
|
|
17
|
+
# 3. События из Bus обрабатываются через case:
|
|
18
|
+
# - 'term' → {#term}
|
|
19
|
+
# - 'crash' → {#crash}
|
|
20
|
+
# - /sig:/ → {#signal}
|
|
21
|
+
# - /reap:/ → {#reap}
|
|
22
|
+
# - nil → reap_children (timeout)
|
|
23
|
+
# 4. {Dispatcher#reap} получает завершенные процессы
|
|
24
|
+
# 5. {Dispatcher#tick} проверяет необходимость принудительного завершения
|
|
25
|
+
#
|
|
26
|
+
# == Пример использования
|
|
27
|
+
#
|
|
28
|
+
# bus = MainLoop::Bus.new
|
|
29
|
+
# dispatcher = MainLoop::Dispatcher.new(bus, timeout: 10)
|
|
30
|
+
# loop = MainLoop::Loop.new(bus, dispatcher)
|
|
31
|
+
#
|
|
32
|
+
# loop.run(30) # запуск с таймаутом 30 секунд
|
|
33
|
+
#
|
|
34
|
+
# == См. также
|
|
35
|
+
# - {MainLoop::Bus} — канал событий
|
|
36
|
+
# - {MainLoop::Dispatcher} — координирует обработчики
|
|
37
|
+
# - {MainLoop::ProcessHandler} — обработчики процессов
|
|
38
|
+
# - {MainLoop::ThreadHandler} — обработчики потоков
|
|
9
39
|
|
|
40
|
+
class Loop
|
|
10
41
|
attr_reader :logger
|
|
11
42
|
|
|
43
|
+
# == Инициализация
|
|
44
|
+
#
|
|
45
|
+
# @param bus [Bus] канал событий
|
|
46
|
+
# @param dispatcher [Dispatcher] диспетчер обработчиков
|
|
47
|
+
# @param logger [Logger] логгер (по умолчанию Logger.new(nil))
|
|
12
48
|
def initialize(bus, dispatcher, logger: nil)
|
|
13
49
|
STDOUT.sync = true
|
|
14
50
|
STDERR.sync = true
|
|
@@ -17,6 +53,12 @@ module MainLoop
|
|
|
17
53
|
@logger = logger || Logger.new(nil)
|
|
18
54
|
end
|
|
19
55
|
|
|
56
|
+
# == Запуск цикла
|
|
57
|
+
#
|
|
58
|
+
# Устанавливает обработчики сигналов и запускает {#start_loop_forever}.
|
|
59
|
+
#
|
|
60
|
+
# @param timeout [Numeric] таймаут цикла в секундах (0 = бесконечный)
|
|
61
|
+
# @raise [StandardError] если произошла ошибка в цикле
|
|
20
62
|
def run(timeout = 0)
|
|
21
63
|
install_signal_handlers(@bus)
|
|
22
64
|
|
|
@@ -28,7 +70,18 @@ module MainLoop
|
|
|
28
70
|
# :nocov:
|
|
29
71
|
end
|
|
30
72
|
|
|
73
|
+
# == Главный цикл обработки событий
|
|
74
|
+
#
|
|
75
|
+
# Цикл с ограниченным временем работы (через Timeouter).
|
|
76
|
+
#
|
|
77
|
+
# Интервал ожидания событий:
|
|
78
|
+
# wait = [[(timeout / 2.5), 5].min, 5].max
|
|
79
|
+
# Минимум 5 секунд (даже при timeout = 0)
|
|
80
|
+
#
|
|
81
|
+
# @param timeout [Numeric] таймаут цикла в секундах (0 = бесконечный)
|
|
31
82
|
def start_loop_forever(timeout = 0)
|
|
83
|
+
# TODO поскольку wait всегда равен 5 секунд, то цикл работы 5 секунд, и потому
|
|
84
|
+
# timeout для Dispatcher нужно ставить больше 2 циклов, чтобы успели завершиться все потоки или процессы
|
|
32
85
|
wait = [[(timeout / 2.5), 5].min, 5].max
|
|
33
86
|
Timeouter.loop(timeout) do
|
|
34
87
|
event = @bus.gets(wait)
|
|
@@ -54,6 +107,11 @@ module MainLoop
|
|
|
54
107
|
end
|
|
55
108
|
end
|
|
56
109
|
|
|
110
|
+
# == Установка обработчиков сигналов
|
|
111
|
+
#
|
|
112
|
+
# Устанавливает trap для TERM, INT и CLD.
|
|
113
|
+
# Сигналы отправляются в Bus через отдельные потоки.
|
|
114
|
+
#
|
|
57
115
|
# :nocov:
|
|
58
116
|
def install_signal_handlers(bus)
|
|
59
117
|
TERM_SIGNALS.each do |sig|
|
|
@@ -68,6 +126,9 @@ module MainLoop
|
|
|
68
126
|
end
|
|
69
127
|
# :nocov:
|
|
70
128
|
|
|
129
|
+
# == Обработка сигнала
|
|
130
|
+
#
|
|
131
|
+
# @param command [String] команда вида "sig:NAME"
|
|
71
132
|
def signal(command)
|
|
72
133
|
_, sig = command.split(':')
|
|
73
134
|
logger.debug("signal:#{sig}")
|
|
@@ -81,25 +142,86 @@ module MainLoop
|
|
|
81
142
|
end
|
|
82
143
|
end
|
|
83
144
|
|
|
145
|
+
# == Инициировать терминацию
|
|
146
|
+
#
|
|
147
|
+
# Передает команду терминации диспетчеру (если не уже терминация).
|
|
148
|
+
#
|
|
149
|
+
# @param _command [String] команда (Unused)
|
|
84
150
|
def term(_command)
|
|
85
151
|
@dispatcher.term unless @dispatcher.terminating?
|
|
86
152
|
end
|
|
87
153
|
|
|
154
|
+
# == Отправить сигнал аварийного завершения
|
|
155
|
+
#
|
|
156
|
+
# Передает команду crash диспетчеру.
|
|
157
|
+
#
|
|
158
|
+
# @param _command [String] команда (Unused)
|
|
88
159
|
def crash(_command)
|
|
89
160
|
@dispatcher.crash
|
|
90
161
|
end
|
|
91
162
|
|
|
163
|
+
# == Обработка завершения процесса
|
|
164
|
+
#
|
|
165
|
+
# Парсит команду "reap:id:status" и отправляет в диспетчер.
|
|
166
|
+
#
|
|
167
|
+
# @param command [String] команда вида "reap:id:status"
|
|
92
168
|
def reap(command)
|
|
93
169
|
_, id, status = command.split(':')
|
|
94
170
|
@dispatcher.reap_by_id(id, status)
|
|
95
171
|
end
|
|
96
172
|
|
|
173
|
+
# == Сбор завершенных процессов
|
|
174
|
+
#
|
|
175
|
+
# Проходит по всем PID обработчиков и собирает их статусы через wait2.
|
|
176
|
+
# Дополнительно собирает все оставшиеся дочерние процессы (wait2(-1)).
|
|
177
|
+
#
|
|
178
|
+
# == Особенности обработки ECHILD
|
|
179
|
+
#
|
|
180
|
+
# Если процесс завершился и был "съеден" другой системой (например, родительский процесс
|
|
181
|
+
# вызвал Process.wait в on_term обработчике), то Process.wait2(pid) вызовет Errno::ECHILD.
|
|
182
|
+
# Это нормальное поведение в Unix/Linux когда PID больше не существует в таблице процессов.
|
|
183
|
+
#
|
|
184
|
+
# В этом случае:
|
|
185
|
+
# - Мы добавляем [pid, nil] в результат, чтобы отметить, что процесс не найден
|
|
186
|
+
# - Обработка продолжается для остальных процессов в списке
|
|
187
|
+
# - Это предотвращает "зависание" обработки всех остальных процессов
|
|
188
|
+
#
|
|
189
|
+
# Пример сценария (см. test_process.rb):
|
|
190
|
+
# 1. ProcessHandler запускает процесс с PID 123
|
|
191
|
+
# 2. При терминации вызывается on_term(pid) в обработчике
|
|
192
|
+
# 3. on_term вызывает Process.wait(pid) и "съедает" статус
|
|
193
|
+
# 4. Позже reap_children пытается wait2(123) и получает ECHILD
|
|
194
|
+
# 5. Обработка продолжается для других процессов, а 123 помечается как [123, nil]
|
|
195
|
+
#
|
|
196
|
+
# == Логика обработки
|
|
197
|
+
#
|
|
198
|
+
# Метод проходит по каждому PID из @dispatcher.pids:
|
|
199
|
+
# - wait2(pid) возвращает [pid, status] если процесс найден
|
|
200
|
+
# - wait2(pid) возвращает nil если процесс еще не завершился
|
|
201
|
+
# - wait2(pid) вызывает Errno::ECHILD если PID уже не существует
|
|
202
|
+
#
|
|
203
|
+
# Для каждого случая:
|
|
204
|
+
# - Нам возвращается [pid, status] -> добавляем в results
|
|
205
|
+
# - Возвращается nil -> ничего не добавляем (не завершился)
|
|
206
|
+
# - ECHILD -> добавляем [pid, nil] (PID не найден, съеден другой системой)
|
|
207
|
+
#
|
|
208
|
+
# После обработки всех известных PID, делается wait2(-1) для сбора
|
|
209
|
+
# любых оставшихся дочерних процессов (с таймаутом 2 секунды).
|
|
210
|
+
#
|
|
211
|
+
# @return [Array<Array>] массив пар (pid, status)
|
|
97
212
|
def reap_children
|
|
98
213
|
results = []
|
|
99
214
|
|
|
100
215
|
@dispatcher.pids.each do |pid|
|
|
101
|
-
|
|
102
|
-
|
|
216
|
+
begin
|
|
217
|
+
if (result = self.wait2(pid))
|
|
218
|
+
results << result
|
|
219
|
+
end
|
|
220
|
+
rescue Errno::ECHILD
|
|
221
|
+
# Процесс "съеден" другой системой (например, Process.wait вызван в on_term)
|
|
222
|
+
# или процесс уже завершился и pid больше не существует
|
|
223
|
+
# Добавляем [pid, nil] чтобы отметить его и продолжить обработку остальных
|
|
224
|
+
results << [pid, nil]
|
|
103
225
|
end
|
|
104
226
|
end
|
|
105
227
|
|
|
@@ -116,13 +238,16 @@ module MainLoop
|
|
|
116
238
|
results
|
|
117
239
|
end
|
|
118
240
|
|
|
241
|
+
# == Ожидание завершения процесса
|
|
242
|
+
#
|
|
243
|
+
# Обертка для Process.wait2 с флагом WNOHANG.
|
|
244
|
+
#
|
|
245
|
+
# @param pid [Integer] PID процесса для ожидания
|
|
246
|
+
# @return [Array<Integer, Process::Status>|nil] пара (pid, status) или nil если нет завершенных
|
|
119
247
|
# :nocov:
|
|
120
248
|
def wait2(pid)
|
|
121
249
|
Process.wait2(pid, ::Process::WNOHANG)
|
|
122
250
|
end
|
|
123
251
|
# :nocov:
|
|
124
|
-
|
|
125
252
|
end
|
|
126
|
-
|
|
127
253
|
end
|
|
128
|
-
|