Make gnc_dbi_safe_sync_all safer.

With SQLite3 and PGSql perform all of the safe_sync actions in a SQL transaction.
Unfortunately MySQL commits the transaction on the first schema-altering
query (CREATE_TABLE in this case) without decrementing sql_savepoint, so
raising an error when we try to release the (non-existent) save point at
the end of writing the tables, so we have to fall back on detecting a failed
safe_sync at the next connection attempt.

Add a GncDbiSqlConnection::check_and_rollback_failed_save() to restore
the database after a failed safe_save; this is performed at the next connection.
This commit is contained in:
John Ralls 2017-02-18 10:25:06 -08:00
parent c1fa1d2fde
commit 4bf3713bf0
4 changed files with 65 additions and 9 deletions

View File

@ -866,6 +866,49 @@ GncDbiBackend<Type>::safe_sync (QofBook* book)
g_return_if_fail (conn != nullptr);
g_return_if_fail (book != nullptr);
ENTER ("book=%p, primary=%p", book, m_book);
if (!conn->begin_transaction())
{
LEAVE("Failed to obtain a transaction.");
return;
}
if (!conn->table_operation (TableOpType::backup))
{
conn->rollback_transaction();
LEAVE ("Failed to rename tables");
return;
}
if (!conn->drop_indexes())
{
conn->rollback_transaction();
LEAVE ("Failed to drop indexes");
return;
}
sync(m_book);
if (check_error())
{
conn->rollback_transaction();
LEAVE ("Failed to create new database tables");
return;
}
conn->table_operation (TableOpType::drop_backup);
conn->commit_transaction();
LEAVE ("book=%p", m_book);
}
/* MySQL commits the transaction and all savepoints after the first CREATE
* TABLE, crashing when we try to RELEASE SAVEPOINT because the savepoint
* doesn't exist after the commit. We must run without a wrapping transaction in
* that case.
*/
template <> void
GncDbiBackend<DbType::DBI_MYSQL>::safe_sync (QofBook* book)
{
auto conn = dynamic_cast<GncDbiSqlConnection*>(m_conn);
g_return_if_fail (conn != nullptr);
g_return_if_fail (book != nullptr);
ENTER ("book=%p, primary=%p", book, m_book);
if (!conn->table_operation (TableOpType::backup))
{

View File

@ -91,6 +91,11 @@ GncDbiSqlConnection::GncDbiSqlConnection (DbType type, QofBackend* qbe,
{
if (!lock_database(ignore_lock))
throw std::runtime_error("Failed to lock database!");
if (!check_and_rollback_failed_save())
{
unlock_database();
throw std::runtime_error("A failed safe-save was detected and rolling it back failed.");
}
}
bool
@ -219,6 +224,15 @@ GncDbiSqlConnection::unlock_database ()
m_qbe->set_error (ERR_BACKEND_SERVER_ERR);
}
bool
GncDbiSqlConnection::check_and_rollback_failed_save()
{
auto backup_tables = m_provider->get_table_list(m_conn, "%back");
if (backup_tables.empty())
return true;
return table_operation(rollback);
}
GncDbiSqlConnection::~GncDbiSqlConnection()
{
if (m_conn)
@ -314,8 +328,8 @@ GncDbiSqlConnection::begin_transaction () noexcept
result = dbi_conn_queryf (m_conn, "BEGIN");
else
{
std::ostringstream savepoint("savepoint_");
savepoint << m_sql_savepoint;
std::ostringstream savepoint;
savepoint << "savepoint_" << m_sql_savepoint;
result = dbi_conn_queryf(m_conn, "SAVEPOINT %s",
savepoint.str().c_str());
}
@ -348,8 +362,8 @@ GncDbiSqlConnection::rollback_transaction () noexcept
result = dbi_conn_query (m_conn, "ROLLBACK");
else
{
std::ostringstream savepoint("savepoint_");
savepoint << m_sql_savepoint;
std::ostringstream savepoint;
savepoint << "savepoint_" << m_sql_savepoint - 1;
result = dbi_conn_queryf(m_conn, "ROLLBACK TO SAVEPOINT %s",
savepoint.str().c_str());
}
@ -381,8 +395,8 @@ GncDbiSqlConnection::commit_transaction () noexcept
result = dbi_conn_queryf (m_conn, "COMMIT");
else
{
std::ostringstream savepoint("savepoint_");
savepoint << m_sql_savepoint;
std::ostringstream savepoint;
savepoint << "savepoint_" << m_sql_savepoint - 1;
result = dbi_conn_queryf(m_conn, "RELEASE SAVEPOINT %s",
savepoint.str().c_str());
}

View File

@ -110,6 +110,7 @@ private:
unsigned int m_sql_savepoint;
bool lock_database(bool ignore_lock);
void unlock_database();
bool check_and_rollback_failed_save();
};

View File

@ -673,9 +673,7 @@ GncSqlBackend::init_version_info() noexcept
bool
GncSqlBackend::reset_version_info() noexcept
{
bool ok = true;
if (!m_conn->does_table_exist (VERSION_TABLE_NAME))
ok = create_table (VERSION_TABLE_NAME, version_table);
bool ok = create_table (VERSION_TABLE_NAME, version_table);
m_versions.clear();
set_table_version ("Gnucash", gnc_prefs_get_long_version ());
set_table_version ("Gnucash-Resave", GNUCASH_RESAVE_VERSION);