Skip to content

Commit

Permalink
Fix service framework (#31)
Browse files Browse the repository at this point in the history
 - Fix deadlock when accessing the ServiceManager parameter in start() method of a Service
 - Known bug: Deadlock still happens when a service accesses itself through the ServiceManager on start
  • Loading branch information
Kitt3120 committed Jan 10, 2024
1 parent 7bad3ad commit 2cd71ac
Show file tree
Hide file tree
Showing 4 changed files with 27 additions and 42 deletions.
6 changes: 3 additions & 3 deletions src/bot.rs
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,7 @@ impl BotBuilder {

pub struct Bot {
pub name: String,
pub service_manager: Arc<RwLock<ServiceManager>>,
pub service_manager: Arc<ServiceManager>,
}

impl Bot {
Expand All @@ -52,15 +52,15 @@ impl Bot {
//TODO: When Rust allows async trait methods to be object-safe, refactor this to use async instead of returning a future
pub fn start(&mut self) -> PinnedBoxedFuture<'_, ()> {
Box::pin(async move {
self.service_manager.write().await.start_services().await;
self.service_manager.start_services().await;
//TODO: Potential for further initialization here, like modules
})
}

//TODO: When Rust allows async trait methods to be object-safe, refactor this to use async instead of returning a future
pub fn stop(&mut self) -> PinnedBoxedFuture<'_, ()> {
Box::pin(async move {
self.service_manager.write().await.stop_services().await;
self.service_manager.stop_services().await;
//TODO: Potential for further deinitialization here, like modules
})
}
Expand Down
6 changes: 2 additions & 4 deletions src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -36,17 +36,15 @@ pub async fn run(mut bot: Bot) {
}
};

let service_manager = bot.service_manager.read().await;
if service_manager.overall_status().await != OverallStatus::Healthy {
let status_tree = service_manager.status_tree().await;
if bot.service_manager.overall_status().await != OverallStatus::Healthy {
let status_tree = bot.service_manager.status_tree().await;

error!("{} is not healthy! Some essential services did not start up successfully. Please check the logs.\nService status tree:\n{}\n{} will exit.",
bot.name,
status_tree,
bot.name);
return;
}
drop(service_manager);

info!("{} is alive", bot.name,);

Expand Down
49 changes: 19 additions & 30 deletions src/service.rs
Original file line number Diff line number Diff line change
Expand Up @@ -17,8 +17,10 @@ use crate::setlock::SetLock;

pub mod discord;

pub type PinnedBoxedFuture<'a, T> = Pin<Box<dyn Future<Output = T> + 'a>>;
pub type BoxedFuture<'a, T> = Box<dyn Future<Output = T> + 'a>;
pub type BoxedFutureResult<'a, T> = BoxedFuture<'a, Result<T, Box<dyn Error + Send + Sync>>>;

pub type PinnedBoxedFuture<'a, T> = Pin<Box<dyn Future<Output = T> + 'a>>;
pub type PinnedBoxedFutureResult<'a, T> =
PinnedBoxedFuture<'a, Result<T, Box<dyn Error + Send + Sync>>>;

Expand All @@ -28,8 +30,8 @@ pub enum Status {
Stopped,
Starting,
Stopping,
FailedStarting(Box<dyn Error + Send + Sync>),
FailedStopping(Box<dyn Error + Send + Sync>),
FailedToStart(Box<dyn Error + Send + Sync>), //TODO: Test out if it'd be better to use a String instead
FailedToStop(Box<dyn Error + Send + Sync>),
RuntimeError(Box<dyn Error + Send + Sync>),
}

Expand All @@ -40,8 +42,8 @@ impl Display for Status {
Status::Stopped => write!(f, "Stopped"),
Status::Starting => write!(f, "Starting"),
Status::Stopping => write!(f, "Stopping"),
Status::FailedStarting(error) => write!(f, "Failed to start: {}", error),
Status::FailedStopping(error) => write!(f, "Failed to stop: {}", error),
Status::FailedToStart(error) => write!(f, "Failed to start: {}", error),
Status::FailedToStop(error) => write!(f, "Failed to stop: {}", error),
Status::RuntimeError(error) => write!(f, "Runtime error: {}", error),
}
}
Expand All @@ -55,8 +57,8 @@ impl PartialEq for Status {
| (Status::Stopped, Status::Stopped)
| (Status::Starting, Status::Starting)
| (Status::Stopping, Status::Stopping)
| (Status::FailedStarting(_), Status::FailedStarting(_))
| (Status::FailedStopping(_), Status::FailedStopping(_))
| (Status::FailedToStart(_), Status::FailedToStart(_))
| (Status::FailedToStop(_), Status::FailedToStop(_))
| (Status::RuntimeError(_), Status::RuntimeError(_))
)
}
Expand Down Expand Up @@ -146,19 +148,13 @@ impl Hash for ServiceInfo {
//TODO: When Rust allows async trait methods to be object-safe, refactor this to use async instead of returning a PinnedBoxedFutureResult
pub trait Service: DowncastSync {
fn info(&self) -> &ServiceInfo;
fn start(
&mut self,
service_manager: Arc<RwLock<ServiceManager>>,
) -> PinnedBoxedFutureResult<'_, ()>;
fn start(&mut self, service_manager: Arc<ServiceManager>) -> PinnedBoxedFutureResult<'_, ()>;
fn stop(&mut self) -> PinnedBoxedFutureResult<'_, ()>;

// Used for downcasting in get_service method of ServiceManager
//fn as_any_arc(&self) -> Arc<dyn Any + Send + Sync>;

fn wrapped_start(
&mut self,
service_manager: Arc<RwLock<ServiceManager>>,
) -> PinnedBoxedFuture<()> {
fn wrapped_start(&mut self, service_manager: Arc<ServiceManager>) -> PinnedBoxedFuture<()> {
Box::pin(async move {
let mut status = self.info().status.write().await;

Expand All @@ -181,7 +177,7 @@ pub trait Service: DowncastSync {
}
Err(error) => {
error!("Failed to start service {}: {}", self.info().name, error);
self.info().set_status(Status::FailedStarting(error)).await;
self.info().set_status(Status::FailedToStart(error)).await;
}
}
})
Expand Down Expand Up @@ -210,7 +206,7 @@ pub trait Service: DowncastSync {
}
Err(error) => {
error!("Failed to stop service {}: {}", self.info().name, error);
self.info().set_status(Status::FailedStopping(error)).await;
self.info().set_status(Status::FailedToStop(error)).await;
}
}
})
Expand Down Expand Up @@ -288,22 +284,15 @@ impl ServiceManagerBuilder {
self
}

pub async fn build(self) -> Arc<RwLock<ServiceManager>> {
pub async fn build(self) -> Arc<ServiceManager> {
let service_manager = ServiceManager {
services: self.services,
arc: RwLock::new(SetLock::new()),
};

let self_arc = Arc::new(RwLock::new(service_manager));
let self_arc = Arc::new(service_manager);

match self_arc
.write()
.await
.arc
.write()
.await
.set(Arc::clone(&self_arc))
{
match self_arc.arc.write().await.set(Arc::clone(&self_arc)) {
Ok(()) => {}
Err(err) => {
panic!(
Expand All @@ -319,7 +308,7 @@ impl ServiceManagerBuilder {

pub struct ServiceManager {
pub services: Vec<Arc<RwLock<dyn Service>>>,
arc: RwLock<SetLock<Arc<RwLock<Self>>>>,
arc: RwLock<SetLock<Arc<Self>>>,
}

impl ServiceManager {
Expand Down Expand Up @@ -418,8 +407,8 @@ impl ServiceManager {
.push_str(&format!(" - {}: {}\n", info.name, status));
}
},
Status::FailedStarting(_)
| Status::FailedStopping(_)
Status::FailedToStart(_)
| Status::FailedToStop(_)
| Status::RuntimeError(_) => match priority {
Priority::Essential => {
failed_essentials.push_str(&format!(" - {}: {}\n", info.name, status));
Expand Down
8 changes: 3 additions & 5 deletions src/service/discord.rs
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ use serenity::{
};
use std::{sync::Arc, time::Duration};
use tokio::{
spawn,
sync::{Mutex, Notify, RwLock},
task::JoinHandle,
time::{sleep, timeout},
Expand Down Expand Up @@ -56,10 +57,7 @@ impl Service for DiscordService {
&self.info
}

fn start(
&mut self,
_service_manager: Arc<RwLock<ServiceManager>>,
) -> PinnedBoxedFutureResult<'_, ()> {
fn start(&mut self, _service_manager: Arc<ServiceManager>) -> PinnedBoxedFutureResult<'_, ()> {
Box::pin(async move {
let framework = StandardFramework::new();
framework.configure(Configuration::new().prefix("!"));
Expand Down Expand Up @@ -101,7 +99,7 @@ impl Service for DiscordService {
}

info!("Connecting to Discord");
let client_handle = tokio::spawn(async move { client.start().await });
let client_handle = spawn(async move { client.start().await });

// This prevents waiting for the timeout if the client fails immediately
// TODO: Optimize this, as it will currently add 1000mqs to the startup time
Expand Down

0 comments on commit 2cd71ac

Please sign in to comment.