chore(model): slightly improve handling of pulling empty blocks (#10679)

In the common case (sparse files enabled, not reusing old data) we'd
optimise away pulling & writing zero blocks. However in the corner cases
we'd go through the whole processing of pulling the block over the
network which is of course entirely unnecessary.

Now, instead, always take an optimised path for all-zeroes blocks. In
the clean case we do nothing, otherwise we materialise a block of zeroes
and write it directly.

---------

Signed-off-by: Jakob Borg <jakob@kastelo.net>
This commit is contained in:
Jakob Borg
2026-05-09 12:15:18 +02:00
committed by GitHub
parent 3709174a86
commit f538b47070
3 changed files with 102 additions and 7 deletions
+82
View File
@@ -917,6 +917,88 @@ func TestPullCtxCancel(t *testing.T) {
}
}
// TestPullEmptyBlock exercises pullBlock for blocks of all zeroes.
// pullBlock distinguishes three cases:
//
// - Sparse files enabled, reused == 0: nothing to write, the offset
// reads as zero anyway because the temp file is sparse-allocated.
// - reused != 0: the temp file is being reused and may already hold
// non-zero data at this offset, so zeroes must be written explicitly.
// - DisableSparseFiles: cannot rely on a sparse hole reading as zero,
// so zeroes must be written explicitly.
func TestPullEmptyBlock(t *testing.T) {
const blockSize = protocol.MinBlockSize
emptyBlock := protocol.BlockInfo{
Offset: 0,
Size: blockSize,
Hash: blocks[0].Hash, // sha256 of a 128 KiB all-zeroes block
}
if !emptyBlock.IsEmpty() {
t.Fatal("test setup: block not recognised as empty")
}
cases := []struct {
name string
reused []int
disableSparse bool
preFillTempFile bool
}{
{name: "sparse-clean"},
{name: "reused-temp-file", reused: []int{1}, preFillTempFile: true},
{name: "disable-sparse-files", disableSparse: true},
}
for _, tc := range cases {
t.Run(tc.name, func(t *testing.T) {
_, f := setupSendReceiveFolder(t)
f.folder.FolderConfiguration.DisableSparseFiles = tc.disableSparse
file := protocol.FileInfo{
Name: "file",
Size: int64(blockSize),
Permissions: 0o644,
Blocks: []protocol.BlockInfo{emptyBlock},
}
tempName := fs.TempName(file.Name)
// Pre-fill the temp file with non-zero data so that a missing
// overwrite would be detectable.
if tc.preFillTempFile {
writeFile(t, f.Filesystem(), tempName, bytes.Repeat([]byte{0xff}, blockSize))
}
state := pullBlockState{
sharedPullerState: newSharedPullerState(file, f.Filesystem(), f.folderID, tempName, file.Blocks, tc.reused, false, false, protocol.FileInfo{}, !tc.disableSparse, false),
block: emptyBlock,
}
out := make(chan *sharedPullerState, 1)
f.pullBlock(t.Context(), state, out)
s := <-out
if err := s.failed(); err != nil {
t.Fatalf("pullBlock failed: %v", err)
}
cleanupSharedPullerState(s)
fd, err := f.Filesystem().Open(tempName)
must(t, err)
defer fd.Close()
data, err := io.ReadAll(fd)
must(t, err)
if len(data) != blockSize {
t.Fatalf("temp file too short: got %d bytes, want at least %d", len(data), emptyBlock.Offset+int64(blockSize))
}
for i, b := range data {
if b != 0 {
t.Fatalf("byte %d: got %#x, want 0", i, b)
}
}
})
}
}
func TestPullDeleteUnscannedDir(t *testing.T) {
_, f := setupSendReceiveFolder(t)
ffs := f.Filesystem()