139 lines
3.8 KiB
Rust
139 lines
3.8 KiB
Rust
use serde::{Deserialize, Serialize};
|
|
use std::collections::{BTreeMap, VecDeque};
|
|
use uuid::Uuid;
|
|
use chrono::{DateTime, Utc};
|
|
|
|
/// Record (L0 evidence).
|
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
|
pub struct Record {
|
|
pub role: String,
|
|
pub text: String,
|
|
pub timestamp: String,
|
|
pub source_position: u32,
|
|
}
|
|
|
|
/// Git context enrichment.
|
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
|
pub struct GitContext {
|
|
pub file: Option<String>,
|
|
pub commit_sha: Option<String>,
|
|
pub author: Option<String>,
|
|
}
|
|
|
|
/// Ingest request with full payload.
|
|
#[derive(Debug, Deserialize, Clone)]
|
|
pub struct IngestRequest {
|
|
pub project: String,
|
|
pub source: String,
|
|
pub ingest_id: String,
|
|
#[serde(default)]
|
|
pub records: Vec<Record>,
|
|
#[serde(default)]
|
|
pub git_repo_path: Option<String>,
|
|
#[serde(default)]
|
|
pub git_head: Option<String>,
|
|
}
|
|
|
|
/// Job status.
|
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
|
pub struct JobStatus {
|
|
pub job_id: String,
|
|
pub ingest_id: String,
|
|
pub project: String,
|
|
pub status: String,
|
|
pub chunks_seen: u32,
|
|
pub chunks_used: u32,
|
|
pub error: Option<String>,
|
|
pub created_at: DateTime<Utc>,
|
|
pub completed_at: Option<DateTime<Utc>>,
|
|
}
|
|
|
|
/// In-memory ingest queue — per-project FIFO + global dedup.
|
|
pub struct IngestQueue {
|
|
/// All jobs (for lookup by job_id or ingest_id)
|
|
jobs: BTreeMap<String, JobStatus>,
|
|
/// Per-project queues (ingest_id order)
|
|
project_queues: BTreeMap<String, VecDeque<String>>,
|
|
}
|
|
|
|
impl IngestQueue {
|
|
/// Create new queue.
|
|
pub fn new() -> Self {
|
|
Self {
|
|
jobs: BTreeMap::new(),
|
|
project_queues: BTreeMap::new(),
|
|
}
|
|
}
|
|
|
|
/// Submit job (idempotent by ingest_id).
|
|
pub fn submit(&mut self, project: &str, ingest_id: &str) -> (String, bool) {
|
|
if let Some(existing) = self.jobs.get(ingest_id) {
|
|
return (existing.job_id.clone(), false);
|
|
}
|
|
|
|
let job_id = format!("ingest-{}", Uuid::new_v4());
|
|
let status = JobStatus {
|
|
job_id: job_id.clone(),
|
|
ingest_id: ingest_id.to_string(),
|
|
project: project.to_string(),
|
|
status: "running".to_string(),
|
|
chunks_seen: 0,
|
|
chunks_used: 0,
|
|
error: None,
|
|
created_at: Utc::now(),
|
|
completed_at: None,
|
|
};
|
|
|
|
// Insert into job map
|
|
self.jobs.insert(ingest_id.to_string(), status);
|
|
|
|
// Enqueue to project-specific queue
|
|
self.project_queues
|
|
.entry(project.to_string())
|
|
.or_insert_with(VecDeque::new)
|
|
.push_back(ingest_id.to_string());
|
|
|
|
(job_id, true)
|
|
}
|
|
|
|
/// Get job status by job_id.
|
|
pub fn get_status(&self, job_id: &str) -> Option<JobStatus> {
|
|
self.jobs.values().find(|j| j.job_id == job_id).cloned()
|
|
}
|
|
|
|
/// Update job status (used by background task during async processing).
|
|
pub fn update_status(
|
|
&mut self,
|
|
ingest_id: &str,
|
|
status: &str,
|
|
chunks_seen: u32,
|
|
chunks_used: u32,
|
|
error: Option<String>,
|
|
) {
|
|
if let Some(job) = self.jobs.get_mut(ingest_id) {
|
|
job.status = status.to_string();
|
|
job.chunks_seen = chunks_seen;
|
|
job.chunks_used = chunks_used;
|
|
job.error = error;
|
|
if status == "completed" || status == "failed" {
|
|
job.completed_at = Some(Utc::now());
|
|
}
|
|
}
|
|
}
|
|
|
|
/// Dequeue next job for a project (FIFO).
|
|
pub fn dequeue(&mut self, project: &str) -> Option<String> {
|
|
self.project_queues
|
|
.get_mut(project)
|
|
.and_then(|q| q.pop_front())
|
|
}
|
|
|
|
/// Get queue depth for a project.
|
|
pub fn queue_depth(&self, project: &str) -> usize {
|
|
self.project_queues
|
|
.get(project)
|
|
.map(|q| q.len())
|
|
.unwrap_or(0)
|
|
}
|
|
}
|