Как запустить несколько процессов и выйти, если любой из них завершится или потерпит неудачу

Вопрос или проблема

Я запущу несколько процессов и хочу выйти с соответствующим кодом завершения (это значит ошибка в случае неудачи, успех в противном случае), если любой из них завершится с ошибкой или выйдет.

Кроме того, если любой дочерний процесс завершится или потерпит неудачу, все другие дочерние процессы также должны быть завершены.

Мое текущее неработающее решение (yarn — это просто пример; может быть любая другая команда):

#!/bin/bash -e

# Запустить другой процесс перед началом
sh ./bin/optimize.sh

trap 'exit' INT TERM
trap 'kill -INT 0' EXIT

# Запустите расписание
sh ./bin/schedule.sh &
# Запустите долго работающую задачу
yarn task &

wait

./bin/schedule.sh:

#!/bin/bash -e

while true; do
  yarn schedule
  sleep 30
done

Если что-то в yarn schedule не удается, все корректно завершится. Но когда я убиваю процесс с помощью ctrl+c или yarn task выходит, yarn schedule продолжает работать.

Как сделать так, чтобы это работало независимо от того, какие дочерние процессы (bash, yarn, php или что-то еще)?

Я не могу использовать GNU parallel.

Это весьма затруднительно в оболочках, потому что встроенная команда wait не делает «ожидание любого», она делает «ожидание всех». wait без аргумента ожидает завершения всех дочерних процессов и возвращает 0. wait с явным списком процессов ожидает завершения всех из них и возвращает статус последнего аргумента. Чтобы дождаться нескольких дочерних процессов и получить их коды завершения, нужен другой подход. wait может дать вам код завершения только в том случае, если вы знаете, какой дочерний процесс уже завершился.

Один из возможных подходов — использовать выделенный именованный канал для сообщения о статусе каждого дочернего процесса. Следующий фрагмент (не тестировался!) возвращает наибольший из статусов дочерних процессов.

mkfifo status_pipe
children=0
{ child1; echo 1 $? >status_pipe; } & children=$((children+1))
{ child2; echo 2 $? >status_pipe; } & children=$((children+1))
max_status=0
while [ $children -ne 0 ]; do
  read -r child status <status_pipe
  children=$((children-1))
  if [ $status -gt $max_status ]; then
    max_status=$status
  fi
done
rm status_pipe

Обратите внимание, что это будет блокироваться навсегда, если один из подпроцессов погибнет, не сообщив о своем статусе. Это не произойдет в типичных условиях, но может произойти, если подпроцесс был убит вручную или если у него не хватает памяти.

Если вы хотите сделать что-то, как только один из дочерних процессов завершится с ошибкой, замените if [ $status -gt $max_status ]; then … на if [ $status -ne 0 ]; then ….

GNU Parallel имеет --halt. Это убьет все работающие задания, если одно из заданий завершится или погибнет, и вернет ложь, если задание не удалось:

parallel --halt now,done=1 ::: 'sleep 1;echo a' 'sleep 2;echo b' ||
  echo задание, которое завершилось, потерпело неудачу

parallel --halt now,done=1 ::: 'sleep 1;echo a;false' 'sleep 2;echo b' ||
  echo задание, которое завершилось, потерпело неудачу

Для систем, на которых не установлен GNU Parallel, вы обычно можете написать ваш скрипт на системе, которая имеет GNU Parallel, и использовать --embed, чтобы встроить GNU Parallel прямо в скрипт:

parallel --embed > myscript.sh

Вот решение с использованием wait -n и функции, которая говорит bash, сколько процессов ожидать:

set -o nounset
set -o errexit
set -o pipefail

wait_many () {
  for ((i = 0 ; i < $1 ; i++)); do
    wait -n
  done
}

(sleep 2 && echo "хорошо 1") &
(sleep 3 && echo "хорошо 2") &
(sleep 4 && echo "плохо 3" && echo $unset) &
(sleep 5 && echo "хорошо 4") &

wait_many 4

Преимущества этого подхода:

  • он простой, если есть фиксированное количество процессов, которые нужно ожидать
  • он может позволить нам выйти из скрипта с ненулевым кодом завершения, если любой из подпроцессов завершится неудачно

Ответ или решение

Вопрос, как корректно запускать несколько процессов и завершаться при выходе любого из них, действительно является важной задачей в программировании и администрировании системы. В данной статье мы рассмотрим несколько эффективных подходов к решению этой проблемы, которые помогут вам контролировать состояние дочерних процессов и корректно завершать выполнение сценария в случае их сбоя.

Введение

Управление жизненным циклом процессов в оболочке Bash требует тщательной проработки, особенно когда необходимо контролировать несколько параллельных задач. При этом важно обеспечить, чтобы при завершении или сбое одного процесса все остальные процессы также были остановлены, а сценарий завершился с адекватным кодом выхода.

Подход с использованием именованного канала

Одним из распространенных методов является использование именованных каналов (named pipes). Этот метод позволяет дочерним процессам сообщать о своем статусе.

#!/bin/bash

set -o nounset
set -o errexit
set -o pipefail

mkfifo status_pipe

# Общее количество дочерних процессов
children=0

# Запуск процессов с записью их статуса
{ child1; echo 1 $? > status_pipe; } &
children=$((children+1))

{ child2; echo 2 $? > status_pipe; } &
children=$((children+1))

max_status=0
while [ $children -ne 0 ]; do
  read -r child status < status_pipe
  children=$((children-1))
  if [ $status -ne 0 ]; then
    max_status=$status
    # Остановить все остальные процессы
    kill 0
  fi
done

# Удалить именованный канал
rm status_pipe

exit $max_status

В данной реализации, если любой из дочерних процессов завершится с ненулевым статусом, скрипт остановит другие процессы и завершится с соответствующим кодом выхода. Однако следует учитывать, что этот подход может блокироваться в случае, если один из процессов завершится, не сообщив о своем статусе.

Использование wait -n

С bash 5.0 имеется возможность использовать команду wait -n, которая позволяет ожидать завершения любого дочернего процесса. Это значительно упрощает задачу.

#!/bin/bash

set -o nounset
set -o errexit
set -o pipefail

# Функция для ожидания множества процессов
wait_many() {
  for ((i = 0; i < $1; i++)); do
    wait -n
  done
}

# Запуск процессов
(sleep 2 && echo "good 1") &
(sleep 3 && echo "good 2") &
(sleep 1 && echo "failed" && false) &
(sleep 5 && echo "good 4") &

# Ожидание завершения всех процессов
wait_many 4

Этот метод предоставляет вам гибкость и простоту при управлении несколькими процессами. Если любой из процессов завершится с ошибкой, вы сможете завершить выполнение скрипта с соответствующим кодом ошибки.

Заключение

Управление несколькими процессами и их завершение при ошибках требует внимательности, но с использованием методов, представленных в этой статье, вы сможете создать надежный скрипт для решения данной задачи. Выбор подхода может зависеть от версии Bash и специфики вашего проекта, однако оба метода демонстрируют, как эффективно контролировать процессы и адекватно реагировать на их завершение.

Следуя описанным подходам, вы сможете значительно упростить процесс управления многими задачами, обеспечивая необходимый контроль и надежность в вашем коде.

Оцените материал
Добавить комментарий

Капча загружается...