185 lines
6.3 KiB
Rust
185 lines
6.3 KiB
Rust
// The 'app' module is shared between the server and the client.
|
|
mod app;
|
|
|
|
// --- SERVER-SIDE CODE --- //
|
|
|
|
#[cfg(feature = "server")]
|
|
mod server {
|
|
use axum::{
|
|
extract::{Path, State},
|
|
http::{Method, StatusCode},
|
|
routing::get,
|
|
Json, Router,
|
|
};
|
|
use deadpool_redis::{Config, Pool, Runtime};
|
|
use redis::{from_redis_value, AsyncCommands, FromRedisValue, Value};
|
|
use std::collections::HashMap;
|
|
use std::env;
|
|
use std::net::SocketAddr;
|
|
use tower_http::cors::{Any, CorsLayer};
|
|
use tower_http::services::ServeDir;
|
|
|
|
// Import the shared application state and data structures
|
|
use crate::app::{QueueStats, TaskDetails, TaskSummary, WorkerDataResponse};
|
|
|
|
const REDIS_TASK_DETAILS_PREFIX: &str = "rhai_task_details:";
|
|
const REDIS_QUEUE_PREFIX: &str = "rhai_tasks:";
|
|
|
|
// The main function to run the server
|
|
pub async fn run() {
|
|
let redis_url = env::var("REDIS_URL").unwrap_or_else(|_| "redis://127.0.0.1/".to_string());
|
|
let cfg = Config::from_url(redis_url);
|
|
let pool = cfg
|
|
.create_pool(Some(Runtime::Tokio1))
|
|
.expect("Failed to create Redis pool");
|
|
|
|
let cors = CorsLayer::new()
|
|
.allow_methods([Method::GET])
|
|
.allow_origin(Any);
|
|
|
|
let app = Router::new()
|
|
.route(
|
|
"/api/worker/:worker_name/tasks_and_stats",
|
|
get(get_worker_data),
|
|
)
|
|
.route("/api/worker/:worker_name/queue_stats", get(get_queue_stats))
|
|
.route("/api/task/:hash", get(get_task_details))
|
|
.nest_service("/", ServeDir::new("dist"))
|
|
.with_state(pool)
|
|
.layer(cors);
|
|
|
|
let addr = SocketAddr::from(([127, 0, 0, 1], 3000));
|
|
println!("Backend server listening on http://{}", addr);
|
|
println!("Serving static files from './dist' directory.");
|
|
|
|
let listener = tokio::net::TcpListener::bind(addr).await.unwrap();
|
|
axum::serve(listener, app).await.unwrap();
|
|
}
|
|
|
|
// --- API Handlers (Live Redis Data) ---
|
|
|
|
async fn get_worker_data(
|
|
State(pool): State<Pool>,
|
|
Path(worker_name): Path<String>,
|
|
) -> Result<Json<WorkerDataResponse>, (StatusCode, String)> {
|
|
let mut conn = pool.get().await.map_err(internal_error)?;
|
|
let queue_key = format!("{}{}", REDIS_QUEUE_PREFIX, worker_name);
|
|
|
|
let task_ids: Vec<String> = conn
|
|
.lrange(&queue_key, 0, -1)
|
|
.await
|
|
.map_err(internal_error)?;
|
|
let mut tasks = Vec::new();
|
|
|
|
for task_id in task_ids {
|
|
let task_key = format!("{}{}", REDIS_TASK_DETAILS_PREFIX, task_id);
|
|
let task_details: redis::Value =
|
|
conn.hgetall(&task_key).await.map_err(internal_error)?;
|
|
if let Ok(summary) = task_summary_from_redis_value(&task_details) {
|
|
tasks.push(summary);
|
|
}
|
|
}
|
|
|
|
let queue_stats = get_queue_stats_internal(&mut conn, &worker_name).await?;
|
|
|
|
Ok(Json(WorkerDataResponse {
|
|
tasks,
|
|
queue_stats: Some(queue_stats),
|
|
}))
|
|
}
|
|
|
|
async fn get_queue_stats(
|
|
State(pool): State<Pool>,
|
|
Path(worker_name): Path<String>,
|
|
) -> Result<Json<QueueStats>, (StatusCode, String)> {
|
|
let mut conn = pool.get().await.map_err(internal_error)?;
|
|
let stats = get_queue_stats_internal(&mut conn, &worker_name).await?;
|
|
Ok(Json(stats))
|
|
}
|
|
|
|
async fn get_task_details(
|
|
State(pool): State<Pool>,
|
|
Path(hash): Path<String>,
|
|
) -> Result<Json<TaskDetails>, (StatusCode, String)> {
|
|
let mut conn = pool.get().await.map_err(internal_error)?;
|
|
let task_key = format!("{}{}", REDIS_TASK_DETAILS_PREFIX, hash);
|
|
let task_details: redis::Value = conn.hgetall(&task_key).await.map_err(internal_error)?;
|
|
let details = task_details_from_redis_value(&task_details).map_err(internal_error)?;
|
|
Ok(Json(details))
|
|
}
|
|
|
|
// --- Internal Helper Functions ---
|
|
|
|
async fn get_queue_stats_internal(
|
|
conn: &mut deadpool_redis::Connection,
|
|
worker_name: &str,
|
|
) -> Result<QueueStats, (StatusCode, String)> {
|
|
let queue_key = format!("{}{}", REDIS_QUEUE_PREFIX, worker_name);
|
|
let size: u32 = conn.llen(&queue_key).await.map_err(internal_error)?;
|
|
let color_code = match size {
|
|
0..=10 => "green",
|
|
11..=50 => "yellow",
|
|
_ => "red",
|
|
}
|
|
.to_string();
|
|
Ok(QueueStats {
|
|
current_size: size,
|
|
color_code,
|
|
})
|
|
}
|
|
|
|
fn internal_error<E: std::error::Error>(err: E) -> (StatusCode, String) {
|
|
(StatusCode::INTERNAL_SERVER_ERROR, err.to_string())
|
|
}
|
|
|
|
fn task_summary_from_redis_value(v: &Value) -> redis::RedisResult<TaskSummary> {
|
|
let map: HashMap<String, String> = from_redis_value(v)?;
|
|
Ok(TaskSummary {
|
|
hash: map.get("hash").cloned().unwrap_or_default(),
|
|
created_at: map
|
|
.get("createdAt")
|
|
.and_then(|s| s.parse().ok())
|
|
.unwrap_or_default(),
|
|
status: map
|
|
.get("status")
|
|
.cloned()
|
|
.unwrap_or_else(|| "Unknown".to_string()),
|
|
})
|
|
}
|
|
|
|
fn task_details_from_redis_value(v: &Value) -> redis::RedisResult<TaskDetails> {
|
|
let map: HashMap<String, String> = from_redis_value(v)?;
|
|
Ok(TaskDetails {
|
|
hash: map.get("hash").cloned().unwrap_or_default(),
|
|
created_at: map
|
|
.get("createdAt")
|
|
.and_then(|s| s.parse().ok())
|
|
.unwrap_or_default(),
|
|
status: map
|
|
.get("status")
|
|
.cloned()
|
|
.unwrap_or_else(|| "Unknown".to_string()),
|
|
script_content: map.get("script").cloned().unwrap_or_default(),
|
|
result: map.get("output").cloned(),
|
|
error: map.get("error").cloned(),
|
|
})
|
|
}
|
|
}
|
|
|
|
// --- MAIN ENTRY POINTS --- //
|
|
|
|
// Main function for the server binary
|
|
#[cfg(feature = "server")]
|
|
#[tokio::main]
|
|
async fn main() {
|
|
server::run().await;
|
|
}
|
|
|
|
// Main function for the WASM client (compiles when 'server' feature is not enabled)
|
|
#[cfg(not(feature = "server"))]
|
|
fn main() {
|
|
wasm_logger::init(wasm_logger::Config::default());
|
|
log::info!("Rhai Worker UI starting...");
|
|
yew::Renderer::<app::App>::new().render();
|
|
}
|