Skip to content

Commit

Permalink
Fix race conditions when multiple processes initialize a Realm file a…
Browse files Browse the repository at this point in the history
…t the same time

Main change here is to unconditionally start a write transaction on
schema init, push the version check inside that write transaction, and
roll it back if it turns out that no changes are needed, to avoid having
two processes both decide that they need to initialize things (in the
single-process case this was previously prevented via a @synchronized
block). I *think* the double-init would have worked out okay other than
a spurious change notification, but I'm much less confident in that than
I am in this working correctly.

There's two functional changes:

1. Dynamic realms are now cached, as we concluded that there was no
reason for them not to be and it simplifies the code a bit

2. Explicitly calling migrateRealmAtPath: will create any missing tables
even if the schema version matches. Main point is to push the schema
version check into the write transaction to avoid a spurious migration,
but it also just seems like the right behavior.

3. Exceptions during migrations cancel the write rather than committing.
  • Loading branch information
tgoyne committed Dec 30, 2014
1 parent 46e0d82 commit 4e5f4a5
Show file tree
Hide file tree
Showing 8 changed files with 154 additions and 112 deletions.
2 changes: 2 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,8 @@ x.x.x Release notes (yyyy-MM-dd)

* Fix an assertion failure when invalidating a Realm which is in a write
transaction, has already been invalidated, or has never been used.
* Roll back all changes when an error occurs during a migration rather than
committing the half-migrated state.

0.89.1 Release notes (2014-12-22)
=============================================================
Expand Down
38 changes: 33 additions & 5 deletions Realm/RLMMigration.mm
Original file line number Diff line number Diff line change
Expand Up @@ -127,12 +127,38 @@ - (void)verifyPrimaryKeyUniqueness {
}

- (void)migrateWithBlock:(RLMMigrationBlock)block version:(NSUInteger)newVersion {
// start write transaction
// begin the write transaction before checking the existing schema version
// to avoid multiple threads/processes seeing an old version and deciding to
// run the migration
[_realm beginWriteTransaction];

NSUInteger schemaVersion = RLMRealmSchemaVersion(_realm);
@try {
if (schemaVersion == newVersion) {
// even if the version matches we may still need to add new tables
if (RLMRealmCreateTables(_realm, RLMSchema.sharedSchema, newVersion, false)) {
[_realm commitWriteTransaction];
}
else {
[_realm cancelWriteTransaction];
}
return;
}
else if (schemaVersion > newVersion && schemaVersion != RLMNotVersioned) {
if (!block) {
@throw [NSException exceptionWithName:@"RLMException"
reason:@"No migration block specified for a Realm with a schema version greater than 0. You must supply a valid schema version and migration block before accessing any Realm by calling `setSchemaVersion:withMigrationBlock:`"
userInfo:@{@"path" : _realm.path}];
}
else {
@throw [NSException exceptionWithName:@"RLMException"
reason:@"Realm version is higher than the current version provided to `setSchemaVersion:withMigrationBlock:`"
userInfo:@{@"path" : _realm.path}];
}
}

// add new tables/columns for the current shared schema
RLMRealmCreateTables(_realm, [RLMSchema sharedSchema], true);
RLMRealmCreateTables(_realm, [RLMSchema sharedSchema], newVersion, true);

// disable all primary keys for migration
for (RLMObjectSchema *objectSchema in _realm.schema.objectSchema) {
Expand All @@ -149,10 +175,12 @@ - (void)migrateWithBlock:(RLMMigrationBlock)block version:(NSUInteger)newVersion
// update new version
RLMRealmSetSchemaVersion(_realm, newVersion);
}
@finally {
// end transaction
[_realm commitWriteTransaction];
@catch (...) {
[_realm cancelWriteTransaction];
@throw;
}

[_realm commitWriteTransaction];
}

-(RLMObject *)createObject:(NSString *)className withObject:(id)object {
Expand Down
4 changes: 3 additions & 1 deletion Realm/RLMObjectStore.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -29,8 +29,10 @@ void RLMRealmSetSchema(RLMRealm *realm, RLMSchema *targetSchema, bool verifyAndA

// sets a realm's schema to a copy of targetSchema and creates/updates tables
// if update existing is true, updates existing tables, otherwise validates existing tables
// returns whether any changes were actually made
// NOTE: must be called from within write transaction
void RLMRealmCreateTables(RLMRealm *realm, RLMSchema *targetSchema, bool updateExisting = false);
bool RLMRealmCreateTables(RLMRealm *realm, RLMSchema *targetSchema,
NSUInteger schemaVersion, bool updateExisting);

// create or get cached accessors for the given schema
void RLMRealmCreateAccessors(RLMSchema *schema);
Expand Down
54 changes: 33 additions & 21 deletions Realm/RLMObjectStore.mm
Original file line number Diff line number Diff line change
Expand Up @@ -109,22 +109,28 @@ static void RLMCreateColumn(RLMRealm *realm, tightdb::Table &table, RLMProperty
}
}


// Schema used to created generated accessors
static NSMutableArray *s_accessorSchema;

void RLMRealmCreateAccessors(RLMSchema *schema) {
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
s_accessorSchema = [NSMutableArray new];
});
// Schema used to created generated accessors
static NSMutableArray * const s_accessorSchema = [NSMutableArray new];

// create accessors for non-dynamic realms
RLMSchema *matchingSchema = nil;
for (RLMSchema *accessorSchema in s_accessorSchema) {
if ([schema isEqualToSchema:accessorSchema]) {
matchingSchema = accessorSchema;
break;
@synchronized(s_accessorSchema) {
for (RLMSchema *accessorSchema in s_accessorSchema) {
if ([schema isEqualToSchema:accessorSchema]) {
matchingSchema = accessorSchema;
break;
}
}

if (!matchingSchema) {
// create accessors and cache in s_accessorSchema
for (RLMObjectSchema *objectSchema in schema.objectSchema) {
NSString *prefix = [NSString stringWithFormat:@"RLMAccessor_v%lu_",
(unsigned long)s_accessorSchema.count];
objectSchema.accessorClass = RLMAccessorClassForObjectClass(objectSchema.objectClass, objectSchema, prefix);
}
[s_accessorSchema addObject:[schema copy]];
}
}

Expand All @@ -134,14 +140,6 @@ void RLMRealmCreateAccessors(RLMSchema *schema) {
objectSchema.accessorClass = matchingSchema[objectSchema.className].accessorClass;
}
}
else {
// create accessors and cache in s_accessorSchema
for (RLMObjectSchema *objectSchema in schema.objectSchema) {
NSString *prefix = [NSString stringWithFormat:@"RLMAccessor_v%lu_", (unsigned long)s_accessorSchema.count];
objectSchema.accessorClass = RLMAccessorClassForObjectClass(objectSchema.objectClass, objectSchema, prefix);
}
[s_accessorSchema addObject:[schema copy]];
}
}

void RLMRealmSetSchema(RLMRealm *realm, RLMSchema *targetSchema, bool verify) {
Expand All @@ -159,9 +157,16 @@ void RLMRealmSetSchema(RLMRealm *realm, RLMSchema *targetSchema, bool verify) {
}


void RLMRealmCreateTables(RLMRealm *realm, RLMSchema *targetSchema, bool updateExisting) {
bool RLMRealmCreateTables(RLMRealm *realm, RLMSchema *targetSchema,
NSUInteger schemaVersion, bool updateExisting) {
realm.schema = [targetSchema copy];

bool anyChanged = RLMRealmCreateMetadataTables(realm);
if (RLMRealmSchemaVersion(realm) == RLMNotVersioned) {
RLMRealmSetSchemaVersion(realm, schemaVersion);
anyChanged = true;
}

// first pass to create missing tables
NSMutableArray *objectSchemaToUpdate = [NSMutableArray array];
for (RLMObjectSchema *objectSchema in realm.schema.objectSchema) {
Expand All @@ -172,6 +177,7 @@ void RLMRealmCreateTables(RLMRealm *realm, RLMSchema *targetSchema, bool updateE
if (updateExisting || created) {
[objectSchemaToUpdate addObject:objectSchema];
}
anyChanged |= created;
}

// second pass adds/removes columns for objectSchemaToUpdate
Expand All @@ -183,6 +189,7 @@ void RLMRealmCreateTables(RLMRealm *realm, RLMSchema *targetSchema, bool updateE
// add any new properties (new name or different type)
if (!tableSchema[prop.name] || ![prop isEqualToProperty:tableSchema[prop.name]]) {
RLMCreateColumn(realm, *objectSchema->_table, prop);
anyChanged = true;
}
}

Expand All @@ -191,6 +198,7 @@ void RLMRealmCreateTables(RLMRealm *realm, RLMSchema *targetSchema, bool updateE
RLMProperty *prop = tableSchema.properties[i];
if (!objectSchema[prop.name] || ![prop isEqualToProperty:objectSchema[prop.name]]) {
objectSchema->_table->remove_column(prop.column);
anyChanged = true;
}
}

Expand All @@ -199,11 +207,13 @@ void RLMRealmCreateTables(RLMRealm *realm, RLMSchema *targetSchema, bool updateE
// if there is a primary key set, check if it is the same as the old key
if (tableSchema.primaryKeyProperty == nil || ![tableSchema.primaryKeyProperty isEqual:objectSchema.primaryKeyProperty]) {
RLMRealmSetPrimaryKeyForObjectClass(realm, objectSchema.className, objectSchema.primaryKeyProperty.name);
anyChanged = true;
}
}
else if (tableSchema.primaryKeyProperty) {
// there is no primary key, so if thre was one nil out
RLMRealmSetPrimaryKeyForObjectClass(realm, objectSchema.objectClass, nil);
anyChanged = true;
}
}

Expand All @@ -214,6 +224,8 @@ void RLMRealmCreateTables(RLMRealm *realm, RLMSchema *targetSchema, bool updateE
RLMObjectSchema *tableSchema = [RLMObjectSchema schemaFromTableForClassName:objectSchema.className realm:realm];
RLMVerifyAndAlignColumns(tableSchema, objectSchema);
}

return anyChanged;
}


Expand Down
Loading

0 comments on commit 4e5f4a5

Please sign in to comment.