lib/fs: Unwrap mtimeFile, get fd the "correct" way (ref #6875) (#6877)

This commit is contained in:
Audrius Butkevicius 2020-08-07 06:47:48 +01:00 committed by GitHub
parent ff84f075d5
commit e9bb17307d
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
8 changed files with 337 additions and 157 deletions

View File

@ -14,6 +14,9 @@ type copyRangeImplementationBasicFile func(src, dst basicFile, srcOffset, dstOff
func copyRangeImplementationForBasicFile(impl copyRangeImplementationBasicFile) copyRangeImplementation { func copyRangeImplementationForBasicFile(impl copyRangeImplementationBasicFile) copyRangeImplementation {
return func(src, dst File, srcOffset, dstOffset, size int64) error { return func(src, dst File, srcOffset, dstOffset, size int64) error {
src = unwrap(src)
dst = unwrap(dst)
// Then see if it's basic files
srcFile, srcOk := src.(basicFile) srcFile, srcOk := src.(basicFile)
dstFile, dstOk := dst.(basicFile) dstFile, dstOk := dst.(basicFile)
if !srcOk || !dstOk { if !srcOk || !dstOk {
@ -22,3 +25,38 @@ func copyRangeImplementationForBasicFile(impl copyRangeImplementationBasicFile)
return impl(srcFile, dstFile, srcOffset, dstOffset, size) return impl(srcFile, dstFile, srcOffset, dstOffset, size)
} }
} }
func withFileDescriptors(first, second basicFile, fn func(first, second uintptr) (int, error)) (int, error) {
fc, err := first.SyscallConn()
if err != nil {
return 0, err
}
sc, err := second.SyscallConn()
if err != nil {
return 0, err
}
var n int
var ferr, serr, fnerr error
ferr = fc.Control(func(first uintptr) {
serr = sc.Control(func(second uintptr) {
n, fnerr = fn(first, second)
})
})
if ferr != nil {
return n, ferr
}
if serr != nil {
return n, serr
}
return n, fnerr
}
func unwrap(f File) File {
for {
if wrapped, ok := f.(interface{ unwrap() File }); ok {
f = wrapped.unwrap()
} else {
return f
}
}
}

View File

@ -29,7 +29,9 @@ func copyRangeCopyFileRange(src, dst basicFile, srcOffset, dstOffset, size int64
// appropriately. // appropriately.
// //
// Also, even if explicitly not stated, the same is true for dstOffset // Also, even if explicitly not stated, the same is true for dstOffset
n, err := unix.CopyFileRange(int(src.Fd()), &srcOffset, int(dst.Fd()), &dstOffset, int(size), 0) n, err := withFileDescriptors(src, dst, func(srcFd, dstFd uintptr) (int, error) {
return unix.CopyFileRange(int(srcFd), &srcOffset, int(dstFd), &dstOffset, int(size), 0)
})
if n == 0 && err == nil { if n == 0 && err == nil {
return io.ErrUnexpectedEOF return io.ErrUnexpectedEOF
} }

View File

@ -9,6 +9,7 @@
package fs package fs
import ( import (
"io"
"syscall" "syscall"
"unsafe" "unsafe"
@ -46,13 +47,24 @@ type duplicateExtentsData struct {
func copyRangeDuplicateExtents(src, dst basicFile, srcOffset, dstOffset, size int64) error { func copyRangeDuplicateExtents(src, dst basicFile, srcOffset, dstOffset, size int64) error {
var err error var err error
// Check that the destination file has sufficient space // Check that the destination file has sufficient space
if fi, err := dst.Stat(); err != nil { dstFi, err := dst.Stat()
if err != nil {
return err return err
} else if fi.Size() < dstOffset+size { }
dstSize := dstFi.Size()
if dstSize < dstOffset+size {
// set file size. There is a requirements "The destination region must not extend past the end of file." // set file size. There is a requirements "The destination region must not extend past the end of file."
if err = dst.Truncate(dstOffset + size); err != nil { if err = dst.Truncate(dstOffset + size); err != nil {
return err return err
} }
dstSize = dstOffset + size
}
// The source file has to be big enough
if fi, err := src.Stat(); err != nil {
return err
} else if fi.Size() < srcOffset+size {
return io.ErrUnexpectedEOF
} }
// Requirement // Requirement
@ -60,9 +72,25 @@ func copyRangeDuplicateExtents(src, dst basicFile, srcOffset, dstOffset, size in
// * cloneRegionSize less than 4GiB. // * cloneRegionSize less than 4GiB.
// see https://docs.microsoft.com/windows/win32/fileio/block-cloning // see https://docs.microsoft.com/windows/win32/fileio/block-cloning
smallestClusterSize := availableClusterSize[len(availableClusterSize)-1]
if srcOffset%smallestClusterSize != 0 || dstOffset%smallestClusterSize != 0 {
return syscall.EINVAL
}
// Each file gets allocated multiple of "clusterSize" blocks, yet file size determines how much of the last block
// is readable/visible.
// Copies only happen in block sized chunks, hence you can copy non block sized regions of data to a file, as long
// as the regions are copied at the end of the file where the block visibility is adjusted by the file size.
if size%smallestClusterSize != 0 && dstOffset+size != dstSize {
return syscall.EINVAL
}
// Clone first xGiB region. // Clone first xGiB region.
for size > GiB { for size > GiB {
err = callDuplicateExtentsToFile(src.Fd(), dst.Fd(), srcOffset, dstOffset, GiB) _, err = withFileDescriptors(src, dst, func(srcFd, dstFd uintptr) (int, error) {
return 0, callDuplicateExtentsToFile(srcFd, dstFd, srcOffset, dstOffset, GiB)
})
if err != nil { if err != nil {
return wrapError(err) return wrapError(err)
} }
@ -73,7 +101,9 @@ func copyRangeDuplicateExtents(src, dst basicFile, srcOffset, dstOffset, size in
// Clone tail. First try with 64KiB round up, then fallback to 4KiB. // Clone tail. First try with 64KiB round up, then fallback to 4KiB.
for _, cloneRegionSize := range availableClusterSize { for _, cloneRegionSize := range availableClusterSize {
err = callDuplicateExtentsToFile(src.Fd(), dst.Fd(), srcOffset, dstOffset, roundUp(size, cloneRegionSize)) _, err = withFileDescriptors(src, dst, func(srcFd, dstFd uintptr) (int, error) {
return 0, callDuplicateExtentsToFile(srcFd, dstFd, srcOffset, dstOffset, roundUp(size, cloneRegionSize))
})
if err != nil { if err != nil {
continue continue
} }
@ -94,7 +124,7 @@ func wrapError(err error) error {
// see https://docs.microsoft.com/en-us/windows/win32/api/winioctl/ni-winioctl-fsctl_duplicate_extents_to_file // see https://docs.microsoft.com/en-us/windows/win32/api/winioctl/ni-winioctl-fsctl_duplicate_extents_to_file
// //
// memo: Overflow (cloneRegionSize is greater than file ends) is safe and just ignored by windows. // memo: Overflow (cloneRegionSize is greater than file ends) is safe and just ignored by windows.
func callDuplicateExtentsToFile(src, dst uintptr, srcOffset, dstOffset int64, cloneRegionSize int64) (err error) { func callDuplicateExtentsToFile(src, dst uintptr, srcOffset, dstOffset int64, cloneRegionSize int64) error {
var ( var (
bytesReturned uint32 bytesReturned uint32
overlapped windows.Overlapped overlapped windows.Overlapped

View File

@ -4,11 +4,12 @@
// License, v. 2.0. If a copy of the MPL was not distributed with this file, // License, v. 2.0. If a copy of the MPL was not distributed with this file,
// You can obtain one at https://mozilla.org/MPL/2.0/. // You can obtain one at https://mozilla.org/MPL/2.0/.
// +build !windows,!solaris,!darwin // +build linux
package fs package fs
import ( import (
"io"
"syscall" "syscall"
"unsafe" "unsafe"
) )
@ -43,6 +44,10 @@ func copyRangeIoctl(src, dst basicFile, srcOffset, dstOffset, size int64) error
return err return err
} }
if srcOffset+size > fi.Size() {
return io.ErrUnexpectedEOF
}
// https://www.man7.org/linux/man-pages/man2/ioctl_ficlonerange.2.html // https://www.man7.org/linux/man-pages/man2/ioctl_ficlonerange.2.html
// If src_length is zero, the ioctl reflinks to the end of the source file. // If src_length is zero, the ioctl reflinks to the end of the source file.
if srcOffset+size == fi.Size() { if srcOffset+size == fi.Size() {
@ -51,20 +56,36 @@ func copyRangeIoctl(src, dst basicFile, srcOffset, dstOffset, size int64) error
if srcOffset == 0 && dstOffset == 0 && size == 0 { if srcOffset == 0 && dstOffset == 0 && size == 0 {
// Optimization for whole file copies. // Optimization for whole file copies.
_, _, errNo := syscall.Syscall(syscall.SYS_IOCTL, dst.Fd(), FICLONE, src.Fd()) var errNo syscall.Errno
_, err := withFileDescriptors(src, dst, func(srcFd, dstFd uintptr) (int, error) {
_, _, errNo = syscall.Syscall(syscall.SYS_IOCTL, dstFd, FICLONE, srcFd)
return 0, nil
})
// Failure in withFileDescriptors
if err != nil {
return err
}
if errNo != 0 { if errNo != 0 {
return errNo return errNo
} }
return nil return nil
} }
var errNo syscall.Errno
_, err = withFileDescriptors(src, dst, func(srcFd, dstFd uintptr) (int, error) {
params := fileCloneRange{ params := fileCloneRange{
srcFd: int64(src.Fd()), srcFd: int64(srcFd),
srcOffset: uint64(srcOffset), srcOffset: uint64(srcOffset),
srcLength: uint64(size), srcLength: uint64(size),
dstOffset: uint64(dstOffset), dstOffset: uint64(dstOffset),
} }
_, _, errNo := syscall.Syscall(syscall.SYS_IOCTL, dst.Fd(), FICLONERANGE, uintptr(unsafe.Pointer(&params))) _, _, errNo = syscall.Syscall(syscall.SYS_IOCTL, dstFd, FICLONERANGE, uintptr(unsafe.Pointer(&params)))
return 0, nil
})
// Failure in withFileDescriptors
if err != nil {
return err
}
if errNo != 0 { if errNo != 0 {
return errNo return errNo
} }

View File

@ -4,7 +4,7 @@
// License, v. 2.0. If a copy of the MPL was not distributed with this file, // License, v. 2.0. If a copy of the MPL was not distributed with this file,
// You can obtain one at https://mozilla.org/MPL/2.0/. // You can obtain one at https://mozilla.org/MPL/2.0/.
// +build !windows,!darwin // +build linux solaris
package fs package fs
@ -51,7 +51,9 @@ func copyRangeSendFile(src, dst basicFile, srcOffset, dstOffset, size int64) err
// following the last byte that was read. If offset is not NULL, then sendfile() does not modify the current // following the last byte that was read. If offset is not NULL, then sendfile() does not modify the current
// file offset of in_fd; otherwise the current file offset is adjusted to reflect the number of bytes read from // file offset of in_fd; otherwise the current file offset is adjusted to reflect the number of bytes read from
// in_fd. // in_fd.
n, err := syscall.Sendfile(int(dst.Fd()), int(src.Fd()), &srcOffset, int(size)) n, err := withFileDescriptors(dst, src, func(dstFd, srcFd uintptr) (int, error) {
return syscall.Sendfile(int(dstFd), int(srcFd), &srcOffset, int(size))
})
if n == 0 && err == nil { if n == 0 && err == nil {
err = io.ErrUnexpectedEOF err = io.ErrUnexpectedEOF
} }

View File

@ -12,6 +12,7 @@ import (
"io/ioutil" "io/ioutil"
"math/rand" "math/rand"
"os" "os"
"path/filepath"
"syscall" "syscall"
"testing" "testing"
) )
@ -51,7 +52,7 @@ var (
expectedErrors: nil, expectedErrors: nil,
}, },
{ {
name: "append to end, offsets at start", name: "append to end offsets at start",
srcSize: generationSize, srcSize: generationSize,
dstSize: generationSize, dstSize: generationSize,
srcOffset: 0, srcOffset: 0,
@ -124,27 +125,87 @@ var (
copySize: defaultCopySize * 10, copySize: defaultCopySize * 10,
// ioctl returns syscall.EINVAL, rest are wrapped // ioctl returns syscall.EINVAL, rest are wrapped
expectedErrors: map[CopyRangeMethod]error{ expectedErrors: map[CopyRangeMethod]error{
CopyRangeMethodIoctl: syscall.EINVAL, CopyRangeMethodIoctl: io.ErrUnexpectedEOF,
CopyRangeMethodStandard: io.ErrUnexpectedEOF, CopyRangeMethodStandard: io.ErrUnexpectedEOF,
CopyRangeMethodCopyFileRange: io.ErrUnexpectedEOF, CopyRangeMethodCopyFileRange: io.ErrUnexpectedEOF,
CopyRangeMethodSendFile: io.ErrUnexpectedEOF, CopyRangeMethodSendFile: io.ErrUnexpectedEOF,
CopyRangeMethodAllWithFallback: io.ErrUnexpectedEOF, CopyRangeMethodAllWithFallback: io.ErrUnexpectedEOF,
CopyRangeMethodDuplicateExtents: io.ErrUnexpectedEOF,
}, },
}, },
// Non block sized file
{ {
name: "not block aligned write", name: "unaligned source offset unaligned size",
srcSize: generationSize + 2, srcSize: generationSize,
dstSize: 0, dstSize: 0,
srcOffset: 1, srcOffset: 1,
dstOffset: 0, dstOffset: 0,
srcStartingPos: 0, srcStartingPos: 0,
dstStartingPos: 0, dstStartingPos: 0,
expectedDstSizeAfterCopy: generationSize + 1, expectedDstSizeAfterCopy: defaultCopySize + 1,
copySize: generationSize + 1, copySize: defaultCopySize + 1,
// Only fails for ioctl
expectedErrors: map[CopyRangeMethod]error{ expectedErrors: map[CopyRangeMethod]error{
CopyRangeMethodIoctl: syscall.EINVAL, CopyRangeMethodIoctl: syscall.EINVAL,
CopyRangeMethodDuplicateExtents: syscall.EINVAL,
},
},
{
name: "unaligned source offset aligned size",
srcSize: generationSize,
dstSize: 0,
srcOffset: 1,
dstOffset: 0,
srcStartingPos: 0,
dstStartingPos: 0,
expectedDstSizeAfterCopy: defaultCopySize,
copySize: defaultCopySize,
expectedErrors: map[CopyRangeMethod]error{
CopyRangeMethodIoctl: syscall.EINVAL,
CopyRangeMethodDuplicateExtents: syscall.EINVAL,
},
},
{
name: "unaligned destination offset unaligned size",
srcSize: generationSize,
dstSize: generationSize,
srcOffset: 0,
dstOffset: 1,
srcStartingPos: 0,
dstStartingPos: 0,
expectedDstSizeAfterCopy: generationSize,
copySize: defaultCopySize + 1,
expectedErrors: map[CopyRangeMethod]error{
CopyRangeMethodIoctl: syscall.EINVAL,
CopyRangeMethodDuplicateExtents: syscall.EINVAL,
},
},
{
name: "unaligned destination offset aligned size",
srcSize: generationSize,
dstSize: generationSize,
srcOffset: 0,
dstOffset: 1,
srcStartingPos: 0,
dstStartingPos: 0,
expectedDstSizeAfterCopy: generationSize,
copySize: defaultCopySize,
expectedErrors: map[CopyRangeMethod]error{
CopyRangeMethodIoctl: syscall.EINVAL,
CopyRangeMethodDuplicateExtents: syscall.EINVAL,
},
},
{
name: "aligned offsets unaligned size",
srcSize: generationSize,
dstSize: generationSize,
srcOffset: 0,
dstOffset: 0,
srcStartingPos: 0,
dstStartingPos: 0,
expectedDstSizeAfterCopy: generationSize,
copySize: defaultCopySize + 1,
expectedErrors: map[CopyRangeMethod]error{
CopyRangeMethodIoctl: syscall.EINVAL,
CopyRangeMethodDuplicateExtents: syscall.EINVAL,
}, },
}, },
// Last block that starts on a nice boundary // Last block that starts on a nice boundary
@ -189,19 +250,35 @@ var (
} }
) )
func TestCopyRange(ttt *testing.T) { func TestCopyRange(tttt *testing.T) {
randSrc := rand.New(rand.NewSource(rand.Int63())) randSrc := rand.New(rand.NewSource(rand.Int63()))
paths := filepath.SplitList(os.Getenv("STFSTESTPATH"))
if len(paths) == 0 {
paths = []string{""}
}
for _, path := range paths {
testPath, err := ioutil.TempDir(path, "")
if err != nil {
tttt.Fatal(err)
}
defer os.RemoveAll(testPath)
name := path
if name == "" {
name = "tmp"
}
tttt.Run(name, func(ttt *testing.T) {
for copyMethod, impl := range copyRangeMethods { for copyMethod, impl := range copyRangeMethods {
ttt.Run(copyMethod.String(), func(tt *testing.T) { ttt.Run(copyMethod.String(), func(tt *testing.T) {
for _, testCase := range testCases { for _, testCase := range testCases {
tt.Run(testCase.name, func(t *testing.T) { tt.Run(testCase.name, func(t *testing.T) {
srcBuf := make([]byte, testCase.srcSize) srcBuf := make([]byte, testCase.srcSize)
dstBuf := make([]byte, testCase.dstSize) dstBuf := make([]byte, testCase.dstSize)
td, err := ioutil.TempDir(os.Getenv("STFSTESTPATH"), "") td, err := ioutil.TempDir(testPath, "")
if err != nil { if err != nil {
t.Fatal(err) t.Fatal(err)
} }
defer func() { _ = os.RemoveAll(td) }() defer os.RemoveAll(td)
fs := NewFilesystem(FilesystemTypeBasic, td) fs := NewFilesystem(FilesystemTypeBasic, td)
if _, err := io.ReadFull(randSrc, srcBuf); err != nil { if _, err := io.ReadFull(randSrc, srcBuf); err != nil {
@ -319,4 +396,6 @@ func TestCopyRange(ttt *testing.T) {
} }
}) })
} }
})
}
} }

View File

@ -130,7 +130,7 @@ func (f *MtimeFS) Create(name string) (File, error) {
if err != nil { if err != nil {
return nil, err return nil, err
} }
return &mtimeFile{fd, f}, nil return mtimeFile{fd, f}, nil
} }
func (f *MtimeFS) Open(name string) (File, error) { func (f *MtimeFS) Open(name string) (File, error) {
@ -138,7 +138,7 @@ func (f *MtimeFS) Open(name string) (File, error) {
if err != nil { if err != nil {
return nil, err return nil, err
} }
return &mtimeFile{fd, f}, nil return mtimeFile{fd, f}, nil
} }
func (f *MtimeFS) OpenFile(name string, flags int, mode FileMode) (File, error) { func (f *MtimeFS) OpenFile(name string, flags int, mode FileMode) (File, error) {
@ -146,7 +146,7 @@ func (f *MtimeFS) OpenFile(name string, flags int, mode FileMode) (File, error)
if err != nil { if err != nil {
return nil, err return nil, err
} }
return &mtimeFile{fd, f}, nil return mtimeFile{fd, f}, nil
} }
// "real" is the on disk timestamp // "real" is the on disk timestamp
@ -208,7 +208,7 @@ type mtimeFile struct {
fs *MtimeFS fs *MtimeFS
} }
func (f *mtimeFile) Stat() (FileInfo, error) { func (f mtimeFile) Stat() (FileInfo, error) {
info, err := f.File.Stat() info, err := f.File.Stat()
if err != nil { if err != nil {
return nil, err return nil, err
@ -228,6 +228,10 @@ func (f *mtimeFile) Stat() (FileInfo, error) {
return info, nil return info, nil
} }
func (f mtimeFile) unwrap() File {
return f.File
}
// The dbMtime is our database representation // The dbMtime is our database representation
type dbMtime struct { type dbMtime struct {

View File

@ -205,7 +205,11 @@ func TestHandleFileWithTemp(t *testing.T) {
} }
func TestCopierFinder(t *testing.T) { func TestCopierFinder(t *testing.T) {
for _, method := range []fs.CopyRangeMethod{fs.CopyRangeMethodStandard, fs.CopyRangeMethodAllWithFallback} { methods := []fs.CopyRangeMethod{fs.CopyRangeMethodStandard, fs.CopyRangeMethodAllWithFallback}
if runtime.GOOS == "linux" {
methods = append(methods, fs.CopyRangeMethodSendFile)
}
for _, method := range methods {
t.Run(method.String(), func(t *testing.T) { t.Run(method.String(), func(t *testing.T) {
// After diff between required and existing we should: // After diff between required and existing we should:
// Copy: 1, 2, 3, 4, 6, 7, 8 // Copy: 1, 2, 3, 4, 6, 7, 8