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(); }