Skip to content

Commit

Permalink
RNET-1154, RNET-1151, RNET-1139: Compact-related fixes (#3618)
Browse files Browse the repository at this point in the history
  • Loading branch information
nirinchev authored Jun 12, 2024
1 parent e61576e commit 61db0ed
Show file tree
Hide file tree
Showing 7 changed files with 126 additions and 119 deletions.
7 changes: 4 additions & 3 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,16 +1,17 @@
## vNext (TBD)

### Enhancements
* None
* Allow `ShouldCompactOnLaunch` to be set on `SyncConfiguration`, not only `RealmConfiguration`. (Issue [#3617](https://github.com/realm/realm-dotnet/issues/3617))

### Fixed
* None
* A `ForCurrentlyOutstandingWork` progress notifier would not immediately call its callback after registration. Instead you would have to wait for some data to be received to get your first update - if you were already caught up when you registered the notifier you could end up waiting a long time for the server to deliver a download that would call/expire your notifier. (Core 14.8.0)
* After compacting, a file upgrade would be triggered. This could cause loss of data if `ShouldDeleteIfMigrationNeeded` is set to `true`. (Issue [#3583](https://github.com/realm/realm-dotnet/issues/3583), Core 14.9.0)

### Compatibility
* Realm Studio: 15.0.0 or later.

### Internal
* Using Core x.y.z.
* Using Core 14.9.0.

## 12.2.0 (2024-05-22)

Expand Down
21 changes: 0 additions & 21 deletions Realm/Realm/Configurations/RealmConfiguration.cs
Original file line number Diff line number Diff line change
Expand Up @@ -49,17 +49,6 @@ public class RealmConfiguration : RealmConfigurationBase
/// </param>
public delegate void MigrationCallbackDelegate(Migration migration, ulong oldSchemaVersion);

/// <summary>
/// A callback, invoked when opening a Realm for the first time during the life
/// of a process to determine if it should be compacted before being returned
/// to the user.
/// </summary>
/// <param name="totalBytes">Total file size (data + free space).</param>
/// <param name="bytesUsed">Total data size.</param>
/// <returns><c>true</c> to indicate that an attempt to compact the file should be made.</returns>
/// <remarks>The compaction will be skipped if another process is accessing it.</remarks>
public delegate bool ShouldCompactDelegate(ulong totalBytes, ulong bytesUsed);

/// <summary>
/// Gets or sets a value indicating whether the database will be deleted if the <see cref="RealmSchema"/>
/// mismatches the one in the code. Use this when debugging and developing your app but never release it with
Expand All @@ -84,15 +73,6 @@ public class RealmConfiguration : RealmConfigurationBase
/// </value>
public MigrationCallbackDelegate? MigrationCallback { get; set; }

/// <summary>
/// Gets or sets the compact on launch callback.
/// </summary>
/// <value>
/// The <see cref="ShouldCompactDelegate"/> that will be invoked when opening a Realm for the first time
/// to determine if it should be compacted before being returned to the user.
/// </value>
public ShouldCompactDelegate? ShouldCompactOnLaunch { get; set; }

/// <summary>
/// Gets or sets the key, used to encrypt the entire Realm. Once set, must be specified each time the file is used.
/// </summary>
Expand Down Expand Up @@ -150,7 +130,6 @@ internal override Configuration CreateNativeConfiguration(Arena arena)
result.delete_if_migration_needed = ShouldDeleteIfMigrationNeeded;
result.read_only = IsReadOnly;
result.invoke_migration_callback = MigrationCallback != null;
result.invoke_should_compact_callback = ShouldCompactOnLaunch != null;
result.automatically_migrate_embedded = true;

return result;
Expand Down
21 changes: 21 additions & 0 deletions Realm/Realm/Configurations/RealmConfigurationBase.cs
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,17 @@ public abstract class RealmConfigurationBase

internal delegate void InitialDataDelegate(Realm realm);

/// <summary>
/// A callback, invoked when opening a Realm for the first time during the life
/// of a process to determine if it should be compacted before being returned
/// to the user.
/// </summary>
/// <param name="totalBytes">Total file size (data + free space).</param>
/// <param name="bytesUsed">Total data size.</param>
/// <returns><c>true</c> to indicate that an attempt to compact the file should be made.</returns>
/// <remarks>The compaction will be skipped if another process is accessing it.</remarks>
public delegate bool ShouldCompactDelegate(ulong totalBytes, ulong bytesUsed);

/// <summary>
/// Gets the filename to be combined with the platform-specific document directory.
/// </summary>
Expand Down Expand Up @@ -69,6 +80,15 @@ public abstract class RealmConfigurationBase
/// <value><c>true</c> if the Realm will be opened in dynamic mode; <c>false</c> otherwise.</value>
public bool IsDynamic { get; set; }

/// <summary>
/// Gets or sets the compact on launch callback.
/// </summary>
/// <value>
/// The <see cref="ShouldCompactDelegate"/> that will be invoked when opening a Realm for the first time
/// to determine if it should be compacted before being returned to the user.
/// </value>
public ShouldCompactDelegate? ShouldCompactOnLaunch { get; set; }

internal bool EnableCache = true;

/// <summary>
Expand Down Expand Up @@ -233,6 +253,7 @@ internal virtual Configuration CreateNativeConfiguration(Arena arena)
invoke_initial_data_callback = PopulateInitialData != null,
managed_config = GCHandle.ToIntPtr(managedConfig),
encryption_key = MarshaledVector<byte>.AllocateFrom(EncryptionKey, arena),
invoke_should_compact_callback = ShouldCompactOnLaunch != null,
};

return config;
Expand Down
9 changes: 5 additions & 4 deletions Realm/Realm/Handles/SharedRealmHandle.cs
Original file line number Diff line number Diff line change
Expand Up @@ -368,7 +368,7 @@ public virtual void AddChild(RealmHandle childHandle)
// if we get !=0 and the real value was in fact 0, then we will just skip and then catch up next time around.
// however, doing things this way will save lots and lots of locks when the list is empty, which it should be if people have
// been using the dispose pattern correctly, or at least have been eager at disposing as soon as they can
// except of course dot notation users that cannot dispose cause they never get a reference in the first place
// except of course dot notation users that cannot dispose because they never get a reference in the first place
lock (_unbindListLock)
{
UnbindLockedList();
Expand Down Expand Up @@ -608,8 +608,7 @@ public void WriteCopy(RealmConfigurationBase config)
public RealmSchema GetSchema()
{
RealmSchema? result = null;
Action<Native.Schema> callback = schema => result = RealmSchema.CreateFromObjectStoreSchema(schema);
var callbackHandle = GCHandle.Alloc(callback);
var callbackHandle = GCHandle.Alloc((Action<Native.Schema>)SchemaCallback);
try
{
NativeMethods.get_schema(this, GCHandle.ToIntPtr(callbackHandle), out var nativeException);
Expand All @@ -621,6 +620,8 @@ public RealmSchema GetSchema()
}

return result!;

void SchemaCallback(Native.Schema schema) => result = RealmSchema.CreateFromObjectStoreSchema(schema);
}

public ObjectHandle CreateObject(TableKey tableKey)
Expand Down Expand Up @@ -868,7 +869,7 @@ private static IntPtr ShouldCompactOnLaunchCallback(IntPtr managedConfigHandle,
try
{
var configHandle = GCHandle.FromIntPtr(managedConfigHandle);
var config = (RealmConfiguration)configHandle.Target!;
var config = (RealmConfigurationBase)configHandle.Target!;

shouldCompact = config.ShouldCompactOnLaunch!.Invoke(totalSize, dataSize);
return IntPtr.Zero;
Expand Down
107 changes: 41 additions & 66 deletions Tests/Realm.Tests/Database/InstanceTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -321,50 +321,6 @@ public void RealmObjectClassesOnlyAllowRealmObjects()
Assert.That(ex.Message, Does.Contain("must descend directly from either RealmObject, EmbeddedObject, or AsymmetricObject"));
}

[TestCase(true)]
[TestCase(false)]
public void ShouldCompact_IsInvokedAfterOpening(bool shouldCompact)
{
var config = (RealmConfiguration)RealmConfiguration.DefaultConfiguration;

using (var realm = GetRealm(config))
{
AddDummyData(realm);
}

var oldSize = new FileInfo(config.DatabasePath).Length;
long projectedNewSize = 0;
var hasPrompted = false;
config.ShouldCompactOnLaunch = (totalBytes, bytesUsed) =>
{
Assert.That(totalBytes, Is.EqualTo(oldSize));
hasPrompted = true;
projectedNewSize = (long)bytesUsed;
return shouldCompact;
};

using (var realm = GetRealm(config))
{
Assert.That(hasPrompted, Is.True);
var newSize = new FileInfo(config.DatabasePath).Length;
if (shouldCompact)
{
// Less than or equal because of the new online compaction mechanism - it's possible
// that the Realm was already at the optimal size.
Assert.That(newSize, Is.LessThanOrEqualTo(oldSize));

// Less than 20% error in projections
Assert.That((newSize - projectedNewSize) / newSize, Is.LessThan(0.2));
}
else
{
Assert.That(newSize, Is.EqualTo(oldSize));
}

Assert.That(realm.All<IntPrimaryKeyWithValueObject>().Count(), Is.EqualTo(DummyDataSize / 2));
}
}

[TestCase(false, true)]
[TestCase(false, false)]
[TestCase(true, true)]
Expand Down Expand Up @@ -454,7 +410,7 @@ public void Compact_WhenResultsAreOpen_ShouldReturnFalse()
{
using var realm = GetRealm();

var token = realm.All<Person>().SubscribeForNotifications((sender, changes) =>
var token = realm.All<Person>().SubscribeForNotifications((_, changes) =>
{
Console.WriteLine(changes?.InsertedIndices);
});
Expand All @@ -463,6 +419,32 @@ public void Compact_WhenResultsAreOpen_ShouldReturnFalse()
token.Dispose();
}

[Test]
public void Compact_WhenShouldDeleteIfMigrationNeeded_PreservesObjects()
{
var config = (RealmConfiguration)RealmConfiguration.DefaultConfiguration;
config.ShouldDeleteIfMigrationNeeded = true;

using (var realm = GetRealm(config))
{
realm.Write(() =>
{
realm.Add(new Person
{
FirstName = "Peter"
});
});
}

Assert.That(Realm.Compact(config), Is.True);

using (var realm = GetRealm(config))
{
Assert.That(realm.All<Person>().Count(), Is.EqualTo(1));
Assert.That(realm.All<Person>().Single().FirstName, Is.EqualTo("Peter"));
}
}

[Test]
public void RealmChangedShouldFireForEveryInstance()
{
Expand All @@ -472,13 +454,13 @@ public void RealmChangedShouldFireForEveryInstance()
using var realm2 = GetRealm();

var changed1 = 0;
realm1.RealmChanged += (sender, e) =>
realm1.RealmChanged += (_, _) =>
{
changed1++;
};

var changed2 = 0;
realm2.RealmChanged += (sender, e) =>
realm2.RealmChanged += (_, _) =>
{
changed2++;
};
Expand Down Expand Up @@ -628,7 +610,7 @@ public void GetInstanceAsync_ExecutesMigrationsInBackground()
var threadId = Environment.CurrentManagedThreadId;
var hasCompletedMigration = false;
config.SchemaVersion = 2;
config.MigrationCallback = (migration, oldSchemaVersion) =>
config.MigrationCallback = (migration, _) =>
{
Assert.That(Environment.CurrentManagedThreadId, Is.Not.EqualTo(threadId));
Task.Delay(300).Wait();
Expand Down Expand Up @@ -914,8 +896,7 @@ public void FrozenRealm_CannotSubscribeForNotifications()
using var realm = GetRealm();
using var frozenRealm = realm.Freeze();

Assert.Throws<RealmFrozenException>(() => frozenRealm.RealmChanged += (_, __) => { });
Assert.Throws<RealmFrozenException>(() => frozenRealm.RealmChanged -= (_, __) => { });
Assert.Throws<RealmFrozenException>(() => frozenRealm.RealmChanged += (_, _) => { });
}

[Test]
Expand Down Expand Up @@ -1046,7 +1027,7 @@ await TestHelpers.EnsureObjectsAreCollected(() =>
using var realm = Realm.GetInstance();
var state = stateAccessor.GetValue(realm)!;

return new object[] { state };
return new[] { state };
});
});
}
Expand Down Expand Up @@ -1093,7 +1074,7 @@ public void GetInstance_WithManualSchema_CanReadAndWrite()
{
Schema = new RealmSchema.Builder
{
new ObjectSchema.Builder("MyType", ObjectSchema.ObjectType.RealmObject)
new ObjectSchema.Builder("MyType")
{
Property.Primitive("IntValue", RealmValueType.Int),
Property.PrimitiveList("ListValue", RealmValueType.Date),
Expand All @@ -1104,7 +1085,7 @@ public void GetInstance_WithManualSchema_CanReadAndWrite()
Property.ObjectSet("ObjectSetValue", "OtherObject"),
Property.ObjectDictionary("ObjectDictionaryValue", "OtherObject"),
},
new ObjectSchema.Builder("OtherObject", ObjectSchema.ObjectType.RealmObject)
new ObjectSchema.Builder("OtherObject")
{
Property.Primitive("Id", RealmValueType.String, isPrimaryKey: true),
Property.Backlinks("MyTypes", "MyType", "ObjectValue")
Expand Down Expand Up @@ -1230,13 +1211,10 @@ public void GetInstance_WithTypedSchemaWithMissingProperties_ThrowsException()

using var realm = GetRealm(config);

var person = realm.Write(() =>
var person = realm.Write(() => realm.Add(new Person
{
return realm.Add(new Person
{
LastName = "Smith"
});
});
LastName = "Smith"
}));

var exGet = Assert.Throws<MissingMemberException>(() => _ = person.FirstName)!;
Assert.That(exGet.Message, Does.Contain(nameof(Person)));
Expand All @@ -1255,13 +1233,10 @@ public void RealmWithFrozenObjects_WhenDeleted_DoesNotThrow()
{
var config = new RealmConfiguration(Guid.NewGuid().ToString());
var realm = GetRealm(config);
var frozenObj = realm.Write(() =>
var frozenObj = realm.Write(() => realm.Add(new IntPropertyObject
{
return realm.Add(new IntPropertyObject
{
Int = 1
}).Freeze();
});
Int = 1
}).Freeze());

frozenObj.Realm!.Dispose();
realm.Dispose();
Expand All @@ -1275,7 +1250,7 @@ public void RealmWithFrozenObjects_WhenDeleted_DoesNotThrow()
public void BeginWrite_CalledMultipleTimes_Throws()
{
using var realm = GetRealm();
var ts = realm.BeginWrite();
using var ts = realm.BeginWrite();

Assert.That(() => realm.BeginWrite(), Throws.TypeOf<RealmInvalidTransactionException>());
}
Expand Down
Loading

0 comments on commit 61db0ed

Please sign in to comment.