diff --git a/CHANGELOG.md b/CHANGELOG.md index 14b77c719c..33506c72ab 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,6 @@ ## 2.5.3 (Unreleased) **Features** +- Add rate limit functionality for ingress bandwidth (bytes downloaded per second) and operations per second ([PR #2093](https://github.com/Azure/azure-storage-fuse/pull/2093)) **Bug Fixes** diff --git a/NOTICE b/NOTICE index fab5a4c6c1..828f5e7a3c 100644 --- a/NOTICE +++ b/NOTICE @@ -4559,4 +4559,45 @@ Apache License + + + + +**************************************************************************** + +============================================================================ +>>> golang.org/x/time +============================================================================== + +Copyright 2009 The Go Authors. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are +met: + + * Redistributions of source code must retain the above copyright +notice, this list of conditions and the following disclaimer. + * Redistributions in binary form must reproduce the above +copyright notice, this list of conditions and the following disclaimer +in the documentation and/or other materials provided with the +distribution. + * Neither the name of Google LLC nor the names of its +contributors may be used to endorse or promote products derived from +this software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + + + + --------------------- END OF THIRD PARTY NOTICE -------------------------------- diff --git a/component/azstorage/azstorage.go b/component/azstorage/azstorage.go index db19803b1a..f4c6a2b1b1 100644 --- a/component/azstorage/azstorage.go +++ b/component/azstorage/azstorage.go @@ -693,6 +693,12 @@ func init() { blobFilter := config.AddStringFlag("filter", "", "Filter string to match blobs. For details refer [https://github.com/Azure/azure-storage-fuse?tab=readme-ov-file#blob-filter]") config.BindPFlag(compName+".filter", blobFilter) + capMbpsRead := config.AddInt64Flag("cap-mbps-read", -1, "Limit the throughput of downloads from your storage account. Value measured in megabits per second. Default is -1 (no limit)") + config.BindPFlag(compName+".cap-mbps-read", capMbpsRead) + + capIOps := config.AddInt64Flag("cap-iops", -1, "Limit the total storage operations per second. Default is -1 (no limit)") + config.BindPFlag(compName+".cap-iops", capIOps) + config.RegisterFlagCompletionFunc("container-name", func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) { return nil, cobra.ShellCompDirectiveNoFileComp }) diff --git a/component/azstorage/config.go b/component/azstorage/config.go index 3b783b7b01..b04a50b13b 100644 --- a/component/azstorage/config.go +++ b/component/azstorage/config.go @@ -195,6 +195,8 @@ type AzStorageOptions struct { PreserveACL bool `config:"preserve-acl" yaml:"preserve-acl"` Filter string `config:"filter" yaml:"filter"` UserAssertion string `config:"user-assertion" yaml:"user-assertions"` + CapMbpsRead int64 `config:"cap-mbps-read" yaml:"cap-mbps-read"` + CapIOps int64 `config:"cap-iops" yaml:"cap-iops"` // v1 support UseAdls bool `config:"use-adls" yaml:"-"` @@ -537,7 +539,8 @@ func ParseAndValidateConfig(az *AzStorage, opt AzStorageOptions) error { log.Crit("ParseAndValidateConfig : Retry Config: retry-count %d, max-timeout %d, backoff-time %d, max-delay %d, preserve-acl: %v", az.stConfig.maxRetries, az.stConfig.maxTimeout, az.stConfig.backoffTime, az.stConfig.maxRetryDelay, az.stConfig.preserveACL) - log.Crit("ParseAndValidateConfig : Telemetry : %s, honour-ACL %v", az.stConfig.telemetry, az.stConfig.honourACL) + log.Crit("ParseAndValidateConfig : Telemetry : %s, honour-ACL %v, cap-mbps-read %d, cap-iops %d", + az.stConfig.telemetry, az.stConfig.honourACL, az.stConfig.capMbpsRead, az.stConfig.capIOps) return nil } @@ -630,5 +633,17 @@ func ParseAndReadDynamicConfig(az *AzStorage, opt AzStorageOptions, reload bool) } } + // Rate limiting, default is no limit + az.stConfig.capMbpsRead = -1 + az.stConfig.capIOps = -1 + + if opt.CapMbpsRead > 0 { + az.stConfig.capMbpsRead = opt.CapMbpsRead + } + + if opt.CapIOps > 0 { + az.stConfig.capIOps = opt.CapIOps + } + return nil } diff --git a/component/azstorage/config_test.go b/component/azstorage/config_test.go index 540b68d55e..576582da14 100644 --- a/component/azstorage/config_test.go +++ b/component/azstorage/config_test.go @@ -437,6 +437,40 @@ func (s *configTestSuite) TestSASRefresh() { assert.NoError(err) } +func (s *configTestSuite) TestRateLimitConfig() { + defer config.ResetConfig() + assert := assert.New(s.T()) + az := &AzStorage{} + opt := AzStorageOptions{} + opt.AccountName = "abcd" + opt.Container = "abcd" + opt.AuthMode = "key" + opt.AccountKey = "abcd" + + // Test default values (no limit) + err := ParseAndReadDynamicConfig(az, opt, false) + assert.NoError(err) + assert.Equal(int64(-1), az.stConfig.capMbpsRead) + assert.Equal(int64(-1), az.stConfig.capIOps) + + // Test setting limits + opt.CapMbpsRead = 100 + opt.CapIOps = 10 + err = ParseAndReadDynamicConfig(az, opt, false) + assert.NoError(err) + assert.Equal(int64(100), az.stConfig.capMbpsRead) + assert.Equal(int64(10), az.stConfig.capIOps) + + // Test setting only one limit + opt.CapMbpsRead = 200 + opt.CapIOps = 0 // reset to no limit + + err = ParseAndReadDynamicConfig(az, opt, false) + assert.NoError(err) + assert.Equal(int64(200), az.stConfig.capMbpsRead) + assert.Equal(int64(-1), az.stConfig.capIOps) +} + func TestConfigTestSuite(t *testing.T) { suite.Run(t, new(configTestSuite)) } diff --git a/component/azstorage/connection.go b/component/azstorage/connection.go index 85f088f96f..d880c0967b 100644 --- a/component/azstorage/connection.go +++ b/component/azstorage/connection.go @@ -85,6 +85,10 @@ type AzStorageConfig struct { // Blob filters filter *blobfilter.BlobFilter + + // Rate limiting + capMbpsRead int64 + capIOps int64 } type AzStorageConnection struct { diff --git a/component/azstorage/policies.go b/component/azstorage/policies.go index 121e8e5aec..8ff74cec80 100644 --- a/component/azstorage/policies.go +++ b/component/azstorage/policies.go @@ -35,10 +35,15 @@ package azstorage import ( "fmt" + "math" "net/http" + "strings" + + "golang.org/x/time/rate" "github.com/Azure/azure-sdk-for-go/sdk/azcore/policy" "github.com/Azure/azure-storage-fuse/v2/common" + "github.com/Azure/azure-storage-fuse/v2/common/log" ) // blobfuseTelemetryPolicy is a custom pipeline policy to prepend the blobfuse user agent string to the one coming from SDK. @@ -79,3 +84,101 @@ func (r *serviceVersionPolicy) Do(req *policy.Request) (*http.Response, error) { req.Raw().Header["x-ms-version"] = []string{r.serviceApiVersion} return req.Next() } + +// --------------------------------------------------------------------------------------------------------------------------------------------------- +// Policy to limit the rate of requests +type rateLimitingPolicy struct { + ingressBandwidthLimiter *rate.Limiter + opsLimiter *rate.Limiter +} + +func newRateLimitingPolicy(readBytesPerSec int64, opsPerSec int64) policy.Policy { + p := &rateLimitingPolicy{} + + // Use 10 second window for burst size calculation for rate limiter. + // This allows for short bursts while still enforcing the average rate over a reasonable time period. + // A larger window size would allow larger bursts but would be less effective at limiting short term spikes. + // A smaller window size would limit bursts more but could lead to underutilization of available bandwidth/ops. + // 10 seconds is a reasonable compromise between these factors. + // Note: The burst size is the maximum number of tokens that can be accumulated in the bucket. + // Therefore, a larger burst size allows for larger bursts of traffic, + // but does not affect the average rate over time. + // Users can adjust the bytesPerSec and opsPerSec values to fine-tune the rate limiting behavior as needed. + // For example, setting a higher bytesPerSec value will allow for higher average bandwidth, + // while setting a lower opsPerSec value will limit the number of operations per second. + const windowSize = 10 + + if readBytesPerSec > 0 { + ingressBandwidthBurstSize := readBytesPerSec * int64(windowSize) + burst := int(ingressBandwidthBurstSize) + // On 32-bit systems, int is 32-bit. If bandwidthBurstSize > MaxInt, we need to clamp it. + // math.MaxInt is platform dependent. + if ingressBandwidthBurstSize > int64(math.MaxInt) { + burst = math.MaxInt + } + + p.ingressBandwidthLimiter = rate.NewLimiter(rate.Limit(readBytesPerSec), burst) + log.Info("RateLimitingPolicy : Bandwidth limit set to %d bytes/sec with burst size of %d bytes", + readBytesPerSec, burst) + } + + if opsPerSec > 0 { + opsBurstSize := opsPerSec * int64(windowSize) + burst := int(opsBurstSize) + if opsBurstSize > int64(math.MaxInt) { + burst = math.MaxInt + } + + p.opsLimiter = rate.NewLimiter(rate.Limit(opsPerSec), burst) + log.Info("RateLimitingPolicy : Ops limit set to %d ops/sec with burst size of %d ops", + opsPerSec, burst) + } + + return p +} + +func (p *rateLimitingPolicy) Do(req *policy.Request) (*http.Response, error) { + ctx := req.Raw().Context() + + // Limit operations per second + if p.opsLimiter != nil { + // Wait for 1 token + err := p.opsLimiter.Wait(ctx) + if err != nil { + log.Err("RateLimitingPolicy : Ops limit wait failed [%s]", err.Error()) + return nil, err + } + } + + // Limit ingress bandwidth for blob downloads (Azure egress: data leaving Azure Storage). + // This policy intentionally applies only to GET requests, which represent download operations. + if p.ingressBandwidthLimiter != nil && req.Raw().Method == http.MethodGet { + // Check for x-ms-range header + // We are not using req.Raw().Header.Get() as it canonicalizes the header name. + // Whereas SDK stores the header in the request is stored in lower case. + // So we directly access the header map with lower case key. + // NOTE: using strings.ToLower to ignore the lint error regarding canonicalized headers. + rangeHeader := req.Raw().Header[strings.ToLower(X_Ms_Range)] + if len(rangeHeader) == 0 { + rangeHeader = req.Raw().Header[RangeHeader] + } + + if len(rangeHeader) > 0 { + size, err := parseRangeHeader(rangeHeader[0]) + if err == nil && size > 0 { + // Wait for tokens equal to size. + // NOTE: range size is guaranteed to be within int range by parseRangeHeader. + err := p.ingressBandwidthLimiter.WaitN(ctx, int(size)) + if err != nil { + log.Err("RateLimitingPolicy : Bandwidth limit wait failed [%s]", err.Error()) + return nil, err + } + } else if err != nil { + log.Err("RateLimitingPolicy : Failed to parse Range header %s: [%s]", rangeHeader[0], err.Error()) + return nil, err + } + } + } + + return req.Next() +} diff --git a/component/azstorage/policies_test.go b/component/azstorage/policies_test.go new file mode 100644 index 0000000000..04240a9f2f --- /dev/null +++ b/component/azstorage/policies_test.go @@ -0,0 +1,198 @@ +/* + _____ _____ _____ ____ ______ _____ ------ + | | | | | | | | | | | | | + | | | | | | | | | | | | | + | --- | | | | |-----| |---- | | |-----| |----- ------ + | | | | | | | | | | | | | + | ____| |_____ | ____| | ____| | |_____| _____| |_____ |_____ + + + Licensed under the MIT License . + + Copyright © 2020-2026 Microsoft Corporation. All rights reserved. + Author : + + Permission is hereby granted, free of charge, to any person obtaining a copy + of this software and associated documentation files (the "Software"), to deal + in the Software without restriction, including without limitation the rights + to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + copies of the Software, and to permit persons to whom the Software is + furnished to do so, subject to the following conditions: + + The above copyright notice and this permission notice shall be included in all + copies or substantial portions of the Software. + + THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + SOFTWARE +*/ + +package azstorage + +import ( + "context" + "net/http" + "testing" + "time" + + "github.com/Azure/azure-sdk-for-go/sdk/azcore/policy" + "github.com/Azure/azure-sdk-for-go/sdk/azcore/runtime" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/suite" +) + +type policiesTestSuite struct { + suite.Suite +} + +type mockTransport struct{} + +func (m *mockTransport) Do(req *http.Request) (*http.Response, error) { + return &http.Response{StatusCode: 200}, nil +} + +func (s *policiesTestSuite) TestRateLimitingPolicy_OpsLimit() { + assert := assert.New(s.T()) + + // Limit to 1 op/sec. Burst will be 1 * 10 = 10 ops. + p := newRateLimitingPolicy(-1, 1) + pipeline := runtime.NewPipeline("test", "v1", runtime.PipelineOptions{ + PerRetry: []policy.Policy{p}, + }, &policy.ClientOptions{Transport: &mockTransport{}}) + + req, err := runtime.NewRequest(context.Background(), http.MethodGet, "http://localhost") + assert.NoError(err) + + // Consume burst + for i := 0; i < 10; i++ { + _, err := pipeline.Do(req) + assert.NoError(err) + } + + // Next request should be delayed by ~1 sec + start := time.Now() + _, err = pipeline.Do(req) + assert.NoError(err) + duration := time.Since(start) + + // It should take at least 1 second (minus some tolerance) + assert.GreaterOrEqual(duration, 900*time.Millisecond, "Expected delay of ~1s, got %v", duration) +} + +func (s *policiesTestSuite) TestRateLimitingPolicy_BandwidthLimit() { + assert := assert.New(s.T()) + + // Limit 100 bytes/sec. Burst will be 100 * 10 = 1000 bytes. + p := newRateLimitingPolicy(100, -1) + pipeline := runtime.NewPipeline("test", "v1", runtime.PipelineOptions{ + PerRetry: []policy.Policy{p}, + }, &policy.ClientOptions{Transport: &mockTransport{}}) + + req, err := runtime.NewRequest(context.Background(), http.MethodGet, "http://localhost") + assert.NoError(err) + + req.Raw().Header["Range"] = []string{"bytes=0-99"} // 100 bytes + + // Consume burst (10 requests of 100 bytes = 1000 bytes) + for i := 0; i < 10; i++ { + _, err := pipeline.Do(req) + assert.NoError(err) + } + + // Next request of 100 bytes should be delayed by ~1 sec + start := time.Now() + _, err = pipeline.Do(req) + assert.NoError(err) + duration := time.Since(start) + + assert.GreaterOrEqual(duration, 900*time.Millisecond, "Expected delay of ~1s, got %v", duration) +} + +func (s *policiesTestSuite) TestRateLimitingPolicy_NoLimit() { + assert := assert.New(s.T()) + + p := newRateLimitingPolicy(-1, -1) + pipeline := runtime.NewPipeline("test", "v1", runtime.PipelineOptions{ + PerRetry: []policy.Policy{p}, + }, &policy.ClientOptions{Transport: &mockTransport{}}) + + req, _ := runtime.NewRequest(context.Background(), http.MethodGet, "http://localhost") + req.Raw().Header["Range"] = []string{"bytes=0-99"} + + start := time.Now() + for i := 0; i < 20; i++ { + _, err := pipeline.Do(req) + assert.NoError(err) + } + duration := time.Since(start) + + // Should be very fast + assert.Less(duration, 100*time.Millisecond, "Expected fast execution, got %v", duration) +} + +func (s *policiesTestSuite) TestRateLimitingPolicy_BandwidthLimit_XMsRange() { + assert := assert.New(s.T()) + + // Limit 100 bytes/sec. Burst 1000 bytes. + p := newRateLimitingPolicy(100, -1) + pipeline := runtime.NewPipeline("test", "v1", runtime.PipelineOptions{ + PerRetry: []policy.Policy{p}, + }, &policy.ClientOptions{Transport: &mockTransport{}}) + + req, _ := runtime.NewRequest(context.Background(), http.MethodGet, "http://localhost") + req.Raw().Header["x-ms-range"] = []string{"bytes=0-99"} // 100 bytes + + // Consume burst + for i := 0; i < 10; i++ { + _, err := pipeline.Do(req) + assert.NoError(err) + } + + // Next request should be delayed + start := time.Now() + _, err := pipeline.Do(req) + assert.NoError(err) + duration := time.Since(start) + + assert.GreaterOrEqual(duration, 900*time.Millisecond, "Expected delay of ~1s, got %v", duration) +} + +func (s *policiesTestSuite) TestRateLimitingPolicy_BandwidthLimit_SkipNonGet() { + assert := assert.New(s.T()) + + // Limit 100 bytes/sec. burst 1000. + p := newRateLimitingPolicy(100, -1) + pipeline := runtime.NewPipeline("test", "v1", runtime.PipelineOptions{ + PerRetry: []policy.Policy{p}, + }, &policy.ClientOptions{Transport: &mockTransport{}}) + + // Create requests for checkable methods + methods := []string{http.MethodPut, http.MethodPost, http.MethodDelete, http.MethodHead} + + for _, method := range methods { + req, _ := runtime.NewRequest(context.Background(), method, "http://localhost") + // Even if we have a range header that implies a large payload + // the policy should ignore it because it's not GET. + req.Raw().Header["Range"] = []string{"bytes=0-999"} // 1000 bytes + + start := time.Now() + // Execute multiple times - if limited, this would take ~10 seconds (1000 bytes * 10 / 100 bytes/sec) + // But since we are skipping non-GET, it should be instant. + for i := 0; i < 11; i++ { + _, err := pipeline.Do(req) + assert.NoError(err) + } + duration := time.Since(start) + + // Each request should be effectively instant, so total should be very fast + assert.Less(duration, 100*time.Millisecond, "Expected fast execution for method %s, got %v", method, duration) + } +} + +func TestPoliciesSuite(t *testing.T) { + suite.Run(t, new(policiesTestSuite)) +} diff --git a/component/azstorage/utils.go b/component/azstorage/utils.go index bea5669550..31479cdccd 100644 --- a/component/azstorage/utils.go +++ b/component/azstorage/utils.go @@ -43,6 +43,7 @@ import ( "net/url" "os" "path/filepath" + "strconv" "strings" "time" @@ -80,6 +81,9 @@ const ( DisableKeepAlives bool = false DisableCompression bool = false MaxResponseHeaderBytes int64 = 0 + + X_Ms_Range string = "x-ms-range" + RangeHeader string = "Range" ) // getAzStorageClientOptions : Create client options based on the config @@ -113,11 +117,19 @@ func getAzStorageClientOptions(conf *AzStorageConfig) (azcore.ClientOptions, err perCallPolicies = append(perCallPolicies, newServiceVersionPolicy(serviceApiVersion)) } + perRetryPolicies := []policy.Policy{} + if conf.capMbpsRead > 0 || conf.capIOps > 0 { + // Convert Mbps to Bytes/sec: 1 Mbps = (1024* 1024) / 8 = 131072 Bytes/sec + bytesPerSec := conf.capMbpsRead * 131072 + perRetryPolicies = append(perRetryPolicies, newRateLimitingPolicy(bytesPerSec, conf.capIOps)) + } + return azcore.ClientOptions{ - Retry: retryOptions, - Logging: logOptions, - PerCallPolicies: perCallPolicies, - Transport: transportOptions, + Retry: retryOptions, + Logging: logOptions, + PerCallPolicies: perCallPolicies, + PerRetryPolicies: perRetryPolicies, + Transport: transportOptions, }, err } @@ -583,6 +595,51 @@ func sanitizeEtag(ETag *azcore.ETag) string { return "" } +// parseRangeHeader parses the x-ms-range header and returns the size of the range requested. +// Examples of x-ms-range header: +// +// bytes=0-499 --> returns 500 +// bytes=500-999 --> returns 500 +// bytes=500- --> returns error (open ended range) +// bytes=-500 --> returns error +// bytes=1000-500 --> returns error (invalid range) +func parseRangeHeader(rangeHeader string) (int64, error) { + if rangeHeader == "" { + return 0, fmt.Errorf("empty x-ms-range header") + } + + if !strings.HasPrefix(rangeHeader, "bytes=") { + return 0, fmt.Errorf("invalid x-ms-range header format %s", rangeHeader) + } + + parts := strings.Split(strings.TrimPrefix(rangeHeader, "bytes="), "-") + if len(parts) != 2 { + return 0, fmt.Errorf("invalid x-ms-range header format %s", rangeHeader) + } + + start, err := strconv.ParseInt(parts[0], 10, 64) + if err != nil { + return 0, err + } + + // Open ended range + if parts[1] == "" { + return 0, fmt.Errorf("invalid x-ms-range header format %s, open ended range not supported", rangeHeader) + } + + end, err := strconv.ParseInt(parts[1], 10, 64) + if err != nil { + return 0, err + } + + // Invalid range + if end < start { + return 0, fmt.Errorf("invalid range %s", rangeHeader) + } + + return end - start + 1, nil +} + // func parseBlobTags(tags *container.BlobTags) map[string]string { // if tags == nil { diff --git a/component/azstorage/utils_test.go b/component/azstorage/utils_test.go index 54b4ac3b1b..9123b71b0d 100644 --- a/component/azstorage/utils_test.go +++ b/component/azstorage/utils_test.go @@ -512,6 +512,36 @@ func (s *utilsTestSuite) TestRemovePrefixPath() { } } +func (s *utilsTestSuite) TestParseRangeHeader() { + assert := assert.New(s.T()) + + tests := []struct { + header string + expected int64 + hasError bool + }{ + {"bytes=0-100", 101, false}, + {"bytes=100-200", 101, false}, + {"bytes=0-0", 1, false}, + {"bytes=0-", 0, true}, // open ended range not supported + {"", 0, true}, + {"invalid", 0, true}, + {"bytes=abc-def", 0, true}, + {"bytes=100-50", 0, true}, // invalid range + } + + for _, test := range tests { + size, err := parseRangeHeader(test.header) + + if test.hasError { + assert.Error(err) + } else { + assert.NoError(err) + assert.Equal(test.expected, size) + } + } +} + func TestUtilsTestSuite(t *testing.T) { suite.Run(t, new(utilsTestSuite)) } diff --git a/go.mod b/go.mod index 5549c94bed..a9a369e33a 100644 --- a/go.mod +++ b/go.mod @@ -24,6 +24,7 @@ require ( github.com/vibhansa-msft/blobfilter v0.0.0-20250115104552-d9d40722be3e github.com/vibhansa-msft/tlru v0.0.0-20240410102558-9e708419e21f go.uber.org/atomic v1.11.0 + golang.org/x/time v0.14.0 gopkg.in/ini.v1 v1.67.1 gopkg.in/yaml.v2 v2.4.0 gopkg.in/yaml.v3 v3.0.1 diff --git a/go.sum b/go.sum index bedd4f2f57..6ee464c19a 100644 --- a/go.sum +++ b/go.sum @@ -130,6 +130,8 @@ golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.33.0 h1:B3njUFyqtHDUI5jMn1YIr5B0IE2U0qck04r6d4KPAxE= golang.org/x/text v0.33.0/go.mod h1:LuMebE6+rBincTi9+xWTY8TztLzKHc/9C1uBCG27+q8= +golang.org/x/time v0.14.0 h1:MRx4UaLrDotUKUdCIqzPC48t1Y9hANFKIRpNx+Te8PI= +golang.org/x/time v0.14.0/go.mod h1:eL/Oa2bBBK0TkX57Fyni+NgnyQQN4LitPmob2Hjnqw4= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.1.1/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk= diff --git a/setup/advancedConfig.yaml b/setup/advancedConfig.yaml index 3380639ef8..efe1342b54 100644 --- a/setup/advancedConfig.yaml +++ b/setup/advancedConfig.yaml @@ -164,6 +164,8 @@ azstorage: cpk-encryption-key: cpk-encryption-key-sha256: preserve-acl: true|false + cap-mbps-read: + cap-iops: # Mount all configuration mountall: