2017-10-17 Atomic Bash.md

Атомарность в bash

Tags: bash, правила программирования

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

data_dir='/tmp/test_data'
rm -rf "${vata_dir}/"

Конечно, первой рекомендацией при начале написания скрипта, является включение всех необходимых флагов:

set -euEo pipefail

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

set -euEo pipefail
f(){
    set -e # Не сработает
    false
    echo "Hello!"
    return 0
}
if ! f; then
    echo "Newer happen"
fi

Здесь вы заметите return 0 в конце функции. Мы ввели правило, что в конце любой функции должно находиться return 0, это спасает от срабатываний set -e, когда это не нужно:

set -euEo pipefail
f(){
    echo "Do somphing..."
    [ -f /tmp/test.$$ ] && rm -f /tmp/test.$$
}
echo "Hello!"
f
echo "Newer happen!"

Раньше мы условное выражение расписывали в виде if then fi, пока не додумались, что return 0 избавит нас от этого

set -euEo pipefail
f(){
    echo "Do somphing..."
    [ -f /tmp/test.$$ ] && rm -f /tmp/test.$$
    return 0
}
echo "Hello!"
f
echo "Goodbye!"

Но вернемся к первоначальной проблеме: скриптов все больше, багов тоже, как жить/что делать? Возьмем среднестатистический пример: нам нужно распарсить один файл и записать результат в соседний.

cat /home/one_file | sed 's/old_stuff/new_stuff/' > /tmp/tmp_file
echo "Do somphing..."
cat /tmp/tmp_file > /home/second_file

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

trap _exit EXIT
_exit(){
    res=$?
    rm -f /tmp/test_script*.$$
    return $res
}
cat /home/one_file | sed 's/old_stuff/new_stuff/' > /tmp/test_script.tmp_file.$$
echo "Do somphing..."
cat /tmp/test_script.tmp_file.$$ > /home/second_file

Все временные файлы мы помечаем PIDом текущего процесса, плюс ловим выход из скрипта и подчищаем за собой все временные файлы, независимо от того, выполнился скрипт до конца или нет. Удаление всех файлов по маске с именем скрипта и пидом позволит не задумываться о «сборке мусора» и захламлению каталога /tmp кучей мелких файлов, хотя у нас в последнее время пропагандируется отказ от этого, но я пока не буду это рассматривать. Но что делать, если скрипт упадет в середине записи в файл (например, от банальной нехватки места)? Сохранять результат во временный файл и когда он полностью готов, заменять новой версией старый файл атомарно, одной командой mv -f:

cat /home/one_file | sed 's/old_stuff/new_stuff/' > /tmp/test_script.tmp_file.$$
echo "Do somphing..."
mv -f /tmp/test_script.tmp_file.$$ /home/second_file

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

Ваш комментарий. Вики-синтаксис разрешён: