use std::collections::{HashMap, HashSet};
use std::future::Future;
use std::pin::Pin;
use std::sync::Arc;
use alloy::primitives::{Address, U256};
use tokio::time::Instant;
use crate::credential::{CredentialSlot, CredentialView, FundingNeeded, QuoteMax, unix_now};
use crate::fault::{Fault, classify};
use crate::recovery::{RecoveryGate, SwapStep, after_step};
use crate::scheduler::{Connecting, connect_future, key_spend, sleep_until_opt};
use crate::source::{Funder, SourceFuture};
use crate::source_set::{BUILD_RETRY_BASE, Holder, NoAffordableSource, SourceProvider, SourceSet};
use crate::stop::StopPolicy;
use crate::streamer::StreamCandidate;
pub async fn first_open<P, F, T, O, Fut>(
sources: &mut SourceSet<'_, P>,
stop: &StopPolicy,
funder: &F,
gate: &RecoveryGate,
credentials: Option<&CredentialSlot>,
open: O,
) -> anyhow::Result<(Address, T)>
where
P: SourceProvider,
F: Funder,
O: Fn(Arc<StreamCandidate<P::Source>>) -> Fut,
Fut: Future<Output = anyhow::Result<T>>,
{
let opened = open_loop(sources, funder, gate, credentials, &open);
tokio::select! {
biased;
opened = opened => {
if opened.is_ok() {
stop.clock.tick();
}
opened
}
gave_up = stop.expired() => Err(anyhow::Error::new(gave_up)),
}
}
async fn poll_some<F: Future + Unpin>(fut: Option<&mut F>) -> F::Output {
match fut {
Some(fut) => fut.await,
None => std::future::pending().await,
}
}
async fn open_loop<'p, P, F, T, O, Fut>(
sources: &mut SourceSet<'p, P>,
funder: &F,
gate: &RecoveryGate,
credentials: Option<&CredentialSlot>,
open: &O,
) -> anyhow::Result<(Address, T)>
where
P: SourceProvider,
F: Funder,
O: Fn(Arc<StreamCandidate<P::Source>>) -> Fut,
Fut: Future<Output = anyhow::Result<T>>,
{
let mut deposit = U256::ZERO;
let mut top_ups_seen = gate.top_ups();
let mut delegate = Delegate::new(credentials);
let mut held_off: HashMap<Address, Instant> = HashMap::new();
let mut connecting: Option<Connecting<'p, P::Source>> = None;
let mut opening: Option<(Address, Pin<Box<Fut>>)> = None;
let mut discovering: Option<SourceFuture<'p, Vec<Holder>>> = None;
loop {
let now = Instant::now();
held_off.retain(|_, until| *until > now);
if connecting.is_none() && opening.is_none() {
let busy: HashSet<Address> = held_off.keys().copied().collect();
if let Some(holder) = sources.next_to_start(now, deposit, &busy) {
match sources.cached_lane(holder.provider) {
Some(lane) => {
deposit = deposit.max(lane_deposit(&lane));
opening = Some((holder.provider, Box::pin(open(lane))));
}
None => connecting = Some(connect_future(sources.provider(), holder)),
}
}
}
delegate.follow_swap(sources);
let attempting = connecting.is_some() || opening.is_some();
if !attempting
&& discovering.is_none()
&& let Some(err) = sources.exhausted(deposit, false)
{
if err.downcast_ref::<NoAffordableSource>().is_none() {
return Err(err);
}
if let Some(err) = delegate.step(sources, gate, err).await? {
deposit =
owner_step(sources, funder, gate, deposit, &mut top_ups_seen, err).await?;
}
continue;
}
if discovering.is_none()
&& sources.wants_discovery(now, deposit, usize::from(attempting), false)
{
discovering = Some(sources.provider().discover(sources.hash()));
}
let wake = sources
.next_wake(now)
.into_iter()
.chain(held_off.values().copied())
.min();
tokio::select! {
biased;
(provider, built) = poll_some(connecting.as_mut()), if connecting.is_some() => {
connecting = None;
match sources.lane_built(provider, built, Instant::now()) {
Ok(lane) => {
deposit = deposit.max(lane_deposit(&lane));
opening = Some((provider, Box::pin(open(lane))));
}
Err(err) => {
if let Fault::Fatal(_) = classify(&err) {
return Err(err);
}
tracing::debug!(%provider, error = %decdn_common::redact::sanitize_err_chain(&err), "lane build failed");
}
}
}
answer = poll_some(opening.as_mut().map(|(_, fut)| fut)), if opening.is_some() => {
let Some((provider, _)) = opening.take() else {
continue;
};
match answer {
Ok(answer) => {
sources.record_progress(provider);
return Ok((provider, answer));
}
Err(err) => {
let at = Instant::now();
delegate.saw(&err);
if gate.settling(at) && sources.hold_while_settling(provider, &err, at) {
continue;
}
match sources.record_fault(provider, &err, None, at, deposit) {
Fault::Fatal(_) => return Err(err),
Fault::Transient => {
held_off.insert(provider, at + BUILD_RETRY_BASE);
}
Fault::Source | Fault::Unaffordable => {}
}
}
}
}
found = poll_some(discovering.as_mut()), if discovering.is_some() => {
discovering = None;
sources.discovery_done(found, Instant::now(), deposit);
}
() = sleep_until_opt(wake) => {}
}
}
}
async fn owner_step<P, F>(
sources: &SourceSet<'_, P>,
funder: &F,
gate: &RecoveryGate,
deposit: U256,
top_ups_seen: &mut u64,
exhausted: anyhow::Error,
) -> anyhow::Result<U256>
where
P: SourceProvider,
F: Funder,
{
let spent = sources
.lanes_spent()
.max(funder.pool_spent().unwrap_or(U256::ZERO));
let current = || sources.lanes_deposit();
let seen = sources.priced_out_at(deposit);
let raised = after_step(
gate.step(funder, seen, top_ups_seen, current, spent).await,
exhausted,
)?;
if raised > deposit {
sources.credit_lanes(raised);
}
if raised <= seen {
sources.health().clear_unaffordable();
}
Ok(raised.max(deposit))
}
struct Delegate<'a> {
slot: Option<&'a CredentialSlot>,
generation_seen: u64,
capability_refused: bool,
}
impl<'a> Delegate<'a> {
fn new(slot: Option<&'a CredentialSlot>) -> Self {
Self {
slot,
generation_seen: slot.map_or(0, CredentialSlot::generation),
capability_refused: false,
}
}
fn saw(&mut self, err: &anyhow::Error) {
if crate::fault::refuses_capability(err) {
self.capability_refused = true;
}
}
fn follow_swap<P: SourceProvider>(&mut self, sources: &mut SourceSet<'_, P>) {
let Some(slot) = self.slot else { return };
if slot.generation() == self.generation_seen {
return;
}
self.generation_seen = slot.generation();
sources.drop_lanes(|lane| slot.is_stale(&lane.ctx));
sources.health().clear_unaffordable();
self.capability_refused = false;
}
async fn step<P: SourceProvider>(
&self,
sources: &SourceSet<'_, P>,
gate: &RecoveryGate,
exhausted: anyhow::Error,
) -> anyhow::Result<Option<anyhow::Error>> {
let Some(slot) = self.slot else {
return Ok(Some(exhausted));
};
let view = CredentialView::of(
&slot.current(),
key_spend(slot, &sources.built_lanes(), None),
0,
&QuoteMax::default(),
unix_now(),
);
slot.report(view.event());
let cause = view.funding_cause(self.capability_refused);
let pool = slot.pool_id();
let Some(cause) = cause else {
return Err(exhausted.context(FundingNeeded::PublisherPool { pool }));
};
match gate.swap_step(slot, self.generation_seen).await {
SwapStep::Swapped => Ok(None),
SwapStep::NoProgress | SwapStep::TimedOut => {
Err(exhausted.context(FundingNeeded::NewCapability { pool, cause }))
}
}
}
}
fn lane_deposit<S>(lane: &StreamCandidate<S>) -> U256 {
lane.ctx.lock().map_or(U256::ZERO, |ctx| ctx.deposit)
}
#[cfg(test)]
#[allow(clippy::unwrap_used, clippy::expect_used, clippy::panic)] mod tests;