Impedir a execução de tarefas cron duplicadas

92

Programei um trabalho cron para ser executado a cada minuto, mas às vezes o script leva mais de um minuto para terminar e não quero que os trabalhos comecem a "empilhar" um sobre o outro. Acho que esse é um problema de simultaneidade - ou seja, a execução do script precisa ser mutuamente exclusiva.

Para resolver o problema, fiz o script procurar a existência de um arquivo específico (" lockfile.txt ") e sair se ele existir ou se não existir touch. Mas este é um semáforo bastante ruim! Existe uma prática recomendada que eu deva conhecer? Eu deveria ter escrito um daemon?

Tom
fonte

Respostas:

118

Existem alguns programas que automatizam esse recurso, evitam o aborrecimento e os possíveis erros e evitam o problema de bloqueio obsoleto usando o rebanho nos bastidores também (que é um risco se você estiver apenas usando o toque) . Eu usei lockrune lckdono passado, mas agora há flock(1) (em versões novas do util-linux) o que é ótimo. É realmente fácil de usar:

* * * * * /usr/bin/flock -n /tmp/fcj.lockfile /usr/local/bin/frequent_cron_job
mulher
fonte
2
O lckdo será removido do moreutils, agora que o flock (1) está no util-linux. E esse pacote é basicamente obrigatório nos sistemas Linux, então você deve poder confiar na sua presença. Para uso, veja abaixo.
achou
Sim, rebanho agora é minha opção preferida. Vou até atualizar minha resposta para atender.
Womble
Alguém sabe a diferença entre flock -n file commande flock -n file -c command?
Nanne
2
@ Nanne, eu teria que verificar o código para ter certeza, mas meu palpite é que ele -cexecuta o comando especificado por meio de um shell (conforme a página de manual), enquanto a forma "nua" (não -c) é apenas execo comando fornecido . Colocar algo no shell permite que você faça coisas do tipo shell (como executar vários comandos separados por ;ou &&), mas também abre para ataques de expansão do shell se você estiver usando entrada não confiável.
womble
1
Foi um argumento para o frequent_cron_jobcomando (hipotético) que tentou mostrar que ele estava sendo executado a cada minuto. Eu o removi, uma vez que não acrescentou nada útil e causou confusão (sua, se mais ninguém estiver ao longo dos anos).
womble
28

A melhor maneira de usar shell é usar o rebanho (1)

(
  flock -x -w 5 99
  ## Do your stuff here
) 99>/path/to/my.lock
Philip Reynolds
fonte
1
Não posso deixar de votar em um uso complicado do redirecionamento fd. É simplesmente demais demais.
Womble
1
Não analisa para mim em Bash ou ZSH, precisa eliminar o espaço entre 99e >por isso é99> /...
Kyle Brandt
2
@Javier: Não significa que não é complicado e misterioso, apenas que está documentado , complicado e misterioso.
Womble
1
o que aconteceria se você reiniciasse enquanto isso estivesse em execução ou matasse o processo de alguma forma? Seria trancado para sempre então?
Alex R
5
Entendo que essa estrutura cria um bloqueio exclusivo, mas não entendo a mecânica de como isso é realizado. Qual é a função do '99' nesta resposta? Alguém quer explicar isso, por favor? Obrigado!
Asciiom
22

Na verdade, flock -npode ser usado em vez de lckdo*, então você estará usando o código dos desenvolvedores do kernel.

Com base no exemplo de womble , você escreveria algo como:

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

BTW, olhando para o código, todos flock, lockrune lckdofazer exatamente a mesma coisa, por isso é apenas uma questão do que é mais prontamente disponível para você.

Amir
fonte
2

Você pode usar um arquivo de bloqueio. Crie esse arquivo quando o script iniciar e exclua-o quando terminar. O script, antes de executar sua rotina principal, deve verificar se o arquivo de bloqueio existe e prosseguir de acordo.

Os arquivos de bloqueio são usados ​​por initscripts e por muitos outros aplicativos e utilitários nos sistemas Unix.

Born To Ride
fonte
1
esta é a única maneira que eu já vi implementada, pessoalmente. Eu uso em como por sugestão do mantenedor como um espelho para um projeto OSS
Warren
2

Você não especificou se deseja que o script aguarde a execução anterior ou não. Em "Não quero que os trabalhos comecem a se" empilhar ", acho que você está sugerindo que deseja que o script saia se já estiver em execução,

Portanto, se você não quiser depender do lckdo ou similar, faça o seguinte:


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

Aleksandar Ivanisevic
fonte
Obrigado, seu exemplo é útil - quero que o script saia se já estiver em execução. Obrigado por mencionar o ickdo - parece fazer o truque.
Tom
FWIW: Eu gosto dessa solução porque ela pode ser incluída em um script, portanto, o bloqueio funciona independentemente de como o script é chamado.
David G
1

Isso também pode ser um sinal de que você está fazendo a coisa errada. Se seus trabalhos são executados de maneira muito próxima e frequente, talvez você deva desconcentrá-lo e torná-lo um programa no estilo daemon.


fonte
3
Eu discordo sinceramente disso. Se você tem algo que precisa ser executado periodicamente, torná-lo um daemon é uma solução "marreta para uma porca". Usar um arquivo de bloqueio para evitar acidentes é uma solução perfeitamente razoável que nunca tive problemas em usar.
Womble
@womble eu concordo; mas eu gosto de esmagar nozes com marretas! :-)
wzzrd 9/11/2009
1

Seu daemon cron não deve chamar trabalhos se as instâncias anteriores deles ainda estiverem em execução. Eu sou o desenvolvedor de um cron daemon dcron e tentamos especificamente impedir isso. Não sei como o Vixie cron ou outros daemons lidam com isso.

dubiousjim
fonte
1

Eu recomendaria usar o comando executar um - muito mais simples do que lidar com os bloqueios. Dos documentos:

run-one é um script de wrapper que executa não mais que uma instância exclusiva de algum comando com um conjunto exclusivo de argumentos. Isso geralmente é útil com cronjobs, quando você não deseja mais de uma cópia em execução por vez.

O run-this-one é exatamente como o run-one, exceto que ele usará o pgrep e o kill para encontrar e eliminar todos os processos em execução pertencentes ao usuário e correspondentes aos comandos e argumentos de destino. Observe que executar este bloqueará ao tentar eliminar os processos correspondentes, até que todos os processos correspondentes estejam mortos.

A execução um funciona constantemente exatamente como a execução um, exceto que ele reaparece "COMMAND [ARGS]" sempre que o COMMAND sai (zero ou diferente de zero).

keep-one-running é um alias para run-one-constantemente.

executar um até o sucesso opera exatamente como executar um constantemente, exceto que ele reaparece "COMMAND [ARGS]" até que o COMMAND saia com êxito (ou seja, sai zero).

executar um até a falha opera exatamente como executar um constantemente, exceto que ele reaparece "COMMAND [ARGS]" até que COMMAND saia com falha (ou seja, sai diferente de zero).

Yurik
fonte
1

Agora que o systemd foi lançado, existe outro mecanismo de agendamento nos sistemas Linux:

UMA systemd.timer

Em /etc/systemd/system/myjob.serviceou ~/.config/systemd/user/myjob.service:

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

Em /etc/systemd/system/myjob.timerou ~/.config/systemd/user/myjob.timer:

[Timer]
OnCalendar=minutely

[Install]
WantedBy=timers.target

Se a unidade de serviço já estiver ativando quando o próximo timer for ativado, outra instância do serviço não será iniciada.

Uma alternativa, que inicia o trabalho uma vez na inicialização e um minuto após a conclusão de cada execução:

[Timer]
OnBootSec=1m
OnUnitInactiveSec=1m 

[Install]
WantedBy=timers.target
Amir
fonte
0

Eu criei um jar para resolver esse problema, como crons duplicados em execução podem ser java ou cron cron. Apenas passe o nome do cron em Duplicates.CloseSessions ("Demo.jar"), isso pesquisará e eliminará o pid existente para esse cron, exceto o atual. Eu implementei o método para fazer isso. Pronome da string = ManagementFactory.getRuntimeMXBean (). GetName (); String pid = proname.split ("@") [0]; System.out.println ("PID atual:" + 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+" ";    
                }
            }

E então mate killid string com o comando shell novamente

Sachin Patil
fonte
Eu não acho que isso esteja realmente respondendo à pergunta.
kasperd
0

A resposta de @Philip Reynolds começará a executar o código após o tempo de espera de 5s de qualquer maneira, sem obter o bloqueio. Seguir Flock não parece estar funcionando . Modifiquei a resposta de @Philip Reynolds para

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

para que o código nunca seja executado simultaneamente. Em vez disso, após os 5 segundos, aguarde o processo sairá com 1 se não tiver o bloqueio até então.

user__42
fonte