-
Notifications
You must be signed in to change notification settings - Fork 3.7k
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
docs: add ADR 054 semver compatible SDK modules #11802
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.
The problem section is clear to me.
The decision chosen here is currently my favorite approach, on a high-level. But I'm personally also not 100% confident we thought about all edge/weird cases. Maybe issues & iterations of this ADR might come up during implementation.
This needs more reviews. @alexanderbez @fdymylja ? This is one of the more complicated pieces of using semver with protobuf and needs to be thought through carefully |
Will be reviewed by EOD |
One thought I have is that maybe in the public |
protoreflect.ProtoMessage | ||
GetFromAddress() string | ||
GetToAddress() string | ||
GetAmount() []v1beta1.CoinI |
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.
Why is this returning v1beta1.CoinI
. What if the proto type changes between versions of MsgSend
?
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.
Because the idea is to move towards interfaces where possible in this codegen. Changing the proto type (say to v1.CoinI) would be breaking which is one thing we're trying to avoid with semver. It should be in a differente semver
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 sorry, I'm not following. Isn't this a type of "foo"
? So I don't see how you can have a version specific type here.
GetToAddress() string | ||
GetAmount() []v1beta1.CoinI | ||
|
||
IsCosmosBankV1MsgSend() // this method distinguishes this interface from other proto types which may otherwise implement the same fields. |
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.
Is this really needed? Can't callers just do an explicit type check?
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.
but they don't know which concrete type it is because there's different generated code in the same binary
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 see. These can get pretty verbose if there are many versions, but I guess there's no other way.
|
||
type MsgClient interface { | ||
Send(ctx context.Context, in MsgSendI, opts …grpc.CallOption) (MsgSendResponseI, error) | ||
// note that for MsgSendResponseI we would need to have a generated response wrapper type to deal with the case where |
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 not following this comment. What does this mean exactly?
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.
For instance if the client version is newer than the server version, then there may be a newer field in the client version of MsgSendResponseI
than the server version. A wrapper type will need to gracefully handle the fact that the newer field is missing and return the default 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.
Is there a distinction in this ADR that describes client vs server version? Or is that something that already exists in the Proto usage within the SDK.
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.
What is meant by client and server version is simply the version of the proto files that the client and server have. It could be the scenario that the client has newer proto files than the server and vice versa. We need to gracefully handle both.
@aaronc any updates on this? |
The main thing here is to get more feedback from the SDK team as to whether this is an acceptable solution. I've tried to outline the rationale and solution as best as I can here. If there's still lack of clarity please ask questions or we can discuss in a call. This is not a simple matter so we should make sure we consider carefully |
This pull request has been automatically marked as stale because it has not had recent activity. It will be closed if no further activity occurs. Thank you for your contributions. |
I agree with Aron's addition of a section on unit and integration test frameworks in the ADR. The concept of a testing matrix that tests different versions of modules against each other is a practical approach. I also believe that ADR-54 and ADR-33 will allow us to integrate with tools built in other ecosystems like for example Rust for ZK, removing the need for some teams to create their own Rust Cosmos SDKs as Zaki pointed out in an interview that the obstacles for ZK in the Cosmos ecosystem can be quite significant without languages like rust. |
|
Idk it feels to me that compatibility is still at the API level and is just protobuff instead of interfaces. |
Yeah, I also think this is not true. It's still API level.
Code generator changes are optional, although it would be a big change if we go that way. |
A bit of an update on my thoughts here. I do actually think using an API module (approach A) generally makes sense and is more convenient. I think changing the assumption that there is a single generated file per proto file introduces lots of complexity. We have discussed scoping of API modules/proto files and I think in the SDK and generally there should be one API module per go module. I think otherwise versioning becomes much more complex as we've discussed elsewhere. I'm not a big fan of approach B currently (code generator changes) unless integrating with something like Rust is a high priority and serialization is benchmarked to be the bottleneck. One alternative idea which hasn't really been discussed yet but is alluded to in approach C may be viable is actually embracing build time tags and |
Can we do it without
How is it possible? I thought that go dependency system only allows to link only one minor (here |
In Approach B) Changes to Generated Code you write about a need to translate types, and generate / create new internal types to satisfy binary interface. Hence for module developer it is ABI level:
With API level compatibility, a developer would directly use |
Both of these points are unfortunate, and certainly make life harder for users, like the Cosmos SDK, who want to express stuff that is straightforward in other language ecosystems like C or Rust. But Go simply doesn't let you do it. |
Despite this approach not being well-supported by go tooling, it might be the least bad short-term way to deal with this. The replace directives would only happen on the app level, so there is no need for them to apply transitively to any other library consumers. And they could be considered temporary - the only real reason you'd want to do this is if you want to hold off on upgrading to a new state machine version after it has been released. Hopefully, that's a temporary concern. Build tags being used to express conditions would hopefully be a rare occurrence where the convention could be to hold off on adopting newer API methods until all consumers are ready to adopt the newest state machine version of a dependency. But as long as they apply transitively to dependencies (I'm not sure of this) they could be an option. At least for the time-being, I'd like to avoid any super radical solutions like requiring all modules to migrate to ADR 033, generated code changes or excessive runtime checks. The speed at which modules are being split up into standalone units is proceeding faster than any of those other solutions will be ready. So having a small startup check seems like the least bad option. |
This document was originally written for, and shared with, the ADR-54 working group and is repeated here. Internally we now have consensus on moving towards message passing (for cross process composability). There is also agreement that forward compatibility, in the context of the same version message with added fields, is a non-goal, which is the same conclusion the below arrives at regarding sane API compatibility. Principles of Sane API DevelopmentThis section identifies the principles of an API development and maintenance path for the CosmosSDK which is Interface
Points (1) through (5), and to some extent (6) (as in the case of x/gov), are already the SDK status quo. The Taken as a whole these points also address Problem 2: Circular dependencies since the API is Implementation
Points (1) through (3) address Problem 1: Semantic Import Versioning Compatibility, (2) for backward, and (3) Point (4) can be implemented without a version-aware message router or inter-module message client, but will Point (5) is nearly the status the quo of the SDK with a few (easily fixable) exceptions. By maintaining a SummaryTo summarize, it seems possible to achieve the goals of ADR-54, namely SDK modules as semantically versioned References
|
Based on our latest working group discussion, we decided to merge this PR as a draft and to update it later once we have a clearer design. |
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.
On the working group call we decided to merge this as a draft.
@kocubinski thanks for the summary.
how this can be achieved? If our msg server defines the following RPC (in Go): import "cosmos-sdk/api/cosmos/bar/v3"
func (s MsgSrv) Foo(b api.Bar) {} How method |
import (
bar_v1 "cosmos-sdk/api/cosmos/bar/v1"
bar_v2 "cosmos-sdk/api/cosmos/bar/v2"
bar_v3 "cosmos-sdk/api/cosmos/bar/v3"
)
func (s MsgServer) FooV1(b bar_v1.Bar) { ... }
func (s MsgServer) FooV2(b bar_v2.Bar) { ... }
func (s MsgServer) FooV3(b bar_v3.Bar) { ... } |
Basically the above. I think there could be a DevUX optimization on top with dynamic dispatch like below. Multiple wire protocol implementations could be bound to the same go RPC function too, but it is probably more useful intraprocess as a go API. import (
bar "cosmos-sdk/api/cosmos/bar"
bar_v1 "cosmos-sdk/api/cosmos/bar/v1"
bar_v2 "cosmos-sdk/api/cosmos/bar/v2"
bar_v3 "cosmos-sdk/api/cosmos/bar/v3"
)
// defined in "cosmos-sdk/api/cosmos/bar"
// each version of `Bar` implements this interface, which could be included in code gen.
type BarMessageI interface {
isBarMessage()
}
// pure boilerplate supporting dynamic dispatch on a interface
func (s MsgServer) Foo(b bar.BarMessageI) {
switch m := b.(type) {
case bar_v1.Bar:
s.FooV1(m)
case bar_v2.Bar:
s.FooV2(m)
case bar_v3.Bar:
s.FooV3(m)
default:
// return error
}
}
func (s MsgServer) FooV1(b bar_v1.Bar) { s.FooV2(b.ToV2(); }
func (s MsgServer) FooV2(b bar_v2.Bar) { so.FooV3(b.ToV3(); }
func (s MsgServer) FooV3(b bar_v3.Bar) { ... } Or if we wanted to throw out even more type safety: func (s MsgServer) Foo(b any) { ... } |
But how do you do it with proto? The protobuf compiler don't generate methods for Also Protobuf compiler uses structs in the interfaces, not interfaces ( So to make it working we would require another custom change in the protobuf compiler, right? |
Practically speaking (about implementation) I think it'd make sense to separate |
No change to the compiler should be necessary. Distinct (and therefore incompatible) proto services BarV1, BarV2, etc. are necessarily defined in distinct proto files, which therefore generate distinct code when run thru protoc, which lives in distinct Go packages. |
Description
Addresses issues related to semantic versioning and protobuf generated code discussed in #10582.
Author Checklist
All items are required. Please add a note to the item if the item is not applicable and
please add links to any relevant follow up issues.
I have...
!
to the type prefix if API or client breaking changeCHANGELOG.md
Reviewers Checklist
All items are required. Please add a note if the item is not applicable and please add
your handle next to the items reviewed if you only reviewed selected items.
I have...
!
in the type prefix if API or client breaking change