diff --git a/doc/xml/release.xml b/doc/xml/release.xml index 83860ad31..2292f71cf 100644 --- a/doc/xml/release.xml +++ b/doc/xml/release.xml @@ -70,6 +70,14 @@

Add Perl interface to C storage layer.

+ + + + + +

Add Db object to encapsulate queries and commands.

+
+ diff --git a/src/Makefile.in b/src/Makefile.in index c6c411141..54063ee21 100644 --- a/src/Makefile.in +++ b/src/Makefile.in @@ -120,6 +120,9 @@ SRCS = \ config/load.c \ config/parse.c \ config/protocol.c \ + db/db.c \ + db/helper.c \ + db/protocol.c \ info/info.c \ info/infoArchive.c \ info/infoBackup.c \ @@ -249,7 +252,7 @@ command/info/info.o: command/info/info.c build.auto.h command/archive/common.h c command/local/local.o: command/local/local.c build.auto.h command/archive/get/protocol.h command/archive/push/protocol.h command/backup/protocol.h command/restore/protocol.h common/assert.h common/debug.h common/error.auto.h common/error.h common/io/filter/filter.h common/io/filter/group.h common/io/handleRead.h common/io/handleWrite.h common/io/read.h common/io/write.h common/lock.h common/log.h common/logLevel.h common/memContext.h common/stackTrace.h common/time.h common/type/buffer.h common/type/convert.h common/type/keyValue.h common/type/string.h common/type/stringList.h common/type/variant.h common/type/variantList.h config/config.auto.h config/config.h config/define.auto.h config/define.h config/protocol.h protocol/client.h protocol/command.h protocol/helper.h protocol/server.h $(CC) $(CPPFLAGS) $(CFLAGS) $(CMAKE) -c command/local/local.c -o command/local/local.o -command/remote/remote.o: command/remote/remote.c build.auto.h common/assert.h common/debug.h common/error.auto.h common/error.h common/io/filter/filter.h common/io/filter/group.h common/io/handleRead.h common/io/handleWrite.h common/io/read.h common/io/write.h common/lock.h common/log.h common/logLevel.h common/memContext.h common/stackTrace.h common/time.h common/type/buffer.h common/type/convert.h common/type/keyValue.h common/type/string.h common/type/stringList.h common/type/variant.h common/type/variantList.h config/config.auto.h config/config.h config/define.auto.h config/define.h config/protocol.h protocol/client.h protocol/command.h protocol/helper.h protocol/server.h storage/remote/protocol.h +command/remote/remote.o: command/remote/remote.c build.auto.h common/assert.h common/debug.h common/error.auto.h common/error.h common/io/filter/filter.h common/io/filter/group.h common/io/handleRead.h common/io/handleWrite.h common/io/read.h common/io/write.h common/lock.h common/log.h common/logLevel.h common/memContext.h common/stackTrace.h common/time.h common/type/buffer.h common/type/convert.h common/type/keyValue.h common/type/string.h common/type/stringList.h common/type/variant.h common/type/variantList.h config/config.auto.h config/config.h config/define.auto.h config/define.h config/protocol.h db/protocol.h protocol/client.h protocol/command.h protocol/helper.h protocol/server.h storage/remote/protocol.h $(CC) $(CPPFLAGS) $(CFLAGS) $(CMAKE) -c command/remote/remote.c -o command/remote/remote.o command/restore/file.o: command/restore/file.c build.auto.h command/restore/file.h common/assert.h common/compress/gzip/common.h common/compress/gzip/decompress.h common/crypto/cipherBlock.h common/crypto/common.h common/crypto/hash.h common/debug.h common/error.auto.h common/error.h common/io/filter/filter.h common/io/filter/group.h common/io/filter/size.h common/io/io.h common/io/read.h common/io/write.h common/lock.h common/log.h common/logLevel.h common/memContext.h common/stackTrace.h common/time.h common/type/buffer.h common/type/convert.h common/type/keyValue.h common/type/string.h common/type/stringList.h common/type/variant.h common/type/variantList.h config/config.auto.h config/config.h config/define.auto.h config/define.h storage/helper.h storage/info.h storage/read.h storage/storage.h storage/write.h @@ -426,6 +429,15 @@ config/parse.o: config/parse.c build.auto.h common/assert.h common/debug.h commo config/protocol.o: config/protocol.c build.auto.h common/assert.h common/debug.h common/error.auto.h common/error.h common/io/filter/filter.h common/io/filter/group.h common/io/io.h common/io/read.h common/io/write.h common/lock.h common/log.h common/logLevel.h common/memContext.h common/stackTrace.h common/time.h common/type/buffer.h common/type/convert.h common/type/keyValue.h common/type/string.h common/type/stringList.h common/type/variant.h common/type/variantList.h config/config.auto.h config/config.h config/define.auto.h config/define.h config/protocol.h protocol/client.h protocol/command.h protocol/server.h $(CC) $(CPPFLAGS) $(CFLAGS) $(CMAKE) -c config/protocol.c -o config/protocol.o +db/db.o: db/db.c build.auto.h common/assert.h common/debug.h common/error.auto.h common/error.h common/io/filter/filter.h common/io/filter/group.h common/io/read.h common/io/write.h common/log.h common/logLevel.h common/macro.h common/memContext.h common/object.h common/stackTrace.h common/time.h common/type/buffer.h common/type/convert.h common/type/keyValue.h common/type/string.h common/type/stringList.h common/type/variant.h common/type/variantList.h db/db.h db/protocol.h postgres/client.h postgres/interface.h postgres/version.h protocol/client.h protocol/command.h protocol/server.h version.h + $(CC) $(CPPFLAGS) $(CFLAGS) $(CMAKE) -c db/db.c -o db/db.o + +db/helper.o: db/helper.c build.auto.h common/assert.h common/debug.h common/error.auto.h common/error.h common/io/filter/filter.h common/io/filter/group.h common/io/read.h common/io/write.h common/lock.h common/log.h common/logLevel.h common/memContext.h common/stackTrace.h common/time.h common/type/buffer.h common/type/convert.h common/type/keyValue.h common/type/string.h common/type/stringList.h common/type/variant.h common/type/variantList.h config/config.auto.h config/config.h config/define.auto.h config/define.h db/db.h db/helper.h postgres/client.h postgres/interface.h protocol/client.h protocol/command.h protocol/helper.h version.h + $(CC) $(CPPFLAGS) $(CFLAGS) $(CMAKE) -c db/helper.c -o db/helper.o + +db/protocol.o: db/protocol.c build.auto.h common/assert.h common/debug.h common/error.auto.h common/error.h common/io/filter/filter.h common/io/filter/group.h common/io/io.h common/io/read.h common/io/write.h common/lock.h common/log.h common/logLevel.h common/memContext.h common/stackTrace.h common/time.h common/type/buffer.h common/type/convert.h common/type/keyValue.h common/type/list.h common/type/string.h common/type/stringList.h common/type/variant.h common/type/variantList.h config/config.auto.h config/config.h config/define.auto.h config/define.h db/protocol.h postgres/client.h postgres/interface.h protocol/client.h protocol/command.h protocol/server.h + $(CC) $(CPPFLAGS) $(CFLAGS) $(CMAKE) -c db/protocol.c -o db/protocol.o + info/info.o: info/info.c build.auto.h common/assert.h common/crypto/cipherBlock.h common/crypto/common.h common/crypto/hash.h common/debug.h common/error.auto.h common/error.h common/ini.h common/io/filter/filter.h common/io/filter/filter.intern.h common/io/filter/group.h common/io/read.h common/io/write.h common/log.h common/logLevel.h common/macro.h common/memContext.h common/object.h common/stackTrace.h common/time.h common/type/buffer.h common/type/convert.h common/type/json.h common/type/keyValue.h common/type/string.h common/type/stringList.h common/type/variant.h common/type/variantList.h info/info.h storage/helper.h storage/info.h storage/read.h storage/storage.h storage/write.h version.h $(CC) $(CPPFLAGS) $(CFLAGS) $(CMAKE) -c info/info.c -o info/info.o diff --git a/src/command/remote/remote.c b/src/command/remote/remote.c index 79fcacfa4..58fbbca01 100644 --- a/src/command/remote/remote.c +++ b/src/command/remote/remote.c @@ -11,6 +11,7 @@ Remote Command #include "common/log.h" #include "config/config.h" #include "config/protocol.h" +#include "db/protocol.h" #include "protocol/helper.h" #include "protocol/server.h" #include "storage/remote/protocol.h" @@ -33,6 +34,7 @@ cmdRemote(int handleRead, int handleWrite) ProtocolServer *server = protocolServerNew(name, PROTOCOL_SERVICE_REMOTE_STR, read, write); protocolServerHandlerAdd(server, storageRemoteProtocol); + protocolServerHandlerAdd(server, dbProtocol); protocolServerHandlerAdd(server, configProtocol); // Acquire a lock if this command needs one. We'll use the noop that is always sent from the client right after the diff --git a/src/db/db.c b/src/db/db.c new file mode 100644 index 000000000..1e0d67c3c --- /dev/null +++ b/src/db/db.c @@ -0,0 +1,313 @@ +/*********************************************************************************************************************************** +Database Client +***********************************************************************************************************************************/ +#include "build.auto.h" + +#include "common/debug.h" +#include "common/log.h" +#include "common/memContext.h" +#include "common/object.h" +#include "db/db.h" +#include "db/protocol.h" +#include "postgres/interface.h" +#include "postgres/version.h" +#include "version.h" + +/*********************************************************************************************************************************** +Object type +***********************************************************************************************************************************/ +struct Db +{ + MemContext *memContext; + PgClient *client; // Local PostgreSQL client + ProtocolClient *remoteClient; // Protocol client for remote db queries + unsigned int remoteIdx; // Index provided by the remote on open for subsequent calls + const String *applicationName; // Used to identify this connection in PostgreSQL + + unsigned int pgVersion; // Version as reported by the database + const String *pgDataPath; // Data directory reported by the database +}; + +OBJECT_DEFINE_FREE(DB); + +/*********************************************************************************************************************************** +Close protocol connection. No need to close a locally created PgClient since it has its own destructor. +***********************************************************************************************************************************/ +OBJECT_DEFINE_FREE_RESOURCE_BEGIN(DB, LOG, logLevelTrace) +{ + ProtocolCommand *command = protocolCommandNew(PROTOCOL_COMMAND_DB_CLOSE_STR); + protocolCommandParamAdd(command, VARUINT(this->remoteIdx)); + + protocolClientExecute(this->remoteClient, command, false); +} +OBJECT_DEFINE_FREE_RESOURCE_END(LOG); + +/*********************************************************************************************************************************** +Create object +***********************************************************************************************************************************/ +Db * +dbNew(PgClient *client, ProtocolClient *remoteClient, const String *applicationName) +{ + FUNCTION_LOG_BEGIN(logLevelDebug); + FUNCTION_LOG_PARAM(PG_CLIENT, client); + FUNCTION_LOG_PARAM(PROTOCOL_CLIENT, remoteClient); + FUNCTION_LOG_PARAM(STRING, applicationName); + FUNCTION_LOG_END(); + + ASSERT((client != NULL && remoteClient == NULL) || (client == NULL && remoteClient != NULL)); + ASSERT(applicationName != NULL); + + Db *this = NULL; + + MEM_CONTEXT_NEW_BEGIN("Db") + { + this = memNew(sizeof(Db)); + this->memContext = memContextCurrent(); + + this->client = pgClientMove(client, this->memContext); + this->remoteClient = remoteClient; + this->applicationName = strDup(applicationName); + } + MEM_CONTEXT_NEW_END(); + + FUNCTION_LOG_RETURN(DB, this); +} + +/*********************************************************************************************************************************** +Execute a query +***********************************************************************************************************************************/ +static VariantList * +dbQuery(Db *this, const String *query) +{ + FUNCTION_LOG_BEGIN(logLevelDebug); + FUNCTION_LOG_PARAM(DB, this); + FUNCTION_LOG_PARAM(STRING, query); + FUNCTION_LOG_END(); + + ASSERT(this != NULL); + ASSERT(query != NULL); + + VariantList *result = NULL; + + // Query remotely + if (this->remoteClient != NULL) + { + ProtocolCommand *command = protocolCommandNew(PROTOCOL_COMMAND_DB_QUERY_STR); + protocolCommandParamAdd(command, VARUINT(this->remoteIdx)); + protocolCommandParamAdd(command, VARSTR(query)); + + result = varVarLst(protocolClientExecute(this->remoteClient, command, true)); + } + // Else locally + else + result = pgClientQuery(this->client, query); + + FUNCTION_LOG_RETURN(VARIANT_LIST, result); +} + +/*********************************************************************************************************************************** +Execute a command that expects no output +***********************************************************************************************************************************/ +static void +dbExec(Db *this, const String *command) +{ + FUNCTION_LOG_BEGIN(logLevelDebug); + FUNCTION_LOG_PARAM(DB, this); + FUNCTION_LOG_PARAM(STRING, command); + FUNCTION_LOG_END(); + + ASSERT(this != NULL); + ASSERT(command != NULL); + + CHECK(dbQuery(this, command) == NULL); + + FUNCTION_LOG_RETURN_VOID(); +} + +/*********************************************************************************************************************************** +Execute a query that returns a single row and column +***********************************************************************************************************************************/ +static Variant * +dbQueryColumn(Db *this, const String *query) +{ + FUNCTION_LOG_BEGIN(logLevelDebug); + FUNCTION_LOG_PARAM(DB, this); + FUNCTION_LOG_PARAM(STRING, query); + FUNCTION_LOG_END(); + + ASSERT(this != NULL); + ASSERT(query != NULL); + + VariantList *result = dbQuery(this, query); + + CHECK(varLstSize(result) == 1); + CHECK(varLstSize(varVarLst(varLstGet(result, 0))) == 1); + + FUNCTION_LOG_RETURN(VARIANT, varLstGet(varVarLst(varLstGet(result, 0)), 0)); +} + +/*********************************************************************************************************************************** +Execute a query that returns a single row +***********************************************************************************************************************************/ +static VariantList * +dbQueryRow(Db *this, const String *query) +{ + FUNCTION_LOG_BEGIN(logLevelDebug); + FUNCTION_LOG_PARAM(DB, this); + FUNCTION_LOG_PARAM(STRING, query); + FUNCTION_LOG_END(); + + ASSERT(this != NULL); + ASSERT(query != NULL); + + VariantList *result = dbQuery(this, query); + + CHECK(varLstSize(result) == 1); + + FUNCTION_LOG_RETURN(VARIANT_LIST, varVarLst(varLstGet(result, 0))); +} + +/*********************************************************************************************************************************** +Open the db connection +***********************************************************************************************************************************/ +void +dbOpen(Db *this) +{ + FUNCTION_LOG_BEGIN(logLevelDebug); + FUNCTION_LOG_PARAM(DB, this); + FUNCTION_LOG_END(); + + ASSERT(this != NULL); + + MEM_CONTEXT_TEMP_BEGIN() + { + // Open the connection + if (this->remoteClient != NULL) + { + ProtocolCommand *command = protocolCommandNew(PROTOCOL_COMMAND_DB_OPEN_STR); + this->remoteIdx = varUIntForce(protocolClientExecute(this->remoteClient, command, true)); + + // Set a callback to notify the remote when a connection is closed + memContextCallbackSet(this->memContext, dbFreeResource, this); + } + else + pgClientOpen(this->client); + + // Set search_path to prevent overrides of the functions we expect to call. All queries should also be schema-qualified, + // but this is an extra level protection. + dbExec(this, STRDEF("set search_path = 'pg_catalog'")); + + // Query the version and data_directory + VariantList *row = dbQueryRow( + this, + STRDEF( + "select (select setting from pg_catalog.pg_settings where name = 'server_version_num')::int4," + " (select setting from pg_catalog.pg_settings where name = 'data_directory')::text")); + + // Strip the minor version off since we don't need it. In the future it might be a good idea to warn users when they are + // running an old minor version. + this->pgVersion = varUIntForce(varLstGet(row, 0)) / 100 * 100; + + // Store the data directory that PostgreSQL is running in. This can be compared to the configured pgBackRest directory when + // validating the configuration. + MEM_CONTEXT_BEGIN(this->memContext) + { + this->pgDataPath = strDup(varStr(varLstGet(row, 1))); + } + MEM_CONTEXT_END(); + + if (this->pgVersion >= PG_VERSION_APPLICATION_NAME) + dbExec(this, strNewFmt("set application_name = '%s'", strPtr(this->applicationName))); + } + MEM_CONTEXT_TEMP_END(); + + FUNCTION_LOG_RETURN_VOID(); +} + +/*********************************************************************************************************************************** +Is this instance a standby? +***********************************************************************************************************************************/ +bool +dbIsStandby(Db *this) +{ + FUNCTION_LOG_BEGIN(logLevelDebug); + FUNCTION_LOG_PARAM(DB, this); + FUNCTION_LOG_END(); + + ASSERT(this != NULL); + + bool result = false; + + if (this->pgVersion >= PG_VERSION_HOT_STANDBY) + { + result = varBool(dbQueryColumn(this, STRDEF("select pg_catalog.pg_is_in_recovery()"))); + } + + FUNCTION_LOG_RETURN(BOOL, result); +} + +/*********************************************************************************************************************************** +Switch the WAL segment and return the segment that should have been archived +***********************************************************************************************************************************/ +String * +dbWalSwitch(Db *this) +{ + FUNCTION_LOG_BEGIN(logLevelDebug); + FUNCTION_LOG_PARAM(DB, this); + FUNCTION_LOG_END(); + + ASSERT(this != NULL); + + String *result = NULL; + + MEM_CONTEXT_TEMP_BEGIN() + { + // Create a restore point to ensure current WAL will be archived. For versions < 9.1 activity will need to be generated by + // the user if there have been no writes since the last WAL switch. + if (this->pgVersion >= PG_VERSION_RESTORE_POINT) + dbQueryColumn(this, STRDEF("select pg_catalog.pg_create_restore_point('" PROJECT_NAME " Archive Check')::text")); + + // Request a WAL segment switch + const char *walName = strPtr(pgWalName(this->pgVersion)); + const String *walFileName = varStr( + dbQueryColumn(this, strNewFmt("select pg_catalog.pg_%sfile_name(pg_catalog.pg_switch_%s())::text", walName, walName))); + + // Copy WAL segment name to the calling context + memContextSwitch(MEM_CONTEXT_OLD()); + result = strDup(walFileName); + memContextSwitch(MEM_CONTEXT_TEMP()); + } + MEM_CONTEXT_TEMP_END(); + + FUNCTION_LOG_RETURN(STRING, result); +} + +/*********************************************************************************************************************************** +Move the object to a new context +***********************************************************************************************************************************/ +Db * +dbMove(Db *this, MemContext *parentNew) +{ + FUNCTION_TEST_BEGIN(); + FUNCTION_TEST_PARAM(DB, this); + FUNCTION_TEST_PARAM(MEM_CONTEXT, parentNew); + FUNCTION_TEST_END(); + + ASSERT(parentNew != NULL); + + if (this != NULL) + memContextMove(this->memContext, parentNew); + + FUNCTION_TEST_RETURN(this); +} + +/*********************************************************************************************************************************** +Render as string for logging +***********************************************************************************************************************************/ +String * +dbToLog(const Db *this) +{ + return strNewFmt( + "{client: %s, remoteClient: %s}", this->client == NULL ? "null" : strPtr(pgClientToLog(this->client)), + this->remoteClient == NULL ? "null" : strPtr(protocolClientToLog(this->remoteClient))); +} diff --git a/src/db/db.h b/src/db/db.h new file mode 100644 index 000000000..e7b1dd4dc --- /dev/null +++ b/src/db/db.h @@ -0,0 +1,51 @@ +/*********************************************************************************************************************************** +Database Client + +Implements the required PostgreSQL queries and commands. Notice that there is no general purpose query function -- all queries are +expected to be embedded in this object. +***********************************************************************************************************************************/ +#ifndef DB_DB_H +#define DB_DB_H + +#include "postgres/client.h" +#include "protocol/client.h" + +/*********************************************************************************************************************************** +Object type +***********************************************************************************************************************************/ +#define DB_TYPE Db +#define DB_PREFIX db + +typedef struct Db Db; + +/*********************************************************************************************************************************** +Constructor +***********************************************************************************************************************************/ +Db *dbNew(PgClient *client, ProtocolClient *remoteClient, const String *applicationName); + +/*********************************************************************************************************************************** +Functions +***********************************************************************************************************************************/ +void dbOpen(Db *this); +bool dbIsStandby(Db *this); +String *dbWalSwitch(Db *this); +void dbClose(Db *this); + +Db *dbMove(Db *this, MemContext *parentNew); + +/*********************************************************************************************************************************** +Destructor +***********************************************************************************************************************************/ +void dbFree(Db *this); + +/*********************************************************************************************************************************** +Macros for function logging +***********************************************************************************************************************************/ +String *dbToLog(const Db *this); + +#define FUNCTION_LOG_DB_TYPE \ + Db * +#define FUNCTION_LOG_DB_FORMAT(value, buffer, bufferSize) \ + FUNCTION_LOG_STRING_OBJECT_FORMAT(value, dbToLog, buffer, bufferSize) + +#endif diff --git a/src/db/helper.c b/src/db/helper.c new file mode 100644 index 000000000..d2f18f796 --- /dev/null +++ b/src/db/helper.c @@ -0,0 +1,127 @@ +/*********************************************************************************************************************************** +Database Helper +***********************************************************************************************************************************/ +#include "build.auto.h" + +#include "common/debug.h" +#include "config/config.h" +#include "db/helper.h" +#include "postgres/interface.h" +#include "protocol/helper.h" +#include "version.h" + +/*********************************************************************************************************************************** +Get specified cluster +***********************************************************************************************************************************/ +static Db * +dbGetId(unsigned int pgId) +{ + FUNCTION_LOG_BEGIN(logLevelDebug); + FUNCTION_LOG_PARAM(UINT, pgId); + FUNCTION_LOG_END(); + + ASSERT(pgId > 0); + + Db *result = NULL; + + MEM_CONTEXT_TEMP_BEGIN() + { + const String *applicationName = strNewFmt(PROJECT_NAME " [%s]", cfgCommandName(cfgCommand())); + + if (pgIsLocal(pgId)) + { + result = dbNew( + pgClientNew( + cfgOptionStr(cfgOptPgSocketPath + pgId - 1), cfgOptionUInt(cfgOptPgPort + pgId - 1), PG_DB_POSTGRES_STR, NULL, + (TimeMSec)(cfgOptionDbl(cfgOptDbTimeout) * MSEC_PER_SEC)), + NULL, applicationName); + } + else + result = dbNew(NULL, protocolRemoteGet(protocolStorageTypePg, pgId), applicationName); + + dbMove(result, MEM_CONTEXT_OLD()); + } + MEM_CONTEXT_TEMP_END(); + + FUNCTION_LOG_RETURN(DB, result); +} + +/*********************************************************************************************************************************** +Get primary cluster or primary and standby cluster +***********************************************************************************************************************************/ +DbGetResult +dbGet(bool primaryOnly, bool primaryRequired) +{ + FUNCTION_LOG_BEGIN(logLevelDebug); + FUNCTION_LOG_PARAM(BOOL, primaryOnly); + FUNCTION_LOG_PARAM(BOOL, primaryRequired); + FUNCTION_LOG_END(); + + DbGetResult result = {0}; + + MEM_CONTEXT_TEMP_BEGIN() + { + // Loop through to look for primary and standby (if required) + for (unsigned int pgIdx = 0; pgIdx < cfgOptionIndexTotal(cfgOptPgPath); pgIdx++) + { + if (cfgOptionTest(cfgOptPgHost + pgIdx) || cfgOptionTest(cfgOptPgPath + pgIdx)) + { + Db *db = NULL; + bool standby = false; + + TRY_BEGIN() + { + db = dbGetId(pgIdx + 1); + dbOpen(db); + standby = dbIsStandby(db); + } + CATCH_ANY() + { + dbFree(db); + db = NULL; + + LOG_WARN("unable to check pg-%u: [%s] %s", pgIdx + 1, errorTypeName(errorType()), errorMessage()); + } + TRY_END(); + + // Was the connection successful + if (db != NULL) + { + // Is this cluster a standby + if (standby) + { + // If a standby has not already been found then assign it + if (result.standbyId == 0 && !primaryOnly) + { + result.standbyId = pgIdx + 1; + result.standby = db; + } + // Else close the connection since we don't need it + else + dbFree(db); + } + // Else is a primary + else + { + // Error if more than one primary was found + if (result.primaryId != 0) + THROW(DbConnectError, "more than one primary cluster found"); + + result.primaryId = pgIdx + 1; + result.primary = db; + } + } + } + } + + // Error if no primary was found + if (result.primaryId == 0 && primaryRequired) + THROW(DbConnectError, "unable to find primary cluster - cannot proceed"); + + dbMove(result.primary, MEM_CONTEXT_OLD()); + dbMove(result.standby, MEM_CONTEXT_OLD()); + } + MEM_CONTEXT_TEMP_END(); + + FUNCTION_LOG_RETURN(DB_GET_RESULT, result); +} diff --git a/src/db/helper.h b/src/db/helper.h new file mode 100644 index 000000000..820004062 --- /dev/null +++ b/src/db/helper.h @@ -0,0 +1,34 @@ +/*********************************************************************************************************************************** +Database Helper + +Helper functions for getting connections to PostgreSQL. +***********************************************************************************************************************************/ +#ifndef DB_HELPER_H +#define DB_HELPER_H + +#include + +#include "db/db.h" + +/*********************************************************************************************************************************** +Functions +***********************************************************************************************************************************/ +typedef struct DbGetResult +{ + unsigned int primaryId; + Db *primary; + unsigned int standbyId; + Db *standby; +} DbGetResult; + +DbGetResult dbGet(bool primaryOnly, bool primaryRequired); + +/*********************************************************************************************************************************** +Macros for function logging +***********************************************************************************************************************************/ +#define FUNCTION_LOG_DB_GET_RESULT_TYPE \ + DbGetResult +#define FUNCTION_LOG_DB_GET_RESULT_FORMAT(value, buffer, bufferSize) \ + objToLog(&value, "DbGetResult", buffer, bufferSize) + +#endif diff --git a/src/db/protocol.c b/src/db/protocol.c new file mode 100644 index 000000000..20cdfd209 --- /dev/null +++ b/src/db/protocol.c @@ -0,0 +1,96 @@ +/*********************************************************************************************************************************** +Db Protocol Handler +***********************************************************************************************************************************/ +#include "build.auto.h" + +#include "common/debug.h" +#include "common/io/io.h" +#include "common/log.h" +#include "common/memContext.h" +#include "common/type/list.h" +#include "config/config.h" +#include "db/protocol.h" +#include "postgres/client.h" +#include "postgres/interface.h" + +/*********************************************************************************************************************************** +Constants +***********************************************************************************************************************************/ +STRING_EXTERN(PROTOCOL_COMMAND_DB_OPEN_STR, PROTOCOL_COMMAND_DB_OPEN); +STRING_EXTERN(PROTOCOL_COMMAND_DB_QUERY_STR, PROTOCOL_COMMAND_DB_QUERY); +STRING_EXTERN(PROTOCOL_COMMAND_DB_CLOSE_STR, PROTOCOL_COMMAND_DB_CLOSE); + +/*********************************************************************************************************************************** +Local variables +***********************************************************************************************************************************/ +static struct +{ + List *pgClientList; // List of db objects +} dbProtocolLocal; + +/*********************************************************************************************************************************** +Process db protocol requests +***********************************************************************************************************************************/ +bool +dbProtocol(const String *command, const VariantList *paramList, ProtocolServer *server) +{ + FUNCTION_LOG_BEGIN(logLevelDebug); + FUNCTION_LOG_PARAM(STRING, command); + FUNCTION_LOG_PARAM(VARIANT_LIST, paramList); + FUNCTION_LOG_PARAM(PROTOCOL_SERVER, server); + FUNCTION_LOG_END(); + + ASSERT(command != NULL); + + // Attempt to satisfy the request -- we may get requests that are meant for other handlers + bool found = true; + + MEM_CONTEXT_TEMP_BEGIN() + { + if (strEq(command, PROTOCOL_COMMAND_DB_OPEN_STR)) + { + // If the db list does not exist then create it in the calling context (which should be persistent) + if (dbProtocolLocal.pgClientList == NULL) + { + memContextSwitch(MEM_CONTEXT_OLD()); + dbProtocolLocal.pgClientList = lstNew(sizeof(PgClient *)); + memContextSwitch(MEM_CONTEXT_TEMP()); + } + + // Add db to the list + unsigned int dbIdx = lstSize(dbProtocolLocal.pgClientList); + + MEM_CONTEXT_BEGIN(lstMemContext(dbProtocolLocal.pgClientList)) + { + // Only a single db is passed to the remote + PgClient *pgClient = pgClientNew( + cfgOptionStr(cfgOptPgSocketPath), cfgOptionUInt(cfgOptPgPort), PG_DB_POSTGRES_STR, NULL, + (TimeMSec)(cfgOptionDbl(cfgOptDbTimeout) * MSEC_PER_SEC)); + pgClientOpen(pgClient); + + lstAdd(dbProtocolLocal.pgClientList, &pgClient); + } + MEM_CONTEXT_END(); + + // Return db index which should be included in subsequent calls + protocolServerResponse(server, VARUINT(dbIdx)); + } + else if (strEq(command, PROTOCOL_COMMAND_DB_QUERY_STR) || strEq(command, PROTOCOL_COMMAND_DB_CLOSE_STR)) + { + PgClient *pgClient = *(PgClient **)lstGet(dbProtocolLocal.pgClientList, varUIntForce(varLstGet(paramList, 0))); + + if (strEq(command, PROTOCOL_COMMAND_DB_QUERY_STR)) + protocolServerResponse(server, varNewVarLst(pgClientQuery(pgClient, varStr(varLstGet(paramList, 1))))); + else + { + pgClientClose(pgClient); + protocolServerResponse(server, NULL); + } + } + else + found = false; + } + MEM_CONTEXT_TEMP_END(); + + FUNCTION_LOG_RETURN(BOOL, found); +} diff --git a/src/db/protocol.h b/src/db/protocol.h new file mode 100644 index 000000000..8a4ff8ab9 --- /dev/null +++ b/src/db/protocol.h @@ -0,0 +1,27 @@ +/*********************************************************************************************************************************** +Db Protocol Handler +***********************************************************************************************************************************/ +#ifndef DB_PROTOCOL_H +#define DB_PROTOCOL_H + +#include "common/type/string.h" +#include "common/type/variantList.h" +#include "protocol/client.h" +#include "protocol/server.h" + +/*********************************************************************************************************************************** +Constants +***********************************************************************************************************************************/ +#define PROTOCOL_COMMAND_DB_OPEN "dbOpen" + STRING_DECLARE(PROTOCOL_COMMAND_DB_OPEN_STR); +#define PROTOCOL_COMMAND_DB_QUERY "dbQuery" + STRING_DECLARE(PROTOCOL_COMMAND_DB_QUERY_STR); +#define PROTOCOL_COMMAND_DB_CLOSE "dbClose" + STRING_DECLARE(PROTOCOL_COMMAND_DB_CLOSE_STR); + +/*********************************************************************************************************************************** +Functions +***********************************************************************************************************************************/ +bool dbProtocol(const String *command, const VariantList *paramList, ProtocolServer *server); + +#endif diff --git a/src/postgres/client.c b/src/postgres/client.c index faf562789..e46b4e5fa 100644 --- a/src/postgres/client.c +++ b/src/postgres/client.c @@ -9,10 +9,7 @@ Postgres Client #include "common/log.h" #include "common/memContext.h" #include "common/object.h" -#include "common/time.h" #include "common/type/list.h" -#include "common/type/string.h" -#include "common/type/variantList.h" #include "common/wait.h" #include "postgres/client.h" @@ -350,15 +347,36 @@ pgClientClose(PgClient *this) FUNCTION_LOG_END(); ASSERT(this != NULL); - CHECK(this->connection != NULL); - memContextCallbackClear(this->memContext); - PQfinish(this->connection); - this->connection = NULL; + if (this->connection != NULL) + { + memContextCallbackClear(this->memContext); + PQfinish(this->connection); + this->connection = NULL; + } FUNCTION_LOG_RETURN_VOID(); } +/*********************************************************************************************************************************** +Move the pg client object to a new context +***********************************************************************************************************************************/ +PgClient * +pgClientMove(PgClient *this, MemContext *parentNew) +{ + FUNCTION_TEST_BEGIN(); + FUNCTION_TEST_PARAM(PG_CLIENT, this); + FUNCTION_TEST_PARAM(MEM_CONTEXT, parentNew); + FUNCTION_TEST_END(); + + ASSERT(parentNew != NULL); + + if (this != NULL) + memContextMove(this->memContext, parentNew); + + FUNCTION_TEST_RETURN(this); +} + /*********************************************************************************************************************************** Render as string for logging ***********************************************************************************************************************************/ diff --git a/src/postgres/client.h b/src/postgres/client.h index 37b74b4f8..9c4a6e236 100644 --- a/src/postgres/client.h +++ b/src/postgres/client.h @@ -8,6 +8,10 @@ casts to queries to output one of these types. #ifndef POSTGRES_QUERY_H #define POSTGRES_QUERY_H +#include "common/type/string.h" +#include "common/type/variantList.h" +#include "common/time.h" + /*********************************************************************************************************************************** Object type ***********************************************************************************************************************************/ @@ -29,6 +33,8 @@ PgClient *pgClientOpen(PgClient *this); VariantList *pgClientQuery(PgClient *this, const String *query); void pgClientClose(PgClient *this); +PgClient *pgClientMove(PgClient *this, MemContext *parentNew); + /*********************************************************************************************************************************** Destructor ***********************************************************************************************************************************/ diff --git a/src/postgres/interface.c b/src/postgres/interface.c index 0f44da130..fb2ca0a05 100644 --- a/src/postgres/interface.c +++ b/src/postgres/interface.c @@ -14,6 +14,12 @@ PostgreSQL Interface #include "postgres/version.h" #include "storage/helper.h" +/*********************************************************************************************************************************** +Defines for various Postgres paths and files +***********************************************************************************************************************************/ +STRING_EXTERN(PG_NAME_WAL_STR, PG_NAME_WAL); +STRING_EXTERN(PG_NAME_XLOG_STR, PG_NAME_XLOG); + /*********************************************************************************************************************************** Define default wal segment size @@ -35,6 +41,11 @@ something far larger needed but <= the minimum read size on just about any syste ***********************************************************************************************************************************/ #define PG_WAL_HEADER_SIZE ((unsigned int)(512)) +/*********************************************************************************************************************************** +Name of default PostgreSQL database used for running all queries and commands +***********************************************************************************************************************************/ +STRING_EXTERN(PG_DB_POSTGRES_STR, PG_DB_POSTGRES); + /*********************************************************************************************************************************** PostgreSQL interface definitions @@ -407,6 +418,19 @@ pgWalFromFile(const String *walFile) FUNCTION_LOG_RETURN(PG_WAL, result); } +/*********************************************************************************************************************************** +Get WAL name (wal/xlog) for a PostgreSQL version +***********************************************************************************************************************************/ +const String * +pgWalName(unsigned int pgVersion) +{ + FUNCTION_TEST_BEGIN(); + FUNCTION_TEST_PARAM(UINT, pgVersion); + FUNCTION_TEST_END(); + + FUNCTION_TEST_RETURN(pgVersion >= PG_VERSION_WAL_RENAME ? PG_NAME_WAL_STR : PG_NAME_XLOG_STR); +} + /*********************************************************************************************************************************** Create pg_control for testing ***********************************************************************************************************************************/ diff --git a/src/postgres/interface.h b/src/postgres/interface.h index a164c42eb..3b2db1264 100644 --- a/src/postgres/interface.h +++ b/src/postgres/interface.h @@ -17,6 +17,17 @@ Defines for various Postgres paths and files #define PG_PATH_ARCHIVE_STATUS "archive_status" #define PG_PATH_GLOBAL "global" +#define PG_NAME_WAL "wal" + STRING_DECLARE(PG_NAME_WAL_STR); +#define PG_NAME_XLOG "xlog" + STRING_DECLARE(PG_NAME_XLOG_STR); + +/*********************************************************************************************************************************** +Name of default PostgreSQL database used for running all queries and commands +***********************************************************************************************************************************/ +#define PG_DB_POSTGRES "postgres" + STRING_DECLARE(PG_DB_POSTGRES_STR); + /*********************************************************************************************************************************** Define default page size @@ -69,6 +80,8 @@ String *pgVersionToStr(unsigned int version); PgWal pgWalFromFile(const String *walFile); PgWal pgWalFromBuffer(const Buffer *walBuffer); +const String *pgWalName(unsigned int pgVersion); + /*********************************************************************************************************************************** Test Functions ***********************************************************************************************************************************/ diff --git a/src/postgres/version.h b/src/postgres/version.h index b83c84afc..2e72f6351 100644 --- a/src/postgres/version.h +++ b/src/postgres/version.h @@ -26,6 +26,21 @@ PostgreSQL version constants #define PG_VERSION_MAX PG_VERSION_11 +/*********************************************************************************************************************************** +Version where various PostgreSQL capabilities were introduced +***********************************************************************************************************************************/ +// application_name can be set to show the application name in pg_stat_activity +#define PG_VERSION_APPLICATION_NAME PG_VERSION_90 + +// pg_is_in_recovery() supported +#define PG_VERSION_HOT_STANDBY PG_VERSION_91 + +// pg_create_restore_point() supported +#define PG_VERSION_RESTORE_POINT PG_VERSION_91 + +// xlog was renamed to wal +#define PG_VERSION_WAL_RENAME PG_VERSION_10 + /*********************************************************************************************************************************** PostgreSQL version string constants for use in error messages ***********************************************************************************************************************************/ diff --git a/test/define.yaml b/test/define.yaml index 0f8a7ad97..85e5d08e4 100644 --- a/test/define.yaml +++ b/test/define.yaml @@ -326,7 +326,7 @@ unit: # ---------------------------------------------------------------------------------------------------------------------------- - name: interface - total: 5 + total: 6 coverage: postgres/interface: full @@ -539,6 +539,20 @@ unit: - name: info-backup-perl total: 3 + # ******************************************************************************************************************************** + - name: db + + test: + # ---------------------------------------------------------------------------------------------------------------------------- + - name: db + total: 2 + perlReq: true + + coverage: + db/db: full + db/helper: full + db/protocol: full + # ******************************************************************************************************************************** - name: command diff --git a/test/src/common/harnessPq.c b/test/src/common/harnessPq.c index 74491d9b2..c04807c4d 100644 --- a/test/src/common/harnessPq.c +++ b/test/src/common/harnessPq.c @@ -64,7 +64,8 @@ harnessPqScriptRun(const char *function, const VariantList *param, HarnessPq *pa harnessPqScriptFail = true; THROW_FMT( - AssertError, "pq script [%u] expected function '%s' but got '%s'", harnessPqScriptIdx, result->function, function); + AssertError, "pq script [%u] expected function %s (%s) but got %s (%s)", harnessPqScriptIdx, result->function, + result->param == NULL ? "" : result->param, function, strPtr(paramStr)); } // Check that parameters match diff --git a/test/src/common/harnessPq.h b/test/src/common/harnessPq.h index 1ca736176..7e32a13ee 100644 --- a/test/src/common/harnessPq.h +++ b/test/src/common/harnessPq.h @@ -9,7 +9,11 @@ usage examples. #ifndef HARNESS_PQ_REAL +#include + +#include "common/macro.h" #include "common/time.h" +#include "version.h" /*********************************************************************************************************************************** Function constants @@ -34,6 +38,115 @@ Function constants #define HRNPQ_SENDQUERY "PQsendQuery" #define HRNPQ_STATUS "PQstatus" +/*********************************************************************************************************************************** +Macros for defining groups of functions that implement various queries and commands +***********************************************************************************************************************************/ +#define HRNPQ_MACRO_OPEN(sessionParam, connectParam) \ + {.session = sessionParam, .function = HRNPQ_CONNECTDB, .param = "[\"" connectParam "\"]"}, \ + {.session = sessionParam, .function = HRNPQ_STATUS, .resultInt = CONNECTION_OK} + +#define HRNPQ_MACRO_SET_SEARCH_PATH(sessionParam) \ + {.session = sessionParam, .function = HRNPQ_SENDQUERY, .param = "[\"set search_path = 'pg_catalog'\"]", .resultInt = 1}, \ + {.session = sessionParam, .function = HRNPQ_CONSUMEINPUT}, \ + {.session = sessionParam, .function = HRNPQ_ISBUSY}, \ + {.session = sessionParam, .function = HRNPQ_GETRESULT}, \ + {.session = sessionParam, .function = HRNPQ_RESULTSTATUS, .resultInt = PGRES_COMMAND_OK}, \ + {.session = sessionParam, .function = HRNPQ_CLEAR}, \ + {.session = sessionParam, .function = HRNPQ_GETRESULT, .resultNull = true} + +#define HRNPQ_MACRO_VALIDATE_QUERY(sessionParam, versionParam, pgPathParam) \ + {.session = sessionParam, .function = HRNPQ_SENDQUERY, .param = \ + "[\"select (select setting from pg_catalog.pg_settings where name = 'server_version_num')::int4," \ + " (select setting from pg_catalog.pg_settings where name = 'data_directory')::text\"]", \ + .resultInt = 1}, \ + {.session = sessionParam, .function = HRNPQ_CONSUMEINPUT}, \ + {.session = sessionParam, .function = HRNPQ_ISBUSY}, \ + {.session = sessionParam, .function = HRNPQ_GETRESULT}, \ + {.session = sessionParam, .function = HRNPQ_RESULTSTATUS, .resultInt = PGRES_TUPLES_OK}, \ + {.session = sessionParam, .function = HRNPQ_NTUPLES, .resultInt = 1}, \ + {.session = sessionParam, .function = HRNPQ_NFIELDS, .resultInt = 2}, \ + {.session = sessionParam, .function = HRNPQ_FTYPE, .param = "[0]", .resultInt = HRNPQ_TYPE_INT}, \ + {.session = sessionParam, .function = HRNPQ_FTYPE, .param = "[1]", .resultInt = HRNPQ_TYPE_TEXT}, \ + {.session = sessionParam, .function = HRNPQ_GETVALUE, .param = "[0,0]", .resultZ = STRINGIFY(versionParam)}, \ + {.session = sessionParam, .function = HRNPQ_GETVALUE, .param = "[0,1]", .resultZ = pgPathParam}, \ + {.session = sessionParam, .function = HRNPQ_CLEAR}, \ + {.session = sessionParam, .function = HRNPQ_GETRESULT, .resultNull = true} + +#define HRNPQ_MACRO_SET_APPLICATION_NAME(sessionParam) \ + {.session = sessionParam, .function = HRNPQ_SENDQUERY, \ + .param = strPtr(strNewFmt("[\"set application_name = '" PROJECT_NAME " [%s]'\"]", cfgCommandName(cfgCommand()))), \ + .resultInt = 1}, \ + {.session = sessionParam, .function = HRNPQ_CONSUMEINPUT}, \ + {.session = sessionParam, .function = HRNPQ_ISBUSY}, \ + {.session = sessionParam, .function = HRNPQ_GETRESULT}, \ + {.session = sessionParam, .function = HRNPQ_RESULTSTATUS, .resultInt = PGRES_COMMAND_OK}, \ + {.session = sessionParam, .function = HRNPQ_CLEAR}, \ + {.session = sessionParam, .function = HRNPQ_GETRESULT, .resultNull = true} + +#define HRNPQ_MACRO_IS_STANDBY_QUERY(sessionParam, standbyParam) \ + {.session = sessionParam, .function = HRNPQ_SENDQUERY, .param = "[\"select pg_catalog.pg_is_in_recovery()\"]", .resultInt = 1},\ + {.session = sessionParam, .function = HRNPQ_CONSUMEINPUT}, \ + {.session = sessionParam, .function = HRNPQ_ISBUSY}, \ + {.session = sessionParam, .function = HRNPQ_GETRESULT}, \ + {.session = sessionParam, .function = HRNPQ_RESULTSTATUS, .resultInt = PGRES_TUPLES_OK}, \ + {.session = sessionParam, .function = HRNPQ_NTUPLES, .resultInt = 1}, \ + {.session = sessionParam, .function = HRNPQ_NFIELDS, .resultInt = 1}, \ + {.session = sessionParam, .function = HRNPQ_FTYPE, .param = "[0]", .resultInt = HRNPQ_TYPE_BOOL}, \ + {.session = sessionParam, .function = HRNPQ_GETVALUE, .param = "[0,0]", .resultZ = STRINGIFY(standbyParam)}, \ + {.session = sessionParam, .function = HRNPQ_CLEAR}, \ + {.session = sessionParam, .function = HRNPQ_GETRESULT, .resultNull = true} + +#define HRNPQ_MACRO_CREATE_RESTORE_POINT(sessionParam, lsnParam) \ + {.session = sessionParam, \ + .function = HRNPQ_SENDQUERY, .param = "[\"select pg_catalog.pg_create_restore_point('pgBackRest Archive Check')::text\"]", \ + .resultInt = 1}, \ + {.session = sessionParam, .function = HRNPQ_CONSUMEINPUT}, \ + {.session = sessionParam, .function = HRNPQ_ISBUSY}, \ + {.session = sessionParam, .function = HRNPQ_GETRESULT}, \ + {.session = sessionParam, .function = HRNPQ_RESULTSTATUS, .resultInt = PGRES_TUPLES_OK}, \ + {.session = sessionParam, .function = HRNPQ_NTUPLES, .resultInt = 1}, \ + {.session = sessionParam, .function = HRNPQ_NFIELDS, .resultInt = 1}, \ + {.session = sessionParam, .function = HRNPQ_FTYPE, .param = "[0]", .resultInt = HRNPQ_TYPE_TEXT}, \ + {.session = sessionParam, .function = HRNPQ_GETVALUE, .param = "[0,0]", .resultZ = lsnParam}, \ + {.session = sessionParam, .function = HRNPQ_CLEAR}, \ + {.session = sessionParam, .function = HRNPQ_GETRESULT, .resultNull = true} + +#define HRNPQ_MACRO_WAL_SWITCH(sessionParam, walNameParam, walFileNameParam) \ + {.session = sessionParam, .function = HRNPQ_SENDQUERY, \ + .param = "[\"select pg_catalog.pg_" walNameParam "file_name(pg_catalog.pg_switch_" walNameParam "())::text\"]", \ + .resultInt = 1}, \ + {.session = sessionParam, .function = HRNPQ_CONSUMEINPUT}, \ + {.session = sessionParam, .function = HRNPQ_ISBUSY}, \ + {.session = sessionParam, .function = HRNPQ_GETRESULT}, \ + {.session = sessionParam, .function = HRNPQ_RESULTSTATUS, .resultInt = PGRES_TUPLES_OK}, \ + {.session = sessionParam, .function = HRNPQ_NTUPLES, .resultInt = 1}, \ + {.session = sessionParam, .function = HRNPQ_NFIELDS, .resultInt = 1}, \ + {.session = sessionParam, .function = HRNPQ_FTYPE, .param = "[0]", .resultInt = HRNPQ_TYPE_TEXT}, \ + {.session = sessionParam, .function = HRNPQ_GETVALUE, .param = "[0,0]", .resultZ = walFileNameParam}, \ + {.session = sessionParam, .function = HRNPQ_CLEAR}, \ + {.session = sessionParam, .function = HRNPQ_GETRESULT, .resultNull = true} + +#define HRNPQ_MACRO_CLOSE(sessionParam) \ + {.session = sessionParam, .function = HRNPQ_FINISH} + +#define HRNPQ_MACRO_DONE() \ + {.function = NULL} + +/*********************************************************************************************************************************** +Macros to simplify dbOpen() for specific database versions +***********************************************************************************************************************************/ +#define HRNPQ_MACRO_OPEN_84(sessionParam, connectParam, pgPathParam) \ + HRNPQ_MACRO_OPEN(sessionParam, connectParam), \ + HRNPQ_MACRO_SET_SEARCH_PATH(sessionParam), \ + HRNPQ_MACRO_VALIDATE_QUERY(sessionParam, PG_VERSION_84, pgPathParam) + +#define HRNPQ_MACRO_OPEN_92(sessionParam, connectParam, pgPathParam, standbyParam) \ + HRNPQ_MACRO_OPEN(sessionParam, connectParam), \ + HRNPQ_MACRO_SET_SEARCH_PATH(sessionParam), \ + HRNPQ_MACRO_VALIDATE_QUERY(sessionParam, PG_VERSION_92, pgPathParam), \ + HRNPQ_MACRO_SET_APPLICATION_NAME(sessionParam), \ + HRNPQ_MACRO_IS_STANDBY_QUERY(sessionParam, standbyParam) + /*********************************************************************************************************************************** Data type constants ***********************************************************************************************************************************/ diff --git a/test/src/module/db/dbTest.c b/test/src/module/db/dbTest.c new file mode 100644 index 000000000..f32c304a4 --- /dev/null +++ b/test/src/module/db/dbTest.c @@ -0,0 +1,279 @@ +/*********************************************************************************************************************************** +Test Database +***********************************************************************************************************************************/ +#include "common/harnessConfig.h" +#include "common/harnessFork.h" +#include "common/harnessLog.h" +#include "common/harnessPq.h" + +#include "common/io/handleRead.h" +#include "common/io/handleWrite.h" + +/*********************************************************************************************************************************** +Test Run +***********************************************************************************************************************************/ +void +testRun(void) +{ + FUNCTION_HARNESS_VOID(); + + // ***************************************************************************************************************************** + if (testBegin("Db and dbProtocol()")) + { + HARNESS_FORK_BEGIN() + { + HARNESS_FORK_CHILD_BEGIN(0, true) + { + IoRead *read = ioHandleReadNew(strNew("client read"), HARNESS_FORK_CHILD_READ(), 2000); + ioReadOpen(read); + IoWrite *write = ioHandleWriteNew(strNew("client write"), HARNESS_FORK_CHILD_WRITE()); + ioWriteOpen(write); + + // Set options + StringList *argList = strLstNew(); + strLstAddZ(argList, "pgbackrest"); + strLstAddZ(argList, "--stanza=test1"); + strLstAddZ(argList, "--pg1-path=/path/to/pg"); + strLstAddZ(argList, "--command=backup"); + strLstAddZ(argList, "--type=db"); + strLstAddZ(argList, "--process=0"); + strLstAddZ(argList, "remote"); + harnessCfgLoad(strLstSize(argList), strLstPtr(argList)); + + // Set script + harnessPqScriptSet((HarnessPq []) + { + HRNPQ_MACRO_OPEN(1, "dbname='postgres' port=5432"), + HRNPQ_MACRO_SET_SEARCH_PATH(1), + HRNPQ_MACRO_VALIDATE_QUERY(1, PG_VERSION_84, "/pgdata"), + HRNPQ_MACRO_CLOSE(1), + + HRNPQ_MACRO_OPEN(1, "dbname='postgres' port=5432"), + HRNPQ_MACRO_SET_SEARCH_PATH(1), + HRNPQ_MACRO_VALIDATE_QUERY(1, PG_VERSION_84, "/pgdata"), + HRNPQ_MACRO_WAL_SWITCH(1, "xlog", "000000030000000200000003"), + HRNPQ_MACRO_CLOSE(1), + + HRNPQ_MACRO_DONE() + }); + + // Create server + ProtocolServer *server = NULL; + + TEST_ASSIGN(server, protocolServerNew(strNew("db test server"), strNew("test"), read, write), "create server"); + TEST_RESULT_VOID(protocolServerHandlerAdd(server, dbProtocol), "add handler"); + TEST_RESULT_VOID(protocolServerProcess(server), "run process loop"); + TEST_RESULT_VOID(protocolServerFree(server), "free server"); + } + HARNESS_FORK_CHILD_END(); + + HARNESS_FORK_PARENT_BEGIN() + { + IoRead *read = ioHandleReadNew(strNew("server read"), HARNESS_FORK_PARENT_READ_PROCESS(0), 2000); + ioReadOpen(read); + IoWrite *write = ioHandleWriteNew(strNew("server write"), HARNESS_FORK_PARENT_WRITE_PROCESS(0)); + ioWriteOpen(write); + + // Create client + ProtocolClient *client = NULL; + Db *db = NULL; + + TEST_ASSIGN(client, protocolClientNew(strNew("db test client"), strNew("test"), read, write), "create client"); + + // Open and free database + TEST_ASSIGN(db, dbNew(NULL, client, strNew("test")), "create db"); + TEST_RESULT_VOID(dbOpen(db), "open db"); + TEST_RESULT_VOID(dbFree(db), "free db"); + + // Open the database, but don't free it so the server is force to do it on shutdown + TEST_ASSIGN(db, dbNew(NULL, client, strNew("test")), "create db"); + TEST_RESULT_VOID(dbOpen(db), "open db"); + TEST_RESULT_STR(strPtr(dbWalSwitch(db)), "000000030000000200000003", " wal switch"); + TEST_RESULT_VOID(memContextCallbackClear(db->memContext), "clear context so close is not called"); + + TEST_RESULT_VOID(protocolClientFree(client), "free client"); + } + HARNESS_FORK_PARENT_END(); + } + HARNESS_FORK_END(); + } + + // ***************************************************************************************************************************** + if (testBegin("dbGet()")) + { + DbGetResult result = {0}; + + // Error connecting to primary + // ------------------------------------------------------------------------------------------------------------------------- + StringList *argList = strLstNew(); + strLstAddZ(argList, "pgbackrest"); + strLstAddZ(argList, "--stanza=test1"); + strLstAddZ(argList, "--repo1-retention-full=1"); + strLstAddZ(argList, "--pg1-path=/path/to/pg"); + strLstAddZ(argList, "backup"); + harnessCfgLoad(strLstSize(argList), strLstPtr(argList)); + + harnessPqScriptSet((HarnessPq []) + { + {.function = HRNPQ_CONNECTDB, .param = "[\"dbname='postgres' port=5432\"]"}, + {.function = HRNPQ_STATUS, .resultInt = CONNECTION_BAD}, + {.function = HRNPQ_ERRORMESSAGE, .resultZ = "error"}, + {.function = HRNPQ_FINISH}, + {.function = NULL} + }); + + TEST_ERROR(dbGet(true, true), DbConnectError, "unable to find primary cluster - cannot proceed"); + harnessLogResult( + "P00 WARN: unable to check pg-1: [DbConnectError] unable to connect to 'dbname='postgres' port=5432': error"); + + // Only cluster is a standby + // ------------------------------------------------------------------------------------------------------------------------- + harnessPqScriptSet((HarnessPq []) + { + HRNPQ_MACRO_OPEN(1, "dbname='postgres' port=5432"), + HRNPQ_MACRO_SET_SEARCH_PATH(1), + HRNPQ_MACRO_VALIDATE_QUERY(1, PG_VERSION_94, "/pgdata"), + HRNPQ_MACRO_SET_APPLICATION_NAME(1), + HRNPQ_MACRO_IS_STANDBY_QUERY(1, true), + HRNPQ_MACRO_CLOSE(1), + HRNPQ_MACRO_DONE() + }); + + TEST_ERROR(dbGet(true, true), DbConnectError, "unable to find primary cluster - cannot proceed"); + + // Primary cluster found + // ------------------------------------------------------------------------------------------------------------------------- + harnessPqScriptSet((HarnessPq []) + { + HRNPQ_MACRO_OPEN_84(1, "dbname='postgres' port=5432", "/pgdata"), + HRNPQ_MACRO_CLOSE(1), + HRNPQ_MACRO_DONE() + }); + + TEST_ASSIGN(result, dbGet(true, true), "get primary only"); + + TEST_RESULT_INT(result.primaryId, 1, " check primary id"); + TEST_RESULT_BOOL(result.primary != NULL, true, " check primary"); + TEST_RESULT_INT(result.standbyId, 0, " check standby id"); + TEST_RESULT_BOOL(result.standby == NULL, true, " check standby"); + + TEST_RESULT_VOID(dbFree(result.primary), "free primary"); + + // More than one primary found + // ------------------------------------------------------------------------------------------------------------------------- + argList = strLstNew(); + strLstAddZ(argList, "pgbackrest"); + strLstAddZ(argList, "--stanza=test1"); + strLstAddZ(argList, "--repo1-retention-full=1"); + strLstAddZ(argList, "--pg1-path=/path/to/pg1"); + strLstAddZ(argList, "--pg8-path=/path/to/pg2"); + strLstAddZ(argList, "--pg8-port=5433"); + strLstAddZ(argList, "backup"); + harnessCfgLoad(strLstSize(argList), strLstPtr(argList)); + + harnessPqScriptSet((HarnessPq []) + { + HRNPQ_MACRO_OPEN_84(1, "dbname='postgres' port=5432", "/pgdata"), + HRNPQ_MACRO_OPEN_84(8, "dbname='postgres' port=5433", "/pgdata"), + + HRNPQ_MACRO_CLOSE(1), + HRNPQ_MACRO_CLOSE(8), + + HRNPQ_MACRO_DONE() + }); + + TEST_ERROR(dbGet(true, true), DbConnectError, "more than one primary cluster found"); + + // Two standbys found but no primary + // ------------------------------------------------------------------------------------------------------------------------- + harnessPqScriptSet((HarnessPq []) + { + HRNPQ_MACRO_OPEN_92(1, "dbname='postgres' port=5432", "/pgdata", true), + HRNPQ_MACRO_OPEN_92(8, "dbname='postgres' port=5433", "/pgdata", true), + + HRNPQ_MACRO_CLOSE(8), + HRNPQ_MACRO_CLOSE(1), + + HRNPQ_MACRO_DONE() + }); + + TEST_ERROR(dbGet(false, true), DbConnectError, "unable to find primary cluster - cannot proceed"); + + // Two standbys and primary not required + // ------------------------------------------------------------------------------------------------------------------------- + harnessPqScriptSet((HarnessPq []) + { + HRNPQ_MACRO_OPEN_92(1, "dbname='postgres' port=5432", "/pgdata", true), + HRNPQ_MACRO_OPEN_92(8, "dbname='postgres' port=5433", "/pgdata", true), + + HRNPQ_MACRO_CLOSE(8), + HRNPQ_MACRO_CLOSE(1), + + HRNPQ_MACRO_DONE() + }); + + TEST_ASSIGN(result, dbGet(false, false), "get standbys"); + + TEST_RESULT_INT(result.primaryId, 0, " check primary id"); + TEST_RESULT_BOOL(result.primary == NULL, true, " check primary"); + TEST_RESULT_INT(result.standbyId, 1, " check standby id"); + TEST_RESULT_BOOL(result.standby != NULL, true, " check standby"); + + TEST_RESULT_VOID(dbFree(result.standby), "free standby"); + + // Primary and standby found + // ------------------------------------------------------------------------------------------------------------------------- + argList = strLstNew(); + strLstAddZ(argList, "pgbackrest"); + strLstAddZ(argList, "--stanza=test1"); + strLstAddZ(argList, "--repo1-retention-full=1"); + strLstAddZ(argList, "--pg1-path=/path/to/pg1"); + strLstAddZ(argList, "--pg4-path=/path/to/pg4"); + strLstAddZ(argList, "--pg4-port=5433"); + strLstAddZ(argList, "--pg5-host=localhost"); + strLstAdd(argList, strNewFmt("--pg5-host-user=%s", testUser())); + strLstAddZ(argList, "--pg5-path=/path/to/pg5"); + strLstAddZ(argList, "--pg8-path=/path/to/pg8"); + strLstAddZ(argList, "--pg8-port=5434"); + strLstAddZ(argList, "backup"); + harnessCfgLoad(strLstSize(argList), strLstPtr(argList)); + + harnessPqScriptSet((HarnessPq []) + { + HRNPQ_MACRO_OPEN_92(1, "dbname='postgres' port=5432", "/pgdata", true), + + // pg-4 error + {.session = 4, .function = HRNPQ_CONNECTDB, .param = "[\"dbname='postgres' port=5433\"]"}, + {.session = 4, .function = HRNPQ_STATUS, .resultInt = CONNECTION_BAD}, + {.session = 4, .function = HRNPQ_ERRORMESSAGE, .resultZ = "error"}, + {.session = 4, .function = HRNPQ_FINISH}, + + HRNPQ_MACRO_OPEN_92(8, "dbname='postgres' port=5434", "/pgdata", false), + + HRNPQ_MACRO_CREATE_RESTORE_POINT(8, "2/3"), + HRNPQ_MACRO_WAL_SWITCH(8, "xlog", "000000010000000200000003"), + + HRNPQ_MACRO_CLOSE(8), + HRNPQ_MACRO_CLOSE(1), + + HRNPQ_MACRO_DONE() + }); + + TEST_ASSIGN(result, dbGet(false, true), "get primary and standy"); + harnessLogResultRegExp( + "P00 WARN: unable to check pg-4: \\[DbConnectError\\] unable to connect to 'dbname='postgres' port=5433': error\n" + "P00 WARN: unable to check pg-5: \\[DbConnectError\\] raised from remote-0 protocol on 'localhost':" + " unable to connect to 'dbname='postgres' port=5432': could not connect to server: No such file or directory.*"); + + TEST_RESULT_INT(result.primaryId, 8, " check primary id"); + TEST_RESULT_BOOL(result.primary != NULL, true, " check primary"); + TEST_RESULT_STR(strPtr(dbWalSwitch(result.primary)), "000000010000000200000003", " wal switch"); + TEST_RESULT_INT(result.standbyId, 1, " check standby id"); + TEST_RESULT_BOOL(result.standby != NULL, true, " check standby"); + + TEST_RESULT_VOID(dbFree(result.primary), "free primary"); + TEST_RESULT_VOID(dbFree(result.standby), "free standby"); + } + + FUNCTION_HARNESS_RESULT_VOID(); +} diff --git a/test/src/module/postgres/clientTest.c b/test/src/module/postgres/clientTest.c index 9c1bf3b03..5c645af37 100644 --- a/test/src/module/postgres/clientTest.c +++ b/test/src/module/postgres/clientTest.c @@ -53,7 +53,15 @@ testRun(void) #endif PgClient *client = NULL; - TEST_ASSIGN(client, pgClientNew(NULL, 5433, strNew("postg '\\res"), NULL, 3000), "new client"); + + MEM_CONTEXT_TEMP_BEGIN() + { + TEST_ASSIGN(client, pgClientNew(NULL, 5433, strNew("postg '\\res"), NULL, 3000), "new client"); + TEST_RESULT_VOID(pgClientMove(client, MEM_CONTEXT_OLD()), "move client"); + TEST_RESULT_VOID(pgClientMove(NULL, MEM_CONTEXT_OLD()), "move null client"); + } + MEM_CONTEXT_TEMP_END(); + TEST_ERROR( pgClientOpen(client), DbConnectError, "unable to connect to 'dbname='postg \\'\\\\res' port=5433': could not connect to server: No such file or directory\n" @@ -284,6 +292,7 @@ testRun(void) }); #endif TEST_RESULT_VOID(pgClientClose(client), "close client"); + TEST_RESULT_VOID(pgClientClose(client), "close client again"); } FUNCTION_HARNESS_RESULT_VOID(); diff --git a/test/src/module/postgres/interfaceTest.c b/test/src/module/postgres/interfaceTest.c index 03790b0c7..57a167012 100644 --- a/test/src/module/postgres/interfaceTest.c +++ b/test/src/module/postgres/interfaceTest.c @@ -94,6 +94,13 @@ testRun(void) TEST_RESULT_INT(info.version, PG_VERSION_83, " check version"); } + // ***************************************************************************************************************************** + if (testBegin("pgWalName()")) + { + TEST_RESULT_STR(strPtr(pgWalName(PG_VERSION_96)), "xlog", "check xlog name"); + TEST_RESULT_STR(strPtr(pgWalName(PG_VERSION_10)), "wal", "check wal name"); + } + // ***************************************************************************************************************************** if (testBegin("pgWalFromBuffer() and pgWalFromFile()")) { @@ -157,7 +164,6 @@ testRun(void) "{version: 110000, systemId: 1030522662895, walSegmentSize: 16777216, pageChecksum: true}", "check log"); } - // ***************************************************************************************************************************** if (testBegin("pgWalToLog()")) {