Skip to content

Commit 73a2140

Browse files
committed
Fixed Issue #306
1 parent 95ed60f commit 73a2140

8 files changed

Lines changed: 588 additions & 157 deletions

File tree

indexer/README.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ A high-performance event indexing service for the Stellar Escrow smart contract
1111
- **Event Replay**: Replay historical events for catch-up or analysis
1212
- **Configurable**: Flexible configuration for different networks and contracts
1313
- **Redis Caching**: Read-through caching, invalidation, monitoring, and warming for hot API paths
14+
- **Background Jobs**: Redis-backed queue with priorities, scheduling, retries, and monitoring
1415

1516
## Architecture
1617

indexer/src/event_monitor.rs

Lines changed: 40 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ use crate::config::StellarConfig;
1010
use crate::database::Database;
1111
use crate::error::AppError;
1212
use crate::fraud_service::FraudDetectionService;
13-
use crate::job_queue::{JobQueue, types::{Job, JobType}};
13+
use crate::job_queue::{JobQueue, types::{Job, JobPriority, JobType}};
1414
use crate::models::{Event, WebSocketMessage};
1515
use crate::websocket::WebSocketManager;
1616

@@ -308,22 +308,48 @@ impl EventMonitor {
308308
}
309309
}
310310

311-
// Enqueue background job
312-
let job = Job {
313-
job_type: JobType::Event,
314-
event_id: event.id.to_string(),
315-
trade_id: event
316-
.data
317-
.get("trade_id")
318-
.and_then(|v| v.as_str())
319-
.unwrap_or("unknown")
320-
.to_string(),
321-
payload: event.data.clone(),
311+
let trade_id = event
312+
.data
313+
.get("trade_id")
314+
.map(|value| {
315+
value
316+
.as_str()
317+
.map(|s| s.to_string())
318+
.unwrap_or_else(|| value.to_string())
319+
})
320+
.unwrap_or_else(|| "unknown".to_string());
321+
322+
let event_priority = match event.event_type.as_str() {
323+
"dispute_raised" | "dispute_resolved" => JobPriority::Critical,
324+
"trade_created" | "trade_funded" | "trade_confirmed" => JobPriority::High,
325+
_ => JobPriority::Normal,
322326
};
327+
let event_job = Job::new(
328+
JobType::Event,
329+
event.id.to_string(),
330+
trade_id.clone(),
331+
event.data.clone(),
332+
event_priority,
333+
);
334+
335+
let notification_job = Job::new(
336+
JobType::Notification,
337+
event.id.to_string(),
338+
trade_id,
339+
serde_json::json!({
340+
"event_type": event.event_type,
341+
"timestamp": event.timestamp,
342+
"data": event.data,
343+
}),
344+
JobPriority::High,
345+
);
323346

324347
let mut queue = self.job_queue.lock().await;
325-
if let Err(e) = queue.enqueue(job).await {
326-
error!("Failed to enqueue job for event {}: {}", event.id, e);
348+
if let Err(e) = queue.enqueue(event_job).await {
349+
error!("Failed to enqueue event job for event {}: {}", event.id, e);
350+
}
351+
if let Err(e) = queue.enqueue(notification_job).await {
352+
error!("Failed to enqueue notification job for event {}: {}", event.id, e);
327353
}
328354
}
329355

indexer/src/handlers.rs

Lines changed: 108 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,9 @@ use crate::database::Database;
1616
use crate::error::AppError;
1717
use crate::fraud_service::FraudDetectionService;
1818
use crate::health::HealthState;
19+
use crate::job_queue::types::{Job, JobPriority, JobType};
20+
use crate::job_queue::worker::JobWorker;
21+
use crate::job_queue::JobQueue;
1922
use crate::webhook_service::WebhookService;
2023
use crate::monitoring_service::{MonitoringService, dashboard};
2124
use crate::models::{
@@ -61,6 +64,9 @@ pub async fn api_index() -> Json<serde_json::Value> {
6164
"cache_stats": "GET /cache/stats",
6265
"cache_invalidate":"POST /cache/invalidate",
6366
"cache_warm": "POST /cache/warm",
67+
"jobs_stats": "GET /jobs/stats",
68+
"jobs_enqueue": "POST /jobs/enqueue",
69+
"jobs_schedule": "POST /jobs/schedule",
6470
"fraud_review": "POST /fraud/review",
6571
"notif_prefs_get": "GET /notifications/preferences/:address",
6672
"notif_prefs_put": "PUT /notifications/preferences/:address",
@@ -425,6 +431,8 @@ pub async fn update_fraud_review(
425431
pub struct AppState {
426432
pub database: Arc<Database>,
427433
pub stellar_contract_id: String,
434+
pub job_queue: Arc<tokio::sync::Mutex<JobQueue>>,
435+
pub job_worker: Arc<JobWorker>,
428436
pub ws_manager: Arc<WebSocketManager>,
429437
pub health: HealthState,
430438
pub fraud_service: Arc<FraudDetectionService>,
@@ -645,6 +653,86 @@ pub struct IntegrationLogQuery {
645653
pub limit: Option<i64>,
646654
}
647655

656+
#[derive(Deserialize)]
657+
pub struct EnqueueJobRequest {
658+
pub job_type: String,
659+
pub event_id: Option<String>,
660+
pub trade_id: Option<String>,
661+
pub priority: Option<String>,
662+
pub payload: Option<serde_json::Value>,
663+
pub run_at: Option<i64>,
664+
pub max_attempts: Option<u32>,
665+
}
666+
667+
/// GET /jobs/stats — job queue depths and worker status.
668+
pub async fn get_job_stats(
669+
State(state): State<AppState>,
670+
) -> Result<Json<serde_json::Value>, AppError> {
671+
let snapshot = state.job_worker.snapshot().await?;
672+
Ok(Json(serde_json::to_value(&snapshot).unwrap_or_default()))
673+
}
674+
675+
/// POST /jobs/enqueue — enqueue an immediate background job.
676+
pub async fn enqueue_job(
677+
State(state): State<AppState>,
678+
Json(body): Json<EnqueueJobRequest>,
679+
) -> Result<Json<serde_json::Value>, AppError> {
680+
let job_type = parse_job_type(&body.job_type)?;
681+
let priority = parse_job_priority(body.priority.as_deref())?;
682+
let mut job = Job::new(
683+
job_type,
684+
body.event_id.unwrap_or_else(|| "manual".to_string()),
685+
body.trade_id.unwrap_or_else(|| "manual".to_string()),
686+
body.payload.unwrap_or_default(),
687+
priority,
688+
);
689+
if let Some(max_attempts) = body.max_attempts {
690+
job = job.with_max_attempts(max_attempts.max(1));
691+
}
692+
693+
let mut queue = state.job_queue.lock().await;
694+
queue.enqueue(job.clone()).await?;
695+
696+
Ok(Json(json!({
697+
"status": "queued",
698+
"job_id": job.id,
699+
"priority": job.priority.as_str(),
700+
})))
701+
}
702+
703+
/// POST /jobs/schedule — enqueue a delayed background job.
704+
pub async fn schedule_job(
705+
State(state): State<AppState>,
706+
Json(body): Json<EnqueueJobRequest>,
707+
) -> Result<Json<serde_json::Value>, AppError> {
708+
let run_at = body
709+
.run_at
710+
.ok_or_else(|| AppError::BadRequest("run_at is required".to_string()))?;
711+
let job_type = parse_job_type(&body.job_type)?;
712+
let priority = parse_job_priority(body.priority.as_deref())?;
713+
let mut job = Job::new(
714+
job_type,
715+
body.event_id.unwrap_or_else(|| "scheduled".to_string()),
716+
body.trade_id.unwrap_or_else(|| "scheduled".to_string()),
717+
body.payload.unwrap_or_default(),
718+
priority,
719+
)
720+
.scheduled_at(run_at);
721+
if let Some(max_attempts) = body.max_attempts {
722+
job = job.with_max_attempts(max_attempts.max(1));
723+
}
724+
725+
let mut queue = state.job_queue.lock().await;
726+
queue.enqueue_at(job.clone(), run_at).await?;
727+
728+
Ok(Json(json!({
729+
"status": "scheduled",
730+
"job_id": job.id,
731+
"run_at": run_at,
732+
"priority": job.priority.as_str(),
733+
})))
734+
}
735+
648736
// =============================================================================
649737
// Performance Monitoring Handlers
650738
// =============================================================================
@@ -993,3 +1081,23 @@ pub async fn get_prometheus_metrics(
9931081
.body(body)
9941082
.unwrap()
9951083
}
1084+
1085+
fn parse_job_type(value: &str) -> Result<JobType, AppError> {
1086+
match value {
1087+
"event" => Ok(JobType::Event),
1088+
"notification" => Ok(JobType::Notification),
1089+
"cache_warm" => Ok(JobType::CacheWarm),
1090+
"compliance" => Ok(JobType::Compliance),
1091+
_ => Err(AppError::BadRequest(format!("unsupported job_type '{}'", value))),
1092+
}
1093+
}
1094+
1095+
fn parse_job_priority(value: Option<&str>) -> Result<JobPriority, AppError> {
1096+
match value.unwrap_or("normal") {
1097+
"critical" => Ok(JobPriority::Critical),
1098+
"high" => Ok(JobPriority::High),
1099+
"normal" => Ok(JobPriority::Normal),
1100+
"low" => Ok(JobPriority::Low),
1101+
other => Err(AppError::BadRequest(format!("unsupported priority '{}'", other))),
1102+
}
1103+
}

0 commit comments

Comments
 (0)