elementor / elementor/image-optimization-releases

Stuck-operation cleanup job full-scans wp_actionscheduler_actions every 5 minutes (unbounded query + per-row fetches; failed actions accumulate indefinitely)

Open
#3 0 comments 0 reactions 0 assignees View on GitHub
Dominant language
No language data
Stars
1
Forks
1
PR merge metrics
No merged PRs in 30d

Description

**Plugin version:** 1.7.6 (latest)
**Environment:** WordPress + WooCommerce, PHP 8.3, MySQL 8 (Aurora), Action Scheduler 4.0.0 data store, ~100k rows in `wp_actionscheduler_actions`

## Summary

The recurring `image-optimization/cleanup/stuck-operation` job (scheduled every 5 minutes) scans the **entire** `wp_actionscheduler_actions` table and then issues **one DB query per matched row**. On a store with a normally-sized Action Scheduler history (~100k rows), each run takes **17–32 minutes of continuous PHP + DB work**, on a 5-minute recurrence — i.e. the cleanup job runs essentially non-stop.

## Root cause

In `modules/optimization/components/actions-cleanup.php`, `cleanup_stuck_operations()` runs:

```sql
SELECT action_id
FROM wp_actionscheduler_actions
WHERE last_attempt_gmt IS NOT NULL
AND UNIX_TIMESTAMP(last_attempt_gmt) < %d -- now - 300s
```

Three problems compound:

1. **The query is unbounded.** It has no filter on `hook`, `group_id`, `status`, or `claim_id`, so it matches essentially every action ever recorded — including tens of thousands of `complete`/`failed` actions belonging to other plugins (WooCommerce, Facebook, Mailchimp, …). Wrapping the column in `UNIX_TIMESTAMP()` also makes the predicate non-sargable, forcing a full table scan even though `last_attempt_gmt` is indexed.
2. **Per-row fetch.** The loop then calls `Async_Operation::get_by_id()` for every matched `action_id` before discarding non-optimize-queue rows in PHP — ~100k individual queries per run in our case.
3. **Failure feedback loop.** When the action is picked up by Action Scheduler's async HTTP runner, PHP's 30s `max_execution_time` kills it mid-scan (`PHP Fatal error: Maximum execution time of 30 seconds exceeded in wp-includes/class-wpdb.php`). The action is marked `failed`, so its recurring successor is never scheduled — and `schedule_cleanup()` (hooked to `action_scheduler_init` on every request) creates a fresh one. Since Action Scheduler's built-in cleaner never purges `failed` actions, they accumulate indefinitely (we reached **12,285 failed `image-optimization/cleanup/stuck-operation` actions**), growing the very table the query scans and making each subsequent run slower.

## Observed impact

- Each cleanup run: 17–32 minutes wall-clock (measured from `wp_actionscheduler_logs` timestamps), every 5 minutes, indefinitely — even when no optimization is running at all.
- ~5,800 PHP fatal "Maximum execution time exceeded" shutdowns logged against this hook.
- 12,285 `failed` + ~7,800 `complete` rows for this single hook.
- Users see `image-optimization/cleanup/stuck-operation` endlessly re-added and piling up under Tools → Scheduled Actions.

## Suggested fix

Constrain the query to the rows the handler actually cares about, with an indexed comparison, e.g.:

```sql
SELECT action_id
FROM wp_actionscheduler_actions
WHERE status = 'in-progress'
AND claim_id != 0
AND last_attempt_gmt < %s -- gmdate('Y-m-d H:i:s', time() - 300)
```

ideally also joining/filtering on the plugin's own group or hook prefix, and dropping the per-row `get_by_id()` round-trips (fetch the needed columns in the initial query). Additionally, `schedule_cleanup()` should re-use/reschedule after a failed run rather than letting failed instances accumulate, or the plugin should purge its own failed cleanup actions.

Happy to provide more diagnostics if useful.

Contributor guide

No contributing guide indexed for this repository

Research direction

Start in modules/optimization/components/actions-cleanup.php, especially cleanup_stuck_operations() and schedule_cleanup(), and inspect the Action Scheduler rows and logs described in the report. Trace how the query selects actions and how each matched row is fetched. Done means cleanup work is limited to relevant stuck operations, avoids the reported unbounded and per-row workload, and failed cleanup runs do not accumulate indefinitely.

Written by the indexing model from the issue text.

Assessment

Tech stack
mysql, php, wordpress
Domain
backend, databases
Issue type
Bug
Difficulty
4/5
Estimated time
3-5 days
Activity status
Active
Clarity
Mostly clear
Newbie friendliness
52/100

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.