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

realm / realm-core / 2293

02 May 2024 08:09PM UTC coverage: 90.759% (+0.01%) from 90.747%
2293

push

Evergreen

web-flow
Fix a deadlock when accessing current user from inside an App listener (#7671)

App::switch_user() emitted changes without first releasing the lock on
m_user_mutex, leading to a deadlock if anyone inside the listener tried to
acquire the mutex. The rest of the places where we emitted changes were
correct.

The newly added wrapper catches this error when building with clang.

101946 of 180246 branches covered (56.56%)

14 of 17 new or added lines in 2 files covered. (82.35%)

67 existing lines in 15 files now uncovered.

212564 of 234207 relevant lines covered (90.76%)

5790527.56 hits per line

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

91.93
/src/realm/sync/noinst/client_reset.cpp
1
///////////////////////////////////////////////////////////////////////////
2
//
3
// Copyright 2021 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/transaction.hpp>
20
#include <realm/dictionary.hpp>
21
#include <realm/object_converter.hpp>
22
#include <realm/table_view.hpp>
23
#include <realm/set.hpp>
24

25
#include <realm/sync/history.hpp>
26
#include <realm/sync/changeset_parser.hpp>
27
#include <realm/sync/instruction_applier.hpp>
28
#include <realm/sync/noinst/client_history_impl.hpp>
29
#include <realm/sync/noinst/client_reset.hpp>
30
#include <realm/sync/noinst/client_reset_recovery.hpp>
31
#include <realm/sync/subscriptions.hpp>
32

33
#include <realm/util/compression.hpp>
34

35
#include <algorithm>
36
#include <chrono>
37
#include <vector>
38

39
using namespace realm;
40
using namespace _impl;
41
using namespace sync;
42

43
namespace realm {
44

45
std::ostream& operator<<(std::ostream& os, const ClientResyncMode& mode)
46
{
23,208✔
47
    switch (mode) {
23,208✔
48
        case ClientResyncMode::Manual:
✔
49
            os << "Manual";
×
50
            break;
×
51
        case ClientResyncMode::DiscardLocal:
11,454✔
52
            os << "DiscardLocal";
11,454✔
53
            break;
11,454✔
54
        case ClientResyncMode::Recover:
11,634✔
55
            os << "Recover";
11,634✔
56
            break;
11,634✔
57
        case ClientResyncMode::RecoverOrDiscard:
120✔
58
            os << "RecoverOrDiscard";
120✔
59
            break;
120✔
60
    }
23,208✔
61
    return os;
23,208✔
62
}
23,208✔
63

64
} // namespace realm
65

66
namespace realm::_impl::client_reset {
67

68
static inline bool should_skip_table(const Transaction& group, TableKey key)
69
{
247,004✔
70
    return !group.table_is_public(key);
247,004✔
71
}
247,004✔
72

73
void transfer_group(const Transaction& group_src, Transaction& group_dst, util::Logger& logger,
74
                    bool allow_schema_additions)
75
{
7,520✔
76
    logger.debug(util::LogCategory::reset,
7,520✔
77
                 "transfer_group, src size = %1, dst size = %2, allow_schema_additions = %3", group_src.size(),
7,520✔
78
                 group_dst.size(), allow_schema_additions);
7,520✔
79

80
    // Turn off the sync history tracking during state transfer since it will be thrown
81
    // away immediately after anyways. This reduces the memory footprint of a client reset.
82
    ClientReplication* client_repl = dynamic_cast<ClientReplication*>(group_dst.get_replication());
7,520✔
83
    REALM_ASSERT_RELEASE(client_repl);
7,520✔
84
    TempShortCircuitReplication sync_history_guard(*client_repl);
7,520✔
85

86
    // Find all tables in dst that should be removed.
87
    std::set<std::string> tables_to_remove;
7,520✔
88
    for (auto table_key : group_dst.get_table_keys()) {
33,744✔
89
        if (should_skip_table(group_dst, table_key))
33,744✔
90
            continue;
15,796✔
91
        StringData table_name = group_dst.get_table_name(table_key);
17,948✔
92
        logger.debug(util::LogCategory::reset, "key = %1, table_name = %2", table_key.value, table_name);
17,948✔
93
        ConstTableRef table_src = group_src.get_table(table_name);
17,948✔
94
        if (!table_src) {
17,948✔
95
            logger.debug(util::LogCategory::reset, "Table '%1' will be removed", table_name);
28✔
96
            tables_to_remove.insert(table_name);
28✔
97
            continue;
28✔
98
        }
28✔
99
        // Check whether the table type is the same.
100
        TableRef table_dst = group_dst.get_table(table_key);
17,920✔
101
        auto pk_col_src = table_src->get_primary_key_column();
17,920✔
102
        auto pk_col_dst = table_dst->get_primary_key_column();
17,920✔
103
        bool has_pk_src = bool(pk_col_src);
17,920✔
104
        bool has_pk_dst = bool(pk_col_dst);
17,920✔
105
        if (has_pk_src != has_pk_dst) {
17,920✔
106
            throw ClientResetFailed(util::format("Client reset requires a primary key column in %1 table '%2'",
×
107
                                                 (has_pk_src ? "dest" : "source"), table_name));
×
108
        }
×
109
        if (!has_pk_src)
17,920✔
110
            continue;
672✔
111

112
        // Now the tables both have primary keys. Check type.
113
        if (pk_col_src.get_type() != pk_col_dst.get_type()) {
17,248✔
114
            throw ClientResetFailed(
4✔
115
                util::format("Client reset found incompatible primary key types (%1 vs %2) on '%3'",
4✔
116
                             pk_col_src.get_type(), pk_col_dst.get_type(), table_name));
4✔
117
        }
4✔
118
        // Check collection type, nullability etc. but having an index doesn't matter;
119
        ColumnAttrMask pk_col_src_attr = pk_col_src.get_attrs();
17,244✔
120
        ColumnAttrMask pk_col_dst_attr = pk_col_dst.get_attrs();
17,244✔
121
        pk_col_src_attr.reset(ColumnAttr::col_attr_Indexed);
17,244✔
122
        pk_col_dst_attr.reset(ColumnAttr::col_attr_Indexed);
17,244✔
123
        if (pk_col_src_attr != pk_col_dst_attr) {
17,244✔
124
            throw ClientResetFailed(
×
125
                util::format("Client reset found incompatible primary key attributes (%1 vs %2) on '%3'",
×
126
                             pk_col_src.value, pk_col_dst.value, table_name));
×
127
        }
×
128
        // Check name.
129
        StringData pk_col_name_src = table_src->get_column_name(pk_col_src);
17,244✔
130
        StringData pk_col_name_dst = table_dst->get_column_name(pk_col_dst);
17,244✔
131
        if (pk_col_name_src != pk_col_name_dst) {
17,244✔
132
            throw ClientResetFailed(
×
133
                util::format("Client reset requires equal pk column names but '%1' != '%2' on '%3'", pk_col_name_src,
×
134
                             pk_col_name_dst, table_name));
×
135
        }
×
136
        // The table survives.
137
        logger.debug(util::LogCategory::reset, "Table '%1' will remain", table_name);
17,244✔
138
    }
17,244✔
139

140
    // If there have been any tables marked for removal stop.
141
    // We consider two possible options for recovery:
142
    // 1: Remove the tables. But this will generate destructive schema
143
    //    schema changes that the local Realm cannot advance through.
144
    //    Since this action will fail down the line anyway, give up now.
145
    // 2: Keep the tables locally and ignore them. But the local app schema
146
    //    still has these classes and trying to modify anything in them will
147
    //    create sync instructions on tables that sync doesn't know about.
148
    // As an exception in recovery mode, we assume that the corresponding
149
    // additive schema changes will be part of the recovery upload. If they
150
    // are present, then the server can choose to allow them (if in dev mode).
151
    // If they are not present, then the server will emit an error the next time
152
    // a value is set on the unknown property.
153
    if (!allow_schema_additions && !tables_to_remove.empty()) {
7,516✔
154
        std::string names_list;
20✔
155
        for (const std::string& table_name : tables_to_remove) {
28✔
156
            names_list += Group::table_name_to_class_name(table_name);
28✔
157
            names_list += ", ";
28✔
158
        }
28✔
159
        if (names_list.size() > 2) {
20✔
160
            // remove the final ", "
161
            names_list = names_list.substr(0, names_list.size() - 2);
20✔
162
        }
20✔
163
        throw ClientResetFailed(
20✔
164
            util::format("Client reset cannot recover when classes have been removed: {%1}", names_list));
20✔
165
    }
20✔
166

167
    // Create new tables in dst if needed.
168
    for (auto table_key : group_src.get_table_keys()) {
25,676✔
169
        if (should_skip_table(group_src, table_key))
25,676✔
170
            continue;
7,760✔
171
        ConstTableRef table_src = group_src.get_table(table_key);
17,916✔
172
        StringData table_name = table_src->get_name();
17,916✔
173
        auto pk_col_src = table_src->get_primary_key_column();
17,916✔
174
        TableRef table_dst = group_dst.get_table(table_name);
17,916✔
175
        if (!table_dst) {
17,916✔
176
            // Create the table.
177
            if (table_src->is_embedded()) {
48✔
178
                REALM_ASSERT(!pk_col_src);
16✔
179
                group_dst.add_table(table_name, Table::Type::Embedded);
16✔
180
            }
16✔
181
            else {
32✔
182
                REALM_ASSERT(pk_col_src); // a sync table will have a pk
32✔
183
                auto pk_col_src = table_src->get_primary_key_column();
32✔
184
                DataType pk_type = DataType(pk_col_src.get_type());
32✔
185
                StringData pk_col_name = table_src->get_column_name(pk_col_src);
32✔
186
                group_dst.add_table_with_primary_key(table_name, pk_type, pk_col_name, pk_col_src.is_nullable(),
32✔
187
                                                     table_src->get_table_type());
32✔
188
            }
32✔
189
        }
48✔
190
    }
17,916✔
191

192
    // Now the class tables are identical.
193
    size_t num_tables;
7,496✔
194
    {
7,496✔
195
        size_t num_tables_src = 0;
7,496✔
196
        for (auto table_key : group_src.get_table_keys()) {
25,676✔
197
            if (!should_skip_table(group_src, table_key))
25,676✔
198
                ++num_tables_src;
17,916✔
199
        }
25,676✔
200
        size_t num_tables_dst = 0;
7,496✔
201
        for (auto table_key : group_dst.get_table_keys()) {
33,648✔
202
            if (!should_skip_table(group_dst, table_key))
33,648✔
203
                ++num_tables_dst;
17,916✔
204
        }
33,648✔
205
        REALM_ASSERT_EX(allow_schema_additions || num_tables_src == num_tables_dst, num_tables_src, num_tables_dst);
7,496✔
206
        num_tables = num_tables_src;
7,496✔
207
    }
7,496✔
208
    logger.debug(util::LogCategory::reset, "The number of tables is %1", num_tables);
7,496✔
209

210
    // Remove columns in dst if they are absent in src.
211
    for (auto table_key : group_src.get_table_keys()) {
25,672✔
212
        if (should_skip_table(group_src, table_key))
25,672✔
213
            continue;
7,760✔
214
        ConstTableRef table_src = group_src.get_table(table_key);
17,912✔
215
        StringData table_name = table_src->get_name();
17,912✔
216
        TableRef table_dst = group_dst.get_table(table_name);
17,912✔
217
        REALM_ASSERT(table_dst);
17,912✔
218
        std::vector<std::string> columns_to_remove;
17,912✔
219
        for (ColKey col_key : table_dst->get_column_keys()) {
57,492✔
220
            StringData col_name = table_dst->get_column_name(col_key);
57,492✔
221
            ColKey col_key_src = table_src->get_column_key(col_name);
57,492✔
222
            if (!col_key_src) {
57,492✔
223
                columns_to_remove.push_back(col_name);
12✔
224
                continue;
12✔
225
            }
12✔
226
        }
57,492✔
227
        if (!allow_schema_additions && !columns_to_remove.empty()) {
17,912✔
228
            std::string columns_list;
4✔
229
            for (const std::string& col_name : columns_to_remove) {
12✔
230
                columns_list += col_name;
12✔
231
                columns_list += ", ";
12✔
232
            }
12✔
233
            throw ClientResetFailed(
4✔
234
                util::format("Client reset cannot recover when columns have been removed from '%1': {%2}", table_name,
4✔
235
                             columns_list));
4✔
236
        }
4✔
237
    }
17,912✔
238

239
    // Add columns in dst if present in src and absent in dst.
240
    for (auto table_key : group_src.get_table_keys()) {
25,656✔
241
        if (should_skip_table(group_src, table_key))
25,656✔
242
            continue;
7,760✔
243
        ConstTableRef table_src = group_src.get_table(table_key);
17,896✔
244
        StringData table_name = table_src->get_name();
17,896✔
245
        TableRef table_dst = group_dst.get_table(table_name);
17,896✔
246
        REALM_ASSERT(table_dst);
17,896✔
247
        for (ColKey col_key : table_src->get_column_keys()) {
57,572✔
248
            StringData col_name = table_src->get_column_name(col_key);
57,572✔
249
            ColKey col_key_dst = table_dst->get_column_key(col_name);
57,572✔
250
            if (!col_key_dst) {
57,572✔
251
                DataType col_type = table_src->get_column_type(col_key);
152✔
252
                bool nullable = col_key.is_nullable();
152✔
253
                auto search_index_type = table_src->search_index_type(col_key);
152✔
254
                logger.trace(util::LogCategory::reset,
152✔
255
                             "Create column, table = %1, column name = %2, "
152✔
256
                             " type = %3, nullable = %4, search_index = %5",
152✔
257
                             table_name, col_name, col_key.get_type(), nullable, search_index_type);
152✔
258
                ColKey col_key_dst;
152✔
259
                if (Table::is_link_type(col_key.get_type())) {
152✔
260
                    ConstTableRef target_src = table_src->get_link_target(col_key);
48✔
261
                    TableRef target_dst = group_dst.get_table(target_src->get_name());
48✔
262
                    if (col_key.is_list()) {
48✔
263
                        col_key_dst = table_dst->add_column_list(*target_dst, col_name);
16✔
264
                    }
16✔
265
                    else if (col_key.is_set()) {
32✔
266
                        col_key_dst = table_dst->add_column_set(*target_dst, col_name);
×
267
                    }
×
268
                    else if (col_key.is_dictionary()) {
32✔
269
                        DataType key_type = table_src->get_dictionary_key_type(col_key);
8✔
270
                        col_key_dst = table_dst->add_column_dictionary(*target_dst, col_name, key_type);
8✔
271
                    }
8✔
272
                    else {
24✔
273
                        REALM_ASSERT(!col_key.is_collection());
24✔
274
                        col_key_dst = table_dst->add_column(*target_dst, col_name);
24✔
275
                    }
24✔
276
                }
48✔
277
                else if (col_key.is_list()) {
104✔
278
                    col_key_dst = table_dst->add_column_list(col_type, col_name, nullable);
8✔
279
                }
8✔
280
                else if (col_key.is_set()) {
96✔
281
                    col_key_dst = table_dst->add_column_set(col_type, col_name, nullable);
8✔
282
                }
8✔
283
                else if (col_key.is_dictionary()) {
88✔
284
                    DataType key_type = table_src->get_dictionary_key_type(col_key);
8✔
285
                    col_key_dst = table_dst->add_column_dictionary(col_type, col_name, nullable, key_type);
8✔
286
                }
8✔
287
                else {
80✔
288
                    REALM_ASSERT(!col_key.is_collection());
80✔
289
                    col_key_dst = table_dst->add_column(col_type, col_name, nullable);
80✔
290
                }
80✔
291

292
                if (search_index_type != IndexType::None)
152✔
293
                    table_dst->add_search_index(col_key_dst, search_index_type);
×
294
            }
152✔
295
            else {
57,420✔
296
                // column preexists in dest, make sure the types match
297
                if (col_key.get_type() != col_key_dst.get_type()) {
57,420✔
298
                    throw ClientResetFailed(util::format(
4✔
299
                        "Incompatible column type change detected during client reset for '%1.%2' (%3 vs %4)",
4✔
300
                        table_name, col_name, col_key.get_type(), col_key_dst.get_type()));
4✔
301
                }
4✔
302
                ColumnAttrMask src_col_attrs = col_key.get_attrs();
57,416✔
303
                ColumnAttrMask dst_col_attrs = col_key_dst.get_attrs();
57,416✔
304
                src_col_attrs.reset(ColumnAttr::col_attr_Indexed);
57,416✔
305
                dst_col_attrs.reset(ColumnAttr::col_attr_Indexed);
57,416✔
306
                // make sure the attributes such as collection type, nullability etc. match
307
                // but index equality doesn't matter here.
308
                if (src_col_attrs != dst_col_attrs) {
57,416✔
309
                    throw ClientResetFailed(util::format(
×
310
                        "Incompatable column attribute change detected during client reset for '%1.%2' (%3 vs %4)",
×
311
                        table_name, col_name, col_key.value, col_key_dst.value));
×
312
                }
×
313
            }
57,416✔
314
        }
57,572✔
315
    }
17,896✔
316

317
    // Now the schemas are identical.
318

319
    // Remove objects in dst that are absent in src.
320
    for (auto table_key : group_src.get_table_keys()) {
25,644✔
321
        if (should_skip_table(group_src, table_key))
25,644✔
322
            continue;
7,760✔
323
        auto table_src = group_src.get_table(table_key);
17,884✔
324
        // There are no primary keys in embedded tables but this is ok, because
325
        // embedded objects are tied to the lifetime of top level objects.
326
        if (table_src->is_embedded())
17,884✔
327
            continue;
688✔
328
        StringData table_name = table_src->get_name();
17,196✔
329
        logger.debug(util::LogCategory::reset, "Removing objects in '%1'", table_name);
17,196✔
330
        auto table_dst = group_dst.get_table(table_name);
17,196✔
331

332
        auto pk_col = table_dst->get_primary_key_column();
17,196✔
333
        REALM_ASSERT_DEBUG(pk_col); // sync realms always have a pk
17,196✔
334
        std::vector<std::pair<Mixed, ObjKey>> objects_to_remove;
17,196✔
335
        for (auto obj : *table_dst) {
25,112✔
336
            auto pk = obj.get_any(pk_col);
25,112✔
337
            if (!table_src->find_primary_key(pk)) {
25,112✔
338
                objects_to_remove.emplace_back(pk, obj.get_key());
828✔
339
            }
828✔
340
        }
25,112✔
341
        for (auto& pair : objects_to_remove) {
17,196✔
342
            logger.debug(util::LogCategory::reset, "  removing '%1'", pair.first);
828✔
343
            table_dst->remove_object(pair.second);
828✔
344
        }
828✔
345
    }
17,196✔
346

347
    // We must re-create any missing objects that are absent in dst before trying to copy
348
    // their properties because creating them may re-create any dangling links which would
349
    // otherwise cause inconsistencies when re-creating lists of links.
350
    for (auto table_key : group_src.get_table_keys()) {
25,644✔
351
        ConstTableRef table_src = group_src.get_table(table_key);
25,644✔
352
        auto table_name = table_src->get_name();
25,644✔
353
        if (should_skip_table(group_src, table_key) || table_src->is_embedded())
25,644✔
354
            continue;
8,448✔
355
        TableRef table_dst = group_dst.get_table(table_name);
17,196✔
356
        auto pk_col = table_src->get_primary_key_column();
17,196✔
357
        REALM_ASSERT(pk_col);
17,196✔
358
        logger.debug(util::LogCategory::reset,
17,196✔
359
                     "Creating missing objects for table '%1', number of rows = %2, "
17,196✔
360
                     "primary_key_col = %3, primary_key_type = %4",
17,196✔
361
                     table_name, table_src->size(), pk_col.get_index().val, pk_col.get_type());
17,196✔
362
        for (const Obj& src : *table_src) {
24,976✔
363
            bool created = false;
24,976✔
364
            table_dst->create_object_with_primary_key(src.get_primary_key(), &created);
24,976✔
365
            if (created) {
24,976✔
366
                logger.debug(util::LogCategory::reset, "   created %1", src.get_primary_key());
692✔
367
            }
692✔
368
        }
24,976✔
369
    }
17,196✔
370

371
    converters::EmbeddedObjectConverter embedded_tracker;
7,488✔
372
    // Now src and dst have identical schemas and all the top level objects are created.
373
    // What is left to do is to diff all properties of the existing objects.
374
    // Embedded objects are created on the fly.
375
    for (auto table_key : group_src.get_table_keys()) {
25,644✔
376
        if (should_skip_table(group_src, table_key))
25,644✔
377
            continue;
7,760✔
378
        ConstTableRef table_src = group_src.get_table(table_key);
17,884✔
379
        // Embedded objects don't have a primary key, so they are handled
380
        // as a special case when they are encountered as a link value.
381
        if (table_src->is_embedded())
17,884✔
382
            continue;
688✔
383
        StringData table_name = table_src->get_name();
17,196✔
384
        TableRef table_dst = group_dst.get_table(table_name);
17,196✔
385
        REALM_ASSERT_EX(allow_schema_additions || table_src->get_column_count() == table_dst->get_column_count(),
17,196✔
386
                        allow_schema_additions, table_src->get_column_count(), table_dst->get_column_count());
17,196✔
387
        auto pk_col = table_src->get_primary_key_column();
17,196✔
388
        REALM_ASSERT(pk_col);
17,196✔
389
        logger.debug(util::LogCategory::reset,
17,196✔
390
                     "Updating values for table '%1', number of rows = %2, "
17,196✔
391
                     "number of columns = %3, primary_key_col = %4, "
17,196✔
392
                     "primary_key_type = %5",
17,196✔
393
                     table_name, table_src->size(), table_src->get_column_count(), pk_col.get_index().val,
17,196✔
394
                     pk_col.get_type());
17,196✔
395

396
        converters::InterRealmObjectConverter converter(table_src, table_dst, &embedded_tracker);
17,196✔
397

398
        for (const Obj& src : *table_src) {
24,976✔
399
            auto src_pk = src.get_primary_key();
24,976✔
400
            // create the object - it should have been created above.
401
            auto dst = table_dst->get_object_with_primary_key(src_pk);
24,976✔
402
            REALM_ASSERT(dst);
24,976✔
403

404
            bool updated = false;
24,976✔
405
            converter.copy(src, dst, &updated);
24,976✔
406
            if (updated) {
24,976✔
407
                logger.debug(util::LogCategory::reset, "  updating %1", src_pk);
7,856✔
408
            }
7,856✔
409
        }
24,976✔
410
        embedded_tracker.process_pending();
17,196✔
411
    }
17,196✔
412
}
7,488✔
413

414
// A table without a "class_" prefix will not generate sync instructions.
415
constexpr static std::string_view s_meta_reset_table_name("client_reset_metadata");
416
constexpr static std::string_view s_pk_col_name("id");
417
constexpr static std::string_view s_version_column_name("version");
418
constexpr static std::string_view s_timestamp_col_name("event_time");
419
constexpr static std::string_view s_reset_type_col_name("type_of_reset");
420
constexpr int64_t metadata_version = 1;
421

422
void remove_pending_client_resets(Transaction& wt)
423
{
266✔
424
    if (auto table = wt.get_table(s_meta_reset_table_name); table && !table->is_empty()) {
266✔
425
        table->clear();
266✔
426
    }
266✔
427
}
266✔
428

429
util::Optional<PendingReset> has_pending_reset(const Transaction& rt)
430
{
18,306✔
431
    ConstTableRef table = rt.get_table(s_meta_reset_table_name);
18,306✔
432
    if (!table || table->size() == 0) {
18,306✔
433
        return util::none;
17,488✔
434
    }
17,488✔
435
    ColKey timestamp_col = table->get_column_key(s_timestamp_col_name);
818✔
436
    ColKey type_col = table->get_column_key(s_reset_type_col_name);
818✔
437
    ColKey version_col = table->get_column_key(s_version_column_name);
818✔
438
    REALM_ASSERT(timestamp_col);
818✔
439
    REALM_ASSERT(type_col);
818✔
440
    REALM_ASSERT(version_col);
818✔
441
    if (table->size() > 1) {
818✔
442
        // this may happen if a future version of this code changes the format and expectations around reset metadata.
443
        throw ClientResetFailed(
×
444
            util::format("Previous client resets detected (%1) but only one is expected.", table->size()));
×
445
    }
×
446
    Obj first = *table->begin();
818✔
447
    REALM_ASSERT(first);
818✔
448
    PendingReset pending;
818✔
449
    int64_t version = first.get<int64_t>(version_col);
818✔
450
    pending.time = first.get<Timestamp>(timestamp_col);
818✔
451
    if (version > metadata_version) {
818✔
452
        throw ClientResetFailed(util::format("Unsupported client reset metadata version: %1 vs %2, from %3", version,
×
453
                                             metadata_version, pending.time));
×
454
    }
×
455
    int64_t type = first.get<int64_t>(type_col);
818✔
456
    if (type == 0) {
818✔
457
        pending.type = ClientResyncMode::DiscardLocal;
436✔
458
    }
436✔
459
    else if (type == 1) {
382✔
460
        pending.type = ClientResyncMode::Recover;
382✔
461
    }
382✔
UNCOV
462
    else {
×
UNCOV
463
        throw ClientResetFailed(
×
UNCOV
464
            util::format("Unsupported client reset metadata type: %1 from %2", type, pending.time));
×
UNCOV
465
    }
×
466
    return pending;
818✔
467
}
818✔
468

469
void track_reset(Transaction& wt, ClientResyncMode mode)
470
{
7,560✔
471
    REALM_ASSERT(mode != ClientResyncMode::Manual);
7,560✔
472
    TableRef table = wt.get_table(s_meta_reset_table_name);
7,560✔
473
    ColKey version_col, timestamp_col, type_col;
7,560✔
474
    if (!table) {
7,560✔
475
        table = wt.add_table_with_primary_key(s_meta_reset_table_name, type_ObjectId, s_pk_col_name);
7,516✔
476
        REALM_ASSERT(table);
7,516✔
477
        version_col = table->add_column(type_Int, s_version_column_name);
7,516✔
478
        timestamp_col = table->add_column(type_Timestamp, s_timestamp_col_name);
7,516✔
479
        type_col = table->add_column(type_Int, s_reset_type_col_name);
7,516✔
480
    }
7,516✔
481
    else {
44✔
482
        version_col = table->get_column_key(s_version_column_name);
44✔
483
        timestamp_col = table->get_column_key(s_timestamp_col_name);
44✔
484
        type_col = table->get_column_key(s_reset_type_col_name);
44✔
485
    }
44✔
486
    REALM_ASSERT(version_col);
7,560✔
487
    REALM_ASSERT(timestamp_col);
7,560✔
488
    REALM_ASSERT(type_col);
7,560✔
489
    int64_t mode_val = 0; // Discard
7,560✔
490
    if (mode == ClientResyncMode::Recover || mode == ClientResyncMode::RecoverOrDiscard) {
7,560✔
491
        mode_val = 1; // Recover
3,816✔
492
    }
3,816✔
493

494
    if (table->size() > 1) {
7,560✔
495
        // this may happen if a future version of this code changes the format and expectations around reset metadata.
496
        throw ClientResetFailed(
×
497
            util::format("Previous client resets detected (%1) but only one is expected.", table->size()));
×
498
    }
×
499
    table->create_object_with_primary_key(ObjectId::gen(),
7,560✔
500
                                          {{version_col, metadata_version},
7,560✔
501
                                           {timestamp_col, Timestamp(std::chrono::system_clock::now())},
7,560✔
502
                                           {type_col, mode_val}});
7,560✔
503

504
    // Ensure we save the tracker object even if we encounter an error and roll
505
    // back the client reset later
506
    wt.commit_and_continue_writing();
7,560✔
507
}
7,560✔
508

509
static ClientResyncMode reset_precheck_guard(Transaction& wt, ClientResyncMode mode, bool recovery_is_allowed,
510
                                             util::Logger& logger)
511
{
7,560✔
512
    if (auto previous_reset = has_pending_reset(wt)) {
7,560✔
513
        logger.info(util::LogCategory::reset, "A previous reset was detected of type: '%1' at: %2",
32✔
514
                    previous_reset->type, previous_reset->time);
32✔
515
        switch (previous_reset->type) {
32✔
516
            case ClientResyncMode::Manual:
✔
517
                REALM_UNREACHABLE();
518
            case ClientResyncMode::DiscardLocal:
12✔
519
                throw ClientResetFailed(util::format("A previous '%1' mode reset from %2 did not succeed, "
12✔
520
                                                     "giving up on '%3' mode to prevent a cycle",
12✔
521
                                                     previous_reset->type, previous_reset->time, mode));
12✔
522
            case ClientResyncMode::Recover:
20✔
523
                switch (mode) {
20✔
524
                    case ClientResyncMode::Recover:
8✔
525
                        throw ClientResetFailed(util::format("A previous '%1' mode reset from %2 did not succeed, "
8✔
526
                                                             "giving up on '%3' mode to prevent a cycle",
8✔
527
                                                             previous_reset->type, previous_reset->time, mode));
8✔
528
                    case ClientResyncMode::RecoverOrDiscard:
8✔
529
                        mode = ClientResyncMode::DiscardLocal;
8✔
530
                        logger.info(util::LogCategory::reset,
8✔
531
                                    "A previous '%1' mode reset from %2 downgrades this mode ('%3') to DiscardLocal",
8✔
532
                                    previous_reset->type, previous_reset->time, mode);
8✔
533
                        remove_pending_client_resets(wt);
8✔
534
                        break;
8✔
535
                    case ClientResyncMode::DiscardLocal:
4✔
536
                        remove_pending_client_resets(wt);
4✔
537
                        // previous mode Recover and this mode is Discard, this is not a cycle yet
538
                        break;
4✔
539
                    case ClientResyncMode::Manual:
✔
540
                        REALM_UNREACHABLE();
541
                }
20✔
542
                break;
12✔
543
            case ClientResyncMode::RecoverOrDiscard:
12✔
544
                throw ClientResetFailed(util::format("Unexpected previous '%1' mode reset from %2 did not "
×
545
                                                     "succeed, giving up on '%3' mode to prevent a cycle",
×
546
                                                     previous_reset->type, previous_reset->time, mode));
×
547
        }
32✔
548
    }
32✔
549
    if (!recovery_is_allowed) {
7,540✔
550
        if (mode == ClientResyncMode::Recover) {
24✔
551
            throw ClientResetFailed(
4✔
552
                "Client reset mode is set to 'Recover' but the server does not allow recovery for this client");
4✔
553
        }
4✔
554
        else if (mode == ClientResyncMode::RecoverOrDiscard) {
20✔
555
            logger.info(util::LogCategory::reset,
12✔
556
                        "Client reset in 'RecoverOrDiscard' is choosing 'DiscardLocal' because the server does not "
12✔
557
                        "permit recovery for this client");
12✔
558
            mode = ClientResyncMode::DiscardLocal;
12✔
559
        }
12✔
560
    }
24✔
561
    track_reset(wt, mode);
7,536✔
562
    return mode;
7,536✔
563
}
7,540✔
564

565
bool perform_client_reset_diff(DB& db_local, DB& db_remote, sync::SaltedFileIdent client_file_ident,
566
                               util::Logger& logger, ClientResyncMode mode, bool recovery_is_allowed,
567
                               sync::SubscriptionStore* sub_store,
568
                               util::FunctionRef<void(int64_t)> on_flx_version_complete)
569
{
7,560✔
570
    auto wt_local = db_local.start_write();
7,560✔
571
    auto actual_mode = reset_precheck_guard(*wt_local, mode, recovery_is_allowed, logger);
7,560✔
572
    bool recover_local_changes =
7,560✔
573
        actual_mode == ClientResyncMode::Recover || actual_mode == ClientResyncMode::RecoverOrDiscard;
7,560✔
574

575
    logger.info(util::LogCategory::reset,
7,560✔
576
                "Client reset: path_local = %1, "
7,560✔
577
                "client_file_ident = (ident: %2, salt: %3), "
7,560✔
578
                "remote_path = %4, requested_mode = %5, recovery_is_allowed = %6, "
7,560✔
579
                "actual_mode = %7, will_recover = %8",
7,560✔
580
                db_local.get_path(), client_file_ident.ident, client_file_ident.salt, db_remote.get_path(), mode,
7,560✔
581
                recovery_is_allowed, actual_mode, recover_local_changes);
7,560✔
582

583
    auto& repl_local = dynamic_cast<ClientReplication&>(*db_local.get_replication());
7,560✔
584
    auto& history_local = repl_local.get_history();
7,560✔
585
    history_local.ensure_updated(wt_local->get_version());
7,560✔
586
    VersionID old_version_local = wt_local->get_version_of_current_transaction();
7,560✔
587

588
    auto& repl_remote = dynamic_cast<ClientReplication&>(*db_remote.get_replication());
7,560✔
589
    auto& history_remote = repl_remote.get_history();
7,560✔
590

591
    sync::SaltedVersion fresh_server_version = {0, 0};
7,560✔
592
    {
7,560✔
593
        SyncProgress remote_progress;
7,560✔
594
        sync::version_type remote_version_unused;
7,560✔
595
        SaltedFileIdent remote_ident_unused;
7,560✔
596
        history_remote.get_status(remote_version_unused, remote_ident_unused, remote_progress);
7,560✔
597
        fresh_server_version = remote_progress.latest_server_version;
7,560✔
598
    }
7,560✔
599

600
    TransactionRef tr_remote;
7,560✔
601
    std::vector<client_reset::RecoveredChange> recovered;
7,560✔
602
    if (recover_local_changes) {
7,560✔
603
        auto frozen_pre_local_state = db_local.start_frozen();
3,804✔
604
        auto local_changes = history_local.get_local_changes(wt_local->get_version());
3,804✔
605
        logger.info("Local changesets to recover: %1", local_changes.size());
3,804✔
606

607
        tr_remote = db_remote.start_write();
3,804✔
608
        recovered = process_recovered_changesets(*tr_remote, *frozen_pre_local_state, logger, local_changes);
3,804✔
609
    }
3,804✔
610
    else {
3,756✔
611
        tr_remote = db_remote.start_read();
3,756✔
612
    }
3,756✔
613

614
    // transform the local Realm such that all public tables become identical to the remote Realm
615
    transfer_group(*tr_remote, *wt_local, logger, false);
7,560✔
616

617
    // now that the state of the fresh and local Realms are identical,
618
    // reset the local sync history and steal the fresh Realm's ident
619
    history_local.set_history_adjustments(logger, wt_local->get_version(), client_file_ident, fresh_server_version,
7,560✔
620
                                          recovered);
7,560✔
621

622
    int64_t subscription_version = 0;
7,560✔
623
    if (sub_store) {
7,560✔
624
        if (recover_local_changes) {
128✔
625
            subscription_version = sub_store->mark_active_as_complete(*wt_local);
80✔
626
        }
80✔
627
        else {
48✔
628
            subscription_version = sub_store->set_active_as_latest(*wt_local);
48✔
629
        }
48✔
630
    }
128✔
631

632
    wt_local->commit_and_continue_as_read();
7,560✔
633
    on_flx_version_complete(subscription_version);
7,560✔
634

635
    VersionID new_version_local = wt_local->get_version_of_current_transaction();
7,560✔
636
    logger.info(util::LogCategory::reset,
7,560✔
637
                "perform_client_reset_diff is done: old_version = (version: %1, index: %2), "
7,560✔
638
                "new_version = (version: %3, index: %4)",
7,560✔
639
                old_version_local.version, old_version_local.index, new_version_local.version,
7,560✔
640
                new_version_local.index);
7,560✔
641

642
    return recover_local_changes;
7,560✔
643
}
7,560✔
644

645
} // namespace realm::_impl::client_reset
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

© 2026 Coveralls, Inc