mirror of
https://github.com/euzu/tuliprox.git
synced 2026-10-19 22:32:26 +02:00
Feature/connection handling (#869)
* **New Features** * Added an opt-in runtime liveness watchdog with diagnostics, health status, and optional automatic restart. * Added configurable stream cleanup capacity and eviction re-entry protection. * Expanded configuration and UI access for users with playlist or user permissions, with safer data sanitization. * Added testkit scenarios and reporting for playback, admission, provider limits, sharing, and VOD behavior. * Added support for experimental-branch Docker builds. * **Bug Fixes** * Improved provider prioritization, shared playback capacity, cleanup, reconnect handling, and concurrent client behavior. * Fixed playlist parsing for declared media types. * Release links now use the configured destination.
This commit is contained in:
@@ -1,38 +1,22 @@
|
||||
use std::sync::Arc;
|
||||
use tuliprox_core::model::ProviderHandle;
|
||||
use tuliprox_session::ActiveProviderManager;
|
||||
use tuliprox_session::{ActiveProviderManager, ManagedProviderHandle};
|
||||
|
||||
pub struct ProbeHandleGuard {
|
||||
manager: Arc<ActiveProviderManager>,
|
||||
handle: Option<ProviderHandle>,
|
||||
}
|
||||
/// Thin wrapper around `ManagedProviderHandle` for probe allocations. It preserves the
|
||||
/// same synchronous release contract without duplicating the release/drop logic.
|
||||
pub struct ProbeHandleGuard(ManagedProviderHandle);
|
||||
|
||||
impl ProbeHandleGuard {
|
||||
pub fn new(manager: &Arc<ActiveProviderManager>, handle: ProviderHandle) -> Self {
|
||||
Self { manager: Arc::clone(manager), handle: Some(handle) }
|
||||
Self(ManagedProviderHandle::new(Arc::clone(manager), handle))
|
||||
}
|
||||
|
||||
#[inline]
|
||||
pub fn handle(&self) -> Option<&ProviderHandle> { self.handle.as_ref() }
|
||||
pub fn handle(&self) -> Option<&ProviderHandle> { self.0.handle() }
|
||||
|
||||
pub async fn release(mut self) {
|
||||
if let Some(handle) = self.handle.take() {
|
||||
self.manager.release_handle(&handle).await;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Drop for ProbeHandleGuard {
|
||||
fn drop(&mut self) {
|
||||
let Some(handle) = self.handle.take() else {
|
||||
return;
|
||||
};
|
||||
let manager = Arc::clone(&self.manager);
|
||||
if let Ok(runtime_handle) = tokio::runtime::Handle::try_current() {
|
||||
runtime_handle.spawn(async move {
|
||||
manager.release_handle(&handle).await;
|
||||
});
|
||||
}
|
||||
pub fn release(self) {
|
||||
// The inner managed handle releases the slot synchronously on drop.
|
||||
drop(self);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -105,15 +89,37 @@ mod tests {
|
||||
|
||||
let handle = manager
|
||||
.acquire_connection_for_probe(&input_name, default_probe_user_priority())
|
||||
.await
|
||||
.expect("probe allocation should succeed");
|
||||
assert_eq!(manager.get_provider_connections_count().await, 1);
|
||||
assert_eq!(manager.get_provider_connections_count(), 1);
|
||||
|
||||
let guard = ProbeHandleGuard::new(&manager, handle);
|
||||
drop(guard);
|
||||
tokio::task::yield_now().await;
|
||||
tokio::task::yield_now().await;
|
||||
|
||||
assert_eq!(manager.get_provider_connections_count().await, 0);
|
||||
assert_eq!(manager.get_provider_connections_count(), 0);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn probe_handle_guard_releases_provider_slot_outside_tokio_runtime() {
|
||||
let rt =
|
||||
tokio::runtime::Builder::new_current_thread().enable_all().build().expect("tokio runtime should build");
|
||||
|
||||
let (manager, guard) = rt.block_on(async {
|
||||
let app_cfg = create_test_app_config();
|
||||
let event_manager = Arc::new(EventManager::new());
|
||||
let manager = Arc::new(ActiveProviderManager::new(&app_cfg, &event_manager));
|
||||
let input_name = "provider_1".intern();
|
||||
|
||||
let handle = manager
|
||||
.acquire_connection_for_probe(&input_name, default_probe_user_priority())
|
||||
.expect("probe allocation should succeed");
|
||||
assert_eq!(manager.get_provider_connections_count(), 1);
|
||||
|
||||
let guard = ProbeHandleGuard::new(&manager, handle);
|
||||
(manager, guard)
|
||||
});
|
||||
|
||||
// Drop outside of any tokio runtime context:
|
||||
drop(guard);
|
||||
assert_eq!(manager.get_provider_connections_count(), 0);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1213,7 +1213,7 @@ mod tests {
|
||||
finish_completed_refresh_checked(&app_config, temp.path(), identity, &checkpoint, update_quality(90, 0, 0))
|
||||
.await?;
|
||||
|
||||
let StalkerRefreshOutcome::Failed { quality_acceptances, error: _, .. } = reports.into_outcome() else {
|
||||
let StalkerRefreshOutcome::Failed { quality_acceptances, .. } = reports.into_outcome() else {
|
||||
return Err("expected failed Stalker completion with retained reports".into());
|
||||
};
|
||||
assert_eq!(
|
||||
@@ -1258,7 +1258,7 @@ mod tests {
|
||||
finish_completed_refresh_checked(&app_config, temp.path(), identity, &checkpoint, update_quality(90, 0, 0))
|
||||
.await?;
|
||||
|
||||
let StalkerRefreshOutcome::Failed { quality_acceptances, error: _, .. } = reports.into_outcome() else {
|
||||
let StalkerRefreshOutcome::Failed { quality_acceptances, .. } = reports.into_outcome() else {
|
||||
return Err("expected failed Stalker completion with retained reports".into());
|
||||
};
|
||||
assert_eq!(quality_acceptances.len(), 1);
|
||||
|
||||
@@ -239,7 +239,6 @@ async fn prepare_generic_stream_metadata<E: EventSink>(
|
||||
} else {
|
||||
active_provider
|
||||
.acquire_connection_for_probe(&input.name, probe_priority)
|
||||
.await
|
||||
.map(|handle| ProbeHandleGuard::new(active_provider, handle))
|
||||
};
|
||||
|
||||
@@ -286,7 +285,7 @@ async fn prepare_generic_stream_metadata<E: EventSink>(
|
||||
};
|
||||
|
||||
if let Some(handle) = acquired_handle {
|
||||
handle.release().await;
|
||||
handle.release();
|
||||
}
|
||||
|
||||
let (raw_video, raw_audio, stats) = match probe_data {
|
||||
|
||||
@@ -120,10 +120,7 @@ async fn playlist_resolve_series_info<E: EventSink + Clone + 'static, M: Metadat
|
||||
target: &ConfigTarget,
|
||||
) {
|
||||
let filter = |pli: &PlaylistItem| {
|
||||
if pli.header.xtream_cluster != XtreamCluster::Series || pli.header.item_type != PlaylistItemType::SeriesInfo {
|
||||
return false;
|
||||
}
|
||||
true
|
||||
pli.header.xtream_cluster == XtreamCluster::Series && pli.header.item_type == PlaylistItemType::SeriesInfo
|
||||
};
|
||||
|
||||
// Skip if nothing to do
|
||||
@@ -928,7 +925,6 @@ pub async fn update_series_metadata(
|
||||
} else {
|
||||
active_provider
|
||||
.acquire_connection_for_probe(&input.name, probe_priority)
|
||||
.await
|
||||
.map(|handle| ProbeHandleGuard::new(active_provider, handle))
|
||||
};
|
||||
|
||||
@@ -957,7 +953,7 @@ pub async fn update_series_metadata(
|
||||
}
|
||||
}
|
||||
if let Some(guard) = temp_handle {
|
||||
guard.release().await;
|
||||
guard.release();
|
||||
}
|
||||
continue;
|
||||
}
|
||||
@@ -971,7 +967,7 @@ pub async fn update_series_metadata(
|
||||
probe_url.as_ref()
|
||||
);
|
||||
if let Some(guard) = temp_handle {
|
||||
guard.release().await;
|
||||
guard.release();
|
||||
}
|
||||
continue;
|
||||
}
|
||||
@@ -1045,7 +1041,7 @@ pub async fn update_series_metadata(
|
||||
}
|
||||
|
||||
if let Some(guard) = temp_handle {
|
||||
guard.release().await;
|
||||
guard.release();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -118,10 +118,7 @@ async fn playlist_resolve_vod_info<E: EventSink + Clone + 'static, M: MetadataUp
|
||||
do_probe: bool,
|
||||
) {
|
||||
let filter = |pli: &PlaylistItem| {
|
||||
if pli.header.xtream_cluster != XtreamCluster::Video || pli.header.item_type != PlaylistItemType::Video {
|
||||
return false;
|
||||
}
|
||||
true
|
||||
pli.header.xtream_cluster == XtreamCluster::Video && pli.header.item_type == PlaylistItemType::Video
|
||||
};
|
||||
|
||||
let resolve_tmdb_enabled = fpl.input.has_flag(ConfigInputFlags::ResolveTmdb);
|
||||
@@ -858,7 +855,6 @@ pub async fn update_vod_metadata(
|
||||
} else {
|
||||
active_provider
|
||||
.acquire_connection_for_probe(&input.name, probe_priority)
|
||||
.await
|
||||
.map(|handle| ProbeHandleGuard::new(active_provider, handle))
|
||||
};
|
||||
|
||||
@@ -927,7 +923,7 @@ pub async fn update_vod_metadata(
|
||||
}
|
||||
|
||||
if let Some(guard) = temp_handle {
|
||||
guard.release().await;
|
||||
guard.release();
|
||||
}
|
||||
} else {
|
||||
debug_if_enabled!(
|
||||
|
||||
Reference in New Issue
Block a user