Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add file package that can wrap files with some helpers #54

Merged
merged 10 commits into from
Oct 28, 2024
17 changes: 17 additions & 0 deletions pkg/directfile/alignedbuffer.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
package directfile

import (
"unsafe"
)

// In addition to reading the full block from disk, the buffer has to be aligned in memory as well
// Create a buffer that is as long as we need plus one extra block, then get the part of the buffer
// that starts memory aligned
func alignedBuffer(size, align int) ([]byte, error) {
raw := make([]byte, size+align)
offset := int(uintptr(unsafe.Pointer(&raw[0])) & uintptr(align-1))
if offset != 0 {
offset = align - offset
}
return raw[offset : size+offset], nil
}
97 changes: 97 additions & 0 deletions pkg/directfile/directfile.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,97 @@
package directfile

import (
"fmt"
"io"
"os"
)

// DirectFile is a wrapper around os.File that ensures aligned reads for O_DIRECT support,
// even when trying to read small chunks
type DirectFile struct {
file *os.File
blockSize uint32

// Direct indicates whether the current platform actually supported O_DIRECT when opening
Direct bool
}

func openNodirect(path string) (*DirectFile, error) {
// O_DIRECT is not supported on this platform, fallback to normal open
fmt.Println("O_DIRECT not supported, using regular open")
file, err := os.Open(path)
if err != nil {
return nil, fmt.Errorf("error calling os.Open on file: %w", err)
}

return &DirectFile{file: file, Direct: false}, nil
}

// Close closes the file
func (df *DirectFile) Close() error {
return df.file.Close()
}

// Read satisfies the io.Reader
func (df *DirectFile) Read(p []byte) (n int, err error) {
blockSize := int(df.blockSize)

// Ensure buffer length is aligned with BlockSize for O_DIRECT
alignedSize := (len(p) / blockSize) * blockSize
if alignedSize == 0 {
return 0, fmt.Errorf("buffer size must be at least %d bytes for O_DIRECT", blockSize)
}

// Create an aligned buffer
buf, err := alignedBuffer(alignedSize, blockSize)
if err != nil {
return 0, fmt.Errorf("failed to create aligned buffer: %w", err)
}

// Perform the read
n, err = df.file.Read(buf)
if n > len(p) {
n = len(p) // Only copy as much as p can hold
}
copy(p, buf[:n])
return n, err
}

// ReadAt satisfies the io.ReaderAt interface
func (df *DirectFile) ReadAt(p []byte, off int64) (n int, err error) {
blockSize := int(df.blockSize)
// Calculate aligned offset by rounding down to the nearest BlockSize boundary
// Integer division in go always discards remainder
alignedOffset := (int(off) / blockSize) * blockSize

// Difference between aligned offset and requested offset
// Need to read at least this many extra bytes, since we moved the starting point earlier this much
offsetDiff := int(off) - alignedOffset

// Calculate how much data to read to cover the requested segment, ensuring alignment
alignedReadSize := ((len(p) + offsetDiff + blockSize - 1) / blockSize) * blockSize

// Create an aligned buffer for the full read
buf, err := alignedBuffer(alignedReadSize, blockSize)
if err != nil {
return 0, fmt.Errorf("failed to create aligned buffer: %w", err)
}

// Perform the read at the aligned offset
n, err = df.file.ReadAt(buf, int64(alignedOffset))
if err != nil && err != io.EOF {
return 0, err
}

// Calculate how much of the read buffer to copy into p
copyLen := n - offsetDiff
if copyLen > len(p) {
copyLen = len(p)
} else if copyLen < 0 {
return 0, fmt.Errorf("read beyond end of file")
}

// Copy the relevant part of the buffer to p
copy(p, buf[offsetDiff:offsetDiff+copyLen])
return copyLen, nil
}
27 changes: 27 additions & 0 deletions pkg/directfile/file_linux.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
//go:build linux

package directfile

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

// OpenFileWithODirect Opens a file without system cache (DIRECT)
func OpenFileWithODirect(path string, blockSize uint32) (*DirectFile, error) {
// Try opening the file with O_DIRECT
fd, err := syscall.Open(path, syscall.O_DIRECT|syscall.O_RDONLY, 0)
if err != nil {
// Fallback to normal open if O_DIRECT is not supported
return openNodirect(path)
}

// Success: Convert file descriptor to os.File and return
file, err := os.NewFile(uintptr(fd), path), nil
if err != nil {
return nil, fmt.Errorf("error calling os.NewFile on file: %w", err)
}

return &DirectFile{file: file, blockSize: blockSize, Direct: true}, nil
}
9 changes: 9 additions & 0 deletions pkg/directfile/file_nodirect.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
//go:build !linux

package directfile

// OpenFileWithODirect Opens a file without system cache (DIRECT)
func OpenFileWithODirect(path string, blockSize uint32) (*DirectFile, error) {
// O_DIRECT is not supported on this platform, fallback to normal open
return openNodirect(path)
}