Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Avoid scheduling known events on front end #713

Open
wants to merge 1 commit into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
60 changes: 58 additions & 2 deletions docs/scheduled-tasks.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,12 +11,68 @@ The integration with WordPress is seamless, so existing WordPress themes, plugin

## Creating Scheduled Tasks

Scheduled events work by triggering an action hook, effectively running `do_action( 'hook' )` at the scheduled time. The functions you need to run at the scheduled time should added to that hook using `add_action()`.
Scheduled tasks, also known as "events", work by triggering an action hook, effectively running `do_action( 'hook' )` at the scheduled time. The functions you need to run at the scheduled time should added to that hook using `add_action()`.

Events can be one-off or recurring.

**Note**: Scheduling a one-off event to occur within 10 minutes of an existing event with the same hook and the same arguments will fail in order to prevent the accidental scheduling of duplicate events. This does not affect the scheduling of recurring events.

### Example Recurring Scheduled Event

A typical pattern for scheduling tasks is to check if the task is already scheduled, and if not to schedule it. This is particularly useful for recurring events.

```php
// Schedule an hourly process on the admin_init hook.
add_action( 'admin_init', function () {
if ( ! wp_next_scheduled( 'do_background_process' ) ) {
wp_schedule_event( time(), 'hourly', 'do_background_process' );
}
} );

// Add a callback to the hook we just scheduled.
add_action( 'do_background_process', function () {
// ... Background process code goes here.
} );
```

### Example Single Event

When you only need a one-off event like some post-processing that takes a long time, you should schedule it based on a specific user action. The following example schedules an event when saving a post:

```php
add_action( 'save_post', function ( $post_id ) {
// Schedule a background task and pass the post ID as an argument.
wp_schedule_single_event( time(), 'do_background_process', [ $post_id ] );
} );

// Add a callback to the hook we just scheduled.
add_action( 'do_background_process', function ( $post_id ) {
// ... Background process code goes here and can use $post_id.
} );
```

### Best Practices

- Avoid scheduling events on front end requests, especially for non logged in users
- Use the `altis.migrate` or `admin_init` action hooks for scheduling recurring events
- Only schedule single events on a specific user action
- Use scheduled events to offload time consuming work and speed up the app for your users

### Dealing With Third-Party Events

It's common practice for WordPress plugins to schedule their events on the `init` hook. In a standard WordPress set up this is typically fine, as the scheduled events are stored in the Options table and autoloaded. This doesn't work on a multi-server architecture so Altis uses Cavalcade to handle background tasks.

This means that each call to `wp_next_scheduled()` is a database lookup, rather than there being one lookup. Coupled with request latency this can cause unnecessary requests on the front end of the site and slower performance, particularly for logged in users.

Use the `altis.cloud.admin_only_events` filter to force specific event hooks to only run in the admin context:

```php
add_filter( 'altis.cloud.admin_only_events', function ( array $events ) : array {
$events[] = 'third_party_plugin_event_hook';
return $events;
} );
```

### Intervals

Recurring events need a named interval. Out of the box these intervals are `hourly`, `twicedaily`, `daily` and `weekly`.
Expand Down Expand Up @@ -62,7 +118,7 @@ This is the same as `wp_schedule_event()` but will trigger the event only once.

**`wp_next_scheduled( string $hook, array $args = [] )`**

This function should be used to check if an event for the given hook and set of arguments has already been scheduled. If one has it will return the timestamp for the next occurrence.
This function should be used to check if an event for the given hook and set of arguments has already been scheduled. If one exists it will return the timestamp for the next occurrence.

**`wp_unschedule_event( int $timestamp, string $hook, array $args = [], bool $wp_error = false )`**

Expand Down
33 changes: 33 additions & 0 deletions inc/performance_optimizations/namespace.php
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,9 @@
*/
function bootstrap() {
increase_set_time_limit_on_async_upload();

// Avoid DB requests to Cavalcade on the front end.
add_filter( 'pre_get_scheduled_event', __NAMESPACE__ . '\\schedule_events_in_admin', 1, 2 );
}

/**
Expand Down Expand Up @@ -42,3 +45,33 @@ function increase_set_time_limit_on_async_upload() {
}
set_time_limit( 120 );
}

/**
* Only schedule known events in the admin to avoid extra db requests on front end.
*
* @param null|false|object $pre Value to return instead. Default null to continue retrieving the event.
* @param string $hook Action hook of the event.
* @return null|false|object Value to return instead. Default null to continue retrieving the event.
*/
function schedule_events_in_admin( $pre, string $hook ) {
$admin_only_hooks = [
'wp_site_health_scheduled_check',
'wp_privacy_delete_old_export_files',
'wp_https_detection',
];

/**
* Filter the scheduled event hooks to only fire in the admin context. This
* is useful for avoiding database lookups on front end requests that are not
* needed.
*
* @param array $backend_only_hooks The hook names to only run in the admin context.
*/
$admin_only_hooks = apply_filters( 'altis.cloud.admin_only_events', $admin_only_hooks );

if ( ! in_array( $hook, $admin_only_hooks, true ) || is_admin() ) {
return $pre;
}

return true;
}