use crate::{Step, StepError, StepOutcome, StepRunner};
use serde::{Deserialize, Serialize};
use crate::bulk::cost::CostReport;
use crate::bulk::pipeline::{BulkCtx, Pipeline};
use crate::bulk::progress::{ItemMarker, MarkerStatus};
use crate::keys::bulk_item_kv_key;
use crate::outcome::run_typed_step;
#[derive(Debug, Serialize, Deserialize)]
pub(crate) struct ItemPayload {
pub(crate) batch_id: String,
pub(crate) key: String,
pub(crate) input: Vec<u8>,
}
#[derive(Debug, Serialize, Deserialize)]
pub(crate) struct ItemEnvelope<O> {
pub output: O,
pub cost: CostReport,
}
pub(crate) struct PipelineRunner<P> {
pipeline: P,
}
impl<P> PipelineRunner<P> {
pub(crate) fn new(pipeline: P) -> Self {
Self { pipeline }
}
}
impl<P: Pipeline> StepRunner for PipelineRunner<P> {
async fn run_step(&self, step: &Step) -> Result<StepOutcome, StepError> {
let ItemPayload {
batch_id,
key,
input,
} = rmp_serde::from_slice(&step.payload).map_err(|err| {
StepError::permanent(format!(
"item {} has a malformed payload: {err}",
step.run_id
))
})?;
let outcome = run_typed_step(step, "bulk item", &input, |input: P::Input| async {
let ctx = BulkCtx::new(&batch_id, &key, input, step);
let output = self.pipeline.run(&ctx).await.map_err(Into::into)?;
Ok(ItemEnvelope {
output,
cost: ctx.cost(),
})
})
.await;
let marker_key = bulk_item_kv_key(&batch_id, &key);
match &outcome {
Ok(StepOutcome::Succeed { result }) => {
let cost = rmp_serde::from_slice::<ItemEnvelope<P::Output>>(result)
.map(|envelope| envelope.cost)
.unwrap_or_default();
let marker = ItemMarker::new(MarkerStatus::Succeeded, None, cost);
step.effects.put_reserved(marker_key, marker.encode()?)?;
}
Err(err) => {
let marker = ItemMarker::new(
MarkerStatus::Failed,
Some(err.message.clone()),
CostReport::new(),
);
step.effects
.put_reserved_on_failure(marker_key, marker.encode()?)?;
}
Ok(_) => {}
}
outcome
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::MemoStore;
use std::collections::HashMap;
use std::sync::Arc;
use taquba::object_store::memory::InMemory;
use tokio_util::sync::CancellationToken;
#[derive(Serialize, Deserialize)]
struct Doubler;
impl Pipeline for Doubler {
type Input = u32;
type Output = u32;
type Error = StepError;
async fn run(&self, ctx: &BulkCtx<u32>) -> Result<u32, StepError> {
ctx.record_cost("calls", 1.0);
Ok(ctx.input * 2)
}
}
struct AlwaysFails;
impl Pipeline for AlwaysFails {
type Input = u32;
type Output = u32;
type Error = StepError;
async fn run(&self, _ctx: &BulkCtx<u32>) -> Result<u32, StepError> {
Err(StepError::permanent("nope"))
}
}
fn step_with_input(input: Vec<u8>) -> Step {
let memo_store = MemoStore::new(Arc::new(InMemory::new()), "memo");
let payload = rmp_serde::to_vec_named(&ItemPayload {
batch_id: "b".into(),
key: "item-1".into(),
input,
})
.unwrap();
Step {
run_id: "run-1".into(),
step_number: 0,
payload,
headers: HashMap::new(),
job_id: "job-1".into(),
attempts: 1,
max_attempts: 3,
cancel_token: CancellationToken::new(),
lease: taquba::LeaseHandle::detached(),
memo: memo_store.new_memo("run-1", 0),
run_memo: memo_store.new_run_memo("run-1"),
effects: crate::EffectsHandle::detached(),
kv: crate::KvReadHandle::detached(),
signal: None,
}
}
#[tokio::test]
async fn runs_pipeline_and_encodes_envelope() {
let runner = PipelineRunner::new(Doubler);
let step = step_with_input(rmp_serde::to_vec_named(&21u32).unwrap());
let outcome = runner.run_step(&step).await.unwrap();
let StepOutcome::Succeed { result } = outcome else {
panic!("expected Succeed, got {outcome:?}");
};
let envelope: ItemEnvelope<u32> = rmp_serde::from_slice(&result).unwrap();
assert_eq!(envelope.output, 42);
assert_eq!(envelope.cost.get("calls"), 1.0);
}
#[tokio::test]
async fn undecodable_input_is_permanent() {
let runner = PipelineRunner::new(Doubler);
let step = step_with_input(rmp_serde::to_vec_named(&"not a number").unwrap());
let err = runner.run_step(&step).await.unwrap_err();
assert_eq!(err.kind, crate::StepErrorKind::Permanent);
}
#[tokio::test]
async fn pipeline_error_propagates_as_step_error() {
let runner = PipelineRunner::new(AlwaysFails);
let step = step_with_input(rmp_serde::to_vec_named(&1u32).unwrap());
let err = runner.run_step(&step).await.unwrap_err();
assert_eq!(err.message, "nope");
assert_eq!(err.kind, crate::StepErrorKind::Permanent);
}
}