Skip to content

Commit a5ab4f5

Browse files
authored
feat: allow cancelation of download of a different URL (#71)
Respects the cancelation token provided to `StartDownloadAsync` even when there is another download already in progress to the same destination.
1 parent 272895b commit a5ab4f5

File tree

2 files changed

+43
-1
lines changed

2 files changed

+43
-1
lines changed

Tests.Vpn.Service/DownloaderTest.cs

+26
Original file line numberDiff line numberDiff line change
@@ -412,6 +412,32 @@ public async Task MismatchedETag(CancellationToken ct)
412412
Assert.That(ex.Message, Does.Contain("ETag does not match SHA1 hash of downloaded file").And.Contains("beef"));
413413
}
414414

415+
[Test(Description = "Timeout waiting for existing download")]
416+
[CancelAfter(30_000)]
417+
public async Task CancelledWaitingForOther(CancellationToken ct)
418+
{
419+
var testCts = CancellationTokenSource.CreateLinkedTokenSource(ct);
420+
using var httpServer = new TestHttpServer(async _ =>
421+
{
422+
await Task.Delay(TimeSpan.FromSeconds(5), testCts.Token);
423+
});
424+
var url0 = new Uri(httpServer.BaseUrl + "/test0");
425+
var url1 = new Uri(httpServer.BaseUrl + "/test1");
426+
var destPath = Path.Combine(_tempDir, "test");
427+
var manager = new Downloader(NullLogger<Downloader>.Instance);
428+
429+
// first outer task succeeds, getting download started
430+
var dlTask0 = await manager.StartDownloadAsync(new HttpRequestMessage(HttpMethod.Get, url0), destPath,
431+
NullDownloadValidator.Instance, testCts.Token);
432+
433+
// The second request fails if the timeout is short
434+
var smallerCt = new CancellationTokenSource(TimeSpan.FromSeconds(1)).Token;
435+
Assert.ThrowsAsync<TaskCanceledException>(async () => await manager.StartDownloadAsync(
436+
new HttpRequestMessage(HttpMethod.Get, url1), destPath,
437+
NullDownloadValidator.Instance, smallerCt));
438+
await testCts.CancelAsync();
439+
}
440+
415441
[Test(Description = "Timeout on response body")]
416442
[CancelAfter(30_000)]
417443
public async Task CancelledInner(CancellationToken ct)

Vpn.Service/Downloader.cs

+17-1
Original file line numberDiff line numberDiff line change
@@ -287,6 +287,7 @@ public async Task<DownloadTask> StartDownloadAsync(HttpRequestMessage req, strin
287287
{
288288
while (true)
289289
{
290+
ct.ThrowIfCancellationRequested();
290291
var task = _downloads.GetOrAdd(destinationPath,
291292
_ => new DownloadTask(_logger, req, destinationPath, validator));
292293
// EnsureStarted is a no-op if we didn't create a new DownloadTask.
@@ -322,7 +323,22 @@ public async Task<DownloadTask> StartDownloadAsync(HttpRequestMessage req, strin
322323
_logger.LogWarning(
323324
"Download for '{DestinationPath}' is already in progress, but is for a different Url - awaiting completion",
324325
destinationPath);
325-
await task.Task;
326+
await TaskOrCancellation(task.Task, ct);
327+
}
328+
}
329+
330+
/// <summary>
331+
/// TaskOrCancellation waits for either the task to complete, or the given token to be canceled.
332+
/// </summary>
333+
internal static async Task TaskOrCancellation(Task task, CancellationToken cancellationToken)
334+
{
335+
var cancellationTask = new TaskCompletionSource();
336+
await using (cancellationToken.Register(() => cancellationTask.TrySetCanceled()))
337+
{
338+
// Wait for either the task or the cancellation
339+
var completedTask = await Task.WhenAny(task, cancellationTask.Task);
340+
// Await to propagate exceptions, if any
341+
await completedTask;
326342
}
327343
}
328344
}

0 commit comments

Comments
 (0)