diff --git a/CHANGELOG.md b/CHANGELOG.md index bab12689d9..7bd02b6056 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,7 +9,9 @@ vNEXT (TBD) * Exposed an API - `SyncConfigurationBase.EnableSessionMultiplexing()` that allows toggling session multiplexing on the sync client. (PR [1896](https://github.com/realm/realm-dotnet/pull/1896)) * Added support for faster initial downloads when using `Realm.GetInstanceAsync`. (Issue [1847](https://github.com/realm/realm-dotnet/issues/1847)) * Added an optional `cancellationToken` argument to `Realm.GetInstanceAsync` enabling clean cancelation of the in-progress download. (PR [1859](https://github.com/realm/realm-dotnet/pull/1859)) - +* Added support for Client Resync which automatically will recover the local Realm in case the server is rolled back. This largely replaces the Client Reset mechanism for fully synchronized Realms. Can be configured using `FullSyncConfiguration.ClientResyncMode`. (PR [#1901](https://github.com/realm/realm-dotnet/pull/1901)) +* Made the `createUser` argument in `Credentials.UsernamePassword` optional. If not specified, the user will be created or logged in if they already exist. (PR [#1901](https://github.com/realm/realm-dotnet/pull/1901)) + ### Fixed * Fixed an infinite recursion when calling `RealmCollectionBase.IndexOf`. (Issue [#1892](https://github.com/realm/realm-dotnet/issues/1892)) diff --git a/Realm/Realm/Configurations/FullSyncConfiguration.cs b/Realm/Realm/Configurations/FullSyncConfiguration.cs index 15bae3848f..ca0be1ba06 100644 --- a/Realm/Realm/Configurations/FullSyncConfiguration.cs +++ b/Realm/Realm/Configurations/FullSyncConfiguration.cs @@ -33,6 +33,13 @@ public class FullSyncConfiguration : SyncConfigurationBase { internal override bool IsFullSync => true; + internal override ClientResyncMode ResyncMode => ClientResyncMode; + + /// + /// Gets or sets a value controlling the behavior in case of a Client Resync. Default is + /// + public ClientResyncMode ClientResyncMode { get; set; } = ClientResyncMode.RecoverLocalRealm; + /// /// Initializes a new instance of the class. /// diff --git a/Realm/Realm/Configurations/QueryBasedSyncConfiguration.cs b/Realm/Realm/Configurations/QueryBasedSyncConfiguration.cs index 028287ba25..c277b89fe9 100644 --- a/Realm/Realm/Configurations/QueryBasedSyncConfiguration.cs +++ b/Realm/Realm/Configurations/QueryBasedSyncConfiguration.cs @@ -37,6 +37,8 @@ public class QueryBasedSyncConfiguration : SyncConfigurationBase { internal override bool IsFullSync => false; + internal override ClientResyncMode ResyncMode => ClientResyncMode.Manual; + internal static readonly Type[] _queryBasedPermissionTypes = { typeof(ClassPermission), diff --git a/Realm/Realm/Configurations/SyncConfigurationBase.cs b/Realm/Realm/Configurations/SyncConfigurationBase.cs index bcfb7079dd..046bc6c9cf 100644 --- a/Realm/Realm/Configurations/SyncConfigurationBase.cs +++ b/Realm/Realm/Configurations/SyncConfigurationBase.cs @@ -90,6 +90,8 @@ public abstract class SyncConfigurationBase : RealmConfigurationBase /// public Action OnProgress { get; set; } + internal abstract ClientResyncMode ResyncMode { get; } + /// /// Gets or sets a value indicating how detailed the sync client's logs will be. /// @@ -302,7 +304,8 @@ internal Native.SyncConfiguration ToNative() client_validate_ssl = EnableSSLValidation, TrustedCAPath = TrustedCAPath, is_partial = !IsFullSync, - PartialSyncIdentifier = null + PartialSyncIdentifier = null, + client_resync_mode = ResyncMode, }; } diff --git a/Realm/Realm/Native/SyncConfiguration.cs b/Realm/Realm/Native/SyncConfiguration.cs index 2ae9955f4f..598030c257 100644 --- a/Realm/Realm/Native/SyncConfiguration.cs +++ b/Realm/Realm/Native/SyncConfiguration.cs @@ -38,6 +38,7 @@ internal SyncUserHandle SyncUserHandle [MarshalAs(UnmanagedType.LPWStr)] private string url; + private IntPtr url_len; internal string Url @@ -54,6 +55,7 @@ internal string Url [MarshalAs(UnmanagedType.LPWStr)] private string trusted_ca_path; + private IntPtr trusted_ca_path_len; internal string TrustedCAPath @@ -70,6 +72,7 @@ internal string TrustedCAPath [MarshalAs(UnmanagedType.LPWStr)] private string partial_sync_identifier; + private IntPtr partial_sync_identifier_len; internal string PartialSyncIdentifier @@ -80,5 +83,8 @@ internal string PartialSyncIdentifier partial_sync_identifier_len = (IntPtr)(value?.Length ?? 0); } } + + [MarshalAs(UnmanagedType.U1)] + internal ClientResyncMode client_resync_mode; } } diff --git a/Realm/Realm/Sync/ClientResyncMode.cs b/Realm/Realm/Sync/ClientResyncMode.cs new file mode 100644 index 0000000000..3f23442b56 --- /dev/null +++ b/Realm/Realm/Sync/ClientResyncMode.cs @@ -0,0 +1,45 @@ +using Realms.Sync.Exceptions; + +namespace Realms.Sync +{ + /// + /// Enum describing what should happen in case of a Client Resync. + /// + /// + /// A Client Resync is triggered if the device and server cannot agree on a common shared history + /// for the Realm file, thus making it impossible for the device to upload or receive any changes. + /// This can happen if the server is rolled back or restored from backup. + ///
+ /// IMPORTANT: Just having the device offline will not trigger a Client Resync. + ///
+ public enum ClientResyncMode : byte + { + /// + /// Realm will compare the local Realm with the Realm on the server and automatically transfer + /// any changes from the local Realm that makes sense to the Realm provided by the server. + ///
+ /// This is the default mode for fully synchronized Realms. It is not yet supported by + /// Query-based Realms. + ///
+ RecoverLocalRealm = 0, + + /// + /// The local Realm will be discarded and replaced with the server side Realm. + /// All local changes will be lost. + ///
+ /// This mode is not yet supported by Query-based Realms. + ///
+ DiscardLocalRealm = 1, + + /// + /// A manual Client Resync is also known as a Client Reset. + ///
+ /// A will be sent to , + /// triggering a Client Reset. Doing this provides a handle to both the old and new Realm file, enabling + /// full control over which changes to move, if any. + ///
+ /// This is the only supported mode for Query-based Realms. + ///
+ Manual = 2, + } +} diff --git a/Realm/Realm/Sync/Credentials.cs b/Realm/Realm/Sync/Credentials.cs index 33a6c218db..84efc27123 100644 --- a/Realm/Realm/Sync/Credentials.cs +++ b/Realm/Realm/Sync/Credentials.cs @@ -121,13 +121,19 @@ public static Credentials Google(string googleToken) /// The user's password. /// true if the user should be created, false otherwise. It is not possible to create a user twice when logging in, so this flag should only be set to true the first time a user logs in. /// An instance of that can be used in - public static Credentials UsernamePassword(string username, string password, bool createUser) + public static Credentials UsernamePassword(string username, string password, bool? createUser = null) { + var userInfo = new Dictionary { [Keys.Password] = password }; + if (createUser != null) + { + userInfo[Keys.CreateUser] = createUser; + } + return new Credentials { IdentityProvider = Provider.UsernamePassword, Token = username, - UserInfo = new Dictionary { [Keys.CreateUser] = createUser, [Keys.Password] = password } + UserInfo = userInfo, }; } diff --git a/Tests/Realm.Tests/Sync/SyncTestBase.cs b/Tests/Realm.Tests/Sync/SyncTestBase.cs index b01ac958a1..ac3bd7ce2a 100644 --- a/Tests/Realm.Tests/Sync/SyncTestBase.cs +++ b/Tests/Realm.Tests/Sync/SyncTestBase.cs @@ -100,6 +100,16 @@ protected Session GetSession(Realm realm) return result; } + /// + /// Waits for upload and immediately disposes of the managed handle to ensure the Realm can be safely deleted. + /// + protected async Task WaitForUploadAsync(Realm realm) + { + var session = realm.GetSession(); + await session.WaitForUploadAsync(); + session.CloseHandle(); + } + protected Realm GetRealm(RealmConfigurationBase config) { var result = Realm.GetInstance(config); diff --git a/Tests/Realm.Tests/Sync/SynchronizedInstanceTests.cs b/Tests/Realm.Tests/Sync/SynchronizedInstanceTests.cs index e575ea15cb..46a8d0c149 100644 --- a/Tests/Realm.Tests/Sync/SynchronizedInstanceTests.cs +++ b/Tests/Realm.Tests/Sync/SynchronizedInstanceTests.cs @@ -17,6 +17,7 @@ //////////////////////////////////////////////////////////////////////////// using System; +using System.Diagnostics; using System.IO; using System.Linq; using System.Threading; @@ -287,6 +288,172 @@ public void GetInstance_WhenDynamicAndDoesntExist_ReturnsEmptySchema() }); } + // Used by TestClientResync and TestClientResync2. Must be either RecoverLocal or DiscardLocal. Manual is tested + // by TestManualClientResync. + private ClientResyncMode _clientResyncMode = ClientResyncMode.DiscardLocalRealm; + + private async Task GetClientResyncConfig(ClientResyncMode? _mode = null) + { + if (!_mode.HasValue) + { + _mode = _clientResyncMode; + } + + var user = await User.LoginAsync(Credentials.UsernamePassword("foo", "bar"), SyncTestHelpers.AuthServerUri); + return new FullSyncConfiguration(SyncTestHelpers.RealmUri($"~/{_mode.Value}"), user, $"{_mode}.realm") + { + ClientResyncMode = _mode.Value, + ObjectClasses = new[] { typeof(IntPrimaryKeyWithValueObject) }, + }; + } + + [Test, NUnit.Framework.Explicit("Requires debugger and a lot of manual steps")] + public void TestClientResync() + { + SyncTestHelpers.RunRosTestAsync(async () => + { + var config = await GetClientResyncConfig(); + // Let's delete anything local. + Realm.DeleteRealm(config); + Exception ex = null; + Session.Error += (s, e) => + { + if (e.Exception.Message != "End of input") + { + Debugger.Break(); + ex = e.Exception; + } + }; + + using (var realm = await Realm.GetInstanceAsync(config)) + { + realm.Write(() => + { + realm.Add(new IntPrimaryKeyWithValueObject + { + Id = 1, + StringValue = "1" + }); + }); + + await WaitForUploadAsync(realm); + } + + // Stop ROS and backup the file. Then restart + Debugger.Break(); + + using (var realm = await GetRealmAsync(config)) + { + realm.Write(() => + { + realm.Add(new IntPrimaryKeyWithValueObject + { + Id = 2, + StringValue = "2" + }); + }); + + await WaitForUploadAsync(realm); + + // Stop ROS + Debugger.Break(); + + realm.Write(() => + { + realm.Add(new IntPrimaryKeyWithValueObject + { + Id = 3, + StringValue = "3" + }); + }); + } + + // Replace the file from backup. Restart ROS and run TestClientResync2 + Debugger.Break(); + + Assert.That(ex, Is.Null); + }, (int)TimeSpan.FromMinutes(10).TotalMilliseconds); + } + + [Test, NUnit.Framework.Explicit("Requires debugger and a lot of manual steps")] + public void TestClientResync2() + { + Assert.That(new[] { ClientResyncMode.DiscardLocalRealm, ClientResyncMode.RecoverLocalRealm }, Does.Contain(_clientResyncMode)); + + SyncTestHelpers.RunRosTestAsync(async () => + { + var config = await GetClientResyncConfig(); + + Exception ex = null; + Session.Error += (s, e) => + { + if (e.Exception.Message != "End of input") + { + ex = e.Exception; + } + }; + + using (var realm = await GetRealmAsync(config)) + { + var values = realm.All().AsEnumerable().Select(i => i.StringValue).ToArray(); + + // Verify expected result: + // - RecoverLocalRealm: we have 2 objects - "1" and "3". The "2" is lost because the client had uploaded them to the server already. + // - DiscardLocalRealm: we have 1 object - "1". The "2" is lost because we restored from backup and the "3" is discarded. + switch (_clientResyncMode) + { + case ClientResyncMode.DiscardLocalRealm: + Assert.That(values.Length, Is.EqualTo(1)); + Assert.That(values[0], Is.EqualTo("1")); + Assert.That(ex, Is.Null); + break; + + case ClientResyncMode.RecoverLocalRealm: + Assert.That(values.Length, Is.EqualTo(2)); + CollectionAssert.AreEquivalent(values, new[] { "1", "3" }); + Assert.That(ex, Is.Null); + break; + } + } + }, (int)TimeSpan.FromMinutes(10).TotalMilliseconds); + } + + [Test, NUnit.Framework.Explicit("Requires debugger and a lot of manual steps")] + public void TestManualClientResync() + { + SyncTestHelpers.RunRosTestAsync(async () => + { + var config = await GetClientResyncConfig(ClientResyncMode.Manual); + + Realm.DeleteRealm(config); + using (var realm = await Realm.GetInstanceAsync(config)) + { + realm.Write(() => + { + realm.Add(new IntPrimaryKeyWithValueObject()); + }); + + await WaitForUploadAsync(realm); + } + + // Delete Realm in ROS + Debugger.Break(); + + Exception ex = null; + Session.Error += (s, e) => + { + ex = e.Exception; + }; + + using (var realm = Realm.GetInstance(config)) + { + await Task.Delay(100); + } + + Assert.That(ex, Is.InstanceOf()); + }); + } + private static void AddDummyData(Realm realm, bool singleTransaction) { Action write; diff --git a/wrappers/dependencies.list b/wrappers/dependencies.list index b22d5cdbc6..de4e9f1603 100644 --- a/wrappers/dependencies.list +++ b/wrappers/dependencies.list @@ -1,3 +1,3 @@ -REALM_CORE_VERSION=5.23.1 -REALM_SYNC_VERSION=4.7.1 +REALM_CORE_VERSION=5.23.3 +REALM_SYNC_VERSION=4.7.5 ANDROID_OPENSSL_VERSION=1.0.2k diff --git a/wrappers/src/object-store b/wrappers/src/object-store index cf9788e176..3b9683dd01 160000 --- a/wrappers/src/object-store +++ b/wrappers/src/object-store @@ -1 +1 @@ -Subproject commit cf9788e17606d1bb152516a8e8291ae337f5d20c +Subproject commit 3b9683dd01b88ce8c22b68da397a44a2d70fe938 diff --git a/wrappers/src/sync_manager_cs.cpp b/wrappers/src/sync_manager_cs.cpp index 5f68c4a23d..18eb876f29 100644 --- a/wrappers/src/sync_manager_cs.cpp +++ b/wrappers/src/sync_manager_cs.cpp @@ -97,6 +97,7 @@ Realm::Config get_shared_realm_config(Configuration configuration, SyncConfigura config.sync_config = std::make_shared(*sync_configuration.user, realm_url); config.sync_config->bind_session_handler = bind_session; config.sync_config->error_handler = handle_session_error; + config.sync_config->client_resync_mode = sync_configuration.client_resync_mode; config.path = Utf16StringAccessor(configuration.path, configuration.path_len); // by definition the key is only allowed to be 64 bytes long, enforced by C# code diff --git a/wrappers/src/sync_manager_cs.hpp b/wrappers/src/sync_manager_cs.hpp index 73bec922a9..b814c18051 100644 --- a/wrappers/src/sync_manager_cs.hpp +++ b/wrappers/src/sync_manager_cs.hpp @@ -19,6 +19,9 @@ #pragma once #include "realm_export_decls.hpp" +#include "sync/sync_config.hpp" + +using namespace realm; namespace realm { class SyncUser; @@ -38,9 +41,6 @@ namespace realm { bool is_partial; uint16_t* partial_sync_identifier; size_t partial_sync_identifier_len; + realm::ClientResyncMode client_resync_mode; }; - - namespace binding { - REALM_EXPORT bool has_feature(StringData feature); - } }