-
Notifications
You must be signed in to change notification settings - Fork 5
Add stop behaviour to GenServer #22
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
7b0e33d
138890f
116241d
702e4df
2bed000
ce77a36
2dc4cc1
53f5a3e
ab9f69c
2d6e8db
b5ef429
668b41f
38d6848
412f291
f37178e
e652070
b257707
2adb0fd
633f197
729dc1b
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
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 |
|---|---|---|
| @@ -1,28 +1,34 @@ | ||
| //! GenServer trait and structs to create an abstraction similar to Erlang gen_server. | ||
| //! See examples/name_server for a usage example. | ||
| use futures::future::FutureExt as _; | ||
| use spawned_rt::tasks::{self as rt, mpsc, oneshot}; | ||
| use spawned_rt::tasks::{self as rt, mpsc, oneshot, CancellationToken}; | ||
| use std::{fmt::Debug, future::Future, panic::AssertUnwindSafe}; | ||
|
|
||
| use crate::error::GenServerError; | ||
|
|
||
| #[derive(Debug)] | ||
| pub struct GenServerHandle<G: GenServer + 'static> { | ||
| pub tx: mpsc::Sender<GenServerInMsg<G>>, | ||
| /// Cancellation token to stop the GenServer | ||
| cancellation_token: CancellationToken, | ||
| } | ||
|
|
||
| impl<G: GenServer> Clone for GenServerHandle<G> { | ||
| fn clone(&self) -> Self { | ||
| Self { | ||
| tx: self.tx.clone(), | ||
| cancellation_token: self.cancellation_token.clone(), | ||
| } | ||
| } | ||
| } | ||
|
|
||
| impl<G: GenServer> GenServerHandle<G> { | ||
| pub(crate) fn new(initial_state: G::State) -> Self { | ||
| let (tx, mut rx) = mpsc::channel::<GenServerInMsg<G>>(); | ||
| let handle = GenServerHandle { tx }; | ||
| let cancellation_token = CancellationToken::new(); | ||
| let handle = GenServerHandle { | ||
| tx, | ||
| cancellation_token, | ||
| }; | ||
| let mut gen_server: G = GenServer::new(); | ||
| let handle_clone = handle.clone(); | ||
| // Ignore the JoinHandle for now. Maybe we'll use it in the future | ||
|
|
@@ -40,7 +46,11 @@ impl<G: GenServer> GenServerHandle<G> { | |
|
|
||
| pub(crate) fn new_blocking(initial_state: G::State) -> Self { | ||
| let (tx, mut rx) = mpsc::channel::<GenServerInMsg<G>>(); | ||
| let handle = GenServerHandle { tx }; | ||
| let cancellation_token = CancellationToken::new(); | ||
| let handle = GenServerHandle { | ||
| tx, | ||
| cancellation_token, | ||
| }; | ||
| let mut gen_server: G = GenServer::new(); | ||
| let handle_clone = handle.clone(); | ||
| // Ignore the JoinHandle for now. Maybe we'll use it in the future | ||
|
|
@@ -79,6 +89,10 @@ impl<G: GenServer> GenServerHandle<G> { | |
| .send(GenServerInMsg::Cast { message }) | ||
| .map_err(|_error| GenServerError::Server) | ||
| } | ||
|
|
||
| pub fn cancellation_token(&self) -> CancellationToken { | ||
| self.cancellation_token.clone() | ||
| } | ||
| } | ||
|
|
||
| pub enum GenServerInMsg<G: GenServer> { | ||
|
|
@@ -168,12 +182,16 @@ where | |
| async { | ||
| loop { | ||
| let (new_state, cont) = self.receive(handle, rx, state).await?; | ||
| state = new_state; | ||
| if !cont { | ||
| break; | ||
| } | ||
| state = new_state; | ||
| } | ||
| tracing::trace!("Stopping GenServer"); | ||
| handle.cancellation_token().cancel(); | ||
| if let Err(err) = self.teardown(handle, state).await { | ||
| tracing::error!("Error during teardown: {err:?}"); | ||
| } | ||
| Ok(()) | ||
| } | ||
| } | ||
|
|
@@ -269,6 +287,17 @@ where | |
| ) -> impl Future<Output = CastResponse<Self>> + Send { | ||
| async { CastResponse::Unused } | ||
| } | ||
|
|
||
| /// Teardown function. It's called after the stop message is received. | ||
| /// It can be overrided on implementations in case final steps are required, | ||
| /// like closing streams, stopping timers, etc. | ||
| fn teardown( | ||
|
Collaborator
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. 👏 |
||
| &mut self, | ||
| _handle: &GenServerHandle<Self>, | ||
| _state: Self::State, | ||
| ) -> impl Future<Output = Result<(), Self::Error>> + Send { | ||
| async { Ok(()) } | ||
| } | ||
| } | ||
|
|
||
| #[cfg(test)] | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -22,9 +22,16 @@ where | |
| { | ||
| let cancellation_token = CancellationToken::new(); | ||
| let cloned_token = cancellation_token.clone(); | ||
| let gen_server_cancellation_token = handle.cancellation_token(); | ||
| let join_handle = rt::spawn(async move { | ||
| let _ = select( | ||
| // Timer action is ignored if it was either cancelled or the associated GenServer is no longer running. | ||
| let cancel_conditions = select( | ||
|
Collaborator
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. Ah, nice! I like having both, the individual cancellation_token, and the gen_server associated one |
||
| Box::pin(cloned_token.cancelled()), | ||
| Box::pin(gen_server_cancellation_token.cancelled()), | ||
| ); | ||
|
|
||
| let _ = select( | ||
| cancel_conditions, | ||
| Box::pin(async { | ||
| rt::sleep(period).await; | ||
| let _ = handle.cast(message.clone()).await; | ||
|
|
@@ -49,10 +56,17 @@ where | |
| { | ||
| let cancellation_token = CancellationToken::new(); | ||
| let cloned_token = cancellation_token.clone(); | ||
| let gen_server_cancellation_token = handle.cancellation_token(); | ||
| let join_handle = rt::spawn(async move { | ||
| loop { | ||
| let result = select( | ||
| // Timer action is ignored if it was either cancelled or the associated GenServer is no longer running. | ||
| let cancel_conditions = select( | ||
| Box::pin(cloned_token.cancelled()), | ||
| Box::pin(gen_server_cancellation_token.cancelled()), | ||
| ); | ||
|
|
||
| let result = select( | ||
| Box::pin(cancel_conditions), | ||
| Box::pin(async { | ||
| rt::sleep(period).await; | ||
| let _ = handle.cast(message.clone()).await; | ||
|
|
||
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.
I think here is the place to
handle.cancellation_token().cancel()as we shouldn't rely on the implementers to remember cancelling it (and if possible, it shouldn't be public)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.
Good point, considering the
cancellation_tokenis part of everyGenServer, it would be proper that by default all of them call thecancelfunction. I have applied this changes inside ofteardown:https://github.com/lambdaclass/spawned/pull/22/files#diff-932cb8f7a40a4c5e58805fa19b32b0d0fef1afee68560831b86a61d59e0553dcR300
Furthermore, thinking it a bit more, it may be better to have it outside of
teardownto prevent the user from shooting himself in the foot, as I can't think of any case where it wouldn't want to call thecancelmethod.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.
Yes, it should be standard
GenServerbehavior: Stopping it will cancel it's cancellation token always. And we cannot put it in the defaultteardown()implementation as it can be overridden.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.
Done!