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 12 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
5 changes: 5 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -22,10 +22,15 @@

- `[tendermint/proto-compiler]` Protobuf structs generator now also accepts commit IDs from the Tendermint Go repository ([#660])

### IMPROVEMENTS:

- `[rpc]` The `WebSocketClient` now adds support for all remaining RPC requests
by way of implementing the `Client` trait ([#646])

[#650]: https://github.com/informalsystems/tendermint-rs/issues/650
[#652]: https://github.com/informalsystems/tendermint-rs/pulls/652
[#639]: https://github.com/informalsystems/tendermint-rs/pull/639
[#646]: https://github.com/informalsystems/tendermint-rs/pull/646
[#660]: https://github.com/informalsystems/tendermint-rs/issues/660
[#665]: https://github.com/informalsystems/tendermint-rs/issues/665

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.
4 changes: 2 additions & 2 deletions light-client/src/components/io.rs
Original file line number Diff line number Diff line change
Expand Up @@ -129,7 +129,7 @@ mod prod {
}

fn fetch_signed_header(&self, height: AtHeight) -> Result<TMSignedHeader, IoError> {
let client = self.rpc_client.clone();
let mut client = self.rpc_client.clone();
let res = block_on(self.timeout, async move {
match height {
AtHeight::Highest => client.latest_commit().await,
Expand All @@ -151,7 +151,7 @@ mod prod {
AtHeight::At(height) => height,
};

let client = self.rpc_client.clone();
let mut client = self.rpc_client.clone();
let res = block_on(self.timeout, async move { client.validators(height).await })?;

match res {
Expand Down
2 changes: 1 addition & 1 deletion light-client/src/evidence.rs
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,7 @@ mod prod {
impl EvidenceReporter for ProdEvidenceReporter {
#[pre(self.peer_map.contains_key(&peer))]
fn report(&self, e: Evidence, peer: PeerId) -> Result<Hash, IoError> {
let client = self.rpc_client_for(peer)?;
let mut client = self.rpc_client_for(peer)?;

let res = block_on(
self.timeout,
Expand Down
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
Loading