From 496a561812829934acea6d5e2fec5c65beef8596 Mon Sep 17 00:00:00 2001 From: Thomas Poignant Date: Tue, 15 Apr 2025 11:29:10 +0200 Subject: [PATCH 1/2] WIP Signed-off-by: Thomas Poignant --- Makefile | 3 + exporter/fileexporter/exporter.go | 2 +- internal/evaluation/evaluation.go | 82 ++++++++++++++++++++++++++ internal/evaluation/evaluation_test.go | 56 ++++++++++++++++++ variation.go | 65 +------------------- variation_test.go | 49 --------------- wasm/helpers/wasm.go | 23 ++++++++ wasm/main.go | 30 ++++++++++ wasm/model/evaluate_input.go | 14 +++++ 9 files changed, 211 insertions(+), 113 deletions(-) create mode 100644 internal/evaluation/evaluation.go create mode 100644 internal/evaluation/evaluation_test.go create mode 100644 wasm/helpers/wasm.go create mode 100644 wasm/main.go create mode 100644 wasm/model/evaluate_input.go diff --git a/Makefile b/Makefile index 2d6eadf412a..ab5ff950957 100644 --- a/Makefile +++ b/Makefile @@ -34,6 +34,9 @@ build-editor-api: create-out-dir ## Build the linter in out/bin/ build-jsonschema-generator: create-out-dir ## Build the jsonschema-generator in out/bin/ CGO_ENABLED=0 GO111MODULE=on $(GOCMD) build -mod vendor -o out/bin/jsonschema-generator ./cmd/jsonschema-generator/ +build-wasm: + CGO_ENABLED=0 GO111MODULE=on $(GOCMD) build -mod vendor -o out/bin/evaluation ./lib/evaluation + build-doc: ## Build the documentation cd website; \ npm i && npm run build diff --git a/exporter/fileexporter/exporter.go b/exporter/fileexporter/exporter.go index cf7f59e98f9..e54d9dd7818 100644 --- a/exporter/fileexporter/exporter.go +++ b/exporter/fileexporter/exporter.go @@ -137,7 +137,7 @@ func (f *Exporter) writeFile(filePath string, events []exporter.ExportableEvent) } _, errWrite := file.Write(line) if errWrite != nil { - return fmt.Errorf("error while writing the export file: %v", err) + return fmt.Errorf("error while writing the export file: %v", errWrite) } } return nil diff --git a/internal/evaluation/evaluation.go b/internal/evaluation/evaluation.go new file mode 100644 index 00000000000..9f447633005 --- /dev/null +++ b/internal/evaluation/evaluation.go @@ -0,0 +1,82 @@ +package evaluation + +import ( + "fmt" + "github.com/thomaspoignant/go-feature-flag/ffcontext" + "github.com/thomaspoignant/go-feature-flag/internal/flag" + "github.com/thomaspoignant/go-feature-flag/model" + "maps" +) + +const errorWrongVariation = "wrong variation used for flag %v" + +func Evaluate[T model.JSONType]( + f flag.Flag, + flagKey string, + evaluationCtx ffcontext.Context, + flagCtx flag.Context, + expectedType string, + sdkDefaultValue T) (model.VariationResult[T], error) { + flagValue, resolutionDetails := f.Value(flagKey, evaluationCtx, flagCtx) + var convertedValue interface{} + switch value := flagValue.(type) { + case float64: + // this part ensures that we convert float64 value into int if we call IntVariation on a float64 value. + if expectedType == "int" { + convertedValue = int(value) + } else { + convertedValue = value + } + default: + convertedValue = value + } + + var v T + switch val := convertedValue.(type) { + case T: + v = val + default: + if val != nil { + return model.VariationResult[T]{ + Value: sdkDefaultValue, + VariationType: flag.VariationSDKDefault, + Reason: flag.ReasonError, + ErrorCode: flag.ErrorCodeTypeMismatch, + Failed: true, + TrackEvents: f.IsTrackEvents(), + Version: f.GetVersion(), + Metadata: f.GetMetadata(), + }, fmt.Errorf(errorWrongVariation, flagKey) + } + } + return model.VariationResult[T]{ + Value: v, + VariationType: resolutionDetails.Variant, + Reason: resolutionDetails.Reason, + ErrorCode: resolutionDetails.ErrorCode, + ErrorDetails: resolutionDetails.ErrorMessage, + Failed: resolutionDetails.ErrorCode != "", + TrackEvents: f.IsTrackEvents(), + Version: f.GetVersion(), + Cacheable: resolutionDetails.Cacheable, + Metadata: constructMetadata(f, resolutionDetails), + }, nil +} + +// constructMetadata is the internal generic func used to enhance model.VariationResult adding +// the targeting.rule's name (from configuration) to the Metadata. +// That way, it is possible to see when a targeting rule is match during the evaluation process. +func constructMetadata( + f flag.Flag, + resolutionDetails flag.ResolutionDetails, +) map[string]interface{} { + metadata := maps.Clone(f.GetMetadata()) + if resolutionDetails.RuleName == nil || *resolutionDetails.RuleName == "" { + return metadata + } + if metadata == nil { + metadata = make(map[string]interface{}) + } + metadata["evaluatedRuleName"] = *resolutionDetails.RuleName + return metadata +} diff --git a/internal/evaluation/evaluation_test.go b/internal/evaluation/evaluation_test.go new file mode 100644 index 00000000000..9f0b920b343 --- /dev/null +++ b/internal/evaluation/evaluation_test.go @@ -0,0 +1,56 @@ +package evaluation + +import ( + "fmt" + "github.com/stretchr/testify/assert" + "github.com/thomaspoignant/go-feature-flag/internal/flag" + "runtime" + "testing" +) + +func Test_constructMetadataParallel(t *testing.T) { + sharedFlag := flag.InternalFlag{ + Metadata: &map[string]interface{}{ + "key1": "value1", + }, + } + + type args struct { + resolutionDetails flag.ResolutionDetails + } + var tests []struct { + name string + args args + wantEvaluatedRuleName string + } + + runtime.GOMAXPROCS(runtime.NumCPU()) + + // generate test cases + for i := 0; i < 10_000; i++ { + ruleName := fmt.Sprintf("rule-%d", i) + tests = append(tests, struct { + name string + args args + wantEvaluatedRuleName string + }{ + name: fmt.Sprintf("Rule %d", i), + args: args{ + resolutionDetails: flag.ResolutionDetails{ + RuleName: &ruleName, + }, + }, + wantEvaluatedRuleName: ruleName, + }) + } + + for _, tt := range tests { + tt := tt + t.Run(tt.name, func(t *testing.T) { + t.Parallel() + + got := constructMetadata(&sharedFlag, tt.args.resolutionDetails) + assert.Equal(t, tt.wantEvaluatedRuleName, got["evaluatedRuleName"]) + }) + } +} diff --git a/variation.go b/variation.go index c0d480523b8..dcbee8a7dc3 100644 --- a/variation.go +++ b/variation.go @@ -2,6 +2,7 @@ package ffclient import ( "fmt" + "github.com/thomaspoignant/go-feature-flag/internal/evaluation" "maps" "github.com/thomaspoignant/go-feature-flag/exporter" @@ -399,67 +400,5 @@ func getVariation[T model.JSONType]( if g.config.Environment != "" { flagCtx.AddIntoEvaluationContextEnrichment("env", g.config.Environment) } - flagValue, resolutionDetails := f.Value(flagKey, evaluationCtx, flagCtx) - - var convertedValue interface{} - switch value := flagValue.(type) { - case float64: - // this part ensures that we convert float64 value into int if we call IntVariation on a float64 value. - if expectedType == "int" { - convertedValue = int(value) - } else { - convertedValue = value - } - default: - convertedValue = value - } - - var v T - switch val := convertedValue.(type) { - case T: - v = val - default: - if val != nil { - return model.VariationResult[T]{ - Value: sdkDefaultValue, - VariationType: flag.VariationSDKDefault, - Reason: flag.ReasonError, - ErrorCode: flag.ErrorCodeTypeMismatch, - Failed: true, - TrackEvents: f.IsTrackEvents(), - Version: f.GetVersion(), - Metadata: f.GetMetadata(), - }, fmt.Errorf(errorWrongVariation, flagKey) - } - } - return model.VariationResult[T]{ - Value: v, - VariationType: resolutionDetails.Variant, - Reason: resolutionDetails.Reason, - ErrorCode: resolutionDetails.ErrorCode, - ErrorDetails: resolutionDetails.ErrorMessage, - Failed: resolutionDetails.ErrorCode != "", - TrackEvents: f.IsTrackEvents(), - Version: f.GetVersion(), - Cacheable: resolutionDetails.Cacheable, - Metadata: constructMetadata(f, resolutionDetails), - }, nil -} - -// constructMetadata is the internal generic func used to enhance model.VariationResult adding -// the targeting.rule's name (from configuration) to the Metadata. -// That way, it is possible to see when a targeting rule is match during the evaluation process. -func constructMetadata( - f flag.Flag, - resolutionDetails flag.ResolutionDetails, -) map[string]interface{} { - metadata := maps.Clone(f.GetMetadata()) - if resolutionDetails.RuleName == nil || *resolutionDetails.RuleName == "" { - return metadata - } - if metadata == nil { - metadata = make(map[string]interface{}) - } - metadata["evaluatedRuleName"] = *resolutionDetails.RuleName - return metadata + return evaluation.Evaluate[T](f, flagKey, evaluationCtx, flagCtx, expectedType, sdkDefaultValue) } diff --git a/variation_test.go b/variation_test.go index 1de83867528..772add2d884 100644 --- a/variation_test.go +++ b/variation_test.go @@ -3,10 +3,8 @@ package ffclient import ( "context" "errors" - "fmt" "log/slog" "os" - "runtime" "strings" "testing" "time" @@ -4153,53 +4151,6 @@ func TestRawVariation(t *testing.T) { } } -func Test_constructMetadataParallel(t *testing.T) { - sharedFlag := flag.InternalFlag{ - Metadata: &map[string]interface{}{ - "key1": "value1", - }, - } - - type args struct { - resolutionDetails flag.ResolutionDetails - } - var tests []struct { - name string - args args - wantEvaluatedRuleName string - } - - runtime.GOMAXPROCS(runtime.NumCPU()) - - // generate test cases - for i := 0; i < 10_000; i++ { - ruleName := fmt.Sprintf("rule-%d", i) - tests = append(tests, struct { - name string - args args - wantEvaluatedRuleName string - }{ - name: fmt.Sprintf("Rule %d", i), - args: args{ - resolutionDetails: flag.ResolutionDetails{ - RuleName: &ruleName, - }, - }, - wantEvaluatedRuleName: ruleName, - }) - } - - for _, tt := range tests { - tt := tt - t.Run(tt.name, func(t *testing.T) { - t.Parallel() - - got := constructMetadata(&sharedFlag, tt.args.resolutionDetails) - assert.Equal(t, tt.wantEvaluatedRuleName, got["evaluatedRuleName"]) - }) - } -} - func Test_OverrideContextEnrichmentWithEnvironment(t *testing.T) { tempFile, err := os.CreateTemp("", "") require.NoError(t, err) diff --git a/wasm/helpers/wasm.go b/wasm/helpers/wasm.go new file mode 100644 index 00000000000..82a3f55f888 --- /dev/null +++ b/wasm/helpers/wasm.go @@ -0,0 +1,23 @@ +package helpers + +import "unsafe" + +// WasmReadBufferFromMemory reads a buffer from memory and returns it as a byte slice. +func WasmReadBufferFromMemory(bufferPosition *uint32, length uint32) []byte { + subjectBuffer := make([]byte, length) + pointer := uintptr(unsafe.Pointer(bufferPosition)) + for i := 0; i < int(length); i++ { + s := *(*int32)(unsafe.Pointer(pointer + uintptr(i))) + subjectBuffer[i] = byte(s) + } + return subjectBuffer +} + +// WasmCopyBufferToMemory copies a buffer to memory and returns a pointer to the memory location. +func WasmCopyBufferToMemory(buffer []byte) uint64 { + bufferPtr := &buffer[0] + unsafePtr := uintptr(unsafe.Pointer(bufferPtr)) + pos := uint32(unsafePtr) + size := uint32(len(buffer)) + return (uint64(pos) << uint64(32)) | uint64(size) +} diff --git a/wasm/main.go b/wasm/main.go new file mode 100644 index 00000000000..7e6cf1ff093 --- /dev/null +++ b/wasm/main.go @@ -0,0 +1,30 @@ +package main + +import ( + "fmt" + "github.com/thomaspoignant/go-feature-flag/ffcontext" + "github.com/thomaspoignant/go-feature-flag/internal/evaluation" + "github.com/thomaspoignant/go-feature-flag/internal/flag" + "github.com/thomaspoignant/go-feature-flag/wasm/helpers" + "strings" +) + +func main() { + // We keep this main empty because it is required by the tinygo when building wasm. +} + +//export evaluate +func evaluate(valuePosition *uint32, length uint32) uint64 { + input := helpers.WasmReadBufferFromMemory(valuePosition, length) + inputAsString := strings.SplitAfter(string(input), "\n") + + var f flag.InternalFlag + var flagkey string + var evaluationCtx ffcontext.EvaluationContext + var flagCtx flag.Context + var sdkDefaultValue interface{} + + c, err := evaluation.Evaluate[interface{}]( + &f, flagkey, evaluationCtx, flagCtx, "interface{}", sdkDefaultValue) + fmt.Println(c, err) +} diff --git a/wasm/model/evaluate_input.go b/wasm/model/evaluate_input.go new file mode 100644 index 00000000000..fab68bfd6d5 --- /dev/null +++ b/wasm/model/evaluate_input.go @@ -0,0 +1,14 @@ +package model + +import ( + "github.com/thomaspoignant/go-feature-flag/ffcontext" + "github.com/thomaspoignant/go-feature-flag/internal/flag" +) + +type EvaluateInput struct { + FlagKey string `json:"flagKey"` + Flag flag.InternalFlag `json:"flag"` + EvaluationCtx ffcontext.EvaluationContext `json:"evaluationContext"` + FlagContext flag.Context `json:"flagContext"` + SdkDefaultValue interface{} `json:"sdkDefaultValue"` +} From 20e67f28e0192fc5726dbb543cc40ebafe241457 Mon Sep 17 00:00:00 2001 From: Thomas Poignant Date: Tue, 15 Apr 2025 11:29:29 +0200 Subject: [PATCH 2/2] WIP Signed-off-by: Thomas Poignant --- internal/evaluation/evaluation.go | 3 ++- internal/evaluation/evaluation_test.go | 5 +++-- variation.go | 2 +- wasm/main.go | 3 ++- 4 files changed, 8 insertions(+), 5 deletions(-) diff --git a/internal/evaluation/evaluation.go b/internal/evaluation/evaluation.go index 9f447633005..270546d30eb 100644 --- a/internal/evaluation/evaluation.go +++ b/internal/evaluation/evaluation.go @@ -2,10 +2,11 @@ package evaluation import ( "fmt" + "maps" + "github.com/thomaspoignant/go-feature-flag/ffcontext" "github.com/thomaspoignant/go-feature-flag/internal/flag" "github.com/thomaspoignant/go-feature-flag/model" - "maps" ) const errorWrongVariation = "wrong variation used for flag %v" diff --git a/internal/evaluation/evaluation_test.go b/internal/evaluation/evaluation_test.go index 9f0b920b343..140a0bb3d94 100644 --- a/internal/evaluation/evaluation_test.go +++ b/internal/evaluation/evaluation_test.go @@ -2,10 +2,11 @@ package evaluation import ( "fmt" - "github.com/stretchr/testify/assert" - "github.com/thomaspoignant/go-feature-flag/internal/flag" "runtime" "testing" + + "github.com/stretchr/testify/assert" + "github.com/thomaspoignant/go-feature-flag/internal/flag" ) func Test_constructMetadataParallel(t *testing.T) { diff --git a/variation.go b/variation.go index dcbee8a7dc3..67fe8476e7c 100644 --- a/variation.go +++ b/variation.go @@ -2,11 +2,11 @@ package ffclient import ( "fmt" - "github.com/thomaspoignant/go-feature-flag/internal/evaluation" "maps" "github.com/thomaspoignant/go-feature-flag/exporter" "github.com/thomaspoignant/go-feature-flag/ffcontext" + "github.com/thomaspoignant/go-feature-flag/internal/evaluation" "github.com/thomaspoignant/go-feature-flag/internal/flag" "github.com/thomaspoignant/go-feature-flag/model" ) diff --git a/wasm/main.go b/wasm/main.go index 7e6cf1ff093..63d3f325647 100644 --- a/wasm/main.go +++ b/wasm/main.go @@ -2,11 +2,12 @@ package main import ( "fmt" + "strings" + "github.com/thomaspoignant/go-feature-flag/ffcontext" "github.com/thomaspoignant/go-feature-flag/internal/evaluation" "github.com/thomaspoignant/go-feature-flag/internal/flag" "github.com/thomaspoignant/go-feature-flag/wasm/helpers" - "strings" ) func main() {