mirror of
https://github.com/mentos1386/zdravko.git
synced 2024-11-26 09:17:55 +00:00
535 lines
16 KiB
Go
535 lines
16 KiB
Go
package k6
|
|
|
|
import (
|
|
"context"
|
|
"crypto/x509"
|
|
"fmt"
|
|
"io"
|
|
"net/url"
|
|
"strings"
|
|
"sync"
|
|
"time"
|
|
|
|
"github.com/sirupsen/logrus"
|
|
"go.k6.io/k6/cmd/state"
|
|
"go.k6.io/k6/errext"
|
|
"go.k6.io/k6/errext/exitcodes"
|
|
"go.k6.io/k6/event"
|
|
"go.k6.io/k6/execution"
|
|
"go.k6.io/k6/execution/local"
|
|
"go.k6.io/k6/js"
|
|
"go.k6.io/k6/js/common"
|
|
"go.k6.io/k6/js/modules"
|
|
"go.k6.io/k6/lib"
|
|
"go.k6.io/k6/lib/executor"
|
|
"go.k6.io/k6/lib/fsext"
|
|
"go.k6.io/k6/lib/trace"
|
|
"go.k6.io/k6/lib/types"
|
|
"go.k6.io/k6/loader"
|
|
"go.k6.io/k6/metrics"
|
|
"go.k6.io/k6/metrics/engine"
|
|
"go.k6.io/k6/output"
|
|
)
|
|
|
|
const (
|
|
testTypeJS = "js"
|
|
)
|
|
|
|
const (
|
|
// We use an excessively high timeout to wait for event processing to complete,
|
|
// since prematurely proceeding before it is done could create bigger problems.
|
|
// In practice, this effectively acts as no timeout, and the user will have to
|
|
// kill k6 if a hang happens, which is the behavior without events anyway.
|
|
waitEventDoneTimeout = 30 * time.Minute
|
|
|
|
// This timeout should be long enough to flush all remaining traces, but still
|
|
// provides a safeguard to not block indefinitely.
|
|
waitForTracerProviderStopTimeout = 3 * time.Minute
|
|
)
|
|
|
|
// loadedTest contains all of data, details and dependencies of a loaded
|
|
// k6 test, but without any config consolidation.
|
|
type loadedTest struct {
|
|
sourceRootPath string // contains the raw string the user supplied
|
|
pwd string
|
|
source *loader.SourceData
|
|
fs fsext.Fs
|
|
fileSystems map[string]fsext.Fs
|
|
preInitState *lib.TestPreInitState
|
|
initRunner lib.Runner // TODO: rename to something more appropriate
|
|
keyLogger io.Closer
|
|
moduleResolver *modules.ModuleResolver
|
|
}
|
|
|
|
func loadLocalTest(gs *state.GlobalState, script string) (*loadedTest, error) {
|
|
src, fileSystems, pwd, err := readSource(gs, script)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
resolvedPath := src.URL.String()
|
|
gs.Logger.Debugf(
|
|
"successfully loaded %d bytes!",
|
|
len(src.Data),
|
|
)
|
|
|
|
gs.Logger.Debugf("Gathering k6 runtime options...")
|
|
runtimeOptions := lib.RuntimeOptions{}
|
|
|
|
registry := metrics.NewRegistry()
|
|
state := &lib.TestPreInitState{
|
|
Logger: gs.Logger,
|
|
RuntimeOptions: runtimeOptions,
|
|
Registry: registry,
|
|
BuiltinMetrics: metrics.RegisterBuiltinMetrics(registry),
|
|
Events: gs.Events,
|
|
LookupEnv: func(key string) (string, bool) {
|
|
val, ok := gs.Env[key]
|
|
return val, ok
|
|
},
|
|
}
|
|
|
|
test := &loadedTest{
|
|
pwd: pwd,
|
|
sourceRootPath: "-",
|
|
source: src,
|
|
fs: gs.FS,
|
|
fileSystems: fileSystems,
|
|
preInitState: state,
|
|
}
|
|
|
|
gs.Logger.Debugf("Initializing k6 runner for (%s)...", resolvedPath)
|
|
if err := test.initializeFirstRunner(gs); err != nil {
|
|
return nil, fmt.Errorf("could not initialize: %w", err)
|
|
}
|
|
gs.Logger.Debug("Runner successfully initialized!")
|
|
return test, nil
|
|
}
|
|
|
|
func (lt *loadedTest) initializeFirstRunner(gs *state.GlobalState) error {
|
|
testPath := lt.source.URL.String()
|
|
logger := gs.Logger.WithField("test_path", testPath)
|
|
|
|
testType := lt.preInitState.RuntimeOptions.TestType.String
|
|
if testType == "" {
|
|
testType = testTypeJS
|
|
}
|
|
|
|
// TODO: k6-cli also has TAR support which might be nice to have.
|
|
switch testType {
|
|
case testTypeJS:
|
|
logger.Debug("Trying to load as a JS test...")
|
|
runner, err := js.New(lt.preInitState, lt.source, lt.fileSystems)
|
|
// TODO: should we use common.UnwrapGojaInterruptedError() here?
|
|
if err != nil {
|
|
return fmt.Errorf("could not load JS test '%s': %w", testPath, err)
|
|
}
|
|
lt.initRunner = runner
|
|
lt.moduleResolver = runner.Bundle.ModuleResolver
|
|
return nil
|
|
default:
|
|
return fmt.Errorf("unknown or unspecified test type '%s' for '%s'", testType, testPath)
|
|
}
|
|
}
|
|
|
|
// readSource is a small wrapper around loader.ReadSource returning
|
|
// result of the load and filesystems map
|
|
func readSource(gs *state.GlobalState, script string) (*loader.SourceData, map[string]fsext.Fs, string, error) {
|
|
data := []byte(script)
|
|
|
|
filesystems := loader.CreateFilesystems(gs.FS)
|
|
|
|
err := fsext.WriteFile(filesystems["file"].(fsext.CacheLayerGetter).GetCachingFs(), "/-", data, 0o644)
|
|
if err != nil {
|
|
return nil, nil, "", fmt.Errorf("caching data read from -: %w", err)
|
|
}
|
|
|
|
return &loader.SourceData{URL: &url.URL{Path: "/-", Scheme: "file"}, Data: data}, filesystems, "/", err
|
|
}
|
|
|
|
func (lt *loadedTest) consolidateDeriveAndValidateConfig(
|
|
gs *state.GlobalState,
|
|
) (*loadedAndConfiguredTest, error) {
|
|
gs.Logger.Debug("Consolidating config layers...")
|
|
|
|
config := lib.Options{}
|
|
|
|
config.Apply(lt.initRunner.GetOptions())
|
|
if config.SystemTags == nil {
|
|
config.SystemTags = &metrics.DefaultSystemTagSet
|
|
}
|
|
if config.SummaryTrendStats == nil {
|
|
config.SummaryTrendStats = lib.DefaultSummaryTrendStats
|
|
}
|
|
defDNS := types.DefaultDNSConfig()
|
|
if !config.DNS.TTL.Valid {
|
|
config.DNS.TTL = defDNS.TTL
|
|
}
|
|
if !config.DNS.Select.Valid {
|
|
config.DNS.Select = defDNS.Select
|
|
}
|
|
if !config.DNS.Policy.Valid {
|
|
config.DNS.Policy = defDNS.Policy
|
|
}
|
|
if !config.SetupTimeout.Valid {
|
|
config.SetupTimeout.Duration = types.Duration(60 * time.Second)
|
|
}
|
|
if !config.TeardownTimeout.Valid {
|
|
config.TeardownTimeout.Duration = types.Duration(60 * time.Second)
|
|
}
|
|
|
|
gs.Logger.Debug("Parsing thresholds and validating config...")
|
|
// Parse the thresholds, only if the --no-threshold flag is not set.
|
|
// If parsing the threshold expressions failed, consider it as an
|
|
// invalid configuration error.
|
|
if !lt.preInitState.RuntimeOptions.NoThresholds.Bool {
|
|
for metricName, thresholdsDefinition := range config.Thresholds {
|
|
err := thresholdsDefinition.Parse()
|
|
if err != nil {
|
|
return nil, errext.WithExitCodeIfNone(err, exitcodes.InvalidConfig)
|
|
}
|
|
|
|
err = thresholdsDefinition.Validate(metricName, lt.preInitState.Registry)
|
|
if err != nil {
|
|
return nil, errext.WithExitCodeIfNone(err, exitcodes.InvalidConfig)
|
|
}
|
|
}
|
|
}
|
|
|
|
config, err := executor.DeriveScenariosFromShortcuts(config, gs.Logger)
|
|
if err == nil {
|
|
errors := config.Validate()
|
|
// FIXME: should show them all.
|
|
if len(errors) > 0 {
|
|
err = errors[0]
|
|
}
|
|
}
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
return &loadedAndConfiguredTest{
|
|
loadedTest: lt,
|
|
config: config,
|
|
}, nil
|
|
}
|
|
|
|
// loadedAndConfiguredTest contains the whole loadedTest, as well as the
|
|
// consolidated test config and the full test run state.
|
|
type loadedAndConfiguredTest struct {
|
|
*loadedTest
|
|
config lib.Options
|
|
}
|
|
|
|
func loadAndConfigureLocalTest(
|
|
gs *state.GlobalState,
|
|
script string,
|
|
) (*loadedAndConfiguredTest, error) {
|
|
test, err := loadLocalTest(gs, script)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
return test.consolidateDeriveAndValidateConfig(gs)
|
|
}
|
|
|
|
// loadSystemCertPool attempts to load system certificates.
|
|
func loadSystemCertPool(logger logrus.FieldLogger) {
|
|
if _, err := x509.SystemCertPool(); err != nil {
|
|
logger.WithError(err).Warning("Unable to load system cert pool")
|
|
}
|
|
}
|
|
|
|
func (lct *loadedAndConfiguredTest) buildTestRunState(
|
|
configToReinject lib.Options,
|
|
) (*lib.TestRunState, error) {
|
|
// This might be the full derived or just the consodlidated options
|
|
if err := lct.initRunner.SetOptions(configToReinject); err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
// it pre-loads system certificates to avoid doing it on the first TLS request.
|
|
// This is done async to avoid blocking the rest of the loading process as it will not stop if it fails.
|
|
go loadSystemCertPool(lct.preInitState.Logger)
|
|
|
|
return &lib.TestRunState{
|
|
TestPreInitState: lct.preInitState,
|
|
Runner: lct.initRunner,
|
|
Options: lct.config,
|
|
RunTags: lct.preInitState.Registry.RootTagSet().WithTagsFromMap(configToReinject.RunTags),
|
|
}, nil
|
|
}
|
|
|
|
type Execution struct {
|
|
gs *state.GlobalState
|
|
|
|
// TODO: figure out something more elegant?
|
|
loadConfiguredTest func() (*loadedAndConfiguredTest, execution.Controller, error)
|
|
}
|
|
|
|
func (e *Execution) setupTracerProvider(ctx context.Context, test *loadedAndConfiguredTest) error {
|
|
ro := test.preInitState.RuntimeOptions
|
|
if ro.TracesOutput.String == "" {
|
|
test.preInitState.TracerProvider = trace.NewNoopTracerProvider()
|
|
return nil
|
|
}
|
|
|
|
tp, err := trace.TracerProviderFromConfigLine(ctx, ro.TracesOutput.String)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
test.preInitState.TracerProvider = tp
|
|
|
|
return nil
|
|
}
|
|
|
|
func NewExecution(gs *state.GlobalState, script string) *Execution {
|
|
return &Execution{
|
|
gs: gs,
|
|
loadConfiguredTest: func() (*loadedAndConfiguredTest, execution.Controller, error) {
|
|
test, err := loadAndConfigureLocalTest(gs, script)
|
|
return test, local.NewController(), err
|
|
},
|
|
}
|
|
}
|
|
|
|
func (e *Execution) Start(ctx context.Context) error {
|
|
var err error
|
|
var logger logrus.FieldLogger = logrus.StandardLogger()
|
|
|
|
globalCtx, globalCancel := context.WithCancel(ctx)
|
|
defer globalCancel()
|
|
|
|
// lingerCtx is cancelled by Ctrl+C, and is used to wait for that event when
|
|
// k6 was started with the --linger option.
|
|
lingerCtx, lingerCancel := context.WithCancel(globalCtx)
|
|
defer lingerCancel()
|
|
|
|
// runCtx is used for the test run execution and is created with the special
|
|
// execution.NewTestRunContext() function so that it can be aborted even
|
|
// from sub-contexts while also attaching a reason for the abort.
|
|
runCtx, runAbort := execution.NewTestRunContext(lingerCtx, logger)
|
|
|
|
emitEvent := func(evt *event.Event) func() {
|
|
waitDone := e.gs.Events.Emit(evt)
|
|
return func() {
|
|
waitCtx, waitCancel := context.WithTimeout(globalCtx, waitEventDoneTimeout)
|
|
defer waitCancel()
|
|
if werr := waitDone(waitCtx); werr != nil {
|
|
logger.WithError(werr).Warn()
|
|
}
|
|
}
|
|
}
|
|
|
|
defer func() {
|
|
waitExitDone := emitEvent(&event.Event{
|
|
Type: event.Exit,
|
|
Data: &event.ExitData{Error: err},
|
|
})
|
|
waitExitDone()
|
|
e.gs.Events.UnsubscribeAll()
|
|
}()
|
|
|
|
test, controller, err := e.loadConfiguredTest()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if test.keyLogger != nil {
|
|
defer func() {
|
|
if klErr := test.keyLogger.Close(); klErr != nil {
|
|
logger.WithError(klErr).Warn("Error while closing the SSLKEYLOGFILE")
|
|
}
|
|
}()
|
|
}
|
|
|
|
if err = e.setupTracerProvider(globalCtx, test); err != nil {
|
|
return err
|
|
}
|
|
waitTracesFlushed := func() {
|
|
ctx, cancel := context.WithTimeout(globalCtx, waitForTracerProviderStopTimeout)
|
|
defer cancel()
|
|
if tpErr := test.preInitState.TracerProvider.Shutdown(ctx); tpErr != nil {
|
|
logger.Errorf("The tracer provider didn't stop gracefully: %v", tpErr)
|
|
}
|
|
}
|
|
|
|
// Write the full consolidated *and derived* options back to the Runner.
|
|
conf := test.config
|
|
testRunState, err := test.buildTestRunState(conf)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
// Create a local execution scheduler wrapping the runner.
|
|
logger.Debug("Initializing the execution scheduler...")
|
|
execScheduler, err := execution.NewScheduler(testRunState, controller)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
backgroundProcesses := &sync.WaitGroup{}
|
|
defer backgroundProcesses.Wait()
|
|
|
|
// Create all outputs.
|
|
// executionPlan := execScheduler.GetExecutionPlan()
|
|
outputs := []output.Output{}
|
|
|
|
metricsEngine, err := engine.NewMetricsEngine(testRunState.Registry, logger)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
// We'll need to pipe metrics to the MetricsEngine and process them if any
|
|
// of these are enabled: thresholds, end-of-test summary
|
|
shouldProcessMetrics := (!testRunState.RuntimeOptions.NoSummary.Bool ||
|
|
!testRunState.RuntimeOptions.NoThresholds.Bool)
|
|
var metricsIngester *engine.OutputIngester
|
|
if shouldProcessMetrics {
|
|
err = metricsEngine.InitSubMetricsAndThresholds(conf, testRunState.RuntimeOptions.NoThresholds.Bool)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
// We'll need to pipe metrics to the MetricsEngine if either the
|
|
// thresholds or the end-of-test summary are enabled.
|
|
metricsIngester = metricsEngine.CreateIngester()
|
|
outputs = append(outputs, metricsIngester)
|
|
}
|
|
|
|
executionState := execScheduler.GetState()
|
|
if !testRunState.RuntimeOptions.NoSummary.Bool {
|
|
defer func() {
|
|
logger.Debug("Generating the end-of-test summary...")
|
|
summaryResult, hsErr := test.initRunner.HandleSummary(globalCtx, &lib.Summary{
|
|
Metrics: metricsEngine.ObservedMetrics,
|
|
RootGroup: testRunState.Runner.GetDefaultGroup(),
|
|
TestRunDuration: executionState.GetCurrentTestRunDuration(),
|
|
NoColor: e.gs.Flags.NoColor,
|
|
UIState: lib.UIState{
|
|
IsStdOutTTY: e.gs.Stdout.IsTTY,
|
|
IsStdErrTTY: e.gs.Stderr.IsTTY,
|
|
},
|
|
})
|
|
if hsErr == nil {
|
|
for _, o := range summaryResult {
|
|
_, err := io.Copy(e.gs.Stdout, o)
|
|
if err != nil {
|
|
logger.WithError(err).Error("failed to write summary output")
|
|
}
|
|
}
|
|
}
|
|
if hsErr != nil {
|
|
logger.WithError(hsErr).Error("failed to handle the end-of-test summary")
|
|
}
|
|
}()
|
|
}
|
|
|
|
waitInitDone := emitEvent(&event.Event{Type: event.Init})
|
|
|
|
outputManager := output.NewManager(outputs, logger, func(err error) {
|
|
if err != nil {
|
|
logger.WithError(err).Error("Received error to stop from output")
|
|
}
|
|
// TODO: attach run status and exit code?
|
|
runAbort(err)
|
|
})
|
|
samples := make(chan metrics.SampleContainer, test.config.MetricSamplesBufferSize.Int64)
|
|
waitOutputsFlushed, stopOutputs, err := outputManager.Start(samples)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
defer func() {
|
|
logger.Debug("Stopping outputs...")
|
|
// We call waitOutputsFlushed() below because the threshold calculations
|
|
// need all of the metrics to be sent to the MetricsEngine before we can
|
|
// calculate them one last time. We need the threshold calculated here,
|
|
// since they may change the run status for the outputs.
|
|
stopOutputs(err)
|
|
}()
|
|
|
|
if !testRunState.RuntimeOptions.NoThresholds.Bool {
|
|
finalizeThresholds := metricsEngine.StartThresholdCalculations(
|
|
metricsIngester, runAbort, executionState.GetCurrentTestRunDuration,
|
|
)
|
|
handleFinalThresholdCalculation := func() {
|
|
// This gets called after the Samples channel has been closed and
|
|
// the OutputManager has flushed all of the cached samples to
|
|
// outputs (including MetricsEngine's ingester). So we are sure
|
|
// there won't be any more metrics being sent.
|
|
logger.Debug("Finalizing thresholds...")
|
|
breachedThresholds := finalizeThresholds()
|
|
if len(breachedThresholds) == 0 {
|
|
return
|
|
}
|
|
tErr := errext.WithAbortReasonIfNone(
|
|
errext.WithExitCodeIfNone(
|
|
fmt.Errorf("thresholds on metrics '%s' have been crossed", strings.Join(breachedThresholds, ", ")),
|
|
exitcodes.ThresholdsHaveFailed,
|
|
), errext.AbortedByThresholdsAfterTestEnd)
|
|
|
|
if err == nil {
|
|
err = tErr
|
|
} else {
|
|
logger.WithError(tErr).Debug("Crossed thresholds, but test already exited with another error")
|
|
}
|
|
}
|
|
if finalizeThresholds != nil {
|
|
defer handleFinalThresholdCalculation()
|
|
}
|
|
}
|
|
|
|
defer func() {
|
|
logger.Debug("Waiting for metrics and traces processing to finish...")
|
|
close(samples)
|
|
|
|
ww := [...]func(){
|
|
waitOutputsFlushed,
|
|
waitTracesFlushed,
|
|
}
|
|
var wg sync.WaitGroup
|
|
wg.Add(len(ww))
|
|
for _, w := range ww {
|
|
w := w
|
|
go func() {
|
|
w()
|
|
wg.Done()
|
|
}()
|
|
}
|
|
wg.Wait()
|
|
|
|
logger.Debug("Metrics and traces processing finished!")
|
|
}()
|
|
|
|
// Initialize the VUs and executors
|
|
stopVUEmission, err := execScheduler.Init(runCtx, samples)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
defer stopVUEmission()
|
|
|
|
waitInitDone()
|
|
|
|
waitTestStartDone := emitEvent(&event.Event{Type: event.TestStart})
|
|
waitTestStartDone()
|
|
|
|
// Start the test! However, we won't immediately return if there was an
|
|
// error, we still have things to do.
|
|
err = execScheduler.Run(globalCtx, runCtx, samples)
|
|
|
|
waitTestEndDone := emitEvent(&event.Event{Type: event.TestEnd})
|
|
defer waitTestEndDone()
|
|
|
|
// Check what the execScheduler.Run() error is.
|
|
if err != nil {
|
|
err = common.UnwrapGojaInterruptedError(err)
|
|
logger.WithError(err).Debug("Test finished with an error")
|
|
return err
|
|
}
|
|
|
|
// Warn if no iterations could be completed.
|
|
if executionState.GetFullIterationCount() == 0 {
|
|
logger.Warn("No script iterations fully finished, consider making the test duration longer")
|
|
}
|
|
|
|
logger.Debug("Test finished cleanly")
|
|
|
|
return nil
|
|
}
|