mirror of
https://old.git.ood.ovh/nehu/tokio-messaging.git
synced 2025-04-28 18:30:03 +02:00
Initial import
This commit is contained in:
parent
fc22033964
commit
f454bb04e4
8 changed files with 429 additions and 0 deletions
2
.gitignore
vendored
Normal file
2
.gitignore
vendored
Normal file
|
@ -0,0 +1,2 @@
|
|||
/target
|
||||
/Cargo.lock
|
9
Cargo.toml
Normal file
9
Cargo.toml
Normal file
|
@ -0,0 +1,9 @@
|
|||
[package]
|
||||
name = "tokio-messaging"
|
||||
version = "0.0.1"
|
||||
edition = "2021"
|
||||
|
||||
[dependencies]
|
||||
lazy_static = "1.4"
|
||||
num-traits = "0.2"
|
||||
tokio = { version = "1.21", features = ["macros", "rt", "rt-multi-thread", "sync", "time"] }
|
44
README.md
Normal file
44
README.md
Normal file
|
@ -0,0 +1,44 @@
|
|||
# tokio-messaging
|
||||
|
||||
A crate which offers non-blocking publish/subscribe functionality using Tokio channels.
|
||||
|
||||
Publishing messages and subscribing to them is done using an `Messaging` instance,
|
||||
which acts as a message broker.
|
||||
|
||||
In order to create a message broker, start by defining the structure of messages and their data (payload).
|
||||
The types should implement `Message` and `MessageData` respectively.
|
||||
|
||||
```rust
|
||||
enum MyMessage
|
||||
{
|
||||
Greeting,
|
||||
Request
|
||||
}
|
||||
|
||||
impl Message for MyMessage {}
|
||||
|
||||
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
|
||||
struct MyPayload(&'static str);
|
||||
|
||||
impl MessageData for MyPayload {}
|
||||
```
|
||||
|
||||
Next, create the message broker instance. Usually, you'll have a single, long-living instance.
|
||||
```rust
|
||||
lazy_static! {
|
||||
static ref INSTANCE: Messaging<MyMessage, MyPayload> = { Messaging::new() };
|
||||
}
|
||||
|
||||
pub fn messaging() -> &'static Messaging<MyMessage, MyPayload> { &INSTANCE }
|
||||
```
|
||||
|
||||
Publish messages using the `dispatch()` function and subscribe to them using the `on()` function.
|
||||
```rust
|
||||
// Subscribe to messages
|
||||
tokio::spawn(messaging().on(MyMessage::Request, |data: MyPayload| {
|
||||
assert_eq!(data.0, "Here's a request!");
|
||||
}));
|
||||
|
||||
// Publish a message
|
||||
messaging().dispatch(MyMessage::Request, MyPayload("Here's a request!"));
|
||||
```
|
104
src/lib.rs
Normal file
104
src/lib.rs
Normal file
|
@ -0,0 +1,104 @@
|
|||
/*!
|
||||
A crate which offers non-blocking publish/subscribe functionality using Tokio channels.
|
||||
|
||||
Publishing messages and subscribing to them is done using an `Messaging` instance,
|
||||
which acts as a message broker.
|
||||
|
||||
In order to create a message broker, start by defining the structure of messages and their data (payload).
|
||||
The types should implement `Message` and `MessageData` respectively.
|
||||
|
||||
```
|
||||
# use tokio_messaging::*;
|
||||
#
|
||||
#[derive(Clone, Copy, Debug, Hash, PartialEq, Eq)]
|
||||
enum MyMessage
|
||||
{
|
||||
Greeting,
|
||||
Request
|
||||
}
|
||||
|
||||
impl Message for MyMessage {}
|
||||
|
||||
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
|
||||
struct MyPayload(&'static str);
|
||||
|
||||
impl MessageData for MyPayload {}
|
||||
```
|
||||
|
||||
Next, create the message broker instance. Usually, you'll have a single, long-living instance.
|
||||
```
|
||||
# use lazy_static::lazy_static;
|
||||
# use tokio_messaging::*;
|
||||
#
|
||||
# #[derive(Clone, Copy, Debug, Hash, PartialEq, Eq)]
|
||||
# enum MyMessage
|
||||
# {
|
||||
# Greeting,
|
||||
# Request
|
||||
# }
|
||||
#
|
||||
# impl Message for MyMessage {}
|
||||
#
|
||||
# #[derive(Clone, Copy, Debug, PartialEq, Eq)]
|
||||
# struct MyPayload(&'static str);
|
||||
#
|
||||
# impl MessageData for MyPayload {}
|
||||
#
|
||||
lazy_static! {
|
||||
static ref INSTANCE: Messaging<MyMessage, MyPayload> = { Messaging::new() };
|
||||
}
|
||||
|
||||
pub fn messaging() -> &'static Messaging<MyMessage, MyPayload> { &INSTANCE }
|
||||
```
|
||||
|
||||
Publish messages using the `dispatch()` function and subscribe to them using the `on()` function.
|
||||
```
|
||||
# use lazy_static::lazy_static;
|
||||
# use tokio_messaging::*;
|
||||
#
|
||||
# #[derive(Clone, Copy, Debug, Hash, PartialEq, Eq)]
|
||||
# enum MyMessage
|
||||
# {
|
||||
# Greeting,
|
||||
# Request
|
||||
# }
|
||||
#
|
||||
# impl Message for MyMessage {}
|
||||
#
|
||||
# #[derive(Clone, Copy, Debug, PartialEq, Eq)]
|
||||
# struct MyPayload(&'static str);
|
||||
#
|
||||
# impl MessageData for MyPayload {}
|
||||
#
|
||||
# lazy_static! {
|
||||
# static ref INSTANCE: Messaging<MyMessage, MyPayload> = { Messaging::new() };
|
||||
# }
|
||||
#
|
||||
# pub fn messaging() -> &'static Messaging<MyMessage, MyPayload> { &INSTANCE }
|
||||
#
|
||||
# let mut rt = tokio::runtime::Runtime::new().unwrap();
|
||||
# rt.block_on(async {
|
||||
#
|
||||
# use std::sync::Arc;
|
||||
# use std::sync::Mutex;
|
||||
# use std::time::Duration;
|
||||
#
|
||||
// Subscribe to messages
|
||||
tokio::spawn(messaging().on(MyMessage::Request, |data: MyPayload| {
|
||||
assert_eq!(data.0, "Here's a request!");
|
||||
}));
|
||||
|
||||
// Publish a message
|
||||
messaging().dispatch(MyMessage::Request, MyPayload("Here's a request!"));
|
||||
#
|
||||
# });
|
||||
```
|
||||
*/
|
||||
mod message_data;
|
||||
pub use message_data::*;
|
||||
|
||||
mod message;
|
||||
pub use message::*;
|
||||
|
||||
mod messaging;
|
||||
pub use messaging::*;
|
5
src/message.rs
Normal file
5
src/message.rs
Normal file
|
@ -0,0 +1,5 @@
|
|||
use std::{fmt::Debug, hash::Hash};
|
||||
|
||||
pub trait Message : Copy + Debug + Eq + Hash
|
||||
{
|
||||
}
|
4
src/message_data.rs
Normal file
4
src/message_data.rs
Normal file
|
@ -0,0 +1,4 @@
|
|||
use std::fmt::Debug;
|
||||
|
||||
pub trait MessageData: Clone + Debug + Send
|
||||
{}
|
85
src/messaging.rs
Normal file
85
src/messaging.rs
Normal file
|
@ -0,0 +1,85 @@
|
|||
use std::{collections::HashMap, future::Future};
|
||||
use std::sync::Arc;
|
||||
use std::sync::RwLock;
|
||||
|
||||
use tokio::sync::broadcast;
|
||||
|
||||
use crate::{Message, MessageData};
|
||||
|
||||
/// The message broker which allows publishing and subscribing to messages.
|
||||
pub struct Messaging<M: Message, D: MessageData>
|
||||
{
|
||||
channels: RwLock<HashMap<M, Arc<(broadcast::Sender<D>, broadcast::Receiver<D>)>>>
|
||||
}
|
||||
|
||||
impl<M: Message, D: MessageData + 'static> Messaging<M, D>
|
||||
{
|
||||
/// Create a new instance of the messaging broker.
|
||||
pub fn new() -> Self
|
||||
{
|
||||
return Messaging {
|
||||
channels: RwLock::new(HashMap::new())
|
||||
};
|
||||
}
|
||||
|
||||
fn get_channel(&self, message: M) -> Arc<(broadcast::Sender<D>, broadcast::Receiver<D>)>
|
||||
{
|
||||
let mut channels = self.channels.write().unwrap();
|
||||
|
||||
let channel = channels.entry(message).or_insert_with(|| {
|
||||
return Arc::new(broadcast::channel::<D>(16));
|
||||
});
|
||||
|
||||
return channel.clone();
|
||||
}
|
||||
|
||||
/// Dispatch a message to all listeners.
|
||||
///
|
||||
/// The data may get cloned in the process.
|
||||
pub fn dispatch(&self, message: M, data: D)
|
||||
{
|
||||
let tx = &self.get_channel(message).0;
|
||||
tx.send(data.clone()).unwrap();
|
||||
}
|
||||
|
||||
/// Listen for a message from any source and invokes the specified callback for each recieved
|
||||
/// message.
|
||||
pub async fn on<F>(&self, message: M, mut callback: F)
|
||||
where F: FnMut(D) -> () + Send + 'static
|
||||
{
|
||||
let tx = &self.get_channel(message).0;
|
||||
let mut receiver = tx.subscribe();
|
||||
|
||||
loop {
|
||||
if let Ok(data) = receiver.recv().await {
|
||||
callback(data.clone());
|
||||
}
|
||||
else {
|
||||
// TODO: a better handling (at least a warning) might be necessary.
|
||||
// Err can happen if the receiver lagged behind.
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Listen for a message from any source and invokes the specified callback for each recieved
|
||||
/// message.
|
||||
pub async fn on_async<F, R>(&self, message: M, mut callback: F)
|
||||
where F: FnMut(D) -> R + Send + 'static,
|
||||
R: Future<Output = ()>
|
||||
{
|
||||
let mut receiver = self.get_channel(message).0.subscribe();
|
||||
|
||||
loop {
|
||||
if let Ok(data) = receiver.recv().await {
|
||||
//println!("Message received: {:?}", message);
|
||||
callback(data).await;
|
||||
}
|
||||
else {
|
||||
// TODO: a better handling (at least a warning) might be necessary.
|
||||
// Err can happen if the receiver lagged behind.
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
176
tests/tokio_messaging.rs
Normal file
176
tests/tokio_messaging.rs
Normal file
|
@ -0,0 +1,176 @@
|
|||
extern crate tokio_messaging;
|
||||
|
||||
use std::sync::Arc;
|
||||
use std::sync::Mutex;
|
||||
use std::time::Duration;
|
||||
|
||||
use tokio::time::timeout;
|
||||
|
||||
use tokio_messaging::*;
|
||||
|
||||
#[derive(Clone, Copy, Debug, Hash, PartialEq, Eq)]
|
||||
enum TestMessage
|
||||
{
|
||||
Greeting,
|
||||
Request
|
||||
}
|
||||
|
||||
impl Message for TestMessage {}
|
||||
|
||||
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
|
||||
struct TestData(&'static str);
|
||||
|
||||
impl MessageData for TestData {}
|
||||
|
||||
fn messaging() -> &'static mut Messaging<TestMessage, TestData>
|
||||
{
|
||||
static mut INSTANCE: Option<Messaging<TestMessage, TestData>> = None;
|
||||
|
||||
unsafe {
|
||||
if let None = INSTANCE {
|
||||
INSTANCE = Some(Messaging::new());
|
||||
}
|
||||
|
||||
return INSTANCE.as_mut().unwrap();
|
||||
}
|
||||
}
|
||||
|
||||
/// A single sender sends a message while a single receiver is listening for that message.
|
||||
/// Ensure the receiver receives the message.
|
||||
#[tokio::test]
|
||||
async fn spsc()
|
||||
{
|
||||
let sender_handle = tokio::spawn(async move {
|
||||
// Wait a bit until the receiver has started listening
|
||||
tokio::time::sleep(Duration::from_millis(500)).await;
|
||||
|
||||
messaging().dispatch(TestMessage::Request, TestData("Hello from sender"));
|
||||
});
|
||||
|
||||
let receiver_handle = tokio::spawn(async move {
|
||||
let result: Arc<Mutex<Option<TestData>>> = Arc::new(Mutex::new(None));
|
||||
let result_to_be_filled = result.clone();
|
||||
let task = messaging().on(TestMessage::Request, move |data: TestData| {
|
||||
result_to_be_filled.lock().unwrap().replace(data);
|
||||
});
|
||||
|
||||
// Wait a bit until the sender has sent and the callback has processed the information, then
|
||||
// interrupt the listening after 1s
|
||||
timeout(Duration::from_millis(1000), task).await.expect_err("The listening task is expected to time out.");
|
||||
|
||||
return result.lock().unwrap().clone();
|
||||
});
|
||||
|
||||
let (_sender_result, receiver_result) = tokio::join!(sender_handle, receiver_handle);
|
||||
let receiver_result = receiver_result.unwrap();
|
||||
|
||||
assert_eq!(receiver_result.unwrap().0, "Hello from sender");
|
||||
}
|
||||
|
||||
/// A single sender sends a message while a multiple receivers are listening for that message.
|
||||
/// Ensure each receiver receives the message.
|
||||
#[tokio::test]
|
||||
async fn spmc()
|
||||
{
|
||||
let sender_handle = tokio::spawn(async move {
|
||||
// Wait a bit until the receiver has started listening
|
||||
tokio::time::sleep(Duration::from_millis(500)).await;
|
||||
|
||||
messaging().dispatch(TestMessage::Request, TestData("Hello from sender"));
|
||||
});
|
||||
|
||||
let receiver = || async {
|
||||
let result: Arc<Mutex<Option<TestData>>> = Arc::new(Mutex::new(None));
|
||||
let result_to_be_filled = result.clone();
|
||||
let task = messaging().on(TestMessage::Request, move |data: TestData| {
|
||||
result_to_be_filled.lock().unwrap().replace(data);
|
||||
});
|
||||
|
||||
// Wait a bit until the sender has sent and the callback has processed the information, then
|
||||
// interrupt the listening after 1s
|
||||
timeout(Duration::from_millis(1000), task).await.expect_err("The listening task is expected to time out.");
|
||||
|
||||
return result.lock().unwrap().clone();
|
||||
};
|
||||
let receiver_handle_a = tokio::spawn(receiver());
|
||||
let receiver_handle_b = tokio::spawn(receiver());
|
||||
let receiver_handle_c = tokio::spawn(receiver());
|
||||
|
||||
let (_, receiver_a_result, receiver_b_result, receiver_c_result) = tokio::join!(
|
||||
sender_handle,
|
||||
receiver_handle_a,
|
||||
receiver_handle_b,
|
||||
receiver_handle_c);
|
||||
let receiver_a_result = receiver_a_result.unwrap();
|
||||
let receiver_b_result = receiver_b_result.unwrap();
|
||||
let receiver_c_result = receiver_c_result.unwrap();
|
||||
|
||||
assert_eq!(receiver_a_result.unwrap().0, "Hello from sender");
|
||||
assert_eq!(receiver_b_result.unwrap().0, "Hello from sender");
|
||||
assert_eq!(receiver_c_result.unwrap().0, "Hello from sender");
|
||||
}
|
||||
|
||||
/// Multiple senders send a message while a single receiver is listening for that message.
|
||||
/// Ensure the receiver receives all the messages.
|
||||
#[tokio::test]
|
||||
async fn mpsc()
|
||||
{
|
||||
let sender = || async {
|
||||
// Wait a bit until the receiver has started listening
|
||||
tokio::time::sleep(Duration::from_millis(500)).await;
|
||||
|
||||
messaging().dispatch(TestMessage::Request, TestData("Hello from sender"));
|
||||
};
|
||||
let sender_handle_a = tokio::spawn(sender());
|
||||
let sender_handle_b = tokio::spawn(sender());
|
||||
let sender_handle_c = tokio::spawn(sender());
|
||||
|
||||
let receiver_handle = tokio::spawn(async move {
|
||||
let result: Arc<Mutex<u32>> = Arc::new(Mutex::new(0));
|
||||
let result_to_be_filled = result.clone();
|
||||
let task = messaging().on(TestMessage::Request, move |_data: TestData| {
|
||||
*result_to_be_filled.lock().unwrap() += 1;
|
||||
});
|
||||
|
||||
// Wait a bit until the sender has sent and the callback has processed the information, then
|
||||
// interrupt the listening after 1s
|
||||
timeout(Duration::from_millis(1000), task).await.expect_err("The listening task is expected to time out.");
|
||||
|
||||
return result.lock().unwrap().clone();
|
||||
});
|
||||
|
||||
let (_, _, _, receiver_result) = tokio::join!(
|
||||
sender_handle_a,
|
||||
sender_handle_b,
|
||||
sender_handle_c,
|
||||
receiver_handle);
|
||||
let received_messages = receiver_result.unwrap();
|
||||
|
||||
assert_eq!(received_messages, 3);
|
||||
}
|
||||
|
||||
/// Send a message and listen for another one. Ensure the listener does not receive the sent message.
|
||||
#[tokio::test]
|
||||
async fn message_isolation()
|
||||
{
|
||||
let sender_handle = tokio::spawn(async move {
|
||||
// Wait a bit until the receiver has started listening
|
||||
tokio::time::sleep(Duration::from_millis(500)).await;
|
||||
|
||||
messaging().dispatch(TestMessage::Request, TestData("Hello from sender"));
|
||||
});
|
||||
|
||||
let receiver_handle = tokio::spawn(async move {
|
||||
let task = messaging().on(TestMessage::Greeting, move |_data: TestData| {
|
||||
panic!("Received data; this shouldn't happen.");
|
||||
});
|
||||
|
||||
// Wait a bit until the sender has sent and the callback has processed the information, then
|
||||
// interrupt the listening after 1s
|
||||
timeout(Duration::from_millis(1000), task).await.expect_err("The listening task is expected to time out.");
|
||||
});
|
||||
|
||||
let results = tokio::join!(sender_handle, receiver_handle);
|
||||
results.0.unwrap();
|
||||
results.1.unwrap();
|
||||
}
|
Loading…
Add table
Reference in a new issue