Skip to content

Commit 247ae3b

Browse files
committed
cleaner implementation and ensure autofree works as intended
1 parent 8af6dc2 commit 247ae3b

9 files changed

Lines changed: 366 additions & 274 deletions

File tree

.github/workflows/go.yml

Lines changed: 20 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,3 @@
1-
# This workflow will build a golang project
2-
# For more information see: https://docs.github.com/en/actions/automating-builds-and-tests/building-and-testing-go
3-
41
name: Go
52

63
on:
@@ -10,20 +7,28 @@ on:
107
branches: [ "main" ]
118

129
jobs:
13-
14-
build:
10+
test:
11+
name: Test (Go ${{ matrix.go-version }})
1512
runs-on: ubuntu-latest
13+
14+
strategy:
15+
fail-fast: false
16+
matrix:
17+
go-version:
18+
- '1.21' # Represents the pool_below_1_24 path (SetFinalizer)
19+
- '1.24' # Represents the pool_above_1_24 path (AddCleanup)
20+
1621
steps:
17-
- uses: actions/checkout@v4
22+
- uses: actions/checkout@v4
1823

19-
- name: Set up Go
20-
uses: actions/setup-go@v4
21-
with:
22-
go-version: '1.21'
23-
cache: false
24+
- name: Set up Go ${{ matrix.go-version }}
25+
uses: actions/setup-go@v4
26+
with:
27+
go-version: ${{ matrix.go-version }}
28+
cache: false
2429

25-
- name: Build
26-
run: go build -v ./...
30+
- name: Build
31+
run: go build -v ./...
2732

28-
- name: Test
29-
run: go test -v ./...
33+
- name: Test (race + cover)
34+
run: go test -count=1 -race -timeout 120s -cover -v ./...

base.go

Lines changed: 178 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,178 @@
1+
package errors
2+
3+
import (
4+
"bytes"
5+
"fmt"
6+
"regexp"
7+
"sync"
8+
)
9+
10+
// Constants defining default configuration and context keys.
11+
const (
12+
ctxTimeout = "[error] timeout" // Context key marking timeout errors.
13+
ctxRetry = "[error] retry" // Context key marking retryable errors.
14+
15+
contextSize = 4 // Initial size of fixed-size context array for small contexts.
16+
bufferSize = 256 // Initial buffer size for JSON marshaling.
17+
warmUpSize = 100 // Number of errors to pre-warm the pool for efficiency.
18+
stackDepth = 32 // Maximum stack trace depth to prevent excessive memory use.
19+
20+
DefaultCode = 500 // Default HTTP status code for errors if not specified.
21+
)
22+
23+
// spaceRe is a precompiled regex for normalizing whitespace in error messages.
24+
var spaceRe = regexp.MustCompile(`\s+`)
25+
26+
// jsonBufferPool manages reusable buffers for JSON marshaling to reduce allocations.
27+
var (
28+
jsonBufferPool = sync.Pool{
29+
New: func() interface{} {
30+
return bytes.NewBuffer(make([]byte, 0, bufferSize))
31+
},
32+
}
33+
)
34+
35+
// ErrorCategory is a string type for categorizing errors (e.g., "network", "validation").
36+
type ErrorCategory string
37+
38+
// ErrorOpts provides options for customizing error creation.
39+
type ErrorOpts struct {
40+
SkipStack int // Number of stack frames to skip when capturing the stack trace.
41+
}
42+
43+
// Config defines the global configuration for the errors package, controlling
44+
// stack depth, context size, pooling, and frame filtering.
45+
type Config struct {
46+
StackDepth int // Maximum stack trace depth; 0 uses default (32).
47+
ContextSize int // Initial context map size; 0 uses default (4).
48+
DisablePooling bool // If true, disables object pooling for errors.
49+
FilterInternal bool // If true, filters internal package frames from stack traces.
50+
AutoFree bool // If true, automatically returns errors to pool when GC collects them.
51+
}
52+
53+
// cachedConfig holds the current configuration, updated only by Configure().
54+
// Protected by configMu for thread-safety.
55+
type cachedConfig struct {
56+
stackDepth int
57+
contextSize int
58+
disablePooling bool
59+
filterInternal bool
60+
autoFree bool
61+
}
62+
63+
var (
64+
// currentConfig stores the active configuration, read frequently and updated rarely.
65+
currentConfig cachedConfig
66+
// configMu protects updates to currentConfig for thread-safety.
67+
configMu sync.RWMutex
68+
// errorPool manages reusable Error instances to reduce allocations.
69+
errorPool = NewErrorPool()
70+
// stackPool manages reusable stack trace slices for efficiency.
71+
stackPool = sync.Pool{
72+
New: func() interface{} {
73+
return make([]uintptr, currentConfig.stackDepth)
74+
},
75+
}
76+
// emptyError is a pre-allocated empty error for lightweight reuse.
77+
emptyError = &Error{
78+
smallContext: [contextSize]contextItem{},
79+
msg: "",
80+
name: "",
81+
template: "",
82+
cause: nil,
83+
}
84+
)
85+
86+
// contextItem holds a single key-value pair in the smallContext array.
87+
type contextItem struct {
88+
key string
89+
value interface{}
90+
}
91+
92+
// init sets up the package with default configuration and pre-warms the error pool.
93+
func init() {
94+
currentConfig = cachedConfig{
95+
stackDepth: stackDepth,
96+
contextSize: contextSize,
97+
disablePooling: false,
98+
filterInternal: true,
99+
autoFree: false, // opt-in; explicit Free() is the safe default
100+
}
101+
WarmPool(warmUpSize) // Pre-allocate errors for performance.
102+
}
103+
104+
// Configure updates the global configuration for the errors package.
105+
// It is thread-safe and should be called early to avoid race conditions.
106+
// Changes apply to all subsequent error operations.
107+
// Example:
108+
//
109+
// errors.Configure(errors.Config{StackDepth: 16, DisablePooling: true})
110+
func Configure(cfg Config) {
111+
configMu.Lock()
112+
defer configMu.Unlock()
113+
114+
if cfg.StackDepth != 0 {
115+
currentConfig.stackDepth = cfg.StackDepth
116+
}
117+
if cfg.ContextSize != 0 {
118+
currentConfig.contextSize = cfg.ContextSize
119+
}
120+
currentConfig.disablePooling = cfg.DisablePooling
121+
currentConfig.filterInternal = cfg.FilterInternal
122+
currentConfig.autoFree = cfg.AutoFree
123+
}
124+
125+
// WarmPool pre-populates the error pool with count instances.
126+
// Improves performance by reducing initial allocations.
127+
// No-op if pooling is disabled.
128+
// Example:
129+
//
130+
// errors.WarmPool(1000)
131+
func WarmPool(count int) {
132+
if currentConfig.disablePooling {
133+
return
134+
}
135+
for i := 0; i < count; i++ {
136+
e := &Error{
137+
smallContext: [contextSize]contextItem{},
138+
stack: nil,
139+
}
140+
errorPool.Put(e)
141+
stackPool.Put(make([]uintptr, 0, currentConfig.stackDepth))
142+
}
143+
}
144+
145+
// WarmStackPool pre-populates the stack pool with count slices.
146+
// Improves performance for stack-intensive operations.
147+
// No-op if pooling is disabled.
148+
// Example:
149+
//
150+
// errors.WarmStackPool(500)
151+
func WarmStackPool(count int) {
152+
if currentConfig.disablePooling {
153+
return
154+
}
155+
for i := 0; i < count; i++ {
156+
stackPool.Put(make([]uintptr, 0, currentConfig.stackDepth))
157+
}
158+
}
159+
160+
// FmtErrorCheck safely formats a string using fmt.Sprintf, catching panics.
161+
// Returns the formatted string and any error encountered.
162+
// Internal use by Newf to validate format strings.
163+
// Example:
164+
//
165+
// result, err := FmtErrorCheck("value: %s", "test")
166+
func FmtErrorCheck(format string, args ...interface{}) (result string, err error) {
167+
defer func() {
168+
if r := recover(); r != nil {
169+
if e, ok := r.(error); ok {
170+
err = e
171+
} else {
172+
err = fmt.Errorf("panic during formatting: %v", r)
173+
}
174+
}
175+
}()
176+
result = fmt.Sprintf(format, args...)
177+
return result, nil
178+
}

chain.go

Lines changed: 20 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import (
66
"log/slog" // Standard structured logging package
77
"reflect"
88
"strings"
9+
"sync"
910
"time"
1011
)
1112

@@ -18,6 +19,8 @@ type Chain struct {
1819
lastStep *chainStep // Pointer to the last added step for configuration
1920
logHandler slog.Handler // Optional logging handler (nil means no logging)
2021
cancel context.CancelFunc // Function to cancel the context
22+
runCtx context.Context // Active context for Run/RunAll; shared with StepCtx closures
23+
configMu sync.RWMutex // Protects chainConfig against concurrent Timeout() calls
2124
}
2225

2326
// chainStep represents a single step in the chain.
@@ -136,9 +139,14 @@ func (c *Chain) StepCtx(fn func(ctx context.Context) error) *Chain {
136139
}
137140
// Wrap fn so it satisfies the internal func() error signature used by
138141
// executeStep. The context is captured at execution time via getContextAndCancel.
142+
// Close over c.runCtx — set by Run/RunAll to the chain-level context.
143+
// This ensures StepCtx steps share the same deadline as the chain,
144+
// rather than each getting a fresh full-duration context.
139145
wrapped := func() error {
140-
ctx, cancel := c.getContextAndCancel()
141-
defer cancel()
146+
ctx := c.runCtx
147+
if ctx == nil {
148+
ctx = context.Background()
149+
}
142150
return fn(ctx)
143151
}
144152
step := chainStep{execute: wrapped, config: stepConfig{}}
@@ -186,8 +194,11 @@ func (c *Chain) WithLog(attrs ...slog.Attr) *Chain {
186194
}
187195

188196
// Timeout sets a timeout for the entire chain.
197+
// Thread-safe: protected by configMu.
189198
func (c *Chain) Timeout(d time.Duration) *Chain {
199+
c.configMu.Lock()
190200
c.config.timeout = d
201+
c.configMu.Unlock()
191202
return c
192203
}
193204

@@ -293,6 +304,7 @@ func (c *Chain) Run() error {
293304
ctx, cancel := c.getContextAndCancel()
294305
defer cancel()
295306
c.cancel = cancel
307+
c.runCtx = ctx // share deadline with StepCtx closures
296308
// Clear any previous errors
297309
c.errors = c.errors[:0]
298310

@@ -342,6 +354,7 @@ func (c *Chain) RunAll() error {
342354
ctx, cancel := c.getContextAndCancel()
343355
defer cancel()
344356
c.cancel = cancel
357+
c.runCtx = ctx // share deadline with StepCtx closures
345358
c.errors = c.errors[:0]
346359
multi := NewMultiError()
347360

@@ -437,11 +450,12 @@ func (c *Chain) Unwrap() []error {
437450
// It returns a context and its cancellation function.
438451
func (c *Chain) getContextAndCancel() (context.Context, context.CancelFunc) {
439452
parentCtx := context.Background()
440-
if c.config.timeout > 0 {
441-
// Create a context with a timeout
442-
return context.WithTimeout(parentCtx, c.config.timeout)
453+
c.configMu.RLock()
454+
timeout := c.config.timeout
455+
c.configMu.RUnlock()
456+
if timeout > 0 {
457+
return context.WithTimeout(parentCtx, timeout)
443458
}
444-
// Create a cancellable context
445459
return context.WithCancel(parentCtx)
446460
}
447461

0 commit comments

Comments
 (0)