-
Notifications
You must be signed in to change notification settings - Fork 0
/
Copy pathcounters.go
161 lines (132 loc) · 3.37 KB
/
counters.go
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
package breaker
import (
"errors"
"time"
"golang.org/x/net/context"
)
var (
ErrNumberOfSecondsToStoreOutOfBounds error = errors.New("NumberOfSecondsToStore out of bounds, should be between 1 and 60 seconds")
)
type HealthSummary struct {
Failures int64
Success int64
Total int64
ErrorPercentage float64
// time for metrics
LastFailure time.Time
LastSuccess time.Time
}
// bucket to store the metrics
type HealthCountsBucket struct {
failures int64
success int64
lastWrite time.Time
}
type HealthCounts struct {
// buckets to store the counters
values []HealthCountsBucket
// number of buckets
buckets int
// time frame to store
window time.Duration
// time for the last event
lastFailure time.Time
lastSuccess time.Time
// channels for the event loop
successChan chan struct{}
failuresChan chan struct{}
summaryChan chan struct{}
summaryOutChan chan HealthSummary
// context for cancelation
ctx context.Context
cancel context.CancelFunc
}
func NewHealthCounts(numberOfSecondsToStore int) (*HealthCounts, error) {
if numberOfSecondsToStore <= 0 || numberOfSecondsToStore > 60 {
return nil, ErrNumberOfSecondsToStoreOutOfBounds
}
hc := &HealthCounts{
buckets: numberOfSecondsToStore,
window: time.Duration(numberOfSecondsToStore) * time.Second,
values: make([]HealthCountsBucket, numberOfSecondsToStore, numberOfSecondsToStore),
successChan: make(chan struct{}),
failuresChan: make(chan struct{}),
summaryChan: make(chan struct{}),
summaryOutChan: make(chan HealthSummary),
}
hc.ctx, hc.cancel = context.WithCancel(context.Background())
go hc.run()
return hc, nil
}
func (h *HealthCounts) Fail() {
h.failuresChan <- struct{}{}
}
func (h *HealthCounts) Success() {
h.successChan <- struct{}{}
}
func (m *HealthCounts) Summary() HealthSummary {
m.summaryChan <- struct{}{}
return <-m.summaryOutChan
}
func (hc *HealthCounts) Cancel() {
hc.cancel()
}
func (hc *HealthCounts) run() {
for {
select {
case <-hc.successChan:
hc.doSuccess()
case <-hc.failuresChan:
hc.doFail()
case <-hc.summaryChan:
hc.summaryOutChan <- hc.doSummary()
case <-hc.ctx.Done():
return
}
}
}
func (c *HealthCountsBucket) reset() {
c.failures = 0
c.success = 0
}
// The design of the buckets follows the leaky bucket design from Netflix Hytrix
// The limit in the store is 60 seconds
func (hc *HealthCounts) bucket() *HealthCountsBucket {
now := time.Now()
index := now.Second() % hc.buckets
if !hc.values[index].lastWrite.IsZero() {
elapsed := now.Sub(hc.values[index].lastWrite)
if elapsed > hc.window {
hc.values[index].reset()
}
}
hc.values[index].lastWrite = now
return &hc.values[index]
}
func (h *HealthCounts) doSuccess() {
h.bucket().success++
h.lastSuccess = time.Now()
}
func (h *HealthCounts) doFail() {
h.bucket().failures++
h.lastFailure = time.Now()
}
func (b *HealthCounts) doSummary() HealthSummary {
var sum HealthSummary
now := time.Now()
for _, value := range b.values {
if !value.lastWrite.IsZero() && (now.Sub(value.lastWrite) <= b.window) {
sum.Success += value.success
sum.Failures += value.failures
}
}
sum.Total = sum.Success + sum.Failures
if sum.Total == 0 {
sum.ErrorPercentage = 0
} else {
sum.ErrorPercentage = float64(sum.Failures) / float64(sum.Total) * 100.0
}
sum.LastFailure = b.lastFailure
sum.LastSuccess = b.lastSuccess
return sum
}