diff --git a/core/local/local_test.go b/core/local/local_test.go index c6273100fed..18ad51a934d 100644 --- a/core/local/local_test.go +++ b/core/local/local_test.go @@ -881,6 +881,37 @@ func TestExecutionSchedulerEndTime(t *testing.T) { assert.True(t, runTime < 10*time.Second, "took more than 10 seconds") } +func TestExecutionSchedulerEndTime0VUsAtEnd(t *testing.T) { + t.Parallel() + runner := &minirunner.MiniRunner{ + Fn: func(ctx context.Context, _ *lib.State, out chan<- metrics.SampleContainer) error { + time.Sleep(100 * time.Millisecond) + return nil + }, + } + _, cancel, execScheduler, _ := newTestExecutionScheduler(t, runner, nil, lib.Options{ + Stages: []lib.Stage{ + { + Duration: types.NullDurationFrom(time.Second), + Target: null.IntFrom(1), + }, + { + Duration: types.NullDurationFrom(time.Second), + Target: null.IntFrom(0), + }, + { + Duration: types.NullDurationFrom(time.Hour), + Target: null.IntFrom(0), + }, + }, + }) + defer cancel() + + endTime, isFinal := lib.GetEndOffset(execScheduler.GetExecutionPlan()) + assert.Equal(t, time.Hour+2*time.Second, endTime) // because of the big 0 vu stage at end + assert.True(t, isFinal) +} + func TestExecutionSchedulerRuntimeErrors(t *testing.T) { t.Parallel() runner := &minirunner.MiniRunner{ diff --git a/lib/executor/ramping_vus.go b/lib/executor/ramping_vus.go index 10bcdff20ba..a23545da6a8 100644 --- a/lib/executor/ramping_vus.go +++ b/lib/executor/ramping_vus.go @@ -202,13 +202,19 @@ func (vlvc RampingVUsConfig) getRawExecutionSteps(et *lib.ExecutionTuple, zeroEn } } - for _, stage := range vlvc.Stages { + for i, stage := range vlvc.Stages { stageEndVUs := stage.Target.Int64 stageDuration := stage.Duration.TimeDuration() timeTillEnd += stageDuration stageVUDiff := stageEndVUs - fromVUs if stageVUDiff == 0 { + // skip those as this doesn't change anything + if i == len(vlvc.Stages)-1 { + // don't skip the last step as we won't add another one after it + // unlike in any of the other cases + steps = append(steps, lib.ExecutionStep{TimeOffset: timeTillEnd, PlannedVUs: uint64(scaled)}) + } continue } if stageDuration == 0 { @@ -244,7 +250,7 @@ func (vlvc RampingVUsConfig) getRawExecutionSteps(et *lib.ExecutionTuple, zeroEn } if zeroEnd { - steps = append(steps, lib.ExecutionStep{TimeOffset: timeTillEnd, PlannedVUs: 0}) + addStep(timeTillEnd, 0) } return steps } @@ -449,19 +455,34 @@ func (vlvc RampingVUsConfig) reserveVUsForGracefulRampDowns( //nolint:funlen // - If the last stage's target is more than 0, the VUs at the end of the // executor's life will have more time to finish their last iterations. func (vlvc RampingVUsConfig) GetExecutionRequirements(et *lib.ExecutionTuple) []lib.ExecutionStep { - steps := vlvc.getRawExecutionSteps(et, false) + origSteps := vlvc.getRawExecutionSteps(et, false) executorEndOffset := sumStagesDuration(vlvc.Stages) + vlvc.GracefulStop.TimeDuration() // Handle graceful ramp-downs, if we have them + steps := origSteps if vlvc.GracefulRampDown.Duration > 0 { steps = vlvc.reserveVUsForGracefulRampDowns(steps, executorEndOffset) } + lastIndex := len(steps) + for ; lastIndex > 0; lastIndex-- { + if steps[lastIndex-1].PlannedVUs != 0 { + break + } + } + if len(steps) > lastIndex && steps[lastIndex].TimeOffset < executorEndOffset { + executorEndOffset = steps[lastIndex].TimeOffset + } // add one step for the end of the gracefulStop - if steps[len(steps)-1].PlannedVUs != 0 || steps[len(steps)-1].TimeOffset != executorEndOffset { + if steps[len(steps)-1].PlannedVUs != 0 || steps[len(steps)-1].TimeOffset < executorEndOffset { steps = append(steps, lib.ExecutionStep{TimeOffset: executorEndOffset, PlannedVUs: 0}) } + if steps[len(steps)-1].TimeOffset < origSteps[len(origSteps)-1].TimeOffset { + // This can only happen on multiple 0 vus stages at the end + steps = append(steps, lib.ExecutionStep{TimeOffset: origSteps[len(origSteps)-1].TimeOffset, PlannedVUs: 0}) + } + return steps } diff --git a/lib/executor/ramping_vus_test.go b/lib/executor/ramping_vus_test.go index b6d3dfc3687..bd002458d8a 100644 --- a/lib/executor/ramping_vus_test.go +++ b/lib/executor/ramping_vus_test.go @@ -438,14 +438,14 @@ func TestRampingVUsConfigExecutionPlanExample(t *testing.T) { conf := NewRampingVUsConfig("test") conf.StartVUs = null.IntFrom(4) conf.Stages = []Stage{ - {Target: null.IntFrom(6), Duration: types.NullDurationFrom(2 * time.Second)}, - {Target: null.IntFrom(1), Duration: types.NullDurationFrom(5 * time.Second)}, - {Target: null.IntFrom(5), Duration: types.NullDurationFrom(4 * time.Second)}, - {Target: null.IntFrom(1), Duration: types.NullDurationFrom(4 * time.Second)}, - {Target: null.IntFrom(4), Duration: types.NullDurationFrom(3 * time.Second)}, - {Target: null.IntFrom(4), Duration: types.NullDurationFrom(2 * time.Second)}, - {Target: null.IntFrom(1), Duration: types.NullDurationFrom(0 * time.Second)}, - {Target: null.IntFrom(1), Duration: types.NullDurationFrom(3 * time.Second)}, + {Target: null.IntFrom(6), Duration: types.NullDurationFrom(2 * time.Second)}, // 2 + {Target: null.IntFrom(1), Duration: types.NullDurationFrom(5 * time.Second)}, // 7 + {Target: null.IntFrom(5), Duration: types.NullDurationFrom(4 * time.Second)}, // 11 + {Target: null.IntFrom(1), Duration: types.NullDurationFrom(4 * time.Second)}, // 15 + {Target: null.IntFrom(4), Duration: types.NullDurationFrom(3 * time.Second)}, // 18 + {Target: null.IntFrom(4), Duration: types.NullDurationFrom(2 * time.Second)}, // 20 + {Target: null.IntFrom(1), Duration: types.NullDurationFrom(0 * time.Second)}, // 20 + {Target: null.IntFrom(1), Duration: types.NullDurationFrom(3 * time.Second)}, // 23 } expRawStepsNoZeroEnd := []lib.ExecutionStep{ @@ -469,11 +469,12 @@ func TestRampingVUsConfigExecutionPlanExample(t *testing.T) { {TimeOffset: 17 * time.Second, PlannedVUs: 3}, {TimeOffset: 18 * time.Second, PlannedVUs: 4}, {TimeOffset: 20 * time.Second, PlannedVUs: 1}, + {TimeOffset: 23 * time.Second, PlannedVUs: 1}, } rawStepsNoZeroEnd := conf.getRawExecutionSteps(et, false) assert.Equal(t, expRawStepsNoZeroEnd, rawStepsNoZeroEnd) endOffset, isFinal := lib.GetEndOffset(rawStepsNoZeroEnd) - assert.Equal(t, 20*time.Second, endOffset) + assert.Equal(t, 23*time.Second, endOffset) assert.Equal(t, false, isFinal) rawStepsZeroEnd := conf.getRawExecutionSteps(et, true) @@ -559,13 +560,13 @@ func TestRampingVUsConfigExecutionPlanExampleOneThird(t *testing.T) { {TimeOffset: 15 * time.Second, PlannedVUs: 0}, {TimeOffset: 16 * time.Second, PlannedVUs: 1}, {TimeOffset: 20 * time.Second, PlannedVUs: 0}, + {TimeOffset: 23 * time.Second, PlannedVUs: 0}, } expRawStepsZeroEnd := expRawStepsNoZeroEnd // no need to copy - expRawStepsZeroEnd = append(expRawStepsZeroEnd, lib.ExecutionStep{TimeOffset: 23 * time.Second, PlannedVUs: 0}) rawStepsNoZeroEnd := conf.getRawExecutionSteps(et, false) assert.Equal(t, expRawStepsNoZeroEnd, rawStepsNoZeroEnd) endOffset, isFinal := lib.GetEndOffset(rawStepsNoZeroEnd) - assert.Equal(t, 20*time.Second, endOffset) + assert.Equal(t, 23*time.Second, endOffset) assert.Equal(t, true, isFinal) rawStepsZeroEnd := conf.getRawExecutionSteps(et, true) @@ -580,7 +581,6 @@ func TestRampingVUsConfigExecutionPlanExampleOneThird(t *testing.T) { {TimeOffset: 1 * time.Second, PlannedVUs: 2}, {TimeOffset: 42 * time.Second, PlannedVUs: 1}, {TimeOffset: 50 * time.Second, PlannedVUs: 0}, - {TimeOffset: 53 * time.Second, PlannedVUs: 0}, }, conf.GetExecutionRequirements(et)) // Try a longer GracefulStop than the GracefulRampDown @@ -590,7 +590,6 @@ func TestRampingVUsConfigExecutionPlanExampleOneThird(t *testing.T) { {TimeOffset: 1 * time.Second, PlannedVUs: 2}, {TimeOffset: 42 * time.Second, PlannedVUs: 1}, {TimeOffset: 50 * time.Second, PlannedVUs: 0}, - {TimeOffset: 103 * time.Second, PlannedVUs: 0}, }, conf.GetExecutionRequirements(et)) // Try a much shorter GracefulStop than the GracefulRampDown @@ -611,7 +610,7 @@ func TestRampingVUsConfigExecutionPlanExampleOneThird(t *testing.T) { // Try a zero GracefulStop and GracefulRampDown, i.e. raw steps with 0 end cap conf.GracefulRampDown = types.NullDurationFrom(0 * time.Second) - assert.Equal(t, rawStepsZeroEnd, conf.GetExecutionRequirements(et)) + assert.Equal(t, expRawStepsZeroEnd, conf.GetExecutionRequirements(et)) } func TestRampingVUsConfigExecutionPlanZerosAtEnd(t *testing.T) { @@ -637,13 +636,13 @@ func TestRampingVUsConfigExecutionPlanZerosAtEnd(t *testing.T) { {TimeOffset: 7 * time.Second, PlannedVUs: 2}, {TimeOffset: 8 * time.Second, PlannedVUs: 1}, {TimeOffset: 9 * time.Second, PlannedVUs: 0}, + {TimeOffset: 14 * time.Second, PlannedVUs: 0}, } expRawStepsZeroEnd := expRawStepsNoZeroEnd // no need to copy - expRawStepsZeroEnd = append(expRawStepsZeroEnd, lib.ExecutionStep{TimeOffset: 14 * time.Second, PlannedVUs: 0}) rawStepsNoZeroEnd := conf.getRawExecutionSteps(et, false) assert.Equal(t, expRawStepsNoZeroEnd, rawStepsNoZeroEnd) endOffset, isFinal := lib.GetEndOffset(rawStepsNoZeroEnd) - assert.Equal(t, 9*time.Second, endOffset) + assert.Equal(t, 14*time.Second, endOffset) assert.Equal(t, true, isFinal) rawStepsZeroEnd := conf.getRawExecutionSteps(et, true) diff --git a/lib/executors.go b/lib/executors.go index f0d39afa4f7..15e17cb2b39 100644 --- a/lib/executors.go +++ b/lib/executors.go @@ -303,7 +303,8 @@ func (scs ScenarioConfigs) GetFullExecutionRequirements(et *ExecutionTuple) []Ex stepsLen := len(consolidatedSteps) if stepsLen == 0 || consolidatedSteps[stepsLen-1].PlannedVUs != newPlannedVUs || - consolidatedSteps[stepsLen-1].MaxUnplannedVUs != newMaxUnplannedVUs { + consolidatedSteps[stepsLen-1].MaxUnplannedVUs != newMaxUnplannedVUs || + consolidatedSteps[stepsLen-1].TimeOffset != currentTimeOffset { consolidatedSteps = append(consolidatedSteps, ExecutionStep{ TimeOffset: currentTimeOffset, PlannedVUs: newPlannedVUs,