Skip to content

Commit 8974584

Browse files
adunham-stripeAlCutter
authored andcommitted
Add redis-based quota.Manager (#1977)
1 parent a6efc69 commit 8974584

File tree

12 files changed

+1627
-3
lines changed

12 files changed

+1627
-3
lines changed

.travis.yml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -131,6 +131,7 @@ services:
131131
- docker
132132
- postgresql
133133
- mysql
134+
- redis-server
134135

135136
before_install:
136137
- sudo service mysql stop

CHANGELOG.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,10 @@ the corresponding packages, and are now required to be imported explicitly by
4040
the main file in order to be registered. We are including only MySQL and
4141
cloudspanner providers by default, since these are the ones that we support.
4242

43+
### Quota
44+
45+
An experimental Redis-based `quota.Manager` implementation has been added.
46+
4347
### Tools
4448

4549
The `licenses` tool has been moved from "scripts/licenses" to [a dedicated

docs/Feature_Implementation_Matrix.md

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -153,16 +153,16 @@ Supported frameworks for providing Master Election.
153153

154154
### Quota
155155

156-
Supported frameworks for providing Master Election.
156+
Supported frameworks for quota management.
157157

158-
| Election | Status | Deployed in prod | Notes |
158+
| Implementation | Status | Deployed in prod | Notes |
159159
|:--- | :---: | :---: |:--- |
160160
| Google internal | GA || |
161161
| etcd | GA || |
162162
| MySQL | Beta | ? | |
163+
| Redis | Alpha || |
163164
| Postgres | NI | | |
164165

165-
166166
### Key management
167167

168168
Supported frameworks for key management and signing.

go.mod

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ require (
1919
github.com/coreos/go-systemd v0.0.0-20190620071333-e64a0ec8b42a // indirect
2020
github.com/cpuguy83/go-md2man/v2 v2.0.0 // indirect
2121
github.com/emicklei/proto v1.8.0 // indirect
22+
github.com/go-redis/redis v6.15.6+incompatible
2223
github.com/go-sql-driver/mysql v1.4.1
2324
github.com/gogo/protobuf v1.3.1 // indirect
2425
github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b

go.sum

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -141,6 +141,8 @@ github.com/go-lintpack/lintpack v0.5.2/go.mod h1:NwZuYi2nUHho8XEIZ6SIxihrnPoqBTD
141141
github.com/go-logfmt/logfmt v0.3.0/go.mod h1:Qt1PoO58o5twSAckw1HlFXLmHsOX5/0LbT9GBnD5lWE=
142142
github.com/go-logfmt/logfmt v0.4.0/go.mod h1:3RMwSq7FuexP4Kalkev3ejPJsZTpXXBr9+V4qmtdjCk=
143143
github.com/go-ole/go-ole v1.2.1/go.mod h1:7FAglXiTm7HKlQRDeOQ6ZNUHidzCWXuZWq/1dTyBNF8=
144+
github.com/go-redis/redis v6.15.6+incompatible h1:H9evprGPLI8+ci7fxQx6WNZHJSb7be8FqJQRhdQZ5Sg=
145+
github.com/go-redis/redis v6.15.6+incompatible/go.mod h1:NAIEuMOZ/fxfXJIrKDQDz8wamY7mA7PouImQ2Jvg6kA=
144146
github.com/go-sql-driver/mysql v1.4.1 h1:g24URVg0OFbNUTx9qqY1IRZ9D9z3iPyi5zKhQZpNwpA=
145147
github.com/go-sql-driver/mysql v1.4.1/go.mod h1:zAC/RDZ24gD3HViQzih4MyKcchzm+sOG5ZlKdlhCg5w=
146148
github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY=

quota/redis/redisqm/manager.go

Lines changed: 186 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,186 @@
1+
// Copyright 2017 Google Inc. All Rights Reserved.
2+
//
3+
// Licensed under the Apache License, Version 2.0 (the "License");
4+
// you may not use this file except in compliance with the License.
5+
// You may obtain a copy of the License at
6+
//
7+
// http://www.apache.org/licenses/LICENSE-2.0
8+
//
9+
// Unless required by applicable law or agreed to in writing, software
10+
// distributed under the License is distributed on an "AS IS" BASIS,
11+
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
// See the License for the specific language governing permissions and
13+
// limitations under the License.
14+
15+
// Package redisqm defines a Redis-based quota.Manager implementation.
16+
package redisqm
17+
18+
import (
19+
"context"
20+
"fmt"
21+
22+
"github.com/google/trillian/quota"
23+
"github.com/google/trillian/quota/redis/redistb"
24+
)
25+
26+
// ParameterFunc is a function that should return a token bucket's parameters
27+
// for a given quota specification.
28+
type ParameterFunc func(spec quota.Spec) (capacity int, rate float64)
29+
30+
// ManagerOptions holds the parameters for a Manager.
31+
type ManagerOptions struct {
32+
// Parameters should return the parameters for a given quota.Spec. This
33+
// value must not be nil.
34+
Parameters ParameterFunc
35+
36+
// Prefix is a static prefix to apply to all Redis keys; this is useful
37+
// if running on a multi-tenant Redis cluster.
38+
Prefix string
39+
}
40+
41+
// Manager implements the quota.Manager interface backed by a Redis-based token
42+
// bucket implementation.
43+
type Manager struct {
44+
tb *redistb.TokenBucket
45+
opts ManagerOptions
46+
}
47+
48+
var _ quota.Manager = &Manager{}
49+
50+
// RedisClient is an interface that encompasses the various methods used by
51+
// this quota.Manager, and allows selecting among different Redis client
52+
// implementations (e.g. regular Redis, Redis Cluster, sharded, etc.)
53+
type RedisClient interface {
54+
// Everything required by the redistb.RedisClient interface
55+
redistb.RedisClient
56+
}
57+
58+
// New returns a new Redis-based quota.Manager.
59+
func New(client RedisClient, opts ManagerOptions) *Manager {
60+
tb := redistb.New(client)
61+
return &Manager{tb: tb, opts: opts}
62+
}
63+
64+
// GetTokens implements the quota.Manager API.
65+
func (m *Manager) GetTokens(ctx context.Context, numTokens int, specs []quota.Spec) error {
66+
for _, spec := range specs {
67+
if err := m.getTokensSingle(ctx, numTokens, spec); err != nil {
68+
return err
69+
}
70+
}
71+
72+
return nil
73+
}
74+
75+
func (m *Manager) getTokensSingle(ctx context.Context, numTokens int, spec quota.Spec) error {
76+
capacity, rate := m.opts.Parameters(spec)
77+
78+
// If we get back `MaxTokens` from our parameters call, this indicates
79+
// that there's no actual limit. We don't need to do anything to "get"
80+
// them; just ignore.
81+
if capacity == quota.MaxTokens {
82+
return nil
83+
}
84+
85+
name := specName(m.opts.Prefix, spec)
86+
allowed, remaining, err := m.tb.Call(
87+
ctx,
88+
name,
89+
int64(capacity),
90+
rate,
91+
numTokens,
92+
)
93+
if err != nil {
94+
return err
95+
}
96+
if !allowed {
97+
return fmt.Errorf("insufficient tokens on %v (%v vs %v)", name, remaining, numTokens)
98+
}
99+
100+
return nil
101+
}
102+
103+
// PeekTokens implements the quota.Manager API.
104+
func (m *Manager) PeekTokens(ctx context.Context, specs []quota.Spec) (map[quota.Spec]int, error) {
105+
tokens := make(map[quota.Spec]int)
106+
for _, spec := range specs {
107+
// Calling the limiter with 0 tokens requested is equivalent to
108+
// "peeking", but it will also shrink the token bucket if it
109+
// has too many tokens.
110+
capacity, rate := m.opts.Parameters(spec)
111+
112+
// If we get back `MaxTokens` from our parameters call, this
113+
// indicates that there's no actual limit. We don't need to do
114+
// anything to "get" them; just set that value in the returned
115+
// map as well.
116+
if capacity == quota.MaxTokens {
117+
tokens[spec] = quota.MaxTokens
118+
continue
119+
}
120+
121+
_, remaining, err := m.tb.Call(
122+
ctx,
123+
specName(m.opts.Prefix, spec),
124+
int64(capacity),
125+
rate,
126+
0,
127+
)
128+
if err != nil {
129+
return nil, err
130+
}
131+
132+
tokens[spec] = int(remaining)
133+
}
134+
135+
return tokens, nil
136+
}
137+
138+
// PutTokens implements the quota.Manager API.
139+
func (m *Manager) PutTokens(ctx context.Context, numTokens int, specs []quota.Spec) error {
140+
// Putting tokens into a time-based quota doesn't mean anything (since
141+
// tokens are replenished at the moment they're requested) and since
142+
// that's the only supported mechanism for this package currently, do
143+
// nothing.
144+
return nil
145+
}
146+
147+
// ResetQuota implements the quota.Manager API.
148+
//
149+
// This function will reset every quota and return the first error encountered,
150+
// if any, but will continue trying to reset every quota even if an error is
151+
// encountered.
152+
func (m *Manager) ResetQuota(ctx context.Context, specs []quota.Spec) error {
153+
var firstErr error
154+
155+
for _, name := range specNames(m.opts.Prefix, specs) {
156+
if err := m.tb.Reset(ctx, name); err != nil {
157+
if firstErr == nil {
158+
firstErr = err
159+
}
160+
}
161+
}
162+
163+
return firstErr
164+
}
165+
166+
// Load attempts to load Redis scripts used by the Manager into the Redis
167+
// cluster.
168+
//
169+
// A Manager will operate successfully if this method is not called or fails,
170+
// but a successful Load will reduce bandwidth to/from the Redis cluster
171+
// substantially.
172+
func (m *Manager) Load(ctx context.Context) error {
173+
return m.tb.Load(ctx)
174+
}
175+
176+
func specNames(prefix string, specs []quota.Spec) []string {
177+
names := make([]string, 0, len(specs))
178+
for _, spec := range specs {
179+
names = append(names, specName(prefix, spec))
180+
}
181+
return names
182+
}
183+
184+
func specName(prefix string, spec quota.Spec) string {
185+
return prefix + "trillian/" + spec.Name()
186+
}

quota/redis/redistb/embed_redis.go

Lines changed: 89 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

quota/redis/redistb/gen.go

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
// Copyright 2017 Google Inc. All Rights Reserved.
2+
//
3+
// Licensed under the Apache License, Version 2.0 (the "License");
4+
// you may not use this file except in compliance with the License.
5+
// You may obtain a copy of the License at
6+
//
7+
// http://www.apache.org/licenses/LICENSE-2.0
8+
//
9+
// Unless required by applicable law or agreed to in writing, software
10+
// distributed under the License is distributed on an "AS IS" BASIS,
11+
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
// See the License for the specific language governing permissions and
13+
// limitations under the License.
14+
15+
package redistb
16+
17+
//go:generate go run embed_redis.go updateTokenBucket update_token_bucket.lua update_token_bucket.gen.go

0 commit comments

Comments
 (0)