use std::collections::HashMap;
use std::sync::Arc;
use tokio::sync::{RwLock, mpsc};
use dashmap::DashMap;
use anyhow::{Result, Context};
use tracing::{info, warn, error, instrument};
use serde::{Deserialize, Serialize};
use chrono::{DateTime, Utc};
use kotoba_ocel::OcelEvent;
use kotoba_storage::KeyValueStore;
pub mod event_processor;
pub mod materializer;
pub mod view_manager;
pub mod storage;
pub mod cache_integration;
pub mod metrics;
pub mod gql_integration;
pub use event_processor::*;
pub use materializer::*;
pub use view_manager::*;
pub use storage::*;
pub use cache_integration::*;
pub use metrics::*;
pub struct ProjectionEngine<T: KeyValueStore> {
event_processor: Arc<EventProcessor<T>>,
materializer: Arc<Materializer<T>>,
storage: Arc<T>,
view_manager: Arc<ViewManager>,
metrics: Arc<MetricsCollector>,
config: ProjectionConfig,
active_projections: Arc<DashMap<String, ProjectionState>>,
shutdown_tx: mpsc::Sender<()>,
shutdown_rx: Arc<RwLock<mpsc::Receiver<()>>>,
}
#[derive(Debug, Clone)]
pub struct ProjectionConfig {
pub storage_prefix: String,
pub max_concurrent_projections: usize,
pub batch_size: usize,
pub checkpoint_interval: u64,
pub enable_metrics: bool,
}
impl Default for ProjectionConfig {
fn default() -> Self {
Self {
storage_prefix: "projections".to_string(),
max_concurrent_projections: 10,
batch_size: 100,
checkpoint_interval: 1000,
enable_metrics: true,
}
}
}
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
pub struct ProjectionState {
pub name: String,
pub sequence_number: u64,
pub last_checkpoint: chrono::DateTime<chrono::Utc>,
pub status: ProjectionStatus,
pub stats: ProjectionStats,
}
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
pub enum ProjectionStatus {
Active,
Paused,
Error(String),
Rebuilding,
}
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
pub struct ProjectionStats {
pub events_processed: u64,
pub events_per_second: f64,
pub total_processing_time_ms: u64,
pub avg_processing_time_ms: f64,
pub cache_hits: u64,
pub cache_misses: u64,
}
impl<T: KeyValueStore + 'static> ProjectionEngine<T> {
pub fn new(config: ProjectionConfig, storage: Arc<T>) -> Self {
info!("Initializing Projection Engine with config: {:?}", config);
let view_manager = Arc::new(ViewManager::new());
let metrics = Arc::new(MetricsCollector::new());
let materializer = Arc::new(Materializer::new(
storage.clone(),
config.storage_prefix.clone(),
));
let event_processor = Arc::new(EventProcessor::new(
materializer.clone(),
config.batch_size,
));
let (shutdown_tx, shutdown_rx) = mpsc::channel(1);
let engine = Self {
event_processor,
materializer,
storage,
view_manager,
metrics,
config,
active_projections: Arc::new(DashMap::new()),
shutdown_tx,
shutdown_rx: Arc::new(RwLock::new(shutdown_rx)),
};
info!("Projection Engine initialized successfully");
engine
}
}
impl<T: KeyValueStore> Clone for ProjectionEngine<T> {
fn clone(&self) -> Self {
Self {
event_processor: self.event_processor.clone(),
materializer: self.materializer.clone(),
storage: self.storage.clone(),
view_manager: self.view_manager.clone(),
metrics: self.metrics.clone(),
config: self.config.clone(),
active_projections: self.active_projections.clone(),
shutdown_tx: self.shutdown_tx.clone(),
shutdown_rx: self.shutdown_rx.clone(),
}
}
}
impl<T: KeyValueStore + 'static> ProjectionEngine<T> {
#[instrument(skip(self))]
pub async fn start(&self) -> Result<()> {
info!("Starting Projection Engine");
self.event_processor.start().await?;
self.load_existing_projections().await?;
if self.config.enable_metrics {
self.start_metrics_collection().await?;
}
info!("Projection Engine started successfully");
Ok(())
}
#[instrument(skip(self))]
pub async fn stop(&self) -> Result<()> {
info!("Stopping Projection Engine");
let _ = self.shutdown_tx.send(()).await;
self.event_processor.stop().await?;
self.save_projection_states().await?;
info!("Projection Engine stopped successfully");
Ok(())
}
#[instrument(skip(self))]
pub async fn create_projection(
&self,
name: String,
definition: ProjectionDefinition,
) -> Result<()> {
info!("Creating projection: {}", name);
self.validate_projection_definition(&definition).await?;
self.view_manager.create_projection(name.clone(), definition).await?;
let state = ProjectionState {
name: name.clone(),
sequence_number: 0,
last_checkpoint: chrono::Utc::now(),
status: ProjectionStatus::Active,
stats: ProjectionStats::default(),
};
self.active_projections.insert(name.clone(), state);
self.event_processor.register_projection(&name).await?;
info!("Projection created successfully: {}", name);
Ok(())
}
#[instrument(skip(self))]
pub async fn delete_projection(&self, name: &str) -> Result<()> {
info!("Deleting projection: {}", name);
self.event_processor.unregister_projection(name).await?;
self.view_manager.delete_projection(name).await?;
self.active_projections.remove(name);
info!("Projection deleted successfully: {}", name);
Ok(())
}
pub async fn get_projection_status(&self, name: &str) -> Result<Option<ProjectionState>> {
Ok(self.active_projections.get(name).map(|s| s.clone()))
}
pub async fn list_projections(&self) -> Vec<String> {
self.active_projections.iter().map(|p| p.key().clone()).collect()
}
#[instrument(skip(self, events))]
pub async fn process_ocel_events(&self, events: Vec<OcelEvent>) -> Result<()> {
if self.config.enable_metrics {
}
self.event_processor.process_batch(events).await
}
#[instrument(skip(self, events))]
pub async fn process_events(&self, events: Vec<EventEnvelope>) -> Result<()> {
warn!("Legacy event processing is deprecated. Use process_ocel_events instead.");
Ok(())
}
#[instrument(skip(self, query))]
pub async fn query_projections(&self, query: serde_json::Value) -> Result<serde_json::Value> {
warn!("query_projections is not fully implemented yet");
Ok(serde_json::json!({
"columns": [],
"rows": [],
"statistics": {
"total_rows": 0,
"execution_time_ms": 0
}
}))
}
#[instrument(skip(self, query))]
pub async fn query_view(&self, projection_name: &str, query: ViewQuery) -> Result<ViewResult> {
warn!("Legacy view querying is deprecated. Use query_graph instead.");
Err(anyhow::anyhow!("Legacy view querying not supported"))
}
pub async fn get_statistics(&self) -> EngineStatistics {
let mut total_events = 0u64;
let mut active_projections = 0usize;
for projection in self.active_projections.iter() {
total_events += projection.stats.events_processed;
if matches!(projection.status, ProjectionStatus::Active) {
active_projections += 1;
}
}
EngineStatistics {
total_projections: self.active_projections.len(),
active_projections,
total_events_processed: total_events,
uptime_seconds: 0, storage_size_bytes: 0, }
}
#[instrument(skip(self))]
pub async fn execute_gql_query(
&self,
query: &str,
_user_id: Option<String>,
_timeout_seconds: u64,
_parameters: std::collections::HashMap<String, serde_json::Value>,
) -> Result<serde_json::Value> {
use crate::gql_integration::ProjectionEngineAdapter;
let adapter = ProjectionEngineAdapter::new(Arc::new(self.clone()));
adapter.execute_gql_query(query, serde_json::json!({})).await
}
#[instrument(skip(self))]
pub async fn execute_gql_statement(
&self,
statement: &str,
_user_id: Option<String>,
_timeout_seconds: u64,
_parameters: std::collections::HashMap<String, serde_json::Value>,
) -> Result<serde_json::Value> {
use crate::gql_integration::ProjectionEngineAdapter;
let adapter = ProjectionEngineAdapter::new(Arc::new(self.clone()));
adapter.execute_gql_statement(statement, serde_json::json!({})).await
}
async fn load_existing_projections(&self) -> Result<()> {
info!("Loading existing projections");
let prefix = format!("{}:projection:", self.config.storage_prefix);
let projection_keys = self.storage.scan(prefix.as_bytes()).await?;
for key_bytes in projection_keys {
if let Ok(key_str) = std::str::from_utf8(&key_bytes.0) {
if let Some(projection_name) = key_str.strip_prefix(&prefix) {
let state_key = format!("{}:state:{}", self.config.storage_prefix, projection_name);
if let Some(state_data) = self.storage.get(state_key.as_bytes()).await? {
if let Ok(state) = bincode::deserialize::<ProjectionState>(&state_data) {
self.active_projections.insert(projection_name.to_string(), state);
self.event_processor.register_projection(projection_name).await?;
}
}
}
}
}
info!("Loaded {} existing projections", self.active_projections.len());
Ok(())
}
async fn save_projection_states(&self) -> Result<()> {
for projection in self.active_projections.iter() {
let state_key = format!("{}:state:{}", self.config.storage_prefix, projection.key());
let state_data = bincode::serialize(&projection.value())?;
self.storage.put(state_key.as_bytes(), &state_data).await?;
}
Ok(())
}
async fn validate_projection_definition(&self, definition: &ProjectionDefinition) -> Result<()> {
Ok(())
}
async fn start_metrics_collection(&self) -> Result<()> {
Ok(())
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct EngineStatistics {
pub total_projections: usize,
pub active_projections: usize,
pub total_events_processed: u64,
pub uptime_seconds: u64,
pub storage_size_bytes: u64,
}
pub type EventEnvelope = serde_json::Value;
pub type ProjectionDefinition = serde_json::Value;
pub type QueryResult = serde_json::Value; pub type ViewQuery = serde_json::Value;
pub type ViewResult = serde_json::Value;
impl Default for ProjectionStats {
fn default() -> Self {
Self {
events_processed: 0,
events_per_second: 0.0,
total_processing_time_ms: 0,
avg_processing_time_ms: 0.0,
cache_hits: 0,
cache_misses: 0,
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use tempfile::tempdir;
#[tokio::test]
async fn test_projection_engine_creation() {
let temp_dir = tempdir().unwrap();
let config = ProjectionConfig::default();
let stats_placeholder = EngineStatistics {
total_projections: 0,
active_projections: 0,
total_events_processed: 0,
uptime_seconds: 0,
storage_size_bytes: 0,
};
assert_eq!(stats_placeholder.total_projections, 0);
}
#[tokio::test]
async fn test_projection_lifecycle() {
let config = ProjectionConfig::default();
let projection_def = serde_json::json!({
"name": "test_projection",
"source_events": ["node.created", "edge.created"],
"target_view": "test_view"
});
assert_eq!(projection_def["name"], "test_projection");
}
#[tokio::test]
async fn test_gql_integration() {
let config = ProjectionConfig::default();
let query = "MATCH (v:Person) RETURN v";
assert!(!query.is_empty(), "GQL query should not be empty");
let statement = "CREATE GRAPH test_graph";
assert!(!statement.is_empty(), "GQL statement should not be empty");
}
}