drasi_source_application/lib.rs
1// Copyright 2025 The Drasi Authors.
2//
3// Licensed under the Apache License, Version 2.0 (the "License");
4// you may not use this file except in compliance with the License.
5// You may obtain a copy of the License at
6//
7// http://www.apache.org/licenses/LICENSE-2.0
8//
9// Unless required by applicable law or agreed to in writing, software
10// distributed under the License is distributed on an "AS IS" BASIS,
11// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12// See the License for the specific language governing permissions and
13// limitations under the License.
14
15#![allow(unexpected_cfgs)]
16
17//! Application Source Plugin for Drasi
18//!
19//! This plugin enables programmatic event injection into Drasi's continuous query
20//! processing pipeline. Unlike other sources that connect to external data systems,
21//! the application source allows your Rust code to directly send graph data changes.
22//!
23//! # Architecture
24//!
25//! The application source uses a handle-based pattern:
26//! - **`ApplicationSource`**: The source component that processes events
27//! - **`ApplicationSourceHandle`**: A cloneable handle for sending events from anywhere in your code
28//!
29//! # API Overview
30//!
31//! The `ApplicationSourceHandle` provides high-level methods for common operations:
32//!
33//! - [`send_node_insert`](ApplicationSourceHandle::send_node_insert) - Insert a new node
34//! - [`send_node_update`](ApplicationSourceHandle::send_node_update) - Update an existing node
35//! - [`send_delete`](ApplicationSourceHandle::send_delete) - Delete a node or relation
36//! - [`send_relation_insert`](ApplicationSourceHandle::send_relation_insert) - Insert a relationship
37//! - [`send_batch`](ApplicationSourceHandle::send_batch) - Send multiple changes efficiently
38//! - [`send`](ApplicationSourceHandle::send) - Send a raw `SourceChange` event
39//!
40//! # Building Properties
41//!
42//! Use the [`PropertyMapBuilder`] to construct property maps fluently:
43//!
44//! ```rust,ignore
45//! use drasi_source_application::PropertyMapBuilder;
46//!
47//! let props = PropertyMapBuilder::new()
48//! .string("name", "Alice")
49//! .integer("age", 30)
50//! .float("score", 95.5)
51//! .bool("active", true)
52//! .build();
53//! ```
54//!
55//! # Configuration
56//!
57//! The application source has minimal configuration since it receives events programmatically
58//! rather than connecting to an external system.
59//!
60//! | Field | Type | Default | Description |
61//! |-------|------|---------|-------------|
62//! | `properties` | object | `{}` | Custom properties (passed through to `properties()`) |
63//!
64//! # Bootstrap Support
65//!
66//! The application source supports pluggable bootstrap providers via the `BootstrapProvider`
67//! trait. Configure a bootstrap provider using `set_bootstrap_provider()` or through the
68//! builder pattern. Common options include `ApplicationBootstrapProvider` for replaying
69//! stored events, or any other `BootstrapProvider` implementation.
70//!
71//! # Example Configuration (YAML)
72//!
73//! ```yaml
74//! source_type: application
75//! properties: {}
76//! ```
77//!
78//! # Usage Example
79//!
80//! ```rust,ignore
81//! use drasi_source_application::{
82//! ApplicationSource, ApplicationSourceConfig, ApplicationSourceHandle,
83//! PropertyMapBuilder
84//! };
85//! use std::sync::Arc;
86//!
87//! // Create the source and handle
88//! let config = ApplicationSourceConfig::default();
89//! let (source, handle) = ApplicationSource::new("my-app-source", config)?;
90//!
91//! // Add source to Drasi
92//! let source = Arc::new(source);
93//! drasi.add_source(source).await?;
94//!
95//! // Clone handle for use in different parts of your application
96//! let handle_clone = handle.clone();
97//!
98//! // Insert a node
99//! let props = PropertyMapBuilder::new()
100//! .string("name", "Alice")
101//! .integer("age", 30)
102//! .build();
103//!
104//! handle.send_node_insert("user-1", vec!["User"], props).await?;
105//!
106//! // Insert a relationship
107//! let rel_props = PropertyMapBuilder::new()
108//! .string("since", "2024-01-01")
109//! .build();
110//!
111//! handle.send_relation_insert(
112//! "follows-1",
113//! vec!["FOLLOWS"],
114//! rel_props,
115//! "user-1", // start node
116//! "user-2", // end node
117//! ).await?;
118//!
119//! // Update a node
120//! let updated_props = PropertyMapBuilder::new()
121//! .integer("age", 31)
122//! .build();
123//!
124//! handle.send_node_update("user-1", vec!["User"], updated_props).await?;
125//!
126//! // Delete a node
127//! handle.send_delete("user-1", vec!["User"]).await?;
128//! ```
129//!
130//! # Use Cases
131//!
132//! - **Testing**: Inject test data directly without setting up external sources
133//! - **Integration**: Bridge between your application logic and Drasi queries
134//! - **Simulation**: Generate synthetic events for development and demos
135//! - **Hybrid Sources**: Combine with other sources for complex data pipelines
136
137pub mod config;
138pub use config::ApplicationSourceConfig;
139
140mod property_builder;
141mod time;
142
143#[cfg(test)]
144mod tests;
145
146pub use property_builder::PropertyMapBuilder;
147
148use anyhow::Result;
149use async_trait::async_trait;
150use log::{debug, error, info, trace, warn};
151use std::collections::HashMap;
152use std::sync::Arc;
153use std::time::Duration;
154use tokio::sync::{mpsc, RwLock};
155
156use drasi_core::models::{Element, ElementMetadata, ElementReference, SourceChange};
157use drasi_lib::channels::{ComponentStatus, *};
158use drasi_lib::sources::base::{SourceBase, SourceBaseParams};
159use drasi_lib::wal::{WalError, WalProvider};
160use drasi_lib::Source;
161use tracing::Instrument;
162
163/// Internal event passed through the channel, carrying optional pre-assigned WAL sequence
164struct InternalEvent {
165 change: SourceChange,
166 wal_seq: Option<u64>,
167}
168
169/// Handle for programmatic event injection into an Application Source
170///
171/// `ApplicationSourceHandle` provides a type-safe API for injecting graph data changes
172/// (node inserts, updates, deletes, and relationship inserts) directly from your application
173/// code into the Drasi continuous query processing pipeline.
174#[derive(Clone)]
175pub struct ApplicationSourceHandle {
176 tx: mpsc::Sender<InternalEvent>,
177 source_id: String,
178 /// Shared WAL reference — populated when the source is started with durability enabled
179 wal: Arc<tokio::sync::RwLock<Option<Arc<dyn WalProvider>>>>,
180}
181
182impl ApplicationSourceHandle {
183 /// Send a raw source change event
184 ///
185 /// If WAL durability is enabled, the event is persisted to the WAL before
186 /// being acknowledged (returned Ok). This ensures the WAL-before-ACK guarantee.
187 pub async fn send(&self, change: SourceChange) -> Result<()> {
188 // WAL append before ACK (if durability is enabled)
189 let wal_seq = {
190 let wal_guard = self.wal.read().await;
191 if let Some(ref wal) = *wal_guard {
192 match wal.append(&self.source_id, &change).await {
193 Ok(seq) => Some(seq),
194 Err(WalError::CapacityExhausted(msg)) => {
195 return Err(anyhow::anyhow!("WAL capacity exhausted: {msg}"));
196 }
197 Err(e) => {
198 return Err(anyhow::anyhow!("WAL append failed: {e}"));
199 }
200 }
201 } else {
202 None
203 }
204 };
205
206 self.tx
207 .send(InternalEvent { change, wal_seq })
208 .await
209 .map_err(|_| anyhow::anyhow!("Failed to send event: channel closed"))?;
210 Ok(())
211 }
212
213 /// Insert a new node into the graph
214 pub async fn send_node_insert(
215 &self,
216 element_id: impl Into<Arc<str>>,
217 labels: Vec<impl Into<Arc<str>>>,
218 properties: drasi_core::models::ElementPropertyMap,
219 ) -> Result<()> {
220 let effective_from = crate::time::get_current_timestamp_millis().unwrap_or_else(|e| {
221 warn!("Failed to get timestamp for node insert: {e}, using fallback");
222 chrono::Utc::now().timestamp_millis() as u64
223 });
224
225 let element = Element::Node {
226 metadata: ElementMetadata {
227 reference: ElementReference {
228 source_id: Arc::from(self.source_id.as_str()),
229 element_id: element_id.into(),
230 },
231 labels: Arc::from(labels.into_iter().map(|l| l.into()).collect::<Vec<_>>()),
232 effective_from,
233 },
234 properties,
235 };
236
237 self.send(SourceChange::Insert { element }).await
238 }
239
240 /// Update an existing node in the graph
241 pub async fn send_node_update(
242 &self,
243 element_id: impl Into<Arc<str>>,
244 labels: Vec<impl Into<Arc<str>>>,
245 properties: drasi_core::models::ElementPropertyMap,
246 ) -> Result<()> {
247 let effective_from = crate::time::get_current_timestamp_millis().unwrap_or_else(|e| {
248 warn!("Failed to get timestamp for node update: {e}, using fallback");
249 chrono::Utc::now().timestamp_millis() as u64
250 });
251
252 let element = Element::Node {
253 metadata: ElementMetadata {
254 reference: ElementReference {
255 source_id: Arc::from(self.source_id.as_str()),
256 element_id: element_id.into(),
257 },
258 labels: Arc::from(labels.into_iter().map(|l| l.into()).collect::<Vec<_>>()),
259 effective_from,
260 },
261 properties,
262 };
263
264 self.send(SourceChange::Update { element }).await
265 }
266
267 /// Delete a node or relationship from the graph
268 pub async fn send_delete(
269 &self,
270 element_id: impl Into<Arc<str>>,
271 labels: Vec<impl Into<Arc<str>>>,
272 ) -> Result<()> {
273 let effective_from = crate::time::get_current_timestamp_millis().unwrap_or_else(|e| {
274 warn!("Failed to get timestamp for delete: {e}, using fallback");
275 chrono::Utc::now().timestamp_millis() as u64
276 });
277
278 let metadata = ElementMetadata {
279 reference: ElementReference {
280 source_id: Arc::from(self.source_id.as_str()),
281 element_id: element_id.into(),
282 },
283 labels: Arc::from(labels.into_iter().map(|l| l.into()).collect::<Vec<_>>()),
284 effective_from,
285 };
286
287 self.send(SourceChange::Delete { metadata }).await
288 }
289
290 /// Insert a new relationship into the graph
291 pub async fn send_relation_insert(
292 &self,
293 element_id: impl Into<Arc<str>>,
294 labels: Vec<impl Into<Arc<str>>>,
295 properties: drasi_core::models::ElementPropertyMap,
296 start_node_id: impl Into<Arc<str>>,
297 end_node_id: impl Into<Arc<str>>,
298 ) -> Result<()> {
299 let effective_from = crate::time::get_current_timestamp_millis().unwrap_or_else(|e| {
300 warn!("Failed to get timestamp for relation insert: {e}, using fallback");
301 chrono::Utc::now().timestamp_millis() as u64
302 });
303
304 let element = Element::Relation {
305 metadata: ElementMetadata {
306 reference: ElementReference {
307 source_id: Arc::from(self.source_id.as_str()),
308 element_id: element_id.into(),
309 },
310 labels: Arc::from(labels.into_iter().map(|l| l.into()).collect::<Vec<_>>()),
311 effective_from,
312 },
313 properties,
314 in_node: ElementReference {
315 source_id: Arc::from(self.source_id.as_str()),
316 element_id: start_node_id.into(),
317 },
318 out_node: ElementReference {
319 source_id: Arc::from(self.source_id.as_str()),
320 element_id: end_node_id.into(),
321 },
322 };
323
324 self.send(SourceChange::Insert { element }).await
325 }
326
327 /// Send a batch of source changes efficiently
328 pub async fn send_batch(&self, changes: Vec<SourceChange>) -> Result<()> {
329 for change in changes {
330 self.send(change).await?;
331 }
332 Ok(())
333 }
334
335 /// Get the source ID that this handle is connected to
336 pub fn source_id(&self) -> &str {
337 &self.source_id
338 }
339}
340
341/// A source that allows applications to programmatically inject events.
342///
343/// This source receives events from an [`ApplicationSourceHandle`] and forwards
344/// them to the Drasi query processing pipeline.
345///
346/// # Fields
347///
348/// - `base`: Common source functionality (dispatchers, status, lifecycle, bootstrap)
349/// - `config`: Application source configuration
350/// - `app_rx`: Receiver for events from the handle
351/// - `app_tx`: Sender for creating additional handles
352pub struct ApplicationSource {
353 /// Base source implementation providing common functionality
354 base: SourceBase,
355 /// Application source configuration
356 config: ApplicationSourceConfig,
357 /// Receiver for events from handles (taken when processing starts)
358 app_rx: Arc<RwLock<Option<mpsc::Receiver<InternalEvent>>>>,
359 /// Sender for creating new handles
360 app_tx: mpsc::Sender<InternalEvent>,
361 /// WAL provider for durable event persistence (shared with handles for WAL-before-ACK)
362 wal: Arc<tokio::sync::RwLock<Option<Arc<dyn WalProvider>>>>,
363 /// Handle to the WAL pruning background task (if running)
364 prune_task: tokio::sync::RwLock<Option<tokio::task::JoinHandle<()>>>,
365}
366
367impl ApplicationSource {
368 /// Create a new application source and its handle.
369 ///
370 /// The event channel is automatically injected when the source is added
371 /// to DrasiLib via `add_source()`.
372 ///
373 /// # Arguments
374 ///
375 /// * `id` - Unique identifier for this source instance
376 /// * `config` - Application source configuration
377 ///
378 /// # Returns
379 ///
380 /// A tuple of `(ApplicationSource, ApplicationSourceHandle)` where the handle
381 /// can be used to send events to the source.
382 ///
383 /// # Errors
384 ///
385 /// Returns an error if the base source cannot be initialized.
386 ///
387 /// # Example
388 ///
389 /// ```rust,ignore
390 /// use drasi_source_application::{ApplicationSource, ApplicationSourceConfig};
391 ///
392 /// let config = ApplicationSourceConfig::default();
393 /// let (source, handle) = ApplicationSource::new("my-source", config)?;
394 /// ```
395 pub fn new(
396 id: impl Into<String>,
397 config: ApplicationSourceConfig,
398 ) -> Result<(Self, ApplicationSourceHandle)> {
399 let id = id.into();
400 let params = SourceBaseParams::new(id.clone());
401 let (app_tx, app_rx) = mpsc::channel(1000);
402
403 // Shared WAL reference — populated later in start() when durability is enabled
404 let shared_wal = Arc::new(tokio::sync::RwLock::new(None));
405
406 let handle = ApplicationSourceHandle {
407 tx: app_tx.clone(),
408 source_id: id.clone(),
409 wal: shared_wal.clone(),
410 };
411
412 let source = Self {
413 base: SourceBase::new(params)?,
414 config,
415 app_rx: Arc::new(RwLock::new(Some(app_rx))),
416 app_tx,
417 wal: shared_wal,
418 prune_task: tokio::sync::RwLock::new(None),
419 };
420
421 Ok((source, handle))
422 }
423
424 /// Get a new handle for this source
425 ///
426 /// The handle shares the WAL reference with the source, so handles obtained
427 /// before or after `start()` will automatically use WAL when durability is enabled.
428 pub fn get_handle(&self) -> ApplicationSourceHandle {
429 ApplicationSourceHandle {
430 tx: self.app_tx.clone(),
431 source_id: self.base.id.clone(),
432 wal: self.wal.clone(),
433 }
434 }
435
436 async fn process_events(&self) -> Result<()> {
437 let mut rx = self
438 .app_rx
439 .write()
440 .await
441 .take()
442 .ok_or_else(|| anyhow::anyhow!("Receiver already taken"))?;
443
444 let source_name = self.base.id.clone();
445 let base_dispatchers = self.base.dispatchers.clone();
446 let reporter = self.base.status_handle();
447 let source_id = self.base.id.clone();
448
449 // Get instance_id from context for log route isolation
450 let instance_id = self
451 .base
452 .context()
453 .await
454 .map(|c| c.instance_id)
455 .unwrap_or_default();
456
457 let span = tracing::info_span!(
458 "application_source_processor",
459 instance_id = %instance_id,
460 component_id = %source_id,
461 component_type = "source"
462 );
463 let handle = tokio::spawn(
464 async move {
465 info!("ApplicationSource '{source_name}' event processor started");
466
467 reporter
468 .set_status(
469 ComponentStatus::Running,
470 Some("Processing events".to_string()),
471 )
472 .await;
473
474 while let Some(event) = rx.recv().await {
475 debug!(
476 "ApplicationSource '{source_name}' received event: {:?}",
477 event.change
478 );
479
480 let mut profiling = drasi_lib::profiling::ProfilingMetadata::new();
481 profiling.source_send_ns = Some(drasi_lib::profiling::timestamp_ns());
482
483 let mut wrapper = SourceEventWrapper::with_profiling(
484 source_name.clone(),
485 SourceEvent::Change(event.change),
486 chrono::Utc::now(),
487 profiling,
488 );
489
490 // Use pre-assigned WAL sequence from handle (WAL-before-ACK)
491 if let Some(seq) = event.wal_seq {
492 wrapper.sequence = Some(seq);
493 wrapper.source_position =
494 Some(bytes::Bytes::from(seq.to_be_bytes().to_vec()));
495 }
496
497 if let Err(e) = SourceBase::dispatch_from_task(
498 base_dispatchers.clone(),
499 wrapper,
500 &source_name,
501 )
502 .await
503 {
504 debug!("Failed to dispatch change (no subscribers): {e}");
505 }
506 }
507
508 info!("ApplicationSource '{source_name}' event processor stopped");
509 }
510 .instrument(span),
511 );
512
513 *self.base.task_handle.write().await = Some(handle);
514 Ok(())
515 }
516}
517
518#[async_trait]
519impl Source for ApplicationSource {
520 fn id(&self) -> &str {
521 &self.base.id
522 }
523
524 fn type_name(&self) -> &str {
525 "application"
526 }
527
528 fn properties(&self) -> HashMap<String, serde_json::Value> {
529 self.config.properties.clone()
530 }
531
532 fn auto_start(&self) -> bool {
533 self.base.get_auto_start()
534 }
535
536 async fn start(&self) -> Result<()> {
537 info!("Starting ApplicationSource '{}'", self.base.id);
538
539 self.base
540 .set_status(
541 ComponentStatus::Starting,
542 Some("Starting application source".to_string()),
543 )
544 .await;
545
546 // Initialize WAL if durability is enabled
547 let wal_ref: Option<Arc<dyn WalProvider>> =
548 if self.config.durability.as_ref().is_some_and(|d| d.enabled) {
549 let ctx = self
550 .base
551 .context()
552 .await
553 .ok_or_else(|| anyhow::anyhow!("Context not initialized"))?;
554 let wal = ctx.wal_provider.clone().ok_or_else(|| {
555 anyhow::anyhow!("Durability enabled but no WAL provider configured on DrasiLib")
556 })?;
557 let wal_config = self
558 .config
559 .durability
560 .as_ref()
561 .expect("durability checked above")
562 .to_wal_config();
563 wal.register(&self.base.id, wal_config.clone())
564 .await
565 .map_err(|e| {
566 anyhow::anyhow!(
567 "Failed to register WAL for source '{}': {}",
568 self.base.id,
569 e
570 )
571 })?;
572
573 info!(
574 "[{}] WAL registered: max_events={}, policy={:?}",
575 self.base.id, wal_config.max_events, wal_config.capacity_policy
576 );
577
578 // Resume sequence counter from WAL head
579 let head = wal.head_sequence(&self.base.id).await.unwrap_or(0);
580 if head > 0 {
581 self.base.set_next_sequence(head);
582 info!(
583 "[{}] WAL resumed from persisted state: head={}, next_sequence={}",
584 self.base.id,
585 head,
586 head + 1
587 );
588 }
589
590 *self.wal.write().await = Some(wal.clone());
591 Some(wal)
592 } else {
593 None
594 };
595
596 self.process_events().await?;
597
598 // Spawn WAL pruning task if durability is enabled
599 if let Some(wal) = wal_ref {
600 let base = self.base.clone_shared();
601 let source_id = self.base.id.clone();
602 let prune_handle = tokio::spawn(async move {
603 let mut interval = tokio::time::interval(Duration::from_secs(30));
604 loop {
605 interval.tick().await;
606 if let Some(confirmed) = base.compute_confirmed_position().await {
607 if confirmed > 0 {
608 match wal.prune_up_to(&source_id, confirmed).await {
609 Ok(pruned) => {
610 if pruned > 0 {
611 let remaining =
612 wal.event_count(&source_id).await.unwrap_or(0);
613 debug!(
614 "[{source_id}] WAL pruned: count={pruned}, confirmed_seq={confirmed}, remaining={remaining}"
615 );
616 }
617 }
618 Err(e) => {
619 warn!("[{source_id}] WAL prune failed: {e}");
620 }
621 }
622 }
623 }
624 }
625 });
626 *self.prune_task.write().await = Some(prune_handle);
627 }
628
629 Ok(())
630 }
631
632 async fn stop(&self) -> Result<()> {
633 info!("Stopping ApplicationSource '{}'", self.base.id);
634
635 self.base
636 .set_status(
637 ComponentStatus::Stopping,
638 Some("Stopping application source".to_string()),
639 )
640 .await;
641
642 // Cancel WAL pruning task
643 if let Some(handle) = self.prune_task.write().await.take() {
644 handle.abort();
645 }
646
647 if let Some(handle) = self.base.task_handle.write().await.take() {
648 handle.abort();
649 }
650
651 self.base
652 .set_status(
653 ComponentStatus::Stopped,
654 Some("Application source stopped".to_string()),
655 )
656 .await;
657
658 Ok(())
659 }
660
661 async fn status(&self) -> ComponentStatus {
662 self.base.get_status().await
663 }
664
665 async fn subscribe(
666 &self,
667 settings: drasi_lib::config::SourceSubscriptionSettings,
668 ) -> Result<SubscriptionResponse> {
669 // If WAL is enabled and subscriber is resuming, use WAL replay
670 let wal_guard = self.wal.read().await;
671 if let (Some(wal), Some(ref resume_from)) = (wal_guard.as_ref(), &settings.resume_from) {
672 // Decode resume_from as big-endian u64 sequence
673 if resume_from.len() >= 8 {
674 let resume_seq =
675 u64::from_be_bytes(resume_from[..8].try_into().unwrap_or_default());
676 let wal_clone = wal.clone();
677 drop(wal_guard);
678 return self
679 .base
680 .subscribe_with_replay(&settings, wal_clone.as_ref(), resume_seq, "Application")
681 .await;
682 } else {
683 drop(wal_guard);
684 return Err(anyhow::anyhow!(
685 "Invalid resume_from position: expected at least 8 bytes, got {}",
686 resume_from.len()
687 ));
688 }
689 }
690 drop(wal_guard);
691 self.base
692 .subscribe_with_bootstrap(&settings, "Application")
693 .await
694 }
695
696 fn supports_replay(&self) -> bool {
697 self.config.durability.as_ref().is_some_and(|d| d.enabled)
698 }
699
700 async fn deprovision(&self) -> Result<()> {
701 // Delete WAL data if durability was enabled
702 let wal_guard = self.wal.read().await;
703 if let Some(ref wal) = *wal_guard {
704 info!("[{}] Deprovisioning: deleting WAL data", self.base.id);
705 if let Err(e) = wal.delete_wal(&self.base.id).await {
706 warn!(
707 "[{}] Failed to delete WAL during deprovision: {}",
708 self.base.id, e
709 );
710 }
711 }
712 drop(wal_guard);
713 Ok(())
714 }
715
716 fn as_any(&self) -> &dyn std::any::Any {
717 self
718 }
719
720 async fn initialize(&self, context: drasi_lib::context::SourceRuntimeContext) {
721 self.base.initialize(context).await;
722 }
723
724 async fn set_bootstrap_provider(
725 &self,
726 provider: Box<dyn drasi_lib::bootstrap::BootstrapProvider + 'static>,
727 ) {
728 self.base.set_bootstrap_provider(provider).await;
729 }
730}