v0.28.12 — Fix DL >100% : onBytes au checkpoint, pas par WriteAsync
Régression introduite par v0.28.11 (durability checkpoint) : le footer
affichait des pourcentages > 100% en fin de DL, même sans pause manuelle.
Cause : seg.DownloadedBytes était devenu durable (incrémenté au checkpoint
tous les 64 MiB), MAIS onBytes(seg.Index, n) continuait à fire par
WriteAsync. Quand Polly retry un segment mid-stream (fréquent sur OVH
mutualisé qui coupe les requêtes longues via PHP-FPM
request_terminate_timeout) :
Attempt 1 :
- onBytes fire pour bytes 0..30 MiB (live)
- HttpResumableException (PHP-FPM kill)
- Dispose flush, mais seg.DownloadedBytes encore à 0 (dernier checkpoint)
Polly retry attempt 2 :
- segStart = seg.Start + 0
- Re-download bytes 0..30 MiB (overwrite same content sur disque, OK)
- onBytes fire À NOUVEAU pour ces 30 MiB ← DOUBLE COMPTAGE
Multiplié par 16 segments × N retries → aggregate dépasse total.
Fix : onBytes fire UNIQUEMENT au checkpoint, avec la valeur inFlight
juste avant la reset. Comme ça les bytes d'une attempt qui a failé ne
sont jamais reportés (la failure se produit AVANT que le checkpoint
soit atteint), et la retry re-télécharge + reporte une seule fois.
Trade-off : UI updates tous les 64 MiB par segment au lieu de chaque
4 MiB. Avec 16 segments en parallèle, ça fait ~10-20 reports/s en pic,
le reporter task échantillonne à 4 Hz de toute façon, invisible côté UX.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -11,7 +11,7 @@
|
|||||||
|
|
||||||
#define MyAppName "PROSERVE Launcher"
|
#define MyAppName "PROSERVE Launcher"
|
||||||
#define MyAppShortName "PS_Launcher"
|
#define MyAppShortName "PS_Launcher"
|
||||||
#define MyAppVersion "0.28.11"
|
#define MyAppVersion "0.28.12"
|
||||||
#define MyAppPublisher "ASTERION VR"
|
#define MyAppPublisher "ASTERION VR"
|
||||||
#define MyAppURL "https://asterionvr.com"
|
#define MyAppURL "https://asterionvr.com"
|
||||||
#define MyAppExeName "PS_Launcher.exe"
|
#define MyAppExeName "PS_Launcher.exe"
|
||||||
|
|||||||
@@ -18,9 +18,9 @@
|
|||||||
<Product>PROSERVE Launcher</Product>
|
<Product>PROSERVE Launcher</Product>
|
||||||
<Copyright>© 2026 ASTERION VR — All rights reserved</Copyright>
|
<Copyright>© 2026 ASTERION VR — All rights reserved</Copyright>
|
||||||
<RootNamespace>PSLauncher.App</RootNamespace>
|
<RootNamespace>PSLauncher.App</RootNamespace>
|
||||||
<Version>0.28.11</Version>
|
<Version>0.28.12</Version>
|
||||||
<AssemblyVersion>0.28.11.0</AssemblyVersion>
|
<AssemblyVersion>0.28.12.0</AssemblyVersion>
|
||||||
<FileVersion>0.28.11.0</FileVersion>
|
<FileVersion>0.28.12.0</FileVersion>
|
||||||
|
|
||||||
<!-- Single-file self-contained publish profile (used by `dotnet publish`) -->
|
<!-- Single-file self-contained publish profile (used by `dotnet publish`) -->
|
||||||
<PublishSingleFile>true</PublishSingleFile>
|
<PublishSingleFile>true</PublishSingleFile>
|
||||||
|
|||||||
@@ -479,19 +479,26 @@ public sealed class DownloadManager : IDownloadManager
|
|||||||
await using var dst = new FileStream(partialPath, FileMode.Open, FileAccess.Write, FileShare.ReadWrite, BufferSize, useAsync: true);
|
await using var dst = new FileStream(partialPath, FileMode.Open, FileAccess.Write, FileShare.ReadWrite, BufferSize, useAsync: true);
|
||||||
dst.Seek(segStart, SeekOrigin.Begin);
|
dst.Seek(segStart, SeekOrigin.Begin);
|
||||||
|
|
||||||
// DURABILITÉ : on n'incrémente seg.DownloadedBytes qu'APRÈS un FlushAsync
|
// 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
|
// 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
|
// 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
|
// les compte comme écrits → resume saute ces bytes → trou (zéros NTFS du
|
||||||
// pre-alloc sparse) → SHA-256 fail. Cas observé chez l'utilisateur :
|
// pre-alloc sparse) → SHA-256 fail.
|
||||||
// ~80% des reprises post-pause failaient la vérif hash.
|
|
||||||
//
|
//
|
||||||
// Stratégie : tracker en local le delta non-flushé. Tous les ~64 MiB,
|
// Anti-double-comptage : on appelle ÉGALEMENT `onBytes` uniquement au
|
||||||
// FlushAsync (forcé avec CancellationToken.None pour qu'une pause ne le
|
// checkpoint, avec le delta inFlight. Sans ça, un retry Polly mid-segment
|
||||||
// coupe pas en plein milieu) puis update seg.DownloadedBytes. Le compteur
|
// (très fréquent sur PHP-FPM OVH mutualisé qui coupe les requêtes longues)
|
||||||
// est donc TOUJOURS en arrière (ou égal) de ce qui est sur disque, jamais
|
// re-télécharge les bytes depuis le dernier checkpoint et les compte une
|
||||||
// en avance. Resume re-télécharge au pire 64 MiB par segment — coût
|
// 2e fois dans l'aggregate → footer affiche >100% en fin de DL.
|
||||||
// borné, et c'est ce qu'on appelle un checkpoint.
|
// 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;
|
const long FlushIntervalBytes = 64L * 1024 * 1024;
|
||||||
var buffer = new byte[BufferSize];
|
var buffer = new byte[BufferSize];
|
||||||
long inFlight = 0;
|
long inFlight = 0;
|
||||||
@@ -500,15 +507,12 @@ public sealed class DownloadManager : IDownloadManager
|
|||||||
{
|
{
|
||||||
await dst.WriteAsync(buffer.AsMemory(0, n), ct).ConfigureAwait(false);
|
await dst.WriteAsync(buffer.AsMemory(0, n), ct).ConfigureAwait(false);
|
||||||
inFlight += n;
|
inFlight += n;
|
||||||
// Live counter pour UI (aggregate / footer) — peut overshoot par
|
|
||||||
// rapport au durable de quelques MiB ; le footer revient cohérent
|
|
||||||
// au prochain checkpoint.
|
|
||||||
onBytes(seg.Index, n);
|
|
||||||
|
|
||||||
if (inFlight >= FlushIntervalBytes)
|
if (inFlight >= FlushIntervalBytes)
|
||||||
{
|
{
|
||||||
await dst.FlushAsync(CancellationToken.None).ConfigureAwait(false);
|
await dst.FlushAsync(CancellationToken.None).ConfigureAwait(false);
|
||||||
seg.DownloadedBytes += inFlight;
|
seg.DownloadedBytes += inFlight;
|
||||||
|
onBytes(seg.Index, inFlight);
|
||||||
inFlight = 0;
|
inFlight = 0;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -517,6 +521,7 @@ public sealed class DownloadManager : IDownloadManager
|
|||||||
if (inFlight > 0)
|
if (inFlight > 0)
|
||||||
{
|
{
|
||||||
seg.DownloadedBytes += inFlight;
|
seg.DownloadedBytes += inFlight;
|
||||||
|
onBytes(seg.Index, inFlight);
|
||||||
inFlight = 0;
|
inFlight = 0;
|
||||||
}
|
}
|
||||||
seg.Completed = (seg.DownloadedBytes >= seg.Length);
|
seg.Completed = (seg.DownloadedBytes >= seg.Length);
|
||||||
|
|||||||
Reference in New Issue
Block a user