1
0
mirror of https://github.com/pgbackrest/pgbackrest.git synced 2024-12-12 10:04:14 +02:00

Improved support for dual stack connections.

Connections are established using the "happy eyeballs" approach from RFC 8305, i.e. new addresses (if available) are tried if the prior address has already had a reasonable time to connect. This prevents waiting too long on a failed connection but does not try all the addresses at once. Prior connections that are still waiting are rechecked periodically if no subsequent connection is successful.

This improves substantially on 39bb8a0, which failed to take into account connection attempts that do not fail (but never connect) and use up all the available time.
This commit is contained in:
David Steele 2024-03-10 11:36:39 +13:00 committed by GitHub
parent f287178b70
commit 7448fde157
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
12 changed files with 617 additions and 130 deletions

View File

@ -34,6 +34,19 @@
</release-bug-list> </release-bug-list>
<release-improvement-list> <release-improvement-list>
<release-item>
<github-issue id="2154"/>
<github-pull-request id="2235"/>
<release-item-contributor-list>
<release-item-ideator id="timothee.peignier"/>
<release-item-contributor id="david.steele"/>
<release-item-reviewer id="stephen.frost"/>
</release-item-contributor-list>
<p>Improved support for dual stack connections.</p>
</release-item>
<release-item> <release-item>
<commit subject="Make meson the primary build system."> <commit subject="Make meson the primary build system.">
<github-pull-request id="2264"/> <github-pull-request id="2264"/>

View File

@ -18,8 +18,25 @@ Object type
struct AddressInfo struct AddressInfo
{ {
AddressInfoPub pub; // Publicly accessible variables AddressInfoPub pub; // Publicly accessible variables
struct addrinfo *info; // Linked list of addresses
}; };
/***********************************************************************************************************************************
Local variables
***********************************************************************************************************************************/
// Store preferred addresses for hosts
typedef struct AddressInfoPreference
{
const String *const host; // Host
String *address; // Preferred address for host
} AddressInfoPreference;
static struct AddressInfoLocal
{
MemContext *memContext; // Mem context
List *prefList; // List of preferred addresses for hosts
} addressInfoLocal;
/*********************************************************************************************************************************** /***********************************************************************************************************************************
Free addrinfo linked list allocated by getaddrinfo() Free addrinfo linked list allocated by getaddrinfo()
***********************************************************************************************************************************/ ***********************************************************************************************************************************/
@ -34,11 +51,90 @@ addrInfoFreeResource(THIS_VOID)
ASSERT(this != NULL); ASSERT(this != NULL);
freeaddrinfo(*(struct addrinfo **)lstGet(this->pub.list, 0)); freeaddrinfo(this->info);
FUNCTION_LOG_RETURN_VOID(); FUNCTION_LOG_RETURN_VOID();
} }
/**********************************************************************************************************************************/
FN_EXTERN void
addrInfoSort(AddressInfo *const this)
{
FUNCTION_TEST_BEGIN();
FUNCTION_TEST_PARAM(ADDRESS_INFO, this);
FUNCTION_TEST_END();
// Only sort if there is more than one address
if (lstSize(this->pub.list) > 1)
{
// By default start with IPv6 and first address
int family = AF_INET6;
unsigned int addrIdx = 0;
// If a preferred address is in the list then move it to the first position and update family
const AddressInfoPreference *const addrPref = lstFind(addressInfoLocal.prefList, &this->pub.host);
if (addrPref != NULL)
{
AddressInfoItem *const addrFindItem = lstFind(this->pub.list, &addrPref->address);
if (addrFindItem != NULL)
{
// Swap with first address if the address is not already first
AddressInfoItem *const addrFirstItem = lstGet(this->pub.list, 0);
if (addrFirstItem != addrFindItem)
{
const AddressInfoItem addrCopyItem = *addrFirstItem;
*addrFirstItem = *addrFindItem;
*addrFindItem = addrCopyItem;
}
// Set family and skip first address
family = addrFirstItem->info->ai_family == AF_INET6 ? AF_INET : AF_INET6;
addrIdx++;
}
}
// Alternate IPv6 and IPv4 addresses
for (; addrIdx < lstSize(this->pub.list); addrIdx++)
{
AddressInfoItem *const addrItem = lstGet(this->pub.list, addrIdx);
// If not the family we expect, search for one
if (addrItem->info->ai_family != family)
{
unsigned int addrFindIdx = addrIdx + 1;
for (; addrFindIdx < lstSize(this->pub.list); addrFindIdx++)
{
AddressInfoItem *const addrFindItem = lstGet(this->pub.list, addrFindIdx);
if (addrFindItem->info->ai_family == family)
{
// Swap addresses
const AddressInfoItem addrCopyItem = *addrItem;
*addrItem = *addrFindItem;
*addrFindItem = addrCopyItem;
// Move on to next address
break;
}
}
// Address of the required family not found so sorting is done
if (addrFindIdx == lstSize(this->pub.list))
break;
}
// Switch family
family = family == AF_INET6 ? AF_INET : AF_INET6;
}
}
FUNCTION_TEST_RETURN_VOID();
}
/**********************************************************************************************************************************/ /**********************************************************************************************************************************/
FN_EXTERN AddressInfo * FN_EXTERN AddressInfo *
addrInfoNew(const String *const host, unsigned int port) addrInfoNew(const String *const host, unsigned int port)
@ -51,6 +147,21 @@ addrInfoNew(const String *const host, unsigned int port)
ASSERT(host != NULL); ASSERT(host != NULL);
ASSERT(port != 0); ASSERT(port != 0);
// Initialize mem context and preference list
if (addressInfoLocal.memContext == NULL)
{
MEM_CONTEXT_BEGIN(memContextTop())
{
MEM_CONTEXT_NEW_BEGIN(AddressInfo, .childQty = MEM_CONTEXT_QTY_MAX)
{
addressInfoLocal.memContext = MEM_CONTEXT_NEW();
addressInfoLocal.prefList = lstNewP(sizeof(AddressInfoPreference), .comparator = lstComparatorStr);
}
MEM_CONTEXT_NEW_END();
}
MEM_CONTEXT_END();
}
OBJ_NEW_BEGIN(AddressInfo, .childQty = MEM_CONTEXT_QTY_MAX, .callbackQty = 1) OBJ_NEW_BEGIN(AddressInfo, .childQty = MEM_CONTEXT_QTY_MAX, .callbackQty = 1)
{ {
*this = (AddressInfo) *this = (AddressInfo)
@ -59,7 +170,7 @@ addrInfoNew(const String *const host, unsigned int port)
{ {
.host = strDup(host), .host = strDup(host),
.port = port, .port = port,
.list = lstNewP(sizeof(struct addrinfo *)), .list = lstNewP(sizeof(AddressInfoItem), .comparator = lstComparatorStr),
}, },
}; };
@ -79,23 +190,28 @@ addrInfoNew(const String *const host, unsigned int port)
cvtUIntToZ(port, portZ, sizeof(portZ)); cvtUIntToZ(port, portZ, sizeof(portZ));
// Do the lookup // Do the lookup
struct addrinfo *result;
int error; int error;
if ((error = getaddrinfo(strZ(host), portZ, &hints, &result)) != 0) if ((error = getaddrinfo(strZ(host), portZ, &hints, &this->info)) != 0)
THROW_FMT(HostConnectError, "unable to get address for '%s': [%d] %s", strZ(host), error, gai_strerror(error)); THROW_FMT(HostConnectError, "unable to get address for '%s': [%d] %s", strZ(host), error, gai_strerror(error));
// Set free callback to ensure address info is freed
memContextCallbackSet(objMemContext(this), addrInfoFreeResource, this);
// Convert address linked list to list // Convert address linked list to list
lstAdd(this->pub.list, &result); MEM_CONTEXT_OBJ_BEGIN(this->pub.list)
while (result->ai_next != NULL)
{ {
lstAdd(this->pub.list, &result->ai_next); struct addrinfo *info = this->info;
result = result->ai_next;
lstAdd(this->pub.list, &(AddressInfoItem){.name = addrInfoToStr(info), .info = info});
// Set free callback to ensure address info is freed
memContextCallbackSet(objMemContext(this), addrInfoFreeResource, this);
while (info->ai_next != NULL)
{
info = info->ai_next;
lstAdd(this->pub.list, &(AddressInfoItem){.name = addrInfoToStr(info), .info = info});
}
} }
MEM_CONTEXT_OBJ_END();
} }
MEM_CONTEXT_TEMP_END(); MEM_CONTEXT_TEMP_END();
} }
@ -104,6 +220,41 @@ addrInfoNew(const String *const host, unsigned int port)
FUNCTION_LOG_RETURN(ADDRESS_INFO, this); FUNCTION_LOG_RETURN(ADDRESS_INFO, this);
} }
/**********************************************************************************************************************************/
FN_EXTERN void
addrInfoPrefer(AddressInfo *this, unsigned int index)
{
FUNCTION_LOG_BEGIN(logLevelDebug);
FUNCTION_LOG_PARAM(ADDRESS_INFO, this);
FUNCTION_LOG_PARAM(UINT, index);
FUNCTION_LOG_END();
AddressInfoPreference *const addrPref = lstFind(addressInfoLocal.prefList, &this->pub.host);
const String *const address = addrInfoGet(this, index)->name;
MEM_CONTEXT_OBJ_BEGIN(addressInfoLocal.prefList)
{
// If no preference set yet
if (addrPref == NULL)
{
lstAdd(addressInfoLocal.prefList, &(AddressInfoPreference){.host = strDup(this->pub.host), .address = strDup(address)});
lstSort(addressInfoLocal.prefList, sortOrderAsc);
}
// Else update address
else
{
// Free the old address
strFree(addrPref->address);
// Assign new address
addrPref->address = strDup(address);
}
}
MEM_CONTEXT_OBJ_END();
FUNCTION_LOG_RETURN_VOID();
}
/*********************************************************************************************************************************** /***********************************************************************************************************************************
Convert address to a zero-terminated string Convert address to a zero-terminated string
***********************************************************************************************************************************/ ***********************************************************************************************************************************/
@ -166,21 +317,18 @@ addrInfoToStr(const struct addrinfo *const addrInfo)
FN_EXTERN void FN_EXTERN void
addrInfoToLog(const AddressInfo *const this, StringStatic *const debugLog) addrInfoToLog(const AddressInfo *const this, StringStatic *const debugLog)
{ {
char address[48];
strStcFmt(debugLog, "{host: "); strStcFmt(debugLog, "{host: ");
strToLog(addrInfoHost(this), debugLog); strToLog(addrInfoHost(this), debugLog);
strStcFmt(debugLog, ", port: %u, list: [", addrInfoPort(this)); strStcFmt(debugLog, ", port: %u, list: [", addrInfoPort(this));
for (unsigned int listIdx = 0; listIdx < addrInfoSize(this); listIdx++) for (unsigned int listIdx = 0; listIdx < addrInfoSize(this); listIdx++)
{ {
const struct addrinfo *const addrInfo = addrInfoGet(this, listIdx); const AddressInfoItem *const addrItem = addrInfoGet(this, listIdx);
if (listIdx != 0) if (listIdx != 0)
strStcCat(debugLog, ", "); strStcCat(debugLog, ", ");
addrInfoToZ(addrInfo, address, sizeof(address)); strStcCat(debugLog, strZ(addrItem->name));
strStcCat(debugLog, address);
} }
strStcCat(debugLog, "]}"); strStcCat(debugLog, "]}");

View File

@ -25,6 +25,12 @@ FN_EXTERN AddressInfo *addrInfoNew(const String *const host, unsigned int port);
/*********************************************************************************************************************************** /***********************************************************************************************************************************
Getters/Setters Getters/Setters
***********************************************************************************************************************************/ ***********************************************************************************************************************************/
typedef struct AddressInfoItem
{
String *name; // Address as a string
struct addrinfo *info; // Full address info
} AddressInfoItem;
typedef struct AddressInfoPub typedef struct AddressInfoPub
{ {
const String *host; // Host for address info lookup const String *host; // Host for address info lookup
@ -33,10 +39,10 @@ typedef struct AddressInfoPub
} AddressInfoPub; } AddressInfoPub;
// Get address // Get address
FN_INLINE_ALWAYS const struct addrinfo * FN_INLINE_ALWAYS const AddressInfoItem *
addrInfoGet(const AddressInfo *const this, const unsigned int index) addrInfoGet(const AddressInfo *const this, const unsigned int index)
{ {
return *(const struct addrinfo **)lstGet(THIS_PUB(AddressInfo)->list, index); return (const AddressInfoItem *)lstGet(THIS_PUB(AddressInfo)->list, index);
} }
// Get lookup host // Get lookup host
@ -60,6 +66,15 @@ addrInfoSize(const AddressInfo *const this)
return lstSize(THIS_PUB(AddressInfo)->list); return lstSize(THIS_PUB(AddressInfo)->list);
} }
/***********************************************************************************************************************************
Functions
***********************************************************************************************************************************/
// Sort addresses alternating between IPv6 and IPv4
FN_EXTERN void addrInfoSort(AddressInfo *this);
// Set preferred address for host, which will be preferred in future lookups
FN_EXTERN void addrInfoPrefer(AddressInfo *this, unsigned int index);
/*********************************************************************************************************************************** /***********************************************************************************************************************************
Destructor Destructor
***********************************************************************************************************************************/ ***********************************************************************************************************************************/

View File

@ -11,6 +11,7 @@ Socket Client
#include "common/debug.h" #include "common/debug.h"
#include "common/error/retry.h" #include "common/error/retry.h"
#include "common/io/client.h" #include "common/io/client.h"
#include "common/io/fd.h"
#include "common/io/socket/address.h" #include "common/io/socket/address.h"
#include "common/io/socket/client.h" #include "common/io/socket/client.h"
#include "common/io/socket/common.h" #include "common/io/socket/common.h"
@ -57,7 +58,64 @@ sckClientToLog(const THIS_VOID, StringStatic *const debugLog)
#define FUNCTION_LOG_SOCKET_CLIENT_FORMAT(value, buffer, bufferSize) \ #define FUNCTION_LOG_SOCKET_CLIENT_FORMAT(value, buffer, bufferSize) \
FUNCTION_LOG_OBJECT_FORMAT(value, sckClientToLog, buffer, bufferSize) FUNCTION_LOG_OBJECT_FORMAT(value, sckClientToLog, buffer, bufferSize)
/**********************************************************************************************************************************/ /***********************************************************************************************************************************
Connections are established using the "happy eyeballs" approach from RFC 8305, i.e. new addresses (if available) are tried if the
prior address has already had a reasonable time to connect. This prevents waiting too long on a failed connection but does not try
all the addresses at once. Prior connections that are still waiting are rechecked periodically if no subsequent connection is
successful.
***********************************************************************************************************************************/
typedef struct SckClientOpenData
{
const struct addrinfo *const address; // IP address
const char *name; // Combination of host, address, port
int fd; // File descriptor for socket
int errNo; // Current error (may just indicate to keep waiting)
} SckClientOpenData;
// Helper to determine if non-blocking connection has been established
static bool
sckClientOpenWait(SckClientOpenData *const openData, const TimeMSec timeout)
{
ASSERT(openData != NULL);
FUNCTION_LOG_BEGIN(logLevelTrace);
FUNCTION_LOG_PARAM(STRINGZ, openData->name);
FUNCTION_LOG_PARAM(INT, openData->fd);
FUNCTION_LOG_PARAM(INT, openData->errNo);
FUNCTION_LOG_PARAM(TIME_MSEC, timeout);
FUNCTION_LOG_END();
ASSERT(openData->name != NULL);
ASSERT(openData->fd > 0);
ASSERT(openData->errNo != 0);
bool result = true;
// The connection has started but since we are in non-blocking mode it has not completed yet
if (openData->errNo == EINPROGRESS || openData->errNo == EINTR)
{
// Wait for write-ready
if (fdReadyWrite(openData->fd, timeout))
{
// Check for success or error. If the connection was successful this will set errNo to 0.
socklen_t errNoLen = sizeof(openData->errNo);
THROW_ON_SYS_ERROR_FMT(
getsockopt(openData->fd, SOL_SOCKET, SO_ERROR, &openData->errNo, &errNoLen) == -1, HostConnectError,
"unable to get socket error for '%s'", openData->name);
}
// Else still waiting on connection
else
result = false;
}
// Throw error if it is still set
if (result && openData->errNo != 0)
THROW_SYS_ERROR_CODE_FMT(openData->errNo, HostConnectError, "unable to connect to '%s'", openData->name);
FUNCTION_LOG_RETURN(BOOL, result);
}
static IoSession * static IoSession *
sckClientOpen(THIS_VOID) sckClientOpen(THIS_VOID)
{ {
@ -77,47 +135,124 @@ sckClientOpen(THIS_VOID)
ErrorRetry *const errRetry = errRetryNew(); ErrorRetry *const errRetry = errRetryNew();
bool retry; bool retry;
// Get an address list for the host // Get an address list for the host and sort it
const AddressInfo *const addrInfo = addrInfoNew(this->host, this->port); AddressInfo *const addrInfo = addrInfoNew(this->host, this->port);
unsigned int addrInfoIdx = 0; addrInfoSort(addrInfo);
// Try addresses until success or timeout
List *const openDataList = lstNewP(sizeof(SckClientOpenData));
volatile unsigned int addrInfoIdx = 0;
do do
{ {
// Assume there will be no retry // Assume there will be no retry
SckClientOpenData *volatile openData = NULL;
const ErrorType *errLastType = NULL;
String *errLastMessage = NULL;
retry = false; retry = false;
volatile int fd = -1;
// Try connection
TRY_BEGIN() TRY_BEGIN()
{ {
// Get next address for the host // Iterate waiting connections and see if any of them have completed
const struct addrinfo *const addressFound = addrInfoGet(addrInfo, addrInfoIdx); for (unsigned int openDataIdx = 0; openDataIdx < lstSize(openDataList); openDataIdx++)
// Connect to the host
fd = socket(addressFound->ai_family, addressFound->ai_socktype, addressFound->ai_protocol);
THROW_ON_SYS_ERROR(fd == -1, HostConnectError, "unable to create socket");
sckOptionSet(fd);
sckConnect(fd, this->host, this->port, addressFound, this->timeoutConnect);
// Create the session
MEM_CONTEXT_PRIOR_BEGIN()
{ {
result = sckSessionNew(ioSessionRoleClient, fd, this->host, this->port, this->timeoutSession); SckClientOpenData *const openDataWait = lstGet(openDataList, openDataIdx);
}
MEM_CONTEXT_PRIOR_END();
// Update client name to include address if (openDataWait->fd != 0 && sckClientOpenWait(openDataWait, 0))
strTrunc(this->name); {
strCat(this->name, addrInfoToName(this->host, this->port, addressFound)); openData = openDataWait;
break;
}
}
// Try or retry a connection since none of the waiting connections completed
if (openData == NULL)
{
// If connection does not exist yet then create it
if (addrInfoIdx == lstSize(openDataList))
{
openData = lstAdd(openDataList, &(SckClientOpenData){.address = addrInfoGet(addrInfo, addrInfoIdx)->info});
openData->name = strZ(addrInfoToName(this->host, this->port, openData->address));
}
// Else search for a connection that is not waiting
else
{
for (; addrInfoIdx < lstSize(openDataList); addrInfoIdx++)
{
SckClientOpenData *const openDataNoWait = lstGet(openDataList, addrInfoIdx);
if (openDataNoWait->fd == 0)
{
openData = openDataNoWait;
break;
}
}
}
// Attempt connection to host
if (openData != NULL)
{
openData->fd = socket(
openData->address->ai_family, openData->address->ai_socktype, openData->address->ai_protocol);
THROW_ON_SYS_ERROR(openData->fd == -1, HostConnectError, "unable to create socket");
sckOptionSet(openData->fd);
if (connect(openData->fd, openData->address->ai_addr, openData->address->ai_addrlen) == -1)
{
// Save the error and wait for connection. The initial wait time will be fixed at 250ms, the default
// value recommended by RFC 8305.
openData->errNo = errno;
sckClientOpenWait(openData, 250);
}
}
}
// Connection was successful so open session and set result
if (openData != NULL && openData->errNo == 0)
{
// Create the session
MEM_CONTEXT_PRIOR_BEGIN()
{
result = sckSessionNew(ioSessionRoleClient, openData->fd, this->host, this->port, this->timeoutSession);
}
MEM_CONTEXT_PRIOR_END();
// Update client name to include address
strTrunc(this->name);
strCatZ(this->name, openData->name);
// Set preferred address
addrInfoPrefer(addrInfo, addrInfoIdx);
// Clear socket so it will not be freed later
openData->fd = 0;
}
} }
CATCH_ANY() CATCH_ANY()
{ {
// Close socket // Close socket
close(fd); ASSERT(openData != NULL);
close(openData->fd);
// Clear socket so the connection can be retried
openData->fd = 0;
openData->errNo = 0;
// Add the error retry info // Add the error retry info
errRetryAddP(errRetry); errRetryAddP(errRetry);
// Store error info for later logging
errLastType = errorType();
errLastMessage = strNewZ(errorMessage());
}
TRY_END();
// If the connection was not completed then wait and/or retry
if (result == NULL)
{
// Increment address info index and reset if the end has been reached. When the end has been reached sleep for a bit // Increment address info index and reset if the end has been reached. When the end has been reached sleep for a bit
// to hopefully have better chance at succeeding, otherwise continue right to the next address. // to hopefully have better chance at succeeding, otherwise continue right to the next address.
addrInfoIdx++; addrInfoIdx++;
@ -130,18 +265,47 @@ sckClientOpen(THIS_VOID)
else else
retry = true; retry = true;
// Error when no retries remain // General timeout errors when no retries remain
if (!retry) if (!retry)
THROWP(errRetryType(errRetry), strZ(errRetryMessage(errRetry))); {
for (unsigned int openDataIdx = 0; openDataIdx < lstSize(openDataList); openDataIdx++)
{
SckClientOpenData *const openDataTimeout = lstGet(openDataList, openDataIdx);
// Log retry if (openDataTimeout->fd != 0)
LOG_DEBUG_FMT("retry %s: %s", errorTypeName(errorType()), errorMessage()); {
statInc(SOCKET_STAT_RETRY_STR); errRetryAddP(
errRetry, .type = &HostConnectError,
.message = strNewFmt("timeout connecting to '%s'", openDataTimeout->name));
}
}
}
// Log retry when there was an error
if (errLastType != NULL)
{
LOG_DEBUG_FMT("retry %s: %s", errorTypeName(errLastType), strZ(errLastMessage));
strFree(errLastMessage);
statInc(SOCKET_STAT_RETRY_STR);
}
} }
TRY_END();
} }
while (retry); while (retry);
// Free any connections that were in progress but never completed
for (unsigned int openDataIdx = 0; openDataIdx < lstSize(openDataList); openDataIdx++)
{
SckClientOpenData *const openDataFree = lstGet(openDataList, openDataIdx);
if (openDataFree->fd != 0)
close(openDataFree->fd);
}
// Error when no result
if (result == NULL)
THROWP(errRetryType(errRetry), strZ(errRetryMessage(errRetry)));
statInc(SOCKET_STAT_SESSION_STR); statInc(SOCKET_STAT_SESSION_STR);
} }
MEM_CONTEXT_TEMP_END(); MEM_CONTEXT_TEMP_END();

View File

@ -9,7 +9,6 @@ Socket Common Functions
#include <sys/socket.h> #include <sys/socket.h>
#include "common/debug.h" #include "common/debug.h"
#include "common/io/fd.h"
#include "common/io/socket/address.h" #include "common/io/socket/address.h"
#include "common/io/socket/common.h" #include "common/io/socket/common.h"
#include "common/log.h" #include "common/log.h"
@ -137,55 +136,3 @@ sckOptionSet(int fd)
FUNCTION_TEST_RETURN_VOID(); FUNCTION_TEST_RETURN_VOID();
} }
/**********************************************************************************************************************************/
static bool
sckConnectInProgress(int errNo)
{
return errNo == EINPROGRESS || errNo == EINTR;
}
FN_EXTERN void
sckConnect(int fd, const String *host, unsigned int port, const struct addrinfo *hostAddress, TimeMSec timeout)
{
FUNCTION_LOG_BEGIN(logLevelTrace);
FUNCTION_LOG_PARAM(INT, fd);
FUNCTION_LOG_PARAM(STRING, host);
FUNCTION_LOG_PARAM(UINT, port);
FUNCTION_LOG_PARAM_P(VOID, hostAddress);
FUNCTION_LOG_PARAM(TIME_MSEC, timeout);
FUNCTION_LOG_END();
ASSERT(host != NULL);
ASSERT(hostAddress != NULL);
// Attempt connection
if (connect(fd, hostAddress->ai_addr, hostAddress->ai_addrlen) == -1)
{
// Save the error
int errNo = errno;
// The connection has started but since we are in non-blocking mode it has not completed yet
if (sckConnectInProgress(errNo))
{
// Wait for write-ready
if (!fdReadyWrite(fd, timeout))
THROW_FMT(HostConnectError, "timeout connecting to '%s'", strZ(addrInfoToName(host, port, hostAddress)));
// Check for success or error. If the connection was successful this will set errNo to 0.
socklen_t errNoLen = sizeof(errNo);
THROW_ON_SYS_ERROR(
getsockopt(fd, SOL_SOCKET, SO_ERROR, &errNo, &errNoLen) == -1, HostConnectError, "unable to get socket error");
}
// Throw error if it is still set
if (errNo != 0)
{
THROW_SYS_ERROR_CODE_FMT(
errNo, HostConnectError, "unable to connect to '%s'", strZ(addrInfoToName(host, port, hostAddress)));
}
}
FUNCTION_LOG_RETURN_VOID();
}

View File

@ -18,7 +18,4 @@ FN_EXTERN void sckInit(bool block, bool keepAlive, int tcpKeepAliveCount, int tc
// Set options on a socket // Set options on a socket
FN_EXTERN void sckOptionSet(int fd); FN_EXTERN void sckOptionSet(int fd);
// Connect socket to an IP address
FN_EXTERN void sckConnect(int fd, const String *host, unsigned int port, const struct addrinfo *hostAddress, TimeMSec timeout);
#endif #endif

View File

@ -165,7 +165,7 @@ sckServerNew(const String *const address, const unsigned int port, const TimeMSe
}; };
// Lookup address // Lookup address
const struct addrinfo *const addressFound = addrInfoGet(addrInfoNew(this->address, this->port), 0); const struct addrinfo *const addressFound = addrInfoGet(addrInfoNew(this->address, this->port), 0)->info;
// Create socket // Create socket
THROW_ON_SYS_ERROR( THROW_ON_SYS_ERROR(

View File

@ -371,6 +371,7 @@ unit:
common/io/socket/client: common/io/socket/client:
function: function:
- sckClientOpen - sckClientOpen
- sckClientOpenWait
coverage: coverage:
- common/io/client - common/io/client
@ -404,6 +405,9 @@ unit:
- common/io/http/session - common/io/http/session
- common/io/http/url - common/io/http/url
include:
- common/io/socket/address
# ---------------------------------------------------------------------------------------------------------------------------- # ----------------------------------------------------------------------------------------------------------------------------
- name: exec - name: exec
total: 2 total: 2

View File

@ -3,7 +3,6 @@ Harness for Io Testing
***********************************************************************************************************************************/ ***********************************************************************************************************************************/
#include "build.auto.h" #include "build.auto.h"
#include "common/harnessConfig.h"
#include "common/harnessDebug.h" #include "common/harnessDebug.h"
#include "common/harnessSocket.h" #include "common/harnessSocket.h"
@ -19,8 +18,61 @@ static struct
{ {
// Shim clientOpen() to use a fake file descriptor // Shim clientOpen() to use a fake file descriptor
bool localShimSckClientOpen; bool localShimSckClientOpen;
// Shim sckClientOpenWait() to return false for an address one time
const char *clientOpenWaitAddress;
} hrnIoStatic; } hrnIoStatic;
/**********************************************************************************************************************************/
void
hrnSckClientOpenWaitShimInstall(const char *const address)
{
FUNCTION_HARNESS_BEGIN();
FUNCTION_HARNESS_PARAM(STRINGZ, address);
FUNCTION_HARNESS_END();
hrnIoStatic.clientOpenWaitAddress = address;
FUNCTION_HARNESS_RETURN_VOID();
}
/***********************************************************************************************************************************
Shim sckClientOpen()
***********************************************************************************************************************************/
static bool
sckClientOpenWait(SckClientOpenData *const openData, const TimeMSec timeout)
{
ASSERT(openData != NULL);
FUNCTION_HARNESS_BEGIN();
FUNCTION_HARNESS_PARAM(STRINGZ, openData->name);
FUNCTION_HARNESS_PARAM(INT, openData->fd);
FUNCTION_HARNESS_PARAM(INT, openData->errNo);
FUNCTION_HARNESS_PARAM(TIME_MSEC, timeout);
FUNCTION_HARNESS_END();
bool result = sckClientOpenWait_SHIMMED(openData, timeout);
// If this is the shimmed address then return false and update error code
if (hrnIoStatic.clientOpenWaitAddress != NULL)
{
String *const address = addrInfoToStr(openData->address);
if (strcmp(hrnIoStatic.clientOpenWaitAddress, strZ(address)) == 0)
{
result = false;
openData->errNo = EINPROGRESS;
// Reset the shim
hrnIoStatic.clientOpenWaitAddress = NULL;
}
strFree(address);
}
FUNCTION_HARNESS_RETURN(BOOL, result);
}
/*********************************************************************************************************************************** /***********************************************************************************************************************************
Shim sckClientOpen() Shim sckClientOpen()
***********************************************************************************************************************************/ ***********************************************************************************************************************************/

View File

@ -11,6 +11,9 @@ Constants
/*********************************************************************************************************************************** /***********************************************************************************************************************************
Functions Functions
***********************************************************************************************************************************/ ***********************************************************************************************************************************/
// Install shim for sckClientOpenWait() to return false for an address one time
void hrnSckClientOpenWaitShimInstall(const char *address);
// Install/uninstall shim for clientOpen() to use a fake file descriptor // Install/uninstall shim for clientOpen() to use a fake file descriptor
void hrnSckClientOpenShimInstall(void); void hrnSckClientOpenShimInstall(void);
void hrnSckClientOpenShimUninstall(void); void hrnSckClientOpenShimUninstall(void);

View File

@ -5,6 +5,7 @@ Test HTTP
#include "common/io/fdRead.h" #include "common/io/fdRead.h"
#include "common/io/fdWrite.h" #include "common/io/fdWrite.h"
#include "common/io/socket/address.h"
#include "common/io/socket/client.h" #include "common/io/socket/client.h"
#include "common/io/tls/client.h" #include "common/io/tls/client.h"
@ -25,6 +26,13 @@ testRun(void)
{ {
FUNCTION_HARNESS_VOID(); FUNCTION_HARNESS_VOID();
// Ensure that the ipv4 loopback address will be selected
const String *const ipLoop4 = STRDEF("127.0.0.1");
AddressInfo *addrInfo = NULL;
TEST_ASSIGN(addrInfo, addrInfoNew(STRDEF("localhost"), 443), "localhost addr list");
TEST_RESULT_VOID(addrInfoPrefer(addrInfo, lstFindIdx(addrInfo->pub.list, &ipLoop4)), "prefer 127.0.0.1");
// ***************************************************************************************************************************** // *****************************************************************************************************************************
if (testBegin("httpUriEncode() and httpUriDecode()")) if (testBegin("httpUriEncode() and httpUriDecode()"))
{ {

View File

@ -155,20 +155,22 @@ testRun(void)
if (testBegin("AddressInfo")) if (testBegin("AddressInfo"))
{ {
#ifdef TEST_CONTAINER_REQUIRED #ifdef TEST_CONTAINER_REQUIRED
HRN_SYSTEM("echo \"127.0.0.1 test-addr-loop.pgbackrest.org\" | sudo tee -a /etc/hosts > /dev/null"); #define TEST_ADDR_LOOP_HOST "test-addr-loop.pgbackrest.org"
HRN_SYSTEM("echo \"::1 test-addr-loop.pgbackrest.org\" | sudo tee -a /etc/hosts > /dev/null");
HRN_SYSTEM("echo \"127.0.0.1 " TEST_ADDR_LOOP_HOST "\" | sudo tee -a /etc/hosts > /dev/null");
HRN_SYSTEM("echo \"::1 " TEST_ADDR_LOOP_HOST "\" | sudo tee -a /etc/hosts > /dev/null");
// ------------------------------------------------------------------------------------------------------------------------- // -------------------------------------------------------------------------------------------------------------------------
TEST_TITLE("lookup address info"); TEST_TITLE("lookup address info");
AddressInfo *addrInfo = NULL; AddressInfo *addrInfo = NULL;
TEST_ASSIGN(addrInfo, addrInfoNew(STRDEF("test-addr-loop.pgbackrest.org"), 443), "addr list"); TEST_ASSIGN(addrInfo, addrInfoNew(STRDEF(TEST_ADDR_LOOP_HOST), 443), "addr list");
TEST_RESULT_STR_Z(addrInfoHost(addrInfo), "test-addr-loop.pgbackrest.org", "check host"); TEST_RESULT_STR_Z(addrInfoHost(addrInfo), TEST_ADDR_LOOP_HOST, "check host");
TEST_RESULT_UINT(addrInfoPort(addrInfo), 443, "check port"); TEST_RESULT_UINT(addrInfoPort(addrInfo), 443, "check port");
TEST_RESULT_UINT(addrInfoSize(addrInfo), 2, "check size"); TEST_RESULT_UINT(addrInfoSize(addrInfo), 2, "check size");
// ------------------------------------------------------------------------------------------------------------------------- // -------------------------------------------------------------------------------------------------------------------------
TEST_TITLE("addrInfoToLog"); TEST_TITLE("addrInfoToLog()");
char logBuf[STACK_TRACE_PARAM_MAX]; char logBuf[STACK_TRACE_PARAM_MAX];
@ -176,21 +178,94 @@ testRun(void)
TEST_RESULT_Z( TEST_RESULT_Z(
logBuf, logBuf,
zNewFmt( zNewFmt(
"{host: {\"test-addr-loop.pgbackrest.org\"}, port: 443, list: [%s, %s]}", "{host: {\"" TEST_ADDR_LOOP_HOST "\"}, port: 443, list: [%s, %s]}",
strZ(addrInfoToStr(addrInfoGet(addrInfo, 0))), strZ(addrInfoToStr(addrInfoGet(addrInfo, 1)))), strZ(addrInfoGet(addrInfo, 0)->name), strZ(addrInfoGet(addrInfo, 1)->name)),
"check log"); "check log");
addrInfoSort(addrInfo);
TEST_RESULT_VOID(FUNCTION_LOG_OBJECT_FORMAT(addrInfo, addrInfoToLog, logBuf, sizeof(logBuf)), "addrInfoToLog");
TEST_RESULT_Z(logBuf, "{host: {\"" TEST_ADDR_LOOP_HOST "\"}, port: 443, list: [::1, 127.0.0.1]}", "check log");
// Munge address so it is invalid // Munge address so it is invalid
(*(struct addrinfo **)lstGet(addrInfo->pub.list, 0))->ai_addr = NULL; ((AddressInfoItem *)lstGet(addrInfo->pub.list, 0))->info->ai_addr = NULL;
TEST_RESULT_STR_Z(addrInfoToStr(addrInfoGet(addrInfo, 0)->info), "invalid", "check invalid");
// -------------------------------------------------------------------------------------------------------------------------
TEST_TITLE("addrInfoSort() break early");
struct addrinfo addr4 = {.ai_family = AF_INET};
struct addrinfo addr6 = {.ai_family = AF_INET6};
MEM_CONTEXT_OBJ_BEGIN(addrInfo)
{
addrInfo->pub.host = strNewZ("test");
lstClear(addrInfo->pub.list);
lstAdd(addrInfo->pub.list, &(AddressInfoItem){.name = strNewZ("127.0.0.1"), .info = &addr4});
lstAdd(addrInfo->pub.list, &(AddressInfoItem){.name = strNewZ("::1"), .info = &addr6});
lstAdd(addrInfo->pub.list, &(AddressInfoItem){.name = strNewZ("127.0.0.2"), .info = &addr4});
lstAdd(addrInfo->pub.list, &(AddressInfoItem){.name = strNewZ("::2"), .info = &addr6});
lstAdd(addrInfo->pub.list, &(AddressInfoItem){.name = strNewZ("::3"), .info = &addr6});
lstAdd(addrInfo->pub.list, &(AddressInfoItem){.name = strNewZ("::4"), .info = &addr6});
addrInfoSort(addrInfo);
}
MEM_CONTEXT_OBJ_END();
TEST_RESULT_VOID(FUNCTION_LOG_OBJECT_FORMAT(addrInfo, addrInfoToLog, logBuf, sizeof(logBuf)), "addrInfoToLog"); TEST_RESULT_VOID(FUNCTION_LOG_OBJECT_FORMAT(addrInfo, addrInfoToLog, logBuf, sizeof(logBuf)), "addrInfoToLog");
TEST_RESULT_Z( TEST_RESULT_Z(
logBuf, logBuf, zNewFmt("{host: {\"test\"}, port: 443, list: [::1, 127.0.0.1, ::2, 127.0.0.2, ::3, ::4]}"), "check log");
zNewFmt(
"{host: {\"test-addr-loop.pgbackrest.org\"}, port: 443, list: [invalid, %s]}", // -------------------------------------------------------------------------------------------------------------------------
strZ(addrInfoToStr(addrInfoGet(addrInfo, 1)))), TEST_TITLE("addrInfoSort()");
"check log");
TEST_RESULT_STR_Z(addrInfoToStr(addrInfoGet(addrInfo, 0)), "invalid", "check invalid"); // Set a preference that won't be found
addrInfoPrefer(addrInfo, 5);
MEM_CONTEXT_OBJ_BEGIN(addrInfo)
{
lstClear(addrInfo->pub.list);
lstAdd(addrInfo->pub.list, &(AddressInfoItem){.name = strNewZ("127.0.0.1"), .info = &addr4});
lstAdd(addrInfo->pub.list, &(AddressInfoItem){.name = strNewZ("127.0.0.2"), .info = &addr4});
lstAdd(addrInfo->pub.list, &(AddressInfoItem){.name = strNewZ("::1"), .info = &addr6});
lstAdd(addrInfo->pub.list, &(AddressInfoItem){.name = strNewZ("::2"), .info = &addr6});
addrInfoSort(addrInfo);
}
MEM_CONTEXT_OBJ_END();
TEST_RESULT_VOID(FUNCTION_LOG_OBJECT_FORMAT(addrInfo, addrInfoToLog, logBuf, sizeof(logBuf)), "addrInfoToLog");
TEST_RESULT_Z(
logBuf, zNewFmt("{host: {\"test\"}, port: 443, list: [::1, 127.0.0.2, ::2, 127.0.0.1]}"), "check log");
// -------------------------------------------------------------------------------------------------------------------------
TEST_TITLE("addrInfoSort() prefer v4");
addrInfoPrefer(addrInfo, 1);
addrInfoSort(addrInfo);
TEST_RESULT_VOID(FUNCTION_LOG_OBJECT_FORMAT(addrInfo, addrInfoToLog, logBuf, sizeof(logBuf)), "addrInfoToLog");
TEST_RESULT_Z(
logBuf, zNewFmt("{host: {\"test\"}, port: 443, list: [127.0.0.2, ::1, 127.0.0.1, ::2]}"), "check log");
// -------------------------------------------------------------------------------------------------------------------------
TEST_TITLE("addrInfoSort() prefer v4 (already first)");
addrInfoSort(addrInfo);
TEST_RESULT_VOID(FUNCTION_LOG_OBJECT_FORMAT(addrInfo, addrInfoToLog, logBuf, sizeof(logBuf)), "addrInfoToLog");
TEST_RESULT_Z(
logBuf, zNewFmt("{host: {\"test\"}, port: 443, list: [127.0.0.2, ::1, 127.0.0.1, ::2]}"), "check log");
// -------------------------------------------------------------------------------------------------------------------------
TEST_TITLE("addrInfoSort() prefer v6");
addrInfoPrefer(addrInfo, 3);
addrInfoSort(addrInfo);
TEST_RESULT_VOID(FUNCTION_LOG_OBJECT_FORMAT(addrInfo, addrInfoToLog, logBuf, sizeof(logBuf)), "addrInfoToLog");
TEST_RESULT_Z(
logBuf, zNewFmt("{host: {\"test\"}, port: 443, list: [::2, 127.0.0.1, ::1, 127.0.0.2]}"), "check log");
// ------------------------------------------------------------------------------------------------------------------------- // -------------------------------------------------------------------------------------------------------------------------
TEST_TITLE("free"); TEST_TITLE("free");
@ -343,11 +418,6 @@ testRun(void)
TEST_ERROR( TEST_ERROR(
ioClientOpen(socketClient), HostConnectError, "unable to connect to '127.0.0.1:7777': [111] Connection refused"); ioClientOpen(socketClient), HostConnectError, "unable to connect to '127.0.0.1:7777': [111] Connection refused");
socketLocal.block = false; socketLocal.block = false;
// ---------------------------------------------------------------------------------------------------------------------
TEST_TITLE("uncovered conditions for sckConnect()");
TEST_RESULT_BOOL(sckConnectInProgress(EINTR), true, "connection in progress (EINTR)");
} }
FINALLY() FINALLY()
{ {
@ -365,6 +435,11 @@ testRun(void)
if (testBegin("SocketClient/SocketServer")) if (testBegin("SocketClient/SocketServer"))
{ {
IoClient *client = NULL; IoClient *client = NULL;
AddressInfo *addrInfo = NULL;
const String *const ipLoop4 = STRDEF("127.0.0.1");
TEST_ASSIGN(addrInfo, addrInfoNew(STRDEF("localhost"), 443), "localhost addr list");
TEST_RESULT_VOID(addrInfoPrefer(addrInfo, lstFindIdx(addrInfo->pub.list, &ipLoop4)), "prefer 127.0.0.1");
TEST_ASSIGN(client, sckClientNew(STRDEF("localhost"), HRN_SERVER_PORT_BOGUS, 100, 100), "new client"); TEST_ASSIGN(client, sckClientNew(STRDEF("localhost"), HRN_SERVER_PORT_BOGUS, 100, 100), "new client");
TEST_ERROR( TEST_ERROR(
@ -374,10 +449,71 @@ testRun(void)
// This address should not be in use in a test environment -- if it is the test will fail // This address should not be in use in a test environment -- if it is the test will fail
TEST_ASSIGN(client, sckClientNew(STRDEF("172.31.255.255"), HRN_SERVER_PORT_BOGUS, 100, 100), "new client"); TEST_ASSIGN(client, sckClientNew(STRDEF("172.31.255.255"), HRN_SERVER_PORT_BOGUS, 100, 100), "new client");
TEST_ERROR( TEST_ERROR(ioClientOpen(client), HostConnectError, "timeout connecting to '172.31.255.255:34342'");
ioClientOpen(client), HostConnectError,
"timeout connecting to '172.31.255.255:34342'\n" // -------------------------------------------------------------------------------------------------------------------------
"[RETRY DETAIL OMITTED]"); #ifdef TEST_CONTAINER_REQUIRED
#define TEST_ADDR_CONN_HOST "test-addr-conn.pgbackrest.org"
HRN_SYSTEM("echo \"127.0.0.1 " TEST_ADDR_CONN_HOST "\" | sudo tee -a /etc/hosts > /dev/null");
HRN_SYSTEM("echo \"::1 " TEST_ADDR_CONN_HOST "\" | sudo tee -a /etc/hosts > /dev/null");
HRN_SYSTEM("echo \"172.31.255.255 " TEST_ADDR_CONN_HOST "\" | sudo tee -a /etc/hosts > /dev/null");
// Explicitly set the order of the address list for the test below. The first IP must not respond, the second must error,
// and only the last will succeed.
const String *ipPrefer = STRDEF("172.31.255.255");
TEST_ASSIGN(addrInfo, addrInfoNew(STRDEF(TEST_ADDR_CONN_HOST), 443), "localhost addr list");
TEST_RESULT_VOID(addrInfoPrefer(addrInfo, lstFindIdx(addrInfo->pub.list, &ipPrefer)), "prefer 172.31.255.255");
TEST_ASSIGN(addrInfo, addrInfoNew(STRDEF(TEST_ADDR_CONN_HOST), 443), "localhost addr list");
TEST_RESULT_VOID(addrInfoSort(addrInfo), "sort addresses");
TEST_RESULT_STR(addrInfoToStr(addrInfoGet(addrInfo, 0)->info), ipPrefer, "first 172.31.255.255");
TEST_RESULT_STR_Z(addrInfoToStr(addrInfoGet(addrInfo, 1)->info), "::1", "second ::1");
TEST_RESULT_STR_Z(addrInfoToStr(addrInfoGet(addrInfo, 2)->info), "127.0.0.1", "third 127.0.0.1");
HRN_FORK_BEGIN()
{
const unsigned int testPort = hrnServerPortNext();
HRN_FORK_CHILD_BEGIN(.prefix = "test server", .timeout = 5000)
{
TEST_RESULT_VOID(hrnServerRunP(HRN_FORK_CHILD_READ(), hrnServerProtocolSocket, testPort), "socket server");
}
HRN_FORK_CHILD_END();
HRN_FORK_PARENT_BEGIN()
{
IoWrite *server = hrnServerScriptBegin(HRN_FORK_PARENT_WRITE(0));
IoClient *client;
IoSession *session;
// -----------------------------------------------------------------------------------------------------------------
TEST_TITLE("connect to 127.0.0.1 when ::1 errors and 172.31.255.255 never responds");
TEST_ASSIGN(client, sckClientNew(STRDEF(TEST_ADDR_CONN_HOST), testPort, 1000, 1000), "new client");
hrnServerScriptAccept(server);
hrnServerScriptClose(server);
// Shim the server address to return false one time for write ready. This tests connections that take longer.
hrnSckClientOpenWaitShimInstall("127.0.0.1");
TEST_ASSIGN(session, ioClientOpen(client), "connection established");
TEST_RESULT_VOID(
sckClientOpenWait(
&(SckClientOpenData){.name = "test", .fd = ((SocketSession *)session->pub.driver)->fd, .errNo = EINTR}, 99),
"check EINTR wait condition");
TEST_RESULT_VOID(ioSessionFree(session), "connection closed");
// -----------------------------------------------------------------------------------------------------------------
TEST_TITLE("end server process");
hrnServerScriptEnd(server);
}
HRN_FORK_PARENT_END();
}
HRN_FORK_END();
#endif
// ------------------------------------------------------------------------------------------------------------------------- // -------------------------------------------------------------------------------------------------------------------------
TEST_TITLE("sckServerAccept() returns NULL on interrupt"); TEST_TITLE("sckServerAccept() returns NULL on interrupt");