Skip to content
Merged
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
43 changes: 35 additions & 8 deletions internal/dev_server/db/sqlite.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import (
"encoding/json"
"io"
"os"
"strings"

_ "github.com/mattn/go-sqlite3"
"github.com/pkg/errors"
Expand Down Expand Up @@ -47,12 +48,15 @@ func (s *Sqlite) GetDevProject(ctx context.Context, key string) (*model.Project,
var flagStateData string

row := s.database.QueryRowContext(ctx, `
SELECT key, source_environment_key, context, last_sync_time, flag_state
FROM projects
SELECT key, source_environment_key, context, last_sync_time, flag_state, payload_version
FROM projects
WHERE key = ?
`, key)

if err := row.Scan(&project.Key, &project.SourceEnvironmentKey, &contextData, &project.LastSyncTime, &flagStateData); err != nil {
if err := row.Scan(
&project.Key, &project.SourceEnvironmentKey, &contextData,
&project.LastSyncTime, &flagStateData, &project.PayloadVersion,
); err != nil {
if errors.Is(err, sql.ErrNoRows) {
return nil, model.NewErrNotFound("project", key)
}
Expand Down Expand Up @@ -200,14 +204,15 @@ SELECT 1 FROM projects WHERE key = ?
return
}
_, err = tx.Exec(`
INSERT INTO projects (key, source_environment_key, context, last_sync_time, flag_state)
VALUES (?, ?, ?, ?, ?)
INSERT INTO projects (key, source_environment_key, context, last_sync_time, flag_state, payload_version)
VALUES (?, ?, ?, ?, ?, ?)
`,
project.Key,
project.SourceEnvironmentKey,
project.Context.JSONString(),
project.LastSyncTime,
string(flagsStateJson),
project.PayloadVersion,
)
if err != nil {
return
Expand Down Expand Up @@ -341,6 +346,20 @@ func (s *Sqlite) UpsertOverride(ctx context.Context, override model.Override) (m
return override, nil
}

func (s *Sqlite) IncrementProjectPayloadVersion(ctx context.Context, projectKey string) (int, error) {
row := s.database.QueryRowContext(ctx, `
UPDATE projects
SET payload_version = payload_version + 1
WHERE key = ?
RETURNING payload_version
`, projectKey)
var version int
if err := row.Scan(&version); err != nil {
return 0, errors.Wrap(err, "unable to increment payload version")
}
return version, nil
}

func (s *Sqlite) DeactivateOverride(ctx context.Context, projectKey, flagKey string) (int, error) {
row := s.database.QueryRowContext(ctx, `
UPDATE overrides
Expand Down Expand Up @@ -373,12 +392,12 @@ func (s *Sqlite) RestoreBackup(ctx context.Context, stream io.Reader) (string, e
}
err = os.Rename(filepath, s.dbPath)
if err != nil {
//panic because this would really leave the app in an invalid state
// panic because this would really leave the app in an invalid state
panic(err)
}
s.database, err = sql.Open("sqlite3", s.dbPath)
if err != nil {
//panic because this would really leave the app in an invalid state
// panic because this would really leave the app in an invalid state
panic(err)
}

Expand Down Expand Up @@ -445,12 +464,20 @@ func (s *Sqlite) runMigrations(ctx context.Context) error {
source_environment_key text NOT NULL,
context text NOT NULL,
last_sync_time timestamp NOT NULL,
flag_state TEXT NOT NULL
flag_state TEXT NOT NULL,
payload_version INTEGER NOT NULL DEFAULT 1
)`)
if err != nil {
return err
}

// Migration: add payload_version to existing databases that predate this column.
_, err = tx.Exec(`ALTER TABLE projects ADD COLUMN payload_version INTEGER NOT NULL DEFAULT 1`)
if err != nil && !strings.Contains(err.Error(), "duplicate column name") {
return err
}
err = nil

_, err = tx.Exec(`
CREATE TABLE IF NOT EXISTS overrides (
project_key text NOT NULL,
Expand Down
23 changes: 23 additions & 0 deletions internal/dev_server/db/sqlite_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@ func TestDBFunctions(t *testing.T) {
SourceEnvironmentKey: "env-1",
Context: ldContext,
LastSyncTime: now,
PayloadVersion: 1,
AllFlagsState: model.FlagsState{
"flag-1": model.FlagState{Value: ldvalue.Bool(true), Version: 2},
"flag-2": model.FlagState{Value: ldvalue.String("cool"), Version: 2},
Expand Down Expand Up @@ -71,6 +72,7 @@ func TestDBFunctions(t *testing.T) {
SourceEnvironmentKey: "env-2",
Context: ldContext,
LastSyncTime: now,
PayloadVersion: 1,
AllFlagsState: model.FlagsState{
"flag-1": model.FlagState{Value: ldvalue.Int(123), Version: 2},
"flag-2": model.FlagState{Value: ldvalue.Float64(99.99), Version: 2},
Expand All @@ -97,6 +99,7 @@ func TestDBFunctions(t *testing.T) {
SourceEnvironmentKey: "env-3",
Context: ldContext,
LastSyncTime: now,
PayloadVersion: 1,
AllFlagsState: model.FlagsState{
"flag-1": model.FlagState{Value: ldvalue.Int(123), Version: 2},
"flag-2": model.FlagState{Value: ldvalue.Float64(99.99), Version: 2},
Expand Down Expand Up @@ -169,6 +172,7 @@ func TestDBFunctions(t *testing.T) {
assert.Equal(t, expected.SourceEnvironmentKey, p.SourceEnvironmentKey)
assert.Equal(t, expected.Context, p.Context)
assert.True(t, expected.LastSyncTime.Equal(p.LastSyncTime))
assert.Equal(t, expected.PayloadVersion, p.PayloadVersion)
})

t.Run("GetAvailableVariations returns variations", func(t *testing.T) {
Expand Down Expand Up @@ -364,6 +368,25 @@ func TestDBFunctions(t *testing.T) {
assert.True(t, found)
})

t.Run("IncrementProjectPayloadVersion increments and returns new version", func(t *testing.T) {
proj, err := store.GetDevProject(ctx, projects[0].Key)
require.NoError(t, err)
initialVersion := proj.PayloadVersion

newVersion, err := store.IncrementProjectPayloadVersion(ctx, projects[0].Key)
require.NoError(t, err)
assert.Equal(t, initialVersion+1, newVersion)

proj, err = store.GetDevProject(ctx, projects[0].Key)
require.NoError(t, err)
assert.Equal(t, initialVersion+1, proj.PayloadVersion)

// Calling again should increment once more
newVersion2, err := store.IncrementProjectPayloadVersion(ctx, projects[0].Key)
require.NoError(t, err)
assert.Equal(t, initialVersion+2, newVersion2)
})

t.Run("UpdateProject deletes overrides for flags that are no longer in the project", func(t *testing.T) {
project := projects[2]

Expand Down
1 change: 1 addition & 0 deletions internal/dev_server/model/import_project.go
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,7 @@ func ImportProject(ctx context.Context, projectKey string, importData ImportData
Context: importData.Context,
AllFlagsState: importData.FlagsState,
AvailableVariations: []FlagVariation{},
PayloadVersion: 1,
}

// Convert available variations if present
Expand Down
15 changes: 15 additions & 0 deletions internal/dev_server/model/mocks/store.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

13 changes: 13 additions & 0 deletions internal/dev_server/model/override.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,8 @@ package model
import (
"context"

"github.com/pkg/errors"

"github.com/launchdarkly/go-sdk-common/v3/ldvalue"
)

Expand Down Expand Up @@ -58,6 +60,11 @@ func UpsertOverride(ctx context.Context, projectKey, flagKey string, value ldval
return Override{}, err
}

_, err = store.IncrementProjectPayloadVersion(ctx, projectKey)
if err != nil {
return Override{}, errors.Wrap(err, "unable to increment payload version")
}

GetObserversFromContext(ctx).Notify(OverrideEvent{
FlagKey: flagKey,
ProjectKey: projectKey,
Expand All @@ -76,6 +83,12 @@ func DeleteOverride(ctx context.Context, projectKey, flagKey string) error {
if err != nil {
return err
}

_, err = store.IncrementProjectPayloadVersion(ctx, projectKey)
if err != nil {
return errors.Wrap(err, "unable to increment payload version")
}

override := Override{
ProjectKey: projectKey,
FlagKey: flagKey,
Expand Down
4 changes: 4 additions & 0 deletions internal/dev_server/model/override_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -73,6 +73,7 @@ func TestUpsertOverride(t *testing.T) {
t.Run("override is applied, observers are notified", func(t *testing.T) {
store.EXPECT().GetDevProject(gomock.Any(), projKey).Return(project, nil)
store.EXPECT().UpsertOverride(gomock.Any(), override).Return(override, nil)
store.EXPECT().IncrementProjectPayloadVersion(gomock.Any(), projKey).Return(1, nil)
observer.
EXPECT().
Handle(model.OverrideEvent{
Expand Down Expand Up @@ -128,6 +129,7 @@ func TestDeleteOverride(t *testing.T) {
t.Run("override is applied, observers are notified", func(t *testing.T) {
store.EXPECT().GetDevProject(gomock.Any(), projKey).Return(project, nil)
store.EXPECT().DeactivateOverride(gomock.Any(), projKey, flagKey).Return(2, nil)
store.EXPECT().IncrementProjectPayloadVersion(gomock.Any(), projKey).Return(1, nil)
observer.
EXPECT().
Handle(model.OverrideEvent{
Expand Down Expand Up @@ -198,11 +200,13 @@ func TestDeleteOverrides(t *testing.T) {
// Expectations for first override
store.EXPECT().GetDevProject(gomock.Any(), projKey).Return(project, nil)
store.EXPECT().DeactivateOverride(gomock.Any(), projKey, flagKey).Return(2, nil)
store.EXPECT().IncrementProjectPayloadVersion(gomock.Any(), projKey).Return(1, nil)
observer.EXPECT().Handle(gomock.Any())

// Expectations for second override
store.EXPECT().GetDevProject(gomock.Any(), projKey).Return(project, nil)
store.EXPECT().DeactivateOverride(gomock.Any(), projKey, "flag2").Return(2, nil)
store.EXPECT().IncrementProjectPayloadVersion(gomock.Any(), projKey).Return(2, nil)
observer.EXPECT().Handle(gomock.Any())

err := model.DeleteOverrides(ctx, projKey)
Expand Down
8 changes: 8 additions & 0 deletions internal/dev_server/model/project.go
Original file line number Diff line number Diff line change
Expand Up @@ -18,13 +18,15 @@ type Project struct {
LastSyncTime time.Time
AllFlagsState FlagsState
AvailableVariations []FlagVariation
PayloadVersion int
}

// CreateProject creates a project and adds it to the database.
func CreateProject(ctx context.Context, projectKey, sourceEnvironmentKey string, ldCtx *ldcontext.Context) (Project, error) {
project := Project{
Key: projectKey,
SourceEnvironmentKey: sourceEnvironmentKey,
PayloadVersion: 1,
}

if ldCtx == nil {
Expand Down Expand Up @@ -87,6 +89,12 @@ func UpdateProject(ctx context.Context, projectKey string, context *ldcontext.Co
return Project{}, errors.New("Project not updated")
}

newPayloadVersion, err := store.IncrementProjectPayloadVersion(ctx, projectKey)
if err != nil {
return Project{}, errors.Wrap(err, "unable to increment payload version")
}
project.PayloadVersion = newPayloadVersion

allFlagsWithOverrides, err := project.GetFlagStateWithOverridesForProject(ctx)
if err != nil {
return Project{}, errors.Wrapf(err, "unable to get overrides for project, %s", projectKey)
Expand Down
5 changes: 4 additions & 1 deletion internal/dev_server/model/project_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -183,6 +183,7 @@ func TestUpdateProject(t *testing.T) {
sdk.EXPECT().GetAllFlagsState(gomock.Any(), gomock.Any(), "sdkKey").Return(allFlagsState, nil)
api.EXPECT().GetAllFlags(gomock.Any(), proj.Key).Return(allFlags, nil)
store.EXPECT().UpdateProject(gomock.Any(), gomock.Any()).Return(true, nil)
store.EXPECT().IncrementProjectPayloadVersion(gomock.Any(), proj.Key).Return(2, nil)
store.EXPECT().GetOverridesForProject(gomock.Any(), proj.Key).Return(model.Overrides{}, nil)
observer.
EXPECT().
Expand All @@ -193,7 +194,9 @@ func TestUpdateProject(t *testing.T) {

project, err := model.UpdateProject(ctx, proj.Key, nil, nil)
require.Nil(t, err)
assert.Equal(t, proj, project)
expectedProj := proj
expectedProj.PayloadVersion = 2
assert.Equal(t, expectedProj, project)
})
}

Expand Down
2 changes: 2 additions & 0 deletions internal/dev_server/model/store.go
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,8 @@ type Store interface {
UpsertOverride(ctx context.Context, override Override) (Override, error)
GetOverridesForProject(ctx context.Context, projectKey string) (Overrides, error)
GetAvailableVariationsForProject(ctx context.Context, projectKey string) (map[string][]Variation, error)
// IncrementProjectPayloadVersion atomically increments the payload version for the project and returns the new version.
IncrementProjectPayloadVersion(ctx context.Context, projectKey string) (int, error)

CreateBackup(ctx context.Context) (io.ReadCloser, int64, error)
RestoreBackup(ctx context.Context, stream io.Reader) (string, error)
Expand Down
3 changes: 2 additions & 1 deletion internal/dev_server/model/sync_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -153,8 +153,9 @@ func TestInitialSync(t *testing.T) {
sdk.EXPECT().GetAllFlagsState(gomock.Any(), gomock.Any(), sdkKey).Return(allFlagsState, nil)
api.EXPECT().GetAllFlags(gomock.Any(), projKey).Return(allFlags, nil)
store.EXPECT().InsertProject(gomock.Any(), gomock.Any()).Return(nil)
store.EXPECT().UpsertOverride(gomock.Any(), override).Return(override, nil)
store.EXPECT().GetDevProject(gomock.Any(), projKey).Return(&proj, nil)
store.EXPECT().UpsertOverride(gomock.Any(), override).Return(override, nil)
store.EXPECT().IncrementProjectPayloadVersion(gomock.Any(), projKey).Return(1, nil)

input := model.InitialProjectSettings{
Enabled: true,
Expand Down
Loading
Loading