Skip to content

Commit

Permalink
feature/v1.1.1: update bootstrapping and offline mode (#5)
Browse files Browse the repository at this point in the history
  • Loading branch information
dsun0720 authored Apr 17, 2023
1 parent 0c90a1a commit 2e6d6ec
Show file tree
Hide file tree
Showing 7 changed files with 48 additions and 60 deletions.
32 changes: 12 additions & 20 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -81,35 +81,27 @@ The bootstrapping is in fact the call of constructor of `featbit.FBClient`, in w
streaming from your feature management platform.

The constructor will return when it successfully connects, or when the timeout set
by `featbit.FBConfig.StartWait`
(default: 15 seconds) expires, whichever comes first. If it has not succeeded in connecting when the timeout elapses,
by `featbit.FBConfig.StartWait`(default: 15 seconds) expires, whichever comes first. If it has not succeeded in connecting when the timeout elapses,
you will receive the client in an uninitialized state where feature flags will return default values; it will still
continue trying to connect in the background unless there has been an `net.DNSError` or you close the
client. You can detect whether initialization has succeeded by calling `featbit.FBClient.IsInitialized()`.
continue trying to connect in the background unless there has been an `net.DNSError` or you close the client.
You can detect whether initialization has succeeded by calling `featbit.FBClient.IsInitialized()`.

If `featbit.FBClient.IsInitialized()` returns True, it means the `featbit.FBClient` has succeeded at some point in connecting to feature flag center and
has received feature flag data.

If `featbit.FBClient.IsInitialized()` returns false, it means the client has not yet connected to feature flag center, or has permanently
failed. In this state, feature flag evaluations will always return default values. Another state that will cause this to return false is if the interfaces.DataStorage is empty.
It's strongly recommended to create at least one feature flag in your environment before using the SDK.

`featbit.FBClient.IsInitialized()` is optional, but it is recommended that you use it to avoid to get default values when the SDK is not yet initialized.
If `featbit.FBClient.IsInitialized()` returns True, it means the `featbit.FBClient` has succeeded at some point in connecting to feature flag center,
otherwise client has not yet connected to feature flag center, or has permanently failed. In this state, feature flag evaluations will always return default values.

```go
config := featbit.FBConfig{StartWait: 10 * time.Second}
// DO NOT forget to close the client when you don't need it anymore
client, err := featbit.MakeCustomFBClient(envSecret, streamingUrl, eventUrl, config)
if err == nil && client.IsInitialized() {
// the client is ready
} else {
// the client is not ready
}

```

If you prefer to have the constructor return immediately, and then wait for initialization to finish at some other
point, you can use `featbit.FBClient.GetDataUpdateStatusProvider()`, which provides an asynchronous way, as follows:
point, you can use `featbit.FBClient.GetDataUpdateStatusProvider()`, which will return an implementation of `interfaces.DataUpdateStatusProvider`.
This interface has a `WaitForOKState` method that will block until the client has successfully connected, or until the timeout expires.

```go
config := featbit.FBConfig{StartWait: 0}
Expand All @@ -121,10 +113,10 @@ if err != nil {
ok := client.GetDataSourceStatusProvider().WaitForOKState(10 * time.Second)
if ok {
// the client is ready
} else {
// the client is not ready
}
```
> To check if the client is ready is optional. Even if the client is not ready, you can still evaluate feature flags, but the default value will be returned if SDK is not yet initialized.

### FBConfig and Components

Expand Down Expand Up @@ -206,9 +198,6 @@ in real time, as mentioned in [Bootstrapping](#bootstrapping).
After initialization, the SDK has all the feature flags in the memory and all evaluation is done _**locally and
synchronously**_, the average evaluation time is < _**10**_ ms.

If evaluation called before Go SDK client initialized, or you set the wrong flag key or user for the evaluation, SDK will return
the default value you set.

SDK supports String, Boolean, and Number and Json as the return type of flag values:

- Variation(for string)
Expand Down Expand Up @@ -246,6 +235,9 @@ if client.isInitialized() {
}
```

> Note that if evaluation called before Go SDK client initialized, you set the wrong flag key/user for the evaluation or the related feature flag
is not found, SDK will return the default value you set. `interfaces.EvalDetail` will explain the details of the latest evaluation including error raison.

### Offline Mode

In some situations, you might want to stop making remote calls to FeatBit. Here is how:
Expand Down
4 changes: 2 additions & 2 deletions factories/streaming_builder.go
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,6 @@ func ExternalDataSynchronization() DataSynchronizerFactory {
return &nullDataSynchronizerBuilder{}
}

func (n *nullDataSynchronizerBuilder) CreateDataSynchronizer(Context, DataUpdater) (DataSynchronizer, error) {
return datasynchronization.NewNullDataSynchronizer(), nil
func (n *nullDataSynchronizerBuilder) CreateDataSynchronizer(_ Context, dataUpdater DataUpdater) (DataSynchronizer, error) {
return datasynchronization.NewNullDataSynchronizer(dataUpdater), nil
}
31 changes: 15 additions & 16 deletions fbclient.go
Original file line number Diff line number Diff line change
Expand Up @@ -124,6 +124,8 @@ func MakeCustomFBClient(envSecret string, streamingUrl string, eventUrl string,
} else if !util.IsUrl(streamingUrl) || !util.IsUrl(eventUrl) {
return nil, hostInvalid
}
} else {
log.LogInfo("FB GO SDK: SDK is in offline mode")
}
networkFactory := config.NetworkFactory
if networkFactory == nil {
Expand All @@ -133,13 +135,13 @@ func MakeCustomFBClient(envSecret string, streamingUrl string, eventUrl string,
if err != nil {
return nil, err
}
client := &FBClient{offline: config.Offline}
// init components
// data storage
dataStorageFactory := config.DataStorageFactory
if dataStorageFactory == nil {
dataStorageFactory = factories.NewInMemoryStorageBuilder()
}
client := &FBClient{offline: config.Offline}
// init components
// data storage
client.dataStorage, err = dataStorageFactory.CreateDataStorage(ctx)
if err != nil {
return nil, err
Expand Down Expand Up @@ -199,20 +201,18 @@ func MakeCustomFBClient(envSecret string, streamingUrl string, eventUrl string,
}
ready := client.dataSynchronizer.Start()
if config.StartWait > 0 {
if client.dataSynchronizer != datasynchronization.NewNullDataSynchronizer() {
if _, ok := client.dataSynchronizer.(*datasynchronization.NullDataSynchronizer); !ok {
log.LogInfo("FB GO SDK: waiting for Client initialization in %d milliseconds", config.StartWait/time.Millisecond)
}
if !client.dataUpdater.StorageInitialized() && !config.Offline {
log.LogWarn("FB GO SDK: SDK just returns default variation because of no data found in the given environment")
}
select {
case <-ready:
if !client.dataSynchronizer.IsInitialized() && !config.Offline {
if !client.dataSynchronizer.IsInitialized() {
log.LogWarn("FB GO SDK: SDK was not successfully initialized")
return client, initializationFailed
}
if !client.dataUpdater.StorageInitialized() && !config.Offline {
log.LogWarn("FB GO SDK: SDK was not completely initialized because of no data found in your environment")
return client, nil
}
log.LogInfo("FB GO SDK: SDK initialization is completed")
return client, nil
case <-time.After(config.StartWait):
log.LogWarn("FB GO SDK: timeout encountered when waiting for data update")
Expand All @@ -228,22 +228,21 @@ func MakeCustomFBClient(envSecret string, streamingUrl string, eventUrl string,
}

// IsInitialized tests whether the client is ready to be used.
// return true if the client is ready, or false if it is still initializing/interfaces.DataStorage is empty.
// return true if the client is ready, or false if it is still initializing.
//
// If this value is true, it means the FBClient has succeeded at some point in connecting to feature flag center and
// has received feature flag data. It could still have encountered a connection problem after that point, so
// this does not guarantee that the flags are up-to-date; if you need to know its status in more Detail, use FBClient.GetDataUpdateStatusProvider.
//
// If this value is false, it means the client has not yet connected to feature flag center, or has permanently
// failed. In this state, feature flag evaluations will always return default values. You can use FBClient.GetDataUpdateStatusProvider
// to get information on errors, or to wait for a successful retry.
// Another state that will cause this to return false is if the interfaces.DataStorage is empty. It's strongly recommended to create at least one feature flag in your environment
// before using the SDK.
// to get current status of the client.

func (client *FBClient) IsInitialized() bool {
if client.dataSynchronizer == nil || client.dataUpdater == nil {
if client.dataSynchronizer == nil {
return false
}
return client.dataSynchronizer.IsInitialized() && client.dataUpdater.StorageInitialized()
return client.dataSynchronizer.IsInitialized()
}

// Close shuts down the FBClient. After calling this, the FBClient should no longer be used.
Expand Down
6 changes: 3 additions & 3 deletions fbclient_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -85,10 +85,10 @@ func TestFBClientBootStrap(t *testing.T) {
}
client, err := MakeCustomFBClient(fakeEnvSecret, "ws://fake-url", "http://fake-url", config)
require.NoError(t, err)
assert.False(t, client.IsInitialized())
assert.True(t, client.IsInitialized())
res, detail, err := client.Variation("ff-test-string", testUser1, "error")
assert.Equal(t, err, clientNotInitialized)
assert.Equal(t, ReasonClientNotReady, detail.Reason)
assert.Equal(t, err, flagNotFound)
assert.Equal(t, ReasonFlagNotFound, detail.Reason)
assert.Equal(t, "error", res)
_ = client.Close()
})
Expand Down
8 changes: 4 additions & 4 deletions internal/datasynchronization/mock_streaming.go
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ type MockStreaming struct {
success bool
loadData bool
waitTime time.Duration
realDataUpdator DataUpdater
realDataUpdater DataUpdater
initialized bool
}

Expand All @@ -34,8 +34,8 @@ func (m *MockStreaming) Start() <-chan struct{} {
jsonBytes, _ := fixtures.LoadFBClientTestData()
var all data.All
_ = json.Unmarshal(jsonBytes, &all)
m.realDataUpdator.Init(all.Data.ToStorageType(), all.Data.GetTimestamp())
m.realDataUpdator.UpdateStatus(OKState())
m.realDataUpdater.Init(all.Data.ToStorageType(), all.Data.GetTimestamp())
m.realDataUpdater.UpdateStatus(OKState())
}
}
close(ret)
Expand All @@ -57,5 +57,5 @@ func (m *MockStreamingBuilder) CreateDataSynchronizer(_ Context, dataUpdater Dat
if m.waitTime <= 0 {
m.waitTime = 100 * time.Millisecond
}
return &MockStreaming{success: m.success, loadData: m.loadData, waitTime: m.waitTime, realDataUpdator: dataUpdater}, nil
return &MockStreaming{success: m.success, loadData: m.loadData, waitTime: m.waitTime, realDataUpdater: dataUpdater}, nil
}
19 changes: 9 additions & 10 deletions internal/datasynchronization/offline_data_synchronizer.go
Original file line number Diff line number Diff line change
@@ -1,17 +1,15 @@
package datasynchronization

import "sync"
import (
. "github.com/featbit/featbit-go-sdk/interfaces"
)

type NullDataSynchronizer struct{}

var instance *NullDataSynchronizer
var once sync.Once
type NullDataSynchronizer struct {
realDataUpdater DataUpdater
}

func NewNullDataSynchronizer() *NullDataSynchronizer {
once.Do(func() {
instance = &NullDataSynchronizer{}
})
return instance
func NewNullDataSynchronizer(dataUpdater DataUpdater) *NullDataSynchronizer {
return &NullDataSynchronizer{realDataUpdater: dataUpdater}
}

func (n *NullDataSynchronizer) Close() error {
Expand All @@ -25,5 +23,6 @@ func (n *NullDataSynchronizer) IsInitialized() bool {
func (n *NullDataSynchronizer) Start() <-chan struct{} {
ready := make(chan struct{})
close(ready)
n.realDataUpdater.UpdateStatus(OKState())
return ready
}
8 changes: 3 additions & 5 deletions internal/datasynchronization/streaming.go
Original file line number Diff line number Diff line change
Expand Up @@ -260,11 +260,9 @@ func (s *Streaming) onDataProcess(allData *data.All) bool {
s.initialized = true
close(s.readyCh)
})
// if data storage is not yet initialized, we should keep the status as INITIALIZING
if s.dataUpdater.StorageInitialized() {
log.LogDebug("processing data is well done")
s.dataUpdater.UpdateStatus(OKState())
}
log.LogDebug("processing data is well done")
s.dataUpdater.UpdateStatus(OKState())

}
return success
}
Expand Down

0 comments on commit 2e6d6ec

Please sign in to comment.