Skip to content

Commit 6114e61

Browse files
Upload data and migrate schema (#6944)
* Main work of sync schema migrations * misc * Code review changes
1 parent 690d78a commit 6114e61

15 files changed

+183
-25
lines changed

src/realm/error_codes.cpp

+1
Original file line numberDiff line numberDiff line change
@@ -61,6 +61,7 @@ ErrorCategory ErrorCodes::error_categories(Error code)
6161
case SyncServerPermissionsChanged:
6262
case SyncUserMismatch:
6363
case SyncWriteNotAllowed:
64+
case SyncSchemaMigrationError:
6465
return ErrorCategory().set(ErrorCategory::runtime_error).set(ErrorCategory::sync_error);
6566

6667
case SyncConnectFailed:

src/realm/error_codes.h

+3
Original file line numberDiff line numberDiff line change
@@ -83,6 +83,7 @@ typedef enum realm_errno {
8383
RLM_ERR_TLS_HANDSHAKE_FAILED = 1042,
8484
RLM_ERR_WRONG_SYNC_TYPE = 1043,
8585
RLM_ERR_SYNC_WRITE_NOT_ALLOWED = 1044,
86+
RLM_ERR_SYNC_SCHEMA_MIGRATION_ERROR = 1045,
8687

8788
RLM_ERR_SYSTEM_ERROR = 1999,
8889

@@ -262,6 +263,8 @@ typedef enum realm_sync_errno_session {
262263
RLM_SYNC_ERR_SESSION_MIGRATE_TO_FLX = 232,
263264
RLM_SYNC_ERR_SESSION_BAD_PROGRESS = 233,
264265
RLM_SYNC_ERR_SESSION_REVERT_TO_PBS = 234,
266+
RLM_SYNC_ERR_SESSION_BAD_SCHEMA_VERSION = 235,
267+
RLM_SYNC_ERR_SESSION_SCHEMA_VERSION_CHANGED = 236,
265268
// Error code 299 is reserved as an "unknown session error" in tests
266269
} realm_sync_errno_session_e;
267270

src/realm/error_codes.hpp

+1
Original file line numberDiff line numberDiff line change
@@ -126,6 +126,7 @@ class ErrorCodes {
126126
TlsHandshakeFailed = RLM_ERR_TLS_HANDSHAKE_FAILED,
127127
WrongSyncType = RLM_ERR_WRONG_SYNC_TYPE,
128128
SyncWriteNotAllowed = RLM_ERR_SYNC_WRITE_NOT_ALLOWED,
129+
SyncSchemaMigrationError = RLM_ERR_SYNC_SCHEMA_MIGRATION_ERROR,
129130

130131
SystemError = RLM_ERR_SYSTEM_ERROR,
131132

src/realm/object-store/impl/realm_coordinator.cpp

+7
Original file line numberDiff line numberDiff line change
@@ -563,6 +563,13 @@ void RealmCoordinator::delete_and_reopen()
563563
util::CheckedLockGuard lock(m_realm_mutex);
564564
close();
565565
util::File::remove(m_config.path);
566+
#if REALM_ENABLE_SYNC
567+
// Close the sync session.
568+
if (m_sync_session) {
569+
m_sync_session->force_close();
570+
m_sync_session = nullptr;
571+
}
572+
#endif
566573
open_db();
567574
}
568575

src/realm/object-store/shared_realm.cpp

+13-4
Original file line numberDiff line numberDiff line change
@@ -290,8 +290,7 @@ bool Realm::schema_change_needs_write_transaction(Schema& schema, std::vector<Sc
290290

291291
switch (m_config.schema_mode) {
292292
case SchemaMode::Automatic:
293-
if (version < m_schema_version && m_schema_version != ObjectStore::NotVersioned)
294-
throw InvalidSchemaVersionException(m_schema_version, version, false);
293+
verify_schema_version_not_decreasing(version);
295294
return true;
296295

297296
case SchemaMode::Immutable:
@@ -321,8 +320,7 @@ bool Realm::schema_change_needs_write_transaction(Schema& schema, std::vector<Sc
321320
}
322321

323322
case SchemaMode::Manual:
324-
if (version < m_schema_version && m_schema_version != ObjectStore::NotVersioned)
325-
throw InvalidSchemaVersionException(m_schema_version, version, false);
323+
verify_schema_version_not_decreasing(version);
326324
if (version == m_schema_version) {
327325
ObjectStore::verify_no_changes_required(changes);
328326
REALM_UNREACHABLE(); // changes is non-empty so above line always throws
@@ -332,6 +330,17 @@ bool Realm::schema_change_needs_write_transaction(Schema& schema, std::vector<Sc
332330
REALM_COMPILER_HINT_UNREACHABLE();
333331
}
334332

333+
// Schema version is not allowed to decrease for local realms.
334+
void Realm::verify_schema_version_not_decreasing(uint64_t version)
335+
{
336+
#if REALM_ENABLE_SYNC
337+
if (m_config.sync_config)
338+
return;
339+
#endif
340+
if (version < m_schema_version && m_schema_version != ObjectStore::NotVersioned)
341+
throw InvalidSchemaVersionException(m_schema_version, version, false);
342+
}
343+
335344
Schema Realm::get_full_schema()
336345
{
337346
if (!m_config.immutable())

src/realm/object-store/shared_realm.hpp

+2-1
Original file line numberDiff line numberDiff line change
@@ -365,7 +365,7 @@ class Realm : public std::enable_shared_from_this<Realm> {
365365
* will be thrown instead.
366366
*
367367
* If the destination file does not exist, the action performed depends on
368-
* the type of the source and destimation files. If the destination
368+
* the type of the source and destination files. If the destination
369369
* configuration is a non-sync local Realm configuration, a compacted copy
370370
* of the current Transaction's data (which includes uncommitted changes if
371371
* applicable!) is written in streaming form, with no history.
@@ -550,6 +550,7 @@ class Realm : public std::enable_shared_from_this<Realm> {
550550
void set_schema(Schema const& reference, Schema schema);
551551
bool reset_file(Schema& schema, std::vector<SchemaChange>& changes_required);
552552
bool schema_change_needs_write_transaction(Schema& schema, std::vector<SchemaChange>& changes, uint64_t version);
553+
void verify_schema_version_not_decreasing(uint64_t version);
553554
Schema get_full_schema();
554555

555556
// Ensure that m_schema and m_schema_version match that of the current

src/realm/object-store/sync/async_open_task.cpp

+95-16
Original file line numberDiff line numberDiff line change
@@ -34,7 +34,7 @@ AsyncOpenTask::AsyncOpenTask(std::shared_ptr<_impl::RealmCoordinator> coordinato
3434
{
3535
}
3636

37-
void AsyncOpenTask::start(AsyncOpenCallback async_open_complete)
37+
void AsyncOpenTask::start(AsyncOpenCallback callback)
3838
{
3939
util::CheckedUniqueLock lock(m_mutex);
4040
if (!m_session)
@@ -43,8 +43,7 @@ void AsyncOpenTask::start(AsyncOpenCallback async_open_complete)
4343
lock.unlock();
4444

4545
std::shared_ptr<AsyncOpenTask> self(shared_from_this());
46-
session->wait_for_download_completion([async_open_complete = std::move(async_open_complete), self,
47-
this](Status status) mutable {
46+
session->wait_for_download_completion([callback = std::move(callback), self, this](Status status) mutable {
4847
std::shared_ptr<_impl::RealmCoordinator> coordinator;
4948
{
5049
util::CheckedLockGuard lock(m_mutex);
@@ -56,18 +55,19 @@ void AsyncOpenTask::start(AsyncOpenCallback async_open_complete)
5655
}
5756

5857
if (!status.is_ok()) {
59-
self->async_open_complete(std::move(async_open_complete), coordinator, status);
58+
self->async_open_complete(std::move(callback), coordinator, status);
6059
return;
6160
}
6261

63-
auto config = coordinator->get_config();
64-
if (config.sync_config && config.sync_config->flx_sync_requested &&
65-
config.sync_config->subscription_initializer) {
66-
const bool rerun_on_launch = config.sync_config->rerun_init_subscription_on_open;
67-
self->attach_to_subscription_initializer(std::move(async_open_complete), coordinator, rerun_on_launch);
68-
}
69-
else {
70-
self->async_open_complete(std::move(async_open_complete), coordinator, status);
62+
self->migrate_schema_or_complete(std::move(callback), coordinator, status);
63+
});
64+
// The callback does not extend the lifetime of the task if it's never invoked.
65+
SyncSession::Internal::set_sync_schema_migration_callback(*session, [weak_self = weak_from_this(), this]() {
66+
if (auto self = weak_self.lock()) {
67+
util::CheckedLockGuard lock(m_mutex);
68+
if (!m_session)
69+
return;
70+
m_sync_schema_migration_required = true;
7171
}
7272
});
7373
session->revive_if_needed();
@@ -118,7 +118,7 @@ void AsyncOpenTask::unregister_download_progress_notifier(uint64_t token)
118118
m_session->unregister_progress_notifier(token);
119119
}
120120

121-
void AsyncOpenTask::attach_to_subscription_initializer(AsyncOpenCallback&& async_open_callback,
121+
void AsyncOpenTask::attach_to_subscription_initializer(AsyncOpenCallback&& callback,
122122
std::shared_ptr<_impl::RealmCoordinator> coordinator,
123123
bool rerun_on_launch)
124124
{
@@ -136,13 +136,13 @@ void AsyncOpenTask::attach_to_subscription_initializer(AsyncOpenCallback&& async
136136
// We need to wait until subscription initializer completes
137137
std::shared_ptr<AsyncOpenTask> self(shared_from_this());
138138
init_subscription.get_state_change_notification(sync::SubscriptionSet::State::Complete)
139-
.get_async([self, coordinator, async_open_callback = std::move(async_open_callback)](
139+
.get_async([self, coordinator, callback = std::move(callback)](
140140
StatusWith<realm::sync::SubscriptionSet::State> state) mutable {
141-
self->async_open_complete(std::move(async_open_callback), coordinator, state.get_status());
141+
self->async_open_complete(std::move(callback), coordinator, state.get_status());
142142
});
143143
}
144144
else {
145-
async_open_complete(std::move(async_open_callback), coordinator, Status::OK());
145+
async_open_complete(std::move(callback), coordinator, Status::OK());
146146
}
147147
}
148148

@@ -151,6 +151,10 @@ void AsyncOpenTask::async_open_complete(AsyncOpenCallback&& callback,
151151
{
152152
{
153153
util::CheckedLockGuard lock(m_mutex);
154+
// 'Cancel' may have been called just before 'async_open_complete' is invoked.
155+
if (!m_session)
156+
return;
157+
154158
for (auto token : m_registered_callbacks) {
155159
m_session->unregister_progress_notifier(token);
156160
}
@@ -170,4 +174,79 @@ void AsyncOpenTask::async_open_complete(AsyncOpenCallback&& callback,
170174
return callback({}, std::make_exception_ptr(Exception(status)));
171175
}
172176

177+
void AsyncOpenTask::migrate_schema_or_complete(AsyncOpenCallback&& callback,
178+
std::shared_ptr<_impl::RealmCoordinator> coordinator, Status status)
179+
{
180+
util::CheckedUniqueLock lock(m_mutex);
181+
if (!m_session)
182+
return;
183+
auto session = m_session;
184+
auto migrate_schema = m_sync_schema_migration_required;
185+
lock.unlock();
186+
187+
if (!migrate_schema) {
188+
wait_for_bootstrap_or_complete(std::move(callback), coordinator, status);
189+
return;
190+
}
191+
192+
// Migrate the schema.
193+
// * First upload the changes at the old schema version
194+
// * Then delete the realm, reopen it, and rebootstrap at new schema version
195+
// The lifetime of the task is extended until the bootstrap completes.
196+
std::shared_ptr<AsyncOpenTask> self(shared_from_this());
197+
session->wait_for_upload_completion([callback = std::move(callback), coordinator, session, self,
198+
this](Status status) mutable {
199+
{
200+
util::CheckedLockGuard lock(m_mutex);
201+
if (!m_session)
202+
return; // Swallow all events if the task has been cancelled.
203+
}
204+
205+
if (!status.is_ok()) {
206+
self->async_open_complete(std::move(callback), coordinator, status);
207+
return;
208+
}
209+
210+
auto future = SyncSession::Internal::pause_async(*session);
211+
// Wait until the SessionWrapper is done using the DBRef.
212+
std::move(future).get_async([callback = std::move(callback), coordinator, self, this](Status status) mutable {
213+
{
214+
util::CheckedLockGuard lock(m_mutex);
215+
if (!m_session)
216+
return; // Swallow all events if the task has been cancelled.
217+
}
218+
219+
if (!status.is_ok()) {
220+
self->async_open_complete(std::move(callback), coordinator, status);
221+
return;
222+
}
223+
224+
// Delete the realm file and reopen it.
225+
{
226+
util::CheckedLockGuard lock(m_mutex);
227+
m_session = nullptr;
228+
coordinator->delete_and_reopen();
229+
m_session = coordinator->sync_session();
230+
}
231+
232+
self->wait_for_bootstrap_or_complete(std::move(callback), coordinator, status);
233+
});
234+
});
235+
}
236+
237+
void AsyncOpenTask::wait_for_bootstrap_or_complete(AsyncOpenCallback&& callback,
238+
std::shared_ptr<_impl::RealmCoordinator> coordinator,
239+
Status status)
240+
{
241+
auto config = coordinator->get_config();
242+
if (config.sync_config && config.sync_config->flx_sync_requested &&
243+
config.sync_config->subscription_initializer) {
244+
const bool rerun_on_launch = config.sync_config->rerun_init_subscription_on_open;
245+
attach_to_subscription_initializer(std::move(callback), coordinator, rerun_on_launch);
246+
}
247+
else {
248+
async_open_complete(std::move(callback), coordinator, status);
249+
}
250+
}
251+
173252
} // namespace realm

src/realm/object-store/sync/async_open_task.hpp

+7-2
Original file line numberDiff line numberDiff line change
@@ -48,7 +48,7 @@ class AsyncOpenTask : public std::enable_shared_from_this<AsyncOpenTask> {
4848
//
4949
// If multiple AsyncOpenTasks all attempt to download the same Realm and one of them is canceled,
5050
// the other tasks will receive a "Cancelled" exception.
51-
void start(AsyncOpenCallback async_open_complete) REQUIRES(!m_mutex);
51+
void start(AsyncOpenCallback callback) REQUIRES(!m_mutex);
5252

5353
// Cancels the download and stops the session. No further functions should be called on this class.
5454
void cancel() REQUIRES(!m_mutex);
@@ -62,12 +62,17 @@ class AsyncOpenTask : public std::enable_shared_from_this<AsyncOpenTask> {
6262
REQUIRES(!m_mutex);
6363
void attach_to_subscription_initializer(AsyncOpenCallback&&, std::shared_ptr<_impl::RealmCoordinator>, bool)
6464
REQUIRES(!m_mutex);
65+
void migrate_schema_or_complete(AsyncOpenCallback&&, std::shared_ptr<_impl::RealmCoordinator>, Status)
66+
REQUIRES(!m_mutex);
67+
void wait_for_bootstrap_or_complete(AsyncOpenCallback&&, std::shared_ptr<_impl::RealmCoordinator>, Status)
68+
REQUIRES(!m_mutex);
6569

6670
std::shared_ptr<_impl::RealmCoordinator> m_coordinator GUARDED_BY(m_mutex);
6771
std::shared_ptr<SyncSession> m_session GUARDED_BY(m_mutex);
6872
std::vector<uint64_t> m_registered_callbacks GUARDED_BY(m_mutex);
6973
mutable util::CheckedMutex m_mutex;
70-
bool m_db_first_open{true};
74+
const bool m_db_first_open;
75+
bool m_sync_schema_migration_required GUARDED_BY(m_mutex) = false;
7176
};
7277

7378
} // namespace realm

src/realm/object-store/sync/sync_session.cpp

+17
Original file line numberDiff line numberDiff line change
@@ -749,6 +749,16 @@ void SyncSession::handle_error(sync::SessionErrorInfo error)
749749
next_state = NextStateAfterError::inactive;
750750
log_out_user = true;
751751
break;
752+
case sync::ProtocolErrorInfo::Action::MigrateSchema:
753+
std::function<void()> callback;
754+
{
755+
util::CheckedLockGuard l(m_state_mutex);
756+
callback = std::move(m_sync_schema_migration_callback);
757+
}
758+
if (callback) {
759+
callback();
760+
}
761+
return; // do not propgate the error to the user at this point
752762
}
753763
}
754764
else {
@@ -901,6 +911,7 @@ void SyncSession::create_sync_session()
901911
session_config.flx_bootstrap_batch_size_bytes = sync_config.flx_bootstrap_batch_size_bytes;
902912
session_config.session_reason =
903913
client_reset::is_fresh_path(m_config.path) ? sync::SessionReason::ClientReset : sync::SessionReason::Sync;
914+
session_config.schema_version = m_config.schema_version;
904915

905916
if (sync_config.on_sync_client_event_hook) {
906917
session_config.on_sync_client_event_hook = [hook = sync_config.on_sync_client_event_hook,
@@ -989,6 +1000,12 @@ void SyncSession::create_sync_session()
9891000
});
9901001
}
9911002

1003+
void SyncSession::set_sync_schema_migration_callback(std::function<void()>&& callback)
1004+
{
1005+
util::CheckedLockGuard l(m_state_mutex);
1006+
m_sync_schema_migration_callback = std::move(callback);
1007+
}
1008+
9921009
void SyncSession::nonsync_transact_notify(sync::version_type version)
9931010
{
9941011
m_progress_notifier.set_local_version(version);

src/realm/object-store/sync/sync_session.hpp

+10
Original file line numberDiff line numberDiff line change
@@ -274,6 +274,7 @@ class SyncSession : public std::enable_shared_from_this<SyncSession> {
274274
class Internal {
275275
friend class _impl::RealmCoordinator;
276276
friend struct OnlyForTesting;
277+
friend class AsyncOpenTask;
277278

278279
static void nonsync_transact_notify(SyncSession& session, VersionID::version_type version)
279280
{
@@ -285,6 +286,11 @@ class SyncSession : public std::enable_shared_from_this<SyncSession> {
285286
return session.m_db;
286287
}
287288

289+
static void set_sync_schema_migration_callback(SyncSession& session, std::function<void()>&& callback)
290+
{
291+
session.set_sync_schema_migration_callback(std::move(callback));
292+
}
293+
288294
static util::Future<void> pause_async(SyncSession& session);
289295
};
290296

@@ -397,6 +403,8 @@ class SyncSession : public std::enable_shared_from_this<SyncSession> {
397403

398404
void nonsync_transact_notify(VersionID::version_type) REQUIRES(!m_state_mutex);
399405

406+
void set_sync_schema_migration_callback(std::function<void()>&&) REQUIRES(!m_state_mutex);
407+
400408
void create_sync_session() REQUIRES(m_state_mutex, !m_config_mutex);
401409
void did_drop_external_reference()
402410
REQUIRES(!m_state_mutex, !m_config_mutex, !m_external_reference_mutex, !m_connection_state_mutex);
@@ -431,6 +439,8 @@ class SyncSession : public std::enable_shared_from_this<SyncSession> {
431439

432440
std::function<TransactionCallback> m_sync_transact_callback GUARDED_BY(m_state_mutex);
433441

442+
std::function<void()> m_sync_schema_migration_callback GUARDED_BY(m_state_mutex);
443+
434444
template <typename Field>
435445
auto config(Field f) REQUIRES(!m_config_mutex)
436446
{

src/realm/sync/noinst/client_impl_base.cpp

+11
Original file line numberDiff line numberDiff line change
@@ -2537,6 +2537,17 @@ Status Session::receive_error_message(const ProtocolErrorInfo& info)
25372537
return Status::OK();
25382538
}
25392539

2540+
if (protocol_error == ProtocolError::schema_version_changed) {
2541+
// Enable upload immediately if the session is still active.
2542+
if (m_state == Active) {
2543+
m_allow_upload = true;
2544+
// Notify SyncSession a schema migration is required.
2545+
on_connection_state_changed(m_conn.get_state(), SessionErrorInfo{info});
2546+
}
2547+
// Keep the session active to upload any unsynced changes.
2548+
return Status::OK();
2549+
}
2550+
25402551
m_error_message_received = true;
25412552
suspend(SessionErrorInfo{info, std::move(status)});
25422553
return Status::OK();

src/realm/sync/noinst/protocol_codec.hpp

+1
Original file line numberDiff line numberDiff line change
@@ -490,6 +490,7 @@ class ClientProtocol {
490490
{"RefreshUser", action::RefreshUser},
491491
{"RefreshLocation", action::RefreshLocation},
492492
{"LogOutUser", action::LogOutUser},
493+
{"MigrateSchema", action::MigrateSchema},
493494
};
494495

495496
if (auto action_it = mapping.find(action_string); action_it != mapping.end()) {

0 commit comments

Comments
 (0)