use std::fmt;
use std::pin::Pin;
use std::sync::Arc;
use std::sync::atomic::{AtomicBool, AtomicUsize, Ordering};
use std::task::{Context, Poll};
use chrono::Utc;
use datafusion::arrow::array::RecordBatch;
use datafusion::arrow::datatypes::SchemaRef;
use datafusion::error::Result;
use datafusion::execution::{RecordBatchStream, SendableRecordBatchStream, TaskContext};
use datafusion::physical_plan::metrics::MetricsSet;
use datafusion::physical_plan::{DisplayAs, DisplayFormatType, ExecutionPlan, PlanProperties};
use futures::Stream;
use crate::client::OpenLineageClient;
use crate::event::{RunEvent, RunEventType};
use crate::facets::{
BaseFacet, ErrorMessageRunFacet, InputStatisticsInputDatasetFacet,
OutputStatisticsOutputDatasetFacet,
};
const ERROR_FACET: &str = "1-0-1/ErrorMessageRunFacet.json";
const OUTPUT_STATS_FACET: &str = "1-0-2/OutputStatisticsOutputDatasetFacet.json";
const INPUT_STATS_FACET: &str = "1-0-0/InputStatisticsInputDatasetFacet.json";
struct RunState {
client: OpenLineageClient,
complete: RunEvent,
producer: String,
inner: std::sync::Mutex<Arc<dyn ExecutionPlan>>,
remaining: AtomicUsize,
init: std::sync::Once,
failed: AtomicBool,
error: std::sync::Mutex<Option<String>>,
has_outputs: bool,
rows_written: std::sync::Mutex<Option<u64>>,
emitted: AtomicBool,
}
impl RunState {
fn init_partitions(&self, count: usize) {
self.init.call_once(|| {
self.remaining.store(count.max(1), Ordering::SeqCst);
});
}
fn record_error(&self, message: String) {
self.failed.store(true, Ordering::SeqCst);
let mut slot = self.error.lock().unwrap();
if slot.is_none() {
*slot = Some(message);
}
}
fn record_rows_written(&self, rows: u64) {
let mut slot = self.rows_written.lock().unwrap();
*slot = Some(slot.unwrap_or(0) + rows);
}
fn partition_finished(&self) {
if self.remaining.fetch_sub(1, Ordering::SeqCst) != 1 {
return;
}
self.emit_terminal();
}
fn emit_terminal(&self) {
if self.emitted.swap(true, Ordering::SeqCst) {
return;
}
let mut event = self.complete.clone();
event.event_time = Utc::now().to_rfc3339();
if self.failed.load(Ordering::SeqCst) {
event.event_type = RunEventType::Fail;
let message = self
.error
.lock()
.unwrap()
.clone()
.unwrap_or_else(|| "query failed".to_string());
event.run.facets.error_message = Some(ErrorMessageRunFacet {
base: BaseFacet::new(&self.producer, ERROR_FACET),
message,
programming_language: "Rust".to_string(),
stack_trace: None,
});
} else {
self.attach_output_statistics(&mut event);
self.attach_input_statistics(&mut event);
}
self.client.emit(event);
}
fn attach_output_statistics(&self, event: &mut RunEvent) {
if event.outputs.is_empty() {
return;
}
let row_count = self.rows_written.lock().unwrap().map(|n| n as i64);
let size = self
.inner
.lock()
.unwrap()
.metrics()
.and_then(|m| m.aggregate_by_name().sum_by_name("bytes_scanned"))
.map(|v| v.as_usize() as i64);
if row_count.is_none() && size.is_none() {
return;
}
let stats = OutputStatisticsOutputDatasetFacet {
base: BaseFacet::new(&self.producer, OUTPUT_STATS_FACET),
row_count,
size,
file_count: None,
};
for output in &mut event.outputs {
let facets = output.output_facets.get_or_insert_with(Default::default);
facets.output_statistics = Some(stats.clone());
}
}
fn attach_input_statistics(&self, event: &mut RunEvent) {
if event.inputs.len() != 1 {
return;
}
let inner = self.inner.lock().unwrap().clone();
if count_leaf_scans(&inner) != 1 {
tracing::trace!(
target: "openlineage",
"skipping inputStatistics: dataset scanned by multiple leaf nodes"
);
return;
}
let (rows, bytes) = aggregate_scan_metrics(&inner);
let row_count = rows.map(|n| n as i64);
let size = bytes.map(|n| n as i64);
if row_count.is_none() && size.is_none() {
return;
}
let stats = InputStatisticsInputDatasetFacet {
base: BaseFacet::new(&self.producer, INPUT_STATS_FACET),
row_count,
size,
file_count: None,
};
let facets = event.inputs[0]
.input_facets
.get_or_insert_with(Default::default);
facets.input_statistics = Some(stats);
}
}
fn aggregate_scan_metrics(plan: &Arc<dyn ExecutionPlan>) -> (Option<usize>, Option<usize>) {
let mut rows: Option<usize> = None;
let mut bytes: Option<usize> = None;
if let Some(metrics) = plan.metrics() {
let metrics = metrics.aggregate_by_name();
if plan.children().is_empty() {
if let Some(r) = metrics.output_rows() {
*rows.get_or_insert(0) += r;
}
if let Some(b) = metrics.sum_by_name("bytes_scanned") {
*bytes.get_or_insert(0) += b.as_usize();
}
}
}
for child in plan.children() {
let (cr, cb) = aggregate_scan_metrics(child);
if let Some(r) = cr {
*rows.get_or_insert(0) += r;
}
if let Some(b) = cb {
*bytes.get_or_insert(0) += b;
}
}
(rows, bytes)
}
fn count_leaf_scans(plan: &Arc<dyn ExecutionPlan>) -> usize {
let children = plan.children();
if children.is_empty() {
return 1;
}
children.iter().map(|child| count_leaf_scans(child)).sum()
}
pub struct OpenLineageExec {
inner: Arc<dyn ExecutionPlan>,
state: Arc<RunState>,
}
impl OpenLineageExec {
pub fn new(
inner: Arc<dyn ExecutionPlan>,
client: OpenLineageClient,
complete: RunEvent,
producer: String,
) -> Arc<Self> {
let has_outputs = !complete.outputs.is_empty();
let state = Arc::new(RunState {
client,
complete,
producer,
has_outputs,
inner: std::sync::Mutex::new(inner.clone()),
remaining: AtomicUsize::new(0),
init: std::sync::Once::new(),
failed: AtomicBool::new(false),
error: std::sync::Mutex::new(None),
rows_written: std::sync::Mutex::new(None),
emitted: AtomicBool::new(false),
});
Arc::new(Self { inner, state })
}
fn with_new_inner(&self, inner: Arc<dyn ExecutionPlan>) -> Arc<Self> {
*self.state.inner.lock().unwrap() = inner.clone();
Arc::new(Self {
inner,
state: self.state.clone(),
})
}
}
impl fmt::Debug for OpenLineageExec {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.debug_struct("OpenLineageExec").finish_non_exhaustive()
}
}
impl DisplayAs for OpenLineageExec {
fn fmt_as(&self, t: DisplayFormatType, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match t {
DisplayFormatType::Default | DisplayFormatType::Verbose => {
write!(f, "OpenLineageExec")
}
DisplayFormatType::TreeRender => write!(f, "OpenLineageExec"),
}
}
}
impl ExecutionPlan for OpenLineageExec {
fn name(&self) -> &str {
"OpenLineageExec"
}
fn properties(&self) -> &Arc<PlanProperties> {
self.inner.properties()
}
fn children(&self) -> Vec<&Arc<dyn ExecutionPlan>> {
vec![&self.inner]
}
fn with_new_children(
self: Arc<Self>,
mut children: Vec<Arc<dyn ExecutionPlan>>,
) -> Result<Arc<dyn ExecutionPlan>> {
let child = children.pop().unwrap_or_else(|| self.inner.clone());
Ok(self.with_new_inner(child))
}
fn metrics(&self) -> Option<MetricsSet> {
self.inner.metrics()
}
fn execute(
&self,
partition: usize,
context: Arc<TaskContext>,
) -> Result<SendableRecordBatchStream> {
self.state
.init_partitions(self.properties().output_partitioning().partition_count());
let inner = match self.inner.execute(partition, context) {
Ok(inner) => inner,
Err(err) => {
self.state.record_error(err.to_string());
self.state.partition_finished();
return Err(err);
}
};
Ok(Box::pin(TrackedStream {
schema: inner.schema(),
inner,
state: self.state.clone(),
done: false,
}))
}
}
struct TrackedStream {
schema: SchemaRef,
inner: SendableRecordBatchStream,
state: Arc<RunState>,
done: bool,
}
impl TrackedStream {
fn finish(&mut self) {
if !self.done {
self.done = true;
self.state.partition_finished();
}
}
}
impl Stream for TrackedStream {
type Item = Result<RecordBatch>;
fn poll_next(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Option<Self::Item>> {
match Pin::new(&mut self.inner).poll_next(cx) {
Poll::Ready(Some(Ok(batch))) => {
if self.state.has_outputs
&& let Some(rows) = write_count(&batch)
{
self.state.record_rows_written(rows);
}
Poll::Ready(Some(Ok(batch)))
}
Poll::Ready(Some(Err(e))) => {
self.state.record_error(e.to_string());
self.finish();
Poll::Ready(Some(Err(e)))
}
Poll::Ready(None) => {
self.finish();
Poll::Ready(None)
}
Poll::Pending => Poll::Pending,
}
}
}
impl RecordBatchStream for TrackedStream {
fn schema(&self) -> SchemaRef {
self.schema.clone()
}
}
impl Drop for TrackedStream {
fn drop(&mut self) {
if !self.done {
self.state
.record_error("query stream dropped before completion".to_string());
self.finish();
}
}
}
fn write_count(batch: &RecordBatch) -> Option<u64> {
use datafusion::arrow::array::{Array, UInt64Array};
if batch.num_columns() != 1 || batch.schema().field(0).name() != "count" {
return None;
}
let counts = batch.column(0).as_any().downcast_ref::<UInt64Array>()?;
Some(counts.iter().flatten().sum())
}