// 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) }