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

realm / realm-core / github_pull_request_281922

31 Oct 2023 09:13AM UTC coverage: 90.445% (-0.08%) from 90.528%
github_pull_request_281922

Pull #7039

Evergreen

jedelbo
Merge branch 'next-major' into je/global-key
Pull Request #7039: Remove ability to synchronize objects without primary key

95324 of 175822 branches covered (0.0%)

101 of 105 new or added lines in 13 files covered. (96.19%)

238 existing lines in 19 files now uncovered.

232657 of 257235 relevant lines covered (90.45%)

6351359.67 hits per line

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

93.1
/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
{
14,414✔
47
    switch (mode) {
14,414✔
48
        case ClientResyncMode::Manual:
✔
49
            os << "Manual";
×
50
            break;
×
51
        case ClientResyncMode::DiscardLocal:
7,198✔
52
            os << "DiscardLocal";
7,198✔
53
            break;
7,198✔
54
        case ClientResyncMode::Recover:
7,140✔
55
            os << "Recover";
7,140✔
56
            break;
7,140✔
57
        case ClientResyncMode::RecoverOrDiscard:
76✔
58
            os << "RecoverOrDiscard";
76✔
59
            break;
76✔
60
    }
14,414✔
61
    return os;
14,414✔
62
}
14,414✔
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
{
221,368✔
70
    return !group.table_is_public(key);
221,368✔
71
}
221,368✔
72

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

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

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

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

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

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

3,368✔
192
    // Now the class tables are identical.
3,368✔
193
    size_t num_tables;
6,736✔
194
    {
6,736✔
195
        size_t num_tables_src = 0;
6,736✔
196
        for (auto table_key : group_src.get_table_keys()) {
23,020✔
197
            if (!should_skip_table(group_src, table_key))
23,020✔
198
                ++num_tables_src;
15,928✔
199
        }
23,020✔
200
        size_t num_tables_dst = 0;
6,736✔
201
        for (auto table_key : group_dst.get_table_keys()) {
30,176✔
202
            if (!should_skip_table(group_dst, table_key))
30,176✔
203
                ++num_tables_dst;
15,944✔
204
        }
30,176✔
205
        REALM_ASSERT_EX(allow_schema_additions || num_tables_src == num_tables_dst, num_tables_src, num_tables_dst);
6,736✔
206
        num_tables = num_tables_src;
6,736✔
207
    }
6,736✔
208
    logger.debug(util::LogCategory::reset, "The number of tables is %1", num_tables);
6,736✔
209

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

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

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

3,366✔
317
    // Now the schemas are identical.
3,366✔
318

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

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

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

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

7,612✔
396
        converters::InterRealmObjectConverter converter(table_src, table_dst, &embedded_tracker);
15,224✔
397

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

10,704✔
404
            bool updated = false;
21,408✔
405
            converter.copy(src, dst, &updated);
21,408✔
406
            if (updated) {
21,408✔
407
                logger.debug(util::LogCategory::reset, "  updating %1", src_pk);
7,368✔
408
            }
7,368✔
409
        }
21,408✔
410
        embedded_tracker.process_pending();
15,224✔
411
    }
15,224✔
412
}
6,724✔
413

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

422
void remove_pending_client_resets(TransactionRef wt)
423
{
152✔
424
    REALM_ASSERT(wt);
152✔
425
    if (auto table = wt->get_table(s_meta_reset_table_name)) {
152✔
426
        if (table->size()) {
152✔
427
            table->clear();
152✔
428
        }
152✔
429
    }
152✔
430
}
152✔
431

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

473
void track_reset(TransactionRef wt, ClientResyncMode mode)
474
{
6,780✔
475
    REALM_ASSERT(wt);
6,780✔
476
    REALM_ASSERT(mode != ClientResyncMode::Manual);
6,780✔
477
    TableRef table = wt->get_table(s_meta_reset_table_name);
6,780✔
478
    ColKey version_col, timestamp_col, type_col;
6,780✔
479
    if (!table) {
6,780✔
480
        table = wt->add_table_with_primary_key(s_meta_reset_table_name, type_ObjectId, s_pk_col_name);
6,744✔
481
        REALM_ASSERT(table);
6,744✔
482
        version_col = table->add_column(type_Int, s_version_column_name);
6,744✔
483
        timestamp_col = table->add_column(type_Timestamp, s_timestamp_col_name);
6,744✔
484
        type_col = table->add_column(type_Int, s_reset_type_col_name);
6,744✔
485
    }
6,744✔
486
    else {
36✔
487
        version_col = table->get_column_key(s_version_column_name);
36✔
488
        timestamp_col = table->get_column_key(s_timestamp_col_name);
36✔
489
        type_col = table->get_column_key(s_reset_type_col_name);
36✔
490
    }
36✔
491
    REALM_ASSERT(version_col);
6,780✔
492
    REALM_ASSERT(timestamp_col);
6,780✔
493
    REALM_ASSERT(type_col);
6,780✔
494
    int64_t mode_val = 0; // Discard
6,780✔
495
    if (mode == ClientResyncMode::Recover || mode == ClientResyncMode::RecoverOrDiscard) {
6,780✔
496
        mode_val = 1; // Recover
3,400✔
497
    }
3,400✔
498

3,390✔
499
    if (table->size() > 1) {
6,780✔
500
        // this may happen if a future version of this code changes the format and expectations around reset metadata.
501
        throw ClientResetFailed(
×
502
            util::format("Previous client resets detected (%1) but only one is expected.", table->size()));
×
503
    }
×
504
    table->create_object_with_primary_key(ObjectId::gen(),
6,780✔
505
                                          {{version_col, metadata_version},
6,780✔
506
                                           {timestamp_col, Timestamp(std::chrono::system_clock::now())},
6,780✔
507
                                           {type_col, mode_val}});
6,780✔
508
}
6,780✔
509

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

567
LocalVersionIDs perform_client_reset_diff(DBRef db_local, DBRef db_remote, sync::SaltedFileIdent client_file_ident,
568
                                          util::Logger& logger, ClientResyncMode mode, bool recovery_is_allowed,
569
                                          bool* did_recover_out, sync::SubscriptionStore* sub_store,
570
                                          util::UniqueFunction<void(int64_t)> on_flx_version_complete)
571
{
6,776✔
572
    REALM_ASSERT(db_local);
6,776✔
573
    REALM_ASSERT(db_remote);
6,776✔
574
    logger.info(util::LogCategory::reset,
6,776✔
575
                "Client reset, path_local = %1, "
6,776✔
576
                "client_file_ident.ident = %2, "
6,776✔
577
                "client_file_ident.salt = %3,"
6,776✔
578
                "remote = %4, mode = %5, recovery_is_allowed = %6",
6,776✔
579
                db_local->get_path(), client_file_ident.ident, client_file_ident.salt, db_remote->get_path(), mode,
6,776✔
580
                recovery_is_allowed);
6,776✔
581

3,388✔
582
    auto remake_active_subscription = [&]() {
6,748✔
583
        if (!sub_store) {
6,720✔
584
            return;
6,616✔
585
        }
6,616✔
586
        auto mut_subs = sub_store->get_active().make_mutable_copy();
104✔
587
        int64_t before_version = mut_subs.version();
104✔
588
        mut_subs.update_state(sync::SubscriptionSet::State::Complete);
104✔
589
        auto sub = std::move(mut_subs).commit();
104✔
590
        if (on_flx_version_complete) {
104✔
591
            on_flx_version_complete(sub.version());
104✔
592
        }
104✔
593
        logger.info(util::LogCategory::reset,
104✔
594
                    "Recreated the active subscription set in the complete state (%1 -> %2)", before_version,
104✔
595
                    sub.version());
104✔
596
    };
104✔
597

3,388✔
598
    auto frozen_pre_local_state = db_local->start_frozen();
6,776✔
599
    auto wt_local = db_local->start_write();
6,776✔
600
    auto history_local = dynamic_cast<ClientHistory*>(wt_local->get_replication()->_get_history_write());
6,776✔
601
    REALM_ASSERT(history_local);
6,776✔
602
    VersionID old_version_local = wt_local->get_version_of_current_transaction();
6,776✔
603
    wt_local->get_history()->ensure_updated(old_version_local.version);
6,776✔
604
    SaltedFileIdent orig_file_ident;
6,776✔
605
    {
6,776✔
606
        sync::version_type old_version_unused;
6,776✔
607
        SyncProgress old_progress_unused;
6,776✔
608
        history_local->get_status(old_version_unused, orig_file_ident, old_progress_unused);
6,776✔
609
    }
6,776✔
610
    std::vector<ClientHistory::LocalChange> local_changes;
6,776✔
611

3,388✔
612
    mode = reset_precheck_guard(wt_local, mode, recovery_is_allowed, logger);
6,776✔
613
    bool recover_local_changes = (mode == ClientResyncMode::Recover || mode == ClientResyncMode::RecoverOrDiscard);
6,776✔
614

3,388✔
615
    if (recover_local_changes) {
6,776✔
616
        local_changes = history_local->get_local_changes(wt_local->get_version());
3,388✔
617
        logger.info(util::LogCategory::reset, "Local changesets to recover: %1", local_changes.size());
3,388✔
618
    }
3,388✔
619

3,388✔
620
    sync::SaltedVersion fresh_server_version = {0, 0};
6,776✔
621
    auto wt_remote = db_remote->start_write();
6,776✔
622
    auto history_remote = dynamic_cast<ClientHistory*>(wt_remote->get_replication()->_get_history_write());
6,776✔
623
    REALM_ASSERT(history_remote);
6,776✔
624

3,388✔
625
    SyncProgress remote_progress;
6,776✔
626
    {
6,776✔
627
        sync::version_type remote_version_unused;
6,776✔
628
        SaltedFileIdent remote_ident_unused;
6,776✔
629
        history_remote->get_status(remote_version_unused, remote_ident_unused, remote_progress);
6,776✔
630
    }
6,776✔
631
    fresh_server_version = remote_progress.latest_server_version;
6,776✔
632
    BinaryData recovered_changeset;
6,776✔
633

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

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

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

3,354✔
693
        // Finally, the local Realm is committed. The changes to the remote Realm are discarded.
3,354✔
694
        wt_remote->rollback_and_continue_as_read();
6,708✔
695
        wt_local->commit_and_continue_as_read();
6,708✔
696

3,354✔
697
        // If in FLX mode, make a copy of the active subscription set and mark it as
3,354✔
698
        // complete. This will cause all other subscription sets to become superceded.
3,354✔
699
        // In DiscardLocal mode, only the active subscription set is preserved, so we
3,354✔
700
        // are done.
3,354✔
701
        remake_active_subscription();
6,708✔
702
    }
6,708✔
703

3,388✔
704
    if (did_recover_out) {
6,776✔
705
        *did_recover_out = recover_local_changes;
268✔
706
    }
268✔
707
    VersionID new_version_local = wt_local->get_version_of_current_transaction();
6,776✔
708
    logger.info(util::LogCategory::reset,
6,776✔
709
                "perform_client_reset_diff is done, old_version.version = %1, "
6,776✔
710
                "old_version.index = %2, new_version.version = %3, "
6,776✔
711
                "new_version.index = %4",
6,776✔
712
                old_version_local.version, old_version_local.index, new_version_local.version,
6,776✔
713
                new_version_local.index);
6,776✔
714

3,388✔
715
    return LocalVersionIDs{old_version_local, new_version_local};
6,776✔
716
}
6,776✔
717

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

© 2025 Coveralls, Inc