Skip to content

Commit

Permalink
chore: Create FDv2 compatible datasource implementation (#186)
Browse files Browse the repository at this point in the history
Introduces the new `datasourcev2` package to hold all FDv2 related
functionality.

This package contains a streaming datasource implementation that is
compatible with our current SDK, but pulls data updates from a
FDv2-compatible source.

It does not support any of the other FDv2-specific features like picking
up from a known state. That will be introduced in later work as we
re-shape the datasource integration on a larger scale.
  • Loading branch information
keelerm84 committed Sep 16, 2024
1 parent bc63105 commit ab9c9b8
Show file tree
Hide file tree
Showing 4 changed files with 762 additions and 18 deletions.
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
54 changes: 54 additions & 0 deletions internal/datasourcev2/helpers.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
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"

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

// 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
}
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

0 comments on commit ab9c9b8

Please sign in to comment.