From: Thomas Markwalder Date: Wed, 1 Jun 2016 11:49:37 +0000 (-0400) Subject: [master] Created common PgSqlConnection class from PgSqlLeaseMgr X-Git-Tag: trac4106_update_base~12 X-Git-Url: http://git.ipfire.org/cgi-bin/gitweb.cgi?a=commitdiff_plain;h=ff63173cf051769e5a020972e8c35a2c791186e4;p=thirdparty%2Fkea.git [master] Created common PgSqlConnection class from PgSqlLeaseMgr Merged in trac 4276. --- ff63173cf051769e5a020972e8c35a2c791186e4 diff --cc src/lib/dhcpsrv/pgsql_connection.cc index 0000000000,016b06154d..b7ef34db2f mode 000000,100644..100644 --- a/src/lib/dhcpsrv/pgsql_connection.cc +++ b/src/lib/dhcpsrv/pgsql_connection.cc @@@ -1,0 -1,172 +1,216 @@@ + // Copyright (C) 2016 Internet Systems Consortium, Inc. ("ISC") + // + // This Source Code Form is subject to the terms of the Mozilla Public + // License, v. 2.0. If a copy of the MPL was not distributed with this + // file, You can obtain one at http://mozilla.org/MPL/2.0/. + + #include + + #include + #include + + // PostgreSQL errors should be tested based on the SQL state code. Each state + // code is 5 decimal, ASCII, digits, the first two define the category of + // error, the last three are the specific error. PostgreSQL makes the state + // code as a char[5]. Macros for each code are defined in PostgreSQL's + // server/utils/errcodes.h, although they require a second macro, + // MAKE_SQLSTATE for completion. For example, duplicate key error as: + // + // #define ERRCODE_UNIQUE_VIOLATION MAKE_SQLSTATE('2','3','5','0','5') + // + // PostgreSQL deliberately omits the MAKE_SQLSTATE macro so callers can/must + // supply their own. We'll define it as an initlizer_list: + #define MAKE_SQLSTATE(ch1,ch2,ch3,ch4,ch5) {ch1,ch2,ch3,ch4,ch5} + // So we can use it like this: const char some_error[] = ERRCODE_xxxx; + #define PGSQL_STATECODE_LEN 5 + #include + + using namespace std; + + namespace isc { + namespace dhcp { + ++// Default connection timeout ++const int PGSQL_DEFAULT_CONNECTION_TIMEOUT = 5; // seconds ++ + const char PgSqlConnection::DUPLICATE_KEY[] = ERRCODE_UNIQUE_VIOLATION; + + PgSqlConnection::~PgSqlConnection() { + if (conn_) { + // Deallocate the prepared queries. + PgSqlResult r(PQexec(conn_, "DEALLOCATE all")); + if(PQresultStatus(r) != PGRES_COMMAND_OK) { + // Highly unlikely but we'll log it and go on. + LOG_ERROR(dhcpsrv_logger, DHCPSRV_PGSQL_DEALLOC_ERROR) + .arg(PQerrorMessage(conn_)); + } + } + } + + void + PgSqlConnection::prepareStatement(const PgSqlTaggedStatement& statement) { + // Prepare all statements queries with all known fields datatype + PgSqlResult r(PQprepare(conn_, statement.name, statement.text, + statement.nbparams, statement.types)); + if(PQresultStatus(r) != PGRES_COMMAND_OK) { + isc_throw(DbOperationError, "unable to prepare PostgreSQL statement: " + << statement.text << ", reason: " << PQerrorMessage(conn_)); + } + } + + void + PgSqlConnection::openDatabase() { + string dbconnparameters; + string shost = "localhost"; + try { + shost = getParameter("host"); + } catch(...) { + // No host. Fine, we'll use "localhost" + } + + dbconnparameters += "host = '" + shost + "'" ; + + string suser; + try { + suser = getParameter("user"); + dbconnparameters += " user = '" + suser + "'"; + } catch(...) { + // No user. Fine, we'll use NULL + } + + string spassword; + try { + spassword = getParameter("password"); + dbconnparameters += " password = '" + spassword + "'"; + } catch(...) { + // No password. Fine, we'll use NULL + } + + string sname; + try { + sname = getParameter("name"); + dbconnparameters += " dbname = '" + sname + "'"; + } catch(...) { + // No database name. Throw a "NoDatabaseName" exception + isc_throw(NoDatabaseName, "must specify a name for the database"); + } + ++ unsigned int connect_timeout = PGSQL_DEFAULT_CONNECTION_TIMEOUT; ++ string stimeout; ++ try { ++ stimeout = getParameter("connect-timeout"); ++ } catch (...) { ++ // No timeout parameter, we are going to use the default timeout. ++ stimeout = ""; ++ } ++ ++ if (stimeout.size() > 0) { ++ // Timeout was given, so try to convert it to an integer. ++ ++ try { ++ connect_timeout = boost::lexical_cast(stimeout); ++ } catch (...) { ++ // Timeout given but could not be converted to an unsigned int. Set ++ // the connection timeout to an invalid value to trigger throwing ++ // of an exception. ++ connect_timeout = 0; ++ } ++ ++ // The timeout is only valid if greater than zero, as depending on the ++ // database, a zero timeout might signify someting like "wait ++ // indefinitely". ++ // ++ // The check below also rejects a value greater than the maximum ++ // integer value. The lexical_cast operation used to obtain a numeric ++ // value from a string can get confused if trying to convert a negative ++ // integer to an unsigned int: instead of throwing an exception, it may ++ // produce a large positive value. ++ if ((connect_timeout == 0) || ++ (connect_timeout > numeric_limits::max())) { ++ isc_throw(DbInvalidTimeout, "database connection timeout (" << ++ stimeout << ") must be an integer greater than 0"); ++ } ++ } ++ ++ std::ostringstream oss; ++ oss << connect_timeout; ++ dbconnparameters += " connect_timeout = " + oss.str(); ++ + // Connect to Postgres, saving the low level connection pointer + // in the holder object + PGconn* new_conn = PQconnectdb(dbconnparameters.c_str()); + if (!new_conn) { + isc_throw(DbOpenError, "could not allocate connection object"); + } + + if (PQstatus(new_conn) != CONNECTION_OK) { + // If we have a connection object, we have to call finish + // to release it, but grab the error message first. + std::string error_message = PQerrorMessage(new_conn); + PQfinish(new_conn); + isc_throw(DbOpenError, error_message); + } + + // We have a valid connection, so let's save it to our holder + conn_.setConnection(new_conn); + } + + bool + PgSqlConnection::compareError(const PgSqlResult& r, const char* error_state) { + const char* sqlstate = PQresultErrorField(r, PG_DIAG_SQLSTATE); + // PostgreSQL garuantees it will always be 5 characters long + return ((sqlstate != NULL) && + (memcmp(sqlstate, error_state, PGSQL_STATECODE_LEN) == 0)); + } + + void + PgSqlConnection::checkStatementError(const PgSqlResult& r, + PgSqlTaggedStatement& statement) const { + int s = PQresultStatus(r); + if (s != PGRES_COMMAND_OK && s != PGRES_TUPLES_OK) { + // We're testing the first two chars of SQLSTATE, as this is the + // error class. Note, there is a severity field, but it can be + // misleadingly returned as fatal. + const char* sqlstate = PQresultErrorField(r, PG_DIAG_SQLSTATE); + if ((sqlstate != NULL) && + ((memcmp(sqlstate, "08", 2) == 0) || // Connection Exception + (memcmp(sqlstate, "53", 2) == 0) || // Insufficient resources + (memcmp(sqlstate, "54", 2) == 0) || // Program Limit exceeded + (memcmp(sqlstate, "57", 2) == 0) || // Operator intervention + (memcmp(sqlstate, "58", 2) == 0))) { // System error + LOG_ERROR(dhcpsrv_logger, DHCPSRV_PGSQL_FATAL_ERROR) + .arg(statement.name) + .arg(PQerrorMessage(conn_)) + .arg(sqlstate); + exit (-1); + } + + const char* error_message = PQerrorMessage(conn_); + isc_throw(DbOperationError, "Statement exec failed:" << " for: " + << statement.name << ", reason: " + << error_message); + } + } + + void + PgSqlConnection::commit() { + LOG_DEBUG(dhcpsrv_logger, DHCPSRV_DBG_TRACE_DETAIL, DHCPSRV_PGSQL_COMMIT); + PgSqlResult r(PQexec(conn_, "COMMIT")); + if (PQresultStatus(r) != PGRES_COMMAND_OK) { + const char* error_message = PQerrorMessage(conn_); + isc_throw(DbOperationError, "commit failed: " << error_message); + } + } + + void + PgSqlConnection::rollback() { + LOG_DEBUG(dhcpsrv_logger, DHCPSRV_DBG_TRACE_DETAIL, DHCPSRV_PGSQL_ROLLBACK); + PgSqlResult r(PQexec(conn_, "ROLLBACK")); + if (PQresultStatus(r) != PGRES_COMMAND_OK) { + const char* error_message = PQerrorMessage(conn_); + isc_throw(DbOperationError, "rollback failed: " << error_message); + } + } + + }; // end of isc::dhcp namespace + }; // end of isc namespace