mirror of
https://github.com/nimbold/Firelink.git
synced 2026-07-26 12:08:27 +00:00
fix(queue): dispatch admitted downloads concurrently
This commit is contained in:
@@ -1199,7 +1199,15 @@ impl<R: tauri::Runtime> QueueManager<R> {
|
|||||||
loop {
|
loop {
|
||||||
let notified = self.notify.notified();
|
let notified = self.notify.notified();
|
||||||
if let Some((permit, task)) = self.try_admit_next_task().await {
|
if let Some((permit, task)) = self.try_admit_next_task().await {
|
||||||
Arc::clone(&self).dispatch_one(permit, task).await;
|
// Admission owns the global and per-queue reservation before
|
||||||
|
// this task is spawned. Keep the dispatcher free to admit
|
||||||
|
// other work while an Aria2 addUri call is retrying or a
|
||||||
|
// control RPC is slow; dispatch_one retains the reservation
|
||||||
|
// until that lifecycle reaches its own terminal path.
|
||||||
|
let manager = Arc::clone(&self);
|
||||||
|
tauri::async_runtime::spawn(async move {
|
||||||
|
manager.dispatch_one(permit, task).await;
|
||||||
|
});
|
||||||
} else {
|
} else {
|
||||||
// This covers both an empty pending list and the case where
|
// This covers both an empty pending list and the case where
|
||||||
// all queue/global capacity is occupied. The notification
|
// all queue/global capacity is occupied. The notification
|
||||||
|
|||||||
@@ -27,6 +27,13 @@ struct DelayedAria2Spawner {
|
|||||||
remove_uri_calls: AtomicUsize,
|
remove_uri_calls: AtomicUsize,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
struct BlockingAria2Spawner {
|
||||||
|
first_started: tokio::sync::Notify,
|
||||||
|
second_started: tokio::sync::Notify,
|
||||||
|
release_first: tokio::sync::Notify,
|
||||||
|
add_uri_calls: AtomicUsize,
|
||||||
|
}
|
||||||
|
|
||||||
struct FailFirstAria2Spawner {
|
struct FailFirstAria2Spawner {
|
||||||
add_uri_calls: AtomicUsize,
|
add_uri_calls: AtomicUsize,
|
||||||
fail_first: std::sync::atomic::AtomicBool,
|
fail_first: std::sync::atomic::AtomicBool,
|
||||||
@@ -56,6 +63,49 @@ impl DelayedAria2Spawner {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
impl BlockingAria2Spawner {
|
||||||
|
fn new() -> Self {
|
||||||
|
Self {
|
||||||
|
first_started: tokio::sync::Notify::new(),
|
||||||
|
second_started: tokio::sync::Notify::new(),
|
||||||
|
release_first: tokio::sync::Notify::new(),
|
||||||
|
add_uri_calls: AtomicUsize::new(0),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[async_trait::async_trait]
|
||||||
|
impl SidecarSpawner for BlockingAria2Spawner {
|
||||||
|
async fn add_uri(&self, _id: &str, _payload: &SpawnPayload) -> Result<String, String> {
|
||||||
|
let call = self.add_uri_calls.fetch_add(1, Ordering::SeqCst) + 1;
|
||||||
|
match call {
|
||||||
|
1 => {
|
||||||
|
self.first_started.notify_one();
|
||||||
|
self.release_first.notified().await;
|
||||||
|
Ok("gid-first".to_string())
|
||||||
|
}
|
||||||
|
2 => {
|
||||||
|
self.second_started.notify_one();
|
||||||
|
Ok("gid-second".to_string())
|
||||||
|
}
|
||||||
|
_ => Ok(format!("gid-{call}")),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn remove_uri(&self, _gid: &str) -> Result<(), String> {
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn run_media(
|
||||||
|
&self,
|
||||||
|
_id: &str,
|
||||||
|
_payload: &SpawnPayload,
|
||||||
|
_generation: u64,
|
||||||
|
) -> Result<(), String> {
|
||||||
|
unreachable!("media is not used by blocking aria2 tests")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
#[async_trait::async_trait]
|
#[async_trait::async_trait]
|
||||||
impl SidecarSpawner for DelayedAria2Spawner {
|
impl SidecarSpawner for DelayedAria2Spawner {
|
||||||
async fn add_uri(&self, _id: &str, _payload: &SpawnPayload) -> Result<String, String> {
|
async fn add_uri(&self, _id: &str, _payload: &SpawnPayload) -> Result<String, String> {
|
||||||
@@ -820,6 +870,47 @@ async fn dispatcher_skips_a_full_front_queue_for_later_eligible_work() {
|
|||||||
dispatcher.abort();
|
dispatcher.abort();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn dispatcher_admits_next_task_while_first_add_uri_is_blocked() {
|
||||||
|
let app = mock_builder()
|
||||||
|
.build(mock_context(noop_assets()))
|
||||||
|
.expect("mock app");
|
||||||
|
let spawner = Arc::new(BlockingAria2Spawner::new());
|
||||||
|
let manager = Arc::new(QueueManager::test_new(
|
||||||
|
app.handle().clone(),
|
||||||
|
2,
|
||||||
|
spawner.clone(),
|
||||||
|
));
|
||||||
|
manager.push(aria2_task("blocked-first")).await.unwrap();
|
||||||
|
manager.push(aria2_task("admitted-second")).await.unwrap();
|
||||||
|
|
||||||
|
let first_started = spawner.first_started.notified();
|
||||||
|
let dispatcher = {
|
||||||
|
let manager = Arc::clone(&manager);
|
||||||
|
tokio::spawn(async move { manager.run_dispatcher().await })
|
||||||
|
};
|
||||||
|
timeout(Duration::from_secs(1), first_started)
|
||||||
|
.await
|
||||||
|
.expect("the first addUri should start");
|
||||||
|
|
||||||
|
timeout(Duration::from_secs(1), spawner.second_started.notified())
|
||||||
|
.await
|
||||||
|
.expect("a blocked addUri must not serialize admission of the next task");
|
||||||
|
|
||||||
|
spawner.release_first.notify_one();
|
||||||
|
timeout(Duration::from_secs(1), async {
|
||||||
|
while manager.aria2_gid_for_download("blocked-first").is_none()
|
||||||
|
|| manager.aria2_gid_for_download("admitted-second").is_none()
|
||||||
|
{
|
||||||
|
tokio::time::sleep(Duration::from_millis(10)).await;
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.await
|
||||||
|
.expect("both admitted tasks should finish dispatching");
|
||||||
|
|
||||||
|
dispatcher.abort();
|
||||||
|
}
|
||||||
|
|
||||||
#[tokio::test]
|
#[tokio::test]
|
||||||
async fn dispatcher_rotates_eligible_queues_without_starving_later_work() {
|
async fn dispatcher_rotates_eligible_queues_without_starving_later_work() {
|
||||||
let (manager, spawner) = make_manager(1);
|
let (manager, spawner) = make_manager(1);
|
||||||
|
|||||||
Reference in New Issue
Block a user