use rustc_hash::FxHashMap;
use tracing::warn;
use super::{LiquidityScope, SolverPoolHandle};
use crate::SolveError;
#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
pub enum ExclusiveAccess {
#[default]
Denied,
Granted,
}
#[derive(Debug, Clone, Copy)]
pub(crate) struct OrderClass {
exclusive_access: ExclusiveAccess,
}
impl OrderClass {
pub(crate) fn new(exclusive_access: ExclusiveAccess) -> Self {
Self { exclusive_access }
}
}
impl SolverPoolHandle {
pub(crate) fn serves(&self, class: OrderClass) -> bool {
match self.liquidity_scope() {
LiquidityScope::PublicOnly => true,
LiquidityScope::IncludeExclusive => class.exclusive_access == ExclusiveAccess::Granted,
}
}
}
pub(crate) struct Allocation<'a> {
worker_pools: Vec<&'a SolverPoolHandle>,
scopes: FxHashMap<String, LiquidityScope>,
exclusive_routing_active: bool,
}
impl<'a> Allocation<'a> {
pub(crate) fn worker_pools(&self) -> &[&'a SolverPoolHandle] {
&self.worker_pools
}
pub(crate) fn scopes(&self) -> &FxHashMap<String, LiquidityScope> {
&self.scopes
}
pub(crate) fn exclusive_routing_active(&self) -> bool {
self.exclusive_routing_active
}
pub(crate) fn is_exclusive(&self, worker_pool_name: &str) -> bool {
self.scopes.get(worker_pool_name) == Some(&LiquidityScope::IncludeExclusive)
}
pub(crate) fn is_empty(&self) -> bool {
self.worker_pools.is_empty()
}
}
pub(crate) fn validate_pool_allowlist(
worker_pools: &[SolverPoolHandle],
allowlist: &[String],
) -> Result<(), SolveError> {
if allowlist.is_empty() {
return Err(SolveError::InvalidWorkerPools(
"worker pool allowlist is empty; omit it to use every pool that serves the request"
.to_string(),
));
}
let unknown: Vec<&str> = allowlist
.iter()
.map(String::as_str)
.filter(|name| {
!worker_pools
.iter()
.any(|pool| pool.name() == *name)
})
.collect();
if !unknown.is_empty() {
let configured: Vec<&str> = worker_pools
.iter()
.map(SolverPoolHandle::name)
.collect();
warn!(?configured, ?unknown, "worker pool allowlist names unknown pool(s)");
return Err(SolveError::InvalidWorkerPools(format!("unknown worker pool(s) {unknown:?}")));
}
Ok(())
}
pub(crate) fn allocate<'a>(
worker_pools: &'a [SolverPoolHandle],
class: OrderClass,
pool_allowlist: Option<&[String]>,
) -> Allocation<'a> {
let worker_pools: Vec<&SolverPoolHandle> = worker_pools
.iter()
.filter(|worker_pool| worker_pool.serves(class))
.filter(|worker_pool| {
pool_allowlist.is_none_or(|allowlist| {
allowlist
.iter()
.any(|n| n == worker_pool.name())
})
})
.collect();
let scopes: FxHashMap<String, LiquidityScope> = worker_pools
.iter()
.map(|worker_pool| (worker_pool.name().to_string(), worker_pool.liquidity_scope()))
.collect();
let exclusive_routing_active = scopes
.values()
.any(|scope| *scope == LiquidityScope::IncludeExclusive);
Allocation { worker_pools, scopes, exclusive_routing_active }
}
#[cfg(test)]
mod tests {
use rstest::rstest;
use super::*;
use crate::{worker_pool::TaskQueueHandle, SolveError};
#[rstest]
#[case::public_scope_denied(LiquidityScope::PublicOnly, ExclusiveAccess::Denied, true)]
#[case::public_scope_granted(LiquidityScope::PublicOnly, ExclusiveAccess::Granted, true)]
#[case::exclusive_scope_denied(
LiquidityScope::IncludeExclusive,
ExclusiveAccess::Denied,
false
)]
#[case::exclusive_scope_granted(
LiquidityScope::IncludeExclusive,
ExclusiveAccess::Granted,
true
)]
fn test_serves(
#[case] scope: LiquidityScope,
#[case] access: ExclusiveAccess,
#[case] expected: bool,
) {
let (tx, _rx) = async_channel::bounded(1);
let worker_pool = SolverPoolHandle::new("worker_pool", TaskQueueHandle::from_sender(tx))
.with_liquidity_scope(scope);
assert_eq!(worker_pool.serves(OrderClass::new(access)), expected);
}
fn handle(name: &str) -> SolverPoolHandle {
let (tx, _rx) = async_channel::bounded(1);
SolverPoolHandle::new(name, TaskQueueHandle::from_sender(tx))
}
fn names<'a>(allocation: &Allocation<'a>) -> Vec<&'a str> {
allocation
.worker_pools()
.iter()
.map(|pool| pool.name())
.collect()
}
#[test]
fn test_allocate_without_allowlist_keeps_every_serving_pool() {
let pools = [handle("a"), handle("b")];
let allocation = allocate(&pools, OrderClass::new(ExclusiveAccess::Denied), None);
assert_eq!(names(&allocation), vec!["a", "b"]);
}
#[test]
fn test_allocate_allowlist_selects_subset_in_configuration_order() {
let pools = [handle("a"), handle("b"), handle("c")];
let allowlist = ["c".to_string(), "a".to_string()];
let allocation =
allocate(&pools, OrderClass::new(ExclusiveAccess::Denied), Some(&allowlist));
assert_eq!(names(&allocation), vec!["a", "c"]);
}
#[test]
fn test_validate_pool_allowlist_unknown_pool_is_an_error() {
let pools = [handle("a")];
let allowlist = ["a".to_string(), "nope".to_string()];
let Err(err) = validate_pool_allowlist(&pools, &allowlist) else {
panic!("expected an error for an unknown pool name");
};
let SolveError::InvalidWorkerPools(message) = err else {
panic!("expected InvalidWorkerPools, got {err:?}")
};
assert!(message.contains("nope"), "{message}");
assert!(!message.contains("\"a\""), "{message}");
}
#[test]
fn test_validate_pool_allowlist_empty_is_an_error() {
let pools = [handle("a")];
let allowlist: [String; 0] = [];
let Err(err) = validate_pool_allowlist(&pools, &allowlist) else {
panic!("expected an error for an empty allowlist");
};
let SolveError::InvalidWorkerPools(message) = err else {
panic!("expected InvalidWorkerPools, got {err:?}")
};
assert!(message.contains("empty"), "{message}");
}
#[test]
fn test_allocate_allowlist_cannot_reach_exclusive_pool_without_access() {
let pools = [
handle("public"),
handle("exclusive").with_liquidity_scope(LiquidityScope::IncludeExclusive),
];
let allowlist = ["exclusive".to_string()];
let allocation =
allocate(&pools, OrderClass::new(ExclusiveAccess::Denied), Some(&allowlist));
assert!(allocation.is_empty());
}
}