Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
8 changes: 2 additions & 6 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -189,9 +189,7 @@ func main() {
Remote: client.RemoteOptions{
CertPath: "./certs/ca.pem",
ConnectTimeout: 300 * time.Millisecond,
ReadTimeout: 5 * time.Second,
WriteTimeout: 5 * time.Second,
PoolTimeout: 5 * time.Second,
Timeout: 5 * time.Second,
},
},
})
Expand Down Expand Up @@ -223,9 +221,7 @@ func main() {
|--------|------|-------------|---------|
| `CertPath` | `string` | Path to custom certificate for secure API connections | `""` |
| `ConnectTimeout` | `time.Duration` | Max time to establish a remote connection before failing fast | `300ms` |
| `ReadTimeout` | `time.Duration` | Max time to wait for remote response data | `5s` |
| `WriteTimeout` | `time.Duration` | Max time to send remote request data | `5s` |
| `PoolTimeout` | `time.Duration` | Max time to wait for a pooled HTTP connection | `5s` |
| `Timeout` | `time.Duration` | Max time for remote request/response and idle connection reuse | `5s` |

**Under development:** transport errors are normalized into typed SDK errors, and silent mode uses the configured remote timeouts to fail fast and switch back to local evaluation.

Expand Down
16 changes: 16 additions & 0 deletions client_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,22 @@ func TestClientGetSwitcher(t *testing.T) {
assert.NotSame(t, switcher1, switcher3, "expected different instances for different keys")
})

t.Run("should replace the cached switchers after rebuilding the default context", func(t *testing.T) {
BuildContext(Context{
Domain: "First Domain",
})

first := GetSwitcher("switcher1")

BuildContext(Context{
Domain: "Second Domain",
})

second := GetSwitcher("switcher1")

assert.NotSame(t, first, second, "expected a new cached switcher after rebuilding the default context")
})

t.Run("should return the cached instance after concurrent insert", func(t *testing.T) {
client := NewClient(Context{Domain: "My Domain"})

Expand Down
20 changes: 4 additions & 16 deletions context.go
Original file line number Diff line number Diff line change
Expand Up @@ -11,17 +11,13 @@ const (
DefaultRegexMaxBlacklist = 100
DefaultRegexMaxTimeLimit = 3 * time.Second
DefaultRemoteConnectTimeout = 300 * time.Millisecond
DefaultRemoteReadTimeout = 5 * time.Second
DefaultRemoteWriteTimeout = 5 * time.Second
DefaultRemotePoolTimeout = 5 * time.Second
DefaultRemoteTimeout = 5 * time.Second
)

type RemoteOptions struct {
CertPath string
ConnectTimeout time.Duration
ReadTimeout time.Duration
WriteTimeout time.Duration
PoolTimeout time.Duration
Timeout time.Duration
}

type ContextOptions struct {
Expand Down Expand Up @@ -78,16 +74,8 @@ func (o RemoteOptions) withDefaults() RemoteOptions {
o.ConnectTimeout = DefaultRemoteConnectTimeout
}

if o.ReadTimeout == 0 {
o.ReadTimeout = DefaultRemoteReadTimeout
}

if o.WriteTimeout == 0 {
o.WriteTimeout = DefaultRemoteWriteTimeout
}

if o.PoolTimeout == 0 {
o.PoolTimeout = DefaultRemotePoolTimeout
if o.Timeout == 0 {
o.Timeout = DefaultRemoteTimeout
}

return o
Expand Down
26 changes: 13 additions & 13 deletions context_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,49 +8,49 @@ import (

func TestBuildContext(t *testing.T) {
t.Run("should preserve optional context options", func(t *testing.T) {
BuildContext(Context{
client := NewClient(Context{
Domain: "My Domain",
Options: ContextOptions{
Local: true,
SnapshotLocation: "./tests/snapshots",
},
})

options := defaultClient().context.Options
options := client.Context().Options

assert.True(t, options.Local, "expected Local option to be true")
assert.Equal(t, "./tests/snapshots", options.SnapshotLocation, "expected SnapshotLocation to be './tests/snapshots'")
})

t.Run("should create fresh default options on rebuild", func(t *testing.T) {
BuildContext(Context{Domain: "First Domain"})
BuildContext(Context{
Domain: "First Domain",
Options: ContextOptions{
Local: true,
},
})
firstClient := defaultClient()

firstClient.mu.Lock()
firstClient.context.Options.Local = true
firstClient.mu.Unlock()

BuildContext(Context{Domain: "Second Domain"})
secondClient := defaultClient()

assert.NotSame(t, firstClient, secondClient, "expected different clients for different contexts")
assert.False(t, secondClient.context.Options.Local, "expected Local option to be false for the new client")
assert.True(t, firstClient.Context().Options.Local, "expected Local option to remain true for the first client")
assert.False(t, secondClient.Context().Options.Local, "expected Local option to be false for the new client")
})

t.Run("should apply default values when omitted", func(t *testing.T) {
BuildContext(Context{
client := NewClient(Context{
Domain: "My Domain",
})

ctx := defaultClient().context
ctx := client.Context()

assert.Equal(t, DefaultEnvironment, ctx.Environment)
assert.True(t, ctx.Options.RestrictRelay)
assert.Equal(t, DefaultRegexMaxBlacklist, ctx.Options.RegexMaxBlacklist)
assert.Equal(t, DefaultRegexMaxTimeLimit, ctx.Options.RegexMaxTimeLimit)
assert.Equal(t, DefaultRemoteConnectTimeout, ctx.Options.Remote.ConnectTimeout)
assert.Equal(t, DefaultRemoteReadTimeout, ctx.Options.Remote.ReadTimeout)
assert.Equal(t, DefaultRemoteWriteTimeout, ctx.Options.Remote.WriteTimeout)
assert.Equal(t, DefaultRemotePoolTimeout, ctx.Options.Remote.PoolTimeout)
assert.Equal(t, DefaultRemoteTimeout, ctx.Options.Remote.Timeout)
})
}
35 changes: 7 additions & 28 deletions remote.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ package client

import (
"bytes"
"cmp"
"context"
"crypto/tls"
"encoding/json"
Expand Down Expand Up @@ -134,15 +135,8 @@ func (c *Client) checkCriteria(token string, switcher *Switcher, showDetails boo
}

func (c *Client) doJSONRequest(method, endpoint string, payload any, headers map[string]string) (*http.Response, error) {
body, err := json.Marshal(payload)
if err != nil {
return nil, err
}

request, err := http.NewRequestWithContext(context.Background(), method, endpoint, bytes.NewReader(body))
if err != nil {
return nil, err
}
body, _ := json.Marshal(payload)
request, _ := http.NewRequestWithContext(context.Background(), method, endpoint, bytes.NewReader(body))

for key, value := range headers {
request.Header.Set(key, value)
Expand All @@ -165,32 +159,21 @@ func (c *Client) httpClient() *http.Client {
}

transport := &http.Transport{
DialContext: dialer.DialContext,
ResponseHeaderTimeout: ctx.Options.Remote.ReadTimeout,
TLSHandshakeTimeout: ctx.Options.Remote.ConnectTimeout,
IdleConnTimeout: ctx.Options.Remote.PoolTimeout,
DialContext: dialer.DialContext,
TLSHandshakeTimeout: ctx.Options.Remote.ConnectTimeout,
TLSClientConfig: &tls.Config{
MinVersion: tls.VersionTLS12,
},
}

c.httpClient_ = &http.Client{
Transport: transport,
Timeout: requestTimeout(ctx.Options.Remote),
Timeout: cmp.Or(ctx.Options.Remote.Timeout, DefaultRemoteTimeout),
}

return c.httpClient_
}

func requestTimeout(options RemoteOptions) time.Duration {
timeout := options.ConnectTimeout + options.ReadTimeout + options.WriteTimeout
if timeout <= 0 {
return DefaultRemoteConnectTimeout + DefaultRemoteReadTimeout + DefaultRemoteWriteTimeout
}

return timeout
}

func missingTokenError(token string) error {
if strings.TrimSpace(token) != "" {
return nil
Expand All @@ -200,11 +183,7 @@ func missingTokenError(token string) error {
}

func parseTokenExpiration(value json.Number) int64 {
parsed, err := value.Int64()
if err != nil {
return 0
}

parsed, _ := value.Int64()
return parsed
}

Expand Down
Loading
Loading