From 2eb77f5257d4bb83a488c692d6bfffc348fbd3b7 Mon Sep 17 00:00:00 2001 From: Joshua MacDonald Date: Wed, 30 Apr 2025 13:20:36 -0700 Subject: [PATCH 01/62] 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 00000000000..ded7a36092d --- /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 00000000000..c64b85d7676 --- /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 00000000000..3e2fa915513 --- /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 00000000000..a9da7b72867 --- /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 00000000000..26de4444ce4 --- /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 00000000000..258d157d773 --- /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 00000000000..91735349f63 --- /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 00000000000..70bc9986e35 --- /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 00000000000..704a39e7d5b --- /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 00000000000..16d5c8c4ec9 --- /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 8ce013f88cd..ec076100e16 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 7e5ab5d4b30..4a92a634a28 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/62] 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 c64b85d7676..0c7645e6109 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 3e2fa915513..78fcbf32d90 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 258d157d773..d84b9fe4732 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 91735349f63..95b3788bf63 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 70bc9986e35..cb16f86c78b 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 704a39e7d5b..11dddd3b223 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 00000000000..85b115d009f --- /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/62] 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 0c7645e6109..a1fdb090b84 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/62] 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 a1fdb090b84..29e43943d20 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 78fcbf32d90..0578c4aa246 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 d84b9fe4732..e56e24233db 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 4a92a634a28..c91ed2ea39c 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/62] 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 29e43943d20..24d9fdc600d 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 0578c4aa246..7ada56bc045 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 95b3788bf63..533327479d6 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 cb16f86c78b..a294bb84cb6 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 11dddd3b223..2bd62df41f2 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 85b115d009f..cd521f4487a 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/62] 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 24d9fdc600d..1651302d357 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 c91ed2ea39c..4b2a52e565f 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/62] 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 1651302d357..51cd0efecc6 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/62] 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 7ada56bc045..6d85503dd67 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 e56e24233db..f6810f51e7c 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 533327479d6..b203194cacd 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 a294bb84cb6..a02e9c2c5aa 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 2bd62df41f2..c919fc55979 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 cd521f4487a..0e4f69206f8 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/62] 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/62] 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 6d85503dd67..e2faec9e03e 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 00000000000..bc3fb8c6f49 --- /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 f6810f51e7c..18d05346f65 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 b203194cacd..95570d9b7c4 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 0e4f69206f8..26ec8ab94ac 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 a02e9c2c5aa..f6c0cd49079 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 c919fc55979..e23e38a49ca 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/62] 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 e2faec9e03e..580d1623848 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 bc3fb8c6f49..e5f92ca2e4b 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 26ec8ab94ac..35c7b213c5b 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 f6c0cd49079..b8748281da6 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 e23e38a49ca..2baf417243d 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/62] 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 580d1623848..2a8659c0b9e 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 e5f92ca2e4b..115c8603d52 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 18d05346f65..1db74c5fc16 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 95570d9b7c4..fc284f47f83 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 35c7b213c5b..f3b1e2c7f3e 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 b8748281da6..dcc8d1cc4af 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 2baf417243d..98ba253accf 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 16d5c8c4ec9..47a8d7a4828 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 4b2a52e565f..288be4c3e7f 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/62] 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 51cd0efecc6..fbeb44a0f18 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 2a8659c0b9e..04789e79dab 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 115c8603d52..fcad04b4c18 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 1db74c5fc16..c4eb3aa7d56 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 fc284f47f83..aabcc175aa3 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 47a8d7a4828..28fcfcf0b9b 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/62] 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 fbeb44a0f18..4656ebece4f 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/62] 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 04789e79dab..ef8829c24dd 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 fcad04b4c18..a3023e7196b 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 c4eb3aa7d56..1962f883e4b 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 aabcc175aa3..a2e31729504 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 f3b1e2c7f3e..5f7048c179a 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 dcc8d1cc4af..bd506dde1d4 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 98ba253accf..eae95c84d94 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/62] 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 00000000000..16ac275b7f5 --- /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 ef8829c24dd..00fd29f8179 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/62] 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 1962f883e4b..e8e9dac7c96 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 28fcfcf0b9b..16d5c8c4ec9 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/62] 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 5f7048c179a..9b3702f2ab0 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 bd506dde1d4..e842204fdf3 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 eae95c84d94..97942cf191a 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/62] 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 76fc85c8217..8262370481f 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 00000000000..1428e0b299d --- /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 a3023e7196b..00000000000 --- 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 e8e9dac7c96..25b811d0146 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 00000000000..b1c99386da7 --- /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 00000000000..208d35aab07 --- /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 a2e31729504..17efb438aff 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 9b3702f2ab0..fee38f3f22a 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/62] 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 b1c99386da7..d4b4b918adc 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 208d35aab07..3bec0ad40e9 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/62] 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 8262370481f..7726b08b526 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 26de4444ce4..399c55d64bb 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 1428e0b299d..adbb078fad4 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 25b811d0146..ee521e99443 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 d4b4b918adc..ced96727d86 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 3bec0ad40e9..065244d5812 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 17efb438aff..788f1ff1b5d 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 00000000000..9ef2d4db384 --- /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 00000000000..ed2069126c4 --- /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 00000000000..90b0a2b8570 --- /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 fee38f3f22a..535f0712557 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 e842204fdf3..6100b379686 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 97942cf191a..90fded5c3ab 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/62] 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 4656ebece4f..9476bb86f94 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 788f1ff1b5d..3cfcf95faff 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 90b0a2b8570..ffd90e010f2 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/62] 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 9476bb86f94..fadab4117ff 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 adbb078fad4..dacf20864d2 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 3cfcf95faff..9efd93bbf33 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 00000000000..debda96045d --- /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 ed2069126c4..13f6a5b1d43 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 535f0712557..de8cb05a139 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 90fded5c3ab..dfd1538bec1 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/62] 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 dacf20864d2..c01538d62ad 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 ee521e99443..fc57364d67d 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 9efd93bbf33..9897f96a2c3 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 debda96045d..c56b62f2260 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 13f6a5b1d43..609f77b8cce 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 de8cb05a139..9e1dc13e510 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/62] 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 9ef2d4db384..c54d21a75a8 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 609f77b8cce..315e69aae30 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 ffd90e010f2..eda2d6349cf 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/62] 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 831ba7766fe..b8150bea82e 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 3073e304a92..f2ac287197e 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 b86d93ca300..8805d7f19ab 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 7726b08b526..198c6a3be18 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 ced96727d86..4be50130593 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 065244d5812..88462bb3937 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 9897f96a2c3..b9c16e1733c 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 c56b62f2260..f4ff2a5607e 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 214d2886b45..c1e57f1d212 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 d8601f04317..017b4ea9acf 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 288be4c3e7f..6834b8fec30 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/62] 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 fadab4117ff..80ddb60ad7d 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 b9c16e1733c..2eb5fb263f8 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 eda2d6349cf..d6838c164b7 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 1c30f790314..e3db637cad0 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 4986243ce10..62fbc62f026 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/62] 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 80ddb60ad7d..f552d41fbae 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/62] 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 fc57364d67d..12cdd021ff9 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 2eb5fb263f8..eae69d5702b 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 f4ff2a5607e..36713f34f4a 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 d6838c164b7..a5f7b1f8699 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/62] 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 f552d41fbae..dad88f283c9 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/62] 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 208c93fe5a0..cf1b4704f5b 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 5009176a9ad..7f7ed8c0401 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 45549f5c3f6..4a420a51865 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 62fc766b03f..6fb07f741a2 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 36f9b2eedcc..4f8f7a960d2 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 d2ecf97a466..a3c8fe49a41 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/62] 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 590a7698c4f..244f79bdd3c 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 590a7698c4f..244f79bdd3c 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 31a68540d29..58543183a81 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 a6537e7cac8..d057cf211a9 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/62] 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 d057cf211a9..4e2b0e94025 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/62] 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 4e2b0e94025..497e6479146 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 00fd29f8179..49d130fda12 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 c01538d62ad..b775abdb1c7 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 042786149cd..bfc16d3106b 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 a1e5d12bbca..dd3707881c5 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 ac590893993..9cdd2a357db 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 af158943d7e..717fb64b1a9 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 71ac2e9e5bb..9e47dc9aaa5 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 43ef127fdf9..d00c372c640 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 9e1dc13e510..2b070e8d170 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 6100b379686..1c745849b72 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 dfd1538bec1..0662bf81225 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/62] 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 497e6479146..232191fc0ec 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 49d130fda12..8115f793a44 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 b775abdb1c7..6328dac41f5 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 bfc16d3106b..0ec906865bf 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 9e47dc9aaa5..88621ca2028 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/62] 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 232191fc0ec..9f56586f585 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 0ec906865bf..71947131186 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 16d5c8c4ec9..c7c36acfc14 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/62] 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 71947131186..65c8aafb4e8 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/62] 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 9f56586f585..07e9653e58f 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/62] 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 07e9653e58f..d4a96d7029f 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/62] 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 d4a96d7029f..161b2e9af33 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/62] 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 161b2e9af33..5edb73c9be6 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/62] 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 dd3707881c5..ce644e26a7a 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/62] 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 5edb73c9be6..1335bd2e66c 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/62] more --- extension/extensionlimiter/README.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/extension/extensionlimiter/README.md b/extension/extensionlimiter/README.md index 1335bd2e66c..e4eceeb152d 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/62] 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/62] 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 e4eceeb152d..b40aa381da0 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/62] 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 b40aa381da0..3ea622f66fc 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 65c8aafb4e8..77a4b8c780b 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 c7c36acfc14..1b2e5ce4321 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, } } From e354c1a11f8209f95dab3cfb4d65dcc9f629b835 Mon Sep 17 00:00:00 2001 From: Joshua MacDonald Date: Tue, 17 Jun 2025 11:33:52 -0700 Subject: [PATCH 48/62] wip do not read --- extension/extensionlimiter/README.md | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/extension/extensionlimiter/README.md b/extension/extensionlimiter/README.md index 3ea622f66fc..14a66ff202c 100644 --- a/extension/extensionlimiter/README.md +++ b/extension/extensionlimiter/README.md @@ -10,9 +10,9 @@ through middleware and/or directly by pipeline components. ## Overview This package defines three foundational limiter **kinds**, each with -similar but distinct interfaces. A limiter extension is either a -simple checker for "saturation" (defined below), or it extends the -simple checker with a weight-based interface: +similar but distinct interfaces. A limiter extension can be either a +simple "saturation" checker (defined below), or it extends the simple +checker interface with a weight-based interface: - **Saturation Checker**: Makes a simple yes/no decision without a weight parameter, typically to stop new work in an emergency. @@ -29,7 +29,9 @@ weight keys with a standard definition: 1. Network bytes (compressed) 2. Request count 3. Request items -4. Request bytes (compressed) +4. Request bytes (uncompressed) + + The foundational interfaces are non-blocking, and each calling convention is different. The various limiter kinds are unified From 965692b29ca16d8cae15d2a5564a0e4b69b282e1 Mon Sep 17 00:00:00 2001 From: Joshua MacDonald Date: Tue, 17 Jun 2025 15:26:36 -0700 Subject: [PATCH 49/62] wip --- config/configmiddleware/configmiddleware.go | 16 +- extension/extensionlimiter/README.md | 3 + extension/extensionlimiter/checker.go | 50 ---- .../extensionlimiter/limiterhelper/base.go | 64 ----- .../limiterhelper/consumer.go | 28 +- .../limiterhelper/grpc/grpclimiter.go | 4 +- .../limiterhelper/http/httplimiter.go | 4 +- .../limiterhelper/middleware.go | 99 ++------ .../extensionlimiter/limiterhelper/multi.go | 239 ------------------ .../extensionlimiter/limiterhelper/rate.go | 8 +- .../limiterhelper/resource.go | 6 - .../extensionlimiter/limiterhelper/wrapper.go | 22 +- extension/extensionlimiter/rate.go | 46 ++-- extension/extensionlimiter/resource.go | 36 +-- .../memorylimiterextension/memorylimiter.go | 19 +- 15 files changed, 106 insertions(+), 538 deletions(-) delete mode 100644 extension/extensionlimiter/checker.go delete mode 100644 extension/extensionlimiter/limiterhelper/base.go delete mode 100644 extension/extensionlimiter/limiterhelper/multi.go diff --git a/config/configmiddleware/configmiddleware.go b/config/configmiddleware/configmiddleware.go index b8150bea82e..0ec3e225d4f 100644 --- a/config/configmiddleware/configmiddleware.go +++ b/config/configmiddleware/configmiddleware.go @@ -15,7 +15,7 @@ import ( "google.golang.org/grpc" "go.opentelemetry.io/collector/component" - "go.opentelemetry.io/collector/extension/extensionlimiter" + "go.opentelemetry.io/collector/extension/extensionlimiter/limiterhelper" grpclimiter "go.opentelemetry.io/collector/extension/extensionlimiter/limiterhelper/grpc" httplimiter "go.opentelemetry.io/collector/extension/extensionlimiter/limiterhelper/http" "go.opentelemetry.io/collector/extension/extensionmiddleware" @@ -51,7 +51,7 @@ 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 { + if limiter, ok := ext.(limiterhelper.AnyProvider); ok { limiter, err := httplimiter.NewClientLimiter(limiter) if err != nil { return nil, err @@ -73,7 +73,7 @@ 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 { + if limiter, ok := ext.(limiterhelper.AnyProvider); ok { limiter, err := httplimiter.NewServerLimiter(limiter) if err != nil { return nil, err @@ -95,7 +95,7 @@ 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 { + if limiter, ok := ext.(limiterhelper.AnyProvider); ok { lim, err := grpclimiter.NewClientLimiter(limiter) if err != nil { return nil, err @@ -116,7 +116,7 @@ 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 { + if limiter, ok := ext.(limiterhelper.AnyProvider); ok { lim, err := grpclimiter.NewServerLimiter(limiter) if err != nil { return nil, err @@ -132,9 +132,9 @@ func (m Config) GetGRPCServerOptions(_ context.Context, extensions map[component // 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) { +func GetBaseLimiters(host component.Host, cfgs []Config) ([]limiterhelper.AnyProvider, error) { var err error - var lims []extensionlimiter.BaseLimiterProvider + var lims []limiterhelper.AnyProvider all := host.GetExtensions() for _, m := range cfgs { ext, ok := all[m.ID] @@ -142,7 +142,7 @@ func GetBaseLimiters(host component.Host, cfgs []Config) ([]extensionlimiter.Bas err = multierr.Append(err, resolveFailed(m.ID)) continue } - if lim, ok := ext.(extensionlimiter.BaseLimiterProvider); ok { + if lim, ok := ext.(limiterhelper.AnyProvider); ok { lims = append(lims, lim) } else { // Note: In this case, we skip the middleware diff --git a/extension/extensionlimiter/README.md b/extension/extensionlimiter/README.md index 14a66ff202c..9916ac619e9 100644 --- a/extension/extensionlimiter/README.md +++ b/extension/extensionlimiter/README.md @@ -480,3 +480,6 @@ graph TD; LimiterWrapper -->|wraps| RateLimiter LimiterWrapper -->|wraps| ResourceLimiter ``` + +TODO describe connection with +https://github.com/elastic/opentelemetry-collector-components/blob/main/processor/ratelimitprocessor/README.md diff --git a/extension/extensionlimiter/checker.go b/extension/extensionlimiter/checker.go deleted file mode 100644 index 8115f793a44..00000000000 --- a/extension/extensionlimiter/checker.go +++ /dev/null @@ -1,50 +0,0 @@ -// Copyright The OpenTelemetry Authors -// SPDX-License-Identifier: Apache-2.0 - -package extensionlimiter // import "go.opentelemetry.io/collector/extension/extensionlimiter" - -import ( - "context" -) - -// 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 { - // CheckSaturation is a request to apply a hard limit. If this - // returns non-nil, the caller must not begin new work in this - // context. - CheckSaturation(context.Context) error -} - -// CheckSaturationFunc is a functional way to build CheckSaturation functions. -type CheckSaturationFunc func(context.Context) error - -// A CheckSaturation function is a complete SaturationChecker. -var _ SaturationChecker = CheckSaturationFunc(nil) - -// CheckSaturation implements SaturationChecker. -func (f CheckSaturationFunc) CheckSaturation(ctx context.Context) error { - if f == nil { - return nil - } - return f(ctx) -} - -// SaturationCheckerProvider is an interface to obtain checkers for a group of -// weight keys. -type SaturationCheckerProvider interface { - // GetSaturationChecker returns a checker for a group of weight keys. - GetSaturationChecker(...Option) (SaturationChecker, error) -} - -// GetSaturationCheckerFunc is a functional way to construct GetSaturationChecker -// functions, used in limiter providers. -type GetSaturationCheckerFunc func(...Option) (SaturationChecker, error) - -// SaturationChecker implements SaturationCheckerProvider. -func (f GetSaturationCheckerFunc) GetSaturationChecker(opts ...Option) (SaturationChecker, error) { - if f == nil { - return nil, nil - } - return f(opts...) -} diff --git a/extension/extensionlimiter/limiterhelper/base.go b/extension/extensionlimiter/limiterhelper/base.go deleted file mode 100644 index 6328dac41f5..00000000000 --- a/extension/extensionlimiter/limiterhelper/base.go +++ /dev/null @@ -1,64 +0,0 @@ -// 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" -) - -// BaseToRateLimiterProvider allows a base limiter to act as a rate -// limiter. -func BaseToRateLimiterProvider(blimp extensionlimiter.SaturationCheckerProvider) extensionlimiter.RateLimiterProvider { - return struct { - extensionlimiter.GetSaturationCheckerFunc - extensionlimiter.GetRateLimiterFunc - }{ - blimp.GetSaturationChecker, - func(_ extensionlimiter.WeightKey, opts ...extensionlimiter.Option) (extensionlimiter.RateLimiter, error) { - base, err := blimp.GetSaturationChecker(opts...) - if err != nil { - return nil, err - } - return extensionlimiter.ReserveRateFunc( - func(ctx context.Context, _ int) (extensionlimiter.RateReservation, error) { - if err := base.CheckSaturation(ctx); err != nil { - return nil, err - } - return struct { - extensionlimiter.WaitTimeFunc - extensionlimiter.CancelFunc - }{}, nil - }), nil - }, - } -} - -// BaseToResourceLimiterProvider allows a base limiter to act as a -// resource limiter. -func BaseToResourceLimiterProvider(blimp extensionlimiter.SaturationCheckerProvider) extensionlimiter.ResourceLimiterProvider { - return struct { - extensionlimiter.GetSaturationCheckerFunc - extensionlimiter.GetResourceLimiterFunc - }{ - blimp.GetSaturationChecker, - func(_ extensionlimiter.WeightKey, opts ...extensionlimiter.Option) (extensionlimiter.ResourceLimiter, error) { - base, err := blimp.GetSaturationChecker(opts...) - if err != nil { - return nil, err - } - return extensionlimiter.ReserveResourceFunc( - func(ctx context.Context, _ int) (extensionlimiter.ResourceReservation, error) { - if err := base.CheckSaturation(ctx); err != nil { - return nil, err - } - return struct { - extensionlimiter.DelayFunc - extensionlimiter.ReleaseFunc - }{}, nil - }), nil - }, - } -} diff --git a/extension/extensionlimiter/limiterhelper/consumer.go b/extension/extensionlimiter/limiterhelper/consumer.go index 77a4b8c780b..50f6c007700 100644 --- a/extension/extensionlimiter/limiterhelper/consumer.go +++ b/extension/extensionlimiter/limiterhelper/consumer.go @@ -144,29 +144,6 @@ func limitOne[P any, C any]( }, opts...) } -// applySaturationChecker gets a SaturationChecker and wraps the pipeline in a CheckSaturation -// check. -func applySaturationChecker[P any, C any]( - next C, - provider LimiterWrapperProvider, - m traits[P, C], - opts []consumer.Option, -) (C, error) { - ck, err := provider.GetSaturationChecker() - 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.CheckSaturation(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, @@ -178,7 +155,7 @@ func newLimited[P any, C any]( if provider == nil { return next, nil } - var err1, err2, err3, err4 error + var err1, err2, err3 error // Note: reverse order of evaluation cost => least-cost applied first. next, err1 = limitOne(next, keys, provider, m, extensionlimiter.WeightKeyRequestBytes, opts, func(data P) int { @@ -192,8 +169,7 @@ func newLimited[P any, C any]( func(_ P) int { return 1 }) - next, err4 = applySaturationChecker(next, provider, m, opts) - return next, multierr.Append(err1, multierr.Append(err2, multierr.Append(err3, err4))) + return next, multierr.Append(err1, multierr.Append(err2, err3)) } // NewLimitedTraces applies a limiter using the provider over keys before calling next. diff --git a/extension/extensionlimiter/limiterhelper/grpc/grpclimiter.go b/extension/extensionlimiter/limiterhelper/grpc/grpclimiter.go index ce644e26a7a..13a77a8feab 100644 --- a/extension/extensionlimiter/limiterhelper/grpc/grpclimiter.go +++ b/extension/extensionlimiter/limiterhelper/grpc/grpclimiter.go @@ -46,7 +46,7 @@ func setRateLimiterError(ctx context.Context, err error) { } } -func NewClientLimiter(ext extensionlimiter.SaturationCheckerProvider) (extensionmiddleware.GRPCClient, error) { +func NewClientLimiter(ext limiterhelper.AnyProvider) (extensionmiddleware.GRPCClient, error) { wp, err1 := limiterhelper.MiddlewareToLimiterWrapperProvider(ext) rp, err2 := limiterhelper.MiddlewareToRateLimiterProvider(ext) if err := multierr.Append(err1, err2); err != nil { @@ -107,7 +107,7 @@ func NewClientLimiter(ext extensionlimiter.SaturationCheckerProvider) (extension }), nil } -func NewServerLimiter(ext extensionlimiter.SaturationCheckerProvider) (extensionmiddleware.GRPCServer, error) { +func NewServerLimiter(ext limiterhelper.AnyProvider) (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 9cdd2a357db..b0b99a17eb7 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.SaturationCheckerProvider) (extensionmiddleware.HTTPClient, error) { +func NewClientLimiter(ext limiterhelper.AnyProvider) (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.SaturationCheckerProvider) (extensionmiddleware.HTTPServer, error) { +func NewServerLimiter(ext limiterhelper.AnyProvider) (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 717fb64b1a9..2f0ce9adaaf 100644 --- a/extension/extensionlimiter/limiterhelper/middleware.go +++ b/extension/extensionlimiter/limiterhelper/middleware.go @@ -6,8 +6,7 @@ package limiterhelper // import "go.opentelemetry.io/collector/extension/extensi import ( "errors" - "go.uber.org/multierr" - + "go.opentelemetry.io/collector/extension" "go.opentelemetry.io/collector/extension/extensionlimiter" ) @@ -17,53 +16,19 @@ var ( ErrLimiterConflict = errors.New("limiter implements both rate and resource-limiters") ) -// middlewareCheck applies consistency checks and returns a valid -// limiter extension of any known kind. -func middlewareCheck(ext extensionlimiter.SaturationCheckerProvider) (extensionlimiter.SaturationCheckerProvider, error) { - _, isResource := ext.(extensionlimiter.ResourceLimiterProvider) - _, isRate := ext.(extensionlimiter.RateLimiterProvider) - - if isResource && isRate { - return nil, ErrLimiterConflict - } - return ext, nil -} - -// MultipleProvider constructs a combined limiter from an ordered list -// of middlewares. This constructor ignores middleware configs that -// are not limiters. -func MultipleProvider(exts []extensionlimiter.SaturationCheckerProvider) (MultiLimiterProvider, error) { - var retErr error - var providers MultiLimiterProvider - for _, ext := range exts { - base, err := middlewareCheck(ext) - retErr = multierr.Append(retErr, err) - providers = append(providers, base) - } - return providers, retErr -} - -// 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 MiddlewareToSaturationCheckerProvider(ext extensionlimiter.SaturationCheckerProvider) (extensionlimiter.SaturationCheckerProvider, error) { - return getMiddleware( - ext, - identity[extensionlimiter.SaturationCheckerProvider], - baseProvider[extensionlimiter.RateLimiterProvider], - baseProvider[extensionlimiter.ResourceLimiterProvider], - ) -} +// AnyProvider is an any extension type, possibly one of the limiter +// interfaces. Users will see ErrNotALimiter when the implementation +// does not match a know limiter interface, ErrLimiterConflict if the +// extension implements more than one limiter interface. +type AnyProvider extension.Extension // 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(ext extensionlimiter.SaturationCheckerProvider) (LimiterWrapperProvider, error) { +func MiddlewareToLimiterWrapperProvider(ext AnyProvider) (LimiterWrapperProvider, error) { return getMiddleware( ext, - nilError(BaseToLimiterWrapperProvider), nilError(RateToLimiterWrapperProvider), nilError(ResourceToLimiterWrapperProvider), ) @@ -75,10 +40,9 @@ func MiddlewareToLimiterWrapperProvider(ext extensionlimiter.SaturationCheckerPr // 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.SaturationCheckerProvider) (extensionlimiter.RateLimiterProvider, error) { +func MiddlewareToRateLimiterProvider(ext AnyProvider) (extensionlimiter.RateLimiterProvider, error) { return getMiddleware( ext, - nilError(BaseToRateLimiterProvider), identity[extensionlimiter.RateLimiterProvider], resourceToRateLimiterError, ) @@ -90,10 +54,9 @@ func MiddlewareToRateLimiterProvider(ext extensionlimiter.SaturationCheckerProvi // 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.SaturationCheckerProvider) (extensionlimiter.ResourceLimiterProvider, error) { +func MiddlewareToResourceLimiterProvider(ext AnyProvider) (extensionlimiter.ResourceLimiterProvider, error) { return getMiddleware( ext, - nilError(BaseToResourceLimiterProvider), nilError(RateToResourceLimiterProvider), identity[extensionlimiter.ResourceLimiterProvider], ) @@ -102,37 +65,22 @@ func MiddlewareToResourceLimiterProvider(ext extensionlimiter.SaturationCheckerP // getProvider invokes getProvider if any kind of limiter is detected // for the given host and middleware configuration. func getMiddleware[Out any]( - ext extensionlimiter.SaturationCheckerProvider, - base func(extensionlimiter.SaturationCheckerProvider) (Out, error), + ext AnyProvider, rate func(extensionlimiter.RateLimiterProvider) (Out, error), resource func(extensionlimiter.ResourceLimiterProvider) (Out, error), ) (Out, error) { var out Out - ext, err := middlewareCheck(ext) - if err != nil { - return out, err - } - 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.SaturationCheckerProvider, - base func(extensionlimiter.SaturationCheckerProvider) (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) + res, isResource := ext.(extensionlimiter.ResourceLimiterProvider) + rat, isRate := ext.(extensionlimiter.RateLimiterProvider) + if isResource && isRate { + return out, ErrLimiterConflict } - if lim, ok := ext.(extensionlimiter.RateLimiterProvider); ok { - return rate(lim) + if isResource { + return resource(res) } - if lim, ok := ext.(extensionlimiter.SaturationCheckerProvider); ok { - return base(lim) + if isRate { + return rate(rat) } - var out Out return out, ErrNotALimiter } @@ -141,12 +89,13 @@ func identity[T any](lim T) (T, error) { return lim, nil } -// baseProvider returns a base limiter type from any limiter. -func baseProvider[T extensionlimiter.SaturationCheckerProvider](p T) (extensionlimiter.SaturationCheckerProvider, 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 deleted file mode 100644 index 88621ca2028..00000000000 --- a/extension/extensionlimiter/limiterhelper/multi.go +++ /dev/null @@ -1,239 +0,0 @@ -// Copyright The OpenTelemetry Authors -// SPDX-License-Identifier: Apache-2.0 - -package limiterhelper // import "go.opentelemetry.io/collector/extension/extensionlimiter/limiterhelper" - -import ( - "context" - "time" - - "go.uber.org/multierr" - - "go.opentelemetry.io/collector/extension/extensionlimiter" -) - -// 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.SaturationCheckerProvider - -var _ LimiterWrapperProvider = MultiLimiterProvider{} -var _ extensionlimiter.RateLimiterProvider = MultiLimiterProvider{} -var _ extensionlimiter.ResourceLimiterProvider = MultiLimiterProvider{} -var _ extensionlimiter.SaturationCheckerProvider = MultiLimiterProvider{} - -// GetSaturationChecker implements LimiterWrapperProvider. The combined -// limiter is saturated when any of the base limiers are. -func (ps MultiLimiterProvider) GetSaturationChecker( - opts ...extensionlimiter.Option, -) (extensionlimiter.SaturationChecker, error) { - return getMultiLimiter(ps, - identity[extensionlimiter.SaturationCheckerProvider], - baseProvider[extensionlimiter.RateLimiterProvider], - baseProvider[extensionlimiter.ResourceLimiterProvider], - func(p extensionlimiter.SaturationCheckerProvider) (extensionlimiter.SaturationChecker, error) { - return p.GetSaturationChecker(opts...) - }, - combineSaturationCheckers) -} - -// GetLimiterWrapper implements LimiterWrapperProvider, applies the -// wrappers in a nested sequence. -func (ps MultiLimiterProvider) GetLimiterWrapper( - key extensionlimiter.WeightKey, - opts ...extensionlimiter.Option) (LimiterWrapper, error) { - return getMultiLimiter(ps, - nilError(BaseToLimiterWrapperProvider), - nilError(RateToLimiterWrapperProvider), - nilError(ResourceToLimiterWrapperProvider), - 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) { - return getMultiLimiter(ps, - nilError(BaseToResourceLimiterProvider), - nilError(RateToResourceLimiterProvider), - identity[extensionlimiter.ResourceLimiterProvider], - 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) { - return getMultiLimiter(ps, - nilError(BaseToRateLimiterProvider), - identity[extensionlimiter.RateLimiterProvider], - resourceToRateLimiterError, - func(p extensionlimiter.RateLimiterProvider) (extensionlimiter.RateLimiter, error) { - return p.GetRateLimiter(key, opts...) - }, - combineRateLimiters) -} - -// combineSaturationCheckers combines >= 2 base limiters. -func combineSaturationCheckers(lims []extensionlimiter.SaturationChecker) extensionlimiter.SaturationChecker { - return extensionlimiter.CheckSaturationFunc(func(ctx context.Context) error { - var err error - for _, lim := range lims { - if lim == nil { - continue - } - err = multierr.Append(err, lim.CheckSaturation(ctx)) - } - return err - }) -} - -// combineLimiterWrappers combines >= 2 limiter wrappers (recursive). -func combineLimiterWrappers(lims []LimiterWrapper) LimiterWrapper { - if len(lims) == 1 { - return lims[0] - } - 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) - if rsv != nil { - 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 - } - 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() - } - } - 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 - }{ - func() time.Duration { return wt }, - cancel, - }, nil - } - return extensionlimiter.ReserveRateFunc(reserve) -} - -// getMultiLimiter configures a limiter for multiple limiter -// extensions. -func getMultiLimiter[Out any, Lim comparable]( - multi MultiLimiterProvider, - base func(extensionlimiter.SaturationCheckerProvider) (Out, error), - rate func(extensionlimiter.RateLimiterProvider) (Out, error), - resource func(extensionlimiter.ResourceLimiterProvider) (Out, error), - pfunc func(Out) (Lim, error), - combine func([]Lim) Lim, -) (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.SaturationCheckerProvider(nil)). - var lims []Lim - - for _, baseProvider := range multi { - provider, err := getProvider(baseProvider, base, rate, resource) - if err == nil { - return nilResult, err - } - lim, err := pfunc(provider) - if err == nil { - return nilResult, err - } - var zero Lim - if lim == zero { - continue - } - lims = append(lims, lim) - } - - if len(lims) == 0 { - return nilResult, nil - } - if len(lims) == 1 { - return lims[0], nil - } - return combine(lims), nil -} diff --git a/extension/extensionlimiter/limiterhelper/rate.go b/extension/extensionlimiter/limiterhelper/rate.go index d00c372c640..e1f14c18696 100644 --- a/extension/extensionlimiter/limiterhelper/rate.go +++ b/extension/extensionlimiter/limiterhelper/rate.go @@ -128,11 +128,7 @@ func (b BlockingRateLimiter) waitFor(ctx context.Context, value int, timer timer // 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 { - return struct { - extensionlimiter.GetSaturationCheckerFunc - extensionlimiter.GetResourceLimiterFunc - }{ - blimp.GetSaturationChecker, + return extensionlimiter.GetResourceLimiterFunc( func(weight extensionlimiter.WeightKey, opts ...extensionlimiter.Option) (extensionlimiter.ResourceLimiter, error) { rlim, err := blimp.GetRateLimiter(weight, opts...) if err != nil { @@ -165,5 +161,5 @@ func RateToResourceLimiterProvider(blimp extensionlimiter.RateLimiterProvider) e }, nil }), nil }, - } + ) } diff --git a/extension/extensionlimiter/limiterhelper/resource.go b/extension/extensionlimiter/limiterhelper/resource.go index a5f7b1f8699..87e710d1f92 100644 --- a/extension/extensionlimiter/limiterhelper/resource.go +++ b/extension/extensionlimiter/limiterhelper/resource.go @@ -182,9 +182,3 @@ 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/extensionlimiter/limiterhelper/wrapper.go b/extension/extensionlimiter/limiterhelper/wrapper.go index 2b070e8d170..a2499b59723 100644 --- a/extension/extensionlimiter/limiterhelper/wrapper.go +++ b/extension/extensionlimiter/limiterhelper/wrapper.go @@ -12,8 +12,6 @@ import ( // LimiterWrapperProvider follows the provider pattern for // the LimiterWrapper type type LimiterWrapperProvider interface { - extensionlimiter.SaturationCheckerProvider - GetLimiterWrapper(extensionlimiter.WeightKey, ...extensionlimiter.Option) (LimiterWrapper, error) } @@ -28,12 +26,11 @@ func (f GetLimiterWrapperFunc) GetLimiterWrapper(key extensionlimiter.WeightKey, return f(key, opts...) } -type limiterWrapper struct { +type limiterWrapperProvider struct { GetLimiterWrapperFunc - extensionlimiter.GetSaturationCheckerFunc } -var _ LimiterWrapperProvider = limiterWrapper{} +var _ LimiterWrapperProvider = limiterWrapperProvider{} // LimiterWrapper is a general-purpose interface for limiter consumers // to limit resources with use of a callback. This is the simplest @@ -70,20 +67,10 @@ func (f LimiterWrapperFunc) LimitCall(ctx context.Context, value int, call func( return f(ctx, value, call) } -// BaseToLimiterWrapperProvider constructs a LimiterWrapperProvider -// for a rate limiter extension. -func BaseToLimiterWrapperProvider(rp extensionlimiter.SaturationCheckerProvider) LimiterWrapperProvider { - return limiterWrapper{ - GetSaturationCheckerFunc: rp.GetSaturationChecker, - GetLimiterWrapperFunc: nil, - } -} - // ResourceToLimiterWrapperProvider constructs a // LimiterWrapperProvider for a resource limiter extension. func ResourceToLimiterWrapperProvider(rp extensionlimiter.ResourceLimiterProvider) LimiterWrapperProvider { - return limiterWrapper{ - GetSaturationCheckerFunc: rp.GetSaturationChecker, + return limiterWrapperProvider{ GetLimiterWrapperFunc: func(key extensionlimiter.WeightKey, opts ...extensionlimiter.Option) (LimiterWrapper, error) { lim, err := rp.GetResourceLimiter(key, opts...) if err != nil { @@ -108,8 +95,7 @@ func ResourceToLimiterWrapperProvider(rp extensionlimiter.ResourceLimiterProvide // RateToLimiterWrapperProvider constructs a LimiterWrapperProvider // for a rate limiter extension. func RateToLimiterWrapperProvider(rp extensionlimiter.RateLimiterProvider) LimiterWrapperProvider { - return limiterWrapper{ - GetSaturationCheckerFunc: rp.GetSaturationChecker, + return limiterWrapperProvider{ 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 1c745849b72..775568a9859 100644 --- a/extension/extensionlimiter/rate.go +++ b/extension/extensionlimiter/rate.go @@ -15,8 +15,6 @@ import ( // Limiters are covered by configmiddleware configuration, which is // able to construct LimiterWrappers from these providers. type RateLimiterProvider interface { - SaturationCheckerProvider - // GetRateLimiter returns a rate limiter for a weight key. GetRateLimiter(WeightKey, ...Option) (RateLimiter, error) } @@ -28,15 +26,12 @@ type GetRateLimiterFunc func(WeightKey, ...Option) (RateLimiter, error) // RateLimiter implements RateLimiterProvider. func (f GetRateLimiterFunc) GetRateLimiter(key WeightKey, opts ...Option) (RateLimiter, error) { if f == nil { - return nil, nil + return NewRateLimiterImpl(nil), nil } return f(key, opts...) } -var _ RateLimiterProvider = struct { - GetRateLimiterFunc - GetSaturationCheckerFunc -}{} +var _ RateLimiterProvider = GetRateLimiterFunc(nil) // RateLimiter is an interface that an implementation makes available // to apply time-based limits on quantities such as the number of @@ -55,14 +50,11 @@ type RateLimiter interface { // 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 + // instrumentation and client metadata; the Context deadline // is not used, should be considered by the caller. ReserveRate(context.Context, int) (RateReservation, error) } -// A rate limiter can be made up of two functions. -var _ RateLimiter = ReserveRateFunc(nil) - // RateReservation is modeled on pkg.go.dev/golang.org/x/time/rate#Reservation type RateReservation interface { // WaitTime returns the duration until this reservation may @@ -78,10 +70,10 @@ type RateReservation interface { // ReserveRateFunc is a functional way to construct ReserveRate functions. type ReserveRateFunc func(context.Context, int) (RateReservation, error) -// Reserve implements part of the RateReserveer interface. +// Reserve implements part of the RateLimiter interface. func (f ReserveRateFunc) ReserveRate(ctx context.Context, value int) (RateReservation, error) { if f == nil { - return nil, nil + return NewRateReservationImpl(nil, nil), nil } return f(ctx, value) } @@ -108,8 +100,30 @@ func (f CancelFunc) Cancel() { f.Cancel() } -// A rate limiter can be made up of three functions. -var _ RateReservation = struct { +func NewRateLimiterProviderImpl(f GetRateLimiterFunc) RateLimiterProvider { + return f +} + +func NewRateLimiterImpl(f ReserveRateFunc) RateLimiter { + return f +} + +// rateReservationImpl is a struct that implements RateReservation. +// The zero state is a no-op. +type rateReservationImpl struct { WaitTimeFunc CancelFunc -}{} +} + +var _ RateReservation = rateReservationImpl{} + +func NewRateReservationImpl(wf WaitTimeFunc, cf CancelFunc) RateReservation { + return rateReservationImpl{ + WaitTimeFunc: wf, + CancelFunc: cf, + } +} + +func NewNopRateReservation() RateReservation { + return rateReservationImpl{} +} diff --git a/extension/extensionlimiter/resource.go b/extension/extensionlimiter/resource.go index 0662bf81225..b7ee3fa7530 100644 --- a/extension/extensionlimiter/resource.go +++ b/extension/extensionlimiter/resource.go @@ -14,8 +14,6 @@ import ( // Limiters are covered by configmiddleware configuration, which // is able to construct LimiterWrappers from these providers. type ResourceLimiterProvider interface { - SaturationCheckerProvider - GetResourceLimiter(WeightKey, ...Option) (ResourceLimiter, error) } @@ -31,11 +29,6 @@ func (f GetResourceLimiterFunc) GetResourceLimiter(key WeightKey, opts ...Option return f(key, opts...) } -var _ ResourceLimiterProvider = struct { - GetResourceLimiterFunc - GetSaturationCheckerFunc -}{} - // 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. @@ -55,7 +48,7 @@ type ResourceLimiter interface { // // 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 + // resume after Delay(). The context is provided for // access to instrumentation and client metadata; the Context // deadline is not used. ReserveResource(context.Context, int) (ResourceReservation, error) @@ -74,11 +67,6 @@ type ResourceReservation interface { 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 @@ -118,12 +106,24 @@ type ReserveResourceFunc func(ctx context.Context, value int) (ResourceReservati // ReserveResource implements a ReserveResource interface method. func (f ReserveResourceFunc) ReserveResource(ctx context.Context, value int) (ResourceReservation, error) { if f == nil { - return struct { - DelayFunc - ReleaseFunc - }{}, nil + return NopResourceReservation{}, nil } return f(ctx, value) } -var _ ResourceLimiter = ReserveResourceFunc(nil) +// NopResourceReservation is a no-op ResourceReservation, Delay() +// returns an immediate channel, release does nothing. +type NopResourceReservation struct { + DelayFunc + ReleaseFunc +} + +var _ ResourceReservation = NopResourceReservation{} + +// NopResourceLimiterProvider is a no-op ResourceLimiterProvider. +// It always returns a NopResourceReservation with nil error. +type NopResourceLimiterProvider struct { + GetResourceLimiterFunc +} + +var _ ResourceLimiterProvider = NopResourceLimiterProvider{} diff --git a/extension/memorylimiterextension/memorylimiter.go b/extension/memorylimiterextension/memorylimiter.go index 62fbc62f026..3ac3064d223 100644 --- a/extension/memorylimiterextension/memorylimiter.go +++ b/extension/memorylimiterextension/memorylimiter.go @@ -22,7 +22,7 @@ type memoryLimiterExtension struct { memLimiter *memorylimiter.MemoryLimiter } -var _ extensionlimiter.BaseLimiterProvider = &memoryLimiterExtension{} +var _ extensionlimiter.RateLimiterProvider = &memoryLimiterExtension{} // newMemoryLimiter returns a new memorylimiter extension. func newMemoryLimiter(cfg *Config, logger *zap.Logger) (*memoryLimiterExtension, error) { @@ -42,15 +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 { +// GetRateLimiter implements extensionlimiter.RateLimiterProvider. +// Note that this extension ignores the weight/key, the context, the +// and the options. +func (ml *memoryLimiterExtension) GetRateLimiter( + _ extensionlimiter.WeightKey, + _ ...extensionlimiter.Option, +) (extensionlimiter.RateLimiter, error) { + return extensionlimiter.ReserveRateFunc(func(_ context.Context, _ int) (extensionlimiter.RateReservation, error) { if ml.MustRefuse() { - return ErrMustRefuse + return nil, ErrMustRefuse } - return nil + return extensionlimiter.NewNopRateReservation(), nil }), nil } From a0a15b4ede42369e9167dac401bcfd0a6f6b71d9 Mon Sep 17 00:00:00 2001 From: Joshua MacDonald Date: Wed, 18 Jun 2025 10:30:00 -0700 Subject: [PATCH 50/62] Impl() style; removal of SaturationChecker --- config/configmiddleware/configmiddleware.go | 16 +-- extension/extensionlimiter/any_limiter.go | 19 +++ .../limiterhelper/consumer.go | 16 +-- .../limiterhelper/grpc/grpclimiter.go | 23 ++-- .../limiterhelper/http/httplimiter.go | 15 +- .../limiterhelper/middleware.go | 21 +-- .../extensionlimiter/limiterhelper/rate.go | 86 ++++++------ .../limiterhelper/resource.go | 14 +- .../extensionlimiter/limiterhelper/wrapper.go | 114 ++++++++-------- .../limiterhelper/wrapper_provider.go | 39 ++++++ extension/extensionlimiter/rate.go | 129 ------------------ extension/extensionlimiter/rate_limiter.go | 57 ++++++++ .../extensionlimiter/rate_limiter_provider.go | 46 +++++++ .../extensionlimiter/rate_reservation.go | 61 +++++++++ extension/extensionlimiter/resource.go | 129 ------------------ .../extensionlimiter/resource_limiter.go | 60 ++++++++ .../resource_limiter_provider.go | 41 ++++++ .../extensionlimiter/resource_reservation.go | 68 +++++++++ extension/extensionlimiter/weight.go | 32 ----- .../memorylimiterextension/memorylimiter.go | 2 + 20 files changed, 536 insertions(+), 452 deletions(-) create mode 100644 extension/extensionlimiter/any_limiter.go create mode 100644 extension/extensionlimiter/limiterhelper/wrapper_provider.go delete mode 100644 extension/extensionlimiter/rate.go create mode 100644 extension/extensionlimiter/rate_limiter.go create mode 100644 extension/extensionlimiter/rate_limiter_provider.go create mode 100644 extension/extensionlimiter/rate_reservation.go delete mode 100644 extension/extensionlimiter/resource.go create mode 100644 extension/extensionlimiter/resource_limiter.go create mode 100644 extension/extensionlimiter/resource_limiter_provider.go create mode 100644 extension/extensionlimiter/resource_reservation.go diff --git a/config/configmiddleware/configmiddleware.go b/config/configmiddleware/configmiddleware.go index 0ec3e225d4f..bc10730ba1a 100644 --- a/config/configmiddleware/configmiddleware.go +++ b/config/configmiddleware/configmiddleware.go @@ -15,7 +15,7 @@ import ( "google.golang.org/grpc" "go.opentelemetry.io/collector/component" - "go.opentelemetry.io/collector/extension/extensionlimiter/limiterhelper" + "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" @@ -51,7 +51,7 @@ func (m Config) GetHTTPClientRoundTripper(_ context.Context, extensions map[comp if client, ok := ext.(extensionmiddleware.HTTPClient); ok { return client.GetHTTPRoundTripper, nil } - if limiter, ok := ext.(limiterhelper.AnyProvider); ok { + if limiter, ok := ext.(extensionlimiter.AnyProvider); ok { limiter, err := httplimiter.NewClientLimiter(limiter) if err != nil { return nil, err @@ -73,7 +73,7 @@ func (m Config) GetHTTPServerHandler(_ context.Context, extensions map[component if server, ok := ext.(extensionmiddleware.HTTPServer); ok { return server.GetHTTPHandler, nil } - if limiter, ok := ext.(limiterhelper.AnyProvider); ok { + if limiter, ok := ext.(extensionlimiter.AnyProvider); ok { limiter, err := httplimiter.NewServerLimiter(limiter) if err != nil { return nil, err @@ -95,7 +95,7 @@ func (m Config) GetGRPCClientOptions(_ context.Context, extensions map[component if client, ok := ext.(extensionmiddleware.GRPCClient); ok { return client.GetGRPCClientOptions() } - if limiter, ok := ext.(limiterhelper.AnyProvider); ok { + if limiter, ok := ext.(extensionlimiter.AnyProvider); ok { lim, err := grpclimiter.NewClientLimiter(limiter) if err != nil { return nil, err @@ -116,7 +116,7 @@ func (m Config) GetGRPCServerOptions(_ context.Context, extensions map[component if server, ok := ext.(extensionmiddleware.GRPCServer); ok { return server.GetGRPCServerOptions() } - if limiter, ok := ext.(limiterhelper.AnyProvider); ok { + if limiter, ok := ext.(extensionlimiter.AnyProvider); ok { lim, err := grpclimiter.NewServerLimiter(limiter) if err != nil { return nil, err @@ -132,9 +132,9 @@ func (m Config) GetGRPCServerOptions(_ context.Context, extensions map[component // 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) ([]limiterhelper.AnyProvider, error) { +func GetBaseLimiters(host component.Host, cfgs []Config) ([]extensionlimiter.AnyProvider, error) { var err error - var lims []limiterhelper.AnyProvider + var lims []extensionlimiter.AnyProvider all := host.GetExtensions() for _, m := range cfgs { ext, ok := all[m.ID] @@ -142,7 +142,7 @@ func GetBaseLimiters(host component.Host, cfgs []Config) ([]limiterhelper.AnyPro err = multierr.Append(err, resolveFailed(m.ID)) continue } - if lim, ok := ext.(limiterhelper.AnyProvider); ok { + if lim, ok := ext.(extensionlimiter.AnyProvider); ok { lims = append(lims, lim) } else { // Note: In this case, we skip the middleware diff --git a/extension/extensionlimiter/any_limiter.go b/extension/extensionlimiter/any_limiter.go new file mode 100644 index 00000000000..a2145477dd3 --- /dev/null +++ b/extension/extensionlimiter/any_limiter.go @@ -0,0 +1,19 @@ +// Copyright The OpenTelemetry Authors +// SPDX-License-Identifier: Apache-2.0 + +package extensionlimiter // import "go.opentelemetry.io/collector/extension/extensionlimiter" + +// AnyProvider is an any limiter implementation, possibly one of the +// limiter interfaces. This serves as a marker for implementations +// which provider rate limiters of any (one)kind. +type AnyProvider interface { + // recognizedProvider may be embedded using an AnyProviderImpl. + recognizedProvider() +} + +// AnyProviderImpl can be embedded as a marker that a component +// implements one of the rate limiters. +type AnyProviderImpl struct {} + +func (AnyProviderImpl) recognizedProvider() {} + diff --git a/extension/extensionlimiter/limiterhelper/consumer.go b/extension/extensionlimiter/limiterhelper/consumer.go index 50f6c007700..4abdfed6af7 100644 --- a/extension/extensionlimiter/limiterhelper/consumer.go +++ b/extension/extensionlimiter/limiterhelper/consumer.go @@ -117,11 +117,11 @@ func (profileTraits) consume(ctx context.Context, data pprofile.Profiles, next x return next.ConsumeProfiles(ctx, data) } -// limitOne obtains a LimiterWrapper and applies a single weight limit. +// limitOne obtains a Wrapper and applies a single weight limit. func limitOne[P any, C any]( next C, keys []extensionlimiter.WeightKey, - provider LimiterWrapperProvider, + provider WrapperProvider, m traits[P, C], key extensionlimiter.WeightKey, opts []consumer.Option, @@ -130,7 +130,7 @@ func limitOne[P any, C any]( if !slices.Contains(keys, key) { return next, nil } - lim, err := provider.GetLimiterWrapper(key) + lim, err := provider.GetWrapper(key) if err != nil { return next, err } @@ -148,7 +148,7 @@ func limitOne[P any, C any]( func newLimited[P any, C any]( next C, keys []extensionlimiter.WeightKey, - provider LimiterWrapperProvider, + provider WrapperProvider, m traits[P, C], opts ...consumer.Option, ) (C, error) { @@ -173,25 +173,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 LimiterWrapperProvider) (consumer.Traces, error) { +func NewLimitedTraces(next consumer.Traces, keys []extensionlimiter.WeightKey, provider WrapperProvider) (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) (consumer.Logs, error) { +func NewLimitedLogs(next consumer.Logs, keys []extensionlimiter.WeightKey, provider WrapperProvider) (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) (consumer.Metrics, error) { +func NewLimitedMetrics(next consumer.Metrics, keys []extensionlimiter.WeightKey, provider WrapperProvider) (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) (xconsumer.Profiles, error) { +func NewLimitedProfiles(next xconsumer.Profiles, keys []extensionlimiter.WeightKey, provider WrapperProvider) (xconsumer.Profiles, error) { return newLimited(next, keys, provider, profileTraits{}, consumer.WithCapabilities(next.Capabilities())) } diff --git a/extension/extensionlimiter/limiterhelper/grpc/grpclimiter.go b/extension/extensionlimiter/limiterhelper/grpc/grpclimiter.go index 13a77a8feab..7feeb00310b 100644 --- a/extension/extensionlimiter/limiterhelper/grpc/grpclimiter.go +++ b/extension/extensionlimiter/limiterhelper/grpc/grpclimiter.go @@ -1,3 +1,6 @@ +// Copyright The OpenTelemetry Authors +// SPDX-License-Identifier: Apache-2.0 + package grpclimiter import ( @@ -46,13 +49,13 @@ func setRateLimiterError(ctx context.Context, err error) { } } -func NewClientLimiter(ext limiterhelper.AnyProvider) (extensionmiddleware.GRPCClient, error) { - wp, err1 := limiterhelper.MiddlewareToLimiterWrapperProvider(ext) +func NewClientLimiter(ext extensionlimiter.AnyProvider) (extensionmiddleware.GRPCClient, error) { + wp, err1 := limiterhelper.MiddlewareToWrapperProvider(ext) rp, err2 := limiterhelper.MiddlewareToRateLimiterProvider(ext) if err := multierr.Append(err1, err2); err != nil { return nil, err } - requestLimiter, err3 := wp.GetLimiterWrapper(extensionlimiter.WeightKeyRequestCount) + requestLimiter, err3 := wp.GetWrapper(extensionlimiter.WeightKeyRequestCount) bytesLimiter, err4 := rp.GetRateLimiter(extensionlimiter.WeightKeyNetworkBytes) if err := multierr.Append(err3, err4); err != nil { return nil, err @@ -107,13 +110,13 @@ func NewClientLimiter(ext limiterhelper.AnyProvider) (extensionmiddleware.GRPCCl }), nil } -func NewServerLimiter(ext limiterhelper.AnyProvider) (extensionmiddleware.GRPCServer, error) { - wp, err1 := limiterhelper.MiddlewareToLimiterWrapperProvider(ext) +func NewServerLimiter(ext extensionlimiter.AnyProvider) (extensionmiddleware.GRPCServer, error) { + wp, err1 := limiterhelper.MiddlewareToWrapperProvider(ext) rp, err2 := limiterhelper.MiddlewareToRateLimiterProvider(ext) if err := multierr.Append(err1, err2); err != nil { return nil, err } - requestLimiter, err3 := wp.GetLimiterWrapper(extensionlimiter.WeightKeyRequestCount) + requestLimiter, err3 := wp.GetWrapper(extensionlimiter.WeightKeyRequestCount) bytesLimiter, err4 := rp.GetRateLimiter(extensionlimiter.WeightKeyNetworkBytes) if err := multierr.Append(err3, err4); err != nil { return nil, err @@ -212,7 +215,7 @@ func (h *limiterStatsHandler) HandleConn(ctx context.Context, _ stats.ConnStats) type serverStream struct { grpc.ServerStream - limiter limiterhelper.LimiterWrapper + limiter limiterhelper.Wrapper } // RecvMsg applies rate limiting to server stream message receiving. @@ -228,7 +231,7 @@ func (s *serverStream) RecvMsg(m any) error { } // wrapServerStream wraps a gRPC server stream with rate limiting. -func wrapServerStream(ss grpc.ServerStream, _ *grpc.StreamServerInfo, limiter limiterhelper.LimiterWrapper) grpc.ServerStream { +func wrapServerStream(ss grpc.ServerStream, _ *grpc.StreamServerInfo, limiter limiterhelper.Wrapper) grpc.ServerStream { return &serverStream{ ServerStream: ss, limiter: limiter, @@ -237,7 +240,7 @@ func wrapServerStream(ss grpc.ServerStream, _ *grpc.StreamServerInfo, limiter li type clientStream struct { grpc.ClientStream - limiter limiterhelper.LimiterWrapper + limiter limiterhelper.Wrapper } // SendMsg applies rate limiting to client stream message sending. @@ -253,7 +256,7 @@ func (s *clientStream) SendMsg(m any) error { } // wrapClientStream wraps a gRPC client stream with rate limiting. -func wrapClientStream(cs grpc.ClientStream, _ string, limiter limiterhelper.LimiterWrapper) grpc.ClientStream { +func wrapClientStream(cs grpc.ClientStream, _ string, limiter limiterhelper.Wrapper) 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 b0b99a17eb7..f0b9aced8b0 100644 --- a/extension/extensionlimiter/limiterhelper/http/httplimiter.go +++ b/extension/extensionlimiter/limiterhelper/http/httplimiter.go @@ -1,3 +1,6 @@ +// Copyright The OpenTelemetry Authors +// SPDX-License-Identifier: Apache-2.0 + package httplimiter import ( @@ -13,13 +16,13 @@ import ( "go.opentelemetry.io/collector/extension/extensionmiddleware/extensionmiddlewaretest" ) -func NewClientLimiter(ext limiterhelper.AnyProvider) (extensionmiddleware.HTTPClient, error) { - wp, err1 := limiterhelper.MiddlewareToLimiterWrapperProvider(ext) +func NewClientLimiter(ext extensionlimiter.AnyProvider) (extensionmiddleware.HTTPClient, error) { + wp, err1 := limiterhelper.MiddlewareToWrapperProvider(ext) rp, err2 := limiterhelper.MiddlewareToRateLimiterProvider(ext) if err := multierr.Append(err1, err2); err != nil { return nil, err } - requestLimiter, err3 := wp.GetLimiterWrapper(extensionlimiter.WeightKeyRequestCount) + requestLimiter, err3 := wp.GetWrapper(extensionlimiter.WeightKeyRequestCount) bytesLimiter, err4 := rp.GetRateLimiter(extensionlimiter.WeightKeyNetworkBytes) if err := multierr.Append(err3, err4); err != nil { return nil, err @@ -83,13 +86,13 @@ func (rb *rateLimitedBody) Close() error { return rb.body.Close() } -func NewServerLimiter(ext limiterhelper.AnyProvider) (extensionmiddleware.HTTPServer, error) { - wp, err1 := limiterhelper.MiddlewareToLimiterWrapperProvider(ext) +func NewServerLimiter(ext extensionlimiter.AnyProvider) (extensionmiddleware.HTTPServer, error) { + wp, err1 := limiterhelper.MiddlewareToWrapperProvider(ext) rp, err2 := limiterhelper.MiddlewareToRateLimiterProvider(ext) if err := multierr.Append(err1, err2); err != nil { return nil, err } - requestLimiter, err3 := wp.GetLimiterWrapper(extensionlimiter.WeightKeyRequestCount) + requestLimiter, err3 := wp.GetWrapper(extensionlimiter.WeightKeyRequestCount) bytesLimiter, err4 := rp.GetRateLimiter(extensionlimiter.WeightKeyNetworkBytes) if err := multierr.Append(err3, err4); err != nil { return nil, err diff --git a/extension/extensionlimiter/limiterhelper/middleware.go b/extension/extensionlimiter/limiterhelper/middleware.go index 2f0ce9adaaf..8ac10ca8caa 100644 --- a/extension/extensionlimiter/limiterhelper/middleware.go +++ b/extension/extensionlimiter/limiterhelper/middleware.go @@ -6,7 +6,6 @@ package limiterhelper // import "go.opentelemetry.io/collector/extension/extensi import ( "errors" - "go.opentelemetry.io/collector/extension" "go.opentelemetry.io/collector/extension/extensionlimiter" ) @@ -16,21 +15,15 @@ var ( ErrLimiterConflict = errors.New("limiter implements both rate and resource-limiters") ) -// AnyProvider is an any extension type, possibly one of the limiter -// interfaces. Users will see ErrNotALimiter when the implementation -// does not match a know limiter interface, ErrLimiterConflict if the -// extension implements more than one limiter interface. -type AnyProvider extension.Extension - -// MiddlewareToLimiterWrapperProvider returns a limiter wrapper +// MiddlewareToWrapperProvider 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(ext AnyProvider) (LimiterWrapperProvider, error) { +func MiddlewareToWrapperProvider(ext extensionlimiter.AnyProvider) (WrapperProvider, error) { return getMiddleware( ext, - nilError(RateToLimiterWrapperProvider), - nilError(ResourceToLimiterWrapperProvider), + nilError(RateToWrapperProvider), + nilError(ResourceToWrapperProvider), ) } @@ -40,7 +33,7 @@ func MiddlewareToLimiterWrapperProvider(ext AnyProvider) (LimiterWrapperProvider // 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 AnyProvider) (extensionlimiter.RateLimiterProvider, error) { +func MiddlewareToRateLimiterProvider(ext extensionlimiter.AnyProvider) (extensionlimiter.RateLimiterProvider, error) { return getMiddleware( ext, identity[extensionlimiter.RateLimiterProvider], @@ -54,7 +47,7 @@ func MiddlewareToRateLimiterProvider(ext AnyProvider) (extensionlimiter.RateLimi // 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 AnyProvider) (extensionlimiter.ResourceLimiterProvider, error) { +func MiddlewareToResourceLimiterProvider(ext extensionlimiter.AnyProvider) (extensionlimiter.ResourceLimiterProvider, error) { return getMiddleware( ext, nilError(RateToResourceLimiterProvider), @@ -65,7 +58,7 @@ func MiddlewareToResourceLimiterProvider(ext AnyProvider) (extensionlimiter.Reso // getProvider invokes getProvider if any kind of limiter is detected // for the given host and middleware configuration. func getMiddleware[Out any]( - ext AnyProvider, + ext extensionlimiter.AnyProvider, rate func(extensionlimiter.RateLimiterProvider) (Out, error), resource func(extensionlimiter.ResourceLimiterProvider) (Out, error), ) (Out, error) { diff --git a/extension/extensionlimiter/limiterhelper/rate.go b/extension/extensionlimiter/limiterhelper/rate.go index e1f14c18696..3451d54ea34 100644 --- a/extension/extensionlimiter/limiterhelper/rate.go +++ b/extension/extensionlimiter/limiterhelper/rate.go @@ -25,46 +25,43 @@ 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 extensionlimiter.NewRateLimiterImpl( + func(ctx context.Context, value int) (extensionlimiter.RateReservation, error) { + // Check if context was canceled. + select { + case <-ctx.Done(): + return nil, context.Cause(ctx) + default: } - } - - // 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) + + // 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 extensionlimiter.NewRateReservationImpl( + func() time.Duration { return wait }, + rsv.Cancel, + ), nil + }, + ) } // BlockingRateLimiter wraps a RateLimiter extension in a blocking @@ -128,13 +125,13 @@ func (b BlockingRateLimiter) waitFor(ctx context.Context, value int, timer timer // 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 { - return extensionlimiter.GetResourceLimiterFunc( + return extensionlimiter.NewResourceLimiterProviderImpl( 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( + return extensionlimiter.NewResourceLimiterImpl( func(ctx context.Context, value int) (extensionlimiter.ResourceReservation, error) { rsv, err := rlim.ReserveRate(ctx, value) if err != nil { @@ -144,10 +141,7 @@ func RateToResourceLimiterProvider(blimp extensionlimiter.RateLimiterProvider) e timer := time.AfterFunc(rsv.WaitTime(), func() { close(cch) }) - return struct { - extensionlimiter.DelayFunc - extensionlimiter.ReleaseFunc - }{ + return extensionlimiter.NewResourceReservationImpl( func() <-chan struct{} { return cch }, func() { select { @@ -158,7 +152,7 @@ func RateToResourceLimiterProvider(blimp extensionlimiter.RateLimiterProvider) e timer.Stop() } }, - }, nil + ), nil }), nil }, ) diff --git a/extension/extensionlimiter/limiterhelper/resource.go b/extension/extensionlimiter/limiterhelper/resource.go index 87e710d1f92..7623e28a9f8 100644 --- a/extension/extensionlimiter/limiterhelper/resource.go +++ b/extension/extensionlimiter/limiterhelper/resource.go @@ -61,10 +61,7 @@ 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 struct { - extensionlimiter.DelayFunc - extensionlimiter.ReleaseFunc - }{ + return extensionlimiter.NewResourceReservationImpl( nil, // No delay func() { // There was never a waiter in this @@ -74,7 +71,7 @@ func (bq *boundedQueue) ReserveResource(ctx context.Context, value int) (extensi bq.releaseLocked(value) }, - }, nil + ), nil } // since we were unable to admit, check if we can wait. @@ -86,10 +83,7 @@ func (bq *boundedQueue) ReserveResource(ctx context.Context, value int) (extensi element := bq.addWaiterLocked(value) waiter := element.Value.(*waiter) - return struct { - extensionlimiter.DelayFunc - extensionlimiter.ReleaseFunc - }{ + return extensionlimiter.NewResourceReservationImpl( func() <-chan struct{} { // The caller waits for this notification // to use the resource. @@ -112,7 +106,7 @@ func (bq *boundedQueue) ReserveResource(ctx context.Context, value int) (extensi bq.admitWaitersLocked() } }, - }, nil + ), nil } func (bq *boundedQueue) admitWaitersLocked() { diff --git a/extension/extensionlimiter/limiterhelper/wrapper.go b/extension/extensionlimiter/limiterhelper/wrapper.go index a2499b59723..aebcc3f9d75 100644 --- a/extension/extensionlimiter/limiterhelper/wrapper.go +++ b/extension/extensionlimiter/limiterhelper/wrapper.go @@ -9,30 +9,7 @@ import ( "go.opentelemetry.io/collector/extension/extensionlimiter" ) -// LimiterWrapperProvider follows the provider pattern for -// the LimiterWrapper type -type LimiterWrapperProvider interface { - GetLimiterWrapper(extensionlimiter.WeightKey, ...extensionlimiter.Option) (LimiterWrapper, error) -} - -// GetLimiterWrapperFunc is an easy way to build GetLimiterWrapper functions. -type GetLimiterWrapperFunc func(extensionlimiter.WeightKey, ...extensionlimiter.Option) (LimiterWrapper, error) - -// GetLimiterWrapper implements LimiterWrapperProvider. -func (f GetLimiterWrapperFunc) GetLimiterWrapper(key extensionlimiter.WeightKey, opts ...extensionlimiter.Option) (LimiterWrapper, error) { - if f == nil { - return LimiterWrapperFunc(nil), nil - } - return f(key, opts...) -} - -type limiterWrapperProvider struct { - GetLimiterWrapperFunc -} - -var _ LimiterWrapperProvider = limiterWrapperProvider{} - -// LimiterWrapper is a general-purpose interface for limiter consumers +// Wrapper 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 @@ -40,12 +17,12 @@ var _ LimiterWrapperProvider = limiterWrapperProvider{} // construction of this interface. // // A wrapped limiter is either a RateLimiter or ResourceLimiter -// interface. LimiterWrappers can be constructed from either of the +// interface. Wrappers 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 { +type Wrapper interface { // 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. @@ -54,63 +31,80 @@ type LimiterWrapper interface { 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, int, func(ctx context.Context) error) error +// LimitCallFunc is a functional way to build Wrappers. +type LimitCallFunc 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 int, call func(ctx context.Context) error) error { +// LimitCall implements part of the Wrapper interface. +func (f LimitCallFunc) LimitCall(ctx context.Context, value int, call func(ctx context.Context) error) error { if f == nil { return call(ctx) } return f(ctx, value, call) } -// ResourceToLimiterWrapperProvider constructs a -// LimiterWrapperProvider for a resource limiter extension. -func ResourceToLimiterWrapperProvider(rp extensionlimiter.ResourceLimiterProvider) LimiterWrapperProvider { - return limiterWrapperProvider{ - GetLimiterWrapperFunc: func(key extensionlimiter.WeightKey, opts ...extensionlimiter.Option) (LimiterWrapper, error) { +// wrapperImpl is a functional Wrapper object. The zero state is a +// no-op. +type wrapperImpl struct { + LimitCallFunc +} + +var _ Wrapper = wrapperImpl{} + +// NewWrapperImpl returns a functional implementation of +// Wrapper. Use a nil argument for the no-op implementation. +func NewWrapperImpl(f LimitCallFunc) Wrapper { + return wrapperImpl{ + LimitCallFunc: f, + } +} + +// ResourceToWrapperProvider constructs a +// WrapperProvider for a resource limiter extension. +func ResourceToWrapperProvider(rp extensionlimiter.ResourceLimiterProvider) WrapperProvider { + return NewWrapperProviderImpl( + func(key extensionlimiter.WeightKey, opts ...extensionlimiter.Option) (Wrapper, error) { lim, err := rp.GetResourceLimiter(key, opts...) if err != nil { return nil, err } if lim == nil { - return nil, nil + return NewWrapperImpl(nil), nil } 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 - } - defer release() - return call(ctx) - }), nil - }, - } + return NewWrapperImpl( + func(ctx context.Context, value int, call func(context.Context) error) error { + release, err := blocking.WaitFor(ctx, value) + if err != nil { + return err + } + defer release() + return call(ctx) + }, + ), nil + }) } -// RateToLimiterWrapperProvider constructs a LimiterWrapperProvider +// RateToWrapperProvider constructs a WrapperProvider // for a rate limiter extension. -func RateToLimiterWrapperProvider(rp extensionlimiter.RateLimiterProvider) LimiterWrapperProvider { - return limiterWrapperProvider{ - GetLimiterWrapperFunc: func(key extensionlimiter.WeightKey, opts ...extensionlimiter.Option) (LimiterWrapper, error) { +func RateToWrapperProvider(rp extensionlimiter.RateLimiterProvider) WrapperProvider { + return NewWrapperProviderImpl( + func(key extensionlimiter.WeightKey, opts ...extensionlimiter.Option) (Wrapper, error) { lim, err := rp.GetRateLimiter(key, opts...) if err != nil { return nil, err } if lim == nil { - return nil, nil + return NewWrapperImpl(nil), 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) - }), nil + return NewWrapperImpl( + 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) + }, + ), nil }, - } + ) } diff --git a/extension/extensionlimiter/limiterhelper/wrapper_provider.go b/extension/extensionlimiter/limiterhelper/wrapper_provider.go new file mode 100644 index 00000000000..d2241389e68 --- /dev/null +++ b/extension/extensionlimiter/limiterhelper/wrapper_provider.go @@ -0,0 +1,39 @@ +// Copyright The OpenTelemetry Authors +// SPDX-License-Identifier: Apache-2.0 + +package limiterhelper // import "go.opentelemetry.io/collector/extension/extensionlimiter/limiterhelper" + +import ( + "go.opentelemetry.io/collector/extension/extensionlimiter" +) + +// WrapperProvider follows the provider pattern for +// the Wrapper type +type WrapperProvider interface { + GetWrapper(extensionlimiter.WeightKey, ...extensionlimiter.Option) (Wrapper, error) +} + +// GetWrapperFunc is an easy way to build GetWrapper functions. +type GetWrapperFunc func(extensionlimiter.WeightKey, ...extensionlimiter.Option) (Wrapper, error) + +// GetWrapper implements WrapperProvider. +func (f GetWrapperFunc) GetWrapper(key extensionlimiter.WeightKey, opts ...extensionlimiter.Option) (Wrapper, error) { + if f == nil { + return NewWrapperImpl(nil), nil + } + return f(key, opts...) +} + +type limiterWrapperProviderImpl struct { + GetWrapperFunc +} + +var _ WrapperProvider = limiterWrapperProviderImpl{} + +// NewWrapperProviderImpl returns a functional implementation of +// WrapperProvider. Use a nil argument for the no-op implementation. +func NewWrapperProviderImpl(f GetWrapperFunc) WrapperProvider { + return limiterWrapperProviderImpl{ + GetWrapperFunc: f, + } +} diff --git a/extension/extensionlimiter/rate.go b/extension/extensionlimiter/rate.go deleted file mode 100644 index 775568a9859..00000000000 --- a/extension/extensionlimiter/rate.go +++ /dev/null @@ -1,129 +0,0 @@ -// Copyright The OpenTelemetry Authors -// SPDX-License-Identifier: Apache-2.0 - -package extensionlimiter // import "go.opentelemetry.io/collector/extension/extensionlimiter" - -import ( - "context" - "time" -) - -// 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 { - // GetRateLimiter returns a rate limiter for a weight key. - GetRateLimiter(WeightKey, ...Option) (RateLimiter, error) -} - -// GetRateLimiterFunc is a functional way to construct GetRateLimiter -// functions. -type GetRateLimiterFunc func(WeightKey, ...Option) (RateLimiter, error) - -// RateLimiter implements RateLimiterProvider. -func (f GetRateLimiterFunc) GetRateLimiter(key WeightKey, opts ...Option) (RateLimiter, error) { - if f == nil { - return NewRateLimiterImpl(nil), nil - } - return f(key, opts...) -} - -var _ RateLimiterProvider = GetRateLimiterFunc(nil) - -// 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 { - // ReserveRate is modeled on pkg.go.dev/golang.org/x/time/rate#Limiter.ReserveN - // - // 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, int) (RateReservation, error) -} - -// 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() -} - -// ReserveRateFunc is a functional way to construct ReserveRate functions. -type ReserveRateFunc func(context.Context, int) (RateReservation, error) - -// Reserve implements part of the RateLimiter interface. -func (f ReserveRateFunc) ReserveRate(ctx context.Context, value int) (RateReservation, error) { - if f == nil { - return NewRateReservationImpl(nil, nil), nil - } - return f(ctx, value) -} - -// 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() -} - -func NewRateLimiterProviderImpl(f GetRateLimiterFunc) RateLimiterProvider { - return f -} - -func NewRateLimiterImpl(f ReserveRateFunc) RateLimiter { - return f -} - -// rateReservationImpl is a struct that implements RateReservation. -// The zero state is a no-op. -type rateReservationImpl struct { - WaitTimeFunc - CancelFunc -} - -var _ RateReservation = rateReservationImpl{} - -func NewRateReservationImpl(wf WaitTimeFunc, cf CancelFunc) RateReservation { - return rateReservationImpl{ - WaitTimeFunc: wf, - CancelFunc: cf, - } -} - -func NewNopRateReservation() RateReservation { - return rateReservationImpl{} -} diff --git a/extension/extensionlimiter/rate_limiter.go b/extension/extensionlimiter/rate_limiter.go new file mode 100644 index 00000000000..05c61f36bfe --- /dev/null +++ b/extension/extensionlimiter/rate_limiter.go @@ -0,0 +1,57 @@ +// Copyright The OpenTelemetry Authors +// SPDX-License-Identifier: Apache-2.0 + +package extensionlimiter // import "go.opentelemetry.io/collector/extension/extensionlimiter" + +import ( + "context" +) + +// 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 { + // ReserveRate is modeled on pkg.go.dev/golang.org/x/time/rate#Limiter.ReserveN + // + // 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, int) (RateReservation, error) +} + +// ReserveRateFunc is a functional way to construct ReserveRate functions. +type ReserveRateFunc func(context.Context, int) (RateReservation, error) + +// Reserve implements part of the RateLimiter interface. +func (f ReserveRateFunc) ReserveRate(ctx context.Context, value int) (RateReservation, error) { + if f == nil { + return NewRateReservationImpl(nil, nil), nil + } + return f(ctx, value) +} + +// rateLimiterImpl is a functional RateLimiter object. The zero state +// is a no-op. +type rateLimiterImpl struct { + ReserveRateFunc +} + +var _ RateLimiter = rateLimiterImpl{} + +// NewRateLimiterImpl returns a functional implementation of +// RateLimiter. Use a nil argument for the no-op implementation. +func NewRateLimiterImpl(f ReserveRateFunc) RateLimiter { + return rateLimiterImpl{ + ReserveRateFunc: f, + } +} diff --git a/extension/extensionlimiter/rate_limiter_provider.go b/extension/extensionlimiter/rate_limiter_provider.go new file mode 100644 index 00000000000..97daee44dc0 --- /dev/null +++ b/extension/extensionlimiter/rate_limiter_provider.go @@ -0,0 +1,46 @@ +// Copyright The OpenTelemetry Authors +// SPDX-License-Identifier: Apache-2.0 + +package extensionlimiter // import "go.opentelemetry.io/collector/extension/extensionlimiter" + +// 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 { + // AnyProvider is provided by embedding AnyProviderImpl. + AnyProvider + + // GetRateLimiter returns a rate limiter for a weight key. + GetRateLimiter(WeightKey, ...Option) (RateLimiter, error) +} + +// GetRateLimiterFunc is a functional way to construct GetRateLimiter +// functions. +type GetRateLimiterFunc func(WeightKey, ...Option) (RateLimiter, error) + +// RateLimiter implements RateLimiterProvider. +func (f GetRateLimiterFunc) GetRateLimiter(key WeightKey, opts ...Option) (RateLimiter, error) { + if f == nil { + return NewRateLimiterImpl(nil), nil + } + return f(key, opts...) +} + +type rateLimiterProviderImpl struct { + AnyProviderImpl + + GetRateLimiterFunc +} + +var _ RateLimiterProvider = rateLimiterProviderImpl{} + +// NewRateLimiterProviderImpl returns a functional implementation of +// RateLimiterProvider. Use a nil argument for the no-op implementation. +func NewRateLimiterProviderImpl(f GetRateLimiterFunc) RateLimiterProvider { + return rateLimiterProviderImpl{ + GetRateLimiterFunc: f, + } +} diff --git a/extension/extensionlimiter/rate_reservation.go b/extension/extensionlimiter/rate_reservation.go new file mode 100644 index 00000000000..472e7b5efd1 --- /dev/null +++ b/extension/extensionlimiter/rate_reservation.go @@ -0,0 +1,61 @@ +// Copyright The OpenTelemetry Authors +// SPDX-License-Identifier: Apache-2.0 + +package extensionlimiter // import "go.opentelemetry.io/collector/extension/extensionlimiter" + +import ( + "time" +) + +// 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() +} + +// 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() +} + +// rateReservationImpl is a struct that implements RateReservation. +// The zero state is a no-op. +type rateReservationImpl struct { + WaitTimeFunc + CancelFunc +} + +var _ RateReservation = rateReservationImpl{} + +// NewRateReservationImpl returns a functional implementation of +// RateReservation. Use a nil argument for the no-op implementation. +func NewRateReservationImpl(wf WaitTimeFunc, cf CancelFunc) RateReservation { + return rateReservationImpl{ + WaitTimeFunc: wf, + CancelFunc: cf, + } +} + diff --git a/extension/extensionlimiter/resource.go b/extension/extensionlimiter/resource.go deleted file mode 100644 index b7ee3fa7530..00000000000 --- a/extension/extensionlimiter/resource.go +++ /dev/null @@ -1,129 +0,0 @@ -// 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 { - GetResourceLimiter(WeightKey, ...Option) (ResourceLimiter, error) -} - -// GetResourceLimiterFunc is a functional way to construct -// GetResourceLimiter functions. -type GetResourceLimiterFunc func(WeightKey, ...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...) -} - -// 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 { - // ReserveRate is modeled on pkg.go.dev/golang.org/x/time/rate#Limiter.ReserveN, - // without the time dimension. - // - // 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. - ReserveResource(context.Context, int) (ResourceReservation, 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() -} - -// ReleaseFunc is called when resources have been released after use. -// -// 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() - -// 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{} - -// Delay calls this function. -func (f DelayFunc) Delay() <-chan struct{} { - if f == 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 int) (ResourceReservation, error) - -// ReserveResource implements a ReserveResource interface method. -func (f ReserveResourceFunc) ReserveResource(ctx context.Context, value int) (ResourceReservation, error) { - if f == nil { - return NopResourceReservation{}, nil - } - return f(ctx, value) -} - -// NopResourceReservation is a no-op ResourceReservation, Delay() -// returns an immediate channel, release does nothing. -type NopResourceReservation struct { - DelayFunc - ReleaseFunc -} - -var _ ResourceReservation = NopResourceReservation{} - -// NopResourceLimiterProvider is a no-op ResourceLimiterProvider. -// It always returns a NopResourceReservation with nil error. -type NopResourceLimiterProvider struct { - GetResourceLimiterFunc -} - -var _ ResourceLimiterProvider = NopResourceLimiterProvider{} diff --git a/extension/extensionlimiter/resource_limiter.go b/extension/extensionlimiter/resource_limiter.go new file mode 100644 index 00000000000..7e1c743a336 --- /dev/null +++ b/extension/extensionlimiter/resource_limiter.go @@ -0,0 +1,60 @@ +// Copyright The OpenTelemetry Authors +// SPDX-License-Identifier: Apache-2.0 + +package extensionlimiter // import "go.opentelemetry.io/collector/extension/extensionlimiter" + +import ( + "context" +) + +// 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 { + // ReserveRate is modeled on pkg.go.dev/golang.org/x/time/rate#Limiter.ReserveN, + // without the time dimension. + // + // 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. + ReserveResource(context.Context, int) (ResourceReservation, error) +} + +// ReserveResourceFunc is a functional way to construct ReserveResource interface methods. +type ReserveResourceFunc func(ctx context.Context, value int) (ResourceReservation, error) + +// ReserveResource implements a ReserveResource interface method. +func (f ReserveResourceFunc) ReserveResource(ctx context.Context, value int) (ResourceReservation, error) { + if f == nil { + return NewResourceReservationImpl(nil, nil), nil + } + return f(ctx, value) +} + +// resourceLimiterImpl is a functional ResourceLimiter object. The zero state +// is a no-op. +type resourceLimiterImpl struct { + ReserveResourceFunc +} + +var _ ResourceLimiter = resourceLimiterImpl{} + +// NewResourceLimiterImpl returns a functional implementation of +// ResourceLimiter. Use a nil argument for the no-op implementation. +func NewResourceLimiterImpl(f ReserveResourceFunc) ResourceLimiter { + return resourceLimiterImpl{ + ReserveResourceFunc: f, + } +} diff --git a/extension/extensionlimiter/resource_limiter_provider.go b/extension/extensionlimiter/resource_limiter_provider.go new file mode 100644 index 00000000000..875f68b529f --- /dev/null +++ b/extension/extensionlimiter/resource_limiter_provider.go @@ -0,0 +1,41 @@ +// Copyright The OpenTelemetry Authors +// SPDX-License-Identifier: Apache-2.0 + +package extensionlimiter // import "go.opentelemetry.io/collector/extension/extensionlimiter" + +// 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 { + GetResourceLimiter(WeightKey, ...Option) (ResourceLimiter, error) +} + +// GetResourceLimiterFunc is a functional way to construct +// GetResourceLimiter functions. +type GetResourceLimiterFunc func(WeightKey, ...Option) (ResourceLimiter, error) + +// GetResourceLimiter implements part of ResourceLimiterProvider. +func (f GetResourceLimiterFunc) GetResourceLimiter(key WeightKey, opts ...Option) (ResourceLimiter, error) { + if f == nil { + return NewResourceLimiterImpl(nil), nil + } + return f(key, opts...) +} + + +type resourceLimiterProviderImpl struct { + GetResourceLimiterFunc +} + +var _ ResourceLimiterProvider = resourceLimiterProviderImpl{} + +// NewResourceLimiterProviderImpl returns a functional implementation of +// ResourceLimiterProvider. Use a nil argument for the no-op implementation. +func NewResourceLimiterProviderImpl(f GetResourceLimiterFunc) ResourceLimiterProvider { + return resourceLimiterProviderImpl{ + GetResourceLimiterFunc: f, + } +} diff --git a/extension/extensionlimiter/resource_reservation.go b/extension/extensionlimiter/resource_reservation.go new file mode 100644 index 00000000000..9d447628f4e --- /dev/null +++ b/extension/extensionlimiter/resource_reservation.go @@ -0,0 +1,68 @@ +// Copyright The OpenTelemetry Authors +// SPDX-License-Identifier: Apache-2.0 + +package extensionlimiter // import "go.opentelemetry.io/collector/extension/extensionlimiter" + +// 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() +} + +// ReleaseFunc is called when resources have been released after use. +// +// 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() + +// 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{} + +// Delay calls this function. +func (f DelayFunc) Delay() <-chan struct{} { + if f == 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 +}() + +// resourceReservationImpl is a struct that implements ResourceReservation. +// The zero state is a no-op. +type resourceReservationImpl struct { + DelayFunc + ReleaseFunc +} + +var _ ResourceReservation = resourceReservationImpl{} + +// NewResourceReservationImpl returns a functional implementation of +// ResourceReservation. Use a nil argument for the no-op implementation. +func NewResourceReservationImpl(df DelayFunc, rf ReleaseFunc) ResourceReservation { + return resourceReservationImpl{ + DelayFunc: df, + ReleaseFunc: rf, + } +} diff --git a/extension/extensionlimiter/weight.go b/extension/extensionlimiter/weight.go index 1b2e5ce4321..51b84fb286b 100644 --- a/extension/extensionlimiter/weight.go +++ b/extension/extensionlimiter/weight.go @@ -37,35 +37,3 @@ const ( // for limiting active memory usage. WeightKeyRequestBytes WeightKey = "request_bytes" ) - -// StandardAllKeys is all the keys that can be automatically -// implemented by middleware and/or limiterhelper. -func StandardAllKeys() []WeightKey { - return []WeightKey{ - WeightKeyNetworkBytes, - WeightKeyRequestCount, - WeightKeyRequestItems, - WeightKeyRequestBytes, - } -} - -// 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, - WeightKeyRequestBytes, - } -} diff --git a/extension/memorylimiterextension/memorylimiter.go b/extension/memorylimiterextension/memorylimiter.go index 3ac3064d223..b6e20ad62d5 100644 --- a/extension/memorylimiterextension/memorylimiter.go +++ b/extension/memorylimiterextension/memorylimiter.go @@ -19,6 +19,8 @@ var ( ) type memoryLimiterExtension struct { + extensionlimiter.AnyProviderImpl + memLimiter *memorylimiter.MemoryLimiter } From fb4fb2e0d4e3910d32bfb49d51f079d03a9f9b21 Mon Sep 17 00:00:00 2001 From: Joshua MacDonald Date: Wed, 18 Jun 2025 13:52:35 -0700 Subject: [PATCH 51/62] oops wrong branch --- component/component.go | 18 +++ component/identifiable.go | 15 +++ config/configmiddleware/configmiddleware.go | 27 ++-- extension/extensionlimiter/any_limiter.go | 6 +- extension/extensionlimiter/go.mod | 6 + .../consumerlimiter.go} | 85 +++++++++---- .../limiterhelper/grpc/grpclimiter.go | 8 +- .../limiterhelper/http/httplimiter.go | 8 +- .../limiterhelper/middleware.go | 44 ++++--- receiver/otlpreceiver/config.go | 4 + receiver/otlpreceiver/factory.go | 17 ++- receiver/otlpreceiver/otlp.go | 75 ++--------- receiver/receiver.go | 120 ++++++++++-------- receiver/xreceiver/profiles.go | 61 +++++---- 14 files changed, 271 insertions(+), 223 deletions(-) rename extension/extensionlimiter/limiterhelper/{consumer.go => consumerlimiter/consumerlimiter.go} (62%) diff --git a/component/component.go b/component/component.go index 5a32c5041d7..fd9cdc1bf6a 100644 --- a/component/component.go +++ b/component/component.go @@ -110,6 +110,10 @@ const ( StabilityLevelStable ) +func (sl *StabilityLevel) Self() StabilityLevel { + return *sl +} + func (sl *StabilityLevel) UnmarshalText(in []byte) error { str := strings.ToLower(string(in)) switch str { @@ -194,3 +198,17 @@ type CreateDefaultConfigFunc func() Config func (f CreateDefaultConfigFunc) CreateDefaultConfig() Config { return f() } + +type factoryImpl struct { + TypeFunc + CreateDefaultConfigFunc +} + +var _ Factory = factoryImpl{} + +func NewFactoryImpl(tf TypeFunc, cf CreateDefaultConfigFunc) Factory { + return factoryImpl{ + TypeFunc: tf, + CreateDefaultConfigFunc: cf, + } +} diff --git a/component/identifiable.go b/component/identifiable.go index 6b814768161..11948f20a4f 100644 --- a/component/identifiable.go +++ b/component/identifiable.go @@ -38,6 +38,11 @@ func (t Type) String() string { return t.name } +// Self returns itself. +func (t Type) Self() Type { + return t +} + // MarshalText marshals returns the Type name. func (t Type) MarshalText() ([]byte, error) { return []byte(t.name), nil @@ -71,6 +76,16 @@ func MustNewType(strType string) Type { return ty } +// TypeFunc is ... +type TypeFunc func() Type + +// Type gets the type of the component created by this factory. +func (f TypeFunc) Type() Type { + if f == nil { + } + return f() +} + // ID represents the identity for a component. It combines two values: // * type - the Type of the component. // * name - the name of that component. diff --git a/config/configmiddleware/configmiddleware.go b/config/configmiddleware/configmiddleware.go index bc10730ba1a..3eb786cf2eb 100644 --- a/config/configmiddleware/configmiddleware.go +++ b/config/configmiddleware/configmiddleware.go @@ -30,12 +30,7 @@ var ( ) // Middleware defines the extension ID for a middleware component. -type Config struct { - // ID specifies the name of the extension to use. - ID component.ID `mapstructure:"id,omitempty"` - // prevent unkeyed literal initialization - _ struct{} -} +type Config component.ID func resolveFailed(id component.ID) error { return fmt.Errorf("failed to resolve middleware %q: %w", id, errMiddlewareNotFound) @@ -47,7 +42,7 @@ func resolveFailed(id component.ID) error { // found, an error is returned. This should only be used by HTTP // clients. func (m Config) GetHTTPClientRoundTripper(_ context.Context, extensions map[component.ID]component.Component) (func(http.RoundTripper) (http.RoundTripper, error), error) { - if ext, found := extensions[m.ID]; found { + if ext, found := extensions[component.ID(m)]; found { if client, ok := ext.(extensionmiddleware.HTTPClient); ok { return client.GetHTTPRoundTripper, nil } @@ -60,7 +55,7 @@ func (m Config) GetHTTPClientRoundTripper(_ context.Context, extensions map[comp } return nil, errNotHTTPClient } - return nil, resolveFailed(m.ID) + return nil, resolveFailed(component.ID(m)) } // GetHTTPServerHandler attempts to select the appropriate @@ -69,7 +64,7 @@ func (m Config) GetHTTPClientRoundTripper(_ context.Context, extensions map[comp // found, an error is returned. This should only be used by HTTP // servers. func (m Config) GetHTTPServerHandler(_ context.Context, extensions map[component.ID]component.Component) (func(http.Handler) (http.Handler, error), error) { - if ext, found := extensions[m.ID]; found { + if ext, found := extensions[component.ID(m)]; found { if server, ok := ext.(extensionmiddleware.HTTPServer); ok { return server.GetHTTPHandler, nil } @@ -83,7 +78,7 @@ func (m Config) GetHTTPServerHandler(_ context.Context, extensions map[component return nil, errNotHTTPServer } - return nil, resolveFailed(m.ID) + return nil, resolveFailed(component.ID(m)) } // GetGRPCClientOptions attempts to select the appropriate @@ -91,7 +86,7 @@ func (m Config) GetHTTPServerHandler(_ context.Context, extensions map[component // returns the gRPC dial options. If a middleware is not found, an // error is returned. This should only be used by gRPC clients. func (m Config) GetGRPCClientOptions(_ context.Context, extensions map[component.ID]component.Component) ([]grpc.DialOption, error) { - if ext, found := extensions[m.ID]; found { + if ext, found := extensions[component.ID(m)]; found { if client, ok := ext.(extensionmiddleware.GRPCClient); ok { return client.GetGRPCClientOptions() } @@ -104,7 +99,7 @@ func (m Config) GetGRPCClientOptions(_ context.Context, extensions map[component } return nil, errNotGRPCClient } - return nil, resolveFailed(m.ID) + return nil, resolveFailed(component.ID(m)) } // GetGRPCServerOptions attempts to select the appropriate @@ -112,7 +107,7 @@ func (m Config) GetGRPCClientOptions(_ context.Context, extensions map[component // returns the gRPC server options. If a middleware is not found, an // error is returned. This should only be used by gRPC servers. func (m Config) GetGRPCServerOptions(_ context.Context, extensions map[component.ID]component.Component) ([]grpc.ServerOption, error) { - if ext, found := extensions[m.ID]; found { + if ext, found := extensions[component.ID(m)]; found { if server, ok := ext.(extensionmiddleware.GRPCServer); ok { return server.GetGRPCServerOptions() } @@ -126,7 +121,7 @@ func (m Config) GetGRPCServerOptions(_ context.Context, extensions map[component return nil, errNotGRPCServer } - return nil, resolveFailed(m.ID) + return nil, resolveFailed(component.ID(m)) } // GetBaseLimiters gets a list of basic limiters. These can be @@ -137,9 +132,9 @@ func GetBaseLimiters(host component.Host, cfgs []Config) ([]extensionlimiter.Any var lims []extensionlimiter.AnyProvider all := host.GetExtensions() for _, m := range cfgs { - ext, ok := all[m.ID] + ext, ok := all[component.ID(m)] if !ok { - err = multierr.Append(err, resolveFailed(m.ID)) + err = multierr.Append(err, resolveFailed(component.ID(m))) continue } if lim, ok := ext.(extensionlimiter.AnyProvider); ok { diff --git a/extension/extensionlimiter/any_limiter.go b/extension/extensionlimiter/any_limiter.go index a2145477dd3..fdbef709395 100644 --- a/extension/extensionlimiter/any_limiter.go +++ b/extension/extensionlimiter/any_limiter.go @@ -7,13 +7,13 @@ package extensionlimiter // import "go.opentelemetry.io/collector/extension/exte // limiter interfaces. This serves as a marker for implementations // which provider rate limiters of any (one)kind. type AnyProvider interface { - // recognizedProvider may be embedded using an AnyProviderImpl. - recognizedProvider() + // unexportedProviderFunc may be embedded using an AnyProviderImpl. + unexportedProviderFunc() } // AnyProviderImpl can be embedded as a marker that a component // implements one of the rate limiters. type AnyProviderImpl struct {} -func (AnyProviderImpl) recognizedProvider() {} +func (AnyProviderImpl) unexportedProviderFunc() {} diff --git a/extension/extensionlimiter/go.mod b/extension/extensionlimiter/go.mod index 1200678ea51..85269117471 100644 --- a/extension/extensionlimiter/go.mod +++ b/extension/extensionlimiter/go.mod @@ -63,3 +63,9 @@ replace go.opentelemetry.io/collector/extension/extensionmiddleware/extensionmid replace go.opentelemetry.io/collector/extension => ../ replace go.opentelemetry.io/collector/internal/telemetry => ../../internal/telemetry + +replace go.opentelemetry.io/collector/config/configmiddleware => ../../config/configmiddleware + +replace go.opentelemetry.io/collector/receiver => ../../receiver + +replace go.opentelemetry.io/collector/receiver/xreceiver => ../../receiver/xreceiver diff --git a/extension/extensionlimiter/limiterhelper/consumer.go b/extension/extensionlimiter/limiterhelper/consumerlimiter/consumerlimiter.go similarity index 62% rename from extension/extensionlimiter/limiterhelper/consumer.go rename to extension/extensionlimiter/limiterhelper/consumerlimiter/consumerlimiter.go index 4abdfed6af7..0d247ffb7b2 100644 --- a/extension/extensionlimiter/limiterhelper/consumer.go +++ b/extension/extensionlimiter/limiterhelper/consumerlimiter/consumerlimiter.go @@ -1,7 +1,7 @@ // Copyright The OpenTelemetry Authors // SPDX-License-Identifier: Apache-2.0 -package limiterhelper // import "go.opentelemetry.io/collector/extension/extensionlimiter/limiterhelper" +package consumerlimiter // import "go.opentelemetry.io/collector/extension/extensionlimiter/limiterhelper/consumerlimiter" import ( "context" @@ -9,15 +9,27 @@ import ( "go.uber.org/multierr" + "go.opentelemetry.io/collector/component" "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/pdata/plog" + "go.opentelemetry.io/collector/receiver" + "go.opentelemetry.io/collector/receiver/xreceiver" "go.opentelemetry.io/collector/pdata/pmetric" "go.opentelemetry.io/collector/pdata/pprofile" "go.opentelemetry.io/collector/pdata/ptrace" ) +// Config is the standard pipeline configuration for limiting a +// consumer interface by specific signal. +type Config struct { + RequestCount component.ID `mapstructure:"request_count"` + RequestItems component.ID `mapstructure:"request_items"` + RequestBytes component.ID `mapstructure:"request_bytes"` +} + // Traits object interface is generalized by P the pipeline data type // (e.g., ptrace.Traces) and C the consumer type (e.g., // consumer.Traces) @@ -121,7 +133,7 @@ func (profileTraits) consume(ctx context.Context, data pprofile.Profiles, next x func limitOne[P any, C any]( next C, keys []extensionlimiter.WeightKey, - provider WrapperProvider, + provider limiterhelper.WrapperProvider, m traits[P, C], key extensionlimiter.WeightKey, opts []consumer.Option, @@ -148,7 +160,7 @@ func limitOne[P any, C any]( func newLimited[P any, C any]( next C, keys []extensionlimiter.WeightKey, - provider WrapperProvider, + provider limiterhelper.WrapperProvider, m traits[P, C], opts ...consumer.Option, ) (C, error) { @@ -172,26 +184,49 @@ func newLimited[P any, C any]( return next, multierr.Append(err1, multierr.Append(err2, err3)) } -// NewLimitedTraces applies a limiter using the provider over keys before calling next. -func NewLimitedTraces(next consumer.Traces, keys []extensionlimiter.WeightKey, provider WrapperProvider) (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 WrapperProvider) (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 WrapperProvider) (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 WrapperProvider) (xconsumer.Profiles, error) { - return newLimited(next, keys, provider, profileTraits{}, - consumer.WithCapabilities(next.Capabilities())) +// // NewLimitedTraces applies a limiter using the provider over keys before calling next. +// func NewLimitedTraces(next consumer.Traces, keys []extensionlimiter.WeightKey, provider limiterhelper.WrapperProvider) (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 limiterhelper.WrapperProvider) (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 limiterhelper.WrapperProvider) (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 limiterhelper.WrapperProvider) (xconsumer.Profiles, error) { +// return newLimited(next, keys, provider, profileTraits{}, +// consumer.WithCapabilities(next.Capabilities())) +// } + +// type stabilityFunc func() component.StabilityLevel + +// // TracesStability gets the stability level of the Traces receiver. +// TracesStability() component.StabilityLevel +// // MetricsStability gets the stability level of the Metrics receiver. +// MetricsStability() component.StabilityLevel + + +func NewLimitedFactory(fact xreceiver.Factory) xreceiver.Factory { + return configureLimits{ + TypeFunc: fact.Type, + CreateDefaultConfigFunc: fact.CreateDefaultConfig, + TracesStabilityFunc: fact.TracesStability, + MetricsStabilityFunc: fact.MetricsStability, + LogsStabilityFunc: fact.LogsStability, + ProfilesStabilityFunc: fact.ProfilesStability, + CreateTracesFunc: fact.CreateTraces, + CreateMetricsFunc: fact.CreateMetrics, + CreateLogsFunc: fact.CreateLogs, + CreateProfilesFunc: fact.CreateProfiles, + } } diff --git a/extension/extensionlimiter/limiterhelper/grpc/grpclimiter.go b/extension/extensionlimiter/limiterhelper/grpc/grpclimiter.go index 7feeb00310b..8119eb36237 100644 --- a/extension/extensionlimiter/limiterhelper/grpc/grpclimiter.go +++ b/extension/extensionlimiter/limiterhelper/grpc/grpclimiter.go @@ -50,8 +50,8 @@ func setRateLimiterError(ctx context.Context, err error) { } func NewClientLimiter(ext extensionlimiter.AnyProvider) (extensionmiddleware.GRPCClient, error) { - wp, err1 := limiterhelper.MiddlewareToWrapperProvider(ext) - rp, err2 := limiterhelper.MiddlewareToRateLimiterProvider(ext) + wp, err1 := limiterhelper.AnyToWrapperProvider(ext) + rp, err2 := limiterhelper.AnyToRateLimiterProvider(ext) if err := multierr.Append(err1, err2); err != nil { return nil, err } @@ -111,8 +111,8 @@ func NewClientLimiter(ext extensionlimiter.AnyProvider) (extensionmiddleware.GRP } func NewServerLimiter(ext extensionlimiter.AnyProvider) (extensionmiddleware.GRPCServer, error) { - wp, err1 := limiterhelper.MiddlewareToWrapperProvider(ext) - rp, err2 := limiterhelper.MiddlewareToRateLimiterProvider(ext) + wp, err1 := limiterhelper.AnyToWrapperProvider(ext) + rp, err2 := limiterhelper.AnyToRateLimiterProvider(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 f0b9aced8b0..d3a736fac25 100644 --- a/extension/extensionlimiter/limiterhelper/http/httplimiter.go +++ b/extension/extensionlimiter/limiterhelper/http/httplimiter.go @@ -17,8 +17,8 @@ import ( ) func NewClientLimiter(ext extensionlimiter.AnyProvider) (extensionmiddleware.HTTPClient, error) { - wp, err1 := limiterhelper.MiddlewareToWrapperProvider(ext) - rp, err2 := limiterhelper.MiddlewareToRateLimiterProvider(ext) + wp, err1 := limiterhelper.AnyToWrapperProvider(ext) + rp, err2 := limiterhelper.AnyToRateLimiterProvider(ext) if err := multierr.Append(err1, err2); err != nil { return nil, err } @@ -87,8 +87,8 @@ func (rb *rateLimitedBody) Close() error { } func NewServerLimiter(ext extensionlimiter.AnyProvider) (extensionmiddleware.HTTPServer, error) { - wp, err1 := limiterhelper.MiddlewareToWrapperProvider(ext) - rp, err2 := limiterhelper.MiddlewareToRateLimiterProvider(ext) + wp, err1 := limiterhelper.AnyToWrapperProvider(ext) + rp, err2 := limiterhelper.AnyToRateLimiterProvider(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 8ac10ca8caa..a6258607d2e 100644 --- a/extension/extensionlimiter/limiterhelper/middleware.go +++ b/extension/extensionlimiter/limiterhelper/middleware.go @@ -15,49 +15,49 @@ var ( ErrLimiterConflict = errors.New("limiter implements both rate and resource-limiters") ) -// MiddlewareToWrapperProvider 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 MiddlewareToWrapperProvider(ext extensionlimiter.AnyProvider) (WrapperProvider, error) { - return getMiddleware( +// AnyToWrapperProvider 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 AnyToWrapperProvider(ext extensionlimiter.AnyProvider) (WrapperProvider, error) { + return getAny( ext, nilError(RateToWrapperProvider), nilError(ResourceToWrapperProvider), ) } -// 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 +// AnyToRateLimiterProvider 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(ext extensionlimiter.AnyProvider) (extensionlimiter.RateLimiterProvider, error) { - return getMiddleware( +func AnyToRateLimiterProvider(ext extensionlimiter.AnyProvider) (extensionlimiter.RateLimiterProvider, error) { + return getAny( ext, identity[extensionlimiter.RateLimiterProvider], resourceToRateLimiterError, ) } -// 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 +// AnyToResourceLimiterProvider 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(ext extensionlimiter.AnyProvider) (extensionlimiter.ResourceLimiterProvider, error) { - return getMiddleware( +func AnyToResourceLimiterProvider(ext extensionlimiter.AnyProvider) (extensionlimiter.ResourceLimiterProvider, error) { + return getAny( ext, 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]( +// getAny invokes getProvider if any kind of limiter is detected for +// the given host and middleware configuration. +func getAny[Out any]( ext extensionlimiter.AnyProvider, rate func(extensionlimiter.RateLimiterProvider) (Out, error), resource func(extensionlimiter.ResourceLimiterProvider) (Out, error), @@ -77,14 +77,16 @@ func getMiddleware[Out any]( return out, ErrNotALimiter } -// identity is a pass-through for the correct provider type. +// identity is a pass-through for the matching provider type. func identity[T any](lim T) (T, error) { return lim, 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 } + return func(s S) (T, error) { + return f(s), nil + } } // resourceToRateLimiterError represents the impossible conversion diff --git a/receiver/otlpreceiver/config.go b/receiver/otlpreceiver/config.go index 801595efc2a..9d20b33c706 100644 --- a/receiver/otlpreceiver/config.go +++ b/receiver/otlpreceiver/config.go @@ -14,6 +14,7 @@ import ( "go.opentelemetry.io/collector/config/configgrpc" "go.opentelemetry.io/collector/config/confighttp" "go.opentelemetry.io/collector/config/configoptional" + "go.opentelemetry.io/collector/extension/extensionlimiter/limiterhelper/consumerlimiter" ) type SanitizedURLPath string @@ -62,6 +63,9 @@ type Protocols struct { type Config struct { // Protocols is the configuration for the supported protocols, currently gRPC and HTTP (Proto and JSON). Protocols `mapstructure:"protocols"` + + // Limiters allows applying limiter extensions for request count, items, and bytes. + Limiters consumerlimiter.Config `mapstructure:"limiters"` } var _ component.Config = (*Config)(nil) diff --git a/receiver/otlpreceiver/factory.go b/receiver/otlpreceiver/factory.go index 6564ea4e8f0..ae41051c696 100644 --- a/receiver/otlpreceiver/factory.go +++ b/receiver/otlpreceiver/factory.go @@ -17,6 +17,7 @@ import ( "go.opentelemetry.io/collector/receiver" "go.opentelemetry.io/collector/receiver/otlpreceiver/internal/metadata" "go.opentelemetry.io/collector/receiver/xreceiver" + "go.opentelemetry.io/collector/extension/extensionlimiter/limiterhelper/consumerlimiter" ) const ( @@ -28,13 +29,15 @@ const ( // NewFactory creates a new OTLP receiver factory. func NewFactory() receiver.Factory { - return xreceiver.NewFactory( - metadata.Type, - createDefaultConfig, - xreceiver.WithTraces(createTraces, metadata.TracesStability), - xreceiver.WithMetrics(createMetrics, metadata.MetricsStability), - xreceiver.WithLogs(createLog, metadata.LogsStability), - xreceiver.WithProfiles(createProfiles, metadata.ProfilesStability), + return consumerlimiter.NewLimitedFactory( + xreceiver.NewFactory( + metadata.Type, + createDefaultConfig, + xreceiver.WithTraces(createTraces, metadata.TracesStability), + xreceiver.WithMetrics(createMetrics, metadata.MetricsStability), + xreceiver.WithLogs(createLog, metadata.LogsStability), + xreceiver.WithProfiles(createProfiles, metadata.ProfilesStability), + ), ) } diff --git a/receiver/otlpreceiver/otlp.go b/receiver/otlpreceiver/otlp.go index 06460e50f97..03e7e409e8b 100644 --- a/receiver/otlpreceiver/otlp.go +++ b/receiver/otlpreceiver/otlp.go @@ -16,11 +16,8 @@ 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" - "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" @@ -101,50 +98,20 @@ func (r *otlpReceiver) startGRPCServer(host component.Host) error { return err } - limitKeys := extensionlimiter.StandardNotMiddlewareKeys() - limiters, err := configmiddleware.GetBaseLimiters(host, r.cfg.GRPC.Middlewares) - if err != nil { - return err - } - limiterProvider, err := limiterhelper.MultipleProvider(limiters) - if err != nil { - return err - } - if r.nextTraces != nil { - var next consumer.Traces - next, err = limiterhelper.NewLimitedTraces(r.nextTraces, limitKeys, limiterProvider) - if err != nil { - return err - } - ptraceotlp.RegisterGRPCServer(r.serverGRPC, trace.New(next, r.obsrepGRPC)) + ptraceotlp.RegisterGRPCServer(r.serverGRPC, trace.New(r.nextTraces, r.obsrepGRPC)) } if r.nextMetrics != nil { - var next consumer.Metrics - next, err = limiterhelper.NewLimitedMetrics(r.nextMetrics, limitKeys, limiterProvider) - if err != nil { - return err - } - pmetricotlp.RegisterGRPCServer(r.serverGRPC, metrics.New(next, r.obsrepGRPC)) + pmetricotlp.RegisterGRPCServer(r.serverGRPC, metrics.New(r.nextMetrics, r.obsrepGRPC)) } if r.nextLogs != nil { - var next consumer.Logs - next, err = limiterhelper.NewLimitedLogs(r.nextLogs, limitKeys, limiterProvider) - if err != nil { - return err - } - plogotlp.RegisterGRPCServer(r.serverGRPC, logs.New(next, r.obsrepGRPC)) + plogotlp.RegisterGRPCServer(r.serverGRPC, logs.New(r.nextLogs, r.obsrepGRPC)) } if r.nextProfiles != nil { - var next xconsumer.Profiles - next, err = limiterhelper.NewLimitedProfiles(r.nextProfiles, limitKeys, limiterProvider) - if err != nil { - return err - } - pprofileotlp.RegisterGRPCServer(r.serverGRPC, profiles.New(next)) + pprofileotlp.RegisterGRPCServer(r.serverGRPC, profiles.New(r.nextProfiles)) } r.settings.Logger.Info("Starting GRPC server", zap.String("endpoint", grpcCfg.NetAddr.Endpoint)) @@ -170,57 +137,31 @@ func (r *otlpReceiver) startHTTPServer(ctx context.Context, host component.Host) return nil } - limitKeys := extensionlimiter.StandardNotMiddlewareKeys() - 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 - } - httpCfg := r.cfg.HTTP.Get() httpMux := http.NewServeMux() if r.nextTraces != nil { - next, err := limiterhelper.NewLimitedTraces(r.nextTraces, limitKeys, limiterProvider) - if err != nil { - return err - } - httpTracesReceiver := trace.New(next, r.obsrepHTTP) + httpTracesReceiver := trace.New(r.nextTraces, r.obsrepHTTP) httpMux.HandleFunc(string(httpCfg.TracesURLPath), func(resp http.ResponseWriter, req *http.Request) { handleTraces(resp, req, httpTracesReceiver) }) } if r.nextMetrics != nil { - next, err := limiterhelper.NewLimitedMetrics(r.nextMetrics, limitKeys, limiterProvider) - if err != nil { - return err - } - httpMetricsReceiver := metrics.New(next, r.obsrepHTTP) + httpMetricsReceiver := metrics.New(r.nextMetrics, r.obsrepHTTP) httpMux.HandleFunc(string(httpCfg.MetricsURLPath), func(resp http.ResponseWriter, req *http.Request) { handleMetrics(resp, req, httpMetricsReceiver) }) } if r.nextLogs != nil { - next, err := limiterhelper.NewLimitedLogs(r.nextLogs, limitKeys, limiterProvider) - if err != nil { - return err - } - httpLogsReceiver := logs.New(next, r.obsrepHTTP) + httpLogsReceiver := logs.New(r.nextLogs, r.obsrepHTTP) httpMux.HandleFunc(string(httpCfg.LogsURLPath), func(resp http.ResponseWriter, req *http.Request) { handleLogs(resp, req, httpLogsReceiver) }) } if r.nextProfiles != nil { - next, err := limiterhelper.NewLimitedProfiles(r.nextProfiles, limitKeys, limiterProvider) - if err != nil { - return err - } - httpProfilesReceiver := profiles.New(next) + httpProfilesReceiver := profiles.New(r.nextProfiles) httpMux.HandleFunc(defaultProfilesURLPath, func(resp http.ResponseWriter, req *http.Request) { handleProfiles(resp, req, httpProfilesReceiver) }) diff --git a/receiver/receiver.go b/receiver/receiver.go index f2aa1fe2c58..4d58dd49ba4 100644 --- a/receiver/receiver.go +++ b/receiver/receiver.go @@ -93,14 +93,14 @@ type Factory interface { // FactoryOption apply changes to Factory. type FactoryOption interface { // applyOption applies the option. - applyOption(o *factory) + applyOption(o *factory, cfgType component.Type) } // factoryOptionFunc is an FactoryOption created through a function. -type factoryOptionFunc func(*factory) +type factoryOptionFunc func(*factory, component.Type) -func (f factoryOptionFunc) applyOption(o *factory) { - f(o) +func (f factoryOptionFunc) applyOption(o *factory, cfgType component.Type) { + f(o, cfgType) } // CreateTracesFunc is the equivalent of Factory.CreateTraces. @@ -112,103 +112,117 @@ type CreateMetricsFunc func(context.Context, Settings, component.Config, consume // CreateLogsFunc is the equivalent of Factory.CreateLogs. type CreateLogsFunc func(context.Context, Settings, component.Config, consumer.Logs) (Logs, error) +// TracesStabilityFunc is a functional way to construct Factory implementations. +type TracesStabilityFunc func() component.StabilityLevel + +// MetricsStabilityFunc is a functional way to construct Factory implementations. +type MetricsStabilityFunc func() component.StabilityLevel + +// LogsStabilityFunc is a functional way to construct Factory implementations. +type LogsStabilityFunc func() component.StabilityLevel + type factory struct { - cfgType component.Type + component.TypeFunc component.CreateDefaultConfigFunc - createTracesFunc CreateTracesFunc - tracesStabilityLevel component.StabilityLevel - createMetricsFunc CreateMetricsFunc - metricsStabilityLevel component.StabilityLevel - createLogsFunc CreateLogsFunc - logsStabilityLevel component.StabilityLevel -} - -func (f *factory) Type() component.Type { - return f.cfgType + CreateTracesFunc + TracesStabilityFunc + CreateMetricsFunc + MetricsStabilityFunc + CreateLogsFunc + LogsStabilityFunc } func (f *factory) unexportedFactoryFunc() {} -func (f *factory) TracesStability() component.StabilityLevel { - return f.tracesStabilityLevel -} - -func (f *factory) MetricsStability() component.StabilityLevel { - return f.metricsStabilityLevel +func (f TracesStabilityFunc) TracesStability() component.StabilityLevel { + if f == nil { + return component.StabilityLevelUndefined + } + return f() } -func (f *factory) LogsStability() component.StabilityLevel { - return f.logsStabilityLevel +func (f MetricsStabilityFunc) MetricsStability() component.StabilityLevel { + if f == nil { + return component.StabilityLevelUndefined + } + return f() } -func (f *factory) CreateTraces(ctx context.Context, set Settings, cfg component.Config, next consumer.Traces) (Traces, error) { - if f.createTracesFunc == nil { - return nil, pipeline.ErrSignalNotSupported +func (f LogsStabilityFunc) LogsStability() component.StabilityLevel { + if f == nil { + return component.StabilityLevelUndefined } + return f() +} - if set.ID.Type() != f.Type() { - return nil, internal.ErrIDMismatch(set.ID, f.Type()) - } +type Creator[A, B any] func(ctx context.Context, set Settings, cfg component.Config, next A) (B, error) - return f.createTracesFunc(ctx, set, cfg, next) +func typeChecked[A, B any](cf Creator[A, B], cfgType component.Type) Creator[A, B] { + return func(ctx context.Context, set Settings, cfg component.Config, next A) (B, error) { + if set.ID.Type() != cfgType { + var zero B + return zero, internal.ErrIDMismatch(set.ID, cfgType) + } + return cf(ctx, set, cfg, next) + } } -func (f *factory) CreateMetrics(ctx context.Context, set Settings, cfg component.Config, next consumer.Metrics) (Metrics, error) { - if f.createMetricsFunc == nil { +func (f CreateTracesFunc) CreateTraces(ctx context.Context, set Settings, cfg component.Config, next consumer.Traces) (Traces, error) { + if f == nil { return nil, pipeline.ErrSignalNotSupported } - if set.ID.Type() != f.Type() { - return nil, internal.ErrIDMismatch(set.ID, f.Type()) - } - - return f.createMetricsFunc(ctx, set, cfg, next) + return f(ctx, set, cfg, next) } -func (f *factory) CreateLogs(ctx context.Context, set Settings, cfg component.Config, next consumer.Logs) (Logs, error) { - if f.createLogsFunc == nil { +func (f CreateMetricsFunc) CreateMetrics(ctx context.Context, set Settings, cfg component.Config, next consumer.Metrics) (Metrics, error) { + if f == nil { return nil, pipeline.ErrSignalNotSupported } - if set.ID.Type() != f.Type() { - return nil, internal.ErrIDMismatch(set.ID, f.Type()) + return f(ctx, set, cfg, next) +} + +func (f CreateLogsFunc) CreateLogs(ctx context.Context, set Settings, cfg component.Config, next consumer.Logs) (Logs, error) { + if f == nil { + return nil, pipeline.ErrSignalNotSupported } - return f.createLogsFunc(ctx, set, cfg, next) + return f(ctx, set, cfg, next) } // WithTraces overrides the default "error not supported" implementation for Factory.CreateTraces and the default "undefined" stability level. func WithTraces(createTraces CreateTracesFunc, sl component.StabilityLevel) FactoryOption { - return factoryOptionFunc(func(o *factory) { - o.tracesStabilityLevel = sl - o.createTracesFunc = createTraces + return factoryOptionFunc(func(o *factory, cfgType component.Type) { + o.TracesStabilityFunc = sl.Self + o.CreateTracesFunc = CreateTracesFunc(typeChecked[consumer.Traces, Traces](Creator[consumer.Traces, Traces](createTraces), cfgType)) }) } // WithMetrics overrides the default "error not supported" implementation for Factory.CreateMetrics and the default "undefined" stability level. func WithMetrics(createMetrics CreateMetricsFunc, sl component.StabilityLevel) FactoryOption { - return factoryOptionFunc(func(o *factory) { - o.metricsStabilityLevel = sl - o.createMetricsFunc = createMetrics + return factoryOptionFunc(func(o *factory, cfgType component.Type) { + o.MetricsStabilityFunc = sl.Self + o.CreateMetricsFunc = CreateMetricsFunc(typeChecked[consumer.Metrics, Metrics](Creator[consumer.Metrics, Metrics](createMetrics), cfgType)) }) } // WithLogs overrides the default "error not supported" implementation for Factory.CreateLogs and the default "undefined" stability level. func WithLogs(createLogs CreateLogsFunc, sl component.StabilityLevel) FactoryOption { - return factoryOptionFunc(func(o *factory) { - o.logsStabilityLevel = sl - o.createLogsFunc = createLogs + return factoryOptionFunc(func(o *factory, cfgType component.Type) { + o.LogsStabilityFunc = sl.Self + o.CreateLogsFunc = CreateLogsFunc(typeChecked[consumer.Logs, Logs](Creator[consumer.Logs, Logs](createLogs), cfgType)) }) } // NewFactory returns a Factory. func NewFactory(cfgType component.Type, createDefaultConfig component.CreateDefaultConfigFunc, options ...FactoryOption) Factory { f := &factory{ - cfgType: cfgType, + TypeFunc: cfgType.Self, CreateDefaultConfigFunc: createDefaultConfig, } for _, opt := range options { - opt.applyOption(f) + opt.applyOption(f, cfgType) } return f } diff --git a/receiver/xreceiver/profiles.go b/receiver/xreceiver/profiles.go index 8a912119ab2..8a6fc9e8aaa 100644 --- a/receiver/xreceiver/profiles.go +++ b/receiver/xreceiver/profiles.go @@ -41,37 +41,40 @@ type Factory interface { // CreateProfilesFunc is the equivalent of Factory.CreateProfiles. type CreateProfilesFunc func(context.Context, receiver.Settings, component.Config, xconsumer.Profiles) (Profiles, error) +// ProfilesStabilityFunc is a functional way to construct Factory implementations. +type ProfilesStabilityFunc func() component.StabilityLevel + +func (f ProfilesStabilityFunc) ProfilesStability() component.StabilityLevel { + if f == nil { + return component.StabilityLevelUndefined + } + return f() +} + // FactoryOption apply changes to Factory. type FactoryOption interface { // applyOption applies the option. - applyOption(o *factoryOpts) + applyOption(o *factoryOpts, cfgType component.Type) } // factoryOptionFunc is a FactoryOption created through a function. -type factoryOptionFunc func(*factoryOpts) +type factoryOptionFunc func(*factoryOpts, component.Type) -func (f factoryOptionFunc) applyOption(o *factoryOpts) { - f(o) +func (f factoryOptionFunc) applyOption(o *factoryOpts, cfgType component.Type) { + f(o, cfgType) } type factory struct { receiver.Factory - createProfilesFunc CreateProfilesFunc - profilesStabilityLevel component.StabilityLevel -} - -func (f *factory) ProfilesStability() component.StabilityLevel { - return f.profilesStabilityLevel + CreateProfilesFunc + ProfilesStabilityFunc } -func (f *factory) CreateProfiles(ctx context.Context, set receiver.Settings, cfg component.Config, next xconsumer.Profiles) (Profiles, error) { - if f.createProfilesFunc == nil { +func (f CreateProfilesFunc) CreateProfiles(ctx context.Context, set receiver.Settings, cfg component.Config, next xconsumer.Profiles) (Profiles, error) { + if f == nil { return nil, pipeline.ErrSignalNotSupported } - if set.ID.Type() != f.Type() { - return nil, internal.ErrIDMismatch(set.ID, f.Type()) - } - return f.createProfilesFunc(ctx, set, cfg, next) + return f(ctx, set, cfg, next) } type factoryOpts struct { @@ -79,32 +82,44 @@ type factoryOpts struct { *factory } +type Creator[A, B any] func(ctx context.Context, set receiver.Settings, cfg component.Config, next A) (B, error) + +func typeChecked[A, B any](cf Creator[A, B], cfgType component.Type) Creator[A, B] { + return func(ctx context.Context, set receiver.Settings, cfg component.Config, next A) (B, error) { + if set.ID.Type() != cfgType { + var zero B + return zero, internal.ErrIDMismatch(set.ID, cfgType) + } + return cf(ctx, set, cfg, next) + } +} + // WithTraces overrides the default "error not supported" implementation for Factory.CreateTraces and the default "undefined" stability level. func WithTraces(createTraces receiver.CreateTracesFunc, sl component.StabilityLevel) FactoryOption { - return factoryOptionFunc(func(o *factoryOpts) { + return factoryOptionFunc(func(o *factoryOpts, cfgType component.Type) { o.opts = append(o.opts, receiver.WithTraces(createTraces, sl)) }) } // WithMetrics overrides the default "error not supported" implementation for Factory.CreateMetrics and the default "undefined" stability level. func WithMetrics(createMetrics receiver.CreateMetricsFunc, sl component.StabilityLevel) FactoryOption { - return factoryOptionFunc(func(o *factoryOpts) { + return factoryOptionFunc(func(o *factoryOpts, cfgType component.Type) { o.opts = append(o.opts, receiver.WithMetrics(createMetrics, sl)) }) } // WithLogs overrides the default "error not supported" implementation for Factory.CreateLogs and the default "undefined" stability level. func WithLogs(createLogs receiver.CreateLogsFunc, sl component.StabilityLevel) FactoryOption { - return factoryOptionFunc(func(o *factoryOpts) { + return factoryOptionFunc(func(o *factoryOpts, cfgType component.Type) { o.opts = append(o.opts, receiver.WithLogs(createLogs, sl)) }) } // WithProfiles overrides the default "error not supported" implementation for Factory.CreateProfiles and the default "undefined" stability level. func WithProfiles(createProfiles CreateProfilesFunc, sl component.StabilityLevel) FactoryOption { - return factoryOptionFunc(func(o *factoryOpts) { - o.profilesStabilityLevel = sl - o.createProfilesFunc = createProfiles + return factoryOptionFunc(func(o *factoryOpts, cfgType component.Type) { + o.ProfilesStabilityFunc = sl.Self + o.CreateProfilesFunc = CreateProfilesFunc(typeChecked[xconsumer.Profiles, Profiles](Creator[xconsumer.Profiles, Profiles](createProfiles), cfgType)) }) } @@ -112,7 +127,7 @@ func WithProfiles(createProfiles CreateProfilesFunc, sl component.StabilityLevel func NewFactory(cfgType component.Type, createDefaultConfig component.CreateDefaultConfigFunc, options ...FactoryOption) Factory { opts := factoryOpts{factory: &factory{}} for _, opt := range options { - opt.applyOption(&opts) + opt.applyOption(&opts, cfgType) } opts.Factory = receiver.NewFactory(cfgType, createDefaultConfig, opts.opts...) return opts.factory From e63f1d3014eaa2e963774f3c85c8e4a36ceaeac5 Mon Sep 17 00:00:00 2001 From: Joshua MacDonald Date: Wed, 18 Jun 2025 14:12:11 -0700 Subject: [PATCH 52/62] yess --- .../consumerlimiter/consumerlimiter.go | 33 ++++++------ receiver/receiver.go | 52 +++++++++++++------ receiver/xreceiver/profiles.go | 22 ++++++-- 3 files changed, 72 insertions(+), 35 deletions(-) diff --git a/extension/extensionlimiter/limiterhelper/consumerlimiter/consumerlimiter.go b/extension/extensionlimiter/limiterhelper/consumerlimiter/consumerlimiter.go index 0d247ffb7b2..6f1e82c58a9 100644 --- a/extension/extensionlimiter/limiterhelper/consumerlimiter/consumerlimiter.go +++ b/extension/extensionlimiter/limiterhelper/consumerlimiter/consumerlimiter.go @@ -15,11 +15,11 @@ import ( "go.opentelemetry.io/collector/extension/extensionlimiter" "go.opentelemetry.io/collector/extension/extensionlimiter/limiterhelper" "go.opentelemetry.io/collector/pdata/plog" - "go.opentelemetry.io/collector/receiver" - "go.opentelemetry.io/collector/receiver/xreceiver" "go.opentelemetry.io/collector/pdata/pmetric" "go.opentelemetry.io/collector/pdata/pprofile" "go.opentelemetry.io/collector/pdata/ptrace" + "go.opentelemetry.io/collector/receiver" + "go.opentelemetry.io/collector/receiver/xreceiver" ) // Config is the standard pipeline configuration for limiting a @@ -215,18 +215,21 @@ func newLimited[P any, C any]( // // MetricsStability gets the stability level of the Metrics receiver. // MetricsStability() component.StabilityLevel - func NewLimitedFactory(fact xreceiver.Factory) xreceiver.Factory { - return configureLimits{ - TypeFunc: fact.Type, - CreateDefaultConfigFunc: fact.CreateDefaultConfig, - TracesStabilityFunc: fact.TracesStability, - MetricsStabilityFunc: fact.MetricsStability, - LogsStabilityFunc: fact.LogsStability, - ProfilesStabilityFunc: fact.ProfilesStability, - CreateTracesFunc: fact.CreateTraces, - CreateMetricsFunc: fact.CreateMetrics, - CreateLogsFunc: fact.CreateLogs, - CreateProfilesFunc: fact.CreateProfiles, - } + return xreceiver.NewFactoryImpl( + receiver.NewFactoryImpl( + component.NewFactoryImpl( + fact.Type, + fact.CreateDefaultConfig, + ), + fact.CreateTraces, + fact.TracesStability, + fact.CreateMetrics, + fact.MetricsStability, + fact.CreateLogs, + fact.LogsStability, + ), + fact.CreateProfiles, + fact.ProfilesStability, + ) } diff --git a/receiver/receiver.go b/receiver/receiver.go index 4d58dd49ba4..cf5f15cec0a 100644 --- a/receiver/receiver.go +++ b/receiver/receiver.go @@ -93,13 +93,13 @@ type Factory interface { // FactoryOption apply changes to Factory. type FactoryOption interface { // applyOption applies the option. - applyOption(o *factory, cfgType component.Type) + applyOption(o *factoryImpl, cfgType component.Type) } // factoryOptionFunc is an FactoryOption created through a function. -type factoryOptionFunc func(*factory, component.Type) +type factoryOptionFunc func(*factoryImpl, component.Type) -func (f factoryOptionFunc) applyOption(o *factory, cfgType component.Type) { +func (f factoryOptionFunc) applyOption(o *factoryImpl, cfgType component.Type) { f(o, cfgType) } @@ -121,9 +121,8 @@ type MetricsStabilityFunc func() component.StabilityLevel // LogsStabilityFunc is a functional way to construct Factory implementations. type LogsStabilityFunc func() component.StabilityLevel -type factory struct { - component.TypeFunc - component.CreateDefaultConfigFunc +type factoryImpl struct { + component.Factory CreateTracesFunc TracesStabilityFunc CreateMetricsFunc @@ -132,7 +131,9 @@ type factory struct { LogsStabilityFunc } -func (f *factory) unexportedFactoryFunc() {} +var _ Factory = factoryImpl{} + +func (f factoryImpl) unexportedFactoryFunc() {} func (f TracesStabilityFunc) TracesStability() component.StabilityLevel { if f == nil { @@ -155,7 +156,7 @@ func (f LogsStabilityFunc) LogsStability() component.StabilityLevel { return f() } -type Creator[A, B any] func(ctx context.Context, set Settings, cfg component.Config, next A) (B, error) +type Creator[A, B any] func(ctx context.Context, set Settings, cfg component.Config, next A) (B, error) func typeChecked[A, B any](cf Creator[A, B], cfgType component.Type) Creator[A, B] { return func(ctx context.Context, set Settings, cfg component.Config, next A) (B, error) { @@ -164,7 +165,7 @@ func typeChecked[A, B any](cf Creator[A, B], cfgType component.Type) Creator[A, return zero, internal.ErrIDMismatch(set.ID, cfgType) } return cf(ctx, set, cfg, next) - } + } } func (f CreateTracesFunc) CreateTraces(ctx context.Context, set Settings, cfg component.Config, next consumer.Traces) (Traces, error) { @@ -193,7 +194,7 @@ func (f CreateLogsFunc) CreateLogs(ctx context.Context, set Settings, cfg compon // WithTraces overrides the default "error not supported" implementation for Factory.CreateTraces and the default "undefined" stability level. func WithTraces(createTraces CreateTracesFunc, sl component.StabilityLevel) FactoryOption { - return factoryOptionFunc(func(o *factory, cfgType component.Type) { + return factoryOptionFunc(func(o *factoryImpl, cfgType component.Type) { o.TracesStabilityFunc = sl.Self o.CreateTracesFunc = CreateTracesFunc(typeChecked[consumer.Traces, Traces](Creator[consumer.Traces, Traces](createTraces), cfgType)) }) @@ -201,7 +202,7 @@ func WithTraces(createTraces CreateTracesFunc, sl component.StabilityLevel) Fact // WithMetrics overrides the default "error not supported" implementation for Factory.CreateMetrics and the default "undefined" stability level. func WithMetrics(createMetrics CreateMetricsFunc, sl component.StabilityLevel) FactoryOption { - return factoryOptionFunc(func(o *factory, cfgType component.Type) { + return factoryOptionFunc(func(o *factoryImpl, cfgType component.Type) { o.MetricsStabilityFunc = sl.Self o.CreateMetricsFunc = CreateMetricsFunc(typeChecked[consumer.Metrics, Metrics](Creator[consumer.Metrics, Metrics](createMetrics), cfgType)) }) @@ -209,7 +210,7 @@ func WithMetrics(createMetrics CreateMetricsFunc, sl component.StabilityLevel) F // WithLogs overrides the default "error not supported" implementation for Factory.CreateLogs and the default "undefined" stability level. func WithLogs(createLogs CreateLogsFunc, sl component.StabilityLevel) FactoryOption { - return factoryOptionFunc(func(o *factory, cfgType component.Type) { + return factoryOptionFunc(func(o *factoryImpl, cfgType component.Type) { o.LogsStabilityFunc = sl.Self o.CreateLogsFunc = CreateLogsFunc(typeChecked[consumer.Logs, Logs](Creator[consumer.Logs, Logs](createLogs), cfgType)) }) @@ -217,12 +218,31 @@ func WithLogs(createLogs CreateLogsFunc, sl component.StabilityLevel) FactoryOpt // NewFactory returns a Factory. func NewFactory(cfgType component.Type, createDefaultConfig component.CreateDefaultConfigFunc, options ...FactoryOption) Factory { - f := &factory{ - TypeFunc: cfgType.Self, - CreateDefaultConfigFunc: createDefaultConfig, + f := factoryImpl{ + Factory: component.NewFactoryImpl(cfgType.Self, createDefaultConfig), } for _, opt := range options { - opt.applyOption(f, cfgType) + opt.applyOption(&f, cfgType) } return f } + +func NewFactoryImpl( + factory component.Factory, + tracesFunc CreateTracesFunc, + tracesStab TracesStabilityFunc, + metricsFunc CreateMetricsFunc, + mtricsStab MetricsStabilityFunc, + createLogs CreateLogsFunc, + logsStab LogsStabilityFunc, +) Factory { + return factoryImpl{ + Factory: factory, + CreateTracesFunc: tracesFunc, + TracesStabilityFunc: tracesStab, + CreateMetricsFunc: metricsFunc, + MetricsStabilityFunc: mtricsStab, + CreateLogsFunc: createLogs, + LogsStabilityFunc: logsStab, + } +} diff --git a/receiver/xreceiver/profiles.go b/receiver/xreceiver/profiles.go index 8a6fc9e8aaa..3b93421e0e3 100644 --- a/receiver/xreceiver/profiles.go +++ b/receiver/xreceiver/profiles.go @@ -64,12 +64,14 @@ func (f factoryOptionFunc) applyOption(o *factoryOpts, cfgType component.Type) { f(o, cfgType) } -type factory struct { +type factoryImpl struct { receiver.Factory CreateProfilesFunc ProfilesStabilityFunc } +var _ Factory = factoryImpl{} + func (f CreateProfilesFunc) CreateProfiles(ctx context.Context, set receiver.Settings, cfg component.Config, next xconsumer.Profiles) (Profiles, error) { if f == nil { return nil, pipeline.ErrSignalNotSupported @@ -79,7 +81,7 @@ func (f CreateProfilesFunc) CreateProfiles(ctx context.Context, set receiver.Set type factoryOpts struct { opts []receiver.FactoryOption - *factory + factoryImpl } type Creator[A, B any] func(ctx context.Context, set receiver.Settings, cfg component.Config, next A) (B, error) @@ -125,10 +127,22 @@ func WithProfiles(createProfiles CreateProfilesFunc, sl component.StabilityLevel // NewFactory returns a Factory. func NewFactory(cfgType component.Type, createDefaultConfig component.CreateDefaultConfigFunc, options ...FactoryOption) Factory { - opts := factoryOpts{factory: &factory{}} + var opts factoryOpts for _, opt := range options { opt.applyOption(&opts, cfgType) } opts.Factory = receiver.NewFactory(cfgType, createDefaultConfig, opts.opts...) - return opts.factory + return opts.factoryImpl +} + +func NewFactoryImpl( + factory receiver.Factory, + profilesFunc CreateProfilesFunc, + profilesStab ProfilesStabilityFunc, +) Factory { + return factoryImpl{ + Factory: factory, + CreateProfilesFunc: profilesFunc, + ProfilesStabilityFunc: profilesStab, + } } From 8f02b64838ed588506967772465537665856f5e6 Mon Sep 17 00:00:00 2001 From: Joshua MacDonald Date: Wed, 18 Jun 2025 17:31:40 -0700 Subject: [PATCH 53/62] solid --- component/component.go | 14 ++ .../consumerlimiter/consumerlimiter.go | 179 ++++++++++++------ receiver/otlpreceiver/config.go | 4 +- receiver/receiver.go | 10 +- receiver/xreceiver/profiles.go | 6 +- 5 files changed, 144 insertions(+), 69 deletions(-) diff --git a/component/component.go b/component/component.go index fd9cdc1bf6a..0daa19254c9 100644 --- a/component/component.go +++ b/component/component.go @@ -77,6 +77,20 @@ func (f ShutdownFunc) Shutdown(ctx context.Context) error { return f(ctx) } +type componentImpl struct { + StartFunc + ShutdownFunc +} + +var _ Component = componentImpl{} + +func NewComponentImpl(sf StartFunc, shf ShutdownFunc) Component { + return componentImpl{ + StartFunc: sf, + ShutdownFunc: shf, + } +} + // Kind represents component kinds. type Kind struct { name string diff --git a/extension/extensionlimiter/limiterhelper/consumerlimiter/consumerlimiter.go b/extension/extensionlimiter/limiterhelper/consumerlimiter/consumerlimiter.go index 6f1e82c58a9..5b00e9f6266 100644 --- a/extension/extensionlimiter/limiterhelper/consumerlimiter/consumerlimiter.go +++ b/extension/extensionlimiter/limiterhelper/consumerlimiter/consumerlimiter.go @@ -24,12 +24,32 @@ import ( // Config is the standard pipeline configuration for limiting a // consumer interface by specific signal. -type Config struct { +// +// This type should be embedded, for example: +// +// // LimiterConfig allows applying limiter extensions for request count, items, and bytes. +// consumerlimiter.LimiterConfig `mapstructure:"limiters"` +// +type LimiterConfig struct { RequestCount component.ID `mapstructure:"request_count"` RequestItems component.ID `mapstructure:"request_items"` RequestBytes component.ID `mapstructure:"request_bytes"` } +type HasLimiterConfig interface { + getConfig() LimiterConfig +} + +func (c LimiterConfig) getConfig() LimiterConfig { + return c +} + +var _ HasLimiterConfig = LimiterConfig{} + +type capable interface { + Capabilities() consumer.Capabilities +} + // Traits object interface is generalized by P the pipeline data type // (e.g., ptrace.Traces) and C the consumer type (e.g., // consumer.Traces) @@ -49,6 +69,8 @@ type traits[P, C any] interface { type traceTraits struct{} +var _ traits[ptrace.Traces, consumer.Traces] = traceTraits{} + func (traceTraits) itemCount(data ptrace.Traces) int { return data.SpanCount() } @@ -70,6 +92,8 @@ func (traceTraits) consume(ctx context.Context, data ptrace.Traces, next consume type metricTraits struct{} +var _ traits[pmetric.Metrics, consumer.Metrics] = metricTraits{} + func (metricTraits) itemCount(data pmetric.Metrics) int { return data.DataPointCount() } @@ -91,6 +115,8 @@ func (metricTraits) consume(ctx context.Context, data pmetric.Metrics, next cons type logTraits struct{} +var _ traits[plog.Logs, consumer.Logs] = logTraits{} + func (logTraits) itemCount(data plog.Logs) int { return data.LogRecordCount() } @@ -112,6 +138,8 @@ func (logTraits) consume(ctx context.Context, data plog.Logs, next consumer.Logs type profileTraits struct{} +var _ traits[pprofile.Profiles, xconsumer.Profiles] = profileTraits{} + func (profileTraits) itemCount(data pprofile.Profiles) int { return data.SampleCount() } @@ -130,7 +158,7 @@ func (profileTraits) consume(ctx context.Context, data pprofile.Profiles, next x } // limitOne obtains a Wrapper and applies a single weight limit. -func limitOne[P any, C any]( +func limitOne[P, C, R any]( next C, keys []extensionlimiter.WeightKey, provider limiterhelper.WrapperProvider, @@ -156,64 +184,97 @@ func limitOne[P any, C any]( }, opts...) } -// newLimited is signal-generic limiting logic. -func newLimited[P any, C any]( - next C, - keys []extensionlimiter.WeightKey, - provider limiterhelper.WrapperProvider, - 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.WeightKeyRequestBytes, opts, - func(data P) int { - return m.requestSize(data) - }) - next, err2 = limitOne(next, keys, provider, m, extensionlimiter.WeightKeyRequestItems, opts, - func(data P) int { - return m.itemCount(data) - }) - next, err3 = limitOne(next, keys, provider, m, extensionlimiter.WeightKeyRequestCount, opts, - func(_ P) int { - return 1 - }) - return next, multierr.Append(err1, multierr.Append(err2, err3)) -} - -// // NewLimitedTraces applies a limiter using the provider over keys before calling next. -// func NewLimitedTraces(next consumer.Traces, keys []extensionlimiter.WeightKey, provider limiterhelper.WrapperProvider) (consumer.Traces, error) { -// return newLimited(next, keys, provider, traceTraits{}, -// consumer.WithCapabilities(next.Capabilities())) -// } +type limitedReceiver[P any, C capable, T traits[P, C]] struct { + next C -// // NewLimitedLogs applies a limiter using the provider over keys before calling next. -// func NewLimitedLogs(next consumer.Logs, keys []extensionlimiter.WeightKey, provider limiterhelper.WrapperProvider) (consumer.Logs, error) { -// return newLimited(next, keys, provider, logTraits{}, -// consumer.WithCapabilities(next.Capabilities())) -// } + component.ShutdownFunc +} -// // NewLimitedMetrics applies a limiter using the provider over keys before calling next. -// func NewLimitedMetrics(next consumer.Metrics, keys []extensionlimiter.WeightKey, provider limiterhelper.WrapperProvider) (consumer.Metrics, error) { -// return newLimited(next, keys, provider, metricTraits{}, -// consumer.WithCapabilities(next.Capabilities())) -// } +func (l *limitedReceiver[P, C, T]) Capabilities() consumer.Capabilities { + return l.next.Capabilities() +} -// // NewLimitedProfiles applies a limiter using the provider over keys before calling next. -// func NewLimitedProfiles(next xconsumer.Profiles, keys []extensionlimiter.WeightKey, provider limiterhelper.WrapperProvider) (xconsumer.Profiles, error) { -// return newLimited(next, keys, provider, profileTraits{}, -// consumer.WithCapabilities(next.Capabilities())) -// } +func (l *limitedReceiver[P, C, T]) Start(ctx context.Context, host component.Host) error { + // @@@ + return nil +} -// type stabilityFunc func() component.StabilityLevel +func (l *limitedReceiver[P, C, T]) consume(ctx context.Context, data P) error { + var t T + return t.consume(ctx, data, l.next) +} -// // TracesStability gets the stability level of the Traces receiver. -// TracesStability() component.StabilityLevel -// // MetricsStability gets the stability level of the Metrics receiver. -// MetricsStability() component.StabilityLevel +// newLimited is signal-generic limiting logic. +func newLimited[P any, C capable]( + next C, + cfg LimiterConfig, + t traits[P, C], +) *limitedReceiver[P, C, traits[P, C]] { + return &limitedReceiver[P, C, traits[P, C]]{ + next: next, + } +} + // opts ...consumer.Option, + // 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.WeightKeyRequestBytes, opts, + // func(data P) int { + // return m.requestSize(data) + // }) + // next, err2 = limitOne(next, keys, provider, m, extensionlimiter.WeightKeyRequestItems, opts, + // func(data P) int { + // return m.itemCount(data) + // }) + // next, err3 = limitOne(next, keys, provider, m, extensionlimiter.WeightKeyRequestCount, opts, + // func(_ P) int { + // return 1 + // }) + // return next, multierr.Append(err1, multierr.Append(err2, err3)) + //var zero C + // @@@ @@@!!! + //return zero, nil + +type creator[C capable, R component.Component] func(ctx context.Context, set receiver.Settings, cfg component.Config, next C) (R, error) + +// limitReceiver limits a receiver component where P is pipeline data, +// C is the consumer type, and R is the return type. +func limitReceiver[P any, C capable, R component.Component]( + cf creator[C, R], + t traits[P, C], +) creator[C, R] { + return func(ctx context.Context, set receiver.Settings, cfg component.Config, next C) (R, error) { + var limiter *limitedReceiver[P, C, traits[P, C]] + if lc, ok := cfg.(HasLimiterConfig); ok { + limiter = newLimited(next, lc.getConfig(), t) + var err error + next, err = t.create(limiter.consume) + if err != nil { + var zero R + return zero, err + } + } + + recv, err := cf(ctx, set, cfg, next) + if err != nil { + return recv, err + } + return component.NewComponentImpl( + func (ctx context.Context, host component.Host) error { + err1 := limiter.Start(ctx, host) + err2 := recv.Start(ctx, host) + return multierr.Append(err1, err2) + }, + func (ctx context.Context) error { + err1 := recv.Shutdown(ctx) + err2 := limiter.Shutdown(ctx) + return multierr.Append(err1, err2) + }, + ), nil + } +} func NewLimitedFactory(fact xreceiver.Factory) xreceiver.Factory { return xreceiver.NewFactoryImpl( @@ -222,14 +283,14 @@ func NewLimitedFactory(fact xreceiver.Factory) xreceiver.Factory { fact.Type, fact.CreateDefaultConfig, ), - fact.CreateTraces, + receiver.CreateTracesFunc(limitReceiver(fact.CreateTraces, traceTraits{})), fact.TracesStability, - fact.CreateMetrics, + receiver.CreateMetricsFunc(limitReceiver(fact.CreateMetrics, metricTraits{})), fact.MetricsStability, - fact.CreateLogs, + receiver.CreateLogsFunc(limitReceiver(fact.CreateLogs, logTraits{})), fact.LogsStability, ), - fact.CreateProfiles, + xreceiver.CreateProfilesFunc(limitReceiver(fact.CreateProfiles, profileTraits{})), fact.ProfilesStability, ) } diff --git a/receiver/otlpreceiver/config.go b/receiver/otlpreceiver/config.go index 9d20b33c706..c04feeb6ff7 100644 --- a/receiver/otlpreceiver/config.go +++ b/receiver/otlpreceiver/config.go @@ -64,8 +64,8 @@ type Config struct { // Protocols is the configuration for the supported protocols, currently gRPC and HTTP (Proto and JSON). Protocols `mapstructure:"protocols"` - // Limiters allows applying limiter extensions for request count, items, and bytes. - Limiters consumerlimiter.Config `mapstructure:"limiters"` + // LimiterConfig allows applying limiter extensions for request count, items, and bytes. + consumerlimiter.LimiterConfig `mapstructure:"limiters"` } var _ component.Config = (*Config)(nil) diff --git a/receiver/receiver.go b/receiver/receiver.go index cf5f15cec0a..2e581d9bb4f 100644 --- a/receiver/receiver.go +++ b/receiver/receiver.go @@ -156,9 +156,9 @@ func (f LogsStabilityFunc) LogsStability() component.StabilityLevel { return f() } -type Creator[A, B any] func(ctx context.Context, set Settings, cfg component.Config, next A) (B, error) +type creator[A, B any] func(ctx context.Context, set Settings, cfg component.Config, next A) (B, error) -func typeChecked[A, B any](cf Creator[A, B], cfgType component.Type) Creator[A, B] { +func typeChecked[A, B any](cf creator[A, B], cfgType component.Type) creator[A, B] { return func(ctx context.Context, set Settings, cfg component.Config, next A) (B, error) { if set.ID.Type() != cfgType { var zero B @@ -196,7 +196,7 @@ func (f CreateLogsFunc) CreateLogs(ctx context.Context, set Settings, cfg compon func WithTraces(createTraces CreateTracesFunc, sl component.StabilityLevel) FactoryOption { return factoryOptionFunc(func(o *factoryImpl, cfgType component.Type) { o.TracesStabilityFunc = sl.Self - o.CreateTracesFunc = CreateTracesFunc(typeChecked[consumer.Traces, Traces](Creator[consumer.Traces, Traces](createTraces), cfgType)) + o.CreateTracesFunc = CreateTracesFunc(typeChecked[consumer.Traces, Traces](creator[consumer.Traces, Traces](createTraces), cfgType)) }) } @@ -204,7 +204,7 @@ func WithTraces(createTraces CreateTracesFunc, sl component.StabilityLevel) Fact func WithMetrics(createMetrics CreateMetricsFunc, sl component.StabilityLevel) FactoryOption { return factoryOptionFunc(func(o *factoryImpl, cfgType component.Type) { o.MetricsStabilityFunc = sl.Self - o.CreateMetricsFunc = CreateMetricsFunc(typeChecked[consumer.Metrics, Metrics](Creator[consumer.Metrics, Metrics](createMetrics), cfgType)) + o.CreateMetricsFunc = CreateMetricsFunc(typeChecked[consumer.Metrics, Metrics](creator[consumer.Metrics, Metrics](createMetrics), cfgType)) }) } @@ -212,7 +212,7 @@ func WithMetrics(createMetrics CreateMetricsFunc, sl component.StabilityLevel) F func WithLogs(createLogs CreateLogsFunc, sl component.StabilityLevel) FactoryOption { return factoryOptionFunc(func(o *factoryImpl, cfgType component.Type) { o.LogsStabilityFunc = sl.Self - o.CreateLogsFunc = CreateLogsFunc(typeChecked[consumer.Logs, Logs](Creator[consumer.Logs, Logs](createLogs), cfgType)) + o.CreateLogsFunc = CreateLogsFunc(typeChecked[consumer.Logs, Logs](creator[consumer.Logs, Logs](createLogs), cfgType)) }) } diff --git a/receiver/xreceiver/profiles.go b/receiver/xreceiver/profiles.go index 3b93421e0e3..0b0b8faf6c2 100644 --- a/receiver/xreceiver/profiles.go +++ b/receiver/xreceiver/profiles.go @@ -84,9 +84,9 @@ type factoryOpts struct { factoryImpl } -type Creator[A, B any] func(ctx context.Context, set receiver.Settings, cfg component.Config, next A) (B, error) +type creator[A, B any] func(ctx context.Context, set receiver.Settings, cfg component.Config, next A) (B, error) -func typeChecked[A, B any](cf Creator[A, B], cfgType component.Type) Creator[A, B] { +func typeChecked[A, B any](cf creator[A, B], cfgType component.Type) creator[A, B] { return func(ctx context.Context, set receiver.Settings, cfg component.Config, next A) (B, error) { if set.ID.Type() != cfgType { var zero B @@ -121,7 +121,7 @@ func WithLogs(createLogs receiver.CreateLogsFunc, sl component.StabilityLevel) F func WithProfiles(createProfiles CreateProfilesFunc, sl component.StabilityLevel) FactoryOption { return factoryOptionFunc(func(o *factoryOpts, cfgType component.Type) { o.ProfilesStabilityFunc = sl.Self - o.CreateProfilesFunc = CreateProfilesFunc(typeChecked[xconsumer.Profiles, Profiles](Creator[xconsumer.Profiles, Profiles](createProfiles), cfgType)) + o.CreateProfilesFunc = CreateProfilesFunc(typeChecked[xconsumer.Profiles, Profiles](creator[xconsumer.Profiles, Profiles](createProfiles), cfgType)) }) } From 746fbbd4b8e94a13f89fbbfe1aa755fb8481c084 Mon Sep 17 00:00:00 2001 From: Joshua MacDonald Date: Thu, 19 Jun 2025 12:38:13 -0700 Subject: [PATCH 54/62] consumer limiter --- .../consumerlimiter/consumerlimiter.go | 222 +++++++++++------- receiver/otlpreceiver/factory.go | 3 + 2 files changed, 137 insertions(+), 88 deletions(-) diff --git a/extension/extensionlimiter/limiterhelper/consumerlimiter/consumerlimiter.go b/extension/extensionlimiter/limiterhelper/consumerlimiter/consumerlimiter.go index 5b00e9f6266..fe9da73c231 100644 --- a/extension/extensionlimiter/limiterhelper/consumerlimiter/consumerlimiter.go +++ b/extension/extensionlimiter/limiterhelper/consumerlimiter/consumerlimiter.go @@ -5,7 +5,8 @@ package consumerlimiter // import "go.opentelemetry.io/collector/extension/exten import ( "context" - "slices" + "errors" + "fmt" "go.uber.org/multierr" @@ -22,6 +23,11 @@ import ( "go.opentelemetry.io/collector/receiver/xreceiver" ) +var ( + ErrLimiterNotFound = errors.New("limiter not found") + ErrNotALimiter = errors.New("not a limiter") +) + // Config is the standard pipeline configuration for limiting a // consumer interface by specific signal. // @@ -36,24 +42,16 @@ type LimiterConfig struct { RequestBytes component.ID `mapstructure:"request_bytes"` } -type HasLimiterConfig interface { - getConfig() LimiterConfig -} - -func (c LimiterConfig) getConfig() LimiterConfig { - return c -} - -var _ HasLimiterConfig = LimiterConfig{} - +// capable is an internal interface describing common features of a +// consumer. type capable interface { Capabilities() consumer.Capabilities } // 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 { +// consumer.Traces) and R the return component type. +type traits[P, C, R any] interface { // itemCount is SpanCount(), DataPointCount(), or LogRecordCount(). itemCount(P) int // requestBytes uses the appropriate protobuf Bytesr as a proxy @@ -63,13 +61,18 @@ type traits[P, C any] interface { 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) + // newReceiver constructs the correct receiver type + newReceiver(component.Component) R } +// Creator is the function to create a receiver components. +type creator[C capable, R component.Component] func(ctx context.Context, set receiver.Settings, cfg component.Config, next C) (R, error) + // Traces traits type traceTraits struct{} -var _ traits[ptrace.Traces, consumer.Traces] = traceTraits{} +var _ traits[ptrace.Traces, consumer.Traces, receiver.Traces] = traceTraits{} func (traceTraits) itemCount(data ptrace.Traces) int { return data.SpanCount() @@ -88,11 +91,15 @@ func (traceTraits) consume(ctx context.Context, data ptrace.Traces, next consume return next.ConsumeTraces(ctx, data) } +func (traceTraits) newReceiver(c component.Component) receiver.Traces { + return receiver.Traces(c) +} + // Metrics traits type metricTraits struct{} -var _ traits[pmetric.Metrics, consumer.Metrics] = metricTraits{} +var _ traits[pmetric.Metrics, consumer.Metrics, receiver.Metrics] = metricTraits{} func (metricTraits) itemCount(data pmetric.Metrics) int { return data.DataPointCount() @@ -111,11 +118,15 @@ func (metricTraits) consume(ctx context.Context, data pmetric.Metrics, next cons return next.ConsumeMetrics(ctx, data) } +func (metricTraits) newReceiver(c component.Component) receiver.Metrics { + return receiver.Metrics(c) +} + // Logs traits type logTraits struct{} -var _ traits[plog.Logs, consumer.Logs] = logTraits{} +var _ traits[plog.Logs, consumer.Logs, receiver.Logs] = logTraits{} func (logTraits) itemCount(data plog.Logs) int { return data.LogRecordCount() @@ -134,11 +145,15 @@ func (logTraits) consume(ctx context.Context, data plog.Logs, next consumer.Logs return next.ConsumeLogs(ctx, data) } +func (logTraits) newReceiver(c component.Component) receiver.Logs { + return receiver.Logs(c) +} + // Profiles traits type profileTraits struct{} -var _ traits[pprofile.Profiles, xconsumer.Profiles] = profileTraits{} +var _ traits[pprofile.Profiles, xconsumer.Profiles, xreceiver.Profiles] = profileTraits{} func (profileTraits) itemCount(data pprofile.Profiles) int { return data.SampleCount() @@ -157,98 +172,126 @@ func (profileTraits) consume(ctx context.Context, data pprofile.Profiles, next x return next.ConsumeProfiles(ctx, data) } +func (profileTraits) newReceiver(c component.Component) xreceiver.Profiles { + return xreceiver.Profiles(c) +} + +type limitedReceiver[P any, C capable, R component.Component, T traits[P, C, R]] struct { + cfg LimiterConfig + next C + self T + component.ShutdownFunc +} + +func (l *limitedReceiver[P, C, R, T]) Capabilities() consumer.Capabilities { + return l.next.Capabilities() +} + +func (l *limitedReceiver[P, C, R, T]) Start(ctx context.Context, host component.Host) error { + var unset component.ID + var err1, err2, err3 error + if name := l.cfg.RequestBytes; name != unset { + l.next, err1 = l.limitOne( + host, + name, + extensionlimiter.WeightKeyRequestBytes, + func(data P) int { + return l.self.requestSize(data) + }, + ) + } + if name := l.cfg.RequestItems; name != unset { + l.next, err2 = l.limitOne( + host, + name, + extensionlimiter.WeightKeyRequestItems, + func(data P) int { + return l.self.itemCount(data) + }, + ) + } + if name := l.cfg.RequestCount; name != unset { + l.next, err3 = l.limitOne( + host, + name, + extensionlimiter.WeightKeyRequestCount, + func(data P) int { + return 1 + }, + ) + } + + return multierr.Append(err1, multierr.Append(err2, err3)) +} + +func (l *limitedReceiver[P, C, R, T]) consume(ctx context.Context, data P) error { + return l.self.consume(ctx, data, l.next) +} + // limitOne obtains a Wrapper and applies a single weight limit. -func limitOne[P, C, R any]( - next C, - keys []extensionlimiter.WeightKey, - provider limiterhelper.WrapperProvider, - m traits[P, C], +func (l *limitedReceiver[P, C, R, T]) limitOne( + host component.Host, + name component.ID, key extensionlimiter.WeightKey, - opts []consumer.Option, quantify func(P) int, ) (C, error) { - if !slices.Contains(keys, key) { - return next, nil + exts := host.GetExtensions() + comp := exts[name] + if comp == nil { + return l.next, fmt.Errorf("%w: %s", ErrLimiterNotFound, name.String()) + } + alim, isLim := comp.(extensionlimiter.AnyProvider) + if !isLim { + return l.next, fmt.Errorf("%w: %s", ErrNotALimiter, name.String()) } + provider, err := limiterhelper.AnyToWrapperProvider(alim) + if err != nil { + return l.next, err + } + // Note: not passing options to GetWrapper(), an open question. lim, err := provider.GetWrapper(key) if err != nil { - return next, err + return l.next, err } if lim == nil { - return next, nil + return l.next, nil } - return m.create(func(ctx context.Context, data P) error { + return l.self.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) + return l.self.consume(ctx, data, l.next) }) - }, opts...) -} - -type limitedReceiver[P any, C capable, T traits[P, C]] struct { - next C - - component.ShutdownFunc -} - -func (l *limitedReceiver[P, C, T]) Capabilities() consumer.Capabilities { - return l.next.Capabilities() -} - -func (l *limitedReceiver[P, C, T]) Start(ctx context.Context, host component.Host) error { - // @@@ - return nil -} - -func (l *limitedReceiver[P, C, T]) consume(ctx context.Context, data P) error { - var t T - return t.consume(ctx, data, l.next) + }, consumer.WithCapabilities(l.next.Capabilities())) } // newLimited is signal-generic limiting logic. -func newLimited[P any, C capable]( +func newLimited[P any, C capable, R component.Component]( next C, cfg LimiterConfig, - t traits[P, C], -) *limitedReceiver[P, C, traits[P, C]] { - return &limitedReceiver[P, C, traits[P, C]]{ + self traits[P, C, R], +) *limitedReceiver[P, C, R, traits[P, C, R]] { + return &limitedReceiver[P, C, R, traits[P, C, R]]{ + cfg: cfg, next: next, + self: self, } } - // opts ...consumer.Option, - // 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.WeightKeyRequestBytes, opts, - // func(data P) int { - // return m.requestSize(data) - // }) - // next, err2 = limitOne(next, keys, provider, m, extensionlimiter.WeightKeyRequestItems, opts, - // func(data P) int { - // return m.itemCount(data) - // }) - // next, err3 = limitOne(next, keys, provider, m, extensionlimiter.WeightKeyRequestCount, opts, - // func(_ P) int { - // return 1 - // }) - // return next, multierr.Append(err1, multierr.Append(err2, err3)) - //var zero C - // @@@ @@@!!! - //return zero, nil -type creator[C capable, R component.Component] func(ctx context.Context, set receiver.Settings, cfg component.Config, next C) (R, error) +// LimiterConfigurator lets components configure limiters using +// a field they determine. +type LimiterConfigurator func(component.Config) LimiterConfig // limitReceiver limits a receiver component where P is pipeline data, // C is the consumer type, and R is the return type. func limitReceiver[P any, C capable, R component.Component]( cf creator[C, R], - t traits[P, C], + t traits[P, C, R], + cfgf LimiterConfigurator, ) creator[C, R] { return func(ctx context.Context, set receiver.Settings, cfg component.Config, next C) (R, error) { - var limiter *limitedReceiver[P, C, traits[P, C]] - if lc, ok := cfg.(HasLimiterConfig); ok { - limiter = newLimited(next, lc.getConfig(), t) + var limiter *limitedReceiver[P, C, R, traits[P, C, R]] + var emptyCfg LimiterConfig + if lc := cfgf(cfg); lc != emptyCfg { + limiter = newLimited(next, lc, t) var err error next, err = t.create(limiter.consume) if err != nil { @@ -261,7 +304,10 @@ func limitReceiver[P any, C capable, R component.Component]( if err != nil { return recv, err } - return component.NewComponentImpl( + if limiter == nil { + return recv, nil + } + return t.newReceiver(component.NewComponentImpl( func (ctx context.Context, host component.Host) error { err1 := limiter.Start(ctx, host) err2 := recv.Start(ctx, host) @@ -272,25 +318,25 @@ func limitReceiver[P any, C capable, R component.Component]( err2 := limiter.Shutdown(ctx) return multierr.Append(err1, err2) }, - ), nil + )), nil } } -func NewLimitedFactory(fact xreceiver.Factory) xreceiver.Factory { +func NewLimitedFactory(fact xreceiver.Factory, cfgf LimiterConfigurator) xreceiver.Factory { return xreceiver.NewFactoryImpl( receiver.NewFactoryImpl( component.NewFactoryImpl( fact.Type, fact.CreateDefaultConfig, ), - receiver.CreateTracesFunc(limitReceiver(fact.CreateTraces, traceTraits{})), + receiver.CreateTracesFunc(limitReceiver(fact.CreateTraces, traceTraits{}, cfgf)), fact.TracesStability, - receiver.CreateMetricsFunc(limitReceiver(fact.CreateMetrics, metricTraits{})), + receiver.CreateMetricsFunc(limitReceiver(fact.CreateMetrics, metricTraits{}, cfgf)), fact.MetricsStability, - receiver.CreateLogsFunc(limitReceiver(fact.CreateLogs, logTraits{})), + receiver.CreateLogsFunc(limitReceiver(fact.CreateLogs, logTraits{}, cfgf)), fact.LogsStability, ), - xreceiver.CreateProfilesFunc(limitReceiver(fact.CreateProfiles, profileTraits{})), + xreceiver.CreateProfilesFunc(limitReceiver(fact.CreateProfiles, profileTraits{}, cfgf)), fact.ProfilesStability, ) } diff --git a/receiver/otlpreceiver/factory.go b/receiver/otlpreceiver/factory.go index ae41051c696..d78ea5cd3a6 100644 --- a/receiver/otlpreceiver/factory.go +++ b/receiver/otlpreceiver/factory.go @@ -38,6 +38,9 @@ func NewFactory() receiver.Factory { xreceiver.WithLogs(createLog, metadata.LogsStability), xreceiver.WithProfiles(createProfiles, metadata.ProfilesStability), ), + func(cfg component.Config) consumerlimiter.LimiterConfig { + return cfg.(*Config).LimiterConfig + }, ) } From 645ccefaba312dbf429e3321b717a79be8b3f108 Mon Sep 17 00:00:00 2001 From: Joshua MacDonald Date: Thu, 19 Jun 2025 13:24:41 -0700 Subject: [PATCH 55/62] test middleware config flattening --- config/configgrpc/client_middleware_test.go | 12 +++------- config/configgrpc/configgrpc.go | 2 +- config/configgrpc/go.mod | 2 -- config/configgrpc/server_middleware_test.go | 8 ++----- config/confighttp/client_middleware_test.go | 20 +++++------------ config/confighttp/go.mod | 6 ----- config/confighttp/go.sum | 5 ----- config/confighttp/server_middleware_test.go | 20 +++++------------ config/configmiddleware/configmiddleware.go | 7 ++++++ extension/extensionlimiter/go.mod | 7 ++++-- extension/extensionlimiter/go.sum | 2 ++ .../memorylimiterextension/memorylimiter.go | 2 +- receiver/otlpreceiver/config_test.go | 22 +++++++++++++++++++ receiver/otlpreceiver/testdata/limiters.yaml | 15 +++++++++++++ 14 files changed, 68 insertions(+), 62 deletions(-) create mode 100644 receiver/otlpreceiver/testdata/limiters.yaml diff --git a/config/configgrpc/client_middleware_test.go b/config/configgrpc/client_middleware_test.go index 4bd3dcf8cfc..0fac0184b50 100644 --- a/config/configgrpc/client_middleware_test.go +++ b/config/configgrpc/client_middleware_test.go @@ -31,9 +31,7 @@ type testClientMiddleware struct { } func newTestMiddlewareConfig(name string) configmiddleware.Config { - return configmiddleware.Config{ - ID: component.MustNewID(name), - } + return configmiddleware.Config(component.MustNewID(name)) } func newTestClientMiddleware(name string) extension.Extension { @@ -162,9 +160,7 @@ func TestClientMiddlewareToClientErrors(t *testing.T) { Insecure: true, }, Middlewares: []configmiddleware.Config{ - { - ID: component.MustNewID("nonexistent"), - }, + configmiddleware.Config(component.MustNewID("nonexistent")), }, }, errText: "failed to resolve middleware \"nonexistent\": middleware not found", @@ -182,9 +178,7 @@ func TestClientMiddlewareToClientErrors(t *testing.T) { Insecure: true, }, Middlewares: []configmiddleware.Config{ - { - ID: component.MustNewID("errormw"), - }, + configmiddleware.Config(component.MustNewID("errormw")), }, }, errText: "get options failed", diff --git a/config/configgrpc/configgrpc.go b/config/configgrpc/configgrpc.go index c55cbe2a87e..33a9ca37a0b 100644 --- a/config/configgrpc/configgrpc.go +++ b/config/configgrpc/configgrpc.go @@ -207,7 +207,7 @@ type ServerConfig struct { IncludeMetadata bool `mapstructure:"include_metadata,omitempty"` // Middlewares for the gRPC server. - Middlewares []configmiddleware.Config `mapstructure:"middlewares,omitempty"` + Middlewares []configmiddleware.Config `mapstructure:"middleware,omitempty"` // prevent unkeyed literal initialization _ struct{} diff --git a/config/configgrpc/go.mod b/config/configgrpc/go.mod index 0f09db026eb..a02cced744c 100644 --- a/config/configgrpc/go.mod +++ b/config/configgrpc/go.mod @@ -44,8 +44,6 @@ require ( 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.34.0 // indirect - go.opentelemetry.io/collector/consumer/xconsumer v0.0.0-00010101000000-000000000000 // indirect go.opentelemetry.io/collector/extension/extensionlimiter v0.0.0-00010101000000-000000000000 // indirect go.opentelemetry.io/collector/featuregate v1.34.0 // indirect go.opentelemetry.io/collector/internal/telemetry v0.128.0 // indirect diff --git a/config/configgrpc/server_middleware_test.go b/config/configgrpc/server_middleware_test.go index 0a0b736dff8..598a2dff8ff 100644 --- a/config/configgrpc/server_middleware_test.go +++ b/config/configgrpc/server_middleware_test.go @@ -122,9 +122,7 @@ func TestServerMiddlewareToServerErrors(t *testing.T) { Transport: confignet.TransportTypeTCP, }, Middlewares: []configmiddleware.Config{ - { - ID: component.MustNewID("nonexistent"), - }, + configmiddleware.Config(component.MustNewID("nonexistent")), }, }, errText: "failed to resolve middleware \"nonexistent\": middleware not found", @@ -142,9 +140,7 @@ func TestServerMiddlewareToServerErrors(t *testing.T) { Transport: confignet.TransportTypeTCP, }, Middlewares: []configmiddleware.Config{ - { - ID: component.MustNewID("errormw"), - }, + configmiddleware.Config(component.MustNewID("errormw")), }, }, errText: "get server options failed", diff --git a/config/confighttp/client_middleware_test.go b/config/confighttp/client_middleware_test.go index 01155962613..c8c96f18bec 100644 --- a/config/confighttp/client_middleware_test.go +++ b/config/confighttp/client_middleware_test.go @@ -61,9 +61,7 @@ func newTestClientMiddleware(name string) component.Component { } func newTestClientConfig(name string) configmiddleware.Config { - return configmiddleware.Config{ - ID: component.MustNewID(name), - } + return configmiddleware.Config(component.MustNewID(name)) } func TestClientMiddlewares(t *testing.T) { @@ -162,9 +160,7 @@ func TestClientMiddlewareErrors(t *testing.T) { config: ClientConfig{ Endpoint: server.URL, Middlewares: []configmiddleware.Config{ - { - ID: component.MustNewID("nonexistent"), - }, + configmiddleware.Config(component.MustNewID("nonexistent")), }, }, errText: "failed to resolve middleware \"nonexistent\": middleware not found", @@ -179,9 +175,7 @@ func TestClientMiddlewareErrors(t *testing.T) { config: ClientConfig{ Endpoint: server.URL, Middlewares: []configmiddleware.Config{ - { - ID: component.MustNewID("errormw"), - }, + configmiddleware.Config(component.MustNewID("errormw")), }, }, errText: "http middleware error", @@ -216,9 +210,7 @@ func TestGRPCClientMiddlewareErrors(t *testing.T) { config: ClientConfig{ Endpoint: "localhost:1234", Middlewares: []configmiddleware.Config{ - { - ID: component.MustNewID("nonexistent"), - }, + configmiddleware.Config(component.MustNewID("nonexistent")), }, }, errText: "failed to resolve middleware \"nonexistent\": middleware not found", @@ -233,9 +225,7 @@ func TestGRPCClientMiddlewareErrors(t *testing.T) { config: ClientConfig{ Endpoint: "localhost:1234", Middlewares: []configmiddleware.Config{ - { - ID: component.MustNewID("errormw"), - }, + configmiddleware.Config(component.MustNewID("errormw")), }, }, errText: "grpc middleware error", diff --git a/config/confighttp/go.mod b/config/confighttp/go.mod index cf1b4704f5b..4fdbfc09f05 100644 --- a/config/confighttp/go.mod +++ b/config/confighttp/go.mod @@ -40,17 +40,11 @@ require ( github.com/google/go-tpm v0.9.5 // 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.34.0 // indirect - go.opentelemetry.io/collector/consumer/xconsumer v0.0.0-00010101000000-000000000000 // indirect go.opentelemetry.io/collector/extension/extensionlimiter v0.0.0-00010101000000-000000000000 // indirect go.opentelemetry.io/collector/internal/telemetry v0.128.0 // indirect go.opentelemetry.io/collector/pdata v1.34.0 // indirect - go.opentelemetry.io/collector/pdata/pprofile v0.128.0 // indirect go.opentelemetry.io/contrib/bridges/otelzap v0.11.0 // indirect go.opentelemetry.io/otel/log v0.12.2 // indirect go.opentelemetry.io/otel/metric v1.36.0 // indirect diff --git a/config/confighttp/go.sum b/config/confighttp/go.sum index 244f79bdd3c..4b512feaf01 100644 --- a/config/confighttp/go.sum +++ b/config/confighttp/go.sum @@ -1,4 +1,3 @@ -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/felixge/httpsnoop v1.0.4 h1:NFTV2Zj1bL4mc9sqWACXbQFVBBg2W3GPvqp8/ESS2Wg= @@ -26,7 +25,6 @@ github.com/google/go-tpm v0.9.5 h1:ocUmnDebX54dnW+MQWGQRbdaAcJELsa6PqZhJ48KwVU= github.com/google/go-tpm v0.9.5/go.mod h1:h9jEsEECg7gtLis0upRBQU+GhYVH6jMjrFxI8u6bVUY= github.com/google/go-tpm-tools v0.4.4 h1:oiQfAIkc6xTy9Fl5NKTeTJkBTlXdHsxAofmQyxBKY98= github.com/google/go-tpm-tools v0.4.4/go.mod h1:T8jXkp2s+eltnCDIsXR84/MTcVU9Ja7bh3Mit0pa4AY= -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= @@ -41,7 +39,6 @@ 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= @@ -54,8 +51,6 @@ github.com/rogpeppe/go-internal v1.13.1 h1:KvO1DLK/DRN07sQ1LQKScxyZJuNnedQ5/wKSR github.com/rogpeppe/go-internal v1.13.1/go.mod h1:uMEvuHeurkdAXX61udpOXGD/AzZDWNMNyH2VO9fmH0o= github.com/rs/cors v1.11.1 h1:eU3gRzXLRK57F5rKMGMZURNdIG4EoAmX8k94r9wXWHA= github.com/rs/cors v1.11.1/go.mod h1:XyqrcTp5zjWr1wsJ8PIRZssZ8b/WMcMf71DJnit4EMU= -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= diff --git a/config/confighttp/server_middleware_test.go b/config/confighttp/server_middleware_test.go index ca383ccfe38..5a559890f35 100644 --- a/config/confighttp/server_middleware_test.go +++ b/config/confighttp/server_middleware_test.go @@ -47,9 +47,7 @@ func newTestServerMiddleware(name string) component.Component { } func newTestServerConfig(name string) configmiddleware.Config { - return configmiddleware.Config{ - ID: component.MustNewID(name), - } + return configmiddleware.Config(component.MustNewID(name)) } func TestServerMiddleware(t *testing.T) { @@ -154,9 +152,7 @@ func TestServerMiddlewareErrors(t *testing.T) { config: ServerConfig{ Endpoint: "localhost:0", Middlewares: []configmiddleware.Config{ - { - ID: component.MustNewID("nonexistent"), - }, + configmiddleware.Config(component.MustNewID("nonexistent")), }, }, errText: "failed to resolve middleware \"nonexistent\": middleware not found", @@ -171,9 +167,7 @@ func TestServerMiddlewareErrors(t *testing.T) { config: ServerConfig{ Endpoint: "localhost:0", Middlewares: []configmiddleware.Config{ - { - ID: component.MustNewID("errormw"), - }, + configmiddleware.Config(component.MustNewID("errormw")), }, }, errText: "http middleware error", @@ -209,9 +203,7 @@ func TestServerMiddlewareErrors(t *testing.T) { config: ServerConfig{ Endpoint: "localhost:0", Middlewares: []configmiddleware.Config{ - { - ID: component.MustNewID("nonexistent"), - }, + configmiddleware.Config(component.MustNewID("nonexistent")), }, }, errText: "failed to resolve middleware \"nonexistent\": middleware not found", @@ -226,9 +218,7 @@ func TestServerMiddlewareErrors(t *testing.T) { config: ServerConfig{ Endpoint: "localhost:0", Middlewares: []configmiddleware.Config{ - { - ID: component.MustNewID("errormw"), - }, + configmiddleware.Config(component.MustNewID("errormw")), }, }, errText: "grpc middleware error", diff --git a/config/configmiddleware/configmiddleware.go b/config/configmiddleware/configmiddleware.go index 3eb786cf2eb..1ac2899a198 100644 --- a/config/configmiddleware/configmiddleware.go +++ b/config/configmiddleware/configmiddleware.go @@ -32,6 +32,13 @@ var ( // Middleware defines the extension ID for a middleware component. type Config component.ID +func (cfg *Config) UnmarshalText(in []byte) error { + var id component.ID + err := id.UnmarshalText(in) + *cfg = Config(id) + return err +} + func resolveFailed(id component.ID) error { return fmt.Errorf("failed to resolve middleware %q: %w", id, errMiddlewareNotFound) } diff --git a/extension/extensionlimiter/go.mod b/extension/extensionlimiter/go.mod index 85269117471..025c9e2e53a 100644 --- a/extension/extensionlimiter/go.mod +++ b/extension/extensionlimiter/go.mod @@ -3,12 +3,15 @@ module go.opentelemetry.io/collector/extension/extensionlimiter go 1.23.0 require ( + go.opentelemetry.io/collector/component v1.34.0 go.opentelemetry.io/collector/consumer v1.34.0 - go.opentelemetry.io/collector/consumer/xconsumer v0.0.0-00010101000000-000000000000 + go.opentelemetry.io/collector/consumer/xconsumer v0.128.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.34.0 go.opentelemetry.io/collector/pdata/pprofile v0.128.0 + go.opentelemetry.io/collector/receiver v1.34.0 + go.opentelemetry.io/collector/receiver/xreceiver v0.0.0-00010101000000-000000000000 go.uber.org/multierr v1.11.0 golang.org/x/time v0.11.0 google.golang.org/grpc v1.73.0 @@ -24,10 +27,10 @@ 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.34.0 // indirect go.opentelemetry.io/collector/extension v1.34.0 // indirect go.opentelemetry.io/collector/featuregate v1.34.0 // indirect go.opentelemetry.io/collector/internal/telemetry v0.128.0 // indirect + go.opentelemetry.io/collector/pipeline v0.128.0 // indirect go.opentelemetry.io/contrib/bridges/otelzap v0.11.0 // indirect go.opentelemetry.io/otel v1.36.0 // indirect go.opentelemetry.io/otel/log v0.12.2 // indirect diff --git a/extension/extensionlimiter/go.sum b/extension/extensionlimiter/go.sum index 08fd617af4d..4b826c4b806 100644 --- a/extension/extensionlimiter/go.sum +++ b/extension/extensionlimiter/go.sum @@ -36,6 +36,8 @@ 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/consumertest v0.128.0 h1:x50GB0I/QvU3sQuNCap5z/P2cnq2yHoRJ/8awkiT87w= +go.opentelemetry.io/collector/consumer/consumertest v0.128.0/go.mod h1:Wb3IAbMY/DOIwJPy81PuBiW2GnKoNIz4THE7wfJwovE= 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/memorylimiterextension/memorylimiter.go b/extension/memorylimiterextension/memorylimiter.go index b6e20ad62d5..f9a92eb3fa4 100644 --- a/extension/memorylimiterextension/memorylimiter.go +++ b/extension/memorylimiterextension/memorylimiter.go @@ -55,7 +55,7 @@ func (ml *memoryLimiterExtension) GetRateLimiter( if ml.MustRefuse() { return nil, ErrMustRefuse } - return extensionlimiter.NewNopRateReservation(), nil + return extensionlimiter.NewRateReservationImpl(nil, nil), nil }), nil } diff --git a/receiver/otlpreceiver/config_test.go b/receiver/otlpreceiver/config_test.go index 8247a7bb882..5b34e32eeee 100644 --- a/receiver/otlpreceiver/config_test.go +++ b/receiver/otlpreceiver/config_test.go @@ -15,6 +15,7 @@ import ( "go.opentelemetry.io/collector/config/configauth" "go.opentelemetry.io/collector/config/configgrpc" "go.opentelemetry.io/collector/config/confighttp" + "go.opentelemetry.io/collector/config/configmiddleware" "go.opentelemetry.io/collector/config/confignet" "go.opentelemetry.io/collector/config/configopaque" "go.opentelemetry.io/collector/config/configoptional" @@ -97,6 +98,27 @@ func TestUnmarshalConfigOnlyHTTPEmptyMap(t *testing.T) { assert.Equal(t, defaultOnlyHTTP, cfg) } +func TestUnmarshalLimiterConfig(t *testing.T) { + cm, err := confmaptest.LoadConf(filepath.Join("testdata", "limiters.yaml")) + require.NoError(t, err) + factory := NewFactory() + cfg := factory.CreateDefaultConfig().(*Config) + require.NoError(t, cm.Unmarshal(&cfg)) + + assert.Equal(t, cfg.Protocols.GRPC.Get().Middlewares, []configmiddleware.Config{ + configmiddleware.Config(component.MustNewIDWithName("one", "a")), + configmiddleware.Config(component.MustNewID("two")), + }) + assert.Equal(t, cfg.Protocols.HTTP.Get().ServerConfig.Middlewares, []configmiddleware.Config{ + configmiddleware.Config(component.MustNewIDWithName("three", "b")), + configmiddleware.Config(component.MustNewID("four")), + }) + assert.Equal(t, cfg.LimiterConfig.RequestBytes, component.MustNewIDWithName("five", "c")) + assert.Equal(t, cfg.LimiterConfig.RequestItems, component.MustNewID("six")) + assert.Equal(t, cfg.LimiterConfig.RequestCount, component.MustNewID("seven")) + +} + func TestUnmarshalConfig(t *testing.T) { cm, err := confmaptest.LoadConf(filepath.Join("testdata", "config.yaml")) require.NoError(t, err) diff --git a/receiver/otlpreceiver/testdata/limiters.yaml b/receiver/otlpreceiver/testdata/limiters.yaml new file mode 100644 index 00000000000..9cdb34f3845 --- /dev/null +++ b/receiver/otlpreceiver/testdata/limiters.yaml @@ -0,0 +1,15 @@ +# The following entry initializes the default OTLP receiver. +# The full name of this receiver is `otlp` and can be referenced in pipelines by 'otlp'. +protocols: + grpc: + middleware: + - one/a + - two + http: + middleware: + - three/b + - four +limiters: + request_bytes: five/c + request_items: six + request_count: seven From 3ec8e191711889e120aaecf1eb124d890d7f1a02 Mon Sep 17 00:00:00 2001 From: Joshua MacDonald Date: Thu, 19 Jun 2025 17:32:33 -0700 Subject: [PATCH 56/62] network bytes in pipeline (for reuse --- extension/extensionlimiter/README.md | 347 +++++++----------- .../consumerlimiter/consumerlimiter.go | 8 + 2 files changed, 149 insertions(+), 206 deletions(-) diff --git a/extension/extensionlimiter/README.md b/extension/extensionlimiter/README.md index 9916ac619e9..a60257213dd 100644 --- a/extension/extensionlimiter/README.md +++ b/extension/extensionlimiter/README.md @@ -9,175 +9,101 @@ through middleware and/or directly by pipeline components. ## Overview -This package defines three foundational limiter **kinds**, each with -similar but distinct interfaces. A limiter extension can be either a -simple "saturation" checker (defined below), or it extends the simple -checker interface with a weight-based interface: +This package defines two foundational limiter **kinds**, with similar +but distinct interfaces. A limiter extension can be: -- **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. - **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: +Requests are quantified with an integer value and identified by +**weight key**, indicating the type of quantity being measured and +limited. There are currently four weight keys with a standard +definition: 1. Network bytes (compressed) 2. Request count 3. Request items 4. Request bytes (uncompressed) - - -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. - -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 `CheckSaturation` before making individual requests with the limiter. - -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: - -- a mechanism to wait for the limit -- a mechanism to cancel or release the request. - -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). - -All limiter extensions: - -- MUST implement the `SaturationCheckerProvider` interface -- MUST NOT implement both the `ResourceLimiterProvider` and the `RateLimiterProvider` interfaces - -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. Limiters are automatically initialized as middleware via -`configmiddleware`. The original garbage-collector state-based -limiter can be found in -[`../memorylimiterextension`](../memorylimiterextension/README.md). - -## Recommendations - -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. - -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`. - -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 -`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 `SaturationCheckerToResourceLimiterProvider` -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. - -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 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 an error instead. Blocking adapters are -provided for callers including the `LimiterWrapper`. - -### Limiter saturation - -Rate and resource limiter providers have a `GetSaturationChecker` method to -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. - -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 -to avoid cases of limiter saturation. - -### 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 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. - -When using the low-level interfaces directly, limits SHOULD be applied -before creating new concurrent work. +## Early-as-possible application + +Limiter extensions should be used as early as possible in a +pipeline. There are two automatic ways that receivers can integrate +with rate limiters: + +- Middleware application: rate limiters are automatically recognized + in the list of middleware. Middleware supports HTTP and gRPC, client + and server, unary and streaming cases. Middleware automatically + implements request bytes and request count limits. +- Consumer application: rate limiters can be applied before the next + consumer in the pipeline, using standard `consumerlimiter.LimiterConfig` + configuration. + +Limiters should be applied, if possible, before work on a request +begins. Work that is done before a limit is requested is subject to +loss, in case the limiter causes failure. + +Limiters are not always applied in receivers, but all receivers should +support limiters through middleware and/or `consumerlimiter`. + +## Delay-the-caller application + +Limiters should be applied so that they delay the caller. This is an +important case of the early-as-possible rule: limit requests should be +made before returning control, in order to slow the process that is +contributing to the limit. + +In order to support delaying the caller in complex scenarios, +non-blocking interfaces are provided for each of the limiter +interfaces. Non-blocking APIs allow callers to delay the caller while +requesting the limit and then to perform their work asynchronously. + +The `limiterhelper.Wrapper` limiter interface is provided which +simplifies the application of limits to a scoped callback, making it +easy to use a blocking limit request. + +## Failure options + +Limiters at their discretion can block or fail requests that would +exceed a limit. The decision may influenced by limiter configuration +(e.g., burst, maximum wait parameters) and/or the deadline of the +request context. When the delay is small, it is usually beneficial to +wait instead of failing because (a) avoids the wasted effort (e.g., +re-transmitting data), (b) delays the caller for the effect of +back-pressure. + +When a limiter returns failure, the client should return a +protocol-specific failure code indicating resource exhaustion. The +recognized resource exhaustion codes are HTTP 429 and gRPC +RESOURCE_EXHAUSTED. Receivers should follow protocol-specific +recommendations, which for +[OTLP](https://opentelemetry.io/docs/specs/otlp/) includes returning a +`RetryDelay` parameter. + +If the limit request results in waiting, limiters should delay to +allow the request to proceed, however they should give up and return +at some point. As a recommendation, receivers and other components +should allow requests to wait up to configurable fraction of their +deadline. If the request cannot enter a pipeline before for example +half of its deadline, return failure instead of allowing it to +proceed. + +## Limiter configuration + +Limiters are configurable the same way middleware are configured, by +referring to the name of the extension component. ### Built-in limiters #### 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. +The `memorylimiterextension` gives access to an internal component +named `MemoryLimiter` with an interface named `MustDeny()`. +Components can call this component directly, however when configured +as a limiter extension, this component is modeled as a `RateLimiter` +that is on or off based on the current result of `MustDeny`. #### RateLimiter @@ -188,8 +114,6 @@ rate limiters are parameterized by two numbers: - `limit` (float64): the maximum frequency of weight-units per second - `burst` (uint64): the "burst" value of the Token-bucket algorithm. -The rate limiter is saturated when there is no burst available. - #### ResourceLimiter A built-in helper implementation of the ResourceLimiter interface is @@ -199,38 +123,69 @@ 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 -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): +Limiters applied through middleware and/or via receiver-level +limiters. Middleware limiters are automatically configured using +`configgrpc` or `confighttp`. Receivers can add support at the factory +level using helpers in `consumerlimiter`. + +For the OTLP receiver (e.g., with three `ratelimiter` extensions and a +`resourcelimiter` extension): ```yaml extensions: ratelimiter/limit_for_grpc: - # rate limiter settings for gRPC - ratelimiter/limit_for_grpc: - # rate limiter settings for HTTP + network_bytes: ... + + ratelimiter/limit_for_http: + network_bytes: ... + + ratelimiter/limit_items: + request_items: ... + + resourcelimiter/limit_memory: + request_bytes: ... receivers: otlp: protocols: grpc: - middlewares: + middleware: - ratelimiter/limit_for_grpc + - resourcelimiter/limit_memory http: - middlewares: + middleware: - ratelimiter/limit_for_http + - resourcelimiter/limit_memory + limiters: + request_items: ratelimiter/limit_items ``` -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. +Note that in general, middleware components do not have access to the +number of items in a request, so users are directed to receiver-level +`limiters` configuration to limit items. The OTLP receiver uses the +helper library to configure its factory: + +```golang +func NewFactory() receiver.Factory { + return consumerlimiter.NewLimitedFactory( + receiver.NewFactory( + metadata.Type, + createDefaultConfig, + receiver.WithTraces(createTraces, metadata.TracesStability), + receiver.WithMetrics(createMetrics, metadata.MetricsStability), + receiver.WithLogs(createLog, metadata.LogsStability), + receiver.WithProfiles(createProfiles, metadata.ProfilesStability), + ), + func(cfg component.Config) consumerlimiter.LimiterConfig { + return cfg.(*Config).LimiterConfig + }, + ) +} +``` #### HTTP metrics scraper @@ -242,8 +197,8 @@ automatically configures network bytes and request count limits: receivers: scraper: http: - middlewares: - - ratelimiter/scraper + limiters: + request_ratelimiter/scraper ``` Limiter extensions are derived from a host, a middlewares list, and a @@ -253,45 +208,25 @@ level, it may be added via `receiver.NewFactory` using ```golang func NewFactory() receiver.Factory { - return xreceiver.NewFactory( - metadata.Type, - createDefaultConfig, - xreceiver.WithMetrics(createMetrics, metadata.MetricsStability), - xreceiver.WithLimiters(getLimiters), - ) + return consumerlimiter.NewLimitedFactory( + receiver.NewFactory( + metadata.Type, + createDefaultConfig, + xreceiver.WithMetrics(createMetrics, metadata.MetricsStability), + ), + func(cfg component.Config) consumerlimiter.LimiterConfig { + cpy := cfg.(*Config).LimiterConfig + // RequestCount checked in scraper loop, reset it here: + cpy.RequestCount = component.ID{} + return + }, + ) } ``` -Here, `getLimiters` is a function to get the effective -`[]configmiddleware.Config` and derive pipeline consumers using -`limiterhelper` adapters. - -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 extensions from host and list of middleware. - providers, err := configmiddleware.GetSaturationCheckers( - host, cfg.Middlewares) - if err != nil { ... } - - // 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 CheckSaturation() functionality. - limitKeys := extensionlimiter.StandardNotMiddlewareKeys() - s.nextMetrics, err = limiterhelper.NewLimitedMetrics( - s.nextMetrics, limitKeys, s.limiterProvider) - if err != nil { ... } - - // Compute the saturation checker from the middlewares for use before scrapes. - s.limiter, err := s.limiterProvider.GetSaturationChecker(host, middlewares) - if err != nil { ... } -``` +Here, the second argument to `consumerlimiter.NewLimitedFactory` +is a function providing the `LimiterConfig` struct to be applied +automatically before the next consumer in the pipeline. In the scraper loop, use `CheckSaturation` before starting a scrape: diff --git a/extension/extensionlimiter/limiterhelper/consumerlimiter/consumerlimiter.go b/extension/extensionlimiter/limiterhelper/consumerlimiter/consumerlimiter.go index fe9da73c231..189ca00c2e8 100644 --- a/extension/extensionlimiter/limiterhelper/consumerlimiter/consumerlimiter.go +++ b/extension/extensionlimiter/limiterhelper/consumerlimiter/consumerlimiter.go @@ -26,6 +26,7 @@ import ( var ( ErrLimiterNotFound = errors.New("limiter not found") ErrNotALimiter = errors.New("not a limiter") + ErrLimiterUnsupported = errors.New("limiter unsupported") ) // Config is the standard pipeline configuration for limiting a @@ -40,6 +41,9 @@ type LimiterConfig struct { RequestCount component.ID `mapstructure:"request_count"` RequestItems component.ID `mapstructure:"request_items"` RequestBytes component.ID `mapstructure:"request_bytes"` + + // Not always available. Typically must be a RateLimiter. + NetworkBytes component.ID `mapstructure:"network_bytes"` } // capable is an internal interface describing common features of a @@ -189,6 +193,10 @@ func (l *limitedReceiver[P, C, R, T]) Capabilities() consumer.Capabilities { func (l *limitedReceiver[P, C, R, T]) Start(ctx context.Context, host component.Host) error { var unset component.ID + if name := l.cfg.NetworkBytes; name != unset { + return fmt.Errorf("%w: network bytes unavailable: %s", ErrLimiterUnsupported, name) + } + var err1, err2, err3 error if name := l.cfg.RequestBytes; name != unset { l.next, err1 = l.limitOne( From 414b176e6f4fab6b79d8ae9d5bf6c80a0d59ce92 Mon Sep 17 00:00:00 2001 From: Joshua MacDonald Date: Thu, 19 Jun 2025 17:58:41 -0700 Subject: [PATCH 57/62] grpc uncompressed bytes --- .../consumerlimiter/consumerlimiter.go | 7 --- .../limiterhelper/grpc/grpclimiter.go | 61 +++++++++++++++---- 2 files changed, 48 insertions(+), 20 deletions(-) diff --git a/extension/extensionlimiter/limiterhelper/consumerlimiter/consumerlimiter.go b/extension/extensionlimiter/limiterhelper/consumerlimiter/consumerlimiter.go index 189ca00c2e8..5b2b078914b 100644 --- a/extension/extensionlimiter/limiterhelper/consumerlimiter/consumerlimiter.go +++ b/extension/extensionlimiter/limiterhelper/consumerlimiter/consumerlimiter.go @@ -41,9 +41,6 @@ type LimiterConfig struct { RequestCount component.ID `mapstructure:"request_count"` RequestItems component.ID `mapstructure:"request_items"` RequestBytes component.ID `mapstructure:"request_bytes"` - - // Not always available. Typically must be a RateLimiter. - NetworkBytes component.ID `mapstructure:"network_bytes"` } // capable is an internal interface describing common features of a @@ -193,10 +190,6 @@ func (l *limitedReceiver[P, C, R, T]) Capabilities() consumer.Capabilities { func (l *limitedReceiver[P, C, R, T]) Start(ctx context.Context, host component.Host) error { var unset component.ID - if name := l.cfg.NetworkBytes; name != unset { - return fmt.Errorf("%w: network bytes unavailable: %s", ErrLimiterUnsupported, name) - } - var err1, err2, err3 error if name := l.cfg.RequestBytes; name != unset { l.next, err1 = l.limitOne( diff --git a/extension/extensionlimiter/limiterhelper/grpc/grpclimiter.go b/extension/extensionlimiter/limiterhelper/grpc/grpclimiter.go index 8119eb36237..2e6f7c9c11e 100644 --- a/extension/extensionlimiter/limiterhelper/grpc/grpclimiter.go +++ b/extension/extensionlimiter/limiterhelper/grpc/grpclimiter.go @@ -6,6 +6,7 @@ package grpclimiter import ( "context" "sync" + "time" "go.uber.org/multierr" "google.golang.org/grpc" @@ -56,8 +57,10 @@ func NewClientLimiter(ext extensionlimiter.AnyProvider) (extensionmiddleware.GRP return nil, err } requestLimiter, err3 := wp.GetWrapper(extensionlimiter.WeightKeyRequestCount) - bytesLimiter, err4 := rp.GetRateLimiter(extensionlimiter.WeightKeyNetworkBytes) - if err := multierr.Append(err3, err4); err != nil { + compressedLimiter, err4 := rp.GetRateLimiter(extensionlimiter.WeightKeyNetworkBytes) + uncompressedLimiter, err5 := rp.GetRateLimiter(extensionlimiter.WeightKeyRequestBytes) + + if err := multierr.Append(err3, multierr.Append(err4, err5)); err != nil { return nil, err } @@ -98,10 +101,11 @@ func NewClientLimiter(ext extensionlimiter.AnyProvider) (extensionmiddleware.GRP }), ) } - if bytesLimiter != nil { + if compressedLimiter != nil || uncompressedLimiter != nil { gopts = append(gopts, grpc.WithStatsHandler( &limiterStatsHandler{ - limiter: limiterhelper.NewBlockingRateLimiter(bytesLimiter), + compressedLimiter: compressedLimiter, + uncompressedLimiter: uncompressedLimiter, isClient: true, })) } @@ -117,8 +121,9 @@ func NewServerLimiter(ext extensionlimiter.AnyProvider) (extensionmiddleware.GRP return nil, err } requestLimiter, err3 := wp.GetWrapper(extensionlimiter.WeightKeyRequestCount) - bytesLimiter, err4 := rp.GetRateLimiter(extensionlimiter.WeightKeyNetworkBytes) - if err := multierr.Append(err3, err4); err != nil { + compressedLimiter, err4 := rp.GetRateLimiter(extensionlimiter.WeightKeyNetworkBytes) + uncompressedLimiter, err5 := rp.GetRateLimiter(extensionlimiter.WeightKeyRequestBytes) + if err := multierr.Append(err3, multierr.Append(err4, err5)); err != nil { return nil, err } @@ -154,10 +159,11 @@ func NewServerLimiter(ext extensionlimiter.AnyProvider) (extensionmiddleware.GRP }), ) } - if bytesLimiter != nil { + if compressedLimiter != nil || uncompressedLimiter != nil { gopts = append(gopts, grpc.StatsHandler( &limiterStatsHandler{ - limiter: limiterhelper.NewBlockingRateLimiter(bytesLimiter), + compressedLimiter: compressedLimiter, + uncompressedLimiter: uncompressedLimiter, isClient: false, })) } @@ -169,7 +175,8 @@ func NewServerLimiter(ext extensionlimiter.AnyProvider) (extensionmiddleware.GRP // limiterStatsHandler implements the stats.Handler interface for rate limiting. type limiterStatsHandler struct { - limiter limiterhelper.BlockingRateLimiter + compressedLimiter extensionlimiter.RateLimiter + uncompressedLimiter extensionlimiter.RateLimiter isClient bool } @@ -180,28 +187,56 @@ func (h *limiterStatsHandler) TagRPC(ctx context.Context, _ *stats.RPCTagInfo) c func (h *limiterStatsHandler) HandleRPC(ctx context.Context, s stats.RPCStats) { // Check for payload messages to apply network byte rate limiting var wireBytes int + var reqBytes int switch payload := s.(type) { case *stats.InPayload: // Server receiving payload (or client receiving response) if !h.isClient { wireBytes = payload.WireLength + reqBytes = payload.Length } case *stats.OutPayload: // Client sending payload (or server sending response) if h.isClient { wireBytes = payload.WireLength + reqBytes = payload.Length } default: // Not a payload message, no rate limiting to apply return } - if wireBytes == 0 { - return + // Implement 1 or 2 rate limits in parallel + var err1, err2 error + var res1, res2 extensionlimiter.RateReservation + + if wireBytes != 0 { + res1, err1 = h.compressedLimiter.ReserveRate(ctx, wireBytes) + } + if reqBytes != 0 { + res2, err2 = h.uncompressedLimiter.ReserveRate(ctx, reqBytes) + } + + var wait1, wait2 time.Duration + + if res1 != nil { + wait1 = res1.WaitTime() + defer res1.Cancel() } - // Apply rate limiting based on network bytes - if err := h.limiter.WaitFor(ctx, wireBytes); err != nil { + if res2 != nil { + wait2 = res2.WaitTime() + defer res2.Cancel() + } + + if err := multierr.Append(err1, err2); err != nil { setRateLimiterError(ctx, err) + return + } + + wait := max(wait1, wait2) + select { + case <-ctx.Done(): + case <-time.After(wait): } } From 6f98acec6283676e6735aaf11a52b0f317f71def Mon Sep 17 00:00:00 2001 From: Joshua MacDonald Date: Thu, 19 Jun 2025 17:59:41 -0700 Subject: [PATCH 58/62] http is a todo --- extension/extensionlimiter/limiterhelper/http/httplimiter.go | 3 +++ 1 file changed, 3 insertions(+) diff --git a/extension/extensionlimiter/limiterhelper/http/httplimiter.go b/extension/extensionlimiter/limiterhelper/http/httplimiter.go index d3a736fac25..94a5402e661 100644 --- a/extension/extensionlimiter/limiterhelper/http/httplimiter.go +++ b/extension/extensionlimiter/limiterhelper/http/httplimiter.go @@ -16,6 +16,9 @@ import ( "go.opentelemetry.io/collector/extension/extensionmiddleware/extensionmiddlewaretest" ) +// TODO: network bytes can be implemented after compression middleware, requires +// integration with confighttp. + func NewClientLimiter(ext extensionlimiter.AnyProvider) (extensionmiddleware.HTTPClient, error) { wp, err1 := limiterhelper.AnyToWrapperProvider(ext) rp, err2 := limiterhelper.AnyToRateLimiterProvider(ext) From 1274e19e5eaddf3b0ac49886cf239320c6db481b Mon Sep 17 00:00:00 2001 From: Joshua MacDonald Date: Fri, 20 Jun 2025 14:18:58 -0700 Subject: [PATCH 59/62] document this --- docs/rfcs/functional-composition-pattern.md | 354 ++++++++++++++++++++ 1 file changed, 354 insertions(+) create mode 100644 docs/rfcs/functional-composition-pattern.md diff --git a/docs/rfcs/functional-composition-pattern.md b/docs/rfcs/functional-composition-pattern.md new file mode 100644 index 00000000000..78e2d42f835 --- /dev/null +++ b/docs/rfcs/functional-composition-pattern.md @@ -0,0 +1,354 @@ +# Functional Composition Pattern + +## Overview + +This codebase uses a **Functional Composition** pattern for its core +interfaces. This pattern decomposes individual methods from single- +and multi-method interface types into individual function types, then +recomposes them into a concrete implementations, which may be sealed +(or not), and exported (or not) depending on the context. + +This approach provides flexibility, testability, and safe interface +evolution. + +## Core Principles + +### 1. Decompose Interfaces into Function Types + +Instead of implementing interfaces directly on structs, we create function types for each method: + +```go +// Interface definition +type RateReservation interface { + WaitTime() time.Duration + Cancel() +} + +// Function types for each method +type WaitTimeFunc func() time.Duration +type CancelFunc func() + +// Function types implement their corresponding methods +func (f WaitTimeFunc) WaitTime() time.Duration { + if f == nil { + return 0 + } + return f() +} + +func (f CancelFunc) Cancel() { + if f == nil { + return + } + f() +} +``` + +Note that each function type always implements no-op functionality, +such that passing `nil` corresponds with the no-op implementation for +a that function. + +### 2. Compose Functions into Interface Implementations + +Create concrete implementations embed the corresponding function type: + +```go +type rateReservationImpl struct { + WaitTimeFunc + CancelFunc +} + +// Constructor for an instance +func NewRateReservationImpl(wf WaitTimeFunc, cf CancelFunc) RateReservation { + return rateReservationImpl{ + WaitTimeFunc: wf, + CancelFunc: cf, + } +} +``` + +Note that because the argument to NewXyzImpl() is a list of +`<...>Func` objects, and the Go compiler automatically converts func +values, so we can write: + +```go + return NewRateReservationImpl( + // Wait time 1 second + func() time.Duration { return time.Second }, + // Cancel is a no-op. + nil, + ) +``` + +For constant values and enumerated types, define a `Self()` method to +act as the corresponding function implementation for constant values. + +``` +// Self returns itself. +func (t Type) Self() Type { + return t +} + +// TypeFunc is ... +type TypeFunc func() Type + +// Type gets the type of the component created by this factory. +func (f TypeFunc) Type() Type { + if f == nil { + } + return f() +} +``` + +with usage like: + +```go + // Construct a factory with a new default Config: + factory := sometype.NewFactory() + cfg := factory.CreateDefaultConfig() + // modify the config object + return NewFactoryImpl(factory.Type().Self, cfg.Self) Factory { +``` + +### 3. Use Constructors for Interface Values + +For public interfaces provide constructor functions rather than +exposing concrete types: + +```go +// Good: Constructor function +func NewRateLimiterImpl(f ReserveRateFunc) RateLimiter { + return rateLimiterImpl{ReserveRateFunc: f} +} + +// Avoid: Direct struct instantiation +// rateLimiterImpl{ReserveRateFunc: f} // Don't do this +``` + +Rarely should the implementation type be exposed. This pattern can be +combined with the functional option pattern: + +``` +// Setup the logging implementation functions +func WithLogs(createLogs CreateLogsFunc, sl component.StabilityLevel) FactoryOption { + return factoryOptionFunc(func(o *factoryImpl, cfgType component.Type) { + o.LogsStabilityFunc = sl.Self + o.CreateLogsFunc = ... + }) +} + +// Accept options to configure various aspects of the interface +func NewFactory(cfgType component.Type, createDefaultConfig component.CreateDefaultConfigFunc, options ...FactoryOption) Factory { + f := factoryImpl{ + Factory: component.NewFactoryImpl(cfgType.Self, createDefaultConfig), + } + for _, opt := range options { + opt.applyOption(&f, cfgType) + } + return f +} +``` + +## Rationale + +### Flexibility and Composition + +This pattern enables composition scenarios by making it easy to +compose and decompose interface values. + +```go +// Transform existing factories with cross-cutting concerns +func NewLimitedFactory(fact receiver.Factory, cfgf LimiterConfigurator) receiver.Factory { + return receiver.NewFactoryImpl( + fact.Type, + fact.CreateDefaultConfig, + receiver.CreateTracesFunc(limitReceiver(fact.CreateTraces, traceTraits{}, cfgf)), + fact.TracesStability, + receiver.CreateMetricsFunc(limitReceiver(fact.CreateMetrics, metricTraits{}, cfgf)), + fact.MetricsStability, + receiver.CreateLogsFunc(limitReceiver(fact.CreateLogs, logTraits{}, cfgf)), + fact.LogsStability, + ) +} +``` + +This is sometimes called aspect-oriented programming, for example to +add logging to an existing function: + +```go +func addLogging(f ReserveRateFunc) ReserveRateFunc { + return func(ctx context.Context, n int) (RateReservation, error) { + log.Printf("Reserving rate for %d", n) + return f(ctx, n) + } +} +``` + +### Safe Interface Evolution + +Using a private method allows sealing the interface type, which forces +external users to use functional constructor methods. This allows +interfaces to evolve safely because users are forced to use +constructor functions. + +```go +type RateLimiter interface { + ReserveRate(context.Context, int) (RateReservation, error) + + // Can add new methods without breaking existing code + private() // Prevents external implementations +} +``` + +### Enhanced Testability + +Individual methods can be tested independently: + +```go +func TestWaitTimeFunction(t *testing.T) { + waitFn := WaitTimeFunc(func() time.Duration { return time.Second }) + assert.Equal(t, time.Second, waitFn.WaitTime()) +} +``` + +## Implementation Guidelines + +### 1. Interface Design + +- Define interfaces with clear method signatures +- Include a `private()` method if you need to control implementations +- Keep interfaces focused and cohesive + +### 2. Function Type Naming + +- Use `Func` naming convention +- Ensure function signatures match the interface method exactly +- Always implement the corresponding interface method on the function type + +### 3. Nil Handling + +Always handle nil function types gracefully to act as a no-op implementation. + +```go +func (f ReserveRateFunc) ReserveRate(ctx context.Context, value int) (RateReservation, error) { + if f == nil { + return NewRateReservationImpl(nil, nil), nil // No-op implementation + } + return f(ctx, value) +} +``` + +### 4. Constructor Patterns + +Follow consistent constructor naming for building an implementation +of each interface: + +```go +// For interfaces: NewImpl +func NewRateLimiterImpl(f ReserveRateFunc) RateLimiter +``` + +Follow consistent type naming for each method-function type: + +```go +// For function types: Func +type ReserveRateFunc func(context.Context, int) (RateReservation, error) +``` + +### 5. Implementation Structs + +- Use lowercase names for implementation structs +- Embed function types directly +- Implement private methods for sealing + +```go +type rateLimiterImpl struct { + ReserveRateFunc +} + +func (rateLimiterImpl) private() {} +``` + +## When to Use This Pattern + +### Appropriate Use Cases + +- **Interfaces with single or multiple methods** that benefit from composition +- **Cross-cutting concerns** that need to be applied selectively +- **Interface evolution** where you need to add methods over time +- **Factory patterns** where you're assembling behavior from components +- **Middleware/decorator scenarios** where you're transforming behavior + +### When to Avoid + +- **Stateful objects** where methods need to share significant state +- **Performance-critical code** where function call overhead matters +- **Simple implementations** where the pattern adds unnecessary complexity + +## Examples + +### Simple Rate Limiter + +```go +type RateLimiter interface { + ReserveRate(context.Context, int) (RateReservation, error) + private() +} + +type ReserveRateFunc func(context.Context, int) (RateReservation, error) + +func (f ReserveRateFunc) ReserveRate(ctx context.Context, value int) (RateReservation, error) { + if f == nil { + return NewRateReservationImpl(nil, nil), nil + } + return f(ctx, value) +} + +func NewRateLimiterImpl(f ReserveRateFunc) RateLimiter { + return rateLimiterImpl{ReserveRateFunc: f} +} +``` + +### Multi-Method Interface + +```go +type RateReservation interface { + WaitTime() time.Duration + Cancel() + private() +} + +type WaitTimeFunc func() time.Duration +type CancelFunc func() + +func (f WaitTimeFunc) WaitTime() time.Duration { + if f == nil { return 0 } + return f() +} + +func (f CancelFunc) Cancel() { + if f == nil { return } + f() +} + +func NewRateReservationImpl(wf WaitTimeFunc, cf CancelFunc) RateReservation { + return rateReservationImpl{ + WaitTimeFunc: wf, + CancelFunc: cf, + } +} +``` + +## Benefits Summary + +1. **Compositional**: Build complex interfaces from simple function components +2. **Flexible**: Transform individual methods without affecting others +3. **Testable**: Test each method implementation independently +4. **Evolvable**: Add methods to interfaces without breaking existing code +5. **Type-Safe**: Maintain compile-time safety while providing runtime flexibility + +## Common Pitfalls + +1. **Nil Panics**: Always handle nil function types in method implementations +2. **Performance**: Consider the function call overhead for high-frequency operations +3. **Complexity**: Don't use this pattern when a simple struct implementation suffices From ec75814f7d2ce29548aa46bd9a15ba2efa4c5fe0 Mon Sep 17 00:00:00 2001 From: Joshua MacDonald Date: Fri, 20 Jun 2025 14:31:19 -0700 Subject: [PATCH 60/62] edit pattern doc --- docs/rfcs/functional-composition-pattern.md | 99 ++++----------------- 1 file changed, 19 insertions(+), 80 deletions(-) diff --git a/docs/rfcs/functional-composition-pattern.md b/docs/rfcs/functional-composition-pattern.md index 78e2d42f835..b45fdb323ad 100644 --- a/docs/rfcs/functional-composition-pattern.md +++ b/docs/rfcs/functional-composition-pattern.md @@ -81,9 +81,9 @@ values, so we can write: ``` For constant values and enumerated types, define a `Self()` method to -act as the corresponding function implementation for constant values. +act as the corresponding function implementation: -``` +```go // Self returns itself. func (t Type) Self() Type { return t @@ -100,20 +100,20 @@ func (f TypeFunc) Type() Type { } ``` -with usage like: +For example, we can decompose, modify, and recompose a +`component.Factory`: ```go // Construct a factory with a new default Config: factory := sometype.NewFactory() cfg := factory.CreateDefaultConfig() // modify the config object - return NewFactoryImpl(factory.Type().Self, cfg.Self) Factory { + return NewFactoryImpl(factory.Type().Self, cfg.Self) ``` ### 3. Use Constructors for Interface Values -For public interfaces provide constructor functions rather than -exposing concrete types: +Provide constructor functions rather than exposing concrete types: ```go // Good: Constructor function @@ -126,14 +126,14 @@ func NewRateLimiterImpl(f ReserveRateFunc) RateLimiter { ``` Rarely should the implementation type be exposed. This pattern can be -combined with the functional option pattern: +combined with the Functional Option pattern (from `receiver/receiver.go`): -``` -// Setup the logging implementation functions +```go +// Setup optional logging-related functions func WithLogs(createLogs CreateLogsFunc, sl component.StabilityLevel) FactoryOption { return factoryOptionFunc(func(o *factoryImpl, cfgType component.Type) { + o.CreateLogsFunc = createLogs o.LogsStabilityFunc = sl.Self - o.CreateLogsFunc = ... }) } @@ -257,11 +257,18 @@ type ReserveRateFunc func(context.Context, int) (RateReservation, error) ### 5. Implementation Structs -- Use lowercase names for implementation structs +- Use unexported names for implementation structs - Embed function types directly -- Implement private methods for sealing +- Implement private methods for sealing the interface ```go +type RateLimiter interface { + // ... + + // Must use functional constructors outside this package + private() +} + type rateLimiterImpl struct { ReserveRateFunc } @@ -284,71 +291,3 @@ func (rateLimiterImpl) private() {} - **Stateful objects** where methods need to share significant state - **Performance-critical code** where function call overhead matters - **Simple implementations** where the pattern adds unnecessary complexity - -## Examples - -### Simple Rate Limiter - -```go -type RateLimiter interface { - ReserveRate(context.Context, int) (RateReservation, error) - private() -} - -type ReserveRateFunc func(context.Context, int) (RateReservation, error) - -func (f ReserveRateFunc) ReserveRate(ctx context.Context, value int) (RateReservation, error) { - if f == nil { - return NewRateReservationImpl(nil, nil), nil - } - return f(ctx, value) -} - -func NewRateLimiterImpl(f ReserveRateFunc) RateLimiter { - return rateLimiterImpl{ReserveRateFunc: f} -} -``` - -### Multi-Method Interface - -```go -type RateReservation interface { - WaitTime() time.Duration - Cancel() - private() -} - -type WaitTimeFunc func() time.Duration -type CancelFunc func() - -func (f WaitTimeFunc) WaitTime() time.Duration { - if f == nil { return 0 } - return f() -} - -func (f CancelFunc) Cancel() { - if f == nil { return } - f() -} - -func NewRateReservationImpl(wf WaitTimeFunc, cf CancelFunc) RateReservation { - return rateReservationImpl{ - WaitTimeFunc: wf, - CancelFunc: cf, - } -} -``` - -## Benefits Summary - -1. **Compositional**: Build complex interfaces from simple function components -2. **Flexible**: Transform individual methods without affecting others -3. **Testable**: Test each method implementation independently -4. **Evolvable**: Add methods to interfaces without breaking existing code -5. **Type-Safe**: Maintain compile-time safety while providing runtime flexibility - -## Common Pitfalls - -1. **Nil Panics**: Always handle nil function types in method implementations -2. **Performance**: Consider the function call overhead for high-frequency operations -3. **Complexity**: Don't use this pattern when a simple struct implementation suffices From f4b40cf45542120cb7760f525f911980ae186978 Mon Sep 17 00:00:00 2001 From: Joshua MacDonald Date: Fri, 20 Jun 2025 15:26:55 -0700 Subject: [PATCH 61/62] readme --- extension/extensionlimiter/README.md | 281 +++++++++------------------ 1 file changed, 97 insertions(+), 184 deletions(-) diff --git a/extension/extensionlimiter/README.md b/extension/extensionlimiter/README.md index a60257213dd..6bac9ca6f0e 100644 --- a/extension/extensionlimiter/README.md +++ b/extension/extensionlimiter/README.md @@ -90,14 +90,9 @@ deadline. If the request cannot enter a pipeline before for example half of its deadline, return failure instead of allowing it to proceed. -## Limiter configuration - -Limiters are configurable the same way middleware are configured, by -referring to the name of the extension component. - ### Built-in limiters -#### MemoryLimiter +#### MemoryLimiter extension The `memorylimiterextension` gives access to an internal component named `MemoryLimiter` with an interface named `MustDeny()`. @@ -166,26 +161,7 @@ receivers: Note that in general, middleware components do not have access to the number of items in a request, so users are directed to receiver-level -`limiters` configuration to limit items. The OTLP receiver uses the -helper library to configure its factory: - -```golang -func NewFactory() receiver.Factory { - return consumerlimiter.NewLimitedFactory( - receiver.NewFactory( - metadata.Type, - createDefaultConfig, - receiver.WithTraces(createTraces, metadata.TracesStability), - receiver.WithMetrics(createMetrics, metadata.MetricsStability), - receiver.WithLogs(createLog, metadata.LogsStability), - receiver.WithProfiles(createProfiles, metadata.ProfilesStability), - ), - func(cfg component.Config) consumerlimiter.LimiterConfig { - return cfg.(*Config).LimiterConfig - }, - ) -} -``` +`limiters` configuration to limit items. #### HTTP metrics scraper @@ -195,127 +171,21 @@ automatically configures network bytes and request count limits: ```yaml receivers: - scraper: + httpscraper: http: - limiters: - request_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)`: - -```golang -func NewFactory() receiver.Factory { - return consumerlimiter.NewLimitedFactory( - receiver.NewFactory( - metadata.Type, - createDefaultConfig, - xreceiver.WithMetrics(createMetrics, metadata.MetricsStability), - ), - func(cfg component.Config) consumerlimiter.LimiterConfig { - cpy := cfg.(*Config).LimiterConfig - // RequestCount checked in scraper loop, reset it here: - cpy.RequestCount = component.ID{} - return - }, - ) -} -``` - -Here, the second argument to `consumerlimiter.NewLimitedFactory` -is a function providing the `LimiterConfig` struct to be applied -automatically before the next consumer in the pipeline. - -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.CheckSaturation(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: - -```yaml -receivers: - streamer: - grpc: - middlewares: - - ratelimiter/streamer -``` - -The receiver will check `s.limiter.CheckSaturation()` as above. In a stream, -a blocking limiter is used which blocks the stream (via -`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 -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.limiter.CheckSaturation(ctx) - if err != nil { ... } - - // The network bytes and request count limits are applied in middleware. - req, err := stream.Recv() - if err != nil { ... } - - // Allocate memory objects. - data, err := s.getLogs(ctx, req) - if err != nil { ... } - - // Non-blocking limiter call. - release, err := s.requestSizeLimiter.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) - - // Release the memory. - release() - - // Reply to the caller. - stream.Send(streamResponseFromConsumerError(err)) - } - } -} + middleware: + - ratelimiter/scraper_request_bytes + - compression/zstd + - ratelimiter/scraper_network_bytes + limiters: + request_count: ratelimiter/scraper_count + request_items: ratelimiter/scraper_items ``` ##### Data-dependent limits 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). - -Another option, shown below, is to use the non-blocking rate limiter +multiple distinct limiters, we can 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: @@ -324,6 +194,7 @@ 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 { + // For an individual resource, ... md := resourceToMetadata(rl.Resource()) reservation, err := p.limiter.ReserveRate(withMetadata(ctx, md)) if err != nil { @@ -373,48 +244,90 @@ interfaces. **NOTE: No options are implemented.** Potential options: Because the set of each of these is small, it is possible to pre-compute limiter instances for the cross product of configurations. -## Architecture - -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 "Limiters" - SaturationChecker["SaturationChecker"] - RateLimiter["RateLimiter"] - ResourceLimiter["ResourceLimiter"] - LimiterWrapper["LimiterWrapper"] - end - - subgraph "Providers" - SaturationCheckerProvider["SaturationCheckerProvider"] - RateLimiterProvider["RateLimiterProvider"] - ResourceLimiterProvider["ResourceLimiterProvider"] - LimiterWrapperProvider["LimiterWrapperProvider"] - end - - 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 - ResourceLimiterProvider -->|creates| ResourceLimiter - LimiterWrapperProvider -->|creates| LimiterWrapper - - LimiterWrapper -->|wraps/implements| SaturationChecker - RateLimiter -->|implements| SaturationChecker - ResourceLimiter -->|implements| SaturationChecker - - ResourceLimiter -->|substitution possible| RateLimiter - - LimiterWrapper -->|wraps| RateLimiter - LimiterWrapper -->|wraps| ResourceLimiter +##### Middleware and/or LimiterConfig + +The question is how we avoid double-count certain limits whether they +are implemented in middleware, through a factory, through custom +receiver code, or other. + +In the current limiter extension proposal, middleware references are +component IDs (without referce to weight key), while `LimiterConfig` +is a set of weight-specific limiter references. Users will be able to +double-count certain features when they user the same limiter +extension in both middleware and limiters configuration, unless we +help explicitly avoid this scnario. It could be accomplished using +context variables or start-time settings. + +##### Controlling middleware order + +While middleware order is a list of components, it is difficult for +users to reason about the order of application. To implement a +network-bytes or request-bytes limit, the limiter has to be configured +before or after the compression middleware. + +Today, HTTP middleware for compression is automatically inserted +although [it could become middleware +itself](https://github.com/open-telemetry/opentelemetry-collector/issues/13228). + +Since middlware references are simple identifiers (without weight +keys), additional help is needed to distinguish compressed bytes from +uncompressed bytes, especially for the HTTP cases. Potentially, +middleware can look at Transfer-Encoding or context.Context values +to distinguish these cases. + +#### Middleware component syntax + +In many discussions and documents, a number of authors have shown a +preference for simple component identifiers, without the leading `id:` +field syntax, which is to change + +```go +type Config struct { + ID component.ID `mapstructure:"id,omitempty"` +} ``` -TODO describe connection with -https://github.com/elastic/opentelemetry-collector-components/blob/main/processor/ratelimitprocessor/README.md +to: + +```go +type Config struct component.ID +``` + +This allows middleware to be listed as simple identifiers, + +```yaml +receivers: + otlp: + protocols: + grpc: + middleware: + - ratelimiter/1 + - ratelimiter/2 + limiters: + request_bytes: admissionlimiter/3 +``` + +##### Are built-in rate and resource limiters needed? + +The provided helper implementations are based in +`golang.org/x/time/rate` and +`collector-contrib/internal/otelarrow/admission2`. We could instead +create two extension implementations for these. The code is a hundred +lines or so each. + +The [Elastic rate limiter +processor](https://github.com/elastic/opentelemetry-collector-components/blob/main/processor/ratelimitprocessor/README.md) +would be a good contribution for the community. We are interested in +real-world features such as the ability to set `metadata_keys`. + +##### Instrumentation + +It is possible to extract Counter (RateLimiter) and UpDownCounter +(ResourceLimiter) instrumentation to convey the rates and totals +associated with each limiter. + +This should investigated along with investigation into the topics +raised above. Users will be well served if we are able to confidently +extract network-bytes, request-bytes, request-items, and request-count +information without double-counting and provide instrumentation +covering these variables. From f549b0af58068af70d382e7e1a0043594443df66 Mon Sep 17 00:00:00 2001 From: Joshua MacDonald Date: Fri, 20 Jun 2025 15:48:27 -0700 Subject: [PATCH 62/62] pattern --- docs/rfcs/functional-composition-pattern.md | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/docs/rfcs/functional-composition-pattern.md b/docs/rfcs/functional-composition-pattern.md index b45fdb323ad..1d2a40244ef 100644 --- a/docs/rfcs/functional-composition-pattern.md +++ b/docs/rfcs/functional-composition-pattern.md @@ -44,6 +44,13 @@ func (f CancelFunc) Cancel() { } ``` +Users of the `net/http` package will have seen this pattern +before. The `http.HandlerFunc` type can be seen as a prototype for +functional composition of HTTP handlers. Interestingly, the +single-method `http.RoundTripper` interface, which represents the same +interaction on the client-side, does not have a `RoundTripperFunc`. In +this codebase, the pattern is applied exstensively. + Note that each function type always implements no-op functionality, such that passing `nil` corresponds with the no-op implementation for a that function.