Compare commits
No commits in common. "master" and "v1.1.0" have entirely different histories.
5
go.mod
5
go.mod
@ -2,7 +2,4 @@ module git.crumpington.com/public/toolbox
|
||||
|
||||
go 1.16
|
||||
|
||||
require (
|
||||
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68
|
||||
golang.org/x/term v0.0.0-20210317153231-de623e64d2a6
|
||||
)
|
||||
require golang.org/x/term v0.0.0-20210317153231-de623e64d2a6
|
||||
|
@ -1,67 +0,0 @@
|
||||
package keyedmutex
|
||||
|
||||
import (
|
||||
"container/list"
|
||||
"sync"
|
||||
)
|
||||
|
||||
type KeyedMutex struct {
|
||||
mu *sync.Mutex
|
||||
waitList map[string]*list.List
|
||||
}
|
||||
|
||||
func New() KeyedMutex {
|
||||
return KeyedMutex{
|
||||
mu: new(sync.Mutex),
|
||||
waitList: map[string]*list.List{},
|
||||
}
|
||||
}
|
||||
|
||||
func (m KeyedMutex) Lock(key string) {
|
||||
if ch := m.lock(key); ch != nil {
|
||||
<-ch
|
||||
}
|
||||
}
|
||||
|
||||
func (m KeyedMutex) lock(key string) chan struct{} {
|
||||
m.mu.Lock()
|
||||
defer m.mu.Unlock()
|
||||
|
||||
if waitList, ok := m.waitList[key]; ok {
|
||||
ch := make(chan struct{})
|
||||
waitList.PushBack(ch)
|
||||
return ch
|
||||
}
|
||||
|
||||
m.waitList[key] = list.New()
|
||||
return nil
|
||||
}
|
||||
|
||||
func (m KeyedMutex) TryLock(key string) bool {
|
||||
m.mu.Lock()
|
||||
defer m.mu.Unlock()
|
||||
|
||||
if _, ok := m.waitList[key]; ok {
|
||||
return false
|
||||
}
|
||||
|
||||
m.waitList[key] = list.New()
|
||||
return true
|
||||
}
|
||||
|
||||
func (m KeyedMutex) Unlock(key string) {
|
||||
m.mu.Lock()
|
||||
defer m.mu.Unlock()
|
||||
|
||||
waitList, ok := m.waitList[key]
|
||||
if !ok {
|
||||
panic("unlock of unlocked mutex")
|
||||
}
|
||||
|
||||
if waitList.Len() == 0 {
|
||||
delete(m.waitList, key)
|
||||
} else {
|
||||
ch := waitList.Remove(waitList.Front()).(chan struct{})
|
||||
ch <- struct{}{}
|
||||
}
|
||||
}
|
@ -1,122 +0,0 @@
|
||||
package keyedmutex
|
||||
|
||||
import (
|
||||
"sync"
|
||||
"testing"
|
||||
"time"
|
||||
)
|
||||
|
||||
func TestKeyedMutex(t *testing.T) {
|
||||
checkState := func(t *testing.T, m KeyedMutex, keys ...string) {
|
||||
if len(m.waitList) != len(keys) {
|
||||
t.Fatal(m.waitList, keys)
|
||||
}
|
||||
|
||||
for _, key := range keys {
|
||||
if _, ok := m.waitList[key]; !ok {
|
||||
t.Fatal(key)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
m := New()
|
||||
checkState(t, m)
|
||||
|
||||
m.Lock("a")
|
||||
checkState(t, m, "a")
|
||||
m.Lock("b")
|
||||
checkState(t, m, "a", "b")
|
||||
m.Lock("c")
|
||||
checkState(t, m, "a", "b", "c")
|
||||
|
||||
if m.TryLock("a") {
|
||||
t.Fatal("a")
|
||||
}
|
||||
if m.TryLock("b") {
|
||||
t.Fatal("b")
|
||||
}
|
||||
if m.TryLock("c") {
|
||||
t.Fatal("c")
|
||||
}
|
||||
|
||||
if !m.TryLock("d") {
|
||||
t.Fatal("d")
|
||||
}
|
||||
|
||||
checkState(t, m, "a", "b", "c", "d")
|
||||
|
||||
if !m.TryLock("e") {
|
||||
t.Fatal("e")
|
||||
}
|
||||
checkState(t, m, "a", "b", "c", "d", "e")
|
||||
|
||||
m.Unlock("c")
|
||||
checkState(t, m, "a", "b", "d", "e")
|
||||
m.Unlock("a")
|
||||
checkState(t, m, "b", "d", "e")
|
||||
m.Unlock("e")
|
||||
checkState(t, m, "b", "d")
|
||||
|
||||
wg := sync.WaitGroup{}
|
||||
for i := 0; i < 8; i++ {
|
||||
wg.Add(1)
|
||||
go func() {
|
||||
defer wg.Done()
|
||||
m.Lock("b")
|
||||
m.Unlock("b")
|
||||
}()
|
||||
}
|
||||
|
||||
time.Sleep(100 * time.Millisecond)
|
||||
m.Unlock("b")
|
||||
wg.Wait()
|
||||
|
||||
checkState(t, m, "d")
|
||||
|
||||
m.Unlock("d")
|
||||
checkState(t, m)
|
||||
}
|
||||
|
||||
func TestKeyedMutex_unlockUnlocked(t *testing.T) {
|
||||
defer func() {
|
||||
if r := recover(); r == nil {
|
||||
t.Fatal(r)
|
||||
}
|
||||
}()
|
||||
|
||||
m := New()
|
||||
m.Unlock("aldkfj")
|
||||
}
|
||||
|
||||
func BenchmarkUncontendedMutex(b *testing.B) {
|
||||
m := New()
|
||||
key := "xyz"
|
||||
|
||||
for i := 0; i < b.N; i++ {
|
||||
m.Lock(key)
|
||||
m.Unlock(key)
|
||||
}
|
||||
}
|
||||
|
||||
func BenchmarkContendedMutex(b *testing.B) {
|
||||
m := New()
|
||||
key := "xyz"
|
||||
|
||||
m.Lock(key)
|
||||
|
||||
wg := sync.WaitGroup{}
|
||||
for i := 0; i < b.N; i++ {
|
||||
wg.Add(1)
|
||||
go func() {
|
||||
defer wg.Done()
|
||||
m.Lock(key)
|
||||
m.Unlock(key)
|
||||
}()
|
||||
}
|
||||
|
||||
time.Sleep(time.Second)
|
||||
|
||||
b.ResetTimer()
|
||||
m.Unlock(key)
|
||||
wg.Wait()
|
||||
}
|
@ -1,76 +0,0 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"math/rand"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"git.crumpington.com/public/toolbox/kvmemcache"
|
||||
)
|
||||
|
||||
const (
|
||||
mean = 1000000
|
||||
stdDev = 100
|
||||
computeTime = 2 * time.Millisecond
|
||||
|
||||
cacheSize = 800
|
||||
cacheTTL = time.Second
|
||||
|
||||
numThreads = 8
|
||||
numCalls = 1024 * 1024
|
||||
)
|
||||
|
||||
func randKey() string {
|
||||
fSample := rand.NormFloat64()*stdDev + mean
|
||||
sample := int64(fSample)
|
||||
return fmt.Sprintf("%d", sample)
|
||||
}
|
||||
|
||||
func run(c *kvmemcache.Cache, wg *sync.WaitGroup) {
|
||||
defer wg.Done()
|
||||
for i := 0; i < numCalls; i++ {
|
||||
key := randKey()
|
||||
_, _ = c.Get(key)
|
||||
}
|
||||
}
|
||||
|
||||
func main() {
|
||||
c := kvmemcache.New(kvmemcache.Config{
|
||||
MaxSize: cacheSize,
|
||||
TTL: cacheTTL,
|
||||
Src: func(key string) (interface{}, error) {
|
||||
time.Sleep(computeTime)
|
||||
return key, nil
|
||||
},
|
||||
})
|
||||
|
||||
wg := &sync.WaitGroup{}
|
||||
wg.Add(numThreads)
|
||||
|
||||
t0 := time.Now()
|
||||
|
||||
for i := 0; i < numThreads; i++ {
|
||||
go run(c, wg)
|
||||
}
|
||||
|
||||
wg.Wait()
|
||||
|
||||
dt := time.Since(t0)
|
||||
dtSec := float64(dt) / float64(time.Second)
|
||||
stats := c.Stats()
|
||||
|
||||
fmt.Printf(`
|
||||
Time taken: %12.3f sec
|
||||
Hits: %12d
|
||||
Misses: %12d
|
||||
Calls/sec: %12.3f
|
||||
Speedup: %12.3f
|
||||
`,
|
||||
dtSec,
|
||||
stats.Hits,
|
||||
stats.Misses,
|
||||
float64(stats.Hits+stats.Misses)/dtSec,
|
||||
float64(numThreads*numCalls*computeTime)/float64(dt),
|
||||
)
|
||||
}
|
@ -1,45 +0,0 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"log"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"git.crumpington.com/public/toolbox/kvmemcache"
|
||||
)
|
||||
|
||||
func main() {
|
||||
N := 1024 * 2048
|
||||
trigger := make(chan bool)
|
||||
|
||||
c := kvmemcache.New(kvmemcache.Config{
|
||||
MaxSize: 4,
|
||||
Src: func(key string) (interface{}, error) {
|
||||
<-trigger
|
||||
return key, nil
|
||||
},
|
||||
})
|
||||
|
||||
readyGroup := sync.WaitGroup{}
|
||||
doneGroup := sync.WaitGroup{}
|
||||
|
||||
readyGroup.Add(N)
|
||||
doneGroup.Add(N)
|
||||
|
||||
for i := 0; i < N; i++ {
|
||||
go func() {
|
||||
readyGroup.Done()
|
||||
c.Get("a")
|
||||
doneGroup.Done()
|
||||
}()
|
||||
}
|
||||
|
||||
readyGroup.Wait()
|
||||
|
||||
t0 := time.Now()
|
||||
trigger <- true
|
||||
doneGroup.Wait()
|
||||
dt := time.Since(t0)
|
||||
|
||||
log.Printf("%v", dt)
|
||||
}
|
@ -1,68 +0,0 @@
|
||||
package kvmemcache
|
||||
|
||||
import (
|
||||
"container/list"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"git.crumpington.com/public/toolbox/keyedmutex"
|
||||
)
|
||||
|
||||
type Cache struct {
|
||||
updateLock keyedmutex.KeyedMutex
|
||||
src func(string) (interface{}, error)
|
||||
ttl time.Duration
|
||||
maxSize int
|
||||
|
||||
// Lock protects cache, ll, and stats.
|
||||
lock sync.Mutex
|
||||
cache map[string]*list.Element
|
||||
ll *list.List
|
||||
stats Stats
|
||||
}
|
||||
|
||||
type lruItem struct {
|
||||
key string
|
||||
createdAt time.Time
|
||||
value interface{}
|
||||
err error
|
||||
}
|
||||
|
||||
type Config struct {
|
||||
MaxSize int
|
||||
TTL time.Duration // Zero to ignore.
|
||||
Src func(string) (interface{}, error)
|
||||
}
|
||||
|
||||
func New(conf Config) *Cache {
|
||||
return &Cache{
|
||||
updateLock: keyedmutex.New(),
|
||||
src: conf.Src,
|
||||
ttl: conf.TTL,
|
||||
maxSize: conf.MaxSize,
|
||||
lock: sync.Mutex{},
|
||||
cache: make(map[string]*list.Element, conf.MaxSize+1),
|
||||
ll: list.New(),
|
||||
}
|
||||
}
|
||||
|
||||
func (c *Cache) Get(key string) (interface{}, error) {
|
||||
ok, val, err := c.get(key)
|
||||
if ok {
|
||||
return val, err
|
||||
}
|
||||
|
||||
return c.load(key)
|
||||
}
|
||||
|
||||
func (c *Cache) Evict(key string) {
|
||||
c.lock.Lock()
|
||||
defer c.lock.Unlock()
|
||||
c.evict(key)
|
||||
}
|
||||
|
||||
func (c *Cache) Stats() Stats {
|
||||
c.lock.Lock()
|
||||
defer c.lock.Unlock()
|
||||
return c.stats
|
||||
}
|
@ -1,207 +0,0 @@
|
||||
package kvmemcache
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"sync"
|
||||
"testing"
|
||||
"time"
|
||||
)
|
||||
|
||||
type State struct {
|
||||
Keys []string
|
||||
Stats Stats
|
||||
}
|
||||
|
||||
func (c *Cache) assert(state State) error {
|
||||
c.lock.Lock()
|
||||
defer c.lock.Unlock()
|
||||
|
||||
if len(c.cache) != len(state.Keys) {
|
||||
return fmt.Errorf(
|
||||
"Expected %d keys but found %d.",
|
||||
len(state.Keys),
|
||||
len(c.cache))
|
||||
}
|
||||
|
||||
for _, k := range state.Keys {
|
||||
if _, ok := c.cache[k]; !ok {
|
||||
return fmt.Errorf(
|
||||
"Expected key %s not found.",
|
||||
k)
|
||||
}
|
||||
}
|
||||
|
||||
if c.stats.Hits != state.Stats.Hits {
|
||||
return fmt.Errorf(
|
||||
"Expected %d hits, but found %d.",
|
||||
state.Stats.Hits,
|
||||
c.stats.Hits)
|
||||
}
|
||||
|
||||
if c.stats.Misses != state.Stats.Misses {
|
||||
return fmt.Errorf(
|
||||
"Expected %d misses, but found %d.",
|
||||
state.Stats.Misses,
|
||||
c.stats.Misses)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
var ErrTest = errors.New("Hello")
|
||||
|
||||
func TestCache_basic(t *testing.T) {
|
||||
c := New(Config{
|
||||
MaxSize: 4,
|
||||
TTL: 50 * time.Millisecond,
|
||||
Src: func(key string) (interface{}, error) {
|
||||
if key == "err" {
|
||||
return nil, ErrTest
|
||||
}
|
||||
return key, nil
|
||||
},
|
||||
})
|
||||
|
||||
type testCase struct {
|
||||
name string
|
||||
sleep time.Duration
|
||||
key string
|
||||
evict bool
|
||||
state State
|
||||
}
|
||||
|
||||
cases := []testCase{
|
||||
{
|
||||
name: "get a",
|
||||
key: "a",
|
||||
state: State{
|
||||
Keys: []string{"a"},
|
||||
Stats: Stats{Hits: 0, Misses: 1},
|
||||
},
|
||||
}, {
|
||||
name: "get a again",
|
||||
key: "a",
|
||||
state: State{
|
||||
Keys: []string{"a"},
|
||||
Stats: Stats{Hits: 1, Misses: 1},
|
||||
},
|
||||
}, {
|
||||
name: "sleep, then get a again",
|
||||
sleep: 55 * time.Millisecond,
|
||||
key: "a",
|
||||
state: State{
|
||||
Keys: []string{"a"},
|
||||
Stats: Stats{Hits: 1, Misses: 2},
|
||||
},
|
||||
}, {
|
||||
name: "get b",
|
||||
key: "b",
|
||||
state: State{
|
||||
Keys: []string{"a", "b"},
|
||||
Stats: Stats{Hits: 1, Misses: 3},
|
||||
},
|
||||
}, {
|
||||
name: "get c",
|
||||
key: "c",
|
||||
state: State{
|
||||
Keys: []string{"a", "b", "c"},
|
||||
Stats: Stats{Hits: 1, Misses: 4},
|
||||
},
|
||||
}, {
|
||||
name: "get d",
|
||||
key: "d",
|
||||
state: State{
|
||||
Keys: []string{"a", "b", "c", "d"},
|
||||
Stats: Stats{Hits: 1, Misses: 5},
|
||||
},
|
||||
}, {
|
||||
name: "get e",
|
||||
key: "e",
|
||||
state: State{
|
||||
Keys: []string{"b", "c", "d", "e"},
|
||||
Stats: Stats{Hits: 1, Misses: 6},
|
||||
},
|
||||
}, {
|
||||
name: "get c again",
|
||||
key: "c",
|
||||
state: State{
|
||||
Keys: []string{"b", "c", "d", "e"},
|
||||
Stats: Stats{Hits: 2, Misses: 6},
|
||||
},
|
||||
}, {
|
||||
name: "get err",
|
||||
key: "err",
|
||||
state: State{
|
||||
Keys: []string{"c", "d", "e", "err"},
|
||||
Stats: Stats{Hits: 2, Misses: 7},
|
||||
},
|
||||
}, {
|
||||
name: "get err again",
|
||||
key: "err",
|
||||
state: State{
|
||||
Keys: []string{"c", "d", "e", "err"},
|
||||
Stats: Stats{Hits: 3, Misses: 7},
|
||||
},
|
||||
}, {
|
||||
name: "evict c",
|
||||
key: "c",
|
||||
evict: true,
|
||||
state: State{
|
||||
Keys: []string{"d", "e", "err"},
|
||||
Stats: Stats{Hits: 3, Misses: 7},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range cases {
|
||||
time.Sleep(tc.sleep)
|
||||
if !tc.evict {
|
||||
val, err := c.Get(tc.key)
|
||||
if tc.key == "err" && err != ErrTest {
|
||||
t.Fatal(tc.name, val)
|
||||
}
|
||||
if tc.key != "err" && val.(string) != tc.key {
|
||||
t.Fatal(tc.name, tc.key, val)
|
||||
}
|
||||
} else {
|
||||
c.Evict(tc.key)
|
||||
}
|
||||
|
||||
if err := c.assert(tc.state); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestCache_thunderingHerd(t *testing.T) {
|
||||
c := New(Config{
|
||||
MaxSize: 4,
|
||||
Src: func(key string) (interface{}, error) {
|
||||
time.Sleep(time.Second)
|
||||
return key, nil
|
||||
},
|
||||
})
|
||||
|
||||
wg := sync.WaitGroup{}
|
||||
for i := 0; i < 1024; i++ {
|
||||
wg.Add(1)
|
||||
go func() {
|
||||
defer wg.Done()
|
||||
val, err := c.Get("a")
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
if val != "a" {
|
||||
panic(err)
|
||||
}
|
||||
}()
|
||||
}
|
||||
|
||||
wg.Wait()
|
||||
|
||||
stats := c.Stats()
|
||||
if stats.Hits != 1023 || stats.Misses != 1 {
|
||||
t.Fatal(stats)
|
||||
}
|
||||
}
|
@ -1,67 +0,0 @@
|
||||
package kvmemcache
|
||||
|
||||
import (
|
||||
"time"
|
||||
)
|
||||
|
||||
func (c *Cache) put(key string, value interface{}, err error) {
|
||||
c.lock.Lock()
|
||||
defer c.lock.Unlock()
|
||||
c.stats.Misses++
|
||||
c.cache[key] = c.ll.PushFront(lruItem{
|
||||
key: key,
|
||||
createdAt: time.Now(),
|
||||
value: value,
|
||||
err: err,
|
||||
})
|
||||
|
||||
if c.maxSize != 0 && len(c.cache) > c.maxSize {
|
||||
li := c.ll.Back()
|
||||
c.ll.Remove(li)
|
||||
delete(c.cache, li.Value.(lruItem).key)
|
||||
}
|
||||
}
|
||||
|
||||
func (c *Cache) evict(key string) {
|
||||
elem := c.cache[key]
|
||||
if elem != nil {
|
||||
delete(c.cache, key)
|
||||
c.ll.Remove(elem)
|
||||
}
|
||||
}
|
||||
|
||||
func (c *Cache) get(key string) (ok bool, val interface{}, err error) {
|
||||
c.lock.Lock()
|
||||
defer c.lock.Unlock()
|
||||
|
||||
li := c.cache[key]
|
||||
if li == nil {
|
||||
return false, nil, nil
|
||||
}
|
||||
|
||||
item := li.Value.(lruItem)
|
||||
// Maybe evict.
|
||||
if c.ttl != 0 && time.Since(item.createdAt) > c.ttl {
|
||||
c.evict(key)
|
||||
return false, nil, nil
|
||||
}
|
||||
|
||||
c.stats.Hits++
|
||||
return true, item.value, item.err
|
||||
}
|
||||
|
||||
func (c *Cache) load(key string) (interface{}, error) {
|
||||
c.updateLock.Lock(key)
|
||||
defer c.updateLock.Unlock(key)
|
||||
|
||||
// Check again in case we lost the update race.
|
||||
ok, val, err := c.get(key)
|
||||
if ok {
|
||||
return val, err
|
||||
}
|
||||
|
||||
// Won the update race.
|
||||
val, err = c.src(key)
|
||||
c.put(key, val, err)
|
||||
return val, err
|
||||
}
|
@ -1,6 +0,0 @@
|
||||
package kvmemcache
|
||||
|
||||
type Stats struct {
|
||||
Hits uint64
|
||||
Misses uint64
|
||||
}
|
@ -1,73 +0,0 @@
|
||||
package ratelimiter
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"sync"
|
||||
"time"
|
||||
)
|
||||
|
||||
var ErrBackoff = errors.New("Backoff")
|
||||
|
||||
type Config struct {
|
||||
BurstLimit int64 // Number of requests to allow to burst.
|
||||
FillPeriod time.Duration // Add one call per period.
|
||||
MaxWaitCount int64 // Max number of waiting requests. 0 disables.
|
||||
}
|
||||
|
||||
type Limiter struct {
|
||||
lock sync.Mutex
|
||||
|
||||
fillPeriod time.Duration
|
||||
minWaitTime time.Duration
|
||||
maxWaitTime time.Duration
|
||||
|
||||
waitTime time.Duration
|
||||
lastRequest time.Time
|
||||
}
|
||||
|
||||
func New(conf Config) *Limiter {
|
||||
if conf.BurstLimit < 0 {
|
||||
panic(conf.BurstLimit)
|
||||
}
|
||||
if conf.FillPeriod <= 0 {
|
||||
panic(conf.FillPeriod)
|
||||
}
|
||||
if conf.MaxWaitCount < 0 {
|
||||
panic(conf.MaxWaitCount)
|
||||
}
|
||||
|
||||
return &Limiter{
|
||||
fillPeriod: conf.FillPeriod,
|
||||
waitTime: -conf.FillPeriod * time.Duration(conf.BurstLimit),
|
||||
minWaitTime: -conf.FillPeriod * time.Duration(conf.BurstLimit),
|
||||
maxWaitTime: conf.FillPeriod * time.Duration(conf.MaxWaitCount-1),
|
||||
lastRequest: time.Now(),
|
||||
}
|
||||
}
|
||||
|
||||
func (lim *Limiter) limit() (time.Duration, error) {
|
||||
lim.lock.Lock()
|
||||
defer lim.lock.Unlock()
|
||||
|
||||
dt := time.Since(lim.lastRequest)
|
||||
waitTime := lim.waitTime - dt
|
||||
if waitTime < lim.minWaitTime {
|
||||
waitTime = lim.minWaitTime
|
||||
} else if waitTime >= lim.maxWaitTime {
|
||||
return 0, ErrBackoff
|
||||
}
|
||||
|
||||
lim.waitTime = waitTime + lim.fillPeriod
|
||||
lim.lastRequest = lim.lastRequest.Add(dt)
|
||||
|
||||
return lim.waitTime, nil
|
||||
}
|
||||
|
||||
// Apply the limiter to the calling thread. The function may sleep for up to
|
||||
// maxWaitTime before returning. If the timeout would need to be more than
|
||||
// maxWaitTime to enforce the rate limit, ErrBackoff is returned.
|
||||
func (lim *Limiter) Limit() error {
|
||||
dt, err := lim.limit()
|
||||
time.Sleep(dt) // Will return immediately for dt <= 0.
|
||||
return err
|
||||
}
|
@ -1,101 +0,0 @@
|
||||
package ratelimiter
|
||||
|
||||
import (
|
||||
"sync"
|
||||
"testing"
|
||||
"time"
|
||||
)
|
||||
|
||||
func TestRateLimiter_Limit_Errors(t *testing.T) {
|
||||
type TestCase struct {
|
||||
Name string
|
||||
Conf Config
|
||||
N int
|
||||
ErrCount int
|
||||
DT time.Duration
|
||||
}
|
||||
|
||||
cases := []TestCase{
|
||||
{
|
||||
Name: "no burst, no wait",
|
||||
Conf: Config{
|
||||
BurstLimit: 0,
|
||||
FillPeriod: 100 * time.Millisecond,
|
||||
MaxWaitCount: 0,
|
||||
},
|
||||
N: 32,
|
||||
ErrCount: 31,
|
||||
DT: 100 * time.Millisecond,
|
||||
}, {
|
||||
Name: "no wait",
|
||||
Conf: Config{
|
||||
BurstLimit: 10,
|
||||
FillPeriod: 100 * time.Millisecond,
|
||||
MaxWaitCount: 0,
|
||||
},
|
||||
N: 32,
|
||||
ErrCount: 22,
|
||||
DT: 0,
|
||||
}, {
|
||||
Name: "no burst",
|
||||
Conf: Config{
|
||||
BurstLimit: 0,
|
||||
FillPeriod: 10 * time.Millisecond,
|
||||
MaxWaitCount: 10,
|
||||
},
|
||||
N: 32,
|
||||
ErrCount: 22,
|
||||
DT: 100 * time.Millisecond,
|
||||
}, {
|
||||
Name: "burst and wait",
|
||||
Conf: Config{
|
||||
BurstLimit: 10,
|
||||
FillPeriod: 10 * time.Millisecond,
|
||||
MaxWaitCount: 10,
|
||||
},
|
||||
N: 32,
|
||||
ErrCount: 12,
|
||||
DT: 100 * time.Millisecond,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range cases {
|
||||
wg := sync.WaitGroup{}
|
||||
l := New(tc.Conf)
|
||||
errs := make([]error, tc.N)
|
||||
|
||||
t0 := time.Now()
|
||||
|
||||
for i := 0; i < tc.N; i++ {
|
||||
wg.Add(1)
|
||||
go func(i int) {
|
||||
errs[i] = l.Limit()
|
||||
wg.Done()
|
||||
}(i)
|
||||
}
|
||||
|
||||
wg.Wait()
|
||||
|
||||
dt := time.Since(t0)
|
||||
|
||||
errCount := 0
|
||||
for _, err := range errs {
|
||||
if err != nil {
|
||||
errCount++
|
||||
}
|
||||
}
|
||||
|
||||
if errCount != tc.ErrCount {
|
||||
t.Fatalf("%s: Expected %d errors but got %d.",
|
||||
tc.Name, tc.ErrCount, errCount)
|
||||
}
|
||||
|
||||
if dt < tc.DT {
|
||||
t.Fatal(tc.Name, dt, tc.DT)
|
||||
}
|
||||
|
||||
if dt > tc.DT+10*time.Millisecond {
|
||||
t.Fatal(tc.Name, dt, tc.DT)
|
||||
}
|
||||
}
|
||||
}
|
Loading…
Reference in New Issue
Block a user