use crate::core::Column as _;
use crate::sql::sqlx::Database;
use crate::sql::FetcherPool as _;
use super::error::TenancyError;
use super::org::Org;
use super::pools::TenantPools;
#[derive(Debug)]
pub struct TenantOutcome<T, E> {
pub slug: String,
pub result: Result<T, SweepError<E>>,
}
#[derive(Debug, thiserror::Error)]
pub enum SweepError<E> {
#[error("could not resolve pool: {0}")]
Pool(TenancyError),
#[error("sweep failed: {0}")]
Sweep(E),
}
#[derive(Debug)]
pub struct TenantSweep<T, E> {
pub outcomes: Vec<TenantOutcome<T, E>>,
}
impl<T, E> TenantSweep<T, E> {
#[must_use]
pub fn succeeded(&self) -> usize {
self.outcomes.iter().filter(|o| o.result.is_ok()).count()
}
#[must_use]
pub fn failed(&self) -> usize {
self.outcomes.iter().filter(|o| o.result.is_err()).count()
}
pub fn errors(&self) -> impl Iterator<Item = (&str, &SweepError<E>)> {
self.outcomes.iter().filter_map(|o| match &o.result {
Err(e) => Some((o.slug.as_str(), e)),
Ok(_) => None,
})
}
pub fn values(&self) -> impl Iterator<Item = (&str, &T)> {
self.outcomes.iter().filter_map(|o| match &o.result {
Ok(v) => Some((o.slug.as_str(), v)),
Err(_) => None,
})
}
}
pub async fn active_tenants<DB>(pools: &TenantPools<DB>) -> Result<Vec<Org>, TenancyError>
where
DB: Database,
crate::sql::Pool: From<crate::sql::sqlx::Pool<DB>>,
{
let registry = pools.registry_pool();
Ok(Org::objects()
.where_(Org::active.eq(true))
.fetch(®istry)
.await?)
}
pub async fn for_each_tenant<DB, F, Fut, T, E>(
pools: &TenantPools<DB>,
f: F,
) -> Result<TenantSweep<T, E>, TenancyError>
where
DB: Database,
crate::sql::Pool: From<crate::sql::sqlx::Pool<DB>>,
F: Fn(Org, crate::sql::Pool) -> Fut,
Fut: std::future::Future<Output = Result<T, E>>,
{
let orgs = active_tenants(pools).await?;
let mut outcomes = Vec::with_capacity(orgs.len());
for org in orgs {
let slug = org.slug.clone();
let pool = match pools.scoped_pool_dyn(&org).await {
Ok(p) => p,
Err(e) => {
tracing::warn!(
target: "rustango::tenancy::sweep",
slug = %slug,
error = %e,
"skipping tenant: could not resolve its pool",
);
outcomes.push(TenantOutcome {
slug,
result: Err(SweepError::Pool(e)),
});
continue;
}
};
let result = match crate::audit::writing_to_tenant(slug.clone(), f(org, pool)).await {
Ok(v) => Ok(v),
Err(e) => Err(SweepError::Sweep(e)),
};
if let Err(SweepError::Sweep(ref e)) = result {
let _ = e;
tracing::warn!(
target: "rustango::tenancy::sweep",
slug = %slug,
"tenant sweep returned an error; continuing with the remaining tenants",
);
}
outcomes.push(TenantOutcome { slug, result });
}
Ok(TenantSweep { outcomes })
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn sweep_accounting_partitions_outcomes() {
let sweep: TenantSweep<u64, String> = TenantSweep {
outcomes: vec![
TenantOutcome {
slug: "acme".into(),
result: Ok(3),
},
TenantOutcome {
slug: "globex".into(),
result: Err(SweepError::Sweep("boom".to_owned())),
},
TenantOutcome {
slug: "initech".into(),
result: Ok(0),
},
],
};
assert_eq!(sweep.succeeded(), 2);
assert_eq!(sweep.failed(), 1);
assert_eq!(sweep.succeeded() + sweep.failed(), sweep.outcomes.len());
let values: Vec<_> = sweep.values().map(|(s, v)| (s, *v)).collect();
assert_eq!(values, vec![("acme", 3), ("initech", 0)]);
let errors: Vec<_> = sweep.errors().map(|(s, _)| s).collect();
assert_eq!(errors, vec!["globex"]);
}
}