#![allow(unexpected_cfgs)]
mod config;
mod convert;
pub mod descriptor;
mod rest_api;
mod thread;
pub use config::{
RestApiConfig, SqliteSourceBuilder, SqliteSourceConfig, StartFrom, TableKeyConfig,
};
pub use thread::SqliteParam;
use anyhow::{anyhow, Result};
use async_trait::async_trait;
use drasi_lib::channels::{ComponentStatus, SourceEvent, SourceEventWrapper, SubscriptionResponse};
use drasi_lib::sources::base::SourceBase;
use drasi_lib::Source;
use std::collections::HashMap;
use std::future::Future;
use std::sync::Arc;
use tokio::sync::{mpsc, oneshot, RwLock};
use tracing::Instrument;
use crate::thread::SqliteCommand;
#[derive(Clone)]
pub struct SqliteSourceHandle {
command_tx: Arc<RwLock<Option<mpsc::UnboundedSender<SqliteCommand>>>>,
source_id: Arc<str>,
}
impl SqliteSourceHandle {
async fn sender(&self) -> Result<mpsc::UnboundedSender<SqliteCommand>> {
self.command_tx
.read()
.await
.clone()
.ok_or_else(|| anyhow!("source '{}' is not running", self.source_id))
}
pub fn source_id(&self) -> &str {
&self.source_id
}
pub async fn execute(&self, sql: impl Into<String>) -> Result<usize> {
let (response_tx, response_rx) = oneshot::channel();
self.sender().await?.send(SqliteCommand::Execute {
sql: sql.into(),
response_tx,
})?;
response_rx
.await
.map_err(|_| anyhow!("sqlite thread closed response channel"))?
}
pub async fn execute_parameterized(
&self,
sql: impl Into<String>,
params: Vec<SqliteParam>,
) -> Result<usize> {
let (response_tx, response_rx) = oneshot::channel();
self.sender()
.await?
.send(SqliteCommand::ExecuteParameterized {
sql: sql.into(),
params,
response_tx,
})?;
response_rx
.await
.map_err(|_| anyhow!("sqlite thread closed response channel"))?
}
pub async fn execute_batch(&self, sql: impl Into<String>) -> Result<()> {
let (response_tx, response_rx) = oneshot::channel();
self.sender().await?.send(SqliteCommand::ExecuteBatch {
sql: sql.into(),
response_tx,
})?;
response_rx
.await
.map_err(|_| anyhow!("sqlite thread closed response channel"))?
}
pub async fn query(
&self,
sql: impl Into<String>,
) -> Result<Vec<serde_json::Map<String, serde_json::Value>>> {
let (response_tx, response_rx) = oneshot::channel();
self.sender().await?.send(SqliteCommand::QueryRows {
sql: sql.into(),
response_tx,
})?;
response_rx
.await
.map_err(|_| anyhow!("sqlite thread closed response channel"))?
}
pub async fn query_parameterized(
&self,
sql: impl Into<String>,
params: Vec<SqliteParam>,
) -> Result<Vec<serde_json::Map<String, serde_json::Value>>> {
let (response_tx, response_rx) = oneshot::channel();
self.sender()
.await?
.send(SqliteCommand::QueryRowsParameterized {
sql: sql.into(),
params,
response_tx,
})?;
response_rx
.await
.map_err(|_| anyhow!("sqlite thread closed response channel"))?
}
async fn begin_transaction(&self) -> Result<()> {
let (response_tx, response_rx) = oneshot::channel();
self.sender()
.await?
.send(SqliteCommand::BeginTransaction { response_tx })?;
response_rx
.await
.map_err(|_| anyhow!("sqlite thread closed response channel"))?
}
async fn commit_transaction(&self) -> Result<()> {
let (response_tx, response_rx) = oneshot::channel();
self.sender()
.await?
.send(SqliteCommand::CommitTransaction { response_tx })?;
response_rx
.await
.map_err(|_| anyhow!("sqlite thread closed response channel"))?
}
async fn rollback_transaction(&self) -> Result<()> {
let (response_tx, response_rx) = oneshot::channel();
self.sender()
.await?
.send(SqliteCommand::RollbackTransaction { response_tx })?;
response_rx
.await
.map_err(|_| anyhow!("sqlite thread closed response channel"))?
}
pub async fn execute_statements_in_transaction(&self, statements: Vec<String>) -> Result<()> {
self.begin_transaction().await?;
for statement in statements {
if let Err(err) = self.execute(statement).await {
let _ = self.rollback_transaction().await;
return Err(err);
}
}
self.commit_transaction().await
}
pub async fn execute_parameterized_in_transaction(
&self,
statements: Vec<(String, Vec<SqliteParam>)>,
) -> Result<()> {
self.begin_transaction().await?;
for (sql, params) in statements {
if let Err(err) = self.execute_parameterized(sql, params).await {
let _ = self.rollback_transaction().await;
return Err(err);
}
}
self.commit_transaction().await
}
pub async fn transaction<F, Fut, T>(&self, f: F) -> Result<T>
where
F: FnOnce(SqliteTxHandle) -> Fut,
Fut: Future<Output = Result<T>>,
{
self.begin_transaction().await?;
let tx_handle = SqliteTxHandle {
handle: self.clone(),
};
match f(tx_handle).await {
Ok(value) => {
self.commit_transaction().await?;
Ok(value)
}
Err(err) => {
let _ = self.rollback_transaction().await;
Err(err)
}
}
}
}
#[derive(Clone)]
pub struct SqliteTxHandle {
handle: SqliteSourceHandle,
}
impl SqliteTxHandle {
pub async fn execute(&self, sql: impl Into<String>) -> Result<usize> {
self.handle.execute(sql).await
}
pub async fn execute_batch(&self, sql: impl Into<String>) -> Result<()> {
self.handle.execute_batch(sql).await
}
}
pub struct SqliteSource {
base: SourceBase,
config: SqliteSourceConfig,
command_tx: Arc<RwLock<Option<mpsc::UnboundedSender<SqliteCommand>>>>,
thread_handle: Arc<RwLock<Option<std::thread::JoinHandle<()>>>>,
rest_shutdown_tx: Arc<RwLock<Option<oneshot::Sender<()>>>>,
}
impl SqliteSource {
pub fn builder(id: impl Into<String>) -> SqliteSourceBuilder {
SqliteSourceBuilder::new(id)
}
pub(crate) fn from_parts(base: SourceBase, config: SqliteSourceConfig) -> Result<Self> {
Ok(Self {
base,
config,
command_tx: Arc::new(RwLock::new(None)),
thread_handle: Arc::new(RwLock::new(None)),
rest_shutdown_tx: Arc::new(RwLock::new(None)),
})
}
pub fn handle(&self) -> SqliteSourceHandle {
SqliteSourceHandle {
command_tx: self.command_tx.clone(),
source_id: Arc::from(self.base.id.as_str()),
}
}
}
#[async_trait]
impl Source for SqliteSource {
fn id(&self) -> &str {
&self.base.id
}
fn type_name(&self) -> &str {
"sqlite"
}
fn properties(&self) -> HashMap<String, serde_json::Value> {
use crate::descriptor::SqliteSourceConfigDto;
self.base
.properties_or_serialize(&SqliteSourceConfigDto::from(&self.config))
}
fn auto_start(&self) -> bool {
self.base.get_auto_start()
}
async fn start(&self) -> Result<()> {
if self.base.get_status().await == ComponentStatus::Running {
return Ok(());
}
self.base
.set_status(
ComponentStatus::Starting,
Some("Starting SQLite source".to_string()),
)
.await;
let (command_tx, command_rx) = mpsc::unbounded_channel::<SqliteCommand>();
*self.command_tx.write().await = Some(command_tx.clone());
let (event_tx, mut event_rx) = mpsc::unbounded_channel::<thread::ChangeEvent>();
let thread_config = thread::SqliteThreadConfig {
path: self.config.path.clone(),
tables: self.config.tables.clone(),
};
let source_id_for_thread = self.base.id.clone();
let handle = std::thread::spawn(move || {
if let Err(err) = thread::run_sqlite_thread(thread_config, command_rx, event_tx) {
log::error!("sqlite source thread '{source_id_for_thread}' failed: {err}");
}
});
*self.thread_handle.write().await = Some(handle);
let configured_keys = self
.config
.table_keys
.iter()
.map(|item| (item.table.clone(), item.key_columns.clone()))
.collect::<HashMap<_, _>>();
let source_id = self.base.id.clone();
let dispatchers = self.base.dispatchers.clone();
let instance_id = self
.base
.context()
.await
.map(|ctx| ctx.instance_id)
.unwrap_or_default();
let span = tracing::info_span!(
"sqlite_source_dispatcher",
instance_id = %instance_id,
component_id = %source_id,
component_type = "source"
);
let task = tokio::spawn(
async move {
while let Some(event) = event_rx.recv().await {
let configured = configured_keys.get(&event.table).map(|v| v.as_slice());
let change =
convert::change_event_to_source_change(event, &source_id, configured);
let mut profiling = drasi_lib::profiling::ProfilingMetadata::new();
profiling.source_send_ns = Some(drasi_lib::profiling::timestamp_ns());
let wrapper = SourceEventWrapper::with_profiling(
source_id.clone(),
SourceEvent::Change(change),
chrono::Utc::now(),
profiling,
);
if let Err(err) =
SourceBase::dispatch_from_task(dispatchers.clone(), wrapper, &source_id)
.await
{
log::debug!("failed dispatching sqlite change for '{source_id}': {err}");
}
}
}
.instrument(span),
);
*self.base.task_handle.write().await = Some(task);
if let Some(rest_config) = &self.config.rest_api {
let (shutdown_tx, shutdown_rx) = oneshot::channel();
rest_api::start_rest_api(
rest_config.clone(),
self.handle(),
self.config.tables.clone(),
self.config.table_keys.clone(),
shutdown_rx,
)
.await?;
*self.rest_shutdown_tx.write().await = Some(shutdown_tx);
}
self.base
.set_status(
ComponentStatus::Running,
Some("SQLite source running".to_string()),
)
.await;
Ok(())
}
async fn stop(&self) -> Result<()> {
if self.base.get_status().await != ComponentStatus::Running {
return Ok(());
}
self.base
.set_status(
ComponentStatus::Stopping,
Some("Stopping SQLite source".to_string()),
)
.await;
if let Some(rest_shutdown_tx) = self.rest_shutdown_tx.write().await.take() {
let _ = rest_shutdown_tx.send(());
}
if let Some(sender) = self.command_tx.write().await.take() {
let _ = sender.send(SqliteCommand::Shutdown);
}
if let Some(handle) = self.thread_handle.write().await.take() {
let _ = tokio::task::spawn_blocking(move || handle.join()).await;
}
if let Some(task) = self.base.task_handle.write().await.take() {
task.abort();
}
self.base
.set_status(
ComponentStatus::Stopped,
Some("SQLite source stopped".to_string()),
)
.await;
Ok(())
}
async fn status(&self) -> ComponentStatus {
self.base.get_status().await
}
async fn subscribe(
&self,
settings: drasi_lib::config::SourceSubscriptionSettings,
) -> Result<SubscriptionResponse> {
self.base
.subscribe_with_bootstrap(&settings, "SQLite")
.await
}
fn as_any(&self) -> &dyn std::any::Any {
self
}
async fn initialize(&self, context: drasi_lib::context::SourceRuntimeContext) {
self.base.initialize(context).await;
}
async fn set_bootstrap_provider(
&self,
provider: Box<dyn drasi_lib::bootstrap::BootstrapProvider + 'static>,
) {
self.base.set_bootstrap_provider(provider).await;
}
}
#[cfg(feature = "dynamic-plugin")]
drasi_plugin_sdk::export_plugin!(
plugin_id = "sqlite-source",
core_version = env!("CARGO_PKG_VERSION"),
lib_version = env!("CARGO_PKG_VERSION"),
plugin_version = env!("CARGO_PKG_VERSION"),
source_descriptors = [descriptor::SqliteSourceDescriptor],
reaction_descriptors = [],
bootstrap_descriptors = [],
);