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

realm / realm-core / 1845

17 Nov 2023 10:56AM UTC coverage: 91.697% (+0.04%) from 91.661%
1845

push

Evergreen

web-flow
Merge pull request #7144 from realm/tg/set-fixes

Use typed comparisons for set algebra on strings and binary

92280 of 169116 branches covered (0.0%)

256 of 263 new or added lines in 4 files covered. (97.34%)

79 existing lines in 17 files now uncovered.

231307 of 252252 relevant lines covered (91.7%)

6180067.37 hits per line

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

93.49
/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
{
20,986✔
47
    switch (mode) {
20,986✔
48
        case ClientResyncMode::Manual:
✔
49
            os << "Manual";
×
50
            break;
×
51
        case ClientResyncMode::DiscardLocal:
10,442✔
52
            os << "DiscardLocal";
10,442✔
53
            break;
10,442✔
54
        case ClientResyncMode::Recover:
10,436✔
55
            os << "Recover";
10,436✔
56
            break;
10,436✔
57
        case ClientResyncMode::RecoverOrDiscard:
108✔
58
            os << "RecoverOrDiscard";
108✔
59
            break;
108✔
60
    }
20,986✔
61
    return os;
20,986✔
62
}
20,986✔
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
{
222,124✔
70
    return !group.table_is_public(key);
222,124✔
71
}
222,124✔
72

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

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

3,406✔
85
    // Find all tables in dst that should be removed.
3,406✔
86
    std::set<std::string> tables_to_remove;
6,812✔
87
    for (auto table_key : group_dst.get_table_keys()) {
30,412✔
88
        if (should_skip_table(group_dst, table_key))
30,412✔
89
            continue;
14,368✔
90
        StringData table_name = group_dst.get_table_name(table_key);
16,044✔
91
        logger.debug("key = %1, table_name = %2", table_key.value, table_name);
16,044✔
92
        ConstTableRef table_src = group_src.get_table(table_name);
16,044✔
93
        if (!table_src) {
16,044✔
94
            logger.debug("Table '%1' will be removed", table_name);
40✔
95
            tables_to_remove.insert(table_name);
40✔
96
            continue;
40✔
97
        }
40✔
98
        // Check whether the table type is the same.
8,002✔
99
        TableRef table_dst = group_dst.get_table(table_key);
16,004✔
100
        auto pk_col_src = table_src->get_primary_key_column();
16,004✔
101
        auto pk_col_dst = table_dst->get_primary_key_column();
16,004✔
102
        bool has_pk_src = bool(pk_col_src);
16,004✔
103
        bool has_pk_dst = bool(pk_col_dst);
16,004✔
104
        if (has_pk_src != has_pk_dst) {
16,004✔
105
            throw ClientResetFailed(util::format("Client reset requires a primary key column in %1 table '%2'",
×
106
                                                 (has_pk_src ? "dest" : "source"), table_name));
×
107
        }
×
108
        if (!has_pk_src)
16,004✔
109
            continue;
648✔
110

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

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

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

3,396✔
191
    // Now the class tables are identical.
3,396✔
192
    size_t num_tables;
6,792✔
193
    {
6,792✔
194
        size_t num_tables_src = 0;
6,792✔
195
        for (auto table_key : group_src.get_table_keys()) {
23,088✔
196
            if (!should_skip_table(group_src, table_key))
23,088✔
197
                ++num_tables_src;
15,988✔
198
        }
23,088✔
199
        size_t num_tables_dst = 0;
6,792✔
200
        for (auto table_key : group_dst.get_table_keys()) {
30,316✔
201
            if (!should_skip_table(group_dst, table_key))
30,316✔
202
                ++num_tables_dst;
16,004✔
203
        }
30,316✔
204
        REALM_ASSERT_EX(allow_schema_additions || num_tables_src == num_tables_dst, num_tables_src, num_tables_dst);
6,792✔
205
        num_tables = num_tables_src;
6,792✔
206
    }
6,792✔
207
    logger.debug("The number of tables is %1", num_tables);
6,792✔
208

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

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

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

3,394✔
315
    // Now the schemas are identical.
3,394✔
316

3,394✔
317
    // Remove objects in dst that are absent in src.
3,394✔
318
    for (auto table_key : group_src.get_table_keys()) {
23,024✔
319
        if (should_skip_table(group_src, table_key))
23,024✔
320
            continue;
7,076✔
321
        auto table_src = group_src.get_table(table_key);
15,948✔
322
        // There are no primary keys in embedded tables but this is ok, because
7,974✔
323
        // embedded objects are tied to the lifetime of top level objects.
7,974✔
324
        if (table_src->is_embedded())
15,948✔
325
            continue;
664✔
326
        StringData table_name = table_src->get_name();
15,284✔
327
        logger.debug("Removing objects in '%1'", table_name);
15,284✔
328
        auto table_dst = group_dst.get_table(table_name);
15,284✔
329

7,642✔
330
        auto pk_col = table_dst->get_primary_key_column();
15,284✔
331
        REALM_ASSERT_DEBUG(pk_col); // sync realms always have a pk
15,284✔
332
        std::vector<std::pair<Mixed, ObjKey>> objects_to_remove;
15,284✔
333
        for (auto obj : *table_dst) {
21,548✔
334
            auto pk = obj.get_any(pk_col);
21,548✔
335
            if (!table_src->find_primary_key(pk)) {
21,548✔
336
                objects_to_remove.emplace_back(pk, obj.get_key());
464✔
337
            }
464✔
338
        }
21,548✔
339
        for (auto& pair : objects_to_remove) {
7,874✔
340
            logger.debug("  removing '%1'", pair.first);
464✔
341
            table_dst->remove_object(pair.second);
464✔
342
        }
464✔
343
    }
15,284✔
344

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

3,390✔
368
    converters::EmbeddedObjectConverter embedded_tracker;
6,780✔
369
    // Now src and dst have identical schemas and no extraneous objects from dst.
3,390✔
370
    // There may be missing object from src and the values of existing objects may
3,390✔
371
    // still differ. Diff all the values and create missing objects on the fly.
3,390✔
372
    for (auto table_key : group_src.get_table_keys()) {
23,024✔
373
        if (should_skip_table(group_src, table_key))
23,024✔
374
            continue;
7,076✔
375
        ConstTableRef table_src = group_src.get_table(table_key);
15,948✔
376
        // Embedded objects don't have a primary key, so they are handled
7,974✔
377
        // as a special case when they are encountered as a link value.
7,974✔
378
        if (table_src->is_embedded())
15,948✔
379
            continue;
664✔
380
        StringData table_name = table_src->get_name();
15,284✔
381
        TableRef table_dst = group_dst.get_table(table_name);
15,284✔
382
        REALM_ASSERT_EX(allow_schema_additions || table_src->get_column_count() == table_dst->get_column_count(),
15,284✔
383
                        allow_schema_additions, table_src->get_column_count(), table_dst->get_column_count());
15,284✔
384
        auto pk_col = table_src->get_primary_key_column();
15,284✔
385
        REALM_ASSERT(pk_col);
15,284✔
386
        logger.debug("Updating values for table '%1', number of rows = %2, "
15,284✔
387
                     "number of columns = %3, primary_key_col = %4, "
15,284✔
388
                     "primary_key_type = %5",
15,284✔
389
                     table_name, table_src->size(), table_src->get_column_count(), pk_col.get_index().val,
15,284✔
390
                     pk_col.get_type());
15,284✔
391

7,642✔
392
        converters::InterRealmObjectConverter converter(table_src, table_dst, &embedded_tracker);
15,284✔
393

7,642✔
394
        for (const Obj& src : *table_src) {
21,664✔
395
            auto src_pk = src.get_primary_key();
21,664✔
396
            // create the object - it should have been created above.
10,832✔
397
            auto dst = table_dst->get_object_with_primary_key(src_pk);
21,664✔
398
            REALM_ASSERT(dst);
21,664✔
399

10,832✔
400
            bool updated = false;
21,664✔
401
            converter.copy(src, dst, &updated);
21,664✔
402
            if (updated) {
21,664✔
403
                logger.debug("  updating %1", src_pk);
7,392✔
404
            }
7,392✔
405
        }
21,664✔
406
        embedded_tracker.process_pending();
15,284✔
407
    }
15,284✔
408
}
6,780✔
409

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

418
void remove_pending_client_resets(Transaction& wt)
419
{
208✔
420
    if (auto table = wt.get_table(s_meta_reset_table_name); table && !table->is_empty()) {
208✔
421
        table->clear();
208✔
422
    }
208✔
423
}
208✔
424

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

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

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

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

554
LocalVersionIDs perform_client_reset_diff(DB& db_local, DB& db_remote, sync::SaltedFileIdent client_file_ident,
555
                                          util::Logger& logger, ClientResyncMode mode, bool recovery_is_allowed,
556
                                          bool* did_recover_out, sync::SubscriptionStore* sub_store,
557
                                          util::FunctionRef<void(int64_t)> on_flx_version_complete)
558
{
6,836✔
559
    auto wt_local = db_local.start_write();
6,836✔
560
    auto actual_mode = reset_precheck_guard(*wt_local, mode, recovery_is_allowed, logger);
6,836✔
561
    bool recover_local_changes =
6,836✔
562
        actual_mode == ClientResyncMode::Recover || actual_mode == ClientResyncMode::RecoverOrDiscard;
6,836✔
563

3,418✔
564
    logger.info("Client reset: path_local = %1, "
6,836✔
565
                "client_file_ident = (ident: %2, salt: %3), "
6,836✔
566
                "remote_path = %4, requested_mode = %5, recovery_is_allowed = %6, "
6,836✔
567
                "actual_mode = %7, will_recover = %8",
6,836✔
568
                db_local.get_path(), client_file_ident.ident, client_file_ident.salt, db_remote.get_path(), mode,
6,836✔
569
                recovery_is_allowed, actual_mode, recover_local_changes);
6,836✔
570

3,418✔
571
    auto& repl_local = dynamic_cast<ClientReplication&>(*db_local.get_replication());
6,836✔
572
    auto& history_local = repl_local.get_history();
6,836✔
573
    history_local.ensure_updated(wt_local->get_version());
6,836✔
574
    SaltedFileIdent orig_file_ident = history_local.get_client_file_ident(*wt_local);
6,836✔
575
    VersionID old_version_local = wt_local->get_version_of_current_transaction();
6,836✔
576

3,418✔
577
    auto& repl_remote = dynamic_cast<ClientReplication&>(*db_remote.get_replication());
6,836✔
578
    auto& history_remote = repl_remote.get_history();
6,836✔
579

3,418✔
580
    sync::SaltedVersion fresh_server_version = {0, 0};
6,836✔
581
    {
6,836✔
582
        SyncProgress remote_progress;
6,836✔
583
        sync::version_type remote_version_unused;
6,836✔
584
        SaltedFileIdent remote_ident_unused;
6,836✔
585
        history_remote.get_status(remote_version_unused, remote_ident_unused, remote_progress);
6,836✔
586
        fresh_server_version = remote_progress.latest_server_version;
6,836✔
587
    }
6,836✔
588

3,418✔
589
    if (!recover_local_changes) {
6,836✔
590
        auto rt_remote = db_remote.start_read();
3,396✔
591
        // transform the local Realm such that all public tables become identical to the remote Realm
1,698✔
592
        transfer_group(*rt_remote, *wt_local, logger, false);
3,396✔
593

1,698✔
594
        // now that the state of the fresh and local Realms are identical,
1,698✔
595
        // reset the local sync history and steal the fresh Realm's ident
1,698✔
596
        history_local.set_client_reset_adjustments(wt_local->get_version(), client_file_ident, fresh_server_version,
3,396✔
597
                                                   BinaryData());
3,396✔
598

1,698✔
599
        int64_t subscription_version = 0;
3,396✔
600
        if (sub_store) {
3,396✔
601
            subscription_version = sub_store->set_active_as_latest(*wt_local);
48✔
602
        }
48✔
603

1,698✔
604
        wt_local->commit_and_continue_as_read();
3,396✔
605
        if (did_recover_out) {
3,396✔
606
            *did_recover_out = false;
196✔
607
        }
196✔
608
        on_flx_version_complete(subscription_version);
3,396✔
609

1,698✔
610
        VersionID new_version_local = wt_local->get_version_of_current_transaction();
3,396✔
611
        logger.info("perform_client_reset_diff is done: old_version = (version: %1, index: %2), "
3,396✔
612
                    "new_version = (version: %3, index: %4)",
3,396✔
613
                    old_version_local.version, old_version_local.index, new_version_local.version,
3,396✔
614
                    new_version_local.index);
3,396✔
615
        return LocalVersionIDs{old_version_local, new_version_local};
3,396✔
616
    }
3,396✔
617

1,720✔
618
    auto remake_active_subscription = [&]() {
3,440✔
619
        if (!sub_store) {
64✔
620
            return;
×
621
        }
×
622
        auto subs = sub_store->get_active();
64✔
623
        int64_t before_version = subs.version();
64✔
624
        auto mut_subs = subs.make_mutable_copy();
64✔
625
        mut_subs.update_state(sync::SubscriptionSet::State::Complete);
64✔
626
        auto sub = std::move(mut_subs).commit();
64✔
627
        on_flx_version_complete(sub.version());
64✔
628
        logger.info("Recreated the active subscription set in the complete state (%1 -> %2)", before_version,
64✔
629
                    sub.version());
64✔
630
    };
64✔
631

1,720✔
632
    auto frozen_pre_local_state = db_local.start_frozen();
3,440✔
633
    auto local_changes = history_local.get_local_changes(wt_local->get_version());
3,440✔
634
    logger.info("Local changesets to recover: %1", local_changes.size());
3,440✔
635

1,720✔
636
    auto wt_remote = db_remote.start_write();
3,440✔
637

1,720✔
638
    BinaryData recovered_changeset;
3,440✔
639

1,720✔
640
    // FLX with recovery has to be done in multiple commits, which is significantly different than other modes
1,720✔
641
    if (sub_store) {
3,440✔
642
        // In FLX recovery, save a copy of the pending subscriptions for later. This
34✔
643
        // needs to be done before they are wiped out by remake_active_subscription()
34✔
644
        std::vector<SubscriptionSet> pending_subscriptions = sub_store->get_pending_subscriptions();
68✔
645
        // transform the local Realm such that all public tables become identical to the remote Realm
34✔
646
        transfer_group(*wt_remote, *wt_local, logger, recover_local_changes);
68✔
647
        // now that the state of the fresh and local Realms are identical,
34✔
648
        // reset the local sync history.
34✔
649
        // Note that we do not set the new file ident yet! This is done in the last commit.
34✔
650
        history_local.set_client_reset_adjustments(wt_local->get_version(), orig_file_ident, fresh_server_version,
68✔
651
                                                   recovered_changeset);
68✔
652
        // The local Realm is committed. There are no changes to the remote Realm.
34✔
653
        wt_remote->rollback_and_continue_as_read();
68✔
654
        wt_local->commit_and_continue_as_read();
68✔
655
        // Make a copy of the active subscription set and mark it as
34✔
656
        // complete. This will cause all other subscription sets to become superceded.
34✔
657
        remake_active_subscription();
68✔
658
        // Apply local changes interleaved with pending subscriptions in separate commits
34✔
659
        // as needed. This has the consequence that there may be extra notifications along
34✔
660
        // the way to the final state, but since separate commits are necessary, this is
34✔
661
        // unavoidable.
34✔
662
        wt_local = db_local.start_write();
68✔
663
        RecoverLocalChangesetsHandler handler{*wt_local, *frozen_pre_local_state, logger, db_local.get_replication()};
68✔
664
        handler.process_changesets(local_changes, std::move(pending_subscriptions)); // throws on error
68✔
665
        // The new file ident is set as part of the final commit. This is to ensure that if
34✔
666
        // there are any exceptions during recovery, or the process is killed for some
34✔
667
        // reason, the client reset cycle detection will catch this and we will not attempt
34✔
668
        // to recover again. If we had set the ident in the first commit, a Realm which was
34✔
669
        // partially recovered, but interrupted may continue sync the next time it is
34✔
670
        // opened with only partially recovered state while having lost the history of any
34✔
671
        // offline modifications.
34✔
672
        history_local.set_client_file_ident_in_wt(wt_local->get_version(), client_file_ident);
68✔
673
        wt_local->commit_and_continue_as_read();
68✔
674
    }
68✔
675
    else {
3,372✔
676
        // In PBS recovery, the strategy is to apply all local changes to the remote realm first,
1,686✔
677
        // and then transfer the modified state all at once to the local Realm. This creates a
1,686✔
678
        // nice side effect for notifications because only the minimal state change is made.
1,686✔
679
        RecoverLocalChangesetsHandler handler{*wt_remote, *frozen_pre_local_state, logger,
3,372✔
680
                                              db_remote.get_replication()};
3,372✔
681
        handler.process_changesets(local_changes, {}); // throws on error
3,372✔
682
        ChangesetEncoder& encoder = repl_remote.get_instruction_encoder();
3,372✔
683
        const sync::ChangesetEncoder::Buffer& buffer = encoder.buffer();
3,372✔
684
        recovered_changeset = {buffer.data(), buffer.size()};
3,372✔
685

1,686✔
686
        // transform the local Realm such that all public tables become identical to the remote Realm
1,686✔
687
        transfer_group(*wt_remote, *wt_local, logger, recover_local_changes);
3,372✔
688

1,686✔
689
        // now that the state of the fresh and local Realms are identical,
1,686✔
690
        // reset the local sync history and steal the fresh Realm's ident
1,686✔
691
        history_local.set_client_reset_adjustments(wt_local->get_version(), client_file_ident, fresh_server_version,
3,372✔
692
                                                   recovered_changeset);
3,372✔
693

1,686✔
694
        // Finally, the local Realm is committed. The changes to the remote Realm are discarded.
1,686✔
695
        wt_remote->rollback_and_continue_as_read();
3,372✔
696
        wt_local->commit_and_continue_as_read();
3,372✔
697
    }
3,372✔
698

1,720✔
699
    if (did_recover_out) {
3,440✔
700
        *did_recover_out = true;
128✔
701
    }
128✔
702
    VersionID new_version_local = wt_local->get_version_of_current_transaction();
3,440✔
703
    logger.info("perform_client_reset_diff is done, old_version.version = %1, "
3,440✔
704
                "old_version.index = %2, new_version.version = %3, "
3,440✔
705
                "new_version.index = %4",
3,440✔
706
                old_version_local.version, old_version_local.index, new_version_local.version,
3,440✔
707
                new_version_local.index);
3,440✔
708

1,720✔
709
    return LocalVersionIDs{old_version_local, new_version_local};
3,440✔
710
}
3,440✔
711

712
} // 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