WordPress

¿Cómo funciona el cron de WordPress? Analizamos su código línea a línea

Helen Kayda - pexels.com

wp_cron.php al detalle

Cuando programamos una noticia, encargamos a un plugin que nos haga una copia de seguridad cada noche, a un antivirus que vigile nuestros archivos, o un tema nos avisa de que hay una actualización pendiente, se han ejecutado tareas de las que no hemos sido conscientes y eso, pasa cada segundo en un servidor.

Al igual que en nuestro cuerpo, es el bulbo raquídeo el que controla los latidos del corazón, la respiración o la presión arterial sin ser nosotros conscientes, en un servidor, esa tarea la realizan las tareas programadas o cron.

En el artículo ¿Cansado de las tareas repetitivas? ¿Qué es cron?, tratábamos al detalle este tema y, una vez conocemos la importancia de estos procesos, y el uso que podemos darle, llega el momento de entender exactamente cómo funciona el cron de WordPress, llamado wp_cron.php

 

Localizado en la raíz de la instalación de WordPress, el código del archivo es el siguiente:

<?php
/**
 * WordPress Cron Implementation for hosts, which do not offer CRON or for which
 * the user has not set up a CRON job pointing to this file.
 *
 * The HTTP request to this file will not slow down the visitor who happens to
 * visit when the cron job is needed to run.
 *
 * @package WordPress
 */

ignore_user_abort(true);

if ( !empty($_POST) || defined('DOING_AJAX') || defined('DOING_CRON') )
  die();

/**
 * Tell WordPress we are doing the CRON task.
 *
 * @var bool
 */
define('DOING_CRON', true);

if ( !defined('ABSPATH') ) {
  /** Set up WordPress environment */
  require_once( dirname( __FILE__ ) . '/wp-load.php' );
}

/**
 * Retrieves the cron lock.
 *
 * Returns the uncached `doing_cron` transient.
 *
 * @ignore
 * @since 3.3.0
 *
 * @return string|false Value of the `doing_cron` transient, 0|false otherwise.
 */
function _get_cron_lock() {
  global $wpdb;

  $value = 0;
  if ( wp_using_ext_object_cache() ) {
    /*
     * Skip local cache and force re-fetch of doing_cron transient
     * in case another process updated the cache.
     */
    $value = wp_cache_get( 'doing_cron', 'transient', true );
  } else {
    $row = $wpdb->get_row( $wpdb->prepare( "SELECT option_value FROM $wpdb->options WHERE option_name = %s LIMIT 1", '_transient_doing_cron' ) );
    if ( is_object( $row ) )
      $value = $row->option_value;
  }

  return $value;
}

if ( false === $crons = _get_cron_array() )
  die();

$keys = array_keys( $crons );
$gmt_time = microtime( true );

if ( isset($keys[0]) && $keys[0] > $gmt_time )
  die();


// The cron lock: a unix timestamp from when the cron was spawned.
$doing_cron_transient = get_transient( 'doing_cron' );

// Use global $doing_wp_cron lock otherwise use the GET lock. If no lock, trying grabbing a new lock.
if ( empty( $doing_wp_cron ) ) {
  if ( empty( $_GET[ 'doing_wp_cron' ] ) ) {
    // Called from external script/job. Try setting a lock.
    if ( $doing_cron_transient && ( $doing_cron_transient + WP_CRON_LOCK_TIMEOUT > $gmt_time ) )
      return;
    $doing_cron_transient = $doing_wp_cron = sprintf( '%.22F', microtime( true ) );
    set_transient( 'doing_cron', $doing_wp_cron );
  } else {
    $doing_wp_cron = $_GET[ 'doing_wp_cron' ];
  }
}

/*
 * The cron lock (a unix timestamp set when the cron was spawned),
 * must match $doing_wp_cron (the "key").
 */
if ( $doing_cron_transient != $doing_wp_cron )
  return;

foreach ( $crons as $timestamp => $cronhooks ) {
  if ( $timestamp > $gmt_time )
    break;

  foreach ( $cronhooks as $hook => $keys ) {

    foreach ( $keys as $k => $v ) {

      $schedule = $v['schedule'];

      if ( $schedule != false ) {
        $new_args = array($timestamp, $schedule, $hook, $v['args']);
        call_user_func_array('wp_reschedule_event', $new_args);
      }

      wp_unschedule_event( $timestamp, $hook, $v['args'] );

      /**
       * Fires scheduled events.
       *
       * @ignore
       * @since 2.1.0
       *
       * @param string $hook Name of the hook that was scheduled to be fired.
       * @param array  $args The arguments to be passed to the hook.
       */
 			do_action_ref_array( $hook, $v['args'] );

      // If the hook ran too long and another cron process stole the lock, quit.
      if ( f_get_cron_lock() != $doing_wp_cron )
        return;
    }
  }
}

if ( _get_cron_lock() == $doing_wp_cron )
  delete_transient( 'doing_cron' );

die();

 

El archivo comienza con unas líneas comentadas que justifican la existencia del mismo:

/**
 * WordPress Cron Implementation for hosts, which do not offer CRON or for which
 * the user has not set up a CRON job pointing to this file.
 *
 * The HTTP request to this file will not slow down the visitor who happens to
 * visit when the cron job is needed to run.
 *
 * @package WordPress
 */

Estas líneas avisan de que existe la posibilidad de crear un cron real desde el servidor, y de que la ejecución en segundo plano del script o «demonio«, no relentizará la carga de la web.

Crear un cron real que refuerce la función este este cron virtual, no es un tema baladí. Pues previene errores frecuentes como el de «programación perdida«.

 

A continuación, arranca el código propiamente dicho:

ignore_user_abort(true);

La primera función que se nos presenta, establece que la desconexión de un usuario no interrumpa la ejecución del script.

Esta línea es realmente importante, ya que el cron de WordPress, se ejecuta únicamente cuando un usuario accede a nuestra web. De ahí que reciba la consideración de cron virtual, en lugar de cron real o de servidor.

La función en php se define como:

int ignore_user_abort ([ bool $value ] )

Siendo su value TRUE, tendrá el efecto que hemos mencionado. Y con FALSE, el contrario.

Más información sobre la función:
http://php.net/manual/es/function.ignore-user-abort.php

 

A continuación, un condicional preventivo:

if ( !empty($_POST) || defined('DOING_AJAX') || defined('DOING_CRON') )
  die();

La función die() es equivalente a exit() es decir, provoca la interrupción de la ejecución del script actual.

Por lo tanto, el condicional dice que si se cumple alguna de las tres condiciones, la ejecución del script debe finalizar.

O más detalladamente, si:

  • !empty($_POST) No se ha recibido nada a través de un formulario o
  • defined(‘DOING_AJAX’) Existe una constante llamada “DOING_AJAX” (No confundir constante con variable) o
  • defined(‘DOING_CRON’) ) Existe una constante llamada “DOING_CRON”

Finaliza el script. En caso contrario, continúa la lectura de código.

Más información sobre los conceptos tratados:
http://php.net/manual/es/function.die.php
http://php.net/manual/es/reserved.variables.post.php
http://php.net/manual/es/function.defined.php

 

Si continúa la ejecución del script, es porque la constante “DOING_CRON” no existía, pues en caso contrario, en el paso anterior se habría detenido el script [die()].

Por lo tanto, ahora la creamos:

define('DOING_CRON', true);

 

 

Tras ello se comprueba si se ha definido la constante “ABSPATH”, que es la que guarda la url absoluta donde se aloja WordPress. Por ejemplo: https://www.loopeando.com/portal/

if ( !defined('ABSPATH') ) {
  /** Set up WordPress environment */
  require_once( dirname( __FILE__ ) . '/wp-load.php' );
}

En caso de que no se haya definido, llama al archivo “wp-load.php” que, entre otras funciones, tiene la que define la constante “ABSPATH”.

¿Y cómo define dicha constante?

Así:

define( ‘ABSPATH’, dirname( __FILE__ ) . ‘/’ );

define() es una función diferente de defined().
Como hemos comentado, defined() comprueba si dicha constante existe pero, define() le asigna valor y la crea.

Por lo tanto, esa línea asigna el valor “dirname( __FILE__ )” a la constante “ABSPATH”. Y dicho valor es la url absoluta de la instalación de WordPress.

 

Una vez comprobadas y creadas las constantes, empieza el meollo:

function _get_cron_lock() {
  global $wpdb;

  $value = 0;
  if ( wp_using_ext_object_cache() ) {
    /*
     * Skip local cache and force re-fetch of doing_cron transient
     * in case another process updated the cache.
     */
    $value = wp_cache_get( 'doing_cron', 'transient', true );
  } else {
    $row = $wpdb->get_row( $wpdb->prepare( "SELECT option_value FROM $wpdb->options WHERE option_name = %s LIMIT 1", '_transient_doing_cron' ) );
    if ( is_object( $row ) )
      $value = $row->option_value;
  }

  return $value;
}

 

Crea una función llamada “_get_cron_lock”. Dentro de la cual crea una variable global (“wpdb”) e inicializa otra: $value = 0;

Y con un condicional comprueba si se está utilizando memoria caché de objetos:

if ( wp_using_ext_object_cache() ) {

En caso de que así sea, llama a la función “wp_cache_get”, cuya estructura es:

<?php wp_cache_get( $key, $group, $force, $found ); ?>

Como está reflejada como:

$value = wp_cache_get( 'doing_cron', 'transient', true );

 

En este caso, cogería la caché de ‘doing_cron’, que son los objetos destinados a ser ejecutados en segundo plano.

Si no hubiese estado definida la función para comprobar si se está utilizando la memoria caché, se ejecutaría el “else”:

} else {
    $row = $wpdb->get_row( $wpdb->prepare( "SELECT option_value FROM $wpdb->options WHERE option_name = %s LIMIT 1", '_transient_doing_cron' ) );
    if ( is_object( $row ) )
      $value = $row->option_value;
  }

  return $value;
}

 

Que a través de una consulta a la base de datos, extraería esa información.

Así que en definitiva, esa función comprueba que las acciones a realizar por cron estén asignadas a la caché, y si no lo están, las carga de la base de datos.

¿Por qué lo hace así? Porque si por defecto cogiese esa información directamente desde la base de datos, la ejecución continua de las consultas, relentizaría la carga de la web.

if ( false === $crons = _get_cron_array() )
  die();

$keys = array_keys( $crons );
$gmt_time = microtime( true );

if ( isset($keys[0]) && $keys[0] > $gmt_time )
  die();

Como lo que obtiene en realidad es un array, separa  sus componentes asignándolos a la variable «key«. Siendo $keys[0] el primer resultado del array. $keys[1] el segundo, etc

$gmt_time = microtime( true );

«microtime» es una función php que devuelve la fecha actual y, con ese dato, compara si la primera tarea que hay en cola en el cron [$keys[0]], tiene una fecha mayor que la actual. En cuyo caso, finaliza la ejecución del script.

if ( isset($keys[0]) && $keys[0] > $gmt_time )
  die();

Si la fecha de la primera tarea cron hubiese sido menor que la hora actual, seguiría ejecutándose el script.

Más información sobre «microtime«:
http://php.net/manual/es/function.microtime.php

 

A continuación ejecuta un seguro:

// The cron lock: a unix timestamp from when the cron was spawned.
$doing_cron_transient = get_transient( 'doing_cron' );

«get_transient«es una función de WordPress que comprueba si la función a la que referencia (‘doing_cron’) existe o tiene un valor asignado. En caso contrario, devuelve un booleano (false).

 

// Use global $doing_wp_cron lock otherwise use the GET lock. If no lock, trying grabbing a new lock.
if ( empty( $doing_wp_cron ) ) {
  if ( empty( $_GET[ 'doing_wp_cron' ] ) ) {
    // Called from external script/job. Try setting a lock.
    if ( $doing_cron_transient && ( $doing_cron_transient + WP_CRON_LOCK_TIMEOUT > $gmt_time ) )
      return;
    $doing_cron_transient = $doing_wp_cron = sprintf( '%.22F', microtime( true ) );
    set_transient( 'doing_cron', $doing_wp_cron );
  } else {
    $doing_wp_cron = $_GET[ 'doing_wp_cron' ];
  }
}

Finalmente, verifica que la variable ($doing_wp_cron) esté vacía y que no se le esté pasando nada por GET: [$_GET[ ‘doing_wp_cron’ ]]. O lo que es lo mismo, que no se esté ejecutando ninguna tarea ahora mismo.

Tras ello comprueba que si hay algo previsto en ejecución para ahora mismo o para dentro de 1 minuto:

if ( $doing_cron_transient && ( $doing_cron_transient + WP_CRON_LOCK_TIMEOUT > $gmt_time ) )
return;

¿Por qué sabemos que un minuto? Porque WP_CRON_LOCK_TIMEOUT se define a su vez como:

define('WP_CRON_LOCK_TIMEOUT', 60);  // In seconds

 

Si hubiese algo previsto, actualiza la hora del cron:

$doing_cron_transient = $doing_wp_cron = sprintf( '%.22F', microtime( true ) );

 

Siendo sprintf() una función en php que da formato a una cadena. En este caso, a la cadena microtime, que es la que marca la hora actual.

El 22F indica la precisión del valor dado por microtime. En este código, 22 decimales.

Y asigna esa hora actual a la constante “doing_cron”, quedando así actualizada a este mismo instante:

set_transient( 'doing_cron', $doing_wp_cron );

 

Es decir, ha comprobado si había algo que debía ser ejecutado (su hora de ejecución era menor que la hora actual) y, si lo había, actualiza la hora del cron a este mismo momento. Pero… ¿Y no ejecuta esa tarea pendiente? Sí, lo hace a continuación:

if ( $doing_cron_transient != $doing_wp_cron )
  return;

foreach ( $crons as $timestamp => $cronhooks ) {
  if ( $timestamp > $gmt_time )
    break;

  foreach ( $cronhooks as $hook => $keys ) {

    foreach ( $keys as $k => $v ) {

      $schedule = $v['schedule'];

      if ( $schedule != false ) {
        $new_args = array($timestamp, $schedule, $hook, $v['args']);
        call_user_func_array('wp_reschedule_event', $new_args);
      }

      wp_unschedule_event( $timestamp, $hook, $v['args'] );

      /**
       * Fires scheduled events.
       *
       * @ignore
       * @since 2.1.0
       *
       * @param string $hook Name of the hook that was scheduled to be fired.
       * @param array  $args The arguments to be passed to the hook.
       */
 			do_action_ref_array( $hook, $v['args'] );

 

A través de un foreach va recorriendo toda la cadena de tareas pendientes:

foreach ( $crons as $timestamp => $cronhooks ) {

Comprobando que su hora de ejecución es menor que la hora actual, y por lo tanto deban ser ejecutados. En caso contrario, salgo de la ejecución (“break”).

if ( $timestamp > $gmt_time )
break;

Y finalmente, ejecuta las tareas:

do_action_ref_array( $hook, $v['args'] );

 

Para finalizar el script, hay un par de seguros más:

if ( _get_cron_lock() != $doing_wp_cron )
return;
}
if ( _get_cron_lock() == $doing_wp_cron )
  delete_transient( 'doing_cron' );
die();

 

Este comprueba si el tiempo de ejecución tarda demasiado. ¿Cómo? Recordemos que la función _get_cron_lock(); asignaba las tareas pendientes de la caché de objetos, y $doing_wp_cron contiene las tareas que se están ejecutando. Si ambas son idénticas (if ( _get_cron_lock() == $doing_wp_cron )), no se están ejecutando las tareas cron, posiblemente por algún fallo en alguna de ellas. Y por lo tanto debo destruir las tareas para evitar que la web se cuelgue por un loop (delete_transient( ‘doing_cron’ );) y detener la ejecución del script (die();)

Si ambas no coinciden (if ( _get_cron_lock() != $doing_wp_cron )) quiere decir que al menos una de las tareas, ya se ha ejecutado y, por lo tanto, devuelvo el resultado: “return”.

 

Explicar un código siempre es una tarea ardua, sin embargo esperamos haber sido lo suficientemente docentes. En caso contrario, ¡no dudes en hacernos llegar tus dudas a través de los comentarios!

Cristian Sarabia Martínez

Desde que a principios de los 90 mi padre desempolvó su Spectrum, no he dejado de probar y experimentar con la tecnología.

Enamorado del mundo web, Full Stack Developer de profesión y diseñador por devoción.

Ahora hago mis pinitos en esto del blogging para compartir con vosotros un poquito de todo lo que la comunidad me ha dado.

Escribir comentario

Haz clic aquí para dejar tu comentario