diff --git a/src/GameCult.Mesh/CultMeshCdn.cs b/src/GameCult.Mesh/CultMeshCdn.cs
index 7aaf7eba..6ea4696f 100644
--- a/src/GameCult.Mesh/CultMeshCdn.cs
+++ b/src/GameCult.Mesh/CultMeshCdn.cs
@@ -42,7 +42,7 @@ namespace GameCult.Mesh
public sealed class CultMeshCdnPackOptions
{
/// Default CDN chunk size.
- public const int DefaultChunkSizeBytes = 256 * 1024;
+ public const int DefaultChunkSizeBytes = 4 * 1024 * 1024;
/// Maximum bytes per content chunk.
public int ChunkSizeBytes { get; set; } = DefaultChunkSizeBytes;
diff --git a/src/GameCult.Mesh/CultMeshContentTransfer.cs b/src/GameCult.Mesh/CultMeshContentTransfer.cs
index f6a426b8..b7f8c505 100644
--- a/src/GameCult.Mesh/CultMeshContentTransfer.cs
+++ b/src/GameCult.Mesh/CultMeshContentTransfer.cs
@@ -263,16 +263,37 @@ namespace GameCult.Mesh
{
await pending.Copy.ConfigureAwait(false);
verified.Add(pending.Item.Index);
- await SaveStateAsync(contentHash, fingerprint, manifest.SizeBytes, verified, stateKey).ConfigureAwait(false);
+ await SaveStateAsync(
+ contentHash,
+ fingerprint,
+ manifest.SizeBytes,
+ verified,
+ stateKey,
+ flush: false).ConfigureAwait(false);
}
+ await _stateCache.FlushAsync().ConfigureAwait(false);
}
catch
{
foreach (var pending in window)
{
- try { await pending.Copy.ConfigureAwait(false); }
+ try
+ {
+ await pending.Copy.ConfigureAwait(false);
+ if (verified.Add(pending.Item.Index))
+ {
+ await SaveStateAsync(
+ contentHash,
+ fingerprint,
+ manifest.SizeBytes,
+ verified,
+ stateKey,
+ flush: false).ConfigureAwait(false);
+ }
+ }
catch { }
}
+ await _stateCache.FlushAsync().ConfigureAwait(false);
throw;
}
}
@@ -450,7 +471,8 @@ namespace GameCult.Mesh
string fingerprint,
long sizeBytes,
HashSet verified,
- CultRecordKey stateKey)
+ CultRecordKey stateKey,
+ bool flush)
{
var state = new CultMeshContentTransferStateDocument
{
@@ -461,7 +483,7 @@ namespace GameCult.Mesh
UpdatedAtUtc = DateTimeOffset.UtcNow.ToString("O", CultureInfo.InvariantCulture)
};
await _stateCache.UpsertAsync(typeof(CultMeshContentTransferStateDocument), state, stateKey).ConfigureAwait(false);
- await _stateCache.FlushAsync().ConfigureAwait(false);
+ if (flush) await _stateCache.FlushAsync().ConfigureAwait(false);
}
private static CultRecordKey StateKey(string contentHash) => new CultRecordKey("mesh:content-transfer:" + contentHash);
diff --git a/tests/GameCult.Mesh.Tests/CultMeshContentTransferTests.cs b/tests/GameCult.Mesh.Tests/CultMeshContentTransferTests.cs
index 04c6e283..6b5212c3 100644
--- a/tests/GameCult.Mesh.Tests/CultMeshContentTransferTests.cs
+++ b/tests/GameCult.Mesh.Tests/CultMeshContentTransferTests.cs
@@ -192,7 +192,7 @@ public sealed class CultMeshContentTransferTests
}
[Test]
- public async Task FetchAsync_ObservesRemainingWindowTasksWhenEarlierFetchFails()
+ public async Task FetchAsync_CheckpointsVerifiedWindowPeersWhenEarlierFetchFails()
{
var artifact = Artifact("failed-window", 2);
using var cache = Cache();
@@ -210,7 +210,8 @@ public sealed class CultMeshContentTransferTests
.Should().ThrowAsync();
provider.ActiveRequests.Should().Be(0, "the failed window must observe every task before returning");
- cache.Get(StateKey(artifact.Manifest)).Should().BeNull();
+ cache.Get(StateKey(artifact.Manifest))!
+ .VerifiedChunkIndexes.Should().Equal(1, 2);
File.Exists(Path.Combine(_directory, artifact.Manifest.ContentHash + ".body")).Should().BeFalse();
}