A sys event will be emitted when the controller is constructed, and if the rate limits are changed via a SIGHUP. The event contains the details of the rate limits for each combination of resource and action. This event can be used by operators to confirm the limits are set as they would expect based on the controller config.
226 lines
5.4 KiB
Go
226 lines
5.4 KiB
Go
// Copyright (c) HashiCorp, Inc.
|
|
// SPDX-License-Identifier: BUSL-1.1
|
|
|
|
package controller
|
|
|
|
import (
|
|
"context"
|
|
|
|
"github.com/hashicorp/boundary/internal/cmd/config"
|
|
"github.com/hashicorp/boundary/internal/errors"
|
|
"github.com/hashicorp/boundary/internal/event"
|
|
"github.com/hashicorp/boundary/internal/ratelimit"
|
|
"github.com/hashicorp/go-rate"
|
|
)
|
|
|
|
// limit is a representation of a rate.Limit that is used when emitting a sys
|
|
// event to report the rate limit configuration.
|
|
type limit struct {
|
|
Resource string `json:"resource"`
|
|
Action string `json:"action"`
|
|
Per string `json:"per"`
|
|
Unlimited bool `json:"unlimited"`
|
|
Limit uint64 `json:"limit"`
|
|
Period string `json:"period"`
|
|
}
|
|
|
|
// Types used for the nested structure of the sys event.
|
|
type (
|
|
actionLimits []limit
|
|
resourceActions map[string]actionLimits
|
|
resources map[string]resourceActions
|
|
)
|
|
|
|
type rateLimiterConfig struct {
|
|
maxSize int
|
|
disabled bool
|
|
configs ratelimit.Configs
|
|
|
|
limits []rate.Limit
|
|
}
|
|
|
|
func newRateLimiterConfig(ctx context.Context, configs ratelimit.Configs, maxSize int, disabled bool) (*rateLimiterConfig, error) {
|
|
const op = "controller.newRateLimiterConfig"
|
|
|
|
switch {
|
|
case disabled && len(configs) != 0:
|
|
return nil, errors.New(ctx, errors.InvalidConfiguration, op, "disabled rate limiter with rate limit configs")
|
|
}
|
|
|
|
var limits []rate.Limit
|
|
var err error
|
|
if !disabled {
|
|
if limits, err = configs.Limits(ctx); err != nil {
|
|
return nil, err
|
|
}
|
|
}
|
|
|
|
return &rateLimiterConfig{
|
|
maxSize: maxSize,
|
|
disabled: disabled,
|
|
configs: configs,
|
|
limits: limits,
|
|
}, nil
|
|
}
|
|
|
|
// writeSysEvent writes a sys event for c
|
|
func (c *rateLimiterConfig) writeSysEvent(ctx context.Context) {
|
|
const op = "controller.(rateLimiterConfig).writeSysEvent"
|
|
|
|
if c.disabled {
|
|
event.WriteSysEvent(
|
|
ctx,
|
|
op,
|
|
"controller api rate limiter",
|
|
"disabled",
|
|
true,
|
|
)
|
|
return
|
|
}
|
|
|
|
e := make(resources)
|
|
|
|
for _, l := range c.limits {
|
|
var r resourceActions
|
|
var a actionLimits
|
|
var ok bool
|
|
r, ok = e[l.GetResource()]
|
|
if !ok {
|
|
r = make(resourceActions)
|
|
e[l.GetResource()] = r
|
|
}
|
|
|
|
a, ok = r[l.GetAction()]
|
|
if !ok {
|
|
a = make(actionLimits, 0, 3)
|
|
}
|
|
|
|
switch ll := l.(type) {
|
|
case *rate.Limited:
|
|
a = append(a, limit{
|
|
Resource: ll.Resource,
|
|
Action: ll.Action,
|
|
Per: ll.Per.String(),
|
|
Unlimited: false,
|
|
Limit: ll.MaxRequests,
|
|
Period: ll.Period.String(),
|
|
})
|
|
case *rate.Unlimited:
|
|
a = append(a, limit{
|
|
Resource: ll.Resource,
|
|
Action: ll.Action,
|
|
Per: ll.Per.String(),
|
|
Unlimited: true,
|
|
})
|
|
}
|
|
r[l.GetAction()] = a
|
|
}
|
|
event.WriteSysEvent(
|
|
ctx,
|
|
op,
|
|
"controller api rate limiter",
|
|
"limits",
|
|
e,
|
|
"max_size",
|
|
c.maxSize,
|
|
)
|
|
}
|
|
|
|
func (c *Controller) initializeRateLimiter(conf *config.Config) error {
|
|
const op = "controller.(Controller).initializeRateLimiter"
|
|
switch {
|
|
case conf == nil:
|
|
return errors.New(c.baseContext, errors.InvalidParameter, op, "nil config")
|
|
case conf.Controller == nil:
|
|
return errors.New(c.baseContext, errors.InvalidParameter, op, "nil config.Controller")
|
|
}
|
|
|
|
c.rateLimiterMu.Lock()
|
|
defer c.rateLimiterMu.Unlock()
|
|
|
|
rlConfig, err := newRateLimiterConfig(
|
|
c.baseContext,
|
|
conf.Controller.ApiRateLimits,
|
|
conf.Controller.ApiRateLimiterMaxEntries,
|
|
conf.Controller.ApiRateLimitDisable,
|
|
)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
switch {
|
|
case rlConfig.disabled:
|
|
c.rateLimiter = rate.NopLimiter
|
|
default:
|
|
c.rateLimiter, err = ratelimit.NewLimiter(rlConfig.limits, rlConfig.maxSize)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
}
|
|
|
|
c.conf.rateLimiterConfig = rlConfig
|
|
|
|
rlConfig.writeSysEvent(c.baseContext)
|
|
return nil
|
|
}
|
|
|
|
func (c *Controller) getRateLimiter() ratelimit.Limiter {
|
|
c.rateLimiterMu.RLock()
|
|
defer c.rateLimiterMu.RUnlock()
|
|
return c.rateLimiter
|
|
}
|
|
|
|
// ReloadRateLimiter replaces the Controller's rate.Limiter with a new rate.Limiter
|
|
// using the supplied ratelimit.Configs and max entries. If the configs and max
|
|
// entries match the current values, the rate.Limiter is not replaced and no
|
|
// error is returned. Otherwise the rate.Limiter is replaced, and the old rate.Limiter
|
|
// is shutdown. This means that a client's quota is effectively reset if the
|
|
// configuration changes.
|
|
func (c *Controller) ReloadRateLimiter(newConfig *config.Config) error {
|
|
const op = "controller.(Controller).ReloadRateLimiter"
|
|
|
|
switch {
|
|
case newConfig == nil:
|
|
return errors.New(c.baseContext, errors.InvalidParameter, op, "nil config")
|
|
case newConfig.Controller == nil:
|
|
return errors.New(c.baseContext, errors.InvalidParameter, op, "nil config.Controller")
|
|
}
|
|
|
|
rlConfig, err := newRateLimiterConfig(
|
|
c.baseContext,
|
|
newConfig.Controller.ApiRateLimits,
|
|
newConfig.Controller.ApiRateLimiterMaxEntries,
|
|
newConfig.Controller.ApiRateLimitDisable,
|
|
)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
// Config has not changed, no need to reload.
|
|
if c.conf.rateLimiterConfig.maxSize == rlConfig.maxSize &&
|
|
c.conf.rateLimiterConfig.disabled == rlConfig.disabled &&
|
|
c.conf.rateLimiterConfig.configs.Equal(rlConfig.configs) {
|
|
return nil
|
|
}
|
|
|
|
var limiter ratelimit.Limiter
|
|
switch {
|
|
case rlConfig.disabled:
|
|
limiter = rate.NopLimiter
|
|
default:
|
|
limiter, err = ratelimit.NewLimiter(rlConfig.limits, rlConfig.maxSize)
|
|
if err != nil {
|
|
return errors.Wrap(c.baseContext, err, op)
|
|
}
|
|
|
|
}
|
|
c.rateLimiterMu.Lock()
|
|
old := c.rateLimiter
|
|
c.rateLimiter = limiter
|
|
c.conf.rateLimiterConfig = rlConfig
|
|
c.rateLimiterMu.Unlock()
|
|
rlConfig.writeSysEvent(c.baseContext)
|
|
|
|
return old.Shutdown()
|
|
}
|