-
Notifications
You must be signed in to change notification settings - Fork 0
Implement stream listener #20
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
Changes from all commits
da0dacd
c15cd54
cfca334
4e3db7f
de32666
6bfb8a1
7a6f79a
4d49882
5138e09
04222ae
51ecbb2
0a78b64
835bf08
0c24840
541cc1e
2950644
376deda
6f7a305
702e4df
2bed000
ce77a36
2dc4cc1
2638334
bbabca9
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1 +1 @@ | ||
rust 1.85.1 | ||
rust 1.88.0 |
Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,57 @@ | ||
use crate::tasks::{GenServer, GenServerHandle}; | ||
use futures::{future::select, Stream, StreamExt}; | ||
use spawned_rt::tasks::{CancellationToken, JoinHandle}; | ||
|
||
/// Spawns a listener that listens to a stream and sends messages to a GenServer. | ||
/// | ||
/// Items sent through the stream are required to be wrapped in a Result type. | ||
/// | ||
/// This function returns a handle to the spawned task and a cancellation token | ||
/// to stop it. | ||
pub fn spawn_listener<T, F, S, I, E>( | ||
mut handle: GenServerHandle<T>, | ||
message_builder: F, | ||
mut stream: S, | ||
) -> (JoinHandle<()>, CancellationToken) | ||
where | ||
T: GenServer + 'static, | ||
F: Fn(I) -> T::CastMsg + Send + 'static + std::marker::Sync, | ||
I: Send, | ||
E: std::fmt::Debug + Send, | ||
S: Unpin + Send + Stream<Item = Result<I, E>> + 'static, | ||
{ | ||
let cancelation_token = CancellationToken::new(); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I had the idea of creating the There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I agree that it'd be a good idea to have the cancellation token inside of the gen server, considering this change would also requiere a change in the I'll branch from this one and start work on it ASAP. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Yeah, given GenServer is just a Trait, I guess we should put the token in the GenServerHandler, right? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Yes, I've taken an approach of that kind, see the PR here |
||
let cloned_token = cancelation_token.clone(); | ||
let join_handle = spawned_rt::tasks::spawn(async move { | ||
let result = select( | ||
Box::pin(cloned_token.cancelled()), | ||
Box::pin(async { | ||
loop { | ||
match stream.next().await { | ||
Some(Ok(i)) => match handle.cast(message_builder(i)).await { | ||
Ok(_) => tracing::trace!("Message sent successfully"), | ||
Err(e) => { | ||
tracing::error!("Failed to send message: {e:?}"); | ||
break; | ||
} | ||
}, | ||
Some(Err(e)) => { | ||
tracing::trace!("Received Error in msg {e:?}"); | ||
break; | ||
} | ||
None => { | ||
tracing::trace!("Stream finished"); | ||
break; | ||
} | ||
} | ||
} | ||
}), | ||
) | ||
.await; | ||
match result { | ||
futures::future::Either::Left(_) => tracing::trace!("Listener cancelled"), | ||
futures::future::Either::Right(_) => (), // Stream finished or errored out | ||
} | ||
}); | ||
(join_handle, cancelation_token) | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,174 @@ | ||
use std::time::Duration; | ||
|
||
use spawned_rt::tasks::{self as rt, BroadcastStream, ReceiverStream}; | ||
|
||
use crate::tasks::{ | ||
stream::spawn_listener, CallResponse, CastResponse, GenServer, GenServerHandle, | ||
}; | ||
|
||
type SummatoryHandle = GenServerHandle<Summatory>; | ||
|
||
struct Summatory; | ||
|
||
type SummatoryState = u16; | ||
|
||
#[derive(Clone)] | ||
struct UpdateSumatory { | ||
added_value: u16, | ||
} | ||
|
||
impl Summatory { | ||
pub async fn get_value(server: &mut SummatoryHandle) -> Result<SummatoryState, ()> { | ||
server.call(()).await.map_err(|_| ()) | ||
} | ||
} | ||
|
||
impl GenServer for Summatory { | ||
type CallMsg = (); // We only handle one type of call, so there is no need for a specific message type. | ||
type CastMsg = UpdateSumatory; | ||
type OutMsg = SummatoryState; | ||
type State = SummatoryState; | ||
type Error = (); | ||
|
||
fn new() -> Self { | ||
Self | ||
} | ||
|
||
async fn handle_cast( | ||
&mut self, | ||
message: Self::CastMsg, | ||
_handle: &GenServerHandle<Self>, | ||
state: Self::State, | ||
) -> CastResponse<Self> { | ||
let new_state = state + message.added_value; | ||
CastResponse::NoReply(new_state) | ||
} | ||
|
||
async fn handle_call( | ||
&mut self, | ||
_message: Self::CallMsg, | ||
_handle: &SummatoryHandle, | ||
state: Self::State, | ||
) -> CallResponse<Self> { | ||
let current_value = state; | ||
CallResponse::Reply(state, current_value) | ||
} | ||
} | ||
|
||
// In this example, the stream sends u8 values, which are converted to the type | ||
// supported by the GenServer (UpdateSumatory / u16). | ||
fn message_builder(value: u8) -> UpdateSumatory { | ||
UpdateSumatory { | ||
added_value: value as u16, | ||
} | ||
} | ||
|
||
#[test] | ||
pub fn test_sum_numbers_from_stream() { | ||
let runtime = rt::Runtime::new().unwrap(); | ||
runtime.block_on(async move { | ||
let mut summatory_handle = Summatory::start(0); | ||
let stream = tokio_stream::iter(vec![1u8, 2, 3, 4, 5].into_iter().map(Ok::<u8, ()>)); | ||
|
||
spawn_listener(summatory_handle.clone(), message_builder, stream); | ||
|
||
// Wait for 1 second so the whole stream is processed | ||
rt::sleep(Duration::from_secs(1)).await; | ||
|
||
let val = Summatory::get_value(&mut summatory_handle).await.unwrap(); | ||
assert_eq!(val, 15); | ||
}) | ||
} | ||
|
||
#[test] | ||
pub fn test_sum_numbers_from_channel() { | ||
let runtime = rt::Runtime::new().unwrap(); | ||
runtime.block_on(async move { | ||
let mut summatory_handle = Summatory::start(0); | ||
let (tx, rx) = spawned_rt::tasks::mpsc::channel::<Result<u8, ()>>(); | ||
|
||
// Spawn a task to send numbers to the channel | ||
spawned_rt::tasks::spawn(async move { | ||
for i in 1..=5 { | ||
tx.send(Ok(i)).unwrap(); | ||
} | ||
}); | ||
|
||
spawn_listener( | ||
summatory_handle.clone(), | ||
message_builder, | ||
ReceiverStream::new(rx), | ||
); | ||
|
||
// Wait for 1 second so the whole stream is processed | ||
rt::sleep(Duration::from_secs(1)).await; | ||
|
||
let val = Summatory::get_value(&mut summatory_handle).await.unwrap(); | ||
assert_eq!(val, 15); | ||
}) | ||
} | ||
|
||
#[test] | ||
pub fn test_sum_numbers_from_broadcast_channel() { | ||
let runtime = rt::Runtime::new().unwrap(); | ||
runtime.block_on(async move { | ||
let mut summatory_handle = Summatory::start(0); | ||
let (tx, rx) = tokio::sync::broadcast::channel::<u8>(5); | ||
|
||
// Spawn a task to send numbers to the channel | ||
spawned_rt::tasks::spawn(async move { | ||
for i in 1u8..=5 { | ||
tx.send(i).unwrap(); | ||
} | ||
}); | ||
|
||
spawn_listener( | ||
summatory_handle.clone(), | ||
message_builder, | ||
BroadcastStream::new(rx), | ||
); | ||
|
||
// Wait for 1 second so the whole stream is processed | ||
rt::sleep(Duration::from_secs(1)).await; | ||
|
||
let val = Summatory::get_value(&mut summatory_handle).await.unwrap(); | ||
assert_eq!(val, 15); | ||
}) | ||
} | ||
|
||
#[test] | ||
pub fn test_stream_cancellation() { | ||
const RUNNING_TIME: u64 = 1000; | ||
|
||
let runtime = rt::Runtime::new().unwrap(); | ||
runtime.block_on(async move { | ||
let mut summatory_handle = Summatory::start(0); | ||
let (tx, rx) = spawned_rt::tasks::mpsc::channel::<Result<u8, ()>>(); | ||
|
||
// Spawn a task to send numbers to the channel | ||
spawned_rt::tasks::spawn(async move { | ||
for i in 1..=5 { | ||
tx.send(Ok(i)).unwrap(); | ||
rt::sleep(Duration::from_millis(RUNNING_TIME / 4)).await; | ||
} | ||
}); | ||
|
||
let (_handle, cancellation_token) = spawn_listener( | ||
summatory_handle.clone(), | ||
message_builder, | ||
ReceiverStream::new(rx), | ||
); | ||
|
||
// Wait for 1 second so the whole stream is processed | ||
rt::sleep(Duration::from_millis(RUNNING_TIME)).await; | ||
|
||
cancellation_token.cancel(); | ||
|
||
// The reasoning for this assertion is that each message takes a quarter of the total time | ||
// to be processed, so having a stream of 5 messages, the last one won't be processed. | ||
// We could safely assume that it will get to process 4 messages, but in case of any extenal | ||
// slowdown, it could process less. | ||
let val = Summatory::get_value(&mut summatory_handle).await.unwrap(); | ||
assert!(val > 0 && val < 15); | ||
}) | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,17 @@ | ||
use crate::threads::{GenServer, GenServerHandle}; | ||
|
||
use futures::Stream; | ||
|
||
/// Spawns a listener that listens to a stream and sends messages to a GenServer. | ||
/// | ||
/// Items sent through the stream are required to be wrapped in a Result type. | ||
pub fn spawn_listener<T, F, S, I, E>(_handle: GenServerHandle<T>, _message_builder: F, _stream: S) | ||
where | ||
T: GenServer + 'static, | ||
F: Fn(I) -> T::CastMsg + Send + 'static, | ||
I: Send + 'static, | ||
E: std::fmt::Debug + Send + 'static, | ||
S: Unpin + Send + Stream<Item = Result<I, E>> + 'static, | ||
{ | ||
unimplemented!("Unsupported function in threads mode") | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
One hidden goal we have is not to depend on
tokio
in more than one place. This deps should be inrt
crate, and reexported so we can use them inconcurrency
crate(that way, if we ever plan to replace the runtime, we do it in a single place, ideally)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This imports are only used for testing (inside of the streaming tests more specifically). They are meant to show intercompatibility with
tokio
(e.g we receive a broadcast channel and need to make use of it inside ofspawned
).