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

realm / realm-core / github_pull_request_301264

30 Jul 2024 07:11PM UTC coverage: 91.111% (+0.009%) from 91.102%
github_pull_request_301264

Pull #7936

Evergreen

web-flow
Add support for multi-process subscription state change notifications (#7862)

As with the other multi-process notifications, the core idea here is to
eliminate the in-memory state and produce notifications based entirely on the
current state of the Realm file.

SubscriptionStore::update_state() has been replaced with separate functions for
the specific legal state transitions, which also take a write transaction as a
parameter. These functions are called by PendingBootstrapStore inside the same
write transaction as the bootstrap updates which changed the subscription
state. This is both a minor performance optimization (due to fewer writes) and
eliminates a brief window between the two writes where the Realm file was in an
inconsistent state.

There's a minor functional change here: previously old subscription sets were
superseded when the new one reached the Completed state, and now they are
superseded on AwaitingMark. This aligns it with when the new subscription set
becomes the one which is returned by get_active().
Pull Request #7936: Fix connection callback crashes when reloading with React Native

102800 of 181570 branches covered (56.62%)

216840 of 237996 relevant lines covered (91.11%)

5918493.47 hits per line

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

94.5
/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
{
24,068✔
48
    switch (mode) {
24,068✔
49
        case ClientResyncMode::Manual:
4✔
50
            os << "Manual";
4✔
51
            break;
4✔
52
        case ClientResyncMode::DiscardLocal:
11,746✔
53
            os << "DiscardLocal";
11,746✔
54
            break;
11,746✔
55
        case ClientResyncMode::Recover:
12,198✔
56
            os << "Recover";
12,198✔
57
            break;
12,198✔
58
        case ClientResyncMode::RecoverOrDiscard:
120✔
59
            os << "RecoverOrDiscard";
120✔
60
            break;
120✔
61
    }
24,068✔
62
    return os;
24,068✔
63
}
24,068✔
64

65
namespace _impl::client_reset {
66

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

72
void transfer_group(const Transaction& group_src, Transaction& group_dst, util::Logger& logger,
73
                    bool allow_schema_additions)
74
{
7,768✔
75
    logger.debug(util::LogCategory::reset,
7,768✔
76
                 "transfer_group, src size = %1, dst size = %2, allow_schema_additions = %3", group_src.size(),
7,768✔
77
                 group_dst.size(), allow_schema_additions);
7,768✔
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,768✔
82
    REALM_ASSERT_RELEASE(client_repl);
7,768✔
83
    TempShortCircuitReplication sync_history_guard(*client_repl);
7,768✔
84

85
    // Find all tables in dst that should be removed.
86
    std::set<std::string> tables_to_remove;
7,768✔
87
    for (auto table_key : group_dst.get_table_keys()) {
42,548✔
88
        if (should_skip_table(group_dst, table_key))
42,548✔
89
            continue;
24,104✔
90
        StringData table_name = group_dst.get_table_name(table_key);
18,444✔
91
        logger.debug(util::LogCategory::reset, "key = %1, table_name = %2", table_key.value, table_name);
18,444✔
92
        ConstTableRef table_src = group_src.get_table(table_name);
18,444✔
93
        if (!table_src) {
18,444✔
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,416✔
100
        auto pk_col_src = table_src->get_primary_key_column();
18,416✔
101
        auto pk_col_dst = table_dst->get_primary_key_column();
18,416✔
102
        bool has_pk_src = bool(pk_col_src);
18,416✔
103
        bool has_pk_dst = bool(pk_col_dst);
18,416✔
104
        if (has_pk_src != has_pk_dst) {
18,416✔
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,416✔
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,744✔
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,740✔
119
        ColumnAttrMask pk_col_dst_attr = pk_col_dst.get_attrs();
17,740✔
120
        pk_col_src_attr.reset(ColumnAttr::col_attr_Indexed);
17,740✔
121
        pk_col_dst_attr.reset(ColumnAttr::col_attr_Indexed);
17,740✔
122
        if (pk_col_src_attr != pk_col_dst_attr) {
17,740✔
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,740✔
129
        StringData pk_col_name_dst = table_dst->get_column_name(pk_col_dst);
17,740✔
130
        if (pk_col_name_src != pk_col_name_dst) {
17,740✔
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,740✔
137
    }
17,740✔
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,764✔
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,612✔
168
        if (should_skip_table(group_src, table_key))
26,612✔
169
            continue;
8,200✔
170
        ConstTableRef table_src = group_src.get_table(table_key);
18,412✔
171
        StringData table_name = table_src->get_name();
18,412✔
172
        auto pk_col_src = table_src->get_primary_key_column();
18,412✔
173
        TableRef table_dst = group_dst.get_table(table_name);
18,412✔
174
        if (!table_dst) {
18,412✔
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,412✔
190

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

209
    // Remove columns in dst if they are absent in src.
210
    for (auto table_key : group_src.get_table_keys()) {
26,608✔
211
        if (should_skip_table(group_src, table_key))
26,608✔
212
            continue;
8,200✔
213
        ConstTableRef table_src = group_src.get_table(table_key);
18,408✔
214
        StringData table_name = table_src->get_name();
18,408✔
215
        TableRef table_dst = group_dst.get_table(table_name);
18,408✔
216
        REALM_ASSERT(table_dst);
18,408✔
217
        std::vector<std::string> columns_to_remove;
18,408✔
218
        for (ColKey col_key : table_dst->get_column_keys()) {
59,496✔
219
            StringData col_name = table_dst->get_column_name(col_key);
59,496✔
220
            ColKey col_key_src = table_src->get_column_key(col_name);
59,496✔
221
            if (!col_key_src) {
59,496✔
222
                columns_to_remove.push_back(col_name);
12✔
223
                continue;
12✔
224
            }
12✔
225
        }
59,496✔
226
        if (!allow_schema_additions && !columns_to_remove.empty()) {
18,408✔
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,408✔
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,592✔
240
        if (should_skip_table(group_src, table_key))
26,592✔
241
            continue;
8,200✔
242
        ConstTableRef table_src = group_src.get_table(table_key);
18,392✔
243
        StringData table_name = table_src->get_name();
18,392✔
244
        TableRef table_dst = group_dst.get_table(table_name);
18,392✔
245
        REALM_ASSERT(table_dst);
18,392✔
246
        for (ColKey col_key : table_src->get_column_keys()) {
59,604✔
247
            StringData col_name = table_src->get_column_name(col_key);
59,604✔
248
            ColKey col_key_dst = table_dst->get_column_key(col_name);
59,604✔
249
            if (!col_key_dst) {
59,604✔
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,420✔
295
                // column preexists in dest, make sure the types match
296
                if (col_key.get_type() != col_key_dst.get_type()) {
59,420✔
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,416✔
302
                ColumnAttrMask dst_col_attrs = col_key_dst.get_attrs();
59,416✔
303
                src_col_attrs.reset(ColumnAttr::col_attr_Indexed);
59,416✔
304
                dst_col_attrs.reset(ColumnAttr::col_attr_Indexed);
59,416✔
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,416✔
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,416✔
313
        }
59,604✔
314
    }
18,392✔
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,580✔
320
        if (should_skip_table(group_src, table_key))
26,580✔
321
            continue;
8,200✔
322
        auto table_src = group_src.get_table(table_key);
18,380✔
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,380✔
326
            continue;
688✔
327
        StringData table_name = table_src->get_name();
17,692✔
328
        logger.debug(util::LogCategory::reset, "Removing objects in '%1'", table_name);
17,692✔
329
        auto table_dst = group_dst.get_table(table_name);
17,692✔
330

331
        auto pk_col = table_dst->get_primary_key_column();
17,692✔
332
        REALM_ASSERT_DEBUG(pk_col); // sync realms always have a pk
17,692✔
333
        std::vector<std::pair<Mixed, ObjKey>> objects_to_remove;
17,692✔
334
        for (auto obj : *table_dst) {
27,348✔
335
            auto pk = obj.get_any(pk_col);
27,348✔
336
            if (!table_src->find_primary_key(pk)) {
27,348✔
337
                objects_to_remove.emplace_back(pk, obj.get_key());
2,036✔
338
            }
2,036✔
339
        }
27,348✔
340
        for (auto& pair : objects_to_remove) {
17,692✔
341
            logger.debug(util::LogCategory::reset, "  removing '%1'", pair.first);
2,036✔
342
            table_dst->remove_object(pair.second);
2,036✔
343
        }
2,036✔
344
    }
17,692✔
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,580✔
350
        ConstTableRef table_src = group_src.get_table(table_key);
26,580✔
351
        auto table_name = table_src->get_name();
26,580✔
352
        if (should_skip_table(group_src, table_key) || table_src->is_embedded())
26,580✔
353
            continue;
8,888✔
354
        TableRef table_dst = group_dst.get_table(table_name);
17,692✔
355
        auto pk_col = table_src->get_primary_key_column();
17,692✔
356
        REALM_ASSERT(pk_col);
17,692✔
357
        logger.debug(util::LogCategory::reset,
17,692✔
358
                     "Creating missing objects for table '%1', number of rows = %2, "
17,692✔
359
                     "primary_key_col = %3, primary_key_type = %4",
17,692✔
360
                     table_name, table_src->size(), pk_col.get_index().val, pk_col.get_type());
17,692✔
361
        for (const Obj& src : *table_src) {
29,620✔
362
            bool created = false;
29,620✔
363
            table_dst->create_object_with_primary_key(src.get_primary_key(), &created);
29,620✔
364
            if (created) {
29,620✔
365
                logger.debug(util::LogCategory::reset, "   created %1", src.get_primary_key());
4,308✔
366
            }
4,308✔
367
        }
29,620✔
368
    }
17,692✔
369

370
    converters::EmbeddedObjectConverter embedded_tracker;
7,736✔
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,580✔
375
        if (should_skip_table(group_src, table_key))
26,580✔
376
            continue;
8,200✔
377
        ConstTableRef table_src = group_src.get_table(table_key);
18,380✔
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,380✔
381
            continue;
688✔
382
        StringData table_name = table_src->get_name();
17,692✔
383
        TableRef table_dst = group_dst.get_table(table_name);
17,692✔
384
        REALM_ASSERT_EX(allow_schema_additions || table_src->get_column_count() == table_dst->get_column_count(),
17,692✔
385
                        allow_schema_additions, table_src->get_column_count(), table_dst->get_column_count());
17,692✔
386
        auto pk_col = table_src->get_primary_key_column();
17,692✔
387
        REALM_ASSERT(pk_col);
17,692✔
388
        logger.debug(util::LogCategory::reset,
17,692✔
389
                     "Updating values for table '%1', number of rows = %2, "
17,692✔
390
                     "number of columns = %3, primary_key_col = %4, "
17,692✔
391
                     "primary_key_type = %5",
17,692✔
392
                     table_name, table_src->size(), table_src->get_column_count(), pk_col.get_index().val,
17,692✔
393
                     pk_col.get_type());
17,692✔
394

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

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

403
            bool updated = false;
29,620✔
404
            converter.copy(src, dst, &updated);
29,620✔
405
            if (updated) {
29,620✔
406
                logger.debug(util::LogCategory::reset, "  updating %1", src_pk);
11,140✔
407
            }
11,140✔
408
        }
29,620✔
409
        embedded_tracker.process_pending();
17,692✔
410
    }
17,692✔
411
}
7,736✔
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,808✔
417
    if (auto previous_reset = sync::PendingResetStore::has_pending_reset(wt_local)) {
7,808✔
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✔
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✔
453
                        case ClientResyncMode::Manual:
✔
454
                            REALM_UNREACHABLE();
455
                    }
20✔
456
                    break;
12✔
457
                case ClientResyncMode::RecoverOrDiscard:
12✔
458
                    throw ClientResetFailed(util::format("Unexpected previous '%1' mode reset from %2 did not "
×
459
                                                         "succeed, giving up on '%3' mode to prevent a cycle",
×
460
                                                         previous_reset->mode, previous_reset->time, mode));
×
461
            }
32✔
462
        }
32✔
463
    }
36✔
464
    if (action == PendingReset::Action::ClientResetNoRecovery) {
7,788✔
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,784✔
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,784✔
480
    return mode;
7,784✔
481
}
7,788✔
482

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

493
    auto& repl_local = dynamic_cast<ClientReplication&>(*db_local.get_replication());
7,808✔
494
    auto& history_local = repl_local.get_history();
7,808✔
495
    history_local.ensure_updated(wt_local->get_version());
7,808✔
496
    VersionID old_version_local = wt_local->get_version_of_current_transaction();
7,808✔
497

498
    auto& repl_remote = dynamic_cast<ClientReplication&>(*db_remote.get_replication());
7,808✔
499
    auto& history_remote = repl_remote.get_history();
7,808✔
500

501
    sync::SaltedVersion fresh_server_version = {0, 0};
7,808✔
502
    sync::SaltedFileIdent fresh_file_ident = {0, 0};
7,808✔
503
    {
7,808✔
504
        SyncProgress remote_progress;
7,808✔
505
        sync::version_type remote_version_unused;
7,808✔
506
        history_remote.get_status(remote_version_unused, fresh_file_ident, remote_progress);
7,808✔
507
        fresh_server_version = remote_progress.latest_server_version;
7,808✔
508
    }
7,808✔
509

510
    logger.info(util::LogCategory::reset,
7,808✔
511
                "Client reset: path_local = %1, fresh_file_ident = (ident: %2, salt: %3), "
7,808✔
512
                "fresh_server_version = (ident: %4, salt: %5), remote_path = %6, requested_mode = %7, action = %8, "
7,808✔
513
                "actual_mode = %9, will_recover = %10, originating_error = %11",
7,808✔
514
                db_local.get_path(), fresh_file_ident.ident, fresh_file_ident.salt, fresh_server_version.version,
7,808✔
515
                fresh_server_version.salt, db_remote.get_path(), reset_config.mode, reset_config.action, actual_mode,
7,808✔
516
                recover_local_changes, reset_config.error);
7,808✔
517

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

525
        tr_remote = db_remote.start_write();
3,956✔
526
        recovered = process_recovered_changesets(*tr_remote, *frozen_pre_local_state, logger, local_changes);
3,956✔
527
    }
3,956✔
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,808✔
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(), fresh_file_ident, fresh_server_version,
7,808✔
538
                                          recovered);
7,808✔
539

540
    if (sub_store) {
7,808✔
541
        if (recover_local_changes) {
168✔
542
            sub_store->mark_active_as_complete(*wt_local);
120✔
543
        }
120✔
544
        else {
48✔
545
            sub_store->set_active_as_latest(*wt_local);
48✔
546
        }
48✔
547
    }
168✔
548

549
    wt_local->commit_and_continue_as_read();
7,808✔
550

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

558
    return recover_local_changes;
7,808✔
559
}
7,808✔
560

561
} // namespace _impl::client_reset
562
} // 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