Ulf Wendel

PHP/C: Does MYSQL[I]_OPT_CONNECT_TIMEOUT work?

C and PHP MySQL clients can set a connection timeout before a connection is established to MySQL. The MySQL C API manual states about MYSQL_OPT_CONNECT_TIMEOUT , which is equal to PHPs MYSQLI_OPT_CONNECT_TIMEOUT:

MYSQL_OPT_CONNECT_TIMEOUT

Connect timeout in seconds.


From: http://dev.mysql.com/doc/refman/5.1/en/mysql-options.html

That is half of the story. The actual behaviour depends on the library you use, the operating system and the transport protocol (TCP/IP, Unix domain sockets, Windows named pipes, Shared Memory).

C users can choose between the MySQL Client Library, which ships with the MySQL server, and the MySQL Connector/C, a new standalone version of the client library. PHP users have a third library option. PHP users can also choose to use the MySQL native driver for PHP (mysqlnd). All three libraries create TCP/IP connections differently resulting in different behaviour.

This article focusses on PHP and TCP/IP. Nevertheless it may be a valuable read for C developers, because PHP itself is written in C. Therefore, PHP inherits all limitations of the underlying MySQL Client Library, just like any other C client.

Library Supports connection timeout since TCP/IP timeout works on Windows?
MySQL Client Library MySQL 3.23 (= ever since?) No
MySQL Connector/C 6.0 (= first release) No
mysqlnd PHP 5.3.1 Yes

PHP 5.3.0 vs. PHP 5.3.1

I would have had never cared much about the connection timeout, if we had not gotten bug reports on it in PHP 5.3.0, mainly on Windows.

PHP binaries for Windows offered for download on php.net use mysqlnd by default as of PHP 5.3.0. Before PHP 5.3.0, php.net has used the MySQL Client Library for its Windows binaries.

Unfortunately mysqlnd does not feature the connection timeout setting before PHP 5.3.1. Internally mysqlnd is using PHP streams. Without setting a connection timeout, mysqlnd defaults to PHP’s default timeout for socket based streams which is 60 seconds default_socket_timeout . As a result any extension using mysqlnd (ext/mysql, ext/mysqli, PDO_MYSQL) may wait up to 60 seconds during the connection establishment. The only way to change this in PHP 5.3.0 is to change the default_socket_timeout setting.


default_socket_timeout = 60
max_execution_time = 30

Note that the default socket timeout is twice the maximum execution time. That PHP manual explains why this is not a misconfiguration:

Note: The set_time_limit() function and the configuration directive max_execution_time only affect the execution time of the script itself. Any time spent on activity that happens outside the execution of the script such as system calls using system(), stream operations, database queries, etc. is not included when determining the maximum time that the script has been running. This is not true on Windows where the measured time is real.

From: http://de.php.net/manual/en/function.set-time-limit.php

Note the exception for Windows and recall that mysqlnd is a default on Windows…. to make it short: you may get an uncatchable fatal error on Windows with PHP 5.3.0 if mysqlnd defaults to the socket timeout, waits for upto 60 seconds but your standard max_execution_time setting is smaller than default_socket_timeout.

This has been fixed in PHP 5.3.1 by adding a connection timeout feature to mysqlnd.

MYSQLI_OPT_CONNECT_TIMEOUT never worked on Windows for TCP/IP

During the bug verification it turned out that MYSQLI_OPT_CONNECT_TIMEOUT (C: MYSQL_OPT_CONNECT_TIMEOUT) had never worked properly on Windows for TCP/IP connections - see MySQL bug #36225. Nonetheless some PHP scripts make use of it to test the reachability of MySQL servers, for example, remote servers of affordable but slow shared hosting services.

What timeout means

Upon closer inspection more surprises came up. All three libraries (MySQL Client Library, Connector/C, mysqlnd) use the same basic approach to implement a connection timeout but differ in detail. After resolving the host name all three do a non-blocking call to the operating system function connect(). If the operating system is not able to immediately establish a TCP/IP connection the libraries - in general - make use of select() or poll() to check the progress of the connection establishment. This is done because connect() does not allow setting a timeout but select()/poll() do. Once a TCP/IP connection has been established, the libraries start the MySQL protocol handshake with the MySQL server.

All three libraries do not consider the MySQL protocol handshake as part of the connection timeout. Time spent for the handshake cannot be controled using MYSQLI_OPT_CONNECT_TIMEOUT. If your TCP/IP connection is really, really slow and your connection timeout setting is short this may become visible. Other timeout settings exist to control the time spend after a successful connect() and before the library returns to the caller.

The below control-flow diagram tries to illustrate the steps and hints for differences in the implementations. The difference on the socket() call is not of relevance for PHP or C clients. The MySQL Client Library which ships with MySQL 5.1.39 uses a timer thread timer to monitor the runtime of socket(), if the code is run as a server (e.g. embedded server). It is shown for completeness.

Client Library MySQL Differences Timeout
Set timeout = 10 seconds
Timeout = 10 seconds
mysqli_real_connect(host, ...)
socket() !  
Resolve host name !
Non-blocking connect()  
select() or poll() for time control !  
Connection established
Send MySQL protocol greetings
Read greetings
MySQL protocol handshake
MySQL protocol handshake
return
connection established

Name resolution

Name resolution is done differently by the MySQL Client Library (from MySQL 5.1.39), Connector/C 6.0 and by mysqlnd. Name resolution refers to the process of translating human readable host names such as "db23.example.com" or "mymysqlbox" into a network address representation understood by the operating system function connect(). Various operating system calls get used which differ in their support for IPv4 and IPv6 and if or how their behaviour can be impacted by resolver (environment) settings. Not only the functions itself differ. They get used in different ways, which impacts the meaning of the connection timeout.

Library Name resolution calls IPv6 accepted
MySQL Client Library 5.1.39 inet_addr() with runtime fallback to gethostbyname() No
Connector/C 6.0 getaddrinfo() Yes
mysqlnd = PHP streams getaddrinfo() with compile time fallback to inet_aton() Yes

Differences all over

Lets go over all libraries and see how they establish a TCP/IP connection using operating system calls.

The MySQL Client Library (from MySQL 5.1.39) first tries to resolve the hostname using inet_addr() and if it fails it inspects all IPv4 addresses returned by gethostbyname(). There is one pitfalls here. The connection timeout is valid for every connection attempt made: Tmax = n x MYSQLI_OPT_CONNECT_TIMEOUT.

Library Maximum connect timeout
MySQL Client Library 5.1.39 n x MYSQLI_OPT_CONNECT_TIMEOUT
Connector/C 6.0 MYSQLI_OPT_CONNECT_TIMEOUT
mysqlnd = PHP streams MYSQLI_OPT_CONNECT_TIMEOUT

The MySQL Connector/C 6.0 uses getaddrinfo() just like PHP streams do. Connector/C asks getaddrinfo() for address running TCP and leaves IPv4 vs. IPv6 unspecified (AF_UNSPEC). This allows the resolver of the operating system to return both IPv4 and IPv6 addresses in whatever order is default on the system. Unlike the MySQL Client Library and PHP, Connector/C picks only the first address and makes a connection attempt.

Library Loops over addresses
MySQL Client Library 5.1.39 Yes
Connector/C 6.0 No
mysqlnd = PHP streams Yes

The MySQL native driver for PHP (mysqlnd) leaves it to PHP streams to do the name resolution. PHP does both a compile time and a runtime check to see if it shall try to use IPv6. If IPv6 support is detected at compile time and PHP can create a datagram socket at runtime (socket(PF_INET6, SOCK_DGRAM, 0)), PHP assumes that IPv6 is not borked and tells getaddrinfo() to return both IPv4 and IPv6 addresses (AF_UNSPEC). Similar to the MySQL Client Library, PHP now starts a loop over all addresses it gets and tries to establish a connection. If there are multiple connection attempts, PHP properly decrements the connection timeout for every attempt made and, if available, additionally uses gettimeofday() to monitor the total runtime. PHP stops looping prematurely, if any address has caused a timeout error code to be set.

Library Protocol hint to resolver?
MySQL Client Library 5.1.39 No
Connector/C 6.0 Yes, TCP (ai_protocol= IPPROTO_TCP)
mysqlnd = PHP streams No

The story about Windows

If you, as a user of any PHP before 5.3.0 on Windows, rely on MYSQLI_OPT_CONNECT_TIMEOUT and believe it is working with TCP/IP connections, you must be mistaken. There is a simple proof. It cannot only be proven with the bug I list above but also with the source code of the MySQL Client Library, which you must be using, because you did not have any alternative before PHP 5.3.0. Neither the MySQL Client Library (from MySQL 5.1.39) nor the MySQL Connector/C 6.0 contain code for Windows to handle the timeout. Both call connect() and let whatever timeout happen that may happen – but they don’t actively control it.

Good news is that MYSQLI_OPT_CONNECT_TIMEOUT works fine with PHP 5.3.1 on Windows: you can set it through mysqlnd, mysqlnd forwards it to PHP streams and PHP streams does its best to implement it. Users of PHP 5.3.0 should upgrade to PHP 5.3.1 as soon as it becomes available, because mysqlnd does not feature the connection timeout setting before PHP 5.3.1.

Summary

TCP/IP connection establishment is implemented differently among the libraries discussed. The different implementations cause different behaviour, for example, with regards to the connection timeout. The connection timeout setting refers only to the operating system level call to establish a connection: connect(). Time spend during the MySQL client server handshake is not considered. Other timeout settings, for example, mysqlnd’s mysqlnd.net_read_timeout or MYSQL_OPT_READ_TIMEOUT, MYSQL_OPT_WRITE_TIMEOUT exist to control time spend while a client reads data from the server or writes data to the server.

Comments are closed.