Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 13 additions & 3 deletions cmd/micromize/root.go
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ package main
import (
"context"
_ "embed"
"errors"
"fmt"
"log/slog"
"os"
Expand Down Expand Up @@ -168,12 +169,21 @@ func run(ctx context.Context) error {
})

// Run all gadgets
if err := registry.RunAll(ctx); err != nil {
g, err := registry.RunAll(ctx)
if err != nil {
return fmt.Errorf("running gadgets: %w", err)
}

// Wait for context to be done (which happens on signal)
<-ctx.Done()
// Wait for all gadgets to complete.
// On graceful shutdown (signal), context is canceled and gadgets stop.
// If a gadget fails unexpectedly, errgroup cancels the context, stopping the rest.
if err := g.Wait(); err != nil {
if errors.Is(err, context.Canceled) || errors.Is(err, context.DeadlineExceeded) {
return nil
}
return fmt.Errorf("gadget error: %w", err)
}

return nil
}

Expand Down
39 changes: 29 additions & 10 deletions internal/gadget/registry.go
Original file line number Diff line number Diff line change
Expand Up @@ -17,9 +17,9 @@ package gadget
import (
"context"
"fmt"
"log/slog"

gadgetcontext "github.com/inspektor-gadget/inspektor-gadget/pkg/gadget-context"
"golang.org/x/sync/errgroup"
)

// RuntimeManager defines the interface for running gadgets
Expand Down Expand Up @@ -61,24 +61,43 @@ func (r *Registry) Register(name string, config *GadgetConfig) {
r.gadgets[name] = config
}

// RunAll starts all registered gadgets
func (r *Registry) RunAll(ctx context.Context) error {
// RunAll starts all registered gadgets and returns an errgroup that the caller
// can Wait() on. If any gadget fails, the errgroup's context is canceled,
// signaling other gadgets to stop.
func (r *Registry) RunAll(ctx context.Context) (*errgroup.Group, error) {
g, gCtx := errgroup.WithContext(ctx)

// Pre-pass: create all gadget contexts before starting any goroutines.
// This ensures that if CreateContext fails, no goroutines have been started.
type gadgetEntry struct {
name string
config *GadgetConfig
gadgetCtx *gadgetcontext.GadgetContext
}

entries := make([]gadgetEntry, 0, len(r.gadgets))
for name, config := range r.gadgets {
contextManager := r.defaultContextManager
if config.Context != nil {
contextManager = config.Context
}

gadgetCtx, err := contextManager.CreateContext(ctx, config.Bytes, config.ImageName)
gadgetCtx, err := contextManager.CreateContext(gCtx, config.Bytes, config.ImageName)
if err != nil {
return fmt.Errorf("creating context for gadget %s: %w", name, err)
return nil, fmt.Errorf("creating context for gadget %s: %w", name, err)
}

go func(name string, config *GadgetConfig, gadgetCtx *gadgetcontext.GadgetContext) {
if err := r.runtimeManager.RunGadget(gadgetCtx, config.Params); err != nil {
slog.Error("Error running gadget", "name", name, "error", err)
entries = append(entries, gadgetEntry{name: name, config: config, gadgetCtx: gadgetCtx})
}

for _, e := range entries {
g.Go(func() error {
if err := r.runtimeManager.RunGadget(e.gadgetCtx, e.config.Params); err != nil {
return fmt.Errorf("running gadget %s: %w", e.name, err)
}
}(name, config, gadgetCtx)
return nil
})
Comment on lines +93 to +99
Copy link

Copilot AI Mar 1, 2026

Choose a reason for hiding this comment

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

The goroutine passed to errgroup.Go closes over the loop variable. While this is safe in Go 1.22+ (per-iteration range vars), it’s easy to misread and can become a real bug if this code is ever backported or refactored to reuse the loop variable. Consider shadowing e inside the loop (or passing it as an argument) to make the capture explicit.

Copilot uses AI. Check for mistakes.
}
return nil

return g, nil
}
81 changes: 77 additions & 4 deletions internal/gadget/registry_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -16,9 +16,9 @@ package gadget

import (
"context"
"fmt"
"sync"
"testing"
"time"

gadgetcontext "github.com/inspektor-gadget/inspektor-gadget/pkg/gadget-context"
)
Expand Down Expand Up @@ -80,14 +80,87 @@ func TestRegistry_RunAll(t *testing.T) {
Params: map[string]string{"foo": "bar"},
})

if err := r.RunAll(context.Background()); err != nil {
g, err := r.RunAll(context.Background())
if err != nil {
t.Fatalf("RunAll failed: %v", err)
}

if err := g.Wait(); err != nil {
t.Fatalf("Wait returned error: %v", err)
}

select {
case <-done:
// success
case <-time.After(2 * time.Millisecond):
t.Fatal("timeout waiting for RunGadget")
default:
t.Fatal("RunGadget was never called")
}
}

func TestRegistry_RunAll_ErrorPropagation(t *testing.T) {
mockRuntime := &mockRuntimeManager{
runGadgetFunc: func(gadgetCtx *gadgetcontext.GadgetContext, params map[string]string) error {
return fmt.Errorf("gadget startup failed")
},
}

mockContext := &mockContextCreator{}
r := NewRegistry(mockContext, mockRuntime)

r.Register("failing-gadget", &GadgetConfig{
ImageName: "test-image",
Params: map[string]string{},
})

g, err := r.RunAll(context.Background())
if err != nil {
t.Fatalf("RunAll failed: %v", err)
}

err = g.Wait()
if err == nil {
t.Fatal("expected error from Wait, got nil")
}

expected := "running gadget failing-gadget: gadget startup failed"
if err.Error() != expected {
t.Errorf("expected error %q, got %q", expected, err.Error())
}
Comment on lines +125 to +128
Copy link

Copilot AI Mar 1, 2026

Choose a reason for hiding this comment

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

This test asserts on the full error string, which is brittle (small wording changes in RunAll’s wrapping will break the test while behavior is still correct). Consider asserting with errors.Is / errors.As for the wrapped error, or at least checking that the message contains the gadget name and underlying error text.

Copilot uses AI. Check for mistakes.
}

func TestRegistry_RunAll_ContextCancellation(t *testing.T) {
mockRuntime := &mockRuntimeManager{
runGadgetFunc: func(gadgetCtx *gadgetcontext.GadgetContext, params map[string]string) error {
// Simulate a long-running gadget that respects context cancellation
<-gadgetCtx.Context().Done()
return gadgetCtx.Context().Err()
},
Comment on lines +131 to +137
Copy link

Copilot AI Mar 1, 2026

Choose a reason for hiding this comment

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

The context-cancellation test only covers parent context cancellation. It doesn’t verify the key behavior introduced by errgroup.WithContext: cancellation of other gadgets when one gadget returns a non-cancellation error. Consider adding a test with two gadgets where one returns a real error and the other blocks on ctx.Done(), and assert the second one is unblocked due to errgroup cancellation (and that Wait returns the original gadget error).

Copilot uses AI. Check for mistakes.
}

mockContext := &mockContextCreator{}
r := NewRegistry(mockContext, mockRuntime)

r.Register("long-running", &GadgetConfig{
ImageName: "test-image",
Params: map[string]string{},
})

r.Register("another", &GadgetConfig{
ImageName: "test-image",
Params: map[string]string{},
})

ctx, cancel := context.WithCancel(context.Background())
g, err := r.RunAll(ctx)
if err != nil {
t.Fatalf("RunAll failed: %v", err)
}

// Cancel the context to simulate shutdown signal
cancel()

err = g.Wait()
if err == nil {
t.Fatal("expected error from Wait after cancellation, got nil")
}
}
Loading