Skip to content

[PLTFRM-2722] Add index for action/instance lookups on the events table - #515

Open
WRasada wants to merge 1 commit into
mainfrom
fix/add-action-instance-index
Open

[PLTFRM-2722] Add index for action/instance lookups on the events table#515
WRasada wants to merge 1 commit into
mainfrom
fix/add-action-instance-index

Conversation

@WRasada

@WRasada WRasada commented Aug 18, 2026

Copy link
Copy Markdown

Fixes #516

Lookups by action + instance can't use ts_action_instance_status, since that key leads with timestamp and those queries don't filter on it. They fall back to scanning the whole table, which shows up on any site with a large pending queue.

Both wp_clear_scheduled_hook() (via pre_clear_scheduled_hook(), which queries with limit => 500) and wp_next_scheduled() (limit => 1) produce that shape, and WP core calls the latter inside every wp_schedule_single_event().

This adds an index that covers that access pattern, plus the version check needed for existing installs to pick it up.

What's here

  • New KEY action_instance_status_ts (action(191), instance, status, timestamp) in the schema.
  • DB_VERSION bumped to 2.
  • A version check in class_init() that registers an upgrade on shutdown when the stored version is behind.
  • maybe_upgrade_during_shutdown(), which mirrors the existing install method.

On the upgrade path

DB_VERSION wasn't being read anywhere - it's written on install but never compared - and _prepare_table() only runs when the table is missing. So without a version check the schema change would only reach new installs.

The check sits alongside the existing install branch, so the shutdown hook is only registered while a site is actually behind. DB_VERSION_OPTION is autoloaded, so the check itself doesn't add a query. It runs once, the option bumps, and the hook stops registering after that.

maybe_upgrade_during_shutdown() is a sibling method rather than a change to the install one, since that method returns early when the table already exists - which is exactly the case an upgrade needs to run in. It keeps the same gating, so it only fires on cron, WP-CLI, and non-ajax admin requests, and it takes its own one-minute cache lock before doing anything.

If the upgrade ever failed, the option wouldn't bump and it'd retry on the next eligible request. That matches the "keep trying" behaviour the install path already has.

On running this against existing tables

Adding a secondary index in InnoDB runs with ALGORITHM=INPLACE, LOCK=NONE, so reads and writes keep going while it builds. We ran the equivalent statement on a production table of roughly 38,000 rows and 49 MB with no measurable impact.

Happy to switch the upgrade to a targeted ALTER TABLE ... ADD INDEX instead of reusing _prepare_table() if you'd prefer something more predictable than a full dbDelta() pass. Smaller blast radius, just more code.

Why it showed up hourly

These lookups are object cached, so the scan doesn't normally reach response times. The hourly a8c_cron_control_purge_completed_events run calls flush_event_cache() with no arguments, which bumps last_changed for both cron-control-queries and cron-control-event:

class-events-store.php:537   wp_cache_set( 'last_changed', microtime(), 'cron-control-queries' );
class-events-store.php:542   wp_cache_set( 'last_changed', microtime(), $cache_group );  // cron-control-event

Since the keys are built from wp_cache_get_last_changed(), every cached lookup invalidates at once and they all hit the database together, each running the full scan.

The index doesn't change any of that - it just makes those cold lookups cheap. On the site we were looking at, the hourly window went from 620 transactions over 15s down to 3, and the datastore max for the table went from 51.19s to 0.215s.

Worth mentioning separately: the narrower cron-control-event group exists to "avoid most bulk invalidations" per the comment at line 463, but the null-argument flush_event_cache() call clears it anyway. That seems like its own thing rather than something to change here, so I've left it alone.

Verifying

Before, on a table with about 37,000 pending rows:

-> Limit: 500 row(s)  (actual time=98.3..98.3 rows=0 loops=1)
    -> Index scan using ts_action_instance_status
       (cost=119 rows=999) (actual time=0.0197..93.9 rows=38986 loops=1)

After:

-> Limit: 500 row(s)  (actual time=0.0266..0.0266 rows=0 loops=1)
    -> Index range scan using action_instance_status_ts
       (actual time=0.0168..0.0168 rows=0 loops=1)

38,986 rows read down to 0, and 98.3 ms down to 0.027 ms.

Still to add

Tests aren't in this PR yet - happy to add them here or in a follow-up, whichever you prefer:

  • Schema assertion for the new index.
  • Upgrade path: set the stored version behind, confirm the hook registers, the index gets added, and the option bumps.
  • Confirm the hook isn't registered when the version is current.

Related: #517 covers ready-event selection loading the full pending queue into PHP, which is a separate problem this PR doesn't touch.

Tracking: PLTFRM-2722

Lookups by action + instance can't use ts_action_instance_status, since
that key leads with timestamp and those queries don't filter on it. They
fall back to scanning the whole table, which shows up on any site with a
large pending queue.

Adds KEY action_instance_status_ts (action(191), instance, status,
timestamp), bumps DB_VERSION, and adds a version check so existing
installs pick up the new index. DB_VERSION was previously written on
install but never read back, so a schema change alone would only have
reached new installs.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@github-actions

Copy link
Copy Markdown

Dependency Review

✅ No vulnerabilities or license issues or OpenSSF Scorecard issues found.

Scanned Files

None

PRIMARY KEY (`ID`),
UNIQUE KEY `ts_action_instance_status` (`timestamp`, `action` (191), `instance`, `status`),
KEY `status` (`status`)
KEY `status` (`status`),

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What about only upgrading the option version if this succeeds?

@sjinks can use your input here

@rebeccahum rebeccahum changed the title Add index for action/instance lookups on the events table [PLTFRM-2722] Add index for action/instance lookups on the events table Aug 19, 2026
@sjinks

sjinks commented Aug 20, 2026

Copy link
Copy Markdown
Member

We later have:

		$is_installed = 1 === count( $wpdb->get_col( $wpdb->prepare( 'SELECT TABLE_NAME FROM information_schema.tables WHERE TABLE_NAME = %s', $table_name ) ) );
		wp_cache_set( 'is_installed', $is_installed, 'cron-control' );

		if ( $is_installed ) {
			update_option( self::DB_VERSION_OPTION, self::DB_VERSION );
		}

But if the upgrade fails, we will still have the table but lack the index. However, $is_installed will be true, and the index will never be created.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Missing index causes a full table scan on action/instance lookups

3 participants