WordPress: Moving Expensive Per-Post Operations into a Cron Task
Let us say that you are writing a WordPress plugin that must perform an operation every time a post updates. For simple, low-cost operations, one can just hook into the save operation:
/** * Take additional actions when a post is saved. * * @param int $post_id The post ID. * @param post $post The post object. * @param bool $update True if this is an update of an existing post. */ function example_plugin_run_on_save_post($post_id, $post, $update) { // Perform necessary operations here. } add_action('save_post', 'example_plugin_run_on_save_post', 10, 3);
What to do in the case of more expensive operations, however? Administrators won't be lining up to thank you if you run a half-second duration task every time a post is saved. Sometimes, unfortunately, you are put in the position of having to run half-second duration tasks, and rerun them whenever a post is updated. Obviously, the best options here are to make that task fast, or to not run it at all. If it has to be done, however, then one possible solution is to move it into the WordPress cron system, accepting that tasks will be delayed to some degree following post updates.
Firstly, one needs to set up the hooks needed to run the example_plugin_run_cron_tasks()
function every hour, registering on plugin activation and deregistering on plugin deactivation. This is boilerplate code of the sort that appears in any WordPress plugin that uses the cron system.
define('EXAMPLE_PLUGIN_BASENAME', plugin_basename(__FILE__)); /** * Set up the hourly cron task for the plugin. */ function example_plugin_cron_activation() { if (!wp_next_scheduled('example_plugin_run_cron_tasks')) { wp_schedule_event(time(), 'hourly', 'example_plugin_run_cron_tasks'); } } register_activation_hook(EXAMPLE_PLUGIN_BASENAME, 'example_plugin_cron_activation'); /** * Remove the hourly cron task for the plugin. */ function example_plugin_cron_deactivation() { $timestamp = wp_next_scheduled('example_plugin_run_cron_tasks'); wp_unschedule_event($timestamp, 'example_plugin_run_cron_tasks'); } register_deactivation_hook(EXAMPLE_PLUGIN_BASENAME, 'example_plugin_cron_deactivation');
Next, we need some way to flag a post as having been processed, and to remove that flag whenever a post is saved. A post metadata value is the easiest approach here, so define a named value and use the save_post
hook to remove it.
define('EXAMPLE_PLUGIN_POST_PROCESSED_FLAG', 'example_plugin_post_processed'); /** * Remove the 'post processed' flag when a post is updated. * * @param int $post_id The post ID. * @param post $post The post object. * @param bool $update Whether this is an existing post being updated or not. */ function example_plugin_remove_flag_on_save_post($post_id, $post, $update) { // A new post won't have the flag, so no point in doing anything here. if (!$update) { return; } // Deleting this ensures that the post will be processed for its link content // in a future cron run. delete_post_meta($post_id, EXAMPLE_PLUGIN_POST_PROCESSED_FLAG); } add_action('save_post', 'example_plugin_remove_flag_on_save_post', 10, 3);
Lastly, fill in the example_plugin_run_cron_tasks()
function definition. It should find and process some number of posts lacking the processed flag with each run. Running it against all unprocessed posts would be excessive - the plugin might be installed on a blog with a very large number of posts, so limiting each cron run to a sensible number of operations makes sense.
// An arbitrary number. In reality, this should be configurable and added to // the options page for the plugin. define('EXAMPLE_PLUGIN_POSTS_PER_CRON_RUN', 100); /** * Run all cron tasks. */ function example_plugin_run_cron_tasks() { example_plugin_process_unprocessed_posts(); // // ... and whatever else needs doing. // } /** * Load a limited number of unprocessed posts and process them. */ function example_plugin_process_unprocessed_posts() { // Work backwards through posts by descending post ID. $query = new WP_Query(array( 'posts_per_page' => EXAMPLE_PLUGIN_POSTS_PER_CRON_RUN, 'meta_query' => array( array( 'key' => EXAMPLE_PLUGIN_POST_PROCESSED_FLAG, 'compare' => 'NOT EXISTS', ) ), 'orderby' => 'ID', 'order' => 'DESC' )); while ($query->have_posts()) { $query->the_post(); example_plugin_process_unprocessed_post($query->post); } wp_reset_postdata(); } /** * Process one unprocessed post. * * @param post $post The post object. */ function example_plugin_process_unprocessed_post($post) { // The expensive operation takes place here. }
All of which is easy enough to set up and test. Now all that is left is to craft the code for the expensive operation, something that is outside the scope of this brief post.