-
Notifications
You must be signed in to change notification settings - Fork 1.3k
/
execution.go
608 lines (540 loc) · 24.6 KB
/
execution.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
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
/*
*
* k6 - a next-generation load testing tool
* Copyright (C) 2019 Load Impact
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*
*/
package lib
import (
"context"
"errors"
"fmt"
"sync"
"sync/atomic"
"time"
"github.com/sirupsen/logrus"
"github.com/loadimpact/k6/stats"
)
// An ExecutionScheduler is in charge of initializing executors and using them
// to initialize and schedule VUs created by a wrapped Runner. It decouples how
// a swarm of VUs is controlled from the details of how or even where they're
// scheduled.
//
// The core/local execution scheduler schedules VUs on the local machine, but
// the same interface may be implemented to control a test running on a cluster
// or in the cloud.
//
// TODO: flesh out the interface after actually having more than one
// implementation...
type ExecutionScheduler interface {
// Returns the wrapped runner. May return nil if not applicable, eg.
// if we're remote controlling a test running on another machine.
GetRunner() Runner
// Return the ExecutionState instance from which different statistics for the
// current state of the runner could be retrieved.
GetState() *ExecutionState
// Return the instances of the configured executors
GetExecutors() []Executor
// Init initializes all executors, including all of their needed VUs.
Init(ctx context.Context, samplesOut chan<- stats.SampleContainer) error
// Run the ExecutionScheduler, funneling the generated metric samples
// through the supplied out channel.
Run(globalCtx, runCtx context.Context, samplesOut chan<- stats.SampleContainer) error
// Pause a test, or start/resume it. To check if a test is paused, use
// GetState().IsPaused().
//
// Currently, any executor, so any test, can be started in a paused state.
// This will cause k6 to initialize all needed VUs, but it won't actually
// start the test. Later, the test can be started for real by
// resuming/unpausing it from the REST API.
//
// After a test is actually started, it may become impossible to pause it
// again. That is denoted by having SetPaused(true) return an error. The
// likely cause is that some of the executors for the test don't support
// pausing after the test has been started.
//
// IMPORTANT: Currently only the externally controlled executor can be
// paused and resumed multiple times in the middle of the test execution!
// Even then, "pausing" is a bit misleading, since k6 won't pause in the
// middle of the currently executing iterations. It will allow the currently
// in progress iterations to finish, and it just won't start any new ones
// nor will it increment the value returned by GetCurrentTestRunDuration().
SetPaused(paused bool) error
}
// MaxTimeToWaitForPlannedVU specifies the maximum allowable time for an executor
// to wait for a planned VU to be retrieved from the ExecutionState.PlannedVUs
// buffer. If it's exceeded, k6 will emit a warning log message, since it either
// means that there's a bug in the k6 scheduling code, or that the machine is
// overloaded and the scheduling code suffers from delays.
//
// Critically, exceeding this time *doesn't* result in an aborted test or any
// test errors, and the executor will continue to try and borrow the VU
// (potentially resulting in further warnings). We likely should emit a k6
// metric about it in the future. TODO: emit a metric every time this is
// exceeded?
const MaxTimeToWaitForPlannedVU = 400 * time.Millisecond
// MaxRetriesGetPlannedVU how many times we should wait for
// MaxTimeToWaitForPlannedVU before we actually return an error.
const MaxRetriesGetPlannedVU = 5
// ExecutionStatus is similar to RunStatus, but more fine grained and concerns
// only local execution.
//go:generate enumer -type=ExecutionStatus -trimprefix ExecutionStatus -output execution_status_gen.go
type ExecutionStatus uint32
// Possible execution status values
const (
ExecutionStatusCreated ExecutionStatus = iota
ExecutionStatusInitVUs
ExecutionStatusInitExecutors
ExecutionStatusInitDone
ExecutionStatusPausedBeforeRun
ExecutionStatusStarted
ExecutionStatusSetup
ExecutionStatusRunning
ExecutionStatusTeardown
ExecutionStatusEnded
)
// ExecutionState contains a few different things:
// - Some convenience items, that are needed by all executors, like the
// execution segment and the unique VU ID generator. By keeping those here,
// we can just pass the ExecutionState to the different executors, instead of
// individually passing them each item.
// - Mutable counters that different executors modify and other parts of
// k6 can read, e.g. for the vus and vus_max metrics k6 emits every second.
// - Pausing controls and statistics.
//
// The counters and timestamps here are primarily meant to be used for
// information extraction and avoidance of ID collisions. Using many of the
// counters here for synchronization between VUs could result in HIDDEN data
// races, because the Go data race detector can't detect any data races
// involving atomics...
//
// The only functionality intended for synchronization is the one revolving
// around pausing, and uninitializedUnplannedVUs for restricting the number of
// unplanned VUs being initialized.
type ExecutionState struct {
// A copy of the options, so the different executors have access to them.
// They will need to access things like the current execution segment, the
// per-run metrics tags, etc.
//
// Obviously, they are not meant to be changed... They should be a constant
// during the execution of a single test, but we can't easily enforce that
// via the Go type system...
Options Options
ExecutionTuple *ExecutionTuple // TODO Rename, possibly move
// vus is the shared channel buffer that contains all of the VUs that have
// been initialized and aren't currently being used by a executor.
//
// It contains both pre-initialized (i.e. planned) VUs, as well as any
// unplanned VUs. Planned VUs are initialized before a test begins, while
// unplanned VUS can be initialized in the middle of the test run by a
// executor and have been relinquished after it has finished working with
// them. Usually, unplanned VUs are initialized by one of the arrival-rate
// executors, after they have exhausted their PreAllocatedVUs. After the
// executor is done with the VUs, it will put in this channel, so it could
// potentially be reused by other executors further along in the test.
//
// Different executors cooperatively borrow VUs from here when they are
// needed and return them when they are done with them. There's no central
// enforcement of correctness, i.e. that a executor takes more VUs from
// here than its execution plan has stipulated. The correctness guarantee
// lies with the actual executors - bugs in one can affect others.
//
// That's why the field is private and we force executors to use the
// GetPlannedVU(), GetUnplannedVU(), and ReturnVU() methods instead of work
// directly with the channel. These methods will emit a warning or can even
// return an error if retrieving a VU takes more than
// MaxTimeToWaitForPlannedVU.
vus chan InitializedVU
// The current VU ID, used for the __VU execution context variable. Use the
// GetUniqueVUIdentifier() to get unique values for each VU, starting from 1
// (for backwards compatibility...)
currentVUIdentifier *uint64
// TODO: add something similar, but for iterations? Currently, there isn't
// a straightforward way to get a unique sequential identifier per iteration
// in the context of a single k6 instance. Combining __VU and __ITER gives us
// a unique identifier, but it's unwieldy and somewhat cumbersome.
// Total number of currently initialized VUs. Generally equal to
// currentVUIdentifier minus 1, since initializedVUs starts from 0 and is
// incremented only after a VU is initialized, while CurrentVUIdentifier is
// incremented before a VU is initialized. It should always be greater than
// or equal to 0, but int64 is used for simplification of the used atomic
// arithmetic operations.
initializedVUs *int64
// Total number of unplanned VUs we haven't initialized yet. It starts
// being equal to GetMaxPossibleVUs(executionPlan)-GetMaxPlannedVUs(), and
// may stay that way if no unplanned VUs are initialized. Once it reaches 0,
// no more unplanned VUs can be initialized.
uninitializedUnplannedVUs *int64
// Injected when the execution scheduler's Init function is called, used for
// initializing unplanned VUs.
initVUFunc InitVUFunc
// The number of VUs that are currently executing the test script. This also
// includes any VUs that are in the process of gracefully winding down,
// either at the end of the test, or when VUs are ramping down. It should
// always be greater than or equal to 0, but int64 is used for
// simplification of the used atomic arithmetic operations.
activeVUs *int64
// The total number of full (i.e uninterrupted) iterations that have been
// completed so far.
fullIterationsCount *uint64
// The total number of iterations that have been interrupted during their
// execution. The potential interruption causes vary - end of a specified
// script `duration`, scaling down of VUs via `stages`, a user hitting
// Ctrl+C, change of `vus` via the externally controlled executor's REST
// API, etc.
interruptedIterationsCount *uint64
// A machine-readable indicator in which the current state of the test
// execution is currently stored. Useful for the REST API and external
// observability of the k6 test run progress.
executionStatus *uint32
// A nanosecond UNIX timestamp that is set when the test is actually
// started. The default 0 value is used to denote that the test hasn't
// started yet...
startTime *int64
// A nanosecond UNIX timestamp that is set when the test ends, either
// by an early context cancel or at its regularly scheduled time.
// The default 0 value is used to denote that the test hasn't ended yet.
endTime *int64
// Stuff related to pausing follows. Read the docs in ExecutionScheduler for
// more information regarding how pausing works in k6.
//
// When we pause the execution in the middle of the test, we save the
// current timestamp in currentPauseTime. When we resume the execution, we
// set currentPauseTime back to 0 and we add the (time.Now() -
// currentPauseTime) duration to totalPausedDuration (unless the test hasn't
// started yet).
//
// Thus, the algorithm for GetCurrentTestRunDuration() is very
// straightforward:
// - if the test hasn't started, return 0
// - set endTime to:
// - the current pauseTime, if not zero
// - time.Now() otherwise
// - return (endTime - startTime - totalPausedDuration)
//
// Quickly checking for IsPaused() just means comparing the currentPauseTime
// with 0, a single atomic operation.
//
// But if we want to wait until a script resumes, or be notified of the
// start/resume event from a channel (as part of a select{}), we have to
// acquire the pauseStateLock, get the current resumeNotify instance,
// release the lock and wait to read from resumeNotify (when it's closed by
// Resume()).
currentPauseTime *int64
pauseStateLock sync.RWMutex
totalPausedDuration time.Duration // only modified behind the lock
resumeNotify chan struct{}
}
// NewExecutionState initializes all of the pointers in the ExecutionState
// with zeros. It also makes sure that the initial state is unpaused, by
// setting resumeNotify to an already closed channel.
func NewExecutionState(options Options, et *ExecutionTuple, maxPlannedVUs, maxPossibleVUs uint64) *ExecutionState {
resumeNotify := make(chan struct{})
close(resumeNotify) // By default the ExecutionState starts unpaused
maxUnplannedUninitializedVUs := int64(maxPossibleVUs - maxPlannedVUs)
return &ExecutionState{
Options: options,
vus: make(chan InitializedVU, maxPossibleVUs),
executionStatus: new(uint32),
currentVUIdentifier: new(uint64),
initializedVUs: new(int64),
uninitializedUnplannedVUs: &maxUnplannedUninitializedVUs,
activeVUs: new(int64),
fullIterationsCount: new(uint64),
interruptedIterationsCount: new(uint64),
startTime: new(int64),
endTime: new(int64),
currentPauseTime: new(int64),
pauseStateLock: sync.RWMutex{},
totalPausedDuration: 0, // Accessed only behind the pauseStateLock
resumeNotify: resumeNotify,
ExecutionTuple: et,
}
}
// GetUniqueVUIdentifier returns an auto-incrementing unique VU ID, used for __VU.
// It starts from 1 (for backwards compatibility...)
func (es *ExecutionState) GetUniqueVUIdentifier() uint64 {
return atomic.AddUint64(es.currentVUIdentifier, 1)
}
// GetInitializedVUsCount returns the total number of currently initialized VUs.
//
// Important: this doesn't include any temporary/service VUs that are destroyed
// after they are used. These are created for the initial retrieval of the
// exported script options and for the execution of setup() and teardown()
//
// IMPORTANT: for UI/information purposes only, don't use for synchronization.
func (es *ExecutionState) GetInitializedVUsCount() int64 {
return atomic.LoadInt64(es.initializedVUs)
}
// ModInitializedVUsCount changes the total number of currently initialized VUs.
//
// IMPORTANT: for UI/information purposes only, don't use for synchronization.
func (es *ExecutionState) ModInitializedVUsCount(mod int64) int64 {
return atomic.AddInt64(es.initializedVUs, mod)
}
// GetCurrentlyActiveVUsCount returns the number of VUs that are currently
// executing the test script. This also includes any VUs that are in the process
// of gracefully winding down.
//
// IMPORTANT: for UI/information purposes only, don't use for synchronization.
func (es *ExecutionState) GetCurrentlyActiveVUsCount() int64 {
return atomic.LoadInt64(es.activeVUs)
}
// ModCurrentlyActiveVUsCount changes the total number of currently active VUs.
//
// IMPORTANT: for UI/information purposes only, don't use for synchronization.
func (es *ExecutionState) ModCurrentlyActiveVUsCount(mod int64) int64 {
return atomic.AddInt64(es.activeVUs, mod)
}
// GetFullIterationCount returns the total of full (i.e uninterrupted) iterations
// that have been completed so far.
//
// IMPORTANT: for UI/information purposes only, don't use for synchronization.
func (es *ExecutionState) GetFullIterationCount() uint64 {
return atomic.LoadUint64(es.fullIterationsCount)
}
// AddFullIterations increments the number of full (i.e uninterrupted) iterations
// by the provided amount.
//
// IMPORTANT: for UI/information purposes only, don't use for synchronization.
func (es *ExecutionState) AddFullIterations(count uint64) uint64 {
return atomic.AddUint64(es.fullIterationsCount, count)
}
// GetPartialIterationCount returns the total of partial (i.e interrupted)
// iterations that have been completed so far.
//
// IMPORTANT: for UI/information purposes only, don't use for synchronization.
func (es *ExecutionState) GetPartialIterationCount() uint64 {
return atomic.LoadUint64(es.interruptedIterationsCount)
}
// AddInterruptedIterations increments the number of partial (i.e interrupted)
// iterations by the provided amount.
//
// IMPORTANT: for UI/information purposes only, don't use for synchronization.
func (es *ExecutionState) AddInterruptedIterations(count uint64) uint64 {
return atomic.AddUint64(es.interruptedIterationsCount, count)
}
// SetExecutionStatus changes the current execution status to the supplied value
// and returns the current value.
func (es *ExecutionState) SetExecutionStatus(newStatus ExecutionStatus) (oldStatus ExecutionStatus) {
return ExecutionStatus(atomic.SwapUint32(es.executionStatus, uint32(newStatus)))
}
// GetCurrentExecutionStatus returns the current execution status. Don't use
// this for synchronization unless you've made the k6 behavior somewhat
// predictable with options like --paused or --linger.
func (es *ExecutionState) GetCurrentExecutionStatus() ExecutionStatus {
return ExecutionStatus(atomic.LoadUint32(es.executionStatus))
}
// MarkStarted saves the current timestamp as the test start time.
//
// CAUTION: Calling MarkStarted() a second time for the same execution state will
// result in a panic!
func (es *ExecutionState) MarkStarted() {
if !atomic.CompareAndSwapInt64(es.startTime, 0, time.Now().UnixNano()) {
panic("the execution scheduler was started a second time")
}
es.SetExecutionStatus(ExecutionStatusStarted)
}
// MarkEnded saves the current timestamp as the test end time.
//
// CAUTION: Calling MarkEnded() a second time for the same execution state will
// result in a panic!
func (es *ExecutionState) MarkEnded() {
if !atomic.CompareAndSwapInt64(es.endTime, 0, time.Now().UnixNano()) {
panic("the execution scheduler was stopped a second time")
}
es.SetExecutionStatus(ExecutionStatusEnded)
}
// HasStarted returns true if the test has actually started executing.
// It will return false while a test is in the init phase, or if it has
// been initially paused. But if will return true if a test is paused
// midway through its execution (see above for details regarding the
// feasibility of that pausing for normal executors).
func (es *ExecutionState) HasStarted() bool {
return atomic.LoadInt64(es.startTime) != 0
}
// HasEnded returns true if the test has finished executing. It will return
// false until MarkEnded() is called.
func (es *ExecutionState) HasEnded() bool {
return atomic.LoadInt64(es.endTime) != 0
}
// IsPaused quickly returns whether the test is currently paused, by reading
// the atomic currentPauseTime timestamp
func (es *ExecutionState) IsPaused() bool {
return atomic.LoadInt64(es.currentPauseTime) != 0
}
// GetCurrentTestRunDuration returns the duration for which the test has already
// ran. If the test hasn't started yet, that's 0. If it has started, but has
// been paused midway through, it will return the time up until the pause time.
// And if it's currently running, it will return the time since the start time.
//
// IMPORTANT: for UI/information purposes only, don't use for synchronization.
func (es *ExecutionState) GetCurrentTestRunDuration() time.Duration {
startTime := atomic.LoadInt64(es.startTime)
if startTime == 0 {
// The test hasn't started yet
return 0
}
es.pauseStateLock.RLock()
endTime := atomic.LoadInt64(es.endTime)
pausedDuration := es.totalPausedDuration
es.pauseStateLock.RUnlock()
if endTime == 0 {
pauseTime := atomic.LoadInt64(es.currentPauseTime)
if pauseTime != 0 {
endTime = pauseTime
} else {
// The test isn't paused or finished, use the current time instead
endTime = time.Now().UnixNano()
}
}
return time.Duration(endTime-startTime) - pausedDuration
}
// Pause pauses the current execution. It acquires the lock, writes
// the current timestamp in currentPauseTime, and makes a new
// channel for resumeNotify.
// Pause can return an error if the test was already paused.
func (es *ExecutionState) Pause() error {
es.pauseStateLock.Lock()
defer es.pauseStateLock.Unlock()
if !atomic.CompareAndSwapInt64(es.currentPauseTime, 0, time.Now().UnixNano()) {
return errors.New("test execution was already paused")
}
es.resumeNotify = make(chan struct{})
return nil
}
// Resume unpauses the test execution. Unless the test wasn't
// yet started, it calculates the duration between now and
// the old currentPauseTime and adds it to
// Resume will emit an error if the test wasn't paused.
func (es *ExecutionState) Resume() error {
es.pauseStateLock.Lock()
defer es.pauseStateLock.Unlock()
currentPausedTime := atomic.SwapInt64(es.currentPauseTime, 0)
if currentPausedTime == 0 {
return errors.New("test execution wasn't paused")
}
// Check that it's not the pause before execution actually starts
if atomic.LoadInt64(es.startTime) != 0 {
es.totalPausedDuration += time.Duration(time.Now().UnixNano() - currentPausedTime)
}
close(es.resumeNotify)
return nil
}
// ResumeNotify returns a channel which will be closed (i.e. could
// be read from) as soon as the test execution is resumed.
//
// Since tests would likely be paused only rarely, unless you
// directly need to be notified via a channel that the test
// isn't paused or that it has resumed, it's probably a good
// idea to first use the IsPaused() method, since it will be much
// faster.
//
// And, since tests won't be paused most of the time, it's
// probably better to check for that like this:
// if executionState.IsPaused() {
// <-executionState.ResumeNotify()
// }
func (es *ExecutionState) ResumeNotify() <-chan struct{} {
es.pauseStateLock.RLock()
defer es.pauseStateLock.RUnlock()
return es.resumeNotify
}
// GetPlannedVU tries to get a pre-initialized VU from the buffer channel. This
// shouldn't fail and should generally be an instantaneous action, but if it
// doesn't happen for MaxTimeToWaitForPlannedVU (for example, because the system
// is overloaded), a warning will be printed. If we reach that timeout more than
// MaxRetriesGetPlannedVU number of times, this function will return an error,
// since we either have a bug with some executor, or the machine is very, very
// overloaded.
//
// If modifyActiveVUCount is true, the method would also increment the counter
// for active VUs. In most cases, that's the desired behavior, but some
// executors might have to retrieve their reserved VUs without using them
// immediately - for example, the externally-controlled executor when the
// configured maxVUs number is greater than the configured starting VUs.
func (es *ExecutionState) GetPlannedVU(logger *logrus.Entry, modifyActiveVUCount bool) (InitializedVU, error) {
for i := 1; i <= MaxRetriesGetPlannedVU; i++ {
select {
case vu := <-es.vus:
if modifyActiveVUCount {
es.ModCurrentlyActiveVUsCount(+1)
}
// TODO: set environment and exec
return vu, nil
case <-time.After(MaxTimeToWaitForPlannedVU):
logger.Warnf("Could not get a VU from the buffer for %s", time.Duration(i)*MaxTimeToWaitForPlannedVU)
}
}
return nil, fmt.Errorf(
"could not get a VU from the buffer in %s",
MaxRetriesGetPlannedVU*MaxTimeToWaitForPlannedVU,
)
}
// SetInitVUFunc is called by the execution scheduler's init function, and it's
// used for setting the "constructor" function used for the initializing
// unplanned VUs.
//
// TODO: figure out a better dependency injection method?
func (es *ExecutionState) SetInitVUFunc(initVUFunc InitVUFunc) {
es.initVUFunc = initVUFunc
}
// GetUnplannedVU checks if any unplanned VUs remain to be initialized, and if
// they do, it initializes one and returns it. If all unplanned VUs have already
// been initialized, it returns one from the global vus buffer.
//
// IMPORTANT: GetUnplannedVU() doesn't do any checking if the requesting
// executor is actually allowed to have the VU at this particular time.
// Executors are trusted to correctly declare their needs (via their
// GetExecutionRequirements() methods) and then to never ask for more VUs than
// they have specified in those requirements.
func (es *ExecutionState) GetUnplannedVU(ctx context.Context, logger *logrus.Entry) (InitializedVU, error) {
remVUs := atomic.AddInt64(es.uninitializedUnplannedVUs, -1)
if remVUs < 0 {
logger.Debug("Reusing a previously initialized unplanned VU")
atomic.AddInt64(es.uninitializedUnplannedVUs, 1)
return es.GetPlannedVU(logger, true)
}
logger.Debug("Initializing an unplanned VU, this may affect test results")
return es.InitializeNewVU(ctx, logger)
}
// InitializeNewVU creates and returns a brand new VU, updating the relevant
// tracking counters.
func (es *ExecutionState) InitializeNewVU(ctx context.Context, logger *logrus.Entry) (InitializedVU, error) {
if es.initVUFunc == nil {
return nil, fmt.Errorf("initVUFunc wasn't set in the execution state")
}
newVU, err := es.initVUFunc(ctx, logger)
if err != nil {
return nil, err
}
es.ModInitializedVUsCount(+1)
return newVU, err
}
// AddInitializedVU is a helper function that adds VUs into the buffer and
// increases the initialized VUs counter.
func (es *ExecutionState) AddInitializedVU(vu InitializedVU) {
es.vus <- vu
es.ModInitializedVUsCount(+1)
}
// ReturnVU is a helper function that puts VUs back into the buffer and
// decreases the active VUs counter.
func (es *ExecutionState) ReturnVU(vu InitializedVU, wasActive bool) {
es.vus <- vu
if wasActive {
es.ModCurrentlyActiveVUsCount(-1)
}
}