From 254868aa27c2936831f2d80e5ad16cee4427dd70 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 21 Sep 2026 07:56:08 +0000 Subject: [PATCH] build(deps): bump github.com/olekukonko/errors from 1.2.0 to 1.3.0 Bumps [github.com/olekukonko/errors](https://github.com/olekukonko/errors) from 1.2.0 to 1.3.0. - [Commits](https://github.com/olekukonko/errors/compare/v1.2.0...v1.3.0) --- updated-dependencies: - dependency-name: github.com/olekukonko/errors dependency-version: 1.3.0 dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] --- go.mod | 2 +- go.sum | 4 +- vendor/github.com/olekukonko/errors/README.md | 1748 ++++------------- vendor/github.com/olekukonko/errors/base.go | 178 ++ vendor/github.com/olekukonko/errors/chain.go | 55 +- vendor/github.com/olekukonko/errors/chan.go | 309 +++ vendor/github.com/olekukonko/errors/errors.go | 363 ++-- .../github.com/olekukonko/errors/generic.go | 150 ++ vendor/github.com/olekukonko/errors/group.go | 119 ++ vendor/github.com/olekukonko/errors/helper.go | 27 + vendor/github.com/olekukonko/errors/http.go | 117 ++ .../github.com/olekukonko/errors/inspect.go | 354 ++-- .../olekukonko/errors/multi_error.go | 36 +- vendor/github.com/olekukonko/errors/pool.go | 8 +- .../olekukonko/errors/pool_above_1_24.go | 32 +- .../olekukonko/errors/pool_below_1_24.go | 28 +- vendor/github.com/olekukonko/errors/retry.go | 4 +- .../github.com/olekukonko/errors/sentinel.go | 135 ++ vendor/github.com/olekukonko/errors/utils.go | 69 +- vendor/modules.txt | 2 +- 20 files changed, 1916 insertions(+), 1824 deletions(-) create mode 100644 vendor/github.com/olekukonko/errors/base.go create mode 100644 vendor/github.com/olekukonko/errors/chan.go create mode 100644 vendor/github.com/olekukonko/errors/generic.go create mode 100644 vendor/github.com/olekukonko/errors/group.go create mode 100644 vendor/github.com/olekukonko/errors/http.go create mode 100644 vendor/github.com/olekukonko/errors/sentinel.go diff --git a/go.mod b/go.mod index e984ca4970..d92aef4024 100644 --- a/go.mod +++ b/go.mod @@ -56,7 +56,7 @@ require ( github.com/mohae/deepcopy v0.0.0-20170929034955-c48cc78d4826 github.com/nats-io/nats-server/v2 v2.14.5 github.com/nats-io/nats.go v1.53.1 - github.com/olekukonko/errors v1.2.0 + github.com/olekukonko/errors v1.3.0 github.com/olekukonko/tablewriter v1.1.4 github.com/onsi/ginkgo v1.16.5 github.com/onsi/ginkgo/v2 v2.32.1 diff --git a/go.sum b/go.sum index 5f2f07925c..75ef94ba34 100644 --- a/go.sum +++ b/go.sum @@ -915,8 +915,8 @@ github.com/oklog/run v1.2.0/go.mod h1:mgDbKRSwPhJfesJ4PntqFUbKQRZ50NgmZTSPlFA0YF github.com/oklog/ulid v1.3.1/go.mod h1:CirwcVhetQ6Lv90oh/F+FBtV6XMibvdAFo93nm5qn4U= github.com/olekukonko/cat v0.0.0-20250911104152-50322a0618f6 h1:zrbMGy9YXpIeTnGj4EljqMiZsIcE09mmF8XsD5AYOJc= github.com/olekukonko/cat v0.0.0-20250911104152-50322a0618f6/go.mod h1:rEKTHC9roVVicUIfZK7DYrdIoM0EOr8mK1Hj5s3JjH0= -github.com/olekukonko/errors v1.2.0 h1:10Zcn4GeV59t/EGqJc8fUjtFT/FuUh5bTMzZ1XwmCRo= -github.com/olekukonko/errors v1.2.0/go.mod h1:ppzxA5jBKcO1vIpCXQ9ZqgDh8iwODz6OXIGKU8r5m4Y= +github.com/olekukonko/errors v1.3.0 h1:teJvgLGUEqMzBUms+Dj3/3szNqCG/Jdw9iDbum8fR6U= +github.com/olekukonko/errors v1.3.0/go.mod h1:ppzxA5jBKcO1vIpCXQ9ZqgDh8iwODz6OXIGKU8r5m4Y= github.com/olekukonko/ll v0.1.6 h1:lGVTHO+Qc4Qm+fce/2h2m5y9LvqaW+DCN7xW9hsU3uA= github.com/olekukonko/ll v0.1.6/go.mod h1:NVUmjBb/aCtUpjKk75BhWrOlARz3dqsM+OtszpY4o88= github.com/olekukonko/tablewriter v0.0.5/go.mod h1:hPp6KlRPjbx+hW8ykQs1w3UBbZlj6HuIJcUGPhkA7kY= diff --git a/vendor/github.com/olekukonko/errors/README.md b/vendor/github.com/olekukonko/errors/README.md index 8830486ea0..a2a42e6692 100644 --- a/vendor/github.com/olekukonko/errors/README.md +++ b/vendor/github.com/olekukonko/errors/README.md @@ -1,39 +1,40 @@ -# Enhanced Error Handling for Go with Context, Stack Traces, Monitoring, and More +# errors — production-grade error handling for Go [![Go Reference](https://pkg.go.dev/badge/github.com/olekukonko/errors.svg)](https://pkg.go.dev/github.com/olekukonko/errors) [![Go Report Card](https://goreportcard.com/badge/github.com/olekukonko/errors)](https://goreportcard.com/report/github.com/olekukonko/errors) [![License](https://img.shields.io/badge/license-MIT-blue.svg)](LICENSE) -[![Benchmarks](https://img.shields.io/badge/benchmarks-included-success)](README.md#benchmarks) - -A production-grade error handling library for Go, offering zero-cost abstractions, stack traces, multi-error support, retries, and advanced monitoring through two complementary packages: `errors` (core) and `errmgr` (management). - -## Features - -### `errors` Package (Core) -- **Performance Optimized** - - Optional memory pooling (12 ns/op with pooling) - - Lazy stack trace collection (205 ns/op with stack) - - Small context optimization (≤4 items, 40 ns/op) - - Lock-free configuration reads - -- **Debugging Tools** - - Full stack traces with internal frame filtering - - Error wrapping and chaining - - Structured context attachment - - JSON serialization (662 ns/op) - -- **Advanced Utilities** - - Configurable retry mechanism - - Multi-error aggregation with sampling - - HTTP status code support - - Callback triggers for cleanup or side effects - -### `errmgr` Package (Management) -- **Production Monitoring** - - Error occurrence counting - - Threshold-based alerting - - Categorized metrics - - Predefined error templates +[![Go 1.21+](https://img.shields.io/badge/go-1.21+-blue.svg)](https://golang.org/dl/) + +A feature-complete error handling library for Go. Fully compatible with `errors.Is`, `errors.As`, and `errors.Unwrap`. Optimised for high-throughput systems with object pooling, hybrid context storage, and inlining-immune stack capture. + +--- + +## Contents + +- [Installation](#installation) +- [Package overview](#package-overview) +- [Core — `errors`](#core--errors) + - [Creating errors](#creating-errors) + - [Stack traces](#stack-traces) + - [Context](#context) + - [Wrapping and chaining](#wrapping-and-chaining) + - [Sentinel errors](#sentinel-errors) + - [Type assertions — Is / As](#type-assertions--is--as) + - [Multi-error aggregation](#multi-error-aggregation) + - [Retry](#retry) + - [Chain execution](#chain-execution) + - [Channel utilities and streaming](#channel-utilities-and-streaming) + - [HTTP helpers](#http-helpers) + - [Concurrent group](#concurrent-group) + - [Inspect](#inspect) + - [slog integration](#slog-integration) + - [Pool management](#pool-management) +- [Management — `errmgr`](#management--errmgr) +- [Performance](#performance) +- [Migration guide](#migration-guide) +- [FAQ](#faq) + +--- ## Installation @@ -41,1525 +42,572 @@ A production-grade error handling library for Go, offering zero-cost abstraction go get github.com/olekukonko/errors@latest ``` -## Package Overview - -- **`errors`**: Core error handling with creation, wrapping, context, stack traces, retries, and multi-error support. -- **`errmgr`**: Error management with templates, monitoring, and predefined errors for consistent application use. +Requires Go 1.21 or later. --- -> [!NOTE] -> ✓ added support for `errors.Errorf("user %w not found", errors.New("bob"))` -> ✓ added support for `sequential chain` execution -`` - -## Using the `errors` Package +## Package overview -### Basic Error Creation +| Package | Purpose | +|---|---| +| `errors` | Core error type, wrapping, context, stack traces, retry, chain, multi-error, channel utilities | +| `errmgr` | Parameterised error templates, occurrence monitoring, threshold alerting | -#### Simple Error -```go -package main - -import ( - "fmt" - "github.com/olekukonko/errors" -) +--- -func main() { - // Fast error with no stack trace - err := errors.New("connection failed") - fmt.Println(err) // "connection failed" +## Core — `errors` - // Standard error, no allocation, same speed - stdErr := errors.Std("connection failed") - fmt.Println(stdErr) // "connection failed" -} -``` +### Creating errors -#### Formatted Error ```go -// main.go -package main - -import ( - "fmt" - "github.com/olekukonko/errors" -) +// Fast — no stack trace, 0 allocations with pooling +err := errors.New("connection failed") -func main() { - // Formatted error without stack trace - errNoWrap := errors.Newf("user %s not found", "bob") - fmt.Println(errNoWrap) // Output: "user bob not found" +// Formatted — full fmt verb support including %w +err := errors.Newf("user %s not found", "alice") +err := errors.Errorf("query failed: %w", cause) // alias of Newf - // Standard formatted error, no fmt.Errorf needed (using own pkg) - stdErrNoWrap := errors.Stdf("user %s not found", "bob") - fmt.Println(stdErrNoWrap) // Output: "user bob not found" +// With stack trace +err := errors.Trace("critical issue") +err := errors.Tracef("query %s failed: %w", query, cause) - // Added support for %w (compatible with fmt.Errorf output) - // errors.Errorf is alias of errors.Newf - errWrap := errors.Errorf("user %w not found", errors.New("bob")) - fmt.Println(errWrap) // Output: "user bob not found" +// Named — useful for sentinel-style matching +err := errors.Named("AuthError") - // Standard formatted error for comparison - stdErrWrap := fmt.Errorf("user %w not found", fmt.Errorf("bob")) - fmt.Println(stdErrWrap) // Output: "user bob not found" -} +// Standard library compatible +err := errors.Std("connection failed") // returns plain error +err := errors.Stdf("error %s", "detail") // formatted plain error ``` -#### Error with Stack Trace -```go -package main - -import ( - "fmt" - "github.com/olekukonko/errors" -) - -func main() { - // Create an error with stack trace using Trace - err := errors.Trace("critical issue") - fmt.Println(err) // Output: "critical issue" - fmt.Println(err.Stack()) // Output: e.g., ["main.go:15", "caller.go:42"] - - // Convert basic error to traceable with WithStack - errS := errors.New("critical issue") - errS = errS.WithStack() // Add stack trace and update error - fmt.Println(errS) // Output: "critical issue" - fmt.Println(errS.Stack()) // Output: e.g., ["main.go:19", "caller.go:42"] -} -``` +### Stack traces -#### Named Error ```go -package main +// Capture at creation +err := errors.Trace("critical issue") -import ( - "fmt" - "github.com/olekukonko/errors" -) +// Add to an existing error +err = err.WithStack() -func main() { - // Create a named error with stack trace - err := errors.Named("InputError") - fmt.Println(err.Name()) // Output: "InputError" - fmt.Println(err) // Output: "InputError" +// Read frames +for _, frame := range err.Stack() { + fmt.Println(frame) // "main.go:42 main.main" } -``` - -### Adding Context - -#### Basic Context -```go -package main -import ( - "fmt" - "github.com/olekukonko/errors" -) - -func main() { - // Create an error with context - err := errors.New("processing failed"). - With("id", "123"). - With("attempt", 3). - With("retryable", true) - fmt.Println("Error:", err) // Output: "processing failed" - fmt.Println("Full context:", errors.Context(err)) // Output: map[id:123 attempt:3 retryable:true] +// Lightweight version (file:line only, no function names) +for _, frame := range err.FastStack() { + fmt.Println(frame) } ``` -#### Context with Wrapped Standard Error -```go -package main - -import ( - "fmt" - "github.com/olekukonko/errors" -) +Stack capture is immune to compiler inlining — frames are collected from +the physical call stack and trimmed by slice arithmetic, not by skip count. -func main() { - // Wrap a standard error and add context - err := errors.New("processing failed"). - With("id", "123") - wrapped := fmt.Errorf("wrapped: %w", err) - fmt.Println("Wrapped error:", wrapped) // Output: "wrapped: processing failed" - fmt.Println("Direct context:", errors.Context(wrapped)) // Output: nil - - // Convert to access context - e := errors.Convert(wrapped) - fmt.Println("Converted context:", e.Context()) // Output: map[id:123] -} -``` +### Context -#### Adding Context to Standard Error ```go -package main +err := errors.New("processing failed"). + With("user_id", "123"). + With("attempt", 3). + With("retryable", true) -import ( - "fmt" - "github.com/olekukonko/errors" -) - -func main() { - // Convert a standard error and add context - stdErr := fmt.Errorf("standard error") - converted := errors.Convert(stdErr). - With("source", "legacy"). - With("severity", "high") - fmt.Println("Message:", converted.Error()) // Output: "standard error" - fmt.Println("Context:", converted.Context()) // Output: map[source:legacy severity:high] -} -``` +// Read back +ctx := errors.Context(err) // map[user_id:123 attempt:3 retryable:true] -#### Complex Context -```go -package main +// Check for a key +if err.HasContextKey("user_id") { ... } -import ( - "fmt" - "github.com/olekukonko/errors" -) +// Variadic bulk attach +err.With("k1", v1, "k2", v2) -func main() { - // Create an error with complex context - err := errors.New("database operation failed"). - With("query", "SELECT * FROM users"). - With("params", map[string]interface{}{ - "limit": 100, - "offset": 0, - }). - With("duration_ms", 45.2) - fmt.Println("Complex error context:") - for k, v := range errors.Context(err) { - fmt.Printf("%s: %v (%T)\n", k, v, v) - } - // Output: - // query: SELECT * FROM users (string) - // params: map[limit:100 offset:0] (map[string]interface {}) - // duration_ms: 45.2 (float64) -} +// Semantic helpers +err.WithCode(500) +err.WithCategory("network") +err.WithTimeout() +err.WithRetryable() ``` -### Stack Traces +The first four context items are stored in a fixed-size array (no allocation). +Items beyond four spill to a map. -#### Adding Stack to Any Error -```go -package main +### Wrapping and chaining -import ( - "fmt" - "github.com/olekukonko/errors" -) +```go +lowErr := errors.New("connection timeout").With("server", "db01") +bizErr := errors.New("failed to load user").Wrap(lowErr) +apiErr := errors.Wrapf(bizErr, "request failed: %w", bizErr) -func main() { - // Add stack trace to a standard error - err := fmt.Errorf("basic error") - enhanced := errors.WithStack(err) - fmt.Println("Error with stack:") - fmt.Println("Message:", enhanced.Error()) // Output: "basic error" - fmt.Println("Stack:", enhanced.Stack()) // Output: e.g., "main.go:15" +// Traverse +for i, e := range errors.UnwrapAll(apiErr) { + fmt.Printf("%d. %s\n", i+1, e) } +// 1. request failed: ... +// 2. failed to load user +// 3. connection timeout ``` -#### Chaining with Stack -```go -package main +### Sentinel errors -import ( - "fmt" - "github.com/olekukonko/errors" - "time" -) +`Const` creates a stable, pointer-comparable sentinel safe for package-level variables. -func main() { - // Create an enhanced error and add stack/context - err := errors.New("validation error"). - With("field", "email") - stackErr := errors.WithStack(err). - With("timestamp", time.Now()). - WithCode(500) - fmt.Println("Message:", stackErr.Error()) // Output: "validation error" - fmt.Println("Context:", stackErr.Context()) // Output: map[field:email timestamp:...] - fmt.Println("Stack:") - for _, frame := range stackErr.Stack() { - fmt.Println(frame) - } -} -``` -### Stack Traces with `WithStack()` ```go -package main - -import ( - "fmt" - "github.com/olekukonko/errors" - "math/rand" - "time" +var ( + ErrNotFound = errors.Const("not_found", "resource not found") + ErrForbidden = errors.Const("forbidden", "access denied") ) -func basicFunc() error { - return fmt.Errorf("basic error") -} - -func enhancedFunc() *errors.Error { - return errors.New("enhanced error") -} +// Match anywhere in a chain +if errors.Is(err, ErrNotFound) { ... } -func main() { - // 1. Package-level WithStack - works with ANY error type - err1 := basicFunc() - enhanced1 := errors.WithStack(err1) // Handles basic errors - fmt.Println("Package-level WithStack:") - fmt.Println(enhanced1.Stack()) - - // 2. Method-style WithStack - only for *errors.Error - err2 := enhancedFunc() - enhanced2 := err2.WithStack() // More natural chaining - fmt.Println("\nMethod-style WithStack:") - fmt.Println(enhanced2.Stack()) - - // 3. Combined usage in real-world scenario - result := processData() - if result != nil { - // Use package-level when type is unknown - stackErr := errors.WithStack(result) - - // Then use method-style for chaining - finalErr := stackErr. - With("timestamp", time.Now()). - WithCode(500) - - fmt.Println("\nCombined Usage:") - fmt.Println("Message:", finalErr.Error()) - fmt.Println("Context:", finalErr.Context()) - fmt.Println("Stack:") - for _, frame := range finalErr.Stack() { - fmt.Println(frame) - } - } -} +// Add call-site context without losing the sentinel +err := ErrNotFound.With("user 42 not found") +errors.Is(err, ErrNotFound) // true — sentinel is the cause -func processData() error { - // Could return either basic or enhanced error - if rand.Intn(2) == 0 { - return fmt.Errorf("database error") - } - return errors.New("validation error").With("field", "email") -} +// JSON and slog work automatically +b, _ := json.Marshal(ErrNotFound) // {"error":"resource not found","code":"not_found"} +slog.Error("lookup failed", "err", ErrNotFound) ``` -### Error Wrapping and Chaining +> **`Const` vs `errmgr.Define`** +> `errors.Const` — static comparable value for `errors.Is` matching. +> `errmgr.Define` — parameterised factory that creates new `*Error` instances from a format template. -#### Basic Wrapping -```go -package main - -import ( - "fmt" - "github.com/olekukonko/errors" -) +### Type assertions — Is / As -func main() { - // Wrap an error with additional context - lowErr := errors.New("low-level failure") - highErr := errors.Wrapf(lowErr, "high-level operation failed: %s", "details") - fmt.Println(highErr) // Output: "high-level operation failed: details: low-level failure" - fmt.Println(errors.Unwrap(highErr)) // Output: "low-level failure" -} -``` - -#### Walking Error Chain ```go -package main +// Is — checks identity or name match +err := errors.Named("AuthError") +wrapped := errors.Wrapf(err, "login failed") +errors.Is(wrapped, err) // true -import ( - "fmt" - "github.com/olekukonko/errors" -) - -func main() { - // Create a chained error - dbErr := errors.New("connection timeout"). - With("server", "db01.prod") - bizErr := errors.New("failed to process user 12345"). - With("user_id", "12345"). - Wrap(dbErr) - apiErr := errors.New("API request failed"). - WithCode(500). - Wrap(bizErr) - - // Walk the error chain - fmt.Println("Error Chain:") - for i, e := range errors.UnwrapAll(apiErr) { - fmt.Printf("%d. %s\n", i+1, e) - } - // Output: - // 1. API request failed - // 2. failed to process user 12345 - // 3. connection timeout +// As — extract the first matching *Error from the chain +var target *errors.Error +if errors.As(wrapped, &target) { + fmt.Println(target.Name()) // "AuthError" } -``` -### Type Assertions - -#### Using Is -```go -package main +// Generic helpers (Go 1.18+) +if e, ok := errors.AsType[*MyError](err); ok { ... } +if errors.IsType[*MyError](err) { ... } -import ( - "fmt" - "github.com/olekukonko/errors" -) +found, ok := errors.FindType(err, func(e *MyError) bool { + return e.Code() == 404 +}) -func main() { - // Check error type with Is - err := errors.Named("AuthError") - wrapped := errors.Wrapf(err, "login failed") - if errors.Is(wrapped, err) { - fmt.Println("Is an AuthError") // Output: "Is an AuthError" - } -} +codes := errors.Map(err, func(e *MyError) int { return e.Code() }) +errors.Filter[*MyError](err) // [] *MyError from chain +errors.FirstOfType[*MyError](err) // first *MyError ``` -#### Using As -```go -package main - -import ( - "fmt" - "github.com/olekukonko/errors" -) - -func main() { - // Extract error type with As - err := errors.Named("AuthError") - wrapped := errors.Wrapf(err, "login failed") - var authErr *errors.Error - if wrapped.As(&authErr) { - fmt.Println("Extracted:", authErr.Name()) // Output: "Extracted: AuthError" - } -} -``` +> **`Is()` string-equality note** — `(*Error).Is` falls back to string comparison as a convenience for matching stdlib errors by message. For strict identity matching use `Const()`. -### Retry Mechanism +### Multi-error aggregation -#### Basic Retry ```go -package main +// Basic +m := errors.NewMultiError() +m.Add(errors.New("name required")) +m.Add(errors.New("email invalid")) +fmt.Println(m.Count()) // 2 -import ( - "fmt" - "github.com/olekukonko/errors" - "math/rand" - "time" +// With limits and sampling +m := errors.NewMultiError( + errors.WithLimit(100), + errors.WithSampling(10), // 10% sample rate ) -func main() { - // Simulate a flaky operation - attempts := 0 - retry := errors.NewRetry( - errors.WithMaxAttempts(3), - errors.WithDelay(100*time.Millisecond), - ) - err := retry.Execute(func() error { - attempts++ - if rand.Intn(2) == 0 { - return errors.New("temporary failure").WithRetryable() - } - return nil - }) - if err != nil { - fmt.Printf("Failed after %d attempts: %v\n", attempts, err) - } else { - fmt.Printf("Succeeded after %d attempts\n", attempts) - } -} +// Custom formatter +m := errors.NewMultiError( + errors.WithFormatter(func(errs []error) string { + return fmt.Sprintf("%d errors", len(errs)) + }), +) -``` +// Inspect +m.First() // first error +m.Last() // last error +m.Errors() // []error snapshot +m.Has() // bool +m.Single() // nil | first error | *MultiError -#### Retry with Context Timeout -```go -package main +// Filter +networkErrs := m.Filter(func(e error) bool { + return strings.Contains(e.Error(), "network") +}) -import ( - "context" - "fmt" - "github.com/olekukonko/errors" - "time" -) +// Merge two MultiErrors +m.Merge(other) -func main() { - // Retry with context timeout - ctx, cancel := context.WithTimeout(context.Background(), 500*time.Millisecond) - defer cancel() - retry := errors.NewRetry( - errors.WithContext(ctx), - errors.WithMaxAttempts(5), - errors.WithDelay(200*time.Millisecond), - ) - err := retry.Execute(func() error { - return errors.New("operation failed").WithRetryable() - }) - if errors.Is(err, context.DeadlineExceeded) { - fmt.Println("Operation timed out:", err) - } else if err != nil { - fmt.Println("Operation failed:", err) - } -} +// Join is a convenience that collapses errors to *MultiError or nil +err := errors.Join(err1, err2, err3) ``` - -### Retry Comprehensive +### Retry ```go -package main - -import ( - "context" - "fmt" - "github.com/olekukonko/errors" - "math/rand" - "time" -) - -// DatabaseClient simulates a flaky database connection -type DatabaseClient struct { - healthyAfterAttempt int -} - -func (db *DatabaseClient) Query() error { - if db.healthyAfterAttempt > 0 { - db.healthyAfterAttempt-- - return errors.New("database connection failed"). - With("attempt_remaining", db.healthyAfterAttempt). - WithRetryable() // Mark as retryable - } - return nil -} - -// ExternalService simulates an unreliable external API -func ExternalService() error { - if rand.Intn(100) < 30 { // 30% failure rate - return errors.New("service unavailable"). - WithCode(503). - WithRetryable() - } - return nil -} - -func main() { - // Configure retry with exponential backoff and jitter - retry := errors.NewRetry( +retry := errors.NewRetry( errors.WithMaxAttempts(5), errors.WithDelay(200*time.Millisecond), errors.WithMaxDelay(2*time.Second), errors.WithJitter(true), errors.WithBackoff(errors.ExponentialBackoff{}), + errors.WithRetryIf(errors.IsRetryable), errors.WithOnRetry(func(attempt int, err error) { - // Calculate delay using the same logic as in Execute - baseDelay := 200 * time.Millisecond - maxDelay := 2 * time.Second - delay := errors.ExponentialBackoff{}.Backoff(attempt, baseDelay) - if delay > maxDelay { - delay = maxDelay - } - fmt.Printf("Attempt %d failed: %v (retrying in %v)\n", - attempt, - err.Error(), - delay) + log.Printf("attempt %d: %v", attempt, err) }), - ) - - // Scenario 1: Database connection with known recovery point - db := &DatabaseClient{healthyAfterAttempt: 3} - fmt.Println("Starting database operation...") - err := retry.Execute(func() error { - return db.Query() - }) - if err != nil { - fmt.Printf("Database operation failed after %d attempts: %v\n", retry.Attempts(), err) - } else { - fmt.Println("Database operation succeeded!") - } - - // Scenario 2: External service with random failures - fmt.Println("\nStarting external service call...") - var lastAttempts int - start := time.Now() - - // Using ExecuteReply to demonstrate return values - result, err := errors.ExecuteReply[string](retry, func() (string, error) { - lastAttempts++ - if err := ExternalService(); err != nil { - return "", err - } - return "service response data", nil - }) - - duration := time.Since(start) - - if err != nil { - fmt.Printf("Service call failed after %d attempts (%.2f sec): %v\n", - lastAttempts, - duration.Seconds(), - err) - } else { - fmt.Printf("Service call succeeded after %d attempts (%.2f sec): %s\n", - lastAttempts, - duration.Seconds(), - result) - } - - // Scenario 3: Context cancellation with more visibility - fmt.Println("\nStarting operation with timeout...") - ctx, cancel := context.WithTimeout(context.Background(), 500*time.Millisecond) - defer cancel() - - timeoutRetry := retry.Transform( - errors.WithContext(ctx), - errors.WithMaxAttempts(10), - errors.WithOnRetry(func(attempt int, err error) { - fmt.Printf("Timeout scenario attempt %d: %v\n", attempt, err) - }), - ) - - startTimeout := time.Now() - err = timeoutRetry.Execute(func() error { - time.Sleep(300 * time.Millisecond) // Simulate long operation - return errors.New("operation timed out") - }) - - if errors.Is(err, context.DeadlineExceeded) { - fmt.Printf("Operation cancelled by timeout after %.2f sec: %v\n", - time.Since(startTimeout).Seconds(), - err) - } else if err != nil { - fmt.Printf("Operation failed: %v\n", err) - } else { - fmt.Println("Operation succeeded (unexpected)") - } -} -``` - -### Multi-Error Aggregation - -#### Form Validation -```go -package main - -import ( - "fmt" - "github.com/olekukonko/errors" ) -func main() { - // Validate a form with multiple errors - multi := errors.NewMultiError() - multi.Add(errors.New("name is required")) - multi.Add(errors.New("email is invalid")) - multi.Add(errors.New("password too short")) - if multi.Has() { - fmt.Println(multi) // Output: "errors(3): name is required; email is invalid; password too short" - fmt.Printf("Total errors: %d\n", multi.Count()) - } -} -``` +err := retry.Execute(func() error { + return callExternalService() +}) -#### Sampling Multi-Errors -```go -package main +// Generic version — preserves return value +result, err := errors.ExecuteReply[string](retry, func() (string, error) { + return fetchData() +}) -import ( - "fmt" - "github.com/olekukonko/errors" -) +// Context-aware +ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second) +defer cancel() +retry2 := retry.Transform(errors.WithContext(ctx)) +err = retry2.Execute(fn) -func main() { - // Simulate many errors with sampling - multi := errors.NewMultiError( - errors.WithSampling(10), // 10% sampling - errors.WithLimit(5), - ) - for i := 0; i < 100; i++ { - multi.Add(errors.Newf("error %d", i)) - } - fmt.Println(multi) - fmt.Printf("Captured %d out of 100 errors\n", multi.Count()) -} +// Backoff strategies +errors.ConstantBackoff{} +errors.LinearBackoff{} +errors.ExponentialBackoff{} ``` -### Additional Examples - -#### Using Callbacks -```go -package main - -import ( - "fmt" - "github.com/olekukonko/errors" -) +### Chain execution -func main() { - // Add a callback to an error - err := errors.New("transaction failed"). - Callback(func() { - fmt.Println("Reversing transaction...") - }) - fmt.Println(err) // Output: "transaction failed" + "Reversing transaction..." - err.Free() -} -``` +Sequential steps with per-step retry, timeout, tagging, and optional steps. -#### Copying Errors ```go -package main - -import ( - "fmt" - "github.com/olekukonko/errors" -) +chain := errors.NewChain( + errors.ChainWithTimeout(10*time.Second), + errors.ChainWithLogHandler(slog.Default().Handler()), +). + Step(validateInput).Tag("validation"). + Step(verifyKYC).Tag("kyc"). + Step(processPayment).Tag("billing").Code(402). + Retry(3, 100*time.Millisecond, errors.WithRetryIf(errors.IsRetryable)). + Step(sendNotification).Tag("notification").Optional() -func main() { - // Copy an error and modify the copy - original := errors.New("base error").With("key", "value") - copied := original.Copy().With("extra", "data") - fmt.Println("Original:", original, original.Context()) // Output: "base error" map[key:value] - fmt.Println("Copied:", copied, copied.Context()) // Output: "base error" map[key:value extra:data] +if err := chain.Run(); err != nil { + errors.Inspect(err, os.Stderr) } -``` - -#### Transforming Errors -```go -package main -import ( - "fmt" - "github.com/olekukonko/errors" -) - -func main() { - // Transform an error with additional context - err := errors.New("base error") - transformed := errors.Transform(err, func(e *errors.Error) { - e.With("env", "prod"). - WithCode(500). - WithStack() - }) - fmt.Println(transformed.Error()) // Output: "base error" - fmt.Println(transformed.Context()) // Output: map[env:prod] - fmt.Println(transformed.Code()) // Output: 500 - fmt.Println(len(transformed.Stack()) > 0) // Output: true - transformed.Free() +// Run all steps, collect every error +if err := chain.RunAll(); err != nil { + errors.Inspect(err, os.Stderr) } ``` -### Transformation and Enrichment +`StepCtx` passes the chain-level context (with its deadline) to the step, so +blocking calls like HTTP or database queries respect the chain timeout: ```go -package main - -import ( - "fmt" - "github.com/olekukonko/errors" -) +chain.StepCtx(func(ctx context.Context) error { + req, _ := http.NewRequestWithContext(ctx, "GET", url, nil) + _, err := http.DefaultClient.Do(req) + return err +}) +``` -func process() error { - return errors.New("base error") -} +### Channel utilities and streaming -func main() { - err := process() - transformedErr := errors.Transform(err, func(e *errors.Error) { - e.With("env", "prod"). - WithCode(500). - WithStack() - }) - - // No type assertion needed now - fmt.Println(transformedErr.Error()) // "base error" - fmt.Println(transformedErr.Context()) // map[env:prod] - fmt.Println(transformedErr.Code()) // 500 - fmt.Println(len(transformedErr.Stack()) > 0) // true - transformedErr.Free() // Clean up - - stdErr := process() - convertedErr := errors.Convert(stdErr) // Convert standard error to *Error - convertedErr.With("source", "external"). - WithCode(400). - Callback(func() { - fmt.Println("Converted error processed...") - }) - fmt.Println("Converted Error:", convertedErr.Error()) - fmt.Println("Context:", convertedErr.Context()) - fmt.Println("Code:", convertedErr.Code()) - convertedErr.Free() +#### `<-chan error` utilities -} +These compose with the standard Go `(chan T, chan error)` idiom rather than replacing it. -``` - -#### Fast Stack Trace ```go -package main - -import ( - "fmt" - "github.com/olekukonko/errors" -) +// Drain — block until channel closes, collect into *MultiError +err := errors.Drain(errs) -func main() { - // Get a lightweight stack trace - err := errors.Trace("lightweight error") - fastStack := err.FastStack() - fmt.Println("Fast Stack:") - for _, frame := range fastStack { - fmt.Println(frame) // Output: e.g., "main.go:15" - } +// First — return first non-nil error; ctx for deadline only, caller owns cancel +err := errors.First(ctx, errs) +if err != nil { + cancel() // caller decides to stop siblings } -``` - -#### WarmStackPool -```go -package main -import ( - "fmt" - "github.com/olekukonko/errors" -) +// Collect — bounded sample; wraps ErrLimitReached when n is hit +err := errors.Collect(ctx, errs, 10) +if errors.Is(err, errors.ErrLimitReached) { + log.Warn("more than 10 errors — some dropped") +} -func main() { - // Pre-warm the stack pool - errors.WarmStackPool(10) - err := errors.Trace("pre-warmed error") - fmt.Println("Stack after warming pool:") - for _, frame := range err.Stack() { - fmt.Println(frame) - } +// Fan — merge multiple error channels; caller must drain or cancel to avoid leak +merged := errors.Fan(ctx, validateErrs, enrichErrs) +for err := range merged { + log.Println(err) } ``` -### Multi-Error Aggregation +#### Stream — concurrent item processing ```go -package main - -import ( - "fmt" - "net/mail" - "strings" - "time" - - "github.com/olekukonko/errors" -) +// Process items concurrently, collect all errors +s := errors.NewStream(ctx, urls, func(url string) error { + return fetch(url) +}, 8) // 8 workers; omit for len(items) workers -type UserForm struct { - Name string - Email string - Password string - Birthday string +// Option A — block until done +if err := s.Wait(); err != nil { + errors.Inspect(err, os.Stderr) } -func validateUser(form UserForm) *errors.MultiError { - multi := errors.NewMultiError( - errors.WithLimit(10), - errors.WithFormatter(customFormat), - ) - - // Name validation - if form.Name == "" { - multi.Add(errors.New("name is required")) - } else if len(form.Name) > 50 { - multi.Add(errors.New("name cannot exceed 50 characters")) - } - - // Email validation - if form.Email == "" { - multi.Add(errors.New("email is required")) - } else { - if _, err := mail.ParseAddress(form.Email); err != nil { - multi.Add(errors.New("invalid email format")) - } - if !strings.Contains(form.Email, "@") { - multi.Add(errors.New("email must contain @ symbol")) - } - } - - // Password validation - if len(form.Password) < 8 { - multi.Add(errors.New("password must be at least 8 characters")) - } - if !strings.ContainsAny(form.Password, "0123456789") { - multi.Add(errors.New("password must contain at least one number")) - } - if !strings.ContainsAny(form.Password, "!@#$%^&*") { - multi.Add(errors.New("password must contain at least one special character")) - } - - // Birthday validation - if form.Birthday != "" { - if _, err := time.Parse("2006-01-02", form.Birthday); err != nil { - multi.Add(errors.New("birthday must be in YYYY-MM-DD format")) - } else if bday, _ := time.Parse("2006-01-02", form.Birthday); time.Since(bday).Hours()/24/365 < 13 { - multi.Add(errors.New("must be at least 13 years old")) - } - } +// Option B — process errors as they arrive +s.Each(func(err error) { + log.Println(err) +}) - return multi -} - -func customFormat(errs []error) string { - var sb strings.Builder - sb.WriteString("🚨 Validation Errors:\n") - for i, err := range errs { - sb.WriteString(fmt.Sprintf(" %d. %s\n", i+1, err)) - } - sb.WriteString(fmt.Sprintf("\nTotal issues found: %d\n", len(errs))) - return sb.String() -} - -func main() { - fmt.Println("=== User Registration Validation ===") - - user := UserForm{ - Name: "", // Empty name - Email: "invalid-email", - Password: "weak", - Birthday: "2015-01-01", // Under 13 - } - - // Generate multiple validation errors - validationErrors := validateUser(user) - - if validationErrors.Has() { - fmt.Println(validationErrors) - - // Detailed error analysis - fmt.Println("\n🔍 Error Analysis:") - fmt.Printf("Total errors: %d\n", validationErrors.Count()) - fmt.Printf("First error: %v\n", validationErrors.First()) - fmt.Printf("Last error: %v\n", validationErrors.Last()) - - // Categorized errors with consistent formatting - fmt.Println("\n📋 Error Categories:") - if emailErrors := validationErrors.Filter(contains("email")); emailErrors.Has() { - fmt.Println("Email Issues:") - if emailErrors.Count() == 1 { - fmt.Println(customFormat([]error{emailErrors.First()})) - } else { - fmt.Println(emailErrors) - } - } - if pwErrors := validationErrors.Filter(contains("password")); pwErrors.Has() { - fmt.Println("Password Issues:") - if pwErrors.Count() == 1 { - fmt.Println(customFormat([]error{pwErrors.First()})) - } else { - fmt.Println(pwErrors) - } - } - if ageErrors := validationErrors.Filter(contains("13 years")); ageErrors.Has() { - fmt.Println("Age Restriction:") - if ageErrors.Count() == 1 { - fmt.Println(customFormat([]error{ageErrors.First()})) - } else { - fmt.Println(ageErrors) - } - } - } - - // System Error Aggregation Example - fmt.Println("\n=== System Error Aggregation ===") - systemErrors := errors.NewMultiError( - errors.WithLimit(5), - errors.WithFormatter(systemErrorFormat), - ) - - // Simulate system errors - systemErrors.Add(errors.New("database connection timeout").WithRetryable()) - systemErrors.Add(errors.New("API rate limit exceeded").WithRetryable()) - systemErrors.Add(errors.New("disk space low")) - systemErrors.Add(errors.New("database connection timeout").WithRetryable()) // Duplicate - systemErrors.Add(errors.New("cache miss")) - systemErrors.Add(errors.New("database connection timeout").WithRetryable()) // Over limit - - fmt.Println(systemErrors) - fmt.Printf("\nSystem Status: %d active issues\n", systemErrors.Count()) - - // Filter retryable errors - if retryable := systemErrors.Filter(errors.IsRetryable); retryable.Has() { - fmt.Println("\n🔄 Retryable Errors:") - fmt.Println(retryable) - } -} - -func systemErrorFormat(errs []error) string { - var sb strings.Builder - sb.WriteString("⚠️ System Alerts:\n") - for i, err := range errs { - sb.WriteString(fmt.Sprintf(" %d. %s", i+1, err)) - if errors.IsRetryable(err) { - sb.WriteString(" (retryable)") - } - sb.WriteString("\n") - } - return sb.String() -} - -func contains(substr string) func(error) bool { - return func(err error) bool { - return strings.Contains(err.Error(), substr) - } -} +// Stop early (drains channel to avoid goroutine leak) +s.Stop() ``` -### Chain Execution +`Wait` and `Each` are mutually exclusive. Calling either a second time panics immediately. + +### HTTP helpers -#### Sequential Task Processing ```go -package main +// Resolve HTTP status from an *Error's code +status := errors.HTTPStatusCode(err, http.StatusInternalServerError) + +// Write HTTP error response +errors.HTTPError(w, err) // plain text, status from err.Code() -import ( - "fmt" - "github.com/olekukonko/errors" - "time" +// With options +errors.HTTPError(w, err, + errors.WithFallbackCode(http.StatusBadGateway), + errors.WithBody(false), // header only + errors.WithBodyFunc(func(e error) string { + return fmt.Sprintf(`{"error":%q}`, e.Error()) + }), ) +``` -// validateOrder checks order input. -func validateOrder() error { - return nil // Simulate successful validation -} +### Concurrent group -// processKYC handles payment processing. -func processKYC() error { - return nil // Simulate successful validation -} +`Group` collects all errors from concurrent goroutines — unlike `errgroup` which stops at the first. -// processPayment handles payment processing. -func processPayment() error { - return errors.New("payment declined") // Simulate payment failure -} +```go +g := errors.NewGroup() -// generateInvoice creates an invoice. -func generateInvoice() error { - return errors.New("invoicing unavailable") // Simulate invoicing issue -} +g.Go(func() error { return validateUser(id) }) +g.Go(func() error { return validatePerms(id) }) -// sendNotification sends a confirmation. -func sendNotification() error { - return errors.New("notification failed") // Simulate notification failure +if err := g.Wait(); err != nil { + // err is *MultiError containing every failure + errors.Inspect(err, os.Stderr) } -// processOrder simulates a multi-step order processing workflow. -func processOrder() error { - c := errors.NewChain() - - // Validate order input - c.Step(validateOrder).Tag("validation") - - // KYC Process - c.Step(validateOrder).Tag("validation") - - // Process payment with retries - c.Step(processPayment).Tag("billing").Retry(3, 100*time.Millisecond) - - // Generate invoice - c.Step(generateInvoice).Tag("invoicing") +// Context-aware +g := errors.NewGroup( + errors.GroupWithContext(ctx, true), // cancelOnFirst=true + errors.GroupWithLimit(50), +) - // Send notification (optional) - c.Step(sendNotification).Tag("notification").Optional() +g.GoCtx(func(ctx context.Context) error { + return longRunningCheck(ctx) +}) - return c.Run() -} - -func main() { - if err := processOrder(); err != nil { - // Print error to stderr and exit - errors.Inspect(err) - } - fmt.Println("Order processed successfully") -} +_ = g.Wait() ``` -#### Sequential Task Processing 2 -```go -package main - -import ( - "fmt" - "os" +### Inspect - "github.com/olekukonko/errors" -) +```go +// Default — writes to os.Stderr +errors.Inspect(err) -// validate simulates a validation check that fails. -func validate(name string) error { - return errors.Newf("validation for %s failed", name) -} +// Targeted output +var buf bytes.Buffer +errors.Inspect(err, &buf) -// validateOrder checks order input. -func validateOrder() error { - return nil // Simulate successful validation -} +// Multiple destinations +errors.Inspect(err, os.Stderr, logFile) -// verifyKYC handles Know Your Customer verification. -func verifyKYC(name string) error { - return validate(name) // Simulate KYC validation failure -} +// Options +errors.Inspect(err, os.Stderr, + errors.WithStackFrames(5), + errors.WithMaxDepth(20), +) -// processPayment handles payment processing. -func processPayment() error { - return nil // Simulate successful payment -} +// *Error-specific convenience +errors.InspectError(err, os.Stderr) +``` -// processOrder coordinates the order processing workflow. -func processOrder() error { - chain := errors.NewChain(). - Step(validateOrder). // Step 1: Validate order - Call(verifyKYC, "john"). // Step 2: Verify customer - Step(processPayment) // Step 3: Process payment - - if err := chain.Run(); err != nil { - return errors.Errorf("processing order: %w", err) - } - return nil -} +`Inspect` handles `*Error`, `*MultiError`, and any stdlib error. It writes +to the supplied `io.Writer` values (merged via `io.MultiWriter`) and never +touches stdout. -func main() { - if err := processOrder(); err != nil { - // Print the full error chain to stderr - fmt.Fprintf(os.Stderr, "ERROR: %v\n", err) - // Output - // ERROR: processing order: validation for john failed +### slog integration - // For debugging, you could print the stack trace: - // errors.Inspect(err) - os.Exit(1) - } +Both `*Error` and `*Sentinel` implement `slog.LogValuer`: - fmt.Println("order processed successfully") -} +```go +slog.Error("request failed", "err", err) +// produces structured group: err.message, err.name, err.code, err.category, err.context, err.cause +slog.Error("lookup failed", "err", errors.ErrNotFound) +// produces: err.error="resource not found", err.code="not_found" ``` +### Pool management -#### Retry with Timeout ```go -package main - -import ( - "context" - "fmt" - "github.com/olekukonko/errors" - "time" -) - -func main() { - c := errors.NewChain( - errors.ChainWithTimeout(1*time.Second), - ). - Step(func() error { - time.Sleep(2 * time.Second) - return errors.New("fetch failed") - }). - Tag("api"). - Retry(3, 200*time.Millisecond) - - err := c.Run() - if err != nil { - var deadlineErr error - if errors.As(err, &deadlineErr) && deadlineErr == context.DeadlineExceeded { - fmt.Println("Fetch timed out") - } else { - fmt.Printf("Fetch failed: %v\n", err) - } - return - } - fmt.Println("Fetch succeeded") -} -``` +// Pre-warm (called automatically at init with 100 instances) +errors.WarmPool(1000) +errors.WarmStackPool(500) -#### Collecting All Errors -```go -package main +// Tune global config +errors.Configure(errors.Config{ + StackDepth: 32, + ContextSize: 4, + DisablePooling: false, + FilterInternal: true, + AutoFree: false, // opt-in GC-based pool return +}) -import ( - "fmt" - "github.com/olekukonko/errors" -) +// Explicit pool return (preferred) +err := errors.New("temp") +defer err.Free() -func main() { - c := errors.NewChain( - errors.ChainWithMaxErrors(2), - ). - Step(func() error { return errors.New("task 1 failed") }).Tag("task1"). - Step(func() error { return nil }).Tag("task2"). - Step(func() error { return errors.New("task 3 failed") }).Tag("task3") - - err := c.RunAll() - if err != nil { - errors.Inspect(err) - return - } - fmt.Println("All tasks completed successfully") -} +// Copy without affecting original +copied := err.Copy().With("extra", "data") +// Transform (non-destructive) +enriched := errors.Transform(err, func(e *errors.Error) { + e.WithCode(500).With("env", "prod").WithStack() +}) ``` --- -## Using the `errmgr` Package +## Management — `errmgr` -### Predefined Errors +### Parameterised error templates -#### Static Error ```go -package main - -import ( - "fmt" - "github.com/olekukonko/errors/errmgr" -) +// Define a reusable template +var ErrDBQuery = errmgr.Define("DBQuery", "database query failed: %s") -func main() { - // Use a predefined static error - err := errmgr.ErrNotFound - fmt.Println(err) // Output: "not found" - fmt.Println(err.Code()) // Output: 404 -} +// Instantiate with arguments +err := ErrDBQuery("SELECT timed out") +fmt.Println(err) // "database query failed: SELECT timed out" +fmt.Println(err.Category()) // "database" ``` -#### Templated Error -```go -package main +### Predefined errors -import ( - "fmt" - "github.com/olekukonko/errors/errmgr" -) +```go +err := errmgr.ErrNotFound +fmt.Println(err.Code()) // 404 -func main() { - // Use a templated error with category - err := errmgr.ErrDBQuery("SELECT failed") - fmt.Println(err) // Output: "database query failed: SELECT failed" - fmt.Println(err.Category()) // Output: "database" -} +err := errmgr.ErrDBQuery("SELECT failed") ``` -### Error Monitoring +### Threshold monitoring -#### Basic Monitoring ```go -package main +netErr := errmgr.Define("NetError", "network issue: %s") +monitor := errmgr.NewMonitor("NetError") +errmgr.SetThreshold("NetError", 3) +defer monitor.Close() -import ( - "fmt" - "github.com/olekukonko/errors/errmgr" - "time" -) +go func() { + for alert := range monitor.Alerts() { + fmt.Printf("alert: %s (count: %d)\n", alert, alert.Count()) + } +}() -func main() { - // Define and monitor an error - netErr := errmgr.Define("NetError", "network issue: %s") - monitor := errmgr.NewMonitor("NetError") - errmgr.SetThreshold("NetError", 2) - defer monitor.Close() - - go func() { - for alert := range monitor.Alerts() { - fmt.Printf("Alert: %s, count: %d\n", alert.Error(), alert.Count()) - } - }() - - for i := 0; i < 4; i++ { - err := netErr(fmt.Sprintf("attempt %d", i)) - err.Free() - } - time.Sleep(100 * time.Millisecond) -} +err := netErr("timeout") +err.Free() ``` -#### Realistic Monitoring -```go -package main - -import ( - "fmt" - "github.com/olekukonko/errors" - "github.com/olekukonko/errors/errmgr" - "os" - "os/signal" - "syscall" - "time" -) - -func main() { - // Define our error types - netErr := errmgr.Define("NetError", "network connection failed: %s (attempt %d)") - dbErr := errmgr.Define("DBError", "database operation failed: %s") - - // Create monitors with different buffer sizes - netMonitor := errmgr.NewMonitorBuffered("NetError", 10) // Larger buffer for network errors - dbMonitor := errmgr.NewMonitorBuffered("DBError", 5) // Smaller buffer for DB errors - defer netMonitor.Close() - defer dbMonitor.Close() - - // Set different thresholds - errmgr.SetThreshold("NetError", 3) // Alert after 3 network errors - errmgr.SetThreshold("DBError", 2) // Alert after 2 database errors - - // Set up signal handling for graceful shutdown - sigChan := make(chan os.Signal, 1) - signal.Notify(sigChan, syscall.SIGINT, syscall.SIGTERM) - - // Alert handler goroutine - done := make(chan struct{}) - go func() { - defer close(done) - for { - select { - case alert, ok := <-netMonitor.Alerts(): - if !ok { - fmt.Println("Network alert channel closed") - return - } - handleAlert("NETWORK", alert) - case alert, ok := <-dbMonitor.Alerts(): - if !ok { - fmt.Println("Database alert channel closed") - return - } - handleAlert("DATABASE", alert) - case <-time.After(2 * time.Second): - // Periodic check for shutdown - continue - } - } - }() - - // Simulate operations with potential failures - go func() { - for i := 1; i <= 15; i++ { - // Simulate different error scenarios - if i%4 == 0 { - // Database error - err := dbErr("connection timeout") - fmt.Printf("DB Operation %d: Failed\n", i) - err.Free() - } else { - // Network error - var errMsg string - switch { - case i%3 == 0: - errMsg = "timeout" - case i%5 == 0: - errMsg = "connection reset" - default: - errMsg = "unknown error" - } - - err := netErr(errMsg, i) - fmt.Printf("Network Operation %d: Failed with %q\n", i, errMsg) - err.Free() - } - - // Random delay between operations - time.Sleep(time.Duration(100+(i%200)) * time.Millisecond) - } - }() - - // Wait for shutdown signal or completion - select { - case <-sigChan: - fmt.Println("\nReceived shutdown signal...") - case <-time.After(5 * time.Second): - fmt.Println("Completion timeout reached...") - } - - // Cleanup - fmt.Println("Initiating shutdown...") - netMonitor.Close() - dbMonitor.Close() - - // Wait for the alert handler to finish - select { - case <-done: - fmt.Println("Alert handler shutdown complete") - case <-time.After(1 * time.Second): - fmt.Println("Alert handler shutdown timeout") - } - - fmt.Println("Application shutdown complete") -} - -func handleAlert(service string, alert *errors.Error) { - if alert == nil { - fmt.Printf("[%s] Received nil alert\n", service) - return - } +--- - fmt.Printf("[%s ALERT] %s (total occurrences: %d)\n", - service, alert.Error(), alert.Count()) +Key design decisions: - if alert.Count() > 5 { - fmt.Printf("[%s CRITICAL] High error rate detected!\n", service) - } -} -``` +- **Pool** — `New` and `Wrap` reuse `*Error` instances from `sync.Pool` (12 ns/op, 0 allocs). +- **Hybrid context** — up to 4 key-value pairs in a fixed array; overflow to map. Avoids heap allocation for the common case. +- **Stack capture** — `captureStack` is inlining-immune: it always starts from `runtime.Callers` frame 1 and trims by array slicing, so the compiler's inlining decisions never corrupt the skip count. +- **Pool capacity preservation** — the pool buffer is trimmed in-place (`copy(buf, buf[trimmed:n])`), not re-allocated. Prevents progressive capacity shrinkage under repeated `Free()` cycles. +- **`MarshalJSON`** — bytes are copied out of the pool buffer before returning it, eliminating the race between concurrent JSON serialisations. +- **`With()`** — the mutex is acquired once at entry, eliminating the TOCTOU race in the former optimistic read-then-lock path. --- -## Performance Optimization +## Migration guide -### Configuration Tuning -```go -package main - -import ( - "fmt" - "github.com/olekukonko/errors" -) +### From standard library -func main() { - // Tune error package configuration - errors.Configure(errors.Config{ - StackDepth: 32, // Limit stack frames - ContextSize: 4, // Optimize small contexts - DisablePooling: false, // Enable pooling - }) - err := errors.New("configured error") - fmt.Println(err) // Output: "configured error" -} -``` - -### Using `Free()` for Performance ```go -package main +// Before +err := fmt.Errorf("user %s not found: %w", username, cause) -import ( - "fmt" - "github.com/olekukonko/errors" -) - -func main() { - // Use Free() to return error to pool - err := errors.New("temp error") - fmt.Println(err) // Output: "temp error" - err.Free() // Immediate pool return, reduces GC pressure -} -``` - -### Benchmarks -Real performance data (Apple M3 Pro, Go 1.21): +// After — same output, plus context, code, and chain traversal +err := errors.Newf("user %s not found: %w", username, cause). + With("username", username). + WithCode(404) ``` -goos: darwin -goarch: arm64 -pkg: github.com/olekukonko/errors -cpu: Apple M3 Pro -BenchmarkBasic_New-12 99810412 12.00 ns/op 0 B/op 0 allocs/op -BenchmarkStack_WithStack-12 5879510 205.6 ns/op 24 B/op 1 allocs/op -BenchmarkContext_Small-12 29600850 40.34 ns/op 16 B/op 1 allocs/op -BenchmarkWrapping_Simple-12 100000000 11.73 ns/op 0 B/op 0 allocs/op -``` -- **New with Pooling**: 12 ns/op, 0 allocations -- **WithStack**: 205 ns/op, minimal allocation -- **Context**: 40 ns/op for small contexts -- Run: `go test -bench=. -benchmem` -## Migration Guide +### From `pkg/errors` -### From Standard Library ```go -package main - -import ( - "fmt" - "github.com/olekukonko/errors" -) +// Before +err := pkgerrors.Wrap(cause, "operation failed") -func main() { - // Before: Standard library error - err1 := fmt.Errorf("error: %v", "oops") - fmt.Println(err1) - - // After: Enhanced error with context and stack - err2 := errors.Newf("error: %v", "oops"). - With("source", "api"). - WithStack() - fmt.Println(err2) -} +// After +err := errors.New("operation failed").Wrap(cause).WithStack() ``` -### From `pkg/errors` -```go -package main +### Stdlib `errors.Is` / `errors.As` compatibility -import ( - "fmt" - "github.com/olekukonko/errors" -) - -func main() { - // Before: pkg/errors (assuming similar API) - // err := pkgerrors.Wrap(err, "context") +```go +// Fully compatible — no changes needed +if errors.Is(err, io.EOF) { ... } - // After: Enhanced wrapping - err := errors.New("low-level"). - Msgf("context: %s", "details"). - WithStack() - fmt.Println(err) +var target *errors.Error +if errors.As(err, &target) { + fmt.Println(target.Name()) } ``` -### Compatibility with `errors.Is` and `errors.As` -```go -package main +--- -import ( - "fmt" - "github.com/olekukonko/errors" -) +## FAQ -func main() { - // Check compatibility with standard library - err := errors.Named("MyError") - wrapped := errors.Wrapf(err, "outer") - if errors.Is(wrapped, err) { // Stdlib compatible - fmt.Println("Matches MyError") // Output: "Matches MyError" - } -} -``` +**When should I use `Const` vs `Named`?** +`Const` — package-level sentinel for `errors.Is` matching. Returns the same pointer every call, so pointer equality works. `Named` — creates a new `*Error` instance each call; useful for structured errors with context but not for `==` comparison. -## FAQ +**When should I use `Const` vs `errmgr.Define`?** +`errors.Const("not_found", "resource not found")` creates a static sentinel. `errmgr.Define("DBQuery", "query failed: %s")` creates a parameterised factory — you call it with arguments to produce a new `*Error` each time. -- **When to use `Copy()`?** - - Use ` SOCIALCopy()` to create a modifiable duplicate of an error without altering the original. +**When should I call `Free()`?** +In hot paths where the error is short-lived and you want to return it to the pool immediately. For most application code, letting the GC handle it is fine. If `AutoFree` is enabled in `Config`, the GC returns the error automatically — but `defer err.Free()` is more predictable. -- **When to use `Free()`?** - - Use in performance-critical loops; otherwise, autofree handles it (Go 1.24+). +**Why does `First` not cancel the context?** +`context.Context` is immutable — only `context.WithCancel` produces a cancellable context. `First` accepts `ctx` for deadline support only. The pattern is: call `First`, then call `cancel()` yourself if you want to stop siblings. -- **How to handle cleanup?** - - Use `Callback()` for automatic actions like rollbacks or logging. +**Why do `Each` and `Wait` on `Stream` panic on second call?** +Consuming the same channel twice silently splits errors between two callers. The panic surfaces the bug immediately rather than letting it produce subtly wrong results in production. -- **How to add stack traces later?** - - Use `WithStack()` to upgrade a simple error: - ```go - package main +**How do I debug a deep error chain?** +```go +errors.Inspect(err, os.Stderr, errors.WithMaxDepth(30), errors.WithStackFrames(10)) +``` - import ( - "fmt" - "github.com/olekukonko/errors" - ) +**How do I write to both stderr and a log file?** +```go +errors.Inspect(err, os.Stderr, logFile) // io.MultiWriter internally +``` - func main() { - err := errors.New("simple") - err = err.WithStack() - fmt.Println(err.Stack()) - } - ``` +--- ## Contributing -- Fork, branch, commit, and PR—see [CONTRIBUTING.md](#). + +Fork → branch → commit → PR. Please include tests for new behaviour and run `go test -count=10 -race ./...` before opening a PR. ## License -MIT License - See [LICENSE](LICENSE). + +MIT — see [LICENSE](LICENSE). \ No newline at end of file diff --git a/vendor/github.com/olekukonko/errors/base.go b/vendor/github.com/olekukonko/errors/base.go new file mode 100644 index 0000000000..a44d429f57 --- /dev/null +++ b/vendor/github.com/olekukonko/errors/base.go @@ -0,0 +1,178 @@ +package errors + +import ( + "bytes" + "fmt" + "regexp" + "sync" +) + +// Constants defining default configuration and context keys. +const ( + ctxTimeout = "[error] timeout" // Context key marking timeout errors. + ctxRetry = "[error] retry" // Context key marking retryable errors. + + contextSize = 4 // Initial size of fixed-size context array for small contexts. + bufferSize = 256 // Initial buffer size for JSON marshaling. + warmUpSize = 100 // Number of errors to pre-warm the pool for efficiency. + stackDepth = 32 // Maximum stack trace depth to prevent excessive memory use. + + DefaultCode = 500 // Default HTTP status code for errors if not specified. +) + +// spaceRe is a precompiled regex for normalizing whitespace in error messages. +var spaceRe = regexp.MustCompile(`\s+`) + +// jsonBufferPool manages reusable buffers for JSON marshaling to reduce allocations. +var ( + jsonBufferPool = sync.Pool{ + New: func() interface{} { + return bytes.NewBuffer(make([]byte, 0, bufferSize)) + }, + } +) + +// ErrorCategory is a string type for categorizing errors (e.g., "network", "validation"). +type ErrorCategory string + +// ErrorOpts provides options for customizing error creation. +type ErrorOpts struct { + SkipStack int // Number of stack frames to skip when capturing the stack trace. +} + +// Config defines the global configuration for the errors package, controlling +// stack depth, context size, pooling, and frame filtering. +type Config struct { + StackDepth int // Maximum stack trace depth; 0 uses default (32). + ContextSize int // Initial context map size; 0 uses default (4). + DisablePooling bool // If true, disables object pooling for errors. + FilterInternal bool // If true, filters internal package frames from stack traces. + AutoFree bool // If true, automatically returns errors to pool when GC collects them. +} + +// cachedConfig holds the current configuration, updated only by Configure(). +// Protected by configMu for thread-safety. +type cachedConfig struct { + stackDepth int + contextSize int + disablePooling bool + filterInternal bool + autoFree bool +} + +var ( + // currentConfig stores the active configuration, read frequently and updated rarely. + currentConfig cachedConfig + // configMu protects updates to currentConfig for thread-safety. + configMu sync.RWMutex + // errorPool manages reusable Error instances to reduce allocations. + errorPool = NewErrorPool() + // stackPool manages reusable stack trace slices for efficiency. + stackPool = sync.Pool{ + New: func() interface{} { + return make([]uintptr, currentConfig.stackDepth) + }, + } + // emptyError is a pre-allocated empty error for lightweight reuse. + emptyError = &Error{ + smallContext: [contextSize]contextItem{}, + msg: "", + name: "", + template: "", + cause: nil, + } +) + +// contextItem holds a single key-value pair in the smallContext array. +type contextItem struct { + key string + value interface{} +} + +// init sets up the package with default configuration and pre-warms the error pool. +func init() { + currentConfig = cachedConfig{ + stackDepth: stackDepth, + contextSize: contextSize, + disablePooling: false, + filterInternal: true, + autoFree: false, // opt-in; explicit Free() is the safe default + } + WarmPool(warmUpSize) // Pre-allocate errors for performance. +} + +// Configure updates the global configuration for the errors package. +// It is thread-safe and should be called early to avoid race conditions. +// Changes apply to all subsequent error operations. +// Example: +// +// errors.Configure(errors.Config{StackDepth: 16, DisablePooling: true}) +func Configure(cfg Config) { + configMu.Lock() + defer configMu.Unlock() + + if cfg.StackDepth != 0 { + currentConfig.stackDepth = cfg.StackDepth + } + if cfg.ContextSize != 0 { + currentConfig.contextSize = cfg.ContextSize + } + currentConfig.disablePooling = cfg.DisablePooling + currentConfig.filterInternal = cfg.FilterInternal + currentConfig.autoFree = cfg.AutoFree +} + +// WarmPool pre-populates the error pool with count instances. +// Improves performance by reducing initial allocations. +// No-op if pooling is disabled. +// Example: +// +// errors.WarmPool(1000) +func WarmPool(count int) { + if currentConfig.disablePooling { + return + } + for i := 0; i < count; i++ { + e := &Error{ + smallContext: [contextSize]contextItem{}, + stack: nil, + } + errorPool.Put(e) + stackPool.Put(make([]uintptr, 0, currentConfig.stackDepth)) + } +} + +// WarmStackPool pre-populates the stack pool with count slices. +// Improves performance for stack-intensive operations. +// No-op if pooling is disabled. +// Example: +// +// errors.WarmStackPool(500) +func WarmStackPool(count int) { + if currentConfig.disablePooling { + return + } + for i := 0; i < count; i++ { + stackPool.Put(make([]uintptr, 0, currentConfig.stackDepth)) + } +} + +// FmtErrorCheck safely formats a string using fmt.Sprintf, catching panics. +// Returns the formatted string and any error encountered. +// Internal use by Newf to validate format strings. +// Example: +// +// result, err := FmtErrorCheck("value: %s", "test") +func FmtErrorCheck(format string, args ...interface{}) (result string, err error) { + defer func() { + if r := recover(); r != nil { + if e, ok := r.(error); ok { + err = e + } else { + err = fmt.Errorf("panic during formatting: %v", r) + } + } + }() + result = fmt.Sprintf(format, args...) + return result, nil +} diff --git a/vendor/github.com/olekukonko/errors/chain.go b/vendor/github.com/olekukonko/errors/chain.go index 2bb390ef00..11eb9bb590 100644 --- a/vendor/github.com/olekukonko/errors/chain.go +++ b/vendor/github.com/olekukonko/errors/chain.go @@ -6,6 +6,7 @@ import ( "log/slog" // Standard structured logging package "reflect" "strings" + "sync" "time" ) @@ -18,6 +19,8 @@ type Chain struct { lastStep *chainStep // Pointer to the last added step for configuration logHandler slog.Handler // Optional logging handler (nil means no logging) cancel context.CancelFunc // Function to cancel the context + runCtx context.Context // Active context for Run/RunAll; shared with StepCtx closures + configMu sync.RWMutex // Protects chainConfig against concurrent Timeout() calls } // chainStep represents a single step in the chain. @@ -114,6 +117,44 @@ func (c *Chain) Step(fn func() error) *Chain { return c } +// StepCtx adds a context-aware step to the chain. The provided function +// receives the chain's context (which carries any chain-level deadline/timeout), +// so cancellation and timeouts propagate correctly into blocking operations such +// as HTTP requests, database queries, or gRPC calls. +// +// StepCtx is the context-safe alternative to Step; existing Step calls are +// unchanged and fully compatible. +// +// Example: +// +// chain := NewChain(ChainWithTimeout(5 * time.Second)). +// StepCtx(func(ctx context.Context) error { +// req, _ := http.NewRequestWithContext(ctx, "GET", url, nil) +// _, err := http.DefaultClient.Do(req) +// return err +// }) +func (c *Chain) StepCtx(fn func(ctx context.Context) error) *Chain { + if fn == nil { + panic("Chain.StepCtx: provided function cannot be nil") + } + // Wrap fn so it satisfies the internal func() error signature used by + // executeStep. The context is captured at execution time via getContextAndCancel. + // Close over c.runCtx — set by Run/RunAll to the chain-level context. + // This ensures StepCtx steps share the same deadline as the chain, + // rather than each getting a fresh full-duration context. + wrapped := func() error { + ctx := c.runCtx + if ctx == nil { + ctx = context.Background() + } + return fn(ctx) + } + step := chainStep{execute: wrapped, config: stepConfig{}} + c.steps = append(c.steps, step) + c.lastStep = &c.steps[len(c.steps)-1] + return c +} + // Call adds a step by wrapping a function with arguments. // It uses reflection to validate and invoke the function. func (c *Chain) Call(fn interface{}, args ...interface{}) *Chain { @@ -153,8 +194,11 @@ func (c *Chain) WithLog(attrs ...slog.Attr) *Chain { } // Timeout sets a timeout for the entire chain. +// Thread-safe: protected by configMu. func (c *Chain) Timeout(d time.Duration) *Chain { + c.configMu.Lock() c.config.timeout = d + c.configMu.Unlock() return c } @@ -260,6 +304,7 @@ func (c *Chain) Run() error { ctx, cancel := c.getContextAndCancel() defer cancel() c.cancel = cancel + c.runCtx = ctx // share deadline with StepCtx closures // Clear any previous errors c.errors = c.errors[:0] @@ -309,6 +354,7 @@ func (c *Chain) RunAll() error { ctx, cancel := c.getContextAndCancel() defer cancel() c.cancel = cancel + c.runCtx = ctx // share deadline with StepCtx closures c.errors = c.errors[:0] multi := NewMultiError() @@ -404,11 +450,12 @@ func (c *Chain) Unwrap() []error { // It returns a context and its cancellation function. func (c *Chain) getContextAndCancel() (context.Context, context.CancelFunc) { parentCtx := context.Background() - if c.config.timeout > 0 { - // Create a context with a timeout - return context.WithTimeout(parentCtx, c.config.timeout) + c.configMu.RLock() + timeout := c.config.timeout + c.configMu.RUnlock() + if timeout > 0 { + return context.WithTimeout(parentCtx, timeout) } - // Create a cancellable context return context.WithCancel(parentCtx) } diff --git a/vendor/github.com/olekukonko/errors/chan.go b/vendor/github.com/olekukonko/errors/chan.go new file mode 100644 index 0000000000..7e125abaef --- /dev/null +++ b/vendor/github.com/olekukonko/errors/chan.go @@ -0,0 +1,309 @@ +// Channel-based error utilities and streaming error collection. +// All functions compose with the standard (chan T, chan error) idiom. + +package errors + +import ( + "context" + "fmt" + "sync" + "sync/atomic" +) + +// ErrLimitReached is returned by Collect when n errors have been gathered +// before the channel closed or the context was done. +// Callers can use errors.Is(err, ErrLimitReached) to distinguish this case. +var ErrLimitReached = Const("limit_reached", "error collection limit reached") + +// Drain reads all errors from ch until it is closed and returns them as a +// *MultiError. Returns nil if every received value was nil. +// Blocks until ch is closed. +// +// Example: +// +// results, errs := processItems(ctx, items) +// if err := errors.Drain(errs); err != nil { +// log.Println(err) +// } +func Drain(ch <-chan error) error { + m := NewMultiError() + for err := range ch { + if err != nil { + m.Add(err) + } + } + return m.Single() +} + +// First returns the first non-nil error received from ch, then returns. +// Uses ctx for deadline/cancellation only — it does NOT call any cancel +// function. The caller is responsible for cancelling sibling work after +// First returns. +// +// Returns nil if ch is closed before any error arrives, or ctx is done. +// Returns ctx.Err() if the context is cancelled or times out. +// +// Example: +// +// ctx, cancel := context.WithCancel(context.Background()) +// results, errs := processItems(ctx, items) +// if err := errors.First(ctx, errs); err != nil { +// cancel() // caller decides to stop siblings +// log.Println(err) +// } +// defer cancel() +func First(ctx context.Context, ch <-chan error) error { + for { + select { + case err, ok := <-ch: + if !ok { + return nil + } + if err != nil { + return err + } + case <-ctx.Done(): + return ctx.Err() + } + } +} + +// Fan merges multiple error channels into a single output channel that closes +// when all inputs have closed or ctx is done. +// +// Callers MUST either drain the returned channel to completion OR cancel ctx — +// failing to do so leaks the internal goroutines. The select in each forwarder +// respects ctx.Done() so cancellation is always safe. +// +// Example: +// +// errs1, errs2 := stage1(ctx), stage2(ctx) +// for err := range errors.Fan(ctx, errs1, errs2) { +// if err != nil { log.Println(err) } +// } +func Fan(ctx context.Context, chans ...<-chan error) <-chan error { + bufSize := len(chans) + if bufSize < 1 { + bufSize = 1 + } + + out := make(chan error, bufSize) + var wg sync.WaitGroup + wg.Add(len(chans)) + + for _, ch := range chans { + ch := ch + go func() { + defer wg.Done() + for { + select { + case err, ok := <-ch: + if !ok { + return + } + select { + case out <- err: + case <-ctx.Done(): + return + } + case <-ctx.Done(): + return + } + } + }() + } + + go func() { + wg.Wait() + close(out) + }() + + return out +} + +// Collect reads up to n non-nil errors from ch (or until ch closes or ctx is +// done) and returns them as a *MultiError. +// +// If the limit n is reached before the channel closes, the returned error +// wraps ErrLimitReached as its cause so callers can distinguish the two cases: +// +// err := errors.Collect(ctx, errs, 10) +// if errors.Is(err, errors.ErrLimitReached) { +// // stopped early — more errors may exist +// } +func Collect(ctx context.Context, ch <-chan error, n int) error { + m := NewMultiError(WithLimit(n)) + for { + select { + case err, ok := <-ch: + if !ok { + return m.Single() + } + if err != nil { + m.Add(err) + if m.Count() >= n { + // Wrap in an *Error so errors.Is traversal finds ErrLimitReached. + e := New(fmt.Sprintf("collected %d errors (limit reached)", n)) + e.cause = ErrLimitReached + if inner := m.Single(); inner != nil { + return New(e.Error()).Wrap(inner).Wrap(ErrLimitReached) + } + return e + } + } + case <-ctx.Done(): + return m.Single() + } + } +} + +// Stream — concurrent item processing with progressive error collection + +// Stream processes a slice of items concurrently, collecting errors as they +// occur without stopping execution. Use Wait() to block until all items are +// done, or Each() to process errors as they arrive. +// +// Example — collect all: +// +// s := errors.NewStream(ctx, items, process, 8) +// if err := s.Wait(); err != nil { +// log.Println(err) +// } +// +// Example — process as they arrive: +// +// s := errors.NewStream(ctx, items, process, 8) +// s.Each(func(err error) { log.Println(err) }) +type Stream[T any] struct { + ch chan error + done chan struct{} + stopCh chan struct{} + closeOnce sync.Once + stopOnce sync.Once + + // consumed guards Each/Wait — only one consumer is permitted. + // 0 = available, 1 = consumed. Enforced with atomic CAS. + consumed atomic.Int32 +} + +// NewStream creates a Stream that applies fn to every item in items using +// up to workers concurrent goroutines. +// +// workers <= 0 defaults to len(items), running all items at once. +// Respects ctx: in-flight work completes but no new items start once ctx +// is done. +// +// Example: +// +// s := errors.NewStream(ctx, urls, func(url string) error { +// return fetch(url) +// }, 8) +func NewStream[T any](ctx context.Context, items []T, fn func(T) error, workers ...int) *Stream[T] { + w := len(items) + if len(workers) > 0 && workers[0] > 0 { + w = workers[0] + } + if w < 1 { + w = 1 + } + + s := &Stream[T]{ + ch: make(chan error, w), + done: make(chan struct{}), + stopCh: make(chan struct{}), + } + + go s.run(ctx, items, fn, w) + return s +} + +func (s *Stream[T]) run(ctx context.Context, items []T, fn func(T) error, workers int) { + defer func() { + s.closeOnce.Do(func() { close(s.ch) }) + close(s.done) + }() + + work := make(chan T, workers) + + var wg sync.WaitGroup + for i := 0; i < workers; i++ { + wg.Add(1) + go func() { + defer wg.Done() + for item := range work { + if err := fn(item); err != nil { + select { + case s.ch <- err: + case <-s.stopCh: + return + case <-ctx.Done(): + return + } + } + } + }() + } + +feed: + for _, item := range items { + select { + case work <- item: + case <-s.stopCh: + break feed + case <-ctx.Done(): + break feed + } + } + close(work) + wg.Wait() +} + +// acquireConsumer atomically marks the stream as consumed. +// Panics if called more than once — Each and Wait are mutually exclusive. +func (s *Stream[T]) acquireConsumer(name string) { + if !s.consumed.CompareAndSwap(0, 1) { + panic(fmt.Sprintf("errors.Stream: %s called on an already-consumed Stream; Each and Wait are mutually exclusive", name)) + } +} + +// Each calls fn for every error produced by the stream, in the order they +// arrive. Blocks until all items have been processed. +// +// Panics if called after Wait (or a second call to Each). +func (s *Stream[T]) Each(fn func(error)) { + s.acquireConsumer("Each") + for err := range s.ch { + fn(err) + } + <-s.done +} + +// Wait blocks until all items have been processed and returns a *MultiError +// containing every error collected, or nil if all items succeeded. +// +// Panics if called after Each (or a second call to Wait). +func (s *Stream[T]) Wait() error { + s.acquireConsumer("Wait") + m := NewMultiError() + for err := range s.ch { + m.Add(err) + } + <-s.done + return m.Single() +} + +// Stop signals the stream to stop processing new items and drains any +// buffered errors in the background. Safe to call multiple times. +// After Stop, Wait and Each will still return promptly but may not see +// all errors. +func (s *Stream[T]) Stop() { + s.stopOnce.Do(func() { + close(s.stopCh) + // Drain the error channel in the background so run() goroutines + // are not blocked trying to send, preventing a goroutine leak. + go func() { + for range s.ch { + } + }() + }) +} diff --git a/vendor/github.com/olekukonko/errors/errors.go b/vendor/github.com/olekukonko/errors/errors.go index 6bf3bca6f8..cef216a283 100644 --- a/vendor/github.com/olekukonko/errors/errors.go +++ b/vendor/github.com/olekukonko/errors/errors.go @@ -11,86 +11,13 @@ import ( "encoding/json" "errors" "fmt" - "regexp" + "log/slog" "runtime" "strings" "sync" "sync/atomic" ) -// Constants defining default configuration and context keys. -const ( - ctxTimeout = "[error] timeout" // Context key marking timeout errors. - ctxRetry = "[error] retry" // Context key marking retryable errors. - - contextSize = 4 // Initial size of fixed-size context array for small contexts. - bufferSize = 256 // Initial buffer size for JSON marshaling. - warmUpSize = 100 // Number of errors to pre-warm the pool for efficiency. - stackDepth = 32 // Maximum stack trace depth to prevent excessive memory use. - - DefaultCode = 500 // Default HTTP status code for errors if not specified. -) - -// spaceRe is a precompiled regex for normalizing whitespace in error messages. -var spaceRe = regexp.MustCompile(`\s+`) - -// ErrorCategory is a string type for categorizing errors (e.g., "network", "validation"). -type ErrorCategory string - -// ErrorOpts provides options for customizing error creation. -type ErrorOpts struct { - SkipStack int // Number of stack frames to skip when capturing the stack trace. -} - -// Config defines the global configuration for the errors package, controlling -// stack depth, context size, pooling, and frame filtering. -type Config struct { - StackDepth int // Maximum stack trace depth; 0 uses default (32). - ContextSize int // Initial context map size; 0 uses default (4). - DisablePooling bool // If true, disables object pooling for errors. - FilterInternal bool // If true, filters internal package frames from stack traces. - AutoFree bool // If true, automatically frees errors to pool after use. -} - -// cachedConfig holds the current configuration, updated only by Configure(). -// Protected by configMu for thread-safety. -type cachedConfig struct { - stackDepth int - contextSize int - disablePooling bool - filterInternal bool - autoFree bool -} - -var ( - // currentConfig stores the active configuration, read frequently and updated rarely. - currentConfig cachedConfig - // configMu protects updates to currentConfig for thread-safety. - configMu sync.RWMutex - // errorPool manages reusable Error instances to reduce allocations. - errorPool = NewErrorPool() - // stackPool manages reusable stack trace slices for efficiency. - stackPool = sync.Pool{ - New: func() interface{} { - return make([]uintptr, currentConfig.stackDepth) - }, - } - // emptyError is a pre-allocated empty error for lightweight reuse. - emptyError = &Error{ - smallContext: [contextSize]contextItem{}, - msg: "", - name: "", - template: "", - cause: nil, - } -) - -// contextItem holds a single key-value pair in the smallContext array. -type contextItem struct { - key string - value interface{} -} - // Error is a custom error type with enhanced features: message, name, stack trace, // context, cause, and metadata like code and category. It is thread-safe and // supports pooling for performance. @@ -123,39 +50,6 @@ type Error struct { formatWrapped bool // True if created by Newf with %w verb. } -// init sets up the package with default configuration and pre-warms the error pool. -func init() { - currentConfig = cachedConfig{ - stackDepth: stackDepth, - contextSize: contextSize, - disablePooling: false, - filterInternal: true, - autoFree: true, - } - WarmPool(warmUpSize) // Pre-allocate errors for performance. -} - -// Configure updates the global configuration for the errors package. -// It is thread-safe and should be called early to avoid race conditions. -// Changes apply to all subsequent error operations. -// Example: -// -// errors.Configure(errors.Config{StackDepth: 16, DisablePooling: true}) -func Configure(cfg Config) { - configMu.Lock() - defer configMu.Unlock() - - if cfg.StackDepth != 0 { - currentConfig.stackDepth = cfg.StackDepth - } - if cfg.ContextSize != 0 { - currentConfig.contextSize = cfg.ContextSize - } - currentConfig.disablePooling = cfg.DisablePooling - currentConfig.filterInternal = cfg.FilterInternal - currentConfig.autoFree = cfg.AutoFree -} - // newError creates a new Error instance, reusing from the pool if enabled. // Initializes smallContext and sets stack to nil. // Internal use; prefer New, Named, or Trace for public API. @@ -422,7 +316,6 @@ func Newf(f any, args ...interface{}) *Error { } } // End: Processing Valid Format String - return err } @@ -434,30 +327,11 @@ func Newf(f any, args ...interface{}) *Error { // // err := errors.Errorf("failed: %w", errors.New("cause")) // // err.Error() == "failed: cause" + func Errorf(format string, args ...interface{}) *Error { return Newf(format, args...) } -// FmtErrorCheck safely formats a string using fmt.Sprintf, catching panics. -// Returns the formatted string and any error encountered. -// Internal use by Newf to validate format strings. -// Example: -// -// result, err := FmtErrorCheck("value: %s", "test") -func FmtErrorCheck(format string, args ...interface{}) (result string, err error) { - defer func() { - if r := recover(); r != nil { - if e, ok := r.(error); ok { - err = e - } else { - err = fmt.Errorf("panic during formatting: %v", r) - } - } - }() - result = fmt.Sprintf(format, args...) - return result, nil -} - // Std creates a standard error using errors.New for compatibility. // Does not capture stack traces or add context. // Example: @@ -509,12 +383,20 @@ func (e *Error) As(target interface{}) bool { if e == nil { return false } - // Handle *Error target. - if targetPtr, ok := target.(*Error); ok { + // Handle **Error target (i.e. caller passed &myErrPtr where myErrPtr is *Error). + // Traverse the chain and return the first *Error that has a name; if none has a + // name, return the first *Error in the chain. This satisfies both: + // - TestErrorAs: wraps Named("target") -> finds it by name + // - TestErrorFullChain: finds Named("AuthError") deep in the chain + if targetPtr, ok := target.(**Error); ok { + var first *Error current := e for current != nil { + if first == nil { + first = current + } if current.name != "" { - *targetPtr = *current + *targetPtr = current return true } if next, ok := current.cause.(*Error); ok { @@ -522,9 +404,13 @@ func (e *Error) As(target interface{}) bool { } else if current.cause != nil { return errors.As(current.cause, target) } else { - return false + break } } + if first != nil { + *targetPtr = first + return true + } return false } // Handle *error target. @@ -622,6 +508,8 @@ func (e *Error) Copy() *Error { newErr.code = e.code newErr.category = e.category newErr.count = e.count + newErr.callback = e.callback // was silently dropped by Copy + newErr.formatWrapped = e.formatWrapped // was silently dropped by Copy if e.smallCount > 0 { newErr.smallCount = e.smallCount @@ -716,7 +604,8 @@ func (e *Error) Error() string { // fmt.Println(frame) // e.g., "main.go:42" // } func (e *Error) FastStack() []string { - if e.stack == nil { + // Same len-vs-nil reasoning as Stack(). + if len(e.stack) == 0 { return nil } configMu.RLock() @@ -829,7 +718,8 @@ func (e *Error) contextAtThisLevel() map[string]interface{} { // Free resets the error and returns it to the pool if pooling is enabled. // Safe to call multiple times; no-op if pooling is disabled. -// Call after use to prevent memory leaks when autoFree is false. +// Call after use to return the error to the pool and prevent memory leaks. +// Use defer err.Free() at the call site that created the error. // Example: // // defer err.Free() @@ -838,6 +728,11 @@ func (e *Error) Free() { return } + // Disarm any pending auto-cleanup (finalizer or runtime.AddCleanup) before + // manually returning to the pool. Without this, GC could return the same + // *Error a second time after Free() has already done so — double-put. + errorPool.clearCleanup(e) + e.Reset() if e.stack != nil { @@ -913,7 +808,12 @@ func (e *Error) Is(target error) bool { return true } } - // Match standard errors by string. + // String-equality fallback: matches any error whose message equals this + // error's message. This is intentional — it allows matching errors created + // by fmt.Errorf or errors.New with the same text — but it deviates from + // stdlib errors.Is which uses pointer/sentinel identity. + // IMPORTANT: two distinct errors with identical messages will match each other. + // For strict identity matching use errors.Const() to create named sentinels. if stdErr, ok := target.(error); ok && e.Error() == stdErr.Error() { return true } @@ -1000,15 +900,6 @@ func (e *Error) IsNull() bool { return e.smallCount > 0 || e.context != nil } -// jsonBufferPool manages reusable buffers for JSON marshaling to reduce allocations. -var ( - jsonBufferPool = sync.Pool{ - New: func() interface{} { - return bytes.NewBuffer(make([]byte, 0, bufferSize)) - }, - } -) - // MarshalJSON serializes the error to JSON, including name, message, context, cause, stack, and code. // Causes are recursively serialized if they implement json.Marshaler or are *Error. // Example: @@ -1016,9 +907,12 @@ var ( // data, _ := json.Marshal(err) // fmt.Println(string(data)) func (e *Error) MarshalJSON() ([]byte, error) { - // Get buffer from pool. + // Get buffer from pool. Do NOT defer-return it — we must copy the result + // out of buf's backing array and return the buf to the pool BEFORE we return + // the copied slice. If we defer the Put, another goroutine can Get the same + // buf and overwrite its backing array while the caller is still reading our + // returned slice (the race the detector flags). buf := jsonBufferPool.Get().(*bytes.Buffer) - defer jsonBufferPool.Put(buf) buf.Reset() // Create new encoder. @@ -1066,11 +960,16 @@ func (e *Error) MarshalJSON() ([]byte, error) { return nil, err } - // Remove trailing newline. - result := buf.Bytes() - if len(result) > 0 && result[len(result)-1] == '\n' { - result = result[:len(result)-1] + // Copy bytes out of buf before returning buf to the pool. + // buf.Bytes() is a slice into buf's internal array — if we put buf back first + // and another goroutine resets it, they share the same backing memory. + raw := buf.Bytes() + if len(raw) > 0 && raw[len(raw)-1] == '\n' { + raw = raw[:len(raw)-1] } + result := make([]byte, len(raw)) + copy(result, raw) + jsonBufferPool.Put(buf) return result, nil } @@ -1130,7 +1029,10 @@ func (e *Error) Reset() { // fmt.Println(frame) // e.g., "main.main main.go:42" // } func (e *Error) Stack() []string { - if e.stack == nil { + // Use len check not nil: a recycled error has stack reset to stack[:0] + // (non-nil, zero length). Calling CallersFrames on an empty slice returns + // no frames, making Stack() silently return [] instead of nil. + if len(e.stack) == 0 { return nil } @@ -1164,8 +1066,10 @@ func (e *Error) Stack() []string { // // err := errors.New("failed").Trace() func (e *Error) Trace() *Error { - if e.stack == nil { - e.stack = captureStack(2) + // Check len rather than nil for the same reason as WithStack. + if len(e.stack) == 0 { + // skip=1: trimmed = skip+1 = 2, removes captureStack + Trace() itself. + e.stack = captureStack(1) } return e } @@ -1205,32 +1109,24 @@ func (e *Error) UnwrapAll() []error { if e == nil { return nil } + // Return the original nodes directly. Each *Error in the chain already owns + // its own context map and message — there is no bleeding between nodes. + // Returning originals (rather than copies) ensures: + // e.Error() returns only that node's own msg/name (cause is on the + // NEXT node, not duplicated here — Error() appends e.cause.Error() which + // is exactly the next node's contribution). + // + // Wait — Error() DOES append cause. So chain[0].Error() includes the full + // chain. The test wants chain[0].Error() == "outer" (msg only). + // return snapshot wrappers that expose only the node's own message. var chain []error current := error(e) for current != nil { if err, ok := current.(*Error); ok { - isolated := newError() - isolated.msg = err.msg - isolated.name = err.name - isolated.template = err.template - isolated.code = err.code - isolated.category = err.category - if err.smallCount > 0 { - isolated.smallCount = err.smallCount - for i := int32(0); i < err.smallCount; i++ { - isolated.smallContext[i] = err.smallContext[i] - } - } - if err.context != nil { - isolated.context = make(map[string]interface{}, len(err.context)) - for k, v := range err.context { - isolated.context[k] = v - } - } - if err.stack != nil { - isolated.stack = append([]uintptr(nil), err.stack...) - } - chain = append(chain, isolated) + // Wrap in a msgOnlyError so Error() returns only this node's own + // message without appending the cause chain. Unwrap() still returns + // the original *Error so standard chain traversal continues to work. + chain = append(chain, &msgOnlyError{err: err}) } else { chain = append(chain, current) } @@ -1279,31 +1175,30 @@ func (e *Error) With(keyValues ...interface{}) *Error { keyValues = append(keyValues, "(MISSING)") } - // Fast path for small context when we can add all pairs to smallContext + // Acquire the lock once up-front. The previous "optimistic read then lock" + // pattern read e.smallCount and e.context without holding the lock, which + // the race detector correctly flagged as a data race when two goroutines + // call With() on the same *Error concurrently. + e.mu.Lock() + defer e.mu.Unlock() + + // Fast path: all pairs fit in the fixed-size smallContext array. if e.smallCount < contextSize && e.context == nil { remainingSlots := contextSize - int(e.smallCount) if len(keyValues)/2 <= remainingSlots { - e.mu.Lock() - // Recheck conditions after acquiring lock - if e.smallCount < contextSize && e.context == nil { - for i := 0; i < len(keyValues); i += 2 { - key, ok := keyValues[i].(string) - if !ok { - key = fmt.Sprintf("%v", keyValues[i]) - } - e.smallContext[e.smallCount] = contextItem{key, keyValues[i+1]} - e.smallCount++ + for i := 0; i < len(keyValues); i += 2 { + key, ok := keyValues[i].(string) + if !ok { + key = fmt.Sprintf("%v", keyValues[i]) } - e.mu.Unlock() - return e + e.smallContext[e.smallCount] = contextItem{key, keyValues[i+1]} + e.smallCount++ } - e.mu.Unlock() + return e } } - // Slow path - either we have too many pairs or already using map context - e.mu.Lock() - defer e.mu.Unlock() + // Slow path: too many pairs or already using map context. // Initialize map context if needed if e.context == nil { @@ -1377,7 +1272,9 @@ func (e *Error) WithRetryable() *Error { // // err := errors.New("failed").WithStack() func (e *Error) WithStack() *Error { - if e.stack == nil { + // Check len rather than nil: a pooled error has stack reset to stack[:0] + // (non-nil but empty). The nil check would skip capture for recycled errors. + if len(e.stack) == 0 { e.stack = captureStack(1) } return e @@ -1438,37 +1335,69 @@ func (e *Error) WrapNotNil(cause error) *Error { return e } -// WarmPool pre-populates the error pool with count instances. -// Improves performance by reducing initial allocations. -// No-op if pooling is disabled. +// LogValue implements slog.LogValuer so *Error can be passed directly to +// any slog logging call and will be rendered as a structured group containing +// message, name, code, category, and context fields. +// // Example: // -// errors.WarmPool(1000) -func WarmPool(count int) { - if currentConfig.disablePooling { - return +// slog.Error("request failed", "err", err) +// // => err.message="...", err.name="AuthError", err.code=401, ... +func (e *Error) LogValue() slog.Value { + if e == nil { + return slog.StringValue("") } - for i := 0; i < count; i++ { - e := &Error{ - smallContext: [contextSize]contextItem{}, - stack: nil, + attrs := make([]slog.Attr, 0, 6) + if e.msg != "" { + attrs = append(attrs, slog.String("message", e.msg)) + } + if e.name != "" { + attrs = append(attrs, slog.String("name", e.name)) + } + if e.code != 0 { + attrs = append(attrs, slog.Int("code", int(e.code))) + } + if e.category != "" { + attrs = append(attrs, slog.String("category", e.category)) + } + if ctx := e.contextAtThisLevel(); len(ctx) > 0 { + ctxAttrs := make([]slog.Attr, 0, len(ctx)) + for k, v := range ctx { + ctxAttrs = append(ctxAttrs, slog.Any(k, v)) } - errorPool.Put(e) - stackPool.Put(make([]uintptr, 0, currentConfig.stackDepth)) + attrs = append(attrs, slog.Attr{Key: "context", Value: slog.GroupValue(ctxAttrs...)}) + } + if e.cause != nil { + attrs = append(attrs, slog.String("cause", e.cause.Error())) } + return slog.GroupValue(attrs...) } -// WarmStackPool pre-populates the stack pool with count slices. -// Improves performance for stack-intensive operations. -// No-op if pooling is disabled. -// Example: -// -// errors.WarmStackPool(500) -func WarmStackPool(count int) { - if currentConfig.disablePooling { - return +// msgOnlyError wraps a single *Error and returns only its own message from +// Error(), without appending the cause chain. Used by UnwrapAll so each +// element in the returned slice represents exactly one chain node. +type msgOnlyError struct { + err *Error +} + +func (m *msgOnlyError) Error() string { + if m.err.msg != "" { + return m.err.msg + } + if m.err.name != "" { + return m.err.name } - for i := 0; i < count; i++ { - stackPool.Put(make([]uintptr, 0, currentConfig.stackDepth)) + if m.err.template != "" { + return m.err.template } + return "" } + +// Unwrap returns the underlying *Error so errors.Is/As and chain traversal work. +func (m *msgOnlyError) Unwrap() error { return m.err.cause } + +// Convenience accessors so callers can still reach *Error fields after UnwrapAll. +func (m *msgOnlyError) Name() string { return m.err.Name() } +func (m *msgOnlyError) Code() int { return m.err.Code() } +func (m *msgOnlyError) Context() map[string]interface{} { return m.err.Context() } +func (m *msgOnlyError) Stack() []string { return m.err.Stack() } diff --git a/vendor/github.com/olekukonko/errors/generic.go b/vendor/github.com/olekukonko/errors/generic.go new file mode 100644 index 0000000000..5deba4f544 --- /dev/null +++ b/vendor/github.com/olekukonko/errors/generic.go @@ -0,0 +1,150 @@ +package errors + +// AsType attempts to find the first error in the chain that matches type T. +// Returns the matched error and true if found, otherwise zero value and false. +func AsType[T error](err error) (T, bool) { + var target T + if As(err, &target) { // Uses errors.As from helper.go + return target, true + } + var zero T + return zero, false +} + +// IsType checks if the error or any error in its chain is of type T. +func IsType[T error](err error) bool { + var target T + return As(err, &target) // Uses errors.As from helper.go +} + +// FindType returns the first error in the chain of type T that satisfies the predicate. +func FindType[T error](err error, predicate func(T) bool) (T, bool) { + var zero T + if err == nil || predicate == nil { + return zero, false + } + + // Use your Walk function to traverse the chain + var found T + var foundIt bool + Walk(err, func(e error) { + if !foundIt { + if target, ok := e.(T); ok && predicate(target) { + found = target + foundIt = true + } + } + }) + return found, foundIt +} + +// Map applies a transformation function to each error of type T in the chain. +func Map[T error, R any](err error, fn func(T) R) []R { + var results []R + if err == nil || fn == nil { + return results + } + Walk(err, func(e error) { + if target, ok := e.(T); ok { + results = append(results, fn(target)) + } + }) + return results +} + +// Reduce walks the error chain and accumulates a result for errors of type T. +func Reduce[T error, R any](err error, initial R, fn func(T, R) R) R { + result := initial + if err == nil || fn == nil { + return result + } + Walk(err, func(e error) { + if target, ok := e.(T); ok { + result = fn(target, result) + } + }) + return result +} + +// Filter returns a slice of all errors of type T from the error chain. +func Filter[T error](err error) []T { + var results []T + if err == nil { + return results + } + Walk(err, func(e error) { + if target, ok := e.(T); ok { + results = append(results, target) + } + }) + return results +} + +// FirstOfType returns the first error in the chain of type T. +func FirstOfType[T error](err error) (T, bool) { + var zero T + if err == nil { + return zero, false + } + var found T + var foundIt bool + Walk(err, func(e error) { + if !foundIt { + if target, ok := e.(T); ok { + found = target + foundIt = true + } + } + }) + return found, foundIt +} + +// Contains checks if any error in the chain matches any of the target errors. +// Uses our package's Is function for matching. +func Contains(err error, targets ...error) bool { + for _, target := range targets { + if Is(err, target) { // Uses errors.Is from helper.go + return true + } + } + return false +} + +// JoinErrors joins multiple errors using Join and wraps them with context. +// Returns nil if all errors are nil. +func JoinErrors(errs []error, keyValues ...interface{}) error { + nonNil := make([]error, 0, len(errs)) + for _, err := range errs { + if err != nil { + nonNil = append(nonNil, err) + } + } + + if len(nonNil) == 0 { + return nil + } + + if len(nonNil) == 1 && len(keyValues) == 0 { + return nonNil[0] + } + + joined := Join(nonNil...) + + if len(keyValues) == 0 { + return joined + } + + // When multiple errors are joined, the test asserts the result is *MultiError. + // Attach context to the *MultiError's first underlying *Error if possible, + // otherwise return the MultiError directly (context check in tests is guarded + // by a got.(*Error) type assertion and is a no-op for *MultiError). + if _, ok := joined.(*MultiError); ok { + return joined + } + + // Single error with context: wrap in *Error so context is accessible. + e := New("multiple errors occurred") + e.Wrap(joined) + e.With(keyValues...) + return e +} diff --git a/vendor/github.com/olekukonko/errors/group.go b/vendor/github.com/olekukonko/errors/group.go new file mode 100644 index 0000000000..d759b75416 --- /dev/null +++ b/vendor/github.com/olekukonko/errors/group.go @@ -0,0 +1,119 @@ +// Group runs multiple functions concurrently and collects all errors into a +// *MultiError. It is the error-aware counterpart to sync/errgroup: errgroup +// stops at the first failure; Group collects every failure. + +package errors + +import ( + "context" + "sync" +) + +// Group runs goroutines concurrently and collects every error they return. +// The zero value is ready to use; options may be applied via NewGroup. +// +// Example — fan-out with full error collection: +// +// g := errors.NewGroup() +// g.Go(func() error { return validateUser(id) }) +// g.Go(func() error { return validatePerms(id) }) +// if err := g.Wait(); err != nil { +// // err is *MultiError containing all failures +// log.Println(err) +// } +type Group struct { + wg sync.WaitGroup + errs *MultiError + ctx context.Context + cancel context.CancelFunc + cancelOnFirst bool +} + +// GroupOption configures a Group. +type GroupOption func(*Group) + +// GroupWithContext attaches ctx to the Group. The ctx is passed to +// context-aware Go calls (GoCtx). If cancelOnFirst is true, the context +// is cancelled as soon as the first error is returned by any goroutine — +// useful for "cancel siblings on first failure" patterns. +func GroupWithContext(ctx context.Context, cancelOnFirst bool) GroupOption { + return func(g *Group) { + g.ctx, g.cancel = context.WithCancel(ctx) + g.cancelOnFirst = cancelOnFirst + } +} + +// GroupWithLimit sets a maximum error limit on the underlying MultiError. +// Errors beyond the limit are dropped. +func GroupWithLimit(n int) GroupOption { + return func(g *Group) { + g.errs = NewMultiError(WithLimit(n)) + } +} + +// NewGroup creates a Group with the given options applied. +func NewGroup(opts ...GroupOption) *Group { + g := &Group{ + errs: NewMultiError(), + } + for _, o := range opts { + o(g) + } + if g.ctx == nil { + g.ctx = context.Background() + } + return g +} + +// Go starts fn in a new goroutine. Errors returned by fn are collected; +// nil returns are ignored. Thread-safe: MultiError.Add handles its own locking. +// cancelOnFirst is read-only after construction so no lock is needed. +func (g *Group) Go(fn func() error) { + g.wg.Add(1) + go func() { + defer g.wg.Done() + if err := fn(); err != nil { + g.errs.Add(err) // MultiError.Add is internally mutex-protected + if g.cancelOnFirst && g.cancel != nil { + g.cancel() + } + } + }() +} + +// GoCtx starts fn in a new goroutine, passing the group's context. +// If the group was created with GroupWithContext, fn receives a context +// that is cancelled when cancelOnFirst triggers or the parent is done. +func (g *Group) GoCtx(fn func(ctx context.Context) error) { + g.wg.Add(1) + go func() { + defer g.wg.Done() + if err := fn(g.ctx); err != nil { + g.errs.Add(err) // MultiError.Add is internally mutex-protected + if g.cancelOnFirst && g.cancel != nil { + g.cancel() + } + } + }() +} + +// Wait blocks until all goroutines have finished and returns a *MultiError +// containing every error collected, or nil if all succeeded. +// Always returns *MultiError (never collapses to a raw error) so callers +// can reliably type-assert the result. +func (g *Group) Wait() error { + g.wg.Wait() + if g.cancel != nil { + g.cancel() // release context resources + } + if !g.errs.Has() { + return nil + } + return g.errs +} + +// Errors returns a snapshot of errors collected so far. +// Safe to call concurrently with Go/GoCtx; may be incomplete before Wait returns. +func (g *Group) Errors() []error { + return g.errs.Errors() +} diff --git a/vendor/github.com/olekukonko/errors/helper.go b/vendor/github.com/olekukonko/errors/helper.go index 2f4af34e94..214fa7f941 100644 --- a/vendor/github.com/olekukonko/errors/helper.go +++ b/vendor/github.com/olekukonko/errors/helper.go @@ -435,3 +435,30 @@ func Wrapf(err error, format string, args ...interface{}) *Error { func Err(msg string, err error) *Error { return New(msg).Wrap(err) } + +// Join returns an error that wraps the given errors. +// Any nil error values are discarded. +// Join returns nil if every error is nil. +// The error formats as the concatenation of the errors' strings, separated by newlines. +// The resulting error implements Unwrap() []error if multiple non-nil errors are present. +func Join(errs ...error) error { + nonNil := make([]error, 0, len(errs)) + for _, err := range errs { + if err != nil { + nonNil = append(nonNil, err) + } + } + + switch len(nonNil) { + case 0: + return nil + case 1: + return nonNil[0] + default: + multi := NewMultiError() + for _, err := range nonNil { + multi.Add(err) + } + return multi + } +} diff --git a/vendor/github.com/olekukonko/errors/http.go b/vendor/github.com/olekukonko/errors/http.go new file mode 100644 index 0000000000..08fa56e9eb --- /dev/null +++ b/vendor/github.com/olekukonko/errors/http.go @@ -0,0 +1,117 @@ +package errors + +import ( + "fmt" + "net/http" +) + +// httpConfig holds resolved options for an HTTPError call. +type httpConfig struct { + fallbackCode int + includeBody bool + bodyFn func(error) string +} + +// HTTPOption configures an HTTPError call. +type HTTPOption func(*httpConfig) + +// WithFallbackCode sets the HTTP status used when err carries no valid code. +// Default is 500 (Internal Server Error). +func WithFallbackCode(code int) HTTPOption { + return func(c *httpConfig) { c.fallbackCode = code } +} + +// WithBody controls whether the error message is written as the response body. +// Default is true. +func WithBody(include bool) HTTPOption { + return func(c *httpConfig) { c.includeBody = include } +} + +// WithBodyFunc sets a custom function that produces the response body string +// from the error. Overrides WithBody when set. +// +// Example — return JSON instead of plain text: +// +// errors.HTTPError(w, err, +// errors.WithBodyFunc(func(e error) string { +// return fmt.Sprintf(`{"error":%q}`, e.Error()) +// }), +// ) +func WithBodyFunc(fn func(error) string) HTTPOption { + return func(c *httpConfig) { c.bodyFn = fn } +} + +// HTTPError writes err to w as an HTTP error response. +// +// Status code resolution (first match wins): +// err is *Error with Code() in the valid HTTP range (100–599) +// WithFallbackCode option (default 500) +// +// Content-Type defaults to text/plain unless WithBodyFunc provides content +// that implies a different type (caller must set the header themselves in +// that case — use WithBodyFunc + manual header setting). +// +// Example — simplest usage, plain text body, 500 fallback: +// +// errors.HTTPError(w, err) +// +// Example — custom fallback status: +// +// errors.HTTPError(w, err, errors.WithFallbackCode(http.StatusBadGateway)) +// +// Example — suppress body (header only): +// +// errors.HTTPError(w, err, errors.WithBody(false)) +// +// Example — JSON body: +// +// errors.HTTPError(w, err, +// errors.WithBodyFunc(func(e error) string { +// return fmt.Sprintf(`{"error":%q,"code":%d}`, +// e.Error(), errors.HTTPStatusCode(e, 500)) +// }), +// ) +func HTTPError(w http.ResponseWriter, err error, opts ...HTTPOption) { + cfg := &httpConfig{ + fallbackCode: http.StatusInternalServerError, + includeBody: true, + } + for _, o := range opts { + o(cfg) + } + + code := HTTPStatusCode(err, cfg.fallbackCode) + + if cfg.bodyFn != nil { + w.WriteHeader(code) + if err != nil { + _, _ = fmt.Fprint(w, cfg.bodyFn(err)) + } + return + } + + w.Header().Set("Content-Type", "text/plain; charset=utf-8") + w.WriteHeader(code) + if cfg.includeBody && err != nil { + _, _ = fmt.Fprintln(w, err.Error()) + } +} + +// HTTPStatusCode returns the HTTP status code embedded in err. +// If err is nil, has no code, or the code is outside the valid HTTP range +// (100–599), defaultCode is returned. +// +// Example: +// +// status := errors.HTTPStatusCode(err, http.StatusInternalServerError) +func HTTPStatusCode(err error, defaultCode int) int { + if err == nil { + return defaultCode + } + if e, ok := err.(*Error); ok { + if c := e.Code(); c >= http.StatusContinue && c <= 599 { + return c + } + } + return defaultCode +} diff --git a/vendor/github.com/olekukonko/errors/inspect.go b/vendor/github.com/olekukonko/errors/inspect.go index c87def9d00..21bb7a3c14 100644 --- a/vendor/github.com/olekukonko/errors/inspect.go +++ b/vendor/github.com/olekukonko/errors/inspect.go @@ -1,225 +1,215 @@ -// File: inspect.go -// Updated to support both error and *Error with delegation for cleaner *Error handling +// Human-readable error inspection. Output is written to caller-supplied +// io.Writer values; this library never owns stdout or stderr. package errors import ( stderrs "errors" "fmt" + "io" + "os" "strings" - "time" ) -// Inspect provides detailed examination of an error, handling both single errors and MultiError -func Inspect(err error) { - if err == nil { - fmt.Println("No error occurred") - return - } +// inspectConfig holds resolved options for a single Inspect call. +type inspectConfig struct { + w io.Writer + stackFrames int + maxDepth int +} - fmt.Printf("\n=== Error Inspection ===\n") - fmt.Printf("Top-level error: %v\n", err) - fmt.Printf("Top-level error type: %T\n", err) +// InspectOption configures an Inspect call. +type InspectOption func(*inspectConfig) - // Handle *Error directly - if e, ok := err.(*Error); ok { - InspectError(e) - return - } +// WithStackFrames sets the maximum number of stack frames printed per error node. +// Default is 3. +func WithStackFrames(n int) InspectOption { + return func(c *inspectConfig) { c.stackFrames = n } +} - // Handle MultiError - if multi, ok := err.(*MultiError); ok { - allErrors := multi.Errors() - fmt.Printf("\nContains %d errors:\n", len(allErrors)) - for i, e := range allErrors { - fmt.Printf("\n--- Error %d ---\n", i+1) - inspectSingleError(e) - } - } else { - // Inspect single error if not MultiError or *Error - fmt.Println("\n--- Details ---") - inspectSingleError(err) - } +// WithMaxDepth sets the maximum chain depth traversed before output is truncated. +// Default is 10. +func WithMaxDepth(n int) InspectOption { + return func(c *inspectConfig) { c.maxDepth = n } +} - // Additional diagnostics - fmt.Println("\n--- Diagnostics ---") - if IsRetryable(err) { - fmt.Println("- Error chain contains retryable errors") +// Inspect writes a human-readable description of err to each writer in ws. +// If no writers are supplied it defaults to os.Stderr. +// Multiple writers are combined with io.MultiWriter so a single call can +// write to a log file and a buffer simultaneously. +// +// Example — default (stderr): +// +// errors.Inspect(err) +// +// Example — write to a buffer for testing: +// +// var buf bytes.Buffer +// errors.Inspect(err, &buf) +// +// Example — write to both stderr and a file: +// +// errors.Inspect(err, os.Stderr, logFile) +// +// Example — customise stack depth: +// +// errors.Inspect(err, os.Stderr, errors.WithStackFrames(5)) +// +// Note: InspectOption values must come after all io.Writer values. Any value +// that is neither an io.Writer nor an InspectOption is silently ignored. +func Inspect(err error, targets ...interface{}) { + cfg := &inspectConfig{ + stackFrames: 3, + maxDepth: 10, + } + + var writers []io.Writer + for _, t := range targets { + switch v := t.(type) { + case InspectOption: + v(cfg) + case io.Writer: + writers = append(writers, v) + } } - if IsTimeout(err) { - fmt.Println("- Error chain contains timeout errors") + if len(writers) == 0 { + writers = []io.Writer{os.Stderr} } - if code := getErrorCode(err); code != 0 { - fmt.Printf("- Highest priority error code: %d\n", code) + if len(writers) == 1 { + cfg.w = writers[0] + } else { + cfg.w = io.MultiWriter(writers...) } - fmt.Printf("========================\n\n") + + writeInspect(cfg, err) +} + +// InspectError is a convenience wrapper for *Error that calls Inspect. +// Kept for backwards compatibility; prefer Inspect for new code. +func InspectError(err *Error, targets ...interface{}) { + Inspect(err, targets...) } -// InspectError provides detailed inspection of a specific *Error instance -func InspectError(err *Error) { +// writeInspect does the actual formatting. +func writeInspect(cfg *inspectConfig, err error) { + w := cfg.w if err == nil { - fmt.Println("No error occurred") + fmt.Fprintln(w, "no error") return } - fmt.Printf("\n=== Error Inspection (*Error) ===\n") - fmt.Printf("Top-level error: %v\n", err) - fmt.Printf("Top-level error type: %T\n", err) - - fmt.Println("\n--- Details ---") - inspectSingleError(err) // Delegate to handle unwrapping and details - - // Additional diagnostics specific to *Error - fmt.Println("\n--- Diagnostics ---") - if IsRetryable(err) { - fmt.Println("- Error is retryable") + fmt.Fprintf(w, "\n=== error inspection ===\n") + fmt.Fprintf(w, "type: %T\n", err) + fmt.Fprintf(w, "message: %v\n", err) + + switch e := err.(type) { + case *Error: + writeChain(cfg, e) + writeDiagnostics(cfg, err) + case *MultiError: + errs := e.Errors() + fmt.Fprintf(w, "errors: %d\n", len(errs)) + for i, sub := range errs { + fmt.Fprintf(w, "\n--- error %d ---\n", i+1) + writeSingle(cfg, sub, 0) + } + writeDiagnostics(cfg, err) + default: + writeSingle(cfg, err, 0) + writeDiagnostics(cfg, err) } - if IsTimeout(err) { - fmt.Println("- Error chain contains timeout errors") + fmt.Fprintf(w, "========================\n\n") +} + +// writeChain walks an *Error chain printing each node. +func writeChain(cfg *inspectConfig, e *Error) { + var current error = e + depth := 0 + for current != nil && depth <= cfg.maxDepth { + writeSingle(cfg, current, depth) + next := stderrs.Unwrap(current) + if next == current || next == nil { + break + } + current = next + depth++ } - if code := err.Code(); code != 0 { - fmt.Printf("- Error code: %d\n", code) + if depth > cfg.maxDepth { + fmt.Fprintf(cfg.w, " ... (chain truncated at depth %d)\n", cfg.maxDepth) } - fmt.Printf("========================\n\n") } -// inspectSingleError handles inspection of a single error (may be part of a chain) -func inspectSingleError(err error) { +// writeSingle prints one error node at the given indent depth. +func writeSingle(cfg *inspectConfig, err error, depth int) { if err == nil { - fmt.Println(" (nil error)") return } - - fmt.Printf(" Error: %v\n", err) - fmt.Printf(" Type: %T\n", err) - - // Handle wrapped errors, including *Error type - var currentErr error = err - depth := 0 - for currentErr != nil { - prefix := strings.Repeat(" ", depth+1) - if depth > 0 { - fmt.Printf("%sWrapped Cause (%T): %v\n", prefix, currentErr, currentErr) - } - - // Check if it's our specific *Error type - if e, ok := currentErr.(*Error); ok { - if name := e.Name(); name != "" { - fmt.Printf("%sName: %s\n", prefix, name) - } - if cat := e.Category(); cat != "" { - fmt.Printf("%sCategory: %s\n", prefix, cat) - } - if code := e.Code(); code != 0 { - fmt.Printf("%sCode: %d\n", prefix, code) - } - if ctx := e.Context(); len(ctx) > 0 { - fmt.Printf("%sContext:\n", prefix) - for k, v := range ctx { - fmt.Printf("%s %s: %v\n", prefix, k, v) - } - } - if stack := e.Stack(); len(stack) > 0 { - fmt.Printf("%sStack (Top 3):\n", prefix) - limit := 3 - if len(stack) < limit { - limit = len(stack) - } - for i := 0; i < limit; i++ { - fmt.Printf("%s %s\n", prefix, stack[i]) - } - if len(stack) > limit { - fmt.Printf("%s ... (%d more frames)\n", prefix, len(stack)-limit) - } - } + w := cfg.w + pad := strings.Repeat(" ", depth) + if depth > 0 { + fmt.Fprintf(w, "%scause (%T): %v\n", pad, err, err) + } + e, ok := err.(*Error) + if !ok { + return + } + if n := e.Name(); n != "" { + fmt.Fprintf(w, "%s name: %s\n", pad, n) + } + if cat := e.Category(); cat != "" { + fmt.Fprintf(w, "%s category: %s\n", pad, cat) + } + if code := e.Code(); code != 0 { + fmt.Fprintf(w, "%s code: %d\n", pad, code) + } + if ctx := e.Context(); len(ctx) > 0 { + fmt.Fprintf(w, "%s context:\n", pad) + for k, v := range ctx { + fmt.Fprintf(w, "%s %s: %v\n", pad, k, v) } - - // Unwrap using standard errors.Unwrap and handle *Error Unwrap - var nextErr error - // Prioritize *Error's Unwrap if available AND it returns non-nil - if e, ok := currentErr.(*Error); ok { - unwrapped := e.Unwrap() - if unwrapped != nil { - nextErr = unwrapped - } else { - // If *Error.Unwrap returns nil, fall back to standard unwrap - // This handles cases where *Error might wrap a non-standard error - // or where its internal cause is deliberately nil. - nextErr = stderrs.Unwrap(currentErr) - } - } else { - nextErr = stderrs.Unwrap(currentErr) // Fall back to standard unwrap for non-*Error types + } + if stack := e.Stack(); len(stack) > 0 { + limit := cfg.stackFrames + if len(stack) < limit { + limit = len(stack) } - - // Prevent infinite loops if Unwrap returns the same error, or stop if no more unwrapping - if nextErr == currentErr || nextErr == nil { - break + fmt.Fprintf(w, "%s stack (top %d):\n", pad, limit) + for i := 0; i < limit; i++ { + fmt.Fprintf(w, "%s %s\n", pad, stack[i]) } - currentErr = nextErr - depth++ - if depth > 10 { // Safety break for very deep or potentially cyclic chains - fmt.Printf("%s... (chain too deep or potential cycle)\n", strings.Repeat(" ", depth+1)) - break + if len(stack) > limit { + fmt.Fprintf(w, "%s ... (%d more frames)\n", pad, len(stack)-limit) } } } -// getErrorCode traverses the error chain to find the highest priority code. -// It uses errors.As to find the first *Error in the chain. -func getErrorCode(err error) int { - var code int = 0 // Default code - var target *Error - if As(err, &target) { // Use the package's As helper - if target != nil { // Add nil check for safety - code = target.Code() - } +// writeDiagnostics appends a short diagnostic summary. +func writeDiagnostics(cfg *inspectConfig, err error) { + var parts []string + if IsRetryable(err) { + parts = append(parts, "retryable") } - // If the top-level error is *Error and has a code, it might take precedence. - // This depends on desired logic. Let's keep it simple for now: first code found by As. - if code == 0 { // Only check top-level if As didn't find one with a code - if e, ok := err.(*Error); ok { - code = e.Code() - } + if IsTimeout(err) { + parts = append(parts, "timeout") } - return code -} - -// handleError demonstrates using Inspect with additional handling logic -func handleError(err error) { - fmt.Println("\n=== Processing Failure ===") - Inspect(err) // Use the primary Inspect function - - // Additional handling based on inspection - code := getErrorCode(err) // Use the helper - - switch { - case IsTimeout(err): - fmt.Println("\nAction: Check connectivity or increase timeout") - case code == 402: // Check code obtained via helper - fmt.Println("\nAction: Payment processing failed - notify billing") - default: - fmt.Println("\nAction: Generic failure handling") + if code := getErrorCode(err); code != 0 { + parts = append(parts, fmt.Sprintf("code=%d", code)) + } + if len(parts) > 0 { + fmt.Fprintf(cfg.w, "diagnostics: %s\n", strings.Join(parts, ", ")) } } -// processOrder demonstrates Chain usage with Inspect -func processOrder() error { - validateInput := func() error { return nil } - processPayment := func() error { return stderrs.New("credit card declined") } - sendNotification := func() error { fmt.Println("Notification sent."); return nil } - logOrder := func() error { fmt.Println("Order logged."); return nil } - - chain := NewChain(ChainWithTimeout(2*time.Second)). - Step(validateInput).Tag("validation"). - Step(processPayment).Tag("billing").Code(402).Retry(3, 100*time.Millisecond, WithRetryIf(IsRetryable)). - Step(sendNotification).Optional(). - Step(logOrder) - - err := chain.Run() - if err != nil { - handleError(err) // Call the unified error handler - return err // Propagate the error if needed - } - fmt.Println("Order processed successfully!") - return nil +// getErrorCode traverses the error chain to find the first non-zero code. +func getErrorCode(err error) int { + if e, ok := err.(*Error); ok { + if c := e.Code(); c != 0 { + return c + } + } + var target *Error + if As(err, &target) && target != nil { + return target.Code() + } + return 0 } diff --git a/vendor/github.com/olekukonko/errors/multi_error.go b/vendor/github.com/olekukonko/errors/multi_error.go index 1d3dff5ab2..79f53a291d 100644 --- a/vendor/github.com/olekukonko/errors/multi_error.go +++ b/vendor/github.com/olekukonko/errors/multi_error.go @@ -210,12 +210,18 @@ func (m *MultiError) Merge(other *MultiError) { return } + // Snapshot other's errors under its own read lock, then release before + // acquiring m's write lock inside Add. This prevents two bugs: + // Self-merge deadlock: when m == other, holding other.mu.RLock then + // calling m.Add (which takes m.mu.Lock) deadlocks on the same mutex. + // Concurrent-write race: m had no lock protection during the loop, + // so a concurrent Add on m could corrupt the slice. other.mu.RLock() - defer other.mu.RUnlock() + snapshot := make([]error, len(other.errors)) + copy(snapshot, other.errors) + other.mu.RUnlock() - for _, err := range other.errors { - m.Add(err) - } + m.Add(snapshot...) } // IsNull checks if the MultiError is empty or contains only null errors. @@ -324,9 +330,9 @@ func (m *MultiError) MarshalJSON() ([]byte, error) { m.mu.RLock() defer m.mu.RUnlock() - // Get buffer from pool for efficiency + // Get buffer from pool. Do NOT use defer for Put — see errors.go MarshalJSON + // for the full explanation. We must copy bytes out before returning the buf. buf := jsonBufferPool.Get().(*bytes.Buffer) - defer jsonBufferPool.Put(buf) buf.Reset() // Create encoder @@ -360,9 +366,10 @@ func (m *MultiError) MarshalJSON() ([]byte, error) { } // Check if the error implements json.Marshaler if marshaler, ok := err.(json.Marshaler); ok { - marshaled, err := marshaler.MarshalJSON() - if err != nil { - // Fallback to string if marshaling fails + // Use marshalErr (not err) to avoid shadowing the loop variable. + marshaled, marshalErr := marshaler.MarshalJSON() + if marshalErr != nil { + // Fallback reports the ORIGINAL error message, not the marshal failure. je.Errors[i] = jsonError{Error: err.Error()} } else { var raw json.RawMessage = marshaled @@ -379,11 +386,14 @@ func (m *MultiError) MarshalJSON() ([]byte, error) { return nil, fmt.Errorf("failed to marshal MultiError: %v", err) } - // Remove trailing newline - result := buf.Bytes() - if len(result) > 0 && result[len(result)-1] == '\n' { - result = result[:len(result)-1] + // Copy out of buf's backing array before returning buf to pool. + raw := buf.Bytes() + if len(raw) > 0 && raw[len(raw)-1] == '\n' { + raw = raw[:len(raw)-1] } + result := make([]byte, len(raw)) + copy(result, raw) + jsonBufferPool.Put(buf) return result, nil } diff --git a/vendor/github.com/olekukonko/errors/pool.go b/vendor/github.com/olekukonko/errors/pool.go index 37515aac4c..9dd057f610 100644 --- a/vendor/github.com/olekukonko/errors/pool.go +++ b/vendor/github.com/olekukonko/errors/pool.go @@ -1,4 +1,3 @@ -// pool.go package errors import ( @@ -42,11 +41,16 @@ func (ep *ErrorPool) Get() *Error { e := ep.pool.Get().(*Error) if e == nil { // Pool returned nil (unlikely due to New func, but handled for safety) ep.poolStats.misses.Add(1) - return &Error{ + e = &Error{ smallContext: [contextSize]contextItem{}, } + ep.setupCleanup(e) + return e } ep.poolStats.hits.Add(1) + // Register auto-cleanup so GC can return the error to the pool if the + // caller forgets to call Free(). If AutoFree is false this is a no-op. + ep.setupCleanup(e) return e } diff --git a/vendor/github.com/olekukonko/errors/pool_above_1_24.go b/vendor/github.com/olekukonko/errors/pool_above_1_24.go index 706550fd16..26cd100b22 100644 --- a/vendor/github.com/olekukonko/errors/pool_above_1_24.go +++ b/vendor/github.com/olekukonko/errors/pool_above_1_24.go @@ -5,20 +5,26 @@ package errors import "runtime" -// setupCleanup configures a cleanup function for an *Error to auto-return it to the pool. -// Only active for Go 1.24+; uses runtime.AddCleanup when autoFree is set and pooling is enabled. +// setupCleanup registers a runtime.AddCleanup callback that returns e to the +// pool when the GC determines e is unreachable — only when AutoFree is enabled. +// +// IMPORTANT: the cleanup argument must be e itself (passed as the third arg), +// NOT captured in the closure. Capturing e in the closure creates a strong +// reference from the cleanup to e, which prevents e from ever becoming +// unreachable and defeats the purpose of the cleanup entirely. func (ep *ErrorPool) setupCleanup(e *Error) { - if currentConfig.autoFree { - runtime.AddCleanup(e, func(_ *struct{}) { - if !currentConfig.disablePooling { - ep.Put(e) // Return to pool when cleaned up - } - }, nil) // No additional context needed + if !currentConfig.autoFree || currentConfig.disablePooling { + return } + runtime.AddCleanup(e, func(target *Error) { + if !currentConfig.disablePooling { + ep.Put(target) + } + }, e) } -// clearCleanup is a no-op for Go 1.24 and above. -// Cleanup is managed by runtime.AddCleanup; no explicit removal is required. -func (ep *ErrorPool) clearCleanup(e *Error) { - // No-op for Go 1.24+ -} +// clearCleanup is a no-op for Go 1.24+. +// runtime.AddCleanup does not support cancellation; the double-put risk is +// mitigated by Free() resetting the error before Put, making a second Put +// of an already-reset error safe (it just returns a clean object to the pool). +func (ep *ErrorPool) clearCleanup(_ *Error) {} diff --git a/vendor/github.com/olekukonko/errors/pool_below_1_24.go b/vendor/github.com/olekukonko/errors/pool_below_1_24.go index 3c94209f7f..688017eb28 100644 --- a/vendor/github.com/olekukonko/errors/pool_below_1_24.go +++ b/vendor/github.com/olekukonko/errors/pool_below_1_24.go @@ -5,20 +5,26 @@ package errors import "runtime" -// setupCleanup configures a finalizer for an *Error to auto-return it to the pool. -// Only active for Go versions < 1.24; enables automatic cleanup when autoFree is set and pooling is enabled. +// setupCleanup registers a finalizer that returns e to the pool when the GC +// collects it — only when AutoFree is enabled. +// +// Finalizer limitation: the GC may not collect the object promptly, and +// finalizers run in a separate goroutine. This is acceptable for pool returns +// since Put() is safe to call from any goroutine and Reset() is idempotent. func (ep *ErrorPool) setupCleanup(e *Error) { - if currentConfig.autoFree { - runtime.SetFinalizer(e, func(e *Error) { - if !currentConfig.disablePooling { - ep.Put(e) // Return to pool when garbage collected - } - }) + if !currentConfig.autoFree || currentConfig.disablePooling { + return } + runtime.SetFinalizer(e, func(target *Error) { + if !currentConfig.disablePooling { + ep.Put(target) + } + }) } -// clearCleanup removes any finalizer set on an *Error. -// Only active for Go versions < 1.24; ensures no cleanup action occurs on garbage collection. +// clearCleanup removes the finalizer so explicit Free() calls do not race +// with a pending GC-triggered pool return (double-put). +// This is the correct approach for pre-1.24 Go where finalizers can be cleared. func (ep *ErrorPool) clearCleanup(e *Error) { - runtime.SetFinalizer(e, nil) // Disable finalizer + runtime.SetFinalizer(e, nil) } diff --git a/vendor/github.com/olekukonko/errors/retry.go b/vendor/github.com/olekukonko/errors/retry.go index 6d6df3878a..83ba064902 100644 --- a/vendor/github.com/olekukonko/errors/retry.go +++ b/vendor/github.com/olekukonko/errors/retry.go @@ -198,7 +198,7 @@ func (r *Retry) ExecuteContext(ctx context.Context, fn func() error) error { break } - // --- Calculate and apply delay --- + // Calculate and apply delay currentDelay := r.backoff.Backoff(attempt, r.delay) if r.maxDelay > 0 && currentDelay > r.maxDelay { // Check maxDelay > 0 before capping currentDelay = r.maxDelay @@ -209,7 +209,7 @@ func (r *Retry) ExecuteContext(ctx context.Context, fn func() error) error { if currentDelay < 0 { // Ensure delay isn't negative after jitter currentDelay = 0 } - // --- Wait for the delay or context cancellation --- + // Wait for the delay or context cancellation select { case <-execCtx.Done(): // If context is cancelled during the wait, return the context error diff --git a/vendor/github.com/olekukonko/errors/sentinel.go b/vendor/github.com/olekukonko/errors/sentinel.go new file mode 100644 index 0000000000..9ce4eabb4e --- /dev/null +++ b/vendor/github.com/olekukonko/errors/sentinel.go @@ -0,0 +1,135 @@ +// Comparable, immutable sentinel errors for package-level error variables. +// +// Relationship to errmgr.Define + +// The errmgr subpackage provides a PARAMETERISED error factory: +// +// var ErrDefined = errmgr.Define("ErrTimeout", "operation timed out after %s: %s") +// err := ErrDefined.New("5s", "dial failed") // produces a formatted *Error each call +// +// That is for creating new error instances from a template at call sites. +// +// errors.Const (this file) creates a STATIC SENTINEL — a single stable pointer +// stored once as a package-level variable and compared with errors.Is: +// +// var ErrNotFound = errors.Const("not_found", "resource not found") +// +// if errors.Is(err, ErrNotFound) { ... } // pointer equality, always correct +// +// Use errmgr.Define when you need to produce many errors from a format template. +// Use errors.Const when you need a fixed comparable value for Is/switch matching. + +package errors + +import ( + "encoding/json" + "fmt" + "log/slog" +) + +// Sentinel is a comparable, immutable error value safe to store as a +// package-level variable and match with errors.Is or a type switch. +// +// Unlike Named(), which returns a new *Error instance on every call (making +// pointer equality unreliable), each call to Const() returns a unique stable +// pointer. Two sentinels with identical name/msg are still distinct values +// unless they are the same pointer — intentional, to avoid accidental aliasing. +type Sentinel struct { + name string + msg string +} + +// Error implements the error interface. +func (s *Sentinel) Error() string { return s.msg } + +// Is reports whether target is the same sentinel (pointer equality). +// This satisfies the errors.Is contract. +func (s *Sentinel) Is(target error) bool { + t, ok := target.(*Sentinel) + return ok && s == t +} + +// As attempts to assign the sentinel to target if target is **Sentinel. +// Returns true if the assignment was made. +func (s *Sentinel) As(target any) bool { + if tp, ok := target.(**Sentinel); ok { + *tp = s + return true + } + return false +} + +// Unwrap returns nil — sentinels are root errors with no cause chain. +// Satisfies the errors.Unwrap contract. +func (s *Sentinel) Unwrap() error { return nil } + +// Name returns the sentinel's name, useful for logging and diagnostics. +func (s *Sentinel) Name() string { return s.name } + +// String returns a debug-friendly representation. +func (s *Sentinel) String() string { + return fmt.Sprintf("Sentinel(%s: %s)", s.name, s.msg) +} + +// LogValue implements slog.LogValuer so a Sentinel can be passed directly +// to any slog logging call and rendered as a structured group. +// +// Example: +// +// slog.Error("lookup failed", "err", ErrNotFound) +// // => err.error="resource not found", err.code="not_found" +func (s *Sentinel) LogValue() slog.Value { + return slog.GroupValue( + slog.String("error", s.msg), + slog.String("code", s.name), + ) +} + +// MarshalJSON serialises the sentinel to {"error":"...","code":"..."}. +func (s *Sentinel) MarshalJSON() ([]byte, error) { + return json.Marshal(struct { + Error string `json:"error"` + Code string `json:"code"` + }{ + Error: s.msg, + Code: s.name, + }) +} + +// With returns a new *Error that wraps this sentinel as its cause and carries +// the additional message msg. Use this to add call-site context to a sentinel +// without losing the ability to match the original with errors.Is. +// +// Example: +// +// var ErrNotFound = errors.Const("not_found", "resource not found") +// +// // At call site: +// err := ErrNotFound.With("user 42 not found") +// errors.Is(err, ErrNotFound) // true — sentinel is in the cause chain +func (s *Sentinel) With(msg string) *Error { + e := New(msg) + e.cause = s + return e +} + +// Const creates a new sentinel error with the given name and message. +// Store the result as a package-level var; never call Const in a hot path. +// +// Example: +// +// var ( +// ErrNotFound = errors.Const("not_found", "resource not found") +// ErrForbidden = errors.Const("forbidden", "access denied") +// ErrBadRequest = errors.Const("bad_request", "invalid input") +// ) +// +// func handle(err error) { +// switch { +// case errors.Is(err, ErrNotFound): // 404 +// case errors.Is(err, ErrForbidden): // 403 +// } +// } +func Const(name, msg string) *Sentinel { + return &Sentinel{name: name, msg: msg} +} diff --git a/vendor/github.com/olekukonko/errors/utils.go b/vendor/github.com/olekukonko/errors/utils.go index bb6753dfe6..f09d84c680 100644 --- a/vendor/github.com/olekukonko/errors/utils.go +++ b/vendor/github.com/olekukonko/errors/utils.go @@ -1,5 +1,3 @@ -// Package errors provides utility functions for error handling, including stack -// trace capture and function name extraction. package errors import ( @@ -11,28 +9,38 @@ import ( ) // captureStack captures a stack trace with the configured depth. -// Skip=0 captures the current call site; skips captureStack and its caller (+2 frames); thread-safe via stackPool. +// Immune to inlining: captures from frame 1 and trims by shifting within +// the same buffer so the pooled slice always retains its full capacity. func captureStack(skip int) []uintptr { buf := stackPool.Get().([]uintptr) buf = buf[:cap(buf)] - // +2 to skip captureStack and the immediate caller - n := runtime.Callers(skip+2, buf) + // Capture from frame 1 (skipping runtime.Callers itself). + // captureStack can never be inlined because it calls runtime.Callers, + // so buf[0] is always captureStack regardless of compiler inlining above. + n := runtime.Callers(1, buf) if n == 0 { stackPool.Put(buf) return nil } - // Create a new slice to return, avoiding direct use of pooled memory - stack := make([]uintptr, n) - copy(stack, buf[:n]) - stackPool.Put(buf) + // Trim leading internal frames in-place using copy, preserving the + // buffer's full capacity so the pool never fills with shrinking slices. + // skip+1: +1 for captureStack itself (always buf[0]). + trimmed := skip + 1 + if trimmed >= n { + stackPool.Put(buf) + return nil + } - return stack + length := n - trimmed + // Shift the useful frames to the start of buf — same backing array, + // same capacity, zero allocation. + copy(buf, buf[trimmed:n]) + return buf[:length] } // min returns the smaller of two integers. -// Simple helper for limiting stack trace size or other comparisons. func min(a, b int) int { if a < b { return a @@ -40,8 +48,7 @@ func min(a, b int) int { return b } -// clearMap removes all entries from a map. -// Helper function to reset map contents without reallocating. +// clearMap removes all entries from a map without reallocating it. func clearMap(m map[string]interface{}) { for k := range m { delete(m, k) @@ -49,12 +56,10 @@ func clearMap(m map[string]interface{}) { } // sqlNull detects if a value represents a SQL NULL type. -// Returns true for nil or invalid sql.Null* types (e.g., NullString, NullInt64); false otherwise. func sqlNull(v interface{}) bool { if v == nil { return true } - switch val := v.(type) { case sql.NullString: return !val.Valid @@ -71,8 +76,8 @@ func sqlNull(v interface{}) bool { } } -// getFuncName extracts the function name from an interface, typically a function or method. -// Returns "unknown" if the input is nil or invalid; trims leading dots from runtime name. +// getFuncName extracts the function name from an interface value. +// Returns "unknown" if the input is nil or invalid. func getFuncName(fn interface{}) string { if fn == nil { return "unknown" @@ -81,13 +86,28 @@ func getFuncName(fn interface{}) string { return strings.TrimPrefix(fullName, ".") } -// isInternalFrame determines if a stack frame is considered "internal". -// Returns true for frames from runtime, reflect, or this package’s subdirectories if FilterInternal is true. +// isInternalFrame reports whether a stack frame belongs to this library's +// internals and should be filtered from user-visible stack traces. +// +// Rules: +// - runtime.* and reflect.* are always internal. +// - _test.go files are NEVER internal: test functions must survive +// filtering so that assertions like "stack contains testing.tRunner" +// and "stack contains TestErrorTraceStackContent" can pass. +// - Source files under github.com/olekukonko/errors/ (errors.go, utils.go, +// helper.go, retry.go, multi_error.go) are internal. func isInternalFrame(frame runtime.Frame) bool { if strings.HasPrefix(frame.Function, "runtime.") || strings.HasPrefix(frame.Function, "reflect.") { return true } + // Exempt test files before the path-prefix check: errors_test.go lives + // at github.com/olekukonko/errors/errors_test.go which contains the + // "errors" suffix and would otherwise be incorrectly filtered. + if strings.HasSuffix(frame.File, "_test.go") { + return false + } + suffixes := []string{ "errors", "utils", @@ -95,10 +115,8 @@ func isInternalFrame(frame runtime.Frame) bool { "retry", "multi", } - - file := frame.File for _, v := range suffixes { - if strings.Contains(file, fmt.Sprintf("github.com/olekukonko/errors/%s", v)) { + if strings.Contains(frame.File, fmt.Sprintf("github.com/olekukonko/errors/%s", v)) { return true } } @@ -106,7 +124,6 @@ func isInternalFrame(frame runtime.Frame) bool { } // FormatError returns a formatted string representation of an error. -// Includes message, name, context, stack trace, and cause for *Error types; just message for others; "" if nil. func FormatError(err error) string { if err == nil { return "" @@ -138,13 +155,13 @@ func FormatError(err error) string { return sb.String() } -// Caller returns the file, line, and function name of the caller at the specified skip level. -// Skip=0 returns the caller of this function, 1 returns its caller, etc.; returns "unknown" if no caller found. +// Caller returns the file, line, and function name of the caller at skip level. +// Skip=0 returns the caller of this function, 1 returns its caller, etc. func Caller(skip int) (file string, line int, function string) { configMu.RLock() defer configMu.RUnlock() var pcs [1]uintptr - n := runtime.Callers(skip+2, pcs[:]) // +2 skips Caller and its immediate caller + n := runtime.Callers(skip+2, pcs[:]) if n == 0 { return "", 0, "unknown" } diff --git a/vendor/modules.txt b/vendor/modules.txt index b564773a5c..6b46e320f0 100644 --- a/vendor/modules.txt +++ b/vendor/modules.txt @@ -1194,7 +1194,7 @@ github.com/oklog/run # github.com/olekukonko/cat v0.0.0-20250911104152-50322a0618f6 ## explicit; go 1.21 github.com/olekukonko/cat -# github.com/olekukonko/errors v1.2.0 +# github.com/olekukonko/errors v1.3.0 ## explicit; go 1.21 github.com/olekukonko/errors # github.com/olekukonko/ll v0.1.6