fix: invalidate the object cache when bulk-deleting plugin options - #1685
Merged
jakejackson1 merged 1 commit intoJul 24, 2026
Merged
Conversation
jakejackson1
force-pushed
the
fix/uninstall-option-cache-invalidation
branch
from
July 24, 2026 02:51
42c6bd4 to
08b1297
Compare
Three cleanup routines removed options with a raw `DELETE ... LIKE` query, which takes the rows out of the database but leaves whatever WordPress cached behind: the individually cached entry in the `options`/`site-options` group, and — for an autoloaded row — a copy inside the `alloptions` blob that keeps being loaded on every request. - Model_Uninstall::remove_plugin_options() gpdf_sl_% - Model_Uninstall::remove_plugin_network_options() gpdf_sl_net_% - Controller_Upgrade_Routines::remove_legacy_update_cache() edd_sl_% The upgrade routine is the one that bites: the plugin keeps running afterwards, and leaving a legacy autoloaded option in the cache is the exact cost the routine exists to remove. It was masked by the two delete_option() calls that follow it — those force a wp_load_alloptions( true ) rebuild — but only when one of those two options actually exists, which on a site that never hit an API failure it doesn't. Each query now selects the names and deletes through the API. The network sweep uses delete_network_option() with the row's own site_id rather than delete_site_option(), which would silently narrow the cleanup to the current network and strand rows belonging to the others sharing the sitemeta table. Wildcards are escaped with esc_like(), so `gpdf_sl_` no longer also matches `gpdfXslY`. Both uninstaller tests previously flushed the cache by hand before asserting, which papered over the bug; the flushes are gone and the assertions now cover it. The upgrade routine had no coverage and gains a test. All three fail without the corresponding fix. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
jakejackson1
force-pushed
the
fix/uninstall-option-cache-invalidation
branch
from
July 24, 2026 02:52
08b1297 to
4959171
Compare
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Follow-up to #1684.
The problem
Three cleanup routines removed options with a raw
DELETE ... LIKEquery. That takes the rows out of the database but leaves whatever WordPress cached behind — the individually cached entry in theoptions/site-optionsgroup, and for an autoloaded row a copy inside thealloptionsblob that keeps being loaded on every request.Model_Uninstall::remove_plugin_options()gpdf_sl_%Model_Uninstall::remove_plugin_network_options()gpdf_sl_net_%Controller_Upgrade_Routines::remove_legacy_update_cache()edd_sl_%This is observable without a persistent object cache —
get_option()/get_site_option()read through the per-request cache too.Why the upgrade routine is the one that bites
The uninstaller calls
deactivate_plugins()immediately afterwards, so little gets a chance to read the stale values. The upgrade routine doesn't — the plugin keeps running, and leaving a legacy autoloaded option in the cache is the exact cost the routine exists to remove.It was masked by the two
delete_option()calls that follow it: core'sdelete_option()runswp_load_alloptions( true ), forcing a rebuild from the database that drops the swept rows along the way. But it bails atif ( is_null( $row ) ) return false;before touching the cache when the option doesn't exist — and on a site that never hit an API failure, neither of those two options exists. So the mitigation was real but accidental and conditional.The fix
Each query now selects the names and deletes through the API.
Two details worth review attention:
delete_network_option()with the row's ownsite_id, notdelete_site_option().sitemetais shared across networks and the original query had nosite_idfilter, so it swept all of them;delete_site_option()would silently narrow the cleanup to the current network and strand the rest.esc_like().'gpdf_sl_%'previously meant "gpdf, any char, sl, any char, anything" — harmless in practice, but wrong.Testing
Both uninstaller tests previously flushed the cache by hand before asserting, which papered over the bug:
Those flushes are gone, so the assertions now cover the real behaviour. The upgrade routine had no coverage at all and gains a test, including an unrelated
edd_sl_*option that must survive the value filter.All three tests fail without their corresponding fix (verified by stashing the
src/changes). Full suite green both modes: 1150 tests / 3581 assertions single-site, 1150 / 3680 multisite.phpcsclean.No changelog entry, by request.
🤖 Generated with Claude Code