{"record":{"id":"fc054f85a806f55f","repo":"grafana/k6","slug":"context-is-done-before-all-s-events-were-proces","errorCode":null,"errorMessage":"context is done before all '%s' events were processed","messagePattern":"context is done before all '(.+?)' events were processed","errorType":"console","errorClass":null,"httpStatus":null,"severity":"warning","filePath":"internal/event/system.go","lineNumber":117,"sourceCode":"\t}\n\n\ts.logger.WithFields(logrus.Fields{\n\t\t\"subscribers\": totalSubs,\n\t\t\"event\":       event.Type,\n\t}).Trace(\"Emitted event\")\n\n\treturn func(ctx context.Context) error {\n\t\tvar doneCount int\n\t\tfor {\n\t\t\tif doneCount == totalSubs {\n\t\t\t\tclose(doneCh)\n\t\t\t\treturn nil\n\t\t\t}\n\t\t\tselect {\n\t\t\tcase <-doneCh:\n\t\t\t\tdoneCount++\n\t\t\tcase <-ctx.Done():\n\t\t\t\treturn fmt.Errorf(\"context is done before all '%s' events were processed\", event.Type)\n\t\t\t}\n\t\t}\n\t}\n}\n\n// Unsubscribe closes the Event channel and removes the subscription with ID\n// subID.\nfunc (s *System) Unsubscribe(subID uint64) {\n\ts.subMx.Lock()\n\tdefer s.subMx.Unlock()\n\tvar seen bool\n\tfor _, sub := range s.subscribers {\n\t\tif evtCh, ok := sub[subID]; ok {\n\t\t\tif !seen {\n\t\t\t\tclose(evtCh)\n\t\t\t}\n\t\t\tdelete(sub, subID)\n\t\t\tseen = true","sourceCodeStart":99,"sourceCodeEnd":135,"githubUrl":"https://github.com/grafana/k6/blob/93accf6570dcd306ca5e99cc44c393ee3797761b/internal/event/system.go#L99-L135","documentation":"The internal event system (internal/event/system.go) fans each emitted event out to all subscribers and returns a wait function that blocks until every subscriber has signaled done (doneCount == totalSubs). If the context passed to the wait function is cancelled first, it fails with \"context is done before all '<type>' events were processed\" — typically because the run was aborted (signal, shutdown) while a slow or stuck subscriber was still processing.","triggerScenarios":"Emitting an event (e.g. the execution/wakeup events used by the cloud output) and waiting on the run context, which gets cancelled by an abort; a subscriber that blocks in its handler or never drains its channel so doneCount never reaches totalSubs before ctx.Done() fires.","commonSituations":"Cloud-output runs aborted mid-flight while the event pipeline still has buffered work; embedding k6 as a library and subscribing to the event System without promptly consuming the event channel.","solutions":["Make every subscriber non-blocking: drain channels promptly and always signal done, including on error paths","Give the drain step its own timeout context independent of the run context so aborts don't cut off event processing mid-wait","For embedders, treat this error as 'canceled', log it, and continue shutdown rather than failing hard","If observed with stock k6 cloud runs, report the reproduction at https://github.com/grafana/k6/issues"],"exampleFix":"// before (embedding k6, same context aborts mid-wait)\nwait := events.Emit(ctx, evt)\nreturn wait(ctx)\n// after (independent bounded drain context)\nwait := events.Emit(ctx, evt)\ndrainCtx, cancel := context.WithTimeout(context.Background(), 5*time.Second)\ndefer cancel()\nif err := wait(drainCtx); err != nil {\n\tlog.Printf(\"event drain cancelled: %v\", err) // non-fatal\n}","handlingStrategy":"try-catch","validationCode":"// For embedders: bound the drain wait with a context independent of the run context\ndrainCtx, cancel := context.WithTimeout(context.Background(), 5*time.Second)\ndefer cancel()\nif err := wait(drainCtx); err != nil {\n\tlog.Printf(\"event drain cancelled: %v\", err) // treat as cancellation, not fatal\n}","typeGuard":null,"tryCatchPattern":"Go: after wait(ctx), check errors.Is(err, context.Canceled) || errors.Is(err, context.DeadlineExceeded); treat as a graceful-cancel path — log it and continue shutdown instead of failing the run. Reserve hard failure for subscriber panics.","preventionTips":["Never block inside event subscribers; drain channels promptly and always signal done","Give the event-drain step its own timeout context separate from the run context","Ensure every subscriber returns on error paths so the done count can reach the total","On aborts, allow a short grace period before hard-cancelling the context"],"tags":["events","concurrency","context","internal-api"],"backgroundTag":null,"analyzedSha":"93accf6570dcd306ca5e99cc44c393ee3797761b","analyzedAt":"2026-08-15T21:23:27.118Z","schemaVersion":2},"datasetVersion":"2026-08-15T22:17:37.221Z"}