变更记录
- 2020 年 3 月 27 日:初始草案
状态
已接受背景
本 ADR 延续了 ADR 019 和 ADR 020 中确立的动机、设计与背景。也就是说,我们的目标是为 Cosmos SDK 的客户端侧设计 Protocol Buffer 迁移路径。 本 ADR 在 ADD 020 的基础上继续展开,用于规定查询的编码方式。决策
自定义查询定义
模块通过 protocol buffers 的service 定义来自定义查询。这些 service 定义通常与 GRPC 协议相关并被其使用。不过,protocol buffers 规范指出,它们也可以更通用地用于任何采用 protocol buffer 编码的请求/响应协议。因此,我们可以使用 service 定义来描述自定义 ABCI 查询,甚至复用相当大一部分 GRPC 基础设施。
每个带有自定义查询的模块都应定义一个规范命名为 Query 的服务:
接口类型的处理
使用接口类型并需要真正多态性的模块,通常会将oneof 提升到应用层,由应用层提供该接口所支持的具体实现集合。虽然应用也完全可以对查询采用同样方式,并实现一个应用层查询服务,但更推荐模块通过 google.protobuf.Any 暴露这些接口对应的查询方法。在交易层面,一个顾虑是 Any 的开销过高,不足以支撑其使用价值。不过对于查询而言,这不是问题;提供使用 Any 的通用模块级查询,也不会妨碍应用额外提供返回应用层 oneof 的应用级查询。
对于 gov 模块,一个假设性的示例如下:
自定义查询实现
为了实现查询服务,我们可以复用现有的 gogo protobuf grpc 插件。对于名为Query 的服务,它会生成一个名为 QueryServer 的接口,如下所示:
context.Context,而 querier 方法通常需要一个 sdk.Context 实例来从 store 中读取数据。由于可以使用 WithValue 和 Value 方法向 context.Context 附加任意值,因此 Cosmos SDK 应提供一个函数 sdk.UnwrapSDKContext,用于从传入的 context.Context 中取回 sdk.Context。
以上述 bank 模块为例,QueryBalance 的实现大致如下:
自定义查询注册与路由
如上所示的查询服务实现将通过一个新的RegisterQueryService(grpc.Server) 方法注册到 AppModule 中,其实现可以非常简单,如下:
baseapp.QueryRouter 将新增一个 RegisterService(sd *grpc.ServiceDesc, handler interface{}) 方法,用于将这些查询加入自定义查询路由表中(其路由方式将在下文说明)。这个方法的签名与 GRPC Server 类型上现有的 RegisterServer 方法一致,其中 handler 就是上文所述的自定义查询服务实现。
类似 GRPC 的请求会根据服务名(例如 cosmos_sdk.x.bank.v1.Query)和方法名(例如 QueryBalance),再用 / 组合成完整方法名(例如 /cosmos_sdk.x.bank.v1.Query/QueryBalance)进行路由。随后它会被转换为一个 ABCI 查询:custom/cosmos_sdk.x.bank.v1.Query/QueryBalance。通过 QueryRouter.RegisterService 注册的服务处理器,就会按这种方式进行路由。
除了方法名之外,类似 GRPC 的请求还会携带一个 protobuf 编码的负载,这与 RequestQuery.Data 可以自然对应,并接收一个 protobuf 编码的响应或错误。因此,类似 GRPC 的 rpc 方法与现有的 sdk.Query 和 QueryRouter 基础设施之间存在非常自然的映射关系。
这一基础规范使我们能够将 protocol buffer 的 service 定义复用于 ABCI 自定义查询,从而大幅减少在查询方法中手动解码和编码的需求。
GRPC 协议支持
除了提供一条 ABCI 查询路径之外,我们还可以很容易地提供一个 GRPC 代理服务器,在底层将 GRPC 协议请求路由为 ABCI 查询请求。这样一来,客户端就可以使用其宿主语言中现有的 GRPC 实现,基于这些service 定义,直接对 Cosmos SDK 应用发起查询。为了让这个服务器能够工作,BaseApp 上的 QueryRouter 需要将通过 QueryRouter.RegisterService 注册的服务处理器暴露给该代理服务器实现。节点可以通过命令行参数,在与 ABCI 应用相同的进程中、使用单独端口启动该代理服务器。
REST 查询与 Swagger 生成
grpc-gateway 是一个项目,它通过服务方法上的特殊注解将 REST 调用转换为 GRPC 调用。希望暴露 REST 查询的模块,应当像下面示例那样,在其rpc 方法上添加 google.api.http 注解。
.proto 文件和 swagger.json 文件的命令。
客户端使用
gogo protobuf 的 grpc 插件除了生成服务端接口,也会生成客户端接口。对于上文定义的Query 服务,我们会得到一个类似如下的 QueryClient 接口:
Context 将新增一个 QueryConn 方法,该方法返回一个 ClientConn,用于将调用路由到 ABCI 查询。
随后,客户端(例如 CLI 方法)就可以像下面这样调用查询方法:
测试
测试可以通过一个QueryServerTestHelper,直接基于 keeper 和 sdk.Context 引用创建查询客户端,如下所示:
后续改进
影响
正面
- 大幅简化 querier 实现(无需手动编码/解码)
- 查询客户端生成更加容易(可以使用现有 grpc 和 swagger 工具)
- 无需再实现 REST 查询
- 类型安全的查询方法(由 grpc 插件生成)
- 未来查询方法的破坏性变更会更少,因为 buf 提供了向后兼容保证
负面
- 所有使用现有 ABCI/REST 查询的客户端都需要重构,以适配新的 GRPC/REST 查询路径,以及 protobuf/proto-json 编码的数据;但在 protobuf 重构中,这基本上是不可避免的
中性
参考
Changelog
- 2020 March 27: Initial Draft
Status
AcceptedContext
This ADR is a continuation of the motivation, design, and context established in ADR 019 and ADR 020, namely, we aim to design the Protocol Buffer migration path for the client-side of the Cosmos SDK. This ADR continues from ADD 020 to specify the encoding of queries.Decision
Custom Query Definition
Modules define custom queries through a protocol buffersservice definition.
These service definitions are generally associated with and used by the
GRPC protocol. However, the protocol buffers specification indicates that
they can be used more generically by any request/response protocol that uses
protocol buffer encoding. Thus, we can use service definitions for specifying
custom ABCI queries and even reuse a substantial amount of the GRPC infrastructure.
Each module with custom queries should define a service canonically named Query:
Handling of Interface Types
Modules that use interface types and need true polymorphism generally force aoneof up to the app-level that provides the set of concrete implementations of
that interface that the app supports. While app’s are welcome to do the same for
queries and implement an app-level query service, it is recommended that modules
provide query methods that expose these interfaces via google.protobuf.Any.
There is a concern on the transaction level that the overhead of Any is too
high to justify its usage. However for queries this is not a concern, and
providing generic module-level queries that use Any does not preclude apps
from also providing app-level queries that return use the app-level oneofs.
A hypothetical example for the gov module would look something like:
Custom Query Implementation
In order to implement the query service, we can reuse the existing gogo protobuf grpc plugin, which for a service namedQuery generates an interface named
QueryServer as below:
context.Context,
whereas querier methods generally need an instance of sdk.Context to read
from the store. Since arbitrary values can be attached to context.Context
using the WithValue and Value methods, the Cosmos SDK should provide a function
sdk.UnwrapSDKContext to retrieve the sdk.Context from the provided
context.Context.
An example implementation of QueryBalance for the bank module as above would
look something like:
Custom Query Registration and Routing
Query server implementations as above would be registered withAppModules using
a new method RegisterQueryService(grpc.Server) which could be implemented simply
as below:
RegisterService(sd *grpc.ServiceDesc, handler interface{})
will be added to the existing baseapp.QueryRouter to add the queries to the custom
query routing table (with the routing method being described below).
The signature for this method matches the existing
RegisterServer method on the GRPC Server type where handler is the custom
query server implementation described above.
GRPC-like requests are routed by the service name (ex. cosmos_sdk.x.bank.v1.Query)
and method name (ex. QueryBalance) combined with /s to form a full
method name (ex. /cosmos_sdk.x.bank.v1.Query/QueryBalance). This gets translated
into an ABCI query as custom/cosmos_sdk.x.bank.v1.Query/QueryBalance. Service handlers
registered with QueryRouter.RegisterService will be routed this way.
Beyond the method name, GRPC requests carry a protobuf encoded payload, which maps naturally
to RequestQuery.Data, and receive a protobuf encoded response or error. Thus
there is a quite natural mapping of GRPC-like rpc methods to the existing
sdk.Query and QueryRouter infrastructure.
This basic specification allows us to reuse protocol buffer service definitions
for ABCI custom queries substantially reducing the need for manual decoding and
encoding in query methods.
GRPC Protocol Support
In addition to providing an ABCI query pathway, we can easily provide a GRPC proxy server that routes requests in the GRPC protocol to ABCI query requests under the hood. In this way, clients could use their host languages’ existing GRPC implementations to make direct queries against Cosmos SDK app’s using theseservice definitions. In order for this server to work, the QueryRouter
on BaseApp will need to expose the service handlers registered with
QueryRouter.RegisterService to the proxy server implementation. Nodes could
launch the proxy server on a separate port in the same process as the ABCI app
with a command-line flag.
REST Queries and Swagger Generation
grpc-gateway is a project that translates REST calls into GRPC calls using special annotations on service methods. Modules that want to expose REST queries should addgoogle.api.http
annotations to their rpc methods as in this example below.
.proto files and the swagger.json
file.
Client Usage
The gogo protobuf grpc plugin generates client interfaces in addition to server interfaces. For theQuery service defined above we would get a QueryClient
interface like:
will receive a new methodQueryConnthat returns aClientConn`
that routes calls to ABCI queries
Clients (such as CLI methods) will then be able to call query methods like this:
Testing
Tests would be able to create a query client directly from keeper andsdk.Context
references using a QueryServerTestHelper as below:
Future Improvements
Consequences
Positive
- greatly simplified querier implementation (no manual encoding/decoding)
- easy query client generation (can use existing grpc and swagger tools)
- no need for REST query implementations
- type safe query methods (generated via grpc plugin)
- going forward, there will be less breakage of query methods because of the backwards compatibility guarantees provided by buf
Negative
- all clients using the existing ABCI/REST queries will need to be refactored for both the new GRPC/REST query paths as well as protobuf/proto-json encoded data, but this is more or less unavoidable in the protobuf refactoring