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
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -223,7 +223,7 @@ func main() {
| `ConnectTimeout` | `time.Duration` | Max time to establish a remote connection before failing fast | `300ms` |
| `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.
**Note:** lower remote connect timeouts help silent mode fall back faster when the upstream is unavailable.

#### Security Features

Expand Down
40 changes: 15 additions & 25 deletions client.go
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,9 @@ type Client struct {

httpClientMu sync.Mutex
httpClient_ *http.Client

notifyErrorMu sync.RWMutex
notifyErrorCallback func(error)
}

func NewClient(ctx Context) *Client {
Expand Down Expand Up @@ -171,38 +174,25 @@ func (c *Client) CheckSnapshot() (bool, error) {
return true, nil
}

func (c *Client) snapshotState() *Snapshot {
c.mu.RLock()
defer c.mu.RUnlock()

return c.snapshot
func SubscribeNotifyError(callback func(error)) {
defaultClient().SubscribeNotifyError(callback)
}

func (c *Client) setSnapshot(snapshot *Snapshot) {
c.mu.Lock()
defer c.mu.Unlock()

c.snapshot = snapshot
}
func (c *Client) SubscribeNotifyError(callback func(error)) {
c.notifyErrorMu.Lock()
defer c.notifyErrorMu.Unlock()

func (c *Client) stopBackgroundTasks() {
c.TerminateSnapshotAutoUpdate()
c.UnwatchSnapshot()
c.notifyErrorCallback = callback
}

func (c *Client) shouldCheckSnapshot(fetchRemote bool) bool {
ctx := c.Context()
return c.SnapshotVersion() == 0 && (fetchRemote || !ctx.Options.Local)
}
func (c *Client) notifyError(err error) {
c.notifyErrorMu.RLock()
callback := c.notifyErrorCallback
c.notifyErrorMu.RUnlock()

func (c *Client) loadSnapshotFromCurrentFile() (*Snapshot, error) {
snapshot, err := loadSnapshotFromFile(c.Context())
if err != nil {
return nil, err
if callback != nil {
callback(err)
}

c.setSnapshot(snapshot)
return snapshot, nil
}

func defaultClient() *Client {
Expand Down
65 changes: 65 additions & 0 deletions client_silent_mode.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
package client

import "time"

const silentModeAuthToken = "SILENT"

func (c *Client) shouldUseLocalSilentMode() bool {
if !c.hasSilentMode() {
return false
}

token, expiration := c.authState()
if token != silentModeAuthToken {
return false
}

if !tokenExpired(expiration) {
return true
}

c.updateSilentToken()
if c.checkAPIHealth() {
c.clearSilentToken()
return false
}

return true
}

func (c *Client) fallbackToSilentMode(switcher *Switcher, err error) (ResultDetail, error) {
if !c.hasSilentMode() {
return ResultDetail{}, err
}

c.notifyError(err)
c.updateSilentToken()
return checkLocalCriteria(c.snapshotState(), switcher)
}

func (c *Client) hasSilentMode() bool {
return c.Context().Options.SilentMode > 0
}

func (c *Client) authState() (string, int64) {
c.authMu.Lock()
defer c.authMu.Unlock()

return c.authToken, c.authTokenExp
}

func (c *Client) updateSilentToken() {
c.authMu.Lock()
defer c.authMu.Unlock()

c.authToken = silentModeAuthToken
c.authTokenExp = time.Now().Add(c.Context().Options.SilentMode).UnixMilli()
}

func (c *Client) clearSilentToken() {
c.authMu.Lock()
defer c.authMu.Unlock()

c.authToken = ""
c.authTokenExp = 0
}
Loading
Loading