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.
@@ -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.inspect}")
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.inspect}. Skipped")
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
-
@@ -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
- # :nocov:
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(&@block)
181
+ self.run
85
182
  elsif @retry_count && (@retry_count -= 1) >= 0
86
183
  logger.info "#{@handler_type}[#{name}] retry...."
87
- self.run(&@block)
184
+ self.run
88
185
  else
89
186
  publish(:term)
90
187
  end
91
188
  end
92
-
93
189
  end
94
190
  end
95
-
@@ -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
- class Loop
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
- if (result = self.wait2(pid))
102
- results << result
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
-