Skip to main content

tinyflow_framework/runtime/chained_stream_task/
mod.rs

1//! In-process operator chain (framework-only; no connector channel types).
2
3mod run_loop;
4
5#[cfg(test)]
6mod tests;
7
8use std::sync::Arc;
9
10use crate::runtime::chain_segment::{
11    ChainSegmentFactory, ChainSourceSegment, ChainStepKindInner, ChainTailSegment, NamedStep,
12    instantiate_chain_steps,
13};
14use crate::runtime::control_msg::{ControlMsg, ControlReceiver};
15use tinyflow_api::checkpoint::CheckpointContext;
16use tinyflow_api::context::RuntimeContext;
17use tinyflow_api::error::StreamResult;
18use tinyflow_api::state::StateStore;
19
20use run_loop::run_through_chain;
21
22/// In-process operator chain for a single subtask.
23pub struct ChainedStreamTask {
24    task_id: u32,
25    task_name: String,
26    subtask_index: u32,
27    chain_id: u64,
28    parallelism: u32,
29    source: Option<Box<dyn ChainSourceSegment>>,
30    tail_steps: Vec<Box<dyn ChainTailSegment>>,
31    step_names: Vec<String>,
32    last_checkpoint_metrics_at_ms: u64,
33    metric_key: String,
34    control_rx: ControlReceiver,
35    ack_tx: tokio::sync::mpsc::Sender<ControlMsg>,
36    job_state: Arc<dyn StateStore>,
37    chain_state: Option<Arc<dyn StateStore>>,
38}
39
40impl ChainedStreamTask {
41    #[allow(clippy::too_many_arguments)]
42    pub fn from_factories(
43        task_id: u32,
44        task_name: impl Into<String>,
45        subtask_index: u32,
46        chain_id: u64,
47        parallelism: u32,
48        factories: &[ChainSegmentFactory],
49        control_rx: ControlReceiver,
50        ack_tx: tokio::sync::mpsc::Sender<ControlMsg>,
51        job_state: Arc<dyn StateStore>,
52        chain_state: Option<Arc<dyn StateStore>>,
53    ) -> StreamResult<Self> {
54        Self::from_steps(
55            task_id,
56            task_name,
57            subtask_index,
58            chain_id,
59            parallelism,
60            instantiate_chain_steps(factories),
61            control_rx,
62            ack_tx,
63            job_state,
64            chain_state,
65        )
66    }
67
68    #[allow(clippy::too_many_arguments)]
69    pub fn from_steps(
70        task_id: u32,
71        task_name: impl Into<String>,
72        subtask_index: u32,
73        chain_id: u64,
74        parallelism: u32,
75        steps: Vec<NamedStep>,
76        control_rx: ControlReceiver,
77        ack_tx: tokio::sync::mpsc::Sender<ControlMsg>,
78        job_state: Arc<dyn StateStore>,
79        chain_state: Option<Arc<dyn StateStore>>,
80    ) -> StreamResult<Self> {
81        let mut source = None;
82        let mut tail_steps = Vec::new();
83        let mut step_names = Vec::new();
84        for step in steps {
85            step_names.push(step.name);
86            match step.kind {
87                ChainStepKindInner::Source(s) => {
88                    if source.is_some() {
89                        return Err(tinyflow_api::error::StreamError::TaskFailed {
90                            task_id,
91                            reason: "chain has multiple source steps".into(),
92                        });
93                    }
94                    source = Some(s);
95                }
96                ChainStepKindInner::Tail(o) => tail_steps.push(o),
97            }
98        }
99        if source.is_none() {
100            return Err(tinyflow_api::error::StreamError::TaskFailed {
101                task_id,
102                reason: "chain missing source head".into(),
103            });
104        }
105        Ok(Self {
106            task_id,
107            task_name: task_name.into(),
108            subtask_index,
109            chain_id,
110            parallelism,
111            source,
112            tail_steps,
113            step_names,
114            last_checkpoint_metrics_at_ms: 0,
115            metric_key: format!("metric/{}", subtask_index),
116            control_rx,
117            ack_tx,
118            job_state,
119            chain_state,
120        })
121    }
122
123    async fn open_all(&mut self, ctx: &mut RuntimeContext) -> StreamResult<()> {
124        if let Some(s) = &mut self.source {
125            s.open(ctx).await?;
126        }
127        for step in &mut self.tail_steps {
128            step.open(ctx).await?;
129        }
130        Ok(())
131    }
132
133    async fn close_all(&mut self, ctx: &mut RuntimeContext) -> StreamResult<()> {
134        for step in &mut self.tail_steps {
135            step.close(ctx).await?;
136        }
137        if let Some(s) = &mut self.source {
138            s.close(ctx).await?;
139        }
140        Ok(())
141    }
142
143    async fn on_checkpoint_all(
144        &mut self,
145        runtime: &mut RuntimeContext,
146        ckpt: &CheckpointContext,
147    ) -> StreamResult<()> {
148        if let Some(s) = &mut self.source {
149            s.on_checkpoint(runtime, ckpt).await?;
150        }
151        for step in &mut self.tail_steps {
152            step.on_checkpoint(runtime, ckpt).await?;
153        }
154        Ok(())
155    }
156
157    async fn on_checkpoint_end_all(
158        &mut self,
159        runtime: &mut RuntimeContext,
160        ckpt: &CheckpointContext,
161    ) -> StreamResult<()> {
162        if let Some(s) = &mut self.source {
163            s.on_checkpoint_end(runtime, ckpt).await?;
164        }
165        for step in &mut self.tail_steps {
166            step.on_checkpoint_end(runtime, ckpt).await?;
167        }
168        self.collect_and_store_metrics(runtime, ckpt).await?;
169        Ok(())
170    }
171
172    async fn collect_and_store_metrics(
173        &mut self,
174        runtime: &mut RuntimeContext,
175        ckpt: &CheckpointContext,
176    ) -> StreamResult<()> {
177        let now_ms = ckpt.timestamp_ms;
178        let elapsed_ms = now_ms.saturating_sub(self.last_checkpoint_metrics_at_ms);
179        self.last_checkpoint_metrics_at_ms = now_ms;
180
181        if elapsed_ms == 0 {
182            return Ok(());
183        }
184
185        let mut all_metrics = serde_json::Map::new();
186        let mut idx = 0;
187
188        let mut add_entry =
189            |label: String,
190             input_delta: u64,
191             output_delta: u64,
192             custom: std::collections::HashMap<String, f64>| {
193                let input_rate = (input_delta as f64 / elapsed_ms as f64) * 1000.0;
194                let output_rate = (output_delta as f64 / elapsed_ms as f64) * 1000.0;
195                let mut entry = serde_json::json!({
196                    "input_rate": input_rate,
197                    "output_rate": output_rate,
198                });
199                for (k, v) in custom {
200                    if v.is_nan() || v.is_infinite() {
201                        log::warn!("metric value is {v}, replacing with 0.0");
202                        entry[k] =
203                            serde_json::Value::Number(serde_json::Number::from_f64(0.0).unwrap());
204                    } else {
205                        entry[k] = serde_json::Value::Number(
206                            serde_json::Number::from_f64(v)
207                                .unwrap_or_else(|| serde_json::Number::from_f64(0.0).unwrap()),
208                        );
209                    }
210                }
211                all_metrics.insert(label, entry);
212            };
213
214        if let Some(s) = &mut self.source {
215            let label = if idx < self.step_names.len() {
216                format!("{}/{}", self.step_names[idx], self.subtask_index)
217            } else {
218                format!("source/{}", self.subtask_index)
219            };
220            idx += 1;
221            let (input_delta, output_delta) = s.drain_counters();
222            add_entry(label, input_delta, output_delta, s.collect_metrics());
223        }
224
225        for step in &mut self.tail_steps {
226            let label = if idx < self.step_names.len() {
227                format!("{}/{}", self.step_names[idx], self.subtask_index)
228            } else {
229                format!("tail/{}", self.subtask_index)
230            };
231            idx += 1;
232            let (input_delta, output_delta) = step.drain_counters();
233            add_entry(label, input_delta, output_delta, step.collect_metrics());
234        }
235
236        let json_bytes = serde_json::to_vec(&all_metrics).map_err(|e| {
237            tinyflow_api::error::StreamError::TaskFailed {
238                task_id: self.task_id,
239                reason: format!("serialize metrics: {e}"),
240            }
241        })?;
242
243        let metric_key = &self.metric_key;
244        runtime
245            .job_state
246            .put("job_status", metric_key.as_bytes(), &json_bytes)
247            .await
248            .map_err(|e| tinyflow_api::error::StreamError::TaskFailed {
249                task_id: self.task_id,
250                reason: format!("persist metrics: {e}"),
251            })?;
252
253        Ok(())
254    }
255
256    async fn handle_checkpoint(
257        &mut self,
258        ckpt_id: u64,
259        runtime: &mut RuntimeContext,
260    ) -> StreamResult<()> {
261        let timestamp = std::time::SystemTime::now()
262            .duration_since(std::time::UNIX_EPOCH)
263            .unwrap_or_default()
264            .as_millis() as u64;
265        let ckpt_ctx = CheckpointContext::new(ckpt_id, timestamp);
266        self.on_checkpoint_all(runtime, &ckpt_ctx).await?;
267
268        self.ack_tx
269            .send(ControlMsg::Ack(ckpt_id, self.task_id))
270            .await
271            .map_err(|_| tinyflow_api::error::StreamError::ChannelClosed {
272                reason: format!("ack_tx for task {} at checkpoint {}", self.task_id, ckpt_id),
273            })?;
274        Ok(())
275    }
276
277    async fn run_loop(&mut self, ctx: &mut RuntimeContext) -> StreamResult<()> {
278        loop {
279            tokio::select! {
280                ctrl = self.control_rx.recv() => {
281                    match ctrl {
282                        Some(ControlMsg::Checkpoint(ckpt_id)) => {
283                            self.handle_checkpoint(ckpt_id, ctx).await?;
284                        }
285                        Some(ControlMsg::CheckpointEnd(id)) => {
286                            let timestamp = std::time::SystemTime::now()
287                                .duration_since(std::time::UNIX_EPOCH)
288                                .unwrap_or_default()
289                                .as_millis() as u64;
290                            let ckpt = CheckpointContext::new(id, timestamp);
291                            self.on_checkpoint_end_all(ctx, &ckpt).await?;
292                        }
293                        Some(ControlMsg::Ack(_, _)) => {}
294                        None => break,
295                    }
296                }
297                record = async {
298                    self.source
299                        .as_mut()
300                        .expect("chain source present")
301                        .poll_next()
302                        .await
303                } => {
304                    match record? {
305                        Some(r) => {
306                            run_through_chain(&mut self.tail_steps, r, ctx).await?;
307                        }
308                        None => {
309                            tokio::task::yield_now().await;
310                        }
311                    }
312                }
313            }
314        }
315        Ok(())
316    }
317
318    pub async fn run(mut self) -> StreamResult<()> {
319        log::info!(
320            "[{}] chained task running (task_id={}, chain={}, parallelism={})",
321            self.task_name,
322            self.task_id,
323            self.chain_id,
324            self.parallelism
325        );
326        let mut ctx = RuntimeContext::new(
327            self.task_id,
328            self.task_name.clone(),
329            self.subtask_index,
330            self.parallelism,
331            self.chain_id,
332            Arc::clone(&self.job_state),
333            self.chain_state.as_ref().map(Arc::clone),
334        );
335        self.open_all(&mut ctx).await?;
336        let result = self.run_loop(&mut ctx).await;
337        if let Err(e) = self.close_all(&mut ctx).await {
338            log::warn!("[{}] close_all failed: {e}", self.task_name);
339        }
340        result?;
341        log::info!("[{}] chained task finished", self.task_name);
342        Ok(())
343    }
344}