use crate::error::BrokerError;
use crate::models::{Task, TaskStatus};
use base64::Engine;
use chrono::{DateTime, Utc};
use redis::aio::MultiplexedConnection;
use redis::AsyncCommands;
use serde_json::Value;
use std::collections::HashMap;
const MAX_TASK_RESULTS: usize = 100;
const MAX_QUEUE_MESSAGES: usize = 100;
const MAX_PENDING_TASKS: usize = 20;
pub struct TaskParser;
impl TaskParser {
pub async fn parse_tasks(connection: &MultiplexedConnection) -> Result<Vec<Task>, BrokerError> {
let mut conn = connection.clone();
let mut tasks = Vec::new();
let task_names = Self::get_queue_messages(&mut conn).await?;
Self::parse_task_metadata(&mut conn, &mut tasks, &task_names).await?;
Self::add_pending_tasks_from_queues(&mut conn, &mut tasks).await?;
Ok(tasks)
}
async fn get_queue_messages(
conn: &mut MultiplexedConnection,
) -> Result<HashMap<String, String>, BrokerError> {
let mut task_names: HashMap<String, String> = HashMap::new();
let queue_names = vec!["celery", "default", "priority"];
for queue_name in &queue_names {
match conn.llen::<_, u64>(queue_name).await {
Ok(queue_length) if queue_length > 0 => {
match conn
.lrange::<_, Vec<String>>(queue_name, 0, MAX_QUEUE_MESSAGES as isize)
.await
{
Ok(messages) => {
for message in &messages {
if let Ok(task_message) = serde_json::from_str::<Value>(message) {
if let Some(headers) = task_message.get("headers") {
if let (Some(task_id), Some(task_name)) = (
headers.get("id").and_then(|id| id.as_str()),
headers.get("task").and_then(|task| task.as_str()),
) {
task_names
.insert(task_id.to_string(), task_name.to_string());
}
}
}
}
}
Err(_) => {
continue;
}
}
}
_ => {
continue;
}
}
}
Ok(task_names)
}
async fn parse_task_metadata(
conn: &mut MultiplexedConnection,
tasks: &mut Vec<Task>,
task_names: &HashMap<String, String>,
) -> Result<(), BrokerError> {
let task_keys: Vec<String> = conn.keys("celery-task-meta-*").await.map_err(|e| {
BrokerError::OperationError(format!("Failed to get task metadata keys: {e}"))
})?;
for key in task_keys.iter().take(MAX_TASK_RESULTS) {
match conn.get::<_, String>(key).await {
Ok(data) => {
match serde_json::from_str::<Value>(&data) {
Ok(task_data) => {
match Self::extract_task_from_metadata(key, &task_data, task_names) {
Ok(task) => tasks.push(task),
Err(_) => {
continue;
}
}
}
Err(_) => {
continue;
}
}
}
Err(_) => {
continue;
}
}
}
Ok(())
}
fn extract_task_from_metadata(
key: &str,
task_data: &Value,
task_names: &HashMap<String, String>,
) -> Result<Task, BrokerError> {
let task_id = key
.strip_prefix("celery-task-meta-")
.unwrap_or("unknown")
.to_string();
let timestamp = Self::parse_timestamp(task_data);
let task_name = Self::get_task_name(&task_id, task_data, task_names);
let status = Self::parse_task_status(task_data);
Ok(Task {
id: task_id,
name: task_name,
args: task_data
.get("args")
.map(|a| a.to_string())
.unwrap_or_else(|| "[]".to_string()),
kwargs: task_data
.get("kwargs")
.map(|k| k.to_string())
.unwrap_or_else(|| "{}".to_string()),
status,
worker: None, timestamp,
result: task_data.get("result").and_then(|r| {
if r.is_null() {
None
} else {
Some(r.to_string())
}
}),
traceback: task_data
.get("traceback")
.and_then(|t| t.as_str())
.map(|s| s.to_string()),
})
}
fn parse_timestamp(task_data: &Value) -> DateTime<Utc> {
if let Some(date_done) = task_data.get("date_done").and_then(|d| d.as_str()) {
date_done
.parse::<DateTime<Utc>>()
.unwrap_or_else(|_| Utc::now())
} else {
Utc::now()
}
}
fn get_task_name(
task_id: &str,
task_data: &Value,
task_names: &HashMap<String, String>,
) -> String {
task_names
.get(task_id)
.cloned()
.or_else(|| {
task_data
.get("task")
.and_then(|t| t.as_str())
.map(|s| s.to_string())
})
.unwrap_or_else(|| "unknown".to_string())
}
fn parse_task_status(task_data: &Value) -> TaskStatus {
match task_data.get("status").and_then(|s| s.as_str()) {
Some("SUCCESS") => TaskStatus::Success,
Some("FAILURE") => TaskStatus::Failure,
Some("PENDING") => TaskStatus::Pending,
Some("RETRY") => TaskStatus::Retry,
Some("REVOKED") => TaskStatus::Revoked,
Some("STARTED") => TaskStatus::Active,
_ => TaskStatus::Pending,
}
}
async fn add_pending_tasks_from_queues(
conn: &mut MultiplexedConnection,
tasks: &mut Vec<Task>,
) -> Result<(), BrokerError> {
let queue_names = vec!["celery", "default", "priority"];
for queue_name in &queue_names {
match conn.llen::<_, u64>(queue_name).await {
Ok(queue_length) if queue_length > 0 => {
match conn
.lrange::<_, Vec<String>>(queue_name, 0, MAX_PENDING_TASKS as isize)
.await
{
Ok(messages) => {
for message in &messages {
if let Ok(task_message) = serde_json::from_str::<Value>(message) {
match Self::parse_task_message(&task_message, tasks) {
Ok(Some(task)) => tasks.push(task),
Ok(None) => continue, Err(_) => continue, }
}
}
}
Err(_) => {
continue;
}
}
}
_ => {
continue;
}
}
}
Ok(())
}
fn parse_task_message(
task_message: &Value,
existing_tasks: &[Task],
) -> Result<Option<Task>, BrokerError> {
if let Some(headers) = task_message.get("headers") {
if let (Some(task_id), Some(task_name)) = (
headers.get("id").and_then(|id| id.as_str()),
headers.get("task").and_then(|task| task.as_str()),
) {
if !existing_tasks.iter().any(|t| t.id == task_id) {
let (args, kwargs) = Self::decode_task_body(task_message);
return Ok(Some(Task {
id: task_id.to_string(),
name: task_name.to_string(),
args,
kwargs,
status: TaskStatus::Pending,
worker: None,
timestamp: Utc::now(),
result: None,
traceback: None,
}));
}
}
}
Ok(None)
}
fn decode_task_body(task_message: &Value) -> (String, String) {
if let Some(body) = task_message.get("body").and_then(|b| b.as_str()) {
if let Ok(decoded) = base64::engine::general_purpose::STANDARD.decode(body) {
if let Ok(body_str) = String::from_utf8(decoded) {
if let Ok(body_json) = serde_json::from_str::<Value>(&body_str) {
let args = body_json
.get(0)
.map(|a| a.to_string())
.unwrap_or_else(|| "[]".to_string());
let kwargs = body_json
.get(1)
.map(|k| k.to_string())
.unwrap_or_else(|| "{}".to_string());
return (args, kwargs);
}
}
}
}
("[]".to_string(), "{}".to_string())
}
}