Skip to content

Commit 7683ffb

Browse files
HofmeisterAnclaude
andcommitted
fix: Restore Dockerfile build performance regressed by tar block factor
PR #1684 lowered the SharpZipLib block factor to 1 to drop the trailing record padding that races Podman's archive reader, causing a broken pipe (HTTP 500) on resource mappings (#1683). It applied this to both archive paths, but the on-disk Dockerfile build context is written to a FileStream, where a 512 B record forces a write and flush per block and regresses image builds by up to 10x (worst on .NET Framework). Only the in-memory archive streamed to the daemon (resource mappings) needs the padding gone, and a MemoryStream pays almost nothing for the small record size (~1.3x, benchmarked). So keep block factor 1 there and revert the Dockerfile build context to SharpZipLib's default block factor. Document the asymmetry so it is not "consistency-fixed" back into a regression, and add a test asserting the streamed archive carries no padding beyond the two end-of-archive blocks. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
1 parent d6cf08d commit 7683ffb

3 files changed

Lines changed: 33 additions & 1 deletion

File tree

src/Testcontainers/Images/DockerfileArchive.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -192,7 +192,7 @@ public async Task<string> Tar(CancellationToken ct = default)
192192

193193
using (var tarOutputFileStream = new FileStream(dockerfileArchiveFilePath, FileMode.Create, FileAccess.Write))
194194
{
195-
using (var tarOutputStream = new TarOutputStream(tarOutputFileStream, TarArchiveDefaults.TarBlockFactor, Encoding.Default))
195+
using (var tarOutputStream = new TarOutputStream(tarOutputFileStream, Encoding.Default))
196196
{
197197
tarOutputStream.IsStreamOwner = false;
198198

src/Testcontainers/TarArchiveDefaults.cs

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,13 @@ internal static class TarArchiveDefaults
99
// blocks while the HTTP sender is still flushing the padding, causing EPIPE
1010
// (HTTP 500 "broken pipe"). See:
1111
// https://github.com/testcontainers/testcontainers-dotnet/issues/1683.
12+
//
13+
// This only applies to archives we stream to the daemon over a pipe (resource
14+
// mappings via a MemoryStream), where the surplus padding is both unnecessary
15+
// and harmful. Do NOT reuse it for the Dockerfile build context, which is
16+
// written to a FileStream on disk: a 512 B record forces a write and flush per
17+
// block and regresses image builds by up to 10x. That path keeps SharpZipLib's
18+
// default block factor on purpose.
1219
internal const int TarBlockFactor = 1;
1320
}
1421
}

tests/Testcontainers.Platform.Linux.Tests/TarOutputMemoryStreamTest.cs

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -59,6 +59,31 @@ public void TestFileExistsInTarFile()
5959
Assert.Contains(actual, file => file.EndsWith(_testFile.Name));
6060
}
6161

62+
[Fact]
63+
[Trait(nameof(DockerCli.DockerPlatform), nameof(DockerCli.DockerPlatform.Linux))]
64+
public void TarFileDoesNotContainPaddingBeyondEofBlocks()
65+
{
66+
// Given
67+
_tarOutputMemoryStream.Close();
68+
_tarOutputMemoryStream.Seek(0, SeekOrigin.Begin);
69+
70+
using var buffer = new MemoryStream();
71+
_tarOutputMemoryStream.CopyTo(buffer);
72+
73+
var archive = buffer.ToArray();
74+
75+
// When
76+
var length = archive.Length;
77+
78+
// Then
79+
// The archive holds a single 1-byte file: one header block, one data block and
80+
// the two end-of-archive blocks. SharpZipLib's record padding (block factor 20)
81+
// would otherwise inflate it to a 10240-byte record boundary.
82+
Assert.Equal(4 * TarBuffer.BlockSize, length);
83+
Assert.Equal(0, length % TarBuffer.BlockSize);
84+
Assert.All(archive.Skip(length - (2 * TarBuffer.BlockSize)), b => Assert.Equal(0, b));
85+
}
86+
6287
[UsedImplicitly]
6388
public sealed class FromResourceMapping : TarOutputMemoryStreamTest, IResourceMapping, IClassFixture<FromResourceMapping.HttpFixture>, IAsyncLifetime
6489
{

0 commit comments

Comments
 (0)