1use super::assign::RunnableAssign;
9use super::config::RunnableConfig;
10use super::error::LcelError;
11use super::runnable_trait::Runnable;
12use async_trait::async_trait;
13use futures_util::future::join_all;
14use futures_util::Stream;
15use serde_json::Value;
16use std::collections::HashMap;
17use std::pin::Pin;
18use std::sync::Arc;
19use tokio::sync::Semaphore;
20
21pub struct RunnableParallel<I: Send + Sync + 'static> {
38 steps: Vec<(String, Arc<dyn ParallelStep<I>>)>,
39}
40
41impl<I: Send + Sync + 'static> std::fmt::Debug for RunnableParallel<I> {
42 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
43 let keys: Vec<&str> = self.steps.iter().map(|(k, _)| k.as_str()).collect();
44 f.debug_struct("RunnableParallel")
45 .field("steps", &keys)
46 .field("input", &std::any::type_name::<I>())
47 .finish()
48 }
49}
50
51impl<I: Clone + Send + Sync + 'static> Default for RunnableParallel<I> {
52 fn default() -> Self {
53 Self::new()
54 }
55}
56
57impl<I: Clone + Send + Sync + 'static> RunnableParallel<I> {
58 pub fn new() -> Self {
60 Self { steps: Vec::new() }
61 }
62
63 pub fn with<O, R>(mut self, key: &str, runnable: R) -> Self
68 where
69 O: serde::Serialize + Send + Sync + 'static,
70 R: Runnable<I, O> + Send + Sync + 'static,
71 R::Error: Into<LcelError>,
72 {
73 self.steps.push((
74 key.to_string(),
75 Arc::new(ParallelStepImpl {
76 inner: runnable,
77 serialize: |output: &O| serde_json::to_value(output),
78 _marker: std::marker::PhantomData,
79 }),
80 ));
81 self
82 }
83
84 pub fn len(&self) -> usize {
86 self.steps.len()
87 }
88
89 pub fn is_empty(&self) -> bool {
91 self.steps.is_empty()
92 }
93
94 pub fn assign<O, R>(self, key: &str, runnable: R) -> RunnableSequence<I, HashMap<String, Value>>
119 where
120 I: 'static,
121 O: serde::Serialize + Send + Sync + 'static,
122 R: Runnable<HashMap<String, Value>, O> + Send + Sync + 'static,
123 R::Error: Into<LcelError>,
124 {
125 use super::ext::RunnableExt;
126
127 let assign = RunnableAssign::new().with(key, runnable);
128 self.pipe(assign)
129 }
130}
131
132use super::sequence::RunnableSequence;
133
134#[async_trait]
136trait ParallelStep<I: Send + Sync + 'static>: Send + Sync {
137 async fn invoke(&self, input: I, config: Option<RunnableConfig>) -> Result<Value, LcelError>;
138}
139
140struct ParallelStepImpl<I, O, R>
142where
143 I: Send + Sync + 'static,
144 O: serde::Serialize + Send + Sync + 'static,
145 R: Runnable<I, O>,
146{
147 inner: R,
148 serialize: fn(&O) -> Result<Value, serde_json::Error>,
149 _marker: std::marker::PhantomData<I>,
150}
151
152#[async_trait]
153impl<I, O, R> ParallelStep<I> for ParallelStepImpl<I, O, R>
154where
155 I: Clone + Send + Sync + 'static,
156 O: serde::Serialize + Send + Sync + 'static,
157 R: Runnable<I, O>,
158 R::Error: Into<LcelError>,
159{
160 async fn invoke(&self, input: I, config: Option<RunnableConfig>) -> Result<Value, LcelError> {
161 let result = self.inner.invoke(input, config).await.map_err(Into::into)?;
162 (self.serialize)(&result)
163 .map_err(|e| LcelError::Other(format!("parallel serialization: {}", e)))
164 }
165}
166
167#[async_trait]
168impl<I: Clone + Send + Sync + 'static> Runnable<I, HashMap<String, Value>> for RunnableParallel<I> {
169 type Error = LcelError;
170
171 async fn invoke(
178 &self,
179 input: I,
180 config: Option<RunnableConfig>,
181 ) -> Result<HashMap<String, Value>, LcelError> {
182 let limit = config
183 .as_ref()
184 .and_then(|c| c.max_concurrency)
185 .unwrap_or(self.steps.len())
186 .max(1);
187 let semaphore = Arc::new(Semaphore::new(limit));
188
189 let mut handles = Vec::with_capacity(self.steps.len());
190
191 for (key, step) in &self.steps {
192 let key = key.clone();
193 let step = step.clone();
194 let input = input.clone();
195 let config = config.clone();
196 let sem = semaphore.clone();
197
198 let handle = tokio::spawn(async move {
199 let _permit = sem
202 .acquire()
203 .await
204 .map_err(|e| LcelError::Other(format!("parallel semaphore: {e}")))?;
205 let value = step.invoke(input, config).await?;
206 Ok::<(String, Value), LcelError>((key, value))
207 });
208
209 handles.push(handle);
210 }
211
212 let joined = join_all(handles).await;
213 let mut results = HashMap::new();
214 for res in joined {
215 let inner =
217 res.map_err(|e| LcelError::Other(format!("parallel task join error: {e}")))?;
218 let (k, v) = inner?;
219 results.insert(k, v);
220 }
221
222 Ok(results)
223 }
224
225 async fn batch(
227 &self,
228 inputs: Vec<I>,
229 config: Option<RunnableConfig>,
230 ) -> Result<Vec<HashMap<String, Value>>, LcelError> {
231 let mut results = Vec::with_capacity(inputs.len());
232 for input in inputs {
233 results.push(self.invoke(input, config.clone()).await?);
234 }
235 Ok(results)
236 }
237
238 async fn stream(
240 &self,
241 input: I,
242 config: Option<RunnableConfig>,
243 ) -> Result<
244 Pin<Box<dyn Stream<Item = Result<HashMap<String, Value>, LcelError>> + Send>>,
245 LcelError,
246 > {
247 let result = self.invoke(input, config).await?;
248 Ok(Box::pin(futures_util::stream::once(
249 async move { Ok(result) },
250 )))
251 }
252}
253
254#[cfg(test)]
255mod tests {
256 use super::*;
257 use crate::RunnableLambda;
258
259 #[tokio::test]
260 async fn parallel_invoke() {
261 let parallel = RunnableParallel::<String>::new()
262 .with("len", RunnableLambda::new_sync(|s: String| s.len() as i64))
263 .with(
264 "upper",
265 RunnableLambda::new_sync(|s: String| s.to_uppercase()),
266 );
267
268 let result = parallel.invoke("hello".to_string(), None).await.unwrap();
269 assert_eq!(
270 result.get("len").unwrap(),
271 &Value::Number(serde_json::Number::from(5))
272 );
273 assert_eq!(
274 result.get("upper").unwrap(),
275 &Value::String("HELLO".to_string())
276 );
277 }
278
279 #[tokio::test]
280 async fn parallel_empty() {
281 let parallel = RunnableParallel::<i32>::new();
282 let result = parallel.invoke(42, None).await.unwrap();
283 assert!(result.is_empty());
284 }
285
286 #[tokio::test]
287 async fn parallel_batch() {
288 let parallel = RunnableParallel::<String>::new()
289 .with("len", RunnableLambda::new_sync(|s: String| s.len() as i64));
290
291 let results = parallel
292 .batch(vec!["hi".to_string(), "hello".to_string()], None)
293 .await
294 .unwrap();
295 assert_eq!(results.len(), 2);
296 assert_eq!(
297 results[0].get("len").unwrap(),
298 &Value::Number(serde_json::Number::from(2))
299 );
300 assert_eq!(
301 results[1].get("len").unwrap(),
302 &Value::Number(serde_json::Number::from(5))
303 );
304 }
305
306 #[tokio::test]
307 async fn parallel_assign_adds_field() {
308 let chain = RunnableParallel::<String>::new()
309 .with("len", RunnableLambda::new_sync(|s: String| s.len() as i64))
310 .assign(
311 "upper",
312 RunnableLambda::new_sync(|m: HashMap<String, Value>| {
313 m.get("len")
315 .and_then(|v| v.as_i64())
316 .map(|n| format!("length={}", n))
317 .unwrap_or_default()
318 }),
319 );
320
321 let result = chain.invoke("hello".to_string(), None).await.unwrap();
322 assert_eq!(
324 result.get("len").unwrap(),
325 &Value::Number(serde_json::Number::from(5))
326 );
327 assert_eq!(
329 result.get("upper").unwrap(),
330 &Value::String("length=5".to_string())
331 );
332 }
333
334 #[tokio::test]
335 async fn parallel_respects_max_concurrency() {
336 use std::sync::atomic::{AtomicUsize, Ordering};
337 use std::time::Duration;
338
339 let in_flight = Arc::new(AtomicUsize::new(0));
340 let peak = Arc::new(AtomicUsize::new(0));
341
342 let mk = |in_flight: Arc<AtomicUsize>, peak: Arc<AtomicUsize>| {
343 RunnableLambda::new_async(move |_: String| {
344 let a = in_flight.clone();
345 let b = peak.clone();
346 async move {
347 let cur = a.fetch_add(1, Ordering::SeqCst) + 1;
348 b.fetch_max(cur, Ordering::SeqCst);
349 tokio::time::sleep(Duration::from_millis(20)).await;
350 a.fetch_sub(1, Ordering::SeqCst);
351 Ok::<i32, LcelError>(1)
352 }
353 })
354 };
355
356 let parallel = RunnableParallel::<String>::new()
357 .with("a", mk(in_flight.clone(), peak.clone()))
358 .with("b", mk(in_flight.clone(), peak.clone()))
359 .with("c", mk(in_flight.clone(), peak.clone()))
360 .with("d", mk(in_flight.clone(), peak.clone()));
361
362 let config = RunnableConfig::new().with_max_concurrency(2);
363 let result = parallel
364 .invoke("x".to_string(), Some(config))
365 .await
366 .unwrap();
367 assert_eq!(result.len(), 4);
368
369 assert!(
371 peak.load(Ordering::SeqCst) <= 2,
372 "peak concurrency {} exceeded cap 2",
373 peak.load(Ordering::SeqCst)
374 );
375 }
376
377 #[tokio::test]
383 async fn parallel_failure_waits_for_other_steps_instead_of_orphaning() {
384 use std::sync::atomic::{AtomicUsize, Ordering};
385 use std::time::{Duration, Instant};
386
387 let completed = Arc::new(AtomicUsize::new(0));
388
389 let slow = |completed: Arc<AtomicUsize>| {
390 RunnableLambda::new_async(move |_: String| {
391 let done = completed.clone();
392 async move {
393 tokio::time::sleep(Duration::from_millis(60)).await;
394 done.fetch_add(1, Ordering::SeqCst);
395 Ok::<i32, LcelError>(1)
396 }
397 })
398 };
399
400 let failing = RunnableLambda::new_async(|_: String| async move {
401 Err::<i32, LcelError>(LcelError::Other("deliberate step failure".to_string()))
403 });
404
405 let parallel = RunnableParallel::<String>::new()
406 .with("a", slow(completed.clone()))
407 .with("boom", failing)
408 .with("c", slow(completed.clone()))
409 .with("d", slow(completed.clone()));
410
411 let start = Instant::now();
412 let err = parallel.invoke("x".to_string(), None).await.unwrap_err();
413 let elapsed = start.elapsed();
414
415 assert!(
416 err.to_string().contains("deliberate step failure"),
417 "expected the step error, got: {err}"
418 );
419 assert_eq!(
422 completed.load(Ordering::SeqCst),
423 3,
424 "surviving steps must finish before invoke returns the error"
425 );
426 assert!(
429 elapsed >= Duration::from_millis(45),
430 "invoke returned after {elapsed:?} — orphaned steps were not awaited"
431 );
432 }
433}