@@ -20,6 +20,7 @@ import (
20
20
"golang.org/x/build/buildenv"
21
21
"golang.org/x/build/buildlet"
22
22
"golang.org/x/build/dashboard"
23
+ "golang.org/x/build/internal"
23
24
"golang.org/x/build/internal/cloud"
24
25
"golang.org/x/build/internal/spanlog"
25
26
)
@@ -72,10 +73,6 @@ func WithVMDeleteTimeout(timeout time.Duration) EC2Opt {
72
73
73
74
// EC2Buildlet manages a pool of AWS EC2 buildlets.
74
75
type EC2Buildlet struct {
75
- once sync.Once
76
- // done channel closing will signal the pollers to discontinue polling
77
- done chan struct {}
78
-
79
76
// awsClient is the client used to interact with AWS services.
80
77
awsClient awsClient
81
78
// buildEnv contains the build enviornment settings.
@@ -93,8 +90,12 @@ type EC2Buildlet struct {
93
90
isRemoteBuildlet IsRemoteBuildletFunc
94
91
// ledger tracks instances and their resource allocations.
95
92
ledger * ledger
93
+ // cancelPoll will signal to the pollers to discontinue polling.
94
+ cancelPoll context.CancelFunc
96
95
// vmDeleteTimeout contains the timeout used to determine if a VM should be deleted.
97
96
vmDeleteTimeout time.Duration
97
+ // pollWait waits for all pollers to terminate polling.
98
+ pollWait sync.WaitGroup
98
99
}
99
100
100
101
// ec2BuildletClient represents an EC2 buildlet client in the buildlet package.
@@ -111,11 +112,12 @@ func NewEC2Buildlet(client *cloud.AWSClient, buildEnv *buildenv.Environment, hos
111
112
if fn == nil {
112
113
return nil , errors .New ("remote buildlet check function is not set" )
113
114
}
115
+ ctx , cancel := context .WithCancel (context .Background ())
114
116
b := & EC2Buildlet {
115
117
awsClient : client ,
116
118
buildEnv : buildEnv ,
117
119
buildletClient : buildlet .NewEC2Client (client ),
118
- done : make ( chan struct {}) ,
120
+ cancelPoll : cancel ,
119
121
hosts : hosts ,
120
122
isRemoteBuildlet : fn ,
121
123
ledger : newLedger (),
@@ -124,14 +126,34 @@ func NewEC2Buildlet(client *cloud.AWSClient, buildEnv *buildenv.Environment, hos
124
126
for _ , opt := range opts {
125
127
opt (b )
126
128
}
127
- if err := b .retrieveAndSetQuota (); err != nil {
129
+ if err := b .retrieveAndSetQuota (ctx ); err != nil {
128
130
return nil , fmt .Errorf ("unable to create EC2 pool: %w" , err )
129
131
}
130
132
if err := b .retrieveAndSetInstanceTypes (); err != nil {
131
133
return nil , fmt .Errorf ("unable to create EC2 pool: %w" , err )
132
134
}
133
- go b .cleanupUnusedVMs ()
134
- go b .pollQuota ()
135
+
136
+ b .pollWait .Add (1 )
137
+ // polls for the EC2 quota data and sets the quota data in
138
+ // the ledger. When the context has been cancelled, the polling will stop.
139
+ go func () {
140
+ go internal .PeriodicallyDo (ctx , time .Hour , func (ctx context.Context , _ time.Time ) {
141
+ log .Printf ("retrieveing EC2 quota" )
142
+ _ = b .retrieveAndSetQuota (ctx )
143
+ })
144
+ b .pollWait .Done ()
145
+ }()
146
+
147
+ b .pollWait .Add (1 )
148
+ // poll queries for VMs which are not tracked in the ledger and
149
+ // deletes them. When the context has been cancelled, the polling will stop.
150
+ go func () {
151
+ go internal .PeriodicallyDo (ctx , 2 * time .Minute , func (ctx context.Context , _ time.Time ) {
152
+ log .Printf ("cleaning up unused EC2 instances" )
153
+ b .destroyUntrackedInstances (ctx )
154
+ })
155
+ b .pollWait .Done ()
156
+ }()
135
157
136
158
// TODO(golang.org/issues/38337) remove once a package level variable is no longer
137
159
// required by the main package.
@@ -256,45 +278,24 @@ func (eb *EC2Buildlet) buildletDone(instName string) {
256
278
257
279
// Close stops the pollers used by the EC2Buildlet pool from running.
258
280
func (eb * EC2Buildlet ) Close () {
259
- eb .once .Do (func () {
260
- close (eb .done )
261
- })
281
+ eb .cancelPoll ()
282
+ eb .pollWait .Wait ()
262
283
}
263
284
264
285
// retrieveAndSetQuota queries EC2 for account relevant quotas and sets the quota in the ledger.
265
- func (eb * EC2Buildlet ) retrieveAndSetQuota () error {
266
- ctx , cancel := context .WithTimeout (context . Background () , 10 * time .Second )
286
+ func (eb * EC2Buildlet ) retrieveAndSetQuota (ctx context. Context ) error {
287
+ ctx , cancel := context .WithTimeout (ctx , 10 * time .Second )
267
288
defer cancel ()
268
289
269
290
cpuQuota , err := eb .awsClient .Quota (ctx , cloud .QuotaServiceEC2 , cloud .QuotaCodeCPUOnDemand )
270
291
if err != nil {
271
- log .Printf ("unable to query for cpu quota: %s" , err )
292
+ log .Printf ("unable to query for EC2 cpu quota: %s" , err )
272
293
return err
273
294
}
274
295
eb .ledger .SetCPULimit (cpuQuota )
275
296
return nil
276
297
}
277
298
278
- // pollQuota repeatedly polls for the EC2 quota data and sets the quota data in
279
- // the ledger. It stops polling once the done channel has been closed.
280
- func (eb * EC2Buildlet ) pollQuota () {
281
- t := time .NewTicker (time .Hour )
282
- defer t .Stop ()
283
- for {
284
- select {
285
- case <- t .C :
286
- err := eb .retrieveAndSetQuota ()
287
- if err != nil {
288
- log .Printf ("polling for EC2 quota failed: %s" , err )
289
- }
290
- case <- eb .done :
291
- // closing the done channel signals the end of the polling loop.
292
- log .Printf ("stopped polling for EC2 quota" )
293
- return
294
- }
295
- }
296
- }
297
-
298
299
// retrieveAndSetInstanceTypes retrieves the ARM64 instance types from the EC2
299
300
// service and sets them in the ledger.
300
301
func (eb * EC2Buildlet ) retrieveAndSetInstanceTypes () error {
@@ -303,35 +304,17 @@ func (eb *EC2Buildlet) retrieveAndSetInstanceTypes() error {
303
304
304
305
its , err := eb .awsClient .InstanceTypesARM (ctx )
305
306
if err != nil {
306
- return fmt .Errorf ("unable to retrieve instance types: %w" , err )
307
+ return fmt .Errorf ("unable to retrieve EC2 instance types: %w" , err )
307
308
}
308
309
eb .ledger .UpdateInstanceTypes (its )
309
310
log .Printf ("ec2 buildlet pool instance types updated" )
310
311
return nil
311
312
}
312
313
313
- // cleanupUnusedVMs periodically queries for VMs which are not tracked in the ledger and
314
- // deletes them. If the done channel has been closed then the polling will exit.
315
- func (eb * EC2Buildlet ) cleanupUnusedVMs () {
316
- t := time .NewTicker (2 * time .Minute )
317
- defer t .Stop ()
318
- for {
319
- select {
320
- case <- t .C :
321
- log .Printf ("cleaning up unused EC2 instances" )
322
- eb .destroyUntrackedInstances ()
323
- case <- eb .done :
324
- // closing the done channel signals the end of the polling loop.
325
- log .Printf ("stopped cleaning up unused EC2 instances" )
326
- return
327
- }
328
- }
329
- }
330
-
331
314
// destroyUntrackedInstances searches for VMs which exist but are not being tracked in the
332
315
// ledger and deletes them.
333
- func (eb * EC2Buildlet ) destroyUntrackedInstances () {
334
- ctx , cancel := context .WithTimeout (context . Background () , 30 * time .Second )
316
+ func (eb * EC2Buildlet ) destroyUntrackedInstances (ctx context. Context ) {
317
+ ctx , cancel := context .WithTimeout (ctx , 30 * time .Second )
335
318
defer cancel ()
336
319
337
320
insts , err := eb .awsClient .RunningInstances (ctx )
0 commit comments