Repository: fluid-cloudnative/fluid

Issue:
Add comments to CheckRuntimeReady in pkg/ddc/alluxio/load_data.go.
Which function do you want to add comments to?

CheckRuntimeReady in pkg/ddc/alluxio/load_data.go.

What comments do you want to add?

```go
// CheckRuntimeReady checks if the Alluxio runtime is operational.
// It obtains master pod details, creates file utilities, and checks readiness.
//
// Returns:
//   ready bool - Runtime readiness status (true = ready, false = not ready).`
```

Discussion:

Solution patch:
diff --git a/charts/fluid/fluid/Chart.yaml b/charts/fluid/fluid/Chart.yaml
index e1d83f67..32520175 100644
--- a/charts/fluid/fluid/Chart.yaml
+++ b/charts/fluid/fluid/Chart.yaml
@@ -18,7 +18,7 @@ version: 1.0.6
 
 # This is the version number of the application being deployed. This version number should be
 # incremented each time you make changes to the application.
-appVersion: 1.0.6-ef1d0d0
+appVersion: 1.0.6-df3d2bb
 home: https://github.com/fluid-cloudnative/fluid
 keywords:
   - category:data
diff --git a/charts/fluid/fluid/values.yaml b/charts/fluid/fluid/values.yaml
index 03b2d732..873582db 100644
--- a/charts/fluid/fluid/values.yaml
+++ b/charts/fluid/fluid/values.yaml
@@ -14,7 +14,7 @@ image:
 # Default registry, namespace and version tag for images managed by fluid
 imagePrefix: &defaultImagePrefix fluidcloudnative
 # imagePrefix: &defaultImagePrefix registry.aliyuncs.com/fluid
-version: &defaultVersion v1.0.6-ef1d0d0
+version: &defaultVersion v1.0.6-df3d2bb
 
 crdUpgrade:
   enabled: true
@@ -319,6 +319,7 @@ webhook:
           - NodeAffinityWithCache
           - MountPropagationInjector
           - DatasetUsageInjector
+          - FilePrefetcher
         withoutDataset:
           - PreferNodesWithoutCache
       # serverless webhook plugins
@@ -326,6 +327,7 @@ webhook:
         withDataset:
           - FuseSidecar
           - DatasetUsageInjector
+          - FilePrefetcher
         withoutDataset: []
     pluginConfig:
       - name: NodeAffinityWithCache
diff --git a/go.mod b/go.mod
index f0b47731..e3830b9e 100644
--- a/go.mod
+++ b/go.mod
@@ -1,6 +1,7 @@
 module github.com/fluid-cloudnative/fluid
 
 go 1.22.10
+toolchain go1.23.7
 
 replace k8s.io/api => k8s.io/api v0.29.5
 
@@ -73,7 +74,7 @@ require (
 	github.com/spf13/cobra v1.9.1
 	github.com/stretchr/testify v1.10.0
 	go.uber.org/zap v1.27.0
-	golang.org/x/net v0.35.0
+	golang.org/x/net v0.37.0
 	golang.org/x/time v0.10.0
 	google.golang.org/grpc v1.70.0
 	gopkg.in/yaml.v2 v2.4.0
@@ -136,9 +137,9 @@ require (
 	golang.org/x/arch v0.0.0-20190312162104-788fe5ffcd8c // indirect
 	golang.org/x/exp v0.0.0-20240719175910-8a7402abbf56 // indirect
 	golang.org/x/oauth2 v0.24.0 // indirect
-	golang.org/x/sys v0.30.0 // indirect
-	golang.org/x/term v0.29.0 // indirect
-	golang.org/x/text v0.22.0 // indirect
+	golang.org/x/sys v0.31.0 // indirect
+	golang.org/x/term v0.30.0 // indirect
+	golang.org/x/text v0.23.0 // indirect
 	gomodules.xyz/jsonpatch/v2 v2.4.0 // indirect
 	google.golang.org/genproto/googleapis/rpc v0.0.0-20241202173237-19429a94021a // indirect
 	google.golang.org/protobuf v1.36.1 // indirect
diff --git a/go.sum b/go.sum
index b840f1b5..cffc3659 100644
--- a/go.sum
+++ b/go.sum
@@ -1524,8 +1524,8 @@ golang.org/x/net v0.17.0/go.mod h1:NxSsAGuq816PNPmqtQdLE42eU2Fs7NoRIZrHJAlaCOE=
 golang.org/x/net v0.19.0/go.mod h1:CfAk/cbD4CthTvqiEl8NpboMuiuOYsAr/7NOjZJtv1U=
 golang.org/x/net v0.21.0/go.mod h1:bIjVDfnllIU7BJ2DNgfnXvpSvtn8VRwhlsaeUTyUS44=
 golang.org/x/net v0.23.0/go.mod h1:JKghWKKOSdJwpW2GEx0Ja7fmaKnMsbu+MWVZTokSYmg=
-golang.org/x/net v0.35.0 h1:T5GQRQb2y08kTAByq9L4/bz8cipCdA8FbRTXewonqY8=
-golang.org/x/net v0.35.0/go.mod h1:EglIi67kWsHKlRzzVMUD93VMSWGFOMSZgxFjparz1Qk=
+golang.org/x/net v0.37.0 h1:1zLorHbz+LYj7MQlSf1+2tPIIgibq2eL5xkrGk6f+2c=
+golang.org/x/net v0.37.0/go.mod h1:ivrbrMbzFq5J41QOQh0siUuly180yBYtLp+CKbEaFx8=
 golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
 golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
 golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
@@ -1682,8 +1682,8 @@ golang.org/x/sys v0.13.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
 golang.org/x/sys v0.15.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
 golang.org/x/sys v0.17.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
 golang.org/x/sys v0.18.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
-golang.org/x/sys v0.30.0 h1:QjkSwP/36a20jFYWkSue1YwXzLmsV5Gfq7Eiy72C1uc=
-golang.org/x/sys v0.30.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
+golang.org/x/sys v0.31.0 h1:ioabZlmFYtWhL+TRYpcnNlLwhyxaM9kWTDEmfnprqik=
+golang.org/x/sys v0.31.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k=
 golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
 golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
 golang.org/x/term v0.1.0/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
@@ -1702,8 +1702,8 @@ golang.org/x/term v0.13.0/go.mod h1:LTmsnFJwVN6bCy1rVCoS+qHT1HhALEFxKncY3WNNh4U=
 golang.org/x/term v0.15.0/go.mod h1:BDl952bC7+uMoWR75FIrCDx79TPU9oHkTZ9yRbYOrX0=
 golang.org/x/term v0.17.0/go.mod h1:lLRBjIVuehSbZlaOtGMbcMncT+aqLLLmKrsjNrUguwk=
 golang.org/x/term v0.18.0/go.mod h1:ILwASektA3OnRv7amZ1xhE/KTR+u50pbXfZ03+6Nx58=
-golang.org/x/term v0.29.0 h1:L6pJp37ocefwRRtYPKSWOWzOtWSxVajvz2ldH/xi3iU=
-golang.org/x/term v0.29.0/go.mod h1:6bl4lRlvVuDgSf3179VpIxBF0o10JUpXWOnI7nErv7s=
+golang.org/x/term v0.30.0 h1:PQ39fJZ+mfadBm0y5WlL4vlM7Sx1Hgf13sMIY2+QS9Y=
+golang.org/x/term v0.30.0/go.mod h1:NYYFdzHoI5wRh/h5tDMdMqCqPJZEuNqVR5xJLd/n67g=
 golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
 golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
 golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
@@ -1725,8 +1725,8 @@ golang.org/x/text v0.11.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE=
 golang.org/x/text v0.12.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE=
 golang.org/x/text v0.13.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE=
 golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
-golang.org/x/text v0.22.0 h1:bofq7m3/HAFvbF51jz3Q9wLg3jkvSPuiZu/pD1XwgtM=
-golang.org/x/text v0.22.0/go.mod h1:YRoo4H8PVmsu+E3Ou7cqLVH8oXWIHVoX0jqUWALQhfY=
+golang.org/x/text v0.23.0 h1:D71I7dUrlY+VX0gQShAThNGHFxZ13dGLBHQLVl1mJlY=
+golang.org/x/text v0.23.0/go.mod h1:/BLNzu4aZCJ1+kcD0DNRotWKage4q2rGVAg4o22unh4=
 golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
 golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
 golang.org/x/time v0.0.0-20191024005414-555d28b269f0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
diff --git a/pkg/ddc/alluxio/load_data.go b/pkg/ddc/alluxio/load_data.go
index 93cd24ab..1c6d775e 100644
--- a/pkg/ddc/alluxio/load_data.go
+++ b/pkg/ddc/alluxio/load_data.go
@@ -171,6 +171,11 @@ func (e *AlluxioEngine) genDataLoadValue(image string, targetDataset *datav1alph
 	return dataLoadValue, nil
 }
 
+// CheckRuntimeReady checks if the Alluxio runtime is operational.
+// It obtains master pod details, creates file utilities, and checks readiness.
+//
+// Returns:
+//   ready bool - Runtime readiness status (true = ready, false = not ready).
 func (e *AlluxioEngine) CheckRuntimeReady() (ready bool) {
 	podName, containerName := e.getMasterPodInfo()
 	fileUtils := operations.NewAlluxioFileUtils(podName, containerName, e.namespace, e.Log)
diff --git a/pkg/ddc/jindo/metadata.go b/pkg/ddc/jindo/metadata.go
index f572400e..5d297658 100644
--- a/pkg/ddc/jindo/metadata.go
+++ b/pkg/ddc/jindo/metadata.go
@@ -27,6 +27,13 @@ import (
 	"k8s.io/client-go/util/retry"
 )
 
+// SyncMetadata synchronizes metadata for the JindoEngine instance.
+// It first checks whether metadata synchronization is necessary using `shouldSyncMetadata`.
+// If synchronization is needed, it calls `syncMetadataInternal` to perform the update.
+// Execution time is tracked, and logs are recorded for debugging and monitoring purposes.
+//
+// Returns:
+//   - err: An error if metadata synchronization fails or if the check for synchronization necessity encounters an issue.
 func (e *JindoEngine) SyncMetadata() (err error) {
 	defer utils.TimeTrack(time.Now(), "JindoEngine.SyncMetadata", "name", e.name, "namespace", e.namespace)
 	defer e.Log.V(1).Info("End to sync metadata", "name", e.name, "namespace", e.namespace)
diff --git a/pkg/webhook/plugins/fileprefetcher/const.go b/pkg/webhook/plugins/fileprefetcher/const.go
new file mode 100644
index 00000000..8309167d
--- /dev/null
+++ b/pkg/webhook/plugins/fileprefetcher/const.go
@@ -0,0 +1,36 @@
+/*
+Copyright 2025 The Fluid Authors.
+
+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 fileprefetcher
+
+// Environment variables for file prefetcher
+const (
+	envKeyFilePrefetcherFileList       = "FILE_PREFETCHER_FILE_LIST"
+	envKeyFilePrefetcherAsyncPrefetch  = "FILE_PREFETCHER_ASYNC_PREFETCH"
+	envKeyFilePrefetcherTimeoutSeconds = "FILE_PREFETCHER_TIMEOUT_SECONDS"
+
+	envKeyFilePrefetcherImage = "FILE_PREFETCHER_IMAGE"
+)
+
+// Constants for file prefetcher
+const (
+	filePrefetcherContainerName         = "fluid-file-prefetcher"
+	filePrefetcherStatusVolumeName      = "fluid-file-prefetcher-status-vol"
+	filePrefetcherStatusVolumeMountPath = "/tmp/fluid-file-prefetcher/status"
+
+	filePrefetcherDefaultFileList          = "<ALL>"
+	filePrefetcherDefaultTimeoutSecondsStr = "120"
+)
diff --git a/pkg/webhook/plugins/fileprefetcher/file_prefetcher.go b/pkg/webhook/plugins/fileprefetcher/file_prefetcher.go
new file mode 100644
index 00000000..5da0c7fa
--- /dev/null
+++ b/pkg/webhook/plugins/fileprefetcher/file_prefetcher.go
@@ -0,0 +1,317 @@
+/*
+Copyright 2025 The Fluid Authors.
+
+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 fileprefetcher
+
+import (
+	"fmt"
+	stdlog "log"
+	"path"
+	"path/filepath"
+	"strconv"
+	"strings"
+
+	"github.com/fluid-cloudnative/fluid/pkg/common"
+	"github.com/fluid-cloudnative/fluid/pkg/ddc/base"
+	"github.com/fluid-cloudnative/fluid/pkg/utils/docker"
+	"github.com/fluid-cloudnative/fluid/pkg/webhook/plugins/api"
+	"github.com/go-logr/logr"
+	corev1 "k8s.io/api/core/v1"
+	ctrl "sigs.k8s.io/controller-runtime"
+	"sigs.k8s.io/controller-runtime/pkg/client"
+)
+
+const Name string = "FilePrefetcher"
+
+var defaultFilePrefetcherImage = ""
+
+func init() {
+	imageRepo := docker.GetImageRepoFromEnv(envKeyFilePrefetcherImage)
+	imageTag := docker.GetImageTagFromEnv(envKeyFilePrefetcherImage)
+	if len(imageRepo) == 0 || len(imageTag) == 0 {
+		stdlog.Printf("WARNING: env variable %s is not set, file prefetcher image is required in Pod's annotation", envKeyFilePrefetcherImage)
+		return
+	}
+	defaultFilePrefetcherImage = fmt.Sprintf("%s:%s", imageRepo, imageTag)
+	stdlog.Printf("Found %s value %s, using it as defaultFilePrefetcherImage", envKeyFilePrefetcherImage, defaultFilePrefetcherImage)
+}
+
+var _ api.MutatingHandler = &FilePrefetcher{}
+
+type FilePrefetcher struct {
+	client client.Client
+	name   string
+	log    logr.Logger
+}
+
+func NewPlugin(c client.Client, args string) (api.MutatingHandler, error) {
+	return &FilePrefetcher{
+		client: c,
+		name:   Name,
+		log:    ctrl.Log.WithName("FilePrefetcher"),
+	}, nil
+}
+
+func (p *FilePrefetcher) GetName() string {
+	return p.name
+}
+
+func (p *FilePrefetcher) Mutate(pod *corev1.Pod, runtimeInfos map[string]base.RuntimeInfoInterface) (shouldStop bool, err error) {
+	if !common.CheckExpectValue(pod.Annotations, AnnotationFilePrefetcherInject, common.True) {
+		return false, nil
+	}
+
+	if common.CheckExpectValue(pod.Annotations, AnnotationFilePrefetcherInjectDone, common.True) {
+		return false, nil
+	}
+
+	config, err := p.buildFilePrefetcherConfig(pod, runtimeInfos)
+	if err != nil {
+		p.log.Error(err, "failed to build file prefetcher config")
+		err = fmt.Errorf("failed to build file prefetcher config: %v", err)
+		return true, err
+	}
+
+	if len(config.GlobPaths) == 0 {
+		p.log.Info("Skipping injecting file prefetcher sidecar container because there's no valid file-list defined in annotation", "annotation", AnnotationFilePrefetcherFileList)
+		return false, nil
+	}
+
+	containerSpec, statusFileVolume := p.buildFilePrefetcherSidecarContainer(config)
+	if config.AsyncPrefetch {
+		statusVolumeMount := corev1.VolumeMount{
+			Name:      filePrefetcherStatusVolumeName,
+			MountPath: filePrefetcherStatusVolumeMountPath,
+		}
+		for idx, ctr := range pod.Spec.Containers {
+			if strings.HasPrefix(ctr.Name, common.FuseContainerName) {
+				// Skip injecting file prefetcher status volume into fluid's fuse sidecar containers.
+				continue
+			}
+			pod.Spec.Containers[idx].VolumeMounts = append(pod.Spec.Containers[idx].VolumeMounts, statusVolumeMount)
+		}
+	}
+
+	// Inject file prefetcher container right after fuse sidecar containers, we assume fluid's fuse sidecar container is injected together.
+	// e.g. before injection: [C1, FUSE1, FUSE2, C2, C3], after injection: [C1, FUSE1, FUSE2, FILEPREFETCHER, C2, C3]
+	pod.Spec.Containers = p.injectFilePrefetcherSidecar(pod.Spec.Containers, containerSpec)
+	pod.Spec.Volumes = append(pod.Spec.Volumes, statusFileVolume)
+	pod.Annotations[AnnotationFilePrefetcherInjectDone] = common.True
+
+	return false, nil
+}
+
+type filePrefetcherConfig struct {
+	// Image is the image of the file prefetcher sidecar container
+	Image string
+	// AsyncPrefetch indicates whether to use async prefetching, defaulting to false
+	AsyncPrefetch bool
+	// VolumeMountPaths is a map of volume name to mount path
+	VolumeMountPaths map[string]string
+	// GlobPaths is a string indicating all the paths to prefetch. It is a semicolon-separated list of paths
+	GlobPaths string
+	// TimeoutSeconds is a int indicating the timeout for file prefetcher, defined in seconds
+	TimeoutSeconds int
+	// ExtraEnvs is a map of extra envs to inject into the file prefetcher sidecar container
+	ExtraEnvs map[string]string
+}
+
+func (p *FilePrefetcher) buildFilePrefetcherConfig(pod *corev1.Pod, runtimeInfos map[string]base.RuntimeInfoInterface) (config filePrefetcherConfig, err error) {
+	defaultFn := func(keyValues map[string]string, key string, defaultValue string) (value string) {
+		if value, ok := keyValues[key]; ok {
+			return value
+		}
+		return defaultValue
+	}
+
+	config.Image = defaultFn(pod.Annotations, AnnotationFilePrefetcherImage, defaultFilePrefetcherImage)
+	if len(config.Image) == 0 {
+		err = fmt.Errorf("file prefetcher's image is required, set it in pod's annotation \"%s=<image>\"", AnnotationFilePrefetcherImage)
+		return
+	}
+
+	extraEnvs := map[string]string{}
+	// extraEnvsStr takes the format like: '<key1>=<value1> <key2>=<value2> <key3>=<value3>'
+	extraEnvsStr := defaultFn(pod.Annotations, AnnotationFilePrefetcherExtraEnvs, "")
+	if len(extraEnvsStr) > 0 {
+		keyValuePairs := strings.Split(extraEnvsStr, " ")
+		for _, keyValuePair := range keyValuePairs {
+			kvSlice := strings.Split(keyValuePair, "=")
+			if len(kvSlice) != 2 {
+				err = fmt.Errorf("file prefetcher's extra envs is required to be '<key1>=<value1> <key2>=<value2> <key3>=<value3>', but found unexpected key-value pair: %s", keyValuePair)
+				return
+			}
+			extraEnvs[kvSlice[0]] = kvSlice[1]
+		}
+	}
+	config.ExtraEnvs = extraEnvs
+
+	fileList := defaultFn(pod.Annotations, AnnotationFilePrefetcherFileList, filePrefetcherDefaultFileList)
+	if fileList == filePrefetcherDefaultFileList {
+		pvcNames := make([]string, 0)
+		for pvcName := range runtimeInfos {
+			pvcNames = append(pvcNames, fmt.Sprintf("pvc://%s", pvcName))
+		}
+		fileList = strings.Join(pvcNames, ";")
+	}
+	volumeMountPaths, globPaths := p.parseGlobPathsFromFileList(fileList, pod, runtimeInfos)
+	config.VolumeMountPaths = volumeMountPaths
+	config.GlobPaths = strings.Join(globPaths, ";")
+
+	asyncPrefetchStr := defaultFn(pod.Annotations, AnnotationFilePrefetcherAsync, "false")
+	if asyncPrefetch, parseErr := strconv.ParseBool(asyncPrefetchStr); parseErr != nil {
+		err = fmt.Errorf("invalid value for %s: %s, must either be false or true: %v", AnnotationFilePrefetcherAsync, asyncPrefetchStr, parseErr)
+		return
+	} else {
+		config.AsyncPrefetch = asyncPrefetch
+	}
+
+	timeoutSecondsStr := defaultFn(pod.Annotations, AnnotationFilePrefetcherTimeoutSeconds, filePrefetcherDefaultTimeoutSecondsStr)
+	if timeoutSeconds, parseErr := strconv.ParseInt(timeoutSecondsStr, 10, 32); parseErr != nil {
+		err = fmt.Errorf("invalid value for %s: %s, must be of type integer: %v", AnnotationFilePrefetcherTimeoutSeconds, timeoutSecondsStr, parseErr)
+		return
+	} else {
+		config.TimeoutSeconds = int(timeoutSeconds)
+	}
+	p.log.V(1).Info("building file prefetcher config", "config", config)
+
+	return
+}
+
+func (p *FilePrefetcher) parseGlobPathsFromFileList(fileList string, pod *corev1.Pod, runtimeInfos map[string]base.RuntimeInfoInterface) (volumeMountPaths map[string]string, globPaths []string) {
+	volumeMountPaths = map[string]string{}
+	globPaths = []string{}
+
+	if len(fileList) == 0 {
+		return
+	}
+
+	uriPaths := strings.Split(fileList, ";")
+	for _, uriPath := range uriPaths {
+		if !strings.HasPrefix(uriPath, string(common.VolumeScheme)) {
+			p.log.Info("skip adding path to prefetch list because it does not start with pvc://", "path", uriPath)
+			continue
+		}
+		// e.g. uriPath="pvc://mypvc/path/to/myfolder/*.pkl" => items=["mypvc", "path", "to", "myfolder", "*.pkl"]
+		items := strings.Split(strings.TrimPrefix(uriPath, string(common.VolumeScheme)), string(filepath.Separator))
+		if len(items) == 0 {
+			p.log.Info("skip adding path to prefetch list because it does not specify a valid persistentVolumeClaim", "path", uriPath)
+			continue
+		}
+
+		var pvcName, globPath string
+		if len(items) == 1 {
+			pvcName = items[0]
+			globPath = "**"
+		} else {
+			pvcName = items[0]
+			globPath = filepath.Clean(fmt.Sprintf("%c%s", filepath.Separator, filepath.Join(items[1:]...)))
+		}
+
+		if _, ok := runtimeInfos[pvcName]; !ok {
+			p.log.Info("skip adding path to prefetch list because the persistentVolumeClaim is not managed by Fluid", "path", uriPath)
+			continue
+		}
+
+		for _, volume := range pod.Spec.Volumes {
+			if volume.PersistentVolumeClaim != nil && volume.PersistentVolumeClaim.ClaimName == pvcName {
+				volumeMountPaths[volume.Name] = path.Join("/data", volume.Name)
+				globPaths = append(globPaths, path.Join(volumeMountPaths[volume.Name], globPath))
+			}
+		}
+	}
+
+	return
+}
+
+// This function assembles and returns a file prefetcher sidecar container
+func (p *FilePrefetcher) buildFilePrefetcherSidecarContainer(config filePrefetcherConfig) (corev1.Container, corev1.Volume) {
+	volumeMounts := []corev1.VolumeMount{}
+	for volumeName, mountPath := range config.VolumeMountPaths {
+		volumeMounts = append(volumeMounts, corev1.VolumeMount{
+			Name:      volumeName,
+			MountPath: mountPath,
+		})
+	}
+
+	statusFileVolume := corev1.Volume{
+		Name: filePrefetcherStatusVolumeName,
+		VolumeSource: corev1.VolumeSource{
+			EmptyDir: &corev1.EmptyDirVolumeSource{},
+		},
+	}
+	volumeMounts = append(volumeMounts, corev1.VolumeMount{
+		Name:      statusFileVolume.Name,
+		MountPath: filePrefetcherStatusVolumeMountPath,
+	})
+
+	containerSpec := corev1.Container{
+		Name:  filePrefetcherContainerName,
+		Image: config.Image,
+		Env: []corev1.EnvVar{
+			{
+				Name:  envKeyFilePrefetcherFileList,
+				Value: config.GlobPaths,
+			},
+			{
+				Name:  envKeyFilePrefetcherAsyncPrefetch,
+				Value: strconv.FormatBool(config.AsyncPrefetch),
+			},
+			{
+				Name:  envKeyFilePrefetcherTimeoutSeconds,
+				Value: strconv.FormatInt(int64(config.TimeoutSeconds), 10),
+			},
+		},
+		VolumeMounts: volumeMounts,
+	}
+
+	for k, v := range config.ExtraEnvs {
+		containerSpec.Env = append(containerSpec.Env, corev1.EnvVar{Name: k, Value: v})
+	}
+
+	if !config.AsyncPrefetch {
+		containerSpec.Lifecycle = &corev1.Lifecycle{
+			PostStart: &corev1.LifecycleHandler{
+				Exec: &corev1.ExecAction{
+					Command: []string{
+						"bash",
+						"-c",
+						`cnt=0; while [[ $cnt -lt $FILE_PREFETCHER_TIMEOUT_SECONDS ]]; do if [[ -e "/tmp/fluid-file-prefetcher/status/prefetcher.status" ]]; then exit 0; fi; cnt=$(expr $cnt + 1); sleep 1; done; echo "time out waiting for prefetching done"; exit 1`,
+					},
+				},
+			},
+		}
+	}
+
+	return containerSpec, statusFileVolume
+}
+
+func (p *FilePrefetcher) injectFilePrefetcherSidecar(oldContainers []corev1.Container, filePrefetcherCtr corev1.Container) (newContainers []corev1.Container) {
+	lastFuseSidecarIndex := -1
+	for idx, ctr := range oldContainers {
+		if strings.HasPrefix(ctr.Name, common.FuseContainerName) {
+			lastFuseSidecarIndex = idx
+		}
+	}
+
+	// Insert file prefetcher sidecar after
+	newContainers = make([]corev1.Container, 0, len(oldContainers)+1)
+	newContainers = append(newContainers, oldContainers[:lastFuseSidecarIndex+1]...)
+	newContainers = append(newContainers, filePrefetcherCtr)
+	newContainers = append(newContainers, oldContainers[lastFuseSidecarIndex+1:]...)
+
+	return newContainers
+}
diff --git a/pkg/webhook/plugins/fileprefetcher/labels.go b/pkg/webhook/plugins/fileprefetcher/labels.go
new file mode 100644
index 00000000..5e9fe52b
--- /dev/null
+++ b/pkg/webhook/plugins/fileprefetcher/labels.go
@@ -0,0 +1,54 @@
+/*
+Copyright 2025 The Fluid Authors.
+
+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 fileprefetcher
+
+import "github.com/fluid-cloudnative/fluid/pkg/common"
+
+const (
+	// prefix for labels and annotations used in file prefetcher feature
+	// i.e. file-prefetcher.fluid.io/
+	LabelAnnotationFilePrefetcherPrefix = "file-prefetcher." + common.LabelAnnotationPrefix
+
+	// annotation to inject file prefetcher sidecar container
+	// i.e. file-prefetcher.fluid.io/inject
+	AnnotationFilePrefetcherInject = LabelAnnotationFilePrefetcherPrefix + "inject"
+
+	// annotation to mark file prefetcher sidecar container has been injected
+	// i.e. file-prefetcher.fluid.io/inject-done
+	AnnotationFilePrefetcherInjectDone = LabelAnnotationFilePrefetcherPrefix + "inject-done"
+
+	// annotation to set file prefetcher sidecar container's image
+	// i.e. file-prefetcher.fluid.io/image
+	AnnotationFilePrefetcherImage = LabelAnnotationFilePrefetcherPrefix + "image"
+
+	// annotation to set file list to prefetch
+	// i.e. file-prefetcher.fluid.io/file-list
+	AnnotationFilePrefetcherFileList = LabelAnnotationFilePrefetcherPrefix + "file-list"
+
+	// annotation to set if prefetch files asynchronously. When setting it to true,
+	// app container will start up after file prefetcher finishes.
+	// i.e. file-prefetcher.fluid.io/async-prefetch
+	AnnotationFilePrefetcherAsync = LabelAnnotationFilePrefetcherPrefix + "async-prefetch"
+
+	// annotation to set extra envs for file prefetcher
+	// i.e. file-prefetcher.fluid.io/extra-envs
+	AnnotationFilePrefetcherExtraEnvs = LabelAnnotationFilePrefetcherPrefix + "extra-envs"
+
+	// annotation to set timeout for file prefetcher
+	// i.e. file-prefetcher.fluid.io/prefetch-timeout-seconds
+	AnnotationFilePrefetcherTimeoutSeconds = LabelAnnotationFilePrefetcherPrefix + "prefetch-timeout-seconds"
+)
diff --git a/pkg/webhook/plugins/plugins_impl.go b/pkg/webhook/plugins/plugins_impl.go
index 1a1aa0f0..b7ef312f 100644
--- a/pkg/webhook/plugins/plugins_impl.go
+++ b/pkg/webhook/plugins/plugins_impl.go
@@ -24,6 +24,7 @@ import (
 	"github.com/fluid-cloudnative/fluid/pkg/common"
 	"github.com/fluid-cloudnative/fluid/pkg/webhook/plugins/api"
 	"github.com/fluid-cloudnative/fluid/pkg/webhook/plugins/datasetusageinjector"
+	"github.com/fluid-cloudnative/fluid/pkg/webhook/plugins/fileprefetcher"
 	"github.com/fluid-cloudnative/fluid/pkg/webhook/plugins/fusesidecar"
 	"github.com/fluid-cloudnative/fluid/pkg/webhook/plugins/mountpropagationinjector"
 	"github.com/fluid-cloudnative/fluid/pkg/webhook/plugins/nodeaffinitywithcache"
@@ -50,6 +51,7 @@ func RegisterMutatingHandlers(client client.Client) error {
 	_ = registry.Register(nodeaffinitywithcache.Name, nodeaffinitywithcache.NewPlugin)
 	_ = registry.Register(fusesidecar.Name, fusesidecar.NewPlugin)
 	_ = registry.Register(datasetusageinjector.Name, datasetusageinjector.NewPlugin)
+	_ = registry.Register(fileprefetcher.Name, fileprefetcher.NewPlugin)
 
 	// get the handlers through the config file
 	data, err := os.ReadFile(common.WebhookPluginFilePath)
diff --git a/tools/file-prefetcher/Dockerfile b/tools/file-prefetcher/Dockerfile
new file mode 100644
index 00000000..4fc055de
--- /dev/null
+++ b/tools/file-prefetcher/Dockerfile
@@ -0,0 +1,13 @@
+FROM debian:bookworm-slim
+
+RUN apt update && apt install -y python3
+
+COPY ./main.py /root/main.py
+
+COPY ./entrypoint.sh /root/entrypoint.sh
+
+RUN chmod u+x /root/entrypoint.sh
+
+ENV PYTHONUNBUFFERED=1
+
+ENTRYPOINT ["/root/entrypoint.sh"]
\ No newline at end of file
diff --git a/tools/file-prefetcher/entrypoint.sh b/tools/file-prefetcher/entrypoint.sh
new file mode 100644
index 00000000..5f8d8f96
--- /dev/null
+++ b/tools/file-prefetcher/entrypoint.sh
@@ -0,0 +1,7 @@
+#!/bin/bash
+
+if [[ ! -e "/tmp/fluid-file-prefetcher/status/prefetcher.status" ]]; then
+    python3 /root/main.py
+fi
+
+exec sleep inf
\ No newline at end of file
diff --git a/tools/file-prefetcher/main.py b/tools/file-prefetcher/main.py
new file mode 100644
index 00000000..630a96e8
--- /dev/null
+++ b/tools/file-prefetcher/main.py
@@ -0,0 +1,48 @@
+import os
+import glob
+import time
+
+buffer_size_in_bytes = int(os.getenv("BUFFER_SIZE_IN_BYTES", "16777216"))# 16MiB
+
+def file_read(file):
+    print("")
+    buffer_size = buffer_size_in_bytes
+    with open(file, 'rb') as f:
+        while True:
+            buffer = f.read(buffer_size)
+            if not buffer:
+                break
+
+def main():
+    glob_patterns = os.getenv("FILE_PREFETCHER_FILE_LIST", None)
+    assert glob_patterns is not None, "env variable FILE_PREFETCHER_FILE_LIST is not set"
+
+     # Get all files to prefetch
+    files_to_prefetch = []
+    glob_patterns = glob_patterns.split(";")
+    for glob_pattern in glob_patterns:
+        if os.path.isdir(glob_pattern):
+            glob_pattern = os.path.join(glob_pattern, "**")
+        files = glob.glob(glob_pattern, recursive=True)
+        files_to_prefetch.extend([file for file in files if os.path.isfile(file)])
+
+    print(f"Found {len(files_to_prefetch)} files to prefetch")
+    start_time = time.time()
+    for file in files_to_prefetch:
+        file_start_time = time.time()
+        file_read(file)
+        print(f"Prefetching file {file} end in {time.time() - file_start_time:.2f} seconds")
+    print(f"Total time: {time.time() - start_time:.2f} seconds")
+
+if __name__ == '__main__':
+    prefetch_result = ""
+    try:
+        main()
+        prefetch_result = "success"
+    except Exception as e:
+        print(e)
+        prefetch_result = "fail"
+    finally:
+        os.makedirs("/tmp/fluid-file-prefetcher/status/", exist_ok=True)
+        with open("/tmp/fluid-file-prefetcher/status/prefetcher.status", "w") as f:
+            f.write(f"prefetch_result={prefetch_result}\n")
\ No newline at end of file
diff --git a/vendor/golang.org/x/net/context/context.go b/vendor/golang.org/x/net/context/context.go
index cf66309c..db1c95fa 100644
--- a/vendor/golang.org/x/net/context/context.go
+++ b/vendor/golang.org/x/net/context/context.go
@@ -3,29 +3,31 @@
 // license that can be found in the LICENSE file.
 
 // Package context defines the Context type, which carries deadlines,
-// cancelation signals, and other request-scoped values across API boundaries
+// cancellation signals, and other request-scoped values across API boundaries
 // and between processes.
 // As of Go 1.7 this package is available in the standard library under the
-// name context.  https://golang.org/pkg/context.
+// name [context], and migrating to it can be done automatically with [go fix].
 //
-// Incoming requests to a server should create a Context, and outgoing calls to
-// servers should accept a Context. The chain of function calls between must
-// propagate the Context, optionally replacing it with a modified copy created
-// using WithDeadline, WithTimeout, WithCancel, or WithValue.
+// Incoming requests to a server should create a [Context], and outgoing
+// calls to servers should accept a Context. The chain of function
+// calls between them must propagate the Context, optionally replacing
+// it with a derived Context created using [WithCancel], [WithDeadline],
+// [WithTimeout], or [WithValue].
 //
 // Programs that use Contexts should follow these rules to keep interfaces
 // consistent across packages and enable static analysis tools to check context
 // propagation:
 //
 // Do not store Contexts inside a struct type; instead, pass a Context
-// explicitly to each function that needs it. The Context should be the first
+// explicitly to each function that needs it. This is discussed further in
+// https://go.dev/blog/context-and-structs. The Context should be the first
 // parameter, typically named ctx:
 //
 //	func DoSomething(ctx context.Context, arg Arg) error {
 //		// ... use ctx ...
 //	}
 //
-// Do not pass a nil Context, even if a function permits it. Pass context.TODO
+// Do not pass a nil [Context], even if a function permits it. Pass [context.TODO]
 // if you are unsure about which Context to use.
 //
 // Use context Values only for request-scoped data that transits processes and
@@ -34,9 +36,30 @@
 // The same Context may be passed to functions running in different goroutines;
 // Contexts are safe for simultaneous use by multiple goroutines.
 //
-// See http://blog.golang.org/context for example code for a server that uses
+// See https://go.dev/blog/context for example code for a server that uses
 // Contexts.
-package context // import "golang.org/x/net/context"
+//
+// [go fix]: https://go.dev/cmd/go#hdr-Update_packages_to_use_new_APIs
+package context
+
+import (
+	"context" // standard library's context, as of Go 1.7
+	"time"
+)
+
+// A Context carries a deadline, a cancellation signal, and other values across
+// API boundaries.
+//
+// Context's methods may be called by multiple goroutines simultaneously.
+type Context = context.Context
+
+// Canceled is the error returned by [Context.Err] when the context is canceled
+// for some reason other than its deadline passing.
+var Canceled = context.Canceled
+
+// DeadlineExceeded is the error returned by [Context.Err] when the context is canceled
+// due to its deadline passing.
+var DeadlineExceeded = context.DeadlineExceeded
 
 // Background returns a non-nil, empty Context. It is never canceled, has no
 // values, and has no deadline. It is typically used by the main function,
@@ -49,8 +72,73 @@ func Background() Context {
 // TODO returns a non-nil, empty Context. Code should use context.TODO when
 // it's unclear which Context to use or it is not yet available (because the
 // surrounding function has not yet been extended to accept a Context
-// parameter).  TODO is recognized by static analysis tools that determine
-// whether Contexts are propagated correctly in a program.
+// parameter).
 func TODO() Context {
 	return todo
 }
+
+var (
+	background = context.Background()
+	todo       = context.TODO()
+)
+
+// A CancelFunc tells an operation to abandon its work.
+// A CancelFunc does not wait for the work to stop.
+// A CancelFunc may be called by multiple goroutines simultaneously.
+// After the first call, subsequent calls to a CancelFunc do nothing.
+type CancelFunc = context.CancelFunc
+
+// WithCancel returns a derived context that points to the parent context
+// but has a new Done channel. The returned context's Done channel is closed
+// when the returned cancel function is called or when the parent context's
+// Done channel is closed, whichever happens first.
+//
+// Canceling this context releases resources associated with it, so code should
+// call cancel as soon as the operations running in this [Context] complete.
+func WithCancel(parent Context) (ctx Context, cancel CancelFunc) {
+	return context.WithCancel(parent)
+}
+
+// WithDeadline returns a derived context that points to the parent context
+// but has the deadline adjusted to be no later than d. If the parent's
+// deadline is already earlier than d, WithDeadline(parent, d) is semantically
+// equivalent to parent. The returned [Context.Done] channel is closed when
+// the deadline expires, when the returned cancel function is called,
+// or when the parent context's Done channel is closed, whichever happens first.
+//
+// Canceling this context releases resources associated with it, so code should
+// call cancel as soon as the operations running in this [Context] complete.
+func WithDeadline(parent Context, d time.Time) (Context, CancelFunc) {
+	return context.WithDeadline(parent, d)
+}
+
+// WithTimeout returns WithDeadline(parent, time.Now().Add(timeout)).
+//
+// Canceling this context releases resources associated with it, so code should
+// call cancel as soon as the operations running in this [Context] complete:
+//
+//	func slowOperationWithTimeout(ctx context.Context) (Result, error) {
+//		ctx, cancel := context.WithTimeout(ctx, 100*time.Millisecond)
+//		defer cancel()  // releases resources if slowOperation completes before timeout elapses
+//		return slowOperation(ctx)
+//	}
+func WithTimeout(parent Context, timeout time.Duration) (Context, CancelFunc) {
+	return context.WithTimeout(parent, timeout)
+}
+
+// WithValue returns a derived context that points to the parent Context.
+// In the derived context, the value associated with key is val.
+//
+// Use context Values only for request-scoped data that transits processes and
+// APIs, not for passing optional parameters to functions.
+//
+// The provided key must be comparable and should not be of type
+// string or any other built-in type to avoid collisions between
+// packages using context. Users of WithValue should define their own
+// types for keys. To avoid allocating when assigning to an
+// interface{}, context keys often have concrete type
+// struct{}. Alternatively, exported context key variables' static
+// type should be a pointer or interface.
+func WithValue(parent Context, key, val interface{}) Context {
+	return context.WithValue(parent, key, val)
+}
diff --git a/vendor/golang.org/x/net/context/go17.go b/vendor/golang.org/x/net/context/go17.go
deleted file mode 100644
index 0c1b8679..00000000
--- a/vendor/golang.org/x/net/context/go17.go
+++ /dev/null
@@ -1,72 +0,0 @@
-// Copyright 2016 The Go Authors. All rights reserved.
-// Use of this source code is governed by a BSD-style
-// license that can be found in the LICENSE file.
-
-//go:build go1.7
-
-package context
-
-import (
-	"context" // standard library's context, as of Go 1.7
-	"time"
-)
-
-var (
-	todo       = context.TODO()
-	background = context.Background()
-)
-
-// Canceled is the error returned by Context.Err when the context is canceled.
-var Canceled = context.Canceled
-
-// DeadlineExceeded is the error returned by Context.Err when the context's
-// deadline passes.
-var DeadlineExceeded = context.DeadlineExceeded
-
-// WithCancel returns a copy of parent with a new Done channel. The returned
-// context's Done channel is closed when the returned cancel function is called
-// or when the parent context's Done channel is closed, whichever happens first.
-//
-// Canceling this context releases resources associated with it, so code should
-// call cancel as soon as the operations running in this Context complete.
-func WithCancel(parent Context) (ctx Context, cancel CancelFunc) {
-	ctx, f := context.WithCancel(parent)
-	return ctx, f
-}
-
-// WithDeadline returns a copy of the parent context with the deadline adjusted
-// to be no later than d. If the parent's deadline is already earlier than d,
-// WithDeadline(parent, d) is semantically equivalent to parent. The returned
-// context's Done channel is closed when the deadline expires, when the returned
-// cancel function is called, or when the parent context's Done channel is
-// closed, whichever happens first.
-//
-// Canceling this context releases resources associated with it, so code should
-// call cancel as soon as the operations running in this Context complete.
-func WithDeadline(parent Context, deadline time.Time) (Context, CancelFunc) {
-	ctx, f := context.WithDeadline(parent, deadline)
-	return ctx, f
-}
-
-// WithTimeout returns WithDeadline(parent, time.Now().Add(timeout)).
-//
-// Canceling this context releases resources associated with it, so code should
-// call cancel as soon as the operations running in this Context complete:
-//
-//	func slowOperationWithTimeout(ctx context.Context) (Result, error) {
-//		ctx, cancel := context.WithTimeout(ctx, 100*time.Millisecond)
-//		defer cancel()  // releases resources if slowOperation completes before timeout elapses
-//		return slowOperation(ctx)
-//	}
-func WithTimeout(parent Context, timeout time.Duration) (Context, CancelFunc) {
-	return WithDeadline(parent, time.Now().Add(timeout))
-}
-
-// WithValue returns a copy of parent in which the value associated with key is
-// val.
-//
-// Use context Values only for request-scoped data that transits processes and
-// APIs, not for passing optional parameters to functions.
-func WithValue(parent Context, key interface{}, val interface{}) Context {
-	return context.WithValue(parent, key, val)
-}
diff --git a/vendor/golang.org/x/net/context/go19.go b/vendor/golang.org/x/net/context/go19.go
deleted file mode 100644
index e31e35a9..00000000
--- a/vendor/golang.org/x/net/context/go19.go
+++ /dev/null
@@ -1,20 +0,0 @@
-// Copyright 2017 The Go Authors. All rights reserved.
-// Use of this source code is governed by a BSD-style
-// license that can be found in the LICENSE file.
-
-//go:build go1.9
-
-package context
-
-import "context" // standard library's context, as of Go 1.7
-
-// A Context carries a deadline, a cancelation signal, and other values across
-// API boundaries.
-//
-// Context's methods may be called by multiple goroutines simultaneously.
-type Context = context.Context
-
-// A CancelFunc tells an operation to abandon its work.
-// A CancelFunc does not wait for the work to stop.
-// After the first call, subsequent calls to a CancelFunc do nothing.
-type CancelFunc = context.CancelFunc
diff --git a/vendor/golang.org/x/net/context/pre_go17.go b/vendor/golang.org/x/net/context/pre_go17.go
deleted file mode 100644
index 065ff3df..00000000
--- a/vendor/golang.org/x/net/context/pre_go17.go
+++ /dev/null
@@ -1,300 +0,0 @@
-// Copyright 2014 The Go Authors. All rights reserved.
-// Use of this source code is governed by a BSD-style
-// license that can be found in the LICENSE file.
-
-//go:build !go1.7
-
-package context
-
-import (
-	"errors"
-	"fmt"
-	"sync"
-	"time"
-)
-
-// An emptyCtx is never canceled, has no values, and has no deadline. It is not
-// struct{}, since vars of this type must have distinct addresses.
-type emptyCtx int
-
-func (*emptyCtx) Deadline() (deadline time.Time, ok bool) {
-	return
-}
-
-func (*emptyCtx) Done() <-chan struct{} {
-	return nil
-}
-
-func (*emptyCtx) Err() error {
-	return nil
-}
-
-func (*emptyCtx) Value(key interface{}) interface{} {
-	return nil
-}
-
-func (e *emptyCtx) String() string {
-	switch e {
-	case background:
-		return "context.Background"
-	case todo:
-		return "context.TODO"
-	}
-	return "unknown empty Context"
-}
-
-var (
-	background = new(emptyCtx)
-	todo       = new(emptyCtx)
-)
-
-// Canceled is the error returned by Context.Err when the context is canceled.
-var Canceled = errors.New("context canceled")
-
-// DeadlineExceeded is the error returned by Context.Err when the context's
-// deadline passes.
-var DeadlineExceeded = errors.New("context deadline exceeded")
-
-// WithCancel returns a copy of parent with a new Done channel. The returned
-// context's Done channel is closed when the returned cancel function is called
-// or when the parent context's Done channel is closed, whichever happens first.
-//
-// Canceling this context releases resources associated with it, so code should
-// call cancel as soon as the operations running in this Context complete.
-func WithCancel(parent Context) (ctx Context, cancel CancelFunc) {
-	c := newCancelCtx(parent)
-	propagateCancel(parent, c)
-	return c, func() { c.cancel(true, Canceled) }
-}
-
-// newCancelCtx returns an initialized cancelCtx.
-func newCancelCtx(parent Context) *cancelCtx {
-	return &cancelCtx{
-		Context: parent,
-		done:    make(chan struct{}),
-	}
-}
-
-// propagateCancel arranges for child to be canceled when parent is.
-func propagateCancel(parent Context, child canceler) {
-	if parent.Done() == nil {
-		return // parent is never canceled
-	}
-	if p, ok := parentCancelCtx(parent); ok {
-		p.mu.Lock()
-		if p.err != nil {
-			// parent has already been canceled
-			child.cancel(false, p.err)
-		} else {
-			if p.children == nil {
-				p.children = make(map[canceler]bool)
-			}
-			p.children[child] = true
-		}
-		p.mu.Unlock()
-	} else {
-		go func() {
-			select {
-			case <-parent.Done():
-				child.cancel(false, parent.Err())
-			case <-child.Done():
-			}
-		}()
-	}
-}
-
-// parentCancelCtx follows a chain of parent references until it finds a
-// *cancelCtx. This function understands how each of the concrete types in this
-// package represents its parent.
-func parentCancelCtx(parent Context) (*cancelCtx, bool) {
-	for {
-		switch c := parent.(type) {
-		case *cancelCtx:
-			return c, true
-		case *timerCtx:
-			return c.cancelCtx, true
-		case *valueCtx:
-			parent = c.Context
-		default:
-			return nil, false
-		}
-	}
-}
-
-// removeChild removes a context from its parent.
-func removeChild(parent Context, child canceler) {
-	p, ok := parentCancelCtx(parent)
-	if !ok {
-		return
-	}
-	p.mu.Lock()
-	if p.children != nil {
-		delete(p.children, child)
-	}
-	p.mu.Unlock()
-}
-
-// A canceler is a context type that can be canceled directly. The
-// implementations are *cancelCtx and *timerCtx.
-type canceler interface {
-	cancel(removeFromParent bool, err error)
-	Done() <-chan struct{}
-}
-
-// A cancelCtx can be canceled. When canceled, it also cancels any children
-// that implement canceler.
-type cancelCtx struct {
-	Context
-
-	done chan struct{} // closed by the first cancel call.
-
-	mu       sync.Mutex
-	children map[canceler]bool // set to nil by the first cancel call
-	err      error             // set to non-nil by the first cancel call
-}
-
-func (c *cancelCtx) Done() <-chan struct{} {
-	return c.done
-}
-
-func (c *cancelCtx) Err() error {
-	c.mu.Lock()
-	defer c.mu.Unlock()
-	return c.err
-}
-
-func (c *cancelCtx) String() string {
-	return fmt.Sprintf("%v.WithCancel", c.Context)
-}
-
-// cancel closes c.done, cancels each of c's children, and, if
-// removeFromParent is true, removes c from its parent's children.
-func (c *cancelCtx) cancel(removeFromParent bool, err error) {
-	if err == nil {
-		panic("context: internal error: missing cancel error")
-	}
-	c.mu.Lock()
-	if c.err != nil {
-		c.mu.Unlock()
-		return // already canceled
-	}
-	c.err = err
-	close(c.done)
-	for child := range c.children {
-		// NOTE: acquiring the child's lock while holding parent's lock.
-		child.cancel(false, err)
-	}
-	c.children = nil
-	c.mu.Unlock()
-
-	if removeFromParent {
-		removeChild(c.Context, c)
-	}
-}
-
-// WithDeadline returns a copy of the parent context with the deadline adjusted
-// to be no later than d. If the parent's deadline is already earlier than d,
-// WithDeadline(parent, d) is semantically equivalent to parent. The returned
-// context's Done channel is closed when the deadline expires, when the returned
-// cancel function is called, or when the parent context's Done channel is
-// closed, whichever happens first.
-//
-// Canceling this context releases resources associated with it, so code should
-// call cancel as soon as the operations running in this Context complete.
-func WithDeadline(parent Context, deadline time.Time) (Context, CancelFunc) {
-	if cur, ok := parent.Deadline(); ok && cur.Before(deadline) {
-		// The current deadline is already sooner than the new one.
-		return WithCancel(parent)
-	}
-	c := &timerCtx{
-		cancelCtx: newCancelCtx(parent),
-		deadline:  deadline,
-	}
-	propagateCancel(parent, c)
-	d := deadline.Sub(time.Now())
-	if d <= 0 {
-		c.cancel(true, DeadlineExceeded) // deadline has already passed
-		return c, func() { c.cancel(true, Canceled) }
-	}
-	c.mu.Lock()
-	defer c.mu.Unlock()
-	if c.err == nil {
-		c.timer = time.AfterFunc(d, func() {
-			c.cancel(true, DeadlineExceeded)
-		})
-	}
-	return c, func() { c.cancel(true, Canceled) }
-}
-
-// A timerCtx carries a timer and a deadline. It embeds a cancelCtx to
-// implement Done and Err. It implements cancel by stopping its timer then
-// delegating to cancelCtx.cancel.
-type timerCtx struct {
-	*cancelCtx
-	timer *time.Timer // Under cancelCtx.mu.
-
-	deadline time.Time
-}
-
-func (c *timerCtx) Deadline() (deadline time.Time, ok bool) {
-	return c.deadline, true
-}
-
-func (c *timerCtx) String() string {
-	return fmt.Sprintf("%v.WithDeadline(%s [%s])", c.cancelCtx.Context, c.deadline, c.deadline.Sub(time.Now()))
-}
-
-func (c *timerCtx) cancel(removeFromParent bool, err error) {
-	c.cancelCtx.cancel(false, err)
-	if removeFromParent {
-		// Remove this timerCtx from its parent cancelCtx's children.
-		removeChild(c.cancelCtx.Context, c)
-	}
-	c.mu.Lock()
-	if c.timer != nil {
-		c.timer.Stop()
-		c.timer = nil
-	}
-	c.mu.Unlock()
-}
-
-// WithTimeout returns WithDeadline(parent, time.Now().Add(timeout)).
-//
-// Canceling this context releases resources associated with it, so code should
-// call cancel as soon as the operations running in this Context complete:
-//
-//	func slowOperationWithTimeout(ctx context.Context) (Result, error) {
-//		ctx, cancel := context.WithTimeout(ctx, 100*time.Millisecond)
-//		defer cancel()  // releases resources if slowOperation completes before timeout elapses
-//		return slowOperation(ctx)
-//	}
-func WithTimeout(parent Context, timeout time.Duration) (Context, CancelFunc) {
-	return WithDeadline(parent, time.Now().Add(timeout))
-}
-
-// WithValue returns a copy of parent in which the value associated with key is
-// val.
-//
-// Use context Values only for request-scoped data that transits processes and
-// APIs, not for passing optional parameters to functions.
-func WithValue(parent Context, key interface{}, val interface{}) Context {
-	return &valueCtx{parent, key, val}
-}
-
-// A valueCtx carries a key-value pair. It implements Value for that key and
-// delegates all other calls to the embedded Context.
-type valueCtx struct {
-	Context
-	key, val interface{}
-}
-
-func (c *valueCtx) String() string {
-	return fmt.Sprintf("%v.WithValue(%#v, %#v)", c.Context, c.key, c.val)
-}
-
-func (c *valueCtx) Value(key interface{}) interface{} {
-	if c.key == key {
-		return c.val
-	}
-	return c.Context.Value(key)
-}
diff --git a/vendor/golang.org/x/net/context/pre_go19.go b/vendor/golang.org/x/net/context/pre_go19.go
deleted file mode 100644
index ec5a6380..00000000
--- a/vendor/golang.org/x/net/context/pre_go19.go
+++ /dev/null
@@ -1,109 +0,0 @@
-// Copyright 2014 The Go Authors. All rights reserved.
-// Use of this source code is governed by a BSD-style
-// license that can be found in the LICENSE file.
-
-//go:build !go1.9
-
-package context
-
-import "time"
-
-// A Context carries a deadline, a cancelation signal, and other values across
-// API boundaries.
-//
-// Context's methods may be called by multiple goroutines simultaneously.
-type Context interface {
-	// Deadline returns the time when work done on behalf of this context
-	// should be canceled. Deadline returns ok==false when no deadline is
-	// set. Successive calls to Deadline return the same results.
-	Deadline() (deadline time.Time, ok bool)
-
-	// Done returns a channel that's closed when work done on behalf of this
-	// context should be canceled. Done may return nil if this context can
-	// never be canceled. Successive calls to Done return the same value.
-	//
-	// WithCancel arranges for Done to be closed when cancel is called;
-	// WithDeadline arranges for Done to be closed when the deadline
-	// expires; WithTimeout arranges for Done to be closed when the timeout
-	// elapses.
-	//
-	// Done is provided for use in select statements:
-	//
-	//  // Stream generates values with DoSomething and sends them to out
-	//  // until DoSomething returns an error or ctx.Done is closed.
-	//  func Stream(ctx context.Context, out chan<- Value) error {
-	//  	for {
-	//  		v, err := DoSomething(ctx)
-	//  		if err != nil {
-	//  			return err
-	//  		}
-	//  		select {
-	//  		case <-ctx.Done():
-	//  			return ctx.Err()
-	//  		case out <- v:
-	//  		}
-	//  	}
-	//  }
-	//
-	// See http://blog.golang.org/pipelines for more examples of how to use
-	// a Done channel for cancelation.
-	Done() <-chan struct{}
-
-	// Err returns a non-nil error value after Done is closed. Err returns
-	// Canceled if the context was canceled or DeadlineExceeded if the
-	// context's deadline passed. No other values for Err are defined.
-	// After Done is closed, successive calls to Err return the same value.
-	Err() error
-
-	// Value returns the value associated with this context for key, or nil
-	// if no value is associated with key. Successive calls to Value with
-	// the same key returns the same result.
-	//
-	// Use context values only for request-scoped data that transits
-	// processes and API boundaries, not for passing optional parameters to
-	// functions.
-	//
-	// A key identifies a specific value in a Context. Functions that wish
-	// to store values in Context typically allocate a key in a global
-	// variable then use that key as the argument to context.WithValue and
-	// Context.Value. A key can be any type that supports equality;
-	// packages should define keys as an unexported type to avoid
-	// collisions.
-	//
-	// Packages that define a Context key should provide type-safe accessors
-	// for the values stores using that key:
-	//
-	// 	// Package user defines a User type that's stored in Contexts.
-	// 	package user
-	//
-	// 	import "golang.org/x/net/context"
-	//
-	// 	// User is the type of value stored in the Contexts.
-	// 	type User struct {...}
-	//
-	// 	// key is an unexported type for keys defined in this package.
-	// 	// This prevents collisions with keys defined in other packages.
-	// 	type key int
-	//
-	// 	// userKey is the key for user.User values in Contexts. It is
-	// 	// unexported; clients use user.NewContext and user.FromContext
-	// 	// instead of using this key directly.
-	// 	var userKey key = 0
-	//
-	// 	// NewContext returns a new Context that carries value u.
-	// 	func NewContext(ctx context.Context, u *User) context.Context {
-	// 		return context.WithValue(ctx, userKey, u)
-	// 	}
-	//
-	// 	// FromContext returns the User value stored in ctx, if any.
-	// 	func FromContext(ctx context.Context) (*User, bool) {
-	// 		u, ok := ctx.Value(userKey).(*User)
-	// 		return u, ok
-	// 	}
-	Value(key interface{}) interface{}
-}
-
-// A CancelFunc tells an operation to abandon its work.
-// A CancelFunc does not wait for the work to stop.
-// After the first call, subsequent calls to a CancelFunc do nothing.
-type CancelFunc func()
diff --git a/vendor/golang.org/x/net/http2/server.go b/vendor/golang.org/x/net/http2/server.go
index 7434b878..b640deb0 100644
--- a/vendor/golang.org/x/net/http2/server.go
+++ b/vendor/golang.org/x/net/http2/server.go
@@ -2233,25 +2233,25 @@ func (sc *serverConn) newStream(id, pusherID uint32, state streamState) *stream
 func (sc *serverConn) newWriterAndRequest(st *stream, f *MetaHeadersFrame) (*responseWriter, *http.Request, error) {
 	sc.serveG.check()
 
-	rp := requestParam{
-		method:    f.PseudoValue("method"),
-		scheme:    f.PseudoValue("scheme"),
-		authority: f.PseudoValue("authority"),
-		path:      f.PseudoValue("path"),
-		protocol:  f.PseudoValue("protocol"),
+	rp := httpcommon.ServerRequestParam{
+		Method:    f.PseudoValue("method"),
+		Scheme:    f.PseudoValue("scheme"),
+		Authority: f.PseudoValue("authority"),
+		Path:      f.PseudoValue("path"),
+		Protocol:  f.PseudoValue("protocol"),
 	}
 
 	// extended connect is disabled, so we should not see :protocol
-	if disableExtendedConnectProtocol && rp.protocol != "" {
+	if disableExtendedConnectProtocol && rp.Protocol != "" {
 		return nil, nil, sc.countError("bad_connect", streamError(f.StreamID, ErrCodeProtocol))
 	}
 
-	isConnect := rp.method == "CONNECT"
+	isConnect := rp.Method == "CONNECT"
 	if isConnect {
-		if rp.protocol == "" && (rp.path != "" || rp.scheme != "" || rp.authority == "") {
+		if rp.Protocol == "" && (rp.Path != "" || rp.Scheme != "" || rp.Authority == "") {
 			return nil, nil, sc.countError("bad_connect", streamError(f.StreamID, ErrCodeProtocol))
 		}
-	} else if rp.method == "" || rp.path == "" || (rp.scheme != "https" && rp.scheme != "http") {
+	} else if rp.Method == "" || rp.Path == "" || (rp.Scheme != "https" && rp.Scheme != "http") {
 		// See 8.1.2.6 Malformed Requests and Responses:
 		//
 		// Malformed requests or responses that are detected
@@ -2265,15 +2265,16 @@ func (sc *serverConn) newWriterAndRequest(st *stream, f *MetaHeadersFrame) (*res
 		return nil, nil, sc.countError("bad_path_method", streamError(f.StreamID, ErrCodeProtocol))
 	}
 
-	rp.header = make(http.Header)
+	header := make(http.Header)
+	rp.Header = header
 	for _, hf := range f.RegularFields() {
-		rp.header.Add(sc.canonicalHeader(hf.Name), hf.Value)
+		header.Add(sc.canonicalHeader(hf.Name), hf.Value)
 	}
-	if rp.authority == "" {
-		rp.authority = rp.header.Get("Host")
+	if rp.Authority == "" {
+		rp.Authority = header.Get("Host")
 	}
-	if rp.protocol != "" {
-		rp.header.Set(":protocol", rp.protocol)
+	if rp.Protocol != "" {
+		header.Set(":protocol", rp.Protocol)
 	}
 
 	rw, req, err := sc.newWriterAndRequestNoBody(st, rp)
@@ -2282,7 +2283,7 @@ func (sc *serverConn) newWriterAndRequest(st *stream, f *MetaHeadersFrame) (*res
 	}
 	bodyOpen := !f.StreamEnded()
 	if bodyOpen {
-		if vv, ok := rp.header["Content-Length"]; ok {
+		if vv, ok := rp.Header["Content-Length"]; ok {
 			if cl, err := strconv.ParseUint(vv[0], 10, 63); err == nil {
 				req.ContentLength = int64(cl)
 			} else {
@@ -2298,84 +2299,38 @@ func (sc *serverConn) newWriterAndRequest(st *stream, f *MetaHeadersFrame) (*res
 	return rw, req, nil
 }
 
-type requestParam struct {
-	method                  string
-	scheme, authority, path string
-	protocol                string
-	header                  http.Header
-}
-
-func (sc *serverConn) newWriterAndRequestNoBody(st *stream, rp requestParam) (*responseWriter, *http.Request, error) {
+func (sc *serverConn) newWriterAndRequestNoBody(st *stream, rp httpcommon.ServerRequestParam) (*responseWriter, *http.Request, error) {
 	sc.serveG.check()
 
 	var tlsState *tls.ConnectionState // nil if not scheme https
-	if rp.scheme == "https" {
+	if rp.Scheme == "https" {
 		tlsState = sc.tlsState
 	}
 
-	needsContinue := httpguts.HeaderValuesContainsToken(rp.header["Expect"], "100-continue")
-	if needsContinue {
-		rp.header.Del("Expect")
-	}
-	// Merge Cookie headers into one "; "-delimited value.
-	if cookies := rp.header["Cookie"]; len(cookies) > 1 {
-		rp.header.Set("Cookie", strings.Join(cookies, "; "))
-	}
-
-	// Setup Trailers
-	var trailer http.Header
-	for _, v := range rp.header["Trailer"] {
-		for _, key := range strings.Split(v, ",") {
-			key = http.CanonicalHeaderKey(textproto.TrimString(key))
-			switch key {
-			case "Transfer-Encoding", "Trailer", "Content-Length":
-				// Bogus. (copy of http1 rules)
-				// Ignore.
-			default:
-				if trailer == nil {
-					trailer = make(http.Header)
-				}
-				trailer[key] = nil
-			}
-		}
-	}
-	delete(rp.header, "Trailer")
-
-	var url_ *url.URL
-	var requestURI string
-	if rp.method == "CONNECT" && rp.protocol == "" {
-		url_ = &url.URL{Host: rp.authority}
-		requestURI = rp.authority // mimic HTTP/1 server behavior
-	} else {
-		var err error
-		url_, err = url.ParseRequestURI(rp.path)
-		if err != nil {
-			return nil, nil, sc.countError("bad_path", streamError(st.id, ErrCodeProtocol))
-		}
-		requestURI = rp.path
+	res := httpcommon.NewServerRequest(rp)
+	if res.InvalidReason != "" {
+		return nil, nil, sc.countError(res.InvalidReason, streamError(st.id, ErrCodeProtocol))
 	}
 
 	body := &requestBody{
 		conn:          sc,
 		stream:        st,
-		needsContinue: needsContinue,
+		needsContinue: res.NeedsContinue,
 	}
-	req := &http.Request{
-		Method:     rp.method,
-		URL:        url_,
+	req := (&http.Request{
+		Method:     rp.Method,
+		URL:        res.URL,
 		RemoteAddr: sc.remoteAddrStr,
-		Header:     rp.header,
-		RequestURI: requestURI,
+		Header:     rp.Header,
+		RequestURI: res.RequestURI,
 		Proto:      "HTTP/2.0",
 		ProtoMajor: 2,
 		ProtoMinor: 0,
 		TLS:        tlsState,
-		Host:       rp.authority,
+		Host:       rp.Authority,
 		Body:       body,
-		Trailer:    trailer,
-	}
-	req = req.WithContext(st.ctx)
-
+		Trailer:    res.Trailer,
+	}).WithContext(st.ctx)
 	rw := sc.newResponseWriter(st, req)
 	return rw, req, nil
 }
@@ -3270,12 +3225,12 @@ func (sc *serverConn) startPush(msg *startPushRequest) {
 		// we start in "half closed (remote)" for simplicity.
 		// See further comments at the definition of stateHalfClosedRemote.
 		promised := sc.newStream(promisedID, msg.parent.id, stateHalfClosedRemote)
-		rw, req, err := sc.newWriterAndRequestNoBody(promised, requestParam{
-			method:    msg.method,
-			scheme:    msg.url.Scheme,
-			authority: msg.url.Host,
-			path:      msg.url.RequestURI(),
-			header:    cloneHeader(msg.header), // clone since handler runs concurrently with writing the PUSH_PROMISE
+		rw, req, err := sc.newWriterAndRequestNoBody(promised, httpcommon.ServerRequestParam{
+			Method:    msg.method,
+			Scheme:    msg.url.Scheme,
+			Authority: msg.url.Host,
+			Path:      msg.url.RequestURI(),
+			Header:    cloneHeader(msg.header), // clone since handler runs concurrently with writing the PUSH_PROMISE
 		})
 		if err != nil {
 			// Should not happen, since we've already validated msg.url.
diff --git a/vendor/golang.org/x/net/http2/transport.go b/vendor/golang.org/x/net/http2/transport.go
index f2c166b6..f26356b9 100644
--- a/vendor/golang.org/x/net/http2/transport.go
+++ b/vendor/golang.org/x/net/http2/transport.go
@@ -1286,6 +1286,19 @@ func (cc *ClientConn) responseHeaderTimeout() time.Duration {
 	return 0
 }
 
+// actualContentLength returns a sanitized version of
+// req.ContentLength, where 0 actually means zero (not unknown) and -1
+// means unknown.
+func actualContentLength(req *http.Request) int64 {
+	if req.Body == nil || req.Body == http.NoBody {
+		return 0
+	}
+	if req.ContentLength != 0 {
+		return req.ContentLength
+	}
+	return -1
+}
+
 func (cc *ClientConn) decrStreamReservations() {
 	cc.mu.Lock()
 	defer cc.mu.Unlock()
@@ -1310,7 +1323,7 @@ func (cc *ClientConn) roundTrip(req *http.Request, streamf func(*clientStream))
 		reqCancel:            req.Cancel,
 		isHead:               req.Method == "HEAD",
 		reqBody:              req.Body,
-		reqBodyContentLength: httpcommon.ActualContentLength(req),
+		reqBodyContentLength: actualContentLength(req),
 		trace:                httptrace.ContextClientTrace(ctx),
 		peerClosed:           make(chan struct{}),
 		abort:                make(chan struct{}),
@@ -1318,7 +1331,7 @@ func (cc *ClientConn) roundTrip(req *http.Request, streamf func(*clientStream))
 		donec:                make(chan struct{}),
 	}
 
-	cs.requestedGzip = httpcommon.IsRequestGzip(req, cc.t.disableCompression())
+	cs.requestedGzip = httpcommon.IsRequestGzip(req.Method, req.Header, cc.t.disableCompression())
 
 	go cs.doRequest(req, streamf)
 
@@ -1349,7 +1362,7 @@ func (cc *ClientConn) roundTrip(req *http.Request, streamf func(*clientStream))
 		}
 		res.Request = req
 		res.TLS = cc.tlsState
-		if res.Body == noBody && httpcommon.ActualContentLength(req) == 0 {
+		if res.Body == noBody && actualContentLength(req) == 0 {
 			// If there isn't a request or response body still being
 			// written, then wait for the stream to be closed before
 			// RoundTrip returns.
@@ -1596,12 +1609,7 @@ func (cs *clientStream) encodeAndWriteHeaders(req *http.Request) error {
 	// sent by writeRequestBody below, along with any Trailers,
 	// again in form HEADERS{1}, CONTINUATION{0,})
 	cc.hbuf.Reset()
-	res, err := httpcommon.EncodeHeaders(httpcommon.EncodeHeadersParam{
-		Request:               req,
-		AddGzipHeader:         cs.requestedGzip,
-		PeerMaxHeaderListSize: cc.peerMaxHeaderListSize,
-		DefaultUserAgent:      defaultUserAgent,
-	}, func(name, value string) {
+	res, err := encodeRequestHeaders(req, cs.requestedGzip, cc.peerMaxHeaderListSize, func(name, value string) {
 		cc.writeHeader(name, value)
 	})
 	if err != nil {
@@ -1617,6 +1625,22 @@ func (cs *clientStream) encodeAndWriteHeaders(req *http.Request) error {
 	return err
 }
 
+func encodeRequestHeaders(req *http.Request, addGzipHeader bool, peerMaxHeaderListSize uint64, headerf func(name, value string)) (httpcommon.EncodeHeadersResult, error) {
+	return httpcommon.EncodeHeaders(req.Context(), httpcommon.EncodeHeadersParam{
+		Request: httpcommon.Request{
+			Header:              req.Header,
+			Trailer:             req.Trailer,
+			URL:                 req.URL,
+			Host:                req.Host,
+			Method:              req.Method,
+			ActualContentLength: actualContentLength(req),
+		},
+		AddGzipHeader:         addGzipHeader,
+		PeerMaxHeaderListSize: peerMaxHeaderListSize,
+		DefaultUserAgent:      defaultUserAgent,
+	}, headerf)
+}
+
 // cleanupWriteRequest performs post-request tasks.
 //
 // If err (the result of writeRequest) is non-nil and the stream is not closed,
@@ -2186,6 +2210,13 @@ func (rl *clientConnReadLoop) cleanup() {
 	}
 	cc.cond.Broadcast()
 	cc.mu.Unlock()
+
+	if !cc.seenSettings {
+		// If we have a pending request that wants extended CONNECT,
+		// let it continue and fail with the connection error.
+		cc.extendedConnectAllowed = true
+		close(cc.seenSettingsChan)
+	}
 }
 
 // countReadFrameError calls Transport.CountError with a string
@@ -2278,9 +2309,6 @@ func (rl *clientConnReadLoop) run() error {
 			if VerboseLogs {
 				cc.vlogf("http2: Transport conn %p received error from processing frame %v: %v", cc, summarizeFrame(f), err)
 			}
-			if !cc.seenSettings {
-				close(cc.seenSettingsChan)
-			}
 			return err
 		}
 	}
diff --git a/vendor/golang.org/x/net/internal/httpcommon/headermap.go b/vendor/golang.org/x/net/internal/httpcommon/headermap.go
index ad3fbacd..92483d8e 100644
--- a/vendor/golang.org/x/net/internal/httpcommon/headermap.go
+++ b/vendor/golang.org/x/net/internal/httpcommon/headermap.go
@@ -5,7 +5,7 @@
 package httpcommon
 
 import (
-	"net/http"
+	"net/textproto"
 	"sync"
 )
 
@@ -82,7 +82,7 @@ func buildCommonHeaderMaps() {
 	commonLowerHeader = make(map[string]string, len(common))
 	commonCanonHeader = make(map[string]string, len(common))
 	for _, v := range common {
-		chk := http.CanonicalHeaderKey(v)
+		chk := textproto.CanonicalMIMEHeaderKey(v)
 		commonLowerHeader[chk] = v
 		commonCanonHeader[v] = chk
 	}
@@ -104,7 +104,7 @@ func CanonicalHeader(v string) string {
 	if s, ok := commonCanonHeader[v]; ok {
 		return s
 	}
-	return http.CanonicalHeaderKey(v)
+	return textproto.CanonicalMIMEHeaderKey(v)
 }
 
 // CachedCanonicalHeader returns the canonical form of a well-known header name.
diff --git a/vendor/golang.org/x/net/internal/httpcommon/request.go b/vendor/golang.org/x/net/internal/httpcommon/request.go
index 34391477..4b705531 100644
--- a/vendor/golang.org/x/net/internal/httpcommon/request.go
+++ b/vendor/golang.org/x/net/internal/httpcommon/request.go
@@ -5,10 +5,12 @@
 package httpcommon
 
 import (
+	"context"
 	"errors"
 	"fmt"
-	"net/http"
 	"net/http/httptrace"
+	"net/textproto"
+	"net/url"
 	"sort"
 	"strconv"
 	"strings"
@@ -21,9 +23,21 @@ var (
 	ErrRequestHeaderListSize = errors.New("request header list larger than peer's advertised limit")
 )
 
+// Request is a subset of http.Request.
+// It'd be simpler to pass an *http.Request, of course, but we can't depend on net/http
+// without creating a dependency cycle.
+type Request struct {
+	URL                 *url.URL
+	Method              string
+	Host                string
+	Header              map[string][]string
+	Trailer             map[string][]string
+	ActualContentLength int64 // 0 means 0, -1 means unknown
+}
+
 // EncodeHeadersParam is parameters to EncodeHeaders.
 type EncodeHeadersParam struct {
-	Request *http.Request
+	Request Request
 
 	// AddGzipHeader indicates that an "accept-encoding: gzip" header should be
 	// added to the request.
@@ -47,11 +61,11 @@ type EncodeHeadersResult struct {
 // It validates a request and calls headerf with each pseudo-header and header
 // for the request.
 // The headerf function is called with the validated, canonicalized header name.
-func EncodeHeaders(param EncodeHeadersParam, headerf func(name, value string)) (res EncodeHeadersResult, _ error) {
+func EncodeHeaders(ctx context.Context, param EncodeHeadersParam, headerf func(name, value string)) (res EncodeHeadersResult, _ error) {
 	req := param.Request
 
 	// Check for invalid connection-level headers.
-	if err := checkConnHeaders(req); err != nil {
+	if err := checkConnHeaders(req.Header); err != nil {
 		return res, err
 	}
 
@@ -73,7 +87,10 @@ func EncodeHeaders(param EncodeHeadersParam, headerf func(name, value string)) (
 
 	// isNormalConnect is true if this is a non-extended CONNECT request.
 	isNormalConnect := false
-	protocol := req.Header.Get(":protocol")
+	var protocol string
+	if vv := req.Header[":protocol"]; len(vv) > 0 {
+		protocol = vv[0]
+	}
 	if req.Method == "CONNECT" && protocol == "" {
 		isNormalConnect = true
 	} else if protocol != "" && req.Method != "CONNECT" {
@@ -107,9 +124,7 @@ func EncodeHeaders(param EncodeHeadersParam, headerf func(name, value string)) (
 		return res, fmt.Errorf("invalid HTTP trailer %s", err)
 	}
 
-	contentLength := ActualContentLength(req)
-
-	trailers, err := commaSeparatedTrailers(req)
+	trailers, err := commaSeparatedTrailers(req.Trailer)
 	if err != nil {
 		return res, err
 	}
@@ -123,7 +138,7 @@ func EncodeHeaders(param EncodeHeadersParam, headerf func(name, value string)) (
 		f(":authority", host)
 		m := req.Method
 		if m == "" {
-			m = http.MethodGet
+			m = "GET"
 		}
 		f(":method", m)
 		if !isNormalConnect {
@@ -198,8 +213,8 @@ func EncodeHeaders(param EncodeHeadersParam, headerf func(name, value string)) (
 				f(k, v)
 			}
 		}
-		if shouldSendReqContentLength(req.Method, contentLength) {
-			f("content-length", strconv.FormatInt(contentLength, 10))
+		if shouldSendReqContentLength(req.Method, req.ActualContentLength) {
+			f("content-length", strconv.FormatInt(req.ActualContentLength, 10))
 		}
 		if param.AddGzipHeader {
 			f("accept-encoding", "gzip")
@@ -225,7 +240,7 @@ func EncodeHeaders(param EncodeHeadersParam, headerf func(name, value string)) (
 		}
 	}
 
-	trace := httptrace.ContextClientTrace(req.Context())
+	trace := httptrace.ContextClientTrace(ctx)
 
 	// Header list size is ok. Write the headers.
 	enumerateHeaders(func(name, value string) {
@@ -243,19 +258,19 @@ func EncodeHeaders(param EncodeHeadersParam, headerf func(name, value string)) (
 		}
 	})
 
-	res.HasBody = contentLength != 0
+	res.HasBody = req.ActualContentLength != 0
 	res.HasTrailers = trailers != ""
 	return res, nil
 }
 
 // IsRequestGzip reports whether we should add an Accept-Encoding: gzip header
 // for a request.
-func IsRequestGzip(req *http.Request, disableCompression bool) bool {
+func IsRequestGzip(method string, header map[string][]string, disableCompression bool) bool {
 	// TODO(bradfitz): this is a copy of the logic in net/http. Unify somewhere?
 	if !disableCompression &&
-		req.Header.Get("Accept-Encoding") == "" &&
-		req.Header.Get("Range") == "" &&
-		req.Method != "HEAD" {
+		len(header["Accept-Encoding"]) == 0 &&
+		len(header["Range"]) == 0 &&
+		method != "HEAD" {
 		// Request gzip only, not deflate. Deflate is ambiguous and
 		// not as universally supported anyway.
 		// See: https://zlib.net/zlib_faq.html#faq39
@@ -280,22 +295,22 @@ func IsRequestGzip(req *http.Request, disableCompression bool) bool {
 //
 // Certain headers are special-cased as okay but not transmitted later.
 // For example, we allow "Transfer-Encoding: chunked", but drop the header when encoding.
-func checkConnHeaders(req *http.Request) error {
-	if v := req.Header.Get("Upgrade"); v != "" {
-		return fmt.Errorf("invalid Upgrade request header: %q", req.Header["Upgrade"])
+func checkConnHeaders(h map[string][]string) error {
+	if vv := h["Upgrade"]; len(vv) > 0 && (vv[0] != "" && vv[0] != "chunked") {
+		return fmt.Errorf("invalid Upgrade request header: %q", vv)
 	}
-	if vv := req.Header["Transfer-Encoding"]; len(vv) > 0 && (len(vv) > 1 || vv[0] != "" && vv[0] != "chunked") {
+	if vv := h["Transfer-Encoding"]; len(vv) > 0 && (len(vv) > 1 || vv[0] != "" && vv[0] != "chunked") {
 		return fmt.Errorf("invalid Transfer-Encoding request header: %q", vv)
 	}
-	if vv := req.Header["Connection"]; len(vv) > 0 && (len(vv) > 1 || vv[0] != "" && !asciiEqualFold(vv[0], "close") && !asciiEqualFold(vv[0], "keep-alive")) {
+	if vv := h["Connection"]; len(vv) > 0 && (len(vv) > 1 || vv[0] != "" && !asciiEqualFold(vv[0], "close") && !asciiEqualFold(vv[0], "keep-alive")) {
 		return fmt.Errorf("invalid Connection request header: %q", vv)
 	}
 	return nil
 }
 
-func commaSeparatedTrailers(req *http.Request) (string, error) {
-	keys := make([]string, 0, len(req.Trailer))
-	for k := range req.Trailer {
+func commaSeparatedTrailers(trailer map[string][]string) (string, error) {
+	keys := make([]string, 0, len(trailer))
+	for k := range trailer {
 		k = CanonicalHeader(k)
 		switch k {
 		case "Transfer-Encoding", "Trailer", "Content-Length":
@@ -310,19 +325,6 @@ func commaSeparatedTrailers(req *http.Request) (string, error) {
 	return "", nil
 }
 
-// ActualContentLength returns a sanitized version of
-// req.ContentLength, where 0 actually means zero (not unknown) and -1
-// means unknown.
-func ActualContentLength(req *http.Request) int64 {
-	if req.Body == nil || req.Body == http.NoBody {
-		return 0
-	}
-	if req.ContentLength != 0 {
-		return req.ContentLength
-	}
-	return -1
-}
-
 // validPseudoPath reports whether v is a valid :path pseudo-header
 // value. It must be either:
 //
@@ -340,7 +342,7 @@ func validPseudoPath(v string) bool {
 	return (len(v) > 0 && v[0] == '/') || v == "*"
 }
 
-func validateHeaders(hdrs http.Header) string {
+func validateHeaders(hdrs map[string][]string) string {
 	for k, vv := range hdrs {
 		if !httpguts.ValidHeaderFieldName(k) && k != ":protocol" {
 			return fmt.Sprintf("name %q", k)
@@ -377,3 +379,89 @@ func shouldSendReqContentLength(method string, contentLength int64) bool {
 		return false
 	}
 }
+
+// ServerRequestParam is parameters to NewServerRequest.
+type ServerRequestParam struct {
+	Method                  string
+	Scheme, Authority, Path string
+	Protocol                string
+	Header                  map[string][]string
+}
+
+// ServerRequestResult is the result of NewServerRequest.
+type ServerRequestResult struct {
+	// Various http.Request fields.
+	URL        *url.URL
+	RequestURI string
+	Trailer    map[string][]string
+
+	NeedsContinue bool // client provided an "Expect: 100-continue" header
+
+	// If the request should be rejected, this is a short string suitable for passing
+	// to the http2 package's CountError function.
+	// It might be a bit odd to return errors this way rather than returing an error,
+	// but this ensures we don't forget to include a CountError reason.
+	InvalidReason string
+}
+
+func NewServerRequest(rp ServerRequestParam) ServerRequestResult {
+	needsContinue := httpguts.HeaderValuesContainsToken(rp.Header["Expect"], "100-continue")
+	if needsContinue {
+		delete(rp.Header, "Expect")
+	}
+	// Merge Cookie headers into one "; "-delimited value.
+	if cookies := rp.Header["Cookie"]; len(cookies) > 1 {
+		rp.Header["Cookie"] = []string{strings.Join(cookies, "; ")}
+	}
+
+	// Setup Trailers
+	var trailer map[string][]string
+	for _, v := range rp.Header["Trailer"] {
+		for _, key := range strings.Split(v, ",") {
+			key = textproto.CanonicalMIMEHeaderKey(textproto.TrimString(key))
+			switch key {
+			case "Transfer-Encoding", "Trailer", "Content-Length":
+				// Bogus. (copy of http1 rules)
+				// Ignore.
+			default:
+				if trailer == nil {
+					trailer = make(map[string][]string)
+				}
+				trailer[key] = nil
+			}
+		}
+	}
+	delete(rp.Header, "Trailer")
+
+	// "':authority' MUST NOT include the deprecated userinfo subcomponent
+	// for "http" or "https" schemed URIs."
+	// https://www.rfc-editor.org/rfc/rfc9113.html#section-8.3.1-2.3.8
+	if strings.IndexByte(rp.Authority, '@') != -1 && (rp.Scheme == "http" || rp.Scheme == "https") {
+		return ServerRequestResult{
+			InvalidReason: "userinfo_in_authority",
+		}
+	}
+
+	var url_ *url.URL
+	var requestURI string
+	if rp.Method == "CONNECT" && rp.Protocol == "" {
+		url_ = &url.URL{Host: rp.Authority}
+		requestURI = rp.Authority // mimic HTTP/1 server behavior
+	} else {
+		var err error
+		url_, err = url.ParseRequestURI(rp.Path)
+		if err != nil {
+			return ServerRequestResult{
+				InvalidReason: "bad_path",
+			}
+		}
+		requestURI = rp.Path
+	}
+
+	return ServerRequestResult{
+		URL:           url_,
+		NeedsContinue: needsContinue,
+		RequestURI:    requestURI,
+		Trailer:       trailer,
+	}
+}
diff --git a/vendor/golang.org/x/net/proxy/per_host.go b/vendor/golang.org/x/net/proxy/per_host.go
index d7d4b8b6..32bdf435 100644
--- a/vendor/golang.org/x/net/proxy/per_host.go
+++ b/vendor/golang.org/x/net/proxy/per_host.go
@@ -7,6 +7,7 @@ package proxy
 import (
 	"context"
 	"net"
+	"net/netip"
 	"strings"
 )
 
@@ -57,7 +58,8 @@ func (p *PerHost) DialContext(ctx context.Context, network, addr string) (c net.
 }
 
 func (p *PerHost) dialerForRequest(host string) Dialer {
-	if ip := net.ParseIP(host); ip != nil {
+	if nip, err := netip.ParseAddr(host); err == nil {
+		ip := net.IP(nip.AsSlice())
 		for _, net := range p.bypassNetworks {
 			if net.Contains(ip) {
 				return p.bypass
@@ -108,8 +110,8 @@ func (p *PerHost) AddFromString(s string) {
 			}
 			continue
 		}
-		if ip := net.ParseIP(host); ip != nil {
-			p.AddIP(ip)
+		if nip, err := netip.ParseAddr(host); err == nil {
+			p.AddIP(net.IP(nip.AsSlice()))
 			continue
 		}
 		if strings.HasPrefix(host, "*.") {
diff --git a/vendor/golang.org/x/text/language/parse.go b/vendor/golang.org/x/text/language/parse.go
index 4d57222e..053336e2 100644
--- a/vendor/golang.org/x/text/language/parse.go
+++ b/vendor/golang.org/x/text/language/parse.go
@@ -59,7 +59,7 @@ func (c CanonType) Parse(s string) (t Tag, err error) {
 	if changed {
 		tt.RemakeString()
 	}
-	return makeTag(tt), err
+	return makeTag(tt), nil
 }
 
 // Compose creates a Tag from individual parts, which may be of type Tag, Base,
diff --git a/vendor/modules.txt b/vendor/modules.txt
index d10cecb2..b617ad8b 100644
--- a/vendor/modules.txt
+++ b/vendor/modules.txt
@@ -272,8 +272,8 @@ golang.org/x/arch/x86/x86asm
 # golang.org/x/exp v0.0.0-20240719175910-8a7402abbf56
 ## explicit; go 1.20
 golang.org/x/exp/maps
-# golang.org/x/net v0.35.0
-## explicit; go 1.18
+# golang.org/x/net v0.37.0
+## explicit; go 1.23.0
 golang.org/x/net/context
 golang.org/x/net/html
 golang.org/x/net/html/atom
@@ -291,16 +291,16 @@ golang.org/x/net/trace
 ## explicit; go 1.18
 golang.org/x/oauth2
 golang.org/x/oauth2/internal
-# golang.org/x/sys v0.30.0
-## explicit; go 1.18
+# golang.org/x/sys v0.31.0
+## explicit; go 1.23.0
 golang.org/x/sys/plan9
 golang.org/x/sys/unix
 golang.org/x/sys/windows
-# golang.org/x/term v0.29.0
-## explicit; go 1.18
+# golang.org/x/term v0.30.0
+## explicit; go 1.23.0
 golang.org/x/term
-# golang.org/x/text v0.22.0
-## explicit; go 1.18
+# golang.org/x/text v0.23.0
+## explicit; go 1.23.0
 golang.org/x/text/encoding
 golang.org/x/text/encoding/charmap
 golang.org/x/text/encoding/htmlindex



Test patch:
diff --git a/pkg/ddc/alluxio/transform_test.go b/pkg/ddc/alluxio/transform_test.go
index 4f27550d..2c313584 100755
--- a/pkg/ddc/alluxio/transform_test.go
+++ b/pkg/ddc/alluxio/transform_test.go
@@ -872,6 +872,19 @@ func TestAlluxioEngine_allocatePorts(t *testing.T) {
 	}
 }
 
+// TestTransformMasterProperties tests the transformMasters function of AlluxioEngine.
+// It verifies whether the master properties are correctly transformed based on the given runtime configuration.
+// The test cases ensure that the properties from the master template take precedence over the global properties.
+//
+// Test Cases:
+// 1. "master properties is not null":
+//    - Ensures that when master-specific properties exist, they override the global properties.
+//
+// 2. "properties is not null for master":
+//    - Ensures that both master-specific and additional global properties are correctly handled.
+//
+// The function iterates over multiple test cases and checks if the transformed properties
+// match the expected values. If the transformation does not produce the expected result, the test fails.
 func TestTransformMasterProperties(t *testing.T) {
 	engine := &AlluxioEngine{Log: fake.NullLogger()}
 
diff --git a/pkg/ddc/alluxio/ufs_test.go b/pkg/ddc/alluxio/ufs_test.go
index a2bd72c0..9b04fc1a 100644
--- a/pkg/ddc/alluxio/ufs_test.go
+++ b/pkg/ddc/alluxio/ufs_test.go
@@ -907,6 +907,12 @@ func TestFindUnmountedUFS(t *testing.T) {
 	}
 }
 
+// TestUpdateMountTime verifies if AlluxioEngine's updateMountTime method correctly updates runtime's MountTime status.
+// It creates a runtime with outdated MountTime, executes the update method, then checks if MountTime gets refreshed timestamp.
+//
+// param: t *testing.T - The testing context used for running the test and reporting failures.
+//
+// returns: None (This is a test function and does not return any value.)
 func TestUpdateMountTime(t *testing.T) {
 	yesterday := time.Now().AddDate(0, 0, -1)
 
diff --git a/pkg/ddc/jindo/dataset_test.go b/pkg/ddc/jindo/dataset_test.go
index 8cea4e51..2bd1416d 100644
--- a/pkg/ddc/jindo/dataset_test.go
+++ b/pkg/ddc/jindo/dataset_test.go
@@ -28,6 +28,9 @@ import (
 	"k8s.io/apimachinery/pkg/runtime"
 )
 
+// TestUpdateCacheOfDataset tests the UpdateCacheOfDataset function of the JindoEngine.
+// It sets up a fake client with a dataset and runtime, then calls the function and verifies
+// that the dataset's status is updated correctly.
 func TestUpdateCacheOfDataset(t *testing.T) {
 	testDatasetInputs := []*datav1alpha1.Dataset{
 		{
diff --git a/pkg/ddc/juicefs/data_migrate_test.go b/pkg/ddc/juicefs/data_migrate_test.go
index d21c8959..c25d433f 100644
--- a/pkg/ddc/juicefs/data_migrate_test.go
+++ b/pkg/ddc/juicefs/data_migrate_test.go
@@ -597,6 +597,12 @@ func TestJuiceFSEngine_genDataUrl(t *testing.T) {
 	}
 }
 
+// TestJuiceFSEngine_setParallelMigrateOptions tests the setParallelMigrateOptions method of JuiceFSEngine.
+// This method is responsible for setting parallel migration options, including SSH port and SSH ready timeout.
+// The test cases cover:
+// 1. Normal scenario where parallel migration options are set correctly.
+// 2. Default values are used when no parallel options are provided.
+// 3. Error scenario where invalid parallel options are provided.
 func TestJuiceFSEngine_setParallelMigrateOptions(t *testing.T) {
 	type args struct {
 		dataMigrateInfo *cdatamigrate.DataMigrateInfo
diff --git a/pkg/ddc/juicefs/datasetinfo_parser_test.go b/pkg/ddc/juicefs/datasetinfo_parser_test.go
index dd7f4209..13973de3 100644
--- a/pkg/ddc/juicefs/datasetinfo_parser_test.go
+++ b/pkg/ddc/juicefs/datasetinfo_parser_test.go
@@ -169,6 +169,16 @@ func TestGetFSInfoFromConfigMap(t *testing.T) {
 	}
 }
 
+// Test_parseFSInfoFromConfigMap is a unit test function for the parseFSInfoFromConfigMap method.
+// It validates whether the function correctly extracts and parses dataset information 
+// from a given Kubernetes ConfigMap.
+//
+// Steps:
+// 1. Define test cases with different ConfigMap data inputs.
+// 2. Execute parseFSInfoFromConfigMap using the provided test cases.
+// 3. Verify the returned metadata information against expected values.
+// 4. Check for expected errors in erroneous cases.
+// 5. Use assertions to ensure the function behaves as intended.
 func Test_parseFSInfoFromConfigMap(t *testing.T) {
 	type args struct {
 		configMap *v1.ConfigMap
diff --git a/pkg/webhook/plugins/fileprefetcher/file_prefetcher_test.go b/pkg/webhook/plugins/fileprefetcher/file_prefetcher_test.go
new file mode 100644
index 00000000..db7ac69b
--- /dev/null
+++ b/pkg/webhook/plugins/fileprefetcher/file_prefetcher_test.go
@@ -0,0 +1,423 @@
+/*
+Copyright 2025 The Fluid Authors.
+
+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 fileprefetcher
+
+import (
+	"testing"
+
+	"github.com/fluid-cloudnative/fluid/pkg/common"
+	"github.com/fluid-cloudnative/fluid/pkg/ddc/base"
+	"github.com/stretchr/testify/assert"
+	corev1 "k8s.io/api/core/v1"
+	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
+
+	. "github.com/onsi/ginkgo"
+	. "github.com/onsi/gomega"
+)
+
+func TestInjectFilePrefetcherSidecar_WithMatchingContainers_ShouldInsertAfterLastMatch(t *testing.T) {
+	oldContainers := []corev1.Container{
+		{Name: common.FuseContainerName + "-2"},
+		{Name: common.FuseContainerName + "-1"},
+		{Name: "other-container"},
+		{Name: "another-container"},
+	}
+	prefetcher := &FilePrefetcher{}
+	filePrefetcherCtr := corev1.Container{Name: "file-prefetcher-ctr"}
+
+	newContainers := prefetcher.injectFilePrefetcherSidecar(oldContainers, filePrefetcherCtr)
+
+	assert.Equal(t, 5, len(newContainers))
+	assert.Equal(t, common.FuseContainerName+"-2", newContainers[0].Name)
+	assert.Equal(t, common.FuseContainerName+"-1", newContainers[1].Name)
+	assert.Equal(t, "file-prefetcher-ctr", newContainers[2].Name)
+	assert.Equal(t, "other-container", newContainers[3].Name)
+	assert.Equal(t, "another-container", newContainers[4].Name)
+}
+
+func TestInjectFilePrefetcherSidecar_WithoutMatchingContainers_ShouldInsertAtBeginning(t *testing.T) {
+	oldContainers := []corev1.Container{
+		{Name: "other-container"},
+		{Name: "another-container"},
+	}
+	prefetcher := &FilePrefetcher{}
+	filePrefetcherCtr := corev1.Container{Name: "file-prefetcher-ctr"}
+
+	newContainers := prefetcher.injectFilePrefetcherSidecar(oldContainers, filePrefetcherCtr)
+
+	assert.Equal(t, 3, len(newContainers))
+	assert.Equal(t, "file-prefetcher-ctr", newContainers[0].Name)
+	assert.Equal(t, "other-container", newContainers[1].Name)
+	assert.Equal(t, "another-container", newContainers[2].Name)
+}
+
+func TestInjectFilePrefetcherSidecar_EmptyContainerList_ShouldOnlyContainFilePrefetcher(t *testing.T) {
+	oldContainers := []corev1.Container{}
+	filePrefetcherCtr := corev1.Container{Name: "file-prefetcher-ctr"}
+
+	prefetcher := &FilePrefetcher{}
+	newContainers := prefetcher.injectFilePrefetcherSidecar(oldContainers, filePrefetcherCtr)
+
+	assert.Equal(t, 1, len(newContainers))
+	assert.Equal(t, "file-prefetcher-ctr", newContainers[0].Name)
+}
+
+var _ = Describe("buildFilePrefetcherConfig", func() {
+	var (
+		pod          *corev1.Pod
+		runtimeInfos map[string]base.RuntimeInfoInterface
+		prefetcher   *FilePrefetcher
+	)
+
+	BeforeEach(func() {
+		pod = &corev1.Pod{
+			ObjectMeta: metav1.ObjectMeta{
+				Annotations: map[string]string{},
+			},
+		}
+		runtimeInfos = map[string]base.RuntimeInfoInterface{}
+		prefetcher = &FilePrefetcher{}
+	})
+
+	Context("when all annotations are provided correctly", func() {
+		BeforeEach(func() {
+			pod.Annotations[AnnotationFilePrefetcherImage] = "test-image"
+			pod.Annotations[AnnotationFilePrefetcherExtraEnvs] = "KEY1=VALUE1 KEY2=VALUE2"
+			pod.Annotations[AnnotationFilePrefetcherFileList] = "pvc://mypvc/path/to/myfolder/*.pkl"
+			pod.Annotations[AnnotationFilePrefetcherAsync] = "true"
+			pod.Annotations[AnnotationFilePrefetcherTimeoutSeconds] = "30"
+			pod.Spec.Volumes = append(pod.Spec.Volumes, corev1.Volume{Name: "myvol", VolumeSource: corev1.VolumeSource{
+				PersistentVolumeClaim: &corev1.PersistentVolumeClaimVolumeSource{
+					ClaimName: "mypvc",
+				},
+			}})
+			runtimeInfos["mypvc"] = &base.RuntimeInfo{}
+		})
+
+		It("should build the configuration successfully", func() {
+			config, err := prefetcher.buildFilePrefetcherConfig(pod, runtimeInfos)
+			Expect(err).NotTo(HaveOccurred())
+			Expect(config.Image).To(Equal("test-image"))
+			Expect(config.ExtraEnvs).To(HaveKeyWithValue("KEY1", "VALUE1"))
+			Expect(config.ExtraEnvs).To(HaveKeyWithValue("KEY2", "VALUE2"))
+			Expect(config.GlobPaths).To(Equal("/data/myvol/path/to/myfolder/*.pkl"))
+			Expect(config.AsyncPrefetch).To(BeTrue())
+			Expect(config.TimeoutSeconds).To(Equal(30))
+		})
+
+	})
+
+	Context("when the image annotation is missing, with default image set", func() {
+		BeforeEach(func() {
+			pod.Annotations[AnnotationFilePrefetcherExtraEnvs] = "KEY1=VALUE1 KEY2=VALUE2"
+			pod.Annotations[AnnotationFilePrefetcherFileList] = "pvc://mypvc/path/to/myfolder/*.pkl"
+			pod.Annotations[AnnotationFilePrefetcherAsync] = "true"
+			pod.Annotations[AnnotationFilePrefetcherTimeoutSeconds] = "30"
+			pod.Spec.Volumes = append(pod.Spec.Volumes, corev1.Volume{Name: "myvol", VolumeSource: corev1.VolumeSource{
+				PersistentVolumeClaim: &corev1.PersistentVolumeClaimVolumeSource{
+					ClaimName: "mypvc",
+				},
+			}})
+			runtimeInfos["mypvc"] = &base.RuntimeInfo{}
+			defaultFilePrefetcherImage = "test-image"
+		})
+
+		It("should use the default image", func() {
+			config, err := prefetcher.buildFilePrefetcherConfig(pod, runtimeInfos)
+			Expect(err).NotTo(HaveOccurred())
+			Expect(config.Image).To(Equal(defaultFilePrefetcherImage))
+			Expect(config.ExtraEnvs).To(HaveKeyWithValue("KEY1", "VALUE1"))
+			Expect(config.ExtraEnvs).To(HaveKeyWithValue("KEY2", "VALUE2"))
+			Expect(config.GlobPaths).To(Equal("/data/myvol/path/to/myfolder/*.pkl"))
+			Expect(config.AsyncPrefetch).To(BeTrue())
+			Expect(config.TimeoutSeconds).To(Equal(30))
+		})
+	})
+
+	Context("when the image annotation is missing, no default image set", func() {
+		BeforeEach(func() {
+			pod.Annotations[AnnotationFilePrefetcherExtraEnvs] = "KEY1=VALUE1 KEY2=VALUE2"
+			pod.Annotations[AnnotationFilePrefetcherFileList] = "pvc://mypvc/path/to/myfolder/*.pkl"
+			pod.Annotations[AnnotationFilePrefetcherAsync] = "true"
+			pod.Annotations[AnnotationFilePrefetcherTimeoutSeconds] = "30"
+			defaultFilePrefetcherImage = ""
+		})
+
+		It("should return an error", func() {
+			_, err := prefetcher.buildFilePrefetcherConfig(pod, runtimeInfos)
+			Expect(err).To(HaveOccurred())
+		})
+	})
+
+	Context("when the extra envs annotation has an invalid format", func() {
+		BeforeEach(func() {
+			pod.Annotations[AnnotationFilePrefetcherImage] = "test-image"
+			pod.Annotations[AnnotationFilePrefetcherExtraEnvs] = "KEY1=VALUE1 KEY2"
+			pod.Annotations[AnnotationFilePrefetcherFileList] = "pvc://mypvc/path/to/myfolder/*.pkl"
+			pod.Annotations[AnnotationFilePrefetcherAsync] = "true"
+			pod.Annotations[AnnotationFilePrefetcherTimeoutSeconds] = "30"
+		})
+
+		It("should return an error", func() {
+			_, err := prefetcher.buildFilePrefetcherConfig(pod, runtimeInfos)
+			Expect(err).To(HaveOccurred())
+		})
+	})
+
+	Context("when the async prefetch annotation has an invalid value", func() {
+		BeforeEach(func() {
+			pod.Annotations[AnnotationFilePrefetcherImage] = "test-image"
+			pod.Annotations[AnnotationFilePrefetcherExtraEnvs] = "KEY1=VALUE1 KEY2=VALUE2"
+			pod.Annotations[AnnotationFilePrefetcherFileList] = "pvc://mypvc/path/to/myfolder/*.pkl"
+			pod.Annotations[AnnotationFilePrefetcherAsync] = "invalid"
+			pod.Annotations[AnnotationFilePrefetcherTimeoutSeconds] = "30"
+		})
+
+		It("should return an error", func() {
+			_, err := prefetcher.buildFilePrefetcherConfig(pod, runtimeInfos)
+			Expect(err).To(HaveOccurred())
+		})
+	})
+
+	Context("when the timeout seconds annotation has an invalid value", func() {
+		BeforeEach(func() {
+			pod.Annotations[AnnotationFilePrefetcherImage] = "test-image"
+			pod.Annotations[AnnotationFilePrefetcherExtraEnvs] = "KEY1=VALUE1 KEY2=VALUE2"
+			pod.Annotations[AnnotationFilePrefetcherFileList] = "pvc://mypvc/path/to/myfolder/*.pkl"
+			pod.Annotations[AnnotationFilePrefetcherAsync] = "true"
+			pod.Annotations[AnnotationFilePrefetcherTimeoutSeconds] = "invalid"
+		})
+
+		It("should return an error", func() {
+			_, err := prefetcher.buildFilePrefetcherConfig(pod, runtimeInfos)
+			Expect(err).To(HaveOccurred())
+		})
+	})
+})
+
+var _ = Describe("parseGlobPathsFromFileList", func() {
+	var (
+		pod          *corev1.Pod
+		runtimeInfos map[string]base.RuntimeInfoInterface
+		prefetcher   *FilePrefetcher
+	)
+
+	BeforeEach(func() {
+		pod = &corev1.Pod{
+			ObjectMeta: metav1.ObjectMeta{
+				Annotations: map[string]string{},
+			},
+			Spec: corev1.PodSpec{
+				Volumes: []corev1.Volume{
+					{
+						Name: "mypvc-volume",
+						VolumeSource: corev1.VolumeSource{
+							PersistentVolumeClaim: &corev1.PersistentVolumeClaimVolumeSource{
+								ClaimName: "mypvc",
+							},
+						},
+					},
+				},
+			},
+		}
+		runtimeInfos = map[string]base.RuntimeInfoInterface{
+			"mypvc": &base.RuntimeInfo{},
+		}
+		prefetcher = &FilePrefetcher{}
+	})
+
+	Context("when the file list is valid", func() {
+		BeforeEach(func() {
+			fileList := "pvc://mypvc/path/to/myfolder/*.pkl"
+			pod.Annotations[AnnotationFilePrefetcherFileList] = fileList
+		})
+
+		It("should parse the glob paths correctly", func() {
+			volumeMountPaths, globPaths := prefetcher.parseGlobPathsFromFileList(pod.Annotations[AnnotationFilePrefetcherFileList], pod, runtimeInfos)
+			Expect(volumeMountPaths).To(HaveKeyWithValue("mypvc-volume", "/data/mypvc-volume"))
+			Expect(globPaths).To(ConsistOf("/data/mypvc-volume/path/to/myfolder/*.pkl"))
+		})
+	})
+
+	Context("when the file list contains multiple valid paths", func() {
+		BeforeEach(func() {
+			fileList := "pvc://mypvc/path/to/myfolder/*.pkl;pvc://mypvc/another/path/*.txt"
+			pod.Annotations[AnnotationFilePrefetcherFileList] = fileList
+		})
+
+		It("should parse the glob paths correctly", func() {
+			volumeMountPaths, globPaths := prefetcher.parseGlobPathsFromFileList(pod.Annotations[AnnotationFilePrefetcherFileList], pod, runtimeInfos)
+			Expect(volumeMountPaths).To(HaveKeyWithValue("mypvc-volume", "/data/mypvc-volume"))
+			Expect(globPaths).To(ConsistOf("/data/mypvc-volume/path/to/myfolder/*.pkl", "/data/mypvc-volume/another/path/*.txt"))
+		})
+	})
+
+	Context("when the file list contains an invalid URI path", func() {
+		BeforeEach(func() {
+			fileList := "invalid://mypvc/path/to/myfolder/*.pkl"
+			pod.Annotations[AnnotationFilePrefetcherFileList] = fileList
+		})
+
+		It("should skip the invalid path", func() {
+			volumeMountPaths, globPaths := prefetcher.parseGlobPathsFromFileList(pod.Annotations[AnnotationFilePrefetcherFileList], pod, runtimeInfos)
+			Expect(volumeMountPaths).To(BeEmpty())
+			Expect(globPaths).To(BeEmpty())
+		})
+	})
+
+	Context("when the file list contains a path without a valid PVC", func() {
+		BeforeEach(func() {
+			fileList := "pvc://nonexistent-pvc/path/to/myfolder/*.pkl"
+			pod.Annotations[AnnotationFilePrefetcherFileList] = fileList
+		})
+
+		It("should skip the path with the nonexistent PVC", func() {
+			volumeMountPaths, globPaths := prefetcher.parseGlobPathsFromFileList(pod.Annotations[AnnotationFilePrefetcherFileList], pod, runtimeInfos)
+			Expect(volumeMountPaths).To(BeEmpty())
+			Expect(globPaths).To(BeEmpty())
+		})
+	})
+
+	Context("when the file list is empty", func() {
+		BeforeEach(func() {
+			fileList := ""
+			pod.Annotations[AnnotationFilePrefetcherFileList] = fileList
+		})
+
+		It("should return empty volume mount paths and glob paths", func() {
+			volumeMountPaths, globPaths := prefetcher.parseGlobPathsFromFileList(pod.Annotations[AnnotationFilePrefetcherFileList], pod, runtimeInfos)
+			Expect(volumeMountPaths).To(BeEmpty())
+			Expect(globPaths).To(BeEmpty())
+		})
+	})
+
+	Context("when the file list contains a path without a subpath", func() {
+		BeforeEach(func() {
+			fileList := "pvc://mypvc"
+			pod.Annotations[AnnotationFilePrefetcherFileList] = fileList
+		})
+
+		It("should default the glob path to **", func() {
+			volumeMountPaths, globPaths := prefetcher.parseGlobPathsFromFileList(pod.Annotations[AnnotationFilePrefetcherFileList], pod, runtimeInfos)
+			Expect(volumeMountPaths).To(HaveKeyWithValue("mypvc-volume", "/data/mypvc-volume"))
+			Expect(globPaths).To(ConsistOf("/data/mypvc-volume/**"))
+		})
+	})
+
+	Context("when the file list contains relative path like \"..\" and \".\"", func() {
+		BeforeEach(func() {
+			fileList := "pvc://mypvc/../path/to/myfolder/.././mydir*/test1"
+			pod.Annotations[AnnotationFilePrefetcherFileList] = fileList
+		})
+
+		It("should clean the file list", func() {
+			volumeMountPaths, globPaths := prefetcher.parseGlobPathsFromFileList(pod.Annotations[AnnotationFilePrefetcherFileList], pod, runtimeInfos)
+			Expect(volumeMountPaths).To(HaveKeyWithValue("mypvc-volume", "/data/mypvc-volume"))
+			Expect(globPaths).To(ConsistOf("/data/mypvc-volume/path/to/mydir*/test1"))
+		})
+	})
+})
+
+var _ = Describe("buildFilePrefetcherSidecarContainer", func() {
+	var (
+		config     filePrefetcherConfig
+		prefetcher *FilePrefetcher
+	)
+
+	BeforeEach(func() {
+		prefetcher = &FilePrefetcher{}
+		config = filePrefetcherConfig{
+			Image:            "test-image",
+			AsyncPrefetch:    false,
+			VolumeMountPaths: map[string]string{"mypvc-volume": "/data/mypvc-volume"},
+			GlobPaths:        "/data/mypvc-volume/path/to/myfolder/*.pkl",
+			TimeoutSeconds:   30,
+			ExtraEnvs:        map[string]string{"KEY1": "VALUE1", "KEY2": "VALUE2"},
+		}
+	})
+
+	Context("when the configuration is valid", func() {
+		It("should build the sidecar container correctly", func() {
+			containerSpec, statusFileVolume := prefetcher.buildFilePrefetcherSidecarContainer(config)
+			Expect(containerSpec.Name).To(Equal(filePrefetcherContainerName))
+			Expect(containerSpec.Image).To(Equal("test-image"))
+			Expect(containerSpec.Env).To(ContainElements(
+				corev1.EnvVar{Name: envKeyFilePrefetcherFileList, Value: "/data/mypvc-volume/path/to/myfolder/*.pkl"},
+				corev1.EnvVar{Name: envKeyFilePrefetcherAsyncPrefetch, Value: "false"},
+				corev1.EnvVar{Name: envKeyFilePrefetcherTimeoutSeconds, Value: "30"},
+				corev1.EnvVar{Name: "KEY1", Value: "VALUE1"},
+				corev1.EnvVar{Name: "KEY2", Value: "VALUE2"},
+			))
+			Expect(containerSpec.VolumeMounts).To(ContainElements(
+				corev1.VolumeMount{Name: "mypvc-volume", MountPath: "/data/mypvc-volume"},
+				corev1.VolumeMount{Name: filePrefetcherStatusVolumeName, MountPath: filePrefetcherStatusVolumeMountPath},
+			))
+			Expect(statusFileVolume.Name).To(Equal(filePrefetcherStatusVolumeName))
+			Expect(statusFileVolume.VolumeSource.EmptyDir).NotTo(BeNil())
+		})
+	})
+
+	Context("when async prefetch is enabled", func() {
+		BeforeEach(func() {
+			config.AsyncPrefetch = true
+		})
+
+		It("should not have a PostStart lifecycle hook", func() {
+			containerSpec, _ := prefetcher.buildFilePrefetcherSidecarContainer(config)
+			Expect(containerSpec.Lifecycle).To(BeNil())
+		})
+	})
+
+	Context("when async prefetch is disabled", func() {
+		BeforeEach(func() {
+			config.AsyncPrefetch = false
+		})
+
+		It("should have a PostStart lifecycle hook", func() {
+			containerSpec, _ := prefetcher.buildFilePrefetcherSidecarContainer(config)
+			Expect(containerSpec.Lifecycle).NotTo(BeNil())
+			Expect(containerSpec.Lifecycle.PostStart.Exec.Command).To(Equal([]string{
+				"bash",
+				"-c",
+				`cnt=0; while [[ $cnt -lt $FILE_PREFETCHER_TIMEOUT_SECONDS ]]; do if [[ -e "/tmp/fluid-file-prefetcher/status/prefetcher.status" ]]; then exit 0; fi; cnt=$(expr $cnt + 1); sleep 1; done; echo "time out waiting for prefetching done"; exit 1`,
+			}))
+		})
+	})
+
+	Context("when there are no extra environment variables", func() {
+		BeforeEach(func() {
+			config.ExtraEnvs = map[string]string{}
+		})
+
+		It("should not include extra environment variables", func() {
+			containerSpec, _ := prefetcher.buildFilePrefetcherSidecarContainer(config)
+			Expect(containerSpec.Env).To(ContainElements(
+				corev1.EnvVar{Name: envKeyFilePrefetcherFileList, Value: "/data/mypvc-volume/path/to/myfolder/*.pkl"},
+				corev1.EnvVar{Name: envKeyFilePrefetcherAsyncPrefetch, Value: "false"},
+				corev1.EnvVar{Name: envKeyFilePrefetcherTimeoutSeconds, Value: "30"},
+			))
+			Expect(containerSpec.Env).NotTo(ContainElement(corev1.EnvVar{Name: "KEY1", Value: "VALUE1"}))
+			Expect(containerSpec.Env).NotTo(ContainElement(corev1.EnvVar{Name: "KEY2", Value: "VALUE2"}))
+		})
+	})
+})
+
+func TestFilePrefetcher(t *testing.T) {
+
+	RegisterFailHandler(Fail)
+	RunSpecs(t, "FilePrefetcher")
+}