Commit bb393a68 authored by Erick Hitter's avatar Erick Hitter Committed by GitHub

Merge pull request #145 from Automattic/add/internal-events-constant

Allow additional internal events via a constant
parents f98f15fa 70c2a4c1
composer.lock
vendor
.DS_Store
Thumbs.db
wp-cli.local.yml
node_modules/
*.sql
*.tar.gz
*.zip
......@@ -27,10 +27,6 @@ matrix:
env: WP_VERSION=latest
- php: 7.0
env: WP_VERSION=trunk
- php: 5.6
env: WP_VERSION=latest
- php: 5.6
env: WP_VERSION=trunk
# PHPCS
- php: 7.1
env: WP_TRAVISCI=phpcs
......
module.exports = function( grunt ) {
'use strict';
var banner = '/**\n * <%= pkg.homepage %>\n * Copyright (c) <%= grunt.template.today("yyyy") %>\n * This file is generated automatically. Do not edit.\n */\n';
// Project configuration
grunt.initConfig( {
pkg: grunt.file.readJSON( 'package.json' ),
addtextdomain: {
options: {
textdomain: 'automattic-cron-control',
},
update_all_domains: {
options: {
updateDomains: true
},
src: [ '*.php', '**/*.php', '!node_modules/**', '!php-tests/**', '!bin/**' ]
}
},
wp_readme_to_markdown: {
your_target: {
files: {
'README.md': 'readme.txt'
}
},
},
makepot: {
target: {
options: {
domainPath: '/languages',
mainFile: 'cron-control.php',
potFilename: 'cron-control.pot',
potHeaders: {
poedit: true,
'x-poedit-keywordslist': true
},
type: 'wp-plugin',
updateTimestamp: true
}
}
},
} );
grunt.loadNpmTasks( 'grunt-wp-i18n' );
grunt.loadNpmTasks( 'grunt-wp-readme-to-markdown' );
grunt.registerTask( 'i18n', ['addtextdomain', 'makepot'] );
grunt.registerTask( 'readme', ['wp_readme_to_markdown'] );
grunt.util.linefeed = '\n';
};
Cron Control
============
# Cron Control #
**Contributors:** automattic, ethitter
**Tags:** cron, cron control, concurrency, parallel, async
**Requires at least:** 4.4
**Tested up to:** 4.9
**Requires PHP:** 7.0
**Stable tag:** 0.1.0
**License:** GPLv2 or later
**License URI:** http://www.gnu.org/licenses/gpl-2.0.html
Execute WordPress cron events in parallel, using a custom post type for event storage.
Execute WordPress cron events in parallel, with custom event storage for high-volume cron.
Using REST API endpoints (requires WordPress 4.4+), an event queue is produced and events are triggered.
## Description ##
## PHP Compatibility
Execute WordPress cron events in parallel, with custom event storage for high-volume cron.
Cron Control requires PHP 7 or greater to be able to catch fatal errors triggered by event callbacks. While the plugin may work with previous versions of PHP, internal locks may become deadlocked if callbacks fail.
Using REST API endpoints (requires WordPress 4.4+), or a Golang daemon, an event queue is produced and events are triggered.
## Event Concurrency
## Installation ##
1. Define `WP_CRON_CONTROL_SECRET` in `wp-config.php`
1. Upload the `cron-control` directory to the `/wp-content/mu-plugins/` directory
1. Create a file at `/wp-content/mu-plugins/cron-control.php` to load `/wp-content/mu-plugins/cron-control/cron-control.php`
## Frequently Asked Questions ##
### Why is PHP 7 required? ###
To be able to catch fatal errors triggered by event callbacks, and define arrays in constants (such as for adding "Internal Events"), PHP 7 is necessary.
### Adding Internal Events ###
**This should be done sparingly as "Internal Events" bypass certain locks and limits built into the plugin.** Overuse will lead to unexpected resource usage, and likely resource exhaustion.
In `wp-config.php` or a similarly-early and appropriate place, define `CRON_CONTROL_ADDITIONAL_INTERNAL_EVENTS` as an array of arrays like:
```
define( 'CRON_CONTROL_ADDITIONAL_INTERNAL_EVENTS', array(
array(
'schedule' => 'hourly',
'action' => 'do_a_thing',
'callback' => '__return_true',
),
) );
```
Due to the early loading (to limit additions), the `action` and `callback` generally can't directly reference any Core, plugin, or theme code. Since WordPress uses actions to trigger cron, class methods can be referenced, so long as the class name is not dynamically referenced. For example:
```
define( 'CRON_CONTROL_ADDITIONAL_INTERNAL_EVENTS', array(
array(
'schedule' => 'hourly',
'action' => 'do_a_thing',
'callback' => array( 'Some_Class', 'some_method' ),
),
) );
```
Take care to reference the full namespace when appropriate.
### Increasing Event Concurrency ###
In some circumstances, multiple events with the same action can safely run in parallel. This is usually not the case, largely due to Core's alloptions, but sometimes an event is written in a way that we can support concurrent executions.
To allow concurrency for your event, and to specify the level of concurrency, please hook the `a8c_cron_control_concurrent_event_whitelist` filter as in the following example:
``` php
```
add_filter( 'a8c_cron_control_concurrent_event_whitelist', function( $wh ) {
$wh['my_custom_event'] = 2;
$wh['my_custom_event'] = 2;
return $wh;
return $wh;
} );
```
## Changelog ##
### 1.5 ###
* Convert from custom post type to custom table with proper indices
### 1.0 ###
* Initial release
{
"name": "automattic/cron-control",
"description": "Execute WordPress cron events in parallel, with custom event storage for high-volume cron.",
"license": "GPL-2.0",
"require": {
"php": ">=7.0.0"
},
"require-dev": {
"wp-cli/wp-cli": "*"
}
}
......@@ -20,7 +20,7 @@ class Internal_Events extends Singleton {
*
* @var array
*/
private $internal_jobs = array();
private $internal_events = array();
/**
* Schedules for internal events
......@@ -29,37 +29,83 @@ class Internal_Events extends Singleton {
*
* @var array
*/
private $internal_jobs_schedules = array();
private $internal_events_schedules = array();
/**
* Register hooks
*/
protected function class_init() {
// Internal jobs variables.
$this->internal_jobs = array(
// Prepare events and their schedules, allowing for additions.
$this->prepare_internal_events();
$this->prepare_internal_events_schedules();
// Register hooks.
if ( defined( 'WP_CLI' ) && \WP_CLI ) {
add_action( 'wp_loaded', array( $this, 'schedule_internal_events' ) );
} else {
add_action( 'admin_init', array( $this, 'schedule_internal_events' ) );
add_action( 'rest_api_init', array( $this, 'schedule_internal_events' ) );
}
add_filter( 'cron_schedules', array( $this, 'register_internal_events_schedules' ) );
foreach ( $this->internal_events as $internal_event ) {
add_action( $internal_event['action'], $internal_event['callback'] );
}
}
/**
* Populate internal events, allowing for additions
*/
private function prepare_internal_events() {
$internal_events = array(
array(
'schedule' => 'a8c_cron_control_minute',
'action' => 'a8c_cron_control_force_publish_missed_schedules',
'callback' => 'force_publish_missed_schedules',
'callback' => array( $this, 'force_publish_missed_schedules' ),
),
array(
'schedule' => 'a8c_cron_control_ten_minutes',
'action' => 'a8c_cron_control_confirm_scheduled_posts',
'callback' => 'confirm_scheduled_posts',
'callback' => array( $this, 'confirm_scheduled_posts' ),
),
array(
'schedule' => 'daily',
'action' => 'a8c_cron_control_clean_legacy_data',
'callback' => 'clean_legacy_data',
'callback' => array( $this, 'clean_legacy_data' ),
),
array(
'schedule' => 'hourly',
'action' => 'a8c_cron_control_purge_completed_events',
'callback' => 'purge_completed_events',
'callback' => array( $this, 'purge_completed_events' ),
),
);
$this->internal_jobs_schedules = array(
// Allow additional internal events to be specified, ensuring the above cannot be overwritten.
if ( defined( 'CRON_CONTROL_ADDITIONAL_INTERNAL_EVENTS' ) && is_array( \CRON_CONTROL_ADDITIONAL_INTERNAL_EVENTS ) ) {
$internal_actions = wp_list_pluck( $internal_events, 'action' );
foreach ( \CRON_CONTROL_ADDITIONAL_INTERNAL_EVENTS as $additional ) {
if ( in_array( $additional['action'], $internal_actions, true ) ) {
continue;
}
if ( ! array_key_exists( 'schedule', $additional ) || ! array_key_exists( 'action', $additional ) || ! array_key_exists( 'callback', $additional ) ) {
continue;
}
$internal_events[] = $additional;
}
}
$this->internal_events = $internal_events;
}
/**
* Allow custom internal events to provide their own schedules
*/
private function prepare_internal_events_schedules() {
$internal_events_schedules = array(
'a8c_cron_control_minute' => array(
'interval' => 1 * MINUTE_IN_SECONDS,
'display' => __( 'Cron Control internal job - every minute', 'automattic-cron-control' ),
......@@ -70,45 +116,53 @@ class Internal_Events extends Singleton {
),
);
// Register hooks.
add_action( 'admin_init', array( $this, 'schedule_internal_events' ) );
add_action( 'rest_api_init', array( $this, 'schedule_internal_events' ) );
add_filter( 'cron_schedules', array( $this, 'register_internal_events_schedules' ) );
// Allow additional schedules for custom events, ensuring the above cannot be overwritten.
if ( defined( 'CRON_CONTROL_ADDITIONAL_INTERNAL_EVENTS_SCHEDULES' ) && is_array( \CRON_CONTROL_ADDITIONAL_INTERNAL_EVENTS_SCHEDULES ) ) {
foreach ( \CRON_CONTROL_ADDITIONAL_INTERNAL_EVENTS_SCHEDULES as $name => $attrs ) {
if ( array_key_exists( $name, $internal_events_schedules ) ) {
continue;
}
foreach ( $this->internal_jobs as $internal_job ) {
add_action( $internal_job['action'], array( $this, $internal_job['callback'] ) );
if ( ! array_key_exists( 'interval', $attrs ) || ! array_key_exists( 'display', $attrs ) ) {
continue;
}
$internal_events_schedules[ $name ] = $attrs;
}
}
$this->internal_events_schedules = $internal_events_schedules;
}
/**
* Include custom schedules used for internal jobs
* Include custom schedules used for internal events
*
* @param array $schedules List of registered event intervals.
* @return array
*/
public function register_internal_events_schedules( $schedules ) {
return array_merge( $schedules, $this->internal_jobs_schedules );
return array_merge( $schedules, $this->internal_events_schedules );
}
/**
* Schedule internal jobs
* Schedule internal events
*/
public function schedule_internal_events() {
$when = strtotime( sprintf( '+%d seconds', JOB_QUEUE_WINDOW_IN_SECONDS ) );
$schedules = wp_get_schedules();
foreach ( $this->internal_jobs as $job_args ) {
if ( ! wp_next_scheduled( $job_args['action'] ) ) {
$interval = array_key_exists( $job_args['schedule'], $schedules ) ? $schedules[ $job_args['schedule'] ]['interval'] : 0;
foreach ( $this->internal_events as $event_args ) {
if ( ! wp_next_scheduled( $event_args['action'] ) ) {
$interval = array_key_exists( $event_args['schedule'], $schedules ) ? $schedules[ $event_args['schedule'] ]['interval'] : 0;
$args = array(
'schedule' => $job_args['schedule'],
'schedule' => $event_args['schedule'],
'args' => array(),
'interval' => $interval,
);
schedule_event( $when, $job_args['action'], $args );
schedule_event( $when, $event_args['action'], $args );
}
}
}
......@@ -124,7 +178,7 @@ class Internal_Events extends Singleton {
* @return bool
*/
public function is_internal_event( $action ) {
return in_array( $action, wp_list_pluck( $this->internal_jobs, 'action' ), true );
return in_array( $action, wp_list_pluck( $this->internal_events, 'action' ), true );
}
/**
......@@ -207,32 +261,32 @@ class Internal_Events extends Singleton {
// Confirm internal events are scheduled for when they're expected.
$schedules = wp_get_schedules();
foreach ( $this->internal_jobs as $internal_job ) {
$timestamp = wp_next_scheduled( $internal_job['action'] );
foreach ( $this->internal_events as $internal_event ) {
$timestamp = wp_next_scheduled( $internal_event['action'] );
// Will reschedule on its own.
if ( false === $timestamp ) {
continue;
}
$job_details = get_event_by_attributes( array(
$event_details = get_event_by_attributes( array(
'timestamp' => $timestamp,
'action' => $internal_job['action'],
'action' => $internal_event['action'],
'instance' => md5( maybe_serialize( array() ) ),
) );
if ( $job_details->schedule !== $internal_job['schedule'] ) {
if ( $event_details->schedule !== $internal_event['schedule'] ) {
if ( $timestamp <= time() ) {
$timestamp = time() + ( 1 * \MINUTE_IN_SECONDS );
}
$args = array(
'schedule' => $internal_job['schedule'],
'args' => $job_details->args,
'interval' => $schedules[ $internal_job['schedule'] ]['interval'],
'schedule' => $internal_event['schedule'],
'args' => $event_details->args,
'interval' => $schedules[ $internal_event['schedule'] ]['interval'],
);
schedule_event( $timestamp, $job_details->action, $args, $job_details->ID );
schedule_event( $timestamp, $event_details->action, $args, $event_details->ID );
}
}
}
......
......@@ -144,7 +144,7 @@ class One_Time_Fixers extends \WP_CLI_Command {
}
/* translators: 1: Event count for this batch */
\WP_CLI::log( sprintf( __( 'Found %s items in this batch' ), number_format_i18n( count( $items ) ) ) );
\WP_CLI::log( sprintf( __( 'Found %s items in this batch', 'automattic-cron-control' ), number_format_i18n( count( $items ) ) ) );
foreach ( $items as $item ) {
\WP_CLI::log( "{$item->ID}, `{$item->post_title}`" );
......
# Copyright (C) 2017 Erick Hitter, Automattic
# This file is distributed under the same license as the Cron Control package.
msgid ""
msgstr ""
"Project-Id-Version: Cron Control 1.5\n"
"Report-Msgid-Bugs-To: https://wordpress.org/support/plugin/cron-control\n"
"POT-Creation-Date: 2017-10-03 20:24:01+00:00\n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=utf-8\n"
"Content-Transfer-Encoding: 8bit\n"
"PO-Revision-Date: 2017-MO-DA HO:MI+ZONE\n"
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
"Language-Team: LANGUAGE <LL@li.org>\n"
"X-Generator: grunt-wp-i18n 0.5.4\n"
"X-Poedit-KeywordsList: "
"__;_e;_x:1,2c;_ex:1,2c;_n:1,2;_nx:1,2,4c;_n_noop:1,2;_nx_noop:1,2,3c;esc_"
"attr__;esc_html__;esc_attr_e;esc_html_e;esc_attr_x:1,2c;esc_html_x:1,2c;\n"
"Language: en\n"
"Plural-Forms: nplurals=2; plural=(n != 1);\n"
"X-Poedit-Country: United States\n"
"X-Poedit-SourceCharset: UTF-8\n"
"X-Poedit-Basepath: ../\n"
"X-Poedit-SearchPath-0: .\n"
"X-Poedit-Bookmarks: \n"
"X-Textdomain-Support: yes\n"
#: includes/class-events.php:256
msgid "Invalid or incomplete request data."
msgstr ""
#: includes/class-events.php:264
#. translators: 1: Job identifier
msgid "Job with identifier `%1$s` is not scheduled to run yet."
msgstr ""
#: includes/class-events.php:280
#. translators: 1: Job identifier
msgid "Job with identifier `%1$s` could not be found."
msgstr ""
#: includes/class-events.php:294
#. translators: 1: Event action, 2: Event arguments
msgid ""
"No resources available to run the job with action `%1$s` and arguments "
"`%2$s`."
msgstr ""
#: includes/class-events.php:322
#. translators: 1: Event action, 2: Event arguments, 3: Throwable error, 4:
#. Line number that raised Throwable error
msgid ""
"Callback for job with action `%1$s` and arguments `%2$s` raised a Throwable "
"- %3$s in %4$s on line %5$d."
msgstr ""
#: includes/class-events.php:340
#. translators: 1: Event action, 2: Event arguments
msgid "Job with action `%1$s` and arguments `%2$s` executed."
msgstr ""
#: includes/class-internal-events.php:111
msgid "Cron Control internal job - every minute"
msgstr ""
#: includes/class-internal-events.php:115
msgid "Cron Control internal job - every 10 minutes"
msgstr ""
#: includes/class-main.php:79
#. translators: 1: Plugin name, 2: Constant name
msgid "%1$s: %2$s set to unexpected value; must be corrected for proper behaviour."
msgstr ""
#: includes/class-main.php:94
#. translators: 1: Plugin name
msgid "Normal cron execution is blocked when the %s plugin is active."
msgstr ""
#: includes/class-main.php:125
#. translators: 1: Plugin name, 2: Constant name
msgid "<strong>%1$s</strong>: To use this plugin, define the constant %2$s."
msgstr ""
#: includes/class-rest-api.php:90
msgid "Automatic event execution is disabled indefinitely."
msgstr ""
#: includes/class-rest-api.php:93
#. translators: 1: Time automatic execution is disabled until, 2: Unix
#. timestamp
msgid "Automatic event execution is disabled until %1$s UTC (%2$d)."
msgstr ""
#: includes/class-rest-api.php:124
msgid "Secret must be specified with all requests"
msgstr ""
#: includes/wp-cli/class-cache.php:25
msgid "Internal caches cleared"
msgstr ""
#: includes/wp-cli/class-cache.php:27
msgid "No caches to clear"
msgstr ""
#: includes/wp-cli/class-events.php:30
msgid "Invalid page requested"
msgstr ""
#: includes/wp-cli/class-events.php:39
msgid ""
"Entries are purged automatically, so this cannot be relied upon as a record "
"of past event execution."
msgstr ""
#: includes/wp-cli/class-events.php:44
msgid "No events to display"
msgstr ""
#: includes/wp-cli/class-events.php:55
#. translators: 1: Number of events to display
msgid "Displaying %s entry"
msgid_plural "Displaying all %s entries"
msgstr[0] ""
msgstr[1] ""
#: includes/wp-cli/class-events.php:58
#. translators: 1: Entries on this page, 2: Total entries, 3: Current page, 4:
#. Total pages
msgid "Displaying %1$s of %2$s entries, page %3$s of %4$s"
msgstr ""
#: includes/wp-cli/class-events.php:109
msgid ""
"Specify something to delete, or see the `cron-control-fixers` command to "
"remove all data."
msgstr ""
#: includes/wp-cli/class-events.php:123
msgid "Specify the ID of an event to run"
msgstr ""
#: includes/wp-cli/class-events.php:131
#. translators: 1: Event ID
msgid ""
"Failed to locate event %d. Please confirm that the entry exists and that "
"the ID is that of an event."
msgstr ""
#: includes/wp-cli/class-events.php:135
#. translators: 1: Event ID, 2: Event action, 3. Event instance
msgid "Found event %1$d with action `%2$s` and instance identifier `%3$s`"
msgstr ""
#: includes/wp-cli/class-events.php:141
#. translators: 1: Time in UTC, 2: Human time diff
msgid "This event is not scheduled to run until %1$s UTC (%2$s)"
msgstr ""
#: includes/wp-cli/class-events.php:144
msgid "Run this event?"
msgstr ""
#: includes/wp-cli/class-events.php:158
msgid "Failed to run event"
msgstr ""
#: includes/wp-cli/class-events.php:193
msgid "Invalid status specified"
msgstr ""
#: includes/wp-cli/class-events.php:222
msgid "Problem retrieving events"
msgstr ""
#: includes/wp-cli/class-events.php:250
msgid "Non-repeating"
msgstr ""
#: includes/wp-cli/class-events.php:252 includes/wp-cli/class-rest-api.php:94
msgid "n/a"
msgstr ""
#: includes/wp-cli/class-events.php:260 includes/wp-cli/class-rest-api.php:93
msgid "true"
msgstr ""
#: includes/wp-cli/class-events.php:335
msgid "%s year"
msgid_plural "%s years"
msgstr[0] ""
msgstr[1] ""
#: includes/wp-cli/class-events.php:336
msgid "%s month"
msgid_plural "%s months"
msgstr[0] ""
msgstr[1] ""
#: includes/wp-cli/class-events.php:337
msgid "%s week"
msgid_plural "%s weeks"
msgstr[0] ""
msgstr[1] ""
#: includes/wp-cli/class-events.php:338
msgid "%s day"
msgid_plural "%s days"
msgstr[0] ""
msgstr[1] ""
#: includes/wp-cli/class-events.php:339
msgid "%s hour"
msgid_plural "%s hours"
msgstr[0] ""
msgstr[1] ""
#: includes/wp-cli/class-events.php:340
msgid "%s minute"
msgid_plural "%s minutes"
msgstr[0] ""
msgstr[1] ""
#: includes/wp-cli/class-events.php:341