main_loop 0.1.4.364822 → 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 +415 -16
- data/lib/main_loop/bus.rb +81 -5
- data/lib/main_loop/dispatcher.rb +100 -3
- data/lib/main_loop/handler.rb +100 -5
- data/lib/main_loop/loop.rb +126 -7
- data/lib/main_loop/process_handler.rb +61 -5
- data/lib/main_loop/thread_handler.rb +59 -6
- data/lib/main_loop.rb +41 -2
- metadata +2 -2
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 850bb2f02f1bff7caf3ebca532839b4143a24810948b2ac29446d0321542911b
|
|
4
|
+
data.tar.gz: 88f4324fb104706a0705260d6c82ca7f2f04e66af1a9ca2c88d63e3142bcbe62
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 2d2675f76bb8127405617c12f4fa39a5e6ae2f3507de8131b4f878a22dc9884ba46e2d50125729e2471bbaee9c2be2fbd164b047914738c5f2b7f13642b71197
|
|
7
|
+
data.tar.gz: eaa4672e9905b147a25b8a376c871debc1841879e8f486d58f8928c14c7289ce19b3813b6ad7a8042f49a0eca88f5d190270e334b9799371b252468575a2a803
|
data/README.md
CHANGED
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
# MainLoop
|
|
2
2
|
|
|
3
|
+
<div align="center">
|
|
4
|
+
|
|
3
5
|
[](https://rubygems.org/gems/main_loop)
|
|
4
6
|
[](https://rubygems.org/gems/main_loop/versions)
|
|
5
7
|
[](http://www.rubydoc.info/gems/main_loop)
|
|
@@ -9,34 +11,227 @@
|
|
|
9
11
|
[](https://lysander.rnds.pro/api/v1/badges/main_loop_outdated.html)
|
|
10
12
|
[](https://lysander.rnds.pro/api/v1/badges/main_loop_vulnerable.html)
|
|
11
13
|
|
|
12
|
-
|
|
14
|
+
</div>
|
|
15
|
+
|
|
16
|
+
**MainLoop** — Ruby-библиотека для управления субпроцессами и потоками с функциями:
|
|
17
|
+
- автоматический сбор дочерних процессов (reaping)
|
|
18
|
+
- корректное завершение (SIGTERM/SIGINT) процессов и потоков
|
|
19
|
+
- автоматический перезапуск по количеству повторов
|
|
20
|
+
- принудительное завершение по таймауту
|
|
21
|
+
- обработка завершения процессов и потоков
|
|
22
|
+
|
|
23
|
+
---
|
|
24
|
+
|
|
25
|
+
**MainLoop** is a Ruby library for managing subprocesses and threads with features:
|
|
26
|
+
- automatic child process reaping
|
|
27
|
+
- graceful shutdown (SIGTERM/SIGINT) for processes and threads
|
|
28
|
+
- automatic restart by retry count
|
|
29
|
+
- timeout-based force termination
|
|
30
|
+
- process/thread completion handling
|
|
31
|
+
|
|
32
|
+
<div align="left">
|
|
33
|
+
<a href="https://rnds.pro/" >
|
|
34
|
+
<img src="https://library.rnds.pro/repository/public-blob/logo/RNDS.svg" alt="Supported by RNDSOFT" height="60">
|
|
35
|
+
</a>
|
|
36
|
+
</div>
|
|
37
|
+
|
|
38
|
+
## Возможности / Features
|
|
39
|
+
|
|
40
|
+
- Потоко-безопасный канал обмена событиями (IO.pipe) / Thread-safe event bus (IO.pipe)
|
|
41
|
+
- Управление процессами через Kernel.fork / Process management via Kernel.fork
|
|
42
|
+
- Управление потоками через Thread.new / Thread management via Thread.new
|
|
43
|
+
- Обработка сигналов TERM/INT/CLD / SIGTERM/INT/CLD signal handling
|
|
44
|
+
- Автоматический сбор завершенных процессов / Automatic child process reaping
|
|
45
|
+
- Retry-логика для перезапуска / Retry logic for restarts
|
|
46
|
+
- Принудительное завершение по таймауту / Timeout-based force termination
|
|
47
|
+
|
|
48
|
+
## Начало работы / Getting started
|
|
49
|
+
|
|
50
|
+
> gem install main_loop
|
|
51
|
+
|
|
52
|
+
При установке `MainLoop` через bundler добавьте следующую строку в `Gemfile`:
|
|
53
|
+
|
|
54
|
+
---
|
|
55
|
+
|
|
56
|
+
If you'd rather install `MainLoop` using bundler, add a line for it in your `Gemfile`:
|
|
57
|
+
|
|
58
|
+
> gem 'main_loop'
|
|
59
|
+
|
|
60
|
+
Затем выполните / Then run:
|
|
61
|
+
|
|
62
|
+
> bundle install # для установки гема / gem installation
|
|
63
|
+
|
|
64
|
+
## Корневой модуль / Root module
|
|
65
|
+
|
|
66
|
+
`MainLoop` - это корневой модуль, который подключает все компоненты:
|
|
67
|
+
|
|
68
|
+
---
|
|
69
|
+
|
|
70
|
+
`MainLoop` is the root module that requires all components:
|
|
71
|
+
|
|
72
|
+
```ruby
|
|
73
|
+
require 'main_loop'
|
|
74
|
+
```
|
|
75
|
+
|
|
76
|
+
Субмодули / Submodules:
|
|
77
|
+
|
|
78
|
+
- `MainLoop::Bus` — канал обмена событиями (IO.pipe)
|
|
79
|
+
- `MainLoop::Dispatcher` — координация обработчиков и управление жизненным циклом
|
|
80
|
+
- `MainLoop::Loop` — главный цикл обработки событий и сигналов
|
|
81
|
+
- `MainLoop::Handler` — абстрактный базовый класс
|
|
82
|
+
- `MainLoop::ProcessHandler` — управление субпроцессами
|
|
83
|
+
- `MainLoop::ThreadHandler` — управление потоками
|
|
84
|
+
|
|
85
|
+
## Архитектура / Architecture
|
|
86
|
+
|
|
87
|
+
```mermaid
|
|
88
|
+
graph TB
|
|
89
|
+
subgraph "MainLoop"
|
|
90
|
+
Loop["Loop<br><i>Главный цикл</i>"]
|
|
91
|
+
Dispatcher["Dispatcher<br><i>Координирует обработчиков</i>"]
|
|
92
|
+
Bus["Bus<br><i>Канал обмена событиями (IO.pipe)</i>"]
|
|
93
|
+
Handler["Handler<br><i>Базовый класс</i>"]
|
|
94
|
+
ProcessHandler["ProcessHandler<br><i>Управляет процессами</i>"]
|
|
95
|
+
ThreadHandler["ThreadHandler<br><i>Управляет потоками</i>"]
|
|
96
|
+
|
|
97
|
+
Loop -->|координирует| Dispatcher
|
|
98
|
+
Dispatcher -->|использует| Bus
|
|
99
|
+
Loop -->|получает события из| Bus
|
|
100
|
+
ProcessHandler -->|наследует| Handler
|
|
101
|
+
ThreadHandler -->|наследует| Handler
|
|
102
|
+
ProcessHandler -->|регистрируется в| Dispatcher
|
|
103
|
+
ThreadHandler -->|регистрируется в| Dispatcher
|
|
104
|
+
end
|
|
105
|
+
```
|
|
106
|
+
|
|
107
|
+
## Жизненный цикл обработчика / Handler lifecycle
|
|
108
|
+
|
|
109
|
+
```mermaid
|
|
110
|
+
sequenceDiagram
|
|
111
|
+
participant Cycle as Loop
|
|
112
|
+
participant Bus
|
|
113
|
+
participant Dispatcher
|
|
114
|
+
participant Handler
|
|
115
|
+
|
|
116
|
+
note over Cycle,Handler: Инициализация / Initialization
|
|
117
|
+
Cycle->>Bus: install_signal_handlers
|
|
118
|
+
Handler->>Dispatcher: add_handler
|
|
119
|
+
|
|
120
|
+
Cycle->>Cycle: start_loop_forever
|
|
121
|
+
loop Forever
|
|
122
|
+
Cycle->>Bus: gets(wait)
|
|
123
|
+
alt sig:TERM/INT
|
|
124
|
+
Bus-->>Cycle: "sig:TERM"
|
|
125
|
+
Cycle->>Dispatcher: term
|
|
126
|
+
Dispatcher->>Dispatcher: @terminating_at = Time.now
|
|
127
|
+
note right of Dispatcher: Graceful termination
|
|
128
|
+
Dispatcher->>Handler: term
|
|
129
|
+
opt @on_term || @runnable.on_term
|
|
130
|
+
Handler->>Handler: @on_term.call(pid/thread)
|
|
131
|
+
end
|
|
132
|
+
else sig:CLD
|
|
133
|
+
Cycle->>Cycle: wait for reap_children
|
|
134
|
+
else reap:pid:status
|
|
135
|
+
Cycle->>Dispatcher: reap_by_id
|
|
136
|
+
Dispatcher->>Handler: reap(status)
|
|
137
|
+
alt retry_count > 0
|
|
138
|
+
Handler->>Handler: handle_retry -> run
|
|
139
|
+
else
|
|
140
|
+
Handler->>Bus: publish(:term)
|
|
141
|
+
end
|
|
142
|
+
end
|
|
143
|
+
Cycle->>Dispatcher: tick
|
|
144
|
+
alt need_force_kill?
|
|
145
|
+
note right of Dispatcher: Превышен timeout / Timeout exceeded
|
|
146
|
+
Dispatcher->>Dispatcher: @killed = true
|
|
147
|
+
Dispatcher->>Handler: kill
|
|
148
|
+
end
|
|
149
|
+
end
|
|
150
|
+
```
|
|
151
|
+
|
|
152
|
+
## Способы определения логики
|
|
13
153
|
|
|
14
|
-
|
|
15
|
-
- reaping children
|
|
16
|
-
- handling SIGTERM SIGINT to shutdown children(and threads) gracefully
|
|
17
|
-
- restarting children
|
|
18
|
-
- termination the children
|
|
154
|
+
Библиотека поддерживает два основных подхода для описания работы процессов и потоков:
|
|
19
155
|
|
|
20
|
-
|
|
156
|
+
### 1. **Блок (inline block)**
|
|
21
157
|
|
|
22
|
-
|
|
158
|
+
Передайте блок кода непосредственно в конструктор обработчика (`ProcessHandler` или `ThreadHandler`). В этом блоке размещается основная логика. При завершении (по сигналу, ошибке или таймауту) блок прерывается; вы можете определить дополнительные действия, используя переданный объект обработчика.
|
|
159
|
+
|
|
160
|
+
```ruby
|
|
161
|
+
MainLoop::ProcessHandler.new(dispatcher, 'my_process', retry_count: 3) do
|
|
162
|
+
# основная логика
|
|
163
|
+
loop { sleep 1 }
|
|
164
|
+
end
|
|
165
|
+
|
|
166
|
+
MainLoop::ThreadHandler.new(dispatcher, 'my_thread', retry_count: 0) do |handler|
|
|
167
|
+
handler.on_term do
|
|
168
|
+
# действия при завершении (очистка, закрытие ресурсов)
|
|
169
|
+
@stop = true
|
|
170
|
+
end
|
|
171
|
+
# основная логика с возможностью проверки флага
|
|
172
|
+
@stop = false
|
|
173
|
+
loop { sleep 1; break if @stop }
|
|
174
|
+
end
|
|
175
|
+
```
|
|
176
|
+
|
|
177
|
+
### 2. **Объект с интерфейсом run / on_term**
|
|
178
|
+
|
|
179
|
+
Передайте экземпляр класса, который реализует два обязательных метода:
|
|
180
|
+
- `run` — содержит основную логику; для `ProcessHandler` метод не принимает аргументов, для `ThreadHandler` получает объект потока.
|
|
181
|
+
- `on_term` — вызывается при необходимости завершить процесс/поток; для `ProcessHandler` принимает PID, для `ThreadHandler` — объект потока. В этом методе следует инициировать корректное завершение (например, послать сигнал процессу или установить флаг остановки для потока).
|
|
182
|
+
|
|
183
|
+
```ruby
|
|
184
|
+
class Worker
|
|
185
|
+
def run
|
|
186
|
+
trap('USR1') { @stop = true; raise Interrupt }
|
|
187
|
+
@stop = false
|
|
188
|
+
loop { sleep 1; break if @stop }
|
|
189
|
+
rescue Interrupt
|
|
190
|
+
# завершаемся
|
|
191
|
+
exit 0
|
|
192
|
+
end
|
|
193
|
+
|
|
194
|
+
def on_term(pid)
|
|
195
|
+
Process.kill('USR1', pid)
|
|
196
|
+
end
|
|
197
|
+
end
|
|
198
|
+
|
|
199
|
+
MainLoop::ProcessHandler.new(dispatcher, 'worker', runnable: Worker.new)
|
|
200
|
+
```
|
|
201
|
+
|
|
202
|
+
Оба подхода могут комбинироваться с параметрами (`retry_count`, `logger` и т.д.) и одинаково хорошо интегрируются с циклом `MainLoop::Loop`.
|
|
203
|
+
|
|
204
|
+
Выбор зависит от удобства: для простых сценариев подойдёт блок, для сложной логики управления завершением — объект с явными методами.
|
|
205
|
+
|
|
206
|
+
## Использование / Usage
|
|
207
|
+
|
|
208
|
+
### Базовая настройка / Basic setup
|
|
23
209
|
|
|
24
210
|
```ruby
|
|
25
211
|
require 'main_loop'
|
|
212
|
+
require 'logger'
|
|
26
213
|
|
|
27
214
|
logger = Logger.new(STDOUT)
|
|
28
215
|
logger.level = Logger::DEBUG
|
|
29
216
|
|
|
217
|
+
# Шина и диспетчер. Параметр timeout (в секундах) опционален.
|
|
30
218
|
bus = MainLoop::Bus.new
|
|
31
|
-
|
|
32
|
-
dispatcher = MainLoop::Dispatcher.new(bus, logger: logger)
|
|
219
|
+
dispatcher = MainLoop::Dispatcher.new(bus, timeout: 10, logger: logger)
|
|
33
220
|
mainloop = MainLoop::Loop.new(bus, dispatcher, logger: logger)
|
|
221
|
+
```
|
|
222
|
+
|
|
223
|
+
### Обработка процессов / Process handling
|
|
34
224
|
|
|
225
|
+
#### Простейший пример / Simplest example
|
|
226
|
+
|
|
227
|
+
```ruby
|
|
228
|
+
# Процесс test1: будет перезапущен 3 раза, после завершения выходит с кодом 0
|
|
35
229
|
MainLoop::ProcessHandler.new dispatcher, 'test1', retry_count: 3, logger: logger do
|
|
36
230
|
sleep 2
|
|
37
231
|
exit! 0
|
|
38
232
|
end
|
|
39
233
|
|
|
234
|
+
# Процесс test2: обрабатывает SIGTERM и выходит с кодом 1 после 2 перезапусков
|
|
40
235
|
MainLoop::ProcessHandler.new dispatcher, 'test2', retry_count: 2, logger: logger do
|
|
41
236
|
trap 'TERM' do
|
|
42
237
|
exit(0)
|
|
@@ -44,24 +239,228 @@ MainLoop::ProcessHandler.new dispatcher, 'test2', retry_count: 2, logger: logger
|
|
|
44
239
|
sleep 2
|
|
45
240
|
exit! 1
|
|
46
241
|
end
|
|
242
|
+
```
|
|
243
|
+
|
|
244
|
+
#### С блоком кода / With code block
|
|
245
|
+
|
|
246
|
+
```ruby
|
|
247
|
+
MainLoop::ProcessHandler.new dispatcher, 'worker', retry_count: 3, logger: logger do
|
|
248
|
+
loop do
|
|
249
|
+
# основная логика / main logic
|
|
250
|
+
sleep 1
|
|
251
|
+
end
|
|
252
|
+
exit 0
|
|
253
|
+
end
|
|
254
|
+
```
|
|
255
|
+
|
|
256
|
+
#### С объектом runnable / With runnable object
|
|
257
|
+
|
|
258
|
+
```ruby
|
|
259
|
+
class Worker
|
|
260
|
+
def run
|
|
261
|
+
# Используем пользовательский сигнал, чтобы прервать системные вызовы (например, sleep)
|
|
262
|
+
trap('USR1') do
|
|
263
|
+
puts "Получен USR1. pid = #{Process.pid}"
|
|
264
|
+
@stop = true
|
|
265
|
+
raise Interrupt # прерывает текущий блок (sleep и т.д.)
|
|
266
|
+
end
|
|
267
|
+
|
|
268
|
+
@stop = false
|
|
269
|
+
loop do
|
|
270
|
+
puts "работаю..."
|
|
271
|
+
sleep 100
|
|
272
|
+
break if @stop
|
|
273
|
+
rescue Interrupt
|
|
274
|
+
puts "прерывание, выходим"
|
|
275
|
+
break
|
|
276
|
+
end
|
|
277
|
+
exit 0
|
|
278
|
+
end
|
|
279
|
+
|
|
280
|
+
def on_term(pid)
|
|
281
|
+
puts "Завершаю работу. pid = #{pid}"
|
|
282
|
+
Process.kill('USR1', pid) # посылаем пользовательский сигнал
|
|
283
|
+
|
|
284
|
+
begin
|
|
285
|
+
Timeout.timeout(5) { Process.wait(pid) }
|
|
286
|
+
rescue Timeout::Error
|
|
287
|
+
puts "не завершился за 5 секунд"
|
|
288
|
+
end
|
|
289
|
+
|
|
290
|
+
puts "Завершил работу"
|
|
291
|
+
end
|
|
292
|
+
end
|
|
47
293
|
|
|
294
|
+
worker = Worker.new
|
|
295
|
+
MainLoop::ProcessHandler.new(dispatcher, 'worker', runnable: worker, retry_count: :unlimited, logger: logger)
|
|
296
|
+
```
|
|
297
|
+
|
|
298
|
+
### Обработка потоков / Thread handling
|
|
299
|
+
|
|
300
|
+
#### Поток с блоком / Thread with block
|
|
301
|
+
|
|
302
|
+
```ruby
|
|
303
|
+
# Поток thread2: не перезапускается (retry_count: 0), выполняет внешнюю команду
|
|
48
304
|
MainLoop::ThreadHandler.new dispatcher, 'thread2', retry_count: 0, logger: logger do
|
|
49
305
|
system('sleep 15;echo ok')
|
|
50
306
|
end
|
|
307
|
+
```
|
|
308
|
+
|
|
309
|
+
#### Поток с блоком и колбэком завершения / Thread with block and termination callback
|
|
310
|
+
|
|
311
|
+
```ruby
|
|
312
|
+
MainLoop::ThreadHandler.new dispatcher, 'worker', retry_count: 0, logger: logger do |handler|
|
|
313
|
+
# Устанавливаем обработчик завершения потока
|
|
314
|
+
handler.on_term do
|
|
315
|
+
puts "Завершаем поток, выполняем cleanup..."
|
|
316
|
+
@stop = true
|
|
317
|
+
end
|
|
318
|
+
|
|
319
|
+
@stop = false
|
|
320
|
+
loop do
|
|
321
|
+
puts "Работаю..."
|
|
322
|
+
sleep 1
|
|
323
|
+
break if @stop
|
|
324
|
+
end
|
|
325
|
+
end
|
|
326
|
+
```
|
|
327
|
+
|
|
328
|
+
#### Поток с объектом runnable / Thread with runnable object
|
|
51
329
|
|
|
330
|
+
```ruby
|
|
331
|
+
class Worker
|
|
332
|
+
def run(thread)
|
|
333
|
+
@stop = false
|
|
334
|
+
loop do
|
|
335
|
+
sleep 60
|
|
336
|
+
break if @stop
|
|
337
|
+
end
|
|
338
|
+
end
|
|
339
|
+
|
|
340
|
+
def on_term(thread)
|
|
341
|
+
@stop = true
|
|
342
|
+
thread&.wakeup
|
|
343
|
+
end
|
|
344
|
+
end
|
|
345
|
+
|
|
346
|
+
worker = Worker.new
|
|
347
|
+
MainLoop::ThreadHandler.new dispatcher, 'worker', runnable: worker, logger: logger
|
|
348
|
+
```
|
|
349
|
+
|
|
350
|
+
### Запуск цикла / Start loop
|
|
351
|
+
|
|
352
|
+
```ruby
|
|
353
|
+
# Бесконечный цикл / Infinite loop
|
|
52
354
|
mainloop.run
|
|
355
|
+
|
|
356
|
+
# С таймаутом (30 секунд) / With timeout (30 seconds)
|
|
357
|
+
mainloop.run(30)
|
|
53
358
|
```
|
|
54
359
|
|
|
360
|
+
## Обработка завершения / Termination handling
|
|
55
361
|
|
|
56
|
-
|
|
362
|
+
Когда отправляется сигнал `TERM` или `INT`:
|
|
57
363
|
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
364
|
+
1. `trap` перехватывает сигнал и отправляет `bus.puts("sig:TERM")`
|
|
365
|
+
2. `Loop` получает событие из `Bus` и вызывает `Dispatcher#term`
|
|
366
|
+
3. `Dispatcher` устанавливает `@terminating_at = Time.now`
|
|
367
|
+
4. Все обработчики получают `term`:
|
|
368
|
+
- `ProcessHandler` посылает `Process.kill('TERM', pid)`
|
|
369
|
+
- `ThreadHandler` вызывает `@on_term` блок
|
|
370
|
+
5. Если через `timeout` (по умолчанию 5 сек) процессы не завершились:
|
|
371
|
+
- `Dispatcher#tick` проверяет `need_force_kill?`
|
|
372
|
+
- Если `true` — посылает `kill` всем обработчикам
|
|
373
|
+
6. Когда все обработчики завершаются (`finished?`):
|
|
374
|
+
- `Dispatcher#try_exit!` вызывает `exit(@exit_code)`
|
|
375
|
+
|
|
376
|
+
---
|
|
377
|
+
|
|
378
|
+
When sending `TERM` or `INT` signal:
|
|
379
|
+
|
|
380
|
+
1. `trap` catches the signal and sends `bus.puts("sig:TERM")`
|
|
381
|
+
2. `Loop` gets the event from `Bus` and calls `Dispatcher#term`
|
|
382
|
+
3. `Dispatcher` sets `@terminating_at = Time.now`
|
|
383
|
+
4. All handlers receive `term`:
|
|
384
|
+
- `ProcessHandler` sends `Process.kill('TERM', pid)`
|
|
385
|
+
- `ThreadHandler` calls `@on_term` block
|
|
386
|
+
5. If processes don't terminate within `timeout` (default 5 seconds):
|
|
387
|
+
- `Dispatcher#tick` checks `need_force_kill?`
|
|
388
|
+
- If `true` — sends `kill` to all handlers
|
|
389
|
+
6. When all handlers finish (`finished?`):
|
|
390
|
+
- `Dispatcher#try_exit!` calls `exit(@exit_code)`
|
|
391
|
+
|
|
392
|
+
## Повторы (retry) / Retry
|
|
393
|
+
|
|
394
|
+
```ruby
|
|
395
|
+
retry_count: 3 # повторить 3 раза / retry 3 times
|
|
396
|
+
retry_count: 0 # не повторять / don't retry
|
|
397
|
+
retry_count: :unlimited # бесконечные повторы / infinite retries
|
|
61
398
|
```
|
|
62
|
-
|
|
399
|
+
|
|
400
|
+
## Публикация событий / Publishing events
|
|
401
|
+
|
|
402
|
+
Обработчики могут отправлять события в шину:
|
|
403
|
+
|
|
404
|
+
---
|
|
405
|
+
|
|
406
|
+
Handlers can publish events to the bus:
|
|
407
|
+
|
|
63
408
|
```ruby
|
|
64
|
-
|
|
409
|
+
# Из любого места обработчика / From any handler place:
|
|
410
|
+
publish("reap:#{id}:exited")
|
|
411
|
+
publish(:term)
|
|
412
|
+
```
|
|
413
|
+
|
|
414
|
+
## Особенности / Features
|
|
415
|
+
|
|
416
|
+
- **Потоко-безопасность**: `Bus` и `Dispatcher` используют `MonitorMixin`
|
|
417
|
+
- **Таймауты**: `Timeouter` используется для timeout в `Bus#gets` и `Loop#start_loop_forever`
|
|
418
|
+
- **Логирование**: все классы принимают параметр `logger:`, по умолчанию `Logger.new(nil)`
|
|
419
|
+
- **Ошибки**: используйте `rescue StandardError` (не пустой `rescue`)
|
|
420
|
+
- **Коды выхода**: `exit!(code)` в процессах, `exit(code)` в основном потоке
|
|
421
|
+
|
|
422
|
+
---
|
|
423
|
+
|
|
424
|
+
- **Thread safety**: `Bus` and `Dispatcher` use `MonitorMixin`
|
|
425
|
+
- **Timeouts**: `Timeouter` is used for timeout in `Bus#gets` и `Loop#start_loop_forever`
|
|
426
|
+
- **Logging**: all classes accept `logger:` parameter, default is `Logger.new(nil)`
|
|
427
|
+
- **Error handling**: use `rescue StandardError` (not bare `rescue`)
|
|
428
|
+
- **Exit codes**: `exit!(code)` in processes, `exit(code)` in main thread
|
|
429
|
+
|
|
430
|
+
## Тестирование / Testing
|
|
431
|
+
|
|
432
|
+
```bash
|
|
433
|
+
# Запуск всех тестов / Run all tests
|
|
434
|
+
bundle exec rspec
|
|
435
|
+
|
|
436
|
+
# Запуск одного файла / Run single file
|
|
437
|
+
bundle exec rspec spec/bus_spec.rb
|
|
438
|
+
|
|
439
|
+
# Запуск одного теста / Run single test
|
|
440
|
+
bundle exec rspec spec/bus_spec.rb:12
|
|
441
|
+
|
|
442
|
+
# Покрытие кода / Code coverage (96%+)
|
|
443
|
+
bundle exec rspec --format progress
|
|
65
444
|
```
|
|
66
445
|
|
|
446
|
+
Для запуска примеров из локальной копии репозитория (без установки гема) используйте опцию `-I` интерпретатора Ruby, указав путь к директории `lib` относительно текущей папки:
|
|
447
|
+
|
|
448
|
+
```bash
|
|
449
|
+
ruby -I ./lib examples/имя_файла.rb
|
|
450
|
+
```
|
|
451
|
+
|
|
452
|
+
## Версия / Version
|
|
453
|
+
|
|
454
|
+
Текущая версия / Current version: `0.1.4`
|
|
455
|
+
|
|
456
|
+
## Автор / Author
|
|
457
|
+
|
|
458
|
+
Юрий Самойленко / Yuri Samoylenko <kinnalru@gmail.com>
|
|
459
|
+
|
|
460
|
+
## Лицензия / License
|
|
461
|
+
|
|
462
|
+
Библиотека доступна с открытым исходным кодом в соответствии с условиями [лицензии MIT](LICENSE).
|
|
463
|
+
|
|
464
|
+
---
|
|
67
465
|
|
|
466
|
+
The gem is available as open source under the terms of the [MIT License](LICENSE).
|
data/lib/main_loop/bus.rb
CHANGED
|
@@ -1,15 +1,49 @@
|
|
|
1
1
|
require 'monitor'
|
|
2
2
|
require 'timeouter'
|
|
3
3
|
|
|
4
|
+
# = MainLoop::Bus
|
|
5
|
+
#
|
|
6
|
+
# Потоко-безопасный канал обмена событиями между компонентами на основе IO.pipe.
|
|
7
|
+
#
|
|
8
|
+
# Использует {MonitorMixin} для синхронизации доступа и {Timeouter} для таймаутов.
|
|
9
|
+
# Буферизация строк для корректного парсинга построчно.
|
|
10
|
+
#
|
|
11
|
+
# == События
|
|
12
|
+
#
|
|
13
|
+
# События — это строки, отправляемые через {puts}. События читаются методами {gets}
|
|
14
|
+
# и {gets_nonblock}. Разделитель строк — newline ("\n").
|
|
15
|
+
#
|
|
16
|
+
# == Пример использования
|
|
17
|
+
#
|
|
18
|
+
# bus = MainLoop::Bus.new
|
|
19
|
+
# bus.puts("term") # Отправить событие
|
|
20
|
+
# event = bus.gets(5) # Получить событие с таймаутом 5 секунд
|
|
21
|
+
#
|
|
22
|
+
# == См. также
|
|
23
|
+
# - {MainLoop::Loop} — потребитель событий
|
|
24
|
+
# - {MainLoop::Dispatcher} — потребитель событий
|
|
25
|
+
# - {MonitorMixin} — реализация потоко-безопасности
|
|
26
|
+
|
|
4
27
|
module MainLoop
|
|
5
28
|
class Bus
|
|
6
|
-
|
|
7
29
|
include MonitorMixin
|
|
30
|
+
# Константа конца строки (end of line)
|
|
31
|
+
# @return [String]
|
|
32
|
+
EOL = "\n".freeze
|
|
8
33
|
|
|
9
34
|
attr_reader :read, :write
|
|
10
35
|
|
|
11
|
-
|
|
12
|
-
|
|
36
|
+
# == Инициализация
|
|
37
|
+
#
|
|
38
|
+
# Создает IO.pipe, настраивает синхронный режим, инициализирует буфер.
|
|
39
|
+
#
|
|
40
|
+
# @example
|
|
41
|
+
# bus = MainLoop::Bus.new
|
|
42
|
+
#
|
|
43
|
+
# @!attribute [r] read
|
|
44
|
+
# @return [IO] конец канала для чтения
|
|
45
|
+
# @!attribute [r] write
|
|
46
|
+
# @return [IO] конец канала для записи
|
|
13
47
|
def initialize
|
|
14
48
|
super()
|
|
15
49
|
@read, @write = IO.pipe
|
|
@@ -18,29 +52,67 @@ module MainLoop
|
|
|
18
52
|
@buffer = ''
|
|
19
53
|
end
|
|
20
54
|
|
|
55
|
+
# == Проверка наличия событий
|
|
56
|
+
#
|
|
57
|
+
# Проверяет, есть ли события в канале.
|
|
58
|
+
#
|
|
59
|
+
# @param timeout [Numeric] таймаут ожидания в секундах
|
|
60
|
+
# @return [Boolean] true если событий нет, false если событие доступно
|
|
21
61
|
def empty?(timeout = 0)
|
|
22
62
|
!wait_for_event(timeout)
|
|
23
63
|
end
|
|
24
64
|
|
|
65
|
+
# == Закрытие канала
|
|
66
|
+
#
|
|
67
|
+
# Закрывает оба конца канала (read и write).
|
|
68
|
+
# Ошибки закрытия игнорируются (rescue nil).
|
|
69
|
+
#
|
|
70
|
+
# @example
|
|
71
|
+
# bus.close
|
|
25
72
|
def close
|
|
26
73
|
@write.close rescue nil
|
|
27
74
|
@read.close rescue nil
|
|
28
75
|
end
|
|
29
76
|
|
|
77
|
+
# == Проверка закрытости канала
|
|
78
|
+
#
|
|
79
|
+
# @return [Boolean] true если любой конец канала закрыт
|
|
30
80
|
def closed?
|
|
31
81
|
@write.closed? || @read.closed?
|
|
32
82
|
end
|
|
33
83
|
|
|
84
|
+
# == Отправка события
|
|
85
|
+
#
|
|
86
|
+
# Отправляет строку в канал с потоко-безопасной синхронизацией.
|
|
87
|
+
# Автоматически добавляет конец строки.
|
|
88
|
+
#
|
|
89
|
+
# @param str [String] событие для отправки
|
|
90
|
+
# @example
|
|
91
|
+
# bus.puts("term")
|
|
92
|
+
# bus.puts("reap:123:0")
|
|
34
93
|
def puts(str)
|
|
35
94
|
synchronize do
|
|
36
95
|
@write.puts str.to_s
|
|
37
96
|
end
|
|
38
97
|
end
|
|
39
98
|
|
|
99
|
+
# == Ожидание события
|
|
100
|
+
#
|
|
101
|
+
# Использует IO.select для ожидания события в канале.
|
|
102
|
+
#
|
|
103
|
+
# @param timeout [Numeric] таймаут ожидания в секундах
|
|
104
|
+
# @return [Array<IO>|nil] результат IO.select или nil при таймауте
|
|
40
105
|
def wait_for_event(timeout)
|
|
41
106
|
IO.select([@read], [], [], timeout)
|
|
42
107
|
end
|
|
43
108
|
|
|
109
|
+
# == Блокирующее чтение события
|
|
110
|
+
#
|
|
111
|
+
# Блокирует до получения строки или таймаута.
|
|
112
|
+
# Использует {Timeouter.loop} для ограничения времени ожидания.
|
|
113
|
+
#
|
|
114
|
+
# @param timeout [Numeric] таймаут ожидания в секундах
|
|
115
|
+
# @return [String|nil] прочитанная строка (без символа новой строки) или nil при таймауте
|
|
44
116
|
def gets(timeout)
|
|
45
117
|
Timeouter.loop(timeout) do |t|
|
|
46
118
|
line = gets_nonblock if wait_for_event(t.left)
|
|
@@ -48,6 +120,12 @@ module MainLoop
|
|
|
48
120
|
end
|
|
49
121
|
end
|
|
50
122
|
|
|
123
|
+
# == Неблокирующее чтение символа
|
|
124
|
+
#
|
|
125
|
+
# Читает посимвольно до конца строки (EOL).
|
|
126
|
+
# Буферизует символы и возвращает полную строку.
|
|
127
|
+
#
|
|
128
|
+
# @return [String|nil] строка (без EOL и пробелов по краям) или nil если нет данных
|
|
51
129
|
def gets_nonblock
|
|
52
130
|
while (ch = @read.read_nonblock(1))
|
|
53
131
|
@buffer << ch
|
|
@@ -61,7 +139,5 @@ module MainLoop
|
|
|
61
139
|
rescue IO::WaitReadable
|
|
62
140
|
nil
|
|
63
141
|
end
|
|
64
|
-
|
|
65
142
|
end
|
|
66
143
|
end
|
|
67
|
-
|