Repository navigation
Description
The following script:
import { Counter } from "k6/metrics";
import { group } from "k6";
const delay = () => { return Promise.resolve(); } // this is just a placeholder for something useful.
let l = new Counter("my coutner")
export default () => {
group("coolgroup", () => {
l.add(1) // nice
delay(1).then(() => {
l.add(1)// not so nice
})
})
}Will not emit "my counter" with "group=coolgroup".
To that the user will need to wrap the inner parts of the then in a group as in
import { Counter } from "k6/metrics";
import { group } from "k6";
const delay = () => { return Promise.resolve(); }
let l = new Counter("my coutner")
export default () => {
group("coolgroup", () => {
l.add(1) // nice
delay(1).then(() => {
group("coolgroup", () => {
l.add(1)// not so nice
});
})
})
}This is because group is kind of
import exec from "k6/execution"
function group (name, fn) {
let oldGroup = exec.vu.tags["group"];
exec.vu.tags["group"] = oldGroup + ":" + name;
try {
fn() // the actual inner function
} finally {
exec.vu.tags["group"] = oldGroup;
}
}As the then "callbacks" get called only after the stack is empty the whole group code would have been executed, resetting the group back to the root name (which is empty). And when the then get's called it will have an empty group.
The "workaround" works as it just calls group again.
A more hacking but ... more useful workaround is
import { Counter } from "k6/metrics";
import { group } from "k6";
const delay = () => { return Promise.resolve(); }
let l = new Counter("my coutner")
let originalThen = Promise.prototype.then;
Promise.prototype.then = function(onResolved, onRejected) {
let o = onResolved
onResolved = function(res) {
group("coolgroup", () => { // this in practice will need to get hte group name
o(res);
})
}
// https://stackoverflow.com/questions/55413715/cant-i-overwrite-the-then-function-of-promise
return originalThen.call(originalThen.call(this, onResolved, onRejected), undefined, error => {
console.log(`upload the error to server: ${error}`);
throw error;
});
}
export default () => {
group("coolgroup", () => {
l.add(1) // nice
delay(1).then(() => {
l.add(1)// not so nice
})
})
}This ... likely has other problems.
Solutions:
I would argue this might be just showing that group is a bad abstraction and should be deprecated. It makes little sense in a lot of the cases.
On the other hand people are using it (albeit sometimes wrongly) and likely will still want to use it once k6 has a lot more async APIs (which return promises).
Some of those can be fixed in the actual go code - it can get the group before returning the promise. Which in practice is needed as getting the group in a goroutine while js code is runnign si racy as group() call can be made, changing it, while the goroutine gets it.
This in no way helps though with later .then calls.
So for a more real example
delay(1).then(() => {
return anotherPromise();
}).then((resultFromAnotherPromise)=>{
return morepromises();
}.then((.....for all of those setting group will currently not work great.
async/await support:
While we might be able to fix some of this for case wher group("name" , async function () {...}) through just doing some magic in group - that will likely only work for the await usages, but not if you then ( 😉 ) call .then or something.
So it might work great for some of the "async" stuff but not for the other.
pure callbacks
If somebody has an API that just have callbacks ... having them support group will be on a case by case callback basis. They will need to make certain to get the group from where the callback is defined and then when making it to set the group ... and then unset it.
API changes
I guess it is possible for the js/eventloop to provide API to ...that, but that will likely be quite involved, but might be the easiest way forward.
Activity
[-]`group` doesn't work with promises [/-][+]`group` doesn't work with async calls well [/+]oleiade commentedon Oct 21, 2022
First and foremost, thanks for putting such a detailed analysis and explanation of the problem together, much appreciated 👍🏻 🙇🏻
I can see the issue clearly too now, and I kind of come to the same conclusion as you: there doesn't seem to be any elegant solution to this. This leads me to believe that any solution we pick would end up turning into some complicated legacy that we would likely regret.
Disclaimer⚠️ Much of the changes I can imagine to even remotely attempt at addressing this issue would end up being breaking changes; that is, changing the current look and feel of
group.Thus here are a bunch of notes and questions that the issue raised for me. I don't expect those to bring solutions, but maybe they open the discussion in ways that helps us move towards an acceptable solution somehow:
groupAPI as behaving/looking like syntactic sugar. If we didn't havegroup, or deprecated it what would be alternative ways of achieving the same functionality? Besides setting tags manually through theexecmodule. Do you have ideas already on that front?groupcallback with some code that tags but I can't see an elegant way to do that either.async/awaitbased solution satisfactory either. With the syntax being syntactic sugar for "put this current thread of execution on pause, do something else, and come back to me when the promise is resolved", I would expect whatever solution we reach to still work with "only/pure" promises, and without the specific syntax.js/eventloop, but my initial gut feeling would be to try to avoid it as much as possible. That sounds like the best way to add some obscure, hard to maintain, implementation detail, deep in the tech stack, and although I'm fine with introducing debt when it's unavoidable, this solution would be my least favorite.Finally, I don't think you've mentioned if you had any preferences toward one or the other solution yet; if you do, I'd love to hear it.
I hope this melting pot of questions, intuitions, and ideas 💡 turns out somehow useful, and maybe sparks new ideas and approaches. Regardless, I'll keep thinking about it and will come back with anything else that might pop up 🙇🏻
mstoykov commentedon Oct 26, 2022
This is mostly why I am leaning to in practice ... deprecating
group()or at least not doing anything to make it more usable with async calls.My other problem here is that in a lot of the cases I would argue group adds more or less no value. And people do seem to expect it to do stuff ... it doesn't. For example #880 and #921 were because people expected that group_duration will be just the time it takes to do the requests. As k6 gets more and more features, there will arguably be more and more stuff that users will expect are automatically added or not to different metrics. I would argue most of those need to be decided on a case by case basis by the user.
This also remind me that ... currently
group()will emit the metricgroup_durationas soon as the internal call ends, but this changes will also need to make it wait until the last async call started from within is done. Whatever that means. This likely is just as hairy as the other problems. This also begs the question - should the time be from whengroupwas started until the last thing ended or somehow measure how long things inside it ... take, but not take into account the time it was waiting for the event loop to be free (for example).An example
Should the above group_duration be 10s longer? Or not take that into consideration. How do we then measure what happens off the main thread (so not in the JS executin time) and do we need to do this for each operation? Some kind of "AddToGroupTime?" API maybe.
Since grafana/k6-docs#210 I have been pretty sure most of the uses of
group()by k6 users have been ... not very useful.On the other hand I have seen users use it nicely and I do agree there is value in it. I am just not certain that can't be done in some other way.
Looking at the group + checks high coupling I feel like the correct approach here will be to make it possible to write better
check()andgroup()in pure JS and then maybe adopt one of those at a later point.Like the Group is not just a metric it is this whole struct and all the things on it and it has a bunch of things directly related to Checks
k6/lib/models.go
Lines 72 to 100 in 0b73cd6
I think I went enough on this point
The thing is that it doesn't happen "inside" it is asynchronous. The whole thing with async calls is that you call them and the program continues running and later at some point you will get your result. In our case we then do somethign with that result and the expectation (from purely visual reading of the code) is that this something will be in the same group.
So the issues is basically that due to callbacks (whether from promises or not) are not executed within the group, they act the same, instead of how users will likely expect.
Which is IMO where we should go and just - let users know that
groupdoesn't work with asynchronous calls (or at least not great ;) ).I guess this means that we need a way to make some kind of similar functionality for async calls 🤷♂
This both needs to work for non promises as well and k6 does not get any notification that a promise was created inside a group. We can probably hack something around
Promise.prototypeagain or even request/make a change to goja.I guess this also again will mean that the
group()will not end until all of those calls end.I am not certain I got what you are saying, but this likely needs to work with non promises as well. See Websocket API example it uses an event API with pure callbacks (that get more than once, so they are called "handlers"). But should if those are defined within a
groupfor them to always be tagged with thegroup? Does that mean that a group will wait until the websocket is done? How do we know this is has happened for other APIs. Aka what should extension developers do to make this work.The only thing that I can think of is that if
group()itself was async function then maybe we can wait for it to finish. But that will be a breaking change.And that likely can be a new API. But I am not really certain if this is going to be a thing we can either way - so maybe that will be more involved. Also, arguably it is blocked on goja having async functions which currently isn't the case.
This is also my feeling.
For example if we "record" the group in RegisterCallback (or more likely the parent call on
modules.VU) but that only works becauseRegisterCallbackis now the only way to get a Callback. But if we implement #2544 it might be that most code will not use it, and it will be a lot harder to figure out how to "record" the group.I guess my proposal at this point is to heavily discourage usage of
groupwith async and try to refactor the internals and expose them in some way ... so something like startGroup, and then endGroup function .... later.But I guess that means that with such an API anything in between will be part of the group. SO maybe even
pauseGroupandresumeGroup.I guess it will be nice if we could see some kind of other API that does this in the wild ...
mstoykov commentedon Jan 19, 2023
edit: the below is no longer true see #2728 (comment)
After a lot of internal discussions in which @sniku was instrumental it was decided that:
asyncfunctions in group and will want to go with async support across the APIawaitbefore and after that call we want the samegroupto be tagged.example with the API from #2825
mstoykov commentedon Jan 26, 2023
After even more discussion it was decided to not support async functions in
groupandcheckat this time and actively return exception to prevent anyone from misusing them.The magical solution of only supporting async/await has both proven to have:
make this solution inconsistent and likely to be confusing when and for what it will tag correctly and or will wait for.
It is also kind of hard to define what a group of request needs to be with async APIs unless you specify per each call.
Given also the fact k6 has very low amount of actually async APIs at this point and the brand new async/await syntax the k6 OSS team thinks its better to explore other possibilities.
For the next release cycle (it is too late for this one unfortunately) we will try to make the group and check API less magical. In practice making it possible for simple JS code to have the same UX as the current special API. This will also enable users and us to experiment more easily with possible APIs.
Disable async function as group and check callback
3 remaining items
inancgumus commentedon Apr 24, 2025
Hi, @rasert, unfortunately, we still don't have a good solution for this.
SebastianStavar commentedon Oct 1, 2025
Wouldn't having something that works with async/await at least be better than nothing?
blog: reframe k6 post around runtime guarantees
feat(js): propagate metric context through promise reactions (#6340)
andrewslotin commentedon Oct 2, 2026
Closing as completed.
The experimental, opt-in
async-metric-contextfeature preserves group tags across Promise continuations and supported callbacks, including timers, WebSocket and gRPC listeners, and browser callbacks and metrics. Enable it with--features=async-metric-context.group()now supports async callbacks when the feature is enabled. Its duration includes the time until the callback’s returned Promise settles; it does not wait for detached asynchronous work. Browser network attribution remains best-effort.Without the feature enabled, the existing behavior remains unchanged.