php.net |  support |  documentation |  report a bug |  advanced search |  search howto |  statistics |  random bug |  login
Bug #80412 PHP-FPM : PDO / Broken Pipe when DBMS ends the connection
Submitted: 2020-11-24 23:00 UTC Modified: 2020-11-26 11:55 UTC
From: heavy-traffic-website at yopmail dot com Assigned: nikic (profile)
Status: Closed Package: PDO Core
PHP Version: 7.4.12 OS: Debian 11
Private report: No CVE-ID: None
View Developer Edit
Welcome! If you don't have a Git account, you can't do anything here.
If you reported this bug, you can edit this bug over here.
(description)
Block user comment
Status: Assign to:
Package:
Bug Type:
Summary:
From: heavy-traffic-website at yopmail dot com
New email:
PHP Version: OS:

 

 [2020-11-24 23:00 UTC] heavy-traffic-website at yopmail dot com
Description:
------------
Related to this very old bug reported 10 years ago
=> https://bugs.php.net/bug.php?id=53287


(johannes@php.net) was wrong - the bug still exists and no one fixed it.
--> Please consider the bug onto the PHP side, not the DBMS side.

------------------------

new PDO("mysql:host=127.0.0.1;dbname=XXX", "XXX", "XXX", [ PDO::ATTR_PERSISTENT => true ]);

PHP-FPM have threads with broken persistent connections and SHOULD try to reconnect a new one connection rather than CRASHING an exception in the case where send of 5 bytes failed with errno=32 Broken pipe appens in __construct.

Test script:
---------------
With php7.4-fpm, i have some threads and when i have a wide blank time between DBMS and PHP, probably the timeout of the DBMS break the pipe (default: 8h), however PHP/PDO don't care and throw an exception instead of making a new one connection, inside each thread alive.

Currently, I have to try{ new PDO() }catch(){ new PDO() } to be working without error, it sucks !

Expected result:
----------------
The PDO side CAN be patched, so this is a bug. It is expected that PDO will try to reconnect silently if the case described appears, before lauching any exception. And if any exception has to be launched, it will certainly will not be these one but the new connection exception.

Actual result:
--------------
Catched Exception :

PDO::__construct(): send of 5 bytes failed with errno=32 Broken pipe

Patches

Pull Requests

History

AllCommentsChangesGit/SVN commitsRelated reports
 [2020-11-24 23:03 UTC] daverandom@php.net
Note that this would be per-driver behaviour, specific to the RDBMS being used rather than a generic PDO problem. Any further information on the RDBMS system/version affected would be useful - obviously the DSN used with PDO indicates that MySQL is affected by this.
 [2020-11-24 23:06 UTC] daverandom@php.net
Note also that there is no implied re-connect on remote connection drop in any PHP-RDBMS drivers (afaik). It is generally assumed that reconnection would be a thing that should be dealt with in userland, as there is no universal strategy for interval, back-off or max retry count.
 [2020-11-24 23:23 UTC] heavy-traffic-website at yopmail dot com
Thank you for your review.

The code PDO::__contruct() is very explicit : I want a persistent connection. And this line works until the pipe exists.

Maybe not everybody use HAproxy between Galera Cluster and PHP-FPM but, the code to keep it working without error is really poor :
try{ new PDO() }catch(){ new PDO() }  

Please don't justify this is normal, when the pipe broke whatever the reason is : PHP/PDO should DETECT the broken PIPE and treat the __construct like no one persistent connection exists yet inside each thead.

We are on the ends of 2020, it has to think logic and serve the langage, not saying like 10 year ago : it is still a feature.
 [2020-11-24 23:38 UTC] heavy-traffic-website at yopmail dot com
It doesn't need any strategy or interval or whatever. Just to connect like the first persistent connexion when the pipe is broken. That's all.

Independantly what is the DBMS used, even if Haproxy break the pipe.
 [2020-11-25 08:44 UTC] nikic@php.net
Just a note, if you see that "broken pipe" error, that probably means you have a broken error handler implementation somewhere. This error is suppressed internally (error_reporting=0), but an incorrectly implemented error handler that does not check the error_reporting level may make it visible again.
 [2020-11-25 14:24 UTC] heavy-traffic-website at yopmail dot com
Thank you for your note.

The debate is not about the log itself, independantly the state of error_reporting is. For developpment, mine is set to E_ALL and works fine, there is no error implementing that.

PHP can procude any warning that he wants, but fail to __contruct PDO after a broken pipe IS NOT A NORMAL sence or wanted. You cannot tell each pipe connection will never end, so you made a mistake in your configuration... or that is a desired effect.

There is no error somewhere due to the other parties. It can happen of a pipe is broken due to long inactivity ; do network activities and you will know. Softwares have to retry retablishing the pipe BEFORE throwing any error : it should be the same with PHP/PDO core.

If PHP/PDO fails to send data ; it is because the pipe onto the thread is cached (due to the persistance), and reused like it should be always be working, it is incorrect.

When PHP/PDO have a "send of 5 bytes failed with errno=32", you should flush the cache about persistent connexion then RETRYING __construct with persistant connection LIKE it has been a new thread without cached yet.

If an error had to be throwed, it can be adjusted :
- "send of 5 bytes failed with errno=32", moreover, "server has gone away".
- "send of 5 bytes failed with errno=32", moreover, "[last connection error]".

This error will be consistent. But to let a thread with inconsistent broken pipe cached, is not consistent neither wanted neither normal. By the way, you can't have 2 identical "Pipe error" when retrying connection. So, it is logic to retry ONCE a persistent broken pipe.
 [2020-11-25 15:22 UTC] heavy-traffic-website at yopmail dot com
Some precisons :

ini_set("error_reporting", (string) E_ALL);
ini_set("display_errors", "On");
ini_set("display_startup_errors", "On");
ini_set("track_errors", "On");

set_exception_handler("my_exception");
set_error_handler("my_error");


[ERROR HANDLER]
errno: int(8)
errstr: string(68) "PDO::__construct(): send of 5 bytes failed with errno=32 Broken pipe"
errfile: string(31) "/var/www/XXX/Database.class.php"
errline: int(28)


line 27 --> $db = new PDO("mysql:host=127.0.0.1;dbname=XXX", "XXX", "XXX",
line 28 --> PDO::ATTR_PERSISTENT => true,
line 29 --> PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION]);
 [2020-11-25 20:01 UTC] heavy-traffic-website at yopmail dot com
Description:
------------
Related to this very old bug reported 10 years ago
=> https://bugs.php.net/bug.php?id=53287

(johannes@php.net) was wrong - the bug still exists and no one fixed it.
--> Please consider the bug onto the PHP side, not the DBMS side.

PHP (php7.4-fpm) have threads with broken persistent connections cached with PDO and this should try to reconnect a new one connection rather than calling the error handler in the case where the pipe is already broken, during opening a nwe connection using persistence. Maybe there is a leak, why don't you free a broken pipe and try to reuse it ?

Also, the PDO::_construct() return a PDO objet (without throwing an Exception), but PHP rises an E_NOTICE :
string(68) "PDO::__construct(): send of 5 bytes failed with errno=32 Broken pipe"
That seems to deal with a E_CORE_WARNING ; generated by the core of PHP.

Currently, there is NO SOLUTION UNLESS FORGET PERSITANCE, it sucks !


Test script:
---------------

ini_set("error_reporting", (string) E_ALL);
ini_set("display_errors", "On");
ini_set("display_startup_errors", "On");
ini_set("track_errors", "On");

function my_error($errno, $errstr, $errfile, $errline)
{
	var_dump("------------[ ERROR ]-------------"); 
	var_dump("-- errstr: " . $errstr);
	var_dump("----------------------------------");
}

set_error_handler("my_error");

abstract class Database
{
    private static ?PDO $_db = null;
   
    final public static function test() : ?PDO
    {
        for ($i = 1 ; $i <=6 ; ++$i)
        {
            var_dump("<BEFORE new PDO(), loop $i>");
			try {
				self::$_db = new PDO("mysql:host=127.0.0.1;dbname=XXX",
					 "XXX", "XXX", [
							 PDO::ATTR_PERSISTENT => true,
							 PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION,
							]);
			}
			catch (PDOException $e) {
				var_dump("----------[ EXCEPTION ]-----------"); 
				var_dump("-- excstr: " . $e->getMessage());
				var_dump("----------------------------------");	
			}
		
            var_dump("<AFTER new PDO(), loop $i>")
            var_dump(self::$_db);
            var_dump("<PING select, loop $i>");
            var_dump(self::$_db->query("SELECT 1")->rowCount());
        }
        return self::$_db;
    }
}

Database::test();


Expected result:
----------------

string(26) "<BEFORE new PDO(), loop 1>"
string(25) "<AFTER new PDO(), loop 1>"
object(PDO)#2 (0) {
}
string(21) "<PING select, loop 1>"
int(1)

string(26) "<BEFORE new PDO(), loop 2>"
string(25) "<AFTER new PDO(), loop 2>"
object(PDO)#4 (0) {
}
string(21) "<PING select, loop 2>"
int(1)

string(26) "<BEFORE new PDO(), loop 3>"
string(25) "<AFTER new PDO(), loop 3>"
object(PDO)#3 (0) {
}
string(21) "<PING select, loop 3>"
int(1)

string(26) "<BEFORE new PDO(), loop 4>"
string(25) "<AFTER new PDO(), loop 4>"
object(PDO)#2 (0) {
}
string(21) "<PING select, loop 4>"
int(1)

string(26) "<BEFORE new PDO(), loop 5>"
string(25) "<AFTER new PDO(), loop 5>"
object(PDO)#4 (0) {
}
string(21) "<PING select, loop 5>"
int(1)

string(26) "<BEFORE new PDO(), loop 6>"
string(25) "<AFTER new PDO(), loop 6>"
object(PDO)#3 (0) {
}
string(21) "<PING select, loop 6>"
int(1)


Actual result:
--------------

< I AM DOING SOME TESTS NOW >
< Please wait for an update >
 [2020-11-26 10:52 UTC] heavy-traffic-website at yopmail dot com
Actual result:
--------------

string(26) "<BEFORE new PDO(), loop 1>"
string(34) "------------[ ERROR ]-------------"
string(79) "-- errstr: PDO::__construct(): send of 5 bytes failed with errno=32 Broken pipe"
string(34) "----------------------------------"
string(25) "<AFTER new PDO(), loop 1>"
object(PDO)#2 (0) {
}
int(1)
string(26) "<BEFORE new PDO(), loop 2>"
string(25) "<AFTER new PDO(), loop 2>"
object(PDO)#3 (0) {
}
int(1)
string(26) "<BEFORE new PDO(), loop 3>"
string(25) "<AFTER new PDO(), loop 3>"
object(PDO)#2 (0) {
}
int(1)
string(26) "<BEFORE new PDO(), loop 4>"
string(25) "<AFTER new PDO(), loop 4>"
object(PDO)#3 (0) {
}
int(1)
string(26) "<BEFORE new PDO(), loop 5>"
string(25) "<AFTER new PDO(), loop 5>"
object(PDO)#2 (0) {
}
int(1)
string(26) "<BEFORE new PDO(), loop 6>"
string(25) "<AFTER new PDO(), loop 6>"
object(PDO)#3 (0) {
}
int(1)
 [2020-11-26 10:59 UTC] heavy-traffic-website at yopmail dot com
-Status: Open +Status: Closed
 [2020-11-26 10:59 UTC] heavy-traffic-website at yopmail dot com
Ok, so, i now understand, seeing lot of PHP INTERNAL errors, but PDO still works.

nikic@php.net ; I supposed you have right.

Then, this was just surprising me, i thought PDO was not working fine but i seems it is. I will suppress this error from the logs, and go on like that...

Thank you for support.
 [2020-11-26 11:55 UTC] nikic@php.net
-Assigned To: +Assigned To: nikic
 [2020-11-26 11:55 UTC] nikic@php.net
I've put up https://github.com/php/php-src/pull/6458 to suppress the broken pipe error more thoroughly.
 [2020-11-26 22:22 UTC] heavy-traffic-website at yopmail dot com
I saw it and approve, thank you !
 
PHP Copyright © 2001-2024 The PHP Group
All rights reserved.
Last updated: Wed Oct 09 09:01:26 2024 UTC