Compare commits

...

34 Commits

Author SHA1 Message Date
github-actions[bot]
a4d6caf8a7 style: Update pre-commit hooks (#2968)
Co-authored-by: mathbunnyru <12270691+mathbunnyru@users.noreply.github.com>
2026-03-03 15:58:30 +00:00
dependabot[bot]
b9ee5187b6 ci: [DEPENDABOT] Bump actions/upload-artifact from 6.0.0 to 7.0.0 (#2969)
Bumps
[actions/upload-artifact](https://github.com/actions/upload-artifact)
from 6.0.0 to 7.0.0.
<details>
<summary>Release notes</summary>
<p><em>Sourced from <a
href="https://github.com/actions/upload-artifact/releases">actions/upload-artifact's
releases</a>.</em></p>
<blockquote>
<h2>v7.0.0</h2>
<h2>v7 What's new</h2>
<h3>Direct Uploads</h3>
<p>Adds support for uploading single files directly (unzipped). Callers
can set the new <code>archive</code> parameter to <code>false</code> to
skip zipping the file during upload. Right now, we only support single
files. The action will fail if the glob passed resolves to multiple
files. The <code>name</code> parameter is also ignored with this
setting. Instead, the name of the artifact will be the name of the
uploaded file.</p>
<h3>ESM</h3>
<p>To support new versions of the <code>@actions/*</code> packages,
we've upgraded the package to ESM.</p>
<h2>What's Changed</h2>
<ul>
<li>Add proxy integration test by <a
href="https://github.com/Link"><code>@​Link</code></a>- in <a
href="https://redirect.github.com/actions/upload-artifact/pull/754">actions/upload-artifact#754</a></li>
<li>Upgrade the module to ESM and bump dependencies by <a
href="https://github.com/danwkennedy"><code>@​danwkennedy</code></a> in
<a
href="https://redirect.github.com/actions/upload-artifact/pull/762">actions/upload-artifact#762</a></li>
<li>Support direct file uploads by <a
href="https://github.com/danwkennedy"><code>@​danwkennedy</code></a> in
<a
href="https://redirect.github.com/actions/upload-artifact/pull/764">actions/upload-artifact#764</a></li>
</ul>
<h2>New Contributors</h2>
<ul>
<li><a href="https://github.com/Link"><code>@​Link</code></a>- made
their first contribution in <a
href="https://redirect.github.com/actions/upload-artifact/pull/754">actions/upload-artifact#754</a></li>
</ul>
<p><strong>Full Changelog</strong>: <a
href="https://github.com/actions/upload-artifact/compare/v6...v7.0.0">https://github.com/actions/upload-artifact/compare/v6...v7.0.0</a></p>
</blockquote>
</details>
<details>
<summary>Commits</summary>
<ul>
<li><a
href="bbbca2ddaa"><code>bbbca2d</code></a>
Support direct file uploads (<a
href="https://redirect.github.com/actions/upload-artifact/issues/764">#764</a>)</li>
<li><a
href="589182c5a4"><code>589182c</code></a>
Upgrade the module to ESM and bump dependencies (<a
href="https://redirect.github.com/actions/upload-artifact/issues/762">#762</a>)</li>
<li><a
href="47309c993a"><code>47309c9</code></a>
Merge pull request <a
href="https://redirect.github.com/actions/upload-artifact/issues/754">#754</a>
from actions/Link-/add-proxy-integration-tests</li>
<li><a
href="02a8460834"><code>02a8460</code></a>
Add proxy integration test</li>
<li>See full diff in <a
href="b7c566a772...bbbca2ddaa">compare
view</a></li>
</ul>
</details>
<br />


[![Dependabot compatibility
score](https://dependabot-badges.githubapp.com/badges/compatibility_score?dependency-name=actions/upload-artifact&package-manager=github_actions&previous-version=6.0.0&new-version=7.0.0)](https://docs.github.com/en/github/managing-security-vulnerabilities/about-dependabot-security-updates#about-compatibility-scores)

Dependabot will resolve any conflicts with this PR as long as you don't
alter it yourself. You can also trigger a rebase manually by commenting
`@dependabot rebase`.

[//]: # (dependabot-automerge-start)
[//]: # (dependabot-automerge-end)

---

<details>
<summary>Dependabot commands and options</summary>
<br />

You can trigger Dependabot actions by commenting on this PR:
- `@dependabot rebase` will rebase this PR
- `@dependabot recreate` will recreate this PR, overwriting any edits
that have been made to it
- `@dependabot show <dependency name> ignore conditions` will show all
of the ignore conditions of the specified dependency
- `@dependabot ignore this major version` will close this PR and stop
Dependabot creating any more for this major version (unless you reopen
the PR or upgrade to it yourself)
- `@dependabot ignore this minor version` will close this PR and stop
Dependabot creating any more for this minor version (unless you reopen
the PR or upgrade to it yourself)
- `@dependabot ignore this dependency` will close this PR and stop
Dependabot creating any more for this dependency (unless you reopen the
PR or upgrade to it yourself)


</details>

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-03-02 12:55:05 +01:00
dependabot[bot]
e43b678872 ci: [DEPENDABOT] Bump actions/download-artifact from 7.0.0 to 8.0.0 (#2970)
Bumps
[actions/download-artifact](https://github.com/actions/download-artifact)
from 7.0.0 to 8.0.0.
<details>
<summary>Release notes</summary>
<p><em>Sourced from <a
href="https://github.com/actions/download-artifact/releases">actions/download-artifact's
releases</a>.</em></p>
<blockquote>
<h2>v8.0.0</h2>
<h2>v8 - What's new</h2>
<h3>Direct downloads</h3>
<p>To support direct uploads in <code>actions/upload-artifact</code>,
the action will no longer attempt to unzip all downloaded files.
Instead, the action checks the <code>Content-Type</code> header ahead of
unzipping and skips non-zipped files. Callers wishing to download a
zipped file as-is can also set the new <code>skip-decompress</code>
parameter to <code>false</code>.</p>
<h3>Enforced checks (breaking)</h3>
<p>A previous release introduced digest checks on the download. If a
download hash didn't match the expected hash from the server, the action
would log a warning. Callers can now configure the behavior on mismatch
with the <code>digest-mismatch</code> parameter. To be secure by
default, we are now defaulting the behavior to <code>error</code> which
will fail the workflow run.</p>
<h3>ESM</h3>
<p>To support new versions of the @actions/* packages, we've upgraded
the package to ESM.</p>
<h2>What's Changed</h2>
<ul>
<li>Don't attempt to un-zip non-zipped downloads by <a
href="https://github.com/danwkennedy"><code>@​danwkennedy</code></a> in
<a
href="https://redirect.github.com/actions/download-artifact/pull/460">actions/download-artifact#460</a></li>
<li>Add a setting to specify what to do on hash mismatch and default it
to <code>error</code> by <a
href="https://github.com/danwkennedy"><code>@​danwkennedy</code></a> in
<a
href="https://redirect.github.com/actions/download-artifact/pull/461">actions/download-artifact#461</a></li>
</ul>
<p><strong>Full Changelog</strong>: <a
href="https://github.com/actions/download-artifact/compare/v7...v8.0.0">https://github.com/actions/download-artifact/compare/v7...v8.0.0</a></p>
</blockquote>
</details>
<details>
<summary>Commits</summary>
<ul>
<li><a
href="70fc10c6e5"><code>70fc10c</code></a>
Merge pull request <a
href="https://redirect.github.com/actions/download-artifact/issues/461">#461</a>
from actions/danwkennedy/digest-mismatch-behavior</li>
<li><a
href="f258da9a50"><code>f258da9</code></a>
Add change docs</li>
<li><a
href="ccc058e5fb"><code>ccc058e</code></a>
Fix linting issues</li>
<li><a
href="bd7976ba57"><code>bd7976b</code></a>
Add a setting to specify what to do on hash mismatch and default it to
<code>error</code></li>
<li><a
href="ac21fcf45e"><code>ac21fcf</code></a>
Merge pull request <a
href="https://redirect.github.com/actions/download-artifact/issues/460">#460</a>
from actions/danwkennedy/download-no-unzip</li>
<li><a
href="15999bff51"><code>15999bf</code></a>
Add note about package bumps</li>
<li><a
href="974686ed50"><code>974686e</code></a>
Bump the version to <code>v8</code> and add release notes</li>
<li><a
href="fbe48b1d27"><code>fbe48b1</code></a>
Update test names to make it clearer what they do</li>
<li><a
href="96bf374a61"><code>96bf374</code></a>
One more test fix</li>
<li><a
href="b8c4819ef5"><code>b8c4819</code></a>
Fix skip decompress test</li>
<li>Additional commits viewable in <a
href="37930b1c2a...70fc10c6e5">compare
view</a></li>
</ul>
</details>
<br />


[![Dependabot compatibility
score](https://dependabot-badges.githubapp.com/badges/compatibility_score?dependency-name=actions/download-artifact&package-manager=github_actions&previous-version=7.0.0&new-version=8.0.0)](https://docs.github.com/en/github/managing-security-vulnerabilities/about-dependabot-security-updates#about-compatibility-scores)

Dependabot will resolve any conflicts with this PR as long as you don't
alter it yourself. You can also trigger a rebase manually by commenting
`@dependabot rebase`.

[//]: # (dependabot-automerge-start)
[//]: # (dependabot-automerge-end)

---

<details>
<summary>Dependabot commands and options</summary>
<br />

You can trigger Dependabot actions by commenting on this PR:
- `@dependabot rebase` will rebase this PR
- `@dependabot recreate` will recreate this PR, overwriting any edits
that have been made to it
- `@dependabot show <dependency name> ignore conditions` will show all
of the ignore conditions of the specified dependency
- `@dependabot ignore this major version` will close this PR and stop
Dependabot creating any more for this major version (unless you reopen
the PR or upgrade to it yourself)
- `@dependabot ignore this minor version` will close this PR and stop
Dependabot creating any more for this minor version (unless you reopen
the PR or upgrade to it yourself)
- `@dependabot ignore this dependency` will close this PR and stop
Dependabot creating any more for this dependency (unless you reopen the
PR or upgrade to it yourself)


</details>

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-03-02 12:54:56 +01:00
dependabot[bot]
ef0a765e07 ci: [DEPENDABOT] Bump actions/upload-artifact from 6.0.0 to 7.0.0 in /.github/actions/code-coverage (#2971)
Bumps
[actions/upload-artifact](https://github.com/actions/upload-artifact)
from 6.0.0 to 7.0.0.
<details>
<summary>Release notes</summary>
<p><em>Sourced from <a
href="https://github.com/actions/upload-artifact/releases">actions/upload-artifact's
releases</a>.</em></p>
<blockquote>
<h2>v7.0.0</h2>
<h2>v7 What's new</h2>
<h3>Direct Uploads</h3>
<p>Adds support for uploading single files directly (unzipped). Callers
can set the new <code>archive</code> parameter to <code>false</code> to
skip zipping the file during upload. Right now, we only support single
files. The action will fail if the glob passed resolves to multiple
files. The <code>name</code> parameter is also ignored with this
setting. Instead, the name of the artifact will be the name of the
uploaded file.</p>
<h3>ESM</h3>
<p>To support new versions of the <code>@actions/*</code> packages,
we've upgraded the package to ESM.</p>
<h2>What's Changed</h2>
<ul>
<li>Add proxy integration test by <a
href="https://github.com/Link"><code>@​Link</code></a>- in <a
href="https://redirect.github.com/actions/upload-artifact/pull/754">actions/upload-artifact#754</a></li>
<li>Upgrade the module to ESM and bump dependencies by <a
href="https://github.com/danwkennedy"><code>@​danwkennedy</code></a> in
<a
href="https://redirect.github.com/actions/upload-artifact/pull/762">actions/upload-artifact#762</a></li>
<li>Support direct file uploads by <a
href="https://github.com/danwkennedy"><code>@​danwkennedy</code></a> in
<a
href="https://redirect.github.com/actions/upload-artifact/pull/764">actions/upload-artifact#764</a></li>
</ul>
<h2>New Contributors</h2>
<ul>
<li><a href="https://github.com/Link"><code>@​Link</code></a>- made
their first contribution in <a
href="https://redirect.github.com/actions/upload-artifact/pull/754">actions/upload-artifact#754</a></li>
</ul>
<p><strong>Full Changelog</strong>: <a
href="https://github.com/actions/upload-artifact/compare/v6...v7.0.0">https://github.com/actions/upload-artifact/compare/v6...v7.0.0</a></p>
</blockquote>
</details>
<details>
<summary>Commits</summary>
<ul>
<li><a
href="bbbca2ddaa"><code>bbbca2d</code></a>
Support direct file uploads (<a
href="https://redirect.github.com/actions/upload-artifact/issues/764">#764</a>)</li>
<li><a
href="589182c5a4"><code>589182c</code></a>
Upgrade the module to ESM and bump dependencies (<a
href="https://redirect.github.com/actions/upload-artifact/issues/762">#762</a>)</li>
<li><a
href="47309c993a"><code>47309c9</code></a>
Merge pull request <a
href="https://redirect.github.com/actions/upload-artifact/issues/754">#754</a>
from actions/Link-/add-proxy-integration-tests</li>
<li><a
href="02a8460834"><code>02a8460</code></a>
Add proxy integration test</li>
<li>See full diff in <a
href="b7c566a772...bbbca2ddaa">compare
view</a></li>
</ul>
</details>
<br />


[![Dependabot compatibility
score](https://dependabot-badges.githubapp.com/badges/compatibility_score?dependency-name=actions/upload-artifact&package-manager=github_actions&previous-version=6.0.0&new-version=7.0.0)](https://docs.github.com/en/github/managing-security-vulnerabilities/about-dependabot-security-updates#about-compatibility-scores)

Dependabot will resolve any conflicts with this PR as long as you don't
alter it yourself. You can also trigger a rebase manually by commenting
`@dependabot rebase`.

[//]: # (dependabot-automerge-start)
[//]: # (dependabot-automerge-end)

---

<details>
<summary>Dependabot commands and options</summary>
<br />

You can trigger Dependabot actions by commenting on this PR:
- `@dependabot rebase` will rebase this PR
- `@dependabot recreate` will recreate this PR, overwriting any edits
that have been made to it
- `@dependabot show <dependency name> ignore conditions` will show all
of the ignore conditions of the specified dependency
- `@dependabot ignore this major version` will close this PR and stop
Dependabot creating any more for this major version (unless you reopen
the PR or upgrade to it yourself)
- `@dependabot ignore this minor version` will close this PR and stop
Dependabot creating any more for this minor version (unless you reopen
the PR or upgrade to it yourself)
- `@dependabot ignore this dependency` will close this PR and stop
Dependabot creating any more for this dependency (unless you reopen the
PR or upgrade to it yourself)


</details>

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-03-02 12:54:46 +01:00
Ayaz Salikhov
f9c89264da chore: Enable debug symbols (#2967) 2026-02-25 16:42:23 +00:00
Ayaz Salikhov
16030d1d81 chore: Update cleanup-workspace to delete old .conan2 dir on macOS (#2964) 2026-02-24 02:40:06 +00:00
Ayaz Salikhov
1220d632b5 style: Fix lint comments due to style changes (#2963) 2026-02-23 16:24:27 +00:00
Ayaz Salikhov
e9052bcd80 style: Remove readability-identifier-naming where not needed (#2962) 2026-02-23 14:43:19 +00:00
Ayaz Salikhov
c1f6a6eb31 chore: Fix compilation due to use of std::ranges::mismatch (#2960) 2026-02-23 14:32:07 +00:00
github-actions[bot]
af736717fc style: clang-tidy auto fixes (#2958)
Co-authored-by: godexsoft <385326+godexsoft@users.noreply.github.com>
2026-02-23 13:54:07 +00:00
Sergey Kuznetsov
2d6f82c27f feat: Metrics for requested ledger age (#2947)
Adding metrics to be able to analyse requested ledger age distribution.

---------

Co-authored-by: Claude Sonnet 4.5 <noreply@anthropic.com>
2026-02-20 18:01:15 +00:00
Ayaz Salikhov
6ba58f42f0 style: Set clang-format width 100 (#2953) 2026-02-20 15:56:03 +00:00
Ayaz Salikhov
480264ff8f perf: Use string_view in AccountInfo (#2951) 2026-02-18 17:11:31 +00:00
Ayaz Salikhov
584d2bb5f2 feat: Support Lending Protocol (#2945) 2026-02-17 22:54:24 +00:00
emrearıyürek
9d3dbce73b fix: Remove RpcEntryNotFound from ClioError (#2661)
Since rippled has its own error codes, all occurrences have been
replaced except for [PR2549](https://github.com/XRPLF/clio/pull/2549),
and the entry has also been removed entirely from Clio’s error enum.
This modification follows the feedback provided in this comment:
https://github.com/XRPLF/clio/pull/2549#discussion_r2394840688

Pending compatible libxrpl — temporarily on hold

---------

Co-authored-by: Ayaz Salikhov <mathbunnyru@users.noreply.github.com>
Co-authored-by: Alex Kremer <akremer@ripple.com>
2026-02-06 15:33:18 +00:00
Ayaz Salikhov
8b6f65f0b7 chore: Update hashes of XRPLF/actions (#2944) 2026-02-02 20:48:50 +00:00
dependabot[bot]
91aba853af ci: [DEPENDABOT] Bump docker/login-action from 3.6.0 to 3.7.0 in /.github/actions/build-docker-image (#2942)
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-02-02 14:50:37 +00:00
dependabot[bot]
af03a2fbe4 ci: [DEPENDABOT] Bump actions/cache from 5.0.2 to 5.0.3 (#2941)
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-02-02 14:50:28 +00:00
dependabot[bot]
61c47c8efd ci: [DEPENDABOT] Bump docker/login-action from 3.6.0 to 3.7.0 (#2940)
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-02-02 14:50:14 +00:00
github-actions[bot]
8f36b1d5ca style: Update pre-commit hooks (#2939)
Co-authored-by: mathbunnyru <12270691+mathbunnyru@users.noreply.github.com>
2026-02-02 14:49:33 +00:00
Alex Kremer
9fd15eb08b chore: Enable TSAN without ignoring errors (#2828) 2026-01-30 19:14:36 +00:00
Ayaz Salikhov
cf77a10555 style: Adopt cmake-format from xrpld (#2938) 2026-01-29 16:44:04 +00:00
Sergey Kuznetsov
437168aa13 docs: Add section about faster cache loading (#2932)
Co-authored-by: Maria Shodunke <maria-robobug@users.noreply.github.com>
2026-01-26 13:30:58 +00:00
dependabot[bot]
8a7c6b0aa4 ci: [DEPENDABOT] Bump actions/checkout from 6.0.1 to 6.0.2 (#2933) 2026-01-26 08:33:39 +00:00
dependabot[bot]
93a344c3fa ci: [DEPENDABOT] Bump peter-evans/create-pull-request from 8.0.0 to 8.1.0 (#2934) 2026-01-26 08:33:08 +00:00
Sergey Kuznetsov
59d07fab64 fix: Flush buffers before renaming cache file (#2927)
If clio shuts itself down due to exceeding graceful period when cache is
saved and renamed but the buffers are not flushed, we may end up with a
corrupted cache file. Clio will detect corruption and will not load
corrupted cache file, but we could avoid it by explicitly flushing
ofstream buffer.
2026-01-22 11:35:00 +00:00
github-actions[bot]
3bb3e0b9f9 style: clang-tidy auto fixes (#2930)
Fixes #2929.
2026-01-22 11:34:04 +00:00
Sergey Kuznetsov
a72e5a180f feat: Choose writer by cluster communication (#2830)
Fixes #1974
2026-01-21 11:41:26 +00:00
dependabot[bot]
0ebbaaadef ci: [DEPENDABOT] Bump actions/cache from 5.0.1 to 5.0.2 (#2925)
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-01-19 14:09:14 +00:00
Ayaz Salikhov
b983aea15d chore: Delete duplicate etlng tests (#2920) 2026-01-14 15:28:31 +00:00
Ayaz Salikhov
63e7f9a72b ci: Run colima delete on macOS (#2915) 2026-01-14 15:21:02 +00:00
Alex Kremer
eebee4d671 chore: Fix linker warning in benchmark (#2918) 2026-01-14 14:10:21 +00:00
Alex Kremer
a6d5f94470 chore: Add .zed to .gitignore (#2919) 2026-01-14 13:38:00 +00:00
github-actions[bot]
2b473c8613 style: clang-tidy auto fixes (#2917) 2026-01-14 09:50:38 +00:00
638 changed files with 19563 additions and 8430 deletions

View File

@@ -22,7 +22,7 @@ BreakBeforeBinaryOperators: false
BreakBeforeBraces: WebKit BreakBeforeBraces: WebKit
BreakBeforeTernaryOperators: true BreakBeforeTernaryOperators: true
BreakConstructorInitializersBeforeComma: true BreakConstructorInitializersBeforeComma: true
ColumnLimit: 120 ColumnLimit: 100
CommentPragmas: "^ IWYU pragma:" CommentPragmas: "^ IWYU pragma:"
ConstructorInitializerAllOnOneLineOrOnePerLine: true ConstructorInitializerAllOnOneLineOrOnePerLine: true
ConstructorInitializerIndentWidth: 4 ConstructorInitializerIndentWidth: 4

View File

@@ -2,15 +2,6 @@ _help_parse: Options affecting listfile parsing
parse: parse:
_help_additional_commands: _help_additional_commands:
- Specify structure for custom cmake functions - Specify structure for custom cmake functions
additional_commands:
foo:
flags:
- BAR
- BAZ
kwargs:
HEADERS: "*"
SOURCES: "*"
DEPENDS: "*"
_help_override_spec: _help_override_spec:
- Override configurations per-command where available - Override configurations per-command where available
override_spec: {} override_spec: {}
@@ -30,7 +21,7 @@ format:
line_width: 120 line_width: 120
_help_tab_size: _help_tab_size:
- How many spaces to tab for indent - How many spaces to tab for indent
tab_size: 2 tab_size: 4
_help_use_tabchars: _help_use_tabchars:
- If true, lines are indented using tab characters (utf-8 - If true, lines are indented using tab characters (utf-8
- 0x09) instead of <tab_size> space characters (utf-8 0x20). - 0x09) instead of <tab_size> space characters (utf-8 0x20).
@@ -70,19 +61,19 @@ format:
_help_dangle_parens: _help_dangle_parens:
- If a statement is wrapped to more than one line, than dangle - If a statement is wrapped to more than one line, than dangle
- the closing parenthesis on its own line. - the closing parenthesis on its own line.
dangle_parens: true dangle_parens: false
_help_dangle_align: _help_dangle_align:
- If the trailing parenthesis must be 'dangled' on its on - If the trailing parenthesis must be 'dangled' on its on
- "line, then align it to this reference: `prefix`: the start" - "line, then align it to this reference: `prefix`: the start"
- "of the statement, `prefix-indent`: the start of the" - "of the statement, `prefix-indent`: the start of the"
- "statement, plus one indentation level, `child`: align to" - "statement, plus one indentation level, `child`: align to"
- the column of the arguments - the column of the arguments
dangle_align: prefix dangle_align: prefix
_help_min_prefix_chars: _help_min_prefix_chars:
- If the statement spelling length (including space and - If the statement spelling length (including space and
- parenthesis) is smaller than this amount, then force reject - parenthesis) is smaller than this amount, then force reject
- nested layouts. - nested layouts.
min_prefix_chars: 4 min_prefix_chars: 18
_help_max_prefix_chars: _help_max_prefix_chars:
- If the statement spelling length (including space and - If the statement spelling length (including space and
- parenthesis) is larger than the tab width by more than this - parenthesis) is larger than the tab width by more than this
@@ -127,7 +118,7 @@ _help_markup: Options affecting comment reflow and formatting.
markup: markup:
_help_bullet_char: _help_bullet_char:
- What character to use for bulleted lists - What character to use for bulleted lists
bullet_char: "*" bullet_char: "-"
_help_enum_char: _help_enum_char:
- What character to use as punctuation after numerals in an - What character to use as punctuation after numerals in an
- enumerated list - enumerated list

View File

@@ -34,14 +34,14 @@ runs:
steps: steps:
- name: Login to DockerHub - name: Login to DockerHub
if: ${{ inputs.push_image == 'true' && inputs.dockerhub_repo != '' }} if: ${{ inputs.push_image == 'true' && inputs.dockerhub_repo != '' }}
uses: docker/login-action@5e57cd118135c172c3672efd75eb46360885c0ef # v3.6.0 uses: docker/login-action@c94ce9fb468520275223c153574b00df6fe4bcc9 # v3.7.0
with: with:
username: ${{ env.DOCKERHUB_USER }} username: ${{ env.DOCKERHUB_USER }}
password: ${{ env.DOCKERHUB_PW }} password: ${{ env.DOCKERHUB_PW }}
- name: Login to GitHub Container Registry - name: Login to GitHub Container Registry
if: ${{ inputs.push_image == 'true' }} if: ${{ inputs.push_image == 'true' }}
uses: docker/login-action@5e57cd118135c172c3672efd75eb46360885c0ef # v3.6.0 uses: docker/login-action@c94ce9fb468520275223c153574b00df6fe4bcc9 # v3.7.0
with: with:
registry: ghcr.io registry: ghcr.io
username: ${{ github.repository_owner }} username: ${{ github.repository_owner }}

View File

@@ -24,7 +24,7 @@ runs:
-j8 --exclude-throw-branches -j8 --exclude-throw-branches
- name: Archive coverage report - name: Archive coverage report
uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6.0.0 uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0
with: with:
name: coverage-report.xml name: coverage-report.xml
path: build/coverage_report.xml path: build/coverage_report.xml

View File

@@ -1,46 +0,0 @@
#!/bin/bash
set -o pipefail
# Note: This script is intended to be run from the root of the repository.
#
# This script runs each unit-test separately and generates reports from the currently active sanitizer.
# Output is saved in ./.sanitizer-report in the root of the repository
if [[ -z "$1" ]]; then
cat <<EOF
ERROR
-----------------------------------------------------------------------------
Path to clio_tests should be passed as first argument to the script.
-----------------------------------------------------------------------------
EOF
exit 1
fi
TEST_BINARY=$1
if [[ ! -f "$TEST_BINARY" ]]; then
echo "Test binary not found: $TEST_BINARY"
exit 1
fi
TESTS=$($TEST_BINARY --gtest_list_tests | awk '/^ / {print suite $1} !/^ / {suite=$1}')
OUTPUT_DIR="./.sanitizer-report"
mkdir -p "$OUTPUT_DIR"
export TSAN_OPTIONS="die_after_fork=0"
export MallocNanoZone='0' # for MacOSX
for TEST in $TESTS; do
OUTPUT_FILE="$OUTPUT_DIR/${TEST//\//_}.log"
$TEST_BINARY --gtest_filter="$TEST" >"$OUTPUT_FILE" 2>&1
if [ $? -ne 0 ]; then
echo "'$TEST' failed a sanitizer check."
else
rm "$OUTPUT_FILE"
fi
done

View File

@@ -48,11 +48,11 @@ jobs:
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1 - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
- name: Download Clio binary from artifact - name: Download Clio binary from artifact
if: ${{ inputs.artifact_name != null }} if: ${{ inputs.artifact_name != null }}
uses: actions/download-artifact@37930b1c2abaa49bbe596cd826c3c89aef350131 # v7.0.0 uses: actions/download-artifact@70fc10c6e5e1ce46ad2ea6f2b72d43f7d47b13c3 # v8.0.0
with: with:
name: ${{ inputs.artifact_name }} name: ${{ inputs.artifact_name }}
path: ./docker/clio/artifact/ path: ./docker/clio/artifact/

View File

@@ -23,6 +23,7 @@ on:
- "cmake/**" - "cmake/**"
- "src/**" - "src/**"
- "tests/**" - "tests/**"
- "benchmarks/**"
- docs/config-description.md - docs/config-description.md
workflow_dispatch: workflow_dispatch:
@@ -100,9 +101,9 @@ jobs:
image: ghcr.io/xrplf/clio-ci:14342e087ceb8b593027198bf9ef06a43833c696 image: ghcr.io/xrplf/clio-ci:14342e087ceb8b593027198bf9ef06a43833c696
steps: steps:
- uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1 - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
- uses: actions/download-artifact@37930b1c2abaa49bbe596cd826c3c89aef350131 # v7.0.0 - uses: actions/download-artifact@70fc10c6e5e1ce46ad2ea6f2b72d43f7d47b13c3 # v8.0.0
with: with:
name: clio_server_Linux_Release_gcc name: clio_server_Linux_Release_gcc

View File

@@ -24,12 +24,12 @@ jobs:
image: ghcr.io/xrplf/clio-ci:14342e087ceb8b593027198bf9ef06a43833c696 image: ghcr.io/xrplf/clio-ci:14342e087ceb8b593027198bf9ef06a43833c696
steps: steps:
- uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1 - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
with: with:
fetch-depth: 0 fetch-depth: 0
- name: Prepare runner - name: Prepare runner
uses: XRPLF/actions/prepare-runner@f05cab7b8541eee6473aa42beb9d2fe35608a190 uses: XRPLF/actions/prepare-runner@2cbf481018d930656e9276fcc20dc0e3a0be5b6d
with: with:
enable_ccache: false enable_ccache: false
@@ -59,7 +59,7 @@ jobs:
run: strip build/clio_tests run: strip build/clio_tests
- name: Upload clio_tests - name: Upload clio_tests
uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6.0.0 uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0
with: with:
name: clio_tests_check_libxrpl name: clio_tests_check_libxrpl
path: build/clio_tests path: build/clio_tests
@@ -72,7 +72,7 @@ jobs:
image: ghcr.io/xrplf/clio-ci:14342e087ceb8b593027198bf9ef06a43833c696 image: ghcr.io/xrplf/clio-ci:14342e087ceb8b593027198bf9ef06a43833c696
steps: steps:
- uses: actions/download-artifact@37930b1c2abaa49bbe596cd826c3c89aef350131 # v7.0.0 - uses: actions/download-artifact@70fc10c6e5e1ce46ad2ea6f2b72d43f7d47b13c3 # v8.0.0
with: with:
name: clio_tests_check_libxrpl name: clio_tests_check_libxrpl
@@ -92,7 +92,7 @@ jobs:
issues: write issues: write
steps: steps:
- uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1 - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
- name: Create an issue - name: Create an issue
uses: ./.github/actions/create-issue uses: ./.github/actions/create-issue

View File

@@ -39,12 +39,12 @@ jobs:
pull-requests: write pull-requests: write
steps: steps:
- uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1 - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
with: with:
fetch-depth: 0 fetch-depth: 0
- name: Prepare runner - name: Prepare runner
uses: XRPLF/actions/prepare-runner@f05cab7b8541eee6473aa42beb9d2fe35608a190 uses: XRPLF/actions/prepare-runner@2cbf481018d930656e9276fcc20dc0e3a0be5b6d
with: with:
enable_ccache: false enable_ccache: false
@@ -107,7 +107,7 @@ jobs:
- name: Create PR with fixes - name: Create PR with fixes
if: ${{ steps.files_changed.outcome != 'success' && github.event_name != 'pull_request' }} if: ${{ steps.files_changed.outcome != 'success' && github.event_name != 'pull_request' }}
uses: peter-evans/create-pull-request@98357b18bf14b5342f975ff684046ec3b2a07725 # v8.0.0 uses: peter-evans/create-pull-request@c0f553fe549906ede9cf27b5156039d195d2ece0 # v8.1.0
env: env:
GH_REPO: ${{ github.repository }} GH_REPO: ${{ github.repository }}
GH_TOKEN: ${{ github.token }} GH_TOKEN: ${{ github.token }}

View File

@@ -22,12 +22,12 @@ jobs:
steps: steps:
- name: Checkout - name: Checkout
uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1 uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
with: with:
lfs: true lfs: true
- name: Prepare runner - name: Prepare runner
uses: XRPLF/actions/prepare-runner@f05cab7b8541eee6473aa42beb9d2fe35608a190 uses: XRPLF/actions/prepare-runner@2cbf481018d930656e9276fcc20dc0e3a0be5b6d
with: with:
enable_ccache: false enable_ccache: false

View File

@@ -169,7 +169,7 @@ jobs:
issues: write issues: write
steps: steps:
- uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1 - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
- name: Create an issue - name: Create an issue
uses: ./.github/actions/create-issue uses: ./.github/actions/create-issue

View File

@@ -12,7 +12,7 @@ on:
jobs: jobs:
auto-update: auto-update:
uses: XRPLF/actions/.github/workflows/pre-commit-autoupdate.yml@ad4ab1ae5a54a4bab0e87294c31fc0729f788b2b uses: XRPLF/actions/.github/workflows/pre-commit-autoupdate.yml@8b19d3462e52cd8ea4d76b4c8d0f7533e7469c15
with: with:
sign_commit: true sign_commit: true
committer: "Clio CI <skuznetsov@ripple.com>" committer: "Clio CI <skuznetsov@ripple.com>"

View File

@@ -8,7 +8,7 @@ on:
jobs: jobs:
run-hooks: run-hooks:
uses: XRPLF/actions/.github/workflows/pre-commit.yml@282890f46d6921249d5659dd38babcb0bd8aef48 uses: XRPLF/actions/.github/workflows/pre-commit.yml@320be44621ca2a080f05aeb15817c44b84518108
with: with:
runs_on: heavy runs_on: heavy
container: '{ "image": "ghcr.io/xrplf/clio-pre-commit:14342e087ceb8b593027198bf9ef06a43833c696" }' container: '{ "image": "ghcr.io/xrplf/clio-pre-commit:14342e087ceb8b593027198bf9ef06a43833c696" }'

View File

@@ -88,14 +88,14 @@ jobs:
steps: steps:
- name: Cleanup workspace - name: Cleanup workspace
if: ${{ runner.os == 'macOS' }} if: ${{ runner.os == 'macOS' }}
uses: XRPLF/actions/cleanup-workspace@cf0433aa74563aead044a1e395610c96d65a37cf uses: XRPLF/actions/cleanup-workspace@c7d9ce5ebb03c752a354889ecd870cadfc2b1cd4
- uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1 - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
with: with:
fetch-depth: 0 fetch-depth: 0
- name: Prepare runner - name: Prepare runner
uses: XRPLF/actions/prepare-runner@f05cab7b8541eee6473aa42beb9d2fe35608a190 uses: XRPLF/actions/prepare-runner@2cbf481018d930656e9276fcc20dc0e3a0be5b6d
with: with:
enable_ccache: ${{ inputs.download_ccache }} enable_ccache: ${{ inputs.download_ccache }}
@@ -113,7 +113,7 @@ jobs:
- name: Restore ccache cache - name: Restore ccache cache
if: ${{ inputs.download_ccache && github.ref != 'refs/heads/develop' }} if: ${{ inputs.download_ccache && github.ref != 'refs/heads/develop' }}
uses: actions/cache/restore@9255dc7a253b0ccc959486e2bca901246202afeb # v5.0.1 uses: actions/cache/restore@cdf6c1fa76f9f475f3d7449005a359c84ca0f306 # v5.0.3
with: with:
path: ${{ env.CCACHE_DIR }} path: ${{ env.CCACHE_DIR }}
key: ${{ steps.cache_key.outputs.key }} key: ${{ steps.cache_key.outputs.key }}
@@ -151,7 +151,7 @@ jobs:
- name: Upload build time analyze report - name: Upload build time analyze report
if: ${{ inputs.analyze_build_time }} if: ${{ inputs.analyze_build_time }}
uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6.0.0 uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0
with: with:
name: build_time_report_${{ runner.os }}_${{ inputs.build_type }}_${{ inputs.conan_profile }} name: build_time_report_${{ runner.os }}_${{ inputs.build_type }}_${{ inputs.conan_profile }}
path: build_time_report.txt path: build_time_report.txt
@@ -164,7 +164,7 @@ jobs:
- name: Save ccache cache - name: Save ccache cache
if: ${{ inputs.upload_ccache && github.ref == 'refs/heads/develop' }} if: ${{ inputs.upload_ccache && github.ref == 'refs/heads/develop' }}
uses: actions/cache/save@9255dc7a253b0ccc959486e2bca901246202afeb # v5.0.1 uses: actions/cache/save@cdf6c1fa76f9f475f3d7449005a359c84ca0f306 # v5.0.3
with: with:
path: ${{ env.CCACHE_DIR }} path: ${{ env.CCACHE_DIR }}
key: ${{ steps.cache_key.outputs.key }} key: ${{ steps.cache_key.outputs.key }}
@@ -179,28 +179,28 @@ jobs:
- name: Upload clio_server - name: Upload clio_server
if: ${{ inputs.upload_clio_server && !inputs.code_coverage && !inputs.analyze_build_time }} if: ${{ inputs.upload_clio_server && !inputs.code_coverage && !inputs.analyze_build_time }}
uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6.0.0 uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0
with: with:
name: clio_server_${{ runner.os }}_${{ inputs.build_type }}_${{ inputs.conan_profile }} name: clio_server_${{ runner.os }}_${{ inputs.build_type }}_${{ inputs.conan_profile }}
path: build/clio_server path: build/clio_server
- name: Upload clio_tests - name: Upload clio_tests
if: ${{ !inputs.code_coverage && !inputs.analyze_build_time && !inputs.package }} if: ${{ !inputs.code_coverage && !inputs.analyze_build_time && !inputs.package }}
uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6.0.0 uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0
with: with:
name: clio_tests_${{ runner.os }}_${{ inputs.build_type }}_${{ inputs.conan_profile }} name: clio_tests_${{ runner.os }}_${{ inputs.build_type }}_${{ inputs.conan_profile }}
path: build/clio_tests path: build/clio_tests
- name: Upload clio_integration_tests - name: Upload clio_integration_tests
if: ${{ !inputs.code_coverage && !inputs.analyze_build_time && !inputs.package }} if: ${{ !inputs.code_coverage && !inputs.analyze_build_time && !inputs.package }}
uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6.0.0 uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0
with: with:
name: clio_integration_tests_${{ runner.os }}_${{ inputs.build_type }}_${{ inputs.conan_profile }} name: clio_integration_tests_${{ runner.os }}_${{ inputs.build_type }}_${{ inputs.conan_profile }}
path: build/clio_integration_tests path: build/clio_integration_tests
- name: Upload Clio Linux package - name: Upload Clio Linux package
if: ${{ inputs.package }} if: ${{ inputs.package }}
uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6.0.0 uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0
with: with:
name: clio_deb_package_${{ runner.os }}_${{ inputs.build_type }}_${{ inputs.conan_profile }} name: clio_deb_package_${{ runner.os }}_${{ inputs.build_type }}_${{ inputs.conan_profile }}
path: build/*.deb path: build/*.deb

View File

@@ -55,16 +55,16 @@ jobs:
contents: write contents: write
steps: steps:
- uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1 - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
with: with:
fetch-depth: 0 fetch-depth: 0
- name: Prepare runner - name: Prepare runner
uses: XRPLF/actions/prepare-runner@f05cab7b8541eee6473aa42beb9d2fe35608a190 uses: XRPLF/actions/prepare-runner@2cbf481018d930656e9276fcc20dc0e3a0be5b6d
with: with:
enable_ccache: false enable_ccache: false
- uses: actions/download-artifact@37930b1c2abaa49bbe596cd826c3c89aef350131 # v7.0.0 - uses: actions/download-artifact@70fc10c6e5e1ce46ad2ea6f2b72d43f7d47b13c3 # v8.0.0
with: with:
path: release_artifacts path: release_artifacts
pattern: clio_server_* pattern: clio_server_*
@@ -72,7 +72,7 @@ jobs:
- name: Prepare release artifacts - name: Prepare release artifacts
run: .github/scripts/prepare-release-artifacts.sh release_artifacts run: .github/scripts/prepare-release-artifacts.sh release_artifacts
- uses: actions/download-artifact@37930b1c2abaa49bbe596cd826c3c89aef350131 # v7.0.0 - uses: actions/download-artifact@70fc10c6e5e1ce46ad2ea6f2b72d43f7d47b13c3 # v8.0.0
with: with:
path: release_artifacts path: release_artifacts
pattern: clio_deb_package_* pattern: clio_deb_package_*
@@ -94,7 +94,7 @@ jobs:
git-cliff "${BASE_COMMIT}..HEAD" --ignore-tags "nightly|-b|-rc" >> "${RUNNER_TEMP}/release_notes.md" git-cliff "${BASE_COMMIT}..HEAD" --ignore-tags "nightly|-b|-rc" >> "${RUNNER_TEMP}/release_notes.md"
- name: Upload release notes - name: Upload release notes
uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6.0.0 uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0
with: with:
name: release_notes_${{ inputs.version }} name: release_notes_${{ inputs.version }}
path: "${RUNNER_TEMP}/release_notes.md" path: "${RUNNER_TEMP}/release_notes.md"

View File

@@ -45,54 +45,29 @@ jobs:
if: ${{ inputs.run_unit_tests }} if: ${{ inputs.run_unit_tests }}
env:
# TODO: remove completely when we have fixed all currently existing issues with sanitizers
SANITIZER_IGNORE_ERRORS: ${{ endsWith(inputs.conan_profile, '.tsan') }}
steps: steps:
- name: Cleanup workspace - name: Cleanup workspace
if: ${{ runner.os == 'macOS' }} if: ${{ runner.os == 'macOS' }}
uses: XRPLF/actions/cleanup-workspace@cf0433aa74563aead044a1e395610c96d65a37cf uses: XRPLF/actions/cleanup-workspace@c7d9ce5ebb03c752a354889ecd870cadfc2b1cd4
- uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1 - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
with: with:
fetch-depth: 0 fetch-depth: 0
- uses: actions/download-artifact@37930b1c2abaa49bbe596cd826c3c89aef350131 # v7.0.0 - uses: actions/download-artifact@70fc10c6e5e1ce46ad2ea6f2b72d43f7d47b13c3 # v8.0.0
with: with:
name: clio_tests_${{ runner.os }}_${{ inputs.build_type }}_${{ inputs.conan_profile }} name: clio_tests_${{ runner.os }}_${{ inputs.build_type }}_${{ inputs.conan_profile }}
- name: Make clio_tests executable - name: Make clio_tests executable
run: chmod +x ./clio_tests run: chmod +x ./clio_tests
- name: Run clio_tests (regular) - name: Run clio_tests
if: ${{ env.SANITIZER_IGNORE_ERRORS == 'false' }} continue-on-error: true
id: run_clio_tests
run: ./clio_tests run: ./clio_tests
- name: Run clio_tests (sanitizer errors ignored)
if: ${{ env.SANITIZER_IGNORE_ERRORS == 'true' }}
run: ./.github/scripts/execute-tests-under-sanitizer.sh ./clio_tests
- name: Check for sanitizer report
if: ${{ env.SANITIZER_IGNORE_ERRORS == 'true' }}
id: check_report
run: |
if ls .sanitizer-report/* 1> /dev/null 2>&1; then
echo "found_report=true" >> $GITHUB_OUTPUT
else
echo "found_report=false" >> $GITHUB_OUTPUT
fi
- name: Upload sanitizer report
if: ${{ env.SANITIZER_IGNORE_ERRORS == 'true' && steps.check_report.outputs.found_report == 'true' }}
uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6.0.0
with:
name: sanitizer_report_${{ runner.os }}_${{ inputs.build_type }}_${{ inputs.conan_profile }}
path: .sanitizer-report/*
include-hidden-files: true
- name: Create an issue - name: Create an issue
if: ${{ false && env.SANITIZER_IGNORE_ERRORS == 'true' && steps.check_report.outputs.found_report == 'true' }} if: ${{ steps.run_clio_tests.outcome == 'failure' && endsWith(inputs.conan_profile, 'san') }}
uses: ./.github/actions/create-issue uses: ./.github/actions/create-issue
env: env:
GH_TOKEN: ${{ github.token }} GH_TOKEN: ${{ github.token }}
@@ -100,10 +75,13 @@ jobs:
labels: "bug" labels: "bug"
title: "[${{ inputs.conan_profile }}] reported issues" title: "[${{ inputs.conan_profile }}] reported issues"
body: > body: >
Clio tests failed one or more sanitizer checks when built with ${{ inputs.conan_profile }}`. Clio tests failed one or more sanitizer checks when built with `${{ inputs.conan_profile }}`.
Workflow: ${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}/ Workflow: ${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}/
Reports are available as artifacts.
- name: Fail the job if clio_tests failed
if: ${{ steps.run_clio_tests.outcome == 'failure' }}
run: exit 1
integration_tests: integration_tests:
name: Integration testing name: Integration testing
@@ -124,13 +102,19 @@ jobs:
steps: steps:
- name: Cleanup workspace - name: Cleanup workspace
if: ${{ runner.os == 'macOS' }} if: ${{ runner.os == 'macOS' }}
uses: XRPLF/actions/cleanup-workspace@cf0433aa74563aead044a1e395610c96d65a37cf uses: XRPLF/actions/cleanup-workspace@c7d9ce5ebb03c752a354889ecd870cadfc2b1cd4
- name: Spin up scylladb - name: Delete and start colima (macOS)
# This is a temporary workaround for colima issues on macOS runners
if: ${{ runner.os == 'macOS' }} if: ${{ runner.os == 'macOS' }}
timeout-minutes: 3
run: | run: |
docker rm --force scylladb || true colima delete --force
colima start
- name: Spin up scylladb (macOS)
if: ${{ runner.os == 'macOS' }}
timeout-minutes: 1
run: |
docker run \ docker run \
--detach \ --detach \
--name scylladb \ --name scylladb \
@@ -142,11 +126,15 @@ jobs:
--memory 16G \ --memory 16G \
scylladb/scylla scylladb/scylla
- name: Wait for scylladb container to be healthy (macOS)
if: ${{ runner.os == 'macOS' }}
timeout-minutes: 1
run: |
until [ "$(docker inspect -f '{{.State.Health.Status}}' scylladb)" == "healthy" ]; do until [ "$(docker inspect -f '{{.State.Health.Status}}' scylladb)" == "healthy" ]; do
sleep 5 sleep 1
done done
- uses: actions/download-artifact@37930b1c2abaa49bbe596cd826c3c89aef350131 # v7.0.0 - uses: actions/download-artifact@70fc10c6e5e1ce46ad2ea6f2b72d43f7d47b13c3 # v8.0.0
with: with:
name: clio_integration_tests_${{ runner.os }}_${{ inputs.build_type }}_${{ inputs.conan_profile }} name: clio_integration_tests_${{ runner.os }}_${{ inputs.build_type }}_${{ inputs.conan_profile }}

View File

@@ -16,12 +16,12 @@ jobs:
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1 - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
with: with:
fetch-depth: 0 fetch-depth: 0
- name: Download report artifact - name: Download report artifact
uses: actions/download-artifact@37930b1c2abaa49bbe596cd826c3c89aef350131 # v7.0.0 uses: actions/download-artifact@70fc10c6e5e1ce46ad2ea6f2b72d43f7d47b13c3 # v8.0.0
with: with:
name: coverage-report.xml name: coverage-report.xml
path: build path: build

View File

@@ -15,7 +15,6 @@ on:
- ".github/actions/**" - ".github/actions/**"
- "!.github/actions/build-docker-image/**" - "!.github/actions/build-docker-image/**"
- "!.github/actions/create-issue/**" - "!.github/actions/create-issue/**"
- .github/scripts/execute-tests-under-sanitizer.sh
- CMakeLists.txt - CMakeLists.txt
- conanfile.py - conanfile.py

View File

@@ -56,7 +56,7 @@ jobs:
needs: repo needs: repo
steps: steps:
- uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1 - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
- name: Get changed files - name: Get changed files
id: changed-files id: changed-files
@@ -94,7 +94,7 @@ jobs:
needs: repo needs: repo
steps: steps:
- uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1 - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
- name: Get changed files - name: Get changed files
id: changed-files id: changed-files
@@ -132,7 +132,7 @@ jobs:
needs: [repo, gcc-amd64, gcc-arm64] needs: [repo, gcc-amd64, gcc-arm64]
steps: steps:
- uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1 - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
- name: Get changed files - name: Get changed files
id: changed-files id: changed-files
@@ -145,7 +145,7 @@ jobs:
- name: Login to GitHub Container Registry - name: Login to GitHub Container Registry
if: ${{ github.event_name != 'pull_request' }} if: ${{ github.event_name != 'pull_request' }}
uses: docker/login-action@5e57cd118135c172c3672efd75eb46360885c0ef # v3.6.0 uses: docker/login-action@c94ce9fb468520275223c153574b00df6fe4bcc9 # v3.7.0
with: with:
registry: ghcr.io registry: ghcr.io
username: ${{ github.repository_owner }} username: ${{ github.repository_owner }}
@@ -153,7 +153,7 @@ jobs:
- name: Login to DockerHub - name: Login to DockerHub
if: ${{ github.repository_owner == 'XRPLF' && github.event_name != 'pull_request' }} if: ${{ github.repository_owner == 'XRPLF' && github.event_name != 'pull_request' }}
uses: docker/login-action@5e57cd118135c172c3672efd75eb46360885c0ef # v3.6.0 uses: docker/login-action@c94ce9fb468520275223c153574b00df6fe4bcc9 # v3.7.0
with: with:
username: ${{ secrets.DOCKERHUB_USER }} username: ${{ secrets.DOCKERHUB_USER }}
password: ${{ secrets.DOCKERHUB_PW }} password: ${{ secrets.DOCKERHUB_PW }}
@@ -183,7 +183,7 @@ jobs:
needs: repo needs: repo
steps: steps:
- uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1 - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
- name: Get changed files - name: Get changed files
id: changed-files id: changed-files
@@ -219,7 +219,7 @@ jobs:
needs: [repo, gcc-merge] needs: [repo, gcc-merge]
steps: steps:
- uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1 - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
- name: Get changed files - name: Get changed files
id: changed-files id: changed-files
@@ -250,7 +250,7 @@ jobs:
needs: [repo, gcc-merge] needs: [repo, gcc-merge]
steps: steps:
- uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1 - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
- name: Get changed files - name: Get changed files
id: changed-files id: changed-files
@@ -281,7 +281,7 @@ jobs:
needs: [repo, tools-amd64, tools-arm64] needs: [repo, tools-amd64, tools-arm64]
steps: steps:
- uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1 - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
- name: Get changed files - name: Get changed files
id: changed-files id: changed-files
@@ -294,7 +294,7 @@ jobs:
- name: Login to GitHub Container Registry - name: Login to GitHub Container Registry
if: ${{ github.event_name != 'pull_request' }} if: ${{ github.event_name != 'pull_request' }}
uses: docker/login-action@5e57cd118135c172c3672efd75eb46360885c0ef # v3.6.0 uses: docker/login-action@c94ce9fb468520275223c153574b00df6fe4bcc9 # v3.7.0
with: with:
registry: ghcr.io registry: ghcr.io
username: ${{ github.repository_owner }} username: ${{ github.repository_owner }}
@@ -316,7 +316,7 @@ jobs:
needs: [repo, tools-merge] needs: [repo, tools-merge]
steps: steps:
- uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1 - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
- uses: ./.github/actions/build-docker-image - uses: ./.github/actions/build-docker-image
env: env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
@@ -338,7 +338,7 @@ jobs:
needs: [repo, gcc-merge, clang, tools-merge] needs: [repo, gcc-merge, clang, tools-merge]
steps: steps:
- uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1 - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
- uses: ./.github/actions/build-docker-image - uses: ./.github/actions/build-docker-image
env: env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}

View File

@@ -52,7 +52,7 @@ jobs:
outputs: outputs:
matrix: ${{ steps.set-matrix.outputs.matrix }} matrix: ${{ steps.set-matrix.outputs.matrix }}
steps: steps:
- uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1 - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
- name: Calculate conan matrix - name: Calculate conan matrix
id: set-matrix id: set-matrix
@@ -75,10 +75,10 @@ jobs:
CONAN_PROFILE: ${{ matrix.compiler }}${{ matrix.sanitizer_ext }} CONAN_PROFILE: ${{ matrix.compiler }}${{ matrix.sanitizer_ext }}
steps: steps:
- uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1 - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
- name: Prepare runner - name: Prepare runner
uses: XRPLF/actions/prepare-runner@f05cab7b8541eee6473aa42beb9d2fe35608a190 uses: XRPLF/actions/prepare-runner@2cbf481018d930656e9276fcc20dc0e3a0be5b6d
with: with:
enable_ccache: false enable_ccache: false

3
.gitignore vendored
View File

@@ -4,8 +4,11 @@
.build .build
.cache .cache
.vscode .vscode
.zed
.python-version .python-version
.DS_Store .DS_Store
.sanitizer-report .sanitizer-report
CMakeUserPresets.json CMakeUserPresets.json
config.json config.json
CLAUDE.md
.claude/**

View File

@@ -29,7 +29,7 @@ repos:
# Autoformat: YAML, JSON, Markdown, etc. # Autoformat: YAML, JSON, Markdown, etc.
- repo: https://github.com/rbubley/mirrors-prettier - repo: https://github.com/rbubley/mirrors-prettier
rev: 14abee445aea04b39069c19b4bd54efff6775819 # frozen: v3.7.4 rev: c2bc67fe8f8f549cc489e00ba8b45aa18ee713b1 # frozen: v3.8.1
hooks: hooks:
- id: prettier - id: prettier
@@ -59,7 +59,7 @@ repos:
] ]
- repo: https://github.com/psf/black-pre-commit-mirror - repo: https://github.com/psf/black-pre-commit-mirror
rev: 831207fd435b47aeffdf6af853097e64322b4d44 # frozen: 25.12.0 rev: ea488cebbfd88a5f50b8bd95d5c829d0bb76feb8 # frozen: 26.1.0
hooks: hooks:
- id: black - id: black
@@ -94,7 +94,7 @@ repos:
language: script language: script
- repo: https://github.com/pre-commit/mirrors-clang-format - repo: https://github.com/pre-commit/mirrors-clang-format
rev: 75ca4ad908dc4a99f57921f29b7e6c1521e10b26 # frozen: v21.1.8 rev: cd481d7b0bfb5c7b3090c21846317f9a8262e891 # frozen: v22.1.0
hooks: hooks:
- id: clang-format - id: clang-format
args: [--style=file] args: [--style=file]

View File

@@ -1,8 +1,7 @@
cmake_minimum_required(VERSION 3.20) cmake_minimum_required(VERSION 3.20)
project(clio VERSION ${CLIO_VERSION} HOMEPAGE_URL "https://github.com/XRPLF/clio" project(clio VERSION ${CLIO_VERSION} HOMEPAGE_URL "https://github.com/XRPLF/clio"
DESCRIPTION "An XRP Ledger API Server" DESCRIPTION "An XRP Ledger API Server")
)
# =========================== Options ====================================== # # =========================== Options ====================================== #
option(verbose "Verbose build" FALSE) option(verbose "Verbose build" FALSE)
@@ -36,7 +35,7 @@ target_compile_features(clio_options INTERFACE cxx_std_23) # Clio needs c++23 bu
target_include_directories(clio_options INTERFACE ${CMAKE_SOURCE_DIR}/src) target_include_directories(clio_options INTERFACE ${CMAKE_SOURCE_DIR}/src)
if (verbose) if (verbose)
set(CMAKE_VERBOSE_MAKEFILE TRUE) set(CMAKE_VERBOSE_MAKEFILE TRUE)
endif () endif ()
# Clio tweaks and checks # Clio tweaks and checks
@@ -58,36 +57,36 @@ add_subdirectory(src)
add_subdirectory(tests) add_subdirectory(tests)
if (benchmark) if (benchmark)
add_subdirectory(benchmarks) add_subdirectory(benchmarks)
endif () endif ()
# Enable selected sanitizer if enabled via `san` # Enable selected sanitizer if enabled via `san`
if (san) if (san)
set(SUPPORTED_SANITIZERS "address" "thread" "memory" "undefined") set(SUPPORTED_SANITIZERS "address" "thread" "memory" "undefined")
if (NOT san IN_LIST SUPPORTED_SANITIZERS) if (NOT san IN_LIST SUPPORTED_SANITIZERS)
message(FATAL_ERROR "Error: Unsupported sanitizer '${san}'. Supported values are: ${SUPPORTED_SANITIZERS}.") message(FATAL_ERROR "Error: Unsupported sanitizer '${san}'. Supported values are: ${SUPPORTED_SANITIZERS}.")
endif () endif ()
# Sanitizers recommend minimum of -O1 for reasonable performance so we enable it for debug builds # Sanitizers recommend minimum of -O1 for reasonable performance so we enable it for debug builds
set(SAN_OPTIMIZATION_FLAG "") set(SAN_OPTIMIZATION_FLAG "")
if (CMAKE_BUILD_TYPE STREQUAL "Debug") if (CMAKE_BUILD_TYPE STREQUAL "Debug")
set(SAN_OPTIMIZATION_FLAG -O1) set(SAN_OPTIMIZATION_FLAG -O1)
endif () endif ()
target_compile_options(clio_options INTERFACE ${SAN_OPTIMIZATION_FLAG} ${SAN_FLAG} -fno-omit-frame-pointer) target_compile_options(clio_options INTERFACE ${SAN_OPTIMIZATION_FLAG} ${SAN_FLAG} -fno-omit-frame-pointer)
target_link_libraries(clio_options INTERFACE ${SAN_FLAG} ${SAN_LIB}) target_link_libraries(clio_options INTERFACE ${SAN_FLAG} ${SAN_LIB})
endif () endif ()
# Generate `docs` target for doxygen documentation if enabled Note: use `make docs` to generate the documentation # Generate `docs` target for doxygen documentation if enabled Note: use `make docs` to generate the documentation
if (docs) if (docs)
add_subdirectory(docs) add_subdirectory(docs)
endif () endif ()
include(install/install) include(install/install)
if (package) if (package)
include(ClioPackage) include(ClioPackage)
endif () endif ()
if (snapshot) if (snapshot)
add_subdirectory(tools/snapshot) add_subdirectory(tools/snapshot)
endif () endif ()

View File

@@ -1,20 +1,18 @@
add_executable(clio_benchmark) add_executable(clio_benchmark)
target_sources( target_sources(clio_benchmark
clio_benchmark PRIVATE # Common
PRIVATE # Common Main.cpp
Main.cpp Playground.cpp
Playground.cpp # ExecutionContext
# ExecutionContext util/async/ExecutionContextBenchmarks.cpp
util/async/ExecutionContextBenchmarks.cpp # Logger
# Logger util/log/LoggerBenchmark.cpp
util/log/LoggerBenchmark.cpp # WorkQueue
# WorkQueue rpc/WorkQueueBenchmarks.cpp)
rpc/WorkQueueBenchmarks.cpp
)
include(deps/gbench) include(deps/gbench)
target_include_directories(clio_benchmark PRIVATE .) target_include_directories(clio_benchmark PRIVATE .)
target_link_libraries(clio_benchmark PUBLIC clio_util clio_rpc benchmark::benchmark_main spdlog::spdlog) target_link_libraries(clio_benchmark PRIVATE clio_rpc clio_util benchmark::benchmark_main spdlog::spdlog)
set_target_properties(clio_benchmark PROPERTIES RUNTIME_OUTPUT_DIRECTORY ${CMAKE_BINARY_DIR}) set_target_properties(clio_benchmark PROPERTIES RUNTIME_OUTPUT_DIRECTORY ${CMAKE_BINARY_DIR})

View File

@@ -29,8 +29,6 @@
#include <benchmark/benchmark.h> #include <benchmark/benchmark.h>
#include <boost/asio/steady_timer.hpp> #include <boost/asio/steady_timer.hpp>
#include <boost/asio/thread_pool.hpp>
#include <boost/json/object.hpp>
#include <algorithm> #include <algorithm>
#include <atomic> #include <atomic>
@@ -53,12 +51,15 @@ auto const kCONFIG = ClioConfigDefinition{
{"log.channels.[].channel", Array{ConfigValue{ConfigType::String}}}, {"log.channels.[].channel", Array{ConfigValue{ConfigType::String}}},
{"log.channels.[].level", Array{ConfigValue{ConfigType::String}}}, {"log.channels.[].level", Array{ConfigValue{ConfigType::String}}},
{"log.level", ConfigValue{ConfigType::String}.defaultValue("info")}, {"log.level", ConfigValue{ConfigType::String}.defaultValue("info")},
{"log.format", ConfigValue{ConfigType::String}.defaultValue(R"(%Y-%m-%d %H:%M:%S.%f %^%3!l:%n%$ - %v)")}, {"log.format",
ConfigValue{ConfigType::String}.defaultValue(R"(%Y-%m-%d %H:%M:%S.%f %^%3!l:%n%$ - %v)")},
{"log.is_async", ConfigValue{ConfigType::Boolean}.defaultValue(false)}, {"log.is_async", ConfigValue{ConfigType::Boolean}.defaultValue(false)},
{"log.enable_console", ConfigValue{ConfigType::Boolean}.defaultValue(false)}, {"log.enable_console", ConfigValue{ConfigType::Boolean}.defaultValue(false)},
{"log.directory", ConfigValue{ConfigType::String}.optional()}, {"log.directory", ConfigValue{ConfigType::String}.optional()},
{"log.rotation_size", ConfigValue{ConfigType::Integer}.defaultValue(2048).withConstraint(gValidateUint32)}, {"log.rotation_size",
{"log.directory_max_files", ConfigValue{ConfigType::Integer}.defaultValue(25).withConstraint(gValidateUint32)}, ConfigValue{ConfigType::Integer}.defaultValue(2048).withConstraint(gValidateUint32)},
{"log.directory_max_files",
ConfigValue{ConfigType::Integer}.defaultValue(25).withConstraint(gValidateUint32)},
{"log.tag_style", ConfigValue{ConfigType::String}.defaultValue("none")}, {"log.tag_style", ConfigValue{ConfigType::String}.defaultValue("none")},
}; };
@@ -126,9 +127,14 @@ benchmarkWorkQueue(benchmark::State& state)
ASSERT(totalQueued <= itemsPerClient * clientThreads, "Queued more than requested"); ASSERT(totalQueued <= itemsPerClient * clientThreads, "Queued more than requested");
if (maxQueueSize == 0) { if (maxQueueSize == 0) {
ASSERT(totalQueued == itemsPerClient * clientThreads, "Queued exactly the expected amount"); ASSERT(
totalQueued == itemsPerClient * clientThreads, "Queued exactly the expected amount"
);
} else { } else {
ASSERT(totalQueued >= std::min(maxQueueSize, itemsPerClient * clientThreads), "Queued less than expected"); ASSERT(
totalQueued >= std::min(maxQueueSize, itemsPerClient * clientThreads),
"Queued less than expected"
);
} }
} }
} }

View File

@@ -62,8 +62,9 @@ uniqueLogDir()
{ {
auto const epochTime = std::chrono::high_resolution_clock::now().time_since_epoch(); auto const epochTime = std::chrono::high_resolution_clock::now().time_since_epoch();
auto const tmpDir = std::filesystem::temp_directory_path(); auto const tmpDir = std::filesystem::temp_directory_path();
std::string const dirName = std::string const dirName = fmt::format(
fmt::format("logs_{}", std::chrono::duration_cast<std::chrono::microseconds>(epochTime).count()); "logs_{}", std::chrono::duration_cast<std::chrono::microseconds>(epochTime).count()
);
return tmpDir / "clio_benchmark" / dirName; return tmpDir / "clio_benchmark" / dirName;
} }
@@ -108,7 +109,8 @@ benchmarkConcurrentFileLogging(benchmark::State& state)
channel, fileSink, spdlog::thread_pool(), spdlog::async_overflow_policy::block channel, fileSink, spdlog::thread_pool(), spdlog::async_overflow_policy::block
); );
spdlog::register_logger(logger); spdlog::register_logger(logger);
Logger const threadLogger = BenchmarkLoggingInitializer::getLogger(std::move(logger)); Logger const threadLogger =
BenchmarkLoggingInitializer::getLogger(std::move(logger));
barrier.arrive_and_wait(); barrier.arrive_and_wait();
@@ -124,13 +126,16 @@ benchmarkConcurrentFileLogging(benchmark::State& state)
spdlog::shutdown(); spdlog::shutdown();
auto const end = std::chrono::high_resolution_clock::now(); auto const end = std::chrono::high_resolution_clock::now();
state.SetIterationTime(std::chrono::duration_cast<std::chrono::duration<double>>(end - start).count()); state.SetIterationTime(
std::chrono::duration_cast<std::chrono::duration<double>>(end - start).count()
);
std::filesystem::remove_all(logDir); std::filesystem::remove_all(logDir);
} }
auto const totalMessages = numThreads * messagesPerThread; auto const totalMessages = numThreads * messagesPerThread;
state.counters["TotalMessagesRate"] = benchmark::Counter(totalMessages, benchmark::Counter::kIsRate); state.counters["TotalMessagesRate"] =
benchmark::Counter(totalMessages, benchmark::Counter::kIsRate);
state.counters["Threads"] = numThreads; state.counters["Threads"] = numThreads;
state.counters["MessagesPerThread"] = messagesPerThread; state.counters["MessagesPerThread"] = messagesPerThread;
} }

View File

@@ -1,5 +1,5 @@
find_program(CCACHE_PATH "ccache") find_program(CCACHE_PATH "ccache")
if (CCACHE_PATH) if (CCACHE_PATH)
set(CMAKE_CXX_COMPILER_LAUNCHER "${CCACHE_PATH}") set(CMAKE_CXX_COMPILER_LAUNCHER "${CCACHE_PATH}")
message(STATUS "Using ccache: ${CCACHE_PATH}") message(STATUS "Using ccache: ${CCACHE_PATH}")
endif () endif ()

View File

@@ -1,42 +1,42 @@
if (CMAKE_CXX_COMPILER_ID STREQUAL "Clang") if (CMAKE_CXX_COMPILER_ID STREQUAL "Clang")
if (CMAKE_CXX_COMPILER_VERSION VERSION_LESS 16) if (CMAKE_CXX_COMPILER_VERSION VERSION_LESS 16)
message(FATAL_ERROR "Clang 16+ required for building clio") message(FATAL_ERROR "Clang 16+ required for building clio")
endif () endif ()
set(is_clang TRUE) set(is_clang TRUE)
elseif (CMAKE_CXX_COMPILER_ID STREQUAL "AppleClang") elseif (CMAKE_CXX_COMPILER_ID STREQUAL "AppleClang")
if (CMAKE_CXX_COMPILER_VERSION VERSION_LESS 15) if (CMAKE_CXX_COMPILER_VERSION VERSION_LESS 15)
message(FATAL_ERROR "AppleClang 15+ required for building clio") message(FATAL_ERROR "AppleClang 15+ required for building clio")
endif () endif ()
set(is_appleclang TRUE) set(is_appleclang TRUE)
elseif (CMAKE_CXX_COMPILER_ID STREQUAL "GNU") elseif (CMAKE_CXX_COMPILER_ID STREQUAL "GNU")
if (CMAKE_CXX_COMPILER_VERSION VERSION_LESS 12) if (CMAKE_CXX_COMPILER_VERSION VERSION_LESS 12)
message(FATAL_ERROR "GCC 12+ required for building clio") message(FATAL_ERROR "GCC 12+ required for building clio")
endif () endif ()
set(is_gcc TRUE) set(is_gcc TRUE)
else () else ()
message(FATAL_ERROR "Supported compilers: AppleClang 15+, Clang 16+, GCC 12+") message(FATAL_ERROR "Supported compilers: AppleClang 15+, Clang 16+, GCC 12+")
endif () endif ()
if (san) if (san)
string(TOLOWER ${san} san) string(TOLOWER ${san} san)
set(SAN_FLAG "-fsanitize=${san}") set(SAN_FLAG "-fsanitize=${san}")
set(SAN_LIB "") set(SAN_LIB "")
if (is_gcc) if (is_gcc)
if (san STREQUAL "address") if (san STREQUAL "address")
set(SAN_LIB "asan") set(SAN_LIB "asan")
elseif (san STREQUAL "thread") elseif (san STREQUAL "thread")
set(SAN_LIB "tsan") set(SAN_LIB "tsan")
elseif (san STREQUAL "memory") elseif (san STREQUAL "memory")
set(SAN_LIB "msan") set(SAN_LIB "msan")
elseif (san STREQUAL "undefined") elseif (san STREQUAL "undefined")
set(SAN_LIB "ubsan") set(SAN_LIB "ubsan")
endif ()
endif ()
set(_saved_CRL ${CMAKE_REQUIRED_LIBRARIES})
set(CMAKE_REQUIRED_LIBRARIES "${SAN_FLAG};${SAN_LIB}")
check_cxx_compiler_flag(${SAN_FLAG} COMPILER_SUPPORTS_SAN)
set(CMAKE_REQUIRED_LIBRARIES ${_saved_CRL})
if (NOT COMPILER_SUPPORTS_SAN)
message(FATAL_ERROR "${san} sanitizer does not seem to be supported by your compiler")
endif () endif ()
endif ()
set(_saved_CRL ${CMAKE_REQUIRED_LIBRARIES})
set(CMAKE_REQUIRED_LIBRARIES "${SAN_FLAG};${SAN_LIB}")
check_cxx_compiler_flag(${SAN_FLAG} COMPILER_SUPPORTS_SAN)
set(CMAKE_REQUIRED_LIBRARIES ${_saved_CRL})
if (NOT COMPILER_SUPPORTS_SAN)
message(FATAL_ERROR "${san} sanitizer does not seem to be supported by your compiler")
endif ()
endif () endif ()

View File

@@ -1,33 +1,30 @@
if (lint) if (lint)
# Find clang-tidy binary # Find clang-tidy binary
if (DEFINED ENV{CLIO_CLANG_TIDY_BIN}) if (DEFINED ENV{CLIO_CLANG_TIDY_BIN})
set(_CLANG_TIDY_BIN $ENV{CLIO_CLANG_TIDY_BIN}) set(_CLANG_TIDY_BIN $ENV{CLIO_CLANG_TIDY_BIN})
if ((NOT EXISTS ${_CLANG_TIDY_BIN}) OR IS_DIRECTORY ${_CLANG_TIDY_BIN}) if ((NOT EXISTS ${_CLANG_TIDY_BIN}) OR IS_DIRECTORY ${_CLANG_TIDY_BIN})
message(FATAL_ERROR "$ENV{CLIO_CLANG_TIDY_BIN} no such file. Check CLIO_CLANG_TIDY_BIN env variable") message(FATAL_ERROR "$ENV{CLIO_CLANG_TIDY_BIN} no such file. Check CLIO_CLANG_TIDY_BIN env variable")
endif ()
message(STATUS "Using clang-tidy from CLIO_CLANG_TIDY_BIN")
else ()
find_program(_CLANG_TIDY_BIN NAMES "clang-tidy-20" "clang-tidy" REQUIRED)
endif () endif ()
message(STATUS "Using clang-tidy from CLIO_CLANG_TIDY_BIN")
else ()
find_program(_CLANG_TIDY_BIN NAMES "clang-tidy-20" "clang-tidy" REQUIRED)
endif ()
if (NOT _CLANG_TIDY_BIN) if (NOT _CLANG_TIDY_BIN)
message( message(FATAL_ERROR "clang-tidy binary not found. Please set the CLIO_CLANG_TIDY_BIN environment variable or install clang-tidy."
FATAL_ERROR )
"clang-tidy binary not found. Please set the CLIO_CLANG_TIDY_BIN environment variable or install clang-tidy." endif ()
)
endif ()
# Support for https://github.com/matus-chochlik/ctcache # Support for https://github.com/matus-chochlik/ctcache
find_program(CLANG_TIDY_CACHE_PATH NAMES "clang-tidy-cache") find_program(CLANG_TIDY_CACHE_PATH NAMES "clang-tidy-cache")
if (CLANG_TIDY_CACHE_PATH) if (CLANG_TIDY_CACHE_PATH)
set(_CLANG_TIDY_CMD "${CLANG_TIDY_CACHE_PATH};${_CLANG_TIDY_BIN}" set(_CLANG_TIDY_CMD "${CLANG_TIDY_CACHE_PATH};${_CLANG_TIDY_BIN}"
CACHE STRING "A combined command to run clang-tidy with caching wrapper" CACHE STRING "A combined command to run clang-tidy with caching wrapper")
) else ()
else () set(_CLANG_TIDY_CMD "${_CLANG_TIDY_BIN}")
set(_CLANG_TIDY_CMD "${_CLANG_TIDY_BIN}") endif ()
endif ()
set(CMAKE_CXX_CLANG_TIDY "${_CLANG_TIDY_CMD};--quiet") set(CMAKE_CXX_CLANG_TIDY "${_CLANG_TIDY_CMD};--quiet")
message(STATUS "Using clang-tidy: ${CMAKE_CXX_CLANG_TIDY}") message(STATUS "Using clang-tidy: ${CMAKE_CXX_CLANG_TIDY}")
endif () endif ()

View File

@@ -1,47 +1,41 @@
find_package(Git REQUIRED) find_package(Git REQUIRED)
if (DEFINED ENV{GITHUB_BRANCH_NAME}) if (DEFINED ENV{GITHUB_BRANCH_NAME})
set(GIT_BUILD_BRANCH $ENV{GITHUB_BRANCH_NAME}) set(GIT_BUILD_BRANCH $ENV{GITHUB_BRANCH_NAME})
set(GIT_COMMIT_HASH $ENV{GITHUB_HEAD_SHA}) set(GIT_COMMIT_HASH $ENV{GITHUB_HEAD_SHA})
else () else ()
set(GIT_COMMAND branch --show-current) set(GIT_COMMAND branch --show-current)
execute_process( execute_process(COMMAND ${GIT_EXECUTABLE} ${GIT_COMMAND} WORKING_DIRECTORY ${CMAKE_SOURCE_DIR}
COMMAND ${GIT_EXECUTABLE} ${GIT_COMMAND} WORKING_DIRECTORY ${CMAKE_SOURCE_DIR} OUTPUT_VARIABLE GIT_BUILD_BRANCH OUTPUT_VARIABLE GIT_BUILD_BRANCH OUTPUT_STRIP_TRAILING_WHITESPACE COMMAND_ERROR_IS_FATAL ANY)
OUTPUT_STRIP_TRAILING_WHITESPACE COMMAND_ERROR_IS_FATAL ANY
)
set(GIT_COMMAND rev-parse HEAD) set(GIT_COMMAND rev-parse HEAD)
execute_process( execute_process(COMMAND ${GIT_EXECUTABLE} ${GIT_COMMAND} WORKING_DIRECTORY ${CMAKE_SOURCE_DIR}
COMMAND ${GIT_EXECUTABLE} ${GIT_COMMAND} WORKING_DIRECTORY ${CMAKE_SOURCE_DIR} OUTPUT_VARIABLE GIT_COMMIT_HASH OUTPUT_VARIABLE GIT_COMMIT_HASH OUTPUT_STRIP_TRAILING_WHITESPACE COMMAND_ERROR_IS_FATAL ANY)
OUTPUT_STRIP_TRAILING_WHITESPACE COMMAND_ERROR_IS_FATAL ANY
)
endif () endif ()
execute_process( execute_process(COMMAND date +%Y%m%d%H%M%S WORKING_DIRECTORY ${CMAKE_SOURCE_DIR} OUTPUT_VARIABLE BUILD_DATE
COMMAND date +%Y%m%d%H%M%S WORKING_DIRECTORY ${CMAKE_SOURCE_DIR} OUTPUT_VARIABLE BUILD_DATE OUTPUT_STRIP_TRAILING_WHITESPACE COMMAND_ERROR_IS_FATAL ANY)
OUTPUT_STRIP_TRAILING_WHITESPACE COMMAND_ERROR_IS_FATAL ANY
)
message(STATUS "Git branch: ${GIT_BUILD_BRANCH}") message(STATUS "Git branch: ${GIT_BUILD_BRANCH}")
message(STATUS "Git commit hash: ${GIT_COMMIT_HASH}") message(STATUS "Git commit hash: ${GIT_COMMIT_HASH}")
message(STATUS "Build date: ${BUILD_DATE}") message(STATUS "Build date: ${BUILD_DATE}")
if (DEFINED ENV{FORCE_CLIO_VERSION} AND NOT "$ENV{FORCE_CLIO_VERSION}" STREQUAL "") if (DEFINED ENV{FORCE_CLIO_VERSION} AND NOT "$ENV{FORCE_CLIO_VERSION}" STREQUAL "")
message(STATUS "Using explicitly provided '${FORCE_CLIO_VERSION}' as Clio version") message(STATUS "Using explicitly provided '${FORCE_CLIO_VERSION}' as Clio version")
set(CLIO_VERSION "$ENV{FORCE_CLIO_VERSION}") set(CLIO_VERSION "$ENV{FORCE_CLIO_VERSION}")
set(DOC_CLIO_VERSION "$ENV{FORCE_CLIO_VERSION}") set(DOC_CLIO_VERSION "$ENV{FORCE_CLIO_VERSION}")
else () else ()
message(STATUS "Using 'YYYYMMDDHMS-<branch>-<git short rev>' as Clio version") message(STATUS "Using 'YYYYMMDDHMS-<branch>-<git short rev>' as Clio version")
string(SUBSTRING ${GIT_COMMIT_HASH} 0 7 GIT_COMMIT_HASH_SHORT) string(SUBSTRING ${GIT_COMMIT_HASH} 0 7 GIT_COMMIT_HASH_SHORT)
set(CLIO_VERSION "${BUILD_DATE}-${GIT_BUILD_BRANCH}-${GIT_COMMIT_HASH_SHORT}") set(CLIO_VERSION "${BUILD_DATE}-${GIT_BUILD_BRANCH}-${GIT_COMMIT_HASH_SHORT}")
set(DOC_CLIO_VERSION "develop") set(DOC_CLIO_VERSION "develop")
endif () endif ()
if (CMAKE_BUILD_TYPE MATCHES Debug) if (CMAKE_BUILD_TYPE MATCHES Debug)
set(CLIO_VERSION "${CLIO_VERSION}+DEBUG") set(CLIO_VERSION "${CLIO_VERSION}+DEBUG")
endif () endif ()
message(STATUS "Build version: ${CLIO_VERSION}") message(STATUS "Build version: ${CLIO_VERSION}")

View File

@@ -107,76 +107,69 @@ option(CODE_COVERAGE_VERBOSE "Verbose information" FALSE)
find_program(GCOVR_PATH gcovr PATHS ${CMAKE_SOURCE_DIR}/scripts/test) find_program(GCOVR_PATH gcovr PATHS ${CMAKE_SOURCE_DIR}/scripts/test)
if (DEFINED CODE_COVERAGE_GCOV_TOOL) if (DEFINED CODE_COVERAGE_GCOV_TOOL)
set(GCOV_TOOL "${CODE_COVERAGE_GCOV_TOOL}") set(GCOV_TOOL "${CODE_COVERAGE_GCOV_TOOL}")
elseif (DEFINED ENV{CODE_COVERAGE_GCOV_TOOL}) elseif (DEFINED ENV{CODE_COVERAGE_GCOV_TOOL})
set(GCOV_TOOL "$ENV{CODE_COVERAGE_GCOV_TOOL}") set(GCOV_TOOL "$ENV{CODE_COVERAGE_GCOV_TOOL}")
elseif ("${CMAKE_CXX_COMPILER_ID}" MATCHES "(Apple)?[Cc]lang") elseif ("${CMAKE_CXX_COMPILER_ID}" MATCHES "(Apple)?[Cc]lang")
if (APPLE) if (APPLE)
execute_process(COMMAND xcrun -f llvm-cov OUTPUT_VARIABLE LLVMCOV_PATH OUTPUT_STRIP_TRAILING_WHITESPACE) execute_process(COMMAND xcrun -f llvm-cov OUTPUT_VARIABLE LLVMCOV_PATH OUTPUT_STRIP_TRAILING_WHITESPACE)
else () else ()
find_program(LLVMCOV_PATH llvm-cov) find_program(LLVMCOV_PATH llvm-cov)
endif () endif ()
if (LLVMCOV_PATH) if (LLVMCOV_PATH)
set(GCOV_TOOL "${LLVMCOV_PATH} gcov") set(GCOV_TOOL "${LLVMCOV_PATH} gcov")
endif () endif ()
elseif ("${CMAKE_CXX_COMPILER_ID}" MATCHES "GNU") elseif ("${CMAKE_CXX_COMPILER_ID}" MATCHES "GNU")
find_program(GCOV_PATH gcov) find_program(GCOV_PATH gcov)
set(GCOV_TOOL "${GCOV_PATH}") set(GCOV_TOOL "${GCOV_PATH}")
endif () endif ()
# Check supported compiler (Clang, GNU and Flang) # Check supported compiler (Clang, GNU and Flang)
get_property(LANGUAGES GLOBAL PROPERTY ENABLED_LANGUAGES) get_property(LANGUAGES GLOBAL PROPERTY ENABLED_LANGUAGES)
foreach (LANG ${LANGUAGES}) foreach (LANG ${LANGUAGES})
if ("${CMAKE_${LANG}_COMPILER_ID}" MATCHES "(Apple)?[Cc]lang") if ("${CMAKE_${LANG}_COMPILER_ID}" MATCHES "(Apple)?[Cc]lang")
if ("${CMAKE_${LANG}_COMPILER_VERSION}" VERSION_LESS 3) if ("${CMAKE_${LANG}_COMPILER_VERSION}" VERSION_LESS 3)
message(FATAL_ERROR "Clang version must be 3.0.0 or greater! Aborting...") message(FATAL_ERROR "Clang version must be 3.0.0 or greater! Aborting...")
endif ()
elseif (NOT "${CMAKE_${LANG}_COMPILER_ID}" MATCHES "GNU" AND NOT "${CMAKE_${LANG}_COMPILER_ID}" MATCHES
"(LLVM)?[Ff]lang")
message(FATAL_ERROR "Compiler is not GNU or Flang! Aborting...")
endif () endif ()
elseif (NOT "${CMAKE_${LANG}_COMPILER_ID}" MATCHES "GNU" AND NOT "${CMAKE_${LANG}_COMPILER_ID}" MATCHES
"(LLVM)?[Ff]lang"
)
message(FATAL_ERROR "Compiler is not GNU or Flang! Aborting...")
endif ()
endforeach () endforeach ()
set(COVERAGE_COMPILER_FLAGS "-g --coverage" CACHE INTERNAL "") set(COVERAGE_COMPILER_FLAGS "-g --coverage" CACHE INTERNAL "")
if (CMAKE_CXX_COMPILER_ID MATCHES "(GNU|Clang)") if (CMAKE_CXX_COMPILER_ID MATCHES "(GNU|Clang)")
include(CheckCXXCompilerFlag) include(CheckCXXCompilerFlag)
check_cxx_compiler_flag(-fprofile-abs-path HAVE_cxx_fprofile_abs_path) check_cxx_compiler_flag(-fprofile-abs-path HAVE_cxx_fprofile_abs_path)
if (HAVE_cxx_fprofile_abs_path) if (HAVE_cxx_fprofile_abs_path)
set(COVERAGE_CXX_COMPILER_FLAGS "${COVERAGE_COMPILER_FLAGS} -fprofile-abs-path") set(COVERAGE_CXX_COMPILER_FLAGS "${COVERAGE_COMPILER_FLAGS} -fprofile-abs-path")
endif () endif ()
include(CheckCCompilerFlag) include(CheckCCompilerFlag)
check_c_compiler_flag(-fprofile-abs-path HAVE_c_fprofile_abs_path) check_c_compiler_flag(-fprofile-abs-path HAVE_c_fprofile_abs_path)
if (HAVE_c_fprofile_abs_path) if (HAVE_c_fprofile_abs_path)
set(COVERAGE_C_COMPILER_FLAGS "${COVERAGE_COMPILER_FLAGS} -fprofile-abs-path") set(COVERAGE_C_COMPILER_FLAGS "${COVERAGE_COMPILER_FLAGS} -fprofile-abs-path")
endif () endif ()
endif () endif ()
set(CMAKE_Fortran_FLAGS_COVERAGE ${COVERAGE_COMPILER_FLAGS} set(CMAKE_Fortran_FLAGS_COVERAGE ${COVERAGE_COMPILER_FLAGS}
CACHE STRING "Flags used by the Fortran compiler during coverage builds." FORCE CACHE STRING "Flags used by the Fortran compiler during coverage builds." FORCE)
)
set(CMAKE_CXX_FLAGS_COVERAGE ${COVERAGE_COMPILER_FLAGS} set(CMAKE_CXX_FLAGS_COVERAGE ${COVERAGE_COMPILER_FLAGS}
CACHE STRING "Flags used by the C++ compiler during coverage builds." FORCE CACHE STRING "Flags used by the C++ compiler during coverage builds." FORCE)
)
set(CMAKE_C_FLAGS_COVERAGE ${COVERAGE_COMPILER_FLAGS} set(CMAKE_C_FLAGS_COVERAGE ${COVERAGE_COMPILER_FLAGS}
CACHE STRING "Flags used by the C compiler during coverage builds." FORCE CACHE STRING "Flags used by the C compiler during coverage builds." FORCE)
)
set(CMAKE_EXE_LINKER_FLAGS_COVERAGE "" CACHE STRING "Flags used for linking binaries during coverage builds." FORCE) set(CMAKE_EXE_LINKER_FLAGS_COVERAGE "" CACHE STRING "Flags used for linking binaries during coverage builds." FORCE)
set(CMAKE_SHARED_LINKER_FLAGS_COVERAGE "" set(CMAKE_SHARED_LINKER_FLAGS_COVERAGE ""
CACHE STRING "Flags used by the shared libraries linker during coverage builds." FORCE CACHE STRING "Flags used by the shared libraries linker during coverage builds." FORCE)
) mark_as_advanced(CMAKE_Fortran_FLAGS_COVERAGE CMAKE_CXX_FLAGS_COVERAGE CMAKE_C_FLAGS_COVERAGE
mark_as_advanced( CMAKE_EXE_LINKER_FLAGS_COVERAGE CMAKE_SHARED_LINKER_FLAGS_COVERAGE)
CMAKE_Fortran_FLAGS_COVERAGE CMAKE_CXX_FLAGS_COVERAGE CMAKE_C_FLAGS_COVERAGE CMAKE_EXE_LINKER_FLAGS_COVERAGE
CMAKE_SHARED_LINKER_FLAGS_COVERAGE
)
get_property(GENERATOR_IS_MULTI_CONFIG GLOBAL PROPERTY GENERATOR_IS_MULTI_CONFIG) get_property(GENERATOR_IS_MULTI_CONFIG GLOBAL PROPERTY GENERATOR_IS_MULTI_CONFIG)
if (NOT (CMAKE_BUILD_TYPE STREQUAL "Debug" OR GENERATOR_IS_MULTI_CONFIG)) if (NOT (CMAKE_BUILD_TYPE STREQUAL "Debug" OR GENERATOR_IS_MULTI_CONFIG))
message(WARNING "Code coverage results with an optimised (non-Debug) build may be misleading") message(WARNING "Code coverage results with an optimised (non-Debug) build may be misleading")
endif () # NOT (CMAKE_BUILD_TYPE STREQUAL "Debug" OR GENERATOR_IS_MULTI_CONFIG) endif () # NOT (CMAKE_BUILD_TYPE STREQUAL "Debug" OR GENERATOR_IS_MULTI_CONFIG)
if (CMAKE_C_COMPILER_ID STREQUAL "GNU" OR CMAKE_Fortran_COMPILER_ID STREQUAL "GNU") if (CMAKE_C_COMPILER_ID STREQUAL "GNU" OR CMAKE_Fortran_COMPILER_ID STREQUAL "GNU")
link_libraries(gcov) link_libraries(gcov)
endif () endif ()
# Defines a target for running and collection code coverage information Builds dependencies, runs the given executable # Defines a target for running and collection code coverage information Builds dependencies, runs the given executable
@@ -192,170 +185,164 @@ endif ()
# with CMake 3.4+) ) The user can set the variable GCOVR_ADDITIONAL_ARGS to supply additional flags to the GCVOR # with CMake 3.4+) ) The user can set the variable GCOVR_ADDITIONAL_ARGS to supply additional flags to the GCVOR
# command. # command.
function (setup_target_for_coverage_gcovr) function (setup_target_for_coverage_gcovr)
set(options NONE) set(options NONE)
set(oneValueArgs BASE_DIRECTORY NAME FORMAT) set(oneValueArgs BASE_DIRECTORY NAME FORMAT)
set(multiValueArgs EXCLUDE EXECUTABLE EXECUTABLE_ARGS DEPENDENCIES) set(multiValueArgs EXCLUDE EXECUTABLE EXECUTABLE_ARGS DEPENDENCIES)
cmake_parse_arguments(Coverage "${options}" "${oneValueArgs}" "${multiValueArgs}" ${ARGN}) cmake_parse_arguments(Coverage "${options}" "${oneValueArgs}" "${multiValueArgs}" ${ARGN})
if (NOT GCOV_TOOL) if (NOT GCOV_TOOL)
message(FATAL_ERROR "Could not find gcov or llvm-cov tool! Aborting...") message(FATAL_ERROR "Could not find gcov or llvm-cov tool! Aborting...")
endif () endif ()
if (NOT GCOVR_PATH) if (NOT GCOVR_PATH)
message(FATAL_ERROR "Could not find gcovr tool! Aborting...") message(FATAL_ERROR "Could not find gcovr tool! Aborting...")
endif () endif ()
# Set base directory (as absolute path), or default to PROJECT_SOURCE_DIR # Set base directory (as absolute path), or default to PROJECT_SOURCE_DIR
if (DEFINED Coverage_BASE_DIRECTORY) if (DEFINED Coverage_BASE_DIRECTORY)
get_filename_component(BASEDIR ${Coverage_BASE_DIRECTORY} ABSOLUTE) get_filename_component(BASEDIR ${Coverage_BASE_DIRECTORY} ABSOLUTE)
else ()
set(BASEDIR ${PROJECT_SOURCE_DIR})
endif ()
if (NOT DEFINED Coverage_FORMAT)
set(Coverage_FORMAT xml)
endif ()
if ("--output" IN_LIST GCOVR_ADDITIONAL_ARGS)
message(FATAL_ERROR "Unsupported --output option detected in GCOVR_ADDITIONAL_ARGS! Aborting...")
else ()
if ((Coverage_FORMAT STREQUAL "html-details") OR (Coverage_FORMAT STREQUAL "html-nested"))
set(GCOVR_OUTPUT_FILE ${PROJECT_BINARY_DIR}/${Coverage_NAME}/index.html)
set(GCOVR_CREATE_FOLDER ${PROJECT_BINARY_DIR}/${Coverage_NAME})
elseif (Coverage_FORMAT STREQUAL "html-single")
set(GCOVR_OUTPUT_FILE ${Coverage_NAME}.html)
elseif ((Coverage_FORMAT STREQUAL "json-summary") OR (Coverage_FORMAT STREQUAL "json-details")
OR (Coverage_FORMAT STREQUAL "coveralls")
)
set(GCOVR_OUTPUT_FILE ${Coverage_NAME}.json)
elseif (Coverage_FORMAT STREQUAL "txt")
set(GCOVR_OUTPUT_FILE ${Coverage_NAME}.txt)
elseif (Coverage_FORMAT STREQUAL "csv")
set(GCOVR_OUTPUT_FILE ${Coverage_NAME}.csv)
else () else ()
set(GCOVR_OUTPUT_FILE ${Coverage_NAME}.xml) set(BASEDIR ${PROJECT_SOURCE_DIR})
endif ()
endif ()
if ((Coverage_FORMAT STREQUAL "cobertura") OR (Coverage_FORMAT STREQUAL "xml"))
list(APPEND GCOVR_ADDITIONAL_ARGS --cobertura "${GCOVR_OUTPUT_FILE}")
list(APPEND GCOVR_ADDITIONAL_ARGS --cobertura-pretty)
set(Coverage_FORMAT cobertura) # overwrite xml
elseif (Coverage_FORMAT STREQUAL "sonarqube")
list(APPEND GCOVR_ADDITIONAL_ARGS --sonarqube "${GCOVR_OUTPUT_FILE}")
elseif (Coverage_FORMAT STREQUAL "json-summary")
list(APPEND GCOVR_ADDITIONAL_ARGS --json-summary "${GCOVR_OUTPUT_FILE}")
list(APPEND GCOVR_ADDITIONAL_ARGS --json-summary-pretty)
elseif (Coverage_FORMAT STREQUAL "json-details")
list(APPEND GCOVR_ADDITIONAL_ARGS --json "${GCOVR_OUTPUT_FILE}")
list(APPEND GCOVR_ADDITIONAL_ARGS --json-pretty)
elseif (Coverage_FORMAT STREQUAL "coveralls")
list(APPEND GCOVR_ADDITIONAL_ARGS --coveralls "${GCOVR_OUTPUT_FILE}")
list(APPEND GCOVR_ADDITIONAL_ARGS --coveralls-pretty)
elseif (Coverage_FORMAT STREQUAL "csv")
list(APPEND GCOVR_ADDITIONAL_ARGS --csv "${GCOVR_OUTPUT_FILE}")
elseif (Coverage_FORMAT STREQUAL "txt")
list(APPEND GCOVR_ADDITIONAL_ARGS --txt "${GCOVR_OUTPUT_FILE}")
elseif (Coverage_FORMAT STREQUAL "html-single")
list(APPEND GCOVR_ADDITIONAL_ARGS --html "${GCOVR_OUTPUT_FILE}")
list(APPEND GCOVR_ADDITIONAL_ARGS --html-self-contained)
elseif (Coverage_FORMAT STREQUAL "html-nested")
list(APPEND GCOVR_ADDITIONAL_ARGS --html-nested "${GCOVR_OUTPUT_FILE}")
elseif (Coverage_FORMAT STREQUAL "html-details")
list(APPEND GCOVR_ADDITIONAL_ARGS --html-details "${GCOVR_OUTPUT_FILE}")
else ()
message(FATAL_ERROR "Unsupported output style ${Coverage_FORMAT}! Aborting...")
endif ()
# Collect excludes (CMake 3.4+: Also compute absolute paths)
set(GCOVR_EXCLUDES "")
foreach (EXCLUDE ${Coverage_EXCLUDE} ${COVERAGE_EXCLUDES} ${COVERAGE_GCOVR_EXCLUDES})
if (CMAKE_VERSION VERSION_GREATER 3.4)
get_filename_component(EXCLUDE ${EXCLUDE} ABSOLUTE BASE_DIR ${BASEDIR})
endif ()
list(APPEND GCOVR_EXCLUDES "${EXCLUDE}")
endforeach ()
list(REMOVE_DUPLICATES GCOVR_EXCLUDES)
# Combine excludes to several -e arguments
set(GCOVR_EXCLUDE_ARGS "")
foreach (EXCLUDE ${GCOVR_EXCLUDES})
list(APPEND GCOVR_EXCLUDE_ARGS "-e")
list(APPEND GCOVR_EXCLUDE_ARGS "${EXCLUDE}")
endforeach ()
# Set up commands which will be run to generate coverage data Run tests
set(GCOVR_EXEC_TESTS_CMD ${Coverage_EXECUTABLE} ${Coverage_EXECUTABLE_ARGS})
# Create folder
if (DEFINED GCOVR_CREATE_FOLDER)
set(GCOVR_FOLDER_CMD ${CMAKE_COMMAND} -E make_directory ${GCOVR_CREATE_FOLDER})
else ()
set(GCOVR_FOLDER_CMD echo) # dummy
endif ()
# Running gcovr
set(GCOVR_CMD
${GCOVR_PATH}
--gcov-executable
${GCOV_TOOL}
--gcov-ignore-parse-errors=negative_hits.warn_once_per_file
-r
${BASEDIR}
${GCOVR_ADDITIONAL_ARGS}
${GCOVR_EXCLUDE_ARGS}
--object-directory=${PROJECT_BINARY_DIR}
)
if (CODE_COVERAGE_VERBOSE)
message(STATUS "Executed command report")
message(STATUS "Command to run tests: ")
string(REPLACE ";" " " GCOVR_EXEC_TESTS_CMD_SPACED "${GCOVR_EXEC_TESTS_CMD}")
message(STATUS "${GCOVR_EXEC_TESTS_CMD_SPACED}")
if (NOT GCOVR_FOLDER_CMD STREQUAL "echo")
message(STATUS "Command to create a folder: ")
string(REPLACE ";" " " GCOVR_FOLDER_CMD_SPACED "${GCOVR_FOLDER_CMD}")
message(STATUS "${GCOVR_FOLDER_CMD_SPACED}")
endif () endif ()
message(STATUS "Command to generate gcovr coverage data: ") if (NOT DEFINED Coverage_FORMAT)
string(REPLACE ";" " " GCOVR_CMD_SPACED "${GCOVR_CMD}") set(Coverage_FORMAT xml)
message(STATUS "${GCOVR_CMD_SPACED}") endif ()
endif ()
add_custom_target( if ("--output" IN_LIST GCOVR_ADDITIONAL_ARGS)
${Coverage_NAME} message(FATAL_ERROR "Unsupported --output option detected in GCOVR_ADDITIONAL_ARGS! Aborting...")
COMMAND ${GCOVR_EXEC_TESTS_CMD} else ()
COMMAND ${GCOVR_FOLDER_CMD} if ((Coverage_FORMAT STREQUAL "html-details") OR (Coverage_FORMAT STREQUAL "html-nested"))
COMMAND ${GCOVR_CMD} set(GCOVR_OUTPUT_FILE ${PROJECT_BINARY_DIR}/${Coverage_NAME}/index.html)
BYPRODUCTS ${GCOVR_OUTPUT_FILE} set(GCOVR_CREATE_FOLDER ${PROJECT_BINARY_DIR}/${Coverage_NAME})
WORKING_DIRECTORY ${PROJECT_BINARY_DIR} elseif (Coverage_FORMAT STREQUAL "html-single")
DEPENDS ${Coverage_DEPENDENCIES} set(GCOVR_OUTPUT_FILE ${Coverage_NAME}.html)
VERBATIM # Protect arguments to commands elseif ((Coverage_FORMAT STREQUAL "json-summary") OR (Coverage_FORMAT STREQUAL "json-details")
COMMENT "Running gcovr to produce code coverage report." OR (Coverage_FORMAT STREQUAL "coveralls"))
) set(GCOVR_OUTPUT_FILE ${Coverage_NAME}.json)
elseif (Coverage_FORMAT STREQUAL "txt")
set(GCOVR_OUTPUT_FILE ${Coverage_NAME}.txt)
elseif (Coverage_FORMAT STREQUAL "csv")
set(GCOVR_OUTPUT_FILE ${Coverage_NAME}.csv)
else ()
set(GCOVR_OUTPUT_FILE ${Coverage_NAME}.xml)
endif ()
endif ()
# Show info where to find the report if ((Coverage_FORMAT STREQUAL "cobertura") OR (Coverage_FORMAT STREQUAL "xml"))
add_custom_command( list(APPEND GCOVR_ADDITIONAL_ARGS --cobertura "${GCOVR_OUTPUT_FILE}")
TARGET ${Coverage_NAME} POST_BUILD COMMAND ; list(APPEND GCOVR_ADDITIONAL_ARGS --cobertura-pretty)
COMMENT "Code coverage report saved in ${GCOVR_OUTPUT_FILE} formatted as ${Coverage_FORMAT}" set(Coverage_FORMAT cobertura) # overwrite xml
) elseif (Coverage_FORMAT STREQUAL "sonarqube")
list(APPEND GCOVR_ADDITIONAL_ARGS --sonarqube "${GCOVR_OUTPUT_FILE}")
elseif (Coverage_FORMAT STREQUAL "json-summary")
list(APPEND GCOVR_ADDITIONAL_ARGS --json-summary "${GCOVR_OUTPUT_FILE}")
list(APPEND GCOVR_ADDITIONAL_ARGS --json-summary-pretty)
elseif (Coverage_FORMAT STREQUAL "json-details")
list(APPEND GCOVR_ADDITIONAL_ARGS --json "${GCOVR_OUTPUT_FILE}")
list(APPEND GCOVR_ADDITIONAL_ARGS --json-pretty)
elseif (Coverage_FORMAT STREQUAL "coveralls")
list(APPEND GCOVR_ADDITIONAL_ARGS --coveralls "${GCOVR_OUTPUT_FILE}")
list(APPEND GCOVR_ADDITIONAL_ARGS --coveralls-pretty)
elseif (Coverage_FORMAT STREQUAL "csv")
list(APPEND GCOVR_ADDITIONAL_ARGS --csv "${GCOVR_OUTPUT_FILE}")
elseif (Coverage_FORMAT STREQUAL "txt")
list(APPEND GCOVR_ADDITIONAL_ARGS --txt "${GCOVR_OUTPUT_FILE}")
elseif (Coverage_FORMAT STREQUAL "html-single")
list(APPEND GCOVR_ADDITIONAL_ARGS --html "${GCOVR_OUTPUT_FILE}")
list(APPEND GCOVR_ADDITIONAL_ARGS --html-self-contained)
elseif (Coverage_FORMAT STREQUAL "html-nested")
list(APPEND GCOVR_ADDITIONAL_ARGS --html-nested "${GCOVR_OUTPUT_FILE}")
elseif (Coverage_FORMAT STREQUAL "html-details")
list(APPEND GCOVR_ADDITIONAL_ARGS --html-details "${GCOVR_OUTPUT_FILE}")
else ()
message(FATAL_ERROR "Unsupported output style ${Coverage_FORMAT}! Aborting...")
endif ()
# Collect excludes (CMake 3.4+: Also compute absolute paths)
set(GCOVR_EXCLUDES "")
foreach (EXCLUDE ${Coverage_EXCLUDE} ${COVERAGE_EXCLUDES} ${COVERAGE_GCOVR_EXCLUDES})
if (CMAKE_VERSION VERSION_GREATER 3.4)
get_filename_component(EXCLUDE ${EXCLUDE} ABSOLUTE BASE_DIR ${BASEDIR})
endif ()
list(APPEND GCOVR_EXCLUDES "${EXCLUDE}")
endforeach ()
list(REMOVE_DUPLICATES GCOVR_EXCLUDES)
# Combine excludes to several -e arguments
set(GCOVR_EXCLUDE_ARGS "")
foreach (EXCLUDE ${GCOVR_EXCLUDES})
list(APPEND GCOVR_EXCLUDE_ARGS "-e")
list(APPEND GCOVR_EXCLUDE_ARGS "${EXCLUDE}")
endforeach ()
# Set up commands which will be run to generate coverage data Run tests
set(GCOVR_EXEC_TESTS_CMD ${Coverage_EXECUTABLE} ${Coverage_EXECUTABLE_ARGS})
# Create folder
if (DEFINED GCOVR_CREATE_FOLDER)
set(GCOVR_FOLDER_CMD ${CMAKE_COMMAND} -E make_directory ${GCOVR_CREATE_FOLDER})
else ()
set(GCOVR_FOLDER_CMD echo) # dummy
endif ()
# Running gcovr
set(GCOVR_CMD
${GCOVR_PATH}
--gcov-executable
${GCOV_TOOL}
--gcov-ignore-parse-errors=negative_hits.warn_once_per_file
-r
${BASEDIR}
${GCOVR_ADDITIONAL_ARGS}
${GCOVR_EXCLUDE_ARGS}
--object-directory=${PROJECT_BINARY_DIR})
if (CODE_COVERAGE_VERBOSE)
message(STATUS "Executed command report")
message(STATUS "Command to run tests: ")
string(REPLACE ";" " " GCOVR_EXEC_TESTS_CMD_SPACED "${GCOVR_EXEC_TESTS_CMD}")
message(STATUS "${GCOVR_EXEC_TESTS_CMD_SPACED}")
if (NOT GCOVR_FOLDER_CMD STREQUAL "echo")
message(STATUS "Command to create a folder: ")
string(REPLACE ";" " " GCOVR_FOLDER_CMD_SPACED "${GCOVR_FOLDER_CMD}")
message(STATUS "${GCOVR_FOLDER_CMD_SPACED}")
endif ()
message(STATUS "Command to generate gcovr coverage data: ")
string(REPLACE ";" " " GCOVR_CMD_SPACED "${GCOVR_CMD}")
message(STATUS "${GCOVR_CMD_SPACED}")
endif ()
add_custom_target(${Coverage_NAME}
COMMAND ${GCOVR_EXEC_TESTS_CMD}
COMMAND ${GCOVR_FOLDER_CMD}
COMMAND ${GCOVR_CMD}
BYPRODUCTS ${GCOVR_OUTPUT_FILE}
WORKING_DIRECTORY ${PROJECT_BINARY_DIR}
DEPENDS ${Coverage_DEPENDENCIES}
VERBATIM # Protect arguments to commands
COMMENT "Running gcovr to produce code coverage report.")
# Show info where to find the report
add_custom_command(TARGET ${Coverage_NAME} POST_BUILD COMMAND ;
COMMENT "Code coverage report saved in ${GCOVR_OUTPUT_FILE} formatted as ${Coverage_FORMAT}")
endfunction () # setup_target_for_coverage_gcovr endfunction () # setup_target_for_coverage_gcovr
function (append_coverage_compiler_flags) function (append_coverage_compiler_flags)
set(CMAKE_C_FLAGS "${CMAKE_C_FLAGS} ${COVERAGE_COMPILER_FLAGS}" PARENT_SCOPE) set(CMAKE_C_FLAGS "${CMAKE_C_FLAGS} ${COVERAGE_COMPILER_FLAGS}" PARENT_SCOPE)
set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} ${COVERAGE_COMPILER_FLAGS}" PARENT_SCOPE) set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} ${COVERAGE_COMPILER_FLAGS}" PARENT_SCOPE)
set(CMAKE_Fortran_FLAGS "${CMAKE_Fortran_FLAGS} ${COVERAGE_COMPILER_FLAGS}" PARENT_SCOPE) set(CMAKE_Fortran_FLAGS "${CMAKE_Fortran_FLAGS} ${COVERAGE_COMPILER_FLAGS}" PARENT_SCOPE)
message(STATUS "Appending code coverage compiler flags: ${COVERAGE_COMPILER_FLAGS}") message(STATUS "Appending code coverage compiler flags: ${COVERAGE_COMPILER_FLAGS}")
endfunction () # append_coverage_compiler_flags endfunction () # append_coverage_compiler_flags
# Setup coverage for specific library # Setup coverage for specific library
function (append_coverage_compiler_flags_to_target name mode) function (append_coverage_compiler_flags_to_target name mode)
separate_arguments(_flag_list NATIVE_COMMAND "${COVERAGE_COMPILER_FLAGS}") separate_arguments(_flag_list NATIVE_COMMAND "${COVERAGE_COMPILER_FLAGS}")
target_compile_options(${name} ${mode} ${_flag_list}) target_compile_options(${name} ${mode} ${_flag_list})
if (CMAKE_C_COMPILER_ID STREQUAL "GNU" OR CMAKE_Fortran_COMPILER_ID STREQUAL "GNU") if (CMAKE_C_COMPILER_ID STREQUAL "GNU" OR CMAKE_Fortran_COMPILER_ID STREQUAL "GNU")
target_link_libraries(${name} ${mode} gcov) target_link_libraries(${name} ${mode} gcov)
endif () endif ()
endfunction () endfunction ()

View File

@@ -11,10 +11,8 @@ set(DOXYGEN_IN ${CMAKE_CURRENT_SOURCE_DIR}/Doxyfile)
set(DOXYGEN_OUT ${CMAKE_CURRENT_BINARY_DIR}/Doxyfile) set(DOXYGEN_OUT ${CMAKE_CURRENT_BINARY_DIR}/Doxyfile)
configure_file(${DOXYGEN_IN} ${DOXYGEN_OUT}) configure_file(${DOXYGEN_IN} ${DOXYGEN_OUT})
add_custom_target( add_custom_target(docs
docs COMMAND ${DOXYGEN_EXECUTABLE} ${DOXYGEN_OUT}
COMMAND ${DOXYGEN_EXECUTABLE} ${DOXYGEN_OUT} WORKING_DIRECTORY ${CMAKE_CURRENT_BINARY_DIR}
WORKING_DIRECTORY ${CMAKE_CURRENT_BINARY_DIR} COMMENT "Generating API documentation with Doxygen"
COMMENT "Generating API documentation with Doxygen" VERBATIM)
VERBATIM
)

View File

@@ -1,11 +1,11 @@
if (DEFINED CMAKE_LINKER_TYPE) if (DEFINED CMAKE_LINKER_TYPE)
message(STATUS "Custom linker is already set: ${CMAKE_LINKER_TYPE}") message(STATUS "Custom linker is already set: ${CMAKE_LINKER_TYPE}")
return() return()
endif () endif ()
find_program(MOLD_PATH mold) find_program(MOLD_PATH mold)
if (MOLD_PATH AND CMAKE_SYSTEM_NAME STREQUAL "Linux") if (MOLD_PATH AND CMAKE_SYSTEM_NAME STREQUAL "Linux")
message(STATUS "Using Mold linker: ${MOLD_PATH}") message(STATUS "Using Mold linker: ${MOLD_PATH}")
set(CMAKE_LINKER_TYPE MOLD) set(CMAKE_LINKER_TYPE MOLD)
endif () endif ()

View File

@@ -31,52 +31,54 @@ set(COMPILER_FLAGS
# -Wduplicated-cond -Wlogical-op -Wuseless-cast ) endif () # -Wduplicated-cond -Wlogical-op -Wuseless-cast ) endif ()
if (is_clang) if (is_clang)
list(APPEND COMPILER_FLAGS -Wshadow # gcc is to aggressive with shadowing list(APPEND COMPILER_FLAGS -Wshadow # gcc is to aggressive with shadowing
# https://gcc.gnu.org/bugzilla/show_bug.cgi?id=78147 # https://gcc.gnu.org/bugzilla/show_bug.cgi?id=78147
) )
endif () endif ()
if (is_appleclang) if (is_appleclang)
list(APPEND COMPILER_FLAGS -Wreorder-init-list) list(APPEND COMPILER_FLAGS -Wreorder-init-list)
endif () endif ()
if (san) if (san)
# When building with sanitizers some compilers will actually produce extra warnings/errors. We don't want this yet, at # When building with sanitizers some compilers will actually produce extra warnings/errors. We don't want this yet,
# least not until we have fixed all runtime issues reported by the sanitizers. Once that is done we can start removing # at least not until we have fixed all runtime issues reported by the sanitizers. Once that is done we can start
# some of these and trying to fix it in our codebase. We can never remove all of below because most of them are # removing some of these and trying to fix it in our codebase. We can never remove all of below because most of them
# reported from deep inside libraries like boost or libxrpl. # are reported from deep inside libraries like boost or libxrpl.
# #
# TODO: Address in https://github.com/XRPLF/clio/issues/1885 # TODO: Address in https://github.com/XRPLF/clio/issues/1885
list( list(APPEND
APPEND COMPILER_FLAGS
COMPILER_FLAGS -Wno-error=tsan # Disables treating TSAN warnings as errors
-Wno-error=tsan # Disables treating TSAN warnings as errors -Wno-tsan # Disables TSAN warnings (thread-safety analysis)
-Wno-tsan # Disables TSAN warnings (thread-safety analysis) -Wno-uninitialized # Disables warnings about uninitialized variables (AddressSanitizer,
-Wno-uninitialized # Disables warnings about uninitialized variables (AddressSanitizer, UndefinedBehaviorSanitizer, # UndefinedBehaviorSanitizer, etc.)
# etc.) -Wno-stringop-overflow # Disables warnings about potential string operation overflows (AddressSanitizer)
-Wno-stringop-overflow # Disables warnings about potential string operation overflows (AddressSanitizer) -Wno-unsafe-buffer-usage # Disables warnings about unsafe memory operations (AddressSanitizer)
-Wno-unsafe-buffer-usage # Disables warnings about unsafe memory operations (AddressSanitizer) -Wno-frame-larger-than # Disables warnings about stack frame size being too large (AddressSanitizer)
-Wno-frame-larger-than # Disables warnings about stack frame size being too large (AddressSanitizer) -Wno-unused-function # Disables warnings about unused functions (LeakSanitizer, memory-related issues)
-Wno-unused-function # Disables warnings about unused functions (LeakSanitizer, memory-related issues) -Wno-unused-but-set-variable # Disables warnings about unused variables (MemorySanitizer)
-Wno-unused-but-set-variable # Disables warnings about unused variables (MemorySanitizer) -Wno-thread-safety-analysis # Disables warnings related to thread safety usage (ThreadSanitizer)
-Wno-thread-safety-analysis # Disables warnings related to thread safety usage (ThreadSanitizer) -Wno-thread-safety # Disables warnings related to thread safety usage (ThreadSanitizer)
-Wno-thread-safety # Disables warnings related to thread safety usage (ThreadSanitizer) -Wno-sign-compare # Disables warnings about signed/unsigned comparison (UndefinedBehaviorSanitizer)
-Wno-sign-compare # Disables warnings about signed/unsigned comparison (UndefinedBehaviorSanitizer) -Wno-nonnull # Disables warnings related to null pointer dereferencing (UndefinedBehaviorSanitizer)
-Wno-nonnull # Disables warnings related to null pointer dereferencing (UndefinedBehaviorSanitizer) -Wno-address # Disables warnings about address-related issues (UndefinedBehaviorSanitizer)
-Wno-address # Disables warnings about address-related issues (UndefinedBehaviorSanitizer) -Wno-array-bounds # Disables array bounds checks (UndefinedBehaviorSanitizer)
-Wno-array-bounds # Disables array bounds checks (UndefinedBehaviorSanitizer) )
)
endif () endif ()
# See https://github.com/cpp-best-practices/cppbestpractices/blob/master/02-Use_the_Tools_Available.md#gcc--clang for # See https://github.com/cpp-best-practices/cppbestpractices/blob/master/02-Use_the_Tools_Available.md#gcc--clang for
# the flags description # the flags description
if (time_trace) if (time_trace)
if (is_clang OR is_appleclang) if (is_clang OR is_appleclang)
list(APPEND COMPILER_FLAGS -ftime-trace) list(APPEND COMPILER_FLAGS -ftime-trace)
else () else ()
message(FATAL_ERROR "Clang or AppleClang is required to use `-ftime-trace`") message(FATAL_ERROR "Clang or AppleClang is required to use `-ftime-trace`")
endif () endif ()
endif () endif ()
target_compile_options(clio_options INTERFACE ${COMPILER_FLAGS}) target_compile_options(clio_options INTERFACE ${COMPILER_FLAGS})
# Add debug symbols for all builds, including Release. This is needed to get useful stack traces in production.
target_compile_options(clio_options INTERFACE -g)

View File

@@ -2,10 +2,10 @@ include(CheckIncludeFileCXX)
check_include_file_cxx("source_location" SOURCE_LOCATION_AVAILABLE) check_include_file_cxx("source_location" SOURCE_LOCATION_AVAILABLE)
if (SOURCE_LOCATION_AVAILABLE) if (SOURCE_LOCATION_AVAILABLE)
target_compile_definitions(clio_options INTERFACE "HAS_SOURCE_LOCATION") target_compile_definitions(clio_options INTERFACE "HAS_SOURCE_LOCATION")
endif () endif ()
check_include_file_cxx("experimental/source_location" EXPERIMENTAL_SOURCE_LOCATION_AVAILABLE) check_include_file_cxx("experimental/source_location" EXPERIMENTAL_SOURCE_LOCATION_AVAILABLE)
if (EXPERIMENTAL_SOURCE_LOCATION_AVAILABLE) if (EXPERIMENTAL_SOURCE_LOCATION_AVAILABLE)
target_compile_definitions(clio_options INTERFACE "HAS_EXPERIMENTAL_SOURCE_LOCATION") target_compile_definitions(clio_options INTERFACE "HAS_EXPERIMENTAL_SOURCE_LOCATION")
endif () endif ()

View File

@@ -1,11 +1,11 @@
if ("${san}" STREQUAL "") if ("${san}" STREQUAL "")
target_compile_definitions(clio_options INTERFACE BOOST_STACKTRACE_LINK) target_compile_definitions(clio_options INTERFACE BOOST_STACKTRACE_LINK)
target_compile_definitions(clio_options INTERFACE BOOST_STACKTRACE_USE_BACKTRACE) target_compile_definitions(clio_options INTERFACE BOOST_STACKTRACE_USE_BACKTRACE)
find_package(libbacktrace REQUIRED CONFIG) find_package(libbacktrace REQUIRED CONFIG)
else () else ()
# Some sanitizers (TSAN and ASAN for sure) can't be used with libbacktrace because they have their own backtracing # Some sanitizers (TSAN and ASAN for sure) can't be used with libbacktrace because they have their own backtracing
# capabilities and there are conflicts. In any case, this makes sure Clio code knows that backtrace is not available. # capabilities and there are conflicts. In any case, this makes sure Clio code knows that backtrace is not
# See relevant conan profiles for sanitizers where we disable stacktrace in Boost explicitly. # available. See relevant conan profiles for sanitizers where we disable stacktrace in Boost explicitly.
target_compile_definitions(clio_options INTERFACE CLIO_WITHOUT_STACKTRACE) target_compile_definitions(clio_options INTERFACE CLIO_WITHOUT_STACKTRACE)
message(STATUS "Sanitizer enabled, disabling stacktrace") message(STATUS "Sanitizer enabled, disabling stacktrace")
endif () endif ()

View File

@@ -1,5 +1,5 @@
find_package(spdlog REQUIRED) find_package(spdlog REQUIRED)
if (NOT TARGET spdlog::spdlog) if (NOT TARGET spdlog::spdlog)
message(FATAL_ERROR "spdlog::spdlog target not found") message(FATAL_ERROR "spdlog::spdlog target not found")
endif () endif ()

View File

@@ -3,15 +3,15 @@
"requires": [ "requires": [
"zlib/1.3.1#b8bc2603263cf7eccbd6e17e66b0ed76%1765850150.075", "zlib/1.3.1#b8bc2603263cf7eccbd6e17e66b0ed76%1765850150.075",
"xxhash/0.8.3#681d36a0a6111fc56e5e45ea182c19cc%1765850149.987", "xxhash/0.8.3#681d36a0a6111fc56e5e45ea182c19cc%1765850149.987",
"xrpl/3.0.0#534d3f65a336109eee929b88962bae4e%1765375071.547", "xrpl/3.1.0#3d408ab8c8020014fa7dd52bc7cc7ea8%1769706825.165",
"sqlite3/3.49.1#8631739a4c9b93bd3d6b753bac548a63%1765850149.926", "sqlite3/3.49.1#8631739a4c9b93bd3d6b753bac548a63%1765850149.926",
"spdlog/1.17.0#bcbaaf7147bda6ad24ffbd1ac3d7142c%1767636069.964", "spdlog/1.17.0#bcbaaf7147bda6ad24ffbd1ac3d7142c%1768312128.781",
"soci/4.0.3#a9f8d773cd33e356b5879a4b0564f287%1765850149.46", "soci/4.0.3#a9f8d773cd33e356b5879a4b0564f287%1765850149.46",
"re2/20230301#ca3b241baec15bd31ea9187150e0b333%1765850148.103", "re2/20230301#ca3b241baec15bd31ea9187150e0b333%1765850148.103",
"rapidjson/cci.20220822#1b9d8c2256876a154172dc5cfbe447c6%1754325007.656", "rapidjson/cci.20220822#1b9d8c2256876a154172dc5cfbe447c6%1754325007.656",
"protobuf/3.21.12#44ee56c0a6eea0c19aeeaca680370b88%1764175361.456", "protobuf/3.21.12#44ee56c0a6eea0c19aeeaca680370b88%1764175361.456",
"openssl/1.1.1w#a8f0792d7c5121b954578a7149d23e03%1756223730.729", "openssl/1.1.1w#a8f0792d7c5121b954578a7149d23e03%1756223730.729",
"nudb/2.0.9#fb8dfd1a5557f5e0528114c2da17721e%1765850143.957", "nudb/2.0.9#0432758a24204da08fee953ec9ea03cb%1769436073.32",
"minizip/1.2.13#9e87d57804bd372d6d1e32b1871517a3%1754325004.374", "minizip/1.2.13#9e87d57804bd372d6d1e32b1871517a3%1754325004.374",
"lz4/1.10.0#59fc63cac7f10fbe8e05c7e62c2f3504%1765850143.914", "lz4/1.10.0#59fc63cac7f10fbe8e05c7e62c2f3504%1765850143.914",
"libuv/1.46.0#dc28c1f653fa197f00db5b577a6f6011%1754325003.592", "libuv/1.46.0#dc28c1f653fa197f00db5b577a6f6011%1754325003.592",
@@ -19,7 +19,7 @@
"libbacktrace/cci.20210118#a7691bfccd8caaf66309df196790a5a1%1765842973.03", "libbacktrace/cci.20210118#a7691bfccd8caaf66309df196790a5a1%1765842973.03",
"libarchive/3.8.1#ffee18995c706e02bf96e7a2f7042e0d%1765850144.736", "libarchive/3.8.1#ffee18995c706e02bf96e7a2f7042e0d%1765850144.736",
"http_parser/2.9.4#98d91690d6fd021e9e624218a85d9d97%1754325001.385", "http_parser/2.9.4#98d91690d6fd021e9e624218a85d9d97%1754325001.385",
"gtest/1.17.0#5224b3b3ff3b4ce1133cbdd27d53ee7d%1755784855.585", "gtest/1.17.0#5224b3b3ff3b4ce1133cbdd27d53ee7d%1768312129.152",
"grpc/1.50.1#02291451d1e17200293a409410d1c4e1%1756234248.958", "grpc/1.50.1#02291451d1e17200293a409410d1c4e1%1756234248.958",
"fmt/12.1.0#50abab23274d56bb8f42c94b3b9a40c7%1763984116.926", "fmt/12.1.0#50abab23274d56bb8f42c94b3b9a40c7%1763984116.926",
"doctest/2.4.11#a4211dfc329a16ba9f280f9574025659%1756234220.819", "doctest/2.4.11#a4211dfc329a16ba9f280f9574025659%1756234220.819",
@@ -40,17 +40,20 @@
], ],
"python_requires": [], "python_requires": [],
"overrides": { "overrides": {
"boost/1.83.0": [ "protobuf/3.21.12#44ee56c0a6eea0c19aeeaca680370b88": [
null,
"protobuf/3.21.12"
],
"boost/1.83.0#91d8b1572534d2c334d6790e3c34d0c1": [
null, null,
"boost/1.83.0#91d8b1572534d2c334d6790e3c34d0c1" "boost/1.83.0#91d8b1572534d2c334d6790e3c34d0c1"
], ],
"protobuf/3.21.12": [
null,
"protobuf/3.21.12#44ee56c0a6eea0c19aeeaca680370b88"
],
"lz4/1.9.4": [ "lz4/1.9.4": [
"lz4/1.10.0" "lz4/1.10.0"
], ],
"boost/1.90.0": [
"boost/1.83.0"
],
"sqlite3/3.44.2": [ "sqlite3/3.44.2": [
"sqlite3/3.49.1" "sqlite3/3.49.1"
] ]

View File

@@ -12,7 +12,6 @@ class ClioConan(ConanFile):
options = {} options = {}
requires = [ requires = [
"boost/1.83.0",
"cassandra-cpp-driver/2.17.0", "cassandra-cpp-driver/2.17.0",
"fmt/12.1.0", "fmt/12.1.0",
"grpc/1.50.1", "grpc/1.50.1",
@@ -20,7 +19,7 @@ class ClioConan(ConanFile):
"openssl/1.1.1w", "openssl/1.1.1w",
"protobuf/3.21.12", "protobuf/3.21.12",
"spdlog/1.17.0", "spdlog/1.17.0",
"xrpl/3.0.0", "xrpl/3.1.0",
"zlib/1.3.1", "zlib/1.3.1",
] ]
@@ -43,6 +42,7 @@ class ClioConan(ConanFile):
exports_sources = ("CMakeLists.txt", "cmake/*", "src/*") exports_sources = ("CMakeLists.txt", "cmake/*", "src/*")
def requirements(self): def requirements(self):
self.requires("boost/1.83.0", force=True)
self.requires("gtest/1.17.0") self.requires("gtest/1.17.0")
self.requires("benchmark/1.9.4") self.requires("benchmark/1.9.4")

View File

@@ -93,3 +93,42 @@ To completely disable Prometheus metrics add `"prometheus": { "enabled": false }
It is important to know that Clio responds to Prometheus request only if they are admin requests. If you are using the admin password feature, the same password should be provided in the Authorization header of Prometheus requests. It is important to know that Clio responds to Prometheus request only if they are admin requests. If you are using the admin password feature, the same password should be provided in the Authorization header of Prometheus requests.
You can find an example Docker Compose file, with Prometheus and Grafana configs, in [examples/infrastructure](../docs/examples/infrastructure/). You can find an example Docker Compose file, with Prometheus and Grafana configs, in [examples/infrastructure](../docs/examples/infrastructure/).
## Ledger cache file
Since version 2.7.0, Clio supports saving the ledger cache to a local file on shutdown and loading it on startup. This feature is disabled by default but can significantly improve restart times.
### Benefits
- **Faster startup**: Loading cache from a file takes less than a minute, compared to 40-90 minutes on Mainnet when loading from the database.
- **Reduced database load**: Clio doesn't put extra load on the database when starting with a cache file.
- **Improved availability**: Faster restart times mean less downtime during maintenance or updates.
> [!NOTE]
> This feature only works when Clio is restarted. When starting Clio for the first time, the cache must be loaded from `rippled` or the database as usual.
### Configuration
To enable the ledger cache file feature, specify the [`cache.file.path`](./config-description.md#cachefilepath) option in your `config.json`:
```json
"cache": {
"file": {
"path": "/path/to/cache/file"
}
}
```
You can optionally configure additional settings such as [`cache.file.max_sequence_age`](./config-description.md#cachefilemax_sequence_age) and [`cache.file.async_save`](./config-description.md#cachefileasync_save) to fine-tune the behavior. For a complete list of available options and their default values, see the [Configuration Description](./config-description.md#cachefilepath) documentation.
### How it works
1. **On shutdown**: Clio saves the current ledger cache to the specified file path. The file includes a hash for integrity verification.
2. **On startup**: Clio checks if a cache file exists at the configured path. If the file exists, Clio will:
- Verify the file's integrity to ensure it is complete and not corrupted.
- Compare the latest ledger sequence in the cache file with the latest sequence in the database.
- Use the cache file only if the difference is less than [`cache.file.max_sequence_age`](./config-description.md#cachefilemax_sequence_age).
- If validation fails or the cache is too old, Clio will fall back to loading from the database.
> [!IMPORTANT]
> The cache file path should point to a location with sufficient disk space. On typical deployments, the cache file size can be several gigabytes.

View File

@@ -2,12 +2,11 @@ add_library(clio_app)
target_sources(clio_app PRIVATE CliArgs.cpp ClioApplication.cpp Stopper.cpp WebHandlers.cpp) target_sources(clio_app PRIVATE CliArgs.cpp ClioApplication.cpp Stopper.cpp WebHandlers.cpp)
target_link_libraries( target_link_libraries(
clio_app clio_app
PUBLIC clio_cluster PUBLIC clio_cluster
clio_etl clio_etl
clio_feed clio_feed
clio_migration clio_migration
clio_rpc clio_rpc
clio_web clio_web
PRIVATE Boost::program_options PRIVATE Boost::program_options)
)

View File

@@ -58,12 +58,16 @@ CliArgs::parse(int argc, char const* argv[])
positional.add("conf", 1); positional.add("conf", 1);
auto const printHelp = [&description]() { auto const printHelp = [&description]() {
std::cout << "Clio server " << util::build::getClioFullVersionString() << "\n\n" << description; std::cout << "Clio server " << util::build::getClioFullVersionString() << "\n\n"
<< description;
}; };
po::variables_map parsed; po::variables_map parsed;
try { try {
po::store(po::command_line_parser(argc, argv).options(description).positional(positional).run(), parsed); po::store(
po::command_line_parser(argc, argv).options(description).positional(positional).run(),
parsed
);
po::notify(parsed); po::notify(parsed);
} catch (po::error const& e) { } catch (po::error const& e) {
std::cerr << "Error: " << e.what() << std::endl << std::endl; std::cerr << "Error: " << e.what() << std::endl << std::endl;
@@ -87,7 +91,8 @@ CliArgs::parse(int argc, char const* argv[])
if (parsed.contains("config-description")) { if (parsed.contains("config-description")) {
std::filesystem::path const filePath = parsed["config-description"].as<std::string>(); std::filesystem::path const filePath = parsed["config-description"].as<std::string>();
auto const res = util::config::ClioConfigDescription::generateConfigDescriptionToFile(filePath); auto const res =
util::config::ClioConfigDescription::generateConfigDescriptionToFile(filePath);
if (res.has_value()) if (res.has_value())
return Action{Action::Exit{EXIT_SUCCESS}}; return Action{Action::Exit{EXIT_SUCCESS}};
@@ -99,15 +104,22 @@ CliArgs::parse(int argc, char const* argv[])
if (parsed.contains("migrate")) { if (parsed.contains("migrate")) {
auto const opt = parsed["migrate"].as<std::string>(); auto const opt = parsed["migrate"].as<std::string>();
if (opt == "status") if (opt == "status") {
return Action{Action::Migrate{.configPath = std::move(configPath), .subCmd = MigrateSubCmd::status()}}; return Action{Action::Migrate{
return Action{Action::Migrate{.configPath = std::move(configPath), .subCmd = MigrateSubCmd::migration(opt)}}; .configPath = std::move(configPath), .subCmd = MigrateSubCmd::status()
}};
}
return Action{Action::Migrate{
.configPath = std::move(configPath), .subCmd = MigrateSubCmd::migration(opt)
}};
} }
if (parsed.contains("verify")) if (parsed.contains("verify"))
return Action{Action::VerifyConfig{.configPath = std::move(configPath)}}; return Action{Action::VerifyConfig{.configPath = std::move(configPath)}};
return Action{Action::Run{.configPath = std::move(configPath), .useNgWebServer = parsed.contains("ng-web-server")}}; return Action{Action::Run{
.configPath = std::move(configPath), .useNgWebServer = parsed.contains("ng-web-server")
}};
} }
} // namespace app } // namespace app

View File

@@ -79,7 +79,8 @@ public:
/** /**
* @brief Apply a function to the action. * @brief Apply a function to the action.
* *
* @tparam Processors Action processors types. Must be callable with the action type and return int. * @tparam Processors Action processors types. Must be callable with the action type and
* return int.
* @param processors Action processors. * @param processors Action processors.
* @return Exit code. * @return Exit code.
*/ */

View File

@@ -29,6 +29,8 @@
#include "etl/ETLService.hpp" #include "etl/ETLService.hpp"
#include "etl/LoadBalancer.hpp" #include "etl/LoadBalancer.hpp"
#include "etl/NetworkValidatedLedgers.hpp" #include "etl/NetworkValidatedLedgers.hpp"
#include "etl/SystemState.hpp"
#include "etl/WriterState.hpp"
#include "feed/SubscriptionManager.hpp" #include "feed/SubscriptionManager.hpp"
#include "migration/MigrationInspectorFactory.hpp" #include "migration/MigrationInspectorFactory.hpp"
#include "rpc/Counters.hpp" #include "rpc/Counters.hpp"
@@ -121,7 +123,11 @@ ClioApplication::run(bool const useNgWebServer)
// Interface to the database // Interface to the database
auto backend = data::makeBackend(config_, cache); auto backend = data::makeBackend(config_, cache);
cluster::ClusterCommunicationService clusterCommunicationService{backend}; auto systemState = etl::SystemState::makeSystemState(config_);
cluster::ClusterCommunicationService clusterCommunicationService{
backend, std::make_unique<etl::WriterState>(systemState)
};
clusterCommunicationService.run(); clusterCommunicationService.run();
auto const amendmentCenter = std::make_shared<data::AmendmentCenter const>(backend); auto const amendmentCenter = std::make_shared<data::AmendmentCenter const>(backend);
@@ -130,14 +136,15 @@ ClioApplication::run(bool const useNgWebServer)
auto const migrationInspector = migration::makeMigrationInspector(config_, backend); auto const migrationInspector = migration::makeMigrationInspector(config_, backend);
// Check if any migration is blocking Clio server starting. // Check if any migration is blocking Clio server starting.
if (migrationInspector->isBlockingClio() and backend->hardFetchLedgerRangeNoThrow()) { if (migrationInspector->isBlockingClio() and backend->hardFetchLedgerRangeNoThrow()) {
LOG(util::LogService::error()) LOG(util::LogService::error()) << "Existing Migration is blocking Clio, Please "
<< "Existing Migration is blocking Clio, Please complete the database migration first."; "complete the database migration first.";
return EXIT_FAILURE; return EXIT_FAILURE;
} }
} }
// Manages clients subscribed to streams // Manages clients subscribed to streams
auto subscriptions = feed::SubscriptionManager::makeSubscriptionManager(config_, backend, amendmentCenter); auto subscriptions =
feed::SubscriptionManager::makeSubscriptionManager(config_, backend, amendmentCenter);
// Tracks which ledgers have been validated by the network // Tracks which ledgers have been validated by the network
auto ledgers = etl::NetworkValidatedLedgers::makeValidatedLedgers(); auto ledgers = etl::NetworkValidatedLedgers::makeValidatedLedgers();
@@ -150,8 +157,11 @@ ClioApplication::run(bool const useNgWebServer)
config_, ioc, backend, subscriptions, std::make_unique<util::MTRandomGenerator>(), ledgers config_, ioc, backend, subscriptions, std::make_unique<util::MTRandomGenerator>(), ledgers
); );
// ETL is responsible for writing and publishing to streams. In read-only mode, ETL only publishes // ETL is responsible for writing and publishing to streams. In read-only mode, ETL only
auto etl = etl::ETLService::makeETLService(config_, ctx, backend, subscriptions, balancer, ledgers); // publishes
auto etl = etl::ETLService::makeETLService(
config_, std::move(systemState), ctx, backend, subscriptions, balancer, ledgers
);
auto workQueue = rpc::WorkQueue::makeWorkQueue(config_); auto workQueue = rpc::WorkQueue::makeWorkQueue(config_);
auto counters = rpc::Counters::makeCounters(workQueue); auto counters = rpc::Counters::makeCounters(workQueue);
@@ -161,15 +171,19 @@ ClioApplication::run(bool const useNgWebServer)
); );
using RPCEngineType = rpc::RPCEngine<rpc::Counters>; using RPCEngineType = rpc::RPCEngine<rpc::Counters>;
auto const rpcEngine = auto const rpcEngine = RPCEngineType::makeRPCEngine(
RPCEngineType::makeRPCEngine(config_, backend, balancer, dosGuard, workQueue, counters, handlerProvider); config_, backend, balancer, dosGuard, workQueue, counters, handlerProvider
);
if (useNgWebServer or config_.get<bool>("server.__ng_web_server")) { if (useNgWebServer or config_.get<bool>("server.__ng_web_server")) {
web::ng::RPCServerHandler<RPCEngineType> handler{config_, backend, rpcEngine, etl, dosGuard}; web::ng::RPCServerHandler<RPCEngineType> handler{
config_, backend, rpcEngine, etl, dosGuard
};
auto expectedAdminVerifier = web::makeAdminVerificationStrategy(config_); auto expectedAdminVerifier = web::makeAdminVerificationStrategy(config_);
if (not expectedAdminVerifier.has_value()) { if (not expectedAdminVerifier.has_value()) {
LOG(util::LogService::error()) << "Error creating admin verifier: " << expectedAdminVerifier.error(); LOG(util::LogService::error())
<< "Error creating admin verifier: " << expectedAdminVerifier.error();
return EXIT_FAILURE; return EXIT_FAILURE;
} }
auto const adminVerifier = std::move(expectedAdminVerifier).value(); auto const adminVerifier = std::move(expectedAdminVerifier).value();
@@ -197,7 +211,16 @@ ClioApplication::run(bool const useNgWebServer)
} }
appStopper_.setOnStop( appStopper_.setOnStop(
Stopper::makeOnStopCallback(httpServer.value(), *balancer, *etl, *subscriptions, *backend, cacheSaver, ioc) Stopper::makeOnStopCallback(
httpServer.value(),
*balancer,
*etl,
*subscriptions,
*backend,
cacheSaver,
clusterCommunicationService,
ioc
)
); );
// Blocks until stopped. // Blocks until stopped.
@@ -209,11 +232,22 @@ ClioApplication::run(bool const useNgWebServer)
} }
// Init the web server // Init the web server
auto handler = std::make_shared<web::RPCServerHandler<RPCEngineType>>(config_, backend, rpcEngine, etl, dosGuard); auto handler = std::make_shared<web::RPCServerHandler<RPCEngineType>>(
config_, backend, rpcEngine, etl, dosGuard
);
auto const httpServer = web::makeHttpServer(config_, ioc, dosGuard, handler, cache); auto const httpServer = web::makeHttpServer(config_, ioc, dosGuard, handler, cache);
appStopper_.setOnStop( appStopper_.setOnStop(
Stopper::makeOnStopCallback(*httpServer, *balancer, *etl, *subscriptions, *backend, cacheSaver, ioc) Stopper::makeOnStopCallback(
*httpServer,
*balancer,
*etl,
*subscriptions,
*backend,
cacheSaver,
clusterCommunicationService,
ioc
)
); );
// Blocks until stopped. // Blocks until stopped.

View File

@@ -19,6 +19,7 @@
#pragma once #pragma once
#include "cluster/Concepts.hpp"
#include "data/BackendInterface.hpp" #include "data/BackendInterface.hpp"
#include "data/LedgerCacheSaver.hpp" #include "data/LedgerCacheSaver.hpp"
#include "etl/ETLServiceInterface.hpp" #include "etl/ETLServiceInterface.hpp"
@@ -38,7 +39,8 @@
namespace app { namespace app {
/** /**
* @brief Application stopper class. On stop it will create a new thread to run all the shutdown tasks. * @brief Application stopper class. On stop it will create a new thread to run all the shutdown
* tasks.
*/ */
class Stopper { class Stopper {
boost::asio::io_context ctx_; boost::asio::io_context ctx_;
@@ -82,10 +84,14 @@ public:
* @param subscriptions The subscription manager to stop. * @param subscriptions The subscription manager to stop.
* @param backend The backend to stop. * @param backend The backend to stop.
* @param cacheSaver The ledger cache saver * @param cacheSaver The ledger cache saver
* @param clusterCommunicationService The cluster communication service to stop.
* @param ioc The io_context to stop. * @param ioc The io_context to stop.
* @return The callback to be called on application stop. * @return The callback to be called on application stop.
*/ */
template <web::SomeServer ServerType, data::SomeLedgerCacheSaver LedgerCacheSaverType> template <
web::SomeServer ServerType,
data::SomeLedgerCacheSaver LedgerCacheSaverType,
cluster::SomeClusterCommunicationService ClusterCommunicationServiceType>
static std::function<void(boost::asio::yield_context)> static std::function<void(boost::asio::yield_context)>
makeOnStopCallback( makeOnStopCallback(
ServerType& server, ServerType& server,
@@ -94,6 +100,7 @@ public:
feed::SubscriptionManagerInterface& subscriptions, feed::SubscriptionManagerInterface& subscriptions,
data::BackendInterface& backend, data::BackendInterface& backend,
LedgerCacheSaverType& cacheSaver, LedgerCacheSaverType& cacheSaver,
ClusterCommunicationServiceType& clusterCommunicationService,
boost::asio::io_context& ioc boost::asio::io_context& ioc
) )
{ {
@@ -111,6 +118,8 @@ public:
}); });
coroutineGroup.asyncWait(yield); coroutineGroup.asyncWait(yield);
clusterCommunicationService.stop();
etl.stop(); etl.stop();
LOG(util::LogService::info()) << "ETL stopped"; LOG(util::LogService::info()) << "ETL stopped";

View File

@@ -41,7 +41,8 @@ parseConfig(std::string_view configPath)
auto const json = ConfigFileJson::makeConfigFileJson(configPath); auto const json = ConfigFileJson::makeConfigFileJson(configPath);
if (!json.has_value()) { if (!json.has_value()) {
std::cerr << "Error parsing json from config: " << configPath << "\n" << json.error().error << std::endl; std::cerr << "Error parsing json from config: " << configPath << "\n"
<< json.error().error << std::endl;
return false; return false;
} }
auto const errors = getClioConfig().parse(json.value()); auto const errors = getClioConfig().parse(json.value());

View File

@@ -51,9 +51,9 @@ OnConnectCheck::operator()(web::ng::Connection const& connection)
{ {
dosguard_.get().increment(connection.ip()); dosguard_.get().increment(connection.ip());
if (not dosguard_.get().isOk(connection.ip())) { if (not dosguard_.get().isOk(connection.ip())) {
return std::unexpected{ return std::unexpected{web::ng::Response{
web::ng::Response{boost::beast::http::status::too_many_requests, "Too many requests", connection} boost::beast::http::status::too_many_requests, "Too many requests", connection
}; }};
} }
return {}; return {};
@@ -80,7 +80,10 @@ DisconnectHook::operator()(web::ng::Connection const& connection)
dosguard_.get().decrement(connection.ip()); dosguard_.get().decrement(connection.ip());
} }
MetricsHandler::MetricsHandler(std::shared_ptr<web::AdminVerificationStrategy> adminVerifier, rpc::WorkQueue& workQueue) MetricsHandler::MetricsHandler(
std::shared_ptr<web::AdminVerificationStrategy> adminVerifier,
rpc::WorkQueue& workQueue
)
: adminVerifier_{std::move(adminVerifier)}, workQueue_{std::ref(workQueue)} : adminVerifier_{std::move(adminVerifier)}, workQueue_{std::ref(workQueue)}
{ {
} }
@@ -120,7 +123,9 @@ MetricsHandler::operator()(
if (!postSuccessful) { if (!postSuccessful) {
return web::ng::Response{ return web::ng::Response{
boost::beast::http::status::too_many_requests, rpc::makeError(rpc::RippledError::rpcTOO_BUSY), request boost::beast::http::status::too_many_requests,
rpc::makeError(rpc::RippledError::rpcTOO_BUSY),
request
}; };
} }
@@ -177,7 +182,9 @@ CacheStateHandler::operator()(
if (cache_.get().isFull()) if (cache_.get().isFull())
return web::ng::Response{boost::beast::http::status::ok, kCACHE_CHECK_LOADED_HTML, request}; return web::ng::Response{boost::beast::http::status::ok, kCACHE_CHECK_LOADED_HTML, request};
return web::ng::Response{boost::beast::http::status::service_unavailable, kCACHE_CHECK_NOT_LOADED_HTML, request}; return web::ng::Response{
boost::beast::http::status::service_unavailable, kCACHE_CHECK_NOT_LOADED_HTML, request
};
} }
} // namespace app } // namespace app

View File

@@ -68,8 +68,8 @@ public:
}; };
/** /**
* @brief A function object that is called when the IP of a connection changes (usually if proxy detected). * @brief A function object that is called when the IP of a connection changes (usually if proxy
* This is used to update the DOS guard. * detected). This is used to update the DOS guard.
*/ */
class IpChangeHook { class IpChangeHook {
std::reference_wrapper<web::dosguard::DOSGuardInterface> dosguard_; std::reference_wrapper<web::dosguard::DOSGuardInterface> dosguard_;
@@ -126,10 +126,14 @@ public:
/** /**
* @brief Construct a new MetricsHandler object * @brief Construct a new MetricsHandler object
* *
* @param adminVerifier The AdminVerificationStrategy to use for verifying the connection for admin access. * @param adminVerifier The AdminVerificationStrategy to use for verifying the connection for
* admin access.
* @param workQueue The WorkQueue to use for handling the request. * @param workQueue The WorkQueue to use for handling the request.
*/ */
MetricsHandler(std::shared_ptr<web::AdminVerificationStrategy> adminVerifier, rpc::WorkQueue& workQueue); MetricsHandler(
std::shared_ptr<web::AdminVerificationStrategy> adminVerifier,
rpc::WorkQueue& workQueue
);
/** /**
* @brief The call of the function object. * @brief The call of the function object.
@@ -214,10 +218,14 @@ public:
/** /**
* @brief Construct a new RequestHandler object * @brief Construct a new RequestHandler object
* *
* @param adminVerifier The AdminVerificationStrategy to use for verifying the connection for admin access. * @param adminVerifier The AdminVerificationStrategy to use for verifying the connection for
* admin access.
* @param rpcHandler The RPC handler to use for handling the request. * @param rpcHandler The RPC handler to use for handling the request.
*/ */
RequestHandler(std::shared_ptr<web::AdminVerificationStrategy> adminVerifier, RpcHandlerType& rpcHandler) RequestHandler(
std::shared_ptr<web::AdminVerificationStrategy> adminVerifier,
RpcHandlerType& rpcHandler
)
: adminVerifier_(std::move(adminVerifier)), rpcHandler_(rpcHandler) : adminVerifier_(std::move(adminVerifier)), rpcHandler_(rpcHandler)
{ {
} }

136
src/cluster/Backend.cpp Normal file
View File

@@ -0,0 +1,136 @@
//------------------------------------------------------------------------------
/*
This file is part of clio: https://github.com/XRPLF/clio
Copyright (c) 2025, the clio developers.
Permission to use, copy, modify, and distribute this software for any
purpose with or without fee is hereby granted, provided that the above
copyright notice and this permission notice appear in all copies.
THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
*/
//==============================================================================
#include "cluster/Backend.hpp"
#include "cluster/ClioNode.hpp"
#include "data/BackendInterface.hpp"
#include "etl/WriterState.hpp"
#include <boost/asio/spawn.hpp>
#include <boost/asio/thread_pool.hpp>
#include <boost/json/parse.hpp>
#include <boost/json/serialize.hpp>
#include <boost/json/value.hpp>
#include <boost/json/value_from.hpp>
#include <boost/json/value_to.hpp>
#include <boost/uuid/random_generator.hpp>
#include <boost/uuid/uuid.hpp>
#include <fmt/format.h>
#include <chrono>
#include <memory>
#include <utility>
#include <vector>
namespace cluster {
Backend::Backend(
boost::asio::thread_pool& ctx,
std::shared_ptr<data::BackendInterface> backend,
std::unique_ptr<etl::WriterStateInterface const> writerState,
std::chrono::steady_clock::duration readInterval,
std::chrono::steady_clock::duration writeInterval
)
: backend_(std::move(backend))
, writerState_(std::move(writerState))
, readerTask_(readInterval, ctx)
, writerTask_(writeInterval, ctx)
, selfUuid_(std::make_shared<boost::uuids::uuid>(boost::uuids::random_generator{}()))
{
}
void
Backend::run()
{
readerTask_.run([this](boost::asio::yield_context yield) {
auto clusterData = doRead(yield);
onNewState_(selfUuid_, std::make_shared<ClusterData>(std::move(clusterData)));
});
writerTask_.run([this]() { doWrite(); });
}
Backend::~Backend()
{
stop();
}
void
Backend::stop()
{
readerTask_.stop();
writerTask_.stop();
}
ClioNode::CUuid
Backend::selfId() const
{
return selfUuid_;
}
Backend::ClusterData
Backend::doRead(boost::asio::yield_context yield)
{
BackendInterface::ClioNodesDataFetchResult expectedResult;
try {
expectedResult = backend_->fetchClioNodesData(yield);
} catch (...) {
expectedResult = std::unexpected{"Failed to fetch Clio nodes data"};
}
if (!expectedResult.has_value()) {
return std::unexpected{std::move(expectedResult).error()};
}
std::vector<ClioNode> otherNodesData;
for (auto const& [uuid, nodeDataStr] : expectedResult.value()) {
if (uuid == *selfUuid_) {
continue;
}
boost::system::error_code errorCode;
auto const json = boost::json::parse(nodeDataStr, errorCode);
if (errorCode.failed()) {
return std::unexpected{fmt::format("Error parsing json from DB: {}", nodeDataStr)};
}
auto expectedNodeData = boost::json::try_value_to<ClioNode>(json);
if (expectedNodeData.has_error()) {
return std::unexpected{
fmt::format("Error converting json to ClioNode: {}", nodeDataStr)
};
}
*expectedNodeData->uuid = uuid;
otherNodesData.push_back(std::move(expectedNodeData).value());
}
otherNodesData.push_back(ClioNode::from(selfUuid_, *writerState_));
return otherNodesData;
}
void
Backend::doWrite()
{
auto const selfData = ClioNode::from(selfUuid_, *writerState_);
boost::json::value jsonValue{};
boost::json::value_from(selfData, jsonValue);
backend_->writeNodeMessage(*selfData.uuid, boost::json::serialize(jsonValue.as_object()));
}
} // namespace cluster

148
src/cluster/Backend.hpp Normal file
View File

@@ -0,0 +1,148 @@
//------------------------------------------------------------------------------
/*
This file is part of clio: https://github.com/XRPLF/clio
Copyright (c) 2025, the clio developers.
Permission to use, copy, modify, and distribute this software for any
purpose with or without fee is hereby granted, provided that the above
copyright notice and this permission notice appear in all copies.
THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
*/
//==============================================================================
#pragma once
#include "cluster/ClioNode.hpp"
#include "cluster/impl/RepeatedTask.hpp"
#include "data/BackendInterface.hpp"
#include "etl/WriterState.hpp"
#include "util/log/Logger.hpp"
#include <boost/asio/any_io_executor.hpp>
#include <boost/asio/cancellation_signal.hpp>
#include <boost/asio/execution_context.hpp>
#include <boost/asio/executor.hpp>
#include <boost/asio/spawn.hpp>
#include <boost/asio/strand.hpp>
#include <boost/asio/thread_pool.hpp>
#include <boost/signals2/connection.hpp>
#include <boost/signals2/signal.hpp>
#include <boost/signals2/variadic_signal.hpp>
#include <boost/uuid/uuid.hpp>
#include <chrono>
#include <concepts>
#include <memory>
#include <string>
#include <vector>
namespace cluster {
/**
* @brief Backend communication handler for cluster state synchronization.
*
* This class manages reading and writing cluster state information to/from the backend database.
* It periodically reads the state of other nodes in the cluster and writes the current node's
* state, enabling cluster-wide coordination and awareness.
*/
class Backend {
public:
/** @brief Type representing cluster data result - either a vector of nodes or an error message
*/
using ClusterData = std::expected<std::vector<ClioNode>, std::string>;
private:
util::Logger log_{"ClusterCommunication"};
std::shared_ptr<data::BackendInterface> backend_;
std::unique_ptr<etl::WriterStateInterface const> writerState_;
impl::RepeatedTask<boost::asio::thread_pool> readerTask_;
impl::RepeatedTask<boost::asio::thread_pool> writerTask_;
ClioNode::Uuid selfUuid_;
boost::signals2::signal<void(ClioNode::CUuid, std::shared_ptr<ClusterData const>)> onNewState_;
public:
/**
* @brief Construct a Backend communication handler.
*
* @param ctx The execution context for asynchronous operations
* @param backend Interface to the backend database
* @param writerState State indicating whether this node is writing to the database
* @param readInterval How often to read cluster state from the backend
* @param writeInterval How often to write this node's state to the backend
*/
Backend(
boost::asio::thread_pool& ctx,
std::shared_ptr<data::BackendInterface> backend,
std::unique_ptr<etl::WriterStateInterface const> writerState,
std::chrono::steady_clock::duration readInterval,
std::chrono::steady_clock::duration writeInterval
);
~Backend();
Backend(Backend&&) = delete;
Backend&
operator=(Backend&&) = delete;
Backend(Backend const&) = delete;
Backend&
operator=(Backend const&) = delete;
/**
* @brief Start the backend read and write tasks.
*
* Begins periodic reading of cluster state from the backend and writing of this node's state.
*/
void
run();
/**
* @brief Stop the backend read and write tasks.
*
* Stops all periodic tasks and waits for them to complete.
*/
void
stop();
/**
* @brief Subscribe to new cluster state notifications.
*
* @tparam S Callable type accepting (ClioNode::cUUID, ClusterData)
* @param s Subscriber callback to be invoked when new cluster state is available
* @return A connection object that can be used to unsubscribe
*/
template <typename S>
requires std::invocable<S, ClioNode::CUuid, std::shared_ptr<ClusterData const>>
boost::signals2::connection
subscribeToNewState(S&& s)
{
return onNewState_.connect(s);
}
/**
* @brief Get the UUID of this node in the cluster.
*
* @return The UUID of this node.
*/
ClioNode::CUuid
selfId() const;
private:
ClusterData
doRead(boost::asio::yield_context yield);
void
doWrite();
};
} // namespace cluster

View File

@@ -1,5 +1,6 @@
add_library(clio_cluster) add_library(clio_cluster)
target_sources(clio_cluster PRIVATE ClioNode.cpp ClusterCommunicationService.cpp) target_sources(clio_cluster PRIVATE Backend.cpp ClioNode.cpp ClusterCommunicationService.cpp Metrics.cpp
WriterDecider.cpp)
target_link_libraries(clio_cluster PRIVATE clio_util clio_data) target_link_libraries(clio_cluster PRIVATE clio_util clio_data)

View File

@@ -19,6 +19,7 @@
#include "cluster/ClioNode.hpp" #include "cluster/ClioNode.hpp"
#include "etl/WriterState.hpp"
#include "util/TimeUtils.hpp" #include "util/TimeUtils.hpp"
#include <boost/json/conversion.hpp> #include <boost/json/conversion.hpp>
@@ -26,39 +27,76 @@
#include <boost/json/value.hpp> #include <boost/json/value.hpp>
#include <boost/uuid/uuid.hpp> #include <boost/uuid/uuid.hpp>
#include <chrono>
#include <cstdint>
#include <memory> #include <memory>
#include <stdexcept> #include <stdexcept>
#include <string> #include <string>
#include <string_view> #include <string_view>
#include <utility>
namespace cluster { namespace cluster {
namespace { namespace {
struct Fields { struct JsonFields {
static constexpr std::string_view const kUPDATE_TIME = "update_time"; static constexpr std::string_view const kUPDATE_TIME = "update_time";
static constexpr std::string_view const kDB_ROLE = "db_role";
}; };
} // namespace } // namespace
ClioNode
ClioNode::from(ClioNode::Uuid uuid, etl::WriterStateInterface const& writerState)
{
auto const dbRole = [&writerState]() {
if (writerState.isReadOnly()) {
return ClioNode::DbRole::ReadOnly;
}
if (writerState.isFallback()) {
return ClioNode::DbRole::Fallback;
}
if (writerState.isLoadingCache()) {
return ClioNode::DbRole::LoadingCache;
}
return writerState.isWriting() ? ClioNode::DbRole::Writer : ClioNode::DbRole::NotWriter;
}();
return ClioNode{
.uuid = std::move(uuid), .updateTime = std::chrono::system_clock::now(), .dbRole = dbRole
};
}
void void
tag_invoke(boost::json::value_from_tag, boost::json::value& jv, ClioNode const& node) tag_invoke(boost::json::value_from_tag, boost::json::value& jv, ClioNode const& node)
{ {
jv = { jv = {
{Fields::kUPDATE_TIME, util::systemTpToUtcStr(node.updateTime, ClioNode::kTIME_FORMAT)}, {JsonFields::kUPDATE_TIME, util::systemTpToUtcStr(node.updateTime, ClioNode::kTIME_FORMAT)},
{JsonFields::kDB_ROLE, static_cast<int64_t>(node.dbRole)}
}; };
} }
ClioNode ClioNode
tag_invoke(boost::json::value_to_tag<ClioNode>, boost::json::value const& jv) tag_invoke(boost::json::value_to_tag<ClioNode>, boost::json::value const& jv)
{ {
auto const& updateTimeStr = jv.as_object().at(Fields::kUPDATE_TIME).as_string(); auto const& updateTimeStr = jv.as_object().at(JsonFields::kUPDATE_TIME).as_string();
auto const updateTime = util::systemTpFromUtcStr(std::string(updateTimeStr), ClioNode::kTIME_FORMAT); auto const updateTime =
util::systemTpFromUtcStr(std::string(updateTimeStr), ClioNode::kTIME_FORMAT);
if (!updateTime.has_value()) { if (!updateTime.has_value()) {
throw std::runtime_error("Failed to parse update time"); throw std::runtime_error("Failed to parse update time");
} }
return ClioNode{.uuid = std::make_shared<boost::uuids::uuid>(), .updateTime = updateTime.value()}; auto const dbRoleValue = jv.as_object().at(JsonFields::kDB_ROLE).as_int64();
if (dbRoleValue > static_cast<int64_t>(ClioNode::DbRole::MAX))
throw std::runtime_error("Invalid db_role value");
return ClioNode{
// Json data doesn't contain uuid so leaving it empty here. It will be filled outside of
// this parsing
.uuid = std::make_shared<boost::uuids::uuid>(),
.updateTime = updateTime.value(),
.dbRole = static_cast<ClioNode::DbRole>(dbRoleValue)
};
} }
} // namespace cluster } // namespace cluster

View File

@@ -19,6 +19,8 @@
#pragma once #pragma once
#include "etl/WriterState.hpp"
#include <boost/json/conversion.hpp> #include <boost/json/conversion.hpp>
#include <boost/json/value.hpp> #include <boost/json/value.hpp>
#include <boost/uuid/uuid.hpp> #include <boost/uuid/uuid.hpp>
@@ -37,16 +39,45 @@ struct ClioNode {
*/ */
static constexpr char const* kTIME_FORMAT = "%Y-%m-%dT%H:%M:%SZ"; static constexpr char const* kTIME_FORMAT = "%Y-%m-%dT%H:%M:%SZ";
// enum class WriterRole { /**
// ReadOnly, * @brief Database role of a node in the cluster.
// NotWriter, *
// Writer * Roles are used to coordinate which node writes to the database:
// }; * - ReadOnly: Node is configured to never write (strict read-only mode)
* - NotWriter: Node can write but is currently not the designated writer
* - Writer: Node is actively writing to the database
* - Fallback: Node is using the fallback writer decision mechanism
*
* When any node in the cluster is in Fallback mode, the entire cluster switches
* from the cluster communication mechanism to the slower but more reliable
* database-based conflict detection mechanism.
*/
enum class DbRole {
ReadOnly = 0,
LoadingCache = 1,
NotWriter = 2,
Writer = 3,
Fallback = 4,
MAX = 4
};
std::shared_ptr<boost::uuids::uuid> uuid; ///< The UUID of the node. using Uuid = std::shared_ptr<boost::uuids::uuid>;
std::chrono::system_clock::time_point updateTime; ///< The time the data about the node was last updated. using CUuid = std::shared_ptr<boost::uuids::uuid const>;
// WriterRole writerRole; Uuid uuid; ///< The UUID of the node.
std::chrono::system_clock::time_point
updateTime; ///< The time the data about the node was last updated.
DbRole dbRole; ///< The database role of the node
/**
* @brief Create a ClioNode from writer state.
*
* @param uuid The UUID of the node
* @param writerState The writer state to determine the node's database role
* @return A ClioNode with the current time and role derived from writerState
*/
static ClioNode
from(Uuid uuid, etl::WriterStateInterface const& writerState);
}; };
void void

View File

@@ -19,98 +19,37 @@
#include "cluster/ClusterCommunicationService.hpp" #include "cluster/ClusterCommunicationService.hpp"
#include "cluster/ClioNode.hpp"
#include "data/BackendInterface.hpp" #include "data/BackendInterface.hpp"
#include "util/Assert.hpp" #include "etl/WriterState.hpp"
#include "util/Spawn.hpp"
#include "util/log/Logger.hpp"
#include <boost/asio/bind_cancellation_slot.hpp>
#include <boost/asio/cancellation_type.hpp>
#include <boost/asio/error.hpp>
#include <boost/asio/spawn.hpp>
#include <boost/asio/steady_timer.hpp>
#include <boost/asio/use_future.hpp>
#include <boost/json/parse.hpp>
#include <boost/json/serialize.hpp>
#include <boost/json/value.hpp>
#include <boost/json/value_from.hpp>
#include <boost/json/value_to.hpp>
#include <boost/uuid/random_generator.hpp>
#include <boost/uuid/uuid.hpp>
#include <chrono> #include <chrono>
#include <ctime> #include <ctime>
#include <latch>
#include <memory> #include <memory>
#include <string>
#include <utility> #include <utility>
#include <vector>
namespace {
constexpr auto kTOTAL_WORKERS = 2uz; // 1 reading and 1 writing worker (coroutines)
} // namespace
namespace cluster { namespace cluster {
ClusterCommunicationService::ClusterCommunicationService( ClusterCommunicationService::ClusterCommunicationService(
std::shared_ptr<data::BackendInterface> backend, std::shared_ptr<data::BackendInterface> backend,
std::unique_ptr<etl::WriterStateInterface> writerState,
std::chrono::steady_clock::duration readInterval, std::chrono::steady_clock::duration readInterval,
std::chrono::steady_clock::duration writeInterval std::chrono::steady_clock::duration writeInterval
) )
: backend_(std::move(backend)) : backend_(ctx_, std::move(backend), writerState->clone(), readInterval, writeInterval)
, readInterval_(readInterval) , writerDecider_(ctx_, std::move(writerState))
, writeInterval_(writeInterval)
, finishedCountdown_(kTOTAL_WORKERS)
, selfData_{ClioNode{
.uuid = std::make_shared<boost::uuids::uuid>(boost::uuids::random_generator{}()),
.updateTime = std::chrono::system_clock::time_point{}
}}
{ {
nodesInClusterMetric_.set(1); // The node always sees itself
isHealthy_ = true;
} }
void void
ClusterCommunicationService::run() ClusterCommunicationService::run()
{ {
ASSERT(not running_ and not stopped_, "Can only be ran once"); backend_.subscribeToNewState([this](auto&&... args) {
running_ = true; metrics_.onNewState(std::forward<decltype(args)>(args)...);
util::spawn(strand_, [this](boost::asio::yield_context yield) {
boost::asio::steady_timer timer(yield.get_executor());
boost::system::error_code ec;
while (running_) {
timer.expires_after(readInterval_);
auto token = cancelSignal_.slot();
timer.async_wait(boost::asio::bind_cancellation_slot(token, yield[ec]));
if (ec == boost::asio::error::operation_aborted or not running_)
break;
doRead(yield);
}
finishedCountdown_.count_down(1);
}); });
backend_.subscribeToNewState([this](auto&&... args) {
util::spawn(strand_, [this](boost::asio::yield_context yield) { writerDecider_.onNewState(std::forward<decltype(args)>(args)...);
boost::asio::steady_timer timer(yield.get_executor());
boost::system::error_code ec;
while (running_) {
doWrite();
timer.expires_after(writeInterval_);
auto token = cancelSignal_.slot();
timer.async_wait(boost::asio::bind_cancellation_slot(token, yield[ec]));
if (ec == boost::asio::error::operation_aborted or not running_)
break;
}
finishedCountdown_.count_down(1);
}); });
backend_.run();
} }
ClusterCommunicationService::~ClusterCommunicationService() ClusterCommunicationService::~ClusterCommunicationService()
@@ -121,107 +60,7 @@ ClusterCommunicationService::~ClusterCommunicationService()
void void
ClusterCommunicationService::stop() ClusterCommunicationService::stop()
{ {
if (stopped_) backend_.stop();
return;
stopped_ = true;
// for ASAN to see through concurrency correctly we need to exit all coroutines before joining the ctx
running_ = false;
// cancelSignal_ is not thread safe so we execute emit on the same strand
boost::asio::spawn(
strand_, [this](auto&&) { cancelSignal_.emit(boost::asio::cancellation_type::all); }, boost::asio::use_future
)
.wait();
finishedCountdown_.wait();
ctx_.join();
}
std::shared_ptr<boost::uuids::uuid>
ClusterCommunicationService::selfUuid() const
{
// Uuid never changes so it is safe to copy it without using strand_
return selfData_.uuid;
}
ClioNode
ClusterCommunicationService::selfData() const
{
ClioNode result{};
util::spawn(strand_, [this, &result](boost::asio::yield_context) { result = selfData_; });
return result;
}
std::expected<std::vector<ClioNode>, std::string>
ClusterCommunicationService::clusterData() const
{
if (not isHealthy_) {
return std::unexpected{"Service is not healthy"};
}
std::vector<ClioNode> result;
util::spawn(strand_, [this, &result](boost::asio::yield_context) {
result = otherNodesData_;
result.push_back(selfData_);
});
return result;
}
void
ClusterCommunicationService::doRead(boost::asio::yield_context yield)
{
otherNodesData_.clear();
BackendInterface::ClioNodesDataFetchResult expectedResult;
try {
expectedResult = backend_->fetchClioNodesData(yield);
} catch (...) {
expectedResult = std::unexpected{"Failed to fecth Clio nodes data"};
}
if (!expectedResult.has_value()) {
LOG(log_.error()) << "Failed to fetch nodes data";
isHealthy_ = false;
return;
}
// Create a new vector here to not have partially parsed data in otherNodesData_
std::vector<ClioNode> otherNodesData;
for (auto const& [uuid, nodeDataStr] : expectedResult.value()) {
if (uuid == *selfData_.uuid) {
continue;
}
boost::system::error_code errorCode;
auto const json = boost::json::parse(nodeDataStr, errorCode);
if (errorCode.failed()) {
LOG(log_.error()) << "Error parsing json from DB: " << nodeDataStr;
isHealthy_ = false;
return;
}
auto expectedNodeData = boost::json::try_value_to<ClioNode>(json);
if (expectedNodeData.has_error()) {
LOG(log_.error()) << "Error converting json to ClioNode: " << json;
isHealthy_ = false;
return;
}
*expectedNodeData->uuid = uuid;
otherNodesData.push_back(std::move(expectedNodeData).value());
}
otherNodesData_ = std::move(otherNodesData);
nodesInClusterMetric_.set(otherNodesData_.size() + 1);
isHealthy_ = true;
}
void
ClusterCommunicationService::doWrite()
{
selfData_.updateTime = std::chrono::system_clock::now();
boost::json::value jsonValue{};
boost::json::value_from(selfData_, jsonValue);
backend_->writeNodeMessage(*selfData_.uuid, boost::json::serialize(jsonValue.as_object()));
} }
} // namespace cluster } // namespace cluster

View File

@@ -19,13 +19,12 @@
#pragma once #pragma once
#include "cluster/ClioNode.hpp" #include "cluster/Backend.hpp"
#include "cluster/ClusterCommunicationServiceInterface.hpp" #include "cluster/Concepts.hpp"
#include "cluster/Metrics.hpp"
#include "cluster/WriterDecider.hpp"
#include "data/BackendInterface.hpp" #include "data/BackendInterface.hpp"
#include "util/log/Logger.hpp" #include "etl/WriterState.hpp"
#include "util/prometheus/Bool.hpp"
#include "util/prometheus/Gauge.hpp"
#include "util/prometheus/Prometheus.hpp"
#include <boost/asio/cancellation_signal.hpp> #include <boost/asio/cancellation_signal.hpp>
#include <boost/asio/spawn.hpp> #include <boost/asio/spawn.hpp>
@@ -33,67 +32,51 @@
#include <boost/asio/thread_pool.hpp> #include <boost/asio/thread_pool.hpp>
#include <boost/uuid/uuid.hpp> #include <boost/uuid/uuid.hpp>
#include <atomic>
#include <chrono> #include <chrono>
#include <latch>
#include <memory> #include <memory>
#include <string>
#include <vector>
namespace cluster { namespace cluster {
/** /**
* @brief Service to post and read messages to/from the cluster. It uses a backend to communicate with the cluster. * @brief Service to post and read messages to/from the cluster. It uses a backend to communicate
* with the cluster.
*/ */
class ClusterCommunicationService : public ClusterCommunicationServiceInterface { class ClusterCommunicationService : public ClusterCommunicationServiceTag {
util::prometheus::GaugeInt& nodesInClusterMetric_ = PrometheusService::gaugeInt( // TODO: Use util::async::CoroExecutionContext after https://github.com/XRPLF/clio/issues/1973
"cluster_nodes_total_number", // is implemented
{},
"Total number of nodes this node can detect in the cluster."
);
util::prometheus::Bool isHealthy_ = PrometheusService::boolMetric(
"cluster_communication_is_healthy",
{},
"Whether cluster communication service is operating healthy (1 - healthy, 0 - we have a problem)"
);
// TODO: Use util::async::CoroExecutionContext after https://github.com/XRPLF/clio/issues/1973 is implemented
boost::asio::thread_pool ctx_{1}; boost::asio::thread_pool ctx_{1};
boost::asio::strand<boost::asio::thread_pool::executor_type> strand_ = boost::asio::make_strand(ctx_); Backend backend_;
Metrics metrics_;
util::Logger log_{"ClusterCommunication"}; WriterDecider writerDecider_;
std::shared_ptr<data::BackendInterface> backend_;
std::chrono::steady_clock::duration readInterval_;
std::chrono::steady_clock::duration writeInterval_;
boost::asio::cancellation_signal cancelSignal_;
std::latch finishedCountdown_;
std::atomic_bool running_ = false;
bool stopped_ = false;
ClioNode selfData_;
std::vector<ClioNode> otherNodesData_;
public: public:
static constexpr std::chrono::milliseconds kDEFAULT_READ_INTERVAL{2100}; static constexpr std::chrono::milliseconds kDEFAULT_READ_INTERVAL{1000};
static constexpr std::chrono::milliseconds kDEFAULT_WRITE_INTERVAL{1200}; static constexpr std::chrono::milliseconds kDEFAULT_WRITE_INTERVAL{1000};
/** /**
* @brief Construct a new Cluster Communication Service object. * @brief Construct a new Cluster Communication Service object.
* *
* @param backend The backend to use for communication. * @param backend The backend to use for communication.
* @param writerState The state showing whether clio is writing to the database.
* @param readInterval The interval to read messages from the cluster. * @param readInterval The interval to read messages from the cluster.
* @param writeInterval The interval to write messages to the cluster. * @param writeInterval The interval to write messages to the cluster.
*/ */
ClusterCommunicationService( ClusterCommunicationService(
std::shared_ptr<data::BackendInterface> backend, std::shared_ptr<data::BackendInterface> backend,
std::unique_ptr<etl::WriterStateInterface> writerState,
std::chrono::steady_clock::duration readInterval = kDEFAULT_READ_INTERVAL, std::chrono::steady_clock::duration readInterval = kDEFAULT_READ_INTERVAL,
std::chrono::steady_clock::duration writeInterval = kDEFAULT_WRITE_INTERVAL std::chrono::steady_clock::duration writeInterval = kDEFAULT_WRITE_INTERVAL
); );
~ClusterCommunicationService() override; ~ClusterCommunicationService() override;
ClusterCommunicationService(ClusterCommunicationService&&) = delete;
ClusterCommunicationService(ClusterCommunicationService const&) = delete;
ClusterCommunicationService&
operator=(ClusterCommunicationService&&) = delete;
ClusterCommunicationService&
operator=(ClusterCommunicationService const&) = delete;
/** /**
* @brief Start the service. * @brief Start the service.
*/ */
@@ -105,44 +88,6 @@ public:
*/ */
void void
stop(); stop();
ClusterCommunicationService(ClusterCommunicationService&&) = delete;
ClusterCommunicationService(ClusterCommunicationService const&) = delete;
ClusterCommunicationService&
operator=(ClusterCommunicationService&&) = delete;
ClusterCommunicationService&
operator=(ClusterCommunicationService const&) = delete;
/**
* @brief Get the UUID of the current node.
*
* @return The UUID of the current node.
*/
std::shared_ptr<boost::uuids::uuid>
selfUuid() const;
/**
* @brief Get the data of the current node.
*
* @return The data of the current node.
*/
ClioNode
selfData() const override;
/**
* @brief Get the data of all nodes in the cluster (including self).
*
* @return The data of all nodes in the cluster or error if the service is not healthy.
*/
std::expected<std::vector<ClioNode>, std::string>
clusterData() const override;
private:
void
doRead(boost::asio::yield_context yield);
void
doWrite();
}; };
} // namespace cluster } // namespace cluster

39
src/cluster/Concepts.hpp Normal file
View File

@@ -0,0 +1,39 @@
//------------------------------------------------------------------------------
/*
This file is part of clio: https://github.com/XRPLF/clio
Copyright (c) 2025, the clio developers.
Permission to use, copy, modify, and distribute this software for any
purpose with or without fee is hereby granted, provided that the above
copyright notice and this permission notice appear in all copies.
THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
*/
//==============================================================================
#pragma once
#include <concepts>
namespace cluster {
/**
* @brief Tag type for cluster communication service implementations.
*
* This tag is used to identify types that implement cluster communication functionality.
* Types should inherit from this tag to be recognized as cluster communication services.
*/
struct ClusterCommunicationServiceTag {
virtual ~ClusterCommunicationServiceTag() = default;
};
template <typename T>
concept SomeClusterCommunicationService = std::derived_from<T, ClusterCommunicationServiceTag>;
} // namespace cluster

View File

@@ -17,38 +17,31 @@
*/ */
//============================================================================== //==============================================================================
#pragma once #include "cluster/Metrics.hpp"
#include "cluster/Backend.hpp"
#include "cluster/ClioNode.hpp" #include "cluster/ClioNode.hpp"
#include <expected> #include <memory>
#include <string>
#include <vector>
namespace cluster { namespace cluster {
/** Metrics::Metrics()
* @brief Interface for the cluster communication service. {
*/ nodesInClusterMetric_.set(1); // The node always sees itself
class ClusterCommunicationServiceInterface { isHealthy_ = true;
public: }
virtual ~ClusterCommunicationServiceInterface() = default;
/** void
* @brief Get the data of the current node. Metrics::onNewState(ClioNode::CUuid, std::shared_ptr<Backend::ClusterData const> clusterData)
* {
* @return The data of the current node. if (clusterData->has_value()) {
*/ isHealthy_ = true;
[[nodiscard]] virtual ClioNode nodesInClusterMetric_.set(clusterData->value().size());
selfData() const = 0; } else {
isHealthy_ = false;
/** nodesInClusterMetric_.set(1);
* @brief Get the data of all nodes in the cluster (including self). }
* }
* @return The data of all nodes in the cluster or error if the service is not healthy.
*/
[[nodiscard]] virtual std::expected<std::vector<ClioNode>, std::string>
clusterData() const = 0;
};
} // namespace cluster } // namespace cluster

78
src/cluster/Metrics.hpp Normal file
View File

@@ -0,0 +1,78 @@
//------------------------------------------------------------------------------
/*
This file is part of clio: https://github.com/XRPLF/clio
Copyright (c) 2025, the clio developers.
Permission to use, copy, modify, and distribute this software for any
purpose with or without fee is hereby granted, provided that the above
copyright notice and this permission notice appear in all copies.
THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
*/
//==============================================================================
#pragma once
#include "cluster/Backend.hpp"
#include "cluster/ClioNode.hpp"
#include "util/prometheus/Bool.hpp"
#include "util/prometheus/Gauge.hpp"
#include "util/prometheus/Prometheus.hpp"
#include <memory>
namespace cluster {
/**
* @brief Manages Prometheus metrics for cluster communication and node tracking.
*
* This class tracks cluster-related metrics including:
* - Total number of nodes detected in the cluster
* - Health status of cluster communication
*/
class Metrics {
/** @brief Gauge tracking the total number of nodes visible in the cluster */
util::prometheus::GaugeInt& nodesInClusterMetric_ = PrometheusService::gaugeInt(
"cluster_nodes_total_number",
{},
"Total number of nodes this node can detect in the cluster."
);
/** @brief Boolean metric indicating whether cluster communication is healthy */
util::prometheus::Bool isHealthy_ = PrometheusService::boolMetric(
"cluster_communication_is_healthy",
{},
"Whether cluster communication service is operating healthy (1 - healthy, 0 - we have a "
"problem)"
);
public:
/**
* @brief Constructs a Metrics instance and initializes metrics.
*
* Sets the initial node count to 1 (self) and marks communication as healthy.
*/
Metrics();
/**
* @brief Updates metrics based on new cluster state.
*
* This callback is invoked when cluster state changes. It updates:
* - Health status based on whether cluster data is available
* - Node count to reflect the current cluster size
*
* @param uuid The UUID of the node (unused in current implementation)
* @param clusterData Shared pointer to the current cluster data; may be empty if communication
* failed
*/
void
onNewState(ClioNode::CUuid uuid, std::shared_ptr<Backend::ClusterData const> clusterData);
};
} // namespace cluster

View File

@@ -0,0 +1,106 @@
//------------------------------------------------------------------------------
/*
This file is part of clio: https://github.com/XRPLF/clio
Copyright (c) 2025, the clio developers.
Permission to use, copy, modify, and distribute this software for any
purpose with or without fee is hereby granted, provided that the above
copyright notice and this permission notice appear in all copies.
THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
*/
//==============================================================================
#include "cluster/WriterDecider.hpp"
#include "cluster/Backend.hpp"
#include "cluster/ClioNode.hpp"
#include "etl/WriterState.hpp"
#include "util/Assert.hpp"
#include "util/Spawn.hpp"
#include <boost/asio/thread_pool.hpp>
#include <algorithm>
#include <memory>
#include <utility>
#include <vector>
namespace cluster {
WriterDecider::WriterDecider(
boost::asio::thread_pool& ctx,
std::unique_ptr<etl::WriterStateInterface> writerState
)
: ctx_(ctx), writerState_(std::move(writerState))
{
}
void
WriterDecider::onNewState(
ClioNode::CUuid selfId,
std::shared_ptr<Backend::ClusterData const> clusterData
)
{
if (not clusterData->has_value())
return;
util::spawn(
ctx_,
[writerState = writerState_->clone(),
selfId = std::move(selfId),
clusterData = clusterData->value()](auto&&) mutable {
auto const selfData = std::ranges::find_if(
clusterData, [&selfId](ClioNode const& node) { return node.uuid == selfId; }
);
ASSERT(selfData != clusterData.end(), "Self data should always be in the cluster data");
if (selfData->dbRole == ClioNode::DbRole::Fallback) {
return;
}
if (selfData->dbRole == ClioNode::DbRole::ReadOnly) {
writerState->giveUpWriting();
return;
}
// If any node in the cluster is in Fallback mode, the entire cluster must switch
// to the fallback writer decision mechanism for consistency
if (std::ranges::any_of(clusterData, [](ClioNode const& node) {
return node.dbRole == ClioNode::DbRole::Fallback;
})) {
writerState->setWriterDecidingFallback();
return;
}
// We are not ReadOnly and there is no Fallback in the cluster
std::ranges::sort(clusterData, [](ClioNode const& lhs, ClioNode const& rhs) {
return *lhs.uuid < *rhs.uuid;
});
auto const it = std::ranges::find_if(clusterData, [](ClioNode const& node) {
return node.dbRole == ClioNode::DbRole::NotWriter or
node.dbRole == ClioNode::DbRole::Writer;
});
if (it == clusterData.end()) {
// No writer nodes in the cluster yet
return;
}
if (*it->uuid == *selfId) {
writerState->startWriting();
} else {
writerState->giveUpWriting();
}
}
);
}
} // namespace cluster

View File

@@ -0,0 +1,79 @@
//------------------------------------------------------------------------------
/*
This file is part of clio: https://github.com/XRPLF/clio
Copyright (c) 2025, the clio developers.
Permission to use, copy, modify, and distribute this software for any
purpose with or without fee is hereby granted, provided that the above
copyright notice and this permission notice appear in all copies.
THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
*/
//==============================================================================
#pragma once
#include "cluster/Backend.hpp"
#include "cluster/ClioNode.hpp"
#include "etl/WriterState.hpp"
#include <boost/asio/thread_pool.hpp>
#include <memory>
namespace cluster {
/**
* @brief Decides which node in the cluster should be the writer based on cluster state.
*
* This class monitors cluster state changes and determines whether the current node
* should act as the writer to the database. The decision is made by:
* 1. Sorting all nodes by UUID for deterministic ordering
* 2. Selecting the first node that is allowed to write (not ReadOnly)
* 3. Activating writing on this node if it's the current node, otherwise deactivating
*
* This ensures only one node in the cluster actively writes to the database at a time.
*/
class WriterDecider {
/** @brief Thread pool for spawning asynchronous tasks */
boost::asio::thread_pool& ctx_;
/** @brief Interface for controlling the writer state of this node */
std::unique_ptr<etl::WriterStateInterface> writerState_;
public:
/**
* @brief Constructs a WriterDecider.
*
* @param ctx Thread pool for executing asynchronous operations
* @param writerState Writer state interface for controlling write operations
*/
WriterDecider(
boost::asio::thread_pool& ctx,
std::unique_ptr<etl::WriterStateInterface> writerState
);
/**
* @brief Handles cluster state changes and decides whether this node should be the writer.
*
* This method is called when cluster state changes. It asynchronously:
* - Sorts all nodes by UUID to establish a deterministic order
* - Identifies the first node allowed to write (not ReadOnly)
* - Activates writing if this node is selected, otherwise deactivates writing
* - Logs a warning if no nodes in the cluster are allowed to write
*
* @param selfId The UUID of the current node
* @param clusterData Shared pointer to current cluster data; may be empty if communication
* failed
*/
void
onNewState(ClioNode::CUuid selfId, std::shared_ptr<Backend::ClusterData const> clusterData);
};
} // namespace cluster

View File

@@ -0,0 +1,109 @@
//------------------------------------------------------------------------------
/*
This file is part of clio: https://github.com/XRPLF/clio
Copyright (c) 2025, the clio developers.
Permission to use, copy, modify, and distribute this software for any
purpose with or without fee is hereby granted, provided that the above
copyright notice and this permission notice appear in all copies.
THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
*/
//==============================================================================
#pragma once
#include "util/Assert.hpp"
#include "util/Spawn.hpp"
#include <boost/asio/bind_cancellation_slot.hpp>
#include <boost/asio/cancellation_signal.hpp>
#include <boost/asio/cancellation_type.hpp>
#include <boost/asio/error.hpp>
#include <boost/asio/executor.hpp>
#include <boost/asio/spawn.hpp>
#include <boost/asio/steady_timer.hpp>
#include <boost/asio/strand.hpp>
#include <atomic>
#include <chrono>
#include <concepts>
#include <semaphore>
namespace cluster::impl {
// TODO: Try to replace util::Repeat by this. https://github.com/XRPLF/clio/issues/2926
template <typename Context>
class RepeatedTask {
std::chrono::steady_clock::duration interval_;
boost::asio::strand<typename Context::executor_type> strand_;
enum class State { Running, Stopped };
std::atomic<State> state_ = State::Stopped;
std::binary_semaphore semaphore_{0};
boost::asio::steady_timer timer_;
public:
RepeatedTask(std::chrono::steady_clock::duration interval, Context& ctx)
: interval_(interval), strand_(boost::asio::make_strand(ctx)), timer_(strand_)
{
}
~RepeatedTask()
{
stop();
}
template <typename Fn>
requires std::invocable<Fn, boost::asio::yield_context> or std::invocable<Fn>
void
run(Fn&& f)
{
ASSERT(state_ == State::Stopped, "Can only be ran once");
state_ = State::Running;
util::spawn(strand_, [this, f = std::forward<Fn>(f)](boost::asio::yield_context yield) {
boost::system::error_code ec;
while (state_ == State::Running) {
timer_.expires_after(interval_);
timer_.async_wait(yield[ec]);
if (ec or state_ != State::Running)
break;
if constexpr (std::invocable<decltype(f), boost::asio::yield_context>) {
f(yield);
} else {
f();
}
}
semaphore_.release();
});
}
void
stop()
{
if (auto expected = State::Running;
not state_.compare_exchange_strong(expected, State::Stopped))
return; // Already stopped or not started
std::binary_semaphore cancelSemaphore{0};
boost::asio::post(strand_, [this, &cancelSemaphore]() {
timer_.cancel();
cancelSemaphore.release();
});
cancelSemaphore.acquire();
semaphore_.acquire();
}
};
} // namespace cluster::impl

View File

@@ -57,10 +57,15 @@ supportedAmendments()
} }
bool bool
lookupAmendment(auto const& allAmendments, std::vector<ripple::uint256> const& ledgerAmendments, std::string_view name) lookupAmendment(
auto const& allAmendments,
std::vector<ripple::uint256> const& ledgerAmendments,
std::string_view name
)
{ {
namespace rg = std::ranges; namespace rg = std::ranges;
if (auto const am = rg::find(allAmendments, name, &data::Amendment::name); am != rg::end(allAmendments)) if (auto const am = rg::find(allAmendments, name, &data::Amendment::name);
am != rg::end(allAmendments))
return rg::find(ledgerAmendments, am->feature) != rg::end(ledgerAmendments); return rg::find(ledgerAmendments, am->feature) != rg::end(ledgerAmendments);
return false; return false;
} }
@@ -70,9 +75,12 @@ lookupAmendment(auto const& allAmendments, std::vector<ripple::uint256> const& l
namespace data { namespace data {
namespace impl { namespace impl {
WritingAmendmentKey::WritingAmendmentKey(std::string amendmentName) : AmendmentKey{std::move(amendmentName)} WritingAmendmentKey::WritingAmendmentKey(std::string amendmentName)
: AmendmentKey{std::move(amendmentName)}
{ {
ASSERT(not supportedAmendments().contains(name), "Attempt to register the same amendment twice"); ASSERT(
not supportedAmendments().contains(name), "Attempt to register the same amendment twice"
);
supportedAmendments().insert(name); supportedAmendments().insert(name);
} }
@@ -96,7 +104,8 @@ operator ripple::uint256() const
return Amendment::getAmendmentId(name); return Amendment::getAmendmentId(name);
} }
AmendmentCenter::AmendmentCenter(std::shared_ptr<data::BackendInterface> const& backend) : backend_{backend} AmendmentCenter::AmendmentCenter(std::shared_ptr<data::BackendInterface> const& backend)
: backend_{backend}
{ {
namespace rg = std::ranges; namespace rg = std::ranges;
namespace vs = std::views; namespace vs = std::views;
@@ -108,7 +117,8 @@ AmendmentCenter::AmendmentCenter(std::shared_ptr<data::BackendInterface> const&
.name = name, .name = name,
.feature = Amendment::getAmendmentId(name), .feature = Amendment::getAmendmentId(name),
.isSupportedByXRPL = support != ripple::AmendmentSupport::Unsupported, .isSupportedByXRPL = support != ripple::AmendmentSupport::Unsupported,
.isSupportedByClio = rg::find(supportedAmendments(), name) != rg::end(supportedAmendments()), .isSupportedByClio =
rg::find(supportedAmendments(), name) != rg::end(supportedAmendments()),
.isRetired = support == ripple::AmendmentSupport::Retired .isRetired = support == ripple::AmendmentSupport::Retired
}; };
}), }),
@@ -144,19 +154,28 @@ AmendmentCenter::isEnabled(AmendmentKey const& key, uint32_t seq) const
} }
bool bool
AmendmentCenter::isEnabled(boost::asio::yield_context yield, AmendmentKey const& key, uint32_t seq) const AmendmentCenter::isEnabled(
boost::asio::yield_context yield,
AmendmentKey const& key,
uint32_t seq
) const
{ {
try { try {
if (auto const listAmendments = fetchAmendmentsList(yield, seq); listAmendments) if (auto const listAmendments = fetchAmendmentsList(yield, seq); listAmendments)
return lookupAmendment(all_, *listAmendments, key); return lookupAmendment(all_, *listAmendments, key);
} catch (std::runtime_error const&) { } catch (std::runtime_error const&) {
return false; // Some old ledger does not contain Amendments ledger object so do best we can for now return false; // Some old ledger does not contain Amendments ledger object so do best we
// can for now
} }
return false; return false;
} }
std::vector<bool> std::vector<bool>
AmendmentCenter::isEnabled(boost::asio::yield_context yield, std::vector<AmendmentKey> const& keys, uint32_t seq) const AmendmentCenter::isEnabled(
boost::asio::yield_context yield,
std::vector<AmendmentKey> const& keys,
uint32_t seq
) const
{ {
namespace rg = std::ranges; namespace rg = std::ranges;
@@ -181,7 +200,11 @@ AmendmentCenter::isEnabled(boost::asio::yield_context yield, std::vector<Amendme
Amendment const& Amendment const&
AmendmentCenter::getAmendment(AmendmentKey const& key) const AmendmentCenter::getAmendment(AmendmentKey const& key) const
{ {
ASSERT(supported_.contains(key), "The amendment '{}' must be present in supported amendments list", key.name); ASSERT(
supported_.contains(key),
"The amendment '{}' must be present in supported amendments list",
key.name
);
return supported_.at(key); return supported_.at(key);
} }
@@ -201,7 +224,8 @@ std::optional<std::vector<ripple::uint256>>
AmendmentCenter::fetchAmendmentsList(boost::asio::yield_context yield, uint32_t seq) const AmendmentCenter::fetchAmendmentsList(boost::asio::yield_context yield, uint32_t seq) const
{ {
// the amendments should always be present on the ledger // the amendments should always be present on the ledger
auto const amendments = backend_->fetchLedgerObject(ripple::keylet::amendments().key, seq, yield); auto const amendments =
backend_->fetchLedgerObject(ripple::keylet::amendments().key, seq, yield);
if (not amendments.has_value()) if (not amendments.has_value())
throw std::runtime_error("Amendments ledger object must be present in the database"); throw std::runtime_error("Amendments ledger object must be present in the database");

View File

@@ -62,9 +62,9 @@ struct WritingAmendmentKey : AmendmentKey {
*/ */
struct Amendments { struct Amendments {
// NOTE: if Clio wants to report it supports an Amendment it should be listed here. // NOTE: if Clio wants to report it supports an Amendment it should be listed here.
// Whether an amendment is obsolete and/or supported by libxrpl is extracted directly from libxrpl. // Whether an amendment is obsolete and/or supported by libxrpl is extracted directly from
// If an amendment is in the list below it just means Clio did whatever changes needed to support it. // libxrpl. If an amendment is in the list below it just means Clio did whatever changes needed
// Most of the time it's going to be no changes at all. // to support it. Most of the time it's going to be no changes at all.
/** @cond */ /** @cond */
// NOLINTBEGIN(readability-identifier-naming) // NOLINTBEGIN(readability-identifier-naming)
@@ -177,6 +177,7 @@ struct Amendments {
REGISTER(fix1512); REGISTER(fix1512);
REGISTER(fix1523); REGISTER(fix1523);
REGISTER(fix1528); REGISTER(fix1528);
REGISTER(fixBatchInnerSigs);
// NOLINTEND(readability-identifier-naming) // NOLINTEND(readability-identifier-naming)
/** @endcond */ /** @endcond */
}; };
@@ -255,7 +256,11 @@ public:
* @return A vector of bools representing enabled state for each of the given keys * @return A vector of bools representing enabled state for each of the given keys
*/ */
[[nodiscard]] std::vector<bool> [[nodiscard]] std::vector<bool>
isEnabled(boost::asio::yield_context yield, std::vector<AmendmentKey> const& keys, uint32_t seq) const final; isEnabled(
boost::asio::yield_context yield,
std::vector<AmendmentKey> const& keys,
uint32_t seq
) const final;
/** /**
* @brief Get an amendment * @brief Get an amendment

View File

@@ -92,7 +92,11 @@ public:
* @return A vector of bools representing enabled state for each of the given keys * @return A vector of bools representing enabled state for each of the given keys
*/ */
[[nodiscard]] virtual std::vector<bool> [[nodiscard]] virtual std::vector<bool>
isEnabled(boost::asio::yield_context yield, std::vector<AmendmentKey> const& keys, uint32_t seq) const = 0; isEnabled(
boost::asio::yield_context yield,
std::vector<AmendmentKey> const& keys,
uint32_t seq
) const = 0;
/** /**
* @brief Get an amendment * @brief Get an amendment

View File

@@ -41,7 +41,10 @@ std::vector<std::int64_t> const kHISTOGRAM_BUCKETS{1, 2, 5, 10, 20, 50, 100, 200
std::int64_t std::int64_t
durationInMillisecondsSince(std::chrono::steady_clock::time_point const startTime) durationInMillisecondsSince(std::chrono::steady_clock::time_point const startTime)
{ {
return std::chrono::duration_cast<std::chrono::milliseconds>(std::chrono::steady_clock::now() - startTime).count(); return std::chrono::duration_cast<std::chrono::milliseconds>(
std::chrono::steady_clock::now() - startTime
)
.count();
} }
} // namespace } // namespace
@@ -144,7 +147,10 @@ BackendCounters::registerReadStarted(std::uint64_t const count)
} }
void void
BackendCounters::registerReadFinished(std::chrono::steady_clock::time_point const startTime, std::uint64_t const count) BackendCounters::registerReadFinished(
std::chrono::steady_clock::time_point const startTime,
std::uint64_t const count
)
{ {
asyncReadCounters_.registerFinished(count); asyncReadCounters_.registerFinished(count);
auto const duration = durationInMillisecondsSince(startTime); auto const duration = durationInMillisecondsSince(startTime);
@@ -238,7 +244,8 @@ void
BackendCounters::AsyncOperationCounters::registerError(std::uint64_t count) BackendCounters::AsyncOperationCounters::registerError(std::uint64_t count)
{ {
ASSERT( ASSERT(
pendingCounter_.get().value() >= static_cast<std::int64_t>(count), "Error operations can't be more than pending" pendingCounter_.get().value() >= static_cast<std::int64_t>(count),
"Error operations can't be more than pending"
); );
pendingCounter_.get() -= count; pendingCounter_.get() -= count;
errorCounter_.get() += count; errorCounter_.get() += count;

View File

@@ -46,7 +46,9 @@ concept SomeBackendCounters = requires(T a) {
{ a.registerWriteFinished(std::chrono::steady_clock::time_point{}) } -> std::same_as<void>; { a.registerWriteFinished(std::chrono::steady_clock::time_point{}) } -> std::same_as<void>;
{ a.registerWriteRetry() } -> std::same_as<void>; { a.registerWriteRetry() } -> std::same_as<void>;
{ a.registerReadStarted(std::uint64_t{}) } -> std::same_as<void>; { a.registerReadStarted(std::uint64_t{}) } -> std::same_as<void>;
{ a.registerReadFinished(std::chrono::steady_clock::time_point{}, std::uint64_t{}) } -> std::same_as<void>; {
a.registerReadFinished(std::chrono::steady_clock::time_point{}, std::uint64_t{})
} -> std::same_as<void>;
{ a.registerReadRetry(std::uint64_t{}) } -> std::same_as<void>; { a.registerReadRetry(std::uint64_t{}) } -> std::same_as<void>;
{ a.registerReadError(std::uint64_t{}) } -> std::same_as<void>; { a.registerReadError(std::uint64_t{}) } -> std::same_as<void>;
{ a.report() } -> std::same_as<boost::json::object>; { a.report() } -> std::same_as<boost::json::object>;

View File

@@ -128,7 +128,8 @@ BackendInterface::fetchLedgerObjects(
misses.push_back(keys[i]); misses.push_back(keys[i]);
} }
} }
LOG(log_.trace()) << "Cache hits = " << keys.size() - misses.size() << " - cache misses = " << misses.size(); LOG(log_.trace()) << "Cache hits = " << keys.size() - misses.size()
<< " - cache misses = " << misses.size();
if (!misses.empty()) { if (!misses.empty()) {
auto objs = doFetchLedgerObjects(misses, sequence, yield); auto objs = doFetchLedgerObjects(misses, sequence, yield);
@@ -192,7 +193,9 @@ BackendInterface::fetchBookOffers(
ripple::uint256 const bookEnd = ripple::getQualityNext(book); ripple::uint256 const bookEnd = ripple::getQualityNext(book);
ripple::uint256 uTipIndex = book; ripple::uint256 uTipIndex = book;
std::vector<ripple::uint256> keys; std::vector<ripple::uint256> keys;
auto getMillis = [](auto diff) { return std::chrono::duration_cast<std::chrono::milliseconds>(diff).count(); }; auto getMillis = [](auto diff) {
return std::chrono::duration_cast<std::chrono::milliseconds>(diff).count();
};
auto begin = std::chrono::system_clock::now(); auto begin = std::chrono::system_clock::now();
std::uint32_t numSucc = 0; std::uint32_t numSucc = 0;
std::uint32_t numPages = 0; std::uint32_t numPages = 0;
@@ -233,20 +236,23 @@ BackendInterface::fetchBookOffers(
auto mid = std::chrono::system_clock::now(); auto mid = std::chrono::system_clock::now();
auto objs = fetchLedgerObjects(keys, ledgerSequence, yield); auto objs = fetchLedgerObjects(keys, ledgerSequence, yield);
for (size_t i = 0; i < keys.size() && i < limit; ++i) { for (size_t i = 0; i < keys.size() && i < limit; ++i) {
LOG(log_.trace()) << "Key = " << ripple::strHex(keys[i]) << " blob = " << ripple::strHex(objs[i]) LOG(log_.trace()) << "Key = " << ripple::strHex(keys[i])
<< " blob = " << ripple::strHex(objs[i])
<< " ledgerSequence = " << ledgerSequence; << " ledgerSequence = " << ledgerSequence;
ASSERT(!objs[i].empty(), "Ledger object can't be empty"); ASSERT(!objs[i].empty(), "Ledger object can't be empty");
page.offers.push_back({keys[i], objs[i]}); page.offers.push_back({keys[i], objs[i]});
} }
auto end = std::chrono::system_clock::now(); auto end = std::chrono::system_clock::now();
LOG(log_.debug()) << "Fetching " << std::to_string(keys.size()) << " offers took " LOG(log_.debug()) << "Fetching " << std::to_string(keys.size()) << " offers took "
<< std::to_string(getMillis(mid - begin)) << " milliseconds. Fetching next dir took " << std::to_string(getMillis(mid - begin))
<< std::to_string(succMillis) << " milliseconds. Fetched next dir " << std::to_string(numSucc) << " milliseconds. Fetching next dir took " << std::to_string(succMillis)
<< " times" << " milliseconds. Fetched next dir " << std::to_string(numSucc) << " times"
<< " Fetching next page of dir took " << std::to_string(pageMillis) << " milliseconds" << " Fetching next page of dir took " << std::to_string(pageMillis)
<< ". num pages = " << std::to_string(numPages) << ". Fetching all objects took " << " milliseconds"
<< std::to_string(getMillis(end - mid)) << ". num pages = " << std::to_string(numPages)
<< " milliseconds. total time = " << std::to_string(getMillis(end - begin)) << " milliseconds" << ". Fetching all objects took " << std::to_string(getMillis(end - mid))
<< " milliseconds. total time = " << std::to_string(getMillis(end - begin))
<< " milliseconds"
<< " book = " << ripple::strHex(book); << " book = " << ripple::strHex(book);
return page; return page;
@@ -273,7 +279,8 @@ BackendInterface::updateRange(uint32_t newMax)
if (range_.has_value() and newMax < range_->maxSequence) { if (range_.has_value() and newMax < range_->maxSequence) {
ASSERT( ASSERT(
false, false,
"Range shouldn't exist yet or newMax should be at least range->maxSequence. newMax = {}, " "Range shouldn't exist yet or newMax should be at least range->maxSequence. newMax = "
"{}, "
"range->maxSequence = {}", "range->maxSequence = {}",
newMax, newMax,
range_->maxSequence range_->maxSequence
@@ -339,8 +346,8 @@ BackendInterface::fetchLedgerPage(
if (!objects[i].empty()) { if (!objects[i].empty()) {
page.objects.push_back({keys[i], std::move(objects[i])}); page.objects.push_back({keys[i], std::move(objects[i])});
} else if (!outOfOrder) { } else if (!outOfOrder) {
LOG(log_.error()) << "Deleted or non-existent object in successor table. key = " << ripple::strHex(keys[i]) LOG(log_.error()) << "Deleted or non-existent object in successor table. key = "
<< " - seq = " << ledgerSequence; << ripple::strHex(keys[i]) << " - seq = " << ledgerSequence;
std::stringstream msg; std::stringstream msg;
for (size_t j = 0; j < objects.size(); ++j) { for (size_t j = 0; j < objects.size(); ++j) {
msg << " - " << ripple::strHex(keys[j]); msg << " - " << ripple::strHex(keys[j]);

View File

@@ -109,18 +109,23 @@ synchronous(FnType&& func)
using R = typename boost::result_of<FnType(boost::asio::yield_context)>::type; using R = typename boost::result_of<FnType(boost::asio::yield_context)>::type;
if constexpr (!std::is_same_v<R, void>) { if constexpr (!std::is_same_v<R, void>) {
R res; R res;
util::spawn(ctx, [_ = boost::asio::make_work_guard(ctx), &func, &res](auto yield) { res = func(yield); }); util::spawn(ctx, [_ = boost::asio::make_work_guard(ctx), &func, &res](auto yield) {
res = func(yield);
});
ctx.run(); ctx.run();
return res; return res;
} else { } else {
util::spawn(ctx, [_ = boost::asio::make_work_guard(ctx), &func](auto yield) { func(yield); }); util::spawn(ctx, [_ = boost::asio::make_work_guard(ctx), &func](auto yield) {
func(yield);
});
ctx.run(); ctx.run();
} }
} }
/** /**
* @brief Synchronously execute the given function object and retry until no DatabaseTimeout is thrown. * @brief Synchronously execute the given function object and retry until no DatabaseTimeout is
* thrown.
* *
* @tparam FnType The type of function object to execute * @tparam FnType The type of function object to execute
* @param func The function object to execute * @param func The function object to execute
@@ -225,7 +230,8 @@ public:
fetchLedgerRange() const; fetchLedgerRange() const;
/** /**
* @brief Fetch the specified number of account root object indexes by page, the accounts need to exist for seq. * @brief Fetch the specified number of account root object indexes by page, the accounts need
* to exist for seq.
* *
* @param number The number of accounts to fetch * @param number The number of accounts to fetch
* @param pageSize The maximum number of accounts per page * @param pageSize The maximum number of accounts per page
@@ -296,7 +302,10 @@ public:
* @return A vector of TransactionAndMetadata matching the given hashes * @return A vector of TransactionAndMetadata matching the given hashes
*/ */
virtual std::vector<TransactionAndMetadata> virtual std::vector<TransactionAndMetadata>
fetchTransactions(std::vector<ripple::uint256> const& hashes, boost::asio::yield_context yield) const = 0; fetchTransactions(
std::vector<ripple::uint256> const& hashes,
boost::asio::yield_context yield
) const = 0;
/** /**
* @brief Fetches all transactions for a specific account. * @brief Fetches all transactions for a specific account.
@@ -325,7 +334,10 @@ public:
* @return Results as a vector of TransactionAndMetadata * @return Results as a vector of TransactionAndMetadata
*/ */
virtual std::vector<TransactionAndMetadata> virtual std::vector<TransactionAndMetadata>
fetchAllTransactionsInLedger(std::uint32_t ledgerSequence, boost::asio::yield_context yield) const = 0; fetchAllTransactionsInLedger(
std::uint32_t ledgerSequence,
boost::asio::yield_context yield
) const = 0;
/** /**
* @brief Fetches all transaction hashes from a specific ledger. * @brief Fetches all transaction hashes from a specific ledger.
@@ -335,7 +347,10 @@ public:
* @return Hashes as ripple::uint256 in a vector * @return Hashes as ripple::uint256 in a vector
*/ */
virtual std::vector<ripple::uint256> virtual std::vector<ripple::uint256>
fetchAllTransactionHashesInLedger(std::uint32_t ledgerSequence, boost::asio::yield_context yield) const = 0; fetchAllTransactionHashesInLedger(
std::uint32_t ledgerSequence,
boost::asio::yield_context yield
) const = 0;
/** /**
* @brief Fetches a specific NFT. * @brief Fetches a specific NFT.
@@ -346,7 +361,11 @@ public:
* @return NFT object on success; nullopt otherwise * @return NFT object on success; nullopt otherwise
*/ */
virtual std::optional<NFT> virtual std::optional<NFT>
fetchNFT(ripple::uint256 const& tokenID, std::uint32_t ledgerSequence, boost::asio::yield_context yield) const = 0; fetchNFT(
ripple::uint256 const& tokenID,
std::uint32_t ledgerSequence,
boost::asio::yield_context yield
) const = 0;
/** /**
* @brief Fetches all transactions for a specific NFT. * @brief Fetches all transactions for a specific NFT.
@@ -376,7 +395,8 @@ public:
* @param limit Paging limit. * @param limit Paging limit.
* @param cursorIn Optional cursor to allow us to pick up from where we last left off. * @param cursorIn Optional cursor to allow us to pick up from where we last left off.
* @param yield Currently executing coroutine. * @param yield Currently executing coroutine.
* @return NFTs issued by this account, or this issuer/taxon combination if taxon is passed and an optional marker * @return NFTs issued by this account, or this issuer/taxon combination if taxon is passed and
* an optional marker
*/ */
virtual NFTsAndCursor virtual NFTsAndCursor
fetchNFTsByIssuer( fetchNFTsByIssuer(
@@ -410,8 +430,8 @@ public:
/** /**
* @brief Fetches a specific ledger object. * @brief Fetches a specific ledger object.
* *
* Currently the real fetch happens in doFetchLedgerObject and fetchLedgerObject attempts to fetch from Cache first * Currently the real fetch happens in doFetchLedgerObject and fetchLedgerObject attempts to
* and only calls out to the real DB if a cache miss occurred. * fetch from Cache first and only calls out to the real DB if a cache miss occurred.
* *
* @param key The key of the object * @param key The key of the object
* @param sequence The ledger sequence to fetch for * @param sequence The ledger sequence to fetch for
@@ -419,7 +439,11 @@ public:
* @return The object as a Blob on success; nullopt otherwise * @return The object as a Blob on success; nullopt otherwise
*/ */
std::optional<Blob> std::optional<Blob>
fetchLedgerObject(ripple::uint256 const& key, std::uint32_t sequence, boost::asio::yield_context yield) const; fetchLedgerObject(
ripple::uint256 const& key,
std::uint32_t sequence,
boost::asio::yield_context yield
) const;
/** /**
* @brief Fetches a specific ledger object sequence. * @brief Fetches a specific ledger object sequence.
@@ -432,13 +456,18 @@ public:
* @return The sequence in unit32_t on success; nullopt otherwise * @return The sequence in unit32_t on success; nullopt otherwise
*/ */
std::optional<std::uint32_t> std::optional<std::uint32_t>
fetchLedgerObjectSeq(ripple::uint256 const& key, std::uint32_t sequence, boost::asio::yield_context yield) const; fetchLedgerObjectSeq(
ripple::uint256 const& key,
std::uint32_t sequence,
boost::asio::yield_context yield
) const;
/** /**
* @brief Fetches all ledger objects by their keys. * @brief Fetches all ledger objects by their keys.
* *
* Currently the real fetch happens in doFetchLedgerObjects and fetchLedgerObjects attempts to fetch from Cache * Currently the real fetch happens in doFetchLedgerObjects and fetchLedgerObjects attempts to
* first and only calls out to the real DB for each of the keys that was not found in the cache. * fetch from Cache first and only calls out to the real DB for each of the keys that was not
* found in the cache.
* *
* @param keys A vector with the keys of the objects to fetch * @param keys A vector with the keys of the objects to fetch
* @param sequence The ledger sequence to fetch for * @param sequence The ledger sequence to fetch for
@@ -461,7 +490,11 @@ public:
* @return The object as a Blob on success; nullopt otherwise * @return The object as a Blob on success; nullopt otherwise
*/ */
virtual std::optional<Blob> virtual std::optional<Blob>
doFetchLedgerObject(ripple::uint256 const& key, std::uint32_t sequence, boost::asio::yield_context yield) const = 0; doFetchLedgerObject(
ripple::uint256 const& key,
std::uint32_t sequence,
boost::asio::yield_context yield
) const = 0;
/** /**
* @brief The database-specific implementation for fetching a ledger object sequence. * @brief The database-specific implementation for fetching a ledger object sequence.
@@ -531,13 +564,18 @@ public:
* @return The successor on success; nullopt otherwise * @return The successor on success; nullopt otherwise
*/ */
std::optional<LedgerObject> std::optional<LedgerObject>
fetchSuccessorObject(ripple::uint256 key, std::uint32_t ledgerSequence, boost::asio::yield_context yield) const; fetchSuccessorObject(
ripple::uint256 key,
std::uint32_t ledgerSequence,
boost::asio::yield_context yield
) const;
/** /**
* @brief Fetches the successor key. * @brief Fetches the successor key.
* *
* Thea real fetch happens in doFetchSuccessorKey. This function will attempt to lookup the successor in the cache * Thea real fetch happens in doFetchSuccessorKey. This function will attempt to lookup the
* first and only if it's not found in the cache will it fetch from the actual DB. * successor in the cache first and only if it's not found in the cache will it fetch from the
* actual DB.
* *
* @param key The key to fetch for * @param key The key to fetch for
* @param ledgerSequence The ledger sequence to fetch for * @param ledgerSequence The ledger sequence to fetch for
@@ -545,7 +583,11 @@ public:
* @return The successor key on success; nullopt otherwise * @return The successor key on success; nullopt otherwise
*/ */
std::optional<ripple::uint256> std::optional<ripple::uint256>
fetchSuccessorKey(ripple::uint256 key, std::uint32_t ledgerSequence, boost::asio::yield_context yield) const; fetchSuccessorKey(
ripple::uint256 key,
std::uint32_t ledgerSequence,
boost::asio::yield_context yield
) const;
/** /**
* @brief Database-specific implementation of fetching the successor key * @brief Database-specific implementation of fetching the successor key
@@ -556,7 +598,11 @@ public:
* @return The successor on success; nullopt otherwise * @return The successor on success; nullopt otherwise
*/ */
virtual std::optional<ripple::uint256> virtual std::optional<ripple::uint256>
doFetchSuccessorKey(ripple::uint256 key, std::uint32_t ledgerSequence, boost::asio::yield_context yield) const = 0; doFetchSuccessorKey(
ripple::uint256 key,
std::uint32_t ledgerSequence,
boost::asio::yield_context yield
) const = 0;
/** /**
* @brief Fetches book offers. * @brief Fetches book offers.
@@ -583,7 +629,10 @@ public:
* @return The status of the migrator if found; nullopt otherwise * @return The status of the migrator if found; nullopt otherwise
*/ */
virtual std::optional<std::string> virtual std::optional<std::string>
fetchMigratorStatus(std::string const& migratorName, boost::asio::yield_context yield) const = 0; fetchMigratorStatus(
std::string const& migratorName,
boost::asio::yield_context yield
) const = 0;
/** @brief Return type for fetchClioNodesData() method */ /** @brief Return type for fetchClioNodesData() method */
using ClioNodesDataFetchResult = using ClioNodesDataFetchResult =
@@ -601,7 +650,8 @@ public:
/** /**
* @brief Synchronously fetches the ledger range from DB. * @brief Synchronously fetches the ledger range from DB.
* *
* This function just wraps hardFetchLedgerRange(boost::asio::yield_context) using synchronous(FnType&&). * This function just wraps hardFetchLedgerRange(boost::asio::yield_context) using
* synchronous(FnType&&).
* *
* @return The ledger range if available; nullopt otherwise * @return The ledger range if available; nullopt otherwise
*/ */

View File

@@ -1,23 +1,21 @@
add_library(clio_data) add_library(clio_data)
target_sources( target_sources(clio_data
clio_data PRIVATE AmendmentCenter.cpp
PRIVATE AmendmentCenter.cpp BackendCounters.cpp
BackendCounters.cpp BackendInterface.cpp
BackendInterface.cpp LedgerCache.cpp
LedgerCache.cpp LedgerCacheSaver.cpp
LedgerCacheSaver.cpp LedgerHeaderCache.cpp
LedgerHeaderCache.cpp cassandra/impl/Future.cpp
cassandra/impl/Future.cpp cassandra/impl/Cluster.cpp
cassandra/impl/Cluster.cpp cassandra/impl/Batch.cpp
cassandra/impl/Batch.cpp cassandra/impl/Result.cpp
cassandra/impl/Result.cpp cassandra/impl/Tuple.cpp
cassandra/impl/Tuple.cpp cassandra/impl/SslContext.cpp
cassandra/impl/SslContext.cpp cassandra/Handle.cpp
cassandra/Handle.cpp cassandra/SettingsProvider.cpp
cassandra/SettingsProvider.cpp impl/InputFile.cpp
impl/InputFile.cpp impl/LedgerCacheFile.cpp
impl/LedgerCacheFile.cpp impl/OutputFile.cpp)
impl/OutputFile.cpp
)
target_link_libraries(clio_data PUBLIC cassandra-cpp-driver::cassandra-cpp-driver clio_util) target_link_libraries(clio_data PUBLIC cassandra-cpp-driver::cassandra-cpp-driver clio_util)

View File

@@ -102,10 +102,14 @@ public:
this->waitForWritesToFinish(); this->waitForWritesToFinish();
if (!range_) { if (!range_) {
executor_.writeSync(schema_->updateLedgerRange, ledgerSequence_, false, ledgerSequence_); executor_.writeSync(
schema_->updateLedgerRange, ledgerSequence_, false, ledgerSequence_
);
} }
if (not this->executeSyncUpdate(schema_->updateLedgerRange.bind(ledgerSequence_, true, ledgerSequence_ - 1))) { if (not this->executeSyncUpdate(
schema_->updateLedgerRange.bind(ledgerSequence_, true, ledgerSequence_ - 1)
)) {
LOG(log_.warn()) << "Update failed for ledger " << ledgerSequence_; LOG(log_.warn()) << "Update failed for ledger " << ledgerSequence_;
return false; return false;
} }
@@ -139,7 +143,8 @@ public:
r.bindAt( r.bindAt(
1, 1,
std::make_tuple( std::make_tuple(
cursorIn.has_value() ? ripple::nft::toUInt32(ripple::nft::getTaxon(*cursorIn)) : 0, cursorIn.has_value() ? ripple::nft::toUInt32(ripple::nft::getTaxon(*cursorIn))
: 0,
cursorIn.value_or(ripple::uint256(0)) cursorIn.value_or(ripple::uint256(0))
) )
); );
@@ -170,9 +175,10 @@ public:
selectNFTStatements.reserve(nftIDs.size()); selectNFTStatements.reserve(nftIDs.size());
std::transform( std::transform(
std::cbegin(nftIDs), std::cend(nftIDs), std::back_inserter(selectNFTStatements), [&](auto const& nftID) { std::cbegin(nftIDs),
return schema_->selectNFT.bind(nftID, ledgerSequence); std::cend(nftIDs),
} std::back_inserter(selectNFTStatements),
[&](auto const& nftID) { return schema_->selectNFT.bind(nftID, ledgerSequence); }
); );
auto const nftInfos = executor_.readEach(yield, selectNFTStatements); auto const nftInfos = executor_.readEach(yield, selectNFTStatements);
@@ -181,9 +187,10 @@ public:
selectNFTURIStatements.reserve(nftIDs.size()); selectNFTURIStatements.reserve(nftIDs.size());
std::transform( std::transform(
std::cbegin(nftIDs), std::cend(nftIDs), std::back_inserter(selectNFTURIStatements), [&](auto const& nftID) { std::cbegin(nftIDs),
return schema_->selectNFTURI.bind(nftID, ledgerSequence); std::cend(nftIDs),
} std::back_inserter(selectNFTURIStatements),
[&](auto const& nftID) { return schema_->selectNFTURI.bind(nftID, ledgerSequence); }
); );
auto const nftUris = executor_.readEach(yield, selectNFTURIStatements); auto const nftUris = executor_.readEach(yield, selectNFTURIStatements);
@@ -193,7 +200,8 @@ public:
maybeRow.has_value()) { maybeRow.has_value()) {
auto [seq, owner, isBurned] = *maybeRow; auto [seq, owner, isBurned] = *maybeRow;
NFT nft(nftIDs[i], seq, owner, isBurned); NFT nft(nftIDs[i], seq, owner, isBurned);
if (auto const maybeUri = nftUris[i].template get<ripple::Blob>(); maybeUri.has_value()) if (auto const maybeUri = nftUris[i].template get<ripple::Blob>();
maybeUri.has_value())
nft.uri = *maybeUri; nft.uri = *maybeUri;
ret.nfts.push_back(nft); ret.nfts.push_back(nft);
} }
@@ -213,8 +221,9 @@ public:
std::optional<ripple::AccountID> lastItem; std::optional<ripple::AccountID> lastItem;
while (liveAccounts.size() < number) { while (liveAccounts.size() < number) {
Statement const statement = lastItem ? schema_->selectAccountFromToken.bind(*lastItem, Limit{pageSize}) Statement const statement = lastItem
: schema_->selectAccountFromBeginning.bind(Limit{pageSize}); ? schema_->selectAccountFromToken.bind(*lastItem, Limit{pageSize})
: schema_->selectAccountFromBeginning.bind(Limit{pageSize});
auto const res = executor_.read(yield, statement); auto const res = executor_.read(yield, statement);
if (res) { if (res) {

View File

@@ -83,8 +83,15 @@ struct NFTTransactionsData {
* @param meta The transaction metadata * @param meta The transaction metadata
* @param txHash The transaction hash * @param txHash The transaction hash
*/ */
NFTTransactionsData(ripple::uint256 const& tokenID, ripple::TxMeta const& meta, ripple::uint256 const& txHash) NFTTransactionsData(
: tokenID(tokenID), ledgerSequence(meta.getLgrSeq()), transactionIndex(meta.getIndex()), txHash(txHash) ripple::uint256 const& tokenID,
ripple::TxMeta const& meta,
ripple::uint256 const& txHash
)
: tokenID(tokenID)
, ledgerSequence(meta.getLgrSeq())
, transactionIndex(meta.getIndex())
, txHash(txHash)
{ {
} }
}; };
@@ -94,11 +101,13 @@ struct NFTTransactionsData {
* *
* Gets written to nf_tokens table and the like. * Gets written to nf_tokens table and the like.
* *
* The transaction index is only stored because we want to store only the final state of an NFT per ledger. * The transaction index is only stored because we want to store only the final state of an NFT per
* Since we pull this from transactions we keep track of which tx index created this so we can de-duplicate, as it is * ledger. Since we pull this from transactions we keep track of which tx index created this so we
* possible for one ledger to have multiple txs that change the state of the same NFT. * can de-duplicate, as it is possible for one ledger to have multiple txs that change the state of
* the same NFT.
* *
* We only set the uri if this is a mint tx, or if we are loading initial state from NFTokenPage objects. * We only set the uri if this is a mint tx, or if we are loading initial state from NFTokenPage
* objects.
*/ */
struct NFTsData { struct NFTsData {
ripple::uint256 tokenID; ripple::uint256 tokenID;
@@ -113,8 +122,9 @@ struct NFTsData {
* @brief Construct a new NFTsData object * @brief Construct a new NFTsData object
* *
* @note This constructor is used when parsing an NFTokenMint tx * @note This constructor is used when parsing an NFTokenMint tx
* Unfortunately because of the extreme edge case of being able to re-mint an NFT with the same ID, we must * Unfortunately because of the extreme edge case of being able to re-mint an NFT with the same
* explicitly record a null URI. For this reason, we _always_ write this field as a result of this tx. * ID, we must explicitly record a null URI. For this reason, we _always_ write this field as a
* result of this tx.
* *
* @param tokenID The token ID * @param tokenID The token ID
* @param owner The owner * @param owner The owner
@@ -127,7 +137,11 @@ struct NFTsData {
ripple::Blob const& uri, ripple::Blob const& uri,
ripple::TxMeta const& meta ripple::TxMeta const& meta
) )
: tokenID(tokenID), ledgerSequence(meta.getLgrSeq()), transactionIndex(meta.getIndex()), owner(owner), uri(uri) : tokenID(tokenID)
, ledgerSequence(meta.getLgrSeq())
, transactionIndex(meta.getIndex())
, owner(owner)
, uri(uri)
{ {
} }
@@ -141,7 +155,12 @@ struct NFTsData {
* @param meta The transaction metadata * @param meta The transaction metadata
* @param isBurned Whether the NFT is burned * @param isBurned Whether the NFT is burned
*/ */
NFTsData(ripple::uint256 const& tokenID, ripple::AccountID const& owner, ripple::TxMeta const& meta, bool isBurned) NFTsData(
ripple::uint256 const& tokenID,
ripple::AccountID const& owner,
ripple::TxMeta const& meta,
bool isBurned
)
: tokenID(tokenID) : tokenID(tokenID)
, ledgerSequence(meta.getLgrSeq()) , ledgerSequence(meta.getLgrSeq())
, transactionIndex(meta.getIndex()) , transactionIndex(meta.getIndex())
@@ -154,8 +173,9 @@ struct NFTsData {
* @brief Construct a new NFTsData object * @brief Construct a new NFTsData object
* *
* @note This constructor is used when parsing an NFTokenPage directly from ledger state. * @note This constructor is used when parsing an NFTokenPage directly from ledger state.
* Unfortunately because of the extreme edge case of being able to re-mint an NFT with the same ID, we must * Unfortunately because of the extreme edge case of being able to re-mint an NFT with the same
* explicitly record a null URI. For this reason, we _always_ write this field as a result of this tx. * ID, we must explicitly record a null URI. For this reason, we _always_ write this field as a
* result of this tx.
* *
* @param tokenID The token ID * @param tokenID The token ID
* @param ledgerSequence The ledger sequence * @param ledgerSequence The ledger sequence

View File

@@ -102,11 +102,17 @@ public:
// This would be the first write to the table. // This would be the first write to the table.
// In this case, insert both min_sequence/max_sequence range into the table. // In this case, insert both min_sequence/max_sequence range into the table.
if (not range_.has_value()) { if (not range_.has_value()) {
executor_.writeSync(schema_->insertLedgerRange, /* isLatestLedger =*/false, ledgerSequence_); executor_.writeSync(
executor_.writeSync(schema_->insertLedgerRange, /* isLatestLedger =*/true, ledgerSequence_); schema_->insertLedgerRange, /* isLatestLedger =*/false, ledgerSequence_
);
executor_.writeSync(
schema_->insertLedgerRange, /* isLatestLedger =*/true, ledgerSequence_
);
} }
if (not this->executeSyncUpdate(schema_->updateLedgerRange.bind(ledgerSequence_, true, ledgerSequence_ - 1))) { if (not this->executeSyncUpdate(
schema_->updateLedgerRange.bind(ledgerSequence_, true, ledgerSequence_ - 1)
)) {
log_.warn() << "Update failed for ledger " << ledgerSequence_; log_.warn() << "Update failed for ledger " << ledgerSequence_;
return false; return false;
} }
@@ -131,7 +137,8 @@ public:
nftIDs = fetchNFTIDsByTaxon(issuer, *taxon, limit, cursorIn, yield); nftIDs = fetchNFTIDsByTaxon(issuer, *taxon, limit, cursorIn, yield);
} else { } else {
// Amazon Keyspaces Workflow for non-taxon queries // Amazon Keyspaces Workflow for non-taxon queries
auto const startTaxon = cursorIn.has_value() ? ripple::nft::toUInt32(ripple::nft::getTaxon(*cursorIn)) : 0; auto const startTaxon =
cursorIn.has_value() ? ripple::nft::toUInt32(ripple::nft::getTaxon(*cursorIn)) : 0;
auto const startTokenID = cursorIn.value_or(ripple::uint256(0)); auto const startTokenID = cursorIn.value_or(ripple::uint256(0));
Statement const firstQuery = schema_->selectNFTIDsByIssuerTaxon.bind(issuer); Statement const firstQuery = schema_->selectNFTIDsByIssuerTaxon.bind(issuer);
@@ -163,10 +170,10 @@ public:
/** /**
* @brief (Unsupported in Keyspaces) Fetches account root object indexes by page. * @brief (Unsupported in Keyspaces) Fetches account root object indexes by page.
* @note Loading the cache by enumerating all accounts is currently unsupported by the AWS Keyspaces backend. * @note Loading the cache by enumerating all accounts is currently unsupported by the AWS
* This function's logic relies on "PER PARTITION LIMIT 1", which Keyspaces does not support, and there is * Keyspaces backend. This function's logic relies on "PER PARTITION LIMIT 1", which Keyspaces
* no efficient alternative. This is acceptable as the cache is primarily loaded via diffs. Calling this * does not support, and there is no efficient alternative. This is acceptable as the cache is
* function will throw an exception. * primarily loaded via diffs. Calling this function will throw an exception.
* *
* @param number The total number of accounts to fetch. * @param number The total number of accounts to fetch.
* @param pageSize The maximum number of accounts per page. * @param pageSize The maximum number of accounts per page.
@@ -220,7 +227,8 @@ private:
{ {
std::vector<ripple::uint256> nftIDs; std::vector<ripple::uint256> nftIDs;
auto const startTaxon = cursorIn.has_value() ? ripple::nft::toUInt32(ripple::nft::getTaxon(*cursorIn)) : 0; auto const startTaxon =
cursorIn.has_value() ? ripple::nft::toUInt32(ripple::nft::getTaxon(*cursorIn)) : 0;
auto const startTokenID = cursorIn.value_or(ripple::uint256(0)); auto const startTokenID = cursorIn.value_or(ripple::uint256(0));
Statement firstQuery = schema_->selectNFTIDsByIssuerTaxon.bind(issuer); Statement firstQuery = schema_->selectNFTIDsByIssuerTaxon.bind(issuer);
@@ -250,7 +258,8 @@ private:
} }
/** /**
* @brief Takes a list of NFT IDs, fetches their full data, and assembles the final result with a cursor. * @brief Takes a list of NFT IDs, fetches their full data, and assembles the final result with
* a cursor.
*/ */
NFTsAndCursor NFTsAndCursor
populateNFTsAndCreateCursor( populateNFTsAndCreateCursor(
@@ -273,17 +282,19 @@ private:
std::vector<Statement> selectNFTStatements; std::vector<Statement> selectNFTStatements;
selectNFTStatements.reserve(nftIDs.size()); selectNFTStatements.reserve(nftIDs.size());
std::transform( std::transform(
std::cbegin(nftIDs), std::cend(nftIDs), std::back_inserter(selectNFTStatements), [&](auto const& nftID) { std::cbegin(nftIDs),
return schema_->selectNFT.bind(nftID, ledgerSequence); std::cend(nftIDs),
} std::back_inserter(selectNFTStatements),
[&](auto const& nftID) { return schema_->selectNFT.bind(nftID, ledgerSequence); }
); );
std::vector<Statement> selectNFTURIStatements; std::vector<Statement> selectNFTURIStatements;
selectNFTURIStatements.reserve(nftIDs.size()); selectNFTURIStatements.reserve(nftIDs.size());
std::transform( std::transform(
std::cbegin(nftIDs), std::cend(nftIDs), std::back_inserter(selectNFTURIStatements), [&](auto const& nftID) { std::cbegin(nftIDs),
return schema_->selectNFTURI.bind(nftID, ledgerSequence); std::cend(nftIDs),
} std::back_inserter(selectNFTURIStatements),
[&](auto const& nftID) { return schema_->selectNFTURI.bind(nftID, ledgerSequence); }
); );
auto const nftInfos = executor_.readEach(yield, selectNFTStatements); auto const nftInfos = executor_.readEach(yield, selectNFTStatements);
@@ -295,7 +306,8 @@ private:
maybeRow.has_value()) { maybeRow.has_value()) {
auto [seq, owner, isBurned] = *maybeRow; auto [seq, owner, isBurned] = *maybeRow;
NFT nft(nftIDs[i], seq, owner, isBurned); NFT nft(nftIDs[i], seq, owner, isBurned);
if (auto const maybeUri = nftUris[i].template get<ripple::Blob>(); maybeUri.has_value()) if (auto const maybeUri = nftUris[i].template get<ripple::Blob>();
maybeUri.has_value())
nft.uri = *maybeUri; nft.uri = *maybeUri;
ret.nfts.push_back(nft); ret.nfts.push_back(nft);
} }

View File

@@ -254,7 +254,8 @@ LedgerCache::getSuccessorHitRate() const
{ {
if (successorReqCounter_.get().value() == 0u) if (successorReqCounter_.get().value() == 0u)
return 1; return 1;
return static_cast<float>(successorHitCounter_.get().value()) / successorReqCounter_.get().value(); return static_cast<float>(successorHitCounter_.get().value()) /
successorReqCounter_.get().value();
} }
std::expected<void, std::string> std::expected<void, std::string>
@@ -266,7 +267,9 @@ LedgerCache::saveToFile(std::string const& path) const
impl::LedgerCacheFile file{path}; impl::LedgerCacheFile file{path};
std::shared_lock const lock{mtx_}; std::shared_lock const lock{mtx_};
impl::LedgerCacheFile::DataView const data{.latestSeq = latestSeq_, .map = map_, .deleted = deleted_}; impl::LedgerCacheFile::DataView const data{
.latestSeq = latestSeq_, .map = map_, .deleted = deleted_
};
return file.write(data); return file.write(data);
} }

View File

@@ -58,26 +58,34 @@ public:
private: private:
// counters for fetchLedgerObject(s) hit rate // counters for fetchLedgerObject(s) hit rate
std::reference_wrapper<util::prometheus::CounterInt> objectReqCounter_{PrometheusService::counterInt( std::reference_wrapper<util::prometheus::CounterInt> objectReqCounter_{
"ledger_cache_counter_total_number", PrometheusService::counterInt(
util::prometheus::Labels({{"type", "request"}, {"fetch", "ledger_objects"}}), "ledger_cache_counter_total_number",
"LedgerCache statistics" util::prometheus::Labels({{"type", "request"}, {"fetch", "ledger_objects"}}),
)}; "LedgerCache statistics"
std::reference_wrapper<util::prometheus::CounterInt> objectHitCounter_{PrometheusService::counterInt( )
"ledger_cache_counter_total_number", };
util::prometheus::Labels({{"type", "cache_hit"}, {"fetch", "ledger_objects"}}) std::reference_wrapper<util::prometheus::CounterInt> objectHitCounter_{
)}; PrometheusService::counterInt(
"ledger_cache_counter_total_number",
util::prometheus::Labels({{"type", "cache_hit"}, {"fetch", "ledger_objects"}})
)
};
// counters for fetchSuccessorKey hit rate // counters for fetchSuccessorKey hit rate
std::reference_wrapper<util::prometheus::CounterInt> successorReqCounter_{PrometheusService::counterInt( std::reference_wrapper<util::prometheus::CounterInt> successorReqCounter_{
"ledger_cache_counter_total_number", PrometheusService::counterInt(
util::prometheus::Labels({{"type", "request"}, {"fetch", "successor_key"}}), "ledger_cache_counter_total_number",
"ledgerCache" util::prometheus::Labels({{"type", "request"}, {"fetch", "successor_key"}}),
)}; "ledgerCache"
std::reference_wrapper<util::prometheus::CounterInt> successorHitCounter_{PrometheusService::counterInt( )
"ledger_cache_counter_total_number", };
util::prometheus::Labels({{"type", "cache_hit"}, {"fetch", "successor_key"}}) std::reference_wrapper<util::prometheus::CounterInt> successorHitCounter_{
)}; PrometheusService::counterInt(
"ledger_cache_counter_total_number",
util::prometheus::Labels({{"type", "cache_hit"}, {"fetch", "successor_key"}})
)
};
CacheMap map_; CacheMap map_;
CacheMap deleted_; CacheMap deleted_;
@@ -96,7 +104,8 @@ private:
"Whether ledger cache is disabled or not" "Whether ledger cache is disabled or not"
)}; )};
// temporary set to prevent background thread from writing already deleted data. not used when cache is full // temporary set to prevent background thread from writing already deleted data. not used when
// cache is full
std::unordered_set<ripple::uint256, ripple::hardened_hash<>> deletes_; std::unordered_set<ripple::uint256, ripple::hardened_hash<>> deletes_;
public: public:

View File

@@ -126,9 +126,9 @@ public:
/** /**
* @brief Sets the full flag to true. * @brief Sets the full flag to true.
* *
* This is used when cache loaded in its entirety at startup of the application. This can be either loaded from DB, * This is used when cache loaded in its entirety at startup of the application. This can be
* populated together with initial ledger download (on first run) or downloaded from a peer node (specified in * either loaded from DB, populated together with initial ledger download (on first run) or
* config). * downloaded from a peer node (specified in config).
*/ */
virtual void virtual void
setFull() = 0; setFull() = 0;
@@ -152,13 +152,15 @@ public:
size() const = 0; size() const = 0;
/** /**
* @return A number representing the success rate of hitting an object in the cache versus missing it. * @return A number representing the success rate of hitting an object in the cache versus
* missing it.
*/ */
virtual float virtual float
getObjectHitRate() const = 0; getObjectHitRate() const = 0;
/** /**
* @return A number representing the success rate of hitting a successor in the cache versus missing it. * @return A number representing the success rate of hitting a successor in the cache versus
* missing it.
*/ */
virtual float virtual float
getSuccessorHitRate() const = 0; getSuccessorHitRate() const = 0;

View File

@@ -29,7 +29,10 @@
namespace data { namespace data {
LedgerCacheSaver::LedgerCacheSaver(util::config::ClioConfigDefinition const& config, LedgerCacheInterface const& cache) LedgerCacheSaver::LedgerCacheSaver(
util::config::ClioConfigDefinition const& config,
LedgerCacheInterface const& cache
)
: cacheFilePath_(config.maybeValue<std::string>("cache.file.path")) : cacheFilePath_(config.maybeValue<std::string>("cache.file.path"))
, cache_(cache) , cache_(cache)
, isAsync_(config.get<bool>("cache.file.async_save")) , isAsync_(config.get<bool>("cache.file.async_save"))
@@ -51,11 +54,14 @@ LedgerCacheSaver::save()
} }
LOG(util::LogService::info()) << "Saving ledger cache to " << *cacheFilePath_; LOG(util::LogService::info()) << "Saving ledger cache to " << *cacheFilePath_;
if (auto const [success, durationMs] = util::timed([&]() { return cache_.get().saveToFile(*cacheFilePath_); }); if (auto const [success, durationMs] =
util::timed([&]() { return cache_.get().saveToFile(*cacheFilePath_); });
success.has_value()) { success.has_value()) {
LOG(util::LogService::info()) << "Successfully saved ledger cache in " << durationMs << " ms"; LOG(util::LogService::info())
<< "Successfully saved ledger cache in " << durationMs << " ms";
} else { } else {
LOG(util::LogService::error()) << "Error saving LedgerCache to file: " << success.error(); LOG(util::LogService::error())
<< "Error saving LedgerCache to file: " << success.error();
} }
}); });
if (not isAsync_) { if (not isAsync_) {

View File

@@ -62,7 +62,10 @@ public:
* @param config The configuration object containing the cache file path setting * @param config The configuration object containing the cache file path setting
* @param cache Reference to the ledger cache interface to be saved * @param cache Reference to the ledger cache interface to be saved
*/ */
LedgerCacheSaver(util::config::ClioConfigDefinition const& config, LedgerCacheInterface const& cache); LedgerCacheSaver(
util::config::ClioConfigDefinition const& config,
LedgerCacheInterface const& cache
);
/** /**
* @brief Destructor that ensures the saving thread is properly joined. * @brief Destructor that ensures the saving thread is properly joined.

View File

@@ -81,8 +81,16 @@ struct TransactionAndMetadata {
* @param ledgerSequence The ledger sequence * @param ledgerSequence The ledger sequence
* @param date The date * @param date The date
*/ */
TransactionAndMetadata(Blob transaction, Blob metadata, std::uint32_t ledgerSequence, std::uint32_t date) TransactionAndMetadata(
: transaction{std::move(transaction)}, metadata{std::move(metadata)}, ledgerSequence{ledgerSequence}, date{date} Blob transaction,
Blob metadata,
std::uint32_t ledgerSequence,
std::uint32_t date
)
: transaction{std::move(transaction)}
, metadata{std::move(metadata)}
, ledgerSequence{ledgerSequence}
, date{date}
{ {
} }
@@ -192,7 +200,11 @@ struct NFT {
ripple::AccountID const& owner, ripple::AccountID const& owner,
Blob uri, Blob uri,
bool isBurned) bool isBurned)
: tokenID{tokenID}, ledgerSequence{ledgerSequence}, owner{owner}, uri{std::move(uri)}, isBurned{isBurned} : tokenID{tokenID}
, ledgerSequence{ledgerSequence}
, owner{owner}
, uri{std::move(uri)}
, isBurned{isBurned}
{ {
} }
@@ -204,7 +216,10 @@ struct NFT {
* @param owner The owner * @param owner The owner
* @param isBurned Whether the token is burned * @param isBurned Whether the token is burned
*/ */
NFT(ripple::uint256 const& tokenID, std::uint32_t ledgerSequence, ripple::AccountID const& owner, bool isBurned) NFT(ripple::uint256 const& tokenID,
std::uint32_t ledgerSequence,
ripple::AccountID const& owner,
bool isBurned)
: NFT(tokenID, ledgerSequence, owner, {}, isBurned) : NFT(tokenID, ledgerSequence, owner, {}, isBurned)
{ {
} }
@@ -212,8 +227,8 @@ struct NFT {
/** /**
* @brief Check if the NFT is the same as another * @brief Check if the NFT is the same as another
* *
* Clearly two tokens are the same if they have the same ID, but this struct stores the state of a given * Clearly two tokens are the same if they have the same ID, but this struct stores the state of
* token at a given ledger sequence, so we also need to compare with ledgerSequence. * a given token at a given ledger sequence, so we also need to compare with ledgerSequence.
* *
* @param other The other NFT * @param other The other NFT
* @return true if they are the same; false otherwise * @return true if they are the same; false otherwise
@@ -293,7 +308,8 @@ struct AmendmentKey {
* @brief Construct a new AmendmentKey * @brief Construct a new AmendmentKey
* @param val Anything convertible to a string * @param val Anything convertible to a string
*/ */
AmendmentKey(std::convertible_to<std::string> auto&& val) : name{std::forward<decltype(val)>(val)} AmendmentKey(std::convertible_to<std::string> auto&& val)
: name{std::forward<decltype(val)>(val)}
{ {
} }
@@ -315,8 +331,14 @@ struct AmendmentKey {
operator<=>(AmendmentKey const& other) const = default; operator<=>(AmendmentKey const& other) const = default;
}; };
constexpr ripple::uint256 kFIRST_KEY{"0000000000000000000000000000000000000000000000000000000000000000"}; constexpr ripple::uint256 kFIRST_KEY{
constexpr ripple::uint256 kLAST_KEY{"FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF"}; "0000000000000000000000000000000000000000000000000000000000000000"
constexpr ripple::uint256 kHI192{"0000000000000000000000000000000000000000000000001111111111111111"}; };
constexpr ripple::uint256 kLAST_KEY{
"FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF"
};
constexpr ripple::uint256 kHI192{
"0000000000000000000000000000000000000000000000001111111111111111"
};
} // namespace data } // namespace data

View File

@@ -104,7 +104,11 @@ public:
* @param cache The ledger cache * @param cache The ledger cache
* @param readOnly Whether the database should be in readonly mode * @param readOnly Whether the database should be in readonly mode
*/ */
CassandraBackendFamily(SettingsProviderType settingsProvider, data::LedgerCacheInterface& cache, bool readOnly) CassandraBackendFamily(
SettingsProviderType settingsProvider,
data::LedgerCacheInterface& cache,
bool readOnly
)
: BackendInterface(cache) : BackendInterface(cache)
, settingsProvider_{std::move(settingsProvider)} , settingsProvider_{std::move(settingsProvider)}
, schema_{settingsProvider_} , schema_{settingsProvider_}
@@ -116,8 +120,8 @@ public:
if (not readOnly) { if (not readOnly) {
if (auto const res = handle_.execute(schema_.createKeyspace); not res.has_value()) { if (auto const res = handle_.execute(schema_.createKeyspace); not res.has_value()) {
// on datastax, creation of keyspaces can be configured to only be done thru the admin // on datastax, creation of keyspaces can be configured to only be done thru the
// interface. this does not mean that the keyspace does not already exist tho. // admin interface. this does not mean that the keyspace does not already exist tho.
if (res.error().code() != CASS_ERROR_SERVER_UNAUTHORIZED) if (res.error().code() != CASS_ERROR_SERVER_UNAUTHORIZED)
throw std::runtime_error("Could not create keyspace: " + res.error()); throw std::runtime_error("Could not create keyspace: " + res.error());
} }
@@ -130,7 +134,8 @@ public:
schema_.prepareStatements(handle_); schema_.prepareStatements(handle_);
} catch (std::runtime_error const& ex) { } catch (std::runtime_error const& ex) {
auto const error = fmt::format( auto const error = fmt::format(
"Failed to prepare the statements: {}; readOnly: {}. ReadOnly should be turned off or another Clio " "Failed to prepare the statements: {}; readOnly: {}. ReadOnly should be turned off "
"or another Clio "
"node with write access to DB should be started first.", "node with write access to DB should be started first.",
ex.what(), ex.what(),
readOnly readOnly
@@ -169,8 +174,8 @@ public:
auto cursor = txnCursor; auto cursor = txnCursor;
if (cursor) { if (cursor) {
statement.bindAt(1, cursor->asTuple()); statement.bindAt(1, cursor->asTuple());
LOG(log_.debug()) << "account = " << ripple::strHex(account) << " tuple = " << cursor->ledgerSequence LOG(log_.debug()) << "account = " << ripple::strHex(account)
<< cursor->transactionIndex; << " tuple = " << cursor->ledgerSequence << cursor->transactionIndex;
} else { } else {
auto const seq = forward ? rng->minSequence : rng->maxSequence; auto const seq = forward ? rng->minSequence : rng->maxSequence;
auto const placeHolder = forward ? 0u : std::numeric_limits<std::uint32_t>::max(); auto const placeHolder = forward ? 0u : std::numeric_limits<std::uint32_t>::max();
@@ -195,7 +200,8 @@ public:
auto numRows = results.numRows(); auto numRows = results.numRows();
LOG(log_.info()) << "num_rows = " << numRows; LOG(log_.info()) << "num_rows = " << numRows;
for (auto [hash, data] : extract<ripple::uint256, std::tuple<uint32_t, uint32_t>>(results)) { for (auto [hash, data] :
extract<ripple::uint256, std::tuple<uint32_t, uint32_t>>(results)) {
hashes.push_back(hash); hashes.push_back(hash);
if (--numRows == 0) { if (--numRows == 0) {
LOG(log_.debug()) << "Setting cursor"; LOG(log_.debug()) << "Setting cursor";
@@ -251,7 +257,10 @@ public:
} }
std::optional<ripple::LedgerHeader> std::optional<ripple::LedgerHeader>
fetchLedgerBySequence(std::uint32_t const sequence, boost::asio::yield_context yield) const override fetchLedgerBySequence(
std::uint32_t const sequence,
boost::asio::yield_context yield
) const override
{ {
if (auto const lock = ledgerCache_.get(); lock.has_value() && lock->seq == sequence) if (auto const lock = ledgerCache_.get(); lock.has_value() && lock->seq == sequence)
return lock->ledger; return lock->ledger;
@@ -259,7 +268,8 @@ public:
auto const res = executor_.read(yield, schema_->selectLedgerBySeq, sequence); auto const res = executor_.read(yield, schema_->selectLedgerBySeq, sequence);
if (res) { if (res) {
if (auto const& result = res.value(); result) { if (auto const& result = res.value(); result) {
if (auto const maybeValue = result.template get<std::vector<unsigned char>>(); maybeValue) { if (auto const maybeValue = result.template get<std::vector<unsigned char>>();
maybeValue) {
auto const header = util::deserializeHeader(ripple::makeSlice(*maybeValue)); auto const header = util::deserializeHeader(ripple::makeSlice(*maybeValue));
ledgerCache_.put(FetchLedgerCache::CacheEntry{header, sequence}); ledgerCache_.put(FetchLedgerCache::CacheEntry{header, sequence});
return header; return header;
@@ -336,7 +346,10 @@ public:
} }
std::vector<TransactionAndMetadata> std::vector<TransactionAndMetadata>
fetchAllTransactionsInLedger(std::uint32_t const ledgerSequence, boost::asio::yield_context yield) const override fetchAllTransactionsInLedger(
std::uint32_t const ledgerSequence,
boost::asio::yield_context yield
) const override
{ {
auto hashes = fetchAllTransactionHashesInLedger(ledgerSequence, yield); auto hashes = fetchAllTransactionHashesInLedger(ledgerSequence, yield);
return fetchTransactions(hashes, yield); return fetchTransactions(hashes, yield);
@@ -349,7 +362,8 @@ public:
) const override ) const override
{ {
auto start = std::chrono::system_clock::now(); auto start = std::chrono::system_clock::now();
auto const res = executor_.read(yield, schema_->selectAllTransactionHashesInLedger, ledgerSequence); auto const res =
executor_.read(yield, schema_->selectAllTransactionHashesInLedger, ledgerSequence);
if (not res) { if (not res) {
LOG(log_.error()) << "Could not fetch all transaction hashes: " << res.error(); LOG(log_.error()) << "Could not fetch all transaction hashes: " << res.error();
@@ -368,9 +382,12 @@ public:
hashes.push_back(std::move(hash)); hashes.push_back(std::move(hash));
auto end = std::chrono::system_clock::now(); auto end = std::chrono::system_clock::now();
LOG(log_.debug()) << "Fetched " << hashes.size() << " transaction hashes from database in " LOG(
<< std::chrono::duration_cast<std::chrono::milliseconds>(end - start).count() log_.debug()
<< " milliseconds"; ) << "Fetched "
<< hashes.size() << " transaction hashes from database in "
<< std::chrono::duration_cast<std::chrono::milliseconds>(end - start).count()
<< " milliseconds";
return hashes; return hashes;
} }
@@ -386,7 +403,8 @@ public:
if (not res) if (not res)
return std::nullopt; return std::nullopt;
if (auto const maybeRow = res->template get<uint32_t, ripple::AccountID, bool>(); maybeRow) { if (auto const maybeRow = res->template get<uint32_t, ripple::AccountID, bool>();
maybeRow) {
auto [seq, owner, isBurned] = *maybeRow; auto [seq, owner, isBurned] = *maybeRow;
auto result = std::make_optional<NFT>(tokenID, seq, owner, isBurned); auto result = std::make_optional<NFT>(tokenID, seq, owner, isBurned);
@@ -437,8 +455,8 @@ public:
auto cursor = cursorIn; auto cursor = cursorIn;
if (cursor) { if (cursor) {
statement.bindAt(1, cursor->asTuple()); statement.bindAt(1, cursor->asTuple());
LOG(log_.debug()) << "token_id = " << ripple::strHex(tokenID) << " tuple = " << cursor->ledgerSequence LOG(log_.debug()) << "token_id = " << ripple::strHex(tokenID)
<< cursor->transactionIndex; << " tuple = " << cursor->ledgerSequence << cursor->transactionIndex;
} else { } else {
auto const seq = forward ? rng->minSequence : rng->maxSequence; auto const seq = forward ? rng->minSequence : rng->maxSequence;
auto const placeHolder = forward ? 0 : std::numeric_limits<std::uint32_t>::max(); auto const placeHolder = forward ? 0 : std::numeric_limits<std::uint32_t>::max();
@@ -461,7 +479,8 @@ public:
auto numRows = results.numRows(); auto numRows = results.numRows();
LOG(log_.info()) << "num_rows = " << numRows; LOG(log_.info()) << "num_rows = " << numRows;
for (auto [hash, data] : extract<ripple::uint256, std::tuple<uint32_t, uint32_t>>(results)) { for (auto [hash, data] :
extract<ripple::uint256, std::tuple<uint32_t, uint32_t>>(results)) {
hashes.push_back(hash); hashes.push_back(hash);
if (--numRows == 0) { if (--numRows == 0) {
LOG(log_.debug()) << "Setting cursor"; LOG(log_.debug()) << "Setting cursor";
@@ -495,7 +514,11 @@ public:
) const override ) const override
{ {
auto const holderEntries = executor_.read( auto const holderEntries = executor_.read(
yield, schema_->selectMPTHolders, mptID, cursorIn.value_or(ripple::AccountID(0)), Limit{limit} yield,
schema_->selectMPTHolders,
mptID,
cursorIn.value_or(ripple::AccountID(0)),
Limit{limit}
); );
auto const& holderResults = holderEntries.value(); auto const& holderResults = holderEntries.value();
@@ -513,7 +536,9 @@ public:
auto mptObjects = doFetchLedgerObjects(mptKeys, ledgerSequence, yield); auto mptObjects = doFetchLedgerObjects(mptKeys, ledgerSequence, yield);
auto it = std::remove_if(mptObjects.begin(), mptObjects.end(), [](Blob const& mpt) { return mpt.empty(); }); auto it = std::remove_if(mptObjects.begin(), mptObjects.end(), [](Blob const& mpt) {
return mpt.empty();
});
mptObjects.erase(it, mptObjects.end()); mptObjects.erase(it, mptObjects.end());
@@ -531,7 +556,8 @@ public:
boost::asio::yield_context yield boost::asio::yield_context yield
) const override ) const override
{ {
LOG(log_.debug()) << "Fetching ledger object for seq " << sequence << ", key = " << ripple::to_string(key); LOG(log_.debug()) << "Fetching ledger object for seq " << sequence
<< ", key = " << ripple::to_string(key);
if (auto const res = executor_.read(yield, schema_->selectObject, key, sequence); res) { if (auto const res = executor_.read(yield, schema_->selectObject, key, sequence); res) {
if (auto const result = res->template get<Blob>(); result) { if (auto const result = res->template get<Blob>(); result) {
if (result->size()) if (result->size())
@@ -553,7 +579,8 @@ public:
boost::asio::yield_context yield boost::asio::yield_context yield
) const override ) const override
{ {
LOG(log_.debug()) << "Fetching ledger object for seq " << sequence << ", key = " << ripple::to_string(key); LOG(log_.debug()) << "Fetching ledger object for seq " << sequence
<< ", key = " << ripple::to_string(key);
if (auto const res = executor_.read(yield, schema_->selectObject, key, sequence); res) { if (auto const res = executor_.read(yield, schema_->selectObject, key, sequence); res) {
if (auto const result = res->template get<Blob, std::uint32_t>(); result) { if (auto const result = res->template get<Blob, std::uint32_t>(); result) {
auto [_, seq] = result.value(); auto [_, seq] = result.value();
@@ -571,7 +598,8 @@ public:
fetchTransaction(ripple::uint256 const& hash, boost::asio::yield_context yield) const override fetchTransaction(ripple::uint256 const& hash, boost::asio::yield_context yield) const override
{ {
if (auto const res = executor_.read(yield, schema_->selectTransaction, hash); res) { if (auto const res = executor_.read(yield, schema_->selectTransaction, hash); res) {
if (auto const maybeValue = res->template get<Blob, Blob, uint32_t, uint32_t>(); maybeValue) { if (auto const maybeValue = res->template get<Blob, Blob, uint32_t, uint32_t>();
maybeValue) {
auto [transaction, meta, seq, date] = *maybeValue; auto [transaction, meta, seq, date] = *maybeValue;
return std::make_optional<TransactionAndMetadata>(transaction, meta, seq, date); return std::make_optional<TransactionAndMetadata>(transaction, meta, seq, date);
} }
@@ -591,7 +619,8 @@ public:
boost::asio::yield_context yield boost::asio::yield_context yield
) const override ) const override
{ {
if (auto const res = executor_.read(yield, schema_->selectSuccessor, key, ledgerSequence); res) { if (auto const res = executor_.read(yield, schema_->selectSuccessor, key, ledgerSequence);
res) {
if (auto const result = res->template get<ripple::uint256>(); result) { if (auto const result = res->template get<ripple::uint256>(); result) {
if (*result == kLAST_KEY) if (*result == kLAST_KEY)
return std::nullopt; return std::nullopt;
@@ -607,7 +636,10 @@ public:
} }
std::vector<TransactionAndMetadata> std::vector<TransactionAndMetadata>
fetchTransactions(std::vector<ripple::uint256> const& hashes, boost::asio::yield_context yield) const override fetchTransactions(
std::vector<ripple::uint256> const& hashes,
boost::asio::yield_context yield
) const override
{ {
if (hashes.empty()) if (hashes.empty())
return {}; return {};
@@ -622,9 +654,10 @@ public:
auto const timeDiff = util::timed([this, yield, &results, &hashes, &statements]() { auto const timeDiff = util::timed([this, yield, &results, &hashes, &statements]() {
// TODO: seems like a job for "hash IN (list of hashes)" instead? // TODO: seems like a job for "hash IN (list of hashes)" instead?
std::transform( std::transform(
std::cbegin(hashes), std::cend(hashes), std::back_inserter(statements), [this](auto const& hash) { std::cbegin(hashes),
return schema_->selectTransaction.bind(hash); std::cend(hashes),
} std::back_inserter(statements),
[this](auto const& hash) { return schema_->selectTransaction.bind(hash); }
); );
auto const entries = executor_.readEach(yield, statements); auto const entries = executor_.readEach(yield, statements);
@@ -633,7 +666,8 @@ public:
std::cend(entries), std::cend(entries),
std::back_inserter(results), std::back_inserter(results),
[](auto const& res) -> TransactionAndMetadata { [](auto const& res) -> TransactionAndMetadata {
if (auto const maybeRow = res.template get<Blob, Blob, uint32_t, uint32_t>(); maybeRow) if (auto const maybeRow = res.template get<Blob, Blob, uint32_t, uint32_t>();
maybeRow)
return *maybeRow; return *maybeRow;
return {}; return {};
@@ -642,8 +676,8 @@ public:
}); });
ASSERT(numHashes == results.size(), "Number of hashes and results must match"); ASSERT(numHashes == results.size(), "Number of hashes and results must match");
LOG(log_.debug()) << "Fetched " << numHashes << " transactions from database in " << timeDiff LOG(log_.debug()) << "Fetched " << numHashes << " transactions from database in "
<< " milliseconds"; << timeDiff << " milliseconds";
return results; return results;
} }
@@ -668,14 +702,18 @@ public:
// TODO: seems like a job for "key IN (list of keys)" instead? // TODO: seems like a job for "key IN (list of keys)" instead?
std::transform( std::transform(
std::cbegin(keys), std::cend(keys), std::back_inserter(statements), [this, &sequence](auto const& key) { std::cbegin(keys),
return schema_->selectObject.bind(key, sequence); std::cend(keys),
} std::back_inserter(statements),
[this, &sequence](auto const& key) { return schema_->selectObject.bind(key, sequence); }
); );
auto const entries = executor_.readEach(yield, statements); auto const entries = executor_.readEach(yield, statements);
std::transform( std::transform(
std::cbegin(entries), std::cend(entries), std::back_inserter(results), [](auto const& res) -> Blob { std::cbegin(entries),
std::cend(entries),
std::back_inserter(results),
[](auto const& res) -> Blob {
if (auto const maybeValue = res.template get<Blob>(); maybeValue) if (auto const maybeValue = res.template get<Blob>(); maybeValue)
return *maybeValue; return *maybeValue;
@@ -688,34 +726,40 @@ public:
} }
std::vector<LedgerObject> std::vector<LedgerObject>
fetchLedgerDiff(std::uint32_t const ledgerSequence, boost::asio::yield_context yield) const override fetchLedgerDiff(
std::uint32_t const ledgerSequence,
boost::asio::yield_context yield
) const override
{ {
auto const [keys, timeDiff] = util::timed([this, &ledgerSequence, yield]() -> std::vector<ripple::uint256> { auto const [keys, timeDiff] =
auto const res = executor_.read(yield, schema_->selectDiff, ledgerSequence); util::timed([this, &ledgerSequence, yield]() -> std::vector<ripple::uint256> {
if (not res) { auto const res = executor_.read(yield, schema_->selectDiff, ledgerSequence);
LOG(log_.error()) << "Could not fetch ledger diff: " << res.error() << "; ledger = " << ledgerSequence; if (not res) {
return {}; LOG(log_.error()) << "Could not fetch ledger diff: " << res.error()
} << "; ledger = " << ledgerSequence;
return {};
}
auto const& results = res.value(); auto const& results = res.value();
if (not results) { if (not results) {
LOG(log_.error()) << "Could not fetch ledger diff - no rows; ledger = " << ledgerSequence; LOG(log_.error())
return {}; << "Could not fetch ledger diff - no rows; ledger = " << ledgerSequence;
} return {};
}
std::vector<ripple::uint256> resultKeys; std::vector<ripple::uint256> resultKeys;
for (auto [key] : extract<ripple::uint256>(results)) for (auto [key] : extract<ripple::uint256>(results))
resultKeys.push_back(key); resultKeys.push_back(key);
return resultKeys; return resultKeys;
}); });
// one of the above errors must have happened // one of the above errors must have happened
if (keys.empty()) if (keys.empty())
return {}; return {};
LOG(log_.debug()) << "Fetched " << keys.size() << " diff hashes from database in " << timeDiff LOG(log_.debug()) << "Fetched " << keys.size() << " diff hashes from database in "
<< " milliseconds"; << timeDiff << " milliseconds";
auto const objs = fetchLedgerObjects(keys, ledgerSequence, yield); auto const objs = fetchLedgerObjects(keys, ledgerSequence, yield);
std::vector<LedgerObject> results; std::vector<LedgerObject> results;
@@ -733,7 +777,10 @@ public:
} }
std::optional<std::string> std::optional<std::string>
fetchMigratorStatus(std::string const& migratorName, boost::asio::yield_context yield) const override fetchMigratorStatus(
std::string const& migratorName,
boost::asio::yield_context yield
) const override
{ {
auto const res = executor_.read(yield, schema_->selectMigratorStatus, Text(migratorName)); auto const res = executor_.read(yield, schema_->selectMigratorStatus, Text(migratorName));
if (not res) { if (not res) {
@@ -771,7 +818,8 @@ public:
void void
doWriteLedgerObject(std::string&& key, std::uint32_t const seq, std::string&& blob) override doWriteLedgerObject(std::string&& key, std::uint32_t const seq, std::string&& blob) override
{ {
LOG(log_.trace()) << " Writing ledger object " << key.size() << ":" << seq << " [" << blob.size() << " bytes]"; LOG(log_.trace()) << " Writing ledger object " << key.size() << ":" << seq << " ["
<< blob.size() << " bytes]";
if (range_) if (range_)
executor_.write(schema_->insertDiff, seq, key); executor_.write(schema_->insertDiff, seq, key);
@@ -783,7 +831,8 @@ public:
writeSuccessor(std::string&& key, std::uint32_t const seq, std::string&& successor) override writeSuccessor(std::string&& key, std::uint32_t const seq, std::string&& successor) override
{ {
LOG(log_.trace()) << "Writing successor. key = " << key.size() << " bytes. " LOG(log_.trace()) << "Writing successor. key = " << key.size() << " bytes. "
<< " seq = " << std::to_string(seq) << " successor = " << successor.size() << " bytes."; << " seq = " << std::to_string(seq) << " successor = " << successor.size()
<< " bytes.";
ASSERT(!key.empty(), "Key must not be empty"); ASSERT(!key.empty(), "Key must not be empty");
ASSERT(!successor.empty(), "Successor must not be empty"); ASSERT(!successor.empty(), "Successor must not be empty");
@@ -797,13 +846,15 @@ public:
statements.reserve(data.size() * 10); // assume 10 transactions avg statements.reserve(data.size() * 10); // assume 10 transactions avg
for (auto& record : data) { for (auto& record : data) {
std::ranges::transform(record.accounts, std::back_inserter(statements), [this, &record](auto&& account) { std::ranges::transform(
return schema_->insertAccountTx.bind( record.accounts, std::back_inserter(statements), [this, &record](auto&& account) {
std::forward<decltype(account)>(account), return schema_->insertAccountTx.bind(
std::make_tuple(record.ledgerSequence, record.transactionIndex), std::forward<decltype(account)>(account),
record.txHash std::make_tuple(record.ledgerSequence, record.transactionIndex),
); record.txHash
}); );
}
);
} }
executor_.write(std::move(statements)); executor_.write(std::move(statements));
@@ -815,13 +866,15 @@ public:
std::vector<Statement> statements; std::vector<Statement> statements;
statements.reserve(record.accounts.size()); statements.reserve(record.accounts.size());
std::ranges::transform(record.accounts, std::back_inserter(statements), [this, &record](auto&& account) { std::ranges::transform(
return schema_->insertAccountTx.bind( record.accounts, std::back_inserter(statements), [this, &record](auto&& account) {
std::forward<decltype(account)>(account), return schema_->insertAccountTx.bind(
std::make_tuple(record.ledgerSequence, record.transactionIndex), std::forward<decltype(account)>(account),
record.txHash std::make_tuple(record.ledgerSequence, record.transactionIndex),
); record.txHash
}); );
}
);
executor_.write(std::move(statements)); executor_.write(std::move(statements));
} }
@@ -834,7 +887,9 @@ public:
std::ranges::transform(data, std::back_inserter(statements), [this](auto const& record) { std::ranges::transform(data, std::back_inserter(statements), [this](auto const& record) {
return schema_->insertNFTTx.bind( return schema_->insertNFTTx.bind(
record.tokenID, std::make_tuple(record.ledgerSequence, record.transactionIndex), record.txHash record.tokenID,
std::make_tuple(record.ledgerSequence, record.transactionIndex),
record.txHash
); );
}); });
@@ -854,7 +909,12 @@ public:
executor_.write(schema_->insertLedgerTransaction, seq, hash); executor_.write(schema_->insertLedgerTransaction, seq, hash);
executor_.write( executor_.write(
schema_->insertTransaction, std::move(hash), seq, date, std::move(transaction), std::move(metadata) schema_->insertTransaction,
std::move(hash),
seq,
date,
std::move(transaction),
std::move(metadata)
); );
} }
@@ -866,9 +926,9 @@ public:
for (NFTsData const& record : data) { for (NFTsData const& record : data) {
if (!record.onlyUriChanged) { if (!record.onlyUriChanged) {
statements.push_back( statements.push_back(schema_->insertNFT.bind(
schema_->insertNFT.bind(record.tokenID, record.ledgerSequence, record.owner, record.isBurned) record.tokenID, record.ledgerSequence, record.owner, record.isBurned
); ));
// If `uri` is set (and it can be set to an empty uri), we know this // If `uri` is set (and it can be set to an empty uri), we know this
// is a net-new NFT. That is, this NFT has not been seen before by // is a net-new NFT. That is, this NFT has not been seen before by
@@ -881,15 +941,15 @@ public:
static_cast<uint32_t>(ripple::nft::getTaxon(record.tokenID)), static_cast<uint32_t>(ripple::nft::getTaxon(record.tokenID)),
record.tokenID record.tokenID
)); ));
statements.push_back( statements.push_back(schema_->insertNFTURI.bind(
schema_->insertNFTURI.bind(record.tokenID, record.ledgerSequence, record.uri.value()) record.tokenID, record.ledgerSequence, record.uri.value()
); ));
} }
} else { } else {
// only uri changed, we update the uri table only // only uri changed, we update the uri table only
statements.push_back( statements.push_back(schema_->insertNFTURI.bind(
schema_->insertNFTURI.bind(record.tokenID, record.ledgerSequence, record.uri.value()) record.tokenID, record.ledgerSequence, record.uri.value()
); ));
} }
} }
@@ -918,14 +978,18 @@ public:
writeMigratorStatus(std::string const& migratorName, std::string const& status) override writeMigratorStatus(std::string const& migratorName, std::string const& status) override
{ {
executor_.writeSync( executor_.writeSync(
schema_->insertMigratorStatus, data::cassandra::Text{migratorName}, data::cassandra::Text(status) schema_->insertMigratorStatus,
data::cassandra::Text{migratorName},
data::cassandra::Text(status)
); );
} }
void void
writeNodeMessage(boost::uuids::uuid const& uuid, std::string message) override writeNodeMessage(boost::uuids::uuid const& uuid, std::string message) override
{ {
executor_.writeSync(schema_->updateClioNodeMessage, data::cassandra::Text{std::move(message)}, uuid); executor_.writeSync(
schema_->updateClioNodeMessage, data::cassandra::Text{std::move(message)}, uuid
);
} }
bool bool

View File

@@ -78,12 +78,13 @@ concept SomeExecutionStrategy = requires(
* @brief The requirements of a retry policy. * @brief The requirements of a retry policy.
*/ */
template <typename T> template <typename T>
concept SomeRetryPolicy = requires(T a, boost::asio::io_context ioc, CassandraError err, uint32_t attempt) { concept SomeRetryPolicy =
{ T(ioc) }; requires(T a, boost::asio::io_context ioc, CassandraError err, uint32_t attempt) {
{ a.shouldRetry(err) } -> std::same_as<bool>; { T(ioc) };
{ { a.shouldRetry(err) } -> std::same_as<bool>;
a.retry([]() {}) {
} -> std::same_as<void>; a.retry([]() {})
}; } -> std::same_as<void>;
};
} // namespace data::cassandra } // namespace data::cassandra

View File

@@ -105,9 +105,9 @@ public:
bool bool
isTimeout() const isTimeout() const
{ {
return code_ == CASS_ERROR_LIB_NO_HOSTS_AVAILABLE or code_ == CASS_ERROR_LIB_REQUEST_TIMED_OUT or return code_ == CASS_ERROR_LIB_NO_HOSTS_AVAILABLE or
code_ == CASS_ERROR_SERVER_UNAVAILABLE or code_ == CASS_ERROR_SERVER_OVERLOADED or code_ == CASS_ERROR_LIB_REQUEST_TIMED_OUT or code_ == CASS_ERROR_SERVER_UNAVAILABLE or
code_ == CASS_ERROR_SERVER_READ_TIMEOUT; code_ == CASS_ERROR_SERVER_OVERLOADED or code_ == CASS_ERROR_SERVER_READ_TIMEOUT;
} }
/** /**

View File

@@ -36,7 +36,8 @@ Handle::Handle(Settings clusterSettings) : cluster_{clusterSettings}
{ {
} }
Handle::Handle(std::string_view contactPoints) : Handle{Settings::defaultSettings().withContactPoints(contactPoints)} Handle::Handle(std::string_view contactPoints)
: Handle{Settings::defaultSettings().withContactPoints(contactPoints)}
{ {
} }
@@ -84,8 +85,11 @@ Handle::disconnect() const
Handle::FutureType Handle::FutureType
Handle::asyncReconnect(std::string_view keyspace) const Handle::asyncReconnect(std::string_view keyspace) const
{ {
if (auto rc = asyncDisconnect().await(); not rc) // sync if (auto rc = asyncDisconnect().await(); not rc) { // sync
throw std::logic_error("Reconnect to keyspace '" + std::string{keyspace} + "' failed: " + rc.error()); throw std::logic_error(
"Reconnect to keyspace '" + std::string{keyspace} + "' failed: " + rc.error()
);
}
return asyncConnect(keyspace); return asyncConnect(keyspace);
} }
@@ -123,7 +127,10 @@ Handle::asyncExecute(StatementType const& statement) const
} }
Handle::FutureWithCallbackType Handle::FutureWithCallbackType
Handle::asyncExecute(StatementType const& statement, std::function<void(ResultOrErrorType)>&& cb) const Handle::asyncExecute(
StatementType const& statement,
std::function<void(ResultOrErrorType)>&& cb
) const
{ {
return Handle::FutureWithCallbackType{cass_session_execute(session_, statement), std::move(cb)}; return Handle::FutureWithCallbackType{cass_session_execute(session_, statement), std::move(cb)};
} }
@@ -147,9 +154,14 @@ Handle::execute(std::vector<StatementType> const& statements) const
} }
Handle::FutureWithCallbackType Handle::FutureWithCallbackType
Handle::asyncExecute(std::vector<StatementType> const& statements, std::function<void(ResultOrErrorType)>&& cb) const Handle::asyncExecute(
std::vector<StatementType> const& statements,
std::function<void(ResultOrErrorType)>&& cb
) const
{ {
return Handle::FutureWithCallbackType{cass_session_execute_batch(session_, Batch{statements}), std::move(cb)}; return Handle::FutureWithCallbackType{
cass_session_execute_batch(session_, Batch{statements}), std::move(cb)
};
} }
Handle::PreparedStatementType Handle::PreparedStatementType

View File

@@ -293,14 +293,18 @@ public:
execute(std::vector<StatementType> const& statements) const; execute(std::vector<StatementType> const& statements) const;
/** /**
* @brief Execute a batch of (bound or simple) statements asynchronously with a completion callback. * @brief Execute a batch of (bound or simple) statements asynchronously with a completion
* callback.
* *
* @param statements The statements to execute * @param statements The statements to execute
* @param cb The callback to execute when data is ready * @param cb The callback to execute when data is ready
* @return A future that holds onto the callback provided * @return A future that holds onto the callback provided
*/ */
[[nodiscard]] FutureWithCallbackType [[nodiscard]] FutureWithCallbackType
asyncExecute(std::vector<StatementType> const& statements, std::function<void(ResultOrErrorType)>&& cb) const; asyncExecute(
std::vector<StatementType> const& statements,
std::function<void(ResultOrErrorType)>&& cb
) const;
/** /**
* @brief Prepare a statement. * @brief Prepare a statement.
@@ -314,8 +318,8 @@ public:
}; };
/** /**
* @brief Extracts the results into series of std::tuple<Types...> by creating a simple wrapper with an STL input * @brief Extracts the results into series of std::tuple<Types...> by creating a simple wrapper with
* iterator inside. * an STL input iterator inside.
* *
* You can call .begin() and .end() in order to iterate as usual. * You can call .begin() and .end() in order to iterate as usual.
* This also means that you can use it in a range-based for or with some algorithms. * This also means that you can use it in a range-based for or with some algorithms.

View File

@@ -43,9 +43,14 @@ namespace data::cassandra {
* @return The qualified table name * @return The qualified table name
*/ */
template <SomeSettingsProvider SettingsProviderType> template <SomeSettingsProvider SettingsProviderType>
[[nodiscard]] std::string inline qualifiedTableName(SettingsProviderType const& provider, std::string_view name) [[nodiscard]] std::string inline qualifiedTableName(
SettingsProviderType const& provider,
std::string_view name
)
{ {
return fmt::format("{}.{}{}", provider.getKeyspace(), provider.getTablePrefix().value_or(""), name); return fmt::format(
"{}.{}{}", provider.getKeyspace(), provider.getTablePrefix().value_or(""), name
);
} }
/** /**
@@ -65,7 +70,8 @@ public:
* *
* @param settingsProvider The settings provider * @param settingsProvider The settings provider
*/ */
explicit Schema(SettingsProviderType const& settingsProvider) : settingsProvider_{std::cref(settingsProvider)} explicit Schema(SettingsProviderType const& settingsProvider)
: settingsProvider_{std::cref(settingsProvider)}
{ {
} }

View File

@@ -61,12 +61,18 @@ SettingsProvider::parseOptionalCertificate() const
auto const path = std::filesystem::path(certPath.asString()); auto const path = std::filesystem::path(certPath.asString());
std::ifstream fileStream(path.string(), std::ios::in); std::ifstream fileStream(path.string(), std::ios::in);
if (!fileStream) { if (!fileStream) {
throw std::system_error(errno, std::generic_category(), "Opening certificate " + path.string()); throw std::system_error(
errno, std::generic_category(), "Opening certificate " + path.string()
);
} }
std::string contents(std::istreambuf_iterator<char>{fileStream}, std::istreambuf_iterator<char>{}); std::string contents(
std::istreambuf_iterator<char>{fileStream}, std::istreambuf_iterator<char>{}
);
if (fileStream.bad()) { if (fileStream.bad()) {
throw std::system_error(errno, std::generic_category(), "Reading certificate " + path.string()); throw std::system_error(
errno, std::generic_category(), "Reading certificate " + path.string()
);
} }
return contents; return contents;
@@ -82,7 +88,8 @@ SettingsProvider::parseSettings() const
// all config values used in settings is under "database.cassandra" prefix // all config values used in settings is under "database.cassandra" prefix
if (config_.getValueView("secure_connect_bundle").hasValue()) { if (config_.getValueView("secure_connect_bundle").hasValue()) {
auto const bundle = Settings::SecureConnectionBundle{(config_.get<std::string>("secure_connect_bundle"))}; auto const bundle =
Settings::SecureConnectionBundle{(config_.get<std::string>("secure_connect_bundle"))};
settings.connectionInfo = bundle; settings.connectionInfo = bundle;
} else { } else {
Settings::ContactPoints out; Settings::ContactPoints out;
@@ -101,12 +108,14 @@ SettingsProvider::parseSettings() const
if (config_.getValueView("connect_timeout").hasValue()) { if (config_.getValueView("connect_timeout").hasValue()) {
auto const connectTimeoutSecond = config_.get<uint32_t>("connect_timeout"); auto const connectTimeoutSecond = config_.get<uint32_t>("connect_timeout");
settings.connectionTimeout = std::chrono::milliseconds{connectTimeoutSecond * util::kMILLISECONDS_PER_SECOND}; settings.connectionTimeout =
std::chrono::milliseconds{connectTimeoutSecond * util::kMILLISECONDS_PER_SECOND};
} }
if (config_.getValueView("request_timeout").hasValue()) { if (config_.getValueView("request_timeout").hasValue()) {
auto const requestTimeoutSecond = config_.get<uint32_t>("request_timeout"); auto const requestTimeoutSecond = config_.get<uint32_t>("request_timeout");
settings.requestTimeout = std::chrono::milliseconds{requestTimeoutSecond * util::kMILLISECONDS_PER_SECOND}; settings.requestTimeout =
std::chrono::milliseconds{requestTimeoutSecond * util::kMILLISECONDS_PER_SECOND};
} }
settings.certificate = parseOptionalCertificate(); settings.certificate = parseOptionalCertificate();

View File

@@ -52,7 +52,8 @@ template <
typename StatementType, typename StatementType,
typename HandleType = Handle, typename HandleType = Handle,
SomeRetryPolicy RetryPolicyType = ExponentialBackoffRetryPolicy> SomeRetryPolicy RetryPolicyType = ExponentialBackoffRetryPolicy>
class AsyncExecutor : public std::enable_shared_from_this<AsyncExecutor<StatementType, HandleType, RetryPolicyType>> { class AsyncExecutor : public std::enable_shared_from_this<
AsyncExecutor<StatementType, HandleType, RetryPolicyType>> {
using FutureWithCallbackType = typename HandleType::FutureWithCallbackType; using FutureWithCallbackType = typename HandleType::FutureWithCallbackType;
using CallbackType = std::function<void(typename HandleType::ResultOrErrorType)>; using CallbackType = std::function<void(typename HandleType::ResultOrErrorType)>;
using RetryCallbackType = std::function<void()>; using RetryCallbackType = std::function<void()>;
@@ -92,7 +93,9 @@ public:
} }
}; };
auto ptr = std::make_shared<EnableMakeShared>(ioc, std::move(data), std::move(onComplete), std::move(onRetry)); auto ptr = std::make_shared<EnableMakeShared>(
ioc, std::move(data), std::move(onComplete), std::move(onRetry)
);
ptr->execute(handle); ptr->execute(handle);
} }
@@ -103,7 +106,10 @@ private:
CallbackType&& onComplete, CallbackType&& onComplete,
RetryCallbackType&& onRetry RetryCallbackType&& onRetry
) )
: data_{std::move(data)}, retryPolicy_{ioc}, onComplete_{std::move(onComplete)}, onRetry_{std::move(onRetry)} : data_{std::move(data)}
, retryPolicy_{ioc}
, onComplete_{std::move(onComplete)}
, onRetry_{std::move(onRetry)}
{ {
} }

View File

@@ -44,7 +44,8 @@ namespace data::cassandra::impl {
* UNLOGGED: For performance. Sends many separate updates in one network trip to be fast. * UNLOGGED: For performance. Sends many separate updates in one network trip to be fast.
* Use this for bulk-loading unrelated data, but know there's NO all-or-nothing guarantee. * Use this for bulk-loading unrelated data, but know there's NO all-or-nothing guarantee.
* *
* More info here: https://docs.datastax.com/en/developer/cpp-driver-dse/1.10/features/basics/batches/index.html * More info here:
* https://docs.datastax.com/en/developer/cpp-driver-dse/1.10/features/basics/batches/index.html
*/ */
Batch::Batch(std::vector<Statement> const& statements) Batch::Batch(std::vector<Statement> const& statements)
: ManagedObject{cass_batch_new(CASS_BATCH_TYPE_UNLOGGED), kBATCH_DELETER} : ManagedObject{cass_batch_new(CASS_BATCH_TYPE_UNLOGGED), kBATCH_DELETER}

View File

@@ -44,7 +44,8 @@ Cluster::Cluster(Settings const& settings) : ManagedObject{cass_cluster_new(), k
using std::to_string; using std::to_string;
cass_cluster_set_token_aware_routing(*this, cass_true); cass_cluster_set_token_aware_routing(*this, cass_true);
if (auto const rc = cass_cluster_set_protocol_version(*this, CASS_PROTOCOL_VERSION_V4); rc != CASS_OK) { if (auto const rc = cass_cluster_set_protocol_version(*this, CASS_PROTOCOL_VERSION_V4);
rc != CASS_OK) {
throw std::runtime_error( throw std::runtime_error(
fmt::format("Error setting cassandra protocol version to v4: {}", cass_error_desc(rc)) fmt::format("Error setting cassandra protocol version to v4: {}", cass_error_desc(rc))
); );
@@ -52,7 +53,11 @@ Cluster::Cluster(Settings const& settings) : ManagedObject{cass_cluster_new(), k
if (auto const rc = cass_cluster_set_num_threads_io(*this, settings.threads); rc != CASS_OK) { if (auto const rc = cass_cluster_set_num_threads_io(*this, settings.threads); rc != CASS_OK) {
throw std::runtime_error( throw std::runtime_error(
fmt::format("Error setting cassandra io threads to {}: {}", settings.threads, cass_error_desc(rc)) fmt::format(
"Error setting cassandra io threads to {}: {}",
settings.threads,
cass_error_desc(rc)
)
); );
} }
@@ -62,24 +67,36 @@ Cluster::Cluster(Settings const& settings) : ManagedObject{cass_cluster_new(), k
// TODO: AWS keyspace reads should be local_one to save cost // TODO: AWS keyspace reads should be local_one to save cost
if (settings.provider == cassandra::impl::Provider::Keyspace) { if (settings.provider == cassandra::impl::Provider::Keyspace) {
if (auto const rc = cass_cluster_set_consistency(*this, CASS_CONSISTENCY_LOCAL_QUORUM); rc != CASS_OK) { if (auto const rc = cass_cluster_set_consistency(*this, CASS_CONSISTENCY_LOCAL_QUORUM);
throw std::runtime_error(fmt::format("Error setting keyspace consistency: {}", cass_error_desc(rc))); rc != CASS_OK) {
throw std::runtime_error(
fmt::format("Error setting keyspace consistency: {}", cass_error_desc(rc))
);
} }
} else { } else {
if (auto const rc = cass_cluster_set_consistency(*this, CASS_CONSISTENCY_QUORUM); rc != CASS_OK) { if (auto const rc = cass_cluster_set_consistency(*this, CASS_CONSISTENCY_QUORUM);
throw std::runtime_error(fmt::format("Error setting cassandra consistency: {}", cass_error_desc(rc))); rc != CASS_OK) {
throw std::runtime_error(
fmt::format("Error setting cassandra consistency: {}", cass_error_desc(rc))
);
} }
} }
if (auto const rc = cass_cluster_set_core_connections_per_host(*this, settings.coreConnectionsPerHost); if (auto const rc =
cass_cluster_set_core_connections_per_host(*this, settings.coreConnectionsPerHost);
rc != CASS_OK) { rc != CASS_OK) {
throw std::runtime_error(fmt::format("Could not set core connections per host: {}", cass_error_desc(rc))); throw std::runtime_error(
fmt::format("Could not set core connections per host: {}", cass_error_desc(rc))
);
} }
auto const queueSize = auto const queueSize = settings.queueSizeIO.value_or(
settings.queueSizeIO.value_or(settings.maxWriteRequestsOutstanding + settings.maxReadRequestsOutstanding); settings.maxWriteRequestsOutstanding + settings.maxReadRequestsOutstanding
);
if (auto const rc = cass_cluster_set_queue_size_io(*this, queueSize); rc != CASS_OK) { if (auto const rc = cass_cluster_set_queue_size_io(*this, queueSize); rc != CASS_OK) {
throw std::runtime_error(fmt::format("Could not set queue size for IO per host: {}", cass_error_desc(rc))); throw std::runtime_error(
fmt::format("Could not set queue size for IO per host: {}", cass_error_desc(rc))
);
} }
setupConnection(settings); setupConnection(settings);
@@ -111,7 +128,9 @@ Cluster::setupContactPoints(Settings::ContactPoints const& points)
auto throwErrorIfNeeded = [](CassError rc, std::string const& label, std::string const& value) { auto throwErrorIfNeeded = [](CassError rc, std::string const& label, std::string const& value) {
if (rc != CASS_OK) { if (rc != CASS_OK) {
throw std::runtime_error( throw std::runtime_error(
fmt::format("Cassandra: Error setting {} [{}]: {}", label, value, cass_error_desc(rc)) fmt::format(
"Cassandra: Error setting {} [{}]: {}", label, value, cass_error_desc(rc)
)
); );
} }
}; };
@@ -132,8 +151,12 @@ void
Cluster::setupSecureBundle(Settings::SecureConnectionBundle const& bundle) Cluster::setupSecureBundle(Settings::SecureConnectionBundle const& bundle)
{ {
LOG(log_.debug()) << "Attempt connection using secure bundle"; LOG(log_.debug()) << "Attempt connection using secure bundle";
if (auto const rc = cass_cluster_set_cloud_secure_connection_bundle(*this, bundle.bundle.data()); rc != CASS_OK) { if (auto const rc =
throw std::runtime_error("Failed to connect using secure connection bundle " + bundle.bundle); cass_cluster_set_cloud_secure_connection_bundle(*this, bundle.bundle.data());
rc != CASS_OK) {
throw std::runtime_error(
"Failed to connect using secure connection bundle " + bundle.bundle
);
} }
} }
@@ -155,7 +178,9 @@ Cluster::setupCredentials(Settings const& settings)
return; return;
LOG(log_.debug()) << "Set credentials; username: " << settings.username.value(); LOG(log_.debug()) << "Set credentials; username: " << settings.username.value();
cass_cluster_set_credentials(*this, settings.username.value().c_str(), settings.password.value().c_str()); cass_cluster_set_credentials(
*this, settings.username.value().c_str(), settings.password.value().c_str()
);
} }
} // namespace data::cassandra::impl } // namespace data::cassandra::impl

View File

@@ -79,7 +79,8 @@ struct Settings {
bool enableLog = false; bool enableLog = false;
/** @brief Connect timeout specified in milliseconds */ /** @brief Connect timeout specified in milliseconds */
std::chrono::milliseconds connectionTimeout = std::chrono::milliseconds{kDEFAULT_CONNECTION_TIMEOUT}; std::chrono::milliseconds connectionTimeout =
std::chrono::milliseconds{kDEFAULT_CONNECTION_TIMEOUT};
/** @brief Request timeout specified in milliseconds */ /** @brief Request timeout specified in milliseconds */
std::chrono::milliseconds requestTimeout = std::chrono::milliseconds{0}; // no timeout at all std::chrono::milliseconds requestTimeout = std::chrono::milliseconds{0}; // no timeout at all
@@ -106,25 +107,31 @@ struct Settings {
Provider provider = kDEFAULT_PROVIDER; Provider provider = kDEFAULT_PROVIDER;
/** @brief Size of the IO queue */ /** @brief Size of the IO queue */
std::optional<uint32_t> queueSizeIO = std::nullopt; // NOLINT(readability-redundant-member-init) std::optional<uint32_t> queueSizeIO =
std::nullopt; // NOLINT(readability-redundant-member-init)
/** @brief SSL certificate */ /** @brief SSL certificate */
std::optional<std::string> certificate = std::nullopt; // NOLINT(readability-redundant-member-init) std::optional<std::string> certificate =
std::nullopt; // NOLINT(readability-redundant-member-init)
/** @brief Username/login */ /** @brief Username/login */
std::optional<std::string> username = std::nullopt; // NOLINT(readability-redundant-member-init) std::optional<std::string> username =
std::nullopt; // NOLINT(readability-redundant-member-init)
/** @brief Password to match the `username` */ /** @brief Password to match the `username` */
std::optional<std::string> password = std::nullopt; // NOLINT(readability-redundant-member-init) std::optional<std::string> password =
std::nullopt; // NOLINT(readability-redundant-member-init)
/** /**
* @brief Creates a new Settings object as a copy of the current one with overridden contact points. * @brief Creates a new Settings object as a copy of the current one with overridden contact
* points.
*/ */
Settings Settings
withContactPoints(std::string_view contactPoints) withContactPoints(std::string_view contactPoints)
{ {
auto tmp = *this; auto tmp = *this;
tmp.connectionInfo = ContactPoints{.contactPoints = std::string{contactPoints}, .port = std::nullopt}; tmp.connectionInfo =
ContactPoints{.contactPoints = std::string{contactPoints}, .port = std::nullopt};
return tmp; return tmp;
} }

View File

@@ -267,8 +267,8 @@ public:
} }
/** /**
* @brief Non-blocking query execution used for writing data. Contrast with write, this method does not execute * @brief Non-blocking query execution used for writing data. Contrast with write, this method
* the statements in a batch. * does not execute the statements in a batch.
* *
* Retries forever with retry policy specified by @ref AsyncExecutor. * Retries forever with retry policy specified by @ref AsyncExecutor.
* *
@@ -278,7 +278,9 @@ public:
void void
writeEach(std::vector<StatementType>&& statements) writeEach(std::vector<StatementType>&& statements)
{ {
std::ranges::for_each(std::move(statements), [this](auto& statement) { this->write(std::move(statement)); }); std::ranges::for_each(std::move(statements), [this](auto& statement) {
this->write(std::move(statement));
});
} }
/** /**
@@ -328,7 +330,9 @@ public:
future.emplace(handle_.get().asyncExecute(statements, [sself](auto&& res) mutable { future.emplace(handle_.get().asyncExecute(statements, [sself](auto&& res) mutable {
boost::asio::post( boost::asio::post(
boost::asio::get_associated_executor(*sself), boost::asio::get_associated_executor(*sself),
[sself, res = std::forward<decltype(res)>(res)]() mutable { sself->complete(std::move(res)); } [sself, res = std::forward<decltype(res)>(res)]() mutable {
sself->complete(std::move(res));
}
); );
})); }));
}; };
@@ -381,7 +385,9 @@ public:
future.emplace(handle_.get().asyncExecute(statement, [sself](auto&& res) mutable { future.emplace(handle_.get().asyncExecute(statement, [sself](auto&& res) mutable {
boost::asio::post( boost::asio::post(
boost::asio::get_associated_executor(*sself), boost::asio::get_associated_executor(*sself),
[sself, res = std::forward<decltype(res)>(res)]() mutable { sself->complete(std::move(res)); } [sself, res = std::forward<decltype(res)>(res)]() mutable {
sself->complete(std::move(res));
}
); );
})); }));
}; };
@@ -431,19 +437,23 @@ public:
futures.reserve(numOutstanding); futures.reserve(numOutstanding);
counters_->registerReadStarted(statements.size()); counters_->registerReadStarted(statements.size());
auto init = [this, &statements, &futures, &errorsCount, &numOutstanding]<typename Self>(Self& self) { auto init = [this, &statements, &futures, &errorsCount, &numOutstanding]<typename Self>(
Self& self
) {
auto sself = std::make_shared<Self>(std::move(self)); auto sself = std::make_shared<Self>(std::move(self));
auto executionHandler = [&errorsCount, &numOutstanding, sself](auto const& res) mutable { auto executionHandler =
if (not res) [&errorsCount, &numOutstanding, sself](auto const& res) mutable {
++errorsCount; if (not res)
++errorsCount;
// when all async operations complete unblock the result // when all async operations complete unblock the result
if (--numOutstanding == 0) { if (--numOutstanding == 0) {
boost::asio::post(boost::asio::get_associated_executor(*sself), [sself]() mutable { boost::asio::post(
sself->complete(); boost::asio::get_associated_executor(*sself),
}); [sself]() mutable { sself->complete(); }
} );
}; }
};
std::transform( std::transform(
std::cbegin(statements), std::cbegin(statements),
@@ -461,7 +471,9 @@ public:
numReadRequestsOutstanding_ -= statements.size(); numReadRequestsOutstanding_ -= statements.size();
if (errorsCount > 0) { if (errorsCount > 0) {
ASSERT(errorsCount <= statements.size(), "Errors number cannot exceed statements number"); ASSERT(
errorsCount <= statements.size(), "Errors number cannot exceed statements number"
);
counters_->registerReadError(errorsCount); counters_->registerReadError(errorsCount);
counters_->registerReadFinished(startTime, statements.size() - errorsCount); counters_->registerReadFinished(startTime, statements.size() - errorsCount);
throw DatabaseTimeout{}; throw DatabaseTimeout{};
@@ -471,7 +483,8 @@ public:
std::vector<ResultType> results; std::vector<ResultType> results;
results.reserve(futures.size()); results.reserve(futures.size());
// it's safe to call blocking get on futures here as we already waited for the coroutine to resume above. // it's safe to call blocking get on futures here as we already waited for the coroutine to
// resume above.
std::transform( std::transform(
std::make_move_iterator(std::begin(futures)), std::make_move_iterator(std::begin(futures)),
std::make_move_iterator(std::end(futures)), std::make_move_iterator(std::end(futures)),

View File

@@ -76,8 +76,8 @@ void
invokeHelper(CassFuture* ptr, void* cbPtr) invokeHelper(CassFuture* ptr, void* cbPtr)
{ {
// Note: can't use Future{ptr}.get() because double free will occur :/ // Note: can't use Future{ptr}.get() because double free will occur :/
// Note2: we are moving/copying it locally as a workaround for an issue we are seeing from asio recently. // Note2: we are moving/copying it locally as a workaround for an issue we are seeing from asio
// stackoverflow.com/questions/77004137/boost-asio-async-compose-gets-stuck-under-load // recently. stackoverflow.com/questions/77004137/boost-asio-async-compose-gets-stuck-under-load
auto* cb = static_cast<FutureWithCallback::FnType*>(cbPtr); auto* cb = static_cast<FutureWithCallback::FnType*>(cbPtr);
auto local = std::make_unique<FutureWithCallback::FnType>(std::move(*cb)); auto local = std::make_unique<FutureWithCallback::FnType>(std::move(*cb));
if (auto const rc = cass_future_error_code(ptr); rc) { if (auto const rc = cass_future_error_code(ptr); rc) {

View File

@@ -139,7 +139,9 @@ struct Result : public ManagedObject<CassResult const> {
std::size_t idx = 0; std::size_t idx = 0;
auto advanceId = [&idx]() { return idx++; }; auto advanceId = [&idx]() { return idx++; };
return std::make_optional<std::tuple<RowTypes...>>({extractColumn<RowTypes>(row, advanceId())...}); return std::make_optional<std::tuple<RowTypes...>>(
{extractColumn<RowTypes>(row, advanceId())...}
);
} }
template <typename RowType> template <typename RowType>

View File

@@ -63,9 +63,11 @@ public:
[[nodiscard]] bool [[nodiscard]] bool
shouldRetry([[maybe_unused]] CassandraError err) shouldRetry([[maybe_unused]] CassandraError err)
{ {
auto const delayMs = std::chrono::duration_cast<std::chrono::milliseconds>(retry_.delayValue()).count(); auto const delayMs =
LOG(log_.error()) << "Cassandra write error: " << err << ", current retries " << retry_.attemptNumber() std::chrono::duration_cast<std::chrono::milliseconds>(retry_.delayValue()).count();
<< ", retrying in " << delayMs << " milliseconds"; LOG(log_.error()) << "Cassandra write error: " << err << ", current retries "
<< retry_.attemptNumber() << ", retrying in " << delayMs
<< " milliseconds";
return true; // keep retrying forever return true; // keep retrying forever
} }

View File

@@ -32,11 +32,14 @@ constexpr auto kCONTEXT_DELETER = [](CassSsl* ptr) { cass_ssl_free(ptr); };
namespace data::cassandra::impl { namespace data::cassandra::impl {
SslContext::SslContext(std::string const& certificate) : ManagedObject{cass_ssl_new(), kCONTEXT_DELETER} SslContext::SslContext(std::string const& certificate)
: ManagedObject{cass_ssl_new(), kCONTEXT_DELETER}
{ {
cass_ssl_set_verify_flags(*this, CASS_SSL_VERIFY_NONE); cass_ssl_set_verify_flags(*this, CASS_SSL_VERIFY_NONE);
if (auto const rc = cass_ssl_add_trusted_cert(*this, certificate.c_str()); rc != CASS_OK) { if (auto const rc = cass_ssl_add_trusted_cert(*this, certificate.c_str()); rc != CASS_OK) {
throw std::runtime_error(std::string{"Error setting Cassandra SSL Context: "} + cass_error_desc(rc)); throw std::runtime_error(
std::string{"Error setting Cassandra SSL Context: "} + cass_error_desc(rc)
);
} }
} }

View File

@@ -96,12 +96,17 @@ public:
{ {
using std::to_string; using std::to_string;
auto throwErrorIfNeeded = [idx](CassError rc, std::string_view label) { auto throwErrorIfNeeded = [idx](CassError rc, std::string_view label) {
if (rc != CASS_OK) if (rc != CASS_OK) {
throw std::logic_error(fmt::format("[{}] at idx {}: {}", label, idx, cass_error_desc(rc))); throw std::logic_error(
fmt::format("[{}] at idx {}: {}", label, idx, cass_error_desc(rc))
);
}
}; };
auto bindBytes = [this, idx](auto const* data, size_t size) { auto bindBytes = [this, idx](auto const* data, size_t size) {
return cass_statement_bind_bytes(*this, idx, static_cast<cass_byte_t const*>(data), size); return cass_statement_bind_bytes(
*this, idx, static_cast<cass_byte_t const*>(data), size
);
}; };
using DecayedType = std::decay_t<Type>; using DecayedType = std::decay_t<Type>;
@@ -110,7 +115,10 @@ public:
using UintByteTupleType = std::tuple<uint32_t, ripple::uint256>; using UintByteTupleType = std::tuple<uint32_t, ripple::uint256>;
using ByteVectorType = std::vector<ripple::uint256>; using ByteVectorType = std::vector<ripple::uint256>;
if constexpr (std::is_same_v<DecayedType, ripple::uint256> || std::is_same_v<DecayedType, ripple::uint192>) { if constexpr (
std::is_same_v<DecayedType, ripple::uint256> ||
std::is_same_v<DecayedType, ripple::uint192>
) {
auto const rc = bindBytes(value.data(), value.size()); auto const rc = bindBytes(value.data(), value.size());
throwErrorIfNeeded(rc, "Bind ripple::base_uint"); throwErrorIfNeeded(rc, "Bind ripple::base_uint");
} else if constexpr (std::is_same_v<DecayedType, ripple::AccountID>) { } else if constexpr (std::is_same_v<DecayedType, ripple::AccountID>) {
@@ -121,17 +129,22 @@ public:
throwErrorIfNeeded(rc, "Bind vector<unsigned char>"); throwErrorIfNeeded(rc, "Bind vector<unsigned char>");
} else if constexpr (std::is_convertible_v<DecayedType, std::string>) { } else if constexpr (std::is_convertible_v<DecayedType, std::string>) {
// reinterpret_cast is needed here :'( // reinterpret_cast is needed here :'(
auto const rc = bindBytes(reinterpret_cast<unsigned char const*>(value.data()), value.size()); auto const rc =
bindBytes(reinterpret_cast<unsigned char const*>(value.data()), value.size());
throwErrorIfNeeded(rc, "Bind string (as bytes)"); throwErrorIfNeeded(rc, "Bind string (as bytes)");
} else if constexpr (std::is_convertible_v<DecayedType, Text>) { } else if constexpr (std::is_convertible_v<DecayedType, Text>) {
auto const rc = cass_statement_bind_string_n(*this, idx, value.text.c_str(), value.text.size()); auto const rc =
cass_statement_bind_string_n(*this, idx, value.text.c_str(), value.text.size());
throwErrorIfNeeded(rc, "Bind string (as TEXT)"); throwErrorIfNeeded(rc, "Bind string (as TEXT)");
} else if constexpr (std::is_same_v<DecayedType, UintTupleType> || } else if constexpr (
std::is_same_v<DecayedType, UintByteTupleType>) { std::is_same_v<DecayedType, UintTupleType> ||
std::is_same_v<DecayedType, UintByteTupleType>
) {
auto const rc = cass_statement_bind_tuple(*this, idx, Tuple{std::forward<Type>(value)}); auto const rc = cass_statement_bind_tuple(*this, idx, Tuple{std::forward<Type>(value)});
throwErrorIfNeeded(rc, "Bind tuple<uint32, uint32> or <uint32_t, ripple::uint256>"); throwErrorIfNeeded(rc, "Bind tuple<uint32, uint32> or <uint32_t, ripple::uint256>");
} else if constexpr (std::is_same_v<DecayedType, ByteVectorType>) { } else if constexpr (std::is_same_v<DecayedType, ByteVectorType>) {
auto const rc = cass_statement_bind_collection(*this, idx, Collection{std::forward<Type>(value)}); auto const rc =
cass_statement_bind_collection(*this, idx, Collection{std::forward<Type>(value)});
throwErrorIfNeeded(rc, "Bind collection"); throwErrorIfNeeded(rc, "Bind collection");
} else if constexpr (std::is_same_v<DecayedType, bool>) { } else if constexpr (std::is_same_v<DecayedType, bool>) {
auto const rc = cass_statement_bind_bool(*this, idx, value ? cass_true : cass_false); auto const rc = cass_statement_bind_bool(*this, idx, value ? cass_true : cass_false);

View File

@@ -34,7 +34,8 @@ namespace data::cassandra::impl {
{ {
} }
/* implicit */ TupleIterator::TupleIterator(CassIterator* ptr) : ManagedObject{ptr, kTUPLE_ITERATOR_DELETER} /* implicit */ TupleIterator::TupleIterator(CassIterator* ptr)
: ManagedObject{ptr, kTUPLE_ITERATOR_DELETER}
{ {
} }

View File

@@ -65,7 +65,9 @@ public:
auto throwErrorIfNeeded = [idx](CassError rc, std::string_view label) { auto throwErrorIfNeeded = [idx](CassError rc, std::string_view label) {
if (rc != CASS_OK) { if (rc != CASS_OK) {
auto const tag = '[' + std::string{label} + ']'; auto const tag = '[' + std::string{label} + ']';
throw std::logic_error(tag + " at idx " + to_string(idx) + ": " + cass_error_desc(rc)); throw std::logic_error(
tag + " at idx " + to_string(idx) + ": " + cass_error_desc(rc)
);
} }
}; };

View File

@@ -92,7 +92,9 @@ LedgerCacheFile::write(DataView dataView)
} }
Header const header{ Header const header{
.latestSeq = dataView.latestSeq, .mapSize = dataView.map.size(), .deletedSize = dataView.deleted.size() .latestSeq = dataView.latestSeq,
.mapSize = dataView.map.size(),
.deletedSize = dataView.deleted.size()
}; };
file.write(header); file.write(header);
file.write(kSEPARATOR); file.write(kSEPARATOR);
@@ -115,10 +117,17 @@ LedgerCacheFile::write(DataView dataView)
auto const hash = file.hash(); auto const hash = file.hash();
file.write(hash.data(), decltype(hash)::bytes); file.write(hash.data(), decltype(hash)::bytes);
// flush internal buffer explicitly before renaming
if (auto const expectedSuccess = file.close(); not expectedSuccess.has_value()) {
return expectedSuccess;
}
try { try {
std::filesystem::rename(newFilePath, path_); std::filesystem::rename(newFilePath, path_);
} catch (std::exception const& e) { } catch (std::exception const& e) {
return std::unexpected{fmt::format("Error moving cache file from {} to {}: {}", newFilePath, path_, e.what())}; return std::unexpected{
fmt::format("Error moving cache file from {} to {}: {}", newFilePath, path_, e.what())
};
} }
return {}; return {};
@@ -140,12 +149,14 @@ LedgerCacheFile::read(uint32_t minLatestSequence)
return std::unexpected{"Error reading cache header"}; return std::unexpected{"Error reading cache header"};
} }
if (header.version != kVERSION) { if (header.version != kVERSION) {
return std::unexpected{ return std::unexpected{fmt::format(
fmt::format("Cache has wrong version: expected {} found {}", kVERSION, header.version) "Cache has wrong version: expected {} found {}", kVERSION, header.version
}; )};
} }
if (header.latestSeq < minLatestSequence) { if (header.latestSeq < minLatestSequence) {
return std::unexpected{fmt::format("Latest sequence ({}) in the cache file is too low.", header.latestSeq)}; return std::unexpected{
fmt::format("Latest sequence ({}) in the cache file is too low.", header.latestSeq)
};
} }
result.latestSeq = header.latestSeq; result.latestSeq = header.latestSeq;
@@ -153,7 +164,8 @@ LedgerCacheFile::read(uint32_t minLatestSequence)
if (not file.readRaw(separator.data(), separator.size())) { if (not file.readRaw(separator.data(), separator.size())) {
return std::unexpected{"Error reading cache header"}; return std::unexpected{"Error reading cache header"};
} }
if (auto verificationResult = verifySeparator(separator); not verificationResult.has_value()) { if (auto verificationResult = verifySeparator(separator);
not verificationResult.has_value()) {
return std::unexpected{std::move(verificationResult).error()}; return std::unexpected{std::move(verificationResult).error()};
} }
@@ -162,15 +174,16 @@ LedgerCacheFile::read(uint32_t minLatestSequence)
if (not cacheEntryExpected.has_value()) { if (not cacheEntryExpected.has_value()) {
return std::unexpected{std::move(cacheEntryExpected).error()}; return std::unexpected{std::move(cacheEntryExpected).error()};
} }
// Using insert with hint here to decrease insert operation complexity to the amortized constant instead of // Using insert with hint here to decrease insert operation complexity to the amortized
// logN // constant instead of logN
result.map.insert(result.map.end(), std::move(cacheEntryExpected).value()); result.map.insert(result.map.end(), std::move(cacheEntryExpected).value());
} }
if (not file.readRaw(separator.data(), separator.size())) { if (not file.readRaw(separator.data(), separator.size())) {
return std::unexpected{"Error reading separator"}; return std::unexpected{"Error reading separator"};
} }
if (auto verificationResult = verifySeparator(separator); not verificationResult.has_value()) { if (auto verificationResult = verifySeparator(separator);
not verificationResult.has_value()) {
return std::unexpected{std::move(verificationResult).error()}; return std::unexpected{std::move(verificationResult).error()};
} }
@@ -185,13 +198,16 @@ LedgerCacheFile::read(uint32_t minLatestSequence)
if (not file.readRaw(separator.data(), separator.size())) { if (not file.readRaw(separator.data(), separator.size())) {
return std::unexpected{"Error reading separator"}; return std::unexpected{"Error reading separator"};
} }
if (auto verificationResult = verifySeparator(separator); not verificationResult.has_value()) { if (auto verificationResult = verifySeparator(separator);
not verificationResult.has_value()) {
return std::unexpected{std::move(verificationResult).error()}; return std::unexpected{std::move(verificationResult).error()};
} }
auto const dataHash = file.hash(); auto const dataHash = file.hash();
ripple::uint256 hashFromFile{}; ripple::uint256 hashFromFile{};
if (not file.readRaw(reinterpret_cast<char*>(hashFromFile.data()), decltype(hashFromFile)::bytes)) { if (not file.readRaw(
reinterpret_cast<char*>(hashFromFile.data()), decltype(hashFromFile)::bytes
)) {
return std::unexpected{"Error reading hash"}; return std::unexpected{"Error reading hash"};
} }

View File

@@ -23,6 +23,7 @@
#include <cstddef> #include <cstddef>
#include <cstring> #include <cstring>
#include <expected>
#include <ios> #include <ios>
#include <string> #include <string>
#include <utility> #include <utility>
@@ -59,4 +60,14 @@ OutputFile::hash() const
return std::move(sum).finalize(); return std::move(sum).finalize();
} }
std::expected<void, std::string>
OutputFile::close()
{
file_.close();
if (not file_) {
return std::unexpected{"Error closing cache file"};
}
return {};
}
} // namespace data::impl } // namespace data::impl

View File

@@ -25,6 +25,7 @@
#include <cstddef> #include <cstddef>
#include <cstring> #include <cstring>
#include <expected>
#include <fstream> #include <fstream>
#include <string> #include <string>
@@ -60,6 +61,9 @@ public:
ripple::uint256 ripple::uint256
hash() const; hash() const;
std::expected<void, std::string>
close();
private: private:
void void
writeToFile(char const* data, size_t size); writeToFile(char const* data, size_t size);

Some files were not shown because too many files have changed in this diff Show More