Fix an issue where `Realm.open` would complain about the Realm already
being open with a different schema version In order to correctly open read-only synchronized Realms, `Realm.open` would open the Realm without specifying a schema or schema version, wait for any remote changes to be downloaded (if appropriate), and then re-open the Realm with the specified schema and schema version. This would lead to an exception about the Realm being open with a different schema version if the Realm had previously been opened with a different schema version, due to the way `RealmCoordinator` caches information about the schema of open Realms. We address this by making two changes: 1. `Realm.open` for non-synchronized Realms no longer goes through `_waitForDownload`. This means the dance described above where the Realm is opened twice is not used for local Realms. 2. `_waitForDownload` no longer keeps the `Realm` alive until after its callback has returned. It instead keeps the `SyncSession` alive. This is sufficient to avoid the connection being torn down and having to reconnect when `_waitForDownload`'s callback later opens the Realm with the correct schema and schema version, while also allowing for `RealmCoordinator`'s cached information to be cleared when the schemaless Realm is closed prior to the Realm being reopened. In addition, tests have been added that reproduced the problem in both a local and sync context.
This commit is contained in:
parent
9505b750f9
commit
c2de29ab0c
|
@ -8,6 +8,7 @@
|
|||
|
||||
### Bug fixes
|
||||
* [Object Server] Fixed a bug where long reconnection happens when a proxy in front of the sync worker returns one of those.
|
||||
* Fix a bug where `Realm.open` could unexpectedly raise an "Realm at path ... already opened with different schema version" error.
|
||||
|
||||
### Internal
|
||||
* Updated to Realm Sync 2.1.10 (see "Bug fixes").
|
||||
|
|
|
@ -62,14 +62,26 @@ module.exports = function(realmConstructor) {
|
|||
//Add async open API
|
||||
Object.defineProperties(realmConstructor, getOwnPropertyDescriptors({
|
||||
open(config) {
|
||||
// For local Realms we open the Realm and return it in a resolved Promise.
|
||||
if (!("sync" in config)) {
|
||||
let promise = Promise.resolve(new realmConstructor(config));
|
||||
promise.progress = (callback) => { };
|
||||
return promise;
|
||||
}
|
||||
|
||||
// For synced Realms we open the Realm without specifying the schema and then wait until
|
||||
// the Realm has finished its initial sync with the server. We then reopen it with the correct
|
||||
// schema. This avoids writing the schema to a potentially read-only Realm file, which would
|
||||
// result in sync rejecting the writes. `_waitForDownload` ensures that the session is kept
|
||||
// alive until our callback has returned, which prevents it from being torn down and recreated
|
||||
// when we close the schemaless Realm and open it with the correct schema.
|
||||
let syncSession;
|
||||
let promise = new Promise((resolve, reject) => {
|
||||
let realm = new realmConstructor(waitForDownloadConfig(config));
|
||||
realm._waitForDownload(
|
||||
(session) => {
|
||||
syncSession = session;
|
||||
},
|
||||
(session) => { syncSession = session; },
|
||||
(error) => {
|
||||
realm.close();
|
||||
if (error) {
|
||||
setTimeout(() => { reject(error); }, 1);
|
||||
}
|
||||
|
@ -91,7 +103,6 @@ module.exports = function(realmConstructor) {
|
|||
|
||||
return promise;
|
||||
};
|
||||
|
||||
return promise;
|
||||
},
|
||||
|
||||
|
|
|
@ -245,9 +245,9 @@ public:
|
|||
{"close", wrap<close>},
|
||||
{"compact", wrap<compact>},
|
||||
{"deleteModel", wrap<delete_model>},
|
||||
{"_waitForDownload", wrap<wait_for_download_completion>},
|
||||
{"_objectForObjectId", wrap<object_for_object_id>},
|
||||
#if REALM_ENABLE_SYNC
|
||||
{"_waitForDownload", wrap<wait_for_download_completion>},
|
||||
{"_subscribeToObjects", wrap<subscribe_to_objects>},
|
||||
#endif
|
||||
};
|
||||
|
@ -720,6 +720,7 @@ void RealmClass<T>::get_sync_session(ContextType ctx, ObjectType object, ReturnV
|
|||
}
|
||||
#endif
|
||||
|
||||
#if REALM_ENABLE_SYNC
|
||||
template<typename T>
|
||||
void RealmClass<T>::wait_for_download_completion(ContextType ctx, ObjectType this_object, Arguments args, ReturnValue &return_value) {
|
||||
args.validate_maximum(2);
|
||||
|
@ -730,13 +731,27 @@ void RealmClass<T>::wait_for_download_completion(ContextType ctx, ObjectType thi
|
|||
session_callback = Value::validated_to_function(ctx, args[0]);
|
||||
}
|
||||
|
||||
#if REALM_ENABLE_SYNC
|
||||
auto realm = *get_internal<T, RealmClass<T>>(this_object);
|
||||
if (auto* sync_config = realm->config().sync_config.get()) {
|
||||
auto* sync_config = realm->config().sync_config.get();
|
||||
if (!sync_config) {
|
||||
throw std::logic_error("_waitForDownload can only be used on a synchronized Realm.");
|
||||
}
|
||||
|
||||
Protected<FunctionType> protected_callback(ctx, callback_function);
|
||||
Protected<ObjectType> protected_this(ctx, this_object);
|
||||
Protected<typename T::GlobalContext> protected_ctx(Context<T>::get_global_context(ctx));
|
||||
|
||||
std::shared_ptr<SyncUser> user = sync_config->user;
|
||||
if (user && user->state() != SyncUser::State::Error) {
|
||||
if (auto session = user->session_for_on_disk_path(realm->config().path)) {
|
||||
if (!Value::is_null(ctx, session_callback)) {
|
||||
FunctionType session_callback_func = Value::to_function(ctx, session_callback);
|
||||
auto syncSession = create_object<T, SessionClass<T>>(ctx, new WeakSession(session));
|
||||
ValueType callback_arguments[1];
|
||||
callback_arguments[0] = syncSession;
|
||||
Function<T>::callback(protected_ctx, session_callback_func, typename T::Object(), 1, callback_arguments);
|
||||
}
|
||||
|
||||
EventLoopDispatcher<WaitHandler> wait_handler([=](std::error_code error_code) {
|
||||
HANDLESCOPE
|
||||
if (!error_code) {
|
||||
|
@ -754,24 +769,10 @@ void RealmClass<T>::wait_for_download_completion(ContextType ctx, ObjectType thi
|
|||
|
||||
Function<T>::callback(protected_ctx, protected_callback, typename T::Object(), 1, callback_arguments);
|
||||
}
|
||||
|
||||
// We keep our Realm instance alive until the callback has had a chance to open its own instance.
|
||||
// This allows it to share the sync session that our Realm opened.
|
||||
if (realm)
|
||||
realm->close();
|
||||
// Ensure that the session remains alive until the callback has had an opportunity to reopen the Realm
|
||||
// with the appropriate schema.
|
||||
(void)session;
|
||||
});
|
||||
|
||||
std::shared_ptr<SyncUser> user = sync_config->user;
|
||||
if (user && user->state() != SyncUser::State::Error) {
|
||||
if (auto session = user->session_for_on_disk_path(realm->config().path)) {
|
||||
if (!Value::is_null(ctx, session_callback)) {
|
||||
FunctionType session_callback_func = Value::to_function(ctx, session_callback);
|
||||
auto syncSession = create_object<T, SessionClass<T>>(ctx, new WeakSession(session));
|
||||
ValueType callback_arguments[1];
|
||||
callback_arguments[0] = syncSession;
|
||||
Function<T>::callback(protected_ctx, session_callback_func, typename T::Object(), 1, callback_arguments);
|
||||
}
|
||||
|
||||
session->wait_for_download_completion(std::move(wait_handler));
|
||||
return;
|
||||
}
|
||||
|
@ -785,12 +786,8 @@ void RealmClass<T>::wait_for_download_completion(ContextType ctx, ObjectType thi
|
|||
ValueType callback_arguments[1];
|
||||
callback_arguments[0] = object;
|
||||
Function<T>::callback(protected_ctx, protected_callback, protected_this, 1, callback_arguments);
|
||||
return;
|
||||
}
|
||||
#endif
|
||||
|
||||
Function<T>::callback(ctx, callback_function, this_object, 0, nullptr);
|
||||
}
|
||||
#endif
|
||||
|
||||
template<typename T>
|
||||
void RealmClass<T>::objects(ContextType ctx, ObjectType this_object, Arguments args, ReturnValue &return_value) {
|
||||
|
|
|
@ -207,6 +207,21 @@ module.exports = {
|
|||
TestCase.assertEqual(realm.readOnly, true);
|
||||
},
|
||||
|
||||
testRealmOpen: function() {
|
||||
let realm = new Realm({schema: [schemas.TestObject], schemaVersion: 1});
|
||||
realm.write(() => {
|
||||
realm.create('TestObject', [1])
|
||||
});
|
||||
realm.close();
|
||||
|
||||
return Realm.open({schema: [schemas.TestObject], schemaVersion: 2}).then(realm => {
|
||||
const objects = realm.objects('TestObject');
|
||||
TestCase.assertEqual(objects.length, 1);
|
||||
TestCase.assertEqual(objects[0].doubleCol, 1.0);
|
||||
realm.close();
|
||||
});
|
||||
},
|
||||
|
||||
testDefaultPath: function() {
|
||||
const defaultPath = Realm.defaultPath;
|
||||
let defaultRealm = new Realm({schema: []});
|
||||
|
|
|
@ -169,6 +169,50 @@ module.exports = {
|
|||
});
|
||||
},
|
||||
|
||||
testRealmOpenWithExistingLocalRealm() {
|
||||
if (!isNodeProccess) {
|
||||
return;
|
||||
}
|
||||
|
||||
const username = uuid();
|
||||
const realmName = uuid();
|
||||
const expectedObjectsCount = 3;
|
||||
|
||||
let user, config;
|
||||
return runOutOfProcess(__dirname + '/download-api-helper.js', username, realmName, REALM_MODULE_PATH)
|
||||
.then(() => Realm.Sync.User.login('http://localhost:9080', username, 'password'))
|
||||
.then(u => {
|
||||
user = u;
|
||||
const accessTokenRefreshed = this;
|
||||
let successCounter = 0;
|
||||
|
||||
config = {
|
||||
sync: { user, url: `realm://localhost:9080/~/${realmName}` },
|
||||
schema: [{ name: 'Dog', properties: { name: 'string' } }],
|
||||
schemaVersion: 1,
|
||||
};
|
||||
|
||||
// Open the Realm with a schema version of 1, then immediately close it.
|
||||
// This verifies that Realm.open doesn't hit issues when the schema version
|
||||
// of an existing, local Realm is different than the one passed in the configuration.
|
||||
let realm = new Realm(config);
|
||||
realm.close();
|
||||
|
||||
config.schemaVersion = 2;
|
||||
return Realm.open(config)
|
||||
}).then(realm => {
|
||||
let actualObjectsCount = realm.objects('Dog').length;
|
||||
TestCase.assertEqual(actualObjectsCount, expectedObjectsCount, "Synced realm does not contain the expected objects count");
|
||||
|
||||
const session = realm.syncSession;
|
||||
TestCase.assertInstanceOf(session, Realm.Sync.Session);
|
||||
TestCase.assertEqual(session.user.identity, user.identity);
|
||||
TestCase.assertEqual(session.config.url, config.sync.url);
|
||||
TestCase.assertEqual(session.config.user.identity, config.sync.user.identity);
|
||||
TestCase.assertEqual(session.state, 'active');
|
||||
});
|
||||
},
|
||||
|
||||
testRealmOpenAsync() {
|
||||
if (!isNodeProccess) {
|
||||
return;
|
||||
|
|
Loading…
Reference in New Issue