How to Handle Big Tasks in WordPress

Posted on:

PHP is not natively built for large tasks that take a long time. It's designed to run a fast, small script and load the page promptly. WordPress's database is designed to run fast, small queries as well. Both PHP and MySQL can run large tasks, but doing so tends to either crash your site, fail to complete, or force everyone else to wait until your task is done before the site will finish loading. None of those are great options.

To work around this, many WordPress developers use batch tasks to get the job done. A Batch task takes a big task and breaks it up into many smaller tasks. This makes big tasks take a bit longer to complete, but since the tasks are smaller, it makes completing the task much more likely to complete successfully. In-order to-do this, you have to give a batch task some key points of data:

  1. How to know when the task is done
  2. What to do in a single step in the process
  3. The data necessary to actually do the task

Batching can be accomplished in a few different ways, and each one has their own pros and cons. You will find some bigger plugins employ all three for different tasks, depending on which option is the best choice for the particular task. Others will stick to a single option.

Batching is a good idea when you have a large, slow task that you need to-do on your site. Usually, this is some kind of database operation, such as an upgrade, or optimization, but it can be anything as long as it is something that needs done, but is not required for the page to load.

Batching With Underpin's Batch Task Loader

One option is to use Underpin's batch task loader. This loader adds a notice in the admin area if the batch task needs to run. When the person is ready to run the batch task, they simply click the "run" button. When clicked, this will work with the person's browser to keep track of the batch task progress, automatically sending requests to handle each step until the process is complete.

This method is a great choice if you want a person to choose when to run a task. Some batch tasks are better-done when someone is physically at the computer. this allows the person to check and confirm that the updates were applied successfully, and that the updates did not break something on the site.

This method requires Underpin to run, and setting up a batch task looks like this:

{
  "name": "acf/snippet",
  "attributes": {
    "id": "block_6105b24028aef",
    "name": "acf/snippet",
    "data": {
      "language": "php",
      "_language": "field_60d7f32b1b540",
      "code": "\\Underpin\\underpin()->batch_tasks()->add( 'example-batch', [\r\n    'description'             => 'A batch task that does nothing 20 times',\r\n    'name'                    => 'Batch Task Example',\r\n    'tasks_per_request'       => 50,\r\n    'stop_on_error'           => true,\r\n    'total_items'             => 1000,\r\n    'notice_message'          => 'Run the most pointless batch task ever made.',\r\n    'button_text'             => 'LETS GO.',\r\n    'capability'              => 'administrator',\r\n    'batch_id'                => 'example-batch',\r\n    'task_callback'           => '__return_null', // The callback that iterates on every task\r\n    'finish_process_callback' => '__return_null', // The callback that runs after everything is finished\r\n    'prepare_task_callback'   => '__return_null', // The callback that runs before each task\r\n    'finish_task_callback'    => '__return_null', // The callback that runs after each task\r\n  ] );",
      "_code": "field_60d7f36c1b541"
    },
    "align": "",
    "mode": "edit"
  },
  "innerBlocks": []
}

From there, you just need to instruct your site to add the batch task if it's necessary like so:

{
  "name": "acf/snippet",
  "attributes": {
    "id": "block_6105b24c28af0",
    "name": "acf/snippet",
    "data": {
      "language": "php",
      "_language": "field_60d7f32b1b540",
      "code": "Underpin\\underpin()->batch_tasks()->enqueue( 'example-batch' );",
      "_code": "field_60d7f36c1b541"
    },
    "align": "",
    "mode": "edit"
  },
  "innerBlocks": []
}

Batching With a Background Processor

The other option is to use a background processing library. This works much like Underpin's batch task loader, only instead of waiting for a user's input to start the process, this will run silently in the background, and automatically run at a pace the server can handle at that moment until the task is complete. This is the best option when you don't need to configure the rate at which tasks run, and you don't want to wait for a user to initiate the process. It's so cool.

You can do this using Underpin using the background process builder loader like so:

{
  "name": "acf/snippet",
  "attributes": {
    "id": "block_6105b25a28af1",
    "name": "acf/snippet",
    "data": {
      "language": "php",
      "_language": "field_60d7f32b1b540",
      "code": "underpin()->background_processes()->add( 'example', [\r\n    'action'               => 'example_action_name', // Action Name. Must be unique.\r\n    'task_action_callback' => function ( $item ) {   // Callback to fire on a single item.\r\n      // Do an action\r\n    },\r\n] );",
      "_code": "field_60d7f36c1b541"
    },
    "align": "",
    "mode": "edit"
  },
  "innerBlocks": []
}

Once registered you can add items to the queue, like so:

{
  "name": "acf/snippet",
  "attributes": {
    "id": "block_6105b26328af2",
    "name": "acf/snippet",
    "data": {
      "language": "php",
      "_language": "field_60d7f32b1b540",
      "code": "underpin()->background_processes()->enqueue( 'key', ['args' => 'to pass to task_action_callback'] );",
      "_code": "field_60d7f36c1b541"
    },
    "align": "",
    "mode": "edit"
  },
  "innerBlocks": []
}

Then actually instruct WordPress to run the task using dispatch:

{
  "name": "acf/snippet",
  "attributes": {
    "id": "block_6105b27328af3",
    "name": "acf/snippet",
    "data": {
      "language": "php",
      "_language": "field_60d7f32b1b540",
      "code": "underpin()->background_processes()->dispatch( 'key' );",
      "_code": "field_60d7f36c1b541"
    },
    "align": "",
    "mode": "edit"
  },
  "innerBlocks": []
}

If you don't want to use Underpin, you can directly use the background process library it uses directly. This is a bit more verbose, but works all the same.

{
  "name": "acf/snippet",
  "attributes": {
    "id": "block_6105b28128af4",
    "name": "acf/snippet",
    "data": {
      "language": "php",
      "_language": "field_60d7f32b1b540",
      "code": "class WP_Example_Process extends WP_Background_Process {\r\n\r\n    /**\r\n     * @var string\r\n     */\r\n    protected $action = 'example_process';\r\n\r\n    /**\r\n     * Task\r\n     *\r\n     * Override this method to perform any actions required on each\r\n     * queue item. Return the modified item for further processing\r\n     * in the next pass through. Or, return false to remove the\r\n     * item from the queue.\r\n     *\r\n     * @param mixed $item Queue item to iterate over\r\n     *\r\n     * @return mixed\r\n     */\r\n    protected function task( $item ) {\r\n        // Actions to perform\r\n\r\n        return false;\r\n    }\r\n\r\n}\r\n\r\n$example_process = new WP_Example_Process();",
      "_code": "field_60d7f36c1b541"
    },
    "align": "",
    "mode": "edit"
  },
  "innerBlocks": []
}

Then you need to push some items to the queue, like so:

{
  "name": "acf/snippet",
  "attributes": {
    "id": "block_6105b28d28af5",
    "name": "acf/snippet",
    "data": {
      "language": "php",
      "_language": "field_60d7f32b1b540",
      "code": "// Push an item to the queue\r\n$example_process->push_to_queue( ['args' => 'to_pass_to_function'] );",
      "_code": "field_60d7f36c1b541"
    },
    "align": "",
    "mode": "edit"
  },
  "innerBlocks": []
}

Finally, you can dispatch the process:

{
  "name": "acf/snippet",
  "attributes": {
    "id": "block_6105b29a28af6",
    "name": "acf/snippet",
    "data": {
      "language": "php",
      "_language": "field_60d7f32b1b540",
      "code": "$example_process->save()->dispatch(); ",
      "_code": "field_60d7f36c1b541"
    },
    "align": "",
    "mode": "edit"
  },
  "innerBlocks": []
}

Batching With a Cron Job

Another option is to use a WordPress cron job. Cron jobs run in WordPress's background, and are meant to run on an approximate time interval. Cron jobs are effective for indefinite tasks that need to run in the background all the time, such as a sync relationship with another server, or a plugin/license update check. These are also the easiest choice if you're building your plugin to be 100% native WordPress without any libraries.

Cron jobs, however, do not actually run on the time interval you specify. The cron job only runs when someone visits the site, and instructs WordPress to check and see if the cron needs fired. This means if your site has low-traffic, these tasks could take much longer to run than you expect. You can, however, attach WordPress' cron to a real cron job if you have the appropriate access to your server.

The easiest way to set up a cron job is with Underpin's cron job loader, but it's not required if you're not using Underpin. The Underpin method looks like this:

{
  "name": "acf/snippet",
  "attributes": {
    "id": "block_6105b2aa28af7",
    "name": "acf/snippet",
    "data": {
      "language": "php",
      "_language": "field_60d7f32b1b540",
      "code": "underpin()->cron_jobs()->add( 'test', [\r\n    'action_callback' => '__return_true',\r\n    'name'            => 'Event Name',\r\n    'frequency'       => 'daily',\r\n    'description'     => 'The description',\r\n] );",
      "_code": "field_60d7f36c1b541"
    },
    "align": "",
    "mode": "edit"
  },
  "innerBlocks": []
}

Not only is it simpler to set up in Underpin, but it also includes built-in logging, which makes it a lot easier to track when cron tasks fire.

If you're setting one up manually, it requires a few more steps, but it can be done. It looks something like this:

{
  "name": "acf/snippet",
  "attributes": {
    "id": "block_6105b2b728af8",
    "name": "acf/snippet",
    "data": {
      "language": "php",
      "_language": "field_60d7f32b1b540",
      "code": "add_action( 'init', function(){\r\n        // If this event is not scheduled, schedule it.\r\n        if ( ! wp_next_scheduled( 'event_name' ) ) {\r\n           // Set up the task to run daily\r\n            wp_schedule_event( time(), 'daily', 'event_name' );\r\n        }\r\n} );\r\n\r\nadd_action( 'event_name', function(){\r\n   // Do this task once a day.\r\n} )",
      "_code": "field_60d7f36c1b541"
    },
    "align": "",
    "mode": "edit"
  },
  "innerBlocks": []
}

Summing it All Up

Batch tasks allows you to do big tasks, and keep your site running smoothly at the same time. For years, the only viable option for doing batch tasks was the WordPress cron job, or building your own custom batching solution, but that is no-longer the case. We now have several different ways to approach batching, and each one has its own strengths.

When used correctly, each of the options detailed in this post will allow you to build a stable way to run batch tasks on any WordPress website without trouble.