Make WordPress Core

Opened 7 years ago

Closed 8 weeks ago

#44118 closed defect (bug) (fixed)

WordPress performs some unnecessary plugin update checks

Reported by: siliconforks's profile siliconforks Owned by: johnbillion's profile johnbillion
Milestone: 6.8 Priority: normal
Severity: normal Version:
Component: Upgrade/Install Keywords: has-patch commit
Focuses: Cc:

Description

In the function wp_update_plugins(), it seems clear that the intent is not to check for updates if there has already been an update check recently (e.g., on the wp-admin/plugins.php page, "recently" means within the last hour). However, in some cases it will check for updates anyway because of subtle issues in the implementation.

The easiest way to reproduce the issue is to install the Query Monitor plugin (to view HTTP requests) and then perform the following steps:

  1. Visit the admin section "Updates" page (wp-admin/update-core.php). Look at Query Monitor's "HTTP API Calls" - you should see a call to https://api.wordpress.org/plugins/update-check/1.1/ to check for plugin updates. (You might also see additional HTTP API calls for theme and core update checks.) Note: if you don't see the plugin update check, it's possible that there was already a check for updates in the last minute - update-core.php is supposed to check for updates no more than once per minute. Try waiting a minute and reloading the page.
  1. Once you have seen the plugin update check being made, visit the "Plugins" page (wp-admin/plugins.php). Look at "HTTP API Calls" - you will see another plugin update check being made. This doesn't make much sense, because WordPress just checked for plugin updates on the update-core.php page.
  1. Note that the additional update check in only occurs once. Try reloading the plugins.php page and look at "HTTP API Calls" again - this time there will be no plugin update check (which is the expected behavior, since WordPress just checked for plugin updates).

Looking at the code for wp_update_plugins(), it seems that the update_plugins transient is sometimes stored with a checked property, sometimes not. This doesn't make sense to me - when the checked property is not stored in the transient, the next call to wp_update_plugins() will always check for updates regardless of how recently the last update check occurred.

Attachments (4)

fix-update_plugins-transient.patch (1.1 KB) - added by siliconforks 7 years ago.
proposed fix
44118.diff (1.5 KB) - added by pbiron 4 years ago.
refreshing patch
44118-after-update-uri-header.diff (1.4 KB) - added by siliconforks 4 years ago.
Refreshed patch to work after r50921 (which added support for the Update URI header)
44118-after-coding-standards-change.diff (1.4 KB) - added by siliconforks 2 years ago.
Refreshed patch to work after r54891 (which modified coding standards to require parentheses for calls to constructors)

Download all attachments as: .zip

Change History (24)

#1 @pento
6 years ago

  • Keywords has-patch added
  • Version trunk deleted

#2 @johnbillion
6 years ago

  • Component changed from Plugins to Upgrade/Install
  • Keywords needs-testing added
  • Milestone changed from Awaiting Review to Future Release

Thanks for the report and the patch @siliconforks . Sorry that it's taken so long to get a response.

This patch looks good but it needs some testing and a sanity check by someone familiar with this component.

#3 @bookdude13
5 years ago

  • Keywords 2nd-opinion added; needs-testing removed

Hey all, thanks for the patience on this issue :)

I can confirm that this behavior still exists in trunk as of today. I was also able to get the second request simply refreshing the update page after seeing the first plugin update request. After the second request there were no more until the 1min timeout was reached, then this could be repeated.

The patch applied cleanly and solved the described bug (no more double requests). I am new to this component though, so a sanity check is still needed.

@pbiron
4 years ago

refreshing patch

#4 @pbiron
4 years ago

44118.diff refreshes the patch so that it applies cleanly.

This ticket was mentioned in Slack in #core-auto-updates by francina. View the logs.


4 years ago

#6 @francina
4 years ago

  • Keywords needs-testing added; 2nd-opinion removed
  • Owner set to francina
  • Status changed from new to assigned

#7 @francina
4 years ago

I tested the patch today

  • MacOS 11.4
  • Safari 16611.2.7.1.4
  • wordpress-develop
  • WordPress 5.8-beta1-51132-src

I can reproduce the issues, but the patch doesn't apply. This is the result I get

Hunk #1 succeeded at 151 with fuzz 1 (offset -145 lines).
Hunk #2 FAILED at 182.
Hunk #3 succeeded at 424 with fuzz 1 (offset 8 lines).
1 out of 3 hunks FAILED -- saving rejects to file src/wp-includes/update.php.rej

And here is what I have in the file:

*** 179,184 ****
  		$plugin_changed = false;
  
  		foreach ( $plugins as $file => $p ) {
  			if ( ! isset( $current->checked[ $file ] ) || (string) $current->checked[ $file ] !== (string) $p['Version'] ) {
  				$plugin_changed = true;
  			}
--- 182,189 ----
  		$plugin_changed = false;
  
  		foreach ( $plugins as $file => $p ) {
+ 			$new_option->checked[ $file ] = $p['Version'];
+ 
  			if ( ! isset( $current->checked[ $file ] ) || (string) $current->checked[ $file ] !== (string) $p['Version'] ) {
  				$plugin_changed = true;
  			}

I think the patch needs a refresh :) Thanks!

@siliconforks
4 years ago

Refreshed patch to work after r50921 (which added support for the Update URI header)

@siliconforks
2 years ago

Refreshed patch to work after r54891 (which modified coding standards to require parentheses for calls to constructors)

#8 @siliconforks
2 years ago

The previous patch no longer applied cleanly due to a coding standards change - I've uploaded a refreshed version.

Also, as this bug report approaches its fifth anniversary, it seems that there's not a lot of interest in fixing this issue... I'd just like to point out that I don't think I'm the only WordPress user who would like to see a fix for this; there appears to be some broader concern in the WordPress community that WordPress is performing an excessive number of outgoing HTTP requests. This project aims to make a plugin which will reduce these:

https://www.cloudfest.com/eco-mode-reduce-outgoing-network-traffic-of-your-wordpress-server

That plugin might indeed be useful; however, I think it would be even better to fix any issue in the WordPress core which may be causing it to perform unnecessary HTTP requests in the first place...

#9 @desrosj
6 months ago

  • Milestone changed from Future Release to 6.8

Moving to 6.8 to try and resolve this with #61055.

#10 @johnbillion
2 months ago

  • Owner francina deleted

This ticket was mentioned in Slack in #core by audrasjb. View the logs.


2 months ago

#12 @johnbillion
2 months ago

  • Owner set to johnbillion
  • Status changed from assigned to reviewing

This ticket was mentioned in Slack in #core-test by krupajnanda. View the logs.


2 months ago

#14 follow-up: @johnbillion
8 weeks ago

There is a subtle difference between the patch on this ticket and the PR on #61055, and I think we should go with the latter.

@siliconforks @Cybr @snehapatil02 Does that sound correct to you?

#15 in reply to: ↑ 14 @siliconforks
8 weeks ago

Replying to johnbillion:

There is a subtle difference between the patch on this ticket and the PR on #61055, and I think we should go with the latter.

Yes, there is definitely a difference there, but isn't that a bug with https://github.com/WordPress/wordpress-develop/pull/6736 ?

Here is a scenario to illustrate:

  1. Suppose that https://github.com/WordPress/wordpress-develop/pull/6736 is applied, and suppose that there is one plugin installed - classic-editor, version 1.6.7. Suppose that the update_plugins transient has a checked property which contains ['classic-editor/classic-editor.php' => '1.6.7']. (Nothing out of the ordinary so far.)
  1. Install another plugin, the classic-widgets plugin, manually, by unpacking a .zip file. (No need to activate it.)
  1. Wait at least one minute and visit /wp-admin/update-core.php. This will call wp_update_plugins(). The $time_not_changed variable will be false. This means that all the code in the if ( $time_not_changed && ! $extra_stats ) statement will not be executed. So that means it's not going to bail out - it's going to proceed with the API call to https://api.wordpress.org/plugins/update-check/1.1/ and it will submit the following data:
    ...
    'classic-editor/classic-editor.php' => [
      ...
      'Version' => '1.6.7',
      ...
    ],
    'classic-widgets/classic-widgets.php' => [
      ...
      'Version' => '0.3',
      ...
    ],
    ...
    
  1. So far everything is working fine, but the value that will be saved in the update_plugins transient checked property is: ['classic-editor/classic-editor.php' => '1.6.7']. In other words, it's saving the same value that was there previously.
  1. Now suppose that the next time wp_update_plugins() gets called, the timeout has not expired, and the $time_not_changed variable is set to true, and it will execute the code in the if statement. It will loop through all the installed plugins, eventually hitting the classic-widgets plugin. Because this is not in $current->checked, the $plugin_changed variable will be set to true, and it's going to proceed with the API call again. It will submit the following data:
    ...
    'classic-editor/classic-editor.php' => [
      ...
      'Version' => '1.6.7',
      ...
    ],
    'classic-widgets/classic-widgets.php' => [
      ...
      'Version' => '0.3',
      ...
    ],
    ...
    

But that's the same thing it submitted last time. It's making the same API call it did last time, and the timeout has not expired, so we're basically back to the same issue - WordPress is still performing some unnecessary plugin update checks. Not as many as before, but there are still situations like this where it's making the API call unnecessarily.

I don't really understand this. The patch is adding all plugins to the $updates->checked property, which will include any plugins which have been added or manually updated since the last update check. So this list of plugins should be up-to-date and accurate, and it should be the same as the list of plugins that was submitted in the API call. I think this is what we want? Can you provide a more detailed example of a scenario where this would cause problems?

The problem (as I've illustrated in the scenario above) is that $current->checked may potentially be out of date, and then it gets copied to $updates->checked, so it might potentially be out of date too. This gets saved to the transient, so it's potentially saving old, out-of-date data to the transient.

#16 follow-up: @johnbillion
8 weeks ago

@siliconforks It looks you're right! Thanks a lot for the explanation. I'll carry on testing to confirm for sure, then hopefully we can get this in.

#17 in reply to: ↑ 16 @siliconforks
8 weeks ago

Replying to johnbillion:

@siliconforks It looks you're right! Thanks a lot for the explanation. I'll carry on testing to confirm for sure, then hopefully we can get this in.

Just a suggestion for you or anyone else who is reviewing/testing this: it may be helpful to compare the behavior of wp_update_plugins() to wp_update_themes(). The code for the two functions is very similar - or at least, it was very similar originally, though it's diverged a lot over the years.

The fundamental problem with wp_update_plugins() is that it's essentially trying to jam two different loops into one. Conceptually, there are two separate loops here: there's a loop which goes through all plugins and determines whether any have changed since the last update, and there's another loop which populates the checked array which ultimately gets saved in the transient. The issue with wp_update_plugins() is that it tries to do both these things in a single loop. That doesn't really work here, because the loop which populates the checked array should always be executed, while the loop that determines whether any plugins have changed is sometimes skipped over entirely.

In contrast, if you look at the code for wp_update_themes(), it doesn't have this issue. There are two separate loops, one which populates the checked array, and one which goes through all the themes to see whether any have changed.

My patch basically just attempts to split the code into two separate loops and make the behavior of wp_update_plugins() a little bit more like wp_update_themes().

This ticket was mentioned in PR #8454 on WordPress/wordpress-develop by @johnbillion.


8 weeks ago
#18

#19 @johnbillion
8 weeks ago

  • Keywords commit added; needs-testing removed

Yeah I also noticed that those two functions have diverged. I don't know if it's worth spending time to bring them back inline.

I've been testing this change and I concur that the preferred behaviour is to always push the current full list of plugin data into the changed property otherwise it would go stale from one check to another. Populating this property is correctly preventing the unnecessary update check from being triggered on the Plugins screen when the plugin update transient is otherwise up to date and doesn't require a refresh.

#20 @johnbillion
8 weeks ago

  • Resolution set to fixed
  • Status changed from reviewing to closed

In 59926:

Upgrade/Install: Prevent an unnecessary plugin update check when the plugin update data is up to date.

This ensures the checked property is always populated with the latest plugin data before determining whether to perform an update check. Previously this was only populated when the data was already identified as being stale, which could result in a subsequent unnecessary update check when viewing the Plugins screen.

Props siliconforks, bookdude13, pbiron, francina, Cybr, snehapatil02, johnbillion

Fixes #44118, #61055

Note: See TracTickets for help on using tickets.