Skip to content

Commit 54a8586

Browse files
committed
Add directfile package that satisfies io.Reader and io.ReaderAt
1 parent e7a4df2 commit 54a8586

File tree

4 files changed

+145
-0
lines changed

4 files changed

+145
-0
lines changed

pkg/directfile/alignedbuffer.go

+17
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
package directfile
2+
3+
import (
4+
"unsafe"
5+
)
6+
7+
// In addition to reading the full block from disk, the buffer has to be aligned in memory as well
8+
// Create a buffer that is as long as we need plus one extra block, then get the part of the buffer
9+
// that starts memory aligned
10+
func alignedBuffer(size, align int) ([]byte, error) {
11+
raw := make([]byte, size+align)
12+
offset := int(uintptr(unsafe.Pointer(&raw[0])) & uintptr(align-1))
13+
if offset != 0 {
14+
offset = align - offset
15+
}
16+
return raw[offset : size+offset], nil
17+
}

pkg/directfile/directfile.go

+92
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,92 @@
1+
package directfile
2+
3+
import (
4+
"fmt"
5+
"io"
6+
"os"
7+
)
8+
9+
// DirectFile is a wrapper around os.File that ensures aligned reads for O_DIRECT support,
10+
// even when trying to read small chunks
11+
type DirectFile struct {
12+
file *os.File
13+
blockSize uint32
14+
15+
// Direct indicates whether the current platform actually supported O_DIRECT when opening
16+
Direct bool
17+
}
18+
19+
func openNodirect(path string) (*DirectFile, error) {
20+
// O_DIRECT is not supported on this platform, fallback to normal open
21+
fmt.Println("O_DIRECT not supported, using regular open")
22+
file, err := os.Open(path)
23+
if err != nil {
24+
return nil, fmt.Errorf("error calling os.Open on file: %w", err)
25+
}
26+
27+
return &DirectFile{file: file, Direct: false}, nil
28+
}
29+
30+
// Read satisfies the io.Reader
31+
func (df *DirectFile) Read(p []byte) (n int, err error) {
32+
blockSize := int(df.blockSize)
33+
34+
// Ensure buffer length is aligned with BlockSize for O_DIRECT
35+
alignedSize := (len(p) / blockSize) * blockSize
36+
if alignedSize == 0 {
37+
return 0, fmt.Errorf("buffer size must be at least %d bytes for O_DIRECT", blockSize)
38+
}
39+
40+
// Create an aligned buffer
41+
buf, err := alignedBuffer(alignedSize, blockSize)
42+
if err != nil {
43+
return 0, fmt.Errorf("failed to create aligned buffer: %w", err)
44+
}
45+
46+
// Perform the read
47+
n, err = df.file.Read(buf)
48+
if n > len(p) {
49+
n = len(p) // Only copy as much as p can hold
50+
}
51+
copy(p, buf[:n])
52+
return n, err
53+
}
54+
55+
// ReadAt satisfies the io.ReaderAt interface
56+
func (df *DirectFile) ReadAt(p []byte, off int64) (n int, err error) {
57+
blockSize := int(df.blockSize)
58+
// Calculate aligned offset by rounding down to the nearest BlockSize boundary
59+
// Integer division in go always discards remainder
60+
alignedOffset := (int(off) / blockSize) * blockSize
61+
62+
// Difference between aligned offset and requested offset
63+
// Need to read at least this many extra bytes, since we moved the starting point earlier this much
64+
offsetDiff := int(off) - alignedOffset
65+
66+
// Calculate how much data to read to cover the requested segment, ensuring alignment
67+
alignedReadSize := ((len(p) + offsetDiff + blockSize - 1) / blockSize) * blockSize
68+
69+
// Create an aligned buffer for the full read
70+
buf, err := alignedBuffer(alignedReadSize, blockSize)
71+
if err != nil {
72+
return 0, fmt.Errorf("failed to create aligned buffer: %w", err)
73+
}
74+
75+
// Perform the read at the aligned offset
76+
n, err = df.file.ReadAt(buf, int64(alignedOffset))
77+
if err != nil && err != io.EOF {
78+
return 0, err
79+
}
80+
81+
// Calculate how much of the read buffer to copy into p
82+
copyLen := n - offsetDiff
83+
if copyLen > len(p) {
84+
copyLen = len(p)
85+
} else if copyLen < 0 {
86+
return 0, fmt.Errorf("read beyond end of file")
87+
}
88+
89+
// Copy the relevant part of the buffer to p
90+
copy(p, buf[offsetDiff:offsetDiff+copyLen])
91+
return copyLen, nil
92+
}

pkg/directfile/file_linux.go

+27
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
//go:build linux
2+
3+
package directfile
4+
5+
import (
6+
"fmt"
7+
"os"
8+
"syscall"
9+
)
10+
11+
// OpenFileWithODirect Opens a file without system cache (DIRECT)
12+
func OpenFileWithODirect(path string, blockSize uint32) (*DirectFile, error) {
13+
// Try opening the file with O_DIRECT
14+
fd, err := syscall.Open(path, syscall.O_DIRECT|syscall.O_RDONLY, 0)
15+
if err != nil {
16+
// Fallback to normal open if O_DIRECT is not supported
17+
return openNodirect(path)
18+
}
19+
20+
// Success: Convert file descriptor to os.File and return
21+
file, err := os.NewFile(uintptr(fd), path), nil
22+
if err != nil {
23+
return nil, fmt.Errorf("error calling os.NewFile on file: %w", err)
24+
}
25+
26+
return &DirectFile{file: file, blockSize: blockSize, Direct: true}, nil
27+
}

pkg/directfile/file_nodirect.go

+9
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
//go:build !linux
2+
3+
package directfile
4+
5+
// OpenFileWithODirect Opens a file without system cache (DIRECT)
6+
func OpenFileWithODirect(path string, blockSize uint32) (*DirectFile, error) {
7+
// O_DIRECT is not supported on this platform, fallback to normal open
8+
return openNodirect(path)
9+
}

0 commit comments

Comments
 (0)