Skip to content
Permalink
Browse files

Speed up schema upgrades.

The old schema upgrade didn't lock before checking the schema version, so two processes
try to upgrade the same schema. To try to avoid this happening in practice we would sleep
for a few seconds whenever the schema didn't match what we expected. This was safe since
the actual schema upgrade used a lock, but it was unnecessarily slow.

Now we just lock before we check the schema version. If the schema is locked we wait,
if not we proceed.
  • Loading branch information
daniel-kristjansson committed May 11, 2012
1 parent 653a236 commit da86aeb60a4b97763993c4279b018a3f06a4121d
@@ -71,36 +71,82 @@ bool UpgradeMusicDatabaseSchema(void)
#ifdef IGNORE_SCHEMA_VER_MISMATCH
return true;
#endif
SchemaUpgradeWizard *schema_wizard = NULL;

SchemaUpgradeWizard * DBup;
// Suppress DB messages and turn of the settings cache,
// These are likely to confuse the users and the code, respectively.
GetMythDB()->SetSuppressDBMessages(true);
gCoreContext->ActivateSettingsCache(false);

// Get the schema upgrade lock
MSqlQuery query(MSqlQuery::InitCon());
bool locked = DBUtil::TryLockSchema(query, 1);
for (uint i = 0; i < 2*60 && !locked; i++)
{
LOG(VB_GENERAL, LOG_INFO, "Waiting for database schema upgrade lock");
locked = DBUtil::TryLockSchema(query, 1);
if (locked)
LOG(VB_GENERAL, LOG_INFO, "Got schema upgrade lock");
}
if (!locked)
{
LOG(VB_GENERAL, LOG_INFO, "Failed to get schema upgrade lock");
goto upgrade_error_exit;
}

DBup = SchemaUpgradeWizard::Get("MusicDBSchemaVer", "MythMusic",
currentDatabaseVersion);

// There may be a race condition where another frontend is upgrading,
// so wait up to 3 seconds for a more accurate version:
DBup->CompareAndWait(3);
schema_wizard = SchemaUpgradeWizard::Get(
"MusicDBSchemaVer", "MythMusic", currentDatabaseVersion);

if (DBup->versionsBehind == 0) // same schema
return true;
if (schema_wizard->Compare() == 0) // DB schema is what we need it to be..
goto upgrade_ok_exit;

if (DBup->DBver.isEmpty())
return doUpgradeMusicDatabaseSchema(DBup->DBver);
if (schema_wizard->DBver.isEmpty())
{
// We need to create a database from scratch
if (doUpgradeMusicDatabaseSchema(schema_wizard->DBver))
goto upgrade_ok_exit;
else
goto upgrade_error_exit;
}

// Pop up messages, questions, warnings, et c.
switch (DBup->PromptForUpgrade("Music", true, false))
switch (schema_wizard->PromptForUpgrade("Music", true, false))
{
case MYTH_SCHEMA_USE_EXISTING:
return true;
goto upgrade_ok_exit;
case MYTH_SCHEMA_ERROR:
case MYTH_SCHEMA_EXIT:
return false;
goto upgrade_error_exit;
case MYTH_SCHEMA_UPGRADE:
break;
}

return doUpgradeMusicDatabaseSchema(DBup->DBver);
if (!doUpgradeMusicDatabaseSchema(schema_wizard->DBver))
{
LOG(VB_GENERAL, LOG_ERR, "Database schema upgrade failed.");
goto upgrade_error_exit;
}

LOG(VB_GENERAL, LOG_INFO, "MythMusic database schema upgrade complete.");

// On any exit we want to re-enable the DB messages so errors
// are reported and we want to make sure the setting cache is
// enabled for good performance and we must unlock the schema
// lock. We use gotos with labels so it's impossible to miss
// these steps.
upgrade_ok_exit:
GetMythDB()->SetSuppressDBMessages(false);
gCoreContext->ActivateSettingsCache(true);
if (locked)
DBUtil::UnlockSchema(query);
return true;

upgrade_error_exit:
GetMythDB()->SetSuppressDBMessages(false);
gCoreContext->ActivateSettingsCache(true);
if (locked)
DBUtil::UnlockSchema(query);
return false;
}


@@ -119,70 +119,24 @@ int SchemaUpgradeWizard::Compare(void)
#if TESTING
//DBver = "9" + DBver + "-testing";
DBver += "-testing";
return 0;
return 0;
#endif

return versionsBehind = m_newSchemaVer.toInt() - DBver.toUInt();
}

int SchemaUpgradeWizard::CompareAndWait(const int seconds)
{
if (Compare() > 0) // i.e. if DB is older than expected
if (m_newSchemaVer == DBver)
{
QString message = tr("%1 database schema is old. Waiting to see if DB "
"is being upgraded.").arg(m_schemaName);

LOG(VB_GENERAL, LOG_CRIT, message);

MSqlQuery query(MSqlQuery::InitCon(MSqlQuery::kDedicatedConnection));
bool backupRunning = false;
bool upgradeRunning = false;

MythTimer elapsedTimer;
elapsedTimer.start();
while (versionsBehind && (elapsedTimer.elapsed() < seconds * 1000))
{
sleep(1);

if (IsBackupInProgress())
{
LOG(VB_GENERAL, LOG_CRIT,
"Waiting for Database Backup to complete.");
if (!backupRunning)
{
elapsedTimer.restart();
backupRunning = true;
}
continue;
}

if (!lockSchema(query))
{
LOG(VB_GENERAL, LOG_CRIT,
"Waiting for Database Upgrade to complete.");
if (!upgradeRunning)
{
elapsedTimer.restart();
upgradeRunning = true;
}
continue;
}

Compare();
unlockSchema(query);

if (m_expertMode)
break;
}

if (versionsBehind)
LOG(VB_GENERAL, LOG_CRIT, "Timed out waiting.");
versionsBehind = 0;
}
else
{
// Branch DB versions may not be integer version numbers.
bool new_ok, old_ok;
int new_version = m_newSchemaVer.toInt(&new_ok);
int old_version = DBver.toInt(&old_ok);
if (new_ok && old_ok)
versionsBehind = new_version - old_version;
else
LOG(VB_GENERAL, LOG_CRIT,
"Schema version was upgraded while we were waiting.");
versionsBehind = 5000;
}
// else DB is same version, or newer. Either way, we won't upgrade it

return versionsBehind;
}

@@ -1,3 +1,4 @@

#ifndef SCHEMA_WIZARD_H
#define SCHEMA_WIZARD_H

@@ -18,34 +19,9 @@ enum MythSchemaUpgrade
MYTH_SCHEMA_USE_EXISTING = 4
};

/**
* \usage
* #include <libmyth/schemawizard.h>
* ...
* {
* SchemaUpgradeWizard * DBup;
*
* gContext->ActivateSettingsCache(false);
* DBup = SchemaUpgradeWizard::Get("DBSchemaVer", "1123");
*
* DBup->CompareAndWait(5);
*
* switch (DBup->PromptForUpgrade("Music", true, false, 5, 0, 2)
* {
* case MYTH_SCHEMA_EXIT:
* case MYTH_SCHEMA_ERROR:
* exit(GENERIC_EXIT_NOT_OK);
* case MYTH_SCHEMA_UPGRADE:
* if (!DBup->didBackup())
* (void)DBup->BackupDB();
* // Upgrade schema
* break;
* case MYTH_SCHEMA_USE_EXISTING:
* break;
* }
* }
/** \brief Provides UI and helper functions for DB Schema updates.
* See dbcheck.cpp's UpgradeTVDatabaseSchema() for usage.
*/

class MPUBLIC SchemaUpgradeWizard : public QObject, public DBUtil
{
Q_OBJECT
@@ -62,9 +38,6 @@ class MPUBLIC SchemaUpgradeWizard : public QObject, public DBUtil
/// How many schema versions old is the DB?
int Compare(void);

/// Waits for DB backups/schema locks, to ensure accuracy
int CompareAndWait(const int seconds);

/// Instead of creating a new wizard, use the existing one
/// for its DB backup file & results and expert settings.
static SchemaUpgradeWizard *Get(const QString &DBSchemaSetting,
@@ -853,39 +853,20 @@ int DBUtil::CountClients(void)
return count;
}

/**
* \brief Try to get a lock on the table schemalock.
*
* To prevent upgrades by different programs of the same schema.
* (<I>e.g.</I> when both mythbackend and mythfrontend start at the same time)
/** \brief Try to get a lock on the table schemalock.
* Prevents multiple upgrades by different programs of the same schema.
*/
bool DBUtil::lockSchema(MSqlQuery &query)
bool DBUtil::TryLockSchema(MSqlQuery &query, uint timeout_secs)
{
if (!query.exec("CREATE TABLE IF NOT EXISTS "
"schemalock ( schemalock int(1));"))
{
LOG(VB_GENERAL, LOG_CRIT,
QString("ERROR: Unable to create schemalock table: %1")
.arg(MythDB::DBErrorMessage(query.lastError())));
return false;
}

if (!query.exec("LOCK TABLE schemalock WRITE;"))
{
LOG(VB_GENERAL, LOG_CRIT,
QString("ERROR: Unable to acquire database upgrade lock")
.arg(MythDB::DBErrorMessage(query.lastError())));
return false;
}

return true;
query.prepare("SELECT GET_LOCK('schemaLock', :TIMEOUT)");
query.bindValue(":TIMEOUT", timeout_secs);
return query.exec() && query.first() && query.value(0).toBool();
}

void DBUtil::unlockSchema(MSqlQuery &query)
void DBUtil::UnlockSchema(MSqlQuery &query)
{
// Should this _just_ unlock schemalock?
if (!query.exec("UNLOCK TABLES;"))
MythDB::DBError("unlockSchema -- unlocking tables", query);
query.prepare("SELECT RELEASE_LOCK('schemaLock')");
query.exec();
}

/* vim: set expandtab tabstop=4 shiftwidth=4: */
@@ -46,9 +46,8 @@ class MBASE_PUBLIC DBUtil
static bool IsBackupInProgress(void);
static int CountClients(void);

static bool lockSchema(MSqlQuery &);
static void unlockSchema(MSqlQuery &);

static bool TryLockSchema(MSqlQuery &, uint timeout_secs);
static void UnlockSchema(MSqlQuery &);

static const int kUnknownVersionNumber;

1 comment on commit da86aeb

@NigelPearson

This comment has been minimized.

Copy link
Member

NigelPearson commented on da86aeb Jun 4, 2012

  1. Much faster, but the amount of duplicated code is getting large. (especially if it ever goes into MythWeather/Gallery/Archive/et c).

How do you feel about something like "bool SchemaUpgradeWizard::GetSchemaLock()" which suppresses messages, deactivates the cache, and does the TryLock loop. Maybe with a partner ::ClearSchemaLock()

  1. UpgradeMusicDatabaseSchema() now has an additional check for schema_wizard->DBver.isEmpty(). I thought that was already handled by versionsBehind? (i.e. DBver=0, m_newSchemaVer!=0 in old code, or versionsBehind=5000 in new)

If this was to produce a different log message for new table creation, UpgradeTVDatabaseSchema() should do to too?

Please sign in to comment.
You can’t perform that action at this time.