use std::sync::Arc;
use sqlx::PgPool;
use tracing::warn;
use uuid::Uuid;
use crate::application::service::procurement_service::{
MovePipeline, ProcurementService,
};
use crate::domain::entity::MoveState;
use crate::infrastructure::persistence::procurement_repository::ProcurementRepository;
pub const SCHEDULER_BATCH_SIZE: i64 = 50;
pub const MAX_BATCHES_PER_TASK: usize = 1_000;
#[derive(Debug, Clone, Default)]
pub struct SchedulerReport {
pub orderpoints_claimed: usize,
pub orderpoints_triggered: usize,
pub orderpoints_skipped: usize,
pub replenishment_moves_minted: usize,
pub moves_assign_attempted: usize,
pub moves_assigned: usize,
pub moves_partially_available: usize,
pub assign_failures: usize,
pub quants_vacuumed: u64,
}
#[derive(Debug, Clone, Copy)]
pub struct SchedulerBatching {
pub batch_size: i64,
pub max_batches: usize,
}
impl Default for SchedulerBatching {
fn default() -> Self {
Self { batch_size: SCHEDULER_BATCH_SIZE, max_batches: MAX_BATCHES_PER_TASK }
}
}
pub async fn run_scheduler(
pool: &PgPool,
service: &ProcurementService,
pipeline: Arc<dyn MovePipeline>,
) -> Result<SchedulerReport, sqlx::Error> {
run_scheduler_with(pool, service, pipeline, SchedulerBatching::default()).await
}
pub async fn run_scheduler_with(
pool: &PgPool,
service: &ProcurementService,
pipeline: Arc<dyn MovePipeline>,
batching: SchedulerBatching,
) -> Result<SchedulerReport, sqlx::Error> {
let pool = crate::request_pool::current().unwrap_or_else(|| pool.clone());
let mut report = SchedulerReport::default();
let mut seen: std::collections::HashSet<Uuid> = std::collections::HashSet::new();
for _ in 0..batching.max_batches {
let mut tx = pool.begin().await?;
let claimed =
ProcurementRepository::claim_orderpoints(&mut *tx, batching.batch_size).await?;
if claimed.is_empty() || claimed.iter().all(|op| seen.contains(&op.id)) {
tx.rollback().await?;
break;
}
seen.extend(claimed.iter().map(|op| op.id));
report.orderpoints_claimed += claimed.len();
let mut fired = Vec::new();
for op in &claimed {
match service.reorder_one(&mut tx, op).await {
Ok(Some(outcome)) => {
report.orderpoints_triggered += 1;
report.replenishment_moves_minted += 1;
fired.push(outcome);
}
Ok(None) => report.orderpoints_skipped += 1,
Err(e) => warn!(
target: "inventory.scheduler",
orderpoint_id = %op.id, error = %e,
"orderpoint reorder rung failed; skipped this run"
),
}
}
tx.commit().await?;
for outcome in &fired {
service.emit_orderpoint_triggered(outcome);
}
}
let mut seen: std::collections::HashSet<Uuid> = std::collections::HashSet::new();
for _ in 0..batching.max_batches {
let mut tx = pool.begin().await?;
let moves =
ProcurementRepository::claim_assignable_moves(&mut *tx, batching.batch_size).await?;
if moves.is_empty() || moves.iter().all(|m| seen.contains(&m.id)) {
tx.rollback().await?;
break;
}
seen.extend(moves.iter().map(|m| m.id));
report.moves_assign_attempted += moves.len();
for m in &moves {
match service.assign_one(&mut tx, pipeline.as_ref(), m.id).await {
Ok(MoveState::Assigned) => report.moves_assigned += 1,
Ok(MoveState::PartiallyAvailable) => report.moves_partially_available += 1,
Ok(other) => {
warn!(
target: "inventory.scheduler",
move_id = %m.id, state = %other,
"assign verb returned a non-reserving state"
);
}
Err(e) => {
report.assign_failures += 1;
warn!(
target: "inventory.scheduler",
move_id = %m.id, error = %e,
"assign verb failed; move stays for the next run"
);
}
}
}
tx.commit().await?;
}
for _ in 0..batching.max_batches {
let mut tx = pool.begin().await?;
let removed = service.housekeep_quants(&mut tx, batching.batch_size).await?;
tx.commit().await?;
report.quants_vacuumed += removed;
if removed < batching.batch_size as u64 {
break;
}
}
Ok(report)
}