handle lost connection during transactions

- bugfix reset transactions when lost connection occurs during
   commit()/rollBack()/savepoint()/rollBackToSavepoint() method calls
 - enhanced beginTransaction(), reconnect when connection was lost
This commit is contained in:
silverqx
2022-02-23 11:29:28 +01:00
parent 37c5f6e0d5
commit fbee3297ac
6 changed files with 100 additions and 48 deletions

View File

@@ -11,6 +11,8 @@ TINY_SYSTEM_HEADER
TINYORM_BEGIN_COMMON_NAMESPACE
class QSqlError;
namespace Orm
{
@@ -35,6 +37,8 @@ namespace Concerns
/*! Determine if the given exception was caused by a lost connection. */
bool causedByLostConnection(const Exceptions::SqlError &e) const;
/*! Determine if the given exception was caused by a lost connection. */
bool causedByLostConnection(const QSqlError &e) const;
};
} // namespace Concerns

View File

@@ -12,6 +12,8 @@ TINY_SYSTEM_HEADER
TINYORM_BEGIN_COMMON_NAMESPACE
class QSqlError;
namespace Orm
{
@@ -75,6 +77,19 @@ namespace Concerns
/*! Dynamic cast *this to the Concerns::CountsQueries & base type. */
Concerns::CountsQueries &countsQueries();
/*! Transform a QtSql transaction error to TinyORM SqlTransactionError
exception. */
void throwIfTransactionError(
QString &&functionName, const QString &queryString, QSqlError &&error);
/*! Handle an error returned when beginning a transaction. */
void handleStartTransactionError(
QString &&functionName, const QString &queryString, QSqlError &&error);
/*! Handle an error returned during a transaction commit, rollBack, savepoint or
rollbackToSavepoint. */
void handleCommonTransactionError(
QString &&functionName, const QString &queryString, QSqlError &&error);
/*! The connection is in the transaction state. */
bool m_inTransaction = false;
/*! Active savepoints counter. */

View File

@@ -238,7 +238,7 @@ namespace Schema
const QString &queryString, const QVector<QVariant> &bindings,
const RunCallback<Return> &callback) const;
/*! Reconnect to the database if a PDO connection is missing. */
/*! Reconnect to the database if a Qt connection is missing. */
void reconnectIfMissingConnection() const;
/*! The active QSqlDatabase connection name. */

View File

@@ -10,6 +10,11 @@ namespace Orm::Concerns
{
bool DetectsLostConnections::causedByLostConnection(const Exceptions::SqlError &e) const
{
return causedByLostConnection(e.getSqlError().databaseText());
}
bool DetectsLostConnections::causedByLostConnection(const QSqlError &e) const
{
// TODO verify this will be pain in the ass 😕 silverqx
static const QVector<QString> lostMessagesCache {
@@ -53,7 +58,7 @@ bool DetectsLostConnections::causedByLostConnection(const Exceptions::SqlError &
};
return std::ranges::any_of(lostMessagesCache,
[databaseError = e.getSqlError().databaseText()]
[databaseError = e.databaseText()]
(const auto &lostMessage)
{
// found

View File

@@ -20,10 +20,11 @@ ManagesTransactions::ManagesTransactions()
bool ManagesTransactions::beginTransaction()
{
Q_ASSERT(m_inTransaction == false);
Q_ASSERT(m_savepoints == 0);
databaseConnection().reconnectIfMissingConnection();
static const auto query = QStringLiteral("START TRANSACTION");
static const auto queryString = QStringLiteral("START TRANSACTION");
// Elapsed timer needed
const auto countElapsed = databaseConnection().shouldCountElapsed();
@@ -35,10 +36,9 @@ bool ManagesTransactions::beginTransaction()
if (!databaseConnection().pretending() &&
!databaseConnection().getQtConnection().transaction()
)
throw Exceptions::SqlTransactionError(
QStringLiteral("Statement in %1() failed : %2")
.arg(__tiny_func__, query),
databaseConnection().getRawQtConnection().lastError());
handleStartTransactionError(
__tiny_func__, queryString,
databaseConnection().getRawQtConnection().lastError());
m_inTransaction = true;
@@ -49,9 +49,9 @@ bool ManagesTransactions::beginTransaction()
that it took to run and then log the query and execution time.
We'll log time in milliseconds. */
if (databaseConnection().pretending())
databaseConnection().logTransactionQueryForPretend(query);
databaseConnection().logTransactionQueryForPretend(queryString);
else
databaseConnection().logTransactionQuery(query, std::move(elapsed));
databaseConnection().logTransactionQuery(queryString, std::move(elapsed));
return true;
}
@@ -60,7 +60,7 @@ bool ManagesTransactions::commit()
{
Q_ASSERT(m_inTransaction);
static const auto query = QStringLiteral("COMMIT");
static const auto queryString = QStringLiteral("COMMIT");
// Elapsed timer needed
const auto countElapsed = databaseConnection().shouldCountElapsed();
@@ -72,10 +72,8 @@ bool ManagesTransactions::commit()
if (!databaseConnection().pretending() &&
!databaseConnection().getQtConnection().commit()
)
throw Exceptions::SqlTransactionError(
QStringLiteral("Statement in %1() failed : %2")
.arg(__tiny_func__, query),
databaseConnection().getRawQtConnection().lastError());
handleCommonTransactionError(__tiny_func__, queryString,
databaseConnection().getRawQtConnection().lastError());
resetTransactions();
@@ -86,9 +84,9 @@ bool ManagesTransactions::commit()
that it took to run and then log the query and execution time.
We'll log time in milliseconds. */
if (databaseConnection().pretending())
databaseConnection().logTransactionQueryForPretend(query);
databaseConnection().logTransactionQueryForPretend(queryString);
else
databaseConnection().logTransactionQuery(query, std::move(elapsed));
databaseConnection().logTransactionQuery(queryString, std::move(elapsed));
return true;
}
@@ -97,7 +95,7 @@ bool ManagesTransactions::rollBack()
{
Q_ASSERT(m_inTransaction);
static const auto query = QStringLiteral("ROLLBACK");
static const auto queryString = QStringLiteral("ROLLBACK");
// Elapsed timer needed
const auto countElapsed = databaseConnection().shouldCountElapsed();
@@ -109,10 +107,8 @@ bool ManagesTransactions::rollBack()
if (!databaseConnection().pretending() &&
!databaseConnection().getQtConnection().rollback()
)
throw Exceptions::SqlTransactionError(
QStringLiteral("Statement in %1() failed : %2")
.arg(__tiny_func__, query),
databaseConnection().getRawQtConnection().lastError());
handleCommonTransactionError(__tiny_func__, queryString,
databaseConnection().getRawQtConnection().lastError());
resetTransactions();
@@ -123,20 +119,20 @@ bool ManagesTransactions::rollBack()
that it took to run and then log the query and execution time.
We'll log time in milliseconds. */
if (databaseConnection().pretending())
databaseConnection().logTransactionQueryForPretend(query);
databaseConnection().logTransactionQueryForPretend(queryString);
else
databaseConnection().logTransactionQuery(query, std::move(elapsed));
databaseConnection().logTransactionQuery(queryString, std::move(elapsed));
return true;
}
bool ManagesTransactions::savepoint(const QString &id)
{
// TODO rewrite savepoint() and rollBack() with a new m_connection.statement() API silverqx
Q_ASSERT(m_inTransaction);
auto savePoint = databaseConnection().getQtQuery();
const auto query = QStringLiteral("SAVEPOINT %1_%2").arg(m_savepointNamespace, id);
const auto queryString =
QStringLiteral("SAVEPOINT %1_%2").arg(m_savepointNamespace, id);
// Elapsed timer needed
const auto countElapsed = databaseConnection().shouldCountElapsed();
@@ -146,11 +142,9 @@ bool ManagesTransactions::savepoint(const QString &id)
timer.start();
// Execute a savepoint query
if (!databaseConnection().pretending() && !savePoint.exec(query))
throw Exceptions::SqlTransactionError(
QStringLiteral("Statement in %1() failed : %2")
.arg(__tiny_func__, query),
savePoint.lastError());
if (!databaseConnection().pretending() && !savePoint.exec(queryString))
handleCommonTransactionError(__tiny_func__, queryString,
databaseConnection().getRawQtConnection().lastError());
++m_savepoints;
@@ -161,9 +155,9 @@ bool ManagesTransactions::savepoint(const QString &id)
that it took to run and then log the query and execution time.
We'll log time in milliseconds. */
if (databaseConnection().pretending())
databaseConnection().logTransactionQueryForPretend(query);
databaseConnection().logTransactionQueryForPretend(queryString);
else
databaseConnection().logTransactionQuery(query, std::move(elapsed));
databaseConnection().logTransactionQuery(queryString, std::move(elapsed));
return true;
}
@@ -179,8 +173,8 @@ bool ManagesTransactions::rollbackToSavepoint(const QString &id)
Q_ASSERT(m_savepoints > 0);
auto rollbackToSavepoint = databaseConnection().getQtQuery();
const auto query = QStringLiteral("ROLLBACK TO SAVEPOINT %1_%2")
.arg(m_savepointNamespace, id);
const auto queryString =
QStringLiteral("ROLLBACK TO SAVEPOINT %1_%2").arg(m_savepointNamespace, id);
// Elapsed timer needed
const auto countElapsed = databaseConnection().shouldCountElapsed();
@@ -190,11 +184,9 @@ bool ManagesTransactions::rollbackToSavepoint(const QString &id)
timer.start();
// Execute a rollback to savepoint query
if (!databaseConnection().pretending() && !rollbackToSavepoint.exec(query))
throw Exceptions::SqlTransactionError(
QStringLiteral("Statement in %1() failed : %2")
.arg(__tiny_func__, query),
rollbackToSavepoint.lastError());
if (!databaseConnection().pretending() && !rollbackToSavepoint.exec(queryString))
handleCommonTransactionError(__tiny_func__, queryString,
databaseConnection().getRawQtConnection().lastError());
m_savepoints = std::max<std::size_t>(0, m_savepoints - 1);
@@ -205,9 +197,9 @@ bool ManagesTransactions::rollbackToSavepoint(const QString &id)
that it took to run and then log the query and execution time.
We'll log time in milliseconds. */
if (databaseConnection().pretending())
databaseConnection().logTransactionQueryForPretend(query);
databaseConnection().logTransactionQueryForPretend(queryString);
else
databaseConnection().logTransactionQuery(query, std::move(elapsed));
databaseConnection().logTransactionQuery(queryString, std::move(elapsed));
return true;
}
@@ -245,6 +237,35 @@ CountsQueries &ManagesTransactions::countsQueries()
return dynamic_cast<CountsQueries &>(*this);
}
void ManagesTransactions::throwIfTransactionError(
QString &&functionName, const QString &queryString, QSqlError &&error)
{
throw Exceptions::SqlTransactionError(
QStringLiteral("Statement in %1() failed : %2")
.arg(functionName, queryString),
error);
}
void ManagesTransactions::handleStartTransactionError(
QString &&functionName, const QString &queryString, QSqlError &&error)
{
if (!databaseConnection().causedByLostConnection(error))
throwIfTransactionError(std::move(functionName), queryString, std::move(error));
databaseConnection().reconnect();
databaseConnection().getQtConnection().transaction();
}
void ManagesTransactions::handleCommonTransactionError(
QString &&functionName, const QString &queryString, QSqlError &&error)
{
if (databaseConnection().causedByLostConnection(error))
resetTransactions();
throwIfTransactionError(std::move(functionName), queryString, std::move(error));
}
} // namespace Orm::Concerns
TINYORM_END_COMMON_NAMESPACE

View File

@@ -280,7 +280,7 @@ DatabaseConnection::setQtConnectionResolver(
/* m_qtConnection.reset() is called also in DatabaseConnection::disconnect(),
because both methods are public apis.
m_qtConnection can also be understood as m_qtConnectionWasResolved,
because it performs two functions, saves active connection name and
because it has two functions, saves active connection name and
if it's not nullopt, then it means, that the database connection was
resolved by m_qtConnectionResolver.
If it's nullopt, then m_qtConnectionResolver should be called to
@@ -373,7 +373,7 @@ void DatabaseConnection::disconnect()
and invalidating any existing QSqlQuery objects that are used
with the database.
Only close the QSqlDatabase database connection and don't remove it
from QSqlDatabase connection repository, so they can be reused, it's
from QSqlDatabase connection repository, so it can be reused, it's
better for performance. */
getRawQtConnection().close();
@@ -492,12 +492,19 @@ std::unique_ptr<QueryProcessor> DatabaseConnection::getDefaultPostProcessor() co
void DatabaseConnection::reconnectIfMissingConnection() const
{
if (!m_qtConnectionResolver) {
// This should never happen, but when it does, I want to know about that
Q_ASSERT(m_qtConnection);
/* Calls a connection resolver defined
in the ConnectionFactory::createQSqlDatabaseResolver(), the connection resolver
is passed to the DatabaseConnection constructor and is always available. Only
one exception is when disconnect() is called, it resets connection resolver which
will be recreated (with the db connection of course) here, that is only one case
when code below (reconnect() logic) is true as I'm aware of.*/
if (m_qtConnectionResolver)
return;
reconnect();
}
// This should never happen, but when it does, I want to know about that
Q_ASSERT(m_qtConnection);
reconnect();
}
/* private */