# Go Concurrency Patterns Used in angela This document explains the concurrency patterns angela uses to fetch data from PyPI and OSV.dev in parallel. These are production patterns used at Uber, Google, and Cloudflare — not textbook exercises. --- ## The problem angela needs to query PyPI for every dependency in your `pyproject.toml`. A typical project has 20-50 dependencies. Making those requests sequentially would take 10-25 seconds. Making them all at once would hammer PyPI with 50 simultaneous connections. The solution: **bounded concurrency** — run up to N requests in parallel, queuing the rest. --- ## Pattern 1: errgroup.SetLimit The `golang.org/x/sync/errgroup` package provides the cleanest API for bounded concurrent work in Go. Here's how angela uses it in `internal/pypi/client.go`: ```go g, ctx := errgroup.WithContext(ctx) g.SetLimit(c.maxWorkers) // max 10 concurrent goroutines for _, name := range names { g.Go(func() error { versions, err := c.FetchVersions(ctx, name) mu.Lock() results = append(results, FetchResult{ Name: name, Versions: versions, Err: err, }) mu.Unlock() return nil }) } _ = g.Wait() ``` Key decisions: 1. **`SetLimit(10)`** — caps concurrent HTTP requests. PyPI recommends 5-10. 2. **Always returns nil** — individual package failures are collected in results, not propagated. One failed package shouldn't cancel the others. 3. **Mutex protects shared slice** — `results` is appended to from multiple goroutines. ### Why not channels? A channel-based worker pool would work, but errgroup.SetLimit does the same thing with less code: ```go // Channel approach: ~30 lines of setup jobs := make(chan string, len(names)) results := make(chan FetchResult, len(names)) for range maxWorkers { go func() { for name := range jobs { // ... fetch ... results <- result } }() } // ... send jobs, collect results ... // errgroup approach: ~15 lines g.SetLimit(maxWorkers) for _, name := range names { g.Go(func() error { /* ... */ }) } g.Wait() ``` The errgroup version is shorter, handles context cancellation automatically, and has no channel lifecycle to manage. --- ## Pattern 2: Panic recovery in goroutines Every goroutine angela launches includes panic recovery: ```go g.Go(func() (err error) { defer func() { if r := recover(); r != nil { err = fmt.Errorf("panic fetching %s: %v", name, r) } }() versions, fetchErr := c.FetchVersions(ctx, name) // ... return nil }) ``` **Why this matters**: an unrecovered panic in a goroutine kills the entire process. In a CLI tool, that means the user sees a stack trace instead of a helpful error message. The `defer recover()` converts panics into errors that flow through the normal error path. The named return `(err error)` is essential — it lets the deferred function assign the recovered panic as the return value. --- ## Pattern 3: Context cancellation Every HTTP request in angela uses context: ```go req, err := http.NewRequestWithContext(ctx, http.MethodGet, url, nil) ``` This means: - If the user presses Ctrl+C, pending requests are cancelled - If `errgroup.WithContext` detects an error, remaining work stops - HTTP timeouts are enforced at the transport level The context flows from `cobra.Command.Context()` through `runUpdate()` through `FetchAllVersions()` through `FetchVersions()` to the actual HTTP call. This is the standard Go pattern: context as the first parameter, threaded through the entire call chain. --- ## Pattern 4: Retry with exponential backoff `internal/pypi/client.go` implements retry for transient failures: ```go for attempt := range maxRetries { if attempt > 0 { delay := time.Duration(1<= 500 { lastErr = fmt.Errorf("server error: %d", resp.StatusCode) continue } return resp, nil } ``` Design choices: 1. **Exponential backoff** — `1<