-
Notifications
You must be signed in to change notification settings - Fork 3.8k
/
local.go
167 lines (145 loc) · 4.42 KB
/
local.go
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
// Copyright 2020 The Cockroach Authors.
//
// Licensed as a CockroachDB Enterprise file under the Cockroach Community
// License (the "License"); you may not use this file except in compliance with
// the License. You may obtain a copy of the License at
//
// https://github.com/cockroachdb/cockroach/blob/master/licenses/CCL.txt
package admitter
import (
"errors"
"math/rand"
"sync"
"time"
)
const (
// The maximum size of localService's address map.
maxMapSize = 1e6 // 1 million
)
var errRequestDenied = errors.New("request denied")
type timeNow func() time.Time
type limiter struct {
// The next time an operation on this limiter can proceed.
nextTime time.Time
// The number of operation attempts that have been performed. On success, the
// limiter will be removed.
attempts int
// The index of the limiter in the addresses array.
index int
}
// localService is an admitter Service that manages state purely in local
// memory. Internally, it maintains a map from IP address to rate limiting info
// for that address. In order to put a cap on memory usage, the map is capped
// at a maximum size, at which point a random IP address will be evicted.
type localService struct {
clock timeNow
baseDelay time.Duration
maxDelay time.Duration
maxMapSize int
mu struct {
sync.Mutex
// Map from IP address to limiter.
limiters map[string]*limiter
// Map from IP address to known tenant ids for this IP.
knownTenants map[string]map[uint64]bool
// Array of addresses, used for randomly evicting an address when the max
// entries is reached.
addrs []string
}
}
// LocalOption allows configuration of a local admission service.
type LocalOption func(s *localService)
// WithBaseDelay specifies the base delay for rate limiting repeated accesses.
func WithBaseDelay(d time.Duration) LocalOption {
return func(s *localService) {
s.baseDelay = d
}
}
// NewLocalService returns an admitter Service that manages state purely in
// local memory.
func NewLocalService(opts ...LocalOption) Service {
s := &localService{
clock: time.Now,
baseDelay: 2 * time.Second,
maxDelay: 60 * 60 * time.Second,
maxMapSize: maxMapSize,
}
s.mu.limiters = make(map[string]*limiter)
s.mu.knownTenants = make(map[string]map[uint64]bool)
for _, opt := range opts {
opt(s)
}
return s
}
func (s *localService) AllowRequest(ipAddress string, now time.Time) error {
s.mu.Lock()
defer s.mu.Unlock()
l := s.mu.limiters[ipAddress]
if l == nil {
l = s.addLocked(ipAddress)
}
if now.Before(l.nextTime) {
return errRequestDenied
}
s.nextLimitLocked(l)
return nil
}
func (s *localService) RequestSuccess(ipAddress string, tenID uint64) {
s.mu.Lock()
defer s.mu.Unlock()
s.evictLocked(ipAddress)
if _, ok := s.mu.knownTenants[ipAddress]; !ok {
s.mu.knownTenants[ipAddress] = map[uint64]bool{tenID: true}
} else {
s.mu.knownTenants[ipAddress][tenID] = true
}
}
func (s *localService) KnownClient(ipAddress string, tenID uint64) bool {
s.mu.Lock()
defer s.mu.Unlock()
_, ok := s.mu.knownTenants[ipAddress]
if ok {
_, ok = s.mu.knownTenants[ipAddress][tenID]
}
return ok
}
func (s *localService) addLocked(addr string) *limiter {
if len(s.mu.addrs) >= s.maxMapSize {
addr := s.mu.addrs[rand.Intn(len(s.mu.limiters))]
s.evictLocked(addr)
}
l := &limiter{
index: len(s.mu.limiters),
}
s.mu.limiters[addr] = l
s.mu.addrs = append(s.mu.addrs, addr)
return l
}
func (s *localService) evictLocked(addr string) {
l := s.mu.limiters[addr]
if l == nil {
return
}
// Swap the address we're evicting to the end of the address array.
n := len(s.mu.addrs) - 1
s.mu.addrs[l.index], s.mu.addrs[n] = s.mu.addrs[n], s.mu.addrs[l.index]
// Fix-up the index of the limiter we're keeping.
s.mu.limiters[s.mu.addrs[l.index]].index = l.index
// Trim the evicted address from the address array.
s.mu.addrs = s.mu.addrs[:n]
// Delete the address from the limiters map.
delete(s.mu.limiters, addr)
}
func (s *localService) nextLimitLocked(l *limiter) {
// This calculation implements a simple capped exponential backoff. No
// randomization is done. We could use github.com/cenkalti/backoff, but this
// gives us a more control over the precise calculation and is about half the
// size in terms of memory usage. The latter part may be important for IP
// address based admission control.
delay := s.baseDelay * (1 << l.attempts)
if delay >= s.maxDelay {
delay = s.maxDelay
}
l.attempts++
l.nextTime = s.clock().Add(delay)
}