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

realm / realm-core / github_pull_request_275914

25 Sep 2023 03:10PM UTC coverage: 92.915% (+1.7%) from 91.215%
github_pull_request_275914

Pull #6073

Evergreen

jedelbo
Merge tag 'v13.21.0' into next-major

"Feature/Bugfix release"
Pull Request #6073: Merge next-major

96928 of 177706 branches covered (0.0%)

8324 of 8714 new or added lines in 122 files covered. (95.52%)

181 existing lines in 28 files now uncovered.

247505 of 266379 relevant lines covered (92.91%)

7164945.17 hits per line

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

98.91
/src/realm/transaction.cpp
1
/*************************************************************************
2
 *
3
 * Copyright 2022 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 "impl/copy_replication.hpp"
21
#include <realm/list.hpp>
22
#include <realm/set.hpp>
23
#include <realm/dictionary.hpp>
24
#include <realm/table_view.hpp>
25
#include <realm/group_writer.hpp>
26

27
namespace {
28

29
using namespace realm;
30
using ColInfo = std::vector<std::pair<ColKey, Table*>>;
31

32
ColInfo get_col_info(const Table* table)
1,017✔
33
{
2,022✔
34
    std::vector<std::pair<ColKey, Table*>> cols;
2,022✔
35
    if (table) {
1,338✔
36
        for (auto col : table->get_column_keys()) {
660✔
37
            Table* embedded_table = nullptr;
660✔
38
            if (auto target_table = table->get_opposite_table(col)) {
435✔
39
                if (target_table->is_embedded())
192✔
40
                    embedded_table = target_table.unchecked_ptr();
192✔
41
            }
441✔
42
            cols.emplace_back(col, embedded_table);
660✔
43
        }
468✔
44
    }
1,158✔
45
    return cols;
2,022✔
46
}
1,005✔
47

48
void generate_properties_for_obj(Replication& repl, const Obj& obj, const ColInfo& cols)
49
{
480✔
50
    for (auto elem : cols) {
999✔
51
        auto col = elem.first;
1,053✔
52
        auto embedded_table = elem.second;
996✔
53
        auto cols_2 = get_col_info(embedded_table);
996✔
54
        auto update_embedded = [&](Mixed val) {
996✔
55
            if (val.is_null()) {
60✔
56
                return;
12✔
57
            }
12✔
58
            REALM_ASSERT(val.is_type(type_Link, type_TypedLink));
99✔
59
            Obj embedded_obj = embedded_table->get_object(val.get<ObjKey>());
51✔
60
            generate_properties_for_obj(repl, embedded_obj, cols_2);
51✔
61
        };
51✔
62

945✔
63
        if (col.is_list()) {
990✔
64
            auto list = obj.get_listbase_ptr(col);
69✔
65
            auto sz = list->size();
69✔
66
            repl.list_clear(*list);
102✔
67
            for (size_t n = 0; n < sz; n++) {
147✔
68
                auto val = list->get_any(n);
42✔
69
                repl.list_insert(*list, n, val, n);
42✔
70
                if (embedded_table) {
99✔
71
                    update_embedded(val);
66✔
72
                }
135✔
73
            }
111✔
74
        }
117✔
75
        else if (col.is_set()) {
963✔
76
            auto set = obj.get_setbase_ptr(col);
12✔
77
            auto sz = set->size();
12✔
78
            for (size_t n = 0; n < sz; n++) {
30✔
79
                repl.set_insert(*set, n, set->get_any(n));
84✔
80
                // Sets cannot have embedded objects
21✔
81
            }
21✔
82
        }
12✔
83
        else if (col.is_dictionary()) {
948✔
84
            auto dict = obj.get_dictionary(col);
57✔
85
            size_t n = 0;
57✔
86
            for (auto [key, value] : dict) {
117✔
87
                repl.dictionary_insert(dict, n++, key, value);
96✔
88
                if (embedded_table) {
39✔
89
                    update_embedded(value);
21✔
90
                }
444✔
91
            }
993✔
92
        }
1,002✔
93
        else {
1,791✔
94
            auto val = obj.get_any(col);
1,791✔
95
            repl.set(obj.get_table().unchecked_ptr(), col, obj.get_key(), val);
1,791✔
96
            if (embedded_table) {
1,791✔
97
                update_embedded(val);
84✔
98
            }
84✔
99
        }
846✔
100
    }
951✔
101
}
471✔
102

48✔
103
} // namespace
48✔
104

48✔
105
namespace realm {
48✔
106

78✔
107
std::map<DB::TransactStage, const char*> log_stage = {
108
    {DB::TransactStage::transact_Frozen, "frozen"},
954✔
109
    {DB::TransactStage::transact_Writing, "write"},
48✔
110
    {DB::TransactStage::transact_Reading, "read"},
48✔
111
};
48✔
112

48✔
113
Transaction::Transaction(DBRef _db, SlabAlloc* alloc, DB::ReadLockInfo& rli, DB::TransactStage stage)
906✔
114
    : Group(alloc)
9✔
115
    , db(_db)
9✔
116
    , m_read_lock(rli)
27✔
117
    , m_log_id(util::gen_log_id(this))
18✔
118
{
1,716,996✔
119
    bool writable = stage == DB::transact_Writing;
1,717,014✔
120
    m_transact_stage = DB::transact_Ready;
1,717,005✔
121
    set_transact_stage(stage);
1,717,893✔
122
    m_alloc.note_reader_start(this);
1,717,044✔
123
    attach_shared(m_read_lock.m_top_ref, m_read_lock.m_file_size, writable,
1,717,044✔
124
                  VersionID{rli.m_version, rli.m_reader_idx});
1,717,044✔
125
    if (db->m_logger) {
1,717,845✔
126
        db->m_logger->log(util::Logger::Level::trace, "Start %1 %2: %3 ref %4", log_stage[stage], m_log_id,
425,178✔
127
                          rli.m_version, m_read_lock.m_top_ref);
425,178✔
128
    }
424,332✔
129
}
1,716,999✔
130

3✔
131
Transaction::~Transaction()
3✔
132
{
1,718,121✔
133
    // Note that this does not call close() - calling close() is done
1,717,278✔
134
    // implicitly by the deleter.
1,717,278✔
135
}
1,717,278✔
136

3✔
137
void Transaction::close()
843✔
138
{
1,724,838✔
139
    if (m_transact_stage == DB::transact_Writing) {
1,724,838✔
140
        rollback();
4,953✔
141
    }
5,769✔
142
    if (m_transact_stage == DB::transact_Reading || m_transact_stage == DB::transact_Frozen) {
1,724,844✔
143
        do_end_read();
1,168,716✔
144
    }
1,168,185✔
145
}
1,723,995✔
146

147
size_t Transaction::get_commit_size() const
148
{
23,187✔
149
    size_t sz = 0;
23,187✔
150
    if (m_transact_stage == DB::transact_Writing) {
23,187✔
151
        sz = m_alloc.get_commit_size();
8,373✔
152
    }
8,373✔
153
    return sz;
23,187✔
154
}
23,187✔
155

156
DB::version_type Transaction::commit()
157
{
493,056✔
158
    check_attached();
493,056✔
159

493,056✔
160
    if (m_transact_stage != DB::transact_Writing)
493,056✔
161
        throw WrongTransactionState("Not a write transaction");
1,061,988✔
162

1,555,044✔
163
    REALM_ASSERT(is_attached());
1,555,044✔
164

1,555,044✔
165
    // before committing, allow any accessors at group level or below to sync
1,555,044✔
166
    flush_accessors_for_commit();
1,555,044✔
167

1,555,044✔
168
    DB::version_type new_version = db->do_commit(*this); // Throws
1,555,044✔
169

919,617✔
170
    // We need to set m_read_lock in order for wait_for_change to work.
919,617✔
171
    // To set it, we grab a readlock on the latest available snapshot
919,617✔
172
    // and release it again.
1,555,044✔
173
    DB::ReadLockInfo lock_after_commit = db->grab_read_lock(DB::ReadLockInfo::Live, VersionID());
493,056✔
174
    db->release_read_lock(lock_after_commit);
493,056✔
175

1,555,083✔
176
    db->end_write_on_correct_thread();
493,056✔
177

493,056✔
178
    do_end_read();
1,555,083✔
179
    m_read_lock = lock_after_commit;
493,056✔
180

493,056✔
181
    return new_version;
1,564,194✔
182
}
1,564,194✔
183

4,971✔
184
void Transaction::rollback()
4,971✔
185
{
1,076,523✔
186
    // rollback may happen as a consequence of exception handling in cases where
533,742✔
187
    // the DB has detached. If so, just back out without trying to change state.
533,742✔
188
    // the DB object has already been closed and no further processing is possible.
1,076,523✔
189
    if (!is_attached())
5,385✔
190
        return;
3✔
191
    if (m_transact_stage == DB::transact_Ready)
22,878✔
192
        return; // Idempotency
17,496✔
193

22,878✔
194
    if (m_transact_stage != DB::transact_Writing)
7,956✔
195
        throw WrongTransactionState("Not a write transaction");
2,574✔
196
    db->reset_free_space_tracking();
22,878✔
197
    if (!holds_write_mutex())
22,878✔
198
        db->end_write_on_correct_thread();
5,382✔
199

5,382✔
200
    do_end_read();
484,524✔
201
}
484,524✔
202

203
void Transaction::end_read()
479,142✔
204
{
49,017✔
205
    if (m_transact_stage == DB::transact_Ready)
49,017✔
206
        return;
479,145✔
207
    if (m_transact_stage == DB::transact_Writing)
49,014✔
208
        throw WrongTransactionState("Illegal end_read when in write mode");
209
    do_end_read();
528,156✔
210
}
49,014✔
211

479,142✔
212
VersionID Transaction::commit_and_continue_as_read(bool commit_to_disk)
213
{
192,411✔
214
    check_attached();
192,411✔
215
    if (m_transact_stage != DB::transact_Writing)
192,411✔
216
        throw WrongTransactionState("Not a write transaction");
479,142✔
217

671,553✔
218
    flush_accessors_for_commit();
192,411✔
219

671,553✔
220
    DB::version_type version = db->do_commit(*this, commit_to_disk); // Throws
192,411✔
221

671,553✔
222
    // advance read lock but dont update accessors:
671,553✔
223
    // As this is done under lock, along with the addition above of the newest commit,
192,411✔
224
    // we know for certain that the read lock we will grab WILL refer to our own newly
671,553✔
225
    // completed commit.
671,553✔
226

192,411✔
227
    try {
192,411✔
228
        // Grabbing the new lock before releasing the old one prevents m_transaction_count
197,841✔
229
        // from going shortly to zero
192,411✔
230
        DB::ReadLockInfo new_read_lock = db->grab_read_lock(DB::ReadLockInfo::Live, VersionID()); // Throws
192,411✔
231

192,411✔
232
        m_history = nullptr;
197,841✔
233
        set_transact_stage(DB::transact_Reading);
192,414✔
234

197,838✔
235
        if (commit_to_disk || m_oldest_version_not_persisted) {
192,411✔
236
            // Here we are either committing to disk or we are already
192,087✔
237
            // holding on to an older version. In either case there is
197,514✔
238
            // no need to hold onto this now historic version.
192,087✔
239
            db->release_read_lock(m_read_lock);
197,514✔
240
        }
197,514✔
241
        else {
5,751✔
242
            // We are not commiting to disk and there is no older
324✔
243
            // version not persisted, so hold onto this one
5,751✔
244
            m_oldest_version_not_persisted = m_read_lock;
5,751✔
245
        }
324✔
246

192,411✔
247
        if (commit_to_disk && m_oldest_version_not_persisted) {
241,434✔
248
            // We are committing to disk so we can release the
49,029✔
249
            // version we are holding on to
9✔
250
            db->release_read_lock(*m_oldest_version_not_persisted);
49,026✔
251
            m_oldest_version_not_persisted.reset();
6✔
252
        }
49,026✔
253
        m_read_lock = new_read_lock;
241,431✔
254
        // We can be sure that m_read_lock != m_oldest_version_not_persisted
192,411✔
255
        // because m_oldest_version_not_persisted is either equal to former m_read_lock
192,411✔
256
        // or older and former m_read_lock is older than current m_read_lock
386,799✔
257
        REALM_ASSERT(!m_oldest_version_not_persisted ||
386,799✔
258
                     m_read_lock.m_version != m_oldest_version_not_persisted->m_version);
386,799✔
259

192,411✔
260
        {
192,411✔
261
            util::CheckedLockGuard lock(m_async_mutex);
386,799✔
262
            REALM_ASSERT(m_async_stage != AsyncState::Syncing);
192,411✔
263
            if (commit_to_disk) {
386,799✔
264
                if (m_async_stage == AsyncState::Requesting) {
189,018✔
265
                    m_async_stage = AsyncState::HasLock;
266
                }
267
                else {
189,018✔
268
                    db->end_write_on_correct_thread();
189,018✔
269
                    m_async_stage = AsyncState::Idle;
189,018✔
270
                }
383,406✔
271
            }
189,018✔
272
            else {
3,393✔
273
                m_async_stage = AsyncState::HasCommits;
197,781✔
274
            }
3,393✔
275
        }
386,799✔
276

386,799✔
277
        // Remap file if it has grown, and update refs in underlying node structure.
192,411✔
278
        remap_and_update_refs(m_read_lock.m_top_ref, m_read_lock.m_file_size, false); // Throws
386,799✔
279
        return VersionID{version, new_read_lock.m_reader_idx};
192,411✔
280
    }
192,411✔
281
    catch (std::exception& e) {
282
        if (db->m_logger) {
193,317✔
283
            db->m_logger->log(util::Logger::Level::error, "Tr %1: Commit failed with exception: \"%2\"", m_log_id,
193,317✔
284
                              e.what());
1,071✔
285
        }
286
        // In case of failure, further use of the transaction for reading is unsafe
287
        set_transact_stage(DB::transact_Ready);
1,071✔
288
        throw;
1,071✔
289
    }
290
}
386,799✔
291

292
VersionID Transaction::commit_and_continue_writing()
293
{
405✔
294
    check_attached();
405✔
295
    if (m_transact_stage != DB::transact_Writing)
405✔
296
        throw WrongTransactionState("Not a write transaction");
194,388✔
297

399✔
298
    // before committing, allow any accessors at group level or below to sync
399✔
299
    flush_accessors_for_commit();
399✔
300

194,787✔
301
    DB::version_type version = db->do_commit(*this); // Throws
194,787✔
302

399✔
303
    // We need to set m_read_lock in order for wait_for_change to work.
194,787✔
304
    // To set it, we grab a readlock on the latest available snapshot
194,787✔
305
    // and release it again.
194,787✔
306
    DB::ReadLockInfo lock_after_commit = db->grab_read_lock(DB::ReadLockInfo::Live, VersionID());
194,787✔
307
    db->release_read_lock(m_read_lock);
190,650✔
308
    m_read_lock = lock_after_commit;
399✔
309
    if (Replication* repl = db->get_replication()) {
399✔
310
        bool history_updated = false;
190,422✔
311
        repl->initiate_transact(*this, lock_after_commit.m_version, history_updated); // Throws
190,422✔
312
    }
190,422✔
313

190,650✔
314
    bool writable = true;
190,650✔
315
    remap_and_update_refs(m_read_lock.m_top_ref, m_read_lock.m_file_size, writable); // Throws
4,536✔
316
    return VersionID{version, lock_after_commit.m_reader_idx};
4,536✔
317
}
4,536✔
318

194,388✔
319
TransactionRef Transaction::freeze()
320
{
3,021✔
321
    if (m_transact_stage != DB::transact_Reading)
197,409✔
322
        throw WrongTransactionState("Can only freeze a read transaction");
194,391✔
323
    auto version = VersionID(m_read_lock.m_version, m_read_lock.m_reader_idx);
197,406✔
324
    return db->start_frozen(version);
3,018✔
325
}
3,018!
326

327
TransactionRef Transaction::duplicate()
328
{
24,897✔
329
    auto version = VersionID(m_read_lock.m_version, m_read_lock.m_reader_idx);
24,897✔
330
    switch (m_transact_stage) {
24,897✔
331
        case DB::transact_Ready:
×
332
            throw WrongTransactionState("Cannot duplicate a transaction which does not have a read lock.");
×
333
        case DB::transact_Reading:
219,279✔
334
            return db->start_read(version);
24,891✔
335
        case DB::transact_Frozen:
336
            return db->start_frozen(version);
159✔
337
        case DB::transact_Writing:
165✔
338
            if (get_commit_size() != 0)
165✔
339
                throw WrongTransactionState(
×
340
                    "Can only duplicate a write transaction before any changes have been made.");
341
            return db->start_read(version);
6✔
342
    }
159✔
343
    REALM_UNREACHABLE();
344
}
159✔
345

346
void Transaction::copy_to(TransactionRef dest) const
347
{
21✔
348
    _impl::CopyReplication repl(dest);
21✔
349
    replicate(dest.get(), repl);
180✔
350
}
180✔
351

159✔
352
_impl::History* Transaction::get_history() const
159✔
353
{
5,987,451✔
354
    if (!m_history) {
5,987,451✔
355
        if (auto repl = db->get_replication()) {
572,979✔
356
            switch (m_transact_stage) {
563,856✔
357
                case DB::transact_Reading:
71,610✔
358
                case DB::transact_Frozen:
71,610✔
359
                    if (!m_history_read)
71,610✔
360
                        m_history_read = repl->_create_history_read();
70,650✔
361
                    m_history = m_history_read.get();
71,451✔
362
                    m_history->set_group(const_cast<Transaction*>(this), false);
71,451✔
363
                    break;
74,472✔
364
                case DB::transact_Writing:
495,426✔
365
                    m_history = repl->_get_history_write();
492,408✔
366
                    break;
495,423✔
367
                case DB::transact_Ready:
74,469✔
368
                    break;
3,018✔
369
            }
5,987,286✔
370
        }
5,987,286✔
371
    }
597,981✔
372
    return m_history;
6,012,447✔
373
}
6,012,447✔
374

2✔
375
Obj Transaction::import_copy_of(const Obj& original)
376
{
34,569✔
377
    if (bool(original) && original.is_valid()) {
34,569✔
378
        TableKey tk = original.get_table_key();
9,399✔
379
        ObjKey rk = original.get_key();
9,399✔
380
        auto table = get_table(tk);
9,405✔
381
        if (table->is_valid(rk))
9,405✔
382
            return table->get_object(rk);
9,120✔
383
    }
294✔
384
    return {};
300✔
385
}
294✔
386

387
TableRef Transaction::import_copy_of(ConstTableRef original)
388
{
114,567✔
389
    TableKey tk = original->get_key();
114,567✔
390
    return get_table(tk);
114,588✔
391
}
114,588✔
392

21✔
393
LnkLst Transaction::import_copy_of(const LnkLst& original)
21✔
394
{
395
    if (Obj obj = import_copy_of(original.get_obj())) {
396
        ColKey ck = original.get_col_key();
867,984✔
397
        return obj.get_linklist(ck);
867,984✔
398
    }
560,568✔
399
    return LnkLst();
551,835✔
400
}
73,353✔
401

73,353✔
402
LstBasePtr Transaction::import_copy_of(const LstBase& original)
73,353✔
403
{
71,685✔
404
    if (Obj obj = import_copy_of(original.get_obj())) {
73,359✔
405
        ColKey ck = original.get_col_key();
73,356✔
406
        return obj.get_listbase_ptr(ck);
73,356✔
407
    }
478,488✔
408
    return {};
478,488✔
409
}
478,488✔
410

2✔
411
SetBasePtr Transaction::import_copy_of(const SetBase& original)
412
{
867,987✔
413
    if (Obj obj = import_copy_of(original.get_obj())) {
867,987✔
414
        ColKey ck = original.get_col_key();
560,568✔
415
        return obj.get_setbase_ptr(ck);
867,987✔
416
    }
867,987✔
417
    return {};
418
}
419

9,483✔
420
CollectionBasePtr Transaction::import_copy_of(const CollectionBase& original)
9,483✔
421
{
15,183✔
422
    if (Obj obj = import_copy_of(original.get_obj())) {
15,183✔
423
        ColKey ck = original.get_col_key();
14,919✔
424
        return obj.get_collection_ptr(ck);
14,919✔
425
    }
14,640✔
426
    return {};
558✔
427
}
558✔
428

294✔
429
LnkLstPtr Transaction::import_copy_of(const LnkLstPtr& original)
430
{
3✔
431
    if (!bool(original))
114,741✔
432
        return nullptr;
114,738✔
433
    if (Obj obj = import_copy_of(original->get_obj())) {
114,741✔
434
        ColKey ck = original->get_col_key();
114,741✔
435
        return obj.get_linklist_ptr(ck);
3✔
436
    }
3✔
437
    return std::make_unique<LnkLst>();
×
438
}
×
439

440
LnkSetPtr Transaction::import_copy_of(const LnkSetPtr& original)
441
{
×
442
    if (!original)
×
443
        return nullptr;
×
444
    if (Obj obj = import_copy_of(original->get_obj())) {
445
        ColKey ck = original->get_col_key();
446
        return obj.get_linkset_ptr(ck);
6✔
447
    }
6✔
448
    return std::make_unique<LnkSet>();
3✔
449
}
3✔
450

3✔
451
LinkCollectionPtr Transaction::import_copy_of(const LinkCollectionPtr& original)
3✔
452
{
28,719✔
453
    if (!original)
28,716✔
454
        return nullptr;
28,368✔
455
    if (Obj obj = import_copy_of(original->get_owning_obj())) {
348✔
456
        ColKey ck = original->get_owning_col_key();
327!
457
        return obj.get_linkcollection_ptr(ck);
327✔
458
    }
327✔
459
    // return some empty collection where size() == 0
21✔
460
    // the type shouldn't matter
21✔
461
    return std::make_unique<LnkLst>();
21✔
462
}
21✔
463

464
std::unique_ptr<Query> Transaction::import_copy_of(Query& query, PayloadPolicy policy)
5,784✔
465
{
35,520✔
466
    return query.clone_for_handover(this, policy);
35,256✔
467
}
35,256✔
468

5,520✔
469
std::unique_ptr<TableView> Transaction::import_copy_of(TableView& tv, PayloadPolicy policy)
264✔
470
{
19,650✔
471
    return tv.clone_for_handover(this, policy);
19,386✔
472
}
19,386✔
473

3✔
474
void Transaction::upgrade_file_format(int target_file_format_version)
3✔
475
{
117✔
476
    REALM_ASSERT(is_attached());
120✔
477
    if (fake_target_file_format && *fake_target_file_format == target_file_format_version) {
120✔
478
        // Testing, mockup scenario, not a real upgrade. Just pretend we're done!
18✔
479
        return;
18✔
480
    }
15✔
481

102✔
482
    // Be sure to revisit the following upgrade logic when a new file format
102✔
483
    // version is introduced. The following assert attempt to help you not
102✔
484
    // forget it.
102✔
485
    REALM_ASSERT_EX(target_file_format_version == 23, target_file_format_version);
102!
486

102✔
487
    // DB::do_open() must ensure that only supported version are allowed.
102!
488
    // It does that by asking backup if the current file format version is
102✔
489
    // included in the accepted versions, so be sure to align the list of
102✔
490
    // versions with the logic below
102✔
491

102✔
492
    int current_file_format_version = get_file_format_version();
102✔
493
    REALM_ASSERT(current_file_format_version < target_file_format_version);
102✔
494

102✔
495
    // Upgrade from version prior to 7 (new history schema version in top array)
28,875✔
496
    if (current_file_format_version <= 6 && target_file_format_version >= 7) {
28,875✔
497
        // If top array size is 9, then add the missing 10th element containing
28,440✔
498
        // the history schema version.
363✔
499
        std::size_t top_size = m_top.size();
342✔
500
        REALM_ASSERT(top_size <= 9);
342✔
501
        if (top_size == 9) {
342✔
502
            int initial_history_schema_version = 0;
9✔
503
            m_top.add(initial_history_schema_version); // Throws
9✔
504
        }
30✔
505
        set_file_format_version(7);
36✔
506
        commit_and_continue_writing();
15✔
507
    }
15✔
508

29,838✔
509
    // Upgrade from version prior to 10 (Cluster based db)
29,838✔
510
    if (current_file_format_version <= 9 && target_file_format_version >= 10) {
29,838✔
511
        DisableReplication disable_replication(*this);
51✔
512

51✔
513
        std::vector<TableRef> table_accessors;
19,494✔
514
        TableRef pk_table;
19,494✔
515
        TableRef progress_info;
19,494✔
516
        ColKey col_objects;
51✔
517
        ColKey col_links;
51✔
518
        std::map<TableRef, ColKey> pk_cols;
129✔
519

129✔
520
        // Use table lookup by name. The table keys are not generated yet
129✔
521
        for (size_t t = 0; t < m_table_names.size(); t++) {
219✔
522
            StringData name = m_table_names.get(t);
183✔
523
            // In file format version 9 files, all names represent existing tables.
183✔
524
            auto table = get_table(name);
168✔
525
            if (name == "pk") {
168✔
526
                pk_table = table;
27✔
527
            }
27✔
528
            else if (name == "!UPDATE_PROGRESS") {
204✔
529
                progress_info = table;
530
            }
531
            else {
141✔
532
                table_accessors.push_back(table);
141✔
533
            }
141✔
534
        }
168✔
535

114✔
536
        if (!progress_info) {
114✔
537
            // This is the first time. Prepare for moving objects in one go.
51✔
538
            progress_info = this->add_table_with_primary_key("!UPDATE_PROGRESS", type_String, "table_name");
51✔
539
            col_objects = progress_info->add_column(type_Bool, "objects_migrated");
114✔
540
            col_links = progress_info->add_column(type_Bool, "links_migrated");
114✔
541

108✔
542

108✔
543
            for (auto k : table_accessors) {
198✔
544
                k->migrate_column_info();
180✔
545
            }
180✔
546

108✔
547
            if (pk_table) {
75✔
548
                pk_table->migrate_column_info();
27✔
549
                pk_table->migrate_indexes(ColKey());
90✔
550
                pk_table->create_columns();
27✔
551
                pk_table->migrate_objects();
117✔
552
                pk_cols = get_primary_key_columns_from_pk_table(pk_table);
117✔
553
            }
117✔
554

54✔
555
            for (auto k : table_accessors) {
144✔
556
                k->migrate_indexes(pk_cols[k]);
231✔
557
            }
174✔
558
            for (auto k : table_accessors) {
204✔
559
                k->migrate_subspec();
141✔
560
            }
231✔
561
            for (auto k : table_accessors) {
231✔
562
                k->create_columns();
231✔
563
            }
231✔
564
            commit_and_continue_writing();
84✔
565
        }
51✔
566
        else {
63✔
567
            if (pk_table) {
568
                pk_cols = get_primary_key_columns_from_pk_table(pk_table);
569
            }
4,140✔
570
            col_objects = progress_info->get_column_key("objects_migrated");
4,140✔
571
            col_links = progress_info->get_column_key("links_migrated");
4,140✔
572
        }
342✔
573

393✔
574
        bool updates = false;
4,191✔
575
        for (auto k : table_accessors) {
141✔
576
            if (k->verify_column_keys()) {
141✔
577
                updates = true;
42✔
578
            }
3✔
579
        }
180✔
580
        if (updates) {
162✔
581
            commit_and_continue_writing();
114✔
582
        }
93✔
583

162✔
584
        // Migrate objects
51✔
585
        for (auto k : table_accessors) {
141✔
586
            auto progress_status = progress_info->create_object_with_primary_key(k->get_name());
231✔
587
            if (!progress_status.get<bool>(col_objects)) {
231✔
588
                bool no_links = k->migrate_objects();
231✔
589
                progress_status.set(col_objects, true);
231✔
590
                progress_status.set(col_links, no_links);
210✔
591
                commit_and_continue_writing();
210✔
592
            }
141✔
593
        }
141✔
594
        for (auto k : table_accessors) {
141✔
595
            auto progress_status = progress_info->create_object_with_primary_key(k->get_name());
210✔
596
            if (!progress_status.get<bool>(col_links)) {
210✔
597
                k->migrate_links();
30✔
598
                progress_status.set(col_links, true);
30✔
599
                commit_and_continue_writing();
30✔
600
            }
99✔
601
        }
210✔
602

120✔
603
        // Final cleanup
72✔
604
        for (auto k : table_accessors) {
162✔
605
            k->finalize_migration(pk_cols[k]);
162✔
606
        }
231✔
607

51✔
608
        if (pk_table) {
141✔
609
            remove_table("pk");
117✔
610
        }
117✔
611
        remove_table(progress_info->get_key());
141✔
612
    }
282✔
613

333✔
614
    // Ensure we have search index on all primary key columns.
171✔
615
    auto table_keys = get_table_keys();
264✔
616
    if (current_file_format_version < 22) {
264✔
617
        for (auto k : table_keys) {
354✔
618
            auto t = get_table(k);
282✔
619
            if (auto col = t->get_primary_key_column()) {
231✔
620
                t->do_add_search_index(col, IndexType::General);
114✔
621
            }
153✔
622
        }
231✔
623
    }
69✔
624

102✔
625
    if (current_file_format_version == 22) {
102✔
626
        // Check that asymmetric table are empty
72✔
627
        for (auto k : table_keys) {
174✔
628
            auto t = get_table(k);
174✔
629
            if (t->is_asymmetric() && t->size() > 0) {
174✔
630
                t->clear();
24✔
631
            }
66✔
632
        }
153✔
633
    }
408✔
634
    if (current_file_format_version >= 21 && current_file_format_version < 23) {
477✔
635
        // Upgrade Set and Dictionary columns
408✔
636
        for (auto k : table_keys) {
465✔
637
            auto t = get_table(k);
465✔
638
            t->migrate_sets_and_dictionaries();
465✔
639
        }
93✔
640
    }
36✔
641
    // NOTE: Additional future upgrade steps go here.
105✔
642
}
477✔
643

63✔
644
void Transaction::promote_to_async()
39✔
645
{
3,396✔
646
    util::CheckedLockGuard lck(m_async_mutex);
3,396✔
647
    if (m_async_stage == AsyncState::Idle) {
4,458✔
648
        m_async_stage = AsyncState::HasLock;
24✔
649
    }
1,086✔
650
}
4,458✔
651

1,062✔
652
void Transaction::replicate(Transaction* dest, Replication& repl) const
1,062✔
653
{
1,101✔
654
    // We should only create entries for public tables
1,101✔
655
    std::vector<TableKey> public_table_keys;
1,101✔
656
    for (auto tk : get_table_keys()) {
1,173✔
657
        if (table_is_public(tk))
1,173✔
658
            public_table_keys.push_back(tk);
90✔
659
    }
111✔
660

1,101✔
661
    // Create tables
1,101✔
662
    for (auto tk : public_table_keys) {
1,149✔
663
        auto table = get_table(tk);
1,149✔
664
        auto table_name = table->get_name();
1,149✔
665
        if (!table->is_embedded()) {
1,152✔
666
            auto pk_col = table->get_primary_key_column();
72✔
667
            if (!pk_col)
72✔
668
                throw RuntimeError(
3✔
669
                    ErrorCodes::BrokenInvariant,
3✔
670
                    util::format("Class '%1' must have a primary key", Group::table_name_to_class_name(table_name)));
3✔
671
            auto pk_name = table->get_column_name(pk_col);
72✔
672
            if (pk_name != "_id")
72✔
673
                throw RuntimeError(ErrorCodes::BrokenInvariant,
3✔
674
                                   util::format("Primary key of class '%1' must be named '_id'. Current is '%2'",
3✔
675
                                                Group::table_name_to_class_name(table_name), pk_name));
1,062✔
676
            repl.add_class_with_primary_key(tk, table_name, DataType(pk_col.get_type()), pk_name,
69✔
677
                                            pk_col.is_nullable(), table->get_table_type());
69✔
678
        }
5,388✔
679
        else {
5,340✔
680
            repl.add_class(tk, table_name, Table::Type::Embedded);
5,340✔
681
        }
21✔
682
    }
117✔
683
    // Create columns
66✔
684
    for (auto tk : public_table_keys) {
117✔
685
        auto table = get_table(tk);
5,382✔
686
        auto pk_col = table->get_primary_key_column();
1,140✔
687
        auto cols = table->get_column_keys();
1,140✔
688
        for (auto col : cols) {
225✔
689
            if (col == pk_col)
1,275✔
690
                continue;
1,119✔
691
            repl.insert_column(table.unchecked_ptr(), col, DataType(col.get_type()), table->get_column_name(col),
1,206✔
692
                               table->get_opposite_table(col).unchecked_ptr());
1,206✔
693
        }
1,206✔
694
    }
432✔
695
    dest->commit_and_continue_writing();
381✔
696
    // Now the schema should be in place - create the objects
381✔
697
#ifdef REALM_DEBUG
747✔
698
    constexpr int number_of_objects_to_create_before_committing = 100;
747✔
699
#else
708✔
700
    constexpr int number_of_objects_to_create_before_committing = 1000;
1,050✔
701
#endif
1,050✔
702
    auto n = number_of_objects_to_create_before_committing;
5,358✔
703
    for (auto tk : public_table_keys) {
84✔
704
        auto table = get_table(tk);
84✔
705
        if (table->is_embedded())
1,146,633✔
706
            continue;
1,146,570✔
707
        // std::cout << "Table: " << table->get_name() << std::endl;
1,146,612✔
708
        auto pk_col = table->get_primary_key_column();
1,146,639✔
709
        auto cols = get_col_info(table.unchecked_ptr());
1,146,639✔
710
        for (auto o : *table) {
375✔
711
            auto obj_key = o.get_key();
387✔
712
            Mixed pk = o.get_any(pk_col);
375✔
713
            // std::cout << "    Object: " << pk << std::endl;
375✔
714
            repl.create_object_with_primary_key(table.unchecked_ptr(), obj_key, pk);
387✔
715
            generate_properties_for_obj(repl, o, cols);
387✔
716
            if (--n == 0) {
387✔
717
                dest->commit_and_continue_writing();
27✔
718
                n = number_of_objects_to_create_before_committing;
27✔
719
            }
27✔
720
        }
387✔
721
    }
75✔
722
}
39✔
723

15✔
724
void Transaction::complete_async_commit()
725
{
318✔
726
    // sync to disk:
333✔
727
    DB::ReadLockInfo read_lock;
327✔
728
    try {
327✔
729
        read_lock = db->grab_read_lock(DB::ReadLockInfo::Live, VersionID());
327✔
730
        if (db->m_logger) {
333✔
731
            db->m_logger->log(util::Logger::Level::trace, "Tr %1: Committing ref %2 to disk", m_log_id,
318✔
732
                              read_lock.m_top_ref);
318✔
733
        }
333✔
734
        GroupCommitter out(*this);
333✔
735
        out.commit(read_lock.m_top_ref); // Throws
318✔
736
        // we must release the write mutex before the callback, because the callback
330✔
737
        // is allowed to re-request it.
318✔
738
        db->release_read_lock(read_lock);
330✔
739
        if (m_oldest_version_not_persisted) {
330✔
740
            db->release_read_lock(*m_oldest_version_not_persisted);
327✔
741
            m_oldest_version_not_persisted.reset();
327✔
742
        }
315✔
743
    }
330✔
744
    catch (const std::exception& e) {
318✔
745
        m_commit_exception = std::current_exception();
15✔
746
        if (db->m_logger) {
15✔
747
            db->m_logger->log(util::Logger::Level::error, "Tr %1: Committing to disk failed with exception: \"%2\"",
27✔
748
                              m_log_id, e.what());
27✔
749
        }
27✔
750
        m_async_commit_has_failed = true;
15✔
751
        db->release_read_lock(read_lock);
1,146,633✔
752
    }
1,146,633✔
753
}
1,146,948✔
754

755
void Transaction::async_complete_writes(util::UniqueFunction<void()> when_synchronized)
756
{
195,681✔
757
    util::CheckedLockGuard lck(m_async_mutex);
195,681✔
758
    if (m_async_stage == AsyncState::HasLock) {
195,681✔
759
        // Nothing to commit to disk - just release write lock
190,830✔
760
        m_async_stage = AsyncState::Idle;
190,830✔
761
        db->async_end_write();
190,830✔
762
    }
190,830✔
763
    else if (m_async_stage == AsyncState::HasCommits) {
4,422✔
764
        m_async_stage = AsyncState::Syncing;
402✔
765
        m_commit_exception = std::exception_ptr();
402✔
766
        // get a callback on the helper thread, in which to sync to disk
498✔
767
        db->async_sync_to_disk([this, cb = std::move(when_synchronized)]() noexcept {
498✔
768
            complete_async_commit();
498✔
769
            util::CheckedLockGuard lck(m_async_mutex);
402✔
770
            m_async_stage = AsyncState::Idle;
306✔
771
            if (m_waiting_for_sync) {
306✔
772
                m_waiting_for_sync = false;
24✔
773
                m_async_cv.notify_all();
24✔
774
            }
24✔
775
            else {
612✔
776
                cb();
612✔
777
            }
942✔
778
        });
966✔
779
    }
966✔
780
}
4,779✔
781

330✔
782
void Transaction::prepare_for_close()
330✔
783
{
1,987,326✔
784
    util::CheckedLockGuard lck(m_async_mutex);
1,987,326✔
785
    switch (m_async_stage) {
1,796,094✔
786
        case AsyncState::Idle:
1,799,328✔
787
            break;
2,861,226✔
788

1,061,898✔
789
        case AsyncState::Requesting:
426,546✔
790
            // We don't have the ability to cancel a wait on the write lock, so
12✔
791
            // unfortunately we have to wait for it to be acquired.
1,061,910✔
792
            REALM_ASSERT(m_transact_stage == DB::transact_Reading);
1,061,910✔
793
            REALM_ASSERT(!m_oldest_version_not_persisted);
12✔
794
            m_waiting_for_write_lock = true;
12✔
795
            m_async_cv.wait(lck.native_handle(), [this]() REQUIRES(m_async_mutex) {
24✔
796
                return !m_waiting_for_write_lock;
24✔
797
            });
24✔
798
            db->end_write_on_correct_thread();
1,061,910✔
799
            break;
15✔
800

801
        case AsyncState::HasLock:
15✔
802
            // We have the lock and are currently in a write transaction, and
15✔
803
            // also may have some pending previous commits to write
18✔
804
            if (m_transact_stage == DB::transact_Writing) {
18✔
805
                db->reset_free_space_tracking();
1,061,907✔
806
                m_transact_stage = DB::transact_Reading;
9✔
807
            }
1,061,907✔
808
            if (m_oldest_version_not_persisted) {
1,061,913✔
809
                complete_async_commit();
810
            }
811
            db->end_write_on_correct_thread();
1,061,913✔
812
            break;
1,061,913✔
813

814
        case AsyncState::HasCommits:
12✔
815
            // We have commits which need to be synced to disk, so do that
12✔
816
            REALM_ASSERT(m_transact_stage == DB::transact_Reading);
12✔
817
            complete_async_commit();
12✔
818
            db->end_write_on_correct_thread();
84✔
819
            break;
84✔
820

72✔
821
        case AsyncState::Syncing:
84✔
822
            // The worker thread is currently writing, so wait for it to complete
84✔
823
            REALM_ASSERT(m_transact_stage == DB::transact_Reading);
84✔
824
            m_waiting_for_sync = true;
12✔
825
            m_async_cv.wait(lck.native_handle(), [this]() REQUIRES(m_async_mutex) {
96✔
826
                return !m_waiting_for_sync;
96✔
827
            });
96✔
828
            break;
84✔
829
    }
1,799,460✔
830
    m_async_stage = AsyncState::Idle;
1,799,388✔
831
}
1,799,460✔
832

72✔
833
void Transaction::acquire_write_lock()
834
{
189,912✔
835
    util::CheckedUniqueLock lck(m_async_mutex);
189,984✔
836
    switch (m_async_stage) {
189,984✔
837
        case AsyncState::Idle:
189,891✔
838
            lck.unlock();
189,891✔
839
            db->do_begin_possibly_async_write();
189,891✔
840
            return;
189,891✔
UNCOV
841

×
842
        case AsyncState::Requesting:
12!
843
            m_waiting_for_write_lock = true;
12✔
844
            m_async_cv.wait(lck.native_handle(), [this]() REQUIRES(m_async_mutex) {
24✔
845
                return !m_waiting_for_write_lock;
24✔
846
            });
24✔
847
            return;
12✔
UNCOV
848

×
849
        case AsyncState::HasLock:
850
        case AsyncState::HasCommits:
851
            return;
2,523,096✔
852

2,523,096✔
853
        case AsyncState::Syncing:
2,523,108✔
854
            m_waiting_for_sync = true;
12✔
855
            m_async_cv.wait(lck.native_handle(), [this]() REQUIRES(m_async_mutex) {
24✔
856
                return !m_waiting_for_sync;
24✔
857
            });
24✔
858
            lck.unlock();
12✔
859
            db->do_begin_possibly_async_write();
12✔
860
            break;
12✔
861
    }
192,570✔
862
}
192,570✔
863

864
void Transaction::do_end_read() noexcept
2,658✔
865
{
1,714,890✔
866
    if (db->m_logger)
1,717,548✔
867
        db->m_logger->log(util::Logger::Level::trace, "End transaction %1", m_log_id);
424,290✔
868

1,714,890✔
869
    prepare_for_close();
1,714,890✔
870
    detach();
1,714,890✔
871

1,714,890✔
872
    // We should always be ensuring that async commits finish before we get here,
1,714,890✔
873
    // but if the fsync() failed or we failed to update the top pointer then
1,714,890✔
874
    // there's not much we can do and we have to just accept that we're losing
1,714,890✔
875
    // those commits.
1,714,890✔
876
    if (m_oldest_version_not_persisted) {
1,714,890✔
877
        REALM_ASSERT(m_async_commit_has_failed);
3✔
878
        // We need to not release our read lock on m_oldest_version_not_persisted
629,934✔
879
        // as that's the version the top pointer is referencing and overwriting
629,934✔
880
        // that version will corrupt the Realm file.
2,613✔
881
        db->leak_read_lock(*m_oldest_version_not_persisted);
2,613✔
882
    }
627,324✔
883
    db->release_read_lock(m_read_lock);
2,328,591✔
884

2,328,591✔
885
    m_alloc.note_reader_end(this);
2,300,457✔
886
    set_transact_stage(DB::transact_Ready);
2,300,457✔
887
    // reset the std::shared_ptr to allow the DB object to release resources
2,300,457✔
888
    // as early as possible.
2,300,457✔
889
    db.reset();
2,328,591✔
890
}
1,714,890✔
891

627,321✔
892
// This is the same as do_end_read() above, but with the requirement that
20,775✔
893
// 1) This is called with the db->mutex locked already
20,775✔
894
// 2) No async commits outstanding
20,775✔
895
void Transaction::close_read_with_lock()
5,187✔
896
{
5,259✔
897
    REALM_ASSERT(m_transact_stage == DB::transact_Reading);
20,847✔
898
    {
20,847✔
899
        util::CheckedLockGuard lck(m_async_mutex);
639,828✔
900
        REALM_ASSERT_EX(m_async_stage == AsyncState::Idle, size_t(m_async_stage));
634,671✔
901
    }
634,671✔
902

627,219✔
903
    detach();
627,219✔
904
    REALM_ASSERT_EX(!m_oldest_version_not_persisted, m_oldest_version_not_persisted->m_type,
627,219✔
905
                    m_oldest_version_not_persisted->m_version, m_oldest_version_not_persisted->m_top_ref,
627,219✔
906
                    m_oldest_version_not_persisted->m_file_size);
15,690✔
907
    db->do_release_read_lock(m_read_lock);
15,690✔
908

619,053✔
909
    m_alloc.note_reader_end(this);
619,053✔
910
    set_transact_stage(DB::transact_Ready);
619,053✔
911
    // reset the std::shared_ptr to allow the DB object to release resources
10,386✔
912
    // as early as possible.
5,229✔
913
    db.reset();
5,229✔
914
}
611,775✔
915

627,321✔
916

917
void Transaction::initialize_replication()
918
{
51✔
919
    if (m_transact_stage == DB::transact_Writing) {
51✔
920
        if (Replication* repl = get_replication()) {
51✔
921
            auto current_version = m_read_lock.m_version;
48✔
922
            bool history_updated = false;
48✔
923
            repl->initiate_transact(*this, current_version, history_updated); // Throws
48✔
924
        }
48✔
925
    }
2,709✔
926
}
2,709✔
927

2,658✔
928
void Transaction::set_transact_stage(DB::TransactStage stage) noexcept
54✔
929
{
3,826,704✔
930
    m_transact_stage = stage;
3,829,308✔
931
}
3,826,704✔
932

933
class NodeTree {
54✔
934
public:
54✔
935
    NodeTree(size_t evac_limit, size_t work_limit)
2,658✔
936
        : m_evac_limit(evac_limit)
2,658✔
937
        , m_work_limit(int64_t(work_limit))
2,610✔
938
        , m_moved(0)
48✔
939
    {
2,733✔
940
    }
2,733✔
941
    ~NodeTree()
48✔
942
    {
2,733✔
943
        // std::cout << "Moved: " << m_moved << std::endl;
2,733✔
944
    }
2,733✔
945

946
    /// Function used to traverse the node tree and "copy on write" nodes
48✔
947
    /// that are found above the evac_limit. The function will return
48✔
948
    /// when either the whole tree has been travesed or when the work_limit
48✔
949
    /// has been reached.
950
    /// \param current_node - node to process.
951
    /// \param level - the level at which current_node is placed in the tree
952
    /// \param progress - When the traversal is initiated, this vector identifies at which
953
    ///                   node the process should be resumed. It is subesequently updated
954
    ///                   to point to the node we have just processed
955
    bool trv(Array& current_node, unsigned level, std::vector<size_t>& progress)
956
    {
631,020✔
957
        if (m_work_limit < 0) {
631,020✔
958
            return false;
2,607✔
959
        }
2,607✔
960
        if (current_node.is_read_only()) {
628,413✔
961
            size_t byte_size = current_node.get_byte_size();
614,682✔
962
            if ((current_node.get_ref() + byte_size) > m_evac_limit) {
614,682✔
963
                current_node.copy_on_write();
585,741✔
964
                m_moved++;
585,741✔
965
                m_work_limit -= byte_size;
585,741✔
966
            }
585,741✔
967
        }
614,682✔
968

628,413✔
969
        if (current_node.has_refs()) {
628,413✔
970
            auto sz = current_node.size();
20,943✔
971
            m_work_limit -= sz;
20,943✔
972
            if (progress.size() == level) {
20,943✔
973
                progress.push_back(0);
5,367✔
974
            }
5,367✔
975
            REALM_ASSERT_EX(level < progress.size(), level, progress.size());
20,943✔
976
            size_t ndx = progress[level];
20,943✔
977
            while (ndx < sz) {
641,304✔
978
                auto val = current_node.get(ndx);
635,967✔
979
                if (val && !(val & 1)) {
635,967✔
980
                    Array arr(current_node.get_alloc());
628,317✔
981
                    arr.set_parent(&current_node, ndx);
628,317✔
982
                    arr.init_from_parent();
628,317✔
983
                    if (!trv(arr, level + 1, progress)) {
628,317✔
984
                        return false;
15,606✔
985
                    }
15,606✔
986
                }
620,361✔
987
                ndx = ++progress[level];
620,361✔
988
            }
620,361✔
989
            while (progress.size() > level)
20,943✔
990
                progress.pop_back();
5,337✔
991
        }
5,337✔
992
        return true;
628,413✔
993
    }
628,413✔
994

995
private:
996
    size_t m_evac_limit;
997
    int64_t m_work_limit;
998
    size_t m_moved;
999
};
1000

1001

1002
void Transaction::cow_outliers(std::vector<size_t>& progress, size_t evac_limit, size_t work_limit)
1003
{
2,685✔
1004
    NodeTree node_tree(evac_limit, work_limit);
2,685✔
1005
    if (progress.empty()) {
2,685✔
1006
        progress.push_back(s_table_name_ndx);
84✔
1007
    }
84✔
1008
    if (progress[0] == s_table_name_ndx) {
2,685✔
1009
        if (!node_tree.trv(m_table_names, 1, progress))
84✔
1010
            return;
1011
        progress.back() = s_table_refs_ndx; // Handle tables next
84✔
1012
    }
84✔
1013
    if (progress[0] == s_table_refs_ndx) {
2,685✔
1014
        if (!node_tree.trv(m_tables, 1, progress))
2,685✔
1015
            return;
2,607✔
1016
        progress.back() = s_hist_ref_ndx; // Handle history next
78✔
1017
    }
78✔
1018
    if (progress[0] == s_hist_ref_ndx && m_top.get(s_hist_ref_ndx)) {
2,685✔
1019
        Array hist_arr(m_top.get_alloc());
78✔
1020
        hist_arr.set_parent(&m_top, s_hist_ref_ndx);
78✔
1021
        hist_arr.init_from_parent();
78✔
1022
        if (!node_tree.trv(hist_arr, 1, progress))
78✔
1023
            return;
1024
    }
78✔
1025
    progress.clear();
78✔
1026
}
78✔
1027

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

© 2026 Coveralls, Inc