@@ -10,6 +10,7 @@ import (
1010 "net/url"
1111 "runtime"
1212 "sync"
13+ "sync/atomic"
1314 "time"
1415
1516 "github.com/Unleash/unleash-go-sdk/v5/internal/api"
@@ -87,20 +88,28 @@ type metric struct {
8788 Enabled bool
8889}
8990
91+ type toggleCounters struct {
92+ yes int64
93+ no int64
94+
95+ mu sync.Mutex
96+ variants map [string ]int64
97+ }
98+
9099type metrics struct {
91100 metricsChannels
92- options metricsOptions
93- started time.Time
94- bucketMu sync. Mutex
95- bucket api. Bucket
96- ticker * time.Ticker
97- close chan struct {}
98- closed chan struct {}
99- ctx context.Context
100- cancel func ()
101- maxSkips float64
102- errors float64
103- skips float64
101+ options metricsOptions
102+ started time.Time
103+ last_close_time time. Time
104+ counters sync. Map // map[string]*toggleCounters
105+ ticker * time.Ticker
106+ close chan struct {}
107+ closed chan struct {}
108+ ctx context.Context
109+ cancel func ()
110+ maxSkips float64
111+ errors float64
112+ skips float64
104113}
105114
106115func newMetrics (options metricsOptions , channels metricsChannels ) * metrics {
@@ -113,6 +122,7 @@ func newMetrics(options metricsOptions, channels metricsChannels) *metrics {
113122 maxSkips : 10 ,
114123 errors : 0 ,
115124 skips : 0 ,
125+ last_close_time : time .Now (),
116126 }
117127 ctx , cancel := context .WithCancel (context .Background ())
118128 m .ctx = ctx
@@ -122,7 +132,6 @@ func newMetrics(options metricsOptions, channels metricsChannels) *metrics {
122132 m .options .httpClient = http .DefaultClient
123133 }
124134
125- m .resetBucket ()
126135 if m .options .metricsInterval <= 0 {
127136 m .options .disableMetrics = true
128137 }
@@ -196,13 +205,70 @@ func (m *metrics) successfulPost() {
196205func (m * metrics ) decrementSkip () {
197206 m .skips = math .Max (0 , m .skips - 1 )
198207}
208+
209+ // This does not remove stale toggle names from the map. I don't think there's a safe, lock free way to do that
210+ // The consequence is that if the user archives a lot of toggles this internal representation will not lose those
211+ // toggles until the process is terminated. In practice, I don't believe this is a big problem, just means a
212+ // little bit more memory is held than necessary
213+ func (m * metrics ) buildBucketAndReset (last_close_time time.Time ) (api.Bucket , bool ) {
214+ bucket := api.Bucket {
215+ Start : last_close_time ,
216+ Toggles : make (map [string ]api.ToggleCount ),
217+ }
218+
219+ m .counters .Range (func (key , value any ) bool {
220+ name := key .(string )
221+ c := value .(* toggleCounters )
222+
223+ yes := atomic .SwapInt64 (& c .yes , 0 )
224+ no := atomic .SwapInt64 (& c .no , 0 )
225+
226+ if yes == 0 && no == 0 {
227+ c .mu .Lock ()
228+ emptyVariants := len (c .variants ) == 0
229+ c .mu .Unlock ()
230+ if emptyVariants {
231+ return true
232+ }
233+ }
234+
235+ tc := api.ToggleCount {
236+ Yes : int32 (yes ),
237+ No : int32 (no ),
238+ }
239+
240+ // we can have a little locking, as a treat. Variants are likely a luke warm path at best
241+ // until we have evidence that this is a hot path API, I'd like to keep this simple
242+ // simple here means a local lock per toggle counter while we swap out the variants map
243+ c .mu .Lock ()
244+ if len (c .variants ) > 0 {
245+ vars := make (map [string ]int32 , len (c .variants ))
246+ for vName , cnt := range c .variants {
247+ vars [vName ] = int32 (cnt )
248+ }
249+ tc .Variants = vars
250+
251+ c .variants = make (map [string ]int64 )
252+ }
253+ c .mu .Unlock ()
254+
255+ bucket .Toggles [name ] = tc
256+ return true
257+ })
258+
259+ if len (bucket .Toggles ) == 0 {
260+ return api.Bucket {}, false
261+ }
262+
263+ return bucket , true
264+ }
265+
199266func (m * metrics ) sendMetrics () {
200- m .bucketMu .Lock ()
201- bucket := m .resetBucket ()
202- m .bucketMu .Unlock ()
203- if bucket .IsEmpty () {
267+ bucket , ok := m .buildBucketAndReset (m .last_close_time )
268+ if ! ok {
204269 return
205270 }
271+ m .last_close_time = time .Now ()
206272 bucket .Stop = time .Now ()
207273 payload := MetricsData {
208274 AppName : m .options .appName ,
@@ -233,23 +299,19 @@ func (m *metrics) sendMetrics() {
233299 m .warn (fmt .Errorf ("%s return %d" , u .String (), resp .StatusCode ))
234300 // The post failed, re-add the metrics we attempted to send so
235301 // they are included in the next post.
236- for name , tc := range bucket .Toggles {
237- m .add (name , true , tc .Yes )
238- m .add (name , false , tc .No )
239- }
302+ m .reinsertBucket (bucket )
240303
241- m .bucketMu .Lock ()
242304 // Set the start time of the current bucket to the one we
243305 // attempted to send.
244- m .bucket . Start = bucket .Start
245- m . bucketMu . Unlock ()
306+ m .last_close_time = bucket .Start
307+
246308 } else {
247309 m .successfulPost ()
248310 m .sent <- payload
249311 }
250312}
251313
252- func (m * metrics ) doPost (url * url.URL , payload any ) (* http.Response , error ) {
314+ func (m * metrics ) doPost (url * url.URL , payload interface {} ) (* http.Response , error ) {
253315 var body bytes.Buffer
254316 enc := json .NewEncoder (& body )
255317 if err := enc .Encode (payload ); err != nil {
@@ -274,24 +336,57 @@ func (m *metrics) doPost(url *url.URL, payload any) (*http.Response, error) {
274336 return m .options .httpClient .Do (req )
275337}
276338
339+ func (m * metrics ) getOrCreateCounter (name string ) * toggleCounters {
340+ c , ok := m .counters .Load (name )
341+ if ok {
342+ return c .(* toggleCounters )
343+ }
344+
345+ nc := & toggleCounters {
346+ variants : make (map [string ]int64 ),
347+ }
348+ actual , _ := m .counters .LoadOrStore (name , nc )
349+ return actual .(* toggleCounters )
350+ }
351+
352+ func (m * metrics ) reinsertBucket (bucket api.Bucket ) {
353+ for name , tc := range bucket .Toggles {
354+ c := m .getOrCreateCounter (name )
355+ if tc .Yes != 0 {
356+ atomic .AddInt64 (& c .yes , int64 (tc .Yes ))
357+ }
358+ if tc .No != 0 {
359+ atomic .AddInt64 (& c .no , int64 (tc .No ))
360+ }
361+
362+ if len (tc .Variants ) > 0 {
363+ c := m .getOrCreateCounter (name )
364+
365+ c .mu .Lock ()
366+ if c .variants == nil {
367+ c .variants = make (map [string ]int64 , len (tc .Variants ))
368+ }
369+ for vName , cnt := range tc .Variants {
370+ if cnt == 0 {
371+ continue
372+ }
373+ c .variants [vName ] += int64 (cnt )
374+ }
375+ c .mu .Unlock ()
376+ }
377+ }
378+ }
379+
277380func (m * metrics ) add (name string , enabled bool , num int32 ) {
278381 if m .options .disableMetrics || num == 0 {
279382 return
280383 }
281- m .bucketMu .Lock ()
282- defer m .bucketMu .Unlock ()
283- t , exists := m .bucket .Toggles [name ]
284- if ! exists {
285- t = api.ToggleCount {
286- Variants : map [string ]int32 {},
287- }
288- }
384+ c := m .getOrCreateCounter (name )
289385 if enabled {
290- t . Yes += num
386+ atomic . AddInt64 ( & c . yes , int64 ( num ))
291387 } else {
292- t . No += num
388+ atomic . AddInt64 ( & c . no , int64 ( num ))
293389 }
294- m .bucket .Toggles [name ] = t
295390}
296391
297392func (m * metrics ) count (name string , enabled bool ) {
@@ -310,29 +405,14 @@ func (m *metrics) countVariants(name string, enabled bool, variantName string) {
310405 m .add (name , enabled , 1 )
311406 m .metricsChannels .count <- metric {Name : name , Enabled : enabled }
312407
313- m .bucketMu .Lock ()
314- defer m .bucketMu .Unlock ()
315-
316- t := m .bucket .Toggles [name ]
317- if len (t .Variants ) == 0 {
318- t .Variants = make (map [string ]int32 )
319- }
320-
321- if _ , ok := t .Variants [variantName ]; ! ok {
322- t .Variants [variantName ] = 1
323- } else {
324- t .Variants [variantName ] += 1
325- }
326- m .bucket .Toggles [name ] = t
327- }
408+ c := m .getOrCreateCounter (name )
328409
329- func (m * metrics ) resetBucket () api.Bucket {
330- prev := m .bucket
331- m .bucket = api.Bucket {
332- Start : time .Now (),
333- Toggles : map [string ]api.ToggleCount {},
410+ c .mu .Lock ()
411+ if c .variants == nil {
412+ c .variants = make (map [string ]int64 )
334413 }
335- return prev
414+ c .variants [variantName ]++
415+ c .mu .Unlock ()
336416}
337417
338418func (m * metrics ) getClientData () ClientData {
0 commit comments