mod combined_job_group;
mod disabled_job_group;
mod named_job_group;
use either::Either;
use futures::{Stream, future::Either as FutureEither, stream};
use itertools::Itertools;
use tokio::sync::mpsc;
use tokio_stream::wrappers::ReceiverStream;
use std::fmt::{self, Display};
use std::iter;
use super::{JobResult, OpaqueJob};
use crate::StaticStr;
use crate::maybe_send::{MaybeSend, MaybeSendSync};
pub use self::combined_job_group::CombinedJobGroup;
pub use self::disabled_job_group::DisabledJobGroup;
pub use self::named_job_group::NamedJobGroup;
pub type JobGroupResult = Vec<JobResult>;
pub trait JobGroup: MaybeSendSync {
#[must_use = "the jobs could've finished with errors"]
fn run(self) -> impl Stream<Item = (JobId, JobResult)> + MaybeSend
where
Self: Sized + 'static;
fn combine_with<G>(self, other: G) -> CombinedJobGroup<Self, G>
where
Self: Sized,
G: JobGroup,
{
CombinedJobGroup(self, other)
}
fn disable(self) -> DisabledJobGroup<Self>
where
Self: Sized,
{
DisabledJobGroup(self)
}
fn with_name<S>(self, name: S) -> NamedJobGroup<Self>
where
Self: Sized,
S: Into<StaticStr>,
{
NamedJobGroup {
inner: self,
name: name.into(),
}
}
}
#[derive(Debug)]
pub struct JobId {
pub job_name: Option<StaticStr>,
pub group_hierarchy: Vec<StaticStr>,
}
impl<J> JobGroup for Option<J>
where
J: JobGroup,
{
fn run(self) -> impl Stream<Item = (JobId, JobResult)> + MaybeSend
where
Self: Sized + 'static,
{
let Some(group) = self else {
return FutureEither::Left(stream::empty());
};
FutureEither::Right(group.run())
}
}
impl<A, B> JobGroup for Either<A, B>
where
A: JobGroup,
B: JobGroup,
{
fn run(self) -> impl Stream<Item = (JobId, JobResult)> + MaybeSend
where
Self: Sized + 'static,
{
match self {
Either::Left(a) => FutureEither::Left(a.run()),
Either::Right(b) => FutureEither::Right(b.run()),
}
}
}
impl JobGroup for () {
fn run(self) -> impl Stream<Item = (JobId, JobResult)> + MaybeSend
where
Self: Sized + 'static,
{
stream::empty()
}
}
impl<G> JobGroup for (G,)
where
G: OpaqueJob,
{
fn run(mut self) -> impl Stream<Item = (JobId, JobResult)> + MaybeSend
where
Self: Sized + 'static,
{
stream::once(async move {
let name = self.0.name().map(|n| StaticStr::from(n.to_owned()));
(JobId::new(name), self.0.run().await)
})
}
}
macro_rules! impl_jobgroup_for_tuples {
($($type_name:ident)+) => {
impl<$($type_name),+> JobGroup for ($($type_name),+)
where
$($type_name: OpaqueJob),+
{
#[expect(
non_snake_case,
reason = "it's fine to re-use the names to make calling the macro easier"
)]
fn run(self) -> impl Stream<Item = (JobId, JobResult)> + MaybeSend
where
Self: Sized + 'static,
{
let (tx, rx) = mpsc::channel(128);
let ($($type_name),+) = self;
$(
{
let tx = tx.clone();
let mut job = $type_name;
spawn(async move {
let result = OpaqueJob::run(&mut job).await;
let id = JobId::new(job.name().map(|s| StaticStr::from(s.to_owned())));
if let Err(e) = tx.send((id, result)).await {
tracing::debug!(
"run Stream channel closed before job result has been sent. JobId: {}",
e.0.0
);
}
});
}
)+
ReceiverStream::new(rx)
}
}
};
}
impl_jobgroup_for_tuples!(J1 J2);
impl_jobgroup_for_tuples!(J1 J2 J3);
impl_jobgroup_for_tuples!(J1 J2 J3 J4);
impl_jobgroup_for_tuples!(J1 J2 J3 J4 J5);
impl_jobgroup_for_tuples!(J1 J2 J3 J4 J5 J6);
impl_jobgroup_for_tuples!(J1 J2 J3 J4 J5 J6 J7);
impl_jobgroup_for_tuples!(J1 J2 J3 J4 J5 J6 J7 J8);
impl_jobgroup_for_tuples!(J1 J2 J3 J4 J5 J6 J7 J8 J9);
impl_jobgroup_for_tuples!(J1 J2 J3 J4 J5 J6 J7 J8 J9 J10);
impl_jobgroup_for_tuples!(J1 J2 J3 J4 J5 J6 J7 J8 J9 J10 J11);
impl_jobgroup_for_tuples!(J1 J2 J3 J4 J5 J6 J7 J8 J9 J10 J11 J12);
impl JobId {
#[must_use]
pub fn new(job_name: Option<StaticStr>) -> Self {
Self {
group_hierarchy: Vec::new(),
job_name,
}
}
}
impl Display for JobId {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
const UNKNOWN_JOB: StaticStr = StaticStr::from_static_str("<UNKNOWN>");
let path = self
.group_hierarchy
.iter()
.rev()
.chain(iter::once(self.job_name.as_ref().unwrap_or(&UNKNOWN_JOB)))
.join("/");
f.write_str(&path)
}
}
pub(crate) fn spawn<F, T>(fut: F)
where
F: Future<Output = T> + MaybeSend + 'static,
T: MaybeSend + 'static,
{
#[cfg(feature = "send")]
tokio::spawn(fut);
#[cfg(not(feature = "send"))]
tokio::task::spawn_local(fut);
}