Skip to content

Commit b2e3478

Browse files
committed
Merge branch 'alloc_no_trace'
2 parents 79bc001 + 249fa0b commit b2e3478

File tree

9 files changed

+149
-64
lines changed

9 files changed

+149
-64
lines changed

builder.go

Lines changed: 14 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -12,21 +12,20 @@ type ErrorBuilder struct {
1212
errorType *Type
1313
message string
1414
cause error
15-
underlying []error
1615
mode callStackBuildMode
1716
isTransparent bool
1817
}
1918

2019
// NewErrorBuilder creates error builder from an existing error type.
21-
func NewErrorBuilder(t *Type) *ErrorBuilder {
20+
func NewErrorBuilder(t *Type) ErrorBuilder {
2221
getMode := func() callStackBuildMode {
2322
if !t.modifiers.CollectStackTrace() {
2423
return stackTraceOmit
2524
}
2625
return stackTraceCollect
2726
}
2827

29-
return &ErrorBuilder{
28+
return ErrorBuilder{
3029
errorType: t,
3130
mode: getMode(),
3231
isTransparent: t.modifiers.Transparent(),
@@ -37,7 +36,7 @@ func NewErrorBuilder(t *Type) *ErrorBuilder {
3736
// For non-errorx errors, a stack trace is collected.
3837
// Otherwise, it is inherited by default, as error wrapping is typically performed 'en passe'.
3938
// Note that even if an original error explicitly omitted the stack trace, it could be added on wrap.
40-
func (eb *ErrorBuilder) WithCause(err error) *ErrorBuilder {
39+
func (eb ErrorBuilder) WithCause(err error) ErrorBuilder {
4140
eb.cause = err
4241
if Cast(err) != nil {
4342
eb.mode = stackTraceBorrow
@@ -50,7 +49,7 @@ func (eb *ErrorBuilder) WithCause(err error) *ErrorBuilder {
5049
// Transparent wrap hides the current error type from the type checks and exposes the error type of the cause instead.
5150
// The same holds true for traits, and the dynamic properties are visible from both cause and transparent wrapper.
5251
// Note that if the cause error is non-errorx, transparency will still hold, type check against wrapper will still fail.
53-
func (eb *ErrorBuilder) Transparent() *ErrorBuilder {
52+
func (eb ErrorBuilder) Transparent() ErrorBuilder {
5453
if eb.cause == nil {
5554
panic("wrong builder usage: wrap modifier without non-nil cause")
5655
}
@@ -64,7 +63,7 @@ func (eb *ErrorBuilder) Transparent() *ErrorBuilder {
6463
// This is typically a way to handle an error received from another goroutine - say, a worker pool.
6564
// When stack traces overlap, formatting makes a conservative attempt not to repeat itself,
6665
// preserving the *original* stack trace in its entirety.
67-
func (eb *ErrorBuilder) EnhanceStackTrace() *ErrorBuilder {
66+
func (eb ErrorBuilder) EnhanceStackTrace() ErrorBuilder {
6867
if eb.cause == nil {
6968
panic("wrong builder usage: wrap modifier without non-nil cause")
7069
}
@@ -81,7 +80,7 @@ func (eb *ErrorBuilder) EnhanceStackTrace() *ErrorBuilder {
8180
// WithConditionallyFormattedMessage provides a message for an error in flexible format, to simplify its usages.
8281
// Without args, leaves the original message intact, so a message may be generated or provided externally.
8382
// With args, a formatting is performed, and it is therefore expected a format string to be constant.
84-
func (eb *ErrorBuilder) WithConditionallyFormattedMessage(message string, args ...interface{}) *ErrorBuilder {
83+
func (eb ErrorBuilder) WithConditionallyFormattedMessage(message string, args ...interface{}) ErrorBuilder {
8584
if len(args) == 0 {
8685
eb.message = message
8786
} else {
@@ -92,15 +91,15 @@ func (eb *ErrorBuilder) WithConditionallyFormattedMessage(message string, args .
9291
}
9392

9493
// Create returns an error with specified params.
95-
func (eb *ErrorBuilder) Create() *Error {
96-
return &Error{
94+
func (eb ErrorBuilder) Create() *Error {
95+
err := &Error{
9796
errorType: eb.errorType,
9897
message: eb.message,
9998
cause: eb.cause,
100-
underlying: eb.underlying,
10199
transparent: eb.isTransparent,
102100
stackTrace: eb.assembleStackTrace(),
103101
}
102+
return err
104103
}
105104

106105
type callStackBuildMode int
@@ -112,7 +111,7 @@ const (
112111
stackTraceOmit callStackBuildMode = 4
113112
)
114113

115-
func (eb *ErrorBuilder) assembleStackTrace() *stackTrace {
114+
func (eb ErrorBuilder) assembleStackTrace() *stackTrace {
116115
switch eb.mode {
117116
case stackTraceCollect:
118117
return eb.collectOriginalStackTrace()
@@ -127,19 +126,19 @@ func (eb *ErrorBuilder) assembleStackTrace() *stackTrace {
127126
}
128127
}
129128

130-
func (eb *ErrorBuilder) collectOriginalStackTrace() *stackTrace {
129+
func (eb ErrorBuilder) collectOriginalStackTrace() *stackTrace {
131130
return collectStackTrace()
132131
}
133132

134-
func (eb *ErrorBuilder) borrowStackTraceFromCause() *stackTrace {
133+
func (eb ErrorBuilder) borrowStackTraceFromCause() *stackTrace {
135134
originalStackTrace := eb.extractStackTraceFromCause(eb.cause)
136135
if originalStackTrace != nil {
137136
return originalStackTrace
138137
}
139138
return collectStackTrace()
140139
}
141140

142-
func (eb *ErrorBuilder) combineStackTraceWithCause() *stackTrace {
141+
func (eb ErrorBuilder) combineStackTraceWithCause() *stackTrace {
143142
currentStackTrace := collectStackTrace()
144143

145144
originalStackTrace := eb.extractStackTraceFromCause(eb.cause)
@@ -150,7 +149,7 @@ func (eb *ErrorBuilder) combineStackTraceWithCause() *stackTrace {
150149
return currentStackTrace
151150
}
152151

153-
func (eb *ErrorBuilder) extractStackTraceFromCause(cause error) *stackTrace {
152+
func (eb ErrorBuilder) extractStackTraceFromCause(cause error) *stackTrace {
154153
if typedCause := Cast(cause); typedCause != nil {
155154
return typedCause.stackTrace
156155
}

error.go

Lines changed: 37 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -10,13 +10,15 @@ import (
1010
// At the moment of creation, Error collects information based on context, creation modifiers and type it belongs to.
1111
// Error is mostly immutable, and distinct errors composition is achieved through wrap.
1212
type Error struct {
13-
message string
14-
errorType *Type
15-
cause error
16-
underlying []error
17-
stackTrace *stackTrace
18-
transparent bool
19-
properties map[Property]interface{}
13+
message string
14+
errorType *Type
15+
cause error
16+
stackTrace *stackTrace
17+
// properties are used both for public properties inherited through "transparent" wrapping
18+
// and for some optional per-instance information like "underlying errors"
19+
properties *propertyMap
20+
transparent bool
21+
hasUnderlying bool
2022
}
2123

2224
var _ fmt.Formatter = (*Error)(nil)
@@ -25,41 +27,36 @@ var _ fmt.Formatter = (*Error)(nil)
2527
// If an error already contained another value for the same property, it is overwritten.
2628
// It is a caller's responsibility to accumulate and update a property, if needed.
2729
// Dynamic properties is a brittle mechanism and should therefore be used with care and in a simple and robust manner.
30+
// Currently, properties are implemented as a linked list, therefore it is not safe to have many dozens of them. But couple of dozen is just ok.
2831
func (e *Error) WithProperty(key Property, value interface{}) *Error {
2932
errorCopy := *e
30-
31-
if errorCopy.properties == nil {
32-
errorCopy.properties = make(map[Property]interface{}, 1)
33-
} else {
34-
errorCopy.properties = make(map[Property]interface{}, len(e.properties) + 1)
35-
for k, v := range e.properties {
36-
errorCopy.properties[k] = v
37-
}
38-
}
39-
40-
errorCopy.properties[key] = value
33+
errorCopy.properties = errorCopy.properties.with(key, value)
4134
return &errorCopy
4235
}
4336

4437
// WithUnderlyingErrors adds multiple additional related (hidden, suppressed) errors to be used exclusively in error output.
4538
// Note that these errors make no other effect whatsoever: their traits, types, properties etc. are lost on the observer.
4639
// Consider using errorx.DecorateMany instead.
4740
func (e *Error) WithUnderlyingErrors(errs ...error) *Error {
48-
errorCopy := *e
49-
50-
newUnderying := make([]error, 0, len(e.underlying) + len(errs))
51-
newUnderying = append(newUnderying, e.underlying...)
41+
underlying := e.underlying()
42+
newUnderlying := underlying
5243

5344
for _, err := range errs {
5445
if err == nil {
5546
continue
5647
}
5748

58-
newUnderying = append(newUnderying, err)
49+
newUnderlying = append(newUnderlying, err)
5950
}
6051

61-
errorCopy.underlying = newUnderying
62-
return &errorCopy
52+
if len(newUnderlying) == len(underlying) {
53+
return e
54+
}
55+
56+
l := len(newUnderlying) // note: l > 0, because non-increased 0 length is handled above
57+
errorCopy := e.WithProperty(propertyUnderlying, newUnderlying[:l:l])
58+
errorCopy.hasUnderlying = true
59+
return errorCopy
6360
}
6461

6562
// Property extracts a dynamic property value from an error.
@@ -69,7 +66,7 @@ func (e *Error) WithUnderlyingErrors(errs ...error) *Error {
6966
func (e *Error) Property(key Property) (interface{}, bool) {
7067
cause := e
7168
for cause != nil {
72-
value, ok := cause.properties[key]
69+
value, ok := cause.properties.get(key)
7370
if ok {
7471
return value, true
7572
}
@@ -195,18 +192,29 @@ func (e *Error) messageWithUnderlyingInfo() string {
195192
}
196193

197194
func (e *Error) underlyingInfo() string {
198-
if len(e.underlying) == 0 {
195+
if !e.hasUnderlying {
199196
return ""
200197
}
201198

202-
infos := make([]string, 0, len(e.underlying))
203-
for _, err := range e.underlying {
199+
underlying := e.underlying()
200+
infos := make([]string, 0, len(underlying))
201+
for _, err := range underlying {
204202
infos = append(infos, err.Error())
205203
}
206204

207205
return fmt.Sprintf("(hidden: %s)", joinStringsIfNonEmpty(", ", infos...))
208206
}
209207

208+
func (e *Error) underlying() []error {
209+
if !e.hasUnderlying {
210+
return nil
211+
}
212+
// Note: properties are used as storage for optional "underlying errors".
213+
// Chain of cause should not be traversed here.
214+
u, _ := e.properties.get(propertyUnderlying)
215+
return u.([]error)
216+
}
217+
210218
func (e *Error) messageText() string {
211219
if e.Cause() == nil {
212220
return e.message

error_test.go

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -101,9 +101,9 @@ func TestImmutableError(t *testing.T) {
101101
require.True(t, err.errorType.IsOfType(err2.errorType))
102102
require.Equal(t, err.message, err2.message)
103103

104-
require.Len(t, err.underlying, 0)
105-
require.Len(t, err1.underlying, 1)
106-
require.Len(t, err2.underlying, 2)
104+
require.Len(t, err.underlying(), 0)
105+
require.Len(t, err1.underlying(), 1)
106+
require.Len(t, err2.underlying(), 2)
107107
})
108108
}
109109

id.go

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,13 @@
11
package errorx
22

3-
import "sync/atomic"
3+
import (
4+
"sync/atomic"
5+
)
46

5-
var internalID int64
7+
var internalID uint64
68

79
// nextInternalID creates next unique id for errorx entities.
810
// All equality comparison should take id into account, lest there be some false positive matches.
9-
func nextInternalID() int64 {
10-
return atomic.AddInt64(&internalID, 1)
11+
func nextInternalID() uint64 {
12+
return atomic.AddUint64(&internalID, 1)
1113
}

namespace.go

Lines changed: 3 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -11,16 +11,15 @@ import "fmt"
1111
//
1212
type Namespace struct {
1313
parent *Namespace
14-
id int64
14+
id uint64
1515
name string
1616
traits []Trait
1717
modifiers modifiers
1818
}
1919

2020
// NamespaceKey is a comparable descriptor of a Namespace.
2121
type NamespaceKey struct {
22-
id int64
23-
name string
22+
id uint64
2423
}
2524

2625
// NewNamespace defines a namespace with a name and, optionally, a number of inheritable traits.
@@ -51,8 +50,7 @@ func (n Namespace) NewType(typeName string, traits ...Trait) *Type {
5150
// Key returns a comparison key for namespace.
5251
func (n Namespace) Key() NamespaceKey {
5352
return NamespaceKey{
54-
id: n.id,
55-
name: n.name,
53+
id: n.id,
5654
}
5755
}
5856

property.go

Lines changed: 35 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,10 @@ import (
88
// Property value belongs to an error instance only, never inherited from a type.
99
// Property visibility is hindered by Wrap, preserved by Decorate.
1010
type Property struct {
11-
id int64
11+
*property // Property is compared by this pointer.
12+
}
13+
14+
type property struct {
1215
label string
1316
}
1417

@@ -65,13 +68,39 @@ func ExtractProperty(err error, key Property) (interface{}, bool) {
6568
}
6669

6770
var (
68-
propertyContext = RegisterProperty("ctx")
69-
propertyPayload = RegisterProperty("payload")
71+
propertyContext = RegisterProperty("ctx")
72+
propertyPayload = RegisterProperty("payload")
73+
propertyUnderlying = RegisterProperty("underlying")
7074
)
7175

7276
func newProperty(label string) Property {
73-
return Property{
74-
id: nextInternalID(),
75-
label: label,
77+
p := Property{
78+
&property{
79+
label: label,
80+
},
81+
}
82+
return p
83+
}
84+
85+
// propertyMap represents map of properties.
86+
// Compared to builtin type, it uses less allocations and reallocations on copy.
87+
// It is implemented as a simple linked list.
88+
type propertyMap struct {
89+
p Property
90+
value interface{}
91+
next *propertyMap
92+
}
93+
94+
func (pm *propertyMap) with(p Property, value interface{}) *propertyMap {
95+
return &propertyMap{p: p, value: value, next: pm}
96+
}
97+
98+
func (pm *propertyMap) get(p Property) (value interface{}, ok bool) {
99+
for pm != nil {
100+
if pm.p == p {
101+
return pm.value, true
102+
}
103+
pm = pm.next
76104
}
105+
return nil, false
77106
}

0 commit comments

Comments
 (0)