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

realm / realm-core / michael.wilkersonbarker_1147

04 Jun 2024 01:18PM UTC coverage: 90.835% (-0.008%) from 90.843%
michael.wilkersonbarker_1147

Pull #7542

Evergreen

michael-wb
Removed unused SyncClientHookEvent entries
Pull Request #7542: RCORE-2063 Fix some client resets potentially failing with AutoClientResetFailed if a new client reset condition occurred before the first one completed

101700 of 180094 branches covered (56.47%)

61 of 66 new or added lines in 5 files covered. (92.42%)

84 existing lines in 12 files now uncovered.

214628 of 236283 relevant lines covered (90.84%)

5386848.77 hits per line

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

94.51
/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/noinst/pending_reset_store.hpp>
32
#include <realm/sync/subscriptions.hpp>
33

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

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

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

44
namespace realm {
45

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

65
namespace _impl::client_reset {
66

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

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

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

85
    // Find all tables in dst that should be removed.
86
    std::set<std::string> tables_to_remove;
7,720✔
87
    for (auto table_key : group_dst.get_table_keys()) {
42,172✔
88
        if (should_skip_table(group_dst, table_key))
42,172✔
89
            continue;
23,776✔
90
        StringData table_name = group_dst.get_table_name(table_key);
18,396✔
91
        logger.debug(util::LogCategory::reset, "key = %1, table_name = %2", table_key.value, table_name);
18,396✔
92
        ConstTableRef table_src = group_src.get_table(table_name);
18,396✔
93
        if (!table_src) {
18,396✔
94
            logger.debug(util::LogCategory::reset, "Table '%1' will be removed", table_name);
28✔
95
            tables_to_remove.insert(table_name);
28✔
96
            continue;
28✔
97
        }
28✔
98
        // Check whether the table type is the same.
99
        TableRef table_dst = group_dst.get_table(table_key);
18,368✔
100
        auto pk_col_src = table_src->get_primary_key_column();
18,368✔
101
        auto pk_col_dst = table_dst->get_primary_key_column();
18,368✔
102
        bool has_pk_src = bool(pk_col_src);
18,368✔
103
        bool has_pk_dst = bool(pk_col_dst);
18,368✔
104
        if (has_pk_src != has_pk_dst) {
18,368✔
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)
18,368✔
109
            continue;
672✔
110

111
        // Now the tables both have primary keys. Check type.
112
        if (pk_col_src.get_type() != pk_col_dst.get_type()) {
17,696✔
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;
118
        ColumnAttrMask pk_col_src_attr = pk_col_src.get_attrs();
17,692✔
119
        ColumnAttrMask pk_col_dst_attr = pk_col_dst.get_attrs();
17,692✔
120
        pk_col_src_attr.reset(ColumnAttr::col_attr_Indexed);
17,692✔
121
        pk_col_dst_attr.reset(ColumnAttr::col_attr_Indexed);
17,692✔
122
        if (pk_col_src_attr != pk_col_dst_attr) {
17,692✔
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.
128
        StringData pk_col_name_src = table_src->get_column_name(pk_col_src);
17,692✔
129
        StringData pk_col_name_dst = table_dst->get_column_name(pk_col_dst);
17,692✔
130
        if (pk_col_name_src != pk_col_name_dst) {
17,692✔
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.
136
        logger.debug(util::LogCategory::reset, "Table '%1' will remain", table_name);
17,692✔
137
    }
17,692✔
138

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

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

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

209
    // Remove columns in dst if they are absent in src.
210
    for (auto table_key : group_src.get_table_keys()) {
26,344✔
211
        if (should_skip_table(group_src, table_key))
26,344✔
212
            continue;
7,984✔
213
        ConstTableRef table_src = group_src.get_table(table_key);
18,360✔
214
        StringData table_name = table_src->get_name();
18,360✔
215
        TableRef table_dst = group_dst.get_table(table_name);
18,360✔
216
        REALM_ASSERT(table_dst);
18,360✔
217
        std::vector<std::string> columns_to_remove;
18,360✔
218
        for (ColKey col_key : table_dst->get_column_keys()) {
59,316✔
219
            StringData col_name = table_dst->get_column_name(col_key);
59,316✔
220
            ColKey col_key_src = table_src->get_column_key(col_name);
59,316✔
221
            if (!col_key_src) {
59,316✔
222
                columns_to_remove.push_back(col_name);
12✔
223
                continue;
12✔
224
            }
12✔
225
        }
59,316✔
226
        if (!allow_schema_additions && !columns_to_remove.empty()) {
18,360✔
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
    }
18,360✔
237

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

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

316
    // Now the schemas are identical.
317

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

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

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

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

395
        converters::InterRealmObjectConverter converter(table_src, table_dst, &embedded_tracker);
17,644✔
396

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

403
            bool updated = false;
25,404✔
404
            converter.copy(src, dst, &updated);
25,404✔
405
            if (updated) {
25,404✔
406
                logger.debug(util::LogCategory::reset, "  updating %1", src_pk);
7,546✔
407
            }
7,546✔
408
        }
25,404✔
409
        embedded_tracker.process_pending();
17,644✔
410
    }
17,644✔
411
}
7,688✔
412

413
ClientResyncMode reset_precheck_guard(const TransactionRef& wt_local, ClientResyncMode mode,
414
                                      PendingReset::Action action, const std::optional<Status>& error,
415
                                      util::Logger& logger)
416
{
7,760✔
417
    if (auto previous_reset = sync::PendingResetStore::has_pending_reset(wt_local)) {
7,760✔
418
        logger.info(util::LogCategory::reset, "Found a previous %1", *previous_reset);
36✔
419
        if (action != previous_reset->action) {
36✔
420
            // IF a different client reset is being performed, cler the pending client reset and start over.
421
            logger.info(util::LogCategory::reset,
4✔
422
                        "New '%1' client reset of type: '%2' is incompatible - clearing previous reset", action,
4✔
423
                        mode);
4✔
424
            sync::PendingResetStore::clear_pending_reset(wt_local);
4✔
425
        }
4✔
426
        else {
32✔
427
            switch (previous_reset->mode) {
32✔
NEW
428
                case ClientResyncMode::Manual:
✔
429
                    REALM_UNREACHABLE();
430
                case ClientResyncMode::DiscardLocal:
12✔
431
                    throw ClientResetFailed(util::format("A previous '%1' mode reset from %2 did not succeed, "
12✔
432
                                                         "giving up on '%3' mode to prevent a cycle",
12✔
433
                                                         previous_reset->mode, previous_reset->time, mode));
12✔
434
                case ClientResyncMode::Recover:
20✔
435
                    switch (mode) {
20✔
436
                        case ClientResyncMode::Recover:
8✔
437
                            throw ClientResetFailed(
8✔
438
                                util::format("A previous '%1' mode reset from %2 did not succeed, "
8✔
439
                                             "giving up on '%3' mode to prevent a cycle",
8✔
440
                                             previous_reset->mode, previous_reset->time, mode));
8✔
441
                        case ClientResyncMode::RecoverOrDiscard:
8✔
442
                            mode = ClientResyncMode::DiscardLocal;
8✔
443
                            logger.info(
8✔
444
                                util::LogCategory::reset,
8✔
445
                                "A previous '%1' mode reset from %2 downgrades this mode ('%3') to DiscardLocal",
8✔
446
                                previous_reset->mode, previous_reset->time, mode);
8✔
447
                            sync::PendingResetStore::clear_pending_reset(wt_local);
8✔
448
                            break;
8✔
449
                        case ClientResyncMode::DiscardLocal:
4✔
450
                            sync::PendingResetStore::clear_pending_reset(wt_local);
4✔
451
                            // previous mode Recover and this mode is Discard, this is not a cycle yet
452
                            break;
4✔
NEW
453
                        case ClientResyncMode::Manual:
✔
454
                            REALM_UNREACHABLE();
455
                    }
20✔
456
                    break;
12✔
457
                case ClientResyncMode::RecoverOrDiscard:
12✔
NEW
458
                    throw ClientResetFailed(util::format("Unexpected previous '%1' mode reset from %2 did not "
×
NEW
459
                                                         "succeed, giving up on '%3' mode to prevent a cycle",
×
NEW
460
                                                         previous_reset->mode, previous_reset->time, mode));
×
461
            }
32✔
462
        }
32✔
463
    }
36✔
464
    if (action == PendingReset::Action::ClientResetNoRecovery) {
7,740✔
465
        if (mode == ClientResyncMode::Recover) {
28✔
466
            throw ClientResetFailed(
4✔
467
                "Client reset mode is set to 'Recover' but the server does not allow recovery for this client");
4✔
468
        }
4✔
469
        else if (mode == ClientResyncMode::RecoverOrDiscard) {
24✔
470
            logger.info(util::LogCategory::reset,
12✔
471
                        "Client reset in 'RecoverOrDiscard' is choosing 'DiscardLocal' because the server does not "
12✔
472
                        "permit recovery for this client");
12✔
473
            mode = ClientResyncMode::DiscardLocal;
12✔
474
        }
12✔
475
    }
28✔
476
    sync::PendingResetStore::track_reset(wt_local, mode, action, error);
7,736✔
477
    // Ensure we save the tracker object even if we encounter an error and roll
478
    // back the client reset later
479
    wt_local->commit_and_continue_writing();
7,736✔
480
    return mode;
7,736✔
481
}
7,740✔
482

483
bool perform_client_reset_diff(DB& db_local, sync::ClientReset& reset_config, sync::SaltedFileIdent client_file_ident,
484
                               util::Logger& logger, sync::SubscriptionStore* sub_store,
485
                               util::FunctionRef<void(int64_t)> on_flx_version_complete)
486
{
7,760✔
487
    DB& db_remote = *reset_config.fresh_copy;
7,760✔
488
    auto wt_local = db_local.start_write();
7,760✔
489
    auto actual_mode =
7,760✔
490
        reset_precheck_guard(wt_local, reset_config.mode, reset_config.action, reset_config.error, logger);
7,760✔
491
    bool recover_local_changes =
7,760✔
492
        actual_mode == ClientResyncMode::Recover || actual_mode == ClientResyncMode::RecoverOrDiscard;
7,760✔
493

494
    logger.info(util::LogCategory::reset,
7,760✔
495
                "Client reset: path_local = %1, client_file_ident = (ident: %2, salt: %3), "
7,760✔
496
                "remote_path = %4, requested_mode = %5, action = %6, actual_mode = %7, will_recover = %8, "
7,760✔
497
                "originating_error = %9",
7,760✔
498
                db_local.get_path(), client_file_ident.ident, client_file_ident.salt, db_remote.get_path(),
7,760✔
499
                reset_config.mode, reset_config.action, actual_mode, recover_local_changes, reset_config.error);
7,760✔
500

501
    auto& repl_local = dynamic_cast<ClientReplication&>(*db_local.get_replication());
7,760✔
502
    auto& history_local = repl_local.get_history();
7,760✔
503
    history_local.ensure_updated(wt_local->get_version());
7,760✔
504
    VersionID old_version_local = wt_local->get_version_of_current_transaction();
7,760✔
505

506
    auto& repl_remote = dynamic_cast<ClientReplication&>(*db_remote.get_replication());
7,760✔
507
    auto& history_remote = repl_remote.get_history();
7,760✔
508

509
    sync::SaltedVersion fresh_server_version = {0, 0};
7,760✔
510
    {
7,760✔
511
        SyncProgress remote_progress;
7,760✔
512
        sync::version_type remote_version_unused;
7,760✔
513
        SaltedFileIdent remote_ident_unused;
7,760✔
514
        history_remote.get_status(remote_version_unused, remote_ident_unused, remote_progress);
7,760✔
515
        fresh_server_version = remote_progress.latest_server_version;
7,760✔
516
    }
7,760✔
517

518
    TransactionRef tr_remote;
7,760✔
519
    std::vector<client_reset::RecoveredChange> recovered;
7,760✔
520
    if (recover_local_changes) {
7,760✔
521
        auto frozen_pre_local_state = db_local.start_frozen();
3,908✔
522
        auto local_changes = history_local.get_local_changes(wt_local->get_version());
3,908✔
523
        logger.info("Local changesets to recover: %1", local_changes.size());
3,908✔
524

525
        tr_remote = db_remote.start_write();
3,908✔
526
        recovered = process_recovered_changesets(*tr_remote, *frozen_pre_local_state, logger, local_changes);
3,908✔
527
    }
3,908✔
528
    else {
3,852✔
529
        tr_remote = db_remote.start_read();
3,852✔
530
    }
3,852✔
531

532
    // transform the local Realm such that all public tables become identical to the remote Realm
533
    transfer_group(*tr_remote, *wt_local, logger, false);
7,760✔
534

535
    // now that the state of the fresh and local Realms are identical,
536
    // reset the local sync history and steal the fresh Realm's ident
537
    history_local.set_history_adjustments(logger, wt_local->get_version(), client_file_ident, fresh_server_version,
7,760✔
538
                                          recovered);
7,760✔
539

540
    int64_t subscription_version = 0;
7,760✔
541
    if (sub_store) {
7,760✔
542
        if (recover_local_changes) {
132✔
543
            subscription_version = sub_store->mark_active_as_complete(*wt_local);
84✔
544
        }
84✔
545
        else {
48✔
546
            subscription_version = sub_store->set_active_as_latest(*wt_local);
48✔
547
        }
48✔
548
    }
132✔
549

550
    wt_local->commit_and_continue_as_read();
7,760✔
551
    on_flx_version_complete(subscription_version);
7,760✔
552

553
    VersionID new_version_local = wt_local->get_version_of_current_transaction();
7,760✔
554
    logger.info(util::LogCategory::reset,
7,760✔
555
                "perform_client_reset_diff is done: old_version = (version: %1, index: %2), "
7,760✔
556
                "new_version = (version: %3, index: %4)",
7,760✔
557
                old_version_local.version, old_version_local.index, new_version_local.version,
7,760✔
558
                new_version_local.index);
7,760✔
559

560
    return recover_local_changes;
7,760✔
561
}
7,760✔
562

563
} // namespace _impl::client_reset
564
} // namespace realm
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2025 Coveralls, Inc