Crates.io | client-handle |
lib.rs | client-handle |
version | 0.2.0 |
source | src |
created_at | 2023-01-14 12:31:44.321884 |
updated_at | 2023-01-20 13:49:09.071668 |
description | A macro to generate client handles when using multithreaded / asynchronous code |
homepage | |
repository | https://github.com/stedmeister/client-handle.git |
max_upload_size | |
id | 758814 |
size | 23,947 |
A common pattern with writting multithreaded / asynchronous code is to allow a thread / task to own a resource and to send messages through a channel to access it. e.g. From the tokio redis example: https://tokio.rs/tokio/tutorial/channels.
The pattern is along the lines of:
rx
Receiver.tx
SendersTo provide an ergonomic handle, I also often end up wrapper the tx
Sender and
duplicating all of the client functions. As shown below, this results in
a lot of boiler plate.
// Generate the message enum
enum Command {
Get {
reponse: oneshot::Sender<String>,
key: String,
}
}
// Create a channel and
// Spawn a receiver task
let (tx, mut rx) = mpsc::channel(32);
tokio::spawn(async move {
while let Some(cmd) = rx.recv().await {
use Command::*;
match cmd {
Get { reponse, key } => {
let value = get_value(&key).await;
let _ = response.send(value);
}
}
}
// Send messages to the channel using an ergonic client
struct Handle {
tx: mpsc::Sender<Command>,
}
impl Handle {
async fn get(&self, key: &String) {
let (resp_tx, resp_rx) = oneshot::channel();
let cmd = Command::Get {
key: key.to_string(),
resp: resp_tx,
};
// Send the GET request
tx.send(cmd).await.unwrap();
// Await the response
let res = resp_rx.await;
println!("GOT = {:?}", res);
}
}
The boiler plate in question is the duplication in:
It should be possible to provide only one of the above parts code and derive the
others. This is where client-handle
comes in as it will derive the mesage
format based on a trait that the receiving code has to adere to.
In short, the code above could be replaced with the following:
use client_handle::async_tokio_handle;
#[async_tokio_handle]
trait KvCommand {
fn get(&self, key: String) -> String {
self.get_value(&key)
}
}
And it can be used as follows:
// create a struct for the trait
struct KvReceiver { /* data owned by the receiver */ };
impl KvCommand for KvReceiver {
// Nothing to do here as the trait has default implemenations
}
#[tokio::main]
async fn main() {
let receiver = KvReceiver;
let handle = receiver.to_async_handle();
let result = handle.get("foo".to_string()).await;
}
async_tokio_handle
macro.There are other examples in the code. For the full details of the code
generated, please see the unit tests in the client-handle-core
crate.
It was chosen to place the macro on the trait for the following reasons:
Decorating the enum would have involved having users create "magic strings" for return values.
Using a trait allows for tools like automock
to be used for testing
Please see the notes file for details on resources used to create this proc macro.