Skip to content

Commit 6548beb

Browse files
author
Mike Zorn
authored
fix: Trigger change events when overrides are deleted (#455)
This ensures that we're sending flag change events to SDKs whenever overrides are deleted.
1 parent 56ee0a6 commit 6548beb

10 files changed

Lines changed: 134 additions & 41 deletions

File tree

internal/dev_server/api/delete_flag_override.go

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -9,8 +9,7 @@ import (
99
)
1010

1111
func (s server) DeleteFlagOverride(ctx context.Context, request DeleteFlagOverrideRequestObject) (DeleteFlagOverrideResponseObject, error) {
12-
store := model.StoreFromContext(ctx)
13-
err := store.DeactivateOverride(ctx, request.ProjectKey, request.FlagKey)
12+
err := model.DeleteOverride(ctx, request.ProjectKey, request.FlagKey)
1413
if err != nil {
1514
if errors.Is(err, model.ErrNotFound) {
1615
return DeleteFlagOverride404Response{}, nil

internal/dev_server/db/sqlite.go

Lines changed: 13 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -324,26 +324,25 @@ func (s Sqlite) UpsertOverride(ctx context.Context, override model.Override) (mo
324324
return override, nil
325325
}
326326

327-
func (s Sqlite) DeactivateOverride(ctx context.Context, projectKey, flagKey string) error {
328-
result, err := s.database.Exec(`
329-
UPDATE overrides set active = false, version = version+1 where project_key = ? and flag_key = ? and active = true
327+
func (s Sqlite) DeactivateOverride(ctx context.Context, projectKey, flagKey string) (int, error) {
328+
row := s.database.QueryRowContext(ctx, `
329+
UPDATE overrides
330+
set active = false, version = version+1
331+
where project_key = ? and flag_key = ? and active = true
332+
returning version
330333
`,
331334
projectKey,
332335
flagKey,
333336
)
334-
if err != nil {
335-
return err
336-
}
337-
338-
rowsAffected, err := result.RowsAffected()
339-
if err != nil {
340-
return err
341-
}
342-
if rowsAffected == 0 {
343-
return model.ErrNotFound
337+
var version int
338+
if err := row.Scan(&version); err != nil {
339+
if errors.Is(err, sql.ErrNoRows) {
340+
return 0, errors.Wrapf(model.ErrNotFound, "no override found for flag with key, '%s', in project with key, '%s'", projectKey, flagKey)
341+
}
342+
return 0, err
344343
}
345344

346-
return nil
345+
return version, nil
347346
}
348347

349348
func NewSqlite(ctx context.Context, dbPath string) (Sqlite, error) {

internal/dev_server/db/sqlite_test.go

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -302,13 +302,13 @@ func TestDBFunctions(t *testing.T) {
302302
})
303303

304304
t.Run("DeactivateOverride returns error when override not found", func(t *testing.T) {
305-
err := store.DeactivateOverride(ctx, projects[0].Key, "nope")
305+
_, err := store.DeactivateOverride(ctx, projects[0].Key, "nope")
306306
assert.ErrorIs(t, err, model.ErrNotFound)
307307
})
308308

309-
t.Run("DeactivateOverride sets the override inactive", func(t *testing.T) {
309+
t.Run("DeactivateOverride sets the override inactive and returns the current version", func(t *testing.T) {
310310
toDelete := overrides[flagKeys[0]]
311-
err := store.DeactivateOverride(ctx, toDelete.ProjectKey, toDelete.FlagKey)
311+
version, err := store.DeactivateOverride(ctx, toDelete.ProjectKey, toDelete.FlagKey)
312312
assert.NoError(t, err)
313313

314314
result, err := store.GetOverridesForProject(ctx, toDelete.ProjectKey)
@@ -323,6 +323,7 @@ func TestDBFunctions(t *testing.T) {
323323

324324
found = true
325325
assert.False(t, r.Active)
326+
assert.Equal(t, version, r.Version)
326327
}
327328

328329
assert.True(t, found)

internal/dev_server/model/events.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
package model
22

33
// Event for individual flag overrides
4-
type UpsertOverrideEvent struct {
4+
type OverrideEvent struct {
55
FlagKey string
66
ProjectKey string
77
FlagState FlagState

internal/dev_server/model/mocks/store.go

Lines changed: 4 additions & 3 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

internal/dev_server/model/override.go

Lines changed: 43 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -14,14 +14,15 @@ type Override struct {
1414
Version int
1515
}
1616

17-
func UpsertOverride(ctx context.Context, projectKey, flagKey string, value ldvalue.Value) (Override, error) {
18-
// TODO: validate if the flag type matches
19-
17+
// getFlagStateForFlagAndProject fetches state from the store so that it can later be used to apply an override and
18+
// construct an update. You want to call this before you write the override so that written overrides don't
19+
// less often don't cause updates.
20+
func getFlagStateForFlagAndProject(ctx context.Context, projectKey, flagKey string) (FlagState, error) {
2021
store := StoreFromContext(ctx)
2122

2223
project, err := store.GetDevProject(ctx, projectKey)
23-
if err != nil || project == nil {
24-
return Override{}, NewError("project does not exist within dev server")
24+
if err != nil {
25+
return FlagState{}, err
2526
}
2627

2728
var flagExists bool
@@ -32,7 +33,15 @@ func UpsertOverride(ctx context.Context, projectKey, flagKey string, value ldval
3233
}
3334
}
3435
if !flagExists {
35-
return Override{}, NewError("flag does not exist within dev project")
36+
return FlagState{}, ErrNotFound
37+
}
38+
return project.AllFlagsState[flagKey], nil
39+
}
40+
41+
func UpsertOverride(ctx context.Context, projectKey, flagKey string, value ldvalue.Value) (Override, error) {
42+
flagState, err := getFlagStateForFlagAndProject(ctx, projectKey, flagKey)
43+
if err != nil {
44+
return Override{}, err
3645
}
3746

3847
override := Override{
@@ -43,21 +52,45 @@ func UpsertOverride(ctx context.Context, projectKey, flagKey string, value ldval
4352
Version: 1,
4453
}
4554

55+
store := StoreFromContext(ctx)
4656
override, err = store.UpsertOverride(ctx, override)
4757
if err != nil {
4858
return Override{}, err
4959
}
5060

51-
flagState := override.Apply(project.AllFlagsState[flagKey])
52-
GetObserversFromContext(ctx).Notify(UpsertOverrideEvent{
61+
GetObserversFromContext(ctx).Notify(OverrideEvent{
5362
FlagKey: flagKey,
5463
ProjectKey: projectKey,
55-
FlagState: flagState,
64+
FlagState: override.Apply(flagState),
5665
})
57-
5866
return override, nil
5967
}
6068

69+
func DeleteOverride(ctx context.Context, projectKey, flagKey string) error {
70+
flagState, err := getFlagStateForFlagAndProject(ctx, projectKey, flagKey)
71+
if err != nil {
72+
return err
73+
}
74+
store := StoreFromContext(ctx)
75+
version, err := store.DeactivateOverride(ctx, projectKey, flagKey)
76+
if err != nil {
77+
return err
78+
}
79+
override := Override{
80+
ProjectKey: projectKey,
81+
FlagKey: flagKey,
82+
Value: ldvalue.Null(), // since inactive, will get use the one from flagState
83+
Active: false,
84+
Version: version,
85+
}
86+
GetObserversFromContext(ctx).Notify(OverrideEvent{
87+
FlagKey: flagKey,
88+
ProjectKey: projectKey,
89+
FlagState: override.Apply(flagState),
90+
})
91+
return err
92+
}
93+
6194
func (o Override) Apply(state FlagState) FlagState {
6295
flagVersion := state.Version + o.Version
6396
flagValue := state.Value

internal/dev_server/model/override_test.go

Lines changed: 62 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -13,10 +13,12 @@ import (
1313
)
1414

1515
func TestUpsertOverride(t *testing.T) {
16+
t.Parallel()
1617
ctx := context.Background()
1718
mockController := gomock.NewController(t)
19+
defer mockController.Finish()
1820
store := mocks.NewMockStore(mockController)
19-
projKey := "proj"
21+
projKey := t.Name()
2022
flagKey := "flg"
2123
ldValue := ldvalue.Bool(true)
2224
override := model.Override{
@@ -45,7 +47,6 @@ func TestUpsertOverride(t *testing.T) {
4547

4648
_, err := model.UpsertOverride(ctx, projKey, flagKey, ldValue)
4749
assert.Error(t, err)
48-
assert.Contains(t, err.Error(), "project does not exist within dev server")
4950
})
5051

5152
t.Run("Returns error if flag does not exist in project", func(t *testing.T) {
@@ -57,7 +58,7 @@ func TestUpsertOverride(t *testing.T) {
5758

5859
_, err := model.UpsertOverride(ctx, projKey, flagKey, ldValue)
5960
assert.Error(t, err)
60-
assert.Contains(t, err.Error(), "flag does not exist within dev project")
61+
assert.ErrorIs(t, model.ErrNotFound, err)
6162
})
6263

6364
t.Run("store fails to upsert, returns error", func(t *testing.T) {
@@ -74,7 +75,7 @@ func TestUpsertOverride(t *testing.T) {
7475
store.EXPECT().UpsertOverride(gomock.Any(), override).Return(override, nil)
7576
observer.
7677
EXPECT().
77-
Handle(model.UpsertOverrideEvent{
78+
Handle(model.OverrideEvent{
7879
FlagKey: flagKey,
7980
ProjectKey: projKey,
8081
FlagState: model.FlagState{Value: ldvalue.Bool(true), Version: 2},
@@ -86,6 +87,63 @@ func TestUpsertOverride(t *testing.T) {
8687
})
8788
}
8889

90+
func TestDeleteOverride(t *testing.T) {
91+
t.Parallel()
92+
ctx := context.Background()
93+
mockController := gomock.NewController(t)
94+
defer mockController.Finish()
95+
store := mocks.NewMockStore(mockController)
96+
projKey := t.Name()
97+
flagKey := "flg"
98+
ldValue := ldvalue.Bool(true)
99+
100+
project := &model.Project{
101+
Key: projKey,
102+
AllFlagsState: model.FlagsState{flagKey: model.FlagState{Value: ldvalue.Bool(false), Version: 1}},
103+
}
104+
105+
ctx = model.ContextWithStore(ctx, store)
106+
107+
observers := model.NewObservers()
108+
observer := mocks.NewMockObserver(mockController)
109+
110+
observers.RegisterObserver(observer)
111+
ctx = model.SetObserversOnContext(ctx, observers)
112+
113+
t.Run("store unable to get project, returns error", func(t *testing.T) {
114+
store.EXPECT().GetDevProject(gomock.Any(), projKey).Return(nil, errors.New("test 2"))
115+
116+
_, err := model.UpsertOverride(ctx, projKey, flagKey, ldValue)
117+
assert.Error(t, err)
118+
})
119+
120+
t.Run("Returns error if store errors on delete", func(t *testing.T) {
121+
store.EXPECT().GetDevProject(gomock.Any(), projKey).Return(project, nil)
122+
store.EXPECT().DeactivateOverride(gomock.Any(), projKey, flagKey).Return(0, errors.New("store error on deactive override"))
123+
124+
err := model.DeleteOverride(ctx, projKey, flagKey)
125+
assert.Error(t, err)
126+
})
127+
128+
t.Run("override is applied, observers are notified", func(t *testing.T) {
129+
store.EXPECT().GetDevProject(gomock.Any(), projKey).Return(project, nil)
130+
store.EXPECT().DeactivateOverride(gomock.Any(), projKey, flagKey).Return(2, nil)
131+
observer.
132+
EXPECT().
133+
Handle(model.OverrideEvent{
134+
FlagKey: flagKey,
135+
ProjectKey: projKey,
136+
FlagState: model.FlagState{
137+
Value: ldvalue.Bool(false),
138+
Version: 3, // override version 2 + flag version 1
139+
},
140+
})
141+
142+
err := model.DeleteOverride(ctx, projKey, flagKey)
143+
assert.Nil(t, err)
144+
})
145+
}
146+
89147
func TestOverrideApply(t *testing.T) {
90148
projKey := "proj"
91149
flagKey := "flg"

internal/dev_server/model/store.go

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,10 +2,10 @@ package model
22

33
import (
44
"context"
5+
"errors"
56
"net/http"
67

78
"github.com/gorilla/mux"
8-
"github.com/pkg/errors"
99
)
1010

1111
type ctxKey string
@@ -15,7 +15,9 @@ const ctxKeyStore = ctxKey("model.Store")
1515
//go:generate go run go.uber.org/mock/mockgen -destination mocks/store.go -package mocks . Store
1616

1717
type Store interface {
18-
DeactivateOverride(ctx context.Context, projectKey, flagKey string) error
18+
// DeactivateOverride deactivates the override for the flag, returning the updated version of the override.
19+
// ErrNotFound is returned if there isn't an override for the flag.
20+
DeactivateOverride(ctx context.Context, projectKey, flagKey string) (int, error)
1921
GetDevProjectKeys(ctx context.Context) ([]string, error)
2022
// GetDevProject fetches the project based on the projectKey. If it doesn't exist, ErrNotFound is returned
2123
GetDevProject(ctx context.Context, projectKey string) (*Project, error)

internal/dev_server/sdk/stream_client_flags.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -53,7 +53,7 @@ type clientFlagsObserver struct {
5353
func (c clientFlagsObserver) Handle(event interface{}) {
5454
log.Printf("clientFlagsObserver: handling flag state event: %v", event)
5555
switch event := event.(type) {
56-
case model.UpsertOverrideEvent:
56+
case model.OverrideEvent:
5757
err := SendMessage(c.updateChan, TYPE_PATCH, clientFlag{
5858
Key: event.FlagKey,
5959
Version: event.FlagState.Version,

internal/dev_server/sdk/stream_server_flags.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -54,7 +54,7 @@ type serverFlagsObserver struct {
5454
func (c serverFlagsObserver) Handle(event interface{}) {
5555
log.Printf("serverFlagsObserver: handling flag state event: %v", event)
5656
switch event := event.(type) {
57-
case model.UpsertOverrideEvent:
57+
case model.OverrideEvent:
5858
if event.ProjectKey != c.projectKey {
5959
return
6060
}

0 commit comments

Comments
 (0)