use crate::utils::ChainError;
use std::sync::LazyLock;
use tokio::sync::Semaphore;
use tracing::warn;
pub const DEFAULT_MAX_CONCURRENT_PRICING_JOBS: usize = 4;
static PRICING_PERMITS: LazyLock<Semaphore> = LazyLock::new(|| {
let raw = super::env::read_var("OCS_MAX_CONCURRENT_PRICING_JOBS");
let configured = raw
.as_deref()
.and_then(|raw| raw.trim().parse::<usize>().ok())
.filter(|permits| *permits >= 1)
.unwrap_or_else(|| {
if raw.is_some() {
warn!(
default = DEFAULT_MAX_CONCURRENT_PRICING_JOBS,
"invalid OCS_MAX_CONCURRENT_PRICING_JOBS; falling back to the default"
);
}
DEFAULT_MAX_CONCURRENT_PRICING_JOBS
});
Semaphore::new(configured)
});
pub async fn admit_blocking<T, F>(job: F) -> Result<T, ChainError>
where
F: FnOnce() -> Result<T, ChainError> + Send + 'static,
T: Send + 'static,
{
let permit = PRICING_PERMITS.acquire().await.map_err(|error| {
ChainError::Internal(format!("the pricing admission gate is closed: {error}"))
})?;
let outcome = tokio::task::spawn_blocking(job)
.await
.map_err(|error| ChainError::Internal(format!("a pricing job did not finish: {error}")))?;
drop(permit);
outcome
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_the_default_matches_the_documentation() {
assert_eq!(DEFAULT_MAX_CONCURRENT_PRICING_JOBS, 4);
}
#[tokio::test]
async fn test_a_job_runs_under_the_bound() {
match admit_blocking(|| Ok(7_usize)).await {
Ok(value) => assert_eq!(value, 7),
Err(error) => panic!("the job must run: {error}"),
}
}
#[tokio::test]
async fn test_a_job_error_is_propagated() {
let outcome: Result<usize, ChainError> =
admit_blocking(|| Err(ChainError::Internal("the job failed".to_string()))).await;
match outcome {
Err(ChainError::Internal(message)) => assert_eq!(message, "the job failed"),
other => panic!("the job's own error must survive, got {other:?}"),
}
}
#[tokio::test]
async fn test_a_job_waits_when_every_permit_is_held() {
let held = match PRICING_PERMITS
.acquire_many(u32::try_from(DEFAULT_MAX_CONCURRENT_PRICING_JOBS).unwrap_or(1))
.await
{
Ok(permits) => permits,
Err(error) => panic!("the semaphore must hand out its permits: {error}"),
};
let mut job = Box::pin(admit_blocking(|| Ok(1_usize)));
match futures::future::select(
&mut job,
Box::pin(tokio::time::sleep(std::time::Duration::from_millis(50))),
)
.await
{
futures::future::Either::Left((outcome, _)) => {
panic!("the job must not start while the bound is full, got {outcome:?}")
}
futures::future::Either::Right(((), _)) => {}
}
drop(held);
match job.await {
Ok(value) => assert_eq!(value, 1),
Err(error) => panic!("the job must run once a permit frees: {error}"),
}
}
}