Skip to content
Open
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
14 changes: 10 additions & 4 deletions client.go
Original file line number Diff line number Diff line change
Expand Up @@ -548,11 +548,17 @@ func (uc *Client) Warnings() <-chan error {
return uc.warnings
}

// Ready returns the ready channel for the client. A value will be available on
// the channel when the feature toggles have been loaded from the Unleash
// server.
// Ready returns a channel that will receive `true` when the client has loaded
// feature toggles from the Unleash server. It returns a fresh channel per call
// and will deliver immediately if the client is already ready.
func (uc *Client) Ready() <-chan bool {
return uc.ready
ch := make(chan bool, 1)
go func() {
<-uc.onReady
ch <- true
close(ch)
}()
return ch
}

// Count returns the count channel which gives an update when a toggle has been queried.
Expand Down
83 changes: 83 additions & 0 deletions client_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -1496,3 +1496,86 @@ func TestConnectionAndIntervalHeadersAndBody(t *testing.T) {

assert.True(gock.IsDone(), "there should be no more mocks")
}

func TestReady(t *testing.T) {
// waitReadyChannelWithTimeout is a safety net for waiting on the ready channel to send the ready state.
// In the worst case, if the channel never sends anything after this subscribe, this function will time out and fail the test.
waitReadyChannelWithTimeout := func(t *testing.T, ready <-chan bool) (bool, bool) {
t.Helper()
select {
case actual, ok := <-ready:
return actual, ok
case <-time.After(100 * time.Millisecond):
t.Error("timeout waiting for unleash client to be ready after 100ms")
}
return false, false
}

t.Run("call ready", func(t *testing.T) {
assert := assert.New(t)
defer gock.OffAll()

gock.New(mockerServer).
Get("/client/features").
Reply(200).
JSON(api.FeatureResponse{})

mockListener := &MockedListener{}
mockListener.On("OnRegistered", mock.AnythingOfType("ClientData"))
mockListener.On("OnReady").Return()

client, err := NewClient(
WithUrl(mockerServer),
WithAppName(mockAppName),
WithInstanceId(mockInstanceId),
WithListener(mockListener),
WithDisableMetrics(true),
)
assert.NoError(err)

ready, ok := waitReadyChannelWithTimeout(t, client.Ready())
assert.True(ready, "the ready should be true when client has synced the feature flags")
assert.True(ok)

err = client.Close()

assert.True(gock.IsDone(), "there should be no more mocks")
})

t.Run("call Ready again after client is ready", func(t *testing.T) {
assert := assert.New(t)
defer gock.OffAll()

gock.New(mockerServer).
Get("/client/features").
Reply(200).
JSON(api.FeatureResponse{})

mockListener := &MockedListener{}
mockListener.On("OnRegistered", mock.AnythingOfType("ClientData"))
mockListener.On("OnReady").Return()

client, err := NewClient(
WithUrl(mockerServer),
WithAppName(mockAppName),
WithInstanceId(mockInstanceId),
WithListener(mockListener),
WithDisableMetrics(true),
)
assert.NoError(err)

ready, ok := waitReadyChannelWithTimeout(t, client.Ready())
assert.True(ready, "the ready should be true when client has synced the feature flags")
assert.True(ok)

// Issue since v5.0.3 and below.
// If client is ready after client.Ready is called, The ready channel return nothing and it will be stuck
ready, ok = waitReadyChannelWithTimeout(t, client.Ready())
assert.True(ready, "the ready should remain true even if the ready channel has already broadcast the ready state.")
assert.True(ok)

err = client.Close()

assert.True(gock.IsDone(), "there should be no more mocks")
})
}
Loading