Initial QSfera import

This commit is contained in:
Курнат Андрей
2026-06-07 10:20:04 +03:00
commit 2315f25754
16485 changed files with 4826827 additions and 0 deletions
+3
View File
@@ -0,0 +1,3 @@
# Created by .ignore support plugin (hsz.mobi)
.idea/
tmp/
+21
View File
@@ -0,0 +1,21 @@
MIT License
Copyright (c) 2025 Oleku Konko
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
File diff suppressed because it is too large Load Diff
+598
View File
@@ -0,0 +1,598 @@
package errors
import (
"context"
"fmt"
"log/slog" // Standard structured logging package
"reflect"
"strings"
"time"
)
// Chain executes functions sequentially with enhanced error handling.
// Logging is optional and configured via a slog.Handler.
type Chain struct {
steps []chainStep // List of steps to execute
errors []error // Accumulated errors during execution
config chainConfig // Chain-wide configuration
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
}
// chainStep represents a single step in the chain.
type chainStep struct {
execute func() error // Function to execute for this step
optional bool // If true, errors don't stop the chain
config stepConfig // Step-specific configuration
}
// chainConfig holds chain-wide settings.
type chainConfig struct {
timeout time.Duration // Maximum duration for the entire chain
maxErrors int // Maximum number of errors before stopping (-1 for unlimited)
autoWrap bool // Whether to automatically wrap errors with additional context
}
// stepConfig holds configuration for an individual step.
type stepConfig struct {
context map[string]interface{} // Arbitrary key-value pairs for context
category ErrorCategory // Category for error classification
code int // Numeric error code
retry *Retry // Retry policy for the step
logOnFail bool // Whether to log errors automatically
metricsLabel string // Label for metrics (not used in this code)
logAttrs []slog.Attr // Additional attributes for logging
}
// ChainOption defines a function that configures a Chain.
type ChainOption func(*Chain)
// NewChain creates a new Chain with the given options.
// Logging is disabled by default (logHandler is nil).
func NewChain(opts ...ChainOption) *Chain {
c := &Chain{
config: chainConfig{
autoWrap: true, // Enable error wrapping by default
maxErrors: -1, // No limit on errors by default
},
// logHandler is nil, meaning no logging unless explicitly configured
}
// Apply each configuration option
for _, opt := range opts {
opt(c)
}
return c
}
// ChainWithLogHandler sets a custom slog.Handler for logging.
// If handler is nil, logging is effectively disabled.
func ChainWithLogHandler(handler slog.Handler) ChainOption {
return func(c *Chain) {
c.logHandler = handler
}
}
// ChainWithTimeout sets a timeout for the entire chain.
func ChainWithTimeout(d time.Duration) ChainOption {
return func(c *Chain) {
c.config.timeout = d
}
}
// ChainWithMaxErrors sets the maximum number of errors allowed.
// A value <= 0 means no limit.
func ChainWithMaxErrors(max int) ChainOption {
return func(c *Chain) {
if max <= 0 {
c.config.maxErrors = -1 // No limit
} else {
c.config.maxErrors = max
}
}
}
// ChainWithAutoWrap enables or disables automatic error wrapping.
func ChainWithAutoWrap(auto bool) ChainOption {
return func(c *Chain) {
c.config.autoWrap = auto
}
}
// Step adds a new step to the chain with the provided function.
// The function must return an error or nil.
func (c *Chain) Step(fn func() error) *Chain {
if fn == nil {
// Panic to enforce valid input
panic("Chain.Step: provided function cannot be nil")
}
// Create a new step with default configuration
step := chainStep{execute: fn, config: stepConfig{}}
c.steps = append(c.steps, step)
// Update lastStep to point to the newly added 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 {
// Wrap the function and arguments into an executable step
wrappedFn, err := c.wrapCallable(fn, args...)
if err != nil {
// Panic on setup errors to catch them early
panic(fmt.Sprintf("Chain.Call setup error: %v", err))
}
// Add the wrapped function as a step
step := chainStep{execute: wrappedFn, config: stepConfig{}}
c.steps = append(c.steps, step)
c.lastStep = &c.steps[len(c.steps)-1]
return c
}
// Optional marks the last step as optional.
// Optional steps don't stop the chain on error.
func (c *Chain) Optional() *Chain {
if c.lastStep == nil {
// Panic if no step exists to mark as optional
panic("Chain.Optional: must call Step() or Call() before Optional()")
}
c.lastStep.optional = true
return c
}
// WithLog adds logging attributes to the last step.
func (c *Chain) WithLog(attrs ...slog.Attr) *Chain {
if c.lastStep == nil {
// Panic if no step exists to configure
panic("Chain.WithLog: must call Step() or Call() before WithLog()")
}
// Append attributes to the step's logging configuration
c.lastStep.config.logAttrs = append(c.lastStep.config.logAttrs, attrs...)
return c
}
// Timeout sets a timeout for the entire chain.
func (c *Chain) Timeout(d time.Duration) *Chain {
c.config.timeout = d
return c
}
// MaxErrors sets the maximum number of errors allowed.
func (c *Chain) MaxErrors(max int) *Chain {
if max <= 0 {
c.config.maxErrors = -1 // No limit
} else {
c.config.maxErrors = max
}
return c
}
// With adds a key-value pair to the last step's context.
func (c *Chain) With(key string, value interface{}) *Chain {
if c.lastStep == nil {
// Panic if no step exists to configure
panic("Chain.With: must call Step() or Call() before With()")
}
// Initialize context map if nil
if c.lastStep.config.context == nil {
c.lastStep.config.context = make(map[string]interface{})
}
// Add the key-value pair
c.lastStep.config.context[key] = value
return c
}
// Tag sets an error category for the last step.
func (c *Chain) Tag(category ErrorCategory) *Chain {
if c.lastStep == nil {
// Panic if no step exists to configure
panic("Chain.Tag: must call Step() or Call() before Tag()")
}
c.lastStep.config.category = category
return c
}
// Code sets a numeric error code for the last step.
func (c *Chain) Code(code int) *Chain {
if c.lastStep == nil {
// Panic if no step exists to configure
panic("Chain.Code: must call Step() or Call() before Code()")
}
c.lastStep.config.code = code
return c
}
// Retry configures retry behavior for the last step.
// Retry configures retry behavior for the last step.
func (c *Chain) Retry(maxAttempts int, delay time.Duration, opts ...RetryOption) *Chain {
if c.lastStep == nil {
panic("Chain.Retry: must call Step() or Call() before Retry()")
}
if maxAttempts < 1 {
maxAttempts = 1
}
// Define default retry options
retryOpts := []RetryOption{
WithMaxAttempts(maxAttempts),
WithDelay(delay),
WithRetryIf(func(err error) bool { return IsRetryable(err) }),
}
// Add logging for retry attempts if a handler is configured
if c.logHandler != nil {
step := c.lastStep
retryOpts = append(retryOpts, WithOnRetry(func(attempt int, err error) {
// Prepare logging attributes
logAttrs := []slog.Attr{
slog.Int("attempt", attempt),
slog.Int("max_attempts", maxAttempts),
}
// Enhance the error with step context
enhancedErr := c.enhanceError(err, step)
// Log the retry attempt
c.logError(enhancedErr, fmt.Sprintf("Retrying step (attempt %d/%d)", attempt, maxAttempts), step.config, logAttrs...)
}))
}
// Append any additional retry options
retryOpts = append(retryOpts, opts...)
// Create and assign the retry configuration
c.lastStep.config.retry = NewRetry(retryOpts...)
return c
}
// LogOnFail enables automatic logging of errors for the last step.
func (c *Chain) LogOnFail() *Chain {
if c.lastStep == nil {
// Panic if no step exists to configure
panic("Chain.LogOnFail: must call Step() or Call() before LogOnFail()")
}
c.lastStep.config.logOnFail = true
return c
}
// Run executes the chain, stopping on the first non-optional error.
// It returns the first error encountered or nil if all steps succeed.
func (c *Chain) Run() error {
// Create a context with timeout or cancellation
ctx, cancel := c.getContextAndCancel()
defer cancel()
c.cancel = cancel
// Clear any previous errors
c.errors = c.errors[:0]
// Execute each step in sequence
for i := range c.steps {
step := &c.steps[i]
// Check if the context has been canceled
select {
case <-ctx.Done():
err := ctx.Err()
// Enhance the error with step context
enhancedErr := c.enhanceError(err, step)
c.errors = append(c.errors, enhancedErr)
// Log the context error
c.logError(enhancedErr, "Chain stopped due to context error before step", step.config)
return enhancedErr
default:
}
// Execute the step
err := c.executeStep(ctx, step)
if err != nil {
// Enhance the error with step context
enhancedErr := c.enhanceError(err, step)
c.errors = append(c.errors, enhancedErr)
// Log the error if required
if step.config.logOnFail || !step.optional {
logMsg := "Chain stopped due to error in step"
if step.optional {
logMsg = "Optional step failed"
}
c.logError(enhancedErr, logMsg, step.config)
}
// Stop execution if the step is not optional
if !step.optional {
return enhancedErr
}
}
}
// Return nil if all steps completed successfully
return nil
}
// RunAll executes all steps, collecting errors without stopping.
// It returns a MultiError containing all errors or nil if none occurred.
func (c *Chain) RunAll() error {
ctx, cancel := c.getContextAndCancel()
defer cancel()
c.cancel = cancel
c.errors = c.errors[:0]
multi := NewMultiError()
for i := range c.steps {
step := &c.steps[i]
select {
case <-ctx.Done():
err := ctx.Err()
enhancedErr := c.enhanceError(err, step)
c.errors = append(c.errors, enhancedErr)
multi.Add(enhancedErr)
c.logError(enhancedErr, "Chain stopped due to context error before step (RunAll)", step.config)
goto endRunAll
default:
}
err := c.executeStep(ctx, step)
if err != nil {
enhancedErr := c.enhanceError(err, step)
c.errors = append(c.errors, enhancedErr)
multi.Add(enhancedErr)
if step.config.logOnFail && c.logHandler != nil {
c.logError(enhancedErr, "Step failed during RunAll", step.config)
}
if c.config.maxErrors > 0 && multi.Count() >= c.config.maxErrors {
if c.logHandler != nil {
// Create a logger to log the max errors condition
logger := slog.New(c.logHandler)
logger.LogAttrs(
context.Background(),
slog.LevelError,
fmt.Sprintf("Stopping RunAll after reaching max errors (%d)", c.config.maxErrors),
slog.Int("max_errors", c.config.maxErrors),
)
}
goto endRunAll
}
}
}
endRunAll:
return multi.Single()
}
// Errors returns a copy of the collected errors.
func (c *Chain) Errors() []error {
if len(c.errors) == 0 {
return nil
}
// Create a copy to prevent external modification
errs := make([]error, len(c.errors))
copy(errs, c.errors)
return errs
}
// Len returns the number of steps in the chain.
func (c *Chain) Len() int {
return len(c.steps)
}
// HasErrors checks if any errors were collected.
func (c *Chain) HasErrors() bool {
return len(c.errors) > 0
}
// LastError returns the most recent error or nil if none exist.
func (c *Chain) LastError() error {
if len(c.errors) > 0 {
return c.errors[len(c.errors)-1]
}
return nil
}
// Reset clears the chain's steps, errors, and context.
func (c *Chain) Reset() {
if c.cancel != nil {
// Cancel any active context
c.cancel()
c.cancel = nil
}
// Clear steps and errors
c.steps = c.steps[:0]
c.errors = c.errors[:0]
c.lastStep = nil
}
// Unwrap returns the collected errors (alias for Errors).
func (c *Chain) Unwrap() []error {
return c.errors
}
// getContextAndCancel creates a context based on the chain's timeout.
// 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)
}
// Create a cancellable context
return context.WithCancel(parentCtx)
}
// logError logs an error with step-specific context and attributes.
// It only logs if a handler is configured and the error is non-nil.
func (c *Chain) logError(err error, msg string, config stepConfig, additionalAttrs ...slog.Attr) {
// Skip logging if no handler is set or error is nil
if c == nil || c.logHandler == nil || err == nil {
return
}
// Create a logger on demand using the configured handler
logger := slog.New(c.logHandler)
// Initialize attributes with error and timestamp
allAttrs := make([]slog.Attr, 0, 5+len(config.logAttrs)+len(additionalAttrs))
allAttrs = append(allAttrs, slog.Any("error", err))
allAttrs = append(allAttrs, slog.Time("timestamp", time.Now()))
// Add step-specific metadata
if config.category != "" {
allAttrs = append(allAttrs, slog.String("category", string(config.category)))
}
if config.code != 0 {
allAttrs = append(allAttrs, slog.Int("code", config.code))
}
for k, v := range config.context {
allAttrs = append(allAttrs, slog.Any(k, v))
}
allAttrs = append(allAttrs, config.logAttrs...)
allAttrs = append(allAttrs, additionalAttrs...)
// Add stack trace and error name if the error is of type *Error
if e, ok := err.(*Error); ok {
if stack := e.Stack(); len(stack) > 0 {
// Format stack trace, truncating if too long
stackStr := "\n\t" + strings.Join(stack, "\n\t")
if len(stackStr) > 1000 {
stackStr = stackStr[:1000] + "..."
}
allAttrs = append(allAttrs, slog.String("stacktrace", stackStr))
}
if name := e.Name(); name != "" {
allAttrs = append(allAttrs, slog.String("error_name", name))
}
}
// Log the error at ERROR level with all attributes
// Use a defer to catch any panics during logging
defer func() {
if r := recover(); r != nil {
// Print to stdout to avoid infinite recursion
fmt.Printf("ERROR: Recovered from panic during logging: %v\nAttributes: %v\n", r, allAttrs)
}
}()
logger.LogAttrs(context.Background(), slog.LevelError, msg, allAttrs...)
}
// wrapCallable wraps a function and its arguments into an executable step.
// It uses reflection to validate the function and arguments.
func (c *Chain) wrapCallable(fn interface{}, args ...interface{}) (func() error, error) {
val := reflect.ValueOf(fn)
typ := val.Type()
// Ensure the provided value is a function
if typ.Kind() != reflect.Func {
return nil, fmt.Errorf("provided 'fn' is not a function (got %T)", fn)
}
// Check if the number of arguments matches the function's signature
if typ.NumIn() != len(args) {
return nil, fmt.Errorf("function expects %d arguments, but %d were provided", typ.NumIn(), len(args))
}
// Prepare argument values
argVals := make([]reflect.Value, len(args))
errorType := reflect.TypeOf((*error)(nil)).Elem()
for i, arg := range args {
expectedType := typ.In(i)
var providedVal reflect.Value
if arg != nil {
providedVal = reflect.ValueOf(arg)
// Check if the argument type is assignable to the expected type
if !providedVal.Type().AssignableTo(expectedType) {
// Special case for error interfaces
if expectedType.Kind() == reflect.Interface && expectedType.Implements(errorType) && providedVal.Type().Implements(errorType) {
// Allow error interface
} else {
return nil, fmt.Errorf("argument %d type mismatch: expected %s, got %s", i, expectedType, providedVal.Type())
}
}
} else {
// Handle nil arguments for nullable types
switch expectedType.Kind() {
case reflect.Chan, reflect.Func, reflect.Interface, reflect.Map, reflect.Pointer, reflect.Slice:
providedVal = reflect.Zero(expectedType)
default:
return nil, fmt.Errorf("argument %d is nil, but expected non-nillable type %s", i, expectedType)
}
}
argVals[i] = providedVal
}
// Validate the function's return type
if typ.NumOut() > 1 || (typ.NumOut() == 1 && !typ.Out(0).Implements(errorType)) {
return nil, fmt.Errorf("function must return either no values or a single error (got %d return values)", typ.NumOut())
}
// Return a wrapped function that calls the original with the provided arguments
return func() error {
results := val.Call(argVals)
if len(results) == 1 && results[0].Interface() != nil {
return results[0].Interface().(error)
}
return nil
}, nil
}
// executeStep runs a single step, applying retries if configured.
// This version is synchronous and avoids the bugs caused by the previous goroutine-based implementation.
func (c *Chain) executeStep(ctx context.Context, step *chainStep) error {
// First, check if the context has already been canceled before starting the step.
// This allows the chain to fail fast.
select {
case <-ctx.Done():
return ctx.Err()
default:
// Context is still active, proceed.
}
// If the step has retry logic configured...
if step.config.retry != nil {
// Create a new retry instance that is aware of the chain's context.
// The retry executor will be responsible for checking ctx.Done() between attempts.
retryExecutor := step.config.retry.Transform(WithContext(ctx))
// Execute the step's function directly. The retry mechanism will manage the loop,
// delays, and context cancellation checks. We pass step.execute without any
// extra goroutine wrappers.
return retryExecutor.Execute(step.execute)
}
// For a simple, non-retrying step, execute the function directly and synchronously
// in the current goroutine. This is the simplest, fastest, and most correct approach.
// It ensures that database connections are used and returned to the pool sequentially,
// preventing the deadlock issue.
return step.execute()
}
// enhanceError wraps an error with additional context from the step.
func (c *Chain) enhanceError(err error, step *chainStep) error {
if err == nil || !c.config.autoWrap {
// Return the error unchanged if nil or autoWrap is disabled
return err
}
// Initialize the base error
var baseError *Error
if e, ok := err.(*Error); ok {
// Copy existing *Error to preserve its properties
baseError = e.Copy()
} else {
// Create a new *Error wrapping the original
baseError = New(err.Error()).Wrap(err).WithStack()
}
if step != nil {
// Add step-specific context to the error
if step.config.category != "" && baseError.Category() == "" {
baseError.WithCategory(step.config.category)
}
if step.config.code != 0 && baseError.Code() == 0 {
baseError.WithCode(step.config.code)
}
for k, v := range step.config.context {
baseError.With(k, v)
}
for _, attr := range step.config.logAttrs {
baseError.With(attr.Key, attr.Value.Any())
}
if step.config.retry != nil && !baseError.HasContextKey(ctxRetry) {
// Mark the error as retryable if retries are configured
baseError.WithRetryable()
}
}
return baseError
}
File diff suppressed because it is too large Load Diff
+437
View File
@@ -0,0 +1,437 @@
package errors
import (
"context"
"errors"
"fmt"
"strings"
"time"
)
// As wraps errors.As, using custom type assertion for *Error types.
// Falls back to standard errors.As for non-*Error types.
// Returns false if either err or target is nil.
func As(err error, target interface{}) bool {
if err == nil || target == nil {
return false
}
// First try our custom *Error handling
if e, ok := err.(*Error); ok {
return e.As(target)
}
// Fall back to standard errors.As
return errors.As(err, target)
}
// Code returns the status code of an error, if it is an *Error.
// Returns 500 as a default for non-*Error types to indicate an internal error.
func Code(err error) int {
if e, ok := err.(*Error); ok {
return e.Code()
}
return DefaultCode
}
// Context extracts the context map from an error, if it is an *Error.
// Returns nil for non-*Error types or if no context is present.
func Context(err error) map[string]interface{} {
if e, ok := err.(*Error); ok {
return e.Context()
}
return nil
}
// Convert transforms any error into an *Error, preserving its message and wrapping it if needed.
// Returns nil if the input is nil; returns the original if already an *Error.
// Uses multiple strategies: direct assertion, errors.As, manual unwrapping, and fallback creation.
func Convert(err error) *Error {
if err == nil {
return nil
}
// First try direct type assertion (fast path)
if e, ok := err.(*Error); ok {
return e
}
// Try using errors.As (more flexible)
var e *Error
if errors.As(err, &e) {
return e
}
// Manual unwrapping as fallback
visited := make(map[error]bool)
for unwrapped := err; unwrapped != nil; {
if visited[unwrapped] {
break // Cycle detected
}
visited[unwrapped] = true
if e, ok := unwrapped.(*Error); ok {
return e
}
unwrapped = errors.Unwrap(unwrapped)
}
// Final fallback: create new error with original message and wrap it
return New(err.Error()).Wrap(err)
}
// Count returns the occurrence count of an error, if it is an *Error.
// Returns 0 for non-*Error types.
func Count(err error) uint64 {
if e, ok := err.(*Error); ok {
return e.Count()
}
return 0
}
// Find searches the error chain for the first error matching pred.
// Returns nil if no match is found or pred is nil; traverses both Unwrap() and Cause() chains.
func Find(err error, pred func(error) bool) error {
for current := err; current != nil; {
if pred(current) {
return current
}
// Attempt to unwrap using Unwrap() or Cause()
switch v := current.(type) {
case interface{ Unwrap() error }:
current = v.Unwrap()
case interface{ Cause() error }:
current = v.Cause()
default:
return nil
}
}
return nil
}
// From transforms any error into an *Error, preserving its message and wrapping it if needed.
// Alias of Convert; returns nil if input is nil, original if already an *Error.
func From(err error) *Error {
return Convert(err)
}
// FromContext creates an *Error from a context and an existing error.
// Enhances the error with context info: timeout status, deadline, or cancellation.
// Returns nil if input error is nil; does not store context values directly.
func FromContext(ctx context.Context, err error) *Error {
if err == nil {
return nil
}
e := New(err.Error())
// Handle context errors
switch ctx.Err() {
case context.DeadlineExceeded:
e.WithTimeout()
if deadline, ok := ctx.Deadline(); ok {
e.With("deadline", deadline.Format(time.RFC3339))
}
case context.Canceled:
e.With("cancelled", true)
}
return e
}
// Category returns the category of an error, if it is an *Error.
// Returns an empty string for non-*Error types or unset categories.
func Category(err error) string {
if e, ok := err.(*Error); ok {
return e.category
}
return ""
}
// Has checks if an error contains meaningful content.
// Returns true for non-nil standard errors or *Error with content (msg, name, template, or cause).
func Has(err error) bool {
if e, ok := err.(*Error); ok {
return e.Has()
}
return err != nil
}
// HasContextKey checks if the error's context contains the specified key.
// Returns false for non-*Error types or if the key is not present in the context.
func HasContextKey(err error, key string) bool {
if e, ok := err.(*Error); ok {
ctx := e.Context()
if ctx != nil {
_, exists := ctx[key]
return exists
}
}
return false
}
// Is wraps errors.Is, using custom matching for *Error types.
// Falls back to standard errors.Is for non-*Error types; returns true if err equals target.
func Is(err, target error) bool {
if err == nil || target == nil {
return err == target
}
if e, ok := err.(*Error); ok {
return e.Is(target)
}
// Use standard errors.Is for non-Error types
return errors.Is(err, target)
}
// IsError checks if an error is an instance of *Error.
// Returns true only for this package's custom error type; false for nil or other types.
func IsError(err error) bool {
_, ok := err.(*Error)
return ok
}
// IsEmpty checks if an error has no meaningful content.
// Returns true for nil errors, empty *Error instances, or standard errors with whitespace-only messages.
func IsEmpty(err error) bool {
if err == nil {
return true
}
if e, ok := err.(*Error); ok {
return e.IsEmpty()
}
return strings.TrimSpace(err.Error()) == ""
}
// IsNull checks if an error is nil or represents a NULL value.
// Delegates to *Errors IsNull for custom errors; uses sqlNull for others.
func IsNull(err error) bool {
if err == nil {
return true
}
if e, ok := err.(*Error); ok {
return e.IsNull()
}
return sqlNull(err)
}
// IsRetryable checks if an error is retryable.
// For *Error, checks context for retry flag; for others, looks for "retry" or timeout in message.
// Returns false for nil errors; thread-safe for *Error types.
func IsRetryable(err error) bool {
if err == nil {
return false
}
if e, ok := err.(*Error); ok {
e.mu.RLock()
defer e.mu.RUnlock()
// Check smallContext directly if context map isnt populated
for i := int32(0); i < e.smallCount; i++ {
if e.smallContext[i].key == ctxRetry {
if val, ok := e.smallContext[i].value.(bool); ok {
return val
}
}
}
// Check regular context
if e.context != nil {
if val, ok := e.context[ctxRetry].(bool); ok {
return val
}
}
// Check cause recursively
if e.cause != nil {
return IsRetryable(e.cause)
}
}
lowerMsg := strings.ToLower(err.Error())
return IsTimeout(err) || strings.Contains(lowerMsg, "retry")
}
// IsTimeout checks if an error indicates a timeout.
// For *Error, checks context for timeout flag; for others, looks for "timeout" in message.
// Returns false for nil errors.
func IsTimeout(err error) bool {
if err == nil {
return false
}
if e, ok := err.(*Error); ok {
if val, ok := e.Context()[ctxTimeout].(bool); ok {
return val
}
}
return strings.Contains(strings.ToLower(err.Error()), "timeout")
}
// Merge combines multiple errors into a single *Error.
// Aggregates messages with "; " separator, merges contexts and stacks; returns nil if no errors provided.
func Merge(errs ...error) *Error {
if len(errs) == 0 {
return nil
}
var messages []string
combined := New("")
for _, err := range errs {
if err == nil {
continue
}
messages = append(messages, err.Error())
if e, ok := err.(*Error); ok {
if e.stack != nil && combined.stack == nil {
combined.WithStack() // Capture stack from first *Error with stack
}
if ctx := e.Context(); ctx != nil {
for k, v := range ctx {
combined.With(k, v)
}
}
if e.cause != nil {
combined.Wrap(e.cause)
}
} else {
combined.Wrap(err)
}
}
if len(messages) > 0 {
combined.msg = strings.Join(messages, "; ")
}
return combined
}
// Name returns the name of an error, if it is an *Error.
// Returns an empty string for non-*Error types or unset names.
func Name(err error) string {
if e, ok := err.(*Error); ok {
return e.name
}
return ""
}
// UnwrapAll returns a slice of all errors in the chain, including the root error.
// Traverses both Unwrap() and Cause() chains; returns nil if err is nil.
func UnwrapAll(err error) []error {
if err == nil {
return nil
}
if e, ok := err.(*Error); ok {
return e.UnwrapAll()
}
var result []error
Walk(err, func(e error) {
result = append(result, e)
})
return result
}
// Stack extracts the stack trace from an error, if it is an *Error.
// Returns nil for non-*Error types or if no stack is present.
func Stack(err error) []string {
if e, ok := err.(*Error); ok {
return e.Stack()
}
return nil
}
// Transform applies transformations to an error, returning a new *Error.
// Creates a new *Error from non-*Error types before applying fn; returns nil if err is nil.
func Transform(err error, fn func(*Error)) *Error {
if err == nil {
return nil
}
if e, ok := err.(*Error); ok {
newErr := e.Copy()
fn(newErr)
return newErr
}
// If not an *Error, create a new one and transform it
newErr := New(err.Error())
fn(newErr)
return newErr
}
// Unwrap returns the underlying cause of an error, if it implements Unwrap.
// For *Error, returns cause; for others, returns the error itself; nil if err is nil.
func Unwrap(err error) error {
for current := err; current != nil; {
if e, ok := current.(*Error); ok {
if e.cause == nil {
return current
}
current = e.cause
} else {
return current
}
}
return nil
}
// Walk traverses the error chain, applying fn to each error.
// Supports both Unwrap() and Cause() interfaces; stops at nil or non-unwrappable errors.
func Walk(err error, fn func(error)) {
for current := err; current != nil; {
fn(current)
// Attempt to unwrap using Unwrap() or Cause()
switch v := current.(type) {
case interface{ Unwrap() error }:
current = v.Unwrap()
case interface{ Cause() error }:
current = v.Cause()
default:
return
}
}
}
// With adds a key-value pair to an error's context, if it is an *Error.
// Returns the original error unchanged if not an *Error; no-op for non-*Error types.
func With(err error, key string, value interface{}) error {
if e, ok := err.(*Error); ok {
return e.With(key, value)
}
return err
}
// WithStack converts any error to an *Error and captures a stack trace.
// Returns nil if input is nil; adds stack to existing *Error or wraps non-*Error types.
func WithStack(err error) *Error {
if err == nil {
return nil
}
if e, ok := err.(*Error); ok {
return e.WithStack()
}
return New(err.Error()).WithStack().Wrap(err)
}
// Wrap creates a new *Error that wraps another error with additional context.
// Uses a copy of the provided wrapper *Error; returns nil if err is nil.
func Wrap(err error, wrapper *Error) *Error {
if err == nil {
return nil
}
if wrapper == nil {
wrapper = newError()
}
newErr := wrapper.Copy()
newErr.cause = err
return newErr
}
// Wrapf creates a new formatted *Error that wraps another error.
// Formats the message and sets the cause; returns nil if err is nil.
func Wrapf(err error, format string, args ...interface{}) *Error {
if err == nil {
return nil
}
e := newError()
e.msg = fmt.Sprintf(format, args...)
e.cause = err
return e
}
// Err creates a new Error with the given message and wraps the provided error as its cause.
func Err(msg string, err error) *Error {
return New(msg).Wrap(err)
}
+225
View File
@@ -0,0 +1,225 @@
// File: inspect.go
// Updated to support both error and *Error with delegation for cleaner *Error handling
package errors
import (
stderrs "errors"
"fmt"
"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
}
fmt.Printf("\n=== Error Inspection ===\n")
fmt.Printf("Top-level error: %v\n", err)
fmt.Printf("Top-level error type: %T\n", err)
// Handle *Error directly
if e, ok := err.(*Error); ok {
InspectError(e)
return
}
// 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)
}
// Additional diagnostics
fmt.Println("\n--- Diagnostics ---")
if IsRetryable(err) {
fmt.Println("- Error chain contains retryable errors")
}
if IsTimeout(err) {
fmt.Println("- Error chain contains timeout errors")
}
if code := getErrorCode(err); code != 0 {
fmt.Printf("- Highest priority error code: %d\n", code)
}
fmt.Printf("========================\n\n")
}
// InspectError provides detailed inspection of a specific *Error instance
func InspectError(err *Error) {
if err == nil {
fmt.Println("No error occurred")
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")
}
if IsTimeout(err) {
fmt.Println("- Error chain contains timeout errors")
}
if code := err.Code(); code != 0 {
fmt.Printf("- Error code: %d\n", code)
}
fmt.Printf("========================\n\n")
}
// inspectSingleError handles inspection of a single error (may be part of a chain)
func inspectSingleError(err error) {
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)
}
}
}
// 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
}
// Prevent infinite loops if Unwrap returns the same error, or stop if no more unwrapping
if nextErr == currentErr || nextErr == nil {
break
}
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
}
}
}
// 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()
}
}
// 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()
}
}
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")
}
}
// 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
}
+423
View File
@@ -0,0 +1,423 @@
package errors
import (
"bytes"
"encoding/json"
"fmt"
"math/rand"
"strings"
"sync"
"sync/atomic"
)
// MultiError represents a thread-safe collection of errors with enhanced features.
// Supports limits, sampling, and custom formatting for error aggregation.
type MultiError struct {
errors []error
mu sync.RWMutex
// Configuration fields
limit int // Maximum number of errors to store (0 = unlimited)
formatter ErrorFormatter // Custom formatting function for error string
sampling bool // Whether sampling is enabled to limit error collection
sampleRate uint32 // Sampling percentage (1-100) when sampling is enabled
rand *rand.Rand // Random source for sampling (nil defaults to fastRand)
}
// ErrorFormatter defines a function for custom error message formatting.
// Takes a slice of errors and returns a single formatted string.
type ErrorFormatter func([]error) string
// MultiErrorOption configures MultiError behavior during creation.
type MultiErrorOption func(*MultiError)
// NewMultiError creates a new MultiError instance with optional configuration.
// Initial capacity is set to 4; applies options in the order provided.
func NewMultiError(opts ...MultiErrorOption) *MultiError {
m := &MultiError{
errors: make([]error, 0, 4),
limit: 0, // Unlimited by default
}
for _, opt := range opts {
opt(m)
}
return m
}
// Add appends an error to the collection with optional sampling, limit checks, and duplicate prevention.
// Ignores nil errors and duplicates based on string equality; thread-safe.
func (m *MultiError) Add(errs ...error) {
if len(errs) == 0 {
return
}
m.mu.Lock()
defer m.mu.Unlock()
for _, err := range errs {
if err == nil {
continue
}
// Check for duplicates by comparing error messages
duplicate := false
for _, e := range m.errors {
if e.Error() == err.Error() {
duplicate = true
break
}
}
if duplicate {
continue
}
// Apply sampling if enabled and collection isnt empty
if m.sampling && len(m.errors) > 0 {
var r uint32
if m.rand != nil {
r = uint32(m.rand.Int31n(100))
} else {
r = fastRand() % 100
}
if r > m.sampleRate { // Accept if random value is within sample rate
continue
}
}
// Respect limit if set
if m.limit > 0 && len(m.errors) >= m.limit {
continue
}
m.errors = append(m.errors, err)
}
}
// Addf formats and adds a new error to the collection.
func (m *MultiError) Addf(format string, args ...interface{}) {
m.Add(Newf(format, args...))
}
// Clear removes all errors from the collection.
// Thread-safe; resets the slice while preserving capacity.
func (m *MultiError) Clear() {
m.mu.Lock()
defer m.mu.Unlock()
m.errors = m.errors[:0]
}
// Count returns the number of errors in the collection.
// Thread-safe.
func (m *MultiError) Count() int {
m.mu.RLock()
defer m.mu.RUnlock()
return len(m.errors)
}
// Error returns a formatted string representation of the errors.
// Returns empty string if no errors, single error message if one exists,
// or a formatted list using custom formatter or default if multiple; thread-safe.
func (m *MultiError) Error() string {
m.mu.RLock()
defer m.mu.RUnlock()
switch len(m.errors) {
case 0:
return ""
case 1:
return m.errors[0].Error()
default:
if m.formatter != nil {
return m.formatter(m.errors)
}
return defaultFormat(m.errors)
}
}
// Errors returns a copy of the contained errors.
// Thread-safe; returns nil if no errors exist.
func (m *MultiError) Errors() []error {
m.mu.RLock()
defer m.mu.RUnlock()
if len(m.errors) == 0 {
return nil
}
errs := make([]error, len(m.errors))
copy(errs, m.errors)
return errs
}
// Filter returns a new MultiError containing only errors that match the predicate.
// Thread-safe; preserves original configuration including limit, formatter, and sampling.
func (m *MultiError) Filter(fn func(error) bool) *MultiError {
m.mu.RLock()
defer m.mu.RUnlock()
var opts []MultiErrorOption
opts = append(opts, WithLimit(m.limit))
if m.formatter != nil {
opts = append(opts, WithFormatter(m.formatter))
}
if m.sampling {
opts = append(opts, WithSampling(m.sampleRate))
}
filtered := NewMultiError(opts...)
for _, err := range m.errors {
if fn(err) {
filtered.Add(err)
}
}
return filtered
}
// First returns the first error in the collection, if any.
// Thread-safe; returns nil if the collection is empty.
func (m *MultiError) First() error {
m.mu.RLock()
defer m.mu.RUnlock()
if len(m.errors) > 0 {
return m.errors[0]
}
return nil
}
// Has reports whether the collection contains any errors.
// Thread-safe.
func (m *MultiError) Has() bool {
m.mu.RLock()
defer m.mu.RUnlock()
return len(m.errors) > 0
}
// Last returns the most recently added error in the collection, if any.
// Thread-safe; returns nil if the collection is empty.
func (m *MultiError) Last() error {
m.mu.RLock()
defer m.mu.RUnlock()
if len(m.errors) > 0 {
return m.errors[len(m.errors)-1]
}
return nil
}
// Merge combines another MultiError's errors into this one.
// Thread-safe; respects this instances limit and sampling settings; no-op if other is nil or empty.
func (m *MultiError) Merge(other *MultiError) {
if other == nil || !other.Has() {
return
}
other.mu.RLock()
defer other.mu.RUnlock()
for _, err := range other.errors {
m.Add(err)
}
}
// IsNull checks if the MultiError is empty or contains only null errors.
// Returns true if empty or all errors are null (via IsNull() or empty message); thread-safe.
func (m *MultiError) IsNull() bool {
m.mu.RLock()
defer m.mu.RUnlock()
// Fast path for empty MultiError
if len(m.errors) == 0 {
return true
}
// Check each error for null status
allNull := true
for _, err := range m.errors {
switch e := err.(type) {
case interface{ IsNull() bool }:
if !e.IsNull() {
allNull = false
break
}
case nil:
continue
default:
if e.Error() != "" {
allNull = false
break
}
}
}
return allNull
}
// Single returns nil if the collection is empty, the single error if only one exists,
// or the MultiError itself if multiple errors are present.
// Thread-safe; useful for unwrapping to a single error when possible.
func (m *MultiError) Single() error {
m.mu.RLock()
defer m.mu.RUnlock()
switch len(m.errors) {
case 0:
return nil
case 1:
return m.errors[0]
default:
return m
}
}
// String implements the Stringer interface for a concise string representation.
// Thread-safe; delegates to Error() for formatting.
func (m *MultiError) String() string {
return m.Error()
}
// Unwrap returns a copy of the contained errors for multi-error unwrapping.
// Implements the errors.Unwrap interface; thread-safe; returns nil if empty.
func (m *MultiError) Unwrap() []error {
return m.Errors()
}
// WithFormatter sets a custom error formatting function.
// Returns a MultiErrorOption for use with NewMultiError; overrides default formatting.
func WithFormatter(f ErrorFormatter) MultiErrorOption {
return func(m *MultiError) {
m.formatter = f
}
}
// WithLimit sets the maximum number of errors to store.
// Returns a MultiErrorOption for use with NewMultiError; 0 means unlimited, negative values are ignored.
func WithLimit(n int) MultiErrorOption {
return func(m *MultiError) {
if n < 0 {
n = 0 // Ensure non-negative limit
}
m.limit = n
}
}
// WithSampling enables error sampling with a specified rate (1-100).
// Returns a MultiErrorOption for use with NewMultiError; caps rate at 100 for validity.
func WithSampling(rate uint32) MultiErrorOption {
return func(m *MultiError) {
if rate > 100 {
rate = 100
}
m.sampling = true
m.sampleRate = rate
}
}
// WithRand sets a custom random source for sampling, useful for testing.
// Returns a MultiErrorOption for use with NewMultiError; defaults to fastRand if nil.
func WithRand(r *rand.Rand) MultiErrorOption {
return func(m *MultiError) {
m.rand = r
}
}
// MarshalJSON serializes the MultiError to JSON, including all contained errors and configuration metadata.
// Thread-safe; errors are serialized using their MarshalJSON method if available, otherwise as strings.
func (m *MultiError) MarshalJSON() ([]byte, error) {
m.mu.RLock()
defer m.mu.RUnlock()
// Get buffer from pool for efficiency
buf := jsonBufferPool.Get().(*bytes.Buffer)
defer jsonBufferPool.Put(buf)
buf.Reset()
// Create encoder
enc := json.NewEncoder(buf)
enc.SetEscapeHTML(false)
// Define JSON structure
type jsonError struct {
Error interface{} `json:"error"` // Holds either JSON-marshaled error or string
}
je := struct {
Count int `json:"count"` // Number of errors
Limit int `json:"limit,omitempty"` // Maximum error limit (omitted if 0)
Sampling bool `json:"sampling,omitempty"` // Whether sampling is enabled
SampleRate uint32 `json:"sample_rate,omitempty"` // Sampling rate (1-100, omitted if not sampling)
Errors []jsonError `json:"errors"` // List of errors
}{
Count: len(m.errors),
Limit: m.limit,
Sampling: m.sampling,
SampleRate: m.sampleRate,
}
// Serialize each error
je.Errors = make([]jsonError, len(m.errors))
for i, err := range m.errors {
if err == nil {
je.Errors[i] = jsonError{Error: nil}
continue
}
// 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
je.Errors[i] = jsonError{Error: err.Error()}
} else {
var raw json.RawMessage = marshaled
je.Errors[i] = jsonError{Error: raw}
}
} else {
// Use error string for non-marshaler errors
je.Errors[i] = jsonError{Error: err.Error()}
}
}
// Encode JSON
if err := enc.Encode(je); err != nil {
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]
}
return result, nil
}
// defaultFormat provides the default formatting for multiple errors.
// Returns a semicolon-separated list prefixed with the error count (e.g., "errors(3): err1; err2; err3").
func defaultFormat(errs []error) string {
var sb strings.Builder
sb.WriteString(fmt.Sprintf("errors(%d): ", len(errs)))
for i, err := range errs {
if i > 0 {
sb.WriteString("; ")
}
sb.WriteString(err.Error())
}
return sb.String()
}
// fastRand generates a quick pseudo-random number for sampling.
// Uses a simple xorshift algorithm based on the current time; not cryptographically secure.
var fastRandState uint32 = 1 // Must be non-zero
func fastRand() uint32 {
for {
// Atomically load the current state
old := atomic.LoadUint32(&fastRandState)
// Xorshift computation
x := old
x ^= x << 13
x ^= x >> 17
x ^= x << 5
// Attempt to store the new state atomically
if atomic.CompareAndSwapUint32(&fastRandState, old, x) {
return x
}
// Otherwise retry
}
}
+75
View File
@@ -0,0 +1,75 @@
// pool.go
package errors
import (
"sync"
"sync/atomic"
)
// ErrorPool is a high-performance, thread-safe pool for reusing *Error instances.
// Reduces allocation overhead by recycling errors; tracks hit/miss statistics.
type ErrorPool struct {
pool sync.Pool // Underlying pool for storing *Error instances
poolStats struct { // Embedded struct for pool usage statistics
hits atomic.Int64 // Number of times an error was reused from the pool
misses atomic.Int64 // Number of times a new error was created due to pool miss
}
}
// NewErrorPool creates a new ErrorPool instance.
// Initializes the pool with a New function that returns a fresh *Error with default smallContext.
func NewErrorPool() *ErrorPool {
return &ErrorPool{
pool: sync.Pool{
New: func() interface{} {
return &Error{
smallContext: [contextSize]contextItem{},
}
},
},
}
}
// Get retrieves an *Error from the pool or creates a new one if pooling is disabled or pool is empty.
// Resets are handled by Put; thread-safe; updates hit/miss stats when pooling is enabled.
func (ep *ErrorPool) Get() *Error {
if currentConfig.disablePooling {
return &Error{
smallContext: [contextSize]contextItem{},
}
}
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{
smallContext: [contextSize]contextItem{},
}
}
ep.poolStats.hits.Add(1)
return e
}
// Put returns an *Error to the pool after resetting it.
// Ignores nil errors or if pooling is disabled; preserves stack capacity; thread-safe.
func (ep *ErrorPool) Put(e *Error) {
if e == nil || currentConfig.disablePooling {
return
}
// Reset the error to a clean state, preserving capacity
e.Reset()
// Reset stack length while keeping capacity for reuse
if e.stack != nil {
e.stack = e.stack[:0]
}
ep.pool.Put(e)
}
// Stats returns the current pool statistics as hits and misses.
// Thread-safe; uses atomic loads to ensure accurate counts.
func (ep *ErrorPool) Stats() (hits, misses int64) {
return ep.poolStats.hits.Load(), ep.poolStats.misses.Load()
}
+24
View File
@@ -0,0 +1,24 @@
//go:build go1.24
// +build go1.24
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.
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
}
}
// 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+
}
+24
View File
@@ -0,0 +1,24 @@
//go:build !go1.24
// +build !go1.24
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.
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
}
})
}
}
// clearCleanup removes any finalizer set on an *Error.
// Only active for Go versions < 1.24; ensures no cleanup action occurs on garbage collection.
func (ep *ErrorPool) clearCleanup(e *Error) {
runtime.SetFinalizer(e, nil) // Disable finalizer
}
+368
View File
@@ -0,0 +1,368 @@
// Package errors provides utilities for error handling, including a flexible retry mechanism.
package errors
import (
"context"
"math/rand"
"time"
)
// BackoffStrategy defines the interface for calculating retry delays.
type BackoffStrategy interface {
// Backoff returns the delay for a given attempt based on the base delay.
Backoff(attempt int, baseDelay time.Duration) time.Duration
}
// ConstantBackoff provides a fixed delay for each retry attempt.
type ConstantBackoff struct{}
// Backoff returns the base delay regardless of the attempt number.
// Implements BackoffStrategy with a constant delay.
func (c ConstantBackoff) Backoff(_ int, baseDelay time.Duration) time.Duration {
return baseDelay
}
// ExponentialBackoff provides an exponentially increasing delay for retry attempts.
type ExponentialBackoff struct{}
// Backoff returns a delay that doubles with each attempt, starting from the base delay.
// Uses bit shifting for efficient exponential growth (e.g., baseDelay * 2^(attempt-1)).
func (e ExponentialBackoff) Backoff(attempt int, baseDelay time.Duration) time.Duration {
if attempt <= 1 {
return baseDelay
}
return baseDelay * time.Duration(1<<uint(attempt-1))
}
// LinearBackoff provides a linearly increasing delay for retry attempts.
type LinearBackoff struct{}
// Backoff returns a delay that increases linearly with each attempt (e.g., baseDelay * attempt).
// Implements BackoffStrategy with linear progression.
func (l LinearBackoff) Backoff(attempt int, baseDelay time.Duration) time.Duration {
return baseDelay * time.Duration(attempt)
}
// RetryOption configures a Retry instance.
// Defines a function type for setting retry parameters.
type RetryOption func(*Retry)
// Retry represents a retryable operation with configurable backoff and retry logic.
// Supports multiple attempts, delay strategies, jitter, and context-aware cancellation.
type Retry struct {
maxAttempts int // Maximum number of attempts (including initial try)
delay time.Duration // Base delay for backoff calculations
maxDelay time.Duration // Maximum delay cap to prevent excessive waits
retryIf func(error) bool // Condition to determine if retry should occur
onRetry func(int, error) // Callback executed after each failed attempt
backoff BackoffStrategy // Strategy for calculating retry delays
jitter bool // Whether to add random jitter to delays
ctx context.Context // Context for cancellation and deadlines
}
// NewRetry creates a new Retry instance with the given options.
// Defaults: 3 attempts, 100ms base delay, 10s max delay, exponential backoff with jitter,
// and retrying on IsRetryable errors; ensures retryIf is never nil.
func NewRetry(options ...RetryOption) *Retry {
r := &Retry{
maxAttempts: 3,
delay: 100 * time.Millisecond,
maxDelay: 10 * time.Second,
retryIf: func(err error) bool { return IsRetryable(err) },
onRetry: nil,
backoff: ExponentialBackoff{},
jitter: true,
ctx: context.Background(),
}
for _, opt := range options {
opt(r)
}
// Ensure retryIf is never nil, falling back to IsRetryable
if r.retryIf == nil {
r.retryIf = func(err error) bool { return IsRetryable(err) }
}
return r
}
// addJitter adds ±25% jitter to avoid thundering herd problems.
// Returns a duration adjusted by a random value between -25% and +25% of the input; not thread-safe.
func addJitter(d time.Duration) time.Duration {
jitter := time.Duration(rand.Int63n(int64(d/2))) - (d / 4)
return d + jitter
}
// Attempts returns the configured maximum number of retry attempts.
// Includes the initial attempt in the count.
func (r *Retry) Attempts() int {
return r.maxAttempts
}
// Execute runs the provided function with the configured retry logic.
// Returns nil on success or the last error if all attempts fail; respects context cancellation.
func (r *Retry) Execute(fn func() error) error {
var lastErr error
for attempt := 1; attempt <= r.maxAttempts; attempt++ {
// Check context before each attempt
select {
case <-r.ctx.Done():
return r.ctx.Err()
default:
}
err := fn()
if err == nil {
return nil
}
lastErr = err
// Check if we should retry
if r.retryIf != nil && !r.retryIf(err) {
return err
}
if r.onRetry != nil {
r.onRetry(attempt, err)
}
// Don't delay after last attempt
if attempt == r.maxAttempts {
break
}
// Calculate delay with backoff
delay := r.backoff.Backoff(attempt, r.delay)
if r.maxDelay > 0 && delay > r.maxDelay {
delay = r.maxDelay
}
if r.jitter {
delay = addJitter(delay)
}
// Wait with context
select {
case <-r.ctx.Done():
return r.ctx.Err()
case <-time.After(delay):
}
}
return lastErr
}
// ExecuteContext runs the provided function with retry logic, respecting context cancellation.
// Returns nil on success or the last error if all attempts fail or context is cancelled.
func (r *Retry) ExecuteContext(ctx context.Context, fn func() error) error {
var lastErr error
// If the retry instance already has a context, use it. Otherwise, use the provided one.
// If both are provided, maybe create a derived context? For now, prioritize the one from WithContext.
execCtx := r.ctx
if execCtx == context.Background() && ctx != nil { // Use provided ctx if retry ctx is default and provided one isn't nil
execCtx = ctx
} else if ctx == nil { // Ensure we always have a non-nil context
execCtx = context.Background()
}
// Note: This logic might need refinement depending on how contexts should interact.
// A safer approach might be: if r.ctx != background, use it. Else use provided ctx.
for attempt := 1; attempt <= r.maxAttempts; attempt++ {
// Check context before executing the function
select {
case <-execCtx.Done():
return execCtx.Err() // Return context error immediately
default:
// Context is okay, proceed
}
err := fn()
if err == nil {
return nil // Success
}
// Check if retry is applicable based on the error
if r.retryIf != nil && !r.retryIf(err) {
return err // Not retryable, return the error
}
lastErr = err // Store the last encountered error
// Execute the OnRetry callback if configured
if r.onRetry != nil {
r.onRetry(attempt, err)
}
// Exit loop if this was the last attempt
if attempt == r.maxAttempts {
break
}
// --- 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
}
if r.jitter {
currentDelay = addJitter(currentDelay)
}
if currentDelay < 0 { // Ensure delay isn't negative after jitter
currentDelay = 0
}
// --- Wait for the delay or context cancellation ---
select {
case <-execCtx.Done():
// If context is cancelled during the wait, return the context error
// Often more informative than returning the last application error.
return execCtx.Err()
case <-time.After(currentDelay):
// Wait finished, continue to the next attempt
}
}
// All attempts failed, return the last error encountered
return lastErr
}
// Transform creates a new Retry instance with modified configuration.
// Copies all settings from the original Retry and applies the given options.
func (r *Retry) Transform(opts ...RetryOption) *Retry {
newRetry := &Retry{
maxAttempts: r.maxAttempts,
delay: r.delay,
maxDelay: r.maxDelay,
retryIf: r.retryIf,
onRetry: r.onRetry,
backoff: r.backoff,
jitter: r.jitter,
ctx: r.ctx,
}
for _, opt := range opts {
opt(newRetry)
}
return newRetry
}
// WithBackoff sets the backoff strategy using the BackoffStrategy interface.
// Returns a RetryOption; no-op if strategy is nil, retaining the existing strategy.
func WithBackoff(strategy BackoffStrategy) RetryOption {
return func(r *Retry) {
if strategy != nil {
r.backoff = strategy
}
}
}
// WithContext sets the context for cancellation and deadlines.
// Returns a RetryOption; retains context.Background if ctx is nil.
func WithContext(ctx context.Context) RetryOption {
return func(r *Retry) {
if ctx != nil {
r.ctx = ctx
}
}
}
// WithDelay sets the initial delay between retries.
// Returns a RetryOption; ensures non-negative delay by setting negatives to 0.
func WithDelay(delay time.Duration) RetryOption {
return func(r *Retry) {
if delay < 0 {
delay = 0
}
r.delay = delay
}
}
// WithJitter enables or disables jitter in the backoff delay.
// Returns a RetryOption; toggles random delay variation.
func WithJitter(jitter bool) RetryOption {
return func(r *Retry) {
r.jitter = jitter
}
}
// WithMaxAttempts sets the maximum number of retry attempts.
// Returns a RetryOption; ensures at least 1 attempt by adjusting lower values.
func WithMaxAttempts(maxAttempts int) RetryOption {
return func(r *Retry) {
if maxAttempts < 1 {
maxAttempts = 1
}
r.maxAttempts = maxAttempts
}
}
// WithMaxDelay sets the maximum delay between retries.
// Returns a RetryOption; ensures non-negative delay by setting negatives to 0.
func WithMaxDelay(maxDelay time.Duration) RetryOption {
return func(r *Retry) {
if maxDelay < 0 {
maxDelay = 0
}
r.maxDelay = maxDelay
}
}
// WithOnRetry sets a callback to execute after each failed attempt.
// Returns a RetryOption; callback receives attempt number and error.
func WithOnRetry(onRetry func(attempt int, err error)) RetryOption {
return func(r *Retry) {
r.onRetry = onRetry
}
}
// WithRetryIf sets the condition under which to retry.
// Returns a RetryOption; retains IsRetryable default if retryIf is nil.
func WithRetryIf(retryIf func(error) bool) RetryOption {
return func(r *Retry) {
if retryIf != nil {
r.retryIf = retryIf
}
}
}
// ExecuteReply runs the provided function with retry logic and returns its result.
// Returns the result and nil on success, or zero value and last error on failure; generic type T.
func ExecuteReply[T any](r *Retry, fn func() (T, error)) (T, error) {
var lastErr error
var zero T
for attempt := 1; attempt <= r.maxAttempts; attempt++ {
result, err := fn()
if err == nil {
return result, nil
}
// Check if retry is applicable; return immediately if not retryable
if r.retryIf != nil && !r.retryIf(err) {
return zero, err
}
lastErr = err
if r.onRetry != nil {
r.onRetry(attempt, err)
}
if attempt == r.maxAttempts {
break
}
// Calculate delay with backoff, cap at maxDelay, and apply jitter if enabled
currentDelay := r.backoff.Backoff(attempt, r.delay)
if currentDelay > r.maxDelay {
currentDelay = r.maxDelay
}
if r.jitter {
currentDelay = addJitter(currentDelay)
}
// Wait with respect to context cancellation or timeout
select {
case <-r.ctx.Done():
return zero, r.ctx.Err()
case <-time.After(currentDelay):
}
}
return zero, lastErr
}
+153
View File
@@ -0,0 +1,153 @@
// Package errors provides utility functions for error handling, including stack
// trace capture and function name extraction.
package errors
import (
"database/sql"
"fmt"
"reflect"
"runtime"
"strings"
)
// 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.
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)
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)
return stack
}
// 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
}
return b
}
// clearMap removes all entries from a map.
// Helper function to reset map contents without reallocating.
func clearMap(m map[string]interface{}) {
for k := range m {
delete(m, k)
}
}
// 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
case sql.NullTime:
return !val.Valid
case sql.NullInt64:
return !val.Valid
case sql.NullBool:
return !val.Valid
case sql.NullFloat64:
return !val.Valid
default:
return false
}
}
// 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.
func getFuncName(fn interface{}) string {
if fn == nil {
return "unknown"
}
fullName := runtime.FuncForPC(reflect.ValueOf(fn).Pointer()).Name()
return strings.TrimPrefix(fullName, ".")
}
// isInternalFrame determines if a stack frame is considered "internal".
// Returns true for frames from runtime, reflect, or this packages subdirectories if FilterInternal is true.
func isInternalFrame(frame runtime.Frame) bool {
if strings.HasPrefix(frame.Function, "runtime.") || strings.HasPrefix(frame.Function, "reflect.") {
return true
}
suffixes := []string{
"errors",
"utils",
"helper",
"retry",
"multi",
}
file := frame.File
for _, v := range suffixes {
if strings.Contains(file, fmt.Sprintf("github.com/olekukonko/errors/%s", v)) {
return true
}
}
return false
}
// FormatError returns a formatted string representation of an error.
// Includes message, name, context, stack trace, and cause for *Error types; just message for others; "<nil>" if nil.
func FormatError(err error) string {
if err == nil {
return "<nil>"
}
var sb strings.Builder
if e, ok := err.(*Error); ok {
sb.WriteString(fmt.Sprintf("Error: %s\n", e.Error()))
if e.name != "" {
sb.WriteString(fmt.Sprintf("Name: %s\n", e.name))
}
if ctx := e.Context(); len(ctx) > 0 {
sb.WriteString("Context:\n")
for k, v := range ctx {
sb.WriteString(fmt.Sprintf("\t%s: %v\n", k, v))
}
}
if stack := e.Stack(); len(stack) > 0 {
sb.WriteString("Stack Trace:\n")
for _, frame := range stack {
sb.WriteString(fmt.Sprintf("\t%s\n", frame))
}
}
if e.cause != nil {
sb.WriteString(fmt.Sprintf("Caused by: %s\n", FormatError(e.cause)))
}
} else {
sb.WriteString(fmt.Sprintf("Error: %s\n", err.Error()))
}
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.
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
if n == 0 {
return "", 0, "unknown"
}
frame, _ := runtime.CallersFrames(pcs[:n]).Next()
return frame.File, frame.Line, frame.Function
}