tinyflow_framework/runtime/chained_stream_task/
mod.rs1mod 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
22pub 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}