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

rpc: Upgrade WebSocketClient to support full client functionality #646

Merged
merged 21 commits into from
Nov 26, 2020
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
21 commits
Select commit Hold shift + click to select a range
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,12 @@
### IMPROVEMENTS:

- `[light-client]` Only require Tokio when `rpc-client` feature is enabled ([#425])
- `[rpc]` The `WebSocketClient` now adds support for all remaining RPC requests
by way of implementing the `Client` trait ([#646])

[#425]: https://github.com/informalsystems/tendermint-rs/issues/425
[#646]: https://github.com/informalsystems/tendermint-rs/pull/646


## v0.17.0-rc3

Expand Down
101 changes: 73 additions & 28 deletions docs/architecture/adr-008-event-subscription.md
Original file line number Diff line number Diff line change
Expand Up @@ -218,7 +218,7 @@ WebSocket connection to provide subscription functionality (the
#[async_trait]
pub trait SubscriptionClient {
/// `/subscribe`: subscribe to receive events produced by the given query.
async fn subscribe(&mut self, query: String) -> Result<Subscription>;
async fn subscribe(&mut self, query: Query) -> Result<Subscription>;
}
```

Expand Down Expand Up @@ -323,19 +323,23 @@ pub enum EventType {
ValidatorSetUpdates,
}

pub struct Condition {
key: String,
op: Operation,
}

pub enum Operation {
Eq(Operand),
Lt(Operand),
Lte(Operand),
Gt(Operand),
Gte(Operand),
Contains(Operand),
Exists,
// A condition specifies a key (first parameter) and, depending on the
// operation, an value which is an operand of some kind.
pub enum Condition {
// Equals
Eq(String, Operand),
// Less than
Lt(String, Operand),
// Less than or equal to
Lte(String, Operand),
// Greater than
Gt(String, Operand),
// Greater than or equal to
Gte(String, Operand),
// Contains (to check if a key contains a certain sub-string)
Contains(String, String),
// Exists (to check if a key exists)
Exists(String),
}

// According to https://docs.tendermint.com/master/rpc/#/Websocket/subscribe,
Expand All @@ -346,7 +350,8 @@ pub enum Operation {
// operand types to the `Operand` enum, as this would improve ergonomics.
pub enum Operand {
String(String),
Integer(i64),
Signed(i64),
Unsigned(u64),
Float(f64),
Date(chrono::Date),
DateTime(chrono::DateTime),
Expand All @@ -361,7 +366,7 @@ track of all of the queries relating to a particular client.
```rust
pub struct SubscriptionRouter {
// A map of queries -> (map of subscription IDs -> result event tx channels)
subscriptions: HashMap<Query, HashMap<SubscriptionId, ChannelTx<Result<Event>>>>,
subscriptions: HashMap<String, HashMap<String, SubscriptionTx>>,
}
```

Expand All @@ -372,21 +377,61 @@ server [drops subscription IDs from events][tendermint-2949], which is likely if
we want to conform more strictly to the [JSON-RPC standard for
notifications][jsonrpc-notifications].

#### Two-Phase Subscribe/Unsubscribe
### Handling Mixed Events and Responses

Since a full client needs to implement both the `Client` and
`SubscriptionClient` traits, for certain transports (like a WebSocket
connection) we could end up receiving a mixture of events from subscriptions
and responses to RPC requests. To disambiguate these different types of
incoming messages, a simple mechanism is proposed for the
`WebSocketClientDriver` that keeps track of pending requests and only matures
them once it receives its corresponding response.

Due to the fact that a WebSocket connection lacks request/response semantics,
when managing multiple subscriptions from a single client we need to implement a
**two-phase subscription creation/removal process**:
```rust
pub struct WebSocketClientDriver {
// ...

1. An outgoing, but unconfirmed, subscribe/unsubscribe request is tracked.
2. The subscribe/unsubscribe request is confirmed or cancelled by a response
from the remote WebSocket server.
// Commands we've received but have not yet completed, indexed by their ID.
// A Terminate command is executed immediately.
pending_commands: HashMap<String, DriverCommand>,
}

The need for this two-phase subscribe/unsubscribe process is more clearly
illustrated in the following sequence diagram:
// The different types of requests that the WebSocketClient can send to its
// driver.
//
// Each of SubscribeCommand, UnsubscribeCommand and SimpleRequestCommand keep
// a response channel that allows for the driver to send a response later on
// when it receives a relevant one.
enum DriverCommand {
// Initiate a subscription request.
Subscribe(SubscribeCommand),
// Initiate an unsubscribe request.
Unsubscribe(UnsubscribeCommand),
// For non-subscription-related requests.
SimpleRequest(SimpleRequestCommand),
Terminate,
}
```

![RPC client two-phase
subscribe/unsubscribe](./assets/rpc-client-two-phase-subscribe.png)
IDs of outgoing requests are randomly generated [UUIDv4] strings.

The logic here is as follows:

1. A call is made to `WebSocketClient::subscribe` or
`WebSocketClient::perform`.
2. The client sends the relevant `DriverCommand` to its driver via its internal
communication channel.
3. The driver receives the command, sends the relevant simple or subscription
request, and keeps track of the command in its `pending_commands` member
along with its ID. This allows the driver to continue handling outgoing
requests and incoming responses in the meantime.
4. If the driver receives a JSON-RPC message whose ID corresponds to an ID in
its `pending_commands` member, it assumes that response is relevant to that
command and sends back to the original caller by way of a channel stored in
one of the `SubscribeCommand`, `UnsubscribeCommand` or
`SimpleRequestCommand` structs. Failures are also communicated through this
same mechanism.
5. The pending command is evicted from the `pending_commands` member.

## Status

Expand Down Expand Up @@ -433,4 +478,4 @@ None
[futures-stream-mod]: https://docs.rs/futures/*/futures/stream/index.html
[tendermint-2949]: https://github.com/tendermint/tendermint/issues/2949
[jsonrpc-notifications]: https://www.jsonrpc.org/specification#notification

[UUIDv4]: https://en.wikipedia.org/wiki/Universally_unique_identifier#Version_4_(random)
49 changes: 12 additions & 37 deletions docs/architecture/assets/rpc-client-erd.graphml
Original file line number Diff line number Diff line change
Expand Up @@ -94,10 +94,10 @@
<node id="n7">
<data key="d6">
<y:ShapeNode>
<y:Geometry height="56.0" width="209.0" x="390.5" y="182.0"/>
<y:Geometry height="56.0" width="155.0" x="417.5" y="182.0"/>
<y:Fill color="#DFC0FF" transparent="false"/>
<y:BorderStyle color="#000000" raised="false" type="dashed" width="1.0"/>
<y:NodeLabel alignment="center" autoSizePolicy="content" fontFamily="Helvetica" fontSize="14" fontStyle="plain" hasBackgroundColor="false" hasLineColor="false" height="18.0" horizontalTextPosition="center" iconTextGap="4" modelName="custom" textColor="#000000" verticalTextPosition="bottom" visible="true" width="189.2060546875" x="9.89697265625" xml:space="preserve" y="19.0">TwoPhaseSubscriptionRouter<y:LabelModel><y:SmartNodeLabelModel distance="4.0"/></y:LabelModel><y:ModelParameter><y:SmartNodeLabelModelParameter labelRatioX="0.0" labelRatioY="0.0" nodeRatioX="0.0" nodeRatioY="0.0" offsetX="0.0" offsetY="0.0" upX="0.0" upY="-1.0"/></y:ModelParameter></y:NodeLabel>
<y:NodeLabel alignment="center" autoSizePolicy="content" fontFamily="Helvetica" fontSize="14" fontStyle="plain" hasBackgroundColor="false" hasLineColor="false" height="18.0" horizontalTextPosition="center" iconTextGap="4" modelName="custom" textColor="#000000" verticalTextPosition="bottom" visible="true" width="123.0615234375" x="15.96923828125" xml:space="preserve" y="19.0">SubscriptionRouter<y:LabelModel><y:SmartNodeLabelModel distance="4.0"/></y:LabelModel><y:ModelParameter><y:SmartNodeLabelModelParameter labelRatioX="0.0" labelRatioY="0.0" nodeRatioX="0.0" nodeRatioY="0.0" offsetX="0.0" offsetY="0.0" upX="0.0" upY="-1.0"/></y:ModelParameter></y:NodeLabel>
<y:Shape type="roundrectangle"/>
</y:ShapeNode>
</data>
Expand Down Expand Up @@ -216,26 +216,15 @@ Detail)<y:LabelModel><y:SmartNodeLabelModel distance="4.0"/></y:LabelModel><y:Mo
<node id="n13">
<data key="d6">
<y:ShapeNode>
<y:Geometry height="56.0" width="109.0" x="429.5" y="607.0"/>
<y:Fill color="#BBDCFC" transparent="false"/>
<y:BorderStyle color="#000000" raised="false" type="line" width="1.0"/>
<y:NodeLabel alignment="center" autoSizePolicy="content" fontFamily="Helvetica" fontSize="14" fontStyle="plain" hasBackgroundColor="false" hasLineColor="false" height="18.0" horizontalTextPosition="center" iconTextGap="4" modelName="custom" textColor="#000000" verticalTextPosition="bottom" visible="true" width="65.482421875" x="21.7587890625" xml:space="preserve" y="19.0">Operation<y:LabelModel><y:SmartNodeLabelModel distance="4.0"/></y:LabelModel><y:ModelParameter><y:SmartNodeLabelModelParameter labelRatioX="0.0" labelRatioY="0.0" nodeRatioX="0.0" nodeRatioY="0.0" offsetX="0.0" offsetY="0.0" upX="0.0" upY="-1.0"/></y:ModelParameter></y:NodeLabel>
<y:Shape type="roundrectangle"/>
</y:ShapeNode>
</data>
</node>
<node id="n14">
<data key="d6">
<y:ShapeNode>
<y:Geometry height="56.0" width="109.0" x="616.75" y="607.0"/>
<y:Geometry height="56.0" width="109.0" x="440.5" y="607.0"/>
<y:Fill color="#BBDCFC" transparent="false"/>
<y:BorderStyle color="#000000" raised="false" type="line" width="1.0"/>
<y:NodeLabel alignment="center" autoSizePolicy="content" fontFamily="Helvetica" fontSize="14" fontStyle="plain" hasBackgroundColor="false" hasLineColor="false" height="18.0" horizontalTextPosition="center" iconTextGap="4" modelName="custom" textColor="#000000" verticalTextPosition="bottom" visible="true" width="58.482421875" x="25.2587890625" xml:space="preserve" y="19.0">Operand<y:LabelModel><y:SmartNodeLabelModel distance="4.0"/></y:LabelModel><y:ModelParameter><y:SmartNodeLabelModelParameter labelRatioX="0.0" labelRatioY="0.0" nodeRatioX="0.0" nodeRatioY="0.0" offsetX="0.0" offsetY="0.0" upX="0.0" upY="-1.0"/></y:ModelParameter></y:NodeLabel>
<y:Shape type="roundrectangle"/>
</y:ShapeNode>
</data>
</node>
<node id="n15">
<node id="n14">
<data key="d6">
<y:ShapeNode>
<y:Geometry height="56.0" width="155.0" x="1198.0" y="182.0"/>
Expand Down Expand Up @@ -305,16 +294,14 @@ Detail)<y:LabelModel><y:SmartNodeLabelModel distance="4.0"/></y:LabelModel><y:Mo
</y:PolyLineEdge>
</data>
</edge>
<edge id="e5" source="n3" target="n7">
<edge id="e5" source="n8" target="n7">
<data key="d10">
<y:PolyLineEdge>
<y:Path sx="0.0" sy="0.0" tx="0.0" ty="0.0">
<y:Point x="613.0" y="210.0"/>
</y:Path>
<y:Path sx="-110.009765625" sy="0.0" tx="0.0" ty="0.0"/>
<y:LineStyle color="#000000" type="line" width="1.0"/>
<y:Arrows source="diamond" target="crows_foot_one"/>
<y:EdgeLabel alignment="center" configuration="AutoFlippingLabel" distance="2.0" fontFamily="Helvetica" fontSize="12" fontStyle="plain" hasBackgroundColor="false" hasLineColor="false" height="28.0" horizontalTextPosition="center" iconTextGap="4" modelName="custom" preferredPlacement="anywhere" ratio="0.5" textColor="#000000" verticalTextPosition="bottom" visible="true" width="31.33984375" x="-123.43225215534017" xml:space="preserve" y="-51.67025936280061">Has/
Uses<y:LabelModel><y:SmartEdgeLabelModel autoRotationEnabled="false" defaultAngle="0.0" defaultDistance="10.0"/></y:LabelModel><y:ModelParameter><y:SmartEdgeLabelModelParameter angle="6.283185307179586" distance="33.36407661646361" distanceToCenter="true" position="left" ratio="0.4515565075001525" segment="0"/></y:ModelParameter><y:PreferredPlacementDescriptor angle="0.0" angleOffsetOnRightSide="0" angleReference="absolute" angleRotationOnRightSide="co" distance="-1.0" frozen="true" placement="anywhere" side="anywhere" sideReference="relative_to_edge_flow"/></y:EdgeLabel>
<y:EdgeLabel alignment="center" configuration="AutoFlippingLabel" distance="2.0" fontFamily="Helvetica" fontSize="12" fontStyle="plain" hasBackgroundColor="false" hasLineColor="false" height="28.0" horizontalTextPosition="center" iconTextGap="4" modelName="custom" preferredPlacement="anywhere" ratio="0.5" textColor="#000000" verticalTextPosition="bottom" visible="true" width="31.33984375" x="-65.30288833566169" xml:space="preserve" y="8.364076616463592">Has/
Uses<y:LabelModel><y:SmartEdgeLabelModel autoRotationEnabled="false" defaultAngle="0.0" defaultDistance="10.0"/></y:LabelModel><y:ModelParameter><y:SmartEdgeLabelModelParameter angle="6.283185307179586" distance="8.36407661646358" distanceToCenter="false" position="left" ratio="0.5954113682210318" segment="-1"/></y:ModelParameter><y:PreferredPlacementDescriptor angle="0.0" angleOffsetOnRightSide="0" angleReference="absolute" angleRotationOnRightSide="co" distance="-1.0" frozen="true" placement="anywhere" side="anywhere" sideReference="relative_to_edge_flow"/></y:EdgeLabel>
<y:BendStyle smoothed="false"/>
</y:PolyLineEdge>
</data>
Expand Down Expand Up @@ -385,26 +372,15 @@ Uses<y:LabelModel><y:SmartEdgeLabelModel autoRotationEnabled="false" defaultAngl
<edge id="e11" source="n12" target="n13">
<data key="d10">
<y:PolyLineEdge>
<y:Path sx="0.0" sy="0.0" tx="0.0" ty="0.0"/>
<y:LineStyle color="#000000" type="line" width="1.0"/>
<y:Arrows source="diamond" target="crows_foot_one"/>
<y:EdgeLabel alignment="center" configuration="AutoFlippingLabel" distance="2.0" fontFamily="Helvetica" fontSize="12" fontStyle="plain" hasBackgroundColor="false" hasLineColor="false" height="16.0" horizontalTextPosition="center" iconTextGap="4" modelName="custom" preferredPlacement="anywhere" ratio="0.5" textColor="#000000" verticalTextPosition="bottom" visible="true" width="25.33984375" x="2.04168701171875" xml:space="preserve" y="-22.0">Has<y:LabelModel><y:SmartEdgeLabelModel autoRotationEnabled="false" defaultAngle="0.0" defaultDistance="10.0"/></y:LabelModel><y:ModelParameter><y:SmartEdgeLabelModelParameter angle="6.283185307179586" distance="14.0" distanceToCenter="true" position="left" ratio="-2.919921875" segment="-1"/></y:ModelParameter><y:PreferredPlacementDescriptor angle="0.0" angleOffsetOnRightSide="0" angleReference="absolute" angleRotationOnRightSide="co" distance="-1.0" frozen="true" placement="anywhere" side="anywhere" sideReference="relative_to_edge_flow"/></y:EdgeLabel>
<y:BendStyle smoothed="false"/>
</y:PolyLineEdge>
</data>
</edge>
<edge id="e12" source="n13" target="n14">
<data key="d10">
<y:PolyLineEdge>
<y:Path sx="0.0" sy="0.0" tx="0.0" ty="0.0"/>
<y:Path sx="24.75" sy="0.0" tx="0.0" ty="0.0"/>
<y:LineStyle color="#000000" type="line" width="1.0"/>
<y:Arrows source="diamond" target="crows_foot_one_optional"/>
<y:EdgeLabel alignment="center" configuration="AutoFlippingLabel" distance="2.0" fontFamily="Helvetica" fontSize="12" fontStyle="plain" hasBackgroundColor="false" hasLineColor="false" height="16.0" horizontalTextPosition="center" iconTextGap="4" modelName="custom" preferredPlacement="anywhere" ratio="0.5" textColor="#000000" verticalTextPosition="bottom" visible="true" width="25.33984375" x="1.04168701171875" xml:space="preserve" y="-22.0">Has<y:LabelModel><y:SmartEdgeLabelModel autoRotationEnabled="false" defaultAngle="0.0" defaultDistance="10.0"/></y:LabelModel><y:ModelParameter><y:SmartEdgeLabelModelParameter angle="6.283185307179586" distance="14.0" distanceToCenter="true" position="left" ratio="-3.919921875" segment="-1"/></y:ModelParameter><y:PreferredPlacementDescriptor angle="0.0" angleOffsetOnRightSide="0" angleReference="absolute" angleRotationOnRightSide="co" distance="-1.0" frozen="true" placement="anywhere" side="anywhere" sideReference="relative_to_edge_flow"/></y:EdgeLabel>
<y:EdgeLabel alignment="center" configuration="AutoFlippingLabel" distance="2.0" fontFamily="Helvetica" fontSize="12" fontStyle="plain" hasBackgroundColor="false" hasLineColor="false" height="16.0" horizontalTextPosition="center" iconTextGap="4" modelName="custom" preferredPlacement="anywhere" ratio="0.5" textColor="#000000" verticalTextPosition="bottom" visible="true" width="25.33984375" x="1.0521240234375" xml:space="preserve" y="-22.0">Has<y:LabelModel><y:SmartEdgeLabelModel autoRotationEnabled="false" defaultAngle="0.0" defaultDistance="10.0"/></y:LabelModel><y:ModelParameter><y:SmartEdgeLabelModelParameter angle="6.283185307179586" distance="14.0" distanceToCenter="true" position="left" ratio="-3.919921875" segment="-1"/></y:ModelParameter><y:PreferredPlacementDescriptor angle="0.0" angleOffsetOnRightSide="0" angleReference="absolute" angleRotationOnRightSide="co" distance="-1.0" frozen="true" placement="anywhere" side="anywhere" sideReference="relative_to_edge_flow"/></y:EdgeLabel>
<y:BendStyle smoothed="false"/>
</y:PolyLineEdge>
</data>
</edge>
<edge id="e13" source="n5" target="n15">
<edge id="e12" source="n5" target="n14">
<data key="d10">
<y:PolyLineEdge>
<y:Path sx="0.0" sy="-11.0" tx="0.0" ty="0.0">
Expand All @@ -417,8 +393,7 @@ Uses<y:LabelModel><y:SmartEdgeLabelModel autoRotationEnabled="false" defaultAngl
</y:PolyLineEdge>
</data>
</edge>
<edge id="e14" source="n3" target="n0">
<data key="d9"/>
<edge id="e13" source="n3" target="n0">
<data key="d10">
<y:PolyLineEdge>
<y:Path sx="0.0" sy="0.0" tx="0.0" ty="0.0">
Expand Down
Binary file modified docs/architecture/assets/rpc-client-erd.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
10 changes: 9 additions & 1 deletion rpc/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,15 @@ all-features = true

[features]
default = []
http-client = [ "async-trait", "futures", "http", "hyper", "tokio/fs", "tokio/macros" ]
http-client = [
"async-trait",
"futures",
"http",
"hyper",
"tokio/fs",
"tokio/macros",
"tracing"
]
secp256k1 = [ "tendermint/secp256k1" ]
websocket-client = [
"async-trait",
Expand Down
18 changes: 8 additions & 10 deletions rpc/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -21,25 +21,23 @@ This crate optionally provides access to different types of RPC client
functionality and different client transports based on which features you
select when using it.

Two features are provided at present:
Two features are provided at present.

* `http-client` - Provides `HttpClient`, which is a basic RPC client that
interacts with remote Tendermint nodes via **JSON-RPC over HTTP**. This
client does not provide `Event` subscription functionality. See the
client does not provide `Event` subscription functionality. See the
[Tendermint RPC] for more details.
* `websocket-client` - Provides `WebSocketClient`, which currently only
provides `Event` subscription functionality over a WebSocket connection. See
the [`/subscribe` endpoint] in the Tendermint RPC for more details. This
client does not yet provide access to the RPC methods provided by the
`Client` trait (this is planned for a future release).
* `websocket-client` - Provides `WebSocketClient`, which provides full
client functionality, including general RPC functionality (such as that
provided by `HttpClient`) as well as `Event` subscription
functionality.

### Mock Clients

Mock clients are included when either of the `http-client` or
`websocket-client` features are enabled to aid in testing. This includes
`MockClient`, which only implements `Client` (no subscription
functionality), and `MockSubscriptionClient`, which helps you simulate
subscriptions to events being generated by a Tendermint node.
`MockClient`, which implements both `Client` and `SubscriptionClient`
traits.

### Related

Expand Down
Loading