|
|
|
|
@@ -298,7 +298,12 @@ public sealed class DownloadManager : IDownloadManager
|
|
|
|
|
|
|
|
|
|
// 4. Lance les workers en parallèle.
|
|
|
|
|
// Compteur agrégé via Interlocked → pas de lock dans le hot path.
|
|
|
|
|
long aggregateBytes = state.DownloadedBytes;
|
|
|
|
|
// Initialisé sur la somme des seg.DownloadedBytes DURABLES plutôt que sur
|
|
|
|
|
// state.DownloadedBytes (live aggregate persisté, qui peut être en avance
|
|
|
|
|
// de la réalité disque si pause survenue entre checkpoints). Sans ça, le
|
|
|
|
|
// footer afficherait "paused at X" mais segments resumeraient à un point
|
|
|
|
|
// en arrière, et l'aggregate finirait avec un drift cosmétique.
|
|
|
|
|
long aggregateBytes = state.Segments.Sum(s => s.DownloadedBytes);
|
|
|
|
|
|
|
|
|
|
// Action passée aux segments : juste un Add atomique (zéro contention).
|
|
|
|
|
void OnSegmentBytes(int _, long deltaBytes)
|
|
|
|
|
@@ -474,15 +479,51 @@ public sealed class DownloadManager : IDownloadManager
|
|
|
|
|
await using var dst = new FileStream(partialPath, FileMode.Open, FileAccess.Write, FileShare.ReadWrite, BufferSize, useAsync: true);
|
|
|
|
|
dst.Seek(segStart, SeekOrigin.Begin);
|
|
|
|
|
|
|
|
|
|
// CHECKPOINT : durabilité ET anti-double-comptage.
|
|
|
|
|
//
|
|
|
|
|
// Durabilité : on n'incrémente seg.DownloadedBytes qu'APRÈS un FlushAsync
|
|
|
|
|
// réussi. Sans ça, sur pause/cancel, le buffer FileStream (4 MiB) ou le
|
|
|
|
|
// cache OS peuvent ne pas avoir atteint le disque, mais seg.DownloadedBytes
|
|
|
|
|
// les compte comme écrits → resume saute ces bytes → trou (zéros NTFS du
|
|
|
|
|
// pre-alloc sparse) → SHA-256 fail.
|
|
|
|
|
//
|
|
|
|
|
// Anti-double-comptage : on appelle ÉGALEMENT `onBytes` uniquement au
|
|
|
|
|
// checkpoint, avec le delta inFlight. Sans ça, un retry Polly mid-segment
|
|
|
|
|
// (très fréquent sur PHP-FPM OVH mutualisé qui coupe les requêtes longues)
|
|
|
|
|
// re-télécharge les bytes depuis le dernier checkpoint et les compte une
|
|
|
|
|
// 2e fois dans l'aggregate → footer affiche >100% en fin de DL.
|
|
|
|
|
// Avec onBytes au checkpoint : les bytes re-téléchargés n'ont jamais été
|
|
|
|
|
// reportés à la 1re tentative (l'erreur Polly est levée AVANT le checkpoint
|
|
|
|
|
// sur l'attempt failed), donc pas de double count.
|
|
|
|
|
//
|
|
|
|
|
// Trade-off : UI/footer update tous les 64 MiB par segment au lieu de
|
|
|
|
|
// chaque 4 MiB. Avec 16 segments en parallèle, ça reste ~10-20 updates/s
|
|
|
|
|
// au pic, le reporter task échantillonne à 4 Hz donc invisible côté UX.
|
|
|
|
|
const long FlushIntervalBytes = 64L * 1024 * 1024;
|
|
|
|
|
var buffer = new byte[BufferSize];
|
|
|
|
|
long inFlight = 0;
|
|
|
|
|
int n;
|
|
|
|
|
while ((n = await src.ReadAsync(buffer.AsMemory(0, BufferSize), ct).ConfigureAwait(false)) > 0)
|
|
|
|
|
{
|
|
|
|
|
await dst.WriteAsync(buffer.AsMemory(0, n), ct).ConfigureAwait(false);
|
|
|
|
|
seg.DownloadedBytes += n;
|
|
|
|
|
onBytes(seg.Index, n);
|
|
|
|
|
inFlight += n;
|
|
|
|
|
|
|
|
|
|
if (inFlight >= FlushIntervalBytes)
|
|
|
|
|
{
|
|
|
|
|
await dst.FlushAsync(CancellationToken.None).ConfigureAwait(false);
|
|
|
|
|
seg.DownloadedBytes += inFlight;
|
|
|
|
|
onBytes(seg.Index, inFlight);
|
|
|
|
|
inFlight = 0;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
// Checkpoint final : flush + ack du reste du buffer.
|
|
|
|
|
await dst.FlushAsync(CancellationToken.None).ConfigureAwait(false);
|
|
|
|
|
if (inFlight > 0)
|
|
|
|
|
{
|
|
|
|
|
seg.DownloadedBytes += inFlight;
|
|
|
|
|
onBytes(seg.Index, inFlight);
|
|
|
|
|
inFlight = 0;
|
|
|
|
|
}
|
|
|
|
|
await dst.FlushAsync(ct).ConfigureAwait(false);
|
|
|
|
|
seg.Completed = (seg.DownloadedBytes >= seg.Length);
|
|
|
|
|
|
|
|
|
|
// Si on est sorti de la boucle sans avoir atteint la fin du segment, le serveur a
|
|
|
|
|
|