Skip to content

Commit

Permalink
- Patch #331611 by sun, joshmiller, TheRec, Rob Loach, Damien Tournou…
Browse files Browse the repository at this point in the history
…d: add a poormanscron-like feature to core.
  • Loading branch information
dries committed Aug 22, 2009
1 parent 838e41f commit 8a0dfd4
Show file tree
Hide file tree
Showing 5 changed files with 204 additions and 12 deletions.
4 changes: 3 additions & 1 deletion CHANGELOG.txt
@@ -1,4 +1,4 @@
// $Id: CHANGELOG.txt,v 1.329 2009/08/19 22:46:05 webchick Exp $
// $Id: CHANGELOG.txt,v 1.330 2009/08/22 16:01:09 dries Exp $

Drupal 7.0, xxxx-xx-xx (development version)
----------------------
Expand Down Expand Up @@ -32,6 +32,8 @@ Drupal 7.0, xxxx-xx-xx (development version)
now be achieved with Field API.
* Added additional features to the default install profile, and implemented
a "slimmed down" install profile designed for developers.
* Added a built-in, automated cron run feature, which is triggered by site
visitors.
* Added an administrator role which is assigned all permisions for
installed modules automatically.
* Image toolkits are now provided by modules (rather than requiring a manual
Expand Down
17 changes: 13 additions & 4 deletions INSTALL.txt
@@ -1,4 +1,4 @@
// $Id: INSTALL.txt,v 1.74 2009/08/17 19:14:39 webchick Exp $
// $Id: INSTALL.txt,v 1.75 2009/08/22 16:01:10 dries Exp $

CONTENTS OF THIS FILE
---------------------
Expand Down Expand Up @@ -231,9 +231,18 @@ INSTALLATION
maintenance task, including search module (to build and update the index
used for keyword searching), aggregator module (to retrieve feeds from other
sites), and system module (to perform routine maintenance and pruning on
system tables). To activate these tasks, visit the page "cron.php", which
executes maintenance tasks on behalf of installed modules. The URL of the
cron.php page requires a "cron key" to protect against unauthorized access.
system tables).

For most sites, the built-in, automated cron feature should be sufficient.
Note, however, that cron tasks will only be executed when there are site
visitors. You can enable the built-in cron feature at:

Administer > Configuration and modules > Development > Maintenance mode

Advanced users may want to ensure that cron tasks are executed periodically.
To do this, visit the page "cron.php", which executes maintenance tasks on
behalf of installed modules. The URL of the cron.php page requires a "cron
key" to protect against unauthorized access.
Each cron key is automatically generated during installation and is specific
to your site. The full URL of the page, with cron key, is available in the
"Cron maintenance tasks" section of the "Status report page" at:
Expand Down
23 changes: 22 additions & 1 deletion modules/system/system.admin.inc
@@ -1,5 +1,5 @@
<?php
// $Id: system.admin.inc,v 1.185 2009/08/22 14:34:22 webchick Exp $
// $Id: system.admin.inc,v 1.186 2009/08/22 16:01:10 dries Exp $

/**
* @file
Expand Down Expand Up @@ -1313,7 +1313,16 @@ function system_site_information_settings() {
'#description' => t('Render all blocks on the default 404 (not found) page. Disabling blocks can help with performance but might leave users with a less functional site.'),
'#default_value' => variable_get('site_404_blocks', 0)
);
$form['cron_safe_threshold'] = array(
'#type' => 'select',
'#title' => t('Automatically run cron'),
'#default_value' => variable_get('cron_safe_threshold', DRUPAL_CRON_DEFAULT_THRESHOLD),
'#options' => array(0 => t('Never')) + drupal_map_assoc(array(3600, 10800, 21600, 43200, 86400, 604800), 'format_interval'),
'#description' => t('When enabled, the site will check whether cron has been run in the configured interval and automatically run it upon the next page request. For more information visit the <a href="@status-report-url">status report page</a>.', array('@status-report-url' => url('admin/reports/status'))),
);

$form['#validate'][] = 'system_site_information_settings_validate';
$form['#submit'][] = 'system_site_information_settings_submit';

return system_settings_form($form);
}
Expand All @@ -1333,6 +1342,18 @@ function system_site_information_settings_validate($form, &$form_state) {
}
}

/**
* Form submit handler for the site-information form.
*/
function system_site_information_settings_submit($form, &$form_state) {
// Clear caches when the cron threshold is changed to ensure that the cron
// image is not contained in cached pages.
$cron_threshold = variable_get('cron_safe_threshold', DRUPAL_CRON_DEFAULT_THRESHOLD);
if (($cron_threshold > 0 && $form_state['input']['cron_safe_threshold'] == 0) || ($cron_threshold == 0 && $form_state['input']['cron_safe_threshold'] > 0)) {
cache_clear_all();
}
}

/**
* Form builder; Configure error reporting settings.
*
Expand Down
117 changes: 116 additions & 1 deletion modules/system/system.module
@@ -1,5 +1,5 @@
<?php
// $Id: system.module,v 1.760 2009/08/22 14:34:22 webchick Exp $
// $Id: system.module,v 1.761 2009/08/22 16:01:10 dries Exp $

/**
* @file
Expand Down Expand Up @@ -41,6 +41,11 @@ define('DRUPAL_MINIMUM_PGSQL', '8.3');
*/
define('DRUPAL_MAXIMUM_TEMP_FILE_AGE', 21600);

/**
* Default interval for automatic cron executions in seconds.
*/
define('DRUPAL_CRON_DEFAULT_THRESHOLD', 10800);

/**
* New users will be set to the default time zone at registration.
*/
Expand Down Expand Up @@ -196,6 +201,9 @@ function system_theme() {
'arguments' => array('version' => NULL),
),
'system_compact_link' => array(),
'system_run_cron_image' => array(
'arguments' => array('image_path' => NULL),
),
));
}

Expand Down Expand Up @@ -496,6 +504,12 @@ function system_menu() {
'access callback' => TRUE,
'type' => MENU_CALLBACK,
);
$items['system/run-cron-image'] = array(
'title' => 'Execute cron',
'page callback' => 'system_run_cron_image',
'access callback' => 'system_run_cron_image_access',
'type' => MENU_CALLBACK,
);
$items['admin'] = array(
'title' => 'Administer',
'access arguments' => array('access administration pages'),
Expand Down Expand Up @@ -2968,3 +2982,104 @@ function system_retrieve_file($url, $destination = NULL, $overwrite = TRUE) {

return $local;
}

/**
* Implement hook_page_alter().
*/
function system_page_alter(&$page) {
// Automatic cron runs.
// @see system_run_cron_image()
if (system_run_cron_image_access()) {
$page['page_bottom']['run_cron'] = array(
// Trigger cron run via AJAX.
'#attached_js' => array(
'(function($){ $.get(' . drupal_to_js(url('system/run-cron-image')) . '); })(jQuery);' => array('type' => 'inline', 'scope' => 'header'),
),
// Trigger cron run for clients not supporting JavaScript (fall-back).
'#markup' => theme('system_run_cron_image', 'system/run-cron-image'),
);
}
}

/**
* Menu callback; executes cron via an image callback.
*
* This callback runs cron in a separate HTTP request to prevent "mysterious"
* slow-downs of regular HTTP requests. It is either invoked via an AJAX request
* (if the client's browser supports JavaScript) or by an IMG tag directly in
* the page output (for clients not supporting JavaScript). For the latter case,
* we need to output a transparent 1x1 image, so the browser does not render the
* image's alternate text or a 'missing image placeholder'. The AJAX request
* does not process the returned output.
*
* @see system_page_alter()
* @see theme_system_run_cron_image()
* @see system_run_cron_image_access()
*/
function system_run_cron_image() {
drupal_page_is_cacheable(FALSE);

// Output a transparent 1x1 image to the browser; required for clients not
// supporting JavaScript.
drupal_set_header('Content-Type', 'image/gif');
echo "\x47\x49\x46\x38\x39\x61\x1\x0\x1\x0\x80\xff\x0\xc0\xc0\xc0\x0\x0\x0\x21\xf9\x4\x1\x0\x0\x0\x0\x2c\x0\x0\x0\x0\x1\x0\x1\x0\x0\x2\x2\x44\x1\x0\x3b";

// Cron threshold semaphore is used to avoid errors every time the image
// callback is displayed when a previous cron is still running.
$threshold_semaphore = variable_get('cron_threshold_semaphore', FALSE);
if ($threshold_semaphore) {
if (REQUEST_TIME - $threshold_semaphore > 3600) {
// Either cron has been running for more than an hour or the semaphore
// was not reset due to a database error.
watchdog('cron', 'Cron has been running for more than an hour and is most likely stuck.', array(), WATCHDOG_ERROR);

// Release the cron threshold semaphore.
variable_del('cron_threshold_semaphore');
}
}
else {
// Run cron automatically if it has never run or threshold was crossed.
$cron_last = variable_get('cron_last', NULL);
$cron_threshold = variable_get('cron_safe_threshold', DRUPAL_CRON_DEFAULT_THRESHOLD);
if (!isset($cron_last) || (REQUEST_TIME - $cron_last > $cron_threshold)) {
// Lock cron threshold semaphore.
variable_set('cron_threshold_semaphore', REQUEST_TIME);
drupal_cron_run();
// Release the cron threshold semaphore.
variable_del('cron_threshold_semaphore');
}
}

exit;
}

/**
* Checks if the feature to automatically run cron is enabled.
*
* Also used as a menu access callback for this feature.
*
* @return
* TRUE if cron threshold is enabled, FALSE otherwise.
*
* @see system_run_cron_image()
* @see system_page_alter()
*/
function system_run_cron_image_access() {
return variable_get('cron_safe_threshold', DRUPAL_CRON_DEFAULT_THRESHOLD) > 0;
}

/**
* Display image used to run cron automatically.
*
* Renders an image pointing to the automatic cron run menu callback for
* graceful degradation when Javascript is not available. The wrapping NOSCRIPT
* tag ensures that only browsers not supporting JavaScript render the image.
*
* @see system_page_alter()
* @see system_run_cron_image()
* @ingroup themeable
*/
function theme_system_run_cron_image($image_path) {
return '<noscript><div id="system-cron-image">' . theme('image', $image_path, '', '', array(), FALSE) . '</div></noscript>';
}

55 changes: 50 additions & 5 deletions modules/system/system.test
@@ -1,5 +1,5 @@
<?php
// $Id: system.test,v 1.68 2009/08/22 09:44:56 dries Exp $
// $Id: system.test,v 1.69 2009/08/22 16:01:10 dries Exp $

/**
* Helper class for module test cases.
Expand Down Expand Up @@ -372,6 +372,7 @@ class CronRunTestCase extends DrupalWebTestCase {
*/
function testCronRun() {
global $base_url;

// Run cron anonymously without any cron key.
$this->drupalGet($base_url . '/cron.php', array('external' => TRUE));
$this->assertResponse(403);
Expand All @@ -390,14 +391,58 @@ class CronRunTestCase extends DrupalWebTestCase {
$this->assertTrue(drupal_cron_run(), t('Cron ran successfully.'));
}

/**
* Follow every image paths in the previously retrieved content.
*/
function drupalGetAllImages() {
foreach ($this->xpath('//img') as $image) {
$this->drupalGet($this->getAbsoluteUrl($image['src']));
}
}

/**
* Ensure that the cron image callback to run it automatically is working.
*
* In these tests we do not use REQUEST_TIME to track start time, because we
* need the exact time when cron is triggered.
*/
function testCronThreshold() {
// Ensure cron does not run when the cron threshold is enabled and was
// not passed.
$start_cron_last = time();
variable_set('cron_last', $start_cron_last);
variable_set('cron_safe_threshold', 10);
$this->drupalGet('');
// Follow every image path on the page.
$this->drupalGetAllImages();
$this->assertTrue($start_cron_last == variable_get('cron_last', NULL), t('Cron does not run when the cron threshold is not passed.'));

// Test if cron runs when the cron threshold was passed.
$start_cron_last = time() - 15;
variable_set('cron_last', $start_cron_last);
$this->drupalGet('');
// Follow every image path on the page.
$this->drupalGetAllImages();
$this->assertTrue(variable_get('cron_last', NULL) > $start_cron_last, t('Cron runs when the cron threshold is passed.'));

// Test if cron does not run when the cron threshold was is disabled.
$start_cron_last = time() - 15;
variable_set('cron_safe_threshold', 0);
variable_set('cron_last', $start_cron_last);
$this->drupalGet('');
// Follow every image path on the page.
$this->drupalGetAllImages();
$this->assertTrue($start_cron_last == variable_get('cron_last', NULL), t('Cron does not run when the cron threshold is disabled.'));
}

/**
* Ensure that temporary files are removed.
*
* Create files for all the possible combinations of age and status. We are
* using UPDATE statments rather than file_save() because it would set the
* timestamp.
*/
function testTempFileCleanup() {
// Create files for all the possible combinations of age and status. We're
// using UPDATE statments rather than file_save() because it would set the
// timestamp.

// Temporary file that is older than DRUPAL_MAXIMUM_TEMP_FILE_AGE.
$temp_old = file_save_data('');
db_update('file')
Expand Down

0 comments on commit 8a0dfd4

Please sign in to comment.