Очередь тяжелых заданий на bash

в 7:45, , рубрики: bash, linux, метки:

Доброго времени суток, читатели и хабрачитатели!
Возникла у меня недавно следующая задача: требуется мониторить определенный каталог на наличие файлов, и, в случае появления в нем файла необходимо этот файл перенести в более безопасное другое место, и запустить на нем довольно длительную обработку. Казалось бы, все просто, однако ситуация омрачается тем, что нельзя делать обработку одновременно нескольких файлов (обработка тянет файлы с буржуйских серверов, которые не позволяют качать много всего с одного 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:

  1. Который следит за новыми данными и переносит их
  2. Который отправляет новые данные в очередь (этот скрипт выносится отдельно — о причинах можно прочитать в комментариях)
  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

Поделиться

* - обязательные к заполнению поля