Запретить выполнение дублирующихся заданий cron

92

Я запланировал запуск задания cron каждую минуту, но иногда выполнение сценария занимает больше минуты, и я не хочу, чтобы задания начинали «складываться» друг с другом. Я предполагаю, что это проблема параллелизма - т.е. выполнение скрипта должно быть взаимоисключающим.

Чтобы решить эту проблему, я заставил скрипт проверить наличие определенного файла (« lockfile.txt ») и завершить работу, если он существует, или touchего нет. Но это довольно паршивый семафор! Есть ли лучшая практика, о которой я должен знать? Должен ли я написать вместо этого демон?

Том
источник

Ответы:

118

Есть пара программ, которые автоматизируют эту функцию, устраняют раздражение и потенциальные ошибки, делая это самостоятельно, и избегают проблемы устаревшей блокировки, также используя flock за кулисами (что является риском, если вы используете только касание) , Я использовал lockrunи lckdoв прошлом, но теперь есть flock(1) (в новых версиях util-linux), что замечательно. Это действительно легко использовать:

* * * * * /usr/bin/flock -n /tmp/fcj.lockfile /usr/local/bin/frequent_cron_job
romble
источник
2
lckdo будет удален из moreutils, теперь, когда flock (1) находится в util-linux. И этот пакет в основном является обязательным в системах Linux, поэтому вы должны иметь возможность полагаться на его наличие. Для использования смотрите ниже.
jldugger
Да, стадо теперь мой любимый вариант. Я даже обновлю свой ответ, чтобы удовлетворить.
Уомбл
Кто-нибудь знает разницу между flock -n file commandа flock -n file -c command?
Нанн
2
@Nanne, я должен был бы проверить код, чтобы быть уверенным, но мое образованное предположение состоит в том, что -cзапускает указанную команду через оболочку (согласно man-странице), в то время как «голая» (не -c) форма просто execс данной командой , Помещение чего-либо через оболочку позволяет вам делать вещи, подобные оболочке (например, запускать несколько команд, разделенных ;или &&), но также открывает вам возможности для атак расширения оболочки, если вы используете ненадежный ввод.
womble
1
Это был аргумент для (гипотетической) frequent_cron_jobкоманды, которая пыталась показать, что она запускается каждую минуту. Я удалил его, так как он не добавил ничего полезного и вызвал замешательство (ваше, если никто не будет за эти годы).
Вомбл
28

Лучший способ в оболочке - использовать flock (1)

(
  flock -x -w 5 99
  ## Do your stuff here
) 99>/path/to/my.lock
Филип Рейнольдс
источник
1
Я не могу не одобрить хитрое использование перенаправления fd. Это просто невероятно круто.
womble
1
Не разбирает для меня в Bash или ZSH, нужно устранить пробел между 99и >так оно и есть99> /...
Кайл Брандт
2
@Javier: Это не значит, что он не хитрый и не таинственный, просто он задокументированный , хитрый и таинственный.
womble
1
что произойдет, если вы перезапустите компьютер во время его работы или как-нибудь убьете процесс? Будет ли он заперт навсегда тогда?
Алекс Р
5
Я понимаю, что эта структура создает исключительную блокировку, но я не понимаю, как это делается. Какова функция «99» в этом ответе? Кто-нибудь хочет объяснить это, пожалуйста? Спасибо!
Asciiom
22

На самом деле, flock -nможет использоваться вместо lckdo*, поэтому вы будете использовать код от разработчиков ядра.

Основываясь на примере womble , вы бы написали что-то вроде:

* * * * * flock -n /some/lockfile command_to_run_every_minute

Кстати, глядя на код, все flock, lockrunи lckdoсделать то же самое, так что это просто вопрос , который является наиболее доступным для вас.

эмир
источник
2

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

Lock-файлы используются initscripts и многими другими приложениями и утилитами в системах Unix.

Рожденный ездить
источник
1
это единственный способ, которым я когда-либо видел, чтобы это было реализовано лично. Я использую в соответствии с предложением сопровождающего в качестве зеркала для проекта OSS
Уоррен
2

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

Итак, если вы не хотите зависеть от lckdo или подобного, вы можете сделать это:


PIDFILE=/tmp/`basename $0`.pid

if [ -f $PIDFILE ]; then
  if ps -p `cat $PIDFILE` > /dev/null 2>&1; then
      echo "$0 already running!"
      exit
  fi
fi
echo $$ > $PIDFILE

trap 'rm -f "$PIDFILE" >/dev/null 2>&1' EXIT HUP KILL INT QUIT TERM

# do the work

Александар Иванишевич
источник
Спасибо, ваш пример полезен - я хочу, чтобы скрипт завершился, если он уже запущен. Спасибо за упоминание ickdo - похоже, все в порядке.
Том
FWIW: мне нравится это решение, потому что оно может быть включено в скрипт, поэтому блокировка работает независимо от того, как вызывается скрипт.
Дэвид Г
1

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


источник
3
Я искренне не согласен с этим. Если у вас есть что-то, что нужно периодически запускать, то сделать его демоном - это решение «кувалды для ореха». Использование файла блокировки для предотвращения несчастных случаев - вполне разумное решение, с которым у меня никогда не возникало проблем.
womble
@womble Я согласен; но мне нравится разбивать орехи кувалдой! :-)
wzzrd
1

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

dubiousjim
источник
1

Я бы порекомендовал использовать команду run-one - намного проще, чем иметь дело с блокировками. Из документов:

run-one - это скрипт-обертка, который запускает не более одного уникального экземпляра некоторой команды с уникальным набором аргументов. Это часто полезно с cronjobs, когда вы хотите, чтобы одновременно работало не более одной копии.

run-this-one точно такой же, как и run-one, за исключением того, что он будет использовать pgrep и kill для поиска и уничтожения любых запущенных процессов, принадлежащих пользователю и соответствующих целевым командам и аргументам. Обратите внимание, что run-this-one будет блокироваться при попытке уничтожить соответствующие процессы, пока все соответствующие процессы не будут мертвыми.

run-one-постоянно работает точно так же, как run-one, за исключением того, что он вызывает «COMMAND [ARGS]» каждый раз, когда выходит из команды COMMAND (ноль или не ноль).

keep-one-running - это псевдоним для run-one-постоянно.

run-one-till-success работает точно так же, как run-one-постоянно, за исключением того, что она вызывает «COMMAND [ARGS]» до тех пор, пока COMMAND не завершится успешно (то есть выйдет из нуля).

run-one-till-fail работает точно так же, как run-one-постоянно, за исключением того, что он вызывает «COMMAND [ARGS]» до тех пор, пока COMMAND не завершится с ошибкой (то есть выйдет с ненулевым значением).

Юрик
источник
1

Теперь, когда systemd отсутствует, в системах Linux есть еще один механизм планирования:

systemd.timer

В /etc/systemd/system/myjob.serviceили ~/.config/systemd/user/myjob.service:

[Service]
ExecStart=/usr/local/bin/myjob

В /etc/systemd/system/myjob.timerили ~/.config/systemd/user/myjob.timer:

[Timer]
OnCalendar=minutely

[Install]
WantedBy=timers.target

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

Альтернатива, которая запускает задание один раз при загрузке и через одну минуту после каждого запуска:

[Timer]
OnBootSec=1m
OnUnitInactiveSec=1m 

[Install]
WantedBy=timers.target
эмир
источник
0

Я создал одну банку, чтобы решить такую ​​проблему, как если бы дублирующиеся кроны работали, это мог быть java или shell cron. Просто передайте имя cron в Duplicates.CloseSessions ("Demo.jar"), это будет искать и убивать существующий pid для этого cron, кроме текущего. Я реализовал метод, чтобы сделать это. String proname = ManagementFactory.getRuntimeMXBean (). GetName (); String pid = proname.split ("@") [0]; System.out.println («Текущий PID:» + pid);

            Process proc = Runtime.getRuntime().exec(new String[]{"bash","-c"," ps aux | grep "+cronname+" | awk '{print $2}' "});

            BufferedReader stdInput = new BufferedReader(new InputStreamReader(proc.getInputStream()));
            String s = null;
            String killid="";

            while ((s = stdInput.readLine()) != null ) {                                        
                if(s.equals(pid)==false)
                {
                    killid=killid+s+" ";    
                }
            }

А затем убить строку killid снова командой shell

Сачин Патил
источник
Я не думаю, что это действительно отвечает на вопрос.
kasperd
0

Ответ @Philip Reynolds начнет выполнять код после того, как время ожидания 5 секунд все равно не получит блокировку. Кажется, что следующий Flock не работает, я изменил ответ @Philip Reynolds на

(
  flock -w 5 -x 99 || exit 1
  ## Do your stuff here
) 99>/path/to/my.lock

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

user__42
источник