21
21
package worker
22
22
23
23
import (
24
- "context"
25
24
"math"
26
25
"sync"
27
26
"sync/atomic"
28
27
"time"
29
28
29
+ "github.com/jonboulle/clockwork"
30
30
"github.com/uber-go/tally"
31
31
"go.uber.org/zap"
32
32
33
33
"go.uber.org/cadence/.gen/go/shared"
34
34
)
35
35
36
36
const (
37
- concurrencyAutoScalerUpdateTick = time .Second
38
- concurrencyAutoScalerObservabilityTick = time .Millisecond * 500
39
- targetPollerWaitTimeInMsLog2 = 4 // 16 ms
40
- numberOfPollsInRollingAverage = 20
37
+ defaultAutoScalerUpdateTick = time .Second
38
+ // concurrencyAutoScalerObservabilityTick = time.Millisecond * 500
39
+ targetPollerWaitTimeInMsLog2 = 4 // 16 ms
40
+ numberOfPollsInRollingAverage = 20
41
+
42
+ autoScalerEventPollerUpdate autoScalerEvent = "update-poller-limit"
43
+ autoScalerEventPollerSkipUpdateCooldown = "skip-update-poller-limit-cooldown"
44
+ autoScalerEventPollerSkipUpdateNoChange = "skip-update-poller-limit-no-change"
45
+ autoScalerEventPollerSkipUpdateNotEnabled = "skip-update-poller-limit-not-enabled"
46
+ autoScalerEventMetrics = "metrics"
47
+ autoScalerEventEnable = "enable"
48
+ autoScalerEventDisable = "disable"
49
+ autoScalerEventStart = "start"
50
+ autoScalerEventStop = "stop"
51
+ autoScalerEventLogMsg string = "concurrency auto scaler event"
52
+ testTimeFormat string = "15:04:05"
41
53
)
42
54
43
- type ConcurrencyAutoScaler struct {
44
- ctx context.Context
45
- cancel context.CancelFunc
46
- wg * sync.WaitGroup
47
- log * zap.Logger
48
- scope tally.Scope
55
+ type (
56
+ ConcurrencyAutoScaler struct {
57
+ shutdownChan chan struct {}
58
+ wg sync.WaitGroup
59
+ log * zap.Logger
60
+ scope tally.Scope
61
+ clock clockwork.Clock
49
62
50
- concurrency * ConcurrencyLimit
51
- cooldown time.Duration
63
+ concurrency * ConcurrencyLimit
64
+ cooldown time.Duration
65
+ updateTick time.Duration
52
66
53
- // enable auto scaler on concurrency or not
54
- enable atomic.Bool
67
+ // enable auto scaler on concurrency or not
68
+ enable atomic.Bool
55
69
56
- // poller
57
- pollerMaxCount int
58
- pollerMinCount int
59
- pollerWaitTimeInMsLog2 * rollingAverage // log2(pollerWaitTimeInMs+1) for smoothing (ideal value is 0)
60
- pollerPermitLastUpdate time.Time
61
- }
70
+ // poller
71
+ pollerInitCount int
72
+ pollerMaxCount int
73
+ pollerMinCount int
74
+ pollerWaitTimeInMsLog2 * rollingAverage // log2(pollerWaitTimeInMs+1) for smoothing (ideal value is 0)
75
+ pollerPermitLastUpdate time.Time
76
+ }
62
77
63
- type ConcurrencyAutoScalerInput struct {
64
- Concurrency * ConcurrencyLimit
65
- Cooldown time.Duration // cooldown time of update
66
- PollerMaxCount int
67
- PollerMinCount int
68
- Logger * zap.Logger
69
- Scope tally.Scope
70
- }
78
+ ConcurrencyAutoScalerInput struct {
79
+ Concurrency * ConcurrencyLimit
80
+ Cooldown time.Duration // cooldown time of update
81
+ Tick time.Duration // frequency of update check
82
+ PollerMaxCount int
83
+ PollerMinCount int
84
+ Logger * zap.Logger
85
+ Scope tally.Scope
86
+ Clock clockwork.Clock
87
+ }
71
88
72
- func NewPollerAutoScaler ( input ConcurrencyAutoScalerInput ) * ConcurrencyAutoScaler {
73
- ctx , cancel := context . WithCancel ( context . Background () )
89
+ autoScalerEvent string
90
+ )
74
91
92
+ func NewConcurrencyAutoScaler (input ConcurrencyAutoScalerInput ) * ConcurrencyAutoScaler {
93
+ tick := defaultAutoScalerUpdateTick
94
+ if input .Tick != 0 {
95
+ tick = input .Tick
96
+ }
75
97
return & ConcurrencyAutoScaler {
76
- ctx : ctx ,
77
- cancel : cancel ,
78
- wg : & sync.WaitGroup {},
98
+ shutdownChan : make (chan struct {}),
79
99
concurrency : input .Concurrency ,
80
100
cooldown : input .Cooldown ,
81
101
log : input .Logger ,
82
102
scope : input .Scope ,
103
+ clock : input .Clock ,
104
+ updateTick : tick ,
83
105
enable : atomic.Bool {}, // initial value should be false and is only turned on from auto config hint
106
+ pollerInitCount : input .Concurrency .PollerPermit .Quota (),
84
107
pollerMaxCount : input .PollerMaxCount ,
85
108
pollerMinCount : input .PollerMinCount ,
86
109
pollerWaitTimeInMsLog2 : newRollingAverage (numberOfPollsInRollingAverage ),
110
+ pollerPermitLastUpdate : input .Clock .Now (),
87
111
}
88
112
}
89
113
90
114
func (c * ConcurrencyAutoScaler ) Start () {
115
+ c .logEvent (autoScalerEventStart )
116
+
91
117
c .wg .Add (1 )
92
- go func () { // scaling daemon
118
+
119
+ go func () {
93
120
defer c .wg .Done ()
94
- ticker := time .NewTicker (concurrencyAutoScalerUpdateTick )
121
+ ticker := c .clock .NewTicker (c .updateTick )
122
+ defer ticker .Stop ()
95
123
for {
96
124
select {
97
- case <- c .ctx .Done ():
98
- ticker .Stop ()
99
- case <- ticker .C :
125
+ case <- c .shutdownChan :
126
+ return
127
+ case <- ticker .Chan ():
128
+ c .logEvent (autoScalerEventMetrics )
100
129
c .updatePollerPermit ()
101
130
}
102
131
}
103
132
}()
104
- c .wg .Add (1 )
105
- go func () { // observability daemon
106
- defer c .wg .Done ()
107
- ticker := time .NewTicker (concurrencyAutoScalerUpdateTick )
108
- for {
109
- select {
110
- case <- c .ctx .Done ():
111
- ticker .Stop ()
112
- case <- ticker .C :
113
- c .emit ()
114
- }
115
- }
116
- }()
117
133
}
118
134
119
135
func (c * ConcurrencyAutoScaler ) Stop () {
120
- c . cancel ( )
136
+ close ( c . shutdownChan )
121
137
c .wg .Wait ()
138
+ c .logEvent (autoScalerEventStop )
122
139
}
123
140
124
141
// ProcessPollerHint reads the poller response hint and take actions
125
142
// 1. update poller wait time
126
143
// 2. enable/disable auto scaler
127
144
func (c * ConcurrencyAutoScaler ) ProcessPollerHint (hint * shared.AutoConfigHint ) {
128
145
if hint == nil {
146
+ c .log .Warn ("auto config hint is nil, this results in no action" )
129
147
return
130
148
}
131
149
if hint .PollerWaitTimeInMs != nil {
@@ -134,41 +152,53 @@ func (c *ConcurrencyAutoScaler) ProcessPollerHint(hint *shared.AutoConfigHint) {
134
152
}
135
153
136
154
/*
137
- Atomically compare and switch the auto scaler enable flag. If auto scaler is turned off, reset the concurrency limits.
155
+ Atomically compare and switch the auto scaler enable flag. If auto scaler is turned off, IMMEDIATELY reset the concurrency limits.
138
156
*/
139
157
var shouldEnable bool
140
158
if hint .EnableAutoConfig != nil && * hint .EnableAutoConfig {
141
159
shouldEnable = true
142
160
}
143
161
if switched := c .enable .CompareAndSwap (! shouldEnable , shouldEnable ); switched {
144
162
if shouldEnable {
145
- c .log . Sugar (). Infof ( "auto scaler enabled" )
163
+ c .logEvent ( autoScalerEventEnable )
146
164
} else {
147
- c .log . Sugar (). Infof ( "auto scaler disabled" )
148
- c .ResetConcurrency ( )
165
+ c .resetConcurrency ( )
166
+ c .logEvent ( autoScalerEventDisable )
149
167
}
150
168
}
151
169
}
152
170
153
- // ResetConcurrency reset poller quota to the max value. This will be used for gracefully switching the auto scaler off to avoid workers stuck in the wrong state
154
- func (c * ConcurrencyAutoScaler ) ResetConcurrency () {
155
- c .concurrency .PollerPermit .SetQuota (c .pollerMaxCount )
171
+ // resetConcurrency reset poller quota to the max value. This will be used for gracefully switching the auto scaler off to avoid workers stuck in the wrong state
172
+ func (c * ConcurrencyAutoScaler ) resetConcurrency () {
173
+ c .concurrency .PollerPermit .SetQuota (c .pollerInitCount )
156
174
}
157
175
158
- func (c * ConcurrencyAutoScaler ) emit ( ) {
176
+ func (c * ConcurrencyAutoScaler ) logEvent ( event autoScalerEvent ) {
159
177
if c .enable .Load () {
160
178
c .scope .Counter ("concurrency_auto_scaler.enabled" ).Inc (1 )
161
179
} else {
162
180
c .scope .Counter ("concurrency_auto_scaler.disabled" ).Inc (1 )
163
181
}
164
- c .scope .Gauge ("poller_in_action" ).Update (float64 (c .concurrency .PollerPermit .Quota () - c . concurrency . PollerPermit . Count ()))
182
+ c .scope .Gauge ("poller_in_action" ).Update (float64 (c .concurrency .PollerPermit .Count ()))
165
183
c .scope .Gauge ("poller_quota" ).Update (float64 (c .concurrency .PollerPermit .Quota ()))
166
184
c .scope .Gauge ("poller_wait_time" ).Update (math .Exp2 (c .pollerWaitTimeInMsLog2 .Average ()))
185
+ c .log .Debug (autoScalerEventLogMsg ,
186
+ zap .Time ("time" , c .clock .Now ()),
187
+ zap .String ("event" , string (event )),
188
+ zap .Bool ("enabled" , c .enable .Load ()),
189
+ zap .Int ("poller_quota" , c .concurrency .PollerPermit .Quota ()),
190
+ zap .Int ("poller_in_action" , c .concurrency .PollerPermit .Count ()),
191
+ )
167
192
}
168
193
169
194
func (c * ConcurrencyAutoScaler ) updatePollerPermit () {
170
- updateTime := time .Now ()
195
+ if ! c .enable .Load () { // skip update if auto scaler is disabled
196
+ c .logEvent (autoScalerEventPollerSkipUpdateNotEnabled )
197
+ return
198
+ }
199
+ updateTime := c .clock .Now ()
171
200
if updateTime .Before (c .pollerPermitLastUpdate .Add (c .cooldown )) { // before cooldown
201
+ c .logEvent (autoScalerEventPollerSkipUpdateCooldown )
172
202
return
173
203
}
174
204
currentQuota := c .concurrency .PollerPermit .Quota ()
@@ -180,19 +210,16 @@ func (c *ConcurrencyAutoScaler) updatePollerPermit() {
180
210
newQuota = c .pollerMaxCount
181
211
}
182
212
if newQuota == currentQuota {
183
- return
184
- }
185
- enabled := c .enable .Load ()
186
- c .log .Sugar ().With ("applied" , enabled ).Infof ("update poller permit: %v -> %v" , currentQuota , newQuota )
187
- if ! c .enable .Load () {
213
+ c .logEvent (autoScalerEventPollerSkipUpdateNoChange )
188
214
return
189
215
}
190
216
c .concurrency .PollerPermit .SetQuota (newQuota )
191
217
c .pollerPermitLastUpdate = updateTime
218
+ c .logEvent (autoScalerEventPollerUpdate )
192
219
}
193
220
194
221
type rollingAverage struct {
195
- mu sync.Mutex
222
+ mu sync.RWMutex
196
223
window []float64
197
224
index int
198
225
sum float64
@@ -210,6 +237,11 @@ func (r *rollingAverage) Add(value float64) {
210
237
r .mu .Lock ()
211
238
defer r .mu .Unlock ()
212
239
240
+ // no op on zero rolling window
241
+ if len (r .window ) == 0 {
242
+ return
243
+ }
244
+
213
245
// replace the old value with the new value
214
246
r .index %= len (r .window )
215
247
r .sum += value - r .window [r .index ]
@@ -222,8 +254,8 @@ func (r *rollingAverage) Add(value float64) {
222
254
}
223
255
224
256
func (r * rollingAverage ) Average () float64 {
225
- r .mu .Lock ()
226
- defer r .mu .Unlock ()
257
+ r .mu .RLock ()
258
+ defer r .mu .RUnlock ()
227
259
if r .count == 0 {
228
260
return 0
229
261
}
0 commit comments