diff --git a/tsdb/engine/tsm1/copy_or_link_unix.go b/tsdb/engine/tsm1/copy_or_link_unix.go deleted file mode 100644 index 2ba7974ccd..0000000000 --- a/tsdb/engine/tsm1/copy_or_link_unix.go +++ /dev/null @@ -1,17 +0,0 @@ -//go:build !windows -// +build !windows - -package tsm1 - -import ( - "fmt" - "os" -) - -// copyOrLink - allow substitution of a file copy for a hard link when running on Windows systems. -func copyOrLink(oldPath, newPath string) error { - if err := os.Link(oldPath, newPath); err != nil { - return fmt.Errorf("error creating hard link for backup from %s to %s: %q", oldPath, newPath, err) - } - return nil -} diff --git a/tsdb/engine/tsm1/copy_or_link_windows.go b/tsdb/engine/tsm1/copy_or_link_windows.go deleted file mode 100644 index fdccfbd5d7..0000000000 --- a/tsdb/engine/tsm1/copy_or_link_windows.go +++ /dev/null @@ -1,46 +0,0 @@ -//go:build windows -// +build windows - -package tsm1 - -import ( - "fmt" - "io" - "os" -) - -// copyOrLink - Windows does not permit deleting a file with open file handles, so -// instead of hard links, make temporary copies of files that can then be deleted. -func copyOrLink(oldPath, newPath string) (returnErr error) { - rfd, err := os.Open(oldPath) - if err != nil { - return fmt.Errorf("error opening file for backup %s: %q", oldPath, err) - } else { - defer func() { - if e := rfd.Close(); returnErr == nil && e != nil { - returnErr = fmt.Errorf("error closing source file for backup %s: %q", oldPath, e) - } - }() - } - fi, err := rfd.Stat() - if err != nil { - fmt.Errorf("error collecting statistics from file for backup %s: %q", oldPath, err) - } - wfd, err := os.OpenFile(newPath, os.O_RDWR|os.O_CREATE, fi.Mode()) - if err != nil { - return fmt.Errorf("error creating temporary file for backup %s: %q", newPath, err) - } else { - defer func() { - if e := wfd.Close(); returnErr == nil && e != nil { - returnErr = fmt.Errorf("error closing temporary file for backup %s: %q", newPath, e) - } - }() - } - if _, err := io.Copy(wfd, rfd); err != nil { - return fmt.Errorf("unable to copy file for backup from %s to %s: %q", oldPath, newPath, err) - } - if err := os.Chtimes(newPath, fi.ModTime(), fi.ModTime()); err != nil { - return fmt.Errorf("unable to set modification time on temporary backup file %s: %q", newPath, err) - } - return nil -} diff --git a/tsdb/engine/tsm1/file_store.go b/tsdb/engine/tsm1/file_store.go index 6d3c410185..cc140640fb 100644 --- a/tsdb/engine/tsm1/file_store.go +++ b/tsdb/engine/tsm1/file_store.go @@ -5,6 +5,7 @@ import ( "context" "errors" "fmt" + "io" "math" "os" "path/filepath" @@ -14,6 +15,7 @@ import ( "strings" "sync" "sync/atomic" + "syscall" "time" "github.com/influxdata/influxdb/v2/influxql/query" @@ -192,6 +194,8 @@ type FileStore struct { parseFileName ParseFileNameFunc obs tsdb.FileStoreObserver + + copyFiles bool } // FileStat holds information about a TSM file on disk. @@ -243,6 +247,7 @@ func NewFileStore(dir string) *FileStore { }, obs: noFileStoreObserver{}, parseFileName: DefaultParseFileName, + copyFiles: runtime.GOOS == "windows", } fs.purger.fileStore = fs return fs @@ -1078,12 +1083,14 @@ func (f *FileStore) locations(key []byte, t int64, ascending bool) []*location { func (f *FileStore) MakeSnapshotLinks(destPath string, files []TSMFile) (returnErr error) { for _, tsmf := range files { newpath := filepath.Join(destPath, filepath.Base(tsmf.Path())) - if err := copyOrLink(tsmf.Path(), newpath); err != nil { + err := f.copyOrLink(tsmf.Path(), newpath) + if err != nil { return err } if tf := tsmf.TombstoneStats(); tf.TombstoneExists { newpath := filepath.Join(destPath, filepath.Base(tf.Path)) - if err := copyOrLink(tf.Path, newpath); err != nil { + err := f.copyOrLink(tf.Path, newpath) + if err != nil { return err } } @@ -1091,6 +1098,81 @@ func (f *FileStore) MakeSnapshotLinks(destPath string, files []TSMFile) (returnE return nil } +func (f *FileStore) copyOrLink(oldpath string, newpath string) error { + if f.copyFiles { + f.logger.Info("copying backup snapshots", zap.String("OldPath", oldpath), zap.String("NewPath", newpath)) + if err := f.copyNotLink(oldpath, newpath); err != nil { + return err + } + } else { + f.logger.Info("linking backup snapshots", zap.String("OldPath", oldpath), zap.String("NewPath", newpath)) + if err := f.linkNotCopy(oldpath, newpath); err != nil { + return err + } + } + return nil +} + +// copyNotLink - use file copies instead of hard links for 2 scenarios: +// Windows does not permit deleting a file with open file handles +// Azure does not support hard links in its default file system +func (f *FileStore) copyNotLink(oldPath, newPath string) (returnErr error) { + rfd, err := os.Open(oldPath) + if err != nil { + return fmt.Errorf("error opening file for backup %s: %q", oldPath, err) + } else { + defer func() { + if e := rfd.Close(); returnErr == nil && e != nil { + returnErr = fmt.Errorf("error closing source file for backup %s: %w", oldPath, e) + } + }() + } + fi, err := rfd.Stat() + if err != nil { + return fmt.Errorf("error collecting statistics from file for backup %s: %w", oldPath, err) + } + wfd, err := os.OpenFile(newPath, os.O_RDWR|os.O_CREATE, fi.Mode()) + if err != nil { + return fmt.Errorf("error creating temporary file for backup %s: %w", newPath, err) + } else { + defer func() { + if e := wfd.Close(); returnErr == nil && e != nil { + returnErr = fmt.Errorf("error closing temporary file for backup %s: %w", newPath, e) + } + }() + } + if _, err := io.Copy(wfd, rfd); err != nil { + return fmt.Errorf("unable to copy file for backup from %s to %s: %w", oldPath, newPath, err) + } + if err := os.Chtimes(newPath, fi.ModTime(), fi.ModTime()); err != nil { + return fmt.Errorf("unable to set modification time on temporary backup file %s: %w", newPath, err) + } + return nil +} + +// linkNotCopy - use hard links for backup snapshots +func (f *FileStore) linkNotCopy(oldPath, newPath string) error { + if err := os.Link(oldPath, newPath); err != nil { + if errors.Is(err, syscall.ENOTSUP) { + if fi, e := os.Stat(oldPath); e == nil && !fi.IsDir() { + f.logger.Info("file system does not support hard links, switching to copies for backup", zap.String("OldPath", oldPath), zap.String("NewPath", newPath)) + // Force future snapshots to copy + f.copyFiles = true + return f.copyNotLink(oldPath, newPath) + } else if e != nil { + // Stat failed + return fmt.Errorf("error creating hard link for backup, cannot determine if %s is a file or directory: %w", oldPath, e) + } else { + return fmt.Errorf("error creating hard link for backup - %s is a directory, not a file: %q", oldPath, err) + } + } else { + return fmt.Errorf("error creating hard link for backup from %s to %s: %w", oldPath, newPath, err) + } + } else { + return nil + } +} + // CreateSnapshot creates hardlinks for all tsm and tombstone files // in the path provided. func (f *FileStore) CreateSnapshot() (string, error) {