feat: Mise en place Framework Scheduler/Api with Actix

This commit is contained in:
Max batleforc 2025-05-28 00:24:06 +02:00
parent a03cb86f7a
commit 1bd8b07f47
No known key found for this signature in database
GPG Key ID: 25D243AB4B6AC9E7
12 changed files with 633 additions and 10 deletions

212
Cargo.lock generated
View File

@ -19,6 +19,21 @@ dependencies = [
"tracing",
]
[[package]]
name = "actix-cors"
version = "0.7.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "daa239b93927be1ff123eebada5a3ff23e89f0124ccb8609234e5103d5a5ae6d"
dependencies = [
"actix-utils",
"actix-web",
"derive_more",
"futures-util",
"log",
"once_cell",
"smallvec",
]
[[package]]
name = "actix-http"
version = "3.11.0"
@ -31,12 +46,15 @@ dependencies = [
"actix-utils",
"base64 0.22.1",
"bitflags 2.9.1",
"brotli",
"bytes",
"bytestring",
"derive_more",
"encoding_rs",
"flate2",
"foldhash",
"futures-core",
"h2 0.3.26",
"http 0.2.12",
"httparse",
"httpdate",
@ -52,6 +70,7 @@ dependencies = [
"tokio",
"tokio-util",
"tracing",
"zstd",
]
[[package]]
@ -73,6 +92,7 @@ dependencies = [
"bytestring",
"cfg-if",
"http 0.2.12",
"regex",
"regex-lite",
"serde",
"tracing",
@ -143,6 +163,7 @@ dependencies = [
"bytes",
"bytestring",
"cfg-if",
"cookie",
"derive_more",
"encoding_rs",
"foldhash",
@ -155,6 +176,7 @@ dependencies = [
"mime",
"once_cell",
"pin-project-lite",
"regex",
"regex-lite",
"serde",
"serde_json",
@ -227,6 +249,21 @@ dependencies = [
"memchr",
]
[[package]]
name = "alloc-no-stdlib"
version = "2.0.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "cc7bb162ec39d46ab1ca8c77bf72e890535becd1751bb45f64c597edb4c8c6b3"
[[package]]
name = "alloc-stdlib"
version = "0.2.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "94fb8275041c72129eb51b7d0322c29b8387a0386127718b096429201a5d6ece"
dependencies = [
"alloc-no-stdlib",
]
[[package]]
name = "android-tzdata"
version = "0.1.1"
@ -248,6 +285,24 @@ version = "1.0.98"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e16d2d3311acee920a9eb8d33b8cbc1787ce4a264e85f964c2404b969bdcd487"
[[package]]
name = "api"
version = "0.1.0"
dependencies = [
"actix-cors",
"actix-web",
"clickhouse_pool",
"config",
"database",
"poise",
"tokio",
"tracing",
"tracing-actix-web",
"utoipa",
"utoipa-actix-web",
"utoipa-scalar",
]
[[package]]
name = "arrayvec"
version = "0.7.6"
@ -412,6 +467,27 @@ dependencies = [
"tracing",
]
[[package]]
name = "brotli"
version = "8.0.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9991eea70ea4f293524138648e41ee89b0b2b12ddef3b255effa43c8056e0e0d"
dependencies = [
"alloc-no-stdlib",
"alloc-stdlib",
"brotli-decompressor",
]
[[package]]
name = "brotli-decompressor"
version = "5.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "874bb8112abecc98cbd6d81ea4fa7e94fb9449648c93cc89aa40c81c24d7de03"
dependencies = [
"alloc-no-stdlib",
"alloc-stdlib",
]
[[package]]
name = "bstr"
version = "1.12.0"
@ -491,6 +567,8 @@ version = "1.2.23"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5f4ac86a9e5bc1e2b3449ab9d7d3a6a405e3d1bb28d7b9be8614f55846ae3766"
dependencies = [
"jobserver",
"libc",
"shlex",
]
@ -525,11 +603,12 @@ checksum = "93a719913643003b84bd13022b4b7e703c09342cd03b679c4641c7d2e50dc34d"
name = "cli"
version = "0.1.0"
dependencies = [
"api",
"bot",
"config",
"cron_scheduler",
"database",
"tokio",
"tokio-cron-scheduler",
"tool_tracing",
"tracing",
]
@ -605,6 +684,17 @@ dependencies = [
"tracing",
]
[[package]]
name = "cookie"
version = "0.16.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e859cd57d0710d9e06c381b550c06e76992472a8c6d527aecd2fc673dcc231fb"
dependencies = [
"percent-encoding",
"time",
"version_check",
]
[[package]]
name = "core-foundation"
version = "0.9.4"
@ -639,6 +729,20 @@ dependencies = [
"cfg-if",
]
[[package]]
name = "cron_scheduler"
version = "0.1.0"
dependencies = [
"chrono",
"config",
"database",
"poise",
"tokio",
"tokio-cron-scheduler",
"tracing",
"uuid",
]
[[package]]
name = "croner"
version = "2.1.0"
@ -1478,6 +1582,7 @@ checksum = "cea70ddb795996207ad57735b50c5982d8844f38ba9ee5f1aedcfb708a2aa11e"
dependencies = [
"equivalent",
"hashbrown 0.15.3",
"serde",
]
[[package]]
@ -1501,6 +1606,16 @@ version = "1.0.15"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4a5f13b858c8d314ee3e8f639011f7ccefe71f97f96e50151fb991f267928e2c"
[[package]]
name = "jobserver"
version = "0.1.33"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "38f262f097c174adebe41eb73d66ae9c06b2844fb0da69969647bbddd9b0538a"
dependencies = [
"getrandom 0.3.3",
"libc",
]
[[package]]
name = "js-sys"
version = "0.3.77"
@ -1657,6 +1772,12 @@ dependencies = [
"windows-sys 0.52.0",
]
[[package]]
name = "mutually_exclusive_features"
version = "0.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e94e1e6445d314f972ff7395df2de295fe51b71821694f0b0e1e79c4f12c8577"
[[package]]
name = "native-tls"
version = "0.2.14"
@ -3212,6 +3333,21 @@ dependencies = [
"tracing-core",
]
[[package]]
name = "tracing-actix-web"
version = "0.7.18"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2340b7722695166c7fc9b3e3cd1166e7c74fedb9075b8f0c74d3822d2e41caf5"
dependencies = [
"actix-web",
"mutually_exclusive_features",
"opentelemetry",
"pin-project",
"tracing",
"tracing-opentelemetry",
"uuid",
]
[[package]]
name = "tracing-attributes"
version = "0.1.28"
@ -3444,6 +3580,52 @@ version = "1.0.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b6c140620e7ffbb22c2dee59cafe6084a59b5ffc27a8859a5f0d494b5d52b6be"
[[package]]
name = "utoipa"
version = "5.3.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "435c6f69ef38c9017b4b4eea965dfb91e71e53d869e896db40d1cf2441dd75c0"
dependencies = [
"indexmap 2.9.0",
"serde",
"serde_json",
"utoipa-gen",
]
[[package]]
name = "utoipa-actix-web"
version = "0.1.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b7eda9c23c05af0fb812f6a177514047331dac4851a2c8e9c4b895d6d826967f"
dependencies = [
"actix-service",
"actix-web",
"utoipa",
]
[[package]]
name = "utoipa-gen"
version = "5.3.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a77d306bc75294fd52f3e99b13ece67c02c1a2789190a6f31d32f736624326f7"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.101",
]
[[package]]
name = "utoipa-scalar"
version = "0.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "59559e1509172f6b26c1cdbc7247c4ddd1ac6560fe94b584f81ee489b141f719"
dependencies = [
"actix-web",
"serde",
"serde_json",
"utoipa",
]
[[package]]
name = "uuid"
version = "1.16.0"
@ -4129,3 +4311,31 @@ dependencies = [
"quote",
"syn 2.0.101",
]
[[package]]
name = "zstd"
version = "0.13.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e91ee311a569c327171651566e07972200e76fcfe2242a4fa446149a3881c08a"
dependencies = [
"zstd-safe",
]
[[package]]
name = "zstd-safe"
version = "7.2.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8f49c4d5f0abb602a93fb8736af2a4f4dd9512e36f7f570d66e65ff867ed3b9d"
dependencies = [
"zstd-sys",
]
[[package]]
name = "zstd-sys"
version = "2.0.15+zstd.1.5.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "eb81183ddd97d0c74cedf1d50d85c8d08c1b8b68ee863bdee9e706eedba1a237"
dependencies = [
"cc",
"pkg-config",
]

View File

@ -8,6 +8,8 @@ members = [
'libs/database',
'libs/bot',
'libs/config',
'libs/cron_scheduler',
'libs/api',
]
[workspace.dependencies]

View File

@ -9,11 +9,9 @@ tokio = { workspace = true }
tracing = { workspace = true }
database = { path = "../../libs/database" }
tool_tracing = { path = "../../libs/tool_tracing" }
tokio-cron-scheduler = { version = "0.14", features = [
"tracing-subscriber",
"signal",
] }
config = { path = "../../libs/config" }
bot = { path = "../../libs/bot" }
cron_scheduler = { path = "../../libs/cron_scheduler" }
api = { path = "../../libs/api" }
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html

View File

@ -1,11 +1,16 @@
use std::time::Duration;
use api::init_api;
use bot::start_bot;
use config::parse_local_config;
use cron_scheduler::ScheduleJob;
use database::{create_manager_and_init, create_pool_manager};
use tokio::{sync::oneshot, time::sleep};
use tool_tracing::init::init_tracing;
use tracing::{error, info};
#[tokio::main]
async fn main() {
async fn main() -> Result<(), ()> {
println!(include_str!("banner.art"));
let config = parse_local_config();
init_tracing(config.tracing.clone(), config.bot_name.clone());
@ -19,10 +24,30 @@ async fn main() {
}
Err(e) => {
error!("Failed to create database manager: {}", e);
return;
return Err(());
}
};
info!("Database manager initialized successfully");
let mut cron_scheduler = match ScheduleJob::start_cron_scheduler().await {
Ok(scheduler) => {
info!("Cron scheduler started successfully");
scheduler
}
Err(_) => {
error!("Failed to start cron scheduler");
return Err(());
}
};
let (tx_bot, rx_bot) = oneshot::channel();
let http = start_bot(config.clone(), manager.clone(), rx_bot).await;
start_bot(config, manager).await;
let _ = init_api(config, manager, http).await;
tx_bot.send(()).unwrap_or_else(|_| {
error!("Failed to send shutdown signal to bot");
});
cron_scheduler.stop_cron_scheduler().await;
sleep(Duration::from_secs(2)).await;
//process::exit(1);
Ok(())
}

22
libs/api/Cargo.toml Normal file
View File

@ -0,0 +1,22 @@
[package]
name = "api"
version = "0.1.0"
edition = "2021"
[dependencies]
tokio = { workspace = true }
tracing = { workspace = true }
poise = { workspace = true }
utoipa-actix-web = "0.1"
actix-web = "4"
actix-cors = "0.7"
utoipa-scalar = { version = "0.3", features = ["actix-web"] }
utoipa = "5"
tracing-actix-web = { version = "0.7", features = ["opentelemetry_0_29"] }
config = { path = "../../libs/config" }
database = { path = "../../libs/database" }
clickhouse_pool = { path = "../../libs/clickhouse_pool" }
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html

43
libs/api/project.json Normal file
View File

@ -0,0 +1,43 @@
{
"name": "api",
"$schema": "../../node_modules/nx/schemas/project-schema.json",
"projectType": "library",
"sourceRoot": "libs/api/src",
"targets": {
"build": {
"executor": "@monodon/rust:check",
"outputs": [
"{options.target-dir}"
],
"options": {
"target-dir": "dist/target/api"
}
},
"test": {
"cache": true,
"executor": "@monodon/rust:test",
"outputs": [
"{options.target-dir}"
],
"options": {
"target-dir": "dist/target/api"
},
"configurations": {
"production": {
"release": true
}
}
},
"lint": {
"cache": true,
"executor": "@monodon/rust:lint",
"outputs": [
"{options.target-dir}"
],
"options": {
"target-dir": "dist/target/api"
}
}
},
"tags": []
}

25
libs/api/src/apidocs.rs Normal file
View File

@ -0,0 +1,25 @@
use utoipa::OpenApi;
#[derive(OpenApi)]
#[openapi(
info(
title = "Bot API",
description = "API documentation for the Bot application",
version = "1.0.0"
),
tags(
( name = "Bot",
description = "Bot related endpoints"
),
( name = "User",
description = "User related endpoints"
),
( name = "Admin",
description = "Admin related endpoints"
),
( name = "Cron",
description = "Cron job related endpoints"
),
)
)]
pub struct ApiDocs;

63
libs/api/src/lib.rs Normal file
View File

@ -0,0 +1,63 @@
use actix_cors::Cors;
use actix_web::{dev::Service, http::header, App, HttpServer};
use apidocs::ApiDocs;
use clickhouse_pool::pool_manager::PoolManager;
use config::Config;
use poise::serenity_prelude::Http;
use std::{net::Ipv4Addr, sync::Arc};
use tracing::{error, info};
use tracing_actix_web::{RequestId, TracingLogger};
use utoipa::OpenApi;
use utoipa_actix_web::AppExt;
use utoipa_scalar::{Scalar, Servable as ScalarServable};
pub mod apidocs;
pub async fn init_api(config: Config, pool: Arc<PoolManager>, http: Arc<Http>) -> Result<(), ()> {
let port = config.port.clone();
HttpServer::new(move || {
let cors = Cors::default()
.allow_any_origin()
.allow_any_method()
.allow_any_header()
.max_age(3600);
App::new()
.wrap(cors)
.wrap_fn(|mut req, srv| {
let request_id_asc = req.extract::<RequestId>();
let fut = srv.call(req);
async move {
let mut res = fut.await?;
let request_id: RequestId = request_id_asc.await.unwrap();
let request_id_str = format!("{}", request_id);
let headers = res.headers_mut();
headers.insert(
header::HeaderName::from_static("x-request-id"),
header::HeaderValue::from_str(request_id_str.as_str()).unwrap(),
);
Ok(res)
}
})
.wrap(TracingLogger::default())
.into_utoipa_app()
.openapi(ApiDocs::openapi())
.app_data(actix_web::web::Data::new(pool.clone()))
.app_data(actix_web::web::Data::new(http.clone()))
.app_data(actix_web::web::Data::new(config.clone()))
.openapi_service(|api| Scalar::with_url("/api/docs", api))
.into_app()
})
.bind((Ipv4Addr::UNSPECIFIED, port))
.map_err(|e| {
error!("Failed to bind API server: {}", e);
()
})?
.run()
.await
.map_err(|e| {
eprintln!("Failed to start API server: {}", e);
()
})?;
info!("Api server stopped");
Ok(())
}

View File

@ -3,6 +3,7 @@ use std::sync::Arc;
use clickhouse_pool::pool_manager::PoolManager;
use event::event_handler;
use poise::serenity_prelude::{GatewayIntents, Http};
use tokio::sync::oneshot;
use tracing::info;
use trivia::trivia;
use utility::{age::age, help::help, server::servers};
@ -23,7 +24,11 @@ pub struct Data {
pub type Error = Box<dyn std::error::Error + Send + Sync>;
pub type Context<'a> = poise::Context<'a, Data, Error>;
pub async fn start_bot(config: Config, datalake_config: Arc<PoolManager>) -> Arc<Http> {
pub async fn start_bot(
config: Config,
datalake_config: Arc<PoolManager>,
rx: oneshot::Receiver<()>,
) -> Arc<Http> {
let intents = GatewayIntents::GUILD_MESSAGES
| GatewayIntents::DIRECT_MESSAGES
| GatewayIntents::MESSAGE_CONTENT
@ -69,6 +74,27 @@ pub async fn start_bot(config: Config, datalake_config: Arc<PoolManager>) -> Arc
}
};
let http = client.http.clone();
client.start().await.unwrap();
let shard_manager = client.shard_manager.clone();
tokio::spawn(async move {
match rx.await {
Ok(_) => {
tracing::info!("Received shutdown signal");
shard_manager.shutdown_all().await;
tracing::info!("Shutting down bot");
}
Err(_) => {
tracing::info!("Channel dropped signal");
shard_manager.shutdown_all().await;
tracing::info!("Shutting down bot");
}
}
});
tokio::spawn(async move {
info!("Bot is running...");
if let Err(why) = client.start_autosharded().await {
tracing::error!("Client error: {why:?}");
}
info!("Bot is stopped...");
});
http
}

View File

@ -0,0 +1,19 @@
[package]
name = "cron_scheduler"
version = "0.1.0"
edition = "2021"
[dependencies]
tokio = { workspace = true }
poise = { workspace = true }
tracing = { workspace = true }
uuid = { workspace = true }
chrono = { workspace = true }
database = { path = "../../libs/database" }
config = { path = "../../libs/config" }
tokio-cron-scheduler = { version = "0.14", features = [
"tracing-subscriber",
"signal",
] }
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html

View File

@ -0,0 +1,43 @@
{
"name": "cron_scheduler",
"$schema": "../../node_modules/nx/schemas/project-schema.json",
"projectType": "library",
"sourceRoot": "libs/cron_scheduler/src",
"targets": {
"build": {
"executor": "@monodon/rust:check",
"outputs": [
"{options.target-dir}"
],
"options": {
"target-dir": "dist/target/cron_scheduler"
}
},
"test": {
"cache": true,
"executor": "@monodon/rust:test",
"outputs": [
"{options.target-dir}"
],
"options": {
"target-dir": "dist/target/cron_scheduler"
},
"configurations": {
"production": {
"release": true
}
}
},
"lint": {
"cache": true,
"executor": "@monodon/rust:lint",
"outputs": [
"{options.target-dir}"
],
"options": {
"target-dir": "dist/target/cron_scheduler"
}
}
},
"tags": []
}

View File

@ -0,0 +1,147 @@
use poise::serenity_prelude::Http;
use std::{
collections::HashMap,
fmt::{self, Display},
sync::Arc,
};
use tokio::sync::RwLock;
use tokio_cron_scheduler::JobScheduler;
use tracing::{error, info, instrument};
use uuid::Uuid;
#[derive(Debug)]
pub enum StopScheduleJob {
JobNotFound,
RemoveFailed,
}
impl Display for StopScheduleJob {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "{:?}", self)
}
}
pub struct ScheduleJob {
pub job_id: Arc<RwLock<HashMap<(u64, u64), Uuid>>>,
pub scheduler: JobScheduler,
}
impl Clone for ScheduleJob {
fn clone(&self) -> Self {
ScheduleJob {
job_id: self.job_id.clone(),
scheduler: self.scheduler.clone(),
}
}
}
impl ScheduleJob {
#[instrument(level = "info")]
pub async fn start_cron_scheduler() -> Result<Self, ()> {
let scheduler = JobScheduler::new().await;
let mut future_self = match scheduler {
Ok(scheduler) => ScheduleJob {
job_id: Default::default(),
scheduler,
},
Err(e) => {
error!("Error starting cron scheduler: {:?}", e);
return Err(());
}
};
future_self.scheduler.set_shutdown_handler(Box::new(|| {
Box::pin(async {
info!("Cron scheduler stopped");
})
}));
future_self.scheduler.shutdown_on_ctrl_c();
match future_self.scheduler.start().await {
Ok(_) => {
info!("Cron scheduler started");
Ok(future_self)
}
Err(e) => {
error!("Error starting cron scheduler: {:?}", e);
Err(())
}
}
}
#[instrument(skip(self, _http), level = "info")]
pub async fn load_all_trivial_cron_job(&mut self, _http: &Http) -> Result<(), bool> {
// Load all trivial jobs
Ok(())
}
#[instrument(skip(self), level = "info")]
pub async fn stop_cron_scheduler(&mut self) -> &mut Self {
let job_id = self.job_id.write().await;
for (server_id, channel_id) in job_id.keys() {
match self
.scheduler
.remove(job_id.get(&(*server_id, *channel_id)).unwrap())
.await
{
Ok(_) => {
info!("Cron job removed");
}
Err(e) => {
error!("Error removing cron job: {:?}", e);
}
}
}
drop(job_id);
match self.scheduler.shutdown().await {
Ok(_) => {
info!("Cron scheduler stopped");
self
}
Err(e) => {
error!("Error stopping cron scheduler: {:?}", e);
self
}
}
}
#[instrument(skip(self, http), level = "info")]
pub async fn add_trivial_cron_job(
&mut self,
server_id: u64,
channel_id: u64,
cron_expression: String,
http: &Http,
) -> Result<Uuid, ()> {
let _http = Arc::new(Http::new(http.token()));
// Create a new job with the provided cron expression
Err(())
}
#[instrument(skip(self), level = "info")]
pub async fn stop_scheduled_job(
&mut self,
server_id: u64,
channel_id: u64,
) -> Result<(), StopScheduleJob> {
let remove_job = {
let mut job_id = self.job_id.write().await;
job_id.remove(&(server_id, channel_id))
};
match remove_job {
Some(job_uid) => match self.scheduler.remove(&job_uid).await {
Ok(_) => {
info!("Cron job removed");
Ok(())
}
Err(e) => {
error!("Error removing cron job: {:?}", e);
Err(StopScheduleJob::RemoveFailed)
}
},
None => {
error!("Cron job not found");
Err(StopScheduleJob::JobNotFound)
}
}
}
}