parent
							
								
									4ed54ff9c6
								
							
						
					
					
						commit
						077a4e2dc4
					
				| 
						 | 
				
			
			@ -34,6 +34,10 @@ type limit struct {
 | 
			
		|||
	// bucket to go from empty to full (burst * (period / count)). This is
 | 
			
		||||
	// precomputed to avoid doing the same calculation on every request.
 | 
			
		||||
	burstOffset int64
 | 
			
		||||
 | 
			
		||||
	// isOverride is true if this limit is an override limit, false if it is a
 | 
			
		||||
	// default limit.
 | 
			
		||||
	isOverride bool
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func precomputeLimit(l limit) limit {
 | 
			
		||||
| 
						 | 
				
			
			@ -131,6 +135,7 @@ func loadAndParseOverrideLimits(path string) (limits, error) {
 | 
			
		|||
			fqdnSet := core.HashNames(domains)
 | 
			
		||||
			id = fmt.Sprintf("%s:%s", regId, fqdnSet)
 | 
			
		||||
		}
 | 
			
		||||
		v.isOverride = true
 | 
			
		||||
		parsed[bucketKey(name, id)] = precomputeLimit(v)
 | 
			
		||||
	}
 | 
			
		||||
	return parsed, nil
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -7,6 +7,7 @@ import (
 | 
			
		|||
	"time"
 | 
			
		||||
 | 
			
		||||
	"github.com/jmhodges/clock"
 | 
			
		||||
	"github.com/prometheus/client_golang/prometheus"
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
// ErrInvalidCost indicates that the cost specified was <= 0.
 | 
			
		||||
| 
						 | 
				
			
			@ -34,13 +35,15 @@ type Limiter struct {
 | 
			
		|||
	// source is used to store buckets. It must be safe for concurrent use.
 | 
			
		||||
	source source
 | 
			
		||||
	clk    clock.Clock
 | 
			
		||||
 | 
			
		||||
	overrideUsageGauge *prometheus.GaugeVec
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// NewLimiter returns a new *Limiter. The provided source must be safe for
 | 
			
		||||
// concurrent use. The defaults and overrides paths are expected to be paths to
 | 
			
		||||
// YAML files that contain the default and override limits, respectively. The
 | 
			
		||||
// overrides file is optional, all other arguments are required.
 | 
			
		||||
func NewLimiter(clk clock.Clock, source source, defaults, overrides string) (*Limiter, error) {
 | 
			
		||||
func NewLimiter(clk clock.Clock, source source, defaults, overrides string, stats prometheus.Registerer) (*Limiter, error) {
 | 
			
		||||
	limiter := &Limiter{source: source, clk: clk}
 | 
			
		||||
 | 
			
		||||
	var err error
 | 
			
		||||
| 
						 | 
				
			
			@ -60,6 +63,12 @@ func NewLimiter(clk clock.Clock, source source, defaults, overrides string) (*Li
 | 
			
		|||
		return nil, err
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	limiter.overrideUsageGauge = prometheus.NewGaugeVec(prometheus.GaugeOpts{
 | 
			
		||||
		Name: "ratelimits_override_usage",
 | 
			
		||||
		Help: "Proportion of override limit used, by limit name and client id.",
 | 
			
		||||
	}, []string{"limit_name", "client_id"})
 | 
			
		||||
	stats.MustRegister(limiter.overrideUsageGauge)
 | 
			
		||||
 | 
			
		||||
	return limiter, nil
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			@ -160,6 +169,13 @@ func (l *Limiter) Spend(ctx context.Context, name Name, id string, cost int64) (
 | 
			
		|||
 | 
			
		||||
	d := maybeSpend(l.clk, limit, tat, cost)
 | 
			
		||||
 | 
			
		||||
	if limit.isOverride {
 | 
			
		||||
		// Calculate the current utilization of the override limit for the
 | 
			
		||||
		// specified client id.
 | 
			
		||||
		utilization := float64(limit.Burst-d.Remaining) / float64(limit.Burst)
 | 
			
		||||
		l.overrideUsageGauge.WithLabelValues(nameToString[name], id).Set(utilization)
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	if !d.Allowed {
 | 
			
		||||
		return d, nil
 | 
			
		||||
	}
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -8,7 +8,9 @@ import (
 | 
			
		|||
	"time"
 | 
			
		||||
 | 
			
		||||
	"github.com/jmhodges/clock"
 | 
			
		||||
	"github.com/letsencrypt/boulder/metrics"
 | 
			
		||||
	"github.com/letsencrypt/boulder/test"
 | 
			
		||||
	"github.com/prometheus/client_golang/prometheus"
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
// tenZeroZeroTwo is overridden in 'testdata/working_override.yml' to have
 | 
			
		||||
| 
						 | 
				
			
			@ -19,7 +21,7 @@ const tenZeroZeroTwo = "10.0.0.2"
 | 
			
		|||
//   - 'NewRegistrationsPerIPAddress' burst: 20 count: 20 period: 1s
 | 
			
		||||
//   - 'NewRegistrationsPerIPAddress:10.0.0.2' burst: 40 count: 40 period: 1s
 | 
			
		||||
func newTestLimiter(t *testing.T, s source, clk clock.FakeClock) *Limiter {
 | 
			
		||||
	l, err := NewLimiter(clk, s, "testdata/working_default.yml", "testdata/working_override.yml")
 | 
			
		||||
	l, err := NewLimiter(clk, s, "testdata/working_default.yml", "testdata/working_override.yml", metrics.NoopRegisterer)
 | 
			
		||||
	test.AssertNotError(t, err, "should not error")
 | 
			
		||||
	return l
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			@ -44,16 +46,16 @@ func setup(t *testing.T) (context.Context, map[string]*Limiter, clock.FakeClock,
 | 
			
		|||
 | 
			
		||||
func Test_Limiter_WithBadLimitsPath(t *testing.T) {
 | 
			
		||||
	t.Parallel()
 | 
			
		||||
	_, err := NewLimiter(clock.NewFake(), newInmem(), "testdata/does-not-exist.yml", "")
 | 
			
		||||
	_, err := NewLimiter(clock.NewFake(), newInmem(), "testdata/does-not-exist.yml", "", metrics.NoopRegisterer)
 | 
			
		||||
	test.AssertError(t, err, "should error")
 | 
			
		||||
 | 
			
		||||
	_, err = NewLimiter(clock.NewFake(), newInmem(), "testdata/defaults.yml", "testdata/does-not-exist.yml")
 | 
			
		||||
	_, err = NewLimiter(clock.NewFake(), newInmem(), "testdata/defaults.yml", "testdata/does-not-exist.yml", metrics.NoopRegisterer)
 | 
			
		||||
	test.AssertError(t, err, "should error")
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func Test_Limiter_getLimitNoExist(t *testing.T) {
 | 
			
		||||
	t.Parallel()
 | 
			
		||||
	l, err := NewLimiter(clock.NewFake(), newInmem(), "testdata/working_default.yml", "")
 | 
			
		||||
	l, err := NewLimiter(clock.NewFake(), newInmem(), "testdata/working_default.yml", "", metrics.NoopRegisterer)
 | 
			
		||||
	test.AssertNotError(t, err, "should not error")
 | 
			
		||||
	_, err = l.getLimit(Name(9999), "")
 | 
			
		||||
	test.AssertError(t, err, "should error")
 | 
			
		||||
| 
						 | 
				
			
			@ -76,6 +78,11 @@ func Test_Limiter_CheckWithLimitOverrides(t *testing.T) {
 | 
			
		|||
	testCtx, limiters, clk, _ := setup(t)
 | 
			
		||||
	for name, l := range limiters {
 | 
			
		||||
		t.Run(name, func(t *testing.T) {
 | 
			
		||||
			// Verify our overrideUsageGauge is being set correctly. 0.0 == 0% of
 | 
			
		||||
			// the bucket has been consumed.
 | 
			
		||||
			test.AssertMetricWithLabelsEquals(t, l.overrideUsageGauge, prometheus.Labels{
 | 
			
		||||
				"limit_name": nameToString[NewRegistrationsPerIPAddress], "client_id": tenZeroZeroTwo}, 0)
 | 
			
		||||
 | 
			
		||||
			// Attempt to check a spend of 41 requests (a cost > the limit burst
 | 
			
		||||
			// capacity), this should fail with a specific error.
 | 
			
		||||
			_, err := l.Check(testCtx, NewRegistrationsPerIPAddress, tenZeroZeroTwo, 41)
 | 
			
		||||
| 
						 | 
				
			
			@ -98,6 +105,11 @@ func Test_Limiter_CheckWithLimitOverrides(t *testing.T) {
 | 
			
		|||
			test.AssertEquals(t, d.Remaining, int64(0))
 | 
			
		||||
			test.AssertEquals(t, d.ResetIn, time.Second)
 | 
			
		||||
 | 
			
		||||
			// Verify our overrideUsageGauge is being set correctly. 1.0 == 100% of
 | 
			
		||||
			// the bucket has been consumed.
 | 
			
		||||
			test.AssertMetricWithLabelsEquals(t, l.overrideUsageGauge, prometheus.Labels{
 | 
			
		||||
				"limit_name": nameToString[NewRegistrationsPerIPAddress], "client_id": tenZeroZeroTwo}, 1.0)
 | 
			
		||||
 | 
			
		||||
			// Verify our RetryIn is correct. 1 second == 1000 milliseconds and
 | 
			
		||||
			// 1000/40 = 25 milliseconds per request.
 | 
			
		||||
			test.AssertEquals(t, d.RetryIn, time.Millisecond*25)
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
		Loading…
	
		Reference in New Issue