Skip to content

Commit

Permalink
fix(flags): Add configurable flag timeouts
Browse files Browse the repository at this point in the history
  • Loading branch information
neilkakkar committed Mar 14, 2024
1 parent 4944045 commit 81865b0
Show file tree
Hide file tree
Showing 5 changed files with 51 additions and 28 deletions.
10 changes: 10 additions & 0 deletions config.go
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,9 @@ type Config struct {
// Interval at which to fetch new feature flags, 5min by default
DefaultFeatureFlagsPollingInterval time.Duration

// Timeout for fetching feature flags, 3 seconds by default
FeatureFlagRequestTimeout time.Duration

// The HTTP transport used by the client, this allows an application to
// redefine how requests are being sent at the HTTP level (for example,
// to change the connection pooling policy).
Expand Down Expand Up @@ -94,6 +97,9 @@ const DefaultInterval = 5 * time.Second
// Specifies the default interval at which to fetch new feature flags
const DefaultFeatureFlagsPollingInterval = 5 * time.Minute

// Specifies the default timeout for fetching feature flags
const DefaultFeatureFlagRequestTimeout = 3 * time.Second

// This constant sets the default batch size used by client instances if none
// was explicitly set.
const DefaultBatchSize = 250
Expand Down Expand Up @@ -135,6 +141,10 @@ func makeConfig(c Config) Config {
c.DefaultFeatureFlagsPollingInterval = DefaultFeatureFlagsPollingInterval
}

if c.FeatureFlagRequestTimeout == 0 {
c.FeatureFlagRequestTimeout = DefaultFeatureFlagRequestTimeout
}

if c.Transport == nil {
c.Transport = http.DefaultTransport
}
Expand Down
26 changes: 17 additions & 9 deletions examples/featureflags.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,11 +8,13 @@ import (
)

func TestIsFeatureEnabled() {
client, _ := posthog.NewWithConfig("phc_X8B6bhR1QgQKP1WdpFLN82LxLxgZ7WPXDgJyRyvIpib", posthog.Config{
Interval: 30 * time.Second,
BatchSize: 100,
Verbose: true,
PersonalApiKey: "phx_vXZ7AOnFjDrCxfWLyo9V6P0SWLLfXT2d5euy3U0nRGk",
client, _ := posthog.NewWithConfig("phc_36WfBWNJEQcYotMZ7Ui7EWzqKLbIo2LWJFG5fIg1EER", posthog.Config{
Interval: 30 * time.Second,
BatchSize: 100,
Verbose: true,
PersonalApiKey: "phx_n79cT52OfsxAWDhZs9j3w67aRoBCZ7l5ksRRKmAi5nr",
Endpoint: "http://localhost:8000",
FeatureFlagRequestTimeout: 3 * time.Second,
})
defer client.Close()

Expand All @@ -22,38 +24,44 @@ func TestIsFeatureEnabled() {
DistinctId: "hello",
})

fmt.Println("boolResult:", boolResult)

if boolErr != nil || boolResult == nil {
fmt.Println("error:", boolErr)
return
// return
}

// Simple flag
simpleResult, simpleErr := client.GetFeatureFlag(posthog.FeatureFlagPayload{
Key: "simple-test",
DistinctId: "hello",
})

fmt.Println("simpleResult:", simpleResult)
if simpleErr != nil || simpleResult == false {
fmt.Println("error:", simpleErr)
return
// return
}

// Multivariate flag
variantResult, variantErr := client.GetFeatureFlag(posthog.FeatureFlagPayload{
Key: "multivariate-test",
DistinctId: "hello",
})
fmt.Println("variantResult:", variantResult)
if variantErr != nil || variantResult != "variant-value" {
fmt.Println("error:", variantErr)
return
// return
}

// Multivariate + simple flag
variantResult, variantErr = client.GetFeatureFlag(posthog.FeatureFlagPayload{
Key: "multivariate-simple-test",
DistinctId: "hello",
})
fmt.Println("variantResult:", variantResult)
if variantErr != nil || variantResult == true {
fmt.Println("error:", variantErr)
return
// return
}
}
4 changes: 2 additions & 2 deletions examples/main.go
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
package main

func main() {
TestCapture()
TestCaptureWithSendFeatureFlagOption()
// TestCapture()
// TestCaptureWithSendFeatureFlagOption()
TestIsFeatureEnabled()
}
37 changes: 21 additions & 16 deletions featureflags.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ package posthog

import (
"bytes"
"context"
"crypto/sha1"
"encoding/json"
"errors"
Expand Down Expand Up @@ -34,6 +35,7 @@ type FeatureFlagsPoller struct {
http http.Client
mutex sync.RWMutex
fetchedFlagsSuccessfullyOnce bool
flagTimeout time.Duration
}

type FeatureFlag struct {
Expand Down Expand Up @@ -113,7 +115,7 @@ func (e *InconclusiveMatchError) Error() string {
return e.msg
}

func newFeatureFlagsPoller(projectApiKey string, personalApiKey string, errorf func(format string, args ...interface{}), endpoint string, httpClient http.Client, pollingInterval time.Duration) *FeatureFlagsPoller {
func newFeatureFlagsPoller(projectApiKey string, personalApiKey string, errorf func(format string, args ...interface{}), endpoint string, httpClient http.Client, pollingInterval time.Duration, flagTimeout time.Duration) *FeatureFlagsPoller {
poller := FeatureFlagsPoller{
ticker: time.NewTicker(pollingInterval),
loaded: make(chan bool),
Expand All @@ -126,6 +128,7 @@ func newFeatureFlagsPoller(projectApiKey string, personalApiKey string, errorf f
http: httpClient,
mutex: sync.RWMutex{},
fetchedFlagsSuccessfullyOnce: false,
flagTimeout: flagTimeout,
}

go poller.run()
Expand Down Expand Up @@ -154,7 +157,8 @@ func (poller *FeatureFlagsPoller) run() {
func (poller *FeatureFlagsPoller) fetchNewFeatureFlags() {
personalApiKey := poller.personalApiKey
headers := [][2]string{{"Authorization", "Bearer " + personalApiKey + ""}}
res, err := poller.localEvaluationFlags(headers)
res, err, cancel := poller.localEvaluationFlags(headers)
defer cancel()
if err != nil || res.StatusCode != http.StatusOK {
poller.loaded <- false
poller.Errorf("Unable to fetch feature flags", err)
Expand All @@ -178,9 +182,7 @@ func (poller *FeatureFlagsPoller) fetchNewFeatureFlags() {
poller.loaded <- true
}
newFlags := []FeatureFlag{}
for _, flag := range featureFlagsResponse.Flags {
newFlags = append(newFlags, flag)
}
newFlags = append(newFlags, featureFlagsResponse.Flags...)
poller.mutex.Lock()
poller.featureFlags = newFlags
poller.cohorts = featureFlagsResponse.Cohorts
Expand Down Expand Up @@ -731,7 +733,7 @@ func interfaceToFloat(val interface{}) (float64, error) {
case uint64:
i = float64(t)
default:
errMessage := "Argument not orderable"
errMessage := "argument not orderable"
return 0.0, errors.New(errMessage)
}

Expand Down Expand Up @@ -800,18 +802,18 @@ func (poller *FeatureFlagsPoller) GetFeatureFlags() []FeatureFlag {
return poller.featureFlags
}

func (poller *FeatureFlagsPoller) decide(requestData []byte, headers [][2]string) (*http.Response, error) {
localEvaluationEndpoint := "decide/?v=2"
func (poller *FeatureFlagsPoller) decide(requestData []byte, headers [][2]string) (*http.Response, error, context.CancelFunc) {
decideEndpoint := "decide/?v=2"

url, err := url.Parse(poller.Endpoint + "/" + localEvaluationEndpoint + "")
url, err := url.Parse(poller.Endpoint + "/" + decideEndpoint + "")
if err != nil {
poller.Errorf("creating url - %s", err)
}

return poller.request("POST", url, requestData, headers)
return poller.request("POST", url, requestData, headers, poller.flagTimeout)
}

func (poller *FeatureFlagsPoller) localEvaluationFlags(headers [][2]string) (*http.Response, error) {
func (poller *FeatureFlagsPoller) localEvaluationFlags(headers [][2]string) (*http.Response, error, context.CancelFunc) {
localEvaluationEndpoint := "api/feature_flag/local_evaluation"

url, err := url.Parse(poller.Endpoint + "/" + localEvaluationEndpoint + "")
Expand All @@ -823,11 +825,13 @@ func (poller *FeatureFlagsPoller) localEvaluationFlags(headers [][2]string) (*ht
searchParams.Add("send_cohorts", "true")
url.RawQuery = searchParams.Encode()

return poller.request("GET", url, []byte{}, headers)
return poller.request("GET", url, []byte{}, headers, time.Duration(10)*time.Second)
}

func (poller *FeatureFlagsPoller) request(method string, url *url.URL, requestData []byte, headers [][2]string) (*http.Response, error) {
req, err := http.NewRequest(method, url.String(), bytes.NewReader(requestData))
func (poller *FeatureFlagsPoller) request(method string, url *url.URL, requestData []byte, headers [][2]string, timeout time.Duration) (*http.Response, error, context.CancelFunc) {
ctx, cancel := context.WithTimeout(context.Background(), timeout)

req, err := http.NewRequestWithContext(ctx, method, url.String(), bytes.NewReader(requestData))
if err != nil {
poller.Errorf("creating request - %s", err)
}
Expand All @@ -847,7 +851,7 @@ func (poller *FeatureFlagsPoller) request(method string, url *url.URL, requestDa
poller.Errorf("sending request - %s", err)
}

return res, err
return res, err, cancel
}

func (poller *FeatureFlagsPoller) ForceReload() {
Expand All @@ -873,7 +877,8 @@ func (poller *FeatureFlagsPoller) getFeatureFlagVariants(distinctId string, grou
poller.Errorf(errorMessage)
return nil, errors.New(errorMessage)
}
res, err := poller.decide(requestDataBytes, headers)
res, err, cancel := poller.decide(requestDataBytes, headers)
defer cancel()
if err != nil || res.StatusCode != http.StatusOK {
errorMessage = "Error calling /decide/"
poller.Errorf(errorMessage)
Expand Down
2 changes: 1 addition & 1 deletion posthog.go
Original file line number Diff line number Diff line change
Expand Up @@ -111,7 +111,7 @@ func NewWithConfig(apiKey string, config Config) (cli Client, err error) {
}

if len(c.PersonalApiKey) > 0 {
c.featureFlagsPoller = newFeatureFlagsPoller(c.key, c.Config.PersonalApiKey, c.Errorf, c.Endpoint, c.http, c.DefaultFeatureFlagsPollingInterval)
c.featureFlagsPoller = newFeatureFlagsPoller(c.key, c.Config.PersonalApiKey, c.Errorf, c.Endpoint, c.http, c.DefaultFeatureFlagsPollingInterval, c.FeatureFlagRequestTimeout)
}

go c.loop()
Expand Down

0 comments on commit 81865b0

Please sign in to comment.