use std::collections::BTreeMap;
use std::sync::Arc;
use crate::pull::{ProductReport, PurgeAnswer};
use crate::{
Absorb, Activity, EntitlementFact, MeteredDoor, ProductError, ProductFacts, Purged, Servable,
TenantId, TenantOwner, TenantUsage, Usage, UsagePage, UsagePageReq, UsersAnswer, UsersAsk,
};
pub const USERS_ACK_WAIT: std::time::Duration = std::time::Duration::from_secs(20);
pub type FactsSource = Arc<dyn Fn() -> ProductFacts + Send + Sync>;
pub trait PullLane: Send + Sync {
fn instance(&self) -> &str;
fn serving(&self, tenant: &TenantId) -> Result<(String, String), String>;
fn report_of(&self, instance: &str) -> Result<(ProductReport, u64), String>;
fn fresh_of(&self, instance: &str) -> Result<ProductReport, String>;
fn sold_estates(&self) -> Vec<String>;
fn queue_entitlement(&self, fact: &EntitlementFact) -> Result<(), String>;
fn users_queued(&self, tenant: &TenantId, reference: &str) -> bool;
fn add_users(
&self,
tenant: &TenantId,
ask: &UsersAsk,
reference: &str,
) -> Result<Option<Result<UsersAnswer, String>>, String>;
fn queue_room(&self, tenant: &TenantId, need_bytes: u64, why: &str) -> Result<(), String>;
fn purge(
&self,
tenant: &TenantId,
reason: &str,
) -> Result<Option<Result<PurgeAnswer, String>>, String>;
fn report(&self) -> Result<(ProductReport, u64), String> {
self.report_of(self.instance())
}
fn fresh(&self) -> Result<ProductReport, String> {
self.fresh_of(self.instance())
}
}
#[derive(Clone)]
pub struct Pulled {
facts: FactsSource,
lane: Arc<dyn PullLane>,
ack_wait: std::time::Duration,
}
impl std::fmt::Debug for Pulled {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("Pulled")
.field("instance", &self.lane.instance())
.finish()
}
}
impl Pulled {
pub fn new(facts: FactsSource, lane: Arc<dyn PullLane>) -> Self {
Self {
facts,
lane,
ack_wait: USERS_ACK_WAIT,
}
}
pub fn with_ack_wait(mut self, wait: std::time::Duration) -> Self {
self.ack_wait = wait;
self
}
pub fn instance(&self) -> &str {
self.lane.instance()
}
fn report(&self) -> Result<(ProductReport, u64), String> {
self.lane.report()
}
fn fresh(&self) -> Result<ProductReport, String> {
self.lane.fresh()
}
fn every_report(&self) -> Result<Vec<ProductReport>, ProductError> {
let (own, _) = self.lane.report().map_err(ProductError::Unavailable)?;
let mut all = vec![own];
for estate in self.lane.sold_estates() {
let Ok((mut p, _)) = self.lane.report_of(&estate) else {
continue;
};
for o in &mut p.tenants {
o.tenant = TenantId::on_estate(&estate, &o.tenant.0);
}
p.usage = std::mem::take(&mut p.usage)
.into_iter()
.map(|(t, u)| (TenantId::on_estate(&estate, &t).0, u))
.collect();
all.push(p);
}
Ok(all)
}
pub fn facts(&self) -> crate::ProductFacts {
(self.facts)()
}
pub fn list_tenants(&self) -> Result<Vec<TenantId>, ProductError> {
Ok(self
.every_report()?
.into_iter()
.flat_map(|p| p.tenants)
.map(|o| o.tenant)
.collect())
}
pub fn read_usage(&self, tenant: &TenantId) -> Result<Usage, ProductError> {
let (instance, ns) = self.lane.serving(tenant).map_err(ProductError::Refused)?;
let (mut p, _) = self
.lane
.report_of(&instance)
.map_err(ProductError::Unavailable)?;
p.usage.remove(&ns).ok_or_else(|| {
ProductError::Refused(format!(
"{instance}: the main's report has no usage for tenant {}",
tenant.0
))
})
}
pub fn read_usage_page(&self, req: &UsagePageReq) -> Result<Option<UsagePage>, ProductError> {
let _ = req;
let all = self.every_report()?;
let as_of_unix_ms = all
.iter()
.map(|p| p.measured_unix_ms)
.min()
.unwrap_or_default();
let rows = all
.into_iter()
.flat_map(|p| p.usage)
.map(|(t, usage)| TenantUsage {
tenant: TenantId(t),
usage,
})
.collect();
Ok(Some(UsagePage {
rows,
as_of_unix_ms,
..Default::default()
}))
}
pub fn tenant_owners(&self) -> Result<Option<Vec<TenantOwner>>, ProductError> {
Ok(Some(
self.every_report()?
.into_iter()
.flat_map(|p| p.tenants)
.collect(),
))
}
pub fn read_activity(
&self,
tenant: &TenantId,
since_unix_ms: u64,
until_unix_ms: u64,
) -> Result<Activity, ProductError> {
let (instance, ns) = self.lane.serving(tenant).map_err(ProductError::Refused)?;
let (mut p, _) = self
.lane
.report_of(&instance)
.map_err(ProductError::Unavailable)?;
let mut a = p.activity.remove(&ns).ok_or_else(|| {
ProductError::Unavailable(format!(
"{instance}: the main's report carries no activity for tenant {}",
tenant.0
))
})?;
let until = if until_unix_ms == 0 {
u64::MAX
} else {
until_unix_ms
};
a.days
.retain(|d| d.day_unix_ms >= since_unix_ms && d.day_unix_ms <= until);
Ok(a)
}
pub fn push_entitlement(&self, fact: &EntitlementFact) -> Result<(), ProductError> {
self.lane
.queue_entitlement(fact)
.map_err(ProductError::Unavailable)
}
pub fn can_absorb(
&self,
tenant: &TenantId,
caps: &BTreeMap<String, u64>,
) -> Result<Absorb, String> {
let (instance, ns) = self.lane.serving(tenant)?;
let p = self.lane.fresh_of(&instance)?;
let held = p.promised.get(&ns);
let short: Vec<String> = caps
.iter()
.filter_map(|(meter, target)| {
let room = *p.room.get(meter)?;
let need =
target.saturating_sub(held.and_then(|h| h.get(meter)).copied().unwrap_or(0));
(need > room).then(|| {
format!("{meter} needs {need} beyond the {room} the main has room for")
})
})
.collect();
if short.is_empty() {
return Ok(Absorb::OnHand);
}
match p.with_iron {
Some(note) => Ok(Absorb::WithIron { note }),
None => Err(format!("{instance}: {}", short.join("; "))),
}
}
pub fn servable(&self) -> Servable {
match self.fresh() {
Ok(p) => p.servable.unwrap_or_else(|| {
Servable::Unmeasured(format!(
"{}: its main's report does not say what it can serve",
self.instance()
))
}),
Err(w) => Servable::Unmeasured(w),
}
}
pub fn servable_on(&self, estate: &str) -> Servable {
if estate == self.instance() {
return self.servable();
}
match self.lane.fresh_of(estate) {
Ok(p) => p.servable.unwrap_or_else(|| {
Servable::Unmeasured(format!(
"{estate}: its main's report does not say what it can serve"
))
}),
Err(w) => Servable::Unmeasured(w),
}
}
pub fn metered_door(&self) -> MeteredDoor {
match self.report() {
Ok((p, _)) => p.metered.unwrap_or_else(|| {
MeteredDoor::Unproven(format!(
"{}: its main's report does not say",
self.instance()
))
}),
Err(w) => MeteredDoor::Unproven(w),
}
}
pub fn users_fit(
&self,
tenant: &TenantId,
ask: &UsersAsk,
) -> Result<UsersAnswer, ProductError> {
let (instance, ns) = self.lane.serving(tenant).map_err(ProductError::Refused)?;
let p = self
.lane
.fresh_of(&instance)
.map_err(ProductError::Unavailable)?;
let name = ask.name.trim();
if ask.count.max(1) == 1 && !name.is_empty() && p.names.iter().any(|n| n == name) {
return Err(ProductError::Refused(format!(
"{name} is already a name on {instance}: pick another"
)));
}
let room = p.accounts.get(&ns).ok_or_else(|| {
ProductError::Unavailable(format!(
"{instance}: the main's report says nothing of {}'s room yet",
tenant.0
))
})?;
let room_bytes = if ask.room_bytes > 0 {
ask.room_bytes
} else {
room.room_bytes
};
let need_bytes = room_bytes.saturating_mul(u64::from(ask.count.max(1)));
let shortfall_bytes = room
.unused_bytes
.map(|u| need_bytes.saturating_sub(u))
.unwrap_or(0);
Ok(UsersAnswer {
principal: String::new(),
room_bytes,
need_bytes,
unused_bytes: room.unused_bytes,
shortfall_bytes,
seats: room.seats,
seats_open: room.seats_open,
unchanged: true,
invite_secret: String::new(),
})
}
pub fn add_users(
&self,
tenant: &TenantId,
ask: &UsersAsk,
order: &str,
) -> Result<UsersAnswer, ProductError> {
let waits = !self.lane.users_queued(tenant, order);
let until = std::time::Instant::now() + self.ack_wait;
loop {
match self
.lane
.add_users(tenant, ask, order)
.map_err(ProductError::Unavailable)?
{
Some(Ok(answer)) => return Ok(answer),
Some(Err(words)) => return Err(ProductError::Refused(words)),
None if !waits || std::time::Instant::now() >= until => {
return Err(ProductError::Unavailable(format!(
"{order}: queued for {}'s main to grant on its next call",
self.instance()
)))
}
None => std::thread::sleep(std::time::Duration::from_millis(250)),
}
}
}
pub fn queues_room(&self) -> bool {
matches!(self.fresh().ok().and_then(|p| p.servable), Some(Servable::Measured { growth, .. }) if !matches!(growth, crate::Growth::Sealed(_)))
}
pub fn queue_room(
&self,
tenant: &TenantId,
need_bytes: u64,
why: &str,
) -> Result<(), ProductError> {
self.lane
.queue_room(tenant, need_bytes, why)
.map_err(ProductError::Unavailable)
}
pub fn purge_tenant(&self, tenant: &TenantId, reason: &str) -> Result<Purged, ProductError> {
let locked = |detail: String| Purged {
tenant: tenant.clone(),
detail,
bytes_reclaimed: None,
still_locked: true,
failures: Vec::new(),
};
match self.lane.purge(tenant, reason).map_err(ProductError::Unavailable)? {
None => Ok(locked(format!("queued for {}'s main to purge on its next call; the account stays locked", self.instance()))),
Some(Err(words)) => Err(ProductError::Refused(words)),
Some(Ok(a)) if a.failed => Err(ProductError::Refused(format!(
"{}'s purge of {} did not finish ({}): {}{}",
self.instance(),
tenant.0,
a.detail,
if a.error.is_empty() { "no reason given" } else { &a.error },
if a.failures.is_empty() { String::new() } else { format!("; still on the disk: {}", a.failures.join("; ")) }
))),
Some(Ok(a)) if a.running => Ok(locked(format!("{}; the main's job {} is still running — the account stays LOCKED until it settles", a.detail, a.job))),
Some(Ok(a)) => Ok(Purged { tenant: tenant.clone(), detail: a.detail, bytes_reclaimed: a.bytes_reclaimed, still_locked: a.still_locked, failures: a.failures }),
}
}
}
pub struct Unplaced(pub &'static str);
impl Unplaced {
fn no(&self) -> String {
format!("[products.{}] names no instance: no estate's main reports for it, and monetize never dials one", self.0)
}
}
impl PullLane for Unplaced {
fn instance(&self) -> &str {
""
}
fn serving(&self, _tenant: &TenantId) -> Result<(String, String), String> {
Err(self.no())
}
fn report_of(&self, _instance: &str) -> Result<(ProductReport, u64), String> {
Err(self.no())
}
fn fresh_of(&self, _instance: &str) -> Result<ProductReport, String> {
Err(self.no())
}
fn sold_estates(&self) -> Vec<String> {
Vec::new()
}
fn queue_entitlement(&self, _fact: &EntitlementFact) -> Result<(), String> {
Err(self.no())
}
fn users_queued(&self, _tenant: &TenantId, _reference: &str) -> bool {
false
}
fn add_users(
&self,
_tenant: &TenantId,
_ask: &UsersAsk,
_reference: &str,
) -> Result<Option<Result<UsersAnswer, String>>, String> {
Err(self.no())
}
fn queue_room(&self, _tenant: &TenantId, _need_bytes: u64, _why: &str) -> Result<(), String> {
Err(self.no())
}
fn purge(
&self,
_tenant: &TenantId,
_reason: &str,
) -> Result<Option<Result<PurgeAnswer, String>>, String> {
Err(self.no())
}
}
#[derive(Default)]
pub struct HeldMain {
instance: String,
held: std::sync::Mutex<Held>,
}
#[derive(Default, Clone)]
pub struct Held {
pub report: Option<ProductReport>,
pub entitlements: Vec<EntitlementFact>,
pub users: Vec<crate::pull::AddUsers>,
pub room: Option<crate::pull::GrowRoom>,
pub purges: Vec<crate::pull::PurgeTenant>,
pub users_answer: Option<Result<UsersAnswer, String>>,
pub purge_answer: Option<Result<PurgeAnswer, String>>,
}
impl HeldMain {
pub fn new(instance: impl Into<String>) -> Arc<HeldMain> {
Arc::new(HeldMain {
instance: instance.into(),
..Default::default()
})
}
pub fn report(&self, edit: impl FnOnce(&mut ProductReport)) {
edit(
self.lock()
.report
.get_or_insert_with(ProductReport::default),
);
}
pub fn hold(&self, edit: impl FnOnce(&mut Held)) {
edit(&mut self.lock());
}
pub fn held(&self) -> Held {
self.lock().clone()
}
pub fn entitlements(&self) -> Vec<EntitlementFact> {
self.lock().entitlements.clone()
}
fn lock(&self) -> std::sync::MutexGuard<'_, Held> {
self.held.lock().unwrap_or_else(|e| e.into_inner())
}
pub fn serve(self: &Arc<Self>, facts: ProductFacts) -> Pulled {
Pulled::new(
Arc::new(move || facts.clone()),
Arc::clone(self) as Arc<dyn PullLane>,
)
.with_ack_wait(std::time::Duration::ZERO)
}
}
impl PullLane for HeldMain {
fn instance(&self) -> &str {
&self.instance
}
fn serving(&self, tenant: &TenantId) -> Result<(String, String), String> {
match tenant.estate() {
Some((estate, ns)) if estate != self.instance => {
Err(format!("{estate}: no estate of this product holds {ns}"))
}
Some((_, ns)) => Ok((self.instance.clone(), ns.to_owned())),
None => Ok((self.instance.clone(), tenant.0.clone())),
}
}
fn report_of(&self, instance: &str) -> Result<(ProductReport, u64), String> {
if instance != self.instance {
return Err(format!("{instance}: not this estate"));
}
self.lock().report.clone().map(|r| (r, 1)).ok_or_else(|| {
format!("{instance}: its main has sent no report yet; monetize never dials it")
})
}
fn fresh_of(&self, instance: &str) -> Result<ProductReport, String> {
self.report_of(instance).map(|(r, _)| r)
}
fn sold_estates(&self) -> Vec<String> {
Vec::new()
}
fn queue_entitlement(&self, fact: &EntitlementFact) -> Result<(), String> {
self.lock().entitlements.push(fact.clone());
Ok(())
}
fn users_queued(&self, _tenant: &TenantId, reference: &str) -> bool {
let h = self.lock();
h.users_answer.is_none() && h.users.iter().any(|u| u.order == reference)
}
fn add_users(
&self,
tenant: &TenantId,
ask: &UsersAsk,
reference: &str,
) -> Result<Option<Result<UsersAnswer, String>>, String> {
let ns = self.serving(tenant)?.1;
let mut h = self.lock();
if !h.users.iter().any(|u| u.order == reference) {
h.users.push(crate::pull::AddUsers {
tenant: ns,
ask: ask.clone(),
order: reference.to_owned(),
});
}
Ok(h.users_answer.clone())
}
fn queue_room(&self, _tenant: &TenantId, need_bytes: u64, why: &str) -> Result<(), String> {
self.lock().room = Some(crate::pull::GrowRoom {
need_bytes,
reason: why.to_owned(),
});
Ok(())
}
fn purge(
&self,
tenant: &TenantId,
reason: &str,
) -> Result<Option<Result<PurgeAnswer, String>>, String> {
let ns = self.serving(tenant)?.1;
let mut h = self.lock();
if !h.purges.iter().any(|p| p.tenant == ns) {
h.purges.push(crate::pull::PurgeTenant {
tenant: ns,
reason: reason.to_owned(),
..Default::default()
});
}
Ok(h.purge_answer.clone())
}
}