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

realm / realm-core / 2274

29 Apr 2024 07:20PM UTC coverage: 90.709% (-0.04%) from 90.748%
2274

push

Evergreen

web-flow
Merge pull request #7645 from realm/mwb/fix-warning

Fix warning introduced by PR #7632

101872 of 180246 branches covered (56.52%)

212397 of 234153 relevant lines covered (90.71%)

5624274.76 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,210✔
47
    switch (mode) {
23,210✔
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,636✔
55
            os << "Recover";
11,636✔
56
            break;
11,636✔
57
        case ClientResyncMode::RecoverOrDiscard:
120✔
58
            os << "RecoverOrDiscard";
120✔
59
            break;
120✔
60
    }
23,210✔
61
    return os;
23,210✔
62
}
23,210✔
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,852✔
408
            }
7,852✔
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
{
268✔
424
    if (auto table = wt.get_table(s_meta_reset_table_name); table && !table->is_empty()) {
268✔
425
        table->clear();
268✔
426
    }
268✔
427
}
268✔
428

429
util::Optional<PendingReset> has_pending_reset(const Transaction& rt)
430
{
18,304✔
431
    ConstTableRef table = rt.get_table(s_meta_reset_table_name);
18,304✔
432
    if (!table || table->size() == 0) {
18,304✔
433
        return util::none;
17,484✔
434
    }
17,484✔
435
    ColKey timestamp_col = table->get_column_key(s_timestamp_col_name);
820✔
436
    ColKey type_col = table->get_column_key(s_reset_type_col_name);
820✔
437
    ColKey version_col = table->get_column_key(s_version_column_name);
820✔
438
    REALM_ASSERT(timestamp_col);
820✔
439
    REALM_ASSERT(type_col);
820✔
440
    REALM_ASSERT(version_col);
820✔
441
    if (table->size() > 1) {
820✔
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();
820✔
447
    REALM_ASSERT(first);
820✔
448
    PendingReset pending;
820✔
449
    int64_t version = first.get<int64_t>(version_col);
820✔
450
    pending.time = first.get<Timestamp>(timestamp_col);
820✔
451
    if (version > metadata_version) {
820✔
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);
820✔
456
    if (type == 0) {
820✔
457
        pending.type = ClientResyncMode::DiscardLocal;
436✔
458
    }
436✔
459
    else if (type == 1) {
384✔
460
        pending.type = ClientResyncMode::Recover;
384✔
461
    }
384✔
462
    else {
×
463
        throw ClientResetFailed(
×
464
            util::format("Unsupported client reset metadata type: %1 from %2", type, pending.time));
×
465
    }
×
466
    return pending;
820✔
467
}
820✔
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