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-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>
<commit subject="Make meson the primary build system.">
<github-pull-request id="2264"/>

View File

@ -18,8 +18,25 @@ Object type
struct AddressInfo
{
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()
***********************************************************************************************************************************/
@ -34,11 +51,90 @@ addrInfoFreeResource(THIS_VOID)
ASSERT(this != NULL);
freeaddrinfo(*(struct addrinfo **)lstGet(this->pub.list, 0));
freeaddrinfo(this->info);
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 *
addrInfoNew(const String *const host, unsigned int port)
@ -51,6 +147,21 @@ addrInfoNew(const String *const host, unsigned int port)
ASSERT(host != NULL);
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)
{
*this = (AddressInfo)
@ -59,7 +170,7 @@ addrInfoNew(const String *const host, unsigned int port)
{
.host = strDup(host),
.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));
// Do the lookup
struct addrinfo *result;
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));
// Set free callback to ensure address info is freed
memContextCallbackSet(objMemContext(this), addrInfoFreeResource, this);
// Convert address linked list to list
lstAdd(this->pub.list, &result);
while (result->ai_next != NULL)
MEM_CONTEXT_OBJ_BEGIN(this->pub.list)
{
lstAdd(this->pub.list, &result->ai_next);
result = result->ai_next;
struct addrinfo *info = this->info;
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();
}
@ -104,6 +220,41 @@ addrInfoNew(const String *const host, unsigned int port)
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
***********************************************************************************************************************************/
@ -166,21 +317,18 @@ addrInfoToStr(const struct addrinfo *const addrInfo)
FN_EXTERN void
addrInfoToLog(const AddressInfo *const this, StringStatic *const debugLog)
{
char address[48];
strStcFmt(debugLog, "{host: ");
strToLog(addrInfoHost(this), debugLog);
strStcFmt(debugLog, ", port: %u, list: [", addrInfoPort(this));
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)
strStcCat(debugLog, ", ");
addrInfoToZ(addrInfo, address, sizeof(address));
strStcCat(debugLog, address);
strStcCat(debugLog, strZ(addrItem->name));
}
strStcCat(debugLog, "]}");

View File

@ -25,6 +25,12 @@ FN_EXTERN AddressInfo *addrInfoNew(const String *const host, unsigned int port);
/***********************************************************************************************************************************
Getters/Setters
***********************************************************************************************************************************/
typedef struct AddressInfoItem
{
String *name; // Address as a string
struct addrinfo *info; // Full address info
} AddressInfoItem;
typedef struct AddressInfoPub
{
const String *host; // Host for address info lookup
@ -33,10 +39,10 @@ typedef struct AddressInfoPub
} AddressInfoPub;
// Get address
FN_INLINE_ALWAYS const struct addrinfo *
FN_INLINE_ALWAYS const AddressInfoItem *
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
@ -60,6 +66,15 @@ addrInfoSize(const AddressInfo *const this)
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
***********************************************************************************************************************************/

View File

@ -11,6 +11,7 @@ Socket Client
#include "common/debug.h"
#include "common/error/retry.h"
#include "common/io/client.h"
#include "common/io/fd.h"
#include "common/io/socket/address.h"
#include "common/io/socket/client.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) \
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 *
sckClientOpen(THIS_VOID)
{
@ -77,47 +135,124 @@ sckClientOpen(THIS_VOID)
ErrorRetry *const errRetry = errRetryNew();
bool retry;
// Get an address list for the host
const AddressInfo *const addrInfo = addrInfoNew(this->host, this->port);
unsigned int addrInfoIdx = 0;
// Get an address list for the host and sort it
AddressInfo *const addrInfo = addrInfoNew(this->host, this->port);
addrInfoSort(addrInfo);
// Try addresses until success or timeout
List *const openDataList = lstNewP(sizeof(SckClientOpenData));
volatile unsigned int addrInfoIdx = 0;
do
{
// Assume there will be no retry
SckClientOpenData *volatile openData = NULL;
const ErrorType *errLastType = NULL;
String *errLastMessage = NULL;
retry = false;
volatile int fd = -1;
// Try connection
TRY_BEGIN()
{
// Get next address for the host
const struct addrinfo *const addressFound = addrInfoGet(addrInfo, addrInfoIdx);
// 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()
// Iterate waiting connections and see if any of them have completed
for (unsigned int openDataIdx = 0; openDataIdx < lstSize(openDataList); openDataIdx++)
{
result = sckSessionNew(ioSessionRoleClient, fd, this->host, this->port, this->timeoutSession);
}
MEM_CONTEXT_PRIOR_END();
SckClientOpenData *const openDataWait = lstGet(openDataList, openDataIdx);
// Update client name to include address
strTrunc(this->name);
strCat(this->name, addrInfoToName(this->host, this->port, addressFound));
if (openDataWait->fd != 0 && sckClientOpenWait(openDataWait, 0))
{
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()
{
// 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
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
// to hopefully have better chance at succeeding, otherwise continue right to the next address.
addrInfoIdx++;
@ -130,18 +265,47 @@ sckClientOpen(THIS_VOID)
else
retry = true;
// Error when no retries remain
// General timeout errors when no retries remain
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
LOG_DEBUG_FMT("retry %s: %s", errorTypeName(errorType()), errorMessage());
statInc(SOCKET_STAT_RETRY_STR);
if (openDataTimeout->fd != 0)
{
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);
// 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);
}
MEM_CONTEXT_TEMP_END();

View File

@ -9,7 +9,6 @@ Socket Common Functions
#include <sys/socket.h>
#include "common/debug.h"
#include "common/io/fd.h"
#include "common/io/socket/address.h"
#include "common/io/socket/common.h"
#include "common/log.h"
@ -137,55 +136,3 @@ sckOptionSet(int fd)
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
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

View File

@ -165,7 +165,7 @@ sckServerNew(const String *const address, const unsigned int port, const TimeMSe
};
// 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
THROW_ON_SYS_ERROR(

View File

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

View File

@ -3,7 +3,6 @@ Harness for Io Testing
***********************************************************************************************************************************/
#include "build.auto.h"
#include "common/harnessConfig.h"
#include "common/harnessDebug.h"
#include "common/harnessSocket.h"
@ -19,8 +18,61 @@ static struct
{
// Shim clientOpen() to use a fake file descriptor
bool localShimSckClientOpen;
// Shim sckClientOpenWait() to return false for an address one time
const char *clientOpenWaitAddress;
} 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()
***********************************************************************************************************************************/

View File

@ -11,6 +11,9 @@ Constants
/***********************************************************************************************************************************
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
void hrnSckClientOpenShimInstall(void);
void hrnSckClientOpenShimUninstall(void);

View File

@ -5,6 +5,7 @@ Test HTTP
#include "common/io/fdRead.h"
#include "common/io/fdWrite.h"
#include "common/io/socket/address.h"
#include "common/io/socket/client.h"
#include "common/io/tls/client.h"
@ -25,6 +26,13 @@ testRun(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()"))
{

View File

@ -155,20 +155,22 @@ testRun(void)
if (testBegin("AddressInfo"))
{
#ifdef TEST_CONTAINER_REQUIRED
HRN_SYSTEM("echo \"127.0.0.1 test-addr-loop.pgbackrest.org\" | sudo tee -a /etc/hosts > /dev/null");
HRN_SYSTEM("echo \"::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 \"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");
AddressInfo *addrInfo = NULL;
TEST_ASSIGN(addrInfo, addrInfoNew(STRDEF("test-addr-loop.pgbackrest.org"), 443), "addr list");
TEST_RESULT_STR_Z(addrInfoHost(addrInfo), "test-addr-loop.pgbackrest.org", "check host");
TEST_ASSIGN(addrInfo, addrInfoNew(STRDEF(TEST_ADDR_LOOP_HOST), 443), "addr list");
TEST_RESULT_STR_Z(addrInfoHost(addrInfo), TEST_ADDR_LOOP_HOST, "check host");
TEST_RESULT_UINT(addrInfoPort(addrInfo), 443, "check port");
TEST_RESULT_UINT(addrInfoSize(addrInfo), 2, "check size");
// -------------------------------------------------------------------------------------------------------------------------
TEST_TITLE("addrInfoToLog");
TEST_TITLE("addrInfoToLog()");
char logBuf[STACK_TRACE_PARAM_MAX];
@ -176,21 +178,94 @@ testRun(void)
TEST_RESULT_Z(
logBuf,
zNewFmt(
"{host: {\"test-addr-loop.pgbackrest.org\"}, port: 443, list: [%s, %s]}",
strZ(addrInfoToStr(addrInfoGet(addrInfo, 0))), strZ(addrInfoToStr(addrInfoGet(addrInfo, 1)))),
"{host: {\"" TEST_ADDR_LOOP_HOST "\"}, port: 443, list: [%s, %s]}",
strZ(addrInfoGet(addrInfo, 0)->name), strZ(addrInfoGet(addrInfo, 1)->name)),
"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
(*(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_Z(
logBuf,
zNewFmt(
"{host: {\"test-addr-loop.pgbackrest.org\"}, port: 443, list: [invalid, %s]}",
strZ(addrInfoToStr(addrInfoGet(addrInfo, 1)))),
"check log");
TEST_RESULT_STR_Z(addrInfoToStr(addrInfoGet(addrInfo, 0)), "invalid", "check invalid");
logBuf, zNewFmt("{host: {\"test\"}, port: 443, list: [::1, 127.0.0.1, ::2, 127.0.0.2, ::3, ::4]}"), "check log");
// -------------------------------------------------------------------------------------------------------------------------
TEST_TITLE("addrInfoSort()");
// 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");
@ -343,11 +418,6 @@ testRun(void)
TEST_ERROR(
ioClientOpen(socketClient), HostConnectError, "unable to connect to '127.0.0.1:7777': [111] Connection refused");
socketLocal.block = false;
// ---------------------------------------------------------------------------------------------------------------------
TEST_TITLE("uncovered conditions for sckConnect()");
TEST_RESULT_BOOL(sckConnectInProgress(EINTR), true, "connection in progress (EINTR)");
}
FINALLY()
{
@ -365,6 +435,11 @@ testRun(void)
if (testBegin("SocketClient/SocketServer"))
{
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_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
TEST_ASSIGN(client, sckClientNew(STRDEF("172.31.255.255"), HRN_SERVER_PORT_BOGUS, 100, 100), "new client");
TEST_ERROR(
ioClientOpen(client), HostConnectError,
"timeout connecting to '172.31.255.255:34342'\n"
"[RETRY DETAIL OMITTED]");
TEST_ERROR(ioClientOpen(client), HostConnectError, "timeout connecting to '172.31.255.255:34342'");
// -------------------------------------------------------------------------------------------------------------------------
#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");