Skip to content

Commit a18123b

Browse files
JusterZhuclaude
andcommitted
feat(core): replace 80% size threshold with count-first chain-to-full fallback
Swap the single 80%-of-full-size heuristic for a two-condition strategy: 1. Chain count exceeds MaxChainBeforeFallback (default 8) → full package 2. Combined chain size >= full size → full package Add MaxChainBeforeFallback to UpdateConfiguration (default 8), plumb it through DownloadPlanBuilder.Build() and ClientStrategy. The new logic stops switching to full when the chain total is only marginally below the full size — the runtime fallback (FallbackFull) already handles chain application failures, so an overly eager size threshold is unnecessary. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
1 parent feea1a2 commit a18123b

4 files changed

Lines changed: 116 additions & 11 deletions

File tree

src/c#/GeneralUpdate.Core/Configuration/UpdateConfiguration.cs

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -245,5 +245,23 @@ public abstract class UpdateConfiguration
245245
/// </remarks>
246246
/// </remarks>
247247
public Dictionary<string, string> CustomHeaders { get; set; }
248+
249+
/// <summary>
250+
/// Maximum number of chain (delta) packages allowed before falling back to a single
251+
/// full replacement package. When the number of chain packages for an AppType exceeds
252+
/// this limit, the download plan switches to the full package for that AppType —
253+
/// even if the combined chain size is smaller than the full package.
254+
/// </summary>
255+
/// <remarks>
256+
/// <para>
257+
/// A full package is also selected when the combined download size of all chain
258+
/// packages equals or exceeds the full package size, regardless of the count.
259+
/// </para>
260+
/// <para>
261+
/// Set to <c>0</c> or a negative value to disable the count-based fallback
262+
/// (only the size comparison applies).
263+
/// </para>
264+
/// </remarks>
265+
public int MaxChainBeforeFallback { get; set; } = 8;
248266
}
249267
}

src/c#/GeneralUpdate.Core/Download/DownloadPlanBuilder.cs

Lines changed: 28 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -28,8 +28,10 @@ namespace GeneralUpdate.Core.Download;
2828
/// if the current client version is below the minimum, the package is skipped.</description></item>
2929
/// </list>
3030
/// <para>
31-
/// All packages are treated uniformly; the builder evaluates chain vs full packages
32-
/// based on total download size.
31+
/// Chain vs full evaluation uses a count-first heuristic:
32+
/// a full package is selected when the number of chain packages exceeds
33+
/// <c>MaxChainBeforeFallback</c> (default 8), or when the combined download size
34+
/// of all chain packages equals or exceeds the full package size.
3335
/// </para>
3436
/// </remarks>
3537
public static class DownloadPlanBuilder
@@ -108,10 +110,17 @@ public static bool HasUpdate(
108110
/// <param name="upgradeClientVersion">
109111
/// The current upgrade (updater) version, or null to fall back to <paramref name="clientVersion"/>.
110112
/// </param>
113+
/// <param name="maxChainBeforeFallback">
114+
/// Maximum number of chain packages allowed before falling back to a single full package.
115+
/// Default is <c>8</c>. Set to <c>0</c> to disable the count-based fallback; set to
116+
/// a negative value to always prefer chain packages when their combined size is smaller
117+
/// than the full package.
118+
/// </param>
111119
public static DownloadPlan Build(
112120
IEnumerable<DownloadAsset> assets,
113121
string clientVersion,
114-
string? upgradeClientVersion)
122+
string? upgradeClientVersion,
123+
int maxChainBeforeFallback = 8)
115124
{
116125
if (assets == null) return DownloadPlan.Empty;
117126
var parsedClient = ParseVersion(clientVersion);
@@ -171,7 +180,7 @@ public static DownloadPlan Build(
171180
.Where(a => a.PackageType == (int)Configuration.PackageType.Full)
172181
.ToList();
173182

174-
// ── Chain vs Full size-based decision ──
183+
// ── Chain vs Full: count-first heuristic ──
175184
if (chainCandidates.Count > 0 && fullCandidates.Count > 0)
176185
{
177186
var bestFull = fullCandidates
@@ -181,11 +190,23 @@ public static DownloadPlan Build(
181190
long chainTotal = chainCandidates
182191
.Where(a => a.AppType == bestFull.AppType)
183192
.Sum(a => a.Size);
184-
var threshold = (long)(bestFull.Size * 0.8);
193+
int chainCount = chainCandidates.Count(a => a.AppType == bestFull.AppType);
194+
195+
if (maxChainBeforeFallback > 0 && chainCount > maxChainBeforeFallback)
196+
{
197+
GeneralTracer.Info($"DownloadPlanBuilder: chain count {chainCount} exceeds MaxChainBeforeFallback {maxChainBeforeFallback}, switching to full package {bestFull.Name} (chain total {chainTotal}, full size {bestFull.Size})");
198+
var bestFullSv = Lookup(bestFull);
199+
var planAssets = new List<DownloadAsset> { bestFull };
200+
planAssets.AddRange(chainCandidates
201+
.Where(a => a.AppType != bestFull.AppType
202+
|| (Lookup(a) is { } av && bestFullSv != null && av > bestFullSv))
203+
.OrderBy(a => Lookup(a)));
204+
return new DownloadPlan(planAssets, isForcibly);
205+
}
185206

186-
if (chainTotal >= threshold)
207+
if (chainTotal >= bestFull.Size)
187208
{
188-
GeneralTracer.Info($"DownloadPlanBuilder: chain total {chainTotal} >= 80% of full size {bestFull.Size}, switching to full package {bestFull.Name}");
209+
GeneralTracer.Info($"DownloadPlanBuilder: chain total {chainTotal} >= full size {bestFull.Size}, switching to full package {bestFull.Name}");
189210
var bestFullSv = Lookup(bestFull);
190211
var planAssets = new List<DownloadAsset> { bestFull };
191212
planAssets.AddRange(chainCandidates

src/c#/GeneralUpdate.Core/Strategy/ClientStrategy.cs

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -475,7 +475,8 @@ private async Task ExecuteStandardWorkflowAsync()
475475
var downloadPlan = Download.DownloadPlanBuilder.Build(
476476
sourceResult.Assets,
477477
localClientVersion,
478-
resolvedUpgradeVersion);
478+
resolvedUpgradeVersion,
479+
_configInfo.MaxChainBeforeFallback);
479480
_configInfo.LastVersion = downloadPlan.Assets.LastOrDefault()?.Version;
480481
GeneralTracer.Info($"ClientStrategy: Scenario={scenario}, AssetCount={downloadPlan.Assets.Count}");
481482

tests/CoreTest/Download/DownloadPlanBuilderTests.cs

Lines changed: 68 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -81,23 +81,88 @@ public void Build_NoAssetIsForcibly_IsForciblyFalse()
8181
}
8282

8383
[Fact]
84-
public void Build_FullPackageSelected_WhenChainExceedsThreshold()
84+
public void Build_FullPackageSelected_WhenChainTotalExceedsFullSize()
8585
{
86-
// Small chain + small full → chain_total >= 80% full → Full selected
86+
// 2 chain packages (total 200), full=100200 >= 100 → full selected
8787
var assets = new[]
8888
{
8989
Asset("chain1", "1.1.0", size: 100),
9090
Asset("chain2", "2.0.0", size: 100),
9191
Asset("full", "2.0.0", packageType: (int)PackageType.Full),
9292
};
93-
// chain_total=200, 80% of full(100) = 80 → 200 >= 80 → full selected
9493
var result = DownloadPlanBuilder.Build(assets, "1.0.0");
9594
Assert.True(result.HasAssets);
9695
Assert.Single(result.Assets);
9796
Assert.Equal((int)PackageType.Full, result.Assets[0].PackageType);
9897
Assert.Equal("2.0.0", result.Assets[0].Version);
9998
}
10099

100+
[Fact]
101+
public void Build_FullPackageSelected_WhenChainCountExceedsMaxChainBeforeFallback()
102+
{
103+
// 12 small chain packages (total 120), full=500, maxChainBeforeFallback=10
104+
// → chain count 12 > 10 → full selected despite chain being much smaller
105+
var assets = new List<DownloadAsset>();
106+
for (int i = 1; i <= 12; i++)
107+
assets.Add(Asset($"chain{i}", $"1.{i}.0", size: 10));
108+
assets.Add(Asset("full", "2.0.0", size: 500, packageType: (int)PackageType.Full));
109+
110+
var result = DownloadPlanBuilder.Build(assets, "1.0.0", null, maxChainBeforeFallback: 10);
111+
Assert.True(result.HasAssets);
112+
Assert.Single(result.Assets);
113+
Assert.Equal((int)PackageType.Full, result.Assets[0].PackageType);
114+
}
115+
116+
[Fact]
117+
public void Build_ChainSelected_WhenCountBelowThresholdAndTotalBelowFullSize()
118+
{
119+
// 3 chain packages (total 150), full=500, maxChainBeforeFallback=default(8)
120+
// → chain count 3 <= 8 AND 150 < 500 → chain selected
121+
var assets = new[]
122+
{
123+
Asset("chain1", "1.1.0", size: 50),
124+
Asset("chain2", "1.2.0", size: 50),
125+
Asset("chain3", "2.0.0", size: 50),
126+
Asset("full", "2.0.0", size: 500, packageType: (int)PackageType.Full),
127+
};
128+
var result = DownloadPlanBuilder.Build(assets, "1.0.0");
129+
Assert.True(result.HasAssets);
130+
Assert.Equal(3, result.Assets.Count);
131+
Assert.All(result.Assets, a => Assert.Equal((int)PackageType.Chain, a.PackageType));
132+
}
133+
134+
[Fact]
135+
public void Build_FullPackageSelected_WhenChainTotalEqualsFullSize()
136+
{
137+
// chain total == full size → full selected (boundary: >= not >)
138+
var assets = new[]
139+
{
140+
Asset("chain1", "1.1.0", size: 200),
141+
Asset("chain2", "2.0.0", size: 300),
142+
Asset("full", "2.0.0", size: 500, packageType: (int)PackageType.Full),
143+
};
144+
var result = DownloadPlanBuilder.Build(assets, "1.0.0", null, maxChainBeforeFallback: 10);
145+
Assert.True(result.HasAssets);
146+
Assert.Single(result.Assets);
147+
Assert.Equal((int)PackageType.Full, result.Assets[0].PackageType);
148+
}
149+
150+
[Fact]
151+
public void Build_ChainSelected_WhenMaxChainBeforeFallbackIsZero()
152+
{
153+
// maxChainBeforeFallback=0 disables count-based fallback
154+
// 25 chain packages (total 400), full=500 → 400 < 500 → chain selected
155+
var assets = new List<DownloadAsset>();
156+
for (int i = 1; i <= 25; i++)
157+
assets.Add(Asset($"chain{i}", $"1.{i}.0", size: 16));
158+
assets.Add(Asset("full", "2.0.0", size: 500, packageType: (int)PackageType.Full));
159+
160+
var result = DownloadPlanBuilder.Build(assets, "1.0.0", null, maxChainBeforeFallback: 0);
161+
Assert.True(result.HasAssets);
162+
Assert.Equal(25, result.Assets.Count);
163+
Assert.All(result.Assets, a => Assert.Equal((int)PackageType.Chain, a.PackageType));
164+
}
165+
101166
[Fact]
102167
public void Build_MinClientVersionTooHigh_FilteredOut()
103168
{

0 commit comments

Comments
 (0)