Skip to content

feat(js): propagate metric context into timer callbacks - #6342

Merged
mstoykov merged 1 commit into
masterfrom
feature/async-group-timers
Sep 15, 2026
Merged

mstoykov merged 1 commit into
masterfrom
feature/async-group-timers

Conversation

@mstoykov

Copy link
Copy Markdown
Contributor

What?

Stack 3 of 6

Base: async-group-base (stack PR 2, #6341)

Head: feature/async-group-timers

The next PR will target this branch and add k6/websockets listener propagation.

When async-metric-context is enabled, setTimeout() and setInterval() capture the complete tags
and metadata active when their callback is registered. Each callback invocation applies a clone of
that snapshot and restores the context it interrupted afterward.

For repeating intervals, the already-wrapped callback is rearmed so every tick starts from the
original registration snapshot. Mutations made by one tick remain available to Promise work created
by that tick, through the tracker in stack PR 1, but do not leak into the parent or the next tick.

When the feature is disabled, timers use the existing direct callback path without capturing or
applying metric context.

Tests cover one-shot and repeating timers, group-tag propagation, complete tag and metadata
restoration, callback-mutation isolation, and reuse of the interval's registration snapshot. The
Promise tracker from stack PR 1 separately covers async descendants created by a timer callback.

Why?

Timer callbacks are scheduled by k6's timer and event-loop implementation, not as Sobek Promise
reactions. The Promise tracker therefore cannot infer the context in which setTimeout() or
setInterval() was called. Without an explicit snapshot, a timer fires under whatever mutable VU
context happens to be live at dispatch time, which can attribute its checks, custom metrics, and
asynchronous descendants to an unrelated group or set of tags.

Registration time is the stable ownership boundary: it reflects the operation that created the
timer. Dispatch-time capture would preserve the race, while carrying only the group tag would make
custom tags and metadata behave differently from Promise continuations.

Alternatives considered:

  • A general wrapper around every event-loop task would not know which registration logically owns a
    timer and would affect unrelated internal tasks. Capturing in the timer API keeps the ownership rule
    explicit while reusing the common helper from stack PR 1.
  • Capturing a new context whenever an interval rearms would make one tick's mutations leak into later
    ticks. Reusing the original wrapped callback gives intervals repeatable registration-time
    semantics.
  • Implementing this only inside group() would miss timers registered under custom tag or metadata
    contexts and timers created without importing k6.

Checklist

  • I have performed a self-review of my code.
  • I have commented on my code, particularly in hard-to-understand areas.
  • I have added tests for my changes.
  • I have run linter and tests locally (make check) and all pass.

Checklist: Documentation (only for k6 maintainers and if relevant)

Please do not merge this PR until the following items are filled out.

  • I have added the correct milestone and labels to the PR.
  • I have updated the release notes: TBD for the complete experimental stack
  • I have updated or added an issue to the
    k6-documentation: TBD for async metric-context semantics
  • I have updated or added an issue to the
    TypeScript definitions:
    not applicable; timer signatures do not change

Related PR(s)/Issue(s)

@mstoykov mstoykov added this to the v2.3.0 milestone Aug 26, 2026
@mstoykov
mstoykov requested a review from a team as a code owner August 26, 2026 13:03
@mstoykov
mstoykov requested review from joanlopez and szkiba and removed request for a team August 26, 2026 13:03
@mstoykov
mstoykov temporarily deployed to azure-trusted-signing August 26, 2026 13:09 — with GitHub Actions Inactive
@mstoykov
mstoykov temporarily deployed to azure-trusted-signing August 26, 2026 13:11 — with GitHub Actions Inactive

@szkiba szkiba left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM. Good work.

szkiba
szkiba previously approved these changes Sep 11, 2026
@mstoykov
mstoykov force-pushed the feature/async-group-timers branch from 4146bfc to dbe88bc Compare September 15, 2026 07:53
@mstoykov
mstoykov temporarily deployed to azure-trusted-signing September 15, 2026 07:58 — with GitHub Actions Inactive
@mstoykov
mstoykov temporarily deployed to azure-trusted-signing September 15, 2026 07:59 — with GitHub Actions Inactive
@mstoykov
mstoykov temporarily deployed to azure-trusted-signing September 15, 2026 08:00 — with GitHub Actions Inactive
@mstoykov
mstoykov temporarily deployed to azure-trusted-signing September 15, 2026 08:01 — with GitHub Actions Inactive
Base automatically changed from async-group-base to master September 15, 2026 09:12
When async-metric-context is enabled, capture the complete tags and
metadata active when setTimeout or setInterval registers a callback.
Apply a cloned snapshot for each invocation and restore the
interrupted context afterward.

Callback mutations remain visible to asynchronous work created by the
callback but do not leak into its parent or later interval ticks.
Repeating intervals reuse the original registration snapshot when they
rearm.

Timers run outside Sobek promise reactions and therefore require an
explicit snapshot. Disabled runs pass callbacks through the existing
path without allocating or applying metric context.

Tests cover one-shot and repeating timers, complete tag and metadata
restoration, callback-mutation isolation, and reusable interval
snapshots.
@mstoykov
mstoykov force-pushed the feature/async-group-timers branch from dbe88bc to e1ca2be Compare September 15, 2026 09:12
@mstoykov
mstoykov temporarily deployed to azure-trusted-signing September 15, 2026 09:19 — with GitHub Actions Inactive
@mstoykov
mstoykov temporarily deployed to azure-trusted-signing September 15, 2026 09:20 — with GitHub Actions Inactive
@janHildebrandt98
janHildebrandt98 self-requested a review September 15, 2026 10:04
@mstoykov
mstoykov merged commit 0ae3c29 into master Sep 15, 2026
64 checks passed
@mstoykov
mstoykov deleted the feature/async-group-timers branch September 15, 2026 10:13
inancgumus added a commit to grafana/k6-docs that referenced this pull request Sep 15, 2026
Document how to enable asynchronous groups and keep metrics tied to
their operation across promises, timers, and protocol callbacks.

Related: grafana/k6#6340
Related: grafana/k6#6341
Related: grafana/k6#6342
Related: grafana/k6#6343
Related: grafana/k6#6344

This branch was previously deployed

1 inactive deployment
azure-trusted-signing โ€” e1ca2be9 Deployed Sep 15, 2026 by mstoykov via sign-packages #10678
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants