Skip to content

Commit aa9e9cd

Browse files
committed
FFM-9462 Refresh Segments/Flags keys when we get delete SSE events
**What** - Adds DeleteFlags/DeleteSegments functions that get called whenever we get a Delete SSE event **Why** - This fixes a bug where Flags/Segments keys wouldn't be refreshed. This doesn't have any impact on flag evaluations from the SDK. But does impact the Proxy which embeds this SDK and relies on it for updating its cache
1 parent 25c087a commit aa9e9cd

File tree

5 files changed

+131
-6
lines changed

5 files changed

+131
-6
lines changed

go.mod

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ require (
1717
github.com/spaolacci/murmur3 v1.1.0
1818
github.com/stretchr/testify v1.7.1
1919
go.uber.org/zap v1.16.0
20+
golang.org/x/exp v0.0.0-20230905200255-921286631fa9
2021
gopkg.in/cenkalti/backoff.v1 v1.1.0
2122
)
2223

go.sum

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -153,11 +153,13 @@ golang.org/x/crypto v0.0.0-20211215153901-e495a2d5b3d3/go.mod h1:IxCIyHEi3zRg3s0
153153
golang.org/x/crypto v0.0.0-20220427172511-eb4f295cb31f/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4=
154154
golang.org/x/crypto v0.0.0-20220513210258-46612604a0f9 h1:NUzdAbFtCJSXU20AOXgeqaUwg8Ypg4MPYmL+d+rsB5c=
155155
golang.org/x/crypto v0.0.0-20220513210258-46612604a0f9/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4=
156+
golang.org/x/exp v0.0.0-20230905200255-921286631fa9 h1:GoHiUyI/Tp2nVkLI2mCxVkOjsbSXD66ic0XW0js0R9g=
157+
golang.org/x/exp v0.0.0-20230905200255-921286631fa9/go.mod h1:S2oDrQGGwySpoQPVqRShND87VCbxmc6bL1Yd2oYrm6k=
156158
golang.org/x/lint v0.0.0-20190930215403-16217165b5de h1:5hukYrvBGR8/eNkX5mdUezrA6JiaEZDtJb9Ei+1LlBs=
157159
golang.org/x/lint v0.0.0-20190930215403-16217165b5de/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
158160
golang.org/x/mod v0.0.0-20190513183733-4bf6d317e70e/go.mod h1:mXi4GBBbnImb6dmsKGUJ2LatrhH/nqhxcFungHvyanc=
159-
golang.org/x/mod v0.6.0-dev.0.20220106191415-9b9b3d81d5e3 h1:kQgndtyPBW/JIYERgdxfwMYh3AVStj88WQTlNDi2a+o=
160161
golang.org/x/mod v0.6.0-dev.0.20220106191415-9b9b3d81d5e3/go.mod h1:3p9vT2HGsQu2K1YbXdKPJLVgG5VJdoTa1poYQBtP1AY=
162+
golang.org/x/mod v0.12.0 h1:rmsUpXtvNzj340zd98LZ4KntptpfRHwpFOHG188oHXc=
161163
golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
162164
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
163165
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
@@ -183,7 +185,7 @@ golang.org/x/sys v0.0.0-20211019181941-9d821ace8654/go.mod h1:oPkhp1MJrh7nUepCBc
183185
golang.org/x/sys v0.0.0-20211103235746-7861aae1554b/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
184186
golang.org/x/sys v0.0.0-20211216021012-1d35b9e2eb4e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
185187
golang.org/x/sys v0.0.0-20220513210249-45d2b4557a2a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
186-
golang.org/x/sys v0.5.0 h1:MUK/U/4lj1t1oPg0HfuXDN/Z1wv31ZJ/YcPiGccS4DU=
188+
golang.org/x/sys v0.12.0 h1:CM0HF96J0hcLAwsHPJZjfdNzs0gftsLfgKt57wWHJ0o=
187189
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
188190
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
189191
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
@@ -200,13 +202,12 @@ golang.org/x/tools v0.0.0-20190621195816-6e04913cbbac/go.mod h1:/rFqwRUd4F7ZHNgw
200202
golang.org/x/tools v0.0.0-20191029041327-9cc4af7d6b2c/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
201203
golang.org/x/tools v0.0.0-20191029190741-b9c20aec41a5/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
202204
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
203-
golang.org/x/tools v0.1.10 h1:QjFRCZxdOhBJ/UNgnBZLbNV13DlbnK0quyivTnXJM20=
204205
golang.org/x/tools v0.1.10/go.mod h1:Uh6Zz+xoGYZom868N8YTex3t7RhtHDBrE8Gzo9bV56E=
206+
golang.org/x/tools v0.13.0 h1:Iey4qkscZuv0VvIt8E0neZjtPVQFSc870HQ448QgEmQ=
205207
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
206208
golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
207209
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
208210
golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
209-
golang.org/x/xerrors v0.0.0-20220411194840-2f41105eb62f h1:GGU+dLjvlC3qDwqYgL6UgRmHXhOOgns0bZu2Ty5mm6U=
210211
golang.org/x/xerrors v0.0.0-20220411194840-2f41105eb62f/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
211212
google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw=
212213
google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc=

pkg/repository/repository.go

Lines changed: 115 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,8 @@ package repository
33
import (
44
"fmt"
55

6+
"golang.org/x/exp/slices"
7+
68
"github.com/harness/ff-golang-server-sdk/log"
79
"github.com/harness/ff-golang-server-sdk/rest"
810
"github.com/harness/ff-golang-server-sdk/storage"
@@ -20,7 +22,9 @@ type Repository interface {
2022
SetSegments(initialLoad bool, envID string, segment ...rest.Segment)
2123

2224
DeleteFlag(identifier string)
25+
DeleteFlags(envID string, identifier string)
2326
DeleteSegment(identifier string)
27+
DeleteSegments(envID string, identifier string)
2428

2529
Close()
2630
}
@@ -29,10 +33,12 @@ type Repository interface {
2933
type Callback interface {
3034
OnFlagStored(identifier string)
3135
OnFlagsStored(envID string)
36+
OnFlagsDeleted(envID string, identifier string)
3237
OnFlagDeleted(identifier string)
3338
OnSegmentStored(identifier string)
3439
OnSegmentsStored(envID string)
3540
OnSegmentDeleted(identifier string)
41+
OnSegmentsDeleted(envID string, identifier string)
3642
}
3743

3844
// FFRepository holds cache and optionally offline data
@@ -77,6 +83,16 @@ func (r FFRepository) getFlags(envID string) ([]rest.FeatureConfig, error) {
7783
return []rest.FeatureConfig{}, fmt.Errorf("%w with environment: %s", ErrFeatureConfigNotFound, envID)
7884
}
7985

86+
func (r FFRepository) getSegments(envID string) ([]rest.Segment, error) {
87+
segmentsKey := formatSegmentsKey(envID)
88+
flags, ok := r.cache.Get(segmentsKey)
89+
if ok {
90+
return flags.([]rest.Segment), nil
91+
}
92+
93+
return []rest.Segment{}, fmt.Errorf("%w with environment: %s", ErrFeatureConfigNotFound, envID)
94+
}
95+
8096
func (r FFRepository) getFlagAndCache(identifier string, cacheable bool) (rest.FeatureConfig, error) {
8197
flagKey := formatFlagKey(identifier)
8298
flag, ok := r.cache.Get(flagKey)
@@ -201,7 +217,7 @@ func (r FFRepository) SetSegment(segment rest.Segment, initialLoad bool) {
201217
func (r FFRepository) SetSegments(initialLoad bool, envID string, segments ...rest.Segment) {
202218
if !initialLoad {
203219
// If segments aren't outdated then we can exit as we don't need to refresh the cache
204-
if !r.areSegmentsOutdated(segments...) {
220+
if !r.areSegmentsOutdated(envID, segments...) {
205221
return
206222
}
207223
}
@@ -238,6 +254,42 @@ func (r FFRepository) DeleteFlag(identifier string) {
238254
}
239255
}
240256

257+
// DeleteFlags removes a flag from the flags key.
258+
//
259+
// We can't just delete the key here the way we can for a single flag because then we'd be removing flags that
260+
// haven't been deleted. So we have to first fetch value, then remove the specific flag that has been deleted
261+
// and update the key in the cache/storage
262+
func (r FFRepository) DeleteFlags(envID string, identifier string) {
263+
flagsKey := formatFlagsKey(envID)
264+
if r.storage != nil {
265+
// remove from storage
266+
if err := r.storage.Remove(flagsKey); err != nil {
267+
log.Errorf("error while removing flags %s from repository", envID)
268+
}
269+
}
270+
271+
value, ok := r.cache.Get(flagsKey)
272+
if !ok {
273+
log.Errorf("error fetching flags from cache for env=%s", envID)
274+
return
275+
}
276+
277+
featureConfigs, ok := value.([]rest.FeatureConfig)
278+
if !ok {
279+
log.Errorf("failed to delete flags, expected type to be []rest.FeatureConfig but got %T", featureConfigs)
280+
return
281+
}
282+
283+
updatedFeatureConfigs := slices.DeleteFunc(featureConfigs, func(element rest.FeatureConfig) bool {
284+
return element.Feature == identifier
285+
})
286+
r.cache.Set(flagsKey, updatedFeatureConfigs)
287+
288+
if r.callback != nil {
289+
r.callback.OnFlagsDeleted(envID, identifier)
290+
}
291+
}
292+
241293
// DeleteSegment removes a segment from the repository
242294
func (r FFRepository) DeleteSegment(identifier string) {
243295
segmentKey := formatSegmentKey(identifier)
@@ -254,6 +306,42 @@ func (r FFRepository) DeleteSegment(identifier string) {
254306
}
255307
}
256308

309+
// DeleteSegments removes a Segment from the segments key.
310+
//
311+
// We can't just delete the key here the way we can for a single flag because then we'd be removing segments that
312+
// haven't been deleted. So we have to first fetch value, then remove the specific segment that has been deleted
313+
// and update the key in the cache/storage
314+
func (r FFRepository) DeleteSegments(envID string, identifier string) {
315+
segmentsKey := formatSegmentsKey(envID)
316+
if r.storage != nil {
317+
// remove from storage
318+
if err := r.storage.Remove(segmentsKey); err != nil {
319+
log.Errorf("error while removing segments %s from repository", envID)
320+
}
321+
}
322+
323+
value, ok := r.cache.Get(segmentsKey)
324+
if !ok {
325+
log.Errorf("error fetching segments from cache for env=%s", envID)
326+
return
327+
}
328+
329+
segments, ok := value.([]rest.Segment)
330+
if !ok {
331+
log.Errorf("failed to delete flags, expected type to be []rest.Segment but got %T", segments)
332+
return
333+
}
334+
335+
updatedSegments := slices.DeleteFunc(segments, func(element rest.Segment) bool {
336+
return element.Identifier == identifier
337+
})
338+
r.cache.Set(segmentsKey, updatedSegments)
339+
340+
if r.callback != nil {
341+
r.callback.OnSegmentsDeleted(envID, identifier)
342+
}
343+
}
344+
257345
func (r FFRepository) isFlagOutdated(featureConfig rest.FeatureConfig) bool {
258346
oldFlag, err := r.getFlagAndCache(featureConfig.Feature, false)
259347
if err != nil || oldFlag.Version == nil {
@@ -319,7 +407,32 @@ func (r FFRepository) isSegmentOutdated(segment rest.Segment) bool {
319407
return *oldSegment.Version < *segment.Version
320408
}
321409

322-
func (r FFRepository) areSegmentsOutdated(segments ...rest.Segment) bool {
410+
func (r FFRepository) areSegmentsOutdated(envID string, segments ...rest.Segment) bool {
411+
oldSegments, err := r.getSegments(envID)
412+
if err != nil {
413+
// If we get an error return true to force a cache refresh
414+
return true
415+
}
416+
417+
oldSegmentsMap := map[string]rest.Segment{}
418+
for _, v := range oldSegments {
419+
oldSegmentsMap[v.Identifier] = v
420+
}
421+
422+
for _, seg := range segments {
423+
os, ok := oldSegmentsMap[seg.Identifier]
424+
if !ok {
425+
// If a new flag isn't in the oldFlagMap then the list of old flags are outdated and we'll
426+
// want to refresh the cache
427+
return true
428+
}
429+
430+
if *os.Version < *seg.Version {
431+
return true
432+
}
433+
}
434+
return false
435+
323436
for _, segment := range segments {
324437
if r.isSegmentOutdated(segment) {
325438
return true

pkg/repository/repository_test.go

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -161,6 +161,10 @@ func (m *mockCache) Get(key interface{}) (value interface{}, ok bool) {
161161
return m.features, true
162162
}
163163

164+
if s == "target-segments/123" {
165+
return m.segments, true
166+
}
167+
164168
if strings.Contains(s, "target-segment") {
165169
seg := strings.TrimPrefix(s, "target-segment/")
166170

@@ -275,6 +279,10 @@ func (m *mockCallback) OnSegmentsStored(envID string) {
275279

276280
func (m *mockCallback) OnSegmentDeleted(identifier string) {}
277281

282+
func (m *mockCallback) OnSegmentsDeleted(envID string, identifier string) {}
283+
284+
func (m *mockCallback) OnFlagsDeleted(envID string, identifier string) {}
285+
278286
func TestFFRepository_SetFlags(t *testing.T) {
279287
type args struct {
280288
initialLoad bool

stream/sse.go

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -130,6 +130,7 @@ func (c *SSEClient) handleEvent(event Event) {
130130
switch cfMsg.Event {
131131
case dto.SseDeleteEvent:
132132
c.repository.DeleteFlag(cfMsg.Identifier)
133+
c.repository.DeleteFlags(event.Environment, cfMsg.Identifier)
133134
case dto.SsePatchEvent, dto.SseCreateEvent:
134135
fallthrough
135136
default:
@@ -173,6 +174,7 @@ func (c *SSEClient) handleEvent(event Event) {
173174
switch cfMsg.Event {
174175
case dto.SseDeleteEvent:
175176
c.repository.DeleteSegment(cfMsg.Identifier)
177+
c.repository.DeleteSegments(event.Environment, cfMsg.Identifier)
176178
case dto.SsePatchEvent, dto.SseCreateEvent:
177179
fallthrough
178180
default:

0 commit comments

Comments
 (0)