Skip to content

Commit e9b0630

Browse files
authored
ScanAndRepair() now allow to set the number of concurrent downloads (#12)
* ScanAndRepair() now allow to set the number of concurrent downloads --------------------- GameScanner --------------------- Add concurrentDownload "setting": - if = 0 (default) then the amount of concurrentDownload woulbe ProcessorCount * 5. - if = 1 then then "SimpleFileDownloader" will be used not "ChunkFileDownload". - if > 1 then the amount of concurrentDownload woulbe the value passed. In all case the amount of concurrentDownload would be limited at max to 10 (that more than enough to get an great speed without abusing too much server bp). Usage: "int concurrentDownload = 0" has been add as last paramater in "ScanAndRepair()". --------------------- ChunckFileDownloader --------------------- - The amount of concurrent worker can now be configured (see upper -> GameScanner: concurrentDownload). - Enforce usage of the "cancellation token" on download operation so if throw they are canceled and we don't have to wait for them to complete. - Worker now start one by one with a delay of 1sec. - Now if an chunk fail to download instead of failling the "whole download" we retry to download it once in the same worker and if it fail again the worker stop and the item is re-enqueue so it can be processed by another worker, so in the end if an error append we will reduce the amount of concurent worker one by one until none left (their is an fallback in case an chunk is re-enqueue but all worker have completed already).
1 parent d88fff6 commit e9b0630

5 files changed

Lines changed: 148 additions & 121 deletions

File tree

src/FileDownloader/ChunkDownload.cs

Lines changed: 16 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,16 @@
11
using System;
22
using System.IO;
33
using System.Net;
4+
using System.Threading;
45
using System.Threading.Tasks;
56

67
namespace ProjectCeleste.GameFiles.GameScanner.FileDownloader
78
{
89
internal class ChunkDownload
910
{
10-
private const int ChunkBufferSize = 32 * 1024; // 32Kb
11+
internal const int ChunkBufferSize = 32 * 1024; // 32Kb
1112

12-
internal string DownloadTmpFileName { get; private set; }
13+
internal string DownloadTmpFileName { get; }
1314

1415
private readonly string _fileToDownload;
1516
private readonly FileRange _fileRange;
@@ -38,28 +39,26 @@ private HttpWebRequest CreateHttpWebRequest()
3839
return downloadRequest;
3940
}
4041

41-
internal async Task<bool> TryDownloadAsync(Action<int> progressCallback)
42+
internal async Task<bool> TryDownloadAsync(Action<int> progressCallback, CancellationToken ct = default)
4243
{
4344
var downloadRequest = CreateHttpWebRequest();
4445

4546
try
4647
{
47-
using (var downloadResponse = (HttpWebResponse) downloadRequest.GetResponse())
48-
using (var downloadSource = downloadResponse.GetResponseStream())
49-
using (var downloadTarget = new FileStream(DownloadTmpFileName, FileMode.Create, FileAccess.Write))
50-
{
51-
int bytesRead;
52-
var buffer = new byte[ChunkBufferSize];
48+
using var downloadResponse = (HttpWebResponse) downloadRequest.GetResponse();
49+
using var downloadSource = downloadResponse.GetResponseStream();
50+
using var downloadTarget = new FileStream(DownloadTmpFileName, FileMode.Create, FileAccess.Write);
51+
int bytesRead;
52+
var buffer = new byte[ChunkBufferSize];
5353

54-
do
55-
{
56-
bytesRead = await downloadSource.ReadAsync(buffer, 0, ChunkBufferSize);
57-
downloadTarget.Write(buffer, 0, bytesRead);
54+
do
55+
{
56+
bytesRead = await downloadSource.ReadAsync(buffer, 0, ChunkBufferSize, ct);
57+
await downloadTarget.WriteAsync(buffer, 0, bytesRead, ct);
5858

59-
progressCallback(bytesRead);
60-
_bytesDownloaded += bytesRead;
61-
} while (bytesRead > 0);
62-
}
59+
progressCallback(bytesRead);
60+
_bytesDownloaded += bytesRead;
61+
} while (bytesRead > 0);
6362
}
6463
catch
6564
{

src/FileDownloader/ChunkFileDownloader.cs

Lines changed: 90 additions & 63 deletions
Original file line numberDiff line numberDiff line change
@@ -13,22 +13,29 @@ namespace ProjectCeleste.GameFiles.GameScanner.FileDownloader
1313
public class ChunkFileDownloader : IFileDownloader
1414
{
1515
internal const int MaxChunkSize = 10 * 1024 * 1024; //10Mb
16-
private const int MaxConcurrentDownloads = 6;
16+
public const int MaxConcurrentDownloads = 10;
1717

18+
private readonly int _concurrentDownloads;
1819
private readonly Stopwatch _downloadSpeedStopwatch;
1920
private readonly string _downloadTempFolder;
20-
2121
private readonly ConcurrentDictionary<long, string> _completedChunks = new ConcurrentDictionary<long, string>();
22-
private ConcurrentQueue<FileRange> _chunkDownloadQueue;
2322

23+
private ConcurrentQueue<FileRange> _chunkDownloadQueue;
2424
private long _downloadSizeCompleted;
25-
private int _activeDownloads = 1;
26-
private bool _downloadFailed = false;
2725

28-
public ChunkFileDownloader(string httpLink, string outputFileName, string tmpFolder)
26+
public ChunkFileDownloader(string httpLink, string outputFileName, string tmpFolder, int concurrentDownload = 0)
2927
{
3028
DownloadUrl = httpLink;
3129
FilePath = outputFileName;
30+
31+
if (concurrentDownload <= 0)
32+
concurrentDownload = 5;
33+
34+
if (concurrentDownload > MaxConcurrentDownloads)
35+
concurrentDownload = MaxConcurrentDownloads;
36+
37+
_concurrentDownloads = concurrentDownload;
38+
3239
_downloadTempFolder = tmpFolder;
3340
_downloadSpeedStopwatch = new Stopwatch();
3441

@@ -37,7 +44,7 @@ public ChunkFileDownloader(string httpLink, string outputFileName, string tmpFol
3744
ServicePointManager.MaxServicePointIdleTime = 1000;
3845
}
3946

40-
public FileDownloaderState State { get; private set; } = FileDownloaderState.Invalid;
47+
public FileDownloaderState State { get; private set; }
4148

4249
public double DownloadProgress => DownloadSize > 0 ? (double) BytesDownloaded / DownloadSize * 100 : 0;
4350

@@ -80,31 +87,36 @@ public async Task DownloadAsync(CancellationToken ct = default)
8087
try
8188
{
8289
_downloadSpeedStopwatch.Start();
83-
84-
OnProgressChanged();
85-
8690
DownloadSize = await GetDownloadSizeAsync();
8791

88-
var readRanges = CalculateFileChunkRanges();
92+
ReportProgress(null); //Forced
8993

90-
//Parallel download
91-
using (new Timer(ReportProgress, null, 100, 100))
94+
using (var reportProgressTimer = new Timer(ReportProgress, null, 500, 500))
9295
{
93-
await OrchestrateDownloadWorkersAsync(readRanges, ct);
96+
await StartDownloadAsync(ct);
9497
_downloadSpeedStopwatch.Stop();
98+
reportProgressTimer.Change(Timeout.Infinite, Timeout.Infinite);
99+
}
95100

96-
if (BytesDownloaded != DownloadSize)
97-
throw new Exception(
98-
$"Download was completed ({BytesDownloaded} bytes), but did not receive expected size of {DownloadSize} bytes");
101+
ReportProgress(null); //Forced
99102

100-
State = FileDownloaderState.Finalize;
101-
ReportProgress(null); //Forced
103+
if (_chunkDownloadQueue.Count > 0)
104+
throw new Exception(
105+
$"Download was incomplete ({_chunkDownloadQueue.Count} missing chunks)");
102106

103-
WriteChunksToFile(_completedChunks);
107+
if (BytesDownloaded != DownloadSize)
108+
throw new Exception(
109+
$"Download was incomplete ({BytesDownloaded}/{DownloadSize} bytes)");
104110

105-
State = FileDownloaderState.Complete;
106-
ReportProgress(null); //Forced
107-
}
111+
State = FileDownloaderState.Finalize;
112+
113+
ReportProgress(null); //Forced
114+
115+
ct.ThrowIfCancellationRequested();
116+
117+
WriteChunksToFile(_completedChunks);
118+
119+
State = FileDownloaderState.Complete;
108120
}
109121
catch (Exception e)
110122
{
@@ -115,46 +127,61 @@ public async Task DownloadAsync(CancellationToken ct = default)
115127
}
116128
finally
117129
{
118-
OnProgressChanged();
130+
ReportProgress(null); //Forced
119131
}
120132
}
121133

122-
private async Task OrchestrateDownloadWorkersAsync(IEnumerable<FileRange> chunks, CancellationToken ct)
134+
private async Task StartDownloadAsync(CancellationToken ct = default)
123135
{
124-
var downloaderWorkers = new ConcurrentQueue<Task>();
125-
_chunkDownloadQueue = new ConcurrentQueue<FileRange>(chunks);
136+
var readRanges = CalculateFileChunkRanges();
137+
var fileRanges = readRanges as FileRange[] ?? readRanges.ToArray();
138+
_chunkDownloadQueue = new ConcurrentQueue<FileRange>(fileRanges);
126139

127-
while (_activeDownloads < MaxConcurrentDownloads && _chunkDownloadQueue.Count > 0 && !_downloadFailed)
128-
{
129-
_activeDownloads++;
140+
var tasks = Enumerable.Range(1, Math.Min(_concurrentDownloads, _chunkDownloadQueue.Count)).Select(
141+
async workerIndex =>
142+
{
143+
if (workerIndex > 1)
144+
await Task.Delay(1000 * (workerIndex - 1), ct);
130145

131-
downloaderWorkers.Enqueue(Task.Run(DequeueAndDownloadChunksAsync, ct));
132-
await Task.Delay(1000, ct);
133-
}
146+
await DequeueAndDownloadChunksAsync(ct);
147+
});
134148

135-
await Task.WhenAll(downloaderWorkers);
149+
await Task.WhenAll(tasks); //Start parallel download
150+
151+
if (_completedChunks.Count > 0 && _chunkDownloadQueue.Count > 0)
152+
{
153+
//Try to get missing chunk if any
154+
await DequeueAndDownloadChunksAsync(ct);
155+
}
136156
}
137157

138-
private async Task DequeueAndDownloadChunksAsync()
158+
private async Task DequeueAndDownloadChunksAsync(CancellationToken ct = default)
139159
{
140-
var workerFailedDownloading = false;
141-
var taskId = _activeDownloads;
142-
143-
while (_chunkDownloadQueue.TryDequeue(out var fileChunk) && !workerFailedDownloading)
160+
while (_chunkDownloadQueue.TryDequeue(out var fileChunk))
144161
{
162+
var workerFailedDownloadingOnce = false;
163+
164+
retry:
145165
var chunkDownload = new ChunkDownload(DownloadUrl, fileChunk, _downloadTempFolder);
146-
var downloadSuccesfullyCompleted = await chunkDownload.TryDownloadAsync(IncrementTotalDownloadProgress);
166+
var downloadSuccessfullyCompleted =
167+
await chunkDownload.TryDownloadAsync(IncrementTotalDownloadProgress, ct);
147168

148-
if (!downloadSuccesfullyCompleted)
149-
{
150-
_chunkDownloadQueue.Enqueue(fileChunk);
151-
workerFailedDownloading = true;
152-
_downloadFailed = true;
153-
}
154-
else
169+
if (!downloadSuccessfullyCompleted)
155170
{
156-
_completedChunks.TryAdd(fileChunk.Start, chunkDownload.DownloadTmpFileName);
171+
if (workerFailedDownloadingOnce)
172+
{
173+
_chunkDownloadQueue.Enqueue(fileChunk);
174+
break;
175+
}
176+
177+
workerFailedDownloadingOnce = true;
178+
await Task.Delay(1000, ct);
179+
goto retry;
157180
}
181+
182+
_completedChunks.TryAdd(fileChunk.Start, chunkDownload.DownloadTmpFileName);
183+
184+
await Task.Delay(1000, ct);
158185
}
159186
}
160187

@@ -176,23 +203,28 @@ private async Task<long> GetDownloadSizeAsync()
176203
var sizeDownloadRequest = WebRequest.Create(DownloadUrl);
177204
sizeDownloadRequest.Method = "HEAD";
178205

179-
using (var response = await sizeDownloadRequest.GetResponseAsync())
180-
{
181-
return long.Parse(response.Headers.Get("Content-Length"));
182-
}
206+
using var response = await sizeDownloadRequest.GetResponseAsync();
207+
return long.Parse(response.Headers.Get("Content-Length"));
183208
}
184209

185210
private void WriteChunksToFile(IDictionary<long, string> fileChunks)
186211
{
187-
using (var targetFile = new BufferedStream(new FileStream(FilePath, FileMode.Create, FileAccess.Write)))
212+
using var targetFile = new BufferedStream(new FileStream(FilePath, FileMode.Create, FileAccess.Write),
213+
ChunkDownload.ChunkBufferSize);
214+
foreach (var tempFile in fileChunks.ToArray().OrderBy(b => b.Key))
188215
{
189-
foreach (var tempFile in fileChunks.ToArray().OrderBy(b => b.Key))
190-
{
191-
using (var sourceChunks = new BufferedStream(File.OpenRead(tempFile.Value)))
192-
sourceChunks.CopyTo(targetFile);
216+
using (var sourceChunks =
217+
new BufferedStream(File.OpenRead(tempFile.Value), ChunkDownload.ChunkBufferSize))
218+
sourceChunks.CopyTo(targetFile);
193219

220+
try
221+
{
194222
File.Delete(tempFile.Value);
195223
}
224+
catch
225+
{
226+
//
227+
}
196228
}
197229
}
198230

@@ -203,14 +235,9 @@ private void IncrementTotalDownloadProgress(int bytesDownloaded)
203235

204236
public event EventHandler ProgressChanged;
205237

206-
protected virtual void OnProgressChanged()
207-
{
208-
ProgressChanged?.Invoke(this, null);
209-
}
210-
211238
private void ReportProgress(object state)
212239
{
213-
OnProgressChanged();
240+
ProgressChanged?.Invoke(this, null);
214241
}
215242
}
216243
}

src/FileDownloader/IFileDownloader.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,6 @@ public interface IFileDownloader
1717

1818
event EventHandler ProgressChanged;
1919

20-
Task DownloadAsync(CancellationToken ct = default(CancellationToken));
20+
Task DownloadAsync(CancellationToken ct = default);
2121
}
2222
}

0 commit comments

Comments
 (0)