Hey, I have a question about xk6 extensions. I have one that has an infinite loop sending UDP datagrams. It stops when the context is cancelled. The problem I have is that the context never gets cancelled, even when the VU is killed (i.e. when a http request timeout). In Go, I use the context like so:
func (o *Object) XConstructor(ctxPtr *context.Context) interface{} {
ctx, cancel := context.WithCancel(*ctxPtr)
rt := common.GetRuntime(ctx)
// Then I save this context in my object, so I can cancel it later on
I need the context to be cancelled when the VU is killed, as I can’t always call my method to cancel the context (i.e. when the http request timeout). Is there a way to manage that?
Hi @davidgourde ,
as a quick question( I haven’t looked into this). Do you make the object in the init context (outside of the default
function) or inside it. And if you do change to be inside … does it work?
The object is being created inside the default function()
. Right now, the only way I found to cancel the context is to add a cancelContext()
method on the object and cancel it manually. This doesn’t work for k6 http timeouts though, as I don’t have a kind of “VU level teardown” where to manually call this method. The only other way I see that I could do this right now is to move all the Go code in JavaScript, but I’d like to find a better solution then this, as it would require lots of work.
okay … I did try it, and I was wrong in what I though was the problem:
so I have the following code:
package counter
import (
"context"
"fmt"
"time"
"go.k6.io/k6/js/common"
"go.k6.io/k6/js/modules"
)
func init() {
modules.Register("k6/x/module", New())
}
type (
module struct{}
object struct {
cancel func()
}
)
func (o *module) XConstructor(ctxPtr *context.Context) interface{} {
ctx, cancel := context.WithCancel(*ctxPtr)
rt := common.GetRuntime(ctx)
return common.Bind(rt, &object{cancel: cancel}, ctxPtr)
}
func (o *object) Listen(ctxPtr *context.Context) {
fmt.Println("inside")
go func() {
o.cancel()
}()
select {
case <-(*ctxPtr).Done():
fmt.Println("context pointer got canceled")
case <-time.After(time.Second * 2):
fmt.Println("timeout")
}
}
func New() *module {
return &module{}
}
And no matter what you do you will never have the context pointer got canceled
as the way context.WithCancel
works is that it will always creates a new context that you can cancel but not the original one.
The way to “fix” this is to replace ctxPtr
in the last parameter with &ctx
, but this means that you no longer will get context updates with w/e k6 currently updates it with.
The updates mostly happen between:
- init context and either execution of iterations or setup/teardown/handleSummary
- between iterations.
So as long as you do this only for within an iteration it should work … until we break it (if we do) as the whole common.Bind thing is a PITA.
Hm … I might’ve misread your question based on what you wrote in your second comment …
Do you want for the context that the object gets to be canceled due to the VU finishing the iteration? Because that seems to not work as the context doesn’t seem to be canceled between each iteration, which seems like a bug to me
But I will need to look more in depth into it tomorrow.
Do you want for the context that the object gets to be canceled due to the VU finishing the iteration?
Exactly that, yes. So if the VU iteration stops, the goroutines from the extension also stops. Without this, I would need a way to do a VU teardown at the end of any VU iteration (to manually cancel the context), which is not currently possible.
@ned pointed out that at least for now you can do something like
import { sleep } from "k6";
import { Constructor } from "k6/x/module";
import http from "k6/http";
export const options={
throw: true,
}
export default function() {
var b = new Constructor();
try{
b.listen()
http.get("https://test.k6.io", {timeout: 1})
} finally {
b.cancel()
}
sleep(1)
}
and the module code I tested with was
import { sleep } from "k6";
import { Constructor } from "k6/x/module";
import http from "k6/http";
export const options={
throw: true,
}
export default function() {
var b = new Constructor();
try{
b.listen()
http.get("https://test.k6.io", {timeout: 1})
} finally {
b.cancel()
}
sleep(1)
}
Hope this helps you
You can go one step further and actually have a closure in your xk6 module that wraps the JS functions and returns another function:
import { sleep } from "k6";
import { myclosure } from "k6/x/module";
import http from "k6/http";
function jsdefault() {
// do whatever JS things you want here
}
export default myclosure(jsdefault); // myclosure returns another function