From 2eb77f5257d4bb83a488c692d6bfffc348fbd3b7 Mon Sep 17 00:00:00 2001 From: Joshua MacDonald Date: Wed, 30 Apr 2025 13:20:36 -0700 Subject: [PATCH 01/47] lint --- extension/extensionlimiter/Makefile | 1 + extension/extensionlimiter/README.md | 137 ++++++++++++ .../extensionlimiter/extensionlimiter.go | 103 +++++++++ extension/extensionlimiter/go.mod | 66 ++++++ extension/extensionlimiter/go.sum | 97 +++++++++ .../limiterhelper/consumer.go | 197 ++++++++++++++++++ .../limiterhelper/middleware.go | 140 +++++++++++++ extension/extensionlimiter/rate.go | 100 +++++++++ extension/extensionlimiter/resource.go | 125 +++++++++++ extension/extensionlimiter/weight.go | 71 +++++++ receiver/otlpreceiver/go.mod | 20 +- receiver/otlpreceiver/otlp.go | 61 +++++- 12 files changed, 1102 insertions(+), 16 deletions(-) create mode 100644 extension/extensionlimiter/Makefile create mode 100644 extension/extensionlimiter/README.md create mode 100644 extension/extensionlimiter/extensionlimiter.go create mode 100644 extension/extensionlimiter/go.mod create mode 100644 extension/extensionlimiter/go.sum create mode 100644 extension/extensionlimiter/limiterhelper/consumer.go create mode 100644 extension/extensionlimiter/limiterhelper/middleware.go create mode 100644 extension/extensionlimiter/rate.go create mode 100644 extension/extensionlimiter/resource.go create mode 100644 extension/extensionlimiter/weight.go diff --git a/extension/extensionlimiter/Makefile b/extension/extensionlimiter/Makefile new file mode 100644 index 000000000000..ded7a36092dc --- /dev/null +++ b/extension/extensionlimiter/Makefile @@ -0,0 +1 @@ +include ../../Makefile.Common diff --git a/extension/extensionlimiter/README.md b/extension/extensionlimiter/README.md new file mode 100644 index 000000000000..c64b85d76767 --- /dev/null +++ b/extension/extensionlimiter/README.md @@ -0,0 +1,137 @@ +# OpenTelemetry Collector Extension Limiter Package + +**Document status: development** + +The `extensionlimiter` package provides interfaces for rate limiting +and resource limiting in the OpenTelemetry Collector, enabling control +over data flow and resource usage through extensions which can be +configured through and middleware and/or directly by pipeline +components. + +## Overview + +This package defines two primary limiter types with their respective +interfaces: + +- **Rate Limiters**: Control time-based limits on quantities such as + bytes or items per second. +- **Resource Limiters**: Manage physical limits on quantities such as + concurrent requests or memory usage. + +Both limiter types are unified through the `LimiterWrapper` interface, +which simplifies consumer usage by providing a consistent `LimitCall` +interface. + +A limiter is **saturated** by definition when a limit is completely +overloaded, generally it means a limit request of any size would fail. + +Each each base limiter type and the wrapper type have corresponding +providers that give access to a limiter instance based on a weight +key. + +Weight keys describes the standard limiting dimensions. There are +currently four standard weight keys: network bytes, request count, +request items, and memory size. + +## Key Interfaces + +- `LimiterWrapper`: Provides a callback-based limiting interface that + works with both rate and resource limiters, has a `LimitCall` method. +- `RateLimiter`: Applies time-based limits, has a `Limit` method. +- `ResourceLimiter`: Manages physical resource limits, has + an `Acquire` method and corresponding `ReleaseFunc`. + +### Limiter helpers + +The `limiterhelper` subpackage provides: + +- Consumer wrappers apply limits to a collector pipeline (e.g., + `NewLimitedLogs` to combine a limiter using `consumer.NewLogs`) +- Multi-limiter combinators: `MultiLimiterWrapperProvider` builds a sequence of wrapped limiters. +- Middleware conversion utilities: Convert middleware configurations to `LimiterWrapperProvider`. + +## Recommendations + +For general use cases, prefer the `LimiterWrapper` interface with its +callback-based approach because it is agnostic to the difference between +rate and resource limiters. + +Use the direct `RateLimiter` or `ResourceLimiter` interfaces only in +special cases where control flow can't be easily scoped. + +Middleware configuration typically automates the configuration of +network bytes and request count weight keys relatively early in a +pipeline. Receivers are responsible for limiting request items and +memory size through one of the available helpers. + +Processors can apply limiters for specific reasons, for example to +apply limits in data-dependent ways. Exporters can apply limiters for +the same reasons, for example to apply limits in destination-dependent +ways. + +### Limiter blocking and failing + +Limiters implementations MAY block the request or fail immediately, +subject to internal logic. A limiter aims to avoid waste, which +requires balancing several factors. To fail a request that has already +been transmitted, received and parsed is sometimes more wasteful than +waiting for a little while; on the other hand waiting for a long time +risks wasting memory. In general, an overloaded limiter that is saturated SHOULD +fail requests immediately. + +Limiters implementations SHOULD consider the context deadline when +they block. If the deadline is likely to expire before the limit +becomes available, they should return a standard overload signal. + +### Limiter saturation + +All limiters feature a `MustDeny` method which is made available for +applications to test when a limit is fully saturated. This special +limit request is defined as the equivalent of passing a zero value to +the limiter. + +Limiters SHOULD treat a request for zero units of the limit as a +special case, used for indicating when non-zero limit requests are +likely to fail. This is not an exact requirement; implementations are +free to define their own saturation parameters. + +### Limit before or after use + +It is sometimes possible to request a limit before it is actually +used. As an example, consider a protocol using a compressed payload, +such that the receivers knows how much memory will be allocated before +the fact. In this case the receiver can request the limit before using +it, but this will not always be the case. Generally, prefer to limit +before use, but either way be consistent. + +When using the low-level interfaces directly, limits SHOULD be applied +before creating new concurrent work. + +### Examples + +Limiters applied through middleware are an implementation detail, +simply configure them using `configgrpc` or `confighttp`. For the +OTLP receiver (e.g., with two `ratelimiter` extensions): + +``` +extensions: + ratelimiter/limit_for_grpc: + # rate limiter settings for gRPC + ratelimiter/limit_for_grpc: + # rate limiter settings for HTTP + +receivers: + otlp: + protocols: + grpc: + middlewares: + - ratelimiter/limit_for_grpc + http: + middlewares: + - ratelimiter/limit_for_http +``` + +@@@ +a stream one +a pull-based one +a data-dependent one diff --git a/extension/extensionlimiter/extensionlimiter.go b/extension/extensionlimiter/extensionlimiter.go new file mode 100644 index 000000000000..3e2fa915513a --- /dev/null +++ b/extension/extensionlimiter/extensionlimiter.go @@ -0,0 +1,103 @@ +// Copyright The OpenTelemetry Authors +// SPDX-License-Identifier: Apache-2.0 + +package extensionlimiter // import "go.opentelemetry.io/collector/extension/extensionlimiter" + +import ( + "context" +) + +// LimiterWrapper is a general-purpose interface for limiter consumers +// to limit resources with use of a callback. This is the simplest +// form of rate limiting interface from a callers perspective. If the +// caller is a pipeline component, consider using a consumer-oriented +// limiterhelper (e.g., limiterhelper.NewLimitedLogs) to apply a list of +// +// Limiter implementions are meant to implement either the RateLimiter +// or ResourceLimiter interfaces. LimiterWrappers can be constructed +// from either of the underlying limiters and their corresponding +// providers. Usually configmiddleware or limiterhelper is responsible +// for constructing the correct wrapper from these two kinds of limiter; +// users will use this interface consistently. +type LimiterWrapper interface { + // Must deny is the logical equivalent of Acquire(0). If the + // Acquire would fail even for 0 units of a rate, the + // caller must deny the request. Implementations are + // encouraged to ensure that when MustDeny() is false, + // Acquire(0) is also false, however callers could use a + // faster code path to implement MustDeny() since it does not + // depend on the value. + MustDeny(context.Context) error + + // LimitCall applies the limiter and with the rate or resource + // granted makes a scoped call, returning success or an error + // from either the limiter or the enclosed callback. + LimitCall(context.Context, uint64, func(ctx context.Context) error) error +} + +// LimiterWrapperProvider provides access to LimiterWrappers, which is +// the appropriate interface for callers that can easily wrap a +// function call, because for wrapped calls there is no distinction +// between rate limiters and resource limiters. +type LimiterWrapperProvider interface { + LimiterWrapper(WeightKey) (LimiterWrapper, error) +} + +// LimiterWrapperFunc is a functional way to build LimiterWrappers. +type LimiterWrapperFunc func(context.Context, uint64, func(ctx context.Context) error) error + +var _ LimiterWrapper = LimiterWrapperFunc(nil) + +// MustDeny implements LimiterWrapper. +func (f LimiterWrapperFunc) MustDeny(ctx context.Context) error { + return f.LimitCall(ctx, 0, func(_ context.Context) error { + return nil + }) +} + +// LimitCall implements LimiterWrapper. +func (f LimiterWrapperFunc) LimitCall(ctx context.Context, value uint64, call func(ctx context.Context) error) error { + if f == nil { + return call(ctx) + } + return f(ctx, value, call) +} + +// PassThrough returns a LimiterWrapper that imposes no limit. +func PassThrough() LimiterWrapper { + return LimiterWrapperFunc(nil) +} + +// LimiterWrapperProviderFunc is a functional way to build LimiterWrappers. +type LimiterWrapperProviderFunc func(WeightKey) (LimiterWrapper, error) + +var _ LimiterWrapperProvider = LimiterWrapperProviderFunc(nil) + +// LimiterWrapper implements LimiterWrapperProvider. +func (f LimiterWrapperProviderFunc) LimiterWrapper(key WeightKey) (LimiterWrapper, error) { + return f(key) +} + +// NewResourceLimiterWrapperProvider constructs a +// LimiterWrapperProvider for a resource limiter extension. +func NewResourceLimiterWrapperProvider(rp ResourceLimiterProvider) LimiterWrapperProvider { + return LimiterWrapperProviderFunc(func(key WeightKey) (LimiterWrapper, error) { + lim, err := rp.ResourceLimiter(key) + if err == nil { + return nil, err + } + return NewResourceLimiterWrapper(lim), err + }) +} + +// NewRateLimiterWrapperProvider constructs a LimiterWrapperProvider +// for a rate limiter extension. +func NewRateLimiterWrapperProvider(rp RateLimiterProvider) LimiterWrapperProvider { + return LimiterWrapperProviderFunc(func(key WeightKey) (LimiterWrapper, error) { + lim, err := rp.RateLimiter(key) + if err == nil { + return nil, err + } + return NewRateLimiterWrapper(lim), err + }) +} diff --git a/extension/extensionlimiter/go.mod b/extension/extensionlimiter/go.mod new file mode 100644 index 000000000000..a9da7b728672 --- /dev/null +++ b/extension/extensionlimiter/go.mod @@ -0,0 +1,66 @@ +module go.opentelemetry.io/collector/extension/extensionlimiter + +go 1.23.0 + +require ( + go.opentelemetry.io/collector/component v1.31.0 + go.opentelemetry.io/collector/config/configmiddleware v0.0.0-00010101000000-000000000000 + go.opentelemetry.io/collector/consumer v1.31.0 + go.opentelemetry.io/collector/consumer/xconsumer v0.0.0-00010101000000-000000000000 + go.opentelemetry.io/collector/extension v1.31.0 + go.opentelemetry.io/collector/pdata v1.31.0 + go.opentelemetry.io/collector/pdata/pprofile v0.125.0 +) + +require ( + github.com/go-logr/logr v1.4.2 // indirect + github.com/go-logr/stdr v1.2.2 // indirect + github.com/gogo/protobuf v1.3.2 // indirect + github.com/google/uuid v1.6.0 // indirect + github.com/hashicorp/go-version v1.7.0 // indirect + github.com/json-iterator/go v1.1.12 // indirect + github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect + github.com/modern-go/reflect2 v1.0.2 // indirect + go.opentelemetry.io/auto/sdk v1.1.0 // indirect + go.opentelemetry.io/collector/extension/extensionmiddleware v1.30.0 // indirect + go.opentelemetry.io/collector/featuregate v1.31.0 // indirect + go.opentelemetry.io/collector/internal/telemetry v0.125.0 // indirect + go.opentelemetry.io/contrib/bridges/otelzap v0.10.0 // indirect + go.opentelemetry.io/otel v1.35.0 // indirect + go.opentelemetry.io/otel/log v0.11.0 // indirect + go.opentelemetry.io/otel/metric v1.35.0 // indirect + go.opentelemetry.io/otel/sdk v1.35.0 // indirect + go.opentelemetry.io/otel/trace v1.35.0 // indirect + go.uber.org/multierr v1.11.0 // indirect + go.uber.org/zap v1.27.0 // indirect + golang.org/x/net v0.39.0 // indirect + golang.org/x/sys v0.32.0 // indirect + golang.org/x/text v0.24.0 // indirect + google.golang.org/genproto/googleapis/rpc v0.0.0-20250218202821-56aae31c358a // indirect + google.golang.org/grpc v1.72.0 // indirect + google.golang.org/protobuf v1.36.6 // indirect +) + +replace go.opentelemetry.io/collector/consumer => ../../consumer + +replace go.opentelemetry.io/collector/featuregate => ../../featuregate + +replace go.opentelemetry.io/collector/component => ../../component + +replace go.opentelemetry.io/collector/consumer/xconsumer => ../../consumer/xconsumer + +replace go.opentelemetry.io/collector/pdata => ../../pdata + +replace go.opentelemetry.io/collector/pdata/pprofile => ../../pdata/pprofile + +replace go.opentelemetry.io/collector/config/configmiddleware => ../../config/configmiddleware + +replace go.opentelemetry.io/collector/extension/extensionmiddleware => ../../extension/extensionmiddleware + +replace go.opentelemetry.io/collector/pipeline => ../../pipeline + +replace go.opentelemetry.io/collector/extension/extensionmiddleware/extensionmiddlewaretest => ../extensionmiddleware/extensionmiddlewaretest + +replace go.opentelemetry.io/collector/extension => ../ + +replace go.opentelemetry.io/collector/internal/telemetry => ../../internal/telemetry diff --git a/extension/extensionlimiter/go.sum b/extension/extensionlimiter/go.sum new file mode 100644 index 000000000000..26de4444ce43 --- /dev/null +++ b/extension/extensionlimiter/go.sum @@ -0,0 +1,97 @@ +github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= +github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A= +github.com/go-logr/logr v1.4.2 h1:6pFjapn8bFcIbiKo3XT4j/BhANplGihG6tvd+8rYgrY= +github.com/go-logr/logr v1.4.2/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= +github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag= +github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE= +github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q= +github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q= +github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek= +github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps= +github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8= +github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU= +github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= +github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= +github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/hashicorp/go-version v1.7.0 h1:5tqGy27NaOTB8yJKUZELlFAS/LTKJkrmONwQKeRZfjY= +github.com/hashicorp/go-version v1.7.0/go.mod h1:fltr4n8CU8Ke44wwGCBoEymUuxUHl09ZGVZPK5anwXA= +github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM= +github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo= +github.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI2bnpBCr8= +github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= +github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= +github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg= +github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= +github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M= +github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk= +github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= +github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA= +github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= +github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= +github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= +go.opentelemetry.io/auto/sdk v1.1.0 h1:cH53jehLUN6UFLY71z+NDOiNJqDdPRaXzTel0sJySYA= +go.opentelemetry.io/auto/sdk v1.1.0/go.mod h1:3wSPjt5PWp2RhlCcmmOial7AvC4DQqZb7a7wCow3W8A= +go.opentelemetry.io/contrib/bridges/otelzap v0.10.0 h1:ojdSRDvjrnm30beHOmwsSvLpoRF40MlwNCA+Oo93kXU= +go.opentelemetry.io/contrib/bridges/otelzap v0.10.0/go.mod h1:oTTm4g7NEtHSV2i/0FeVdPaPgUIZPfQkFbq0vbzqnv0= +go.opentelemetry.io/otel v1.35.0 h1:xKWKPxrxB6OtMCbmMY021CqC45J+3Onta9MqjhnusiQ= +go.opentelemetry.io/otel v1.35.0/go.mod h1:UEqy8Zp11hpkUrL73gSlELM0DupHoiq72dR+Zqel/+Y= +go.opentelemetry.io/otel/log v0.11.0 h1:c24Hrlk5WJ8JWcwbQxdBqxZdOK7PcP/LFtOtwpDTe3Y= +go.opentelemetry.io/otel/log v0.11.0/go.mod h1:U/sxQ83FPmT29trrifhQg+Zj2lo1/IPN1PF6RTFqdwc= +go.opentelemetry.io/otel/metric v1.35.0 h1:0znxYu2SNyuMSQT4Y9WDWej0VpcsxkuklLa4/siN90M= +go.opentelemetry.io/otel/metric v1.35.0/go.mod h1:nKVFgxBZ2fReX6IlyW28MgZojkoAkJGaE8CpgeAU3oE= +go.opentelemetry.io/otel/sdk v1.35.0 h1:iPctf8iprVySXSKJffSS79eOjl9pvxV9ZqOWT0QejKY= +go.opentelemetry.io/otel/sdk v1.35.0/go.mod h1:+ga1bZliga3DxJ3CQGg3updiaAJoNECOgJREo9KHGQg= +go.opentelemetry.io/otel/sdk/metric v1.35.0 h1:1RriWBmCKgkeHEhM7a2uMjMUfP7MsOF5JpUCaEqEI9o= +go.opentelemetry.io/otel/sdk/metric v1.35.0/go.mod h1:is6XYCUMpcKi+ZsOvfluY5YstFnhW0BidkR+gL+qN+w= +go.opentelemetry.io/otel/trace v1.35.0 h1:dPpEfJu1sDIqruz7BHFG3c7528f6ddfSWfFDVt/xgMs= +go.opentelemetry.io/otel/trace v1.35.0/go.mod h1:WUk7DtFp1Aw2MkvqGdwiXYDZZNvA/1J8o6xRXLrIkyc= +go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto= +go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE= +go.uber.org/multierr v1.11.0 h1:blXXJkSxSSfBVBlC76pxqeO+LN3aDfLQo+309xJstO0= +go.uber.org/multierr v1.11.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y= +go.uber.org/zap v1.27.0 h1:aJMhYGrd5QSmlpLMr2MftRKl7t8J8PTZPA732ud/XR8= +go.uber.org/zap v1.27.0/go.mod h1:GB2qFLM7cTU87MWRP2mPIjqfIDnGu+VIO4V/SdhGo2E= +golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= +golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= +golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= +golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= +golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= +golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= +golang.org/x/net v0.39.0 h1:ZCu7HMWDxpXpaiKdhzIfaltL9Lp31x/3fCP11bc6/fY= +golang.org/x/net v0.39.0/go.mod h1:X7NRbYVEA+ewNkCNyJ513WmMdQ3BineSwVtN2zD/d+E= +golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.32.0 h1:s77OFDvIQeibCmezSnk/q6iAfkdiQaJi4VzroCFrN20= +golang.org/x/sys v0.32.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k= +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.24.0 h1:dd5Bzh4yt5KYA8f9CJHCP4FB4D51c2c6JvN37xJJkJ0= +golang.org/x/text v0.24.0/go.mod h1:L8rBsPeo2pSS+xqN0d5u2ikmjtmoJbDBT1b7nHvFCdU= +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.0.0-20200619180055-7c47624df98f/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= +golang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= +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= +golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +google.golang.org/genproto/googleapis/rpc v0.0.0-20250218202821-56aae31c358a h1:51aaUVRocpvUOSQKM6Q7VuoaktNIaMCLuhZB6DKksq4= +google.golang.org/genproto/googleapis/rpc v0.0.0-20250218202821-56aae31c358a/go.mod h1:uRxBH1mhmO8PGhU89cMcHaXKZqO+OfakD8QQO0oYwlQ= +google.golang.org/grpc v1.72.0 h1:S7UkcVa60b5AAQTaO6ZKamFp1zMZSU0fGDK2WZLbBnM= +google.golang.org/grpc v1.72.0/go.mod h1:wH5Aktxcg25y1I3w7H69nHfXdOG3UiadoBtjh3izSDM= +google.golang.org/protobuf v1.36.6 h1:z1NpPI8ku2WgiWnf+t9wTPsn6eP1L7ksHUlkfLvd9xY= +google.golang.org/protobuf v1.36.6/go.mod h1:jduwjTPXsFjZGTmRluh+L6NjiWu7pchiJ2/5YcXBHnY= +gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= +gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/extension/extensionlimiter/limiterhelper/consumer.go b/extension/extensionlimiter/limiterhelper/consumer.go new file mode 100644 index 000000000000..258d157d7737 --- /dev/null +++ b/extension/extensionlimiter/limiterhelper/consumer.go @@ -0,0 +1,197 @@ +// Copyright The OpenTelemetry Authors +// SPDX-License-Identifier: Apache-2.0 + +package limiterhelper // import "go.opentelemetry.io/collector/extension/extensionlimiter/limiterhelper" + +import ( + "context" + "errors" + "slices" + + "go.opentelemetry.io/collector/consumer" + "go.opentelemetry.io/collector/consumer/xconsumer" + "go.opentelemetry.io/collector/extension/extensionlimiter" + "go.opentelemetry.io/collector/pdata/plog" + "go.opentelemetry.io/collector/pdata/pmetric" + "go.opentelemetry.io/collector/pdata/pprofile" + "go.opentelemetry.io/collector/pdata/ptrace" +) + +// Traits object interface is generalized by P the pipeline data type +// (e.g., ptrace.Traces) and C the consumer type (e.g., +// consumer.Traces) +type traits[P, C any] interface { + // itemCount is SpanCount(), DataPointCount(), or LogRecordCount(). + itemCount(P) uint64 + // memorySize uses the appropriate protobuf Sizer as a proxy + // for memory used. + memorySize(data P) uint64 + // consume calls the appropriate consumer method (e.g., ConsumeTraces) + consume(ctx context.Context, data P, next C) error + // create is a functional constructor the consumer type (e.g., consumer.NewTraces) + create(func(ctx context.Context, data P) error, ...consumer.Option) (C, error) +} + +// Traces traits + +type traceTraits struct{} + +func (traceTraits) itemCount(data ptrace.Traces) uint64 { + return uint64(data.SpanCount()) +} + +func (traceTraits) memorySize(data ptrace.Traces) uint64 { + var sizer ptrace.MarshalSizer + return uint64(sizer.TracesSize(data)) +} + +func (traceTraits) create(next func(ctx context.Context, data ptrace.Traces) error, opts ...consumer.Option) (consumer.Traces, error) { + return consumer.NewTraces(next, opts...) +} + +func (traceTraits) consume(ctx context.Context, data ptrace.Traces, next consumer.Traces) error { + return next.ConsumeTraces(ctx, data) +} + +// Metrics traits + +type metricTraits struct{} + +func (metricTraits) itemCount(data pmetric.Metrics) uint64 { + return uint64(data.DataPointCount()) +} + +func (metricTraits) memorySize(data pmetric.Metrics) uint64 { + var sizer pmetric.MarshalSizer + return uint64(sizer.MetricsSize(data)) +} + +func (metricTraits) create(next func(ctx context.Context, data pmetric.Metrics) error, opts ...consumer.Option) (consumer.Metrics, error) { + return consumer.NewMetrics(next, opts...) +} + +func (metricTraits) consume(ctx context.Context, data pmetric.Metrics, next consumer.Metrics) error { + return next.ConsumeMetrics(ctx, data) +} + +// Logs traits + +type logTraits struct{} + +func (logTraits) itemCount(data plog.Logs) uint64 { + return uint64(data.LogRecordCount()) +} + +func (logTraits) memorySize(data plog.Logs) uint64 { + var sizer plog.MarshalSizer + return uint64(sizer.LogsSize(data)) +} + +func (logTraits) create(next func(ctx context.Context, data plog.Logs) error, opts ...consumer.Option) (consumer.Logs, error) { + return consumer.NewLogs(next, opts...) +} + +func (logTraits) consume(ctx context.Context, data plog.Logs, next consumer.Logs) error { + return next.ConsumeLogs(ctx, data) +} + +// Profiles traits + +type profileTraits struct{} + +func (profileTraits) itemCount(data pprofile.Profiles) uint64 { + return uint64(data.SampleCount()) +} + +func (profileTraits) memorySize(data pprofile.Profiles) uint64 { + var sizer pprofile.MarshalSizer + return uint64(sizer.ProfilesSize(data)) +} + +func (profileTraits) create(next func(ctx context.Context, data pprofile.Profiles) error, opts ...consumer.Option) (xconsumer.Profiles, error) { + return xconsumer.NewProfiles(next, opts...) +} + +func (profileTraits) consume(ctx context.Context, data pprofile.Profiles, next xconsumer.Profiles) error { + return next.ConsumeProfiles(ctx, data) +} + +// limitOne obtains a LimiterWrapper and applies a single weight limit. +func limitOne[P any, C any]( + next C, + keys []extensionlimiter.WeightKey, + provider extensionlimiter.LimiterWrapperProvider, + m traits[P, C], + key extensionlimiter.WeightKey, + opts []consumer.Option, + quantify func(P) uint64, +) (C, error) { + if !slices.Contains(keys, key) { + return next, nil + } + lim, err := provider.LimiterWrapper(key) + if err != nil { + return next, err + } + if lim == nil { + return next, nil + } + return m.create(func(ctx context.Context, data P) error { + return lim.LimitCall(ctx, quantify(data), func(ctx context.Context) error { + return m.consume(ctx, data, next) + }) + }, opts...) +} + +// newLimited is signal-generic limiting logic. +func newLimited[P any, C any]( + next C, + keys []extensionlimiter.WeightKey, + provider extensionlimiter.LimiterWrapperProvider, + m traits[P, C], + opts ...consumer.Option, +) (C, error) { + if provider == nil { + return next, nil + } + var err1, err2, err3 error + // Note: reverse order of evaluation cost => least-cost applied first. + next, err1 = limitOne(next, keys, provider, m, extensionlimiter.WeightKeyMemorySize, opts, + + func(data P) uint64 { + return m.memorySize(data) + }) + next, err2 = limitOne(next, keys, provider, m, extensionlimiter.WeightKeyRequestItems, opts, + func(data P) uint64 { + return m.itemCount(data) + }) + next, err3 = limitOne(next, keys, provider, m, extensionlimiter.WeightKeyRequestCount, opts, + func(_ P) uint64 { + return 1 + }) + return next, errors.Join(err1, err2, err3) +} + +// NewLimitedTraces applies a limiter using the provider over keys before calling next. +func NewLimitedTraces(next consumer.Traces, keys []extensionlimiter.WeightKey, provider extensionlimiter.LimiterWrapperProvider) (consumer.Traces, error) { + return newLimited(next, keys, provider, traceTraits{}, + consumer.WithCapabilities(next.Capabilities())) +} + +// NewLimitedLogs applies a limiter using the provider over keys before calling next. +func NewLimitedLogs(next consumer.Logs, keys []extensionlimiter.WeightKey, provider extensionlimiter.LimiterWrapperProvider) (consumer.Logs, error) { + return newLimited(next, keys, provider, logTraits{}, + consumer.WithCapabilities(next.Capabilities())) +} + +// NewLimitedMetrics applies a limiter using the provider over keys before calling next. +func NewLimitedMetrics(next consumer.Metrics, keys []extensionlimiter.WeightKey, provider extensionlimiter.LimiterWrapperProvider) (consumer.Metrics, error) { + return newLimited(next, keys, provider, metricTraits{}, + consumer.WithCapabilities(next.Capabilities())) +} + +// NewLimitedProfiles applies a limiter using the provider over keys before calling next. +func NewLimitedProfiles(next xconsumer.Profiles, keys []extensionlimiter.WeightKey, provider extensionlimiter.LimiterWrapperProvider) (xconsumer.Profiles, error) { + return newLimited(next, keys, provider, profileTraits{}, + consumer.WithCapabilities(next.Capabilities())) +} diff --git a/extension/extensionlimiter/limiterhelper/middleware.go b/extension/extensionlimiter/limiterhelper/middleware.go new file mode 100644 index 000000000000..91735349f63f --- /dev/null +++ b/extension/extensionlimiter/limiterhelper/middleware.go @@ -0,0 +1,140 @@ +// Copyright The OpenTelemetry Authors +// SPDX-License-Identifier: Apache-2.0 + +package limiterhelper // import "go.opentelemetry.io/collector/extension/extensionlimiter/limiterhelper" + +import ( + "context" + "errors" + "fmt" + + "go.opentelemetry.io/collector/component" + "go.opentelemetry.io/collector/config/configmiddleware" + "go.opentelemetry.io/collector/extension" + "go.opentelemetry.io/collector/extension/extensionlimiter" +) + +var ( + ErrNotALimiter = errors.New("middleware is not a limiter") + ErrLimiterConflict = errors.New("limiter implements both rate and resource-limiters") + ErrUnresolvedLimiter = errors.New("could not resolve middleware limiter") +) + +// MiddlewareIsLimiter returns true if a middleware configuration +// represents a valid limiter, returns false for not found or invalid +// cases. If the named extension is found but is not a limiter, +// returns (false, nil). +func MiddlewareIsLimiter(host component.Host, middleware configmiddleware.Config) (bool, error) { + _, ok, err := middlewareIsLimiter(host, middleware) + return ok, err +} + +// MiddlewaresToLimiterWrapperProvider constructs a combined limiter +// from an ordered list of middlewares. This constructor ignores +// middleware configs that are not limiters. +// +// When no limiters are found (with no errors), the returned provider +// is nil. When a nil is passed to the consumer helpers (e.g., +// NewLimitedLogs) it will pass-through when the limiter is nil. +func MiddlewaresToLimiterWrapperProvider(host component.Host, middleware []configmiddleware.Config) (extensionlimiter.LimiterWrapperProvider, error) { + var retErr error + var providers []extensionlimiter.LimiterWrapperProvider + for _, mid := range middleware { + ok, err := MiddlewareIsLimiter(host, mid) + retErr = errors.Join(retErr, err) + if !ok { + continue + } + provider, err := MiddlewareToLimiterWrapperProvider(host, mid) + providers = append(providers, provider) + retErr = errors.Join(retErr, err) + } + if len(providers) == 0 { + return nil, nil + } + return MultiLimiterWrapperProvider(providers), nil +} + +// MiddlewareToLimiterWrapperProvider returns a limiter wrapper +// provider from middleware. Returns a package-level error if the +// middleware does not implement exactly one of the limiter +// interfaces (i.e., rate or resource). +func MiddlewareToLimiterWrapperProvider(host component.Host, middleware configmiddleware.Config) (extensionlimiter.LimiterWrapperProvider, error) { + ext, ok, err := middlewareIsLimiter(host, middleware) + if err != nil { + return nil, err + } + if ok { + if lim, ok := ext.(extensionlimiter.ResourceLimiterProvider); ok { + return extensionlimiter.NewResourceLimiterWrapperProvider(lim), nil + } + if lim, ok := ext.(extensionlimiter.RateLimiterProvider); ok { + return extensionlimiter.NewRateLimiterWrapperProvider(lim), nil + } + } + return nil, fmt.Errorf("%w: %s", ErrNotALimiter, ext) +} + +// middlewareIsLimiter applies consistency checks and returns a valid +// limiter extensions. +func middlewareIsLimiter(host component.Host, middleware configmiddleware.Config) (extension.Extension, bool, error) { + exts := host.GetExtensions() + ext := exts[middleware.ID] + if ext == nil { + return nil, false, fmt.Errorf("%w: %s", ErrUnresolvedLimiter, ext) + } + _, isResource := ext.(extensionlimiter.ResourceLimiterProvider) + _, isRate := ext.(extensionlimiter.RateLimiterProvider) + + switch { + case isResource && isRate: + return nil, false, fmt.Errorf("%w: %s", ErrLimiterConflict, ext) + case isResource, isRate: + return ext, true, nil + default: + return nil, false, nil + } +} + +// MultiLimiterWrapperProvider combines multiple limiter wrappers +// providers into a single provider by sequencing wrapped limiters. +// Returns errors from the underlying LimiterWrapper() calls, if any. +type MultiLimiterWrapperProvider []extensionlimiter.LimiterWrapperProvider + +var _ extensionlimiter.LimiterWrapperProvider = MultiLimiterWrapperProvider{} + +// LimiterWrapper implements LimiterWrapperProvider. +func (ps MultiLimiterWrapperProvider) LimiterWrapper(key extensionlimiter.WeightKey) (extensionlimiter.LimiterWrapper, error) { + if len(ps) == 0 { + return extensionlimiter.PassThrough(), nil + } + + // Map provider list to limiter list. + var lims []extensionlimiter.LimiterWrapper + + for _, provider := range ps { + lim, err := provider.LimiterWrapper(key) + if err == nil { + return nil, err + } + lims = append(lims, lim) + } + + // Compose limiters in sequence. + return sequenceLimiters(lims), nil +} + +func sequenceLimiters(lims []extensionlimiter.LimiterWrapper) extensionlimiter.LimiterWrapper { + if len(lims) == 1 { + return lims[0] + } + return composeLimiters(lims[0], sequenceLimiters(lims[1:])) +} + +func composeLimiters(first, second extensionlimiter.LimiterWrapper) extensionlimiter.LimiterWrapper { + return extensionlimiter.LimiterWrapperFunc(func(ctx context.Context, value uint64, call func(ctx context.Context) error) error { + return first.LimitCall(ctx, value, func(ctx context.Context) error { + return second.LimitCall(ctx, value, call) + }) + }) +} diff --git a/extension/extensionlimiter/rate.go b/extension/extensionlimiter/rate.go new file mode 100644 index 000000000000..70bc9986e353 --- /dev/null +++ b/extension/extensionlimiter/rate.go @@ -0,0 +1,100 @@ +// Copyright The OpenTelemetry Authors +// SPDX-License-Identifier: Apache-2.0 + +package extensionlimiter // import "go.opentelemetry.io/collector/extension/extensionlimiter" + +import ( + "context" +) + +// RateLimiterProvider is a provider for rate limiters. +// +// Limiter implementations will implement this or the +// ResourceLimiterProvider interface, but MUST not implement both. +// Limiters are covered by configmiddleware configuration, which is +// able to construct LimiterWrappers from these providers. +type RateLimiterProvider interface { + RateLimiter(WeightKey) (RateLimiter, error) +} + +// RateLimiterProviderFunc is a functional way to build RateLimters. +type RateLimiterProviderFunc func(WeightKey) (RateLimiter, error) + +var _ RateLimiterProvider = RateLimiterProviderFunc(nil) + +// RateLimiter implements RateLimiterProvider. +func (f RateLimiterProviderFunc) RateLimiter(key WeightKey) (RateLimiter, error) { + return f(key) +} + +// RateLimiter is an interface that an implementation makes available +// to apply time-based limits on quantities such as the number of +// bytes or items per second. +// +// This is a relatively low-level interface. Callers that can use a +// LimiterWrapper should choose that interface instead. This interface +// is meant for direct use only in special cases where control flow +// cannot be easily scoped to a callback, for example inside +// middleware (e.g., grpc.StatsHandler). +// +// See the README for more recommendations. +type RateLimiter interface { + // Must deny is the logical equivalent of Acquire(0). If the + // Acquire would fail even for 0 units of a rate, the + // caller must deny the request. Implementations are + // encouraged to ensure that when MustDeny() is false, + // Acquire(0) is also false, however callers could use a + // faster code path to implement MustDeny() since it does not + // depend on the value. + MustDeny(context.Context) error + + // Limit attempts to apply rate limiting with the provided + // weight, based on the key that was given to the provider. + // + // This is expected to block the caller until the weight can + // be admitted, or when the limit is completely saturated, + // limiters may also return immediate errors. + Limit(ctx context.Context, value uint64) error +} + +// RateLimiterFunc is an easy way to construct RateLimiters. +type RateLimiterFunc func(ctx context.Context, value uint64) error + +var _ RateLimiter = RateLimiterFunc(nil) + +// MustDeny implements RateLimiter. +func (f RateLimiterFunc) MustDeny(ctx context.Context) error { + return f.Limit(ctx, 0) +} + +// Limit implements RateLimiter. +func (f RateLimiterFunc) Limit(ctx context.Context, value uint64) error { + if f == nil { + return nil + } + return f(ctx, value) +} + +// NewRateLimiterWrapper returns a LimiterWrapper from a RateLimiter. +func NewRateLimiterWrapper(limiter RateLimiter) LimiterWrapper { + return rateLimiterWrapper{limiter: limiter} +} + +type rateLimiterWrapper struct { + limiter RateLimiter +} + +var _ LimiterWrapper = rateLimiterWrapper{} + +// MustDeny implements LimiterWrapper. +func (w rateLimiterWrapper) MustDeny(ctx context.Context) error { + return w.limiter.MustDeny(ctx) +} + +// LimitCall implements LimiterWrapper. +func (w rateLimiterWrapper) LimitCall(ctx context.Context, value uint64, call func(context.Context) error) error { + if err := w.limiter.Limit(ctx, value); err != nil { + return err + } + return call(ctx) +} diff --git a/extension/extensionlimiter/resource.go b/extension/extensionlimiter/resource.go new file mode 100644 index 000000000000..704a39e7d5b3 --- /dev/null +++ b/extension/extensionlimiter/resource.go @@ -0,0 +1,125 @@ +// Copyright The OpenTelemetry Authors +// SPDX-License-Identifier: Apache-2.0 + +package extensionlimiter // import "go.opentelemetry.io/collector/extension/extensionlimiter" + +import ( + "context" +) + +// ResourceLimiterProvider is a provider for resource limiters. +// +// Limiter implementations will implement this or the +// RateLimiterProvider interface, but MUST not implement both. +// Limiters are covered by configmiddleware configuration, which +// is able to construct LimiterWrappers from these providers. +type ResourceLimiterProvider interface { + ResourceLimiter(WeightKey) (ResourceLimiter, error) +} + +// ResourceLimiterProviderFunc is a functional way to build ResourceLimters. +type ResourceLimiterProviderFunc func(WeightKey) (ResourceLimiter, error) + +var _ ResourceLimiterProvider = ResourceLimiterProviderFunc(nil) + +// ResourceLimiter implements ResourceLimiterProvider. +func (f ResourceLimiterProviderFunc) ResourceLimiter(key WeightKey) (ResourceLimiter, error) { + return f(key) +} + +// ResourceLimiter is an interface that an implementation makes +// available to apply physical limits on quantities such as the number +// of concurrent requests or amount of memory in use. +// +// This is a relatively low-level interface. Callers that can use a +// LimiterWrapper should choose that interface instead. This +// interface is meant for direct use only in special cases where +// control flow is not scoped to a callback, for example in a +// streaming receiver where a limiter might be Acquired in the body of +// Send() and released prior to a corresponding Recv() (e.g., +// OTel-Arrow receiver). +// +// See the README for more recommendations. +type ResourceLimiter interface { + // Must deny is the logical equivalent of Acquire(0). If the + // Acquire would fail even for 0 units of a resource, the + // caller must deny the request. Implementations are + // encouraged to ensure that when MustDeny() is false, + // Acquire(0) is also false, however callers could use a + // faster code path to implement MustDeny() since it does not + // depend on the value. + MustDeny(context.Context) error + + // Acquire attempts to acquire a quantified resource with the + // provided weight, based on the key that was given to the + // provider. The caller has these options: + // + // - Accept and let the request proceed by returning a release func and a nil error + // - Fail and return a non-nil error and a nil release func + // - Block until the resource becomes available, then accept + // - Block until the context times out, return the error. + // + // See the README for more recommendations. + // + // On success, it returns a ReleaseFunc that should be called + // when the resources are no longer needed. + // + // Implementations are not required to call a release func + // when Acquire(0) is called, because there is nothing to + // release. Acquire(0) the equivalent of MustDeny(). + Acquire(ctx context.Context, value uint64) (ReleaseFunc, error) +} + +// ReleaseFunc is called when resources should be released after limiting. +// +// RelaseFunc values are never nil values, even in the error case, for +// safety. Users should unconditionally defer these. +type ReleaseFunc func() + +// ResourceLimiterFunc is a functional way to construct ResourceLimiters. +type ResourceLimiterFunc func(ctx context.Context, value uint64) (ReleaseFunc, error) + +var _ ResourceLimiter = ResourceLimiterFunc(nil) + +// MustDeny implements ResourceLimiter +func (f ResourceLimiterFunc) MustDeny(ctx context.Context) error { + _, err := f.Acquire(ctx, 0) + return err +} + +// Acquire implements ResourceLimiter +func (f ResourceLimiterFunc) Acquire(ctx context.Context, value uint64) (ReleaseFunc, error) { + if f == nil { + return func() {}, nil + } + return f(ctx, value) +} + +// NewResourceLimiterWrapper returns a LimiterWrapper from a ResourceLimiter. +func NewResourceLimiterWrapper(limiter ResourceLimiter) LimiterWrapper { + return resourceLimiterWrapper{limiter: limiter} +} + +type resourceLimiterWrapper struct { + limiter ResourceLimiter +} + +var _ LimiterWrapper = resourceLimiterWrapper{} + +// MustDeny implements LimiterWrapper. +func (w resourceLimiterWrapper) MustDeny(ctx context.Context) error { + if w.limiter == nil { + return nil + } + return w.limiter.MustDeny(ctx) +} + +// LimitCall implements LimiterWrapper. +func (w resourceLimiterWrapper) LimitCall(ctx context.Context, value uint64, call func(context.Context) error) error { + release, err := w.limiter.Acquire(ctx, value) + if err != nil { + return err + } + defer release() + return call(ctx) +} diff --git a/extension/extensionlimiter/weight.go b/extension/extensionlimiter/weight.go new file mode 100644 index 000000000000..16d5c8c4ec9d --- /dev/null +++ b/extension/extensionlimiter/weight.go @@ -0,0 +1,71 @@ +// Copyright The OpenTelemetry Authors +// SPDX-License-Identifier: Apache-2.0 + +package extensionlimiter // import "go.opentelemetry.io/collector/extension/extensionlimiter" + +// WeightKey is an enum type for common rate limits. The +// StandardAllKeys, StandardMiddlewareKeys, and +// StandardNotMiddlewareKeys methods return the list of middleware +// keys that can be automatically configured through middleware and +// not. +type WeightKey string + +// Predefined weight keys for common rate limits. This is not meant +// to be a closed set, new weight keys may be added in the future, +// possibly to restrict other kinds of event (e.g., auths, retries). +// +// Providers should return errors when they do not recognize a weight +// key. +const ( + // WeightKeyNetworkBytes is for network bytes. This is + // typically used with rate limiters. + WeightKeyNetworkBytes WeightKey = "network_bytes" + + // WeightKeyRequestCount can be used to limit the rate or + // total concurrent number of requests (i.e., pipeline data + // objects). This is typically used with both rate and + // resource limiters. + WeightKeyRequestCount WeightKey = "request_count" + + // WeightKeyRequestItems can be used to limit the rate or + // total concurrent number of items (log records, metric data + // points, spans, profiles). This is typically used with both + // rate and resource limiters. + WeightKeyRequestItems WeightKey = "request_items" + + // WeightKeyMemorySize is typically used with ResourceLimiters + // for limiting active memory usage. + WeightKeyMemorySize WeightKey = "memory_size" +) + +// StandardAllKeys is all the keys that can be automatically +// implemented by middleware and/or limiterhelper. +func StandardAllKeys() []WeightKey { + return []WeightKey{ + WeightKeyNetworkBytes, + WeightKeyRequestCount, + WeightKeyRequestItems, + WeightKeyMemorySize, + } +} + +// StandardMiddlewareKeys are typically handled in middleware for +// protocols that support it. Receivers should be careful not to +// re-apply these limits, especially not to twice-limit by +// WeightKeyRequestItems. +func StandardMiddlewareKeys() []WeightKey { + return []WeightKey{ + WeightKeyNetworkBytes, + WeightKeyRequestCount, + } +} + +// StandardNotMiddlewareKeys are the keys that are typically not +// handled through middlware because they are protocol specific and +// generally easier to handle after the input has become pdata. +func StandardNotMiddlewareKeys() []WeightKey { + return []WeightKey{ + WeightKeyRequestItems, + WeightKeyMemorySize, + } +} diff --git a/receiver/otlpreceiver/go.mod b/receiver/otlpreceiver/go.mod index 8ce013f88cdb..ec076100e16f 100644 --- a/receiver/otlpreceiver/go.mod +++ b/receiver/otlpreceiver/go.mod @@ -6,30 +6,31 @@ require ( github.com/gogo/protobuf v1.3.2 github.com/klauspost/compress v1.18.0 github.com/stretchr/testify v1.10.0 - go.opentelemetry.io/collector v0.125.0 + go.opentelemetry.io/collector v0.124.0 go.opentelemetry.io/collector/component v1.31.0 go.opentelemetry.io/collector/component/componentstatus v0.125.0 go.opentelemetry.io/collector/component/componenttest v0.125.0 go.opentelemetry.io/collector/config/configauth v0.125.0 - go.opentelemetry.io/collector/config/configgrpc v0.125.0 - go.opentelemetry.io/collector/config/confighttp v0.125.0 + go.opentelemetry.io/collector/config/configgrpc v0.124.0 + go.opentelemetry.io/collector/config/confighttp v0.124.0 go.opentelemetry.io/collector/config/confignet v1.31.0 go.opentelemetry.io/collector/config/configopaque v1.31.0 go.opentelemetry.io/collector/config/configtls v1.31.0 go.opentelemetry.io/collector/confmap v1.31.0 - go.opentelemetry.io/collector/confmap/xconfmap v0.125.0 + go.opentelemetry.io/collector/confmap/xconfmap v0.124.0 go.opentelemetry.io/collector/consumer v1.31.0 go.opentelemetry.io/collector/consumer/consumererror v0.125.0 go.opentelemetry.io/collector/consumer/consumertest v0.125.0 go.opentelemetry.io/collector/consumer/xconsumer v0.125.0 - go.opentelemetry.io/collector/internal/sharedcomponent v0.125.0 + go.opentelemetry.io/collector/extension/extensionlimiter v0.0.0-00010101000000-000000000000 + go.opentelemetry.io/collector/internal/sharedcomponent v0.124.0 go.opentelemetry.io/collector/internal/telemetry v0.125.0 go.opentelemetry.io/collector/pdata v1.31.0 go.opentelemetry.io/collector/pdata/pprofile v0.125.0 go.opentelemetry.io/collector/pdata/testdata v0.125.0 go.opentelemetry.io/collector/receiver v1.31.0 - go.opentelemetry.io/collector/receiver/receiverhelper v0.125.0 - go.opentelemetry.io/collector/receiver/receivertest v0.125.0 + go.opentelemetry.io/collector/receiver/receiverhelper v0.124.0 + go.opentelemetry.io/collector/receiver/receivertest v0.124.0 go.opentelemetry.io/collector/receiver/xreceiver v0.125.0 go.opentelemetry.io/otel v1.35.0 go.opentelemetry.io/otel/sdk/metric v1.35.0 @@ -68,8 +69,9 @@ require ( go.opentelemetry.io/collector/client v1.31.0 // indirect go.opentelemetry.io/collector/config/configcompression v1.31.0 // indirect go.opentelemetry.io/collector/config/configmiddleware v0.125.0 // indirect + go.opentelemetry.io/collector/extension v1.31.0 // indirect go.opentelemetry.io/collector/extension/extensionauth v1.31.0 // indirect - go.opentelemetry.io/collector/extension/extensionmiddleware v0.125.0 // indirect + go.opentelemetry.io/collector/extension/extensionmiddleware v1.30.0 // indirect go.opentelemetry.io/collector/featuregate v1.31.0 // indirect go.opentelemetry.io/collector/pipeline v0.125.0 // indirect go.opentelemetry.io/contrib/bridges/otelzap v0.10.0 // indirect @@ -159,6 +161,8 @@ replace go.opentelemetry.io/collector/extension/extensionauth/extensionauthtest replace go.opentelemetry.io/collector/extension/extensionmiddleware => ../../extension/extensionmiddleware +replace go.opentelemetry.io/collector/extension/extensionlimiter => ../../extension/extensionlimiter + replace go.opentelemetry.io/collector/config/configmiddleware => ../../config/configmiddleware replace go.opentelemetry.io/collector/extension/extensionmiddleware/extensionmiddlewaretest => ../../extension/extensionmiddleware/extensionmiddlewaretest diff --git a/receiver/otlpreceiver/otlp.go b/receiver/otlpreceiver/otlp.go index 7e5ab5d4b30d..4a92a634a288 100644 --- a/receiver/otlpreceiver/otlp.go +++ b/receiver/otlpreceiver/otlp.go @@ -18,6 +18,8 @@ import ( "go.opentelemetry.io/collector/config/confighttp" "go.opentelemetry.io/collector/consumer" "go.opentelemetry.io/collector/consumer/xconsumer" + "go.opentelemetry.io/collector/extension/extensionlimiter" + "go.opentelemetry.io/collector/extension/extensionlimiter/limiterhelper" "go.opentelemetry.io/collector/internal/telemetry" "go.opentelemetry.io/collector/internal/telemetry/componentattribute" "go.opentelemetry.io/collector/pdata/plog/plogotlp" @@ -97,20 +99,42 @@ func (r *otlpReceiver) startGRPCServer(host component.Host) error { return err } + limitKeys := extensionlimiter.StandardNotMiddlewareKeys() + limiterProvider, err := limiterhelper.MiddlewaresToLimiterWrapperProvider(host, r.cfg.GRPC.Middlewares) + if err != nil { + return err + } + if r.nextTraces != nil { - ptraceotlp.RegisterGRPCServer(r.serverGRPC, trace.New(r.nextTraces, r.obsrepGRPC)) + next, err := limiterhelper.NewLimitedTraces(r.nextTraces, limitKeys, limiterProvider) + if err != nil { + return err + } + ptraceotlp.RegisterGRPCServer(r.serverGRPC, trace.New(next, r.obsrepGRPC)) } if r.nextMetrics != nil { - pmetricotlp.RegisterGRPCServer(r.serverGRPC, metrics.New(r.nextMetrics, r.obsrepGRPC)) + next, err := limiterhelper.NewLimitedMetrics(r.nextMetrics, limitKeys, limiterProvider) + if err != nil { + return err + } + pmetricotlp.RegisterGRPCServer(r.serverGRPC, metrics.New(next, r.obsrepGRPC)) } if r.nextLogs != nil { - plogotlp.RegisterGRPCServer(r.serverGRPC, logs.New(r.nextLogs, r.obsrepGRPC)) + next, err := limiterhelper.NewLimitedLogs(r.nextLogs, limitKeys, limiterProvider) + if err != nil { + return err + } + plogotlp.RegisterGRPCServer(r.serverGRPC, logs.New(next, r.obsrepGRPC)) } if r.nextProfiles != nil { - pprofileotlp.RegisterGRPCServer(r.serverGRPC, profiles.New(r.nextProfiles)) + next, err := limiterhelper.NewLimitedProfiles(r.nextProfiles, limitKeys, limiterProvider) + if err != nil { + return err + } + pprofileotlp.RegisterGRPCServer(r.serverGRPC, profiles.New(next)) } r.settings.Logger.Info("Starting GRPC server", zap.String("endpoint", r.cfg.GRPC.NetAddr.Endpoint)) @@ -136,15 +160,29 @@ func (r *otlpReceiver) startHTTPServer(ctx context.Context, host component.Host) return nil } + limitKeys := extensionlimiter.StandardNotMiddlewareKeys() + limiterProvider, err := limiterhelper.MiddlewaresToLimiterWrapperProvider(host, r.cfg.HTTP.ServerConfig.Middlewares) + if err != nil { + return err + } + httpMux := http.NewServeMux() if r.nextTraces != nil { - httpTracesReceiver := trace.New(r.nextTraces, r.obsrepHTTP) + next, err := limiterhelper.NewLimitedTraces(r.nextTraces, limitKeys, limiterProvider) + if err != nil { + return err + } + httpTracesReceiver := trace.New(next, r.obsrepHTTP) httpMux.HandleFunc(r.cfg.HTTP.TracesURLPath, func(resp http.ResponseWriter, req *http.Request) { handleTraces(resp, req, httpTracesReceiver) }) } if r.nextMetrics != nil { + _, err := limiterhelper.NewLimitedMetrics(r.nextMetrics, limitKeys, limiterProvider) + if err != nil { + return err + } httpMetricsReceiver := metrics.New(r.nextMetrics, r.obsrepHTTP) httpMux.HandleFunc(r.cfg.HTTP.MetricsURLPath, func(resp http.ResponseWriter, req *http.Request) { handleMetrics(resp, req, httpMetricsReceiver) @@ -152,20 +190,27 @@ func (r *otlpReceiver) startHTTPServer(ctx context.Context, host component.Host) } if r.nextLogs != nil { - httpLogsReceiver := logs.New(r.nextLogs, r.obsrepHTTP) + next, err := limiterhelper.NewLimitedLogs(r.nextLogs, limitKeys, limiterProvider) + if err != nil { + return err + } + httpLogsReceiver := logs.New(next, r.obsrepHTTP) httpMux.HandleFunc(r.cfg.HTTP.LogsURLPath, func(resp http.ResponseWriter, req *http.Request) { handleLogs(resp, req, httpLogsReceiver) }) } if r.nextProfiles != nil { - httpProfilesReceiver := profiles.New(r.nextProfiles) + next, err := limiterhelper.NewLimitedProfiles(r.nextProfiles, limitKeys, limiterProvider) + if err != nil { + return err + } + httpProfilesReceiver := profiles.New(next) httpMux.HandleFunc(defaultProfilesURLPath, func(resp http.ResponseWriter, req *http.Request) { handleProfiles(resp, req, httpProfilesReceiver) }) } - var err error if r.serverHTTP, err = r.cfg.HTTP.ServerConfig.ToServer(ctx, host, r.settings.TelemetrySettings, httpMux, confighttp.WithErrorHandler(errorHandler)); err != nil { return err } From 04380f4243bbf0bd9057235ed48b363d1cc7e99d Mon Sep 17 00:00:00 2001 From: Joshua MacDonald Date: Wed, 30 Apr 2025 14:39:19 -0700 Subject: [PATCH 02/47] close --- extension/extensionlimiter/README.md | 137 +++++++++++++++++- .../extensionlimiter/extensionlimiter.go | 96 +++--------- .../limiterhelper/consumer.go | 32 ++-- .../limiterhelper/middleware.go | 4 + extension/extensionlimiter/rate.go | 10 +- extension/extensionlimiter/resource.go | 10 +- extension/extensionlimiter/wrapper.go | 97 +++++++++++++ 7 files changed, 278 insertions(+), 108 deletions(-) create mode 100644 extension/extensionlimiter/wrapper.go diff --git a/extension/extensionlimiter/README.md b/extension/extensionlimiter/README.md index c64b85d76767..0c7645e6109e 100644 --- a/extension/extensionlimiter/README.md +++ b/extension/extensionlimiter/README.md @@ -109,6 +109,8 @@ before creating new concurrent work. ### Examples +#### OTLP receiver + Limiters applied through middleware are an implementation detail, simply configure them using `configgrpc` or `confighttp`. For the OTLP receiver (e.g., with two `ratelimiter` extensions): @@ -131,7 +133,134 @@ receivers: - ratelimiter/limit_for_http ``` -@@@ -a stream one -a pull-based one -a data-dependent one +Note that the OTLP receiver specifically supports multiple protocols +with separate middleware configurations, thus it configures limiters +for request items and memory size on a protocol-by-protocol +basis. + +#### HTTP metrics scraper + +A HTTP pull-based receiver can implement a basic limited scraper loop +as follows. The HTTP client config object's `middlewares` field +automatically configures network bytes and request count limits: + +``` +receivers: + scraper: + http: + middlewares: + - ratelimiter/scraper +``` + +Limiter extensions are derived from a host, a middlewares list, and a +list of weight keys. When middleware is configurable at the factory +level, it may be added via `receiver.NewFactory` using +`receiver.WithLimiters(getLimiters)`: + +``` +func NewFactory() receiver.Factory { + return xreceiver.NewFactory( + metadata.Type, + createDefaultConfig, + xreceiver.WithMetrics(createMetrics, metadata.MetricsStability), + xreceiver.WithLimiters(getLimiters), + ) +} +``` + +Here, `getLimiters` is a function to get the effective +`[]configmiddleware.Config` and derive pipeline consumers using +`limiterhelper` adapters. + +To acquire a limiter, use `MiddlewaresToLimiterWrapperProvider` to +obtain a combined limiter wrapper around the input `nextMetrics` +consumer. It will pass `StandardNotMiddlewareKeys()` indicating to +apply request items and memory size: + +``` + // Extract limiter provider from middlewares. + s.limiterProvider, err = limiterhelper.MiddlewaresToLimiterWrapperProvider( + cfg.Middlewares) + if err != nil { ... } + + // Here get a limiter-wrapped pipeline and a combination of weight-specific + // limiters for MustDeny() functionality. + s.anyLimiter, s.nextMetrics, err = limiterhelper.NewLimitedMetrics( + s.nextMetrics, limiterhelper.StandardNotMiddlewareKeys(), s.limiterProvider) + if err != nil { ... } +``` + +In the scraper loop, use `MustDeny` before starting a scrape: + +``` +func (s *scraper) scrapeOnce(ctx context.Context) error { + if err := s.anyLimiter.MustDeny(ctx); err != nil { + return err + } + + // Network bytes and request count limits are applied in middleware. + // before this returns: + data, err := s.getData(ctx) + if err != nil { + return err + } + + // Request items and memory size are applied in the pipeline. + return s.nextMetrics.ConsumeMetrics(ctx, data) +} +``` + +#### gRPC stream receiver + +A gRPC streaming receiver that holds memory across its allocated in +`Send()` and does not release it until after a corresponding `Recv()` +requires use of the lower-level `ResourceLimiter` interface. +The gRPC config object's `middlewares` field +automatically configures network bytes and request count limits: + +``` +receivers: + streamer: + grpc: + middlewares: + - ratelimiter/streamer +``` + +The receiver will check `s.anyLimiter.MustDeny()` as above. In a +stream, limiters are expected to block the stream until limit requests +succeed, however after the limit requests succeed, the receiver may +wish to return from `Send()` to continue accepting new requests while +the consumer works in a separate goroutine. The limit will be released +after the consumer returns. + +``` +func (s *scraper) LogsStream(ctx context.Context, stream *Stream) error { + for { + // Check saturation for all limiters. + err := s.anyLimiter.MustDeny(ctx) + if err != nil { ... } + + // The network bytes and request count are applied in middleware. + req, err := stream.Recv() + if err != nil { ... } + + // Allocate memory objects. + data, err := s.getLogs(ctx, req) + if err != nil { ... } + + release, err := s.memorySizeLimiter.Acquire(ctx, pdataSize(data)) + if err != nil { ... } + + go func() { + // Request items limit is applied in the pipeline consumer + err := s.nextMetrics.ConsumeMetrics(ctx, data) + + // Release the memory. + release() + + // Reply to the caller. + stream.Send(streamResponseFromConsumerError(err)) + } + } +} +``` diff --git a/extension/extensionlimiter/extensionlimiter.go b/extension/extensionlimiter/extensionlimiter.go index 3e2fa915513a..78fcbf32d908 100644 --- a/extension/extensionlimiter/extensionlimiter.go +++ b/extension/extensionlimiter/extensionlimiter.go @@ -7,19 +7,10 @@ import ( "context" ) -// LimiterWrapper is a general-purpose interface for limiter consumers -// to limit resources with use of a callback. This is the simplest -// form of rate limiting interface from a callers perspective. If the -// caller is a pipeline component, consider using a consumer-oriented -// limiterhelper (e.g., limiterhelper.NewLimitedLogs) to apply a list of -// -// Limiter implementions are meant to implement either the RateLimiter -// or ResourceLimiter interfaces. LimiterWrappers can be constructed -// from either of the underlying limiters and their corresponding -// providers. Usually configmiddleware or limiterhelper is responsible -// for constructing the correct wrapper from these two kinds of limiter; -// users will use this interface consistently. -type LimiterWrapper interface { +// Limiter is the common functionality implemented by LimiterWrapper, +// RateLimiter, and ResourceLimiter. This can be called prior to the +// start of work to check for limiter saturation. +type Limiter interface { // Must deny is the logical equivalent of Acquire(0). If the // Acquire would fail even for 0 units of a rate, the // caller must deny the request. Implementations are @@ -28,76 +19,35 @@ type LimiterWrapper interface { // faster code path to implement MustDeny() since it does not // depend on the value. MustDeny(context.Context) error - - // LimitCall applies the limiter and with the rate or resource - // granted makes a scoped call, returning success or an error - // from either the limiter or the enclosed callback. - LimitCall(context.Context, uint64, func(ctx context.Context) error) error -} - -// LimiterWrapperProvider provides access to LimiterWrappers, which is -// the appropriate interface for callers that can easily wrap a -// function call, because for wrapped calls there is no distinction -// between rate limiters and resource limiters. -type LimiterWrapperProvider interface { - LimiterWrapper(WeightKey) (LimiterWrapper, error) } -// LimiterWrapperFunc is a functional way to build LimiterWrappers. -type LimiterWrapperFunc func(context.Context, uint64, func(ctx context.Context) error) error +// LimiterFunc is a functional way to build MustDeny functions. +type LimiterFunc func(context.Context) error -var _ LimiterWrapper = LimiterWrapperFunc(nil) +var _ Limiter = LimiterFunc(nil) -// MustDeny implements LimiterWrapper. -func (f LimiterWrapperFunc) MustDeny(ctx context.Context) error { - return f.LimitCall(ctx, 0, func(_ context.Context) error { - return nil - }) -} - -// LimitCall implements LimiterWrapper. -func (f LimiterWrapperFunc) LimitCall(ctx context.Context, value uint64, call func(ctx context.Context) error) error { +// MustDeny implements Limiter. +func (f LimiterFunc) MustDeny(ctx context.Context) error { if f == nil { - return call(ctx) + return nil } - return f(ctx, value, call) + return f(ctx) } -// PassThrough returns a LimiterWrapper that imposes no limit. -func PassThrough() LimiterWrapper { - return LimiterWrapperFunc(nil) -} - -// LimiterWrapperProviderFunc is a functional way to build LimiterWrappers. -type LimiterWrapperProviderFunc func(WeightKey) (LimiterWrapper, error) +// MultiLimiter returns MustDeny when any element returns MustDeny. +type MultiLimiter []Limiter -var _ LimiterWrapperProvider = LimiterWrapperProviderFunc(nil) - -// LimiterWrapper implements LimiterWrapperProvider. -func (f LimiterWrapperProviderFunc) LimiterWrapper(key WeightKey) (LimiterWrapper, error) { - return f(key) -} +var _ Limiter = MultiLimiter{} -// NewResourceLimiterWrapperProvider constructs a -// LimiterWrapperProvider for a resource limiter extension. -func NewResourceLimiterWrapperProvider(rp ResourceLimiterProvider) LimiterWrapperProvider { - return LimiterWrapperProviderFunc(func(key WeightKey) (LimiterWrapper, error) { - lim, err := rp.ResourceLimiter(key) - if err == nil { - return nil, err +// MustDeny implements Limiter. +func (ls MultiLimiter) MustDeny(ctx context.Context) error { + for _, lim := range ls { + if lim == nil { + continue } - return NewResourceLimiterWrapper(lim), err - }) -} - -// NewRateLimiterWrapperProvider constructs a LimiterWrapperProvider -// for a rate limiter extension. -func NewRateLimiterWrapperProvider(rp RateLimiterProvider) LimiterWrapperProvider { - return LimiterWrapperProviderFunc(func(key WeightKey) (LimiterWrapper, error) { - lim, err := rp.RateLimiter(key) - if err == nil { - return nil, err + if err := lim.MustDeny(ctx); err != nil { + return err } - return NewRateLimiterWrapper(lim), err - }) + } + return nil } diff --git a/extension/extensionlimiter/limiterhelper/consumer.go b/extension/extensionlimiter/limiterhelper/consumer.go index 258d157d7737..d84b9fe4732f 100644 --- a/extension/extensionlimiter/limiterhelper/consumer.go +++ b/extension/extensionlimiter/limiterhelper/consumer.go @@ -125,22 +125,23 @@ func limitOne[P any, C any]( key extensionlimiter.WeightKey, opts []consumer.Option, quantify func(P) uint64, -) (C, error) { +) (extensionlimiter.Limiter, C, error) { if !slices.Contains(keys, key) { - return next, nil + return nil, next, nil } lim, err := provider.LimiterWrapper(key) if err != nil { - return next, err + return nil, next, err } if lim == nil { - return next, nil + return nil, next, nil } - return m.create(func(ctx context.Context, data P) error { + con, err := m.create(func(ctx context.Context, data P) error { return lim.LimitCall(ctx, quantify(data), func(ctx context.Context) error { return m.consume(ctx, data, next) }) }, opts...) + return lim, con, err } // newLimited is signal-generic limiting logic. @@ -150,48 +151,49 @@ func newLimited[P any, C any]( provider extensionlimiter.LimiterWrapperProvider, m traits[P, C], opts ...consumer.Option, -) (C, error) { +) (extensionlimiter.Limiter, C, error) { if provider == nil { - return next, nil + return nil, next, nil } + var lim1, lim2, lim3 extensionlimiter.Limiter var err1, err2, err3 error // Note: reverse order of evaluation cost => least-cost applied first. - next, err1 = limitOne(next, keys, provider, m, extensionlimiter.WeightKeyMemorySize, opts, + lim1, next, err1 = limitOne(next, keys, provider, m, extensionlimiter.WeightKeyMemorySize, opts, func(data P) uint64 { return m.memorySize(data) }) - next, err2 = limitOne(next, keys, provider, m, extensionlimiter.WeightKeyRequestItems, opts, + lim2, next, err2 = limitOne(next, keys, provider, m, extensionlimiter.WeightKeyRequestItems, opts, func(data P) uint64 { return m.itemCount(data) }) - next, err3 = limitOne(next, keys, provider, m, extensionlimiter.WeightKeyRequestCount, opts, + lim3, next, err3 = limitOne(next, keys, provider, m, extensionlimiter.WeightKeyRequestCount, opts, func(_ P) uint64 { return 1 }) - return next, errors.Join(err1, err2, err3) + return extensionlimiter.MultiLimiter{lim1, lim2, lim3}, next, errors.Join(err1, err2, err3) } // NewLimitedTraces applies a limiter using the provider over keys before calling next. -func NewLimitedTraces(next consumer.Traces, keys []extensionlimiter.WeightKey, provider extensionlimiter.LimiterWrapperProvider) (consumer.Traces, error) { +func NewLimitedTraces(next consumer.Traces, keys []extensionlimiter.WeightKey, provider extensionlimiter.LimiterWrapperProvider) (extensionlimiter.Limiter, consumer.Traces, error) { return newLimited(next, keys, provider, traceTraits{}, consumer.WithCapabilities(next.Capabilities())) } // NewLimitedLogs applies a limiter using the provider over keys before calling next. -func NewLimitedLogs(next consumer.Logs, keys []extensionlimiter.WeightKey, provider extensionlimiter.LimiterWrapperProvider) (consumer.Logs, error) { +func NewLimitedLogs(next consumer.Logs, keys []extensionlimiter.WeightKey, provider extensionlimiter.LimiterWrapperProvider) (extensionlimiter.Limiter, consumer.Logs, error) { return newLimited(next, keys, provider, logTraits{}, consumer.WithCapabilities(next.Capabilities())) } // NewLimitedMetrics applies a limiter using the provider over keys before calling next. -func NewLimitedMetrics(next consumer.Metrics, keys []extensionlimiter.WeightKey, provider extensionlimiter.LimiterWrapperProvider) (consumer.Metrics, error) { +func NewLimitedMetrics(next consumer.Metrics, keys []extensionlimiter.WeightKey, provider extensionlimiter.LimiterWrapperProvider) (extensionlimiter.Limiter, consumer.Metrics, error) { return newLimited(next, keys, provider, metricTraits{}, consumer.WithCapabilities(next.Capabilities())) } // NewLimitedProfiles applies a limiter using the provider over keys before calling next. -func NewLimitedProfiles(next xconsumer.Profiles, keys []extensionlimiter.WeightKey, provider extensionlimiter.LimiterWrapperProvider) (xconsumer.Profiles, error) { +func NewLimitedProfiles(next xconsumer.Profiles, keys []extensionlimiter.WeightKey, provider extensionlimiter.LimiterWrapperProvider) (extensionlimiter.Limiter, xconsumer.Profiles, error) { return newLimited(next, keys, provider, profileTraits{}, consumer.WithCapabilities(next.Capabilities())) } diff --git a/extension/extensionlimiter/limiterhelper/middleware.go b/extension/extensionlimiter/limiterhelper/middleware.go index 91735349f63f..95b3788bf63a 100644 --- a/extension/extensionlimiter/limiterhelper/middleware.go +++ b/extension/extensionlimiter/limiterhelper/middleware.go @@ -55,6 +55,10 @@ func MiddlewaresToLimiterWrapperProvider(host component.Host, middleware []confi return MultiLimiterWrapperProvider(providers), nil } +// Note: MiddlewaresToRateLimiterProvider, MiddlewaresToResourceLimiterProvider +// are needed for special cases, however these functions can be implemented +// manually, they are similar to the above. + // MiddlewareToLimiterWrapperProvider returns a limiter wrapper // provider from middleware. Returns a package-level error if the // middleware does not implement exactly one of the limiter diff --git a/extension/extensionlimiter/rate.go b/extension/extensionlimiter/rate.go index 70bc9986e353..cb16f86c78b6 100644 --- a/extension/extensionlimiter/rate.go +++ b/extension/extensionlimiter/rate.go @@ -39,14 +39,8 @@ func (f RateLimiterProviderFunc) RateLimiter(key WeightKey) (RateLimiter, error) // // See the README for more recommendations. type RateLimiter interface { - // Must deny is the logical equivalent of Acquire(0). If the - // Acquire would fail even for 0 units of a rate, the - // caller must deny the request. Implementations are - // encouraged to ensure that when MustDeny() is false, - // Acquire(0) is also false, however callers could use a - // faster code path to implement MustDeny() since it does not - // depend on the value. - MustDeny(context.Context) error + // Limiter includes MustDeny(). + Limiter // Limit attempts to apply rate limiting with the provided // weight, based on the key that was given to the provider. diff --git a/extension/extensionlimiter/resource.go b/extension/extensionlimiter/resource.go index 704a39e7d5b3..11dddd3b2232 100644 --- a/extension/extensionlimiter/resource.go +++ b/extension/extensionlimiter/resource.go @@ -41,14 +41,8 @@ func (f ResourceLimiterProviderFunc) ResourceLimiter(key WeightKey) (ResourceLim // // See the README for more recommendations. type ResourceLimiter interface { - // Must deny is the logical equivalent of Acquire(0). If the - // Acquire would fail even for 0 units of a resource, the - // caller must deny the request. Implementations are - // encouraged to ensure that when MustDeny() is false, - // Acquire(0) is also false, however callers could use a - // faster code path to implement MustDeny() since it does not - // depend on the value. - MustDeny(context.Context) error + // Limiter includes MustDeny(). + Limiter // Acquire attempts to acquire a quantified resource with the // provided weight, based on the key that was given to the diff --git a/extension/extensionlimiter/wrapper.go b/extension/extensionlimiter/wrapper.go new file mode 100644 index 000000000000..85b115d009f3 --- /dev/null +++ b/extension/extensionlimiter/wrapper.go @@ -0,0 +1,97 @@ +// Copyright The OpenTelemetry Authors +// SPDX-License-Identifier: Apache-2.0 + +package extensionlimiter // import "go.opentelemetry.io/collector/extension/extensionlimiter" + +import ( + "context" +) + +// LimiterWrapper is a general-purpose interface for limiter consumers +// to limit resources with use of a callback. This is the simplest +// form of rate limiting interface from a callers perspective. If the +// caller is a pipeline component, consider using a consumer-oriented +// limiterhelper (e.g., limiterhelper.NewLimitedLogs) to apply a list of +// +// Limiter implementions are meant to implement either the RateLimiter +// or ResourceLimiter interfaces. LimiterWrappers can be constructed +// from either of the underlying limiters and their corresponding +// providers. Usually configmiddleware or limiterhelper is responsible +// for constructing the correct wrapper from these two kinds of limiter; +// users will use this interface consistently. +type LimiterWrapper interface { + // Limiter includes MustDeny(). + Limiter + + // LimitCall applies the limiter and with the rate or resource + // granted makes a scoped call, returning success or an error + // from either the limiter or the enclosed callback. + LimitCall(context.Context, uint64, func(ctx context.Context) error) error +} + +// LimiterWrapperProvider provides access to LimiterWrappers, which is +// the appropriate interface for callers that can easily wrap a +// function call, because for wrapped calls there is no distinction +// between rate limiters and resource limiters. +type LimiterWrapperProvider interface { + LimiterWrapper(WeightKey) (LimiterWrapper, error) +} + +// LimiterWrapperFunc is a functional way to build LimiterWrappers. +type LimiterWrapperFunc func(context.Context, uint64, func(ctx context.Context) error) error + +var _ LimiterWrapper = LimiterWrapperFunc(nil) + +// MustDeny implements LimiterWrapper. +func (f LimiterWrapperFunc) MustDeny(ctx context.Context) error { + return f.LimitCall(ctx, 0, func(_ context.Context) error { + return nil + }) +} + +// LimitCall implements LimiterWrapper. +func (f LimiterWrapperFunc) LimitCall(ctx context.Context, value uint64, call func(ctx context.Context) error) error { + if f == nil { + return call(ctx) + } + return f(ctx, value, call) +} + +// PassThrough returns a LimiterWrapper that imposes no limit. +func PassThrough() LimiterWrapper { + return LimiterWrapperFunc(nil) +} + +// LimiterWrapperProviderFunc is a functional way to build LimiterWrappers. +type LimiterWrapperProviderFunc func(WeightKey) (LimiterWrapper, error) + +var _ LimiterWrapperProvider = LimiterWrapperProviderFunc(nil) + +// LimiterWrapper implements LimiterWrapperProvider. +func (f LimiterWrapperProviderFunc) LimiterWrapper(key WeightKey) (LimiterWrapper, error) { + return f(key) +} + +// NewResourceLimiterWrapperProvider constructs a +// LimiterWrapperProvider for a resource limiter extension. +func NewResourceLimiterWrapperProvider(rp ResourceLimiterProvider) LimiterWrapperProvider { + return LimiterWrapperProviderFunc(func(key WeightKey) (LimiterWrapper, error) { + lim, err := rp.ResourceLimiter(key) + if err == nil { + return nil, err + } + return NewResourceLimiterWrapper(lim), err + }) +} + +// NewRateLimiterWrapperProvider constructs a LimiterWrapperProvider +// for a rate limiter extension. +func NewRateLimiterWrapperProvider(rp RateLimiterProvider) LimiterWrapperProvider { + return LimiterWrapperProviderFunc(func(key WeightKey) (LimiterWrapper, error) { + lim, err := rp.RateLimiter(key) + if err == nil { + return nil, err + } + return NewRateLimiterWrapper(lim), err + }) +} From 7444153a89d9fcd5e11af97c92e1e28e277bd731 Mon Sep 17 00:00:00 2001 From: Joshua MacDonald Date: Wed, 30 Apr 2025 14:42:58 -0700 Subject: [PATCH 03/47] readme --- extension/extensionlimiter/README.md | 20 +++++++++++--------- 1 file changed, 11 insertions(+), 9 deletions(-) diff --git a/extension/extensionlimiter/README.md b/extension/extensionlimiter/README.md index 0c7645e6109e..a1fdb090b849 100644 --- a/extension/extensionlimiter/README.md +++ b/extension/extensionlimiter/README.md @@ -5,27 +5,28 @@ The `extensionlimiter` package provides interfaces for rate limiting and resource limiting in the OpenTelemetry Collector, enabling control over data flow and resource usage through extensions which can be -configured through and middleware and/or directly by pipeline -components. +configured through middleware and/or directly by pipeline components. ## Overview -This package defines two primary limiter types with their respective -interfaces: +This package defines two primary limiter **kinds**, which have +different interfaces: - **Rate Limiters**: Control time-based limits on quantities such as bytes or items per second. - **Resource Limiters**: Manage physical limits on quantities such as concurrent requests or memory usage. -Both limiter types are unified through the `LimiterWrapper` interface, -which simplifies consumer usage by providing a consistent `LimitCall` -interface. +Both limiter kinds are unified through the `LimiterWrapper` interface, +which simplifies consumers in most cases by providing a consistent +`LimitCall` interface. A limiter is **saturated** by definition when a limit is completely -overloaded, generally it means a limit request of any size would fail. +overloaded, generally it means a limit request of any size would fail +at this moment and should be taken as a strong signal to stop +accepting requests. -Each each base limiter type and the wrapper type have corresponding +Each kind of limiter as well as the wrapper type have corresponding providers that give access to a limiter instance based on a weight key. @@ -40,6 +41,7 @@ request items, and memory size. - `RateLimiter`: Applies time-based limits, has a `Limit` method. - `ResourceLimiter`: Manages physical resource limits, has an `Acquire` method and corresponding `ReleaseFunc`. +- `Limiter`: Any of the above, has a `MustDeny` method. ### Limiter helpers From e597fa1d1fc5c6e455cf910bface64650b0b0191 Mon Sep 17 00:00:00 2001 From: Joshua MacDonald Date: Wed, 30 Apr 2025 14:50:15 -0700 Subject: [PATCH 04/47] move multi-limiter --- extension/extensionlimiter/README.md | 22 +++++++++++-------- .../extensionlimiter/extensionlimiter.go | 18 --------------- .../limiterhelper/consumer.go | 20 ++++++++++++++++- receiver/otlpreceiver/otlp.go | 18 +++++++-------- 4 files changed, 41 insertions(+), 37 deletions(-) diff --git a/extension/extensionlimiter/README.md b/extension/extensionlimiter/README.md index a1fdb090b849..29e43943d20f 100644 --- a/extension/extensionlimiter/README.md +++ b/extension/extensionlimiter/README.md @@ -27,20 +27,23 @@ at this moment and should be taken as a strong signal to stop accepting requests. Each kind of limiter as well as the wrapper type have corresponding -providers that give access to a limiter instance based on a weight -key. +**provider** interface that returns a limiter instance based on a +weight key or keys. -Weight keys describes the standard limiting dimensions. There are +Weight keys describe the standard limiting dimensions. There are currently four standard weight keys: network bytes, request count, request items, and memory size. ## Key Interfaces - `LimiterWrapper`: Provides a callback-based limiting interface that - works with both rate and resource limiters, has a `LimitCall` method. -- `RateLimiter`: Applies time-based limits, has a `Limit` method. + works with both rate and resource limiters, has a `LimitCall` method, + plus a provider type. +- `RateLimiter`: Applies time-based limits, has a `Limit` method, + plus provider type. - `ResourceLimiter`: Manages physical resource limits, has - an `Acquire` method and corresponding `ReleaseFunc`. + an `Acquire` method and corresponding `ReleaseFunc`, + plus a provider type. - `Limiter`: Any of the above, has a `MustDeny` method. ### Limiter helpers @@ -48,9 +51,10 @@ request items, and memory size. The `limiterhelper` subpackage provides: - Consumer wrappers apply limits to a collector pipeline (e.g., - `NewLimitedLogs` to combine a limiter using `consumer.NewLogs`) -- Multi-limiter combinators: `MultiLimiterWrapperProvider` builds a sequence of wrapped limiters. -- Middleware conversion utilities: Convert middleware configurations to `LimiterWrapperProvider`. + `NewLimitedLogs` for a limiter combined with `consumer.NewLogs`) +- Multi-limiter combinators: for simple combined limiter functionality +- Middleware conversion utilities: convert middleware configurations to + liiter providers. ## Recommendations diff --git a/extension/extensionlimiter/extensionlimiter.go b/extension/extensionlimiter/extensionlimiter.go index 78fcbf32d908..0578c4aa2465 100644 --- a/extension/extensionlimiter/extensionlimiter.go +++ b/extension/extensionlimiter/extensionlimiter.go @@ -33,21 +33,3 @@ func (f LimiterFunc) MustDeny(ctx context.Context) error { } return f(ctx) } - -// MultiLimiter returns MustDeny when any element returns MustDeny. -type MultiLimiter []Limiter - -var _ Limiter = MultiLimiter{} - -// MustDeny implements Limiter. -func (ls MultiLimiter) MustDeny(ctx context.Context) error { - for _, lim := range ls { - if lim == nil { - continue - } - if err := lim.MustDeny(ctx); err != nil { - return err - } - } - return nil -} diff --git a/extension/extensionlimiter/limiterhelper/consumer.go b/extension/extensionlimiter/limiterhelper/consumer.go index d84b9fe4732f..e56e24233db6 100644 --- a/extension/extensionlimiter/limiterhelper/consumer.go +++ b/extension/extensionlimiter/limiterhelper/consumer.go @@ -17,6 +17,24 @@ import ( "go.opentelemetry.io/collector/pdata/ptrace" ) +// MultiLimiter returns MustDeny when any element returns MustDeny. +type MultiLimiter []extensionlimiter.Limiter + +var _ extensionlimiter.Limiter = MultiLimiter{} + +// MustDeny implements Limiter. +func (ls MultiLimiter) MustDeny(ctx context.Context) error { + for _, lim := range ls { + if lim == nil { + continue + } + if err := lim.MustDeny(ctx); err != nil { + return err + } + } + return nil +} + // Traits object interface is generalized by P the pipeline data type // (e.g., ptrace.Traces) and C the consumer type (e.g., // consumer.Traces) @@ -171,7 +189,7 @@ func newLimited[P any, C any]( func(_ P) uint64 { return 1 }) - return extensionlimiter.MultiLimiter{lim1, lim2, lim3}, next, errors.Join(err1, err2, err3) + return MultiLimiter{lim1, lim2, lim3}, next, errors.Join(err1, err2, err3) } // NewLimitedTraces applies a limiter using the provider over keys before calling next. diff --git a/receiver/otlpreceiver/otlp.go b/receiver/otlpreceiver/otlp.go index 4a92a634a288..c91ed2ea39cc 100644 --- a/receiver/otlpreceiver/otlp.go +++ b/receiver/otlpreceiver/otlp.go @@ -106,7 +106,7 @@ func (r *otlpReceiver) startGRPCServer(host component.Host) error { } if r.nextTraces != nil { - next, err := limiterhelper.NewLimitedTraces(r.nextTraces, limitKeys, limiterProvider) + _, next, err := limiterhelper.NewLimitedTraces(r.nextTraces, limitKeys, limiterProvider) if err != nil { return err } @@ -114,7 +114,7 @@ func (r *otlpReceiver) startGRPCServer(host component.Host) error { } if r.nextMetrics != nil { - next, err := limiterhelper.NewLimitedMetrics(r.nextMetrics, limitKeys, limiterProvider) + _, next, err := limiterhelper.NewLimitedMetrics(r.nextMetrics, limitKeys, limiterProvider) if err != nil { return err } @@ -122,7 +122,7 @@ func (r *otlpReceiver) startGRPCServer(host component.Host) error { } if r.nextLogs != nil { - next, err := limiterhelper.NewLimitedLogs(r.nextLogs, limitKeys, limiterProvider) + _, next, err := limiterhelper.NewLimitedLogs(r.nextLogs, limitKeys, limiterProvider) if err != nil { return err } @@ -130,7 +130,7 @@ func (r *otlpReceiver) startGRPCServer(host component.Host) error { } if r.nextProfiles != nil { - next, err := limiterhelper.NewLimitedProfiles(r.nextProfiles, limitKeys, limiterProvider) + _, next, err := limiterhelper.NewLimitedProfiles(r.nextProfiles, limitKeys, limiterProvider) if err != nil { return err } @@ -168,7 +168,7 @@ func (r *otlpReceiver) startHTTPServer(ctx context.Context, host component.Host) httpMux := http.NewServeMux() if r.nextTraces != nil { - next, err := limiterhelper.NewLimitedTraces(r.nextTraces, limitKeys, limiterProvider) + _, next, err := limiterhelper.NewLimitedTraces(r.nextTraces, limitKeys, limiterProvider) if err != nil { return err } @@ -179,18 +179,18 @@ func (r *otlpReceiver) startHTTPServer(ctx context.Context, host component.Host) } if r.nextMetrics != nil { - _, err := limiterhelper.NewLimitedMetrics(r.nextMetrics, limitKeys, limiterProvider) + _, next, err := limiterhelper.NewLimitedMetrics(r.nextMetrics, limitKeys, limiterProvider) if err != nil { return err } - httpMetricsReceiver := metrics.New(r.nextMetrics, r.obsrepHTTP) + httpMetricsReceiver := metrics.New(next, r.obsrepHTTP) httpMux.HandleFunc(r.cfg.HTTP.MetricsURLPath, func(resp http.ResponseWriter, req *http.Request) { handleMetrics(resp, req, httpMetricsReceiver) }) } if r.nextLogs != nil { - next, err := limiterhelper.NewLimitedLogs(r.nextLogs, limitKeys, limiterProvider) + _, next, err := limiterhelper.NewLimitedLogs(r.nextLogs, limitKeys, limiterProvider) if err != nil { return err } @@ -201,7 +201,7 @@ func (r *otlpReceiver) startHTTPServer(ctx context.Context, host component.Host) } if r.nextProfiles != nil { - next, err := limiterhelper.NewLimitedProfiles(r.nextProfiles, limitKeys, limiterProvider) + _, next, err := limiterhelper.NewLimitedProfiles(r.nextProfiles, limitKeys, limiterProvider) if err != nil { return err } From e6675c8fed3d5b2cb1d60ab22fdf375759317ef4 Mon Sep 17 00:00:00 2001 From: Joshua MacDonald Date: Wed, 30 Apr 2025 15:11:02 -0700 Subject: [PATCH 05/47] data-dep example --- extension/extensionlimiter/README.md | 32 +++++++++++++ .../extensionlimiter/extensionlimiter.go | 10 ++++ .../limiterhelper/middleware.go | 4 +- extension/extensionlimiter/rate.go | 9 ++-- extension/extensionlimiter/resource.go | 8 ++-- extension/extensionlimiter/wrapper.go | 46 +++++++++---------- 6 files changed, 76 insertions(+), 33 deletions(-) diff --git a/extension/extensionlimiter/README.md b/extension/extensionlimiter/README.md index 29e43943d20f..24d9fdc600d9 100644 --- a/extension/extensionlimiter/README.md +++ b/extension/extensionlimiter/README.md @@ -270,3 +270,35 @@ func (s *scraper) LogsStream(ctx context.Context, stream *Stream) error { } } ``` + +#### Data-dependent limiter processor + +**NOTE: This is not implemented.** + +The provider interfaces can be extended to accept a +`map[string]string` that identify limiter instances based on +additional metadata, such as tenant information. Since the limits are +data specific, the limiter will be computed for each request and for +each specific weight key. + +Limiter implementations would support options, likely assisted by +`limiterhelper` features to configure them, for configuring +metadata-specific limits. + +``` +func handleRequest(ctx context.Context, req *Request) error { + // Get a data-specific limiter: + md := metadataFromRequest(req) + lim, err := s.limiterProvider.LimiterWrapper(weightKey, md) + if err != nil { ... } + + if err = lim.MustDeny(ctx); err != nil { ... } + + // Calculate the data and its weight. + data := dataFromReq(req) + weight := getWeight(data) + + return lim.LimitCall(ctx, weight, func(ctx context.Context) error { + return s.nextLogs.ConsumeLogs(ctx, data) + }) +``` diff --git a/extension/extensionlimiter/extensionlimiter.go b/extension/extensionlimiter/extensionlimiter.go index 0578c4aa2465..7ada56bc0458 100644 --- a/extension/extensionlimiter/extensionlimiter.go +++ b/extension/extensionlimiter/extensionlimiter.go @@ -7,6 +7,16 @@ import ( "context" ) +// Option is passed to limiter providers. +// +// NOTE: For data-specific or tenant-specific limits we will extend +// providers with Options and add a Config type, but none are +// supported yet and this PR contains only interfaces, not need for +// options in core repository components. +type Option interface { + apply() +} + // Limiter is the common functionality implemented by LimiterWrapper, // RateLimiter, and ResourceLimiter. This can be called prior to the // start of work to check for limiter saturation. diff --git a/extension/extensionlimiter/limiterhelper/middleware.go b/extension/extensionlimiter/limiterhelper/middleware.go index 95b3788bf63a..533327479d62 100644 --- a/extension/extensionlimiter/limiterhelper/middleware.go +++ b/extension/extensionlimiter/limiterhelper/middleware.go @@ -108,7 +108,7 @@ type MultiLimiterWrapperProvider []extensionlimiter.LimiterWrapperProvider var _ extensionlimiter.LimiterWrapperProvider = MultiLimiterWrapperProvider{} // LimiterWrapper implements LimiterWrapperProvider. -func (ps MultiLimiterWrapperProvider) LimiterWrapper(key extensionlimiter.WeightKey) (extensionlimiter.LimiterWrapper, error) { +func (ps MultiLimiterWrapperProvider) LimiterWrapper(key extensionlimiter.WeightKey, opts ...extensionlimiter.Option) (extensionlimiter.LimiterWrapper, error) { if len(ps) == 0 { return extensionlimiter.PassThrough(), nil } @@ -117,7 +117,7 @@ func (ps MultiLimiterWrapperProvider) LimiterWrapper(key extensionlimiter.Weight var lims []extensionlimiter.LimiterWrapper for _, provider := range ps { - lim, err := provider.LimiterWrapper(key) + lim, err := provider.LimiterWrapper(key, opts...) if err == nil { return nil, err } diff --git a/extension/extensionlimiter/rate.go b/extension/extensionlimiter/rate.go index cb16f86c78b6..a294bb84cb69 100644 --- a/extension/extensionlimiter/rate.go +++ b/extension/extensionlimiter/rate.go @@ -14,17 +14,18 @@ import ( // Limiters are covered by configmiddleware configuration, which is // able to construct LimiterWrappers from these providers. type RateLimiterProvider interface { - RateLimiter(WeightKey) (RateLimiter, error) + // RateLimiter returns a provider for rate limiters. + RateLimiter(WeightKey, ...Option) (RateLimiter, error) } // RateLimiterProviderFunc is a functional way to build RateLimters. -type RateLimiterProviderFunc func(WeightKey) (RateLimiter, error) +type RateLimiterProviderFunc func(WeightKey, ...Option) (RateLimiter, error) var _ RateLimiterProvider = RateLimiterProviderFunc(nil) // RateLimiter implements RateLimiterProvider. -func (f RateLimiterProviderFunc) RateLimiter(key WeightKey) (RateLimiter, error) { - return f(key) +func (f RateLimiterProviderFunc) RateLimiter(key WeightKey, opts ...Option) (RateLimiter, error) { + return f(key, opts...) } // RateLimiter is an interface that an implementation makes available diff --git a/extension/extensionlimiter/resource.go b/extension/extensionlimiter/resource.go index 11dddd3b2232..2bd62df41f2a 100644 --- a/extension/extensionlimiter/resource.go +++ b/extension/extensionlimiter/resource.go @@ -14,17 +14,17 @@ import ( // Limiters are covered by configmiddleware configuration, which // is able to construct LimiterWrappers from these providers. type ResourceLimiterProvider interface { - ResourceLimiter(WeightKey) (ResourceLimiter, error) + ResourceLimiter(WeightKey, ...Option) (ResourceLimiter, error) } // ResourceLimiterProviderFunc is a functional way to build ResourceLimters. -type ResourceLimiterProviderFunc func(WeightKey) (ResourceLimiter, error) +type ResourceLimiterProviderFunc func(WeightKey, ...Option) (ResourceLimiter, error) var _ ResourceLimiterProvider = ResourceLimiterProviderFunc(nil) // ResourceLimiter implements ResourceLimiterProvider. -func (f ResourceLimiterProviderFunc) ResourceLimiter(key WeightKey) (ResourceLimiter, error) { - return f(key) +func (f ResourceLimiterProviderFunc) ResourceLimiter(key WeightKey, opts ...Option) (ResourceLimiter, error) { + return f(key, opts...) } // ResourceLimiter is an interface that an implementation makes diff --git a/extension/extensionlimiter/wrapper.go b/extension/extensionlimiter/wrapper.go index 85b115d009f3..cd521f4487a0 100644 --- a/extension/extensionlimiter/wrapper.go +++ b/extension/extensionlimiter/wrapper.go @@ -7,6 +7,19 @@ import ( "context" ) +// LimiterWrapperProvider provides access to LimiterWrappers, which is +// the appropriate interface for callers that can easily wrap a +// function call, because for wrapped calls there is no distinction +// between rate limiters and resource limiters. +type LimiterWrapperProvider interface { + LimiterWrapper(WeightKey, ...Option) (LimiterWrapper, error) +} + +// LimiterWrapperFunc is a functional way to build LimiterWrappers. +type LimiterWrapperFunc func(context.Context, uint64, func(ctx context.Context) error) error + +var _ LimiterWrapper = LimiterWrapperFunc(nil) + // LimiterWrapper is a general-purpose interface for limiter consumers // to limit resources with use of a callback. This is the simplest // form of rate limiting interface from a callers perspective. If the @@ -29,19 +42,11 @@ type LimiterWrapper interface { LimitCall(context.Context, uint64, func(ctx context.Context) error) error } -// LimiterWrapperProvider provides access to LimiterWrappers, which is -// the appropriate interface for callers that can easily wrap a -// function call, because for wrapped calls there is no distinction -// between rate limiters and resource limiters. -type LimiterWrapperProvider interface { - LimiterWrapper(WeightKey) (LimiterWrapper, error) +// PassThrough returns a LimiterWrapper that imposes no limit. +func PassThrough() LimiterWrapper { + return LimiterWrapperFunc(nil) } -// LimiterWrapperFunc is a functional way to build LimiterWrappers. -type LimiterWrapperFunc func(context.Context, uint64, func(ctx context.Context) error) error - -var _ LimiterWrapper = LimiterWrapperFunc(nil) - // MustDeny implements LimiterWrapper. func (f LimiterWrapperFunc) MustDeny(ctx context.Context) error { return f.LimitCall(ctx, 0, func(_ context.Context) error { @@ -57,26 +62,21 @@ func (f LimiterWrapperFunc) LimitCall(ctx context.Context, value uint64, call fu return f(ctx, value, call) } -// PassThrough returns a LimiterWrapper that imposes no limit. -func PassThrough() LimiterWrapper { - return LimiterWrapperFunc(nil) -} - // LimiterWrapperProviderFunc is a functional way to build LimiterWrappers. -type LimiterWrapperProviderFunc func(WeightKey) (LimiterWrapper, error) +type LimiterWrapperProviderFunc func(WeightKey, ...Option) (LimiterWrapper, error) var _ LimiterWrapperProvider = LimiterWrapperProviderFunc(nil) // LimiterWrapper implements LimiterWrapperProvider. -func (f LimiterWrapperProviderFunc) LimiterWrapper(key WeightKey) (LimiterWrapper, error) { - return f(key) +func (f LimiterWrapperProviderFunc) LimiterWrapper(key WeightKey, opts ...Option) (LimiterWrapper, error) { + return f(key, opts...) } // NewResourceLimiterWrapperProvider constructs a // LimiterWrapperProvider for a resource limiter extension. func NewResourceLimiterWrapperProvider(rp ResourceLimiterProvider) LimiterWrapperProvider { - return LimiterWrapperProviderFunc(func(key WeightKey) (LimiterWrapper, error) { - lim, err := rp.ResourceLimiter(key) + return LimiterWrapperProviderFunc(func(key WeightKey, opts ...Option) (LimiterWrapper, error) { + lim, err := rp.ResourceLimiter(key, opts...) if err == nil { return nil, err } @@ -87,8 +87,8 @@ func NewResourceLimiterWrapperProvider(rp ResourceLimiterProvider) LimiterWrappe // NewRateLimiterWrapperProvider constructs a LimiterWrapperProvider // for a rate limiter extension. func NewRateLimiterWrapperProvider(rp RateLimiterProvider) LimiterWrapperProvider { - return LimiterWrapperProviderFunc(func(key WeightKey) (LimiterWrapper, error) { - lim, err := rp.RateLimiter(key) + return LimiterWrapperProviderFunc(func(key WeightKey, opts ...Option) (LimiterWrapper, error) { + lim, err := rp.RateLimiter(key, opts...) if err == nil { return nil, err } From ac0d1ec638d0ac9708d5471252aea28e72f2b066 Mon Sep 17 00:00:00 2001 From: Joshua MacDonald Date: Wed, 30 Apr 2025 15:13:38 -0700 Subject: [PATCH 06/47] lint --- extension/extensionlimiter/README.md | 4 +++- receiver/otlpreceiver/otlp.go | 12 ++++++++---- 2 files changed, 11 insertions(+), 5 deletions(-) diff --git a/extension/extensionlimiter/README.md b/extension/extensionlimiter/README.md index 24d9fdc600d9..1651302d3572 100644 --- a/extension/extensionlimiter/README.md +++ b/extension/extensionlimiter/README.md @@ -273,7 +273,9 @@ func (s *scraper) LogsStream(ctx context.Context, stream *Stream) error { #### Data-dependent limiter processor -**NOTE: This is not implemented.** +**NOTE: This is not implemented.** An `Option` type has been added as +a placeholder in the provider interfaces to support adding this +feature. The provider interfaces can be extended to accept a `map[string]string` that identify limiter instances based on diff --git a/receiver/otlpreceiver/otlp.go b/receiver/otlpreceiver/otlp.go index c91ed2ea39cc..4b2a52e565fb 100644 --- a/receiver/otlpreceiver/otlp.go +++ b/receiver/otlpreceiver/otlp.go @@ -106,7 +106,8 @@ func (r *otlpReceiver) startGRPCServer(host component.Host) error { } if r.nextTraces != nil { - _, next, err := limiterhelper.NewLimitedTraces(r.nextTraces, limitKeys, limiterProvider) + var next consumer.Traces + _, next, err = limiterhelper.NewLimitedTraces(r.nextTraces, limitKeys, limiterProvider) if err != nil { return err } @@ -114,7 +115,8 @@ func (r *otlpReceiver) startGRPCServer(host component.Host) error { } if r.nextMetrics != nil { - _, next, err := limiterhelper.NewLimitedMetrics(r.nextMetrics, limitKeys, limiterProvider) + var next consumer.Metrics + _, next, err = limiterhelper.NewLimitedMetrics(r.nextMetrics, limitKeys, limiterProvider) if err != nil { return err } @@ -122,7 +124,8 @@ func (r *otlpReceiver) startGRPCServer(host component.Host) error { } if r.nextLogs != nil { - _, next, err := limiterhelper.NewLimitedLogs(r.nextLogs, limitKeys, limiterProvider) + var next consumer.Logs + _, next, err = limiterhelper.NewLimitedLogs(r.nextLogs, limitKeys, limiterProvider) if err != nil { return err } @@ -130,7 +133,8 @@ func (r *otlpReceiver) startGRPCServer(host component.Host) error { } if r.nextProfiles != nil { - _, next, err := limiterhelper.NewLimitedProfiles(r.nextProfiles, limitKeys, limiterProvider) + var next xconsumer.Profiles + _, next, err = limiterhelper.NewLimitedProfiles(r.nextProfiles, limitKeys, limiterProvider) if err != nil { return err } From b3e4554c3564779bb2e6ab93fa049db230f58ad8 Mon Sep 17 00:00:00 2001 From: Joshua MacDonald Date: Wed, 30 Apr 2025 15:25:22 -0700 Subject: [PATCH 07/47] lint --- extension/extensionlimiter/README.md | 199 +++++++++++++-------------- 1 file changed, 99 insertions(+), 100 deletions(-) diff --git a/extension/extensionlimiter/README.md b/extension/extensionlimiter/README.md index 1651302d3572..51cd0efecc6e 100644 --- a/extension/extensionlimiter/README.md +++ b/extension/extensionlimiter/README.md @@ -4,7 +4,7 @@ The `extensionlimiter` package provides interfaces for rate limiting and resource limiting in the OpenTelemetry Collector, enabling control -over data flow and resource usage through extensions which can be +over data flow and resource usage through extensions that can be configured through middleware and/or directly by pipeline components. ## Overview @@ -23,11 +23,11 @@ which simplifies consumers in most cases by providing a consistent A limiter is **saturated** by definition when a limit is completely overloaded, generally it means a limit request of any size would fail -at this moment and should be taken as a strong signal to stop +at that moment and should be taken as a strong signal to stop accepting requests. Each kind of limiter as well as the wrapper type have corresponding -**provider** interface that returns a limiter instance based on a +**provider** interfaces that return a limiter instance based on a weight key or keys. Weight keys describe the standard limiting dimensions. There are @@ -42,7 +42,7 @@ request items, and memory size. - `RateLimiter`: Applies time-based limits, has a `Limit` method, plus provider type. - `ResourceLimiter`: Manages physical resource limits, has - an `Acquire` method and corresponding `ReleaseFunc`, + an `Acquire` method and a corresponding `ReleaseFunc`, plus a provider type. - `Limiter`: Any of the above, has a `MustDeny` method. @@ -53,8 +53,8 @@ The `limiterhelper` subpackage provides: - Consumer wrappers apply limits to a collector pipeline (e.g., `NewLimitedLogs` for a limiter combined with `consumer.NewLogs`) - Multi-limiter combinators: for simple combined limiter functionality -- Middleware conversion utilities: convert middleware configurations to - liiter providers. +- Middleware conversion utilities: convert middleware configurations + to limiter providers. ## Recommendations @@ -82,10 +82,10 @@ subject to internal logic. A limiter aims to avoid waste, which requires balancing several factors. To fail a request that has already been transmitted, received and parsed is sometimes more wasteful than waiting for a little while; on the other hand waiting for a long time -risks wasting memory. In general, an overloaded limiter that is saturated SHOULD -fail requests immediately. +risks wasting memory. In general, an overloaded limiter that is +saturated SHOULD fail requests immediately. -Limiters implementations SHOULD consider the context deadline when +Limiter implementations SHOULD consider the context deadline when they block. If the deadline is likely to expire before the limit becomes available, they should return a standard overload signal. @@ -105,7 +105,7 @@ free to define their own saturation parameters. It is sometimes possible to request a limit before it is actually used. As an example, consider a protocol using a compressed payload, -such that the receivers knows how much memory will be allocated before +such that the receiver knows how much memory will be allocated before the fact. In this case the receiver can request the limit before using it, but this will not always be the case. Generally, prefer to limit before use, but either way be consistent. @@ -118,10 +118,10 @@ before creating new concurrent work. #### OTLP receiver Limiters applied through middleware are an implementation detail, -simply configure them using `configgrpc` or `confighttp`. For the +simply configure them using `configgrpc` or `confighttp`. For the OTLP receiver (e.g., with two `ratelimiter` extensions): -``` +```yaml extensions: ratelimiter/limit_for_grpc: # rate limiter settings for gRPC @@ -131,18 +131,17 @@ extensions: receivers: otlp: protocols: - grpc: - middlewares: - - ratelimiter/limit_for_grpc - http: - middlewares: - - ratelimiter/limit_for_http + grpc: + middlewares: + - ratelimiter/limit_for_grpc + http: + middlewares: + - ratelimiter/limit_for_http ``` Note that the OTLP receiver specifically supports multiple protocols with separate middleware configurations, thus it configures limiters -for request items and memory size on a protocol-by-protocol -basis. +for request items and memory size on a protocol-by-protocol basis. #### HTTP metrics scraper @@ -150,12 +149,12 @@ A HTTP pull-based receiver can implement a basic limited scraper loop as follows. The HTTP client config object's `middlewares` field automatically configures network bytes and request count limits: -``` +```yaml receivers: scraper: http: middlewares: - - ratelimiter/scraper + - ratelimiter/scraper ``` Limiter extensions are derived from a host, a middlewares list, and a @@ -163,14 +162,14 @@ list of weight keys. When middleware is configurable at the factory level, it may be added via `receiver.NewFactory` using `receiver.WithLimiters(getLimiters)`: -``` +```golang func NewFactory() receiver.Factory { - return xreceiver.NewFactory( - metadata.Type, - createDefaultConfig, - xreceiver.WithMetrics(createMetrics, metadata.MetricsStability), - xreceiver.WithLimiters(getLimiters), - ) + return xreceiver.NewFactory( + metadata.Type, + createDefaultConfig, + xreceiver.WithMetrics(createMetrics, metadata.MetricsStability), + xreceiver.WithLimiters(getLimiters), + ) } ``` @@ -180,39 +179,39 @@ Here, `getLimiters` is a function to get the effective To acquire a limiter, use `MiddlewaresToLimiterWrapperProvider` to obtain a combined limiter wrapper around the input `nextMetrics` -consumer. It will pass `StandardNotMiddlewareKeys()` indicating to +consumer. It will pass `StandardNotMiddlewareKeys()` indicating to apply request items and memory size: -``` - // Extract limiter provider from middlewares. - s.limiterProvider, err = limiterhelper.MiddlewaresToLimiterWrapperProvider( - cfg.Middlewares) - if err != nil { ... } - - // Here get a limiter-wrapped pipeline and a combination of weight-specific - // limiters for MustDeny() functionality. - s.anyLimiter, s.nextMetrics, err = limiterhelper.NewLimitedMetrics( - s.nextMetrics, limiterhelper.StandardNotMiddlewareKeys(), s.limiterProvider) - if err != nil { ... } +```golang + // Extract limiter provider from middlewares. + s.limiterProvider, err = limiterhelper.MiddlewaresToLimiterWrapperProvider( + cfg.Middlewares) + if err != nil { ... } + + // Here get a limiter-wrapped pipeline and a combination of weight-specific + // limiters for MustDeny() functionality. + s.anyLimiter, s.nextMetrics, err = limiterhelper.NewLimitedMetrics( + s.nextMetrics, limiterhelper.StandardNotMiddlewareKeys(), s.limiterProvider) + if err != nil { ... } ``` In the scraper loop, use `MustDeny` before starting a scrape: -``` +```golang func (s *scraper) scrapeOnce(ctx context.Context) error { if err := s.anyLimiter.MustDeny(ctx); err != nil { - return err - } - - // Network bytes and request count limits are applied in middleware. - // before this returns: - data, err := s.getData(ctx) - if err != nil { - return err - } - - // Request items and memory size are applied in the pipeline. - return s.nextMetrics.ConsumeMetrics(ctx, data) + return err + } + + // Network bytes and request count limits are applied in middleware. + // before this returns: + data, err := s.getData(ctx) + if err != nil { + return err + } + + // Request items and memory size are applied in the pipeline. + return s.nextMetrics.ConsumeMetrics(ctx, data) } ``` @@ -220,16 +219,16 @@ func (s *scraper) scrapeOnce(ctx context.Context) error { A gRPC streaming receiver that holds memory across its allocated in `Send()` and does not release it until after a corresponding `Recv()` -requires use of the lower-level `ResourceLimiter` interface. +requires use of the lower-level `ResourceLimiter` interface. The gRPC config object's `middlewares` field automatically configures network bytes and request count limits: -``` +```yaml receivers: streamer: grpc: middlewares: - - ratelimiter/streamer + - ratelimiter/streamer ``` The receiver will check `s.anyLimiter.MustDeny()` as above. In a @@ -239,43 +238,43 @@ wish to return from `Send()` to continue accepting new requests while the consumer works in a separate goroutine. The limit will be released after the consumer returns. -``` +```golang func (s *scraper) LogsStream(ctx context.Context, stream *Stream) error { for { - // Check saturation for all limiters. - err := s.anyLimiter.MustDeny(ctx) - if err != nil { ... } + // Check saturation for all limiters. + err := s.anyLimiter.MustDeny(ctx) + if err != nil { ... } // The network bytes and request count are applied in middleware. - req, err := stream.Recv() - if err != nil { ... } - - // Allocate memory objects. - data, err := s.getLogs(ctx, req) - if err != nil { ... } - - release, err := s.memorySizeLimiter.Acquire(ctx, pdataSize(data)) - if err != nil { ... } - - go func() { - // Request items limit is applied in the pipeline consumer - err := s.nextMetrics.ConsumeMetrics(ctx, data) - - // Release the memory. - release() - - // Reply to the caller. - stream.Send(streamResponseFromConsumerError(err)) - } - } + req, err := stream.Recv() + if err != nil { ... } + + // Allocate memory objects. + data, err := s.getLogs(ctx, req) + if err != nil { ... } + + release, err := s.memorySizeLimiter.Acquire(ctx, pdataSize(data)) + if err != nil { ... } + + go func() { + // Request items limit is applied in the pipeline consumer + err := s.nextMetrics.ConsumeMetrics(ctx, data) + + // Release the memory. + release() + + // Reply to the caller. + stream.Send(streamResponseFromConsumerError(err)) + } + } } ``` #### Data-dependent limiter processor -**NOTE: This is not implemented.** An `Option` type has been added as -a placeholder in the provider interfaces to support adding this -feature. +An `Option` type has been added as a placeholder in the provider +interfaces to support adding this feature. **NOTE: This is not +implemented.** The provider interfaces can be extended to accept a `map[string]string` that identify limiter instances based on @@ -287,20 +286,20 @@ Limiter implementations would support options, likely assisted by `limiterhelper` features to configure them, for configuring metadata-specific limits. -``` +```golang func handleRequest(ctx context.Context, req *Request) error { - // Get a data-specific limiter: - md := metadataFromRequest(req) - lim, err := s.limiterProvider.LimiterWrapper(weightKey, md) - if err != nil { ... } - - if err = lim.MustDeny(ctx); err != nil { ... } - - // Calculate the data and its weight. - data := dataFromReq(req) - weight := getWeight(data) - - return lim.LimitCall(ctx, weight, func(ctx context.Context) error { - return s.nextLogs.ConsumeLogs(ctx, data) - }) + // Get a data-specific limiter: + md := metadataFromRequest(req) + lim, err := s.limiterProvider.LimiterWrapper(weightKey, md) + if err != nil { ... } + + if err = lim.MustDeny(ctx); err != nil { ... } + + // Calculate the data and its weight. + data := dataFromReq(req) + weight := getWeight(data) + + return lim.LimitCall(ctx, weight, func(ctx context.Context) error { + return s.nextLogs.ConsumeLogs(ctx, data) + }) ``` From b8ed41d5c6b7b7de95d687ff59cd38b47832522a Mon Sep 17 00:00:00 2001 From: Joshua MacDonald Date: Mon, 5 May 2025 16:57:14 -0700 Subject: [PATCH 08/47] wip split Checker (was Limiter) --- .../extensionlimiter/extensionlimiter.go | 29 +++++--- .../limiterhelper/consumer.go | 28 ++++---- .../limiterhelper/middleware.go | 2 +- extension/extensionlimiter/rate.go | 38 ++-------- extension/extensionlimiter/resource.go | 58 ++++----------- extension/extensionlimiter/wrapper.go | 70 ++++++++++++------- 6 files changed, 97 insertions(+), 128 deletions(-) diff --git a/extension/extensionlimiter/extensionlimiter.go b/extension/extensionlimiter/extensionlimiter.go index 7ada56bc0458..6d85503dd67b 100644 --- a/extension/extensionlimiter/extensionlimiter.go +++ b/extension/extensionlimiter/extensionlimiter.go @@ -7,6 +7,11 @@ import ( "context" ) +// Defines: +// - Option +// - Checker +// - CheckerFunc + // Option is passed to limiter providers. // // NOTE: For data-specific or tenant-specific limits we will extend @@ -17,11 +22,10 @@ type Option interface { apply() } -// Limiter is the common functionality implemented by LimiterWrapper, -// RateLimiter, and ResourceLimiter. This can be called prior to the -// start of work to check for limiter saturation. -type Limiter interface { - // Must deny is the logical equivalent of Acquire(0). If the +// Checker is for checking when a limit is saturated. This can be +// called prior to the start of work to check for limiter saturation. +type Checker interface { + // MustDeny is the logical equivalent of Acquire(0). If the // Acquire would fail even for 0 units of a rate, the // caller must deny the request. Implementations are // encouraged to ensure that when MustDeny() is false, @@ -31,15 +35,20 @@ type Limiter interface { MustDeny(context.Context) error } -// LimiterFunc is a functional way to build MustDeny functions. -type LimiterFunc func(context.Context) error +// CheckerFunc is a functional way to build Checker implementations. +type CheckerFunc func(context.Context) error -var _ Limiter = LimiterFunc(nil) +var _ Checker = CheckerFunc(nil) -// MustDeny implements Limiter. -func (f LimiterFunc) MustDeny(ctx context.Context) error { +// MustDeny implements Checker. +func (f CheckerFunc) MustDeny(ctx context.Context) error { if f == nil { return nil } return f(ctx) } + +// PassThroughChecker returns a Checker that never denies. +func PassThroughChecker() Checker { + return CheckerFunc(nil) +} diff --git a/extension/extensionlimiter/limiterhelper/consumer.go b/extension/extensionlimiter/limiterhelper/consumer.go index e56e24233db6..f6810f51e7c1 100644 --- a/extension/extensionlimiter/limiterhelper/consumer.go +++ b/extension/extensionlimiter/limiterhelper/consumer.go @@ -17,13 +17,13 @@ import ( "go.opentelemetry.io/collector/pdata/ptrace" ) -// MultiLimiter returns MustDeny when any element returns MustDeny. -type MultiLimiter []extensionlimiter.Limiter +// MultiChecker returns MustDeny when any element returns MustDeny. +type MultiChecker []extensionlimiter.Checker -var _ extensionlimiter.Limiter = MultiLimiter{} +var _ extensionlimiter.Checker = MultiChecker{} -// MustDeny implements Limiter. -func (ls MultiLimiter) MustDeny(ctx context.Context) error { +// MustDeny implements Checker. +func (ls MultiChecker) MustDeny(ctx context.Context) error { for _, lim := range ls { if lim == nil { continue @@ -143,7 +143,7 @@ func limitOne[P any, C any]( key extensionlimiter.WeightKey, opts []consumer.Option, quantify func(P) uint64, -) (extensionlimiter.Limiter, C, error) { +) (extensionlimiter.Checker, C, error) { if !slices.Contains(keys, key) { return nil, next, nil } @@ -159,7 +159,7 @@ func limitOne[P any, C any]( return m.consume(ctx, data, next) }) }, opts...) - return lim, con, err + return NewLimiterWrapperChecker(lim), con, err } // newLimited is signal-generic limiting logic. @@ -169,11 +169,11 @@ func newLimited[P any, C any]( provider extensionlimiter.LimiterWrapperProvider, m traits[P, C], opts ...consumer.Option, -) (extensionlimiter.Limiter, C, error) { +) (extensionlimiter.Checker, C, error) { if provider == nil { return nil, next, nil } - var lim1, lim2, lim3 extensionlimiter.Limiter + var lim1, lim2, lim3 extensionlimiter.Checker var err1, err2, err3 error // Note: reverse order of evaluation cost => least-cost applied first. lim1, next, err1 = limitOne(next, keys, provider, m, extensionlimiter.WeightKeyMemorySize, opts, @@ -189,29 +189,29 @@ func newLimited[P any, C any]( func(_ P) uint64 { return 1 }) - return MultiLimiter{lim1, lim2, lim3}, next, errors.Join(err1, err2, err3) + return MultiChecker{lim1, lim2, lim3}, next, errors.Join(err1, err2, err3) } // NewLimitedTraces applies a limiter using the provider over keys before calling next. -func NewLimitedTraces(next consumer.Traces, keys []extensionlimiter.WeightKey, provider extensionlimiter.LimiterWrapperProvider) (extensionlimiter.Limiter, consumer.Traces, error) { +func NewLimitedTraces(next consumer.Traces, keys []extensionlimiter.WeightKey, provider extensionlimiter.LimiterWrapperProvider) (extensionlimiter.Checker, consumer.Traces, error) { return newLimited(next, keys, provider, traceTraits{}, consumer.WithCapabilities(next.Capabilities())) } // NewLimitedLogs applies a limiter using the provider over keys before calling next. -func NewLimitedLogs(next consumer.Logs, keys []extensionlimiter.WeightKey, provider extensionlimiter.LimiterWrapperProvider) (extensionlimiter.Limiter, consumer.Logs, error) { +func NewLimitedLogs(next consumer.Logs, keys []extensionlimiter.WeightKey, provider extensionlimiter.LimiterWrapperProvider) (extensionlimiter.Checker, consumer.Logs, error) { return newLimited(next, keys, provider, logTraits{}, consumer.WithCapabilities(next.Capabilities())) } // NewLimitedMetrics applies a limiter using the provider over keys before calling next. -func NewLimitedMetrics(next consumer.Metrics, keys []extensionlimiter.WeightKey, provider extensionlimiter.LimiterWrapperProvider) (extensionlimiter.Limiter, consumer.Metrics, error) { +func NewLimitedMetrics(next consumer.Metrics, keys []extensionlimiter.WeightKey, provider extensionlimiter.LimiterWrapperProvider) (extensionlimiter.Checker, consumer.Metrics, error) { return newLimited(next, keys, provider, metricTraits{}, consumer.WithCapabilities(next.Capabilities())) } // NewLimitedProfiles applies a limiter using the provider over keys before calling next. -func NewLimitedProfiles(next xconsumer.Profiles, keys []extensionlimiter.WeightKey, provider extensionlimiter.LimiterWrapperProvider) (extensionlimiter.Limiter, xconsumer.Profiles, error) { +func NewLimitedProfiles(next xconsumer.Profiles, keys []extensionlimiter.WeightKey, provider extensionlimiter.LimiterWrapperProvider) (extensionlimiter.Checker, xconsumer.Profiles, error) { return newLimited(next, keys, provider, profileTraits{}, consumer.WithCapabilities(next.Capabilities())) } diff --git a/extension/extensionlimiter/limiterhelper/middleware.go b/extension/extensionlimiter/limiterhelper/middleware.go index 533327479d62..b203194cacda 100644 --- a/extension/extensionlimiter/limiterhelper/middleware.go +++ b/extension/extensionlimiter/limiterhelper/middleware.go @@ -110,7 +110,7 @@ var _ extensionlimiter.LimiterWrapperProvider = MultiLimiterWrapperProvider{} // LimiterWrapper implements LimiterWrapperProvider. func (ps MultiLimiterWrapperProvider) LimiterWrapper(key extensionlimiter.WeightKey, opts ...extensionlimiter.Option) (extensionlimiter.LimiterWrapper, error) { if len(ps) == 0 { - return extensionlimiter.PassThrough(), nil + return extensionlimiter.PassThroughWrapper(), nil } // Map provider list to limiter list. diff --git a/extension/extensionlimiter/rate.go b/extension/extensionlimiter/rate.go index a294bb84cb69..a02e9c2c5aab 100644 --- a/extension/extensionlimiter/rate.go +++ b/extension/extensionlimiter/rate.go @@ -7,6 +7,12 @@ import ( "context" ) +// Defines: +// - RateLimiterProvider +// - RateLimiterProviderFunc +// - RateLimiter +// - RateLimiterFunc + // RateLimiterProvider is a provider for rate limiters. // // Limiter implementations will implement this or the @@ -40,9 +46,6 @@ func (f RateLimiterProviderFunc) RateLimiter(key WeightKey, opts ...Option) (Rat // // See the README for more recommendations. type RateLimiter interface { - // Limiter includes MustDeny(). - Limiter - // Limit attempts to apply rate limiting with the provided // weight, based on the key that was given to the provider. // @@ -57,11 +60,6 @@ type RateLimiterFunc func(ctx context.Context, value uint64) error var _ RateLimiter = RateLimiterFunc(nil) -// MustDeny implements RateLimiter. -func (f RateLimiterFunc) MustDeny(ctx context.Context) error { - return f.Limit(ctx, 0) -} - // Limit implements RateLimiter. func (f RateLimiterFunc) Limit(ctx context.Context, value uint64) error { if f == nil { @@ -69,27 +67,3 @@ func (f RateLimiterFunc) Limit(ctx context.Context, value uint64) error { } return f(ctx, value) } - -// NewRateLimiterWrapper returns a LimiterWrapper from a RateLimiter. -func NewRateLimiterWrapper(limiter RateLimiter) LimiterWrapper { - return rateLimiterWrapper{limiter: limiter} -} - -type rateLimiterWrapper struct { - limiter RateLimiter -} - -var _ LimiterWrapper = rateLimiterWrapper{} - -// MustDeny implements LimiterWrapper. -func (w rateLimiterWrapper) MustDeny(ctx context.Context) error { - return w.limiter.MustDeny(ctx) -} - -// LimitCall implements LimiterWrapper. -func (w rateLimiterWrapper) LimitCall(ctx context.Context, value uint64, call func(context.Context) error) error { - if err := w.limiter.Limit(ctx, value); err != nil { - return err - } - return call(ctx) -} diff --git a/extension/extensionlimiter/resource.go b/extension/extensionlimiter/resource.go index 2bd62df41f2a..c919fc55979f 100644 --- a/extension/extensionlimiter/resource.go +++ b/extension/extensionlimiter/resource.go @@ -7,6 +7,13 @@ import ( "context" ) +// Defines: +// - ResourceLimiterProvider +// - ResourceLimiterProviderFunc +// - ResourceLimiter +// - ResourceLimiterFunc +// - ReleaseFunc + // ResourceLimiterProvider is a provider for resource limiters. // // Limiter implementations will implement this or the @@ -41,9 +48,6 @@ func (f ResourceLimiterProviderFunc) ResourceLimiter(key WeightKey, opts ...Opti // // See the README for more recommendations. type ResourceLimiter interface { - // Limiter includes MustDeny(). - Limiter - // Acquire attempts to acquire a quantified resource with the // provided weight, based on the key that was given to the // provider. The caller has these options: @@ -56,18 +60,17 @@ type ResourceLimiter interface { // See the README for more recommendations. // // On success, it returns a ReleaseFunc that should be called - // when the resources are no longer needed. - // - // Implementations are not required to call a release func - // when Acquire(0) is called, because there is nothing to - // release. Acquire(0) the equivalent of MustDeny(). + // after the resources is no longer in use. Acquire(ctx context.Context, value uint64) (ReleaseFunc, error) } -// ReleaseFunc is called when resources should be released after limiting. +// ReleaseFunc is called when resources have been released after use. // // RelaseFunc values are never nil values, even in the error case, for -// safety. Users should unconditionally defer these. +// safety. Users may unconditionally defer these. +// +// Implementations are not required to call a release func after +// Acquire(0) is called, since there is nothing to release. type ReleaseFunc func() // ResourceLimiterFunc is a functional way to construct ResourceLimiters. @@ -75,12 +78,6 @@ type ResourceLimiterFunc func(ctx context.Context, value uint64) (ReleaseFunc, e var _ ResourceLimiter = ResourceLimiterFunc(nil) -// MustDeny implements ResourceLimiter -func (f ResourceLimiterFunc) MustDeny(ctx context.Context) error { - _, err := f.Acquire(ctx, 0) - return err -} - // Acquire implements ResourceLimiter func (f ResourceLimiterFunc) Acquire(ctx context.Context, value uint64) (ReleaseFunc, error) { if f == nil { @@ -88,32 +85,3 @@ func (f ResourceLimiterFunc) Acquire(ctx context.Context, value uint64) (Release } return f(ctx, value) } - -// NewResourceLimiterWrapper returns a LimiterWrapper from a ResourceLimiter. -func NewResourceLimiterWrapper(limiter ResourceLimiter) LimiterWrapper { - return resourceLimiterWrapper{limiter: limiter} -} - -type resourceLimiterWrapper struct { - limiter ResourceLimiter -} - -var _ LimiterWrapper = resourceLimiterWrapper{} - -// MustDeny implements LimiterWrapper. -func (w resourceLimiterWrapper) MustDeny(ctx context.Context) error { - if w.limiter == nil { - return nil - } - return w.limiter.MustDeny(ctx) -} - -// LimitCall implements LimiterWrapper. -func (w resourceLimiterWrapper) LimitCall(ctx context.Context, value uint64, call func(context.Context) error) error { - release, err := w.limiter.Acquire(ctx, value) - if err != nil { - return err - } - defer release() - return call(ctx) -} diff --git a/extension/extensionlimiter/wrapper.go b/extension/extensionlimiter/wrapper.go index cd521f4487a0..0e4f69206f88 100644 --- a/extension/extensionlimiter/wrapper.go +++ b/extension/extensionlimiter/wrapper.go @@ -15,44 +15,32 @@ type LimiterWrapperProvider interface { LimiterWrapper(WeightKey, ...Option) (LimiterWrapper, error) } -// LimiterWrapperFunc is a functional way to build LimiterWrappers. -type LimiterWrapperFunc func(context.Context, uint64, func(ctx context.Context) error) error - -var _ LimiterWrapper = LimiterWrapperFunc(nil) - // LimiterWrapper is a general-purpose interface for limiter consumers // to limit resources with use of a callback. This is the simplest // form of rate limiting interface from a callers perspective. If the // caller is a pipeline component, consider using a consumer-oriented -// limiterhelper (e.g., limiterhelper.NewLimitedLogs) to apply a list of +// limiterhelper (e.g., limiterhelper.NewLimitedLogs) to simplify +// construction of this interface. // -// Limiter implementions are meant to implement either the RateLimiter -// or ResourceLimiter interfaces. LimiterWrappers can be constructed -// from either of the underlying limiters and their corresponding -// providers. Usually configmiddleware or limiterhelper is responsible -// for constructing the correct wrapper from these two kinds of limiter; -// users will use this interface consistently. +// A wrapped limiter is either a RateLimiter or ResourceLimiter +// interface. LimiterWrappers can be constructed from either of the +// underlying limiters and their corresponding providers. Usually +// configmiddleware or limiterhelper is responsible for constructing +// the correct wrapper from these two kinds of limiter; users will use +// this interface consistently. type LimiterWrapper interface { - // Limiter includes MustDeny(). - Limiter - // LimitCall applies the limiter and with the rate or resource // granted makes a scoped call, returning success or an error // from either the limiter or the enclosed callback. - LimitCall(context.Context, uint64, func(ctx context.Context) error) error + // + // The `call` parameter must be non-nil. + LimitCall(ctx context.Context, weight uint64, call func(ctx context.Context) error) error } -// PassThrough returns a LimiterWrapper that imposes no limit. -func PassThrough() LimiterWrapper { - return LimiterWrapperFunc(nil) -} +// LimiterWrapperFunc is a functional way to build LimiterWrappers. +type LimiterWrapperFunc func(context.Context, uint64, func(ctx context.Context) error) error -// MustDeny implements LimiterWrapper. -func (f LimiterWrapperFunc) MustDeny(ctx context.Context) error { - return f.LimitCall(ctx, 0, func(_ context.Context) error { - return nil - }) -} +var _ LimiterWrapper = LimiterWrapperFunc(nil) // LimitCall implements LimiterWrapper. func (f LimiterWrapperFunc) LimitCall(ctx context.Context, value uint64, call func(ctx context.Context) error) error { @@ -62,6 +50,11 @@ func (f LimiterWrapperFunc) LimitCall(ctx context.Context, value uint64, call fu return f(ctx, value, call) } +// PassThroughWrapper returns a LimiterWrapper that imposes no limit. +func PassThroughWrapper() LimiterWrapper { + return LimiterWrapperFunc(nil) +} + // LimiterWrapperProviderFunc is a functional way to build LimiterWrappers. type LimiterWrapperProviderFunc func(WeightKey, ...Option) (LimiterWrapper, error) @@ -69,6 +62,9 @@ var _ LimiterWrapperProvider = LimiterWrapperProviderFunc(nil) // LimiterWrapper implements LimiterWrapperProvider. func (f LimiterWrapperProviderFunc) LimiterWrapper(key WeightKey, opts ...Option) (LimiterWrapper, error) { + if f == nil { + return PassThroughWrapper(), nil + } return f(key, opts...) } @@ -95,3 +91,25 @@ func NewRateLimiterWrapperProvider(rp RateLimiterProvider) LimiterWrapperProvide return NewRateLimiterWrapper(lim), err }) } + +// NewRateLimiterWrapper returns a LimiterWrapper from a RateLimiter. +func NewRateLimiterWrapper(limiter RateLimiter) LimiterWrapper { + return LimiterWrapperFunc(func(ctx context.Context, value uint64, call func(context.Context) error) error { + if err := limiter.Limit(ctx, value); err != nil { + return err + } + return call(ctx) + }) +} + +// NewResourceLimiterWrapper returns a LimiterWrapper from a ResourceLimiter. +func NewResourceLimiterWrapper(limiter ResourceLimiter) LimiterWrapper { + return LimiterWrapperFunc(func(ctx context.Context, value uint64, call func(context.Context) error) error { + release, err := limiter.Acquire(ctx, value) + if err != nil { + return err + } + defer release() + return call(ctx) + }) +} From d07da61ea168e07096c2aa3ca2d4644f559c5cdd Mon Sep 17 00:00:00 2001 From: Joshua MacDonald Date: Mon, 5 May 2025 16:59:21 -0700 Subject: [PATCH 09/47] rename --- extension/extensionlimiter/{ => limiterhelper}/wrapper.go | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename extension/extensionlimiter/{ => limiterhelper}/wrapper.go (100%) diff --git a/extension/extensionlimiter/wrapper.go b/extension/extensionlimiter/limiterhelper/wrapper.go similarity index 100% rename from extension/extensionlimiter/wrapper.go rename to extension/extensionlimiter/limiterhelper/wrapper.go From f3a2f2f213be1060f10f29b771ee833b9b6ec574 Mon Sep 17 00:00:00 2001 From: Joshua MacDonald Date: Mon, 5 May 2025 17:09:25 -0700 Subject: [PATCH 10/47] move wrapper into limiterhelper --- .../extensionlimiter/extensionlimiter.go | 5 -- .../extensionlimiter/limiterhelper/checker.go | 50 +++++++++++++++++++ .../limiterhelper/consumer.go | 30 +++-------- .../limiterhelper/middleware.go | 26 +++++----- .../extensionlimiter/limiterhelper/wrapper.go | 44 ++++++++-------- extension/extensionlimiter/rate.go | 6 --- extension/extensionlimiter/resource.go | 7 --- 7 files changed, 92 insertions(+), 76 deletions(-) create mode 100644 extension/extensionlimiter/limiterhelper/checker.go diff --git a/extension/extensionlimiter/extensionlimiter.go b/extension/extensionlimiter/extensionlimiter.go index 6d85503dd67b..e2faec9e03ef 100644 --- a/extension/extensionlimiter/extensionlimiter.go +++ b/extension/extensionlimiter/extensionlimiter.go @@ -7,11 +7,6 @@ import ( "context" ) -// Defines: -// - Option -// - Checker -// - CheckerFunc - // Option is passed to limiter providers. // // NOTE: For data-specific or tenant-specific limits we will extend diff --git a/extension/extensionlimiter/limiterhelper/checker.go b/extension/extensionlimiter/limiterhelper/checker.go new file mode 100644 index 000000000000..bc3fb8c6f49c --- /dev/null +++ b/extension/extensionlimiter/limiterhelper/checker.go @@ -0,0 +1,50 @@ +// Copyright The OpenTelemetry Authors +// SPDX-License-Identifier: Apache-2.0 + +package limiterhelper // import "go.opentelemetry.io/collector/extension/extensionlimiter/limiterhelper" + +import ( + "context" + + "go.opentelemetry.io/collector/extension/extensionlimiter" +) + +// MultiChecker returns MustDeny when any element returns MustDeny. +type MultiChecker []extensionlimiter.Checker + +var _ extensionlimiter.Checker = MultiChecker{} + +// MustDeny implements Checker. +func (ls MultiChecker) MustDeny(ctx context.Context) error { + for _, lim := range ls { + if lim == nil { + continue + } + if err := lim.MustDeny(ctx); err != nil { + return err + } + } + return nil +} + +// NewLimiterWrapperChecker returns a Checker for a LimiterWrapper. +func NewLimiterWrapperChecker(limiter LimiterWrapper) extensionlimiter.Checker { + return extensionlimiter.CheckerFunc(func(ctx context.Context) error { + return limiter.LimitCall(ctx, 0, func(_ context.Context) error { return nil }) + }) +} + +// NewRateLimiterChecker returns a Checker for a RateLimiter. +func NewRateLimiterChecker(limiter extensionlimiter.RateLimiter) extensionlimiter.Checker { + return extensionlimiter.CheckerFunc(func(ctx context.Context) error { + return limiter.Limit(ctx, 0) + }) +} + +// NewResourceLimiterChecker returns a Checker for ResourceLimiter. +func NewResourceLimiterChecker(limiter extensionlimiter.ResourceLimiter) extensionlimiter.Checker { + return extensionlimiter.CheckerFunc(func(ctx context.Context) error { + _, err := limiter.Acquire(ctx, 0) + return err + }) +} diff --git a/extension/extensionlimiter/limiterhelper/consumer.go b/extension/extensionlimiter/limiterhelper/consumer.go index f6810f51e7c1..18d05346f650 100644 --- a/extension/extensionlimiter/limiterhelper/consumer.go +++ b/extension/extensionlimiter/limiterhelper/consumer.go @@ -17,24 +17,6 @@ import ( "go.opentelemetry.io/collector/pdata/ptrace" ) -// MultiChecker returns MustDeny when any element returns MustDeny. -type MultiChecker []extensionlimiter.Checker - -var _ extensionlimiter.Checker = MultiChecker{} - -// MustDeny implements Checker. -func (ls MultiChecker) MustDeny(ctx context.Context) error { - for _, lim := range ls { - if lim == nil { - continue - } - if err := lim.MustDeny(ctx); err != nil { - return err - } - } - return nil -} - // Traits object interface is generalized by P the pipeline data type // (e.g., ptrace.Traces) and C the consumer type (e.g., // consumer.Traces) @@ -138,7 +120,7 @@ func (profileTraits) consume(ctx context.Context, data pprofile.Profiles, next x func limitOne[P any, C any]( next C, keys []extensionlimiter.WeightKey, - provider extensionlimiter.LimiterWrapperProvider, + provider LimiterWrapperProvider, m traits[P, C], key extensionlimiter.WeightKey, opts []consumer.Option, @@ -166,7 +148,7 @@ func limitOne[P any, C any]( func newLimited[P any, C any]( next C, keys []extensionlimiter.WeightKey, - provider extensionlimiter.LimiterWrapperProvider, + provider LimiterWrapperProvider, m traits[P, C], opts ...consumer.Option, ) (extensionlimiter.Checker, C, error) { @@ -193,25 +175,25 @@ func newLimited[P any, C any]( } // NewLimitedTraces applies a limiter using the provider over keys before calling next. -func NewLimitedTraces(next consumer.Traces, keys []extensionlimiter.WeightKey, provider extensionlimiter.LimiterWrapperProvider) (extensionlimiter.Checker, consumer.Traces, error) { +func NewLimitedTraces(next consumer.Traces, keys []extensionlimiter.WeightKey, provider LimiterWrapperProvider) (extensionlimiter.Checker, consumer.Traces, error) { return newLimited(next, keys, provider, traceTraits{}, consumer.WithCapabilities(next.Capabilities())) } // NewLimitedLogs applies a limiter using the provider over keys before calling next. -func NewLimitedLogs(next consumer.Logs, keys []extensionlimiter.WeightKey, provider extensionlimiter.LimiterWrapperProvider) (extensionlimiter.Checker, consumer.Logs, error) { +func NewLimitedLogs(next consumer.Logs, keys []extensionlimiter.WeightKey, provider LimiterWrapperProvider) (extensionlimiter.Checker, consumer.Logs, error) { return newLimited(next, keys, provider, logTraits{}, consumer.WithCapabilities(next.Capabilities())) } // NewLimitedMetrics applies a limiter using the provider over keys before calling next. -func NewLimitedMetrics(next consumer.Metrics, keys []extensionlimiter.WeightKey, provider extensionlimiter.LimiterWrapperProvider) (extensionlimiter.Checker, consumer.Metrics, error) { +func NewLimitedMetrics(next consumer.Metrics, keys []extensionlimiter.WeightKey, provider LimiterWrapperProvider) (extensionlimiter.Checker, consumer.Metrics, error) { return newLimited(next, keys, provider, metricTraits{}, consumer.WithCapabilities(next.Capabilities())) } // NewLimitedProfiles applies a limiter using the provider over keys before calling next. -func NewLimitedProfiles(next xconsumer.Profiles, keys []extensionlimiter.WeightKey, provider extensionlimiter.LimiterWrapperProvider) (extensionlimiter.Checker, xconsumer.Profiles, error) { +func NewLimitedProfiles(next xconsumer.Profiles, keys []extensionlimiter.WeightKey, provider LimiterWrapperProvider) (extensionlimiter.Checker, xconsumer.Profiles, error) { return newLimited(next, keys, provider, profileTraits{}, consumer.WithCapabilities(next.Capabilities())) } diff --git a/extension/extensionlimiter/limiterhelper/middleware.go b/extension/extensionlimiter/limiterhelper/middleware.go index b203194cacda..95570d9b7c4e 100644 --- a/extension/extensionlimiter/limiterhelper/middleware.go +++ b/extension/extensionlimiter/limiterhelper/middleware.go @@ -36,9 +36,9 @@ func MiddlewareIsLimiter(host component.Host, middleware configmiddleware.Config // When no limiters are found (with no errors), the returned provider // is nil. When a nil is passed to the consumer helpers (e.g., // NewLimitedLogs) it will pass-through when the limiter is nil. -func MiddlewaresToLimiterWrapperProvider(host component.Host, middleware []configmiddleware.Config) (extensionlimiter.LimiterWrapperProvider, error) { +func MiddlewaresToLimiterWrapperProvider(host component.Host, middleware []configmiddleware.Config) (LimiterWrapperProvider, error) { var retErr error - var providers []extensionlimiter.LimiterWrapperProvider + var providers []LimiterWrapperProvider for _, mid := range middleware { ok, err := MiddlewareIsLimiter(host, mid) retErr = errors.Join(retErr, err) @@ -63,17 +63,17 @@ func MiddlewaresToLimiterWrapperProvider(host component.Host, middleware []confi // provider from middleware. Returns a package-level error if the // middleware does not implement exactly one of the limiter // interfaces (i.e., rate or resource). -func MiddlewareToLimiterWrapperProvider(host component.Host, middleware configmiddleware.Config) (extensionlimiter.LimiterWrapperProvider, error) { +func MiddlewareToLimiterWrapperProvider(host component.Host, middleware configmiddleware.Config) (LimiterWrapperProvider, error) { ext, ok, err := middlewareIsLimiter(host, middleware) if err != nil { return nil, err } if ok { if lim, ok := ext.(extensionlimiter.ResourceLimiterProvider); ok { - return extensionlimiter.NewResourceLimiterWrapperProvider(lim), nil + return NewResourceLimiterWrapperProvider(lim), nil } if lim, ok := ext.(extensionlimiter.RateLimiterProvider); ok { - return extensionlimiter.NewRateLimiterWrapperProvider(lim), nil + return NewRateLimiterWrapperProvider(lim), nil } } return nil, fmt.Errorf("%w: %s", ErrNotALimiter, ext) @@ -103,18 +103,18 @@ func middlewareIsLimiter(host component.Host, middleware configmiddleware.Config // MultiLimiterWrapperProvider combines multiple limiter wrappers // providers into a single provider by sequencing wrapped limiters. // Returns errors from the underlying LimiterWrapper() calls, if any. -type MultiLimiterWrapperProvider []extensionlimiter.LimiterWrapperProvider +type MultiLimiterWrapperProvider []LimiterWrapperProvider -var _ extensionlimiter.LimiterWrapperProvider = MultiLimiterWrapperProvider{} +var _ LimiterWrapperProvider = MultiLimiterWrapperProvider{} // LimiterWrapper implements LimiterWrapperProvider. -func (ps MultiLimiterWrapperProvider) LimiterWrapper(key extensionlimiter.WeightKey, opts ...extensionlimiter.Option) (extensionlimiter.LimiterWrapper, error) { +func (ps MultiLimiterWrapperProvider) LimiterWrapper(key extensionlimiter.WeightKey, opts ...extensionlimiter.Option) (LimiterWrapper, error) { if len(ps) == 0 { - return extensionlimiter.PassThroughWrapper(), nil + return PassThroughWrapper(), nil } // Map provider list to limiter list. - var lims []extensionlimiter.LimiterWrapper + var lims []LimiterWrapper for _, provider := range ps { lim, err := provider.LimiterWrapper(key, opts...) @@ -128,15 +128,15 @@ func (ps MultiLimiterWrapperProvider) LimiterWrapper(key extensionlimiter.Weight return sequenceLimiters(lims), nil } -func sequenceLimiters(lims []extensionlimiter.LimiterWrapper) extensionlimiter.LimiterWrapper { +func sequenceLimiters(lims []LimiterWrapper) LimiterWrapper { if len(lims) == 1 { return lims[0] } return composeLimiters(lims[0], sequenceLimiters(lims[1:])) } -func composeLimiters(first, second extensionlimiter.LimiterWrapper) extensionlimiter.LimiterWrapper { - return extensionlimiter.LimiterWrapperFunc(func(ctx context.Context, value uint64, call func(ctx context.Context) error) error { +func composeLimiters(first, second LimiterWrapper) LimiterWrapper { + return LimiterWrapperFunc(func(ctx context.Context, value uint64, call func(ctx context.Context) error) error { return first.LimitCall(ctx, value, func(ctx context.Context) error { return second.LimitCall(ctx, value, call) }) diff --git a/extension/extensionlimiter/limiterhelper/wrapper.go b/extension/extensionlimiter/limiterhelper/wrapper.go index 0e4f69206f88..26ec8ab94ac9 100644 --- a/extension/extensionlimiter/limiterhelper/wrapper.go +++ b/extension/extensionlimiter/limiterhelper/wrapper.go @@ -1,10 +1,12 @@ // Copyright The OpenTelemetry Authors // SPDX-License-Identifier: Apache-2.0 -package extensionlimiter // import "go.opentelemetry.io/collector/extension/extensionlimiter" +package limiterhelper // import "go.opentelemetry.io/collector/extension/extensionlimiter/limiterhelper" import ( "context" + + "go.opentelemetry.io/collector/extension/extensionlimiter" ) // LimiterWrapperProvider provides access to LimiterWrappers, which is @@ -12,7 +14,20 @@ import ( // function call, because for wrapped calls there is no distinction // between rate limiters and resource limiters. type LimiterWrapperProvider interface { - LimiterWrapper(WeightKey, ...Option) (LimiterWrapper, error) + LimiterWrapper(extensionlimiter.WeightKey, ...extensionlimiter.Option) (LimiterWrapper, error) +} + +// LimiterWrapperProviderFunc is a functional way to build LimiterWrappers. +type LimiterWrapperProviderFunc func(extensionlimiter.WeightKey, ...extensionlimiter.Option) (LimiterWrapper, error) + +var _ LimiterWrapperProvider = LimiterWrapperProviderFunc(nil) + +// LimiterWrapper implements LimiterWrapperProvider. +func (f LimiterWrapperProviderFunc) LimiterWrapper(key extensionlimiter.WeightKey, opts ...extensionlimiter.Option) (LimiterWrapper, error) { + if f == nil { + return PassThroughWrapper(), nil + } + return f(key, opts...) } // LimiterWrapper is a general-purpose interface for limiter consumers @@ -55,23 +70,10 @@ func PassThroughWrapper() LimiterWrapper { return LimiterWrapperFunc(nil) } -// LimiterWrapperProviderFunc is a functional way to build LimiterWrappers. -type LimiterWrapperProviderFunc func(WeightKey, ...Option) (LimiterWrapper, error) - -var _ LimiterWrapperProvider = LimiterWrapperProviderFunc(nil) - -// LimiterWrapper implements LimiterWrapperProvider. -func (f LimiterWrapperProviderFunc) LimiterWrapper(key WeightKey, opts ...Option) (LimiterWrapper, error) { - if f == nil { - return PassThroughWrapper(), nil - } - return f(key, opts...) -} - // NewResourceLimiterWrapperProvider constructs a // LimiterWrapperProvider for a resource limiter extension. -func NewResourceLimiterWrapperProvider(rp ResourceLimiterProvider) LimiterWrapperProvider { - return LimiterWrapperProviderFunc(func(key WeightKey, opts ...Option) (LimiterWrapper, error) { +func NewResourceLimiterWrapperProvider(rp extensionlimiter.ResourceLimiterProvider) LimiterWrapperProvider { + return LimiterWrapperProviderFunc(func(key extensionlimiter.WeightKey, opts ...extensionlimiter.Option) (LimiterWrapper, error) { lim, err := rp.ResourceLimiter(key, opts...) if err == nil { return nil, err @@ -82,8 +84,8 @@ func NewResourceLimiterWrapperProvider(rp ResourceLimiterProvider) LimiterWrappe // NewRateLimiterWrapperProvider constructs a LimiterWrapperProvider // for a rate limiter extension. -func NewRateLimiterWrapperProvider(rp RateLimiterProvider) LimiterWrapperProvider { - return LimiterWrapperProviderFunc(func(key WeightKey, opts ...Option) (LimiterWrapper, error) { +func NewRateLimiterWrapperProvider(rp extensionlimiter.RateLimiterProvider) LimiterWrapperProvider { + return LimiterWrapperProviderFunc(func(key extensionlimiter.WeightKey, opts ...extensionlimiter.Option) (LimiterWrapper, error) { lim, err := rp.RateLimiter(key, opts...) if err == nil { return nil, err @@ -93,7 +95,7 @@ func NewRateLimiterWrapperProvider(rp RateLimiterProvider) LimiterWrapperProvide } // NewRateLimiterWrapper returns a LimiterWrapper from a RateLimiter. -func NewRateLimiterWrapper(limiter RateLimiter) LimiterWrapper { +func NewRateLimiterWrapper(limiter extensionlimiter.RateLimiter) LimiterWrapper { return LimiterWrapperFunc(func(ctx context.Context, value uint64, call func(context.Context) error) error { if err := limiter.Limit(ctx, value); err != nil { return err @@ -103,7 +105,7 @@ func NewRateLimiterWrapper(limiter RateLimiter) LimiterWrapper { } // NewResourceLimiterWrapper returns a LimiterWrapper from a ResourceLimiter. -func NewResourceLimiterWrapper(limiter ResourceLimiter) LimiterWrapper { +func NewResourceLimiterWrapper(limiter extensionlimiter.ResourceLimiter) LimiterWrapper { return LimiterWrapperFunc(func(ctx context.Context, value uint64, call func(context.Context) error) error { release, err := limiter.Acquire(ctx, value) if err != nil { diff --git a/extension/extensionlimiter/rate.go b/extension/extensionlimiter/rate.go index a02e9c2c5aab..f6c0cd49079e 100644 --- a/extension/extensionlimiter/rate.go +++ b/extension/extensionlimiter/rate.go @@ -7,12 +7,6 @@ import ( "context" ) -// Defines: -// - RateLimiterProvider -// - RateLimiterProviderFunc -// - RateLimiter -// - RateLimiterFunc - // RateLimiterProvider is a provider for rate limiters. // // Limiter implementations will implement this or the diff --git a/extension/extensionlimiter/resource.go b/extension/extensionlimiter/resource.go index c919fc55979f..e23e38a49caa 100644 --- a/extension/extensionlimiter/resource.go +++ b/extension/extensionlimiter/resource.go @@ -7,13 +7,6 @@ import ( "context" ) -// Defines: -// - ResourceLimiterProvider -// - ResourceLimiterProviderFunc -// - ResourceLimiter -// - ResourceLimiterFunc -// - ReleaseFunc - // ResourceLimiterProvider is a provider for resource limiters. // // Limiter implementations will implement this or the From 8b31cd61d1bf925e7196f5509257503ebfb76f6f Mon Sep 17 00:00:00 2001 From: Joshua MacDonald Date: Tue, 6 May 2025 08:46:41 -0700 Subject: [PATCH 11/47] style --- .../extensionlimiter/extensionlimiter.go | 25 ++++++-------- .../extensionlimiter/limiterhelper/checker.go | 6 ++-- .../extensionlimiter/limiterhelper/wrapper.go | 4 +-- extension/extensionlimiter/rate.go | 34 ++++++++++++------- extension/extensionlimiter/resource.go | 34 ++++++++++++------- 5 files changed, 60 insertions(+), 43 deletions(-) diff --git a/extension/extensionlimiter/extensionlimiter.go b/extension/extensionlimiter/extensionlimiter.go index e2faec9e03ef..580d16238481 100644 --- a/extension/extensionlimiter/extensionlimiter.go +++ b/extension/extensionlimiter/extensionlimiter.go @@ -20,30 +20,27 @@ type Option interface { // Checker is for checking when a limit is saturated. This can be // called prior to the start of work to check for limiter saturation. type Checker interface { - // MustDeny is the logical equivalent of Acquire(0). If the - // Acquire would fail even for 0 units of a rate, the - // caller must deny the request. Implementations are - // encouraged to ensure that when MustDeny() is false, - // Acquire(0) is also false, however callers could use a - // faster code path to implement MustDeny() since it does not - // depend on the value. + // MustDeny is a request to apply a hard limit. If this + // returns non-nil, the caller must not begin new work in this + // context. MustDeny(context.Context) error } -// CheckerFunc is a functional way to build Checker implementations. -type CheckerFunc func(context.Context) error +// MustDenyFunc is a functional way to build MustDeny functions. +type MustDenyFunc func(context.Context) error -var _ Checker = CheckerFunc(nil) +// A MustDeny function is a complete Checker. +var _ Checker = MustDenyFunc(nil) // MustDeny implements Checker. -func (f CheckerFunc) MustDeny(ctx context.Context) error { +func (f MustDenyFunc) MustDeny(ctx context.Context) error { if f == nil { return nil } return f(ctx) } -// PassThroughChecker returns a Checker that never denies. -func PassThroughChecker() Checker { - return CheckerFunc(nil) +// NeverDeny returns a Checker that never denies. +func NeverDeny() Checker { + return MustDenyFunc(nil) } diff --git a/extension/extensionlimiter/limiterhelper/checker.go b/extension/extensionlimiter/limiterhelper/checker.go index bc3fb8c6f49c..e5f92ca2e4bf 100644 --- a/extension/extensionlimiter/limiterhelper/checker.go +++ b/extension/extensionlimiter/limiterhelper/checker.go @@ -29,21 +29,21 @@ func (ls MultiChecker) MustDeny(ctx context.Context) error { // NewLimiterWrapperChecker returns a Checker for a LimiterWrapper. func NewLimiterWrapperChecker(limiter LimiterWrapper) extensionlimiter.Checker { - return extensionlimiter.CheckerFunc(func(ctx context.Context) error { + return extensionlimiter.MustDenyFunc(func(ctx context.Context) error { return limiter.LimitCall(ctx, 0, func(_ context.Context) error { return nil }) }) } // NewRateLimiterChecker returns a Checker for a RateLimiter. func NewRateLimiterChecker(limiter extensionlimiter.RateLimiter) extensionlimiter.Checker { - return extensionlimiter.CheckerFunc(func(ctx context.Context) error { + return extensionlimiter.MustDenyFunc(func(ctx context.Context) error { return limiter.Limit(ctx, 0) }) } // NewResourceLimiterChecker returns a Checker for ResourceLimiter. func NewResourceLimiterChecker(limiter extensionlimiter.ResourceLimiter) extensionlimiter.Checker { - return extensionlimiter.CheckerFunc(func(ctx context.Context) error { + return extensionlimiter.MustDenyFunc(func(ctx context.Context) error { _, err := limiter.Acquire(ctx, 0) return err }) diff --git a/extension/extensionlimiter/limiterhelper/wrapper.go b/extension/extensionlimiter/limiterhelper/wrapper.go index 26ec8ab94ac9..35c7b213c5bb 100644 --- a/extension/extensionlimiter/limiterhelper/wrapper.go +++ b/extension/extensionlimiter/limiterhelper/wrapper.go @@ -74,7 +74,7 @@ func PassThroughWrapper() LimiterWrapper { // LimiterWrapperProvider for a resource limiter extension. func NewResourceLimiterWrapperProvider(rp extensionlimiter.ResourceLimiterProvider) LimiterWrapperProvider { return LimiterWrapperProviderFunc(func(key extensionlimiter.WeightKey, opts ...extensionlimiter.Option) (LimiterWrapper, error) { - lim, err := rp.ResourceLimiter(key, opts...) + lim, err := rp.GetResourceLimiter(key, opts...) if err == nil { return nil, err } @@ -86,7 +86,7 @@ func NewResourceLimiterWrapperProvider(rp extensionlimiter.ResourceLimiterProvid // for a rate limiter extension. func NewRateLimiterWrapperProvider(rp extensionlimiter.RateLimiterProvider) LimiterWrapperProvider { return LimiterWrapperProviderFunc(func(key extensionlimiter.WeightKey, opts ...extensionlimiter.Option) (LimiterWrapper, error) { - lim, err := rp.RateLimiter(key, opts...) + lim, err := rp.GetRateLimiter(key, opts...) if err == nil { return nil, err } diff --git a/extension/extensionlimiter/rate.go b/extension/extensionlimiter/rate.go index f6c0cd49079e..b8748281da64 100644 --- a/extension/extensionlimiter/rate.go +++ b/extension/extensionlimiter/rate.go @@ -14,17 +14,21 @@ import ( // Limiters are covered by configmiddleware configuration, which is // able to construct LimiterWrappers from these providers. type RateLimiterProvider interface { - // RateLimiter returns a provider for rate limiters. - RateLimiter(WeightKey, ...Option) (RateLimiter, error) + // GetRateLimiter returns a provider for rate limiters. + GetRateLimiter(WeightKey, ...Option) (RateLimiter, error) } -// RateLimiterProviderFunc is a functional way to build RateLimters. -type RateLimiterProviderFunc func(WeightKey, ...Option) (RateLimiter, error) +// RateLimiterFunc is a functional way to construct GetRateLimiter +// functions. +type GetRateLimiterFunc func(WeightKey, ...Option) (RateLimiter, error) -var _ RateLimiterProvider = RateLimiterProviderFunc(nil) +var _ RateLimiterProvider = GetRateLimiterFunc(nil) // RateLimiter implements RateLimiterProvider. -func (f RateLimiterProviderFunc) RateLimiter(key WeightKey, opts ...Option) (RateLimiter, error) { +func (f GetRateLimiterFunc) GetRateLimiter(key WeightKey, opts ...Option) (RateLimiter, error) { + if f == nil { + return nil, nil + } return f(key, opts...) } @@ -40,6 +44,8 @@ func (f RateLimiterProviderFunc) RateLimiter(key WeightKey, opts ...Option) (Rat // // See the README for more recommendations. type RateLimiter interface { + Checker + // Limit attempts to apply rate limiting with the provided // weight, based on the key that was given to the provider. // @@ -49,15 +55,19 @@ type RateLimiter interface { Limit(ctx context.Context, value uint64) error } -// RateLimiterFunc is an easy way to construct RateLimiters. -type RateLimiterFunc func(ctx context.Context, value uint64) error +// LimitFunc is a functional way to construct Limit functions. +type LimitFunc func(ctx context.Context, value uint64) error -var _ RateLimiter = RateLimiterFunc(nil) - -// Limit implements RateLimiter. -func (f RateLimiterFunc) Limit(ctx context.Context, value uint64) error { +// Limit implements part of the RateLimiter interface. +func (f LimitFunc) Limit(ctx context.Context, value uint64) error { if f == nil { return nil } return f(ctx, value) } + +// Verify that a rate limiter is constructed of two functions. +var _ RateLimiter = struct { + MustDenyFunc + LimitFunc +}{} diff --git a/extension/extensionlimiter/resource.go b/extension/extensionlimiter/resource.go index e23e38a49caa..2baf417243dc 100644 --- a/extension/extensionlimiter/resource.go +++ b/extension/extensionlimiter/resource.go @@ -14,16 +14,20 @@ import ( // Limiters are covered by configmiddleware configuration, which // is able to construct LimiterWrappers from these providers. type ResourceLimiterProvider interface { - ResourceLimiter(WeightKey, ...Option) (ResourceLimiter, error) + GetResourceLimiter(WeightKey, ...Option) (ResourceLimiter, error) } -// ResourceLimiterProviderFunc is a functional way to build ResourceLimters. -type ResourceLimiterProviderFunc func(WeightKey, ...Option) (ResourceLimiter, error) +// GetResourceLimiterFunc is a functional way to construct +// GetResourceLimiter functions. +type GetResourceLimiterFunc func(WeightKey, ...Option) (ResourceLimiter, error) -var _ ResourceLimiterProvider = ResourceLimiterProviderFunc(nil) +var _ ResourceLimiterProvider = GetResourceLimiterFunc(nil) -// ResourceLimiter implements ResourceLimiterProvider. -func (f ResourceLimiterProviderFunc) ResourceLimiter(key WeightKey, opts ...Option) (ResourceLimiter, error) { +// GetResourceLimiter implements part of ResourceLimiterProvider. +func (f GetResourceLimiterFunc) GetResourceLimiter(key WeightKey, opts ...Option) (ResourceLimiter, error) { + if f == nil { + return nil, nil + } return f(key, opts...) } @@ -41,6 +45,8 @@ func (f ResourceLimiterProviderFunc) ResourceLimiter(key WeightKey, opts ...Opti // // See the README for more recommendations. type ResourceLimiter interface { + Checker + // Acquire attempts to acquire a quantified resource with the // provided weight, based on the key that was given to the // provider. The caller has these options: @@ -66,15 +72,19 @@ type ResourceLimiter interface { // Acquire(0) is called, since there is nothing to release. type ReleaseFunc func() -// ResourceLimiterFunc is a functional way to construct ResourceLimiters. -type ResourceLimiterFunc func(ctx context.Context, value uint64) (ReleaseFunc, error) +// AcquireFunc is a functional way to construct Acquire functions. +type AcquireFunc func(ctx context.Context, value uint64) (ReleaseFunc, error) -var _ ResourceLimiter = ResourceLimiterFunc(nil) - -// Acquire implements ResourceLimiter -func (f ResourceLimiterFunc) Acquire(ctx context.Context, value uint64) (ReleaseFunc, error) { +// Acquire implements part of ResourceLimiter. +func (f AcquireFunc) Acquire(ctx context.Context, value uint64) (ReleaseFunc, error) { if f == nil { return func() {}, nil } return f(ctx, value) } + +// Verify that a rate limiter is constructed of two functions. +var _ ResourceLimiter = struct { + MustDenyFunc + AcquireFunc +}{} From 40aee984eebd3330f9f54ca8b776528eb43bc1c0 Mon Sep 17 00:00:00 2001 From: Joshua MacDonald Date: Tue, 6 May 2025 11:05:28 -0700 Subject: [PATCH 12/47] call checker once --- .../extensionlimiter/extensionlimiter.go | 19 ++++ .../extensionlimiter/limiterhelper/checker.go | 30 +----- .../limiterhelper/consumer.go | 67 ++++++++----- .../limiterhelper/middleware.go | 34 +++++-- .../extensionlimiter/limiterhelper/wrapper.go | 94 ++++++++++--------- extension/extensionlimiter/rate.go | 21 ++--- extension/extensionlimiter/resource.go | 17 ++-- extension/extensionlimiter/weight.go | 9 +- receiver/otlpreceiver/otlp.go | 16 ++-- 9 files changed, 179 insertions(+), 128 deletions(-) diff --git a/extension/extensionlimiter/extensionlimiter.go b/extension/extensionlimiter/extensionlimiter.go index 580d16238481..2a8659c0b9ec 100644 --- a/extension/extensionlimiter/extensionlimiter.go +++ b/extension/extensionlimiter/extensionlimiter.go @@ -44,3 +44,22 @@ func (f MustDenyFunc) MustDeny(ctx context.Context) error { func NeverDeny() Checker { return MustDenyFunc(nil) } + +// CheckerProvider is an interface to obtain checkers for a group of +// weight keys. +type CheckerProvider interface { + // GetChecker returns a checker for a group of weight keys. + GetChecker(WeightSet, ...Option) (Checker, error) +} + +// GetCheckerFunc is a functional way to construct GetChecker +// functions, used in limiter providers. +type GetCheckerFunc func(WeightSet, ...Option) (Checker, error) + +// Checker implements CheckerProvider. +func (f GetCheckerFunc) GetChecker(keys WeightSet, opts ...Option) (Checker, error) { + if f == nil { + return nil, nil + } + return f(keys, opts...) +} diff --git a/extension/extensionlimiter/limiterhelper/checker.go b/extension/extensionlimiter/limiterhelper/checker.go index e5f92ca2e4bf..115c8603d525 100644 --- a/extension/extensionlimiter/limiterhelper/checker.go +++ b/extension/extensionlimiter/limiterhelper/checker.go @@ -5,6 +5,7 @@ package limiterhelper // import "go.opentelemetry.io/collector/extension/extensi import ( "context" + "errors" "go.opentelemetry.io/collector/extension/extensionlimiter" ) @@ -16,35 +17,12 @@ var _ extensionlimiter.Checker = MultiChecker{} // MustDeny implements Checker. func (ls MultiChecker) MustDeny(ctx context.Context) error { + var err error for _, lim := range ls { if lim == nil { continue } - if err := lim.MustDeny(ctx); err != nil { - return err - } + err = errors.Join(err, lim.MustDeny(ctx)) } - return nil -} - -// NewLimiterWrapperChecker returns a Checker for a LimiterWrapper. -func NewLimiterWrapperChecker(limiter LimiterWrapper) extensionlimiter.Checker { - return extensionlimiter.MustDenyFunc(func(ctx context.Context) error { - return limiter.LimitCall(ctx, 0, func(_ context.Context) error { return nil }) - }) -} - -// NewRateLimiterChecker returns a Checker for a RateLimiter. -func NewRateLimiterChecker(limiter extensionlimiter.RateLimiter) extensionlimiter.Checker { - return extensionlimiter.MustDenyFunc(func(ctx context.Context) error { - return limiter.Limit(ctx, 0) - }) -} - -// NewResourceLimiterChecker returns a Checker for ResourceLimiter. -func NewResourceLimiterChecker(limiter extensionlimiter.ResourceLimiter) extensionlimiter.Checker { - return extensionlimiter.MustDenyFunc(func(ctx context.Context) error { - _, err := limiter.Acquire(ctx, 0) - return err - }) + return err } diff --git a/extension/extensionlimiter/limiterhelper/consumer.go b/extension/extensionlimiter/limiterhelper/consumer.go index 18d05346f650..1db74c5fc168 100644 --- a/extension/extensionlimiter/limiterhelper/consumer.go +++ b/extension/extensionlimiter/limiterhelper/consumer.go @@ -6,7 +6,6 @@ package limiterhelper // import "go.opentelemetry.io/collector/extension/extensi import ( "context" "errors" - "slices" "go.opentelemetry.io/collector/consumer" "go.opentelemetry.io/collector/consumer/xconsumer" @@ -119,81 +118,101 @@ func (profileTraits) consume(ctx context.Context, data pprofile.Profiles, next x // limitOne obtains a LimiterWrapper and applies a single weight limit. func limitOne[P any, C any]( next C, - keys []extensionlimiter.WeightKey, + keys extensionlimiter.WeightSet, provider LimiterWrapperProvider, m traits[P, C], key extensionlimiter.WeightKey, opts []consumer.Option, quantify func(P) uint64, -) (extensionlimiter.Checker, C, error) { - if !slices.Contains(keys, key) { - return nil, next, nil +) (C, error) { + if !keys.Contains(key) { + return next, nil } - lim, err := provider.LimiterWrapper(key) + lim, err := provider.GetLimiterWrapper(key) if err != nil { - return nil, next, err + return next, err } if lim == nil { - return nil, next, nil + return next, nil } - con, err := m.create(func(ctx context.Context, data P) error { + return m.create(func(ctx context.Context, data P) error { return lim.LimitCall(ctx, quantify(data), func(ctx context.Context) error { return m.consume(ctx, data, next) }) }, opts...) - return NewLimiterWrapperChecker(lim), con, err +} + +// applyChecker gets a Checker and wraps the pipeline in a MustDeny +// check. +func applyChecker[P any, C any]( + next C, + keys extensionlimiter.WeightSet, + provider LimiterWrapperProvider, + m traits[P, C], + opts []consumer.Option, +) (C, error) { + // Apply the Checker. + ck, err := provider.GetChecker(keys) + if err != nil { + return next, err + } + return m.create(func(ctx context.Context, data P) error { + if err := ck.MustDeny(ctx); err != nil { + return err + } + return m.consume(ctx, data, next) + }, opts...) } // newLimited is signal-generic limiting logic. func newLimited[P any, C any]( next C, - keys []extensionlimiter.WeightKey, + keys extensionlimiter.WeightSet, provider LimiterWrapperProvider, m traits[P, C], opts ...consumer.Option, -) (extensionlimiter.Checker, C, error) { +) (C, error) { if provider == nil { - return nil, next, nil + return next, nil } - var lim1, lim2, lim3 extensionlimiter.Checker - var err1, err2, err3 error + var err1, err2, err3, err4 error // Note: reverse order of evaluation cost => least-cost applied first. - lim1, next, err1 = limitOne(next, keys, provider, m, extensionlimiter.WeightKeyMemorySize, opts, - + next, err1 = limitOne(next, keys, provider, m, extensionlimiter.WeightKeyMemorySize, opts, func(data P) uint64 { return m.memorySize(data) }) - lim2, next, err2 = limitOne(next, keys, provider, m, extensionlimiter.WeightKeyRequestItems, opts, + next, err2 = limitOne(next, keys, provider, m, extensionlimiter.WeightKeyRequestItems, opts, func(data P) uint64 { return m.itemCount(data) }) - lim3, next, err3 = limitOne(next, keys, provider, m, extensionlimiter.WeightKeyRequestCount, opts, + next, err3 = limitOne(next, keys, provider, m, extensionlimiter.WeightKeyRequestCount, opts, func(_ P) uint64 { return 1 }) - return MultiChecker{lim1, lim2, lim3}, next, errors.Join(err1, err2, err3) + next, err4 = applyChecker(next, keys, provider, m, opts) + return next, errors.Join(err1, err2, err3, err4) } // NewLimitedTraces applies a limiter using the provider over keys before calling next. -func NewLimitedTraces(next consumer.Traces, keys []extensionlimiter.WeightKey, provider LimiterWrapperProvider) (extensionlimiter.Checker, consumer.Traces, error) { +func NewLimitedTraces(next consumer.Traces, keys extensionlimiter.WeightSet, provider LimiterWrapperProvider) (consumer.Traces, error) { return newLimited(next, keys, provider, traceTraits{}, consumer.WithCapabilities(next.Capabilities())) } // NewLimitedLogs applies a limiter using the provider over keys before calling next. -func NewLimitedLogs(next consumer.Logs, keys []extensionlimiter.WeightKey, provider LimiterWrapperProvider) (extensionlimiter.Checker, consumer.Logs, error) { +func NewLimitedLogs(next consumer.Logs, keys extensionlimiter.WeightSet, provider LimiterWrapperProvider) (consumer.Logs, error) { return newLimited(next, keys, provider, logTraits{}, consumer.WithCapabilities(next.Capabilities())) } // NewLimitedMetrics applies a limiter using the provider over keys before calling next. -func NewLimitedMetrics(next consumer.Metrics, keys []extensionlimiter.WeightKey, provider LimiterWrapperProvider) (extensionlimiter.Checker, consumer.Metrics, error) { +func NewLimitedMetrics(next consumer.Metrics, keys extensionlimiter.WeightSet, provider LimiterWrapperProvider) (consumer.Metrics, error) { return newLimited(next, keys, provider, metricTraits{}, consumer.WithCapabilities(next.Capabilities())) } // NewLimitedProfiles applies a limiter using the provider over keys before calling next. -func NewLimitedProfiles(next xconsumer.Profiles, keys []extensionlimiter.WeightKey, provider LimiterWrapperProvider) (extensionlimiter.Checker, xconsumer.Profiles, error) { +func NewLimitedProfiles(next xconsumer.Profiles, keys extensionlimiter.WeightSet, provider LimiterWrapperProvider) (xconsumer.Profiles, error) { return newLimited(next, keys, provider, profileTraits{}, consumer.WithCapabilities(next.Capabilities())) } diff --git a/extension/extensionlimiter/limiterhelper/middleware.go b/extension/extensionlimiter/limiterhelper/middleware.go index 95570d9b7c4e..fc284f47f83a 100644 --- a/extension/extensionlimiter/limiterhelper/middleware.go +++ b/extension/extensionlimiter/limiterhelper/middleware.go @@ -107,24 +107,46 @@ type MultiLimiterWrapperProvider []LimiterWrapperProvider var _ LimiterWrapperProvider = MultiLimiterWrapperProvider{} -// LimiterWrapper implements LimiterWrapperProvider. -func (ps MultiLimiterWrapperProvider) LimiterWrapper(key extensionlimiter.WeightKey, opts ...extensionlimiter.Option) (LimiterWrapper, error) { - if len(ps) == 0 { - return PassThroughWrapper(), nil +// GetLimiterWrapper implements LimiterWrapperProvider, combining +// checkers for all wrappers in a sequence. +func (ps MultiLimiterWrapperProvider) GetChecker(keys extensionlimiter.WeightSet, opts ...extensionlimiter.Option) (extensionlimiter.Checker, error) { + var retErr error + var cks MultiChecker + for _, provider := range ps { + ck, err := provider.GetChecker(keys, opts...) + retErr = errors.Join(retErr, err) + if ck == nil { + continue + } + cks = append(cks, ck) + } + if len(cks) == 0 { + return extensionlimiter.NeverDeny(), retErr } + return cks, retErr +} +// GetLimiterWrapper implements LimiterWrapperProvider, calling +// wrappers in a sequence. +func (ps MultiLimiterWrapperProvider) GetLimiterWrapper(key extensionlimiter.WeightKey, opts ...extensionlimiter.Option) (LimiterWrapper, error) { // Map provider list to limiter list. var lims []LimiterWrapper for _, provider := range ps { - lim, err := provider.LimiterWrapper(key, opts...) + lim, err := provider.GetLimiterWrapper(key, opts...) if err == nil { return nil, err } + if lim == nil { + continue + } lims = append(lims, lim) } - // Compose limiters in sequence. + if len(lims) == 0 { + return PassThroughWrapper(), nil + } + return sequenceLimiters(lims), nil } diff --git a/extension/extensionlimiter/limiterhelper/wrapper.go b/extension/extensionlimiter/limiterhelper/wrapper.go index 35c7b213c5bb..f3b1e2c7f3ea 100644 --- a/extension/extensionlimiter/limiterhelper/wrapper.go +++ b/extension/extensionlimiter/limiterhelper/wrapper.go @@ -14,22 +14,27 @@ import ( // function call, because for wrapped calls there is no distinction // between rate limiters and resource limiters. type LimiterWrapperProvider interface { - LimiterWrapper(extensionlimiter.WeightKey, ...extensionlimiter.Option) (LimiterWrapper, error) -} + extensionlimiter.CheckerProvider -// LimiterWrapperProviderFunc is a functional way to build LimiterWrappers. -type LimiterWrapperProviderFunc func(extensionlimiter.WeightKey, ...extensionlimiter.Option) (LimiterWrapper, error) + GetLimiterWrapper(extensionlimiter.WeightKey, ...extensionlimiter.Option) (LimiterWrapper, error) +} -var _ LimiterWrapperProvider = LimiterWrapperProviderFunc(nil) +// GetLimiterWrapperFunc is an easy way to build GetLimiterWrapper functions. +type GetLimiterWrapperFunc func(extensionlimiter.WeightKey, ...extensionlimiter.Option) (LimiterWrapper, error) -// LimiterWrapper implements LimiterWrapperProvider. -func (f LimiterWrapperProviderFunc) LimiterWrapper(key extensionlimiter.WeightKey, opts ...extensionlimiter.Option) (LimiterWrapper, error) { +// GetLimiterWrapper implements LimiterWrapperProvider. +func (f GetLimiterWrapperFunc) GetLimiterWrapper(key extensionlimiter.WeightKey, opts ...extensionlimiter.Option) (LimiterWrapper, error) { if f == nil { return PassThroughWrapper(), nil } return f(key, opts...) } +var _ LimiterWrapperProvider = struct { + GetLimiterWrapperFunc + extensionlimiter.GetCheckerFunc +}{} + // LimiterWrapper is a general-purpose interface for limiter consumers // to limit resources with use of a callback. This is the simplest // form of rate limiting interface from a callers perspective. If the @@ -70,48 +75,51 @@ func PassThroughWrapper() LimiterWrapper { return LimiterWrapperFunc(nil) } +// wrapperProvider is a combinator for building wrapper providers from +// the underlying limter types. +type wrapperProvider struct { + GetLimiterWrapperFunc + extensionlimiter.GetCheckerFunc +} + // NewResourceLimiterWrapperProvider constructs a // LimiterWrapperProvider for a resource limiter extension. func NewResourceLimiterWrapperProvider(rp extensionlimiter.ResourceLimiterProvider) LimiterWrapperProvider { - return LimiterWrapperProviderFunc(func(key extensionlimiter.WeightKey, opts ...extensionlimiter.Option) (LimiterWrapper, error) { - lim, err := rp.GetResourceLimiter(key, opts...) - if err == nil { - return nil, err - } - return NewResourceLimiterWrapper(lim), err - }) + return wrapperProvider{ + GetCheckerFunc: rp.GetChecker, + GetLimiterWrapperFunc: func(key extensionlimiter.WeightKey, opts ...extensionlimiter.Option) (LimiterWrapper, error) { + lim, err := rp.GetResourceLimiter(key, opts...) + if err == nil { + return nil, err + } + return LimiterWrapperFunc(func(ctx context.Context, value uint64, call func(context.Context) error) error { + release, err := lim.Acquire(ctx, value) + if err != nil { + return err + } + defer release() + return call(ctx) + }), err + }, + } } // NewRateLimiterWrapperProvider constructs a LimiterWrapperProvider // for a rate limiter extension. func NewRateLimiterWrapperProvider(rp extensionlimiter.RateLimiterProvider) LimiterWrapperProvider { - return LimiterWrapperProviderFunc(func(key extensionlimiter.WeightKey, opts ...extensionlimiter.Option) (LimiterWrapper, error) { - lim, err := rp.GetRateLimiter(key, opts...) - if err == nil { - return nil, err - } - return NewRateLimiterWrapper(lim), err - }) -} - -// NewRateLimiterWrapper returns a LimiterWrapper from a RateLimiter. -func NewRateLimiterWrapper(limiter extensionlimiter.RateLimiter) LimiterWrapper { - return LimiterWrapperFunc(func(ctx context.Context, value uint64, call func(context.Context) error) error { - if err := limiter.Limit(ctx, value); err != nil { - return err - } - return call(ctx) - }) -} - -// NewResourceLimiterWrapper returns a LimiterWrapper from a ResourceLimiter. -func NewResourceLimiterWrapper(limiter extensionlimiter.ResourceLimiter) LimiterWrapper { - return LimiterWrapperFunc(func(ctx context.Context, value uint64, call func(context.Context) error) error { - release, err := limiter.Acquire(ctx, value) - if err != nil { - return err - } - defer release() - return call(ctx) - }) + return wrapperProvider{ + GetCheckerFunc: rp.GetChecker, + GetLimiterWrapperFunc: func(key extensionlimiter.WeightKey, opts ...extensionlimiter.Option) (LimiterWrapper, error) { + lim, err := rp.GetRateLimiter(key, opts...) + if err == nil { + return nil, err + } + return LimiterWrapperFunc(func(ctx context.Context, value uint64, call func(context.Context) error) error { + if err := lim.Limit(ctx, value); err != nil { + return err + } + return call(ctx) + }), err + }, + } } diff --git a/extension/extensionlimiter/rate.go b/extension/extensionlimiter/rate.go index b8748281da64..dcc8d1cc4af5 100644 --- a/extension/extensionlimiter/rate.go +++ b/extension/extensionlimiter/rate.go @@ -14,16 +14,16 @@ import ( // Limiters are covered by configmiddleware configuration, which is // able to construct LimiterWrappers from these providers. type RateLimiterProvider interface { - // GetRateLimiter returns a provider for rate limiters. + CheckerProvider + + // GetRateLimiter returns a rate limiter for a weight key. GetRateLimiter(WeightKey, ...Option) (RateLimiter, error) } -// RateLimiterFunc is a functional way to construct GetRateLimiter +// GetRateLimiterFunc is a functional way to construct GetRateLimiter // functions. type GetRateLimiterFunc func(WeightKey, ...Option) (RateLimiter, error) -var _ RateLimiterProvider = GetRateLimiterFunc(nil) - // RateLimiter implements RateLimiterProvider. func (f GetRateLimiterFunc) GetRateLimiter(key WeightKey, opts ...Option) (RateLimiter, error) { if f == nil { @@ -32,6 +32,11 @@ func (f GetRateLimiterFunc) GetRateLimiter(key WeightKey, opts ...Option) (RateL return f(key, opts...) } +var _ RateLimiterProvider = struct { + GetRateLimiterFunc + GetCheckerFunc +}{} + // RateLimiter is an interface that an implementation makes available // to apply time-based limits on quantities such as the number of // bytes or items per second. @@ -44,8 +49,6 @@ func (f GetRateLimiterFunc) GetRateLimiter(key WeightKey, opts ...Option) (RateL // // See the README for more recommendations. type RateLimiter interface { - Checker - // Limit attempts to apply rate limiting with the provided // weight, based on the key that was given to the provider. // @@ -66,8 +69,4 @@ func (f LimitFunc) Limit(ctx context.Context, value uint64) error { return f(ctx, value) } -// Verify that a rate limiter is constructed of two functions. -var _ RateLimiter = struct { - MustDenyFunc - LimitFunc -}{} +var _ RateLimiter = LimitFunc(nil) diff --git a/extension/extensionlimiter/resource.go b/extension/extensionlimiter/resource.go index 2baf417243dc..98ba253accf4 100644 --- a/extension/extensionlimiter/resource.go +++ b/extension/extensionlimiter/resource.go @@ -14,6 +14,8 @@ import ( // Limiters are covered by configmiddleware configuration, which // is able to construct LimiterWrappers from these providers. type ResourceLimiterProvider interface { + CheckerProvider + GetResourceLimiter(WeightKey, ...Option) (ResourceLimiter, error) } @@ -21,8 +23,6 @@ type ResourceLimiterProvider interface { // GetResourceLimiter functions. type GetResourceLimiterFunc func(WeightKey, ...Option) (ResourceLimiter, error) -var _ ResourceLimiterProvider = GetResourceLimiterFunc(nil) - // GetResourceLimiter implements part of ResourceLimiterProvider. func (f GetResourceLimiterFunc) GetResourceLimiter(key WeightKey, opts ...Option) (ResourceLimiter, error) { if f == nil { @@ -31,6 +31,11 @@ func (f GetResourceLimiterFunc) GetResourceLimiter(key WeightKey, opts ...Option return f(key, opts...) } +var _ ResourceLimiterProvider = struct { + GetResourceLimiterFunc + GetCheckerFunc +}{} + // ResourceLimiter is an interface that an implementation makes // available to apply physical limits on quantities such as the number // of concurrent requests or amount of memory in use. @@ -45,8 +50,6 @@ func (f GetResourceLimiterFunc) GetResourceLimiter(key WeightKey, opts ...Option // // See the README for more recommendations. type ResourceLimiter interface { - Checker - // Acquire attempts to acquire a quantified resource with the // provided weight, based on the key that was given to the // provider. The caller has these options: @@ -83,8 +86,4 @@ func (f AcquireFunc) Acquire(ctx context.Context, value uint64) (ReleaseFunc, er return f(ctx, value) } -// Verify that a rate limiter is constructed of two functions. -var _ ResourceLimiter = struct { - MustDenyFunc - AcquireFunc -}{} +var _ ResourceLimiter = AcquireFunc(nil) diff --git a/extension/extensionlimiter/weight.go b/extension/extensionlimiter/weight.go index 16d5c8c4ec9d..47a8d7a48282 100644 --- a/extension/extensionlimiter/weight.go +++ b/extension/extensionlimiter/weight.go @@ -3,7 +3,7 @@ package extensionlimiter // import "go.opentelemetry.io/collector/extension/extensionlimiter" -// WeightKey is an enum type for common rate limits. The +import "slices" // WeightKey is an enum type for common rate limits. The // StandardAllKeys, StandardMiddlewareKeys, and // StandardNotMiddlewareKeys methods return the list of middleware // keys that can be automatically configured through middleware and @@ -38,6 +38,13 @@ const ( WeightKeyMemorySize WeightKey = "memory_size" ) +// WeightSet are a group of weights to be tested. +type WeightSet []WeightKey + +func (ws WeightSet) Contains(w WeightKey) bool { + return slices.Contains(ws, w) +} + // StandardAllKeys is all the keys that can be automatically // implemented by middleware and/or limiterhelper. func StandardAllKeys() []WeightKey { diff --git a/receiver/otlpreceiver/otlp.go b/receiver/otlpreceiver/otlp.go index 4b2a52e565fb..288be4c3e7fc 100644 --- a/receiver/otlpreceiver/otlp.go +++ b/receiver/otlpreceiver/otlp.go @@ -107,7 +107,7 @@ func (r *otlpReceiver) startGRPCServer(host component.Host) error { if r.nextTraces != nil { var next consumer.Traces - _, next, err = limiterhelper.NewLimitedTraces(r.nextTraces, limitKeys, limiterProvider) + next, err = limiterhelper.NewLimitedTraces(r.nextTraces, limitKeys, limiterProvider) if err != nil { return err } @@ -116,7 +116,7 @@ func (r *otlpReceiver) startGRPCServer(host component.Host) error { if r.nextMetrics != nil { var next consumer.Metrics - _, next, err = limiterhelper.NewLimitedMetrics(r.nextMetrics, limitKeys, limiterProvider) + next, err = limiterhelper.NewLimitedMetrics(r.nextMetrics, limitKeys, limiterProvider) if err != nil { return err } @@ -125,7 +125,7 @@ func (r *otlpReceiver) startGRPCServer(host component.Host) error { if r.nextLogs != nil { var next consumer.Logs - _, next, err = limiterhelper.NewLimitedLogs(r.nextLogs, limitKeys, limiterProvider) + next, err = limiterhelper.NewLimitedLogs(r.nextLogs, limitKeys, limiterProvider) if err != nil { return err } @@ -134,7 +134,7 @@ func (r *otlpReceiver) startGRPCServer(host component.Host) error { if r.nextProfiles != nil { var next xconsumer.Profiles - _, next, err = limiterhelper.NewLimitedProfiles(r.nextProfiles, limitKeys, limiterProvider) + next, err = limiterhelper.NewLimitedProfiles(r.nextProfiles, limitKeys, limiterProvider) if err != nil { return err } @@ -172,7 +172,7 @@ func (r *otlpReceiver) startHTTPServer(ctx context.Context, host component.Host) httpMux := http.NewServeMux() if r.nextTraces != nil { - _, next, err := limiterhelper.NewLimitedTraces(r.nextTraces, limitKeys, limiterProvider) + next, err := limiterhelper.NewLimitedTraces(r.nextTraces, limitKeys, limiterProvider) if err != nil { return err } @@ -183,7 +183,7 @@ func (r *otlpReceiver) startHTTPServer(ctx context.Context, host component.Host) } if r.nextMetrics != nil { - _, next, err := limiterhelper.NewLimitedMetrics(r.nextMetrics, limitKeys, limiterProvider) + next, err := limiterhelper.NewLimitedMetrics(r.nextMetrics, limitKeys, limiterProvider) if err != nil { return err } @@ -194,7 +194,7 @@ func (r *otlpReceiver) startHTTPServer(ctx context.Context, host component.Host) } if r.nextLogs != nil { - _, next, err := limiterhelper.NewLimitedLogs(r.nextLogs, limitKeys, limiterProvider) + next, err := limiterhelper.NewLimitedLogs(r.nextLogs, limitKeys, limiterProvider) if err != nil { return err } @@ -205,7 +205,7 @@ func (r *otlpReceiver) startHTTPServer(ctx context.Context, host component.Host) } if r.nextProfiles != nil { - _, next, err := limiterhelper.NewLimitedProfiles(r.nextProfiles, limitKeys, limiterProvider) + next, err := limiterhelper.NewLimitedProfiles(r.nextProfiles, limitKeys, limiterProvider) if err != nil { return err } From 137be274e2c7f3108339e3f3393da8d666706e0c Mon Sep 17 00:00:00 2001 From: Joshua MacDonald Date: Tue, 6 May 2025 13:25:56 -0700 Subject: [PATCH 13/47] checker all keys --- extension/extensionlimiter/README.md | 57 +++++++++++-------- .../extensionlimiter/extensionlimiter.go | 13 ++--- .../extensionlimiter/limiterhelper/checker.go | 5 ++ .../limiterhelper/consumer.go | 3 +- .../limiterhelper/middleware.go | 6 +- extension/extensionlimiter/weight.go | 18 +++--- 6 files changed, 56 insertions(+), 46 deletions(-) diff --git a/extension/extensionlimiter/README.md b/extension/extensionlimiter/README.md index 51cd0efecc6e..fbeb44a0f18a 100644 --- a/extension/extensionlimiter/README.md +++ b/extension/extensionlimiter/README.md @@ -22,17 +22,17 @@ which simplifies consumers in most cases by providing a consistent `LimitCall` interface. A limiter is **saturated** by definition when a limit is completely -overloaded, generally it means a limit request of any size would fail -at that moment and should be taken as a strong signal to stop -accepting requests. +overloaded in at least one weight, generally it means callers should +immediately deny work to continue on the request. Each kind of limiter as well as the wrapper type have corresponding **provider** interfaces that return a limiter instance based on a -weight key or keys. +weight keys. Weight keys describe the standard limiting dimensions. There are currently four standard weight keys: network bytes, request count, -request items, and memory size. +request items, and memory size. Callers use the `Checker` interface +to check whether any weight keys (from a set) are saturated. ## Key Interfaces @@ -44,7 +44,7 @@ request items, and memory size. - `ResourceLimiter`: Manages physical resource limits, has an `Acquire` method and a corresponding `ReleaseFunc`, plus a provider type. -- `Limiter`: Any of the above, has a `MustDeny` method. +- `Checker`: Has a `MustDeny` method. ### Limiter helpers @@ -91,15 +91,17 @@ becomes available, they should return a standard overload signal. ### Limiter saturation -All limiters feature a `MustDeny` method which is made available for -applications to test when a limit is fully saturated. This special -limit request is defined as the equivalent of passing a zero value to -the limiter. +Rate and resource limiter providers have a `GetChecker` method to +provide a `Checker`, featuring a `MustDeny` method which is made +available for applications to test when any limit is fully +saturated that would eventually deny the request. -Limiters SHOULD treat a request for zero units of the limit as a -special case, used for indicating when non-zero limit requests are -likely to fail. This is not an exact requirement; implementations are -free to define their own saturation parameters. +The `Checker` is consulted at least once and applies to all weight +keys. Because a `Checker` can be consulted more than once by a +receiver and/or middleware, it is possible for requests to be denied +over the saturation of limits they were already granted. Users should +configure external load balancers and/or horizontal scaling policies +to avoid cases of limiter saturation. ### Limit before or after use @@ -187,10 +189,14 @@ apply request items and memory size: s.limiterProvider, err = limiterhelper.MiddlewaresToLimiterWrapperProvider( cfg.Middlewares) if err != nil { ... } + + // Extract a checker from the provider + s.checker, err = s.limiterProvider.GetChecker() + if err != nil { ... } // Here get a limiter-wrapped pipeline and a combination of weight-specific // limiters for MustDeny() functionality. - s.anyLimiter, s.nextMetrics, err = limiterhelper.NewLimitedMetrics( + s.nextMetrics, err = limiterhelper.NewLimitedMetrics( s.nextMetrics, limiterhelper.StandardNotMiddlewareKeys(), s.limiterProvider) if err != nil { ... } ``` @@ -199,7 +205,7 @@ In the scraper loop, use `MustDeny` before starting a scrape: ```golang func (s *scraper) scrapeOnce(ctx context.Context) error { - if err := s.anyLimiter.MustDeny(ctx); err != nil { + if err := s.checker.MustDeny(ctx); err != nil { return err } @@ -231,21 +237,22 @@ receivers: - ratelimiter/streamer ``` -The receiver will check `s.anyLimiter.MustDeny()` as above. In a -stream, limiters are expected to block the stream until limit requests -succeed, however after the limit requests succeed, the receiver may -wish to return from `Send()` to continue accepting new requests while -the consumer works in a separate goroutine. The limit will be released -after the consumer returns. +The receiver will create with `extensionlimiter.StandardAllKeys()` and +check `s.checker.MustDeny()` as above. In a stream, limiters are +expected to block the stream until limit requests succeed, however +after the limit requests succeed, the receiver may wish to return from +`Send()` to continue accepting new requests while the consumer works +in a separate goroutine. The limit will be released after the consumer +returns. ```golang func (s *scraper) LogsStream(ctx context.Context, stream *Stream) error { for { - // Check saturation for all limiters. - err := s.anyLimiter.MustDeny(ctx) + // Check saturation for all limiters, all keys. + err := s.checker.MustDeny(ctx) if err != nil { ... } - // The network bytes and request count are applied in middleware. + // The network bytes and request count limits are applied in middleware. req, err := stream.Recv() if err != nil { ... } diff --git a/extension/extensionlimiter/extensionlimiter.go b/extension/extensionlimiter/extensionlimiter.go index 2a8659c0b9ec..04789e79dab4 100644 --- a/extension/extensionlimiter/extensionlimiter.go +++ b/extension/extensionlimiter/extensionlimiter.go @@ -40,26 +40,21 @@ func (f MustDenyFunc) MustDeny(ctx context.Context) error { return f(ctx) } -// NeverDeny returns a Checker that never denies. -func NeverDeny() Checker { - return MustDenyFunc(nil) -} - // CheckerProvider is an interface to obtain checkers for a group of // weight keys. type CheckerProvider interface { // GetChecker returns a checker for a group of weight keys. - GetChecker(WeightSet, ...Option) (Checker, error) + GetChecker(...Option) (Checker, error) } // GetCheckerFunc is a functional way to construct GetChecker // functions, used in limiter providers. -type GetCheckerFunc func(WeightSet, ...Option) (Checker, error) +type GetCheckerFunc func(...Option) (Checker, error) // Checker implements CheckerProvider. -func (f GetCheckerFunc) GetChecker(keys WeightSet, opts ...Option) (Checker, error) { +func (f GetCheckerFunc) GetChecker(opts ...Option) (Checker, error) { if f == nil { return nil, nil } - return f(keys, opts...) + return f(opts...) } diff --git a/extension/extensionlimiter/limiterhelper/checker.go b/extension/extensionlimiter/limiterhelper/checker.go index 115c8603d525..fcad04b4c18e 100644 --- a/extension/extensionlimiter/limiterhelper/checker.go +++ b/extension/extensionlimiter/limiterhelper/checker.go @@ -26,3 +26,8 @@ func (ls MultiChecker) MustDeny(ctx context.Context) error { } return err } + +// NeverDeny returns a Checker that never denies. +func NeverDeny() extensionlimiter.Checker { + return extensionlimiter.MustDenyFunc(nil) +} diff --git a/extension/extensionlimiter/limiterhelper/consumer.go b/extension/extensionlimiter/limiterhelper/consumer.go index 1db74c5fc168..c4eb3aa7d564 100644 --- a/extension/extensionlimiter/limiterhelper/consumer.go +++ b/extension/extensionlimiter/limiterhelper/consumer.go @@ -151,8 +151,7 @@ func applyChecker[P any, C any]( m traits[P, C], opts []consumer.Option, ) (C, error) { - // Apply the Checker. - ck, err := provider.GetChecker(keys) + ck, err := provider.GetChecker() if err != nil { return next, err } diff --git a/extension/extensionlimiter/limiterhelper/middleware.go b/extension/extensionlimiter/limiterhelper/middleware.go index fc284f47f83a..aabcc175aa3d 100644 --- a/extension/extensionlimiter/limiterhelper/middleware.go +++ b/extension/extensionlimiter/limiterhelper/middleware.go @@ -109,11 +109,11 @@ var _ LimiterWrapperProvider = MultiLimiterWrapperProvider{} // GetLimiterWrapper implements LimiterWrapperProvider, combining // checkers for all wrappers in a sequence. -func (ps MultiLimiterWrapperProvider) GetChecker(keys extensionlimiter.WeightSet, opts ...extensionlimiter.Option) (extensionlimiter.Checker, error) { +func (ps MultiLimiterWrapperProvider) GetChecker(opts ...extensionlimiter.Option) (extensionlimiter.Checker, error) { var retErr error var cks MultiChecker for _, provider := range ps { - ck, err := provider.GetChecker(keys, opts...) + ck, err := provider.GetChecker(opts...) retErr = errors.Join(retErr, err) if ck == nil { continue @@ -121,7 +121,7 @@ func (ps MultiLimiterWrapperProvider) GetChecker(keys extensionlimiter.WeightSet cks = append(cks, ck) } if len(cks) == 0 { - return extensionlimiter.NeverDeny(), retErr + return NeverDeny(), retErr } return cks, retErr } diff --git a/extension/extensionlimiter/weight.go b/extension/extensionlimiter/weight.go index 47a8d7a48282..28fcfcf0b9b7 100644 --- a/extension/extensionlimiter/weight.go +++ b/extension/extensionlimiter/weight.go @@ -38,7 +38,11 @@ const ( WeightKeyMemorySize WeightKey = "memory_size" ) -// WeightSet are a group of weights to be tested. +// WeightSet are a group of weights to be tested. The purpose of this +// type is to be explicit about a group of weights that have to be +// checked at a certain stage. The receiver and middleware can both +// be responsible for applying limits, and this type helps ensure +// limits are applied only across cooperating sub-components. type WeightSet []WeightKey func (ws WeightSet) Contains(w WeightKey) bool { @@ -47,8 +51,8 @@ func (ws WeightSet) Contains(w WeightKey) bool { // StandardAllKeys is all the keys that can be automatically // implemented by middleware and/or limiterhelper. -func StandardAllKeys() []WeightKey { - return []WeightKey{ +func StandardAllKeys() WeightSet { + return WeightSet{ WeightKeyNetworkBytes, WeightKeyRequestCount, WeightKeyRequestItems, @@ -60,8 +64,8 @@ func StandardAllKeys() []WeightKey { // protocols that support it. Receivers should be careful not to // re-apply these limits, especially not to twice-limit by // WeightKeyRequestItems. -func StandardMiddlewareKeys() []WeightKey { - return []WeightKey{ +func StandardMiddlewareKeys() WeightSet { + return WeightSet{ WeightKeyNetworkBytes, WeightKeyRequestCount, } @@ -70,8 +74,8 @@ func StandardMiddlewareKeys() []WeightKey { // StandardNotMiddlewareKeys are the keys that are typically not // handled through middlware because they are protocol specific and // generally easier to handle after the input has become pdata. -func StandardNotMiddlewareKeys() []WeightKey { - return []WeightKey{ +func StandardNotMiddlewareKeys() WeightSet { + return WeightSet{ WeightKeyRequestItems, WeightKeyMemorySize, } From 4a44264c5772673cc1a17b689d531237ebf68b53 Mon Sep 17 00:00:00 2001 From: Joshua MacDonald Date: Tue, 6 May 2025 15:13:10 -0700 Subject: [PATCH 14/47] readme --- extension/extensionlimiter/README.md | 102 +++++++++++++++++++-------- 1 file changed, 71 insertions(+), 31 deletions(-) diff --git a/extension/extensionlimiter/README.md b/extension/extensionlimiter/README.md index fbeb44a0f18a..4656ebece4fe 100644 --- a/extension/extensionlimiter/README.md +++ b/extension/extensionlimiter/README.md @@ -237,13 +237,12 @@ receivers: - ratelimiter/streamer ``` -The receiver will create with `extensionlimiter.StandardAllKeys()` and -check `s.checker.MustDeny()` as above. In a stream, limiters are -expected to block the stream until limit requests succeed, however -after the limit requests succeed, the receiver may wish to return from -`Send()` to continue accepting new requests while the consumer works -in a separate goroutine. The limit will be released after the consumer -returns. +The receiver will check `s.checker.MustDeny()` as above. In a stream, +limiters are expected to block the stream until limit requests +succeed, however after the limit requests succeed, the receiver may +wish to return from `Send()` to continue accepting new requests while +the consumer works in a separate goroutine. The limit will be released +after the consumer returns. ```golang func (s *scraper) LogsStream(ctx context.Context, stream *Stream) error { @@ -277,36 +276,77 @@ func (s *scraper) LogsStream(ctx context.Context, stream *Stream) error { } ``` -#### Data-dependent limiter processor +#### Open questions + +##### Middleware implementation details + +Details are +important. [#12700](https://github.com/open-telemetry/opentelemetry-collector/pull/12700) +contained a `limitermiddleware` implementation which was a middleware +that called a limiter for HTTP and gRPC. Roughly the same code will be +used, and the details will come out. + +##### Provider options An `Option` type has been added as a placeholder in the provider -interfaces to support adding this feature. **NOTE: This is not -implemented.** +interfaces. **NOTE: No options are implemented.** Potential options: -The provider interfaces can be extended to accept a -`map[string]string` that identify limiter instances based on -additional metadata, such as tenant information. Since the limits are -data specific, the limiter will be computed for each request and for -each specific weight key. +- The protocol name +- The signal kind +- The caller's component ID -Limiter implementations would support options, likely assisted by -`limiterhelper` features to configure them, for configuring -metadata-specific limits. +Because the set of each of these is small, it is possible to +pre-compute limiter instances for the cross product of configurations. -```golang -func handleRequest(ctx context.Context, req *Request) error { - // Get a data-specific limiter: - md := metadataFromRequest(req) - lim, err := s.limiterProvider.LimiterWrapper(weightKey, md) - if err != nil { ... } +##### Context-dependent limits + +Client metadata (i.e., headers) may be used in the context to make +limiter decisions. These details are automatically extracted from the +Context passed to `MustDeny`, `Limit`, `Acquire`, and `LimitCall` +functions. No examples are provided. How will limiters configure, for +example, tenant-specific limits? - if err = lim.MustDeny(ctx); err != nil { ... } +##### Data-dependent limits - // Calculate the data and its weight. - data := dataFromReq(req) - weight := getWeight(data) +When a single unit of data contains limits that are assignable to +multiple distinct limiters, one option available to users is to split +requests and add to their context and run them concurrently through +context-dependent limiters. See +[#39199](https://github.com/open-telemetry/opentelemetry-collector-contrib/issues/39199). - return lim.LimitCall(ctx, weight, func(ctx context.Context) error { - return s.nextLogs.ConsumeLogs(ctx, data) - }) +Another option is to add support for non-blocking limit requests. For +example, to apply limits using information derived from the +OpenTelemetry resource, we might do something like this pseudo-code: + +``` +func (p *processor) limitLogs(ctx context.Context, logsData plog.Logs) (plog.Logs, extensionlimiter.ReleaseFunc, error) { + var rels extensionlimiter.ReleaseFuncs + logsData.ResourceLogs().RemoveIf(func(rl plog.ResourceLogs) bool { + md := resourceToMetadata(rl.Resource()) + rel, err := p.nonBlockingLimiter.TryLimitOrAcquire(withMetadata(ctx, md)) + if err != nil { + return false + } + rels = append(rels, rel) + return true + }) + if logsData.ResourceLogs().Len() == 0 { + return logsData, func() {}, processorhelper.ErrSkipProcessingData + } + return logsData, rels.Release, nil +} + +func (p *processor) ConsumeLogs(ctx context.Context, logsData plog.Logs) error { + logsData, release, err = limitLogs(ctx, logsData) + if err != nil { + return err + } + defer release() + return p.nextLogs.ConsumeLogs(ctx, logsData) +} ``` + +Here, the release a new `TryLimitOrAcquire` function abstracts the +form of a non-blocking call to either form of limiter. If the +underyling limiter is a rate limiter, the release function will be a +no-op. From 0fcdf27788a8046d7c936baefc6bf56d9607d4ff Mon Sep 17 00:00:00 2001 From: Joshua MacDonald Date: Thu, 15 May 2025 16:56:12 -0700 Subject: [PATCH 15/47] Checker to BaseLimiter --- .../extensionlimiter/extensionlimiter.go | 26 +++++++++---------- .../extensionlimiter/limiterhelper/checker.go | 14 +++++----- .../limiterhelper/consumer.go | 8 +++--- .../limiterhelper/middleware.go | 6 ++--- .../extensionlimiter/limiterhelper/wrapper.go | 10 +++---- extension/extensionlimiter/rate.go | 4 +-- extension/extensionlimiter/resource.go | 4 +-- 7 files changed, 36 insertions(+), 36 deletions(-) diff --git a/extension/extensionlimiter/extensionlimiter.go b/extension/extensionlimiter/extensionlimiter.go index 04789e79dab4..ef8829c24dd2 100644 --- a/extension/extensionlimiter/extensionlimiter.go +++ b/extension/extensionlimiter/extensionlimiter.go @@ -17,9 +17,9 @@ type Option interface { apply() } -// Checker is for checking when a limit is saturated. This can be +// BaseLimiter is for checking when a limit is saturated. This can be // called prior to the start of work to check for limiter saturation. -type Checker interface { +type BaseLimiter interface { // MustDeny is a request to apply a hard limit. If this // returns non-nil, the caller must not begin new work in this // context. @@ -29,10 +29,10 @@ type Checker interface { // MustDenyFunc is a functional way to build MustDeny functions. type MustDenyFunc func(context.Context) error -// A MustDeny function is a complete Checker. -var _ Checker = MustDenyFunc(nil) +// A MustDeny function is a complete BaseLimiter. +var _ BaseLimiter = MustDenyFunc(nil) -// MustDeny implements Checker. +// MustDeny implements BaseLimiter. func (f MustDenyFunc) MustDeny(ctx context.Context) error { if f == nil { return nil @@ -40,19 +40,19 @@ func (f MustDenyFunc) MustDeny(ctx context.Context) error { return f(ctx) } -// CheckerProvider is an interface to obtain checkers for a group of +// BaseLimiterProvider is an interface to obtain checkers for a group of // weight keys. -type CheckerProvider interface { - // GetChecker returns a checker for a group of weight keys. - GetChecker(...Option) (Checker, error) +type BaseLimiterProvider interface { + // GetBaseLimiter returns a checker for a group of weight keys. + GetBaseLimiter(...Option) (BaseLimiter, error) } -// GetCheckerFunc is a functional way to construct GetChecker +// GetBaseLimiterFunc is a functional way to construct GetBaseLimiter // functions, used in limiter providers. -type GetCheckerFunc func(...Option) (Checker, error) +type GetBaseLimiterFunc func(...Option) (BaseLimiter, error) -// Checker implements CheckerProvider. -func (f GetCheckerFunc) GetChecker(opts ...Option) (Checker, error) { +// BaseLimiter implements BaseLimiterProvider. +func (f GetBaseLimiterFunc) GetBaseLimiter(opts ...Option) (BaseLimiter, error) { if f == nil { return nil, nil } diff --git a/extension/extensionlimiter/limiterhelper/checker.go b/extension/extensionlimiter/limiterhelper/checker.go index fcad04b4c18e..a3023e7196b7 100644 --- a/extension/extensionlimiter/limiterhelper/checker.go +++ b/extension/extensionlimiter/limiterhelper/checker.go @@ -10,13 +10,13 @@ import ( "go.opentelemetry.io/collector/extension/extensionlimiter" ) -// MultiChecker returns MustDeny when any element returns MustDeny. -type MultiChecker []extensionlimiter.Checker +// MultiBaseLimiter returns MustDeny when any element returns MustDeny. +type MultiBaseLimiter []extensionlimiter.BaseLimiter -var _ extensionlimiter.Checker = MultiChecker{} +var _ extensionlimiter.BaseLimiter = MultiBaseLimiter{} -// MustDeny implements Checker. -func (ls MultiChecker) MustDeny(ctx context.Context) error { +// MustDeny implements BaseLimiter. +func (ls MultiBaseLimiter) MustDeny(ctx context.Context) error { var err error for _, lim := range ls { if lim == nil { @@ -27,7 +27,7 @@ func (ls MultiChecker) MustDeny(ctx context.Context) error { return err } -// NeverDeny returns a Checker that never denies. -func NeverDeny() extensionlimiter.Checker { +// NeverDeny returns a BaseLimiter that never denies. +func NeverDeny() extensionlimiter.BaseLimiter { return extensionlimiter.MustDenyFunc(nil) } diff --git a/extension/extensionlimiter/limiterhelper/consumer.go b/extension/extensionlimiter/limiterhelper/consumer.go index c4eb3aa7d564..1962f883e4b9 100644 --- a/extension/extensionlimiter/limiterhelper/consumer.go +++ b/extension/extensionlimiter/limiterhelper/consumer.go @@ -142,16 +142,16 @@ func limitOne[P any, C any]( }, opts...) } -// applyChecker gets a Checker and wraps the pipeline in a MustDeny +// applyBaseLimiter gets a BaseLimiter and wraps the pipeline in a MustDeny // check. -func applyChecker[P any, C any]( +func applyBaseLimiter[P any, C any]( next C, keys extensionlimiter.WeightSet, provider LimiterWrapperProvider, m traits[P, C], opts []consumer.Option, ) (C, error) { - ck, err := provider.GetChecker() + ck, err := provider.GetBaseLimiter() if err != nil { return next, err } @@ -188,7 +188,7 @@ func newLimited[P any, C any]( func(_ P) uint64 { return 1 }) - next, err4 = applyChecker(next, keys, provider, m, opts) + next, err4 = applyBaseLimiter(next, keys, provider, m, opts) return next, errors.Join(err1, err2, err3, err4) } diff --git a/extension/extensionlimiter/limiterhelper/middleware.go b/extension/extensionlimiter/limiterhelper/middleware.go index aabcc175aa3d..a2e317295044 100644 --- a/extension/extensionlimiter/limiterhelper/middleware.go +++ b/extension/extensionlimiter/limiterhelper/middleware.go @@ -109,11 +109,11 @@ var _ LimiterWrapperProvider = MultiLimiterWrapperProvider{} // GetLimiterWrapper implements LimiterWrapperProvider, combining // checkers for all wrappers in a sequence. -func (ps MultiLimiterWrapperProvider) GetChecker(opts ...extensionlimiter.Option) (extensionlimiter.Checker, error) { +func (ps MultiLimiterWrapperProvider) GetBaseLimiter(opts ...extensionlimiter.Option) (extensionlimiter.BaseLimiter, error) { var retErr error - var cks MultiChecker + var cks MultiBaseLimiter for _, provider := range ps { - ck, err := provider.GetChecker(opts...) + ck, err := provider.GetBaseLimiter(opts...) retErr = errors.Join(retErr, err) if ck == nil { continue diff --git a/extension/extensionlimiter/limiterhelper/wrapper.go b/extension/extensionlimiter/limiterhelper/wrapper.go index f3b1e2c7f3ea..5f7048c179a8 100644 --- a/extension/extensionlimiter/limiterhelper/wrapper.go +++ b/extension/extensionlimiter/limiterhelper/wrapper.go @@ -14,7 +14,7 @@ import ( // function call, because for wrapped calls there is no distinction // between rate limiters and resource limiters. type LimiterWrapperProvider interface { - extensionlimiter.CheckerProvider + extensionlimiter.BaseLimiterProvider GetLimiterWrapper(extensionlimiter.WeightKey, ...extensionlimiter.Option) (LimiterWrapper, error) } @@ -32,7 +32,7 @@ func (f GetLimiterWrapperFunc) GetLimiterWrapper(key extensionlimiter.WeightKey, var _ LimiterWrapperProvider = struct { GetLimiterWrapperFunc - extensionlimiter.GetCheckerFunc + extensionlimiter.GetBaseLimiterFunc }{} // LimiterWrapper is a general-purpose interface for limiter consumers @@ -79,14 +79,14 @@ func PassThroughWrapper() LimiterWrapper { // the underlying limter types. type wrapperProvider struct { GetLimiterWrapperFunc - extensionlimiter.GetCheckerFunc + extensionlimiter.GetBaseLimiterFunc } // NewResourceLimiterWrapperProvider constructs a // LimiterWrapperProvider for a resource limiter extension. func NewResourceLimiterWrapperProvider(rp extensionlimiter.ResourceLimiterProvider) LimiterWrapperProvider { return wrapperProvider{ - GetCheckerFunc: rp.GetChecker, + GetBaseLimiterFunc: rp.GetBaseLimiter, GetLimiterWrapperFunc: func(key extensionlimiter.WeightKey, opts ...extensionlimiter.Option) (LimiterWrapper, error) { lim, err := rp.GetResourceLimiter(key, opts...) if err == nil { @@ -108,7 +108,7 @@ func NewResourceLimiterWrapperProvider(rp extensionlimiter.ResourceLimiterProvid // for a rate limiter extension. func NewRateLimiterWrapperProvider(rp extensionlimiter.RateLimiterProvider) LimiterWrapperProvider { return wrapperProvider{ - GetCheckerFunc: rp.GetChecker, + GetBaseLimiterFunc: rp.GetBaseLimiter, GetLimiterWrapperFunc: func(key extensionlimiter.WeightKey, opts ...extensionlimiter.Option) (LimiterWrapper, error) { lim, err := rp.GetRateLimiter(key, opts...) if err == nil { diff --git a/extension/extensionlimiter/rate.go b/extension/extensionlimiter/rate.go index dcc8d1cc4af5..bd506dde1d46 100644 --- a/extension/extensionlimiter/rate.go +++ b/extension/extensionlimiter/rate.go @@ -14,7 +14,7 @@ import ( // Limiters are covered by configmiddleware configuration, which is // able to construct LimiterWrappers from these providers. type RateLimiterProvider interface { - CheckerProvider + BaseLimiterProvider // GetRateLimiter returns a rate limiter for a weight key. GetRateLimiter(WeightKey, ...Option) (RateLimiter, error) @@ -34,7 +34,7 @@ func (f GetRateLimiterFunc) GetRateLimiter(key WeightKey, opts ...Option) (RateL var _ RateLimiterProvider = struct { GetRateLimiterFunc - GetCheckerFunc + GetBaseLimiterFunc }{} // RateLimiter is an interface that an implementation makes available diff --git a/extension/extensionlimiter/resource.go b/extension/extensionlimiter/resource.go index 98ba253accf4..eae95c84d947 100644 --- a/extension/extensionlimiter/resource.go +++ b/extension/extensionlimiter/resource.go @@ -14,7 +14,7 @@ import ( // Limiters are covered by configmiddleware configuration, which // is able to construct LimiterWrappers from these providers. type ResourceLimiterProvider interface { - CheckerProvider + BaseLimiterProvider GetResourceLimiter(WeightKey, ...Option) (ResourceLimiter, error) } @@ -33,7 +33,7 @@ func (f GetResourceLimiterFunc) GetResourceLimiter(key WeightKey, opts ...Option var _ ResourceLimiterProvider = struct { GetResourceLimiterFunc - GetCheckerFunc + GetBaseLimiterFunc }{} // ResourceLimiter is an interface that an implementation makes From 0348677829e9dc6091d75290427fc0ca8d660730 Mon Sep 17 00:00:00 2001 From: Joshua MacDonald Date: Thu, 15 May 2025 16:57:18 -0700 Subject: [PATCH 16/47] create config.go --- extension/extensionlimiter/config.go | 14 ++++++++++++++ extension/extensionlimiter/extensionlimiter.go | 10 ---------- 2 files changed, 14 insertions(+), 10 deletions(-) create mode 100644 extension/extensionlimiter/config.go diff --git a/extension/extensionlimiter/config.go b/extension/extensionlimiter/config.go new file mode 100644 index 000000000000..16ac275b7f5a --- /dev/null +++ b/extension/extensionlimiter/config.go @@ -0,0 +1,14 @@ +// Copyright The OpenTelemetry Authors +// SPDX-License-Identifier: Apache-2.0 + +package extensionlimiter // import "go.opentelemetry.io/collector/extension/extensionlimiter" + +// Option is passed to limiter providers. +// +// NOTE: For data-specific or tenant-specific limits we will extend +// providers with Options and add a Config type, but none are +// supported yet and this PR contains only interfaces, not need for +// options in core repository components. +type Option interface { + apply() +} diff --git a/extension/extensionlimiter/extensionlimiter.go b/extension/extensionlimiter/extensionlimiter.go index ef8829c24dd2..00fd29f81798 100644 --- a/extension/extensionlimiter/extensionlimiter.go +++ b/extension/extensionlimiter/extensionlimiter.go @@ -7,16 +7,6 @@ import ( "context" ) -// Option is passed to limiter providers. -// -// NOTE: For data-specific or tenant-specific limits we will extend -// providers with Options and add a Config type, but none are -// supported yet and this PR contains only interfaces, not need for -// options in core repository components. -type Option interface { - apply() -} - // BaseLimiter is for checking when a limit is saturated. This can be // called prior to the start of work to check for limiter saturation. type BaseLimiter interface { From 144b8660cb45b2c4ff4047ee77b33847c4230bab Mon Sep 17 00:00:00 2001 From: Joshua MacDonald Date: Thu, 15 May 2025 17:00:12 -0700 Subject: [PATCH 17/47] weightset --- .../limiterhelper/consumer.go | 17 +++++++------ extension/extensionlimiter/weight.go | 25 ++++++------------- 2 files changed, 16 insertions(+), 26 deletions(-) diff --git a/extension/extensionlimiter/limiterhelper/consumer.go b/extension/extensionlimiter/limiterhelper/consumer.go index 1962f883e4b9..e8e9dac7c969 100644 --- a/extension/extensionlimiter/limiterhelper/consumer.go +++ b/extension/extensionlimiter/limiterhelper/consumer.go @@ -6,6 +6,7 @@ package limiterhelper // import "go.opentelemetry.io/collector/extension/extensi import ( "context" "errors" + "slices" "go.opentelemetry.io/collector/consumer" "go.opentelemetry.io/collector/consumer/xconsumer" @@ -118,14 +119,14 @@ func (profileTraits) consume(ctx context.Context, data pprofile.Profiles, next x // limitOne obtains a LimiterWrapper and applies a single weight limit. func limitOne[P any, C any]( next C, - keys extensionlimiter.WeightSet, + keys []extensionlimiter.WeightKey, provider LimiterWrapperProvider, m traits[P, C], key extensionlimiter.WeightKey, opts []consumer.Option, quantify func(P) uint64, ) (C, error) { - if !keys.Contains(key) { + if !slices.Contains(keys, key) { return next, nil } lim, err := provider.GetLimiterWrapper(key) @@ -146,7 +147,7 @@ func limitOne[P any, C any]( // check. func applyBaseLimiter[P any, C any]( next C, - keys extensionlimiter.WeightSet, + keys []extensionlimiter.WeightKey, provider LimiterWrapperProvider, m traits[P, C], opts []consumer.Option, @@ -166,7 +167,7 @@ func applyBaseLimiter[P any, C any]( // newLimited is signal-generic limiting logic. func newLimited[P any, C any]( next C, - keys extensionlimiter.WeightSet, + keys []extensionlimiter.WeightKey, provider LimiterWrapperProvider, m traits[P, C], opts ...consumer.Option, @@ -193,25 +194,25 @@ func newLimited[P any, C any]( } // NewLimitedTraces applies a limiter using the provider over keys before calling next. -func NewLimitedTraces(next consumer.Traces, keys extensionlimiter.WeightSet, provider LimiterWrapperProvider) (consumer.Traces, error) { +func NewLimitedTraces(next consumer.Traces, keys []extensionlimiter.WeightKey, provider LimiterWrapperProvider) (consumer.Traces, error) { return newLimited(next, keys, provider, traceTraits{}, consumer.WithCapabilities(next.Capabilities())) } // NewLimitedLogs applies a limiter using the provider over keys before calling next. -func NewLimitedLogs(next consumer.Logs, keys extensionlimiter.WeightSet, provider LimiterWrapperProvider) (consumer.Logs, error) { +func NewLimitedLogs(next consumer.Logs, keys []extensionlimiter.WeightKey, provider LimiterWrapperProvider) (consumer.Logs, error) { return newLimited(next, keys, provider, logTraits{}, consumer.WithCapabilities(next.Capabilities())) } // NewLimitedMetrics applies a limiter using the provider over keys before calling next. -func NewLimitedMetrics(next consumer.Metrics, keys extensionlimiter.WeightSet, provider LimiterWrapperProvider) (consumer.Metrics, error) { +func NewLimitedMetrics(next consumer.Metrics, keys []extensionlimiter.WeightKey, provider LimiterWrapperProvider) (consumer.Metrics, error) { return newLimited(next, keys, provider, metricTraits{}, consumer.WithCapabilities(next.Capabilities())) } // NewLimitedProfiles applies a limiter using the provider over keys before calling next. -func NewLimitedProfiles(next xconsumer.Profiles, keys extensionlimiter.WeightSet, provider LimiterWrapperProvider) (xconsumer.Profiles, error) { +func NewLimitedProfiles(next xconsumer.Profiles, keys []extensionlimiter.WeightKey, provider LimiterWrapperProvider) (xconsumer.Profiles, error) { return newLimited(next, keys, provider, profileTraits{}, consumer.WithCapabilities(next.Capabilities())) } diff --git a/extension/extensionlimiter/weight.go b/extension/extensionlimiter/weight.go index 28fcfcf0b9b7..16d5c8c4ec9d 100644 --- a/extension/extensionlimiter/weight.go +++ b/extension/extensionlimiter/weight.go @@ -3,7 +3,7 @@ package extensionlimiter // import "go.opentelemetry.io/collector/extension/extensionlimiter" -import "slices" // WeightKey is an enum type for common rate limits. The +// WeightKey is an enum type for common rate limits. The // StandardAllKeys, StandardMiddlewareKeys, and // StandardNotMiddlewareKeys methods return the list of middleware // keys that can be automatically configured through middleware and @@ -38,21 +38,10 @@ const ( WeightKeyMemorySize WeightKey = "memory_size" ) -// WeightSet are a group of weights to be tested. The purpose of this -// type is to be explicit about a group of weights that have to be -// checked at a certain stage. The receiver and middleware can both -// be responsible for applying limits, and this type helps ensure -// limits are applied only across cooperating sub-components. -type WeightSet []WeightKey - -func (ws WeightSet) Contains(w WeightKey) bool { - return slices.Contains(ws, w) -} - // StandardAllKeys is all the keys that can be automatically // implemented by middleware and/or limiterhelper. -func StandardAllKeys() WeightSet { - return WeightSet{ +func StandardAllKeys() []WeightKey { + return []WeightKey{ WeightKeyNetworkBytes, WeightKeyRequestCount, WeightKeyRequestItems, @@ -64,8 +53,8 @@ func StandardAllKeys() WeightSet { // protocols that support it. Receivers should be careful not to // re-apply these limits, especially not to twice-limit by // WeightKeyRequestItems. -func StandardMiddlewareKeys() WeightSet { - return WeightSet{ +func StandardMiddlewareKeys() []WeightKey { + return []WeightKey{ WeightKeyNetworkBytes, WeightKeyRequestCount, } @@ -74,8 +63,8 @@ func StandardMiddlewareKeys() WeightSet { // StandardNotMiddlewareKeys are the keys that are typically not // handled through middlware because they are protocol specific and // generally easier to handle after the input has become pdata. -func StandardNotMiddlewareKeys() WeightSet { - return WeightSet{ +func StandardNotMiddlewareKeys() []WeightKey { + return []WeightKey{ WeightKeyRequestItems, WeightKeyMemorySize, } From 3edd1d875e8d3512c2d292e0513dd8859a8c0f3f Mon Sep 17 00:00:00 2001 From: Joshua MacDonald Date: Fri, 16 May 2025 12:54:33 -0700 Subject: [PATCH 18/47] add non-blocking interface resembling x/time/rate --- .../extensionlimiter/limiterhelper/wrapper.go | 4 +- extension/extensionlimiter/rate.go | 86 ++++++++++++-- extension/extensionlimiter/resource.go | 111 ++++++++++++++---- 3 files changed, 165 insertions(+), 36 deletions(-) diff --git a/extension/extensionlimiter/limiterhelper/wrapper.go b/extension/extensionlimiter/limiterhelper/wrapper.go index 5f7048c179a8..9b3702f2ab03 100644 --- a/extension/extensionlimiter/limiterhelper/wrapper.go +++ b/extension/extensionlimiter/limiterhelper/wrapper.go @@ -93,7 +93,7 @@ func NewResourceLimiterWrapperProvider(rp extensionlimiter.ResourceLimiterProvid return nil, err } return LimiterWrapperFunc(func(ctx context.Context, value uint64, call func(context.Context) error) error { - release, err := lim.Acquire(ctx, value) + release, err := lim.WaitForResource(ctx, value) if err != nil { return err } @@ -115,7 +115,7 @@ func NewRateLimiterWrapperProvider(rp extensionlimiter.RateLimiterProvider) Limi return nil, err } return LimiterWrapperFunc(func(ctx context.Context, value uint64, call func(context.Context) error) error { - if err := lim.Limit(ctx, value); err != nil { + if err := lim.WaitForRate(ctx, value); err != nil { return err } return call(ctx) diff --git a/extension/extensionlimiter/rate.go b/extension/extensionlimiter/rate.go index bd506dde1d46..e842204fdf37 100644 --- a/extension/extensionlimiter/rate.go +++ b/extension/extensionlimiter/rate.go @@ -5,6 +5,7 @@ package extensionlimiter // import "go.opentelemetry.io/collector/extension/exte import ( "context" + "time" ) // RateLimiterProvider is a provider for rate limiters. @@ -49,24 +50,87 @@ var _ RateLimiterProvider = struct { // // See the README for more recommendations. type RateLimiter interface { - // Limit attempts to apply rate limiting with the provided - // weight, based on the key that was given to the provider. + // ReserveRate is modeled on pkg.go.dev/golang.org/x/time/rate#Limiter.ReserveN // - // This is expected to block the caller until the weight can - // be admitted, or when the limit is completely saturated, - // limiters may also return immediate errors. - Limit(ctx context.Context, value uint64) error + // This is a non-blocking interface; use this interface for + // callers that cannot be blocked but will instead schedule a + // resume after Delay(). The context is provided for access to + // instrumentation and client metadata; the Context deadline + // is not used, should be considered by the caller. + ReserveRate(context.Context, uint64) (RateReservation, error) + + // WaitForRate is modeled on pkg.go.dev/golang.org/x/time/rate#Limiter.WaitN + // + // This is a blocking interface. Use this interface for + // callers that are constrained by a Context deadline, which + // will be incorporated into the limiter decision. + WaitForRate(context.Context, uint64) error +} + +// A rate limiter can be made up of two functions. +var _ RateLimiter = struct { + ReserveRateFunc + WaitForRateFunc +}{} + +// RateReservation is modeled on pkg.go.dev/golang.org/x/time/rate#Reservation +type RateReservation interface { + // WaitTime returns the duration until this reservation may + // proceed. A typical implementation uses Reservation.DelayFrom(time.Now()), + // and callers typically/ use time.After or time.Timer to implement a delay. + WaitTime() time.Duration + + // Cancel cancels the reservation before it is used. A typical + // implementation uses Reservation.CancelAt(time.Now()). + Cancel() } -// LimitFunc is a functional way to construct Limit functions. -type LimitFunc func(ctx context.Context, value uint64) error +// ReserveRateFunc is a functional way to construct ReserveRate functions. +type ReserveRateFunc func(context.Context, uint64) (RateReservation, error) -// Limit implements part of the RateLimiter interface. -func (f LimitFunc) Limit(ctx context.Context, value uint64) error { +// Reserve implements part of the RateReserveer interface. +func (f ReserveRateFunc) ReserveRate(ctx context.Context, value uint64) (RateReservation, error) { + if f == nil { + return nil, nil + } + return f(ctx, value) +} + +// WaitForRateFunc is a functional way to construct WaitForRate functions. +type WaitForRateFunc func(context.Context, uint64) error + +// Reserve implements part of the RateReserveer interface. +func (f WaitForRateFunc) WaitForRate(ctx context.Context, value uint64) error { if f == nil { return nil } return f(ctx, value) } -var _ RateLimiter = LimitFunc(nil) +// WaitTimeFunc is a functional way to construct WaitTime functions. +type WaitTimeFunc func() time.Duration + +// WaitTime implements part of Reservation. +func (f WaitTimeFunc) WaitTime() time.Duration { + if f == nil { + return 0 + } + return f.WaitTime() +} + +// CancelFunc is a functional way to construct Cancel functions. +type CancelFunc func() + +// Reserve implements part of the RateReserveer interface. +func (f CancelFunc) Cancel() { + if f == nil { + return + } + f.Cancel() +} + +// A rate limiter can be made up of three functions. +var _ RateReservation = struct { + WaitTimeFunc + CancelFunc +}{} diff --git a/extension/extensionlimiter/resource.go b/extension/extensionlimiter/resource.go index eae95c84d947..97942cf191ab 100644 --- a/extension/extensionlimiter/resource.go +++ b/extension/extensionlimiter/resource.go @@ -50,40 +50,105 @@ var _ ResourceLimiterProvider = struct { // // See the README for more recommendations. type ResourceLimiter interface { - // Acquire attempts to acquire a quantified resource with the - // provided weight, based on the key that was given to the - // provider. The caller has these options: + // ReserveRate is modeled on pkg.go.dev/golang.org/x/time/rate#Limiter.ReserveN, + // without the time dimension. // - // - Accept and let the request proceed by returning a release func and a nil error - // - Fail and return a non-nil error and a nil release func - // - Block until the resource becomes available, then accept - // - Block until the context times out, return the error. - // - // See the README for more recommendations. + // This is a non-blocking interface; use this interface for + // callers that cannot be blocked but will instead schedule a + // resume after DelayFrom(). The context is provided for + // access to instrumentation and client metadata; the Context + // deadline is not used. + ReserveResource(context.Context, uint64) (ResourceReservation, error) + + // WaitForRate is modeled on pkg.go.dev/golang.org/x/time/rate#Limiter.WaitN, + // without the time dimension. // - // On success, it returns a ReleaseFunc that should be called - // after the resources is no longer in use. - Acquire(ctx context.Context, value uint64) (ReleaseFunc, error) + // This is a blocking interface. Use this interface for + // callers that are constrained by a Context deadline, which + // will be incorporated into the limiter decision. + WaitForResource(context.Context, uint64) (ReleaseFunc, error) } +// ResourceReservation is modeled on pkg.go.dev/golang.org/x/time/rate#Reservation +// without the time dimension. +type ResourceReservation interface { + // Delay returns a channel that will be closed when the + // reservation request is granted. This resembles a context + // Done channel. + Delay() <-chan struct{} + + // Release is called after finishing with the reservation, + // whether the delay has been reached or not. + Release() +} + +var _ ResourceReservation = struct { + DelayFunc + ReleaseFunc +}{} + // ReleaseFunc is called when resources have been released after use. // -// RelaseFunc values are never nil values, even in the error case, for -// safety. Users may unconditionally defer these. -// -// Implementations are not required to call a release func after -// Acquire(0) is called, since there is nothing to release. +// RelaseFunc values are never nil values, even in the error case for +// safety. Users typically will immediately defer a call to this. type ReleaseFunc func() -// AcquireFunc is a functional way to construct Acquire functions. -type AcquireFunc func(ctx context.Context, value uint64) (ReleaseFunc, error) +// Release calls this function. +func (f ReleaseFunc) Release() { + if f == nil { + return + } + f() +} + +// DelayFunc returns a channel that is closed when the request is +// permitted to go ahead. +type DelayFunc func() <-chan struct{} -// Acquire implements part of ResourceLimiter. -func (f AcquireFunc) Acquire(ctx context.Context, value uint64) (ReleaseFunc, error) { +// Delay calls this function. +func (f DelayFunc) Delay() <-chan struct{} { if f == nil { - return func() {}, nil + return immediateChan + } + return f() +} + +// immediateChan is a singleton channel, already closed, used when a DelayFunc is nil. +var immediateChan = func() <-chan struct{} { + ic := make(chan struct{}) + close(ic) + return ic +}() + +// ReserveResourceFunc is a functional way to construct ReserveResource interface methods. +type ReserveResourceFunc func(ctx context.Context, value uint64) (ResourceReservation, error) + +// ReserveResource implements a ReserveResource interface method. +func (f ReserveResourceFunc) ReserveResource(ctx context.Context, value uint64) (ResourceReservation, error) { + if f == nil { + return struct { + DelayFunc + ReleaseFunc + }{ + DelayFunc(nil), + ReleaseFunc(nil), + }, nil + } + return f(ctx, value) +} + +// WaitForResourceFunc is a functional way to construct WaitForResource interface methods. +type WaitForResourceFunc func(context.Context, uint64) (ReleaseFunc, error) + +// WaitForResource implements a WaitForResource interface method. +func (f WaitForResourceFunc) WaitForResource(ctx context.Context, value uint64) (ReleaseFunc, error) { + if f == nil { + return ReleaseFunc(nil), nil } return f(ctx, value) } -var _ ResourceLimiter = AcquireFunc(nil) +var _ ResourceLimiter = struct { + ReserveResourceFunc + WaitForResourceFunc +}{} From 785e6656cfdcbfb21e5fb23d90028fe1b3a1c6d8 Mon Sep 17 00:00:00 2001 From: Joshua MacDonald Date: Fri, 16 May 2025 17:35:12 -0700 Subject: [PATCH 19/47] http --- extension/extensionlimiter/go.mod | 5 +- .../extensionlimiter/limiterhelper/base.go | 66 +++++++ .../extensionlimiter/limiterhelper/checker.go | 33 ---- .../limiterhelper/consumer.go | 3 +- .../limiterhelper/grpc/grpclimiter.go | 185 ++++++++++++++++++ .../limiterhelper/http/httplimiter.go | 124 ++++++++++++ .../limiterhelper/middleware.go | 77 +++++--- .../extensionlimiter/limiterhelper/wrapper.go | 30 ++- 8 files changed, 447 insertions(+), 76 deletions(-) create mode 100644 extension/extensionlimiter/limiterhelper/base.go delete mode 100644 extension/extensionlimiter/limiterhelper/checker.go create mode 100644 extension/extensionlimiter/limiterhelper/grpc/grpclimiter.go create mode 100644 extension/extensionlimiter/limiterhelper/http/httplimiter.go diff --git a/extension/extensionlimiter/go.mod b/extension/extensionlimiter/go.mod index 76fc85c82170..8262370481fe 100644 --- a/extension/extensionlimiter/go.mod +++ b/extension/extensionlimiter/go.mod @@ -8,8 +8,11 @@ require ( go.opentelemetry.io/collector/consumer v1.32.0 go.opentelemetry.io/collector/consumer/xconsumer v0.0.0-00010101000000-000000000000 go.opentelemetry.io/collector/extension v1.32.0 + go.opentelemetry.io/collector/extension/extensionmiddleware v1.30.0 + go.opentelemetry.io/collector/extension/extensionmiddleware/extensionmiddlewaretest v0.126.0 go.opentelemetry.io/collector/pdata v1.32.0 go.opentelemetry.io/collector/pdata/pprofile v0.126.0 + go.uber.org/multierr v1.11.0 ) require ( @@ -22,7 +25,6 @@ require ( github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect github.com/modern-go/reflect2 v1.0.2 // indirect go.opentelemetry.io/auto/sdk v1.1.0 // indirect - go.opentelemetry.io/collector/extension/extensionmiddleware v1.30.0 // indirect go.opentelemetry.io/collector/featuregate v1.32.0 // indirect go.opentelemetry.io/collector/internal/telemetry v0.126.0 // indirect go.opentelemetry.io/contrib/bridges/otelzap v0.10.0 // indirect @@ -31,7 +33,6 @@ require ( go.opentelemetry.io/otel/metric v1.35.0 // indirect go.opentelemetry.io/otel/sdk v1.35.0 // indirect go.opentelemetry.io/otel/trace v1.35.0 // indirect - go.uber.org/multierr v1.11.0 // indirect go.uber.org/zap v1.27.0 // indirect golang.org/x/net v0.39.0 // indirect golang.org/x/sys v0.32.0 // indirect diff --git a/extension/extensionlimiter/limiterhelper/base.go b/extension/extensionlimiter/limiterhelper/base.go new file mode 100644 index 000000000000..1428e0b299d4 --- /dev/null +++ b/extension/extensionlimiter/limiterhelper/base.go @@ -0,0 +1,66 @@ +// Copyright The OpenTelemetry Authors +// SPDX-License-Identifier: Apache-2.0 + +package limiterhelper // import "go.opentelemetry.io/collector/extension/extensionlimiter/limiterhelper" + +import ( + "context" + "errors" + + "go.opentelemetry.io/collector/extension/extensionlimiter" +) + +// MultiBaseLimiter returns MustDeny when any element returns MustDeny. +type MultiBaseLimiter []extensionlimiter.BaseLimiter + +var _ extensionlimiter.BaseLimiter = MultiBaseLimiter{} + +// MustDeny implements BaseLimiter. +func (ls MultiBaseLimiter) MustDeny(ctx context.Context) error { + var err error + for _, lim := range ls { + if lim == nil { + continue + } + err = errors.Join(err, lim.MustDeny(ctx)) + } + return err +} + +// BaseToRateLimiterProvider allows a base limiter to act as a rate +// limiter. This allows a base limiter to apply to individual Read() +// calls. +func BaseToRateLimiterProvider(blimp extensionlimiter.BaseLimiterProvider) (extensionlimiter.RateLimiterProvider, error) { + return struct { + extensionlimiter.GetBaseLimiterFunc + extensionlimiter.GetRateLimiterFunc + }{ + blimp.GetBaseLimiter, + func(_ extensionlimiter.WeightKey, opts ...extensionlimiter.Option) (extensionlimiter.RateLimiter, error) { + base, err := blimp.GetBaseLimiter(opts...) + if err != nil { + return nil, err + } + return struct { + extensionlimiter.ReserveRateFunc + extensionlimiter.WaitForRateFunc + }{ + func(ctx context.Context, _ uint64) (extensionlimiter.RateReservation, error) { + if err := base.MustDeny(ctx); err != nil { + return nil, err + } + return struct { + extensionlimiter.WaitTimeFunc + extensionlimiter.CancelFunc + }{ + extensionlimiter.WaitTimeFunc(nil), + extensionlimiter.CancelFunc(nil), + }, nil + }, + func(ctx context.Context, _ uint64) error { + return base.MustDeny(ctx) + }, + }, nil + }, + }, nil +} diff --git a/extension/extensionlimiter/limiterhelper/checker.go b/extension/extensionlimiter/limiterhelper/checker.go deleted file mode 100644 index a3023e7196b7..000000000000 --- a/extension/extensionlimiter/limiterhelper/checker.go +++ /dev/null @@ -1,33 +0,0 @@ -// Copyright The OpenTelemetry Authors -// SPDX-License-Identifier: Apache-2.0 - -package limiterhelper // import "go.opentelemetry.io/collector/extension/extensionlimiter/limiterhelper" - -import ( - "context" - "errors" - - "go.opentelemetry.io/collector/extension/extensionlimiter" -) - -// MultiBaseLimiter returns MustDeny when any element returns MustDeny. -type MultiBaseLimiter []extensionlimiter.BaseLimiter - -var _ extensionlimiter.BaseLimiter = MultiBaseLimiter{} - -// MustDeny implements BaseLimiter. -func (ls MultiBaseLimiter) MustDeny(ctx context.Context) error { - var err error - for _, lim := range ls { - if lim == nil { - continue - } - err = errors.Join(err, lim.MustDeny(ctx)) - } - return err -} - -// NeverDeny returns a BaseLimiter that never denies. -func NeverDeny() extensionlimiter.BaseLimiter { - return extensionlimiter.MustDenyFunc(nil) -} diff --git a/extension/extensionlimiter/limiterhelper/consumer.go b/extension/extensionlimiter/limiterhelper/consumer.go index e8e9dac7c969..25b811d0146f 100644 --- a/extension/extensionlimiter/limiterhelper/consumer.go +++ b/extension/extensionlimiter/limiterhelper/consumer.go @@ -147,7 +147,6 @@ func limitOne[P any, C any]( // check. func applyBaseLimiter[P any, C any]( next C, - keys []extensionlimiter.WeightKey, provider LimiterWrapperProvider, m traits[P, C], opts []consumer.Option, @@ -189,7 +188,7 @@ func newLimited[P any, C any]( func(_ P) uint64 { return 1 }) - next, err4 = applyBaseLimiter(next, keys, provider, m, opts) + next, err4 = applyBaseLimiter(next, provider, m, opts) return next, errors.Join(err1, err2, err3, err4) } diff --git a/extension/extensionlimiter/limiterhelper/grpc/grpclimiter.go b/extension/extensionlimiter/limiterhelper/grpc/grpclimiter.go new file mode 100644 index 000000000000..b1c99386da79 --- /dev/null +++ b/extension/extensionlimiter/limiterhelper/grpc/grpclimiter.go @@ -0,0 +1,185 @@ +package grpclimiter + +import ( + "context" + + "go.opentelemetry.io/collector/extension/extensionlimiter" + "google.golang.org/grpc" + "google.golang.org/grpc/codes" + "google.golang.org/grpc/stats" + "google.golang.org/grpc/status" +) + +func (lm *limiterMiddleware) GetGRPCClientOptions() (options []grpc.DialOption, _ error) { + if resourceLimiter := lm.provider.ResourceLimiter(extensionlimiter.WeightKeyRequestCount); resourceLimiter != nil { + options = append(options, grpc.WithUnaryInterceptor( + func( + ctx context.Context, + method string, + req, reply any, + cc *grpc.ClientConn, + invoker grpc.UnaryInvoker, + opts ...grpc.CallOption, + ) error { + release, err := resourceLimiter.Acquire(ctx, 1) + defer release() + if err != nil { + return status.Errorf(codes.ResourceExhausted, "limit exceeded: %v", err) + } + return invoker(ctx, method, req, reply, cc, opts...) + }), + grpc.WithStreamInterceptor( + func( + ctx context.Context, + desc *grpc.StreamDesc, + cc *grpc.ClientConn, + method string, + streamer grpc.Streamer, + opts ...grpc.CallOption, + ) (grpc.ClientStream, error) { + cstream, err := streamer(ctx, desc, cc, method, opts...) + if err != nil { + return nil, err + } + return lm.wrapClientStream(cstream, method, resourceLimiter), nil + }), + ) + } + if rateLimiter := lm.provider.RateLimiter(extensionlimiter.WeightKeyNetworkBytes); rateLimiter != nil { + options = append(options, grpc.WithStatsHandler( + &limiterStatsHandler{ + rateLimiter: rateLimiter, + isClient: true, + })) + } + return options, nil +} + +// ServerUnaryInterceptor returns a gRPC interceptor for unary server calls. +func (lm *limiterMiddleware) GetGRPCServerOptions() (options []grpc.ServerOption, _ error) { + if resourceLimiter := lm.provider.ResourceLimiter(extensionlimiter.WeightKeyRequestCount); resourceLimiter != nil { + options = append(options, grpc.ChainUnaryInterceptor( + // The unary resource case + func( + ctx context.Context, + req any, + info *grpc.UnaryServerInfo, + handler grpc.UnaryHandler, + ) (any, error) { + release, err := resourceLimiter.Acquire(ctx, 1) + defer release() + if err != nil { + return nil, status.Errorf(codes.ResourceExhausted, "limit exceeded: %v", err) + } + return handler(ctx, req) + }), grpc.ChainStreamInterceptor( + // The stream resource case + func( + srv interface{}, + ss grpc.ServerStream, + info *grpc.StreamServerInfo, + handler grpc.StreamHandler, + ) error { + return handler(srv, lm.wrapServerStream(ss, info, resourceLimiter)) + }), + ) + } + if rateLimiter := lm.provider.RateLimiter(extensionlimiter.WeightKeyNetworkBytes); rateLimiter != nil { + options = append(options, grpc.StatsHandler( + &limiterStatsHandler{ + rateLimiter: rateLimiter, + isClient: false, + })) + } + + return options, nil +} + +// limiterStatsHandler implements the stats.Handler interface for rate limiting. +type limiterStatsHandler struct { + rateLimiter extensionlimiter.RateLimiter + isClient bool +} + +func (h *limiterStatsHandler) TagRPC(ctx context.Context, _ *stats.RPCTagInfo) context.Context { + return ctx +} + +func (h *limiterStatsHandler) HandleRPC(ctx context.Context, s stats.RPCStats) { + // Check for payload messages to apply network byte rate limiting + var wireBytes int + switch payload := s.(type) { + case *stats.InPayload: + // Server receiving payload (or client receiving response) + if !h.isClient { + wireBytes = payload.WireLength + } + case *stats.OutPayload: + // Client sending payload (or server sending response) + if h.isClient { + wireBytes = payload.WireLength + } + default: + // Not a payload message, no rate limiting to apply + return + } + + if wireBytes == 0 { + return + } + // Apply rate limiting based on network bytes + h.rateLimiter.Limit(ctx, uint64(wireBytes)) +} + +func (h *limiterStatsHandler) TagConn(ctx context.Context, _ *stats.ConnTagInfo) context.Context { + return ctx +} + +func (h *limiterStatsHandler) HandleConn(ctx context.Context, _ stats.ConnStats) { +} + +type serverStream struct { + grpc.ServerStream + limiter extensionlimiter.ResourceLimiter +} + +// RecvMsg applies rate limiting to server stream message receiving. +func (s *serverStream) RecvMsg(m any) error { + release, err := s.limiter.Acquire(s.Context(), 1) + defer release() + if err != nil { + return status.Errorf(codes.ResourceExhausted, "limit exceeded: %v", err) + } + return s.ServerStream.RecvMsg(m) +} + +// wrapServerStream wraps a gRPC server stream with rate limiting. +func (lm *limiterMiddleware) wrapServerStream(ss grpc.ServerStream, _ *grpc.StreamServerInfo, limiter extensionlimiter.ResourceLimiter) grpc.ServerStream { + return &serverStream{ + ServerStream: ss, + limiter: limiter, + } +} + +type clientStream struct { + grpc.ClientStream + limiter extensionlimiter.ResourceLimiter +} + +// SendMsg applies rate limiting to client stream message sending. +func (s *clientStream) SendMsg(m any) error { + release, err := s.limiter.Acquire(s.Context(), 1) + defer release() + if err != nil { + return status.Errorf(codes.ResourceExhausted, "limit exceeded: %v", err) + } + return s.ClientStream.SendMsg(m) +} + +// wrapClientStream wraps a gRPC client stream with rate limiting. +func (lm *limiterMiddleware) wrapClientStream(cs grpc.ClientStream, _ string, limiter extensionlimiter.ResourceLimiter) grpc.ClientStream { + return &clientStream{ + ClientStream: cs, + limiter: limiter, + } +} diff --git a/extension/extensionlimiter/limiterhelper/http/httplimiter.go b/extension/extensionlimiter/limiterhelper/http/httplimiter.go new file mode 100644 index 000000000000..208d35aab073 --- /dev/null +++ b/extension/extensionlimiter/limiterhelper/http/httplimiter.go @@ -0,0 +1,124 @@ +package httplimiter + +import ( + "context" + "io" + "net/http" + + "go.opentelemetry.io/collector/component" + "go.opentelemetry.io/collector/config/configmiddleware" + "go.opentelemetry.io/collector/extension/extensionlimiter" + "go.opentelemetry.io/collector/extension/extensionlimiter/limiterhelper" + "go.opentelemetry.io/collector/extension/extensionmiddleware" + "go.opentelemetry.io/collector/extension/extensionmiddleware/extensionmiddlewaretest" + "go.uber.org/multierr" +) + +func NewHTTPClientLimiter(host component.Host, middleware configmiddleware.Config) (extensionmiddleware.HTTPClient, error) { + wp, err1 := limiterhelper.MiddlewareToLimiterWrapperProvider(host, middleware) + rp, err2 := limiterhelper.MiddlewareToRateLimiterProvider(host, middleware) + if err := multierr.Append(err1, err2); err != nil { + return nil, err + } + requestLimiter, err3 := wp.GetLimiterWrapper(extensionlimiter.WeightKeyRequestCount) + bytesLimiter, err4 := rp.GetRateLimiter(extensionlimiter.WeightKeyNetworkBytes) + if err := multierr.Append(err3, err4); err != nil { + return nil, err + } + + roundtrip := func(base http.RoundTripper) (http.RoundTripper, error) { + + return extensionmiddlewaretest.RoundTripperFunc(func(req *http.Request) (*http.Response, error) { + if requestLimiter == nil && bytesLimiter == nil { + // If no limiters are configured, return the base round tripper + return base.RoundTrip(req) + } + + var resp *http.Response + err := requestLimiter.LimitCall( + req.Context(), + 1, + func(_ context.Context) error { + if bytesLimiter != nil && req.Body != nil && req.Body != http.NoBody { + // If bytes are limited, create a limited ReadCloser body. + req.Body = &rateLimitedBody{ + body: req.Body, + rateLimiter: bytesLimiter, + ctx: req.Context(), + } + } + var err error + resp, err = base.RoundTrip(req) + return err + }) + return resp, err + }), multierr.Append(err1, err2) + } + return extensionmiddleware.GetHTTPRoundTripperFunc(roundtrip), nil +} + +// rateLimitedBody wraps an http.Request.Body to track bytes and call the rate limiter +type rateLimitedBody struct { + body io.ReadCloser + rateLimiter extensionlimiter.RateLimiter + ctx context.Context +} + +var _ io.ReadCloser = &rateLimitedBody{} + +// Read implements io.Reader interface, counting bytes as they are read +func (rb *rateLimitedBody) Read(p []byte) (n int, err error) { + n, err = rb.body.Read(p) + if n > 0 { + // Apply rate limiting based on network bytes after they are read + limitErr := rb.rateLimiter.WaitForRate(rb.ctx, uint64(n)) + if limitErr != nil { + // If the rate limiter rejects the bytes, return the error + return n, limitErr // TODO: How to return HTTP 429? + } + } + return n, err +} + +// Close implements io.Closer interface +func (rb *rateLimitedBody) Close() error { + return rb.body.Close() +} + +func NewHTTPServerLimiter(host component.Host, middleware configmiddleware.Config) (extensionmiddleware.HTTPServer, error) { + wp, err1 := limiterhelper.MiddlewareToLimiterWrapperProvider(host, middleware) + rp, err2 := limiterhelper.MiddlewareToRateLimiterProvider(host, middleware) + if err := multierr.Append(err1, err2); err != nil { + return nil, err + } + requestLimiter, err3 := wp.GetLimiterWrapper(extensionlimiter.WeightKeyRequestCount) + bytesLimiter, err4 := rp.GetRateLimiter(extensionlimiter.WeightKeyNetworkBytes) + if err := multierr.Append(err3, err4); err != nil { + return nil, err + } + + handler := func(base http.Handler) (http.Handler, error) { + if requestLimiter == nil && bytesLimiter == nil { + return base, nil + } + + return http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) { + _ = requestLimiter.LimitCall( + req.Context(), + 1, + func(_ context.Context) error { + if bytesLimiter != nil && req.Body != nil && req.Body != http.NoBody { + // If bytes are limited, create a limited ReadCloser body. + req.Body = &rateLimitedBody{ + body: req.Body, + rateLimiter: bytesLimiter, + ctx: req.Context(), + } + } + base.ServeHTTP(w, req) + return nil + }) + }), nil + } + return extensionmiddleware.GetHTTPHandlerFunc(handler), nil +} diff --git a/extension/extensionlimiter/limiterhelper/middleware.go b/extension/extensionlimiter/limiterhelper/middleware.go index a2e317295044..17efb438aff0 100644 --- a/extension/extensionlimiter/limiterhelper/middleware.go +++ b/extension/extensionlimiter/limiterhelper/middleware.go @@ -15,20 +15,13 @@ import ( ) var ( - ErrNotALimiter = errors.New("middleware is not a limiter") - ErrLimiterConflict = errors.New("limiter implements both rate and resource-limiters") - ErrUnresolvedLimiter = errors.New("could not resolve middleware limiter") + ErrNotALimiter = errors.New("middleware is not a limiter") + ErrNotARateLimiter = errors.New("middleware is not a rate or base limiter") + ErrNotAResourceLimiter = errors.New("middleware is not a resource or base limiter") + ErrLimiterConflict = errors.New("limiter implements both rate and resource-limiters") + ErrUnresolvedLimiter = errors.New("could not resolve middleware limiter") ) -// MiddlewareIsLimiter returns true if a middleware configuration -// represents a valid limiter, returns false for not found or invalid -// cases. If the named extension is found but is not a limiter, -// returns (false, nil). -func MiddlewareIsLimiter(host component.Host, middleware configmiddleware.Config) (bool, error) { - _, ok, err := middlewareIsLimiter(host, middleware) - return ok, err -} - // MiddlewaresToLimiterWrapperProvider constructs a combined limiter // from an ordered list of middlewares. This constructor ignores // middleware configs that are not limiters. @@ -40,7 +33,7 @@ func MiddlewaresToLimiterWrapperProvider(host component.Host, middleware []confi var retErr error var providers []LimiterWrapperProvider for _, mid := range middleware { - ok, err := MiddlewareIsLimiter(host, mid) + _, ok, err := MiddlewareIsLimiter(host, mid) retErr = errors.Join(retErr, err) if !ok { continue @@ -64,42 +57,68 @@ func MiddlewaresToLimiterWrapperProvider(host component.Host, middleware []confi // middleware does not implement exactly one of the limiter // interfaces (i.e., rate or resource). func MiddlewareToLimiterWrapperProvider(host component.Host, middleware configmiddleware.Config) (LimiterWrapperProvider, error) { - ext, ok, err := middlewareIsLimiter(host, middleware) + ext, ok, err := MiddlewareIsLimiter(host, middleware) if err != nil { return nil, err } - if ok { - if lim, ok := ext.(extensionlimiter.ResourceLimiterProvider); ok { - return NewResourceLimiterWrapperProvider(lim), nil - } - if lim, ok := ext.(extensionlimiter.RateLimiterProvider); ok { - return NewRateLimiterWrapperProvider(lim), nil - } + if !ok { + return nil, fmt.Errorf("%w: %s", ErrNotALimiter, middleware.ID) } - return nil, fmt.Errorf("%w: %s", ErrNotALimiter, ext) + if lim, ok := ext.(extensionlimiter.ResourceLimiterProvider); ok { + return NewResourceLimiterWrapperProvider(lim), nil + } + if lim, ok := ext.(extensionlimiter.RateLimiterProvider); ok { + return NewRateLimiterWrapperProvider(lim), nil + } + if lim, ok := ext.(extensionlimiter.BaseLimiterProvider); ok { + return NewBaseLimiterWrapperProvider(lim), nil + } + // This is an internal error. + return nil, fmt.Errorf("%w: %s: unrecognized limiter", ErrNotALimiter, middleware.ID) } -// middlewareIsLimiter applies consistency checks and returns a valid -// limiter extensions. -func middlewareIsLimiter(host component.Host, middleware configmiddleware.Config) (extension.Extension, bool, error) { +// MiddlewareIsLimiter applies consistency checks and returns a valid +// limiter extension of any known kind. +func MiddlewareIsLimiter(host component.Host, middleware configmiddleware.Config) (extension.Extension, bool, error) { exts := host.GetExtensions() ext := exts[middleware.ID] if ext == nil { - return nil, false, fmt.Errorf("%w: %s", ErrUnresolvedLimiter, ext) + return nil, false, fmt.Errorf("%w: %s", ErrUnresolvedLimiter, middleware.ID) } _, isResource := ext.(extensionlimiter.ResourceLimiterProvider) _, isRate := ext.(extensionlimiter.RateLimiterProvider) + _, isBase := ext.(extensionlimiter.BaseLimiterProvider) switch { case isResource && isRate: - return nil, false, fmt.Errorf("%w: %s", ErrLimiterConflict, ext) + return ext, false, fmt.Errorf("%w: %s", ErrLimiterConflict, middleware.ID) case isResource, isRate: return ext, true, nil + case isBase: + return ext, true, nil default: return nil, false, nil } } +// MiddlewareToRateLimiterProvider allows a base limiter to act as a rate +func MiddlewareToRateLimiterProvider(host component.Host, middleware configmiddleware.Config) (extensionlimiter.RateLimiterProvider, error) { + ext, ok, err := MiddlewareIsLimiter(host, middleware) + if err != nil { + return nil, err + } + if !ok { + return nil, fmt.Errorf("%w: %s", ErrNotALimiter, middleware.ID) + } + if rlimp, ok := ext.(extensionlimiter.RateLimiterProvider); ok { + return rlimp, nil + } + if blimp, ok := ext.(extensionlimiter.BaseLimiterProvider); ok { + return BaseToRateLimiterProvider(blimp) + } + return nil, fmt.Errorf("%w: %s", ErrNotARateLimiter, middleware.ID) +} + // MultiLimiterWrapperProvider combines multiple limiter wrappers // providers into a single provider by sequencing wrapped limiters. // Returns errors from the underlying LimiterWrapper() calls, if any. @@ -121,7 +140,7 @@ func (ps MultiLimiterWrapperProvider) GetBaseLimiter(opts ...extensionlimiter.Op cks = append(cks, ck) } if len(cks) == 0 { - return NeverDeny(), retErr + return extensionlimiter.MustDenyFunc(nil), retErr } return cks, retErr } @@ -144,7 +163,7 @@ func (ps MultiLimiterWrapperProvider) GetLimiterWrapper(key extensionlimiter.Wei } if len(lims) == 0 { - return PassThroughWrapper(), nil + return LimiterWrapper(nil), nil } return sequenceLimiters(lims), nil diff --git a/extension/extensionlimiter/limiterhelper/wrapper.go b/extension/extensionlimiter/limiterhelper/wrapper.go index 9b3702f2ab03..fee38f3f22af 100644 --- a/extension/extensionlimiter/limiterhelper/wrapper.go +++ b/extension/extensionlimiter/limiterhelper/wrapper.go @@ -25,7 +25,7 @@ type GetLimiterWrapperFunc func(extensionlimiter.WeightKey, ...extensionlimiter. // GetLimiterWrapper implements LimiterWrapperProvider. func (f GetLimiterWrapperFunc) GetLimiterWrapper(key extensionlimiter.WeightKey, opts ...extensionlimiter.Option) (LimiterWrapper, error) { if f == nil { - return PassThroughWrapper(), nil + return LimiterWrapperFunc(nil), nil } return f(key, opts...) } @@ -70,11 +70,6 @@ func (f LimiterWrapperFunc) LimitCall(ctx context.Context, value uint64, call fu return f(ctx, value, call) } -// PassThroughWrapper returns a LimiterWrapper that imposes no limit. -func PassThroughWrapper() LimiterWrapper { - return LimiterWrapperFunc(nil) -} - // wrapperProvider is a combinator for building wrapper providers from // the underlying limter types. type wrapperProvider struct { @@ -82,6 +77,15 @@ type wrapperProvider struct { extensionlimiter.GetBaseLimiterFunc } +// NewBaseLimiterWrapperProvider constructs a LimiterWrapperProvider +// for a rate limiter extension. +func NewBaseLimiterWrapperProvider(rp extensionlimiter.BaseLimiterProvider) LimiterWrapperProvider { + return wrapperProvider{ + GetBaseLimiterFunc: rp.GetBaseLimiter, + GetLimiterWrapperFunc: GetLimiterWrapperFunc(nil), + } +} + // NewResourceLimiterWrapperProvider constructs a // LimiterWrapperProvider for a resource limiter extension. func NewResourceLimiterWrapperProvider(rp extensionlimiter.ResourceLimiterProvider) LimiterWrapperProvider { @@ -89,9 +93,12 @@ func NewResourceLimiterWrapperProvider(rp extensionlimiter.ResourceLimiterProvid GetBaseLimiterFunc: rp.GetBaseLimiter, GetLimiterWrapperFunc: func(key extensionlimiter.WeightKey, opts ...extensionlimiter.Option) (LimiterWrapper, error) { lim, err := rp.GetResourceLimiter(key, opts...) - if err == nil { + if err != nil { return nil, err } + if lim == nil { + return nil, nil + } return LimiterWrapperFunc(func(ctx context.Context, value uint64, call func(context.Context) error) error { release, err := lim.WaitForResource(ctx, value) if err != nil { @@ -99,7 +106,7 @@ func NewResourceLimiterWrapperProvider(rp extensionlimiter.ResourceLimiterProvid } defer release() return call(ctx) - }), err + }), nil }, } } @@ -111,15 +118,18 @@ func NewRateLimiterWrapperProvider(rp extensionlimiter.RateLimiterProvider) Limi GetBaseLimiterFunc: rp.GetBaseLimiter, GetLimiterWrapperFunc: func(key extensionlimiter.WeightKey, opts ...extensionlimiter.Option) (LimiterWrapper, error) { lim, err := rp.GetRateLimiter(key, opts...) - if err == nil { + if err != nil { return nil, err } + if lim == nil { + return nil, nil + } return LimiterWrapperFunc(func(ctx context.Context, value uint64, call func(context.Context) error) error { if err := lim.WaitForRate(ctx, value); err != nil { return err } return call(ctx) - }), err + }), nil }, } } From 294dbc5e744c21ff12410316c969859ce57c833f Mon Sep 17 00:00:00 2001 From: Joshua MacDonald Date: Fri, 16 May 2025 17:59:32 -0700 Subject: [PATCH 20/47] grpc --- .../limiterhelper/grpc/grpclimiter.go | 145 +++++++++++------- .../limiterhelper/http/httplimiter.go | 5 +- 2 files changed, 89 insertions(+), 61 deletions(-) diff --git a/extension/extensionlimiter/limiterhelper/grpc/grpclimiter.go b/extension/extensionlimiter/limiterhelper/grpc/grpclimiter.go index b1c99386da79..d4b4b918adce 100644 --- a/extension/extensionlimiter/limiterhelper/grpc/grpclimiter.go +++ b/extension/extensionlimiter/limiterhelper/grpc/grpclimiter.go @@ -3,102 +3,132 @@ package grpclimiter import ( "context" + "go.opentelemetry.io/collector/component" + "go.opentelemetry.io/collector/config/configmiddleware" "go.opentelemetry.io/collector/extension/extensionlimiter" + "go.opentelemetry.io/collector/extension/extensionlimiter/limiterhelper" + "go.opentelemetry.io/collector/extension/extensionmiddleware" + "go.uber.org/multierr" "google.golang.org/grpc" - "google.golang.org/grpc/codes" "google.golang.org/grpc/stats" - "google.golang.org/grpc/status" ) -func (lm *limiterMiddleware) GetGRPCClientOptions() (options []grpc.DialOption, _ error) { - if resourceLimiter := lm.provider.ResourceLimiter(extensionlimiter.WeightKeyRequestCount); resourceLimiter != nil { - options = append(options, grpc.WithUnaryInterceptor( +func NewClientLimiter(host component.Host, middleware configmiddleware.Config) (extensionmiddleware.GRPCClient, error) { + wp, err1 := limiterhelper.MiddlewareToLimiterWrapperProvider(host, middleware) + rp, err2 := limiterhelper.MiddlewareToRateLimiterProvider(host, middleware) + if err := multierr.Append(err1, err2); err != nil { + return nil, err + } + requestLimiter, err3 := wp.GetLimiterWrapper(extensionlimiter.WeightKeyRequestCount) + bytesLimiter, err4 := rp.GetRateLimiter(extensionlimiter.WeightKeyNetworkBytes) + if err := multierr.Append(err3, err4); err != nil { + return nil, err + } + + var gopts []grpc.DialOption + if requestLimiter != nil { + gopts = append(gopts, grpc.WithUnaryInterceptor( func( - ctx context.Context, + ctxIn context.Context, method string, req, reply any, cc *grpc.ClientConn, invoker grpc.UnaryInvoker, opts ...grpc.CallOption, ) error { - release, err := resourceLimiter.Acquire(ctx, 1) - defer release() - if err != nil { - return status.Errorf(codes.ResourceExhausted, "limit exceeded: %v", err) - } - return invoker(ctx, method, req, reply, cc, opts...) + return requestLimiter.LimitCall( + ctxIn, 1, + func(ctx context.Context) error { + return invoker(ctx, method, req, reply, cc, opts...) + }) }), grpc.WithStreamInterceptor( func( - ctx context.Context, + ctxIn context.Context, desc *grpc.StreamDesc, cc *grpc.ClientConn, method string, streamer grpc.Streamer, opts ...grpc.CallOption, ) (grpc.ClientStream, error) { - cstream, err := streamer(ctx, desc, cc, method, opts...) + cstream, err := streamer(ctxIn, desc, cc, method, opts...) if err != nil { return nil, err } - return lm.wrapClientStream(cstream, method, resourceLimiter), nil + return wrapClientStream(cstream, method, requestLimiter), nil }), ) } - if rateLimiter := lm.provider.RateLimiter(extensionlimiter.WeightKeyNetworkBytes); rateLimiter != nil { - options = append(options, grpc.WithStatsHandler( + if bytesLimiter != nil { + gopts = append(gopts, grpc.WithStatsHandler( &limiterStatsHandler{ - rateLimiter: rateLimiter, - isClient: true, + bytesLimiter: bytesLimiter, + isClient: true, })) } - return options, nil + return extensionmiddleware.GetGRPCClientOptionsFunc(func() ([]grpc.DialOption, error) { + return gopts, nil + }), nil } -// ServerUnaryInterceptor returns a gRPC interceptor for unary server calls. -func (lm *limiterMiddleware) GetGRPCServerOptions() (options []grpc.ServerOption, _ error) { - if resourceLimiter := lm.provider.ResourceLimiter(extensionlimiter.WeightKeyRequestCount); resourceLimiter != nil { - options = append(options, grpc.ChainUnaryInterceptor( - // The unary resource case +func NewServerLimiter(host component.Host, middleware configmiddleware.Config) (extensionmiddleware.GRPCServer, error) { + wp, err1 := limiterhelper.MiddlewareToLimiterWrapperProvider(host, middleware) + rp, err2 := limiterhelper.MiddlewareToRateLimiterProvider(host, middleware) + if err := multierr.Append(err1, err2); err != nil { + return nil, err + } + requestLimiter, err3 := wp.GetLimiterWrapper(extensionlimiter.WeightKeyRequestCount) + bytesLimiter, err4 := rp.GetRateLimiter(extensionlimiter.WeightKeyNetworkBytes) + if err := multierr.Append(err3, err4); err != nil { + return nil, err + } + + var gopts []grpc.ServerOption + if requestLimiter != nil { + gopts = append(gopts, grpc.ChainUnaryInterceptor( func( - ctx context.Context, + ctxIn context.Context, req any, info *grpc.UnaryServerInfo, handler grpc.UnaryHandler, ) (any, error) { - release, err := resourceLimiter.Acquire(ctx, 1) - defer release() - if err != nil { - return nil, status.Errorf(codes.ResourceExhausted, "limit exceeded: %v", err) - } - return handler(ctx, req) + var resp any + err := requestLimiter.LimitCall( + ctxIn, 1, + func(ctx context.Context) error { + var err error + resp, err = handler(ctx, req) + return err + }) + return resp, err }), grpc.ChainStreamInterceptor( - // The stream resource case func( srv interface{}, ss grpc.ServerStream, info *grpc.StreamServerInfo, handler grpc.StreamHandler, ) error { - return handler(srv, lm.wrapServerStream(ss, info, resourceLimiter)) + return handler(srv, wrapServerStream(ss, info, requestLimiter)) }), ) } - if rateLimiter := lm.provider.RateLimiter(extensionlimiter.WeightKeyNetworkBytes); rateLimiter != nil { - options = append(options, grpc.StatsHandler( + if bytesLimiter != nil { + gopts = append(gopts, grpc.StatsHandler( &limiterStatsHandler{ - rateLimiter: rateLimiter, - isClient: false, + bytesLimiter: bytesLimiter, + isClient: false, })) } - return options, nil + return extensionmiddleware.GetGRPCServerOptionsFunc(func() ([]grpc.ServerOption, error) { + return gopts, nil + }), nil } // limiterStatsHandler implements the stats.Handler interface for rate limiting. type limiterStatsHandler struct { - rateLimiter extensionlimiter.RateLimiter - isClient bool + bytesLimiter extensionlimiter.RateLimiter + isClient bool } func (h *limiterStatsHandler) TagRPC(ctx context.Context, _ *stats.RPCTagInfo) context.Context { @@ -128,7 +158,8 @@ func (h *limiterStatsHandler) HandleRPC(ctx context.Context, s stats.RPCStats) { return } // Apply rate limiting based on network bytes - h.rateLimiter.Limit(ctx, uint64(wireBytes)) + // TODO: How does the limiter break the stream? + _ = h.bytesLimiter.WaitForRate(ctx, uint64(wireBytes)) } func (h *limiterStatsHandler) TagConn(ctx context.Context, _ *stats.ConnTagInfo) context.Context { @@ -140,21 +171,20 @@ func (h *limiterStatsHandler) HandleConn(ctx context.Context, _ stats.ConnStats) type serverStream struct { grpc.ServerStream - limiter extensionlimiter.ResourceLimiter + limiter limiterhelper.LimiterWrapper } // RecvMsg applies rate limiting to server stream message receiving. func (s *serverStream) RecvMsg(m any) error { - release, err := s.limiter.Acquire(s.Context(), 1) - defer release() - if err != nil { - return status.Errorf(codes.ResourceExhausted, "limit exceeded: %v", err) - } - return s.ServerStream.RecvMsg(m) + return s.limiter.LimitCall( + s.Context(), 1, + func(_ context.Context) error { + return s.ServerStream.RecvMsg(m) + }) } // wrapServerStream wraps a gRPC server stream with rate limiting. -func (lm *limiterMiddleware) wrapServerStream(ss grpc.ServerStream, _ *grpc.StreamServerInfo, limiter extensionlimiter.ResourceLimiter) grpc.ServerStream { +func wrapServerStream(ss grpc.ServerStream, _ *grpc.StreamServerInfo, limiter limiterhelper.LimiterWrapper) grpc.ServerStream { return &serverStream{ ServerStream: ss, limiter: limiter, @@ -163,21 +193,20 @@ func (lm *limiterMiddleware) wrapServerStream(ss grpc.ServerStream, _ *grpc.Stre type clientStream struct { grpc.ClientStream - limiter extensionlimiter.ResourceLimiter + limiter limiterhelper.LimiterWrapper } // SendMsg applies rate limiting to client stream message sending. func (s *clientStream) SendMsg(m any) error { - release, err := s.limiter.Acquire(s.Context(), 1) - defer release() - if err != nil { - return status.Errorf(codes.ResourceExhausted, "limit exceeded: %v", err) - } - return s.ClientStream.SendMsg(m) + return s.limiter.LimitCall( + s.Context(), 1, + func(_ context.Context) error { + return s.ClientStream.SendMsg(m) + }) } // wrapClientStream wraps a gRPC client stream with rate limiting. -func (lm *limiterMiddleware) wrapClientStream(cs grpc.ClientStream, _ string, limiter extensionlimiter.ResourceLimiter) grpc.ClientStream { +func wrapClientStream(cs grpc.ClientStream, _ string, limiter limiterhelper.LimiterWrapper) grpc.ClientStream { return &clientStream{ ClientStream: cs, limiter: limiter, diff --git a/extension/extensionlimiter/limiterhelper/http/httplimiter.go b/extension/extensionlimiter/limiterhelper/http/httplimiter.go index 208d35aab073..3bec0ad40e94 100644 --- a/extension/extensionlimiter/limiterhelper/http/httplimiter.go +++ b/extension/extensionlimiter/limiterhelper/http/httplimiter.go @@ -14,7 +14,7 @@ import ( "go.uber.org/multierr" ) -func NewHTTPClientLimiter(host component.Host, middleware configmiddleware.Config) (extensionmiddleware.HTTPClient, error) { +func NewClientLimiter(host component.Host, middleware configmiddleware.Config) (extensionmiddleware.HTTPClient, error) { wp, err1 := limiterhelper.MiddlewareToLimiterWrapperProvider(host, middleware) rp, err2 := limiterhelper.MiddlewareToRateLimiterProvider(host, middleware) if err := multierr.Append(err1, err2); err != nil { @@ -27,7 +27,6 @@ func NewHTTPClientLimiter(host component.Host, middleware configmiddleware.Confi } roundtrip := func(base http.RoundTripper) (http.RoundTripper, error) { - return extensionmiddlewaretest.RoundTripperFunc(func(req *http.Request) (*http.Response, error) { if requestLimiter == nil && bytesLimiter == nil { // If no limiters are configured, return the base round tripper @@ -85,7 +84,7 @@ func (rb *rateLimitedBody) Close() error { return rb.body.Close() } -func NewHTTPServerLimiter(host component.Host, middleware configmiddleware.Config) (extensionmiddleware.HTTPServer, error) { +func NewServerLimiter(host component.Host, middleware configmiddleware.Config) (extensionmiddleware.HTTPServer, error) { wp, err1 := limiterhelper.MiddlewareToLimiterWrapperProvider(host, middleware) rp, err2 := limiterhelper.MiddlewareToRateLimiterProvider(host, middleware) if err := multierr.Append(err1, err2); err != nil { From 193bd606de4d18df9b429afd59c5f1bcfb51a876 Mon Sep 17 00:00:00 2001 From: Joshua MacDonald Date: Mon, 19 May 2025 18:00:01 -0700 Subject: [PATCH 21/47] draft with non-blocking limiters and blocking helpers --- extension/extensionlimiter/go.mod | 3 +- extension/extensionlimiter/go.sum | 2 + .../extensionlimiter/limiterhelper/base.go | 13 +- .../limiterhelper/consumer.go | 44 ++--- .../limiterhelper/grpc/grpclimiter.go | 14 +- .../limiterhelper/http/httplimiter.go | 22 +-- .../limiterhelper/middleware.go | 2 +- .../limiterhelper/notification.go | 33 ++++ .../extensionlimiter/limiterhelper/rate.go | 124 ++++++++++++ .../limiterhelper/resource.go | 179 ++++++++++++++++++ .../extensionlimiter/limiterhelper/wrapper.go | 16 +- extension/extensionlimiter/rate.go | 29 +-- extension/extensionlimiter/resource.go | 30 +-- 13 files changed, 401 insertions(+), 110 deletions(-) create mode 100644 extension/extensionlimiter/limiterhelper/notification.go create mode 100644 extension/extensionlimiter/limiterhelper/rate.go create mode 100644 extension/extensionlimiter/limiterhelper/resource.go diff --git a/extension/extensionlimiter/go.mod b/extension/extensionlimiter/go.mod index 8262370481fe..7726b08b5261 100644 --- a/extension/extensionlimiter/go.mod +++ b/extension/extensionlimiter/go.mod @@ -13,6 +13,8 @@ require ( go.opentelemetry.io/collector/pdata v1.32.0 go.opentelemetry.io/collector/pdata/pprofile v0.126.0 go.uber.org/multierr v1.11.0 + golang.org/x/time v0.11.0 + google.golang.org/grpc v1.72.0 ) require ( @@ -38,7 +40,6 @@ require ( golang.org/x/sys v0.32.0 // indirect golang.org/x/text v0.24.0 // indirect google.golang.org/genproto/googleapis/rpc v0.0.0-20250218202821-56aae31c358a // indirect - google.golang.org/grpc v1.72.0 // indirect google.golang.org/protobuf v1.36.6 // indirect ) diff --git a/extension/extensionlimiter/go.sum b/extension/extensionlimiter/go.sum index 26de4444ce43..399c55d64bbc 100644 --- a/extension/extensionlimiter/go.sum +++ b/extension/extensionlimiter/go.sum @@ -79,6 +79,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.24.0 h1:dd5Bzh4yt5KYA8f9CJHCP4FB4D51c2c6JvN37xJJkJ0= golang.org/x/text v0.24.0/go.mod h1:L8rBsPeo2pSS+xqN0d5u2ikmjtmoJbDBT1b7nHvFCdU= +golang.org/x/time v0.11.0 h1:/bpjEDfN9tkoN/ryeYHnv5hcMlc8ncjMcM4XBk5NWV0= +golang.org/x/time v0.11.0/go.mod h1:CDIdPxbZBQxdj6cxyCIdrNogrJKMJ7pr37NYpMcMDSg= 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.0.0-20200619180055-7c47624df98f/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= diff --git a/extension/extensionlimiter/limiterhelper/base.go b/extension/extensionlimiter/limiterhelper/base.go index 1428e0b299d4..adbb078fad42 100644 --- a/extension/extensionlimiter/limiterhelper/base.go +++ b/extension/extensionlimiter/limiterhelper/base.go @@ -41,11 +41,8 @@ func BaseToRateLimiterProvider(blimp extensionlimiter.BaseLimiterProvider) (exte if err != nil { return nil, err } - return struct { - extensionlimiter.ReserveRateFunc - extensionlimiter.WaitForRateFunc - }{ - func(ctx context.Context, _ uint64) (extensionlimiter.RateReservation, error) { + return extensionlimiter.ReserveRateFunc( + func(ctx context.Context, _ int) (extensionlimiter.RateReservation, error) { if err := base.MustDeny(ctx); err != nil { return nil, err } @@ -56,11 +53,7 @@ func BaseToRateLimiterProvider(blimp extensionlimiter.BaseLimiterProvider) (exte extensionlimiter.WaitTimeFunc(nil), extensionlimiter.CancelFunc(nil), }, nil - }, - func(ctx context.Context, _ uint64) error { - return base.MustDeny(ctx) - }, - }, nil + }), nil }, }, nil } diff --git a/extension/extensionlimiter/limiterhelper/consumer.go b/extension/extensionlimiter/limiterhelper/consumer.go index 25b811d0146f..ee521e994436 100644 --- a/extension/extensionlimiter/limiterhelper/consumer.go +++ b/extension/extensionlimiter/limiterhelper/consumer.go @@ -22,10 +22,10 @@ import ( // consumer.Traces) type traits[P, C any] interface { // itemCount is SpanCount(), DataPointCount(), or LogRecordCount(). - itemCount(P) uint64 + itemCount(P) int // memorySize uses the appropriate protobuf Sizer as a proxy // for memory used. - memorySize(data P) uint64 + memorySize(data P) int // consume calls the appropriate consumer method (e.g., ConsumeTraces) consume(ctx context.Context, data P, next C) error // create is a functional constructor the consumer type (e.g., consumer.NewTraces) @@ -36,13 +36,13 @@ type traits[P, C any] interface { type traceTraits struct{} -func (traceTraits) itemCount(data ptrace.Traces) uint64 { - return uint64(data.SpanCount()) +func (traceTraits) itemCount(data ptrace.Traces) int { + return data.SpanCount() } -func (traceTraits) memorySize(data ptrace.Traces) uint64 { +func (traceTraits) memorySize(data ptrace.Traces) int { var sizer ptrace.MarshalSizer - return uint64(sizer.TracesSize(data)) + return sizer.TracesSize(data) } func (traceTraits) create(next func(ctx context.Context, data ptrace.Traces) error, opts ...consumer.Option) (consumer.Traces, error) { @@ -57,13 +57,13 @@ func (traceTraits) consume(ctx context.Context, data ptrace.Traces, next consume type metricTraits struct{} -func (metricTraits) itemCount(data pmetric.Metrics) uint64 { - return uint64(data.DataPointCount()) +func (metricTraits) itemCount(data pmetric.Metrics) int { + return data.DataPointCount() } -func (metricTraits) memorySize(data pmetric.Metrics) uint64 { +func (metricTraits) memorySize(data pmetric.Metrics) int { var sizer pmetric.MarshalSizer - return uint64(sizer.MetricsSize(data)) + return sizer.MetricsSize(data) } func (metricTraits) create(next func(ctx context.Context, data pmetric.Metrics) error, opts ...consumer.Option) (consumer.Metrics, error) { @@ -78,13 +78,13 @@ func (metricTraits) consume(ctx context.Context, data pmetric.Metrics, next cons type logTraits struct{} -func (logTraits) itemCount(data plog.Logs) uint64 { - return uint64(data.LogRecordCount()) +func (logTraits) itemCount(data plog.Logs) int { + return data.LogRecordCount() } -func (logTraits) memorySize(data plog.Logs) uint64 { +func (logTraits) memorySize(data plog.Logs) int { var sizer plog.MarshalSizer - return uint64(sizer.LogsSize(data)) + return sizer.LogsSize(data) } func (logTraits) create(next func(ctx context.Context, data plog.Logs) error, opts ...consumer.Option) (consumer.Logs, error) { @@ -99,13 +99,13 @@ func (logTraits) consume(ctx context.Context, data plog.Logs, next consumer.Logs type profileTraits struct{} -func (profileTraits) itemCount(data pprofile.Profiles) uint64 { - return uint64(data.SampleCount()) +func (profileTraits) itemCount(data pprofile.Profiles) int { + return data.SampleCount() } -func (profileTraits) memorySize(data pprofile.Profiles) uint64 { +func (profileTraits) memorySize(data pprofile.Profiles) int { var sizer pprofile.MarshalSizer - return uint64(sizer.ProfilesSize(data)) + return sizer.ProfilesSize(data) } func (profileTraits) create(next func(ctx context.Context, data pprofile.Profiles) error, opts ...consumer.Option) (xconsumer.Profiles, error) { @@ -124,7 +124,7 @@ func limitOne[P any, C any]( m traits[P, C], key extensionlimiter.WeightKey, opts []consumer.Option, - quantify func(P) uint64, + quantify func(P) int, ) (C, error) { if !slices.Contains(keys, key) { return next, nil @@ -177,15 +177,15 @@ func newLimited[P any, C any]( var err1, err2, err3, err4 error // Note: reverse order of evaluation cost => least-cost applied first. next, err1 = limitOne(next, keys, provider, m, extensionlimiter.WeightKeyMemorySize, opts, - func(data P) uint64 { + func(data P) int { return m.memorySize(data) }) next, err2 = limitOne(next, keys, provider, m, extensionlimiter.WeightKeyRequestItems, opts, - func(data P) uint64 { + func(data P) int { return m.itemCount(data) }) next, err3 = limitOne(next, keys, provider, m, extensionlimiter.WeightKeyRequestCount, opts, - func(_ P) uint64 { + func(_ P) int { return 1 }) next, err4 = applyBaseLimiter(next, provider, m, opts) diff --git a/extension/extensionlimiter/limiterhelper/grpc/grpclimiter.go b/extension/extensionlimiter/limiterhelper/grpc/grpclimiter.go index d4b4b918adce..ced96727d86f 100644 --- a/extension/extensionlimiter/limiterhelper/grpc/grpclimiter.go +++ b/extension/extensionlimiter/limiterhelper/grpc/grpclimiter.go @@ -62,8 +62,8 @@ func NewClientLimiter(host component.Host, middleware configmiddleware.Config) ( if bytesLimiter != nil { gopts = append(gopts, grpc.WithStatsHandler( &limiterStatsHandler{ - bytesLimiter: bytesLimiter, - isClient: true, + limiter: limiterhelper.NewBlockingRateLimiter(bytesLimiter), + isClient: true, })) } return extensionmiddleware.GetGRPCClientOptionsFunc(func() ([]grpc.DialOption, error) { @@ -115,8 +115,8 @@ func NewServerLimiter(host component.Host, middleware configmiddleware.Config) ( if bytesLimiter != nil { gopts = append(gopts, grpc.StatsHandler( &limiterStatsHandler{ - bytesLimiter: bytesLimiter, - isClient: false, + limiter: limiterhelper.NewBlockingRateLimiter(bytesLimiter), + isClient: false, })) } @@ -127,8 +127,8 @@ func NewServerLimiter(host component.Host, middleware configmiddleware.Config) ( // limiterStatsHandler implements the stats.Handler interface for rate limiting. type limiterStatsHandler struct { - bytesLimiter extensionlimiter.RateLimiter - isClient bool + limiter limiterhelper.BlockingRateLimiter + isClient bool } func (h *limiterStatsHandler) TagRPC(ctx context.Context, _ *stats.RPCTagInfo) context.Context { @@ -159,7 +159,7 @@ func (h *limiterStatsHandler) HandleRPC(ctx context.Context, s stats.RPCStats) { } // Apply rate limiting based on network bytes // TODO: How does the limiter break the stream? - _ = h.bytesLimiter.WaitForRate(ctx, uint64(wireBytes)) + _ = h.limiter.WaitFor(ctx, wireBytes) } func (h *limiterStatsHandler) TagConn(ctx context.Context, _ *stats.ConnTagInfo) context.Context { diff --git a/extension/extensionlimiter/limiterhelper/http/httplimiter.go b/extension/extensionlimiter/limiterhelper/http/httplimiter.go index 3bec0ad40e94..065244d58125 100644 --- a/extension/extensionlimiter/limiterhelper/http/httplimiter.go +++ b/extension/extensionlimiter/limiterhelper/http/httplimiter.go @@ -41,9 +41,9 @@ func NewClientLimiter(host component.Host, middleware configmiddleware.Config) ( if bytesLimiter != nil && req.Body != nil && req.Body != http.NoBody { // If bytes are limited, create a limited ReadCloser body. req.Body = &rateLimitedBody{ - body: req.Body, - rateLimiter: bytesLimiter, - ctx: req.Context(), + body: req.Body, + limiter: limiterhelper.NewBlockingRateLimiter(bytesLimiter), + ctx: req.Context(), } } var err error @@ -58,19 +58,19 @@ func NewClientLimiter(host component.Host, middleware configmiddleware.Config) ( // rateLimitedBody wraps an http.Request.Body to track bytes and call the rate limiter type rateLimitedBody struct { - body io.ReadCloser - rateLimiter extensionlimiter.RateLimiter - ctx context.Context + body io.ReadCloser + limiter limiterhelper.BlockingRateLimiter + ctx context.Context } var _ io.ReadCloser = &rateLimitedBody{} -// Read implements io.Reader interface, counting bytes as they are read +// Read implements io.Reader interface, limiting bytes as they are read func (rb *rateLimitedBody) Read(p []byte) (n int, err error) { n, err = rb.body.Read(p) if n > 0 { // Apply rate limiting based on network bytes after they are read - limitErr := rb.rateLimiter.WaitForRate(rb.ctx, uint64(n)) + limitErr := rb.limiter.WaitFor(rb.ctx, n) if limitErr != nil { // If the rate limiter rejects the bytes, return the error return n, limitErr // TODO: How to return HTTP 429? @@ -109,9 +109,9 @@ func NewServerLimiter(host component.Host, middleware configmiddleware.Config) ( if bytesLimiter != nil && req.Body != nil && req.Body != http.NoBody { // If bytes are limited, create a limited ReadCloser body. req.Body = &rateLimitedBody{ - body: req.Body, - rateLimiter: bytesLimiter, - ctx: req.Context(), + body: req.Body, + limiter: limiterhelper.NewBlockingRateLimiter(bytesLimiter), + ctx: req.Context(), } } base.ServeHTTP(w, req) diff --git a/extension/extensionlimiter/limiterhelper/middleware.go b/extension/extensionlimiter/limiterhelper/middleware.go index 17efb438aff0..788f1ff1b5d1 100644 --- a/extension/extensionlimiter/limiterhelper/middleware.go +++ b/extension/extensionlimiter/limiterhelper/middleware.go @@ -177,7 +177,7 @@ func sequenceLimiters(lims []LimiterWrapper) LimiterWrapper { } func composeLimiters(first, second LimiterWrapper) LimiterWrapper { - return LimiterWrapperFunc(func(ctx context.Context, value uint64, call func(ctx context.Context) error) error { + return LimiterWrapperFunc(func(ctx context.Context, value int, call func(ctx context.Context) error) error { return first.LimitCall(ctx, value, func(ctx context.Context) error { return second.LimitCall(ctx, value, call) }) diff --git a/extension/extensionlimiter/limiterhelper/notification.go b/extension/extensionlimiter/limiterhelper/notification.go new file mode 100644 index 000000000000..9ef2d4db3848 --- /dev/null +++ b/extension/extensionlimiter/limiterhelper/notification.go @@ -0,0 +1,33 @@ +// Copyright The OpenTelemetry Authors +// SPDX-License-Identifier: Apache-2.0 + +package limiterhelper + +type notification struct { + c chan struct{} +} + +func newNotification() notification { + return notification{c: make(chan struct{})} +} + +func (n *notification) notice() { + close(n.c) +} + +func (n *notification) hasBeen() bool { + select { + case <-n.c: + return true + default: + return false + } +} + +func (n *notification) waitFor() { + <-n.c +} + +func (n *notification) channel() <-chan struct{} { + return n.c +} diff --git a/extension/extensionlimiter/limiterhelper/rate.go b/extension/extensionlimiter/limiterhelper/rate.go new file mode 100644 index 000000000000..ed2069126c4a --- /dev/null +++ b/extension/extensionlimiter/limiterhelper/rate.go @@ -0,0 +1,124 @@ +// Copyright The OpenTelemetry Authors +// SPDX-License-Identifier: Apache-2.0 + +package limiterhelper // import "go.opentelemetry.io/collector/extension/extensionlimiter/limiterhelper" + +import ( + "context" + "errors" + "time" + + "go.opentelemetry.io/collector/extension/extensionlimiter" + "golang.org/x/time/rate" +) + +var ( + ErrRateLimitExceeded = errors.New("rate limit is saturated") + ErrRateRequestTooLarge = errors.New("rate request exceeds burst size") + ErrRateRequestDeadline = errors.New("rate request exceeds context deadline") +) + +// NewRateLimiter returns an implementation of the rate-limiter +// extension based on logic from x/time/rate. +func NewRateLimiter(frequency float64, burst int) extensionlimiter.RateLimiter { + limit := rate.Limit(frequency) + limiter := rate.NewLimiter(limit, burst) + + reserve := func(ctx context.Context, value int) (extensionlimiter.RateReservation, error) { + // Check if context was canceled. + select { + case <-ctx.Done(): + return nil, context.Cause(ctx) + default: + } + + // Check for out-of-range requests. + if value > burst && limit != rate.Inf { + return nil, ErrRateRequestTooLarge + } + + // Call the non-blocking API. + rsv := limiter.ReserveN(time.Now(), value) + if !rsv.OK() { + return nil, ErrRateLimitExceeded + } + + // Compare the wait and deadline. + when := time.Now() + wait := rsv.DelayFrom(when) + if deadline, ok := ctx.Deadline(); ok { + if deadline.Sub(when) < wait { + rsv.Cancel() + return nil, ErrRateRequestDeadline + } + } + + // Return the wait time and cancel function. + return struct { + extensionlimiter.WaitTimeFunc + extensionlimiter.CancelFunc + }{ + func() time.Duration { return wait }, + rsv.Cancel, + }, nil + } + + return extensionlimiter.ReserveRateFunc(reserve) +} + +// BlockingRateLimiter wraps a RateLimiter extension in a blocking +// interface which takes the context deadline into account before +// waiting for the rate allowance. +type BlockingRateLimiter struct { + limiter extensionlimiter.RateLimiter +} + +// NewBlockingRateLimiter returns a blocking wrapper for RateLimiter +// extensions. +func NewBlockingRateLimiter(limiter extensionlimiter.RateLimiter) BlockingRateLimiter { + return BlockingRateLimiter{ + limiter: limiter, + } +} + +// WaitFor blocks the caller until the requested value is allowed by +// the limiter. +func (b BlockingRateLimiter) WaitFor(ctx context.Context, value int) error { + newTimer := func(d time.Duration) (<-chan time.Time, func() bool, func()) { + timer := time.NewTimer(d) + return timer.C, timer.Stop, func() {} + } + return b.waitFor(ctx, value, newTimer) +} + +// timerFunc is a test helper for testing the blocking rate limter. +type timerFunc func(time.Duration) (<-chan time.Time, func() bool, func()) + +// waitFor is a testable form of BlockingRateLimiter.WaitFor. +func (b BlockingRateLimiter) waitFor(ctx context.Context, value int, timer timerFunc) error { + // Reserve from the underlying limiter. + rsv, err := b.limiter.ReserveRate(ctx, value) + if err != nil { + return err + } + + // Wait, if necessary. + delay := rsv.WaitTime() + if delay == 0 { + return nil + } + + ch, stop, advance := timer(delay) + defer stop() + advance() // only has an effect when testing + select { + case <-ch: + // Proceed. Do not cancel. + return nil + + case <-ctx.Done(): + // Context was canceled before we could proceed. + rsv.Cancel() + return context.Cause(ctx) + } +} diff --git a/extension/extensionlimiter/limiterhelper/resource.go b/extension/extensionlimiter/limiterhelper/resource.go new file mode 100644 index 000000000000..90b0a2b8570a --- /dev/null +++ b/extension/extensionlimiter/limiterhelper/resource.go @@ -0,0 +1,179 @@ +// Copyright The OpenTelemetry Authors +// SPDX-License-Identifier: Apache-2.0 + +package limiterhelper // import "go.opentelemetry.io/collector/extension/extensionlimiter/limiterhelper" + +import ( + "container/list" + "context" + "errors" + "sync" + + "go.opentelemetry.io/collector/extension/extensionlimiter" +) + +var ( + // TODO: was grpccodes.ResourceExhausted + ErrResourceWaitLimit = errors.New("too much waiting data") + + // TODO: was grpccodes.InvalidArgument + ErrResourceSizeLimit = errors.New("request is too large") +) + +// NewResourceLimiter returns an implementation of the +// resource-limiter extension based on a LIFO queue. +func NewResourceLimiter(admitLimit, waitLimit uint64) extensionlimiter.ResourceLimiter { + return &boundedQueue{ + limitAdmit: admitLimit, + limitWait: waitLimit, + } +} + +var _ extensionlimiter.ResourceLimiter = &boundedQueue{} + +type boundedQueue struct { + limitAdmit uint64 + limitWait uint64 + + // lock protects currentAdmitted, currentWaiting, and waiters + lock sync.Mutex + currentAdmitted uint64 + currentWaiting uint64 + waiters *list.List // of *waiter +} + +// waiter is an item in the BoundedQueue waiters list. +type waiter struct { + notify notification + value int +} + +func (bq *boundedQueue) ReserveResource(ctx context.Context, value int) (extensionlimiter.ResourceReservation, error) { + if uint64(value) > bq.limitAdmit { + return nil, ErrResourceSizeLimit + } + + bq.lock.Lock() + defer bq.lock.Unlock() + + if bq.currentAdmitted+uint64(value) <= bq.limitAdmit { + // the fast success path. + bq.currentAdmitted += uint64(value) + return nil, nil + } + + // since we were unable to admit, check if we can wait. + if bq.currentWaiting+uint64(value) > bq.limitWait { + return nil, ErrResourceWaitLimit + } + + // otherwise we need to wait + element := bq.addWaiterLocked(value) + waiter := element.Value.(*waiter) + + select { + case <-waiter.notify.channel(): + return struct { + extensionlimiter.DelayFunc + extensionlimiter.ReleaseFunc + }{ + func() <-chan struct{} { + // The caller waits for this notification + // to use the resource. + return waiter.notify.channel() + }, + func() { + // This call returns the resource. + bq.lock.Lock() + defer bq.lock.Unlock() + + bq.releaseLocked(value) + }, + }, nil + + case <-ctx.Done(): + bq.lock.Lock() + defer bq.lock.Unlock() + + if waiter.notify.hasBeen() { + // We were also admitted, which can happen + // concurrently with cancellation. Make sure + // to release since no one else will do it. + bq.releaseLocked(value) + } else { + // Remove ourselves from the list of waiters + // so that we can't be admitted in the future. + bq.removeWaiterLocked(value, element) + bq.admitWaitersLocked() + } + + return nil, context.Cause(ctx) + } +} + +func (bq *boundedQueue) admitWaitersLocked() { + for bq.waiters.Len() != 0 { + // Ensure there is enough room to admit the next waiter. + element := bq.waiters.Back() + waiter := element.Value.(*waiter) + if bq.currentAdmitted+uint64(waiter.value) > bq.limitAdmit { + // Returning means continuing to wait for the + // most recent arrival to get service by another release. + return + } + + // Release the next waiter and tell it that it has been admitted. + bq.removeWaiterLocked(waiter.value, element) + bq.currentAdmitted += uint64(waiter.value) + + waiter.notify.notice() + } +} + +func (bq *boundedQueue) addWaiterLocked(value int) *list.Element { + bq.currentWaiting += uint64(value) + return bq.waiters.PushBack(&waiter{ + value: value, + notify: newNotification(), + }) +} + +func (bq *boundedQueue) removeWaiterLocked(value int, element *list.Element) { + bq.currentWaiting -= uint64(value) + bq.waiters.Remove(element) +} + +func (bq *boundedQueue) releaseLocked(value int) { + bq.currentAdmitted -= uint64(value) + bq.admitWaitersLocked() +} + +// BlockingResourceLimiter wraps for ResourceLimiter extension in a +// blocking interface which considers the context deadline while +// waiting for the resource. +type BlockingResourceLimiter struct { + limiter extensionlimiter.ResourceLimiter +} + +// NewBlockingResourceLimiter returns a blocking wrapper for +// ResourceLimiter extensions. +func NewBlockingResourceLimiter(limiter extensionlimiter.ResourceLimiter) BlockingResourceLimiter { + return BlockingResourceLimiter{ + limiter: limiter, + } +} + +// WaitFor blocks the caller until the requested value is allowed by +// the limiter. +func (b BlockingResourceLimiter) WaitFor(ctx context.Context, value int) (extensionlimiter.ReleaseFunc, error) { + rsv, err := b.limiter.ReserveResource(ctx, value) + if err != nil { + return func() {}, err + } + select { + case <-ctx.Done(): + return func() {}, context.Cause(ctx) + case <-rsv.Delay(): + return rsv.Release, nil + } +} diff --git a/extension/extensionlimiter/limiterhelper/wrapper.go b/extension/extensionlimiter/limiterhelper/wrapper.go index fee38f3f22af..535f07125579 100644 --- a/extension/extensionlimiter/limiterhelper/wrapper.go +++ b/extension/extensionlimiter/limiterhelper/wrapper.go @@ -54,16 +54,16 @@ type LimiterWrapper interface { // from either the limiter or the enclosed callback. // // The `call` parameter must be non-nil. - LimitCall(ctx context.Context, weight uint64, call func(ctx context.Context) error) error + LimitCall(ctx context.Context, weight int, call func(ctx context.Context) error) error } // LimiterWrapperFunc is a functional way to build LimiterWrappers. -type LimiterWrapperFunc func(context.Context, uint64, func(ctx context.Context) error) error +type LimiterWrapperFunc func(context.Context, int, func(ctx context.Context) error) error var _ LimiterWrapper = LimiterWrapperFunc(nil) // LimitCall implements LimiterWrapper. -func (f LimiterWrapperFunc) LimitCall(ctx context.Context, value uint64, call func(ctx context.Context) error) error { +func (f LimiterWrapperFunc) LimitCall(ctx context.Context, value int, call func(ctx context.Context) error) error { if f == nil { return call(ctx) } @@ -99,8 +99,9 @@ func NewResourceLimiterWrapperProvider(rp extensionlimiter.ResourceLimiterProvid if lim == nil { return nil, nil } - return LimiterWrapperFunc(func(ctx context.Context, value uint64, call func(context.Context) error) error { - release, err := lim.WaitForResource(ctx, value) + blocking := NewBlockingResourceLimiter(lim) + return LimiterWrapperFunc(func(ctx context.Context, value int, call func(context.Context) error) error { + release, err := blocking.WaitFor(ctx, value) if err != nil { return err } @@ -124,8 +125,9 @@ func NewRateLimiterWrapperProvider(rp extensionlimiter.RateLimiterProvider) Limi if lim == nil { return nil, nil } - return LimiterWrapperFunc(func(ctx context.Context, value uint64, call func(context.Context) error) error { - if err := lim.WaitForRate(ctx, value); err != nil { + blocking := NewBlockingRateLimiter(lim) + return LimiterWrapperFunc(func(ctx context.Context, value int, call func(context.Context) error) error { + if err := blocking.WaitFor(ctx, value); err != nil { return err } return call(ctx) diff --git a/extension/extensionlimiter/rate.go b/extension/extensionlimiter/rate.go index e842204fdf37..6100b3796863 100644 --- a/extension/extensionlimiter/rate.go +++ b/extension/extensionlimiter/rate.go @@ -57,21 +57,11 @@ type RateLimiter interface { // resume after Delay(). The context is provided for access to // instrumentation and client metadata; the Context deadline // is not used, should be considered by the caller. - ReserveRate(context.Context, uint64) (RateReservation, error) - - // WaitForRate is modeled on pkg.go.dev/golang.org/x/time/rate#Limiter.WaitN - // - // This is a blocking interface. Use this interface for - // callers that are constrained by a Context deadline, which - // will be incorporated into the limiter decision. - WaitForRate(context.Context, uint64) error + ReserveRate(context.Context, int) (RateReservation, error) } // A rate limiter can be made up of two functions. -var _ RateLimiter = struct { - ReserveRateFunc - WaitForRateFunc -}{} +var _ RateLimiter = ReserveRateFunc(nil) // RateReservation is modeled on pkg.go.dev/golang.org/x/time/rate#Reservation type RateReservation interface { @@ -86,27 +76,16 @@ type RateReservation interface { } // ReserveRateFunc is a functional way to construct ReserveRate functions. -type ReserveRateFunc func(context.Context, uint64) (RateReservation, error) +type ReserveRateFunc func(context.Context, int) (RateReservation, error) // Reserve implements part of the RateReserveer interface. -func (f ReserveRateFunc) ReserveRate(ctx context.Context, value uint64) (RateReservation, error) { +func (f ReserveRateFunc) ReserveRate(ctx context.Context, value int) (RateReservation, error) { if f == nil { return nil, nil } return f(ctx, value) } -// WaitForRateFunc is a functional way to construct WaitForRate functions. -type WaitForRateFunc func(context.Context, uint64) error - -// Reserve implements part of the RateReserveer interface. -func (f WaitForRateFunc) WaitForRate(ctx context.Context, value uint64) error { - if f == nil { - return nil - } - return f(ctx, value) -} - // WaitTimeFunc is a functional way to construct WaitTime functions. type WaitTimeFunc func() time.Duration diff --git a/extension/extensionlimiter/resource.go b/extension/extensionlimiter/resource.go index 97942cf191ab..90fded5c3ab0 100644 --- a/extension/extensionlimiter/resource.go +++ b/extension/extensionlimiter/resource.go @@ -58,15 +58,7 @@ type ResourceLimiter interface { // resume after DelayFrom(). The context is provided for // access to instrumentation and client metadata; the Context // deadline is not used. - ReserveResource(context.Context, uint64) (ResourceReservation, error) - - // WaitForRate is modeled on pkg.go.dev/golang.org/x/time/rate#Limiter.WaitN, - // without the time dimension. - // - // This is a blocking interface. Use this interface for - // callers that are constrained by a Context deadline, which - // will be incorporated into the limiter decision. - WaitForResource(context.Context, uint64) (ReleaseFunc, error) + ReserveResource(context.Context, int) (ResourceReservation, error) } // ResourceReservation is modeled on pkg.go.dev/golang.org/x/time/rate#Reservation @@ -121,10 +113,10 @@ var immediateChan = func() <-chan struct{} { }() // ReserveResourceFunc is a functional way to construct ReserveResource interface methods. -type ReserveResourceFunc func(ctx context.Context, value uint64) (ResourceReservation, error) +type ReserveResourceFunc func(ctx context.Context, value int) (ResourceReservation, error) // ReserveResource implements a ReserveResource interface method. -func (f ReserveResourceFunc) ReserveResource(ctx context.Context, value uint64) (ResourceReservation, error) { +func (f ReserveResourceFunc) ReserveResource(ctx context.Context, value int) (ResourceReservation, error) { if f == nil { return struct { DelayFunc @@ -137,18 +129,4 @@ func (f ReserveResourceFunc) ReserveResource(ctx context.Context, value uint64) return f(ctx, value) } -// WaitForResourceFunc is a functional way to construct WaitForResource interface methods. -type WaitForResourceFunc func(context.Context, uint64) (ReleaseFunc, error) - -// WaitForResource implements a WaitForResource interface method. -func (f WaitForResourceFunc) WaitForResource(ctx context.Context, value uint64) (ReleaseFunc, error) { - if f == nil { - return ReleaseFunc(nil), nil - } - return f(ctx, value) -} - -var _ ResourceLimiter = struct { - ReserveResourceFunc - WaitForResourceFunc -}{} +var _ ResourceLimiter = ReserveResourceFunc(nil) From 7bdee790716952e0cd9e4775545460e63c3a6f86 Mon Sep 17 00:00:00 2001 From: Joshua MacDonald Date: Tue, 20 May 2025 13:57:11 -0700 Subject: [PATCH 22/47] Work on readme --- extension/extensionlimiter/README.md | 148 ++++++++++++------ .../limiterhelper/middleware.go | 4 +- .../limiterhelper/resource.go | 2 + 3 files changed, 107 insertions(+), 47 deletions(-) diff --git a/extension/extensionlimiter/README.md b/extension/extensionlimiter/README.md index 4656ebece4fe..9476bb86f94c 100644 --- a/extension/extensionlimiter/README.md +++ b/extension/extensionlimiter/README.md @@ -2,73 +2,129 @@ **Document status: development** -The `extensionlimiter` package provides interfaces for rate limiting -and resource limiting in the OpenTelemetry Collector, enabling control -over data flow and resource usage through extensions that can be -configured through middleware and/or directly by pipeline components. +The `extensionlimiter` package provides interfaces for limiting +pipelines in the OpenTelemetry Collector, enabling control over data +flow and resource usage through extensions which are configured +through middleware and/or directly by pipeline components. ## Overview -This package defines two primary limiter **kinds**, which have -different interfaces: +This package defines three foundational limiter **kinds**, each with +similar but distinct interfaces. A limiter extension is either a +basic limiter, or it implements the basic limiter interface and one of +the weight-based interfaces: -- **Rate Limiters**: Control time-based limits on quantities such as +- **Basic Limiter**: Makes a simple yes/no decision without a weight + parameter, typically to stop new work in an emergency. +- **Rate Limiter**: Controls time-based limits over weights such as bytes or items per second. -- **Resource Limiters**: Manage physical limits on quantities such as - concurrent requests or memory usage. +- **Resource Limiter**: Controls physical limits over weights such as + concurrent requests or active memory in use. + +For the two weight-based limiters, requests are quantified with an +integer value and identified by a **weight key** indicating the type +of quantity being measured and limited. There are currently four +weight keys with a standard definition: + +1. Network bytes +2. Request count +3. Request items +4. Memory size + +The foundational interfaces are non-blocking, and each calling +convention is different. The various limiter kinds are unified +through a `LimiterWrapper` interface, which simplifies consumers in +many cases by providing a consistent `LimitCall` interface for each +limiter kind using a synchronous callback. Limiter wrappers provide an +abstraction over the details of requesting the limit, blocking the +caller temporarily (considering deadline), and making the call. + +There are circumstances where the kind of limiter matters. For +example, in current middleware, the network bytes weight key can be +measured through a `grpc.StatsHandler` or an `io.ReadCloser`, and in +both cases the resource (e.g., byte slice, pdata object) remains in +use after the method returns. These callers can apply rate limit and +basic limits, but they cannot apply resource limits. + +The kind of limiter matters in other situations where program control +flow does not permit the use of a wrapper, especially as needed to +maintain back-pressure in a pipeline. In a streaming asynchronous +receiver (e.g., +[otelarrowreceiver](https://github.com/open-telemetry/opentelemetry-collector-contrib/blob/main/receiver/otelarrowreceiver/README.md)), +for example, a limiter can slow the arrival of new data by stalling a +response, it means synchronously waiting for the limit and +asynchronously processing the request. + +Resource limiters require the most of callers, as the interface +requires a `Release` function to be called after the caller is +finished with the resource. Adapters are provided for convenience. + +A limiter is defined as **saturated** when a limit is completely +overloaded in at least one weight, generally it means callers should +deny new requests. All limiter extensions implement the basic limiter +interface, and callers are expected to check for saturation by +invoking `MustDeny` once before applying individual weight limits. -Both limiter kinds are unified through the `LimiterWrapper` interface, -which simplifies consumers in most cases by providing a consistent -`LimitCall` interface. +Whereas the basic limiter's `MustDeny` method indicates only +saturation, the rate and resource limiter interfaces both return a +`Reservation`. While the details are slightly different, the +reservation generally has two features: -A limiter is **saturated** by definition when a limit is completely -overloaded in at least one weight, generally it means callers should -immediately deny work to continue on the request. +- a mechanism to wait for the limit (if possible) +- a mechanism to cancel or release the request. -Each kind of limiter as well as the wrapper type have corresponding -**provider** interfaces that return a limiter instance based on a -weight keys. +Each kind of limiter have corresponding **provider** interfaces that +return a specific limiter instance based on a weight key. Components +are expected to initialize limiters during startup, through limiter +extension providers (which may produce configuration errors). -Weight keys describe the standard limiting dimensions. There are -currently four standard weight keys: network bytes, request count, -request items, and memory size. Callers use the `Checker` interface -to check whether any weight keys (from a set) are saturated. +Any limiter extension: -## Key Interfaces +- MUST implement the `BaseLimiterProvider` interface +- MUST NOT implement both the `ResourceLimiterProvider` and the `RateLimiterProvider` interfaces -- `LimiterWrapper`: Provides a callback-based limiting interface that - works with both rate and resource limiters, has a `LimitCall` method, - plus a provider type. -- `RateLimiter`: Applies time-based limits, has a `Limit` method, - plus provider type. -- `ResourceLimiter`: Manages physical resource limits, has - an `Acquire` method and a corresponding `ReleaseFunc`, - plus a provider type. -- `Checker`: Has a `MustDeny` method. +The `limiterhelper` package contains features for composing limiters +as well as foundational rate and resource limiter implementations. +The `limiterhelper/http` and `limiterhelper/grpc` packages provide +connectors allowing limiters to act as specific kinds of middleware. +The original garbage-collector state-based limiter can be found in +[`../memorylimiterextension`](../memorylimiterextension/README.md). -### Limiter helpers +## Recommendations -The `limiterhelper` subpackage provides: +For processors, exporters, and sometimes receivers, the easiest way to +integrate with any kind of limiter is to use the a consumer wrapper +function (e.g. `NewLimitedLogs`). These helper methods check for +saturation and then apply multiple weight keys in sequence. -- Consumer wrappers apply limits to a collector pipeline (e.g., - `NewLimitedLogs` for a limiter combined with `consumer.NewLogs`) -- Multi-limiter combinators: for simple combined limiter functionality -- Middleware conversion utilities: convert middleware configurations - to limiter providers. +Multi-limiter adapters (@@@), ... -## Recommendations +At a lower level, a simple way to integrate with any kind of limiter +is to use the `LimiterWrapper` interface with its callback-based +approach. + +For blocking access to rate and resource limiters without wrapper +constraints, use `NewBlockingRateLimiter` or `NewBlockingResourceLimiter`. -For general use cases, prefer the `LimiterWrapper` interface with its -callback-based approach because it is agnostic to the difference between -rate and resource limiters. +In cases where control flow is not request scoped (e.g., in middleware +measuring network bytes), use a `RateLimiter` interface. If the +extension is a basic limiter in this scenario, use the +`BaseToRateLimiterProvider` adapter. Callers MUST NOT configure a +resource limiter for a caller that is restricted to the `RateLimiter` +interface; this configuration SHOULD fail at startup or during +component validation. -Use the direct `RateLimiter` or `ResourceLimiter` interfaces only in -special cases where control flow can't be easily scoped. +In cases where due to control flow a wrapper interface cannot be used, +as long as the caller is able to arrange for a `Release` function to +be called at the proper time, then any kind of limiter can be applied +in the form of a `ResourceLimiter`. If the extension is a basic or +rate limiter in this scenario, use the `BaseToResourceLimiterProvider` +or `RateToResourceLimiterProvider` adapters. Middleware configuration typically automates the configuration of network bytes and request count weight keys relatively early in a pipeline. Receivers are responsible for limiting request items and -memory size through one of the available helpers. +memory size through one of the available helpers. Processors can apply limiters for specific reasons, for example to apply limits in data-dependent ways. Exporters can apply limiters for diff --git a/extension/extensionlimiter/limiterhelper/middleware.go b/extension/extensionlimiter/limiterhelper/middleware.go index 788f1ff1b5d1..3cfcf95faff5 100644 --- a/extension/extensionlimiter/limiterhelper/middleware.go +++ b/extension/extensionlimiter/limiterhelper/middleware.go @@ -101,7 +101,9 @@ func MiddlewareIsLimiter(host component.Host, middleware configmiddleware.Config } } -// MiddlewareToRateLimiterProvider allows a base limiter to act as a rate +// MiddlewareToRateLimiterProvider allows a base limiter to act as a +// rate. This encodes the fact that a resource limiter extension +// cannot be adapted to a rate limiter interface. func MiddlewareToRateLimiterProvider(host component.Host, middleware configmiddleware.Config) (extensionlimiter.RateLimiterProvider, error) { ext, ok, err := MiddlewareIsLimiter(host, middleware) if err != nil { diff --git a/extension/extensionlimiter/limiterhelper/resource.go b/extension/extensionlimiter/limiterhelper/resource.go index 90b0a2b8570a..ffd90e010f23 100644 --- a/extension/extensionlimiter/limiterhelper/resource.go +++ b/extension/extensionlimiter/limiterhelper/resource.go @@ -22,6 +22,8 @@ var ( // NewResourceLimiter returns an implementation of the // resource-limiter extension based on a LIFO queue. +// See this [article](https://medium.com/swlh/fifo-considered-harmful-793b76f98374) +// explaining why LIFO is preferred here. func NewResourceLimiter(admitLimit, waitLimit uint64) extensionlimiter.ResourceLimiter { return &boundedQueue{ limitAdmit: admitLimit, From 5ddab1cc46cf4c1b1e5d7145a1007de491384520 Mon Sep 17 00:00:00 2001 From: Joshua MacDonald Date: Wed, 21 May 2025 14:11:55 -0700 Subject: [PATCH 23/47] wip --- extension/extensionlimiter/README.md | 2 +- .../extensionlimiter/limiterhelper/base.go | 53 ++++--- .../limiterhelper/middleware.go | 145 ++++++++---------- .../extensionlimiter/limiterhelper/multi.go | 107 +++++++++++++ .../extensionlimiter/limiterhelper/rate.go | 42 +++++ .../extensionlimiter/limiterhelper/wrapper.go | 44 +++--- extension/extensionlimiter/resource.go | 5 +- 7 files changed, 259 insertions(+), 139 deletions(-) create mode 100644 extension/extensionlimiter/limiterhelper/multi.go diff --git a/extension/extensionlimiter/README.md b/extension/extensionlimiter/README.md index 9476bb86f94c..fadab4117ff7 100644 --- a/extension/extensionlimiter/README.md +++ b/extension/extensionlimiter/README.md @@ -78,7 +78,7 @@ return a specific limiter instance based on a weight key. Components are expected to initialize limiters during startup, through limiter extension providers (which may produce configuration errors). -Any limiter extension: +All limiter extensions: - MUST implement the `BaseLimiterProvider` interface - MUST NOT implement both the `ResourceLimiterProvider` and the `RateLimiterProvider` interfaces diff --git a/extension/extensionlimiter/limiterhelper/base.go b/extension/extensionlimiter/limiterhelper/base.go index adbb078fad42..dacf20864d2f 100644 --- a/extension/extensionlimiter/limiterhelper/base.go +++ b/extension/extensionlimiter/limiterhelper/base.go @@ -5,31 +5,12 @@ package limiterhelper // import "go.opentelemetry.io/collector/extension/extensi import ( "context" - "errors" "go.opentelemetry.io/collector/extension/extensionlimiter" ) -// MultiBaseLimiter returns MustDeny when any element returns MustDeny. -type MultiBaseLimiter []extensionlimiter.BaseLimiter - -var _ extensionlimiter.BaseLimiter = MultiBaseLimiter{} - -// MustDeny implements BaseLimiter. -func (ls MultiBaseLimiter) MustDeny(ctx context.Context) error { - var err error - for _, lim := range ls { - if lim == nil { - continue - } - err = errors.Join(err, lim.MustDeny(ctx)) - } - return err -} - // BaseToRateLimiterProvider allows a base limiter to act as a rate -// limiter. This allows a base limiter to apply to individual Read() -// calls. +// limiter. func BaseToRateLimiterProvider(blimp extensionlimiter.BaseLimiterProvider) (extensionlimiter.RateLimiterProvider, error) { return struct { extensionlimiter.GetBaseLimiterFunc @@ -49,10 +30,34 @@ func BaseToRateLimiterProvider(blimp extensionlimiter.BaseLimiterProvider) (exte return struct { extensionlimiter.WaitTimeFunc extensionlimiter.CancelFunc - }{ - extensionlimiter.WaitTimeFunc(nil), - extensionlimiter.CancelFunc(nil), - }, nil + }{}, nil + }), nil + }, + }, nil +} + +// BaseToResourceLimiterProvider allows a base limiter to act as a +// resource limiter. +func BaseToResourceLimiterProvider(blimp extensionlimiter.BaseLimiterProvider) (extensionlimiter.ResourceLimiterProvider, error) { + return struct { + extensionlimiter.GetBaseLimiterFunc + extensionlimiter.GetResourceLimiterFunc + }{ + blimp.GetBaseLimiter, + func(_ extensionlimiter.WeightKey, opts ...extensionlimiter.Option) (extensionlimiter.ResourceLimiter, error) { + base, err := blimp.GetBaseLimiter(opts...) + if err != nil { + return nil, err + } + return extensionlimiter.ReserveResourceFunc( + func(ctx context.Context, _ int) (extensionlimiter.ResourceReservation, error) { + if err := base.MustDeny(ctx); err != nil { + return nil, err + } + return struct { + extensionlimiter.DelayFunc + extensionlimiter.ReleaseFunc + }{}, nil }), nil }, }, nil diff --git a/extension/extensionlimiter/limiterhelper/middleware.go b/extension/extensionlimiter/limiterhelper/middleware.go index 3cfcf95faff5..9efd93bbf333 100644 --- a/extension/extensionlimiter/limiterhelper/middleware.go +++ b/extension/extensionlimiter/limiterhelper/middleware.go @@ -4,13 +4,11 @@ package limiterhelper // import "go.opentelemetry.io/collector/extension/extensionlimiter/limiterhelper" import ( - "context" "errors" "fmt" "go.opentelemetry.io/collector/component" "go.opentelemetry.io/collector/config/configmiddleware" - "go.opentelemetry.io/collector/extension" "go.opentelemetry.io/collector/extension/extensionlimiter" ) @@ -22,6 +20,30 @@ var ( ErrUnresolvedLimiter = errors.New("could not resolve middleware limiter") ) +// MiddlewareIsLimiter applies consistency checks and returns a valid +// limiter extension of any known kind. +func MiddlewareIsLimiter(host component.Host, middleware configmiddleware.Config) (extensionlimiter.BaseLimiterProvider, bool, error) { + exts := host.GetExtensions() + ext := exts[middleware.ID] + if ext == nil { + return nil, false, fmt.Errorf("%w: %s", ErrUnresolvedLimiter, middleware.ID) + } + _, isResource := ext.(extensionlimiter.ResourceLimiterProvider) + _, isRate := ext.(extensionlimiter.RateLimiterProvider) + base, isBase := ext.(extensionlimiter.BaseLimiterProvider) + + switch { + case isResource && isRate: + return nil, false, fmt.Errorf("%w: %s", ErrLimiterConflict, middleware.ID) + case isResource, isRate: + return base, true, nil + case isBase: + return base, true, nil + default: + return nil, false, nil + } +} + // MiddlewaresToLimiterWrapperProvider constructs a combined limiter // from an ordered list of middlewares. This constructor ignores // middleware configs that are not limiters. @@ -31,21 +53,20 @@ var ( // NewLimitedLogs) it will pass-through when the limiter is nil. func MiddlewaresToLimiterWrapperProvider(host component.Host, middleware []configmiddleware.Config) (LimiterWrapperProvider, error) { var retErr error - var providers []LimiterWrapperProvider + var providers []extensionlimiter.BaseLimiterProvider for _, mid := range middleware { - _, ok, err := MiddlewareIsLimiter(host, mid) + base, ok, err := MiddlewareIsLimiter(host, mid) retErr = errors.Join(retErr, err) if !ok { continue } - provider, err := MiddlewareToLimiterWrapperProvider(host, mid) - providers = append(providers, provider) + providers = append(providers, base) retErr = errors.Join(retErr, err) } if len(providers) == 0 { return nil, nil } - return MultiLimiterWrapperProvider(providers), nil + return MultiLimiterProvider(providers), nil } // Note: MiddlewaresToRateLimiterProvider, MiddlewaresToResourceLimiterProvider @@ -77,30 +98,6 @@ func MiddlewareToLimiterWrapperProvider(host component.Host, middleware configmi return nil, fmt.Errorf("%w: %s: unrecognized limiter", ErrNotALimiter, middleware.ID) } -// MiddlewareIsLimiter applies consistency checks and returns a valid -// limiter extension of any known kind. -func MiddlewareIsLimiter(host component.Host, middleware configmiddleware.Config) (extension.Extension, bool, error) { - exts := host.GetExtensions() - ext := exts[middleware.ID] - if ext == nil { - return nil, false, fmt.Errorf("%w: %s", ErrUnresolvedLimiter, middleware.ID) - } - _, isResource := ext.(extensionlimiter.ResourceLimiterProvider) - _, isRate := ext.(extensionlimiter.RateLimiterProvider) - _, isBase := ext.(extensionlimiter.BaseLimiterProvider) - - switch { - case isResource && isRate: - return ext, false, fmt.Errorf("%w: %s", ErrLimiterConflict, middleware.ID) - case isResource, isRate: - return ext, true, nil - case isBase: - return ext, true, nil - default: - return nil, false, nil - } -} - // MiddlewareToRateLimiterProvider allows a base limiter to act as a // rate. This encodes the fact that a resource limiter extension // cannot be adapted to a rate limiter interface. @@ -121,67 +118,47 @@ func MiddlewareToRateLimiterProvider(host component.Host, middleware configmiddl return nil, fmt.Errorf("%w: %s", ErrNotARateLimiter, middleware.ID) } -// MultiLimiterWrapperProvider combines multiple limiter wrappers -// providers into a single provider by sequencing wrapped limiters. -// Returns errors from the underlying LimiterWrapper() calls, if any. -type MultiLimiterWrapperProvider []LimiterWrapperProvider - -var _ LimiterWrapperProvider = MultiLimiterWrapperProvider{} - -// GetLimiterWrapper implements LimiterWrapperProvider, combining -// checkers for all wrappers in a sequence. -func (ps MultiLimiterWrapperProvider) GetBaseLimiter(opts ...extensionlimiter.Option) (extensionlimiter.BaseLimiter, error) { - var retErr error - var cks MultiBaseLimiter - for _, provider := range ps { - ck, err := provider.GetBaseLimiter(opts...) - retErr = errors.Join(retErr, err) - if ck == nil { - continue - } - cks = append(cks, ck) +// MiddlewareToResourceLimiterProvider allows a base limiter to act as a +// resource. This encodes the fact that a resource limiter extension +// cannot be adapted to a resource limiter interface. +func MiddlewareToResourceLimiterProvider(host component.Host, middleware configmiddleware.Config) (extensionlimiter.ResourceLimiterProvider, error) { + ext, ok, err := MiddlewareIsLimiter(host, middleware) + if err != nil { + return nil, err } - if len(cks) == 0 { - return extensionlimiter.MustDenyFunc(nil), retErr + if !ok { + return nil, fmt.Errorf("%w: %s", ErrNotALimiter, middleware.ID) } - return cks, retErr + return getProvider(ext, + BaseToResourceLimiterProvider, + RateToResourceLimiterProvider, + identity[extensionlimiter.ResourceLimiterProvider], + ) } -// GetLimiterWrapper implements LimiterWrapperProvider, calling -// wrappers in a sequence. -func (ps MultiLimiterWrapperProvider) GetLimiterWrapper(key extensionlimiter.WeightKey, opts ...extensionlimiter.Option) (LimiterWrapper, error) { - // Map provider list to limiter list. - var lims []LimiterWrapper - - for _, provider := range ps { - lim, err := provider.GetLimiterWrapper(key, opts...) - if err == nil { - return nil, err - } - if lim == nil { - continue - } - lims = append(lims, lim) +func getProvider[Out any]( + ext extensionlimiter.BaseLimiterProvider, + base func(extensionlimiter.BaseLimiterProvider) (Out, error), + rate func(extensionlimiter.RateLimiterProvider) (Out, error), + resource func(extensionlimiter.ResourceLimiterProvider) (Out, error), +) (Out, error) { + if lim, ok := ext.(extensionlimiter.ResourceLimiterProvider); ok { + return resource(lim) } - - if len(lims) == 0 { - return LimiterWrapper(nil), nil + if lim, ok := ext.(extensionlimiter.RateLimiterProvider); ok { + return rate(lim) } - - return sequenceLimiters(lims), nil + if lim, ok := ext.(extensionlimiter.BaseLimiterProvider); ok { + return base(lim) + } + var out Out + return out, ErrNotALimiter } -func sequenceLimiters(lims []LimiterWrapper) LimiterWrapper { - if len(lims) == 1 { - return lims[0] - } - return composeLimiters(lims[0], sequenceLimiters(lims[1:])) +func identity[T any](lim T) (T, error) { + return lim, nil } -func composeLimiters(first, second LimiterWrapper) LimiterWrapper { - return LimiterWrapperFunc(func(ctx context.Context, value int, call func(ctx context.Context) error) error { - return first.LimitCall(ctx, value, func(ctx context.Context) error { - return second.LimitCall(ctx, value, call) - }) - }) +func nilError[T any](f func() T) (T, error) { + return f(), nil } diff --git a/extension/extensionlimiter/limiterhelper/multi.go b/extension/extensionlimiter/limiterhelper/multi.go new file mode 100644 index 000000000000..debda96045da --- /dev/null +++ b/extension/extensionlimiter/limiterhelper/multi.go @@ -0,0 +1,107 @@ +// Copyright The OpenTelemetry Authors +// SPDX-License-Identifier: Apache-2.0 + +package limiterhelper // import "go.opentelemetry.io/collector/extension/extensionlimiter/limiterhelper" + +import ( + "context" + "errors" + + "go.opentelemetry.io/collector/extension/extensionlimiter" +) + +// MultiBaseLimiter returns MustDeny when any element returns MustDeny. +type MultiBaseLimiter []extensionlimiter.BaseLimiter + +var _ extensionlimiter.BaseLimiter = MultiBaseLimiter{} + +// MustDeny implements BaseLimiter. +func (ls MultiBaseLimiter) MustDeny(ctx context.Context) error { + var err error + for _, lim := range ls { + if lim == nil { + continue + } + err = errors.Join(err, lim.MustDeny(ctx)) + } + return err +} + +// MultiLimiterProvider combines multiple limiters and implements all +// provider interfaces through use of the available adapters. +type MultiLimiterProvider []extensionlimiter.BaseLimiterProvider + +var _ LimiterWrapperProvider = MultiLimiterProvider{} +var _ extensionlimiter.RateLimiterProvider = MultiLimiterProvider{} +var _ extensionlimiter.ResourceLimiterProvider = MultiLimiterProvider{} +var _ extensionlimiter.BaseLimiterProvider = MultiLimiterProvider{} + +// GetLimiterWrapper implements LimiterWrapperProvider. The combined +// limiter is saturated when any of the base limiers are. +func (ps MultiLimiterProvider) GetBaseLimiter(opts ...extensionlimiter.Option) (extensionlimiter.BaseLimiter, error) { + var retErr error + var cks MultiBaseLimiter + for _, provider := range ps { + ck, err := provider.GetBaseLimiter(opts...) + retErr = errors.Join(retErr, err) + if ck == nil { + continue + } + cks = append(cks, ck) + } + if len(cks) == 0 { + return extensionlimiter.MustDenyFunc(nil), retErr + } + if len(cks) == 1 { + return cks[0], retErr + } + return cks, retErr +} + +// GetLimiterWrapper implements LimiterWrapperProvider, wrappers in a +// sequence. +func (ps MultiLimiterProvider) GetLimiterWrapper(key extensionlimiter.WeightKey, opts ...extensionlimiter.Option) (LimiterWrapper, error) { + // Map provider list to limiter list. + var lims []LimiterWrapper + + for _, baseProvider := range ps { + provider, err := getProvider( + baseProvider, + BaseToLimiterWrapperProvider, + RateToLimiterWrapperProvider, + ResourceToLimiterWrapperProvider, + ) + if err == nil { + return nil, err + } + lim, err := provider.GetLimiterWrapper(key, opts...) + if err == nil { + return nil, err + } + if lim == nil { + continue + } + lims = append(lims, lim) + } + + if len(lims) == 0 { + return LimiterWrapperFunc(nil), nil + } + + return sequenceLimiters(lims), nil +} + +func sequenceLimiters(lims []LimiterWrapper) LimiterWrapper { + if len(lims) == 1 { + return lims[0] + } + return composeLimiters(lims[0], sequenceLimiters(lims[1:])) +} + +func composeLimiters(first, second LimiterWrapper) LimiterWrapper { + return LimiterWrapperFunc(func(ctx context.Context, value int, call func(ctx context.Context) error) error { + return first.LimitCall(ctx, value, func(ctx context.Context) error { + return second.LimitCall(ctx, value, call) + }) + }) +} diff --git a/extension/extensionlimiter/limiterhelper/rate.go b/extension/extensionlimiter/limiterhelper/rate.go index ed2069126c4a..13f6a5b1d43a 100644 --- a/extension/extensionlimiter/limiterhelper/rate.go +++ b/extension/extensionlimiter/limiterhelper/rate.go @@ -122,3 +122,45 @@ func (b BlockingRateLimiter) waitFor(ctx context.Context, value int, timer timer return context.Cause(ctx) } } + +// RateToResourceLimiterProvider allows a rate limiter to act as a +// resource limiter. Note that the opposite direction (i.e., resource +// limter acting as rate limiter) is an invalid configuration. +func RateToResourceLimiterProvider(blimp extensionlimiter.RateLimiterProvider) (extensionlimiter.ResourceLimiterProvider, error) { + return struct { + extensionlimiter.GetBaseLimiterFunc + extensionlimiter.GetRateLimiterFunc + }{ + blimp.GetBaseLimiter, + func(_ extensionlimiter.WeightKey, opts ...extensionlimiter.Option) (extensionlimiter.ResourceLimiter, error) { + base, err := blimp.GetRateLimiter(opts...) + if err != nil { + return nil, err + } + return extensionlimiter.ReserveResourceFunc( + func(ctx context.Context, value int) (extensionlimiter.ResourceReservation, error) { + rsv, err := base.ReserveRate(ctx, value) + if err != nil { + return nil, err + } + cch := make(chan struct{}) + tch := time.After(rsv.WaitTime()) + go func() { + select { + case <-ctx.Done(): + rsv.Cancel() + case <-tch: + close(cch) + } + }() + return struct { + extensionlimiter.DelayFunc + extensionlimiter.ReleaseFunc + }{ + func() <-chan struct{} { return cch }, + func() {}, + }, nil + }), nil + }, + }, nil +} diff --git a/extension/extensionlimiter/limiterhelper/wrapper.go b/extension/extensionlimiter/limiterhelper/wrapper.go index 535f07125579..de8cb05a139f 100644 --- a/extension/extensionlimiter/limiterhelper/wrapper.go +++ b/extension/extensionlimiter/limiterhelper/wrapper.go @@ -9,10 +9,7 @@ import ( "go.opentelemetry.io/collector/extension/extensionlimiter" ) -// LimiterWrapperProvider provides access to LimiterWrappers, which is -// the appropriate interface for callers that can easily wrap a -// function call, because for wrapped calls there is no distinction -// between rate limiters and resource limiters. +// @@@ Why not ... type LimiterWrapperProvider interface { extensionlimiter.BaseLimiterProvider @@ -30,10 +27,12 @@ func (f GetLimiterWrapperFunc) GetLimiterWrapper(key extensionlimiter.WeightKey, return f(key, opts...) } -var _ LimiterWrapperProvider = struct { +type limiterWrapper struct { GetLimiterWrapperFunc extensionlimiter.GetBaseLimiterFunc -}{} +} + +var _ LimiterWrapperProvider = limiterWrapper{} // LimiterWrapper is a general-purpose interface for limiter consumers // to limit resources with use of a callback. This is the simplest @@ -70,26 +69,19 @@ func (f LimiterWrapperFunc) LimitCall(ctx context.Context, value int, call func( return f(ctx, value, call) } -// wrapperProvider is a combinator for building wrapper providers from -// the underlying limter types. -type wrapperProvider struct { - GetLimiterWrapperFunc - extensionlimiter.GetBaseLimiterFunc -} - -// NewBaseLimiterWrapperProvider constructs a LimiterWrapperProvider +// BaseToLimiterWrapperProvider constructs a LimiterWrapperProvider // for a rate limiter extension. -func NewBaseLimiterWrapperProvider(rp extensionlimiter.BaseLimiterProvider) LimiterWrapperProvider { - return wrapperProvider{ +func BaseToLimiterWrapperProvider(rp extensionlimiter.BaseLimiterProvider) (LimiterWrapperProvider, error) { + return limiterWrapper{ GetBaseLimiterFunc: rp.GetBaseLimiter, - GetLimiterWrapperFunc: GetLimiterWrapperFunc(nil), - } + GetLimiterWrapperFunc: nil, + }, nil } -// NewResourceLimiterWrapperProvider constructs a +// ResourceToLimiterWrapperProvider constructs a // LimiterWrapperProvider for a resource limiter extension. -func NewResourceLimiterWrapperProvider(rp extensionlimiter.ResourceLimiterProvider) LimiterWrapperProvider { - return wrapperProvider{ +func ResourceToLimiterWrapperProvider(rp extensionlimiter.ResourceLimiterProvider) (LimiterWrapperProvider, error) { + return limiterWrapper{ GetBaseLimiterFunc: rp.GetBaseLimiter, GetLimiterWrapperFunc: func(key extensionlimiter.WeightKey, opts ...extensionlimiter.Option) (LimiterWrapper, error) { lim, err := rp.GetResourceLimiter(key, opts...) @@ -109,13 +101,13 @@ func NewResourceLimiterWrapperProvider(rp extensionlimiter.ResourceLimiterProvid return call(ctx) }), nil }, - } + }, nil } -// NewRateLimiterWrapperProvider constructs a LimiterWrapperProvider +// RateToLimiterWrapperProvider constructs a LimiterWrapperProvider // for a rate limiter extension. -func NewRateLimiterWrapperProvider(rp extensionlimiter.RateLimiterProvider) LimiterWrapperProvider { - return wrapperProvider{ +func RateToLimiterWrapperProvider(rp extensionlimiter.RateLimiterProvider) (LimiterWrapperProvider, error) { + return limiterWrapper{ GetBaseLimiterFunc: rp.GetBaseLimiter, GetLimiterWrapperFunc: func(key extensionlimiter.WeightKey, opts ...extensionlimiter.Option) (LimiterWrapper, error) { lim, err := rp.GetRateLimiter(key, opts...) @@ -133,5 +125,5 @@ func NewRateLimiterWrapperProvider(rp extensionlimiter.RateLimiterProvider) Limi return call(ctx) }), nil }, - } + }, nil } diff --git a/extension/extensionlimiter/resource.go b/extension/extensionlimiter/resource.go index 90fded5c3ab0..dfd1538bec19 100644 --- a/extension/extensionlimiter/resource.go +++ b/extension/extensionlimiter/resource.go @@ -121,10 +121,7 @@ func (f ReserveResourceFunc) ReserveResource(ctx context.Context, value int) (Re return struct { DelayFunc ReleaseFunc - }{ - DelayFunc(nil), - ReleaseFunc(nil), - }, nil + }{}, nil } return f(ctx, value) } From 1fd254cdcc6cd3e0268de8939e65e6a97d52c0de Mon Sep 17 00:00:00 2001 From: Joshua MacDonald Date: Wed, 21 May 2025 16:28:41 -0700 Subject: [PATCH 24/47] comments --- .../extensionlimiter/limiterhelper/base.go | 8 +- .../limiterhelper/consumer.go | 4 +- .../limiterhelper/middleware.go | 154 +++++----- .../extensionlimiter/limiterhelper/multi.go | 272 +++++++++++++----- .../extensionlimiter/limiterhelper/rate.go | 12 +- .../extensionlimiter/limiterhelper/wrapper.go | 15 +- 6 files changed, 313 insertions(+), 152 deletions(-) diff --git a/extension/extensionlimiter/limiterhelper/base.go b/extension/extensionlimiter/limiterhelper/base.go index dacf20864d2f..c01538d62add 100644 --- a/extension/extensionlimiter/limiterhelper/base.go +++ b/extension/extensionlimiter/limiterhelper/base.go @@ -11,7 +11,7 @@ import ( // BaseToRateLimiterProvider allows a base limiter to act as a rate // limiter. -func BaseToRateLimiterProvider(blimp extensionlimiter.BaseLimiterProvider) (extensionlimiter.RateLimiterProvider, error) { +func BaseToRateLimiterProvider(blimp extensionlimiter.BaseLimiterProvider) extensionlimiter.RateLimiterProvider { return struct { extensionlimiter.GetBaseLimiterFunc extensionlimiter.GetRateLimiterFunc @@ -33,12 +33,12 @@ func BaseToRateLimiterProvider(blimp extensionlimiter.BaseLimiterProvider) (exte }{}, nil }), nil }, - }, nil + } } // BaseToResourceLimiterProvider allows a base limiter to act as a // resource limiter. -func BaseToResourceLimiterProvider(blimp extensionlimiter.BaseLimiterProvider) (extensionlimiter.ResourceLimiterProvider, error) { +func BaseToResourceLimiterProvider(blimp extensionlimiter.BaseLimiterProvider) extensionlimiter.ResourceLimiterProvider { return struct { extensionlimiter.GetBaseLimiterFunc extensionlimiter.GetResourceLimiterFunc @@ -60,5 +60,5 @@ func BaseToResourceLimiterProvider(blimp extensionlimiter.BaseLimiterProvider) ( }{}, nil }), nil }, - }, nil + } } diff --git a/extension/extensionlimiter/limiterhelper/consumer.go b/extension/extensionlimiter/limiterhelper/consumer.go index ee521e994436..fc57364d67d7 100644 --- a/extension/extensionlimiter/limiterhelper/consumer.go +++ b/extension/extensionlimiter/limiterhelper/consumer.go @@ -5,7 +5,6 @@ package limiterhelper // import "go.opentelemetry.io/collector/extension/extensi import ( "context" - "errors" "slices" "go.opentelemetry.io/collector/consumer" @@ -15,6 +14,7 @@ import ( "go.opentelemetry.io/collector/pdata/pmetric" "go.opentelemetry.io/collector/pdata/pprofile" "go.opentelemetry.io/collector/pdata/ptrace" + "go.uber.org/multierr" ) // Traits object interface is generalized by P the pipeline data type @@ -189,7 +189,7 @@ func newLimited[P any, C any]( return 1 }) next, err4 = applyBaseLimiter(next, provider, m, opts) - return next, errors.Join(err1, err2, err3, err4) + return next, multierr.Append(err1, multierr.Append(err2, multierr.Append(err3, err4))) } // NewLimitedTraces applies a limiter using the provider over keys before calling next. diff --git a/extension/extensionlimiter/limiterhelper/middleware.go b/extension/extensionlimiter/limiterhelper/middleware.go index 9efd93bbf333..9897f96a2c30 100644 --- a/extension/extensionlimiter/limiterhelper/middleware.go +++ b/extension/extensionlimiter/limiterhelper/middleware.go @@ -10,14 +10,14 @@ import ( "go.opentelemetry.io/collector/component" "go.opentelemetry.io/collector/config/configmiddleware" "go.opentelemetry.io/collector/extension/extensionlimiter" + "go.uber.org/multierr" ) var ( - ErrNotALimiter = errors.New("middleware is not a limiter") - ErrNotARateLimiter = errors.New("middleware is not a rate or base limiter") - ErrNotAResourceLimiter = errors.New("middleware is not a resource or base limiter") - ErrLimiterConflict = errors.New("limiter implements both rate and resource-limiters") - ErrUnresolvedLimiter = errors.New("could not resolve middleware limiter") + ErrNotALimiter = errors.New("middleware is not a limiter") + ErrNotARateLimiter = errors.New("middleware cannot implement rate limiter") + ErrLimiterConflict = errors.New("limiter implements both rate and resource-limiters") + ErrUnresolvedLimiter = errors.New("could not resolve middleware limiter") ) // MiddlewareIsLimiter applies consistency checks and returns a valid @@ -44,98 +44,109 @@ func MiddlewareIsLimiter(host component.Host, middleware configmiddleware.Config } } -// MiddlewaresToLimiterWrapperProvider constructs a combined limiter +// MiddlewaresToLimiterProvider constructs a combined limiter // from an ordered list of middlewares. This constructor ignores // middleware configs that are not limiters. -// -// When no limiters are found (with no errors), the returned provider -// is nil. When a nil is passed to the consumer helpers (e.g., -// NewLimitedLogs) it will pass-through when the limiter is nil. -func MiddlewaresToLimiterWrapperProvider(host component.Host, middleware []configmiddleware.Config) (LimiterWrapperProvider, error) { +func MiddlewaresToLimiterProvider(host component.Host, middleware []configmiddleware.Config) (MultiLimiterProvider, error) { var retErr error - var providers []extensionlimiter.BaseLimiterProvider + var providers MultiLimiterProvider for _, mid := range middleware { base, ok, err := MiddlewareIsLimiter(host, mid) - retErr = errors.Join(retErr, err) + retErr = multierr.Append(retErr, err) if !ok { continue } providers = append(providers, base) - retErr = errors.Join(retErr, err) + retErr = multierr.Append(retErr, err) } if len(providers) == 0 { return nil, nil } - return MultiLimiterProvider(providers), nil + return providers, nil } -// Note: MiddlewaresToRateLimiterProvider, MiddlewaresToResourceLimiterProvider -// are needed for special cases, however these functions can be implemented -// manually, they are similar to the above. +// MiddlewareToBaseLimiterProvider returns a base limiter provider +// from middleware. Returns a package-level error if the middleware +// does not implement exactly one of the limiter interfaces (i.e., +// rate or resource). +func MiddlewareToBaseLimiterProvider(host component.Host, middleware configmiddleware.Config) (extensionlimiter.BaseLimiterProvider, error) { + return getMiddleware( + host, + middleware, + identity[extensionlimiter.BaseLimiterProvider], + baseProvider[extensionlimiter.RateLimiterProvider], + baseProvider[extensionlimiter.ResourceLimiterProvider], + ) +} // MiddlewareToLimiterWrapperProvider returns a limiter wrapper // provider from middleware. Returns a package-level error if the // middleware does not implement exactly one of the limiter // interfaces (i.e., rate or resource). func MiddlewareToLimiterWrapperProvider(host component.Host, middleware configmiddleware.Config) (LimiterWrapperProvider, error) { - ext, ok, err := MiddlewareIsLimiter(host, middleware) - if err != nil { - return nil, err - } - if !ok { - return nil, fmt.Errorf("%w: %s", ErrNotALimiter, middleware.ID) - } - if lim, ok := ext.(extensionlimiter.ResourceLimiterProvider); ok { - return NewResourceLimiterWrapperProvider(lim), nil - } - if lim, ok := ext.(extensionlimiter.RateLimiterProvider); ok { - return NewRateLimiterWrapperProvider(lim), nil - } - if lim, ok := ext.(extensionlimiter.BaseLimiterProvider); ok { - return NewBaseLimiterWrapperProvider(lim), nil - } - // This is an internal error. - return nil, fmt.Errorf("%w: %s: unrecognized limiter", ErrNotALimiter, middleware.ID) + return getMiddleware( + host, + middleware, + nilError(BaseToLimiterWrapperProvider), + nilError(RateToLimiterWrapperProvider), + nilError(ResourceToLimiterWrapperProvider), + ) } -// MiddlewareToRateLimiterProvider allows a base limiter to act as a -// rate. This encodes the fact that a resource limiter extension -// cannot be adapted to a rate limiter interface. +// MiddlewareToRateLimiterProvider allows a base limiter provider to +// act as a rate limiter provider. This encodes the fact that a +// resource limiter extension cannot be adapted to a rate limiter +// interface. Returns a package-level error if the middleware does not +// implement exactly one of the limiter interfaces (i.e., rate or +// resource). func MiddlewareToRateLimiterProvider(host component.Host, middleware configmiddleware.Config) (extensionlimiter.RateLimiterProvider, error) { - ext, ok, err := MiddlewareIsLimiter(host, middleware) - if err != nil { - return nil, err - } - if !ok { - return nil, fmt.Errorf("%w: %s", ErrNotALimiter, middleware.ID) - } - if rlimp, ok := ext.(extensionlimiter.RateLimiterProvider); ok { - return rlimp, nil - } - if blimp, ok := ext.(extensionlimiter.BaseLimiterProvider); ok { - return BaseToRateLimiterProvider(blimp) - } - return nil, fmt.Errorf("%w: %s", ErrNotARateLimiter, middleware.ID) + return getMiddleware( + host, + middleware, + nilError(BaseToRateLimiterProvider), + identity[extensionlimiter.RateLimiterProvider], + resourceToRateLimiterError, + ) } -// MiddlewareToResourceLimiterProvider allows a base limiter to act as a -// resource. This encodes the fact that a resource limiter extension -// cannot be adapted to a resource limiter interface. +// MiddlewareToResourceLimiterProvider allows a base limiter provider +// to act as a resource provider. This enforces that a resource +// limiter extension cannot be adapted to a resource limiter +// interface. Returns a package-level error if the middleware does not +// implement exactly one of the limiter interfaces (i.e., rate or +// resource). func MiddlewareToResourceLimiterProvider(host component.Host, middleware configmiddleware.Config) (extensionlimiter.ResourceLimiterProvider, error) { + return getMiddleware( + host, + middleware, + nilError(BaseToResourceLimiterProvider), + nilError(RateToResourceLimiterProvider), + identity[extensionlimiter.ResourceLimiterProvider], + ) +} + +// getProvider invokes getProvider if any kind of limiter is detected +// for the given host and middleware configuration. +func getMiddleware[Out any]( + host component.Host, + middleware configmiddleware.Config, + base func(extensionlimiter.BaseLimiterProvider) (Out, error), + rate func(extensionlimiter.RateLimiterProvider) (Out, error), + resource func(extensionlimiter.ResourceLimiterProvider) (Out, error), +) (Out, error) { + var out Out ext, ok, err := MiddlewareIsLimiter(host, middleware) if err != nil { - return nil, err + return out, err } if !ok { - return nil, fmt.Errorf("%w: %s", ErrNotALimiter, middleware.ID) + return out, fmt.Errorf("%w: %s", ErrNotALimiter, middleware.ID) } - return getProvider(ext, - BaseToResourceLimiterProvider, - RateToResourceLimiterProvider, - identity[extensionlimiter.ResourceLimiterProvider], - ) + return getProvider(ext, base, rate, resource) } +// getProvider handles each limiter kind, case-by-case, for building +// limiters in a functional style. func getProvider[Out any]( ext extensionlimiter.BaseLimiterProvider, base func(extensionlimiter.BaseLimiterProvider) (Out, error), @@ -155,10 +166,23 @@ func getProvider[Out any]( return out, ErrNotALimiter } +// identity is a pass-through for the correct provider type. func identity[T any](lim T) (T, error) { return lim, nil } -func nilError[T any](f func() T) (T, error) { - return f(), nil +// baseProvider returns a base limiter type from any limiter. +func baseProvider[T extensionlimiter.BaseLimiterProvider](p T) (extensionlimiter.BaseLimiterProvider, error) { + return p, nil +} + +// nilError converts an infallible constructor to return a nil error. +func nilError[S, T any](f func(S) T) func(S) (T, error) { + return func(s S) (T, error) { return f(s), nil } +} + +// resourceToRateLimiterError represents the impossible conversion +// from resource limiter to rate limiter. +func resourceToRateLimiterError(_ extensionlimiter.ResourceLimiterProvider) (extensionlimiter.RateLimiterProvider, error) { + return nil, ErrNotARateLimiter } diff --git a/extension/extensionlimiter/limiterhelper/multi.go b/extension/extensionlimiter/limiterhelper/multi.go index debda96045da..c56b62f2260e 100644 --- a/extension/extensionlimiter/limiterhelper/multi.go +++ b/extension/extensionlimiter/limiterhelper/multi.go @@ -5,30 +5,15 @@ package limiterhelper // import "go.opentelemetry.io/collector/extension/extensi import ( "context" - "errors" + "time" "go.opentelemetry.io/collector/extension/extensionlimiter" + "go.uber.org/multierr" ) -// MultiBaseLimiter returns MustDeny when any element returns MustDeny. -type MultiBaseLimiter []extensionlimiter.BaseLimiter - -var _ extensionlimiter.BaseLimiter = MultiBaseLimiter{} - -// MustDeny implements BaseLimiter. -func (ls MultiBaseLimiter) MustDeny(ctx context.Context) error { - var err error - for _, lim := range ls { - if lim == nil { - continue - } - err = errors.Join(err, lim.MustDeny(ctx)) - } - return err -} - -// MultiLimiterProvider combines multiple limiters and implements all -// provider interfaces through use of the available adapters. +// MultiLimiterProvider combines multiple limiter providers of all +// kinds. It automatically applies the adapters in this package to +// implement the desired provider interface from the base object. type MultiLimiterProvider []extensionlimiter.BaseLimiterProvider var _ LimiterWrapperProvider = MultiLimiterProvider{} @@ -36,72 +21,223 @@ var _ extensionlimiter.RateLimiterProvider = MultiLimiterProvider{} var _ extensionlimiter.ResourceLimiterProvider = MultiLimiterProvider{} var _ extensionlimiter.BaseLimiterProvider = MultiLimiterProvider{} -// GetLimiterWrapper implements LimiterWrapperProvider. The combined +// GetBaseLimiter implements LimiterWrapperProvider. The combined // limiter is saturated when any of the base limiers are. -func (ps MultiLimiterProvider) GetBaseLimiter(opts ...extensionlimiter.Option) (extensionlimiter.BaseLimiter, error) { - var retErr error - var cks MultiBaseLimiter - for _, provider := range ps { - ck, err := provider.GetBaseLimiter(opts...) - retErr = errors.Join(retErr, err) - if ck == nil { - continue +func (ps MultiLimiterProvider) GetBaseLimiter( + opts ...extensionlimiter.Option, +) (extensionlimiter.BaseLimiter, error) { + var noop extensionlimiter.BaseLimiter = extensionlimiter.MustDenyFunc(nil) + return getMultiLimiter(ps, + identity[extensionlimiter.BaseLimiterProvider], + baseProvider[extensionlimiter.RateLimiterProvider], + baseProvider[extensionlimiter.ResourceLimiterProvider], + noop, + func(p extensionlimiter.BaseLimiterProvider) (extensionlimiter.BaseLimiter, error) { + return p.GetBaseLimiter(opts...) + }, + combineBaseLimiters) +} + +// GetLimiterWrapper implements LimiterWrapperProvider, applies the +// wrappers in a nested sequence. +func (ps MultiLimiterProvider) GetLimiterWrapper( + key extensionlimiter.WeightKey, + opts ...extensionlimiter.Option, +) (LimiterWrapper, error) { + var noop LimiterWrapper = LimiterWrapperFunc(nil) + return getMultiLimiter(ps, + nilError(BaseToLimiterWrapperProvider), + nilError(RateToLimiterWrapperProvider), + nilError(ResourceToLimiterWrapperProvider), + noop, + func(p LimiterWrapperProvider) (LimiterWrapper, error) { + return p.GetLimiterWrapper(key, opts...) + }, + combineLimiterWrappers) +} + +// GetResourceLimiter implements ResourceLimiterProvider, applies the +// request to all limiters (unless any are saturated). +func (ps MultiLimiterProvider) GetResourceLimiter( + key extensionlimiter.WeightKey, + opts ...extensionlimiter.Option, +) (extensionlimiter.ResourceLimiter, error) { + var noop extensionlimiter.ResourceLimiter = extensionlimiter.ReserveResourceFunc(nil) + return getMultiLimiter(ps, + nilError(BaseToResourceLimiterProvider), + nilError(RateToResourceLimiterProvider), + identity[extensionlimiter.ResourceLimiterProvider], + noop, + func(p extensionlimiter.ResourceLimiterProvider) (extensionlimiter.ResourceLimiter, error) { + return p.GetResourceLimiter(key, opts...) + }, + combineResourceLimiters) +} + +// GetRateLimiter implements RateLimiterProvider, applies the request +// to all limiters, returns the maximum wait time. +func (ps MultiLimiterProvider) GetRateLimiter( + key extensionlimiter.WeightKey, + opts ...extensionlimiter.Option, +) (extensionlimiter.RateLimiter, error) { + var noop extensionlimiter.RateLimiter = extensionlimiter.ReserveRateFunc(nil) + return getMultiLimiter(ps, + nilError(BaseToRateLimiterProvider), + identity[extensionlimiter.RateLimiterProvider], + resourceToRateLimiterError, + noop, + func(p extensionlimiter.RateLimiterProvider) (extensionlimiter.RateLimiter, error) { + return p.GetRateLimiter(key, opts...) + }, + combineRateLimiters) +} + +// combineBaseLimiters combines >= 2 base limiters. +func combineBaseLimiters(lims []extensionlimiter.BaseLimiter) extensionlimiter.BaseLimiter { + return extensionlimiter.MustDenyFunc(func(ctx context.Context) error { + var err error + for _, lim := range lims { + if lim == nil { + continue + } + err = multierr.Append(err, lim.MustDeny(ctx)) } - cks = append(cks, ck) + return err + }) +} + +// combineLimiterWrappers combines >= 2 limiter wrappers (recursive). +func combineLimiterWrappers(lims []LimiterWrapper) LimiterWrapper { + if len(lims) == 1 { + return lims[0] } - if len(cks) == 0 { - return extensionlimiter.MustDenyFunc(nil), retErr + return sequenceLimiterWrappers(lims[0], combineLimiterWrappers(lims[1:])) +} + +// sequenceLimiterWrappers combines 2 limiter wrappers. +func sequenceLimiterWrappers(first, second LimiterWrapper) LimiterWrapper { + return LimiterWrapperFunc(func(ctx context.Context, value int, call func(ctx context.Context) error) error { + return first.LimitCall(ctx, value, func(ctx context.Context) error { + return second.LimitCall(ctx, value, call) + }) + }) +} + +// combineRateLimiters combines >=2 resource limiters. +func combineResourceLimiters(lims []extensionlimiter.ResourceLimiter) extensionlimiter.ResourceLimiter { + reserve := func(ctx context.Context, value int) (extensionlimiter.ResourceReservation, error) { + var err error + rsvs := make([]extensionlimiter.ResourceReservation, 0, len(lims)) + for _, lim := range lims { + rsv, err := lim.ReserveResource(ctx, value) + err = multierr.Append(err, err) + rsvs = append(rsvs, rsv) + } + release := func() { + for _, rsv := range rsvs { + rsv.Release() + } + } + if err != nil { + release() + return nil, err + } + ch := make(chan struct{}) + go func() { + for _, rsv := range rsvs { + select { + case <-rsv.Delay(): + continue + case <-ctx.Done(): + return + } + } + close(ch) + return + }() + return struct { + extensionlimiter.DelayFunc + extensionlimiter.ReleaseFunc + }{ + func() <-chan struct{} { + return ch + }, + release, + }, nil } - if len(cks) == 1 { - return cks[0], retErr + return extensionlimiter.ReserveResourceFunc(reserve) +} + +// combineRateLimiters combines >=2 rate limiters. +func combineRateLimiters(lims []extensionlimiter.RateLimiter) extensionlimiter.RateLimiter { + reserve := func(ctx context.Context, value int) (extensionlimiter.RateReservation, error) { + var err error + rsvs := make([]extensionlimiter.RateReservation, 0, len(lims)) + for _, lim := range lims { + rsv, err := lim.ReserveRate(ctx, value) + err = multierr.Append(err, err) + if rsv != nil { + rsvs = append(rsvs, rsv) + } + } + cancel := func() { + for _, rsv := range rsvs { + rsv.Cancel() + } + } + var wt time.Duration + for _, rsv := range rsvs { + wt = max(wt, rsv.WaitTime()) + } + if err != nil { + cancel() + return nil, err + } + return struct { + extensionlimiter.WaitTimeFunc + extensionlimiter.CancelFunc + }{ + func() time.Duration { return wt }, + cancel, + }, nil } - return cks, retErr + return extensionlimiter.ReserveRateFunc(reserve) } -// GetLimiterWrapper implements LimiterWrapperProvider, wrappers in a -// sequence. -func (ps MultiLimiterProvider) GetLimiterWrapper(key extensionlimiter.WeightKey, opts ...extensionlimiter.Option) (LimiterWrapper, error) { - // Map provider list to limiter list. - var lims []LimiterWrapper - - for _, baseProvider := range ps { - provider, err := getProvider( - baseProvider, - BaseToLimiterWrapperProvider, - RateToLimiterWrapperProvider, - ResourceToLimiterWrapperProvider, - ) +// getMultiLimiter combines multiple providers (all kinds), gets +// limiters from each, and returns the combined result or error. +func getMultiLimiter[Out, Lim comparable]( + multi MultiLimiterProvider, + base func(extensionlimiter.BaseLimiterProvider) (Out, error), + rate func(extensionlimiter.RateLimiterProvider) (Out, error), + resource func(extensionlimiter.ResourceLimiterProvider) (Out, error), + noop Lim, + pfunc func(Out) (Lim, error), + combine func([]Lim) Lim, +) (Lim, error) { + var lims []Lim + + for _, baseProvider := range multi { + provider, err := getProvider(baseProvider, base, rate, resource) if err == nil { - return nil, err + return noop, err } - lim, err := provider.GetLimiterWrapper(key, opts...) + lim, err := pfunc(provider) if err == nil { - return nil, err + return noop, err } - if lim == nil { + var zero Lim + if lim == zero { continue } lims = append(lims, lim) } if len(lims) == 0 { - return LimiterWrapperFunc(nil), nil + return noop, nil } - - return sequenceLimiters(lims), nil -} - -func sequenceLimiters(lims []LimiterWrapper) LimiterWrapper { if len(lims) == 1 { - return lims[0] + return lims[0], nil } - return composeLimiters(lims[0], sequenceLimiters(lims[1:])) -} - -func composeLimiters(first, second LimiterWrapper) LimiterWrapper { - return LimiterWrapperFunc(func(ctx context.Context, value int, call func(ctx context.Context) error) error { - return first.LimitCall(ctx, value, func(ctx context.Context) error { - return second.LimitCall(ctx, value, call) - }) - }) + return combine(lims), nil } diff --git a/extension/extensionlimiter/limiterhelper/rate.go b/extension/extensionlimiter/limiterhelper/rate.go index 13f6a5b1d43a..609f77b8cce1 100644 --- a/extension/extensionlimiter/limiterhelper/rate.go +++ b/extension/extensionlimiter/limiterhelper/rate.go @@ -126,20 +126,20 @@ func (b BlockingRateLimiter) waitFor(ctx context.Context, value int, timer timer // RateToResourceLimiterProvider allows a rate limiter to act as a // resource limiter. Note that the opposite direction (i.e., resource // limter acting as rate limiter) is an invalid configuration. -func RateToResourceLimiterProvider(blimp extensionlimiter.RateLimiterProvider) (extensionlimiter.ResourceLimiterProvider, error) { +func RateToResourceLimiterProvider(blimp extensionlimiter.RateLimiterProvider) extensionlimiter.ResourceLimiterProvider { return struct { extensionlimiter.GetBaseLimiterFunc - extensionlimiter.GetRateLimiterFunc + extensionlimiter.GetResourceLimiterFunc }{ blimp.GetBaseLimiter, - func(_ extensionlimiter.WeightKey, opts ...extensionlimiter.Option) (extensionlimiter.ResourceLimiter, error) { - base, err := blimp.GetRateLimiter(opts...) + func(weight extensionlimiter.WeightKey, opts ...extensionlimiter.Option) (extensionlimiter.ResourceLimiter, error) { + rlim, err := blimp.GetRateLimiter(weight, opts...) if err != nil { return nil, err } return extensionlimiter.ReserveResourceFunc( func(ctx context.Context, value int) (extensionlimiter.ResourceReservation, error) { - rsv, err := base.ReserveRate(ctx, value) + rsv, err := rlim.ReserveRate(ctx, value) if err != nil { return nil, err } @@ -162,5 +162,5 @@ func RateToResourceLimiterProvider(blimp extensionlimiter.RateLimiterProvider) ( }, nil }), nil }, - }, nil + } } diff --git a/extension/extensionlimiter/limiterhelper/wrapper.go b/extension/extensionlimiter/limiterhelper/wrapper.go index de8cb05a139f..9e1dc13e510d 100644 --- a/extension/extensionlimiter/limiterhelper/wrapper.go +++ b/extension/extensionlimiter/limiterhelper/wrapper.go @@ -9,7 +9,8 @@ import ( "go.opentelemetry.io/collector/extension/extensionlimiter" ) -// @@@ Why not ... +// LimiterWrapperProvider follows the provider pattern for +// the LimiterWrapper type type LimiterWrapperProvider interface { extensionlimiter.BaseLimiterProvider @@ -71,16 +72,16 @@ func (f LimiterWrapperFunc) LimitCall(ctx context.Context, value int, call func( // BaseToLimiterWrapperProvider constructs a LimiterWrapperProvider // for a rate limiter extension. -func BaseToLimiterWrapperProvider(rp extensionlimiter.BaseLimiterProvider) (LimiterWrapperProvider, error) { +func BaseToLimiterWrapperProvider(rp extensionlimiter.BaseLimiterProvider) LimiterWrapperProvider { return limiterWrapper{ GetBaseLimiterFunc: rp.GetBaseLimiter, GetLimiterWrapperFunc: nil, - }, nil + } } // ResourceToLimiterWrapperProvider constructs a // LimiterWrapperProvider for a resource limiter extension. -func ResourceToLimiterWrapperProvider(rp extensionlimiter.ResourceLimiterProvider) (LimiterWrapperProvider, error) { +func ResourceToLimiterWrapperProvider(rp extensionlimiter.ResourceLimiterProvider) LimiterWrapperProvider { return limiterWrapper{ GetBaseLimiterFunc: rp.GetBaseLimiter, GetLimiterWrapperFunc: func(key extensionlimiter.WeightKey, opts ...extensionlimiter.Option) (LimiterWrapper, error) { @@ -101,12 +102,12 @@ func ResourceToLimiterWrapperProvider(rp extensionlimiter.ResourceLimiterProvide return call(ctx) }), nil }, - }, nil + } } // RateToLimiterWrapperProvider constructs a LimiterWrapperProvider // for a rate limiter extension. -func RateToLimiterWrapperProvider(rp extensionlimiter.RateLimiterProvider) (LimiterWrapperProvider, error) { +func RateToLimiterWrapperProvider(rp extensionlimiter.RateLimiterProvider) LimiterWrapperProvider { return limiterWrapper{ GetBaseLimiterFunc: rp.GetBaseLimiter, GetLimiterWrapperFunc: func(key extensionlimiter.WeightKey, opts ...extensionlimiter.Option) (LimiterWrapper, error) { @@ -125,5 +126,5 @@ func RateToLimiterWrapperProvider(rp extensionlimiter.RateLimiterProvider) (Limi return call(ctx) }), nil }, - }, nil + } } From 68c4cce1f564433a86ac2669312702da20072689 Mon Sep 17 00:00:00 2001 From: Joshua MacDonald Date: Wed, 21 May 2025 16:52:24 -0700 Subject: [PATCH 25/47] goroutine -> AfterFunc --- .../limiterhelper/notification.go | 4 -- .../extensionlimiter/limiterhelper/rate.go | 22 ++++--- .../limiterhelper/resource.go | 64 ++++++++----------- 3 files changed, 39 insertions(+), 51 deletions(-) diff --git a/extension/extensionlimiter/limiterhelper/notification.go b/extension/extensionlimiter/limiterhelper/notification.go index 9ef2d4db3848..c54d21a75a8c 100644 --- a/extension/extensionlimiter/limiterhelper/notification.go +++ b/extension/extensionlimiter/limiterhelper/notification.go @@ -24,10 +24,6 @@ func (n *notification) hasBeen() bool { } } -func (n *notification) waitFor() { - <-n.c -} - func (n *notification) channel() <-chan struct{} { return n.c } diff --git a/extension/extensionlimiter/limiterhelper/rate.go b/extension/extensionlimiter/limiterhelper/rate.go index 609f77b8cce1..315e69aae304 100644 --- a/extension/extensionlimiter/limiterhelper/rate.go +++ b/extension/extensionlimiter/limiterhelper/rate.go @@ -144,21 +144,23 @@ func RateToResourceLimiterProvider(blimp extensionlimiter.RateLimiterProvider) e return nil, err } cch := make(chan struct{}) - tch := time.After(rsv.WaitTime()) - go func() { - select { - case <-ctx.Done(): - rsv.Cancel() - case <-tch: - close(cch) - } - }() + timer := time.AfterFunc(rsv.WaitTime(), func() { + close(cch) + }) return struct { extensionlimiter.DelayFunc extensionlimiter.ReleaseFunc }{ func() <-chan struct{} { return cch }, - func() {}, + func() { + select { + case <-cch: + // The timer fired + default: + rsv.Cancel() + timer.Stop() + } + }, }, nil }), nil }, diff --git a/extension/extensionlimiter/limiterhelper/resource.go b/extension/extensionlimiter/limiterhelper/resource.go index ffd90e010f23..eda2d6349cfd 100644 --- a/extension/extensionlimiter/limiterhelper/resource.go +++ b/extension/extensionlimiter/limiterhelper/resource.go @@ -73,44 +73,33 @@ func (bq *boundedQueue) ReserveResource(ctx context.Context, value int) (extensi element := bq.addWaiterLocked(value) waiter := element.Value.(*waiter) - select { - case <-waiter.notify.channel(): - return struct { - extensionlimiter.DelayFunc - extensionlimiter.ReleaseFunc - }{ - func() <-chan struct{} { - // The caller waits for this notification - // to use the resource. - return waiter.notify.channel() - }, - func() { - // This call returns the resource. - bq.lock.Lock() - defer bq.lock.Unlock() - + return struct { + extensionlimiter.DelayFunc + extensionlimiter.ReleaseFunc + }{ + func() <-chan struct{} { + // The caller waits for this notification + // to use the resource. + return waiter.notify.channel() + }, + func() { + // This call returns the resource. + bq.lock.Lock() + defer bq.lock.Unlock() + + if waiter.notify.hasBeen() { + // We were also admitted, which can happen + // concurrently with cancellation. Make sure + // to release since no one else will do it. bq.releaseLocked(value) - }, - }, nil - - case <-ctx.Done(): - bq.lock.Lock() - defer bq.lock.Unlock() - - if waiter.notify.hasBeen() { - // We were also admitted, which can happen - // concurrently with cancellation. Make sure - // to release since no one else will do it. - bq.releaseLocked(value) - } else { - // Remove ourselves from the list of waiters - // so that we can't be admitted in the future. - bq.removeWaiterLocked(value, element) - bq.admitWaitersLocked() - } - - return nil, context.Cause(ctx) - } + } else { + // Remove ourselves from the list of waiters + // so that we can't be admitted in the future. + bq.removeWaiterLocked(value, element) + bq.admitWaitersLocked() + } + }, + }, nil } func (bq *boundedQueue) admitWaitersLocked() { @@ -174,6 +163,7 @@ func (b BlockingResourceLimiter) WaitFor(ctx context.Context, value int) (extens } select { case <-ctx.Done(): + rsv.Release() return func() {}, context.Cause(ctx) case <-rsv.Delay(): return rsv.Release, nil From c11e76ec62c0ed58f39e11d1dbd4e5dc52e356df Mon Sep 17 00:00:00 2001 From: Joshua MacDonald Date: Wed, 21 May 2025 17:40:21 -0700 Subject: [PATCH 26/47] config middleware / otlp receiver update --- config/configmiddleware/configmiddleware.go | 67 +++++++++++++++- config/configmiddleware/go.mod | 16 +++- config/configmiddleware/go.sum | 15 ++++ extension/extensionlimiter/go.mod | 7 +- .../limiterhelper/grpc/grpclimiter.go | 14 ++-- .../limiterhelper/http/httplimiter.go | 14 ++-- .../limiterhelper/middleware.go | 78 ++++++------------- .../extensionlimiter/limiterhelper/multi.go | 3 +- receiver/otlpreceiver/go.mod | 6 +- receiver/otlpreceiver/go.sum | 2 + receiver/otlpreceiver/otlp.go | 13 +++- 11 files changed, 150 insertions(+), 85 deletions(-) diff --git a/config/configmiddleware/configmiddleware.go b/config/configmiddleware/configmiddleware.go index 831ba7766fec..b8150bea82e7 100644 --- a/config/configmiddleware/configmiddleware.go +++ b/config/configmiddleware/configmiddleware.go @@ -11,9 +11,13 @@ import ( "fmt" "net/http" + "go.uber.org/multierr" "google.golang.org/grpc" "go.opentelemetry.io/collector/component" + "go.opentelemetry.io/collector/extension/extensionlimiter" + grpclimiter "go.opentelemetry.io/collector/extension/extensionlimiter/limiterhelper/grpc" + httplimiter "go.opentelemetry.io/collector/extension/extensionlimiter/limiterhelper/http" "go.opentelemetry.io/collector/extension/extensionmiddleware" ) @@ -33,6 +37,10 @@ type Config struct { _ struct{} } +func resolveFailed(id component.ID) error { + return fmt.Errorf("failed to resolve middleware %q: %w", id, errMiddlewareNotFound) +} + // GetHTTPClientRoundTripper attempts to select the appropriate // extensionmiddleware.HTTPClient from the map of extensions, and // returns the HTTP client wrapper function. If a middleware is not @@ -43,9 +51,16 @@ func (m Config) GetHTTPClientRoundTripper(_ context.Context, extensions map[comp if client, ok := ext.(extensionmiddleware.HTTPClient); ok { return client.GetHTTPRoundTripper, nil } + if limiter, ok := ext.(extensionlimiter.BaseLimiterProvider); ok { + limiter, err := httplimiter.NewClientLimiter(limiter) + if err != nil { + return nil, err + } + return limiter.GetHTTPRoundTripper, nil + } return nil, errNotHTTPClient } - return nil, fmt.Errorf("failed to resolve middleware %q: %w", m.ID, errMiddlewareNotFound) + return nil, resolveFailed(m.ID) } // GetHTTPServerHandler attempts to select the appropriate @@ -58,10 +73,17 @@ func (m Config) GetHTTPServerHandler(_ context.Context, extensions map[component if server, ok := ext.(extensionmiddleware.HTTPServer); ok { return server.GetHTTPHandler, nil } + if limiter, ok := ext.(extensionlimiter.BaseLimiterProvider); ok { + limiter, err := httplimiter.NewServerLimiter(limiter) + if err != nil { + return nil, err + } + return limiter.GetHTTPHandler, nil + } return nil, errNotHTTPServer } - return nil, fmt.Errorf("failed to resolve middleware %q: %w", m.ID, errMiddlewareNotFound) + return nil, resolveFailed(m.ID) } // GetGRPCClientOptions attempts to select the appropriate @@ -73,9 +95,16 @@ func (m Config) GetGRPCClientOptions(_ context.Context, extensions map[component if client, ok := ext.(extensionmiddleware.GRPCClient); ok { return client.GetGRPCClientOptions() } + if limiter, ok := ext.(extensionlimiter.BaseLimiterProvider); ok { + lim, err := grpclimiter.NewClientLimiter(limiter) + if err != nil { + return nil, err + } + return lim.GetGRPCClientOptions() + } return nil, errNotGRPCClient } - return nil, fmt.Errorf("failed to resolve middleware %q: %w", m.ID, errMiddlewareNotFound) + return nil, resolveFailed(m.ID) } // GetGRPCServerOptions attempts to select the appropriate @@ -87,8 +116,38 @@ func (m Config) GetGRPCServerOptions(_ context.Context, extensions map[component if server, ok := ext.(extensionmiddleware.GRPCServer); ok { return server.GetGRPCServerOptions() } + if limiter, ok := ext.(extensionlimiter.BaseLimiterProvider); ok { + lim, err := grpclimiter.NewServerLimiter(limiter) + if err != nil { + return nil, err + } + return lim.GetGRPCServerOptions() + } return nil, errNotGRPCServer } - return nil, fmt.Errorf("failed to resolve middleware %q: %w", m.ID, errMiddlewareNotFound) + return nil, resolveFailed(m.ID) +} + +// GetBaseLimiters gets a list of basic limiters. These can be +// upgraded to any kind of limiter, subject to restrictions documented +// in that extension interface, using limiterhelper.MiddlewaresToLimiterProvider. +func GetBaseLimiters(host component.Host, cfgs []Config) ([]extensionlimiter.BaseLimiterProvider, error) { + var err error + var lims []extensionlimiter.BaseLimiterProvider + all := host.GetExtensions() + for _, m := range cfgs { + ext, ok := all[m.ID] + if !ok { + err = multierr.Append(err, resolveFailed(m.ID)) + continue + } + if lim, ok := ext.(extensionlimiter.BaseLimiterProvider); ok { + lims = append(lims, lim) + } else { + // Note: In this case, we skip the middleware + // that is not a limiter. + } + } + return lims, err } diff --git a/config/configmiddleware/go.mod b/config/configmiddleware/go.mod index 3073e304a923..f2ac287197e5 100644 --- a/config/configmiddleware/go.mod +++ b/config/configmiddleware/go.mod @@ -6,7 +6,8 @@ require ( github.com/stretchr/testify v1.10.0 go.opentelemetry.io/collector/component v1.32.0 go.opentelemetry.io/collector/extension v1.32.0 - go.opentelemetry.io/collector/extension/extensionmiddleware v0.126.0 + go.opentelemetry.io/collector/extension/extensionlimiter v0.0.0-00010101000000-000000000000 + go.opentelemetry.io/collector/extension/extensionmiddleware v1.30.0 go.opentelemetry.io/collector/extension/extensionmiddleware/extensionmiddlewaretest v0.126.0 google.golang.org/grpc v1.72.0 ) @@ -18,11 +19,17 @@ require ( github.com/gogo/protobuf v1.3.2 // indirect github.com/google/uuid v1.6.0 // indirect github.com/hashicorp/go-version v1.7.0 // indirect + github.com/json-iterator/go v1.1.12 // indirect + github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect + github.com/modern-go/reflect2 v1.0.2 // indirect github.com/pmezard/go-difflib v1.0.0 // indirect go.opentelemetry.io/auto/sdk v1.1.0 // indirect + go.opentelemetry.io/collector/consumer v1.32.0 // indirect + go.opentelemetry.io/collector/consumer/xconsumer v0.0.0-00010101000000-000000000000 // indirect go.opentelemetry.io/collector/featuregate v1.32.0 // indirect go.opentelemetry.io/collector/internal/telemetry v0.126.0 // indirect go.opentelemetry.io/collector/pdata v1.32.0 // indirect + go.opentelemetry.io/collector/pdata/pprofile v0.126.0 // indirect go.opentelemetry.io/contrib/bridges/otelzap v0.10.0 // indirect go.opentelemetry.io/otel v1.35.0 // indirect go.opentelemetry.io/otel/log v0.11.0 // indirect @@ -34,6 +41,7 @@ require ( golang.org/x/net v0.39.0 // indirect golang.org/x/sys v0.32.0 // indirect golang.org/x/text v0.24.0 // indirect + golang.org/x/time v0.11.0 // indirect google.golang.org/genproto/googleapis/rpc v0.0.0-20250218202821-56aae31c358a // indirect google.golang.org/protobuf v1.36.6 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect @@ -45,6 +53,8 @@ replace go.opentelemetry.io/collector/internal/telemetry => ../../internal/telem replace go.opentelemetry.io/collector/extension/extensionmiddleware => ../../extension/extensionmiddleware +replace go.opentelemetry.io/collector/extension/extensionlimiter => ../../extension/extensionlimiter + replace go.opentelemetry.io/collector/extension/extensionmiddleware/extensionmiddlewaretest => ../../extension/extensionmiddleware/extensionmiddlewaretest replace go.opentelemetry.io/collector/pdata => ../../pdata @@ -54,3 +64,7 @@ replace go.opentelemetry.io/collector/pipeline => ../../pipeline replace go.opentelemetry.io/collector/featuregate => ../../featuregate replace go.opentelemetry.io/collector/extension => ../../extension + +replace go.opentelemetry.io/collector/consumer => ../../consumer + +replace go.opentelemetry.io/collector/consumer/xconsumer => ../../consumer/xconsumer diff --git a/config/configmiddleware/go.sum b/config/configmiddleware/go.sum index b86d93ca3004..8805d7f19ab3 100644 --- a/config/configmiddleware/go.sum +++ b/config/configmiddleware/go.sum @@ -1,3 +1,4 @@ +github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A= @@ -11,26 +12,38 @@ github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps= github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8= github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU= +github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/hashicorp/go-version v1.7.0 h1:5tqGy27NaOTB8yJKUZELlFAS/LTKJkrmONwQKeRZfjY= github.com/hashicorp/go-version v1.7.0/go.mod h1:fltr4n8CU8Ke44wwGCBoEymUuxUHl09ZGVZPK5anwXA= +github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM= +github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo= github.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI2bnpBCr8= github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= +github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= +github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg= +github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= +github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M= +github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/rogpeppe/go-internal v1.13.1 h1:KvO1DLK/DRN07sQ1LQKScxyZJuNnedQ5/wKSR38lUII= github.com/rogpeppe/go-internal v1.13.1/go.mod h1:uMEvuHeurkdAXX61udpOXGD/AzZDWNMNyH2VO9fmH0o= +github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA= github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= go.opentelemetry.io/auto/sdk v1.1.0 h1:cH53jehLUN6UFLY71z+NDOiNJqDdPRaXzTel0sJySYA= go.opentelemetry.io/auto/sdk v1.1.0/go.mod h1:3wSPjt5PWp2RhlCcmmOial7AvC4DQqZb7a7wCow3W8A= +go.opentelemetry.io/collector/pdata/pprofile v0.126.0 h1:ArYQxg5KdTb98r1X6KSZY7W6/4DPv/q6z7jSbSZ1mBc= +go.opentelemetry.io/collector/pdata/pprofile v0.126.0/go.mod h1:2fBTFDcXjVfseBQKnt/DTM0EYTmFoPKtRpjg8ql38Ek= go.opentelemetry.io/contrib/bridges/otelzap v0.10.0 h1:ojdSRDvjrnm30beHOmwsSvLpoRF40MlwNCA+Oo93kXU= go.opentelemetry.io/contrib/bridges/otelzap v0.10.0/go.mod h1:oTTm4g7NEtHSV2i/0FeVdPaPgUIZPfQkFbq0vbzqnv0= go.opentelemetry.io/otel v1.35.0 h1:xKWKPxrxB6OtMCbmMY021CqC45J+3Onta9MqjhnusiQ= @@ -74,6 +87,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.24.0 h1:dd5Bzh4yt5KYA8f9CJHCP4FB4D51c2c6JvN37xJJkJ0= golang.org/x/text v0.24.0/go.mod h1:L8rBsPeo2pSS+xqN0d5u2ikmjtmoJbDBT1b7nHvFCdU= +golang.org/x/time v0.11.0 h1:/bpjEDfN9tkoN/ryeYHnv5hcMlc8ncjMcM4XBk5NWV0= +golang.org/x/time v0.11.0/go.mod h1:CDIdPxbZBQxdj6cxyCIdrNogrJKMJ7pr37NYpMcMDSg= 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.0.0-20200619180055-7c47624df98f/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= diff --git a/extension/extensionlimiter/go.mod b/extension/extensionlimiter/go.mod index 7726b08b5261..198c6a3be185 100644 --- a/extension/extensionlimiter/go.mod +++ b/extension/extensionlimiter/go.mod @@ -3,11 +3,8 @@ module go.opentelemetry.io/collector/extension/extensionlimiter go 1.23.0 require ( - go.opentelemetry.io/collector/component v1.32.0 - go.opentelemetry.io/collector/config/configmiddleware v0.0.0-00010101000000-000000000000 go.opentelemetry.io/collector/consumer v1.32.0 go.opentelemetry.io/collector/consumer/xconsumer v0.0.0-00010101000000-000000000000 - go.opentelemetry.io/collector/extension v1.32.0 go.opentelemetry.io/collector/extension/extensionmiddleware v1.30.0 go.opentelemetry.io/collector/extension/extensionmiddleware/extensionmiddlewaretest v0.126.0 go.opentelemetry.io/collector/pdata v1.32.0 @@ -27,6 +24,8 @@ require ( github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect github.com/modern-go/reflect2 v1.0.2 // indirect go.opentelemetry.io/auto/sdk v1.1.0 // indirect + go.opentelemetry.io/collector/component v1.32.0 // indirect + go.opentelemetry.io/collector/extension v1.32.0 // indirect go.opentelemetry.io/collector/featuregate v1.32.0 // indirect go.opentelemetry.io/collector/internal/telemetry v0.126.0 // indirect go.opentelemetry.io/contrib/bridges/otelzap v0.10.0 // indirect @@ -55,8 +54,6 @@ replace go.opentelemetry.io/collector/pdata => ../../pdata replace go.opentelemetry.io/collector/pdata/pprofile => ../../pdata/pprofile -replace go.opentelemetry.io/collector/config/configmiddleware => ../../config/configmiddleware - replace go.opentelemetry.io/collector/extension/extensionmiddleware => ../../extension/extensionmiddleware replace go.opentelemetry.io/collector/pipeline => ../../pipeline diff --git a/extension/extensionlimiter/limiterhelper/grpc/grpclimiter.go b/extension/extensionlimiter/limiterhelper/grpc/grpclimiter.go index ced96727d86f..4be501305939 100644 --- a/extension/extensionlimiter/limiterhelper/grpc/grpclimiter.go +++ b/extension/extensionlimiter/limiterhelper/grpc/grpclimiter.go @@ -3,8 +3,6 @@ package grpclimiter import ( "context" - "go.opentelemetry.io/collector/component" - "go.opentelemetry.io/collector/config/configmiddleware" "go.opentelemetry.io/collector/extension/extensionlimiter" "go.opentelemetry.io/collector/extension/extensionlimiter/limiterhelper" "go.opentelemetry.io/collector/extension/extensionmiddleware" @@ -13,9 +11,9 @@ import ( "google.golang.org/grpc/stats" ) -func NewClientLimiter(host component.Host, middleware configmiddleware.Config) (extensionmiddleware.GRPCClient, error) { - wp, err1 := limiterhelper.MiddlewareToLimiterWrapperProvider(host, middleware) - rp, err2 := limiterhelper.MiddlewareToRateLimiterProvider(host, middleware) +func NewClientLimiter(ext extensionlimiter.BaseLimiterProvider) (extensionmiddleware.GRPCClient, error) { + wp, err1 := limiterhelper.MiddlewareToLimiterWrapperProvider(ext) + rp, err2 := limiterhelper.MiddlewareToRateLimiterProvider(ext) if err := multierr.Append(err1, err2); err != nil { return nil, err } @@ -71,9 +69,9 @@ func NewClientLimiter(host component.Host, middleware configmiddleware.Config) ( }), nil } -func NewServerLimiter(host component.Host, middleware configmiddleware.Config) (extensionmiddleware.GRPCServer, error) { - wp, err1 := limiterhelper.MiddlewareToLimiterWrapperProvider(host, middleware) - rp, err2 := limiterhelper.MiddlewareToRateLimiterProvider(host, middleware) +func NewServerLimiter(ext extensionlimiter.BaseLimiterProvider) (extensionmiddleware.GRPCServer, error) { + wp, err1 := limiterhelper.MiddlewareToLimiterWrapperProvider(ext) + rp, err2 := limiterhelper.MiddlewareToRateLimiterProvider(ext) if err := multierr.Append(err1, err2); err != nil { return nil, err } diff --git a/extension/extensionlimiter/limiterhelper/http/httplimiter.go b/extension/extensionlimiter/limiterhelper/http/httplimiter.go index 065244d58125..88462bb3937a 100644 --- a/extension/extensionlimiter/limiterhelper/http/httplimiter.go +++ b/extension/extensionlimiter/limiterhelper/http/httplimiter.go @@ -5,8 +5,6 @@ import ( "io" "net/http" - "go.opentelemetry.io/collector/component" - "go.opentelemetry.io/collector/config/configmiddleware" "go.opentelemetry.io/collector/extension/extensionlimiter" "go.opentelemetry.io/collector/extension/extensionlimiter/limiterhelper" "go.opentelemetry.io/collector/extension/extensionmiddleware" @@ -14,9 +12,9 @@ import ( "go.uber.org/multierr" ) -func NewClientLimiter(host component.Host, middleware configmiddleware.Config) (extensionmiddleware.HTTPClient, error) { - wp, err1 := limiterhelper.MiddlewareToLimiterWrapperProvider(host, middleware) - rp, err2 := limiterhelper.MiddlewareToRateLimiterProvider(host, middleware) +func NewClientLimiter(ext extensionlimiter.BaseLimiterProvider) (extensionmiddleware.HTTPClient, error) { + wp, err1 := limiterhelper.MiddlewareToLimiterWrapperProvider(ext) + rp, err2 := limiterhelper.MiddlewareToRateLimiterProvider(ext) if err := multierr.Append(err1, err2); err != nil { return nil, err } @@ -84,9 +82,9 @@ func (rb *rateLimitedBody) Close() error { return rb.body.Close() } -func NewServerLimiter(host component.Host, middleware configmiddleware.Config) (extensionmiddleware.HTTPServer, error) { - wp, err1 := limiterhelper.MiddlewareToLimiterWrapperProvider(host, middleware) - rp, err2 := limiterhelper.MiddlewareToRateLimiterProvider(host, middleware) +func NewServerLimiter(ext extensionlimiter.BaseLimiterProvider) (extensionmiddleware.HTTPServer, error) { + wp, err1 := limiterhelper.MiddlewareToLimiterWrapperProvider(ext) + rp, err2 := limiterhelper.MiddlewareToRateLimiterProvider(ext) if err := multierr.Append(err1, err2); err != nil { return nil, err } diff --git a/extension/extensionlimiter/limiterhelper/middleware.go b/extension/extensionlimiter/limiterhelper/middleware.go index 9897f96a2c30..b9c16e1733c2 100644 --- a/extension/extensionlimiter/limiterhelper/middleware.go +++ b/extension/extensionlimiter/limiterhelper/middleware.go @@ -5,59 +5,39 @@ package limiterhelper // import "go.opentelemetry.io/collector/extension/extensi import ( "errors" - "fmt" - "go.opentelemetry.io/collector/component" - "go.opentelemetry.io/collector/config/configmiddleware" "go.opentelemetry.io/collector/extension/extensionlimiter" "go.uber.org/multierr" ) var ( - ErrNotALimiter = errors.New("middleware is not a limiter") - ErrNotARateLimiter = errors.New("middleware cannot implement rate limiter") - ErrLimiterConflict = errors.New("limiter implements both rate and resource-limiters") - ErrUnresolvedLimiter = errors.New("could not resolve middleware limiter") + ErrNotALimiter = errors.New("middleware is not a limiter") + ErrNotARateLimiter = errors.New("middleware cannot implement rate limiter") + ErrLimiterConflict = errors.New("limiter implements both rate and resource-limiters") ) -// MiddlewareIsLimiter applies consistency checks and returns a valid +// middlewareCheck applies consistency checks and returns a valid // limiter extension of any known kind. -func MiddlewareIsLimiter(host component.Host, middleware configmiddleware.Config) (extensionlimiter.BaseLimiterProvider, bool, error) { - exts := host.GetExtensions() - ext := exts[middleware.ID] - if ext == nil { - return nil, false, fmt.Errorf("%w: %s", ErrUnresolvedLimiter, middleware.ID) - } +func middlewareCheck(ext extensionlimiter.BaseLimiterProvider) (extensionlimiter.BaseLimiterProvider, error) { _, isResource := ext.(extensionlimiter.ResourceLimiterProvider) _, isRate := ext.(extensionlimiter.RateLimiterProvider) - base, isBase := ext.(extensionlimiter.BaseLimiterProvider) - - switch { - case isResource && isRate: - return nil, false, fmt.Errorf("%w: %s", ErrLimiterConflict, middleware.ID) - case isResource, isRate: - return base, true, nil - case isBase: - return base, true, nil - default: - return nil, false, nil + + if isResource && isRate { + return nil, ErrLimiterConflict } + return ext, nil } -// MiddlewaresToLimiterProvider constructs a combined limiter -// from an ordered list of middlewares. This constructor ignores -// middleware configs that are not limiters. -func MiddlewaresToLimiterProvider(host component.Host, middleware []configmiddleware.Config) (MultiLimiterProvider, error) { +// MultipleProvider constructs a combined limiter from an ordered list +// of middlewares. This constructor ignores middleware configs that +// are not limiters. +func MultipleProvider(exts []extensionlimiter.BaseLimiterProvider) (MultiLimiterProvider, error) { var retErr error var providers MultiLimiterProvider - for _, mid := range middleware { - base, ok, err := MiddlewareIsLimiter(host, mid) + for _, ext := range exts { + base, err := middlewareCheck(ext) retErr = multierr.Append(retErr, err) - if !ok { - continue - } providers = append(providers, base) - retErr = multierr.Append(retErr, err) } if len(providers) == 0 { return nil, nil @@ -69,10 +49,9 @@ func MiddlewaresToLimiterProvider(host component.Host, middleware []configmiddle // from middleware. Returns a package-level error if the middleware // does not implement exactly one of the limiter interfaces (i.e., // rate or resource). -func MiddlewareToBaseLimiterProvider(host component.Host, middleware configmiddleware.Config) (extensionlimiter.BaseLimiterProvider, error) { +func MiddlewareToBaseLimiterProvider(ext extensionlimiter.BaseLimiterProvider) (extensionlimiter.BaseLimiterProvider, error) { return getMiddleware( - host, - middleware, + ext, identity[extensionlimiter.BaseLimiterProvider], baseProvider[extensionlimiter.RateLimiterProvider], baseProvider[extensionlimiter.ResourceLimiterProvider], @@ -83,10 +62,9 @@ func MiddlewareToBaseLimiterProvider(host component.Host, middleware configmiddl // provider from middleware. Returns a package-level error if the // middleware does not implement exactly one of the limiter // interfaces (i.e., rate or resource). -func MiddlewareToLimiterWrapperProvider(host component.Host, middleware configmiddleware.Config) (LimiterWrapperProvider, error) { +func MiddlewareToLimiterWrapperProvider(ext extensionlimiter.BaseLimiterProvider) (LimiterWrapperProvider, error) { return getMiddleware( - host, - middleware, + ext, nilError(BaseToLimiterWrapperProvider), nilError(RateToLimiterWrapperProvider), nilError(ResourceToLimiterWrapperProvider), @@ -99,10 +77,9 @@ func MiddlewareToLimiterWrapperProvider(host component.Host, middleware configmi // interface. Returns a package-level error if the middleware does not // implement exactly one of the limiter interfaces (i.e., rate or // resource). -func MiddlewareToRateLimiterProvider(host component.Host, middleware configmiddleware.Config) (extensionlimiter.RateLimiterProvider, error) { +func MiddlewareToRateLimiterProvider(ext extensionlimiter.BaseLimiterProvider) (extensionlimiter.RateLimiterProvider, error) { return getMiddleware( - host, - middleware, + ext, nilError(BaseToRateLimiterProvider), identity[extensionlimiter.RateLimiterProvider], resourceToRateLimiterError, @@ -115,10 +92,9 @@ func MiddlewareToRateLimiterProvider(host component.Host, middleware configmiddl // interface. Returns a package-level error if the middleware does not // implement exactly one of the limiter interfaces (i.e., rate or // resource). -func MiddlewareToResourceLimiterProvider(host component.Host, middleware configmiddleware.Config) (extensionlimiter.ResourceLimiterProvider, error) { +func MiddlewareToResourceLimiterProvider(ext extensionlimiter.BaseLimiterProvider) (extensionlimiter.ResourceLimiterProvider, error) { return getMiddleware( - host, - middleware, + ext, nilError(BaseToResourceLimiterProvider), nilError(RateToResourceLimiterProvider), identity[extensionlimiter.ResourceLimiterProvider], @@ -128,20 +104,16 @@ func MiddlewareToResourceLimiterProvider(host component.Host, middleware configm // getProvider invokes getProvider if any kind of limiter is detected // for the given host and middleware configuration. func getMiddleware[Out any]( - host component.Host, - middleware configmiddleware.Config, + ext extensionlimiter.BaseLimiterProvider, base func(extensionlimiter.BaseLimiterProvider) (Out, error), rate func(extensionlimiter.RateLimiterProvider) (Out, error), resource func(extensionlimiter.ResourceLimiterProvider) (Out, error), ) (Out, error) { var out Out - ext, ok, err := MiddlewareIsLimiter(host, middleware) + ext, err := middlewareCheck(ext) if err != nil { return out, err } - if !ok { - return out, fmt.Errorf("%w: %s", ErrNotALimiter, middleware.ID) - } return getProvider(ext, base, rate, resource) } diff --git a/extension/extensionlimiter/limiterhelper/multi.go b/extension/extensionlimiter/limiterhelper/multi.go index c56b62f2260e..f4ff2a5607ea 100644 --- a/extension/extensionlimiter/limiterhelper/multi.go +++ b/extension/extensionlimiter/limiterhelper/multi.go @@ -42,8 +42,7 @@ func (ps MultiLimiterProvider) GetBaseLimiter( // wrappers in a nested sequence. func (ps MultiLimiterProvider) GetLimiterWrapper( key extensionlimiter.WeightKey, - opts ...extensionlimiter.Option, -) (LimiterWrapper, error) { + opts ...extensionlimiter.Option) (LimiterWrapper, error) { var noop LimiterWrapper = LimiterWrapperFunc(nil) return getMultiLimiter(ps, nilError(BaseToLimiterWrapperProvider), diff --git a/receiver/otlpreceiver/go.mod b/receiver/otlpreceiver/go.mod index 214d2886b45d..c1e57f1d2121 100644 --- a/receiver/otlpreceiver/go.mod +++ b/receiver/otlpreceiver/go.mod @@ -13,6 +13,7 @@ require ( go.opentelemetry.io/collector/config/configauth v0.126.0 go.opentelemetry.io/collector/config/configgrpc v0.126.0 go.opentelemetry.io/collector/config/confighttp v0.126.0 + go.opentelemetry.io/collector/config/configmiddleware v0.126.0 go.opentelemetry.io/collector/config/confignet v1.32.0 go.opentelemetry.io/collector/config/configopaque v1.32.0 go.opentelemetry.io/collector/config/configtls v1.32.0 @@ -35,6 +36,7 @@ require ( go.opentelemetry.io/otel v1.35.0 go.opentelemetry.io/otel/sdk/metric v1.35.0 go.uber.org/goleak v1.3.0 + go.uber.org/multierr v1.11.0 go.uber.org/zap v1.27.0 google.golang.org/genproto/googleapis/rpc v0.0.0-20250218202821-56aae31c358a google.golang.org/grpc v1.72.0 @@ -69,10 +71,10 @@ require ( go.opentelemetry.io/auto/sdk v1.1.0 // indirect go.opentelemetry.io/collector/client v1.32.0 // indirect go.opentelemetry.io/collector/config/configcompression v1.32.0 // indirect - go.opentelemetry.io/collector/config/configmiddleware v0.126.0 // indirect go.opentelemetry.io/collector/extension v1.32.0 // indirect go.opentelemetry.io/collector/extension/extensionauth v1.32.0 // indirect go.opentelemetry.io/collector/extension/extensionmiddleware v1.30.0 // indirect + go.opentelemetry.io/collector/extension/extensionmiddleware/extensionmiddlewaretest v0.126.0 // indirect go.opentelemetry.io/collector/featuregate v1.32.0 // indirect go.opentelemetry.io/collector/pipeline v0.126.0 // indirect go.opentelemetry.io/contrib/bridges/otelzap v0.10.0 // indirect @@ -82,11 +84,11 @@ require ( go.opentelemetry.io/otel/metric v1.35.0 // indirect go.opentelemetry.io/otel/sdk v1.35.0 // indirect go.opentelemetry.io/otel/trace v1.35.0 // indirect - go.uber.org/multierr v1.11.0 // indirect golang.org/x/crypto v0.38.0 // indirect golang.org/x/net v0.40.0 // indirect golang.org/x/sys v0.33.0 // indirect golang.org/x/text v0.25.0 // indirect + golang.org/x/time v0.11.0 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect sigs.k8s.io/yaml v1.4.0 // indirect ) diff --git a/receiver/otlpreceiver/go.sum b/receiver/otlpreceiver/go.sum index d8601f043177..017b4ea9acfe 100644 --- a/receiver/otlpreceiver/go.sum +++ b/receiver/otlpreceiver/go.sum @@ -128,6 +128,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.25.0 h1:qVyWApTSYLk/drJRO5mDlNYskwQznZmkpV2c8q9zls4= golang.org/x/text v0.25.0/go.mod h1:WEdwpYrmk1qmdHvhkSTNPm3app7v4rsT8F2UD6+VHIA= +golang.org/x/time v0.11.0 h1:/bpjEDfN9tkoN/ryeYHnv5hcMlc8ncjMcM4XBk5NWV0= +golang.org/x/time v0.11.0/go.mod h1:CDIdPxbZBQxdj6cxyCIdrNogrJKMJ7pr37NYpMcMDSg= 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.0.0-20200619180055-7c47624df98f/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= diff --git a/receiver/otlpreceiver/otlp.go b/receiver/otlpreceiver/otlp.go index 288be4c3e7fc..6834b8fec30b 100644 --- a/receiver/otlpreceiver/otlp.go +++ b/receiver/otlpreceiver/otlp.go @@ -16,6 +16,7 @@ import ( "go.opentelemetry.io/collector/component" "go.opentelemetry.io/collector/component/componentstatus" "go.opentelemetry.io/collector/config/confighttp" + "go.opentelemetry.io/collector/config/configmiddleware" "go.opentelemetry.io/collector/consumer" "go.opentelemetry.io/collector/consumer/xconsumer" "go.opentelemetry.io/collector/extension/extensionlimiter" @@ -100,7 +101,11 @@ func (r *otlpReceiver) startGRPCServer(host component.Host) error { } limitKeys := extensionlimiter.StandardNotMiddlewareKeys() - limiterProvider, err := limiterhelper.MiddlewaresToLimiterWrapperProvider(host, r.cfg.GRPC.Middlewares) + limiters, err := configmiddleware.GetBaseLimiters(host, r.cfg.GRPC.Middlewares) + if err != nil { + return err + } + limiterProvider, err := limiterhelper.MultipleProvider(limiters) if err != nil { return err } @@ -165,7 +170,11 @@ func (r *otlpReceiver) startHTTPServer(ctx context.Context, host component.Host) } limitKeys := extensionlimiter.StandardNotMiddlewareKeys() - limiterProvider, err := limiterhelper.MiddlewaresToLimiterWrapperProvider(host, r.cfg.HTTP.ServerConfig.Middlewares) + limiters, err := configmiddleware.GetBaseLimiters(host, r.cfg.HTTP.ServerConfig.Middlewares) + if err != nil { + return err + } + limiterProvider, err := limiterhelper.MultipleProvider(limiters) if err != nil { return err } From ecdc9d50b48c93bf6869540034fc8bfbb965c6b6 Mon Sep 17 00:00:00 2001 From: Joshua MacDonald Date: Wed, 21 May 2025 19:13:28 -0700 Subject: [PATCH 27/47] memorylimiter extension --- extension/extensionlimiter/README.md | 11 ++++++---- .../limiterhelper/middleware.go | 6 ------ .../limiterhelper/resource.go | 6 ++++++ extension/memorylimiterextension/go.mod | 3 +++ .../memorylimiterextension/memorylimiter.go | 20 +++++++++++++++++++ 5 files changed, 36 insertions(+), 10 deletions(-) diff --git a/extension/extensionlimiter/README.md b/extension/extensionlimiter/README.md index fadab4117ff7..80ddb60ad7de 100644 --- a/extension/extensionlimiter/README.md +++ b/extension/extensionlimiter/README.md @@ -86,8 +86,10 @@ All limiter extensions: The `limiterhelper` package contains features for composing limiters as well as foundational rate and resource limiter implementations. The `limiterhelper/http` and `limiterhelper/grpc` packages provide -connectors allowing limiters to act as specific kinds of middleware. -The original garbage-collector state-based limiter can be found in +connectors allowing limiters to act as specific kinds of +middleware. Limiters are automatically initialized as middleware via +`configmiddleware`. The original garbage-collector state-based +limiter can be found in [`../memorylimiterextension`](../memorylimiterextension/README.md). ## Recommendations @@ -97,12 +99,13 @@ integrate with any kind of limiter is to use the a consumer wrapper function (e.g. `NewLimitedLogs`). These helper methods check for saturation and then apply multiple weight keys in sequence. -Multi-limiter adapters (@@@), ... - At a lower level, a simple way to integrate with any kind of limiter is to use the `LimiterWrapper` interface with its callback-based approach. +Multi-limiter adapters are available for all provider interfaces via +`MultipleProvider`. + For blocking access to rate and resource limiters without wrapper constraints, use `NewBlockingRateLimiter` or `NewBlockingResourceLimiter`. diff --git a/extension/extensionlimiter/limiterhelper/middleware.go b/extension/extensionlimiter/limiterhelper/middleware.go index b9c16e1733c2..2eb5fb263f8c 100644 --- a/extension/extensionlimiter/limiterhelper/middleware.go +++ b/extension/extensionlimiter/limiterhelper/middleware.go @@ -152,9 +152,3 @@ func baseProvider[T extensionlimiter.BaseLimiterProvider](p T) (extensionlimiter func nilError[S, T any](f func(S) T) func(S) (T, error) { return func(s S) (T, error) { return f(s), nil } } - -// resourceToRateLimiterError represents the impossible conversion -// from resource limiter to rate limiter. -func resourceToRateLimiterError(_ extensionlimiter.ResourceLimiterProvider) (extensionlimiter.RateLimiterProvider, error) { - return nil, ErrNotARateLimiter -} diff --git a/extension/extensionlimiter/limiterhelper/resource.go b/extension/extensionlimiter/limiterhelper/resource.go index eda2d6349cfd..d6838c164b7b 100644 --- a/extension/extensionlimiter/limiterhelper/resource.go +++ b/extension/extensionlimiter/limiterhelper/resource.go @@ -169,3 +169,9 @@ func (b BlockingResourceLimiter) WaitFor(ctx context.Context, value int) (extens return rsv.Release, nil } } + +// resourceToRateLimiterError represents the impossible conversion +// from resource limiter to rate limiter. +func resourceToRateLimiterError(_ extensionlimiter.ResourceLimiterProvider) (extensionlimiter.RateLimiterProvider, error) { + return nil, ErrNotARateLimiter +} diff --git a/extension/memorylimiterextension/go.mod b/extension/memorylimiterextension/go.mod index 1c30f7903141..e3db637cad03 100644 --- a/extension/memorylimiterextension/go.mod +++ b/extension/memorylimiterextension/go.mod @@ -8,6 +8,7 @@ require ( go.opentelemetry.io/collector/component/componenttest v0.126.0 go.opentelemetry.io/collector/confmap v1.32.0 go.opentelemetry.io/collector/extension v1.32.0 + go.opentelemetry.io/collector/extension/extensionlimiter v0.0.0-00010101000000-000000000000 go.opentelemetry.io/collector/extension/extensiontest v0.126.0 go.opentelemetry.io/collector/internal/memorylimiter v0.126.0 go.uber.org/goleak v1.3.0 @@ -73,6 +74,8 @@ replace go.opentelemetry.io/collector/internal/memorylimiter => ../../internal/m replace go.opentelemetry.io/collector/extension/extensiontest => ../../extension/extensiontest +replace go.opentelemetry.io/collector/extension/extensionlimiter => ../../extension/extensionlimiter + replace go.opentelemetry.io/collector/featuregate => ../../featuregate replace go.opentelemetry.io/collector/internal/telemetry => ../../internal/telemetry diff --git a/extension/memorylimiterextension/memorylimiter.go b/extension/memorylimiterextension/memorylimiter.go index 4986243ce10b..62fbc62f0265 100644 --- a/extension/memorylimiterextension/memorylimiter.go +++ b/extension/memorylimiterextension/memorylimiter.go @@ -5,17 +5,25 @@ package memorylimiterextension // import "go.opentelemetry.io/collector/extensio import ( "context" + "errors" "go.uber.org/zap" "go.opentelemetry.io/collector/component" + "go.opentelemetry.io/collector/extension/extensionlimiter" "go.opentelemetry.io/collector/internal/memorylimiter" ) +var ( + ErrMustRefuse = errors.New("system is near memory limit") +) + type memoryLimiterExtension struct { memLimiter *memorylimiter.MemoryLimiter } +var _ extensionlimiter.BaseLimiterProvider = &memoryLimiterExtension{} + // newMemoryLimiter returns a new memorylimiter extension. func newMemoryLimiter(cfg *Config, logger *zap.Logger) (*memoryLimiterExtension, error) { ml, err := memorylimiter.NewMemoryLimiter(cfg, logger) @@ -34,6 +42,18 @@ func (ml *memoryLimiterExtension) Shutdown(ctx context.Context) error { return ml.memLimiter.Shutdown(ctx) } +// GetBaseLimiter implements extensionlimiter.BaseLimiterProvider. +func (ml *memoryLimiterExtension) GetBaseLimiter( + opts ...extensionlimiter.Option, +) (extensionlimiter.BaseLimiter, error) { + return extensionlimiter.MustDenyFunc(func(_ context.Context) error { + if ml.MustRefuse() { + return ErrMustRefuse + } + return nil + }), nil +} + // MustRefuse returns if the caller should deny because memory has reached it's configured limits func (ml *memoryLimiterExtension) MustRefuse() bool { return ml.memLimiter.MustRefuse() From d529eb0d29dd3a5a29114086d20007feab854d86 Mon Sep 17 00:00:00 2001 From: Joshua MacDonald Date: Wed, 21 May 2025 19:43:04 -0700 Subject: [PATCH 28/47] more readme; fewer open questions --- extension/extensionlimiter/README.md | 164 ++++++++++++++++----------- 1 file changed, 97 insertions(+), 67 deletions(-) diff --git a/extension/extensionlimiter/README.md b/extension/extensionlimiter/README.md index 80ddb60ad7de..f552d41fbae9 100644 --- a/extension/extensionlimiter/README.md +++ b/extension/extensionlimiter/README.md @@ -136,27 +136,25 @@ ways. ### Limiter blocking and failing -Limiters implementations MAY block the request or fail immediately, -subject to internal logic. A limiter aims to avoid waste, which -requires balancing several factors. To fail a request that has already -been transmitted, received and parsed is sometimes more wasteful than -waiting for a little while; on the other hand waiting for a long time -risks wasting memory. In general, an overloaded limiter that is -saturated SHOULD fail requests immediately. +Limiters implementations never block. The `RateLimiter` and +`ResourceLimiter` interfaces return reservations instead informing the +caller how they can wait on their own, allowing them to cancel the +request if they return early. Limiter implementations SHOULD consider the context deadline when they block. If the deadline is likely to expire before the limit becomes available, they should return a standard overload signal. +Blocking adapters are provided for callers including the `LimiterWrapper`. ### Limiter saturation -Rate and resource limiter providers have a `GetChecker` method to -provide a `Checker`, featuring a `MustDeny` method which is made +Rate and resource limiter providers have a `GetBaseLimiter` method to +provide a `BaseLimiter`, featuring a `MustDeny` method which is made available for applications to test when any limit is fully saturated that would eventually deny the request. -The `Checker` is consulted at least once and applies to all weight -keys. Because a `Checker` can be consulted more than once by a +The `BaseLimiter` is consulted at least once and applies to all weight +keys. Because a `BaseLimiter` can be consulted more than once by a receiver and/or middleware, it is possible for requests to be denied over the saturation of limits they were already granted. Users should configure external load balancers and/or horizontal scaling policies @@ -174,6 +172,38 @@ before use, but either way be consistent. When using the low-level interfaces directly, limits SHOULD be applied before creating new concurrent work. +### Built-in limiters + +#### Base + +The `memorylimiterextension` is a `BasedLimiterProvider` that bases +its decisions on memory statistics from the garbage collector. This +logic was traditionally included in the `memorylimiterprocessor`, +however the limiter extension interface is preferred. + +#### Rate + +A built-in helper implementation of the RateLimiter interface is +provided, based on `golang.org/x/time/rate.Limter`. These underlying +rate limiters are parameterized by two numbers: + +- `limit` (float64): the maximum frequency of weight-units per second +- `burst` (uint64): the "burst" configured in a Token-bucket algorithm. + +The rate limiter is saturated when there is no burst available. + +#### Resource + +A built-in helper implementation of the ResourceLimiter interface is +provided, based on a bounded queue with LIFO semantics. These +underlying resource limiters are parameterized by two numbers: + +- `request` (uint64): the maximum of concurrent resource value admitted +- `waiting` (uint64): the maximum of concurrent resource value permitted to wait + +The resource limiter is saturated when the sum of current `request` +and `waiting` values exceed the sum of their maximum values. + ### Examples #### OTLP receiver @@ -238,33 +268,39 @@ Here, `getLimiters` is a function to get the effective `[]configmiddleware.Config` and derive pipeline consumers using `limiterhelper` adapters. -To acquire a limiter, use `MiddlewaresToLimiterWrapperProvider` to +To acquire a limiter, use `MiddlewaresToLimiterProvider` to obtain a combined limiter wrapper around the input `nextMetrics` consumer. It will pass `StandardNotMiddlewareKeys()` indicating to apply request items and memory size: ```golang - // Extract limiter provider from middlewares. - s.limiterProvider, err = limiterhelper.MiddlewaresToLimiterWrapperProvider( - cfg.Middlewares) + // Extract limiter extensions from host and list of middleware. + providers, err := configmiddleware.GetBaseLimiters( + host, cfg.Middlewares) if err != nil { ... } - // Extract a checker from the provider - s.checker, err = s.limiterProvider.GetChecker() + // Extract a multi-limiter from the provider + s.limiterProvider, err = limiterhelper.MultipleProvider(providers) if err != nil { ... } // Here get a limiter-wrapped pipeline and a combination of weight-specific // limiters for MustDeny() functionality. + limitKeys := extensionlimiter.StandardNotMiddlewareKeys() s.nextMetrics, err = limiterhelper.NewLimitedMetrics( - s.nextMetrics, limiterhelper.StandardNotMiddlewareKeys(), s.limiterProvider) + s.nextMetrics, limitKeys, s.limiterProvider) if err != nil { ... } + + // Compute the base limiter from the middlewares for use before scrapes. + s.limiter, err := s.limiterProvider.GetBaseLimiter(host, middlewares) + if err != nil { ... } ``` In the scraper loop, use `MustDeny` before starting a scrape: ```golang func (s *scraper) scrapeOnce(ctx context.Context) error { - if err := s.checker.MustDeny(ctx); err != nil { + // Check if any limits are saturated. + if err := s.limiter.MustDeny(ctx); err != nil { return err } @@ -296,18 +332,19 @@ receivers: - ratelimiter/streamer ``` -The receiver will check `s.checker.MustDeny()` as above. In a stream, -limiters are expected to block the stream until limit requests -succeed, however after the limit requests succeed, the receiver may -wish to return from `Send()` to continue accepting new requests while -the consumer works in a separate goroutine. The limit will be released -after the consumer returns. +The receiver will check `s.limiter.MustDeny()` as above. In a stream, +a blocking limiter is used which blocks the stream (via +`s.memorySizeLimiter.WaitFor()`) until limit requests succeed, however +after the limit requests succeed, the receiver returns from `Send()` +to continue accepting new requests while the consumer works in a +separate goroutine. The limit will be released after the consumer +returns in this example: ```golang func (s *scraper) LogsStream(ctx context.Context, stream *Stream) error { for { // Check saturation for all limiters, all keys. - err := s.checker.MustDeny(ctx) + err := s.limiter.MustDeny(ctx) if err != nil { ... } // The network bytes and request count limits are applied in middleware. @@ -318,9 +355,11 @@ func (s *scraper) LogsStream(ctx context.Context, stream *Stream) error { data, err := s.getLogs(ctx, req) if err != nil { ... } - release, err := s.memorySizeLimiter.Acquire(ctx, pdataSize(data)) + // Non-blocking limiter call. + release, err := s.memorySizeLimiter.WaitFor(ctx, pdataSize(data)) if err != nil { ... } + // Asynchronous work starts here. go func() { // Request items limit is applied in the pipeline consumer err := s.nextMetrics.ConsumeMetrics(ctx, data) @@ -335,36 +374,6 @@ func (s *scraper) LogsStream(ctx context.Context, stream *Stream) error { } ``` -#### Open questions - -##### Middleware implementation details - -Details are -important. [#12700](https://github.com/open-telemetry/opentelemetry-collector/pull/12700) -contained a `limitermiddleware` implementation which was a middleware -that called a limiter for HTTP and gRPC. Roughly the same code will be -used, and the details will come out. - -##### Provider options - -An `Option` type has been added as a placeholder in the provider -interfaces. **NOTE: No options are implemented.** Potential options: - -- The protocol name -- The signal kind -- The caller's component ID - -Because the set of each of these is small, it is possible to -pre-compute limiter instances for the cross product of configurations. - -##### Context-dependent limits - -Client metadata (i.e., headers) may be used in the context to make -limiter decisions. These details are automatically extracted from the -Context passed to `MustDeny`, `Limit`, `Acquire`, and `LimitCall` -functions. No examples are provided. How will limiters configure, for -example, tenant-specific limits? - ##### Data-dependent limits When a single unit of data contains limits that are assignable to @@ -373,21 +382,27 @@ requests and add to their context and run them concurrently through context-dependent limiters. See [#39199](https://github.com/open-telemetry/opentelemetry-collector-contrib/issues/39199). -Another option is to add support for non-blocking limit requests. For -example, to apply limits using information derived from the -OpenTelemetry resource, we might do something like this pseudo-code: +Another option, shown below, is to use the non-blocking rate limiter +interface and drop data that would exceed a limit. For example, to +limit based on metadata extracted from the OpenTelemetry resource +value: ``` func (p *processor) limitLogs(ctx context.Context, logsData plog.Logs) (plog.Logs, extensionlimiter.ReleaseFunc, error) { var rels extensionlimiter.ReleaseFuncs logsData.ResourceLogs().RemoveIf(func(rl plog.ResourceLogs) bool { md := resourceToMetadata(rl.Resource()) - rel, err := p.nonBlockingLimiter.TryLimitOrAcquire(withMetadata(ctx, md)) + reservation, err := p.limiter.ReserveRate(withMetadata(ctx, md)) if err != nil { return false } - rels = append(rels, rel) - return true + if reservation.WaitTime() > 0 { + reservation.Cancel() + return false + } + default: + return true + } }) if logsData.ResourceLogs().Len() == 0 { return logsData, func() {}, processorhelper.ErrSkipProcessingData @@ -405,7 +420,22 @@ func (p *processor) ConsumeLogs(ctx context.Context, logsData plog.Logs) error { } ``` -Here, the release a new `TryLimitOrAcquire` function abstracts the -form of a non-blocking call to either form of limiter. If the -underyling limiter is a rate limiter, the release function will be a -no-op. +Here, the limiter's `ReserveRate` function does not block the caller, +allowing the processor to drop data instead. Note the call to +`RateReservation.Cancel` undoes the effect of the untaken reservation. +The same approach works for `ResourceLimiter` as well using using +`ResourceReservation`, its `Delay` channel `Release` function. + +#### Open questions + +##### Provider options + +An `Option` type has been added as a placeholder in the provider +interfaces. **NOTE: No options are implemented.** Potential options: + +- The protocol name +- The signal kind +- The caller's component ID + +Because the set of each of these is small, it is possible to +pre-compute limiter instances for the cross product of configurations. From 23b4f2c6ea9261baba666dabc7ff8f6dc2dac312 Mon Sep 17 00:00:00 2001 From: Joshua MacDonald Date: Thu, 22 May 2025 10:53:42 -0700 Subject: [PATCH 29/47] bugs fixed --- .../limiterhelper/consumer.go | 3 ++ .../limiterhelper/middleware.go | 5 +-- .../extensionlimiter/limiterhelper/multi.go | 38 +++++++++---------- .../limiterhelper/resource.go | 17 ++++++++- 4 files changed, 36 insertions(+), 27 deletions(-) diff --git a/extension/extensionlimiter/limiterhelper/consumer.go b/extension/extensionlimiter/limiterhelper/consumer.go index fc57364d67d7..12cdd021ff9b 100644 --- a/extension/extensionlimiter/limiterhelper/consumer.go +++ b/extension/extensionlimiter/limiterhelper/consumer.go @@ -155,6 +155,9 @@ func applyBaseLimiter[P any, C any]( if err != nil { return next, err } + if ck == nil { + return next, nil + } return m.create(func(ctx context.Context, data P) error { if err := ck.MustDeny(ctx); err != nil { return err diff --git a/extension/extensionlimiter/limiterhelper/middleware.go b/extension/extensionlimiter/limiterhelper/middleware.go index 2eb5fb263f8c..eae69d5702b5 100644 --- a/extension/extensionlimiter/limiterhelper/middleware.go +++ b/extension/extensionlimiter/limiterhelper/middleware.go @@ -39,10 +39,7 @@ func MultipleProvider(exts []extensionlimiter.BaseLimiterProvider) (MultiLimiter retErr = multierr.Append(retErr, err) providers = append(providers, base) } - if len(providers) == 0 { - return nil, nil - } - return providers, nil + return providers, retErr } // MiddlewareToBaseLimiterProvider returns a base limiter provider diff --git a/extension/extensionlimiter/limiterhelper/multi.go b/extension/extensionlimiter/limiterhelper/multi.go index f4ff2a5607ea..36713f34f4a3 100644 --- a/extension/extensionlimiter/limiterhelper/multi.go +++ b/extension/extensionlimiter/limiterhelper/multi.go @@ -26,12 +26,10 @@ var _ extensionlimiter.BaseLimiterProvider = MultiLimiterProvider{} func (ps MultiLimiterProvider) GetBaseLimiter( opts ...extensionlimiter.Option, ) (extensionlimiter.BaseLimiter, error) { - var noop extensionlimiter.BaseLimiter = extensionlimiter.MustDenyFunc(nil) return getMultiLimiter(ps, identity[extensionlimiter.BaseLimiterProvider], baseProvider[extensionlimiter.RateLimiterProvider], baseProvider[extensionlimiter.ResourceLimiterProvider], - noop, func(p extensionlimiter.BaseLimiterProvider) (extensionlimiter.BaseLimiter, error) { return p.GetBaseLimiter(opts...) }, @@ -43,12 +41,10 @@ func (ps MultiLimiterProvider) GetBaseLimiter( func (ps MultiLimiterProvider) GetLimiterWrapper( key extensionlimiter.WeightKey, opts ...extensionlimiter.Option) (LimiterWrapper, error) { - var noop LimiterWrapper = LimiterWrapperFunc(nil) return getMultiLimiter(ps, nilError(BaseToLimiterWrapperProvider), nilError(RateToLimiterWrapperProvider), nilError(ResourceToLimiterWrapperProvider), - noop, func(p LimiterWrapperProvider) (LimiterWrapper, error) { return p.GetLimiterWrapper(key, opts...) }, @@ -61,12 +57,10 @@ func (ps MultiLimiterProvider) GetResourceLimiter( key extensionlimiter.WeightKey, opts ...extensionlimiter.Option, ) (extensionlimiter.ResourceLimiter, error) { - var noop extensionlimiter.ResourceLimiter = extensionlimiter.ReserveResourceFunc(nil) return getMultiLimiter(ps, nilError(BaseToResourceLimiterProvider), nilError(RateToResourceLimiterProvider), identity[extensionlimiter.ResourceLimiterProvider], - noop, func(p extensionlimiter.ResourceLimiterProvider) (extensionlimiter.ResourceLimiter, error) { return p.GetResourceLimiter(key, opts...) }, @@ -79,12 +73,10 @@ func (ps MultiLimiterProvider) GetRateLimiter( key extensionlimiter.WeightKey, opts ...extensionlimiter.Option, ) (extensionlimiter.RateLimiter, error) { - var noop extensionlimiter.RateLimiter = extensionlimiter.ReserveRateFunc(nil) return getMultiLimiter(ps, nilError(BaseToRateLimiterProvider), identity[extensionlimiter.RateLimiterProvider], resourceToRateLimiterError, - noop, func(p extensionlimiter.RateLimiterProvider) (extensionlimiter.RateLimiter, error) { return p.GetRateLimiter(key, opts...) }, @@ -130,7 +122,9 @@ func combineResourceLimiters(lims []extensionlimiter.ResourceLimiter) extensionl for _, lim := range lims { rsv, err := lim.ReserveResource(ctx, value) err = multierr.Append(err, err) - rsvs = append(rsvs, rsv) + if rsv != nil { + rsvs = append(rsvs, rsv) + } } release := func() { for _, rsv := range rsvs { @@ -184,14 +178,14 @@ func combineRateLimiters(lims []extensionlimiter.RateLimiter) extensionlimiter.R rsv.Cancel() } } - var wt time.Duration - for _, rsv := range rsvs { - wt = max(wt, rsv.WaitTime()) - } if err != nil { cancel() return nil, err } + var wt time.Duration + for _, rsv := range rsvs { + wt = max(wt, rsv.WaitTime()) + } return struct { extensionlimiter.WaitTimeFunc extensionlimiter.CancelFunc @@ -203,27 +197,29 @@ func combineRateLimiters(lims []extensionlimiter.RateLimiter) extensionlimiter.R return extensionlimiter.ReserveRateFunc(reserve) } -// getMultiLimiter combines multiple providers (all kinds), gets -// limiters from each, and returns the combined result or error. -func getMultiLimiter[Out, Lim comparable]( +// getMultiLimiter configures a limiter for multiple limiter +// extensions. +func getMultiLimiter[Out any, Lim comparable]( multi MultiLimiterProvider, base func(extensionlimiter.BaseLimiterProvider) (Out, error), rate func(extensionlimiter.RateLimiterProvider) (Out, error), resource func(extensionlimiter.ResourceLimiterProvider) (Out, error), - noop Lim, pfunc func(Out) (Lim, error), combine func([]Lim) Lim, -) (Lim, error) { +) (nilResult Lim, _ error) { + // Note that nilResult is used in error and non-error cases to + // return a nil and not a nil with concrete type (e.g., + // extensionlimiter.BaseLimiterProvider(nil)). var lims []Lim for _, baseProvider := range multi { provider, err := getProvider(baseProvider, base, rate, resource) if err == nil { - return noop, err + return nilResult, err } lim, err := pfunc(provider) if err == nil { - return noop, err + return nilResult, err } var zero Lim if lim == zero { @@ -233,7 +229,7 @@ func getMultiLimiter[Out, Lim comparable]( } if len(lims) == 0 { - return noop, nil + return nilResult, nil } if len(lims) == 1 { return lims[0], nil diff --git a/extension/extensionlimiter/limiterhelper/resource.go b/extension/extensionlimiter/limiterhelper/resource.go index d6838c164b7b..a5f7b1f86994 100644 --- a/extension/extensionlimiter/limiterhelper/resource.go +++ b/extension/extensionlimiter/limiterhelper/resource.go @@ -61,7 +61,20 @@ func (bq *boundedQueue) ReserveResource(ctx context.Context, value int) (extensi if bq.currentAdmitted+uint64(value) <= bq.limitAdmit { // the fast success path. bq.currentAdmitted += uint64(value) - return nil, nil + return struct { + extensionlimiter.DelayFunc + extensionlimiter.ReleaseFunc + }{ + nil, // No delay + func() { + // There was never a waiter in this + // case, just release and admit waiters. + bq.lock.Lock() + defer bq.lock.Unlock() + + bq.releaseLocked(value) + }, + }, nil } // since we were unable to admit, check if we can wait. @@ -83,7 +96,7 @@ func (bq *boundedQueue) ReserveResource(ctx context.Context, value int) (extensi return waiter.notify.channel() }, func() { - // This call returns the resource. + // Called when the caller finishes. bq.lock.Lock() defer bq.lock.Unlock() From ba136deb9590d6fc5999c1af548f59965cbd0599 Mon Sep 17 00:00:00 2001 From: Joshua MacDonald Date: Fri, 23 May 2025 08:57:26 -0700 Subject: [PATCH 30/47] readme --- extension/extensionlimiter/README.md | 36 +++++++++++++--------------- 1 file changed, 16 insertions(+), 20 deletions(-) diff --git a/extension/extensionlimiter/README.md b/extension/extensionlimiter/README.md index f552d41fbae9..dad88f283c90 100644 --- a/extension/extensionlimiter/README.md +++ b/extension/extensionlimiter/README.md @@ -55,22 +55,18 @@ for example, a limiter can slow the arrival of new data by stalling a response, it means synchronously waiting for the limit and asynchronously processing the request. -Resource limiters require the most of callers, as the interface -requires a `Release` function to be called after the caller is -finished with the resource. Adapters are provided for convenience. - A limiter is defined as **saturated** when a limit is completely -overloaded in at least one weight, generally it means callers should +overloaded for at least one weight, generally it means callers should deny new requests. All limiter extensions implement the basic limiter interface, and callers are expected to check for saturation by -invoking `MustDeny` once before applying individual weight limits. +invoking `MustDeny` before making individual requests with the limiter. Whereas the basic limiter's `MustDeny` method indicates only saturation, the rate and resource limiter interfaces both return a `Reservation`. While the details are slightly different, the reservation generally has two features: -- a mechanism to wait for the limit (if possible) +- a mechanism to wait for the limit - a mechanism to cancel or release the request. Each kind of limiter have corresponding **provider** interfaces that @@ -136,15 +132,15 @@ ways. ### Limiter blocking and failing -Limiters implementations never block. The `RateLimiter` and -`ResourceLimiter` interfaces return reservations instead informing the -caller how they can wait on their own, allowing them to cancel the -request if they return early. +Limiters implementations are not expected to block. The `RateLimiter` +and `ResourceLimiter` interfaces return reservations instead, +informing the caller how they can wait on their own and allowing them +to cancel the request if they return early. -Limiter implementations SHOULD consider the context deadline when -they block. If the deadline is likely to expire before the limit -becomes available, they should return a standard overload signal. -Blocking adapters are provided for callers including the `LimiterWrapper`. +Limiter implementations SHOULD consider the context deadline when they +block. If the deadline is likely to expire before the limit becomes +available, they should return an error instead. Blocking adapters are +provided for callers including the `LimiterWrapper`. ### Limiter saturation @@ -176,10 +172,10 @@ before creating new concurrent work. #### Base -The `memorylimiterextension` is a `BasedLimiterProvider` that bases -its decisions on memory statistics from the garbage collector. This +The `memorylimiterextension` is a `BaseLimiterProvider` that makes its +decisions using memory statistics from the garbage collector. This logic was traditionally included in the `memorylimiterprocessor`, -however the limiter extension interface is preferred. +however receiver integration with limiter extensions is preferred. #### Rate @@ -188,14 +184,14 @@ provided, based on `golang.org/x/time/rate.Limter`. These underlying rate limiters are parameterized by two numbers: - `limit` (float64): the maximum frequency of weight-units per second -- `burst` (uint64): the "burst" configured in a Token-bucket algorithm. +- `burst` (uint64): the "burst" value of the Token-bucket algorithm. The rate limiter is saturated when there is no burst available. #### Resource A built-in helper implementation of the ResourceLimiter interface is -provided, based on a bounded queue with LIFO semantics. These +provided, based on a bounded queue with LIFO behavior. These underlying resource limiters are parameterized by two numbers: - `request` (uint64): the maximum of concurrent resource value admitted From a65bed470bce3c1363941305878b753d092c50f5 Mon Sep 17 00:00:00 2001 From: Joshua MacDonald Date: Fri, 13 Jun 2025 16:33:56 -0700 Subject: [PATCH 31/47] linked --- config/confighttp/go.mod | 2 ++ config/confighttp/xconfighttp/go.mod | 2 ++ config/configmiddleware/go.mod | 4 ++++ extension/memorylimiterextension/go.mod | 8 ++++++++ extension/zpagesextension/go.mod | 2 ++ service/hostcapabilities/go.mod | 2 ++ 6 files changed, 20 insertions(+) diff --git a/config/confighttp/go.mod b/config/confighttp/go.mod index 208c93fe5a0b..cf1b4704f5b2 100644 --- a/config/confighttp/go.mod +++ b/config/confighttp/go.mod @@ -107,3 +107,5 @@ replace go.opentelemetry.io/collector/featuregate => ../../featuregate replace go.opentelemetry.io/collector/extension/extensionmiddleware/extensionmiddlewaretest => ../../extension/extensionmiddleware/extensionmiddlewaretest replace go.opentelemetry.io/collector/consumer/xconsumer => ../../consumer/xconsumer + +replace go.opentelemetry.io/collector/pdata/pprofile => ../../pdata/pprofile diff --git a/config/confighttp/xconfighttp/go.mod b/config/confighttp/xconfighttp/go.mod index 5009176a9adf..7f7ed8c0401f 100644 --- a/config/confighttp/xconfighttp/go.mod +++ b/config/confighttp/xconfighttp/go.mod @@ -108,3 +108,5 @@ replace go.opentelemetry.io/collector/extension/extensionmiddleware/extensionmid replace go.opentelemetry.io/collector/consumer/xconsumer => ../../../consumer/xconsumer replace go.opentelemetry.io/collector/extension/extensionlimiter => ../../../extension/extensionlimiter + +replace go.opentelemetry.io/collector/pdata/pprofile => ../../../pdata/pprofile diff --git a/config/configmiddleware/go.mod b/config/configmiddleware/go.mod index 45549f5c3f63..4a420a518656 100644 --- a/config/configmiddleware/go.mod +++ b/config/configmiddleware/go.mod @@ -66,3 +66,7 @@ replace go.opentelemetry.io/collector/extension => ../../extension replace go.opentelemetry.io/collector/extension/extensionlimiter => ../../extension/extensionlimiter replace go.opentelemetry.io/collector/consumer/xconsumer => ../../consumer/xconsumer + +replace go.opentelemetry.io/collector/pdata/pprofile => ../../pdata/pprofile + +replace go.opentelemetry.io/collector/consumer => ../../consumer diff --git a/extension/memorylimiterextension/go.mod b/extension/memorylimiterextension/go.mod index 62fc766b03f3..6fb07f741a22 100644 --- a/extension/memorylimiterextension/go.mod +++ b/extension/memorylimiterextension/go.mod @@ -83,3 +83,11 @@ replace go.opentelemetry.io/collector/pipeline => ../../pipeline replace go.opentelemetry.io/collector/consumer/xconsumer => ../../consumer/xconsumer replace go.opentelemetry.io/collector/extension/extensionlimiter => ../../extension/extensionlimiter + +replace go.opentelemetry.io/collector/extension/extensionmiddleware => ../extensionmiddleware + +replace go.opentelemetry.io/collector/pdata/pprofile => ../../pdata/pprofile + +replace go.opentelemetry.io/collector/extension/extensionmiddleware/extensionmiddlewaretest => ../extensionmiddleware/extensionmiddlewaretest + +replace go.opentelemetry.io/collector/consumer => ../../consumer diff --git a/extension/zpagesextension/go.mod b/extension/zpagesextension/go.mod index 36f9b2eedcc2..4f8f7a960d2e 100644 --- a/extension/zpagesextension/go.mod +++ b/extension/zpagesextension/go.mod @@ -136,3 +136,5 @@ replace go.opentelemetry.io/collector/extension/extensionmiddleware/extensionmid replace go.opentelemetry.io/collector/consumer/xconsumer => ../../consumer/xconsumer replace go.opentelemetry.io/collector/extension/extensionlimiter => ../../extension/extensionlimiter + +replace go.opentelemetry.io/collector/pdata/pprofile => ../../pdata/pprofile diff --git a/service/hostcapabilities/go.mod b/service/hostcapabilities/go.mod index d2ecf97a4668..a3c8fe49a41a 100644 --- a/service/hostcapabilities/go.mod +++ b/service/hostcapabilities/go.mod @@ -95,3 +95,5 @@ replace go.opentelemetry.io/collector/extension/extensionmiddleware/extensionmid replace go.opentelemetry.io/collector/config/configmiddleware => ../../config/configmiddleware replace go.opentelemetry.io/collector/extension/extensionmiddleware => ../../extension/extensionmiddleware + +replace go.opentelemetry.io/collector/extension/extensionlimiter => ../../extension/extensionlimiter From e64c4947f5a6f8f93ec552b340dc84bb76b346df Mon Sep 17 00:00:00 2001 From: Joshua MacDonald Date: Fri, 13 Jun 2025 17:00:35 -0700 Subject: [PATCH 32/47] readme --- config/confighttp/go.sum | 2 - config/confighttp/xconfighttp/go.sum | 2 - config/configmiddleware/go.sum | 4 - extension/extensionlimiter/README.md | 113 ++------------------------- 4 files changed, 8 insertions(+), 113 deletions(-) diff --git a/config/confighttp/go.sum b/config/confighttp/go.sum index 590a7698c4f8..244f79bdd3c7 100644 --- a/config/confighttp/go.sum +++ b/config/confighttp/go.sum @@ -62,8 +62,6 @@ github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9de github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= go.opentelemetry.io/auto/sdk v1.1.0 h1:cH53jehLUN6UFLY71z+NDOiNJqDdPRaXzTel0sJySYA= go.opentelemetry.io/auto/sdk v1.1.0/go.mod h1:3wSPjt5PWp2RhlCcmmOial7AvC4DQqZb7a7wCow3W8A= -go.opentelemetry.io/collector/pdata/pprofile v0.128.0 h1:6DEtzs/liqv/ukz2EHbC5OMaj2V6K2pzuj/LaRg2YmY= -go.opentelemetry.io/collector/pdata/pprofile v0.128.0/go.mod h1:bVVRpz+zKFf1UCCRUFqy8LvnO3tHlXKkdqW2d+Wi/iA= go.opentelemetry.io/contrib/bridges/otelzap v0.11.0 h1:u2E32P7j1a/gRgZDWhIXC+Shd4rLg70mnE7QLI/Ssnw= go.opentelemetry.io/contrib/bridges/otelzap v0.11.0/go.mod h1:pJPCLM8gzX4ASqLlyAXjHBEYxgbOQJ/9bidWxD6PEPQ= go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.61.0 h1:F7Jx+6hwnZ41NSFTO5q4LYDtJRXBf2PD0rNBkeB/lus= diff --git a/config/confighttp/xconfighttp/go.sum b/config/confighttp/xconfighttp/go.sum index 590a7698c4f8..244f79bdd3c7 100644 --- a/config/confighttp/xconfighttp/go.sum +++ b/config/confighttp/xconfighttp/go.sum @@ -62,8 +62,6 @@ github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9de github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= go.opentelemetry.io/auto/sdk v1.1.0 h1:cH53jehLUN6UFLY71z+NDOiNJqDdPRaXzTel0sJySYA= go.opentelemetry.io/auto/sdk v1.1.0/go.mod h1:3wSPjt5PWp2RhlCcmmOial7AvC4DQqZb7a7wCow3W8A= -go.opentelemetry.io/collector/pdata/pprofile v0.128.0 h1:6DEtzs/liqv/ukz2EHbC5OMaj2V6K2pzuj/LaRg2YmY= -go.opentelemetry.io/collector/pdata/pprofile v0.128.0/go.mod h1:bVVRpz+zKFf1UCCRUFqy8LvnO3tHlXKkdqW2d+Wi/iA= go.opentelemetry.io/contrib/bridges/otelzap v0.11.0 h1:u2E32P7j1a/gRgZDWhIXC+Shd4rLg70mnE7QLI/Ssnw= go.opentelemetry.io/contrib/bridges/otelzap v0.11.0/go.mod h1:pJPCLM8gzX4ASqLlyAXjHBEYxgbOQJ/9bidWxD6PEPQ= go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.61.0 h1:F7Jx+6hwnZ41NSFTO5q4LYDtJRXBf2PD0rNBkeB/lus= diff --git a/config/configmiddleware/go.sum b/config/configmiddleware/go.sum index 31a68540d292..58543183a819 100644 --- a/config/configmiddleware/go.sum +++ b/config/configmiddleware/go.sum @@ -42,10 +42,6 @@ github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9de github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= go.opentelemetry.io/auto/sdk v1.1.0 h1:cH53jehLUN6UFLY71z+NDOiNJqDdPRaXzTel0sJySYA= go.opentelemetry.io/auto/sdk v1.1.0/go.mod h1:3wSPjt5PWp2RhlCcmmOial7AvC4DQqZb7a7wCow3W8A= -go.opentelemetry.io/collector/consumer v1.34.0 h1:oBhHH6mgViOGhVDPozE+sUdt7jFBo2Hh32lsSr2L3Tc= -go.opentelemetry.io/collector/consumer v1.34.0/go.mod h1:DVMCb56ZBlPNcmo0lSJKn3rp18oyZQCedRE4GKIMI+Q= -go.opentelemetry.io/collector/pdata/pprofile v0.128.0 h1:6DEtzs/liqv/ukz2EHbC5OMaj2V6K2pzuj/LaRg2YmY= -go.opentelemetry.io/collector/pdata/pprofile v0.128.0/go.mod h1:bVVRpz+zKFf1UCCRUFqy8LvnO3tHlXKkdqW2d+Wi/iA= go.opentelemetry.io/contrib/bridges/otelzap v0.11.0 h1:u2E32P7j1a/gRgZDWhIXC+Shd4rLg70mnE7QLI/Ssnw= go.opentelemetry.io/contrib/bridges/otelzap v0.11.0/go.mod h1:pJPCLM8gzX4ASqLlyAXjHBEYxgbOQJ/9bidWxD6PEPQ= go.opentelemetry.io/otel v1.36.0 h1:UumtzIklRBY6cI/lllNZlALOF5nNIzJVb16APdvgTXg= diff --git a/extension/extensionlimiter/README.md b/extension/extensionlimiter/README.md index a6537e7cac82..d057cf211a91 100644 --- a/extension/extensionlimiter/README.md +++ b/extension/extensionlimiter/README.md @@ -441,127 +441,30 @@ pre-compute limiter instances for the cross product of configurations. The following diagram illustrates the core architecture of the extension limiter system, showing the relationships between interfaces, providers, helpers, and middleware integration: ```mermaid -graph TB +graph LR + subgraph TB %% Core Limiter Interfaces BaseLimiter["🔒 BaseLimiter
MustDeny(ctx) error"] RateLimiter["⏱️ RateLimiter
ReserveRate(ctx, int) (RateReservation, error)"] ResourceLimiter["💾 ResourceLimiter
ReserveResource(ctx, int) (ResourceReservation, error)"] - + end + + subgraph TB %% Provider Interfaces BaseLimiterProvider["🏭 BaseLimiterProvider
GetBaseLimiter(...Option) (BaseLimiter, error)"] RateLimiterProvider["🏭 RateLimiterProvider
GetRateLimiter(WeightKey, ...Option) (RateLimiter, error)"] ResourceLimiterProvider["🏭 ResourceLimiterProvider
GetResourceLimiter(WeightKey, ...Option) (ResourceLimiter, error)"] LimiterWrapperProvider["🏭 LimiterWrapperProvider
GetLimiterWrapper(WeightKey, ...Option) (LimiterWrapper, error)"] - - %% Helper Types - LimiterWrapper["🎯 LimiterWrapper
LimitCall(ctx, weight, callback) error"] - BlockingRateLimiter["⏸️ BlockingRateLimiter
WaitFor(ctx, value) error"] - BlockingResourceLimiter["⏸️ BlockingResourceLimiter
WaitFor(ctx, value) (ReleaseFunc, error)"] - - %% Multi Provider - MultiLimiterProvider["🔗 MultiLimiterProvider
[]BaseLimiterProvider"] - - %% Reservations - RateReservation["📋 RateReservation
WaitTime() time.Duration
Cancel()"] - ResourceReservation["📋 ResourceReservation
Delay() <-chan struct{}
Release()"] - - %% Weight Keys - WeightKey["🏷️ WeightKey
network_bytes
request_count
request_items
memory_size"] - - %% Functional Types - MustDenyFunc["🔧 MustDenyFunc"] - ReserveRateFunc["🔧 ReserveRateFunc"] - ReserveResourceFunc["🔧 ReserveResourceFunc"] - LimiterWrapperFunc["🔧 LimiterWrapperFunc"] - - %% Middleware Integration - HTTPClientLimiter["🌐 HTTP Client Limiter
RoundTripper wrapper"] - HTTPServerLimiter["🌐 HTTP Server Limiter
Handler wrapper"] - GRPCClientLimiter["📡 gRPC Client Limiter
DialOption provider"] - GRPCServerLimiter["📡 gRPC Server Limiter
ServerOption provider"] - - %% Consumer Integration - TracesConsumer["📊 Limited Traces Consumer"] - MetricsConsumer["📊 Limited Metrics Consumer"] - LogsConsumer["📊 Limited Logs Consumer"] - ProfilesConsumer["📊 Limited Profiles Consumer"] - - %% Adapter Functions - BaseToRate["🔄 BaseToRateLimiterProvider"] - BaseToResource["🔄 BaseToResourceLimiterProvider"] - RateToResource["🔄 RateToResourceLimiterProvider"] - BaseToWrapper["🔄 BaseToLimiterWrapperProvider"] - RateToWrapper["🔄 RateToLimiterWrapperProvider"] - ResourceToWrapper["🔄 ResourceToLimiterWrapperProvider"] - + end + %% Provider Inheritance Relationships RateLimiterProvider -.->|extends| BaseLimiterProvider ResourceLimiterProvider -.->|extends| BaseLimiterProvider LimiterWrapperProvider -.->|extends| BaseLimiterProvider - + %% Core Limiter to Provider Relationships BaseLimiterProvider -->|creates| BaseLimiter RateLimiterProvider -->|creates| RateLimiter ResourceLimiterProvider -->|creates| ResourceLimiter LimiterWrapperProvider -->|creates| LimiterWrapper - - %% Reservation Relationships - RateLimiter -->|returns| RateReservation - ResourceLimiter -->|returns| ResourceReservation - - %% Functional Implementation Relationships - MustDenyFunc -.->|implements| BaseLimiter - ReserveRateFunc -.->|implements| RateLimiter - ReserveResourceFunc -.->|implements| ResourceLimiter - LimiterWrapperFunc -.->|implements| LimiterWrapper - - %% Helper Wrapper Relationships - BlockingRateLimiter -->|wraps| RateLimiter - BlockingResourceLimiter -->|wraps| ResourceLimiter - - %% Multi Provider Relationships - MultiLimiterProvider -->|combines| BaseLimiterProvider - MultiLimiterProvider -.->|implements| RateLimiterProvider - MultiLimiterProvider -.->|implements| ResourceLimiterProvider - MultiLimiterProvider -.->|implements| LimiterWrapperProvider - - %% Weight Key Usage - WeightKey -->|used by| RateLimiterProvider - WeightKey -->|used by| ResourceLimiterProvider - WeightKey -->|used by| LimiterWrapperProvider - - %% Adapter Relationships - BaseToRate -->|adapts| BaseLimiterProvider - BaseToResource -->|adapts| BaseLimiterProvider - RateToResource -->|adapts| RateLimiterProvider - BaseToWrapper -->|adapts| BaseLimiterProvider - RateToWrapper -->|adapts| RateLimiterProvider - ResourceToWrapper -->|adapts| ResourceLimiterProvider - - %% Middleware Usage - HTTPClientLimiter -->|uses| LimiterWrapper - HTTPServerLimiter -->|uses| LimiterWrapper - GRPCClientLimiter -->|uses| LimiterWrapper - GRPCServerLimiter -->|uses| LimiterWrapper - - %% Consumer Usage - TracesConsumer -->|uses| LimiterWrapper - MetricsConsumer -->|uses| LimiterWrapper - LogsConsumer -->|uses| LimiterWrapper - ProfilesConsumer -->|uses| LimiterWrapper - - %% Styling - classDef coreInterface fill:#e1f5fe,stroke:#01579b,stroke-width:2px - classDef provider fill:#f3e5f5,stroke:#4a148c,stroke-width:2px - classDef helper fill:#e8f5e8,stroke:#1b5e20,stroke-width:2px - classDef functional fill:#fff3e0,stroke:#e65100,stroke-width:2px - classDef middleware fill:#fce4ec,stroke:#880e4f,stroke-width:2px - classDef adapter fill:#f1f8e9,stroke:#33691e,stroke-width:2px - - class BaseLimiter,RateLimiter,ResourceLimiter,RateReservation,ResourceReservation coreInterface - class BaseLimiterProvider,RateLimiterProvider,ResourceLimiterProvider,LimiterWrapperProvider provider - class LimiterWrapper,BlockingRateLimiter,BlockingResourceLimiter,MultiLimiterProvider helper - class MustDenyFunc,ReserveRateFunc,ReserveResourceFunc,LimiterWrapperFunc functional - class HTTPClientLimiter,HTTPServerLimiter,GRPCClientLimiter,GRPCServerLimiter,TracesConsumer,MetricsConsumer,LogsConsumer,ProfilesConsumer middleware - class BaseToRate,BaseToResource,RateToResource,BaseToWrapper,RateToWrapper,ResourceToWrapper adapter ``` From 54d1fa98092465914fa8b519f3d7df1a327d04e8 Mon Sep 17 00:00:00 2001 From: Joshua MacDonald Date: Fri, 13 Jun 2025 17:06:21 -0700 Subject: [PATCH 33/47] again --- extension/extensionlimiter/README.md | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/extension/extensionlimiter/README.md b/extension/extensionlimiter/README.md index d057cf211a91..4e2b0e94025e 100644 --- a/extension/extensionlimiter/README.md +++ b/extension/extensionlimiter/README.md @@ -444,17 +444,17 @@ The following diagram illustrates the core architecture of the extension limiter graph LR subgraph TB %% Core Limiter Interfaces - BaseLimiter["🔒 BaseLimiter
MustDeny(ctx) error"] - RateLimiter["⏱️ RateLimiter
ReserveRate(ctx, int) (RateReservation, error)"] - ResourceLimiter["💾 ResourceLimiter
ReserveResource(ctx, int) (ResourceReservation, error)"] + BaseLimiter["BaseLimiter"] + RateLimiter["RateLimiter"] + ResourceLimiter["ResourceLimiter"] end subgraph TB %% Provider Interfaces - BaseLimiterProvider["🏭 BaseLimiterProvider
GetBaseLimiter(...Option) (BaseLimiter, error)"] - RateLimiterProvider["🏭 RateLimiterProvider
GetRateLimiter(WeightKey, ...Option) (RateLimiter, error)"] - ResourceLimiterProvider["🏭 ResourceLimiterProvider
GetResourceLimiter(WeightKey, ...Option) (ResourceLimiter, error)"] - LimiterWrapperProvider["🏭 LimiterWrapperProvider
GetLimiterWrapper(WeightKey, ...Option) (LimiterWrapper, error)"] + BaseLimiterProvider["BaseLimiterProvider"] + RateLimiterProvider["RateLimiterProvider"] + ResourceLimiterProvider["ResourceLimiterProvider"] + LimiterWrapperProvider["LimiterWrapperProvider"] end %% Provider Inheritance Relationships From adf2019c44265df88da5cc9eaecb703776068a45 Mon Sep 17 00:00:00 2001 From: Joshua MacDonald Date: Mon, 16 Jun 2025 12:05:31 -0700 Subject: [PATCH 34/47] CheckSaturation --- extension/extensionlimiter/README.md | 28 +++++++++---------- .../extensionlimiter/extensionlimiter.go | 26 ++++++++--------- .../extensionlimiter/limiterhelper/base.go | 16 +++++------ .../limiterhelper/consumer.go | 8 +++--- .../limiterhelper/grpc/grpclimiter.go | 4 +-- .../limiterhelper/http/httplimiter.go | 4 +-- .../limiterhelper/middleware.go | 28 +++++++++---------- .../extensionlimiter/limiterhelper/multi.go | 26 ++++++++--------- .../extensionlimiter/limiterhelper/rate.go | 4 +-- .../extensionlimiter/limiterhelper/wrapper.go | 12 ++++---- extension/extensionlimiter/rate.go | 4 +-- extension/extensionlimiter/resource.go | 4 +-- 12 files changed, 82 insertions(+), 82 deletions(-) diff --git a/extension/extensionlimiter/README.md b/extension/extensionlimiter/README.md index 4e2b0e94025e..497e6479146a 100644 --- a/extension/extensionlimiter/README.md +++ b/extension/extensionlimiter/README.md @@ -76,7 +76,7 @@ extension providers (which may produce configuration errors). All limiter extensions: -- MUST implement the `BaseLimiterProvider` interface +- MUST implement the `SaturationCheckerProvider` interface - MUST NOT implement both the `ResourceLimiterProvider` and the `RateLimiterProvider` interfaces The `limiterhelper` package contains features for composing limiters @@ -144,13 +144,13 @@ provided for callers including the `LimiterWrapper`. ### Limiter saturation -Rate and resource limiter providers have a `GetBaseLimiter` method to -provide a `BaseLimiter`, featuring a `MustDeny` method which is made +Rate and resource limiter providers have a `GetSaturationChecker` method to +provide a `SaturationChecker`, featuring a `MustDeny` method which is made available for applications to test when any limit is fully saturated that would eventually deny the request. -The `BaseLimiter` is consulted at least once and applies to all weight -keys. Because a `BaseLimiter` can be consulted more than once by a +The `SaturationChecker` is consulted at least once and applies to all weight +keys. Because a `SaturationChecker` can be consulted more than once by a receiver and/or middleware, it is possible for requests to be denied over the saturation of limits they were already granted. Users should configure external load balancers and/or horizontal scaling policies @@ -172,7 +172,7 @@ before creating new concurrent work. #### Base -The `memorylimiterextension` is a `BaseLimiterProvider` that makes its +The `memorylimiterextension` is a `SaturationCheckerProvider` that makes its decisions using memory statistics from the garbage collector. This logic was traditionally included in the `memorylimiterprocessor`, however receiver integration with limiter extensions is preferred. @@ -271,7 +271,7 @@ apply request items and memory size: ```golang // Extract limiter extensions from host and list of middleware. - providers, err := configmiddleware.GetBaseLimiters( + providers, err := configmiddleware.GetSaturationCheckers( host, cfg.Middlewares) if err != nil { ... } @@ -287,7 +287,7 @@ apply request items and memory size: if err != nil { ... } // Compute the base limiter from the middlewares for use before scrapes. - s.limiter, err := s.limiterProvider.GetBaseLimiter(host, middlewares) + s.limiter, err := s.limiterProvider.GetSaturationChecker(host, middlewares) if err != nil { ... } ``` @@ -444,26 +444,26 @@ The following diagram illustrates the core architecture of the extension limiter graph LR subgraph TB %% Core Limiter Interfaces - BaseLimiter["BaseLimiter"] + SaturationChecker["SaturationChecker"] RateLimiter["RateLimiter"] ResourceLimiter["ResourceLimiter"] end subgraph TB %% Provider Interfaces - BaseLimiterProvider["BaseLimiterProvider"] + SaturationCheckerProvider["SaturationCheckerProvider"] RateLimiterProvider["RateLimiterProvider"] ResourceLimiterProvider["ResourceLimiterProvider"] LimiterWrapperProvider["LimiterWrapperProvider"] end %% Provider Inheritance Relationships - RateLimiterProvider -.->|extends| BaseLimiterProvider - ResourceLimiterProvider -.->|extends| BaseLimiterProvider - LimiterWrapperProvider -.->|extends| BaseLimiterProvider + RateLimiterProvider -.->|extends| SaturationCheckerProvider + ResourceLimiterProvider -.->|extends| SaturationCheckerProvider + LimiterWrapperProvider -.->|extends| SaturationCheckerProvider %% Core Limiter to Provider Relationships - BaseLimiterProvider -->|creates| BaseLimiter + SaturationCheckerProvider -->|creates| SaturationChecker RateLimiterProvider -->|creates| RateLimiter ResourceLimiterProvider -->|creates| ResourceLimiter LimiterWrapperProvider -->|creates| LimiterWrapper diff --git a/extension/extensionlimiter/extensionlimiter.go b/extension/extensionlimiter/extensionlimiter.go index 00fd29f81798..49d130fda123 100644 --- a/extension/extensionlimiter/extensionlimiter.go +++ b/extension/extensionlimiter/extensionlimiter.go @@ -7,9 +7,9 @@ import ( "context" ) -// BaseLimiter is for checking when a limit is saturated. This can be +// SaturationChecker is for checking when a limit is saturated. This can be // called prior to the start of work to check for limiter saturation. -type BaseLimiter interface { +type SaturationChecker interface { // MustDeny is a request to apply a hard limit. If this // returns non-nil, the caller must not begin new work in this // context. @@ -19,10 +19,10 @@ type BaseLimiter interface { // MustDenyFunc is a functional way to build MustDeny functions. type MustDenyFunc func(context.Context) error -// A MustDeny function is a complete BaseLimiter. -var _ BaseLimiter = MustDenyFunc(nil) +// A MustDeny function is a complete SaturationChecker. +var _ SaturationChecker = MustDenyFunc(nil) -// MustDeny implements BaseLimiter. +// MustDeny implements SaturationChecker. func (f MustDenyFunc) MustDeny(ctx context.Context) error { if f == nil { return nil @@ -30,19 +30,19 @@ func (f MustDenyFunc) MustDeny(ctx context.Context) error { return f(ctx) } -// BaseLimiterProvider is an interface to obtain checkers for a group of +// SaturationCheckerProvider is an interface to obtain checkers for a group of // weight keys. -type BaseLimiterProvider interface { - // GetBaseLimiter returns a checker for a group of weight keys. - GetBaseLimiter(...Option) (BaseLimiter, error) +type SaturationCheckerProvider interface { + // GetSaturationChecker returns a checker for a group of weight keys. + GetSaturationChecker(...Option) (SaturationChecker, error) } -// GetBaseLimiterFunc is a functional way to construct GetBaseLimiter +// GetSaturationCheckerFunc is a functional way to construct GetSaturationChecker // functions, used in limiter providers. -type GetBaseLimiterFunc func(...Option) (BaseLimiter, error) +type GetSaturationCheckerFunc func(...Option) (SaturationChecker, error) -// BaseLimiter implements BaseLimiterProvider. -func (f GetBaseLimiterFunc) GetBaseLimiter(opts ...Option) (BaseLimiter, error) { +// SaturationChecker implements SaturationCheckerProvider. +func (f GetSaturationCheckerFunc) GetSaturationChecker(opts ...Option) (SaturationChecker, error) { if f == nil { return nil, nil } diff --git a/extension/extensionlimiter/limiterhelper/base.go b/extension/extensionlimiter/limiterhelper/base.go index c01538d62add..b775abdb1c70 100644 --- a/extension/extensionlimiter/limiterhelper/base.go +++ b/extension/extensionlimiter/limiterhelper/base.go @@ -11,14 +11,14 @@ import ( // BaseToRateLimiterProvider allows a base limiter to act as a rate // limiter. -func BaseToRateLimiterProvider(blimp extensionlimiter.BaseLimiterProvider) extensionlimiter.RateLimiterProvider { +func BaseToRateLimiterProvider(blimp extensionlimiter.SaturationCheckerProvider) extensionlimiter.RateLimiterProvider { return struct { - extensionlimiter.GetBaseLimiterFunc + extensionlimiter.GetSaturationCheckerFunc extensionlimiter.GetRateLimiterFunc }{ - blimp.GetBaseLimiter, + blimp.GetSaturationChecker, func(_ extensionlimiter.WeightKey, opts ...extensionlimiter.Option) (extensionlimiter.RateLimiter, error) { - base, err := blimp.GetBaseLimiter(opts...) + base, err := blimp.GetSaturationChecker(opts...) if err != nil { return nil, err } @@ -38,14 +38,14 @@ func BaseToRateLimiterProvider(blimp extensionlimiter.BaseLimiterProvider) exten // BaseToResourceLimiterProvider allows a base limiter to act as a // resource limiter. -func BaseToResourceLimiterProvider(blimp extensionlimiter.BaseLimiterProvider) extensionlimiter.ResourceLimiterProvider { +func BaseToResourceLimiterProvider(blimp extensionlimiter.SaturationCheckerProvider) extensionlimiter.ResourceLimiterProvider { return struct { - extensionlimiter.GetBaseLimiterFunc + extensionlimiter.GetSaturationCheckerFunc extensionlimiter.GetResourceLimiterFunc }{ - blimp.GetBaseLimiter, + blimp.GetSaturationChecker, func(_ extensionlimiter.WeightKey, opts ...extensionlimiter.Option) (extensionlimiter.ResourceLimiter, error) { - base, err := blimp.GetBaseLimiter(opts...) + base, err := blimp.GetSaturationChecker(opts...) if err != nil { return nil, err } diff --git a/extension/extensionlimiter/limiterhelper/consumer.go b/extension/extensionlimiter/limiterhelper/consumer.go index 042786149cd2..bfc16d3106b3 100644 --- a/extension/extensionlimiter/limiterhelper/consumer.go +++ b/extension/extensionlimiter/limiterhelper/consumer.go @@ -144,15 +144,15 @@ func limitOne[P any, C any]( }, opts...) } -// applyBaseLimiter gets a BaseLimiter and wraps the pipeline in a MustDeny +// applySaturationChecker gets a SaturationChecker and wraps the pipeline in a MustDeny // check. -func applyBaseLimiter[P any, C any]( +func applySaturationChecker[P any, C any]( next C, provider LimiterWrapperProvider, m traits[P, C], opts []consumer.Option, ) (C, error) { - ck, err := provider.GetBaseLimiter() + ck, err := provider.GetSaturationChecker() if err != nil { return next, err } @@ -192,7 +192,7 @@ func newLimited[P any, C any]( func(_ P) int { return 1 }) - next, err4 = applyBaseLimiter(next, provider, m, opts) + next, err4 = applySaturationChecker(next, provider, m, opts) return next, multierr.Append(err1, multierr.Append(err2, multierr.Append(err3, err4))) } diff --git a/extension/extensionlimiter/limiterhelper/grpc/grpclimiter.go b/extension/extensionlimiter/limiterhelper/grpc/grpclimiter.go index a1e5d12bbca6..dd3707881c56 100644 --- a/extension/extensionlimiter/limiterhelper/grpc/grpclimiter.go +++ b/extension/extensionlimiter/limiterhelper/grpc/grpclimiter.go @@ -12,7 +12,7 @@ import ( "go.opentelemetry.io/collector/extension/extensionmiddleware" ) -func NewClientLimiter(ext extensionlimiter.BaseLimiterProvider) (extensionmiddleware.GRPCClient, error) { +func NewClientLimiter(ext extensionlimiter.SaturationCheckerProvider) (extensionmiddleware.GRPCClient, error) { wp, err1 := limiterhelper.MiddlewareToLimiterWrapperProvider(ext) rp, err2 := limiterhelper.MiddlewareToRateLimiterProvider(ext) if err := multierr.Append(err1, err2); err != nil { @@ -70,7 +70,7 @@ func NewClientLimiter(ext extensionlimiter.BaseLimiterProvider) (extensionmiddle }), nil } -func NewServerLimiter(ext extensionlimiter.BaseLimiterProvider) (extensionmiddleware.GRPCServer, error) { +func NewServerLimiter(ext extensionlimiter.SaturationCheckerProvider) (extensionmiddleware.GRPCServer, error) { wp, err1 := limiterhelper.MiddlewareToLimiterWrapperProvider(ext) rp, err2 := limiterhelper.MiddlewareToRateLimiterProvider(ext) if err := multierr.Append(err1, err2); err != nil { diff --git a/extension/extensionlimiter/limiterhelper/http/httplimiter.go b/extension/extensionlimiter/limiterhelper/http/httplimiter.go index ac5908939930..9cdd2a357db1 100644 --- a/extension/extensionlimiter/limiterhelper/http/httplimiter.go +++ b/extension/extensionlimiter/limiterhelper/http/httplimiter.go @@ -13,7 +13,7 @@ import ( "go.opentelemetry.io/collector/extension/extensionmiddleware/extensionmiddlewaretest" ) -func NewClientLimiter(ext extensionlimiter.BaseLimiterProvider) (extensionmiddleware.HTTPClient, error) { +func NewClientLimiter(ext extensionlimiter.SaturationCheckerProvider) (extensionmiddleware.HTTPClient, error) { wp, err1 := limiterhelper.MiddlewareToLimiterWrapperProvider(ext) rp, err2 := limiterhelper.MiddlewareToRateLimiterProvider(ext) if err := multierr.Append(err1, err2); err != nil { @@ -83,7 +83,7 @@ func (rb *rateLimitedBody) Close() error { return rb.body.Close() } -func NewServerLimiter(ext extensionlimiter.BaseLimiterProvider) (extensionmiddleware.HTTPServer, error) { +func NewServerLimiter(ext extensionlimiter.SaturationCheckerProvider) (extensionmiddleware.HTTPServer, error) { wp, err1 := limiterhelper.MiddlewareToLimiterWrapperProvider(ext) rp, err2 := limiterhelper.MiddlewareToRateLimiterProvider(ext) if err := multierr.Append(err1, err2); err != nil { diff --git a/extension/extensionlimiter/limiterhelper/middleware.go b/extension/extensionlimiter/limiterhelper/middleware.go index af158943d7e4..717fb64b1a9b 100644 --- a/extension/extensionlimiter/limiterhelper/middleware.go +++ b/extension/extensionlimiter/limiterhelper/middleware.go @@ -19,7 +19,7 @@ var ( // middlewareCheck applies consistency checks and returns a valid // limiter extension of any known kind. -func middlewareCheck(ext extensionlimiter.BaseLimiterProvider) (extensionlimiter.BaseLimiterProvider, error) { +func middlewareCheck(ext extensionlimiter.SaturationCheckerProvider) (extensionlimiter.SaturationCheckerProvider, error) { _, isResource := ext.(extensionlimiter.ResourceLimiterProvider) _, isRate := ext.(extensionlimiter.RateLimiterProvider) @@ -32,7 +32,7 @@ func middlewareCheck(ext extensionlimiter.BaseLimiterProvider) (extensionlimiter // MultipleProvider constructs a combined limiter from an ordered list // of middlewares. This constructor ignores middleware configs that // are not limiters. -func MultipleProvider(exts []extensionlimiter.BaseLimiterProvider) (MultiLimiterProvider, error) { +func MultipleProvider(exts []extensionlimiter.SaturationCheckerProvider) (MultiLimiterProvider, error) { var retErr error var providers MultiLimiterProvider for _, ext := range exts { @@ -43,14 +43,14 @@ func MultipleProvider(exts []extensionlimiter.BaseLimiterProvider) (MultiLimiter return providers, retErr } -// MiddlewareToBaseLimiterProvider returns a base limiter provider +// MiddlewareToSaturationCheckerProvider returns a base limiter provider // from middleware. Returns a package-level error if the middleware // does not implement exactly one of the limiter interfaces (i.e., // rate or resource). -func MiddlewareToBaseLimiterProvider(ext extensionlimiter.BaseLimiterProvider) (extensionlimiter.BaseLimiterProvider, error) { +func MiddlewareToSaturationCheckerProvider(ext extensionlimiter.SaturationCheckerProvider) (extensionlimiter.SaturationCheckerProvider, error) { return getMiddleware( ext, - identity[extensionlimiter.BaseLimiterProvider], + identity[extensionlimiter.SaturationCheckerProvider], baseProvider[extensionlimiter.RateLimiterProvider], baseProvider[extensionlimiter.ResourceLimiterProvider], ) @@ -60,7 +60,7 @@ func MiddlewareToBaseLimiterProvider(ext extensionlimiter.BaseLimiterProvider) ( // provider from middleware. Returns a package-level error if the // middleware does not implement exactly one of the limiter // interfaces (i.e., rate or resource). -func MiddlewareToLimiterWrapperProvider(ext extensionlimiter.BaseLimiterProvider) (LimiterWrapperProvider, error) { +func MiddlewareToLimiterWrapperProvider(ext extensionlimiter.SaturationCheckerProvider) (LimiterWrapperProvider, error) { return getMiddleware( ext, nilError(BaseToLimiterWrapperProvider), @@ -75,7 +75,7 @@ func MiddlewareToLimiterWrapperProvider(ext extensionlimiter.BaseLimiterProvider // interface. Returns a package-level error if the middleware does not // implement exactly one of the limiter interfaces (i.e., rate or // resource). -func MiddlewareToRateLimiterProvider(ext extensionlimiter.BaseLimiterProvider) (extensionlimiter.RateLimiterProvider, error) { +func MiddlewareToRateLimiterProvider(ext extensionlimiter.SaturationCheckerProvider) (extensionlimiter.RateLimiterProvider, error) { return getMiddleware( ext, nilError(BaseToRateLimiterProvider), @@ -90,7 +90,7 @@ func MiddlewareToRateLimiterProvider(ext extensionlimiter.BaseLimiterProvider) ( // interface. Returns a package-level error if the middleware does not // implement exactly one of the limiter interfaces (i.e., rate or // resource). -func MiddlewareToResourceLimiterProvider(ext extensionlimiter.BaseLimiterProvider) (extensionlimiter.ResourceLimiterProvider, error) { +func MiddlewareToResourceLimiterProvider(ext extensionlimiter.SaturationCheckerProvider) (extensionlimiter.ResourceLimiterProvider, error) { return getMiddleware( ext, nilError(BaseToResourceLimiterProvider), @@ -102,8 +102,8 @@ func MiddlewareToResourceLimiterProvider(ext extensionlimiter.BaseLimiterProvide // getProvider invokes getProvider if any kind of limiter is detected // for the given host and middleware configuration. func getMiddleware[Out any]( - ext extensionlimiter.BaseLimiterProvider, - base func(extensionlimiter.BaseLimiterProvider) (Out, error), + ext extensionlimiter.SaturationCheckerProvider, + base func(extensionlimiter.SaturationCheckerProvider) (Out, error), rate func(extensionlimiter.RateLimiterProvider) (Out, error), resource func(extensionlimiter.ResourceLimiterProvider) (Out, error), ) (Out, error) { @@ -118,8 +118,8 @@ func getMiddleware[Out any]( // getProvider handles each limiter kind, case-by-case, for building // limiters in a functional style. func getProvider[Out any]( - ext extensionlimiter.BaseLimiterProvider, - base func(extensionlimiter.BaseLimiterProvider) (Out, error), + ext extensionlimiter.SaturationCheckerProvider, + base func(extensionlimiter.SaturationCheckerProvider) (Out, error), rate func(extensionlimiter.RateLimiterProvider) (Out, error), resource func(extensionlimiter.ResourceLimiterProvider) (Out, error), ) (Out, error) { @@ -129,7 +129,7 @@ func getProvider[Out any]( if lim, ok := ext.(extensionlimiter.RateLimiterProvider); ok { return rate(lim) } - if lim, ok := ext.(extensionlimiter.BaseLimiterProvider); ok { + if lim, ok := ext.(extensionlimiter.SaturationCheckerProvider); ok { return base(lim) } var out Out @@ -142,7 +142,7 @@ func identity[T any](lim T) (T, error) { } // baseProvider returns a base limiter type from any limiter. -func baseProvider[T extensionlimiter.BaseLimiterProvider](p T) (extensionlimiter.BaseLimiterProvider, error) { +func baseProvider[T extensionlimiter.SaturationCheckerProvider](p T) (extensionlimiter.SaturationCheckerProvider, error) { return p, nil } diff --git a/extension/extensionlimiter/limiterhelper/multi.go b/extension/extensionlimiter/limiterhelper/multi.go index 71ac2e9e5bbb..9e47dc9aaa52 100644 --- a/extension/extensionlimiter/limiterhelper/multi.go +++ b/extension/extensionlimiter/limiterhelper/multi.go @@ -15,26 +15,26 @@ import ( // MultiLimiterProvider combines multiple limiter providers of all // kinds. It automatically applies the adapters in this package to // implement the desired provider interface from the base object. -type MultiLimiterProvider []extensionlimiter.BaseLimiterProvider +type MultiLimiterProvider []extensionlimiter.SaturationCheckerProvider var _ LimiterWrapperProvider = MultiLimiterProvider{} var _ extensionlimiter.RateLimiterProvider = MultiLimiterProvider{} var _ extensionlimiter.ResourceLimiterProvider = MultiLimiterProvider{} -var _ extensionlimiter.BaseLimiterProvider = MultiLimiterProvider{} +var _ extensionlimiter.SaturationCheckerProvider = MultiLimiterProvider{} -// GetBaseLimiter implements LimiterWrapperProvider. The combined +// GetSaturationChecker implements LimiterWrapperProvider. The combined // limiter is saturated when any of the base limiers are. -func (ps MultiLimiterProvider) GetBaseLimiter( +func (ps MultiLimiterProvider) GetSaturationChecker( opts ...extensionlimiter.Option, -) (extensionlimiter.BaseLimiter, error) { +) (extensionlimiter.SaturationChecker, error) { return getMultiLimiter(ps, - identity[extensionlimiter.BaseLimiterProvider], + identity[extensionlimiter.SaturationCheckerProvider], baseProvider[extensionlimiter.RateLimiterProvider], baseProvider[extensionlimiter.ResourceLimiterProvider], - func(p extensionlimiter.BaseLimiterProvider) (extensionlimiter.BaseLimiter, error) { - return p.GetBaseLimiter(opts...) + func(p extensionlimiter.SaturationCheckerProvider) (extensionlimiter.SaturationChecker, error) { + return p.GetSaturationChecker(opts...) }, - combineBaseLimiters) + combineSaturationCheckers) } // GetLimiterWrapper implements LimiterWrapperProvider, applies the @@ -84,8 +84,8 @@ func (ps MultiLimiterProvider) GetRateLimiter( combineRateLimiters) } -// combineBaseLimiters combines >= 2 base limiters. -func combineBaseLimiters(lims []extensionlimiter.BaseLimiter) extensionlimiter.BaseLimiter { +// combineSaturationCheckers combines >= 2 base limiters. +func combineSaturationCheckers(lims []extensionlimiter.SaturationChecker) extensionlimiter.SaturationChecker { return extensionlimiter.MustDenyFunc(func(ctx context.Context) error { var err error for _, lim := range lims { @@ -202,7 +202,7 @@ func combineRateLimiters(lims []extensionlimiter.RateLimiter) extensionlimiter.R // extensions. func getMultiLimiter[Out any, Lim comparable]( multi MultiLimiterProvider, - base func(extensionlimiter.BaseLimiterProvider) (Out, error), + base func(extensionlimiter.SaturationCheckerProvider) (Out, error), rate func(extensionlimiter.RateLimiterProvider) (Out, error), resource func(extensionlimiter.ResourceLimiterProvider) (Out, error), pfunc func(Out) (Lim, error), @@ -210,7 +210,7 @@ func getMultiLimiter[Out any, Lim comparable]( ) (nilResult Lim, _ error) { // Note that nilResult is used in error and non-error cases to // return a nil and not a nil with concrete type (e.g., - // extensionlimiter.BaseLimiterProvider(nil)). + // extensionlimiter.SaturationCheckerProvider(nil)). var lims []Lim for _, baseProvider := range multi { diff --git a/extension/extensionlimiter/limiterhelper/rate.go b/extension/extensionlimiter/limiterhelper/rate.go index 43ef127fdf95..d00c372c640e 100644 --- a/extension/extensionlimiter/limiterhelper/rate.go +++ b/extension/extensionlimiter/limiterhelper/rate.go @@ -129,10 +129,10 @@ func (b BlockingRateLimiter) waitFor(ctx context.Context, value int, timer timer // limter acting as rate limiter) is an invalid configuration. func RateToResourceLimiterProvider(blimp extensionlimiter.RateLimiterProvider) extensionlimiter.ResourceLimiterProvider { return struct { - extensionlimiter.GetBaseLimiterFunc + extensionlimiter.GetSaturationCheckerFunc extensionlimiter.GetResourceLimiterFunc }{ - blimp.GetBaseLimiter, + blimp.GetSaturationChecker, func(weight extensionlimiter.WeightKey, opts ...extensionlimiter.Option) (extensionlimiter.ResourceLimiter, error) { rlim, err := blimp.GetRateLimiter(weight, opts...) if err != nil { diff --git a/extension/extensionlimiter/limiterhelper/wrapper.go b/extension/extensionlimiter/limiterhelper/wrapper.go index 9e1dc13e510d..2b070e8d1706 100644 --- a/extension/extensionlimiter/limiterhelper/wrapper.go +++ b/extension/extensionlimiter/limiterhelper/wrapper.go @@ -12,7 +12,7 @@ import ( // LimiterWrapperProvider follows the provider pattern for // the LimiterWrapper type type LimiterWrapperProvider interface { - extensionlimiter.BaseLimiterProvider + extensionlimiter.SaturationCheckerProvider GetLimiterWrapper(extensionlimiter.WeightKey, ...extensionlimiter.Option) (LimiterWrapper, error) } @@ -30,7 +30,7 @@ func (f GetLimiterWrapperFunc) GetLimiterWrapper(key extensionlimiter.WeightKey, type limiterWrapper struct { GetLimiterWrapperFunc - extensionlimiter.GetBaseLimiterFunc + extensionlimiter.GetSaturationCheckerFunc } var _ LimiterWrapperProvider = limiterWrapper{} @@ -72,9 +72,9 @@ func (f LimiterWrapperFunc) LimitCall(ctx context.Context, value int, call func( // BaseToLimiterWrapperProvider constructs a LimiterWrapperProvider // for a rate limiter extension. -func BaseToLimiterWrapperProvider(rp extensionlimiter.BaseLimiterProvider) LimiterWrapperProvider { +func BaseToLimiterWrapperProvider(rp extensionlimiter.SaturationCheckerProvider) LimiterWrapperProvider { return limiterWrapper{ - GetBaseLimiterFunc: rp.GetBaseLimiter, + GetSaturationCheckerFunc: rp.GetSaturationChecker, GetLimiterWrapperFunc: nil, } } @@ -83,7 +83,7 @@ func BaseToLimiterWrapperProvider(rp extensionlimiter.BaseLimiterProvider) Limit // LimiterWrapperProvider for a resource limiter extension. func ResourceToLimiterWrapperProvider(rp extensionlimiter.ResourceLimiterProvider) LimiterWrapperProvider { return limiterWrapper{ - GetBaseLimiterFunc: rp.GetBaseLimiter, + GetSaturationCheckerFunc: rp.GetSaturationChecker, GetLimiterWrapperFunc: func(key extensionlimiter.WeightKey, opts ...extensionlimiter.Option) (LimiterWrapper, error) { lim, err := rp.GetResourceLimiter(key, opts...) if err != nil { @@ -109,7 +109,7 @@ func ResourceToLimiterWrapperProvider(rp extensionlimiter.ResourceLimiterProvide // for a rate limiter extension. func RateToLimiterWrapperProvider(rp extensionlimiter.RateLimiterProvider) LimiterWrapperProvider { return limiterWrapper{ - GetBaseLimiterFunc: rp.GetBaseLimiter, + GetSaturationCheckerFunc: rp.GetSaturationChecker, GetLimiterWrapperFunc: func(key extensionlimiter.WeightKey, opts ...extensionlimiter.Option) (LimiterWrapper, error) { lim, err := rp.GetRateLimiter(key, opts...) if err != nil { diff --git a/extension/extensionlimiter/rate.go b/extension/extensionlimiter/rate.go index 6100b3796863..1c745849b72b 100644 --- a/extension/extensionlimiter/rate.go +++ b/extension/extensionlimiter/rate.go @@ -15,7 +15,7 @@ import ( // Limiters are covered by configmiddleware configuration, which is // able to construct LimiterWrappers from these providers. type RateLimiterProvider interface { - BaseLimiterProvider + SaturationCheckerProvider // GetRateLimiter returns a rate limiter for a weight key. GetRateLimiter(WeightKey, ...Option) (RateLimiter, error) @@ -35,7 +35,7 @@ func (f GetRateLimiterFunc) GetRateLimiter(key WeightKey, opts ...Option) (RateL var _ RateLimiterProvider = struct { GetRateLimiterFunc - GetBaseLimiterFunc + GetSaturationCheckerFunc }{} // RateLimiter is an interface that an implementation makes available diff --git a/extension/extensionlimiter/resource.go b/extension/extensionlimiter/resource.go index dfd1538bec19..0662bf81225b 100644 --- a/extension/extensionlimiter/resource.go +++ b/extension/extensionlimiter/resource.go @@ -14,7 +14,7 @@ import ( // Limiters are covered by configmiddleware configuration, which // is able to construct LimiterWrappers from these providers. type ResourceLimiterProvider interface { - BaseLimiterProvider + SaturationCheckerProvider GetResourceLimiter(WeightKey, ...Option) (ResourceLimiter, error) } @@ -33,7 +33,7 @@ func (f GetResourceLimiterFunc) GetResourceLimiter(key WeightKey, opts ...Option var _ ResourceLimiterProvider = struct { GetResourceLimiterFunc - GetBaseLimiterFunc + GetSaturationCheckerFunc }{} // ResourceLimiter is an interface that an implementation makes From adf7e9d20f160885333830512e2169576630a648 Mon Sep 17 00:00:00 2001 From: Joshua MacDonald Date: Mon, 16 Jun 2025 12:08:01 -0700 Subject: [PATCH 35/47] MustDeny-> --- extension/extensionlimiter/README.md | 16 ++++++++-------- extension/extensionlimiter/extensionlimiter.go | 16 ++++++++-------- extension/extensionlimiter/limiterhelper/base.go | 4 ++-- .../extensionlimiter/limiterhelper/consumer.go | 4 ++-- .../extensionlimiter/limiterhelper/multi.go | 4 ++-- 5 files changed, 22 insertions(+), 22 deletions(-) diff --git a/extension/extensionlimiter/README.md b/extension/extensionlimiter/README.md index 497e6479146a..232191fc0ec1 100644 --- a/extension/extensionlimiter/README.md +++ b/extension/extensionlimiter/README.md @@ -59,9 +59,9 @@ A limiter is defined as **saturated** when a limit is completely overloaded for at least one weight, generally it means callers should deny new requests. All limiter extensions implement the basic limiter interface, and callers are expected to check for saturation by -invoking `MustDeny` before making individual requests with the limiter. +invoking `CheckSaturation` before making individual requests with the limiter. -Whereas the basic limiter's `MustDeny` method indicates only +Whereas the basic limiter's `CheckSaturation` method indicates only saturation, the rate and resource limiter interfaces both return a `Reservation`. While the details are slightly different, the reservation generally has two features: @@ -145,7 +145,7 @@ provided for callers including the `LimiterWrapper`. ### Limiter saturation Rate and resource limiter providers have a `GetSaturationChecker` method to -provide a `SaturationChecker`, featuring a `MustDeny` method which is made +provide a `SaturationChecker`, featuring a `CheckSaturation` method which is made available for applications to test when any limit is fully saturated that would eventually deny the request. @@ -280,7 +280,7 @@ apply request items and memory size: if err != nil { ... } // Here get a limiter-wrapped pipeline and a combination of weight-specific - // limiters for MustDeny() functionality. + // limiters for CheckSaturation() functionality. limitKeys := extensionlimiter.StandardNotMiddlewareKeys() s.nextMetrics, err = limiterhelper.NewLimitedMetrics( s.nextMetrics, limitKeys, s.limiterProvider) @@ -291,12 +291,12 @@ apply request items and memory size: if err != nil { ... } ``` -In the scraper loop, use `MustDeny` before starting a scrape: +In the scraper loop, use `CheckSaturation` before starting a scrape: ```golang func (s *scraper) scrapeOnce(ctx context.Context) error { // Check if any limits are saturated. - if err := s.limiter.MustDeny(ctx); err != nil { + if err := s.limiter.CheckSaturation(ctx); err != nil { return err } @@ -328,7 +328,7 @@ receivers: - ratelimiter/streamer ``` -The receiver will check `s.limiter.MustDeny()` as above. In a stream, +The receiver will check `s.limiter.CheckSaturation()` as above. In a stream, a blocking limiter is used which blocks the stream (via `s.memorySizeLimiter.WaitFor()`) until limit requests succeed, however after the limit requests succeed, the receiver returns from `Send()` @@ -340,7 +340,7 @@ returns in this example: func (s *scraper) LogsStream(ctx context.Context, stream *Stream) error { for { // Check saturation for all limiters, all keys. - err := s.limiter.MustDeny(ctx) + err := s.limiter.CheckSaturation(ctx) if err != nil { ... } // The network bytes and request count limits are applied in middleware. diff --git a/extension/extensionlimiter/extensionlimiter.go b/extension/extensionlimiter/extensionlimiter.go index 49d130fda123..8115f793a448 100644 --- a/extension/extensionlimiter/extensionlimiter.go +++ b/extension/extensionlimiter/extensionlimiter.go @@ -10,20 +10,20 @@ import ( // SaturationChecker is for checking when a limit is saturated. This can be // called prior to the start of work to check for limiter saturation. type SaturationChecker interface { - // MustDeny is a request to apply a hard limit. If this + // CheckSaturation is a request to apply a hard limit. If this // returns non-nil, the caller must not begin new work in this // context. - MustDeny(context.Context) error + CheckSaturation(context.Context) error } -// MustDenyFunc is a functional way to build MustDeny functions. -type MustDenyFunc func(context.Context) error +// CheckSaturationFunc is a functional way to build CheckSaturation functions. +type CheckSaturationFunc func(context.Context) error -// A MustDeny function is a complete SaturationChecker. -var _ SaturationChecker = MustDenyFunc(nil) +// A CheckSaturation function is a complete SaturationChecker. +var _ SaturationChecker = CheckSaturationFunc(nil) -// MustDeny implements SaturationChecker. -func (f MustDenyFunc) MustDeny(ctx context.Context) error { +// CheckSaturation implements SaturationChecker. +func (f CheckSaturationFunc) CheckSaturation(ctx context.Context) error { if f == nil { return nil } diff --git a/extension/extensionlimiter/limiterhelper/base.go b/extension/extensionlimiter/limiterhelper/base.go index b775abdb1c70..6328dac41f52 100644 --- a/extension/extensionlimiter/limiterhelper/base.go +++ b/extension/extensionlimiter/limiterhelper/base.go @@ -24,7 +24,7 @@ func BaseToRateLimiterProvider(blimp extensionlimiter.SaturationCheckerProvider) } return extensionlimiter.ReserveRateFunc( func(ctx context.Context, _ int) (extensionlimiter.RateReservation, error) { - if err := base.MustDeny(ctx); err != nil { + if err := base.CheckSaturation(ctx); err != nil { return nil, err } return struct { @@ -51,7 +51,7 @@ func BaseToResourceLimiterProvider(blimp extensionlimiter.SaturationCheckerProvi } return extensionlimiter.ReserveResourceFunc( func(ctx context.Context, _ int) (extensionlimiter.ResourceReservation, error) { - if err := base.MustDeny(ctx); err != nil { + if err := base.CheckSaturation(ctx); err != nil { return nil, err } return struct { diff --git a/extension/extensionlimiter/limiterhelper/consumer.go b/extension/extensionlimiter/limiterhelper/consumer.go index bfc16d3106b3..0ec906865bfb 100644 --- a/extension/extensionlimiter/limiterhelper/consumer.go +++ b/extension/extensionlimiter/limiterhelper/consumer.go @@ -144,7 +144,7 @@ func limitOne[P any, C any]( }, opts...) } -// applySaturationChecker gets a SaturationChecker and wraps the pipeline in a MustDeny +// applySaturationChecker gets a SaturationChecker and wraps the pipeline in a CheckSaturation // check. func applySaturationChecker[P any, C any]( next C, @@ -160,7 +160,7 @@ func applySaturationChecker[P any, C any]( return next, nil } return m.create(func(ctx context.Context, data P) error { - if err := ck.MustDeny(ctx); err != nil { + if err := ck.CheckSaturation(ctx); err != nil { return err } return m.consume(ctx, data, next) diff --git a/extension/extensionlimiter/limiterhelper/multi.go b/extension/extensionlimiter/limiterhelper/multi.go index 9e47dc9aaa52..88621ca20289 100644 --- a/extension/extensionlimiter/limiterhelper/multi.go +++ b/extension/extensionlimiter/limiterhelper/multi.go @@ -86,13 +86,13 @@ func (ps MultiLimiterProvider) GetRateLimiter( // combineSaturationCheckers combines >= 2 base limiters. func combineSaturationCheckers(lims []extensionlimiter.SaturationChecker) extensionlimiter.SaturationChecker { - return extensionlimiter.MustDenyFunc(func(ctx context.Context) error { + return extensionlimiter.CheckSaturationFunc(func(ctx context.Context) error { var err error for _, lim := range lims { if lim == nil { continue } - err = multierr.Append(err, lim.MustDeny(ctx)) + err = multierr.Append(err, lim.CheckSaturation(ctx)) } return err }) From 8779f9b6ca8f47ab38010e8491145e42b66eab61 Mon Sep 17 00:00:00 2001 From: Joshua MacDonald Date: Mon, 16 Jun 2025 12:09:22 -0700 Subject: [PATCH 36/47] MemorySize->RequestSize --- extension/extensionlimiter/README.md | 4 ++-- .../extensionlimiter/limiterhelper/consumer.go | 14 +++++++------- extension/extensionlimiter/weight.go | 8 ++++---- 3 files changed, 13 insertions(+), 13 deletions(-) diff --git a/extension/extensionlimiter/README.md b/extension/extensionlimiter/README.md index 232191fc0ec1..9f56586f5851 100644 --- a/extension/extensionlimiter/README.md +++ b/extension/extensionlimiter/README.md @@ -330,7 +330,7 @@ receivers: The receiver will check `s.limiter.CheckSaturation()` as above. In a stream, a blocking limiter is used which blocks the stream (via -`s.memorySizeLimiter.WaitFor()`) until limit requests succeed, however +`s.requestSizeLimiter.WaitFor()`) until limit requests succeed, however after the limit requests succeed, the receiver returns from `Send()` to continue accepting new requests while the consumer works in a separate goroutine. The limit will be released after the consumer @@ -352,7 +352,7 @@ func (s *scraper) LogsStream(ctx context.Context, stream *Stream) error { if err != nil { ... } // Non-blocking limiter call. - release, err := s.memorySizeLimiter.WaitFor(ctx, pdataSize(data)) + release, err := s.requestSizeLimiter.WaitFor(ctx, pdataSize(data)) if err != nil { ... } // Asynchronous work starts here. diff --git a/extension/extensionlimiter/limiterhelper/consumer.go b/extension/extensionlimiter/limiterhelper/consumer.go index 0ec906865bfb..71947131186f 100644 --- a/extension/extensionlimiter/limiterhelper/consumer.go +++ b/extension/extensionlimiter/limiterhelper/consumer.go @@ -24,9 +24,9 @@ import ( type traits[P, C any] interface { // itemCount is SpanCount(), DataPointCount(), or LogRecordCount(). itemCount(P) int - // memorySize uses the appropriate protobuf Sizer as a proxy + // requestSize uses the appropriate protobuf Sizer as a proxy // for memory used. - memorySize(data P) int + requestSize(data P) int // consume calls the appropriate consumer method (e.g., ConsumeTraces) consume(ctx context.Context, data P, next C) error // create is a functional constructor the consumer type (e.g., consumer.NewTraces) @@ -41,7 +41,7 @@ func (traceTraits) itemCount(data ptrace.Traces) int { return data.SpanCount() } -func (traceTraits) memorySize(data ptrace.Traces) int { +func (traceTraits) requestSize(data ptrace.Traces) int { var sizer ptrace.MarshalSizer return sizer.TracesSize(data) } @@ -62,7 +62,7 @@ func (metricTraits) itemCount(data pmetric.Metrics) int { return data.DataPointCount() } -func (metricTraits) memorySize(data pmetric.Metrics) int { +func (metricTraits) requestSize(data pmetric.Metrics) int { var sizer pmetric.MarshalSizer return sizer.MetricsSize(data) } @@ -83,7 +83,7 @@ func (logTraits) itemCount(data plog.Logs) int { return data.LogRecordCount() } -func (logTraits) memorySize(data plog.Logs) int { +func (logTraits) requestSize(data plog.Logs) int { var sizer plog.MarshalSizer return sizer.LogsSize(data) } @@ -104,7 +104,7 @@ func (profileTraits) itemCount(data pprofile.Profiles) int { return data.SampleCount() } -func (profileTraits) memorySize(data pprofile.Profiles) int { +func (profileTraits) requestSize(data pprofile.Profiles) int { var sizer pprofile.MarshalSizer return sizer.ProfilesSize(data) } @@ -182,7 +182,7 @@ func newLimited[P any, C any]( // Note: reverse order of evaluation cost => least-cost applied first. next, err1 = limitOne(next, keys, provider, m, extensionlimiter.WeightKeyMemorySize, opts, func(data P) int { - return m.memorySize(data) + return m.requestSize(data) }) next, err2 = limitOne(next, keys, provider, m, extensionlimiter.WeightKeyRequestItems, opts, func(data P) int { diff --git a/extension/extensionlimiter/weight.go b/extension/extensionlimiter/weight.go index 16d5c8c4ec9d..c7c36acfc14b 100644 --- a/extension/extensionlimiter/weight.go +++ b/extension/extensionlimiter/weight.go @@ -33,9 +33,9 @@ const ( // rate and resource limiters. WeightKeyRequestItems WeightKey = "request_items" - // WeightKeyMemorySize is typically used with ResourceLimiters + // WeightKeyRequestSize is typically used with ResourceLimiters // for limiting active memory usage. - WeightKeyMemorySize WeightKey = "memory_size" + WeightKeyRequestSize WeightKey = "request_size" ) // StandardAllKeys is all the keys that can be automatically @@ -45,7 +45,7 @@ func StandardAllKeys() []WeightKey { WeightKeyNetworkBytes, WeightKeyRequestCount, WeightKeyRequestItems, - WeightKeyMemorySize, + WeightKeyRequestSize, } } @@ -66,6 +66,6 @@ func StandardMiddlewareKeys() []WeightKey { func StandardNotMiddlewareKeys() []WeightKey { return []WeightKey{ WeightKeyRequestItems, - WeightKeyMemorySize, + WeightKeyRequestSize, } } From 570a3a29fd0568db27175307dce3e58ed3ef8ed2 Mon Sep 17 00:00:00 2001 From: Joshua MacDonald Date: Mon, 16 Jun 2025 12:18:15 -0700 Subject: [PATCH 37/47] ftb --- extension/extensionlimiter/limiterhelper/consumer.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/extension/extensionlimiter/limiterhelper/consumer.go b/extension/extensionlimiter/limiterhelper/consumer.go index 71947131186f..65c8aafb4e86 100644 --- a/extension/extensionlimiter/limiterhelper/consumer.go +++ b/extension/extensionlimiter/limiterhelper/consumer.go @@ -180,7 +180,7 @@ func newLimited[P any, C any]( } var err1, err2, err3, err4 error // Note: reverse order of evaluation cost => least-cost applied first. - next, err1 = limitOne(next, keys, provider, m, extensionlimiter.WeightKeyMemorySize, opts, + next, err1 = limitOne(next, keys, provider, m, extensionlimiter.WeightKeyRequestSize, opts, func(data P) int { return m.requestSize(data) }) From ca0a9f6aa723d9c953affadac761c73854436347 Mon Sep 17 00:00:00 2001 From: Joshua MacDonald Date: Mon, 16 Jun 2025 12:25:07 -0700 Subject: [PATCH 38/47] diagram --- extension/extensionlimiter/README.md | 15 ++++++++++++--- 1 file changed, 12 insertions(+), 3 deletions(-) diff --git a/extension/extensionlimiter/README.md b/extension/extensionlimiter/README.md index 9f56586f5851..07e9653e58f8 100644 --- a/extension/extensionlimiter/README.md +++ b/extension/extensionlimiter/README.md @@ -441,15 +441,16 @@ pre-compute limiter instances for the cross product of configurations. The following diagram illustrates the core architecture of the extension limiter system, showing the relationships between interfaces, providers, helpers, and middleware integration: ```mermaid -graph LR - subgraph TB +graph TB + subgraph %% Core Limiter Interfaces SaturationChecker["SaturationChecker"] RateLimiter["RateLimiter"] ResourceLimiter["ResourceLimiter"] + LimiterWrapper["LimiterWrapper"] end - subgraph TB + subgraph %% Provider Interfaces SaturationCheckerProvider["SaturationCheckerProvider"] RateLimiterProvider["RateLimiterProvider"] @@ -467,4 +468,12 @@ graph LR RateLimiterProvider -->|creates| RateLimiter ResourceLimiterProvider -->|creates| ResourceLimiter LimiterWrapperProvider -->|creates| LimiterWrapper + + %% Core Limiter relationships + RateLimiter -->|is a| SaturationChecker + ResourceLimiter -->|is a| SaturationChecker + ResourceLimiter -->|substitution| RateLimiter + LimiterWrapper -->|wraps| SaturationChecker + LimiterWrapper -->|wraps| RateLimiter + LimiterWrapper -->|wraps| ResourceLiiterLimiter ``` From 920236a7873137e97546222acd8bedc4b270a8ba Mon Sep 17 00:00:00 2001 From: Joshua MacDonald Date: Mon, 16 Jun 2025 12:28:31 -0700 Subject: [PATCH 39/47] diagram --- extension/extensionlimiter/README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/extension/extensionlimiter/README.md b/extension/extensionlimiter/README.md index 07e9653e58f8..d4a96d7029f0 100644 --- a/extension/extensionlimiter/README.md +++ b/extension/extensionlimiter/README.md @@ -441,7 +441,7 @@ pre-compute limiter instances for the cross product of configurations. The following diagram illustrates the core architecture of the extension limiter system, showing the relationships between interfaces, providers, helpers, and middleware integration: ```mermaid -graph TB +graph TD subgraph %% Core Limiter Interfaces SaturationChecker["SaturationChecker"] From 5e3ed3981f872f7a38b385613b57508c4e532db4 Mon Sep 17 00:00:00 2001 From: Joshua MacDonald Date: Mon, 16 Jun 2025 12:33:41 -0700 Subject: [PATCH 40/47] diagram --- extension/extensionlimiter/README.md | 31 ++++++++++++---------------- 1 file changed, 13 insertions(+), 18 deletions(-) diff --git a/extension/extensionlimiter/README.md b/extension/extensionlimiter/README.md index d4a96d7029f0..161b2e9af331 100644 --- a/extension/extensionlimiter/README.md +++ b/extension/extensionlimiter/README.md @@ -441,38 +441,33 @@ pre-compute limiter instances for the cross product of configurations. The following diagram illustrates the core architecture of the extension limiter system, showing the relationships between interfaces, providers, helpers, and middleware integration: ```mermaid -graph TD - subgraph - %% Core Limiter Interfaces - SaturationChecker["SaturationChecker"] - RateLimiter["RateLimiter"] - ResourceLimiter["ResourceLimiter"] - LimiterWrapper["LimiterWrapper"] +graph TD; + subgraph + SaturationChecker["SaturationChecker"] + RateLimiter["RateLimiter"] + ResourceLimiter["ResourceLimiter"] + LimiterWrapper["LimiterWrapper"] end subgraph - %% Provider Interfaces - SaturationCheckerProvider["SaturationCheckerProvider"] - RateLimiterProvider["RateLimiterProvider"] - ResourceLimiterProvider["ResourceLimiterProvider"] - LimiterWrapperProvider["LimiterWrapperProvider"] + SaturationCheckerProvider["SaturationCheckerProvider"] + RateLimiterProvider["RateLimiterProvider"] + ResourceLimiterProvider["ResourceLimiterProvider"] + LimiterWrapperProvider["LimiterWrapperProvider"] end - %% Provider Inheritance Relationships RateLimiterProvider -.->|extends| SaturationCheckerProvider ResourceLimiterProvider -.->|extends| SaturationCheckerProvider LimiterWrapperProvider -.->|extends| SaturationCheckerProvider - %% Core Limiter to Provider Relationships SaturationCheckerProvider -->|creates| SaturationChecker RateLimiterProvider -->|creates| RateLimiter ResourceLimiterProvider -->|creates| ResourceLimiter LimiterWrapperProvider -->|creates| LimiterWrapper - %% Core Limiter relationships - RateLimiter -->|is a| SaturationChecker - ResourceLimiter -->|is a| SaturationChecker - ResourceLimiter -->|substitution| RateLimiter + RateLimiter -->|implements| SaturationChecker + ResourceLimiter -->|implements| SaturationChecker + ResourceLimiter -->|substitution possible| RateLimiter LimiterWrapper -->|wraps| SaturationChecker LimiterWrapper -->|wraps| RateLimiter LimiterWrapper -->|wraps| ResourceLiiterLimiter From abcca804117ae0f17f83cea647ad6b746e446cc0 Mon Sep 17 00:00:00 2001 From: Joshua MacDonald Date: Mon, 16 Jun 2025 12:50:14 -0700 Subject: [PATCH 41/47] readme --- extension/extensionlimiter/README.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/extension/extensionlimiter/README.md b/extension/extensionlimiter/README.md index 161b2e9af331..5edb73c9be63 100644 --- a/extension/extensionlimiter/README.md +++ b/extension/extensionlimiter/README.md @@ -442,14 +442,14 @@ The following diagram illustrates the core architecture of the extension limiter ```mermaid graph TD; - subgraph + subgraph "Limiters" SaturationChecker["SaturationChecker"] RateLimiter["RateLimiter"] ResourceLimiter["ResourceLimiter"] LimiterWrapper["LimiterWrapper"] end - subgraph + subgraph "Providers" SaturationCheckerProvider["SaturationCheckerProvider"] RateLimiterProvider["RateLimiterProvider"] ResourceLimiterProvider["ResourceLimiterProvider"] From 2b0c9ae30c9c00425c469ef07826359dc9296e29 Mon Sep 17 00:00:00 2001 From: Joshua MacDonald Date: Mon, 16 Jun 2025 12:56:46 -0700 Subject: [PATCH 42/47] address grpc todo --- .../limiterhelper/grpc/grpclimiter.go | 58 +++++++++++++++++-- 1 file changed, 53 insertions(+), 5 deletions(-) diff --git a/extension/extensionlimiter/limiterhelper/grpc/grpclimiter.go b/extension/extensionlimiter/limiterhelper/grpc/grpclimiter.go index dd3707881c56..ce644e26a7a9 100644 --- a/extension/extensionlimiter/limiterhelper/grpc/grpclimiter.go +++ b/extension/extensionlimiter/limiterhelper/grpc/grpclimiter.go @@ -2,6 +2,7 @@ package grpclimiter import ( "context" + "sync" "go.uber.org/multierr" "google.golang.org/grpc" @@ -12,6 +13,39 @@ import ( "go.opentelemetry.io/collector/extension/extensionmiddleware" ) +// contextKey is a private type for context keys +type contextKey int + +const ( + rateLimiterErrorKey contextKey = iota +) + +// rateLimiterErrorState holds error state,allowing rate limiters to return +// errors in the correct context. +type rateLimiterErrorState struct { + mu sync.Mutex + err error +} + +// checkRateLimiterError checks if there's a prior rate limiter error in the context. +func checkRateLimiterError(ctx context.Context) error { + if state, ok := ctx.Value(rateLimiterErrorKey).(*rateLimiterErrorState); ok { + state.mu.Lock() + defer state.mu.Unlock() + return state.err + } + return nil +} + +// setRateLimiterError sets a rate limiter error in the context +func setRateLimiterError(ctx context.Context, err error) { + if state, ok := ctx.Value(rateLimiterErrorKey).(*rateLimiterErrorState); ok { + state.mu.Lock() + defer state.mu.Unlock() + state.err = multierr.Append(state.err, err) + } +} + func NewClientLimiter(ext extensionlimiter.SaturationCheckerProvider) (extensionmiddleware.GRPCClient, error) { wp, err1 := limiterhelper.MiddlewareToLimiterWrapperProvider(ext) rp, err2 := limiterhelper.MiddlewareToRateLimiterProvider(ext) @@ -38,6 +72,9 @@ func NewClientLimiter(ext extensionlimiter.SaturationCheckerProvider) (extension return requestLimiter.LimitCall( ctxIn, 1, func(ctx context.Context) error { + if err := checkRateLimiterError(ctx); err != nil { + return err + } return invoker(ctx, method, req, reply, cc, opts...) }) }), @@ -95,6 +132,9 @@ func NewServerLimiter(ext extensionlimiter.SaturationCheckerProvider) (extension err := requestLimiter.LimitCall( ctxIn, 1, func(ctx context.Context) error { + if err := checkRateLimiterError(ctx); err != nil { + return err + } var err error resp, err = handler(ctx, req) return err @@ -157,12 +197,14 @@ func (h *limiterStatsHandler) HandleRPC(ctx context.Context, s stats.RPCStats) { return } // Apply rate limiting based on network bytes - // TODO: How does the limiter break the stream? - _ = h.limiter.WaitFor(ctx, wireBytes) + if err := h.limiter.WaitFor(ctx, wireBytes); err != nil { + setRateLimiterError(ctx, err) + } } func (h *limiterStatsHandler) TagConn(ctx context.Context, _ *stats.ConnTagInfo) context.Context { - return ctx + // Create a new context with rate limiter error state + return context.WithValue(ctx, rateLimiterErrorKey, &rateLimiterErrorState{}) } func (h *limiterStatsHandler) HandleConn(ctx context.Context, _ stats.ConnStats) { @@ -177,7 +219,10 @@ type serverStream struct { func (s *serverStream) RecvMsg(m any) error { return s.limiter.LimitCall( s.Context(), 1, - func(_ context.Context) error { + func(ctx context.Context) error { + if err := checkRateLimiterError(ctx); err != nil { + return err + } return s.ServerStream.RecvMsg(m) }) } @@ -199,7 +244,10 @@ type clientStream struct { func (s *clientStream) SendMsg(m any) error { return s.limiter.LimitCall( s.Context(), 1, - func(_ context.Context) error { + func(ctx context.Context) error { + if err := checkRateLimiterError(ctx); err != nil { + return err + } return s.ClientStream.SendMsg(m) }) } From 91456e4fa33310cd523d20ca7ae87c91a27918eb Mon Sep 17 00:00:00 2001 From: Joshua MacDonald Date: Mon, 16 Jun 2025 12:59:06 -0700 Subject: [PATCH 43/47] mermaid --- extension/extensionlimiter/README.md | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/extension/extensionlimiter/README.md b/extension/extensionlimiter/README.md index 5edb73c9be63..1335bd2e66c2 100644 --- a/extension/extensionlimiter/README.md +++ b/extension/extensionlimiter/README.md @@ -465,10 +465,13 @@ graph TD; ResourceLimiterProvider -->|creates| ResourceLimiter LimiterWrapperProvider -->|creates| LimiterWrapper + LimiterWrapper -->|implements| SaturationChecker RateLimiter -->|implements| SaturationChecker ResourceLimiter -->|implements| SaturationChecker + ResourceLimiter -->|substitution possible| RateLimiter + LimiterWrapper -->|wraps| SaturationChecker LimiterWrapper -->|wraps| RateLimiter - LimiterWrapper -->|wraps| ResourceLiiterLimiter + LimiterWrapper -->|wraps| ResourceLimiter ``` From 8f6c3aa97dd9375cc1984c99b01c3900293c4fbb Mon Sep 17 00:00:00 2001 From: Joshua MacDonald Date: Mon, 16 Jun 2025 13:04:30 -0700 Subject: [PATCH 44/47] more --- extension/extensionlimiter/README.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/extension/extensionlimiter/README.md b/extension/extensionlimiter/README.md index 1335bd2e66c2..e4eceeb152d2 100644 --- a/extension/extensionlimiter/README.md +++ b/extension/extensionlimiter/README.md @@ -459,6 +459,10 @@ graph TD; RateLimiterProvider -.->|extends| SaturationCheckerProvider ResourceLimiterProvider -.->|extends| SaturationCheckerProvider LimiterWrapperProvider -.->|extends| SaturationCheckerProvider + ResourceLimiterProvider -->|substitution possible| RateLimiterProvider + LimiterWrapperProvider -->|wraps| SaturationCheckerProvider + LimiterWrapperProvider -->|wraps| RateLimiterProvider + LimiterWrapperProvider -->|wraps| ResourceLimiterProvider SaturationCheckerProvider -->|creates| SaturationChecker RateLimiterProvider -->|creates| RateLimiter From 0beaf7ce751e4d8c1ad24b29d3c18deb20ccf8a1 Mon Sep 17 00:00:00 2001 From: Joshua MacDonald Date: Mon, 16 Jun 2025 13:05:52 -0700 Subject: [PATCH 45/47] renames --- extension/extensionlimiter/{extensionlimiter.go => checker.go} | 0 extension/extensionlimiter/{config.go => option.go} | 0 2 files changed, 0 insertions(+), 0 deletions(-) rename extension/extensionlimiter/{extensionlimiter.go => checker.go} (100%) rename extension/extensionlimiter/{config.go => option.go} (100%) diff --git a/extension/extensionlimiter/extensionlimiter.go b/extension/extensionlimiter/checker.go similarity index 100% rename from extension/extensionlimiter/extensionlimiter.go rename to extension/extensionlimiter/checker.go diff --git a/extension/extensionlimiter/config.go b/extension/extensionlimiter/option.go similarity index 100% rename from extension/extensionlimiter/config.go rename to extension/extensionlimiter/option.go From 170dc941dc3909afc3ff4a25d79e2ce5b8f38dbb Mon Sep 17 00:00:00 2001 From: Joshua MacDonald Date: Mon, 16 Jun 2025 13:37:54 -0700 Subject: [PATCH 46/47] eliminate Base --- extension/extensionlimiter/README.md | 21 ++++++++++----------- 1 file changed, 10 insertions(+), 11 deletions(-) diff --git a/extension/extensionlimiter/README.md b/extension/extensionlimiter/README.md index e4eceeb152d2..b40aa381da0c 100644 --- a/extension/extensionlimiter/README.md +++ b/extension/extensionlimiter/README.md @@ -108,16 +108,16 @@ constraints, use `NewBlockingRateLimiter` or `NewBlockingResourceLimiter`. In cases where control flow is not request scoped (e.g., in middleware measuring network bytes), use a `RateLimiter` interface. If the extension is a basic limiter in this scenario, use the -`BaseToRateLimiterProvider` adapter. Callers MUST NOT configure a -resource limiter for a caller that is restricted to the `RateLimiter` -interface; this configuration SHOULD fail at startup or during -component validation. +`SaturationCheckerToRateLimiterProvider` adapter. Callers MUST NOT +configure a resource limiter for a caller that is restricted to the +`RateLimiter` interface; this configuration SHOULD fail at startup or +during component validation. In cases where due to control flow a wrapper interface cannot be used, as long as the caller is able to arrange for a `Release` function to be called at the proper time, then any kind of limiter can be applied in the form of a `ResourceLimiter`. If the extension is a basic or -rate limiter in this scenario, use the `BaseToResourceLimiterProvider` +rate limiter in this scenario, use the `SaturationCheckerToResourceLimiterProvider` or `RateToResourceLimiterProvider` adapters. Middleware configuration typically automates the configuration of @@ -170,14 +170,14 @@ before creating new concurrent work. ### Built-in limiters -#### Base +#### MemoryLimiter The `memorylimiterextension` is a `SaturationCheckerProvider` that makes its decisions using memory statistics from the garbage collector. This logic was traditionally included in the `memorylimiterprocessor`, however receiver integration with limiter extensions is preferred. -#### Rate +#### RateLimiter A built-in helper implementation of the RateLimiter interface is provided, based on `golang.org/x/time/rate.Limter`. These underlying @@ -188,7 +188,7 @@ rate limiters are parameterized by two numbers: The rate limiter is saturated when there is no burst available. -#### Resource +#### ResourceLimiter A built-in helper implementation of the ResourceLimiter interface is provided, based on a bounded queue with LIFO behavior. These @@ -286,7 +286,7 @@ apply request items and memory size: s.nextMetrics, limitKeys, s.limiterProvider) if err != nil { ... } - // Compute the base limiter from the middlewares for use before scrapes. + // Compute the saturation checker from the middlewares for use before scrapes. s.limiter, err := s.limiterProvider.GetSaturationChecker(host, middlewares) if err != nil { ... } ``` @@ -469,13 +469,12 @@ graph TD; ResourceLimiterProvider -->|creates| ResourceLimiter LimiterWrapperProvider -->|creates| LimiterWrapper - LimiterWrapper -->|implements| SaturationChecker + LimiterWrapper -->|wraps/implements| SaturationChecker RateLimiter -->|implements| SaturationChecker ResourceLimiter -->|implements| SaturationChecker ResourceLimiter -->|substitution possible| RateLimiter - LimiterWrapper -->|wraps| SaturationChecker LimiterWrapper -->|wraps| RateLimiter LimiterWrapper -->|wraps| ResourceLimiter ``` From 7790d4091eb3f32336257811b586b86c29278f9e Mon Sep 17 00:00:00 2001 From: Joshua MacDonald Date: Mon, 16 Jun 2025 13:44:32 -0700 Subject: [PATCH 47/47] request bytes --- extension/extensionlimiter/README.md | 10 +++++----- extension/extensionlimiter/limiterhelper/consumer.go | 4 ++-- extension/extensionlimiter/weight.go | 8 ++++---- 3 files changed, 11 insertions(+), 11 deletions(-) diff --git a/extension/extensionlimiter/README.md b/extension/extensionlimiter/README.md index b40aa381da0c..3ea622f66fc4 100644 --- a/extension/extensionlimiter/README.md +++ b/extension/extensionlimiter/README.md @@ -11,10 +11,10 @@ through middleware and/or directly by pipeline components. This package defines three foundational limiter **kinds**, each with similar but distinct interfaces. A limiter extension is either a -basic limiter, or it implements the basic limiter interface and one of -the weight-based interfaces: +simple checker for "saturation" (defined below), or it extends the +simple checker with a weight-based interface: -- **Basic Limiter**: Makes a simple yes/no decision without a weight +- **Saturation Checker**: Makes a simple yes/no decision without a weight parameter, typically to stop new work in an emergency. - **Rate Limiter**: Controls time-based limits over weights such as bytes or items per second. @@ -26,10 +26,10 @@ integer value and identified by a **weight key** indicating the type of quantity being measured and limited. There are currently four weight keys with a standard definition: -1. Network bytes +1. Network bytes (compressed) 2. Request count 3. Request items -4. Memory size +4. Request bytes (compressed) The foundational interfaces are non-blocking, and each calling convention is different. The various limiter kinds are unified diff --git a/extension/extensionlimiter/limiterhelper/consumer.go b/extension/extensionlimiter/limiterhelper/consumer.go index 65c8aafb4e86..77a4b8c780bf 100644 --- a/extension/extensionlimiter/limiterhelper/consumer.go +++ b/extension/extensionlimiter/limiterhelper/consumer.go @@ -24,7 +24,7 @@ import ( type traits[P, C any] interface { // itemCount is SpanCount(), DataPointCount(), or LogRecordCount(). itemCount(P) int - // requestSize uses the appropriate protobuf Sizer as a proxy + // requestBytes uses the appropriate protobuf Bytesr as a proxy // for memory used. requestSize(data P) int // consume calls the appropriate consumer method (e.g., ConsumeTraces) @@ -180,7 +180,7 @@ func newLimited[P any, C any]( } var err1, err2, err3, err4 error // Note: reverse order of evaluation cost => least-cost applied first. - next, err1 = limitOne(next, keys, provider, m, extensionlimiter.WeightKeyRequestSize, opts, + next, err1 = limitOne(next, keys, provider, m, extensionlimiter.WeightKeyRequestBytes, opts, func(data P) int { return m.requestSize(data) }) diff --git a/extension/extensionlimiter/weight.go b/extension/extensionlimiter/weight.go index c7c36acfc14b..1b2e5ce43210 100644 --- a/extension/extensionlimiter/weight.go +++ b/extension/extensionlimiter/weight.go @@ -33,9 +33,9 @@ const ( // rate and resource limiters. WeightKeyRequestItems WeightKey = "request_items" - // WeightKeyRequestSize is typically used with ResourceLimiters + // WeightKeyRequestBytes is typically used with ResourceLimiters // for limiting active memory usage. - WeightKeyRequestSize WeightKey = "request_size" + WeightKeyRequestBytes WeightKey = "request_bytes" ) // StandardAllKeys is all the keys that can be automatically @@ -45,7 +45,7 @@ func StandardAllKeys() []WeightKey { WeightKeyNetworkBytes, WeightKeyRequestCount, WeightKeyRequestItems, - WeightKeyRequestSize, + WeightKeyRequestBytes, } } @@ -66,6 +66,6 @@ func StandardMiddlewareKeys() []WeightKey { func StandardNotMiddlewareKeys() []WeightKey { return []WeightKey{ WeightKeyRequestItems, - WeightKeyRequestSize, + WeightKeyRequestBytes, } }