Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion cfg/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -1196,7 +1196,7 @@ func BuildFlagSet(flagSet *pflag.FlagSet) error {

flagSet.BoolP("foreground", "", false, "Stay in the foreground after mounting.")

flagSet.IntP("fuse-max-request-size-kb", "", StorageClassRapid.DefaultFuseMaxRequestSizeKb(), "Sets the target maximum request size in KiB that FUSE can process in a single request (currently used to control read requests only). This is translated to the kernel max_pages limit based on host page size. As max_pages_limit is a global, machine-level configuration across all mounts, the host's limit is only updated if the calculated pages value is greater than the current system limit. Note that the FUSE kernel max_pages limit can be set to at most 65535 (fuse_max_max_pages), so the value of this parameter must be > 0 and translate to at most 65535 pages. Additionally, on GKE, the system-wide setting is capped to 16 MiB (16384 KiB) by default by the CSI driver. If needed to be set beyond that on GKE, the user has to manually increase the value on the node before GCSFuse mounting begins.")
flagSet.IntP("fuse-max-request-size-kb", "", StorageClassRapid.DefaultFuseMaxRequestSizeKb(), "Sets the target maximum request size in KiB that FUSE can process in a single request (currently used to control read requests only). This parameter only takes effect when enable-kernel-reader is set to true. This is translated to the kernel max_pages limit based on host page size. As max_pages_limit is a global, machine-level configuration across all mounts, the host's limit is only updated if the calculated pages value is greater than the current system limit. Note that the FUSE kernel max_pages limit can be set to at most 65535 (fuse_max_max_pages), so the value of this parameter must be > 0 and translate to at most 65535 pages. Additionally, on GKE, the system-wide setting is capped to 16 MiB (16384 KiB) by default by the CSI driver. If needed to be set beyond that on GKE, the user has to manually increase the value on the node before GCSFuse mounting begins. Requires enable-kernel-reader to be set to true, otherwise the value will be ignored.")

if err := flagSet.MarkHidden("fuse-max-request-size-kb"); err != nil {
return err
Expand Down
5 changes: 3 additions & 2 deletions cfg/params.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -452,14 +452,15 @@ params:
type: "int"
usage: >-
Sets the target maximum request size in KiB that FUSE can process in a single request
(currently used to control read requests only). This is translated to the kernel max_pages
(currently used to control read requests only). This parameter only takes effect when
enable-kernel-reader is set to true. This is translated to the kernel max_pages
limit based on host page size. As max_pages_limit is a global, machine-level configuration
across all mounts, the host's limit is only updated if the calculated pages value is greater
than the current system limit. Note that the FUSE kernel max_pages limit can be set to at most
65535 (fuse_max_max_pages), so the value of this parameter must be > 0 and translate to at most 65535 pages.
Additionally, on GKE, the system-wide setting is capped to 16 MiB (16384 KiB) by default by the CSI driver.
If needed to be set beyond that on GKE, the user has to manually increase the value on the node before
GCSFuse mounting begins.
GCSFuse mounting begins. Requires enable-kernel-reader to be set to true, otherwise the value will be ignored.
default: "StorageClassRapid.DefaultFuseMaxRequestSizeKb()"
hide-flag: true
optimizations:
Expand Down
2 changes: 1 addition & 1 deletion cmd/mount.go
Original file line number Diff line number Diff line change
Expand Up @@ -202,7 +202,7 @@ func getFuseMountConfig(fsName string, newConfig *cfg.Config) *fuse.MountConfig
EnableAsyncReads: newConfig.FileSystem.EnableKernelReader,
}

if newConfig.FileSystem.FuseMaxRequestSizeKb > 0 {
if newConfig.FileSystem.EnableKernelReader && newConfig.FileSystem.FuseMaxRequestSizeKb > 0 {
mountCfg.MaxPages = uint16(cfg.MaxPagesForRequestSizeKb(int(newConfig.FileSystem.FuseMaxRequestSizeKb)))
mountCfg.MaxWrite = uint32(util.MiB)
}
Expand Down
50 changes: 50 additions & 0 deletions cmd/mount_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ import (
"testing"

"github.com/googlecloudplatform/gcsfuse/v3/cfg"
"github.com/googlecloudplatform/gcsfuse/v3/internal/util"
"github.com/stretchr/testify/assert"
)

Expand Down Expand Up @@ -148,3 +149,52 @@ func TestGetFuseMountConfig_EnableReaddirplus(t *testing.T) {
})
}
}

func TestGetFuseMountConfig_MaxPagesAndMaxWrite(t *testing.T) {
testCases := []struct {
name string
enableKernelReader bool
fuseMaxRequestSizeKb int64
expectedMaxPages uint16
expectedMaxWrite uint32
}{
{
name: "KernelReaderDisabled_MaxRequestSizeSet",
enableKernelReader: false,
fuseMaxRequestSizeKb: 16384,
expectedMaxPages: 0,
expectedMaxWrite: 0,
},
{
name: "KernelReaderEnabled_MaxRequestSizeZero",
enableKernelReader: true,
fuseMaxRequestSizeKb: 0,
expectedMaxPages: 0,
expectedMaxWrite: 0,
},
{
name: "KernelReaderEnabled_MaxRequestSizeSet",
enableKernelReader: true,
fuseMaxRequestSizeKb: 16384,
expectedMaxPages: uint16(cfg.MaxPagesForRequestSizeKb(16384)),
expectedMaxWrite: uint32(util.MiB),
},
}

fsName := "mybucket"
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
newConfig := &cfg.Config{
FileSystem: cfg.FileSystemConfig{
EnableKernelReader: tc.enableKernelReader,
FuseMaxRequestSizeKb: tc.fuseMaxRequestSizeKb,
},
}

fuseMountCfg := getFuseMountConfig(fsName, newConfig)

assert.Equal(t, tc.expectedMaxPages, fuseMountCfg.MaxPages)
assert.Equal(t, tc.expectedMaxWrite, fuseMountCfg.MaxWrite)
})
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,107 @@
// Copyright 2026 Google LLC
//
// 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 flag_optimizations

import (
"log"
"os"
"path"
"strconv"
"strings"
"testing"

"github.com/googlecloudplatform/gcsfuse/v3/tools/integration_tests/util/operations"
"github.com/googlecloudplatform/gcsfuse/v3/tools/integration_tests/util/setup"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"github.com/stretchr/testify/suite"
)

const (
sixteenMiB = 16 * operations.MiB
sixteenMiBInBytes = "16777216"
)

type MaxRequestSizeReadSuite struct {
suite.Suite
flags []string
}

func (s *MaxRequestSizeReadSuite) SetupSuite() {
mustMountGCSFuseAndSetupTestDir(s.flags, testEnv.ctx, testEnv.storageClient)
}

func (s *MaxRequestSizeReadSuite) TearDownSuite() {
tearDownOptimizationTest(s.T())
}

func (s *MaxRequestSizeReadSuite) TestKernelReaderLargeRead16MiB() {
// 1. Check if the host kernel supports elevating max_pages limit to what is needed for a 16 MiB read.
requiredPages := sixteenMiB / os.Getpagesize()
data, err := os.ReadFile("/proc/sys/fs/fuse/max_pages_limit")
if err != nil {
s.T().Skipf("Skipping TestKernelReaderLargeRead16MiB: kernel does not support elevating max_pages limit (/proc/sys/fs/fuse/max_pages_limit: %v)", err)
}
if limit, err := strconv.Atoi(strings.TrimSpace(string(data))); err != nil || limit < requiredPages {
s.T().Skipf("Skipping TestKernelReaderLargeRead16MiB: /proc/sys/fs/fuse/max_pages_limit (%s, err: %v) is less than required %d pages for 16 MiB read (page size %d bytes)", strings.TrimSpace(string(data)), err, requiredPages, os.Getpagesize())
}

// 2. Create 16 MiB test file in GCS.
testName := strings.ReplaceAll(s.T().Name(), "/", "_")
fileName := path.Join(testEnv.testDirPath, testName+"_16MiB.txt")
operations.CreateFileOfSize(sixteenMiB, fileName, s.T())
s.T().Cleanup(func() {
_ = os.Remove(fileName)
})

// 3. Truncate log file to record only the read operation.
err = os.Truncate(setup.LogFile(), 0)
require.NoError(s.T(), err, "Failed to truncate log file")

// 4. Perform a 16 MiB read.
content, err := os.ReadFile(fileName)
require.NoError(s.T(), err, "Failed to read 16 MiB file")
require.Len(s.T(), content, sixteenMiB)

// 5. Verify in gcsfuse trace log that exactly 1 read request of 16 MiB (16777216 bytes) occurred.
logContent, err := os.ReadFile(setup.LogFile())
require.NoError(s.T(), err, "Failed to read log file after read")

lines := strings.Split(string(logContent), "\n")
var readRequestCount int
for _, line := range lines {
if strings.Contains(line, readFileStartMsg) {
readRequestCount++
assert.Contains(s.T(), line, sixteenMiBInBytes, "Expected read request to be 16 MiB (%s bytes), but got line: %s", sixteenMiBInBytes, line)
}
}
assert.Equal(s.T(), 1, readRequestCount, "Expected exactly 1 read request of 16 MiB when kernel reader and fuse max pages support are enabled, got %d", readRequestCount)
}

func TestKernelReaderLargeReadSuite(t *testing.T) {
if setup.IsDynamicMount(testEnv.mountDir, testEnv.rootDir) {
t.Skip("Skipping test for dynamic mounting")
}
flagsSet := setup.BuildFlagSets(testEnv.cfg, testEnv.bucketType, t.Name())
for _, flags := range flagsSet {
t.Run(strings.Join(flags, "_"), func(t *testing.T) {
log.Printf("Running tests with flags: %s", flags)
s := &MaxRequestSizeReadSuite{
flags: flags,
}
suite.Run(t, s)
})
}
}
18 changes: 18 additions & 0 deletions tools/integration_tests/test_config.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -150,6 +150,15 @@ write_large_files:
- mounted_directory: "${MOUNTED_DIR}"
test_bucket: "${BUCKET_NAME}"
configs:
- run: TestWriteMaxRequestSize16MiB
flags:
- "--enable-kernel-reader=true,--fuse-max-request-size-kb=16384,--log-severity=trace,--client-protocol=http1"
- "--enable-kernel-reader=true,--fuse-max-request-size-kb=16384,--log-severity=trace,--client-protocol=grpc"
compatible:
flat: true
hns: true
zonal: false
run_on_gke: true
- flags:
- "--enable-streaming-writes=false,--client-protocol=http1"
- "--enable-streaming-writes=false,--client-protocol=grpc"
Expand Down Expand Up @@ -1355,6 +1364,15 @@ flag_optimizations:
- mounted_directory: "${MOUNTED_DIR}"
test_bucket: "${BUCKET_NAME}"
configs:
- run: TestKernelReaderLargeReadSuite
flags:
- "--enable-kernel-reader=true,--fuse-max-request-size-kb=16384,--log-severity=trace,--client-protocol=http1"
Comment thread
abhishek10004 marked this conversation as resolved.
- "--enable-kernel-reader=true,--fuse-max-request-size-kb=16384,--log-severity=trace,--client-protocol=grpc"
compatible:
flat: true
hns: true
zonal: false
run_on_gke: true
- run: TestMountFails
flags:
- "--profile=unknown-profile,--client-protocol=http1"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@ import (
"testing"
"time"

"github.com/googlecloudplatform/gcsfuse/v3/internal/cache/util"
"github.com/googlecloudplatform/gcsfuse/v3/internal/storage/gcs"
"github.com/googlecloudplatform/gcsfuse/v3/tools/integration_tests/util/setup"
"github.com/stretchr/testify/assert"
Expand Down Expand Up @@ -276,7 +277,11 @@ func WriteFilesSequentially(t *testing.T, filePaths []string, fileSize int64, ch
}

func ReadChunkFromFile(filePath string, chunkSize int64, offset int64, flag int) (chunk []byte, err error) {
chunk = make([]byte, chunkSize)
chunk, err = util.GetMemoryAlignedBuffer(chunkSize, int64(os.Getpagesize()))
if err != nil {
log.Printf("Error in allocating memory aligned buffer: %v", err)
return
}

file, err := os.OpenFile(filePath, flag, FilePermission_0600)
if err != nil {
Expand Down
12 changes: 10 additions & 2 deletions tools/integration_tests/util/operations/operations.go
Original file line number Diff line number Diff line change
Expand Up @@ -19,15 +19,23 @@ import (
"bytes"
"fmt"
"math/rand"
"os"
"os/exec"
"time"

"github.com/googlecloudplatform/gcsfuse/v3/internal/cache/util"
)

// GenerateRandomData generates random data that can be used to write to a file.
// The returned slice is memory-aligned to os.Getpagesize() so that it can be
// safely used for direct I/O (O_DIRECT) operations.
func GenerateRandomData(sizeInBytes int64) ([]byte, error) {
r := rand.New(rand.NewSource(time.Now().UnixNano()))
data := make([]byte, sizeInBytes)
_, err := r.Read(data)
data, err := util.GetMemoryAlignedBuffer(sizeInBytes, int64(os.Getpagesize()))
if err != nil {
return nil, fmt.Errorf("util.GetMemoryAlignedBuffer(%d, %d): %w", sizeInBytes, os.Getpagesize(), err)
}
_, err = r.Read(data)
if err != nil {
return nil, fmt.Errorf("r.Read(): %v", err)
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
// Copyright 2026 Google LLC
//
// 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 write_large_files

import (
"os"
"path"
"strings"
"syscall"
"testing"

"github.com/googlecloudplatform/gcsfuse/v3/tools/integration_tests/util/operations"
"github.com/googlecloudplatform/gcsfuse/v3/tools/integration_tests/util/setup"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)

const (
writeFileStartMsg = "<- WriteFile"
sixteenMiB = 16 * operations.MiB
oneMiBInBytes = "1048576"
)

func TestWriteMaxRequestSize16MiB(t *testing.T) {
logContent, err := os.ReadFile(setup.LogFile())
require.NoError(t, err, "Failed to read log file")
if !strings.Contains(string(logContent), "--fuse-max-request-size-kb=16384") {
t.Skip("Skipping TestWriteMaxRequestSize16MiB: --fuse-max-request-size-kb=16384 is not set in the mount config")
}

// Setup test directory and target file.
writeDir := setup.SetupTestDirectory("dirForMaxRequestSizeWrite")
filePath := path.Join(writeDir, "16MiB_write_test_"+setup.GenerateRandomString(5)+".txt")

// Truncate the log file right before writing so only our write operations are logged.
err = os.Truncate(setup.LogFile(), 0)
require.NoError(t, err, "Failed to truncate log file")

// Generate 16 MiB of random data (memory-aligned to os.Getpagesize() for O_DIRECT write).
data, err := operations.GenerateRandomData(sixteenMiB)
require.NoError(t, err, "Failed to generate random data")

// Open file with O_DIRECT so writes go straight to FUSE without kernel page-cache buffering.
f, err := os.OpenFile(filePath, os.O_WRONLY|os.O_CREATE|os.O_TRUNC|syscall.O_DIRECT, setup.FilePermission_0600)
require.NoError(t, err, "Failed to open file for direct write")

// Write 16 MiB in a single buffer call.
n, err := f.Write(data)
require.NoError(t, err, "Failed to write 16 MiB to file")
require.Equal(t, sixteenMiB, n)
Comment thread
abhishek10004 marked this conversation as resolved.

err = f.Close()
require.NoError(t, err, "Failed to close file")

// Read log content after write.
logContent, err = os.ReadFile(setup.LogFile())
require.NoError(t, err, "Failed to read log file after write")

lines := strings.Split(string(logContent), "\n")
var writeRequestCount int
for _, line := range lines {
if strings.Contains(line, writeFileStartMsg) {
writeRequestCount++
assert.Contains(t, line, oneMiBInBytes, "Expected write request to be 1 MiB (%s bytes), but got line: %s", oneMiBInBytes, line)
}
}
assert.Equal(t, 16, writeRequestCount, "Expected exactly 16 write requests of 1 MiB each for 16 MiB write, got %d", writeRequestCount)
}
Loading