Доброго времени суток, читатели и хабрачитатели!
Возникла у меня недавно следующая задача: требуется мониторить определенный каталог на наличие файлов, и, в случае появления в нем файла необходимо этот файл перенести в более безопасное другое место, и запустить на нем довольно длительную обработку. Казалось бы, все просто, однако ситуация омрачается тем, что нельзя делать обработку одновременно нескольких файлов (обработка тянет файлы с буржуйских серверов, которые не позволяют качать много всего с одного IP).
На ум сразу же пришла очередь заданий (FIFO), которую хотелось бы сделать на bash (чего уж далеко ходить). Желающих получить готовое решение — прошу под хабракат.
Статья расчитана на начинающих, которые впервые слышат буквосочетание FIFO применительно к Linux.
Вкратце о том, что будет сделано: мы создадим очередь команд, которые надо будет выполнять одну за одной. Скрипт, следящий за очередью, будет проверять файл-локер jobq.lock. Если его нет, значит никто никаких заданий не выполняет и можно смело брать следующую. Если же он есть, то читать что-либо из очереди не нужно и можно смело выходить с чувством выполненного долга.
Для начала создадим очередь и место расположения наших скриптов:
umask 077
mkdir -p ~/jobs/var
mkfifo ~/jobs/var/jobq
mkdir -p ~/jobs/bin
В bin будут лежать наши скрипты, которые будут запускаться, а в var — все, что касается очереди (собственно, сама очередь jobq, а также файл-локер jobq.lock).
Также должны существовать рабочая, входная и выходная папки. В моем случае это ~/jobs/Input, ~/jobs/Work и ~/jobs/Output
Далее начинаем писать наши скрипты. Их получилось 3:
- Который следит за новыми данными и переносит их
- Который отправляет новые данные в очередь (этот скрипт выносится отдельно — о причинах можно прочитать в комментариях)
- Который, собственно, проверяет очередь и запускает оттуда задания
Начнем в порядке нумерации ($HOME/jobs/bin/mover.sh)
#!/bin/bash
# Скрипт, который следит за новыми данными и переносит их
# Соберем нужные нам файлы в кучу
FILES_LIST=( $(ls $HOME/jobs/Input) )
# И по этой куче пройдемся циклом
for raw_file in ${FILES_LIST[@]}; do
mv $HOME/jobs/Input/$raw_file $HOME/jobs/Work/
# Определяем имя файла, без пути. По идее, этого делать даже не надо, но я оставлю это здесь
filename=$(basename $raw_file)
# Определяем имя файла без расширения
name=${filename%.*}
# В этот каталог будем складывать выходные результаты скрипта
mkdir -p $HOME/jobs/Output/$name
# Обращаемся к скрипту #2, который должен засунуть задание в очередь.
# В качестве параметра скрипту передается целиком и полностью вся команда
# А мы хотим запустить $HOME/complicated_task.sh -i $HOME/jobs/$raw_file -o $HOME/jobs/Output/$name >> $HOME/jobs/Output/$name/task.log
$HOME/jobs/submit.sh "$HOME/complicated_task.sh -i $HOME/jobs/$raw_file -o $HOME/jobs/Output/$name >> $HOME/jobs/Output/$name/task.log"
done
В этом скрипте все крайне просто. И хорошо (я надеюсь!) описаны действия в комментариях.
Осталось совсем немного — написать задание crontab'у. Будем выполнять этот скрипт каждую минуту
crontab -e
* * * * * $HOME/jobs/bin/mover.sh
Переходим ко второму скрипту, который будет ставить наши задания в очередь ($HOME/jobs/bin/submit.sh):
#!/bin/bash
# submit.sh.
# Скрипт отправляет задания в очередь
# С подобной очередью есть проблема, когда задание послано в очередь,
# скрипт будет ждать, пока это задание не прочтут из очереди.
# Поэтому приходится оптравлять задание в фоновый режим
# (обращаем внимание на важный & в конце)
echo $* > $HOME/jobs/var/jobq &
Действительно, если не поставить & в конце задания, скрипт повиснет и будет ждать конца работы всех предыдущих заданий. Зачем это терпеть? Отправим в фон.
И, наконец, сам виновник торжества, скрипт, который читает очередь и запускает задания из нее ($HOME/jobs/bin/execute.sh):
#!/bin/sh
# execute.sh
# Скрипт читает очередь и выполняет задания из нее
# jobq.lock - файл, означающий, что другое задание уже выполняется
# Если другое задание выполняется, то необходимо немедленно выйти
test -f $HOME/jobs/var/jobq.lock && exit 0
# Заберем себе возможность выполнение, если не вышли раньше
touch $HOME/jobs/var/jobq.lock || exit 2
# Читаем очередь
read job < $HOME/jobs/var/jobq
# Запускаем программу и заодно пишем лог задач:
date >> $HOME/jobs/jobs.log
echo " RUN: $job" >> $HOME/jobs/jobs.log
echo "" >> $HOME/jobs/jobs.log
eval $job
#Запоминаем статус выхода
status=$?
# Когда закончили, освобождаем очередь
rm -f $HOME/jobs/var/jobq.lock || exit 3
# Выходим с тем же кодом, что и у нашей задачи
exit $status
И снова создаем новое задание нашему другу crontab'у:
crontab -e
* * * * * $HOME/jobs/bin/execute.sh
Такая система стабильно работает уже пару недель, а я решил написать сюда — вдруг кому-нибудь понадобится.
Автор: serenheit