Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Feature Request: Option to clear WordPress Transients when Clearing Cache #459

Closed
raamdev opened this issue Mar 29, 2015 · 17 comments
Closed

Feature Request: Option to clear WordPress Transients when Clearing Cache #459

raamdev opened this issue Mar 29, 2015 · 17 comments
Assignees
Labels
Milestone

Comments

@raamdev
Copy link
Contributor

@raamdev raamdev commented Mar 29, 2015

It would be nice if WordPress Transients could be cleared alongside the ZenCache cache. This could be a new Advanced Option inside ZenCache that a site owner could enable if they want WordPress Transients (which some WordPress plugins use for internal caching) to be cleared whenever the ZenCache cache is cleared.

Referencing https://github.com/websharks/s2member.com/issues/27

@jaswrks
Copy link

@jaswrks jaswrks commented Mar 31, 2015

Adding a list of next actions (↑ above).

This would be an awesome feature to have!

It has always shocked me that WordPress itself does not clean these up. The Transient API is called what? These are "transient". https://codex.wordpress.org/Transients_API

lasting only for a short time; impermanent.

A closer look at the Transient API and we can see that an expiration time is set whenever each Transient entry is created! Yet, WordPress has no functionality that cleans any of this up. It is left for plugin authors to do, and I find that very few plugins that use the Transient API actually do this.

I'm guessing that's because the name of this API would suggest that it's taken care of automatically, and this documentation makes it sound like there's nothing more you need to do, when in fact there is more to do if you intend to make extensive use of this API; i.e., you need to come back and clean up old entries that are no longer being accessed.

The only thing WordPress does to expire a Transient, is to invalidate it whenever you request it again. For instance, if you call get_transient() and the existing entry has expired, this call fails as expected and the expired entry is deleted. However, if you don't ask for the data again, it just sits in your DB consuming storage space.


In the s2Member Pro plugin there are a variety of use cases for the WordPress Transient API. We use it extensively throughout s2Member. For that reason, s2Member needs to remove expired transients periodically through WP-Cron in order to prevent the WordPress database from being unnecessarily large.


If ZenCache offered this feature, and it was smart enough to expire transients when they are supposed to expire; i.e., (doing what the WP core should already do), that would be awesome.

In addition, making it an option to force a deletion of all transients whenever the cache is cleared would be useful also. I'd suggest leaving that off by default, but make it a Cache Clearing option.

@raamdev
Copy link
Contributor Author

@raamdev raamdev commented Mar 31, 2015

This would be an awesome feature to have!

Agreed! Definitely sounds like a GREAT feature to have--I haven't seen this in any other caching plugins.

s2Member needs to remove expired transients periodically through WP-Cron in order to prevent the WordPress database from being unnecessarily large.

Can you reference that code for me to review?

@raamdev raamdev added this to the Future Release milestone Mar 31, 2015
@jaswrks
Copy link

@jaswrks jaswrks commented Apr 3, 2015

@raamdev raamdev modified the milestones: Next Release, Future Release May 16, 2015
@raamdev raamdev modified the milestones: Next Release (Pro), Future Release Jul 10, 2015
@raamdev raamdev modified the milestones: Next Release (Lite), Future Release (Lite) Sep 30, 2015
@raamdev
Copy link
Contributor Author

@raamdev raamdev commented Nov 1, 2015

Noting here that this would make a great addition to the new Clear Cache options menu:

2015-10-31_21-01-56

@raamdev raamdev modified the milestones: Future Release (Pro), Next Release (Lite) Nov 1, 2015
@jaswrks
Copy link

@jaswrks jaswrks commented Nov 5, 2015

Next Actions (Pro Version Only)

  • New feature branch in the websharks/zencache-pro repo.

  • New class file in this directory with the name: WcpTransientUtils.php

    <?php
    /*[pro strip-from="lite"]*/
    namespace WebSharks\ZenCache\Pro;
    
    /*
    * Automatically wipes expired transients.
    *
    * @since 15xxxx Adding support for expired transients.
    *
    * @param bool $manually True if wiping is done manually.
    * @param boolean $maybe Defaults to a true value.
    *
    * @throws \Exception If a wipe failure occurs.
    *
    * @return int Total DB rows wiped by this routine (if any).
    */
    $self->wipeExpiredTransients = function ($manually = false, $maybe = true) use ($self) {
        if (!is_multisite()) {
            return $self->clearExpiredTransients();
        }
        $counter = 0; // Initialize.
    
        if (!$self->options['enable']) {
            return $counter; // Nothing to do.
        }
        if ($maybe && !$self->options['cache_clear_transients_enable']) {
            return $counter; // Not enabled at this time.
        }
        $time                     = time(); // Current UTC time.
        $wpdb                     = $self->wpdb(); // WP database class.
        $_transient_timeout_      = $wpdb->esc_like('_transient_timeout_');
        $_site_transient_timeout_ = $wpdb->esc_like('_site_transient_timeout_');
    
        switch_to_blog(get_current_site()->blog_id);
        $sql = '
            DELETE FROM `timeouts`, `transients`
                USING `'.esc_sql($wpdb->options).'` AS `timeouts`
            JOIN `'.esc_sql($wpdb->options).'` `transients` ON `transients`.`option_name` = REPLACE(`timeouts`.`option_name`, \'_timeout\', \'\')
            WHERE (`timeouts`.`option_name` LIKE \''.esc_sql($_transient_timeout_).'%\' OR `timeouts`.`option_name` LIKE \''.esc_sql($_site_transient_timeout_).'%\')
                AND CAST(`timeouts`.`option_value` AS UNSIGNED) < \''.esc_sql($time).'\'';
        $counter += (int) $wpdb->query(trim($sql));
    
        $child_blogs = wp_get_sites();
        $child_blogs = is_array($child_blogs) ? $child_blogs : array();
    
        foreach ($child_blogs as $_child_blog) {
            switch_to_blog($_child_blog['blog_id']);
            $_sql = '
                DELETE FROM `timeouts`, `transients`
                    USING `'.esc_sql($wpdb->options).'` AS `timeouts`
                JOIN `'.esc_sql($wpdb->options).'` `transients` ON `transients`.`option_name` = REPLACE(`timeouts`.`option_name`, \'_timeout\', \'\')
                WHERE (`timeouts`.`option_name` LIKE \''.esc_sql($_transient_timeout_).'%\' OR `timeouts`.`option_name` LIKE \''.esc_sql($_site_transient_timeout_).'%\')
                    AND CAST(`timeouts`.`option_value` AS UNSIGNED) < \''.esc_sql($time).'\'';
            $counter += (int) $wpdb->query(trim($_sql));
        }
        unset($_child_blog, $_sql); // Housekeeping.
    
        restore_current_blog();
    
        return $counter;
    };
    
    /*
    * Automatically clears expired transients.
    *
    * @since 15xxxx Adding support for expired transients.
    *
    * @param bool $manually True if clearing is done manually.
    * @param boolean $maybe Defaults to a true value.
    *
    * @throws \Exception If a clear failure occurs.
    *
    * @return int Total DB rows cleared by this routine (if any).
    */
    $self->clearExpiredTransients = function ($manually = false, $maybe = true) use($self) {
        $counter = 0; // Initialize.
    
        if (!$self->options['enable']) {
            return $counter; // Nothing to do.
        }
        if ($maybe && !$self->options['cache_clear_transients_enable']) {
            return $counter; // Not enabled at this time.
        }
        $time                     = time(); // Current UTC time.
        $wpdb                     = $self->wpdb(); // WP database class.
        $_transient_timeout_      = $wpdb->esc_like('_transient_timeout_');
        $_site_transient_timeout_ = $wpdb->esc_like('_site_transient_timeout_');
    
        $sql = '
            DELETE FROM `timeouts`, `transients`
                USING `'.esc_sql($wpdb->options).'` AS `timeouts`
            JOIN `'.esc_sql($wpdb->options).'` `transients` ON `transients`.`option_name` = REPLACE(`timeouts`.`option_name`, \'_timeout\', \'\')
            WHERE (`timeouts`.`option_name` LIKE \''.esc_sql($_transient_timeout_).'%\' OR `timeouts`.`option_name` LIKE \''.esc_sql($_site_transient_timeout_).'%\')
                AND CAST(`timeouts`.`option_value` AS UNSIGNED) < \''.esc_sql($time).'\'';
    
        $counter += (int) $wpdb->query(trim($sql));
    
        return $counter;
    };
    /*[/pro]*/
  • After this line add the following:

    /*[pro strip-from="lite"]*/
    /**
    * Action handler.
    *
    * @since 15xxxx Adding transient cache wipe handler.
    *
    * @param mixed Input action argument(s).
    */
    protected function ajaxWipeExpiredTransients($args)
    {
        if (!$this->plugin->currentUserCanWipeExpiredTransients()) {
            return; // Not allowed to clear.
        }
        if (empty($_REQUEST['_wpnonce']) || !wp_verify_nonce($_REQUEST['_wpnonce'])) {
            return; // Unauthenticated POST data.
        }
        $counter = $this->plugin->wipeExpiredTransients(true, false);
    
        $response = sprintf(__('<p>Expired transients wiped successfully.</p>', SLUG_TD), esc_html(NAME));
        $response .= sprintf(__('<p>Wiped <code>%1$s</code> expired rows from the database.</p>', SLUG_TD), esc_html($counter));
    
        exit($response); // JavaScript will take it from here.
    }
    /*[/pro]*/
    
    /*[pro strip-from="lite"]*/
    /**
    * Action handler.
    *
    * @since 15xxxx Adding transient cache clear handler.
    *
    * @param mixed Input action argument(s).
    */
    protected function ajaxClearExpiredTransients($args)
    {
        if (!$this->plugin->currentUserCanClearExpiredTransients()) {
            return; // Not allowed to clear.
        }
        if (empty($_REQUEST['_wpnonce']) || !wp_verify_nonce($_REQUEST['_wpnonce'])) {
            return; // Unauthenticated POST data.
        }
        $counter = $this->plugin->clearExpiredTransients(true, false);
    
        $response = sprintf(__('<p>Expired transients cleared successfully.</p>', SLUG_TD), esc_html(NAME));
        $response .= sprintf(__('<p>Cleared <code>%1$s</code> expired rows from the database.</p>', SLUG_TD), esc_html($counter));
    
        exit($response); // JavaScript will take it from here.
    }
    /*[/pro]*/
  • After this line add the following:

    /*[pro strip-from="lite"]*/
    'ajaxWipeExpiredTransients',
    'ajaxClearExpiredTransients',
    /*[/pro]*/
  • After this line add the following:

    /*
    * Current user can clear expired transients?
    *
    * @since 15xxxx Enhancing user permissions.
    *
    * @return boolean Current user can clear expired transients?
    */
    $self->currentUserCanClearExpiredTransients = function () use ($self) {
        if (!is_null($can = &$self->cacheKey('currentUserCanClearExpiredTransients'))) {
            return $can; // Already cached this.
        }
        $is_multisite = is_multisite();
    
        if (!$is_multisite && current_user_can($self->cap)) {
            return ($can = true); // Plugin admin.
        }
        if ($is_multisite && current_user_can($self->network_cap)) {
            return ($can = true); // Plugin admin.
        }
        return ($can = false);
    };
    $self->currentUserCanWipeExpiredTransients = $self->currentUserCanClearExpiredTransients;
  • After this line add the following:

    if ($self->currentUserCanClearExpiredTransients()) {
        $cache_clear_options .= '<li class="-transients-only"><a href="#" title="'.__('Clear expired transients from the database', SLUG_TD).'">'.__('Expired Transients', SLUG_TD).'</a></li>';
    }
  • After this line add the following:

    $('#wp-admin-bar-' + plugin.namespace + '-clear-options-wrapper .-transients-only > a').on('click', plugin.clearExpiredTransientsOnly);
  • After this line add the following:

    } else if (o.transientsOnly) {
      isClearOption = true;
      postVars[plugin.namespace] = {
        ajaxClearExpiredTransients: '1'
      };
  • Replace this line with the following:

    cdnOnly: false,
    transientsOnly: false
  • After this line add the following:

    plugin.clearExpiredTransientsOnly = function (event) {
      plugin.clearCache(event, {
        transientsOnly: true
      });
    };
  • Compress admin-bar.js into admin-bar.min.js

  • After this line add a new option key.

    'cache_clear_transients_enable' => '0', // `0|1`
  • Test your work to see if the new option shows up in the admin bar and that it works as expected.

  • Submit PR.

@jaswrks
Copy link

@jaswrks jaswrks commented Nov 5, 2015

@kristineds While working on this issue you can take note of the way ZenCache checks a whitelist of allowable actions in this line. That's why it is necessary for you to add the following, as seen in the outline above.

/*[pro strip-from="lite"]*/
'ajaxWipeExpiredTransients',
'ajaxClearExpiredTransients',
/*[/pro]*/

This works the same way in Comment Mail.

kristineds added a commit to wpsharks/comet-cache-pro that referenced this issue Nov 10, 2015
kristineds added a commit to wpsharks/comet-cache-pro that referenced this issue Nov 20, 2015
kristineds added a commit to wpsharks/comet-cache-pro that referenced this issue Nov 21, 2015
jaswrks pushed a commit to wpsharks/comet-cache-pro that referenced this issue Nov 23, 2015
raamdev added a commit to wpsharks/comet-cache-pro that referenced this issue Nov 26, 2015
@raamdev
Copy link
Contributor Author

@raamdev raamdev commented Nov 27, 2015

@jaswsinc writes...

Next Actions

  • Automatically cleanup expired transients. Enabled by default with an option to turn this off if desirable for some reason. Can't think of one though!
  • Forcibly delete all transients when the cache is cleared manually. Off by default, but site owners can choose to turn this on. This could negatively impact some plugins in very small ways, but overall this is relatively safe. Transient data is transient. Plugins using the transient API should not expect this data to live forever anyway.

Does the behavior introduced by wpsharks/comet-cache-pro#173 match the above? I don't think it does, but I wanted to double-check.

AFAIK, wpsharks/comet-cache-pro#173 only adds a new menu item to the Clear Cache options menu in the Admin Bar that allows site owners to clear Expired Transients, correct?

Is there any reason we don't want to have Expired Transients cleared automatically when clicking the "Clear Cache" button to manually clear the cache, or why we don't want to force-delete all transients, as you described in the Next Actions? Or any reason why we don't want to have options for either/both of those?

@jaswrks
Copy link

@jaswrks jaswrks commented Nov 27, 2015

AFAIK, wpsharks/comet-cache-pro#173 only adds a new menu item to the Clear Cache options menu in the Admin Bar that allows site owners to clear Expired Transients, correct?

Correct.

Is there any reason we don't want to have Expired Transients cleared automatically when clicking the "Clear Cache" button to manually clear the cache, or why we don't want to force-delete all transients, as you described in the Next Actions? Or any reason why we don't want to have options for either/both of those?

No. I agree with you.

@raamdev
Copy link
Contributor Author

@raamdev raamdev commented Nov 27, 2015

I agree with you.

In that case, I think a new Options Panel is in order (WP Transients).

There are entire plugins (see this screenshot) dedicated to managing WP Transients and it would probably add a lot of value to ZenCache Pro to have a new options panel with three buttons to "Delete Expired Transients", "Delete Transients with an Expiration", and "Delete All Transients", along with options for automatically having ZenCache do one of those things when clicking "Clear Cache". (I wouldn't bother with actually listing Transients and allowing a site owner to delete them selectively--for that they should just use a plugin like the one I just linked to).

It would also be nice to have a new ZenCache API call that allows you to run those three commands, so that a site owner could include one when building a Custom Clear Cache Command.

All of the above work could probably be punted to a future release so that we can close this GitHub issue and so that this work doesn't hold up the next release.

@jaswrks
Copy link

@jaswrks jaswrks commented Nov 28, 2015

All of the above work could probably be punted to a future release so that we can close this GitHub issue and so that this work doesn't hold up the next release.

Sounds like a really great idea to me. I also agree on the football strategy.

@raamdev
Copy link
Contributor Author

@raamdev raamdev commented Nov 28, 2015

@jaswsinc I've forked the additional features to #626.

@raamdev
Copy link
Contributor Author

@raamdev raamdev commented Nov 28, 2015

Next Pro Release Changelog:

  • New Feature! It's now possible to clear Expired WordPress Transients from the Clear Cache Option Menu in the WordPress Admin Bar. The WordPress Transients API has no functionality to automatically clean up expired transients; doing so is left for plugin authors and we've found that very few plugins that use the Transient API actually clean up expired transients properly, which can lead to your database being full of expired transient data that is no longer used. Props @kristineds. See Issue #459.
@raamdev raamdev closed this Nov 28, 2015
@lkraav
Copy link

@lkraav lkraav commented Nov 28, 2015

doing so is left for plugin authors and we've found that very few plugins that use the Transient API actually clean up expired transients properly

Can you elaborate on "properly"? I am aware of at least two strong plugins from prominent authors, where I am not aware of anything being done improperly. The above sounds like a randomish claim type of thing.

@raamdev
Copy link
Contributor Author

@raamdev raamdev commented Nov 28, 2015

@lkraav By default, old transients are only deleted when requested again (after they've expired), so if a plugin uses the WP Transients API to create transients with an expiration, but then doesn't request them after the expiration, those expired transients will live on forever in the database (i.e., WordPress doesn't periodically clean up expired transients).

Plugins authors need to take that behavior into account when using the Transients API and clean up their own expired transients. If they only rely on a future request for the transients to clean them up, then uninstalling the plugin could leave behind expired transients (the plugin author would need to make removing expired transients part of the uninstallation process, which many do not). So, manually cleaning up all Expired Transients for an entire WordPress site on a regular basis is a good idea to keep the database cleaner.

@lkraav
Copy link

@lkraav lkraav commented Nov 28, 2015

Oh, you're talking about plugins in general. For some reason I read the original text as "Transient cleaning plugins are mostly doing things improperly..." (examples: https://wordpress.org/plugins/transients-manager, https://wordpress.org/plugins/delete-expired-transients/)

OK, all clear.

@raamdev
Copy link
Contributor Author

@raamdev raamdev commented Nov 28, 2015

you're talking about plugins in general

Right. 😄 No worries. That changelog entry could've been a bit more clear.

@wpsharks wpsharks locked and limited conversation to collaborators Dec 21, 2015
@raamdev
Copy link
Contributor Author

@raamdev raamdev commented Dec 21, 2015

ZenCache Pro v151220 has been released and includes changes worked on as part of this GitHub Issue. See the release announcement for further details.


This issue will now be locked to further updates. If you have something to add related to this GitHub Issue, please open a new GitHub Issue and reference this one (#459).

Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Projects
None yet
Linked pull requests

Successfully merging a pull request may close this issue.

None yet
4 participants