[wp-trac] [WordPress Trac] #62176: No stable error detection after Automatic Updates with more than one update
WordPress Trac
noreply at wordpress.org
Sun Oct 6 18:19:12 UTC 2024
#62176: No stable error detection after Automatic Updates with more than one update
-----------------------------+-----------------------------
Reporter: georgwordpress | Owner: (none)
Type: defect (bug) | Status: new
Priority: normal | Milestone: Awaiting Review
Component: Upgrade/Install | Version: 6.6
Severity: normal | Keywords: 2nd-opinion
Focuses: |
-----------------------------+-----------------------------
While testing rollback of buggy plugins updates I found some times the
buggy plugin was not detected.
But even with the same test setup this happens only sometimes.
In {{{class-wp-automatic-updater.php}}} a risk of a racing condition is
already noticed as a comment.
To lower the risk a sleep(2) was introduced as a quick fix.
In another discussion @stoyangeorgiev added a comment:
"... another thing we found is that when you update two plugins that have
fatal errors, It may be possible that the checks for enabling/disabling
maintenance mode in load.php may not be properly getting the state of the
maintenance mode. I've managed to replicate it on my setup ... I'll try to
test it more so I can find a stable way to replicate it."
After digging deep into the related functions I think I have found the
problem.
The detection of errors after automatic plugin updates is done with a
loopback request to the homepage.
While a user request is still answered with the maintenance screen this
loopback request must bypass the maintenance mode to receive his dedicated
scraping result produced by another function.
There are two very ancient WP functions related to the wp maintenance
mode:
**1.) the function {{{maintenance_mode()}}} **
Was introduced in WP over 15 years ago.
This function creates a new PHP script every time it is called, which is
saved as the well-known .maintenance file.
Here the greatly simplifies file operations of this function:
{{{
@unlink( '.maintenance' );
$fp = @fopen( '.maintenance', 'wb' );
fwrite( $fp, '<?php $upgrading = ' . time() . '; ?>' );
fclose( $fp );
}}}
Nothing complicated - but there is a racing condition:
If in parallel another loading process accesses the maintencance file
exactly between fopen and fwrite, the file is still empty.
This hasn't been a problem for the last 15 years because the other
maintenance function has been very fault tolerant.
**2.) the function {{{wp_is_maintenance_mode()}}}**
{{{
global $upgrading;
if (file_exists(ABSPATH . '.maintenance') ) require ABSPATH .
'.maintenance';
}}}
At this point, it can happen that the maintenance file already exists but
does not yet contain any PHP code - so the value of {{{$upgrading}}} is
not updated.
But thanks to {{{$upgrading}}} has already been declared as global the
next line of code is running without any problems.
{{{
if ( ( time() - $upgrading ) >= 10 * MINUTE_IN_SECONDS ) return false;
}}}
Even with {{{$upgrading}}} beeing NULL when this edge case happens - no
php errors - simply return false.
As result: this edge request at the beginning of a maintence phase is not
answered with the maintenance screen.
Does it matter?
No - because putting wp into maintenance mode is only a preparation before
any changes and updates will really take place. For more than 15 years
nobody complains.
What has changed?
Some times ago the automatic updates of core/themes etc was introduced.
In the of loop of Automatic Updates the function {{{maintenance_mode}}} is
called a lot of times now.
(Before it enters the plugins section the function {{{maintenance_mode}}}
is called even twice in every round.)
And as mentioned above with every call of {{{maintenance_mode}}} the
sequence of {{{@unlink/@fopen/fwrite/fclose}}} is deleting and writing the
maintenance file.
So the risk becomes higher that another request could hit the gap betweens
fopen and fwrite.
But still no problems.
**Until in WP 6.6 the {{{Rollback part 3}}} comes with three related
changes:
**
1.) in the old function {{{wp_is_maintenance_mode()}}} a new, additional
condition check was introduced.
The current value of {{{$upgrading}}} is now crucial in this new check.
A new function {{{has_fatal_error}}} was introduced with
2.) require ABSPATH . '.maintenance', so the value of {{{$upgrading}}} as
global was now updated at a second place - and during an update loop every
time when a plugin was updated.
3.) the value of {{{$upgrading}}} was used as part of the loopback request
So - in the first round of an update the loopback request will bypass the
maintenance screen in every case:
- the value of {{{$upgrading}}} used in the loopback request is the same
as expected in the new maintenance bypass condition.
- Even if the edge case happens the 15 Year old bail out is ensuring the
maintenance bypass - so the new condition is not checked in this edge
case.
But after the first round of an update under some racing conditions it
could happen that the value of {{{$upgrading}}} used in the loopback
request does not match the new maintenance bypass check.
Due to {{{$upgrading}}} to be declared global it is possible, that after
the first round {{{$upgrading}}} has already a value, even when
{{{require}}} is loading a empty maintenance file.
As a result at the long end the function {{{has_fatal_error}}} returns
false ==> no errors found - without making a full assessment of the
loopback request.
In this PR the use of {{{$upgrading}}} in the new functions was
removed/replaced.
And the sleep(2) was also removed.
@afragen, @costdev, @stoyangeorgiev
Do you think I'm on the right path with this PR?
Follow-up to [https://core.trac.wordpress.org/changeset/58128]
See [https://core.trac.wordpress.org/ticket/58281]
--
Ticket URL: <https://core.trac.wordpress.org/ticket/62176>
WordPress Trac <https://core.trac.wordpress.org/>
WordPress publishing platform
More information about the wp-trac
mailing list