Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Write proper forAllApiVersions used in NetworkOPs.cpp #4833

Merged
merged 25 commits into from Mar 22, 2024

Conversation

Bronek
Copy link
Collaborator

@Bronek Bronek commented Nov 22, 2023

High Level Overview of Change

We have forAllApiVersions in tests, but since #4820 we also need something similar in NetworkOPs.cpp. Write it properly so we can use it both in prod code and in tests

Context of Change

What's MultiApiJson and why do we need it (not this PR)

Class MultiApiJson was added in version 2.0, to enable us to publish JSON data from NetworkOPs.cpp (where we handle data subscriptions) to clients supporting different API version - that is, slightly different JSON objects, because of differences between API versions. A lazy (and very wasteful) solution would have been to build new JSON object for each subscription on each update, however that would cause a serious performance degradation. So instead, when sending publications to subscribed clients, I chose to expand on the existing (faster) solution, which is to build JSON once and then publish it to all clients. The trouble is, we need to send subtly different JSON depending on the API version supported by the client - the good news is that there are not that many supported API versions, and hopefully that number will stay low. This is where MultiApiJson comes in (technically, it is a specialization of MultivarJson). It is a very small (size equals to the number of valid API versions, 3 at this moment) array of distinct JSON objects, which are indexed by API version. All these JSON objects are created as a copy of a single JSON received in the class constructor - which should be the object that's closest to what we want to publish. After construction, each individual copy can be altered to accommodate variations between versions (this is best seen in the body of NetworkOPsImp::transJson), or all can be altered at the same time (there's a set method for this, which is not affected by this PR). The ownership and functions allowing for inspection and alteration of the individual JSON objects is the responsibility of MultiApiJson. This PR changes how we can inspect and alter these individual JSON objects.

Back to this PR

The purpose of this change is to enable the user of the MultiApiJson class to "visit" (that is, execute a piece of code, typically a lambda) a JSON object selected by API version. If the user wants to "visit" all API versions, the MultiApiJson in this PR is smart enough to know what specific versions that means. The best example is in the NetworkOPsImp::transJson function, where the following:

    MultiApiJson multiObj{jvObj};
    visit<RPC::apiMinimumSupportedVersion, RPC::apiMaximumValidVersion>(
        multiObj,  //
        [&](Json::Value& jvTx, unsigned int apiVersion) {
            RPC::insertDeliverMax(
                jvTx[jss::transaction], transaction->getTxnType(), apiVersion);

            if (apiVersion > 1)
              // . . .
        });

has changed to:

    MultiApiJson multiObj{jvObj};
    forAllApiVersions(
        multiObj.visit(),  //
        [&](Json::Value& jvTx, auto v) {
            constexpr unsigned version = decltype(v)::value;
            RPC::insertDeliverMax(
                jvTx[jss::transaction], transaction->getTxnType(), version);

            if constexpr (version > 1)
              // . . .
        });

This came about because a very similar code was also needed in #4820 , inside function NetworkOPsImp::pubValidation, and of course, many instances of forAllApiVersions inside tests. Such uses are likely to increase as we add new API versions. Hence it is important to establish good practice which minimises potential for bugs.

The approach adopted in this PR is to:

  • factor out API versions from ripple/rpc/impl/RPCHelpers.h to a new file ripple/protocol/ApiVersion.h
  • factor out a "policy" template parameter, used by MultivarJson for conversion from version to index in the array of JSON objects
  • factor out MultiApiJson from ripple/json/MultivarJson.h to ApiVersion.h; this is where "policy" ApiVersionHelper (used to specialize MultivarJson) is defined
  • redefine API version numbers as compile-time constants, using std::integral_constant<unsigned, ...> inside ApiVersion.h
  • define new functions forAllApiVersions (removing equivalent from tests) and forApiVersions (similar to the previously defined visit template) inside ApiVersion.h
  • replace MultivarJson::select with a more powerful MultivarJson::visit and a helper nested class visitor_t

The type change of API version from unsigned to std::integral_constant<unsigned, ...> has several consequences:

  • it is now possible to validate during compilation the correctness of a hardcoded version number. Here's an example from NetworkOPsImp::pubValidation:
        multiObj.visit(
            RPC::apiVersion<1>,  // Invalid API version here will trigger compilation error
            [](Json::Value& jvTx) {
                // . . .
            });
  • it is possible to pass a strongly typed API version to the user function, which in turn enables it to perform dispatch via overloading or if constexpr (instead of branching instruction), as seen in NetworkOPsImp::transJson
  • few existing uses of the API version require an explicit cast to unsigned (in ServerHandler.cpp and RPCHelpers.h)
  • since the API client connection handling does not create different types for requests for different API versions, we still need to support all uses of API version as unsigned (this could in time change, but the cost in terms of code churn is not worth it, at this time).
  • MultivarJson::visitor_t has to support both variants of API version selection:
    • with a std::integral_constant<unsigned, ...> (which enforces version correctness via static_assert)
    • with an unsigned (which enforces version correctness via normal assert)

Additionally, the switch from select to visit reverses the calling convention used to publish JSON inside NetworkOPs.cpp, from:

	p->send(
	    multiObj.select(apiVersionSelector(p->getApiVersion())),
	    true);

to:

        multiObj.visit(
            p->getApiVersion(),
            [&](Json::Value const& jv) { p->send(jv, true); });

As for the complexity of change, the implementation of MultivarJson::visitor_t is probably the most challenging, mostly due to the number of defined overloads. The class itself is a stateless niebloid which should make the reasoning about it slightly easier. The number of overloads inside this class also explains the size of unit tests, both MultivarJson_test.cpp and ApiVersion_test.cpp.

Additionally, the newly added visit function inside MultivarJson has two slightly different meanings depending on the parameters:

  • the overload taking no parameters just returns the visitor object for the lazy execution (wrapped in a tiny lambda to provide the *this object); this object is next called by forAllApiVersions etc.
  • the overload taking one or more parameters just calls the visitor directly, forwarding both *this and all the provided parameters, for the eager execution

I realise that this approach might be foreign to some readers, but I feel it is consistent with the paradigms of functional programming, where function evaluation is lazy when possible and eager when needed.

There is also a small drive-by fix in LedgerToJson.cpp where I removed the superfluous (fill.context->apiVersion > 1) conditional inside the scope of else if (fill.context->apiVersion > 1) section (starting at the line 140).

Type of Change

  • Bug fix (non-breaking change which fixes an issue)
  • New feature (non-breaking change which adds functionality)
  • Breaking change (fix or feature that would cause existing functionality to not work as expected)
  • Refactor (non-breaking change that only restructures code)
  • Tests (you added tests for code that already exists, or your new feature included in this PR)
  • Documentation update
  • Chore (no impact to binary, e.g. .gitignore, formatting, dropping support for older tooling)
  • Release

Perf impact not expected: This does not change any algorithms. The binary will be little different for NetworkOPs compared to before the change, but not in a way that would change anything perceptible. It gives the compiler slightly more space to apply optimisations when building JSON and when publishing but this is very minor.

@Bronek Bronek force-pushed the feature/for_all_versions branch 10 times, most recently from 4f95d0f to aaa843f Compare November 28, 2023 22:19
@intelliot intelliot added this to the 2024 release milestone Nov 28, 2023
@Bronek Bronek force-pushed the feature/for_all_versions branch 11 times, most recently from 021bcfc to 577c160 Compare November 30, 2023 15:00
@Bronek Bronek marked this pull request as ready for review November 30, 2023 15:44
@Bronek Bronek force-pushed the feature/for_all_versions branch 3 times, most recently from e0a1331 to c4f4981 Compare December 7, 2023 11:33
@thejohnfreeman
Copy link
Collaborator

Before I dive into the code, can you please share in an English comment (a) what problem this is meant to solve and (b) the general design / intuition of this solution?

@intelliot intelliot added the Perf impact not expected Change is not expected to improve nor harm performance. label Jan 11, 2024
src/ripple/json/MultivarJson.h Outdated Show resolved Hide resolved
src/ripple/protocol/ApiVersion.h Outdated Show resolved Hide resolved
@codecov-commenter
Copy link

codecov-commenter commented Jan 31, 2024

Codecov Report

Attention: Patch coverage is 94.81481% with 14 lines in your changes are missing coverage. Please review.

Project coverage is 76.97%. Comparing base (47c8cc2) to head (6c78c73).

Files Patch % Lines
src/ripple/app/misc/NetworkOPs.cpp 70.37% 7 Missing and 1 partial ⚠️
src/test/protocol/MultiApiJson_test.cpp 98.12% 0 Missing and 3 partials ⚠️
src/ripple/protocol/MultiApiJson.h 95.45% 0 Missing and 2 partials ⚠️
src/ripple/app/ledger/impl/LedgerToJson.cpp 0.00% 1 Missing ⚠️
Additional details and impacted files
@@             Coverage Diff             @@
##           develop    #4833      +/-   ##
===========================================
+ Coverage    76.96%   76.97%   +0.01%     
===========================================
  Files         1127     1129       +2     
  Lines       131768   131858      +90     
  Branches     39699    39662      -37     
===========================================
+ Hits        101417   101503      +86     
+ Misses       24494    24475      -19     
- Partials      5857     5880      +23     

☔ View full report in Codecov by Sentry.
📢 Have feedback on the report? Share it here.

src/ripple/protocol/ApiVersion.h Outdated Show resolved Hide resolved
src/ripple/protocol/ApiVersion.h Outdated Show resolved Hide resolved
src/ripple/protocol/ApiVersion.h Show resolved Hide resolved
src/ripple/app/misc/NetworkOPs.cpp Outdated Show resolved Hide resolved
src/ripple/protocol/ApiVersion.h Outdated Show resolved Hide resolved
src/ripple/protocol/ApiVersion.h Show resolved Hide resolved
src/ripple/protocol/ApiVersion.h Outdated Show resolved Hide resolved
src/ripple/json/MultivarJson.h Outdated Show resolved Hide resolved
src/ripple/json/MultivarJson.h Outdated Show resolved Hide resolved
@intelliot
Copy link
Collaborator

@Bronek - any preference on whether this goes in 2.1 or 2.2? Given it is a refactor, I'm not sure if there is some urgency to getting it merged/released.

@Bronek
Copy link
Collaborator Author

Bronek commented Feb 2, 2024

@intelliot After 2.1 release is fine.

@Bronek
Copy link
Collaborator Author

Bronek commented Feb 7, 2024

from the coverage report:

src/ripple/app/misc/NetworkOPs.cpp

  • old tests of that same part, which do not have (and did not have) full coverage

src/ripple/protocol/MultiApiJson.h

  • two asserts in new code which are impossible to test

src/ripple/app/ledger/impl/LedgerToJson.cpp

  • old tests of that same part, which do not have (and did not have) full coverage

so basically these gaps are either unavoidable, or old.

@Bronek Bronek added the Passed Passed code review & PR owner thinks it's ready to merge. Perf sign-off may still be required. label Mar 13, 2024
@seelabs seelabs merged commit 6edf03c into XRPLF:develop Mar 22, 2024
17 checks passed
legleux added a commit to legleux/rippled that referenced this pull request Apr 12, 2024
* Price Oracle (XLS-47d): (XRPLF#4789) (XRPLF#4789)

Implement native support for Price Oracles.

 A Price Oracle is used to bring real-world data, such as market prices,
 onto the blockchain, enabling dApps to access and utilize information
 that resides outside the blockchain.

 Add Price Oracle functionality:
 - OracleSet: create or update the Oracle object
 - OracleDelete: delete the Oracle object

 To support this functionality add:
 - New RPC method, `get_aggregate_price`, to calculate aggregate price for a token pair of the specified oracles
 - `ltOracle` object

 The `ltOracle` object maintains:
 - Oracle Owner's account
 - Oracle's metadata
 - Up to ten token pairs with the scaled price
 - The last update time the token pairs were updated

 Add Oracle unit-tests

* fix compile error on gcc 13: (XRPLF#4932)

The compilation fails due to an issue in the initializer list
of an optional argument, which holds a vector of pairs.
The code compiles correctly on earlier gcc versions, but fails on gcc 13.

* Set version to 2.2.0-b1

* Remove default ctors from SecretKey and PublicKey: (XRPLF#4607)

* It is now an invariant that all constructed Public Keys are valid,
  non-empty and contain 33 bytes of data.
* Additionally, the memory footprint of the PublicKey class is reduced.
  The size_ data member is declared as static.
* Distinguish and identify the PublisherList retrieved from the local
  config file, versus the ones obtained from other validators.
* Fixes XRPLF#2942

* Fast base58 codec: (XRPLF#4327)

This algorithm is about an order of magnitude faster than the existing
algorithm (about 10x faster for encoding and about 15x faster for
decoding - including the double hash for the checksum). The algorithms
use gcc's int128 (fast MS version will have to wait, in the meantime MS
falls back to the slow code).

* feat: add user version of `feature` RPC (XRPLF#4781)

* uses same formatting as admin RPC
* hides potentially sensitive data

* build: add STCurrency.h to xrpl_core to fix clio build (XRPLF#4939)

* Embed patched recipe for RocksDB 6.29.5 (XRPLF#4947)

* fix: order book update variable swap: (XRPLF#4890)

This is likely the result of a typo when the code was simplified.

* Fix workflows (XRPLF#4948)

The problem was `CONAN_USERNAME` environment variable, which Conan 1.x uses as the default user in package references.

* Upgrade to xxhash 0.8.2 as a Conan requirement, enable SIMD hashing (XRPLF#4893)

We are currently using old version 0.6.2 of `xxhash`, as a verbatim copy and paste of its header file `xxhash.h`. Switch to the more recent version 0.8.2. Since this version is in Conan Center (and properly protects its ABI by keeping the state object incomplete), add it as a Conan requirement. Switch to the SIMD instructions (in the new `XXH3` family) supported by the new version.

* Update remaining actions (XRPLF#4949)

Downgrade {upload,download}-artifact action to v3 because of unreliability with v4.

* Install more public headers (XRPLF#4940)

Fixes some mistakes in XRPLF#4885

* test: Env unit test RPC errors return a unique result: (XRPLF#4877)

* telENV_RPC_FAILED is a new code, reserved exclusively
  for unit tests when RPC fails. This will
  make those types of errors distinct and easier to test
  for when expected and/or diagnose when not.
* Output RPC command result when result is not expected.

* Fix workflows (XRPLF#4951)

- Update container for Doxygen workflow. Matches Linux workflow, with newer GLIBC version required by newer actions.
- Fixes macOS workflow to install and configure Conan correctly. Still fails on tests, but that does not seem attributable to the workflow.

* perf: improve `account_tx` SQL query: (XRPLF#4955)

The witness server makes heavily use of the `account_tx` RPC command. Perf
testing showed that the SQL query used by `account_tx` became unacceptably slow
when the DB was large and there was a `marker` parameter. The plan for the query
showed only indexed reads. This appears to be an issue with the internal SQLite
optimizer. This patch rewrote the query to use `UNION` instead of `OR` and
significantly improves performance. See RXI-896 and RIPD-1847 for more details.

* `fixEmptyDID`: fix amendment to handle empty DID edge case: (XRPLF#4950)

This amendment fixes an edge case where an empty DID object can be
created. It adds an additional check to ensure that DIDs are
non-empty when created, and returns a `tecEMPTY_DID` error if the DID
would be empty.

* Enforce no duplicate slots from incoming connections: (XRPLF#4944)

We do not currently enforce that incoming peer connection does not have
remote_endpoint which is already used (either by incoming or outgoing
connection), hence already stored in slots_. If we happen to receive a
connection from such a duplicate remote_endpoint, it will eventually result in a
crash (when disconnecting) or weird behavior (when updating slot state), as a
result of an apparently matching remote_endpoint in slots_ being used by a
different connection.

* Remove zaphod.alloy.ee hub from default server list: (XRPLF#4903)

Remove the zaphod.alloy.ee hubs from the bootstrap and default configuration after 5 years. It has been an honor to run these servers, but it is now time for another entity to step into this role.

The zaphod servers will be taken offline in a phased manner keeping all those who have peering arrangements informed.

These would be the preferred attributes of a boostrap set of hubs:

    1. Commitment to run the hubs for a minimum of 2 years
    2. Highly available
    3. Geographically dispersed
    4. Secure and up to date
    5. Committed to ensure that peering information is kept private

* Write improved `forAllApiVersions` used in NetworkOPs (XRPLF#4833)

* Don't reach consensus as quickly if no other proposals seen: (XRPLF#4763)

This fixes a case where a peer can desync under a certain timing
circumstance--if it reaches a certain point in consensus before it receives
proposals. 

This was noticed under high transaction volumes. Namely, when we arrive at the
point of deciding whether consensus is reached after minimum establish phase
duration but before having received any proposals. This could be caused by
finishing the previous round slightly faster and/or having some delay in
receiving proposals. Existing behavior arrives at consensus immediately after
the minimum establish duration with no proposals. This causes us to desync
because we then close a non-validated ledger. The change in this PR causes us to
wait for a configured threshold before making the decision to arrive at
consensus with no proposals. This allows validators to catch up and for brief
delays in receiving proposals to be absorbed. There should be no drawback since,
with no proposals coming in, we needn't be in a huge rush to jump ahead.

* fixXChainRewardRounding: round reward shares down: (XRPLF#4933)

When calculating reward shares, the amount should always be rounded
down. If the `fixUniversalNumber` amendment is not active, this works
correctly. If it is not active, then the amount is incorrectly rounded
up. This patch introduces an amendment so it will be rounded down.

* Remove unused files

* Remove packaging scripts

* Consolidate external libraries

* Simplify protobuf generation

* Rename .hpp to .h

* Format formerly .hpp files

* Rewrite includes

$ find src/ripple/ src/test/ -type f -exec sed -i 's:include\s*["<]ripple/\(.*\)\.h\(pp\)\?[">]:include <ripple/\1.h>:' {} +

* Fix source lists

* Add markers around source lists

* fix: improper handling of large synthetic AMM offers:

A large synthetic offer was not handled correctly in the payment engine.
This patch fixes that issue and introduces a new invariant check while
processing synthetic offers.

* Set version to 2.1.1

* chore: change Github Action triggers for build/test jobs (XRPLF#4956)

Github Actions for the build/test jobs (nix.yml, mac.yml, windows.yml) will only run on branches that build packages (develop, release, master), and branches with names starting with "ci/". This is intended as a compromise between disabling CI jobs on personal forks entirely, and having the jobs run as a free-for-all. Note that it will not affect PR jobs at all.

* Address compiler warnings

* Fix search for protoc

* chore: Default validator-keys-tool to master branch: (XRPLF#4943)

* master is the default branch for that project. There's no point in
  using develop.

* Remove unused lambdas from MultiApiJson_test

* fix Conan component reference typo

* Set version to 2.2.0-b2

* bump version

* 2.2.3

* 2.2.4

* 2.2.5

---------

Co-authored-by: Gregory Tsipenyuk <gregtatcam@users.noreply.github.com>
Co-authored-by: seelabs <scott.determan@yahoo.com>
Co-authored-by: Chenna Keshava B S <21219765+ckeshava@users.noreply.github.com>
Co-authored-by: Mayukha Vadari <mvadari@gmail.com>
Co-authored-by: John Freeman <jfreeman08@gmail.com>
Co-authored-by: Bronek Kozicki <brok@incorrekt.com>
Co-authored-by: Ed Hennis <ed@ripple.com>
Co-authored-by: Olek <115580134+oleks-rip@users.noreply.github.com>
Co-authored-by: Alloy Networks <45832257+alloynetworks@users.noreply.github.com>
Co-authored-by: Mark Travis <mtrippled@users.noreply.github.com>
Co-authored-by: Gregory Tsipenyuk <gtsipenyuk@ripple.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Passed Passed code review & PR owner thinks it's ready to merge. Perf sign-off may still be required. Perf impact not expected Change is not expected to improve nor harm performance.
Projects
Status: Merged
Development

Successfully merging this pull request may close these issues.

None yet

6 participants