Skip to content
Draft
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
8 changes: 4 additions & 4 deletions assemble.go
Original file line number Diff line number Diff line change
Expand Up @@ -123,11 +123,11 @@ func AssembleFile(ctx context.Context, name string, idx Index, s Store, seeds []
isBlank = true
}

// Truncate the output file to the full expected size. Not only does this
// confirm there's enough disk space, but it allows for an optimization
// when dealing with the Null Chunk
// Pre-allocate and truncate the output file to the full expected size.
// On Darwin/APFS, this also physically allocates disk blocks to prevent
// sparse-hole issues with concurrent writes.
if !isBlkDevice {
if err := os.Truncate(name, idx.Length()); err != nil {
if err := preallocateFile(name, idx.Length()); err != nil {
return stats, err
}
}
Expand Down
15 changes: 5 additions & 10 deletions nullseed.go
Original file line number Diff line number Diff line change
Expand Up @@ -47,17 +47,12 @@ func (s *nullChunkSeed) LongestMatchWith(chunks []IndexChunk) (int, SeedSegment)
if len(chunks) == 0 {
return 0, nil
}
var (
n int
limit int
)
if !s.canReflink {
limit = 100
}
// No limit needed: when isBlank=true, WriteInto skips without copying.
// When isBlank=false, we must still write zeros to overwrite stale data.
// The previous limit of 100 caused chunks beyond the limit to fall
// through to other code paths, leading to incorrect assembly.
var n int
for _, c := range chunks {
if limit != 0 && limit == n {
break
}
if c.ID != s.id {
break
}
Expand Down
53 changes: 53 additions & 0 deletions preallocate_darwin.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
//go:build darwin

package desync

import (
"fmt"
"os"
"syscall"
"unsafe"
)

type fstore_t struct {
Flags uint32
Posmode int32
Offset int64
Length int64
Bytesalloc int64
}

const (
fAllocateAll = 0x00000004
fPeofPosmode = 3
fPreallocate = 42
)

// preallocateFile physically allocates disk blocks and sets the file size.
// On APFS, a plain Truncate creates sparse holes. When concurrent workers
// call WriteAt on adjacent regions, copy-on-write of sparse blocks can
// cause non-deterministic data corruption. Pre-allocating real blocks
// avoids this.
func preallocateFile(name string, size int64) error {
f, err := os.OpenFile(name, os.O_WRONLY|os.O_CREATE, 0666)
if err != nil {
return err
}
defer f.Close()

store := fstore_t{
Flags: fAllocateAll,
Posmode: fPeofPosmode,
Offset: 0,
Length: size,
}
_, _, errno := syscall.Syscall(syscall.SYS_FCNTL,
uintptr(f.Fd()),
uintptr(fPreallocate),
uintptr(unsafe.Pointer(&store)))
if errno != 0 {
return fmt.Errorf("F_PREALLOCATE: %w", errno)
}

return f.Truncate(size)
}
13 changes: 13 additions & 0 deletions preallocate_other.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
//go:build !darwin

package desync

import "os"

// preallocateFile truncates the file to the given size.
// On Linux (ext4) and other platforms, Truncate produces a file that
// reads back as zeros without sparse-hole issues, so no special
// preallocation is needed.
func preallocateFile(name string, size int64) error {
return os.Truncate(name, size)
}