Commit 141e56e1 authored by Daniel Vrátil's avatar Daniel Vrátil 🤖
Browse files

Fix replaying queries from aborted transactions

Instead of re-executing the QSqlQuery that was part of the initial
transaction run we store the plain SQL statement and bind values and
we create a new QSqlQuery when replaying the transaction.

This seems to fix an issue when certain queries were not re-executed
properly on MySQL.
parent 68f69a47
......@@ -1265,7 +1265,7 @@ QDateTime DataStore::dateTimeToQDateTime(const QByteArray &dateTime)
return QDateTime::fromString(QString::fromLatin1(dateTime), QStringLiteral("yyyy-MM-dd hh:mm:ss"));
}
void DataStore::addQueryToTransaction(const QSqlQuery &query, bool isBatch)
void DataStore::addQueryToTransaction(const QString &statement, const QVector<QVariant> &bindValues, bool isBatch)
{
// This is used for replaying deadlocked transactions, so only record queries
// for backends that support concurrent transactions.
......@@ -1273,7 +1273,7 @@ void DataStore::addQueryToTransaction(const QSqlQuery &query, bool isBatch)
return;
}
m_transactionQueries.append(qMakePair(query, isBatch));
m_transactionQueries.append({ statement, bindValues, isBatch });
}
QSqlQuery DataStore::retryLastTransaction(bool rollbackFirst)
......@@ -1299,38 +1299,29 @@ QSqlQuery DataStore::retryLastTransaction(bool rollbackFirst)
}
m_transactionLevel = oldTransactionLevel;
typedef QPair<QSqlQuery, bool> QueryBoolPair;
QMutableVectorIterator<QueryBoolPair> iter(m_transactionQueries);
while (iter.hasNext()) {
iter.next();
QSqlQuery query = iter.value().first;
const bool isBatch = iter.value().second;
// Make sure the query is ready to be executed again
if (query.isActive()) {
query.finish();
QSqlQuery lastQuery;
for (auto q = m_transactionQueries.begin(), qEnd = m_transactionQueries.end(); q != qEnd; ++q) {
QSqlQuery query(database());
query.prepare(q->query);
for (int i = 0; i < q->boundValues.count(); ++i) {
query.bindValue(QLatin1Char(':') + QString::number(i), q->boundValues.at(i));
}
bool res = false;
if (isBatch) {
// QSqlQuery::execBatch() does not reset lastError(), so for the sake
// of transparency (make it look to the caller like if the query was
// successful the first time), we create a copy of the original query,
// which has lastError empty.
QSqlQuery copiedQuery(m_database);
copiedQuery.prepare(query.executedQuery());
const QMap<QString, QVariant> boundValues = query.boundValues();
int i = 0;
for (const QVariant &value : boundValues) {
copiedQuery.bindValue(i, value);
++i;
}
query = copiedQuery;
QElapsedTimer t; t.start();
if (q->isBatch) {
res = query.execBatch();
} else {
res = query.exec();
}
if (StorageDebugger::instance()->isSQLDebuggingEnabled()) {
StorageDebugger::instance()->queryExecuted(reinterpret_cast<qint64>(this),
query, t.elapsed());
} else {
StorageDebugger::instance()->incSequence();
}
if (!res) {
// Don't do another deadlock detection here, just give up.
qCCritical(AKONADISERVER_LOG) << "DATABASE ERROR:";
......@@ -1341,14 +1332,13 @@ QSqlQuery DataStore::retryLastTransaction(bool rollbackFirst)
// Return the last query, because that's what caller expects to retrieve
// from QueryBuilder. It is in error state anyway.
return m_transactionQueries.last().first;
return query;
}
// Update the query in the list
iter.setValue(qMakePair(query, isBatch));
lastQuery = query;
}
return m_transactionQueries.last().first;
return lastQuery;
}
bool DataStore::beginTransaction(const QString &name)
......
......@@ -331,7 +331,7 @@ private:
*
* This method should only be used by QueryBuilder.
*/
void addQueryToTransaction(const QSqlQuery &query, bool isBatch);
void addQueryToTransaction(const QString &statement, const QVector<QVariant> &bindValues, bool isBatch);
/**
* Tries to execute all queries from last transaction again. If any of the
......@@ -355,7 +355,12 @@ protected:
QSqlDatabase m_database;
bool m_dbOpened;
uint m_transactionLevel;
QVector<QPair<QSqlQuery, bool /* isBatch */> > m_transactionQueries;
struct TransactionQuery {
QString query;
QVector<QVariant> boundValues;
bool isBatch;
};
QVector<TransactionQuery> m_transactionQueries;
QByteArray mSessionId;
NotificationCollector *mNotificationCollector;
QTimer *m_keepAliveTimer;
......
......@@ -366,7 +366,7 @@ bool QueryBuilder::exec()
bool isBatch = false;
for (int i = 0; i < mBindValues.count(); ++i) {
mQuery.bindValue(QLatin1Char(':') + QString::number(i), mBindValues[i]);
if (!isBatch && mBindValues[i].canConvert<QVariantList>()) {
if (!isBatch && static_cast<QMetaType::Type>(mBindValues[i].type()) == QMetaType::QVariantList) {
isBatch = true;
}
//qCDebug(AKONADISERVER_LOG) << QString::fromLatin1( ":%1" ).arg( i ) << mBindValues[i];
......@@ -397,7 +397,7 @@ bool QueryBuilder::exec()
// The method does nothing when this query is not executed within a transaction.
// We don't care whether the query was successful or not. In case of error, the caller
// will rollback the transaction anyway, and all cached queries will be removed.
DataStore::self()->addQueryToTransaction(mQuery, isBatch);
DataStore::self()->addQueryToTransaction(statement, mBindValues, isBatch);
if (!ret) {
// Handle transaction deadlocks and timeouts by attempting to replay the transaction.
......
Markdown is supported
0% or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment