use super::RunnableConfig;
use async_trait::async_trait;
use futures_util::Stream;
use std::pin::Pin;
#[async_trait]
pub trait Runnable<Input: Send + Sync + 'static, Output: Send + Sync + 'static>:
Send + Sync
{
type Error: std::error::Error + Send + Sync + 'static;
async fn invoke(
&self,
input: Input,
config: Option<RunnableConfig>,
) -> Result<Output, Self::Error>;
async fn batch(
&self,
inputs: Vec<Input>,
config: Option<RunnableConfig>,
) -> Result<Vec<Output>, Self::Error> {
use futures_util::StreamExt;
let limit = config
.as_ref()
.and_then(|c| c.max_concurrency)
.unwrap_or(inputs.len())
.max(1);
let results = futures_util::stream::iter(inputs)
.map(|input| {
let config = config.clone();
async move { self.invoke(input, config).await }
})
.buffered(limit)
.collect::<Vec<Result<_, _>>>()
.await;
results.into_iter().collect()
}
async fn batch_as_completed(
&self,
inputs: Vec<Input>,
config: Option<RunnableConfig>,
) -> Result<Vec<(usize, Output)>, Self::Error> {
use futures_util::StreamExt;
let limit = config
.as_ref()
.and_then(|c| c.max_concurrency)
.unwrap_or(inputs.len())
.max(1);
let results = futures_util::stream::iter(inputs.into_iter().enumerate())
.map(|(index, input)| {
let config = config.clone();
async move {
self.invoke(input, config).await.map(|output| (index, output))
}
})
.buffer_unordered(limit)
.collect::<Vec<Result<(usize, Output), _>>>()
.await;
results.into_iter().collect()
}
async fn stream(
&self,
input: Input,
config: Option<RunnableConfig>,
) -> Result<Pin<Box<dyn Stream<Item = Result<Output, Self::Error>> + Send>>, Self::Error> {
let result = self.invoke(input, config).await?;
let stream = futures_util::stream::once(async move { Ok(result) });
Ok(Box::pin(stream))
}
async fn transform(
&self,
input: Pin<Box<dyn Stream<Item = Result<Input, Self::Error>> + Send>>,
config: Option<RunnableConfig>,
) -> Result<Pin<Box<dyn Stream<Item = Result<Output, Self::Error>> + Send>>, Self::Error> {
use futures_util::StreamExt;
let mut items = Vec::new();
let mut input = input;
while let Some(item) = input.next().await {
items.push(item?);
}
let mut per_item_streams = Vec::with_capacity(items.len());
for item in items {
let stream = self.stream(item, config.clone()).await?;
per_item_streams.push(stream);
}
let flattened = futures_util::stream::iter(per_item_streams).flatten();
Ok(Box::pin(flattened))
}
}
#[cfg(test)]
mod tests {
use super::*;
use futures_util::StreamExt;
struct TestRunnable;
#[async_trait]
impl Runnable<String, String> for TestRunnable {
type Error = std::convert::Infallible;
async fn invoke(
&self,
input: String,
_config: Option<RunnableConfig>,
) -> Result<String, Self::Error> {
Ok(format!("processed: {}", input))
}
}
#[tokio::test]
async fn test_default_stream_returns_single_element() {
let runnable = TestRunnable;
let mut stream = runnable.stream("test".to_string(), None).await.unwrap();
let first = stream.next().await;
assert!(first.is_some());
assert_eq!(first.unwrap().unwrap(), "processed: test");
let second = stream.next().await;
assert!(second.is_none());
}
#[tokio::test]
async fn test_invoke_matches_stream_result() {
let runnable = TestRunnable;
let invoke_result = runnable.invoke("hello".to_string(), None).await.unwrap();
let mut stream = runnable.stream("hello".to_string(), None).await.unwrap();
let stream_result = stream.next().await.unwrap().unwrap();
assert_eq!(invoke_result, stream_result);
}
#[tokio::test]
async fn test_default_transform_maps_elementwise() {
let runnable = TestRunnable;
let input_stream = Box::pin(futures_util::stream::iter(vec![
Ok("first".to_string()),
Ok("second".to_string()),
Ok("third".to_string()),
]))
as Pin<Box<dyn Stream<Item = Result<String, std::convert::Infallible>> + Send>>;
let mut output_stream = runnable.transform(input_stream, None).await.unwrap();
let mut results = Vec::new();
while let Some(item) = output_stream.next().await {
results.push(item.unwrap());
}
assert_eq!(
results,
vec![
"processed: first".to_string(),
"processed: second".to_string(),
"processed: third".to_string(),
]
);
}
#[tokio::test]
async fn test_default_transform_empty_input() {
let runnable = TestRunnable;
let input_stream = Box::pin(futures_util::stream::empty::<
Result<String, std::convert::Infallible>,
>())
as Pin<Box<dyn Stream<Item = Result<String, std::convert::Infallible>> + Send>>;
let mut output_stream = runnable.transform(input_stream, None).await.unwrap();
assert!(output_stream.next().await.is_none());
}
#[tokio::test]
async fn test_default_batch_preserves_order() {
let runnable = TestRunnable;
let results = runnable
.batch(
vec!["a".to_string(), "b".to_string(), "c".to_string()],
None,
)
.await
.unwrap();
assert_eq!(
results,
vec![
"processed: a".to_string(),
"processed: b".to_string(),
"processed: c".to_string(),
]
);
}
#[tokio::test]
async fn test_default_batch_respects_max_concurrency() {
let runnable = TestRunnable;
let config = RunnableConfig::new().with_max_concurrency(1);
let results = runnable
.batch(
vec!["x".to_string(), "y".to_string(), "z".to_string()],
Some(config),
)
.await
.unwrap();
assert_eq!(
results,
vec![
"processed: x".to_string(),
"processed: y".to_string(),
"processed: z".to_string(),
]
);
}
#[tokio::test]
async fn test_default_batch_empty_input() {
let runnable = TestRunnable;
let results = runnable.batch(vec![], None).await.unwrap();
assert!(results.is_empty());
}
struct Delayed;
#[async_trait]
impl Runnable<&'static str, usize> for Delayed {
type Error = std::convert::Infallible;
async fn invoke(
&self,
input: &'static str,
_config: Option<RunnableConfig>,
) -> Result<usize, Self::Error> {
match input {
"slow" => {
tokio::time::sleep(std::time::Duration::from_millis(40)).await;
Ok(10)
}
_ => {
tokio::time::sleep(std::time::Duration::from_millis(1)).await;
Ok(1)
}
}
}
}
#[tokio::test]
async fn test_batch_as_completed_returns_completion_order() {
let results = Delayed
.batch_as_completed(vec!["slow", "fast"], None)
.await
.unwrap();
assert_eq!(results.len(), 2);
assert_eq!(results[0].0, 1, "完成最快的应带原始下标 1");
assert_eq!(results[0].1, 1);
assert_eq!(results[1].0, 0);
assert_eq!(results[1].1, 10);
}
#[tokio::test]
async fn test_batch_as_completed_respects_max_concurrency() {
let config = RunnableConfig::new().with_max_concurrency(1);
let results = Delayed
.batch_as_completed(vec!["fast", "slow"], Some(config))
.await
.unwrap();
assert_eq!(results, vec![(0, 1), (1, 10)]);
}
#[tokio::test]
async fn test_batch_as_completed_empty_input() {
let runnable = TestRunnable;
let results = runnable.batch_as_completed(vec![], None).await.unwrap();
assert!(results.is_empty());
}
}