use faucet_core::FaucetError;
use faucet_core::io::CallContext;
use futures::{Stream, TryStreamExt};
use std::future::Future;
pub async fn once<T, Fut>(calls: &CallContext, op: &'static str, fut: Fut) -> Result<T, FaucetError>
where
Fut: Future<Output = Result<T, FaucetError>>,
{
calls.call_once(op, Box::pin(fut)).await
}
pub async fn next_rows<S, T>(
calls: &CallContext,
op: &'static str,
rows: &mut S,
max: usize,
map_err: fn(sqlx::Error) -> FaucetError,
) -> Result<Vec<T>, FaucetError>
where
S: Stream<Item = Result<T, sqlx::Error>> + Unpin,
{
once(calls, op, async {
let mut out = Vec::new();
while out.len() < max {
match rows.try_next().await.map_err(map_err)? {
Some(row) => out.push(row),
None => break,
}
}
Ok(out)
})
.await
}
#[cfg(test)]
mod tests {
use super::*;
use std::time::Duration;
fn err(e: sqlx::Error) -> FaucetError {
FaucetError::Source(e.to_string())
}
#[tokio::test]
async fn once_never_resends_under_a_retrying_context() {
let fast = Duration::from_millis(1);
let ctx = CallContext::new().with_retry(faucet_core::RetryPolicy {
max_attempts: 2,
base: fast,
max: fast,
jitter: false,
..Default::default()
});
let attempts = std::sync::atomic::AtomicUsize::new(0);
let e = once::<(), _>(&ctx, "query", async {
attempts.fetch_add(1, std::sync::atomic::Ordering::Relaxed);
Err(FaucetError::RateLimited(fast))
})
.await
.expect_err("the one attempt fails");
assert!(matches!(e, FaucetError::RateLimited(_)), "{e:?}");
assert_eq!(attempts.load(std::sync::atomic::Ordering::Relaxed), 1);
}
#[tokio::test]
async fn pages_a_cursor_and_surfaces_its_errors() {
let mut rows = futures::stream::iter(vec![Ok(1), Ok(2), Ok(3)]);
let ctx = CallContext::new();
assert_eq!(
next_rows(&ctx, "fetch", &mut rows, 2, err).await.unwrap(),
[1, 2]
);
assert_eq!(
next_rows(&ctx, "fetch", &mut rows, 2, err).await.unwrap(),
[3]
);
assert!(
next_rows(&ctx, "fetch", &mut rows, 2, err)
.await
.unwrap()
.is_empty()
);
let mut bad = futures::stream::iter(vec![Ok(1), Err(sqlx::Error::RowNotFound)]);
let e = next_rows(&ctx, "fetch", &mut bad, 5, err)
.await
.unwrap_err();
assert!(matches!(e, FaucetError::Source(_)), "{e:?}");
}
#[tokio::test]
async fn a_stalled_cursor_ends_at_the_deadline() {
let mut stalled = futures::stream::pending::<Result<i32, sqlx::Error>>();
let ctx = CallContext::new()
.with_deadline(tokio::time::Instant::now() + Duration::from_millis(50));
let e = next_rows(&ctx, "fetch", &mut stalled, 5, err)
.await
.unwrap_err();
assert!(matches!(e, FaucetError::Timeout { .. }), "{e:?}");
let ok = once(&ctx.clone(), "query", async { Ok(7) }).await;
assert!(ok.is_err(), "the deadline has passed");
let ok = once(&CallContext::new(), "query", async { Ok(7) }).await;
assert_eq!(ok.unwrap(), 7);
}
}