Супервизия процессов
Супервизия процессов
Заголовок раздела «Супервизия процессов»Мониторинг и связывание процессов для построения отказоустойчивых систем.
Мониторинг и связывание
Заголовок раздела «Мониторинг и связывание»Мониторинг обеспечивает одностороннее наблюдение:
- Родитель мониторит потомка
- Потомок завершается, родитель получает событие EXIT
- Родитель продолжает работу
Связывание создаёт двунаправленное разделение судьбы:
- Родитель и потомок связаны
- Один из процессов завершается с ошибкой — оба завершаются
- Если не установлен
trap_links=true
flowchart TB subgraph Monitoring["MONITORING (one-way)"] direction TB P1[Parent monitors] -->|EXIT event<br/>parent continues| C1[Child exits] end
subgraph Linking["LINKING (bidirectional)"] direction TB P2[Parent linked] <-->|LINK_DOWN<br/>both die| C2[Child exits] endМониторинг процессов
Заголовок раздела «Мониторинг процессов»Создание с мониторингом
Заголовок раздела «Создание с мониторингом»Используйте process.spawn_monitored() для создания и мониторинга одним вызовом:
local function main() local events_ch = process.events()
-- Spawn worker and start monitoring local worker_pid, err = process.spawn_monitored( "app.workers:task_worker", "app:processes" ) if err then return nil, "spawn failed: " .. tostring(err) end
-- Wait for worker to complete local event = events_ch:receive()
if event.kind == process.event.EXIT then print("Worker exited:", event.from) if event.result then print("Result:", event.result.value) end if event.result and event.result.error then print("Error:", event.result.error) end endendМониторинг существующего процесса
Заголовок раздела «Мониторинг существующего процесса»Вызовите process.monitor() для начала мониторинга уже запущенного процесса:
local function main() local time = require("time") local events_ch = process.events()
-- Spawn without monitoring local worker_pid, err = process.spawn( "app.workers:long_worker", "app:processes" ) if err then return nil, "spawn failed: " .. tostring(err) end
-- Start monitoring later local ok, monitor_err = process.monitor(worker_pid) if monitor_err then return nil, "monitor failed: " .. tostring(monitor_err) end
-- Cancel the worker time.sleep("5ms") process.cancel(worker_pid)
-- Receive EXIT event local event = events_ch:receive() if event.kind == process.event.EXIT then print("Worker terminated:", event.from) endendПрекращение мониторинга
Заголовок раздела «Прекращение мониторинга»Используйте process.unmonitor() для прекращения получения событий EXIT:
local function main() local time = require("time") local events_ch = process.events()
-- Spawn and monitor local worker_pid, err = process.spawn_monitored( "app.workers:long_worker", "app:processes" )
time.sleep("5ms")
-- Stop monitoring local ok, unmon_err = process.unmonitor(worker_pid) if unmon_err then return nil, "unmonitor failed: " .. tostring(unmon_err) end
-- Cancel worker process.cancel(worker_pid)
-- No EXIT event will be received (we unmonitored) local timeout = time.after("200ms") local result = channel.select { events_ch:case_receive(), timeout:case_receive(), }
if result.channel == events_ch then return nil, "should not receive event after unmonitor" endendСвязывание процессов
Заголовок раздела «Связывание процессов»Явное связывание
Заголовок раздела «Явное связывание»Используйте process.link() для создания двунаправленной связи:
-- Worker that links to a target processlocal function worker_main() local time = require("time") local events_ch = process.events() local inbox_ch = process.inbox()
-- Enable trap_links to receive LINK_DOWN events process.set_options({ trap_links = true })
-- Receive target PID from sender local msg = inbox_ch:receive() local target_pid = msg:payload():data() local sender = msg:from()
-- Create bidirectional link local ok, err = process.link(target_pid) if err then return nil, "link failed: " .. tostring(err) end
-- Notify sender we're linked process.send(sender, "linked", process.pid())
-- Wait for LINK_DOWN when target exits local timeout = time.after("3s") local result = channel.select { events_ch:case_receive(), timeout:case_receive(), }
if result.channel == events_ch then local event = result.value if event.kind == process.event.LINK_DOWN then return "LINK_DOWN_RECEIVED" end end
return nil, "no LINK_DOWN received"endСоздание со связыванием
Заголовок раздела «Создание со связыванием»Используйте process.spawn_linked() для создания и связывания одним вызовом:
local function parent_main() -- Enable trap_links to handle child death process.set_options({ trap_links = true })
local events_ch = process.events()
-- Spawn and link to child local child_pid, err = process.spawn_linked( "app.workers:child_worker", "app:processes" ) if err then return nil, "spawn_linked failed: " .. tostring(err) end
-- If child dies, we receive LINK_DOWN local event = events_ch:receive() if event.kind == process.event.LINK_DOWN then print("Child died:", event.from) endendПерехват связей
Заголовок раздела «Перехват связей»По умолчанию при отказе связанного процесса текущий процесс тоже завершается с ошибкой. Установите trap_links=true для получения событий LINK_DOWN вместо этого.
Поведение по умолчанию (trap_links=false)
Заголовок раздела «Поведение по умолчанию (trap_links=false)»Без trap_links отказ связанного процесса завершает текущий процесс:
local function worker_main() local events_ch = process.events()
-- trap_links is false by default local opts = process.get_options() print("trap_links:", opts.trap_links) -- false
-- Spawn linked worker that will fail local child_pid, err = process.spawn_linked( "app.workers:error_worker", "app:processes" )
-- When child errors, THIS process terminates -- We never reach this point local event = events_ch:receive()endС trap_links=true
Заголовок раздела «С trap_links=true»Включите trap_links для получения событий LINK_DOWN и продолжения работы:
local function worker_main() -- Enable trap_links process.set_options({ trap_links = true })
local events_ch = process.events()
-- Spawn linked worker that will fail local child_pid, err = process.spawn_linked( "app.workers:error_worker", "app:processes" )
-- Wait for LINK_DOWN event local event = events_ch:receive()
if event.kind == process.event.LINK_DOWN then print("Child failed, handling gracefully") return "LINK_DOWN_RECEIVED" endendОтправка сигнала отмены
Заголовок раздела «Отправка сигнала отмены»Используйте process.cancel() для корректного завершения процесса:
local function main() local time = require("time") local events_ch = process.events()
-- Spawn and monitor worker local worker_pid, err = process.spawn_monitored( "app.workers:long_worker", "app:processes" )
time.sleep("5ms")
-- Cancel the worker local ok, cancel_err = process.cancel(worker_pid) if cancel_err then return nil, "cancel failed: " .. tostring(cancel_err) end
-- Wait for EXIT event local event = events_ch:receive() if event.kind == process.event.EXIT then print("Worker cancelled:", event.from) endendОбработка отмены
Заголовок раздела «Обработка отмены»Воркер получает событие CANCEL через process.events():
local function worker_main() local events_ch = process.events() local inbox_ch = process.inbox()
while true do local result = channel.select { inbox_ch:case_receive(), events_ch:case_receive(), }
if result.channel == events_ch then local event = result.value if event.kind == process.event.CANCEL then -- Cleanup resources cleanup() return "cancelled gracefully" end else -- Process inbox message handle_message(result.value) end endendТопологии супервизии
Заголовок раздела «Топологии супервизии»Звёздная топология
Заголовок раздела «Звёздная топология»Родитель с несколькими потомками, связанными с ним:
-- Parent worker spawns children that link TO parentlocal function star_parent_main() local time = require("time") local events_ch = process.events() local child_count = 10
-- Enable trap_links to see children die process.set_options({ trap_links = true })
local children = {}
-- Spawn children for i = 1, child_count do local child_pid, err = process.spawn( "app.workers:linker_child", "app:processes" ) if err then error("spawn child failed: " .. tostring(err)) end
-- Send parent PID to child process.send(child_pid, "inbox", process.pid()) children[child_pid] = true end
-- Wait for all children to confirm link for i = 1, child_count do local msg = process.inbox():receive() if msg:topic() ~= "linked" then error("expected linked confirmation") end end
-- Trigger failure - all children should receive LINK_DOWN error("PARENT_STAR_FAILURE")endВоркер-потомок, связывающийся с родителем:
local function linker_child_main() local events_ch = process.events() local inbox_ch = process.inbox()
-- Receive parent PID local msg = inbox_ch:receive() local parent_pid = msg:payload():data()
-- Link to parent process.link(parent_pid)
-- Confirm link process.send(parent_pid, "linked", process.pid())
-- Wait for LINK_DOWN when parent dies local event = events_ch:receive() if event.kind == process.event.LINK_DOWN then return "parent_died" endendЦепочечная топология
Заголовок раздела «Цепочечная топология»Линейная цепочка, где каждый узел связан с родителем:
-- Chain root: A -> B -> C -> D -> Elocal function chain_root_main() local time = require("time")
-- Spawn first child local child_pid, err = process.spawn_linked( "app.workers:chain_node", "app:processes", 4 -- depth remaining ) if err then error("spawn failed: " .. tostring(err)) end
-- Wait for chain to build time.sleep("100ms")
-- Trigger cascade - all linked processes die error("CHAIN_ROOT_FAILURE")endУзел цепочки создаёт следующий узел и связывается:
local function chain_node_main(depth) local time = require("time")
if depth > 0 then -- Spawn next in chain local child_pid, err = process.spawn_linked( "app.workers:chain_node", "app:processes", depth - 1 ) if err then error("spawn failed: " .. tostring(err)) end end
-- Block until parent death kills us via LINK_DOWN (default trap_links=false) process.inbox():receive()endПул воркеров с супервизией
Заголовок раздела «Пул воркеров с супервизией»Конфигурация
Заголовок раздела «Конфигурация»version: "1.0"namespace: app
entries: - name: processes kind: process.host host: workers: 16 lifecycle: auto_start: trueversion: "1.0"namespace: app.supervisor
entries: - name: pool kind: process.lua source: file://pool.lua method: main modules: - time lifecycle: auto_start: trueРеализация супервизора
Заголовок раздела «Реализация супервизора»-- src/supervisor/pool.lualocal function main(worker_count) local time = require("time") worker_count = worker_count or 4
-- Enable trap_links to handle worker deaths process.set_options({ trap_links = true })
local events_ch = process.events() local workers = {}
local function start_worker(id) local pid, err = process.spawn_linked( "app.workers:task_worker", "app:processes", id ) if err then print("Failed to start worker " .. id .. ": " .. tostring(err)) return nil end
workers[pid] = {id = id, started_at = os.time()} print("Worker " .. id .. " started: " .. pid) return pid end
-- Start initial pool for i = 1, worker_count do start_worker(i) end
print("Supervisor started with " .. worker_count .. " workers")
-- Supervision loop while true do local timeout = time.after("60s") local result = channel.select { events_ch:case_receive(), timeout:case_receive(), }
if result.channel == timeout then -- Periodic health check local count = 0 for _ in pairs(workers) do count = count + 1 end print("Health check: " .. count .. " active workers")
elseif result.channel == events_ch then local event = result.value
if event.kind == process.event.LINK_DOWN then local dead_worker = workers[event.from] if dead_worker then workers[event.from] = nil local uptime = os.time() - dead_worker.started_at print("Worker " .. dead_worker.id .. " died after " .. uptime .. "s, restarting")
-- Brief delay before restart time.sleep("100ms") start_worker(dead_worker.id) end end end endend
return { main = main }Конфигурация процессов
Заголовок раздела «Конфигурация процессов»Определение воркера
Заголовок раздела «Определение воркера»version: "1.0"namespace: app.workers
entries: - name: task_worker kind: process.lua source: file://task_worker.lua method: main modules: - timeРеализация воркера
Заголовок раздела «Реализация воркера»-- src/workers/task_worker.lualocal function main(worker_id) local time = require("time") local events_ch = process.events() local inbox_ch = process.inbox()
print("Task worker " .. worker_id .. " started")
while true do local timeout = time.after("5s") local result = channel.select { inbox_ch:case_receive(), events_ch:case_receive(), timeout:case_receive(), }
if result.channel == events_ch then local event = result.value if event.kind == process.event.CANCEL then print("Worker " .. worker_id .. " cancelled") return "cancelled" elseif event.kind == process.event.LINK_DOWN then print("Worker " .. worker_id .. " linked process died") return nil, "linked_process_died" end
elseif result.channel == inbox_ch then local msg = result.value local topic = msg:topic() local payload = msg:payload():data()
if topic == "work" then print("Worker " .. worker_id .. " processing: " .. payload) time.sleep("100ms") process.send(msg:from(), "result", "completed: " .. payload) end
elseif result.channel == timeout then -- Idle timeout print("Worker " .. worker_id .. " idle") end endend
return { main = main }Конфигурация хоста процессов
Заголовок раздела «Конфигурация хоста процессов»Хост процессов управляет количеством потоков ОС, выполняющих процессы:
version: "1.0"namespace: app
entries: - name: processes kind: process.host host: workers: 16 # Number of OS threads lifecycle: auto_start: trueНастройка workers:
- Управляет параллелизмом для CPU-зависимых задач
- Обычно устанавливается равным числу ядер процессора
- Все процессы используют общий пул потоков
Типы событий
Заголовок раздела «Типы событий»| Событие | Вызывается | Необходимая настройка |
|---|---|---|
EXIT | Завершение мониторируемого процесса | spawn_monitored() или monitor() |
LINK_DOWN | Отказ связанного процесса | spawn_linked() или link() с trap_links=true |
CANCEL | Вызов process.cancel() | Не требуется (всегда доставляется) |
Запуск пула с супервизором
Заголовок раздела «Запуск пула с супервизором»Разместите файлы пула в структуру, показанную в разделе Конфигурация, затем:
wippy initwippy runСупервизор автоматически стартует, порождает четырёх воркеров и логирует перезапуски при гибели любого из них. Для принудительного перезапуска завершите воркер из другого процесса:
-- in an ad-hoc process or chat commandprocess.cancel("<pid-from-supervisor-log>")Пул получает LINK_DOWN, ждёт 100 мс и перезапускает воркер под тем же идентификатором.
Следующие шаги
Заголовок раздела «Следующие шаги»- Процессы — основы процессов
- Каналы — паттерны передачи сообщений
- Модуль процессов — справочник API