diff --git a/js/runner.go b/js/runner.go index 4ff2291023c..515550d4b30 100644 --- a/js/runner.go +++ b/js/runner.go @@ -28,6 +28,7 @@ import ( "net/http" "net/http/cookiejar" "strconv" + "sync" "time" "github.com/dop251/goja" @@ -193,6 +194,7 @@ func (r *Runner) newVU(samplesOut chan<- stats.SampleContainer) (*VU, error) { Console: r.console, BPool: bpool.NewBufferPool(100), Samples: samplesOut, + m: &sync.Mutex{}, } vu.Runtime.Set("console", common.Bind(vu.Runtime, vu.Console, vu.Context)) common.BindToGlobal(vu.Runtime, map[string]interface{}{ @@ -372,6 +374,8 @@ type VU struct { // goroutine per call. interruptTrackedCtx context.Context interruptCancel context.CancelFunc + + m *sync.Mutex } // Verify that VU implements lib.VU @@ -385,6 +389,8 @@ func (u *VU) Reconfigure(id int64) error { } func (u *VU) RunOnce(ctx context.Context) error { + u.m.Lock() + defer u.m.Unlock() // Track the context and interrupt JS execution if it's cancelled. if u.interruptTrackedCtx != ctx { interCtx, interCancel := context.WithCancel(context.Background()) diff --git a/js/runner_test.go b/js/runner_test.go index 2d69ff28d2f..06f76ed787b 100644 --- a/js/runner_test.go +++ b/js/runner_test.go @@ -33,6 +33,7 @@ import ( "net/http" "os" "strings" + "sync" "testing" "time" @@ -534,6 +535,56 @@ func TestVURunInterrupt(t *testing.T) { } } +func TestVURunInterruptDoesntPanic(t *testing.T) { + //TODO: figure out why interrupt sometimes fails... data race in goja? + if isWindows { + t.Skip() + } + + r1, err := getSimpleRunner("/script.js", ` + export default function() { while(true) {} } + `) + require.NoError(t, err) + require.NoError(t, r1.SetOptions(lib.Options{Throw: null.BoolFrom(true)})) + + r2, err := NewFromArchive(r1.MakeArchive(), lib.RuntimeOptions{}) + require.NoError(t, err) + testdata := map[string]*Runner{"Source": r1, "Archive": r2} + for name, r := range testdata { + name, r := name, r + t.Run(name, func(t *testing.T) { + ctx, cancel := context.WithTimeout(context.Background(), 100*time.Minute) + defer cancel() + samples := make(chan stats.SampleContainer, 100) + defer close(samples) + go func() { + for range samples { + } + }() + var wg sync.WaitGroup + + vu, err := r.newVU(samples) + require.NoError(t, err) + for i := 0; i < 1000; i++ { + wg.Add(1) + newCtx, newCancel := context.WithCancel(ctx) + var ch = make(chan struct{}) + go func() { + defer wg.Done() + close(ch) + vuErr := vu.RunOnce(newCtx) + assert.Error(t, vuErr) + assert.Contains(t, vuErr.Error(), "context cancelled") + }() + <-ch + time.Sleep(time.Millisecond * 1) // NOTE: increase this in case of problems ;) + newCancel() + } + wg.Wait() + }) + } +} + func TestVUIntegrationGroups(t *testing.T) { r1, err := getSimpleRunner("/script.js", ` import { group } from "k6";