mirror of
https://github.com/versity/versitygw.git
synced 2026-08-16 12:16:14 +00:00
Merge pull request #2268 from versity/ben/odirect
feat: add best-effort O_DIRECT support for posix put/get-object put-part
This commit is contained in:
@@ -0,0 +1,192 @@
|
||||
// Copyright 2026 Versity Software
|
||||
// This file is licensed under the Apache License, Version 2.0
|
||||
// (the "License"); you may not use this file except in compliance
|
||||
// with the License. You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing,
|
||||
// software distributed under the License is distributed on an
|
||||
// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||
// KIND, either express or implied. See the License for the
|
||||
// specific language governing permissions and limitations
|
||||
// under the License.
|
||||
|
||||
//go:build linux
|
||||
|
||||
package posix
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"io"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strconv"
|
||||
"sync"
|
||||
"syscall"
|
||||
|
||||
"github.com/versity/versitygw/backend"
|
||||
)
|
||||
|
||||
// openDataRead opens object data for reading and applies O_DIRECT when
|
||||
// requested. O_DIRECT is best-effort and falls back to buffered I/O when
|
||||
// unsupported by the filesystem.
|
||||
func openDataRead(name string, useODirect bool) (*os.File, error) {
|
||||
if !useODirect {
|
||||
return os.Open(name)
|
||||
}
|
||||
|
||||
f, err := os.OpenFile(name, syscall.O_RDONLY|syscall.O_DIRECT, 0)
|
||||
if err != nil {
|
||||
if isODirectUnsupportedOpenErr(err) {
|
||||
warnODirectUnsupportedOnce("openDataRead", err)
|
||||
return os.Open(name)
|
||||
}
|
||||
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return f, nil
|
||||
}
|
||||
|
||||
func buildGetObjectBody(f *os.File, path string, startOffset, length, objSize int64, useODirect bool, readBufferSize int) (io.ReadCloser, error) {
|
||||
if startOffset == 0 && length == objSize {
|
||||
if useODirect {
|
||||
return newODirectReadFallbackFile(path, f, true), nil
|
||||
}
|
||||
return f, nil
|
||||
}
|
||||
|
||||
if !useODirect {
|
||||
rdr := io.NewSectionReader(f, startOffset, length)
|
||||
return withReadBufferSize(&backend.FileSectionReadCloser{R: rdr, F: f}, readBufferSize), nil
|
||||
}
|
||||
|
||||
rf := newODirectReadFallbackFile(path, f, true)
|
||||
|
||||
if _, err := rf.Seek(startOffset, io.SeekStart); err != nil {
|
||||
_ = rf.Close()
|
||||
return nil, fmt.Errorf("seek range start: %w", err)
|
||||
}
|
||||
|
||||
return withReadBufferSize(&readerWithCloser{r: io.LimitReader(rf, length), c: rf}, readBufferSize), nil
|
||||
}
|
||||
|
||||
type readerWithCloser struct {
|
||||
r io.Reader
|
||||
c io.Closer
|
||||
}
|
||||
|
||||
func (r *readerWithCloser) Read(p []byte) (int, error) {
|
||||
return r.r.Read(p)
|
||||
}
|
||||
|
||||
func (r *readerWithCloser) Close() error {
|
||||
return r.c.Close()
|
||||
}
|
||||
|
||||
type odirectReadFallbackFile struct {
|
||||
mu sync.Mutex
|
||||
path string
|
||||
f *os.File
|
||||
useODirect bool
|
||||
}
|
||||
|
||||
func newODirectReadFallbackFile(path string, f *os.File, useODirect bool) *odirectReadFallbackFile {
|
||||
return &odirectReadFallbackFile{
|
||||
path: path,
|
||||
f: f,
|
||||
useODirect: useODirect,
|
||||
}
|
||||
}
|
||||
|
||||
func (r *odirectReadFallbackFile) Read(p []byte) (int, error) {
|
||||
r.mu.Lock()
|
||||
defer r.mu.Unlock()
|
||||
|
||||
n, err := r.f.Read(p)
|
||||
if err != nil && n == 0 && r.useODirect && isODirectRuntimeFallbackErr(err) {
|
||||
if fallbackErr := r.switchToBufferedAtCurrentOffsetLocked(); fallbackErr != nil {
|
||||
return 0, fallbackErr
|
||||
}
|
||||
|
||||
return r.f.Read(p)
|
||||
}
|
||||
|
||||
return n, err
|
||||
}
|
||||
|
||||
func (r *odirectReadFallbackFile) WriteTo(w io.Writer) (int64, error) {
|
||||
r.mu.Lock()
|
||||
useODirect := r.useODirect
|
||||
f := r.f
|
||||
r.mu.Unlock()
|
||||
|
||||
if !useODirect {
|
||||
return io.Copy(w, &onlyRead{r})
|
||||
}
|
||||
|
||||
writerTo, ok := interface{}(f).(io.WriterTo)
|
||||
if !ok {
|
||||
return io.Copy(w, &onlyRead{r})
|
||||
}
|
||||
|
||||
n, err := writerTo.WriteTo(w)
|
||||
if err == nil || !isODirectRuntimeFallbackErr(err) {
|
||||
return n, err
|
||||
}
|
||||
|
||||
r.mu.Lock()
|
||||
fallbackErr := r.switchToBufferedAtCurrentOffsetLocked()
|
||||
r.mu.Unlock()
|
||||
if fallbackErr != nil {
|
||||
return n, fallbackErr
|
||||
}
|
||||
|
||||
m, err := io.Copy(w, &onlyRead{r})
|
||||
return n + m, err
|
||||
}
|
||||
|
||||
func (r *odirectReadFallbackFile) Seek(offset int64, whence int) (int64, error) {
|
||||
r.mu.Lock()
|
||||
defer r.mu.Unlock()
|
||||
return r.f.Seek(offset, whence)
|
||||
}
|
||||
|
||||
func (r *odirectReadFallbackFile) Close() error {
|
||||
r.mu.Lock()
|
||||
defer r.mu.Unlock()
|
||||
return r.f.Close()
|
||||
}
|
||||
|
||||
func (r *odirectReadFallbackFile) switchToBufferedAtOffset(offset int64) error {
|
||||
fd := strconv.Itoa(int(r.f.Fd()))
|
||||
bf, openErr := os.Open(filepath.Join(procfddir, fd))
|
||||
if openErr != nil {
|
||||
return openErr
|
||||
}
|
||||
|
||||
if _, seekErr := bf.Seek(offset, io.SeekStart); seekErr != nil {
|
||||
_ = bf.Close()
|
||||
return seekErr
|
||||
}
|
||||
|
||||
if closeErr := r.f.Close(); closeErr != nil {
|
||||
_ = bf.Close()
|
||||
return closeErr
|
||||
}
|
||||
|
||||
r.f = bf
|
||||
r.useODirect = false
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (r *odirectReadFallbackFile) switchToBufferedAtCurrentOffsetLocked() error {
|
||||
offset, seekErr := r.f.Seek(0, io.SeekCurrent)
|
||||
if seekErr != nil {
|
||||
return seekErr
|
||||
}
|
||||
|
||||
return r.switchToBufferedAtOffset(offset)
|
||||
}
|
||||
@@ -0,0 +1,33 @@
|
||||
// Copyright 2026 Versity Software
|
||||
// This file is licensed under the Apache License, Version 2.0
|
||||
// (the "License"); you may not use this file except in compliance
|
||||
// with the License. You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing,
|
||||
// software distributed under the License is distributed on an
|
||||
// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||
// KIND, either express or implied. See the License for the
|
||||
// specific language governing permissions and limitations
|
||||
// under the License.
|
||||
|
||||
//go:build !linux
|
||||
|
||||
package posix
|
||||
|
||||
import (
|
||||
"io"
|
||||
"os"
|
||||
|
||||
"github.com/versity/versitygw/backend"
|
||||
)
|
||||
|
||||
func buildGetObjectBody(f *os.File, _ string, startOffset, length, objSize int64, _ bool, readBufferSize int) (io.ReadCloser, error) {
|
||||
if startOffset == 0 && length == objSize {
|
||||
return f, nil
|
||||
}
|
||||
|
||||
rdr := io.NewSectionReader(f, startOffset, length)
|
||||
return withReadBufferSize(&backend.FileSectionReadCloser{R: rdr, F: f}, readBufferSize), nil
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
// Copyright 2026 Versity Software
|
||||
// This file is licensed under the Apache License, Version 2.0
|
||||
// (the "License"); you may not use this file except in compliance
|
||||
// with the License. You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing,
|
||||
// software distributed under the License is distributed on an
|
||||
// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||
// KIND, either express or implied. See the License for the
|
||||
// specific language governing permissions and limitations
|
||||
// under the License.
|
||||
|
||||
//go:build !linux && !windows
|
||||
|
||||
package posix
|
||||
|
||||
import "os"
|
||||
|
||||
func openDataRead(name string, useODirect bool) (*os.File, error) {
|
||||
if useODirect {
|
||||
warnODirectUnsupportedOnce("openDataRead-nonlinux", os.ErrInvalid)
|
||||
}
|
||||
return os.Open(name)
|
||||
}
|
||||
@@ -0,0 +1,46 @@
|
||||
// Copyright 2026 Versity Software
|
||||
// This file is licensed under the Apache License, Version 2.0
|
||||
// (the "License"); you may not use this file except in compliance
|
||||
// with the License. You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing,
|
||||
// software distributed under the License is distributed on an
|
||||
// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||
// KIND, either express or implied. See the License for the
|
||||
// specific language governing permissions and limitations
|
||||
// under the License.
|
||||
|
||||
//go:build windows
|
||||
|
||||
package posix
|
||||
|
||||
import (
|
||||
"os"
|
||||
"syscall"
|
||||
)
|
||||
|
||||
func openDataRead(name string, useODirect bool) (*os.File, error) {
|
||||
if useODirect {
|
||||
warnODirectUnsupportedOnce("openDataRead-windows", os.ErrInvalid)
|
||||
}
|
||||
|
||||
ptr, err := syscall.UTF16PtrFromString(name)
|
||||
if err != nil {
|
||||
return nil, &os.PathError{Op: "open", Path: name, Err: err}
|
||||
}
|
||||
h, err := syscall.CreateFile(
|
||||
ptr,
|
||||
syscall.GENERIC_READ,
|
||||
syscall.FILE_SHARE_READ|syscall.FILE_SHARE_WRITE|syscall.FILE_SHARE_DELETE,
|
||||
nil,
|
||||
syscall.OPEN_EXISTING,
|
||||
syscall.FILE_ATTRIBUTE_NORMAL,
|
||||
0,
|
||||
)
|
||||
if err != nil {
|
||||
return nil, &os.PathError{Op: "open", Path: name, Err: err}
|
||||
}
|
||||
return os.NewFile(uintptr(h), name), nil
|
||||
}
|
||||
@@ -46,9 +46,7 @@ func isErrDirNotEmpty(err error) bool {
|
||||
return errors.Is(err, syscall.ENOTEMPTY)
|
||||
}
|
||||
|
||||
// openForRead opens a file for reading. On non-Windows systems, os.Open is
|
||||
// sufficient because POSIX allows removing (unlinking) a file that is still
|
||||
// open by another process.
|
||||
func openForRead(name string) (*os.File, error) {
|
||||
return os.Open(name)
|
||||
// openForRead opens an object data file for reading.
|
||||
func openForRead(name string, useODirect bool) (*os.File, error) {
|
||||
return openDataRead(name, useODirect)
|
||||
}
|
||||
|
||||
@@ -110,22 +110,6 @@ func isErrNotDir(err error) bool {
|
||||
// is held open for streaming the GET response body. Without this flag,
|
||||
// Windows returns "The process cannot access the file because it is being
|
||||
// used by another process" on the Remove call.
|
||||
func openForRead(name string) (*os.File, error) {
|
||||
ptr, err := syscall.UTF16PtrFromString(name)
|
||||
if err != nil {
|
||||
return nil, &os.PathError{Op: "open", Path: name, Err: err}
|
||||
}
|
||||
h, err := syscall.CreateFile(
|
||||
ptr,
|
||||
syscall.GENERIC_READ,
|
||||
syscall.FILE_SHARE_READ|syscall.FILE_SHARE_WRITE|syscall.FILE_SHARE_DELETE,
|
||||
nil,
|
||||
syscall.OPEN_EXISTING,
|
||||
syscall.FILE_ATTRIBUTE_NORMAL,
|
||||
0,
|
||||
)
|
||||
if err != nil {
|
||||
return nil, &os.PathError{Op: "open", Path: name, Err: err}
|
||||
}
|
||||
return os.NewFile(uintptr(h), name), nil
|
||||
func openForRead(name string, useODirect bool) (*os.File, error) {
|
||||
return openDataRead(name, useODirect)
|
||||
}
|
||||
|
||||
@@ -0,0 +1,29 @@
|
||||
// Copyright 2026 Versity Software
|
||||
// This file is licensed under the Apache License, Version 2.0
|
||||
// (the "License"); you may not use this file except in compliance
|
||||
// with the License. You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing,
|
||||
// software distributed under the License is distributed on an
|
||||
// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||
// KIND, either express or implied. See the License for the
|
||||
// specific language governing permissions and limitations
|
||||
// under the License.
|
||||
|
||||
//go:build linux
|
||||
|
||||
package posix
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"syscall"
|
||||
)
|
||||
|
||||
// Shared by Linux O_DIRECT open paths in data reads and tmpfile creation.
|
||||
func isODirectUnsupportedOpenErr(err error) bool {
|
||||
return errors.Is(err, syscall.EINVAL) ||
|
||||
errors.Is(err, syscall.EOPNOTSUPP) ||
|
||||
errors.Is(err, syscall.ENOTSUP)
|
||||
}
|
||||
@@ -15,22 +15,83 @@
|
||||
package posix
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"math/rand"
|
||||
"os"
|
||||
"strconv"
|
||||
"syscall"
|
||||
"time"
|
||||
)
|
||||
|
||||
const odirectMinWriteAlign = 512
|
||||
|
||||
func (tmp *tmpfile) Write(b []byte) (int, error) {
|
||||
if int64(len(b)) > tmp.size {
|
||||
return 0, fmt.Errorf("write exceeds content length %v", tmp.size)
|
||||
}
|
||||
|
||||
if tmp.useODirect && !isODirectLenAligned(len(b)) {
|
||||
if err := tmp.switchToBufferedAtCurrentOffset(fmt.Sprintf("unaligned write length: len=%d len%%512=%d", len(b), len(b)%odirectMinWriteAlign)); err != nil {
|
||||
return 0, err
|
||||
}
|
||||
}
|
||||
|
||||
n, err := tmp.f.Write(b)
|
||||
if err != nil && n == 0 && tmp.useODirect && isODirectRuntimeFallbackErr(err) {
|
||||
warnODirectUnsupportedOnce("tmpfile.Write", err)
|
||||
if fallbackErr := tmp.switchToBufferedAtCurrentOffset("O_DIRECT write failure"); fallbackErr != nil {
|
||||
return 0, fallbackErr
|
||||
}
|
||||
|
||||
n, err = tmp.f.Write(b)
|
||||
}
|
||||
tmp.size -= int64(n)
|
||||
return n, err
|
||||
}
|
||||
|
||||
func (tmp *tmpfile) switchToBufferedAtCurrentOffset(reason string) error {
|
||||
offset, seekErr := tmp.f.Seek(0, io.SeekCurrent)
|
||||
if seekErr != nil {
|
||||
return fmt.Errorf("capture write offset before fallback reopen: %w", seekErr)
|
||||
}
|
||||
|
||||
name := tmp.f.Name()
|
||||
f, openErr := os.OpenFile(name, os.O_RDWR, 0)
|
||||
if openErr != nil {
|
||||
return fmt.Errorf("reopen temp file in buffered mode after O_DIRECT fallback (%s): %w", reason, openErr)
|
||||
}
|
||||
|
||||
if _, seekErr = f.Seek(offset, io.SeekStart); seekErr != nil {
|
||||
f.Close()
|
||||
return fmt.Errorf("restore write offset after fallback reopen: %w", seekErr)
|
||||
}
|
||||
|
||||
if closeErr := tmp.f.Close(); closeErr != nil {
|
||||
f.Close()
|
||||
return fmt.Errorf("close O_DIRECT temp file after fallback reopen: %w", closeErr)
|
||||
}
|
||||
|
||||
tmp.f = f
|
||||
if tmp.isOTmp {
|
||||
tmp.procFDName = strconv.Itoa(int(f.Fd()))
|
||||
}
|
||||
tmp.useODirect = false
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func isODirectLenAligned(n int) bool {
|
||||
return n%odirectMinWriteAlign == 0
|
||||
}
|
||||
|
||||
func isODirectRuntimeFallbackErr(err error) bool {
|
||||
return errors.Is(err, syscall.EINVAL) ||
|
||||
errors.Is(err, syscall.EOPNOTSUPP) ||
|
||||
errors.Is(err, syscall.ENOTSUP)
|
||||
}
|
||||
|
||||
func (tmp *tmpfile) File() *os.File {
|
||||
return tmp.f
|
||||
}
|
||||
|
||||
+87
-20
@@ -29,6 +29,7 @@ import (
|
||||
"sort"
|
||||
"strconv"
|
||||
"strings"
|
||||
"sync"
|
||||
"syscall"
|
||||
"time"
|
||||
|
||||
@@ -87,6 +88,10 @@ type Posix struct {
|
||||
// support copy_file_range is mounted over NFSv4.2.
|
||||
forceNoCopyFileRange bool
|
||||
|
||||
// enableODirect is a flag to open object data files with O_DIRECT.
|
||||
// This is best-effort and falls back to buffered I/O when unsupported.
|
||||
enableODirect bool
|
||||
|
||||
// enable posix level bucket name validations, not needed if the
|
||||
// frontend handlers are already validating bucket names
|
||||
validateBucketName bool
|
||||
@@ -108,6 +113,10 @@ type Posix struct {
|
||||
// rejected with an 'InvalidRequest' to comply with the S3 limit
|
||||
// of 5 GiB.
|
||||
copyObjectThreshold int64
|
||||
|
||||
// ioBufferSize is the buffer size used by buffered copy/read paths.
|
||||
ioBufferSize int
|
||||
ioBufferPool sync.Pool
|
||||
}
|
||||
|
||||
var _ backend.Backend = &Posix{}
|
||||
@@ -149,10 +158,17 @@ const (
|
||||
doFalloc = true
|
||||
skipFalloc = false
|
||||
|
||||
odirectAllowed odirectPolicy = true
|
||||
odirectNotAllowed odirectPolicy = false
|
||||
|
||||
// defaultConcurrency is the default limit for concurrent POSIX actions.
|
||||
defaultConcurrency = 5000
|
||||
// defaultIOBufferSize is the default buffer size used by io.CopyBuffer paths.
|
||||
defaultIOBufferSize = 1024 * 1024
|
||||
)
|
||||
|
||||
type odirectPolicy bool
|
||||
|
||||
// PosixOpts are the options for the Posix backend
|
||||
type PosixOpts struct {
|
||||
// ChownUID sets the UID of the object to the UID of the user on PUT
|
||||
@@ -172,6 +188,9 @@ type PosixOpts struct {
|
||||
ForceNoTmpFile bool
|
||||
// ForceNoCopyFileRange disables the use of io.Copy for multipart uploads parts
|
||||
ForceNoCopyFileRange bool
|
||||
// EnableODirect enables best-effort O_DIRECT for object data reads/writes.
|
||||
// Disabled by default.
|
||||
EnableODirect bool
|
||||
// ValidateBucketNames enables minimal bucket name validation to prevent
|
||||
// incorrect access to the filesystem. This is only needed if the
|
||||
// frontend is not already validating bucket names.
|
||||
@@ -194,9 +213,14 @@ type PosixOpts struct {
|
||||
// attribute (e.g. files placed on the filesystem outside of versitygw).
|
||||
// When empty, such objects are served with an empty ETag.
|
||||
DefaultEtag string
|
||||
// IOBufferSize sets the buffer size (in bytes) for copy/read paths that use
|
||||
// io.CopyBuffer or buffered readers. Defaults to 1MiB when unset or invalid.
|
||||
IOBufferSize int
|
||||
}
|
||||
|
||||
func New(rootdir string, meta meta.MetadataStorer, opts PosixOpts) (*Posix, error) {
|
||||
ioBufferSize := ioBufferSizeOrDefault(opts.IOBufferSize)
|
||||
|
||||
if opts.SideCarDir != "" && strings.HasPrefix(opts.SideCarDir, rootdir) {
|
||||
return nil, fmt.Errorf("sidecar directory cannot be inside the gateway root directory")
|
||||
}
|
||||
@@ -255,10 +279,16 @@ func New(rootdir string, meta meta.MetadataStorer, opts PosixOpts) (*Posix, erro
|
||||
newDirPerm: opts.NewDirPerm,
|
||||
forceNoTmpFile: opts.ForceNoTmpFile,
|
||||
forceNoCopyFileRange: opts.ForceNoCopyFileRange,
|
||||
enableODirect: opts.EnableODirect,
|
||||
validateBucketName: opts.ValidateBucketNames,
|
||||
actionLimiter: semaphore.NewWeighted(int64(concurrencyOrDefault(opts.Concurrency))),
|
||||
copyObjectThreshold: opts.CopyObjectThreshold,
|
||||
defaultEtag: opts.DefaultEtag,
|
||||
ioBufferSize: ioBufferSize,
|
||||
ioBufferPool: sync.Pool{New: func() any {
|
||||
b := make([]byte, ioBufferSize)
|
||||
return &b
|
||||
}},
|
||||
}, nil
|
||||
}
|
||||
|
||||
@@ -270,6 +300,32 @@ func concurrencyOrDefault(n int) int {
|
||||
return defaultConcurrency
|
||||
}
|
||||
|
||||
func ioBufferSizeOrDefault(n int) int {
|
||||
if n > 0 {
|
||||
return n
|
||||
}
|
||||
return defaultIOBufferSize
|
||||
}
|
||||
|
||||
func (p *Posix) getIOBuffer() []byte {
|
||||
bp, ok := p.ioBufferPool.Get().(*[]byte)
|
||||
if !ok || bp == nil || cap(*bp) < p.ioBufferSize {
|
||||
return make([]byte, p.ioBufferSize)
|
||||
}
|
||||
return (*bp)[:p.ioBufferSize]
|
||||
}
|
||||
|
||||
func (p *Posix) putIOBuffer(b []byte) {
|
||||
if b == nil {
|
||||
return
|
||||
}
|
||||
if cap(b) < p.ioBufferSize {
|
||||
return
|
||||
}
|
||||
b = b[:p.ioBufferSize]
|
||||
p.ioBufferPool.Put(&b)
|
||||
}
|
||||
|
||||
func validateSubDir(root, dir string) (string, error) {
|
||||
absDir, err := filepath.Abs(dir)
|
||||
if err != nil {
|
||||
@@ -945,12 +1001,13 @@ func (p *Posix) createObjVersion(bucket, key string, size int64, acc auth.Accoun
|
||||
versioningKey := filepath.Join(genObjVersionKey(key), versionId)
|
||||
versionTmpPath := filepath.Join(versionBucketPath, MetaTmpDir)
|
||||
f, err := p.openTmpFile(versionTmpPath, versionBucketPath, versioningKey,
|
||||
size, acc, doFalloc, p.forceNoTmpFile)
|
||||
size, acc, doFalloc, p.forceNoTmpFile, odirectNotAllowed)
|
||||
if err != nil {
|
||||
return versionPath, err
|
||||
}
|
||||
defer f.cleanup()
|
||||
|
||||
// Prioritize copy_file_range for internal file-to-file version copies.
|
||||
_, err = io.Copy(f.File(), sf)
|
||||
if err != nil {
|
||||
return versionPath, err
|
||||
@@ -2092,7 +2149,7 @@ func (p *Posix) CompleteMultipartUploadWithCopy(ctx context.Context, input *s3.C
|
||||
}
|
||||
|
||||
f, err := p.openTmpFile(filepath.Join(bucket, MetaTmpDir), bucket, object,
|
||||
totalsize, acct, skipFalloc, p.forceNoTmpFile)
|
||||
totalsize, acct, skipFalloc, p.forceNoTmpFile, odirectNotAllowed)
|
||||
if err != nil {
|
||||
if errors.Is(err, syscall.EDQUOT) {
|
||||
return res, "", s3err.GetAPIError(s3err.ErrQuotaExceeded)
|
||||
@@ -2119,12 +2176,13 @@ func (p *Posix) CompleteMultipartUploadWithCopy(ctx context.Context, input *s3.C
|
||||
// Fail back to standard copy
|
||||
debuglogger.Logf("custom data block move failed (%q/%q): %v, failing back to io.Copy()",
|
||||
bucket, object, err)
|
||||
fw := f.File()
|
||||
fw.Seek(0, io.SeekEnd)
|
||||
_, _ = f.File().Seek(0, io.SeekEnd)
|
||||
if p.forceNoCopyFileRange {
|
||||
_, err = io.Copy(fw, &onlyRead{pf})
|
||||
_, err = io.Copy(f, &onlyRead{pf})
|
||||
} else {
|
||||
_, err = io.Copy(fw, pf)
|
||||
// Keep both endpoints as *os.File here so
|
||||
// io.Copy can use copy_file_range.
|
||||
_, err = io.Copy(f.File(), pf)
|
||||
}
|
||||
}
|
||||
if !idemp && err == nil {
|
||||
@@ -2145,8 +2203,10 @@ func (p *Posix) CompleteMultipartUploadWithCopy(ctx context.Context, input *s3.C
|
||||
}
|
||||
} else {
|
||||
if p.forceNoCopyFileRange {
|
||||
_, err = io.Copy(f.File(), &onlyRead{pf})
|
||||
_, err = io.Copy(f, &onlyRead{pf})
|
||||
} else {
|
||||
// Keep both endpoints as *os.File here so
|
||||
// io.Copy can use copy_file_range.
|
||||
_, err = io.Copy(f.File(), pf)
|
||||
}
|
||||
}
|
||||
@@ -3030,7 +3090,7 @@ func (p *Posix) UploadPartWithPostFunc(ctx context.Context, input *s3.UploadPart
|
||||
partPath := filepath.Join(mpPath, fmt.Sprintf("%v", *part))
|
||||
|
||||
f, err := p.openTmpFile(filepath.Join(bucket, objdir),
|
||||
bucket, partPath, length, acct, doFalloc, p.forceNoTmpFile)
|
||||
bucket, partPath, length, acct, doFalloc, p.forceNoTmpFile, odirectAllowed)
|
||||
if err != nil {
|
||||
if errors.Is(err, syscall.EDQUOT) {
|
||||
drainBody(r)
|
||||
@@ -3152,7 +3212,10 @@ func (p *Posix) UploadPartWithPostFunc(ctx context.Context, input *s3.UploadPart
|
||||
}
|
||||
}
|
||||
|
||||
_, err = io.Copy(f, tr)
|
||||
buf := p.getIOBuffer()
|
||||
defer p.putIOBuffer(buf)
|
||||
|
||||
_, err = io.CopyBuffer(f, tr, buf)
|
||||
if err != nil {
|
||||
if errors.Is(err, syscall.EDQUOT) {
|
||||
drainBody(tr)
|
||||
@@ -3415,7 +3478,7 @@ func (p *Posix) UploadPartCopy(ctx context.Context, upi *s3.UploadPartCopyInput)
|
||||
}
|
||||
|
||||
f, err := p.openTmpFile(filepath.Join(*upi.Bucket, objdir),
|
||||
*upi.Bucket, partPath, length, acct, doFalloc, p.forceNoTmpFile)
|
||||
*upi.Bucket, partPath, length, acct, doFalloc, p.forceNoTmpFile, odirectNotAllowed)
|
||||
if err != nil {
|
||||
if errors.Is(err, syscall.EDQUOT) {
|
||||
return s3response.CopyPartResult{}, s3err.GetAPIError(s3err.ErrQuotaExceeded)
|
||||
@@ -3803,7 +3866,7 @@ func (p *Posix) PutObjectWithPostFunc(ctx context.Context, po s3response.PutObje
|
||||
}
|
||||
|
||||
f, err := p.openTmpFile(filepath.Join(*po.Bucket, MetaTmpDir),
|
||||
*po.Bucket, *po.Key, contentLength, acct, doFalloc, p.forceNoTmpFile)
|
||||
*po.Bucket, *po.Key, contentLength, acct, doFalloc, p.forceNoTmpFile, odirectAllowed)
|
||||
if err != nil {
|
||||
if errors.Is(err, syscall.EDQUOT) {
|
||||
drainBody(po.Body)
|
||||
@@ -3832,7 +3895,10 @@ func (p *Posix) PutObjectWithPostFunc(ctx context.Context, po s3response.PutObje
|
||||
rdr = hashRdr
|
||||
}
|
||||
|
||||
_, err = io.Copy(f, rdr)
|
||||
buf := p.getIOBuffer()
|
||||
defer p.putIOBuffer(buf)
|
||||
|
||||
_, err = io.CopyBuffer(f, rdr, buf)
|
||||
if err != nil {
|
||||
if errors.Is(err, syscall.EDQUOT) {
|
||||
drainBody(rdr)
|
||||
@@ -4234,13 +4300,14 @@ func (p *Posix) DeleteObject(ctx context.Context, input *s3.DeleteObjectInput) (
|
||||
|
||||
f, err := p.openTmpFile(filepath.Join(bucket, MetaTmpDir),
|
||||
bucket, object, srcObjVersion.Size(), acct, doFalloc,
|
||||
p.forceNoTmpFile)
|
||||
p.forceNoTmpFile, odirectNotAllowed)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("open tmp file: %w", err)
|
||||
}
|
||||
defer f.cleanup()
|
||||
|
||||
_, err = io.Copy(f, sf)
|
||||
// Prioritize copy_file_range for internal file-to-file version restores.
|
||||
_, err = io.Copy(f.File(), sf)
|
||||
if err != nil {
|
||||
_ = sf.Close()
|
||||
return nil, fmt.Errorf("copy object %w", err)
|
||||
@@ -4678,7 +4745,7 @@ func (p *Posix) GetObject(ctx context.Context, input *s3.GetObjectInput) (*s3.Ge
|
||||
// openForRead opens with FILE_SHARE_DELETE on Windows so that a concurrent
|
||||
// DeleteObject can call os.Remove on this file while the GET response body
|
||||
// is still being streamed. On POSIX, os.Open is sufficient.
|
||||
f, err := openForRead(objPath)
|
||||
f, err := openForRead(objPath, p.enableODirect)
|
||||
if errors.Is(err, fs.ErrNotExist) {
|
||||
return nil, s3err.GetAPIError(s3err.ErrNoSuchKey)
|
||||
}
|
||||
@@ -4781,11 +4848,11 @@ func (p *Posix) GetObject(ctx context.Context, input *s3.GetObjectInput) (*s3.Ge
|
||||
}
|
||||
}
|
||||
|
||||
// using an os.File allows zero-copy sendfile via io.Copy(os.File, net.Conn)
|
||||
var body io.ReadCloser = f
|
||||
if startOffset != 0 || length != objSize {
|
||||
rdr := io.NewSectionReader(f, startOffset, length)
|
||||
body = &backend.FileSectionReadCloser{R: rdr, F: f}
|
||||
// Full-object responses can keep the underlying *os.File for sendfile.
|
||||
// Linux range reads on O_DIRECT may need runtime fallback to buffered I/O.
|
||||
body, err := buildGetObjectBody(f, objPath, startOffset, length, objSize, p.enableODirect, p.ioBufferSize)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("build get object body: %w", err)
|
||||
}
|
||||
|
||||
return &s3.GetObjectOutput{
|
||||
|
||||
@@ -0,0 +1,55 @@
|
||||
// Copyright 2026 Versity Software
|
||||
// This file is licensed under the Apache License, Version 2.0
|
||||
// (the "License"); you may not use this file except in compliance
|
||||
// with the License. You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing,
|
||||
// software distributed under the License is distributed on an
|
||||
// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||
// KIND, either express or implied. See the License for the
|
||||
// specific language governing permissions and limitations
|
||||
// under the License.
|
||||
|
||||
package posix
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"io"
|
||||
"log"
|
||||
"sync"
|
||||
)
|
||||
|
||||
func withReadBufferSize(r io.ReadCloser, size int) io.ReadCloser {
|
||||
if size <= 0 {
|
||||
return r
|
||||
}
|
||||
|
||||
return &bufferedReadCloser{
|
||||
r: bufio.NewReaderSize(r, size),
|
||||
c: r,
|
||||
}
|
||||
}
|
||||
|
||||
type bufferedReadCloser struct {
|
||||
r *bufio.Reader
|
||||
c io.Closer
|
||||
}
|
||||
|
||||
func (b *bufferedReadCloser) Read(p []byte) (int, error) {
|
||||
return b.r.Read(p)
|
||||
}
|
||||
|
||||
func (b *bufferedReadCloser) Close() error {
|
||||
return b.c.Close()
|
||||
}
|
||||
|
||||
var odirectUnsupportedWarnByOp sync.Map
|
||||
|
||||
func warnODirectUnsupportedOnce(op string, err error) {
|
||||
v, _ := odirectUnsupportedWarnByOp.LoadOrStore(op, &sync.Once{})
|
||||
v.(*sync.Once).Do(func() {
|
||||
log.Printf("WARNING: O_DIRECT is enabled but unsupported (%s: %v); falling back to buffered I/O. This warning is shown once per operation.", op, err)
|
||||
})
|
||||
}
|
||||
@@ -42,6 +42,8 @@ type tmpfile struct {
|
||||
bucket string
|
||||
objname string
|
||||
isOTmp bool
|
||||
procFDName string
|
||||
useODirect bool
|
||||
size int64
|
||||
doChown bool
|
||||
uid int
|
||||
@@ -54,11 +56,11 @@ var (
|
||||
defaultFilePerm uint32 = 0644
|
||||
)
|
||||
|
||||
func (p *Posix) openTmpFile(dir, bucket, obj string, size int64, acct auth.Account, dofalloc bool, forceNoTmpFile bool) (*tmpfile, error) {
|
||||
func (p *Posix) openTmpFile(dir, bucket, obj string, size int64, acct auth.Account, dofalloc bool, forceNoTmpFile bool, allowODirect odirectPolicy) (*tmpfile, error) {
|
||||
uid, gid, doChown := p.getChownIDs(acct)
|
||||
|
||||
if forceNoTmpFile {
|
||||
return p.openMkTemp(dir, bucket, obj, size, dofalloc, uid, gid, doChown)
|
||||
return p.openMkTemp(dir, bucket, obj, size, dofalloc, uid, gid, doChown, allowODirect)
|
||||
}
|
||||
|
||||
// O_TMPFILE allows for a file handle to an unnamed file in the filesystem.
|
||||
@@ -67,14 +69,34 @@ func (p *Posix) openTmpFile(dir, bucket, obj string, size int64, acct auth.Accou
|
||||
// file descriptor into the namespace.
|
||||
// Not all filesystems support this, so fallback to CreateTemp for when
|
||||
// this is not supported.
|
||||
fd, err := unix.Open(dir, unix.O_RDWR|unix.O_TMPFILE|unix.O_CLOEXEC, defaultFilePerm)
|
||||
openFlags := unix.O_RDWR | unix.O_TMPFILE | unix.O_CLOEXEC
|
||||
useODirect := false
|
||||
if p.enableODirect && bool(allowODirect) {
|
||||
openFlags |= unix.O_DIRECT
|
||||
useODirect = true
|
||||
}
|
||||
|
||||
fd, err := unix.Open(dir, openFlags, defaultFilePerm)
|
||||
if err != nil {
|
||||
if errors.Is(err, syscall.EROFS) {
|
||||
return nil, s3err.GetAPIError(s3err.ErrMethodNotAllowed)
|
||||
}
|
||||
|
||||
// O_TMPFILE not supported, try fallback
|
||||
return p.openMkTemp(dir, bucket, obj, size, dofalloc, uid, gid, doChown)
|
||||
if p.enableODirect && bool(allowODirect) && isODirectUnsupportedOpenErr(err) {
|
||||
warnODirectUnsupportedOnce("openTmpFile", err)
|
||||
|
||||
fd, err = unix.Open(dir, unix.O_RDWR|unix.O_TMPFILE|unix.O_CLOEXEC, defaultFilePerm)
|
||||
if err == nil {
|
||||
useODirect = false
|
||||
} else if errors.Is(err, syscall.EROFS) {
|
||||
return nil, s3err.GetAPIError(s3err.ErrMethodNotAllowed)
|
||||
}
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
// O_TMPFILE not supported, try fallback
|
||||
return p.openMkTemp(dir, bucket, obj, size, dofalloc, uid, gid, doChown, allowODirect)
|
||||
}
|
||||
}
|
||||
|
||||
// for O_TMPFILE, filename is /proc/self/fd/<fd> to be used
|
||||
@@ -86,6 +108,8 @@ func (p *Posix) openTmpFile(dir, bucket, obj string, size int64, acct auth.Accou
|
||||
bucket: bucket,
|
||||
objname: obj,
|
||||
isOTmp: true,
|
||||
procFDName: strconv.Itoa(fd),
|
||||
useODirect: useODirect,
|
||||
size: size,
|
||||
doChown: doChown,
|
||||
uid: uid,
|
||||
@@ -109,7 +133,7 @@ func (p *Posix) openTmpFile(dir, bucket, obj string, size int64, acct auth.Accou
|
||||
return tmp, nil
|
||||
}
|
||||
|
||||
func (p *Posix) openMkTemp(dir, bucket, obj string, size int64, dofalloc bool, uid, gid int, doChown bool) (*tmpfile, error) {
|
||||
func (p *Posix) openMkTemp(dir, bucket, obj string, size int64, dofalloc bool, uid, gid int, doChown bool, allowODirect odirectPolicy) (*tmpfile, error) {
|
||||
err := backend.MkdirAll(dir, uid, gid, doChown, p.newDirPerm)
|
||||
if err != nil {
|
||||
if errors.Is(err, syscall.EROFS) {
|
||||
@@ -125,14 +149,41 @@ func (p *Posix) openMkTemp(dir, bucket, obj string, size int64, dofalloc bool, u
|
||||
}
|
||||
return nil, err
|
||||
}
|
||||
|
||||
useODirect := false
|
||||
if p.enableODirect && bool(allowODirect) {
|
||||
name := f.Name()
|
||||
if err := f.Close(); err != nil {
|
||||
os.Remove(name)
|
||||
return nil, fmt.Errorf("close temp file before O_DIRECT reopen: %w", err)
|
||||
}
|
||||
|
||||
fd, err := unix.Open(name, unix.O_RDWR|unix.O_CLOEXEC|unix.O_DIRECT, defaultFilePerm)
|
||||
if err == nil {
|
||||
f = os.NewFile(uintptr(fd), name)
|
||||
useODirect = true
|
||||
} else if isODirectUnsupportedOpenErr(err) {
|
||||
warnODirectUnsupportedOnce("openMkTemp", err)
|
||||
f, err = os.OpenFile(name, os.O_RDWR, 0)
|
||||
if err != nil {
|
||||
os.Remove(name)
|
||||
return nil, fmt.Errorf("reopen temp file after O_DIRECT fallback: %w", err)
|
||||
}
|
||||
} else {
|
||||
os.Remove(name)
|
||||
return nil, fmt.Errorf("open temp file with O_DIRECT: %w", err)
|
||||
}
|
||||
}
|
||||
|
||||
tmp := &tmpfile{
|
||||
f: f,
|
||||
bucket: bucket,
|
||||
objname: obj,
|
||||
size: size,
|
||||
doChown: doChown,
|
||||
uid: uid,
|
||||
gid: gid,
|
||||
f: f,
|
||||
bucket: bucket,
|
||||
objname: obj,
|
||||
useODirect: useODirect,
|
||||
size: size,
|
||||
doChown: doChown,
|
||||
uid: uid,
|
||||
gid: gid,
|
||||
}
|
||||
// falloc is best effort, its fine if this fails
|
||||
if size > 0 && dofalloc {
|
||||
@@ -237,8 +288,12 @@ func (tmp *tmpfile) link() error {
|
||||
if err != nil {
|
||||
return fmt.Errorf("open parent dir: %w", err)
|
||||
}
|
||||
srcFDName := tmp.procFDName
|
||||
if srcFDName == "" {
|
||||
srcFDName = filepath.Base(tmp.f.Name())
|
||||
}
|
||||
err = linkatOTmpfile(int(procdir.Fd()), int(dirf.Fd()),
|
||||
filepath.Base(tmp.f.Name()), filepath.Base(objPath))
|
||||
srcFDName, filepath.Base(objPath))
|
||||
dirf.Close()
|
||||
if errors.Is(err, syscall.ENOENT) {
|
||||
// The directory was removed between open and linkat; backoff and retry.
|
||||
@@ -254,7 +309,7 @@ func (tmp *tmpfile) link() error {
|
||||
}
|
||||
if err != nil {
|
||||
return fmt.Errorf("link tmpfile (fd %q as %q): %w",
|
||||
filepath.Base(tmp.f.Name()), objPath, err)
|
||||
srcFDName, objPath, err)
|
||||
}
|
||||
break
|
||||
}
|
||||
|
||||
@@ -36,9 +36,13 @@ const (
|
||||
)
|
||||
|
||||
type tmpfile struct {
|
||||
f *os.File
|
||||
bucket string
|
||||
objname string
|
||||
f *os.File
|
||||
bucket string
|
||||
objname string
|
||||
// Retained for compatibility with shared tmpfile methods in otmpfile_common.
|
||||
isOTmp bool
|
||||
procFDName string
|
||||
useODirect bool
|
||||
size int64
|
||||
newDirPerm fs.FileMode
|
||||
uid int
|
||||
@@ -46,9 +50,13 @@ type tmpfile struct {
|
||||
doChown bool
|
||||
}
|
||||
|
||||
func (p *Posix) openTmpFile(dir, bucket, obj string, size int64, acct auth.Account, _ bool, _ bool) (*tmpfile, error) {
|
||||
func (p *Posix) openTmpFile(dir, bucket, obj string, size int64, acct auth.Account, _ bool, _ bool, allowODirect odirectPolicy) (*tmpfile, error) {
|
||||
uid, gid, doChown := p.getChownIDs(acct)
|
||||
|
||||
if p.enableODirect && bool(allowODirect) {
|
||||
warnODirectUnsupportedOnce("openTmpFile-nonlinux", os.ErrInvalid)
|
||||
}
|
||||
|
||||
// Create a temp file for upload while in progress (see link comments below).
|
||||
var err error
|
||||
err = backend.MkdirAll(dir, uid, gid, doChown, p.newDirPerm)
|
||||
@@ -80,6 +88,9 @@ func (p *Posix) openTmpFile(dir, bucket, obj string, size int64, acct auth.Accou
|
||||
f: f,
|
||||
bucket: bucket,
|
||||
objname: obj,
|
||||
isOTmp: false,
|
||||
procFDName: "",
|
||||
useODirect: false,
|
||||
size: size,
|
||||
newDirPerm: p.newDirPerm,
|
||||
uid: uid,
|
||||
|
||||
@@ -33,7 +33,9 @@ var (
|
||||
nometa bool
|
||||
forceNoTmpFile bool
|
||||
forceNoCopyFileRange bool
|
||||
enableODirect bool
|
||||
actionsConcurrency int
|
||||
ioBufferSize int
|
||||
defaultEtag string
|
||||
)
|
||||
|
||||
@@ -98,6 +100,13 @@ will be translated into the file /mnt/fs/gwroot/mybucket/a/b/c/myobject`,
|
||||
Value: 5000,
|
||||
Destination: &actionsConcurrency,
|
||||
},
|
||||
&cli.IntFlag{
|
||||
Name: "io-buffer-size",
|
||||
Usage: "buffer size in bytes used by POSIX put/get/part read and write paths (<=0 uses backend default 1MiB)",
|
||||
EnvVars: []string{"VGW_POSIX_IO_BUFFER_SIZE"},
|
||||
Value: 1024 * 1024,
|
||||
Destination: &ioBufferSize,
|
||||
},
|
||||
&cli.BoolFlag{
|
||||
Name: "nometa",
|
||||
Usage: "disable metadata storage",
|
||||
@@ -116,6 +125,12 @@ will be translated into the file /mnt/fs/gwroot/mybucket/a/b/c/myobject`,
|
||||
EnvVars: []string{"VGW_DISABLE_COPY_FILE_RANGE"},
|
||||
Destination: &forceNoCopyFileRange,
|
||||
},
|
||||
&cli.BoolFlag{
|
||||
Name: "enable-odirect",
|
||||
Usage: "enable best-effort O_DIRECT for object data reads/writes",
|
||||
EnvVars: []string{"VGW_ENABLE_O_DIRECT"},
|
||||
Destination: &enableODirect,
|
||||
},
|
||||
&cli.StringFlag{
|
||||
Name: "default-etag",
|
||||
Usage: "default ETag value returned for objects that do not have a stored etag attribute (e.g. files placed on the filesystem outside of versitygw)",
|
||||
@@ -153,8 +168,10 @@ func runPosix(ctx *cli.Context) error {
|
||||
NewDirPerm: fs.FileMode(dirPerms),
|
||||
ForceNoTmpFile: forceNoTmpFile,
|
||||
ForceNoCopyFileRange: forceNoCopyFileRange,
|
||||
EnableODirect: enableODirect,
|
||||
ValidateBucketNames: disableStrictBucketNames,
|
||||
Concurrency: actionsConcurrency,
|
||||
IOBufferSize: ioBufferSize,
|
||||
CopyObjectThreshold: copyObjectThreshold,
|
||||
DefaultEtag: defaultEtag,
|
||||
}
|
||||
|
||||
@@ -602,6 +602,12 @@ ROOT_SECRET_ACCESS_KEY=
|
||||
# memory use.
|
||||
#VGW_POSIX_CONCURRENCY=5000
|
||||
|
||||
# The VGW_POSIX_IO_BUFFER_SIZE option sets the data transfer buffer size used
|
||||
# by the posix backend for PutObject, UploadPart, and GetObject read/write
|
||||
# paths. The default is 1048576 bytes (1 MiB). When set to 0 or a negative
|
||||
# value, the backend falls back to the same default.
|
||||
#VGW_POSIX_IO_BUFFER_SIZE=1048576
|
||||
|
||||
# The gateway will use O_TMPFILE for writing objects while uploading and
|
||||
# link the file to the final object name when the upload is complete if the
|
||||
# filesystem supports O_TMPFILE. This creates an atomic object creation
|
||||
@@ -623,6 +629,15 @@ ROOT_SECRET_ACCESS_KEY=
|
||||
# NFS servers that may hang on this call.
|
||||
#VGW_DISABLE_COPY_FILE_RANGE=false
|
||||
|
||||
# The VGW_ENABLE_O_DIRECT option enables best-effort O_DIRECT for object data
|
||||
# reads and writes. This is disabled by default. When enabled, versitygw
|
||||
# attempts to open object data files with O_DIRECT and automatically falls back
|
||||
# to buffered I/O when O_DIRECT is not supported by the filesystem/open call.
|
||||
# Some systems may still enforce alignment constraints for direct I/O. The
|
||||
# VGW_POSIX_IO_BUFFER_SIZE setting can be used to tune buffered read/write
|
||||
# chunk sizing while keeping O_DIRECT best-effort behavior enabled.
|
||||
#VGW_ENABLE_O_DIRECT=false
|
||||
|
||||
# The VGW_DEFAULT_ETAG option sets the ETag value returned for objects that do
|
||||
# not have a stored etag attribute. This applies to files that were placed on
|
||||
# the filesystem outside of versitygw and therefore lack S3 metadata. Some S3
|
||||
|
||||
Reference in New Issue
Block a user