Skip to content

Commit a614319

Browse files
Remove panics; replace with error (FF-2512) (#54)
* Remove panics; replace with error (FF-2512) * Remove IsAuthorized * use new constructor newConfigurationStore in test * 5.0.0 * rename promotedAttributeValue
1 parent 5e3cfea commit a614319

13 files changed

+157
-125
lines changed

README.md

Lines changed: 38 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -43,27 +43,29 @@ import (
4343
"github.com/Eppo-exp/golang-sdk/v4/eppoclient"
4444
)
4545

46-
var eppoClient = &eppoclient.EppoClient{}
46+
var eppoClient *eppoclient.EppoClient
4747

4848
func main() {
4949
assignmentLogger := NewExampleAssignmentLogger()
5050

51-
eppoClient = eppoclient.InitClient(eppoclient.Config{
51+
eppoClient, err = eppoclient.InitClient(eppoclient.Config{
5252
SdkKey: "<your_sdk_key>",
5353
AssignmentLogger: assignmentLogger,
5454
})
55+
if err != nil {
56+
log.Fatalf("Failed to initialize Eppo client: %v", err)
57+
}
5558
}
5659
```
5760

58-
5961
#### Assign anywhere
6062

6163
```go
6264
import (
6365
"github.com/Eppo-exp/golang-sdk/v4/eppoclient"
6466
)
6567

66-
var eppoClient = &eppoclient.EppoClient{}
68+
var eppoClient *eppoclient.EppoClient
6769

6870
variation := eppoClient.GetStringAssignment(
6971
"new-user-onboarding",
@@ -131,6 +133,38 @@ func main() {
131133
}
132134
```
133135

136+
### De-duplication of assignments
137+
138+
The SDK may see many duplicate assignments in a short period of time, and if you
139+
have configured a logging function, they will be transmitted to your downstream
140+
event store. This increases the cost of storage as well as warehouse costs during experiment analysis.
141+
142+
To mitigate this, an in-memory assignment cache is optionally available with expiration based on the least recently accessed time.
143+
144+
It can be configured with a maximum size to fit your desired memory allocation.
145+
146+
```go
147+
import (
148+
"github.com/Eppo-exp/golang-sdk/v4/eppoclient"
149+
)
150+
151+
var eppoClient *eppoclient.EppoClient
152+
153+
func main() {
154+
assignmentLogger := NewExampleAssignmentLogger()
155+
156+
eppoClient = eppoclient.InitClient(eppoclient.Config{
157+
ApiKey: "<your_sdk_key>",
158+
// 10000 is the maximum number of assignments to cache
159+
// Depending on the length of your flag and subject keys, taking a median
160+
// length of 32 characters, each assignment cache entry uses approximately 112 bytes.
161+
// Use this calculation to determine the maximum number of assignments to cache
162+
// for the memory you wish to allocate.
163+
AssignmentLogger: eppoclient.NewLruAssignmentLogger(assignmentLogger, 10000),
164+
})
165+
}
166+
```
167+
134168
## Philosophy
135169

136170
Eppo's SDKs are built for simplicity, speed and reliability. Flag configurations are compressed and distributed over a global CDN (Fastly), typically reaching your servers in under 15ms. Server SDKs continue polling Eppo’s API at 10-second intervals. Configurations are then cached locally, ensuring that each assignment is made instantly. Evaluation logic within each SDK consists of a few lines of simple numeric and string comparisons. The typed functions listed above are all developers need to understand, abstracting away the complexity of the Eppo's underlying (and expanding) feature set.

eppoclient/client.go

Lines changed: 2 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -174,15 +174,11 @@ func (ec *EppoClient) GetBanditAction(flagKey string, subjectKey string, subject
174174

175175
func (ec *EppoClient) getAssignment(config configuration, flagKey string, subjectKey string, subjectAttributes Attributes, variationType variationType) (interface{}, error) {
176176
if subjectKey == "" {
177-
panic("no subject key provided")
177+
return nil, fmt.Errorf("no subject key provided")
178178
}
179179

180180
if flagKey == "" {
181-
panic("no flag key provided")
182-
}
183-
184-
if ec.configRequestor != nil && !ec.configRequestor.IsAuthorized() {
185-
panic("Unauthorized: please check your SDK key")
181+
return nil, fmt.Errorf("no flag key provided")
186182
}
187183

188184
flag, err := config.getFlagConfiguration(flagKey)

eppoclient/client_test.go

Lines changed: 6 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,6 @@
11
package eppoclient
22

33
import (
4-
"log"
54
"testing"
65

76
"github.com/stretchr/testify/assert"
@@ -10,26 +9,18 @@ import (
109

1110
func Test_AssignBlankExperiment(t *testing.T) {
1211
var mockLogger = new(mockLogger)
13-
client := newEppoClient(&configurationStore{}, nil, nil, mockLogger)
12+
client := newEppoClient(newConfigurationStore(configuration{}), nil, nil, mockLogger)
1413

15-
assert.Panics(t, func() {
16-
_, err := client.GetStringAssignment("", "subject-1", Attributes{}, "")
17-
if err != nil {
18-
log.Println(err)
19-
}
20-
})
14+
_, err := client.GetStringAssignment("", "subject-1", Attributes{}, "")
15+
assert.Error(t, err)
2116
}
2217

2318
func Test_AssignBlankSubject(t *testing.T) {
2419
var mockLogger = new(mockLogger)
25-
client := newEppoClient(&configurationStore{}, nil, nil, mockLogger)
20+
client := newEppoClient(newConfigurationStore(configuration{}), nil, nil, mockLogger)
2621

27-
assert.Panics(t, func() {
28-
_, err := client.GetStringAssignment("experiment-1", "", Attributes{}, "")
29-
if err != nil {
30-
log.Println(err)
31-
}
32-
})
22+
_, err := client.GetStringAssignment("experiment-1", "", Attributes{}, "")
23+
assert.Error(t, err)
3324
}
3425
func Test_LogAssignment(t *testing.T) {
3526
var mockLogger = new(mockLogger)

eppoclient/config.go

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,9 @@
11
package eppoclient
22

3-
import "time"
3+
import (
4+
"fmt"
5+
"time"
6+
)
47

58
const default_base_url = "https://fscdn.eppo.cloud/api"
69

@@ -11,9 +14,9 @@ type Config struct {
1114
PollerInterval time.Duration
1215
}
1316

14-
func (cfg *Config) validate() {
17+
func (cfg *Config) validate() error {
1518
if cfg.SdkKey == "" {
16-
panic("sdk key not set")
19+
return fmt.Errorf("SDK key not set")
1720
}
1821

1922
if cfg.BaseUrl == "" {
@@ -23,4 +26,6 @@ func (cfg *Config) validate() {
2326
if cfg.PollerInterval <= 0 {
2427
cfg.PollerInterval = 10 * time.Second
2528
}
29+
30+
return nil
2631
}

eppoclient/config_test.go

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@ func Test_config_defaultPollerInterval(t *testing.T) {
1212
SdkKey: "blah",
1313
}
1414

15-
cfg.validate()
16-
15+
err := cfg.validate()
16+
assert.NoError(t, err)
1717
assert.Equal(t, 10*time.Second, cfg.PollerInterval)
1818
}

eppoclient/configurationrequestor.go

Lines changed: 0 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -20,10 +20,6 @@ func newConfigurationRequestor(httpClient httpClient, configStore *configuration
2020
}
2121
}
2222

23-
func (cr *configurationRequestor) IsAuthorized() bool {
24-
return !cr.httpClient.isUnauthorized
25-
}
26-
2723
func (cr *configurationRequestor) FetchAndStoreConfigurations() {
2824
configuration, err := cr.fetchConfiguration()
2925
if err != nil {

eppoclient/doc.go

Lines changed: 0 additions & 29 deletions
This file was deleted.

eppoclient/eppoclient_e2e_test.go

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -36,7 +36,8 @@ func Test_e2e(t *testing.T) {
3636

3737
mockLogger := new(mockLogger)
3838
mockLogger.Mock.On("LogAssignment", mock.Anything).Return()
39-
client := InitClient(Config{BaseUrl: serverUrl, SdkKey: "dummy", AssignmentLogger: mockLogger})
39+
client, err := InitClient(Config{BaseUrl: serverUrl, SdkKey: "dummy", AssignmentLogger: mockLogger})
40+
assert.NoError(t, err)
4041

4142
// give client the time to "fetch" the mock config
4243
time.Sleep(2 * time.Second)

eppoclient/initclient.go

Lines changed: 7 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -4,12 +4,15 @@ package eppoclient
44

55
import "net/http"
66

7-
var __version__ = "4.1.0"
7+
var __version__ = "5.0.0"
88

99
// InitClient is required to start polling of experiments configurations and create
1010
// an instance of EppoClient, which could be used to get assignments information.
11-
func InitClient(config Config) *EppoClient {
12-
config.validate()
11+
func InitClient(config Config) (*EppoClient, error) {
12+
err := config.validate()
13+
if err != nil {
14+
return nil, err
15+
}
1316
sdkParams := SDKParams{sdkKey: config.SdkKey, sdkName: "go", sdkVersion: __version__}
1417

1518
httpClient := newHttpClient(config.BaseUrl, &http.Client{Timeout: REQUEST_TIMEOUT_SECONDS}, sdkParams)
@@ -23,5 +26,5 @@ func InitClient(config Config) *EppoClient {
2326

2427
client.poller.Start()
2528

26-
return client
29+
return client, nil
2730
}

eppoclient/lruassignmentlogger.go

Lines changed: 10 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,9 @@
11
package eppoclient
22

33
import (
4-
"github.com/hashicorp/golang-lru/v2"
4+
"fmt"
5+
6+
lru "github.com/hashicorp/golang-lru/v2"
57
)
68

79
type LruAssignmentLogger struct {
@@ -22,20 +24,20 @@ type cacheValue struct {
2224
variation string
2325
}
2426

25-
func NewLruAssignmentLogger(logger IAssignmentLogger, cacheSize int) IAssignmentLogger {
27+
func NewLruAssignmentLogger(logger IAssignmentLogger, cacheSize int) (IAssignmentLogger, error) {
2628
cache, err := lru.New2Q[cacheKey, cacheValue](cacheSize)
2729
if err != nil {
2830
// err is only returned if `cacheSize` is invalid
2931
// (e.g., <0) which should normally never happen.
30-
panic(err)
32+
return nil, fmt.Errorf("failed to create LRU cache: %w", err)
3133
}
3234
return &LruAssignmentLogger{
3335
cache: cache,
3436
inner: logger,
35-
}
37+
}, nil
3638
}
3739

38-
func (self *LruAssignmentLogger) LogAssignment(event AssignmentEvent) {
40+
func (lal *LruAssignmentLogger) LogAssignment(event AssignmentEvent) {
3941
key := cacheKey{
4042
flag: event.FeatureFlag,
4143
subject: event.Subject,
@@ -44,11 +46,11 @@ func (self *LruAssignmentLogger) LogAssignment(event AssignmentEvent) {
4446
allocation: event.Allocation,
4547
variation: event.Variation,
4648
}
47-
previousValue, recentlyLogged := self.cache.Get(key)
49+
previousValue, recentlyLogged := lal.cache.Get(key)
4850
if !recentlyLogged || previousValue != value {
49-
self.inner.LogAssignment(event)
51+
lal.inner.LogAssignment(event)
5052
// Adding to cache after `LogAssignment` returned in
5153
// case it panics.
52-
self.cache.Add(key, value)
54+
lal.cache.Add(key, value)
5355
}
5456
}

0 commit comments

Comments
 (0)