From 5e0a1bdf59fea56658740fa6d6d9abff51febaea Mon Sep 17 00:00:00 2001 From: Haytham Abuelfutuh Date: Wed, 24 Jun 2026 00:57:48 +0000 Subject: [PATCH 01/14] fuse: experimental write-path FUSE passthrough (WIP) Newly written files get a local staging file (tmpfs/ext4) the kernel reads/ writes directly via FUSE_PASSTHROUGH, bypassing the daemon per-op; reconciled into JuiceFS slices on release. Opt-in via JUICEFS_PASSTHROUGH=1 + JUICEFS_PASSTHROUGH_DIR (must be a non-stacked fs). Results (4K randwrite, local file backend): buffered ~780 -> ~1.88M IOPS, durable ~270 -> ~152k IOPS. Reconcile reads the staging file back into slices (per-chunk buffer copy; reopen-by-path), then invalidates the kernel attr/data cache (InodeNotify) so same-session reads see the committed file. Verified correct read-your-writes for 1/17/64/200/333 MB. The cache fix adds no throughput cost (only 2 notify calls at close). Requires the passthrough-enabled go-fuse fork (local replace). Co-Authored-By: Claude Opus 4.8 Signed-off-by: Haytham Abuelfutuh --- go.mod | 95 ++++++++++++------------ go.sum | 102 ++++++++++++++++++++++++++ pkg/fuse/fuse.go | 29 ++++++++ pkg/fuse/passthrough.go | 158 ++++++++++++++++++++++++++++++++++++++++ pkg/vfs/vfs.go | 2 + 5 files changed, 340 insertions(+), 46 deletions(-) create mode 100644 pkg/fuse/passthrough.go diff --git a/go.mod b/go.mod index 6efb9de9aafc..7271317688c1 100644 --- a/go.mod +++ b/go.mod @@ -14,11 +14,11 @@ require ( github.com/aliyun/alibabacloud-oss-go-sdk-v2 v1.4.1 github.com/aliyun/credentials-go v1.4.5 github.com/apple/foundationdb/bindings/go v0.0.0-20211207225159-47b9a81d1c10 - github.com/aws/aws-sdk-go-v2 v1.41.5 + github.com/aws/aws-sdk-go-v2 v1.42.0 github.com/aws/aws-sdk-go-v2/config v1.32.14 github.com/aws/aws-sdk-go-v2/credentials v1.19.14 github.com/aws/aws-sdk-go-v2/service/s3 v1.99.0 - github.com/aws/smithy-go v1.24.2 + github.com/aws/smithy-go v1.27.1 github.com/baidubce/bce-sdk-go v0.9.263 github.com/bytedance/mockey v1.2.14 github.com/ceph/go-ceph v0.18.0 @@ -34,7 +34,7 @@ require ( github.com/go-sql-driver/mysql v1.9.3 github.com/goccy/go-json v0.10.5 github.com/gofrs/flock v0.8.1 - github.com/golang/snappy v0.0.4 + github.com/golang/snappy v1.0.0 github.com/google/btree v1.1.2 github.com/google/uuid v1.6.0 github.com/grafana/pyroscope-go v1.2.1 @@ -71,7 +71,7 @@ require ( github.com/qingstor/qingstor-sdk-go/v4 v4.4.1 github.com/qiniu/go-sdk/v7 v7.26.8 github.com/redis/go-redis/v9 v9.18.0 - github.com/sirupsen/logrus v1.9.3 + github.com/sirupsen/logrus v1.9.4 github.com/smartystreets/goconvey v1.7.2 github.com/spf13/cast v1.7.1 github.com/stretchr/testify v1.11.1 @@ -88,16 +88,16 @@ require ( github.com/volcengine/ve-tos-golang-sdk/v2 v2.9.0 github.com/winfsp/cgofuse v1.6.0 go.etcd.io/etcd v3.3.27+incompatible - go.etcd.io/etcd/client/v3 v3.5.9 + go.etcd.io/etcd/client/v3 v3.6.12 go.uber.org/automaxprocs v1.6.0 - go.uber.org/zap v1.27.0 - golang.org/x/crypto v0.49.0 - golang.org/x/net v0.52.0 - golang.org/x/oauth2 v0.34.0 - golang.org/x/sync v0.20.0 - golang.org/x/sys v0.42.0 - golang.org/x/term v0.41.0 - golang.org/x/text v0.35.0 + go.uber.org/zap v1.28.0 + golang.org/x/crypto v0.51.0 + golang.org/x/net v0.54.0 + golang.org/x/oauth2 v0.36.0 + golang.org/x/sync v0.21.0 + golang.org/x/sys v0.45.0 + golang.org/x/term v0.43.0 + golang.org/x/text v0.38.0 google.golang.org/api v0.210.0 google.golang.org/protobuf v1.36.11 gopkg.in/kothar/go-backblaze.v0 v0.0.0-20210124194846-35409b867216 @@ -113,13 +113,13 @@ require ( cloud.google.com/go/auth/oauth2adapt v0.2.6 // indirect cloud.google.com/go/iam v1.2.2 // indirect cloud.google.com/go/monitoring v1.21.2 // indirect - filippo.io/edwards25519 v1.1.0 // indirect + filippo.io/edwards25519 v1.2.0 // indirect git.apache.org/thrift.git v0.13.0 // indirect - github.com/Azure/azure-sdk-for-go/sdk/internal v1.11.2 // indirect + github.com/Azure/azure-sdk-for-go/sdk/internal v1.12.0 // indirect github.com/Azure/go-ntlmssp v0.0.0-20200615164410-66371956d46c // indirect - github.com/AzureAD/microsoft-authentication-library-for-go v1.6.0 // indirect + github.com/AzureAD/microsoft-authentication-library-for-go v1.7.2 // indirect github.com/BurntSushi/toml v1.3.2 // indirect - github.com/GoogleCloudPlatform/opentelemetry-operations-go/detectors/gcp v1.31.0 // indirect + github.com/GoogleCloudPlatform/opentelemetry-operations-go/detectors/gcp v1.33.0 // indirect github.com/GoogleCloudPlatform/opentelemetry-operations-go/exporter/metric v0.48.1 // indirect github.com/GoogleCloudPlatform/opentelemetry-operations-go/internal/resourcemapping v0.48.1 // indirect github.com/IBM/go-sdk-core/v5 v5.21.2 // indirect @@ -133,8 +133,8 @@ require ( github.com/armon/go-metrics v0.4.1 // indirect github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.7.8 // indirect github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.18.21 // indirect - github.com/aws/aws-sdk-go-v2/internal/configsources v1.4.21 // indirect - github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.7.21 // indirect + github.com/aws/aws-sdk-go-v2/internal/configsources v1.4.29 // indirect + github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.7.29 // indirect github.com/aws/aws-sdk-go-v2/internal/ini v1.8.6 // indirect github.com/aws/aws-sdk-go-v2/internal/v4a v1.4.22 // indirect github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.13.7 // indirect @@ -152,13 +152,14 @@ require ( github.com/cespare/xxhash/v2 v2.3.0 // indirect github.com/cheggaaa/pb v1.0.29 // indirect github.com/clbanning/mxj v1.8.4 // indirect + github.com/clipperhouse/uax29/v2 v2.2.0 // indirect github.com/cloudsoda/sddl v0.0.0-20250224235906-926454e91efc // indirect - github.com/cncf/xds/go v0.0.0-20251210132809-ee656c7534f5 // indirect + github.com/cncf/xds/go v0.0.0-20260202195803-dba9d589def2 // indirect github.com/coredns/coredns v1.4.0 // indirect github.com/coreos/etcd v3.3.27+incompatible // indirect - github.com/coreos/go-semver v0.3.0 // indirect + github.com/coreos/go-semver v0.3.1 // indirect github.com/coreos/go-systemd v0.0.0-20191104093116-d3cd4ed1dbcf // indirect - github.com/coreos/go-systemd/v22 v22.5.0 // indirect + github.com/coreos/go-systemd/v22 v22.7.0 // indirect github.com/coreos/pkg v0.0.0-20240122114842-bbd7aa9bf6fb // indirect github.com/cpuguy83/go-md2man/v2 v2.0.2 // indirect github.com/cznic/mathutil v0.0.0-20181122101859-297441e03548 // indirect @@ -170,8 +171,8 @@ require ( github.com/djherbis/atime v1.0.0 // indirect github.com/dswarbrick/smart v0.0.0-20190505152634-909a45200d6d // indirect github.com/elastic/gosigar v0.14.2 // indirect - github.com/envoyproxy/go-control-plane/envoy v1.36.0 // indirect - github.com/envoyproxy/protoc-gen-validate v1.3.0 // indirect + github.com/envoyproxy/go-control-plane/envoy v1.37.0 // indirect + github.com/envoyproxy/protoc-gen-validate v1.3.3 // indirect github.com/fatih/color v1.16.0 // indirect github.com/fatih/structs v1.1.0 // indirect github.com/felixge/httpsnoop v1.0.4 // indirect @@ -194,14 +195,14 @@ require ( github.com/go-viper/mapstructure/v2 v2.4.0 // indirect github.com/gogo/protobuf v1.3.2 // indirect github.com/golang-jwt/jwt/v4 v4.5.2 // indirect - github.com/golang-jwt/jwt/v5 v5.3.0 // indirect + github.com/golang-jwt/jwt/v5 v5.3.1 // indirect github.com/golang/glog v1.2.5 // indirect github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da // indirect github.com/golang/protobuf v1.5.4 // indirect github.com/google/flatbuffers v25.2.10+incompatible // indirect github.com/google/go-querystring v1.1.0 // indirect github.com/google/readahead v0.0.0-20161222183148-eaceba169032 // indirect - github.com/google/s2a-go v0.1.8 // indirect + github.com/google/s2a-go v0.1.9 // indirect github.com/googleapis/enterprise-certificate-proxy v0.3.4 // indirect github.com/googleapis/gax-go/v2 v2.14.0 // indirect github.com/gopherjs/gopherjs v1.12.80 // indirect @@ -209,6 +210,7 @@ require ( github.com/gorilla/mux v1.8.1 // indirect github.com/grafana/regexp v0.0.0-20240518133315-a468a5bfb3bc // indirect github.com/grpc-ecosystem/go-grpc-middleware v1.1.0 // indirect + github.com/grpc-ecosystem/grpc-gateway/v2 v2.29.0 // indirect github.com/hashicorp/errwrap v1.1.0 // indirect github.com/hashicorp/go-cleanhttp v0.5.2 // indirect github.com/hashicorp/go-immutable-radix v1.3.1 // indirect @@ -229,7 +231,7 @@ require ( github.com/jmespath/go-jmespath v0.4.0 // indirect github.com/jtolds/gls v4.20.0+incompatible // indirect github.com/jtolio/noiseconn v0.0.0-20230301220541-88105e6c8ac6 // indirect - github.com/klauspost/compress v1.18.0 // indirect + github.com/klauspost/compress v1.18.6 // indirect github.com/klauspost/cpuid v1.3.1 // indirect github.com/klauspost/cpuid/v2 v2.2.5 // indirect github.com/klauspost/pgzip v1.2.5 // indirect @@ -240,7 +242,7 @@ require ( github.com/leodido/go-urn v1.4.0 // indirect github.com/lufia/plan9stats v0.0.0-20211012122336-39d0f177ccd0 // indirect github.com/mattn/go-colorable v0.1.13 // indirect - github.com/mattn/go-runewidth v0.0.15 // indirect + github.com/mattn/go-runewidth v0.0.24 // indirect github.com/miekg/dns v1.1.61 // indirect github.com/minio/highwayhash v1.0.2 // indirect github.com/minio/md5-simd v1.1.1 // indirect @@ -269,7 +271,7 @@ require ( github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect github.com/power-devops/perfstat v0.0.0-20210106213030-5aafc221ea8c // indirect github.com/pquerna/ffjson v0.0.0-20190930134022-aa0246cd15f7 // indirect - github.com/prometheus/procfs v0.15.1 // indirect + github.com/prometheus/procfs v0.20.1 // indirect github.com/rasky/go-xdr v0.0.0-20170124162913-1a41d1a06c93 // indirect github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec // indirect github.com/rivo/uniseg v0.4.7 // indirect @@ -283,7 +285,7 @@ require ( github.com/smartystreets/assertions v1.2.0 // indirect github.com/spacemonkeygo/monkit/v3 v3.0.25-0.20251022131615-eb24eb109368 // indirect github.com/spaolacci/murmur3 v1.1.0 // indirect - github.com/spiffe/go-spiffe/v2 v2.6.0 // indirect + github.com/spiffe/go-spiffe/v2 v2.8.1 // indirect github.com/syndtr/goleveldb v1.0.0 // indirect github.com/tiancaiamao/gp v0.0.0-20221230034425-4025bc8a4d4a // indirect github.com/tidwall/gjson v1.6.7 // indirect @@ -302,33 +304,34 @@ require ( github.com/yusufpapurcu/wmi v1.2.3 // indirect github.com/zeebo/blake3 v0.2.3 // indirect github.com/zeebo/errs v1.4.0 // indirect - go.etcd.io/etcd/api/v3 v3.5.9 // indirect - go.etcd.io/etcd/client/pkg/v3 v3.5.9 // indirect + go.etcd.io/etcd/api/v3 v3.6.12 // indirect + go.etcd.io/etcd/client/pkg/v3 v3.6.12 // indirect go.mongodb.org/mongo-driver v1.17.6 // indirect go.opencensus.io v0.24.0 // indirect go.opentelemetry.io/auto/sdk v1.2.1 // indirect - go.opentelemetry.io/contrib/detectors/gcp v1.39.0 // indirect + go.opentelemetry.io/contrib/detectors/gcp v1.42.0 // indirect go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.68.0 // indirect go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.68.0 // indirect - go.opentelemetry.io/otel v1.43.0 // indirect - go.opentelemetry.io/otel/metric v1.43.0 // indirect - go.opentelemetry.io/otel/sdk v1.43.0 // indirect - go.opentelemetry.io/otel/sdk/metric v1.43.0 // indirect - go.opentelemetry.io/otel/trace v1.43.0 // indirect + go.opentelemetry.io/otel v1.44.0 // indirect + go.opentelemetry.io/otel/metric v1.44.0 // indirect + go.opentelemetry.io/otel/sdk v1.44.0 // indirect + go.opentelemetry.io/otel/sdk/metric v1.44.0 // indirect + go.opentelemetry.io/otel/trace v1.44.0 // indirect go.uber.org/atomic v1.11.0 // indirect go.uber.org/multierr v1.11.0 // indirect - go.yaml.in/yaml/v2 v2.4.3 // indirect + go.yaml.in/yaml/v2 v2.4.4 // indirect golang.org/x/arch v0.11.0 // indirect golang.org/x/exp v0.0.0-20240119083558-1b970713d09a // indirect - golang.org/x/mod v0.33.0 // indirect + golang.org/x/mod v0.36.0 // indirect golang.org/x/time v0.8.0 // indirect - golang.org/x/tools v0.42.0 // indirect + golang.org/x/tools v0.45.0 // indirect golang.org/x/xerrors v0.0.0-20231012003039-104605ab7028 // indirect google.golang.org/genproto v0.0.0-20241118233622-e639e219e697 // indirect - google.golang.org/genproto/googleapis/api v0.0.0-20260120221211-b8f7ae30c516 // indirect - google.golang.org/genproto/googleapis/rpc v0.0.0-20260406210006-6f92a3bedf2d // indirect - google.golang.org/grpc v1.80.0 // indirect - gopkg.in/ini.v1 v1.67.0 // indirect + google.golang.org/genproto/googleapis/api v0.0.0-20260414002931-afd174a4e478 // indirect + google.golang.org/genproto/googleapis/rpc v0.0.0-20260414002931-afd174a4e478 // indirect + google.golang.org/grpc v1.81.1 // indirect + google.golang.org/grpc/stats/opentelemetry v0.0.0-20240907200651-3ffb98b2c93a // indirect + gopkg.in/ini.v1 v1.67.3 // indirect gopkg.in/natefinch/lumberjack.v2 v2.2.1 // indirect gopkg.in/yaml.v2 v2.4.0 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect @@ -344,7 +347,7 @@ require ( replace github.com/minio/minio v0.0.0-20210206053228-97fe57bba92c => github.com/juicedata/minio v0.0.0-20260515071949-69a6cfc9da65 -replace github.com/hanwen/go-fuse/v2 v2.1.1-0.20210611132105-24a1dfe6b4f8 => github.com/juicedata/go-fuse/v2 v2.1.1-0.20260610024748-b44a81936922 +replace github.com/hanwen/go-fuse/v2 => github.com/EngHabu/go-fuse/v2 v2.1.1-0.20260624053043-ef6e8dc3c14e replace github.com/dgrijalva/jwt-go v3.2.0+incompatible => github.com/golang-jwt/jwt v3.2.1+incompatible diff --git a/go.sum b/go.sum index ba397af27758..3e460d509f40 100644 --- a/go.sum +++ b/go.sum @@ -23,6 +23,8 @@ cloud.google.com/go/trace v1.11.2 h1:4ZmaBdL8Ng/ajrgKqY5jfvzqMXbrDcBsUGXOT9aqTtI cloud.google.com/go/trace v1.11.2/go.mod h1:bn7OwXd4pd5rFuAnTrzBuoZ4ax2XQeG3qNgYmfCy0Io= filippo.io/edwards25519 v1.1.0 h1:FNf4tywRC1HmFuKW5xopWpigGjJKiJSV0Cqo0cJWDaA= filippo.io/edwards25519 v1.1.0/go.mod h1:BxyFTGdWcka3PhytdK4V28tE5sGfRvvvRV7EaN4VDT4= +filippo.io/edwards25519 v1.2.0 h1:crnVqOiS4jqYleHd9vaKZ+HKtHfllngJIiOpNpoJsjo= +filippo.io/edwards25519 v1.2.0/go.mod h1:xzAOLCNug/yB62zG1bQ8uziwrIqIuxhctzJT18Q77mc= git.apache.org/thrift.git v0.13.0 h1:/3bz5WZ+sqYArk7MBBBbDufMxKKOA56/6JO6psDpUDY= git.apache.org/thrift.git v0.13.0/go.mod h1:fPE2ZNJGynbRyZ4dJvy6G277gSllfV2HJqblrnkyeyg= gitea.com/davies/xorm v1.0.8-0.20220528043536-552d84d1b34a h1:awR9qREIs6qSnKr/cmSewVwDo74/kQ32x0CDEXUtiB8= @@ -37,6 +39,8 @@ github.com/Azure/azure-sdk-for-go/sdk/azidentity/cache v0.3.2 h1:yz1bePFlP5Vws5+ github.com/Azure/azure-sdk-for-go/sdk/azidentity/cache v0.3.2/go.mod h1:Pa9ZNPuoNu/GztvBSKk9J1cDJW6vk/n0zLtV4mgd8N8= github.com/Azure/azure-sdk-for-go/sdk/internal v1.11.2 h1:9iefClla7iYpfYWdzPCRDozdmndjTm8DXdpCzPajMgA= github.com/Azure/azure-sdk-for-go/sdk/internal v1.11.2/go.mod h1:XtLgD3ZD34DAaVIIAyG3objl5DynM3CQ/vMcbBNJZGI= +github.com/Azure/azure-sdk-for-go/sdk/internal v1.12.0 h1:fhqpLE3UEXi9lPaBRpQ6XuRW0nU7hgg4zlmZZa+a9q4= +github.com/Azure/azure-sdk-for-go/sdk/internal v1.12.0/go.mod h1:7dCRMLwisfRH3dBupKeNCioWYUZ4SS09Z14H+7i8ZoY= github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/storage/armstorage v1.8.1 h1:/Zt+cDPnpC3OVDm/JKLOs7M2DKmLRIIp3XIx9pHHiig= github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/storage/armstorage v1.8.1/go.mod h1:Ng3urmn6dYe8gnbCMoHHVl5APYz2txho3koEkV2o2HA= github.com/Azure/azure-sdk-for-go/sdk/storage/azblob v1.6.4 h1:jWQK1GI+LeGGUKBADtcH2rRqPxYB1Ljwms5gFA2LqrM= @@ -47,14 +51,24 @@ github.com/AzureAD/microsoft-authentication-extensions-for-go/cache v0.1.1 h1:WJ github.com/AzureAD/microsoft-authentication-extensions-for-go/cache v0.1.1/go.mod h1:tCcJZ0uHAmvjsVYzEFivsRTN00oz5BEsRgQHu5JZ9WE= github.com/AzureAD/microsoft-authentication-library-for-go v1.6.0 h1:XRzhVemXdgvJqCH0sFfrBUTnUJSBrBf7++ypk+twtRs= github.com/AzureAD/microsoft-authentication-library-for-go v1.6.0/go.mod h1:HKpQxkWaGLJ+D/5H8QRpyQXA1eKjxkFlOMwck5+33Jk= +github.com/AzureAD/microsoft-authentication-library-for-go v1.7.2 h1:RHK7bS+HQMslb1sZpAokUt+zTVmue0hKSs2C791hhzU= +github.com/AzureAD/microsoft-authentication-library-for-go v1.7.2/go.mod h1:HKpQxkWaGLJ+D/5H8QRpyQXA1eKjxkFlOMwck5+33Jk= github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= github.com/BurntSushi/toml v1.3.2 h1:o7IhLm0Msx3BaB+n3Ag7L8EVlByGnpq14C4YWiu/gL8= github.com/BurntSushi/toml v1.3.2/go.mod h1:CxXYINrC8qIiEnFrOxCa7Jy5BFHlXnUU2pbicEuybxQ= github.com/DataDog/datadog-go v3.2.0+incompatible/go.mod h1:LButxg5PwREeZtORoXG3tL4fMGNddJ+vMq1mwgfaqoQ= github.com/DataDog/zstd v1.5.6 h1:LbEglqepa/ipmmQJUDnSsfvA8e8IStVcGaFWDuxvGOY= github.com/DataDog/zstd v1.5.6/go.mod h1:g4AWEaM3yOg3HYfnJ3YIawPnVdXJh9QME85blwSAmyw= +github.com/EngHabu/go-fuse/v2 v2.1.1-0.20260624004144-146975152cf7 h1:xeVHS9aZZv7lmalxlKBj7X3AuZwe4bvxiXYcsHoEkFc= +github.com/EngHabu/go-fuse/v2 v2.1.1-0.20260624004144-146975152cf7/go.mod h1:xKwi1cF7nXAOBCXujD5ie0ZKsxc8GGSA1rlMJc+8IJs= +github.com/EngHabu/go-fuse/v2 v2.1.1-0.20260624051746-50dbae93aec4 h1:N+ALIK6ZvDtTXJYElmvxb2xLEzlWA1/n56jC05lOLAU= +github.com/EngHabu/go-fuse/v2 v2.1.1-0.20260624051746-50dbae93aec4/go.mod h1:xKwi1cF7nXAOBCXujD5ie0ZKsxc8GGSA1rlMJc+8IJs= +github.com/EngHabu/go-fuse/v2 v2.1.1-0.20260624053043-ef6e8dc3c14e h1:YiDEZO1tvy2jRF/OKaaXiVNgdmQ0XbCxwKq6wPWvAa0= +github.com/EngHabu/go-fuse/v2 v2.1.1-0.20260624053043-ef6e8dc3c14e/go.mod h1:xKwi1cF7nXAOBCXujD5ie0ZKsxc8GGSA1rlMJc+8IJs= github.com/GoogleCloudPlatform/opentelemetry-operations-go/detectors/gcp v1.31.0 h1:DHa2U07rk8syqvCge0QIGMCE1WxGj9njT44GH7zNJLQ= github.com/GoogleCloudPlatform/opentelemetry-operations-go/detectors/gcp v1.31.0/go.mod h1:P4WPRUkOhJC13W//jWpyfJNDAIpvRbAUIYLX/4jtlE0= +github.com/GoogleCloudPlatform/opentelemetry-operations-go/detectors/gcp v1.33.0 h1:l7+6kwRMJNwdCvYdDl7Eax+wzEYHSnNY7zrrfbhDdTA= +github.com/GoogleCloudPlatform/opentelemetry-operations-go/detectors/gcp v1.33.0/go.mod h1:pJTkW8hEUIIi3Pf65lPZOnn4Y81yCllX6IWk2jNXdkM= github.com/GoogleCloudPlatform/opentelemetry-operations-go/exporter/metric v0.48.1 h1:UQ0AhxogsIRZDkElkblfnwjc3IaltCm2HUMvezQaL7s= github.com/GoogleCloudPlatform/opentelemetry-operations-go/exporter/metric v0.48.1/go.mod h1:jyqM3eLpJ3IbIFDTKVz2rF9T/xWGW0rIriGwnz8l9Tk= github.com/GoogleCloudPlatform/opentelemetry-operations-go/internal/cloudmock v0.48.1 h1:oTX4vsorBZo/Zdum6OKPA4o7544hm6smoRv1QjpTwGo= @@ -102,6 +116,8 @@ github.com/armon/go-radix v0.0.0-20180808171621-7fddfc383310/go.mod h1:ufUuZ+zHj github.com/armon/go-radix v1.0.0/go.mod h1:ufUuZ+zHj4x4TnLV4JWEpy2hxWSpsRywHrMgIH9cCH8= github.com/aws/aws-sdk-go-v2 v1.41.5 h1:dj5kopbwUsVUVFgO4Fi5BIT3t4WyqIDjGKCangnV/yY= github.com/aws/aws-sdk-go-v2 v1.41.5/go.mod h1:mwsPRE8ceUUpiTgF7QmQIJ7lgsKUPQOUl3o72QBrE1o= +github.com/aws/aws-sdk-go-v2 v1.42.0 h1:XvXMJTkFQtpBKIWZnmr9ZEOc2InWM2yldjXEJ/bymhA= +github.com/aws/aws-sdk-go-v2 v1.42.0/go.mod h1:27+ACypSLljLAEKsCYOmrjKh83vuTRkuAe9Uv/3A4bg= github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.7.8 h1:eBMB84YGghSocM7PsjmmPffTa+1FBUeNvGvFou6V/4o= github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.7.8/go.mod h1:lyw7GFp3qENLh7kwzf7iMzAxDn+NzjXEAGjKS2UOKqI= github.com/aws/aws-sdk-go-v2/config v1.32.14 h1:opVIRo/ZbbI8OIqSOKmpFaY7IwfFUOCCXBsUpJOwDdI= @@ -112,8 +128,12 @@ github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.18.21 h1:NUS3K4BTDArQqNu2ih7yeD github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.18.21/go.mod h1:YWNWJQNjKigKY1RHVJCuupeWDrrHjRqHm0N9rdrWzYI= github.com/aws/aws-sdk-go-v2/internal/configsources v1.4.21 h1:Rgg6wvjjtX8bNHcvi9OnXWwcE0a2vGpbwmtICOsvcf4= github.com/aws/aws-sdk-go-v2/internal/configsources v1.4.21/go.mod h1:A/kJFst/nm//cyqonihbdpQZwiUhhzpqTsdbhDdRF9c= +github.com/aws/aws-sdk-go-v2/internal/configsources v1.4.29 h1:f3vKqSo13fhTYb+JEcXwXefZQE26I1FB5eTSniU67ko= +github.com/aws/aws-sdk-go-v2/internal/configsources v1.4.29/go.mod h1:MzoLFUArKGpGD+ukmPiTPG1X5x4o6M2kq4v2dr1FiEc= github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.7.21 h1:PEgGVtPoB6NTpPrBgqSE5hE/o47Ij9qk/SEZFbUOe9A= github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.7.21/go.mod h1:p+hz+PRAYlY3zcpJhPwXlLC4C+kqn70WIHwnzAfs6ps= +github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.7.29 h1:RdwIf/CuUsvJX3RgJagbOyotl/cxoLY4xviKuE7p2GY= +github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.7.29/go.mod h1:71wt8W2EgswdZy9Mf9KNnzxZ3TiZlv4caKghPktDOkA= github.com/aws/aws-sdk-go-v2/internal/ini v1.8.6 h1:qYQ4pzQ2Oz6WpQ8T3HvGHnZydA72MnLuFK9tJwmrbHw= github.com/aws/aws-sdk-go-v2/internal/ini v1.8.6/go.mod h1:O3h0IK87yXci+kg6flUKzJnWeziQUKciKrLjcatSNcY= github.com/aws/aws-sdk-go-v2/internal/v4a v1.4.22 h1:rWyie/PxDRIdhNf4DzRk0lvjVOqFJuNnO8WwaIRVxzQ= @@ -138,6 +158,8 @@ github.com/aws/aws-sdk-go-v2/service/sts v1.41.10 h1:p8ogvvLugcR/zLBXTXrTkj0RYBU github.com/aws/aws-sdk-go-v2/service/sts v1.41.10/go.mod h1:60dv0eZJfeVXfbT1tFJinbHrDfSJ2GZl4Q//OSSNAVw= github.com/aws/smithy-go v1.24.2 h1:FzA3bu/nt/vDvmnkg+R8Xl46gmzEDam6mZ1hzmwXFng= github.com/aws/smithy-go v1.24.2/go.mod h1:YE2RhdIuDbA5E5bTdciG9KrW3+TiEONeUWCqxX9i1Fc= +github.com/aws/smithy-go v1.27.1 h1:4T340VFndXtADGF52gYa1POyL7s9E4Z1OeZ1hCscIw8= +github.com/aws/smithy-go v1.27.1/go.mod h1:YE2RhdIuDbA5E5bTdciG9KrW3+TiEONeUWCqxX9i1Fc= github.com/baidubce/bce-sdk-go v0.9.263 h1:tSO+CMS9h+wm+itbyoVy1SUxKO2iNibXn4gtBSC/qeY= github.com/baidubce/bce-sdk-go v0.9.263/go.mod h1:zbYJMQwE4IZuyrJiFO8tO8NbtYiKTFTbwh4eIsqjVdg= github.com/bcicen/jstream v1.0.1 h1:BXY7Cu4rdmc0rhyTVyT3UkxAiX3bnLpKLas9btbH5ck= @@ -173,11 +195,15 @@ github.com/circonus-labs/circonusllhist v0.1.3/go.mod h1:kMXHVDlOchFAehlya5ePtbp github.com/clbanning/mxj v1.8.4 h1:HuhwZtbyvyOw+3Z1AowPkU87JkJUSv751ELWaiTpj8I= github.com/clbanning/mxj v1.8.4/go.mod h1:BVjHeAH+rl9rs6f+QIpeRl0tfu10SXn1pUSa5PVGJng= github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw= +github.com/clipperhouse/uax29/v2 v2.2.0 h1:ChwIKnQN3kcZteTXMgb1wztSgaU+ZemkgWdohwgs8tY= +github.com/clipperhouse/uax29/v2 v2.2.0/go.mod h1:EFJ2TJMRUaplDxHKj1qAEhCtQPW2tJSwu5BF98AuoVM= github.com/cloudsoda/sddl v0.0.0-20250224235906-926454e91efc h1:0xCWmFKBmarCqqqLeM7jFBSw/Or81UEElFqO8MY+GDs= github.com/cloudsoda/sddl v0.0.0-20250224235906-926454e91efc/go.mod h1:uvR42Hb/t52HQd7x5/ZLzZEK8oihrFpgnodIJ1vte2E= github.com/cncf/udpa/go v0.0.0-20191209042840-269d4d468f6f/go.mod h1:M8M6+tZqaGXZJjfX53e64911xZQV5JYwmTeXPW+k8Sc= github.com/cncf/xds/go v0.0.0-20251210132809-ee656c7534f5 h1:6xNmx7iTtyBRev0+D/Tv1FZd4SCg8axKApyNyRsAt/w= github.com/cncf/xds/go v0.0.0-20251210132809-ee656c7534f5/go.mod h1:KdCmV+x/BuvyMxRnYBlmVaq4OLiKW6iRQfvC62cvdkI= +github.com/cncf/xds/go v0.0.0-20260202195803-dba9d589def2 h1:aBangftG7EVZoUb69Os8IaYg++6uMOdKK83QtkkvJik= +github.com/cncf/xds/go v0.0.0-20260202195803-dba9d589def2/go.mod h1:qwXFYgsP6T7XnJtbKlf1HP8AjxZZyzxMmc+Lq5GjlU4= github.com/colinmarc/hdfs/v2 v2.4.0 h1:v6R8oBx/Wu9fHpdPoJJjpGSUxo8NhHIwrwsfhFvU9W0= github.com/colinmarc/hdfs/v2 v2.4.0/go.mod h1:0NAO+/3knbMx6+5pCv+Hcbaz4xn/Zzbn9+WIib2rKVI= github.com/coredns/coredns v1.4.0 h1:RubBkYmkByUqZWWkjRHvNLnUHgkRVqAWgSMmRFvpE1A= @@ -186,10 +212,14 @@ github.com/coreos/etcd v3.3.27+incompatible h1:QIudLb9KeBsE5zyYxd1mjzRSkzLg9Wf9Q github.com/coreos/etcd v3.3.27+incompatible/go.mod h1:uF7uidLiAD3TWHmW31ZFd/JWoc32PjwdhPthX9715RE= github.com/coreos/go-semver v0.3.0 h1:wkHLiw0WNATZnSG7epLsujiMCgPAc9xhjJ4tgnAxmfM= github.com/coreos/go-semver v0.3.0/go.mod h1:nnelYz7RCh+5ahJtPPxZlU+153eP4D4r3EedlOD2RNk= +github.com/coreos/go-semver v0.3.1 h1:yi21YpKnrx1gt5R+la8n5WgS0kCrsPp33dmEyHReZr4= +github.com/coreos/go-semver v0.3.1/go.mod h1:irMmmIw/7yzSRPWryHsK7EYSg09caPQL03VsM8rvUec= github.com/coreos/go-systemd v0.0.0-20191104093116-d3cd4ed1dbcf h1:iW4rZ826su+pqaw19uhpSCzhj44qo35pNgKFGqzDKkU= github.com/coreos/go-systemd v0.0.0-20191104093116-d3cd4ed1dbcf/go.mod h1:F5haX7vjVVG0kc13fIWeqUViNPyEJxv/OmvnBo0Yme4= github.com/coreos/go-systemd/v22 v22.5.0 h1:RrqgGjYQKalulkV8NGVIfkXQf6YYmOyiJKk8iXXhfZs= github.com/coreos/go-systemd/v22 v22.5.0/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSVTIJ3seZv2GcEnc= +github.com/coreos/go-systemd/v22 v22.7.0 h1:LAEzFkke61DFROc7zNLX/WA2i5J8gYqe0rSj9KI28KA= +github.com/coreos/go-systemd/v22 v22.7.0/go.mod h1:xNUYtjHu2EDXbsxz1i41wouACIwT7Ybq9o0BQhMwD0w= github.com/coreos/pkg v0.0.0-20240122114842-bbd7aa9bf6fb h1:GIzvVQ9UkUlOhSDlqmrQAAAUd6R3E+caIisNEyWXvNE= github.com/coreos/pkg v0.0.0-20240122114842-bbd7aa9bf6fb/go.mod h1:E3G3o1h8I7cfcXa63jLwjI0eiQQMgzzUDFVpN/nH/eA= github.com/cpuguy83/go-md2man/v2 v2.0.2 h1:p1EgwI/C7NhT0JmVkwCD2ZBK8j4aeHQX2pMHHBfMQ6w= @@ -234,11 +264,15 @@ github.com/envoyproxy/go-control-plane v0.14.0 h1:hbG2kr4RuFj222B6+7T83thSPqLjwB github.com/envoyproxy/go-control-plane v0.14.0/go.mod h1:NcS5X47pLl/hfqxU70yPwL9ZMkUlwlKxtAohpi2wBEU= github.com/envoyproxy/go-control-plane/envoy v1.36.0 h1:yg/JjO5E7ubRyKX3m07GF3reDNEnfOboJ0QySbH736g= github.com/envoyproxy/go-control-plane/envoy v1.36.0/go.mod h1:ty89S1YCCVruQAm9OtKeEkQLTb+Lkz0k8v9W0Oxsv98= +github.com/envoyproxy/go-control-plane/envoy v1.37.0 h1:u3riX6BoYRfF4Dr7dwSOroNfdSbEPe9Yyl09/B6wBrQ= +github.com/envoyproxy/go-control-plane/envoy v1.37.0/go.mod h1:DReE9MMrmecPy+YvQOAOHNYMALuowAnbjjEMkkWOi6A= github.com/envoyproxy/go-control-plane/ratelimit v0.1.0 h1:/G9QYbddjL25KvtKTv3an9lx6VBE2cnb8wp1vEGNYGI= github.com/envoyproxy/go-control-plane/ratelimit v0.1.0/go.mod h1:Wk+tMFAFbCXaJPzVVHnPgRKdUdwW/KdbRt94AzgRee4= github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c= github.com/envoyproxy/protoc-gen-validate v1.3.0 h1:TvGH1wof4H33rezVKWSpqKz5NXWg5VPuZ0uONDT6eb4= github.com/envoyproxy/protoc-gen-validate v1.3.0/go.mod h1:HvYl7zwPa5mffgyeTUHA9zHIH36nmrm7oCbo4YKoSWA= +github.com/envoyproxy/protoc-gen-validate v1.3.3 h1:MVQghNeW+LZcmXe7SY1V36Z+WFMDjpqGAGacLe2T0ds= +github.com/envoyproxy/protoc-gen-validate v1.3.3/go.mod h1:TsndJ/ngyIdQRhMcVVGDDHINPLWB7C82oDArY51KfB0= github.com/erikdubbelboer/gspt v0.0.0-20210805194459-ce36a5128377 h1:gT+RM6gdTIAzMT7HUvmT5mL8SyG8Wx7iS3+L0V34Km4= github.com/erikdubbelboer/gspt v0.0.0-20210805194459-ce36a5128377/go.mod h1:v6o7m/E9bfvm79dE1iFiF+3T7zLBnrjYjkWMa1J+Hv0= github.com/fatih/color v1.7.0/go.mod h1:Zm6kSWBoL9eyXnKyktHP6abPY2pDugNf5KwzbycvMj4= @@ -321,6 +355,8 @@ github.com/golang-jwt/jwt/v4 v4.5.2/go.mod h1:m21LjoU+eqJr34lmDMbreY2eSTRJ1cv77w github.com/golang-jwt/jwt/v5 v5.2.3/go.mod h1:pqrtFR0X4osieyHYxtmOUWsAWrfe1Q5UVIyoH402zdk= github.com/golang-jwt/jwt/v5 v5.3.0 h1:pv4AsKCKKZuqlgs5sUmn4x8UlGa0kEVt/puTpKx9vvo= github.com/golang-jwt/jwt/v5 v5.3.0/go.mod h1:fxCRLWMO43lRc8nhHWY6LGqRcf+1gQWArsqaEUEa5bE= +github.com/golang-jwt/jwt/v5 v5.3.1 h1:kYf81DTWFe7t+1VvL7eS+jKFVWaUnK9cB1qbwn63YCY= +github.com/golang-jwt/jwt/v5 v5.3.1/go.mod h1:fxCRLWMO43lRc8nhHWY6LGqRcf+1gQWArsqaEUEa5bE= github.com/golang-sql/civil v0.0.0-20190719163853-cb61b32ac6fe/go.mod h1:8vg3r2VgvsThLBIFL93Qb5yWzgyZWhEmBwUJWevAkK0= github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q= github.com/golang/glog v1.2.5 h1:DrW6hGnjIhtvhOIiAKT6Psh/Kd/ldepEa81DKeiRJ5I= @@ -344,6 +380,8 @@ github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6 github.com/golang/snappy v0.0.0-20180518054509-2e65f85255db/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q= github.com/golang/snappy v0.0.4 h1:yAGX7huGHXlcLOEtBnF4w7FQwA26wojNCwOYAEhLjQM= github.com/golang/snappy v0.0.4/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q= +github.com/golang/snappy v1.0.0 h1:Oy607GVXHs7RtbggtPBnr2RmDArIsAefDwvrdWvRhGs= +github.com/golang/snappy v1.0.0/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q= github.com/google/btree v0.0.0-20180813153112-4030bb1f1f0c/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ= github.com/google/btree v1.1.2 h1:xf4v41cLI2Z6FxbKm+8Bu+m8ifhj15JuZ9sa0jZCMUU= github.com/google/btree v1.1.2/go.mod h1:qOPhT0dTNdNzV6Z/lhRX0YXUafgPLFUh+gZMl761Gm4= @@ -373,6 +411,8 @@ github.com/google/readahead v0.0.0-20161222183148-eaceba169032 h1:6Be3nkuJFyRfCg github.com/google/readahead v0.0.0-20161222183148-eaceba169032/go.mod h1:qYysrqQXuV4tzsizt4oOQ6mrBZQ0xnQXP3ylXX8Jk5Y= github.com/google/s2a-go v0.1.8 h1:zZDs9gcbt9ZPLV0ndSyQk6Kacx2g/X+SKYovpnz3SMM= github.com/google/s2a-go v0.1.8/go.mod h1:6iNWHTpQ+nfNRN5E00MSdfDwVesa8hhS32PhPO8deJA= +github.com/google/s2a-go v0.1.9 h1:LGD7gtMgezd8a/Xak7mEWL0PjoTQFvpRudN895yqKW0= +github.com/google/s2a-go v0.1.9/go.mod h1:YA0Ei2ZQL3acow2O62kdp9UlnvMmU7kA6Eutn0dXayM= github.com/google/uuid v1.1.1/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/google/uuid v1.1.2/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= @@ -400,6 +440,10 @@ github.com/grafana/regexp v0.0.0-20240518133315-a468a5bfb3bc h1:GN2Lv3MGO7AS6PrR github.com/grafana/regexp v0.0.0-20240518133315-a468a5bfb3bc/go.mod h1:+JKpmjMGhpgPL+rXZ5nsZieVzvarn86asRlBg4uNGnk= github.com/grpc-ecosystem/go-grpc-middleware v1.1.0 h1:THDBEeQ9xZ8JEaCLyLQqXMMdRqNr0QAUJTIkQAUtFjg= github.com/grpc-ecosystem/go-grpc-middleware v1.1.0/go.mod h1:f5nM7jw/oeRSadq3xCzHAvxcr8HZnzsqU6ILg/0NiiE= +github.com/grpc-ecosystem/grpc-gateway v1.16.0 h1:gmcG1KaJ57LophUzW0Hy8NmPhnMZb4M0+kPpLofRdBo= +github.com/grpc-ecosystem/grpc-gateway v1.16.0/go.mod h1:BDjrQk3hbvj6Nolgz8mAMFbcEtjT1g+wF4CSlocrBnw= +github.com/grpc-ecosystem/grpc-gateway/v2 v2.29.0 h1:5VipnvEpbqr2gA2VbM+nYVbkIF28c5ZQfqCBQ5g2xfk= +github.com/grpc-ecosystem/grpc-gateway/v2 v2.29.0/go.mod h1:Hyl3n6Twe1hvtd9XUXDec4pTvgMSEixRuQKPTMH2bNs= github.com/hashicorp/consul/api v1.29.2 h1:aYyRn8EdE2mSfG14S1+L9Qkjtz8RzmaWh6AcNGRNwPw= github.com/hashicorp/consul/api v1.29.2/go.mod h1:0YObcaLNDSbtlgzIRtmRXI1ZkeuK0trCBxwZQ4MYnIk= github.com/hashicorp/consul/proto-public v0.6.2 h1:+DA/3g/IiKlJZb88NBn0ZgXrxJp2NlvCZdEyl+qxvL0= @@ -519,6 +563,8 @@ github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+o github.com/klauspost/compress v1.11.7/go.mod h1:aoV0uJVorq1K+umq18yTdKaF57EivdYsUV+/s2qKfXs= github.com/klauspost/compress v1.18.0 h1:c/Cqfb0r+Yi+JtIEq73FWXVkRonBlf0CRNYc8Zttxdo= github.com/klauspost/compress v1.18.0/go.mod h1:2Pp+KzxcywXVXMr50+X0Q/Lsb43OQHYWRCY2AiWywWQ= +github.com/klauspost/compress v1.18.6 h1:2jupLlAwFm95+YDR+NwD2MEfFO9d4z4Prjl1XXDjuao= +github.com/klauspost/compress v1.18.6/go.mod h1:cwPg85FWrGar70rWktvGQj8/hthj3wpl0PGDogxkrSQ= github.com/klauspost/cpuid v1.2.3/go.mod h1:Pj4uuM528wm8OyEC2QMXAi2YiTZ96dNQPGgoMS4s3ek= github.com/klauspost/cpuid v1.3.1 h1:5JNjFYYQrZeKRJ0734q51WCEEn2huer72Dc7K+R/b6s= github.com/klauspost/cpuid v1.3.1/go.mod h1:bYW4mA6ZgKPob1/Dlai2LviZJO7KGI3uoWLd42rAQw4= @@ -567,6 +613,8 @@ github.com/mattn/go-runewidth v0.0.4/go.mod h1:LwmH8dsx7+W8Uxz3IHJYH5QSwggIsqBzp github.com/mattn/go-runewidth v0.0.13/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w= github.com/mattn/go-runewidth v0.0.15 h1:UNAjwbU9l54TA3KzvqLGxwWjHmMgBUVhBiTjelZgg3U= github.com/mattn/go-runewidth v0.0.15/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w= +github.com/mattn/go-runewidth v0.0.24 h1:cpokDiIn0MGnhdHwuWnJBITySJ20QyNGnY2kR/ay2DU= +github.com/mattn/go-runewidth v0.0.24/go.mod h1:XBkDxAl56ILZc9knddidhrOlY5R/pDhgLpndooCuJAs= github.com/mattn/go-sqlite3 v1.14.0/go.mod h1:JIl7NbARA7phWnGvh0LKTyg7S9BA+6gx71ShQilpsus= github.com/mattn/go-sqlite3 v1.14.24 h1:tpSp2G2KyMnnQu99ngJ47EIkWVmliIizyZBfPrBWDRM= github.com/mattn/go-sqlite3 v1.14.24/go.mod h1:Uh1q+B4BYcTPb+yiD3kU8Ct7aC0hY9fxUwlHK0RXw+Y= @@ -706,6 +754,8 @@ github.com/prometheus/procfs v0.0.2/go.mod h1:TjEm7ze935MbeOT/UhFTIMYKhuLP4wbCsT github.com/prometheus/procfs v0.0.8/go.mod h1:7Qr8sr6344vo1JqZ6HhLceV9o3AJ1Ff+GxbHq6oeK9A= github.com/prometheus/procfs v0.15.1 h1:YagwOFzUgYfKKHX6Dr+sHT7km/hxC76UB0learggepc= github.com/prometheus/procfs v0.15.1/go.mod h1:fB45yRUv8NstnjriLhBQLuOUt+WW4BsoGhij/e3PBqk= +github.com/prometheus/procfs v0.20.1 h1:XwbrGOIplXW/AU3YhIhLODXMJYyC1isLFfYCsTEycfc= +github.com/prometheus/procfs v0.20.1/go.mod h1:o9EMBZGRyvDrSPH1RqdxhojkuXstoe4UlK79eF5TGGo= github.com/prometheus/prometheus v0.54.1 h1:vKuwQNjnYN2/mDoWfHXDhAsz/68q/dQDb+YbcEqU7MQ= github.com/prometheus/prometheus v0.54.1/go.mod h1:xlLByHhk2g3ycakQGrMaU8K7OySZx98BzeCR99991NY= github.com/qingstor/qingstor-sdk-go/v4 v4.4.1 h1:DeoKecl+Ls8GhfXVWB0mQ0Qiy1HtmzgFCkeAi9v7ydw= @@ -756,6 +806,8 @@ github.com/sirupsen/logrus v1.8.0/go.mod h1:4GuYW9TZmE769R5STWrRakJc4UqQ3+QQ95fy github.com/sirupsen/logrus v1.9.0/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ= github.com/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ= github.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ= +github.com/sirupsen/logrus v1.9.4 h1:TsZE7l11zFCLZnZ+teH4Umoq5BhEIfIzfRDZ1Uzql2w= +github.com/sirupsen/logrus v1.9.4/go.mod h1:ftWc9WdOfJ0a92nsE2jF5u5ZwH8Bv2zdeOC42RjbV2g= github.com/smartystreets/assertions v0.0.0-20180927180507-b2de0cb4f26d/go.mod h1:OnSkiWE9lh6wB0YB77sQom3nweQdgAjqCqsofrRNTgc= github.com/smartystreets/assertions v1.2.0 h1:42S6lae5dvLc7BrLu/0ugRtcFVjoJNMC/N3yZFZkDFs= github.com/smartystreets/assertions v1.2.0/go.mod h1:tcbTF8ujkAEcZ8TElKY+i30BzYlVhC/LOxJk7iOWnoo= @@ -772,6 +824,8 @@ github.com/spf13/cobra v0.0.3/go.mod h1:1l0Ry5zgKvJasoi3XT1TypsSe7PqH0Sj9dhYf7v3 github.com/spf13/pflag v1.0.1/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4= github.com/spiffe/go-spiffe/v2 v2.6.0 h1:l+DolpxNWYgruGQVV0xsfeya3CsC7m8iBzDnMpsbLuo= github.com/spiffe/go-spiffe/v2 v2.6.0/go.mod h1:gm2SeUoMZEtpnzPNs2Csc0D/gX33k1xIx7lEzqblHEs= +github.com/spiffe/go-spiffe/v2 v2.8.1 h1:eXZMLsu+3MLEPJyGJkolqtVrteZfQdUpOWj6LTiDl/E= +github.com/spiffe/go-spiffe/v2 v2.8.1/go.mod h1:47Q0Q9/AqGha8QLHp+kxpH4Wca7X7EnOtlIJy3mxZ3U= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= @@ -867,10 +921,16 @@ go.etcd.io/etcd v3.3.27+incompatible h1:5hMrpf6REqTHV2LW2OclNpRtxI0k9ZplMemJsMSW go.etcd.io/etcd v3.3.27+incompatible/go.mod h1:yaeTdrJi5lOmYerz05bd8+V7KubZs8YSFZfzsF9A6aI= go.etcd.io/etcd/api/v3 v3.5.9 h1:4wSsluwyTbGGmyjJktOf3wFQoTBIURXHnq9n/G/JQHs= go.etcd.io/etcd/api/v3 v3.5.9/go.mod h1:uyAal843mC8uUVSLWz6eHa/d971iDGnCRpmKd2Z+X8k= +go.etcd.io/etcd/api/v3 v3.6.12 h1:OLOZUKEuAA36TR48F0cIaa8FdzrWygjyfrJxXg4iDgs= +go.etcd.io/etcd/api/v3 v3.6.12/go.mod h1:p14EIQXHbuOQbVvL/WEes5uqKnxP9AgKJgpjbMVvzvE= go.etcd.io/etcd/client/pkg/v3 v3.5.9 h1:oidDC4+YEuSIQbsR94rY9gur91UPL6DnxDCIYd2IGsE= go.etcd.io/etcd/client/pkg/v3 v3.5.9/go.mod h1:y+CzeSmkMpWN2Jyu1npecjB9BBnABxGM4pN8cGuJeL4= +go.etcd.io/etcd/client/pkg/v3 v3.6.12 h1:36zzB+pQOdHbhN+kH2iJz/K8bJn0ZLtLfPPO7jozTDo= +go.etcd.io/etcd/client/pkg/v3 v3.6.12/go.mod h1:hh2+ZXtfLzs3o6mn92ntgNPBrTJJOvXqICM5g3L3DMY= go.etcd.io/etcd/client/v3 v3.5.9 h1:r5xghnU7CwbUxD/fbUtRyJGaYNfDun8sp/gTr1hew6E= go.etcd.io/etcd/client/v3 v3.5.9/go.mod h1:i/Eo5LrZ5IKqpbtpPDuaUnDOUv471oDg8cjQaUr2MbA= +go.etcd.io/etcd/client/v3 v3.6.12 h1:kMSP6JcPZMqSJiX+TXdUIBU/4eXEZWBAaui4VihMbIc= +go.etcd.io/etcd/client/v3 v3.6.12/go.mod h1:CMs6fJWYiZQk4ytFjd4lE1diOvvRMmtbbn/alZXd3dQ= go.mongodb.org/mongo-driver v1.17.6 h1:87JUG1wZfWsr6rIz3ZmpH90rL5tea7O3IHuSwHUpsss= go.mongodb.org/mongo-driver v1.17.6/go.mod h1:Hy04i7O2kC4RS06ZrhPRqj/u4DTYkFDAAccj+rVKqgQ= go.opencensus.io v0.24.0 h1:y73uSU6J157QMP2kn2r30vwW1A2W2WFwSCGnAVxeaD0= @@ -879,22 +939,34 @@ go.opentelemetry.io/auto/sdk v1.2.1 h1:jXsnJ4Lmnqd11kwkBV2LgLoFMZKizbCi5fNZ/ipaZ go.opentelemetry.io/auto/sdk v1.2.1/go.mod h1:KRTj+aOaElaLi+wW1kO/DZRXwkF4C5xPbEe3ZiIhN7Y= go.opentelemetry.io/contrib/detectors/gcp v1.39.0 h1:kWRNZMsfBHZ+uHjiH4y7Etn2FK26LAGkNFw7RHv1DhE= go.opentelemetry.io/contrib/detectors/gcp v1.39.0/go.mod h1:t/OGqzHBa5v6RHZwrDBJ2OirWc+4q/w2fTbLZwAKjTk= +go.opentelemetry.io/contrib/detectors/gcp v1.42.0 h1:kpt2PEJuOuqYkPcktfJqWWDjTEd/FNgrxcniL7kQrXQ= +go.opentelemetry.io/contrib/detectors/gcp v1.42.0/go.mod h1:W9zQ439utxymRrXsUOzZbFX4JhLxXU4+ZnCt8GG7yA8= go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.68.0 h1:0Qx7VGBacMm9ZENQ7TnNObTYI4ShC+lHI16seduaxZo= go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.68.0/go.mod h1:Sje3i3MjSPKTSPvVWCaL8ugBzJwik3u4smCjUeuupqg= go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.68.0 h1:CqXxU8VOmDefoh0+ztfGaymYbhdB/tT3zs79QaZTNGY= go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.68.0/go.mod h1:BuhAPThV8PBHBvg8ZzZ/Ok3idOdhWIodywz2xEcRbJo= go.opentelemetry.io/otel v1.43.0 h1:mYIM03dnh5zfN7HautFE4ieIig9amkNANT+xcVxAj9I= go.opentelemetry.io/otel v1.43.0/go.mod h1:JuG+u74mvjvcm8vj8pI5XiHy1zDeoCS2LB1spIq7Ay0= +go.opentelemetry.io/otel v1.44.0 h1:JjwHmHpA4iZ3wBxluu2fbbE7j4kqlE8jXyAyPXH7HqU= +go.opentelemetry.io/otel v1.44.0/go.mod h1:BMgjTHL9WPRlRjL2oZCBTL4whCGtXch2H4BhOPIAyYc= go.opentelemetry.io/otel/exporters/stdout/stdoutmetric v1.29.0 h1:WDdP9acbMYjbKIyJUhTvtzj601sVJOqgWdUxSdR/Ysc= go.opentelemetry.io/otel/exporters/stdout/stdoutmetric v1.29.0/go.mod h1:BLbf7zbNIONBLPwvFnwNHGj4zge8uTCM/UPIVW1Mq2I= go.opentelemetry.io/otel/metric v1.43.0 h1:d7638QeInOnuwOONPp4JAOGfbCEpYb+K6DVWvdxGzgM= go.opentelemetry.io/otel/metric v1.43.0/go.mod h1:RDnPtIxvqlgO8GRW18W6Z/4P462ldprJtfxHxyKd2PY= +go.opentelemetry.io/otel/metric v1.44.0 h1:1w0gILTcHdr3YI+ixLyjemwrVnsMURbTZFrSYCdDdmc= +go.opentelemetry.io/otel/metric v1.44.0/go.mod h1:8O7hanEPBNgEMmybD3s2VBKcgWOCsA6tzHBPODAiquo= go.opentelemetry.io/otel/sdk v1.43.0 h1:pi5mE86i5rTeLXqoF/hhiBtUNcrAGHLKQdhg4h4V9Dg= go.opentelemetry.io/otel/sdk v1.43.0/go.mod h1:P+IkVU3iWukmiit/Yf9AWvpyRDlUeBaRg6Y+C58QHzg= +go.opentelemetry.io/otel/sdk v1.44.0 h1:nHYwb9lK+fJPU/dnT6s7W7Z8itMWyqrnVfbheVYrZ58= +go.opentelemetry.io/otel/sdk v1.44.0/go.mod h1:Osuydd3Se74nqjAKxid74N5eC+jfEqfTegHRnq58oK0= go.opentelemetry.io/otel/sdk/metric v1.43.0 h1:S88dyqXjJkuBNLeMcVPRFXpRw2fuwdvfCGLEo89fDkw= go.opentelemetry.io/otel/sdk/metric v1.43.0/go.mod h1:C/RJtwSEJ5hzTiUz5pXF1kILHStzb9zFlIEe85bhj6A= +go.opentelemetry.io/otel/sdk/metric v1.44.0 h1:3LlKgI+VjbVsjNRFZJZAJ30WjXC5VkNRks6si09iEfI= +go.opentelemetry.io/otel/sdk/metric v1.44.0/go.mod h1:5B5pMARnXxKhltooO4xUuCBorl65a4EpnTalObqOigA= go.opentelemetry.io/otel/trace v1.43.0 h1:BkNrHpup+4k4w+ZZ86CZoHHEkohws8AY+WTX09nk+3A= go.opentelemetry.io/otel/trace v1.43.0/go.mod h1:/QJhyVBUUswCphDVxq+8mld+AvhXZLhe+8WVFxiFff0= +go.opentelemetry.io/otel/trace v1.44.0 h1:jxF5CsGYCe74MCRx2X4g7WsY/VBKRqqpNvXlX/6gtIk= +go.opentelemetry.io/otel/trace v1.44.0/go.mod h1:oLl1jrMQAVo6v3GAggN+1VH9VIz9iUSvW53sW1Q8PIE= go.uber.org/atomic v1.4.0/go.mod h1:gD2HeocX3+yG+ygLZcrzQJaqmWj9AIm7n08wl/qW/PE= go.uber.org/atomic v1.6.0/go.mod h1:sABNBOSYdrvTF6hTgEIbc7YasKWGhgEQZyfxyTvoXHQ= go.uber.org/atomic v1.7.0/go.mod h1:fEN4uk6kAWBTFdckzkM89CLk9XfWZrxpCo0nPH17wJc= @@ -915,8 +987,12 @@ go.uber.org/zap v1.10.0/go.mod h1:vwi/ZaCAaUcBkycHslxD9B2zi4UTXhF60s6SWpuDF0Q= go.uber.org/zap v1.19.0/go.mod h1:xg/QME4nWcxGxrpdeYfq7UvYrLh66cuVKdrbD1XF/NI= go.uber.org/zap v1.27.0 h1:aJMhYGrd5QSmlpLMr2MftRKl7t8J8PTZPA732ud/XR8= go.uber.org/zap v1.27.0/go.mod h1:GB2qFLM7cTU87MWRP2mPIjqfIDnGu+VIO4V/SdhGo2E= +go.uber.org/zap v1.28.0 h1:IZzaP1Fv73/T/pBMLk4VutPl36uNC+OSUh3JLG3FIjo= +go.uber.org/zap v1.28.0/go.mod h1:rDLpOi171uODNm/mxFcuYWxDsqWSAVkFdX4XojSKg/Q= go.yaml.in/yaml/v2 v2.4.3 h1:6gvOSjQoTB3vt1l+CU+tSyi/HOjfOjRLJ4YwYZGwRO0= go.yaml.in/yaml/v2 v2.4.3/go.mod h1:zSxWcmIDjOzPXpjlTTbAsKokqkDNAVtZO0WOMiT90s8= +go.yaml.in/yaml/v2 v2.4.4 h1:tuyd0P+2Ont/d6e2rl3be67goVK4R6deVxCUX5vyPaQ= +go.yaml.in/yaml/v2 v2.4.4/go.mod h1:gMZqIpDtDqOfM0uNfy0SkpRhvUryYH0Z6wdMYcacYXQ= golang.org/x/arch v0.0.0-20201008161808-52c3e6f60cff/go.mod h1:flIaEI6LNU6xOCD5PaJvn9wGP0agmIOqjrtsKGRguv4= golang.org/x/arch v0.11.0 h1:KXV8WWKCXm6tRpLirl2szsO5j/oOODwZf4hATmGVNs4= golang.org/x/arch v0.11.0/go.mod h1:FEVrYAQjsQXMVJ1nsMoVVXPZg6p2JE2mx8psSWTDQys= @@ -937,6 +1013,8 @@ golang.org/x/crypto v0.6.0/go.mod h1:OFC/31mSvZgRz0V1QTNCzfAI1aIRzbiufJtkMIlEp58 golang.org/x/crypto v0.18.0/go.mod h1:R0j02AL6hcrfOiy9T4ZYp/rcWeMxM3L6QYxlOuEG1mg= golang.org/x/crypto v0.49.0 h1:+Ng2ULVvLHnJ/ZFEq4KdcDd/cfjrrjjNSXNzxg0Y4U4= golang.org/x/crypto v0.49.0/go.mod h1:ErX4dUh2UM+CFYiXZRTcMpEcN8b/1gxEuv3nODoYtCA= +golang.org/x/crypto v0.51.0 h1:IBPXwPfKxY7cWQZ38ZCIRPI50YLeevDLlLnyC5wRGTI= +golang.org/x/crypto v0.51.0/go.mod h1:8AdwkbraGNABw2kOX6YFPs3WM22XqI4EXEd8g+x7Oc8= golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= golang.org/x/exp v0.0.0-20240119083558-1b970713d09a h1:Q8/wZp0KX97QFTc2ywcOE0YRjZPVIx+MXInMzdvQqcA= golang.org/x/exp v0.0.0-20240119083558-1b970713d09a/go.mod h1:idGWGoKP1toJGkd5/ig9ZLuPcZBC3ewk7SzmH0uou08= @@ -950,6 +1028,7 @@ golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91 golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= golang.org/x/mod v0.33.0 h1:tHFzIWbBifEmbwtGz65eaWyGiGZatSrT9prnU8DbVL8= golang.org/x/mod v0.33.0/go.mod h1:swjeQEj+6r7fODbD2cqrnje9PnziFuw4bmLbBZFrQ5w= +golang.org/x/mod v0.36.0/go.mod h1:moc6ELqsWcOw5Ef3xVprK5ul/MvtVvkIXLziUOICjUQ= golang.org/x/net v0.0.0-20180218175443-cbe0f9307d01/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= @@ -977,9 +1056,13 @@ golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg= golang.org/x/net v0.20.0/go.mod h1:z8BVo6PvndSri0LbOE3hAn0apkU+1YvI6E70E9jsnvY= golang.org/x/net v0.52.0 h1:He/TN1l0e4mmR3QqHMT2Xab3Aj3L9qjbhRm78/6jrW0= golang.org/x/net v0.52.0/go.mod h1:R1MAz7uMZxVMualyPXb+VaqGSa3LIaUqk0eEt3w36Sw= +golang.org/x/net v0.54.0 h1:2zJIZAxAHV/OHCDTCOHAYehQzLfSXuf/5SoL/Dv6w/w= +golang.org/x/net v0.54.0/go.mod h1:Sj4oj8jK6XmHpBZU/zWHw3BV3abl4Kvi+Ut7cQcY+cQ= golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= golang.org/x/oauth2 v0.34.0 h1:hqK/t4AKgbqWkdkcAeI8XLmbK+4m4G5YeQRrmiotGlw= golang.org/x/oauth2 v0.34.0/go.mod h1:lzm5WQJQwKZ3nwavOZ3IS5Aulzxi68dUSgRHujetwEA= +golang.org/x/oauth2 v0.36.0 h1:peZ/1z27fi9hUOFCAZaHyrpWG5lwe0RJEEEeH0ThlIs= +golang.org/x/oauth2 v0.36.0/go.mod h1:YDBUJMTkDnJS+A4BP4eZBjCqtokkg1hODuPjwiGPO7Q= golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= @@ -992,6 +1075,8 @@ golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJ golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.20.0 h1:e0PTpb7pjO8GAtTs2dQ6jYa5BWYlMuX047Dco/pItO4= golang.org/x/sync v0.20.0/go.mod h1:9xrNwdLfx4jkKbNva9FpL6vEN7evnE43NNNJQ2LF3+0= +golang.org/x/sync v0.21.0 h1:HLII4xRRTtCRkxYp4HNFF0Js/Og6q2i++KXbg0gHCwM= +golang.org/x/sync v0.21.0/go.mod h1:9xrNwdLfx4jkKbNva9FpL6vEN7evnE43NNNJQ2LF3+0= golang.org/x/sys v0.0.0-20180807162357-acbc56fc7007/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20180810173357-98c5dad5d1a0/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20180823144017-11551d06cbcc/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= @@ -1037,6 +1122,8 @@ golang.org/x/sys v0.16.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/sys v0.29.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/sys v0.42.0 h1:omrd2nAlyT5ESRdCLYdm3+fMfNFE/+Rf4bDIQImRJeo= golang.org/x/sys v0.42.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw= +golang.org/x/sys v0.45.0 h1:dO4czNzziLiiXplLQgBCEpCvXQ3dnkn0SdaZSYdQ+FY= +golang.org/x/sys v0.45.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw= 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.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k= @@ -1044,6 +1131,8 @@ golang.org/x/term v0.8.0/go.mod h1:xPskH00ivmX89bAKVGSKKtLOWNx2+17Eiy94tnKShWo= golang.org/x/term v0.16.0/go.mod h1:yn7UURbUtPyrVJPGPq404EukNFxcm/foM+bV/bfcDsY= golang.org/x/term v0.41.0 h1:QCgPso/Q3RTJx2Th4bDLqML4W6iJiaXFq2/ftQF13YU= golang.org/x/term v0.41.0/go.mod h1:3pfBgksrReYfZ5lvYM0kSO0LIkAl4Yl2bXOkKP7Ec2A= +golang.org/x/term v0.43.0 h1:S4RLU2sB31O/NCl+zFN9Aru9A/Cq2aqKpTZJ6B+DwT4= +golang.org/x/term v0.43.0/go.mod h1:lrhlHNdQJHO+1qVYiHfFKVuVioJIheAc3fBSMFYEIsk= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= @@ -1054,6 +1143,8 @@ golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8= golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= golang.org/x/text v0.35.0 h1:JOVx6vVDFokkpaq1AEptVzLTpDe9KGpj5tR4/X+ybL8= golang.org/x/text v0.35.0/go.mod h1:khi/HExzZJ2pGnjenulevKNX1W67CUy0AsXcNubPGCA= +golang.org/x/text v0.38.0 h1:sXmwo9DwP3OK9EZ7PqAdaooSGozfl/3a6/xJcbzPRhE= +golang.org/x/text v0.38.0/go.mod h1:YXZt3QhHUKYT53r2lLKFIVi6Ao1jdzrTR/KQ09qyxF4= golang.org/x/time v0.8.0 h1:9i3RxcPv3PZnitoVGMPDKZSq1xW1gK1Xy3ArNOGZfEg= golang.org/x/time v0.8.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM= golang.org/x/tools v0.0.0-20180221164845-07fd8470d635/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= @@ -1075,6 +1166,7 @@ golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU= golang.org/x/tools v0.42.0 h1:uNgphsn75Tdz5Ji2q36v/nsFSfR/9BRFvqhGBaJGd5k= golang.org/x/tools v0.42.0/go.mod h1:Ma6lCIwGZvHK6XtgbswSoWroEkhugApmsXyrUmBhfr0= +golang.org/x/tools v0.45.0/go.mod h1:LuUGqqaXcXMEFEruIVJVm5mgDD8vww/z/SR1gQ4uE/0= golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= @@ -1094,8 +1186,12 @@ google.golang.org/genproto v0.0.0-20241118233622-e639e219e697 h1:ToEetK57OidYuqD google.golang.org/genproto v0.0.0-20241118233622-e639e219e697/go.mod h1:JJrvXBWRZaFMxBufik1a4RpFw4HhgVtBBWQeQgUj2cc= google.golang.org/genproto/googleapis/api v0.0.0-20260120221211-b8f7ae30c516 h1:vmC/ws+pLzWjj/gzApyoZuSVrDtF1aod4u/+bbj8hgM= google.golang.org/genproto/googleapis/api v0.0.0-20260120221211-b8f7ae30c516/go.mod h1:p3MLuOwURrGBRoEyFHBT3GjUwaCQVKeNqqWxlcISGdw= +google.golang.org/genproto/googleapis/api v0.0.0-20260414002931-afd174a4e478 h1:yQugLulqltosq0B/f8l4w9VryjV+N/5gcW0jQ3N8Qec= +google.golang.org/genproto/googleapis/api v0.0.0-20260414002931-afd174a4e478/go.mod h1:C6ADNqOxbgdUUeRTU+LCHDPB9ttAMCTff6auwCVa4uc= google.golang.org/genproto/googleapis/rpc v0.0.0-20260406210006-6f92a3bedf2d h1:wT2n40TBqFY6wiwazVK9/iTWbsQrgk5ZfCSVFLO9LQA= google.golang.org/genproto/googleapis/rpc v0.0.0-20260406210006-6f92a3bedf2d/go.mod h1:4Hqkh8ycfw05ld/3BWL7rJOSfebL2Q+DVDeRgYgxUU8= +google.golang.org/genproto/googleapis/rpc v0.0.0-20260414002931-afd174a4e478 h1:RmoJA1ujG+/lRGNfUnOMfhCy5EipVMyvUE+KNbPbTlw= +google.golang.org/genproto/googleapis/rpc v0.0.0-20260414002931-afd174a4e478/go.mod h1:4Hqkh8ycfw05ld/3BWL7rJOSfebL2Q+DVDeRgYgxUU8= google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c= google.golang.org/grpc v1.23.0/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg= google.golang.org/grpc v1.25.1/go.mod h1:c3i+UQWmh7LiEpx4sFZnkU36qjEYZ0imhYfXVyQciAY= @@ -1103,6 +1199,10 @@ google.golang.org/grpc v1.27.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8 google.golang.org/grpc v1.33.2/go.mod h1:JMHMWHQWaTccqQQlmk3MJZS+GWXOdAesneDmEnv2fbc= google.golang.org/grpc v1.80.0 h1:Xr6m2WmWZLETvUNvIUmeD5OAagMw3FiKmMlTdViWsHM= google.golang.org/grpc v1.80.0/go.mod h1:ho/dLnxwi3EDJA4Zghp7k2Ec1+c2jqup0bFkw07bwF4= +google.golang.org/grpc v1.81.1 h1:VnnIIZ88UzOOKLukQi+ImGz8O1Wdp8nAGGnvOfEIWQQ= +google.golang.org/grpc v1.81.1/go.mod h1:xGH9GfzOyMTGIOXBJmXt+BX/V0kcdQbdcuwQ/zNw42I= +google.golang.org/grpc/stats/opentelemetry v0.0.0-20240907200651-3ffb98b2c93a h1:UIpYSuWdWHSzjwcAFRLjKcPXFZVVLXGEM23W+NWqipw= +google.golang.org/grpc/stats/opentelemetry v0.0.0-20240907200651-3ffb98b2c93a/go.mod h1:9i1T9n4ZinTUZGgzENMi8MDDgbGC5mqTS75JAv6xN3A= google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8= google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0= google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM= @@ -1126,6 +1226,8 @@ gopkg.in/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMy gopkg.in/ini.v1 v1.57.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k= gopkg.in/ini.v1 v1.67.0 h1:Dgnx+6+nfE+IfzjUEISNeydPJh9AXNNsWbGP9KzCsOA= gopkg.in/ini.v1 v1.67.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k= +gopkg.in/ini.v1 v1.67.3 h1:iM9Lhz5MRSGhHVGGwCuzG9KO8PoirCXj/m/qTmOJJQw= +gopkg.in/ini.v1 v1.67.3/go.mod h1:x/cyOwCgZqOkJoDIJ3c1KNHMo10+nLGAhh+kn3Zizss= gopkg.in/kothar/go-backblaze.v0 v0.0.0-20210124194846-35409b867216 h1:2TSTkQ8PMvGOD5eeqqRVv6Z9+BYI+bowK97RCr3W+9M= gopkg.in/kothar/go-backblaze.v0 v0.0.0-20210124194846-35409b867216/go.mod h1:zJ2QpyDCYo1KvLXlmdnFlQAyF/Qfth0fB8239Qg7BIE= gopkg.in/natefinch/lumberjack.v2 v2.0.0/go.mod h1:l0ndWWf7gzL7RNwBG7wST/UCcT4T24xpD6X8LsfU/+k= diff --git a/pkg/fuse/fuse.go b/pkg/fuse/fuse.go index 33a20875e768..10e3124bf44d 100644 --- a/pkg/fuse/fuse.go +++ b/pkg/fuse/fuse.go @@ -40,6 +40,12 @@ type fileSystem struct { fuse.RawFileSystem conf *vfs.Config v *vfs.VFS + + // passthrough write-path acceleration (experimental). When enabled and + // the kernel supports FUSE passthrough, newly written files are backed by + // a local staging file the kernel reads/writes directly (no daemon upcall), + // reconciled into JuiceFS slices on release. See passthrough.go. + pt *passthroughState } func newFileSystem(conf *vfs.Config, v *vfs.VFS) *fileSystem { @@ -235,6 +241,10 @@ func (fs *fileSystem) Create(cancel <-chan struct{}, in *fuse.CreateIn, name str return fuse.Status(err) } out.Fh = fh + if id, ok := fs.pt.tryOpen(entry.Inode, fh, in.Flags); ok { + out.OpenFlags |= fuse.FOPEN_PASSTHROUGH + out.BackingID = id + } return fs.replyEntry(ctx, &out.EntryOut, entry) } @@ -246,6 +256,11 @@ func (fs *fileSystem) Open(cancel <-chan struct{}, in *fuse.OpenIn, out *fuse.Op return fuse.Status(err) } out.Fh = fh + if id, ok := fs.pt.tryOpen(Ino(in.NodeId), fh, in.Flags); ok { + out.OpenFlags |= fuse.FOPEN_PASSTHROUGH + out.BackingID = id + return 0 + } if vfs.IsSpecialNode(Ino(in.NodeId)) { out.OpenFlags |= fuse.FOPEN_DIRECT_IO } else if entry.Attr.KeepCache { @@ -273,6 +288,7 @@ func (fs *fileSystem) Read(cancel <-chan struct{}, in *fuse.ReadIn, buf []byte) func (fs *fileSystem) Release(cancel <-chan struct{}, in *fuse.ReleaseIn) { ctx := fs.newContext(cancel, &in.InHeader) defer releaseContext(ctx) + fs.pt.reconcile(ctx, fs.v, in.Fh) fs.v.Release(ctx, Ino(in.NodeId), in.Fh) } @@ -516,6 +532,14 @@ func Serve(v *vfs.VFS, options string, xattrs, ioctl bool) error { opt.Options = append(opt.Options, "volname="+conf.Format.Name) opt.Options = append(opt.Options, "daemon_timeout=60", "iosize=65536", "novncache") } + // Experimental: opt into FUSE passthrough write acceleration via env var + // (avoids CLI surface while prototyping). JUICEFS_PASSTHROUGH_DIR must point + // at a non-stacked fs (tmpfs/ext4/xfs), not overlayfs/fuse. + ptEnabled := os.Getenv("JUICEFS_PASSTHROUGH") == "1" + if ptEnabled { + opt.EnablePassthrough = true + opt.MaxStackDepth = 2 + } fssrv, err := fuse.NewServer(imp, conf.Meta.MountPoint, &opt) if err != nil { if execErr, ok := err.(*exec.Error); ok { @@ -539,6 +563,11 @@ func Serve(v *vfs.VFS, options string, xattrs, ioctl bool) error { } } + if ptEnabled { + imp.pt = newPassthroughState(fssrv, os.Getenv("JUICEFS_PASSTHROUGH_DIR")) + logger.Infof("FUSE passthrough enabled (experimental); staging dir=%s", imp.pt.dir) + } + fsserv = fssrv fssrv.Serve() return nil diff --git a/pkg/fuse/passthrough.go b/pkg/fuse/passthrough.go new file mode 100644 index 000000000000..bb5340b92750 --- /dev/null +++ b/pkg/fuse/passthrough.go @@ -0,0 +1,158 @@ +/* + * JuiceFS, Copyright 2026 Juicedata, Inc. + * + * 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 fuse + +import ( + "fmt" + "os" + "path/filepath" + "sync" + "syscall" + + "github.com/hanwen/go-fuse/v2/fuse" + "github.com/juicedata/juicefs/pkg/vfs" +) + +// passthroughState manages per-open backing files used for FUSE passthrough +// write acceleration. EXPERIMENTAL: scoped to the write path. A file opened +// for write gets a local staging file (on a non-stacked fs) that the kernel +// reads/writes directly via FUSE_PASSTHROUGH, bypassing the daemon per-op. On +// release the staging file is reconciled into JuiceFS slices via the normal +// writer path. Durability is therefore deferred to release (commit-style). +type passthroughState struct { + server *fuse.Server + dir string + + mu sync.Mutex + files map[uint64]*ptFile // keyed by fh + warnOne sync.Once +} + +type ptFile struct { + ino Ino + fh uint64 + path string + f *os.File + backingID int32 +} + +func newPassthroughState(server *fuse.Server, dir string) *passthroughState { + if dir == "" { + dir = filepath.Join(os.TempDir(), "juicefs-passthrough") + } + _ = os.MkdirAll(dir, 0700) + return &passthroughState{server: server, dir: dir, files: make(map[uint64]*ptFile)} +} + +func isWriteOpen(flags uint32) bool { + acc := flags & uint32(syscall.O_ACCMODE) + return acc == syscall.O_WRONLY || acc == syscall.O_RDWR +} + +// tryOpen sets up passthrough for a write-opened file. It returns the kernel +// backing ID and true on success; callers then set FOPEN_PASSTHROUGH and +// OpenOut.BackingID. On any failure it returns false and the caller falls back +// to the normal (daemon) path — passthrough is purely an optimization. +func (p *passthroughState) tryOpen(ino Ino, fh uint64, flags uint32) (int32, bool) { + if p == nil || !isWriteOpen(flags) { + return 0, false + } + if !p.server.SupportsPassthrough() { + p.warnOne.Do(func() { + logger.Warnf("FUSE passthrough requested but not supported by the kernel; falling back") + }) + return 0, false + } + path := filepath.Join(p.dir, fmt.Sprintf("%d-%d.tmp", ino, fh)) + f, err := os.OpenFile(path, os.O_RDWR|os.O_CREATE|os.O_TRUNC, 0600) + if err != nil { + logger.Warnf("passthrough: open staging %s: %s", path, err) + return 0, false + } + id, errno := p.server.RegisterBackingFd(&fuse.BackingMap{Fd: int32(f.Fd())}) + if errno != 0 { + logger.Warnf("passthrough: RegisterBackingFd(%s): %s", path, errno) + _ = f.Close() + _ = os.Remove(path) + return 0, false + } + p.mu.Lock() + p.files[fh] = &ptFile{ino: ino, fh: fh, path: path, f: f, backingID: id} + p.mu.Unlock() + return id, true +} + +// reconcile flushes a passthrough staging file back into JuiceFS slices, then +// tears down the backing registration. Called on release, before vfs.Release. +func (p *passthroughState) reconcile(ctx vfs.Context, v *vfs.VFS, fh uint64) { + if p == nil { + return + } + p.mu.Lock() + pf := p.files[fh] + if pf != nil { + delete(p.files, fh) + } + p.mu.Unlock() + if pf == nil { + return + } + // Stop kernel passthrough first so no further direct writes land in the + // backing file, and close the registered fd, then re-open the staging file + // by path for a clean sequential read (reading via the registered backing + // fd can return stale/partial data). + if errno := p.server.UnregisterBackingFd(pf.backingID); errno != 0 { + logger.Warnf("passthrough: UnregisterBackingFd(%d): %s", pf.backingID, errno) + } + _ = pf.f.Close() + defer func() { _ = os.Remove(pf.path) }() + + rf, err := os.Open(pf.path) + if err != nil { + logger.Errorf("passthrough: reopen staging %s: %s", pf.path, err) + return + } + defer rf.Close() + buf := make([]byte, 4<<20) + var off uint64 + for { + n, err := rf.Read(buf) + if n > 0 { + // vfs.Write may retain the buffer until flush; give each chunk its + // own backing array so the next Read doesn't corrupt a pending slice. + chunk := make([]byte, n) + copy(chunk, buf[:n]) + if e := v.Write(ctx, pf.ino, chunk, off, fh); e != 0 { + logger.Errorf("passthrough: reconcile write ino %d off %d: %s", pf.ino, off, e) + return + } + off += uint64(n) + } + if err != nil { + break // io.EOF or read error + } + } + if e := v.Flush(ctx, pf.ino, fh, 0); e != 0 { + logger.Errorf("passthrough: reconcile flush ino %d: %s", pf.ino, e) + } + // Passthrough writes bypassed the daemon, so the kernel's cached size and + // page data for this inode are stale (size is still 0 from the empty + // create). Now that the slices + metadata are committed, invalidate both so + // readers in this mount session see the reconciled file (read-your-writes). + p.server.InodeNotify(uint64(pf.ino), -1, 0) // attributes (size/mtime) + p.server.InodeNotify(uint64(pf.ino), 0, int64(off)) // data range +} diff --git a/pkg/vfs/vfs.go b/pkg/vfs/vfs.go index 45b557c53de6..8654bebb2818 100644 --- a/pkg/vfs/vfs.go +++ b/pkg/vfs/vfs.go @@ -79,6 +79,8 @@ type FuseOptions struct { DisableReadDirPlus bool `json:",omitempty"` EnableReadDirPlusAuto bool EnableWriteback bool + EnablePassthrough bool `json:",omitempty"` + MaxStackDepth int `json:",omitempty"` EnableIoctl bool `json:",omitempty"` DontUmask bool OtherCaps uint32 From d037dd5c179fb556ed680efc3ac3fc7164f0f537 Mon Sep 17 00:00:00 2001 From: Haytham Abuelfutuh Date: Tue, 7 Jul 2026 06:22:28 +0000 Subject: [PATCH 02/14] fuse: never passthrough internal/control files (fixes checkpoint hang) Open() consulted pt.tryOpen() before the IsSpecialNode() branch, so a write-open of a JuiceFS internal inode (.control, .stats, .config, ...) could be granted FOPEN_PASSTHROUGH and served by a kernel backing file. The .control protocol then broke silently: the checkpoint verb's write/read on .control went to a plain temp file instead of the daemon's in-process handler, so the op never dispatched and 'juicefs checkpoint' hung indefinitely (with passthrough enabled). Guard tryOpen to refuse special nodes. Verified before/after with passthrough genuinely engaged: no-fix checkpoint hangs (.control opened special=true and passthrough'd); with the guard .control is skipped, data files still use passthrough, and checkpoint completes with a valid snapshot. Regression test in passthrough_special_test.go. Co-Authored-By: Claude Fable 5 Claude-Session: https://claude.ai/code/session_01SxvU7KdSpd1oRRmAgWTA6Z --- pkg/fuse/passthrough.go | 9 ++++++++ pkg/fuse/passthrough_special_test.go | 34 ++++++++++++++++++++++++++++ 2 files changed, 43 insertions(+) create mode 100644 pkg/fuse/passthrough_special_test.go diff --git a/pkg/fuse/passthrough.go b/pkg/fuse/passthrough.go index bb5340b92750..acad7ff9be68 100644 --- a/pkg/fuse/passthrough.go +++ b/pkg/fuse/passthrough.go @@ -71,6 +71,15 @@ func (p *passthroughState) tryOpen(ino Ino, fh uint64, flags uint32) (int32, boo if p == nil || !isWriteOpen(flags) { return 0, false } + // Never hand JuiceFS's internal/control files (.control, .stats, .config, + // ...) to the kernel via a backing file. Those inodes are served by the + // daemon's in-process handlers; passthrough would silently divert their + // I/O to a plain temp file, breaking the control protocol — e.g. the + // `juicefs checkpoint` verb, whose write/read on .control would go to the + // backing file and never reach the handler, hanging the command. + if vfs.IsSpecialNode(ino) { + return 0, false + } if !p.server.SupportsPassthrough() { p.warnOne.Do(func() { logger.Warnf("FUSE passthrough requested but not supported by the kernel; falling back") diff --git a/pkg/fuse/passthrough_special_test.go b/pkg/fuse/passthrough_special_test.go new file mode 100644 index 000000000000..bb37689e8229 --- /dev/null +++ b/pkg/fuse/passthrough_special_test.go @@ -0,0 +1,34 @@ +//go:build linux + +/* + * JuiceFS, Copyright 2026 Juicedata, Inc. + * Licensed under the Apache License, Version 2.0 (the "License"). + */ + +package fuse + +import ( + "testing" + + "github.com/juicedata/juicefs/pkg/vfs" +) + +// tryOpen must refuse passthrough for JuiceFS internal/control inodes: a +// backing file on .control would divert the control protocol (e.g. the +// checkpoint verb) and hang. A nil passthroughState short-circuits before +// any kernel calls, so this exercises the special-node guard in isolation. +func TestTryOpenSkipsSpecialNodes(t *testing.T) { + var p *passthroughState // nil: guard order still returns (0,false) + // Sanity: control inode is classified special. + control := vfs.Ino(0x7FFFFFFF00000002) + if !vfs.IsSpecialNode(control) { + t.Fatalf("expected %d to be a special node", control) + } + if id, ok := p.tryOpen(control, 1, 0x8002); ok || id != 0 { + t.Fatalf("tryOpen(special) = (%d,%v), want (0,false)", id, ok) + } + // A regular inode with a read-only open is also refused (not a write). + if _, ok := p.tryOpen(vfs.Ino(2), 1, 0); ok { + t.Fatalf("tryOpen(regular, read-only) should be false") + } +} From 38c04c61bf3420039114f834bb6fe7e47f19d5c4 Mon Sep 17 00:00:00 2001 From: Haytham Abuelfutuh Date: Tue, 7 Jul 2026 20:53:30 +0000 Subject: [PATCH 03/14] fuse: pool passthrough backing registrations across opens MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Registering a backing fd costs an ioctl — or, on unprivileged broker mounts, an RPC round trip to the node broker — plus a staging-file create, once per write-open. Small-file workloads open thousands of times and pay it every time (measured 2x slower small-file creates over the broker on EKS, ENG26-869). Keep reconciled staging files registered: truncate to zero and park them (LIFO, capped at 64 per mount) for the next write-open. After warm-up a small-file loop performs no registrations, no unregistrations and no file creates. A backing is never attached to two live opens: checkout is exclusive and a file is only parked after its reconcile copied the data out and truncated it. Failed reconciles retire the backing instead of recycling it; stale staging files from a crashed process are swept at mount. Co-Authored-By: Claude Opus 4.8 (1M context) Claude-Session: https://claude.ai/code/session_01AWLe95UTvfBpTXptpBXLrB --- pkg/fuse/passthrough.go | 137 ++++++++++++++++++++++++------ pkg/fuse/passthrough_pool_test.go | 76 +++++++++++++++++ 2 files changed, 186 insertions(+), 27 deletions(-) create mode 100644 pkg/fuse/passthrough_pool_test.go diff --git a/pkg/fuse/passthrough.go b/pkg/fuse/passthrough.go index acad7ff9be68..84e4eeefa89d 100644 --- a/pkg/fuse/passthrough.go +++ b/pkg/fuse/passthrough.go @@ -33,31 +33,106 @@ import ( // reads/writes directly via FUSE_PASSTHROUGH, bypassing the daemon per-op. On // release the staging file is reconciled into JuiceFS slices via the normal // writer path. Durability is therefore deferred to release (commit-style). +// +// Backing registrations are POOLED: registering a backing fd costs an ioctl +// (or, on unprivileged broker mounts, an RPC round trip to the node broker) +// plus a staging-file create, once per write-open. Small-file workloads open +// thousands of times, so instead of register-at-open/unregister-at-release, +// reconciled staging files are truncated to zero and parked for the next +// open; after warm-up a small-file loop performs no registrations at all +// (ENG26-869). A backing is never attached to two live opens at once: +// checkout is exclusive, and a backing returns to the pool only after its +// reconcile finished (data copied out, file truncated). type passthroughState struct { server *fuse.Server dir string mu sync.Mutex files map[uint64]*ptFile // keyed by fh + pool []*ptBacking // idle registered backings, truncated to 0 + poolSeq int warnOne sync.Once } -type ptFile struct { - ino Ino - fh uint64 +// ptPoolCap bounds the idle registered backings kept per mount. Each entry +// pins one kernel backing registration and one empty staging file; the cap +// only needs to cover the plausible number of concurrent write-opens. +const ptPoolCap = 64 + +// ptBacking is one registered kernel backing: a staging file plus the +// backing ID the kernel handed back for it. It outlives individual opens. +type ptBacking struct { path string f *os.File backingID int32 } +type ptFile struct { + ino Ino + fh uint64 + b *ptBacking +} + func newPassthroughState(server *fuse.Server, dir string) *passthroughState { if dir == "" { dir = filepath.Join(os.TempDir(), "juicefs-passthrough") } _ = os.MkdirAll(dir, 0700) + // Best-effort: drop staging files a crashed predecessor left behind. + if stale, err := filepath.Glob(filepath.Join(dir, "*.tmp")); err == nil { + for _, p := range stale { + _ = os.Remove(p) + } + } return &passthroughState{server: server, dir: dir, files: make(map[uint64]*ptFile)} } +// checkout returns an idle registered backing, or registers a fresh one. +func (p *passthroughState) checkout() (*ptBacking, bool) { + p.mu.Lock() + if n := len(p.pool); n > 0 { + b := p.pool[n-1] + p.pool = p.pool[:n-1] + p.mu.Unlock() + return b, true + } + p.poolSeq++ + seq := p.poolSeq + p.mu.Unlock() + + path := filepath.Join(p.dir, fmt.Sprintf("pool-%d.tmp", seq)) + f, err := os.OpenFile(path, os.O_RDWR|os.O_CREATE|os.O_TRUNC, 0600) + if err != nil { + logger.Warnf("passthrough: open staging %s: %s", path, err) + return nil, false + } + id, errno := p.server.RegisterBackingFd(&fuse.BackingMap{Fd: int32(f.Fd())}) + if errno != 0 { + logger.Warnf("passthrough: RegisterBackingFd(%s): %s", path, errno) + _ = f.Close() + _ = os.Remove(path) + return nil, false + } + return &ptBacking{path: path, f: f, backingID: id}, true +} + +// checkin parks a reconciled backing for reuse, or retires it when the pool +// is full. The staging file MUST already be truncated to zero. +func (p *passthroughState) checkin(b *ptBacking) { + p.mu.Lock() + if len(p.pool) < ptPoolCap { + p.pool = append(p.pool, b) + p.mu.Unlock() + return + } + p.mu.Unlock() + if errno := p.server.UnregisterBackingFd(b.backingID); errno != 0 { + logger.Warnf("passthrough: UnregisterBackingFd(%d): %s", b.backingID, errno) + } + _ = b.f.Close() + _ = os.Remove(b.path) +} + func isWriteOpen(flags uint32) bool { acc := flags & uint32(syscall.O_ACCMODE) return acc == syscall.O_WRONLY || acc == syscall.O_RDWR @@ -86,23 +161,14 @@ func (p *passthroughState) tryOpen(ino Ino, fh uint64, flags uint32) (int32, boo }) return 0, false } - path := filepath.Join(p.dir, fmt.Sprintf("%d-%d.tmp", ino, fh)) - f, err := os.OpenFile(path, os.O_RDWR|os.O_CREATE|os.O_TRUNC, 0600) - if err != nil { - logger.Warnf("passthrough: open staging %s: %s", path, err) - return 0, false - } - id, errno := p.server.RegisterBackingFd(&fuse.BackingMap{Fd: int32(f.Fd())}) - if errno != 0 { - logger.Warnf("passthrough: RegisterBackingFd(%s): %s", path, errno) - _ = f.Close() - _ = os.Remove(path) + b, ok := p.checkout() + if !ok { return 0, false } p.mu.Lock() - p.files[fh] = &ptFile{ino: ino, fh: fh, path: path, f: f, backingID: id} + p.files[fh] = &ptFile{ino: ino, fh: fh, b: b} p.mu.Unlock() - return id, true + return b.backingID, true } // reconcile flushes a passthrough staging file back into JuiceFS slices, then @@ -120,19 +186,28 @@ func (p *passthroughState) reconcile(ctx vfs.Context, v *vfs.VFS, fh uint64) { if pf == nil { return } - // Stop kernel passthrough first so no further direct writes land in the - // backing file, and close the registered fd, then re-open the staging file - // by path for a clean sequential read (reading via the registered backing - // fd can return stale/partial data). - if errno := p.server.UnregisterBackingFd(pf.backingID); errno != 0 { - logger.Warnf("passthrough: UnregisterBackingFd(%d): %s", pf.backingID, errno) - } - _ = pf.f.Close() - defer func() { _ = os.Remove(pf.path) }() + // The kernel stops issuing passthrough I/O for this open once its release + // is processed, and reconcile runs from the RELEASE handler — after the + // application's last close, so no writes are in flight. The registration + // itself is kept alive for reuse (see checkin); read the staging content + // through a fresh path-open fd for a clean sequential pass (reading via + // the registered backing fd can return stale/partial data), then truncate + // and park the backing for the next open. + b := pf.b + done := false + defer func() { + if !done { // reconcile failed: don't reuse a backing with stale data + if errno := p.server.UnregisterBackingFd(b.backingID); errno != 0 { + logger.Warnf("passthrough: UnregisterBackingFd(%d): %s", b.backingID, errno) + } + _ = b.f.Close() + _ = os.Remove(b.path) + } + }() - rf, err := os.Open(pf.path) + rf, err := os.Open(b.path) if err != nil { - logger.Errorf("passthrough: reopen staging %s: %s", pf.path, err) + logger.Errorf("passthrough: reopen staging %s: %s", b.path, err) return } defer rf.Close() @@ -157,6 +232,7 @@ func (p *passthroughState) reconcile(ctx vfs.Context, v *vfs.VFS, fh uint64) { } if e := v.Flush(ctx, pf.ino, fh, 0); e != 0 { logger.Errorf("passthrough: reconcile flush ino %d: %s", pf.ino, e) + return } // Passthrough writes bypassed the daemon, so the kernel's cached size and // page data for this inode are stale (size is still 0 from the empty @@ -164,4 +240,11 @@ func (p *passthroughState) reconcile(ctx vfs.Context, v *vfs.VFS, fh uint64) { // readers in this mount session see the reconciled file (read-your-writes). p.server.InodeNotify(uint64(pf.ino), -1, 0) // attributes (size/mtime) p.server.InodeNotify(uint64(pf.ino), 0, int64(off)) // data range + // Data is safely in JuiceFS: recycle the registration for the next open. + if err := b.f.Truncate(0); err != nil { + logger.Warnf("passthrough: truncate staging %s: %s", b.path, err) + return // defer retires it + } + done = true + p.checkin(b) } diff --git a/pkg/fuse/passthrough_pool_test.go b/pkg/fuse/passthrough_pool_test.go new file mode 100644 index 000000000000..bef053bd1f6b --- /dev/null +++ b/pkg/fuse/passthrough_pool_test.go @@ -0,0 +1,76 @@ +/* + * JuiceFS, Copyright 2026 Juicedata, Inc. + * + * 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 fuse + +import ( + "os" + "path/filepath" + "testing" +) + +// TestPassthroughPoolReuse: a checked-in backing is handed back by the next +// checkout without a new registration (no server round trip), and pooled +// entries come back most-recently-parked first. +func TestPassthroughPoolReuse(t *testing.T) { + dir := t.TempDir() + p := &passthroughState{dir: dir, files: make(map[uint64]*ptFile)} + + mk := func(name string) *ptBacking { + f, err := os.OpenFile(filepath.Join(dir, name), os.O_RDWR|os.O_CREATE, 0600) + if err != nil { + t.Fatal(err) + } + return &ptBacking{path: f.Name(), f: f, backingID: int32(len(p.pool) + 1)} + } + + b1, b2 := mk("pool-1.tmp"), mk("pool-2.tmp") + // Under the cap, checkin parks without touching the (nil) server — + // a server call here would panic the test. + p.checkin(b1) + p.checkin(b2) + if len(p.pool) != 2 { + t.Fatalf("pool size = %d, want 2", len(p.pool)) + } + + // Checkout must reuse parked backings (nil server: a registration + // attempt would panic), most recently parked first. + got, ok := p.checkout() + if !ok || got != b2 { + t.Fatalf("checkout = %v, %v; want %v (LIFO reuse)", got, ok, b2) + } + got, ok = p.checkout() + if !ok || got != b1 { + t.Fatalf("checkout = %v, %v; want %v", got, ok, b1) + } + if len(p.pool) != 0 { + t.Fatalf("pool size = %d, want 0", len(p.pool)) + } +} + +// TestPassthroughPoolStaleCleanup: a fresh state removes leftover staging +// files from a crashed predecessor in the same directory. +func TestPassthroughPoolStaleCleanup(t *testing.T) { + dir := t.TempDir() + stale := filepath.Join(dir, "pool-9.tmp") + if err := os.WriteFile(stale, []byte("junk"), 0600); err != nil { + t.Fatal(err) + } + _ = newPassthroughState(nil, dir) + if _, err := os.Stat(stale); !os.IsNotExist(err) { + t.Fatalf("stale staging file survived: %v", err) + } +} From 1938b2e755dfdf74b0c49089ff6f5657b1bae3bc Mon Sep 17 00:00:00 2001 From: Haytham Abuelfutuh Date: Tue, 7 Jul 2026 21:29:11 +0000 Subject: [PATCH 04/14] fuse: disable passthrough for the mount after EPERM registration MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The backing-registration ioctl needs CAP_SYS_ADMIN in the init user namespace. A non-root mount whose container merely *adds* the capability (bounding set, no ambient) fails EPERM on every attempt — measured 2000+ doomed ioctl+create+warn cycles in one small-file run on EKS (ENG26-869). EPERM is permanent for the process, so latch passthrough off for the mount on first sight; other errnos stay retryable. The warning now says what to do about it. Co-Authored-By: Claude Opus 4.8 (1M context) Claude-Session: https://claude.ai/code/session_01AWLe95UTvfBpTXptpBXLrB --- pkg/fuse/passthrough.go | 33 +++++++++++++++++++++++++------ pkg/fuse/passthrough_pool_test.go | 13 ++++++++++++ 2 files changed, 40 insertions(+), 6 deletions(-) diff --git a/pkg/fuse/passthrough.go b/pkg/fuse/passthrough.go index 84e4eeefa89d..d97742db3e39 100644 --- a/pkg/fuse/passthrough.go +++ b/pkg/fuse/passthrough.go @@ -47,11 +47,12 @@ type passthroughState struct { server *fuse.Server dir string - mu sync.Mutex - files map[uint64]*ptFile // keyed by fh - pool []*ptBacking // idle registered backings, truncated to 0 - poolSeq int - warnOne sync.Once + mu sync.Mutex + files map[uint64]*ptFile // keyed by fh + pool []*ptBacking // idle registered backings, truncated to 0 + poolSeq int + disabled bool // registration failed with a permanent error; stop trying + warnOne sync.Once } // ptPoolCap bounds the idle registered backings kept per mount. Each entry @@ -90,6 +91,10 @@ func newPassthroughState(server *fuse.Server, dir string) *passthroughState { // checkout returns an idle registered backing, or registers a fresh one. func (p *passthroughState) checkout() (*ptBacking, bool) { p.mu.Lock() + if p.disabled { + p.mu.Unlock() + return nil, false + } if n := len(p.pool); n > 0 { b := p.pool[n-1] p.pool = p.pool[:n-1] @@ -108,9 +113,25 @@ func (p *passthroughState) checkout() (*ptBacking, bool) { } id, errno := p.server.RegisterBackingFd(&fuse.BackingMap{Fd: int32(f.Fd())}) if errno != 0 { - logger.Warnf("passthrough: RegisterBackingFd(%s): %s", path, errno) _ = f.Close() _ = os.Remove(path) + // EPERM is permanent: the backing-registration ioctl needs + // CAP_SYS_ADMIN in the init user namespace, and a process that + // lacks it now will lack it for every open (e.g. a non-root + // container whose added capabilities are bounding-set only). + // Without this latch every write-open pays a doomed ioctl, a + // staging create/remove, and a warning line (ENG26-869 saw 2000+ + // per run). Other errnos may be transient; keep trying those. + if errno == syscall.EPERM { + p.mu.Lock() + p.disabled = true + p.mu.Unlock() + logger.Warnf("passthrough: RegisterBackingFd(%s): %s; disabling passthrough for this mount "+ + "(the ioctl needs CAP_SYS_ADMIN in the init user namespace — run the mount as root, "+ + "or use a mount broker that performs registrations node-side)", path, errno) + return nil, false + } + logger.Warnf("passthrough: RegisterBackingFd(%s): %s", path, errno) return nil, false } return &ptBacking{path: path, f: f, backingID: id}, true diff --git a/pkg/fuse/passthrough_pool_test.go b/pkg/fuse/passthrough_pool_test.go index bef053bd1f6b..1eb4be5239fd 100644 --- a/pkg/fuse/passthrough_pool_test.go +++ b/pkg/fuse/passthrough_pool_test.go @@ -61,6 +61,19 @@ func TestPassthroughPoolReuse(t *testing.T) { } } +// TestPassthroughDisabledLatch: once registration hit a permanent error the +// state stops attempting registrations entirely — checkout must return false +// before touching the (nil) server or the filesystem. +func TestPassthroughDisabledLatch(t *testing.T) { + p := &passthroughState{dir: t.TempDir(), files: make(map[uint64]*ptFile), disabled: true} + if b, ok := p.checkout(); ok || b != nil { + t.Fatalf("checkout on disabled state = %v, %v; want nil, false", b, ok) + } + if p.poolSeq != 0 { + t.Fatalf("disabled checkout still allocated a staging sequence") + } +} + // TestPassthroughPoolStaleCleanup: a fresh state removes leftover staging // files from a crashed predecessor in the same directory. func TestPassthroughPoolStaleCleanup(t *testing.T) { From 9a50674d95cf7372bc13b4d37431852abbbecece Mon Sep 17 00:00:00 2001 From: Haytham Abuelfutuh Date: Tue, 7 Jul 2026 23:44:46 +0000 Subject: [PATCH 05/14] vfs+fuse: fence passthrough reconcile for consistency points close(2) returns before the RELEASE-time reconcile copies the staging file into JuiceFS slices, so a checkpoint/commit issued right after a close could FlushAll+snapshot mid-copy and publish a short file (reproduced: 256MB write + immediate checkpoint -> 44MB in snapshot; also seen on EKS as a finalize missing exactly the last written file). Add an external-flush counter to VFS that reconcile brackets; the checkpoint verb (stacked branch) waits for it to reach zero before flushing. Co-Authored-By: Claude Opus 4.8 (1M context) Claude-Session: https://claude.ai/code/session_01AWLe95UTvfBpTXptpBXLrB --- pkg/fuse/passthrough.go | 8 ++++++++ pkg/vfs/vfs.go | 19 +++++++++++++++++++ 2 files changed, 27 insertions(+) diff --git a/pkg/fuse/passthrough.go b/pkg/fuse/passthrough.go index d97742db3e39..7c4dc89bcc81 100644 --- a/pkg/fuse/passthrough.go +++ b/pkg/fuse/passthrough.go @@ -198,6 +198,14 @@ func (p *passthroughState) reconcile(ctx vfs.Context, v *vfs.VFS, fh uint64) { if p == nil { return } + // Fence for consistency points BEFORE looking anything up: the + // application's close(2) has already returned, but the data still lives + // only in the staging file until the copy below finishes. A checkpoint + // or commit that ran concurrently would flush+snapshot without these + // writes and publish a mid-copy (short) file. The external-flush counter + // lets those paths wait for in-flight reconciles first. + v.BeginExternalFlush() + defer v.EndExternalFlush() p.mu.Lock() pf := p.files[fh] if pf != nil { diff --git a/pkg/vfs/vfs.go b/pkg/vfs/vfs.go index 8654bebb2818..40c3d8e85f0f 100644 --- a/pkg/vfs/vfs.go +++ b/pkg/vfs/vfs.go @@ -24,6 +24,7 @@ import ( "runtime" "sort" "sync" + "sync/atomic" "syscall" "time" @@ -1241,9 +1242,27 @@ type VFS struct { modM sync.Mutex modifiedAt map[Ino]time.Time + // externalFlushes counts out-of-band write flows (e.g. FUSE passthrough + // reconcile at release) whose Write calls are not yet visible to + // FlushAll. Consistency points (checkpoint, commit) must wait for it to + // reach zero before flushing, or a file whose close(2) already returned + // can be snapshotted mid-copy. See ExternalFlushes. + externalFlushes int64 + registry *prometheus.Registry } +// BeginExternalFlush marks an out-of-band write flow (such as a passthrough +// staging-file reconcile) as in flight. Call before the flow's first Write so +// a concurrent consistency point cannot observe zero while data is pending. +func (v *VFS) BeginExternalFlush() { atomic.AddInt64(&v.externalFlushes, 1) } + +// EndExternalFlush marks the flow complete (its writes flushed or failed). +func (v *VFS) EndExternalFlush() { atomic.AddInt64(&v.externalFlushes, -1) } + +// ExternalFlushes reports the number of out-of-band write flows in flight. +func (v *VFS) ExternalFlushes() int64 { return atomic.LoadInt64(&v.externalFlushes) } + func NewVFS(conf *Config, m meta.Meta, store chunk.ChunkStore, registerer prometheus.Registerer, registry *prometheus.Registry) *VFS { reader := NewDataReader(conf, m, store) writer := NewDataWriter(conf, m, store, reader) From c8ba37ef61214cdc4fd3fd7a132b0799fee3bb2e Mon Sep 17 00:00:00 2001 From: Haytham Abuelfutuh Date: Wed, 8 Jul 2026 00:08:30 +0000 Subject: [PATCH 06/14] vfs+fuse: cover the unmount path for in-flight passthrough reconciles Two more legs of the reconcile fence (the checkpoint verb was fenced in 9a50674, but finalize goes through unmount, not the verb): - fuse.Serve waits out in-flight reconciles after the kernel session ends, so a daemon exit (umount or SIGTERM->lazy umount) can't strand a closed file's staging data. Reproduced on EKS: finalize published volumes short by exactly the last written 256MB file, both privileged and brokered. - Export the in-flight count as gauge 'passthrough_staging_blocks'. The name contains 'staging_blocks' on purpose: durability watchers that sum staging gauges from .stats before sealing a snapshot (the flyteplugins drain-before-commit path) then wait for reconciles with no client change. Co-Authored-By: Claude Opus 4.8 (1M context) Claude-Session: https://claude.ai/code/session_01AWLe95UTvfBpTXptpBXLrB --- pkg/fuse/fuse.go | 16 ++++++++++++++++ pkg/vfs/vfs.go | 12 ++++++++++++ 2 files changed, 28 insertions(+) diff --git a/pkg/fuse/fuse.go b/pkg/fuse/fuse.go index 10e3124bf44d..d1fc9a247cd7 100644 --- a/pkg/fuse/fuse.go +++ b/pkg/fuse/fuse.go @@ -570,6 +570,22 @@ func Serve(v *vfs.VFS, options string, xattrs, ioctl bool) error { fsserv = fssrv fssrv.Serve() + // The kernel delivers queued RELEASEs before tearing the session down, + // but their passthrough reconciles may still be copying staging data + // when Serve returns; exiting now would lose bytes of files whose + // close(2) long since succeeded. Wait the reconciles out so anything + // that snapshots the metadata store after the daemon exits + // (commit/finalize) sees every closed file in full. + if imp.pt != nil { + deadline := time.Now().Add(time.Minute) + for v.ExternalFlushes() > 0 { + if time.Now().After(deadline) { + logger.Errorf("passthrough: exiting with %d unfinished reconcile(s)", v.ExternalFlushes()) + break + } + time.Sleep(50 * time.Millisecond) + } + } return nil } diff --git a/pkg/vfs/vfs.go b/pkg/vfs/vfs.go index 40c3d8e85f0f..9c62e62cd4e1 100644 --- a/pkg/vfs/vfs.go +++ b/pkg/vfs/vfs.go @@ -1370,6 +1370,18 @@ func initVFSMetrics(v *VFS, writer DataWriter, reader DataReader, registerer pro return float64(len(v.handles)) }) _ = registerer.Register(handlersGause) + // Named to contain "staging_blocks" ON PURPOSE: durability watchers that + // sum the staging gauges from .stats (e.g. drain-before-commit clients) + // then automatically cover in-flight passthrough reconciles too — a + // closed file's data is "staged" in its backing file until the reconcile + // lands it in the writer, invisible to the writeback gauges. + externalGauge := prometheus.NewGaugeFunc(prometheus.GaugeOpts{ + Name: "passthrough_staging_blocks", + Help: "out-of-band flush flows (passthrough staging reconciles) not yet visible to FlushAll.", + }, func() float64 { + return float64(v.ExternalFlushes()) + }) + _ = registerer.Register(externalGauge) InitMemoryBufferMetrics(writer, reader, registerer) } From c4b2f86b8b200ed56977d90afab871847db1a09f Mon Sep 17 00:00:00 2001 From: Haytham Abuelfutuh Date: Wed, 8 Jul 2026 02:12:38 +0000 Subject: [PATCH 07/14] fuse: fix passthrough data-corruption bugs found in review MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Four independent-review CRITICALs on the write-passthrough path: 1. Existing-file corruption: tryOpen gated only on write-intent, so an O_RDWR/O_WRONLY/O_APPEND open of a NON-empty file got a backing that starts empty — reads served as zeros (read-modify-write corruption) and reconcile (linear copy from offset 0) overwrote the real prefix with holes. Gate on emptyAtOpen: Create, O_TRUNC, or zero length. 2. Cross-mount corruption: staging lived in a host-shared dir with a startup glob-sweep and per-process pool-N names — a second mount's sweep unlinked the first's live staging, and pool-N collided across processes. Isolate each mount in a private os.MkdirTemp subdir; drop the shared-root sweep; O_EXCL so a stale name can't be truncated. 3. Read-error-as-EOF: a mid-copy read error broke the loop like io.EOF and committed a truncated file as complete. Distinguish io.EOF; bail loudly otherwise. 4. Reconcile failure destroyed the only copy: the retire path os.Remove'd the staging file holding data whose close(2) already returned. Rename it to an .orphan sibling and log at error instead. Also scope the consistency-point fence past the pf==nil check so plain (non-passthrough) releases don't add spurious checkpoint contention. Co-Authored-By: Claude Opus 4.8 (1M context) Claude-Session: https://claude.ai/code/session_01AWLe95UTvfBpTXptpBXLrB --- pkg/fuse/fuse.go | 9 ++- pkg/fuse/passthrough.go | 98 ++++++++++++++++++++-------- pkg/fuse/passthrough_pool_test.go | 34 +++++++--- pkg/fuse/passthrough_special_test.go | 4 +- 4 files changed, 105 insertions(+), 40 deletions(-) diff --git a/pkg/fuse/fuse.go b/pkg/fuse/fuse.go index d1fc9a247cd7..f7f3b7507e10 100644 --- a/pkg/fuse/fuse.go +++ b/pkg/fuse/fuse.go @@ -241,7 +241,8 @@ func (fs *fileSystem) Create(cancel <-chan struct{}, in *fuse.CreateIn, name str return fuse.Status(err) } out.Fh = fh - if id, ok := fs.pt.tryOpen(entry.Inode, fh, in.Flags); ok { + // A freshly Create'd file is always empty. + if id, ok := fs.pt.tryOpen(entry.Inode, fh, in.Flags, true); ok { out.OpenFlags |= fuse.FOPEN_PASSTHROUGH out.BackingID = id } @@ -256,7 +257,11 @@ func (fs *fileSystem) Open(cancel <-chan struct{}, in *fuse.OpenIn, out *fuse.Op return fuse.Status(err) } out.Fh = fh - if id, ok := fs.pt.tryOpen(Ino(in.NodeId), fh, in.Flags); ok { + // Passthrough only when this open observes an empty file: O_TRUNC (about + // to be emptied) or an already zero-length file. Otherwise the backing + // (which starts empty) would shadow / overwrite real content. + emptyAtOpen := in.Flags&uint32(syscall.O_TRUNC) != 0 || entry.Attr.Length == 0 + if id, ok := fs.pt.tryOpen(Ino(in.NodeId), fh, in.Flags, emptyAtOpen); ok { out.OpenFlags |= fuse.FOPEN_PASSTHROUGH out.BackingID = id return 0 diff --git a/pkg/fuse/passthrough.go b/pkg/fuse/passthrough.go index 7c4dc89bcc81..c3e7201c69d9 100644 --- a/pkg/fuse/passthrough.go +++ b/pkg/fuse/passthrough.go @@ -18,6 +18,7 @@ package fuse import ( "fmt" + "io" "os" "path/filepath" "sync" @@ -75,17 +76,26 @@ type ptFile struct { } func newPassthroughState(server *fuse.Server, dir string) *passthroughState { - if dir == "" { - dir = filepath.Join(os.TempDir(), "juicefs-passthrough") + base := dir + if base == "" { + base = filepath.Join(os.TempDir(), "juicefs-passthrough") } - _ = os.MkdirAll(dir, 0700) - // Best-effort: drop staging files a crashed predecessor left behind. - if stale, err := filepath.Glob(filepath.Join(dir, "*.tmp")); err == nil { - for _, p := range stale { - _ = os.Remove(p) - } + _ = os.MkdirAll(base, 0700) + // Isolate this mount's staging in a per-process subdir. Several juicefs + // mounts (multiple volumes on a node, or a graceful-restart successor) + // can share `base`; a flat shared dir would let one mount's pool-N.tmp + // collide with another's, and — worse — a startup sweep of the shared dir + // would unlink another live mount's in-use staging files, silently losing + // the data of files whose close(2) already returned. A private subdir + // removes both hazards, so we deliberately do NOT garbage-collect the + // shared root here (a crashed predecessor's subdir is inert and can be + // reaped out of band). + sub, err := os.MkdirTemp(base, fmt.Sprintf("m%d-", os.Getpid())) + if err != nil { + logger.Warnf("passthrough: per-process staging dir under %s: %s; using base", base, err) + sub = base } - return &passthroughState{server: server, dir: dir, files: make(map[uint64]*ptFile)} + return &passthroughState{server: server, dir: sub, files: make(map[uint64]*ptFile)} } // checkout returns an idle registered backing, or registers a fresh one. @@ -106,7 +116,11 @@ func (p *passthroughState) checkout() (*ptBacking, bool) { p.mu.Unlock() path := filepath.Join(p.dir, fmt.Sprintf("pool-%d.tmp", seq)) - f, err := os.OpenFile(path, os.O_RDWR|os.O_CREATE|os.O_TRUNC, 0600) + // O_EXCL: never open a staging file that already exists. The dir is + // per-process and seq is monotonic, so a collision would signal a bug + // (or a shared-dir fallback), and truncating a pre-existing file could + // clobber another mount's in-use backing. + f, err := os.OpenFile(path, os.O_RDWR|os.O_CREATE|os.O_EXCL, 0600) if err != nil { logger.Warnf("passthrough: open staging %s: %s", path, err) return nil, false @@ -159,12 +173,22 @@ func isWriteOpen(flags uint32) bool { return acc == syscall.O_WRONLY || acc == syscall.O_RDWR } -// tryOpen sets up passthrough for a write-opened file. It returns the kernel -// backing ID and true on success; callers then set FOPEN_PASSTHROUGH and -// OpenOut.BackingID. On any failure it returns false and the caller falls back -// to the normal (daemon) path — passthrough is purely an optimization. -func (p *passthroughState) tryOpen(ino Ino, fh uint64, flags uint32) (int32, bool) { - if p == nil || !isWriteOpen(flags) { +// tryOpen sets up passthrough for a write-opened file that is EMPTY at open. +// It returns the kernel backing ID and true on success; callers then set +// FOPEN_PASSTHROUGH and OpenOut.BackingID. On any failure it returns false and +// the caller falls back to the normal (daemon) path — passthrough is purely an +// optimization. +// +// emptyAtOpen MUST be true only when the file has no pre-existing content the +// open can observe or extend: a fresh Create, or an Open with O_TRUNC, or a +// zero-length file. This is a correctness gate, not a heuristic. The backing +// staging file always starts empty and FOPEN_PASSTHROUGH diverts reads, writes +// AND mmap to it, so enabling it on a non-empty file would (a) serve reads as +// zeros/EOF instead of real content (read-modify-write corruption), and (b) +// make reconcile — which copies staging linearly from offset 0 — overwrite the +// file's real prefix with holes for O_APPEND / sparse / seek-write patterns. +func (p *passthroughState) tryOpen(ino Ino, fh uint64, flags uint32, emptyAtOpen bool) (int32, bool) { + if p == nil || !isWriteOpen(flags) || !emptyAtOpen { return 0, false } // Never hand JuiceFS's internal/control files (.control, .stats, .config, @@ -198,14 +222,6 @@ func (p *passthroughState) reconcile(ctx vfs.Context, v *vfs.VFS, fh uint64) { if p == nil { return } - // Fence for consistency points BEFORE looking anything up: the - // application's close(2) has already returned, but the data still lives - // only in the staging file until the copy below finishes. A checkpoint - // or commit that ran concurrently would flush+snapshot without these - // writes and publish a mid-copy (short) file. The external-flush counter - // lets those paths wait for in-flight reconciles first. - v.BeginExternalFlush() - defer v.EndExternalFlush() p.mu.Lock() pf := p.files[fh] if pf != nil { @@ -215,6 +231,16 @@ func (p *passthroughState) reconcile(ctx vfs.Context, v *vfs.VFS, fh uint64) { if pf == nil { return } + // Fence for consistency points now that we know this is a passthrough + // release with data to land: the application's close(2) has already + // returned, but the data still lives only in the staging file until the + // copy below finishes. A checkpoint or commit that ran concurrently would + // flush+snapshot without these writes and publish a mid-copy (short) file. + // The external-flush counter lets those paths wait for in-flight + // reconciles first. Scoped past the pf==nil check so plain (non- + // passthrough) releases don't add spurious fence contention. + v.BeginExternalFlush() + defer v.EndExternalFlush() // The kernel stops issuing passthrough I/O for this open once its release // is processed, and reconcile runs from the RELEASE handler — after the // application's last close, so no writes are in flight. The registration @@ -225,12 +251,22 @@ func (p *passthroughState) reconcile(ctx vfs.Context, v *vfs.VFS, fh uint64) { b := pf.b done := false defer func() { - if !done { // reconcile failed: don't reuse a backing with stale data + if !done { + // Reconcile failed: the staging file is the ONLY copy of data whose + // close(2) already returned 0, so do NOT delete it — preserve it as + // an .orphan sibling for manual recovery and log loudly. Drop the + // kernel registration and the live fd so the backing isn't reused + // with stale data. if errno := p.server.UnregisterBackingFd(b.backingID); errno != 0 { logger.Warnf("passthrough: UnregisterBackingFd(%d): %s", b.backingID, errno) } _ = b.f.Close() - _ = os.Remove(b.path) + orphan := b.path + fmt.Sprintf(".orphan-%d", pf.ino) + if err := os.Rename(b.path, orphan); err != nil { + logger.Errorf("passthrough: reconcile of ino %d FAILED and staging %s could not be preserved: %s", pf.ino, b.path, err) + } else { + logger.Errorf("passthrough: reconcile of ino %d FAILED; unreconciled data preserved at %s", pf.ino, orphan) + } } }() @@ -256,7 +292,15 @@ func (p *passthroughState) reconcile(ctx vfs.Context, v *vfs.VFS, fh uint64) { off += uint64(n) } if err != nil { - break // io.EOF or read error + if err != io.EOF { + // A read error mid-copy must NOT be mistaken for end-of-file: + // that would flush+commit a truncated file as if complete. Bail + // so the defer preserves the staging and the file is not sealed + // short. + logger.Errorf("passthrough: read staging %s at off %d: %s", b.path, off, err) + return + } + break } } if e := v.Flush(ctx, pf.ino, fh, 0); e != 0 { diff --git a/pkg/fuse/passthrough_pool_test.go b/pkg/fuse/passthrough_pool_test.go index 1eb4be5239fd..19e587a536c5 100644 --- a/pkg/fuse/passthrough_pool_test.go +++ b/pkg/fuse/passthrough_pool_test.go @@ -74,16 +74,32 @@ func TestPassthroughDisabledLatch(t *testing.T) { } } -// TestPassthroughPoolStaleCleanup: a fresh state removes leftover staging -// files from a crashed predecessor in the same directory. -func TestPassthroughPoolStaleCleanup(t *testing.T) { - dir := t.TempDir() - stale := filepath.Join(dir, "pool-9.tmp") - if err := os.WriteFile(stale, []byte("junk"), 0600); err != nil { +// TestPassthroughStagingIsolation: a fresh state carves a private per-process +// subdir under base and MUST NOT touch files belonging to another mount that +// shares base — the old shared-dir sweep silently deleted a live mount's +// in-use staging (data loss). It also gives distinct states distinct dirs. +func TestPassthroughStagingIsolation(t *testing.T) { + base := t.TempDir() + // A sibling mount's live staging file sitting directly in base. + sibling := filepath.Join(base, "pool-9.tmp") + if err := os.WriteFile(sibling, []byte("live data"), 0600); err != nil { t.Fatal(err) } - _ = newPassthroughState(nil, dir) - if _, err := os.Stat(stale); !os.IsNotExist(err) { - t.Fatalf("stale staging file survived: %v", err) + p1 := newPassthroughState(nil, base) + p2 := newPassthroughState(nil, base) + + if _, err := os.Stat(sibling); err != nil { + t.Fatalf("sibling mount's staging was disturbed: %v", err) + } + if p1.dir == base || p2.dir == base { + t.Fatalf("state did not isolate into a subdir: p1=%q p2=%q base=%q", p1.dir, p2.dir, base) + } + if filepath.Dir(p1.dir) != base { + t.Fatalf("subdir %q not under base %q", p1.dir, base) + } + // Even within one process, two states must not share a dir (else their + // pool-N.tmp names would collide). + if p1.dir == p2.dir { + t.Fatalf("two states shared staging dir %q", p1.dir) } } diff --git a/pkg/fuse/passthrough_special_test.go b/pkg/fuse/passthrough_special_test.go index bb37689e8229..ce1a20c2302c 100644 --- a/pkg/fuse/passthrough_special_test.go +++ b/pkg/fuse/passthrough_special_test.go @@ -24,11 +24,11 @@ func TestTryOpenSkipsSpecialNodes(t *testing.T) { if !vfs.IsSpecialNode(control) { t.Fatalf("expected %d to be a special node", control) } - if id, ok := p.tryOpen(control, 1, 0x8002); ok || id != 0 { + if id, ok := p.tryOpen(control, 1, 0x8002, true); ok || id != 0 { t.Fatalf("tryOpen(special) = (%d,%v), want (0,false)", id, ok) } // A regular inode with a read-only open is also refused (not a write). - if _, ok := p.tryOpen(vfs.Ino(2), 1, 0); ok { + if _, ok := p.tryOpen(vfs.Ino(2), 1, 0, true); ok { t.Fatalf("tryOpen(regular, read-only) should be false") } } From d378c475b04ebc2b6d87a60afce9a4ed8190fb85 Mon Sep 17 00:00:00 2001 From: Haytham Abuelfutuh Date: Wed, 8 Jul 2026 02:15:49 +0000 Subject: [PATCH 08/14] fuse: one passthrough writer per inode (close reconcile-window reuse race) An immediate reopen-for-write of a file just written via passthrough sees metadata size 0 (its reconcile is still in flight), so emptyAtOpen looks true again and a second empty backing would clobber the first's data on reconcile. Reserve the inode across open+reconcile; overlapping write opens fall to the daemon path. Also covers two concurrent write-opens of one empty inode. Co-Authored-By: Claude Opus 4.8 (1M context) Claude-Session: https://claude.ai/code/session_01AWLe95UTvfBpTXptpBXLrB --- .gitignore | 2 +- pkg/fuse/passthrough.go | 41 ++++++++++++++++++++++++++++++++++++++++- 2 files changed, 41 insertions(+), 2 deletions(-) diff --git a/.gitignore b/.gitignore index 19b2b37929ae..4bab4c519a1c 100644 --- a/.gitignore +++ b/.gitignore @@ -47,4 +47,4 @@ test*.bak /juicefs.lite dist/ juicefs.egg-info/ -pkg/meta/jfs-load-dump/ \ No newline at end of file +pkg/meta/jfs-load-dump/juicefs-fork diff --git a/pkg/fuse/passthrough.go b/pkg/fuse/passthrough.go index c3e7201c69d9..2627bcffc44c 100644 --- a/pkg/fuse/passthrough.go +++ b/pkg/fuse/passthrough.go @@ -50,6 +50,7 @@ type passthroughState struct { mu sync.Mutex files map[uint64]*ptFile // keyed by fh + busy map[Ino]int // inodes with a passthrough open or pending reconcile pool []*ptBacking // idle registered backings, truncated to 0 poolSeq int disabled bool // registration failed with a permanent error; stop trying @@ -95,7 +96,12 @@ func newPassthroughState(server *fuse.Server, dir string) *passthroughState { logger.Warnf("passthrough: per-process staging dir under %s: %s; using base", base, err) sub = base } - return &passthroughState{server: server, dir: sub, files: make(map[uint64]*ptFile)} + return &passthroughState{ + server: server, + dir: sub, + files: make(map[uint64]*ptFile), + busy: make(map[Ino]int), + } } // checkout returns an idle registered backing, or registers a fresh one. @@ -206,8 +212,25 @@ func (p *passthroughState) tryOpen(ino Ino, fh uint64, flags uint32, emptyAtOpen }) return 0, false } + // One passthrough writer per inode at a time. A second write-open while an + // earlier one is still open — or while its reconcile is in flight (during + // which the metadata size still reads 0, so emptyAtOpen looks true again) — + // would get its own empty backing whose linear reconcile overwrites the + // first's data. Reserve the inode; overlapping opens fall to the daemon + // path, which serializes correctly. + p.mu.Lock() + if p.disabled || p.busy[ino] > 0 { + p.mu.Unlock() + return 0, false + } + p.busy[ino]++ + p.mu.Unlock() + b, ok := p.checkout() if !ok { + p.mu.Lock() + p.releaseBusyLocked(ino) + p.mu.Unlock() return 0, false } p.mu.Lock() @@ -216,6 +239,15 @@ func (p *passthroughState) tryOpen(ino Ino, fh uint64, flags uint32, emptyAtOpen return b.backingID, true } +// releaseBusyLocked drops one busy reference for ino. Caller holds p.mu. +func (p *passthroughState) releaseBusyLocked(ino Ino) { + if n := p.busy[ino]; n <= 1 { + delete(p.busy, ino) + } else { + p.busy[ino] = n - 1 + } +} + // reconcile flushes a passthrough staging file back into JuiceFS slices, then // tears down the backing registration. Called on release, before vfs.Release. func (p *passthroughState) reconcile(ctx vfs.Context, v *vfs.VFS, fh uint64) { @@ -231,6 +263,13 @@ func (p *passthroughState) reconcile(ctx vfs.Context, v *vfs.VFS, fh uint64) { if pf == nil { return } + // Free the inode for a new passthrough open only once its data has fully + // landed (or been preserved on failure) — held across the whole reconcile. + defer func() { + p.mu.Lock() + p.releaseBusyLocked(pf.ino) + p.mu.Unlock() + }() // Fence for consistency points now that we know this is a passthrough // release with data to land: the application's close(2) has already // returned, but the data still lives only in the staging file until the From b5f97b9e2770e05693dbe5acf55bceb2f8925398 Mon Sep 17 00:00:00 2001 From: Haytham Abuelfutuh Date: Wed, 8 Jul 2026 02:19:17 +0000 Subject: [PATCH 09/14] fuse: wait out in-flight reconcile on reopen (close-to-open consistency) A reopen of a file just written via passthrough could race the async reconcile: until it lands, the size reads 0, so a new write (daemon or passthrough) interleaves with the reconcile's linear copy and is lost. Block Open (bounded 30s) until the inode's reconcile finishes, so the reopen sees authoritative size/content. Co-Authored-By: Claude Opus 4.8 (1M context) Claude-Session: https://claude.ai/code/session_01AWLe95UTvfBpTXptpBXLrB --- .gitignore | 1 + pkg/fuse/fuse.go | 5 +++++ pkg/fuse/passthrough.go | 28 ++++++++++++++++++++++++++++ 3 files changed, 34 insertions(+) diff --git a/.gitignore b/.gitignore index 4bab4c519a1c..bc3b01561e73 100644 --- a/.gitignore +++ b/.gitignore @@ -48,3 +48,4 @@ test*.bak dist/ juicefs.egg-info/ pkg/meta/jfs-load-dump/juicefs-fork +juicefs-fork diff --git a/pkg/fuse/fuse.go b/pkg/fuse/fuse.go index f7f3b7507e10..9ff1026a6950 100644 --- a/pkg/fuse/fuse.go +++ b/pkg/fuse/fuse.go @@ -252,6 +252,11 @@ func (fs *fileSystem) Create(cancel <-chan struct{}, in *fuse.CreateIn, name str func (fs *fileSystem) Open(cancel <-chan struct{}, in *fuse.OpenIn, out *fuse.OpenOut) (status fuse.Status) { ctx := fs.newContext(cancel, &in.InHeader) defer releaseContext(ctx) + // If this inode has a passthrough write still reconciling, wait for it so + // this open sees the file's real (post-reconcile) size and content rather + // than the transient empty state — otherwise a write here would race the + // reconcile's copy and lose data. + fs.pt.waitInode(Ino(in.NodeId)) entry, fh, err := fs.v.Open(ctx, Ino(in.NodeId), in.Flags) if err != 0 { return fuse.Status(err) diff --git a/pkg/fuse/passthrough.go b/pkg/fuse/passthrough.go index 2627bcffc44c..97a783c49a67 100644 --- a/pkg/fuse/passthrough.go +++ b/pkg/fuse/passthrough.go @@ -23,6 +23,7 @@ import ( "path/filepath" "sync" "syscall" + "time" "github.com/hanwen/go-fuse/v2/fuse" "github.com/juicedata/juicefs/pkg/vfs" @@ -248,6 +249,33 @@ func (p *passthroughState) releaseBusyLocked(ino Ino) { } } +// waitInode blocks (bounded) until ino has no passthrough open or in-flight +// reconcile. Called at the start of Open so a reopen of a file this session +// just wrote via passthrough observes the reconciled state: until reconcile +// lands, the metadata size still reads 0 and a new write (passthrough OR +// daemon) would race the reconcile's linear copy and silently lose data. +// After the wait the file's size is authoritative, so tryOpen's emptyAtOpen +// gate correctly sends the reopen down the daemon path. +func (p *passthroughState) waitInode(ino Ino) { + if p == nil { + return + } + deadline := time.Now().Add(30 * time.Second) + for { + p.mu.Lock() + busy := p.busy[ino] > 0 + p.mu.Unlock() + if !busy { + return + } + if time.Now().After(deadline) { + logger.Warnf("passthrough: waited 30s for in-flight reconcile of ino %d; proceeding", ino) + return + } + time.Sleep(2 * time.Millisecond) + } +} + // reconcile flushes a passthrough staging file back into JuiceFS slices, then // tears down the backing registration. Called on release, before vfs.Release. func (p *passthroughState) reconcile(ctx vfs.Context, v *vfs.VFS, fh uint64) { From 2bdc9029f774db679f2c39479128c2cb87ddf515 Mon Sep 17 00:00:00 2001 From: Haytham Abuelfutuh Date: Wed, 8 Jul 2026 03:02:55 +0000 Subject: [PATCH 10/14] fuse: close remaining passthrough durability holes (ENG26-874) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Four fixes for the deferred HIGH/MEDIUM passthrough hardening items: - SIGTERM force-exit: the 30s force-exit path now extends its deadline (10 min hard cap) while passthrough reconciles are in flight, so FlushAll can no longer commit a mid-copy prefix as a durable short file when umount is stuck. - fsync/fdatasync honesty: FSYNC on a passthrough fh used to return success while every byte sat only in the local staging file. It now copies the staging content into JuiceFS slices (open stays live, backing stays registered; release reconcile remains the authority) and flushes the writer, matching non-passthrough fsync durability. Copies are serialized per-open (ptFile.mu) and fenced with Begin/EndExternalFlush so checkpoints wait them out. - SIGHUP smooth upgrade: a handover while passthrough opens are live would strand their staging data (the successor has no record of them). The SIGHUP handler now drains — blocking new passthrough opens, bounded 10s — and refuses the restart if opens remain, re-enabling passthrough. - truncate divergence: SETATTR size changes never reach the backing file (the kernel only diverts read/write/mmap); mirror successful size changes onto the live backing so reads and the release-time reconcile don't undo the truncate. Also drops the obsolete google.golang.org/grpc/stats/opentelemetry carve-out requirement (package merged into grpc; ambiguous import). Co-Authored-By: Claude Fable 5 Claude-Session: https://claude.ai/code/session_011M2cGhQ5cyh5t9u7DrMv2y --- cmd/mount_unix.go | 23 ++++ go.mod | 1 - pkg/fuse/fuse.go | 27 ++++ pkg/fuse/passthrough.go | 196 +++++++++++++++++++++++------ pkg/fuse/passthrough_drain_test.go | 104 +++++++++++++++ 5 files changed, 313 insertions(+), 38 deletions(-) create mode 100644 pkg/fuse/passthrough_drain_test.go diff --git a/cmd/mount_unix.go b/cmd/mount_unix.go index 4e248559c149..55e120d815c6 100644 --- a/cmd/mount_unix.go +++ b/cmd/mount_unix.go @@ -937,6 +937,16 @@ func installHandler(m meta.Meta, mp string, v *vfs.VFS, blob object.ObjectStorag sig := <-signalChan logger.Infof("Received signal %s, exiting...", sig.String()) if sig == syscall.SIGHUP { + // A passthrough open's data lives only in this process's + // staging files; the successor inherits the FUSE session but + // has no record of them, so their release-time reconciles + // would never run and every byte written through them would + // be lost. Drain first; if live opens remain, refuse the + // handover and keep serving. + if !fuse.DrainPassthrough(time.Second * 10) { + logger.Warnf("passthrough opens still in flight, don't restart") + continue + } path := fmt.Sprintf("/tmp/state%d.json", os.Getppid()) if err := v.FlushAll(""); err == nil { fuse.Shutdown() @@ -955,6 +965,19 @@ func installHandler(m meta.Meta, mp string, v *vfs.VFS, blob object.ObjectStorag } go func() { time.Sleep(time.Second * 30) + // The umount is stuck, but a passthrough reconcile may still + // be copying staging data whose close(2) already returned; + // FlushAll now would commit the mid-copy prefix as a durable + // short file. Extend the deadline while reconciles are in + // flight, with a hard cap so a wedged reconcile can't hold + // the exit hostage. + hardStop := time.Now().Add(time.Minute * 10) + for v.ExternalFlushes() > 0 && time.Now().Before(hardStop) { + time.Sleep(time.Millisecond * 100) + } + if n := v.ExternalFlushes(); n > 0 { + logger.Errorf("force exit with %d unfinished passthrough reconcile(s); their files may be committed incomplete", n) + } if err := v.FlushAll(""); err != nil { logger.Errorf("flush all: %s", err) } diff --git a/go.mod b/go.mod index 7271317688c1..88bb957ec020 100644 --- a/go.mod +++ b/go.mod @@ -330,7 +330,6 @@ require ( google.golang.org/genproto/googleapis/api v0.0.0-20260414002931-afd174a4e478 // indirect google.golang.org/genproto/googleapis/rpc v0.0.0-20260414002931-afd174a4e478 // indirect google.golang.org/grpc v1.81.1 // indirect - google.golang.org/grpc/stats/opentelemetry v0.0.0-20240907200651-3ffb98b2c93a // indirect gopkg.in/ini.v1 v1.67.3 // indirect gopkg.in/natefinch/lumberjack.v2 v2.2.1 // indirect gopkg.in/yaml.v2 v2.4.0 // indirect diff --git a/pkg/fuse/fuse.go b/pkg/fuse/fuse.go index 9ff1026a6950..ad585bfada76 100644 --- a/pkg/fuse/fuse.go +++ b/pkg/fuse/fuse.go @@ -125,6 +125,12 @@ func (fs *fileSystem) SetAttr(cancel <-chan struct{}, in *fuse.SetAttrIn, out *f if err != 0 { return fuse.Status(err) } + if in.Valid&fuse.FATTR_SIZE != 0 { + // truncate(2)/ftruncate(2) never reach the backing file (the kernel + // only diverts read/write/mmap); mirror the new size onto any live + // passthrough backing so it doesn't diverge from the metadata. + fs.pt.truncate(Ino(in.NodeId), in.Size) + } fs.replyAttr(ctx, entry, &out.Attr, out.SetTimeout) return 0 } @@ -322,6 +328,12 @@ func (fs *fileSystem) Flush(cancel <-chan struct{}, in *fuse.FlushIn) fuse.Statu func (fs *fileSystem) Fsync(cancel <-chan struct{}, in *fuse.FsyncIn) (code fuse.Status) { ctx := fs.newContext(cancel, &in.InHeader) defer releaseContext(ctx) + // For a passthrough open the daemon's writer holds none of the data (it + // lives in the kernel backing file), so vfs.Fsync alone would succeed + // without making anything durable; reconcile the staging content first. + if handled, errno := fs.pt.fsync(ctx, fs.v, in.Fh); handled { + return fuse.Status(errno) + } err := fs.v.Fsync(ctx, Ino(in.NodeId), int(in.FsyncFlags), in.Fh) return fuse.Status(err) } @@ -575,6 +587,7 @@ func Serve(v *vfs.VFS, options string, xattrs, ioctl bool) error { if ptEnabled { imp.pt = newPassthroughState(fssrv, os.Getenv("JUICEFS_PASSTHROUGH_DIR")) + ptState = imp.pt logger.Infof("FUSE passthrough enabled (experimental); staging dir=%s", imp.pt.dir) } @@ -640,6 +653,7 @@ func GenFuseOpt(conf *vfs.Config, options string, mt int, noxattr, noacl bool, m } var fsserv *fuse.Server +var ptState *passthroughState func Shutdown() bool { if fsserv != nil { @@ -647,3 +661,16 @@ func Shutdown() bool { } return false } + +// DrainPassthrough blocks new passthrough opens and waits (bounded) for all +// live passthrough opens and in-flight reconciles to finish. Returns false — +// with passthrough re-enabled — if any remain; the caller must then refuse a +// session handover, because passthrough data exists only in this process's +// staging files and a successor has no record of them. Returns true +// immediately when passthrough is not enabled. +func DrainPassthrough(timeout time.Duration) bool { + if ptState == nil { + return true + } + return ptState.drain(timeout) +} diff --git a/pkg/fuse/passthrough.go b/pkg/fuse/passthrough.go index 97a783c49a67..a5dc6a0edccb 100644 --- a/pkg/fuse/passthrough.go +++ b/pkg/fuse/passthrough.go @@ -55,6 +55,7 @@ type passthroughState struct { pool []*ptBacking // idle registered backings, truncated to 0 poolSeq int disabled bool // registration failed with a permanent error; stop trying + paused bool // draining for handover/shutdown; refuse new passthrough opens warnOne sync.Once } @@ -75,6 +76,9 @@ type ptFile struct { ino Ino fh uint64 b *ptBacking + // mu serializes staging-content copies for this open: fsync-time copies + // against each other and against the final release-time reconcile. + mu sync.Mutex } func newPassthroughState(server *fuse.Server, dir string) *passthroughState { @@ -207,31 +211,36 @@ func (p *passthroughState) tryOpen(ino Ino, fh uint64, flags uint32, emptyAtOpen if vfs.IsSpecialNode(ino) { return 0, false } - if !p.server.SupportsPassthrough() { - p.warnOne.Do(func() { - logger.Warnf("FUSE passthrough requested but not supported by the kernel; falling back") - }) - return 0, false - } // One passthrough writer per inode at a time. A second write-open while an // earlier one is still open — or while its reconcile is in flight (during // which the metadata size still reads 0, so emptyAtOpen looks true again) — // would get its own empty backing whose linear reconcile overwrites the // first's data. Reserve the inode; overlapping opens fall to the daemon - // path, which serializes correctly. + // path, which serializes correctly. Checked before any server call so the + // disabled latch and a drain-for-handover pause short-circuit cheaply. p.mu.Lock() - if p.disabled || p.busy[ino] > 0 { + if p.disabled || p.paused || p.busy[ino] > 0 { p.mu.Unlock() return 0, false } p.busy[ino]++ p.mu.Unlock() - - b, ok := p.checkout() - if !ok { + release := func() { p.mu.Lock() p.releaseBusyLocked(ino) p.mu.Unlock() + } + + if !p.server.SupportsPassthrough() { + p.warnOne.Do(func() { + logger.Warnf("FUSE passthrough requested but not supported by the kernel; falling back") + }) + release() + return 0, false + } + b, ok := p.checkout() + if !ok { + release() return 0, false } p.mu.Lock() @@ -337,11 +346,44 @@ func (p *passthroughState) reconcile(ctx vfs.Context, v *vfs.VFS, fh uint64) { } }() - rf, err := os.Open(b.path) - if err != nil { - logger.Errorf("passthrough: reopen staging %s: %s", b.path, err) + pf.mu.Lock() + defer pf.mu.Unlock() + off, ok := p.copyStagingLocked(ctx, v, pf) + if !ok { + return + } + if e := v.Flush(ctx, pf.ino, fh, 0); e != 0 { + logger.Errorf("passthrough: reconcile flush ino %d: %s", pf.ino, e) return } + // Passthrough writes bypassed the daemon, so the kernel's cached size and + // page data for this inode are stale (size is still 0 from the empty + // create). Now that the slices + metadata are committed, invalidate both so + // readers in this mount session see the reconciled file (read-your-writes). + p.server.InodeNotify(uint64(pf.ino), -1, 0) // attributes (size/mtime) + p.server.InodeNotify(uint64(pf.ino), 0, int64(off)) // data range + // Data is safely in JuiceFS: recycle the registration for the next open. + if err := b.f.Truncate(0); err != nil { + logger.Warnf("passthrough: truncate staging %s: %s", b.path, err) + return // defer retires it + } + done = true + p.checkin(b) +} + +// copyStagingLocked copies the full staging content of pf into JuiceFS slices +// via the normal writer path. Returns the byte count and false on any read or +// write error — a read error mid-copy must NOT be mistaken for end-of-file, +// or the caller would flush+commit a truncated file as if complete. Always a +// FULL copy from offset 0: passthrough writes land in the backing at +// arbitrary offsets, so an incremental "since last copy" scheme would miss +// overwrites of already-copied ranges. Caller holds pf.mu. +func (p *passthroughState) copyStagingLocked(ctx vfs.Context, v *vfs.VFS, pf *ptFile) (uint64, bool) { + rf, err := os.Open(pf.b.path) + if err != nil { + logger.Errorf("passthrough: reopen staging %s: %s", pf.b.path, err) + return 0, false + } defer rf.Close() buf := make([]byte, 4<<20) var off uint64 @@ -352,39 +394,119 @@ func (p *passthroughState) reconcile(ctx vfs.Context, v *vfs.VFS, fh uint64) { // own backing array so the next Read doesn't corrupt a pending slice. chunk := make([]byte, n) copy(chunk, buf[:n]) - if e := v.Write(ctx, pf.ino, chunk, off, fh); e != 0 { - logger.Errorf("passthrough: reconcile write ino %d off %d: %s", pf.ino, off, e) - return + if e := v.Write(ctx, pf.ino, chunk, off, pf.fh); e != 0 { + logger.Errorf("passthrough: staging copy write ino %d off %d: %s", pf.ino, off, e) + return off, false } off += uint64(n) } if err != nil { if err != io.EOF { - // A read error mid-copy must NOT be mistaken for end-of-file: - // that would flush+commit a truncated file as if complete. Bail - // so the defer preserves the staging and the file is not sealed - // short. - logger.Errorf("passthrough: read staging %s at off %d: %s", b.path, off, err) - return + logger.Errorf("passthrough: read staging %s at off %d: %s", pf.b.path, off, err) + return off, false } break } } - if e := v.Flush(ctx, pf.ino, fh, 0); e != 0 { - logger.Errorf("passthrough: reconcile flush ino %d: %s", pf.ino, e) + return off, true +} + +// fsync makes fsync(2)/fdatasync(2) honest for a passthrough open. The +// kernel diverts only read/write/mmap to the backing file — FSYNC still +// reaches the daemon, whose writer has no data for this fh, so plain +// vfs.Fsync would report success while every byte still sits in a local +// staging file that a crash before release would lose. Instead, copy the +// staging content into JuiceFS slices now (the open stays live and the +// backing stays registered; the release-time reconcile recopies and remains +// the authority on final content) and then flush the writer, giving the +// caller exactly the durability a non-passthrough fsync provides. +// +// Returns handled=false when fh is not a passthrough open (caller proceeds +// with the normal path). +func (p *passthroughState) fsync(ctx vfs.Context, v *vfs.VFS, fh uint64) (bool, syscall.Errno) { + if p == nil { + return false, 0 + } + p.mu.Lock() + pf := p.files[fh] + p.mu.Unlock() + if pf == nil { + return false, 0 + } + pf.mu.Lock() + defer pf.mu.Unlock() + // Fence: a checkpoint/commit racing this copy must wait for it, exactly + // as for a release-time reconcile, or it could snapshot a mid-copy state + // of a file the application believes it just made durable. + v.BeginExternalFlush() + defer v.EndExternalFlush() + if _, ok := p.copyStagingLocked(ctx, v, pf); !ok { + // The open is still live and release will retry the copy; report the + // failure so the application does not trust this fsync. + return true, syscall.EIO + } + if e := v.Fsync(ctx, pf.ino, 0, fh); e != 0 { + return true, e + } + return true, 0 +} + +// truncate mirrors a successful SETATTR size change onto the inode's live +// passthrough backing, if any. truncate(2)/ftruncate(2) reach the daemon — +// the kernel never diverts SETATTR to the backing — so without this the +// backing keeps its old length and diverges from the size the caller just +// set: reads through the passthrough fd see the old data/EOF, and the +// release-time reconcile (linear copy of the staging, authority on final +// content) would silently undo the truncate. +func (p *passthroughState) truncate(ino Ino, size uint64) { + if p == nil { return } - // Passthrough writes bypassed the daemon, so the kernel's cached size and - // page data for this inode are stale (size is still 0 from the empty - // create). Now that the slices + metadata are committed, invalidate both so - // readers in this mount session see the reconciled file (read-your-writes). - p.server.InodeNotify(uint64(pf.ino), -1, 0) // attributes (size/mtime) - p.server.InodeNotify(uint64(pf.ino), 0, int64(off)) // data range - // Data is safely in JuiceFS: recycle the registration for the next open. - if err := b.f.Truncate(0); err != nil { - logger.Warnf("passthrough: truncate staging %s: %s", b.path, err) - return // defer retires it + p.mu.Lock() + var pf *ptFile + for _, f := range p.files { + if f.ino == ino { + pf = f + break + } + } + p.mu.Unlock() + if pf == nil { + return + } + pf.mu.Lock() + defer pf.mu.Unlock() + if err := pf.b.f.Truncate(int64(size)); err != nil { + logger.Errorf("passthrough: mirror truncate ino %d to %d on %s: %s", ino, size, pf.b.path, err) + } +} + +// drain blocks new passthrough opens and waits (bounded) until no inode has +// a live passthrough open or in-flight reconcile. Used before a smooth +// upgrade (SIGHUP handover): a passthrough open's data exists only in this +// process's staging files, which the successor has no record of, so handing +// over while any are live silently loses every byte written through them. +// On timeout it re-enables passthrough and returns false — the caller must +// refuse the handover and keep serving. +func (p *passthroughState) drain(timeout time.Duration) bool { + p.mu.Lock() + p.paused = true + p.mu.Unlock() + deadline := time.Now().Add(timeout) + for { + p.mu.Lock() + n := len(p.busy) + p.mu.Unlock() + if n == 0 { + return true + } + if time.Now().After(deadline) { + p.mu.Lock() + p.paused = false + p.mu.Unlock() + logger.Warnf("passthrough: %d inode(s) still have live passthrough opens or reconciles after %s", n, timeout) + return false + } + time.Sleep(10 * time.Millisecond) } - done = true - p.checkin(b) } diff --git a/pkg/fuse/passthrough_drain_test.go b/pkg/fuse/passthrough_drain_test.go new file mode 100644 index 000000000000..b2328e44e852 --- /dev/null +++ b/pkg/fuse/passthrough_drain_test.go @@ -0,0 +1,104 @@ +//go:build linux + +/* + * JuiceFS, Copyright 2026 Juicedata, Inc. + * Licensed under the Apache License, Version 2.0 (the "License"). + */ + +package fuse + +import ( + "os" + "path/filepath" + "testing" + "time" +) + +// A paused state (drain for smooth-upgrade handover) must refuse new +// passthrough opens before touching the server — handing out a backing +// during a handover would strand its data in a staging file the successor +// has no record of. +func TestTryOpenRefusedWhilePaused(t *testing.T) { + p := &passthroughState{dir: t.TempDir(), files: make(map[uint64]*ptFile), busy: make(map[Ino]int), paused: true} + // nil server: reaching SupportsPassthrough/checkout would panic, proving + // the pause gate short-circuits first. + if id, ok := p.tryOpen(Ino(2), 1, 0x8002 /* O_RDWR|... */, true); ok || id != 0 { + t.Fatalf("tryOpen while paused = (%d,%v), want (0,false)", id, ok) + } + if len(p.busy) != 0 { + t.Fatalf("paused tryOpen leaked a busy reservation: %v", p.busy) + } +} + +// drain returns true immediately when nothing is in flight and leaves the +// state paused (the caller is about to hand the session over); with a live +// open it must time out, RE-ENABLE passthrough, and return false so the +// caller refuses the handover instead of losing the open's staging data. +func TestDrainForHandover(t *testing.T) { + p := &passthroughState{dir: t.TempDir(), files: make(map[uint64]*ptFile), busy: make(map[Ino]int)} + if !p.drain(time.Millisecond) { + t.Fatalf("drain with no passthrough opens should succeed") + } + if !p.paused { + t.Fatalf("successful drain must leave the state paused for the handover") + } + + p2 := &passthroughState{dir: t.TempDir(), files: make(map[uint64]*ptFile), busy: map[Ino]int{Ino(7): 1}} + start := time.Now() + if p2.drain(50 * time.Millisecond) { + t.Fatalf("drain with a live passthrough open must fail") + } + if time.Since(start) < 50*time.Millisecond { + t.Fatalf("drain returned before its deadline") + } + if p2.paused { + t.Fatalf("failed drain must re-enable passthrough (handover was refused)") + } +} + +// truncate must mirror a SETATTR size change onto the inode's live backing: +// the kernel never diverts SETATTR to the backing, so without the mirror the +// staging keeps its old length and the release-time reconcile would undo the +// truncate (and reads through the passthrough fd would see stale bytes). +func TestTruncateMirrorsOntoBacking(t *testing.T) { + dir := t.TempDir() + path := filepath.Join(dir, "pool-1.tmp") + f, err := os.OpenFile(path, os.O_RDWR|os.O_CREATE, 0600) + if err != nil { + t.Fatal(err) + } + defer f.Close() + if _, err := f.Write([]byte("0123456789")); err != nil { + t.Fatal(err) + } + p := &passthroughState{dir: dir, files: make(map[uint64]*ptFile), busy: make(map[Ino]int)} + pf := &ptFile{ino: Ino(42), fh: 9, b: &ptBacking{path: path, f: f}} + p.files[9] = pf + + p.truncate(Ino(42), 4) // shrink + if st, _ := os.Stat(path); st.Size() != 4 { + t.Fatalf("backing size after shrink = %d, want 4", st.Size()) + } + p.truncate(Ino(42), 16) // extend (sparse zeros) + if st, _ := os.Stat(path); st.Size() != 16 { + t.Fatalf("backing size after extend = %d, want 16", st.Size()) + } + // A size change for an inode with no live passthrough open is a no-op. + p.truncate(Ino(43), 1) + if st, _ := os.Stat(path); st.Size() != 16 { + t.Fatalf("truncate of unrelated inode disturbed the backing: %d", st.Size()) + } +} + +// fsync on a non-passthrough fh is not handled (the caller proceeds with the +// plain vfs path), including on a nil state. +func TestFsyncPassesThroughUnknownFh(t *testing.T) { + var nilState *passthroughState + if handled, _ := nilState.fsync(nil, nil, 1); handled { + t.Fatalf("nil state must not handle fsync") + } + p := &passthroughState{dir: t.TempDir(), files: make(map[uint64]*ptFile), busy: make(map[Ino]int)} + if handled, _ := p.fsync(nil, nil, 99); handled { + t.Fatalf("unknown fh must not be handled") + } +} From 26bf42180e6b9408531ba037f63ffbe53c4976ea Mon Sep 17 00:00:00 2001 From: Haytham Abuelfutuh Date: Wed, 8 Jul 2026 06:53:49 +0000 Subject: [PATCH 11/14] fuse: note the fsync exception in the passthrough durability contract Co-Authored-By: Claude Fable 5 Claude-Session: https://claude.ai/code/session_011M2cGhQ5cyh5t9u7DrMv2y --- pkg/fuse/passthrough.go | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/pkg/fuse/passthrough.go b/pkg/fuse/passthrough.go index a5dc6a0edccb..163bcbd48636 100644 --- a/pkg/fuse/passthrough.go +++ b/pkg/fuse/passthrough.go @@ -34,7 +34,9 @@ import ( // for write gets a local staging file (on a non-stacked fs) that the kernel // reads/writes directly via FUSE_PASSTHROUGH, bypassing the daemon per-op. On // release the staging file is reconciled into JuiceFS slices via the normal -// writer path. Durability is therefore deferred to release (commit-style). +// writer path. Durability is therefore deferred to release (commit-style), +// with one exception: fsync(2)/fdatasync(2) reconcile immediately (see +// fsync below), so an application's explicit sync point means what it says. // // Backing registrations are POOLED: registering a backing fd costs an ioctl // (or, on unprivileged broker mounts, an RPC round trip to the node broker) From d1c5b3d19fd50196147345479adf11d0ae3ac48d Mon Sep 17 00:00:00 2001 From: Haytham Abuelfutuh Date: Fri, 10 Jul 2026 07:11:21 +0000 Subject: [PATCH 12/14] build: point go-fuse replace at the unionai fork (repo transferred from EngHabu) --- go.mod | 2 +- go.sum | 8 ++------ 2 files changed, 3 insertions(+), 7 deletions(-) diff --git a/go.mod b/go.mod index 88bb957ec020..fc623c7d6179 100644 --- a/go.mod +++ b/go.mod @@ -346,7 +346,7 @@ require ( replace github.com/minio/minio v0.0.0-20210206053228-97fe57bba92c => github.com/juicedata/minio v0.0.0-20260515071949-69a6cfc9da65 -replace github.com/hanwen/go-fuse/v2 => github.com/EngHabu/go-fuse/v2 v2.1.1-0.20260624053043-ef6e8dc3c14e +replace github.com/hanwen/go-fuse/v2 => github.com/unionai/go-fuse/v2 v2.1.1-0.20260624053043-ef6e8dc3c14e replace github.com/dgrijalva/jwt-go v3.2.0+incompatible => github.com/golang-jwt/jwt v3.2.1+incompatible diff --git a/go.sum b/go.sum index 3e460d509f40..97d0d0614a89 100644 --- a/go.sum +++ b/go.sum @@ -59,12 +59,6 @@ github.com/BurntSushi/toml v1.3.2/go.mod h1:CxXYINrC8qIiEnFrOxCa7Jy5BFHlXnUU2pbi github.com/DataDog/datadog-go v3.2.0+incompatible/go.mod h1:LButxg5PwREeZtORoXG3tL4fMGNddJ+vMq1mwgfaqoQ= github.com/DataDog/zstd v1.5.6 h1:LbEglqepa/ipmmQJUDnSsfvA8e8IStVcGaFWDuxvGOY= github.com/DataDog/zstd v1.5.6/go.mod h1:g4AWEaM3yOg3HYfnJ3YIawPnVdXJh9QME85blwSAmyw= -github.com/EngHabu/go-fuse/v2 v2.1.1-0.20260624004144-146975152cf7 h1:xeVHS9aZZv7lmalxlKBj7X3AuZwe4bvxiXYcsHoEkFc= -github.com/EngHabu/go-fuse/v2 v2.1.1-0.20260624004144-146975152cf7/go.mod h1:xKwi1cF7nXAOBCXujD5ie0ZKsxc8GGSA1rlMJc+8IJs= -github.com/EngHabu/go-fuse/v2 v2.1.1-0.20260624051746-50dbae93aec4 h1:N+ALIK6ZvDtTXJYElmvxb2xLEzlWA1/n56jC05lOLAU= -github.com/EngHabu/go-fuse/v2 v2.1.1-0.20260624051746-50dbae93aec4/go.mod h1:xKwi1cF7nXAOBCXujD5ie0ZKsxc8GGSA1rlMJc+8IJs= -github.com/EngHabu/go-fuse/v2 v2.1.1-0.20260624053043-ef6e8dc3c14e h1:YiDEZO1tvy2jRF/OKaaXiVNgdmQ0XbCxwKq6wPWvAa0= -github.com/EngHabu/go-fuse/v2 v2.1.1-0.20260624053043-ef6e8dc3c14e/go.mod h1:xKwi1cF7nXAOBCXujD5ie0ZKsxc8GGSA1rlMJc+8IJs= github.com/GoogleCloudPlatform/opentelemetry-operations-go/detectors/gcp v1.31.0 h1:DHa2U07rk8syqvCge0QIGMCE1WxGj9njT44GH7zNJLQ= github.com/GoogleCloudPlatform/opentelemetry-operations-go/detectors/gcp v1.31.0/go.mod h1:P4WPRUkOhJC13W//jWpyfJNDAIpvRbAUIYLX/4jtlE0= github.com/GoogleCloudPlatform/opentelemetry-operations-go/detectors/gcp v1.33.0 h1:l7+6kwRMJNwdCvYdDl7Eax+wzEYHSnNY7zrrfbhDdTA= @@ -876,6 +870,8 @@ github.com/tklauser/numcpus v0.6.1/go.mod h1:1XfjsgE2zo8GVw7POkMbHENHzVg3GzmoZ9f github.com/tv42/httpunix v0.0.0-20150427012821-b75d8614f926/go.mod h1:9ESjWnEqriFuLhtthL60Sar/7RFoluCcXsuvEwTV5KM= github.com/twmb/murmur3 v1.1.8 h1:8Yt9taO/WN3l08xErzjeschgZU2QSrwm1kclYq+0aRg= github.com/twmb/murmur3 v1.1.8/go.mod h1:Qq/R7NUyOfr65zD+6Q5IHKsJLwP7exErjN6lyyq3OSQ= +github.com/unionai/go-fuse/v2 v2.1.1-0.20260624053043-ef6e8dc3c14e h1:3DXRSotPxDCHSKY9FGChQSgInim3tFEPjtexoTxx2ps= +github.com/unionai/go-fuse/v2 v2.1.1-0.20260624053043-ef6e8dc3c14e/go.mod h1:xKwi1cF7nXAOBCXujD5ie0ZKsxc8GGSA1rlMJc+8IJs= github.com/valyala/bytebufferpool v1.0.0 h1:GqA5TC/0021Y/b9FG4Oi9Mr3q7XYx6KllzawFIhcdPw= github.com/valyala/bytebufferpool v1.0.0/go.mod h1:6bBcMArwyJ5K/AmCkWv1jt77kVWyCJ6HpOuEn7z0Csc= github.com/valyala/fasthttp v1.67.0 h1:tqKlJMUP6iuNG8hGjK/s9J4kadH7HLV4ijEcPGsezac= From aa93a4fa94bd54073305b1e16191d622e94c53d6 Mon Sep 17 00:00:00 2001 From: Haytham Abuelfutuh Date: Thu, 6 Aug 2026 05:23:19 +0000 Subject: [PATCH 13/14] fuse: never reuse passthrough backings across inodes MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Cross-inode reuse (register-once, checkin/checkout pool) was measurably cheaper for small-file workloads, but is unsafe: once mmap(2) is called on a passthrough fd, fuse_passthrough_mmap() repoints the VMA directly at the backing file (vma_set_file), decoupling the mapping from the fd. close(2) can still trigger reconcile+recycle while a dirty mapping keeps writing to what is now, after recycling, a different inode's staging file — silent cross-tenant data corruption, with no FUSE opcode able to observe mmap activity on a passthrough-diverted file to guard against it. Remove the pool entirely: checkout() always registers a fresh backing, and reconcile() always retires it (unregister + close + remove) instead of truncating and parking it for reuse. This also removes the truncate/checkin race window between a completing reconcile and a new open landing on the same backing. Co-Authored-By: Claude Sonnet 5 Claude-Session: https://claude.ai/code/session_0127fkXsGz1PMmfkFKERDmtM --- pkg/fuse/passthrough.go | 84 ++++++++++++++----------------- pkg/fuse/passthrough_pool_test.go | 41 +-------------- 2 files changed, 38 insertions(+), 87 deletions(-) diff --git a/pkg/fuse/passthrough.go b/pkg/fuse/passthrough.go index 163bcbd48636..1729bac8b391 100644 --- a/pkg/fuse/passthrough.go +++ b/pkg/fuse/passthrough.go @@ -38,15 +38,24 @@ import ( // with one exception: fsync(2)/fdatasync(2) reconcile immediately (see // fsync below), so an application's explicit sync point means what it says. // -// Backing registrations are POOLED: registering a backing fd costs an ioctl -// (or, on unprivileged broker mounts, an RPC round trip to the node broker) -// plus a staging-file create, once per write-open. Small-file workloads open -// thousands of times, so instead of register-at-open/unregister-at-release, -// reconciled staging files are truncated to zero and parked for the next -// open; after warm-up a small-file loop performs no registrations at all -// (ENG26-869). A backing is never attached to two live opens at once: -// checkout is exclusive, and a backing returns to the pool only after its -// reconcile finished (data copied out, file truncated). +// Backing registrations are NOT reused across opens: each write-open gets a +// fresh registration (a new ioctl or, on unprivileged broker mounts, a new +// RPC round trip to the node broker, plus a fresh staging-file create), and +// reconcile() unregisters and removes it — for a DIFFERENT inode's next open, +// never the same one. This was originally pooled (checked-in backings parked +// for reuse by whichever inode opened next), which is measurably cheaper for +// small-file-heavy workloads, but pooling handed a backing file across +// inodes: once mmap(2) has been called on a passthrough fd, the kernel's +// fuse_passthrough_mmap() repoints the VMA directly at the backing file +// (vma_set_file), fully decoupling the mapping's lifetime from the fd — a +// process can close(2) (triggering reconcile+recycle) while a still-live, +// still-dirty mapping keeps writing to what is now, after recycling, a +// DIFFERENT inode's staging file. There is no FUSE opcode or other userspace +// signal that observes mmap activity on a passthrough-diverted file (the +// entire point of passthrough is to bypass such upcalls), so there is no way +// to detect "this backing might still be mmap'd" and only pool the ones that +// aren't. Reusing a backing across inodes is a real, silent cross-tenant +// data-corruption vector; not reusing it is not. type passthroughState struct { server *fuse.Server dir string @@ -54,18 +63,12 @@ type passthroughState struct { mu sync.Mutex files map[uint64]*ptFile // keyed by fh busy map[Ino]int // inodes with a passthrough open or pending reconcile - pool []*ptBacking // idle registered backings, truncated to 0 - poolSeq int - disabled bool // registration failed with a permanent error; stop trying - paused bool // draining for handover/shutdown; refuse new passthrough opens + seq int // monotonic counter for unique staging file names + disabled bool // registration failed with a permanent error; stop trying + paused bool // draining for handover/shutdown; refuse new passthrough opens warnOne sync.Once } -// ptPoolCap bounds the idle registered backings kept per mount. Each entry -// pins one kernel backing registration and one empty staging file; the cap -// only needs to cover the plausible number of concurrent write-opens. -const ptPoolCap = 64 - // ptBacking is one registered kernel backing: a staging file plus the // backing ID the kernel handed back for it. It outlives individual opens. type ptBacking struct { @@ -111,21 +114,17 @@ func newPassthroughState(server *fuse.Server, dir string) *passthroughState { } } -// checkout returns an idle registered backing, or registers a fresh one. +// checkout registers a fresh backing for a new passthrough open. Always a new +// registration — see the passthroughState doc comment for why a backing is +// never reused across inodes. func (p *passthroughState) checkout() (*ptBacking, bool) { p.mu.Lock() if p.disabled { p.mu.Unlock() return nil, false } - if n := len(p.pool); n > 0 { - b := p.pool[n-1] - p.pool = p.pool[:n-1] - p.mu.Unlock() - return b, true - } - p.poolSeq++ - seq := p.poolSeq + p.seq++ + seq := p.seq p.mu.Unlock() path := filepath.Join(p.dir, fmt.Sprintf("pool-%d.tmp", seq)) @@ -164,16 +163,10 @@ func (p *passthroughState) checkout() (*ptBacking, bool) { return &ptBacking{path: path, f: f, backingID: id}, true } -// checkin parks a reconciled backing for reuse, or retires it when the pool -// is full. The staging file MUST already be truncated to zero. -func (p *passthroughState) checkin(b *ptBacking) { - p.mu.Lock() - if len(p.pool) < ptPoolCap { - p.pool = append(p.pool, b) - p.mu.Unlock() - return - } - p.mu.Unlock() +// retire tears a backing down for good: unregisters the kernel registration, +// closes the local fd, and removes the staging file. Never reused by another +// inode — see the passthroughState doc comment. +func (p *passthroughState) retire(b *ptBacking) { if errno := p.server.UnregisterBackingFd(b.backingID); errno != 0 { logger.Warnf("passthrough: UnregisterBackingFd(%d): %s", b.backingID, errno) } @@ -322,10 +315,10 @@ func (p *passthroughState) reconcile(ctx vfs.Context, v *vfs.VFS, fh uint64) { // The kernel stops issuing passthrough I/O for this open once its release // is processed, and reconcile runs from the RELEASE handler — after the // application's last close, so no writes are in flight. The registration - // itself is kept alive for reuse (see checkin); read the staging content - // through a fresh path-open fd for a clean sequential pass (reading via - // the registered backing fd can return stale/partial data), then truncate - // and park the backing for the next open. + // is retired for good once reconciled (see retire) rather than reused by + // a future open, which may belong to a different inode; read the staging + // content through a fresh path-open fd for a clean sequential pass + // (reading via the registered backing fd can return stale/partial data). b := pf.b done := false defer func() { @@ -364,13 +357,10 @@ func (p *passthroughState) reconcile(ctx vfs.Context, v *vfs.VFS, fh uint64) { // readers in this mount session see the reconciled file (read-your-writes). p.server.InodeNotify(uint64(pf.ino), -1, 0) // attributes (size/mtime) p.server.InodeNotify(uint64(pf.ino), 0, int64(off)) // data range - // Data is safely in JuiceFS: recycle the registration for the next open. - if err := b.f.Truncate(0); err != nil { - logger.Warnf("passthrough: truncate staging %s: %s", b.path, err) - return // defer retires it - } + // Data is safely in JuiceFS: retire the registration and staging file for + // good. No truncate needed — retire removes the file outright. done = true - p.checkin(b) + p.retire(b) } // copyStagingLocked copies the full staging content of pf into JuiceFS slices diff --git a/pkg/fuse/passthrough_pool_test.go b/pkg/fuse/passthrough_pool_test.go index 19e587a536c5..9392a8dee5cd 100644 --- a/pkg/fuse/passthrough_pool_test.go +++ b/pkg/fuse/passthrough_pool_test.go @@ -22,45 +22,6 @@ import ( "testing" ) -// TestPassthroughPoolReuse: a checked-in backing is handed back by the next -// checkout without a new registration (no server round trip), and pooled -// entries come back most-recently-parked first. -func TestPassthroughPoolReuse(t *testing.T) { - dir := t.TempDir() - p := &passthroughState{dir: dir, files: make(map[uint64]*ptFile)} - - mk := func(name string) *ptBacking { - f, err := os.OpenFile(filepath.Join(dir, name), os.O_RDWR|os.O_CREATE, 0600) - if err != nil { - t.Fatal(err) - } - return &ptBacking{path: f.Name(), f: f, backingID: int32(len(p.pool) + 1)} - } - - b1, b2 := mk("pool-1.tmp"), mk("pool-2.tmp") - // Under the cap, checkin parks without touching the (nil) server — - // a server call here would panic the test. - p.checkin(b1) - p.checkin(b2) - if len(p.pool) != 2 { - t.Fatalf("pool size = %d, want 2", len(p.pool)) - } - - // Checkout must reuse parked backings (nil server: a registration - // attempt would panic), most recently parked first. - got, ok := p.checkout() - if !ok || got != b2 { - t.Fatalf("checkout = %v, %v; want %v (LIFO reuse)", got, ok, b2) - } - got, ok = p.checkout() - if !ok || got != b1 { - t.Fatalf("checkout = %v, %v; want %v", got, ok, b1) - } - if len(p.pool) != 0 { - t.Fatalf("pool size = %d, want 0", len(p.pool)) - } -} - // TestPassthroughDisabledLatch: once registration hit a permanent error the // state stops attempting registrations entirely — checkout must return false // before touching the (nil) server or the filesystem. @@ -69,7 +30,7 @@ func TestPassthroughDisabledLatch(t *testing.T) { if b, ok := p.checkout(); ok || b != nil { t.Fatalf("checkout on disabled state = %v, %v; want nil, false", b, ok) } - if p.poolSeq != 0 { + if p.seq != 0 { t.Fatalf("disabled checkout still allocated a staging sequence") } } From 37964485f9c9d5a251c5af4401c01c0e86358142 Mon Sep 17 00:00:00 2001 From: Haytham Abuelfutuh Date: Thu, 6 Aug 2026 14:32:06 +0000 Subject: [PATCH 14/14] fuse: account passthrough reconcile's copy buffer against the memory budget MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit copyStagingLocked's 4MB read buffer was a raw make(), invisible to utils.AllocMemory() — the counter vfs/writer.go and vfs/compact.go use as backpressure (utils.AllocMemory()-store.UsedMemory() vs BufferSize). Concurrent reconciles could inflate RSS well past the configured budget without the writer/compactor ever seeing it and throttling. Use the pooled utils.Alloc/Free instead, same as chunk/page.go already does for its buffers. Co-Authored-By: Claude Sonnet 5 Claude-Session: https://claude.ai/code/session_0127fkXsGz1PMmfkFKERDmtM --- pkg/fuse/passthrough.go | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/pkg/fuse/passthrough.go b/pkg/fuse/passthrough.go index 1729bac8b391..15dd75742796 100644 --- a/pkg/fuse/passthrough.go +++ b/pkg/fuse/passthrough.go @@ -26,6 +26,7 @@ import ( "time" "github.com/hanwen/go-fuse/v2/fuse" + "github.com/juicedata/juicefs/pkg/utils" "github.com/juicedata/juicefs/pkg/vfs" ) @@ -377,7 +378,14 @@ func (p *passthroughState) copyStagingLocked(ctx vfs.Context, v *vfs.VFS, pf *pt return 0, false } defer rf.Close() - buf := make([]byte, 4<<20) + // Pooled/accounted allocation: vfs.writer's and vfs/compact's memory + // backpressure (utils.AllocMemory()-store.UsedMemory()) only sees bytes + // allocated through utils.Alloc. A raw make() here would be invisible to + // that accounting, so concurrent reconciles could balloon RSS well past + // the configured buffer budget without the compactor/writer ever + // throttling in response. + buf := utils.Alloc(4 << 20) + defer utils.Free(buf) var off uint64 for { n, err := rf.Read(buf)