Skip to main content

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}