Evite la ejecución de trabajos duplicados cron

92

He programado que se ejecute un trabajo cron cada minuto, pero a veces el script tarda más de un minuto en finalizar y no quiero que los trabajos comiencen a "apilarse" unos sobre otros. Supongo que este es un problema de concurrencia, es decir, la ejecución del script debe ser mutuamente excluyente.

Para resolver el problema, hice que el script buscara la existencia de un archivo en particular (" lockfile.txt ") y salga si existe o touchsi no. ¡Pero este es un semáforo bastante pésimo! ¿Hay alguna práctica recomendada que deba conocer? ¿Debería haber escrito un demonio en su lugar?

Tom
fuente

Respuestas:

118

Hay un par de programas que automatizan esta función, eliminan la molestia y los posibles errores de hacerlo usted mismo y evitan el problema de bloqueo obsoleto al usar bandada detrás de escena (lo cual es un riesgo si solo está usando el tacto) . Lo he usado lockruny lckdoen el pasado, pero ahora hay flock(1) (en versiones más recientes de util-linux) que es genial. Es realmente fácil de usar:

* * * * * /usr/bin/flock -n /tmp/fcj.lockfile /usr/local/bin/frequent_cron_job
womble
fuente
2
lckdo se eliminará de moreutils, ahora que flock (1) está en util-linux. Y ese paquete es básicamente obligatorio en los sistemas Linux, por lo que debería poder confiar en su presencia. Para el uso, mira a continuación.
jldugger
Sí, el rebaño es ahora mi opción preferida. Incluso actualizaré mi respuesta para adaptarla.
womble
¿Alguien sabe la diferencia entre flock -n file commandy flock -n file -c command?
Nanne
2
@Nanne, tendría que verificar el código para estar seguro, pero mi conjetura es que -cejecuta el comando especificado a través de un shell (según la página de manual), mientras que la forma "desnuda" (no -c) solo execes el comando dado . Poner algo a través del shell le permite hacer cosas similares al shell (como ejecutar múltiples comandos separados con ;o &&), pero también lo abre a los ataques de expansión del shell si está utilizando una entrada no confiable.
womble
1
Fue un argumento al frequent_cron_jobcomando (hipotético) que intentó mostrar que se ejecutaba cada minuto. Lo eliminé porque no agregó nada útil y causó confusión (la suya, si nadie más ha pasado los años).
womble
28

La mejor manera en shell es usar flock (1)

(
  flock -x -w 5 99
  ## Do your stuff here
) 99>/path/to/my.lock
Philip Reynolds
fuente
1
No puedo no votar un uso complicado de la redirección de fd. Es simplemente increíblemente asombroso.
womble
1
No me analiza en Bash o ZSH, necesito eliminar el espacio entre ellos 99y >así es99> /...
Kyle Brandt es el
2
@Javier: No significa que no sea complicado y arcano, solo que está documentado , es complicado y arcano.
womble
1
¿Qué pasaría si reinicia mientras esto se está ejecutando o si el proceso se cierra de alguna manera? ¿Estaría cerrado para siempre entonces?
Alex R
55
Entiendo que esta estructura crea un bloqueo exclusivo, pero no entiendo la mecánica de cómo se logra esto. ¿Cuál es la función del '99' en esta respuesta? ¿Alguien quiere explicar esto por favor? ¡Gracias!
Asciiom
22

En realidad, flock -nse puede usar en lugar de lckdo*, por lo que usará código de desarrolladores de kernel.

Sobre la base del ejemplo de womble , escribirías algo como:

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

Por cierto, mirando el código, todos flock, lockruny lckdohacer exactamente lo mismo, por lo que es sólo una cuestión de lo que es más fácilmente disponible para usted.

Amir
fuente
2

Puedes usar un archivo de bloqueo. Cree este archivo cuando comience el script y elimínelo cuando finalice. El script, antes de ejecutar su rutina principal, debe verificar si el archivo de bloqueo existe y proceder en consecuencia.

Lockfiles son utilizados por initscripts y por muchas otras aplicaciones y utilidades en sistemas Unix.

Nacido para montar
fuente
1
Esta es la única forma en que lo he visto implementado, personalmente. Lo utilizo según la sugerencia del mantenedor como un espejo para un proyecto OSS
Warren
2

No ha especificado si desea que el script espere a que se complete la ejecución anterior o no. Al decir "No quiero que los trabajos comiencen a" apilarse "unos sobre otros", supongo que estás insinuando que quieres que el script salga si ya se está ejecutando,

Entonces, si no quieres depender de lckdo o similar, puedes hacer esto:


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
fuente
Gracias, su ejemplo es útil: quiero que el script salga si ya se está ejecutando. Gracias por mencionar ickdo , parece que funciona.
Tom
FWIW: Me gusta esta solución porque puede incluirse en un script, por lo que el bloqueo funciona independientemente de cómo se invoque el script.
David G
1

Esto también podría ser una señal de que estás haciendo algo incorrecto. Si sus trabajos se ejecutan tan de cerca y con tanta frecuencia, tal vez debería considerar eliminar el cronograma y convertirlo en un programa estilo demonio.


fuente
3
Estoy totalmente en desacuerdo con esto. Si tiene algo que necesita ejecutarse periódicamente, convertirlo en un demonio es una solución de "mazo para una nuez". Usar un archivo de bloqueo para evitar accidentes es una solución perfectamente razonable que nunca tuve problemas para usar.
womble
@womble estoy de acuerdo; pero me gusta romper nueces con mazos! :-)
wzzrd el
1

Su demonio cron no debería invocar trabajos si aún se están ejecutando instancias anteriores de ellos. Soy el desarrollador de un cron daemon dcron , y tratamos específicamente de evitarlo. No sé cómo Vixie cron u otros demonios manejan esto.

dubiousjim
fuente
1

Recomendaría usar el comando run-one , mucho más simple que lidiar con los bloqueos. De los documentos:

run-one es un script de contenedor que no ejecuta más de una instancia única de algún comando con un conjunto único de argumentos. Esto suele ser útil con cronjobs, cuando no desea que se ejecute más de una copia a la vez.

run-this-one es exactamente como run-one, excepto que usará pgrep y kill para encontrar y eliminar cualquier proceso en ejecución propiedad del usuario y que coincida con los comandos y argumentos de destino. Tenga en cuenta que run-this-one se bloqueará al intentar eliminar procesos coincidentes, hasta que todos los procesos coincidentes estén muertos.

run-one-constantemente opera exactamente como run-one, excepto que reaparece "COMANDO [ARGS]" cada vez que sale de COMANDO (cero o no cero).

keep-one-running es un alias para run-one-constantemente.

ejecutar-uno-hasta-el-éxito funciona exactamente como ejecutar-uno-constantemente, excepto que reaparece "COMANDO [ARGS]" hasta que COMANDO sale con éxito (es decir, sale de cero).

ejecutar-uno-hasta-falla funciona exactamente como ejecutar-uno-constantemente, excepto que reaparece "COMANDO [ARGS]" hasta que COMANDO sale con falla (es decir, sale de cero).

Yurik
fuente
1

Ahora que systemd está fuera, hay otro mecanismo de programación en los sistemas Linux:

UNA systemd.timer

En /etc/systemd/system/myjob.serviceo ~/.config/systemd/user/myjob.service:

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

En /etc/systemd/system/myjob.timero ~/.config/systemd/user/myjob.timer:

[Timer]
OnCalendar=minutely

[Install]
WantedBy=timers.target

Si la unidad de servicio ya se está activando la próxima vez que se active el temporizador, entonces no se iniciará otra instancia del servicio .

Una alternativa, que inicia el trabajo una vez al inicio y un minuto después de que finalice cada ejecución:

[Timer]
OnBootSec=1m
OnUnitInactiveSec=1m 

[Install]
WantedBy=timers.target
Amir
fuente
0

He creado un jar para resolver este problema, ya que se están ejecutando cron duplicados que podrían ser java o shell cron. Simplemente pase el nombre cron en Duplicates.CloseSessions ("Demo.jar"), esto buscará y matará a pid existente para este cron excepto el actual. He implementado un método para hacer esto. Cadena proname = ManagementFactory.getRuntimeMXBean (). GetName (); Cadena pid = proname.split ("@") [0]; System.out.println ("PID actual:" + 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+" ";    
                }
            }

Y luego mata la cadena killid con nuevamente el comando de shell

Sachin Patil
fuente
No creo que esto realmente esté respondiendo la pregunta.
kasperd
0

La respuesta de @Philip Reynolds comenzará a ejecutar el código después del tiempo de espera de 5 segundos de todos modos sin obtener el bloqueo. Seguir a Flock no parece estar funcionando . Modifiqué la respuesta de @Philip Reynolds a

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

para que el código nunca se ejecute simultáneamente. En cambio, después de 5 segundos de espera, el proceso saldrá con 1 si no obtuvo el bloqueo para entonces.

usuario__42
fuente