Skip to content

Commit ada113c

Browse files
author
Leonidas Loucas
committed
feat: Add rudimentary request_reply api and example
1 parent 9416beb commit ada113c

6 files changed

Lines changed: 312 additions & 2 deletions

File tree

rust/lib/srpc/client/Cargo.toml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,10 +8,12 @@ name = "srpc_client"
88
crate-type = ["cdylib", "rlib"]
99

1010
[dependencies]
11+
async-trait = "0.1"
1112
bytes = "1"
1213
futures = "0.3"
1314
tokio = { version = "1", features = ["full"] }
1415
openssl = "0.10"
16+
serde = {version = "1", features = ["derive"]}
1517
serde_json = "1"
1618
tokio-openssl = "0.6"
1719
tokio-util = { version = "0.7", features = ["codec"] }
Lines changed: 70 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,70 @@
1+
"""
2+
This example demonstrates how to use the srpc_client Python bindings.
3+
4+
To run this example:
5+
1. Build the Rust library: maturin build --features python
6+
2. Install the wheel: pip install target/wheels/srpc_client-*.whl
7+
3. Run this script: python examples/python_client_example.py
8+
"""
9+
10+
import asyncio
11+
import json
12+
import os
13+
from srpc_client import SrpcClientConfig
14+
15+
16+
async def main():
17+
print("Starting client..")
18+
19+
# Create a new ClientConfig instance
20+
client = SrpcClientConfig(
21+
os.environ["EXAMPLE_3_SRPC_SERVER_HOST"],
22+
int(os.environ["EXAMPLE_3_SRPC_SERVER_PORT"]),
23+
os.environ["EXAMPLE_3_SRPC_SERVER_ENPOINT"],
24+
os.environ["EXAMPLE_3_SRPC_SERVER_CERT"],
25+
os.environ["EXAMPLE_3_SRPC_SERVER_KEY"],
26+
)
27+
28+
# Connect to the server
29+
client = await client.connect()
30+
print("Connected to server")
31+
32+
message = "Hypervisor.ListVMs\n"
33+
34+
# Send a message
35+
print(f"Sending message: {message}")
36+
await client.send_message(message)
37+
print(f"Sent message: {message}")
38+
39+
# Receive an empty response
40+
print("Waiting for empty string response...")
41+
responses = await client.receive_message(expect_empty=True, should_continue=False)
42+
async for response in responses:
43+
print(f"Received response: {response}")
44+
45+
# Send a JSON message
46+
payload = json.dumps({"IgnoreStateMask": 0, "OwnerGroups": [], "OwnerUsers": [], "Sort": True, "VmTagsToMatch": {}})
47+
print(f"Sending payload: {payload}")
48+
await client.send_json(payload)
49+
print(f"Sent payload: {payload}")
50+
51+
# Receive an empty response
52+
print("Waiting for empty string response for payload...")
53+
responses = await client.receive_message(expect_empty=True, should_continue=False)
54+
async for response in responses:
55+
print(f"Received response: {response}")
56+
57+
# Receive responses
58+
print("Waiting for response...")
59+
responses = await client.receive_json_cb(should_continue=lambda _: False)
60+
async for response in responses:
61+
print(f"Received response: {json.loads(response)}")
62+
63+
# Use RequestReply
64+
print(f"Sending request_reply: {message}")
65+
res = await client.request_reply(message, json.dumps({"IgnoreStateMask": 0, "OwnerGroups": [], "OwnerUsers": [], "Sort": True, "VmTagsToMatch": {}}))
66+
print(f"Sent request_reply: {message}, got reply: {res}")
67+
68+
69+
if __name__ == "__main__":
70+
asyncio.run(main())
Lines changed: 120 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,120 @@
1+
use std::{collections::HashMap, error::Error};
2+
3+
use srpc_client::{ClientConfig, CustomError, ReceiveOptions, SimpleValue};
4+
use tracing::{error, info, level_filters::LevelFilter};
5+
use tracing_subscriber::{layer::SubscriberExt, util::SubscriberInitExt};
6+
7+
#[tokio::main]
8+
async fn main() -> Result<(), Box<dyn std::error::Error>> {
9+
tracing_subscriber::registry()
10+
.with(
11+
tracing_subscriber::EnvFilter::builder()
12+
.with_default_directive(LevelFilter::INFO.into())
13+
.from_env_lossy(),
14+
)
15+
.with(tracing_subscriber::fmt::Layer::default().compact())
16+
.init();
17+
18+
info!("Starting client...");
19+
20+
// Create a new ClientConfig instance
21+
let config = ClientConfig::new(
22+
&std::env::var("EXAMPLE_3_SRPC_SERVER_HOST")?,
23+
std::env::var("EXAMPLE_3_SRPC_SERVER_PORT")?.parse()?,
24+
&std::env::var("EXAMPLE_3_SRPC_SERVER_ENPOINT")?,
25+
&std::env::var("EXAMPLE_3_SRPC_SERVER_CERT")?,
26+
&std::env::var("EXAMPLE_3_SRPC_SERVER_KEY")?,
27+
);
28+
29+
// Connect to the server
30+
let client = config.connect().await?;
31+
info!("Connected to server");
32+
33+
let message = "Hypervisor.ListVMs\n";
34+
35+
// Send a message
36+
info!("Sending message: {:?}", message);
37+
client.send_message(message).await?;
38+
info!("Sent message: {:?}", message);
39+
40+
// Receive an empty response
41+
info!("Waiting for empty string response...");
42+
let mut rx = client
43+
.receive_message(true, |_| false, &ReceiveOptions::default())
44+
.await?;
45+
while let Some(result) = rx.recv().await {
46+
match result {
47+
Ok(response) => info!("Received response: {:?}", response),
48+
Err(e) => error!("Error receiving message: {:?}", e),
49+
}
50+
}
51+
52+
#[derive(Debug, serde::Serialize)]
53+
struct ListVMsRequest {
54+
ignore_state_mask: u32,
55+
owner_groups: Vec<String>,
56+
owner_users: Vec<String>,
57+
sort: bool,
58+
vm_tags_to_match: HashMap<String, String>,
59+
}
60+
61+
#[derive(Debug, serde::Deserialize)]
62+
struct ListVMsResponse {
63+
ip_addresses: Vec<String>,
64+
}
65+
66+
let request = ListVMsRequest {
67+
ignore_state_mask: 0,
68+
owner_groups: vec![],
69+
owner_users: vec![],
70+
sort: false,
71+
vm_tags_to_match: HashMap::new(),
72+
};
73+
74+
// Send a JSON message
75+
info!("Sending payload: {:?}", request);
76+
client.send_json(&serde_json::to_value(&request)?).await?;
77+
info!("Sent payload: {:?}", request);
78+
79+
// Receive an empty response
80+
info!("Waiting for empty string response for payload...");
81+
let mut rx = client
82+
.receive_message(true, |_| false, &ReceiveOptions::default())
83+
.await?;
84+
while let Some(result) = rx.recv().await {
85+
match result {
86+
Ok(response) => info!("Received response: {:?}", response),
87+
Err(e) => error!("Error receiving message: {:?}", e),
88+
}
89+
}
90+
91+
// Receive responses
92+
let mut rx = client
93+
.receive_json(|_| false, &ReceiveOptions::default())
94+
.await?;
95+
while let Some(result) = rx.recv().await {
96+
match result
97+
.and_then(|response| {
98+
serde_json::from_value::<ListVMsResponse>(response)
99+
.map_err(|e| Box::new(CustomError(e.to_string())) as Box<dyn Error + Send>)
100+
})
101+
.map_err(|e| Box::new(CustomError(e.to_string())) as Box<dyn Error>)
102+
{
103+
Ok(response) => info!("Received response: {:?}", response),
104+
Err(e) => error!("Error receiving message: {:?}", e),
105+
}
106+
}
107+
108+
info!("Sending request_reply: {}", message);
109+
let res = client
110+
.request_reply::<SimpleValue>(message, serde_json::to_value(&request)?)
111+
.await
112+
.map_err(|e| Box::new(CustomError(e.to_string())) as Box<dyn Error>)?;
113+
info!(
114+
"Sent request_reply: {}, got reply: {:?}",
115+
message,
116+
serde_json::to_string(&res)?
117+
);
118+
119+
Ok(())
120+
}

rust/lib/srpc/client/src/lib.rs

Lines changed: 96 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
use async_trait::async_trait;
12
use chunk_limiter::ChunkLimiter;
23
use futures::StreamExt;
34
use openssl::ssl::{Ssl, SslConnector, SslMethod, SslVerifyMode};
@@ -20,7 +21,7 @@ mod tests;
2021

2122
// Custom error type
2223
#[derive(Debug)]
23-
struct CustomError(String);
24+
pub struct CustomError(pub String);
2425

2526
impl fmt::Display for CustomError {
2627
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
@@ -82,6 +83,70 @@ where
8283
stream: Arc<Mutex<T>>,
8384
}
8485

86+
#[async_trait]
87+
pub trait RequestReply {
88+
type Request;
89+
type Reply;
90+
91+
async fn request_reply<T>(
92+
client: &ConnectedClient<T>,
93+
payload: Self::Request,
94+
) -> Result<Self::Reply, Box<dyn Error + Send>>
95+
where
96+
T: AsyncRead + AsyncWrite + Unpin + Send + 'static;
97+
}
98+
99+
pub struct SimpleValue;
100+
101+
#[async_trait]
102+
impl RequestReply for SimpleValue {
103+
type Request = serde_json::Value;
104+
type Reply = serde_json::Value;
105+
106+
async fn request_reply<T>(
107+
client: &ConnectedClient<T>,
108+
payload: Self::Request,
109+
) -> Result<Self::Reply, Box<dyn Error + Send>>
110+
where
111+
T: AsyncRead + AsyncWrite + Unpin + Send + 'static,
112+
{
113+
client.send_json_and_check(&payload).await?;
114+
115+
let mut rx = client
116+
.receive_json(|_| false, &ReceiveOptions::default())
117+
.await
118+
.map_err(|e| Box::new(CustomError(e.to_string())) as Box<dyn Error + Send>)?;
119+
let json_value = rx.recv().await.ok_or_else(|| {
120+
Box::new(CustomError("Expected JSON value".to_string())) as Box<dyn Error + Send>
121+
})??;
122+
Ok(json_value)
123+
}
124+
}
125+
126+
pub struct StreamValue;
127+
128+
#[async_trait]
129+
impl RequestReply for StreamValue {
130+
type Request = serde_json::Value;
131+
type Reply = mpsc::Receiver<Result<serde_json::Value, Box<dyn Error + Send>>>;
132+
133+
async fn request_reply<T>(
134+
client: &ConnectedClient<T>,
135+
payload: Self::Request,
136+
) -> Result<Self::Reply, Box<dyn Error + Send>>
137+
where
138+
T: AsyncRead + AsyncWrite + Unpin + Send + 'static,
139+
{
140+
client.send_json_and_check(&payload).await?;
141+
142+
let rx = client
143+
.receive_json(|_| true, &ReceiveOptions::default())
144+
.await
145+
.map_err(|e| Box::new(CustomError(e.to_string())) as Box<dyn Error + Send>)?;
146+
Ok(rx)
147+
}
148+
}
149+
85150
impl ClientConfig {
86151
pub fn new(host: &str, port: u16, path: &str, cert: &str, key: &str) -> Self {
87152
ClientConfig {
@@ -190,6 +255,36 @@ where
190255
}
191256
}
192257

258+
pub async fn request_reply<R>(
259+
&self,
260+
method: &str,
261+
payload: R::Request,
262+
) -> Result<R::Reply, Box<dyn Error + Send>>
263+
where
264+
R: RequestReply,
265+
{
266+
self.send_message(method)
267+
.await
268+
.map_err(|e| Box::new(CustomError(e.to_string())) as Box<dyn Error + Send>)?;
269+
let mut rx = self
270+
.receive_message(true, |_| false, &ReceiveOptions::default())
271+
.await
272+
.map_err(|e| Box::new(CustomError(e.to_string())) as Box<dyn Error + Send>)?;
273+
if rx
274+
.recv()
275+
.await
276+
.ok_or_else(|| {
277+
Box::new(CustomError("Expected response".to_string())) as Box<dyn Error + Send>
278+
})??
279+
.is_empty()
280+
{
281+
} else {
282+
return Err(Box::new(CustomError("Expected empty line".to_string())));
283+
}
284+
285+
R::request_reply(self, payload).await
286+
}
287+
193288
pub async fn send_message(&self, message: &str) -> Result<(), Box<dyn Error>> {
194289
let stream = self.stream.lock().await;
195290
let mut pinned = Pin::new(stream);

rust/lib/srpc/client/src/python_bindings.rs

Lines changed: 22 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
use crate::{ClientConfig, ConnectedClient, ReceiveOptions};
1+
use crate::{ClientConfig, ConnectedClient, ReceiveOptions, SimpleValue};
22
use futures::{Stream, StreamExt};
33
use pyo3::exceptions::{PyRuntimeError, PyStopAsyncIteration};
44
use pyo3::prelude::*;
@@ -277,4 +277,25 @@ impl ConnectedSrpcClient {
277277
}))
278278
})
279279
}
280+
281+
pub fn request_reply<'p>(
282+
&self,
283+
py: Python<'p>,
284+
method: String,
285+
payload: String,
286+
) -> PyResult<Bound<'p, PyAny>> {
287+
let client = self.0.clone();
288+
289+
pyo3_async_runtimes::tokio::future_into_py(py, async move {
290+
let value: Value = serde_json::from_str(&payload)
291+
.map_err(|e| PyRuntimeError::new_err(e.to_string()))?;
292+
let response = client
293+
.lock()
294+
.await
295+
.request_reply::<SimpleValue>(&method, value)
296+
.await
297+
.map_err(|e| PyRuntimeError::new_err(e.to_string()))?;
298+
Ok(response.to_string())
299+
})
300+
}
280301
}

rust/lib/srpc/client/srpc_client.pyi

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
from typing import Callable, List
22

3+
type JsonStr = str
34

45
class SrpcClientConfig:
56
def __init__(self, host: str, port: int, path: str, cert: str, key: str) -> None: ...
@@ -12,3 +13,4 @@ class ConnectedSrpcClient:
1213
async def send_json(self, payload: str) -> None: ...
1314
async def receive_json(self, should_continue: bool) -> List[str]: ...
1415
async def receive_json_cb(self, should_continue: Callable[[str], bool]) -> List[str]: ...
16+
async def request_reply(self, message: str, payload: JsonStr) -> JsonStr: ...

0 commit comments

Comments
 (0)