Options, Meta APIs: Remove orphaned transient timeout rows on delete and cleanup - #13211
Conversation
…and cleanup. delete_transient() and delete_site_transient() conditioned removal of the _transient_timeout_ row on the value row deletion succeeding. When the value row is absent (e.g. a request died between set_transient()'s two separate writes), the timeout row became permanently stuck: every subsequent delete_transient() call returned false and left the row in place. delete_expired_transients() used a multi-table DELETE that joins from the value row side, so an orphaned timeout row with no matching value row was invisible to the cleanup job regardless of its timestamp. This commit addresses both: 1. Remove the if ( $result ) guard in delete_transient() and delete_site_transient() so the timeout row is always deleted when explicitly requested, whether or not the value row existed. 2. Add a LEFT JOIN DELETE statement in delete_expired_transients() for each of the three table variants (single-site transients, single-site site transients, and multisite sitemeta) that targets expired timeout rows whose value row is absent. Includes three unit tests: delete_transient() clears the orphaned row, delete_expired_transients() removes it when expired, and leaves it when it has not yet expired. Fixes https://core.trac.wordpress.org/ticket/65863
|
The following accounts have interacted with this PR and/or linked issues. I will continue to update these lists as activity occurs. You can also manually ask me to refresh this list by adding the Core Committers: Use this line as a base for the props when committing in SVN: To understand the WordPress project's expectations around crediting contributors, please review the Contributor Attribution page in the Core Handbook. |
Test using WordPress PlaygroundThe changes in this pull request can previewed and tested using a WordPress Playground instance. WordPress Playground is an experimental project that creates a full WordPress instance entirely within the browser. Some things to be aware of
For more details about these limitations and more, check out the Limitations page in the WordPress Playground documentation. |
delete_expired_transients() uses raw SQL and does not flush the option cache, so asserting via get_option() would read the cached value and fail. Switch both new orphan tests to query wpdb directly so they verify the physical row state. See https://core.trac.wordpress.org/ticket/65863
Fixes the two-part bug reported in #65863. This PR addresses both halves of the report — unlike the previously submitted PR #13101 which only fixed the first half.
The problem:
set_transient()writes the timeout row before the value row in two separate, non-transactional statements. If a request dies between those two writes, the timeout row is committed but the value row never arrives.This creates a permanently stuck orphan row because:
delete_transient()conditions cleanup of the timeout row on the value row deletion succeeding. With no value row, it returnsfalseand leaves the timeout row forever.delete_expired_transients()uses a multi-tableDELETEthat joins from the value row side, so an orphaned timeout row has noaside to join from and is never seen — regardless of its timestamp.The same applies to
delete_site_transient().What this PR does:
Fix 1 —
delete_transient()anddelete_site_transient():Removes the
if ( $result )guard so the timeout row is always deleted when requested, whether or not the value row existed.Fix 2 —
delete_expired_transients():Adds a
LEFT JOIN DELETEstatement after each existing cleanup query to remove expired orphaned timeout rows (timeout exists, value row is absent). Covers all three variants: single-site transients, single-site site transients, and multisite sitemeta.Tests:
test_delete_transient_removes_orphaned_timeout_row()— directly simulates the orphan and assertsdelete_transient()cleans it up.test_delete_expired_transients_removes_orphaned_expired_timeout_row()— expired orphan is removed by the cleanup job.test_delete_expired_transients_keeps_orphaned_future_timeout_row()— non-expired orphan is left alone.SQL offset note:
_transient_timeout_is 19 characters, soSUBSTRING(a.option_name, 20)extracts the transient name._site_transient_timeout_is 24 characters, soSUBSTRING(a.option_name, 25)is used for the site transient variants. These match the proposed queries in ticket comment #3 from the reporter.See https://core.trac.wordpress.org/ticket/65863