summaryrefslogtreecommitdiffstats
path: root/vendor/gopkg.in/throttled/throttled.v2/store/storetest/storetest.go
blob: 2233ebdfb46261e394321f0e893dd2915aa30a6a (plain)
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
168
169
170
171
172
173
174
175
176
// Package storetest provides a helper for testing throttled stores.
package storetest // import "github.com/throttled/throttled/store/storetest"

import (
	"math/rand"
	"strconv"
	"sync/atomic"
	"testing"
	"time"

	"github.com/throttled/throttled"
)

// TestGCRAStore tests the behavior of a GCRAStore implementation for
// compliance with the throttled API. It does not require support
// for TTLs.
func TestGCRAStore(t *testing.T, st throttled.GCRAStore) {
	// GetWithTime a missing key
	if have, _, err := st.GetWithTime("foo"); err != nil {
		t.Fatal(err)
	} else if have != -1 {
		t.Errorf("expected GetWithTime to return -1 for a missing key but got %d", have)
	}

	// SetIfNotExists on a new key
	want := int64(1)

	if set, err := st.SetIfNotExistsWithTTL("foo", want, 0); err != nil {
		t.Fatal(err)
	} else if !set {
		t.Errorf("expected SetIfNotExists on an empty key to succeed")
	}

	before := time.Now()

	if have, now, err := st.GetWithTime("foo"); err != nil {
		t.Fatal(err)
	} else if have != want {
		t.Errorf("expected GetWithTime to return %d but got %d", want, have)
	} else if now.UnixNano() <= 0 {
		t.Errorf("expected GetWithTime to return a time representable representable as a positive int64 of nanoseconds since the epoch")
	} else if now.Before(before) || now.After(time.Now()) {
		// Note that we make the assumption here that the store is running on
		// the same machine as this test and thus shares a clock. This can be a
		// little tricky in the case of Redis, which could be running
		// elsewhere. The test assumes that it's running either locally on on
		// Travis (where currently the Redis is available on localhost). If new
		// test environments are procured, this may need to be revisited.
		t.Errorf("expected GetWithTime to return a time between the time before the call and the time after the call")
	}

	// SetIfNotExists on an existing key
	if set, err := st.SetIfNotExistsWithTTL("foo", 123, 0); err != nil {
		t.Fatal(err)
	} else if set {
		t.Errorf("expected SetIfNotExists on an existing key to fail")
	}

	if have, _, err := st.GetWithTime("foo"); err != nil {
		t.Fatal(err)
	} else if have != want {
		t.Errorf("expected GetWithTime to return %d but got %d", want, have)
	}

	// SetIfNotExists on a different key
	if set, err := st.SetIfNotExistsWithTTL("bar", 456, 0); err != nil {
		t.Fatal(err)
	} else if !set {
		t.Errorf("expected SetIfNotExists on an empty key to succeed")
	}

	// Returns the false on a missing key
	if swapped, err := st.CompareAndSwapWithTTL("baz", 1, 2, 0); err != nil {
		t.Fatal(err)
	} else if swapped {
		t.Errorf("expected CompareAndSwap to fail on a missing key")
	}

	// Test a successful CAS
	want = int64(2)

	if swapped, err := st.CompareAndSwapWithTTL("foo", 1, want, 0); err != nil {
		t.Fatal(err)
	} else if !swapped {
		t.Errorf("expected CompareAndSwap to succeed")
	}

	if have, _, err := st.GetWithTime("foo"); err != nil {
		t.Fatal(err)
	} else if have != want {
		t.Errorf("expected GetWithTime to return %d but got %d", want, have)
	}

	// Test an unsuccessful CAS
	if swapped, err := st.CompareAndSwapWithTTL("foo", 1, 2, 0); err != nil {
		t.Fatal(err)
	} else if swapped {
		t.Errorf("expected CompareAndSwap to fail")
	}

	if have, _, err := st.GetWithTime("foo"); err != nil {
		t.Fatal(err)
	} else if have != want {
		t.Errorf("expected GetWithTime to return %d but got %d", want, have)
	}
}

// TestGCRAStoreTTL tests the behavior of TTLs in a GCRAStore implementation.
func TestGCRAStoreTTL(t *testing.T, st throttled.GCRAStore) {
	ttl := time.Second
	want := int64(1)
	key := "ttl"

	if _, err := st.SetIfNotExistsWithTTL(key, want, ttl); err != nil {
		t.Fatal(err)
	}

	if have, _, err := st.GetWithTime(key); err != nil {
		t.Fatal(err)
	} else if have != want {
		t.Errorf("expected GetWithTime to return %d, got %d", want, have)
	}

	// I can't think of a generic way to test expiration without a sleep
	time.Sleep(ttl + time.Millisecond)

	if have, _, err := st.GetWithTime(key); err != nil {
		t.Fatal(err)
	} else if have != -1 {
		t.Errorf("expected GetWithTime to fail on an expired key but got %d", have)
	}
}

// BenchmarkGCRAStore runs parallel benchmarks against a GCRAStore implementation.
// Aside from being useful for performance testing, this is useful for finding
// race conditions with the Go race detector.
func BenchmarkGCRAStore(b *testing.B, st throttled.GCRAStore) {
	seed := int64(42)
	var attempts, updates int64

	b.RunParallel(func(pb *testing.PB) {
		// We need atomic behavior around the RNG or go detects a race in the test
		delta := int64(1)
		seedValue := atomic.AddInt64(&seed, delta) - delta
		gen := rand.New(rand.NewSource(seedValue))

		for pb.Next() {
			key := strconv.FormatInt(gen.Int63n(50), 10)

			var v int64
			var updated bool

			v, _, err := st.GetWithTime(key)
			if v == -1 {
				updated, err = st.SetIfNotExistsWithTTL(key, gen.Int63(), 0)
				if err != nil {
					b.Error(err)
				}
			} else if err != nil {
				b.Error(err)
			} else {
				updated, err = st.CompareAndSwapWithTTL(key, v, gen.Int63(), 0)
				if err != nil {
					b.Error(err)
				}
			}

			atomic.AddInt64(&attempts, 1)
			if updated {
				atomic.AddInt64(&updates, 1)
			}
		}
	})

	b.Logf("%d/%d update operations succeeed", updates, attempts)
}