Skip to content

Options, Meta APIs: Remove orphaned transient timeout rows on delete and cleanup - #13211

Open
HasnainAshfaq wants to merge 2 commits into
WordPress:trunkfrom
HasnainAshfaq:feature/65863-delete-transient-orphaned-timeout
Open

Options, Meta APIs: Remove orphaned transient timeout rows on delete and cleanup#13211
HasnainAshfaq wants to merge 2 commits into
WordPress:trunkfrom
HasnainAshfaq:feature/65863-delete-transient-orphaned-timeout

Conversation

@HasnainAshfaq

Copy link
Copy Markdown

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:

  1. delete_transient() conditions cleanup of the timeout row on the value row deletion succeeding. With no value row, it returns false and leaves the timeout row forever.
  2. delete_expired_transients() uses a multi-table DELETE that joins from the value row side, so an orphaned timeout row has no a side 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() and delete_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 DELETE statement 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 asserts delete_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, so SUBSTRING(a.option_name, 20) extracts the transient name. _site_transient_timeout_ is 24 characters, so SUBSTRING(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

…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
@github-actions

Copy link
Copy Markdown

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 props-bot label.

Core Committers: Use this line as a base for the props when committing in SVN:

Props hasnainashfaq.

To understand the WordPress project's expectations around crediting contributors, please review the Contributor Attribution page in the Core Handbook.

@github-actions

Copy link
Copy Markdown

Test using WordPress Playground

The 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

  • All changes will be lost when closing a tab with a Playground instance.
  • All changes will be lost when refreshing the page.
  • A fresh instance is created each time the link below is clicked.
  • Every time this pull request is updated, a new ZIP file containing all changes is created. If changes are not reflected in the Playground instance,
    it's possible that the most recent build failed, or has not completed. Check the list of workflow runs to be sure.

For more details about these limitations and more, check out the Limitations page in the WordPress Playground documentation.

Test this pull request with WordPress Playground.

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
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.

1 participant