Fix race condition in multiprocess schema init

If the schema was initialized by a different process between when the old
schema was read and the write transaction was began, the schema init code would
see the updated schema version but not re-read the schema, resulting in it
thinking that a migration was required when the schema actually matched.
This commit is contained in:
Thomas Goyne 2015-11-16 11:22:23 -08:00
parent 415bfe4d35
commit 6c25eeb85c
1 changed files with 52 additions and 28 deletions

View File

@ -201,6 +201,7 @@ bool Realm::update_schema(std::unique_ptr<Schema> schema, uint64_t version)
{ {
schema->validate(); schema->validate();
// If the schema version matches, just verify that the schema itself also matches
bool needs_update = !m_config.read_only && (m_config.schema_version != version || ObjectStore::needs_update(*m_config.schema, *schema)); bool needs_update = !m_config.read_only && (m_config.schema_version != version || ObjectStore::needs_update(*m_config.schema, *schema));
if (!needs_update) { if (!needs_update) {
ObjectStore::verify_schema(*m_config.schema, *schema, m_config.read_only); ObjectStore::verify_schema(*m_config.schema, *schema, m_config.read_only);
@ -209,41 +210,64 @@ bool Realm::update_schema(std::unique_ptr<Schema> schema, uint64_t version)
return false; return false;
} }
// Store the old config/schema for the migration function, and update begin_transaction();
// our schema to the new one struct WriteTransactionGuard {
auto old_schema = std::move(m_config.schema); Realm& realm;
Config old_config(m_config); ~WriteTransactionGuard() {
old_config.read_only = true; if (realm.is_in_transaction()) {
old_config.schema = std::move(old_schema); realm.cancel_transaction();
}
}
} write_transaction_guard{*this};
// Recheck the schema version after beginning the write transaction
// If it changed then someone else initialized the schema and we need to
// recheck everything
auto current_schema_version = ObjectStore::get_schema_version(read_group());
if (current_schema_version != m_config.schema_version) {
cancel_transaction();
m_config.schema_version = current_schema_version;
*m_config.schema = ObjectStore::schema_from_group(read_group());
return update_schema(std::move(schema), version);
}
auto migration_function = [&](Group*, Schema&) {
SharedRealm old_realm(new Realm(m_config));
old_realm->m_config.read_only = true;
auto new_realm = shared_from_this();
m_config.schema = std::move(schema); m_config.schema = std::move(schema);
m_config.schema_version = version; m_config.schema_version = version;
auto migration_function = [&](Group*, Schema&) { if (!m_config.migration_function) {
SharedRealm old_realm(new Realm(old_config)); return;
auto updated_realm = shared_from_this(); }
if (m_config.migration_function) {
m_config.migration_function(old_realm, updated_realm); try {
m_config.migration_function(old_realm, new_realm);
}
catch (...) {
m_config.schema = std::move(old_realm->m_config.schema);
m_config.schema_version = old_realm->m_config.schema_version;
throw;
} }
}; };
try { bool changed = ObjectStore::update_realm_with_schema(read_group(), *m_config.schema,
// update and migrate version, *schema,
begin_transaction();
bool changed = ObjectStore::update_realm_with_schema(read_group(), *old_config.schema,
version, *m_config.schema,
migration_function); migration_function);
commit_transaction(); commit_transaction();
if (schema) {
// We update the schema after opening the "old" Realm in the migration
// block to reduce the amount of juggling required, but that means that
// the schema hasn't been updated if no migration occurred
m_config.schema = std::move(schema);
m_config.schema_version = version;
}
return changed; return changed;
}
catch (...) {
if (is_in_transaction()) {
cancel_transaction();
}
m_config.schema_version = old_config.schema_version;
m_config.schema = std::move(old_config.schema);
throw;
}
} }
static void check_read_write(Realm *realm) static void check_read_write(Realm *realm)