Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 6 additions & 3 deletions snapshot/nil.go
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package snapshot

import (
"math/rand"
"time"

"github.com/lyft/goruntime/snapshot/entry"
Expand All @@ -10,13 +11,15 @@ import (
type Nil struct{}

func NewNil() (s *Nil) {
s = &Nil{}
return &Nil{}
}

return
var nilRandom = random{
rr: rand.New(rand.NewSource(time.Now().UnixNano())),
}

func (n *Nil) FeatureEnabled(key string, defaultValue uint64) bool {
return defaultRandomGenerator.Random()%100 < min(defaultValue, 100)
return nilRandom.Uint64()%100 < min(defaultValue, 100)
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Intn?

}

func (n *Nil) FeatureEnabledForID(key string, id uint64, defaultPercentage uint32) bool {
Expand Down
80 changes: 30 additions & 50 deletions snapshot/snapshot.go
Original file line number Diff line number Diff line change
Expand Up @@ -10,96 +10,79 @@ import (
"github.com/lyft/goruntime/snapshot/entry"
)

func min(lhs uint64, rhs uint64) uint64 {
if lhs < rhs {
return lhs
} else {
return rhs
}
}

// Random number generator. Implementations should be thread safe.
type RandomGenerator interface {
// @return uint64 a new random number.
Random() uint64
}

// Implementation of RandomGenerator that uses a time seeded random generator.
type randomGeneratorImpl struct {
sync.Mutex
random *rand.Rand
}

func (r *randomGeneratorImpl) Random() uint64 {
r.Lock()
v := uint64(r.random.Int63())
r.Unlock()
return v
type random struct {
mu sync.Mutex
rr *rand.Rand
}

var defaultRandomGenerator RandomGenerator = &randomGeneratorImpl{
random: rand.New(rand.NewSource(time.Now().UnixNano())),
func (r *random) Uint64() uint64 {
r.mu.Lock()
x := r.rr.Int63()
r.mu.Unlock()
Comment on lines +19 to +21
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should/could we use a threadsafe source instead of locking? Or will a threadsafe source also lock under the hood?

return uint64(x)
}

// Implementation of Snapshot for the filesystem loader.
type Snapshot struct {
entries map[string]*entry.Entry
rand random
}

func New() (s *Snapshot) {
s = &Snapshot{
rr := random{
rr: rand.New(rand.NewSource(time.Now().UnixNano())),
}
return &Snapshot{
entries: make(map[string]*entry.Entry),
rand: rr,
}
}

return
func min(lhs, rhs uint64) uint64 {
if lhs <= rhs {
return lhs
}
return rhs
}

func (s *Snapshot) FeatureEnabled(key string, defaultValue uint64) bool {
return defaultRandomGenerator.Random()%100 < min(s.GetInteger(key, defaultValue), 100)
return s.rand.Uint64()%100 < min(s.GetInteger(key, defaultValue), 100)
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should we use Intn(100) for readability? Is that the same thing?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes.

}

// FeatureEnabledForID checks that the crc32 of the id and key's byte value falls within the mod of
// the 0-100 value for the given feature. Use this method for "sticky" features
func (s *Snapshot) FeatureEnabledForID(key string, id uint64, defaultPercentage uint32) bool {
if e, ok := s.Entries()[key]; ok {
if e.Uint64Valid {
return enabled(id, uint32(e.Uint64Value), key)
}
if e := s.entries[key]; e != nil && e.Uint64Valid {
return enabled(id, uint32(e.Uint64Value), key)
}

return enabled(id, defaultPercentage, key)
}

func (s *Snapshot) Get(key string) string {
e, ok := s.entries[key]
if ok {
if e := s.entries[key]; e != nil {
return e.StringValue
} else {
return ""
}
return ""
}

func (s *Snapshot) GetInteger(key string, defaultValue uint64) uint64 {
e, ok := s.entries[key]
if ok && e.Uint64Valid {
if e := s.entries[key]; e != nil && e.Uint64Valid {
return e.Uint64Value
} else {
return defaultValue
}
return defaultValue
}

// GetModified returns the last modified timestamp for key. If key does not
// exist, the zero value for time.Time is returned.
func (s *Snapshot) GetModified(key string) time.Time {
if e, ok := s.entries[key]; ok {
if e := s.entries[key]; e != nil {
return e.Modified
}

return time.Time{}
}

func (s *Snapshot) Keys() []string {
ret := []string{}
ret := make([]string, 0, len(s.entries))
for key := range s.entries {
ret = append(ret, key)
Comment on lines +85 to 87
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
ret := make([]string, 0, len(s.entries))
for key := range s.entries {
ret = append(ret, key)
ret := make([]string, len(s.entries))
for i, key := range s.entries {
ret[i] = key

This is faster (?)

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It would be if s.entries we're a slice, but it's a map so this will actually fail to compile.

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Oops

}
Expand All @@ -116,14 +99,11 @@ func (s *Snapshot) SetEntry(key string, e *entry.Entry) {

func enabled(id uint64, percentage uint32, feature string) bool {
uid := crc(id, feature)

return uid%100 < percentage
}

func crc(id uint64, feature string) uint32 {
b := make([]byte, 8, len(feature)+8)
binary.LittleEndian.PutUint64(b, id)
b = append(b, []byte(feature)...)

return crc32.ChecksumIEEE(b)
return crc32.ChecksumIEEE(append(b, feature...))
}
84 changes: 73 additions & 11 deletions snapshot/snapshot_test.go
Original file line number Diff line number Diff line change
@@ -1,7 +1,9 @@
package snapshot

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

Expand All @@ -10,17 +12,18 @@ import (
)

func TestRandomGeneratorImpl_Random_Race(t *testing.T) {
rgi := &randomGeneratorImpl{random: rand.New(rand.NewSource(time.Now().UnixNano()))}

go func() {
for i := 0; i < 100; i++ {
rgi.Random()
}
}()

for i := 0; i < 100; i++ {
rgi.Random()
snap := New()
var wg sync.WaitGroup
for i := 0; i < 64; i++ {
wg.Add(1)
go func() {
defer wg.Done()
for i := 0; i < 100; i++ {
snap.rand.Uint64()
}
}()
}
wg.Wait()
}

func TestSnapshot_FeatureEnabledForID(t *testing.T) {
Expand Down Expand Up @@ -59,3 +62,62 @@ func TestSnapshot_GetModified(t *testing.T) {
ss.entries["foo"] = &entry.Entry{Modified: now}
assert.Equal(t, now, ss.GetModified("foo"))
}

func BenchmarkCRC(b *testing.B) {
const a = "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"
const key = a + a
Comment on lines +67 to +68
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why not initialize key to be a literal "aaaaa....a"?

if len(key) != 64 {
panic(len(key))
}
b.SetBytes(int64(8 + len(key)))
for i := 0; i < b.N; i++ {
crc(uint64(i), key)
}
}

func BenchmarkEnabled(b *testing.B) {
const a = "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"
const key = a + a
if len(key) != 64 {
panic(len(key))
}
b.SetBytes(int64(8 + len(key)))
for i := 0; i < b.N; i++ {
enabled(uint64(1), 50, key)
}
}

func setupFeatureEnabled(b *testing.B, key string) *Snapshot {
snap := New()
snap.entries[key] = &entry.Entry{
Uint64Value: 50,
Uint64Valid: true,
}
b.ResetTimer()
return snap
}

func BenchmarkFeatureEnabled(b *testing.B) {
const key = "this_is_a_test_key"
snap := setupFeatureEnabled(b, key)
for i := 0; i < b.N; i++ {
snap.FeatureEnabled(key, 100)
}
}

func BenchmarkFeatureEnabled_Parallel(b *testing.B) {
const key = "this_is_a_test_key"
var snaps [4]*Snapshot
for i := 0; i < 4; i++ {
snaps[i] = setupFeatureEnabled(b, key)
}
n := new(int32)
runtime.GOMAXPROCS(runtime.NumCPU() * 8)
b.RunParallel(func(pb *testing.PB) {
i := int(atomic.AddInt32(n, 1))
snap := snaps[i%len(snaps)]
for pb.Next() {
snap.FeatureEnabled(key, 100)
}
})
}