Skip to content

Commit 7a141bd

Browse files
committed
FFM-2096 Add Interface for hooking into SSE Events#
1 parent c47b55f commit 7a141bd

File tree

5 files changed

+95
-23
lines changed

5 files changed

+95
-23
lines changed

client/client.go

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -184,12 +184,12 @@ func (c *CfClient) streamConnect() {
184184
defer c.mux.RUnlock()
185185
c.streamConnected = false
186186
}
187-
conn := stream.NewSSEClient(c.sdkKey, c.token, sseClient, c.config.Cache, c.api, c.config.Logger, streamErr)
187+
conn := stream.NewSSEClient(c.sdkKey, c.token, sseClient, c.config.Cache, c.api, c.config.Logger, streamErr, c.config.eventStreamListener)
188188

189189
// Connect kicks off a goroutine that attempts to establish a stream connection
190190
// while this is happening we set streamConnected to true - if any errors happen
191191
// in this process streamConnected will be set back to false by the streamErr function
192-
conn.Connect(c.environmentID)
192+
conn.Connect(c.environmentID, c.sdkKey)
193193
c.streamConnected = true
194194
}
195195

client/config.go

Lines changed: 12 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import (
55
"net/http"
66

77
"github.com/harness/ff-golang-server-sdk/evaluation"
8+
"github.com/harness/ff-golang-server-sdk/stream"
89

910
"github.com/harness/ff-golang-server-sdk/cache"
1011
"github.com/harness/ff-golang-server-sdk/logger"
@@ -13,16 +14,17 @@ import (
1314
)
1415

1516
type config struct {
16-
url string
17-
eventsURL string
18-
pullInterval uint // in minutes
19-
Cache cache.Cache
20-
Store storage.Storage
21-
Logger logger.Logger
22-
httpClient *http.Client
23-
enableStream bool
24-
enableStore bool
25-
target evaluation.Target
17+
url string
18+
eventsURL string
19+
pullInterval uint // in minutes
20+
Cache cache.Cache
21+
Store storage.Storage
22+
Logger logger.Logger
23+
httpClient *http.Client
24+
enableStream bool
25+
enableStore bool
26+
target evaluation.Target
27+
eventStreamListener stream.EventStreamListener
2628
}
2729

2830
func newDefaultConfig() *config {

client/options.go

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import (
77
"github.com/harness/ff-golang-server-sdk/evaluation"
88
"github.com/harness/ff-golang-server-sdk/logger"
99
"github.com/harness/ff-golang-server-sdk/storage"
10+
"github.com/harness/ff-golang-server-sdk/stream"
1011
)
1112

1213
// ConfigOption is used as return value for advanced client configuration
@@ -88,3 +89,11 @@ func WithTarget(target evaluation.Target) ConfigOption {
8889
config.target = target
8990
}
9091
}
92+
93+
// WithEventStreamListener configures the SDK to forward Events from the Feature
94+
// Flag server to the passed EventStreamListener
95+
func WithEventStreamListener(cs stream.EventStreamListener) ConfigOption {
96+
return func(config *config) {
97+
config.eventStreamListener = cs
98+
}
99+
}

stream/sse.go

Lines changed: 49 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ package stream
33
import (
44
"context"
55
"fmt"
6+
"sync"
67
"time"
78

89
"github.com/harness/ff-golang-server-sdk/cache"
@@ -17,11 +18,12 @@ import (
1718

1819
// SSEClient is Server Send Event object
1920
type SSEClient struct {
20-
api rest.ClientWithResponsesInterface
21-
client *sse.Client
22-
cache cache.Cache
23-
logger logger.Logger
24-
onStreamError func()
21+
api rest.ClientWithResponsesInterface
22+
client *sse.Client
23+
cache cache.Cache
24+
logger logger.Logger
25+
onStreamError func()
26+
eventStreamListener EventStreamListener
2527
}
2628

2729
var json = jsoniter.ConfigCompatibleWithStandardLibrary
@@ -35,24 +37,26 @@ func NewSSEClient(
3537
api rest.ClientWithResponsesInterface,
3638
logger logger.Logger,
3739
onStreamError func(),
40+
eventStreamListener EventStreamListener,
3841
) *SSEClient {
3942
client.Headers["Authorization"] = fmt.Sprintf("Bearer %s", token)
4043
client.Headers["API-Key"] = apiKey
4144
client.OnDisconnect(func(client *sse.Client) {
4245
onStreamError()
4346
})
4447
sseClient := &SSEClient{
45-
client: client,
46-
cache: cache,
47-
api: api,
48-
logger: logger,
49-
onStreamError: onStreamError,
48+
client: client,
49+
cache: cache,
50+
api: api,
51+
logger: logger,
52+
onStreamError: onStreamError,
53+
eventStreamListener: eventStreamListener,
5054
}
5155
return sseClient
5256
}
5357

5458
// Connect will subscribe to SSE stream
55-
func (c *SSEClient) Connect(environment string) {
59+
func (c *SSEClient) Connect(environment string, apiKey string) {
5660
c.logger.Infof("Start subscribing to Stream")
5761
// don't use the default exponentialBackoff strategy - we have our own disconnect logic
5862
// of polling the service then re-establishing a new stream once we can connect
@@ -62,6 +66,8 @@ func (c *SSEClient) Connect(environment string) {
6266
err := c.client.Subscribe("*", func(msg *sse.Event) {
6367
c.logger.Infof("Event received: %s", msg.Data)
6468

69+
wg := &sync.WaitGroup{}
70+
6571
cfMsg := Message{}
6672
if len(msg.Data) > 0 {
6773
err := json.Unmarshal(msg.Data, &cfMsg)
@@ -76,16 +82,25 @@ func (c *SSEClient) Connect(environment string) {
7682
// and subscribe to that event
7783
switch cfMsg.Event {
7884
case dto.SseDeleteEvent:
85+
wg.Add(1)
86+
7987
go func(identifier string) {
88+
defer wg.Done()
89+
8090
c.cache.Remove(dto.Key{
8191
Type: dto.KeyFeature,
8292
Name: identifier,
8393
})
8494
}(cfMsg.Identifier)
95+
8596
case dto.SsePatchEvent, dto.SseCreateEvent:
8697
fallthrough
8798
default:
99+
wg.Add(1)
100+
88101
go func(env, identifier string) {
102+
defer wg.Done()
103+
89104
ctx, cancel := context.WithTimeout(context.Background(), time.Second*60)
90105
defer cancel()
91106
response, err := c.api.GetFeatureConfigByIdentifierWithResponse(ctx, env, identifier)
@@ -101,20 +116,30 @@ func (c *SSEClient) Connect(environment string) {
101116
}
102117
}(environment, cfMsg.Identifier)
103118
}
119+
104120
case dto.KeySegment:
105121
// need open client spec change
106122
switch cfMsg.Event {
107123
case dto.SseDeleteEvent:
124+
wg.Add(1)
125+
108126
go func(identifier string) {
127+
defer wg.Done()
128+
109129
c.cache.Remove(dto.Key{
110130
Type: dto.KeySegment,
111131
Name: identifier,
112132
})
113133
}(cfMsg.Identifier)
134+
114135
case dto.SsePatchEvent, dto.SseCreateEvent:
115136
fallthrough
116137
default:
138+
wg.Add(1)
139+
117140
go func(env, identifier string) {
141+
defer wg.Done()
142+
118143
ctx, cancel := context.WithTimeout(context.Background(), time.Second*60)
119144
defer cancel()
120145
response, err := c.api.GetSegmentByIdentifierWithResponse(ctx, env, identifier)
@@ -131,6 +156,19 @@ func (c *SSEClient) Connect(environment string) {
131156
}(environment, cfMsg.Identifier)
132157
}
133158
}
159+
160+
if c.eventStreamListener != nil {
161+
sendWithTimeout := func() error {
162+
ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
163+
defer cancel()
164+
return c.eventStreamListener.Pub(ctx, Event{APIKey: apiKey, Environment: environment, Event: msg})
165+
}
166+
167+
wg.Wait()
168+
if err := sendWithTimeout(); err != nil {
169+
c.logger.Errorf("error while forwarding SSE Event to change stream: %s", err)
170+
}
171+
}
134172
}
135173
})
136174
if err != nil {

stream/stream.go

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,30 @@
11
package stream
22

3+
import (
4+
"context"
5+
6+
"github.com/r3labs/sse"
7+
)
8+
39
// Connection is simple interface for streams
410
type Connection interface {
511
Connect(environment string) error
612
OnDisconnect(func() error) error
713
}
14+
15+
// EventStreamListener provides a way to hook in to the SSE Events that the SDK
16+
// recieves from the FeatureFlags server and forward them on to another type.
17+
type EventStreamListener interface {
18+
// Pub publishes an event from the SDK to your Listener
19+
Pub(ctx context.Context, event Event) error
20+
}
21+
22+
// Event defines the structure of an event that gets sent to a EventStreamListener
23+
type Event struct {
24+
// APIKey is the SDKs API Key
25+
APIKey string
26+
// Environment is the ID of the environment that the event occured for
27+
Environment string
28+
// Event is the SSEEvent that was sent from the FeatureFlags server to the SDK
29+
Event *sse.Event
30+
}

0 commit comments

Comments
 (0)