Skip to content

Commit

Permalink
Add redis-based quota.Manager (#1977)
Browse files Browse the repository at this point in the history
  • Loading branch information
adunham-stripe authored and AlCutter committed Nov 23, 2019
1 parent a6efc69 commit 8974584
Show file tree
Hide file tree
Showing 12 changed files with 1,627 additions and 3 deletions.
1 change: 1 addition & 0 deletions .travis.yml
Original file line number Diff line number Diff line change
Expand Up @@ -131,6 +131,7 @@ services:
- docker
- postgresql
- mysql
- redis-server

before_install:
- sudo service mysql stop
Expand Down
4 changes: 4 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,10 @@ the corresponding packages, and are now required to be imported explicitly by
the main file in order to be registered. We are including only MySQL and
cloudspanner providers by default, since these are the ones that we support.

### Quota

An experimental Redis-based `quota.Manager` implementation has been added.

### Tools

The `licenses` tool has been moved from "scripts/licenses" to [a dedicated
Expand Down
6 changes: 3 additions & 3 deletions docs/Feature_Implementation_Matrix.md
Original file line number Diff line number Diff line change
Expand Up @@ -153,16 +153,16 @@ Supported frameworks for providing Master Election.

### Quota

Supported frameworks for providing Master Election.
Supported frameworks for quota management.

| Election | Status | Deployed in prod | Notes |
| Implementation | Status | Deployed in prod | Notes |
|:--- | :---: | :---: |:--- |
| Google internal | GA || |
| etcd | GA || |
| MySQL | Beta | ? | |
| Redis | Alpha || |
| Postgres | NI | | |


### Key management

Supported frameworks for key management and signing.
Expand Down
1 change: 1 addition & 0 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ require (
github.com/coreos/go-systemd v0.0.0-20190620071333-e64a0ec8b42a // indirect
github.com/cpuguy83/go-md2man/v2 v2.0.0 // indirect
github.com/emicklei/proto v1.8.0 // indirect
github.com/go-redis/redis v6.15.6+incompatible
github.com/go-sql-driver/mysql v1.4.1
github.com/gogo/protobuf v1.3.1 // indirect
github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b
Expand Down
2 changes: 2 additions & 0 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -141,6 +141,8 @@ github.com/go-lintpack/lintpack v0.5.2/go.mod h1:NwZuYi2nUHho8XEIZ6SIxihrnPoqBTD
github.com/go-logfmt/logfmt v0.3.0/go.mod h1:Qt1PoO58o5twSAckw1HlFXLmHsOX5/0LbT9GBnD5lWE=
github.com/go-logfmt/logfmt v0.4.0/go.mod h1:3RMwSq7FuexP4Kalkev3ejPJsZTpXXBr9+V4qmtdjCk=
github.com/go-ole/go-ole v1.2.1/go.mod h1:7FAglXiTm7HKlQRDeOQ6ZNUHidzCWXuZWq/1dTyBNF8=
github.com/go-redis/redis v6.15.6+incompatible h1:H9evprGPLI8+ci7fxQx6WNZHJSb7be8FqJQRhdQZ5Sg=
github.com/go-redis/redis v6.15.6+incompatible/go.mod h1:NAIEuMOZ/fxfXJIrKDQDz8wamY7mA7PouImQ2Jvg6kA=
github.com/go-sql-driver/mysql v1.4.1 h1:g24URVg0OFbNUTx9qqY1IRZ9D9z3iPyi5zKhQZpNwpA=
github.com/go-sql-driver/mysql v1.4.1/go.mod h1:zAC/RDZ24gD3HViQzih4MyKcchzm+sOG5ZlKdlhCg5w=
github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY=
Expand Down
186 changes: 186 additions & 0 deletions quota/redis/redisqm/manager.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,186 @@
// Copyright 2017 Google Inc. All Rights Reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

// Package redisqm defines a Redis-based quota.Manager implementation.
package redisqm

import (
"context"
"fmt"

"github.com/google/trillian/quota"
"github.com/google/trillian/quota/redis/redistb"
)

// ParameterFunc is a function that should return a token bucket's parameters
// for a given quota specification.
type ParameterFunc func(spec quota.Spec) (capacity int, rate float64)

// ManagerOptions holds the parameters for a Manager.
type ManagerOptions struct {
// Parameters should return the parameters for a given quota.Spec. This
// value must not be nil.
Parameters ParameterFunc

// Prefix is a static prefix to apply to all Redis keys; this is useful
// if running on a multi-tenant Redis cluster.
Prefix string
}

// Manager implements the quota.Manager interface backed by a Redis-based token
// bucket implementation.
type Manager struct {
tb *redistb.TokenBucket
opts ManagerOptions
}

var _ quota.Manager = &Manager{}

// RedisClient is an interface that encompasses the various methods used by
// this quota.Manager, and allows selecting among different Redis client
// implementations (e.g. regular Redis, Redis Cluster, sharded, etc.)
type RedisClient interface {
// Everything required by the redistb.RedisClient interface
redistb.RedisClient
}

// New returns a new Redis-based quota.Manager.
func New(client RedisClient, opts ManagerOptions) *Manager {
tb := redistb.New(client)
return &Manager{tb: tb, opts: opts}
}

// GetTokens implements the quota.Manager API.
func (m *Manager) GetTokens(ctx context.Context, numTokens int, specs []quota.Spec) error {
for _, spec := range specs {
if err := m.getTokensSingle(ctx, numTokens, spec); err != nil {
return err
}
}

return nil
}

func (m *Manager) getTokensSingle(ctx context.Context, numTokens int, spec quota.Spec) error {
capacity, rate := m.opts.Parameters(spec)

// If we get back `MaxTokens` from our parameters call, this indicates
// that there's no actual limit. We don't need to do anything to "get"
// them; just ignore.
if capacity == quota.MaxTokens {
return nil
}

name := specName(m.opts.Prefix, spec)
allowed, remaining, err := m.tb.Call(
ctx,
name,
int64(capacity),
rate,
numTokens,
)
if err != nil {
return err
}
if !allowed {
return fmt.Errorf("insufficient tokens on %v (%v vs %v)", name, remaining, numTokens)
}

return nil
}

// PeekTokens implements the quota.Manager API.
func (m *Manager) PeekTokens(ctx context.Context, specs []quota.Spec) (map[quota.Spec]int, error) {
tokens := make(map[quota.Spec]int)
for _, spec := range specs {
// Calling the limiter with 0 tokens requested is equivalent to
// "peeking", but it will also shrink the token bucket if it
// has too many tokens.
capacity, rate := m.opts.Parameters(spec)

// If we get back `MaxTokens` from our parameters call, this
// indicates that there's no actual limit. We don't need to do
// anything to "get" them; just set that value in the returned
// map as well.
if capacity == quota.MaxTokens {
tokens[spec] = quota.MaxTokens
continue
}

_, remaining, err := m.tb.Call(
ctx,
specName(m.opts.Prefix, spec),
int64(capacity),
rate,
0,
)
if err != nil {
return nil, err
}

tokens[spec] = int(remaining)
}

return tokens, nil
}

// PutTokens implements the quota.Manager API.
func (m *Manager) PutTokens(ctx context.Context, numTokens int, specs []quota.Spec) error {
// Putting tokens into a time-based quota doesn't mean anything (since
// tokens are replenished at the moment they're requested) and since
// that's the only supported mechanism for this package currently, do
// nothing.
return nil
}

// ResetQuota implements the quota.Manager API.
//
// This function will reset every quota and return the first error encountered,
// if any, but will continue trying to reset every quota even if an error is
// encountered.
func (m *Manager) ResetQuota(ctx context.Context, specs []quota.Spec) error {
var firstErr error

for _, name := range specNames(m.opts.Prefix, specs) {
if err := m.tb.Reset(ctx, name); err != nil {
if firstErr == nil {
firstErr = err
}
}
}

return firstErr
}

// Load attempts to load Redis scripts used by the Manager into the Redis
// cluster.
//
// A Manager will operate successfully if this method is not called or fails,
// but a successful Load will reduce bandwidth to/from the Redis cluster
// substantially.
func (m *Manager) Load(ctx context.Context) error {
return m.tb.Load(ctx)
}

func specNames(prefix string, specs []quota.Spec) []string {
names := make([]string, 0, len(specs))
for _, spec := range specs {
names = append(names, specName(prefix, spec))
}
return names
}

func specName(prefix string, spec quota.Spec) string {
return prefix + "trillian/" + spec.Name()
}
89 changes: 89 additions & 0 deletions quota/redis/redistb/embed_redis.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

17 changes: 17 additions & 0 deletions quota/redis/redistb/gen.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
// Copyright 2017 Google Inc. All Rights Reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

package redistb

//go:generate go run embed_redis.go updateTokenBucket update_token_bucket.lua update_token_bucket.gen.go
Loading

0 comments on commit 8974584

Please sign in to comment.