feat: use anyhow, refactor services & coordinator out of main.rs

This commit is contained in:
2025-08-26 15:33:29 -05:00
parent d4c55a3fd8
commit cff672b30a
8 changed files with 234 additions and 184 deletions

45
src/services/bot.rs Normal file
View File

@@ -0,0 +1,45 @@
use super::{Service, ServiceResult};
use serenity::Client;
use std::sync::Arc;
use tracing::{error, warn};
/// Discord bot service implementation
pub struct BotService {
client: Client,
shard_manager: Arc<serenity::gateway::ShardManager>,
}
impl BotService {
pub fn new(client: Client) -> Self {
let shard_manager = client.shard_manager.clone();
Self {
client,
shard_manager,
}
}
}
#[async_trait::async_trait]
impl Service for BotService {
fn name(&self) -> &'static str {
"bot"
}
async fn run(&mut self) -> Result<(), anyhow::Error> {
match self.client.start().await {
Ok(()) => {
warn!(service = "bot", "Stopped early.");
Err(anyhow::anyhow!("bot stopped early"))
}
Err(e) => {
error!(service = "bot", "Error: {e:?}");
Err(e.into())
}
}
}
async fn shutdown(&mut self) -> Result<(), anyhow::Error> {
self.shard_manager.shutdown_all().await;
Ok(())
}
}

42
src/services/dummy.rs Normal file
View File

@@ -0,0 +1,42 @@
use super::Service;
use std::time::Duration;
use tracing::{error, info};
/// Dummy service implementation for demonstration
pub struct DummyService {
name: &'static str,
}
impl DummyService {
pub fn new(name: &'static str) -> Self {
Self { name }
}
}
#[async_trait::async_trait]
impl Service for DummyService {
fn name(&self) -> &'static str {
self.name
}
async fn run(&mut self) -> Result<(), anyhow::Error> {
let mut counter = 0;
loop {
tokio::time::sleep(Duration::from_secs(10)).await;
counter += 1;
info!(service = self.name, "Service heartbeat ({counter})");
// Simulate service failure after 60 seconds for demo
if counter >= 6 {
error!(service = self.name, "Service encountered an error");
return Err(anyhow::anyhow!("Service error"));
}
}
}
async fn shutdown(&mut self) -> Result<(), anyhow::Error> {
// Simulate cleanup work
tokio::time::sleep(Duration::from_millis(6000)).await;
Ok(())
}
}

69
src/services/mod.rs Normal file
View File

@@ -0,0 +1,69 @@
use std::time::Duration;
use tokio::sync::broadcast;
use tracing::{error, info, warn};
pub mod bot;
pub mod dummy;
#[derive(Debug)]
pub enum ServiceResult {
GracefulShutdown,
NormalCompletion,
Error(anyhow::Error),
}
/// Common trait for all services in the application
#[async_trait::async_trait]
pub trait Service: Send + Sync {
/// The name of the service for logging
fn name(&self) -> &'static str;
/// Run the service's main work loop
async fn run(&mut self) -> Result<(), anyhow::Error>;
/// Gracefully shutdown the service
async fn shutdown(&mut self) -> Result<(), anyhow::Error>;
}
/// Generic service runner that handles the lifecycle
pub async fn run_service(
mut service: Box<dyn Service>,
mut shutdown_rx: broadcast::Receiver<()>,
) -> ServiceResult {
let name = service.name();
info!(service = name, "Service started");
let work = async {
match service.run().await {
Ok(()) => {
warn!(service = name, "Service completed unexpectedly");
ServiceResult::NormalCompletion
}
Err(e) => {
error!(service = name, "Service failed: {e}");
ServiceResult::Error(e)
}
}
};
tokio::select! {
result = work => result,
_ = shutdown_rx.recv() => {
info!(service = name, "Shutting down...");
let start_time = std::time::Instant::now();
match service.shutdown().await {
Ok(()) => {
let elapsed = start_time.elapsed();
info!(service = name, "Shutdown completed in {elapsed:.2?}");
ServiceResult::GracefulShutdown
}
Err(e) => {
let elapsed = start_time.elapsed();
error!(service = name, "Shutdown failed after {elapsed:.2?}: {e}");
ServiceResult::Error(e)
}
}
}
}
}