• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

realm / realm-core / 2466

02 Jul 2024 04:06AM UTC coverage: 90.974% (-0.2%) from 91.147%
2466

push

Evergreen

web-flow
Merge pull request #7576 from realm/tg/multi-process-launch-actions

RCORE-1900 Make "next launch" metadata actions multiprocess-safe

102260 of 180446 branches covered (56.67%)

348 of 356 new or added lines in 15 files covered. (97.75%)

334 existing lines in 18 files now uncovered.

215128 of 236473 relevant lines covered (90.97%)

5909897.4 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

97.67
/src/realm/object-store/sync/impl/app_metadata.cpp
1
////////////////////////////////////////////////////////////////////////////
2
//
3
// Copyright 2024 Realm Inc.
4
//
5
// Licensed under the Apache License, Version 2.0 (the "License");
6
// you may not use this file except in compliance with the License.
7
// You may obtain a copy of the License at
8
//
9
// http://www.apache.org/licenses/LICENSE-2.0
10
//
11
// Unless required by applicable law or agreed to in writing, software
12
// distributed under the License is distributed on an "AS IS" BASIS,
13
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14
// See the License for the specific language governing permissions and
15
// limitations under the License.
16
//
17
////////////////////////////////////////////////////////////////////////////
18

19
#include <realm/object-store/sync/impl/app_metadata.hpp>
20

21
#include <realm/object-store/impl/realm_coordinator.hpp>
22
#include <realm/object-store/object_schema.hpp>
23
#include <realm/object-store/object_store.hpp>
24
#include <realm/object-store/property.hpp>
25
#include <realm/object-store/results.hpp>
26
#include <realm/object-store/schema.hpp>
27
#include <realm/object-store/sync/impl/sync_file.hpp>
28
#include <realm/object-store/util/scheduler.hpp>
29

30
#if REALM_PLATFORM_APPLE
31
#include <realm/object-store/impl/apple/keychain_helper.hpp>
32
#endif
33

34
#include <realm/db.hpp>
35
#include <realm/dictionary.hpp>
36
#include <realm/table.hpp>
37

38
using namespace realm;
39
using realm::app::UserData;
40

41
template <>
42
inline SyncUser::State Obj::get(ColKey ck) const
43
{
4,442✔
44
    return static_cast<SyncUser::State>(get<int64_t>(ck));
4,442✔
45
}
4,442✔
46

47
namespace {
48

49
struct CurrentUserSchema {
50
    TableKey table_key;
51
    ColKey user_id;
52

53
    static constexpr const char* table_name = "current_user_identity";
54

55
    void read(Realm& realm)
56
    {
703✔
57
        auto object_schema = realm.schema().find(table_name);
703✔
58
        table_key = object_schema->table_key;
703✔
59
        user_id = object_schema->persisted_properties[0].column_key;
703✔
60
    }
703✔
61

62
    static ObjectSchema object_schema()
63
    {
704✔
64
        return {table_name, {{table_name, PropertyType::String}}};
704✔
65
    }
704✔
66
};
67

68
struct UserIdentitySchema {
69
    TableKey table_key;
70
    ColKey user_id;
71
    ColKey provider_id;
72

73
    static constexpr const char* table_name = "UserIdentity";
74

75
    void read(Realm& realm)
76
    {
703✔
77
        auto object_schema = realm.schema().find(table_name);
703✔
78
        table_key = object_schema->table_key;
703✔
79
        user_id = object_schema->persisted_properties[0].column_key;
703✔
80
        provider_id = object_schema->persisted_properties[1].column_key;
703✔
81
    }
703✔
82

83
    static ObjectSchema object_schema()
84
    {
704✔
85
        return {table_name,
704✔
86
                ObjectSchema::ObjectType::Embedded,
704✔
87
                {
704✔
88
                    {"id", PropertyType::String},
704✔
89
                    {"provider_type", PropertyType::String},
704✔
90
                }};
704✔
91
    }
704✔
92
};
93

94
struct SyncUserSchema {
95
    TableKey table_key;
96

97
    // The server-supplied user_id for the user. Unique per server instance.
98
    ColKey user_id_col;
99
    // Locally generated UUIDs for the user. These are tracked to be able
100
    // to open pre-existing Realm files, but are no longer generated or
101
    // used for anything else.
102
    ColKey legacy_uuids_col;
103
    // The cached refresh token for this user.
104
    ColKey refresh_token_col;
105
    // The cached access token for this user.
106
    ColKey access_token_col;
107
    // The identities for this user.
108
    ColKey identities_col;
109
    // The current state of this user.
110
    ColKey state_col;
111
    // The device id of this user.
112
    ColKey device_id_col;
113
    // Any additional profile attributes, formatted as a bson string.
114
    ColKey profile_dump_col;
115
    // The set of absolute file paths to Realms belonging to this user.
116
    ColKey realm_file_paths_col;
117

118
    static constexpr const char* table_name = "UserMetadata";
119

120
    void read(Realm& realm)
121
    {
709✔
122
        auto object_schema = realm.schema().find(table_name);
709✔
123
        table_key = object_schema->table_key;
709✔
124
        user_id_col = object_schema->persisted_properties[0].column_key;
709✔
125
        legacy_uuids_col = object_schema->persisted_properties[1].column_key;
709✔
126
        refresh_token_col = object_schema->persisted_properties[2].column_key;
709✔
127
        access_token_col = object_schema->persisted_properties[3].column_key;
709✔
128
        identities_col = object_schema->persisted_properties[4].column_key;
709✔
129
        state_col = object_schema->persisted_properties[5].column_key;
709✔
130
        device_id_col = object_schema->persisted_properties[6].column_key;
709✔
131
        profile_dump_col = object_schema->persisted_properties[7].column_key;
709✔
132
        realm_file_paths_col = object_schema->persisted_properties[8].column_key;
709✔
133
    }
709✔
134

135
    static ObjectSchema object_schema()
136
    {
704✔
137
        return {table_name,
704✔
138
                {{"identity", PropertyType::String},
704✔
139
                 {"legacy_uuids", PropertyType::String | PropertyType::Array},
704✔
140
                 {"refresh_token", PropertyType::String | PropertyType::Nullable},
704✔
141
                 {"access_token", PropertyType::String | PropertyType::Nullable},
704✔
142
                 {"identities", PropertyType::Object | PropertyType::Array, UserIdentitySchema::table_name},
704✔
143
                 {"state", PropertyType::Int},
704✔
144
                 {"device_id", PropertyType::String},
704✔
145
                 {"profile_data", PropertyType::String},
704✔
146
                 {"local_realm_paths", PropertyType::Set | PropertyType::String}}};
704✔
147
    }
704✔
148
};
149

150
struct FileActionSchema {
151
    TableKey table_key;
152

153
    // The original path on disk of the file (generally, the main file for an on-disk Realm).
154
    ColKey idx_original_name;
155
    // A new path on disk for a file to be written to. Context-dependent.
156
    ColKey idx_new_name;
157
    // An enum describing the action to take.
158
    ColKey idx_action;
159
    // The partition key of the Realm.
160
    ColKey idx_partition;
161
    // The user_id of the user to whom the file action applies (despite the internal column name).
162
    ColKey idx_user_identity;
163

164
    static constexpr const char* table_name = "FileActionMetadata";
165

166
    void read(Realm& realm)
167
    {
703✔
168
        auto object_schema = realm.schema().find(table_name);
703✔
169
        table_key = object_schema->table_key;
703✔
170
        idx_original_name = object_schema->persisted_properties[0].column_key;
703✔
171
        idx_new_name = object_schema->persisted_properties[1].column_key;
703✔
172
        idx_action = object_schema->persisted_properties[2].column_key;
703✔
173
        idx_partition = object_schema->persisted_properties[3].column_key;
703✔
174
        idx_user_identity = object_schema->persisted_properties[4].column_key;
703✔
175
    }
703✔
176

177
    static ObjectSchema object_schema()
178
    {
704✔
179
        return {table_name,
704✔
180
                {
704✔
181
                    {"original_name", PropertyType::String, Property::IsPrimary{true}},
704✔
182
                    {"new_name", PropertyType::String | PropertyType::Nullable},
704✔
183
                    {"action", PropertyType::Int},
704✔
184
                    {"url", PropertyType::String},      // actually partition key
704✔
185
                    {"identity", PropertyType::String}, // actually user id
704✔
186
                }};
704✔
187
    }
704✔
188
};
189

190
void migrate_to_v7(std::shared_ptr<Realm> old_realm, std::shared_ptr<Realm> realm)
191
{
6✔
192
    // Before schema version 7 there may have been multiple UserMetadata entries
193
    // for a single user_id with different provider types, so we need to merge
194
    // any duplicates together
195

196
    SyncUserSchema schema;
6✔
197
    schema.read(*realm);
6✔
198

199
    TableRef table = realm->read_group().get_table(schema.table_key);
6✔
200
    TableRef old_table = ObjectStore::table_for_object_type(old_realm->read_group(), SyncUserSchema::table_name);
6✔
201
    if (table->is_empty())
6✔
202
        return;
×
203
    REALM_ASSERT(table->size() == old_table->size());
6✔
204

205
    ColKey old_uuid_col = old_table->get_column_key("local_uuid");
6✔
206

207
    std::unordered_map<std::string, Obj> users;
6✔
208
    for (size_t i = 0, j = 0; i < table->size(); ++j) {
36✔
209
        auto obj = table->get_object(i);
30✔
210

211
        // Move the local uuid from the old column to the list
212
        auto old_obj = old_table->get_object(j);
30✔
213
        obj.get_list<String>(schema.legacy_uuids_col).add(old_obj.get<String>(old_uuid_col));
30✔
214

215
        // Check if we've already seen an object with the same id. If not, store
216
        // this one and move on
217
        std::string user_id = obj.get<String>(schema.user_id_col);
30✔
218
        auto& existing = users[obj.get<String>(schema.user_id_col)];
30✔
219
        if (!existing.is_valid()) {
30✔
220
            existing = obj;
18✔
221
            ++i;
18✔
222
            continue;
18✔
223
        }
18✔
224

225
        // We have a second object for the same id, so we need to merge them.
226
        // First we merge the state: if one is logged in and the other isn't,
227
        // we'll use the logged-in state and tokens. If both are logged in, we'll
228
        // use the more recent login. If one is logged out and the other is
229
        // removed we'll use the logged out state. If both are logged out or
230
        // both are removed then it doesn't matter which we pick.
231
        using State = SyncUser::State;
12✔
232
        auto state = obj.get<State>(schema.state_col);
12✔
233
        auto existing_state = State(existing.get<int64_t>(schema.state_col));
12✔
234
        if (state == existing_state) {
12✔
235
            if (state == State::LoggedIn) {
4✔
236
                RealmJWT token_1(existing.get<StringData>(schema.access_token_col));
4✔
237
                RealmJWT token_2(obj.get<StringData>(schema.access_token_col));
4✔
238
                if (token_1.issued_at < token_2.issued_at) {
4✔
239
                    existing.set(schema.refresh_token_col, obj.get<StringData>(schema.refresh_token_col));
2✔
240
                    existing.set(schema.access_token_col, obj.get<StringData>(schema.access_token_col));
2✔
241
                }
2✔
242
            }
4✔
243
        }
4✔
244
        else if (state == State::LoggedIn || existing_state == State::Removed) {
8✔
245
            existing.set(schema.state_col, int64_t(state));
4✔
246
            existing.set(schema.refresh_token_col, obj.get<StringData>(schema.refresh_token_col));
4✔
247
            existing.set(schema.access_token_col, obj.get<StringData>(schema.access_token_col));
4✔
248
        }
4✔
249

250
        // Next we merge the list properties (identities, legacy uuids, realm file paths)
251
        {
12✔
252
            auto dest = existing.get_linklist(schema.identities_col);
12✔
253
            auto src = obj.get_linklist(schema.identities_col);
12✔
254
            for (size_t i = 0, size = src.size(); i < size; ++i) {
36✔
255
                if (dest.find_first(src.get(i)) == npos) {
24✔
256
                    dest.add(src.get(i));
12✔
257
                }
12✔
258
            }
24✔
259
        }
12✔
260
        {
12✔
261
            auto dest = existing.get_list<String>(schema.legacy_uuids_col);
12✔
262
            auto src = obj.get_list<String>(schema.legacy_uuids_col);
12✔
263
            for (size_t i = 0, size = src.size(); i < size; ++i) {
24✔
264
                if (dest.find_first(src.get(i)) == npos) {
12✔
265
                    dest.add(src.get(i));
12✔
266
                }
12✔
267
            }
12✔
268
        }
12✔
269
        {
12✔
270
            auto dest = existing.get_set<String>(schema.realm_file_paths_col);
12✔
271
            auto src = obj.get_set<String>(schema.realm_file_paths_col);
12✔
272
            for (size_t i = 0, size = src.size(); i < size; ++i) {
36✔
273
                dest.insert(src.get(i));
24✔
274
            }
24✔
275
        }
12✔
276

277
        // Finally we delete the duplicate object. We don't increment `i` as it's
278
        // now the index of the object just after the one we're deleting.
279
        obj.remove();
12✔
280
    }
12✔
281
}
6✔
282

283
std::shared_ptr<Realm> open_realm(RealmConfig& config, const app::AppConfig& app_config)
284
{
704✔
285
    bool should_encrypt = app_config.metadata_mode == app::AppConfig::MetadataMode::Encryption;
704✔
286
    if (!REALM_PLATFORM_APPLE && should_encrypt && !app_config.custom_encryption_key)
704✔
287
        throw InvalidArgument("Metadata Realm encryption was specified, but no encryption key was provided.");
1✔
288

289
    if (app_config.custom_encryption_key && should_encrypt)
703✔
290
        config.encryption_key = *app_config.custom_encryption_key;
10✔
291
    if (app_config.custom_encryption_key || !should_encrypt || !REALM_PLATFORM_APPLE) {
703✔
292
        config.clear_on_invalid_file = true;
703✔
293
        return Realm::get_shared_realm(config);
703✔
294
    }
703✔
295

296
#if REALM_PLATFORM_APPLE
297
    auto try_get_realm = [&]() -> std::shared_ptr<Realm> {
298
        try {
299
            return Realm::get_shared_realm(config);
300
        }
301
        catch (const InvalidDatabase&) {
302
            return nullptr;
303
        }
304
    };
305

306
    // First try to open the Realm with a key already stored in the keychain.
307
    // This works for both the case where everything is sensible and valid and
308
    // when we have a key but no metadata Realm.
309
    auto key = keychain::get_existing_metadata_realm_key(app_config.app_id, app_config.security_access_group);
310
    if (key) {
×
311
        config.encryption_key = *key;
312
        if (auto realm = try_get_realm())
×
313
            return realm;
314
    }
315

316
    // If we have an existing file and either no key or the key didn't work to
317
    // decrypt it, then we might have an unencrypted metadata Realm resulting
318
    // from a previous run being unable to access the keychain.
319
    if (util::File::exists(config.path)) {
×
320
        config.encryption_key.clear();
321
        if (auto realm = try_get_realm())
×
322
            return realm;
323

324
        // We weren't able to open the existing file with either the stored key
325
        // or no key, so just recreate it
326
        config.clear_on_invalid_file = true;
327
    }
328

329
    // We now have no metadata Realm. If we don't have an existing stored key,
330
    // try to create and store a new one. This might fail, in which case we
331
    // just create an unencrypted Realm file.
332
    if (!key)
×
333
        key = keychain::create_new_metadata_realm_key(app_config.app_id, app_config.security_access_group);
334
    if (key)
×
335
        config.encryption_key = std::move(*key);
336
    return try_get_realm();
337
#else  // REALM_PLATFORM_APPLE
338
    REALM_UNREACHABLE();
339
#endif // REALM_PLATFORM_APPLE
340
}
×
341

342
struct PersistedSyncMetadataManager : public app::MetadataStore {
343
    std::shared_ptr<_impl::RealmCoordinator> m_coordinator;
344
    SyncUserSchema m_user_schema;
345
    FileActionSchema m_file_action_schema;
346
    UserIdentitySchema m_user_identity_schema;
347
    CurrentUserSchema m_current_user_schema;
348

349
    using UserState = SyncUser::State;
350

351
    PersistedSyncMetadataManager(const app::AppConfig& app_config, SyncFileManager& file_manager)
352
    {
704✔
353
        // Note that there are several deferred schema changes which don't
354
        // justify bumping the schema version by themself, but should be done
355
        // the next time something does justify a migration.
356
        // These include:
357
        // - remove FileActionSchema url and identity columns
358
        // - rename current_user_identity to CurrentUserId
359
        // - change most of the nullable columns to non-nullable
360
        constexpr uint64_t SCHEMA_VERSION = 7;
704✔
361

362
        RealmConfig config;
704✔
363
        config.automatic_change_notifications = false;
704✔
364
        config.path = file_manager.metadata_path();
704✔
365
        config.schema = Schema{
704✔
366
            UserIdentitySchema::object_schema(),
704✔
367
            SyncUserSchema::object_schema(),
704✔
368
            FileActionSchema::object_schema(),
704✔
369
            CurrentUserSchema::object_schema(),
704✔
370
        };
704✔
371

372
        config.schema_version = SCHEMA_VERSION;
704✔
373
        config.schema_mode = SchemaMode::Automatic;
704✔
374
        config.scheduler = util::Scheduler::make_dummy();
704✔
375
        config.automatically_handle_backlinks_in_migrations = true;
704✔
376
        config.migration_function = [](std::shared_ptr<Realm> old_realm, std::shared_ptr<Realm> realm, Schema&) {
704✔
377
            if (old_realm->schema_version() < 7) {
6✔
378
                migrate_to_v7(old_realm, realm);
6✔
379
            }
6✔
380
        };
6✔
381

382
        auto realm = open_realm(config, app_config);
704✔
383
        m_user_schema.read(*realm);
704✔
384
        m_file_action_schema.read(*realm);
704✔
385
        m_user_identity_schema.read(*realm);
704✔
386
        m_current_user_schema.read(*realm);
704✔
387

388
        m_coordinator = _impl::RealmCoordinator::get_existing_coordinator(config.path);
704✔
389

390
        // When App::remove_user() is called, we mark the user as "removed" but
391
        // don't actually delete the UserMetadata object or the files on disk
392
        // immediately, and instead defer the actual removal until the next
393
        // launch of the application. This makes it so that we don't have to
394
        // require developers to ensure that all Realms associated with a user
395
        // are closed before removing the user, as that can be a difficult thing
396
        // to do.
397
        //
398
        // The "next launch" in a multiprocess scenario can be a somewhat
399
        // complicated concept. If one process calls remove_user(), exits, and
400
        // then is restarted, it still isn't safe to delete the user's files yet
401
        // if another process has also been running the whole time. Instead, we
402
        // need to wait for *all* of the processes which share a metadata Realm
403
        // to exit, and perform the cleanup actions only when one is launching
404
        // in a fresh state with no other processes running (but also we need to
405
        // work if multiple processes launch at once).
406
        //
407
        // The lock file management code already solves this problem - using a
408
        // mix of exclusive and shared locks on the lock file, opening a Realm
409
        // will reinitialize the lock file from scratch only if it is the
410
        // "session initiator" and no one already has the file open. We therefore
411
        // can detect the scenario where we want to perform launch actions by
412
        // using a flag in the lock file: we begin a frozen read transaction,
413
        // attempt to atomically set the flag, and if we were able we proceeed
414
        // to perform the launch actions present in that frozen version.
415
        //
416
        // In the simple scenario of an unshared metadata Realm which is only
417
        // ever accessed by a single process, this all behaves identically to
418
        // the naive solution of always processing all launch actions on launch.
419
        // If the metadata Realm was already open in another process, the flag
420
        // will already be set and we'll skip performing launch actions. If two
421
        // processes open the metadata Realm at once we get to the complicated
422
        // scenario: one of them will successfully set the flag and the other
423
        // will fail. The one which failed may then go on to perform writes on
424
        // the metadata Realm, possibly creating new launch actions. This is why
425
        // we need the frozen read transaction created *before* trying to set
426
        // the flag. The process which does successfully set the flag will only
427
        // process launch actions present in that frozen read, and thus only ones
428
        // which already existing before it set the flag. Any new actions created
429
        // by the second process won't be visible and will wait for the next
430
        // launch.
431
        //
432
        // User cleanup is made slightly more complicated by that users can be
433
        // logged back in after being removed. To handle this, we only delete
434
        // users (and their files) if the user is Removed in both the frozen
435
        // transaction and inside the write transaction. If a second process
436
        // logs the user back in between when we start the frozen read and when
437
        // we acquire the write lock we'll skip it, and if it tries to log in
438
        // after we acquire the write lock it'll be blocked by that and only
439
        // log in after we have completed cleanup.
440
        //
441
        // This scheme is only fully safe if synchronized Realms are only ever
442
        // opened using a non-Removed user, and not by using the fake sync history
443
        // mode where no user is provided. That mode is hopefully only ever used
444
        // by Realm Studio.
445
        //
446
        // To avoid a lockfile format change, the flag used happens to be the
447
        // sync agent flag, which is otherwise unused for the metadata Realm
448
        // (which is a local unsynchronized Realm). The use of this flag should
449
        // not be confused for there actually being a sync agent for the
450
        // metadata Realm.
451
        auto frozen = realm->freeze();
704✔
452
        if (m_coordinator->try_claim_sync_agent()) {
704✔
453
            realm->begin_transaction();
671✔
454
            perform_file_actions(frozen->read_group(), realm->read_group(), file_manager);
671✔
455
            remove_dead_users(frozen->read_group(), realm->read_group(), file_manager);
671✔
456
            realm->commit_transaction();
671✔
457
        }
671✔
458
    }
704✔
459

460
    std::shared_ptr<Realm> get_realm() const
461
    {
6,851✔
462
        return m_coordinator->get_realm(util::Scheduler::make_dummy());
6,851✔
463
    }
6,851✔
464

465
    void for_each_obj(Group& frozen, Group& live, TableKey tk, util::FunctionRef<void(Obj&, Obj&)> fn)
466
    {
1,342✔
467
        auto frozen_table = frozen.get_table(tk);
1,342✔
468
        if (frozen_table->is_empty())
1,342✔
469
            return;
1,302✔
470

471
        // We want to iterate the objects present in both the before and after
472
        // realms. Any other objects are either no longer relevant or too new.
473
        TableRef table = live.get_table(tk);
40✔
474
        for (auto frozen_obj : *frozen_table) {
54✔
475
            if (auto obj = table->get_object(frozen_obj.get_key())) {
54✔
476
                fn(frozen_obj, obj);
54✔
477
            }
54✔
478
        }
54✔
479
    }
40✔
480

481
    void remove_dead_users(Group& frozen, Group& live, SyncFileManager& file_manager)
482
    {
671✔
483
        auto& schema = m_user_schema;
671✔
484
        for_each_obj(frozen, live, schema.table_key, [&](Obj& frozen_obj, Obj& live_obj) {
671✔
485
            // The frozen object being removed but not the live object means that
486
            // another process logged the user back in. The live object being
487
            // removed but not the frozen one means that the removal happened
488
            // after we acquired the sync agent, and so we shouldn't process
489
            // the user yet.
490
            if (frozen_obj.get<UserState>(schema.state_col) != UserState::Removed)
50✔
491
                return;
36✔
492
            if (live_obj.get<UserState>(schema.state_col) != UserState::Removed)
14✔
NEW
493
                return;
×
494
            delete_user_realms(file_manager, live_obj);
14✔
495
        });
14✔
496
    }
671✔
497

498
    void delete_user_realms(SyncFileManager& file_manager, Obj& obj)
499
    {
32✔
500
        Set<StringData> paths = obj.get_set<StringData>(m_user_schema.realm_file_paths_col);
32✔
501
        bool any_failed = false;
32✔
502
        for (auto path : paths) {
32✔
503
            if (!file_manager.remove_realm(path))
14✔
504
                any_failed = true;
1✔
505
        }
14✔
506
        try {
32✔
507
            file_manager.remove_user_realms(obj.get<String>(m_user_schema.user_id_col));
32✔
508
        }
32✔
509
        catch (FileAccessError const&) {
32✔
510
            any_failed = true;
×
511
        }
×
512

513
        // Only remove the object if all of the tracked realms no longer exist,
514
        // and otherwise try again to delete them on the next launch
515
        if (!any_failed) {
32✔
516
            obj.remove();
31✔
517
        }
31✔
518
    }
32✔
519

520
    bool perform_file_action(SyncFileManager& file_manager, Obj& obj)
521
    {
22✔
522
        auto& schema = m_file_action_schema;
22✔
523
        switch (static_cast<SyncFileAction>(obj.get<int64_t>(schema.idx_action))) {
22✔
524
            case SyncFileAction::DeleteRealm:
11✔
525
                // Delete all the files for the given Realm.
526
                return file_manager.remove_realm(obj.get<String>(schema.idx_original_name));
11✔
527

528
            case SyncFileAction::BackUpThenDeleteRealm:
11✔
529
                // Copy the primary Realm file to the recovery dir, and then delete the Realm.
530
                auto new_name = obj.get<String>(schema.idx_new_name);
11✔
531
                auto original_name = obj.get<String>(schema.idx_original_name);
11✔
532
                if (!util::File::exists(original_name)) {
11✔
533
                    // The Realm file doesn't exist anymore, which is fine
534
                    return true;
2✔
535
                }
2✔
536

537
                if (new_name && file_manager.copy_realm_file(original_name, new_name)) {
9✔
538
                    // We successfully copied the Realm file to the recovery directory.
539
                    bool did_remove = file_manager.remove_realm(original_name);
7✔
540
                    // if the copy succeeded but not the delete, then running BackupThenDelete
541
                    // a second time would fail, so change this action to just delete the original file.
542
                    if (did_remove) {
7✔
543
                        return true;
6✔
544
                    }
6✔
545
                    obj.set(schema.idx_action, static_cast<int64_t>(SyncFileAction::DeleteRealm));
1✔
546
                }
1✔
547
        }
22✔
548
        return false;
3✔
549
    }
22✔
550

551
    void perform_file_actions(Group& frozen, Group& live, SyncFileManager& file_manager)
552
    {
671✔
553
        for_each_obj(frozen, live, m_file_action_schema.table_key, [&](Obj&, Obj& obj) {
671✔
554
            if (perform_file_action(file_manager, obj))
4✔
555
                obj.remove();
4✔
556
        });
4✔
557
    }
671✔
558

559
    bool immediately_run_file_actions(SyncFileManager& file_manager, std::string_view realm_path) override
560
    {
39✔
561
        auto realm = get_realm();
39✔
562
        realm->begin_transaction();
39✔
563
        TableRef table = realm->read_group().get_table(m_file_action_schema.table_key);
39✔
564
        auto key = table->where().equal(m_file_action_schema.idx_original_name, StringData(realm_path)).find();
39✔
565
        if (!key) {
39✔
566
            return false;
21✔
567
        }
21✔
568
        auto obj = table->get_object(key);
18✔
569
        bool did_run = perform_file_action(file_manager, obj);
18✔
570
        if (did_run)
18✔
571
            obj.remove();
15✔
572
        realm->commit_transaction();
18✔
573
        return did_run;
18✔
574
    }
39✔
575

576
    bool has_logged_in_user(std::string_view user_id) override
577
    {
74✔
578
        auto realm = get_realm();
74✔
579
        auto obj = find_user(*realm, user_id);
74✔
580
        return is_valid_user(obj);
74✔
581
    }
74✔
582

583
    std::optional<UserData> get_user(std::string_view user_id) override
584
    {
2,081✔
585
        auto realm = get_realm();
2,081✔
586
        return read_user(find_user(*realm, user_id));
2,081✔
587
    }
2,081✔
588

589
    void create_user(std::string_view user_id, std::string_view refresh_token, std::string_view access_token,
590
                     std::string_view device_id) override
591
    {
1,163✔
592
        auto realm = get_realm();
1,163✔
593
        realm->begin_transaction();
1,163✔
594

595
        auto& schema = m_user_schema;
1,163✔
596
        Obj obj = find_user(*realm, user_id);
1,163✔
597
        if (!obj) {
1,163✔
598
            obj = realm->read_group().get_table(m_user_schema.table_key)->create_object();
1,133✔
599
            obj.set<String>(schema.user_id_col, user_id);
1,133✔
600

601
            // Mark the user we just created as the current user
602
            Obj current_user = current_user_obj(*realm);
1,133✔
603
            current_user.set<String>(m_current_user_schema.user_id, user_id);
1,133✔
604
        }
1,133✔
605

606
        obj.set(schema.state_col, (int64_t)UserState::LoggedIn);
1,163✔
607
        obj.set<String>(schema.refresh_token_col, refresh_token);
1,163✔
608
        obj.set<String>(schema.access_token_col, access_token);
1,163✔
609
        obj.set<String>(schema.device_id_col, device_id);
1,163✔
610

611
        realm->commit_transaction();
1,163✔
612
    }
1,163✔
613

614
    void update_user(std::string_view user_id, util::FunctionRef<void(UserData&)> update_fn) override
615
    {
1,134✔
616
        auto realm = get_realm();
1,134✔
617
        realm->begin_transaction();
1,134✔
618
        auto& schema = m_user_schema;
1,134✔
619
        Obj obj = find_user(*realm, user_id);
1,134✔
620
        auto opt_data = read_user(obj);
1,134✔
621
        if (!opt_data) {
1,134✔
NEW
622
            realm->cancel_transaction();
×
NEW
623
            return;
×
NEW
624
        }
×
625

626
        auto& data = *opt_data;
1,134✔
627
        update_fn(data);
1,134✔
628

629
        obj.set(schema.state_col, int64_t(data.access_token ? UserState::LoggedIn : UserState::LoggedOut));
1,134✔
630
        obj.set<String>(schema.refresh_token_col, data.refresh_token.token);
1,134✔
631
        obj.set<String>(schema.access_token_col, data.access_token.token);
1,134✔
632
        obj.set<String>(schema.device_id_col, data.device_id);
1,134✔
633

634
        std::stringstream profile;
1,134✔
635
        profile << data.profile.data();
1,134✔
636
        obj.set(schema.profile_dump_col, profile.str());
1,134✔
637

638
        auto identities_list = obj.get_linklist(schema.identities_col);
1,134✔
639
        identities_list.clear();
1,134✔
640

641
        for (auto& ident : data.identities) {
1,138✔
642
            auto obj = identities_list.create_and_insert_linked_object(identities_list.size());
1,138✔
643
            obj.set<String>(m_user_identity_schema.user_id, ident.id);
1,138✔
644
            obj.set<String>(m_user_identity_schema.provider_id, ident.provider_type);
1,138✔
645
        }
1,138✔
646

647
        // intentionally does not update `legacy_identities` as that field is
648
        // read-only and no longer used
649

650
        realm->commit_transaction();
1,134✔
651
    }
1,134✔
652

653
    Obj current_user_obj(Realm& realm) const
654
    {
3,341✔
655
        TableRef current_user_table = realm.read_group().get_table(m_current_user_schema.table_key);
3,341✔
656
        Obj obj;
3,341✔
657
        if (!current_user_table->is_empty())
3,341✔
658
            obj = *current_user_table->begin();
2,721✔
659
        else if (realm.is_in_transaction())
620✔
660
            obj = current_user_table->create_object();
616✔
661
        return obj;
3,341✔
662
    }
3,341✔
663

664
    // Some of our string columns are nullable. They never should actually be
665
    // null as we store "" rather than null when the value isn't present, but
666
    // be safe and handle it anyway.
667
    static std::string get_string(const Obj& obj, ColKey col)
668
    {
11,596✔
669
        auto str = obj.get<String>(col);
11,596✔
670
        return str.is_null() ? "" : str;
11,596✔
671
    }
11,596✔
672

673
    std::optional<app::UserData> read_user(const Obj& obj) const
674
    {
3,215✔
675
        if (!obj) {
3,215✔
676
            return {};
×
677
        }
×
678
        auto state = obj.get<UserState>(m_user_schema.state_col);
3,215✔
679
        if (state == UserState::Removed) {
3,215✔
680
            return {};
23✔
681
        }
23✔
682

683
        UserData data;
3,192✔
684
        if (state == UserState::LoggedIn) {
3,192✔
685
            try {
3,173✔
686
                data.access_token = RealmJWT(get_string(obj, m_user_schema.access_token_col));
3,173✔
687
                data.refresh_token = RealmJWT(get_string(obj, m_user_schema.refresh_token_col));
3,173✔
688
            }
3,173✔
689
            catch (...) {
3,173✔
690
                // Invalid stored token results in a logged-out user
691
                data.access_token = {};
2✔
692
                data.refresh_token = {};
2✔
693
            }
2✔
694
        }
3,173✔
695

696
        data.device_id = get_string(obj, m_user_schema.device_id_col);
3,192✔
697
        if (auto profile = obj.get<String>(m_user_schema.profile_dump_col); profile.size()) {
3,192✔
698
            data.profile = static_cast<bson::BsonDocument>(bson::parse(std::string_view(profile)));
1,016✔
699
        }
1,016✔
700

701
        auto identities_list = obj.get_linklist(m_user_schema.identities_col);
3,192✔
702
        auto identities_table = identities_list.get_target_table();
3,192✔
703
        data.identities.reserve(identities_list.size());
3,192✔
704
        for (size_t i = 0, size = identities_list.size(); i < size; ++i) {
4,248✔
705
            auto obj = identities_table->get_object(identities_list.get(i));
1,056✔
706
            data.identities.push_back({obj.get<String>(m_user_identity_schema.user_id),
1,056✔
707
                                       obj.get<String>(m_user_identity_schema.provider_id)});
1,056✔
708
        }
1,056✔
709

710
        auto legacy_identities = obj.get_list<String>(m_user_schema.legacy_uuids_col);
3,192✔
711
        data.legacy_identities.reserve(legacy_identities.size());
3,192✔
712
        for (size_t i = 0, size = legacy_identities.size(); i < size; ++i) {
3,224✔
713
            data.legacy_identities.push_back(legacy_identities.get(i));
32✔
714
        }
32✔
715

716
        return data;
3,192✔
717
    }
3,192✔
718

719
    void update_current_user(Realm& realm, std::string_view removed_user_id)
720
    {
81✔
721
        auto current_user = current_user_obj(realm);
81✔
722
        if (current_user.get<String>(m_current_user_schema.user_id) == removed_user_id) {
81✔
723
            // Set to either empty or the first still logged in user
724
            current_user.set(m_current_user_schema.user_id, get_current_user());
72✔
725
        }
72✔
726
    }
81✔
727

728
    void log_out(std::string_view user_id, UserState new_state) override
729
    {
63✔
730
        REALM_ASSERT(new_state != UserState::LoggedIn);
63✔
731
        auto realm = get_realm();
63✔
732
        realm->begin_transaction();
63✔
733
        if (auto obj = find_user(*realm, user_id)) {
63✔
734
            obj.set(m_user_schema.state_col, (int64_t)new_state);
63✔
735
            obj.set<String>(m_user_schema.access_token_col, "");
63✔
736
            obj.set<String>(m_user_schema.refresh_token_col, "");
63✔
737
            update_current_user(*realm, user_id);
63✔
738
        }
63✔
739
        realm->commit_transaction();
63✔
740
    }
63✔
741

742
    void delete_user(SyncFileManager& file_manager, std::string_view user_id) override
743
    {
20✔
744
        auto realm = get_realm();
20✔
745
        realm->begin_transaction();
20✔
746
        if (auto obj = find_user(*realm, user_id)) {
20✔
747
            delete_user_realms(file_manager, obj); // also removes obj
18✔
748
            update_current_user(*realm, user_id);
18✔
749
        }
18✔
750
        realm->commit_transaction();
20✔
751
    }
20✔
752

753
    void add_realm_path(std::string_view user_id, std::string_view path) override
754
    {
17✔
755
        auto realm = get_realm();
17✔
756
        realm->begin_transaction();
17✔
757
        if (auto obj = find_user(*realm, user_id)) {
17✔
758
            obj.get_set<String>(m_user_schema.realm_file_paths_col).insert(path);
17✔
759
        }
17✔
760
        realm->commit_transaction();
17✔
761
    }
17✔
762

763
    bool is_valid_user(Obj& obj)
764
    {
1,137✔
765
        // This is overly cautious and merely checking the state should suffice,
766
        // but because this is a persisted file that can be modified it's possible
767
        // to get invalid combinations of data.
768
        return obj && obj.get<UserState>(m_user_schema.state_col) == UserState::LoggedIn &&
1,137✔
769
               RealmJWT::validate(get_string(obj, m_user_schema.access_token_col)) &&
1,137✔
770
               RealmJWT::validate(get_string(obj, m_user_schema.refresh_token_col));
1,137✔
771
    }
1,137✔
772

773
    std::vector<std::string> get_all_users() override
774
    {
46✔
775
        auto realm = get_realm();
46✔
776
        auto table = realm->read_group().get_table(m_user_schema.table_key);
46✔
777
        std::vector<std::string> users;
46✔
778
        users.reserve(table->size());
46✔
779
        for (auto& obj : *table) {
62✔
780
            if (obj.get<UserState>(m_user_schema.state_col) != UserState::Removed) {
62✔
781
                users.emplace_back(obj.get<String>(m_user_schema.user_id_col));
44✔
782
            }
44✔
783
        }
62✔
784
        return users;
46✔
785
    }
46✔
786

787
    std::string get_current_user() override
788
    {
1,029✔
789
        auto realm = get_realm();
1,029✔
790
        if (auto obj = current_user_obj(*realm)) {
1,029✔
791
            auto user_id = obj.get<String>(m_current_user_schema.user_id);
1,025✔
792
            auto user_obj = find_user(*realm, user_id);
1,025✔
793
            if (is_valid_user(user_obj)) {
1,025✔
794
                return user_id;
993✔
795
            }
993✔
796
        }
1,025✔
797

798
        auto table = realm->read_group().get_table(m_user_schema.table_key);
36✔
799
        for (auto& obj : *table) {
38✔
800
            if (is_valid_user(obj)) {
38✔
801
                return obj.get<String>(m_user_schema.user_id_col);
10✔
802
            }
10✔
803
        }
38✔
804

805
        return "";
26✔
806
    }
36✔
807

808
    void set_current_user(std::string_view user_id) override
809
    {
1,098✔
810
        auto realm = get_realm();
1,098✔
811
        realm->begin_transaction();
1,098✔
812
        current_user_obj(*realm).set<String>(m_current_user_schema.user_id, user_id);
1,098✔
813
        realm->commit_transaction();
1,098✔
814
    }
1,098✔
815

816
    void create_file_action(SyncFileAction action, std::string_view original_path,
817
                            std::string_view recovery_path) override
818
    {
87✔
819
        REALM_ASSERT(action != SyncFileAction::BackUpThenDeleteRealm || !recovery_path.empty());
87✔
820

821
        auto realm = get_realm();
87✔
822
        realm->begin_transaction();
87✔
823
        TableRef table = realm->read_group().get_table(m_file_action_schema.table_key);
87✔
824
        Obj obj = table->create_object_with_primary_key(original_path);
87✔
825
        obj.set(m_file_action_schema.idx_new_name, recovery_path);
87✔
826
        obj.set(m_file_action_schema.idx_action, static_cast<int64_t>(action));
87✔
827
        // There's also partition and user_id fields in the schema, but they
828
        // aren't actually used for anything and are never read
829
        realm->commit_transaction();
87✔
830
    }
87✔
831

832
    Obj find_user(Realm& realm, StringData user_id) const
833
    {
5,577✔
834
        Obj obj;
5,577✔
835
        if (user_id.size() == 0)
5,577✔
836
            return obj;
8✔
837

838
        auto table = realm.read_group().get_table(m_user_schema.table_key);
5,569✔
839
        Query q = table->where().equal(m_user_schema.user_id_col, user_id);
5,569✔
840
        REALM_ASSERT_DEBUG(q.count() < 2); // user_id_col ought to be a primary key
5,569✔
841
        if (auto key = q.find())
5,569✔
842
            obj = table->get_object(key);
4,394✔
843
        return obj;
5,569✔
844
    }
5,577✔
845
};
846

847
class InMemoryMetadataStorage : public app::MetadataStore {
848
    std::mutex m_mutex;
849
    std::map<std::string, UserData, std::less<>> m_users;
850
    std::map<std::string, std::set<std::string>, std::less<>> m_realm_paths;
851
    std::string m_active_user;
852
    struct FileAction {
853
        SyncFileAction action;
854
        std::string backup_path;
855
    };
856
    std::map<std::string, FileAction, std::less<>> m_file_actions;
857

858
    bool has_logged_in_user(std::string_view user_id) override
859
    {
60✔
860
        std::lock_guard lock(m_mutex);
60✔
861
        auto it = m_users.find(user_id);
60✔
862
        return it != m_users.end() && it->second.access_token;
60✔
863
    }
60✔
864

865
    std::optional<UserData> get_user(std::string_view user_id) override
866
    {
7,544✔
867
        std::lock_guard lock(m_mutex);
7,544✔
868
        if (auto it = m_users.find(user_id); it != m_users.end()) {
7,544✔
869
            return it->second;
7,528✔
870
        }
7,528✔
871
        return {};
16✔
872
    }
7,544✔
873

874
    void create_user(std::string_view user_id, std::string_view refresh_token, std::string_view access_token,
875
                     std::string_view device_id) override
876
    {
3,842✔
877
        std::lock_guard lock(m_mutex);
3,842✔
878
        auto it = m_users.find(user_id);
3,842✔
879
        if (it == m_users.end()) {
3,842✔
880
            it = m_users.insert({std::string(user_id), UserData{}}).first;
3,824✔
881
            m_active_user = user_id;
3,824✔
882
        }
3,824✔
883
        auto& user = it->second;
3,842✔
884
        user.device_id = device_id;
3,842✔
885
        try {
3,842✔
886
            user.refresh_token = RealmJWT(refresh_token);
3,842✔
887
            user.access_token = RealmJWT(access_token);
3,842✔
888
        }
3,842✔
889
        catch (...) {
3,842✔
890
            user.refresh_token = {};
16✔
891
            user.access_token = {};
16✔
892
        }
16✔
893
    }
3,842✔
894

895
    void update_user(std::string_view user_id, util::FunctionRef<void(UserData&)> update_fn) override
896
    {
3,810✔
897
        std::lock_guard lock(m_mutex);
3,810✔
898
        auto it = m_users.find(user_id);
3,810✔
899
        if (it == m_users.end()) {
3,810✔
NEW
900
            return;
×
NEW
901
        }
×
902

903
        update_fn(it->second);
3,810✔
904
        it->second.legacy_identities.clear();
3,810✔
905
    }
3,810✔
906

907
    void log_out(std::string_view user_id, SyncUser::State new_state) override
908
    {
34✔
909
        std::lock_guard lock(m_mutex);
34✔
910
        if (auto it = m_users.find(user_id); it != m_users.end()) {
34✔
911
            if (new_state == SyncUser::State::Removed) {
34✔
912
                m_users.erase(it);
22✔
913
            }
22✔
914
            else {
12✔
915
                auto& user = it->second;
12✔
916
                user.access_token = {};
12✔
917
                user.refresh_token = {};
12✔
918
                user.device_id.clear();
12✔
919
            }
12✔
920
        }
34✔
921
    }
34✔
922

923
    void delete_user(SyncFileManager& file_manager, std::string_view user_id) override
924
    {
12✔
925
        std::lock_guard lock(m_mutex);
12✔
926
        if (auto it = m_users.find(user_id); it != m_users.end()) {
12✔
927
            m_users.erase(it);
10✔
928
        }
10✔
929
        if (auto it = m_realm_paths.find(user_id); it != m_realm_paths.end()) {
12✔
930
            for (auto& path : it->second) {
4✔
931
                file_manager.remove_realm(path);
4✔
932
            }
4✔
933
        }
2✔
934
    }
12✔
935

936
    std::string get_current_user() override
937
    {
3,782✔
938
        std::lock_guard lock(m_mutex);
3,782✔
939
        if (auto it = m_users.find(m_active_user); it != m_users.end() && it->second.access_token) {
3,782✔
940
            return m_active_user;
3,742✔
941
        }
3,742✔
942

943
        for (auto& [user_id, data] : m_users) {
40✔
944
            if (data.access_token) {
24✔
945
                m_active_user = user_id;
14✔
946
                return user_id;
14✔
947
            }
14✔
948
        }
24✔
949

950
        return "";
26✔
951
    }
40✔
952

953
    void set_current_user(std::string_view user_id) override
954
    {
3,804✔
955
        std::lock_guard lock(m_mutex);
3,804✔
956
        m_active_user = user_id;
3,804✔
957
    }
3,804✔
958

959
    std::vector<std::string> get_all_users() override
960
    {
60✔
961
        std::lock_guard lock(m_mutex);
60✔
962
        std::vector<std::string> users;
60✔
963
        for (auto& [user_id, _] : m_users) {
62✔
964
            users.push_back(user_id);
62✔
965
        }
62✔
966
        return users;
60✔
967
    }
60✔
968

969
    void add_realm_path(std::string_view user_id, std::string_view path) override
970
    {
36✔
971
        std::lock_guard lock(m_mutex);
36✔
972
        m_realm_paths[std::string(user_id)].insert(std::string(path));
36✔
973
    }
36✔
974

975
    bool immediately_run_file_actions(SyncFileManager& file_manager, std::string_view path) override
976
    {
29✔
977
        std::lock_guard lock(m_mutex);
29✔
978
        auto it = m_file_actions.find(path);
29✔
979
        if (it == m_file_actions.end())
29✔
980
            return false;
13✔
981
        auto& old_path = it->first;
16✔
982
        switch (it->second.action) {
16✔
983
            case SyncFileAction::DeleteRealm:
7✔
984
                if (file_manager.remove_realm(old_path)) {
7✔
985
                    m_file_actions.erase(it);
7✔
986
                    return true;
7✔
987
                }
7✔
988
                return false;
×
989

990
            case SyncFileAction::BackUpThenDeleteRealm:
9✔
991
                if (!util::File::exists(old_path)) {
9✔
992
                    m_file_actions.erase(it);
2✔
993
                    return true;
2✔
994
                }
2✔
995
                auto& new_path = it->second.backup_path;
7✔
996
                if (!file_manager.copy_realm_file(old_path, new_path)) {
7✔
997
                    return false;
2✔
998
                }
2✔
999
                if (file_manager.remove_realm(old_path)) {
5✔
1000
                    m_file_actions.erase(it);
4✔
1001
                    return true;
4✔
1002
                }
4✔
1003
                it->second.action = SyncFileAction::DeleteRealm;
1✔
1004
                return false;
1✔
1005
        }
16✔
1006
        return false;
×
1007
    }
16✔
1008

1009
    void create_file_action(SyncFileAction action, std::string_view path, std::string_view backup_path) override
1010
    {
21✔
1011
        std::lock_guard lock(m_mutex);
21✔
1012
        REALM_ASSERT(action != SyncFileAction::BackUpThenDeleteRealm || !backup_path.empty());
21✔
1013
        m_file_actions[std::string(path)] = FileAction{action, std::string(backup_path)};
21✔
1014
    }
21✔
1015
};
1016

1017
} // anonymous namespace
1018

1019
app::MetadataStore::~MetadataStore() = default;
4,535✔
1020

1021
std::unique_ptr<app::MetadataStore> app::create_metadata_store(const AppConfig& config, SyncFileManager& file_manager)
1022
{
4,535✔
1023
    if (config.metadata_mode == AppConfig::MetadataMode::InMemory) {
4,535✔
1024
        return std::make_unique<InMemoryMetadataStorage>();
3,831✔
1025
    }
3,831✔
1026
    return std::make_unique<PersistedSyncMetadataManager>(config, file_manager);
704✔
1027
}
4,535✔
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2025 Coveralls, Inc