Skip to content

Commit

Permalink
chore: Introduction of redis-backed daemon-mode tests
Browse files Browse the repository at this point in the history
  • Loading branch information
keelerm84 committed Oct 4, 2024
1 parent 859f84d commit ea171f5
Show file tree
Hide file tree
Showing 9 changed files with 406 additions and 11 deletions.
3 changes: 3 additions & 0 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -10,13 +10,16 @@ require (
github.com/launchdarkly/go-sdk-common/v3 v3.1.0
github.com/launchdarkly/go-server-sdk-evaluation/v3 v3.0.0
github.com/launchdarkly/go-test-helpers/v2 v2.3.2
github.com/redis/go-redis/v9 v9.6.1
github.com/stretchr/testify v1.7.0
golang.org/x/exp v0.0.0-20220823124025-807a23277127
gopkg.in/yaml.v3 v3.0.0
)

require (
github.com/cespare/xxhash/v2 v2.2.0 // indirect
github.com/davecgh/go-spew v1.1.1 // indirect
github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f // indirect
github.com/josharian/intern v1.0.0 // indirect
github.com/launchdarkly/go-semver v1.0.2 // indirect
github.com/mailru/easyjson v0.7.6 // indirect
Expand Down
8 changes: 8 additions & 0 deletions go.sum
Original file line number Diff line number Diff line change
@@ -1,6 +1,12 @@
github.com/bsm/ginkgo/v2 v2.12.0 h1:Ny8MWAHyOepLGlLKYmXG4IEkioBysk6GpaRTLC8zwWs=
github.com/bsm/gomega v1.27.10 h1:yeMWxP2pV2fG3FgAODIY8EiRE3dy0aeFYt4l7wh6yKA=
github.com/cespare/xxhash/v2 v2.2.0 h1:DC2CZ1Ep5Y4k3ZQ899DldepgrayRUGE6BBZ/cd9Cj44=
github.com/cespare/xxhash/v2 v2.2.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f h1:lO4WD4F/rVNCu3HqELle0jiPLLBs70cWOduZpkS1E78=
github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f/go.mod h1:cuUVRXasLTGF7a8hSLbxyZXjz+1KgoB3wDUb6vlszIc=
github.com/fatih/color v1.13.0 h1:8LOYc1KYPPmyKMuN8QV2DNRWNbLo6LZ0iLs8+mlH53w=
github.com/fatih/color v1.13.0/go.mod h1:kLAiJbzzSOZDVNGyDpeOxJ47H46qBXwg5ILebYFFOfk=
github.com/gorilla/mux v1.8.0 h1:i40aqfkR1h2SlN9hojwV5ZA91wcXFOvkdNIeFDP5koI=
Expand Down Expand Up @@ -32,6 +38,8 @@ github.com/mattn/go-isatty v0.0.14 h1:yVuAays6BHfxijgZPzw+3Zlu5yQgKGP2/hcQbHb7S9
github.com/mattn/go-isatty v0.0.14/go.mod h1:7GGIvUiUoEMVVmxf/4nioHXj79iQHKdU27kJ6hsGG94=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/redis/go-redis/v9 v9.6.1 h1:HHDteefn6ZkTtY5fGUE8tj8uy85AHk6zP7CpzIAM0y4=
github.com/redis/go-redis/v9 v9.6.1/go.mod h1:0C0c6ycQsdpVNQpxb1njEQIqkx5UcsM8FJCQLgE9+RA=
github.com/rogpeppe/go-internal v1.9.0 h1:73kH8U+JUqXU8lRuOHeVHaa/SZPifC7BkcraZVejAe8=
github.com/rogpeppe/go-internal v1.9.0/go.mod h1:WtVeX8xhTBvf0smdhujwtBcq4Qrzq/fJaraNFVN+nFs=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
Expand Down
113 changes: 113 additions & 0 deletions sdktests/server_side_data_system_base.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,113 @@
package sdktests

import (
"time"

"github.com/launchdarkly/go-sdk-common/v3/ldcontext"
"github.com/launchdarkly/go-sdk-common/v3/ldvalue"
"github.com/launchdarkly/go-server-sdk-evaluation/v3/ldbuilders"
"github.com/launchdarkly/sdk-test-harness/v2/framework/ldtest"
"github.com/launchdarkly/sdk-test-harness/v2/servicedef"
"github.com/redis/go-redis/v9"
"github.com/stretchr/testify/require"
)

func doServerSideDataSystemTests(t *ldtest.T) {
rdb := redis.NewClient(&redis.Options{
Addr: "localhost:6379",
Password: "", // no password set
DB: 0, // use default DB
})

newServerSideDataSystemTests(t, RedisPersistenceStore{redis: rdb}).Run(t)
}

type ServerSideDataSystemTests struct {
persistence PersistenceStore
redis *redis.Client

Check failure on line 27 in sdktests/server_side_data_system_base.go

View workflow job for this annotation

GitHub Actions / build-and-test

field `redis` is unused (unused)
initialFlags map[string]string
}

func newServerSideDataSystemTests(t *ldtest.T, persistence PersistenceStore) *ServerSideDataSystemTests {
flagKeyBytes, err :=
ldbuilders.NewFlagBuilder("flag-key").Version(1).
On(true).Variations(ldvalue.String("off"), ldvalue.String("match"), ldvalue.String("fallthrough")).
OffVariation(0).
FallthroughVariation(2).
Build().MarshalJSON()
require.NoError(t, err)

initialFlags := map[string]string{"flag-key": string(flagKeyBytes)}

uncachedFlagKeyBytes, err :=
ldbuilders.NewFlagBuilder("uncached-flag-key").Version(1).
On(true).Variations(ldvalue.String("off"), ldvalue.String("match"), ldvalue.String("fallthrough")).
OffVariation(0).
FallthroughVariation(2).
Build().MarshalJSON()
require.NoError(t, err)

initialFlags["uncached-flag-key"] = string(uncachedFlagKeyBytes)

return &ServerSideDataSystemTests{
persistence: persistence,
initialFlags: initialFlags,
}
}

func (s *ServerSideDataSystemTests) Run(t *ldtest.T) {
t.Run("uses default prefix", s.usesDefaultPrefix)
t.Run("uses custom prefix", s.usesCustomPrefix)

t.Run("read-only", s.doReadOnlyTests)
}

func (s *ServerSideDataSystemTests) usesDefaultPrefix(t *ldtest.T) {
require.NoError(t, s.persistence.Reset())
require.NoError(t, s.persistence.WriteData("launchdarkly:features", s.initialFlags))

dataSystem := NewDataSystem()
dataSystem.AddPersistence(servicedef.SDKConfigDataSystemPersistence{
Store: servicedef.SDKConfigDataSystemPersistenceStore{
Type: servicedef.Redis,
DSN: s.persistence.DSN(),
},
Cache: servicedef.SDKConfigDataSystemPersistenceCache{
Mode: servicedef.Off,
},
})

client := NewSDKClient(t, dataSystem)
pollUntilFlagValueUpdated(t, client, "flag-key", ldcontext.New("user-key"), ldvalue.String("default"), ldvalue.String("fallthrough"), ldvalue.String("default"))
}

func (s *ServerSideDataSystemTests) usesCustomPrefix(t *ldtest.T) {
require.NoError(t, s.persistence.Reset())
customPrefix := "custom-prefix"

dataSystem := NewDataSystem()
dataSystem.AddPersistence(servicedef.SDKConfigDataSystemPersistence{
Store: servicedef.SDKConfigDataSystemPersistenceStore{
Type: servicedef.Redis,
Prefix: customPrefix,
DSN: s.persistence.DSN(),
},
Cache: servicedef.SDKConfigDataSystemPersistenceCache{
Mode: servicedef.Off,
},
})

client := NewSDKClient(t, dataSystem)

require.Never(
t,
checkForUpdatedValue(t, client, "flag-key", ldcontext.New("user-key"), ldvalue.String("default"), ldvalue.String("fallthrough"), ldvalue.String("default")),
time.Millisecond*100,
time.Millisecond*20,
"flag value was updated, but it should not have been",
)

require.NoError(t, s.persistence.WriteData(customPrefix+":features", s.initialFlags))

pollUntilFlagValueUpdated(t, client, "flag-key", ldcontext.New("user-key"), ldvalue.String("default"), ldvalue.String("fallthrough"), ldvalue.String("default"))
}
35 changes: 35 additions & 0 deletions sdktests/server_side_data_system_persistence_store.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
package sdktests

import (
"context"
"fmt"

"github.com/redis/go-redis/v9"
)

type PersistenceStore interface {
DSN() string

WriteData(key string, data map[string]string) error

Reset() error
}

type RedisPersistenceStore struct {
redis *redis.Client
}

func (r RedisPersistenceStore) DSN() string {
return fmt.Sprintf("redis://%s", r.redis.Options().Addr)
}

func (r RedisPersistenceStore) Reset() error {
var ctx = context.Background()
return r.redis.FlushAll(ctx).Err()
}

func (r RedisPersistenceStore) WriteData(key string, data map[string]string) error {
var ctx = context.Background()
_, err := r.redis.HSet(ctx, key, data).Result()
return err
}
185 changes: 185 additions & 0 deletions sdktests/server_side_data_system_read_only.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,185 @@
package sdktests

import (
"time"

"github.com/launchdarkly/go-sdk-common/v3/ldcontext"
"github.com/launchdarkly/go-sdk-common/v3/ldreason"
"github.com/launchdarkly/go-sdk-common/v3/ldvalue"
m "github.com/launchdarkly/go-test-helpers/v2/matchers"
h "github.com/launchdarkly/sdk-test-harness/v2/framework/helpers"
"github.com/launchdarkly/sdk-test-harness/v2/framework/ldtest"
o "github.com/launchdarkly/sdk-test-harness/v2/framework/opt"
"github.com/launchdarkly/sdk-test-harness/v2/servicedef"
"github.com/stretchr/testify/require"
)

func (s *ServerSideDataSystemTests) doReadOnlyTests(t *ldtest.T) {
t.Run("daemon mode", s.daemonModeTests)
}

func (s *ServerSideDataSystemTests) daemonModeTests(t *ldtest.T) {
t.Run("ignores database initialization flag", s.ignoresInitialization)
t.Run("can disable cache", s.canDisableCache)
t.Run("caches flag for duration", s.cachesFlagForDuration)
t.Run("caches flag forever", s.cachesFlagForever)
}

func (s *ServerSideDataSystemTests) ignoresInitialization(t *ldtest.T) {
dataSystem := NewDataSystem()
dataSystem.AddPersistence(servicedef.SDKConfigDataSystemPersistence{
Store: servicedef.SDKConfigDataSystemPersistenceStore{
Type: servicedef.Redis,
DSN: s.persistence.DSN(),
},
Cache: servicedef.SDKConfigDataSystemPersistenceCache{
Mode: servicedef.Off,
},
})
context := ldcontext.New("user-key")

require.NoError(t, s.persistence.Reset())
client := NewSDKClient(t, dataSystem)

h.RequireEventually(t, func() bool {
result := client.EvaluateFlag(t, servicedef.EvaluateFlagParams{
FlagKey: "flag-key",
Context: o.Some(context),
ValueType: servicedef.ValueTypeAny,
DefaultValue: ldvalue.String("default"),
Detail: true,
})

return result.Reason.IsDefined() &&
result.Reason.Value().GetErrorKind() == ldreason.EvalErrorFlagNotFound
}, time.Second, time.Millisecond*20, "flag was found before it should have been")

require.NoError(t, s.persistence.WriteData("launchdarkly:features", s.initialFlags))
pollUntilFlagValueUpdated(t, client, "flag-key", context, ldvalue.String("default"), ldvalue.String("fallthrough"), ldvalue.String("default"))

}

Check failure on line 60 in sdktests/server_side_data_system_read_only.go

View workflow job for this annotation

GitHub Actions / build-and-test

unnecessary trailing newline (whitespace)

func (s *ServerSideDataSystemTests) canDisableCache(t *ldtest.T) {
require.NoError(t, s.persistence.Reset())
require.NoError(t, s.persistence.WriteData("launchdarkly:features", s.initialFlags))

dataSystem := NewDataSystem()
dataSystem.AddPersistence(servicedef.SDKConfigDataSystemPersistence{
Store: servicedef.SDKConfigDataSystemPersistenceStore{
Type: servicedef.Redis,
DSN: s.persistence.DSN(),
},
Cache: servicedef.SDKConfigDataSystemPersistenceCache{
Mode: servicedef.Off,
},
})
context := ldcontext.New("user-key")

client := NewSDKClient(t, dataSystem)
pollUntilFlagValueUpdated(t, client, "flag-key", context, ldvalue.String("default"), ldvalue.String("fallthrough"), ldvalue.String("default"))

// Completely reset the database so there are no valid flag definitions
require.NoError(t, s.persistence.Reset())

h.RequireEventually(t,
checkForUpdatedValue(t, client, "flag-key", context, ldvalue.String("fallthrough"), ldvalue.String("default"), ldvalue.String("default")),
time.Second, time.Millisecond*20, "flag value was NOT updated after cache TTL")
}

func (s *ServerSideDataSystemTests) cachesFlagForDuration(t *ldtest.T) {
dataSystem := NewDataSystem()
dataSystem.AddPersistence(servicedef.SDKConfigDataSystemPersistence{
Store: servicedef.SDKConfigDataSystemPersistenceStore{
Type: servicedef.Redis,
DSN: s.persistence.DSN(),
},
Cache: servicedef.SDKConfigDataSystemPersistenceCache{
Mode: servicedef.TTL,
TTL: o.Some(1),
},
})
context := ldcontext.New("user-key")

t.Run("will cache found flag for TTL", func(t *ldtest.T) {
require.NoError(t, s.persistence.Reset())
client := NewSDKClient(t, dataSystem)

require.NoError(t, s.persistence.WriteData("launchdarkly:features", s.initialFlags))

pollUntilFlagValueUpdated(t, client, "flag-key", context, ldvalue.String("default"), ldvalue.String("fallthrough"), ldvalue.String("default"))

// Completely reset the database so there are no valid flag definitions
require.NoError(t, s.persistence.Reset())

h.RequireNever(t,
checkForUpdatedValue(t, client, "flag-key", context, ldvalue.String("fallthrough"), ldvalue.String("default"), ldvalue.String("default")),
time.Millisecond*500, time.Millisecond*20, "flag value was updated before cache TTL")

h.RequireEventually(t,
checkForUpdatedValue(t, client, "flag-key", context, ldvalue.String("fallthrough"), ldvalue.String("default"), ldvalue.String("default")),
time.Second, time.Millisecond*20, "flag value was NOT updated after cache TTL")
})

t.Run("will cache missing flag for TTL", func(t *ldtest.T) {
require.NoError(t, s.persistence.Reset())
client := NewSDKClient(t, dataSystem)

result := client.EvaluateFlag(t, servicedef.EvaluateFlagParams{
FlagKey: "flag-key",
Context: o.Some(context),
ValueType: servicedef.ValueTypeAny,
DefaultValue: ldvalue.String("default"),
Detail: true,
})

m.In(t).Assert(result.Value, m.Equal(ldvalue.String("default")))
m.In(t).Assert(result.Reason.Value().GetErrorKind(), m.Equal(ldreason.EvalErrorFlagNotFound))

require.NoError(t, s.persistence.WriteData("launchdarkly:features", s.initialFlags))

h.RequireNever(t,
checkForUpdatedValue(t, client, "flag-key", context, ldvalue.String("default"), ldvalue.String("fallthrough"), ldvalue.String("default")),
time.Microsecond*500, time.Millisecond*20, "flag value was updated before cache TTL")

h.RequireEventually(t,
checkForUpdatedValue(t, client, "flag-key", context, ldvalue.String("default"), ldvalue.String("fallthrough"), ldvalue.String("default")),
time.Second, time.Millisecond*20, "flag value was NOT updated after cache TTL")
})

}

Check failure on line 149 in sdktests/server_side_data_system_read_only.go

View workflow job for this annotation

GitHub Actions / build-and-test

unnecessary trailing newline (whitespace)

func (s *ServerSideDataSystemTests) cachesFlagForever(t *ldtest.T) {
dataSystem := NewDataSystem()
dataSystem.AddPersistence(servicedef.SDKConfigDataSystemPersistence{
Store: servicedef.SDKConfigDataSystemPersistenceStore{
Type: servicedef.Redis,
DSN: s.persistence.DSN(),
},
Cache: servicedef.SDKConfigDataSystemPersistenceCache{
Mode: servicedef.Infinite,
},
})
context := ldcontext.New("user-key")

require.NoError(t, s.persistence.Reset())
require.NoError(t, s.persistence.WriteData("launchdarkly:features", s.initialFlags))

client := NewSDKClient(t, dataSystem)

h.RequireEventually(t,
checkForUpdatedValue(t, client, "flag-key", context, ldvalue.String("default"), ldvalue.String("fallthrough"), ldvalue.String("default")),
time.Millisecond*500, time.Millisecond*20, "flag value was not changed")

// Reset the store and verify that the flag value is still cached
require.NoError(t, s.persistence.Reset())

// Uncached key is gone, so we should NEVER see it evaluate as expected.
h.RequireNever(t,
checkForUpdatedValue(t, client, "uncached-flag-key", context, ldvalue.String("default"), ldvalue.String("fallthrough"), ldvalue.String("default")),
time.Millisecond*500, time.Millisecond*20, "uncached-flag-key was not determined to be missing")

// We are caching the old flag version forever, so this should also never revert to the default.
h.RequireNever(t,
checkForUpdatedValue(t, client, "flag-key", context, ldvalue.String("fallthrough"), ldvalue.String("default"), ldvalue.String("default")),
time.Millisecond*500, time.Millisecond*20, "flag value was not changed")
}
5 changes: 4 additions & 1 deletion sdktests/testapi_sdk_client.go
Original file line number Diff line number Diff line change
Expand Up @@ -190,12 +190,15 @@ func TryNewSDKClient(t *ldtest.T, configurers ...SDKConfigurer) (*SDKClient, err
}

func validateSDKConfig(config servicedef.SDKConfigParams) error {
if !config.Streaming.IsDefined() && !config.Polling.IsDefined() && config.ServiceEndpoints.Value().Streaming == "" {
// TODO: I added this check about the datasystem here, but that isn't
// sufficient. We need better validation for the SDK config.
if !config.Streaming.IsDefined() && !config.Polling.IsDefined() && !config.DataSystem.IsDefined() && config.ServiceEndpoints.Value().Streaming == "" {
// Note that the default is streaming, so we don't necessarily need to set config.Streaming if there are
// no other customized options and if we used serviceEndpoints.streaming to set the stream URI
return errors.New(
"neither streaming nor polling was enabled-- did you forget to include the SDKDataSource as a parameter?")
}
// TODO: We are going to need something more like this for the datasystem and it's various synchronizers and initializers.
if config.Streaming.IsDefined() && config.Streaming.Value().BaseURI == "" &&
(!config.ServiceEndpoints.IsDefined() || config.ServiceEndpoints.Value().Streaming == "") {
return errors.New("streaming was enabled but base URI was not set")
Expand Down
Loading

0 comments on commit ea171f5

Please sign in to comment.