[Перевод] Заметки о Unix: два сценария работы с конвейерами

Мне встречалось множество рекомендаций о повышении безопасности использования shell-скриптов в Bash путём включения опции pipefail (например — это рекомендуется в данном материале 2015 года). Это, с одной стороны, хорошая рекомендация. Но включение pipefail может привести к конфликту. В одном из двух сценариев использования конвейеров эта опция показывает себя замечательно, а вот в другом то, к чему приводит её включение, выглядит просто ужасно.

kfwuy3qrduk8a9xgthipyurx_rg.jpeg
Для того чтобы понять суть этой проблемы давайте сначала разберёмся с тем, за что именно отвечает опция Bash pipefail. Обратимся к документации:

Статусом выхода из конвейера, в том случае, если не включена опция pipefail, служит статус завершения последней команды конвейера. Если опция pipefail включена — статус выхода из конвейера является значением последней (самой правой) команды, завершённой с ненулевым статусом, или ноль — если работа всех команд завершена успешно.

Причина использования pipefail заключается в том, что иначе команда, неожиданно завершившаяся с ошибкой и находящаяся где-нибудь в середине конвейера, обычно остаётся незамеченной. Она, если использовалась опция set -e, не приведёт к аварийному завершению скрипта. Можно пойти другим путём и тщательно проверять всё с использованием $PIPESTATUS, но это означает необходимость выполнения больших объёмов дополнительной работы.

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

generate --thing | sed 1000q | gronkulate

Команда sed, после получения 1000 строк, завершит работу и закроет конвейер, в который пишет данные generate. А generate получит сигнал SIGPIPE и, по умолчанию, остановится. Статус выхода команды будет отличаться от нуля, а это значит, что с использованием pipefail работа всего конвейера «завершится с ошибкой» (а с использованием set -e скрипт нормально завершит работу).

(В некоторых случаях то, что происходит, может, от запуска к запуску, меняться. Причина этого — в системе планирования выполнения процессов. Это может зависеть и от того, какой объём данных производят процессы, находящиеся ближе к началу конвейера, и как он соотносится с тем, что фильтруют процессы, расположенные ближе к концу конвейера. Так, если в нашем примере generate создаст 1000 строк или меньше — sed примет все эти данные.)

Это ведёт к двум шаблонам использования конвейеров командной оболочки. При использовании первого конвейер потребляет все входные данные, действуя так в тех случаях, если всё работает без сбоев. Так как подобным образом работают все процессы — ни у одного процесса никогда не должно возникнуть необходимости выполнять запись в закрытый конвейер. А значит — никогда не появится и сигнал SIGPIPE. Второй шаблон использования конвейеров предусматривает ситуацию, когда хотя бы один процесс завершает обработку входных данных раньше, чем обычно. Часто подобные процессы специально помещают в конвейер для остановки обработки данных в определённый момент (как sed в вышеприведённом примере). Подобные конвейеры иногда или даже всегда генерируют сигналы SIGPIPE, некоторые процессы в них завершаются с ненулевым кодом.

Конечно, пользоваться подобными конвейерами можно и в окружении, где применяется pipefail, и даже с set -e. Например, можно сделать так, чтобы один из шагов конвейера всегда сообщал бы об успешном завершении:

(generate --thing || true) | sed 1000q | gronkulate

Но об этой проблеме нужно помнить, нужно обращать внимание на то, какие команды могут окончить работу раньше, чем обычно, не читая всех входных данных. Если что-то упустить — то в «награду» за это, вероятно, можно получить ошибку из скрипта. Если автору скрипта будет сопутствовать успех, то это будет ошибка, возникающая регулярно. В противном случае ошибки будут возникать время от времени, случаясь тогда, когда одна из команд конвейера выдаёт необычно большой объём выходных данных, или тогда, когда другая команда завершает свою работу необычно рано или необычно быстро.

(Кроме того, очень хорошо было бы игнорировать только ошибки, связанные с SIGPIPE, но не другие ошибки. Если generate завершится с ошибкой по причинам, не связанным с SIGPIPE, то нам хотелось бы, чтобы весь конвейер выглядел бы так, как если бы он завершился с ошибкой.)

Чутьё подсказывает мне, что шаблон использования конвейеров, основанный на полном потреблении всех входных данных, распространён гораздо сильнее, чем шаблон, когда работа конвейера завершается раньше, чем обычно. Правда, я не пытался оценить свои скрипты на предмет особенностей использования в них конвейеров. Это, определённо, совершенно естественный шаблон использования конвейеров, когда в них выполняется фильтрация, трансформация или исследование всех сущностей из некоего набора (например — для того чтобы их посчитать или вывести некие сводные данные по ним).

oug5kh6sjydt9llengsiebnp40w.png

© Habrahabr.ru