mirror of
https://github.com/euzu/tuliprox.git
synced 2026-10-31 11:12:36 +01:00
staged m3u to xtream fix (#877)
* staged m3u to xtream fix * staged m3u to xtream fix * staged m3u to xtream fix * fixed race some conditions * fixed race some conditions * fixed race some conditions * fixed race some conditions * fixed race some conditions --------- Co-authored-by: euzu <euzu@proton.me>
This commit is contained in:
@@ -38,6 +38,18 @@ type PreemptionCandidate = (PriorityOwner, AllocationId, i8, Instant);
|
||||
type PriorityKey = (i8, Reverse<Instant>, AllocationId);
|
||||
|
||||
const PREEMPTION_COMPLETION_TIMEOUT: Duration = Duration::from_millis(1500);
|
||||
const EVICTED_PROVIDER_RELEASE_POLL_INTERVAL: Duration = Duration::from_millis(10);
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, Eq)]
|
||||
pub(crate) struct ProviderReleaseSnapshot {
|
||||
pub addr: SocketAddr,
|
||||
single_allocations: Vec<AllocationId>,
|
||||
shared_subscribers: Vec<SharedSubscriberId>,
|
||||
}
|
||||
|
||||
impl ProviderReleaseSnapshot {
|
||||
pub fn is_empty(&self) -> bool { self.single_allocations.is_empty() && self.shared_subscribers.is_empty() }
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
pub enum ConnectionKind {
|
||||
@@ -118,6 +130,7 @@ impl AcquireProviderParams<'_> {
|
||||
#[derive(Debug, Clone)]
|
||||
struct SharedAllocation {
|
||||
allocation_id: AllocationId,
|
||||
origin_subscriber_id: SharedSubscriberId,
|
||||
allocation: ProviderAllocation,
|
||||
/// Keyed by unique subscriber id, never by socket: two external clients behind one
|
||||
/// reverse proxy must not collapse into a single entry.
|
||||
@@ -444,6 +457,85 @@ impl std::ops::Deref for ActiveProviderManager {
|
||||
}
|
||||
|
||||
impl ActiveProviderManager {
|
||||
fn has_connections_for_addr(&self, addr: &SocketAddr) -> bool {
|
||||
let _transition = self.lock_capacity_transition();
|
||||
let connections = self.read_connections();
|
||||
connections.single.values().any(|info| info.client_addr == *addr)
|
||||
|| connections
|
||||
.shared
|
||||
.by_key
|
||||
.values()
|
||||
.any(|shared| shared.connections.values().any(|subscriber| subscriber.addr == *addr))
|
||||
}
|
||||
|
||||
/// Waits for every allocation currently using an address to leave the registry.
|
||||
/// Admission handoffs use `wait_for_snapshot_release` to ignore later allocations at that address.
|
||||
pub async fn wait_for_addr_release(&self, addr: &SocketAddr, timeout: Duration) -> bool {
|
||||
let deadline = TokioInstant::now() + timeout;
|
||||
loop {
|
||||
if !self.has_connections_for_addr(addr) {
|
||||
return true;
|
||||
}
|
||||
let now = TokioInstant::now();
|
||||
if now >= deadline {
|
||||
return false;
|
||||
}
|
||||
tokio::time::sleep_until((now + EVICTED_PROVIDER_RELEASE_POLL_INTERVAL).min(deadline)).await;
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn release_snapshot_for_addr(&self, addr: &SocketAddr) -> ProviderReleaseSnapshot {
|
||||
let _transition = self.lock_capacity_transition();
|
||||
let connections = self.read_connections();
|
||||
let single_allocations = connections
|
||||
.single
|
||||
.values()
|
||||
.filter_map(|info| (info.client_addr == *addr).then_some(info.allocation_id))
|
||||
.collect();
|
||||
let shared_subscribers = connections
|
||||
.shared
|
||||
.by_key
|
||||
.values()
|
||||
.flat_map(|shared| {
|
||||
shared.connections.iter().filter_map(|(id, subscriber)| (subscriber.addr == *addr).then_some(*id))
|
||||
})
|
||||
.collect();
|
||||
ProviderReleaseSnapshot { addr: *addr, single_allocations, shared_subscribers }
|
||||
}
|
||||
|
||||
fn has_connections_from_snapshot(&self, snapshot: &ProviderReleaseSnapshot) -> bool {
|
||||
let _transition = self.lock_capacity_transition();
|
||||
let connections = self.read_connections();
|
||||
snapshot.single_allocations.iter().any(|id| {
|
||||
connections.single.contains_key(id)
|
||||
|| connections.shared.shared_by_allocation_id.get(id).is_some_and(|key| {
|
||||
connections.shared.by_key.get(key).is_some_and(|shared| {
|
||||
connections.shared.key_by_subscriber.get(&shared.origin_subscriber_id) == Some(key)
|
||||
})
|
||||
})
|
||||
}) || snapshot.shared_subscribers.iter().any(|id| connections.shared.key_by_subscriber.contains_key(id))
|
||||
}
|
||||
|
||||
/// Waits for the kicked transport's original provider allocations to leave the registry.
|
||||
/// The socket close may be signalled before its response bodies and provider handles are dropped.
|
||||
pub(crate) async fn wait_for_snapshot_release(
|
||||
&self,
|
||||
snapshot: &ProviderReleaseSnapshot,
|
||||
timeout: Duration,
|
||||
) -> bool {
|
||||
let deadline = TokioInstant::now() + timeout;
|
||||
loop {
|
||||
if !self.has_connections_from_snapshot(snapshot) {
|
||||
return true;
|
||||
}
|
||||
let now = TokioInstant::now();
|
||||
if now >= deadline {
|
||||
return false;
|
||||
}
|
||||
tokio::time::sleep_until((now + EVICTED_PROVIDER_RELEASE_POLL_INTERVAL).min(deadline)).await;
|
||||
}
|
||||
}
|
||||
|
||||
fn upsert_priority_entry(
|
||||
connections: &mut Connections,
|
||||
provider_name: &Arc<str>,
|
||||
@@ -2515,6 +2607,7 @@ impl ActiveProviderManager {
|
||||
Arc::clone(&shared_key),
|
||||
SharedAllocation {
|
||||
allocation_id: handle.allocation_id,
|
||||
origin_subscriber_id: subscriber_id,
|
||||
allocation: info.allocation,
|
||||
connections: HashMap::from([(
|
||||
subscriber_id,
|
||||
@@ -2632,7 +2725,7 @@ impl ActiveProviderManager {
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::{ActiveProviderManager, ConnectionKind, PlaybackLeaseRef};
|
||||
use crate::{EventManager, SharedStreamManager};
|
||||
use crate::{ActiveUserManager, EventManager, SharedStreamManager};
|
||||
use arc_swap::{ArcSwap, ArcSwapOption};
|
||||
use shared::{
|
||||
defaults::{default_probe_user_priority, default_user_priority},
|
||||
@@ -2873,6 +2966,66 @@ mod tests {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn release_snapshot_tracks_original_allocation_after_addr_reuse() {
|
||||
let app_cfg = create_test_app_config_with_pool(2, 3);
|
||||
let events = Arc::new(EventManager::new());
|
||||
let provider = ActiveProviderManager::new(&app_cfg, &events);
|
||||
let addr = SocketAddr::from(([127, 0, 0, 1], 50_022));
|
||||
let input = "provider_1".intern();
|
||||
let original = provider
|
||||
.acquire_connection_with_grace_for_session(&input, &addr, false, 0, ConnectionKind::Normal, Some("old"))
|
||||
.expect("original allocation");
|
||||
let original_snapshot = provider.release_snapshot_for_addr(&addr);
|
||||
assert!(!provider.wait_for_snapshot_release(&original_snapshot, Duration::ZERO).await);
|
||||
|
||||
provider.release_handle(&original);
|
||||
let replacement = provider
|
||||
.acquire_connection_with_grace_for_session(&input, &addr, false, 0, ConnectionKind::Normal, Some("new"))
|
||||
.expect("replacement allocation");
|
||||
let replacement_snapshot = provider.release_snapshot_for_addr(&addr);
|
||||
assert!(provider.wait_for_snapshot_release(&original_snapshot, Duration::ZERO).await);
|
||||
assert!(!provider.wait_for_snapshot_release(&replacement_snapshot, Duration::ZERO).await);
|
||||
|
||||
let geoip = Arc::new(ArcSwapOption::default());
|
||||
let users = ActiveUserManager::new(&Config::default(), &geoip, &events);
|
||||
users.set_pending_provider_release("user", original_snapshot.clone()).await;
|
||||
users.set_pending_provider_release("user", replacement_snapshot.clone()).await;
|
||||
assert!(!users.clear_pending_provider_release("user", &original_snapshot).await);
|
||||
assert_eq!(users.pending_provider_release("user").await, Some(replacement_snapshot.clone()));
|
||||
assert!(users.clear_pending_provider_release("user", &replacement_snapshot).await);
|
||||
assert_eq!(users.pending_provider_release("user").await, None);
|
||||
provider.release_handle(&replacement);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn release_snapshot_tracks_shared_subscriber_without_waiting_for_other_subscribers() {
|
||||
let app_cfg = create_test_app_config_single_provider_pool();
|
||||
let events = Arc::new(EventManager::new());
|
||||
let provider = ActiveProviderManager::new(&app_cfg, &events);
|
||||
let input = "provider_1".intern();
|
||||
let first_addr = SocketAddr::from(([127, 0, 0, 1], 50_023));
|
||||
let second_addr = SocketAddr::from(([127, 0, 0, 1], 50_024));
|
||||
let first = SharedSubscriberId::from_stream_uid(50_023);
|
||||
let second = SharedSubscriberId::from_stream_uid(50_024);
|
||||
let origin =
|
||||
provider.acquire_connection(&input, &first_addr, 0, ConnectionKind::Normal).expect("shared origin");
|
||||
let before_promotion = provider.release_snapshot_for_addr(&first_addr);
|
||||
assert!(provider.make_shared_connection(&origin, "shared-release", first));
|
||||
provider
|
||||
.add_shared_connection(&second_addr, second, "shared-release", 0, ConnectionKind::Normal)
|
||||
.expect("second subscriber");
|
||||
let snapshot = provider.release_snapshot_for_addr(&first_addr);
|
||||
assert!(!provider.wait_for_snapshot_release(&before_promotion, Duration::ZERO).await);
|
||||
assert!(!provider.wait_for_snapshot_release(&snapshot, Duration::ZERO).await);
|
||||
|
||||
provider.release_connection(&first_addr);
|
||||
assert!(provider.wait_for_snapshot_release(&before_promotion, Duration::ZERO).await);
|
||||
assert!(provider.wait_for_snapshot_release(&snapshot, Duration::ZERO).await);
|
||||
assert_eq!(provider.get_provider_connections_count(), 1);
|
||||
provider.release_connection(&second_addr);
|
||||
}
|
||||
|
||||
#[tokio::test(start_paused = true)]
|
||||
async fn manifest_retries_without_media_do_not_reserve_capacity() {
|
||||
let app_cfg = create_test_app_config_with_pool(2, 3);
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
use crate::{
|
||||
active_provider_manager::ConnectionKind,
|
||||
active_provider_manager::{ConnectionKind, ProviderReleaseSnapshot},
|
||||
connection_manager::CleanupEvent,
|
||||
stream::{uses_direct_body_idle_timeout, DIRECT_BODY_IDLE_TIMEOUT_SECS},
|
||||
ActiveProviderManager, EventManager,
|
||||
@@ -690,6 +690,9 @@ pub struct ActiveUserManager {
|
||||
cleanup_tx: tokio::sync::OnceCell<mpsc::Sender<CleanupEvent>>,
|
||||
provider_manager: tokio::sync::OnceCell<Arc<ActiveProviderManager>>,
|
||||
transition_gates: Mutex<HashMap<String, Arc<Mutex<()>>>>,
|
||||
// An evicted stream can keep its provider slot after its user count is released.
|
||||
// Retain the handoff across bounded admission attempts until the slot is gone.
|
||||
pending_provider_releases: Mutex<HashMap<String, ProviderReleaseSnapshot>>,
|
||||
pub dropped_cleanup_events: AtomicU64,
|
||||
reentry_suppressed_total: AtomicU64,
|
||||
divergence_cache: Mutex<LruCache<String, DivergenceEntry>>,
|
||||
@@ -791,6 +794,7 @@ impl ActiveUserManager {
|
||||
cleanup_tx: tokio::sync::OnceCell::new(),
|
||||
provider_manager: tokio::sync::OnceCell::new(),
|
||||
transition_gates: Mutex::new(HashMap::new()),
|
||||
pending_provider_releases: Mutex::new(HashMap::new()),
|
||||
dropped_cleanup_events: AtomicU64::new(0),
|
||||
reentry_suppressed_total: AtomicU64::new(0),
|
||||
divergence_cache: Mutex::new(LruCache::new(DIVERGENCE_CACHE_CAPACITY)),
|
||||
@@ -845,6 +849,27 @@ impl ActiveUserManager {
|
||||
gate.lock_owned().await
|
||||
}
|
||||
|
||||
pub(crate) async fn pending_provider_release(&self, username: &str) -> Option<ProviderReleaseSnapshot> {
|
||||
self.pending_provider_releases.lock().await.get(username).cloned()
|
||||
}
|
||||
|
||||
pub(crate) async fn set_pending_provider_release(&self, username: &str, snapshot: ProviderReleaseSnapshot) {
|
||||
self.pending_provider_releases.lock().await.insert(username.to_owned(), snapshot);
|
||||
}
|
||||
|
||||
pub(crate) async fn clear_pending_provider_release(
|
||||
&self,
|
||||
username: &str,
|
||||
snapshot: &ProviderReleaseSnapshot,
|
||||
) -> bool {
|
||||
let mut pending = self.pending_provider_releases.lock().await;
|
||||
if pending.get(username) != Some(snapshot) {
|
||||
return false;
|
||||
}
|
||||
pending.remove(username);
|
||||
true
|
||||
}
|
||||
|
||||
fn should_reuse_stream_for_session(existing_stream: &StreamInfo, incoming_channel: &StreamChannel) -> bool {
|
||||
existing_stream.channel.item_type.requires_provider_affinity()
|
||||
|| incoming_channel.item_type.requires_provider_affinity()
|
||||
|
||||
@@ -18,7 +18,7 @@ use crate::{
|
||||
};
|
||||
use log::debug;
|
||||
use shared::model::{AdmissionStrategy, ConnectionDenied, EventMessage, UserConnectionPermission, VirtualId};
|
||||
use std::sync::Arc;
|
||||
use std::{sync::Arc, time::Duration};
|
||||
use tuliprox_core::model::{AppConfig, Fingerprint, ProxyUserCredentials};
|
||||
|
||||
/// The handles admission reads from the running server.
|
||||
@@ -34,6 +34,7 @@ pub struct AdmissionCtx {
|
||||
|
||||
/// Default duration an eviction is remembered if not configured.
|
||||
pub const DEFAULT_RECENT_EVICTION_REENTRY_TTL_MS: u64 = shared::defaults::DEFAULT_RECENT_EVICTION_REENTRY_TTL_MS;
|
||||
const EVICTED_PROVIDER_RELEASE_TIMEOUT: Duration = Duration::from_millis(1500);
|
||||
|
||||
/// Reentry cooldown from a resolved stream config, falling back to the default when
|
||||
/// no `reverse_proxy.stream` block is configured.
|
||||
@@ -287,6 +288,22 @@ enum StrategyLoopResult {
|
||||
Rejected(crate::AdmissionRejectionReason),
|
||||
}
|
||||
|
||||
async fn pending_provider_release_ready(adm: &AdmissionCtx, username: &str) -> bool {
|
||||
let Some(snapshot) = adm.active_users.pending_provider_release(username).await else {
|
||||
return true;
|
||||
};
|
||||
if !adm
|
||||
.connection_manager
|
||||
.provider_manager
|
||||
.wait_for_snapshot_release(&snapshot, EVICTED_PROVIDER_RELEASE_TIMEOUT)
|
||||
.await
|
||||
{
|
||||
debug!("Provider allocation for evicted connection {} remains active after close timeout", snapshot.addr);
|
||||
return false;
|
||||
}
|
||||
adm.active_users.clear_pending_provider_release(username, &snapshot).await
|
||||
}
|
||||
|
||||
/// Shared strategy-evaluation loop used by both the initial admission path
|
||||
/// (`resolve_admission_with_strategies`) and the remaining-strategies path
|
||||
/// (`evaluate_remaining_strategies_after_grace`).
|
||||
@@ -361,7 +378,36 @@ where
|
||||
let connections_before = adm.active_users.user_connections(username).await;
|
||||
let ttl = get_reentry_ttl(adm);
|
||||
adm.active_users.mark_recent_eviction_guard_for_addr(&target.addr, *request_addr, ttl).await;
|
||||
let pending_release = if target.addr == *request_addr {
|
||||
None
|
||||
} else {
|
||||
let snapshot = adm.connection_manager.provider_manager.release_snapshot_for_addr(&target.addr);
|
||||
if snapshot.is_empty() {
|
||||
None
|
||||
} else {
|
||||
adm.active_users.set_pending_provider_release(username, snapshot.clone()).await;
|
||||
Some(snapshot)
|
||||
}
|
||||
};
|
||||
adm.connection_manager.release_connection_as_kicked(&target.addr).await;
|
||||
// This request cannot wait for its own transport to close.
|
||||
if let Some(snapshot) = pending_release {
|
||||
let released = adm
|
||||
.connection_manager
|
||||
.provider_manager
|
||||
.wait_for_snapshot_release(&snapshot, EVICTED_PROVIDER_RELEASE_TIMEOUT)
|
||||
.await;
|
||||
if !released {
|
||||
debug!(
|
||||
"Provider allocation for evicted connection {} remains active after close timeout",
|
||||
target.addr
|
||||
);
|
||||
return StrategyLoopResult::Rejected(crate::AdmissionRejectionReason::UserConnectionsExhausted);
|
||||
}
|
||||
if !adm.active_users.clear_pending_provider_release(username, &snapshot).await {
|
||||
return StrategyLoopResult::Rejected(crate::AdmissionRejectionReason::UserConnectionsExhausted);
|
||||
}
|
||||
}
|
||||
performed_legitimate_eviction = true;
|
||||
let retry_admission = get_admission_for_request(adm, request).await;
|
||||
if retry_admission.permission() == UserConnectionPermission::Allowed {
|
||||
@@ -416,7 +462,34 @@ pub async fn resolve_admission_with_strategies(
|
||||
let username = request.username;
|
||||
let admission = get_admission_for_request(adm, &request).await;
|
||||
|
||||
if admission.permission() != UserConnectionPermission::Exhausted
|
||||
&& adm.active_users.pending_provider_release(username).await.is_none()
|
||||
{
|
||||
return AdmissionStrategyResolution { admission, grace_mode: None, grace_context: None };
|
||||
}
|
||||
|
||||
let _admission_guard = adm.active_users.acquire_user_admission(username).await;
|
||||
|
||||
if !pending_provider_release_ready(adm, username).await {
|
||||
return AdmissionStrategyResolution {
|
||||
admission: crate::ConnectionAdmission::exhausted(
|
||||
crate::AdmissionRejectionReason::UserConnectionsExhausted,
|
||||
admission.kind(),
|
||||
),
|
||||
grace_mode: None,
|
||||
grace_context: None,
|
||||
};
|
||||
}
|
||||
|
||||
// Re-read admission now that the gate is held. The first read above happened
|
||||
// before we queued on the gate, so a request ahead of us may have released
|
||||
// the very slot we are about to evict somebody for. Walking the strategies on
|
||||
// the stale snapshot kicks a live connection to free a slot that is already
|
||||
// free.
|
||||
let admission = get_admission_for_request(adm, &request).await;
|
||||
|
||||
if admission.permission() != UserConnectionPermission::Exhausted {
|
||||
debug!("Admission became available while waiting on the admission gate for user {username}");
|
||||
return AdmissionStrategyResolution { admission, grace_mode: None, grace_context: None };
|
||||
}
|
||||
|
||||
@@ -433,20 +506,6 @@ pub async fn resolve_admission_with_strategies(
|
||||
};
|
||||
}
|
||||
|
||||
let _admission_guard = adm.active_users.acquire_user_admission(username).await;
|
||||
|
||||
// Re-read admission now that the gate is held. The first read above happened
|
||||
// before we queued on the gate, so a request ahead of us may have released
|
||||
// the very slot we are about to evict somebody for. Walking the strategies on
|
||||
// the stale snapshot kicks a live connection to free a slot that is already
|
||||
// free.
|
||||
let admission = get_admission_for_request(adm, &request).await;
|
||||
|
||||
if admission.permission() != UserConnectionPermission::Exhausted {
|
||||
debug!("Admission became available while waiting on the admission gate for user {username}");
|
||||
return AdmissionStrategyResolution { admission, grace_mode: None, grace_context: None };
|
||||
}
|
||||
|
||||
let build_grace_ctx = |global_idx: usize| GraceResolutionContext {
|
||||
strategy_index: global_idx,
|
||||
strategies: strategies.clone(),
|
||||
@@ -513,6 +572,17 @@ pub async fn evaluate_remaining_strategies_after_grace(
|
||||
|
||||
let _admission_guard = adm.active_users.acquire_user_admission(username).await;
|
||||
|
||||
if !pending_provider_release_ready(adm, username).await {
|
||||
return AdmissionStrategyResolution {
|
||||
admission: crate::ConnectionAdmission::exhausted(
|
||||
crate::AdmissionRejectionReason::UserConnectionsExhausted,
|
||||
original_kind,
|
||||
),
|
||||
grace_mode: None,
|
||||
grace_context: None,
|
||||
};
|
||||
}
|
||||
|
||||
match evaluate_admission_strategy_loop(
|
||||
adm,
|
||||
&request,
|
||||
|
||||
@@ -278,32 +278,7 @@ impl MultiProviderLineup {
|
||||
names
|
||||
}
|
||||
|
||||
/// Attempts to acquire the next available provider from a specific priority group.
|
||||
///
|
||||
/// # Parameters
|
||||
/// - `priority_group`: The provider group to search within.
|
||||
///
|
||||
/// # Returns
|
||||
/// - `ProviderAllocation`: A reference to the next available provider in the specified group.
|
||||
///
|
||||
/// # Behavior
|
||||
/// - Iterates through the providers in the given group in a round-robin manner.
|
||||
/// - Checks if a provider has available capacity before selecting it.
|
||||
/// - Uses atomic operations to maintain fair provider selection.
|
||||
///
|
||||
/// # Thread Safety
|
||||
/// - Uses `RwLock` for safe concurrent access.
|
||||
/// - Ensures fair provider allocation across multiple threads.
|
||||
///
|
||||
/// # Example Usage
|
||||
/// ```text
|
||||
/// let lineup = MultiProviderLineup::new(&config);
|
||||
/// match lineup.acquire_next_provider_from_group(priority_group).await {
|
||||
/// ProviderAllocation::Exhausted => println!("All providers exhausted"),
|
||||
/// ProviderAllocation::Available(provider) => println!("Provider available {}", provider.name),
|
||||
/// ProviderAllocation::GracePeriod(provider) => println!("Provider with grace period {}", provider.name),
|
||||
/// }
|
||||
/// ```
|
||||
/// Attempts to acquire the next available provider from a priority group in round-robin order.
|
||||
fn acquire_next_provider_from_group(
|
||||
priority_group: &ProviderPriorityGroup,
|
||||
grace: bool,
|
||||
@@ -400,93 +375,61 @@ impl MultiProviderLineup {
|
||||
}
|
||||
|
||||
/// Attempts to acquire a provider from the lineup based on priority and availability.
|
||||
///
|
||||
/// # Returns
|
||||
/// - `ProviderAllocation`: A reference to the acquired provider if allocation was successful.
|
||||
///
|
||||
/// # Behavior
|
||||
/// - The method iterates through provider priority groups in a round-robin fashion.
|
||||
/// - It attempts to allocate a provider from the highest priority group first.
|
||||
/// - If a provider has available capacity, it is returned.
|
||||
/// - If all providers in a group are exhausted, it moves to the next group.
|
||||
/// - Updates the internal index to ensure fair distribution of requests.
|
||||
///
|
||||
/// # Thread Safety
|
||||
/// - Uses atomic operations (`AtomicUsize`) for thread-safe indexing.
|
||||
/// - Uses `RwLock` for thread-safe provider allocation.
|
||||
///
|
||||
/// # Example Usage
|
||||
/// ```text
|
||||
/// let lineup = MultiProviderLineup::new(&config);
|
||||
/// match lineup.acquire().await {
|
||||
/// ProviderAllocation::Exhausted => println!("All providers exhausted"),
|
||||
/// ProviderAllocation::Available(provider) => println!("Provider available {}", provider.name),
|
||||
/// ProviderAllocation::GracePeriod(provider) => println!("Provider with grace period {}", provider.name),
|
||||
/// }
|
||||
/// ```
|
||||
#[cfg(test)]
|
||||
fn acquire(&self, with_grace: bool, grace_period_timeout_secs: u64) -> ProviderAllocation {
|
||||
self.acquire_excluding(with_grace, grace_period_timeout_secs, &std::collections::HashSet::new())
|
||||
}
|
||||
|
||||
fn scan_priority_groups<T>(
|
||||
&self,
|
||||
with_grace: bool,
|
||||
mut select: impl FnMut(&ProviderPriorityGroup, bool) -> Option<T>,
|
||||
) -> Option<T> {
|
||||
// Prefer providers with available capacity (no grace allocations),
|
||||
// scanning priority groups from highest -> lowest.
|
||||
for priority_group in &self.providers {
|
||||
if let Some(item) = select(priority_group, false) {
|
||||
return Some(item);
|
||||
}
|
||||
}
|
||||
|
||||
if !with_grace {
|
||||
return None;
|
||||
}
|
||||
|
||||
// If every provider is at capacity, allow grace allocations while respecting priority order.
|
||||
for priority_group in &self.providers {
|
||||
if let Some(item) = select(priority_group, true) {
|
||||
return Some(item);
|
||||
}
|
||||
}
|
||||
|
||||
None
|
||||
}
|
||||
|
||||
fn acquire_excluding(
|
||||
&self,
|
||||
with_grace: bool,
|
||||
grace_period_timeout_secs: u64,
|
||||
excluded_providers: &std::collections::HashSet<Arc<str>>,
|
||||
) -> ProviderAllocation {
|
||||
// Prefer providers with available capacity (no grace allocations),
|
||||
// scanning priority groups from highest -> lowest.
|
||||
for priority_group in &self.providers {
|
||||
self.scan_priority_groups(with_grace, |priority_group, grace| {
|
||||
let allocation = Self::acquire_next_provider_from_group(
|
||||
priority_group,
|
||||
false,
|
||||
grace,
|
||||
grace_period_timeout_secs,
|
||||
excluded_providers,
|
||||
);
|
||||
if !matches!(allocation, ProviderAllocation::Exhausted) {
|
||||
return allocation;
|
||||
}
|
||||
}
|
||||
|
||||
if !with_grace {
|
||||
return ProviderAllocation::Exhausted;
|
||||
}
|
||||
|
||||
// If every provider is at capacity, allow grace allocations while respecting priority order.
|
||||
for priority_group in &self.providers {
|
||||
let allocation = Self::acquire_next_provider_from_group(
|
||||
priority_group,
|
||||
true,
|
||||
grace_period_timeout_secs,
|
||||
excluded_providers,
|
||||
);
|
||||
if !matches!(allocation, ProviderAllocation::Exhausted) {
|
||||
return allocation;
|
||||
}
|
||||
}
|
||||
|
||||
ProviderAllocation::Exhausted
|
||||
(!matches!(allocation, ProviderAllocation::Exhausted)).then_some(allocation)
|
||||
})
|
||||
.unwrap_or(ProviderAllocation::Exhausted)
|
||||
}
|
||||
|
||||
// it intended to use with redirects to cycle through provider
|
||||
// It is intended to use with redirects to cycle through providers.
|
||||
fn get_next(&self, grace_period_timeout_secs: u64) -> Option<Arc<ProviderConfig>> {
|
||||
// Prefer providers with available capacity (no grace allocations),
|
||||
// scanning priority groups from highest -> lowest.
|
||||
for priority_group in &self.providers {
|
||||
if let Some(config) = Self::get_next_provider_from_group(priority_group, false, grace_period_timeout_secs) {
|
||||
return Some(config);
|
||||
}
|
||||
}
|
||||
|
||||
// If no provider is available, allow grace.
|
||||
for priority_group in &self.providers {
|
||||
if let Some(config) = Self::get_next_provider_from_group(priority_group, true, grace_period_timeout_secs) {
|
||||
return Some(config);
|
||||
}
|
||||
}
|
||||
|
||||
None
|
||||
self.scan_priority_groups(true, |priority_group, grace| {
|
||||
Self::get_next_provider_from_group(priority_group, grace, grace_period_timeout_secs)
|
||||
})
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
|
||||
Reference in New Issue
Block a user