Implement tcp_user_timeout support with libpq < 12 under Linux

In addition to providing fallback implementation of this option under
Windows, also do it under Linux when using libpq < 12, where this option
was added.

This allows to always specify this option in the connection string,
without worrying about it resulting in fatal error from PQconnectdb() on
older systems: in the worst case, the option is ignored and when using
Linux (or any other system with TCP_USER_TIMEOUT support, although none
seem to currently exist) it will work in the same way as in libpq.
This commit is contained in:
Vadim Zeitlin
2025-06-13 16:56:34 +02:00
parent df64c26f0f
commit 6dac7658d7
3 changed files with 69 additions and 18 deletions

View File

@@ -49,7 +49,7 @@ The set of parameters used in the connection string for PostgreSQL is the same a
* `tracefile`: if specified, enables tracing all database activity using [PQtrace()](https://www.postgresql.org/docs/current/libpq-control.html#LIBPQ-PQTRACE) into the file with the given path. Note that this file is overwritten by default, prepend it with a plus sign, i.e. use `tracefile=+/path/to/file`, to append to the file instead.
* `singlerow` or `singlerows`: if set to `true` or `yes`, enables single-row mode for the session (see next section).
Another parameter is handled specially: while libpq client library supports `tcp_user_timeout` parameter, it only provides support for it under Linux. SOCI also handles this parameter, which can change the time before a broken connection times out (which depends on the system settings but is typically relatively long), under Windows. It handles it in the same way as libpq itself, i.e. the value of this parameter is expressed in milliseconds, but because Windows sockets only use second granularity, SOCI will round the value to the nearest second. In particular, this means that if this parameter is not 0 (which means to use the system default, i.e. is same as not specifying it at all), then it will be always set to at least 1 second. Also note that negative values for this option are currently ignored, but shouldn't be used as their interpretation may change in the future versions of the library.
Another parameter is handled specially: while libpq client library supports `tcp_user_timeout` parameter, it only provides support for it under Linux and only since version 12.0 (released in 2019). SOCI also handles this parameter, which can change the time before a broken connection times out (which depends on the system settings but is typically relatively long), under Windows and when using older libpq versions under Linux. It handles it in the same way as libpq itself, i.e. the value of this parameter is expressed in milliseconds, but because Windows sockets only use second granularity, SOCI rounds the value to the nearest second on this platform. In particular, this means that if this parameter is not 0 (which means to use the system default, i.e. is same as not specifying it at all), then it will be always set to at least 1 second. Also note that negative values for this option are currently ignored, but shouldn't be used as their interpretation may change in the future versions of the library.
Once you have created a `session` object as shown above, you can use it to access the database, for example:

View File

@@ -47,3 +47,9 @@ endif()
if (SOCI_POSTGRESQL_NO_LO64)
target_compile_definitions(soci_postgresql PRIVATE SOCI_POSTGRESQL_NO_LO64)
endif()
if (POSTGRESQL_VERSION VERSION_LESS "9.1.0")
# PQlibVersion() has appeared in PostgreSQL 9.1, so we can't use it with
# older versions.
target_compile_definitions(soci_postgresql PRIVATE SOCI_POSTGRESQL_NO_LIBVERSION)
endif()

View File

@@ -10,10 +10,12 @@
#include "soci/postgresql/soci-postgresql.h"
#include "soci/session.h"
#include "soci-compiler.h"
#include "soci-cstrtoi.h"
#include <libpq-fe.h>
#include <cctype>
#include <cerrno>
#include <cstdio>
#include <cstring>
#include <ctime>
@@ -25,15 +27,15 @@
// - Windows: libpq doesn't handle it at all in all the current versions, but
// we may implement support for it ourselves, see below.
// - Linux (or, to be optimistic, any other system defining TCP_USER_TIMEOUT):
// libpq supports it since v12, so we don't have anything to do.
// libpq supports it since v12, but we also want to provide it when using
// older versions.
// - Other: there is no support for it in libpq and we can't implement it
// (although macOS has TCP_RXT_CONNDROPTIME which seems similar and we could
// try using it if there is interest in it).
//
// Currently we don't differentiate between the last 2 cases as we don't have
// any fallback implementation anyhow, even if, in theory, we could switch to
// using async libpq API to implement it on all platforms, so we only check for
// Windows.
// Note that, in theory, we could switch to using async libpq API to implement
// it on all platforms, but for now having it only under Linux and Windows is
// enough for our needs.
#ifdef _WIN32
// Include the headers we need for setsockopt(TCP_MAXRT) used below.
#include <winsock2.h>
@@ -43,9 +45,11 @@
#ifndef TCP_MAXRT
#define TCP_MAXRT 5
#endif
#include "soci-cstrtoi.h"
#endif
#else // !_WIN32
#include <sys/socket.h>
#include <netinet/in.h>
#include <netinet/tcp.h>
#endif // _WIN32/!_WIN32
using namespace soci;
using namespace soci::details;
@@ -68,7 +72,6 @@ namespace // unnamed
// negative timeout values for consistency with libpq behaviour.
//
// Also throws if the timeout couldn't be set.
#ifdef _WIN32
void set_tcp_user_timeout(int sock, std::string const& timeoutStr)
{
@@ -93,6 +96,7 @@ void set_tcp_user_timeout(int sock, std::string const& timeoutStr)
if (timeoutMs < 0)
return;
#ifdef _WIN32
// The value is in milliseconds, but we need to convert it to seconds,
// prefer to round it rather than truncate.
constexpr int MS_PER_SEC = 1000;
@@ -113,9 +117,38 @@ void set_tcp_user_timeout(int sock, std::string const& timeoutStr)
throw soci_error(oss.str());
}
}
#elif defined(TCP_USER_TIMEOUT)
// We can only set this option for an AF_INET, not AF_UNIX, socket.
sockaddr_storage sa;
socklen_t saLen = sizeof(sa);
if (getsockname(sock, reinterpret_cast<sockaddr*>(&sa), &saLen) != 0)
{
std::ostringstream oss;
oss << "Failed to get socket address: " << strerror(errno) << ".";
#endif // _WIN32
throw soci_error(oss.str());
}
if (sa.ss_family == AF_UNIX)
return;
if (setsockopt(sock, IPPROTO_TCP, TCP_USER_TIMEOUT,
&timeoutMs, sizeof(timeoutMs)) != 0)
{
std::ostringstream oss;
oss << "Failed to set TCP_USER_TIMEOUT option on the socket: "
<< strerror(errno) << ".";
throw soci_error(oss.str());
}
#else
// Don't do anything and silently ignore this option. This is not great,
// but consistent with libpq behaviour and it's not clear what else could
// we do: throwing an exception would seem to be too drastic, but we don't
// have any "warning" mechanism for reporting this otherwise.
SOCI_UNUSED(sock);
#endif // _WIN32/other platforms
}
// helper function for hardcoded queries
void hard_exec(postgresql_session_backend & session_backend,
@@ -293,6 +326,23 @@ void postgresql_session_backend::connect(
}
}
// Support for tcp_user_timeout option has been added in libpq 12, so we
// need to take care of it ourselves if we use an older libpq version. We
// also need to always do it under Windows as no known libpq version has
// support for this parameter there.
//
// Note that if we don't even have PQlibVersion(), it means we're using
// libpq < 9.1, i.e. definitely less than 12.
std::string timeoutStr;
#if !defined(_WIN32) && !defined(SOCI_POSTGRESQL_NO_LIBVERSION)
if ( PQlibVersion() < 120000 )
#endif
{
params.extract_option("tcp_user_timeout", timeoutStr);
}
// else: We're not under Windows and libpq is recent enough to handle this
// connection option itself, so just let it do it.
// We can't use SOCI connection string with PQconnectdb() directly because
// libpq uses single quotes instead of double quotes used by SOCI.
PGconn* const conn = PQconnectdb(params.build_string_from_options('\'').c_str());
@@ -317,15 +367,10 @@ void postgresql_session_backend::connect(
PQtrace(conn, traceFile_);
}
#ifdef _WIN32
// As explained above, implement our own support for this option under
// Windows.
std::string timeoutStr;
if (params.get_option("tcp_user_timeout", timeoutStr))
if (!timeoutStr.empty())
{
set_tcp_user_timeout(PQsocket(conn), timeoutStr);
}
#endif // _WIN32
// With older PostgreSQL versions we need to change the extra_float_digits
// parameter to ensure that the conversions to/from text round trip