Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
24 changes: 18 additions & 6 deletions nullseed.go
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import (
type nullChunkSeed struct {
id ChunkID
blockfile *os.File
blockLen uint64
canReflink bool
}

Expand All @@ -19,18 +20,25 @@ func newNullChunkSeed(dstFile string, blocksize uint64, max uint64) (*nullChunkS
if err != nil {
return nil, err
}
var canReflink bool
var (
canReflink bool
blockLen uint64
)
if CanClone(dstFile, blockfile.Name()) {
canReflink = true
b := make([]byte, blocksize)
if _, err := blockfile.Write(b); err != nil {
// Make the file of zeros as large as the largest chunk, rounded up to
// full blocks, so a run of zeros is cloned in few large pieces rather
// than one block at a time.
blockLen = (max + blocksize - 1) / blocksize * blocksize
if _, err := blockfile.Write(make([]byte, blockLen)); err != nil {
return nil, err
}
}
return &nullChunkSeed{
id: NewNullChunk(max).ID,
canReflink: canReflink,
blockfile: blockfile,
blockLen: blockLen,
}, nil
}

Expand Down Expand Up @@ -69,6 +77,7 @@ func (s *nullChunkSeed) LongestMatchWith(chunks []IndexChunk) (int, SeedSegment)
from: chunks[0].Start,
to: chunks[n-1].Start + chunks[n-1].Size,
blockfile: s.blockfile,
blockLen: s.blockLen,
canReflink: s.canReflink,
}
}
Expand All @@ -89,6 +98,7 @@ func (s *nullChunkSeed) IsInvalid() bool {
type nullChunkSection struct {
from, to uint64
blockfile *os.File
blockLen uint64 // Length of the blockfile, a multiple of the blocksize
canReflink bool
}

Expand Down Expand Up @@ -155,15 +165,17 @@ func (s *nullChunkSection) clone(dst *os.File, offset, length, blocksize uint64)
}
copied += c2

for blkOffset := dstAlignStart; blkOffset < dstAlignEnd; blkOffset += blocksize {
if err := cloneRange(dst, s.blockfile, 0, blocksize, blkOffset); err != nil {
for blkOffset := dstAlignStart; blkOffset < dstAlignEnd; {
n := min(s.blockLen, dstAlignEnd-blkOffset)
if err := cloneRange(dst, s.blockfile, 0, n, blkOffset); err != nil {
// Not every filesystem that passes the CanClone probe can clone
// every range. ZFS for example refuses to clone from the blockfile
// before it has been committed to disk. Fall back to writing zeros.
c3, _, err := s.copy(dst, blkOffset, dstAlignEnd-blkOffset)
return copied + c3, cloned, err
}
cloned += blocksize
cloned += n
blkOffset += n
}
return copied, cloned, nil
}
Expand Down
55 changes: 53 additions & 2 deletions nullseed_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@ func TestNullChunkSectionCloneFallback(t *testing.T) {
require.NoError(t, err)

newSection := func() *nullChunkSection {
return &nullChunkSection{from: 0, to: length, blockfile: blockfile, canReflink: true}
return &nullChunkSection{from: 0, to: length, blockfile: blockfile, blockLen: blocksize, canReflink: true}
}

t.Run("copies zeros when cloning fails", func(t *testing.T) {
Expand Down Expand Up @@ -92,7 +92,7 @@ func TestNullChunkSectionCloneFallback(t *testing.T) {
require.NoError(t, err)
defer dst.Close()

section := &nullChunkSection{from: from, to: from + sectionLen, blockfile: blockfile, canReflink: true}
section := &nullChunkSection{from: from, to: from + sectionLen, blockfile: blockfile, blockLen: blocksize, canReflink: true}
copied, cloned, err := section.WriteInto(dst, from, sectionLen, bigBlock, false)
require.NoError(t, err)
assert.Equal(t, uint64(0), cloned)
Expand Down Expand Up @@ -133,3 +133,54 @@ func TestNullChunkSectionCloneFallback(t *testing.T) {
assert.Equal(t, make([]byte, length), got)
})
}

// A run of zeros is cloned in pieces as large as the blockfile, with the
// last piece shorter, rather than one block at a time.
func TestNullChunkSectionClonesLargePieces(t *testing.T) {
defer func() { cloneRange = CloneRange }()

const (
blocksize = 4096
blockLen = 4 * blocksize
)
dir := t.TempDir()
blockfile, err := os.CreateTemp(dir, ".tmp-block")
require.NoError(t, err)
defer blockfile.Close()

type call struct{ srcOffset, length, dstOffset uint64 }
var calls []call
cloneRange = func(dst, src *os.File, srcOffset, srcLength, dstOffset uint64) error {
calls = append(calls, call{srcOffset, srcLength, dstOffset})
// Write the zeros a real clone would have produced
_, err := dst.WriteAt(make([]byte, srcLength), int64(dstOffset))
return err
}

// Starts 100 bytes into the first block and covers 9 full blocks after it
from := uint64(100)
length := uint64(blocksize - from + 9*blocksize + 50)
fileLen := from + length + 1000
dstName := filepath.Join(dir, "out")
require.NoError(t, os.WriteFile(dstName, bytes.Repeat([]byte{0xff}, int(fileLen)), 0644))
dst, err := os.OpenFile(dstName, os.O_RDWR, 0)
require.NoError(t, err)
defer dst.Close()

section := &nullChunkSection{from: from, to: from + length, blockfile: blockfile, blockLen: blockLen, canReflink: true}
copied, cloned, err := section.WriteInto(dst, from, length, blocksize, false)
require.NoError(t, err)
assert.Equal(t, uint64(9*blocksize), cloned)
assert.Equal(t, length-9*blocksize, copied)
assert.Equal(t, []call{
{0, blockLen, blocksize},
{0, blockLen, 5 * blocksize},
{0, blocksize, 9 * blocksize},
}, calls)

got, err := os.ReadFile(dstName)
require.NoError(t, err)
assert.Equal(t, bytes.Repeat([]byte{0xff}, int(from)), got[:from])
assert.Equal(t, make([]byte, length), got[from:from+length])
assert.Equal(t, bytes.Repeat([]byte{0xff}, int(fileLen-from-length)), got[from+length:])
}
Loading