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

realm / realm-core / github_pull_request_319175

19 May 2025 08:16PM UTC coverage: 91.119% (-0.02%) from 91.143%
github_pull_request_319175

Pull #8082

Evergreen

web-flow
Bump setuptools from 70.0.0 to 78.1.1 in /evergreen/hang_analyzer

Bumps [setuptools](https://github.com/pypa/setuptools) from 70.0.0 to 78.1.1.
- [Release notes](https://github.com/pypa/setuptools/releases)
- [Changelog](https://github.com/pypa/setuptools/blob/main/NEWS.rst)
- [Commits](https://github.com/pypa/setuptools/compare/v70.0.0...v78.1.1)

---
updated-dependencies:
- dependency-name: setuptools
  dependency-version: 78.1.1
  dependency-type: direct:production
...

Signed-off-by: dependabot[bot] <support@github.com>
Pull Request #8082: Bump setuptools from 70.0.0 to 78.1.1 in /evergreen/hang_analyzer

102788 of 181548 branches covered (56.62%)

217441 of 238634 relevant lines covered (91.12%)

5497200.53 hits per line

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

62.92
/src/realm/sync/noinst/server/server_history.cpp
1
#include <algorithm>
2
#include <cstring>
3
#include <stack>
4

5
#include <realm/sync/changeset_encoder.hpp>
6
#include <realm/sync/changeset_parser.hpp>
7
#include <realm/sync/impl/clamped_hex_dump.hpp>
8
#include <realm/sync/instruction_applier.hpp>
9
#include <realm/sync/noinst/server/server_history.hpp>
10
#include <realm/table_view.hpp>
11
#include <realm/util/hex_dump.hpp>
12
#include <realm/util/input_stream.hpp>
13
#include <realm/util/value_reset_guard.hpp>
14
#include <realm/version.hpp>
15

16
using namespace realm;
17
using namespace realm::sync;
18
using namespace realm::util;
19
using namespace _impl;
20

21

22
/*
23

24
Client file            Entry  Client  Identifier  Proxy  Reciprocal   Last seen      Locked server
25
entry type             index  type    Salt        file   history (1)  timestamp (2)  version
26
---------------------------------------------------------------------------------------------------
27
Special (3)            0      0       no          no     YES          no             no
28
Root (4)               1      0       no          no     no           no             no
29
Upstream (5)           2+     0       no          no     no           no             no
30
Self (6)               2+     6       no          no     no           no             no
31
Indirect client        2+     1       no          YES    no           no             no
32
Legacy (7)             2+     5       YES         no     YES          YES            YES
33
Direct regular client  2+     2       YES         no     YES          YES            YES
34
Direct subserver       2+     4       YES         no     YES          YES            YES
35
Direct partial view    2+     3       YES         no     YES          YES            YES
36

37
1) Entries that may have a reciprocal history may also have nonzero
38
   `client_version` and nonzero `rh_base_version`. The reciprocal history is
39
   absent in expired entries.
40

41
2) An entry that represents a direct client has expired if and only if this
42
   timestamp is zero. Here, subservers, partial views, and legacy entries are
43
   considered to be direct clients.
44

45
3) This is a special entry used to represent the upstream server on a subtier
46
   node of a star topology server cluster. Note that there is no valid client
47
   file identifier associated with this entry, because a valid client file
48
   identifier is strictly greater than zero.
49

50
4) This entry represents the root of a star topology server cluster. This entry
51
   is always present. For files that do not function as partial views (or have
52
   not yet been initialized as such) and do not reside on a subtier node of a
53
   star topology server cluster (or have not yet been initialized as such), this
54
   entry represents the file itself.
55

56
5) For a nonpartial file on a subtier server, this is an entry for the upstream
57
   server or for a file reachable via the upstream server. For a partial file,
58
   this is an entry for the reference file, or a file reachable via the
59
   reference file.
60

61
6) This is the entry that represents the file itself. It is present if if the
62
   file functions as a partial view (and has been initialized as such), or if it
63
   resides on a subtier node of a star topology server cluster (and has been
64
   initialized as such).
65

66
7) This is an entry created prior to history schema version 10. The exact type
67
   of this entry is unknown. However, since the star topology feature has not
68
   been publicly released yet, it is likely to represent either an immediate
69
   regular client, an immediate partial view, or a client of an immediate
70
   partial view.
71

72

73
Abstract history schema
74
-----------------------
75

76
  table client_files:
77
    int ident_salt
78
    int client_version
79
    int rh_base_version (0 if recip_hist is null) (doubles as last server version integrated by client in
80
`client_version`)
81
    // recip_hist_index = produced_server_version - recip_hist_base_version - 1
82
    table recip_hist: (nullable)
83
      string changeset (nullable)
84
    link proxy_file (references client_files)
85
    int client_type
86
    int last_seen (UNIX timestamp)
87
    int locked_server_version
88

89
  // Server version on which the first entry in `sync_history` is based. This
90
  // will be zero unless the history has been trimmed (note, history trimming is
91
  // not yet implemented, so for now, it is always zero).
92
  //
93
  // `curr_server_version = history_base_version + sync_history.size()`
94
  int history_base_version
95

96
  // Server version until which the history has been compacted. This can never
97
  // decrease.
98
  int compacted_until_version
99

100
  // Salt attached to `history_base_version`. This is required to be zero if
101
  // `history_base_version` is zero.
102
  int base_version_salt
103

104
  // History of server versions (one entry per server version).
105
  //
106
  // For entries whose changesets are of local origin, `origin_file` is
107
  // zero. For other entries, `origin_file` points to the entry in
108
  // `client_files` representing the file in which the initial untransformed
109
  // changeset originated.
110
  //
111
  // For entries whose changesets are of local origin, `client_version` is
112
  // always zero. For other entries, the a changeset was produced by integration
113
  // of a changeset received from a remote peer (client or upstream server), and
114
  // `client_version` is the version produced on that remote peer by the
115
  // received changeset.
116
  //
117
  // `history_entry_index = produced_server_version - history_base_version - 1`
118
  table sync_history:
119
    int version_salt (random) (formerly known as server_session_ident)
120
    link origin_file (references client_files)
121
    int client_version
122
    int timestamp
123
    string changeset
124

125
  // Continuous transactions history (one entry per Realm version (snapshot number), trimmed)
126
  //
127
  // `ct_history_entry_index = realm_version - ct_history_base_version - 1`
128
  table ct_history:
129
    link history_entry (nullable) (references sync_history) (null for empty changesets of local origin) // FIXME: This
130
looks wrong!
131

132
  int history_byte_size
133

134
  // This object is only present after a successful invocation of
135
  // ServerHistory::add_upstream_sync_status()
136
  optional object upstream_status:
137
    SaltedFileIdent client_file_ident
138
    SaltedVersion   progress_latest_server_version
139
    DownloadCursor  progress_download
140
    UploadCursor    progress_upload
141

142
  // This array is only present after a successful invocation of
143
  // ServerHistory::initiate_as_partial_view()
144
  optional array partial_sync:
145
    // A file identifier allocated in the context of both the reference, and the
146
    // partial Realm, and with the purpose of identifying changesets in the
147
    // history of the reference Realm, that originate from, or via the partial
148
    // Realm.
149
    //
150
    // At the same time, it is used to identify outgoing changesets in the
151
    // history of the partial Realm that originate from, or via the reference
152
    // Realm.
153
    int partial_file_ident
154
    int partial_file_ident_salt
155
    int progress_server_version
156
    int progress_reference_version
157
    int progress_reference_version_salt
158

159

160

161
History representation
162
----------------------
163

164
  mixed_array_ref history:
165
    0 -> mixed_array_ref client_files:
166
      0 -> int_bptree_ref client_file_ident_salts:
167
        file_ident -> ident_salt
168
      1 -> int_bptree_ref client_file_client_versions:
169
        file_ident -> client_version
170
      2 -> int_bptree_ref client_file_rh_base_versions:
171
        file_ident -> rh_base_version
172
      3 -> mixed_bptree_ref cf_recip_hist_refs:
173
        file_ident -> nullable_binary_bptree_ref recip_hist:
174
          recip_hist_index -> changeset
175
      4 -> int_bptree_ref cf_proxy_files:
176
        file_ident -> proxy_file_ident
177
      5 -> int_bptree_ref cf_client_types:
178
        file_ident -> client_type
179
      6 -> int_bptree_ref cf_last_seen:
180
        file_indet -> unix_timestamp
181
      7 -> int_bptree_ref cf_locked_server_versions:
182
        file_ident -> locked_server_version
183
    1 -> tagged_int history_base_version
184
    2 -> tagged_int base_version_salt
185
    3 -> mixed_array_ref sync_history:
186
      0 -> int_bptree_ref sh_version_salts:
187
        history_entry_index -> server_version_salt
188
      1 -> int_bptree_ref sh_origin_files:
189
        history_entry_index -> file_ident
190
      2 -> int_bptree_ref sh_client_versions:
191
        history_entry_index -> sync_history.client_version
192
      3 -> int_bptree_ref sync_history_timestamps:
193
        history_entry_index -> sync_history.timestamp
194
      4 -> binary_bptree_ref sh_changesets:
195
        history_entry_index -> sync_history.changeset
196
    4 -> binary_bptree_ref ct_history
197
      Core history_entry_index -> (changeset in Core's format)
198
    5 -> tagged_int history_byte_size
199
    6 -> ref to ObjectIDHistoryState
200
    7 -> int_array_ref upstream_status:
201
      0 -> int client_file_ident
202
      1 -> int client_file_ident_salt
203
      2 -> int progress_latest_server_version
204
      3 -> int progress_latest_server_version_salt
205
      4 -> int progress_download_server_version
206
      5 -> int progress_download_client_version
207
      6 -> int progress_upload_client_version
208
      7 -> int progress_upload_server_version
209
    8 -> int_array_ref partial_sync:
210
      0 -> int partial_file_ident
211
      1 -> int partial_file_ident_salt
212
      2 -> int progress_server_version
213
      3 -> int progress_reference_version
214
      4 -> int progress_reference_version_salt
215
    9 -> tagged_int compacted_until_version
216
   10 -> tagged_int last_compaction_at
217
*/
218

219

220
namespace {
221

222
// This is the hard-coded file identifier that represents changes of local
223
// origin in a file on the root node of a star topology server cluster, or a
224
// file on a server that is not part of a cluster.
225
constexpr ServerHistory::file_ident_type g_root_node_file_ident = 1;
226

227

228
} // unnamed namespace
229

230

231
void ServerHistory::get_status(VersionInfo& version_info, bool& has_upstream_sync_status,
232
                               file_ident_type& partial_file_ident,
233
                               version_type& partial_progress_reference_version) const
234
{
5,942✔
235
    TransactionRef rt = m_db->start_read(); // Throws
5,942✔
236
    version_type realm_version = rt->get_version();
5,942✔
237
    const_cast<ServerHistory*>(this)->set_group(rt.get());
5,942✔
238
    ensure_updated(realm_version); // Throws
5,942✔
239
    version_info.realm_version = realm_version;
5,942✔
240
    version_info.sync_version = get_salted_server_version();
5,942✔
241
    has_upstream_sync_status = (m_acc && m_acc->upstream_status.is_attached());
5,942✔
242
    bool is_initiated_as_partial_view = (m_acc && m_acc->partial_sync.is_attached());
5,942✔
243
    if (is_initiated_as_partial_view) {
5,942✔
244
        partial_file_ident = file_ident_type(m_acc->partial_sync.get(s_ps_partial_file_ident_iip));
×
245
        REALM_ASSERT(partial_file_ident != 0);
×
246
        partial_progress_reference_version =
×
247
            version_type(m_acc->partial_sync.get(s_ps_progress_reference_version_iip));
×
248
    }
×
249
    else {
5,942✔
250
        partial_file_ident = 0;
5,942✔
251
        partial_progress_reference_version = 0;
5,942✔
252
    }
5,942✔
253
}
5,942✔
254

255

256
void ServerHistory::allocate_file_identifiers(FileIdentAllocSlots& slots, VersionInfo& version_info)
257
{
1,314✔
258
    TransactionRef tr = m_db->start_write(); // Throws
1,314✔
259
    version_type realm_version = tr->get_version();
1,314✔
260
    ensure_updated(realm_version); // Throws
1,314✔
261
    prepare_for_write();           // Throws
1,314✔
262

263
    if (REALM_UNLIKELY(m_acc->upstream_status.is_attached())) {
1,314✔
264
        throw util::runtime_error("Cannot allocate new client file identifiers in a file "
×
265
                                  "that is associated with an upstream server");
×
266
    }
×
267

268
    for (FileIdentAllocSlot& slot : slots)
1,314✔
269
        slot.file_ident = allocate_file_ident(slot.proxy_file, slot.client_type); // Throws
1,374✔
270

271
    version_type new_realm_version = tr->commit(); // Throws
1,314✔
272
    version_info.realm_version = new_realm_version;
1,314✔
273
    version_info.sync_version = get_salted_server_version();
1,314✔
274
}
1,314✔
275

276

277
bool ServerHistory::register_received_file_identifier(file_ident_type received_file_ident,
278
                                                      file_ident_type proxy_file_ident, ClientType client_type,
279
                                                      salt_type& file_ident_salt, VersionInfo& version_info)
280
{
×
281
    TransactionRef tr = m_db->start_write(); // Throws
×
282
    version_type realm_version = tr->get_version();
×
283
    ensure_updated(realm_version); // Throws
×
284
    prepare_for_write();           // Throws
×
285

286
    salt_type salt = 0;
×
287
    bool success = try_register_file_ident(received_file_ident, proxy_file_ident, client_type,
×
288
                                           salt); // Throws
×
289
    if (REALM_UNLIKELY(!success))
×
290
        return false;
×
291

292
    version_type new_realm_version = tr->commit(); // Throws
×
293
    file_ident_salt = salt;
×
294
    version_info.realm_version = new_realm_version;
×
295
    version_info.sync_version = get_salted_server_version();
×
296
    return true;
×
297
}
×
298

299

300
bool ServerHistory::integrate_client_changesets(const IntegratableChangesets& integratable_changesets,
301
                                                VersionInfo& version_info, bool& backup_whole_realm,
302
                                                IntegrationResult& result, util::Logger& logger)
303
{
35,826✔
304
    REALM_ASSERT(!integratable_changesets.empty());
35,826✔
305

306
    // Determine the order in which to process client files. Client files with
307
    // serialized transactions must be processed first. At most one of the
308
    // available serialized transactions can succeed.
309
    //
310
    // Subordinately, the order in which to process client files is randomized
311
    // to prevent integer ordering between client file identifiers from giving
312
    // unfair priority to some client files.
313
    std::vector<file_ident_type>& client_file_order = m_client_file_order_buffer;
35,826✔
314
    client_file_order.clear();
35,826✔
315
    bool has_changesets = false;
35,826✔
316
    for (const auto& pair : integratable_changesets) {
38,500✔
317
        file_ident_type client_file_ident = pair.first;
38,500✔
318
        client_file_order.push_back(client_file_ident);
38,500✔
319
        const IntegratableChangesetList& list = pair.second;
38,500✔
320
        if (list.has_changesets())
38,500✔
321
            has_changesets = true;
20,454✔
322
    }
38,500✔
323

324
    result = {};
35,826✔
325
    for (;;) {
35,848✔
326
        if (has_changesets) {
35,848✔
327
            result.partial_clear();
18,486✔
328
        }
18,486✔
329

330
        bool anything_to_do = (integratable_changesets.size() > result.excluded_client_files.size());
35,848✔
331
        if (REALM_UNLIKELY(!anything_to_do))
35,848✔
332
            return false;
26✔
333

334
        file_ident_type current_client_file_ident = 0;
35,822✔
335
        ExtendedIntegrationError current_error_potential = {};
35,822✔
336
        std::size_t num_changesets_to_dump = 0;
35,822✔
337
        bool dump_changeset_info = false;
35,822✔
338

339
        try {
35,822✔
340
            TransactionRef tr = m_db->start_write(); // Throws
35,822✔
341
            version_type realm_version = tr->get_version_of_current_transaction().version;
35,822✔
342
            ensure_updated(realm_version); // Throws
35,822✔
343
            prepare_for_write();           // Throws
35,822✔
344

345
            bool dirty = false;
35,822✔
346
            bool backup_whole_realm_2 = false;
35,822✔
347
            for (file_ident_type client_file_ident : client_file_order) {
38,500✔
348
                REALM_ASSERT(client_file_ident > 0);
38,500✔
349
                REALM_ASSERT(client_file_ident != g_root_node_file_ident);
38,500✔
350
                REALM_ASSERT(client_file_ident != m_local_file_ident);
38,500✔
351
                bool excluded =
38,500✔
352
                    (result.excluded_client_files.find(client_file_ident) != result.excluded_client_files.end());
38,500✔
353
                if (REALM_UNLIKELY(excluded))
38,500✔
354
                    continue;
×
355
                current_client_file_ident = client_file_ident;
38,500✔
356
                // Verify that the client file entry has not expired
357
                current_error_potential = ExtendedIntegrationError::client_file_expired;
38,500✔
358
                std::size_t client_file_index = std::size_t(client_file_ident);
38,500✔
359
                std::int_fast64_t last_seen_timestamp = m_acc->cf_last_seen_timestamps.get(client_file_index);
38,500✔
360
                bool expired = (last_seen_timestamp == 0);
38,500✔
361
                if (REALM_UNLIKELY(expired))
38,500✔
362
                    goto error;
×
363
                const IntegratableChangesetList& list = integratable_changesets.find(client_file_ident)->second;
38,500✔
364
                std::vector<RemoteChangeset> changesets;
38,500✔
365
                current_error_potential = ExtendedIntegrationError::bad_origin_file_ident;
38,500✔
366
                for (const IntegratableChangeset& ic : list.changesets) {
38,500✔
367
                    REALM_ASSERT(ic.client_file_ident == client_file_ident);
34,490✔
368
                    // Verify that the origin file identifier either is the
369
                    // client's file identifier, or a file identifier of a
370
                    // subordinate client for which the sending client acts as a
371
                    // proxy.
372
                    file_ident_type origin_file_ident = ic.origin_file_ident;
34,490✔
373
                    if (origin_file_ident != 0) {
34,490✔
374
                        static_assert(g_root_node_file_ident == 1, "");
×
375
                        if (REALM_UNLIKELY(origin_file_ident <= g_root_node_file_ident))
×
376
                            goto error;
×
377
                        if (REALM_UNLIKELY(std::uint_fast64_t(origin_file_ident) >= m_num_client_files))
×
378
                            goto error;
×
379
                        std::size_t index = std::size_t(origin_file_ident);
×
380
                        file_ident_type proxy_file_ident = file_ident_type(m_acc->cf_proxy_files.get(index));
×
381
                        if (REALM_UNLIKELY(proxy_file_ident != ic.client_file_ident))
×
382
                            goto error;
×
383
                    }
×
384
                    RemoteChangeset changeset{ic};
34,490✔
385
                    changesets.push_back(changeset);             // Throws
34,490✔
386
                    result.integrated_changesets.push_back(&ic); // Throws
34,490✔
387
                }
34,490✔
388

389
                auto integrate = [&](std::size_t begin, std::size_t end, UploadCursor upload_progress) {
38,500✔
390
                    const RemoteChangeset* changesets_2 = changesets.data() + begin;
38,500✔
391
                    std::size_t num_changesets = end - begin;
38,500✔
392
                    num_changesets_to_dump += num_changesets;
38,500✔
393
                    bool dirty_2 = integrate_remote_changesets(
38,500✔
394
                        client_file_ident, upload_progress, list.locked_server_version, changesets_2, num_changesets,
38,500✔
395
                        logger); // Throws
38,500✔
396
                    if (dirty_2) {
38,500✔
397
                        dirty = true;
38,474✔
398
                        bool backup_whole_realm_3 =
38,474✔
399
                            (begin == end || upload_progress.client_version != changesets[end - 1].remote_version ||
38,474✔
400
                             list.locked_server_version != upload_progress.last_integrated_server_version);
38,474✔
401
                        if (backup_whole_realm_3)
38,474✔
402
                            backup_whole_realm_2 = true;
27,620✔
403
                    }
38,474✔
404
                };
38,500✔
405

406
                // Note: This value will be read if an exception is thrown
407
                // below. Clang's static analyzer incorrectly reports it as
408
                // a dead store.
409
                current_error_potential = ExtendedIntegrationError::bad_changeset;
38,500✔
410
                static_cast<void>(current_error_potential);
38,500✔
411

412
                std::size_t num_changesets = changesets.size();
38,500✔
413
                std::size_t aug_num_changesets = num_changesets;
38,500✔
414
                logger.debug("Integrating %1 changesets from client file %2", aug_num_changesets, client_file_ident);
38,500✔
415

416
                integrate(0, num_changesets, list.upload_progress); // Throws
38,500✔
417
            }
38,500✔
418

419
            if (dirty) {
35,822✔
420
                auto ta = util::make_temp_assign(m_is_local_changeset, false, true);
35,800✔
421
                version_info.realm_version = tr->commit(); // Throws
35,800✔
422
                version_info.sync_version = get_salted_server_version();
35,800✔
423
                if (backup_whole_realm_2)
35,800✔
424
                    backup_whole_realm = true;
26,734✔
425
                return true;
35,800✔
426
            }
35,800✔
427
            return false;
22✔
428
        }
35,822✔
429
        catch (BadChangesetError& e) {
35,822✔
430
            logger.error("Failed to parse, or apply changeset received from client: %1",
4✔
431
                         e.what()); // Throws
4✔
432
            dump_changeset_info = true;
4✔
433
        }
4✔
434
        catch (TransformError& e) {
35,822✔
435
            logger.error("Failed to transform changeset received from client: %1",
22✔
436
                         e.what()); // Throws
22✔
437
            dump_changeset_info = true;
22✔
438
        }
22✔
439

440
        if (dump_changeset_info) {
26✔
441
            std::size_t changeset_ndx = 0;
26✔
442
            std::size_t num_parts = num_changesets_to_dump;
26✔
443
            for (std::size_t i = 0; i < num_parts; ++i) {
52✔
444
                // Regular changeset
445
                const IntegratableChangeset& ic = *result.integrated_changesets[changeset_ndx];
26✔
446
                std::string hex_dump = util::hex_dump(ic.changeset.data(),
26✔
447
                                                      ic.changeset.size()); // Throws
26✔
448
                logger.error("Failed transaction (part %1/%2): Changeset "
26✔
449
                             "(client_file_ident=%3, origin_timestamp=%4, "
26✔
450
                             "origin_file_ident=%5, client_version=%6, "
26✔
451
                             "last_integrated_server_version=%7): %8",
26✔
452
                             (i + 1), num_parts, ic.client_file_ident, ic.origin_timestamp, ic.origin_file_ident,
26✔
453
                             ic.upload_cursor.client_version, ic.upload_cursor.last_integrated_server_version,
26✔
454
                             hex_dump); // Throws
26✔
455
                ++changeset_ndx;
26✔
456
                continue;
26✔
457
            }
26✔
458
        }
26✔
459

460
    error:
26✔
461
        REALM_ASSERT(current_client_file_ident != 0);
26✔
462
        result.excluded_client_files[current_client_file_ident] = current_error_potential; // Throws
26✔
463
    }
26✔
464
}
35,826✔
465

466

467
auto ServerHistory::integrate_backup_idents_and_changeset(
468
    version_type expected_realm_version, salt_type server_version_salt,
469
    const FileIdentAllocSlots& file_ident_alloc_slots,
470
    const std::vector<IntegratableChangeset>& integratable_changesets, util::Logger& logger) -> IntegratedBackup
471
{
×
472
    IntegratedBackup result;
×
473
    result.success = false;
×
474

475
    try {
×
476
        TransactionRef tr = m_db->start_write(); // Throws
×
477
        version_type realm_version = tr->get_version_of_current_transaction().version;
×
478
        ensure_updated(realm_version); // Throws
×
479
        prepare_for_write();           // Throws
×
480

481
        result.version_info.realm_version = realm_version;
×
482

483
        if (realm_version + 1 != expected_realm_version)
×
484
            return result;
×
485

486
        // To ensure identity of a server Realm and its backup, it is necessary
487
        // to set the server_version_salt of the backup Realm to the same value
488
        // as that of the original Realm.
489
        m_salt_for_new_server_versions = server_version_salt;
×
490

491
        for (const auto& slot : file_ident_alloc_slots) {
×
492
            if (std::uint_fast64_t(slot.file_ident.ident) != m_num_client_files)
×
493
                return result;
×
494
            add_client_file(slot.file_ident.salt, slot.proxy_file, slot.client_type); // Throws
×
495
        }
×
496

497
        std::map<file_ident_type, std::vector<RemoteChangeset>> changesets;
×
498

499
        for (const IntegratableChangeset& ic : integratable_changesets)
×
500
            changesets[ic.client_file_ident].push_back(ic);
×
501

502
        for (auto& pair : changesets) {
×
503
            file_ident_type client_file_ident = pair.first;
×
504
            // FIXME: Backup should also get the proper upload progress and
505
            // locked server version. This requires extending the backup
506
            // protocol.
507
            const auto& back = pair.second.back();
×
508
            UploadCursor upload_progress = {back.remote_version, back.last_integrated_local_version};
×
509
            version_type locked_server_version = upload_progress.last_integrated_server_version;
×
510
            integrate_remote_changesets(client_file_ident, upload_progress, locked_server_version, pair.second.data(),
×
511
                                        pair.second.size(), logger); // Throws
×
512
        }
×
513

514
        auto ta = util::make_temp_assign(m_is_local_changeset, false, true);
×
515
        result.version_info.realm_version = tr->commit(); // Throws
×
516
        result.version_info.sync_version = get_salted_server_version();
×
517
        result.success = true;
×
518
    }
×
519
    catch (BadChangesetError& e) {
×
520
        logger.error("Bad incremental backup", e.what()); // Throws
×
521
    }
×
522
    catch (TransformError& e) {
×
523
        logger.error("Bad incremental backup", e.what()); // Throws
×
524
    }
×
525

526
    return result;
×
527
}
×
528

529

530
auto ServerHistory::allocate_file_ident(file_ident_type proxy_file_ident, ClientType client_type) -> SaltedFileIdent
531
{
1,374✔
532
    REALM_ASSERT(!m_acc->upstream_status.is_attached());
1,374✔
533

534
    std::size_t file_index = m_num_client_files;
1,374✔
535
    salt_type salt = register_client_file_by_index(file_index, proxy_file_ident, client_type); // Throws
1,374✔
536

537
    if (uint64_t(file_index) > get_max_file_ident())
1,374✔
538
        throw util::overflow_error{"File identifier"};
×
539

540
    file_ident_type ident = file_ident_type(file_index);
1,374✔
541
    return {ident, salt};
1,374✔
542
}
1,374✔
543

544

545
void ServerHistory::register_assigned_file_ident(file_ident_type file_ident)
546
{
×
547
    file_ident_type proxy_file_ident = 0; // No proxy
×
548
    ClientType client_type = ClientType::self;
×
549
    salt_type file_ident_salt; // Dummy
×
550
    bool success = try_register_file_ident(file_ident, proxy_file_ident, client_type,
×
551
                                           file_ident_salt); // Throws
×
552
    REALM_ASSERT(success);
×
553
}
×
554

555

556
bool ServerHistory::try_register_file_ident(file_ident_type file_ident, file_ident_type proxy_file_ident,
557
                                            ClientType client_type, salt_type& file_ident_salt)
558
{
×
559
    REALM_ASSERT(m_acc->upstream_status.is_attached());
×
560
    static_assert(g_root_node_file_ident == 1, "");
×
561
    if (REALM_UNLIKELY(file_ident < 2))
×
562
        return false;
×
563
    std::size_t max = std::numeric_limits<std::size_t>::max();
×
564
    if (REALM_UNLIKELY(std::uint_fast64_t(file_ident) > max))
×
565
        throw util::overflow_error{"Client file index"};
×
566
    std::size_t file_index = std::size_t(file_ident);
×
567
    if (file_index < m_num_client_files)
×
568
        return false;
×
569
    file_ident_salt = register_client_file_by_index(file_index, proxy_file_ident, client_type); // Throws
×
570
    return true;
×
571
}
×
572

573

574
auto ServerHistory::register_client_file_by_index(std::size_t file_index, file_ident_type proxy_file_ident,
575
                                                  ClientType client_type) -> salt_type
576
{
1,374✔
577
    REALM_ASSERT(file_index >= m_num_client_files);
1,374✔
578
    REALM_ASSERT(proxy_file_ident == 0 || is_valid_proxy_file_ident(proxy_file_ident));
1,374✔
579
    bool generate_salt = is_direct_client(client_type);
1,374✔
580
    salt_type salt = 0;
1,374✔
581
    if (generate_salt) {
1,374✔
582
        auto max_salt = 0x0'7FFF'FFFF'FFFF'FFFF;
1,374✔
583
        std::mt19937_64& random = m_context.server_history_get_random();
1,374✔
584
        salt = std::uniform_int_distribution<salt_type>(1, max_salt)(random);
1,374✔
585
    }
1,374✔
586
    while (file_index > m_num_client_files)
1,374✔
587
        add_client_file(0, 0, ClientType::upstream);      // Throws
×
588
    add_client_file(salt, proxy_file_ident, client_type); // Throws
1,374✔
589
    return salt;
1,374✔
590
}
1,374✔
591

592

593
bool ServerHistory::ensure_upstream_file_ident(file_ident_type file_ident)
594
{
×
595
    REALM_ASSERT(m_acc->upstream_status.is_attached());
×
596

597
    static_assert(g_root_node_file_ident == 1, "");
×
598
    if (REALM_UNLIKELY(file_ident < 2))
×
599
        return (file_ident == 1);
×
600
    std::size_t max = std::numeric_limits<std::size_t>::max();
×
601
    if (REALM_UNLIKELY(std::uint_fast64_t(file_ident) > max))
×
602
        throw util::overflow_error{"Client file index"};
×
603
    std::size_t file_index = std::size_t(file_ident);
×
604
    if (REALM_LIKELY(file_index < m_num_client_files)) {
×
605
        auto client_type = m_acc->cf_client_types.get(file_index);
×
606
        if (REALM_UNLIKELY(client_type != int(ClientType::upstream)))
×
607
            return false;
×
608
        REALM_ASSERT(m_acc->cf_ident_salts.get(file_index) == 0);
×
609
        REALM_ASSERT(m_acc->cf_proxy_files.get(file_index) == 0);
×
610
        return true;
×
611
    }
×
612
    do
×
613
        add_client_file(0, 0, ClientType::upstream); // Throws
×
614
    while (file_index >= m_num_client_files);
×
615
    return true;
×
616
}
×
617

618

619
void ServerHistory::add_client_file(salt_type file_ident_salt, file_ident_type proxy_file_ident,
620
                                    ClientType client_type)
621
{
1,374✔
622
    switch (client_type) {
1,374✔
623
        case ClientType::upstream:
✔
624
        case ClientType::self:
✔
625
            REALM_ASSERT(file_ident_salt == 0);
×
626
            REALM_ASSERT(proxy_file_ident == 0);
×
627
            break;
×
628
        case ClientType::indirect:
✔
629
            REALM_ASSERT(file_ident_salt == 0);
×
630
            REALM_ASSERT(proxy_file_ident != 0);
×
631
            break;
×
632
        case ClientType::regular:
1,374✔
633
        case ClientType::subserver:
1,374✔
634
            REALM_ASSERT(file_ident_salt != 0);
1,374✔
635
            REALM_ASSERT(proxy_file_ident == 0);
1,374✔
636
            break;
1,374✔
637
        case ClientType::legacy:
✔
638
            REALM_ASSERT(false);
×
639
            break;
×
640
    }
1,374✔
641
    std::int_fast64_t client_version = 0;
1,374✔
642
    std::int_fast64_t recip_hist_base_version = 0;
1,374✔
643
    ref_type recip_hist_ref = 0;
1,374✔
644
    std::int_fast64_t last_seen_timestamp = 0;
1,374✔
645
    std::int_fast64_t locked_server_version = 0;
1,374✔
646
    if (is_direct_client(client_type)) {
1,374✔
647
        last_seen_timestamp = 1;
1,374✔
648
    }
1,374✔
649
    m_acc->cf_ident_salts.insert(realm::npos, std::int_fast64_t(file_ident_salt));  // Throws
1,374✔
650
    m_acc->cf_client_versions.insert(realm::npos, client_version);                  // Throws
1,374✔
651
    m_acc->cf_rh_base_versions.insert(realm::npos, recip_hist_base_version);        // Throws
1,374✔
652
    m_acc->cf_recip_hist_refs.insert(realm::npos, recip_hist_ref);                  // Throws
1,374✔
653
    m_acc->cf_proxy_files.insert(realm::npos, std::int_fast64_t(proxy_file_ident)); // Throws
1,374✔
654
    m_acc->cf_client_types.insert(realm::npos, std::int_fast64_t(client_type));     // Throws
1,374✔
655
    m_acc->cf_last_seen_timestamps.insert(realm::npos, last_seen_timestamp);        // Throws
1,374✔
656
    m_acc->cf_locked_server_versions.insert(realm::npos, locked_server_version);    // Throws
1,374✔
657
    std::size_t max_size = std::numeric_limits<std::size_t>::max();
1,374✔
658
    if (m_num_client_files == max_size)
1,374✔
659
        throw util::overflow_error{"Client file index"};
×
660
    ++m_num_client_files;
1,374✔
661
}
1,374✔
662

663

664
void ServerHistory::save_upstream_sync_progress(const SyncProgress& progress)
665
{
×
666
    Array& us = m_acc->upstream_status;
×
667
    us.set(s_us_progress_download_server_version_iip,
×
668
           std::int_fast64_t(progress.download.server_version)); // Throws
×
669
    us.set(s_us_progress_download_client_version_iip,
×
670
           std::int_fast64_t(progress.download.last_integrated_client_version)); // Throws
×
671
    us.set(s_us_progress_latest_server_version_iip,
×
672
           std::int_fast64_t(progress.latest_server_version.version)); // Throws
×
673
    us.set(s_us_progress_latest_server_version_salt_iip,
×
674
           std::int_fast64_t(progress.latest_server_version.salt)); // Throws
×
675
    us.set(s_us_progress_upload_client_version_iip,
×
676
           std::int_fast64_t(progress.upload.client_version)); // Throws
×
677
    us.set(s_us_progress_upload_server_version_iip,
×
678
           std::int_fast64_t(progress.upload.last_integrated_server_version)); // Throws
×
679
}
×
680

681

682
auto ServerHistory::do_bootstrap_client_session(SaltedFileIdent client_file_ident, DownloadCursor download_progress,
683
                                                SaltedVersion server_version, ClientType client_type,
684
                                                UploadCursor& upload_progress, version_type& locked_server_version,
685
                                                Logger& logger) const noexcept -> BootstrapError
686
{
4,238✔
687
    REALM_ASSERT(is_direct_client(client_type));
4,238✔
688
    REALM_ASSERT(client_type != ClientType::legacy);
4,238✔
689

690
    // Validate `client_file_ident`
691
    if (!m_acc)
4,238✔
692
        return BootstrapError::bad_client_file_ident;
×
693
    {
4,238✔
694
        bool good =
4,238✔
695
            (client_file_ident.ident >= 1 && util::int_less_than(client_file_ident.ident, m_num_client_files));
4,238✔
696
        if (!good)
4,238✔
697
            return BootstrapError::bad_client_file_ident;
×
698
    }
4,238✔
699
    std::size_t client_file_index = std::size_t(client_file_ident.ident);
4,238✔
700
    {
4,238✔
701
        auto correct_salt = salt_type(m_acc->cf_ident_salts.get(client_file_index));
4,238✔
702
        bool good = (correct_salt != 0 && // Prevent (spoofed) match on special entries with no salt
4,238✔
703
                     client_file_ident.salt == correct_salt);
4,240✔
704
        if (!good)
4,238✔
705
            return BootstrapError::bad_client_file_ident_salt;
4✔
706
    }
4,238✔
707

708
    // Besides being superfluous, it is also a protocol violation if a client
709
    // asks to download from a point before the base of its reciprocal history.
710
    auto recip_hist_base_version = version_type(m_acc->cf_rh_base_versions.get(client_file_index));
4,234✔
711
    if (download_progress.server_version < recip_hist_base_version) {
4,234✔
712
        logger.debug("Bad download progress: %1 < %2", download_progress.server_version, recip_hist_base_version);
×
713
        return BootstrapError::bad_download_server_version;
×
714
    }
×
715

716
    // If the main history has been trimmed or compacted to a point beyond the
717
    // beginning of the reciprocal history, then the client file entry has
718
    // expired.
719
    //
720
    // NOTE: History trimming (removal of leading history entries) is currently
721
    // never done on server-side files.
722
    //
723
    // NOTE: For an overview of the in-place history compaction mechanism, see
724
    // `/doc/history_compaction.md` in the `realm-sync` Git repository.
725
    std::int_fast64_t last_seen_timestamp = m_acc->cf_last_seen_timestamps.get(client_file_index);
4,234✔
726
    bool expired_due_to_compaction = (last_seen_timestamp == 0);
4,234✔
727
    if (REALM_UNLIKELY(expired_due_to_compaction)) {
4,234✔
728
        logger.debug("Client expired because history has been compacted");
×
729
        return BootstrapError::client_file_expired;
×
730
    }
×
731

732
    REALM_ASSERT_RELEASE(recip_hist_base_version >= m_history_base_version);
4,234✔
733

734
    // Validate `download_progress`
735
    version_type current_server_version = get_server_version();
4,234✔
736
    if (download_progress.server_version > current_server_version)
4,234✔
737
        return BootstrapError::bad_download_server_version;
×
738
    auto last_integrated_client_version = version_type(m_acc->cf_client_versions.get(client_file_index));
4,234✔
739
    if (download_progress.last_integrated_client_version > last_integrated_client_version)
4,234✔
740
        return BootstrapError::bad_download_client_version;
4✔
741

742
    // Validate `server_version`
743
    {
4,230✔
744
        bool good = (server_version.version >= download_progress.server_version &&
4,230✔
745
                     server_version.version <= current_server_version);
4,232✔
746
        if (!good)
4,230✔
747
            return BootstrapError::bad_server_version;
×
748
    }
4,230✔
749
    {
4,230✔
750
        salt_type correct_salt = get_server_version_salt(server_version.version);
4,230✔
751
        bool good = (server_version.salt == correct_salt);
4,230✔
752
        if (!good)
4,230✔
753
            return BootstrapError::bad_server_version_salt;
4✔
754
    }
4,230✔
755

756
    // Validate client type
757
    {
4,226✔
758
        auto client_type_2 = ClientType(m_acc->cf_client_types.get(client_file_index));
4,226✔
759
        bool good = (client_type_2 == ClientType::legacy || client_type == client_type_2);
4,226✔
760
        if (!good)
4,226✔
761
            return BootstrapError::bad_client_type;
×
762
    }
4,226✔
763

764
    upload_progress.client_version = last_integrated_client_version;
4,226✔
765
    upload_progress.last_integrated_server_version = recip_hist_base_version;
4,226✔
766
    locked_server_version = version_type(m_acc->cf_locked_server_versions.get(client_file_index));
4,226✔
767
    return BootstrapError::no_error;
4,226✔
768
}
4,226✔
769

770

771
auto ServerHistory::bootstrap_client_session(SaltedFileIdent client_file_ident, DownloadCursor download_progress,
772
                                             SaltedVersion server_version, ClientType client_type,
773
                                             UploadCursor& upload_progress, version_type& locked_server_version,
774
                                             Logger& logger) const -> BootstrapError
775
{
4,238✔
776
    TransactionRef tr = m_db->start_read(); // Throws
4,238✔
777
    auto realm_version = tr->get_version();
4,238✔
778
    const_cast<ServerHistory*>(this)->set_group(tr.get());
4,238✔
779
    ensure_updated(realm_version); // Throws
4,238✔
780

781
    BootstrapError error = do_bootstrap_client_session(client_file_ident, download_progress, server_version,
4,238✔
782
                                                       client_type, upload_progress, locked_server_version, logger);
4,238✔
783
    return error;
4,238✔
784
}
4,238✔
785

786

787
bool ServerHistory::fetch_download_info(file_ident_type client_file_ident, DownloadCursor& download_progress,
788
                                        version_type end_version, UploadCursor& upload_progress,
789
                                        HistoryEntryHandler& handler,
790
                                        std::uint_fast64_t& cumulative_byte_size_current,
791
                                        std::uint_fast64_t& cumulative_byte_size_total,
792
                                        std::size_t accum_byte_size_soft_limit) const
793
{
42,520✔
794
    REALM_ASSERT(client_file_ident != 0);
42,520✔
795
    REALM_ASSERT(download_progress.server_version <= end_version);
42,520✔
796

797
    TransactionRef tr = m_db->start_read(); // Throws
42,520✔
798
    version_type realm_version = tr->get_version();
42,520✔
799
    const_cast<ServerHistory*>(this)->set_group(tr.get());
42,520✔
800
    ensure_updated(realm_version); // Throws
42,520✔
801

802
    REALM_ASSERT(download_progress.server_version >= m_history_base_version);
42,520✔
803

804
    std::size_t client_file_index = std::size_t(client_file_ident);
42,520✔
805
    {
42,520✔
806
        auto client_type = ClientType(m_acc->cf_client_types.get(client_file_index));
42,520✔
807
        REALM_ASSERT_RELEASE(is_direct_client(client_type));
42,520✔
808
        std::int_fast64_t last_seen_timestamp = m_acc->cf_last_seen_timestamps.get(client_file_index);
42,520✔
809
        bool expired = (last_seen_timestamp == 0);
42,520✔
810
        if (REALM_UNLIKELY(expired))
42,520✔
811
            return false;
×
812
    }
42,520✔
813

814
    std::size_t accum_byte_size = 0;
42,520✔
815
    DownloadCursor download_progress_2 = download_progress;
42,520✔
816

817
    std::vector<Changeset> changesets;
42,520✔
818
    std::vector<std::size_t> original_changeset_sizes;
42,520✔
819

820
    for (;;) {
85,500✔
821
        version_type begin_version = download_progress_2.server_version;
85,500✔
822
        HistoryEntry entry;
85,500✔
823
        version_type version = find_history_entry(client_file_ident, begin_version, end_version, entry,
85,500✔
824
                                                  download_progress_2.last_integrated_client_version);
85,500✔
825
        if (version == 0) {
85,500✔
826
            // End of history reached
827
            download_progress_2.server_version = end_version;
42,482✔
828
            break;
42,482✔
829
        }
42,482✔
830

831
        download_progress_2.server_version = version;
43,018✔
832

833
        entry.remote_version = download_progress_2.last_integrated_client_version;
43,018✔
834

835
        if (entry.origin_file_ident == 0)
43,018✔
836
            entry.origin_file_ident = m_local_file_ident;
4,800✔
837

838
        handler.handle(download_progress_2.server_version, entry, entry.changeset.size()); // Throws
43,018✔
839

840
        accum_byte_size += entry.changeset.size();
43,018✔
841

842
        if (accum_byte_size > accum_byte_size_soft_limit)
43,018✔
843
            break;
36✔
844
    }
43,018✔
845

846
    // Set cumulative byte sizes.
847
    std::int_fast64_t cumulative_byte_size_current_2 = 0;
42,520✔
848
    std::int_fast64_t cumulative_byte_size_total_2 = 0;
42,520✔
849
    if (download_progress_2.server_version > m_history_base_version) {
42,520✔
850
        std::size_t begin_ndx = to_size_t(download_progress_2.server_version - m_history_base_version) - 1;
41,286✔
851
        cumulative_byte_size_current_2 = m_acc->sh_cumul_byte_sizes.get(begin_ndx);
41,286✔
852
        REALM_ASSERT(cumulative_byte_size_current_2 >= 0);
41,286✔
853
    }
41,286✔
854
    if (m_history_size > 0) {
42,520✔
855
        std::size_t end_ndx = m_history_size - 1;
41,288✔
856
        cumulative_byte_size_total_2 = m_acc->sh_cumul_byte_sizes.get(end_ndx);
41,288✔
857
    }
41,288✔
858
    REALM_ASSERT(cumulative_byte_size_current_2 <= cumulative_byte_size_total_2);
42,520✔
859

860
    version_type upload_client_version = version_type(m_acc->cf_client_versions.get(client_file_index));
42,520✔
861
    version_type upload_server_version = version_type(m_acc->cf_rh_base_versions.get(client_file_index));
42,520✔
862

863
    download_progress = download_progress_2;
42,520✔
864
    cumulative_byte_size_current = std::uint_fast64_t(cumulative_byte_size_current_2);
42,520✔
865
    cumulative_byte_size_total = std::uint_fast64_t(cumulative_byte_size_total_2);
42,520✔
866
    upload_progress = UploadCursor{upload_client_version, upload_server_version};
42,520✔
867

868
    return true;
42,520✔
869
}
42,520✔
870

871

872
void ServerHistory::add_upstream_sync_status()
873
{
×
874
    TransactionRef tr = m_db->start_write(); // Throws
×
875
    version_type realm_version = tr->get_version();
×
876
    ensure_updated(realm_version); // Throws
×
877
    prepare_for_write();           // Throws
×
878

879
    REALM_ASSERT(!m_acc->upstream_status.is_attached());
×
880
    REALM_ASSERT(m_local_file_ident == g_root_node_file_ident);
×
881

882
    // An upstream status cannot be added to a file from which new client file
883
    // identifiers have already been allocated, since in a star topology server
884
    // cluster, all file identifiers must be allocated by the root node.
885
    static_assert(g_root_node_file_ident == 1, "");
×
886
    if (REALM_UNLIKELY(m_num_client_files > 2)) {
×
887
        throw util::runtime_error("Realm file has registered client file identifiers, "
×
888
                                  "so can no longer be associated with upstream server "
×
889
                                  "(star topology server cluster)");
×
890
    }
×
891

892
    bool context_flag_no = false;
×
893
    std::size_t size = s_upstream_status_size;
×
894
    m_acc->upstream_status.create(Array::type_Normal, context_flag_no, size); // Throws
×
895
    _impl::ShallowArrayDestroyGuard adg{&m_acc->upstream_status};
×
896
    m_acc->upstream_status.update_parent(); // Throws
×
897
    adg.release();                          // Ref ownership transferred to parent array
×
898
    tr->commit();                           // Throws
×
899
}
×
900

901
std::vector<sync::Changeset> ServerHistory::get_parsed_changesets(version_type begin, version_type end) const
902
{
×
903
    TransactionRef rt = m_db->start_read(); // Throws
×
904
    version_type realm_version = rt->get_version();
×
905
    const_cast<ServerHistory*>(this)->set_group(rt.get());
×
906
    ensure_updated(realm_version);
×
907

908
    REALM_ASSERT(begin > m_history_base_version);
×
909
    if (end == version_type(-1)) {
×
910
        end = m_history_base_version + m_history_size + 1;
×
911
    }
×
912
    REALM_ASSERT(begin <= end);
×
913

914
    std::vector<sync::Changeset> changesets;
×
915
    changesets.reserve(std::size_t(end - begin)); // Throws
×
916
    for (version_type version = begin; version < end; ++version) {
×
917
        std::size_t ndx = std::size_t(version - m_history_base_version - 1);
×
918
        Changeset changeset;
×
919

920
        auto binary = ChunkedBinaryData{m_acc->sh_changesets, ndx};
×
921
        ChunkedBinaryInputStream stream{binary};
×
922
        parse_changeset(stream, changeset); // Throws
×
923

924
        // Add the attributes for the changeset.
925
        changeset.last_integrated_remote_version = m_acc->sh_client_versions.get(ndx);
×
926
        changeset.origin_file_ident = m_acc->sh_origin_files.get(ndx);
×
927
        changeset.origin_timestamp = m_acc->sh_timestamps.get(ndx);
×
928
        changeset.version = version;
×
929
        changesets.emplace_back(std::move(changeset));
×
930
    }
×
931
    return changesets;
×
932
}
×
933

934

935
class ServerHistory::ReciprocalHistory : private ArrayParent {
936
public:
937
    ReciprocalHistory(BPlusTree<ref_type>& cf_recip_hist_refs, std::size_t remote_file_index,
938
                      version_type base_version)
939
        : m_cf_recip_hist_refs{cf_recip_hist_refs}
18,064✔
940
        , m_remote_file_index{remote_file_index}
18,064✔
941
        , m_base_version{base_version}
18,064✔
942
    {
38,500✔
943
        if (ref_type ref = to_ref(cf_recip_hist_refs.get(remote_file_index))) {
38,500✔
944
            init(ref);                     // Throws
33,210✔
945
            m_size = m_changesets->size(); // Relatively expensive
33,210✔
946
        }
33,210✔
947
    }
38,500✔
948

949
    std::size_t remote_file_index() const noexcept
950
    {
38,472✔
951
        return m_remote_file_index;
38,472✔
952
    }
38,472✔
953

954
    version_type base_version() const noexcept
955
    {
38,472✔
956
        return m_base_version;
38,472✔
957
    }
38,472✔
958

959
    std::size_t size() const noexcept
960
    {
×
961
        return m_size;
×
962
    }
×
963

964
    // Returns true iff the reciprocal history has been instantiated
965
    explicit operator bool() const noexcept
966
    {
×
967
        return bool(m_changesets);
×
968
    }
×
969

970
    void ensure_instantiated()
971
    {
20,454✔
972
        if (m_changesets)
20,454✔
973
            return;
19,570✔
974

975
        // Instantiate the reciprocal history
976
        Allocator& alloc = m_cf_recip_hist_refs.get_alloc();
884✔
977
        BinaryColumn recip_hist(alloc);
884✔
978
        recip_hist.create();
884✔
979
        auto ref = recip_hist.get_ref();
884✔
980
        DeepArrayRefDestroyGuard adg{ref, alloc};
884✔
981
        m_cf_recip_hist_refs.set(m_remote_file_index, ref); // Throws
884✔
982
        adg.release();                                      // Ref ownership transferred to parent array
884✔
983
        init(ref);                                          // Throws
884✔
984
    }
884✔
985

986
    // The reciprocal history must have been instantiated (see
987
    // ensure_instantiated()).
988
    bool get(version_type server_version, ChunkedBinaryData& transform) const noexcept
989
    {
82,964✔
990
        REALM_ASSERT(m_changesets);
82,964✔
991
        REALM_ASSERT(server_version > m_base_version);
82,964✔
992

993
        std::size_t i = std::size_t(server_version - m_base_version - 1);
82,964✔
994
        if (i < m_size) {
82,964✔
995
            ChunkedBinaryData transform_2{*m_changesets, i};
31,836✔
996
            if (!transform_2.is_null()) {
31,836✔
997
                transform = transform_2;
8,668✔
998
                return true;
8,668✔
999
            }
8,668✔
1000
        }
31,836✔
1001
        return false;
74,296✔
1002
    }
82,964✔
1003

1004
    // The reciprocal history must have been instantiated (see
1005
    // ensure_instantiated()).
1006
    void set(version_type server_version, BinaryData transform)
1007
    {
4,214✔
1008
        REALM_ASSERT(m_changesets);
4,214✔
1009
        REALM_ASSERT(server_version > m_base_version);
4,214✔
1010
        std::size_t i = std::size_t(server_version - m_base_version - 1);
4,214✔
1011
        while (m_size <= i) {
19,292✔
1012
            m_changesets->add({}); // Throws
15,078✔
1013
            m_size++;
15,078✔
1014
        }
15,078✔
1015
        // FIXME: BinaryColumn::set() currently interprets BinaryData(0,0) as
1016
        // null. It should probably be changed such that BinaryData(0,0) is
1017
        // always interpreted as the empty string. For the purpose of setting
1018
        // null values, BinaryColumn::set() should accept values of type
1019
        // util::Optional<BinaryData>().
1020
        BinaryData transform_2 = (transform.is_null() ? BinaryData{"", 0} : transform);
4,214✔
1021
        m_changesets->set(i, transform_2); // Throws
4,214✔
1022
    }
4,214✔
1023

1024
    // Requires that new_base_version > base_version()
1025
    void trim(version_type new_base_version)
1026
    {
29,278✔
1027
        REALM_ASSERT(new_base_version > m_base_version);
29,278✔
1028
        std::size_t n = std::size_t(new_base_version - m_base_version);
29,278✔
1029
        if (n >= m_size) {
29,278✔
1030
            if (m_changesets)
28,466✔
1031
                m_changesets->clear(); // Throws
24,060✔
1032
            m_base_version = new_base_version;
28,466✔
1033
            m_size = 0;
28,466✔
1034
            return;
28,466✔
1035
        }
28,466✔
1036
        REALM_ASSERT(m_changesets);
812✔
1037
        while (n) {
15,350✔
1038
            m_changesets->erase(0);
14,538✔
1039
            --n;
14,538✔
1040
        }
14,538✔
1041
        m_base_version = new_base_version;
812✔
1042
        m_size -= n;
812✔
1043
    }
812✔
1044

1045
    void update_child_ref(std::size_t child_ndx, ref_type new_ref) override final
1046
    {
936✔
1047
        m_cf_recip_hist_refs.set(child_ndx, new_ref); // Throws
936✔
1048
    }
936✔
1049

1050
    ref_type get_child_ref(std::size_t child_ndx) const noexcept override final
1051
    {
×
1052
        return m_cf_recip_hist_refs.get(child_ndx);
×
1053
    }
×
1054

1055
private:
1056
    BPlusTree<ref_type>& m_cf_recip_hist_refs;
1057
    const std::size_t m_remote_file_index;
1058
    version_type m_base_version;
1059
    std::size_t m_size = 0;
1060
    util::Optional<BinaryColumn> m_changesets;
1061

1062
    void init(ref_type ref)
1063
    {
34,094✔
1064
        Allocator& alloc = m_cf_recip_hist_refs.get_alloc();
34,094✔
1065
        m_changesets.emplace(alloc); // Throws
34,094✔
1066
        m_changesets->init_from_ref(ref);
34,094✔
1067
        m_changesets->set_parent(this, m_remote_file_index);
34,094✔
1068
    }
34,094✔
1069
};
1070

1071

1072
class ServerHistory::TransformHistoryImpl : public TransformHistory {
1073
public:
1074
    TransformHistoryImpl(file_ident_type remote_file_ident, ServerHistory& history,
1075
                         ReciprocalHistory& recip_hist) noexcept
1076
        : m_remote_file_ident{remote_file_ident}
9,238✔
1077
        , m_history{history}
9,238✔
1078
        , m_recip_hist{recip_hist}
9,238✔
1079
    {
20,448✔
1080
    }
20,448✔
1081

1082
    version_type find_history_entry(version_type begin_version, version_type end_version,
1083
                                    HistoryEntry& entry) const noexcept override final
1084
    {
222,692✔
1085
        return m_history.find_history_entry(m_remote_file_ident, begin_version, end_version, entry);
222,692✔
1086
    }
222,692✔
1087

1088
    ChunkedBinaryData get_reciprocal_transform(version_type server_version,
1089
                                               bool& is_compressed) const noexcept override final
1090
    {
82,966✔
1091
        is_compressed = false;
82,966✔
1092
        ChunkedBinaryData transform;
82,966✔
1093
        if (m_recip_hist.get(server_version, transform))
82,966✔
1094
            return transform;
8,668✔
1095
        HistoryEntry entry = m_history.get_history_entry(server_version);
74,298✔
1096
        return entry.changeset;
74,298✔
1097
    }
82,966✔
1098

1099
    void set_reciprocal_transform(version_type server_version, BinaryData transform) override final
1100
    {
4,214✔
1101
        m_recip_hist.set(server_version, transform); // Throws
4,214✔
1102
    }
4,214✔
1103

1104
private:
1105
    const file_ident_type m_remote_file_ident; // Zero for server
1106
    ServerHistory& m_history;
1107
    ReciprocalHistory& m_recip_hist;
1108
};
1109

1110

1111
bool ServerHistory::integrate_remote_changesets(file_ident_type remote_file_ident, UploadCursor upload_progress,
1112
                                                version_type locked_server_version, const RemoteChangeset* changesets,
1113
                                                std::size_t num_changesets, util::Logger& logger)
1114
{
38,500✔
1115
    std::size_t remote_file_index = std::size_t(remote_file_ident);
38,500✔
1116
    REALM_ASSERT(remote_file_index < m_num_client_files);
38,500✔
1117
    bool from_downstream = (remote_file_ident != 0);
38,500✔
1118
    if (from_downstream) {
38,500✔
1119
        auto client_type = ClientType(m_acc->cf_client_types.get(remote_file_index));
38,500✔
1120
        REALM_ASSERT_RELEASE(is_direct_client(client_type));
38,500✔
1121
        std::int_fast64_t last_seen_timestamp = m_acc->cf_last_seen_timestamps.get(remote_file_index);
38,500✔
1122
        bool expired = (last_seen_timestamp == 0);
38,500✔
1123
        REALM_ASSERT_RELEASE(!expired);
38,500✔
1124
    }
38,500✔
1125
    version_type orig_client_version = version_type(m_acc->cf_client_versions.get(remote_file_index));
38,500✔
1126
    version_type recip_hist_base_version = version_type(m_acc->cf_rh_base_versions.get(remote_file_index));
38,500✔
1127
    ReciprocalHistory recip_hist(m_acc->cf_recip_hist_refs, remote_file_index,
38,500✔
1128
                                 recip_hist_base_version); // Throws
38,500✔
1129

1130
    {
38,500✔
1131
        UploadCursor prev_upload_cursor = {orig_client_version, recip_hist_base_version};
38,500✔
1132
        for (std::size_t i = 0; i < num_changesets; ++i) {
72,990✔
1133
            // Note: remote_file_ident may be different from
1134
            // changeset.origin_file_ident in a cluster setup.
1135
            REALM_ASSERT(changesets[i].origin_file_ident > 0);
34,490✔
1136
            UploadCursor upload_cursor = {changesets[i].remote_version, changesets[i].last_integrated_local_version};
34,490✔
1137
            REALM_ASSERT(upload_cursor.client_version > prev_upload_cursor.client_version);
34,490✔
1138
            REALM_ASSERT(are_mutually_consistent(upload_cursor, prev_upload_cursor));
34,490✔
1139
            prev_upload_cursor = upload_cursor;
34,490✔
1140
        }
34,490✔
1141
    }
38,500✔
1142

1143
    if (num_changesets > 0) {
38,500✔
1144
        recip_hist.ensure_instantiated(); // Throws
20,454✔
1145

1146
        // Parse the changesets
1147
        std::vector<Changeset> parsed_transformed_changesets;
20,454✔
1148
        parsed_transformed_changesets.resize(num_changesets);
20,454✔
1149
        for (std::size_t i = 0; i < num_changesets; ++i)
54,942✔
1150
            parse_remote_changeset(changesets[i], parsed_transformed_changesets[i]); // Throws
34,488✔
1151

1152
        // Transform the changesets
1153
        version_type current_server_version = get_server_version();
20,454✔
1154
        Group& group = *m_group;
20,454✔
1155
        Transaction& transaction = dynamic_cast<Transaction&>(group);
20,454✔
1156
        auto apply = [&](const Changeset* c) -> bool {
34,464✔
1157
            TempShortCircuitReplication tdr{*this}; // Short-circuit while integrating changes
34,464✔
1158
            InstructionApplier applier{transaction};
34,464✔
1159
            applier.apply(*c);
34,464✔
1160
            reset(); // Reset the instruction encoder
34,464✔
1161
            return true;
34,464✔
1162
        };
34,464✔
1163
        // Merge with causally unrelated changesets, and resolve the
1164
        // conflicts if there are any.
1165
        TransformHistoryImpl transform_hist{remote_file_ident, *this, recip_hist};
20,454✔
1166
        Transformer transformer;
20,454✔
1167
        transformer.transform_remote_changesets(transform_hist, m_local_file_ident, current_server_version,
20,454✔
1168
                                                parsed_transformed_changesets, apply, logger); // Throws
20,454✔
1169

1170
        for (std::size_t i = 0; i < num_changesets; ++i) {
54,918✔
1171
            REALM_ASSERT(get_instruction_encoder().buffer().size() == 0);
34,464✔
1172
            const Changeset& changeset = parsed_transformed_changesets[i];
34,464✔
1173

1174
            HistoryEntry entry;
34,464✔
1175
            entry.origin_timestamp = changeset.origin_timestamp;
34,464✔
1176
            entry.origin_file_ident = changeset.origin_file_ident;
34,464✔
1177
            entry.remote_version = changeset.version;
34,464✔
1178

1179
            ChangesetEncoder::Buffer changeset_buffer;
34,464✔
1180

1181
            encode_changeset(parsed_transformed_changesets[i], changeset_buffer); // Throws
34,464✔
1182
            entry.changeset = BinaryData{changeset_buffer.data(), changeset_buffer.size()};
34,464✔
1183

1184
            add_sync_history_entry(entry); // Throws
34,464✔
1185
        }
34,464✔
1186
    }
20,454✔
1187

1188
    bool dirty = (num_changesets > 0);
38,500✔
1189

1190
    if (update_upload_progress(orig_client_version, recip_hist, upload_progress)) // Throws
38,500✔
1191
        dirty = true;
38,474✔
1192

1193
    if (from_downstream) {
38,500✔
1194
        version_type orig_version = version_type(m_acc->cf_locked_server_versions.get(remote_file_index));
38,474✔
1195
        if (locked_server_version > orig_version) {
38,474✔
1196
            m_acc->cf_locked_server_versions.set(remote_file_index,
29,068✔
1197
                                                 std::int_fast64_t(locked_server_version)); // Throws
29,068✔
1198
            dirty = true;
29,068✔
1199
        }
29,068✔
1200
    }
38,474✔
1201

1202
    if (from_downstream && dirty) {
38,500✔
1203
        m_acc->cf_last_seen_timestamps.set(remote_file_index, 1);
38,474✔
1204
    }
38,474✔
1205

1206
    return dirty;
38,500✔
1207
}
38,500✔
1208

1209

1210
bool ServerHistory::update_upload_progress(version_type orig_client_version, ReciprocalHistory& recip_hist,
1211
                                           UploadCursor upload_progress)
1212
{
38,472✔
1213
    UploadCursor orig_upload_progress = {orig_client_version, recip_hist.base_version()};
38,472✔
1214
    REALM_ASSERT(upload_progress.client_version >= orig_upload_progress.client_version);
38,472✔
1215
    REALM_ASSERT(are_mutually_consistent(upload_progress, orig_upload_progress));
38,472✔
1216
    std::size_t client_file_index = recip_hist.remote_file_index();
38,472✔
1217
    bool update_client_version = (upload_progress.client_version > orig_upload_progress.client_version);
38,472✔
1218
    if (update_client_version) {
38,472✔
1219
        auto value_1 = std::int_fast64_t(upload_progress.client_version);
38,472✔
1220
        m_acc->cf_client_versions.set(client_file_index, value_1); // Throws
38,472✔
1221
        bool update_server_version =
38,472✔
1222
            (upload_progress.last_integrated_server_version > orig_upload_progress.last_integrated_server_version);
38,472✔
1223
        if (update_server_version) {
38,472✔
1224
            recip_hist.trim(upload_progress.last_integrated_server_version); // Throws
29,278✔
1225
            auto value_2 = std::int_fast64_t(upload_progress.last_integrated_server_version);
29,278✔
1226
            m_acc->cf_rh_base_versions.set(client_file_index, value_2); // Throws
29,278✔
1227
        }
29,278✔
1228
        return true;
38,472✔
1229
    }
38,472✔
1230
    return false;
×
1231
}
38,472✔
1232

1233

1234
// Overriding member in Replication
1235
void ServerHistory::initialize(DB& sg)
1236
{
2,244✔
1237
    REALM_ASSERT(!m_db);
2,244✔
1238
    SyncReplication::initialize(sg); // Throws
2,244✔
1239
    m_db = &sg;
2,244✔
1240
}
2,244✔
1241

1242

1243
// Overriding member in Replication
1244
auto ServerHistory::get_history_type() const noexcept -> HistoryType
1245
{
4,128✔
1246
    return hist_SyncServer;
4,128✔
1247
}
4,128✔
1248

1249

1250
// Overriding member in Replication
1251
int ServerHistory::get_history_schema_version() const noexcept
1252
{
2,228✔
1253
    return get_server_history_schema_version();
2,228✔
1254
}
2,228✔
1255

1256

1257
// Overriding member in Replication
1258
bool ServerHistory::is_upgradable_history_schema(int stored_schema_version) const noexcept
1259
{
×
1260
    if (stored_schema_version >= 20) {
×
1261
        return true;
×
1262
    }
×
1263
    return false;
×
1264
}
×
1265

1266

1267
// Overriding member in Replication
1268
void ServerHistory::upgrade_history_schema(int stored_schema_version)
1269
{
×
1270
    // upgrade_history_schema() is called only when there is a need to upgrade
1271
    // (`stored_schema_version < get_server_history_schema_version()`), and only
1272
    // when is_upgradable_history_schema() returned true (`stored_schema_version
1273
    // >= 1`).
1274
    REALM_ASSERT(stored_schema_version < get_server_history_schema_version());
×
1275
    REALM_ASSERT(stored_schema_version >= 1);
×
1276
    int orig_schema_version = stored_schema_version;
×
1277
    int schema_version = orig_schema_version;
×
1278
    // NOTE: Future migration steps go here.
1279

1280
    REALM_ASSERT(schema_version == get_server_history_schema_version());
×
1281

1282
    // Record migration event
1283
    record_current_schema_version(); // Throws
×
1284
}
×
1285

1286

1287
// Overriding member in Replication
1288
_impl::History* ServerHistory::_get_history_write()
1289
{
83,914✔
1290
    return this;
83,914✔
1291
}
83,914✔
1292

1293
// Overriding member in Replication
1294
std::unique_ptr<_impl::History> ServerHistory::_create_history_read()
1295
{
16✔
1296
    auto server_hist = std::make_unique<ServerHistory>(m_context); // Throws
16✔
1297
    server_hist->initialize(*m_db);                                // Throws
16✔
1298
    return std::unique_ptr<_impl::History>(server_hist.release());
16✔
1299
}
16✔
1300

1301

1302
// Overriding member in Replication
1303
auto ServerHistory::prepare_changeset(const char* data, std::size_t size, version_type realm_version) -> version_type
1304
{
41,938✔
1305
    ensure_updated(realm_version);
41,938✔
1306
    prepare_for_write(); // Throws
41,938✔
1307

1308
    bool nonempty_changeset_of_local_origin = (m_is_local_changeset && size != 0);
41,938✔
1309

1310
    if (nonempty_changeset_of_local_origin) {
41,938✔
1311
        auto& buffer = get_instruction_encoder().buffer();
4,812✔
1312
        BinaryData changeset{buffer.data(), buffer.size()};
4,812✔
1313
        HistoryEntry entry;
4,812✔
1314
        entry.origin_timestamp = sync::generate_changeset_timestamp();
4,812✔
1315
        entry.origin_file_ident = 0; // Of local origin
4,812✔
1316
        entry.remote_version = 0;    // Of local origin on server-side
4,812✔
1317
        entry.changeset = changeset;
4,812✔
1318

1319
        add_sync_history_entry(entry); // Throws
4,812✔
1320
    }
4,812✔
1321

1322
    // Add the standard ct changeset.
1323
    // This is done for changes of both local and remote origin.
1324
    BinaryData core_changeset{data, size};
41,938✔
1325
    add_core_history_entry(core_changeset); // Thows
41,938✔
1326

1327
    return m_ct_base_version + m_ct_history_size; // New snapshot number
41,938✔
1328
}
41,938✔
1329

1330

1331
// Overriding member in _impl::History
1332
void ServerHistory::update_from_parent(version_type realm_version)
1333
{
94,654✔
1334
    using gf = _impl::GroupFriend;
94,654✔
1335
    ref_type ref = gf::get_history_ref(*m_group);
94,654✔
1336
    update_from_ref_and_version(ref, realm_version); // Throws
94,654✔
1337
}
94,654✔
1338

1339

1340
// Overriding member in _impl::History
1341
void ServerHistory::get_changesets(version_type begin_version, version_type end_version,
1342
                                   BinaryIterator* iterators) const noexcept
1343
{
×
1344
    REALM_ASSERT(begin_version <= end_version);
×
1345
    REALM_ASSERT(begin_version >= m_ct_base_version);
×
1346
    REALM_ASSERT(end_version <= m_ct_base_version + m_ct_history_size);
×
1347
    std::size_t n = to_size_t(end_version - begin_version);
×
1348
    REALM_ASSERT(n == 0 || m_acc);
×
1349
    std::size_t offset = to_size_t(begin_version - m_ct_base_version);
×
1350
    for (std::size_t i = 0; i < n; ++i) {
×
1351
        iterators[i] = BinaryIterator(&m_acc->ct_history, offset + i);
×
1352
    }
×
1353
}
×
1354

1355

1356
// Overriding member in _impl::History
1357
void ServerHistory::set_oldest_bound_version(version_type realm_version)
1358
{
41,936✔
1359
    REALM_ASSERT(realm_version >= m_version_of_oldest_bound_snapshot);
41,936✔
1360
    if (realm_version > m_version_of_oldest_bound_snapshot) {
41,936✔
1361
        m_version_of_oldest_bound_snapshot = realm_version;
41,936✔
1362
        trim_cont_transact_history(); // Throws
41,936✔
1363
    }
41,936✔
1364
}
41,936✔
1365

1366

1367
// Overriding member in _impl::History
1368
void ServerHistory::verify() const
1369
{
16✔
1370
#ifdef REALM_DEBUG
16✔
1371
    // The size of the continuous transactions history can only be zero when the
1372
    // Realm is in the initial empty state where top-ref is null.
1373
    version_type initial_realm_version = 1;
16✔
1374
    REALM_ASSERT(m_ct_history_size != 0 || m_ct_base_version == initial_realm_version);
16✔
1375

1376
    if (!m_acc) {
16✔
1377
        REALM_ASSERT(m_local_file_ident == g_root_node_file_ident);
×
1378
        REALM_ASSERT(m_num_client_files == 0);
×
1379
        REALM_ASSERT(m_history_size == 0);
×
1380
        REALM_ASSERT(m_server_version_salt == 0);
×
1381
        REALM_ASSERT(m_history_base_version == 0);
×
1382
        REALM_ASSERT(m_ct_history_size == 0);
×
1383
        return;
×
1384
    }
×
1385

1386
    m_acc->root.verify();
16✔
1387
    m_acc->client_files.verify();
16✔
1388
    m_acc->sync_history.verify();
16✔
1389
    if (m_acc->upstream_status.is_attached())
16✔
1390
        m_acc->upstream_status.verify();
×
1391
    if (m_acc->partial_sync.is_attached())
16✔
1392
        m_acc->partial_sync.verify();
×
1393
    m_acc->cf_ident_salts.verify();
16✔
1394
    m_acc->cf_client_versions.verify();
16✔
1395
    m_acc->cf_rh_base_versions.verify();
16✔
1396
    m_acc->cf_recip_hist_refs.verify();
16✔
1397
    m_acc->cf_proxy_files.verify();
16✔
1398
    m_acc->cf_client_types.verify();
16✔
1399
    m_acc->cf_last_seen_timestamps.verify();
16✔
1400
    m_acc->cf_locked_server_versions.verify();
16✔
1401
    m_acc->sh_version_salts.verify();
16✔
1402
    m_acc->sh_origin_files.verify();
16✔
1403
    m_acc->sh_client_versions.verify();
16✔
1404
    m_acc->sh_timestamps.verify();
16✔
1405
    m_acc->sh_changesets.verify();
16✔
1406
    m_acc->sh_cumul_byte_sizes.verify();
16✔
1407
    m_acc->ct_history.verify();
16✔
1408

1409
    REALM_ASSERT(m_history_base_version == m_acc->root.get_as_ref_or_tagged(s_history_base_version_iip).get_as_int());
16✔
1410
    salt_type base_version_salt = m_acc->root.get_as_ref_or_tagged(s_base_version_salt_iip).get_as_int();
16✔
1411
    REALM_ASSERT((m_history_base_version == 0) == (base_version_salt == 0));
16✔
1412

1413
    REALM_ASSERT(m_acc->cf_ident_salts.size() == m_num_client_files);
16✔
1414
    REALM_ASSERT(m_acc->cf_client_versions.size() == m_num_client_files);
16✔
1415
    REALM_ASSERT(m_acc->cf_rh_base_versions.size() == m_num_client_files);
16✔
1416
    REALM_ASSERT(m_acc->cf_recip_hist_refs.size() == m_num_client_files);
16✔
1417
    REALM_ASSERT(m_acc->cf_proxy_files.size() == m_num_client_files);
16✔
1418
    REALM_ASSERT(m_acc->cf_client_types.size() == m_num_client_files);
16✔
1419
    REALM_ASSERT(m_acc->cf_last_seen_timestamps.size() == m_num_client_files);
16✔
1420
    REALM_ASSERT(m_acc->cf_locked_server_versions.size() == m_num_client_files);
16✔
1421

1422
    REALM_ASSERT(m_acc->sh_version_salts.size() == m_history_size);
16✔
1423
    REALM_ASSERT(m_acc->sh_origin_files.size() == m_history_size);
16✔
1424
    REALM_ASSERT(m_acc->sh_client_versions.size() == m_history_size);
16✔
1425
    REALM_ASSERT(m_acc->sh_timestamps.size() == m_history_size);
16✔
1426
    REALM_ASSERT(m_acc->sh_changesets.size() == m_history_size);
16✔
1427
    REALM_ASSERT(m_acc->sh_cumul_byte_sizes.size() == m_history_size);
16✔
1428

1429
    salt_type server_version_salt =
16✔
1430
        (m_history_size == 0 ? base_version_salt : salt_type(m_acc->sh_version_salts.get(m_history_size - 1)));
16✔
1431
    REALM_ASSERT(m_server_version_salt == server_version_salt);
16✔
1432

1433
    REALM_ASSERT(m_local_file_ident > 0 && std::uint_fast64_t(m_local_file_ident) < m_num_client_files);
16✔
1434

1435
    // Check history entries
1436
    std::int_fast64_t accum_byte_size = 0;
16✔
1437
    struct ClientFile {
16✔
1438
        version_type last_integrated_client_version;
16✔
1439
    };
16✔
1440
    std::unordered_map<file_ident_type, ClientFile> client_files;
16✔
1441
    for (std::size_t i = 0; i < m_history_size; ++i) {
44✔
1442
        auto salt = m_acc->sh_version_salts.get(i);
28✔
1443
        REALM_ASSERT(salt > 0 && salt <= 0x0'7FFF'FFFF'FFFF'FFFF);
28✔
1444
        file_ident_type origin_file_ident = 0;
28✔
1445
        REALM_ASSERT(!util::int_cast_with_overflow_detect(m_acc->sh_origin_files.get(i), origin_file_ident));
28✔
1446
        REALM_ASSERT(origin_file_ident != m_local_file_ident);
28✔
1447
        std::size_t origin_file_index = 0;
28✔
1448
        REALM_ASSERT(!util::int_cast_with_overflow_detect(origin_file_ident, origin_file_index));
28✔
1449
        REALM_ASSERT(origin_file_index < m_num_client_files);
28✔
1450
        version_type client_version = 0;
28✔
1451
        REALM_ASSERT(!util::int_cast_with_overflow_detect(m_acc->sh_client_versions.get(i), client_version));
28✔
1452
        bool of_local_origin = (origin_file_ident == 0);
28✔
1453
        if (of_local_origin) {
28✔
1454
            REALM_ASSERT(client_version == 0);
×
1455
        }
×
1456
        else {
28✔
1457
            file_ident_type client_file_ident = 0;
28✔
1458
            bool from_reference_file = (origin_file_ident == m_local_file_ident);
28✔
1459
            if (!from_reference_file) {
28✔
1460
                auto client_type = m_acc->cf_client_types.get(origin_file_index);
28✔
1461
                bool good_client_type = false;
28✔
1462
                switch (ClientType(client_type)) {
28✔
1463
                    case ClientType::upstream:
✔
1464
                        good_client_type = true;
×
1465
                        break;
×
1466
                    case ClientType::indirect: {
✔
1467
                        auto proxy_file = m_acc->cf_proxy_files.get(origin_file_index);
×
1468
                        REALM_ASSERT(!util::int_cast_with_overflow_detect(proxy_file, client_file_ident));
×
1469
                        good_client_type = true;
×
1470
                        break;
×
1471
                    }
×
1472
                    case ClientType::self:
✔
1473
                        break;
×
1474
                    case ClientType::legacy:
✔
1475
                    case ClientType::regular:
28✔
1476
                    case ClientType::subserver:
28✔
1477
                        client_file_ident = origin_file_ident;
28✔
1478
                        good_client_type = true;
28✔
1479
                        break;
28✔
1480
                }
28✔
1481
                REALM_ASSERT(good_client_type);
28✔
1482
            }
28✔
1483
            ClientFile& client_file = client_files[client_file_ident];
28✔
1484
            if (from_reference_file) {
28✔
1485
                REALM_ASSERT(client_version >= client_file.last_integrated_client_version);
×
1486
            }
×
1487
            else {
28✔
1488
                REALM_ASSERT(client_version > client_file.last_integrated_client_version);
28✔
1489
            }
28✔
1490
            client_file.last_integrated_client_version = client_version;
28✔
1491
        }
28✔
1492

1493
        std::size_t changeset_size = ChunkedBinaryData(m_acc->sh_changesets, i).size();
28✔
1494
        accum_byte_size += changeset_size;
28✔
1495
        REALM_ASSERT(m_acc->sh_cumul_byte_sizes.get(i) == accum_byte_size);
28✔
1496
    }
28✔
1497

1498
    // Check client file entries
1499
    version_type current_server_version = m_history_base_version + m_history_size;
16✔
1500
    REALM_ASSERT(m_num_client_files >= 2);
16✔
1501
    bool found_self = false;
16✔
1502
    for (std::size_t i = 0; i < m_num_client_files; ++i) {
88✔
1503
        file_ident_type client_file_ident = file_ident_type(i);
72✔
1504
        auto j = client_files.find(client_file_ident);
72✔
1505
        ClientFile* client_file = (j == client_files.end() ? nullptr : &j->second);
72✔
1506
        version_type last_integrated_client_version = 0;
72✔
1507
        if (client_file)
72✔
1508
            last_integrated_client_version = client_file->last_integrated_client_version;
28✔
1509
        auto ident_salt = m_acc->cf_ident_salts.get(i);
72✔
1510
        auto client_version = m_acc->cf_client_versions.get(i);
72✔
1511
        auto rh_base_version = m_acc->cf_rh_base_versions.get(i);
72✔
1512
        auto recip_hist_ref = m_acc->cf_recip_hist_refs.get(i);
72✔
1513
        auto proxy_file = m_acc->cf_proxy_files.get(i);
72✔
1514
        auto client_type = m_acc->cf_client_types.get(i);
72✔
1515
        auto last_seen_timestamp = m_acc->cf_last_seen_timestamps.get(i);
72✔
1516
        auto locked_server_version = m_acc->cf_locked_server_versions.get(i);
72✔
1517
        version_type client_version_2 = 0;
72✔
1518
        REALM_ASSERT(!util::int_cast_with_overflow_detect(client_version, client_version_2));
72✔
1519
        file_ident_type proxy_file_2 = 0;
72✔
1520
        REALM_ASSERT(!util::int_cast_with_overflow_detect(proxy_file, proxy_file_2));
72✔
1521
        version_type locked_server_version_2 = 0;
72✔
1522
        REALM_ASSERT(!util::int_cast_with_overflow_detect(locked_server_version, locked_server_version_2));
72✔
1523
        if (client_file_ident == 0) {
72✔
1524
            // Special entry
1525
            REALM_ASSERT(ident_salt == 0);
16✔
1526
            REALM_ASSERT(proxy_file_2 == 0);
16✔
1527
            REALM_ASSERT(client_type == 0);
16✔
1528
            REALM_ASSERT(last_seen_timestamp == 0);
16✔
1529
            REALM_ASSERT(locked_server_version_2 == 0);
16✔
1530
            // Upstream server
1531
            REALM_ASSERT(client_version_2 >= last_integrated_client_version);
16✔
1532
        }
16✔
1533
        else if (client_file_ident == g_root_node_file_ident) {
56✔
1534
            // Root node's entry
1535
            REALM_ASSERT(ident_salt == 0);
16✔
1536
            REALM_ASSERT(client_version_2 == 0);
16✔
1537
            REALM_ASSERT(rh_base_version == 0);
16✔
1538
            REALM_ASSERT(recip_hist_ref == 0);
16✔
1539
            REALM_ASSERT(proxy_file_2 == 0);
16✔
1540
            REALM_ASSERT(client_type == 0);
16✔
1541
            REALM_ASSERT(last_seen_timestamp == 0);
16✔
1542
            REALM_ASSERT(locked_server_version_2 == 0);
16✔
1543
            REALM_ASSERT(!client_file);
16✔
1544
            if (m_local_file_ident == g_root_node_file_ident)
16✔
1545
                found_self = true;
16✔
1546
        }
16✔
1547
        else if (client_file_ident == m_local_file_ident) {
40✔
1548
            // Entry representing the Realm file itself
1549
            REALM_ASSERT(ident_salt == 0);
×
1550
            REALM_ASSERT(client_version_2 == 0);
×
1551
            REALM_ASSERT(rh_base_version == 0);
×
1552
            REALM_ASSERT(recip_hist_ref == 0);
×
1553
            REALM_ASSERT(proxy_file_2 == 0);
×
1554
            REALM_ASSERT(client_type == int(ClientType::self));
×
1555
            REALM_ASSERT(last_seen_timestamp == 0);
×
1556
            REALM_ASSERT(locked_server_version_2 == 0);
×
1557
            REALM_ASSERT(!client_file);
×
1558
            found_self = true;
×
1559
        }
×
1560
        else if (ident_salt == 0) {
40✔
1561
            if (proxy_file_2 == 0) {
×
1562
                // This entry represents a file reachable via the upstream
1563
                // server.
1564
                REALM_ASSERT(client_version_2 == 0);
×
1565
                REALM_ASSERT(rh_base_version == 0);
×
1566
                REALM_ASSERT(recip_hist_ref == 0);
×
1567
                REALM_ASSERT(client_type == int(ClientType::upstream));
×
1568
                REALM_ASSERT(last_seen_timestamp == 0);
×
1569
                REALM_ASSERT(locked_server_version_2 == 0);
×
1570
                REALM_ASSERT(!client_file);
×
1571
            }
×
1572
            else {
×
1573
                // This entry represents a client of a direct client, such as
1574
                // client of a partial view, or a client of a subserver.
1575
                REALM_ASSERT(client_version_2 == 0);
×
1576
                REALM_ASSERT(rh_base_version == 0);
×
1577
                REALM_ASSERT(recip_hist_ref == 0);
×
1578
                REALM_ASSERT(client_type == int(ClientType::indirect));
×
1579
                REALM_ASSERT(last_seen_timestamp == 0);
×
1580
                REALM_ASSERT(locked_server_version_2 == 0);
×
1581
                REALM_ASSERT(is_valid_proxy_file_ident(proxy_file_2));
×
1582
                REALM_ASSERT(!client_file);
×
1583
            }
×
1584
        }
×
1585
        else {
40✔
1586
            // This entry represents a direct client, which can be a regular
1587
            // client, a subserver, or a partial view.
1588
            bool expired = (last_seen_timestamp == 0);
40✔
1589
            REALM_ASSERT(ident_salt > 0 && ident_salt <= 0x0'7FFF'FFFF'FFFF'FFFF);
40✔
1590
            REALM_ASSERT(client_version_2 >= last_integrated_client_version);
40✔
1591
            REALM_ASSERT(!expired || (recip_hist_ref == 0));
40!
1592
            REALM_ASSERT(proxy_file_2 == 0);
40✔
1593
            REALM_ASSERT(is_direct_client(ClientType(client_type)));
40✔
1594
            REALM_ASSERT(locked_server_version_2 <= current_server_version);
40✔
1595
        }
40✔
1596
    }
72✔
1597
    REALM_ASSERT(found_self);
16✔
1598

1599
    REALM_ASSERT(m_ct_history_size >= 1); // See comment above
16✔
1600
    REALM_ASSERT(m_acc->ct_history.size() == m_ct_history_size);
16✔
1601
#endif // REALM_DEBUG
16✔
1602
}
16✔
1603

1604
void ServerHistory::discard_accessors() const noexcept
1605
{
1,840✔
1606
    m_acc = util::none;
1,840✔
1607
}
1,840✔
1608

1609

1610
class ServerHistory::DiscardAccessorsGuard {
1611
public:
1612
    DiscardAccessorsGuard(const ServerHistory& sh) noexcept
1613
        : m_server_history{&sh}
858✔
1614
    {
2,176✔
1615
    }
2,176✔
1616
    ~DiscardAccessorsGuard() noexcept
1617
    {
2,176✔
1618
        if (REALM_UNLIKELY(m_server_history))
2,176✔
1619
            m_server_history->discard_accessors();
×
1620
    }
2,176✔
1621
    void release() noexcept
1622
    {
2,176✔
1623
        m_server_history = nullptr;
2,176✔
1624
    }
2,176✔
1625

1626
private:
1627
    const ServerHistory* m_server_history;
1628
};
1629

1630

1631
// Overriding member in _impl::History
1632
void ServerHistory::update_from_ref_and_version(ref_type ref, version_type realm_version)
1633
{
94,654✔
1634
    if (ref == 0) {
94,654✔
1635
        // No history schema yet
1636
        m_local_file_ident = g_root_node_file_ident;
1,840✔
1637
        m_num_client_files = 0;
1,840✔
1638
        m_history_base_version = 0;
1,840✔
1639
        m_history_size = 0;
1,840✔
1640
        m_server_version_salt = 0;
1,840✔
1641
        m_ct_base_version = realm_version;
1,840✔
1642
        m_ct_history_size = 0;
1,840✔
1643
        discard_accessors();
1,840✔
1644
        return;
1,840✔
1645
    }
1,840✔
1646
    if (REALM_LIKELY(m_acc)) {
92,814✔
1647
        m_acc->init_from_ref(ref); // Throws
91,556✔
1648
    }
91,556✔
1649
    else {
1,258✔
1650
        Allocator& alloc = _impl::GroupFriend::get_alloc(*m_group);
1,258✔
1651
        m_acc.emplace(alloc);
1,258✔
1652
        DiscardAccessorsGuard dag{*this};
1,258✔
1653
        m_acc->init_from_ref(ref);
1,258✔
1654
        _impl::GroupFriend::set_history_parent(*m_group, m_acc->root);
1,258✔
1655

1656
        if (m_acc->upstream_status.is_attached()) {
1,258✔
1657
            REALM_ASSERT(m_acc->upstream_status.size() == s_upstream_status_size);
×
1658
        }
×
1659
        if (m_acc->partial_sync.is_attached()) {
1,258✔
1660
            REALM_ASSERT(m_acc->partial_sync.size() == s_partial_sync_size);
×
1661
        }
×
1662
        dag.release();
1,258✔
1663
    }
1,258✔
1664

1665
    if (m_acc->upstream_status.is_attached()) {
92,814✔
1666
        file_ident_type file_ident = m_group->get_sync_file_id();
×
1667
        m_local_file_ident = (file_ident == 0 ? g_root_node_file_ident : file_ident);
×
1668
    }
×
1669
    else {
92,814✔
1670
        m_local_file_ident = g_root_node_file_ident;
92,814✔
1671
    }
92,814✔
1672

1673
    m_num_client_files = m_acc->cf_ident_salts.size();
92,814✔
1674
    REALM_ASSERT(m_acc->cf_client_versions.size() == m_num_client_files);
92,814✔
1675
    REALM_ASSERT(m_acc->cf_rh_base_versions.size() == m_num_client_files);
92,814✔
1676
    REALM_ASSERT(m_acc->cf_recip_hist_refs.size() == m_num_client_files);
92,814✔
1677
    REALM_ASSERT(m_acc->cf_proxy_files.size() == m_num_client_files);
92,814✔
1678
    REALM_ASSERT(m_acc->cf_client_types.size() == m_num_client_files);
92,814✔
1679
    REALM_ASSERT(m_acc->cf_last_seen_timestamps.size() == m_num_client_files);
92,814✔
1680
    REALM_ASSERT(m_acc->cf_locked_server_versions.size() == m_num_client_files);
92,814✔
1681

1682
    m_history_base_version = version_type(m_acc->root.get_as_ref_or_tagged(s_history_base_version_iip).get_as_int());
92,814✔
1683
    m_history_size = m_acc->sh_changesets.size();
92,814✔
1684
    REALM_ASSERT(m_acc->sh_version_salts.size() == m_history_size);
92,814✔
1685
    REALM_ASSERT(m_acc->sh_origin_files.size() == m_history_size);
92,814✔
1686
    REALM_ASSERT(m_acc->sh_client_versions.size() == m_history_size);
92,814✔
1687
    REALM_ASSERT(m_acc->sh_timestamps.size() == m_history_size);
92,814✔
1688
    REALM_ASSERT(m_acc->sh_cumul_byte_sizes.size() == m_history_size);
92,814✔
1689

1690
    m_server_version_salt =
92,814✔
1691
        (m_history_size > 0 ? salt_type(m_acc->sh_version_salts.get(m_history_size - 1))
92,814✔
1692
                            : salt_type(m_acc->root.get_as_ref_or_tagged(s_base_version_salt_iip).get_as_int()));
92,814✔
1693

1694
    m_ct_history_size = m_acc->ct_history.size();
92,814✔
1695
    m_ct_base_version = realm_version - m_ct_history_size;
92,814✔
1696
}
92,814✔
1697

1698

1699
void ServerHistory::Accessors::init_from_ref(ref_type ref)
1700
{
92,820✔
1701
    root.init_from_ref(ref);
92,820✔
1702
    client_files.init_from_parent();
92,820✔
1703
    sync_history.init_from_parent();
92,820✔
1704

1705
    {
92,820✔
1706
        ref_type ref_2 = upstream_status.get_ref_from_parent();
92,820✔
1707
        if (ref_2 != 0) {
92,820✔
1708
            upstream_status.init_from_ref(ref_2);
×
1709
        }
×
1710
        else {
92,820✔
1711
            upstream_status.detach();
92,820✔
1712
        }
92,820✔
1713
    }
92,820✔
1714

1715
    cf_ident_salts.init_from_parent();            // Throws
92,820✔
1716
    cf_client_versions.init_from_parent();        // Throws
92,820✔
1717
    cf_rh_base_versions.init_from_parent();       // Throws
92,820✔
1718
    cf_recip_hist_refs.init_from_parent();        // Throws
92,820✔
1719
    cf_proxy_files.init_from_parent();            // Throws
92,820✔
1720
    cf_client_types.init_from_parent();           // Throws
92,820✔
1721
    cf_last_seen_timestamps.init_from_parent();   // Throws
92,820✔
1722
    cf_locked_server_versions.init_from_parent(); // Throws
92,820✔
1723
    sh_version_salts.init_from_parent();          // Throws
92,820✔
1724
    sh_origin_files.init_from_parent();           // Throws
92,820✔
1725
    sh_client_versions.init_from_parent();        // Throws
92,820✔
1726
    sh_timestamps.init_from_parent();             // Throws
92,820✔
1727
    sh_changesets.init_from_parent();             // Throws
92,820✔
1728
    sh_cumul_byte_sizes.init_from_parent();       // Throws
92,820✔
1729
    ct_history.init_from_parent();                // Throws
92,820✔
1730

1731
    // Note: If anything throws above, then accessors will be left in an
1732
    // undefined state. However, all IntegerBpTree accessors will still have
1733
    // a root array, and all optional BinaryColumn accessors will still
1734
    // exist, so it will be safe to call update_from_ref() again.
1735
}
92,820✔
1736

1737

1738
void ServerHistory::create_empty_history()
1739
{
918✔
1740
    using gf = _impl::GroupFriend;
918✔
1741

1742
    REALM_ASSERT(m_local_file_ident == g_root_node_file_ident);
918✔
1743
    REALM_ASSERT(m_num_client_files == 0);
918✔
1744
    REALM_ASSERT(m_history_base_version == 0);
918✔
1745
    REALM_ASSERT(m_history_size == 0);
918✔
1746
    REALM_ASSERT(m_server_version_salt == 0);
918✔
1747
    REALM_ASSERT(m_ct_history_size == 0);
918✔
1748
    REALM_ASSERT(!m_acc);
918✔
1749
    Allocator& alloc = m_db->get_alloc();
918✔
1750
    m_acc.emplace(alloc);
918✔
1751
    DiscardAccessorsGuard dag{*this};
918✔
1752
    gf::prepare_history_parent(*m_group, m_acc->root, Replication::hist_SyncServer,
918✔
1753
                               get_server_history_schema_version(), m_local_file_ident); // Throws
918✔
1754
    m_acc->create();                                                                     // Throws
918✔
1755
    dag.release();
918✔
1756

1757
    // Add the special client file entry (index = 0), and the root servers entry
1758
    // (index = 1).
1759
    static_assert(g_root_node_file_ident == 1, "");
918✔
1760
    REALM_ASSERT(m_num_client_files == 0);
918✔
1761
    for (int i = 0; i < 2; ++i) {
2,754✔
1762
        m_acc->cf_ident_salts.insert(realm::npos, 0);            // Throws
1,836✔
1763
        m_acc->cf_client_versions.insert(realm::npos, 0);        // Throws
1,836✔
1764
        m_acc->cf_rh_base_versions.insert(realm::npos, 0);       // Throws
1,836✔
1765
        m_acc->cf_recip_hist_refs.insert(realm::npos, 0);        // Throws
1,836✔
1766
        m_acc->cf_proxy_files.insert(realm::npos, 0);            // Throws
1,836✔
1767
        m_acc->cf_client_types.insert(realm::npos, 0);           // Throws
1,836✔
1768
        m_acc->cf_last_seen_timestamps.insert(realm::npos, 0);   // Throws
1,836✔
1769
        m_acc->cf_locked_server_versions.insert(realm::npos, 0); // Throws
1,836✔
1770
        ++m_num_client_files;
1,836✔
1771
    }
1,836✔
1772
}
918✔
1773

1774
void ServerHistory::Accessors::create()
1775
{
918✔
1776
    // Note: `Array::create()` does *NOT* call `Node::update_parent()`, while
1777
    // `BPlusTree<T>::create()` *DOES* update its parent in an exception-safe
1778
    // way. This means that we need destruction guards for arrays, but not
1779
    // BPlusTrees/BinaryColumns.
1780

1781
    // Note: The arrays `upstream_status` and `partial_sync` are created
1782
    // on-demand instead of here.
1783

1784
    bool context_flag_no = false;
918✔
1785
    root.create(Array::type_HasRefs, context_flag_no, s_root_size); // Throws
918✔
1786
    _impl::DeepArrayDestroyGuard destroy_guard(&root);
918✔
1787

1788
    client_files.create(Array::type_HasRefs, context_flag_no, s_client_files_size); // Throws
918✔
1789
    client_files.update_parent();                                                   // Throws
918✔
1790

1791
    sync_history.create(Array::type_HasRefs, context_flag_no, s_sync_history_size); // Throws
918✔
1792
    sync_history.update_parent();                                                   // Throws
918✔
1793

1794
    schema_versions.create(Array::type_HasRefs, context_flag_no, s_schema_versions_size); // Throws
918✔
1795
    schema_versions.update_parent();
918✔
1796
    Allocator& alloc = schema_versions.get_alloc();
918✔
1797
    for (int i = 0; i < s_schema_versions_size; i++) {
4,590✔
1798
        MemRef mem = Array::create_empty_array(Array::type_Normal, context_flag_no, alloc);
3,672✔
1799
        ref_type ref = mem.get_ref();
3,672✔
1800
        schema_versions.set_as_ref(i, ref);
3,672✔
1801
    }
3,672✔
1802

1803
    cf_ident_salts.create();            // Throws
918✔
1804
    cf_client_versions.create();        // Throws
918✔
1805
    cf_rh_base_versions.create();       // Throws
918✔
1806
    cf_recip_hist_refs.create();        // Throws
918✔
1807
    cf_proxy_files.create();            // Throws
918✔
1808
    cf_client_types.create();           // Throws
918✔
1809
    cf_last_seen_timestamps.create();   // Throws
918✔
1810
    cf_locked_server_versions.create(); // Throws
918✔
1811

1812
    sh_version_salts.create();    // Throws
918✔
1813
    sh_origin_files.create();     // Throws
918✔
1814
    sh_client_versions.create();  // Throws
918✔
1815
    sh_timestamps.create();       // Throws
918✔
1816
    sh_changesets.create();       // Throws
918✔
1817
    sh_cumul_byte_sizes.create(); // Throws
918✔
1818

1819
    ct_history.create(); // Throws
918✔
1820

1821
    destroy_guard.release();
918✔
1822
    root.update_parent(); // Throws
918✔
1823
}
918✔
1824

1825

1826
auto ServerHistory::get_server_version_salt(version_type server_version) const noexcept -> salt_type
1827
{
4,228✔
1828
    REALM_ASSERT(server_version >= m_history_base_version);
4,228✔
1829
    if (server_version == m_history_base_version)
4,228✔
1830
        return salt_type(m_acc->root.get(s_base_version_salt_iip));
1,408✔
1831
    std::size_t history_entry_index = to_size_t(server_version - m_history_base_version) - 1;
2,820✔
1832
    REALM_ASSERT(history_entry_index < m_history_size);
2,820✔
1833
    return salt_type(m_acc->sh_version_salts.get(history_entry_index));
2,820✔
1834
}
4,228✔
1835

1836

1837
bool ServerHistory::is_valid_proxy_file_ident(file_ident_type file_ident) const noexcept
1838
{
×
1839
    static_assert(g_root_node_file_ident == 1, "");
×
1840
    REALM_ASSERT(file_ident >= 2);
×
1841
    REALM_ASSERT(std::uint_fast64_t(file_ident) < m_num_client_files);
×
1842
    std::size_t i = std::size_t(file_ident);
×
1843
    auto client_type = m_acc->cf_client_types.get(i);
×
1844
    return is_direct_client(ClientType(client_type));
×
1845
}
×
1846

1847

1848
void ServerHistory::add_core_history_entry(BinaryData changeset)
1849
{
41,938✔
1850
    REALM_ASSERT(m_acc->ct_history.size() == m_ct_history_size);
41,938✔
1851

1852
    if (changeset.is_null())
41,938✔
1853
        changeset = BinaryData("", 0);
1,210✔
1854

1855
    m_acc->ct_history.add(changeset); // Throws
41,938✔
1856
    ++m_ct_history_size;
41,938✔
1857
}
41,938✔
1858

1859

1860
void ServerHistory::add_sync_history_entry(const HistoryEntry& entry)
1861
{
39,276✔
1862
    REALM_ASSERT(m_acc->sh_version_salts.size() == m_history_size);
39,276✔
1863
    REALM_ASSERT(m_acc->sh_origin_files.size() == m_history_size);
39,276✔
1864
    REALM_ASSERT(m_acc->sh_client_versions.size() == m_history_size);
39,276✔
1865
    REALM_ASSERT(m_acc->sh_timestamps.size() == m_history_size);
39,276✔
1866
    REALM_ASSERT(m_acc->sh_changesets.size() == m_history_size);
39,276✔
1867
    REALM_ASSERT(m_acc->sh_cumul_byte_sizes.size() == m_history_size);
39,276✔
1868

1869
    std::int_fast64_t client_file = std::int_fast64_t(entry.origin_file_ident);
39,276✔
1870
    std::int_fast64_t client_version = std::int_fast64_t(entry.remote_version);
39,276✔
1871
    std::int_fast64_t timestamp = std::int_fast64_t(entry.origin_timestamp);
39,276✔
1872

1873
    // FIXME: BinaryColumn::set() currently interprets BinaryData(0,0) as
1874
    // null. It should probably be changed such that BinaryData(0,0) is
1875
    // always interpreted as the empty string. For the purpose of setting
1876
    // null values, BinaryColumn::set() should accept values of type
1877
    // Optional<BinaryData>().
1878
    BinaryData changeset("", 0);
39,276✔
1879
    if (!entry.changeset.is_null())
39,276✔
1880
        changeset = entry.changeset.get_first_chunk();
38,810✔
1881

1882
    m_acc->sh_version_salts.insert(realm::npos, m_salt_for_new_server_versions); // Throws
39,276✔
1883
    m_acc->sh_origin_files.insert(realm::npos, client_file);                     // Throws
39,276✔
1884
    m_acc->sh_client_versions.insert(realm::npos, client_version);               // Throws
39,276✔
1885
    m_acc->sh_timestamps.insert(realm::npos, timestamp);                         // Throws
39,276✔
1886
    m_acc->sh_changesets.add(changeset);                                         // Throws
39,276✔
1887

1888
    // Update the cumulative byte size.
1889
    std::int_fast64_t previous_history_byte_size =
39,276✔
1890
        (m_history_size == 0 ? 0 : m_acc->sh_cumul_byte_sizes.get(m_history_size - 1));
39,276✔
1891
    std::int_fast64_t history_byte_size = previous_history_byte_size + changeset.size();
39,276✔
1892
    m_acc->sh_cumul_byte_sizes.insert(realm::npos, history_byte_size);
39,276✔
1893

1894
    ++m_history_size;
39,276✔
1895
    m_server_version_salt = m_salt_for_new_server_versions;
39,276✔
1896
}
39,276✔
1897

1898

1899
void ServerHistory::trim_cont_transact_history()
1900
{
41,936✔
1901
    REALM_ASSERT(m_acc->ct_history.size() == m_ct_history_size);
41,936✔
1902

1903
    // `m_version_of_oldest_bound_snapshot` is not updated by transactions
1904
    // occuring through other DB objects than the one associated with
1905
    // this history object. For that reason, it can sometimes happen that it
1906
    // precedes the beginning of the history, even though it seems
1907
    // nonsensical. It would happen if the history was already trimmed via one
1908
    // of the other DB objects. In such a case, no trimming can be done
1909
    // yet.
1910
    if (m_version_of_oldest_bound_snapshot > m_ct_base_version) {
41,936✔
1911
        std::size_t num_entries_to_erase = std::size_t(m_version_of_oldest_bound_snapshot - m_ct_base_version);
41,018✔
1912
        // The new changeset is always added before
1913
        // set_oldest_bound_version() is called. Therefore, the trimming
1914
        // operation can never leave the history empty.
1915
        REALM_ASSERT(num_entries_to_erase < m_ct_history_size);
41,018✔
1916
        for (std::size_t i = 0; i < num_entries_to_erase; ++i) {
82,036✔
1917
            std::size_t j = num_entries_to_erase - i - 1;
41,018✔
1918
            m_acc->ct_history.erase(j);
41,018✔
1919
        }
41,018✔
1920
        m_ct_base_version += num_entries_to_erase;
41,018✔
1921
        m_ct_history_size -= num_entries_to_erase;
41,018✔
1922
    }
41,018✔
1923
}
41,936✔
1924

1925

1926
ChunkedBinaryData ServerHistory::get_changeset(version_type server_version) const noexcept
1927
{
×
1928
    REALM_ASSERT(server_version > m_history_base_version && server_version <= get_server_version());
×
1929
    std::size_t history_entry_ndx = to_size_t(server_version - m_history_base_version) - 1;
×
1930
    return ChunkedBinaryData(m_acc->sh_changesets, history_entry_ndx);
×
1931
}
×
1932

1933

1934
// Skips history entries with empty changesets, and history entries produced by
1935
// integration of changes received from the specified remote file.
1936
//
1937
// Pass zero for `remote_file_ident` if the remote file is on the upstream
1938
// server, or the reference file.
1939
//
1940
// Returns zero if no history entry was found. Otherwise it returns the version
1941
// produced by the changeset of the located history entry.
1942
auto ServerHistory::find_history_entry(file_ident_type remote_file_ident, version_type begin_version,
1943
                                       version_type end_version, HistoryEntry& entry,
1944
                                       version_type& last_integrated_remote_version) const noexcept -> version_type
1945
{
308,096✔
1946
    REALM_ASSERT(remote_file_ident != g_root_node_file_ident);
308,096✔
1947
    REALM_ASSERT(begin_version >= m_history_base_version);
308,096✔
1948
    REALM_ASSERT(begin_version <= end_version);
308,096✔
1949
    auto server_version = begin_version;
308,096✔
1950
    while (server_version < end_version) {
436,802✔
1951
        ++server_version;
367,830✔
1952
        // FIXME: Find a way to avoid dynamically allocating a buffer for, and
1953
        // copying the changeset for all the skipped history entries.
1954
        HistoryEntry entry_2 = get_history_entry(server_version);
367,830✔
1955
        bool received_from_client = received_from(entry_2, remote_file_ident);
367,830✔
1956
        if (received_from_client) {
367,830✔
1957
            last_integrated_remote_version = entry_2.remote_version;
101,048✔
1958
            continue;
101,048✔
1959
        }
101,048✔
1960
        if (entry_2.changeset.size() == 0)
266,782✔
1961
            continue; // Empty
27,658✔
1962
        // These changes were not received from the specified client, and the
1963
        // changeset was not empty.
1964
        entry = entry_2;
239,124✔
1965
        return server_version;
239,124✔
1966
    }
266,782✔
1967
    return 0;
68,972✔
1968
}
308,096✔
1969

1970

1971
auto ServerHistory::get_history_entry(version_type server_version) const noexcept -> HistoryEntry
1972
{
442,092✔
1973
    REALM_ASSERT(server_version > m_history_base_version && server_version <= get_server_version());
442,092✔
1974
    std::size_t history_entry_ndx = to_size_t(server_version - m_history_base_version) - 1;
442,092✔
1975
    auto origin_file = m_acc->sh_origin_files.get(history_entry_ndx);
442,092✔
1976
    auto client_version = m_acc->sh_client_versions.get(history_entry_ndx);
442,092✔
1977
    auto timestamp = m_acc->sh_timestamps.get(history_entry_ndx);
442,092✔
1978
    ChunkedBinaryData chunked_changeset(m_acc->sh_changesets, history_entry_ndx);
442,092✔
1979
    HistoryEntry entry;
442,092✔
1980
    entry.origin_file_ident = file_ident_type(origin_file);
442,092✔
1981
    entry.remote_version = version_type(client_version);
442,092✔
1982
    entry.origin_timestamp = timestamp_type(timestamp);
442,092✔
1983
    entry.changeset = chunked_changeset;
442,092✔
1984
    return entry;
442,092✔
1985
}
442,092✔
1986

1987

1988
// Returns true if, and only if the specified history entry was produced by
1989
// integratrion of a changeset that was received from the specified remote
1990
// file. Use `remote_file_ident = 0` to specify the upstream server when on a
1991
// subtier node of a star topology server cluster, or to specify the reference
1992
// file when in a partial view.
1993
bool ServerHistory::received_from(const HistoryEntry& entry, file_ident_type remote_file_ident) const noexcept
1994
{
367,886✔
1995
    file_ident_type origin_file_ident = entry.origin_file_ident;
367,886✔
1996
    std::size_t origin_file_index = std::size_t(origin_file_ident);
367,886✔
1997
    bool from_upstream_server = (remote_file_ident == 0);
367,886✔
1998
    if (!from_upstream_server) {
367,886✔
1999
        std::size_t remote_file_index = std::size_t(remote_file_ident);
367,880✔
2000
        REALM_ASSERT(is_direct_client(ClientType(m_acc->cf_client_types.get(remote_file_index))));
367,880✔
2001
        if (origin_file_ident == remote_file_ident)
367,880✔
2002
            return true;
101,050✔
2003
        file_ident_type proxy_file = file_ident_type(m_acc->cf_proxy_files.get(origin_file_index));
266,830✔
2004
        return (proxy_file == remote_file_ident);
266,830✔
2005
    }
367,880✔
2006
    bool of_local_origin = (origin_file_ident == 0);
6✔
2007
    if (of_local_origin)
6✔
2008
        return false;
×
2009
    ClientType client_type = ClientType(m_acc->cf_client_types.get(origin_file_index));
6✔
2010
    return (client_type == ClientType::upstream);
6✔
2011
}
6✔
2012

2013

2014
auto ServerHistory::get_history_contents() const -> HistoryContents
2015
{
×
2016
    HistoryContents hc;
×
2017

2018
    TransactionRef tr = m_db->start_read(); // Throws
×
2019
    version_type realm_version = tr->get_version();
×
2020
    const_cast<ServerHistory*>(this)->set_group(tr.get());
×
2021
    ensure_updated(realm_version); // Throws
×
2022

2023
    util::AppendBuffer<char> buffer;
×
2024
    hc.client_files = {};
×
2025
    for (std::size_t i = 0; i < m_num_client_files; ++i) {
×
2026
        HistoryContents::ClientFile cf;
×
2027
        cf.ident_salt = m_acc->cf_ident_salts.get(i);
×
2028
        cf.client_version = m_acc->cf_client_versions.get(i);
×
2029
        cf.rh_base_version = m_acc->cf_rh_base_versions.get(i);
×
2030
        cf.proxy_file = m_acc->cf_proxy_files.get(i);
×
2031
        cf.client_type = m_acc->cf_client_types.get(i);
×
2032
        cf.locked_server_version = m_acc->cf_locked_server_versions.get(i);
×
2033
        cf.reciprocal_history = {};
×
2034
        version_type recip_hist_base_version = version_type(cf.rh_base_version);
×
2035
        ReciprocalHistory recip_hist(m_acc->cf_recip_hist_refs, i, recip_hist_base_version); // Throws
×
2036
        std::size_t recip_hist_size = recip_hist.size();
×
2037
        for (std::size_t j = 0; j < recip_hist_size; ++j) {
×
2038
            version_type version = recip_hist_base_version + i + 1;
×
2039
            ChunkedBinaryData transform;
×
2040
            if (recip_hist.get(version, transform)) {
×
2041
                transform.copy_to(buffer);
×
2042
                cf.reciprocal_history.push_back(std::string{buffer.data(), buffer.size()});
×
2043
            }
×
2044
            else {
×
2045
                cf.reciprocal_history.push_back(util::none);
×
2046
            }
×
2047
        }
×
2048
        hc.client_files.push_back(cf);
×
2049
    }
×
2050

2051
    hc.history_base_version = m_acc->root.get_as_ref_or_tagged(s_history_base_version_iip).get_as_int();
×
2052
    hc.base_version_salt = m_acc->root.get_as_ref_or_tagged(s_base_version_salt_iip).get_as_int();
×
2053

2054
    hc.sync_history = {};
×
2055
    for (size_t i = 0; i < m_history_size; ++i) {
×
2056
        HistoryContents::HistoryEntry he;
×
2057
        he.version_salt = m_acc->sh_version_salts.get(i);
×
2058
        he.client_file_ident = m_acc->sh_origin_files.get(i);
×
2059
        he.client_version = m_acc->sh_client_versions.get(i);
×
2060
        he.timestamp = m_acc->sh_timestamps.get(i);
×
2061
        he.cumul_byte_size = m_acc->sh_cumul_byte_sizes.get(i);
×
2062
        ChunkedBinaryData chunked_changeset(m_acc->sh_changesets, i);
×
2063
        chunked_changeset.copy_to(buffer);
×
2064
        he.changeset = std::string(buffer.data(), buffer.size());
×
2065
        hc.sync_history.push_back(he);
×
2066
    }
×
2067

2068
    hc.servers_client_file_ident = m_local_file_ident;
×
2069

2070
    return hc;
×
2071
}
×
2072

2073

2074
void ServerHistory::fixup_state_and_changesets_for_assigned_file_ident(Transaction& group, file_ident_type file_ident)
2075
{
×
2076
    // Must be in write transaction!
2077

2078
    REALM_ASSERT(file_ident != 0);
×
2079
    REALM_ASSERT(file_ident != g_root_node_file_ident);
×
2080
    REALM_ASSERT(m_acc->upstream_status.is_attached());
×
2081
    REALM_ASSERT(m_local_file_ident == g_root_node_file_ident);
×
2082
    using Instruction = realm::sync::Instruction;
×
2083

2084
    auto promote_global_key = [&](GlobalKey& oid) {
×
2085
        REALM_ASSERT(oid.hi() == 0); // client_file_ident == 0
×
2086
        oid = GlobalKey{uint64_t(file_ident), oid.lo()};
×
2087
    };
×
2088

2089
    auto promote_primary_key = [&](Instruction::PrimaryKey& pk) {
×
2090
        mpark::visit(overload{[&](GlobalKey& key) {
×
2091
                                  promote_global_key(key);
×
2092
                              },
×
2093
                              [](auto&&) {}},
×
2094
                     pk);
×
2095
    };
×
2096

2097
    auto get_table_for_class = [&](StringData class_name) -> ConstTableRef {
×
2098
        Group::TableNameBuffer buffer;
×
2099
        return group.get_table(Group::class_name_to_table_name(class_name, buffer));
×
2100
    };
×
2101

2102
    // Fix up changesets in history. We know that all of these are of our own
2103
    // creation.
2104
    for (std::size_t i = 0; i < m_acc->sh_changesets.size(); ++i) {
×
2105
        ChunkedBinaryData changeset{m_acc->sh_changesets, i};
×
2106
        ChunkedBinaryInputStream in{changeset};
×
2107
        Changeset log;
×
2108
        parse_changeset(in, log);
×
2109

2110
        auto last_class_name = sync::InternString::npos;
×
2111
        ConstTableRef selected_table;
×
2112
        for (auto instr : log) {
×
2113
            if (!instr)
×
2114
                continue;
×
2115

2116
            if (auto obj_instr = instr->get_if<Instruction::ObjectInstruction>()) {
×
2117
                // Cache the TableRef
2118
                if (obj_instr->table != last_class_name) {
×
2119
                    StringData class_name = log.get_string(obj_instr->table);
×
2120
                    last_class_name = obj_instr->table;
×
2121
                    selected_table = get_table_for_class(class_name);
×
2122
                }
×
2123

2124
                // Fix up instructions using GlobalKey to identify objects.
2125
                promote_primary_key(obj_instr->object);
×
2126

2127
                // Fix up the payload for Set and ArrayInsert.
2128
                Instruction::Payload* payload = nullptr;
×
2129
                if (auto set_instr = instr->get_if<Instruction::Update>()) {
×
2130
                    payload = &set_instr->value;
×
2131
                }
×
2132
                else if (auto list_insert_instr = instr->get_if<Instruction::ArrayInsert>()) {
×
2133
                    payload = &list_insert_instr->value;
×
2134
                }
×
2135

2136
                if (payload && payload->type == Instruction::Payload::Type::Link) {
×
2137
                    promote_primary_key(payload->data.link.target);
×
2138
                }
×
2139
            }
×
2140
        }
×
2141

2142
        ChangesetEncoder::Buffer modified;
×
2143
        encode_changeset(log, modified);
×
2144
        BinaryData result = BinaryData{modified.data(), modified.size()};
×
2145
        m_acc->sh_changesets.set(i, result);
×
2146
    }
×
2147
}
×
2148

2149
void ServerHistory::record_current_schema_version()
2150
{
×
2151
    using gf = _impl::GroupFriend;
×
2152
    Allocator& alloc = gf::get_alloc(*m_group);
×
2153
    auto ref = gf::get_history_ref(*m_group);
×
2154
    REALM_ASSERT(ref != 0);
×
2155
    Array root{alloc};
×
2156
    gf::set_history_parent(*m_group, root);
×
2157
    root.init_from_ref(ref);
×
2158
    Array schema_versions{alloc};
×
2159
    schema_versions.set_parent(&root, s_schema_versions_iip);
×
2160
    schema_versions.init_from_parent();
×
2161
    version_type snapshot_version = m_db->get_version_of_latest_snapshot();
×
2162
    record_current_schema_version(schema_versions, snapshot_version); // Throws
×
2163
}
×
2164

2165

2166
void ServerHistory::record_current_schema_version(Array& schema_versions, version_type snapshot_version)
2167
{
×
2168
    static_assert(s_schema_versions_size == 4, "");
×
2169
    REALM_ASSERT(schema_versions.size() == s_schema_versions_size);
×
2170

2171
    Allocator& alloc = schema_versions.get_alloc();
×
2172
    {
×
2173
        Array sv_schema_versions{alloc};
×
2174
        sv_schema_versions.set_parent(&schema_versions, s_sv_schema_versions_iip);
×
2175
        sv_schema_versions.init_from_parent();
×
2176
        int schema_version = get_server_history_schema_version();
×
2177
        sv_schema_versions.add(schema_version); // Throws
×
2178
    }
×
2179
    {
×
2180
        Array sv_library_versions{alloc};
×
2181
        sv_library_versions.set_parent(&schema_versions, s_sv_library_versions_iip);
×
2182
        sv_library_versions.init_from_parent();
×
2183
        const char* library_version = REALM_VERSION_STRING;
×
2184
        std::size_t size = std::strlen(library_version);
×
2185
        Array value{alloc};
×
2186
        bool context_flag = false;
×
2187
        value.create(Array::type_Normal, context_flag, size); // Throws
×
2188
        _impl::ShallowArrayDestroyGuard adg{&value};
×
2189
        using uchar = unsigned char;
×
2190
        for (std::size_t i = 0; i < size; ++i)
×
2191
            value.set(i, std::int_fast64_t(uchar(library_version[i]))); // Throws
×
2192
        sv_library_versions.add(std::int_fast64_t(value.get_ref()));    // Throws
×
2193
        adg.release();                                                  // Ownership transferred to parent array
×
2194
    }
×
2195
    {
×
2196
        Array sv_snapshot_versions{alloc};
×
2197
        sv_snapshot_versions.set_parent(&schema_versions, s_sv_snapshot_versions_iip);
×
2198
        sv_snapshot_versions.init_from_parent();
×
2199
        sv_snapshot_versions.add(std::int_fast64_t(snapshot_version)); // Throws
×
2200
    }
×
2201
    {
×
2202
        Array sv_timestamps{alloc};
×
2203
        sv_timestamps.set_parent(&schema_versions, s_sv_timestamps_iip);
×
2204
        sv_timestamps.init_from_parent();
×
2205
        std::time_t timestamp = std::time(nullptr);
×
2206
        sv_timestamps.add(std::int_fast64_t(timestamp)); // Throws
×
2207
    }
×
2208
}
×
2209

2210

2211
std::ostream& _impl::operator<<(std::ostream& out, const ServerHistory::HistoryContents& hc)
2212
{
×
2213
    out << "client files:\n";
×
2214
    for (std::size_t i = 0; i < hc.client_files.size(); ++i) {
×
2215
        out << "\n";
×
2216
        out << "  client_file_ident = " << i << "\n";
×
2217
        out << "  ident_salt = " << hc.client_files[i].ident_salt << "\n";
×
2218
        out << "  client_version = " << hc.client_files[i].client_version << "\n";
×
2219
        out << "  rh_base_version = " << hc.client_files[i].rh_base_version << "\n";
×
2220
        out << "  proxy_file = " << hc.client_files[i].proxy_file << "\n";
×
2221
        out << "  client_type = " << hc.client_files[i].client_type << "\n";
×
2222
        out << "  locked_server_version = " << hc.client_files[i].locked_server_version << "\n";
×
2223
        out << "  reciprocal history:\n";
×
2224
        for (const util::Optional<std::string>& transform : hc.client_files[i].reciprocal_history) {
×
2225
            if (transform) {
×
2226
                out << "    " << util::hex_dump((*transform).data(), (*transform).size()) << "\n";
×
2227
            }
×
2228
            else {
×
2229
                out << "    NULL\n";
×
2230
            }
×
2231
        }
×
2232
        out << "\n";
×
2233
    }
×
2234
    out << "\n";
×
2235

2236
    out << "history_base_version = " << hc.history_base_version << "\n";
×
2237
    out << "base_version_salt = " << hc.base_version_salt << "\n";
×
2238
    out << "\n";
×
2239

2240
    out << "history entries:\n";
×
2241
    for (std::size_t i = 0; i < hc.sync_history.size(); ++i) {
×
2242
        out << "\n";
×
2243
        out << "  version_salt = " << hc.sync_history[i].version_salt << "\n";
×
2244
        out << "  client_file_ident = " << hc.sync_history[i].client_file_ident << "\n";
×
2245
        out << "  client_version = " << hc.sync_history[i].client_version << "\n";
×
2246
        out << "  timestamp = " << hc.sync_history[i].timestamp << "\n";
×
2247
        out << "  cumul_byte_size = " << hc.sync_history[i].cumul_byte_size << "\n";
×
2248
        const std::string& changeset = hc.sync_history[i].changeset;
×
2249
        out << "  changeset = " << util::hex_dump(changeset.data(), changeset.size()) << "\n";
×
2250
        out << "\n";
×
2251
    }
×
2252
    out << "\n";
×
2253

2254
    out << "servers_client_file_ident = " << hc.servers_client_file_ident << "\n";
×
2255

2256
    return out;
×
2257
}
×
2258

2259
bool _impl::operator==(const ServerHistory::HistoryContents& hc_1, const ServerHistory::HistoryContents& hc_2)
2260
{
×
2261
    if (hc_1.client_files.size() != hc_2.client_files.size())
×
2262
        return false;
×
2263

2264
    for (std::size_t i = 0; i < hc_1.client_files.size(); ++i) {
×
2265
        ServerHistory::HistoryContents::ClientFile cf_1 = hc_1.client_files[i];
×
2266
        ServerHistory::HistoryContents::ClientFile cf_2 = hc_2.client_files[i];
×
2267

2268
        bool partially_equal =
×
2269
            (cf_1.ident_salt == cf_2.ident_salt && cf_1.client_version == cf_2.client_version &&
×
2270
             cf_1.rh_base_version == cf_2.rh_base_version && cf_1.proxy_file == cf_2.proxy_file &&
×
2271
             cf_1.client_type == cf_2.client_type && cf_1.locked_server_version == cf_2.locked_server_version &&
×
2272
             cf_1.reciprocal_history.size() == cf_2.reciprocal_history.size());
×
2273
        if (!partially_equal)
×
2274
            return false;
×
2275

2276
        for (std::size_t j = 0; j < cf_1.reciprocal_history.size(); ++j) {
×
2277
            if (cf_1.reciprocal_history[j] != cf_2.reciprocal_history[j])
×
2278
                return false;
×
2279
        }
×
2280
    }
×
2281

2282
    bool same_base_version =
×
2283
        (hc_1.history_base_version == hc_2.history_base_version && hc_1.base_version_salt == hc_2.base_version_salt);
×
2284
    if (!same_base_version)
×
2285
        return false;
×
2286

2287
    if (hc_1.sync_history.size() != hc_2.sync_history.size())
×
2288
        return false;
×
2289

2290
    for (std::size_t i = 0; i < hc_1.sync_history.size(); ++i) {
×
2291
        ServerHistory::HistoryContents::HistoryEntry sh_1 = hc_1.sync_history[i];
×
2292
        ServerHistory::HistoryContents::HistoryEntry sh_2 = hc_2.sync_history[i];
×
2293
        bool equal = (sh_1.version_salt == sh_2.version_salt && sh_1.client_file_ident == sh_2.client_file_ident &&
×
2294
                      sh_1.client_version == sh_2.client_version && sh_1.timestamp == sh_2.timestamp &&
×
2295
                      sh_1.cumul_byte_size == sh_2.cumul_byte_size);
×
2296
        if (!equal)
×
2297
            return false;
×
2298
    }
×
2299

2300
    if (hc_1.servers_client_file_ident != hc_2.servers_client_file_ident)
×
2301
        return false;
×
2302

2303
    return true;
×
2304
}
×
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