Skip to content

refactor: encapsulate existing SDK data system [DRAFT] #188

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Closed
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
33 commits
Select commit Hold shift + click to select a range
953d678
chore: Create FDv2 compatible datasource implementation
keelerm84 Aug 29, 2024
77aa8df
chore: Create FDv2 compatible polling data source
keelerm84 Aug 29, 2024
91236e8
refactor: add internal data system config
cwaldren-ld Aug 28, 2024
7d47e83
more docs
cwaldren-ld Aug 29, 2024
bf7f04d
refactor persistent store config
cwaldren-ld Aug 30, 2024
d1d1992
make package name shorter in import
cwaldren-ld Sep 6, 2024
0eec886
adding a V2 method to existing data sources
cwaldren-ld Sep 6, 2024
dbdf59d
creating the dataSystem interface
cwaldren-ld Sep 6, 2024
18e415b
tests pass
cwaldren-ld Sep 6, 2024
6bcced3
lints
cwaldren-ld Sep 6, 2024
bfc6cbf
add concept of DataStatus, remove need to check initialized
cwaldren-ld Sep 6, 2024
15ecf0b
create stub fdv2 data system
cwaldren-ld Sep 6, 2024
3b2db18
add dual-mode store
cwaldren-ld Sep 7, 2024
cb76023
refactoring the store component
cwaldren-ld Sep 10, 2024
e0f7cd9
comment
cwaldren-ld Sep 10, 2024
2b8a48f
doc comments
cwaldren-ld Sep 10, 2024
7799ab1
copious comments
cwaldren-ld Sep 10, 2024
2528954
adding store unit tests
cwaldren-ld Sep 10, 2024
9712057
use pointer swap to switch stores
cwaldren-ld Sep 10, 2024
7cb03b1
goimports
cwaldren-ld Sep 10, 2024
1ee88b2
more store unit tests
cwaldren-ld Sep 10, 2024
4850f82
revert changes to StreamingDataSourceBuilder, and make a V2 struct in…
cwaldren-ld Sep 10, 2024
cb3678b
remove now-unnecessary ToSynchronizer converter
cwaldren-ld Sep 10, 2024
cdbac24
make v2 data sources implement Synchronizer
cwaldren-ld Sep 10, 2024
b21c901
ensure closeWhenReady is closed in offline mode
cwaldren-ld Sep 11, 2024
3685b07
expose some fdv2 types in datasourcev2, and use them to implement e2e…
cwaldren-ld Sep 11, 2024
04faaba
break out data destination/status reporter from DataSourceUpdateSink …
cwaldren-ld Sep 11, 2024
b76539a
make separate test file for fdv2 e2e tests
cwaldren-ld Sep 12, 2024
e0c9cd0
use top-level cfg.Offline to determine if data system should be enabled
cwaldren-ld Sep 12, 2024
11bf0e6
add dedicated FDv2 streaming protocol builder for unit tests
cwaldren-ld Sep 12, 2024
bc3c696
add another unit test
cwaldren-ld Sep 13, 2024
7a535af
more fdv2 parity e2e tests
cwaldren-ld Sep 14, 2024
5b8230f
goimports
cwaldren-ld Sep 14, 2024
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
37 changes: 37 additions & 0 deletions config.go
Original file line number Diff line number Diff line change
Expand Up @@ -197,4 +197,41 @@ type Config struct {
// LaunchDarkly provides integration packages, and most applications will not
// need to implement their own hooks.
Hooks []ldhooks.Hook

// This field is not stable, and not subject to any backwards compatibility guarantees or semantic versioning.
// It is not suitable for production usage. Do not use it. You have been warned.
//
// DataSystem configures how data (e.g. flags, segments) are retrieved by the SDK.
//
// Set this field only if you want to specify non-default values for any of the data system configuration,
// such as defining an alternate data source or setting up a persistent store.
//
// Below, the default configuration is described with the relevant config item in parentheses:
// 1. The SDK will first attempt to fetch all data from LaunchDarkly's global Content Delivery Network (Initializer)
// 2. It will then establish a streaming connection with LaunchDarkly's realtime Flag Delivery Network (Primary
// Synchronizer.)
// 3. If at any point the connection to the realtime network is interrupted for a short period of time,
// the connection will be automatically re-established.
// 4. If the connection cannot be re-established over a sustained period, the SDK will begin to make periodic
// requests to LaunchDarkly's global CDN (Secondary Synchronizer)
// 5. After a period of time, the SDK will swap back to the realtime Flag Delivery Network if it becomes
// available again.
//
// The default streaming mode configuration is preferred for most use-cases (DataSystem().StreamingPreferred()).
// Sometimes streaming connections are blocked by firewalls or proxies. If this is the case, a polling-only mode
// can be configured:
//
// config := ld.Config{
// DataSystem: ldcomponents.DataSystem().PollingOnly(),
// }
//
// If you'd like to load data from a local source to provide redundancy if there is a problem
// connecting to LaunchDarkly, you can add a custom initializer:
//
// config := ld.Config {
// DataSystem: ldcomponents.DataSystem().PrependInitializers(myCustomInitializer),
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Instead of making this prepend, can this be a simple add style method? Prepending adds an unnecessary "think of this as a stack" mental overhead I think we'd like to avoid.

I realize these are supposed to run before our default initializer, but maybe we can figure out a better way to represent that. Maybe we call these custom initializers, and the docs make that clear that's before the built-in. We can discuss.

Also we should make this method singular unless it is going to be variadic.

// }
//
// The initializer(s) will run before LaunchDarkly's default initializer.
DataSystem subsystems.ComponentConfigurer[subsystems.DataSystemConfiguration]
}
36 changes: 18 additions & 18 deletions internal/datasource/streaming_data_source_events.go
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ var (
deleteDataRequiredProperties = []string{"path", "version"} //nolint:gochecknoglobals
)

// This is the logical representation of the data in the "put" event. In the JSON representation,
// PutData is the logical representation of the data in the "put" event. In the JSON representation,
// the "data" property is actually a map of maps, but the schema we use internally is a list of
// lists instead.
//
Expand All @@ -37,12 +37,12 @@ var (
// }
// }
// }
type putData struct {
type PutData struct {
Path string // we don't currently do anything with this
Data []ldstoretypes.Collection
}

// This is the logical representation of the data in the "patch" event. In the JSON representation,
// PatchData is the logical representation of the data in the "patch" event. In the JSON representation,
// there is a "path" property in the format "/flags/key" or "/segments/key", which we convert into
// Kind and Key when we parse it. The "data" property is the JSON representation of the flag or
// segment, which we deserialize into an ItemDescriptor.
Expand All @@ -56,13 +56,13 @@ type putData struct {
// "version": 2, ...etc.
// }
// }
type patchData struct {
type PatchData struct {
Kind ldstoretypes.DataKind
Key string
Data ldstoretypes.ItemDescriptor
}

// This is the logical representation of the data in the "delete" event. In the JSON representation,
// DeleteData is the logical representation of the data in the "delete" event. In the JSON representation,
// there is a "path" property in the format "/flags/key" or "/segments/key", which we convert into
// Kind and Key when we parse it.
//
Expand All @@ -72,14 +72,14 @@ type patchData struct {
// "path": "/flags/flagkey",
// "version": 3
// }
type deleteData struct {
type DeleteData struct {
Kind ldstoretypes.DataKind
Key string
Version int
}

func parsePutData(data []byte) (putData, error) {
var ret putData
func parsePutData(data []byte) (PutData, error) {
var ret PutData
r := jreader.NewReader(data)
for obj := r.Object().WithRequiredProperties(putDataRequiredProperties); obj.Next(); {
switch string(obj.Name()) {
Expand All @@ -92,15 +92,15 @@ func parsePutData(data []byte) (putData, error) {
return ret, r.Error()
}

func parsePatchData(data []byte) (patchData, error) {
var ret patchData
func parsePatchData(data []byte) (PatchData, error) {
var ret PatchData
r := jreader.NewReader(data)
var kind datakinds.DataKindInternal
var key string
parseItem := func() (patchData, error) {
parseItem := func() (PatchData, error) {
item, err := kind.DeserializeFromJSONReader(&r)
if err != nil {
return patchData{}, err
return PatchData{}, err
}
ret.Data = item
return ret, nil
Expand All @@ -126,7 +126,7 @@ func parsePatchData(data []byte) (patchData, error) {
}
}
if err := r.Error(); err != nil {
return patchData{}, err
return PatchData{}, err
}
// If we got here, it means we couldn't parse the data model object yet because we saw the
// "data" property first. But we definitely saw both properties (otherwise we would've got
Expand All @@ -138,13 +138,13 @@ func parsePatchData(data []byte) (patchData, error) {
}
}
if r.Error() != nil {
return patchData{}, r.Error()
return PatchData{}, r.Error()
}
return patchData{}, errors.New("patch event had no data property")
return PatchData{}, errors.New("patch event had no data property")
}

func parseDeleteData(data []byte) (deleteData, error) {
var ret deleteData
func parseDeleteData(data []byte) (DeleteData, error) {
var ret DeleteData
r := jreader.NewReader(data)
for obj := r.Object().WithRequiredProperties(deleteDataRequiredProperties); obj.Next(); {
switch string(obj.Name()) {
Expand All @@ -161,7 +161,7 @@ func parseDeleteData(data []byte) (deleteData, error) {
}
}
if r.Error() != nil {
return deleteData{}, r.Error()
return DeleteData{}, r.Error()
}
return ret, nil
}
Expand Down
85 changes: 85 additions & 0 deletions internal/datasourcev2/helpers.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
package datasourcev2

//nolint: godox
// TODO: This was copied from datasource/helpers.go. We should extract these
// out into a common module, or if we decide we don't need these later in the
// v2 implementation, we should clean this up.

import (
"fmt"
"net/http"

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

type httpStatusError struct {
Message string
Code int
}

func (e httpStatusError) Error() string {
return e.Message
}

// Tests whether an HTTP error status represents a condition that might resolve on its own if we retry,
// or at least should not make us permanently stop sending requests.
func isHTTPErrorRecoverable(statusCode int) bool {
if statusCode >= 400 && statusCode < 500 {
switch statusCode {
case 400: // bad request
return true
case 408: // request timeout
return true
case 429: // too many requests
return true
default:
return false // all other 4xx errors are unrecoverable
}
}
return true
}

func httpErrorDescription(statusCode int) string {
message := ""
if statusCode == 401 || statusCode == 403 {
message = " (invalid SDK key)"
}
return fmt.Sprintf("HTTP error %d%s", statusCode, message)
}

// Logs an HTTP error or network error at the appropriate level and determines whether it is recoverable
// (as defined by isHTTPErrorRecoverable).
func checkIfErrorIsRecoverableAndLog(
loggers ldlog.Loggers,
errorDesc, errorContext string,
statusCode int,
recoverableMessage string,
) bool {
if statusCode > 0 && !isHTTPErrorRecoverable(statusCode) {
loggers.Errorf("Error %s (giving up permanently): %s", errorContext, errorDesc)
return false
}
loggers.Warnf("Error %s (%s): %s", errorContext, recoverableMessage, errorDesc)
return true
}

func checkForHTTPError(statusCode int, url string) error {
if statusCode == http.StatusUnauthorized {
return httpStatusError{
Message: fmt.Sprintf("Invalid SDK key when accessing URL: %s. Verify that your SDK key is correct.", url),
Code: statusCode}
}

if statusCode == http.StatusNotFound {
return httpStatusError{
Message: fmt.Sprintf("Resource not found when accessing URL: %s. Verify that this resource exists.", url),
Code: statusCode}
}

if statusCode/100 != 2 {
return httpStatusError{
Message: fmt.Sprintf("Unexpected response code: %d when accessing URL: %s", statusCode, url),
Code: statusCode}
}
return nil
}
9 changes: 9 additions & 0 deletions internal/datasourcev2/package_info.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
// Package datasourcev2 is an internal package containing implementation types for the SDK's data source
// implementations (streaming, polling, etc.) and related functionality. These types are not visible
// from outside of the SDK.
//
// WARNING: This particular implementation supports the upcoming flag delivery v2 format which is not
// publicly available.
//
// This does not include the file data source, which is in the ldfiledata package.
package datasourcev2
Loading
Loading