14
14
* limitations under the License. *
15
15
***************************************************************************/
16
16
17
- // Package decision provides CMAB client implementation
18
- package decision
17
+ // Package cmab provides contextual multi-armed bandit functionality
18
+ package cmab
19
19
20
20
import (
21
21
"bytes"
@@ -44,34 +44,34 @@ const (
44
44
DefaultBackoffMultiplier = 2.0
45
45
)
46
46
47
- // CMABAttribute represents an attribute in a CMAB request
48
- type CMABAttribute struct {
47
+ // Attribute represents an attribute in a CMAB request
48
+ type Attribute struct {
49
49
ID string `json:"id"`
50
50
Value interface {} `json:"value"`
51
51
Type string `json:"type"`
52
52
}
53
53
54
- // CMABInstance represents an instance in a CMAB request
55
- type CMABInstance struct {
56
- VisitorID string `json:"visitorId"`
57
- ExperimentID string `json:"experimentId"`
58
- Attributes []CMABAttribute `json:"attributes"`
59
- CmabUUID string `json:"cmabUUID"`
54
+ // Instance represents an instance in a CMAB request
55
+ type Instance struct {
56
+ VisitorID string `json:"visitorId"`
57
+ ExperimentID string `json:"experimentId"`
58
+ Attributes []Attribute `json:"attributes"`
59
+ CmabUUID string `json:"cmabUUID"`
60
60
}
61
61
62
- // CMABRequest represents a request to the CMAB API
63
- type CMABRequest struct {
64
- Instances []CMABInstance `json:"instances"`
62
+ // Request represents a request to the CMAB API
63
+ type Request struct {
64
+ Instances []Instance `json:"instances"`
65
65
}
66
66
67
- // CMABPrediction represents a prediction in a CMAB response
68
- type CMABPrediction struct {
67
+ // Prediction represents a prediction in a CMAB response
68
+ type Prediction struct {
69
69
VariationID string `json:"variation_id"`
70
70
}
71
71
72
- // CMABResponse represents a response from the CMAB API
73
- type CMABResponse struct {
74
- Predictions []CMABPrediction `json:"predictions"`
72
+ // Response represents a response from the CMAB API
73
+ type Response struct {
74
+ Predictions []Prediction `json:"predictions"`
75
75
}
76
76
77
77
// RetryConfig defines configuration for retry behavior
@@ -93,15 +93,15 @@ type DefaultCmabClient struct {
93
93
logger logging.OptimizelyLogProducer
94
94
}
95
95
96
- // CmabClientOptions defines options for creating a CMAB client
97
- type CmabClientOptions struct {
96
+ // ClientOptions defines options for creating a CMAB client
97
+ type ClientOptions struct {
98
98
HTTPClient * http.Client
99
99
RetryConfig * RetryConfig
100
100
Logger logging.OptimizelyLogProducer
101
101
}
102
102
103
103
// NewDefaultCmabClient creates a new instance of DefaultCmabClient
104
- func NewDefaultCmabClient (options CmabClientOptions ) * DefaultCmabClient {
104
+ func NewDefaultCmabClient (options ClientOptions ) * DefaultCmabClient {
105
105
httpClient := options .HTTPClient
106
106
if httpClient == nil {
107
107
httpClient = & http.Client {
@@ -127,33 +127,28 @@ func NewDefaultCmabClient(options CmabClientOptions) *DefaultCmabClient {
127
127
128
128
// FetchDecision fetches a decision from the CMAB API
129
129
func (c * DefaultCmabClient ) FetchDecision (
130
- ctx context.Context ,
131
130
ruleID string ,
132
131
userID string ,
133
132
attributes map [string ]interface {},
134
133
cmabUUID string ,
135
134
) (string , error ) {
136
- // If no context is provided, create a background context
137
- if ctx == nil {
138
- ctx = context .Background ()
139
- }
140
135
141
136
// Create the URL
142
137
url := fmt .Sprintf (CMABPredictionEndpoint , ruleID )
143
138
144
139
// Convert attributes to CMAB format
145
- cmabAttributes := make ([]CMABAttribute , 0 , len (attributes ))
140
+ cmabAttributes := make ([]Attribute , 0 , len (attributes ))
146
141
for key , value := range attributes {
147
- cmabAttributes = append (cmabAttributes , CMABAttribute {
142
+ cmabAttributes = append (cmabAttributes , Attribute {
148
143
ID : key ,
149
144
Value : value ,
150
145
Type : "custom_attribute" ,
151
146
})
152
147
}
153
148
154
149
// Create the request body
155
- requestBody := CMABRequest {
156
- Instances : []CMABInstance {
150
+ requestBody := Request {
151
+ Instances : []Instance {
157
152
{
158
153
VisitorID : userID ,
159
154
ExperimentID : ruleID ,
@@ -171,26 +166,26 @@ func (c *DefaultCmabClient) FetchDecision(
171
166
172
167
// If no retry config, just do a single fetch
173
168
if c .retryConfig == nil {
174
- return c .doFetch (ctx , url , bodyBytes )
169
+ return c .doFetch (context . Background () , url , bodyBytes )
175
170
}
176
171
177
172
// Retry sending request with exponential backoff
173
+ var lastErr error
178
174
for i := 0 ; i <= c .retryConfig .MaxRetries ; i ++ {
179
- // Check if context is done
180
- if ctx .Err () != nil {
181
- return "" , fmt .Errorf ("context canceled or timed out: %w" , ctx .Err ())
182
- }
183
-
184
175
// Make the request
185
- result , err := c .doFetch (ctx , url , bodyBytes )
176
+ result , err := c .doFetch (context . Background () , url , bodyBytes )
186
177
if err == nil {
187
178
return result , nil
188
179
}
189
180
190
- // If this is the last retry, return the error
191
- if i == c .retryConfig .MaxRetries {
192
- return "" , fmt .Errorf ("failed to fetch CMAB decision after %d attempts: %w" ,
193
- c .retryConfig .MaxRetries , err )
181
+ lastErr = err
182
+
183
+ // Don't wait after the last attempt
184
+ if i < c .retryConfig .MaxRetries {
185
+ backoffDuration := c .retryConfig .InitialBackoff * time .Duration (1 << i )
186
+
187
+ // Wait for backoff duration
188
+ time .Sleep (backoffDuration )
194
189
}
195
190
196
191
// Calculate backoff duration
@@ -204,8 +199,8 @@ func (c *DefaultCmabClient) FetchDecision(
204
199
205
200
// Wait for backoff duration with context awareness
206
201
select {
207
- case <- ctx .Done ():
208
- return "" , fmt .Errorf ("context canceled or timed out during backoff: %w" , ctx .Err ())
202
+ case <- context . Background () .Done ():
203
+ return "" , fmt .Errorf ("context canceled or timed out during backoff: %w" , context . Background () .Err ())
209
204
case <- time .After (backoffDuration ):
210
205
// Continue with retry
211
206
}
@@ -215,7 +210,7 @@ func (c *DefaultCmabClient) FetchDecision(
215
210
}
216
211
217
212
// This should never be reached due to the return in the loop above
218
- return "" , fmt .Errorf ("unexpected error in retry loop" )
213
+ return "" , fmt .Errorf ("failed to fetch CMAB decision after %d attempts: %w" , c . retryConfig . MaxRetries , lastErr )
219
214
}
220
215
221
216
// doFetch performs a single fetch operation to the CMAB API
@@ -248,7 +243,7 @@ func (c *DefaultCmabClient) doFetch(ctx context.Context, url string, bodyBytes [
248
243
}
249
244
250
245
// Parse response
251
- var cmabResponse CMABResponse
246
+ var cmabResponse Response
252
247
if err := json .Unmarshal (respBody , & cmabResponse ); err != nil {
253
248
return "" , fmt .Errorf ("failed to unmarshal CMAB response: %w" , err )
254
249
}
@@ -263,6 +258,6 @@ func (c *DefaultCmabClient) doFetch(ctx context.Context, url string, bodyBytes [
263
258
}
264
259
265
260
// validateResponse validates the CMAB response
266
- func (c * DefaultCmabClient ) validateResponse (response CMABResponse ) bool {
261
+ func (c * DefaultCmabClient ) validateResponse (response Response ) bool {
267
262
return len (response .Predictions ) > 0 && response .Predictions [0 ].VariationID != ""
268
263
}
0 commit comments