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

Add SIGTERM and SIGHUP handling to TLS server.

SIGHUP allows the configuration to be reloaded. Note that the configuration will not be updated in child processes that have already started.

SIGTERM terminates the server process gracefully and sends SIGTERM to all child processes. This also gives the tests an easy way to stop the server.
This commit is contained in:
David Steele 2021-12-07 18:18:43 -05:00 committed by GitHub
parent 49145d72ba
commit 7b3ea883c7
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
7 changed files with 335 additions and 82 deletions

View File

@ -96,10 +96,14 @@
<release-development-list>
<release-item>
<commit subject="Rename server-start command to server."/>
<commit subject="Add SIGTERM and SIGHUP handling to TLS server.">
<github-pull-request id="1572"/>
</commit>
<release-item-contributor-list>
<release-item-contributor id="david.steele"/>
<release-item-reviewer id="stephen.frost"/>
<release-item-reviewer id="reid.thompson"/>
<!-- Actually tester, but we don't have a tag for that yet -->
<release-item-reviewer id="andrew.lecuyer"/>
</release-item-contributor-list>

View File

@ -8,74 +8,206 @@ Server Command
#include "command/remote/remote.h"
#include "command/server/server.h"
#include "common/debug.h"
#include "common/exit.h"
#include "common/fork.h"
#include "common/io/socket/server.h"
#include "common/io/tls/server.h"
#include "config/config.h"
#include "config/load.h"
#include "protocol/helper.h"
/***********************************************************************************************************************************
Local variables
***********************************************************************************************************************************/
static struct ServerLocal
{
MemContext *memContext; // Mem context for server
unsigned int argListSize; // Argument list size
const char **argList; // Argument list
List *processList; // List of child processes
bool sigHup; // SIGHUP was caught
bool sigTerm; // SIGTERM was caught
IoServer *socketServer; // Socket server
IoServer *tlsServer; // TLS server
} serverLocal;
/***********************************************************************************************************************************
Initialization can be redone when options change
***********************************************************************************************************************************/
static void
cmdServerInit(void)
{
// Initialize mem context
if (serverLocal.memContext == NULL)
{
MEM_CONTEXT_BEGIN(memContextTop())
{
MEM_CONTEXT_NEW_BEGIN("Server")
{
serverLocal.memContext = MEM_CONTEXT_NEW();
serverLocal.processList = lstNewP(sizeof(pid_t));
}
MEM_CONTEXT_NEW_END();
}
MEM_CONTEXT_END();
}
MEM_CONTEXT_BEGIN(serverLocal.memContext)
{
// Free old servers
ioServerFree(serverLocal.socketServer);
ioServerFree(serverLocal.tlsServer);
// Create new servers
serverLocal.socketServer = sckServerNew(
cfgOptionStr(cfgOptTlsServerAddress), cfgOptionUInt(cfgOptTlsServerPort), cfgOptionUInt64(cfgOptProtocolTimeout));
serverLocal.tlsServer = tlsServerNew(
cfgOptionStr(cfgOptTlsServerAddress), cfgOptionStr(cfgOptTlsServerCaFile), cfgOptionStr(cfgOptTlsServerKeyFile),
cfgOptionStr(cfgOptTlsServerCertFile), cfgOptionUInt64(cfgOptProtocolTimeout));
}
MEM_CONTEXT_END();
}
/***********************************************************************************************************************************
Handlers to set flags on signals
***********************************************************************************************************************************/
static void
cmdServerSigHup(const int signalType)
{
(void)signalType;
serverLocal.sigHup = true;
}
static void
cmdServerSigTerm(const int signalType)
{
(void)signalType;
serverLocal.sigTerm = true;
}
/***********************************************************************************************************************************
Handler to reap child processes
***********************************************************************************************************************************/
static void
cmdServerSigChild(const int signalType, siginfo_t *signalInfo, void *context)
{
(void)signalType;
(void)context;
ASSERT(signalInfo->si_code == CLD_EXITED);
// Find the process and remove it
for (unsigned int processIdx = 0; processIdx < lstSize(serverLocal.processList); processIdx++)
{
if (*(int *)lstGet(serverLocal.processList, processIdx) == signalInfo->si_pid)
lstRemoveIdx(serverLocal.processList, processIdx);
}
}
/**********************************************************************************************************************************/
void
cmdServer(uint64_t connectionMax)
cmdServer(const unsigned int argListSize, const char *argList[])
{
FUNCTION_LOG_VOID(logLevelDebug);
FUNCTION_LOG_BEGIN(logLevelDebug);
FUNCTION_LOG_PARAM(UINT, argListSize);
FUNCTION_LOG_PARAM(CHARPY, argList);
FUNCTION_LOG_END();
ASSERT(connectionMax > 0);
// Initialize server
cmdServerInit();
// Set arguments used for reload
serverLocal.argListSize = argListSize;
serverLocal.argList = argList;
MEM_CONTEXT_TEMP_BEGIN()
{
IoServer *const tlsServer = tlsServerNew(
cfgOptionStr(cfgOptTlsServerAddress), cfgOptionStr(cfgOptTlsServerCaFile), cfgOptionStr(cfgOptTlsServerKeyFile),
cfgOptionStr(cfgOptTlsServerCertFile), cfgOptionUInt64(cfgOptProtocolTimeout));
IoServer *const socketServer = sckServerNew(
cfgOptionStr(cfgOptTlsServerAddress), cfgOptionUInt(cfgOptTlsServerPort), cfgOptionUInt64(cfgOptProtocolTimeout));
// Set signal handlers
sigaction(SIGHUP, &(struct sigaction){.sa_handler = cmdServerSigHup}, NULL);
sigaction(SIGTERM, &(struct sigaction){.sa_handler = cmdServerSigTerm}, NULL);
sigaction(
SIGCHLD, &(struct sigaction){.sa_sigaction = cmdServerSigChild, .sa_flags = SA_NOCLDSTOP | SA_NOCLDWAIT | SA_SIGINFO},
NULL);
// Accept connections until connection max is reached
// Accept connections indefinitely. The only way to exit this loop is for the process to receive a signal.
do
{
// Accept a new connection
IoSession *const socketSession = ioServerAccept(socketServer, NULL);
IoSession *const socketSession = ioServerAccept(serverLocal.socketServer, NULL);
// Fork off the child process
pid_t pid = forkSafe();
if (pid == 0)
if (socketSession != NULL)
{
// Close the server socket so we don't hold the port open if the parent exits first
ioServerFree(socketServer);
// Fork off the child process
pid_t pid = forkSafe();
// Disable logging and close log file
logClose();
if (pid == 0)
{
// Reset SIGCHLD to default
sigaction(SIGCHLD, &(struct sigaction){.sa_handler = SIG_DFL}, NULL);
// Detach from parent process
forkDetach();
// Set standard signal handlers
exitInit();
// Start standard remote processing if a server is returned
ProtocolServer *server = protocolServer(tlsServer, socketSession);
// Close the server socket so we don't hold the port open if the parent exits first
ioServerFree(serverLocal.socketServer);
if (server != NULL)
cmdRemote(server);
// Disable logging and close log file
logClose();
break;
}
// Wait for first fork to exit
else
{
// The process that was just forked should return immediately
int processStatus;
// Start standard remote processing if a server is returned
ProtocolServer *server = protocolServer(serverLocal.tlsServer, socketSession);
THROW_ON_SYS_ERROR(waitpid(pid, &processStatus, 0) == -1, ExecuteError, "unable to wait for forked process");
if (server != NULL)
cmdRemote(server);
// The first fork should exit with success. If not, something went wrong during the second fork.
CHECK(ExecuteError, WIFEXITED(processStatus) && WEXITSTATUS(processStatus) == 0, "error on first fork");
break;
}
// Add process to list
else
lstAdd(serverLocal.processList, &pid);
// Free the socket since the child is now using it
ioSessionFree(socketSession);
}
// Free the socket since the child is now using it
ioSessionFree(socketSession);
// Reload configuration
if (serverLocal.sigHup)
{
LOG_DETAIL("configuration reload begin");
// Reload configuration
cfgLoad(serverLocal.argListSize, serverLocal.argList);
// Reinitialize server
cmdServerInit();
LOG_DETAIL("configuration reload end");
// Reset flag
serverLocal.sigHup = false;
}
}
while (--connectionMax > 0);
while (!serverLocal.sigTerm);
}
MEM_CONTEXT_TEMP_END();
// Terminate any remaining children on SIGTERM. Disable the callback so it does not fire in the middle of the loop.
if (serverLocal.sigTerm)
{
sigaction(SIGCHLD, &(struct sigaction){.sa_flags = SA_NOCLDSTOP | SA_NOCLDWAIT}, NULL);
for (unsigned int processIdx = 0; processIdx < lstSize(serverLocal.processList); processIdx++)
{
pid_t pid = *(int *)lstGet(serverLocal.processList, processIdx);
LOG_WARN_FMT("terminate child process %d", pid);
kill(pid, SIGTERM);
}
}
FUNCTION_LOG_RETURN_VOID();
}

View File

@ -9,6 +9,6 @@ Server Command
/***********************************************************************************************************************************
Functions
***********************************************************************************************************************************/
void cmdServer(uint64_t connectionMax);
void cmdServer(unsigned int argListSize, const char *argList[]);
#endif

View File

@ -96,18 +96,21 @@ sckServerAccept(THIS_VOID, IoSession *const session)
int serverSocket = accept(this->socket, (struct sockaddr *)&addr, &len);
THROW_ON_SYS_ERROR(serverSocket == -1, FileOpenError, "unable to accept socket");
// Create socket session
sckOptionSet(serverSocket);
MEM_CONTEXT_PRIOR_BEGIN()
if (serverSocket != -1)
{
result = sckSessionNew(ioSessionRoleServer, serverSocket, this->address, this->port, this->timeout);
}
MEM_CONTEXT_PRIOR_END();
// Create socket session
sckOptionSet(serverSocket);
statInc(SOCKET_STAT_SESSION_STR);
MEM_CONTEXT_PRIOR_BEGIN()
{
result = sckSessionNew(ioSessionRoleServer, serverSocket, this->address, this->port, this->timeout);
}
MEM_CONTEXT_PRIOR_END();
statInc(SOCKET_STAT_SESSION_STR);
}
else
THROW_ON_SYS_ERROR(errno != EINTR, FileOpenError, "unable to accept socket");
}
MEM_CONTEXT_TEMP_END();

View File

@ -236,7 +236,7 @@ main(int argListSize, const char *argList[])
// Server command
// -----------------------------------------------------------------------------------------------------------------
case cfgCmdServer:
cmdServer(UINT64_MAX);
cmdServer((unsigned int)argListSize, argList);
break;
// Server ping command

View File

@ -1,6 +1,7 @@
/***********************************************************************************************************************************
Test Server Command
***********************************************************************************************************************************/
#include "common/exit.h"
#include "storage/posix/storage.h"
#include "storage/remote/storage.h"
@ -26,7 +27,7 @@ testRun(void)
{
TEST_TITLE("server");
HRN_FORK_BEGIN(.timeout = 5000)
HRN_FORK_BEGIN(.timeout = 15000)
{
HRN_FORK_CHILD_BEGIN(.prefix = "client repo")
{
@ -109,27 +110,72 @@ testRun(void)
}
HRN_FORK_CHILD_END();
HRN_FORK_PARENT_BEGIN(.prefix = "server")
HRN_FORK_PARENT_BEGIN(.prefix = "client control")
{
StringList *argList = strLstNew();
hrnCfgArgRawZ(argList, cfgOptTlsServerCaFile, HRN_SERVER_CA);
hrnCfgArgRawZ(argList, cfgOptTlsServerCertFile, HRN_SERVER_CERT);
hrnCfgArgRawZ(argList, cfgOptTlsServerKeyFile, HRN_SERVER_KEY);
hrnCfgArgRawZ(argList, cfgOptTlsServerAuth, "pgbackrest-client=db");
hrnCfgArgRawFmt(argList, cfgOptTlsServerPort, "%u", hrnServerPort(0));
HRN_CFG_LOAD(cfgCmdServer, argList);
HRN_FORK_BEGIN(.timeout = 15000)
{
HRN_FORK_CHILD_BEGIN(.prefix = "server")
{
// Write a config file to demonstrate that options are loaded and reloaded
HRN_STORAGE_PUT_Z(
storageTest,
"pgbackrest.conf",
"[global]\n"
CFGOPT_TLS_SERVER_CA_FILE "=" HRN_SERVER_CA "\n"
CFGOPT_TLS_SERVER_CERT_FILE "=" HRN_SERVER_CERT "\n"
CFGOPT_TLS_SERVER_KEY_FILE "=" HRN_SERVER_KEY "\n"
CFGOPT_TLS_SERVER_AUTH "=pgbackrest-client=db\n"
"repo1-path=" TEST_PATH "/repo\n");
// Write a config file to demonstrate that settings are loaded
HRN_STORAGE_PUT_Z(storageTest, "pgbackrest.conf", "[global]\nrepo1-path=" TEST_PATH "/repo");
StringList *argList = strLstNew();
hrnCfgArgRawZ(argList, cfgOptConfig, TEST_PATH "/pgbackrest.conf");
hrnCfgArgRawFmt(argList, cfgOptTlsServerPort, "%u", hrnServerPort(0));
HRN_CFG_LOAD(cfgCmdServer, argList);
// Get pid of this process to identify child process later
pid_t pid = getpid();
// Init exit signal handlers
exitInit();
TEST_RESULT_VOID(cmdServer(3), "server");
// No log testing needed
harnessLogLevelSet(logLevelError);
// If this is a child process then exit immediately
if (pid != getpid())
exit(0);
// Add a fake pid to ensure SIGTERM is sent to unterminated children
cmdServerInit();
int fakePid = INT_MAX;
lstAdd(serverLocal.processList, &fakePid);
// Get pid of this process to identify child process later
pid_t pid = getpid();
// Add parameters to arg list required for a reload
strLstInsert(argList, 0, cfgExe());
strLstAddZ(argList, CFGCMD_SERVER);
TEST_RESULT_VOID(cmdServer(strLstSize(argList), strLstPtr(argList)), "server");
// If this is a child process then exit immediately
if (pid != getpid())
{
HRN_FORK_CHILD_NOTIFY_PUT();
exit(0);
}
}
HRN_FORK_CHILD_END();
HRN_FORK_PARENT_BEGIN(.prefix = "server control")
{
// Wait for forked server processes to exit
HRN_FORK_PARENT_NOTIFY_GET(0);
kill(HRN_FORK_PROCESS_ID(0), SIGHUP);
HRN_FORK_PARENT_NOTIFY_GET(0);
HRN_FORK_PARENT_NOTIFY_GET(0);
// Send term to server processes
kill(HRN_FORK_PROCESS_ID(0), SIGTERM);
}
HRN_FORK_PARENT_END();
}
HRN_FORK_END();
// Wait for both child processes to exit
HRN_FORK_PARENT_NOTIFY_GET(0);
@ -157,7 +203,7 @@ testRun(void)
TEST_ERROR(cmdServerPing(), ParamInvalidError, "extra parameters found");
HRN_FORK_BEGIN(.timeout = 5000)
HRN_FORK_BEGIN(.timeout = 15000)
{
HRN_FORK_CHILD_BEGIN(.prefix = "client")
@ -185,24 +231,52 @@ testRun(void)
}
HRN_FORK_CHILD_END();
HRN_FORK_PARENT_BEGIN(.prefix = "server")
HRN_FORK_PARENT_BEGIN(.prefix = "client control")
{
StringList *argList = strLstNew();
hrnCfgArgRawZ(argList, cfgOptTlsServerCaFile, HRN_SERVER_CA);
hrnCfgArgRawZ(argList, cfgOptTlsServerCertFile, HRN_SERVER_CERT);
hrnCfgArgRawZ(argList, cfgOptTlsServerKeyFile, HRN_SERVER_KEY);
hrnCfgArgRawZ(argList, cfgOptTlsServerAuth, "bogus=*");
hrnCfgArgRawFmt(argList, cfgOptTlsServerPort, "%u", hrnServerPort(0));
HRN_CFG_LOAD(cfgCmdServer, argList);
HRN_FORK_BEGIN(.timeout = 15000)
{
HRN_FORK_CHILD_BEGIN(.prefix = "server")
{
StringList *argList = strLstNew();
hrnCfgArgRawZ(argList, cfgOptTlsServerCaFile, HRN_SERVER_CA);
hrnCfgArgRawZ(argList, cfgOptTlsServerCertFile, HRN_SERVER_CERT);
hrnCfgArgRawZ(argList, cfgOptTlsServerKeyFile, HRN_SERVER_KEY);
hrnCfgArgRawZ(argList, cfgOptTlsServerAuth, "bogus=*");
hrnCfgArgRawFmt(argList, cfgOptTlsServerPort, "%u", hrnServerPort(0));
HRN_CFG_LOAD(cfgCmdServer, argList);
// Get pid of this process to identify child process later
pid_t pid = getpid();
// Init exit signal handlers
exitInit();
TEST_RESULT_VOID(cmdServer(2), "server");
// No log testing needed
harnessLogLevelSet(logLevelError);
// If this is a child process then exit immediately
if (pid != getpid())
exit(0);
// Get pid of this process to identify child process later
pid_t pid = getpid();
TEST_RESULT_VOID(cmdServer(strLstSize(argList), strLstPtr(argList)), "server");
// If this is a child process then exit immediately
if (pid != getpid())
{
HRN_FORK_CHILD_NOTIFY_PUT();
exit(0);
}
}
HRN_FORK_CHILD_END();
HRN_FORK_PARENT_BEGIN(.prefix = "server control")
{
// Wait for forked child processes to exit
HRN_FORK_PARENT_NOTIFY_GET(0);
HRN_FORK_PARENT_NOTIFY_GET(0);
// Send term to child processes
kill(HRN_FORK_PROCESS_ID(0), SIGTERM);
}
HRN_FORK_PARENT_END();
}
HRN_FORK_END();
// Wait for child process to exit
HRN_FORK_PARENT_NOTIFY_GET(0);

View File

@ -127,6 +127,15 @@ static const char *const testClientBadCa =
"qGj7FtRiSdjkZ7pmNpma6ycPR0RBZyL3aHnig+DDfRRt8TgrZzY3aXBReONb\n"
"-----END CERTIFICATE-----";
/***********************************************************************************************************************************
Test signal handler that does nothing
***********************************************************************************************************************************/
static void
testSignalHandler(const int signalType)
{
(void)signalType;
}
/***********************************************************************************************************************************
Test Run
***********************************************************************************************************************************/
@ -299,7 +308,7 @@ testRun(void)
}
// *****************************************************************************************************************************
if (testBegin("SocketClient"))
if (testBegin("SocketClient/SocketServer"))
{
IoClient *client = NULL;
@ -311,6 +320,37 @@ 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"), hrnServerPort(0), 100, 100), "new client");
TEST_ERROR_FMT(ioClientOpen(client), HostConnectError, "timeout connecting to '172.31.255.255:%u'", hrnServerPort(0));
// -------------------------------------------------------------------------------------------------------------------------
TEST_TITLE("sckServerAccept() returns NULL on interrupt");
HRN_FORK_BEGIN(.timeout = 5000)
{
HRN_FORK_CHILD_BEGIN(.prefix = "sighup server")
{
// Ignore SIGHUP
sigaction(SIGHUP, &(struct sigaction){.sa_handler = testSignalHandler}, NULL);
// Wait for connection. Use port 1 to avoid port conflicts later.
IoServer *server = sckServerNew(STRDEF("127.0.0.1"), hrnServerPort(1), 5000);
HRN_FORK_CHILD_NOTIFY_PUT();
TEST_RESULT_PTR(ioServerAccept(server, NULL), NULL, "connection interrupted");
}
HRN_FORK_CHILD_END();
HRN_FORK_PARENT_BEGIN(.prefix = "sighup client")
{
// Wait for client to be ready but also sleep a bit more to allow accept to initialize
HRN_FORK_PARENT_NOTIFY_GET(0);
sleep(1);
// Send SIGHUP and the client should exit
kill(HRN_FORK_PROCESS_ID(0), SIGHUP);
}
HRN_FORK_PARENT_END();
}
HRN_FORK_END();
}
// Additional coverage not provided by testing with actual certificates