tinyflow-framework 0.1.1

Streaming runtime engine — checkpoint, chained task execution, and state store
Documentation
use std::path::PathBuf;
use std::sync::Arc;

use crate::env::stream_env::StreamEnv;
use crate::runtime::chain_job::{ChainJob, ChainOperatorKind};
use crate::state::open_job_state_db;
use tinyflow_api::error::{StreamError, StreamResult};
use tinyflow_api::state::{StateStore, in_memory_job_state};

pub(crate) async fn resolve_job_state_for_execute(
    env: &StreamEnv,
) -> StreamResult<(Arc<dyn StateStore>, Option<PathBuf>)> {
    let needs_persistent = env.checkpoint_interval_ms > 0 || env.enable_chain_state;
    if needs_persistent {
        let dir = env.state_work_dir.as_ref().ok_or_else(|| StreamError::TaskFailed {
            task_id: 0,
            reason: "set_state_work_dir required when checkpoint_interval_ms > 0 or enable_chain_state is true".into(),
        })?;
        Ok((open_job_state_db(dir).await?, Some(dir.clone())))
    } else {
        Ok((in_memory_job_state(), env.state_work_dir.clone()))
    }
}

pub(crate) fn resolve_step_name(kind: ChainOperatorKind, user_name: &Option<String>) -> String {
    user_name.clone().unwrap_or_else(|| match kind {
        ChainOperatorKind::Source => "Source".into(),
        ChainOperatorKind::Map => "Map".into(),
        ChainOperatorKind::Filter => "Filter".into(),
        ChainOperatorKind::FlatMap => "FlatMap".into(),
        ChainOperatorKind::Sink => "Sink".into(),
    })
}

pub(crate) fn validate_chain_job(env: &StreamEnv, job: &ChainJob) -> StreamResult<()> {
    if job.steps.is_empty() {
        return Err(StreamError::TaskFailed {
            task_id: 0,
            reason: "ChainJob has no steps".into(),
        });
    }
    if job.steps[0].kind != ChainOperatorKind::Source {
        return Err(StreamError::TaskFailed {
            task_id: 0,
            reason: "ChainJob must start with Source".into(),
        });
    }
    let expected_par = env.default_parallelism.max(1);
    if job.parallelism != expected_par {
        return Err(StreamError::TaskFailed {
            task_id: 0,
            reason: format!(
                "ChainJob parallelism {} != env.set_parallelism({})",
                job.parallelism, expected_par
            ),
        });
    }
    if env.checkpoint_interval_ms > 0 {
        let tail = job.steps.last().expect("non-empty chain");
        if tail.kind != ChainOperatorKind::Sink {
            return Err(StreamError::TaskFailed {
                task_id: 0,
                reason: "checkpoint enabled requires terminal Sink step (DataStream::sink or add_iceberg_sink)".into(),
            });
        }
    }
    Ok(())
}

#[cfg(test)]
mod tests {
    use super::*;
    use crate::runtime::chain_job::ChainOperatorKind;

    #[test]
    fn resolve_step_name_uses_pascal_case_defaults() {
        assert_eq!(resolve_step_name(ChainOperatorKind::Map, &None), "Map");
        assert_eq!(
            resolve_step_name(ChainOperatorKind::Filter, &None),
            "Filter"
        );
        assert_eq!(
            resolve_step_name(ChainOperatorKind::Source, &None),
            "Source"
        );
    }

    #[test]
    fn resolve_step_name_prefers_user_name() {
        assert_eq!(
            resolve_step_name(ChainOperatorKind::Map, &Some("my_map".into())),
            "my_map"
        );
    }
}