Skip to content

Commit 121cf0a

Browse files
authored
FF-2331 Implement LRU assignment cache (#42)
1 parent cff41b6 commit 121cf0a

File tree

4 files changed

+298
-20
lines changed

4 files changed

+298
-20
lines changed

eppoclient/lruassignmentlogger.go

Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
package eppoclient
2+
3+
import (
4+
"github.com/hashicorp/golang-lru/v2"
5+
)
6+
7+
type LruAssignmentLogger struct {
8+
cache *lru.TwoQueueCache[cacheKey, cacheValue]
9+
inner IAssignmentLogger
10+
}
11+
12+
// We are only interested in whether a subject was ever a part of an
13+
// assignment. We are not interested in the order of assignments or
14+
// knowing the latest assignment. Therefore, both allocation and
15+
// variation are part of the cacheKey.
16+
type cacheKey struct {
17+
flag string
18+
subject string
19+
allocation string
20+
variation string
21+
}
22+
type cacheValue struct {
23+
}
24+
25+
func NewLruAssignmentLogger(logger IAssignmentLogger, cacheSize int) IAssignmentLogger {
26+
cache, err := lru.New2Q[cacheKey, cacheValue](cacheSize)
27+
if err != nil {
28+
// err is only returned if `cacheSize` is invalid
29+
// (e.g., <0) which should normally never happen.
30+
panic(err)
31+
}
32+
return &LruAssignmentLogger{
33+
cache: cache,
34+
inner: logger,
35+
}
36+
}
37+
38+
func (self *LruAssignmentLogger) LogAssignment(event AssignmentEvent) {
39+
key := cacheKey{
40+
flag: event.FeatureFlag,
41+
subject: event.Subject,
42+
allocation: event.Allocation,
43+
variation: event.Variation,
44+
}
45+
value := cacheValue{}
46+
previousValue, recentlyLogged := self.cache.Get(key)
47+
if !recentlyLogged || previousValue != value {
48+
self.inner.LogAssignment(event)
49+
// Adding to cache after `LogAssignment` returned in
50+
// case it panics.
51+
self.cache.Add(key, value)
52+
}
53+
}
Lines changed: 232 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,232 @@
1+
package eppoclient
2+
3+
import (
4+
"testing"
5+
6+
"github.com/stretchr/testify/assert"
7+
"github.com/stretchr/testify/mock"
8+
)
9+
10+
func Test_LruAssignmentLogger_cacheAssignment(t *testing.T) {
11+
innerLogger := new(mockLogger)
12+
innerLogger.On("LogAssignment", mock.Anything).Return()
13+
14+
logger := NewLruAssignmentLogger(innerLogger, 1000)
15+
16+
event := AssignmentEvent{
17+
Experiment: "testExperiment",
18+
FeatureFlag: "testFeatureFlag",
19+
Allocation: "testAllocation",
20+
Variation: "123.45",
21+
Subject: "testSubject",
22+
Timestamp: "testTimestamp",
23+
SubjectAttributes: SubjectAttributes{"testKey": "testValue"},
24+
}
25+
26+
logger.LogAssignment(event)
27+
logger.LogAssignment(event)
28+
29+
innerLogger.AssertNumberOfCalls(t, "LogAssignment", 1)
30+
}
31+
32+
func Test_LruAssignmentLogger_timestampAndAttributesAreNotImportant(t *testing.T) {
33+
innerLogger := new(mockLogger)
34+
innerLogger.On("LogAssignment", mock.Anything).Return()
35+
36+
logger := NewLruAssignmentLogger(innerLogger, 1000)
37+
38+
logger.LogAssignment(AssignmentEvent{
39+
FeatureFlag: "testFeatureFlag",
40+
Allocation: "testAllocation",
41+
Variation: "testVariation",
42+
Subject: "testSubject",
43+
Experiment: "testExperiment",
44+
Timestamp: "t1",
45+
SubjectAttributes: SubjectAttributes{"testKey": "testValue1"},
46+
})
47+
logger.LogAssignment(AssignmentEvent{
48+
FeatureFlag: "testFeatureFlag",
49+
Allocation: "testAllocation",
50+
Variation: "testVariation",
51+
Subject: "testSubject",
52+
Experiment: "testExperiment",
53+
Timestamp: "t2",
54+
SubjectAttributes: SubjectAttributes{"testKey": "testValue2"},
55+
})
56+
57+
innerLogger.AssertNumberOfCalls(t, "LogAssignment", 1)
58+
}
59+
60+
func Test_LruAssignmentLogger_panicsAreNotCached(t *testing.T) {
61+
innerLogger := new(mockLogger)
62+
innerLogger.On("LogAssignment", mock.Anything).Panic("test panic")
63+
64+
logger := NewLruAssignmentLogger(innerLogger, 1000)
65+
66+
event := AssignmentEvent{
67+
Experiment: "testExperiment",
68+
FeatureFlag: "testFeatureFlag",
69+
Allocation: "testAllocation",
70+
Variation: "123.45",
71+
Subject: "testSubject",
72+
Timestamp: "testTimestamp",
73+
SubjectAttributes: SubjectAttributes{"testKey": "testValue"},
74+
}
75+
76+
assert.Panics(t, func() {
77+
logger.LogAssignment(event)
78+
})
79+
assert.Panics(t, func() {
80+
logger.LogAssignment(event)
81+
})
82+
83+
innerLogger.AssertNumberOfCalls(t, "LogAssignment", 2)
84+
}
85+
86+
func Test_LruAssignmentLogger_changeInAllocationCausesLogging(t *testing.T) {
87+
innerLogger := new(mockLogger)
88+
innerLogger.On("LogAssignment", mock.Anything).Return()
89+
90+
logger := NewLruAssignmentLogger(innerLogger, 1000)
91+
92+
logger.LogAssignment(AssignmentEvent{
93+
Experiment: "testExperiment",
94+
FeatureFlag: "testFeatureFlag",
95+
Allocation: "testAllocation1",
96+
Variation: "variation",
97+
Subject: "testSubject",
98+
Timestamp: "testTimestamp",
99+
SubjectAttributes: SubjectAttributes{"testKey": "testValue"},
100+
})
101+
logger.LogAssignment(AssignmentEvent{
102+
Experiment: "testExperiment",
103+
FeatureFlag: "testFeatureFlag",
104+
Allocation: "testAllocation2",
105+
Variation: "variation",
106+
Subject: "testSubject",
107+
Timestamp: "testTimestamp",
108+
SubjectAttributes: SubjectAttributes{"testKey": "testValue"},
109+
})
110+
111+
innerLogger.AssertNumberOfCalls(t, "LogAssignment", 2)
112+
}
113+
114+
func Test_LruAssignmentLogger_changeInVariationCausesLogging(t *testing.T) {
115+
innerLogger := new(mockLogger)
116+
innerLogger.On("LogAssignment", mock.Anything).Return()
117+
118+
logger := NewLruAssignmentLogger(innerLogger, 1000)
119+
120+
logger.LogAssignment(AssignmentEvent{
121+
Experiment: "testExperiment",
122+
FeatureFlag: "testFeatureFlag",
123+
Allocation: "testAllocation",
124+
Variation: "variation1",
125+
Subject: "testSubject",
126+
Timestamp: "testTimestamp",
127+
SubjectAttributes: SubjectAttributes{"testKey": "testValue"},
128+
})
129+
logger.LogAssignment(AssignmentEvent{
130+
Experiment: "testExperiment",
131+
FeatureFlag: "testFeatureFlag",
132+
Allocation: "testAllocation",
133+
Variation: "variation2",
134+
Subject: "testSubject",
135+
Timestamp: "testTimestamp",
136+
SubjectAttributes: SubjectAttributes{"testKey": "testValue"},
137+
})
138+
139+
innerLogger.AssertNumberOfCalls(t, "LogAssignment", 2)
140+
}
141+
142+
func Test_LruAssignmentLogger_allocationOscillationOnlyLoggedOnce(t *testing.T) {
143+
innerLogger := new(mockLogger)
144+
innerLogger.On("LogAssignment", mock.Anything).Return()
145+
146+
logger := NewLruAssignmentLogger(innerLogger, 1000)
147+
148+
logger.LogAssignment(AssignmentEvent{
149+
Experiment: "testExperiment",
150+
FeatureFlag: "testFeatureFlag",
151+
Allocation: "testAllocation1",
152+
Variation: "variation",
153+
Subject: "testSubject",
154+
Timestamp: "t1",
155+
SubjectAttributes: SubjectAttributes{"testKey": "testValue"},
156+
})
157+
logger.LogAssignment(AssignmentEvent{
158+
Experiment: "testExperiment",
159+
FeatureFlag: "testFeatureFlag",
160+
Allocation: "testAllocation2",
161+
Variation: "variation",
162+
Subject: "testSubject",
163+
Timestamp: "t2",
164+
SubjectAttributes: SubjectAttributes{"testKey": "testValue"},
165+
})
166+
logger.LogAssignment(AssignmentEvent{
167+
Experiment: "testExperiment",
168+
FeatureFlag: "testFeatureFlag",
169+
Allocation: "testAllocation1",
170+
Variation: "variation",
171+
Subject: "testSubject",
172+
Timestamp: "t3",
173+
SubjectAttributes: SubjectAttributes{"testKey": "testValue"},
174+
})
175+
logger.LogAssignment(AssignmentEvent{
176+
Experiment: "testExperiment",
177+
FeatureFlag: "testFeatureFlag",
178+
Allocation: "testAllocation2",
179+
Variation: "variation",
180+
Subject: "testSubject",
181+
Timestamp: "t4",
182+
SubjectAttributes: SubjectAttributes{"testKey": "testValue"},
183+
})
184+
185+
innerLogger.AssertNumberOfCalls(t, "LogAssignment", 2)
186+
}
187+
188+
func Test_LruAssignmentLogger_variationOscillationOnlyLoggedOnce(t *testing.T) {
189+
innerLogger := new(mockLogger)
190+
innerLogger.On("LogAssignment", mock.Anything).Return()
191+
192+
logger := NewLruAssignmentLogger(innerLogger, 1000)
193+
194+
logger.LogAssignment(AssignmentEvent{
195+
Experiment: "testExperiment",
196+
FeatureFlag: "testFeatureFlag",
197+
Allocation: "testAllocation",
198+
Variation: "variation1",
199+
Subject: "testSubject",
200+
Timestamp: "t1",
201+
SubjectAttributes: SubjectAttributes{"testKey": "testValue"},
202+
})
203+
logger.LogAssignment(AssignmentEvent{
204+
Experiment: "testExperiment",
205+
FeatureFlag: "testFeatureFlag",
206+
Allocation: "testAllocation",
207+
Variation: "variation2",
208+
Subject: "testSubject",
209+
Timestamp: "t2",
210+
SubjectAttributes: SubjectAttributes{"testKey": "testValue"},
211+
})
212+
logger.LogAssignment(AssignmentEvent{
213+
Experiment: "testExperiment",
214+
FeatureFlag: "testFeatureFlag",
215+
Allocation: "testAllocation",
216+
Variation: "variation1",
217+
Subject: "testSubject",
218+
Timestamp: "t3",
219+
SubjectAttributes: SubjectAttributes{"testKey": "testValue"},
220+
})
221+
logger.LogAssignment(AssignmentEvent{
222+
Experiment: "testExperiment",
223+
FeatureFlag: "testFeatureFlag",
224+
Allocation: "testAllocation",
225+
Variation: "variation2",
226+
Subject: "testSubject",
227+
Timestamp: "t4",
228+
SubjectAttributes: SubjectAttributes{"testKey": "testValue"},
229+
})
230+
231+
innerLogger.AssertNumberOfCalls(t, "LogAssignment", 2)
232+
}

go.mod

Lines changed: 6 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -2,14 +2,15 @@ module github.com/Eppo-exp/golang-sdk/v3
22

33
go 1.19
44

5-
require github.com/stretchr/testify v1.8.0
6-
75
require (
86
github.com/Masterminds/semver/v3 v3.2.1
7+
github.com/hashicorp/golang-lru/v2 v2.0.7
8+
github.com/stretchr/testify v1.9.0
9+
)
10+
11+
require (
912
github.com/davecgh/go-spew v1.1.1 // indirect
10-
github.com/kr/pretty v0.1.0 // indirect
1113
github.com/pmezard/go-difflib v1.0.0 // indirect
12-
github.com/stretchr/objx v0.4.0 // indirect
13-
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127 // indirect
14+
github.com/stretchr/objx v0.5.2 // indirect
1415
gopkg.in/yaml.v3 v3.0.1 // indirect
1516
)

go.sum

Lines changed: 7 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -1,24 +1,16 @@
11
github.com/Masterminds/semver/v3 v3.2.1 h1:RN9w6+7QoMeJVGyfmbcgs28Br8cvmnucEXnY0rYXWg0=
22
github.com/Masterminds/semver/v3 v3.2.1/go.mod h1:qvl/7zhW3nngYb5+80sSMF+FG2BjYrf8m9wsX0PNOMQ=
3-
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
43
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
54
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
6-
github.com/kr/pretty v0.1.0 h1:L/CwN0zerZDmRFUapSPitk6f+Q3+0za1rQkzVuMiMFI=
7-
github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
8-
github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
9-
github.com/kr/text v0.1.0 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE=
10-
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
5+
github.com/hashicorp/golang-lru/v2 v2.0.7 h1:a+bsQ5rvGLjzHuww6tVxozPZFVghXaHOwFs4luLUK2k=
6+
github.com/hashicorp/golang-lru/v2 v2.0.7/go.mod h1:QeFd9opnmA6QUJc5vARoKUSoFhyfM2/ZepoAG6RGpeM=
117
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
128
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
13-
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
14-
github.com/stretchr/objx v0.4.0 h1:M2gUjqZET1qApGOWNSnZ49BAIMX4F/1plDv3+l31EJ4=
15-
github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
16-
github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
17-
github.com/stretchr/testify v1.8.0 h1:pSgiaMZlXftHpm5L7V1+rVB+AZJydKsMxsQBIJw4PKk=
18-
github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
9+
github.com/stretchr/objx v0.5.2 h1:xuMeJ0Sdp5ZMRXx/aWO6RZxdr3beISkG5/G/aIRr3pY=
10+
github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA=
11+
github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg=
12+
github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
13+
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=
1914
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
20-
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127 h1:qIbj1fsPNlZgppZ+VLlY7N33q108Sa+fhmuc+sWQYwY=
21-
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
22-
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
2315
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
2416
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=

0 commit comments

Comments
 (0)