-
Notifications
You must be signed in to change notification settings - Fork 1.5k
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
enable testing experimental API versions in unit tests #3781
Conversation
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Nice job! I left a number of thoughts for your consideration. But I'm also aware that I might have missed the boat on any of them. Let me know what you think.
Once we're happy with the changes I'll see what happens if I put @Frassel's test on top of your changes and see how it works.
Thanks for digging into this!
@@ -166,8 +166,13 @@ class HandlerTable | |||
template <std::size_t N> | |||
explicit HandlerTable(const Handler (&entries)[N]) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I don't have a good place to leave this comment, but here goes.
Convince me of the value provided by member variable table_
being a...
std::array<std::map<std::string, Handler>, ApiNumberVersionSupported>
table_;
As far as I can tell this array contains two identical std::map<std::string, Handler>
.
I think I understand how this came about. People thought there would be different handlers for different versions. But I think, in reality, those handlers that distinguish between API versions do so internally. There is currently no change in the handler registration process based on supported API version.
Thoughts? Am I off base on this? Thanks.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Right, the current code allow different handlers for different versions, in addition to one handler dealing with different API versions internally. I think it is more flexible in case people want different handlers. But I don't against simplifying it. Love to hear what @cjcobb23 has to say.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@cjcobb23, what's your feeling on this? My opinion is still that keeping arrays of handlers for each API version is complexity that we don't see any need for yet. But I'm open to being persuaded otherwise.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I prefer that the handlers distinguish between different API versions internally. It's easier to reuse common logic within the handlers this way, especially when the version differences are small. For drastically different handlers, I think we would just do something similar to how some of the reporting mode handlers were implemented: https://github.com/ripple/rippled/blob/9d89d4c1885e13e3c0aa7cdb4a389c6fbfd3790a/src/ripple/rpc/handlers/Tx.cpp#L173
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
However, if the handlers themselves distinguish between different versions internally, I don't think there is a way for different versions of the handler to have different conditions. The condition check happens before actually calling the handler. I don't know how likely it is that different versions of the handlers have different conditions, but it's probably possible.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@pwang200 @scottschurr I think if we want to support the case where different versions of the handler have different conditions, we need to keep this table. However, maybe we should remove the table for now and add it back if/when we need different conditions for different versions of the handler. Thoughts?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Speaking only for myself, I'd rather remove the table now. We don't know whether we'll need this particular flexibility (or something else) in the future. I'd prefer to postpone the complexity until it's needed.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Thanks for the votes! Let's remove it.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Changed.
@pwang200 @scottschurr I know I'm coming in late on this. I am not sure that the problem the pull request is trying to solve is even a problem, though maybe I don't fully understand the situation. I don't think I agree that "experimental" API versions should be inaccessible via actual RPC. It looks like this pull request is presenting a way to unit test new versions of RPC commands, but those new versions are not actually callable via websocket or HTTP. I don't think this should be the case. We need to be able to also test the calls via actual requests, and not just unit test. I don't think we can say something is ready to go without actually testing it over websocket and HTTP, as well as unit test. Just unit test is not sufficient. Furthermore, why are we merging in code that is experimental, for which we are not confident it works? Developers test their changes, write unit tests,we have a release process and we do QA testing. I don't think we should be merging in changes that we don't want people to be able to even hit. What even is the purpose, since no one can even test the new functionality? All we have are the unit tests. What is going to occur during the time between when we merge in an experimental change, and when we decide that it's ready to go? How is QA even going to test the change, if it's not callable via actual websocket or RPC? Also, it seems strange to me that changes to an existing version of a handler don't need to go through this experimental phase, and instead are immediately callable by clients. Why does a new version of a handler need to go through some experimental phase, but (possibly substantial) changes to an existing version do not? I have a hunch that we are drawing inspiration from the amendment system. However, the amendment system is for changes to transaction processing that would cause a different ledger to be computed. RPCs do not need to be treated the same way. When we make changes to transaction processing and the amendment is enabled, everyone must upgrade and the old way (version) of processing those transactions can no longer be used. For RPCs though, we can call v1 or v2 of a handler. We don't have to call v2; we can always fallback to v1. I think API versioning itself solves this problem. If we release something, and discover there is an issue with a new version of a handler, clients can call the older version. If we really want to be explicit that something is experimental, we can just have the latest API version be considered the beta version. Then the changes are actually callable, but we have a way of saying "this is a beta version. Use with caution", or something like that. We could also leverage the config file as well, such that users must explicitly enable the latest version of the API. I'm not convinced that we actually need this though. Maybe for some changes we are really unsure about, we can use beta versioning. But for other changes that are minor, including the PR that motivated this change, I think the new version can be callable right away, and we don't need to put any sort of beta label on it. EDIT: Sorry for the noise if I'm misunderstanding something. |
@cjcobb23 @scottschurr sorry for the delay, I thought I could get to it today. I will work on the comments soon. |
@cjcobb23 thanks for the comment! As @scottschurr pointed out (in the other PR): "If we're eventually going to support V2 of the API, how do we test additions to the V2 API before that support is enabled for public consumption?" Is it a legitimate concern? I realize it is, because of the need of accumulation of code. What the existing API versioning can do is to accumulate API issues, and when we have enough we code them together and release a new version. This does not allow accumulation of changes in the code base. Because the API versioning (or at least the current setup) is kind of horizontal. When we say V2 API, it intuitively means for the whole set of API functions. If some API functions need changes in V2, then we change them. The rest of the functions are brought to V2 without changes. So if there is one function that we release as V2 and the rest of the functions (without changing) are also release as V2 as well. If we want to change another function in the next release, it will be V3. Let me know if I misunderstood something. We have several options:
Let me know what you think and there could be better options. @cjcobb23 @scottschurr |
@pwang200 Got it, thanks for the explanation. I understand the situation better now. The idea is we accumulate many changes over time into a new version of the API, and then release those changes as one batch. This makes sense to me. I do think it's a bit cumbersome that for developers to test their changes over websocket or HTTP, they need to manually modify the code and rebuild. I feel that having a beta version of the API that is marked as unstable but is still callable over websocket and HTTP (maybe protected by a special config flag as well) would be better. However, that is a somewhat more significant lift than this PR, so I think the changes in this PR are a good solution to the problem for the time being. |
@cjcobb23, I think you have an excellent point that folks need to be able to test the interfaces over RPC, not just with unit tests. So another approach to consider would be to have a flag that goes in the config file to enable the supported API level. Certain unit tests already use changes in the config file to enable specific changes. We could follow that pattern. That would also allow folks to set the config flag in a local rippled instance to test the interface using RPC. This would mean replacing the RAII Thoughts? |
@scottschurr I think using a config flag would be ideal, since we can then easily test the interface using RPC. And you are right that we have other unit tests that already manipulate the config settings, so we could just follow that pattern, instead of having to do something new. |
@cjcobb23 @scottschurr I added a config flag. |
@pwang200 what exactly do you mean by forwarding to peers? |
@pwang200 I see. I think it should be up to the user to make sure both the p2p node and the reporting node have the flag set. I don't think that's incredibly difficult, and if they mess it up, the RPC response clearly indicates the problem. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
cfg/rippled-example.cfg
Outdated
@@ -1502,6 +1502,15 @@ | |||
# Enable or disable access to /vl requests. Default is '1' which | |||
# enables access. | |||
# | |||
# [beta_api] |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'm wondering whether this should be beta_rpc_api
? Currently there are RPC, web socket, and gRPC interfaces. Do we believe we can guarantee that all three of those interfaces switch at the same time? I kinda hope that's the case. But I felt like I should at least ask the question.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Apologies if I'm missing context here, but it seems odd for this to be 0 or 1. In the future, there may be multiple API versions, so why not specify the desired (default?) API version here?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The idea is that the latest changes that are not yet part of a specific version are just part of the beta version. Whether these changes are available for consumption via RPC is controlled by this config value.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I agree with what @cjcobb23 said.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Right, changed.
src/ripple/rpc/impl/Handler.cpp
Outdated
if (version < RPC::apiMinimumSupportedVersion || | ||
(betaEnabled && version > RPC::apiBetaVersion) || | ||
(!betaEnabled && version > RPC::apiMaximumSupportedVersion)) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I wonder if this reformulation is a little more robust since it only checks betaEnabled
once...
if (version < RPC::apiMinimumSupportedVersion ||
version > (betaEnabled ? RPC::apiBetaVersion
: RPC::apiMaximumSupportedVersion))
Just for your consideration...
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Thanks, changed.
src/test/rpc/Version_test.cpp
Outdated
std::string toLoad(R"rippleConfig( | ||
[beta_api] | ||
1 | ||
)rippleConfig"); | ||
c.loadFromString(toLoad); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think all of this `loadFromString stuff can be reduced to a single line and still be readable. Like this:
c.loadFromString("\n[beta_api]\n1\n");
Just for your consideration.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Thanks, changed.
@@ -166,8 +166,13 @@ class HandlerTable | |||
template <std::size_t N> | |||
explicit HandlerTable(const Handler (&entries)[N]) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@cjcobb23, what's your feeling on this? My opinion is still that keeping arrays of handlers for each API version is complexity that we don't see any need for yet. But I'm open to being persuaded otherwise.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
👍 Looks great to me. I think you have a good solution. Thanks for the work and for your flexibility.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
LGTM
2a41b3b
to
2b4aefd
Compare
Motivation
As discussed in #3770, we need a way to unit test API version 2 RPC calls.
Context of Change
The API version that is available for public consumption is 1. But for unit tests, the API version 2 can be enabled.
To show how it works, a version 2
Version
RPC call is also added.Type of Change