kotoba_projection_engine/
lib.rs1use std::collections::HashMap;
7use std::sync::Arc;
8use tokio::sync::{RwLock, mpsc};
9use dashmap::DashMap;
10use anyhow::{Result, Context};
11use tracing::{info, warn, error, instrument};
12use serde::{Deserialize, Serialize};
14use chrono::{DateTime, Utc};
15
16use kotoba_ocel::OcelEvent;
17use kotoba_storage::KeyValueStore;
18
19pub mod event_processor;
20pub mod materializer;
21pub mod view_manager;
22pub mod storage;
23pub mod cache_integration;
24pub mod metrics;
25pub mod gql_integration;
26
27pub use event_processor::*;
29pub use materializer::*;
30pub use view_manager::*;
31pub use storage::*;
32pub use cache_integration::*;
33pub use metrics::*;
34
35pub struct ProjectionEngine<T: KeyValueStore> {
37 event_processor: Arc<EventProcessor<T>>,
39 materializer: Arc<Materializer<T>>,
41 storage: Arc<T>,
43 view_manager: Arc<ViewManager>,
45 metrics: Arc<MetricsCollector>,
47 config: ProjectionConfig,
49 active_projections: Arc<DashMap<String, ProjectionState>>,
51 shutdown_tx: mpsc::Sender<()>,
53 shutdown_rx: Arc<RwLock<mpsc::Receiver<()>>>,
54}
55
56#[derive(Debug, Clone)]
58pub struct ProjectionConfig {
59 pub storage_prefix: String,
61 pub max_concurrent_projections: usize,
63 pub batch_size: usize,
65 pub checkpoint_interval: u64,
67 pub enable_metrics: bool,
69}
70
71impl Default for ProjectionConfig {
72 fn default() -> Self {
73 Self {
74 storage_prefix: "projections".to_string(),
75 max_concurrent_projections: 10,
76 batch_size: 100,
77 checkpoint_interval: 1000,
78 enable_metrics: true,
79 }
80 }
81}
82
83
84
85
86#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
88pub struct ProjectionState {
89 pub name: String,
91 pub sequence_number: u64,
93 pub last_checkpoint: chrono::DateTime<chrono::Utc>,
95 pub status: ProjectionStatus,
97 pub stats: ProjectionStats,
99}
100
101#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
103pub enum ProjectionStatus {
104 Active,
106 Paused,
108 Error(String),
110 Rebuilding,
112}
113
114#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
116pub struct ProjectionStats {
117 pub events_processed: u64,
119 pub events_per_second: f64,
121 pub total_processing_time_ms: u64,
123 pub avg_processing_time_ms: f64,
125 pub cache_hits: u64,
127 pub cache_misses: u64,
129}
130
131impl<T: KeyValueStore + 'static> ProjectionEngine<T> {
132 pub fn new(config: ProjectionConfig, storage: Arc<T>) -> Self {
134 info!("Initializing Projection Engine with config: {:?}", config);
135
136 let view_manager = Arc::new(ViewManager::new());
138
139 let metrics = Arc::new(MetricsCollector::new());
141
142 let materializer = Arc::new(Materializer::new(
144 storage.clone(),
145 config.storage_prefix.clone(),
146 ));
147
148 let event_processor = Arc::new(EventProcessor::new(
150 materializer.clone(),
151 config.batch_size,
152 ));
153
154 let (shutdown_tx, shutdown_rx) = mpsc::channel(1);
156
157 let engine = Self {
158 event_processor,
159 materializer,
160 storage,
161 view_manager,
162 metrics,
163 config,
164 active_projections: Arc::new(DashMap::new()),
165 shutdown_tx,
166 shutdown_rx: Arc::new(RwLock::new(shutdown_rx)),
167 };
168
169 info!("Projection Engine initialized successfully");
170 engine
171 }
172}
173
174impl<T: KeyValueStore> Clone for ProjectionEngine<T> {
175 fn clone(&self) -> Self {
176 Self {
177 event_processor: self.event_processor.clone(),
178 materializer: self.materializer.clone(),
179 storage: self.storage.clone(),
180 view_manager: self.view_manager.clone(),
181 metrics: self.metrics.clone(),
182 config: self.config.clone(),
183 active_projections: self.active_projections.clone(),
184 shutdown_tx: self.shutdown_tx.clone(),
185 shutdown_rx: self.shutdown_rx.clone(),
186 }
187 }
188}
189
190impl<T: KeyValueStore + 'static> ProjectionEngine<T> {
191 #[instrument(skip(self))]
193 pub async fn start(&self) -> Result<()> {
194 info!("Starting Projection Engine");
195
196 self.event_processor.start().await?;
198
199 self.load_existing_projections().await?;
201
202 if self.config.enable_metrics {
204 self.start_metrics_collection().await?;
205 }
206
207 info!("Projection Engine started successfully");
208 Ok(())
209 }
210
211 #[instrument(skip(self))]
213 pub async fn stop(&self) -> Result<()> {
214 info!("Stopping Projection Engine");
215
216 let _ = self.shutdown_tx.send(()).await;
218
219 self.event_processor.stop().await?;
221
222 self.save_projection_states().await?;
224
225 info!("Projection Engine stopped successfully");
226 Ok(())
227 }
228
229 #[instrument(skip(self))]
231 pub async fn create_projection(
232 &self,
233 name: String,
234 definition: ProjectionDefinition,
235 ) -> Result<()> {
236 info!("Creating projection: {}", name);
237
238 self.validate_projection_definition(&definition).await?;
240
241 self.view_manager.create_projection(name.clone(), definition).await?;
243
244 let state = ProjectionState {
246 name: name.clone(),
247 sequence_number: 0,
248 last_checkpoint: chrono::Utc::now(),
249 status: ProjectionStatus::Active,
250 stats: ProjectionStats::default(),
251 };
252
253 self.active_projections.insert(name.clone(), state);
254
255 self.event_processor.register_projection(&name).await?;
257
258 info!("Projection created successfully: {}", name);
259 Ok(())
260 }
261
262 #[instrument(skip(self))]
264 pub async fn delete_projection(&self, name: &str) -> Result<()> {
265 info!("Deleting projection: {}", name);
266
267 self.event_processor.unregister_projection(name).await?;
269
270 self.view_manager.delete_projection(name).await?;
272
273 self.active_projections.remove(name);
275
276 info!("Projection deleted successfully: {}", name);
277 Ok(())
278 }
279
280 pub async fn get_projection_status(&self, name: &str) -> Result<Option<ProjectionState>> {
282 Ok(self.active_projections.get(name).map(|s| s.clone()))
283 }
284
285 pub async fn list_projections(&self) -> Vec<String> {
287 self.active_projections.iter().map(|p| p.key().clone()).collect()
288 }
289
290 #[instrument(skip(self, events))]
292 pub async fn process_ocel_events(&self, events: Vec<OcelEvent>) -> Result<()> {
293 if self.config.enable_metrics {
294 }
296
297 self.event_processor.process_batch(events).await
299 }
300
301 #[instrument(skip(self, events))]
303 pub async fn process_events(&self, events: Vec<EventEnvelope>) -> Result<()> {
304 warn!("Legacy event processing is deprecated. Use process_ocel_events instead.");
305 Ok(())
306 }
307
308 #[instrument(skip(self, query))]
310 pub async fn query_projections(&self, query: serde_json::Value) -> Result<serde_json::Value> {
311 warn!("query_projections is not fully implemented yet");
314
315 Ok(serde_json::json!({
317 "columns": [],
318 "rows": [],
319 "statistics": {
320 "total_rows": 0,
321 "execution_time_ms": 0
322 }
323 }))
324 }
325
326 #[instrument(skip(self, query))]
328 pub async fn query_view(&self, projection_name: &str, query: ViewQuery) -> Result<ViewResult> {
329 warn!("Legacy view querying is deprecated. Use query_graph instead.");
330 Err(anyhow::anyhow!("Legacy view querying not supported"))
331 }
332
333 pub async fn get_statistics(&self) -> EngineStatistics {
335 let mut total_events = 0u64;
336 let mut active_projections = 0usize;
337
338 for projection in self.active_projections.iter() {
339 total_events += projection.stats.events_processed;
340 if matches!(projection.status, ProjectionStatus::Active) {
341 active_projections += 1;
342 }
343 }
344
345 EngineStatistics {
347 total_projections: self.active_projections.len(),
348 active_projections,
349 total_events_processed: total_events,
350 uptime_seconds: 0, storage_size_bytes: 0, }
353 }
354
355 #[instrument(skip(self))]
357 pub async fn execute_gql_query(
358 &self,
359 query: &str,
360 _user_id: Option<String>,
361 _timeout_seconds: u64,
362 _parameters: std::collections::HashMap<String, serde_json::Value>,
363 ) -> Result<serde_json::Value> {
364 use crate::gql_integration::ProjectionEngineAdapter;
365
366 let adapter = ProjectionEngineAdapter::new(Arc::new(self.clone()));
367 adapter.execute_gql_query(query, serde_json::json!({})).await
368 }
369
370 #[instrument(skip(self))]
372 pub async fn execute_gql_statement(
373 &self,
374 statement: &str,
375 _user_id: Option<String>,
376 _timeout_seconds: u64,
377 _parameters: std::collections::HashMap<String, serde_json::Value>,
378 ) -> Result<serde_json::Value> {
379 use crate::gql_integration::ProjectionEngineAdapter;
380
381 let adapter = ProjectionEngineAdapter::new(Arc::new(self.clone()));
382 adapter.execute_gql_statement(statement, serde_json::json!({})).await
383 }
384
385 async fn load_existing_projections(&self) -> Result<()> {
386 info!("Loading existing projections");
387
388 let prefix = format!("{}:projection:", self.config.storage_prefix);
390 let projection_keys = self.storage.scan(prefix.as_bytes()).await?;
391
392 for key_bytes in projection_keys {
393 if let Ok(key_str) = std::str::from_utf8(&key_bytes.0) {
394 if let Some(projection_name) = key_str.strip_prefix(&prefix) {
395 let state_key = format!("{}:state:{}", self.config.storage_prefix, projection_name);
397 if let Some(state_data) = self.storage.get(state_key.as_bytes()).await? {
398 if let Ok(state) = bincode::deserialize::<ProjectionState>(&state_data) {
399 self.active_projections.insert(projection_name.to_string(), state);
400 self.event_processor.register_projection(projection_name).await?;
401 }
402 }
403 }
404 }
405 }
406
407 info!("Loaded {} existing projections", self.active_projections.len());
408 Ok(())
409 }
410
411 async fn save_projection_states(&self) -> Result<()> {
412 for projection in self.active_projections.iter() {
413 let state_key = format!("{}:state:{}", self.config.storage_prefix, projection.key());
414 let state_data = bincode::serialize(&projection.value())?;
415 self.storage.put(state_key.as_bytes(), &state_data).await?;
416 }
417 Ok(())
418 }
419
420 async fn validate_projection_definition(&self, definition: &ProjectionDefinition) -> Result<()> {
421 Ok(())
423 }
424
425 async fn start_metrics_collection(&self) -> Result<()> {
426 Ok(())
428 }
429}
430
431#[derive(Debug, Clone, Serialize, Deserialize)]
433pub struct EngineStatistics {
434 pub total_projections: usize,
435 pub active_projections: usize,
436 pub total_events_processed: u64,
437 pub uptime_seconds: u64,
438 pub storage_size_bytes: u64,
439}
440
441pub type EventEnvelope = serde_json::Value;
443pub type ProjectionDefinition = serde_json::Value;
444pub type QueryResult = serde_json::Value; pub type ViewQuery = serde_json::Value;
447pub type ViewResult = serde_json::Value;
448
449impl Default for ProjectionStats {
450 fn default() -> Self {
451 Self {
452 events_processed: 0,
453 events_per_second: 0.0,
454 total_processing_time_ms: 0,
455 avg_processing_time_ms: 0.0,
456 cache_hits: 0,
457 cache_misses: 0,
458 }
459 }
460}
461
462#[cfg(test)]
463mod tests {
464 use super::*;
465 use tempfile::tempdir;
466
467 #[tokio::test]
468 async fn test_projection_engine_creation() {
469 let temp_dir = tempdir().unwrap();
470 let config = ProjectionConfig::default();
471
472 let stats_placeholder = EngineStatistics {
475 total_projections: 0,
476 active_projections: 0,
477 total_events_processed: 0,
478 uptime_seconds: 0,
479 storage_size_bytes: 0,
480 };
481 assert_eq!(stats_placeholder.total_projections, 0);
482 }
483
484 #[tokio::test]
485 async fn test_projection_lifecycle() {
486 let config = ProjectionConfig::default();
487
488 let projection_def = serde_json::json!({
491 "name": "test_projection",
492 "source_events": ["node.created", "edge.created"],
493 "target_view": "test_view"
494 });
495
496 assert_eq!(projection_def["name"], "test_projection");
497
498 }
499
500 #[tokio::test]
501 async fn test_gql_integration() {
502 let config = ProjectionConfig::default();
503
504 let query = "MATCH (v:Person) RETURN v";
508 assert!(!query.is_empty(), "GQL query should not be empty");
510
511 let statement = "CREATE GRAPH test_graph";
512 assert!(!statement.is_empty(), "GQL statement should not be empty");
513 }
514}