Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

RNET-1154, RNET-1151, RNET-1139: Compact-related fixes #3618

Merged
merged 1 commit into from
Jun 12, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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);
Comment on lines -52 to -61
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

these are moved to RealmConfigurationBase so they become available to sync configs.


/// <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)
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is moved to SynchronizedInstanceTests.

{
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()
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is the test for #3583 and #3612

{
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
Loading